[SCM] davmail packaging branch, master, updated. 02c2b101f323ee4dc652f941a0d9441a59cfa0da
Alexandre Rossi
alexandre.rossi at gmail.com
Mon Oct 1 09:07:46 UTC 2012
The following commit has been merged in the master branch:
commit bfffebffcf7628bb03abce9df7566dd1ac81f594
Author: Alexandre Rossi <alexandre.rossi at gmail.com>
Date: Mon Oct 1 10:30:39 2012 +0200
Imported Upstream version 3.9.9-1976
diff --git a/build.xml b/build.xml
new file mode 100644
index 0000000..b3b941b
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,405 @@
+<project name="DavMail" default="dist" basedir=".">
+ <property file="user.properties"/>
+ <property name="version" value="3.9.9"/>
+
+ <path id="classpath">
+ <pathelement location="classes"/>
+ <fileset dir="lib">
+ <include name="*.jar"/>
+ </fileset>
+ </path>
+
+ <target name="clean">
+ <delete dir="target"/>
+ <delete dir="dist"/>
+ <delete file="build.log"/>
+ </target>
+
+ <condition property="is.windows">
+ <os family="windows"/>
+ </condition>
+ <condition property="is.svn">
+ <available file=".svn"/>
+ </condition>
+
+ <condition property="is.java6">
+ <equals arg1="${ant.java.version}" arg2="1.6"/>
+ </condition>
+
+ <target name="check-java6" unless="is.java6">
+ <fail message="Java 6 needed to build DavMail, current version is ${ant.java.version}, check JAVA_HOME"/>
+ </target>
+
+ <target name="svnrelease" if="is.svn">
+ <typedef resource="org/tigris/subversion/svnant/svnantlib.xml">
+ <classpath>
+ <fileset dir="svnant">
+ <include name="*.jar"/>
+ </fileset>
+ </classpath>
+ </typedef>
+ <svn svnkit="true">
+ <wcversion path="."/>
+ </svn>
+ <!-- failover value for revision -->
+ <property name="revision.max" value=""/>
+ <property name="release" value="${version}-${revision.max}"/>
+ </target>
+
+ <target name="defaultrelease" unless="is.svn">
+ <property name="release" value="${version}"/>
+ </target>
+
+ <target name="init" depends="check-java6, svnrelease, defaultrelease">
+ <echo message="Creating DavMail ${release} dist package"/>
+ <mkdir dir="target/classes"/>
+ </target>
+
+ <target name="compile" depends="init">
+ <mkdir dir="target/classes"/>
+ <javac srcdir="src/java" destdir="target/classes" source="1.5" target="1.5" debug="on" encoding="UTF-8"
+ includeantruntime="false">
+ <classpath>
+ <path refid="classpath"/>
+ </classpath>
+ </javac>
+ <copy todir="target/classes">
+ <fileset dir="src/java">
+ <include name="**/*"/>
+ <exclude name="**/*.java"/>
+ </fileset>
+ </copy>
+ </target>
+
+ <target name="dist-nsis" if="is.windows">
+ <taskdef name="nsis" classname="net.sf.nsisant.Task">
+ <classpath location="lib/nsisant-1.2.jar"/>
+ </taskdef>
+ <nsis script="davmail-setup.nsi" verbosity="4" out="build.log" noconfig="yes">
+ <define name="VERSION" value="${release}"/>
+ </nsis>
+ </target>
+
+ <target name="dist-osx">
+ <taskdef name="jarbundler"
+ classname="net.sourceforge.jarbundler.JarBundler">
+ <classpath location="lib/jarbundler-2.1.0.jar"/>
+ </taskdef>
+ <jarbundler dir="dist"
+ shortname="DavMail"
+ name="DavMail"
+ mainclass="davmail.DavGateway"
+ build="${release}"
+ icon="src/osx/tray.icns"
+ infostring="DavMail Gateway ${release}"
+ jvmversion="1.5+"
+ version="${release}"
+ vmoptions="-Dsun.net.inetaddr.ttl=60 -Xmx512m"
+ workingdirectory="$APP_PACKAGE"
+ stubfile="src/osx/davmail">
+ <jarfileset dir="dist">
+ <include name="*.jar"/>
+ </jarfileset>
+ <jarfileset dir="dist/lib">
+ <include name="*.jar"/>
+ <include name="*.jnilib"/>
+ <exclude name="swt*.jar"/>
+ <exclude name="junit-*.jar"/>
+ <exclude name="winrun4j-*.jar"/>
+ </jarfileset>
+ </jarbundler>
+ <!-- prepare hide from dock option -->
+ <replaceregexp file="dist/DavMail.app/Contents/Info.plist" match="<key>CFBundleName</key>"
+ replace="<key>LSUIElement</key><string>0</string><key>CFBundleName</key>"/>
+ <zip file="dist/DavMail-MacOSX-${release}.app.zip">
+ <zipfileset dir="dist">
+ <include name="DavMail.app/**/*"/>
+ <exclude name="DavMail.app/Contents/MacOS/davmail"/>
+ </zipfileset>
+ <zipfileset dir="dist" filemode="755">
+ <include name="DavMail.app/Contents/MacOS/davmail"/>
+ </zipfileset>
+ </zip>
+ <!--delete dir="dist/DavMail.app"/-->
+ </target>
+
+ <target name="dist-deb">
+ <taskdef resource="ant_deb_task.properties">
+ <classpath location="lib/ant-deb-0.0.1.jar"/>
+ </taskdef>
+ <desktopentry
+ toFile="dist/davmail.desktop"
+ name="DavMail"
+ comment="DavMail POP/SMTP/Caldav/LDAP Exchange Gateway"
+ exec="davmail"
+ icon="/usr/share/davmail/davmail.png"
+ categories="Network;"
+ />
+ <deb todir="dist"
+ package="davmail"
+ section="mail"
+ depends="openjdk-7-jre|openjdk-6-jre|sun-java6-jre,libswt-gtk-3-java|libswt-gtk-3.6-java|libswt-gtk-3.5-java|libswt-gtk-3.4-java">
+ <version upstream="${release}"/>
+ <maintainer email="mguessan at free.fr" name="Mickaël Guessant"/>
+ <description synopsis="DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway">
+ Ever wanted to get rid of Outlook ? DavMail is a POP/IMAP/SMTP/Caldav/Carddav/LDAP exchange
+ gateway allowing users to use any mail/calendar client (e.g. Thunderbird with Lightning or
+ Apple iCal) with an Exchange server, even from the internet or behind a firewall through
+ Outlook Web Access.
+ DavMail now includes an LDAP gateway to Exchange global address book and user personal
+ contacts to allow recipient address completion in mail compose window and full calendar
+ support with attendees free/busy display.
+ The main goal of DavMail is to provide standard compliant protocols in front of proprietary
+ Exchange. This means LDAP for global address book, SMTP to send messages, IMAP to browse
+ messages on the server in any folder, POP to retrieve inbox messages only, Caldav for
+ calendar support and Carddav for personal contacts sync.
+ Thus any standard compliant client can be used with Microsoft Exchange.
+ DavMail gateway is implemented in java and should run on any platform. Releases are tested
+ on Windows, Linux (Ubuntu) and Mac OSX. Tested successfully with the Iphone
+ (gateway running on a server).
+
+ http://davmail.sourceforge.net
+ </description>
+ <tarfileset dir="dist" prefix="usr/share/davmail">
+ <include name="lib/*.jar"/>
+ <include name="*.jar"/>
+ <!-- exclude swt jars from debian package -->
+ <exclude name="lib/swt*.jar"/>
+ <exclude name="lib/libgrowl-*.jar"/>
+ </tarfileset>
+ <tarfileset dir="src/bin" prefix="usr/bin" filemode="755">
+ <include name="davmail"/>
+ </tarfileset>
+ <tarfileset dir="dist" prefix="usr/share/davmail">
+ <include name="davmail.png"/>
+ </tarfileset>
+ <tarfileset dir="dist" prefix="usr/share/applications">
+ <include name="davmail.desktop"/>
+ </tarfileset>
+ </deb>
+ </target>
+
+ <target name='dist-rpm'>
+ <taskdef name='redline' classname='org.freecompany.redline.ant.RedlineTask'>
+ <classpath location="lib/redline-1.1.9.jar"/>
+ </taskdef>
+
+ <redline name='davmail'
+ summary='DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway'
+ version='${release}'
+ release='1'
+ license='GPL'
+ group='Applications/Internet'
+ sourcePackage='davmail-${release}.src.rpm'
+ description='Ever wanted to get rid of Outlook ? DavMail is a POP/IMAP/SMTP/Caldav/Carddav/LDAP exchange
+ gateway allowing users to use any mail/calendar client (e.g. Thunderbird with Lightning or
+ Apple iCal) with an Exchange server, even from the internet or behind a firewall through
+ Outlook Web Access.
+ DavMail now includes an LDAP gateway to Exchange global address book and user personal
+ contacts to allow recipient address completion in mail compose window and full calendar
+ support with attendees free/busy display.
+ The main goal of DavMail is to provide standard compliant protocols in front of proprietary
+ Exchange. This means LDAP for global address book, SMTP to send messages, IMAP to browse
+ messages on the server in any folder, POP to retrieve inbox messages only, Caldav for
+ calendar support and Carddav for personal contacts sync.
+ Thus any standard compliant client can be used with Microsoft Exchange.
+ DavMail gateway is implemented in java and should run on any platform. Releases are tested
+ on Windows, Linux (Ubuntu) and Mac OSX. Tested successfully with the Iphone
+ (gateway running on a server).'
+ url='http://davmail.sourceforge.net'
+ vendor='Mickael Guessant'
+ destination='dist'>
+ <depends name="jre" version="1.6"/>
+ <zipfileset dir="dist" prefix="usr/share/davmail">
+ <include name="lib/*.jar"/>
+ <include name="*.jar"/>
+ <!-- exclude swt jars from package -->
+ <exclude name="lib/swt*.jar"/>
+ <exclude name="lib/libgrowl-*.jar"/>
+ </zipfileset>
+ <zipfileset dir="src/bin" prefix="usr/bin" filemode="755">
+ <include name="davmail"/>
+ </zipfileset>
+ <zipfileset dir="dist" prefix="usr/share/davmail">
+ <include name="davmail.png"/>
+ </zipfileset>
+ <zipfileset dir="dist" prefix="usr/share/applications">
+ <include name="davmail.desktop"/>
+ </zipfileset>
+ </redline>
+
+ </target>
+
+
+ <target name="dist" depends="compile">
+ <property name="release-name" value="${release}-trunk"/>
+ <delete dir="dist"/>
+ <mkdir dir="dist"/>
+ <echo file="dist/version.txt" message="${release}"/>
+ <jar basedir="target/classes" destfile="dist/davmail.jar">
+ <manifest>
+ <section name="davmail/">
+ <attribute name="Implementation-Title" value="DavMail Gateway"/>
+ <attribute name="Implementation-Version" value="${release-name}"/>
+ <attribute name="Implementation-Vendor" value="Mickael Guessant"/>
+ </section>
+ </manifest>
+ </jar>
+ <copy todir="dist/lib">
+ <fileset dir="lib">
+ <include name="*.jar"/>
+ <include name="*.jnilib"/>
+ <exclude name="nsisant*.jar"/>
+ <exclude name="jsmoothgen-ant-*.jar"/>
+ <exclude name="jarbundler-*.jar"/>
+ <exclude name="servlet-api.jar"/>
+ <exclude name="ant-deb-*.jar"/>
+ <exclude name="redline-*.jar"/>
+ </fileset>
+ </copy>
+ <copy file="src/java/tray48.png" tofile="dist/davmail.png"/>
+ <copy file="davmail.sh" todir="dist"/>
+ <taskdef name="jsmoothgen"
+ classname="net.charabia.jsmoothgen.ant.JSmoothGen"
+ classpathref="classpath"/>
+ <jsmoothgen project="davmail.jsmooth" skeletonroot="src/jsmooth/skeletons"/>
+ <jsmoothgen project="davmailconsole.jsmooth" skeletonroot="src/jsmooth/skeletons"/>
+ <!-- use WinRun4J to generate DavMail service -->
+ <copy file="src/winrun4j/davmailservice.exe" todir="dist"/>
+ <jsmoothgen project="davmail64.jsmooth" skeletonroot="src/jsmooth/skeletons"/>
+ <zip file="dist/davmail-${release-name}.zip">
+ <fileset dir="dist">
+ <include name="lib/*.jar"/>
+ <include name="*.jar"/>
+ <include name="davmail.desktop"/>
+ <include name="davmail.sh"/>
+ <!-- exclude swt jars from platform independent package -->
+ <exclude name="lib/swt*.jar"/>
+ <exclude name="lib/libgrowl-*.jar"/>
+ <exclude name="lib/junit-*.jar"/>
+ <exclude name="lib/winrun4j-*.jar"/>
+ </fileset>
+ </zip>
+ <copy todir="dist/web">
+ <fileset dir="src/web"/>
+ </copy>
+ <copy todir="dist/web/WEB-INF/lib">
+ <fileset dir="dist">
+ <include name="*.jar"/>
+ </fileset>
+ <fileset dir="dist/lib">
+ <include name="*.jar"/>
+ <exclude name="nsisant*.jar"/>
+ <exclude name="jsmoothgen-ant*.jar"/>
+ <exclude name="swt*.jar"/>
+ <exclude name="lib/libgrowl-*.jar"/>
+ </fileset>
+ </copy>
+ <jar destfile="dist/davmail-${release-name}.war">
+ <fileset dir="dist/web"/>
+ </jar>
+ <tar tarfile="dist/davmail-linux-x86-${release-name}.tgz" compression="gzip">
+ <tarfileset prefix="davmail-linux-x86-${release}" dir="dist" filemode="755">
+ <include name="davmail.sh"/>
+ </tarfileset>
+ <tarfileset prefix="davmail-linux-x86-${release}" dir="dist">
+ <include name="davmail.desktop"/>
+ <include name="lib/*.jar"/>
+ <include name="*.jar"/>
+ <include name="lib/swt-*-gtk-linux-x86.jar"/>
+ <exclude name="lib/swt-*-gtk-linux-x86_64.jar"/>
+ <exclude name="lib/swt-*-carbon-macosx.jar"/>
+ <exclude name="lib/swt-*-win32-*.jar"/>
+ <exclude name="lib/libgrowl-*.jar"/>
+ </tarfileset>
+ </tar>
+ <tar tarfile="dist/davmail-linux-x86_64-${release-name}.tgz" compression="gzip">
+ <tarfileset prefix="davmail-linux-x86_64-${release}" dir="dist" filemode="755">
+ <include name="davmail.sh"/>
+ </tarfileset>
+ <tarfileset prefix="davmail-linux-x86_64-${release}" dir="dist">
+ <include name="davmail.desktop"/>
+ <include name="lib/*.jar"/>
+ <include name="*.jar"/>
+ <include name="lib/swt-*-gtk-linux-x86_64.jar"/>
+ <exclude name="lib/swt-*-gtk-linux-x86.jar"/>
+ <exclude name="lib/swt-*-carbon-macosx.jar"/>
+ <exclude name="lib/swt-*-win32-*.jar"/>
+ <exclude name="lib/libgrowl-*.jar"/>
+ </tarfileset>
+ </tar>
+ <zip file="dist/davmail-${release-name}-windows-noinstall.zip">
+ <fileset dir="dist">
+ <include name="lib/*.jar"/>
+ <include name="*.jar"/>
+ <include name="davmail*.exe"/>
+ <exclude name="lib/swt-*-gtk-*.jar"/>
+ <exclude name="lib/libgrowl-*.jar"/>
+ <exclude name="lib/junit-*.jar"/>
+ </fileset>
+ </zip>
+ <antcall target="dist-nsis"/>
+ <antcall target="dist-deb"/>
+ <antcall target="dist-rpm"/>
+ <antcall target="dist-osx"/>
+ <!-- source package -->
+ <tar tarfile="dist/davmail-src-${release-name}.tgz" compression="gzip" longfile="gnu">
+ <tarfileset prefix="davmail-src-${release-name}" dir=".">
+ <include name="**/*"/>
+ <exclude name="build.log"/>
+ <exclude name="dist/**"/>
+ <exclude name="target/**"/>
+ <exclude name="archive/**"/>
+ </tarfileset>
+ </tar>
+ </target>
+
+ <target name='fix-site'>
+ <!-- fix site title generated by mvn site -->
+ <replaceregexp match="Maven - "
+ replace="">
+ <fileset dir="target/site"/>
+ </replaceregexp>
+ <replaceregexp match="<a href="http://sourceforge.net/projects/davmail" id="bannerRight">"
+ replace="<script type="text/javascript"><!-- ${line.separator}var pkBaseURL = (("https:" == document.location.protocol) ? "https://sourceforge.net/apps/piwik/davmail/" : "http://sourceforge.net/apps/piwik/davmail/"); ${line.separator}document.write(unescape("%3Cscript src='" + pkBaseURL + "piwik.js' type='text/javascript'%3E%3C/script%3E")); ${line.separator}//--></script>${line.separator}<script type="text/javascript">${line.separator}try {var piwikTracker = Piwik.getTracker(pkBaseURL + "piwik.php", 1);piwikTracker.trackPageView();piwikTracker.enableLinkTracking();}catch(err){}${line.separator}</script><noscript><p><img src="http://sourceforge.net/apps/piwik/davmail/piwik.php?idsite=1" style="border:0" alt=""/></p></noscript>">
+ <fileset dir="target/site"/>
+ </replaceregexp>
+ </target>
+
+ <target name='upload-site' depends="fix-site">
+ <scp todir="${username},davmail at web.sourceforge.net:htdocs"
+ keyfile="${keyfile}" passphrase="${passphrase}" verbose="true" trust="true">
+ <fileset dir="target/site"/>
+ </scp>
+ </target>
+
+ <target name="upload-release" depends="release">
+ <mkdir dir="dist/${version}"/>
+ <move todir="dist/${version}">
+ <fileset dir="dist">
+ <include name="*${version}-*"/>
+ </fileset>
+ </move>
+
+ <scp todir="${username},davmail at frs.sourceforge.net:/home/frs/project/d/da/davmail/davmail"
+ keyfile="${keyfile}" passphrase="${passphrase}" verbose="true" trust="true">
+ <fileset dir="dist">
+ <include name="${version}/*"/>
+ </fileset>
+ </scp>
+ </target>
+
+ <target name="upload-version">
+ <scp todir="${username},davmail at web.sourceforge.net:htdocs"
+ keyfile="${keyfile}" passphrase="${passphrase}" verbose="true">
+ <fileset dir="dist">
+ <include name="version.txt"/>
+ </fileset>
+ </scp>
+ </target>
+
+ <target name="release" depends="init">
+ <property name="release-name" value="${release}"/>
+ <antcall target="dist"/>
+ </target>
+</project>
diff --git a/davmail-setup.nsi b/davmail-setup.nsi
new file mode 100644
index 0000000..c7d2dd4
--- /dev/null
+++ b/davmail-setup.nsi
@@ -0,0 +1,208 @@
+; Script generated by the HM NIS Edit Script Wizard.
+
+; HM NIS Edit Wizard helper defines
+!define PRODUCT_NAME "DavMail"
+!define PRODUCT_VERSION "${VERSION}"
+!define PRODUCT_PUBLISHER "Mickaël Guessant"
+!define PRODUCT_WEB_SITE "http://sourceforge.net/projects/davmail"
+!define PRODUCT_DIR_REGKEY "Software\Microsoft\Windows\CurrentVersion\App Paths\davmail.exe"
+!define PRODUCT_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}"
+!define PRODUCT_UNINST_ROOT_KEY "HKLM"
+
+; MUI 1.67 compatible ------
+!include "MUI.nsh"
+
+; MUI Settings
+!define MUI_ABORTWARNING
+!define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\modern-install.ico"
+!define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\modern-uninstall.ico"
+
+; Language Selection Dialog Settings
+!define MUI_LANGDLL_REGISTRY_ROOT "${PRODUCT_UNINST_ROOT_KEY}"
+!define MUI_LANGDLL_REGISTRY_KEY "${PRODUCT_UNINST_KEY}"
+!define MUI_LANGDLL_REGISTRY_VALUENAME "NSIS:Language"
+
+; Welcome page
+!insertmacro MUI_PAGE_WELCOME
+; License page
+!insertmacro MUI_PAGE_LICENSE "src\license.txt"
+; Directory page
+!insertmacro MUI_PAGE_DIRECTORY
+; Instfiles page
+!insertmacro MUI_PAGE_INSTFILES
+; Finish page
+!define MUI_FINISHPAGE_RUN "$INSTDIR\davmail.exe"
+!insertmacro MUI_PAGE_FINISH
+
+; Uninstaller pages
+!insertmacro MUI_UNPAGE_INSTFILES
+
+; Language files
+!insertmacro MUI_LANGUAGE "English"
+!insertmacro MUI_LANGUAGE "French"
+
+; MUI end ------
+
+Name "${PRODUCT_NAME} ${PRODUCT_VERSION}"
+OutFile "dist\davmail-${PRODUCT_VERSION}-setup.exe"
+InstallDir "$PROGRAMFILES\DavMail"
+InstallDirRegKey HKLM "${PRODUCT_DIR_REGKEY}" ""
+ShowInstDetails show
+ShowUnInstDetails show
+
+Function .onInit
+ !insertmacro MUI_LANGDLL_DISPLAY
+FunctionEnd
+
+Section
+Push $5
+loop:
+ push "davmail.exe"
+ processwork::existsprocess
+ pop $5
+ IntCmp $5 0 no_quest
+ MessageBox MB_RETRYCANCEL|MB_ICONSTOP 'DavMail must be closed during this installation.$\r$\n Close DavMail now, or press "Retry" to automatically close DavMail and continue or press "Cancel" to cancel the installation entirely.' IDCANCEL BailOut
+ push "davmail.exe"
+ processwork::KillProcess
+ Sleep 2000
+Goto loop
+
+BailOut:
+ Abort
+
+no_quest:
+SectionEnd
+
+Section "MainSection" SEC01
+ SetOutPath "$INSTDIR"
+ SetOverwrite try
+ File "dist\davmail.exe"
+ CreateDirectory "$SMPROGRAMS\DavMail"
+ CreateShortCut "$SMPROGRAMS\DavMail\DavMail.lnk" "$INSTDIR\davmail.exe"
+ CreateShortCut "$SMPROGRAMS\DavMail\DavMail Console.lnk" "$INSTDIR\davmailconsole.exe"
+ CreateShortCut "$DESKTOP\DavMail.lnk" "$INSTDIR\davmail.exe"
+ File "dist\davmail.jar"
+ File "dist\davmailconsole.exe"
+ File "dist\davmailservice.exe"
+ File "dist\davmail64.exe"
+ SetOutPath "$INSTDIR\lib"
+ File "dist\lib\activation-1.1.1.jar"
+ File "dist\lib\commons-codec-1.3.jar"
+ File "dist\lib\commons-collections-3.1.jar"
+ File "dist\lib\commons-httpclient-3.1.jar"
+ File "dist\lib\commons-logging-1.0.4.jar"
+ File "dist\lib\htmlcleaner-2.1.jar"
+ File "dist\lib\jackrabbit-webdav-1.4.jar"
+ File "dist\lib\jcharset-1.3.jar"
+ File "dist\lib\jcifs-1.3.14.jar"
+ File "dist\lib\jdom-1.0.jar"
+ File "dist\lib\log4j-1.2.16.jar"
+ File "dist\lib\mail-1.4.3.jar"
+ File "dist\lib\slf4j-api-1.3.1.jar"
+ File "dist\lib\slf4j-log4j12-1.3.1.jar"
+ File "dist\lib\stax-api-1.0.1.jar"
+ File "dist\lib\stax2-api-3.1.1.jar"
+ File "dist\lib\swt-3.7-win32-x86.jar"
+ File "dist\lib\swt-3.7-win32-x86_64.jar"
+ File "dist\lib\winrun4j-0.4.4.jar"
+ File "dist\lib\woodstox-core-asl-4.1.2.jar"
+ File "dist\lib\xercesImpl-2.8.1.jar"
+
+ WriteRegStr HKEY_CURRENT_USER "Software\Microsoft\Windows\CurrentVersion\Run" "DavMail" "$INSTDIR\davmail.exe"
+SectionEnd
+
+Section -AdditionalIcons
+ SetOutPath $INSTDIR
+ WriteIniStr "$INSTDIR\${PRODUCT_NAME}.url" "InternetShortcut" "URL" "${PRODUCT_WEB_SITE}"
+ CreateShortCut "$SMPROGRAMS\DavMail\Website.lnk" "$INSTDIR\${PRODUCT_NAME}.url"
+ CreateShortCut "$SMPROGRAMS\DavMail\Uninstall.lnk" "$INSTDIR\uninst.exe"
+SectionEnd
+
+Section -Post
+ WriteUninstaller "$INSTDIR\uninst.exe"
+ WriteRegStr HKLM "${PRODUCT_DIR_REGKEY}" "" "$INSTDIR\davmail.exe"
+ WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayName" "$(^Name)"
+ WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "UninstallString" "$INSTDIR\uninst.exe"
+ WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayIcon" "$INSTDIR\davmail.exe"
+ WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayVersion" "${PRODUCT_VERSION}"
+ WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}"
+ WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "Publisher" "${PRODUCT_PUBLISHER}"
+SectionEnd
+
+
+Function un.onUninstSuccess
+ HideWindow
+ MessageBox MB_ICONINFORMATION|MB_OK "$(^Name) has been removed from your system."
+FunctionEnd
+
+Function un.onInit
+!insertmacro MUI_UNGETLANGUAGE
+ MessageBox MB_ICONQUESTION|MB_YESNO|MB_DEFBUTTON2 "Are you sure you want to completely remove $(^Name) and all of its components?" IDYES +2
+ Abort
+FunctionEnd
+
+Section Uninstall
+Push $5
+loop:
+ push "davmail.exe"
+ processwork::existsprocess
+ pop $5
+ IntCmp $5 0 no_quest
+ MessageBox MB_RETRYCANCEL|MB_ICONSTOP 'DavMail must be closed during this installation.$\r$\n Close DavMail now, or press "Retry" to automatically close DavMail and continue or press "Cancel" to cancel the installation entirely.' IDCANCEL BailOut
+ push "davmail.exe"
+ processwork::KillProcess
+ Sleep 2000
+Goto loop
+
+BailOut:
+ Abort
+
+no_quest:
+ Delete "$INSTDIR\${PRODUCT_NAME}.url"
+ Delete "$INSTDIR\uninst.exe"
+
+ Delete "$INSTDIR\lib\activation-1.1.1.jar"
+ Delete "$INSTDIR\lib\commons-codec-1.3.jar"
+ Delete "$INSTDIR\lib\commons-collections-3.1.jar"
+ Delete "$INSTDIR\lib\commons-httpclient-3.1.jar"
+ Delete "$INSTDIR\lib\commons-logging-1.0.4.jar"
+ Delete "$INSTDIR\lib\htmlcleaner-2.1.jar"
+ Delete "$INSTDIR\lib\jackrabbit-webdav-1.4.jar"
+ Delete "$INSTDIR\lib\jcharset-1.3.jar"
+ Delete "$INSTDIR\lib\jcifs-1.3.14.jar"
+ Delete "$INSTDIR\lib\jdom-1.0.jar"
+ Delete "$INSTDIR\lib\log4j-1.2.16.jar"
+ Delete "$INSTDIR\lib\mail-1.4.3.jar"
+ Delete "$INSTDIR\lib\slf4j-api-1.3.1.jar"
+ Delete "$INSTDIR\lib\slf4j-log4j12-1.3.1.jar"
+ Delete "$INSTDIR\lib\stax-api-1.0.1.jar"
+ Delete "$INSTDIR\lib\stax2-api-3.1.1.jar"
+ Delete "$INSTDIR\lib\swt-3.7-win32-x86.jar"
+ Delete "$INSTDIR\lib\swt-3.7-win32-x86_64.jar"
+ Delete "$INSTDIR\lib\winrun4j-0.4.4.jar"
+ Delete "$INSTDIR\lib\woodstox-core-asl-4.1.2.jar"
+ Delete "$INSTDIR\lib\xercesImpl-2.8.1.jar"
+
+ Delete "$INSTDIR\davmail64.exe"
+ Delete "$INSTDIR\davmailservice.exe"
+ Delete "$INSTDIR\davmailconsole.exe"
+ Delete "$INSTDIR\davmail.log"
+ Delete "$INSTDIR\davmail.jar"
+ Delete "$INSTDIR\davmail.exe"
+
+ Delete "$SMPROGRAMS\DavMail\Uninstall.lnk"
+ Delete "$SMPROGRAMS\DavMail\Website.lnk"
+ Delete "$DESKTOP\DavMail.lnk"
+ Delete "$SMPROGRAMS\DavMail\DavMail.lnk"
+ Delete "$SMPROGRAMS\DavMail\DavMail Console.lnk"
+
+ RMDir "$SMPROGRAMS\DavMail"
+ RMDir "$INSTDIR\lib"
+ RMDir "$INSTDIR"
+
+ DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\Run" "DavMail"
+
+ DeleteRegKey ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}"
+ DeleteRegKey HKLM "${PRODUCT_DIR_REGKEY}"
+ SetAutoClose true
+SectionEnd
\ No newline at end of file
diff --git a/davmail.desktop b/davmail.desktop
new file mode 100644
index 0000000..2bf824f
--- /dev/null
+++ b/davmail.desktop
@@ -0,0 +1,9 @@
+[Desktop Entry]
+Version=1.0
+Encoding=UTF-8
+Name=DavMail
+Type=Application
+Terminal=false
+Exec=/usr/bin/java -cp davmail.jar:lib/* davmail.DavGateway
+Path=.
+Comment=DavMail POP/SMTP Webdav Exchange Gateway
diff --git a/davmail.jsmooth b/davmail.jsmooth
new file mode 100644
index 0000000..ebb7fd2
--- /dev/null
+++ b/davmail.jsmooth
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<jsmoothproject>
+<JVMSearchPath>registry</JVMSearchPath>
+<JVMSearchPath>javahome</JVMSearchPath>
+<JVMSearchPath>jrepath</JVMSearchPath>
+<JVMSearchPath>jdkpath</JVMSearchPath>
+<JVMSearchPath>exepath</JVMSearchPath>
+<JVMSearchPath>jview</JVMSearchPath>
+<arguments></arguments>
+<classPath>dist/davmail.jar</classPath>
+<classPath>dist/lib/activation-1.1.1.jar</classPath>
+<classPath>dist/lib/commons-codec-1.3.jar</classPath>
+<classPath>dist/lib/commons-collections-3.1.jar</classPath>
+<classPath>dist/lib/commons-httpclient-3.1.jar</classPath>
+<classPath>dist/lib/commons-logging-1.0.4.jar</classPath>
+<classPath>dist/lib/htmlcleaner-2.1.jar</classPath>
+<classPath>dist/lib/jackrabbit-webdav-1.4.jar</classPath>
+<classPath>dist/lib/jcharset-1.3.jar</classPath>
+<classPath>dist/lib/jcifs-1.3.14.jar</classPath>
+<classPath>dist/lib/jdom-1.0.jar</classPath>
+<classPath>dist/lib/log4j-1.2.16.jar</classPath>
+<classPath>dist/lib/mail-1.4.3.jar</classPath>
+<classPath>dist/lib/slf4j-api-1.3.1.jar</classPath>
+<classPath>dist/lib/slf4j-log4j12-1.3.1.jar</classPath>
+<classPath>dist/lib/stax-api-1.0.1.jar</classPath>
+<classPath>dist/lib/stax2-api-3.1.1.jar</classPath>
+<classPath>dist/lib/swt-3.7-win32-x86.jar</classPath>
+<classPath>dist/lib/woodstox-core-asl-4.1.2.jar</classPath>
+<classPath>dist/lib/xercesImpl-2.8.1.jar</classPath>
+<embeddedJar>false</embeddedJar>
+<executableName>dist/davmail.exe</executableName>
+<iconLocation>src/java/tray48.png</iconLocation>
+<initialMemoryHeap>-1</initialMemoryHeap>
+<javaProperties>
+<name>sun.net.inetaddr.ttl</name>
+<value>60</value>
+</javaProperties>
+<mainClassName>davmail.DavGateway</mainClassName>
+<maximumMemoryHeap>268435456</maximumMemoryHeap>
+<maximumVersion></maximumVersion>
+<minimumVersion>1.6</minimumVersion>
+<skeletonName>Autodownload Wrapper</skeletonName>
+<skeletonProperties>
+<key>Message</key>
+<value>Java has not been found on your computer. Do you want to download it?</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>DownloadURL</key>
+<value>http://java.sun.com/update/1.6.0/jinstall-6-windows-i586.cab</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>SingleProcess</key>
+<value>1</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>SingleInstance</key>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>JniSmooth</key>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>Debug</key>
+<value>0</value>
+</skeletonProperties>
+</jsmoothproject>
diff --git a/davmail.sh b/davmail.sh
new file mode 100644
index 0000000..5f7ee6d
--- /dev/null
+++ b/davmail.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+# Ubuntu setup instructions :
+# install java :
+# sudo apt-get install sun-java6-bin
+# launch davmail :
+BASE=`dirname $0`
+for i in $BASE/lib/*; do export CLASSPATH=$CLASSPATH:$i; done
+java -cp $BASE/davmail.jar:$CLASSPATH davmail.DavGateway $1
diff --git a/davmail64.jsmooth b/davmail64.jsmooth
new file mode 100644
index 0000000..8addd7c
--- /dev/null
+++ b/davmail64.jsmooth
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<jsmoothproject>
+<JVMSearchPath>registry</JVMSearchPath>
+<JVMSearchPath>javahome</JVMSearchPath>
+<JVMSearchPath>jrepath</JVMSearchPath>
+<JVMSearchPath>jdkpath</JVMSearchPath>
+<JVMSearchPath>exepath</JVMSearchPath>
+<JVMSearchPath>jview</JVMSearchPath>
+<arguments></arguments>
+<classPath>dist\davmail.jar</classPath>
+<classPath>dist\lib\activation-1.1.1.jar</classPath>
+<classPath>dist\lib\commons-codec-1.3.jar</classPath>
+<classPath>dist\lib\commons-collections-3.1.jar</classPath>
+<classPath>dist\lib\commons-httpclient-3.1.jar</classPath>
+<classPath>dist\lib\commons-logging-1.0.4.jar</classPath>
+<classPath>dist\lib\htmlcleaner-2.1.jar</classPath>
+<classPath>dist\lib\jackrabbit-webdav-1.4.jar</classPath>
+<classPath>dist\lib\jcharset-1.3.jar</classPath>
+<classPath>dist\lib\jcifs-1.3.14.jar</classPath>
+<classPath>dist\lib\jdom-1.0.jar</classPath>
+<classPath>dist\lib\log4j-1.2.16.jar</classPath>
+<classPath>dist\lib\mail-1.4.3.jar</classPath>
+<classPath>dist\lib\slf4j-api-1.3.1.jar</classPath>
+<classPath>dist\lib\slf4j-log4j12-1.3.1.jar</classPath>
+<classPath>dist\lib\stax-api-1.0.1.jar</classPath>
+<classPath>dist\lib\stax2-api-3.1.1.jar</classPath>
+<classPath>dist\lib\swt-3.7-win32-x86_64.jar</classPath>
+<classPath>dist\lib\woodstox-core-asl-4.1.2.jar</classPath>
+<classPath>dist\lib\xercesImpl-2.8.1.jar</classPath>
+<embeddedJar>false</embeddedJar>
+<executableName>dist\davmail64.exe</executableName>
+<iconLocation>src\java\tray48.png</iconLocation>
+<initialMemoryHeap>-1</initialMemoryHeap>
+<javaProperties>
+<name>sun.net.inetaddr.ttl</name>
+<value>60</value>
+</javaProperties>
+<mainClassName>davmail.DavGateway</mainClassName>
+<maximumMemoryHeap>268435456</maximumMemoryHeap>
+<maximumVersion></maximumVersion>
+<minimumVersion>1.6</minimumVersion>
+<skeletonName>Windowed Wrapper x64</skeletonName>
+<skeletonProperties>
+<key>Message</key>
+<value>Java has not been found on your computer. Do you want to download it?</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>URL</key>
+<value>http://www.java.com</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>SingleProcess</key>
+<value>1</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>SingleInstance</key>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>JniSmooth</key>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>Debug</key>
+<value>0</value>
+</skeletonProperties>
+</jsmoothproject>
diff --git a/davmailconsole.jsmooth b/davmailconsole.jsmooth
new file mode 100644
index 0000000..6b11532
--- /dev/null
+++ b/davmailconsole.jsmooth
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<jsmoothproject>
+<JVMSearchPath>registry</JVMSearchPath>
+<JVMSearchPath>javahome</JVMSearchPath>
+<JVMSearchPath>jrepath</JVMSearchPath>
+<JVMSearchPath>jdkpath</JVMSearchPath>
+<JVMSearchPath>exepath</JVMSearchPath>
+<JVMSearchPath>jview</JVMSearchPath>
+<arguments></arguments>
+<classPath>dist/davmail.jar</classPath>
+<classPath>dist/lib/activation-1.1.1.jar</classPath>
+<classPath>dist/lib/commons-codec-1.3.jar</classPath>
+<classPath>dist/lib/commons-collections-3.1.jar</classPath>
+<classPath>dist/lib/commons-httpclient-3.1.jar</classPath>
+<classPath>dist/lib/commons-logging-1.0.4.jar</classPath>
+<classPath>dist/lib/htmlcleaner-2.1.jar</classPath>
+<classPath>dist/lib/jackrabbit-webdav-1.4.jar</classPath>
+<classPath>dist/lib/jcharset-1.3.jar</classPath>
+<classPath>dist/lib/jcifs-1.3.14.jar</classPath>
+<classPath>dist/lib/jdom-1.0.jar</classPath>
+<classPath>dist/lib/log4j-1.2.16.jar</classPath>
+<classPath>dist/lib/mail-1.4.3.jar</classPath>
+<classPath>dist/lib/slf4j-api-1.3.1.jar</classPath>
+<classPath>dist/lib/slf4j-log4j12-1.3.1.jar</classPath>
+<classPath>dist/lib/stax-api-1.0.1.jar</classPath>
+<classPath>dist/lib/stax2-api-3.1.1.jar</classPath>
+<classPath>dist/lib/swt-3.7-win32-x86.jar</classPath>
+<classPath>dist/lib/woodstox-core-asl-4.1.2.jar</classPath>
+<classPath>dist/lib/xercesImpl-2.8.1.jar</classPath>
+<embeddedJar>false</embeddedJar>
+<executableName>dist/davmailconsole.exe</executableName>
+<iconLocation>src/java/tray32.png</iconLocation>
+<initialMemoryHeap>-1</initialMemoryHeap>
+<javaProperties>
+<name>sun.net.inetaddr.ttl</name>
+<value>60</value>
+</javaProperties>
+<mainClassName>davmail.DavGateway</mainClassName>
+<maximumMemoryHeap>268435456</maximumMemoryHeap>
+<maximumVersion></maximumVersion>
+<minimumVersion>1.5</minimumVersion>
+<skeletonName>Console Wrapper</skeletonName>
+<skeletonProperties>
+<key>Message</key>
+<value>Java has not been found on your computer. Do you want to download it?</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>PressKey</key>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>Debug</key>
+<value>0</value>
+</skeletonProperties>
+</jsmoothproject>
diff --git a/davmailservice.jsmooth b/davmailservice.jsmooth
new file mode 100644
index 0000000..bd78ac7
--- /dev/null
+++ b/davmailservice.jsmooth
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<jsmoothproject>
+<JVMSearchPath>registry</JVMSearchPath>
+<JVMSearchPath>javahome</JVMSearchPath>
+<JVMSearchPath>jrepath</JVMSearchPath>
+<JVMSearchPath>jdkpath</JVMSearchPath>
+<JVMSearchPath>exepath</JVMSearchPath>
+<JVMSearchPath>jview</JVMSearchPath>
+<arguments>davmail.properties</arguments>
+<classPath>dist/davmail.jar</classPath>
+<classPath>dist/lib/activation-1.1.1.jar</classPath>
+<classPath>dist/lib/commons-codec-1.3.jar</classPath>
+<classPath>dist/lib/commons-collections-3.1.jar</classPath>
+<classPath>dist/lib/commons-httpclient-3.1.jar</classPath>
+<classPath>dist/lib/commons-logging-1.0.4.jar</classPath>
+<classPath>dist/lib/htmlcleaner-2.1.jar</classPath>
+<classPath>dist/lib/jackrabbit-webdav-1.4.jar</classPath>
+<classPath>dist/lib/jcharset-1.3.jar</classPath>
+<classPath>dist/lib/jcifs-1.3.14.jar</classPath>
+<classPath>dist/lib/jdom-1.0.jar</classPath>
+<classPath>dist/lib/log4j-1.2.16.jar</classPath>
+<classPath>dist/lib/mail-1.4.3.jar</classPath>
+<classPath>dist/lib/slf4j-api-1.3.1.jar</classPath>
+<classPath>dist/lib/slf4j-log4j12-1.3.1.jar</classPath>
+<classPath>dist/lib/stax-api-1.0.1.jar</classPath>
+<classPath>dist/lib/stax2-api-3.1.1.jar</classPath>
+<classPath>dist/lib/swt-3.7-win32-x86.jar</classPath>
+<classPath>dist/lib/woodstox-core-asl-4.1.2.jar</classPath>
+<classPath>dist/lib/xercesImpl-2.8.1.jar</classPath>
+<currentDirectory>${EXECUTABLEPATH}</currentDirectory>
+<embeddedJar>false</embeddedJar>
+<executableName>dist/davmailservice.exe</executableName>
+<iconLocation>src/java/tray32.png</iconLocation>
+<initialMemoryHeap>-1</initialMemoryHeap>
+<javaProperties>
+<name>sun.net.inetaddr.ttl</name>
+<value>60</value>
+<name>-Xrs</name>
+<value></value>
+</javaProperties>
+<mainClassName>davmail.DavGateway</mainClassName>
+<maximumMemoryHeap>268435456</maximumMemoryHeap>
+<maximumVersion></maximumVersion>
+<minimumVersion>1.6</minimumVersion>
+<skeletonName>WinService Wrapper</skeletonName>
+<skeletonProperties>
+<key>ServiceName</key>
+<value>DavMail</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>ServiceDisplayName</key>
+<value>DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>Message</key>
+<value>Java has not been found on your computer. Do you want to download it?</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>Autostart</key>
+<value>1</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>Logfile</key>
+<value>davmailservice.log</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>Interactive</key>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>JniSmooth</key>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>Debug</key>
+<value>0</value>
+</skeletonProperties>
+</jsmoothproject>
diff --git a/debian/ant.properties b/debian/ant.properties
deleted file mode 100644
index 163300d..0000000
--- a/debian/ant.properties
+++ /dev/null
@@ -1,15 +0,0 @@
-# Build flags
-windows-dist = false
-installdir = /usr/share/java
-
-# Deps
-commons-codec = /usr/share/java/commons-codec.jar
-servlet-api = /usr/share/java/servlet-api.jar
-commons-httpclient = /usr/share/java/commons-httpclient.jar
-htmlcleaner = /usr/share/java/htmlcleaner.jar
-gnumail = /usr/share/java/gnumail.jar
-jcifs = /usr/share/java/jcifs.jar
-jackrabbit-webdav = /usr/share/java/jackrabbit-webdav.jar
-woodstox-core = /usr/share/java/woodstox-core-lgpl.jar
-stax2-api = /usr/share/java/stax2-api.jar
-swt = /usr/share/java/swt.jar
diff --git a/debian/changelog b/debian/changelog
deleted file mode 100644
index bf11f07..0000000
--- a/debian/changelog
+++ /dev/null
@@ -1,5 +0,0 @@
-davmail (3.9.9-1976-1~pre+1) unstable; urgency=low
-
- * Initial release (Closes: #569668).
-
- -- Alexandre Rossi <alexandre.rossi at gmail.com> Thu, 26 May 2011 15:33:26 +0200
diff --git a/debian/compat b/debian/compat
deleted file mode 100644
index 7f8f011..0000000
--- a/debian/compat
+++ /dev/null
@@ -1 +0,0 @@
-7
diff --git a/debian/control b/debian/control
deleted file mode 100644
index 2c457c7..0000000
--- a/debian/control
+++ /dev/null
@@ -1,38 +0,0 @@
-Source: davmail
-Section: net
-Priority: extra
-Maintainer: Alexandre Rossi <alexandre.rossi at gmail.com>
-Build-Depends: debhelper (>= 7.0.50~), default-jdk, ant, ant-optional,
- javahelper (>=0.20),
- libcommons-codec-java, libservlet2.4-java,
- libcommons-httpclient-java, libhtmlcleaner-java,
- libgnumail-java, libjcifs-java, libjackrabbit-java,
- liblog4j1.2-java, libwoodstox-java, libstax2-api-java,
- libswt-gtk-3-java
-Standards-Version: 3.9.2
-Homepage: http://davmail.sourceforge.net/
-#Vcs-Git: git://git.debian.org/collab-maint/davmail-src-3.9.1.git
-#Vcs-Browser: http://git.debian.org/?p=collab-maint/davmail-src-3.9.1.git;a=summary
-
-Package: davmail
-Architecture: all
-Depends: lsb-base (>= 3.0-6), logrotate, adduser,
- ${shlibs:Depends}, ${misc:Depends}, ${java:Depends}
-Suggests: libswt-gtk-3.6-java | libswt-gtk-3.5-java | libswt-gtk-3.4-java
-Description: POP/IMAP/SMTP/CalDav/LDAP to Microsoft Exchange gateway
- Ever wanted to get rid of Outlook? DavMail is a POP/IMAP/SMTP/Caldav/LDAP
- exchange gateway allowing users to use any mail/calendar client (e.g.
- Thunderbird with Lightning or Apple iCal) with an Exchange server, even from
- the internet or behind a firewall through Outlook Web Access. DavMail now
- includes an LDAP gateway to Exchange global address book to allow recipient
- address completion in mail compoze window and full calendar support with
- attendees free/busy display.
- .
- The main goal of DavMail is to provide standard compliant protocols in front
- of proprietary Exchange. This means LDAP for address book, SMTP to send
- messages, IMAP to browse messages on the server in any folder, POP to retrieve
- inbox messages only and Caldav for calendar support. Thus any standard
- compliant client can be used with Microsoft Exchange.
- .
- DavMail gateway is implemented in java and should run on any platform.
- Releases are tested on Windows, Linux (Ubuntu) and Mac OSX.
diff --git a/debian/copyright b/debian/copyright
deleted file mode 100644
index be9a401..0000000
--- a/debian/copyright
+++ /dev/null
@@ -1,189 +0,0 @@
-Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
-Upstream-Name: Davmail
-Upstream-Contact: Mickaël Guessant <mguessan at free.fr>
-Source: http://davmail.sourceforge.net/download.html
-
-Files: *
-Copyright: 2009-2012, Mickael Guessant
-License: GPL-2+
- This program is free software; you can redistribute it
- and/or modify it under the terms of the GNU General Public
- License as published by the Free Software Foundation; either
- version 2 of the License, or (at your option) any later
- version.
- .
- This program is distributed in the hope that it will be
- useful, but WITHOUT ANY WARRANTY; without even the implied
- warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
- PURPOSE. See the GNU General Public License for more
- details.
- .
- You should have received a copy of the GNU General Public
- License along with this package; if not, write to the Free
- Software Foundation, Inc., 51 Franklin St, Fifth Floor,
- Boston, MA 02110-1301 USA
- .
- On Debian systems, the full text of the GNU General Public
- License version 2 can be found in the file
- `/usr/share/common-licenses/GPL-2'.
-
-Files: ./jsmooth-0.9.9-7-patch/skeletons/*
-Copyright: 2003-2007, Rodrigo Reyes <reyes at charabia.net>
-License: LGPL-2+
- This library is free software; you can redistribute it and/or
- modify it under the terms of the GNU Library General Public
- License as published by the Free Software Foundation; either
- version 2 of the License, or (at your option) any later version.
- .
- This library is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- Library General Public License for more details.
- .
- You should have received a copy of the GNU Library General Public
- License along with this library; if not, write to the Free Software
- Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- .
- On Debian systems, the full text of the GNU Library General Public
- License version 2 can be found in the file
- `/usr/share/common-licenses/LGPL-2'.
-
-Files: ./jsmooth-0.9.9-7-patch/src/net/*
-Copyright: 2003, Rodrigo Reyes <reyes at charabia.net>
-License: GPL-2+
- This program is free software; you can redistribute it
- and/or modify it under the terms of the GNU General Public
- License as published by the Free Software Foundation; either
- version 2 of the License, or (at your option) any later
- version.
- .
- This program is distributed in the hope that it will be
- useful, but WITHOUT ANY WARRANTY; without even the implied
- warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
- PURPOSE. See the GNU General Public License for more
- details.
- .
- You should have received a copy of the GNU General Public
- License along with this package; if not, write to the Free
- Software Foundation, Inc., 51 Franklin St, Fifth Floor,
- Boston, MA 02110-1301 USA
- .
- On Debian systems, the full text of the GNU General Public
- License version 2 can be found in the file
- `/usr/share/common-licenses/GPL-2'.
-
-Files: ./libgrowl/*
-Copyright: 2008, Michael Stringer
-License: BSD (3 clause)
- Redistribution and use in source and binary forms, with or without
- modification, are permitted provided that the following conditions
- are met:
- 1. Redistributions of source code must retain the above copyright
- notice, this list of conditions and the following disclaimer.
- 2. Redistributions in binary form must reproduce the above copyright
- notice, this list of conditions and the following disclaimer in the
- documentation and/or other materials provided with the distribution.
- 3. Neither the name of the University nor the names of its contributors
- may be used to endorse or promote products derived from this software
- without specific prior written permission.
- .
- THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- SUCH DAMAGE.
-
-Files: src/java/com/ctc/wstx/sr/StreamScanner.java
-Copyright: 2004 Tatu Saloranta <tatu.saloranta at iki.fi>
-License: GPL-2
- This program is free software; you can redistribute it
- and/or modify it under the terms of the GNU General Public
- License as published by the Free Software Foundation; either
- version 2 of the License, or (at your option) any later
- version.
- .
- This program is distributed in the hope that it will be
- useful, but WITHOUT ANY WARRANTY; without even the implied
- warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
- PURPOSE. See the GNU General Public License for more
- details.
- .
- You should have received a copy of the GNU General Public
- License along with this package; if not, write to the Free
- Software Foundation, Inc., 51 Franklin St, Fifth Floor,
- Boston, MA 02110-1301 USA
- .
- On Debian systems, the full text of the GNU General Public
- License version 2 can be found in the file
- `/usr/share/common-licenses/GPL-2'.
-
-Files: debian/*
-Copyright: 2012 Alexandre Rossi <alexandre.rossi at gmail.com>
-License: GPL-2+
- This program is free software; you can redistribute it
- and/or modify it under the terms of the GNU General Public
- License as published by the Free Software Foundation; either
- version 2 of the License, or (at your option) any later
- version.
- .
- This program is distributed in the hope that it will be
- useful, but WITHOUT ANY WARRANTY; without even the implied
- warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
- PURPOSE. See the GNU General Public License for more
- details.
- .
- You should have received a copy of the GNU General Public
- License along with this package; if not, write to the Free
- Software Foundation, Inc., 51 Franklin St, Fifth Floor,
- Boston, MA 02110-1301 USA
- .
- On Debian systems, the full text of the GNU General Public
- License version 2 can be found in the file
- `/usr/share/common-licenses/GPL-2'.
-
-Files: ./nsis/processwork.dll
-Files: ./lib/libgrowl-0.2.jar
-Files: ./lib/slf4j-log4j12-1.3.1.jar
-Files: ./lib/jdom-1.0.jar
-Files: ./lib/redline-1.1.9.jar
-Files: ./lib/xercesImpl-2.8.1.jar
-Files: ./lib/jcharset-1.3.jar
-Files: ./lib/activation-1.1.1.jar
-Files: ./lib/swt-3.7-gtk-linux-x86.jar
-Files: ./lib/htmlcleaner-2.1.jar
-Files: ./lib/jcifs-1.3.14.jar
-Files: ./lib/stax-api-1.0.1.jar
-Files: ./lib/commons-httpclient-3.1.jar
-Files: ./lib/log4j-1.2.16.jar
-Files: ./lib/stax2-api-3.1.1.jar
-Files: ./lib/junit-3.8.1.jar
-Files: ./lib/woodstox-core-asl-4.1.2.jar
-Files: ./lib/nsisant-1.2.jar
-Files: ./lib/slf4j-api-1.3.1.jar
-Files: ./lib/commons-logging-1.0.4.jar
-Files: ./lib/libgrowl.jnilib
-Files: ./lib/commons-codec-1.3.jar
-Files: ./lib/swt-3.7-win32-x86_64.jar
-Files: ./lib/mail-1.4.3.jar
-Files: ./lib/jsmoothgen-ant-0.9.9-7-mgu2.jar
-Files: ./lib/jarbundler-2.1.0.jar
-Files: ./lib/servlet-api.jar
-Files: ./lib/swt-3.7-win32-x86.jar
-Files: ./lib/winrun4j-0.4.4.jar
-Files: ./lib/commons-collections-3.1.jar
-Files: ./lib/swt-3.7-gtk-linux-x86_64.jar
-Files: ./lib/ant-deb-0.0.1.jar
-Files: ./lib/jackrabbit-webdav-1.4.jar
-Files: ./svnant/svnkit-javahl16-1.7.0-beta1.jar
-Files: ./svnant/sequence-library-1.0.0.jar
-Files: ./svnant/antlr-runtime-3.4.jar
-Files: ./svnant/svnkit-1.7.0-beta1.jar
-Files: ./svnant/sqljet-1.1.0-SNAPSHOT_r1190_v20120116_2007.jar
-Files: ./svnant/svnClientAdapter.jar
-Files: ./svnant/svnant.jar
diff --git a/debian/davmail.1 b/debian/davmail.1
deleted file mode 100644
index 07f726f..0000000
--- a/debian/davmail.1
+++ /dev/null
@@ -1,45 +0,0 @@
-.\" Hey, EMACS: -*- nroff -*-
-.\" (C) Copyright 2012 Alexandre Rossi <alexandre.rossi at gmail.com>
-.\"
-.TH DAVMAIL 1 "September 2012"
-.\" Please adjust this date whenever revising the manpage.
-.\"
-.\" Some roff macros, for reference:
-.\" .nh disable hyphenation
-.\" .hy enable hyphenation
-.\" .ad l left justify
-.\" .ad b justify to both left and right margins
-.\" .nf disable filling
-.\" .fi enable filling
-.\" .br insert line break
-.\" .sp <n> insert n+1 empty lines
-.\" for manpage-specific macros, see man(7)
-.SH NAME
-davmail \- POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway
-.SH SYNOPSIS
-.B davmail
-.RI [ config-file-path ]
-.br
-.SH DESCRIPTION
-This manual page documents briefly the
-.B davmail
-command.
-.PP
-\fBdavmail\fP creates a gateway making the Microsoft Exchange mail, contacts
-and calendar protocol available in standard protocols such as POP, IMAP, CalDAV,
-SMTP and LDAP.
-.SH OPTIONS
-\fBdavmail\fP takes a single command line argument, the configuration file
-path. The configuration file syntax is described deeply on the \fBdavmail\fP
-website.
-.SH FILES
-.I /etc/davmail.conf
-.RS
-The system wide configuration file.
-.I /etc/default/davmail
-.RS
-System wide initscript behaviour handling.
-.SH SEE ALSO
-.BR http://davmail.sourceforge.net/serversetup.html
-.BR http://davmail.sourceforge.net/advanced.html
-.BR http://davmail.sourceforge.net/sslsetup.html
diff --git a/debian/davmail.conf b/debian/davmail.conf
deleted file mode 100644
index 6cbe280..0000000
--- a/debian/davmail.conf
+++ /dev/null
@@ -1,51 +0,0 @@
-davmail.url=https://owa.example.com/owa/
-davmail.enableEws=false
-davmail.ldapPort=1389
-davmail.caldavPort=1080
-davmail.smtpPort=1025
-davmail.popPort=1110
-davmail.imapPort=1143
-davmail.proxyPort=
-davmail.disableUpdateCheck=true
-davmail.bindAddress=127.0.0.1
-davmail.logFilePath=/var/log/davmail.log
-davmail.server=true
-davmail.server.certificate.hash=
-davmail.caldavPastDelay=90
-davmail.sentKeepDelay=90
-davmail.keepDelay=30
-davmail.allowRemote=false
-
-davmail.enableProxy=false
-davmail.proxyHost=
-davmail.proxyPassword=
-davmail.proxyUser=
-
-log4j.logger.davmail=WARN
-log4j.logger.httpclient.wire=WARN
-log4j.logger.org.apache.commons.httpclient=WARN
-log4j.rootLogger=WARN
-
-# SSL configuration. From http://davmail.sourceforge.net/sslsetup.html :
-#
-# SSL is not necessary when DavMail is used in workstation mode, as
-# communication between clients and DavMail remain local. However, in server
-# (shared) mode e.g. with a smartphone connecting to DavMail over the internet,
-# you should make sure encryption is enabled.
-#
-# The simplest way to secure communication between mail/calendar clients and
-# DavMail is to create a self signed certificate:
-#
-# keytool -genkey -keyalg rsa -keysize 2048 -storepass password\
-# -keystore davmail.p12 -storetype pkcs12 -validity 3650\
-# -dname cn=davmailhostname.company.com,ou=davmail,o=sf,o=net
-#
-# Note to iPhone users: iOS does not support the default DSA algorithm, make
-# sure you use an RSA key pair.
-# Another note : do not use blank passwords, both keystore and key passwords
-# must be set.
-#
-#davmail.ssl.keystoreType=PKCS12
-#davmail.ssl.keyPass=password
-#davmail.ssl.keystoreFile=/etc/davmail.p12
-#davmail.ssl.keystorePass=password
diff --git a/debian/davmail.desktop b/debian/davmail.desktop
deleted file mode 100644
index dd139f0..0000000
--- a/debian/davmail.desktop
+++ /dev/null
@@ -1,9 +0,0 @@
-[Desktop Entry]
-Version=1.0
-Encoding=UTF-8
-Name=DavMail
-Type=Application
-Terminal=false
-Exec=/usr/bin/davmail
-Comment=DavMail Exchange Gateway
-Icon=/usr/share/icons/davmail.png
diff --git a/debian/davmail.menu b/debian/davmail.menu
deleted file mode 100644
index 93c1e82..0000000
--- a/debian/davmail.menu
+++ /dev/null
@@ -1,3 +0,0 @@
-?package(davmail):needs="X11" section="Applications/Network/Communication" \
- title="DavMail Exchange Gateway" command="/usr/bin/davmail" \
- icon="/usr/share/pixmaps/davmail.xpm"
diff --git a/debian/davmail.xpm b/debian/davmail.xpm
deleted file mode 100644
index ffae546..0000000
--- a/debian/davmail.xpm
+++ /dev/null
@@ -1,519 +0,0 @@
-/* XPM */
-static char * davmail_xpm[] = {
-"32 32 484 2",
-" c None",
-". c #C95900",
-"+ c #D77F00",
-"@ c #E3A200",
-"# c #EABB00",
-"$ c #EEC900",
-"% c #EEC800",
-"& c #EAC200",
-"* c #DFA100",
-"= c #D88000",
-"- c #B34900",
-"; c #C16500",
-"> c #E3A100",
-", c #FBEF00",
-"' c #FFFF05",
-") c #FFFF17",
-"! c #FEFF21",
-"~ c #FEFF28",
-"{ c #FEFF24",
-"] c #FFFF19",
-"^ c #FFFF06",
-"/ c #FDF700",
-"( c #DEA200",
-"_ c #AF5D00",
-": c #CB5F00",
-"< c #E7B100",
-"[ c #FBF300",
-"} c #FFFF09",
-"| c #FEFA74",
-"1 c #FDFDB7",
-"2 c #FFFFCE",
-"3 c #FFFFD8",
-"4 c #FFFFE4",
-"5 c #FFFFDF",
-"6 c #FFFFDE",
-"7 c #FFFFBA",
-"8 c #FFFE7F",
-"9 c #FFFF14",
-"0 c #FBF400",
-"a c #E0B100",
-"b c #AB4A00",
-"c c #CF6D00",
-"d c #F0D500",
-"e c #FFFF0A",
-"f c #FEFE65",
-"g c #FEFEDF",
-"h c #FEFEE0",
-"i c #FDFDDB",
-"j c #E9DCD8",
-"k c #EEE5DC",
-"l c #ECE2DB",
-"m c #EBE2DA",
-"n c #EBE2DB",
-"o c #EEE6DB",
-"p c #EDE3E0",
-"q c #EEE5DE",
-"r c #FEFE73",
-"s c #FFFF0D",
-"t c #F3DE00",
-"u c #C66800",
-"v c #D47300",
-"w c #F7E700",
-"x c #FEFF19",
-"y c #FDFDAD",
-"z c #FEFED1",
-"A c #FEFECD",
-"B c #FEFFCD",
-"C c #ECE3CD",
-"D c #F8F3DA",
-"E c #FEFFDE",
-"F c #FEFEE2",
-"G c #FEFEDE",
-"H c #FEFEE1",
-"I c #FDFDDD",
-"J c #E9DED2",
-"K c #F8F6CD",
-"L c #FAF9D0",
-"M c #FEFEB2",
-"N c #FEFF27",
-"O c #F7E900",
-"P c #C36D00",
-"Q c #BE5D00",
-"R c #F8E700",
-"S c #FEFE22",
-"T c #FEFEC4",
-"U c #FEFEC5",
-"V c #FEFEC2",
-"W c #FDFCC6",
-"X c #E5D6C2",
-"Y c #FEFEDC",
-"Z c #FDFDDC",
-"` c #F6F2D7",
-" . c #EEE5C3",
-".. c #FFFFC4",
-"+. c #FDFDC5",
-"@. c #FEFEC9",
-"#. c #FEFF2A",
-"$. c #FAF100",
-"%. c #B85B00",
-"&. c #EFCA00",
-"*. c #FEFF17",
-"=. c #FDFDAB",
-"-. c #FEFDBC",
-";. c #FEFEB6",
-">. c #FEFEBB",
-",. c #F9F7BC",
-"'. c #ECE1C7",
-"). c #FEFEDD",
-"!. c #FEFED5",
-"~. c #FDFEDD",
-"{. c #FEFDDB",
-"]. c #E0CEBD",
-"^. c #FCFBBA",
-"/. c #FDFDBD",
-"(. c #FEFEBC",
-"_. c #FEFEB3",
-":. c #FDFDB2",
-"<. c #773700",
-"[. c #D98300",
-"}. c #FDFD00",
-"|. c #FEFE8C",
-"1. c #FDFEB2",
-"2. c #FEFEB0",
-"3. c #FEFEB1",
-"4. c #F4EEB0",
-"5. c #F3EBD0",
-"6. c #E4D3C1",
-"7. c #F3ECB0",
-"8. c #FEFD9C",
-"9. c #FEFE00",
-"0. c #D28400",
-"a. c #C65100",
-"b. c #F6E300",
-"c. c #FEFF4C",
-"d. c #FEFEA5",
-"e. c #FEFEA6",
-"f. c #FFFFA8",
-"g. c #E9DAAB",
-"h. c #F9F5D8",
-"i. c #F5EFD2",
-"j. c #E9D9A9",
-"k. c #FFFFA7",
-"l. c #FEFFA6",
-"m. c #FDFCA5",
-"n. c #FDFDA7",
-"o. c #FDFDA6",
-"p. c #FEFEAA",
-"q. c #FEFC5F",
-"r. c #FEF500",
-"s. c #A34000",
-"t. c #DB8E00",
-"u. c #FFFF08",
-"v. c #FEFE92",
-"w. c #FEFD99",
-"x. c #FEFEA4",
-"y. c #FEFD97",
-"z. c #FEFEA1",
-"A. c #FDFDA0",
-"B. c #FFFF99",
-"C. c #DCC3A4",
-"D. c #FDFCDD",
-"E. c #FFFFE0",
-"F. c #DFCAB9",
-"G. c #E8D8A1",
-"H. c #ECDEA2",
-"I. c #ECDDA1",
-"J. c #F6F09B",
-"K. c #FEFE9A",
-"L. c #FEFD9A",
-"M. c #FAF89E",
-"N. c #FDFD8D",
-"O. c #FFFF0E",
-"P. c #C48700",
-"Q. c #EFBE00",
-"R. c #FEFE24",
-"S. c #FDFB92",
-"T. c #FEFC92",
-"U. c #FEFC94",
-"V. c #FEFC93",
-"W. c #F5EE92",
-"X. c #DEC8B9",
-"Y. c #FEFFE3",
-"Z. c #FEFEE3",
-"`. c #F9F5DB",
-" + c #F6F2DA",
-".+ c #F7F3DB",
-"++ c #E6D2C0",
-"@+ c #E5D291",
-"#+ c #FEFD94",
-"$+ c #FDFC92",
-"%+ c #FEFB92",
-"&+ c #FEFD2C",
-"*+ c #EACC00",
-"=+ c #FEFA50",
-"-+ c #FDFA8B",
-";+ c #FDFA89",
-">+ c #FEFC89",
-",+ c #E0C37C",
-"'+ c #ECE2D8",
-")+ c #F8F3DD",
-"!+ c #F8F3DC",
-"~+ c #FAF7DF",
-"{+ c #FEFEE5",
-"]+ c #FEFEEB",
-"^+ c #FEFDE4",
-"/+ c #FDFDEB",
-"(+ c #FAF7DD",
-"_+ c #D7B694",
-":+ c #FFFF84",
-"<+ c #FEFB5C",
-"[+ c #F8E200",
-"}+ c #733800",
-"|+ c #975200",
-"1+ c #FAE700",
-"2+ c #FFF46C",
-"3+ c #FEF885",
-"4+ c #FEF67C",
-"5+ c #FEF67E",
-"6+ c #FEF780",
-"7+ c #FFF77D",
-"8+ c #F4E67D",
-"9+ c #E6CE88",
-"0+ c #E7CF89",
-"a+ c #E6D08B",
-"b+ c #E1C5B0",
-"c+ c #FFFFEE",
-"d+ c #FDFEEB",
-"e+ c #FFFFF6",
-"f+ c #E3CCB3",
-"g+ c #F5E57C",
-"h+ c #FEF985",
-"i+ c #FEF67D",
-"j+ c #FEFD7B",
-"k+ c #FBEB00",
-"l+ c #9C5300",
-"m+ c #CC7000",
-"n+ c #FFF262",
-"o+ c #FEF57A",
-"p+ c #FEF372",
-"q+ c #FEF473",
-"r+ c #FFF67A",
-"s+ c #FFF778",
-"t+ c #FFFA78",
-"u+ c #E8CD7D",
-"v+ c #F5EAE4",
-"w+ c #FEFDF7",
-"x+ c #FEFDF6",
-"y+ c #F2E8DB",
-"z+ c #DAB571",
-"A+ c #FAEE7A",
-"B+ c #FEF57B",
-"C+ c #FEF26D",
-"D+ c #FCE900",
-"E+ c #B56400",
-"F+ c #CD7300",
-"G+ c #FAE800",
-"H+ c #FEE508",
-"I+ c #FDEB57",
-"J+ c #FDF271",
-"K+ c #FEF16E",
-"L+ c #FDF16E",
-"M+ c #FDF473",
-"N+ c #FEF16D",
-"O+ c #FCEE6D",
-"P+ c #D6AE97",
-"Q+ c #FEFEFA",
-"R+ c #FDFDFC",
-"S+ c #FEFDFC",
-"T+ c #FAF8F5",
-"U+ c #D8B185",
-"V+ c #FFF76D",
-"W+ c #FFF76E",
-"X+ c #FDF170",
-"Y+ c #FFF260",
-"Z+ c #F8E609",
-"`+ c #FCEA00",
-" @ c #B96500",
-".@ c #C96D00",
-"+@ c #FDDC00",
-"@@ c #FDE300",
-"#@ c #FEE200",
-"$@ c #FEEA40",
-"%@ c #FDEC5F",
-"&@ c #FEF065",
-"*@ c #FDED64",
-"=@ c #FEED63",
-"-@ c #FEF069",
-";@ c #D9AA61",
-">@ c #F6EFEC",
-",@ c #FEFEFD",
-"'@ c #F6EDE7",
-")@ c #D7AC88",
-"!@ c #D8AE89",
-"~@ c #D19E6E",
-"{@ c #FFF264",
-"]@ c #FDEC64",
-"^@ c #FDED62",
-"/@ c #FEEF64",
-"(@ c #FEEE61",
-"_@ c #FEE100",
-":@ c #FDE200",
-"<@ c #B86300",
-"[@ c #984F00",
-"}@ c #FBDB00",
-"|@ c #FEDB00",
-"1@ c #FEE30B",
-"2@ c #FDE53D",
-"3@ c #FDEB5E",
-"4@ c #FEEB5C",
-"5@ c #FDEB5C",
-"6@ c #F7DD56",
-"7@ c #E2C2A7",
-"8@ c #FFFFFF",
-"9@ c #F9F3F0",
-"0@ c #DCAE69",
-"a@ c #FFED5A",
-"b@ c #FDE656",
-"c@ c #FDEA60",
-"d@ c #FEE840",
-"e@ c #FDDE0D",
-"f@ c #FDDB00",
-"g@ c #FEE400",
-"h@ c #FEDC00",
-"i@ c #FCD800",
-"j@ c #9D5400",
-"k@ c #F7C900",
-"l@ c #FDD900",
-"m@ c #FEDA00",
-"n@ c #FED900",
-"o@ c #FEDB0B",
-"p@ c #FEE134",
-"q@ c #FEE543",
-"r@ c #EEC84F",
-"s@ c #E7C3A3",
-"t@ c #EACEB4",
-"u@ c #F9F3EE",
-"v@ c #DCAD7E",
-"w@ c #F9DD4B",
-"x@ c #FDE446",
-"y@ c #FEE73B",
-"z@ c #FEDB0A",
-"A@ c #FACF00",
-"B@ c #763A00",
-"C@ c #EBB300",
-"D@ c #FED400",
-"E@ c #FED500",
-"F@ c #FED300",
-"G@ c #FDD503",
-"H@ c #FDD40A",
-"I@ c #F7CA0A",
-"J@ c #E3A720",
-"K@ c #F3E3D9",
-"L@ c #D59C7D",
-"M@ c #EEB80B",
-"N@ c #FED60A",
-"O@ c #FED503",
-"P@ c #FDD500",
-"Q@ c #F1B900",
-"R@ c #D58500",
-"S@ c #FFD800",
-"T@ c #FDD200",
-"U@ c #FED200",
-"V@ c #FFD500",
-"W@ c #D78F1C",
-"X@ c #F9F1ED",
-"Y@ c #FEFFFE",
-"Z@ c #EFD9C7",
-"`@ c #DD9404",
-" # c #FFD700",
-".# c #CC8000",
-"+# c #BA5200",
-"@# c #FCC800",
-"## c #FECC00",
-"$# c #FECD00",
-"%# c #FDCD00",
-"&# c #FFCE00",
-"*# c #D18342",
-"=# c #FEFEFE",
-"-# c #FBFCFF",
-";# c #D2770F",
-"># c #FDCC00",
-",# c #FECB00",
-"'# c #FFD000",
-")# c #AB4900",
-"!# c #4E1F00",
-"~# c #DE8201",
-"{# c #FECA00",
-"]# c #FDCA00",
-"^# c #FEC400",
-"/# c #FEC900",
-"(# c #FEC800",
-"_# c #EFAC00",
-":# c #E5B28F",
-"<# c #FEFFFF",
-"[# c #FDFCFA",
-"}# c #D88841",
-"|# c #F7BD00",
-"1# c #DA8300",
-"2# c #572200",
-"3# c #F2B200",
-"4# c #FEC300",
-"5# c #FEC200",
-"6# c #E18100",
-"7# c #FAEFE6",
-"8# c #EDC59D",
-"9# c #F1A300",
-"0# c #FEC600",
-"a# c #FDC400",
-"b# c #F5B600",
-"c# c #723500",
-"d# c #B75C01",
-"e# c #FEBA00",
-"f# c #FEB900",
-"g# c #FEC000",
-"h# c #FEBF00",
-"i# c #E3871B",
-"j# c #F8E5D6",
-"k# c #E37F00",
-"l# c #FFBC00",
-"m# c #FEBC00",
-"n# c #FEC100",
-"o# c #FDBB00",
-"p# c #B35D00",
-"q# c #C46800",
-"r# c #FDB400",
-"s# c #FEB600",
-"t# c #FEBB00",
-"u# c #FAB100",
-"v# c #ECB272",
-"w# c #FDFAF8",
-"x# c #E18925",
-"y# c #D26F00",
-"z# c #2C1100",
-"A# c #C66600",
-"B# c #FBAF00",
-"C# c #FEB400",
-"D# c #FDB300",
-"E# c #F6A300",
-"F# c #F6DBC2",
-"G# c #EAAA6A",
-"H# c #FAAB00",
-"I# c #FEB300",
-"J# c #FEB500",
-"K# c #FBAE00",
-"L# c #C46C00",
-"M# c #371600",
-"N# c #9A4C00",
-"O# c #F19600",
-"P# c #FFB200",
-"Q# c #FFAF00",
-"R# c #EC9715",
-"S# c #E8AC7B",
-"T# c #ED8F00",
-"U# c #FEAD00",
-"V# c #FEAF00",
-"W# c #F19B00",
-"X# c #A54F00",
-"Y# c #1F0B00",
-"Z# c #823200",
-"`# c #BE6700",
-" $ c #F39B00",
-".$ c #ED8D01",
-"+$ c #E58400",
-"@$ c #FEAC00",
-"#$ c #FDAB00",
-"$$ c #FCA800",
-"%$ c #F29A00",
-"&$ c #BC6200",
-"*$ c #6D2A00",
-"=$ c #592800",
-"-$ c #A14E00",
-";$ c #D37000",
-">$ c #EC8900",
-",$ c #F39500",
-"'$ c #F99E00",
-")$ c #F89D00",
-"!$ c #F29400",
-"~$ c #ED8A00",
-"{$ c #D27000",
-"]$ c #A24F00",
-"^$ c #5E2800",
-"/$ c #562200",
-"($ c #773100",
-"_$ c #823400",
-" ",
-" . + @ # $ % & * = - ",
-" ; > , ' ) ! ~ ~ { ] ^ / ( _ ",
-" : < [ } | 1 2 3 4 5 6 2 7 8 9 0 a b ",
-" c d e f g h i j k k l m n o p q r s t u ",
-" v w x y z A B C D E F G H F I J K L M N O P ",
-" Q R S T U T V W X 6 I Y I Z Y ` ...V +. at .#.$.%. ",
-" &.*.=.-.;.;.>.,.'.6 ).I !.~.{.].^./.>.(._.:.x d <. ",
-" [.}.|.1.2.2.2.3.4.5.).).Y I ).6.7.2.2.2.3.2.3.8.9.0. ",
-" a.b.c.=.d.e.e.e.f.g.h.Z Y ).).i.j.k.l.m.m.n.e.o.p.q.r.s. ",
-" t.u.v.w.x.y.z.A.B.C.D.).F ).E.F.G.H.I.J.K.L.M.K.K.N.O.P. ",
-" Q.R.S.T.U.T.U.V.W.X.Y.F F Z.Z.`. +.+++ at +#+$+U.$+V.%+&+*+ ",
-" b.=+-+;+;+;+;+>+,+'+)+!+~+{+]+^+/+(+_+:+;+;+;+;+;+-+<+[+}+ ",
-"|+1+2+3+4+3+5+6+7+8+9+0+a+b+c+]+d+e+f+g+h+i+7+i+i+i+i+j+k+l+ ",
-"m+k+n+o+o+p+o+q+o+r+s+t+u+v+w+x+x+y+z+r+p+r+A+o+o+o+B+C+D+E+ ",
-"F+G+H+I+J+K+L+M+L+L+N+O+P+Q+R+S+T+U+V+W+L+L+K+L+L+X+Y+Z+`+ @ ",
-". at +@@@#@$@%@&@*@=@=@-@;@>@,@,@,@'@)@!@~@{@]@^@/@(@$@@@_@:@<@ ",
-"[@}@|@#@|@1 at 2@3 at 4@5 at 6@7 at 8@,@,@, at S+S+9@0 at a@b at c@d at e@f at g@h at i@j@ ",
-" k at l@m at m@m at n@o at p@q at r@s at t@u@,@,@, at 8@v at w@x at y@z at m@|@n at m@|@A at B@ ",
-" C at D@E at E@E at D@E at F@G at H@I at J@K@,@, at 8@L at M@N at O@F at E@E at E@D at E@P at Q@ ",
-" R at S@T at U@U at F@U at U@U at U@V at W@X at R+Y@Z@`@ #T at F@F at U@U at F@U at T@S at .# ",
-" +#@#######$#######%#&#*#=#, at -#;#@#>#,#$#$#,#,#$#,#,#'#)# ",
-" !#~#{#]#{#^#^#/#(#,#_#:#<#[#}#|#,#^#,#^#^#/#/#^#]#,#1#2# ",
-" 3#^#4#^#^#5#5#4#6#7#8 at 8#9#0#a#^#^#^#^#4#4#^#^#b#c# ",
-" d#e#4#f#g#g#f#h#i#8 at j#k#l#m#m#m#m#e#f#n#g#m#o#p# ",
-" q#r#o#s#t#s#u#v#w#x#f#e#s#t#t#t#s#s#s#o#r#y#z# ",
-" A#B#C#D#C#E#F#G#H#D#I#C#I#I#I#J#r#f#K#L#M# ",
-" N#O#P#Q#R#S#T#U#V#V#U#U#U#I#U#P#W#X#Y# ",
-" Z#`# $.$+$Q#@$@$#$#$U#@$$$%$&$*$ ",
-" =$-$;$>$,$'$)$!$~${$]$^$ ",
-" Y#/$($_$_$($/$Y# ",
-" "};
diff --git a/debian/default b/debian/default
deleted file mode 100644
index dbfda55..0000000
--- a/debian/default
+++ /dev/null
@@ -1,4 +0,0 @@
-# Defaults for davmail initscript
-
-# Whether to run the daemon using the supplied initscript
-ENABLE_DAEMON="false"
diff --git a/debian/gbp.conf b/debian/gbp.conf
deleted file mode 100644
index 958d084..0000000
--- a/debian/gbp.conf
+++ /dev/null
@@ -1,2 +0,0 @@
-[git-import-orig]
-filter = ['lib', 'libgrowl', 'nsis', 'svnant', ]
diff --git a/debian/init b/debian/init
deleted file mode 100755
index d0248dd..0000000
--- a/debian/init
+++ /dev/null
@@ -1,162 +0,0 @@
-#! /bin/sh
-### BEGIN INIT INFO
-# Provides: davmail
-# Required-Start: $local_fs $remote_fs
-# Required-Stop: $local_fs $remote_fs
-# Default-Start: 2 3 4 5
-# Default-Stop: 0 1 6
-# Short-Description: Launch Davmail Exchange gateway
-# Description: Prepare environement and launch the Davmail Exchange
-# gateway daemon.
-### END INIT INFO
-
-# Author: Alexandre Rossi <alexandre.rossi at gmail.com>
-
-# Do NOT "set -e"
-
-# PATH should only include /usr/* if it runs after the mountnfs.sh script
-PATH=/sbin:/usr/sbin:/bin:/usr/bin
-DESC="Davmail Exchange gateway"
-NAME=davmail
-DAEMON=/usr/bin/$NAME
-DAEMON_USER=$NAME
-HOME=/var/lib/$DAEMON_USER
-PIDFILE=/var/run/$NAME.pid
-LOGFILE=/var/log/$NAME.log
-SCRIPTNAME=/etc/init.d/$NAME
-
-
-# Exit if the package is not installed
-[ -x "$DAEMON" ] || exit 0
-
-# Read configuration variable file if it is present
-[ -r /etc/default/$NAME ] && . /etc/default/$NAME
-
-# Exit if daemon run by initscript is disabled
-[ "$ENABLE_DAEMON" = "true" ] || exit 0
-
-DAEMON_ARGS="/etc/davmail.conf"
-
-# Create logfiles if they do not exist
-if [ ! -r "$LOGFILE" ]
-then
- touch $LOGFILE
- chown $NAME:adm $LOGFILE
-fi
-
-# Load the VERBOSE setting and other rcS variables
-. /lib/init/vars.sh
-
-# Define LSB log_* functions.
-# Depend on lsb-base (>= 3.0-6) to ensure that this file is present.
-. /lib/lsb/init-functions
-
-do_start()
-{
- # Return
- # 0 if daemon has been started
- # 2 if daemon could not be started
- $ENABLE_DAEMON
- is_alive && return 0
- start-stop-daemon --start --pidfile $PIDFILE --chuid $DAEMON_USER\
- --background --make-pidfile\
- --exec $DAEMON -- $DAEMON_ARGS \
- || return 2
-}
-
-do_stop()
-{
- # Return
- # 0 if daemon has been stopped
- # 1 if daemon was already stopped
- # 2 if daemon could not be stopped
- # other if a failure occurred
- start-stop-daemon --user $DAEMON_USER --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME
- RETVAL="$?"
- [ "$RETVAL" = 2 ] && return 2
- start-stop-daemon --user $DAEMON_USER --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON
- [ "$?" = 2 ] && return 2
- rm -f $PIDFILE
- return "$RETVAL"
-}
-
-do_reload() {
- #
- # If the daemon can reload its configuration without
- # restarting (for example, when it is sent a SIGHUP),
- # then implement that here.
- #
- start-stop-daemon --user $DAEMON_USER --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME
- return 0
-}
-
-is_alive () {
- ret=1
- if [ -r $PIDFILE ] ; then
- pid=`cat $PIDFILE`
- if [ -e /proc/$pid ] ; then
- procname=`/bin/ps h -p $pid -C $NAME`
- [ -n "$procname" ] && ret=0
- fi
- fi
- return $ret
-}
-
-
-case "$1" in
- start)
- [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
- do_start
- case "$?" in
- 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
- 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
- esac
- ;;
- stop)
- [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
- do_stop
- case "$?" in
- 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
- 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
- esac
- ;;
- reload|force-reload)
- log_daemon_msg "Reloading $DESC" "$NAME"
- do_reload
- log_end_msg $?
- ;;
- restart)
- log_daemon_msg "Restarting $DESC" "$NAME"
- do_stop
- case "$?" in
- 0|1)
- do_start
- case "$?" in
- 0) log_end_msg 0 ;;
- 1) log_end_msg 1 ;; # Old process is still running
- *) log_end_msg 1 ;; # Failed to start
- esac
- ;;
- *)
- # Failed to stop
- log_end_msg 1
- ;;
- esac
- ;;
- status)
- echo -n "Status of $DESC: "
- if is_alive ; then
- echo "alive."
- else
- echo "dead."
- exit 1
- fi
- ;;
- *)
- echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload|status}" >&2
- exit 3
- ;;
-esac
-
-:
-# vim: ts=4 sw=4 expandtab
diff --git a/debian/install b/debian/install
deleted file mode 100644
index 55d6ef5..0000000
--- a/debian/install
+++ /dev/null
@@ -1,4 +0,0 @@
-debian/davmail.conf /etc
-debian/davmail.xpm /usr/share/pixmaps
-dist/davmail.png /usr/share/icons
-debian/davmail.desktop /usr/share/applications
diff --git a/debian/jlibs b/debian/jlibs
deleted file mode 100644
index bde75da..0000000
--- a/debian/jlibs
+++ /dev/null
@@ -1 +0,0 @@
-dist/davmail.jar
diff --git a/debian/links b/debian/links
deleted file mode 100644
index ba120b0..0000000
--- a/debian/links
+++ /dev/null
@@ -1 +0,0 @@
-usr/share/java/davmail.jar usr/bin/davmail
diff --git a/debian/logrotate b/debian/logrotate
deleted file mode 100644
index 2ae1891..0000000
--- a/debian/logrotate
+++ /dev/null
@@ -1,14 +0,0 @@
-/var/log/davmail.log /var/log/davmail.*.log {
- rotate 4
- weekly
- compress
- copytruncate
- missingok
- notifempty
- sharedscripts
- postrotate
- if invoke-rc.d --quiet davmail status > /dev/null; then
- invoke-rc.d --quiet davmail reload > /dev/null
- fi
- endscript
-}
diff --git a/debian/manifest b/debian/manifest
deleted file mode 100644
index b6f54d9..0000000
--- a/debian/manifest
+++ /dev/null
@@ -1,3 +0,0 @@
-usr/share/java/davmail.jar:
- Class-Path: /usr/share/java/commons-codec.jar /usr/share/java/servlet-api.jar /usr/share/java/commons-httpclient.jar /usr/share/java/htmlcleaner.jar /usr/share/java/gnumail.jar /usr/share/java/jcifs.jar /usr/share/java/jackrabbit-webdav.jar /usr/share/java/woodstox-core-lgpl.jar /usr/share/java/stax2-api.jar /usr/share/java/swt.jar /usr/share/java/log4j-1.2.jar /usr/share/java/slf4j-simple.jar /usr/share/java/slf4j-api.jar
- Main-Class: davmail.DavGateway
diff --git a/debian/manpages b/debian/manpages
deleted file mode 100644
index 10ecf14..0000000
--- a/debian/manpages
+++ /dev/null
@@ -1 +0,0 @@
-debian/davmail.1
diff --git a/debian/patches/base64-enc-dec b/debian/patches/base64-enc-dec
deleted file mode 100644
index 5fbebbc..0000000
--- a/debian/patches/base64-enc-dec
+++ /dev/null
@@ -1,474 +0,0 @@
-Description: Add BASE64MailboxDecoder and BASE64MailboxEncoder
-
-Those are missing in libgnumail-java and needed to build davmail. The
-alternative would be to depend on javamail which is now DFSG-free
-(RFP #596469, http://bugs.debian.org/596469 )
-
-Origin: javamail source code, http://kenai.com/projects/javamail/downloads
-Forwarded: not-needed
-Author: Alexandre Rossi <alexandre.rossi at gmail.com>
-
-Index: davmail-src-3.9.9-1976/src/java/com/sun/mail/imap/protocol/BASE64MailboxDecoder.java
-===================================================================
---- /dev/null 1970-01-01 00:00:00.000000000 +0000
-+++ davmail-src-3.9.9-1976/src/java/com/sun/mail/imap/protocol/BASE64MailboxDecoder.java 2012-08-24 15:31:07.946121347 +0200
-@@ -0,0 +1,194 @@
-+/*
-+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
-+ *
-+ * Copyright (c) 1997-2010 Oracle and/or its affiliates. All rights reserved.
-+ *
-+ * The contents of this file are subject to the terms of either the GNU
-+ * General Public License Version 2 only ("GPL") or the Common Development
-+ * and Distribution License("CDDL") (collectively, the "License"). You
-+ * may not use this file except in compliance with the License. You can
-+ * obtain a copy of the License at
-+ * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
-+ * or packager/legal/LICENSE.txt. See the License for the specific
-+ * language governing permissions and limitations under the License.
-+ *
-+ * When distributing the software, include this License Header Notice in each
-+ * file and include the License file at packager/legal/LICENSE.txt.
-+ *
-+ * GPL Classpath Exception:
-+ * Oracle designates this particular file as subject to the "Classpath"
-+ * exception as provided by Oracle in the GPL Version 2 section of the License
-+ * file that accompanied this code.
-+ *
-+ * Modifications:
-+ * If applicable, add the following below the License Header, with the fields
-+ * enclosed by brackets [] replaced by your own identifying information:
-+ * "Portions Copyright [year] [name of copyright owner]"
-+ *
-+ * Contributor(s):
-+ * If you wish your version of this file to be governed by only the CDDL or
-+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
-+ * elects to include this software in this distribution under the [CDDL or GPL
-+ * Version 2] license." If you don't indicate a single choice of license, a
-+ * recipient has the option to distribute your version of this file under
-+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
-+ * its licensees as provided above. However, if you add GPL Version 2 code
-+ * and therefore, elected the GPL Version 2 license, then the option applies
-+ * only if the new code is made subject to such option by the copyright
-+ * holder.
-+ */
-+
-+package com.sun.mail.imap.protocol;
-+
-+import java.text.StringCharacterIterator;
-+import java.text.CharacterIterator;
-+
-+/**
-+ * See the BASE64MailboxEncoder for a description of the RFC2060 and how
-+ * mailbox names should be encoded. This class will do the correct decoding
-+ * for mailbox names.
-+ *
-+ * @author Christopher Cotton
-+ */
-+
-+public class BASE64MailboxDecoder {
-+
-+ public static String decode(String original) {
-+ if (original == null || original.length() == 0)
-+ return original;
-+
-+ boolean changedString = false;
-+ int copyTo = 0;
-+ // it will always be less than the original
-+ char[] chars = new char[original.length()];
-+ StringCharacterIterator iter = new StringCharacterIterator(original);
-+
-+ for(char c = iter.first(); c != CharacterIterator.DONE;
-+ c = iter.next()) {
-+
-+ if (c == '&') {
-+ changedString = true;
-+ copyTo = base64decode(chars, copyTo, iter);
-+ } else {
-+ chars[copyTo++] = c;
-+ }
-+ }
-+
-+ // now create our string from the char array
-+ if (changedString) {
-+ return new String(chars, 0, copyTo);
-+ } else {
-+ return original;
-+ }
-+ }
-+
-+
-+ protected static int base64decode(char[] buffer, int offset,
-+ CharacterIterator iter) {
-+ boolean firsttime = true;
-+ int leftover = -1;
-+
-+ while(true) {
-+ // get the first byte
-+ byte orig_0 = (byte) iter.next();
-+ if (orig_0 == -1) break; // no more chars
-+ if (orig_0 == '-') {
-+ if (firsttime) {
-+ // means we got the string "&-" which is turned into a "&"
-+ buffer[offset++] = '&';
-+ }
-+ // we are done now
-+ break;
-+ }
-+ firsttime = false;
-+
-+ // next byte
-+ byte orig_1 = (byte) iter.next();
-+ if (orig_1 == -1 || orig_1 == '-')
-+ break; // no more chars, invalid base64
-+
-+ byte a, b, current;
-+ a = pem_convert_array[orig_0 & 0xff];
-+ b = pem_convert_array[orig_1 & 0xff];
-+ // The first decoded byte
-+ current = (byte)(((a << 2) & 0xfc) | ((b >>> 4) & 3));
-+
-+ // use the leftover to create a Unicode Character (2 bytes)
-+ if (leftover != -1) {
-+ buffer[offset++] = (char)(leftover << 8 | (current & 0xff));
-+ leftover = -1;
-+ } else {
-+ leftover = current & 0xff;
-+ }
-+
-+ byte orig_2 = (byte) iter.next();
-+ if (orig_2 == '=') { // End of this BASE64 encoding
-+ continue;
-+ } else if (orig_2 == -1 || orig_2 == '-') {
-+ break; // no more chars
-+ }
-+
-+ // second decoded byte
-+ a = b;
-+ b = pem_convert_array[orig_2 & 0xff];
-+ current = (byte)(((a << 4) & 0xf0) | ((b >>> 2) & 0xf));
-+
-+ // use the leftover to create a Unicode Character (2 bytes)
-+ if (leftover != -1) {
-+ buffer[offset++] = (char)(leftover << 8 | (current & 0xff));
-+ leftover = -1;
-+ } else {
-+ leftover = current & 0xff;
-+ }
-+
-+ byte orig_3 = (byte) iter.next();
-+ if (orig_3 == '=') { // End of this BASE64 encoding
-+ continue;
-+ } else if (orig_3 == -1 || orig_3 == '-') {
-+ break; // no more chars
-+ }
-+
-+ // The third decoded byte
-+ a = b;
-+ b = pem_convert_array[orig_3 & 0xff];
-+ current = (byte)(((a << 6) & 0xc0) | (b & 0x3f));
-+
-+ // use the leftover to create a Unicode Character (2 bytes)
-+ if (leftover != -1) {
-+ buffer[offset++] = (char)(leftover << 8 | (current & 0xff));
-+ leftover = -1;
-+ } else {
-+ leftover = current & 0xff;
-+ }
-+ }
-+
-+ return offset;
-+ }
-+
-+ /**
-+ * This character array provides the character to value map
-+ * based on RFC1521, but with the modification from RFC2060
-+ * which changes the '/' to a ','.
-+ */
-+
-+ // shared with BASE64MailboxEncoder
-+ static final char pem_array[] = {
-+ 'A','B','C','D','E','F','G','H', // 0
-+ 'I','J','K','L','M','N','O','P', // 1
-+ 'Q','R','S','T','U','V','W','X', // 2
-+ 'Y','Z','a','b','c','d','e','f', // 3
-+ 'g','h','i','j','k','l','m','n', // 4
-+ 'o','p','q','r','s','t','u','v', // 5
-+ 'w','x','y','z','0','1','2','3', // 6
-+ '4','5','6','7','8','9','+',',' // 7
-+ };
-+
-+ private static final byte pem_convert_array[] = new byte[256];
-+
-+ static {
-+ for (int i = 0; i < 255; i++)
-+ pem_convert_array[i] = -1;
-+ for(int i = 0; i < pem_array.length; i++)
-+ pem_convert_array[pem_array[i]] = (byte) i;
-+ }
-+}
-Index: davmail-src-3.9.9-1976/src/java/com/sun/mail/imap/protocol/BASE64MailboxEncoder.java
-===================================================================
---- /dev/null 1970-01-01 00:00:00.000000000 +0000
-+++ davmail-src-3.9.9-1976/src/java/com/sun/mail/imap/protocol/BASE64MailboxEncoder.java 2012-08-24 15:31:08.017876275 +0200
-@@ -0,0 +1,260 @@
-+/*
-+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
-+ *
-+ * Copyright (c) 1997-2010 Oracle and/or its affiliates. All rights reserved.
-+ *
-+ * The contents of this file are subject to the terms of either the GNU
-+ * General Public License Version 2 only ("GPL") or the Common Development
-+ * and Distribution License("CDDL") (collectively, the "License"). You
-+ * may not use this file except in compliance with the License. You can
-+ * obtain a copy of the License at
-+ * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
-+ * or packager/legal/LICENSE.txt. See the License for the specific
-+ * language governing permissions and limitations under the License.
-+ *
-+ * When distributing the software, include this License Header Notice in each
-+ * file and include the License file at packager/legal/LICENSE.txt.
-+ *
-+ * GPL Classpath Exception:
-+ * Oracle designates this particular file as subject to the "Classpath"
-+ * exception as provided by Oracle in the GPL Version 2 section of the License
-+ * file that accompanied this code.
-+ *
-+ * Modifications:
-+ * If applicable, add the following below the License Header, with the fields
-+ * enclosed by brackets [] replaced by your own identifying information:
-+ * "Portions Copyright [year] [name of copyright owner]"
-+ *
-+ * Contributor(s):
-+ * If you wish your version of this file to be governed by only the CDDL or
-+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
-+ * elects to include this software in this distribution under the [CDDL or GPL
-+ * Version 2] license." If you don't indicate a single choice of license, a
-+ * recipient has the option to distribute your version of this file under
-+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
-+ * its licensees as provided above. However, if you add GPL Version 2 code
-+ * and therefore, elected the GPL Version 2 license, then the option applies
-+ * only if the new code is made subject to such option by the copyright
-+ * holder.
-+ */
-+
-+package com.sun.mail.imap.protocol;
-+
-+import java.io.*;
-+
-+
-+/**
-+ *
-+ *
-+ from RFC2060
-+
-+5.1.3. Mailbox International Naming Convention
-+
-+ By convention, international mailbox names are specified using a
-+ modified version of the UTF-7 encoding described in [UTF-7]. The
-+ purpose of these modifications is to correct the following problems
-+ with UTF-7:
-+
-+ 1) UTF-7 uses the "+" character for shifting; this conflicts with
-+ the common use of "+" in mailbox names, in particular USENET
-+ newsgroup names.
-+
-+ 2) UTF-7's encoding is BASE64 which uses the "/" character; this
-+ conflicts with the use of "/" as a popular hierarchy delimiter.
-+
-+ 3) UTF-7 prohibits the unencoded usage of "\"; this conflicts with
-+ the use of "\" as a popular hierarchy delimiter.
-+
-+ 4) UTF-7 prohibits the unencoded usage of "~"; this conflicts with
-+ the use of "~" in some servers as a home directory indicator.
-+
-+ 5) UTF-7 permits multiple alternate forms to represent the same
-+ string; in particular, printable US-ASCII chararacters can be
-+ represented in encoded form.
-+
-+ In modified UTF-7, printable US-ASCII characters except for "&"
-+ represent themselves; that is, characters with octet values 0x20-0x25
-+ and 0x27-0x7e. The character "&" (0x26) is represented by the two-
-+ octet sequence "&-".
-+
-+ All other characters (octet values 0x00-0x1f, 0x7f-0xff, and all
-+ Unicode 16-bit octets) are represented in modified BASE64, with a
-+ further modification from [UTF-7] that "," is used instead of "/".
-+ Modified BASE64 MUST NOT be used to represent any printing US-ASCII
-+ character which can represent itself.
-+
-+ "&" is used to shift to modified BASE64 and "-" to shift back to US-
-+ ASCII. All names start in US-ASCII, and MUST end in US-ASCII (that
-+ is, a name that ends with a Unicode 16-bit octet MUST end with a "-
-+ ").
-+
-+
-+
-+
-+
-+Crispin Standards Track [Page 15]
-+
-+RFC 2060 IMAP4rev1 December 1996
-+
-+
-+ For example, here is a mailbox name which mixes English, Japanese,
-+ and Chinese text: ~peter/mail/&ZeVnLIqe-/&U,BTFw-
-+
-+
-+ * This class will do the correct Encoding for the IMAP mailboxes
-+ *
-+ * @author Christopher Cotton
-+ */
-+
-+public class BASE64MailboxEncoder {
-+ protected byte[] buffer = new byte[4];
-+ protected int bufsize = 0;
-+ protected boolean started = false;
-+ protected Writer out = null;
-+
-+
-+ public static String encode(String original) {
-+ BASE64MailboxEncoder base64stream = null;
-+ char origchars[] = original.toCharArray();
-+ int length = origchars.length;
-+ boolean changedString = false;
-+ CharArrayWriter writer = new CharArrayWriter(length);
-+
-+ // loop over all the chars
-+ for(int index = 0; index < length; index++) {
-+ char current = origchars[index];
-+
-+ // octets in the range 0x20-0x25,0x27-0x7e are themselves
-+ // 0x26 "&" is represented as "&-"
-+ if (current >= 0x20 && current <= 0x7e) {
-+ if (base64stream != null) {
-+ base64stream.flush();
-+ }
-+
-+ if (current == '&') {
-+ changedString = true;
-+ writer.write('&');
-+ writer.write('-');
-+ } else {
-+ writer.write(current);
-+ }
-+ } else {
-+
-+ // use a B64MailboxEncoder to write out the other bytes
-+ // as a modified BASE64. The stream will write out
-+ // the beginning '&' and the ending '-' which is part
-+ // of every encoding.
-+
-+ if (base64stream == null) {
-+ base64stream = new BASE64MailboxEncoder(writer);
-+ changedString = true;
-+ }
-+
-+ base64stream.write(current);
-+ }
-+ }
-+
-+
-+ if (base64stream != null) {
-+ base64stream.flush();
-+ }
-+
-+ if (changedString) {
-+ return writer.toString();
-+ } else {
-+ return original;
-+ }
-+ }
-+
-+
-+ /**
-+ * Create a BASE64 encoder
-+ */
-+ public BASE64MailboxEncoder(Writer what) {
-+ out = what;
-+ }
-+
-+ public void write(int c) {
-+ try {
-+ // write out the initial character if this is the first time
-+ if (!started) {
-+ started = true;
-+ out.write('&');
-+ }
-+
-+ // we write each character as a 2 byte unicode character
-+ buffer[bufsize++] = (byte) (c >> 8);
-+ buffer[bufsize++] = (byte) (c & 0xff);
-+
-+ if (bufsize >= 3) {
-+ encode();
-+ bufsize -= 3;
-+ }
-+ } catch (IOException e) {
-+ //e.printStackTrace();
-+ }
-+ }
-+
-+
-+ public void flush() {
-+ try {
-+ // flush any bytes we have
-+ if (bufsize > 0) {
-+ encode();
-+ bufsize = 0;
-+ }
-+
-+ // write the terminating character of the encoding
-+ if (started) {
-+ out.write('-');
-+ started = false;
-+ }
-+ } catch (IOException e) {
-+ //e.printStackTrace();
-+ }
-+ }
-+
-+
-+ protected void encode() throws IOException {
-+ byte a, b, c;
-+ if (bufsize == 1) {
-+ a = buffer[0];
-+ b = 0;
-+ c = 0;
-+ out.write(pem_array[(a >>> 2) & 0x3F]);
-+ out.write(pem_array[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]);
-+ // no padding characters are written
-+ } else if (bufsize == 2) {
-+ a = buffer[0];
-+ b = buffer[1];
-+ c = 0;
-+ out.write(pem_array[(a >>> 2) & 0x3F]);
-+ out.write(pem_array[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]);
-+ out.write(pem_array[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]);
-+ // no padding characters are written
-+ } else {
-+ a = buffer[0];
-+ b = buffer[1];
-+ c = buffer[2];
-+ out.write(pem_array[(a >>> 2) & 0x3F]);
-+ out.write(pem_array[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]);
-+ out.write(pem_array[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]);
-+ out.write(pem_array[c & 0x3F]);
-+
-+ // copy back the extra byte
-+ if (bufsize == 4)
-+ buffer[0] = buffer[3];
-+ }
-+ }
-+
-+ private final static char pem_array[] = {
-+ 'A','B','C','D','E','F','G','H', // 0
-+ 'I','J','K','L','M','N','O','P', // 1
-+ 'Q','R','S','T','U','V','W','X', // 2
-+ 'Y','Z','a','b','c','d','e','f', // 3
-+ 'g','h','i','j','k','l','m','n', // 4
-+ 'o','p','q','r','s','t','u','v', // 5
-+ 'w','x','y','z','0','1','2','3', // 6
-+ '4','5','6','7','8','9','+',',' // 7
-+ };
-+}
diff --git a/debian/patches/fix-build b/debian/patches/fix-build
deleted file mode 100644
index d2fb4a9..0000000
--- a/debian/patches/fix-build
+++ /dev/null
@@ -1,99 +0,0 @@
-Description: Use system libraries in the build system and disable windows dist
-Forwarded: no
-Author: Alexandre Rossi <alexandre.rossi at gmail.com>
-
-Index: davmail-src-3.9.9-1976/build.xml
-===================================================================
---- davmail-src-3.9.9-1976.orig/build.xml 2012-07-10 23:13:26.000000000 +0200
-+++ davmail-src-3.9.9-1976/build.xml 2012-08-23 17:57:56.353182085 +0200
-@@ -3,12 +3,22 @@
- <property name="version" value="3.9.9"/>
-
- <path id="classpath">
-- <pathelement location="classes"/>
-- <fileset dir="lib">
-- <include name="*.jar"/>
-- </fileset>
-+ <pathelement location="${commons-codec}" />
-+ <pathelement location="${servlet-api}" />
-+ <pathelement location="${commons-httpclient}" />
-+ <pathelement location="${htmlcleaner}" />
-+ <pathelement location="${gnumail}" />
-+ <pathelement location="${jcifs}" />
-+ <pathelement location="${jackrabbit-webdav}" />
-+ <pathelement location="${woodstox-core}" />
-+ <pathelement location="${stax2-api}" />
-+ <pathelement location="${swt}" />
- </path>
-
-+ <condition property="windows-dist-run">
-+ <equals arg1="${windows-dist}" arg2="true" />
-+ </condition>
-+
- <target name="clean">
- <delete dir="target"/>
- <delete dir="dist"/>
-@@ -230,8 +240,20 @@
-
- </target>
-
-+ <target name="windows-dist" depends="compile" if="windows-dist-run">
-+
-+ <taskdef name="jsmoothgen"
-+ classname="net.charabia.jsmoothgen.ant.JSmoothGen"
-+ classpathref="classpath"/>
-+ <jsmoothgen project="davmail.jsmooth" skeletonroot="src/jsmooth/skeletons"/>
-+ <jsmoothgen project="davmailconsole.jsmooth" skeletonroot="src/jsmooth/skeletons"/>
-+ <!-- use WinRun4J to generate DavMail service -->
-+ <copy file="src/winrun4j/davmailservice.exe" todir="dist"/>
-+ <jsmoothgen project="davmail64.jsmooth" skeletonroot="src/jsmooth/skeletons"/>
-
-- <target name="dist" depends="compile">
-+ </target>
-+
-+ <target name="dist" depends="compile, windows-dist">
- <property name="release-name" value="${release}-trunk"/>
- <delete dir="dist"/>
- <mkdir dir="dist"/>
-@@ -242,6 +264,7 @@
- <attribute name="Implementation-Title" value="DavMail Gateway"/>
- <attribute name="Implementation-Version" value="${release-name}"/>
- <attribute name="Implementation-Vendor" value="Mickael Guessant"/>
-+ <attribute name="Main-Class" value="davmail.DavGateway"/>
- </section>
- </manifest>
- </jar>
-@@ -259,14 +282,6 @@
- </copy>
- <copy file="src/java/tray48.png" tofile="dist/davmail.png"/>
- <copy file="davmail.sh" todir="dist"/>
-- <taskdef name="jsmoothgen"
-- classname="net.charabia.jsmoothgen.ant.JSmoothGen"
-- classpathref="classpath"/>
-- <jsmoothgen project="davmail.jsmooth" skeletonroot="src/jsmooth/skeletons"/>
-- <jsmoothgen project="davmailconsole.jsmooth" skeletonroot="src/jsmooth/skeletons"/>
-- <!-- use WinRun4J to generate DavMail service -->
-- <copy file="src/winrun4j/davmailservice.exe" todir="dist"/>
-- <jsmoothgen project="davmail64.jsmooth" skeletonroot="src/jsmooth/skeletons"/>
- <zip file="dist/davmail-${release-name}.zip">
- <fileset dir="dist">
- <include name="lib/*.jar"/>
-Index: davmail-src-3.9.9-1976/build.properties
-===================================================================
---- /dev/null 1970-01-01 00:00:00.000000000 +0000
-+++ davmail-src-3.9.9-1976/build.properties 2012-08-23 18:01:27.492683383 +0200
-@@ -0,0 +1,14 @@
-+# Build flags
-+windows-dist = yes
-+
-+# Deps
-+commons-codec = lib/commons-codec-1.3.jar
-+servlet-api = lib/servlet-api.jar
-+commons-httpclient = lib/commons-httpclient-3.1.jar
-+htmlcleaner = lib/htmlcleaner-2.1.jar
-+gnumail = lib/mail-1.4.3.jar
-+jcifs = lib/jcifs-1.3.14.jar
-+jackrabbit-webdav = lib/jackrabbit-webdav-1.4.jar
-+woodstox-core = lib/woodstox-core-asl-4.1.2.jar
-+stax2-api = lib/stax2-api-3.1.1.jar
-+swt = lib/swt-3.7-*.jar
diff --git a/debian/patches/htmlcleaner22 b/debian/patches/htmlcleaner22
deleted file mode 100644
index b7b6c30..0000000
--- a/debian/patches/htmlcleaner22
+++ /dev/null
@@ -1,42 +0,0 @@
-Description: Port to HTMLCleaner 2.2 API
-Forwarded: no
-Author: Alexandre Rossi <alexandre.rossi at gmail.com>
-
-Index: davmail-src-3.9.9-1976/src/java/davmail/exchange/ExchangeSession.java
-===================================================================
---- davmail-src-3.9.9-1976.orig/src/java/davmail/exchange/ExchangeSession.java 2012-07-07 00:55:55.000000000 +0200
-+++ davmail-src-3.9.9-1976/src/java/davmail/exchange/ExchangeSession.java 2012-07-18 11:39:24.887318823 +0200
-@@ -33,8 +33,8 @@
- import org.apache.commons.httpclient.params.HttpClientParams;
- import org.apache.commons.httpclient.util.URIUtil;
- import org.apache.log4j.Logger;
--import org.htmlcleaner.CommentToken;
--import org.htmlcleaner.ContentToken;
-+import org.htmlcleaner.CommentNode;
-+import org.htmlcleaner.ContentNode;
- import org.htmlcleaner.HtmlCleaner;
- import org.htmlcleaner.TagNode;
-
-@@ -440,8 +440,8 @@
- for (Object script : scriptList) {
- List contents = ((TagNode) script).getChildren();
- for (Object content : contents) {
-- if (content instanceof CommentToken) {
-- String scriptValue = ((CommentToken) content).getCommentedContent();
-+ if (content instanceof CommentNode) {
-+ String scriptValue = ((CommentNode) content).getCommentedContent();
- String sUrl = StringUtil.getToken(scriptValue, "var a_sUrl = \"", "\"");
- String sLgn = StringUtil.getToken(scriptValue, "var a_sLgnQS = \"", "\"");
- if (sLgn == null) {
-@@ -454,9 +454,9 @@
- logonMethod = buildLogonMethod(httpClient, newInitMethod);
- }
-
-- } else if (content instanceof ContentToken) {
-+ } else if (content instanceof ContentNode) {
- // Microsoft Forefront Unified Access Gateway redirect
-- String scriptValue = ((ContentToken) content).getContent();
-+ String scriptValue = ((ContentNode) content).getContent().toString();
- String location = StringUtil.getToken(scriptValue, "window.location.replace(\"", "\"");
- if (location != null) {
- LOGGER.debug("Post logon redirect to: " + location);
diff --git a/debian/patches/jackrabbit2 b/debian/patches/jackrabbit2
deleted file mode 100644
index 08ec65b..0000000
--- a/debian/patches/jackrabbit2
+++ /dev/null
@@ -1,151 +0,0 @@
-Description: Port to jackrabbit 2 API
-Forwarded: no
-Author: Alexandre Rossi <alexandre.rossi at gmail.com>
-
-Index: davmail-src-3.9.9-1976/src/java/davmail/exchange/dav/DavExchangeSession.java
-===================================================================
---- davmail-src-3.9.9-1976.orig/src/java/davmail/exchange/dav/DavExchangeSession.java 2012-07-07 00:55:54.000000000 +0200
-+++ davmail-src-3.9.9-1976/src/java/davmail/exchange/dav/DavExchangeSession.java 2012-07-18 11:31:08.331318904 +0200
-@@ -41,6 +41,7 @@
- import org.apache.jackrabbit.webdav.property.DavProperty;
- import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
- import org.apache.jackrabbit.webdav.property.DavPropertySet;
-+import org.apache.jackrabbit.webdav.property.PropEntry;
- import org.w3c.dom.Node;
-
- import javax.mail.MessagingException;
-@@ -1685,7 +1686,7 @@
- // trigger activeSync push event, only if davmail.forceActiveSyncUpdate setting is true
- if ((status == HttpStatus.SC_OK || status == HttpStatus.SC_CREATED) &&
- (Settings.getBooleanProperty("davmail.forceActiveSyncUpdate"))) {
-- ArrayList<DavConstants> propertyList = new ArrayList<DavConstants>();
-+ ArrayList<PropEntry> propertyList = new ArrayList<PropEntry>();
- // Set contentclass to make ActiveSync happy
- propertyList.add(Field.createDavProperty("contentclass", contentClass));
- // ... but also set PR_INTERNET_CONTENT to preserve custom properties
-@@ -1884,7 +1885,7 @@
- } else if (statusCode != HttpStatus.SC_CREATED) {
- throw DavGatewayHttpClientFacade.buildHttpException(method);
- } else if (folderPath.equalsIgnoreCase("/users/" + getEmail() + "/calendar")) {
-- // calendar renamed, need to reload well known folders
-+ // calendar renamed, need to reload well known folders
- getWellKnownFolders();
- }
- } finally {
-@@ -2317,7 +2318,7 @@
- public void processItem(String folderPath, String itemName) throws IOException {
- String eventPath = URIUtil.encodePath(getFolderPath(folderPath) + '/' + convertItemNameToEML(itemName));
- // do not delete calendar messages, mark read and processed
-- ArrayList<DavConstants> list = new ArrayList<DavConstants>();
-+ ArrayList<PropEntry> list = new ArrayList<PropEntry>();
- list.add(Field.createDavProperty("processed", "true"));
- list.add(Field.createDavProperty("read", "1"));
- PropPatchMethod patchMethod = new PropPatchMethod(eventPath, list);
-@@ -2359,7 +2360,7 @@
- }
- // failover for Exchange 2007, use PROPPATCH with forced timezone
- if (fakeEventUrl == null) {
-- ArrayList<DavConstants> propertyList = new ArrayList<DavConstants>();
-+ ArrayList<PropEntry> propertyList = new ArrayList<PropEntry>();
- propertyList.add(Field.createDavProperty("contentclass", "urn:content-classes:appointment"));
- propertyList.add(Field.createDavProperty("outlookmessageclass", "IPM.Appointment"));
- propertyList.add(Field.createDavProperty("instancetype", "0"));
-@@ -2460,8 +2461,8 @@
- return new Contact(getFolderPath(folderPath), itemName, properties, etag, noneMatch).createOrUpdate();
- }
-
-- protected List<DavConstants> buildProperties(Map<String, String> properties) {
-- ArrayList<DavConstants> list = new ArrayList<DavConstants>();
-+ protected List<PropEntry> buildProperties(Map<String, String> properties) {
-+ ArrayList<PropEntry> list = new ArrayList<PropEntry>();
- if (properties != null) {
- for (Map.Entry<String, String> entry : properties.entrySet()) {
- if ("read".equals(entry.getKey())) {
-@@ -2506,7 +2507,7 @@
- public void createMessage(String folderPath, String messageName, HashMap<String, String> properties, MimeMessage mimeMessage) throws IOException {
- String messageUrl = URIUtil.encodePathQuery(getFolderPath(folderPath) + '/' + messageName);
- PropPatchMethod patchMethod;
-- List<DavConstants> davProperties = buildProperties(properties);
-+ List<PropEntry> davProperties = buildProperties(properties);
-
- if (properties != null && properties.containsKey("draft")) {
- // note: draft is readonly after create, create the message first with requested messageFlags
-@@ -2549,7 +2550,7 @@
- if (code == HttpStatus.SC_NOT_ACCEPTABLE) {
- LOGGER.warn("Draft message creation failed, failover to property update. Note: attachments are lost");
-
-- ArrayList<DavConstants> propertyList = new ArrayList<DavConstants>();
-+ ArrayList<PropEntry> propertyList = new ArrayList<PropEntry>();
- propertyList.add(Field.createDavProperty("to", mimeMessage.getHeader("to", ",")));
- propertyList.add(Field.createDavProperty("cc", mimeMessage.getHeader("cc", ",")));
- propertyList.add(Field.createDavProperty("message-id", mimeMessage.getHeader("message-id", ",")));
-@@ -2613,7 +2614,7 @@
- try {
- // need to update bcc after put
- if (mimeMessage.getHeader("Bcc") != null) {
-- davProperties = new ArrayList<DavConstants>();
-+ davProperties = new ArrayList<PropEntry>();
- davProperties.add(Field.createDavProperty("bcc", mimeMessage.getHeader("Bcc", ",")));
- patchMethod = new PropPatchMethod(messageUrl, davProperties);
- try {
-@@ -2705,7 +2706,7 @@
- createMessage(DRAFTS, itemName, properties, mimeMessage);
- MoveMethod method = new MoveMethod(URIUtil.encodePath(getFolderPath(DRAFTS + '/' + itemName)),
- URIUtil.encodePath(getFolderPath(SENDMSG)), false);
-- // set header if saveInSent is disabled
-+ // set header if saveInSent is disabled
- if (!Settings.getBooleanProperty("davmail.smtpSaveInSent", true)) {
- method.setRequestHeader("Saveinsent", "f");
- }
-Index: davmail-src-3.9.9-1976/src/java/davmail/exchange/dav/Field.java
-===================================================================
---- davmail-src-3.9.9-1976.orig/src/java/davmail/exchange/dav/Field.java 2012-06-12 00:01:41.000000000 +0200
-+++ davmail-src-3.9.9-1976/src/java/davmail/exchange/dav/Field.java 2012-07-18 11:25:07.520653273 +0200
-@@ -19,7 +19,7 @@
- package davmail.exchange.dav;
-
- import davmail.util.StringUtil;
--import org.apache.jackrabbit.webdav.DavConstants;
-+import org.apache.jackrabbit.webdav.property.PropEntry;
- import org.apache.jackrabbit.webdav.property.DavPropertyName;
- import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
- import org.apache.jackrabbit.webdav.xml.DomUtil;
-@@ -183,7 +183,7 @@
- createField(URN_SCHEMAS_CALENDAR, "exdate"); // DistinguishedPropertySetType.PublicStrings/urn:schemas:calendar:exdate/PtypMultipleTime
-
- createField(SCHEMAS_MAPI, "reminderset"); // PidLidReminderSet
-- createField(SCHEMAS_MAPI, "reminderdelta"); // PidLidReminderDelta
-+ createField(SCHEMAS_MAPI, "reminderdelta"); // PidLidReminderDelta
-
- // TODO
- createField(SCHEMAS_MAPI, "allattendeesstring"); // PidLidAllAttendeesString
-@@ -197,7 +197,7 @@
- createField(URN_SCHEMAS_CALENDAR, "busystatus"); // DistinguishedPropertySetType.PublicStrings/urn:schemas:calendar:busystatus/String
- createField(URN_SCHEMAS_CALENDAR, "exrule"); // DistinguishedPropertySetType.PublicStrings/urn:schemas:calendar:exrule/PtypMultipleString
- createField(URN_SCHEMAS_CALENDAR, "recurrenceidrange"); // DistinguishedPropertySetType.PublicStrings/urn:schemas:calendar:recurrenceidrange/String
-- createField(URN_SCHEMAS_CALENDAR, "rdate"); // DistinguishedPropertySetType.PublicStrings/urn:schemas:calendar:rdate/PtypMultipleTime
-+ createField(URN_SCHEMAS_CALENDAR, "rdate"); // DistinguishedPropertySetType.PublicStrings/urn:schemas:calendar:rdate/PtypMultipleTime
- createField(URN_SCHEMAS_CALENDAR, "reminderoffset"); // DistinguishedPropertySetType.PublicStrings/urn:schemas:calendar:reminderoffset/Integer
- createField(URN_SCHEMAS_CALENDAR, "timezone"); // DistinguishedPropertySetType.PublicStrings/urn:schemas:calendar:timezone/String
-
-@@ -286,7 +286,7 @@
- createField("description", URN_SCHEMAS_HTTPMAIL, "textdescription"); // PR_BODY 0x1000 String
- createField("im", SCHEMAS_MAPI, "InstMsg"); // InstantMessagingAddress DistinguishedPropertySetType.Address/0x00008062/String
- createField(URN_SCHEMAS_CONTACTS, "othermobile"); // PR_CAR_TELEPHONE_NUMBER 0x3A1E String
-- createField(URN_SCHEMAS_CONTACTS, "internationalisdnnumber"); // PR_ISDN_NUMBER 0x3A2D String
-+ createField(URN_SCHEMAS_CONTACTS, "internationalisdnnumber"); // PR_ISDN_NUMBER 0x3A2D String
-
- createField(URN_SCHEMAS_CONTACTS, "otherTelephone"); // PR_OTHER_TELEPHONE_NUMBER 0x3A21 String
- createField(URN_SCHEMAS_CONTACTS, "homefax"); // PR_HOME_FAX_NUMBER 0x3A25 String
-@@ -527,9 +527,9 @@
- *
- * @param alias DavMail field alias
- * @param value field value
-- * @return DavProperty with value or DavPropertyName for null values
-+ * @return PropEntry with value or DavPropertyName for null values
- */
-- public static DavConstants createDavProperty(String alias, String value) {
-+ public static PropEntry createDavProperty(String alias, String value) {
- Field field = Field.get(alias);
- if (value == null) {
- // return DavPropertyName to remove property
diff --git a/debian/patches/no-osx-tray b/debian/patches/no-osx-tray
deleted file mode 100644
index 3921c6e..0000000
--- a/debian/patches/no-osx-tray
+++ /dev/null
@@ -1,259 +0,0 @@
-Description: Remove OSX specific code.
-Forwarded: not-needed
-Author: Alexandre Rossi <alexandre.rossi at gmail.com>
-
-Index: davmail-src-3.9.9-1976/src/java/davmail/ui/tray/OSXAwtGatewayTray.java
-===================================================================
---- davmail-src-3.9.9-1976.orig/src/java/davmail/ui/tray/OSXAwtGatewayTray.java 2011-04-06 22:01:14.000000000 +0200
-+++ /dev/null 1970-01-01 00:00:00.000000000 +0000
-@@ -1,133 +0,0 @@
--/*
-- * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
-- * Copyright (C) 2009 Mickael Guessant
-- *
-- * This program is free software; you can redistribute it and/or
-- * modify it under the terms of the GNU General Public License
-- * as published by the Free Software Foundation; either version 2
-- * of the License, or (at your option) any later version.
-- *
-- * This program is distributed in the hope that it will be useful,
-- * but WITHOUT ANY WARRANTY; without even the implied warranty of
-- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- * GNU General Public License for more details.
-- *
-- * You should have received a copy of the GNU General Public License
-- * along with this program; if not, write to the Free Software
-- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-- */
--package davmail.ui.tray;
--
--import davmail.BundleMessage;
--import davmail.DavGateway;
--import davmail.ui.OSXAdapter;
--import info.growl.Growl;
--import info.growl.GrowlException;
--import info.growl.GrowlUtils;
--import org.apache.log4j.Level;
--import org.apache.log4j.Logger;
--
--import javax.swing.*;
--import java.awt.*;
--import java.awt.image.BufferedImage;
--import java.awt.image.RenderedImage;
--
--/**
-- * Extended Awt tray with OSX extensions.
-- */
--public class OSXAwtGatewayTray extends AwtGatewayTray {
-- protected static final String OSX_TRAY_ACTIVE_PNG = "osxtray2.png";
-- protected static final String OSX_TRAY_PNG = "osxtray.png";
-- protected static final String OSX_TRAY_INACTIVE_PNG = "osxtrayinactive.png";
--
-- private static final Logger LOGGER = Logger.getLogger(OSXAwtGatewayTray.class);
--
-- /**
-- * Exit DavMail Gateway.
-- *
-- * @return true
-- */
-- @SuppressWarnings({"SameReturnValue", "UnusedDeclaration"})
-- public boolean quit() {
-- DavGateway.stop();
-- // dispose frames
-- settingsFrame.dispose();
-- aboutFrame.dispose();
-- if (logBrokerMonitor != null) {
-- logBrokerMonitor.dispose();
-- }
-- return true;
-- }
--
-- @Override
-- protected void createAndShowGUI() {
-- System.setProperty("apple.laf.useScreenMenuBar", "true");
-- super.createAndShowGUI();
-- trayIcon.removeActionListener(settingsListener);
-- try {
-- OSXAdapter.setAboutHandler(this, AwtGatewayTray.class.getDeclaredMethod("about", (Class[]) null));
-- OSXAdapter.setPreferencesHandler(this, AwtGatewayTray.class.getDeclaredMethod("preferences", (Class[]) null));
-- OSXAdapter.setQuitHandler(this, OSXAwtGatewayTray.class.getDeclaredMethod("quit", (Class[]) null));
-- } catch (Exception e) {
-- DavGatewayTray.error(new BundleMessage("LOG_ERROR_LOADING_OSXADAPTER"), e);
-- }
-- }
--
-- @Override
-- protected String getTrayIconPath() {
-- return OSXAwtGatewayTray.OSX_TRAY_PNG;
-- }
--
-- @Override
-- protected String getTrayIconActivePath() {
-- return OSXAwtGatewayTray.OSX_TRAY_ACTIVE_PNG;
-- }
--
-- @Override
-- protected String getTrayIconInactivePath() {
-- return OSXAwtGatewayTray.OSX_TRAY_INACTIVE_PNG;
-- }
--
-- @Override
-- public void displayMessage(final String message, final Level level) {
-- if (!GrowlUtils.isGrowlLoaded()) {
-- super.displayMessage(message, level);
-- } else {
-- SwingUtilities.invokeLater(new Runnable() {
-- public void run() {
-- if (trayIcon != null) {
-- Icon icon = null;
-- if (level.equals(Level.INFO)) {
-- icon = UIManager.getIcon("OptionPane.informationIcon");
-- } else if (level.equals(Level.WARN)) {
-- icon = UIManager.getIcon("OptionPane.warningIcon");
-- } else if (level.equals(Level.ERROR)) {
-- icon = UIManager.getIcon("OptionPane.errorIcon");
-- }
--
-- if (icon != null && message != null && message.length() > 0) {
-- try {
-- String title = BundleMessage.format("UI_DAVMAIL_GATEWAY");
-- Growl growl = GrowlUtils.getGrowlInstance("DavMail");
-- growl.addNotification(title, true);
-- growl.register();
-- growl.sendNotification(title, title, message, (RenderedImage) getImageForIcon(icon));
-- } catch (GrowlException growlException) {
-- LOGGER.error(growlException);
-- }
-- }
-- trayIcon.setToolTip(BundleMessage.format("UI_DAVMAIL_GATEWAY") + '\n' + message);
-- }
-- }
-- });
-- }
-- }
--
-- protected Image getImageForIcon(Icon icon) {
-- BufferedImage bufferedimage = new BufferedImage(icon.getIconWidth(), icon.getIconHeight(), BufferedImage.TYPE_INT_ARGB);
-- Graphics g = bufferedimage.getGraphics();
-- icon.paintIcon(null, g, 0, 0);
-- g.dispose();
-- return bufferedimage;
-- }
--}
-Index: davmail-src-3.9.9-1976/src/java/davmail/ui/tray/OSXFrameGatewayTray.java
-===================================================================
---- davmail-src-3.9.9-1976.orig/src/java/davmail/ui/tray/OSXFrameGatewayTray.java 2011-07-31 21:26:52.000000000 +0200
-+++ /dev/null 1970-01-01 00:00:00.000000000 +0000
-@@ -1,81 +0,0 @@
--/*
-- * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
-- * Copyright (C) 2009 Mickael Guessant
-- *
-- * This program is free software; you can redistribute it and/or
-- * modify it under the terms of the GNU General Public License
-- * as published by the Free Software Foundation; either version 2
-- * of the License, or (at your option) any later version.
-- *
-- * This program is distributed in the hope that it will be useful,
-- * but WITHOUT ANY WARRANTY; without even the implied warranty of
-- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- * GNU General Public License for more details.
-- *
-- * You should have received a copy of the GNU General Public License
-- * along with this program; if not, write to the Free Software
-- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-- */
--package davmail.ui.tray;
--
--import davmail.BundleMessage;
--import davmail.DavGateway;
--import davmail.ui.OSXAdapter;
--
--import javax.swing.*;
--import java.awt.event.ActionEvent;
--import java.awt.event.ActionListener;
--
--/**
-- * MacOSX specific frame to handle menu
-- */
--public class OSXFrameGatewayTray extends FrameGatewayTray {
--
-- /**
-- * Exit DavMail Gateway.
-- *
-- * @return true
-- */
-- @SuppressWarnings({"SameReturnValue", "UnusedDeclaration"})
-- public boolean quit() {
-- DavGateway.stop();
-- // dispose frames
-- settingsFrame.dispose();
-- aboutFrame.dispose();
-- if (logBrokerMonitor != null) {
-- logBrokerMonitor.dispose();
-- }
-- return true;
-- }
--
-- @Override
-- protected void buildMenu() {
-- // create a popup menu
-- JMenu menu = new JMenu(BundleMessage.format("UI_LOGS"));
-- JMenuBar menuBar = new JMenuBar();
-- menuBar.add(menu);
-- mainFrame.setJMenuBar(menuBar);
--
-- JMenuItem logItem = new JMenuItem(BundleMessage.format("UI_SHOW_LOGS"));
-- logItem.addActionListener(new ActionListener() {
-- public void actionPerformed(ActionEvent e) {
-- showLogs();
-- }
-- });
-- menu.add(logItem);
-- }
--
--
-- @Override
-- protected void createAndShowGUI() {
-- System.setProperty("apple.laf.useScreenMenuBar", "true");
-- super.createAndShowGUI();
-- try {
-- OSXAdapter.setAboutHandler(this, FrameGatewayTray.class.getDeclaredMethod("about", (Class[]) null));
-- OSXAdapter.setPreferencesHandler(this, FrameGatewayTray.class.getDeclaredMethod("preferences", (Class[]) null));
-- OSXAdapter.setQuitHandler(this, OSXFrameGatewayTray.class.getDeclaredMethod("quit", (Class[]) null));
-- } catch (Exception e) {
-- DavGatewayTray.error(new BundleMessage("LOG_ERROR_LOADING_OSXADAPTER"), e);
-- }
-- }
--}
-Index: davmail-src-3.9.9-1976/src/java/davmail/ui/tray/DavGatewayTray.java
-===================================================================
---- davmail-src-3.9.9-1976.orig/src/java/davmail/ui/tray/DavGatewayTray.java 2011-10-29 23:29:19.000000000 +0200
-+++ davmail-src-3.9.9-1976/src/java/davmail/ui/tray/DavGatewayTray.java 2012-07-18 10:36:06.439321059 +0200
-@@ -240,11 +240,7 @@
- if (davGatewayTray == null) {
- try {
- if (SystemTray.isSupported()) {
-- if (isOSX()) {
-- davGatewayTray = new OSXAwtGatewayTray();
-- } else {
-- davGatewayTray = new AwtGatewayTray();
-- }
-+ davGatewayTray = new AwtGatewayTray();
- davGatewayTray.init();
- }
- } catch (NoClassDefFoundError e) {
-@@ -252,12 +248,7 @@
- }
- }
- if (davGatewayTray == null) {
-- if (isOSX()) {
-- // MacOS
-- davGatewayTray = new OSXFrameGatewayTray();
-- } else {
-- davGatewayTray = new FrameGatewayTray();
-- }
-+ davGatewayTray = new FrameGatewayTray();
- davGatewayTray.init();
- }
- }
diff --git a/debian/patches/no-windows-service b/debian/patches/no-windows-service
deleted file mode 100644
index 58cf84a..0000000
--- a/debian/patches/no-windows-service
+++ /dev/null
@@ -1,16 +0,0 @@
-Description: Avoid winrun4j build dependency
-Forwarded: not-needed
-Author: Alexandre Rossi <alexandre.rossi at gmail.com>
-
-Index: davmail-src-3.9.9-1976/build.xml
-===================================================================
---- davmail-src-3.9.9-1976.orig/build.xml 2012-09-05 10:26:05.632581656 +0200
-+++ davmail-src-3.9.9-1976/build.xml 2012-09-05 10:26:47.276583673 +0200
-@@ -58,6 +58,7 @@
- <target name="compile" depends="init">
- <mkdir dir="target/classes"/>
- <javac srcdir="src/java" destdir="target/classes" source="1.5" target="1.5" debug="on" encoding="UTF-8"
-+ excludes="davmail/service/DavService.java"
- includeantruntime="false">
- <classpath>
- <path refid="classpath"/>
diff --git a/debian/patches/series b/debian/patches/series
deleted file mode 100644
index fb35001..0000000
--- a/debian/patches/series
+++ /dev/null
@@ -1,6 +0,0 @@
-htmlcleaner22
-no-windows-service
-no-osx-tray
-base64-enc-dec
-jackrabbit2
-fix-build
diff --git a/debian/postinst b/debian/postinst
deleted file mode 100644
index 3e725ec..0000000
--- a/debian/postinst
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/bin/sh -e
-USER=davmail
-
-if ! getent passwd $USER >/dev/null; then
- adduser --quiet --system --no-create-home --home /var/lib/$USER $USER
-fi
-
-for i in /var/log/$USER.log /var/lib/$USER;
-do
- if ! dpkg-statoverride --list --quiet "$i" >/dev/null; then
- dpkg-statoverride --force --quiet --update --add davmail adm 0755 "$i"
- fi
-done
-
-#DEBHELPER#
diff --git a/debian/rules b/debian/rules
deleted file mode 100755
index 586b7eb..0000000
--- a/debian/rules
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/usr/bin/make -f
-# -*- makefile -*-
-#
-# Uncomment this to turn on verbose mode.
-export DH_VERBOSE=1
-
-# This has to be exported to make some magic below work.
-export DH_OPTIONS
-
-%:
- dh $@ --with javahelper
-
-override_dh_auto_build:
- ant -propertyfile debian/ant.properties
diff --git a/debian/source/format b/debian/source/format
deleted file mode 100644
index 163aaf8..0000000
--- a/debian/source/format
+++ /dev/null
@@ -1 +0,0 @@
-3.0 (quilt)
diff --git a/debian/watch b/debian/watch
deleted file mode 100644
index 5bf235d..0000000
--- a/debian/watch
+++ /dev/null
@@ -1,3 +0,0 @@
-version=3
-
-http://sf.net/davmail/davmail-src-(.+)\.tgz
diff --git a/jsmooth-0.9.9-7-patch/build.xml b/jsmooth-0.9.9-7-patch/build.xml
new file mode 100644
index 0000000..091158c
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/build.xml
@@ -0,0 +1,607 @@
+<project name="jsmoothgen" default="jar" basedir=".">
+
+ <!-- set here the properties specific to your computer -->
+ <!-- see the README.txt file for additional information -->
+ <!-- DO NOT SKIP THIS PART! REALLY! IT WON'T WORK OTHERWISE -->
+ <property name="JDKDIR" value="c:\Program Files\Java\jdk1.5.0_11"/>
+ <property name="RM" value="CMD /C DEL"/>
+
+ <!-- the autodownload skeleton needs the fltk lib to be available -->
+ <!-- Unfortunately, I couldn't find a way to call fltk-config -->
+ <!-- directly from ANT or from the makefile. To find the correct -->
+ <!-- values for your system, run the msys/mingw shell and type -->
+ <!-- "fltk-config __ldflags" and "fltk-config __cxxflags" -->
+ <!-- (replace each _ with a - above, due to xml syntax constraints -->
+ <!-- Then, add the windows path so that the makefile gets it right -->
+ <!-- Below are the default values when you ./configure, make and -->
+ <!-- make install fltk under an msys environement -->
+ <property name="fltk-ldflags" value="-LC:/msys/1.0/local/lib -mwindows -mno-cygwin -lfltk -lole32 -luuid -lcomctl32 -lwsock32"/>
+ <property name="fltk-cxxflags" value="-IC:/msys/1.0/local/include -IC:/msys/1.0/local/include/FL/images -mwindows -DWIN32 -mno-cygwin"/>
+
+ <!-- What's below is optional (needed only if you build a dist) -->
+ <property name="FOP" value="c:/Programs/fop-0.93\fop.bat"/>
+ <property name="DOCBOOKBASE" value="c:\programs\docbook-xsl-1.72.0"/>
+
+ <!-- set here the properties specific to the release -->
+
+ <property name="VERSION" value="0.9.9-7"/>
+
+ <!-- You don't need to modify the properties below -->
+
+ <property environment="env"/>
+ <property name="src" value="src"/>
+ <property name="tmp" value="tmp"/>
+ <property name="docs" value="docs"/>
+
+ <property name="anttask-src" value="ant"/>
+ <property name="classes" value="classes"/>
+ <property name="jsmoothjar" value="jsmoothgen.jar"/>
+ <property name="jsmoothjar-ant" value="jsmoothgen-ant.jar"/>
+ <property name="ant.jar" value="${env.ANT_HOME}/lib/ant.jar"/>
+
+ <property name="distbase" value="dist"/>
+ <property name="dist" value="${distbase}/jsmooth-${VERSION}"/>
+
+ <path id="anttask-compile.class.path">
+ <pathelement path="${ant.jar}"/>
+ <pathelement path="${jsmoothjar}/"/>
+ <pathelement path="lib/JimiProClasses.zip"/>
+ <pathelement path="lib/jox116.jar"/>
+ <pathelement path="lib/dtdparser113.jar"/>
+ </path>
+
+ <path id="anttask.class.path">
+ <pathelement path="${ant.jar}"/>
+ <pathelement path="${jsmoothjar-ant}/"/>
+ <pathelement path="lib/JimiProClasses.zip"/>
+ <pathelement path="lib/jox116.jar"/>
+ <pathelement path="lib/dtdparser113.jar"/>
+ </path>
+
+ <path id="jsmooth.class.path">
+ <fileset dir="lib">
+ <include name="**/*.jar"/>
+ </fileset>
+ <fileset dir="skeletons/jnismooth">
+ <include name="*.jar"/>
+ </fileset>
+ </path>
+
+ <target name="init">
+ <!-- Create the time stamp -->
+ <tstamp/>
+ <!-- Create the build directory structure used by compile -->
+ <mkdir dir="${classes}"/>
+ </target>
+
+ <target depends="init" name="compile">
+ <!-- Compile the java code from ${src} into ${classes} -->
+ <echo>Compiling the application from ${src}</echo>
+ <javac target="1.4" source="1.4" deprecation="yes" destdir="${classes}" srcdir="${src}" debug="true">
+ <classpath refid="jsmooth.class.path"/>
+ </javac>
+ </target>
+
+ <target name="docs" depends="init">
+ <!-- Compile the java code from ${src} into ${classes} -->
+
+ <xslt basedir="${docs}" destdir="${docs}"
+ in="${docs}/jsmooth-doc.xml"
+ out="${docs}/jsmooth-doc.html"
+ style="${DOCBOOKBASE}/html/docbook.xsl" >
+
+ <param name="admon.graphics" expression="1"/>
+ <param name="section.autolabel" expression="1"/>
+ <param name="section.label.includes.component.label" expression="1"/>
+ <param name="chunk.section.depth" expression="2"/>
+ <param name="html.stylesheet" expression="jsmooth.css"/>
+
+ </xslt>
+
+ <xslt basedir="${docs}" destdir="${docs}"
+ in="${docs}/jsmooth-doc.xml"
+ out="${docs}/jsmooth-doc.fo"
+ style="${DOCBOOKBASE}/fo/docbook.xsl" >
+ <param name="admon.graphics" expression="1"/>
+ <param name="section.autolabel" expression="1"/>
+ <param name="section.label.includes.component.label" expression="1"/>
+ </xslt>
+
+ <exec executable="${FOP}" dir="${docs}">
+ <arg value="jsmooth-doc.fo"/>
+ <arg value="jsmooth-doc.pdf"/>
+ </exec>
+
+ </target>
+
+ <target depends="compile" name="jar">
+ <!-- Compile the java code from ${src} into ${classes} -->
+ <copy todir="classes/icons">
+ <fileset dir="${src}/icons"/>
+ </copy>
+ <copy todir="classes/locale">
+ <fileset dir="${src}/locale"/>
+ </copy>
+ <jar basedir="classes" jarfile="${jsmoothjar}" manifest="src\MANIFEST.txt"/>
+ </target>
+
+ <target depends="compile" name="anttask">
+ <!-- Compile the java code from ${src} into ${classes} -->
+ <javac deprecation="yes" destdir="${classes}" srcdir="${anttask-src}" debug="true">
+ <classpath refid="anttask-compile.class.path"/>
+ </javac>
+ <unjar src="lib/jox116.jar" dest="${classes}"/>
+ <unjar src="lib/dtdparser113.jar" dest="${classes}"/>
+ <jar basedir="classes" jarfile="${jsmoothjar-ant}" manifest="src\MANIFEST.txt"/>
+ </target>
+
+ <!--
+ Builds the distribution file for jsmooth.
+ -->
+
+ <target depends="jar" name="test">
+ <echo>Running test class... ${jsmoothjar}</echo>
+ <java classname="net.charabia.jsmoothgen.application.gui.beaneditors.BeanPanel" fork="yes" >
+ <classpath refid="jsmooth.class.path"/>
+ <classpath>
+ <pathelement path="${jsmoothjar}"/>
+ </classpath>
+ </java>
+ </target>
+
+ <target depends="" name="dist">
+
+ <tstamp/>
+ <buildnumber/>
+ <property name="RELEASEINFO" value="Build ${DSTAMP}-${build.number}"/>
+
+ <echo>Building release ${VERSION} - ${RELEASEINFO}</echo>
+
+ <!-- Clean up the directories -->
+ <antcall target="clean"/>
+
+ <!-- Clean up the distribution directory -->
+
+ <delete dir="${distbase}"/>
+ <mkdir dir="${dist}"/>
+ <mkdir dir="${dist}/docs"/>
+ <mkdir dir="${dist}/lib"/>
+ <mkdir dir="${dist}/sample"/>
+ <mkdir dir="${dist}/skeletons"/>
+
+
+ <!-- Compile the classes and create the jars -->
+ <!-- The sources are copied in a temp dir, so that -->
+ <!-- the VERSION and RELEASEINFO data can be replaced -->
+
+ <delete dir="${tmp}"/>
+ <mkdir dir="${tmp}"/>
+ <copy todir="${tmp}/src">
+ <fileset dir="src"/>
+ </copy>
+
+ <replace token="@{VERSION}@" dir="${tmp}/src" value="${VERSION}">
+ <include name="**/*.java"/>
+ </replace>
+ <replace token="@{RELEASEINFO}@" dir="${tmp}/src" value="${RELEASEINFO}">
+ <include name="**/*.java"/>
+ </replace>
+
+ <replace token="@{VERSION}@" dir="${tmp}/src" value="${VERSION}">
+ <include name="**/*.properties"/>
+ </replace>
+ <replace token="@{RELEASEINFO}@" dir="${tmp}/src" value="${RELEASEINFO}">
+ <include name="**/*.properties"/>
+ </replace>
+
+ <ant antfile="build.xml" dir="skeletons/jnismooth/" target="dist"/>
+
+ <antcall target="jar">
+ <param name="src" value="${tmp}/src"/>
+ </antcall>
+
+ <antcall target="anttask">
+ <param name="src" value="${tmp}/src"/>
+ </antcall>
+
+ <!-- Copy the jsmooth ant library -->
+ <copy todir="${dist}/lib" file="jsmoothgen.jar"/>
+ <copy todir="${dist}/lib" file="jsmoothgen-ant.jar"/>
+ <copy todir="${dist}/lib" file="lib/jox116.jar"/>
+ <copy todir="${dist}/lib" file="lib/dtdparser113.jar"/>
+ <copy todir="${dist}/lib" file="lib/l2fprod-common-all.jar"/>
+ <copy todir="${dist}/lib" file="lib/riverlayout.jar"/>
+ <copy todir="${dist}/lib" file="lib/BrowserLauncher2-10.jar"/>
+
+ <!-- Now that the ANT task is available, register it -->
+
+ <taskdef name="jsmoothgen"
+ classname="net.charabia.jsmoothgen.ant.JSmoothGen"
+ classpathref="anttask.class.path"/>
+
+ <!-- Builds the documentation -->
+ <copy todir="${tmp}/docs">
+ <fileset dir="docs"/>
+ </copy>
+ <replace token="@{VERSION}@" dir="${tmp}/docs/" value="${VERSION}">
+ <include name="**/*.xml"/>
+ <include name="**/*.txt"/>
+ <include name="**/*.properties"/>
+ </replace>
+ <replace token="@{RELEASEINFO}@" dir="${tmp}/docs/" value="${RELEASEINFO}">
+ <include name="**/*.xml"/>
+ <include name="**/*.txt"/>
+ <include name="**/*.properties"/>
+ </replace>
+
+ <antcall target="docs">
+ <param name="docs" value="${tmp}/docs"/>
+ </antcall>
+
+ <copy todir="${dist}/docs">
+ <fileset dir="${tmp}/docs">
+ <include name="**/*.pdf"/>
+ </fileset>
+ <fileset dir="${tmp}/docs">
+ <include name="**/*.html"/>
+ </fileset>
+ <fileset dir="${tmp}/docs">
+ <include name="**/*.css"/>
+ </fileset>
+ </copy>
+ <copy todir="${dist}/docs/images">
+ <fileset dir="${tmp}/docs/images"/>
+ </copy>
+
+ <copy todir="${dist}" file="readme.txt"/>
+ <copy todir="${dist}" file="LICENSE.txt"/>
+ <copy todir="${dist}" file="LGPL-LICENSE.txt"/>
+ <copy todir="${dist}" file="GPL-LICENSE.txt"/>
+ <copy tofile="${dist}/Changelog.txt" file="Changelog"/>
+
+ <!-- Compile the wrappers -->
+ <ant antfile="build.xml" dir="skeletons/jnismooth/" target="dist"/>
+ <ant antfile="build.xml" dir="skeletons/samplejar/" target="dist"/>
+ <ant antfile="build.xml" dir="skeletons/consolewrapper/samplejar/" target="dist"/>
+
+ <mkdir dir="${dist}/jni"/>
+ <copy todir="${dist}/jni" file="skeletons/jnismooth/jnismooth.jar"/>
+ <javadoc packagenames="jsmooth.*"
+ sourcepath="skeletons/jnismooth/src"
+ destdir="${dist}/docs/jniapi"
+ author="true"
+ version="true"
+ use="true"
+ windowtitle="JSmooth JNI API" />
+
+ <!-- build all 32bit wrappers -->
+ <mkdir dir="${dist}/skeletons/windowed-wrapper"/>
+ <mkdir dir="${dist}/skeletons/console-wrapper"/>
+ <mkdir dir="${dist}/skeletons/autodownload-wrapper"/>
+ <mkdir dir="${dist}/skeletons/winservice-wrapper"/>
+ <antcall target="compileskels">
+ <param name="skelflags" value="-static-libgcc"/>
+ </antcall>
+ <copy todir="${dist}/skeletons/windowed-wrapper" file="skeletons/simplewrap/jwrap.exe"/>
+ <copy todir="${dist}/skeletons/windowed-wrapper" file="skeletons/simplewrap/description.skel"/>
+ <copy todir="${dist}/skeletons/console-wrapper" file="skeletons/consolewrapper/consolewrapper.exe"/>
+ <copy todir="${dist}/skeletons/console-wrapper" file="skeletons/consolewrapper/description.skel"/>
+ <copy todir="${dist}/skeletons/autodownload-wrapper" file="skeletons/autodownload/autodownload.skel"/>
+ <copy todir="${dist}/skeletons/autodownload-wrapper" file="skeletons/autodownload/customdownload.skel"/>
+ <copy todir="${dist}/skeletons/autodownload-wrapper" file="skeletons/autodownload/autodownload.exe"/>
+ <copy todir="${dist}/skeletons/winservice-wrapper" file="skeletons/winservice/description.skel"/>
+ <copy todir="${dist}/skeletons/winservice-wrapper" file="skeletons/winservice/winservice.exe"/>
+ <exec executable="c:\mingw-w32\bin\strip.exe" dir="${dist}/skeletons/console-wrapper">
+ <arg value="consolewrapper.exe"/>
+ </exec>
+ <exec executable="c:\mingw-w32\bin\strip.exe" dir="${dist}/skeletons/windowed-wrapper">
+ <arg value="jwrap.exe"/>
+ </exec>
+ <exec executable="c:\mingw-w32\bin\strip.exe" dir="${dist}/skeletons/autodownload-wrapper">
+ <arg value="autodownload.exe"/>
+ </exec>
+ <exec executable="c:\mingw-w32\bin\strip.exe" dir="${dist}/skeletons/winservice-wrapper">
+ <arg value="winservice.exe"/>
+ </exec>
+
+ <!-- build all 64bits wrappers -->
+ <antcall target="clean64"/>
+ <mkdir dir="${dist}/skeletons/windowed-wrapper64"/>
+ <mkdir dir="${dist}/skeletons/console-wrapper64"/>
+ <!-- not available for x64
+ <mkdir dir="${dist}/skeletons/autodownload-wrapper64"/>
+ -->
+ <mkdir dir="${dist}/skeletons/winservice-wrapper64"/>
+ <antcall target="compileskels64">
+ <param name="skelflags" value="-static-libgcc"/>
+ </antcall>
+ <copy todir="${dist}/skeletons/windowed-wrapper64" file="skeletons/simplewrap/jwrap.exe"/>
+ <copy todir="${dist}/skeletons/windowed-wrapper64" file="skeletons/simplewrap/description64.skel"/>
+ <copy todir="${dist}/skeletons/console-wrapper64" file="skeletons/consolewrapper/consolewrapper.exe"/>
+ <copy todir="${dist}/skeletons/console-wrapper64" file="skeletons/consolewrapper/description64.skel"/>
+ <!-- not available for x64
+ <copy todir="${dist}/skeletons/autodownload-wrapper64" file="skeletons/autodownload/autodownload64.skel"/>
+ <copy todir="${dist}/skeletons/autodownload-wrapper64" file="skeletons/autodownload/customdownload64.skel"/>
+ <copy todir="${dist}/skeletons/autodownload-wrapper64" file="skeletons/autodownload/autodownload.exe"/>
+ -->
+ <copy todir="${dist}/skeletons/winservice-wrapper64" file="skeletons/winservice/description64.skel"/>
+ <copy todir="${dist}/skeletons/winservice-wrapper64" file="skeletons/winservice/winservice.exe"/>
+ <exec executable="c:\mingw-w64\bin\strip.exe" dir="${dist}/skeletons/console-wrapper64">
+ <arg value="consolewrapper.exe"/>
+ </exec>
+ <exec executable="c:\mingw-w64\bin\strip.exe" dir="${dist}/skeletons/windowed-wrapper64">
+ <arg value="jwrap.exe"/>
+ </exec>
+ <!-- not available for x64
+ <exec executable="c:\mingw-w64\bin\strip.exe" dir="${dist}/skeletons/autodownload-wrapper64">
+ <arg value="autodownload.exe"/>
+ </exec>
+ -->
+ <exec executable="c:\mingw-w64\bin\strip.exe" dir="${dist}/skeletons/winservice-wrapper64">
+ <arg value="winservice.exe"/>
+ </exec>
+
+ <!-- Builds the jsmooth executable -->
+ <jsmoothgen project="jsprj/jsmoothgen.jsmooth" skeletonroot="${dist}/skeletons"/>
+ <jsmoothgen project="jsprj/jsmoothcmd.jsmooth" skeletonroot="${dist}/skeletons"/>
+ <jsmoothgen project="jsprj/jsmoothcmd64.jsmooth" skeletonroot="${dist}/skeletons"/>
+ <ant antfile="build.xml" dir="sample" target="dist"/>
+ <jsmoothgen project="jsprj/prooftest.jsmooth" skeletonroot="${dist}/skeletons"/>
+ <jsmoothgen project="jsprj/prooftest64.jsmooth" skeletonroot="${dist}/skeletons"/>
+
+ <copy todir="${dist}" file="jsmoothgen.exe"/>
+ <copy todir="${dist}" file="jsmoothcmd.exe"/>
+ <copy todir="${dist}" file="jsmoothcmd64.exe"/>
+ <copy todir="${dist}" file="proof-test.exe"/>
+ <copy todir="${dist}" file="proof-test64.exe"/>
+ <copy todir="${dist}/sample">
+ <fileset dir="sample"/>
+ </copy>
+
+ </target>
+
+ <target name="compileskel" depends="">
+ <echo>Making ${skelname} ${target}</echo>
+ <exec executable="c:\mingw-w32\bin\make.exe" dir="skeletons/${skelname}">
+ <env key="PATH" value="C:\mingw-w32\bin"/>
+ <arg value="-f"/>
+ <arg value="Makefile.win"/>
+ <arg value="${target}"/>
+ <arg value='CUSTOMFLAGS=${skelflags}'/>
+ <arg value='"MINGW=c:/mingw-w32"'/>
+ <arg value='"TARGET=i686-w64-mingw32"'/>
+ <arg value='"JDK=${JDKDIR}"'/>
+ <arg value='"RM=${RM}"'/>
+ <arg value='"FLTK-LDFLAGS=${fltk-ldflags}"'/>
+ <arg value='"FLTK-CXXFLAGS=${fltk-cxxflags}"'/>
+ </exec>
+ </target>
+
+ <target name="compileskel64" depends="">
+ <echo>Making ${skelname} ${target}</echo>
+ <exec executable="c:\mingw-w64\bin\make.exe" dir="skeletons/${skelname}">
+ <env key="PATH" value="C:\mingw-w64\bin"/>
+ <arg value="-f"/>
+ <arg value="Makefile.win"/>
+ <arg value="${target}"/>
+ <arg value='CUSTOMFLAGS=${skelflags}'/>
+ <arg value='"MINGW=c:/mingw-w64"'/>
+ <arg value='"TARGET=x86_64-w64-mingw32"'/>
+ <arg value='"JDK=${JDKDIR}"'/>
+ <arg value='"RM=${RM}"'/>
+ <arg value='"FLTK-LDFLAGS=${fltk-ldflags}"'/>
+ <arg value='"FLTK-CXXFLAGS=${fltk-cxxflags}"'/>
+ </exec>
+ </target>
+
+ <target name="compileskels" depends="">
+ <ant dir="skeletons/simplewrap/samplejar" />
+ <ant dir="skeletons/consolewrapper/samplejar" />
+
+ <antcall target="compileskel">
+ <param name="skelname" value="util-core"/>
+ <param name="skelflags" value="${skelflags}"/>
+ <param name="target" value="all"/>
+ </antcall>
+ <antcall target="compileskel">
+ <param name="skelname" value="util-net"/>
+ <param name="skelflags" value="${skelflags}"/>
+ <param name="target" value="all"/>
+ </antcall>
+ <antcall target="compileskel">
+ <param name="skelname" value="commonjava"/>
+ <param name="skelflags" value="${skelflags}"/>
+ <param name="target" value="all"/>
+ </antcall>
+ <antcall target="compileskel">
+ <param name="skelname" value="consolewrapper"/>
+ <param name="skelflags" value="${skelflags}"/>
+ <param name="target" value="all"/>
+ </antcall>
+ <antcall target="compileskel">
+ <param name="skelname" value="simplewrap"/>
+ <param name="skelflags" value="${skelflags}"/>
+ <param name="target" value="all"/>
+ </antcall>
+ <antcall target="compileskel">
+ <param name="skelname" value="autodownload"/>
+ <param name="skelflags" value="${skelflags}"/>
+ <param name="target" value="all"/>
+ </antcall>
+ <antcall target="compileskel">
+ <param name="skelname" value="winservice"/>
+ <param name="skelflags" value="${skelflags}"/>
+ <param name="target" value="all"/>
+ </antcall>
+ </target>
+
+ <target name="clean">
+ <delete dir="${classes}"/>
+ <delete dir="${distbase}"/>
+ <delete dir="${tmp}"/>
+ <delete file="${jsmoothjar}"/>
+ <delete file="${jsmoothjar-ant}"/>
+
+ <ant antfile="build.xml" dir="sample" target="clean"/>
+
+ <antcall target="compileskel">
+ <param name="skelname" value="util-core"/>
+ <param name="skelflags" value="${skelflags}"/>
+ <param name="target" value="clean"/>
+ </antcall>
+ <antcall target="compileskel">
+ <param name="skelname" value="util-net"/>
+ <param name="skelflags" value="${skelflags}"/>
+ <param name="target" value="clean"/>
+ </antcall>
+ <antcall target="compileskel">
+ <param name="skelname" value="commonjava"/>
+ <param name="skelflags" value="${skelflags}"/>
+ <param name="target" value="clean"/>
+ </antcall>
+ <antcall target="compileskel">
+ <param name="skelname" value="consolewrapper"/>
+ <param name="skelflags" value="${skelflags}"/>
+ <param name="target" value="clean"/>
+ </antcall>
+ <antcall target="compileskel">
+ <param name="skelname" value="simplewrap"/>
+ <param name="skelflags" value="${skelflags}"/>
+ <param name="target" value="clean"/>
+ </antcall>
+ <antcall target="compileskel">
+ <param name="skelname" value="autodownload"/>
+ <param name="skelflags" value="${skelflags}"/>
+ <param name="target" value="clean"/>
+ </antcall>
+ <antcall target="compileskel">
+ <param name="skelname" value="winservice"/>
+ <param name="skelflags" value="${skelflags}"/>
+ <param name="target" value="clean"/>
+ </antcall>
+
+ <delete file="jsmoothgen.exe"/>
+ <delete file="jsmoothcmd.exe"/>
+ <delete file="proof-test.exe"/>
+ </target>
+
+ <target name="compileskels64" depends="">
+ <ant dir="skeletons/simplewrap/samplejar" />
+ <ant dir="skeletons/consolewrapper/samplejar" />
+
+ <antcall target="compileskel64">
+ <param name="skelname" value="util-core"/>
+ <param name="skelflags" value="${skelflags}"/>
+ <param name="target" value="all"/>
+ </antcall>
+ <!-- not available for x64
+ <antcall target="compileskel64">
+ <param name="skelname" value="util-net"/>
+ <param name="skelflags" value="${skelflags}"/>
+ <param name="target" value="all"/>
+ </antcall>
+ -->
+ <antcall target="compileskel64">
+ <param name="skelname" value="commonjava"/>
+ <param name="skelflags" value="${skelflags}"/>
+ <param name="target" value="all"/>
+ </antcall>
+ <antcall target="compileskel64">
+ <param name="skelname" value="consolewrapper"/>
+ <param name="skelflags" value="${skelflags}"/>
+ <param name="target" value="all"/>
+ </antcall>
+ <antcall target="compileskel64">
+ <param name="skelname" value="simplewrap"/>
+ <param name="skelflags" value="${skelflags}"/>
+ <param name="target" value="all"/>
+ </antcall>
+ <!-- not available for x64
+ <antcall target="compileskel64">
+ <param name="skelname" value="autodownload"/>
+ <param name="skelflags" value="${skelflags}"/>
+ <param name="target" value="all"/>
+ </antcall>
+ -->
+ <antcall target="compileskel64">
+ <param name="skelname" value="winservice"/>
+ <param name="skelflags" value="${skelflags}"/>
+ <param name="target" value="all"/>
+ </antcall>
+ </target>
+
+ <target name="clean64">
+ <antcall target="compileskel64">
+ <param name="skelname" value="util-core"/>
+ <param name="skelflags" value="${skelflags}"/>
+ <param name="target" value="clean"/>
+ </antcall>
+ <antcall target="compileskel64">
+ <param name="skelname" value="util-net"/>
+ <param name="skelflags" value="${skelflags}"/>
+ <param name="target" value="clean"/>
+ </antcall>
+ <antcall target="compileskel64">
+ <param name="skelname" value="commonjava"/>
+ <param name="skelflags" value="${skelflags}"/>
+ <param name="target" value="clean"/>
+ </antcall>
+ <antcall target="compileskel64">
+ <param name="skelname" value="consolewrapper"/>
+ <param name="skelflags" value="${skelflags}"/>
+ <param name="target" value="clean"/>
+ </antcall>
+ <antcall target="compileskel64">
+ <param name="skelname" value="simplewrap"/>
+ <param name="skelflags" value="${skelflags}"/>
+ <param name="target" value="clean"/>
+ </antcall>
+ <antcall target="compileskel64">
+ <param name="skelname" value="autodownload"/>
+ <param name="skelflags" value="${skelflags}"/>
+ <param name="target" value="clean"/>
+ </antcall>
+ <antcall target="compileskel64">
+ <param name="skelname" value="winservice"/>
+ <param name="skelflags" value="${skelflags}"/>
+ <param name="target" value="clean"/>
+ </antcall>
+
+ <delete file="jsmoothgen.exe"/>
+ <delete file="jsmoothcmd.exe"/>
+ <delete file="proof-test.exe"/>
+ </target>
+
+ <target name="run" depends="jar">
+ <java classname="net.charabia.jsmoothgen.application.gui.MainFrame" fork="yes" >
+ <classpath refid="jsmooth.class.path"/>
+ <classpath>
+ <pathelement path="${jsmoothjar}"/>
+ </classpath>
+ <arg value="c:/temp/test/testservice.jsmooth"/>
+ </java>
+ </target>
+
+ <target name="runskeletoneditor" depends="jar">
+ <java classname="net.charabia.jsmoothgen.skeleton.SkeletonEditor" fork="yes" >
+ <classpath refid="jsmooth.class.path"/>
+ <classpath>
+ <pathelement path="${jsmoothjar}"/>
+ </classpath>
+ </java>
+ </target>
+
+ <target name="swt-run" depends="jar">
+ <java classname="net.charabia.jsmoothgen.application.swtgui.JSmoothApplication"
+ fork="yes" >
+
+ <classpath refid="jsmooth.class.path"/>
+ <classpath>
+ <pathelement path="${jsmoothjar}"/>
+ </classpath>
+ <jvmarg value="-Djava.library.path=lib/os"/>
+ </java>
+ </target>
+
+</project>
+
diff --git a/jsmooth-0.9.9-7-patch/jsprj/jsmoothcmd.jsmooth b/jsmooth-0.9.9-7-patch/jsprj/jsmoothcmd.jsmooth
new file mode 100644
index 0000000..b9b4ec0
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/jsprj/jsmoothcmd.jsmooth
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<jsmoothproject>
+<JVMSearchPath>registry</JVMSearchPath>
+<JVMSearchPath>javahome</JVMSearchPath>
+<JVMSearchPath>jrepath</JVMSearchPath>
+<JVMSearchPath>jdkpath</JVMSearchPath>
+<JVMSearchPath>exepath</JVMSearchPath>
+<JVMSearchPath>jview</JVMSearchPath>
+<classPath>..\lib\jox116.jar</classPath>
+<classPath>..\lib\JimiProClasses.zip</classPath>
+<classPath>..\lib\dtdparser113.jar</classPath>
+<classPath>..\lib\jsmoothgen.jar</classPath>
+<embeddedJar>false</embeddedJar>
+<executableName>..\jsmoothcmd.exe</executableName>
+<iconLocation>..\src\icons\gnome-application-x-jar-bm.gif</iconLocation>
+<initialMemoryHeap>-1</initialMemoryHeap>
+<javaProperties>
+<name>jsmooth.basedir</name>
+<value>${EXECUTABLEPATH} </value>
+</javaProperties>
+<mainClassName>net.charabia.jsmoothgen.application.cmdline.CommandLine</mainClassName>
+<maximumMemoryHeap>-1</maximumMemoryHeap>
+<maximumVersion></maximumVersion>
+<minimumVersion>1.4</minimumVersion>
+<uacRequireAdministrator>0</uacRequireAdministrator>
+<skeletonName>Console Wrapper</skeletonName>
+<skeletonProperties>
+<key>Message</key>
+<value>This program needs Java 1.4 or higher to run.
+Please download it at http://www.java.com</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>PressKey</key>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>Debug</key>
+<value>0</value>
+</skeletonProperties>
+</jsmoothproject>
diff --git a/jsmooth-0.9.9-7-patch/jsprj/jsmoothcmd64.jsmooth b/jsmooth-0.9.9-7-patch/jsprj/jsmoothcmd64.jsmooth
new file mode 100644
index 0000000..acde98e
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/jsprj/jsmoothcmd64.jsmooth
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<jsmoothproject>
+<JVMSearchPath>registry</JVMSearchPath>
+<JVMSearchPath>javahome</JVMSearchPath>
+<JVMSearchPath>jrepath</JVMSearchPath>
+<JVMSearchPath>jdkpath</JVMSearchPath>
+<JVMSearchPath>exepath</JVMSearchPath>
+<JVMSearchPath>jview</JVMSearchPath>
+<classPath>..\lib\jox116.jar</classPath>
+<classPath>..\lib\JimiProClasses.zip</classPath>
+<classPath>..\lib\dtdparser113.jar</classPath>
+<classPath>..\lib\jsmoothgen.jar</classPath>
+<embeddedJar>false</embeddedJar>
+<executableName>..\jsmoothcmd64.exe</executableName>
+<iconLocation>..\src\icons\gnome-application-x-jar-bm.gif</iconLocation>
+<initialMemoryHeap>-1</initialMemoryHeap>
+<javaProperties>
+<name>jsmooth.basedir</name>
+<value>${EXECUTABLEPATH} </value>
+</javaProperties>
+<mainClassName>net.charabia.jsmoothgen.application.cmdline.CommandLine</mainClassName>
+<maximumMemoryHeap>-1</maximumMemoryHeap>
+<maximumVersion></maximumVersion>
+<minimumVersion>1.4</minimumVersion>
+<uacRequireAdministrator>0</uacRequireAdministrator>
+<skeletonName>Console Wrapper x64</skeletonName>
+<skeletonProperties>
+<key>Message</key>
+<value>This program needs Java 1.4 or higher to run.
+Please download it at http://www.java.com</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>PressKey</key>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>Debug</key>
+<value>0</value>
+</skeletonProperties>
+</jsmoothproject>
diff --git a/jsmooth-0.9.9-7-patch/jsprj/jsmoothgen.jsmooth b/jsmooth-0.9.9-7-patch/jsprj/jsmoothgen.jsmooth
new file mode 100644
index 0000000..ec86efc
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/jsprj/jsmoothgen.jsmooth
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<jsmoothproject>
+<JVMSearchPath>registry</JVMSearchPath>
+<JVMSearchPath>exepath</JVMSearchPath>
+<JVMSearchPath>javahome</JVMSearchPath>
+<JVMSearchPath>jrepath</JVMSearchPath>
+<JVMSearchPath>jdkpath</JVMSearchPath>
+<JVMSearchPath>jview</JVMSearchPath>
+<classPath>..\lib\jox116.jar</classPath>
+<classPath>..\lib\dtdparser113.jar</classPath>
+<classPath>..\lib\jsmoothgen.jar</classPath>
+<classPath>..\lib\l2fprod-common-all.jar</classPath>
+<classPath>..\jsmoothgen.jar</classPath>
+<classPath>..\lib\riverlayout.jar</classPath>
+<classPath>..\lib\BrowserLauncher2-10.jar</classPath>
+<currentDirectory>${EXECUTABLEPATH}</currentDirectory>
+<embeddedJar>false</embeddedJar>
+<executableName>..\JSmoothGen.exe</executableName>
+<iconLocation>..\src\icons\gnome-application-x-jar-bm.gif</iconLocation>
+<initialMemoryHeap>-1</initialMemoryHeap>
+<mainClassName>net.charabia.jsmoothgen.application.gui.MainFrame</mainClassName>
+<maximumMemoryHeap>-1</maximumMemoryHeap>
+<maximumVersion></maximumVersion>
+<minimumVersion>1.4</minimumVersion>
+<uacRequireAdministrator>0</uacRequireAdministrator>
+<skeletonName>Autodownload Wrapper</skeletonName>
+<skeletonProperties>
+<key>Message</key>
+<value>Java 1.4 or above has not been found on your computer. Do you want to download a compatible Java Environment?</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>DownloadURL</key>
+<value>http://java.sun.com/update/1.6.0/jinstall-6-windows-i586.cab</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>SingleProcess</key>
+<value>1</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>SingleInstance</key>
+<value>1</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>JniSmooth</key>
+<value>1</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>Debug</key>
+<value>0</value>
+</skeletonProperties>
+</jsmoothproject>
diff --git a/jsmooth-0.9.9-7-patch/jsprj/prooftest.jsmooth b/jsmooth-0.9.9-7-patch/jsprj/prooftest.jsmooth
new file mode 100644
index 0000000..fd56942
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/jsprj/prooftest.jsmooth
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<jsmoothproject>
+<JVMSearchPath>registry</JVMSearchPath>
+<JVMSearchPath>javahome</JVMSearchPath>
+<JVMSearchPath>jrepath</JVMSearchPath>
+<JVMSearchPath>jdkpath</JVMSearchPath>
+<JVMSearchPath>exepath</JVMSearchPath>
+<JVMSearchPath>jview</JVMSearchPath>
+<arguments>simpleargument "multiple tokens" c:\my\test\here</arguments>
+<embeddedJar>true</embeddedJar>
+<executableName>..\proof-test.exe</executableName>
+<iconLocation>..\src\icons\stock_web-support.png</iconLocation>
+<initialMemoryHeap>-1</initialMemoryHeap>
+<jarLocation>..\sample\sample.jar</jarLocation>
+<mainClassName>JSmoothPropertiesDisplayer</mainClassName>
+<maximumMemoryHeap>-1</maximumMemoryHeap>
+<maximumVersion></maximumVersion>
+<minimumVersion>1.1</minimumVersion>
+<uacRequireAdministrator>0</uacRequireAdministrator>
+<skeletonName>Console Wrapper</skeletonName>
+<skeletonProperties>
+<key>Message</key>
+<value>This program needs Java 1.4 or higher to run.
+Please download it at http://www.java.com</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>PressKey</key>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>Debug</key>
+<value>1</value>
+</skeletonProperties>
+</jsmoothproject>
diff --git a/jsmooth-0.9.9-7-patch/jsprj/prooftest64.jsmooth b/jsmooth-0.9.9-7-patch/jsprj/prooftest64.jsmooth
new file mode 100644
index 0000000..9b50eca
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/jsprj/prooftest64.jsmooth
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<jsmoothproject>
+<JVMSearchPath>registry</JVMSearchPath>
+<JVMSearchPath>javahome</JVMSearchPath>
+<JVMSearchPath>jrepath</JVMSearchPath>
+<JVMSearchPath>jdkpath</JVMSearchPath>
+<JVMSearchPath>exepath</JVMSearchPath>
+<JVMSearchPath>jview</JVMSearchPath>
+<arguments>simpleargument "multiple tokens" c:\my\test\here</arguments>
+<embeddedJar>true</embeddedJar>
+<executableName>..\proof-test64.exe</executableName>
+<iconLocation>..\src\icons\stock_web-support.png</iconLocation>
+<initialMemoryHeap>-1</initialMemoryHeap>
+<jarLocation>..\sample\sample.jar</jarLocation>
+<mainClassName>JSmoothPropertiesDisplayer</mainClassName>
+<maximumMemoryHeap>-1</maximumMemoryHeap>
+<maximumVersion></maximumVersion>
+<minimumVersion>1.1</minimumVersion>
+<uacRequireAdministrator>0</uacRequireAdministrator>
+<skeletonName>Console Wrapper x64</skeletonName>
+<skeletonProperties>
+<key>Message</key>
+<value>This program needs Java 1.4 or higher to run.
+Please download it at http://www.java.com</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>PressKey</key>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<key>Debug</key>
+<value>1</value>
+</skeletonProperties>
+</jsmoothproject>
diff --git a/jsmooth-0.9.9-7-patch/sample/build.xml b/jsmooth-0.9.9-7-patch/sample/build.xml
new file mode 100644
index 0000000..47c5390
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/sample/build.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project basedir="." default="dist" name="jsmooth-sample">
+
+ <!-- set global properties for this build -->
+ <property name="src" value="src"/>
+ <property name="classes" value="classes"/>
+
+ <path id="build.class.path">
+ <pathelement path="../skeletons/jnismooth/jnismooth.jar"/>
+ </path>
+
+ <target name="init">
+ <!-- Create the time stamp -->
+ <tstamp/>
+ <!-- Create the build directory structure used by compile -->
+ <mkdir dir="${classes}"/>
+ </target>
+
+ <target depends="init" name="compile">
+ <!-- Compile the java code from ${src} into ${classes} -->
+ <javac deprecation="yes" destdir="${classes}" srcdir="${src}" source="1.4" target="1.4">
+ <classpath refid="build.class.path"/>
+ </javac>
+ </target>
+
+ <target depends="compile" name="dist">
+ <!-- Compile the java code from ${src} into ${classes} -->
+ <jar basedir="classes" jarfile="sample.jar" manifest="src\MANIFEST.txt"/>
+ </target>
+
+ <target name="run" depends="dist">
+ <java classname="JSmoothPropertiesDisplayer" fork="yes" >
+ <classpath refid="build.class.path"/>
+ <classpath>
+ <pathelement path="sample.jar"/>
+ </classpath>
+ </java>
+ </target>
+
+ <target name="clean">
+ <delete dir="${classes}"/>
+ <delete file="sample.jar"/>
+ </target>
+</project>
diff --git a/jsmooth-0.9.9-7-patch/skeletons/autodownload/Makefile.win b/jsmooth-0.9.9-7-patch/skeletons/autodownload/Makefile.win
new file mode 100644
index 0000000..eb21df7
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/autodownload/Makefile.win
@@ -0,0 +1,46 @@
+# Project: util-core
+
+PROJECTNAME=autodownload
+##libmscabd_la_SOURCES = mspack/system.c mspack/cabd.c mspack/lzxd.c mspack/mszipd.c mspack/qtmd.c
+##libmscabd_la_OBJS = system.o cabd.o lzxd.o mszipd.o qtmd.o
+
+SHELL = /bin/sh
+RM = rm -f
+CPP = ${MINGW}/bin/${TARGET}-g++.exe
+CC = ${MINGW}/bin/${TARGET}-gcc.exe
+WINDRES = windres.exe
+MINGW =
+RES = JWrap_private.res
+LINKOBJ = main.o execcab.o execexe.o $(RES)
+OBJ = main.o execcab.o execexe.o $(libmscabd_la_OBJS) $(RES)
+FLTK-LDFLAGS = $(shell fltk-config --ldflags --use-images)
+FLTK-CXXFLAGS = $(shell fltk-config --cxxflags --use-images)
+##LINKLIBS = ../commonjava/CommonJava.a ../util-core/util-core.a mspack.a ../util-net/util-net.a
+LINKLIBS = ../commonjava/CommonJava.a ../util-net/util-net.a ../util-core/util-core.a
+LIBS = -static-libgcc -L. -L"$(MINGW)/lib" -lws2_32 -L"${MINGW}/bin/libgcc_s_sjlj-1.dll" -L"/lib" -L"../commonjava" -mwindows -L"../util-core" -L"../util-net" $(LINKLIBS) $(FLTK-LDFLAGS) -lsetupapi -lwininet
+INCS = -I"$(MINGW)/include" -I"$(JDK)/include" -I"$(JDK)/include/win32"
+CXXINCS = -I"$(MINGW)/include/c++" -I"$(MINGW)/include/c++/mingw32" -I"$(MINGW)/include/c++/backward" -I"$(MINGW)/include" -I"$(JDK)/include" -I"$(JDK)/include/win32" -I"../commonjava" -I"../util-core" -I"../util-net" $(FLTK-CXXFLAGS)
+BIN = autodownload.exe
+CXXFLAGS = $(CUSTOMFLAGS) $(CXXINCS) -DJDK="$(JDK)"
+CFLAGS = $(INCS)
+
+.PHONY: all all-before all-after clean clean-custom $(PROJECTNAME)
+
+all: all-before $(BIN) all-after
+
+clean: clean-custom
+ $(RM) $(OBJ) $(BIN) JWrap_private.res
+
+$(BIN): $(LINKOBJ) $(LINKLIBS)
+ $(CPP) $(CXXFLAGS) $(LINKOBJ) $(LIBS) -o $(BIN)
+
+#mspack.a: $(libmscabd_la_SOURCES)
+# $(CC) -c -Imspack $(libmscabd_la_SOURCES)
+# ar r mspack.a $(libmscabd_la_OBJS)
+# ranlib mspack.a
+
+JWrap_private.res: JWrap_private.rc mainres.rc
+ $(WINDRES) -i JWrap_private.rc -J rc -o JWrap_private.res -O coff
+
+main.o: main.cpp
+ $(CPP) -c main.cpp $(CXXFLAGS)
diff --git a/jsmooth-0.9.9-7-patch/skeletons/autodownload/autodownload64.skel b/jsmooth-0.9.9-7-patch/skeletons/autodownload/autodownload64.skel
new file mode 100644
index 0000000..4841107
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/autodownload/autodownload64.skel
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<jsmoothskeleton>
+<debug>false</debug>
+<description>SKEL_AUTODOWNLOAD_PROPERTY_DESCRIPTION</description>
+<executableName>autodownload.exe</executableName>
+<resourceCategory>JAVA</resourceCategory>
+<resourceJarId>102</resourceJarId>
+<resourcePropsId>103</resourcePropsId>
+<shortName>Autodownload Wrapper x64</shortName>
+<skeletonProperties>
+<description>SKEL_AUTODOWNLOAD_PROPERTY_MESSAGE_DESCRIPTION</description>
+<idName>Message</idName>
+<label>SKEL_AUTODOWNLOAD_PROPERTY_MESSAGE_LABEL</label>
+<type>textarea</type>
+<value>Java has not been found on your computer. Do you want to download it?</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_AUTODOWNLOAD_PROPERTY_DLURL_DESCRIPTION</description>
+<idName>DownloadURL</idName>
+<label>SKEL_AUTODOWNLOAD_PROPERTY_DLURL_LABEL</label>
+<type>autodownloadurl</type>
+<value>http://java.sun.com/update/1.5.0/jinstall-1_5_0_11-windows-i586.cab</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_PROPERTY_SINGLEPROCESS_DESCRIPTION</description>
+<idName>SingleProcess</idName>
+<label>SKEL_GENERIC_PROPERTY_SINGLEPROCESS_LABEL</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_SINGLEINSTANCE_DESCRIPTION</description>
+<idName>SingleInstance</idName>
+<label>SKEL_GENERIC_SINGLEINSTANCE</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_JNISMOOTH_DESCRIPTION</description>
+<idName>JniSmooth</idName>
+<label>SKEL_GENERIC_JNISMOOTH</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_PROPERTY_DEBUG_DESCRIPTION</description>
+<idName>Debug</idName>
+<label>SKEL_GENERIC_PROPERTY_DEBUG_LABEL</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+</jsmoothskeleton>
diff --git a/jsmooth-0.9.9-7-patch/skeletons/autodownload/customdownload64.skel b/jsmooth-0.9.9-7-patch/skeletons/autodownload/customdownload64.skel
new file mode 100644
index 0000000..5c81ff2
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/autodownload/customdownload64.skel
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<jsmoothskeleton>
+<debug>false</debug>
+<description>SKEL_CUSTOMDOWNLOAD_DESCRIPTION</description>
+<executableName>autodownload.exe</executableName>
+<resourceCategory>JAVA</resourceCategory>
+<resourceJarId>102</resourceJarId>
+<resourcePropsId>103</resourcePropsId>
+<shortName>Custom Web Download Wrapper x64</shortName>
+<skeletonProperties>
+<description>SKEL_CUSTOMDOWNLOAD_PROPERTY_MESSAGE_DESCRIPTION</description>
+<idName>Message</idName>
+<label>SKEL_CUSTOMDOWNLOAD_PROPERTY_MESSAGE_LABEL</label>
+<type>textarea</type>
+<value>Java has not been found on your computer. Do you want to download it?</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_CUSTOMDOWNLOAD_PROPERTY_URL_DESCRIPTION</description>
+<idName>DownloadURL</idName>
+<label>SKEL_CUSTOMDOWNLOAD_PROPERTY_URL_LABEL</label>
+<type>string</type>
+<value>http://</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_PROPERTY_SINGLEPROCESS_DESCRIPTION</description>
+<idName>SingleProcess</idName>
+<label>SKEL_GENERIC_PROPERTY_SINGLEPROCESS_LABEL</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_SINGLEINSTANCE_DESCRIPTION</description>
+<idName>SingleInstance</idName>
+<label>SKEL_GENERIC_SINGLEINSTANCE</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_JNISMOOTH_DESCRIPTION</description>
+<idName>JniSmooth</idName>
+<label>SKEL_GENERIC_JNISMOOTH</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_PROPERTY_DEBUG_DESCRIPTION</description>
+<idName>Debug</idName>
+<label>SKEL_GENERIC_PROPERTY_DEBUG_LABEL</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+</jsmoothskeleton>
diff --git a/jsmooth-0.9.9-7-patch/skeletons/autodownload/mainres.rc b/jsmooth-0.9.9-7-patch/skeletons/autodownload/mainres.rc
new file mode 100644
index 0000000..f2634f1
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/autodownload/mainres.rc
@@ -0,0 +1,9 @@
+
+#include "resource.h"
+
+JARID JAVA "../samplejar/sample.jar"
+PROPID JAVA "../samplejar/sample.props"
+JNISMOOTHID JAVA "../jnismooth/jnismooth.jar"
+
+A2 ICON MOVEABLE PURE LOADONCALL DISCARDABLE "JWrap.ico"
+1 MANIFEST "../samplejar/sample.manifest"
diff --git a/jsmooth-0.9.9-7-patch/skeletons/commonjava/JVMBase.cpp b/jsmooth-0.9.9-7-patch/skeletons/commonjava/JVMBase.cpp
new file mode 100644
index 0000000..14072b8
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/commonjava/JVMBase.cpp
@@ -0,0 +1,74 @@
+/*
+ JSmooth: a VM wrapper toolkit for Windows
+ Copyright (C) 2003-2007 Rodrigo Reyes <reyes at charabia.net>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Library General Public
+ License as published by the Free Software Foundation; either
+ version 2 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Library General Public License for more details.
+
+ You should have received a copy of the GNU Library General Public
+ License along with this library; if not, write to the Free
+ Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#include "JVMBase.h"
+
+JVMBase::JVMBase()
+{
+ m_maxHeap = -1;
+ m_initialHeap = -1;
+ m_vmParameter = "";
+}
+
+void JVMBase::addPathElement(const std::string& element)
+{
+ m_pathElements.push_back(element);
+}
+
+void JVMBase::addProperty(const JavaProperty& prop)
+{
+ m_properties.push_back(prop);
+}
+
+void JVMBase::setMaxHeap(long size)
+{
+ m_maxHeap = size;
+}
+
+void JVMBase::setInitialHeap(long size)
+{
+ m_initialHeap = size;
+}
+
+void JVMBase::addArgument(const std::string& arg)
+{
+ // m_arguments.push_back(StringUtils::requoteForCommandLine(arg));
+ m_arguments.push_back(arg);
+}
+
+
+void JVMBase::setArguments(const std::string& args)
+{
+ m_arguments.clear();
+ DEBUG("arguments:<" + args + ">");
+ // std::string ua = StringUtils::unescape(args);
+ // DEBUG("arguments unescaped:<" + ua + ">");
+ vector<string> splitted = StringUtils::split(args, " \t\n\r", "\"\'", false);
+ for (int i=0; i<splitted.size(); i++)
+ {
+ DEBUG("SPLITTED-ARG[" + StringUtils::toString(i)+"]="+ splitted[i]);
+ this->addArgument(splitted[i]);
+ }
+}
+
+void JVMBase::setVmParameter(std::string parameter)
+{
+ m_vmParameter = parameter;
+}
diff --git a/jsmooth-0.9.9-7-patch/skeletons/commonjava/JVMBase.h b/jsmooth-0.9.9-7-patch/skeletons/commonjava/JVMBase.h
new file mode 100644
index 0000000..5ad2cd0
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/commonjava/JVMBase.h
@@ -0,0 +1,61 @@
+/*
+ JSmooth: a VM wrapper toolkit for Windows
+ Copyright (C) 2003-2007 Rodrigo Reyes <reyes at charabia.net>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Library General Public
+ License as published by the Free Software Foundation; either
+ version 2 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Library General Public License for more details.
+
+ You should have received a copy of the GNU Library General Public
+ License along with this library; if not, write to the Free
+ Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#ifndef __SUNJVMBASE_H_
+#define __SUNJVMBASE_H_
+
+#include <string>
+#include "jni.h"
+
+#include "Version.h"
+#include "StringUtils.h"
+#include "FileUtils.h"
+#include "ResourceManager.h"
+#include "JavaProperty.h"
+
+/**
+ * @author Rodrigo Reyes <reyes at charabia.net>
+ */
+
+class JVMBase
+{
+ protected:
+
+ std::vector<std::string> m_pathElements;
+ std::vector<JavaProperty> m_properties;
+ int m_maxHeap;
+ int m_initialHeap;
+ std::vector<std::string> m_arguments;
+ std::string m_vmParameter;
+
+ public:
+ JVMBase();
+
+ void addPathElement(const std::string& element);
+ void addProperty(const JavaProperty& prop);
+ void setMaxHeap(long size);
+ void setInitialHeap(long size);
+ void addArgument(const std::string& arg);
+ void setArguments(const std::string& args);
+ void setVmParameter(std::string parameter);
+};
+
+
+#endif
diff --git a/jsmooth-0.9.9-7-patch/skeletons/commonjava/JVMRegistryLookup.cpp b/jsmooth-0.9.9-7-patch/skeletons/commonjava/JVMRegistryLookup.cpp
new file mode 100644
index 0000000..c495ce8
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/commonjava/JVMRegistryLookup.cpp
@@ -0,0 +1,115 @@
+/*
+ JSmooth: a VM wrapper toolkit for Windows
+ Copyright (C) 2003-2007 Rodrigo Reyes <reyes at charabia.net>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Library General Public
+ License as published by the Free Software Foundation; either
+ version 2 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Library General Public License for more details.
+
+ You should have received a copy of the GNU Library General Public
+ License along with this library; if not, write to the Free
+ Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#include "JVMRegistryLookup.h"
+
+struct jvmsorter_dec : public binary_function<const SunJVMLauncher&, const SunJVMLauncher&, bool>
+{
+ bool operator()(const SunJVMLauncher& s1, const SunJVMLauncher& s2)
+ {
+ return s2 < s1;
+ }
+};
+
+vector<SunJVMLauncher> JVMRegistryLookup::lookupJVM()
+{
+ vector<SunJVMLauncher> res = JVMRegistryLookup::lookup(HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Runtime Environment");
+
+ vector<SunJVMLauncher> res2 = JVMRegistryLookup::lookup(HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Development Kit");
+
+ for (vector<SunJVMLauncher>::iterator i = res2.begin(); i != res2.end(); i++)
+ {
+ res.insert(res.end(), *i);
+ }
+
+ sort(res.begin(), res.end(), jvmsorter_dec() );
+
+ return res;
+}
+
+vector<SunJVMLauncher> JVMRegistryLookup::lookup(HKEY key, const string& path)
+{
+ vector<SunJVMLauncher> result;
+
+ HKEY hKey;
+ LONG error = ERROR_SUCCESS;
+ LONG val = RegOpenKeyEx(key, path.c_str(), 0, KEY_READ, &hKey);
+
+ unsigned long buffersize = 1024;
+ char buffer[1024];
+
+ for (int i=0; RegEnumKey(hKey, i, buffer, buffersize) == ERROR_SUCCESS; i++)
+ {
+ int v = i;
+ HKEY version;
+ int foundver = RegOpenKeyEx(hKey, buffer, 0, KEY_READ, &version);
+ if (foundver == ERROR_SUCCESS)
+ {
+ std::string versionname(buffer);
+ HKEY runtimelib;
+ unsigned long datatype;
+ std::string runtimelibstr = "";
+ std::string javahomestr = "";
+
+ unsigned char *b = (unsigned char*)buffer;
+ buffersize = 1024;
+ int foundlib = RegQueryValueEx(version, TEXT("RuntimeLib"),
+ NULL,
+ &datatype,
+ b,
+ &buffersize);
+
+ if (foundlib == ERROR_SUCCESS)
+ {
+ runtimelibstr = buffer;
+ }
+
+ b = (unsigned char*)buffer;
+ buffersize = 1024;
+ int foundhome = RegQueryValueEx(version, TEXT("JavaHome"),
+ NULL,
+ &datatype,
+ b,
+ &buffersize);
+ if (foundhome == ERROR_SUCCESS)
+ {
+ javahomestr = buffer;
+ }
+
+ if ((runtimelibstr.length()>0) || (javahomestr.length()>0))
+ {
+ SunJVMLauncher vm;
+ vm.RuntimeLibPath = runtimelibstr;
+ vm.JavaHome = javahomestr;
+ vm.VmVersion = Version(versionname);
+ result.push_back(vm);
+
+ char buffer[244];
+ sprintf(buffer, "V(%d)(%d)(%d)", vm.VmVersion.getMajor(), vm.VmVersion.getMinor(), vm.VmVersion.getSubMinor());
+ DEBUG(std::string("JVM Lookup: found VM (") + buffer + ") in registry.");
+ }
+ }
+
+ }
+
+ return result;
+}
+
+
diff --git a/jsmooth-0.9.9-7-patch/skeletons/commonjava/JVMRegistryLookup.h b/jsmooth-0.9.9-7-patch/skeletons/commonjava/JVMRegistryLookup.h
new file mode 100644
index 0000000..76ef6f9
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/commonjava/JVMRegistryLookup.h
@@ -0,0 +1,52 @@
+/*
+ JSmooth: a VM wrapper toolkit for Windows
+ Copyright (C) 2003-2007 Rodrigo Reyes <reyes at charabia.net>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Library General Public
+ License as published by the Free Software Foundation; either
+ version 2 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Library General Public License for more details.
+
+ You should have received a copy of the GNU Library General Public
+ License along with this library; if not, write to the Free
+ Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#ifndef _JVMREGISTRYLOOKUP_H_
+#define _JVMREGISTRYLOOKUP_H_
+
+#include <vector>
+#include <algorithm>
+
+#include "SunJVMLauncher.h"
+#include "JVMRegistryLookup.h"
+
+/** Utility class that scans the windows registry for installed JRE.
+ *
+ * @author Rodrigo Reyes <reyes at charabia.net>
+ */
+
+class JVMRegistryLookup
+{
+ public:
+ /**
+ * Scans the Windows Registry for the Java Runtime Environments. If
+ * no JRE is found, an empty vector is returned.
+ *
+ * @return a vector containing the JRE found.
+ */
+ static vector<SunJVMLauncher> lookupJVM();
+
+ private:
+ static vector<SunJVMLauncher> lookup(HKEY key, const string& path);
+
+};
+
+
+#endif
diff --git a/jsmooth-0.9.9-7-patch/skeletons/commonjava/JavaMachineManager.cpp b/jsmooth-0.9.9-7-patch/skeletons/commonjava/JavaMachineManager.cpp
new file mode 100644
index 0000000..5d5ec84
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/commonjava/JavaMachineManager.cpp
@@ -0,0 +1,232 @@
+/*
+ JSmooth: a VM wrapper toolkit for Windows
+ Copyright (C) 2003-2007 Rodrigo Reyes <reyes at charabia.net>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Library General Public
+ License as published by the Free Software Foundation; either
+ version 2 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Library General Public License for more details.
+
+ You should have received a copy of the GNU Library General Public
+ License along with this library; if not, write to the Free
+ Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#include "JavaMachineManager.h"
+
+
+JavaMachineManager::JavaMachineManager(ResourceManager& resman): m_resman(resman)
+{
+ DEBUG("Now searching the JVM installed on the system...");
+
+ m_registryVms = JVMRegistryLookup::lookupJVM();
+ m_javahomeVm = JVMEnvVarLookup::lookupJVM("JAVA_HOME");
+ m_jrepathVm = JVMEnvVarLookup::lookupJVM("JRE_HOME");
+ m_jdkpathVm = JVMEnvVarLookup::lookupJVM("JDK_HOME");
+ m_exitCode = 0;
+ m_useConsole = true;
+ m_acceptExe = true;
+ m_acceptDLL = true;
+ m_preferDLL = false;
+
+ if (resman.getProperty("bundledvm").length() > 0)
+ {
+ string bjvm = resman.getProperty("bundledvm");
+ DEBUG("Found a vm bundled with the application: (" + bjvm + ")");
+ m_localVMenabled = true;
+ std::string home = FileUtils::concFile(resman.getCurrentDirectory(), bjvm);
+ m_localVM.JavaHome = home;
+ } else
+ {
+ m_localVMenabled = false;
+ }
+ DEBUG("Current directory is " + resman.getCurrentDirectory());
+}
+
+bool JavaMachineManager::run()
+{
+ string vmorder = m_resman.getProperty(ResourceManager::KEY_JVMSEARCH);
+
+ if (m_localVMenabled)
+ {
+ if (internalRun(m_localVM, "bundled"))
+ {
+ return true;
+ }
+
+// DEBUG("Trying to use bundled VM " + m_localVM.JavaHome);
+// if (m_localVM.runProc(m_resman, m_useConsole, "bundled"))
+// {
+// m_exitCode = m_localVM.getExitCode();
+// return true;
+// }
+// if (m_localVM.run(m_resman, "bundled"))
+// return true;
+ }
+
+ if (vmorder == "")
+ {
+ vmorder = "registry;jdkpath;jrepath;javahome;jview;exepath";
+ }
+
+ DEBUG("JSmooth will now try to use the VM in the following order: " + vmorder);
+
+ vector<string> jvmorder = StringUtils::split(vmorder, ";,", "");
+
+ Version max(m_resman.getProperty(ResourceManager:: KEY_MAXVERSION));
+ Version min(m_resman.getProperty(ResourceManager:: KEY_MINVERSION));
+
+ for (vector<string>::const_iterator i = jvmorder.begin(); i != jvmorder.end(); i++)
+ {
+ DEBUG("------------------------------");
+
+ if (*i == "registry")
+ {
+ DEBUG("Trying to use a JVM defined in the registry (" + StringUtils::toString(m_registryVms.size()) + " available)");
+ string vms = "VM will be tried in the following order: ";
+ for (int i=0; i<m_registryVms.size(); i++)
+ {
+ vms += m_registryVms[i].VmVersion.toString();
+ vms += ";";
+ }
+ DEBUG(vms);
+
+ for (int i=0; i<m_registryVms.size(); i++)
+ {
+ DEBUG("- Trying registry: " + m_registryVms[i].toString());
+
+ if (internalRun(m_registryVms[i], "registry") == true)
+ return true;
+
+ DEBUG("Couldn't use this VM, now trying something else");
+ }
+ }
+ else if ((*i == "jview") && m_acceptExe)
+ {
+ DEBUG("- Trying to launch the application with JVIEW");
+ if (m_jviewVm.runProc(m_resman, ! m_useConsole))
+ {
+ return true;
+ }
+
+ }
+ else if ((*i == "javahome") && (m_javahomeVm.size()>0))
+ {
+ DEBUG("- Trying to use JAVAHOME");
+ if (internalRun(m_javahomeVm[0], "jrehome"))
+ return true;
+ }
+ else if ((*i == "jrepath") && (m_jrepathVm.size()>0))
+ {
+ DEBUG("- Trying to use JRE_HOME");
+ if (internalRun(m_jrepathVm[0], "jrehome"))
+ return true;
+ }
+ else if (*i == "exepath")
+ {
+ DEBUG("- Trying to use PATH");
+
+ SunJVMLauncher launcher;
+ if (launcher.runProc(m_resman, m_useConsole, "path"))
+ {
+ m_exitCode = m_localVM.getExitCode();
+ return true;
+ }
+ }
+ }
+
+ DEBUG("Couldn't run any suitable JVM!");
+ return false;
+}
+
+
+bool JavaMachineManager::internalRun(SunJVMLauncher& launcher, const string& org)
+{
+ if (m_acceptDLL && m_preferDLL)
+ {
+ if (launcher.run(m_resman, org))
+ return true;
+ }
+
+ if (m_acceptExe)
+ {
+ if (launcher.runProc(m_resman, m_useConsole, org))
+ {
+ m_exitCode = launcher.getExitCode();
+ return true;
+ }
+ }
+
+ if (m_acceptDLL && !m_preferDLL)
+ {
+ if (launcher.run(m_resman, org))
+ return true;
+ }
+
+ return false;
+}
+
+
+
+SunJVMLauncher* JavaMachineManager::runDLLFromRegistry(bool justInstanciate)
+{
+ string vms = "DLL VM will be tried in the following order: ";
+ for (int i=0; i<m_registryVms.size(); i++)
+ {
+ vms += m_registryVms[i].VmVersion.toString();
+ vms += ";";
+ }
+ DEBUG(vms);
+
+ for (int i=0; i<m_registryVms.size(); i++)
+ {
+ DEBUG("- Trying registry: " + m_registryVms[i].toString());
+
+ bool res = m_registryVms[i].run(m_resman, "registry", justInstanciate);
+
+ if (res)
+ return &m_registryVms[i];
+ }
+
+ if (m_localVMenabled) {
+ if (m_localVM.run(m_resman, "bundled", justInstanciate)) {
+ return &m_localVM;
+ } else {
+ DEBUG("Bundled VM launch failed");
+ }
+ }
+
+
+ return NULL;
+}
+
+void JavaMachineManager::setUseConsole(bool useConsole)
+{
+ m_useConsole = useConsole;
+}
+
+void JavaMachineManager::setAcceptExe(bool acceptExe)
+{
+ m_acceptExe = acceptExe;
+}
+
+void JavaMachineManager::setAcceptDLL(bool acceptDLL)
+{
+ m_acceptDLL = acceptDLL;;
+}
+
+void JavaMachineManager::setPreferDLL(bool prefDLL)
+{
+ m_preferDLL = prefDLL;
+}
+
+int JavaMachineManager::getExitCode()
+{
+ return m_exitCode;
+}
diff --git a/jsmooth-0.9.9-7-patch/skeletons/commonjava/Makefile.win b/jsmooth-0.9.9-7-patch/skeletons/commonjava/Makefile.win
new file mode 100644
index 0000000..f4d86e7
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/commonjava/Makefile.win
@@ -0,0 +1,60 @@
+# Project: CommonJava
+# Makefile created by Dev-C++ 4.9.8.0
+
+RM = rm -f
+CPP = ${MINGW}/bin/${TARGET}-g++.exe
+CC = ${MINGW}/bin/${TARGET}-gcc.exe
+WINDRES = windres.exe
+MINGW =
+RES =
+OBJ = JniSmooth.o JVMBase.o SunJVMExe.o JArgs.o JClassProxy.o SunJVMDLL.o JavaMachineManager.o JVMEnvVarLookup.o JVMRegistryLookup.o MSJViewLauncher.o Properties.o ResourceManager.o SunJVMLauncher.o Version.o global.o JavaProperty.o JMethodCaller.o $(RES)
+LINKOBJ = JniSmooth.o JVMBase.o SunJVMExe.o JArgs.o JClassProxy.o SunJVMDLL.o JavaMachineManager.o JVMEnvVarLookup.o JVMRegistryLookup.o MSJViewLauncher.o Properties.o ResourceManager.o SunJVMLauncher.o Version.o global.o JavaProperty.o JMethodCaller.o $(RES)
+LIBS = -static-libgcc -L"$(MINGW)/lib"
+INCS = -I"../util-core" -I"$(MINGW)/include" -I"$(JDK)/include" -I"$(JDK)/include/win32"
+CXXINCS = -I"$(MINGW)/include/c++" -I"$(MINGW)/include/c++/mingw32" -I"$(MINGW)/include/c++/backward" -I"$(MINGW)/include" -I"$(JDK)/include" -I"$(JDK)/include/win32" -I"../commonjava" -I"../util-core" -I"../util-net" $(FLTK-CXXFLAGS)
+BIN = CommonJava.a
+CXXFLAGS = $(CUSTOMFLAGS) $(CXXINCS) -DJDK="$(JDK)"
+CFLAGS = $(INCS)
+
+.PHONY: all all-before all-after clean clean-custom
+
+all: all-before CommonJava.a all-after
+
+
+clean: clean-custom
+ $(RM) $(OBJ) $(BIN)
+
+$(BIN): $(LINKOBJ) test.cpp
+ ar r $(BIN) $(LINKOBJ)
+ ranlib $(BIN)
+ $(CPP) $(CXXFLAGS) test.cpp $(BIN) ../util-core/util-core.a -o test.exe
+
+JavaMachineManager.o: JavaMachineManager.cpp
+ $(CPP) -c JavaMachineManager.cpp -o JavaMachineManager.o $(CXXFLAGS)
+
+JVMEnvVarLookup.o: JVMEnvVarLookup.cpp
+ $(CPP) -c JVMEnvVarLookup.cpp -o JVMEnvVarLookup.o $(CXXFLAGS)
+
+JVMRegistryLookup.o: JVMRegistryLookup.cpp
+ $(CPP) -c JVMRegistryLookup.cpp -o JVMRegistryLookup.o $(CXXFLAGS)
+
+MSJViewLauncher.o: MSJViewLauncher.cpp
+ $(CPP) -c MSJViewLauncher.cpp -o MSJViewLauncher.o $(CXXFLAGS)
+
+Properties.o: Properties.cpp
+ $(CPP) -c Properties.cpp -o Properties.o $(CXXFLAGS)
+
+ResourceManager.o: ResourceManager.cpp
+ $(CPP) -c ResourceManager.cpp -o ResourceManager.o $(CXXFLAGS)
+
+SunJVMLauncher.o: SunJVMLauncher.cpp
+ $(CPP) -c SunJVMLauncher.cpp -o SunJVMLauncher.o $(CXXFLAGS)
+
+Version.o: Version.cpp
+ $(CPP) -c Version.cpp -o Version.o $(CXXFLAGS)
+
+global.o: global.cpp
+ $(CPP) -c global.cpp -o global.o $(CXXFLAGS)
+
+JavaProperty.o: JavaProperty.cpp
+ $(CPP) -c JavaProperty.cpp -o JavaProperty.o $(CXXFLAGS)
diff --git a/jsmooth-0.9.9-7-patch/skeletons/commonjava/ResourceManager.cpp b/jsmooth-0.9.9-7-patch/skeletons/commonjava/ResourceManager.cpp
new file mode 100644
index 0000000..2fb31d2
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/commonjava/ResourceManager.cpp
@@ -0,0 +1,376 @@
+/*
+ JSmooth: a VM wrapper toolkit for Windows
+ Copyright (C) 2003-2007 Rodrigo Reyes <reyes at charabia.net>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Library General Public
+ License as published by the Free Software Foundation; either
+ version 2 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Library General Public License for more details.
+
+ You should have received a copy of the GNU Library General Public
+ License along with this library; if not, write to the Free
+ Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#include "ResourceManager.h"
+
+char * const ResourceManager::KEY_MAINCLASSNAME = "mainclassname";
+char * const ResourceManager::KEY_ARGUMENTS = "arguments";
+char * const ResourceManager::KEY_CLASSPATH = "classpath";
+char * const ResourceManager::KEY_JVMSEARCH = "jvmsearch";
+char * const ResourceManager::KEY_MINVERSION = "minversion";
+char * const ResourceManager::KEY_MAXVERSION = "maxversion";
+char * const ResourceManager::KEY_NOJVMMESSAGE = "nojvmmessage";
+char * const ResourceManager::KEY_NOJVMURL = "nojvmurl";
+char * const ResourceManager::KEY_BUNDLEDVM = "bundledvm";
+char * const ResourceManager::KEY_CURRENTDIR = "currentdir";
+char * const ResourceManager::KEY_EMBEDJAR = "embedjar";
+
+ResourceManager::ResourceManager(std::string category, int propsId, int jarId, int jniId)
+{
+ m_resourceCategory = category;
+ m_resourcePropsId = propsId;
+ m_resourceJarId = jarId;
+
+ //
+ // Load the Properties
+ //
+ DEBUG("Initialize properties...");
+ std::string propsidstr = this->idToResourceName(propsId);
+ HRSRC resprop = FindResource(NULL, propsidstr.c_str(), category.c_str());
+ if (resprop != NULL)
+ {
+ int mainsize = 0;
+ mainsize = SizeofResource(NULL, resprop);
+ // char mainbuf[mainsize+1];
+ HGLOBAL main = LoadResource(NULL, resprop);
+ m_props.setData((const char*)main, mainsize);
+ }
+ else
+ {
+ m_lastError = "Can't find resource 'main name'";
+ return;
+ }
+
+ //
+ // Split the arguments
+ //
+ m_arguments = StringUtils::split(getProperty(KEY_ARGUMENTS, ""), " \t\n\r", "\"\'");
+
+ //
+ // loads the jar information
+ //
+ std::string jaridstr = this->idToResourceName(jarId);
+ HRSRC resjar = FindResource(NULL, jaridstr.c_str(), category.c_str());
+ if (resjar != NULL)
+ {
+ m_jarSize = SizeofResource(NULL, resjar);
+ m_jarHandler = LoadResource(NULL, resjar);
+ }
+ else
+ {
+ m_lastError = "Can't find JAR resource!";
+ return;
+ }
+
+
+ m_jnismoothSize = this->getResourceSize(jniId);
+ m_jnismoothHandler = this->getResource(jniId);
+
+ //
+ // Extract the java properties from the Property
+ //
+ std::string jpropcountstr = m_props.get("javapropertiescount");
+
+ string exepath = FileUtils::getExecutablePath();
+ string exename = FileUtils::getExecutableFileName();
+ string computername = FileUtils::getComputerName();
+
+ int jpropcount = StringUtils::parseInt(jpropcountstr);
+ DEBUG("Number of Java Parameters: "+jpropcountstr);
+ for (int i=0; i<jpropcount; i++)
+ {
+ string namekey = string("javaproperty_name_") + StringUtils::toString(i);
+ string valuekey = string("javaproperty_value_") + StringUtils::toString(i);
+ string name = m_props.get(namekey);
+ string value = m_props.get(valuekey);
+
+ DEBUG("Setting up java properties SOURCE: " + name + "=" + value + " : property if exist: " +getProperty(name,""));
+
+ value = StringUtils::replaceEnvironmentVariable(value);
+ value = StringUtils::replace(value, "${EXECUTABLEPATH}", exepath);
+ value = StringUtils::replace(value, "${EXECUTABLENAME}", exename);
+ value = StringUtils::replace(value, "${COMPUTERNAME}", computername);
+
+ JavaProperty jprop(name, value);
+ m_javaProperties.push_back(jprop);
+
+ DEBUG("Setting up java properties DESTINATION: " + name + "=" + value);
+ }
+
+ std::string curdirmodifier = m_props.get(ResourceManager::KEY_CURRENTDIR);
+ if (curdirmodifier.length()>0)
+ {
+ int pos = string::npos;
+ if ( (pos=curdirmodifier.find("${EXECUTABLEPATH}")) != string::npos)
+ {
+ m_currentDirectory = FileUtils::concFile(exepath, curdirmodifier.substr(pos + string("${EXECUTABLEPATH}").size()));
+ // m_currentDirectory = StringUtils::replace(curdirmodifier, "${EXECUTABLEPATH}", exepath);
+ }
+ else
+ {
+ DEBUG(string("Currentdirectory =") + curdirmodifier);
+ m_currentDirectory = curdirmodifier;
+ // m_currentDirectory = FileUtils::concFile(FileUtils::getExecutablePath(), curdirmodifier);
+ m_currentDirectory = StringUtils::replaceEnvironmentVariable(m_currentDirectory);
+ }
+ }
+ else
+ {
+ m_currentDirectory = "";
+ }
+ // printf("CURDIR SET TO: [%s]\n", m_currentDirectory.c_str());
+}
+
+ResourceManager::~ResourceManager()
+{
+ for (std::vector<std::string>::iterator i=m_deleteOnFinalize.begin(); i != m_deleteOnFinalize.end(); i++)
+ {
+ int res = DeleteFile(i->c_str());
+ }
+}
+
+void ResourceManager::setProperty(const std::string& key, const std::string& value)
+{
+ m_props.set(key, value);
+}
+
+void ResourceManager::saveTemp(std::string tempname, HGLOBAL data, int size)
+{
+ if ((data == 0) || (size == 0))
+ return;
+
+ HANDLE temp = CreateFile(tempname.c_str(),
+ GENERIC_WRITE,
+ FILE_SHARE_WRITE,
+ NULL,
+ CREATE_ALWAYS,
+ FILE_ATTRIBUTE_NORMAL,
+ NULL);
+
+ if (temp != NULL)
+ {
+ DWORD reallyWritten;
+ WriteFile(temp, data, size, &reallyWritten, NULL);
+
+ // TODO: check the reallyWritten value for errors
+
+ CloseHandle(temp);
+ string s = tempname;
+ // m_deleteOnFinalize.push_back(s);
+ FileUtils::deleteOnReboot(s);
+ }
+}
+
+std::string ResourceManager::getMainName() const
+{
+ return getProperty(string("mainclassname"));
+}
+
+std::string ResourceManager::getProperty(const std::string& key) const
+{
+ return m_props.get(key);
+}
+
+std::string ResourceManager::getProperty(const std::string& key, const std::string& def) const
+{
+ if (m_props.contains(key))
+ return m_props.get(key);
+ else
+ return def;
+}
+
+bool ResourceManager::getBooleanProperty(const std::string& key) const
+{
+ std::string prop = getProperty(key, "0");
+
+ if (StringUtils::parseInt(prop)==1)
+ return true;
+
+ if (StringUtils::toLowerCase(prop) == "true")
+ return true;
+
+ return false;
+}
+
+std::string ResourceManager::saveJarInTempFile()
+{
+ if (useEmbeddedJar() == false)
+ return "";
+
+ std::string tempfilename = FileUtils::createTempFileName(".jar");
+ DEBUG("Created temporary filename to hold the jar (" + tempfilename + ")");
+ saveTemp(tempfilename, m_jarHandler, m_jarSize);
+ return tempfilename;
+}
+
+const vector<JavaProperty>& ResourceManager::getJavaProperties()
+{
+ return m_javaProperties;
+}
+
+std::vector<std::string> ResourceManager::getNormalizedClassPathVector() const
+{
+ std::string basepath = FileUtils::getExecutablePath();
+ std::string curdirmodifier = getCurrentDirectory(); //getProperty(string(ResourceManager::KEY_CURRENTDIR));
+ if (FileUtils::isAbsolute(curdirmodifier)){
+ basepath = curdirmodifier;
+ DEBUG("DEBUG: (absolut) Basepath is : " + basepath);
+ } else {
+ basepath = FileUtils::concFile(basepath, curdirmodifier);
+ DEBUG("DEBUG: (not absolut) Basepath is : " + basepath);
+ }
+
+ std::string cp = getProperty(string(ResourceManager::KEY_CLASSPATH));
+ vector<string>cps = StringUtils::split(cp, ";", "", false);
+ for (int i=0; i<cps.size(); i++)
+ {
+ string lib = cps[i];
+ DEBUG("ClassPath element is " + basepath + " + " +cps[i]);
+
+ cps[i] = FileUtils::concFile(basepath, cps[i]);
+ DEBUG("ClassPath element " + StringUtils::toString(i)+ "=" + cps[i]);
+ }
+
+ return cps;
+}
+
+std::string ResourceManager::getNormalizedClassPath() const
+{
+ vector<string> cps = getNormalizedClassPathVector();
+ return StringUtils::join(cps, ";");
+}
+
+std::string ResourceManager::getCurrentDirectory() const
+{
+ return m_currentDirectory;
+}
+
+bool ResourceManager::useEmbeddedJar() const
+{
+ std::string value = m_props.get(ResourceManager::KEY_EMBEDJAR);
+ if (value == "true")
+ return true;
+ return false;
+}
+
+void ResourceManager::printDebug() const
+{
+ DEBUG("ResourceManager resource configuration:");
+ DEBUG(" - Resource category: " + m_resourceCategory);
+ DEBUG(" - Current directory: " + m_currentDirectory);
+
+ map<string, string> props = m_props.getDataCopy();
+ DEBUG(" - Property count: " + StringUtils::toString(props.size()));
+ for (map<string, string>::iterator i = props.begin(); i != props.end(); i++)
+ {
+ DEBUG(" - Property: " + i->first + "=<" + i->second+">");
+ }
+}
+
+void ResourceManager::setUserArguments(std::vector<std::string> arguments)
+{
+ m_arguments.clear();
+
+ for (std::vector<std::string>::iterator i=arguments.begin(); i != arguments.end(); i++)
+ {
+ addUserArgument(*i);
+ }
+}
+
+
+void ResourceManager::addUserArgument(std::string argument)
+{
+ bool keyFound = false;
+ if (argument.size()>3)
+ {
+ int pos = argument.find("=");
+ if (pos != std::string::npos)
+ {
+ string key = argument.substr(2, pos-2);
+ string value = argument.substr(pos+1);
+ string argumentType = argument.substr(0,2);
+ if (argumentType == "-J")
+ {
+ DEBUG("FOUND USER ARGUMENT for JSMOOTH: [" + key + "]=[" + value + "]");
+ keyFound = true;
+ setProperty(key, value);
+ }
+ if (argumentType == "-D")
+ {
+ DEBUG("FOUND USER ARGUMENT for JAVA: [" + key + "]=[" + value + "]");
+ JavaProperty jprop(key, value);
+ keyFound = true;
+ m_javaProperties.push_back(jprop);
+ }
+ }
+ }
+ if (!keyFound)
+ {
+ m_arguments.push_back(argument);
+// setProperty(KEY_ARGUMENTS, getProperty(KEY_ARGUMENTS) + " " + StringUtils::requoteForCommandLine(StringUtils::escape(argument)) );
+ }
+}
+
+
+std::vector<std::string> ResourceManager::getArguments()
+{
+ return m_arguments;
+}
+
+int ResourceManager::getResourceSize(int id)
+{
+ std::string propid = idToResourceName(id);
+ HRSRC resprop = FindResource(NULL, propid.c_str(), m_resourceCategory.c_str());
+ if (resprop != NULL)
+ {
+ return SizeofResource(NULL, resprop);
+ }
+ else
+ return 0;
+}
+
+HGLOBAL ResourceManager::getResource(int id)
+{
+ std::string propid = idToResourceName(id);
+ HRSRC resprop = FindResource(NULL, propid.c_str(), m_resourceCategory.c_str());
+ if (resprop != NULL)
+ {
+ return LoadResource(NULL, resprop);
+ }
+ else
+ return 0;
+}
+
+
+std::string ResourceManager::saveJnismoothInTempFile()
+{
+ if (m_jnismoothHandler == 0)
+ {
+ DEBUG("NO JNI SMOOTH ID !!");
+ return "";
+ }
+
+ std::string tempfilename = FileUtils::createTempFileName(".jar");
+ DEBUG("Saving jnismoothjar in " + tempfilename);
+
+ DEBUG("Created temporary filename to hold the jar (" + tempfilename + ")");
+ saveTemp(tempfilename, m_jnismoothHandler, m_jnismoothSize);
+ return tempfilename;
+}
diff --git a/jsmooth-0.9.9-7-patch/skeletons/commonjava/ResourceManager.h b/jsmooth-0.9.9-7-patch/skeletons/commonjava/ResourceManager.h
new file mode 100644
index 0000000..1c22fab
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/commonjava/ResourceManager.h
@@ -0,0 +1,198 @@
+/*
+ JSmooth: a VM wrapper toolkit for Windows
+ Copyright (C) 2003-2007 Rodrigo Reyes <reyes at charabia.net>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Library General Public
+ License as published by the Free Software Foundation; either
+ version 2 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Library General Public License for more details.
+
+ You should have received a copy of the GNU Library General Public
+ License along with this library; if not, write to the Free
+ Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#ifndef RESOURCEMANAGER_H
+#define RESOURCEMANAGER_H
+
+#include <cstdio>
+#include <windows.h>
+#include <string>
+#include <vector>
+
+#include "common.h"
+#include "Properties.h"
+#include "FileUtils.h"
+#include "JavaProperty.h"
+
+/**
+ * Manages the executable resources associated to a Java
+ * application. This class manages the resources that are used to
+ * store the data associated to a java application. Those resources
+ * are:
+ * - The JAR file, stored as a raw resource.
+ * - The Property file, stored as a raw resource.
+ *
+ * The Property file contains an 8-bit text, as parsed and used by the
+ * Properties class, which defines information relative to the java
+ * application (for instance the name of the main class, the java
+ * properties, and so on).
+ *
+ * @author Rodrigo Reyes <reyes at charabia.net>
+ */
+
+class ResourceManager
+{
+ private:
+ std::string m_mainName;
+ std::string m_resourceCategory;
+ Properties m_props;
+ std::string m_currentDirectory;
+
+ int m_resourcePropsId;
+ int m_resourceJarId;
+ std::string m_lastError;
+ HGLOBAL m_jarHandler;
+ int m_jarSize;
+ HGLOBAL m_jnismoothHandler;
+ int m_jnismoothSize;
+
+ std::vector<std::string> m_arguments;
+
+ std::vector<std::string> m_deleteOnFinalize;
+ std::vector<JavaProperty> m_javaProperties;
+
+ public:
+
+ static char * const KEY_MAINCLASSNAME;
+ static char * const KEY_ARGUMENTS;
+ static char * const KEY_CLASSPATH;
+ static char * const KEY_JVMSEARCH;
+ static char * const KEY_MINVERSION;
+ static char * const KEY_MAXVERSION;
+ static char * const KEY_NOJVMMESSAGE;
+ static char * const KEY_NOJVMURL;
+ static char * const KEY_BUNDLEDVM;
+ static char * const KEY_CURRENTDIR;
+ static char * const KEY_EMBEDJAR;
+
+ /**
+ * Constructs a ResourceManager which extract the jar and property
+ * files from the resources defined by the given parameters. The
+ * resource are loaded from the resource type and resource names
+ * passed as parameters.
+ *
+ * @param category the resource type to look in
+ * @param propsId the resource id, stored under the category type, for the property file
+ * @param jarId the resource id, stored under the category type, for the jar file
+ */
+ ResourceManager(std::string category, int propsId, int jarId, int jniId = -1);
+
+ /**
+ * Default destructor. The detructor tries to delete all the
+ * temporary files that have been created by the object. This is
+ * mainly the files created by the saveJarInTempFile() method.
+ *
+ * @sa ResourceManager::saveJarInTempFile
+ */
+ ~ResourceManager();
+
+ /** Saves the jar in a temporary folder. Extract the jar file
+ * from the resource defined in the consructor, and saves it in
+ * the temporary directory defined by the operating system.
+ *
+ * NOTE: if the KEY_EMBEDJAR key does not return "true", this method
+ * does not save the jar, and returns an empty string ("").
+ *
+ * @return the filename of the temp file where the Jar can be found.
+ */
+ std::string saveJarInTempFile();
+
+ std::string saveJnismoothInTempFile();
+
+ /** Returns the name of the main class. The main class is the
+ * class used to launch the java application. The static "public
+ * void main(String[])" method of this class is called to start
+ * the program.
+ *
+ * @return the name of the main class
+ */
+ std::string getMainName() const;
+
+ /**
+ * Returns the last error string. This is the string that describes
+ * the last error that was raised by an operation on the object.
+ * @return a string
+ */
+ std::string getLastErrorString()
+ {
+ return m_lastError;
+ }
+
+ /**
+ * Retrieves a property value from the Properties resource defined
+ * in the constructor.
+ *
+ * @param key the name of the property
+ * @return a string that contains the value of the property, or an empty string if the property does not exist.
+ */
+ std::string getProperty(const std::string& key) const;
+ std::string getProperty(const std::string& key, const std::string& def) const;
+ bool getBooleanProperty(const std::string& key) const;
+
+ /**
+ * Adds a new property.
+ *
+ * @param key the name of the property
+ * @param value the value associated to the property
+ */
+ void setProperty(const std::string& key, const std::string& value);
+
+ std::vector<std::string> getNormalizedClassPathVector() const;
+ std::string getNormalizedClassPath() const;
+
+ /**
+ * Return the list of JavaProperty elements defined in the property
+ * resource.
+ *
+ * @return a vector of JavaProperty elements, or an empty vector if none are defined.
+ */
+ const vector<JavaProperty>& getJavaProperties();
+
+ std::string getCurrentDirectory() const;
+
+ bool useEmbeddedJar() const;
+
+ void printDebug() const;
+
+ void setUserArguments(std::vector<std::string> arguments);
+ void addUserArgument(std::string argument);
+
+ std::vector<std::string> getArguments();
+
+ int getResourceSize(int id);
+ HGLOBAL getResource(int id);
+
+ private:
+ void saveTemp(std::string tempname, HGLOBAL data, int size);
+
+ std::string idToResourceName(int id) const
+ {
+ char buffer[32];
+ sprintf(buffer, "%d", id);
+ std::string result("#");
+ result += buffer;
+ return result;
+ }
+
+};
+
+
+#endif
+
diff --git a/jsmooth-0.9.9-7-patch/skeletons/commonjava/SunJVMDLL.cpp b/jsmooth-0.9.9-7-patch/skeletons/commonjava/SunJVMDLL.cpp
new file mode 100644
index 0000000..d7efa55
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/commonjava/SunJVMDLL.cpp
@@ -0,0 +1,431 @@
+/*
+ JSmooth: a VM wrapper toolkit for Windows
+ Copyright (C) 2003-2007 Rodrigo Reyes <reyes at charabia.net>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Library General Public
+ License as published by the Free Software Foundation; either
+ version 2 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Library General Public License for more details.
+
+ You should have received a copy of the GNU Library General Public
+ License along with this library; if not, write to the Free
+ Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+
+#include "SunJVMDLL.h"
+
+#include "JClassProxy.h"
+#include "JniSmooth.h"
+
+SunJVMDLL::SunJVMDLL(const std::string& jvmdll, const Version& v)
+{
+ m_dllpath = jvmdll;
+ m_version = v;
+ m_statusCode = SunJVMDLL::JVM_NOT_STARTED;
+ m_vmlib = NULL;
+}
+
+SunJVMDLL::~SunJVMDLL()
+{
+ if (m_vmlib != NULL)
+ {
+ FreeLibrary(m_vmlib);
+ }
+}
+
+bool SunJVMDLL::run(const std::string& mainclass, bool waitDeath)
+{
+ if (m_statusCode == SunJVMDLL::JVM_NOT_STARTED)
+ instanciate();
+
+ if (m_statusCode == SunJVMDLL::JVM_LOADED)
+ {
+ JClassProxy disp(this, mainclass);
+ jstring emptystr = newUTFString(std::string(""));
+ jobjectArray mainargs = newObjectArray(m_arguments.size(), "java.lang.String", emptystr);
+ for (int i =0; i<m_arguments.size(); i++)
+ {
+ env()->SetObjectArrayElement(mainargs, i, newUTFString(m_arguments[i]));
+ }
+ printf("arguments array = %d\n", mainargs);
+ jvalue ma[1];
+ ma[0].l = mainargs;
+ disp.invokeStatic(std::string("void main(java.lang.String[] args)"), ma);
+ if (waitDeath == true)
+ m_javavm->DestroyJavaVM();
+ return true;
+ }
+
+ return false;
+}
+
+void SunJVMDLL::join()
+{
+ if (m_statusCode == SunJVMDLL::JVM_LOADED)
+ {
+ m_javavm->DestroyJavaVM();
+ }
+}
+
+bool SunJVMDLL::instanciate()
+{
+ m_vmlib = LoadLibrary(m_dllpath.c_str());
+ if (m_vmlib == NULL)
+ {
+ m_statusCode = SunJVMDLL::JVM_DLL_CANT_LOAD;
+ return false;
+ }
+ CreateJavaVM_t CreateJavaVM = (CreateJavaVM_t)GetProcAddress(m_vmlib, "JNI_CreateJavaVM");
+ GetDefaultJavaVMInitArgs_t GetDefaultJavaVMInitArgs = (GetDefaultJavaVMInitArgs_t)GetProcAddress(m_vmlib, "JNI_GetDefaultJavaVMInitArgs");
+
+ if ((CreateJavaVM == NULL) || (GetDefaultJavaVMInitArgs == NULL))
+ {
+ m_statusCode = SunJVMDLL::JVM_CANT_USE_VM;
+ return false;
+ }
+
+ DEBUG("VM Created successfully");
+
+ m_javavm = new JavaVM();
+ m_javaenv = new JNIEnv();
+
+ DEBUG("DLL Setup on " + m_version.toString());
+ bool res;
+ if ((m_version.getMajor() == 1) && (m_version.getMinor() == 1))
+ res = setupVM11DLL(CreateJavaVM, GetDefaultJavaVMInitArgs);
+ else
+ res = setupVM12DLL(CreateJavaVM, GetDefaultJavaVMInitArgs);
+
+ registerJniSmooth();
+
+ DEBUG("Result code on DLL: " + StringUtils::toString(res));
+ if (res)
+ {
+ m_statusCode = SunJVMDLL::JVM_LOADED;
+ return true;
+ }
+
+ m_statusCode = SunJVMDLL::JVM_CANT_USE_VM;
+ return false;
+}
+
+bool SunJVMDLL::setupVM12DLL(CreateJavaVM_t CreateJavaVM, GetDefaultJavaVMInitArgs_t GetDefaultJavaVMInitArgs)
+{
+ vector<string> jpropstrv;
+ if (m_vmParameter != "")
+ {
+ std::vector<std::string> vmParameter = StringUtils::split(m_vmParameter, " ", " ", false);
+ for (std::vector<std::string>::iterator i=vmParameter.begin(); i != vmParameter.end(); i++)
+ {
+ jpropstrv.push_back(*i);
+ }
+ }
+
+ for (int i=0; i<m_properties.size(); i++)
+ if(m_properties[i].getName()[0]=='-') {
+ jpropstrv.push_back( StringUtils::requoteForCommandLine(m_properties[i].getName()));
+ } else {
+ jpropstrv.push_back( StringUtils::requoteForCommandLine("-D" + m_properties[i].getName()) + "=" + StringUtils::requoteForCommandLine(m_properties[i].getValue()));
+ }
+
+// DEBUG("MAXHEAP: " + StringUtils::toString(m_maxHeap));
+// DEBUG("INITIALHEAP: " + StringUtils::toString(m_initialHeap));
+
+ if (m_maxHeap > 0)
+ {
+ jpropstrv.push_back("-Xmx" +StringUtils::toString(m_maxHeap));
+ }
+
+ if (m_initialHeap > 0)
+ {
+ jpropstrv.push_back("-Xms" + StringUtils::toString(m_initialHeap));
+ }
+
+ JavaVMInitArgs vm_args;
+ GetDefaultJavaVMInitArgs(&vm_args);
+
+ JavaVMOption options[1 + jpropstrv.size()];
+ std::string cpoption = "-Djava.class.path=" + StringUtils::join(m_pathElements, ";");
+
+ DEBUG("Classpath: " + cpoption);
+ options[0].optionString = (char*)cpoption.c_str();
+ vm_args.version = 0x00010002;
+ vm_args.version = JNI_VERSION_1_2;
+ vm_args.options = options;
+ vm_args.nOptions = 1 + jpropstrv.size();
+
+ for (int i=0; i<jpropstrv.size(); i++)
+ {
+ options[1 + i].optionString = (char*)jpropstrv[i].c_str();
+ DEBUG(string("Option added:") + options[1+i].optionString);
+ }
+
+ vm_args.ignoreUnrecognized = JNI_TRUE;
+
+ //
+ // Create the VM
+ if (CreateJavaVM( &m_javavm, &m_javaenv, &vm_args) != 0)
+ {
+ DEBUG("Can't create VM");
+ m_statusCode = SunJVMDLL::JVM_CANT_CREATE_VM;
+ return false;
+ }
+
+ DEBUG("VM 1.2+ Created successfully !!");
+ return true;
+}
+
+bool SunJVMDLL::setupVM11DLL(CreateJavaVM_t CreateJavaVM, GetDefaultJavaVMInitArgs_t GetDefaultJavaVMInitArgs)
+{
+ JDK1_1InitArgs vm_args;
+ vm_args.version = 0x00010001;
+ GetDefaultJavaVMInitArgs(&vm_args);
+
+ if (m_maxHeap > 0)
+ vm_args.maxHeapSize = m_maxHeap;
+ if (m_initialHeap > 0)
+ vm_args.minHeapSize = m_initialHeap;
+
+ //
+ // create the properties array
+ //
+ char const * props[m_properties.size()+1];
+ vector<string> jpropstrv;
+
+ for (int i=0; i<m_properties.size(); i++)
+ jpropstrv[i] = m_properties[i].getName() + "=" + m_properties[i].getValue();
+
+ for (int i=0; i<m_properties.size(); i++)
+ props[i] = jpropstrv[i].c_str();
+ props[m_properties.size()] = NULL;
+
+ vm_args.properties = (char**)props;
+
+ /* Append USER_CLASSPATH to the default system class path */
+
+ std::string classpath = vm_args.classpath;
+ classpath += StringUtils::join(m_pathElements, ";");
+
+ DEBUG("Classpath = " + classpath);
+ vm_args.classpath = (char*)classpath.c_str();
+
+ //
+ // Create the VM
+ if (CreateJavaVM( &m_javavm, &m_javaenv, &vm_args) != 0)
+ {
+ DEBUG("Can't create VM");
+ m_statusCode = SunJVMDLL::JVM_CANT_CREATE_VM;
+ return false;
+ }
+ DEBUG("VM 1.1 Created successfully !!");
+ return true;
+}
+
+
+jclass SunJVMDLL::findClass(const std::string& clazz)
+{
+ std::string classname = StringUtils::replace(clazz,".", "/");
+ DEBUG("Looking up for class <" + classname + ">");
+ jclass cls = env()->FindClass(classname.c_str());
+ if (cls == 0)
+ DEBUG("Can't find class " + classname + " !");
+ return cls;
+}
+
+jmethodID SunJVMDLL::findMethod(jclass& cls, const std::string& methodname, const std::string& signature, bool isStatic)
+{
+ std::string sig = StringUtils::replace(signature, ".", "/");
+
+ jmethodID mid;
+ if (isStatic)
+ mid = env()->GetStaticMethodID(cls, methodname.c_str(), sig.c_str());
+ else
+ mid = env()->GetMethodID(cls, methodname.c_str(), sig.c_str());
+
+ return mid;
+}
+
+JavaVM* SunJVMDLL::getJavaVM()
+{
+ return m_javavm;
+}
+
+void SunJVMDLL::setIntField(jclass cls, jobject obj, const std::string& fieldName, int value)
+{
+ jfieldID binding = env()->GetFieldID(cls, fieldName.c_str(), "I");
+ env()->SetIntField(obj, binding, (jint)value);
+}
+
+void SunJVMDLL::setLongField(jclass cls, jobject obj, const std::string& fieldName, jlong value)
+{
+ jfieldID binding = env()->GetFieldID(cls, fieldName.c_str(), "J");
+ env()->SetLongField(obj, binding, (jlong)value);
+}
+
+void SunJVMDLL::setObjectField(jclass cls, jobject obj, const std::string& fieldName, const std::string& fieldclass, jobject value)
+{
+ std::string fc = "L" + StringUtils::replace(fieldclass, "." , "/") + ";";
+ jfieldID binding = env()->GetFieldID(cls, fieldName.c_str(), fc.c_str());
+ env()->SetObjectField(obj, binding, value);
+}
+
+jstring SunJVMDLL::newUTFString(const std::string& str)
+{
+ return env()->NewStringUTF(str.c_str());
+}
+
+jobject SunJVMDLL::newObject(jclass clazz, jmethodID& methodid, jvalue arguments[])
+{
+ return env()->NewObjectA(clazz, methodid, arguments);
+}
+
+jobjectArray SunJVMDLL::newObjectArray(int size, jclass clazz, jobject initialValue)
+{
+ return env()->NewObjectArray((jsize)size, clazz, initialValue);
+}
+
+jobjectArray SunJVMDLL::newObjectArray(int size, const std::string& classname, jobject initialValue)
+{
+ jclass cls = findClass(classname);
+ return newObjectArray(size, cls, initialValue);
+}
+
+//
+// Static method invocation
+//
+
+void SunJVMDLL::invokeVoidStatic(jclass clazz, jmethodID& methodid, jvalue arguments[])
+{
+ env()->CallStaticVoidMethodA(clazz, methodid, arguments);
+}
+
+jboolean SunJVMDLL::invokeBooleanStatic(jclass clazz, jmethodID& methodid, jvalue arguments[])
+{
+ return env()->CallStaticBooleanMethodA(clazz, methodid, arguments);
+}
+
+jbyte SunJVMDLL::invokeByteStatic(jclass clazz, jmethodID& methodid, jvalue arguments[])
+{
+ return env()->CallStaticByteMethodA(clazz, methodid, arguments);
+}
+
+jchar SunJVMDLL::invokeCharStatic(jclass clazz, jmethodID& methodid, jvalue arguments[])
+{
+ return env()->CallStaticCharMethodA(clazz, methodid, arguments);
+}
+
+jshort SunJVMDLL::invokeShortStatic(jclass clazz, jmethodID& methodid, jvalue arguments[])
+{
+ return env()->CallStaticShortMethodA(clazz, methodid, arguments);
+}
+
+jint SunJVMDLL::invokeIntStatic(jclass clazz, jmethodID& methodid, jvalue arguments[])
+{
+ return env()->CallStaticIntMethodA(clazz, methodid, arguments);
+}
+
+jlong SunJVMDLL::invokeLongStatic(jclass clazz, jmethodID& methodid, jvalue arguments[])
+{
+ return env()->CallStaticLongMethodA(clazz, methodid, arguments);
+}
+
+jfloat SunJVMDLL::invokeFloatStatic(jclass clazz, jmethodID& methodid, jvalue arguments[])
+{
+ return env()->CallStaticFloatMethodA(clazz, methodid, arguments);
+}
+
+jdouble SunJVMDLL::invokeDoubleStatic(jclass clazz, jmethodID& methodid, jvalue arguments[])
+{
+ return env()->CallStaticDoubleMethodA(clazz, methodid, arguments);
+}
+
+jobject SunJVMDLL::invokeObjectStatic(jclass clazz, jmethodID& methodid, jvalue arguments[])
+{
+ return env()->CallStaticObjectMethodA(clazz, methodid, arguments);
+}
+
+
+//
+// method invocation
+//
+
+void SunJVMDLL::invokeVoid(jobject& obj, jmethodID& methodid, jvalue arguments[])
+{
+ env()->CallVoidMethodA(obj, methodid, arguments);
+}
+
+jboolean SunJVMDLL::invokeBoolean(jobject& obj, jmethodID& methodid, jvalue arguments[])
+{
+ return env()->CallBooleanMethodA(obj, methodid, arguments);
+}
+
+jbyte SunJVMDLL::invokeByte(jobject& obj, jmethodID& methodid, jvalue arguments[])
+{
+ return env()->CallByteMethodA(obj, methodid, arguments);
+}
+
+jchar SunJVMDLL::invokeChar(jobject& obj, jmethodID& methodid, jvalue arguments[])
+{
+ return env()->CallCharMethodA(obj, methodid, arguments);
+}
+
+jshort SunJVMDLL::invokeShort(jobject& obj, jmethodID& methodid, jvalue arguments[])
+{
+ return env()->CallShortMethodA(obj, methodid, arguments);
+}
+
+jint SunJVMDLL::invokeInt(jobject& obj, jmethodID& methodid, jvalue arguments[])
+{
+ return env()->CallIntMethodA(obj, methodid, arguments);
+}
+
+jlong SunJVMDLL::invokeLong(jobject& obj, jmethodID& methodid, jvalue arguments[])
+{
+ return env()->CallLongMethodA(obj, methodid, arguments);
+}
+
+jfloat SunJVMDLL::invokeFloat(jobject& obj, jmethodID& methodid, jvalue arguments[])
+{
+ return env()->CallFloatMethodA(obj, methodid, arguments);
+}
+
+jdouble SunJVMDLL::invokeDouble(jobject& obj, jmethodID& methodid, jvalue arguments[])
+{
+ return env()->CallDoubleMethodA(obj, methodid, arguments);
+}
+
+jobject SunJVMDLL::invokeObject(jobject& obj, jmethodID& methodid, jvalue arguments[])
+{
+ return env()->CallObjectMethodA(obj, methodid, arguments);
+}
+
+bool SunJVMDLL::registerMethod(const std::string& classname, const std::string& methodname, const std::string& signature,
+ void* fn)
+{
+ jclass cc = this->findClass(classname);
+ if (cc == 0)
+ return false;
+ JNINativeMethod jnm;
+ jnm.name = (char*)methodname.c_str();
+ jnm.signature = (char*)signature.c_str();
+ jnm.fnPtr = fn;
+
+ int res = env()->RegisterNatives(cc, &jnm, 1);
+ if (res != 0)
+ return false;
+
+ return true;
+}
+
+bool SunJVMDLL::registerJniSmooth()
+{
+ registerNativeMethods(this);
+ return true;
+}
diff --git a/jsmooth-0.9.9-7-patch/skeletons/commonjava/SunJVMExe.cpp b/jsmooth-0.9.9-7-patch/skeletons/commonjava/SunJVMExe.cpp
new file mode 100644
index 0000000..aa3b518
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/commonjava/SunJVMExe.cpp
@@ -0,0 +1,230 @@
+/*
+ JSmooth: a VM wrapper toolkit for Windows
+ Copyright (C) 2003-2007 Rodrigo Reyes <reyes at charabia.net>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Library General Public
+ License as published by the Free Software Foundation; either
+ version 2 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Library General Public License for more details.
+
+ You should have received a copy of the GNU Library General Public
+ License along with this library; if not, write to the Free
+ Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#include "SunJVMExe.h"
+
+#include <vector>
+#include <string>
+#include "Process.h"
+#include "FileUtils.h"
+
+SunJVMExe::SunJVMExe(const std::string& jrehome)
+{
+ m_jrehome = jrehome;
+}
+
+SunJVMExe::SunJVMExe(const std::string& jrehome, const Version& v)
+{
+ m_jrehome = jrehome;
+ m_version = v;
+}
+
+bool SunJVMExe::run(const std::string& mainclass, bool useconsole)
+{
+ if (!m_version.isValid())
+ {
+ m_version = guessVersion();
+ }
+
+ if (!m_version.isValid())
+ return false;
+
+ std::vector<std::string> execv;
+
+ execv.push_back(StringUtils::requoteForCommandLine(lookUpExecutable(useconsole)));
+
+ if (m_vmParameter != "")
+ {
+ std::vector<std::string> vmParameter = StringUtils::split(m_vmParameter, " ", " ", false);
+ for (std::vector<std::string>::iterator i=vmParameter.begin(); i != vmParameter.end(); i++)
+ {
+ execv.push_back(*i);
+ }
+ }
+
+ if (m_maxHeap > 0)
+ {
+ if ((m_version.getMajor()==1)&&(m_version.getMinor()==1))
+ execv.push_back("-mx" + StringUtils::toString(m_maxHeap));
+ else
+ execv.push_back("-Xmx" + StringUtils::toString(m_maxHeap));
+ }
+
+ if (m_initialHeap > 0)
+ {
+ if ((m_version.getMajor()==1)&&(m_version.getMinor()==1))
+ execv.push_back("-ms" + StringUtils::toString(m_initialHeap));
+ else
+ execv.push_back("-Xms" + StringUtils::toString(m_initialHeap));
+ }
+
+ for (int i=0; i<m_properties.size(); i++)
+ if(m_properties[i].getName()[0]=='-') {
+ execv.push_back( StringUtils::requoteForCommandLine(m_properties[i].getName()));
+ } else {
+ execv.push_back( StringUtils::requoteForCommandLine("-D" + m_properties[i].getName()) + "=" + StringUtils::requoteForCommandLine(m_properties[i].getValue()));
+ }
+
+ std::string classpath;
+ if ((m_version.getMajor()==1)&&(m_version.getMinor()==1))
+ classpath = getClassPath(true);
+ else
+ classpath = getClassPath(false);
+
+ if (classpath.size() > 0)
+ execv.push_back("-classpath " + StringUtils::requoteForCommandLine(classpath));
+
+ execv.push_back(mainclass);
+
+ for (int i=0; i<m_arguments.size(); i++)
+ {
+ execv.push_back( StringUtils::requoteForCommandLine(m_arguments[i]) );
+ }
+
+ std::string execmd = StringUtils::join(execv, " ");
+ DEBUG("COMMAND: <" + execmd + ">");
+
+ Process proc(execmd, useconsole);
+ if (proc.run())
+ {
+ DEBUG("Started successfully");
+ proc.join();
+ m_exitCode = proc.getExitCode();
+ return true;
+ }
+ else
+ {
+ DEBUG("Failed running " + execmd);
+ }
+ return false;
+}
+
+
+std::string SunJVMExe::lookUpExecutable(bool useconsole)
+{
+ std::string java;
+
+ if (m_jrehome.size() == 0)
+ {
+ return useconsole?"java.exe":"javaw.exe";
+ }
+
+ if (useconsole)
+ {
+ if (FileUtils::fileExists(m_jrehome, "bin\\java.exe"))
+ java = FileUtils::concFile(m_jrehome, "bin\\java.exe");
+ else if (FileUtils::fileExists(m_jrehome, "bin\\jre.exe"))
+ java = FileUtils::concFile(m_jrehome, "bin\\jre.exe");
+ else
+ {
+ std::vector<std::string> javas = FileUtils::recursiveSearch(m_jrehome, "java.exe");
+ DEBUG("REC: " + StringUtils::toString(javas));
+
+ if (javas.size() == 0)
+ javas = FileUtils::recursiveSearch(m_jrehome, "jre.exe");
+
+ if (javas.size() > 0)
+ java = javas[0];
+ }
+ }
+ else
+ {
+ if (FileUtils::fileExists(m_jrehome, "bin\\javaw.exe"))
+ java = FileUtils::concFile(m_jrehome, "bin\\javaw.exe");
+ else if (FileUtils::fileExists(m_jrehome, "bin\\jrew.exe"))
+ java = FileUtils::concFile(m_jrehome, "bin\\jrew.exe");
+ else
+ {
+ std::vector<std::string> javas = FileUtils::recursiveSearch(m_jrehome, "javaw.exe");
+
+ DEBUG("REC: " + StringUtils::toString(javas));
+
+ if (javas.size() == 0)
+ javas = FileUtils::recursiveSearch(m_jrehome, "jrew.exe");
+
+ if (javas.size() > 0)
+ java = javas[0];
+ }
+ }
+
+ return java;
+}
+
+Version SunJVMExe::guessVersion()
+{
+ std::string exepath = lookUpExecutable(true);
+ string exeline = exepath + " -version";
+
+ Version result;
+
+ // Return immediatly if the exe does not exist
+ if (!FileUtils::fileExists(exepath))
+ return result;
+
+ string tmpfilename = FileUtils::createTempFileName(".tmp");
+
+ Process proc(exeline, true);
+ proc.setRedirect(tmpfilename);
+ proc.run();
+ proc.join();
+
+ std::string voutput = FileUtils::readFile(tmpfilename);
+ vector<string> split = StringUtils::split(voutput, " \t\n\r", "\"");
+ for (vector<string>::iterator i=split.begin(); i != split.end(); i++)
+ {
+ Version v(*i);
+ if (v.isValid())
+ {
+ result = v;
+ break;
+ }
+ }
+
+ FileUtils::deleteOnReboot(tmpfilename);
+ return result;
+}
+
+
+std::string SunJVMExe::getClassPath(bool full)
+{
+ std::vector<std::string> cp;
+ for (std::vector<std::string>::const_iterator i=m_pathElements.begin(); i!=m_pathElements.end(); i++)
+ cp.push_back(*i);
+
+ if (full)
+ {
+ std::string javaexe = lookUpExecutable(true);
+ std::string home = FileUtils::getParent( FileUtils::getParent( javaexe ));
+ if (FileUtils::fileExists(home))
+ {
+ vector<string> cpzips = FileUtils::recursiveSearch(home, "*.zip");
+ cp.insert(cp.end(), cpzips.begin(), cpzips.end());
+ vector<string> cpjars = FileUtils::recursiveSearch(home, "*.jar");
+ cp.insert(cp.end(), cpjars.begin(), cpjars.end());
+ }
+ }
+
+ return StringUtils::join(cp, ";");
+}
+
+int SunJVMExe::getExitCode()
+{
+ return m_exitCode;
+}
diff --git a/jsmooth-0.9.9-7-patch/skeletons/commonjava/SunJVMLauncher.cpp b/jsmooth-0.9.9-7-patch/skeletons/commonjava/SunJVMLauncher.cpp
new file mode 100644
index 0000000..6441f11
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/commonjava/SunJVMLauncher.cpp
@@ -0,0 +1,233 @@
+/*
+ JSmooth: a VM wrapper toolkit for Windows
+ Copyright (C) 2003-2007 Rodrigo Reyes <reyes at charabia.net>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Library General Public
+ License as published by the Free Software Foundation; either
+ version 2 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Library General Public License for more details.
+
+ You should have received a copy of the GNU Library General Public
+ License along with this library; if not, write to the Free
+ Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#include "SunJVMLauncher.h"
+#include "Process.h"
+
+#include "SunJVMDLL.h"
+#include "JArgs.h"
+#include "JClassProxy.h"
+
+extern "C" {
+ static jint JNICALL myvprintf(FILE *fp, const char *format, va_list args)
+ {
+ DEBUG("MYPRINTF");
+ }
+ void JNICALL myexit(jint code)
+ {
+ DEBUG("EXIT CALLED FROM JVM DLL");
+ exit(code);
+ }
+}
+
+std::string SunJVMLauncher::toString() const
+{
+ return "<" + JavaHome + "><" + RuntimeLibPath + "><" + VmVersion.toString() + ">";
+}
+
+bool SunJVMLauncher::run(ResourceManager& resource, const string& origin, bool justInstanciate)
+{
+ DEBUG("Running now " + this->toString() + ", instanciate=" + (justInstanciate?"yes":"no"));
+
+ Version max(resource.getProperty(ResourceManager:: KEY_MAXVERSION));
+ Version min(resource.getProperty(ResourceManager:: KEY_MINVERSION));
+
+ // patch proposed by zregvart: if you're using bundeled JVM, you
+ // apriori know the version bundled and we can trust. The version
+ // check is therefore unrequired.
+ if (origin != "bundled") {
+
+ if (VmVersion.isValid() == false)
+ {
+ DEBUG("No version identified for " + toString());
+ SunJVMExe exe(this->JavaHome);
+ VmVersion = exe.guessVersion();
+ DEBUG("Version found: " + VmVersion.toString());
+ }
+
+ if (VmVersion.isValid() == false)
+ {
+ DEBUG("No version found, can't instanciate DLL without it");
+ return false;
+ }
+
+ if (min.isValid() && (VmVersion < min))
+ return false;
+
+ if (max.isValid() && (max < VmVersion))
+ return false;
+ }
+
+ DEBUG("Launching " + toString());
+
+ //
+ // search for the dll if it's not set in the registry, or if the
+ // file doesn't exist
+ //
+ if ( (this->JavaHome.size()>0)
+ && ((this->RuntimeLibPath.size() == 0) || (!FileUtils::fileExists(this->RuntimeLibPath))) )
+ {
+ std::string assump = FileUtils::concFile(this->JavaHome, "jre\\bin\\jvm.dll");
+ std::string assump2 = FileUtils::concFile(this->JavaHome, "jre\\bin\\server\\jvm.dll"); // for JRE 1.5+
+ std::string assump3 = FileUtils::concFile(this->JavaHome, "jre\\bin\\client\\jvm.dll"); // for JRE 1.5+
+ std::string assump4 = FileUtils::concFile(this->JavaHome, "bin\\javai.dll"); // For JRE 1.1
+
+ if (FileUtils::fileExists(assump))
+ this->RuntimeLibPath = assump;
+ else if (FileUtils::fileExists(assump2))
+ this->RuntimeLibPath = assump2;
+ else if (FileUtils::fileExists(assump3))
+ this->RuntimeLibPath = assump3;
+ else if (FileUtils::fileExists(assump4))
+ this->RuntimeLibPath = assump4;
+ else
+ {
+ vector<string> dlls = FileUtils::recursiveSearch(this->JavaHome, string("jvm.dll"));
+ if (dlls.size() > 0)
+ this->RuntimeLibPath = dlls[0];
+ }
+ }
+
+ if (FileUtils::fileExists(this->RuntimeLibPath))
+ {
+ DEBUG("RuntimeLibPath used: " + this->RuntimeLibPath);
+ Version v = this->VmVersion;
+ if (!v.isValid())
+ {
+ v = min;
+ if (!v.isValid())
+ v = Version("1.2.0");
+ DEBUG("No version, trying with " + v.toString());
+ }
+
+ m_dllrunner = new SunJVMDLL(this->RuntimeLibPath, v);
+ // set up the vm parameters...
+ setupVM(resource, m_dllrunner);
+
+ if (justInstanciate)
+ return m_dllrunner->instanciate();
+ else
+ return m_dllrunner->run(resource.getProperty(ResourceManager::KEY_MAINCLASSNAME),
+ true);
+ }
+
+ return false;
+}
+
+bool SunJVMLauncher::runProc(ResourceManager& resource, bool useConsole, const string& origin)
+{
+ std::string classname = resource.getProperty(string(ResourceManager::KEY_MAINCLASSNAME));
+
+ if (VmVersion.isValid() == false)
+ {
+ DEBUG("No version identified for " + toString());
+ SunJVMExe exe(this->JavaHome);
+ VmVersion = exe.guessVersion();
+ DEBUG("Version found: " + VmVersion.toString());
+ }
+
+ if (VmVersion.isValid() == false)
+ return false;
+
+ if (origin != "bundled") {
+
+ Version max(resource.getProperty(ResourceManager:: KEY_MAXVERSION));
+ Version min(resource.getProperty(ResourceManager:: KEY_MINVERSION));
+
+ if (min.isValid() && (VmVersion < min))
+ return false;
+
+ if (max.isValid() && (max < VmVersion))
+ return false;
+ }
+
+ SunJVMExe exe(this->JavaHome, VmVersion);
+ setupVM(resource, &exe);
+ if (exe.run(classname, useConsole))
+ {
+ m_exitCode = exe.getExitCode();
+ return true;
+ }
+ return false;
+}
+
+bool SunJVMLauncher::setupVM(ResourceManager& resource, JVMBase* vm)
+{
+ //
+ // create the properties array
+ const vector<JavaProperty>& jprops = resource.getJavaProperties();
+ for (int i=0; i<jprops.size(); i++)
+ {
+ vm->addProperty(jprops[i]);
+ }
+
+ if (resource.getProperty("maxheap") != "")
+ vm->setMaxHeap( StringUtils::parseInt(resource.getProperty("maxheap") ));
+
+ if (resource.getProperty("initialheap") != "")
+ vm->setInitialHeap( StringUtils::parseInt(resource.getProperty("initialheap") ));
+
+ if (resource.getProperty("vmparameter") != "")
+ vm->setVmParameter( resource.getProperty("vmparameter") );
+
+ if (resource.useEmbeddedJar())
+ {
+ std::string embj = resource.saveJarInTempFile();
+ vm->addPathElement(embj);
+ }
+
+ std::string jnijar = resource.saveJnismoothInTempFile();
+ if (jnijar != "")
+ vm->addPathElement(jnijar);
+
+ //
+ // Define the classpath
+ std::vector<std::string> classpath = resource.getNormalizedClassPathVector();
+ for (int i=0; i<classpath.size(); i++)
+ {
+ vm->addPathElement(classpath[i]);
+ }
+
+ //
+ // Defines the arguments passed to the java application
+ // vm->setArguments(resource.getProperty(ResourceManager::KEY_ARGUMENTS));
+ std::vector<std::string> args = resource.getArguments();
+ for (int i=0; i<args.size(); i++)
+ {
+ vm->addArgument(args[i]);
+ }
+
+}
+
+SunJVMDLL* SunJVMLauncher::getDLL()
+{
+ return m_dllrunner;
+}
+
+bool operator < (const SunJVMLauncher& v1, const SunJVMLauncher& v2)
+{
+ return v1.VmVersion < v2.VmVersion;
+}
+
+int SunJVMLauncher::getExitCode()
+{
+ return m_exitCode;
+}
+
diff --git a/jsmooth-0.9.9-7-patch/skeletons/commonjava/Version.h b/jsmooth-0.9.9-7-patch/skeletons/commonjava/Version.h
new file mode 100644
index 0000000..bd94101
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/commonjava/Version.h
@@ -0,0 +1,131 @@
+/*
+ JSmooth: a VM wrapper toolkit for Windows
+ Copyright (C) 2003-2007 Rodrigo Reyes <reyes at charabia.net>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Library General Public
+ License as published by the Free Software Foundation; either
+ version 2 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Library General Public License for more details.
+
+ You should have received a copy of the GNU Library General Public
+ License along with this library; if not, write to the Free
+ Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#ifndef __VERSION_H_
+#define __VERSION_H_
+
+#include <cstdio>
+#include <string>
+
+#include "common.h"
+#include "StringUtils.h"
+
+/**
+ * Manages versions as used by Sun's JVM. The version scheme used is
+ * major.minor.sub, for instance 1.1.8 or 1.4.2.
+ *
+ * @author Rodrigo Reyes <reyes at charabia.net>
+ */
+
+class Version
+{
+ private:
+ int m_major;
+ int m_minor;
+ int m_sub;
+
+ public:
+ std::string Value;
+
+ /**
+ * Creates a Version object from a string representation. This
+ * string needs to start as a normal version, but don't need to
+ * follow exactly the schema X.Y.Z. The Major number is mandatory,
+ * but the Minor and Sub numbers are optional.
+ *
+ * If the string representation does not represent a valid
+ * version, the object is said 'invalid', and returns false to the
+ * isInvalid() method.
+ *
+ * @param val a string representation of the version.
+ */
+ Version(std::string val);
+
+ /**
+ * Creates an invalid Version object. The object created returns
+ * false to the isValid() method.
+ */
+ Version();
+
+ /**
+ * Returns the major number of this Version.
+ */
+ int getMajor() const;
+ /**
+ * Returns the minor number of this Version.
+ */
+ int getMinor() const;
+ /**
+ * Returns the subminor number of this Version.
+ */
+ int getSubMinor() const;
+
+ std::string toString() const;
+
+ // bool operator > (const Version& v) const;
+
+ friend bool operator < (const Version& v1, const Version& v2);
+ friend bool operator <= (const Version& v1, const Version& v2);
+
+ /**
+ * A version object may be invalid if it does not refer to a valid
+ * version. In such a case, the behaviour of object the is altered
+ * as follows:
+ *
+ * - The getMajor(), getMinor(), and getSubMinor() methods return 0
+ * - Any comparison (< or <=) between an invalid Version object
+ * and another object (invalid or not) return true.
+ */
+ bool isValid() const;
+
+ private:
+ void parseValue(const std::string& val);
+
+ int extractIntAt(const std::string& val, int pos) const;
+
+ public:
+ struct AscendingSort
+ {
+ bool operator()(const Version& v1, const Version& v2)
+ {
+ if (v1.getMajor() > v2.getMajor())
+ return true;
+ if (v1.getMajor() < v2.getMajor())
+ return false;
+
+ if (v1.getMinor() > v2.getMinor())
+ return true;
+ if (v1.getMinor() < v2.getMinor())
+ return false;
+
+ if (v1.getSubMinor() > v2.getSubMinor())
+ return true;
+ if (v1.getSubMinor() < v2.getSubMinor())
+ return false;
+
+ return false;
+ }
+ };
+
+};
+
+
+
+#endif
diff --git a/jsmooth-0.9.9-7-patch/skeletons/consolewrapper/Makefile.win b/jsmooth-0.9.9-7-patch/skeletons/consolewrapper/Makefile.win
new file mode 100644
index 0000000..8913456
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/consolewrapper/Makefile.win
@@ -0,0 +1,32 @@
+# Project: ConsoleWrapper
+# Makefile created by Dev-C++ 4.9.8.0
+
+RM = rm -f
+CPP = ${MINGW}/bin/${TARGET}-g++.exe
+CC = ${MINGW}/bin/${TARGET}-gcc.exe
+WINDRES = windres.exe
+RES = ConsoleWrapper_private.res
+OBJ = main.o $(RES)
+LINKOBJ = main.o $(RES)
+LIBS = -static-libgcc -L"${MINGW}/lib" -L"../commonjava" -L"../util-core" ../commonjava/CommonJava.a ../util-core/util-core.a
+INCS = -I"/include" -I"../commonjava" -I"$(JDK)/include" -I"$(JDK)/include/win32"
+CXXINCS = -I"$(MINGW)/include/c++" -I"$(MINGW)/include/c++/mingw32" -I"$(MINGW)/include/c++/backward" -I"$(MINGW)/include" -I"$(JDK)/include" -I"$(JDK)/include/win32" -I"../commonjava" -I"../util-core" -I"../util-net" $(FLTK-CXXFLAGS)
+BIN = ConsoleWrapper.exe
+CXXFLAGS = $(CUSTOMFLAGS) $(CXXINCS) -DJDK="$(JDK)"
+CFLAGS = $(INCS)
+
+.PHONY: all all-before all-after clean clean-custom
+
+all: all-before ConsoleWrapper.exe all-after
+
+clean: clean-custom
+ $(RM) $(OBJ) $(BIN) ConsoleWrapper_private.res
+
+$(BIN): $(LINKOBJ) ../commonjava/CommonJava.a
+ $(CPP) $(LINKOBJ) -o "ConsoleWrapper.exe" $(LIBS)
+
+main.o: main.cpp
+ $(CPP) -c main.cpp -o main.o $(CXXFLAGS)
+
+ConsoleWrapper_private.res: ConsoleWrapper_private.rc mainres.rc
+ $(WINDRES) -i ConsoleWrapper_private.rc -J rc -o ConsoleWrapper_private.res -O coff
diff --git a/jsmooth-0.9.9-7-patch/skeletons/consolewrapper/description64.skel b/jsmooth-0.9.9-7-patch/skeletons/consolewrapper/description64.skel
new file mode 100644
index 0000000..2d2fced
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/consolewrapper/description64.skel
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<jsmoothskeleton>
+<debug>false</debug>
+<description>SKEL_CONSOLEWRAPPER_DESCRIPTION</description>
+<executableName>consolewrapper.exe</executableName>
+<resourceCategory>JAVA</resourceCategory>
+<resourceJarId>102</resourceJarId>
+<resourcePropsId>103</resourcePropsId>
+<shortName>Console Wrapper x64</shortName>
+<skeletonProperties>
+<description>SKEL_CUSTOMWRAPPER_PROPERTY_MESSAGE_DESCRIPTION</description>
+<idName>Message</idName>
+<label>SKEL_CUSTOMWRAPPER_PROPERTY_MESSAGE_LABEL</label>
+<type>textarea</type>
+<value>This program needs Java to run.
+Please download it at http://www.java.com</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_CUSTOMWRAPPER_PROPERTY_KEYPRESS_DESCRIPTION</description>
+<idName>PressKey</idName>
+<label>SKEL_CUSTOMWRAPPER_PROPERTY_KEYPRESS_LABEL</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_PROPERTY_DEBUG_DESCRIPTION</description>
+<idName>Debug</idName>
+<label>SKEL_GENERIC_PROPERTY_DEBUG_LABEL</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+</jsmoothskeleton>
diff --git a/jsmooth-0.9.9-7-patch/skeletons/consolewrapper/mainres.rc b/jsmooth-0.9.9-7-patch/skeletons/consolewrapper/mainres.rc
new file mode 100644
index 0000000..8bd397f
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/consolewrapper/mainres.rc
@@ -0,0 +1,8 @@
+
+#include "resource.h"
+
+JARID JAVA "samplejar/sample.jar"
+PROPID JAVA "samplejar/sample.props"
+
+A2 ICON MOVEABLE PURE LOADONCALL DISCARDABLE "JWrap.ico"
+1 MANIFEST "../samplejar/sample.manifest"
diff --git a/jsmooth-0.9.9-7-patch/skeletons/samplejar/sample.manifest b/jsmooth-0.9.9-7-patch/skeletons/samplejar/sample.manifest
new file mode 100644
index 0000000..3a9962e
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/samplejar/sample.manifest
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
+ <assemblyIdentity type="win32"
+ name="myOrganization.myDivision.mySampleApp"
+ version="0.0.0.0"
+ processorArchitecture="ia64/x86"
+ />
+ <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
+ <security>
+ <requestedPrivileges>
+ <requestedExecutionLevel level="requireAdministrator"/>
+ </requestedPrivileges>
+ </security>
+ </trustInfo>
+</assembly>
\ No newline at end of file
diff --git a/jsmooth-0.9.9-7-patch/skeletons/simplewrap/Makefile.win b/jsmooth-0.9.9-7-patch/skeletons/simplewrap/Makefile.win
new file mode 100644
index 0000000..5841f20
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/simplewrap/Makefile.win
@@ -0,0 +1,32 @@
+# Project: JWrap
+# Makefile created by Dev-C++ 4.9.8.0
+
+CPP = ${MINGW}/bin/${TARGET}-g++.exe
+CC = ${MINGW}/bin/${TARGET}-gcc.exe
+WINDRES = windres.exe
+RES = JWrap_private.res
+OBJ = main.o $(RES)
+LINKOBJ = main.o $(RES)
+LIBS = -static-libgcc -L"${MINGW}/lib" -L"../commonjava" -L"../util-core" -mwindows ../commonjava/CommonJava.a ../util-core/util-core.a
+INCS = -I"/include" -I"$(JDK)/include/win32" -I"$(JDK)/include" -I"../commonjava" -I"../util-core"
+CXXINCS = -I"$(MINGW)/include/c++" -I"$(MINGW)/include/c++/mingw32" -I"$(MINGW)/include/c++/backward" -I"$(MINGW)/include" -I"$(JDK)/include" -I"$(JDK)/include/win32" -I"../commonjava" -I"../util-core" -I"../util-net" $(FLTK-CXXFLAGS)
+BIN = JWrap.exe
+CXXFLAGS = $(CUSTOMFLAGS) $(CXXINCS) -DJDK="$(JDK)"
+CFLAGS = $(INCS)
+
+.PHONY: all all-before all-after clean clean-custom
+
+all: all-before JWrap.exe all-after
+
+
+clean: clean-custom
+ $(RM) $(OBJ) $(BIN) JWrap_private.res
+
+$(BIN): $(LINKOBJ) ../commonjava/CommonJava.a
+ $(CPP) $(LINKOBJ) -o "JWrap.exe" $(LIBS)
+
+main.o: main.cpp
+ $(CPP) -c main.cpp -o main.o $(CXXFLAGS)
+
+JWrap_private.res: JWrap_private.rc mainres.rc
+ $(WINDRES) -i JWrap_private.rc -J rc -o JWrap_private.res -O coff
diff --git a/jsmooth-0.9.9-7-patch/skeletons/simplewrap/description64.skel b/jsmooth-0.9.9-7-patch/skeletons/simplewrap/description64.skel
new file mode 100644
index 0000000..da01764
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/simplewrap/description64.skel
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<jsmoothskeleton>
+<debug>false</debug>
+<description>SKEL_SIMPLEWRAPPER_DESCRIPTION</description>
+<executableName>jwrap.exe</executableName>
+<resourceCategory>JAVA</resourceCategory>
+<resourceJarId>102</resourceJarId>
+<resourcePropsId>103</resourcePropsId>
+<shortName>Windowed Wrapper x64</shortName>
+<skeletonProperties>
+<description>SKEL_SIMPLEWRAPPER_PROPERTY_MESSAGE_DESCRIPTION</description>
+<idName>Message</idName>
+<label>SKEL_SIMPLEWRAPPER_PROPERTY_MESSAGE_LABEL</label>
+<type>textarea</type>
+<value>Java has not been found on your computer. Do you want to download it?</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_SIMPLEWRAPPER_PROPERTY_URL_DESCRIPTION</description>
+<idName>URL</idName>
+<label>SKEL_SIMPLEWRAPPER_PROPERTY_URL_LABEL</label>
+<type>string</type>
+<value>http://www.java.com</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_PROPERTY_SINGLEPROCESS_DESCRIPTION</description>
+<idName>SingleProcess</idName>
+<label>SKEL_GENERIC_PROPERTY_SINGLEPROCESS_LABEL</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_SINGLEINSTANCE_DESCRIPTION</description>
+<idName>SingleInstance</idName>
+<label>SKEL_GENERIC_SINGLEINSTANCE</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_JNISMOOTH_DESCRIPTION</description>
+<idName>JniSmooth</idName>
+<label>SKEL_GENERIC_JNISMOOTH</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_PROPERTY_DEBUG_DESCRIPTION</description>
+<idName>Debug</idName>
+<label>SKEL_GENERIC_PROPERTY_DEBUG_LABEL</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+</jsmoothskeleton>
diff --git a/jsmooth-0.9.9-7-patch/skeletons/simplewrap/mainres.rc b/jsmooth-0.9.9-7-patch/skeletons/simplewrap/mainres.rc
new file mode 100644
index 0000000..e082dad
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/simplewrap/mainres.rc
@@ -0,0 +1,8 @@
+#include "resource.h"
+
+JARID JAVA "../samplejar/sample.jar"
+PROPID JAVA "../samplejar/sample.props"
+JNISMOOTHID JAVA "../jnismooth/jnismooth.jar"
+
+A2 ICON MOVEABLE PURE LOADONCALL DISCARDABLE "JWrap.ico"
+1 MANIFEST "../samplejar/sample.manifest"
diff --git a/jsmooth-0.9.9-7-patch/skeletons/util-core/FileUtils.cpp b/jsmooth-0.9.9-7-patch/skeletons/util-core/FileUtils.cpp
new file mode 100644
index 0000000..f57ebff
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/util-core/FileUtils.cpp
@@ -0,0 +1,211 @@
+/*
+ JSmooth: a VM wrapper toolkit for Windows
+ Copyright (C) 2003-2007 Rodrigo Reyes <reyes at charabia.net>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Library General Public
+ License as published by the Free Software Foundation; either
+ version 2 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Library General Public License for more details.
+
+ You should have received a copy of the GNU Library General Public
+ License along with this library; if not, write to the Free
+ Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#include "FileUtils.h"
+
+string FileUtils::createTempFileName(const string& suffix)
+{
+ char temppath[512];
+ int tplen = GetTempPath(512, temppath);
+
+ int counter = 0;
+ string temp;
+ do {
+
+ temp = string(temppath) + "temp" + StringUtils::toString(counter) + suffix;
+ counter ++;
+
+ } while (GetFileAttributes(temp.c_str()) != 0xFFFFFFFF);
+
+ return temp;
+}
+
+
+bool FileUtils::fileExists(const string& filename)
+{
+ if (filename[0] == '"')
+ {
+ string unquoted = StringUtils::replace(filename, "\"", "");
+ return GetFileAttributes(unquoted.c_str()) != 0xFFFFFFFF;
+ }
+ return GetFileAttributes(filename.c_str()) != 0xFFFFFFFF;
+}
+
+bool FileUtils::fileExists(const std::string& path, const std::string& filename)
+{
+ std::string f = FileUtils::concFile(path, filename);
+ return fileExists(f);
+}
+
+vector<string> FileUtils::recursiveSearch(const string& pathdir, const string& pattern)
+{
+ vector<string> result;
+ string path = StringUtils::replace(pathdir, "\"", "");
+
+ WIN32_FIND_DATA data;
+ string file = path + ((path[path.length()-1]=='\\')?"":"\\") + pattern;
+
+ HANDLE handle = FindFirstFile(file.c_str(), &data);
+ if (handle != INVALID_HANDLE_VALUE)
+ {
+ result.push_back(path + ((path[path.length()-1]=='\\')?"":"\\") + data.cFileName);
+ for ( ; FindNextFile(handle, &data) == TRUE ; )
+ {
+ result.push_back(path + ((path[path.length()-1]=='\\')?"":"\\") + data.cFileName);
+ }
+
+ FindClose(handle);
+ }
+
+ handle = FindFirstFile((path + ((path[path.length()-1]=='\\')?"":"\\") + "*").c_str(), &data);
+ if (handle == INVALID_HANDLE_VALUE)
+ return result;
+
+ do {
+ string foundpath(data.cFileName);
+ if ((foundpath != ".") && (foundpath != "..") && ((data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0))
+ {
+ string npath = path + ((path[path.length()-1]=='\\')?"":"\\") + data.cFileName;
+ vector<string> tres = FileUtils::recursiveSearch(npath, pattern);
+ result.insert(result.end(), tres.begin(), tres.end());
+ }
+ } while (FindNextFile(handle, &data));
+ FindClose(handle);
+
+ return result;
+}
+
+std::string FileUtils::extractFileName(const std::string& filename)
+{
+ int start = filename.rfind("\\", filename.length()-1);
+ if (start != filename.npos)
+ {
+ return filename.substr(start+1);
+ }
+ return filename;
+}
+
+std::string FileUtils::getExecutablePath()
+{
+ char buffer[512];
+ GetModuleFileName(NULL, buffer, 512);
+ string full = buffer;
+ int pos = full.rfind('\\', full.npos);
+
+ return full.substr(0, pos+1);
+}
+
+std::string FileUtils::getExecutableFileName()
+{
+ char buffer[512];
+ GetModuleFileName(NULL, buffer, 512);
+ string full = buffer;
+ int pos = full.rfind('\\', full.npos);
+
+ return full.substr(pos+1);
+}
+
+std::string FileUtils::getParent(const std::string &path)
+{
+ if (path[path.length()-1] == '\\')
+ return getParent( path.substr(0, path.size() - 1) );
+
+ int pos = path.rfind('\\', path.npos);
+ if (pos != path.npos)
+ return path.substr(0, pos+1);
+ else
+ return path;
+}
+
+std::string FileUtils::getComputerName()
+{
+ char buffer[MAX_COMPUTERNAME_LENGTH + 1];
+ DWORD size = MAX_COMPUTERNAME_LENGTH+1;
+ GetComputerName(buffer, &size);
+ return buffer;
+}
+
+std::string FileUtils::concFile(std::string path, std::string file)
+{
+ if (FileUtils::isAbsolute(file))
+ return file;
+
+ if (path.length() > 0)
+ {
+ if (path[path.length()-1] != '\\')
+ path += '\\';
+ }
+ path += file;
+
+ return path;
+}
+
+std::string FileUtils::getFileExtension(const std::string& filename)
+{
+ int pos = filename.rfind('.');
+ if (pos != std::string::npos)
+ {
+ return filename.substr(pos+1);
+ }
+ return "";
+}
+
+bool FileUtils::isAbsolute(const std::string& filename)
+{
+ if (((filename.length()>2) && (filename[1] == ':') && (filename[2] =='\\')) || ((filename.length() >2) && (filename[0] == '\\') && (filename[1]=='\\')))
+ return true;
+
+ return false;
+}
+
+void FileUtils::deleteOnReboot(std::string file)
+{
+ MoveFileEx(file.c_str(), 0, MOVEFILE_DELAY_UNTIL_REBOOT);
+}
+
+std::string FileUtils::readFile(const std::string& filename)
+{
+ HANDLE f = CreateFile(filename.c_str(), GENERIC_READ,
+ FILE_SHARE_READ, NULL,
+ OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
+
+ std::string result;
+
+ if (f != INVALID_HANDLE_VALUE)
+ {
+ char buffer[129];
+ DWORD hasread;
+ buffer[127] = 0;
+
+ do {
+ ReadFile(f, buffer, 127, &hasread, NULL);
+ buffer[hasread] = 0;
+ result += buffer;
+ } while (hasread > 0);
+
+ CloseHandle(f);
+ }
+ else
+ {
+ // printf("Can't open file %s\n",filename.c_str());
+ }
+
+ return result;
+}
diff --git a/jsmooth-0.9.9-7-patch/skeletons/util-core/FileUtils.h b/jsmooth-0.9.9-7-patch/skeletons/util-core/FileUtils.h
new file mode 100644
index 0000000..665c6af
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/util-core/FileUtils.h
@@ -0,0 +1,127 @@
+/*
+ JSmooth: a VM wrapper toolkit for Windows
+ Copyright (C) 2003-2007 Rodrigo Reyes <reyes at charabia.net>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Library General Public
+ License as published by the Free Software Foundation; either
+ version 2 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Library General Public License for more details.
+
+ You should have received a copy of the GNU Library General Public
+ License along with this library; if not, write to the Free
+ Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#ifndef __FILEUTILS_H_
+#define __FILEUTILS_H_
+
+#include <windows.h>
+#include <string>
+#include <vector>
+
+#include "StringUtils.h"
+
+using namespace std;
+
+/** File access utility class.
+ * This class contains static class members that provide facilities
+ * for manamement of files.
+ *
+ * @author Rodrigo Reyes <reyes at charabia.net>
+ */
+
+class FileUtils
+{
+ public:
+
+ /**
+ * Creates a temp filename. This method builds a temporary file
+ * name in the default windows temporary directory.
+ *
+ * @param suffix the suffix to use for the filename.
+ */
+ static string createTempFileName(const string& suffix);
+
+ /**
+ * Test wether a file exists.
+ * @param filename the file name to check.
+ * @return true if the file exists, false otherwise.
+ */
+ static bool fileExists(const string& filename);
+
+ static bool fileExists(const std::string&path, const string& filename);
+
+ /**
+ * Lookup recursively for files matching a given pattern in a given
+ * path. The method scans all the directory provided and its
+ * subdirectories. Each file matching the pattern is added to the
+ * resulting vector of strings.
+ *
+ * @param path the path of the directory to scan.
+ * @param pattern the file pattern, for instance "*.zip".
+ * @return a vector of string that contains the result.
+ */
+ static vector<string> recursiveSearch(const string& path, const string& pattern);
+
+ /**
+ * Extracts the file part of a file path. Given a path of the form
+ * c:\\a\\b\\c\\d or c\\d, return the last component of the file path
+ * (that is, d in the previous example).
+ *
+ * @param filename an absolute or relative filename.
+ * @return a string with the file part of the file.
+ */
+ static std::string extractFileName(const std::string& filename);
+
+ /**
+ * Returns the path where the executable application is executed
+ * from. This is not the current directory, but the directory path
+ * where the application was found. For instance if the application
+ * was runned at c:\\programs\\bin\\test.exe, it returns
+ * c:\\programs\\bin\\.
+ *
+ * @return a std::string text that holds the path.
+ */
+ static std::string getExecutablePath();
+
+
+ static std::string getParent(const std::string &path);
+
+ /**
+ * Returns the name of the executable binary that was used to start
+ * the application.For instance if the application
+ * was runned at c:\\programs\\bin\\test.exe, it returns
+ * test.exe
+ *
+ * @return the executable name.
+ */
+ static std::string getExecutableFileName();
+
+
+ /**
+ * Returns the name of the computer.
+ * @return a std::string that holds the computer name.
+ */
+ static std::string getComputerName();
+
+ static std::string concFile(std::string path, std::string file);
+
+ static std::string getFileExtension(const std::string& filename);
+
+ static bool isAbsolute(const std::string& filename);
+
+ static std::string readFile(const std::string& filename);
+
+ static void deleteOnReboot(std::string file);
+
+};
+
+#endif
+
+
diff --git a/jsmooth-0.9.9-7-patch/skeletons/util-core/Makefile.win b/jsmooth-0.9.9-7-patch/skeletons/util-core/Makefile.win
new file mode 100644
index 0000000..29e409e
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/util-core/Makefile.win
@@ -0,0 +1,32 @@
+# Project: util-core
+
+PROJECTNAME=util-core
+RM = rm -f
+CPP = ${MINGW}/bin/${TARGET}-g++.exe
+CC = ${MINGW}/bin/${TARGET}-gcc.exe
+WINDRES = windres.exe
+MINGW =
+RES =
+OBJ = SingleInstanceManager.o Process.o Log.o Thread.o FileUtils.o StringUtils.o DebugConsole.o $(RES)
+LIBS = -static-libgcc -L"$(MINGW)/lib"
+INCS = -I"$(MINGW)/include" -I"$(JDK)/include" -I"$(JDK)/include/win32"
+CXXINCS = -Os -I"$(MINGW)/include/c++" -I"$(MINGW)/include/c++/mingw32" -I"$(MINGW)/include/c++/backward" -I"$(MINGW)/include" -I"$(JDK)/include" -I"$(JDK)/include/win32"
+BIN = $(PROJECTNAME).a
+CXXFLAGS = $(CUSTOMFLAGS) $(CXXINCS) -DJDK="$(JDK)"
+CFLAGS = $(INCS)
+
+.PHONY: all all-before all-after clean clean-custom $(PROJECTNAME)
+
+all: all-before $(PROJECTNAME).a testmain.cpp all-after
+
+
+clean: clean-custom
+ $(RM) $(OBJ) $(BIN) testmain.o test.exe
+
+$(BIN): $(OBJ)
+ ar r $(BIN) $(OBJ)
+ ranlib $(BIN)
+ $(CPP) $(OBJ) testmain.o -o test.exe $(LIBS)
+
+$(PROJECTNAME).a: $(OBJ) testmain.o
+
diff --git a/jsmooth-0.9.9-7-patch/skeletons/util-core/StringUtils.h b/jsmooth-0.9.9-7-patch/skeletons/util-core/StringUtils.h
new file mode 100644
index 0000000..f64078e
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/util-core/StringUtils.h
@@ -0,0 +1,163 @@
+/*
+ JSmooth: a VM wrapper toolkit for Windows
+ Copyright (C) 2003-2007 Rodrigo Reyes <reyes at charabia.net>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Library General Public
+ License as published by the Free Software Foundation; either
+ version 2 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Library General Public License for more details.
+
+ You should have received a copy of the GNU Library General Public
+ License along with this library; if not, write to the Free
+ Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#ifndef __STRINGUTILS_H_
+#define __STRINGUTILS_H_
+
+#include <cstdio>
+#include <string>
+#include <vector>
+#include <windows.h>
+
+using namespace std;
+
+/**
+ * Provides basic string operations.
+ *
+ * @author Rodrigo Reyes <reyes at charabia.net>
+ */
+
+class StringUtils
+{
+ public:
+ /**
+ * Splits a string in several substrings. The split method takes a
+ * string as input, and outputs a vector of string. The string split
+ * is done according to 2 parameters:
+ *
+ * - the separators parameter, defines which chars are
+ * separators. Typically, this is a string that contains spaces,
+ * line-feed, and tabulation chars (" \n\t").
+ *
+ * - the quotechars parameter, is useful when your input contains
+ * quoted string that should not be broken down into substrings. For
+ * instance, a typical quote char is a quote or a double quote
+ * (resp. ' and ").
+ *
+ * For example, is the input string is the line belows:
+ * - this is "my string" and this is 'another one'.
+ *
+ * Here is the result of the call with the following parameters:
+ * - separators = \\n\\r\\t
+ * - quotechars = \\"\\'
+ *
+ * The output string is:
+ * - this
+ * - is
+ * - my string
+ * - and
+ * - this
+ * - is
+ * - another one
+ *
+ * @param str the string to be splitted
+ * @param separators a string that contains all the characters to be used as separators.
+ * @param quotechars a string that contains all the characters that are used as quote chars.
+ */
+ static vector<string> split(const string& str, const string& separators, const string& quotechars, bool handleEscape = true, bool returnSeparators = false);
+
+ /**
+ * Converts a string to an int. If the string is not a valid
+ * integer, it returns 0.
+ *
+ * @param val the string to parse
+ * @return an integer value
+ */
+ static int parseInt(const string& val);
+
+ static int parseHexa(const string& val);
+
+ /**
+ * Convers an integer to a string.
+ *
+ * @param val an integer
+ * @return a string
+ */
+ static string toString(int val);
+ static string toHexString(int val);
+
+ /**
+ * Provides a string representation of a vector of string. A vector
+ * that contains 2 string aaa and bbb is represented as follows:
+ * [aaa, bbb].
+ *
+ * @param seq a vector of string
+ * @return a string representation
+ */
+ static string toString(const vector<string>& seq);
+
+ /**
+ * Copies the content of a string in a char array.
+ *
+ * @param from the string to copy from
+ * @param to the destination array of chars
+ * @param length the length of the array of char
+ */
+ static void copyTo(const string& from, char* to, int length);
+
+ /**
+ * Returns a copy of the string with the environment variables
+ * replaced. The environment variable must be surrounded by %
+ * signs. For each variable defined between 2 % signs, the method
+ * tries to replace it by the value of the corresponding environment
+ * variable. If no variable exists, it just replaces it with an
+ * empty string.
+ *
+ * @param str the string to transform
+ * @return the same string, but with all the environment variable replaced
+ */
+ static string replaceEnvironmentVariable(const string& str);
+
+ static string replace(const string& str, const string& pattern, const string& replacement);
+
+ static string join(const vector<string>& seq, const string& separator);
+ static string trim(string& str);
+
+ /**
+ * If a string does not start with a quote ("), it returns a copy of
+ * the string with enclosing quotes.
+ *
+ * @param str a string
+ * @return a fixed copy of the string
+ */
+ static std::string fixQuotes(const string& str);
+
+ static std::string escape(const string& str);
+ static std::string unescape(const string& str);
+ /**
+ * Ensures a string is correctly quoted, the quotes are enclosing
+ * the whole string, not a part of the string. For instance
+ * <<"c:\\my path"\\bin>> is transformed into <<"c:\\my path\\bin">>
+ *
+ * @param str a string
+ * @return a fixed copy of the string
+ */
+ static std::string requote(const string& str);
+ static std::string requoteForCommandLine(const string& str);
+ static std::string toLowerCase(const string& str);
+
+ static std::string fixArgumentString(const std::string& arg);
+
+ static std::string sizeToJavaString(int size);
+ static std::string sizeToJavaString(std::string size);
+
+};
+
+#endif
diff --git a/jsmooth-0.9.9-7-patch/skeletons/util-net/Makefile.win b/jsmooth-0.9.9-7-patch/skeletons/util-net/Makefile.win
new file mode 100644
index 0000000..32b2aaa
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/util-net/Makefile.win
@@ -0,0 +1,34 @@
+# Project: CommonJava
+
+PROJECTNAME=util-net
+RM = rm -f
+CPP = ${MINGW}/bin/${TARGET}-g++.exe
+CC = ${MINGW}/bin/${TARGET}-gcc.exe
+WINDRES = windres.exe
+MINGW = "c:/MinGW"
+FLTK-LDFLAGS = $(shell fltk-config --ldflags)
+FLTK-CXXFLAGS = $(shell fltk-config --cxxflags)
+RES =
+OBJ = WinHttpClient.o HttpClient.o URL.o downloadgui.o httpdownload.o $(RES)
+LIBS = -static-libgcc -L"$(MINGW)/lib" -lws2_32 -lwininet $(FLTK-LDFLAGS)
+CXXINCS = -Os -I"../util-core" -I"$(MINGW)/include/c++" -I"$(MINGW)/include/c++/mingw32" -I"$(MINGW)/include/c++/backward" -I"$(MINGW)/include" -I"$(JDK)/include" -I"$(JDK)/include/win32" $(FLTK-CXXFLAGS)
+BIN = $(PROJECTNAME).a
+CXXFLAGS = $(CUSTOMFLAGS) $(CXXINCS) -DJDK="$(JDK)"
+
+.PHONY: all all-before all-after clean clean-custom $(PROJECTNAME)
+
+all: all-before $(PROJECTNAME).a testmain.cpp all-after
+
+clean: clean-custom
+ $(RM) $(OBJ) $(BIN) test.exe testmain.o
+
+$(BIN): $(OBJ)
+ ar r $(BIN) $(OBJ)
+ ranlib $(BIN)
+ $(CPP) -g $(OBJ) ../util-core/util-core.a testmain.o -o test.exe $(LIBS)
+
+$(PROJECTNAME).a: $(OBJ) testmain.o
+
+
+testmain.o: testmain.cpp
+ $(CPP) -c testmain.cpp -o testmain.o $(CXXFLAGS)
diff --git a/jsmooth-0.9.9-7-patch/skeletons/winservice/Makefile.win b/jsmooth-0.9.9-7-patch/skeletons/winservice/Makefile.win
new file mode 100644
index 0000000..064aefc
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/winservice/Makefile.win
@@ -0,0 +1,33 @@
+# Project: WinService Wrapper
+# Makefile created by Dev-C++ 4.9.8.0
+
+RM = rm -f
+#cmd /C DEL
+CPP = ${MINGW}/bin/${TARGET}-g++.exe
+CC = ${MINGW}/bin/${TARGET}-gcc.exe
+WINDRES = windres.exe
+RES = winservice_private.res
+OBJ = main.o $(RES)
+LINKOBJ = WinService.o main.o $(RES)
+LIBS = -static-libgcc -L"${MINGW}/lib" -L"../commonjava" -L"../util-core" ../commonjava/CommonJava.a ../util-core/util-core.a
+INCS = -I"/include" -I"../util-core" -I"../commonjava" -I"$(JDK)/include" -I"$(JDK)/include/win32"
+CXXINCS = -I"$(MINGW)/include/c++" -I"$(MINGW)/include/c++/mingw32" -I"$(MINGW)/include/c++/backward" -I"$(MINGW)/include" -I"$(JDK)/include" -I"$(JDK)/include/win32" -I"../commonjava" -I"../util-core" -I"../util-net" $(FLTK-CXXFLAGS)
+BIN = winservice.exe
+CXXFLAGS = $(CUSTOMFLAGS) $(CXXINCS) -DJDK="$(JDK)"
+CFLAGS = $(INCS)
+
+.PHONY: all all-before all-after clean clean-custom
+
+all: all-before winservice.exe all-after
+
+clean: clean-custom
+ $(RM) $(OBJ) $(BIN) winservice.o winservice_private.res
+
+$(BIN): $(LINKOBJ) ../commonjava/CommonJava.a
+ $(CPP) $(LINKOBJ) -o "winservice.exe" $(LIBS)
+
+main.o: main.cpp
+ $(CPP) -c main.cpp -o main.o $(CXXFLAGS)
+
+winservice_private.res: winservice_private.rc mainres.rc
+ $(WINDRES) -i winservice_private.rc -J rc -o winservice_private.res -O coff
diff --git a/jsmooth-0.9.9-7-patch/skeletons/winservice/description64.skel b/jsmooth-0.9.9-7-patch/skeletons/winservice/description64.skel
new file mode 100644
index 0000000..bede8c4
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/winservice/description64.skel
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<jsmoothskeleton>
+<debug>false</debug>
+<description>SKEL_WINSERVICE_DESCRIPTION</description>
+<executableName>winservice.exe</executableName>
+<resourceCategory>JAVA</resourceCategory>
+<resourceJarId>102</resourceJarId>
+<resourcePropsId>103</resourcePropsId>
+<shortName>WinService Wrapper x64</shortName>
+<skeletonProperties>
+<description>SKEL_WINSERVICE_NAME_DESCRIPTION</description>
+<idName>ServiceName</idName>
+<label>SKEL_WINSERVICE_NAME_LABEL</label>
+<type>string</type>
+<value>myservice</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_WINSERVICE_DISPLAYNAME_DESCRIPTION</description>
+<idName>ServiceDisplayName</idName>
+<label>SKEL_WINSERVICE_DISPLAYNAME_LABEL</label>
+<type>string</type>
+<value>My Service</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_WINSERVICE_PROPERTY_MESSAGE_DESCRIPTION</description>
+<idName>Message</idName>
+<label>SKEL_WINSERVICE_PROPERTY_MESSAGE_LABEL</label>
+<type>textarea</type>
+<value>This program needs Java to run. Please download it at http://www.java.comf</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_WINSERVICE_AUTOSTART_DESCRIPTION</description>
+<idName>Autostart</idName>
+<label>SKEL_WINSERVICE_AUTOSTART_LABEL</label>
+<type>boolean</type>
+<value>1</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_WINSERVICE_LOGFILE_DESCRIPTION</description>
+<idName>Logfile</idName>
+<label>SKEL_WINSERVICE_LOGFILE_LABEL</label>
+<type>string</type>
+<value>service.log</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_WINSERVICE_INTERACTIVE_DESCRIPTION</description>
+<idName>Interactive</idName>
+<label>SKEL_WINSERVICE_INTERACTIVE_LABEL</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_JNISMOOTH_DESCRIPTION</description>
+<idName>JniSmooth</idName>
+<label>SKEL_GENERIC_JNISMOOTH</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_PROPERTY_DEBUG_DESCRIPTION</description>
+<idName>Debug</idName>
+<label>SKEL_GENERIC_PROPERTY_DEBUG_LABEL</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+</jsmoothskeleton>
diff --git a/jsmooth-0.9.9-7-patch/skeletons/winservice/mainres.rc b/jsmooth-0.9.9-7-patch/skeletons/winservice/mainres.rc
new file mode 100644
index 0000000..f2634f1
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/skeletons/winservice/mainres.rc
@@ -0,0 +1,9 @@
+
+#include "resource.h"
+
+JARID JAVA "../samplejar/sample.jar"
+PROPID JAVA "../samplejar/sample.props"
+JNISMOOTHID JAVA "../jnismooth/jnismooth.jar"
+
+A2 ICON MOVEABLE PURE LOADONCALL DISCARDABLE "JWrap.ico"
+1 MANIFEST "../samplejar/sample.manifest"
diff --git a/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/application/ExeCompiler.java b/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/application/ExeCompiler.java
new file mode 100644
index 0000000..d197382
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/application/ExeCompiler.java
@@ -0,0 +1,534 @@
+/*
+ JSmooth: a VM wrapper toolkit for Windows
+ Copyright (C) 2003 Rodrigo Reyes <reyes at charabia.net>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+ */
+
+package net.charabia.jsmoothgen.application;
+
+import net.charabia.jsmoothgen.pe.PEFile;
+import net.charabia.jsmoothgen.pe.PEResourceDirectory;
+import net.charabia.jsmoothgen.skeleton.SkeletonBean;
+
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.awt.image.IndexColorModel;
+import java.awt.image.PixelGrabber;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.channels.FileChannel;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.Vector;
+
+public class ExeCompiler {
+ private java.util.Vector m_errors = new java.util.Vector();
+ private Vector m_listeners = new Vector();
+
+ public interface StepListener {
+ public void setNewState(int percentComplete, String state);
+
+ public void failed();
+
+ public void complete();
+ }
+
+ public void addListener(ExeCompiler.StepListener listener) {
+ m_listeners.add(listener);
+
+ }
+
+ public void cleanErrors() {
+ m_errors.removeAllElements();
+ }
+
+ public java.util.Vector getErrors() {
+ return m_errors;
+ }
+
+ public class CompilerRunner implements Runnable {
+ private File m_skelroot;
+ private SkeletonBean m_skel;
+ private JSmoothModelBean m_data;
+ private File m_out;
+ private File m_basedir;
+
+ public CompilerRunner(File skelroot, SkeletonBean skel, File basedir, JSmoothModelBean data, File out) {
+ m_skelroot = skelroot;
+ m_skel = skel;
+ m_data = data;
+ m_out = out;
+ m_basedir = basedir;
+ }
+
+ public void run() {
+ try {
+ compile(m_skelroot, m_skel, m_basedir, m_data, m_out);
+ } catch (Exception exc) {
+ exc.printStackTrace();
+ }
+ }
+
+ public ExeCompiler getCompiler() {
+ return ExeCompiler.this;
+ }
+ }
+
+ public ExeCompiler.CompilerRunner getRunnable(File skelroot, SkeletonBean skel, File basedir, JSmoothModelBean data, File out) {
+ return new CompilerRunner(skelroot, skel, basedir, data, out);
+ }
+
+ public void compileAsync(File skelroot, SkeletonBean skel, File basedir, JSmoothModelBean data, File out) {
+ Thread t = new Thread(new CompilerRunner(skelroot, skel, basedir, data, out));
+ t.start();
+ }
+
+ public boolean compile(File skelroot, SkeletonBean skel, File basedir, JSmoothModelBean data, File out) throws Exception {
+ try {
+ fireStepChange(0, "Starting compilation");
+
+ File pattern = new File(skelroot, skel.getExecutableName());
+ if (pattern.exists() == false) {
+ m_errors.add("Error: Can't find any skeleton at " + skelroot);
+ fireFailedChange();
+ return false;
+ }
+
+ fireStepChange(10, "Scanning skeleton...");
+ PEFile pe = new PEFile(pattern);
+ pe.open();
+ PEResourceDirectory resdir = pe.getResourceDirectory();
+
+ boolean resb = false;
+
+ //
+ // Adds the jar only if the user selected one
+ //
+ if (data.getEmbeddedJar() == true) {
+ if (data.getJarLocation() == null) {
+ m_errors.add("Error: Jar is not specified!");
+ fireFailedChange();
+ return false;
+ }
+
+ fireStepChange(40, "Loading Jar...");
+ File jarloc = concFile(basedir, new File(data.getJarLocation()));
+ if (jarloc.exists() == false) {
+ m_errors.add("Error: Can't find jar at " + jarloc);
+ fireFailedChange();
+ return false;
+ }
+
+ ByteBuffer jardata = load(jarloc);
+
+ fireStepChange(60, "Adding Jar to Resources...");
+ resb = resdir.replaceResource(skel.getResourceCategory(), skel.getResourceJarId(), 1033, jardata);
+ if (resb == false) {
+ m_errors.add("Error: Can't replace jar resource! It is probably missing from the skeleton.");
+ fireFailedChange();
+ return false;
+ }
+ }
+
+ fireStepChange(70, "Adding Properties and Manifest to Resources...");
+ // Properties...
+ String props = PropertiesBuilder.makeProperties(basedir, data);
+ ByteBuffer propdata = convert(props);
+ resb = resdir.replaceResource(skel.getResourceCategory(), skel.getResourcePropsId(), 1033, propdata);
+
+ // Manifest...
+ String manifest = PropertiesBuilder.makeManifest(data, skel);
+ if (manifest == null || manifest.isEmpty()) {
+ manifest = "";
+ }
+ ByteBuffer manifestData = convert(manifest);
+ resb = resdir.replaceManifest(1, 1033, manifestData);
+
+ if (data.getIconLocation() != null) {
+ fireStepChange(80, "Loading icon...");
+ String iconpath;
+ if (new java.io.File(data.getIconLocation()).isAbsolute())
+ iconpath = data.getIconLocation();
+ else
+ iconpath = new java.io.File(basedir, data.getIconLocation()).getAbsolutePath();
+
+ Image img = getScaledImage(iconpath, 32, 32);
+ //Hashtable set = calculateColorCount(img);
+ // System.out.println("COLORS TOTAL 4: " + set.size());
+
+ if (img != null) {
+ net.charabia.jsmoothgen.pe.res.ResIcon32 resicon = new net.charabia.jsmoothgen.pe.res.ResIcon32(img);
+ pe.replaceDefaultIcon(resicon);
+ }
+ }
+
+ fireStepChange(90, "Saving exe...");
+ pe.dumpTo(out);
+
+ // System.out.println("PROPERTIES:\n" + props);
+
+ fireCompleteChange();
+ return true;
+ } catch (Exception exc) {
+ m_errors.add("Error: " + exc.getMessage());
+ exc.printStackTrace();
+ fireFailedChange();
+ return false;
+ }
+ }
+
+ public Image[] loadImages(String path) {
+ File f = new File(path);
+
+ if (path.toUpperCase().endsWith(".ICO")) {
+ //
+ // Try to load with our ico codec...
+ //
+ try {
+ java.awt.Image[] images = net.charabia.util.codec.IcoCodec.loadImages(f);
+ if ((images != null) && (images.length > 0)) {
+ return images;
+ }
+ } catch (java.io.IOException exc) {
+ exc.printStackTrace();
+ }
+ }
+
+ //
+ // defaults to the standard java loading process
+ //
+ BufferedImage bufferedImage;
+ try {
+ bufferedImage = ImageIO.read(f);
+ javax.swing.ImageIcon icon = new javax.swing.ImageIcon(bufferedImage, "default icon");
+ java.awt.Image[] imgs = new java.awt.Image[1];
+ imgs[0] = icon.getImage();
+ return imgs;
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ public void checkImageLoaded(Image img) {
+ MediaTracker mtrack = new MediaTracker(new Canvas());
+ mtrack.addImage(img, 1);
+ try {
+ mtrack.waitForAll();
+ } catch (InterruptedException e) {
+ }
+ }
+
+ private Hashtable calculateColorCount(Image img) {
+ int width = img.getWidth(null);
+ int height = img.getHeight(null);
+ int[] pixels = new int[width * height];
+ PixelGrabber grabber = new PixelGrabber(img, 0, 0, width, height, pixels, 0, width);
+ try {
+ grabber.grabPixels();
+ } catch (InterruptedException e) {
+ System.err.println("interrupted waiting for pixels!");
+ // throw new Exception("Can't load the image provided",e);
+ }
+
+
+ Hashtable result = new Hashtable();
+ int colorindex = 0;
+ for (int i = 0; i < pixels.length; i++) {
+ int pix = pixels[i];
+ if (((pix >> 24) & 0xFF) > 0) {
+ pix &= 0x00FFFFFF;
+ Integer pixi = new Integer(pix);
+ Object o = result.get(pixi);
+ if (o == null) {
+ result.put(pixi, new Integer(colorindex++));
+ }
+ // if (colorindex > 256)
+ // return result;
+ }
+ }
+ return result;
+ }
+
+ public BufferedImage getQuantizedImage(Image img) {
+ // 32 bit ico file already loaded as BufferedImage
+ if (img instanceof BufferedImage) {
+ return (BufferedImage) img;
+ } else {
+ int width = img.getWidth(null);
+ int height = img.getHeight(null);
+ int[][] data = new int[width][height];
+
+ int[] pixelbuffer = new int[width * height];
+ PixelGrabber grabber = new PixelGrabber(img, 0, 0, width, height, pixelbuffer, 0, width);
+ try {
+ grabber.grabPixels();
+ } catch (InterruptedException e) {
+ System.err.println("interrupted waiting for pixels!");
+ throw new RuntimeException("Can't load the image provided", e);
+ }
+ for (int i = 0; i < pixelbuffer.length; i++) {
+ data[i % width][i / width] = pixelbuffer[i];
+ }
+
+ int[][] savedata = new int[width][height];
+
+ for (int y = 0; y < height; y++)
+ for (int x = 0; x < width; x++)
+ savedata[x][y] = data[x][y];
+
+ int[] palette = net.charabia.util.codec.Quantize.quantizeImage(data, 255);
+ byte[] cmap = new byte[256 * 4];
+
+ for (int i = 0; i < palette.length; i++) {
+ // System.out.println(" i= " + (i));
+ cmap[(i * 4)] = (byte) ((palette[i] >> 16) & 0xFF);
+ cmap[(i * 4) + 1] = (byte) ((palette[i] >> 8) & 0xFF);
+ cmap[(i * 4) + 2] = (byte) (palette[i] & 0xFF);
+ cmap[(i * 4) + 3] = (byte) 0xFF;
+ }
+
+ IndexColorModel colmodel = new IndexColorModel(8, palette.length, cmap, 0, true, 0);
+ BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+ //
+ // The normal manner of quantizing would be to run
+ // result.setRGB(0,0, width, height, pixelbuffer, 0, width);
+ // where result is a BufferedImage of
+ // BufferedImage.TYPE_BYTE_INDEXED type. Unfortunately, I
+ // couldn't make it work. So, here is a work-around that
+ // should work similarly.
+ //
+ java.util.Hashtable set = new java.util.Hashtable();
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ int alpha = (savedata[x][y] >> 24) & 0xFF;
+ if (alpha == 0) {
+ result.setRGB(x, y, 0);
+ // System.out.print(".");
+ } else {
+ int rgb = colmodel.getRGB(data[x][y]);
+ rgb |= 0xFF000000;
+ set.put(new Integer(rgb), new Integer(rgb));
+ result.setRGB(x, y, rgb);
+ // System.out.print("*");
+ }
+ }
+ // System.out.println("");
+ }
+
+
+ return result;
+ }
+ }
+
+ public Image checkImageSize(Image img, int width, int height) {
+ int w = img.getWidth(null);
+ int h = img.getHeight(null);
+ if ((w == width) && (h == height))
+ return img;
+ return null;
+ }
+
+ public Image getScaledImage(String path, int width, int height) {
+ Image[] orgimages = loadImages(path);
+
+ if ((orgimages == null) || (orgimages.length == 0))
+ return null;
+
+ for (int i = 0; i < orgimages.length; i++)
+ checkImageLoaded(orgimages[i]);
+
+ // System.out.println("Loaded " + orgimages.length + " images");
+ for (int i = 0; (i < orgimages.length); i++) {
+ int w = orgimages[i].getWidth(null);
+ int h = orgimages[i].getHeight(null);
+ // System.out.println("Size of " + i + " = " + w + "," + h);
+ }
+
+ //
+ // We prefer 32x32 pictures, then 64x64, then 16x16...
+ //
+ Image selected = null;
+ for (int i = 0; (i < orgimages.length) && (selected == null); i++)
+ selected = checkImageSize(orgimages[i], 32, 32);
+ for (int i = 0; (i < orgimages.length) && (selected == null); i++)
+ selected = checkImageSize(orgimages[i], 64, 64);
+ for (int i = 0; (i < orgimages.length) && (selected == null); i++)
+ selected = checkImageSize(orgimages[i], 16, 16);
+
+ if (selected != null) {
+ return getQuantizedImage(selected);
+ }
+
+ //
+ // If there is no 32x32, 64x64, nor 16x16, then we scale the
+ // biggest image to be 32x32... This should happen mainly when
+ // loading an image from a png of gif file, and in most case
+ // there is only one image on the array.
+ //
+ int maxsize = 0;
+ Image biggest = null;
+ for (int i = 0; (i < orgimages.length) && (selected == null); i++) {
+ int size = orgimages[i].getWidth(null) * orgimages[i].getHeight(null);
+ if (size > maxsize) {
+ maxsize = size;
+ biggest = orgimages[i];
+ }
+ }
+
+ if (biggest != null) {
+ BufferedImage result = getScaledInstance((BufferedImage) biggest, 32, 32, RenderingHints.VALUE_INTERPOLATION_BILINEAR, true);
+ checkImageLoaded(result);
+ return getQuantizedImage(result);
+ }
+ //
+ // Here, we have failed and return null
+ //
+ return null;
+ }
+
+ /**
+ * Convenience method that returns a scaled instance of the
+ * provided {@code BufferedImage}.
+ *
+ * @param img the original image to be scaled
+ * @param targetWidth the desired width of the scaled instance,
+ * in pixels
+ * @param targetHeight the desired height of the scaled instance,
+ * in pixels
+ * @param hint one of the rendering hints that corresponds to
+ * {@code RenderingHints.KEY_INTERPOLATION} (e.g.
+ * {@code RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR},
+ * {@code RenderingHints.VALUE_INTERPOLATION_BILINEAR},
+ * {@code RenderingHints.VALUE_INTERPOLATION_BICUBIC})
+ * @param higherQuality if true, this method will use a multi-step
+ * scaling technique that provides higher quality than the usual
+ * one-step technique (only useful in downscaling cases, where
+ * {@code targetWidth} or {@code targetHeight} is
+ * smaller than the original dimensions, and generally only when
+ * the {@code BILINEAR} hint is specified)
+ * @return a scaled version of the original {@code BufferedImage}
+ */
+ public BufferedImage getScaledInstance(BufferedImage img,
+ int targetWidth,
+ int targetHeight,
+ Object hint,
+ boolean higherQuality) {
+ int type = (img.getTransparency() == Transparency.OPAQUE) ?
+ BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;
+ BufferedImage ret = (BufferedImage) img;
+ int w, h;
+ if (higherQuality) {
+ // Use multi-step technique: start with original size, then
+ // scale down in multiple passes with drawImage()
+ // until the target size is reached
+ w = img.getWidth();
+ h = img.getHeight();
+ } else {
+ // Use one-step technique: scale directly from original
+ // size to target size with a single drawImage() call
+ w = targetWidth;
+ h = targetHeight;
+ }
+
+ do {
+ if (higherQuality && w > targetWidth) {
+ w /= 2;
+ if (w < targetWidth) {
+ w = targetWidth;
+ }
+ }
+
+ if (higherQuality && h > targetHeight) {
+ h /= 2;
+ if (h < targetHeight) {
+ h = targetHeight;
+ }
+ }
+
+ BufferedImage tmp = new BufferedImage(w, h, type);
+ Graphics2D g2 = tmp.createGraphics();
+ g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint);
+ g2.drawImage(ret, 0, 0, w, h, null);
+ g2.dispose();
+
+ ret = tmp;
+ } while (w != targetWidth || h != targetHeight);
+
+ return ret;
+ }
+
+
+ private ByteBuffer load(File in) throws Exception {
+ FileInputStream fis = new FileInputStream(in);
+ ByteBuffer data = ByteBuffer.allocate((int) in.length());
+ data.order(ByteOrder.LITTLE_ENDIAN);
+ FileChannel fischan = fis.getChannel();
+ fischan.read(data);
+ data.position(0);
+ fis.close();
+
+ return data;
+ }
+
+ private ByteBuffer convert(String data) {
+ data = data.replace("\r\n", "\n").replace("\n", "\r\n");
+ ByteBuffer result = ByteBuffer.allocate(data.length());
+ result.position(0);
+
+ for (int i = 0; i < data.length(); i++) {
+ result.put((byte) data.charAt(i));
+ }
+ // result.put((byte)0);
+
+ result.position(0);
+ return result;
+ }
+
+ static public File concFile(File root, File name) {
+ if (name.isAbsolute())
+ return name;
+
+ return new File(root, name.toString());
+ }
+
+ public void fireStepChange(int percentComplete, String state) {
+ for (Iterator i = m_listeners.iterator(); i.hasNext();) {
+ ExeCompiler.StepListener l = (ExeCompiler.StepListener) i.next();
+ l.setNewState(percentComplete, state);
+ }
+ }
+
+ public void fireFailedChange() {
+ for (Iterator i = m_listeners.iterator(); i.hasNext();) {
+ ExeCompiler.StepListener l = (ExeCompiler.StepListener) i.next();
+ l.failed();
+ }
+ }
+
+ public void fireCompleteChange() {
+ for (Iterator i = m_listeners.iterator(); i.hasNext();) {
+ ExeCompiler.StepListener l = (ExeCompiler.StepListener) i.next();
+ l.complete();
+ }
+ }
+
+}
diff --git a/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/application/JSmoothModelBean.java b/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/application/JSmoothModelBean.java
new file mode 100644
index 0000000..5215a1c
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/application/JSmoothModelBean.java
@@ -0,0 +1,408 @@
+/*
+ JSmooth: a VM wrapper toolkit for Windows
+ Copyright (C) 2003 Rodrigo Reyes <reyes at charabia.net>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+ */
+
+/*
+ * JSmoothModelBean.java
+ *
+ * Created on 7 aout 2003, 18:32
+ */
+
+package net.charabia.jsmoothgen.application;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.Vector;
+
+public class JSmoothModelBean {
+ private String m_skeletonName;
+ private String m_executableName;
+ private String m_currentDirectory;
+
+ private String m_iconLocation;
+ private boolean m_embedJar = false;
+ private String m_jarLocation;
+ private String m_mainClassName;
+ private String m_arguments;
+ private String[] m_classPath;
+ private String m_minimumVersion = "";
+ private String m_maximumVersion = "";
+ private String[] m_jvmSearch = null;
+
+ private int m_maxHeap = -1;
+ private int m_initialHeap = -1;
+
+ private String m_vmParamter;
+
+ private int m_uacRequireAdmin;
+
+ private String m_noJvmMessage;
+ private String m_noJvmURL;
+
+ private String m_bundledJVM = null;
+
+ private JavaPropertyPair[] m_javaprops = new JavaPropertyPair[0];
+ private JSmoothModelBean.Property[] m_skelproperties;
+
+ static public class Property {
+ public String Key;
+ public String Value;
+
+ public void setKey(String key) {
+ this.Key = key;
+ }
+
+ public String getKey() {
+ return this.Key;
+ }
+
+ public void setValue(String val) {
+ this.Value = val;
+ }
+
+ public String getValue() {
+ return this.Value;
+ }
+
+ public String toString() {
+ return getKey() + "==" + getValue();
+ }
+ }
+
+ transient Vector m_listeners = new Vector();
+
+ public static interface Listener {
+ public void dataChanged();
+ }
+
+ transient Vector m_skeletonChangedListener = new Vector();
+
+ public static interface SkeletonChangedListener {
+ public void skeletonChanged();
+ }
+
+ /**
+ * Creates a new instance of JSmoothModelBean
+ */
+ public JSmoothModelBean() {
+ }
+
+ public void addListener(JSmoothModelBean.Listener l) {
+ m_listeners.add(l);
+ }
+
+ public void removeListener(JSmoothModelBean.Listener l) {
+ m_listeners.remove(l);
+ }
+
+ public void addSkeletonChangedListener(JSmoothModelBean.SkeletonChangedListener l) {
+ m_skeletonChangedListener.add(l);
+ }
+
+ public void removeSkeletonChangedListener(JSmoothModelBean.SkeletonChangedListener l) {
+ m_skeletonChangedListener.remove(l);
+ }
+
+
+ private void fireChanged() {
+ for (Iterator i = m_listeners.iterator(); i.hasNext();) {
+ JSmoothModelBean.Listener l = (JSmoothModelBean.Listener)i.next();
+ l.dataChanged();
+ }
+ }
+
+ private void fireSkeletonChanged() {
+ for (Iterator i = m_skeletonChangedListener.iterator(); i.hasNext();) {
+ JSmoothModelBean.SkeletonChangedListener l = (JSmoothModelBean.SkeletonChangedListener)i.next();
+ l.skeletonChanged();
+ }
+ }
+
+ public void setSkeletonName(String name) {
+ if (name != m_skeletonName) {
+ m_skeletonName = name;
+ fireSkeletonChanged();
+ fireChanged();
+ }
+ }
+
+ public String getSkeletonName() {
+ return m_skeletonName;
+ }
+
+ public void setExecutableName(String name) {
+ if (name != m_executableName) {
+ m_executableName = name;
+ fireChanged();
+ }
+ }
+
+
+ public void setCurrentDirectory(String curdir) {
+ if (curdir != m_currentDirectory) {
+ m_currentDirectory = curdir;
+ fireChanged();
+ }
+ }
+
+ public String getCurrentDirectory() {
+ return m_currentDirectory;
+ }
+
+ public String getExecutableName() {
+ return m_executableName;
+ }
+
+ public void setIconLocation(String name) {
+ if (name != m_iconLocation) {
+ m_iconLocation = name;
+ fireChanged();
+ }
+ }
+
+ public String getIconLocation() {
+ return m_iconLocation;
+ }
+
+ public boolean getEmbeddedJar() {
+ return m_embedJar;
+ }
+
+ public void setEmbeddedJar(boolean b) {
+ m_embedJar = b;
+ fireChanged();
+ }
+
+ public void setJarLocation(String name) {
+ if (name != m_jarLocation) {
+ m_jarLocation = name;
+ fireChanged();
+ }
+ }
+
+ public String getJarLocation() {
+ return m_jarLocation;
+ }
+
+
+ public void setMainClassName(String name) {
+ if (name != m_mainClassName) {
+ m_mainClassName = name;
+ fireChanged();
+ }
+ }
+
+ public String getMainClassName() {
+ return m_mainClassName;
+ }
+
+ public void setArguments(String args) {
+ m_arguments = args;
+ fireChanged();
+ }
+
+ public String getArguments() {
+ return m_arguments;
+ }
+
+ public void setClassPath(String[] cp) {
+ m_classPath = cp;
+ fireChanged();
+ }
+
+ public String[] getClassPath() {
+ return m_classPath;
+ }
+
+ public void setMaximumVersion(String version) {
+ m_maximumVersion = version;
+ fireChanged();
+ }
+
+ public String getMaximumVersion() {
+ return m_maximumVersion;
+ }
+
+ public void setMinimumVersion(String version) {
+ m_minimumVersion = version;
+ fireChanged();
+ }
+
+ public String getMinimumVersion() {
+ return m_minimumVersion;
+ }
+
+ public void setJVMSearchPath(String[] path) {
+ m_jvmSearch = path;
+ fireChanged();
+ }
+
+ public String[] getJVMSearchPath() {
+ return m_jvmSearch;
+ }
+
+ public void setSkeletonProperties(JSmoothModelBean.Property[] props) {
+ // for (int i=0; i<props.length; i++)
+ // System.out.println("SET PROPERTY: " + props[i].getIdName() + "=" + props[i].getValue());
+
+ m_skelproperties = props;
+ fireChanged();
+ }
+
+ public JSmoothModelBean.Property[] getSkeletonProperties() {
+ return m_skelproperties;
+ }
+
+ public void setNoJvmMessage(String msg) {
+ m_noJvmMessage = msg;
+ fireChanged();
+ }
+
+ public String getNoJvmMessage() {
+ return m_noJvmMessage;
+ }
+
+ public void setNoJvmURL(String url) {
+ m_noJvmURL = url;
+ fireChanged();
+ }
+
+ public String getNoJvmURL() {
+ return m_noJvmURL;
+ }
+
+ public String getBundledJVMPath() {
+ return m_bundledJVM;
+ }
+
+ public void setBundledJVMPath(String path) {
+ m_bundledJVM = path;
+ fireChanged();
+ }
+
+ public void setJavaProperties(JavaPropertyPair[] pairs) {
+ m_javaprops = pairs;
+ }
+
+ public JavaPropertyPair[] getJavaProperties() {
+ return m_javaprops;
+ }
+
+ public void setMaximumMemoryHeap(int size) {
+ m_maxHeap = size;
+ }
+
+ public int getMaximumMemoryHeap() {
+ return m_maxHeap;
+ }
+
+ public void setInitialMemoryHeap(int size) {
+ m_initialHeap = size;
+ }
+
+ public int getInitialMemoryHeap() {
+ return m_initialHeap;
+ }
+
+ public void setVmParameter(String parameter) {
+ m_vmParamter = parameter;
+ }
+
+ public String getVmParameter() {
+ return m_vmParamter;
+ }
+
+ public void setUacRequireAdministrator(int parameter) {
+ m_uacRequireAdmin = parameter;
+ }
+
+ public int getUacRequireAdministrator() {
+ return m_uacRequireAdmin;
+ }
+
+ public boolean isUacRequireAdmin() {
+ return m_uacRequireAdmin > 0;
+ }
+
+ public String[] normalizePaths(java.io.File filebase) {
+ return normalizePaths(filebase, true);
+ }
+
+ public String[] normalizePaths(java.io.File filebase, boolean toRelativePath) {
+ // System.out.println("Normalize Path " + filebase + " / " + toRelativePath);
+ Vector result = new Vector();
+
+ m_iconLocation = checkRelativePath(filebase, m_iconLocation, result, "Icon location", toRelativePath);
+ m_jarLocation = checkRelativePath(filebase, m_jarLocation, result, "Jar location", toRelativePath);
+ m_bundledJVM = checkRelativePath(filebase, m_bundledJVM, result, "Bundle JVM location", toRelativePath);
+ m_executableName = checkRelativePath(filebase, m_executableName, result, "Executable location", toRelativePath);
+
+ if (m_executableName != null) {
+ File exebase = new File(m_executableName);
+ if (exebase.isAbsolute() == false)
+ exebase = new File(filebase, exebase.toString()).getParentFile();
+
+ // System.out.println("EXE FILEBASE: " + exebase.toString());
+ if ((m_currentDirectory != null) && (m_currentDirectory.indexOf("${") >= 0))
+ m_currentDirectory = checkRelativePath(exebase, m_currentDirectory, result, "Current directory", toRelativePath);
+ }
+
+ if (m_classPath != null) {
+ for (int i = 0; i < m_classPath.length; i++) {
+ m_classPath[i] = checkRelativePath(filebase, m_classPath[i], result, "Classpath entry (" + i + ")", toRelativePath);
+ }
+ }
+
+ if (result.size() == 0)
+ return null;
+
+ String[] res = new String[result.size()];
+ result.toArray(res);
+
+ return res;
+ }
+
+ private String checkRelativePath(java.io.File root, String value, java.util.Vector errors, String name, boolean toRelativePath) {
+ if (value == null)
+ return value;
+
+ if (toRelativePath) {
+ File nf = JSmoothModelPersistency.makePathRelativeIfPossible(root, new File(value));
+ if (nf.isAbsolute()) {
+ errors.add(name);
+ }
+ return nf.toString();
+ } else {
+ File nf = new File(value);
+ if (nf.isAbsolute() == false) {
+ nf = new File(root, value);
+ nf = nf.getAbsoluteFile();
+
+ try {
+ nf = nf.getCanonicalFile();
+ nf = nf.getAbsoluteFile();
+ } catch (IOException iox) {
+ // do nothing
+ }
+ }
+ return nf.toString();
+ }
+ }
+}
diff --git a/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/application/PropertiesBuilder.java b/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/application/PropertiesBuilder.java
new file mode 100644
index 0000000..82b7421
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/application/PropertiesBuilder.java
@@ -0,0 +1,250 @@
+/*
+ JSmooth: a VM wrapper toolkit for Windows
+ Copyright (C) 2003 Rodrigo Reyes <reyes at charabia.net>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+ */
+
+package net.charabia.jsmoothgen.application;
+
+import java.io.File;
+
+import net.charabia.jsmoothgen.skeleton.SkeletonBean;
+
+/**
+ * @author Rodrigo
+ */
+public class PropertiesBuilder {
+ /**
+ * Creates a text containing all the relevant properties of a
+ * JSmoothModelBean object. The properties are output in the form
+ * "key=value".
+ * <p/>
+ * <p/>
+ * Note that all the paths are converted to be made relative to
+ * the basedir parameter provided. All the paths converted are
+ * expected to be relative to the targetted executable binary
+ * (before the conversion is applied, that is).
+ */
+ static public String makeProperties(File basedir, JSmoothModelBean obj) {
+ StringBuffer out = new StringBuffer();
+
+ addPair("arguments", obj.getArguments(), out);
+ addPair("mainclassname", obj.getMainClassName(), out);
+ addPair("jvmsearch", makePathConc(obj.getJVMSearchPath()), out);
+ addPair("minversion", obj.getMinimumVersion(), out);
+ addPair("maxversion", obj.getMaximumVersion(), out);
+
+ addPair("currentdir", obj.getCurrentDirectory(), out);
+
+ if (obj.getEmbeddedJar() && (obj.getJarLocation().trim().length() > 0)) {
+ addPair("embedjar", "true", out);
+ } else {
+ addPair("embedjar", "false", out);
+ }
+
+ if (obj.getMaximumMemoryHeap() > 1) {
+ addPair("maxheap", Integer.toString(obj.getMaximumMemoryHeap()), out);
+ }
+
+ if (obj.getInitialMemoryHeap() > 1) {
+ addPair("initialheap", Integer.toString(obj.getInitialMemoryHeap()), out);
+ }
+
+ if ((obj.getVmParameter() != null) && (!obj.getVmParameter().isEmpty())) {
+ addPair("vmparameter", obj.getVmParameter(), out);
+ }
+ // BundledVM & classpaths are changed to be accessible
+ // from the current directory
+ File curdir = new File(obj.getExecutableName()).getParentFile();
+
+ if (curdir == null)
+ curdir = basedir.getAbsoluteFile();
+
+ if (curdir.isAbsolute() == false) {
+ curdir = new File(basedir, curdir.toString());
+ }
+
+
+ // System.out.println("... curdir1 : " + curdir.toString());
+
+ if (obj.getCurrentDirectory() != null) {
+ File newcurdir = new File(obj.getCurrentDirectory());
+ // System.out.println("... curdir1.5 : " + obj.getCurrentDirectory());
+
+ if (!"${EXECUTABLEPATH}".equalsIgnoreCase(obj.getCurrentDirectory())) {
+ if (newcurdir.isAbsolute() == false) {
+ curdir = new File(curdir, newcurdir.toString());
+ } else
+ curdir = newcurdir;
+ }
+ }
+ // System.out.println("... curdir2 : " + curdir.toString());
+
+ if (obj.getBundledJVMPath() != null)
+ addPair("bundledvm", getRenormalizedPathIfNeeded(obj.getBundledJVMPath(), basedir, curdir), out);
+
+ if (obj.getClassPath() != null) {
+ String[] relcp = new String[obj.getClassPath().length];
+ for (int i = 0; i < relcp.length; i++) {
+ relcp[i] = getRenormalizedPathIfNeeded(obj.getClassPath()[i], basedir, curdir);
+ }
+ addPair("classpath", makePathConc(relcp), out);
+ }
+
+ //
+ // Adds all the skeleton-specific properties
+ //
+ if (obj.getSkeletonProperties() != null) {
+ for (int i = 0; i < obj.getSkeletonProperties().length; i++) {
+ JSmoothModelBean.Property prop = obj.getSkeletonProperties()[i];
+ if (prop.getKey() != null) {
+ String val = prop.getValue();
+ if (val == null)
+ val = "";
+ addPair("skel_" + prop.getKey(), val, out);
+ }
+ }
+ }
+
+
+ //
+ // Adds all the java properties. Those properties are
+ // typically passed as -Dname=value arguments for the sun's
+ // JVM.
+ //
+
+ JavaPropertyPair[] javapairs = obj.getJavaProperties();
+ if (javapairs != null) {
+ addPair("javapropertiescount", new Integer(javapairs.length).toString(), out);
+ for (int i = 0; i < javapairs.length; i++) {
+ addPair("javaproperty_name_" + i, javapairs[i].getName(), out);
+ addPair("javaproperty_value_" + i, javapairs[i].getValue(), out);
+ }
+ }
+
+ return out.toString();
+ }
+
+ /**
+ * Create a manifest entry for windows right elevation
+ *
+ * @param data
+ * @param skel
+ * @return the manifest xml
+ */
+ public static String makeManifest(JSmoothModelBean data, SkeletonBean skel) {
+ StringBuilder retVal = new StringBuilder();
+ String platform = "x86";
+ String shortName = skel.getShortName();
+ if (shortName.contains("64")) {
+ platform = "ia64";
+ }
+ retVal.append("<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>").append("\n");
+ retVal.append("<assembly xmlns=\"urn:schemas-microsoft-com:asm.v1\" manifestVersion=\"1.0\">").append("\n");
+ retVal.append(" <assemblyIdentity type=\"win32\"").append("\n");
+ retVal.append(" name=\"").append(shortName).append("\"").append("\n");
+ retVal.append(" version=\"1.0.0.0\"").append("\n");
+ retVal.append(" processorArchitecture=\"").append(platform).append("\"").append("\n");
+ retVal.append(" />").append("\n");
+ retVal.append(" <dependency>").append("\n");
+ retVal.append(" <dependentAssembly>").append("\n");
+ retVal
+ .append(
+ " <assemblyIdentity type=\"win32\" name=\"Microsoft.Windows.Common-Controls\" version=\"6.0.0.0\" processorArchitecture=\"*\" publicKeyToken=\"6595b64144ccf1df\" language=\"*\"/>")
+ .append("\n");
+ retVal.append(" </dependentAssembly>").append("\n");
+ retVal.append(" </dependency>").append("\n");
+ if (data.isUacRequireAdmin()) {
+ retVal.append(" <trustInfo xmlns=\"urn:schemas-microsoft-com:asm.v2\">").append("\n");
+ retVal.append(" <security>").append("\n");
+ retVal.append(" <requestedPrivileges>").append("\n");
+ retVal.append(" <requestedExecutionLevel level=\"requireAdministrator\"/>").append("\n");
+ retVal.append(" </requestedPrivileges>").append("\n");
+ retVal.append(" </security>").append("\n");
+ retVal.append(" </trustInfo>").append("\n");
+ }
+ retVal.append("</assembly>");
+ return retVal.toString();
+ }
+
+ /**
+ * Converts a path relative to previousbasedir into a path
+ * relative to newbasedir.
+ */
+ static public String getRenormalizedPathIfNeeded(String value, File previousbasedir, File newbasedir) {
+ // File f = new File(value);
+ // if (f.isAbsolute())
+ // return value;
+
+ if (newbasedir == null)
+ return value;
+
+ if (value == null)
+ return "";
+
+ File abs = new File(previousbasedir, value).getAbsoluteFile();
+ File n = JSmoothModelPersistency.makePathRelativeIfPossible(newbasedir, abs);
+
+ return n.toString();
+ }
+
+ static public String escapeString(String str) {
+ if (str == null)
+ return "";
+
+ StringBuffer out = new StringBuffer();
+ for (int i = 0; i < str.length(); i++) {
+ char c = str.charAt(i);
+ switch (c) {
+ case '\n':
+ out.append("\\n");
+ break;
+ case '\t':
+ out.append("\\t");
+ break;
+ case '\r':
+ out.append("\\r");
+ break;
+ case '\\':
+ out.append("\\\\");
+ break;
+ default:
+ out.append(c);
+ }
+ }
+ return out.toString();
+ }
+
+ static private void addPair(String name, String value, StringBuffer out) {
+ out.append(escapeString(name));
+ out.append("=");
+ out.append(escapeString(value));
+ out.append("\n");
+ }
+
+ static public String makePathConc(String[] elements) {
+ StringBuffer buf = new StringBuffer();
+ if (elements != null)
+ for (int i = 0; i < elements.length; i++) {
+ buf.append(elements[i]);
+ if ((i + 1) < elements.length)
+ buf.append(";");
+ }
+ return buf.toString();
+ }
+
+}
diff --git a/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/application/gui/editors/ExecutableIcon.java b/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/application/gui/editors/ExecutableIcon.java
new file mode 100644
index 0000000..e7940dd
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/application/gui/editors/ExecutableIcon.java
@@ -0,0 +1,159 @@
+/*
+ JSmooth: a VM wrapper toolkit for Windows
+ Copyright (C) 2003 Rodrigo Reyes <reyes at charabia.net>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+ */
+
+package net.charabia.jsmoothgen.application.gui.editors;
+
+import net.charabia.jsmoothgen.skeleton.*;
+import net.charabia.jsmoothgen.application.*;
+import net.charabia.jsmoothgen.application.gui.*;
+import net.charabia.jsmoothgen.application.gui.util.*;
+import javax.swing.*;
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.util.*;
+import java.io.*;
+import com.l2fprod.common.swing.*;
+import com.l2fprod.common.propertysheet.*;
+
+public class ExecutableIcon extends Editor
+{
+ private FileSelectionTextField m_selector = new FileSelectionTextField();
+ private JLabel m_iconDisplay = new JLabel("(no image)");
+
+ public ExecutableIcon()
+ {
+ setLayout(new BorderLayout());
+ add(BorderLayout.CENTER, m_selector);
+ add(BorderLayout.SOUTH, m_iconDisplay);
+
+ m_iconDisplay.setHorizontalAlignment(JLabel.CENTER);
+
+ m_selector.addListener(new FileSelectionTextField.FileSelected() {
+ public void fileSelected(String filename)
+ {
+// System.out.println("new icon: " + filename);
+ setIconLocation(new File(filename));
+ }
+ });
+ }
+
+ public void dataChanged()
+ {
+ if (getBaseDir() != null)
+ m_selector.setBaseDir(getBaseDir());
+
+ if (m_model.getIconLocation() != null)
+ {
+ m_selector.setFile(getAbsolutePath(new java.io.File(m_model.getIconLocation())));
+ setIconLocation(getAbsolutePath(new java.io.File(m_model.getIconLocation())));
+
+ }
+ else
+ {
+ m_selector.setFile(null);
+ setIconLocation(new File(""));
+ }
+ }
+
+ public void updateModel()
+ {
+ File f = m_selector.getFile();
+ if (f != null)
+ m_model.setIconLocation(m_selector.getFile().toString());
+ else
+ m_model.setIconLocation(null);
+ }
+
+ public String getLabel()
+ {
+ return "ICONLOCATION_LABEL";
+ }
+
+ public String getDescription()
+ {
+ return "ICONLOCATION_HELP";
+ }
+
+ private void setIconLocation(File iconfile)
+ {
+ if (iconfile.isAbsolute() == false)
+ {
+ iconfile = new File(m_basedir, iconfile.toString());
+ }
+ ImageIcon icon = null;
+
+// System.out.println("setIconLocation: " + iconfile);
+
+ if (iconfile.toString().toUpperCase().endsWith(".ICO"))
+ {
+ //
+ // Try to load with our ico codec...
+ //
+ try {
+ java.awt.image.BufferedImage image = net.charabia.util.codec.IcoCodec.getPreferredImage(iconfile);
+ if (image != null)
+ {
+ icon = new ImageIcon(image);
+ }
+ } catch (java.io.IOException exc)
+ {
+ exc.printStackTrace();
+ }
+ }
+ else // Otherwise try with the standard toolkit functions...
+ {
+ BufferedImage bufferedImage;
+ try {
+ bufferedImage = ImageIO.read(iconfile);
+ icon = new javax.swing.ImageIcon(bufferedImage, "default icon");
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+
+ if (icon != null)
+ {
+ int width = icon.getIconWidth();
+ int height = icon.getIconHeight();
+
+ m_iconDisplay.setIcon(icon);
+ m_iconDisplay.setText("");
+ m_model.setIconLocation(iconfile.getAbsolutePath());
+ this.validate();
+ this.invalidate();
+ }
+ else
+ {
+ m_iconDisplay.setIcon(null);
+ m_iconDisplay.setText("(no image)");
+ m_model.setIconLocation(null);
+ }
+
+ doLayout();
+ invalidate();
+ validate();
+ repaint();
+ }
+
+
+}
diff --git a/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/application/gui/util/HTMLPane.java b/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/application/gui/util/HTMLPane.java
new file mode 100644
index 0000000..0129c4b
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/application/gui/util/HTMLPane.java
@@ -0,0 +1,126 @@
+/*
+ JSmooth: a VM wrapper toolkit for Windows
+ Copyright (C) 2003 Rodrigo Reyes <reyes at charabia.net>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+ */
+
+package net.charabia.jsmoothgen.application.gui.util;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import javax.swing.border.*;
+import java.io.*;
+import java.util.*;
+import javax.swing.text.html.*;
+import javax.swing.event.*;
+import java.net.*;
+
+/**
+ *
+ */
+
+public class HTMLPane extends JPanel
+{
+ private JScrollPane m_scroller;
+ private JEditorPane m_html;
+ private URL m_baseurl;
+
+ edu.stanford.ejalbert.BrowserLauncher m_launcher;
+
+ class Hyperactive implements HyperlinkListener {
+
+ public void hyperlinkUpdate(HyperlinkEvent e) {
+ if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
+
+ JEditorPane pane = (JEditorPane)e.getSource();
+ if (e instanceof HTMLFrameHyperlinkEvent) {
+ HTMLFrameHyperlinkEvent evt = (HTMLFrameHyperlinkEvent)e;
+ HTMLDocument doc = (HTMLDocument)pane.getDocument();
+ doc.processHTMLFrameHyperlinkEvent(evt);
+ } else {
+ try {
+ URL nurl = e.getURL();
+ if (nurl == null)
+ nurl = new URL(m_baseurl, e.getDescription());
+ if (jsmooth.Native.isAvailable())
+ {
+ jsmooth.Native.shellExecute(jsmooth.Native.SHELLEXECUTE_OPEN, nurl.toString(), null, null, jsmooth.Native.SW_NORMAL);
+ }
+ else
+ m_launcher.openURLinBrowser(nurl.toExternalForm());
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ }
+ }
+ }
+ }
+
+
+ public HTMLPane()
+ {
+ try {
+ m_baseurl = new File(".").toURI().toURL();
+ } catch (Exception ex) { ex.printStackTrace(); }
+ m_html = new JEditorPane("text/html","<html></html>") {
+ public boolean getScrollableTracksViewportWidth()
+ {
+ return true;
+ }
+ };
+ HTMLEditorKit hek = new HTMLEditorKit();
+ m_html.setEditorKit(hek);
+
+ m_scroller = new JScrollPane(m_html);
+ setLayout(new BorderLayout());
+ m_html.setEditable(false);
+ add(m_scroller, BorderLayout.CENTER);
+ // add(m_html, BorderLayout.CENTER);
+ m_html.addHyperlinkListener(new Hyperactive());
+
+ try {
+ m_launcher = new edu.stanford.ejalbert.BrowserLauncher();
+ }catch (Exception ex)
+ {
+ ex.printStackTrace();
+ }
+
+ }
+
+ public java.awt.Dimension getPreferredSize()
+ {
+ return new java.awt.Dimension(200,200);
+ }
+
+ public void setPage(URL url)
+ {
+ try {
+ URL u = new URL(m_baseurl, url.toExternalForm());
+ m_html.setPage(u);
+ } catch (Exception ex) { ex.printStackTrace(); }
+ }
+
+ public void setText(String s)
+ {
+ m_html.setContentType("text/html");
+ m_html.setText(s);
+ m_html.setCaretPosition(0);
+ }
+
+
+}
diff --git a/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/pe/PEFile.java b/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/pe/PEFile.java
new file mode 100644
index 0000000..e4bb992
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/pe/PEFile.java
@@ -0,0 +1,478 @@
+/*
+ JSmooth: a VM wrapper toolkit for Windows
+ Copyright (C) 2003 Rodrigo Reyes <reyes at charabia.net>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+ */
+
+/*
+ * PEFile.java
+ *
+ * Created on 28 juillet 2003, 21:28
+ */
+
+package net.charabia.jsmoothgen.pe;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.Vector;
+
+import net.charabia.jsmoothgen.pe.res.ResIcon;
+import net.charabia.jsmoothgen.pe.res.ResIconDir;
+
+/**
+ * @author Rodrigo
+ */
+public class PEFile {
+ private File m_file;
+ private FileInputStream m_in = null;
+ private FileChannel m_channel = null;
+
+ private PEOldMSHeader m_oldmsheader;
+ private PEHeader m_header;
+ private Vector m_sections = new Vector();
+
+ private PEResourceDirectory m_resourceDir;
+
+ /**
+ * Creates a new instance of PEFile
+ */
+ public PEFile(File f) {
+ m_file = f;
+ }
+
+ public void close() throws IOException {
+ m_in.close();
+ }
+
+ public void open() throws FileNotFoundException, IOException {
+ m_in = new FileInputStream(m_file);
+ m_channel = m_in.getChannel();
+
+ m_oldmsheader = new PEOldMSHeader(this);
+
+ m_oldmsheader.read();
+ // m_oldmsheader.dump(System.out);
+ long headoffset = m_oldmsheader.e_lfanew;
+
+ m_header = new PEHeader(this, headoffset);
+ m_header.read();
+ // m_header.dump(System.out);
+
+ int seccount = m_header.NumberOfSections;
+ // System.out.println("LOADING " + seccount + " sections...");
+
+ long offset = headoffset + (m_header.NumberOfRvaAndSizes * 8) + 24 + getPeHeaderOffset();
+
+ for (int i = 0; i < seccount; i++) {
+ // System.out.println("Offset: " + offset + " (" + this.m_channel.position());
+
+ PESection sect = new PESection(this, offset);
+ sect.read();
+ // sect.dump(System.out);
+ m_sections.add(sect);
+ offset += 40;
+ }
+ // System.out.println("After sections: " + this.m_channel.position() + " (" + offset + ")");
+
+ ByteBuffer resbuf = null;
+ long resourceoffset = m_header.ResourceDirectory_VA;
+ for (int i = 0; i < seccount; i++) {
+ PESection sect = (PESection)m_sections.get(i);
+ if (sect.VirtualAddress == resourceoffset) {
+ // System.out.println(" Resource section found: " + resourceoffset);
+ PEResourceDirectory prd = new PEResourceDirectory(this, sect);
+ resbuf = prd.buildResource(sect.VirtualAddress);
+ break;
+ }
+ }
+ }
+
+ private int getPeHeaderOffset() {
+ int pe32Offset = 96;
+ if (m_header.isPe32Plus()) {
+ // It is a pe32+ header (x64)
+ pe32Offset = 112;
+ }
+ return pe32Offset;
+ }
+
+ public FileChannel getChannel() {
+ return m_channel;
+ }
+
+ public static void main(String args[]) throws IOException, CloneNotSupportedException, Exception {
+ // (no)PEFile pe = new PEFile(new File("F:/Program Files/LAN Search PRO/lansearch.exe"));
+
+ PEFile pe = new PEFile(new File("c:/scratch/rc3.exe"));
+ // PEFile pe = new PEFile(new File("c:/projects/jwrap/Copie.exe"));
+ // PEFile pe = new PEFile(new File("c:/projects/jwrap/test.exe"));
+ // PEFile pe = new PEFile(new File("F:/Program Files/bQuery/bQuery.exe"));
+ // PEFile pe = new PEFile(new File("F:/Program Files/Server Query/query.exe"));
+ // PEFile pe = new PEFile(new File("F:/Program Files/AvRack/rtlrack.exe"));
+ pe.open();
+ System.out.println("OldMSHeader");
+ pe.m_oldmsheader.dump(System.out);
+ System.out.println("COFFHeader");
+ pe.m_header.dump(System.out);
+
+ // System.out.println("===============\nADDING A RES");
+ // File fout = new File("F:/Documents and Settings/Rodrigo/Mes documents/projects/jsmooth/skeletons/simplewrap/gen-application.jar");
+ // FileInputStream fis = new FileInputStream(fout);
+ //
+ // ByteBuffer data = ByteBuffer.allocate((int)fout.length());
+ // data.order(ByteOrder.LITTLE_ENDIAN);
+ // FileChannel fischan = fis.getChannel();
+ // fischan.read(data);
+ // data.position(0);
+ // fis.close();
+
+ PEResourceDirectory resdir = pe.getResourceDirectory();
+ System.out.println("ResourceDirectory");
+ resdir.dump(System.out);
+
+ // DataEntry inputResData = resdir.getData("JAVA", "#" + String.valueOf(103), "#" + String.valueOf(1033));
+ // ByteBuffer inputResDataBuffer = ByteBuffer.allocate(inputResData.diskSize() + 1024);
+ // inputResDataBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ // inputResData.buildBuffer(inputResDataBuffer, 0, 0);
+ // int inputResDataBufferSize = inputResDataBuffer.position();
+ // inputResDataBuffer.flip();
+ // int offset = inputResDataBuffer.getInt();
+ // inputResDataBuffer.position(offset);
+ // StringBuilder inputResDataString = new StringBuilder(inputResDataBufferSize);
+ // while (inputResDataBuffer.position() <= inputResDataBufferSize - 2) {
+ // byte dummyByte = inputResDataBuffer.get();
+ // inputResDataString.append((char)dummyByte);
+ // }
+ // // Modify the data...
+ // String newInputResDataString = inputResDataString.toString();
+ // newInputResDataString = newInputResDataString.replace("samplejar", "ThisIsMyJarAndOnlyMine");
+ //
+ // inputResDataBuffer = ByteBuffer.allocate(newInputResDataString.length() + 2);
+ // for (int index = 0; index < newInputResDataString.length(); index++) { // C- do not change because buffer can be modified during loop
+ // inputResDataBuffer.put((byte)newInputResDataString.charAt(index));
+ // }
+ // inputResDataBuffer.put((byte)0);
+ // inputResDataBuffer.put((byte)0);
+ // inputResDataBuffer.position(0);
+ //
+ // boolean resb = resdir.replaceResource("JAVA", 102, 1033, inputResDataBuffer);
+
+ // PEResourceDirectory.DataEntry entry = resdir.getData("#14", "A", "#1033");
+ // entry.Data.position(0);
+ // System.out.println("DataEntry found : " + entry + " (size=" + entry.Data.remaining() + ")");
+ // entry.Data.position(0);
+ //
+ // ResIconDir rid = new ResIconDir(entry.Data);
+ // System.out.println("ResIconDir :");
+ // System.out.println(rid.toString());
+ // int iconid = rid.getEntries()[0].dwImageOffset;
+ // System.out.println("Icon Index: " + iconid);
+ //
+ // PEResourceDirectory.DataEntry iconentry = resdir.getData("#3", "#"+iconid, "#1033");
+ // iconentry.Data.position(0);
+ // ResIcon icon = new ResIcon(iconentry.Data);
+ // System.out.println("Icon :");
+ // System.out.println(icon.toString());
+
+ // java.awt.Image img = java.awt.Toolkit.getDefaultToolkit().getImage ("c:\\test.gif");
+ // java.awt.Image img = java.awt.Toolkit.getDefaultToolkit().getImage ("c:\\gnome-day2.png");
+ // java.awt.Image img = java.awt.Toolkit.getDefaultToolkit().getImage("c:\\gnome-color-browser2.png");
+ //
+ // java.awt.MediaTracker mt = new java.awt.MediaTracker(new javax.swing.JLabel("toto"));
+ // mt.addImage(img, 1);
+ // try {
+ // mt.waitForAll();
+ // } catch (Exception exc) {
+ // exc.printStackTrace();
+ // }
+ //
+ // ResIcon newicon = new ResIcon(img);
+ //
+ // pe.replaceDefaultIcon(newicon);
+
+ // System.out.println("-----------------\nNEW ICON:");
+ // System.out.println(newicon.toString());
+ //
+ // rid.getEntries()[0].bWidth = (short)newicon.Width;
+ // rid.getEntries()[0].bHeight = (short)(newicon.Height/2);
+ // rid.getEntries()[0].bColorCount = (short)(1 <<newicon.BitsPerPixel);
+ // rid.getEntries()[0].wBitCount = newicon.BitsPerPixel;
+ // rid.getEntries()[0].dwBytesInRes = newicon.getData().remaining();
+ //
+ // iconentry.Data = newicon.getData();
+ // iconentry.Size = iconentry.Data.remaining();
+ //
+ // entry.setData(rid.getData());
+ // System.out.println("POST CHANGE ResIconDir :");
+ // System.out.println(rid.toString());
+
+ // ResIcon test = new ResIcon(icon.getData());
+ // System.out.println("PROOF-TEST:\n" + test.toString());
+
+ // / BACK
+ //
+ // rid.getEntries()[0].bWidth = (short)icon.Width;
+ // rid.getEntries()[0].bHeight = (short)(icon.Height/2);
+ // rid.getEntries()[0].bColorCount = (short)(1 <<icon.BitsPerPixel);
+ // rid.getEntries()[0].wBitCount = icon.BitsPerPixel;
+ // iconentry.Data = icon.getData();
+ // iconentry.Size = iconentry.Data.remaining();
+
+ // resdir.addNewResource("POUET", "A666", "#1033", data);
+
+ // resdir.dump(System.out);
+
+ // System.out.println("New size = " + resdir.size());
+ File out = new File("c:/scratch/COPIE.exe");
+ pe.dumpTo(out);
+
+ }
+
+ public PEResourceDirectory getResourceDirectory() throws IOException {
+ if (m_resourceDir != null)
+ return m_resourceDir;
+
+ long resourceoffset = m_header.ResourceDirectory_VA;
+ for (int i = 0; i < m_sections.size(); i++) {
+ PESection sect = (PESection)m_sections.get(i);
+ if (sect.VirtualAddress == resourceoffset) {
+ m_resourceDir = new PEResourceDirectory(this, sect);
+ return m_resourceDir;
+ }
+ }
+
+ return null;
+ }
+
+ public void dumpTo(File destination) throws IOException, CloneNotSupportedException {
+ int outputcount = 0;
+ FileOutputStream fos = new FileOutputStream(destination);
+ FileChannel out = fos.getChannel();
+
+ //
+ // Make a copy of the Header, for safe modifications
+ //
+ PEOldMSHeader oldmsheader = (PEOldMSHeader)this.m_oldmsheader.clone();
+ PEHeader peheader = (PEHeader)m_header.clone();
+ Vector sections = new Vector();
+ for (int i = 0; i < m_sections.size(); i++) {
+ PESection sect = (PESection)m_sections.get(i);
+ PESection cs = (PESection)sect.clone();
+ sections.add(cs);
+ }
+
+ //
+ // First, write the old MS Header, the one starting
+ // with "MZ"...
+ //
+ long newexeoffset = oldmsheader.e_lfanew;
+ ByteBuffer msheadbuffer = oldmsheader.get();
+ outputcount = out.write(msheadbuffer);
+ this.m_channel.position(64);
+ out.transferFrom(this.m_channel, 64, newexeoffset - 64);
+
+
+ //
+ // Then Write the new Header...
+ //
+ ByteBuffer headbuffer = peheader.get();
+ out.position(newexeoffset);
+ outputcount = out.write(headbuffer);
+
+ //
+ // After the header, there are all the section
+ // headers...
+ //
+ long offset = oldmsheader.e_lfanew + (m_header.NumberOfRvaAndSizes * 8) + 24 + getPeHeaderOffset();
+ out.position(offset);
+ for (int i = 0; i < sections.size(); i++) {
+ // System.out.println(" offset: " + out.position());
+ PESection sect = (PESection)sections.get(i);
+
+ ByteBuffer buf = sect.get();
+ outputcount = out.write(buf);
+ }
+
+ //
+ // Now, we write the real data: each of the section
+ // and their data...
+ //
+
+ // Not sure why it's always at 1024... ?
+ offset = 1024;
+
+ //
+ // Dump each section data
+ //
+
+ long virtualAddress = offset;
+ if ((virtualAddress % peheader.SectionAlignment) > 0)
+ virtualAddress += peheader.SectionAlignment - (virtualAddress % peheader.SectionAlignment);
+
+ long resourceoffset = m_header.ResourceDirectory_VA;
+ for (int i = 0; i < sections.size(); i++) {
+ PESection sect = (PESection)sections.get(i);
+ if (resourceoffset == sect.VirtualAddress) {
+ // System.out.println("Dumping RES section " + i + " at " + offset + " from " + sect.PointerToRawData + " (VA=" + virtualAddress + ")");
+ out.position(offset);
+ long sectoffset = offset;
+ PEResourceDirectory prd = this.getResourceDirectory();
+ ByteBuffer resbuf = prd.buildResource(sect.VirtualAddress);
+ resbuf.position(0);
+
+ out.write(resbuf);
+ offset += resbuf.capacity();
+ long rem = offset % this.m_header.FileAlignment;
+ if (rem != 0)
+ offset += this.m_header.FileAlignment - rem;
+
+ if (out.size() + 1 < offset) {
+ ByteBuffer padder = ByteBuffer.allocate(1);
+ out.write(padder, offset - 1);
+ }
+
+ long virtualSize = resbuf.capacity();
+ if ((virtualSize % peheader.SectionAlignment) > 0)
+ virtualSize += peheader.SectionAlignment - (virtualSize % peheader.SectionAlignment);
+
+ sect.PointerToRawData = sectoffset;
+ sect.SizeOfRawData = resbuf.capacity();
+ if ((sect.SizeOfRawData % this.m_header.FileAlignment) > 0)
+ sect.SizeOfRawData += (this.m_header.FileAlignment - (sect.SizeOfRawData % this.m_header.FileAlignment));
+ sect.VirtualAddress = virtualAddress;
+ sect.VirtualSize = virtualSize;
+ // System.out.println(" VS=" + virtualSize + " at VA=" + virtualAddress);
+ virtualAddress += virtualSize;
+
+ } else if (sect.PointerToRawData > 0) {
+ // System.out.println("Dumping section " + i + "/" + sect.getName() + " at " + offset + " from " + sect.PointerToRawData + " (VA=" + virtualAddress + ")");
+ out.position(offset);
+ this.m_channel.position(sect.PointerToRawData);
+ long sectoffset = offset;
+
+ out.position(offset + sect.SizeOfRawData);
+ ByteBuffer padder = ByteBuffer.allocate(1);
+ out.write(padder, offset + sect.SizeOfRawData - 1);
+
+ long outted = out.transferFrom(this.m_channel, offset, sect.SizeOfRawData);
+ offset += sect.SizeOfRawData;
+ // System.out.println("offset before alignment, " + offset);
+
+ long rem = offset % this.m_header.FileAlignment;
+ if (rem != 0) {
+ offset += this.m_header.FileAlignment - rem;
+ }
+ // System.out.println("offset after alignment, " + offset);
+
+ // long virtualSize = sect.SizeOfRawData;
+ // if ((virtualSize % peheader.SectionAlignment)>0)
+ // virtualSize += peheader.SectionAlignment - (virtualSize%peheader.SectionAlignment);
+
+ sect.PointerToRawData = sectoffset;
+ // sect.SizeOfRawData =
+ sect.VirtualAddress = virtualAddress;
+ // sect.VirtualSize = virtualSize;
+
+ virtualAddress += sect.VirtualSize;
+ if ((virtualAddress % peheader.SectionAlignment) > 0)
+ virtualAddress += peheader.SectionAlignment - (virtualAddress % peheader.SectionAlignment);
+
+ } else {
+ // generally a BSS, with a virtual size but no
+ // data in the file...
+ // System.out.println("Dumping section " + i + " at " + offset + " from " + sect.PointerToRawData + " (VA=" + virtualAddress + ")");
+ long virtualSize = sect.VirtualSize;
+ if ((virtualSize % peheader.SectionAlignment) > 0)
+ virtualSize += peheader.SectionAlignment - (virtualSize % peheader.SectionAlignment);
+
+ sect.VirtualAddress = virtualAddress;
+ // sect.VirtualSize = virtualSize;
+ virtualAddress += virtualSize;
+
+ }
+ }
+
+ //
+ // Now that all the sections have been written, we have the
+ // correct VirtualAddress and Sizes, so we can update the new
+ // header and all the section headers...
+
+ peheader.updateVAAndSize(m_sections, sections);
+ headbuffer = peheader.get();
+ out.position(newexeoffset);
+ outputcount = out.write(headbuffer);
+
+ // peheader.dump(System.out);
+ // System.out.println("Dumping the section again...");
+ offset = oldmsheader.e_lfanew + (m_header.NumberOfRvaAndSizes * 8) + 24 + getPeHeaderOffset();
+ out.position(offset);
+ for (int i = 0; i < sections.size(); i++) {
+ // System.out.println(" offset: " + out.position());
+ PESection sect = (PESection)sections.get(i);
+ // sect.dump(System.out);
+ ByteBuffer buf = sect.get();
+ outputcount = out.write(buf);
+ }
+
+ fos.flush();
+ fos.close();
+ }
+
+ /*
+ */
+
+ public void replaceDefaultIcon(ResIcon icon) throws Exception {
+ PEResourceDirectory resdir = getResourceDirectory();
+
+ PEResourceDirectory.DataEntry entry = resdir.getData("#14", null, null);
+ if (entry == null) {
+ throw new Exception("Can't find any icon group in the file!");
+ }
+
+ entry.Data.position(0);
+ // System.out.println("DataEntry found : " + entry + " (size=" + entry.Data.remaining() + ")");
+ entry.Data.position(0);
+
+ ResIconDir rid = new ResIconDir(entry.Data);
+ // System.out.println("ResIconDir :");
+ // System.out.println(rid.toString());
+ int iconid = rid.getEntries()[0].dwImageOffset;
+ // System.out.println("Icon Index: " + iconid);
+
+ PEResourceDirectory.DataEntry iconentry = resdir.getData("#3", "#" + iconid, null);
+ iconentry.Data.position(0);
+ // System.out.println("Icon :");
+ // System.out.println(icon.toString());
+
+ rid.getEntries()[0].bWidth = (short)icon.Width;
+ rid.getEntries()[0].bHeight = (short)(icon.Height / 2);
+ rid.getEntries()[0].bColorCount = (short)(1 << icon.BitsPerPixel);
+ rid.getEntries()[0].wBitCount = icon.BitsPerPixel;
+ rid.getEntries()[0].dwBytesInRes = icon.getData().remaining();
+
+ iconentry.Data = icon.getData();
+ iconentry.Size = iconentry.Data.remaining();
+
+ entry.setData(rid.getData());
+ }
+
+}
diff --git a/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/pe/PEHeader.java b/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/pe/PEHeader.java
new file mode 100644
index 0000000..da96141
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/pe/PEHeader.java
@@ -0,0 +1,482 @@
+/*
+ JSmooth: a VM wrapper toolkit for Windows
+ Copyright (C) 2003 Rodrigo Reyes <reyes at charabia.net>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+ */
+
+/*
+ * PEHeader.java
+ *
+ * Created on 28 juillet 2003, 21:38
+ */
+
+package net.charabia.jsmoothgen.pe;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.channels.FileChannel;
+import java.util.Vector;
+
+/**
+ * @author Rodrigo Reyes
+ */
+public class PEHeader implements Cloneable {
+ private int PeMagic; // 0
+ public int Machine; // 4
+ public int NumberOfSections; // 6
+ public long TimeDateStamp; // 8
+ public long PointerToSymbolTable; // C
+ public long NumberOfSymbols; // 10
+ public int SizeOfOptionalHeader; // 14
+ public int Characteristics; // 16
+
+ // Optional Header
+ public int Magic; // 18
+ public short MajorLinkerVersion; // 1a
+ public short MinorLinkerVersion; // 1b
+ public long SizeOfCode; // 1c
+ public long SizeOfInitializedData; // 20
+ public long SizeOfUninitializedData; // 24
+ public long AddressOfEntryPoint; // 28
+ public long BaseOfCode; // 2c
+ public long BaseOfData; // NT additional fields. 30
+ public long ImageBase; // 34
+ public long SectionAlignment; // 38
+ public long FileAlignment; // 3c
+ public int MajorOperatingSystemVersion; // 40
+ public int MinorOperatingSystemVersion; // 42
+ public int MajorImageVersion; // 44
+ public int MinorImageVersion; // 46
+ public int MajorSubsystemVersion; // 48
+ public int MinorSubsystemVersion; // 4a
+ public long Reserved1; // 4c
+ public long SizeOfImage; // 50
+ public long SizeOfHeaders; // 54
+ public long CheckSum; // 58
+ public int Subsystem; // 5c
+ public int DllCharacteristics; // 5e
+ public long SizeOfStackReserve; // 60
+ public long SizeOfStackCommit; // 64
+ public long SizeOfHeapReserve; // 68
+ public long SizeOfHeapCommit; // 6c
+ public long LoaderFlags; // 70
+ public long NumberOfRvaAndSizes; // 74
+
+ public long ExportDirectory_VA; // 78
+ public long ExportDirectory_Size; // 7c
+ public long ImportDirectory_VA; // 80
+ public long ImportDirectory_Size; // 84
+ public long ResourceDirectory_VA; // 88
+ public long ResourceDirectory_Size; // 8c
+ public long ExceptionDirectory_VA; // 90
+ public long ExceptionDirectory_Size; // 94
+ public long SecurityDirectory_VA; // 98
+ public long SecurityDirectory_Size; // 9c
+ public long BaseRelocationTable_VA; // a0
+ public long BaseRelocationTable_Size; // a4
+ public long DebugDirectory_VA; // a8
+ public long DebugDirectory_Size; // ac
+ public long ArchitectureSpecificData_VA; // b0
+ public long ArchitectureSpecificData_Size; // b4
+ public long RVAofGP_VA; // b8
+ public long RVAofGP_Size; // bc
+ public long TLSDirectory_VA; // c0
+ public long TLSDirectory_Size; // c4
+ public long LoadConfigurationDirectory_VA; // c8
+ public long LoadConfigurationDirectory_Size; // cc
+ public long BoundImportDirectoryinheaders_VA; // d0
+ public long BoundImportDirectoryinheaders_Size; // d4
+ public long ImportAddressTable_VA; // d8
+ public long ImportAddressTable_Size; // dc
+ public long DelayLoadImportDescriptors_VA; // e0
+ public long DelayLoadImportDescriptors_Size; // e4
+ public long COMRuntimedescriptor_VA; // e8
+ public long COMRuntimedescriptor_Size; // ec
+
+ private long m_baseoffset;
+ private PEFile m_pe;
+
+ /**
+ * Creates a new instance of PEHeader
+ */
+ public PEHeader(PEFile pef, long baseoffset) {
+ m_pe = pef;
+ m_baseoffset = baseoffset;
+ }
+
+ public Object clone() throws CloneNotSupportedException {
+ return super.clone();
+ }
+
+ public void read() throws IOException {
+ FileChannel ch = m_pe.getChannel();
+ ByteBuffer head = ByteBuffer.allocate(350);
+ head.order(ByteOrder.LITTLE_ENDIAN);
+ ch.position(m_baseoffset);
+ ch.read(head);
+ head.position(0);
+
+ PeMagic = head.getInt();
+ // System.out.println("MAGIC::: " + pemagic);
+ Machine = head.getShort(); // 4
+ NumberOfSections = head.getShort(); // 6
+ TimeDateStamp = head.getInt(); // 8
+ PointerToSymbolTable = head.getInt(); // C
+ NumberOfSymbols = head.getInt(); // 10
+ SizeOfOptionalHeader = head.getShort(); // 14
+ Characteristics = head.getShort(); // 16
+ // Optional Header
+
+ Magic = head.getShort(); // 18
+ MajorLinkerVersion = head.get(); // 1a
+ MinorLinkerVersion = head.get(); // 1b
+ SizeOfCode = head.getInt(); // 1c
+ SizeOfInitializedData = head.getInt(); // 20
+ SizeOfUninitializedData = head.getInt(); // 24
+ AddressOfEntryPoint = head.getInt(); // 28
+ BaseOfCode = head.getInt(); // 2c
+ if (isPe32Plus()) {
+ ImageBase = head.getLong(); // 34
+ } else {
+ BaseOfData = head.getInt(); // // NT additional fields. // 30
+ ImageBase = head.getInt(); // 34
+ }
+ SectionAlignment = head.getInt(); // 38
+ FileAlignment = head.getInt(); // 3c
+ MajorOperatingSystemVersion = head.getShort(); // 40
+ MinorOperatingSystemVersion = head.getShort(); // 42
+ MajorImageVersion = head.getShort(); // 44
+ MinorImageVersion = head.getShort(); // 46
+ MajorSubsystemVersion = head.getShort(); // 48
+ MinorSubsystemVersion = head.getShort(); // 4a
+ Reserved1 = head.getInt(); // 4c
+ SizeOfImage = head.getInt(); // 50
+ SizeOfHeaders = head.getInt(); // 54
+ CheckSum = head.getInt(); // 58
+ Subsystem = head.getShort(); // 5c
+ DllCharacteristics = head.getShort(); // 5e
+ if (isPe32Plus()) {
+ SizeOfStackReserve = head.getLong(); // 60
+ SizeOfStackCommit = head.getLong(); // 64
+ SizeOfHeapReserve = head.getLong(); // 68
+ SizeOfHeapCommit = head.getLong(); // 6c
+ } else {
+ SizeOfStackReserve = head.getInt(); // 60
+ SizeOfStackCommit = head.getInt(); // 64
+ SizeOfHeapReserve = head.getInt(); // 68
+ SizeOfHeapCommit = head.getInt(); // 6c
+ }
+ LoaderFlags = head.getInt(); // 70
+ NumberOfRvaAndSizes = head.getInt(); // 74
+
+ ExportDirectory_VA = head.getInt(); // 78
+ ExportDirectory_Size = head.getInt(); // 7c
+ ImportDirectory_VA = head.getInt(); // 80
+ ImportDirectory_Size = head.getInt(); // 84
+ ResourceDirectory_VA = head.getInt(); // 88
+ ResourceDirectory_Size = head.getInt(); // 8c
+ ExceptionDirectory_VA = head.getInt(); // 90
+ ExceptionDirectory_Size = head.getInt(); // 94
+ SecurityDirectory_VA = head.getInt(); // 98
+ SecurityDirectory_Size = head.getInt(); // 9c
+ BaseRelocationTable_VA = head.getInt(); // a0
+ BaseRelocationTable_Size = head.getInt(); // a4
+ DebugDirectory_VA = head.getInt(); // a8
+ DebugDirectory_Size = head.getInt(); // ac
+ ArchitectureSpecificData_VA = head.getInt(); // b0
+ ArchitectureSpecificData_Size = head.getInt(); // b4
+ RVAofGP_VA = head.getInt(); // b8
+ RVAofGP_Size = head.getInt(); // bc
+ TLSDirectory_VA = head.getInt(); // c0
+ TLSDirectory_Size = head.getInt(); // c4
+ LoadConfigurationDirectory_VA = head.getInt(); // c8
+ LoadConfigurationDirectory_Size = head.getInt(); // cc
+ BoundImportDirectoryinheaders_VA = head.getInt(); // d0
+ BoundImportDirectoryinheaders_Size = head.getInt(); // d4
+ ImportAddressTable_VA = head.getInt(); // d8
+ ImportAddressTable_Size = head.getInt(); // dc
+ DelayLoadImportDescriptors_VA = head.getInt(); // e0
+ DelayLoadImportDescriptors_Size = head.getInt(); // e4
+ COMRuntimedescriptor_VA = head.getInt(); // e8
+ COMRuntimedescriptor_Size = head.getInt(); // ec
+ }
+
+ public void dump(PrintStream out) {
+ out.println("HEADER:");
+ out.println("int Machine=" + Machine + " // 4");
+ out.println("int NumberOfSections=" + NumberOfSections + " // 6");
+ out.println("long TimeDateStamp=" + TimeDateStamp + " // 8");
+ out.println("long PointerToSymbolTable=" + PointerToSymbolTable + " // C");
+ out.println("long NumberOfSymbols=" + NumberOfSymbols + " // 10");
+ out.println("int SizeOfOptionalHeader=" + SizeOfOptionalHeader + " // 14");
+ out.println("int Characteristics=" + Characteristics + " // 16");
+ // Optional Header
+
+ out.println("int Magic=" + Magic + " // 18");
+ out.println("short MajorLinkerVersion=" + MajorLinkerVersion + " // 1a");
+ out.println("short MinorLinkerVersion=" + MinorLinkerVersion + " // 1b");
+ out.println("long SizeOfCode=" + SizeOfCode + " // 1c");
+ out.println("long SizeOfInitializedData=" + SizeOfInitializedData + " // 20");
+ out.println("long SizeOfUninitializedData=" + SizeOfUninitializedData + " // 24");
+ out.println("long AddressOfEntryPoint=" + AddressOfEntryPoint + " // 28");
+ out.println("long BaseOfCode=" + BaseOfCode + " // 2c");
+ out.println("long BaseOfData=" + BaseOfData + " // // NT additional fields. // 30");
+ //
+ out.println("long ImageBase=" + ImageBase + " // 34");
+ out.println("long SectionAlignment=" + SectionAlignment + " // 38");
+ out.println("long FileAlignment=" + FileAlignment + " // 3c");
+ out.println("int MajorOperatingSystemVersion=" + MajorOperatingSystemVersion + " // 40");
+ out.println("int MinorOperatingSystemVersion=" + MinorOperatingSystemVersion + " // 42");
+ out.println("int MajorImageVersion=" + MajorImageVersion + " // 44");
+ out.println("int MinorImageVersion=" + MinorImageVersion + " // 46");
+ out.println("int MajorSubsystemVersion=" + MajorSubsystemVersion + " // 48");
+ out.println("int MinorSubsystemVersion=" + MinorSubsystemVersion + " // 4a");
+ out.println("long Reserved1=" + Reserved1 + " // 4c");
+ out.println("long SizeOfImage=" + SizeOfImage + " // 50");
+ out.println("long SizeOfHeaders=" + SizeOfHeaders + " // 54");
+ out.println("long CheckSum=" + CheckSum + " // 58");
+ out.println("int Subsystem=" + Subsystem + " // 5c");
+ out.println("int DllCharacteristics=" + DllCharacteristics + " // 5e");
+ out.println("long SizeOfStackReserve=" + SizeOfStackReserve + " // 60");
+ out.println("long SizeOfStackCommit=" + SizeOfStackCommit + " // 64");
+ out.println("long SizeOfHeapReserve=" + SizeOfHeapReserve + " // 68");
+ out.println("long SizeOfHeapCommit=" + SizeOfHeapCommit + " // 6c");
+ out.println("long LoaderFlags=" + LoaderFlags + " // 70");
+ out.println("long NumberOfRvaAndSizes=" + NumberOfRvaAndSizes + " // 74");
+
+ out.println("long ExportDirectory_VA=" + ExportDirectory_VA + " // 78");
+ out.println("long ExportDirectory_Size=" + ExportDirectory_Size + " // 7c");
+ out.println("long ImportDirectory_VA=" + ImportDirectory_VA + " // 80");
+ out.println("long ImportDirectory_Size=" + ImportDirectory_Size + " // 84");
+ out.println("long ResourceDirectory_VA=" + ResourceDirectory_VA + " // 88");
+ out.println("long ResourceDirectory_Size=" + ResourceDirectory_Size + " // 8c");
+ out.println("long ExceptionDirectory_VA=" + ExceptionDirectory_VA + " // 90");
+ out.println("long ExceptionDirectory_Size=" + ExceptionDirectory_Size + " // 94");
+ out.println("long SecurityDirectory_VA=" + SecurityDirectory_VA + " // 98");
+ out.println("long SecurityDirectory_Size=" + SecurityDirectory_Size + " // 9c");
+ out.println("long BaseRelocationTable_VA=" + BaseRelocationTable_VA + " // a0");
+ out.println("long BaseRelocationTable_Size=" + BaseRelocationTable_Size + " // a4");
+ out.println("long DebugDirectory_VA=" + DebugDirectory_VA + " // a8");
+ out.println("long DebugDirectory_Size=" + DebugDirectory_Size + " // ac");
+ out.println("long ArchitectureSpecificData_VA=" + ArchitectureSpecificData_VA + " // b0");
+ out.println("long ArchitectureSpecificData_Size=" + ArchitectureSpecificData_Size + " // b4");
+ out.println("long RVAofGP_VA=" + RVAofGP_VA + " // b8");
+ out.println("long RVAofGP_Size=" + RVAofGP_Size + " // bc");
+ out.println("long TLSDirectory_VA=" + TLSDirectory_VA + " // c0");
+ out.println("long TLSDirectory_Size=" + TLSDirectory_Size + " // c4");
+ out.println("long LoadConfigurationDirectory_VA=" + LoadConfigurationDirectory_VA + " // c8");
+ out.println("long LoadConfigurationDirectory_Size=" + LoadConfigurationDirectory_Size + " // cc");
+ out.println("long BoundImportDirectoryinheaders_VA=" + BoundImportDirectoryinheaders_VA + " // d0");
+ out.println("long BoundImportDirectoryinheaders_Size=" + BoundImportDirectoryinheaders_Size + " // d4");
+ out.println("long ImportAddressTable_VA=" + ImportAddressTable_VA + " // d8");
+ out.println("long ImportAddressTable_Size=" + ImportAddressTable_Size + " // dc");
+ out.println("long DelayLoadImportDescriptors_VA=" + DelayLoadImportDescriptors_VA + " // e0");
+ out.println("long DelayLoadImportDescriptors_Size=" + DelayLoadImportDescriptors_Size + " // e4");
+ out.println("long COMRuntimedescriptor_VA=" + COMRuntimedescriptor_VA + " // e8");
+ out.println("long COMRuntimedescriptor_Size=" + COMRuntimedescriptor_Size + " // ec");
+ }
+
+ public ByteBuffer get() {
+ ByteBuffer head = ByteBuffer.allocate(16 + this.SizeOfOptionalHeader);
+ head.order(ByteOrder.LITTLE_ENDIAN);
+ head.position(0);
+
+ head.putInt(PeMagic);
+
+ head.putShort((short)Machine); // 4
+ head.putShort((short)NumberOfSections); // 6
+ head.putInt((int)TimeDateStamp); // 8
+ head.putInt((int)PointerToSymbolTable); // C
+ head.putInt((int)NumberOfSymbols); // 10
+ head.putShort((short)SizeOfOptionalHeader); // 14
+ head.putShort((short)Characteristics); // 16
+ // Optional Header
+
+ head.putShort((short)Magic); // 18
+ head.put((byte)MajorLinkerVersion); // 1a
+ head.put((byte)MinorLinkerVersion); // 1b
+ head.putInt((int)SizeOfCode); // 1c
+ head.putInt((int)SizeOfInitializedData); // 20
+ head.putInt((int)SizeOfUninitializedData); // 24
+ head.putInt((int)AddressOfEntryPoint); // 28
+ head.putInt((int)BaseOfCode); // 2c
+ if (isPe32Plus()) {
+ head.putLong(ImageBase); // 34
+ } else {
+ head.putInt((int)BaseOfData); // // NT additional fields. // 30
+ head.putInt((int)ImageBase); // 34
+ }
+ head.putInt((int)SectionAlignment); // 38
+ head.putInt((int)FileAlignment); // 3c
+ head.putShort((short)MajorOperatingSystemVersion); // 40
+ head.putShort((short)MinorOperatingSystemVersion); // 42
+ head.putShort((short)MajorImageVersion); // 44
+ head.putShort((short)MinorImageVersion); // 46
+ head.putShort((short)MajorSubsystemVersion); // 48
+ head.putShort((short)MinorSubsystemVersion); // 4a
+ head.putInt((int)Reserved1); // 4c
+ head.putInt((int)SizeOfImage); // 50
+ head.putInt((int)SizeOfHeaders); // 54
+ head.putInt((int)CheckSum); // 58
+ head.putShort((short)Subsystem); // 5c
+ head.putShort((short)DllCharacteristics); // 5e
+ if (isPe32Plus()) {
+ head.putLong(SizeOfStackReserve); // 60
+ head.putLong(SizeOfStackCommit); // 64
+ head.putLong(SizeOfHeapReserve); // 68
+ head.putLong(SizeOfHeapCommit); // 6c
+ } else {
+ head.putInt((int)SizeOfStackReserve); // 60
+ head.putInt((int)SizeOfStackCommit); // 64
+ head.putInt((int)SizeOfHeapReserve); // 68
+ head.putInt((int)SizeOfHeapCommit); // 6c
+ }
+ head.putInt((int)LoaderFlags); // 70
+ head.putInt((int)NumberOfRvaAndSizes); // 74
+
+ head.putInt((int)ExportDirectory_VA); // 78
+ head.putInt((int)ExportDirectory_Size); // 7c
+ head.putInt((int)ImportDirectory_VA); // 80
+ head.putInt((int)ImportDirectory_Size); // 84
+ head.putInt((int)ResourceDirectory_VA); // 88
+ head.putInt((int)ResourceDirectory_Size); // 8c
+ head.putInt((int)ExceptionDirectory_VA); // 90
+ head.putInt((int)ExceptionDirectory_Size); // 94
+ head.putInt((int)SecurityDirectory_VA); // 98
+ head.putInt((int)SecurityDirectory_Size); // 9c
+ head.putInt((int)BaseRelocationTable_VA); // a0
+ head.putInt((int)BaseRelocationTable_Size); // a4
+ head.putInt((int)DebugDirectory_VA); // a8
+ head.putInt((int)DebugDirectory_Size); // ac
+ head.putInt((int)ArchitectureSpecificData_VA); // b0
+ head.putInt((int)ArchitectureSpecificData_Size); // b4
+ head.putInt((int)RVAofGP_VA); // b8
+ head.putInt((int)RVAofGP_Size); // bc
+ head.putInt((int)TLSDirectory_VA); // c0
+ head.putInt((int)TLSDirectory_Size); // c4
+ head.putInt((int)LoadConfigurationDirectory_VA); // c8
+ head.putInt((int)LoadConfigurationDirectory_Size); // cc
+ head.putInt((int)BoundImportDirectoryinheaders_VA); // d0
+ head.putInt((int)BoundImportDirectoryinheaders_Size); // d4
+ head.putInt((int)ImportAddressTable_VA); // d8
+ head.putInt((int)ImportAddressTable_Size); // dc
+ head.putInt((int)DelayLoadImportDescriptors_VA); // e0
+ head.putInt((int)DelayLoadImportDescriptors_Size); // e4
+ head.putInt((int)COMRuntimedescriptor_VA); // e8
+ head.putInt((int)COMRuntimedescriptor_Size); // ec
+
+ head.position(0);
+ return head;
+ }
+
+ public void updateVAAndSize(Vector oldsections, Vector newsections) {
+ long codebase = findNewVA(this.BaseOfCode, oldsections, newsections);
+ long codesize = findNewSize(this.BaseOfCode, oldsections, newsections);
+ // System.out.println("New BaseOfCode=" + codebase + " (size=" + codesize + ")");
+ this.BaseOfCode = codebase;
+ this.SizeOfCode = codesize;
+
+ this.AddressOfEntryPoint = findNewVA(this.AddressOfEntryPoint, oldsections, newsections);
+
+ long database = findNewVA(this.BaseOfData, oldsections, newsections);
+ long datasize = findNewSize(this.BaseOfData, oldsections, newsections);
+ // System.out.println("New BaseOfData=" + database + " (size=" + datasize + ")");
+ this.BaseOfData = database;
+
+ long imagesize = 0;
+ for (int i = 0; i < newsections.size(); i++) {
+ PESection sect = (PESection)newsections.get(i);
+ long curmax = sect.VirtualAddress + sect.VirtualSize;
+ if (curmax > imagesize)
+ imagesize = curmax;
+ }
+ this.SizeOfImage = imagesize;
+
+ // this.SizeOfInitializedData = datasize;
+
+ ExportDirectory_Size = findNewSize(ExportDirectory_VA, oldsections, newsections);
+ ExportDirectory_VA = findNewVA(ExportDirectory_VA, oldsections, newsections);
+ ImportDirectory_Size = findNewSize(ImportDirectory_VA, oldsections, newsections);
+ ImportDirectory_VA = findNewVA(ImportDirectory_VA, oldsections, newsections);
+ ResourceDirectory_Size = findNewSize(ResourceDirectory_VA, oldsections, newsections);
+ ResourceDirectory_VA = findNewVA(ResourceDirectory_VA, oldsections, newsections);
+ ExceptionDirectory_Size = findNewSize(ExceptionDirectory_VA, oldsections, newsections);
+ ExceptionDirectory_VA = findNewVA(ExceptionDirectory_VA, oldsections, newsections);
+ SecurityDirectory_Size = findNewSize(SecurityDirectory_VA, oldsections, newsections);
+ SecurityDirectory_VA = findNewVA(SecurityDirectory_VA, oldsections, newsections);
+ BaseRelocationTable_Size = findNewSize(BaseRelocationTable_VA, oldsections, newsections);
+ BaseRelocationTable_VA = findNewVA(BaseRelocationTable_VA, oldsections, newsections);
+ DebugDirectory_Size = findNewSize(DebugDirectory_VA, oldsections, newsections);
+ DebugDirectory_VA = findNewVA(DebugDirectory_VA, oldsections, newsections);
+ ArchitectureSpecificData_Size = findNewSize(ArchitectureSpecificData_VA, oldsections, newsections);
+ ArchitectureSpecificData_VA = findNewVA(ArchitectureSpecificData_VA, oldsections, newsections);
+ RVAofGP_Size = findNewSize(RVAofGP_VA, oldsections, newsections);
+ RVAofGP_VA = findNewVA(RVAofGP_VA, oldsections, newsections);
+ TLSDirectory_Size = findNewSize(TLSDirectory_VA, oldsections, newsections);
+ TLSDirectory_VA = findNewVA(TLSDirectory_VA, oldsections, newsections);
+ LoadConfigurationDirectory_Size = findNewSize(LoadConfigurationDirectory_VA, oldsections, newsections);
+ LoadConfigurationDirectory_VA = findNewVA(LoadConfigurationDirectory_VA, oldsections, newsections);
+ BoundImportDirectoryinheaders_Size = findNewSize(BoundImportDirectoryinheaders_VA, oldsections, newsections);
+ BoundImportDirectoryinheaders_VA = findNewVA(BoundImportDirectoryinheaders_VA, oldsections, newsections);
+ ImportAddressTable_Size = findNewSize(ImportAddressTable_VA, oldsections, newsections);
+ ImportAddressTable_VA = findNewVA(ImportAddressTable_VA, oldsections, newsections);
+ DelayLoadImportDescriptors_Size = findNewSize(DelayLoadImportDescriptors_VA, oldsections, newsections);
+ DelayLoadImportDescriptors_VA = findNewVA(DelayLoadImportDescriptors_VA, oldsections, newsections);
+ COMRuntimedescriptor_Size = findNewSize(COMRuntimedescriptor_VA, oldsections, newsections);
+ COMRuntimedescriptor_VA = findNewVA(COMRuntimedescriptor_VA, oldsections, newsections);
+ }
+
+ public boolean isPe32Plus() {
+ return Magic == 523;
+ }
+
+ private long findNewVA(long current, Vector oldsections, Vector newsections) {
+ for (int i = 0; i < oldsections.size(); i++) {
+ PESection sect = (PESection)oldsections.get(i);
+ if (sect.VirtualAddress == current) {
+ PESection newsect = (PESection)newsections.get(i);
+
+ // System.out.println("Translation VA found for " + current + " = " + i + " (" +newsect.VirtualAddress + ")=" + newsect.getName());
+ return newsect.VirtualAddress;
+ } else if ((current > sect.VirtualAddress) && (current < (sect.VirtualAddress + sect.VirtualSize))) {
+ long diff = current - sect.VirtualAddress;
+ PESection newsect = (PESection)newsections.get(i);
+ // System.out.println("Translation VA found INSIDE " + current + " = " + i + " (" +newsect.VirtualAddress + ")=" + newsect.getName());
+ return newsect.VirtualAddress + diff;
+ }
+ }
+
+
+ return 0;
+ }
+
+ private long findNewSize(long current, Vector oldsections, Vector newsections) {
+ for (int i = 0; i < oldsections.size(); i++) {
+ PESection sect = (PESection)oldsections.get(i);
+ if (sect.VirtualAddress == current) {
+ PESection newsect = (PESection)newsections.get(i);
+ // System.out.println("Translation Size found for " + current + " = " + i + " (" +newsect.VirtualAddress + ")=" + newsect.getName());
+ // System.out.println(" Old size " + sect.VirtualSize + " vs new size " + newsect.VirtualSize);
+ return newsect.VirtualSize;
+ }
+ }
+ return 0;
+ }
+
+}
diff --git a/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/pe/PEResourceDirectory.java b/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/pe/PEResourceDirectory.java
new file mode 100644
index 0000000..09676ad
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/pe/PEResourceDirectory.java
@@ -0,0 +1,625 @@
+/*
+ JSmooth: a VM wrapper toolkit for Windows
+ Copyright (C) 2003 Rodrigo Reyes <reyes at charabia.net>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+ */
+
+/*
+ * PEResourceDirectory.java
+ *
+ * Created on 2 aout 2003, 01:28
+ */
+
+package net.charabia.jsmoothgen.pe;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.channels.FileChannel;
+import java.util.Iterator;
+import java.util.Vector;
+
+/**
+ * @author Rodrigo
+ */
+public class PEResourceDirectory {
+
+ /*
+ typedef struct IMAGE_RESOURCE_DIRECTORY {
+ uint32_t Characteristics;
+ uint32_t TimeDateStamp;
+ uint16_t MajorVersion;
+ uint16_t MinorVersion;
+ uint16_t NumberOfNamedEntries;
+ uint16_t NumberOfIdEntries;
+ }
+ */
+
+ public class DataEntry {
+ long OffsetToData; // To update at each change
+ long Size;
+ long CodePage; // never changed
+ long Reserved; // never changed
+ ByteBuffer Data;
+
+ public DataEntry(ByteBuffer data) {
+ this.Data = data;
+ this.Size = data.capacity();
+ }
+
+ public DataEntry(FileChannel chan, long offset) throws IOException {
+ long orgpos = chan.position();
+ chan.position(PEResourceDirectory.this.m_offsetBase + offset);
+ ByteBuffer buf = ByteBuffer.allocate(16);
+ buf.order(ByteOrder.LITTLE_ENDIAN);
+ chan.read(buf);
+ buf.position(0);
+
+ OffsetToData = buf.getInt();
+ Size = buf.getInt();
+ CodePage = buf.getInt();
+ Reserved = buf.getInt();
+
+ long datapos = PEResourceDirectory.this.m_master.PointerToRawData + (OffsetToData - PEResourceDirectory.this.m_master.VirtualAddress);
+ Data = ByteBuffer.allocate((int)Size);
+ Data.order(ByteOrder.LITTLE_ENDIAN);
+ chan.position(datapos);
+ chan.read(Data);
+ Data.position(0);
+
+ chan.position(orgpos);
+ }
+
+ public int diskSize() {
+ int size = 16 + (int)this.Size;
+ if ((size % 4) > 0)
+ size += 4 - (size % 4);
+ return size;
+ }
+
+ public void dump(PrintStream out, int level) {
+ indent(level, out);
+ out.println("OffsetToData=" + OffsetToData);
+ indent(level, out);
+ out.println("Size=" + Size);
+ indent(level, out);
+ out.println("CodePage=" + CodePage);
+ indent(level, out);
+ out.println("Reserved=" + Reserved);
+ indent(level, out);
+ out.print("Data={ ");
+ for (int i = 0; i < this.Data.capacity(); i++) {
+ out.print("" + Integer.toHexString((byte)Data.get()) + ",");
+ }
+ out.println(" }");
+
+ }
+
+ private void indent(int level, PrintStream out) {
+ for (int i = 0; i < level; i++)
+ out.print(" ");
+ }
+
+ public int buildBuffer(ByteBuffer buffer, long virtualBaseOffset, int dataOffset) {
+ // System.out.println("Building Data Entry buffer @ " + buffer.position() + " (" + dataOffset + ")");
+
+ dataOffset = buffer.position() + 16;
+
+ buffer.putInt((int)(dataOffset + virtualBaseOffset));
+ buffer.putInt((int)Size);
+ buffer.putInt((int)CodePage);
+ buffer.putInt((int)Reserved);
+
+ Data.position(0);
+ buffer.put(Data);
+
+ dataOffset += Size;
+ if ((dataOffset % 4) > 0)
+ dataOffset += (4 - (dataOffset % 4));
+
+ return dataOffset;
+ }
+
+ public void setData(ByteBuffer data) {
+ Data = data;
+ Size = data.capacity();
+ }
+ }
+
+ public class ResourceEntry {
+ int Id;
+ String Name;
+
+ ImageResourceDirectory Directory;
+ DataEntry Data;
+
+ public ResourceEntry(int id, DataEntry data) {
+ this.Id = id;
+ this.Data = data;
+ }
+
+ public ResourceEntry(String name, DataEntry data) {
+ this.Name = name;
+ this.Data = data;
+ }
+
+ public ResourceEntry(int id, ImageResourceDirectory dir) {
+ this.Id = id;
+ this.Directory = dir;
+ }
+
+ public ResourceEntry(String name, ImageResourceDirectory dir) {
+ this.Name = name;
+ this.Directory = dir;
+ }
+
+ public ResourceEntry(FileChannel chan) throws IOException {
+ // System.out.println("Resource Entry Offset: " + chan.position());
+ ByteBuffer buf = ByteBuffer.allocate(8);
+ buf.order(ByteOrder.LITTLE_ENDIAN);
+ chan.read(buf);
+ buf.position(0);
+ long orgchanpos = chan.position();
+ int val = buf.getInt();
+ long offsetToData = buf.getInt();
+ // System.out.println("Entry: Val=" + val);
+ // System.out.println(" Off=" + offsetToData);
+
+ if (val < 0) {
+ val &= 0x7FFFFFFF;
+ Name = extractStringAt(chan, val);
+ Id = -1;
+ // System.out.println(" String at " + val + " = " + Name);
+ } else {
+ Id = val;
+ }
+
+ if (offsetToData < 0) {
+ offsetToData &= 0x7FFFFFFF;
+ long orgpos = chan.position();
+ chan.position(PEResourceDirectory.this.m_offsetBase + offsetToData);
+ Directory = new PEResourceDirectory.ImageResourceDirectory(chan);
+ chan.position(orgpos);
+ } else {
+ Data = new DataEntry(chan, offsetToData);
+ }
+ }
+
+ public String extractStringAt(FileChannel chan, int offset) throws IOException {
+ long orgchanpos = chan.position();
+ chan.position(PEResourceDirectory.this.m_offsetBase + offset);
+
+ ByteBuffer sizebuf = ByteBuffer.allocate(2);
+ sizebuf.order(ByteOrder.LITTLE_ENDIAN);
+ chan.read(sizebuf);
+ sizebuf.position(0);
+
+ int size = sizebuf.getShort();
+ ByteBuffer buffer = ByteBuffer.allocate(size * 2);
+ buffer.order(ByteOrder.LITTLE_ENDIAN);
+ chan.read(buffer);
+ buffer.position(0);
+
+ StringBuffer buf = new StringBuffer(size);
+ for (int i = 0; i < size; i++) {
+ int c = buffer.getShort();
+ buf.append((char)c);
+ }
+
+ chan.position(orgchanpos);
+ return buf.toString();
+ }
+
+ public int diskSize() {
+ int size = 8;
+ if (Name != null)
+ size += (Name.length() * 2) + 2;
+
+ if (Directory != null)
+ size += Directory.diskSize();
+ else if (Data != null)
+ size += Data.diskSize();
+
+ if ((size % 4) > 0)
+ size += 4 - (size % 4);
+ return size;
+ }
+
+ public void dump(PrintStream out, int level) {
+ indent(level, out);
+ if (this.Name != null)
+ out.println("Name=" + Name);
+ else
+ out.println("Id=#" + Id);
+
+ indent(level, out);
+ if (this.Directory != null) {
+ out.println("ENTRY: DIRECTORY POINTER");
+ this.Directory.dump(out, level + 1);
+ } else {
+ out.println("ENTRY: DATA ENTRY");
+ Data.dump(out, level + 1);
+ }
+ }
+
+ private void indent(int level, PrintStream out) {
+ for (int i = 0; i < level; i++)
+ out.print(" ");
+ }
+
+ public int buildBuffer(ByteBuffer buffer, long virtualBaseOffset, int dataOffset) {
+ // System.out.println("Building Resource Entry buffer " + Name + "/" + Id + " @ " + buffer.position() + " (" + dataOffset + ")");
+ if (Name != null) {
+ buffer.putInt((int)(dataOffset | 0x80000000));
+
+ int stringoffset = dataOffset;
+ ByteBuffer strbuf = ByteBuffer.allocate(Name.length() * 2 + 2);
+ strbuf.order(ByteOrder.LITTLE_ENDIAN);
+
+ strbuf.putShort((short)Name.length());
+ for (int i = 0; i < Name.length(); i++) {
+ strbuf.putShort((short)Name.charAt(i));
+ }
+ strbuf.position(0);
+
+ long oldpos = buffer.position();
+ buffer.position(dataOffset);
+ buffer.put(strbuf);
+ dataOffset += Name.length() * 2 + 2;
+ if ((dataOffset % 4) != 0)
+ dataOffset += 4 - (dataOffset % 4);
+ buffer.position((int) oldpos);
+ } else {
+ buffer.putInt(Id);
+ }
+
+ if (Directory != null) {
+ buffer.putInt((int)(dataOffset | 0x80000000));
+
+ int oldpos = buffer.position();
+ buffer.position(dataOffset);
+ int dirsize = Directory.buildBuffer(buffer, virtualBaseOffset);
+ dataOffset = dirsize;
+ buffer.position(oldpos);
+
+ } else if (Data != null) {
+ buffer.putInt(dataOffset);
+ int oldpos = buffer.position();
+ buffer.position(dataOffset);
+ dataOffset = Data.buildBuffer(buffer, virtualBaseOffset, dataOffset);
+ buffer.position(oldpos);
+ } else {
+ throw new RuntimeException("Directory and Data are both null!");
+ }
+
+ return dataOffset;
+ }
+ }
+
+ public class ImageResourceDirectory {
+ long Characteristics; // uint32_t
+ long TimeDateStamp; // uint32_t
+ int MajorVersion; // uint16_t
+ int MinorVersion; // uint16_t
+ int NumberOfNamedEntries; // uint16_t
+ int NumberOfIdEntries; // uint16_t
+ Vector NamedEntries = new Vector();
+ Vector IdEntries = new Vector();
+
+ public ImageResourceDirectory() {
+ }
+
+ public ImageResourceDirectory(FileChannel chan) throws IOException {
+ ByteBuffer header = ByteBuffer.allocate(16);
+ header.order(ByteOrder.LITTLE_ENDIAN);
+ chan.read(header);
+ header.position(0);
+
+ Characteristics = header.getInt();
+ TimeDateStamp = header.getInt();
+ MajorVersion = header.getShort();
+ MinorVersion = header.getShort();
+ NumberOfNamedEntries = header.getShort();
+ NumberOfIdEntries = header.getShort();
+
+ for (int i = 0; i < NumberOfNamedEntries; i++) {
+ ResourceEntry re = new ResourceEntry(chan);
+ NamedEntries.add(re);
+ }
+ for (int i = 0; i < NumberOfIdEntries; i++) {
+ ResourceEntry re = new ResourceEntry(chan);
+ IdEntries.add(re);
+ }
+ }
+
+ public void addNamedEntry(ResourceEntry entry) {
+ this.NamedEntries.add(entry);
+ }
+
+ public void addIdEntry(ResourceEntry entry) {
+ this.IdEntries.add(entry);
+ }
+
+ public void addEntry(ResourceEntry entry) {
+ if (entry.Name != null)
+ addNamedEntry(entry);
+ else
+ addIdEntry(entry);
+ }
+
+ public void dump(PrintStream out, int level) {
+ indent(level, out);
+ out.println("Directory: ");
+ indent(level, out);
+ out.println("Characteristics=" + this.Characteristics);
+ indent(level, out);
+ out.println("TimeDateStamp=" + this.TimeDateStamp);
+ indent(level, out);
+ out.println("MajorVersion=" + this.MajorVersion);
+ indent(level, out);
+ out.println("MinorVersion=" + this.MinorVersion);
+ indent(level, out);
+ out.println("NumberOfNamedEntries=" + this.NumberOfNamedEntries);
+ indent(level, out);
+ out.println("NumberOfIdEntries=" + this.NumberOfIdEntries);
+ indent(level, out);
+ out.println("Named Entries:");
+ for (int i = 0; i < NumberOfNamedEntries; i++) {
+ ResourceEntry re = (ResourceEntry)NamedEntries.get(i);
+ re.dump(out, level + 1);
+ }
+ indent(level, out);
+ out.println("Id Entries:");
+ for (int i = 0; i < NumberOfIdEntries; i++) {
+ ResourceEntry re = (ResourceEntry)IdEntries.get(i);
+ re.dump(out, level + 1);
+ }
+ }
+
+ private void indent(int level, PrintStream out) {
+ for (int i = 0; i < level; i++)
+ out.print(" ");
+ }
+
+ public int diskSize() {
+ int size = 16;
+ for (int i = 0; i < this.NamedEntries.size(); i++) {
+ ResourceEntry re = (ResourceEntry)NamedEntries.get(i);
+ size += re.diskSize();
+ }
+ for (int i = 0; i < this.IdEntries.size(); i++) {
+ ResourceEntry re = (ResourceEntry)IdEntries.get(i);
+ size += re.diskSize();
+ }
+
+ if ((size % 4) > 0)
+ size += 4 - (size % 4);
+
+ return size;
+ }
+
+ public int buildBuffer(ByteBuffer buffer, long virtualBaseOffset) {
+ // System.out.println("Building Directory Entry buffer @ " + buffer.position());
+
+ buffer.putInt((int)this.Characteristics);
+ buffer.putInt((int)this.TimeDateStamp);
+ buffer.putShort((short)this.MajorVersion);
+ buffer.putShort((short)this.MinorVersion);
+ buffer.putShort((short)this.NamedEntries.size());
+ buffer.putShort((short)this.IdEntries.size());
+
+ int dataOffset = buffer.position() + (NamedEntries.size() * 8) + (IdEntries.size() * 8);
+
+ for (int i = 0; i < this.NamedEntries.size(); i++) {
+ ResourceEntry re = (ResourceEntry)this.NamedEntries.get(i);
+ dataOffset = re.buildBuffer(buffer, virtualBaseOffset, dataOffset);
+ }
+
+ for (int i = 0; i < this.IdEntries.size(); i++) {
+ ResourceEntry re = (ResourceEntry)this.IdEntries.get(i);
+ dataOffset = re.buildBuffer(buffer, virtualBaseOffset, dataOffset);
+ }
+
+ buffer.position(dataOffset);
+ return dataOffset;
+ }
+
+ public ResourceEntry getResourceEntry(String name) {
+ // If name == null, get the first entry in lexical
+ // order. If no entry in lexical order, choose the
+ // lowest integer id entry.
+ if (name == null) {
+ if (NamedEntries.size() > 0) {
+ return (PEResourceDirectory.ResourceEntry)NamedEntries.get(0);
+ }
+ if (IdEntries.size() > 0) {
+ return (PEResourceDirectory.ResourceEntry)IdEntries.get(0);
+ }
+ return null;
+ }
+
+ if ((name.length() > 0) && (name.charAt(0) == '#')) {
+ try {
+ String nb = name.substring(1);
+ int i = Integer.parseInt(nb);
+ return getResourceEntry(i);
+ } catch (Exception exc) {
+ exc.printStackTrace();
+ }
+ }
+
+ for (Iterator i = this.NamedEntries.iterator(); i.hasNext();) {
+ ResourceEntry re = (ResourceEntry)i.next();
+ if (name.equals(re.Name)) {
+ return re;
+ }
+ }
+ return null;
+ }
+
+ public ResourceEntry getResourceEntry(int id) {
+ for (Iterator i = this.IdEntries.iterator(); i.hasNext();) {
+ ResourceEntry re = (ResourceEntry)i.next();
+ if (id == re.Id) {
+ return re;
+ }
+ }
+ return null;
+ }
+
+ }
+
+ PESection m_master;
+ PEFile m_file;
+ long m_offsetBase;
+
+ PEResourceDirectory.ImageResourceDirectory m_root;
+
+ /**
+ * Creates a new instance of PEResourceDirectory
+ */
+ public PEResourceDirectory(PEFile file, PESection sect) throws IOException {
+ m_master = sect;
+ m_file = file;
+ m_offsetBase = sect.PointerToRawData;
+ init();
+
+ // System.out.println("--------\nTOTAL SIZE: " + m_root.diskSize());
+
+ // System.out.println("\n\n");
+ }
+
+ public void init() throws IOException {
+ // / System.out.println("RESOURCE INIT");
+ // System.out.println(" Offset: " + m_master.PointerToRawData);
+ FileChannel chan = m_file.getChannel();
+ chan.position(m_master.PointerToRawData);
+ PEResourceDirectory.ImageResourceDirectory dir = new PEResourceDirectory.ImageResourceDirectory(chan);
+ // System.out.println("-----------------\nDUMP\n---------------");
+ m_root = dir;
+
+ // dir.dump(System.out, 0);
+ }
+
+ public void dump(PrintStream out) {
+ m_root.dump(out, 0);
+ }
+
+ public int size() {
+ return m_root.diskSize();
+ }
+
+ public ByteBuffer buildResource(long virtualBaseOffset) {
+ // System.out.println("BUILDING RESOURCE / VIRTUAL: " + virtualBaseOffset);
+ int resourceSize = m_root.diskSize();
+ ByteBuffer resbuf = ByteBuffer.allocate(resourceSize);
+ resbuf.order(ByteOrder.LITTLE_ENDIAN);
+ resbuf.position(0);
+ m_root.buildBuffer(resbuf, virtualBaseOffset);
+ return resbuf;
+ }
+
+ public PEResourceDirectory.ImageResourceDirectory getRoot() {
+ return m_root;
+ }
+
+ public boolean replaceManifest(int resourceId, int langId, ByteBuffer data) {
+ ResourceEntry catEntry = m_root.getResourceEntry(24);
+ if ((catEntry != null) && (catEntry.Directory != null)) {
+ ResourceEntry identEntry = catEntry.Directory.getResourceEntry(resourceId);
+ if ((identEntry != null) && (identEntry.Directory != null)) {
+ ResourceEntry langEntry = identEntry.Directory.getResourceEntry(langId);
+ if ((langEntry != null) && (langEntry.Data != null)) {
+ DataEntry dataslot = langEntry.Data;
+ dataslot.setData(data);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public boolean replaceResource(String catId, int resourceId, int langId, ByteBuffer data) {
+ ResourceEntry catEntry = m_root.getResourceEntry(catId);
+ if ((catEntry != null) && (catEntry.Directory != null)) {
+ ResourceEntry identEntry = catEntry.Directory.getResourceEntry(resourceId);
+ if ((identEntry != null) && (identEntry.Directory != null)) {
+ ResourceEntry langEntry = identEntry.Directory.getResourceEntry(langId);
+ if ((langEntry != null) && (langEntry.Data != null)) {
+ DataEntry dataslot = langEntry.Data;
+ dataslot.setData(data);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public void addNewResource(String catId, String resourceId, String languageId, ByteBuffer data) {
+ DataEntry dataEntry = new DataEntry(data);
+ ResourceEntry languageEntry = buildResourceEntry(languageId, dataEntry);
+ ImageResourceDirectory languageDir = new ImageResourceDirectory();
+
+ languageDir.TimeDateStamp = 0x3F2CCF64;
+ languageDir.addEntry(languageEntry);
+
+ ResourceEntry identEntry = buildResourceEntry(resourceId, languageDir);
+
+ ImageResourceDirectory identDir = new ImageResourceDirectory();
+ identDir.TimeDateStamp = 0x3F2CCF64;
+ identDir.addEntry(identEntry);
+
+ ResourceEntry catEntry = buildResourceEntry(catId, identDir);
+ m_root.addEntry(catEntry);
+ }
+
+ public DataEntry getData(String catId, String resourceId, String langId) {
+ ResourceEntry catEntry = m_root.getResourceEntry(catId);
+ if ((catEntry != null) && (catEntry.Directory != null)) {
+ ResourceEntry identEntry = catEntry.Directory.getResourceEntry(resourceId);
+ if ((identEntry != null) && (identEntry.Directory != null)) {
+ ResourceEntry langEntry = identEntry.Directory.getResourceEntry(langId);
+ if ((langEntry != null) && (langEntry.Data != null)) {
+ DataEntry dataslot = langEntry.Data;
+ return dataslot;
+ }
+ }
+ }
+ return null;
+ }
+
+ public ResourceEntry buildResourceEntry(String id, DataEntry data) {
+ if ((id.length() > 1) && (id.charAt(0) == '#')) {
+ int intid = Integer.parseInt(id.substring(1));
+ return new ResourceEntry(intid, data);
+ }
+
+ return new ResourceEntry(id, data);
+ }
+
+
+ public ResourceEntry buildResourceEntry(String id, ImageResourceDirectory dir) {
+ if ((id.length() > 1) && (id.charAt(0) == '#')) {
+ int intid = Integer.parseInt(id.substring(1));
+ return new ResourceEntry(intid, dir);
+ }
+
+ return new ResourceEntry(id, dir);
+ }
+
+}
diff --git a/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/pe/PESection.java b/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/pe/PESection.java
new file mode 100644
index 0000000..bbe857b
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/pe/PESection.java
@@ -0,0 +1,133 @@
+/*
+ JSmooth: a VM wrapper toolkit for Windows
+ Copyright (C) 2003 Rodrigo Reyes <reyes at charabia.net>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+/*
+ * PESection.java
+ *
+ * Created on 29 juillet 2003, 21:34
+ */
+
+package net.charabia.jsmoothgen.pe;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.channels.FileChannel;
+
+/**
+ * @author Rodrigo
+ */
+public class PESection implements Cloneable {
+ byte[] ANSI_Name; // Name of the Section. Can be anything (0)(8BYTES)
+ long VirtualSize; // The size of the section when it is mapped to memory. Must be a multiple of 4096. (8)(DWORD)
+ long VirtualAddress; // An rva to where it should be mapped in memory. (12)(DWORD)
+ long SizeOfRawData; // The size of the section in the PE file. Must be a multiple of 512 (16)(DWORD)
+ long PointerToRawData; // A file based offset which points to the location of this sections data (20)(DWORD)
+ long PointerToRelocations; // In EXE's this field is meaningless, and is set 0 (24)(DWORD)
+ long PointerToLinenumbers; // This is the file-based offset of the line number table. This field is only used for debug purposes, and is usualy set to 0 (28)(DWORD)
+ int NumberOfRelocations; // In EXE's this field is meaningless, and is set 0 (32)(WORD)
+ int NumberOfLinenumbers; // The number of line numbers in the line number table for this section. This field is only used for debug purposes, and is usualy set to 0 (34)(WORD)
+ long Characteristics; // The kind of data stored in this section ie. Code, Data, Import data, Relocation data (36)(DWORD)
+
+ private long m_baseoffset;
+ private PEFile m_pe;
+
+ /**
+ * Creates a new instance of PESection
+ */
+ public PESection(PEFile pef, long baseoffset) {
+ m_pe = pef;
+ m_baseoffset = baseoffset;
+ }
+
+ public Object clone() throws CloneNotSupportedException {
+ return super.clone();
+ }
+
+ public String getName() {
+ StringBuffer buffer = new StringBuffer();
+ for (int i = 0; i < 8; i++)
+ buffer.append((char) ANSI_Name[i]);
+ return buffer.toString();
+ }
+
+ public void read() throws IOException {
+ FileChannel ch = m_pe.getChannel();
+ ByteBuffer head = ByteBuffer.allocate(40);
+ head.order(ByteOrder.LITTLE_ENDIAN);
+ ch.position(m_baseoffset);
+ ch.read(head);
+ head.position(0);
+
+ ANSI_Name = new byte[8];
+ for (int i = 0; i < 8; i++)
+ ANSI_Name[i] = head.get();
+
+ VirtualSize = head.getInt();
+ VirtualAddress = head.getInt();
+ SizeOfRawData = head.getInt();
+ PointerToRawData = head.getInt();
+ PointerToRelocations = head.getInt();
+ PointerToLinenumbers = head.getInt();
+ NumberOfRelocations = head.getShort();
+ NumberOfLinenumbers = head.getShort();
+ Characteristics = head.getInt();
+ }
+
+ public void dump(PrintStream out) {
+ out.println("SECTION:");
+ out.println(" Name= "+getName());
+ out.println(" VirtualSize= " + VirtualSize + " // The size of the section when it is mapped to memory. Must be a multiple of 4096. (8)(DWORD)");
+ out.println(" VirtualAddress= " + VirtualAddress + " // An rva to where it should be mapped in memory. (12)(DWORD)");
+ out.println(" SizeOfRawData= " + SizeOfRawData + " // The size of the section in the PE file. Must be a multiple of 512 (16)(DWORD)");
+ out.println(" PointerToRawData= " + PointerToRawData + " // A file based offset which points to the location of this sections data (20)(DWORD)");
+ out.println(" PointerToRelocations= " + PointerToRelocations + " // In EXE's this field is meaningless, and is set 0 (24)(DWORD)");
+ out.println(" PointerToLinenumbers= " + PointerToLinenumbers + " // This is the file-based offset of the line number table. This field is only used for debug purposes, and is usualy set to 0 (28)(DWORD)");
+ out.println(" NumberOfRelocations= " + NumberOfRelocations + " // In EXE's this field is meaningless, and is set 0 (32)(WORD)");
+ out.println(" NumberOfLinenumbers= " + NumberOfLinenumbers + " // The number of line numbers in the line number table for this section. This field is only used for debug purposes, and is usualy set to 0 (34)(WORD)");
+ out.println(" Characteristics= " + Characteristics + " // The kind of data stored in this section ie. Code, Data, Import data, Relocation data (36)(DWORD)");
+
+ }
+
+
+ public ByteBuffer get() {
+ ByteBuffer head = ByteBuffer.allocate(40);
+ head.order(ByteOrder.LITTLE_ENDIAN);
+ head.position(0);
+
+ for (int i = 0; i < 8; i++)
+ head.put((byte)ANSI_Name[i]);
+
+ head.putInt((int)VirtualSize);
+ head.putInt((int)VirtualAddress);
+ head.putInt((int)SizeOfRawData);
+ head.putInt((int)PointerToRawData);
+ head.putInt((int)PointerToRelocations);
+ head.putInt((int)PointerToLinenumbers);
+ head.putShort((short)NumberOfRelocations);
+ head.putShort((short)NumberOfLinenumbers);
+ head.putInt((int)Characteristics);
+
+ head.position(0);
+ return head;
+ }
+
+}
diff --git a/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/pe/res/ResIcon.java b/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/pe/res/ResIcon.java
new file mode 100644
index 0000000..b5d3dec
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/pe/res/ResIcon.java
@@ -0,0 +1,462 @@
+/*
+ JSmooth: a VM wrapper toolkit for Windows
+ Copyright (C) 2003 Rodrigo Reyes <reyes at charabia.net>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+ */
+
+/*
+ * ResIcon.java
+ *
+ * Created on 17 août 2003, 22:51
+ */
+
+package net.charabia.jsmoothgen.pe.res;
+
+import java.util.*;
+import java.io.*;
+import java.nio.*;
+import java.nio.channels.*;
+import java.awt.*;
+import java.awt.image.*;
+
+/**
+ * @see
+ */
+public class ResIcon
+{
+ public long Size; /* Size of this header in bytes DWORD*/
+ public long Width; /* Image width in pixels LONG*/
+ public long Height; /* Image height in pixels LONG*/
+ public int Planes; /* Number of color planes WORD*/
+ public int BitsPerPixel; /* Number of bits per pixel WORD*/
+ /* Fields added for Windows 3.x follow this line */
+ public long Compression; /* Compression methods used DWORD*/
+ public long SizeOfBitmap; /* Size of bitmap in bytes DWORD*/
+ public long HorzResolution; /* Horizontal resolution in pixels per meter LONG*/
+ public long VertResolution; /* Vertical resolution in pixels per meter LONG*/
+ public long ColorsUsed; /* Number of colors in the image DWORD*/
+ public long ColorsImportant; /* Minimum number of important colors DWORD*/
+
+ public PaletteElement[] Palette;
+ public short[] BitmapXOR;
+ public short[] BitmapAND;
+
+ public class PaletteElement
+ {
+ public int Blue;
+ public int Green;
+ public int Red;
+ public int Reserved;
+ public String toString()
+ {
+ return "{"+Blue+","+Green+","+Red+","+Reserved+"}";
+ }
+ }
+
+ public ResIcon(){};
+ /**
+ * Creates a new instance of ResIcon
+ * @see
+ * @param in
+ */
+ public ResIcon(ByteBuffer in)
+ {
+ Size = in.getInt();
+ Width = in.getInt();
+ Height = in.getInt();
+ Planes = in.getShort();
+ BitsPerPixel = in.getShort();
+ Compression = in.getInt();
+ SizeOfBitmap = in.getInt();
+ HorzResolution = in.getInt();
+ VertResolution = in.getInt();
+ ColorsUsed = in.getInt();
+ ColorsImportant = in.getInt();
+
+ int cols = (int)ColorsUsed;
+ if (cols == 0)
+ cols = 1 << BitsPerPixel;
+
+ Palette = new PaletteElement[(int)cols];
+ for (int i=0; i<Palette.length; i++)
+ {
+ PaletteElement el = new PaletteElement();
+ el.Blue = in.get();
+ el.Green = in.get();
+ el.Red = in.get();
+ el.Reserved = in.get();
+ Palette[i] = el;
+ }
+
+ // int xorbytes = (((int)Height/2) * (int)Width * (int)BitsPerPixel) / 8;
+ int xorbytes = (((int)Height/2) * (int)Width);
+ // System.out.println("POSITION " + in.position() + " : xorbitmap = " + xorbytes + " bytes");
+
+ BitmapXOR = new short[xorbytes];
+ for (int i=0; i<BitmapXOR.length; i++)
+ {
+ switch(BitsPerPixel)
+ {
+ case 4:
+ {
+ int pix = in.get();
+ BitmapXOR[i] = (short)((pix >> 4) & 0x0F);
+ i++;
+ BitmapXOR[i] = (short)(pix & 0x0F);
+ }
+ break;
+ case 8:
+ {
+ BitmapXOR[i] = in.get();
+ }
+ break;
+ }
+ }
+
+
+ int height = (int)(Height/2);
+ int rowsize = (int)Width / 8;
+ if ((rowsize%4)>0)
+ {
+ rowsize += 4 - (rowsize%4);
+ }
+
+ // System.out.println("POSITION " + in.position() + " : andbitmap = " + andbytes + " bytes");
+
+ int andbytes = height * rowsize; // (((int)Height/2) * (int)Width) / 8;
+
+ BitmapAND = new short[andbytes];
+ for (int i=0; i<BitmapAND.length; i++)
+ {
+ BitmapAND[i] = in.get();
+ }
+
+ }
+
+ /** Creates a new instance based on the data of the Image argument.
+ * @param img
+ */
+ public ResIcon(Image img) throws Exception
+ {
+ int width = img.getWidth(null);
+ int height = img.getHeight(null);
+
+ if ((width % 8) != 0)
+ width += (7-(width%8));
+
+ if ((height % 8) != 0)
+ height += (7-(height%8));
+
+ // System.out.println("FOUND WIDTH " + width + " (was " + img.getWidth(null) + ")");
+ // System.out.println("FOUND HEIGHT " + height + " (was " + img.getHeight(null) + ")");
+
+ // System.out.println("RESICON...");
+ if (img instanceof BufferedImage)
+ {
+ BufferedImage result = (BufferedImage)img;
+
+ for (int y=0; y<result.getHeight(); y++)
+ {
+ for (int x=0; x<result.getWidth(); x++)
+ {
+ int rgb = result.getRGB(x, y);
+ if (((rgb>>24)&0xFF)>0)
+ {
+ // System.out.print(".");
+ }
+ // else
+ // System.out.print("*");
+ }
+ // System.out.println("");
+ }
+
+ }
+
+ int[] pixelbuffer = new int[width*height];
+ PixelGrabber grabber = new PixelGrabber(img, 0, 0, width, height, pixelbuffer, 0, width);
+ try
+ {
+ grabber.grabPixels();
+ } catch (InterruptedException e)
+ {
+ System.err.println("interrupted waiting for pixels!");
+ throw new Exception("Can't load the image provided",e);
+ }
+
+ Hashtable colors = calculateColorCount(pixelbuffer);
+
+ // FORCE ALWAYS to 8
+ this.BitsPerPixel = 8;
+
+ Palette = new ResIcon.PaletteElement[1 << BitsPerPixel];
+ // System.out.println("Creating palette of " + Palette.length + " colors (" + colors.size() + ")");
+ for (Enumeration e=colors.keys(); e.hasMoreElements(); )
+ {
+ Integer pixi = (Integer)e.nextElement();
+ int pix = pixi.intValue();
+ int index = ((Integer)colors.get(pixi)).intValue();
+ // System.out.println("set pixel " + index);
+
+ Palette[index] = new ResIcon.PaletteElement();
+ Palette[index].Blue = pix & 0xFF;
+ Palette[index].Green = (pix >> 8) & 0xff;
+ Palette[index].Red = (pix >> 16) & 0xff;
+ }
+ for (int i=0; i<Palette.length; i++)
+ {
+ if (Palette[i] == null)
+ Palette[i] = new ResIcon.PaletteElement();
+ }
+
+
+ this.Size = 40;
+ this.Width = width;
+ this.Height = height* 2;
+ this.Planes = 1;
+ this.Compression = 0;
+
+ this.SizeOfBitmap = 0;
+ this.HorzResolution = 0;
+ this.VertResolution = 0;
+
+ this.ColorsUsed = 0;
+ this.ColorsImportant = 0;
+
+ //
+ // We calculate the rowsize in bytes. It seems that it must be
+ // aligned on a double word, although none of the
+ // documentation I have on the icon format states so.
+ //
+ int rowsize = width / 8;
+ if ((rowsize%4)>0)
+ {
+ rowsize += 4 - (rowsize%4);
+ }
+
+ BitmapXOR = new short[(((int)Height/2) * (int)Width * (int)BitsPerPixel) / 8];
+ BitmapAND = new short[((int)Height/2) * rowsize];
+
+ int bxl = BitmapXOR.length-1;
+ int bal = BitmapAND.length-1;
+
+ for (int i=0; i<pixelbuffer.length; i++)
+ {
+ int col = i%width;
+ int line = i/width;
+
+ bxl = (width * height) - (((i/width)+1)*width) + (i%width);
+ // bal = ((width * height)/8) - ((line+1)*(width/8)) + (col/8);
+ bal = (rowsize * height) - ((line+1)*(rowsize)) + (col/8);
+
+ // if ((pixelbuffer[i] & 0xFF000000) != 0x00000000)
+
+ //
+ // If the color is transparent, any color will suit
+ // (as it is not supposed to be displayed)
+ //
+ if ( (((pixelbuffer[i]>>24)& 0xFF) == 0))
+ {
+ BitmapAND[ bal ] |= 1 << (7-(i%8));
+ BitmapXOR[bxl] = 0xFF; // (short)getBrightest(); FF
+
+ // int pixel = pixelbuffer[i] & 0x00FFFFFF;
+ // pixel = 0x000000;
+ // Integer icol = (Integer)colors.get(new Integer(pixel));
+ // if (icol != null)
+ // {
+ // int palindex = icol.intValue();
+ // BitmapXOR[bxl] = (short)palindex;
+ // }
+ // else
+ // {
+ // BitmapXOR[bxl] = 0; // (short)getBrightest();
+ // System.out.println("Can't find TRANSP BLACK COL " + icol );
+ // }
+ }
+ else
+ {
+ int pixel = pixelbuffer[i] & 0x00FFFFFF;
+ // pixel = 0x000000;
+ Integer icol = (Integer)colors.get(new Integer(pixel));
+ if (icol != null)
+ {
+ int palindex = icol.intValue();
+ BitmapXOR[bxl] = (short)palindex;
+ }
+ }
+ }
+ }
+
+ private int getBrightest()
+ {
+ int result = 0;
+ int averesult = 0;
+ for (int i=0; i<Palette.length; i++)
+ {
+ int ave1 = (Palette[0].Red + Palette[0].Green + Palette[0].Blue)/3;
+ if (ave1 > averesult)
+ {
+ averesult = ave1;
+ result = i;
+ }
+ }
+ return result;
+ }
+
+ private Hashtable calculateColorCount(int[] pixels)
+ {
+ Hashtable result = new Hashtable();
+ int colorindex = 0;
+ for (int i=0; i<pixels.length; i++)
+ {
+ int pix = pixels[i];
+ if (((pix>>24)&0xFF) > 0)
+ {
+ pix &= 0x00FFFFFF;
+ Integer pixi = new Integer(pix);
+ Object o = result.get(pixi);
+ if (o == null)
+ {
+ result.put(pixi, new Integer(colorindex++));
+ }
+ // if (colorindex > 256)
+ // return result;
+ }
+ }
+ return result;
+ }
+
+ /** Creates and returns a ByteBuffer containing an image under
+ * the .ico format expected by Windows.
+ * @return a ByteBuffer with the .ico data
+ */
+ public ByteBuffer getData()
+ {
+ int cols = (int)ColorsUsed;
+ if (cols == 0)
+ cols = 1 << BitsPerPixel;
+
+ int rowsize = (int)Width / 8;
+ if ((rowsize%4)>0)
+ {
+ rowsize += 4 - (rowsize%4);
+ }
+
+ ByteBuffer buf = ByteBuffer.allocate((int) (40 + (cols*4) + (Width*(Height/2)*BitsPerPixel)/8 + (rowsize*(Height/2))));
+ buf.order(ByteOrder.LITTLE_ENDIAN);
+ buf.position(0);
+
+ buf.putInt((int)Size);
+ buf.putInt((int)Width);
+ buf.putInt((int)Height);
+ buf.putShort((short)Planes);
+ buf.putShort((short)BitsPerPixel);
+ buf.putInt((int)Compression);
+ buf.putInt((int)SizeOfBitmap);
+ buf.putInt((int)HorzResolution);
+ buf.putInt((int)VertResolution);
+ buf.putInt((int)ColorsUsed);
+ buf.putInt((int)ColorsImportant);
+
+ // System.out.println("GET DATA :: Palette.size= "+Palette.length + " // position=" + buf.position());
+ for (int i=0; i<Palette.length; i++)
+ {
+ PaletteElement el = Palette[i];
+ buf.put((byte)el.Blue);
+ buf.put((byte)el.Green);
+ buf.put((byte)el.Red);
+ buf.put((byte)el.Reserved);
+ }
+
+ switch (BitsPerPixel)
+ {
+ case 4:
+ {
+ for (int i=0; i<BitmapXOR.length; i+=2)
+ {
+ int v1 = BitmapXOR[i];
+ int v2 = BitmapXOR[i+1];
+ buf.put((byte)( (v1<<4) | v2 ));
+ }
+ }
+ break;
+
+ case 8:
+ {
+ // System.out.println("GET DATA :: XORBitmap.size= "+BitmapXOR.length + " // position=" + buf.position());
+ for (int i=0; i<BitmapXOR.length; i++)
+ {
+ buf.put((byte)BitmapXOR[i]);
+ }
+ }
+ break;
+
+ default:
+ throw new RuntimeException("BitRes " + BitsPerPixel + " not supported!");
+ }
+
+ // System.out.println("GET DATA :: AndBitmap.size= "+BitmapAND.length + " // position=" + buf.position());
+ for (int i=0; i<BitmapAND.length; i++)
+ {
+ buf.put((byte)BitmapAND[i]);
+ }
+
+ // System.out.println("GET DATA END AT " + buf.position());
+ buf.position(0);
+ return buf;
+ }
+
+ public String toString()
+ {
+ StringBuffer out = new StringBuffer();
+
+ out.append("Size: " + Size);
+ out.append("\nWidth: " + Width);
+ out.append("\nHeight: " + Height);
+ out.append("\nPlanes: " + Planes);
+ out.append("\nBitsPerPixel: " + BitsPerPixel);
+ out.append("\nCompression: " + Compression);
+ out.append("\nSizeOfBitmap: " + SizeOfBitmap);
+ out.append("\nHorzResolution: " + HorzResolution);
+ out.append("\nVertResolution: " + VertResolution);
+ out.append("\nColorsUsed: " + ColorsUsed);
+ out.append("\nColorsImportant: " + ColorsImportant);
+
+ // for (int i = 0; i<Palette.length; i++)
+ // {
+ // out.append("\n");
+ // out.append(Palette[i].toString());
+ // }
+ out.append("\nBitmapXOR["+ BitmapXOR.length+ "]={");
+ for (int i=0; i<BitmapXOR.length; i++)
+ {
+ out.append((byte)BitmapXOR[i]);
+ }
+ out.append("}\nBitmapAnd["+ BitmapAND.length +"]={");
+ for (int i=0; i<BitmapAND.length; i++)
+ {
+ out.append((byte)BitmapAND[i]);
+ }
+
+ return out.toString();
+ }
+
+ public static void main(String[]args) throws Exception
+ {
+ net.charabia.jsmoothgen.pe.PEFile.main(args);
+ }
+}
diff --git a/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/pe/res/ResIcon32.java b/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/pe/res/ResIcon32.java
new file mode 100644
index 0000000..0ecca5a
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/src/net/charabia/jsmoothgen/pe/res/ResIcon32.java
@@ -0,0 +1,126 @@
+package net.charabia.jsmoothgen.pe.res;
+
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * 32 bits res icon with alpha channel
+ */
+public class ResIcon32 extends ResIcon {
+ BufferedImage image;
+
+ /**
+ * Creates a new instance based on the data of the Image argument.
+ *
+ * @param img image
+ * @throws Exception on error
+ */
+ public ResIcon32(Image img) throws Exception {
+ int width = img.getWidth(null);
+ int height = img.getHeight(null);
+
+ image = (BufferedImage) img;
+
+ this.BitsPerPixel = 32;
+
+ // header size
+ this.Size = 40;
+ this.Width = width;
+ this.Height = height * 2;
+ this.Planes = 1;
+ this.Compression = 0;
+
+ this.SizeOfBitmap = 0;
+ this.HorzResolution = 0;
+ this.VertResolution = 0;
+
+ this.ColorsUsed = 0;
+ this.ColorsImportant = 0;
+ }
+
+ /**
+ * Creates and returns a ByteBuffer containing an image under
+ * the .ico format expected by Windows.
+ *
+ * @return a ByteBuffer with the .ico data
+ */
+ public ByteBuffer getData() {
+ // transparency mask rows must be double bytes aligned
+ int rowsize = (int) Width / 8;
+ int padding = 0;
+ if ((rowsize % 4) > 0) {
+ padding = (int) ((4 - Width / 8 % 4) * 8);
+ rowsize += 4 - (rowsize % 4);
+ }
+ // transparency line padding size
+ // create transparency mask buffer
+ int transparencyBytesSize = (int) (rowsize * (Height / 2));
+ int[] transparencyMask = new int[transparencyBytesSize];
+
+ // allocate header + pixel count * bytes / pixel + transparency mask size
+ ByteBuffer buf = ByteBuffer.allocate((int) (40 + (Width * ((Height) / 2) * BitsPerPixel/8) + transparencyBytesSize));
+
+ buf.order(ByteOrder.LITTLE_ENDIAN);
+ buf.position(0);
+
+ // write header
+ buf.putInt((int) Size);
+ buf.putInt((int) Width);
+ buf.putInt((int) Height);
+ buf.putShort((short) Planes);
+ buf.putShort((short) BitsPerPixel);
+ buf.putInt((int) Compression);
+ buf.putInt((int) SizeOfBitmap);
+ buf.putInt((int) HorzResolution);
+ buf.putInt((int) VertResolution);
+ buf.putInt((int) ColorsUsed);
+ buf.putInt((int) ColorsImportant);
+
+ int pixelCount = 0;
+ for (int y = (int) ((Height / 2) - 1); y >= 0; y--) {
+ for (int x = 0; x < Width; x++) {
+ int pix = image.getRGB(x, y);
+ //System.out.println(x+" "+y+":"+Integer.toHexString((pix >> 24) & 0xff));
+ boolean isTransparent = ((pix >> 24) & 0xff) == 0;
+ if (isTransparent) {
+ int index = pixelCount / 8;
+ byte bitindex = (byte) (pixelCount % 8);
+ transparencyMask[index] |= 0x80 >> bitindex;
+ }
+ buf.putInt(pix);
+ pixelCount++;
+ }
+ // skip to next transparency line
+ pixelCount += padding;
+ }
+
+ // transparency mask
+ for (int x = 0; x < transparencyBytesSize; x++) {
+ buf.put((byte) (transparencyMask[x] & 0xff));
+ }
+
+ buf.position(0);
+ return buf;
+ }
+
+ public String toString() {
+ StringBuffer out = new StringBuffer();
+
+ out.append("Size: ").append(Size);
+ out.append("\nWidth: ").append(Width);
+ out.append("\nHeight: ").append(Height);
+ out.append("\nPlanes: ").append(Planes);
+ out.append("\nBitsPerPixel: ").append(BitsPerPixel);
+ out.append("\nCompression: ").append(Compression);
+ out.append("\nSizeOfBitmap: ").append(SizeOfBitmap);
+ out.append("\nHorzResolution: ").append(HorzResolution);
+ out.append("\nVertResolution: ").append(VertResolution);
+ out.append("\nColorsUsed: ").append(ColorsUsed);
+ out.append("\nColorsImportant: ").append(ColorsImportant);
+
+ return out.toString();
+ }
+
+}
diff --git a/jsmooth-0.9.9-7-patch/src/net/charabia/util/codec/IcoCodec.java b/jsmooth-0.9.9-7-patch/src/net/charabia/util/codec/IcoCodec.java
new file mode 100644
index 0000000..7e88b27
--- /dev/null
+++ b/jsmooth-0.9.9-7-patch/src/net/charabia/util/codec/IcoCodec.java
@@ -0,0 +1,522 @@
+/*
+ JSmooth: a VM wrapper toolkit for Windows
+ Copyright (C) 2003 Rodrigo Reyes <reyes at charabia.net>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+ */
+
+package net.charabia.util.codec;
+
+import java.awt.Image;
+import java.awt.image.BufferedImage;
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+
+import javax.imageio.ImageIO;
+
+import net.charabia.util.io.BinaryInputStream;
+
+/**
+ *
+ * @author Rodrigo Reyes
+ * @author Riccardo Gerosa
+ */
+public class IcoCodec
+{
+
+ static private byte[] PNG_SIGNATURE = new byte[] { -119, 80, 78, 71, 13, 10, 26, 10 };
+
+ static public class IconDir
+ {
+ int idType;
+ int idCount;
+
+ public IconDir(BinaryInputStream in) throws IOException
+ {
+ in.readUShortLE();
+ idType = in.readUShortLE();
+ idCount = in.readUShortLE();
+ }
+
+ public String toString()
+ {
+ return "{ idType=" + idType + ", " + idCount + " }";
+ }
+ }
+
+ static public class IconEntry
+ {
+ short bWidth;
+ short bHeight;
+ short bColorCount;
+ short bReserved;
+ int wPlanes;
+ int wBitCount;
+ long dwBytesInRes;
+ long dwImageOffset;
+
+ public IconEntry(BinaryInputStream in) throws IOException
+ {
+ bWidth = in.readUByte();
+ if (bWidth == 0) { bWidth = 256; }
+ bHeight = in.readUByte();
+ if (bHeight == 0) { bHeight = 256; }
+ bColorCount = in.readUByte();
+ bReserved = in.readUByte();
+ wPlanes = in.readUShortLE();
+ wBitCount = in.readUShortLE();
+ dwBytesInRes = in.readUIntLE();
+ dwImageOffset = in.readUIntLE();
+ }
+
+ public String toString()
+ {
+ StringBuffer buffer = new StringBuffer();
+ buffer.append("{ bWidth="+bWidth+"\n");
+ buffer.append(" bHeight="+bHeight+"\n");
+ buffer.append(" bColorCount="+bColorCount+(bColorCount == 0 ? "(not paletted)":"")+"\n");
+ buffer.append(" wPlanes="+wPlanes+"\n");
+ buffer.append(" wBitCount="+wBitCount+"\n");
+ buffer.append(" dwBytesInRes="+dwBytesInRes+"\n");
+ buffer.append(" dwImageOffset="+dwImageOffset+"\n");
+ buffer.append("}");
+
+ return buffer.toString();
+ }
+
+ }
+
+ static public class IconHeader
+ {
+ public long Size; /* Size of this header in bytes DWORD 0*/
+ public long Width; /* Image width in pixels LONG 4*/
+ public long Height; /* Image height in pixels LONG 8*/
+ public int Planes; /* Number of color planes WORD 12 */
+ public int BitsPerPixel; /* Number of bits per pixel WORD 14 */
+ /* Fields added for Windows 3.x follow this line */
+ public long Compression; /* Compression methods used DWORD 16 */
+ public long SizeOfBitmap; /* Size of bitmap in bytes DWORD 20 */
+ public long HorzResolution; /* Horizontal resolution in pixels per meter LONG 24 */
+ public long VertResolution; /* Vertical resolution in pixels per meter LONG 28*/
+ public long ColorsUsed; /* Number of colors in the image DWORD 32 */
+ public long ColorsImportant; /* Minimum number of important colors DWORD 36 */
+
+ public IconHeader(BinaryInputStream in) throws IOException
+ {
+ Size = in.readUIntLE();
+ Width = in.readUIntLE();
+ Height = in.readUIntLE();
+ Planes = in.readUShortLE();
+ BitsPerPixel = in.readUShortLE();
+ Compression = in.readUIntLE();
+ SizeOfBitmap = in.readUIntLE();
+ HorzResolution = in.readUIntLE();
+ VertResolution = in.readUIntLE();
+ ColorsUsed = in.readUIntLE();
+ ColorsImportant = in.readUIntLE();
+ }
+
+ public String toString()
+ {
+ StringBuffer buffer = new StringBuffer();
+ buffer.append("Size="); buffer.append(Size);
+ buffer.append("\nWidth="); buffer.append(Width);
+ buffer.append("\nHeight="); buffer.append(Height);
+ buffer.append("\nPlanes="); buffer.append(Planes);
+ buffer.append("\nBitsPerPixel="); buffer.append(BitsPerPixel);
+ buffer.append("\nCompression="); buffer.append(Compression);
+ buffer.append("\nSizeOfBitmap="); buffer.append(SizeOfBitmap);
+ buffer.append("\nHorzResolution="); buffer.append(HorzResolution);
+ buffer.append("\nVertResolution="); buffer.append(VertResolution);
+ buffer.append("\nColorsUsed="); buffer.append(ColorsUsed);
+ buffer.append("\nColorsImportant="); buffer.append(ColorsImportant);
+
+ return buffer.toString();
+ }
+
+ }
+
+ public static BufferedImage checkImageSize(BufferedImage img, int width, int height) {
+ int w = img.getWidth(null);
+ int h = img.getHeight(null);
+ if ((w == width) && (h == height))
+ return img;
+ return null;
+ }
+
+ static boolean isSquareSize(BufferedImage bufferedImage, int size) {
+ return bufferedImage.getWidth() == bufferedImage.getHeight() && bufferedImage.getWidth() == size;
+ }
+
+ static int getColorDepth(BufferedImage bufferedImage) {
+ return bufferedImage.getColorModel().getPixelSize();
+ }
+
+ static public BufferedImage getPreferredImage(File f) throws IOException {
+ BufferedImage selected = null;
+ BufferedImage[] orgimages = loadImages(f);
+ if (orgimages != null) {
+ // select first image
+ selected = orgimages[0];
+ for (int i = 1; (i < orgimages.length); i++) {
+ // We prefer 32x32 pictures, then 64x64, then 16x16...
+ if (isSquareSize(orgimages[i], 32)) {
+ if (!isSquareSize(selected, 32) || getColorDepth(orgimages[i]) > getColorDepth(selected)) {
+ selected = orgimages[i];
+ }
+ } else if (isSquareSize(orgimages[i], 64)) {
+ if (!isSquareSize(selected, 32) ||
+ (isSquareSize(selected, 64) && getColorDepth(orgimages[i]) > getColorDepth(selected))) {
+ selected = orgimages[i];
+ }
+
+ } else if (isSquareSize(orgimages[i], 16)) {
+ if ((!isSquareSize(selected, 32) && !isSquareSize(selected, 64)) ||
+ (isSquareSize(selected, 16) && getColorDepth(orgimages[i]) > getColorDepth(selected))) {
+ selected = orgimages[i];
+ }
+ }
+ }
+
+ if (selected == null) {
+ //
+ // If there is no 32x32, 64x64, nor 16x16, then we scale the
+ // biggest image to be 32x32... This should happen mainly when
+ // loading an image from a png of gif file, and in most case
+ // there is only one image on the array.
+ //
+ int maxsize = 0;
+ for (int i = 0; (i < orgimages.length) && (selected == null); i++) {
+ int size = orgimages[i].getWidth(null) * orgimages[i].getHeight(null);
+ if (size > maxsize) {
+ maxsize = size;
+ selected = orgimages[i];
+ }
+ }
+ }
+ }
+ return selected;
+ }
+
+ static public BufferedImage[] loadImages(File f) throws IOException
+ {
+ InputStream istream = new FileInputStream(f);
+ BufferedInputStream buffin = new BufferedInputStream(istream);
+ BinaryInputStream in = new BinaryInputStream(buffin);
+
+ try {
+ in.mark(1024 * 1024);
+
+ IconDir dir = new IconDir(in);
+ // System.out.println("DIR = " + dir);
+
+ IconEntry[] entries = new IconEntry[dir.idCount];
+ BufferedImage[] images = new BufferedImage[dir.idCount];
+
+ for (int i=0; i<dir.idCount; i++)
+ {
+ entries[i] = new IconEntry(in);
+ // System.out.println("ENTRY " + i + " = " + entries[i]);
+ }
+
+ IconEntry entry = entries[0];
+ // System.out.println("ENTRYx = " + entry);
+
+ for (int i=0; i<dir.idCount; i++)
+ {
+ in.reset();
+
+ boolean pngIcon = false;
+ if (entries[i].dwBytesInRes > PNG_SIGNATURE.length) {
+ // Check if this is a PNG icon (introduced in Windows Vista)
+ in.mark(1024 * 1024);
+ in.skip(entries[i].dwImageOffset);
+ byte[] signatureBuffer = new byte[PNG_SIGNATURE.length];
+ in.read(signatureBuffer, 0, PNG_SIGNATURE.length);
+ pngIcon = Arrays.equals(PNG_SIGNATURE, signatureBuffer);
+ in.reset();
+ }
+ // System.out.println("PNG Icon = " + pngIcon);
+
+ in.skip(entries[i].dwImageOffset);
+
+ BufferedImage image;
+
+ if (pngIcon) {
+ // This is a PNG icon (introduced in Windows Vista)
+ byte[] imageData = new byte[(int) entries[i].dwBytesInRes];
+ in.read(imageData, 0, (int) entries[i].dwBytesInRes);
+ ByteArrayInputStream imageDataBuffer = new ByteArrayInputStream(imageData);
+ image = ImageIO.read(imageDataBuffer);
+ } else {
+ // This is a standard icon (not PNG)
+ IconHeader header = new IconHeader(in);
+ // System.out.println("Header: " + header);
+
+ long toskip = header.Size - 40;
+ if (toskip>0)
+ in.skip((int)toskip);
+
+ // System.out.println("skipped data");
+
+
+ switch(header.BitsPerPixel)
+ {
+ case 4:
+ case 8:
+ image = new BufferedImage((int)header.Width, (int)header.Height/2,
+ BufferedImage.TYPE_BYTE_INDEXED);
+ loadPalettedImage(in, entries[i], header, image);
+ break;
+ case 24:
+ case 32:
+ image = new BufferedImage((int)header.Width, (int)header.Height/2,
+ BufferedImage.TYPE_INT_ARGB);
+ // This is a true-color icon (introduced in Windows XP)
+ loadTrueColorImage(in, entries[i], header, image);
+ break;
+ default:
+ throw new Exception("Unsupported ICO color depth: " + header.BitsPerPixel);
+ }
+ }
+
+ images[i] = image;
+ }
+
+ return images;
+
+ } catch (Exception exc)
+ {
+ exc.printStackTrace();
+ }
+
+ return null;
+ }
+
+ static private void loadPalettedImage(BinaryInputStream in, IconEntry entry, IconHeader header, BufferedImage image) throws Exception
+ {
+ // System.out.println("Loading image...");
+
+ // System.out.println("Loading palette...");
+
+ //
+ // First, load the palette
+ //
+ int cols = (int)header.ColorsUsed;
+ if (cols == 0)
+ {
+ if (entry.bColorCount != 0)
+ cols = entry.bColorCount;
+ else
+ cols = 1 << header.BitsPerPixel;
+ }
+
+ int[] redp = new int[cols];
+ int[] greenp = new int[cols];
+ int[] bluep = new int[cols];
+
+ for (int i=0; i<cols; i++)
+ {
+ bluep[i] = in.readUByte();
+ greenp[i] = in.readUByte();
+ redp[i] = in.readUByte();
+ in.readUByte();
+ }
+
+ // System.out.println("Palette read!");
+
+ //
+ // Set the image
+
+ //int xorbytes = (((int)header.Height/2) * (int)header.Width);
+ int readbytes = 0;
+
+ for (int y=(int)(header.Height/2)-1; y>=0; y--)
+ {
+ for (int x=0; x<header.Width; x++)
+ {
+ switch(header.BitsPerPixel)
+ {
+ case 4:
+ {
+ int pix = in.readUByte();
+ readbytes++;
+
+ int col1 = (pix>>4) & 0x0F;
+ int col2 = pix & 0x0F;
+ image.setRGB(x, y, (0xFF<<24) | (redp[col1]<<16) | (greenp[col1]<<8) | bluep[col1]);
+ image.setRGB(++x, y, (0xFF<<24) | (redp[col2]<<16) | (greenp[col2]<<8) | bluep[col2]);
+ }
+ break;
+ case 8:
+ {
+ int col1 = in.readUByte();
+ readbytes++;
+
+ image.setRGB(x, y, (0xFF<<24) | (redp[col1]<<16) | (greenp[col1]<<8) | bluep[col1]);
+ }
+ break;
+ }
+ }
+ }
+ // System.out.println("XOR data read (" + readbytes + " bytes)");
+
+ int height = (int)(header.Height/2);
+
+ int rowsize = (int)header.Width / 8;
+ if ((rowsize%4)>0)
+ {
+ rowsize += 4 - (rowsize%4);
+ }
+
+ // System.out.println("rowsize = " + rowsize);
+ int[] andbytes = new int[rowsize * height ];
+
+ for (int i=0; i<andbytes.length; i++)
+ andbytes[i] = in.readUByte();
+
+
+ for (int y=height-1; y>=0; y--)
+ {
+ for (int x=0; x<header.Width; x++)
+ {
+ int offset = ((height - (y+1))*rowsize) + (x/8);
+ if ( (andbytes[offset] & (1<<(7-x%8))) != 0)
+ {
+ image.setRGB(x, y, 0);
+ }
+ }
+ }
+
+ // for (int i=0; i<andbytes; i++)
+ // {
+ // int pix = in.readUByte();
+ // readbytes++;
+
+ // int xb = (i*8) % (int)header.Width;
+ // int yb = ((int)header.Height/2) - (((i*8) / (int)header.Width)+1);
+
+ // for (int offset=7; offset>=0; offset--)
+ // {
+ // //
+ // // Modify the transparency only if necessary
+ // //
+ // System.out.println("SET AND (" + xb + "," + yb + ")-" + (7-offset));
+
+ // if (((1<<offset) & pix)!=0)
+ // {
+ // int argb = image.getRGB(xb+(7-offset), yb);
+ // image.setRGB(xb+(7-offset), yb, argb & 0xFFFFFF);
+ // }
+ // }
+ // }
+
+ // System.out.println("AND data read (" + readbytes + " bytes total)");
+ }
+
+ static private void loadTrueColorImage(BinaryInputStream in, IconEntry entry, IconHeader header, BufferedImage image) throws Exception
+ {
+ // System.out.println("Loading image...");
+
+ //
+ // Set the image
+
+ //int xorbytes = (((int)header.Height/2) * (int)header.Width);
+ int readbytes = 0;
+
+ for (int y=(int)(header.Height/2)-1; y>=0; y--)
+ {
+ for (int x=0; x<header.Width; x++)
+ {
+ switch(header.BitsPerPixel)
+ {
+ case 32:
+ {
+ int b = in.readUByte();
+ int g = in.readUByte();
+ int r = in.readUByte();
+ int a = in.readUByte();
+ readbytes++;
+
+ image.setRGB(x, y, (a<<24) | (r<<16) | (g<<8) | b);
+ }
+ break;
+ case 24:
+ {
+ int b = in.readUByte();
+ int g = in.readUByte();
+ int r = in.readUByte();
+ readbytes++;
+
+ image.setRGB(x, y, (0xFF<<24) | (r<<16) | (g<<8) | b);
+ }
+ break;
+ }
+ }
+ }
+ // System.out.println("XOR data read (" + readbytes + " bytes)");
+
+ if (header.BitsPerPixel < 32) {
+ int height = (int)(header.Height/2);
+
+ int rowsize = (int)header.Width / 8;
+ if ((rowsize%4)>0)
+ {
+ rowsize += 4 - (rowsize%4);
+ }
+
+ // System.out.println("rowsize = " + rowsize);
+ int[] andbytes = new int[rowsize * height ];
+
+ for (int i=0; i<andbytes.length; i++)
+ andbytes[i] = in.readUByte();
+
+
+ for (int y=height-1; y>=0; y--)
+ {
+ for (int x=0; x<header.Width; x++)
+ {
+ int offset = ((height - (y+1))*rowsize) + (x/8);
+ if ( (andbytes[offset] & (1<<(7-x%8))) != 0)
+ {
+ image.setRGB(x, y, 0);
+ }
+ }
+ }
+ }
+
+ // System.out.println("AND data read (" + readbytes + " bytes total)");
+ }
+
+ static public void main(String[]args) throws Exception
+ {
+ File f = new File(args[0]);
+ Image img = IcoCodec.loadImages(f)[0];
+ // System.out.println("img = " + img);
+
+ javax.swing.JFrame jf = new javax.swing.JFrame("Test");
+ javax.swing.JButton button = new javax.swing.JButton(new javax.swing.ImageIcon(img));
+ jf.getContentPane().add(button);
+ jf.pack();
+ jf.setVisible(true);
+ }
+
+}
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..898b4a5
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,355 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>davmail</groupId>
+ <artifactId>davmail</artifactId>
+ <packaging>jar</packaging>
+ <version>3.9.9</version>
+ <name>DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway</name>
+ <organization>
+ <name>Mickaël Guessant</name>
+ </organization>
+ <url>http://www.sourceforge.net/projects/davmail</url>
+ <inceptionYear>2001</inceptionYear>
+ <description>
+ Ever wanted to get rid of Outlook ? DavMail is a POP/IMAP/SMTP/Caldav/Carddav/LDAP exchange gateway
+ allowing users to use any mail/calendar client (e.g. Thunderbird with Lightning or Apple iCal) with
+ an Exchange server, even from the internet or behind a firewall through Outlook Web Access.
+ DavMail now includes an LDAP gateway to Exchange global address book and user personal contacts
+ to allow recipient address completion in mail compose window and full calendar support
+ with attendees free/busy display.
+ The main goal of DavMail is to provide standard compliant protocols in front of proprietary
+ Exchange. This means LDAP for address book, SMTP to send messages, IMAP to browse messages
+ on the server in any folder, POP to retrieve inbox messages only and Caldav for calendar support.
+ Thus any standard compliant client can be used with Microsoft Exchange.
+ DavMail gateway is implemented in java and should run on any platform. Releases are tested on Windows,
+ Linux (Ubuntu) and Mac OSX. Tray does not work on MacOS and is replaced with a full frame.
+ Tested successfully with the Iphone (gateway running on a server).
+ DavMail gateway is implemented in java and should run on any platform. Releases are tested
+ on Windows, Linux (Ubuntu) and Mac OSX. Tested successfully with the Iphone
+ (gateway running on a server).
+ </description>
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ </properties>
+ <developers>
+ <developer>
+ <name>Mickaël Guessant</name>
+ <id>mguessan</id>
+ <email>mguessan at free.fr</email>
+ <url>http://mguessan.free.fr</url>
+ <roles>
+ <role>Project Founder</role>
+ <role>Java Developer</role>
+ </roles>
+ </developer>
+ </developers>
+ <contributors>
+ <contributor>
+ <name>Dfoody</name>
+ <email>dfoody at users.sourceforge.net</email>
+ <url>http://sourceforge.net/users/dfoody</url>
+ <roles>
+ <role>Main Java Contributor</role>
+ </roles>
+ </contributor>
+ <contributor>
+ <name>John Ahern</name>
+ <email>jahern2502 at users.sourceforge.net</email>
+ <url>http://sourceforge.net/users/jahern2502</url>
+ <roles>
+ <role>Java Contributor</role>
+ </roles>
+ </contributor>
+ <contributor>
+ <name>Eivind Tagseth</name>
+ <email>eivindt at users.sourceforge.net</email>
+ <url>http://sourceforge.net/users/eivindt</url>
+ <roles>
+ <role>Java Contributor</role>
+ </roles>
+ </contributor>
+ <contributor>
+ <name>Mitchell V. Oliver</name>
+ <email>pianoman113 at users.sourceforge.net</email>
+ <url>https://sourceforge.net/users/pianoman113</url>
+ <roles>
+ <role>Java Contributor</role>
+ </roles>
+ </contributor>
+ <contributor>
+ <name>Gellule</name>
+ <email>gellule at users.sourceforge.net</email>
+ <url>https://sourceforge.net/users/gellule</url>
+ <roles>
+ <role>Java Contributor</role>
+ </roles>
+ </contributor>
+ <contributor>
+ <name>Jeremiah Albrant</name>
+ <url>http://sourceforge.net/users/?user_id=2903536</url>
+ <roles>
+ <role>Java Contributor</role>
+ </roles>
+ </contributor>
+ <contributor>
+ <name>Henning Holtschneider</name>
+ <email>hehol at users.sourceforge.net</email>
+ <url>http://sourceforge.net/users/hehol</url>
+ <roles>
+ <role>Tester</role>
+ </roles>
+ </contributor>
+ <contributor>
+ <name>Shocker</name>
+ <email>gkwait at users.sourceforge.net</email>
+ <url>http://sourceforge.net/users/gkwait</url>
+ <roles>
+ <role>Tester</role>
+ </roles>
+ </contributor>
+ <contributor>
+ <name>Guido</name>
+ <email>guido2 at users.sourceforge.net</email>
+ <url>http://sourceforge.net/users/guido2</url>
+ <roles>
+ <role>Tester</role>
+ </roles>
+ </contributor>
+ </contributors>
+ <licenses>
+ <license>
+ <name>GNU General Public License</name>
+ <url>http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt</url>
+ <distribution>repo</distribution>
+ </license>
+ </licenses>
+ <mailingLists>
+ <mailingList>
+ <name>DavMail User List</name>
+ <post>davmail-users at lists.sourceforge.net.</post>
+ <subscribe>http://lists.sourceforge.net/lists/listinfo/davmail-users</subscribe>
+ <unsubscribe>http://lists.sourceforge.net/lists/listinfo/davmail-users</unsubscribe>
+ <archive>http://sourceforge.net/mailarchive/forum.php?forum_name=davmail-users</archive>
+ </mailingList>
+ </mailingLists>
+ <scm>
+ <connection>scm:svn:https://davmail.svn.sourceforge.net/svnroot/davmail/trunk</connection>
+ <developerConnection>scm:svn:https://davmail.svn.sourceforge.net/svnroot/davmail/trunk</developerConnection>
+ <tag>HEAD</tag>
+ <url>http://davmail.svn.sourceforge.net/viewvc/davmail/trunk/</url>
+ </scm>
+ <issueManagement>
+ <system>SourceForge</system>
+ <url>http://sourceforge.net/tracker/?group_id=184600</url>
+ </issueManagement>
+ <repositories>
+ <repository>
+ <id>central</id>
+ <name>Maven Repository Switchboard</name>
+ <layout>default</layout>
+ <url>http://repo1.maven.org/maven2</url>
+ <snapshots>
+ <enabled>false</enabled>
+ </snapshots>
+ </repository>
+ <repository>
+ <id>xwiki</id>
+ <layout>default</layout>
+ <url>http://maven.xwiki.org/externals</url>
+ <snapshots>
+ <enabled>false</enabled>
+ </snapshots>
+ </repository>
+ </repositories>
+ <dependencies>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>3.8.1</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.jackrabbit</groupId>
+ <artifactId>jackrabbit-webdav</artifactId>
+ <version>1.4</version>
+ <exclusions>
+ <exclusion>
+ <groupId>org.apache.jackrabbit</groupId>
+ <artifactId>jackrabbit-jcr-commons</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>xml-apis</groupId>
+ <artifactId>xml-apis</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-log4j12</artifactId>
+ <version>1.3.1</version>
+ </dependency>
+ <dependency>
+ <groupId>commons-httpclient</groupId>
+ <artifactId>commons-httpclient</artifactId>
+ <version>3.1</version>
+ </dependency>
+ <dependency>
+ <groupId>commons-codec</groupId>
+ <artifactId>commons-codec</artifactId>
+ <version>1.3</version>
+ </dependency>
+ <dependency>
+ <groupId>log4j</groupId>
+ <artifactId>log4j</artifactId>
+ <version>1.2.16</version>
+ <exclusions>
+ <exclusion>
+ <groupId>javax.jms</groupId>
+ <artifactId>jms</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>com.sun.jdmk</groupId>
+ <artifactId>jmxtools</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>com.sun.jmx</groupId>
+ <artifactId>jmxri</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>javax.mail</groupId>
+ <artifactId>mail</artifactId>
+ <version>1.4.3</version>
+ </dependency>
+ <dependency>
+ <groupId>javax.activation</groupId>
+ <artifactId>activation</artifactId>
+ <version>1.1.1</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse</groupId>
+ <artifactId>swt</artifactId>
+ <version>3.7.0</version>
+ <optional>true</optional>
+ </dependency>
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>servlet-api</artifactId>
+ <version>2.4</version>
+ <optional>true</optional>
+ </dependency>
+ <dependency>
+ <groupId>net.sourceforge.htmlcleaner</groupId>
+ <artifactId>htmlcleaner</artifactId>
+ <version>2.1</version>
+ </dependency>
+ <!-- included in Java 1.6, needed with Java 1.5 -->
+ <dependency>
+ <groupId>org.codehaus.woodstox</groupId>
+ <artifactId>woodstox-core-asl</artifactId>
+ <version>4.1.2</version>
+ </dependency>
+ <dependency>
+ <groupId>org.samba.jcifs</groupId>
+ <artifactId>jcifs</artifactId>
+ <version>1.3.14</version>
+ </dependency>
+ <dependency>
+ <groupId>net.freeutils.charset</groupId>
+ <artifactId>jcharset</artifactId>
+ <version>1.3</version>
+ </dependency>
+ <dependency>
+ <groupId>org.boris.winrun4j</groupId>
+ <artifactId>winrun4j</artifactId>
+ <version>0.4.4</version>
+ </dependency>
+ </dependencies>
+ <pluginRepositories>
+ <pluginRepository>
+ <id>apache.snapshots</id>
+ <url>http://repository.apache.org/snapshots</url>
+ <releases>
+ <enabled>false</enabled>
+ </releases>
+ </pluginRepository>
+ </pluginRepositories>
+ <build>
+ <sourceDirectory>src/java</sourceDirectory>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <source>1.5</source>
+ <target>1.5</target>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-site-plugin</artifactId>
+ <version>2.2</version>
+ </plugin>
+ </plugins>
+ </build>
+ <distributionManagement>
+ <site>
+ <id>sourceforge.net</id>
+ <url>scp://web.sourceforge.net/home/groups/d/da/davmail/htdocs</url>
+ </site>
+ </distributionManagement>
+ <reporting>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-project-info-reports-plugin</artifactId>
+ <reportSets>
+ <reportSet>
+ <reports>
+ <report>dependencies</report>
+ <report>dependency-convergence</report>
+ <report>project-team</report>
+ <report>mailing-list</report>
+ <report>issue-tracking</report>
+ <report>license</report>
+ <report>scm</report>
+ <report>index</report>
+ </reports>
+ </reportSet>
+ </reportSets>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-javadoc-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-changelog-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>findbugs-maven-plugin</artifactId>
+ <version>1.2</version>
+ <configuration>
+ <threshold>Normal</threshold>
+ <effort>Default</effort>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-checkstyle-plugin</artifactId>
+ <version>2.4</version>
+ <configuration>
+ <configLocation>src/checkstyle/checkstyle-configuration.xml</configLocation>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jxr-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </reporting>
+</project>
diff --git a/releaseguide.txt b/releaseguide.txt
new file mode 100644
index 0000000..3f18c1e
--- /dev/null
+++ b/releaseguide.txt
@@ -0,0 +1,22 @@
+Release guide :
+- update roadmap
+- update releasenotes.txt from svn log
+- change version in build.xml and pom.xml, commit
+- update staging (clean checkout) directory
+- launch ant release, check generated packages
+- upload packages: ant upload-release
+- make sure swt and jcifs are available in local maven repository:
+mvn install:install-file -DgroupId=org.eclipse -DartifactId=swt -Dversion=3.7.0 -Dpackaging=jar -Dfile=lib/swt-3.7-win32-x86.jar
+mvn install:install-file -DgroupId=org.samba.jcifs -DartifactId=jcifs -Dversion=1.3.14 -Dpackaging=jar -Dfile=lib/jcifs-1.3.14.jar
+mvn install:install-file -DgroupId=javax.mail -DartifactId=mail -Dversion=1.4.3 -Dpackaging=jar -Dfile=lib/mail-1.4.3.jar
+mvn install:install-file -DgroupId=net.freeutils.charset -DartifactId=jcharset -Dversion=1.3 -Dpackaging=jar -Dfile=lib/jcharset-1.3.jar
+mvn install:install-file -DgroupId=org.boris.winrun4j -DartifactId=winrun4j -Dversion=0.4.4 -Dpackaging=jar -Dfile=lib/winrun4j-0.4.4.jar
+- generate site: mvn site
+- upload site: ant upload-site
+- upload releasenotes.txt to sourceforge
+- create tag in subversion
+- create news message at https://sourceforge.net/news/admin/?group_id=184600
+- send user mailing list message
+- change default download files on frs
+- upload version.txt with ant upload-version
+- create new version in sourceforge bug tracker
diff --git a/releasenotes.txt b/releasenotes.txt
new file mode 100644
index 0000000..f95554c
--- /dev/null
+++ b/releasenotes.txt
@@ -0,0 +1,2007 @@
+** DavMail 3.9.9 released **
+Bugfix release with major IMAP changes to improve sync performance,
+many Caldav enhancements and bugfixes and some documentation updates.
+
+Caldav:
+- Caldav: encode ? in urlcompname
+- Caldav: fix 3534615, patch allday dates only on Exchange 2007
+- Caldav: implement full contact folder dump at /users/<email>/contacts/
+- Caldav: implement task priority over EWS
+- Caldav: remove unsupported attachment reference to avoid iPhone/iPad crash
+- Caldav: reintroduce davmail.caldavDisableTasks setting to disable tasks support
+- Caldav: fix encode pipe | to %7C in urlcompname
+- Caldav: encode pipe | to %7C in urlcompname
+- CalDav: Fix 3512857, avoid double path encoding in DavExchangeSession.loadVtimezone()
+- Caldav: improve Exchange 2007 EWS meeting support
+- Caldav: rebuild meeting attendees only for Exchange 2007, Exchange 2010 ics parser is correct
+
+Enhancements:
+- Fixes from audit
+- store davmail.log in user home folder to avoid crash on first start when current directory is not writable by user
+- Add WinRun4J to Maven POM and update windows service documentation
+- Switch to WinRun4J for Windows service wrapper
+- Fix 3494770: Add missing antlr runtime
+- Upgrade svnkit for subversion 1.7 compatibility
+
+IMAP:
+- IMAP: Fix 3534801, workaround for missing From header
+- IMAP: fix 3441891, workaround for Exchange 2003 ActiveSync bug
+- IMAP: experimental implementation of header only FETCH, do not download full message content and send approximate RFC822.SIZE (MAPI size)
+- IMAP: avoid full message download on OSX Lion flags request with content-class header
+- IMAP: exclude IDLE from infinite loop detection
+- IMAP: add date header to rebuilt message
+- IMAP: Force UTF-8 on message rebuild
+- IMAP: implement RFC822 fetch request
+
+GUI:
+- GUI: force alwaysOnTop on dialogs to make sure they are visible
+- GUI: always bring dialog windows to front
+
+Documentation:
+- Doc: add a new FAQ entry on shared mailbox access over IMAP
+- Doc: Update doc to include Java 7
+- Doc: small fix in Linux setup doc
+- Doc: Update Linux instructions for Ubuntu 12 Natty
+- Doc: New review
+- Doc: update Thunderbird POP account setup doc
+- Doc: Update SSL setup documentation on PKCS12 passwords
+- Doc: add a note on hidden folders on OSX Lion
+- Doc: Fix new thunderbird doc
+
+OSX:
+- OSX: new hide from Dock setting available directly in UI (DavMail restart needed)
+
+Carddav:
+- Carddav: Fix 3511472, implement fileas over EWS
+- Carddav: Skip carriage return in ICSBufferedWriter
+
+EWS:
+- EWS: disable gzip encoding if WIRE logging is at DEBUG level
+- EWS: fix 3263905 ErrorInvalidPropertyRequest, do not update message:IsRead on appointments
+- EWS: make isMainCalendar case insensitive
+- EWS: revert chunked inputstream inside gzip and create new setting davmail.acceptEncodingGzip
+- EWS: handle chunked inputstream inside gzip
+- EWS: improve error message handling, log error description
+- EWS: improve error handling on socket exception
+- EWS: avoid NullPointerException in broken message rebuild
+
+WebDav:
+- Dav: decode permanenturl to avoid double urlencoding issue
+- Dav: decode url returned on saveappt cmd in DavExchangeSession.loadVtimezone()
+
+** DavMail 3.9.8 released **
+Prepare 4.0 release with improved Exchange 2010 support, added IMAP MOVE extension support,
+include a new windows noinstall package and implement captcha authentication support.
+
+Documentation:
+- Doc: update roadmap
+- Doc: add a statement on adding NSIS to system path in build instructions
+- Doc: update Thunderbird IMAP setup instructions for Thunderbird 10
+- Doc: update java package reference
+- Doc: update address book setup instructions for OSX Lion
+- Doc: add Growl reference in OSX setup
+
+Enhancements:
+- Fix nsis script: delete stax api jar on uninstall
+- Fixes from audit
+- New redline ant task definition fix
+- Exclude Junit from binary packages
+- Create Windows noinstall package
+- Implement a new davmail.clientSoTimeout setting to adjust or disable connection timeout
+- Improve message on invalid OWA uri
+- Fix notification dialog test
+- Improve Pinsafe captcha display
+- workaround for broken form with empty action
+- Implement ISA server PINsafeISAFilter support (captcha image)
+- Upgrade Redline RPM
+- Add StreamScanner.java from Woodstox 4.1.2
+- Upgrade to Woodstox 4.1.2
+- Fix 3454332: davmail.sh script missing shebang
+- add trust=true in upload-site
+
+IMAP:
+- IMAP: fix search date format for Exchange 2010 support (ErrorInvalidValueForProperty)
+- IMAP: implement SEARCH TEXT on from, to, cc, subject and body
+- IMAP: send error on COPY/MOVE when message iterator is empty
+- IMAP: implement MOVE RFC draft http://tools.ietf.org/id/draft-krecicki-imap-move-00.html
+- IMAP: fix 3480516, () instead of NIL on empty envelope header
+- IMAP: Fix 3479993, backslash in header
+
+SMTP:
+- SMTP: fix 3489007, Sparrow AUTH PLAIN authentication support
+
+Caldav:
+- Caldav: force context Timezone on Exchange 2010
+- Caldav: add missing timezones from Exchange 2007 over WebDav
+- Caldav: let users edit outgoing notifications for meeting requests
+- Caldav: fix NullPointerException on addressbook request
+- Caldav: workaround for broken items with \n as first line character
+
+POP:
+- POP: add a new setting to mark messages read after RETR
+
+EWS:
+- EWS: fix ErrorInvalidValueForProperty on search undeleted with Exchange 2010, set type Integer on PidLidImapDeleted and junk 0x1083
+- EWS: new fix to improve failover on error retrieving MimeContent
+- EWS: improve failover on error retrieving MimeContent
+- EWS: Fix 3471671, workaround for Exchange invalid chars
+
+LDAP:
+- LDAP: improve invalid dn message
+
+OSX:
+- OSX: make nodock mode the default
+- OSX: make sure davmail.jar is first in classpath
+
+DAV:
+- Dav: set contact email type to SMTP
+- Dav: add email type MAPI properties
+
+Carddav:
+- Carddav: avoid NullPointerException on broken contact
+- Carddav: fix regression on address book handling on Snow Leopard
+- Carddav: decode urlcompname before search to retrieve contacts with & in url
+
+
+** DavMail 3.9.7 released **
+Another bugfix release with new stax based webdav search method implementation to reduce memory footprint with large folders,
+exclude non event items from calendar to avoid errors, some EWS fixes on tasks handling and a few documentation updates
+
+WebDav:
+- Dav: fix regression in new Stax implementation
+- Dav: new stax based WebDav requests implementation to reduce memory usage, enabled on Search requests
+- Dav: switch back to mailbox path on Exchange 2003 for CmdBasePath
+
+Caldav:
+- Caldav: Experimental patch to support spaces in calendar or contacts path on OSX, see 3464086
+- Caldav: Create a new davmail.caldavEnableLegacyTasks to allow access to tasks created in calendar folder by previous DavMail versions
+- Caldav: drop davmail.caldavDisableTasks setting, retrieve only events from calendar
+- Caldav: Change field update order for Exchange 2007 over EWS
+- Caldav: apply date filter to tasks
+- Caldav: new timezone for Mexico
+- Caldav: fix 3433584, encode comma in LOCATION field
+
+IMAP:
+- IMAP: fix double slash in folder path
+- IMAP: return all search results uids on a single line for Wanderlust
+- IMAP: new davmail.imapIncludeSpecialFolders setting to access all folders including calendar and tasks over IMAP
+- IMAP: fix wanderlust support, allow lower case fetch params
+
+Documentation:
+- Doc: Added DavMail hangs on 64-bit Linux FAQ entry
+- Doc: add documentation for davmail.logFileSize option
+
+Enhancements:
+- Change default use system proxies value to false
+- Avoid NullPointerException on WebdavNotAvailableException
+- Fix upload-version target site
+
+EWS:
+- EWS: fix Exchange 2010 SP1 support
+- EWS: use archivemsgfolderroot as archive root
+- EWS: enable preemptive authentication on non NTLM endpoints
+- EWS: add Exchange2010_SP1 support for online archive
+
+LDAP:
+- LDAP: avoid NullPointerException during SASL authentication
+
+Carddav:
+- Carddav: encode star in urlcompname
+
+
+** DavMail 3.9.6 released **
+Another bugfix release to improve iPad 2 and Debian based Linux support.
+Also includes new protocol mode options (EWS, WebDav or Auto), experimental Exchange online archive
+support, IMAP UTF-8 search parameter and many Caldav fixes
+
+Enhancements:
+- Add a new upload-version ant target to upload version.txt
+- Workaround for broken servers that send invalid Basic authentication challenge
+- Add exchangecookie to the list of authentication cookies for direct EWS access
+- Add a new auto value to davmail.enableEws setting to avoid unwanted switch from WebDav to EWS on temporary Exchange connection issue
+- Encode # in urlcompname
+- Fix bug on ITEM_PROPERTIES value on EWS/WebDav mode switch
+- Add new Default button to reset log levels
+- Implement a new option to let users disable all GUI notifications
+- Additional exception trace exclusion
+- Revert 1.7 test on SWT, tray implementation is still broken on Linux
+
+Documentation:
+- Doc: reformat urls in FAQ
+- Doc: add a note to help users with broken Unity desktop manager on Ubuntu
+- Doc: Fix typo in project description
+- Doc: additional note on Caldav setup in Thunderbird and new external review
+- Doc: document new disable balloon notifications setting
+- Doc: Update roadmap
+- Doc: New reviews
+- Doc: Update iCal doc to match both Snow Leopard and Lion
+- Doc: Update FAQ
+- Doc: Update DavMail settings screenshot
+
+Linux
+- Allow openjdk-7-jre dependency in deb package
+- Fix 3418960: Update dependencies for Ubuntu 11.10, add libswt-gtk-3-java
+
+Caldav:
+- Caldav: apply iCal 5 workaround to iOS 5
+- Caldav: new timezone in rename table
+- Caldav: try to merge Exchange 2010 and 2007 filters
+- Caldav: additional unit tests
+- Caldav: fix 3426148 decode and encode comma in RESOURCES field value
+- Caldav: Fix complex timezones sent by clients, leave only latest STANDARD and DAYLIGHT definition
+- Caldav: Fix 3420240, retrieve description from tasks over Dav
+
+EWS:
+- EWS: add new DistinguishedFolderId value for Exchange archive support
+- EWS: throw exception on 400 Bad request answer
+
+IMAP:
+- IMAP: fix 3426383, implement CHARSET in SEARCH command, allow ASCII and UTF-8
+- IMAP: fix 3353862, long file names encoding in BODYSTRUCTURE
+
+SMTP:
+- SMTP: adjust workaround for misconfigured Exchange server that return 406 Not Acceptable on draft message creation, look inside multipart messages
+
+** DavMail 3.9.5 released **
+Bugfix release to avoid Growl plugin crash on OSX, make DavMail work with both
+Snow Leopard and Lion. Also includes DIGEST-MD5 implementation for OSX Lion
+Directory Utility support, however iCal attendee completion is still broken.
+
+OSX:
+- Fix crash in Growl plugin on OSX: do not sent SSL content to Growl
+
+EWS:
+- EWS: rebuild broken message (null MimeContent) from properties
+- EWS: improve error logging on invalid character
+- EWS: fix tasks field order, send Extended Properties first to match EWS schema
+- EWS: replace extension before looking for items in task folder
+- EWS: Fix 3407395, do not set mailbox on FolderIds returned by Exchange
+
+LDAP:
+- LDAP: fix DIGEST-MD5 authentication and adjust dn context for OSX Lion, still experimental
+- LDAP: fix DIGEST-MD5 SASL authentication for OSX Lion
+- LDAP: experimental SASL DIGEST-MD5 implementation for OSX Lion Directory Utility support
+
+Caldav:
+- Caldav: need to send principal-URL on principal path, only for OSX Lion
+- Caldav: allow direct access to task folder
+
+Enhancements:
+- Force toFront and requestFocus to bring windows to front
+- Additional proxy selector logging
+- Fixes from audit
+- Upgrade Log4J to 1.2.16
+
+Documentation
+- Doc: Add saveInSent reference in FAQ
+
+** DavMail 3.9.4 released **
+First release with full Exchange tasks (VTODO) support, automatically convert VTODOs to tasks
+inside default task folder. Also includes many bugfixes on iCal OSX Snow Leopard support
+(note: need to recreate the calendar to fix sync), mixed Exchange 2010/2003 architecture support and
+IMAP DRAFT and UNDRAFT search condition support
+
+Caldav:
+- Caldav: fix regression
+- Caldav: implement update folder
+- Caldav: fix regression on Snow Leopard
+- Caldav: more general fix for misconfigured Exchange server, replace host name in url also over Caldav
+- Caldav: additional fix for CRLF in urlcompname
+- Caldav: additional task fields over EWS and fix urlcompname decoding
+- Caldav: implement task categories over EWS
+- Caldav: update additional MAPI properties for tasks
+- Caldav: implement startdate and duedate on tasks
+- Caldav: implement task categories
+- Caldav: Need to encode % in urlcompname
+- Caldav: implement task percent complete and status over WebDav
+- Caldav: improve task support over WebDav, rename .ics to .EML and implement priority (importance)
+- Caldav: do not try to get ICS content from tasks
+- Caldav: encode @ in path only for iCal 5 (OSX Lion)
+- Caldav: implement supported-report-set
+
+Enhancements:
+- Do not always log stacktrace in handleNetworkDown
+- Fix IllegalArgumentException in fixClientHost when scheme is null
+- Temporary fix: log exception stack trace in handleNetworkDown
+- Temporary fix: log exception in handleNetworkDown
+- Another init script
+- Small failover fix
+- Improve client host update fix
+- Test various event count
+
+IMAP:
+- IMAP: implement DRAFT and UNDRAFT search conditions, fix 3396248
+- IMAP: fix failover for misconfigured Exchange server, replace host name in url
+- IMAP: fix regression in EwsExchangeSession.createMessage
+- IMAP: Fix 3383832, set ItemClass to send read receipt over EWS to avoid ErrorObjectTypeChanged
+
+EWS:
+- EWS: Allow null value in StringUtil.decodeUrlcompname
+- EWS: use isrecurring with Exchange 2010 and instancetype with Exchange 2007
+- EWS: revert 3317867 XML1.1 header workaround to fix 3385308
+
+DAV:
+- Dav: check checkPublicFolder calls
+- Dav: Avoid returning null in getCmdBasePath
+
+Documentation:
+- Doc: fix OSX iCal setup documentation for OSX Lion
+
+LDAP:
+- LDAP: send error on DIGEST-MD5 bind request
+
+** DavMail 3.9.3 released **
+New release with improved iCal 5 (OSX Lion) support, partial VTODO conversion to Outlook tasks
+ and many other bugfixes including event move item url encoding and event filter over EWS fix
+
+Caldav:
+- Caldav: new fix for @ encoding
+- Caldav: fix regression, do not encode @ in calendar-user-address-set
+- Caldav: fix principal-URL response
+- Caldav: encode @ in current-user-principal
+- Caldav: force @ encode to %40 for iCal 5
+- Caldav: new CRLF in urlcompname patch for EWS, use _x000D__x000A_ as encoded value
+- Caldav: implement merged folder ctag over WebDav
+- Caldav: fix noneMatch handling over WebDav
+- Caldav: implement tasks delete over WebDav
+- Caldav: implement tasks support over WebDav
+- Caldav: send principal-URL for OSX Lion
+- Caldav: first duedate implementation on tasks, fix delete task and concat ctag to detect changes on both calendar and tasks folders
+- Caldav: implement percent complete and status VTODO updates
+- Caldav: implement task support over EWS
+- Caldav: decode destination path on move
+- Caldav: encode LF to %0A in urlcompname
+- Caldav: check Depth before search
+- Caldav: Task folder flag
+- Caldav: move remove quotes for Evolution to EWS mode only
+- Caldav: fix regression on iCal FreeBusy handling
+- Caldav: implement PROPFIND on single item
+- Caldav: remove quotes on etag for Evolution
+- Caldav: first Task (VTODO) implementation step
+- Caldav: allow infinity as Depth value
+
+Enhancements:
+- Fix DoubleDotInputStream
+- Improve system proxies and move item logging
+- Refactor buildSessionInfo to use /public first and mailbox path as failover for galfind requests
+- Fix bug in removeQuotes
+- Fix 3315942, patch cleanup
+- Fix server certificate label
+- Fixes from audit
+- Upgrade SWT to 3.7
+- Fix 3315942, merge patch provided by Jeremiah Albrant: Ask user to select client certificate
+- Improve message download progress logging, switch icon every 100KB
+- Remove unused SwtAwtEventQueue class
+- Implement davmail.smtpSaveInSent option and reorganize tabs
+- Fix 3153691: Username with apostrophe
+- Patch by Manuel Barkhau: exclude private events flag
+- Reformat and fixes from audit
+
+EWS:
+- EWS: new recurring event filter implementation, exclude recurrence exception in results
+- EWS: fix new Exchange 2010 ItemId length support
+- EWS: Fix for some Exchange 2010 ItemIds different length
+- EWS: workaround for Exchange bug, replace xml 1.0 header with xml 1.1 and log message download progress
+- EWS: implement gzip encoding on response
+
+Dav:
+- Dav: update httpClient host after login
+
+IMAP:
+- IMAP: need to include base folder in recursive search, except on root
+- IMAP: Fix 3151800, force UTF-8 in appendEnvelopeHeaderValue
+
+Documentation:
+- Doc: Add davmail.smtpSaveInSent description in doc
+
+
+** DavMail 3.9.2 released **
+This release includes some documentation updates, implement IMAP Recent flag,
+Caldav support enhancements, 64 bits wrapper on windows, hanging issue with SWT
+on Linux 64 and many other bugfixes.
+
+LDAP:
+- LDAP: cancel search threads on connection close
+
+Enhancements:
+- Adjust system proxy log statement
+- Jsmooth patch with 64 bits skeletons
+- Additional statement on proxy load
+- SWT: register error handler early
+- Serialize session creation in workstation mode to avoid multiple OTP requests
+- SWT: register error handler to avoid application crash on concurrent X access from SWT and AWT
+- Revert LookAndFeel changes, switch to System.setProperty to set default LAF
+- SWT: make sure we don't start AWT threads too early
+- Update Jsmooth patch with 64bits exe support
+- SWT: delayed AWT frames creation to reduce memory usage
+- Experimental 64 bits windows exe
+- Add a log file size field in UI
+
+DAV:
+- Dav: back to old path in Destination header behavior
+- DAV: switch icon on large message download
+- Dav: Log message download progress
+- Dav: new patch to reset session timeout with a GET method on /owa/
+- Dav: experimental, try to reset session timeout with a GET method
+- Dav: do not try property update failover on 507 SC_INSUFFICIENT_STORAGE
+
+OSX:
+- OSX: Avoid sending empty message to Growl
+
+IMAP:
+- IMAP: test custom header search
+- IMAP: workaround for Exchange 2003 search deleted support
+- IMAP: fix 3303767, do not send line count for non text bodyparts
+- IMAP: another fix for 3297849, ENVELOPE formating error/bogus quotes
+- IMAP: fix 3297849, ENVELOPE formating error/bogus quotes
+- IMAP: Fix nullpointer in broken message handling
+- IMAP: fix infinite loop detection
+- IMAP: detect infinite loop on the client side
+- IMAP: implement Recent flag on new messages based on read flag and creation/modification date
+- IMAP: fix 3223513 default flags on append
+
+Documentation:
+- Doc: How to run multiple instances of DavMail
+- Doc: FAQ note, iCal does not support folder names with spaces or special characters
+- Doc: Add reference to default windows domain setting in FAQ
+- Doc: additional Thunderbird and DavMail review
+- Doc: add Duplicate messages in Sent folder FAQ entry
+- Doc: add Piwik code to DavMail site
+- Doc: New (french) review
+- Doc: document custom certificate authority handling
+- Doc: improve initial setup documentation
+- Doc: describe the usual paths to use in OWA url field
+- Doc: update imapAutoExpunge flag doc
+- Doc: update roadmap
+- Doc: document public folder access in Lightning
+
+Caldav:
+- Caldav: allow tab as folding character, see RFC2445
+- Caldav: Fix NullPointerException in getTimezoneIdFromExchange
+- Caldav: instancetype is null on Exchange 2010, switch to isrecurring in EWS FindItem
+- Caldav: Disable schedule-inbox for all Lightning versions
+- Caldav: prepare xmoz custom property support over DAV
+
+EWS:
+- EWS: fix UID and RECURRENCE-ID, broken at least on Exchange 2007 with recurring events
+- EWS: fix 3105534 GetUserAvailability default timezone compatibility with Exchange 2010
+- EWS: new failovers on Timezone settings: use davmail.timezoneId setting or default to GMT Standard Time
+
+
+** DavMail 3.9.1 released **
+Another bugfix release, mainly on EWS Caldav support (fix 404 not found).
+Also implemented Microsoft Forefront Unified Access Gateway support.
+
+Documentation:
+- Doc: additional FAQ entry on shared calendars
+
+Caldav:
+- Caldav: first check that email address is valid to avoid InvalidSmtpAddress error on FreeBusy request and new timezone name mapping
+- Caldav: New fix for fix 3190219, regression on quote encoding since 3165749 fix
+- Caldav: rethrow SocketException to avoid event not available on client connection close or DavMail listener restart
+- Caldav: Fix timezone name
+- Caldav: fix 3190219, regression on quote encoding since 3165749 fix
+
+Exchange Web Services:
+- EWS: fix 3190774, LDAP galfind email address handling, use Mailbox value instead of EmailAddress1/2/3
+- EWS: fix NullPointerException in item getContent
+- EWS: fix 404 not found with Exchange 2010 calendars
+
+Enhancements
+- Convert shell script to unix LF
+- Implement Microsoft Forefront Unified Access Gateway logon form compatibility
+
+IMAP:
+- IMAP: fix 3201374 envelope superflous space
+- IMAP: fix LOGOUT implementation to improve SquirrelMail compatibility
+
+OSX:
+- OSX: Avoid sending null message to Growl
+
+
+** DavMail 3.9.0 released **
+Making progress towards 4.0 and full EWS support, some issues remaining on recurring
+events. This is mainly a bugfix release, with some Caldav enhancements, huge memory usage fix
+on IMAP and a workaround for Linux 64 bits futex issue (deadlock on first connection).
+
+POP:
+- POP: test new double dot implementation
+- POP: Fix from Stefan Guggisberg, handle invalid CR or LF line feeds in DoubleDotOutputStream
+
+Caldav:
+- Caldav: additional timezone names in table
+- Caldav: 3132513, implement well-known url, see http://tools.ietf.org/html/draft-daboo-srv-caldav-10
+- Caldav: implement a new setting to disable task (VTODO) support: davmail.caldavDisableTasks and probably exclude most broken events
+- Caldav: throw exception on empty event body (EWS)
+- Caldav: fix multivalued param support in VProperty and always quote CN values
+
+Documentation:
+- Doc: Update release guide
+- Doc: Additional FAQ entry on public calendar access with iCal
+- Doc: Add Manchester wiki review
+
+LDAP:
+- LDAP: dump BER content on error
+- LDAP: fix 3166460, do not fail on NOT (0xa2) filter
+
+Bugfix:
+- New workaround for bug 3168560, load system proxy settings in static block
+- Fix 3161913 klauncher says davmail.desktop misses trailing semicolon
+- Restore stax-api jar for Java 1.5 compatibility
+- Fix 3150426 huge memory usage with IMAP
+- Workaround for bug 3168560, synchronize system proxy access
+- New NTLMv2 patch: provide fake workstation name and adjust Type3 message flags
+
+EWS:
+- EWS: Fix 3165749, exception with quotes in meeting subject and EWS
+
+Webdav:
+- Dav: log search response count
+
+
+** DavMail 3.8.8 released **
+Yet another bugfix release with many EWS support enhancements and fixes,
+many documentation improvements (still need to update Thunderbird/OSX instructions
+to latest versions though).
+
+Documentation:
+- Doc: change Maven site plugin version
+- Doc: additional external links
+- Doc: Add anew reviews page
+- Doc: Update POM and release guide
+- Doc: move advanced settings to a separate page to keep getting started page simple
+- Doc: add SWT bug reference to FAQ, on Ubuntu, notify text conflicts with default theme
+- Doc: full iPhone setup instructions
+- Doc: update war deployment description
+- Doc: Additional smartcard PKCS11 setup instructions with NSS and Coolkey examples
+
+Caldav:
+- Caldav: Additional timezone mappings
+- Caldav: workaround for Exchange 2010 bug, \n in timezone name generates invalid ICS content
+- Caldav: improve timezone rename error message
+- Caldav: fix floating timezone in iCal: rename TZID for maximum iCal/iPhone compatibility
+- Do not send Exchange 2003 appointment creation request to Exchange 2007
+
+EWS:
+- EWS: return HttpNotFoundException on event not found to trigger Lightning workaround
+- EWS: fix instancetype field definition: Integer instead of String (fix Caldav filter over EWS)
+- EWS: improved email/alias failover fix
+- EWS: fix regression in comment
+- EWS: additional failover mail build on logon form failure
+- EWS: store X-MOZ-SEND-INVITATIONS property to fix no notification issue with Lightning
+- EWS: fix Caldav inbox handling over EWS
+- EWS: improve timezone handling
+- EWS: Update Field list
+- EWS: fix 3098008, implement result paging to handle message folders with more than 1000 messages
+- EWS: exclude non message types from searchMessages
+- EWS: fix email mapping on LDAP response
+- EWS: add BusinessCountryOrRegion contact field
+
+Enhancements
+- Additional session create log statement
+- New multiple user fields implementation: expect userid|username as login value
+- Improve connection pool handling: do not pool simple checkConfig and getVersion connections.
+- Implement OTP form with multiple username fields (username and userid)
+- Contribution from Geert Stappers: start/stop script
+- Improve NTLM authentication detection
+- Always use private connection manager to avoid session conflict
+- Fixes from audit
+- Update javamail to 1.4.3
+- Adjust Mime decoder settings (fix)
+- Adjust Mime decoder settings
+- Workaround for space in cookie name
+- Use a_sLgnQS instead of a_sLgn first to support new OWA 8.3.83.4
+- Additional NTLM flags to match Firefox flags
+- Add UTF-7 support with jcharset
+- Failover for misconfigured Exchange server, replace host name in url
+
+SMTP:
+- SMTP: fix 3132569, always remove From header to avoid 403 error on send
+- SMTP: workaround for misconfigured Exchange servers: failover on Draft message creation through properties. Warning: attachments are lost
+
+IMAP:
+- IMAP: Fix 3137275 Imap header fetch bug
+
+WebDav:
+- Dav: make sure Destination contains full url and not only path, may fix SMTP send and IMAP copyMessage on Exchange 2003
+
+Carddav
+- Carddav: Update contact test
+
+** DavMail 3.8.7 released **
+Bugfix and performance release with new Woodstox parser to reduce memory
+footprint in EWS mode, more Caldav broken events fixes and IMAP regression
+fixes.
+
+Documentation:
+- Doc: Update Carddav setup doc
+- Doc: ssl setup doc update from kerstkonijn
+
+Enhancements
+- Unzip contribs content
+- Update rpm ant task parameters to create valid rpm package
+- Workaround for malformed cookies with space in name
+- From Geert Stappers: add includeantruntime="false" to avoid ant 1.8 warning
+- Workaround for invalid redirect location
+- Improve error handling: detect redirect to reason=0 as session expired
+- Suggestion from Geert Stappers: add svn:ignore property
+- RPM spec from Marko Myllynen
+
+Caldav:
+- Caldav: Fix timezone support with Exchange 2010 SP1
+- Caldav: use rebuild event from MAPI properties failover in all error cases
+- Caldav: add requestFocus() to bring notification dialog to foreground
+- Caldav: added edit notifications checkbox in settings frame
+
+IMAP:
+- IMAP: include current folder in recursive search
+- IMAP: encode source path in copyMessage
+- IMAP: new test case to show Thunderbird perf issue
+- IMAP: Fix 3109303 Handle null string during mail fetch
+- IMAP: fix nullpointerException in header fetch
+- IMAP: fix 3106803, IMAP client stuck scanning Inbox, fix header and body fetch in same request
+- IMAP: throw error on 440 Login Timeout to avoid message corruption
+
+LDAP:
+- LDAP: do not log error on OSX groups request
+
+EWS:
+- EWS: Upgrade woodstox version to use enhanced base64 conversion (reduced memory usage)
+- EWS: allow autodiscover after authentication failure
+- EWS: fix contact email update
+
+OSX:
+- OSX: search and replace on existing file, spotted by Geert Stappers
+
+** DavMail 3.8.6 released **
+First release with automatic EWS mode detection, also includes many bugfixes
+on LDAP support over EWS, IMAP enhancements, Exchange 2010 SP1 cookie bug workaround
+and a brand new UI frame to let users edit Caldav notifications.
+
+LDAP:
+- LDAP: fix galfind search: add uid in response and use cn in fullsearch filter
+- LDAP: additional EWS attributes
+- LDAP: additional attributes for iPad
+
+Enhancements:
+- Fix 3103349: Cannot login if display name contains [brackets], regression after first patch
+- Fix 3103349: Cannot login if display name contains [brackets]
+- configFilePath is null in some test cases
+- Added passcode as token field for RSA support
+- Add DavMail version in welcome IMAP and SMTP header
+- Update test case
+- Handle exceptions on invalid UTF-8 characters or unexpected content triggered by XmlStreamReader.getElementText (based on patch 3081264)
+- Add exchange 2010 PBack cookie in compatibility mode
+- Novell iChain workaround
+
+POP:
+- POP: add version in welcome banner
+
+Caldav:
+- Caldav: Fix bug in Dav mode with broken events dtstart -> dtend
+- Caldav: fix french notification message
+- Caldav: protect ':' in VCALENDAR property params
+- Caldav: initial edit notification implementation
+- Caldav: Create fake DTEND on broken event
+- Caldav: fix nullpointer in VCalendar on missing DTEND
+- Caldav: implement main calendar folder rename
+- Caldav: use i18n calendar name as display name for iCal
+- Caldav: avoid renaming default calendar to null
+
+EWS:
+- Ews: improve ResolveNames implementation, parse addresses and phone attributes
+- EWS: implement failover on OWA authentication failure (e.g. with outlook.com)
+- Ews: improve invalid item in calendar error handling
+- EWS: improve resolveNames logging
+- EWS: add enableEws flag in UI settings frame
+- EWS: automatically detect Webdav not available and set davmail.enableEws flag
+
+IMAP:
+- IMAP: failover in message copy on 404 not found
+- IMAP: Fix append with no optional parameters
+- IMAP: additional test cases
+- IMAP: fix from kolos_dm: implement fake line count in BODYSTRUCTURE and [] block in IMAPTokenizer
+- IMAP: fix from kolos_dm: implement attachment name in BODYSTRUCTURE
+- IMAP: improve logging, do not log message content on 404 or 403
+- IMAP: fix from kolos_dm: In-Reply-To is not email header and unfold header to remove CRLF in ENVELOPE response
+- IMAP: merge fix from Kolos, search command with message sequence set
+- IMAP: implement index (non uid) COPY
+- IMAP: workaround for broken message (500 error), rebuild mime message from properties
+- IMAP: send error on idle command without selected folder (Outlook)
+
+Documentation:
+- Doc: fixes and updates on ssl setup and build
+- Doc: update roadmap
+- Doc: Update architecture image
+- Doc: update ssl server certificate doc
+- Doc: Document PKCS12 self signed certificate creation to enable SSL in DavMail
+- Doc: iPhone screenshots
+
+SWT:
+- SWT: Custom AWT event queue to trap X errors and avoid application crash
+- SWT:enable debug mode
+
+** DavMail 3.8.5 released **
+Includes much progress on Caldav over EWS support, a few regression fixes
+and improved IMAP BODYSTRUCTURE implementation for complex messages.
+
+Bugfixes:
+- Fix regression in Exchange 2007 over Dav session
+
+Enhancements:
+- Detect and submit language selection automatically
+- More fixes from audit
+- Fixes from audit
+- Restore cookies on error
+- Improve buildSessionInfo failover
+- Fix ssl trustmanager error handling
+- Enable Webdav/Galfind failover on Exchange 2007
+- Workaround for basic authentication on /exchange and form based authentication at /owa
+
+Caldav:
+- Caldav: detect invalid events with empty dtstart property
+- Caldav: implement mozilla alarm flags X-MOZ-LASTACK and X-MOZ-SNOOZE-TIME over EWS
+- Caldav: EWS, rebuild attendee list from properties
+- Caldav: test principal request
+- Caldav: fix 3067915 getRangeCondition too restrictive
+- Caldav: implememnt Busy flag over EWS and refactor create code
+- Caldav: fix create allday event over EWS and check if current user is organizer
+- Caldav: Fixed regression in allday event handling
+- Caldav: improve EWS implementation
+- Caldav: improve timezone error handling
+- Caldav: remove empty properties
+- Caldav: avoid invalid X-CALENDARSERVER-ACCESS and CLASS
+- Caldav: avoid empty X-CALENDARSERVER-ACCESS and CLASS
+- Caldav: reinsert the deleteBroken check
+- Caldav: fix VProperty parser
+- Caldav: additional VCalendar properties for rebuilt item: VALARM (reminder)
+- Caldav: additional VCalendar properties for rebuilt item: RRULE, EXDATE, CLASS
+- Caldav: failover for broken event, rebuild VCalendar content from raw properties
+- Caldav: fix 3063407, regression in sendPrincipal
+
+Carddav:
+- Carddav: fix null value in email address
+- Carddav: fix email address handling over EWS
+
+Exchange Web Services:
+- EWS: fix 3047563 double inbox
+- EWS: more caldav ews fixes
+
+SMTP:
+- SMTP: rewrite getAllRecipients to disable strict header check
+- SMTP: new try at encoding fix: set mailOverrideFormat and messageFormat
+
+Documentation:
+- Upgrade maven site-plugin and update release guide
+
+IMAP:
+- IMAP: fix 3072497 Imap server too picky about case
+- IMAP: improve BODYSTRUCTURE implementation, make it recursive
+- IMAP: implement partial header fetch
+
+LDAP:
+- LDAP: new attribute mapping
+- LDAP: cache current hostname value in sendComputerContext to improve iCal address completion performance
+- LDAP: additional ignore attributes
+- LDAP: add gidnumber to attribute ignore list
+- LDAP: fix regression on iCal 3 search completion
+
+SWT:
+- SWT: allow libswt-gtk-3.6-java on debian, available from ppa:aelmahmoudy/ppa
+
+** DavMail 3.8.4 released **
+Yet another bugfix release with more regressions fixes on SMTP,
+a few LDAP fixes and a caldav timezone update.
+
+Documentation:
+- Doc: Update release guide
+- Doc: Update swt version in maven pom
+
+SMTP:
+- SMTP: try to force IMS encoding mode according to message contenttype
+- SMTP: switching back to Draft then send mode over DAV for calendar messages
+- SMTP: switching back to Draft then send mode over DAV
+- SMTP: new duplicate message-id detection implementation, no need to search Sent folder
+
+LDAP:
+- LDAP: improve EWS filter support
+- LDAP: another gallookup detection fix to improve address completion in thunderbird
+
+Carddav:
+- Carddav: improve OSX client detection
+
+Enhancements
+- Fixes from audit
+
+Caldav:
+- Caldav: accept login as alias in caldav principals path
+- Caldav: basic move item implementation
+- Caldav: adjust Lightning bug workaround
+- Caldav: yet another timezone fix, adjust Outlook created event time before allday conversion
+- Caldav: fix regression on meeting response subject
+
+** DavMail 3.8.3 released **
+Another bugfix release with major regressions fixed:
+missing calendar meeting messages and delivery status notification on
+some external addresses. Also includes improved autodiscover support.
+
+Enhancements:
+- Disable broken rpm generation
+- Fix test cases
+- Upgrade swt to 3.6
+- workaround for TLS Renegotiation issue,
+ see http://java.sun.com/javase/javaseforbusiness/docs/TLSReadme.html
+- Switch back to StreamReader.next instead of nextTag
+- Fix autodiscover support
+- Merge patch 3053324: Implement per service SSL flag (patch provided by scairt)
+- Fix XMLStreamUtil regression
+- Refactor XMLStreamUtil
+
+Exchange Web Services:
+- EWS: improve autodiscover implementation
+- EWS: fix possible NullPonterException
+- EWS: implement autodiscover to find actual EWS endpoint url
+
+Caldav:
+- Caldav: extend Lightning broken tests to all 1.* versions
+- Caldav: switch back to contentclass to get calendarmessages over webdav
+- Caldav : revert previous changes and fix meeting cancel support (IPM.Schedule.Meeting.Canceled)
+- Caldav: move to trash on processItem
+- Caldav: fix request parser regression on nextTag
+- Caldav: improve filter handling, support VTODO/VEVENT comp-filter
+- Caldav: make timezone name retrieval more robust
+
+SMTP:
+- SMTP: make duplicates check optional with davmail.smtpCheckDuplicates setting
+- SMTP: always remove From header with Exchange 2007 and 2010
+- SMTP: Improve message on MAIL FROM without authentication
+- SMTP: experimental, advertise 8BITMIME
+
+IMAP:
+- IMAP: implement shared mailbox access
+
+Documentation:
+- minor doc fix
+- Doc: Additional Exchange Webdav setup documentation
+- Add ohloh widget on home page
+- Doc: a few doc fixes and update roadmap
+
+** DavMail 3.8.2 released **
+Bugfix release with improved Exchange 2010 IMAP support, CardDav fixes and
+improved error handling
+
+Enhancements:
+- Disable SWT on Java 7
+- Update debian package description and categories
+- fix 2995990: Add support for already authenticated users
+- Fix missing hide password in log over IMAP
+- More session creation enhancements, fix public folder test when /public is 403
+- Refactor email and alias retrieval: always use options page with Exchange 2007
+- Improve socket closed error handling
+- Try default form url on authentication form not found
+- Add Java Service Wrapper contribution from Dustin Hawkins
+
+Caldav:
+- Caldav: move delete broken event logic to DavExchangeSession
+- Caldav: delete broken events when davmail.deleteBroken is true
+- Caldav: improve event logging, include subject
+
+IMAP:
+- IMAP: handle 507 InsufficientStorage error
+- IMAP: fix regression in NOT DELETED filter
+
+Documentation:
+- Doc: Update OSX directory setup documentation
+
+DAV:
+- DAV: Encode apos in urlcompname used in DAV search request
+
+EWS:
+- EWS: fix single message in folder with Exchange 2010 bug
+- EWS: implement loadVTimezone for Exchange 2010
+
+SMTP:
+- SMTP: fix regression on bcc handling
+- SMTP: convert Resent- headers, see 3019708
+
+LDAP:
+- LDAP: avoid galLookup in iCal searches
+
+Carddav:
+- Carddav: Fix email update over EWS
+
+** DavMail 3.8.1 released **
+Includes a full refactoring of Vcalendar content handling, much progress on
+Exchange Web Services support, LDAP optimizations and many other bufixes.
+
+Exchange Web Services:
+- EWS: hard method: delete/create on update
+- EWS: Fix DeleteItem for CalendarItem
+- EWS: implement loadVtimezone, get user timezone id from OWA settings
+- EWS: Fix FieldURIOrConstant test
+- EWS: separate domain from userName in NTLM mode
+- EWS: MultiCondition galFind
+- EWS: implement basic galFind search
+- EWS: implement resolvenames response parsing
+- EWS: fix subfolder search on Exchange 2010
+- EWS: implement user availability (freebusy) and shared folder access
+- EWS: implement sendEvent
+- EWS: force urlcompname only on create
+- EWS: implement ResolveNames method
+- EWS: Apply workaround to events
+- EWS: workaround for missing urlcompname on Exchange 2010, use encoded ItemId instead
+- EWS: rename equals to isEqualTo and format search date
+- EWS: dynamic version detection
+- EWS: Exchange 2010 message handling
+- EWS: Exchange 2010 folder handling
+- EWS: Exchange 2010 compatibility: add test cookie, access /ews/exchange.asmx endpoint
+
+Caldav:
+- Caldav: Fix missing TZID in DTSTART from iPhone
+- Caldav: return reoccuring events on time-range request
+- Caldav: Fix METHOD on create from iPhone
+- Caldav: need to encode colon (:) in urlcompname search, implement a last failover on item search
+- Caldav: implement 2899430, change the subject line when replying to invites
+- Caldav: workaround for Lightning 1.0b2 bug
+- Caldav: disable caldav inbox with Lightning 1.0b2
+- Caldav: fix regression in fixVCalendar (missing organizer)
+- Caldav: skip empty lines
+- Caldav: Fix regressions in Vcalendar handling
+- Caldav: fix nullpointer in VCalendar
+- Caldav: fix regressions and do not filter on outlookmessageclass
+- Caldav: major refactoring of event content handling and notifications
+- Caldav: switch to new VCalendar parser/patcher
+- Caldav: implement VALARM in VCalendar
+- Caldav: more vcalendar patches
+- Caldav: start new VCalendar fixICS implementation
+- Caldav: call fixICS on download
+- Caldav: reenable Lightning 1.0b2 bug workaround
+- Caldav: failover for 404 not found on items containing '+' in url, search item by urlcompname to get permanenturl
+
+LDAP:
+- LDAP: create a separate thread only for person/contact searches
+- LDAP: implement galFind MultiCondition over webdav and improve search by mail
+- LDAP: need to galLookup when search attribute is not in galfind result
+- LDAP: another search attribute mapping fix
+- LDAP: code cleanup and some galfind search fixes
+- LDAP: fix 3043659, include entries starting with Z
+- LDAP: Improve sizeLimit handling and ignore attributes
+- LDAP: a few more attribute fixes
+- LDAP: move galLookup to DavExchangeSession
+- LDAP: progress on EWS LDAP implementation and refactoring
+- LDAP: fix regression on OSX directory request on iCal start: filter invalid imapUid condition
+- LDAP: use sizeLimit in contactFind
+- LDAP: Fix OSX directory search on uid
+
+Enhancements:
+- Improve error handling
+- Add custom cookie policy to support extended host name
+- Fixes from audit
+
+Bugfixes:
+- Fix regression in getAliasFromMailboxDisplayName
+- Deb: Fix regression in debian desktop link
+
+Dav:
+- Dav: disable galFind on error
+
+SMTP:
+- SMTP: compare actual email address, not email with alias
+- SMTP: no need to remove From header with new sendMessage implementation
+
+SWT:
+- SWT: fix 2992428, hide instead of dispose on close
+
+Carddav:
+- Carddav: refactor VCard handling to merge with VCalendar code
+- Carddav: disable contact picture handling on Exchange 2007
+- Carddav: implement range search
+
+** DavMail 3.8.0b2 released **
+Fixes the most obvious regressions in 3.8.0b1 and some documentation
+updates on Carddav. Note for EWS only users: add davmail.enableEws=true in
+davmail.properties
+
+Caldav: fix sendEvent regression, conflict on outbox notifications
+Caldav: improve HttpNotFound message
+Caldav: Refactor getItem
+Doc: fix carddav thunderbird doc
+Doc: update left menu
+Doc: set source encoding to UTF-8 in maven pom
+Doc: update roadmap
+Doc: Basic OSX setup instructions
+Doc: thunderbird carddav setup with SOGO connector
+Doc: Update homepage and project description
+Carddav: additional TEL properties
+Caldav: fix MKCALENDAR http status code: return 201 instead of 207
+Carddav: add fburl field
+Caldav: Another request parsing bug: handle empty elements
+Caldav: fix regression in REPORT requests parsing
+
+** DavMail 3.8.0b1 released **
+First public release after major refactoring to implement Exchange 2010 and Exchange 2007 without
+Webdav support. This implementation is based on Exchange Web Services. EWS support is not yet
+complete: global address list search and free/busy support is missing.
+This release includes the new Carddav service sponsored by French Defense / DGA through
+project Trustedbird. OSX notifications will now use Growl if available.
+
+Carddav:
+- Carddav: another urlcompname encoding fix
+- Carddav: generate OSX compatible VCARD photo and change addressbook-home-set with OSX Address Book
+- Carddav: use new ExchangePropPatchMethod in full contact create/update
+- CardDav: use new ExchangePropPatchMethod to create haspicture boolean property
+- Carddav: improve error logging on photo update failure
+- Carddav: use email1 as default email on update
+- Carddav: fix multiple mail MAPI properties handling
+- Carddav: fix GET request on folder support for SOGO
+- Carddav: encode contact picture url
+- Carddav: return 404 not found on missing folder
+- Carddav: fix line folding in generated VCARD
+- Carddav: Fix regression in single value multiline properties
+- Carddav: add gender property
+- Carddav: adjust bday to timezone
+- Carddav: another anniversary property candidate
+- Carddav: Add Anniversary support
+- Carddav: Fix bday generation
+- Carddav: fix iPhone BDAY parser
+- Carddav: adjust fields accepting multiple values
+- Carddav: fix semicolon encoding in compound value
+- Carddav: workaround for iPhone categories encoding
+- CardDav: do not encode simple (not compound) properties
+- Carddav: fix regression in VCardWriter
+- Carddav: always encode values
+- Carddav: protect semicolon
+- Carddav: iPhone personalHomePage support
+- Carddav: ignore key prefix in VCARD
+- Carddav: resize contact picture
+- Carddav: Fix lower case param names
+- Carddav: add contact create or update log statement
+- Carddav: handle param values as parameter list
+- Carddav: encode photo href
+- Carddav: fix regression on VCARD photo detection
+- Carddav: use urlcompname value instead of path to get contact details
+- Carddav: fix case insensitive param values
+- Carddav: add haspicture to test case
+- Carddav: Implement picture delete and private flag over EWS
+- Carddav: handle picture delete
+- Carddav: fix boolean field handling
+- Carddav: Remove missing properties on update
+- Carddav: implement CLASS (private) flag
+- Carddav: convert image to jpeg over EWS
+- Carddav: implement photo update over WebDav
+- Carddav: implement photo handling over EWS
+- Carddav: implement categories support in EWS mode
+- Carddav: implement categories
+- Carddav: get SMTP email address
+- Carddav: move value decoding back to VCardReader
+- Carddav: decode multiline values
+- Carddav: encode comma and \n in values
+- CardDav: make getContactPhoto more robust
+- Carddav: iPhone iOS4 compatibility
+- Carddav: implement contact photo support (readonly)
+- Carddav: implement quoted param value support
+- Carddav: bday, assistant, manager and spouse properties
+- Carddav: other address and homeposteofficebox properties
+- Carddav: instant messaging and role properties
+- Carddav: more properties
+- Carddav: Implement phone, address and email properties
+- Carddav: handle multiple values on a single line and add new properties
+- CardDav: fix contact folder path handling and add create contact unit test
+- Carddav: refactor Contact creation and create VCardReader
+- CardDav: move Contact getBody to ExchangeSession and add more attributes support
+- CardDav: map contact fields
+- CardDav: improve automatic address book setup for OSX
+- CardDav: implement OSX AddressBook requests: current-user-privilege-set property, current-user-principal on root request, addressbook-home-set on principal request, addressbook-multiget REPORT request with address-data response, urn:ietf:params:xml:ns:carddav namespace
+
+Enhancement:
+- Disable preemptive authentication when adding NTLM scheme
+- Fixes from audit
+- Force log file encoding to UTF-8
+- Add new davmail.logFileSize setting
+- Use linux friendly path separator in jsmooth config files
+- Fixes from audit
+- Major refactoring: use straight inpustream instead of reader everywhere
+- Disable ConsoleAppender in gui mode
+- Add missing Junit jar
+- Cleanup: System.setProperty of httpclient.useragent no longer needed
+- Improve item not found logging
+- Log gateway stop at info level
+- Improve empty keystore password handling to avoid NullPointerException
+- Fix 2999717 redirect console to /dev/null in desktop file
+
+Exchange Web Services:
+- EWS: fix urlcompname encoding issues
+- EWS: fix folder name ampersand encoding issue
+- EWS: return 403 forbidden on ErrorAccessDenied
+- EWS: xml encode values
+- EWS: use UTF-8 to decode request on error
+- EWS: send extended properties first on update
+- EWS: format datereceived date
+- EWS: fix bug in UnindexedFieldURI
+- EWS: update createMessage bcc handling to match sendMessage
+- EWS: implement bcc support in sendMessage
+- EWS: implement send message (SMTP)
+- EWS: fixes from audit
+- EWS: fix CalendarItem creation, no need to wrap ics in a MIME message
+- EWS: implement calendar event create or update, processed field, subfolder path handling
+- EWS: fix internaldate conversion
+- EWS: convert read flag to boolean and noneMatch/etag to detect create or update on items
+- EWS: use UnindexedFieldURI for read flag
+- EWS: fixes for Caldav and Carddav compatibility
+- EWS: fix folder id regression
+- EWS: fix country contact property mapping
+- EWS: implement getItem and various contact handling fixes
+- EWS: map all contact properties
+- EWS: implement more contact and event methods
+- EWS: implement copy method
+- EWS: datereceived flag support
+- EWS: handle bcc field
+- EWS: various flag handling fixes, implement message delete
+- EWS: implement getContent
+- EWS: fix iconIndex flag property
+- EWS: implement create and update message
+- EWS: fix single value in MultiCondition handling
+- EWS: rely on uid (PR_SEARCH_KEY) instead of permanentUrl to detect imap uid changes
+- EWS: implement searchMessages
+- EWS: fix bug in MultiCondition search
+- EWS: fix from audit
+- EWS: implement folder handling, including the new MoveFolderMethod
+- EWS: move mailbox folder urls to DavExchangeSession
+- EWS: use searchContacts in contactFind
+- EWS: fix regression in deleted flag handling
+- EWS: refactor contactFind, use new Condition API
+- EWS: still more WebDav code to DavExchangeSession
+- EWS: move more WebDav code to DavExchangeSession
+- EWS: Various fixes after refactoring on DASL request generation
+- EWS: in progress refactoring of contacts and events handling
+- EWS: implement folder ctag, remove deprecated foldername property
+- EWS: move WebDav message write and delete to DavExchangeSession
+- EWS: move WebDav code to DavExchangeSession
+- EWS: refactor IMAP search, use Conditions classes instead of string search filder
+- EWS: Use int values to create ExtendedFieldURI propertyTags
+- EWS: map folder path to and from IMAP
+- EWS: implement NotCondition and public folder access
+- EWS: implement IndexedFieldURI and InternetMessageHeader
+- EWS: refactor search to use classes instead of String filters
+- EWS: implement MultipleOperandBooleanExpression (And, Or, Not conditions)
+- EWS: refactor folder search, create abstract getFolder methods
+- EWS: start ExchangeSession refactoring to extract Dav calls
+- EWS: refactor options, use enums
+- EWS: implement basic SearchExpression restriction
+- EWS: Implement CreateFolder, DeleteFolder and CreateItem, refactor options
+- EWS: retrieve and decode MIME content
+- EWS: add standard field additional property, implement IncludeMimeContent in GetItem, add DeleteItemMethod
+- EWS: Generic item property mapping
+- EWS: refactor EWS code
+- EWS: experimental HttpClient based EWS methods
+
+Caldav:
+- Caldav: fix time-range filter support in EWS mode
+- Caldav: move calendar on displayname update
+- Caldav: partial MKCALENDAR implementation
+- Caldav: implement time-range request
+- Caldav: add missing dtstart field
+- Caldav: improve 404 error handling
+- Caldav: fix regression in processItem
+- Caldav: UTF-8 encode report body
+- Caldav: catch any exception in reportItems
+- Caldav: Process request before sending response to avoid sending headers twice on error
+- Caldav: Workaround for Lightning/1.0b2 href encoding bug in REPORT requests
+- Caldav: move processItem logic back to CaldavConnection
+- Caldav: Workaround for emClient broken href encoding
+- Caldav: remove buildCalendarPath method
+- Caldav: allows mixed case contentType in event MIME message (fix Unable to get event error)
+- Caldav: fix 3014204 missing timezone
+- Caldav: fix 2902372 private flag handling undex iCal 4 (OSX 10.6 Snow Leopard)
+- Caldav: send current-user-principal on principals folder for iCal
+- Caldav: workaround for iCal bug: do not notify if reply explicitly not requested
+- Caldav: add CRLF after END:VCALENDAR to comply with RFC
+- Caldav: fix regression in getItem, allow urn:content-classes:calendarmessage contentClass
+- Caldav: Fix Carddav etag handling (additional Head request) and implement card delete
+- Caldav: Implement Carddav create (only a few attributes mapped)
+- Caldav: Implement basic Carddav search requests
+
+Dav:
+- Dav: more property update fixes
+- Dav: patch filter on invalid Exchange Webdav response
+- Dav: new ExchangePropPatchMethod to handle custom exchange propertyupdate and invalid response tag names
+- Dav: refactor getContentReader and fix regression on null date value
+- Dav: fix nullpointer in DavExchangeSession
+- Dav: handle null properties with new createMessage
+- Dav: another datereceived fix
+- Dav: switch back to DAV:uid, used mainly in POP service (case sensitive)
+- Dav: fix bug 3022451 in new search filter implementation with empty sub conditions
+- Dav: Add folder unit tests
+- Dav: add private and sensitivity fields
+- Dav: implement timezone mapping for Exchange 2007, should fix the allday issue with Outlook
+- Dav: use search expression to request ishidden
+- Dav: fix regression in deleteItem
+- Dav: fix regression 3020385 on folder handling
+- Dav: Refactor folder search to use searchItems
+- Dav: use Email1EmailAddress mapi property to get mail attribute, add uid attribute
+- Dav: fix from audit
+- Dav: add unit tests, move buildCalendarPath logic to getFolderPath
+
+Bug fixes:
+- Use private MultiThreadedHttpConnectionManager with NTLM to avoid persistent authentication on connection issues
+- Fix regression in AbstractConnection: return null instead of empty string on closed connection
+- Fix 3001579: improve NTLM support
+
+IMAP:
+- IMAP: add uidNext MAPI property (not available under Exchange 2003)
+- IMAP: fix deleted flag handling over Webdav
+- IMAP: fix flag handling in createMessage
+- IMAP: new seen flag test case
+- IMAP: fix regression on imap uid restore
+- IMAP: fix 3023386, support BODY.PEEK[1.MIME] partial fetch
+- IMAP: new unit tests and fix $Forwarded flag removal
+- IMAP: implement deleted/undeleted search as condition instead of post filter
+- IMAP: add IMAP unit test
+- IMAP: fix 3014787 remove property over WebDav
+- IMAP: implement last message (simple *) fetch range
+- IMAP: send required "* SEARCH" on empty search response
+- IMAP: Add a new hidden davmail.deleteBroken setting to delete broken messages
+- IMAP: implement a new imapAutoExpunge setting to delete messages immediately over IMAP
+
+SMTP:
+- SMTP: send message directly without creating a Draft message to preserve Message-id
+- SMTP: fix log message
+- SMTP: fix 3024482, avoid duplicate messages with gmail
+- SMTP: Fix DoubleDotInputStream pushback size
+- SMTP: last CRLF is not included in message content
+
+Documentation:
+- Doc: javadoc and code cleanup
+- Doc: fix default domain label
+- Doc: new FAQ entry on OSX auto start "Login Items"
+- Doc: typos fixes from Raphael Fairise
+- Doc: update release guide
+- Doc: add a new mail.strictly_mime FAQ entry to enable quoted-printable
+
+POP:
+- POP: fix regression in TOP command
+- POP: fix message termination, append CRLF only when necessary
+- POP: replace deprecated write method, use DoubleDotOutputStream instead
+- POP: allow space in username
+
+LDAP:
+- LDAP: fix contact attributes reverse mapping
+- LDAP: improve contact attribute mapping and add a few new properties
+- LDAP: fix attribute map
+- LDAP: fix regression after EWS refactoring
+- LDAP: use imap uid as ldap uid
+- LDAP: use PR_SEARCH_KEY instead of DAV:uid as uid string
+
+OSX:
+- Exclude growl from non OSX packages
+- Fix growl build project name
+- OSX: implement growl support
+- include jnilib in OSX package
+- libgrowl-0.2 with libgrowl.jnilib compiled on OSX Snow Leopard
+- set libgrowl version to 0.2
+- rename generated jar with version, exclude test classes and create Manifest with Michael Stringer author
+- Improve Growl exception handling, remove System.out and a few fixes from audit
+- Initial growl import from http://forums.cocoaforge.com/viewtopic.php?f=6&t=17320
+
+** DavMail 3.6.6 released **
+This release is mainly focused on IMAP enhancements, including IDLE (RFC2177)
+aka "Push Mail" support and other protocol compliance fixes, particularly on
+partial fetch. NTLMv2 is also supported thanks to the JCIFS library.
+
+Documentation:
+- Doc: update doc and roadmap
+- Doc: adjust settings message
+- Doc: improve server/client certificates description
+- Doc: new FAQ entry on message deleted over IMAP still visible through OWA
+- Doc: fix maven site generation
+
+IMAP:
+- IMAP: send BAD instead of BYE on exception
+- IMAP: fix 2992976, implement complex index and uid range in SEARCH
+- IMAP: Handle exception during IDLE
+- IMAP: add a new setting to enable/disable IDLE
+- IMAP: use getRawInputStream instead of writeTo to avoid MIME message changes, cache message body in SharedByteArrayInputStream
+- IMAP: poll folder every 30 seconds in IDLE mode, clear cached message
+- IMAP: implement IDLE extension (RFC2177)
+- IMAP: fix 2971184, do not decode content in partial fetch (replace getDataHandler with PartOutputStream)
+
+Enhancements:
+- Exclude redline lib from distribution packages
+- Use https in default Exchange url
+- Make sure log messages are not localized
+- Remove unused messageId field
+- Do not shutdown connection manager on restart
+- Allow Exchange server to use gzip compression
+- Sample SocketAppender configuration
+- Improve NTLM mode detection
+- JCIFS based NTLMv2 implementation
+- Hardcode /owa/ path in getAliasFromOptions and getEmailFromOptions for Exchange 2007, improve failure message
+- Improve xmlEncode, use compiled static patterns
+
+Caldav:
+- Caldav: fix 2992811, missing timezones
+- Caldav: fix 2991030 tasks disappeared
+- Caldav: add VTODO to supported-calendar-component-set response
+- Caldav: fix regression in getAllDayLine()
+- Caldav: make shared calendar test case insensitive
+- Caldav: 0 or no value in caldavPastDelay means no limit
+
+** DavMail 3.6.5 released **
+This release includes a major refactoring of the IMAP FETCH implementation
+to improve performance and provide RFC compliant partial fetch. The Carddav
+support sponsored by french DGA through project TrustedBird is now included
+in the roadmap. Private events filter on shared calendar is also available
+and DavMail can now retrieve proxy settings directly from system configuration.
+
+SMTP:
+- SMTP: implement AUTH LOGIN username (with optional initial-response, see RFC2554)
+
+IMAP:
+- IMAP: Keep a single message in MessageList cache to handle chunked fetch, reenable maxSize in ImapConnection.
+- IMAP: implement subparts partial fetch
+- IMAP: Fix message write, double dot only for POP, not IMAP
+- IMAP: Do not advertise not yet supported custom flags
+- IMAP: fix from audit
+- IMAP: major FETCH implementation refactoring, make code simpler and more efficient
+- IMAP: add BODY.PEEK[index] support
+- IMAP: improve partial fetch support
+- IMAP: fix 2962071, quote folder names in STATUS response
+- IMAP: allow partial part fetch
+- IMAP: fix regression on unknown parameter handling
+- IMAP: implement part fetch (BODY[1])
+- IMAP: detect unsupported parameter
+- IMAP: fix 2973213, escape quotes in subject
+- IMAP: fixes to improve JavaMail support
+
+Doc:
+- Doc: move CardDav reference before architecture schema
+- Doc: update project description in Maven pom and ant package
+- Doc: update project description and RoadMap, announce CardDav support sponsored by french DGA through project Trustedbird
+- Doc: update roadmap
+
+Enhancements:
+- Add a new setting to disable startup notification window (contribution from jsquyres)
+- Improve getAliasFromOptions to retrieve alias with custom dn
+- Workaround for NTLM authentication only on /public
+- Add a new setting to retrieve proxies from system configuration
+- Fix empty setting behavior: return null instead of empty string
+- Sort properties file
+- Fix new RPM ant task definition
+- Improve public folder url check
+- Experimental rpm package build
+
+Carddav:
+- Carddav: refactor folder handling code to prepart CardDav support
+
+Caldav:
+- Caldav: fix broken inbox, missing instancetype in search request and add is null in search query
+- Caldav: do not try to access inbox on shared calendar (to avoid 440 login timeout errors and session reset)
+- Caldav: exclude private events on shared or public calendar PROPFIND
+- Caldav: fix regression on invalid events handling, just warn on broken events
+- Caldav: drop timezone when converting allday events to client
+
+** DavMail 3.6.4 released **
+Well, yet another bugfix release, with improved IMAP support,
+SMTP enhancements to support Eudora, NTLM proxy authentication
+support and other bug fixes
+
+SMTP:
+- SMTP: fix 2953552, allow RSET in AUTHENTICATED state
+- SMTP: bug id 2953554, implement NOOP
+
+LDAP:
+- LDAP: Enable tray icon on LDAP connection
+
+Bug fixes:
+- Fix regression in 3.6.3: basic authentication broken in checkConfig
+- GUI: Fix client certificate setting switch
+
+Enhancements:
+- Change debian package dependence to accept openjdk-6-jre and libswt-gtk-3.5-java
+- Fix from audit
+- Improve log message on HTTP header error
+- Implement NTLM HTTP proxy support
+- Improve logging of expired sessions
+- Support multiple forms in form based authentication logon page
+- Catch error on SWT exit
+- Enable NTLM on Proxy-Authenticate return code with only NTLM available
+
+Documentation:
+- Doc: Document davmailservice.exe usage
+- Doc: Document Force ActiveSync setting in Getting Started
+- Doc: Add an FAQ entry on DavMail settings location
+- Doc: Update release notes and guide
+
+IMAP:
+- IMAP: new patch from Gellule to fix disappearing messages issue
+- IMAP: rethrow SocketException after error in handleFetch
+
+Caldav:
+- Caldav: new fix for invalid events
+- Caldav: add a hidden davmail.caldavDisableInbox to allow users to disable Caldav Inbox with Thunderbird 3 and Lightning
+- Caldav: improve broken events logging
+- Caldav: Follow redirects on GET with permanentUrl
+
+
+** DavMail 3.6.3 released **
+Another bugfix release, mostly documentation updates, some regressions
+in 3.6.2 in error handling fixed, a new IMAP workaround to completely
+hide the uid change issue, emacs IMAP support and new UI settings for
+previously hidden parameters.
+
+Bug fix:
+- Fix logging settings handling in webapp mode
+
+Enhancements:
+- Improve error handling: detect SocketException to avoid client socket closed errors
+- Implement file based (PKCS12 and JKS) client certificates in addition to smartcard support
+
+Documentation:
+- Doc: update roadmap
+- Doc: remove replace token and search page
+- Doc: added Gellule as Java Contributor
+- Doc: add a security section in the FAQ
+- Doc: update FAQ with Exchange prerequisites details
+- Document client keystore file settings
+
+IMAP:
+- IMAP: brand new IMAP uid workaround and refresh folder on Expunge from Gellule
+- IMAP: implement LIST "" "*%" for emacs
+- IMAP: another fix for the message uid bump issue
+- IMAP: fix 2934922, implement (NOT DELETED) in search filter
+- IMAP: extend thunderbird changed uid workaround to all contexts
+
+GUI:
+- Add new setting davmail.defaultDomain to set default windows domain
+- Prepare new advanced options
+
+Caldav:
+- Caldav: add davmail.forceActiveSyncUpdate option to the settings frame
+- Caldav: add davmail.caldavAlarmSound option to the settings frame (used to force conversion of Caldav alarms to AUDIO supported by iCal)
+- Caldav: fix 2884864, send notifications to all participants on CANCEL
+- Caldav: Fix invalid event handling, exclude events from returned list
+
+SMTP:
+- SMTP: implement RSET (reset) command to avoid connection timeout with Evolution
+
+** DavMail 3.6.2 released **
+New bugfix release, with improved OSX tray icon, Kontact
+support, a new workaround for thunderbird IMAP no message error,
+public folders on a separate server support, improved ActiveSync
+support and some documentation enhancements.
+
+LDAP:
+- LDAP: fix bug 2919463, escape quotes in search filter
+- LDAP: fix Kontact ldap filter parsing, allow LDAP_FILTER_PRESENT in subfilter
+
+Documentation:
+- Doc: fix script replace
+- Doc: new download and build pages
+- Doc: update roadmap
+- Doc: update doc
+- Doc: add search icon
+- Doc: Update roadmap
+- Doc: Add roadmap to site menu
+
+IMAP:
+- IMAP: workaround for thunderbird random issue with no message found, keep previous message list to cope with recent message uid change.
+- IMAP: try to support public folders hosted on a separate server (302 redirect on PROPFIND)
+- IMAP: fix date parsing error, see bug 2878289
+- IMAP: fix 2878289, implement extended MIME header search in http://schemas.microsoft.com/mapi/string/{00020386-0000-0000-C000-000000000046}/ namespace
+- IMAP: improve error logging on 500 internal server error
+- IMAP: Improve error handling, do not fail on message retrieval error, just send error message
+- IMAP: implement EXPUNGE untagged response on NOOP to avoid NO message not found on Exchange message message uid change
+- IMAP: implement RFC822.HEADER for Sylpheed
+
+Caldav:
+- Caldav: do not send fake inbox for public calendars to iCal
+- Caldav: id 2900599, implement optional attendees in notifications
+- Caldav: fix bug 2896135, iCal login fails at iCal startup
+- Caldav: Send root instead of calendar href as inbox to fix iCal regression
+- Caldav: Exclude events with a null instancetype and no ICS content
+- Caldav: Workaround for Lightning 1.0pre public calendar, send calendar href as inbox/outbox urls
+- Caldav: Convert DISPLAY to AUDIO only if user defined an alarm sound in settings (davmail.caldavAlarmSound)
+- Caldav: fix NullPointerException in notifications
+- Caldav: Fix bug 2907350, multiple calendar support issue with iCal
+- Caldav: another timezone fix
+- Caldav: Improve error handling on invalid events
+- Caldav: another timezone fix
+- Caldav: do not return invalid message content
+- Caldav: move failover for Exchange 2007 plus encoding issue to Exchange session
+- Caldav: a brand new ActiveSync fix, set contentclass to make ActiveSync happy, but also set PR_INTERNET_CONTENT to preserve custom properties. Also get etag from updated event.
+- Caldav: major refactoring, move all ICS methods to Event inner class
+- Caldav: fix bug 2902358, encode messageUrl in PropPatch with forceActiveSyncUpdate=true
+- Caldav: improve MIME message headers in createOrUpdateEvent
+
+Bugfixes:
+- Fix last open session failover: do not append @hostname when alias contains @
+- Revert to message url as default, use permanentUrl as failover
+- Always use NTCredentials for proxy authorization
+- Another NTLM fix: activate NTLM only on 401 unauthorized in executeGetMethod
+
+Enhancements:
+- Fix from audit (spelling errors)
+- Add search page and change default package name for default svn builds
+- Improve message logging
+- Fixes from audit
+- Additional Jsmooth settings
+- Force flags parameter to 4 in Form Based Authentication
+- Jsmooth patch to allow -X jvm options
+
+OSX:
+- OSX: replace inverted active icon
+- OSX: new Mac OS X only icons
+
+** DavMail 3.6.1 released **
+This is a bugfix release with fixed regressions from 3.6.0
+and a few enhancements from user feedback.
+
+Documentation:
+- Doc: switch download links to generic link
+- Doc: Update roadmap
+- Doc: add roadmap
+
+Bugfixes:
+- Fix regression in Form Based Authentication: detect Exchange 2007 UserContext cookie
+- Host is mandatory for NTLMScheme, get current hostname for proxy authentication
+
+Enhancements:
+- Experimental: reactivate NTLM authentication but leave authentication preemptive mode to allow basic authentication.
+- Move PKCS11 registration to a separate class to avoid ClassNotFoundException
+- Experimental OTP (token) based authentication
+- Vista png icons support for JSmooth
+- Fix from audit
+- New upload-dist ant task to upload new release files
+
+Caldav:
+- Caldav: failover for Exchange 2007 plus encoding issue, search event by displayname to get permanent Url
+- Caldav: Additional timezones
+- Caldav: Revert commit 765, VTODO events have a null instancetype
+- Caldav: additional timezone
+- Caldav: Remove MAILTO: in addition to mailto: in getFreeBusy
+- Caldav: Bug 2898469 do not UrlEncode draft url twice to avoid 404 not found on send event message
+
+** DavMail 3.6.0 released **
+This release contains a lot of enhancements, both bug fixes
+and new features from user feedback on 3.5.0, including improved
+Evolution LDAP support, LDAP abandon support (faster searches with
+Evolution and OSX), experimental windows service wrapper, improved
+form based authentication support and ENVELOPE IMAP command support.
+I wish to thank Dan Foody for his valuable contributions on
+OSX Snow Leopard support (attendee completion in iCal and complex
+LDAP filters handling).
+Also added an architecture schema on DavMail home page to quickly
+describe DavMail features.
+
+LDAP:
+- LDAP: implement cn=* filter for Evolution
+- LDAP: run searches in separate threads to implement ABANDON, will make searches faster with some clients (Evolution and OSX address book)
+- LDAP: implement startsWith on Contact search, only objectclass=* is a full search
+- LDAP: fix for iCal4 attendee completion, send localhost if current socket is local, else send fully qualified domain name
+- LDAP: major refactoring from Dan Foody to improve complex filters handling
+- LDAP: improve contact search, reencode uids to hex to avoid case sensitivity issues
+
+Documentation:
+- Doc: Set Dan Foody as main java contributor
+- Doc: improve DavMail logo
+- Doc: add new Logo, improve internet explorer compatibility
+- Doc: Add an architecture schema on site welcome page
+- Doc: Improve getting started documentation, explain Exchange 2003 and 2007 paths (/exchange/ and /owa/)
+- Doc: fix site style
+- Doc: fix maven site title
+
+SMTP:
+- SMTP: fix by Marc Macenko, case sensitive RCPT TO:
+- SMTP: allow lower case commands
+- SMTP: experimental: remove Content-Type on create message to avoid 406 not acceptable with some Exchange servers.
+
+Bugfixes:
+- Fix 2887947: Exchange server with a username hidden field
+
+Enhancements:
+- Check for released version in a separate thread and set timeout to ten seconds
+- Refactor message url encoding
+- Upgrade Jmooth wrappers, add -Xrs jvm option to davmailservice wrapper to avoid service stop on user session logout (http://sourceforge.net/projects/jsmooth/forums/forum/297041/topic/2370742)
+- Fix regression from revision 811
+- Refactor ExchangeSession, use StringUtil to simplify code
+- Remove username duplicate check, as formLogin now resets values before POST
+- Start refactoring: StringUtil class
+- Fix classpath in jsmooth wrappers to use new javamail
+- Allow custom form with userid/pw fields in form based authentication
+- Improve form based authentication, look for Exchange session cookies sessionid and cadata
+- Fix test
+- Upgrade JavaMail to 1.4.1
+- New create folder method
+- Fix FBA authentication, reset query string in getAbsoluteURI()
+- New abstract JUnit test case class
+- Detect redirect form instead of logon form, follow redirect to logon form
+- Add an upload-site ant task
+- Fixes from audit
+- Fix settings default values and update doc
+- Drop icon activity switches under 250ms to avoid fast flickering on OSX, add new switch icon in IMAP fetch iterations
+- Improve script based form redirect to handle more cases
+- Refactor ExchangeSession to allow independent session creation.
+- Allow directory in logFilePath settings, add /davmail.log suffix in this case
+- Allow follow redirects on /public GET requests
+
+Caldav:
+- Caldav: use permanenturl for Caldav to avoid encoding issues
+- Caldav: do not close connection on 401 authorization required, may help iCal authentication
+- Caldav: Additional Allday fix for Exchange 2007 and Outlook, implement a failover with a new davmail.timezoneId setting.
+- Caldav: fix regression on create event, missing CRLF in mime message
+- Caldav: Fix regression on public calendar folders linked to multiple calendar support for iCal
+- Caldav: use chunked response to send calendar folder content as ICS to avoid timeout
+- Caldav: Experimental GET ics on folder and fix regression on public folder access
+- Caldav: get current VTIMEZONE body from OWA to create Allday events compatible with Outlook. Users still need to select the same Timezone in Outlook and OWA.
+- Caldav: Fix Timezone value
+- Caldav: Create a new setting davmail.forceActiveSyncUpdate to let users choose to force ActiveSync event. Note: custom iCal or Lightning ICS properties are lost if this option is enabled.
+- Caldav: Some Exchange servers redirect to a different host for freebusy, use wide auth scope
+- Caldav: Another fix from Dan Foody: improve dumpICS debug option
+- Caldav: need to check session on each request, credentials may have changed or session expired
+- Caldav: fix regression after ActiveSync patch, PROPPATCH on contentClass removes all custom ICS properties
+- Caldav: improve getICSValue, do not return values inside VALARM section
+- Caldav: do not send events with a null instancetype (may be the cause of iCal failure)
+- CalDav: Send sub folders for multi-calendar support under iCal
+- Caldav: fix path translation to Exchange for calendars in sub folders under /calendar
+- Caldav: Added supported-calendar-component-set to calendar response
+- Caldav: added a debug trace when requested calendar is not user calendar (maybe shared, but often url mistake in Caldav client)
+- Caldav: fix Bug 2686125, PROPPATCH event after PUT to trigger activeSync PUSH, tested with iPhone 3 using activeSync
+
+IMAP:
+- IMAP: use permanenturl instead of href to avoid url encoding issues
+- IMAP: Revert convert absolute IMAP path to relative path, breaks Caldav
+- IMAP: Convert absolute IMAP path to relative path and detect ISA server cookie starting with cadata (instead of equals cadata)
+- IMAP: use upper case NIL in ENVELOPE
+- IMAP: improve MimeMessage handling, drop after fetch to avoid keeping full message in memory
+- IMAP: fix new ENVELOPE feature, must return encoded values
+- IMAP: implement store by id and ENVELOPE
+- IMAP: update message flag only if changed to avoid unneeded message uid bump, may fix Evolution and Apple Mail constant reload issue
+- IMAP: implement search by id
+- IMAP: send default BODYSTRUCTURE on MIME encoding error
+- IMAP: improve complex content-type handling in BODYSTRUCTURE
+- IMAP: fix deleted flag handling, switch to official Exchange IMAP property http://schemas.microsoft.com/mapi/id/{00062008-0000-0000-C000-000000000046}/0x8570
+- IMAP: detect HttpNotFoundException on folder select
+- IMAP: improve public folder error handling
+- IMAP: fix space at end of folder name
+- IMAP: Fix regression on LIST INBOX
+- IMAP: experimental public folder access
+- IMAP: switch to http://schemas.microsoft.com/exchange/contentstate to handle deleted flag (DAV:isdeleted did not work with some Exchange servers).
+- IMAP: implement undelete message
+
+** DavMail 3.5.0 released **
+This release improves OSX Snow Leopard support, thank to
+contributions from Dan Foody. Contact searches are also
+available now in addition to global address book searches
+over LDAP. IMAP with iPhone should now work correctly with
+most messages, Evolution IMAP read flag is fixed.
+Also added an experimental windows service support
+and a lot of other bug fixes and enhancements.
+
+Doc:
+- Doc: Fix FAQ whitespaces
+- Doc: improve javadoc and code cleanup
+- Doc: New OSX settings screenshot
+- Doc: update release guide
+- Doc: improve index and build doc
+- Doc: detailed WIRE debug log file creation
+
+Windows:
+- Windows: Include DavMail service in windows package
+- Windows: create windows service exe
+- Windows: create windows service exe
+
+IMAP:
+- IMAP: test session expiration on each command, get a new session on expiration
+- IMAP: improve error logging on network down
+- IMAP: fix bug 2845530 implement FLAGS.SILENT command
+- IMAP: fix absolute (public) path handling
+- IMAP: fix BODYSTRUCTURE, build message on full buffer, do not rely on partial buffer (header, body, ...)
+- IMAP: fix bug 2835529 FETCH with unordered range
+- IMAP: send default BODYSTRUCTURE on mime parsing failure
+- IMAP: Improve IMAP bodystructure error logging
+- IMAP: Send bodystructure with headers for iPhone request (BODYSTRUCTURE BODY.PEEK[HEADER])
+- IMAP: send BODY[TEXT] for BODY.PEEK[TEXT] request, may improve iPhone support
+- IMAP: First fix for bug 2840255, do not follow redirects on message FETCH
+
+Caldav:
+- Caldav: fix conflict between X-MICROSOFT-CDO-BUSYSTATUS, X-MICROSOFT-CDO-ALLDAYEVENT and ORGANIZER ics patches
+- Caldav: check credentials on each request
+- Caldav: Disable broken sub calendar folders code
+- Caldav: Do not fail on Inbox access denied, just return an empty folder
+- Caldav: fix multi calendar Exchange path for sub folders
+- Caldav: Experimental, fix sub calendar folders path
+- Caldav: Experimental, send sub calendar folders on propfind with depth 1
+- Caldav: Handle multi line description in calendar message body
+- Caldav: merged contribution from Dan Foody,
+- Caldav: convert sound to display alarms and back
+- Caldav: remove additional organizer attendee line
+- Caldav: remove RSVP=TRUE if PARTSTAT is not NEEDS-ACTION
+- Caldav: add dump ICS logging feature
+- Caldav: add a text/plain body to calendar messages
+- Caldav: create a subject for calendar messages
+- Caldav: fixed some encoding issues in Dan's code
+- Caldav: Additional timezones
+- Caldav: failover to DAV:comment instead of CALDAV:schedule-state on some Exchange servers
+
+LDAP:
+- LDAP: iCal fix to suit both iCal 3 and 4: move cn to sn, remove cn
+- LDAP: iCal: do not send LDAP_SIZE_LIMIT_EXCEEDED on apple-computer search by cn with sizelimit 1
+- LDAP: copy uid to apple-generateduid for iCal attendee search
+- LDAP: Make sure we do not send empty description field, replace " \n" with null
+- LDAP: fix thread name
+- LDAP: exclude non contact entries from search, fiw map key and sn copy for iCal
+- LDAP: fix contact search, do not send unrequested attributes
+- LDAP: improve Contact search filter support
+- LDAP: Additional Contact attributes
+- LDAP: refactor contact find, generic attribute mapping
+- LDAP: experimental contact search support
+
+OSX:
+- OSX: Prepare hide from Dock option
+- OSX: crazy workaround from Dan Foody to fix attendee search on OSX Snow Leopard
+- OSX: iCal4 (OSX Snow Leopard fixes)
+
+Bug Fixes:
+- Fix regression in Form Based Logon: fix script based logon URI creation with path and query
+- Another network down fix: DavMailException is not network down
+- Improve Form Based Logon: fix script based logon URI creation
+- Improve Form Based Logon: use full URI instead of path in PostMethod, also force trusted=4
+- Simplify HttpClient creation to avoid password decoding bug in commons httpclient ('+' in password decoded as ' ')
+
+Enhancements:
+- Improve HttpException error logging
+- Fixes from checkStyle audit
+- Adjust checkStyle settings
+- Improve error handling on invalid URL
+- Various fixes from FindBugs audit
+- Fix from audit: synchronize access to HttpConnectionManager
+- Refactor ExchangeSession: do not follow redirects with GET methods
+- Fix from audit
+- Do not pass DavMailAuthenticationException to handle network down
+- Custom form (txtUserName, tstUserPass) support
+- Another network down fix from Dan Foody
+- Merged another patch from Dan Foody on network down detection
+- New settings method: return log file directory
+
+** DavMail 3.4.0 released **
+This release includes iPhone 3 Caldav support, upgrade to SWT 3.5,
+Palm Pre IMAP fixes, improved shared/public calendar support
+and a lot of bug fixes.
+
+Doc:
+- Doc: Code cleanup and improve javadoc
+- Doc: Update doc
+- Doc: Upgrade maven site plugin and improve style
+- Doc: Update maven pom inceptionYear
+
+Bug:
+- Bug: Do not try to set Nimbus Look And Feel on Linux with Gtk
+
+Enhancements:
+- Remove NTLM authentication, breaks Basic authentication (missing domain in username)
+- Set NTLM as last authentication scheme
+- Experimental: reenable NTLM authentication
+- Upgrade SWT to 3.5
+- Use getFolderPath in getSubFolders
+- Make API more consistent: createMessage must get a folder path, not URL
+- Enhancement: Patch 2826966 from Eivind Tagseth, Make davmail.sh work from any location
+
+IMAP:
+- IMAP: Need to reset index for Palm pre
+- IMAP: case insensitive search operators
+- IMAP: Fix bug 2835529, implement SEARCH ALL SINCE for Palm Pre
+
+Caldav:
+- Caldav: improve error handling, 440 means 403 forbidden on Exchange
+- Caldav: Fix shared calendar support for Lightning
+- Caldav: additional patch for Outlook created recurring events
+- Caldav: set X-MICROSOFT-CDO-BUSYSTATUS according to TRANSP field
+- Caldav: implement a timezone translation table for iPhone 3, revert organizer patch (breaks notifications with Lightning)
+- Caldav: another iPhone fix, remove organizer line if user is organizer
+- Caldav: generic timezone patch for iPhone 3
+- Caldav: remove empty ics properties
+- Caldav: Remove calendar-proxy, only used for delegate calendars
+- Caldav: try to improve responses for iCal
+- Caldav: fix bug 2833044 Event not found error on dismissing reminders with events created in Outlook with a plus sign in subject
+- Caldav: Experimental, add calendar-proxy DAV option and version in server header
+- Caldav: Add missing allow OPTIONS header
+- Caldav: improve public (shared) calendar support, accept calendars at any depth
+- Caldav: set caldav logging to davmail logging level
+- Caldav: updated fix, remove organizer line if event has no attendees for iPhone
+- Caldav: remove organizer line if current user is organizer for iPhone, will not remove line for events with attendees
+- Caldav: Improve principal -> actualPrincipal detection: use session alias instead of login
+- Caldav: fix bug 2819028, case insensitive email in sendPrincipal test
+- Caldav: iPhone compatibility, remove <C:comp name="VTODO"/>
+- Caldav: iPhone workaround: send calendar subfolder
+- Caldav: revert @ encoding, breaks iCal
+- Caldav: iPhone fix, encode @ in Caldav response hrefs
+- Caldav: untested, extended PROPFIND / response for iPhone 3.0
+- Caldav: fix infinite loop with Sunbird 1.0pre with invalid credentials
+
+SMTP:
+- SMTP: fix bug 2791607, do not patch message body (breaks electronic signature), no longer needed with latest Thunderbird
+
+** DavMail 3.3.0 released **
+This is a bug fix release after two beta releases,
+including PKCS11 (smartcard) client certificate support,
+gateway encryption (SSL) support, the new jackrabbit and httpclient libraries
+and I18N support (french and english available).
+
+- Caldav: updated caldav response headers according to gmail, added Expires and Cache-control HTTP headers
+- POP3: implement NOOP command
+- Doc: Update documentation header
+- Doc: Add GPLv2 header to all source files
+- Doc: Remove Apache license from checkstyle config file header
+- IMAP: fix DELETED/UNDELETED SEARCH parameters
+- IMAP: Fix bug 2822625: support index range in IMAP SEARCH
+- Enhancements: Merged network down (with firewall) code from Dan Foody
+- Caldav: Additional properties and ignore cases for Sunbird
+- Caldav: Fix empty organizer field in ICS (active sync support) and another getParticipants bug
+- GUI: Fix OSX menu default ActionListener
+- GUI: Try to set Nimbus Look And Feel on Linux with Gtk
+
+** DavMail 3.3.0b2 released with smartcard (PKCS11) support **
+This is a bug fix release, with nonetheless one main new feature:
+PKCS11 (smartcard) client certificate support !
+Tested with ActivIdentity ActivClient and Mozilla soft token, should
+work with any PKCS11 module.
+
+Security:
+- (RFE 2800206) PKCS11 (smartcard) client certificate support
+
+Server (daemon) mode:
+- Fix server mode: now all listener threads are daemon, avoid main thread exit and add a shutdown hook
+- Name shutdown thread
+
+Caldav:
+- Move wire debug log with headers
+- Fix NoSuchMethodError with Java 5
+- revert supported-calendar-component-set on root and improve logging
+- fix regression on iCal calendar color change
+- only include attendees with RSVP=TRUE or PARTSTAT=NEEDS-ACTION for notifications (avoid iCal additional notifications)
+- Improve error handling on FreeBusy failure
+- add supported-calendar-component-set property requested by iPhone 3.0
+ Sunbird compatibility, exclude events with empty names
+- Fix for iCal: send etag on GET and HEAD requests
+- Send empty response instead of error on freebusy with unknown attendee
+
+IMAP:
+- revert refreshFolder calls that break Outlook
+
+OSX:
+- Remove default trayIcon listener on OSX
+- Fixed logFilePath logic on OSX
+
+Enhancements:
+- Update ExchangeSession test
+- Exclude optional log4j maven dependencies
+- Added a logFilePath setting to set log4j file appender path, this appender is now added dynamically to avoid davmail.log file create failure
+- Upgrade Log4J to 1.2.15
+- Make sure we do exit: catch exceptions before System.exit
+
+Doc:
+- Document build process in FAQ
+
+Known issues :
+- Does not - yet - work with iPhone 3.0 Caldav
+- Still issues with Exchange activeSync mode
+
+** Davmail 3.3.0b1 released **
+This release is mainly a deep refactoring: replaced deprecated
+jakarta slide library with jackrabbit and upgrade httpclient.
+Also added client to gateway encryption (SSL) support, started I18N
+(french and english available) and many bug fixes and enhancements.
+
+I18N:
+- I18N: FrameGatewayTray
+- I18N: Format port numbers and add missing message
+- I18N: improve startup log message
+- I18N: remove Locale.ROOT not available under Java 1.5
+- I18N: externalize and translate exception messages
+- I18N: Do not apply i18n on log file
+- I18N: french localization
+- I18N: externalize all DavGatewayTray log statements for i18n
+- I18N: ldap package
+- I18N: davmail package
+- I18N: start internationalization conversion
+
+POP:
+- POP: Defer message list after login phase and load only uid and size attributes
+- POP: make sure the url is encoded correctly on delete
+
+IMAP:
+- IMAP: Detect fetch of a missing (probably deleted) message to avoid infinite loop with Thunderbird
+- IMAP: reset icon after each command
+
+SMTP:
+- SMTP: fixed two bugs, header ignored because of Exchange 2007 from patch and bccbuffer
+ double xml encoding (=> Delivery status notification)
+
+LDAP:
+- LDAP: improve ldap search logging
+
+Doc:
+- Add Mitchell V. Oliver as Java Contributor
+- SSL certificate settings documentation in getting started
+- Update OSX doc: credentials are mandatory in Directory Utility settings
+- Add Eivind Tagseth as Java Contributor
+
+Enhancements:
+- Check java version in ant build.xml
+- Fix ExchangeSession test
+- Refactor DavProperty handling with new jackrabbit library
+- Close idle connections to exchange after one minute
+- Avoid 401 roundtrips
+- Remove deprecated HttpException api
+- Replace deprecated setRequestBody with setRequestEntity
+- Refactor DavProperty handling with new jackrabbit library
+- Update packaging and Maven POM after library update
+- Major refactoring: replace deprecated jakarta slide with jackrabbit and upgrade httpclient
+- Upgrade svnkit library
+- Sort properties and display version in startup message
+- Use interactive console certificate accept in headless and/or server mode
+- Append svn build number to release name
+- Additional login failover : get email from options page
+- Replace greyscale inactive icon with a color icon
+- Avoid nullpointerexception in Settings.setProperty
+- Reinsert System.exit after clean shutdown to make sure we do exit
+- Make all threads daemon and remove System.exit calls
+- Patch 2790299 by Mitchell V. Oliver: Add support for SSL to client connections
+- Remember previous checkConfig status to detect network down
+- Fixes from audit
+- Refactor email retrieval : do not throw IOException in failovers
+- Implement BundleMessage.toString() for direct usage in Log4J logger
+- Revert to simple class names in thread names
+- Catch unknown host on session login
+- Workaround for post logon script redirect
+- Workaround for Exchange server misconfiguration: move galfind requests to mailPath or /exchange instead of /public
+- Enhancements from audit
+- Fix exchangeSession test class
+- Improve BindException error message
+- Cleanup from audit
+- Improve exception handling
+- Implement a last resort failover to build email from alias and domain name
+- Limit redirects to 10 instead of 100
+- Replace hardcoded strings
+- Refactor SimpleDateFormat usage
+- Reorganize packages
+
+OSX:
+- OSX: replace JavaApplicationStub link with actual file
+- OSX: Move davmail.log to Library/Logs/DavMail on OSX
+- OSX: Improve Mac OSX Java6 support
+- OSX: fix regression on OSX Quit handler
+
+Bugs fixed:
+- Fix bug 2797272: Add disable update check
+- Do not localize port numbers
+- Replaced localhost check with the isLoopbackAddress() method, should be IPV6 compatible
+- Fix regression : /exchange/ does not work for galfind under Exchange 2007
+- Fixed 2717547: Unsupported filter attribute: apple-group-memberguid
+- URI encode alias in getEmail()
+- Fix SSLProtocolSocketFactory with HttpClient 3.1
+- Reenable limited timeout on getReleasedVersion
+- Always exclude NTLM authentication, not only for proxy authorization
+- Fix 2717446 from Eivind Tagseth
+
+Caldav:
+- Caldav: fix unknown recipient message
+- Caldav: do not send freebusy info if attendee is unknown
+- Caldav: Improve calendar-color patch answer
+- Caldav: implement HEAD request
+- Improve network down detection for Caldav
+- Caldav: No need to check connectivity on HTTPS
+- Caldav: Fix Bug 2783595, allow empty lines in ICS content
+- Caldav: Exclude RSVP=FALSE from notifications recipients list for Outlook 2003 compatibility
+- Caldav: exclude invalid attendees address from recipient list
+- Caldav: avoid duplicate / in event path
+- Caldav: implement public shared calendar
+- Caldav: In progress multi calendar support
+- Caldav: fix regression in FreeBusy date handling
+- Caldav: switch icon during event report
+- Caldav: refactor CaldavConnection, prepare /public context
+- Caldav: another special characters handling improvement
+- Caldav: iCal decodes hrefs, not lightning => detect client in CaldavRequest
+- Caldav: replace etag by resourcetag in getCalendarEtag
+- Caldav: Send events back to the client after each get on REPORT request (avoid iCal timeout)
+- Caldav: no inbox/outbox for delegated calendars
diff --git a/src/bin/davmail b/src/bin/davmail
new file mode 100644
index 0000000..f1e0eaf
--- /dev/null
+++ b/src/bin/davmail
@@ -0,0 +1,4 @@
+#!/bin/sh
+export LD_LIBRARY_PATH=/usr/lib/jni
+for i in /usr/share/davmail/lib/*; do export CLASSPATH=$CLASSPATH:$i; done
+java -Xmx512M -cp /usr/share/davmail/davmail.jar:/usr/share/java/swt.jar:$CLASSPATH davmail.DavGateway "$@"
diff --git a/src/checkstyle/checkstyle-configuration.xml b/src/checkstyle/checkstyle-configuration.xml
new file mode 100644
index 0000000..bc51427
--- /dev/null
+++ b/src/checkstyle/checkstyle-configuration.xml
@@ -0,0 +1,192 @@
+<?xml version="1.0"?>
+<!DOCTYPE module PUBLIC
+ "-//Puppy Crawl//DTD Check Configuration 1.2//EN"
+ "http://www.puppycrawl.com/dtds/configuration_1_2.dtd">
+
+<!--
+
+ Checkstyle configuration that checks the sun coding conventions from:
+
+ - the Java Language Specification at
+ http://java.sun.com/docs/books/jls/second_edition/html/index.html
+
+ - the Sun Code Conventions at http://java.sun.com/docs/codeconv/
+
+ - the Javadoc guidelines at
+ http://java.sun.com/j2se/javadoc/writingdoccomments/index.html
+
+ - the JDK Api documentation http://java.sun.com/j2se/docs/api/index.html
+
+ - some best practices
+
+ Checkstyle is very configurable. Be sure to read the documentation at
+ http://checkstyle.sf.net (or in your downloaded distribution).
+
+ Most Checks are configurable, be sure to consult the documentation.
+
+ To completely disable a check, just comment it out or delete it from the file.
+
+ Finally, it is worth reading the documentation.
+
+-->
+
+<module name="Checker">
+ <!--
+ If you set the basedir property below, then all reported file
+ names will be relative to the specified directory. See
+ http://checkstyle.sourceforge.net/5.x/config.html#Checker
+
+ <property name="basedir" value="${basedir}"/>
+ -->
+
+ <!-- Checks that a package-info.java file exists for each package. -->
+ <!-- See http://checkstyle.sf.net/config_javadoc.html#JavadocPackage -->
+ <!--module name="JavadocPackage"/-->
+
+ <!-- Checks whether files end with a new line. -->
+ <!-- See http://checkstyle.sf.net/config_misc.html#NewlineAtEndOfFile -->
+ <!--module name="NewlineAtEndOfFile"/-->
+
+ <!-- Checks that property files contain the same keys. -->
+ <!-- See http://checkstyle.sf.net/config_misc.html#Translation -->
+ <module name="Translation"/>
+
+ <!-- Checks for Size Violations. -->
+ <!-- See http://checkstyle.sf.net/config_sizes.html -->
+ <!--module name="FileLength"/-->
+
+ <!-- Checks for whitespace -->
+ <!-- See http://checkstyle.sf.net/config_whitespace.html -->
+ <!--module name="FileTabCharacter"/-->
+
+ <!-- Miscellaneous other checks. -->
+ <!-- See http://checkstyle.sf.net/config_misc.html -->
+ <!--module name="RegexpSingleline">
+ <property name="format" value="\s+$"/>
+ <property name="minimum" value="0"/>
+ <property name="maximum" value="0"/>
+ <property name="message" value="Line has trailing spaces."/>
+ </module-->
+
+ <module name="TreeWalker">
+
+ <!-- Checks for Javadoc comments. -->
+ <!-- See http://checkstyle.sf.net/config_javadoc.html -->
+ <!--module name="JavadocMethod"/>
+ <module name="JavadocType"/>
+ <module name="JavadocVariable"/>
+ <module name="JavadocStyle"/-->
+
+
+ <!-- Checks for Naming Conventions. -->
+ <!-- See http://checkstyle.sf.net/config_naming.html -->
+ <module name="ConstantName"/>
+ <module name="LocalFinalVariableName"/>
+ <module name="LocalVariableName"/>
+ <module name="MemberName"/>
+ <module name="MethodName"/>
+ <module name="PackageName"/>
+ <module name="ParameterName"/>
+ <module name="StaticVariableName"/>
+ <module name="TypeName"/>
+
+
+ <!-- Checks for Headers -->
+ <!-- See http://checkstyle.sf.net/config_header.html -->
+ <!-- <module name="Header"> -->
+ <!-- The follow property value demonstrates the ability -->
+ <!-- to have access to ANT properties. In this case it uses -->
+ <!-- the ${basedir} property to allow Checkstyle to be run -->
+ <!-- from any directory within a project. See property -->
+ <!-- expansion, -->
+ <!-- http://checkstyle.sf.net/config.html#properties -->
+ <!-- <property -->
+ <!-- name="headerFile" -->
+ <!-- value="${basedir}/java.header"/> -->
+ <!-- </module> -->
+
+ <!-- Following interprets the header file as regular expressions. -->
+ <!-- <module name="RegexpHeader"/> -->
+
+
+ <!-- Checks for imports -->
+ <!-- See http://checkstyle.sf.net/config_import.html -->
+ <!--module name="AvoidStarImport"/-->
+ <!--module name="IllegalImport"/--> <!-- defaults to sun.* packages -->
+ <module name="RedundantImport"/>
+ <module name="UnusedImports"/>
+
+
+ <!-- Checks for Size Violations. -->
+ <!-- See http://checkstyle.sf.net/config_sizes.html -->
+ <!--module name="LineLength"/-->
+ <!--module name="MethodLength"/-->
+ <module name="ParameterNumber"/>
+
+
+ <!-- Checks for whitespace -->
+ <!-- See http://checkstyle.sf.net/config_whitespace.html -->
+ <!--module name="EmptyForIteratorPad"/>
+ <module name="GenericWhitespace"/>
+ <module name="MethodParamPad"/>
+ <module name="NoWhitespaceAfter"/>
+ <module name="NoWhitespaceBefore"/>
+ <module name="OperatorWrap"/>
+ <module name="ParenPad"/>
+ <module name="TypecastParenPad"/>
+ <module name="WhitespaceAfter"/>
+ <module name="WhitespaceAround"/-->
+
+
+ <!-- Modifier Checks -->
+ <!-- See http://checkstyle.sf.net/config_modifiers.html -->
+ <module name="ModifierOrder"/>
+ <module name="RedundantModifier"/>
+
+
+ <!-- Checks for blocks. You know, those {}'s -->
+ <!-- See http://checkstyle.sf.net/config_blocks.html -->
+ <module name="AvoidNestedBlocks"/>
+ <!--module name="EmptyBlock"/-->
+ <module name="LeftCurly"/>
+ <module name="NeedBraces"/>
+ <module name="RightCurly"/>
+
+ <!-- Checks for common coding problems -->
+ <!-- See http://checkstyle.sf.net/config_coding.html -->
+ <!--module name="AvoidInlineConditionals"/-->
+ <module name="DoubleCheckedLocking"/>
+ <!-- MY FAVOURITE -->
+ <module name="EmptyStatement"/>
+ <module name="EqualsHashCode"/>
+ <!--module name="HiddenField"/-->
+ <module name="IllegalInstantiation"/>
+ <!--module name="InnerAssignment"/-->
+ <!--module name="MagicNumber"/-->
+ <module name="MissingSwitchDefault"/>
+ <module name="RedundantThrows"/>
+ <module name="SimplifyBooleanExpression"/>
+ <module name="SimplifyBooleanReturn"/>
+
+ <!-- Checks for class design -->
+ <!-- See http://checkstyle.sf.net/config_design.html -->
+ <!--module name="DesignForExtension"/-->
+ <module name="FinalClass"/>
+ <module name="HideUtilityClassConstructor"/>
+ <module name="InterfaceIsType"/>
+ <!--module name="VisibilityModifier">
+ <property name="packageAllowed" value="true"/>
+ <property name="protectedAllowed" value="true"/>
+ </module-->
+
+
+ <!-- Miscellaneous other checks. -->
+ <!-- See http://checkstyle.sf.net/config_misc.html -->
+ <module name="ArrayTypeStyle"/>
+ <!--module name="FinalParameters"/-->
+ <module name="TodoComment"/>
+ <module name="UpperEll"/>
+
+ </module>
+
+</module>
diff --git a/src/contribs/davmailpd/davmailpd b/src/contribs/davmailpd/davmailpd
new file mode 100644
index 0000000..98cf0b0
--- /dev/null
+++ b/src/contribs/davmailpd/davmailpd
@@ -0,0 +1,86 @@
+#!/bin/bash
+#
+# davmailpd: Davmail private daemon
+#
+# copyright Geert Stappers <stappers at stappers.nl> 2010
+# distributed under the terms of the GNU General Public License
+#
+usage() {
+ cat << USAGE
+
+ davmailpd {status|start|stop|restart}
+
+ restart: stops and starts davmail
+ status: tells if davmail is running for the executing user
+ start: starts davmail
+ stop: sends SIGHUP to davmail proces of the executing user
+
+USAGE
+}
+
+#### Example output of commands used in find_process_id()
+## # Executing User
+## $ who am i
+## stappers pts/4 2010-09-06 07:22 (:0.0)
+## # Possible Processes
+## $ ps -ef | grep -e "^stappers.*gv.*vol.*tor$"
+## stappers 23201 1 0 Sep06 ? 00:00:00 /usr/lib/gvfs-gdu-volume-monitor
+## stappers 23203 1 0 Sep06 ? 00:00:00 /usr/lib/gvfs-gphoto2-volume-monitor
+
+find_process_id() {
+ EU=$( who ami | awk '{ print $1 }' )
+ PP=$(ps -ef | grep -e "^${EU}.*java.*\.jar.*\.jar.*davmail\.DavGateway" )
+ if [ -z "${PP}" ] ; then
+ echo -n 'None'
+ else
+ echo ${PP} | awk '{ printf(" %s",$2) }'
+ fi
+}
+
+status_cmd() {
+ if [ $(find_process_id) == 'None' ] ; then
+ echo 'NO Davmail running for you'
+ else
+ echo 'OKay, you have running Davmail process(es)'
+ fi
+}
+
+send_signal_hangup() {
+ if [ $(find_process_id) != 'None' ] ; then
+ kill -n SIGHUP $(find_process_id)
+ fi
+}
+
+case "$1" in
+ status)
+ status_cmd
+ exit 0
+ ;;
+ stop)
+ send_signal_hangup
+ exit 0
+ ;;
+ start)
+ shift # remove first parameter ( and leave others untouched )
+ # continue, no exit here
+ ;;
+ restart)
+ shift # remove first parameter ( and leave others untouched )
+ send_signal_hangup
+ # continue, no exit here
+ ;;
+ processes|proc*|debug|d*)
+ EU=$( who ami | awk '{ print $1 }' )
+ ps -ef | grep -e "^${EU}.*java.*\.jar.*\.jar.*davmail\.DavGateway"
+ find_process_id ; echo
+ exit 0
+ ;;
+ *)
+ usage
+ exit 0
+ ;;
+esac
+# the actual start
+export LD_LIBRARY_PATH=/usr/lib/jni
+for i in /usr/share/davmail/lib/*; do export CLASSPATH=$CLASSPATH:$i; done
+java -Xmx512M -cp /usr/share/davmail/davmail.jar:$CLASSPATH davmail.DavGateway "$@" > /dev/null 2>&1 &
diff --git a/src/contribs/init/davmail-init b/src/contribs/init/davmail-init
new file mode 100644
index 0000000..9f43b6c
--- /dev/null
+++ b/src/contribs/init/davmail-init
@@ -0,0 +1,162 @@
+#!/bin/bash
+#
+# chkconfig: 345 99 05
+# # description: Java deamon script for davmail (http://davmail.sourceforge.net/) write by qk4l at tem4uk.ru 2011
+#
+# Derived from -
+# Home page: http://www.source-code.biz
+# License: GNU/LGPL (http://www.gnu.org/licenses/lgpl.html)
+# Copyright 2006 Christian d'Heureuse, Inventec Informatik AG, Switzerland.
+#
+# History:
+# 2010-09-21 Josh Davis: Changed 'sudo' to 'su', fix some typos, removed unused variables
+# 2009-03-04 Josh Davis: Ubuntu/Redhat version.
+# 2006-06-27 Christian d'Heureuse: Script created.
+# 2006-07-02 chdh: Minor improvements.
+# 2006-07-10 chdh: Changes for SUSE 10.0.
+
+
+# Set this to your Java installation
+
+serviceNameLo="davmail" # service name with the first letter in lowercase
+serviceName="Davmail" # service name
+serviceUser="davmail" # OS user name for the service
+serviceGroup="root" # OS group name for the service
+applDir="/usr/share/$serviceNameLo/lib" # home directory of the service application
+serviceUserHome="/etc/$serviceNameLo" # home directory of the service user
+serviceLogFile="/var/log/$serviceNameLo.log" # log file for StdOut/StdErr
+maxShutdownTime=15 # maximum number of seconds to wait for the daemon to terminate normally
+pidFile="/var/run/$serviceNameLo.pid" # name of PID file (PID = process ID number)
+javaCommand="java" # name of the Java launcher without the path
+javaExe="/usr/bin/$javaCommand -Xmx512M" # file name of the Java application launcher executable
+javaCommandLineKeyword="davmail.jar" # a keyword that occurs on the commandline, used to detect an already running service process and to distinguish it from others
+DAEMON_ARGS="/etc/davmail/davmail.properties"
+
+# Makes the file $1 writable by the group $serviceGroup.
+function makeFileWritable {
+ local filename="$1"
+ touch $filename || return 1
+ chown $serviceUser:$serviceGroup $filename || return 1
+ #chgrp $serviceGroup $filename || return 1
+ chmod g+w $filename || return 1
+ return 0; }
+
+# Returns 0 if the process with PID $1 is running.
+function checkProcessIsRunning {
+ local pid="$1"
+ if [ -z "$pid" -o "$pid" == " " ]; then return 1; fi
+ if [ ! -e /proc/$pid ]; then return 1; fi
+ return 0; }
+
+# Returns 0 if the process with PID $1 is our Java service process.
+function checkProcessIsOurService {
+ local pid="$1"
+ if [ "$(ps -p $pid --no-headers -o comm)" != "$javaCommand" ]; then return 1; fi
+ grep -q --binary -F "$javaCommandLineKeyword" /proc/$pid/cmdline
+ if [ $? -ne 0 ]; then return 1; fi
+ return 0; }
+
+# Returns 0 when the service is running and sets the variable $pid to the PID.
+function getServicePID {
+ if [ ! -f $pidFile ]; then return 1; fi
+ pid="$(<$pidFile)"
+ checkProcessIsRunning $pid || return 1
+ checkProcessIsOurService $pid || return 1
+ return 0; }
+
+function startServiceProcess {
+ cd $applDir || return 1
+ rm -f $pidFile
+ makeFileWritable $pidFile || return 1
+ makeFileWritable $serviceLogFile || return 1
+ for i in $applDir/*; do export CLASSPATH=$CLASSPATH:$i; done
+ cmd="nohup $javaExe -cp /usr/share/davmail/davmail.jar:$CLASSPATH davmail.DavGateway $DAEMON_ARGS >>$serviceLogFile 2>&1 & echo \$! >$pidFile"
+ #echo $cmd
+ su -m $serviceUser -s $SHELL -c "$cmd" || return 1
+ sleep 0.1
+ pid="$(<$pidFile)"
+ if checkProcessIsRunning $pid; then :; else
+ echo -ne "\n$serviceName start failed, see logfile."
+ return 1
+ fi
+ return 0; }
+
+function stopServiceProcess {
+ kill $pid || return 1
+ for ((i=0; i<maxShutdownTime*10; i++)); do
+ checkProcessIsRunning $pid
+ if [ $? -ne 0 ]; then
+ rm -f $pidFile
+ return 0
+ fi
+ sleep 0.1
+ done
+ echo -e "\n$serviceName did not terminate within $maxShutdownTime seconds, sending SIGKILL..."
+ kill -s KILL $pid || return 1
+ local killWaitTime=15
+ for ((i=0; i<killWaitTime*10; i++)); do
+ checkProcessIsRunning $pid
+ if [ $? -ne 0 ]; then
+ rm -f $pidFile
+ return 0
+ fi
+ sleep 0.1
+ done
+ echo "Error: $serviceName could not be stopped within $maxShutdownTime+$killWaitTime seconds!"
+ return 1; }
+
+function startService {
+ getServicePID
+ if [ $? -eq 0 ]; then echo -n "$serviceName is already running"; RETVAL=0; return 0; fi
+ echo -n "Starting $serviceName "
+ startServiceProcess
+ if [ $? -ne 0 ]; then RETVAL=1; echo "failed"; return 1; fi
+ echo "started PID=$pid"
+ RETVAL=0
+ return 0; }
+
+function stopService {
+ getServicePID
+ if [ $? -ne 0 ]; then echo -n "$serviceName is not running"; RETVAL=0; echo ""; return 0; fi
+ echo -n "Stopping $serviceName "
+ stopServiceProcess
+ if [ $? -ne 0 ]; then RETVAL=1; echo "failed"; return 1; fi
+ echo "stopped PID=$pid"
+ RETVAL=0
+ return 0; }
+
+function checkServiceStatus {
+ echo -n "Checking for $serviceName: "
+ if getServicePID; then
+ echo "running PID=$pid"
+ RETVAL=0
+ else
+ echo "stopped"
+ RETVAL=3
+ fi
+ return 0; }
+
+function main {
+ RETVAL=0
+ case "$1" in
+ start) # starts the Java program as a Linux service
+ startService
+ ;;
+ stop) # stops the Java program service
+ stopService
+ ;;
+ restart) # stops and restarts the service
+ stopService && startService
+ ;;
+ status) # displays the service status
+ checkServiceStatus
+ ;;
+ *)
+ echo "Usage: $0 {start|stop|restart|status}"
+ exit 1
+ ;;
+ esac
+ exit $RETVAL
+}
+
+main $1
\ No newline at end of file
diff --git a/src/contribs/rpm/SOURCES/davmail-init b/src/contribs/rpm/SOURCES/davmail-init
new file mode 100644
index 0000000..6bf9914
--- /dev/null
+++ b/src/contribs/rpm/SOURCES/davmail-init
@@ -0,0 +1,134 @@
+#!/bin/sh
+#
+# davmail: davmail exchange gateway daemon
+#
+# chkconfig: 345 98 02
+# description: DavMail gateway for Microsoft Exchange
+# processname: davmail
+# config: /etc/davmail.properties
+
+# LSB init-info
+### BEGIN INIT INFO
+# Provides: davmail
+# Required-Start: $network
+# Required-Stop: $network
+# Default-Start: 2 3 4 5
+# Default-Stop: 0 1 6
+# Short-Description: DavMail gateway for Microsoft Exchange
+### END INIT INFO
+
+# Source function library.
+if [ -e /etc/init.d/functions ]; then
+ . /etc/init.d/functions
+fi
+
+# LSB functions
+. /lib/lsb/init-functions
+
+# Check that networking is configured.
+[ "${NETWORKING}" = "no" ] && exit 0
+
+DAVMAIL_CONF=/etc/davmail.properties
+test -f $DAVMAIL_CONF || exit 4
+DAVMAIL_HOME=/var/lib/davmail
+test -d $DAVMAIL_HOME || exit 5
+
+LOGFILE=/var/log/davmail.log
+PIDFILE=/var/run/davmail.pid
+LOCKFILE=/var/lock/subsys/davmail
+
+start() {
+ echo -n $"Starting DavMail gateway: "
+ dostatus > /dev/null 2>&1
+ if [ $RETVAL -eq 0 ]
+ then
+ echo -n $"DavMail gateway already running"
+ log_failure_msg
+ RETVAL=1
+ return
+ fi
+ runuser - davmail -s /bin/sh -c "exec nohup $DAVMAIL_HOME/davmail $DAVMAIL_CONF >> $LOGFILE 2>&1 &"
+ RETVAL=$?
+ if [ $RETVAL -eq 0 ]
+ then
+ sleep 1
+ cat $DAVMAIL_HOME/pid > $PIDFILE
+ touch $LOCKFILE
+ log_success_msg
+ else
+ log_failure_msg
+ fi
+ return $RETVAL
+}
+
+stop() {
+ echo -n $"Shutting down DavMail gateway: "
+ kill $(cat $PIDFILE 2>/dev/null) > /dev/null 2>&1
+ RETVAL=$?
+ sleep 1
+ if [ $RETVAL -eq 0 ]
+ then
+ rm -f $PIDFILE $LOCKFILE
+ log_success_msg
+ else
+ log_failure_msg
+ fi
+ return $RETVAL
+}
+
+restart() {
+ stop
+ start
+}
+
+condrestart() {
+ [ -f $LOCKFILE ] && restart || :
+}
+
+dostatus() {
+ kill -0 $(cat $PIDFILE 2>/dev/null) > /dev/null 2>&1
+ RETVAL=$?
+ if [ $RETVAL -eq 0 ]
+ then
+ echo "DavMail gateway (pid $(cat $PIDFILE 2>/dev/null)) is running..."
+ else
+ if [ -f $PIDFILE ]
+ then
+ echo "DavMail gateway dead but pid file exists"
+ RETVAL=1
+ return
+ fi
+ if [ -f $LOCKFILE ]
+ then
+ echo "DavMail gateway dead but subsys locked"
+ RETVAL=2
+ return
+ fi
+ echo "DavMail gateway is stopped"
+ RETVAL=3
+ fi
+}
+
+# See how we were called.
+case "$1" in
+ start)
+ start
+ ;;
+ stop)
+ stop
+ ;;
+ status)
+ dostatus
+ ;;
+ restart|reload)
+ restart
+ ;;
+ condrestart)
+ condrestart
+ ;;
+ *)
+ echo $"Usage: $0 {start|stop|status|restart|reload|condrestart}"
+ exit 1
+esac
+
+exit $RETVAL
diff --git a/src/contribs/rpm/SOURCES/davmail-logrotate b/src/contribs/rpm/SOURCES/davmail-logrotate
new file mode 100644
index 0000000..a88f0ae
--- /dev/null
+++ b/src/contribs/rpm/SOURCES/davmail-logrotate
@@ -0,0 +1,7 @@
+/var/log/davmail.log {
+ missingok
+ notifempty
+ size 30k
+ monthly
+ create 0640 davmail davmail
+}
diff --git a/src/contribs/rpm/SOURCES/davmail-wrapper b/src/contribs/rpm/SOURCES/davmail-wrapper
new file mode 100644
index 0000000..96a726e
--- /dev/null
+++ b/src/contribs/rpm/SOURCES/davmail-wrapper
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+LOGFILE=/var/log/davmail.log
+PIDFILE=/var/lib/davmail/pid
+
+for class in /usr/share/davmail/lib/*
+do
+ export CLASSPATH=${CLASSPATH}:${class}
+done
+
+echo $$ > $PIDFILE
+exec java -cp /usr/share/davmail/davmail.jar:${CLASSPATH} \
+ davmail.DavGateway "$@" >> $LOGFILE 2>&1
diff --git a/src/contribs/rpm/SOURCES/davmail.desktop b/src/contribs/rpm/SOURCES/davmail.desktop
new file mode 100644
index 0000000..ac4916b
--- /dev/null
+++ b/src/contribs/rpm/SOURCES/davmail.desktop
@@ -0,0 +1,10 @@
+[Desktop Entry]
+Version=1.0
+Type=Application
+Terminal=false
+Encoding=UTF-8
+Name=DavMail
+Icon=davmail
+Exec=/usr/bin/davmail
+Comment=DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway
+Categories=Application;Office;Email;Network;Calendar;ContactManagement;
diff --git a/src/contribs/rpm/SOURCES/davmail.properties b/src/contribs/rpm/SOURCES/davmail.properties
new file mode 100644
index 0000000..e703b91
--- /dev/null
+++ b/src/contribs/rpm/SOURCES/davmail.properties
@@ -0,0 +1,26 @@
+# DavMail settings
+davmail.url=https\://owa.example.com/owa/
+davmail.enableEws=true
+davmail.ldapPort=1389
+davmail.caldavPort=1080
+davmail.smtpPort=1025
+davmail.popPort=1110
+davmail.imapPort=1143
+davmail.proxyPort=
+davmail.disableUpdateCheck=true
+davmail.bindAddress=127.0.0.1
+davmail.logFilePath=/var/log/davmail.log
+davmail.server=true
+davmail.server.certificate.hash=
+davmail.caldavPastDelay=90
+davmail.sentKeepDelay=90
+davmail.keepDelay=30
+davmail.allowRemote=false
+davmail.enableProxy=false
+davmail.proxyHost=
+davmail.proxyPassword=
+davmail.proxyUser=
+log4j.logger.davmail=WARN
+log4j.logger.httpclient.wire=WARN
+log4j.logger.org.apache.commons.httpclient=WARN
+log4j.rootLogger=WARN
diff --git a/src/contribs/rpm/SOURCES/davmail.sh b/src/contribs/rpm/SOURCES/davmail.sh
new file mode 100644
index 0000000..44b45db
--- /dev/null
+++ b/src/contribs/rpm/SOURCES/davmail.sh
@@ -0,0 +1,19 @@
+#!/bin/sh
+#
+# Usage: davmail [</path/to/davmail.properties>]
+#
+
+# Create the $HOME/.davmail.properties if necessary
+if [ -r /etc/davmail.properties ]; then
+ if [ ! -f ${HOME}/.davmail.properties ]; then
+ grep -v ^davmail.logFilePath /etc/davmail.properties | \
+ sed -e 's/^davmail.server=true/davmail.server=false/' > \
+ ${HOME}/.davmail.properties
+ fi
+fi
+
+# Add our libs into CLASSPATH
+for i in /usr/share/davmail/lib/*; do export CLASSPATH=${CLASSPATH}:${i}; done
+
+# Start davmail
+java -cp /usr/share/davmail/davmail.jar:${CLASSPATH} davmail.DavGateway $*
diff --git a/src/contribs/rpm/SPECS/davmail.spec b/src/contribs/rpm/SPECS/davmail.spec
new file mode 100644
index 0000000..2eb570e
--- /dev/null
+++ b/src/contribs/rpm/SPECS/davmail.spec
@@ -0,0 +1,116 @@
+%define davrel 3.8.5
+%define davsvn 1480
+%define davver %{davrel}-%{davsvn}
+%ifarch i686
+%define davarch x86
+%endif
+%ifarch x86_64
+%define davarch x86_64
+%endif
+
+Summary: DavMail is a POP/IMAP/SMTP/Caldav/Carddav/LDAP gateway for Microsoft Exchange
+Name: davmail
+Version: %{davrel}
+Release: 1%{?dist}
+License: GPL
+Group: Applications/Internet
+BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
+BuildRequires: ant >= 1.7.1, ant-antlr, desktop-file-utils
+Requires(pre): chkconfig, coreutils, initscripts, shadow-utils
+Requires: logrotate, jre
+
+Source0: %{name}-src-%{davver}.tgz
+Source1: davmail.sh
+Source2: davmail-logrotate
+Source3: davmail-init
+Source4: davmail.properties
+Source5: davmail.desktop
+Source6: davmail-wrapper
+
+%description
+DavMail is a POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange gateway allowing
+users to use any mail/calendar client with an Exchange server, even from
+the internet or behind a firewall through Outlook Web Access. DavMail
+now includes an LDAP gateway to Exchange global address book and user
+personal contacts to allow recipient address completion in mail compose
+window and full calendar support with attendees free/busy display.
+
+%prep
+%setup -q -n %{name}-src-%{davver}
+
+%build
+export JAVA_HOME=/etc/alternatives/java_sdk
+ant
+
+%install
+rm -rf $RPM_BUILD_ROOT
+mkdir -p $RPM_BUILD_ROOT/%{_bindir}
+mkdir -p $RPM_BUILD_ROOT/%{_sysconfdir}/logrotate.d
+mkdir -p $RPM_BUILD_ROOT/%{_sysconfdir}/rc.d/init.d
+mkdir -p $RPM_BUILD_ROOT/%{_datadir}/applications
+mkdir -p $RPM_BUILD_ROOT/%{_datadir}/pixmaps
+mkdir -p $RPM_BUILD_ROOT/%{_datadir}/davmail/lib
+mkdir -p $RPM_BUILD_ROOT/%{_localstatedir}/lib/davmail
+mkdir -p $RPM_BUILD_ROOT/%{_localstatedir}/log
+
+# Init scripts, icons, configurations
+install -m 0775 %{SOURCE1} $RPM_BUILD_ROOT/%{_bindir}/davmail
+install -m 0644 %{SOURCE2} $RPM_BUILD_ROOT/%{_sysconfdir}/logrotate.d/davmail
+install -m 0775 %{SOURCE3} $RPM_BUILD_ROOT/%{_sysconfdir}/rc.d/init.d/davmail
+install -m 0644 %{SOURCE4} $RPM_BUILD_ROOT/%{_sysconfdir}
+desktop-file-install --dir $RPM_BUILD_ROOT/%{_datadir}/applications/ %{SOURCE5}
+install -m 0775 %{SOURCE6} $RPM_BUILD_ROOT/%{_localstatedir}/lib/davmail/davmail
+
+# Actual DavMail files
+install -m 0644 src/java/tray32.png $RPM_BUILD_ROOT/%{_datadir}/pixmaps/davmail.png
+rm -f dist/lib/*win32*.jar
+install -m 0664 dist/lib/*-%{davarch}.jar $RPM_BUILD_ROOT/%{_datadir}/davmail/lib/
+rm -f dist/lib/*x86*.jar
+install -m 0664 dist/lib/* $RPM_BUILD_ROOT/%{_datadir}/davmail/lib/
+install -m 0664 dist/*.jar $RPM_BUILD_ROOT/%{_datadir}/davmail/
+
+%clean
+rm -rf $RPM_BUILD_ROOT
+
+%pre
+/usr/sbin/groupadd -r davmail > /dev/null 2>&1 || :
+/usr/sbin/useradd -r -s /sbin/nologin -d /var/lib/davmail -M \
+ -g davmail davmail > /dev/null 2>&1 || :
+
+%post
+if [ ! -f /var/log/davmail.log ]
+then
+ /bin/touch /var/log/davmail.log
+fi
+/bin/chown davmail:davmail /var/log/davmail.log
+/bin/chmod 0640 /var/log/davmail.log
+/sbin/chkconfig --add davmail
+#/sbin/chkconfig davmail on
+
+%preun
+if [ "$1" = "0" ]; then
+ /sbin/service davmail stop > /dev/null 2>&1 || :
+ /bin/rm -f /var/lib/davmail/pid > /dev/null 2>&1 || :
+ /sbin/chkconfig davmail off
+ /sbin/chkconfig --del davmail
+fi
+
+%postun
+if [ $1 -ge 1 ]; then
+ /sbin/service davmail condrestart > /dev/null 2>&1 || :
+fi
+
+%files
+%defattr (-,root,root,-)
+%{_bindir}/*
+%{_sysconfdir}/rc.d/init.d/davmail
+%{_sysconfdir}/logrotate.d/davmail
+%{_sysconfdir}/davmail.properties
+%{_datadir}/applications/*
+%{_datadir}/pixmaps/*
+%{_datadir}/davmail/
+%attr(0775,davmail,davmail) %{_localstatedir}/lib/davmail
+
+%changelog
+* Mon Oct 18 2010 Marko Myllynen <myllynen at redhat.com>
+- Initial version
diff --git a/src/contribs/wrapper/bin/davmail b/src/contribs/wrapper/bin/davmail
new file mode 100644
index 0000000..59ad632
--- /dev/null
+++ b/src/contribs/wrapper/bin/davmail
@@ -0,0 +1,732 @@
+#! /bin/sh
+
+#
+# Copyright (c) 1999, 2009 Tanuki Software, Ltd.
+# http://www.tanukisoftware.com
+# All rights reserved.
+#
+# This software is the proprietary information of Tanuki Software.
+# You shall use it only in accordance with the terms of the
+# license agreement you entered into with Tanuki Software.
+# http://wrapper.tanukisoftware.org/doc/english/licenseOverview.html
+#
+# Java Service Wrapper sh script. Suitable for starting and stopping
+# wrapped Java applications on UNIX platforms.
+#
+
+#-----------------------------------------------------------------------------
+# These settings can be modified to fit the needs of your application
+# Optimized for use with version 3.3.6 of the Wrapper.
+
+# Application
+BASEDIR="/opt/davmail/bin"
+APP_NAME="davmail"
+APP_LONG_NAME="DavMail Exchange Gateway"
+
+# Wrapper
+WRAPPER_CMD="$BASEDIR/wrapper"
+WRAPPER_CONF="$BASEDIR/../conf/wrapper.conf"
+
+# Priority at which to run the wrapper. See "man nice" for valid priorities.
+# nice is only used if a priority is specified.
+PRIORITY=
+
+# Location of the pid file.
+PIDDIR="$BASEDIR"
+
+# If uncommented, causes the Wrapper to be shutdown using an anchor file.
+# When launched with the 'start' command, it will also ignore all INT and
+# TERM signals.
+#IGNORE_SIGNALS=true
+
+# Wrapper will start the JVM asynchronously. Your application may have some
+# initialization tasks and it may be desirable to wait a few seconds
+# before returning. For example, to delay the invocation of following
+# startup scripts. Setting WAIT_AFTER_STARTUP to a positive number will
+# cause the start command to delay for the indicated period of time
+# (in seconds).
+#
+WAIT_AFTER_STARTUP=0
+
+# If set, the status, start_msg and stop_msg commands will print out detailed
+# state information on the Wrapper and Java processes.
+#DETAIL_STATUS=true
+
+# If specified, the Wrapper will be run as the specified user.
+# IMPORTANT - Make sure that the user has the required privileges to write
+# the PID file and wrapper.log files. Failure to be able to write the log
+# file will cause the Wrapper to exit without any way to write out an error
+# message.
+# NOTE - This will set the user which is used to run the Wrapper as well as
+# the JVM and is not useful in situations where a privileged resource or
+# port needs to be allocated prior to the user being changed.
+#RUN_AS_USER=
+
+# The following two lines are used by the chkconfig command. Change as is
+# appropriate for your application. They should remain commented.
+# chkconfig: 2345 20 80
+# description: @app.long.name@
+
+# Initialization block for the install_initd and remove_initd scripts used by
+# SUSE linux distributions.
+### BEGIN INIT INFO
+# Provides: @app.name@
+# Required-Start: $local_fs $network $syslog
+# Should-Start:
+# Required-Stop:
+# Default-Start: 2 3 4 5
+# Default-Stop: 0 1 6
+# Short-Description: @app.long.name@
+# Description: @app.description@
+### END INIT INFO
+
+# Do not modify anything beyond this point
+#-----------------------------------------------------------------------------
+
+# Get the fully qualified path to the script
+case $0 in
+ /*)
+ SCRIPT="$0"
+ ;;
+ *)
+ PWD=`pwd`
+ SCRIPT="$PWD/$0"
+ ;;
+esac
+
+# Resolve the true real path without any sym links.
+CHANGED=true
+while [ "X$CHANGED" != "X" ]
+do
+ # Change spaces to ":" so the tokens can be parsed.
+ SAFESCRIPT=`echo $SCRIPT | sed -e 's; ;:;g'`
+ # Get the real path to this script, resolving any symbolic links
+ TOKENS=`echo $SAFESCRIPT | sed -e 's;/; ;g'`
+ REALPATH=
+ for C in $TOKENS; do
+ # Change any ":" in the token back to a space.
+ C=`echo $C | sed -e 's;:; ;g'`
+ REALPATH="$REALPATH/$C"
+ # If REALPATH is a sym link, resolve it. Loop for nested links.
+ while [ -h "$REALPATH" ] ; do
+ LS="`ls -ld "$REALPATH"`"
+ LINK="`expr "$LS" : '.*-> \(.*\)$'`"
+ if expr "$LINK" : '/.*' > /dev/null; then
+ # LINK is absolute.
+ REALPATH="$LINK"
+ else
+ # LINK is relative.
+ REALPATH="`dirname "$REALPATH"`""/$LINK"
+ fi
+ done
+ done
+
+ if [ "$REALPATH" = "$SCRIPT" ]
+ then
+ CHANGED=""
+ else
+ SCRIPT="$REALPATH"
+ fi
+done
+
+# Change the current directory to the location of the script
+cd "`dirname "$REALPATH"`"
+REALDIR=`pwd`
+
+# If the PIDDIR is relative, set its value relative to the full REALPATH to avoid problems if
+# the working directory is later changed.
+FIRST_CHAR=`echo $PIDDIR | cut -c1,1`
+if [ "$FIRST_CHAR" != "/" ]
+then
+ PIDDIR=$REALDIR/$PIDDIR
+fi
+# Same test for WRAPPER_CMD
+FIRST_CHAR=`echo $WRAPPER_CMD | cut -c1,1`
+if [ "$FIRST_CHAR" != "/" ]
+then
+ WRAPPER_CMD=$REALDIR/$WRAPPER_CMD
+fi
+# Same test for WRAPPER_CONF
+FIRST_CHAR=`echo $WRAPPER_CONF | cut -c1,1`
+if [ "$FIRST_CHAR" != "/" ]
+then
+ WRAPPER_CONF=$REALDIR/$WRAPPER_CONF
+fi
+
+# Process ID
+ANCHORFILE="$PIDDIR/$APP_NAME.anchor"
+STATUSFILE="$PIDDIR/$APP_NAME.status"
+JAVASTATUSFILE="$PIDDIR/$APP_NAME.java.status"
+PIDFILE="$PIDDIR/$APP_NAME.pid"
+LOCKDIR="/var/lock/subsys"
+LOCKFILE="$LOCKDIR/$APP_NAME"
+pid=""
+
+# Resolve the location of the 'ps' command
+PSEXE="/usr/ucb/ps"
+ if [ ! -x "$PSEXE" ]
+ then
+ PSEXE="/usr/bin/ps"
+ if [ ! -x "$PSEXE" ]
+ then
+ PSEXE="/bin/ps"
+ if [ ! -x "$PSEXE" ]
+ then
+ echo "Unable to locate 'ps'."
+ echo "Please report this message along with the location of the command on your system."
+ exit 1
+ fi
+ fi
+ fi
+
+# Resolve the os
+DIST_OS=`uname -s | tr [:upper:] [:lower:] | tr -d [:blank:]`
+case "$DIST_OS" in
+ 'sunos')
+ DIST_OS="solaris"
+ ;;
+ 'hp-ux' | 'hp-ux64')
+ # HP-UX needs the XPG4 version of ps (for -o args)
+ DIST_OS="hpux"
+ UNIX95=""
+ export UNIX95
+ ;;
+ 'darwin')
+ DIST_OS="macosx"
+ ;;
+ 'unix_sv')
+ DIST_OS="unixware"
+ ;;
+esac
+
+# Resolve the architecture
+if [ "$DIST_OS" = "macosx" ]
+then
+ DIST_ARCH="universal"
+else
+ DIST_ARCH=
+ DIST_ARCH=`uname -p 2>/dev/null | tr [:upper:] [:lower:] | tr -d [:blank:]`
+ if [ "X$DIST_ARCH" = "X" ]
+ then
+ DIST_ARCH="unknown"
+ fi
+ if [ "$DIST_ARCH" = "unknown" ]
+ then
+ DIST_ARCH=`uname -m 2>/dev/null | tr [:upper:] [:lower:] | tr -d [:blank:]`
+ fi
+ case "$DIST_ARCH" in
+ 'amd64' | 'athlon' | 'i386' | 'i486' | 'i586' | 'i686' | 'x86_64')
+ DIST_ARCH="x86"
+ ;;
+ 'ia32' | 'ia64' | 'ia64n' | 'ia64w')
+ DIST_ARCH="ia"
+ ;;
+ 'ip27')
+ DIST_ARCH="mips"
+ ;;
+ 'power' | 'powerpc' | 'power_pc' | 'ppc64')
+ DIST_ARCH="ppc"
+ ;;
+ 'pa_risc' | 'pa-risc')
+ DIST_ARCH="parisc"
+ ;;
+ 'sun4u' | 'sparcv9')
+ DIST_ARCH="sparc"
+ ;;
+ '9000/800')
+ DIST_ARCH="parisc"
+ ;;
+ esac
+fi
+
+# OSX always places Java in the same location so we can reliably set JAVA_HOME
+if [ "$DIST_OS" = "macosx" ]
+then
+ if [ -z "$JAVA_HOME" ]; then
+ JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
+ fi
+fi
+
+outputFile() {
+ if [ -f "$1" ]
+ then
+ echo " $1 (Found but not executable.)";
+ else
+ echo " $1"
+ fi
+}
+
+# Decide on the wrapper binary to use.
+# If a 32-bit wrapper binary exists then it will work on 32 or 64 bit
+# platforms, if the 64-bit binary exists then the distribution most
+# likely wants to use long names. Otherwise, look for the default.
+WRAPPER_TEST_CMD="$WRAPPER_CMD-$DIST_OS-$DIST_ARCH-32"
+if [ -x "$WRAPPER_TEST_CMD" ]
+then
+ WRAPPER_CMD="$WRAPPER_TEST_CMD"
+else
+ WRAPPER_TEST_CMD="$WRAPPER_CMD-$DIST_OS-$DIST_ARCH-64"
+ if [ -x "$WRAPPER_TEST_CMD" ]
+ then
+ WRAPPER_CMD="$WRAPPER_TEST_CMD"
+ else
+ if [ ! -x "$WRAPPER_CMD" ]
+ then
+ echo "Unable to locate any of the following binaries:"
+ outputFile "$WRAPPER_CMD-$DIST_OS-$DIST_ARCH-32"
+ outputFile "$WRAPPER_CMD-$DIST_OS-$DIST_ARCH-64"
+ outputFile "$WRAPPER_CMD"
+ exit 1
+ fi
+ fi
+fi
+
+# Build the nice clause
+if [ "X$PRIORITY" = "X" ]
+then
+ CMDNICE=""
+else
+ CMDNICE="nice -$PRIORITY"
+fi
+
+# Build the anchor file clause.
+if [ "X$IGNORE_SIGNALS" = "X" ]
+then
+ ANCHORPROP=
+ IGNOREPROP=
+else
+ ANCHORPROP=wrapper.anchorfile=\"$ANCHORFILE\"
+ IGNOREPROP=wrapper.ignore_signals=TRUE
+fi
+
+# Build the status file clause.
+if [ "X$DETAIL_STATUS" = "X" ]
+then
+ STATUSPROP=
+else
+ STATUSPROP="wrapper.statusfile=\"$STATUSFILE\" wrapper.java.statusfile=\"$JAVASTATUSFILE\""
+fi
+
+# Build the lock file clause. Only create a lock file if the lock directory exists on this platform.
+LOCKPROP=
+if [ -d $LOCKDIR ]
+then
+ if [ -w $LOCKDIR ]
+ then
+ LOCKPROP=wrapper.lockfile=\"$LOCKFILE\"
+ fi
+fi
+
+checkUser() {
+ # $1 touchLock flag
+ # $2 command
+
+ # Check the configured user. If necessary rerun this script as the desired user.
+ if [ "X$RUN_AS_USER" != "X" ]
+ then
+ # Resolve the location of the 'id' command
+ IDEXE="/usr/xpg4/bin/id"
+ if [ ! -x "$IDEXE" ]
+ then
+ IDEXE="/usr/bin/id"
+ if [ ! -x "$IDEXE" ]
+ then
+ echo "Unable to locate 'id'."
+ echo "Please report this message along with the location of the command on your system."
+ exit 1
+ fi
+ fi
+
+ if [ "`$IDEXE -u -n`" = "$RUN_AS_USER" ]
+ then
+ # Already running as the configured user. Avoid password prompts by not calling su.
+ RUN_AS_USER=""
+ fi
+ fi
+ if [ "X$RUN_AS_USER" != "X" ]
+ then
+ # If LOCKPROP and $RUN_AS_USER are defined then the new user will most likely not be
+ # able to create the lock file. The Wrapper will be able to update this file once it
+ # is created but will not be able to delete it on shutdown. If $2 is defined then
+ # the lock file should be created for the current command
+ if [ "X$LOCKPROP" != "X" ]
+ then
+ if [ "X$1" != "X" ]
+ then
+ # Resolve the primary group
+ RUN_AS_GROUP=`groups $RUN_AS_USER | awk '{print $3}' | tail -1`
+ if [ "X$RUN_AS_GROUP" = "X" ]
+ then
+ RUN_AS_GROUP=$RUN_AS_USER
+ fi
+ touch $LOCKFILE
+ chown $RUN_AS_USER:$RUN_AS_GROUP $LOCKFILE
+ fi
+ fi
+
+ # Still want to change users, recurse. This means that the user will only be
+ # prompted for a password once. Variables shifted by 1
+ #
+ # Use "runuser" if this exists. runuser should be used on RedHat in preference to su.
+ #
+ if test -f "/sbin/runuser"
+ then
+ /sbin/runuser - $RUN_AS_USER -c "\"$REALPATH\" $2"
+ else
+ su - $RUN_AS_USER -c "\"$REALPATH\" $2"
+ fi
+
+ # Now that we are the original user again, we may need to clean up the lock file.
+ if [ "X$LOCKPROP" != "X" ]
+ then
+ getpid
+ if [ "X$pid" = "X" ]
+ then
+ # Wrapper is not running so make sure the lock file is deleted.
+ if [ -f "$LOCKFILE" ]
+ then
+ rm "$LOCKFILE"
+ fi
+ fi
+ fi
+
+ exit 0
+ fi
+}
+
+getpid() {
+ pid=""
+ if [ -f "$PIDFILE" ]
+ then
+ if [ -r "$PIDFILE" ]
+ then
+ pid=`cat "$PIDFILE"`
+ if [ "X$pid" != "X" ]
+ then
+ # It is possible that 'a' process with the pid exists but that it is not the
+ # correct process. This can happen in a number of cases, but the most
+ # common is during system startup after an unclean shutdown.
+ # The ps statement below looks for the specific wrapper command running as
+ # the pid. If it is not found then the pid file is considered to be stale.
+ case "$DIST_OS" in
+ 'macosx')
+ pidtest=`$PSEXE -ww -p $pid -o command | grep "$WRAPPER_CMD" | tail -1`
+ ;;
+ 'solaris')
+ pidtest=`$PSEXE -auxww $pid | grep "$WRAPPER_CMD" | tail -1`
+ ;;
+ 'hpux')
+ pidtest=`$PSEXE -p $pid -x -o args | grep "$WRAPPER_CMD" | tail -1`
+ ;;
+ *)
+ pidtest=`$PSEXE -p $pid -o args | grep "$WRAPPER_CMD" | tail -1`
+ ;;
+ esac
+
+ if [ "X$pidtest" = "X" ]
+ then
+ # This is a stale pid file.
+ rm -f "$PIDFILE"
+ echo "Removed stale pid file: $PIDFILE"
+ pid=""
+ fi
+ fi
+ else
+ echo "Cannot read $PIDFILE."
+ exit 1
+ fi
+ fi
+}
+
+getstatus() {
+ STATUS=
+ if [ -f "$STATUSFILE" ]
+ then
+ if [ -r "$STATUSFILE" ]
+ then
+ STATUS=`cat "$STATUSFILE"`
+ fi
+ fi
+ if [ "X$STATUS" = "X" ]
+ then
+ STATUS="Unknown"
+ fi
+
+ JAVASTATUS=
+ if [ -f "$JAVASTATUSFILE" ]
+ then
+ if [ -r "$JAVASTATUSFILE" ]
+ then
+ JAVASTATUS=`cat "$JAVASTATUSFILE"`
+ fi
+ fi
+ if [ "X$JAVASTATUS" = "X" ]
+ then
+ JAVASTATUS="Unknown"
+ fi
+}
+
+testpid() {
+ case "$DIST_OS" in
+ 'solaris')
+ pid=`$PSEXE $pid | grep $pid | grep -v grep | awk '{print $1}' | tail -1`
+ ;;
+ *)
+ pid=`$PSEXE -p $pid | grep $pid | grep -v grep | awk '{print $1}' | tail -1`
+ ;;
+ esac
+ if [ "X$pid" = "X" ]
+ then
+ # Process is gone so remove the pid file.
+ rm -f "$PIDFILE"
+ pid=""
+ fi
+}
+
+console() {
+ echo "Running $APP_LONG_NAME..."
+ getpid
+ if [ "X$pid" = "X" ]
+ then
+ # The string passed to eval must handles spaces in paths correctly.
+ COMMAND_LINE="$CMDNICE \"$WRAPPER_CMD\" \"$WRAPPER_CONF\" wrapper.syslog.ident=\"$APP_NAME\" wrapper.pidfile=\"$PIDFILE\" wrapper.name=\"$APP_NAME\" wrapper.displayname=\"$APP_LONG_NAME\" $ANCHORPROP $STATUSPROP $LOCKPROP"
+ eval $COMMAND_LINE
+ else
+ echo "$APP_LONG_NAME is already running."
+ exit 1
+ fi
+}
+
+start() {
+ echo -n "Starting $APP_LONG_NAME..."
+ getpid
+ if [ "X$pid" = "X" ]
+ then
+ # The string passed to eval must handles spaces in paths correctly.
+ COMMAND_LINE="$CMDNICE \"$WRAPPER_CMD\" \"$WRAPPER_CONF\" wrapper.syslog.ident=\"$APP_NAME\" wrapper.pidfile=\"$PIDFILE\" wrapper.name=\"$APP_NAME\" wrapper.displayname=\"$APP_LONG_NAME\" wrapper.daemonize=TRUE $ANCHORPROP $IGNOREPROP $STATUSPROP $LOCKPROP"
+ eval $COMMAND_LINE
+ else
+ echo "$APP_LONG_NAME is already running."
+ exit 1
+ fi
+
+ # Sleep for a few seconds to allow for intialization if required
+ # then test to make sure we're still running.
+ #
+ i=0
+ while [ $i -lt $WAIT_AFTER_STARTUP ]
+ do
+ sleep 1
+ echo -n "."
+ i=`expr $i + 1`
+ done
+ if [ $WAIT_AFTER_STARTUP -gt 0 ]
+ then
+ getpid
+ if [ "X$pid" = "X" ]
+ then
+ echo " WARNING: $APP_LONG_NAME may have failed to start."
+ exit 1
+ else
+ echo " running ($pid)."
+ fi
+ else
+ echo ""
+ fi
+}
+
+stopit() {
+ # $1 exit if down flag
+
+ echo "Stopping $APP_LONG_NAME..."
+ getpid
+ if [ "X$pid" = "X" ]
+ then
+ echo "$APP_LONG_NAME was not running."
+ if [ "X$1" = "X1" ]
+ then
+ exit 1
+ fi
+ else
+ if [ "X$IGNORE_SIGNALS" = "X" ]
+ then
+ # Running so try to stop it.
+ kill $pid
+ if [ $? -ne 0 ]
+ then
+ # An explanation for the failure should have been given
+ echo "Unable to stop $APP_LONG_NAME."
+ exit 1
+ fi
+ else
+ rm -f "$ANCHORFILE"
+ if [ -f "$ANCHORFILE" ]
+ then
+ # An explanation for the failure should have been given
+ echo "Unable to stop $APP_LONG_NAME."
+ exit 1
+ fi
+ fi
+
+ # We can not predict how long it will take for the wrapper to
+ # actually stop as it depends on settings in wrapper.conf.
+ # Loop until it does.
+ savepid=$pid
+ CNT=0
+ TOTCNT=0
+ while [ "X$pid" != "X" ]
+ do
+ # Show a waiting message every 5 seconds.
+ if [ "$CNT" -lt "5" ]
+ then
+ CNT=`expr $CNT + 1`
+ else
+ echo "Waiting for $APP_LONG_NAME to exit..."
+ CNT=0
+ fi
+ TOTCNT=`expr $TOTCNT + 1`
+
+ sleep 1
+
+ testpid
+ done
+
+ pid=$savepid
+ testpid
+ if [ "X$pid" != "X" ]
+ then
+ echo "Failed to stop $APP_LONG_NAME."
+ exit 1
+ else
+ echo "Stopped $APP_LONG_NAME."
+ fi
+ fi
+}
+
+status() {
+ getpid
+ if [ "X$pid" = "X" ]
+ then
+ echo "$APP_LONG_NAME is not running."
+ exit 1
+ else
+ if [ "X$DETAIL_STATUS" = "X" ]
+ then
+ echo "$APP_LONG_NAME is running (PID:$pid)."
+ else
+ getstatus
+ echo "$APP_LONG_NAME is running (PID:$pid, Wrapper:$STATUS, Java:$JAVASTATUS)"
+ fi
+ exit 0
+ fi
+}
+
+dump() {
+ echo "Dumping $APP_LONG_NAME..."
+ getpid
+ if [ "X$pid" = "X" ]
+ then
+ echo "$APP_LONG_NAME was not running."
+ else
+ kill -3 $pid
+
+ if [ $? -ne 0 ]
+ then
+ echo "Failed to dump $APP_LONG_NAME."
+ exit 1
+ else
+ echo "Dumped $APP_LONG_NAME."
+ fi
+ fi
+}
+
+# Used by HP-UX init scripts.
+startmsg() {
+ getpid
+ if [ "X$pid" = "X" ]
+ then
+ echo "Starting $APP_LONG_NAME... (Wrapper:Stopped)"
+ else
+ if [ "X$DETAIL_STATUS" = "X" ]
+ then
+ echo "Starting $APP_LONG_NAME... (Wrapper:Running)"
+ else
+ getstatus
+ echo "Starting $APP_LONG_NAME... (Wrapper:$STATUS, Java:$JAVASTATUS)"
+ fi
+ fi
+}
+
+# Used by HP-UX init scripts.
+stopmsg() {
+ getpid
+ if [ "X$pid" = "X" ]
+ then
+ echo "Stopping $APP_LONG_NAME... (Wrapper:Stopped)"
+ else
+ if [ "X$DETAIL_STATUS" = "X" ]
+ then
+ echo "Stopping $APP_LONG_NAME... (Wrapper:Running)"
+ else
+ getstatus
+ echo "Stopping $APP_LONG_NAME... (Wrapper:$STATUS, Java:$JAVASTATUS)"
+ fi
+ fi
+}
+
+case "$1" in
+
+ 'console')
+ checkUser touchlock $1
+ console
+ ;;
+
+ 'start')
+ checkUser touchlock $1
+ start
+ ;;
+
+ 'stop')
+ checkUser "" $1
+ stopit "0"
+ ;;
+
+ 'restart')
+ checkUser touchlock $1
+ stopit "0"
+ start
+ ;;
+
+ 'condrestart')
+ checkUser touchlock $1
+ stopit "1"
+ start
+ ;;
+
+ 'status')
+ checkUser "" $1
+ status
+ ;;
+
+ 'dump')
+ checkUser "" $1
+ dump
+ ;;
+
+ 'start_msg')
+ checkUser "" $1
+ startmsg
+ ;;
+
+ 'stop_msg')
+ checkUser "" $1
+ stopmsg
+ ;;
+
+ *)
+ echo "Usage: $0 { console | start | stop | restart | condrestart | status | dump }"
+ exit 1
+ ;;
+esac
+
+exit 0
diff --git a/src/contribs/wrapper/bin/davmail_2 b/src/contribs/wrapper/bin/davmail_2
new file mode 100644
index 0000000..b6603b2
--- /dev/null
+++ b/src/contribs/wrapper/bin/davmail_2
@@ -0,0 +1,732 @@
+#! /bin/sh
+
+#
+# Copyright (c) 1999, 2009 Tanuki Software, Ltd.
+# http://www.tanukisoftware.com
+# All rights reserved.
+#
+# This software is the proprietary information of Tanuki Software.
+# You shall use it only in accordance with the terms of the
+# license agreement you entered into with Tanuki Software.
+# http://wrapper.tanukisoftware.org/doc/english/licenseOverview.html
+#
+# Java Service Wrapper sh script. Suitable for starting and stopping
+# wrapped Java applications on UNIX platforms.
+#
+
+#-----------------------------------------------------------------------------
+# These settings can be modified to fit the needs of your application
+# Optimized for use with version 3.3.6 of the Wrapper.
+
+# Application
+BASEDIR="/opt/davmail/bin"
+APP_NAME="davmail_2"
+APP_LONG_NAME="DavMail Exchange Gateway #2"
+
+# Wrapper
+WRAPPER_CMD="$BASEDIR/wrapper"
+WRAPPER_CONF="$BASEDIR/../conf/wrapper.conf_2"
+
+# Priority at which to run the wrapper. See "man nice" for valid priorities.
+# nice is only used if a priority is specified.
+PRIORITY=
+
+# Location of the pid file.
+PIDDIR="$BASEDIR"
+
+# If uncommented, causes the Wrapper to be shutdown using an anchor file.
+# When launched with the 'start' command, it will also ignore all INT and
+# TERM signals.
+#IGNORE_SIGNALS=true
+
+# Wrapper will start the JVM asynchronously. Your application may have some
+# initialization tasks and it may be desirable to wait a few seconds
+# before returning. For example, to delay the invocation of following
+# startup scripts. Setting WAIT_AFTER_STARTUP to a positive number will
+# cause the start command to delay for the indicated period of time
+# (in seconds).
+#
+WAIT_AFTER_STARTUP=0
+
+# If set, the status, start_msg and stop_msg commands will print out detailed
+# state information on the Wrapper and Java processes.
+#DETAIL_STATUS=true
+
+# If specified, the Wrapper will be run as the specified user.
+# IMPORTANT - Make sure that the user has the required privileges to write
+# the PID file and wrapper.log files. Failure to be able to write the log
+# file will cause the Wrapper to exit without any way to write out an error
+# message.
+# NOTE - This will set the user which is used to run the Wrapper as well as
+# the JVM and is not useful in situations where a privileged resource or
+# port needs to be allocated prior to the user being changed.
+#RUN_AS_USER=
+
+# The following two lines are used by the chkconfig command. Change as is
+# appropriate for your application. They should remain commented.
+# chkconfig: 2345 20 80
+# description: @app.long.name@
+
+# Initialization block for the install_initd and remove_initd scripts used by
+# SUSE linux distributions.
+### BEGIN INIT INFO
+# Provides: @app.name@
+# Required-Start: $local_fs $network $syslog
+# Should-Start:
+# Required-Stop:
+# Default-Start: 2 3 4 5
+# Default-Stop: 0 1 6
+# Short-Description: @app.long.name@
+# Description: @app.description@
+### END INIT INFO
+
+# Do not modify anything beyond this point
+#-----------------------------------------------------------------------------
+
+# Get the fully qualified path to the script
+case $0 in
+ /*)
+ SCRIPT="$0"
+ ;;
+ *)
+ PWD=`pwd`
+ SCRIPT="$PWD/$0"
+ ;;
+esac
+
+# Resolve the true real path without any sym links.
+CHANGED=true
+while [ "X$CHANGED" != "X" ]
+do
+ # Change spaces to ":" so the tokens can be parsed.
+ SAFESCRIPT=`echo $SCRIPT | sed -e 's; ;:;g'`
+ # Get the real path to this script, resolving any symbolic links
+ TOKENS=`echo $SAFESCRIPT | sed -e 's;/; ;g'`
+ REALPATH=
+ for C in $TOKENS; do
+ # Change any ":" in the token back to a space.
+ C=`echo $C | sed -e 's;:; ;g'`
+ REALPATH="$REALPATH/$C"
+ # If REALPATH is a sym link, resolve it. Loop for nested links.
+ while [ -h "$REALPATH" ] ; do
+ LS="`ls -ld "$REALPATH"`"
+ LINK="`expr "$LS" : '.*-> \(.*\)$'`"
+ if expr "$LINK" : '/.*' > /dev/null; then
+ # LINK is absolute.
+ REALPATH="$LINK"
+ else
+ # LINK is relative.
+ REALPATH="`dirname "$REALPATH"`""/$LINK"
+ fi
+ done
+ done
+
+ if [ "$REALPATH" = "$SCRIPT" ]
+ then
+ CHANGED=""
+ else
+ SCRIPT="$REALPATH"
+ fi
+done
+
+# Change the current directory to the location of the script
+cd "`dirname "$REALPATH"`"
+REALDIR=`pwd`
+
+# If the PIDDIR is relative, set its value relative to the full REALPATH to avoid problems if
+# the working directory is later changed.
+FIRST_CHAR=`echo $PIDDIR | cut -c1,1`
+if [ "$FIRST_CHAR" != "/" ]
+then
+ PIDDIR=$REALDIR/$PIDDIR
+fi
+# Same test for WRAPPER_CMD
+FIRST_CHAR=`echo $WRAPPER_CMD | cut -c1,1`
+if [ "$FIRST_CHAR" != "/" ]
+then
+ WRAPPER_CMD=$REALDIR/$WRAPPER_CMD
+fi
+# Same test for WRAPPER_CONF
+FIRST_CHAR=`echo $WRAPPER_CONF | cut -c1,1`
+if [ "$FIRST_CHAR" != "/" ]
+then
+ WRAPPER_CONF=$REALDIR/$WRAPPER_CONF
+fi
+
+# Process ID
+ANCHORFILE="$PIDDIR/$APP_NAME.anchor"
+STATUSFILE="$PIDDIR/$APP_NAME.status"
+JAVASTATUSFILE="$PIDDIR/$APP_NAME.java.status"
+PIDFILE="$PIDDIR/$APP_NAME.pid"
+LOCKDIR="/var/lock/subsys"
+LOCKFILE="$LOCKDIR/$APP_NAME"
+pid=""
+
+# Resolve the location of the 'ps' command
+PSEXE="/usr/ucb/ps"
+ if [ ! -x "$PSEXE" ]
+ then
+ PSEXE="/usr/bin/ps"
+ if [ ! -x "$PSEXE" ]
+ then
+ PSEXE="/bin/ps"
+ if [ ! -x "$PSEXE" ]
+ then
+ echo "Unable to locate 'ps'."
+ echo "Please report this message along with the location of the command on your system."
+ exit 1
+ fi
+ fi
+ fi
+
+# Resolve the os
+DIST_OS=`uname -s | tr [:upper:] [:lower:] | tr -d [:blank:]`
+case "$DIST_OS" in
+ 'sunos')
+ DIST_OS="solaris"
+ ;;
+ 'hp-ux' | 'hp-ux64')
+ # HP-UX needs the XPG4 version of ps (for -o args)
+ DIST_OS="hpux"
+ UNIX95=""
+ export UNIX95
+ ;;
+ 'darwin')
+ DIST_OS="macosx"
+ ;;
+ 'unix_sv')
+ DIST_OS="unixware"
+ ;;
+esac
+
+# Resolve the architecture
+if [ "$DIST_OS" = "macosx" ]
+then
+ DIST_ARCH="universal"
+else
+ DIST_ARCH=
+ DIST_ARCH=`uname -p 2>/dev/null | tr [:upper:] [:lower:] | tr -d [:blank:]`
+ if [ "X$DIST_ARCH" = "X" ]
+ then
+ DIST_ARCH="unknown"
+ fi
+ if [ "$DIST_ARCH" = "unknown" ]
+ then
+ DIST_ARCH=`uname -m 2>/dev/null | tr [:upper:] [:lower:] | tr -d [:blank:]`
+ fi
+ case "$DIST_ARCH" in
+ 'amd64' | 'athlon' | 'i386' | 'i486' | 'i586' | 'i686' | 'x86_64')
+ DIST_ARCH="x86"
+ ;;
+ 'ia32' | 'ia64' | 'ia64n' | 'ia64w')
+ DIST_ARCH="ia"
+ ;;
+ 'ip27')
+ DIST_ARCH="mips"
+ ;;
+ 'power' | 'powerpc' | 'power_pc' | 'ppc64')
+ DIST_ARCH="ppc"
+ ;;
+ 'pa_risc' | 'pa-risc')
+ DIST_ARCH="parisc"
+ ;;
+ 'sun4u' | 'sparcv9')
+ DIST_ARCH="sparc"
+ ;;
+ '9000/800')
+ DIST_ARCH="parisc"
+ ;;
+ esac
+fi
+
+# OSX always places Java in the same location so we can reliably set JAVA_HOME
+if [ "$DIST_OS" = "macosx" ]
+then
+ if [ -z "$JAVA_HOME" ]; then
+ JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
+ fi
+fi
+
+outputFile() {
+ if [ -f "$1" ]
+ then
+ echo " $1 (Found but not executable.)";
+ else
+ echo " $1"
+ fi
+}
+
+# Decide on the wrapper binary to use.
+# If a 32-bit wrapper binary exists then it will work on 32 or 64 bit
+# platforms, if the 64-bit binary exists then the distribution most
+# likely wants to use long names. Otherwise, look for the default.
+WRAPPER_TEST_CMD="$WRAPPER_CMD-$DIST_OS-$DIST_ARCH-32"
+if [ -x "$WRAPPER_TEST_CMD" ]
+then
+ WRAPPER_CMD="$WRAPPER_TEST_CMD"
+else
+ WRAPPER_TEST_CMD="$WRAPPER_CMD-$DIST_OS-$DIST_ARCH-64"
+ if [ -x "$WRAPPER_TEST_CMD" ]
+ then
+ WRAPPER_CMD="$WRAPPER_TEST_CMD"
+ else
+ if [ ! -x "$WRAPPER_CMD" ]
+ then
+ echo "Unable to locate any of the following binaries:"
+ outputFile "$WRAPPER_CMD-$DIST_OS-$DIST_ARCH-32"
+ outputFile "$WRAPPER_CMD-$DIST_OS-$DIST_ARCH-64"
+ outputFile "$WRAPPER_CMD"
+ exit 1
+ fi
+ fi
+fi
+
+# Build the nice clause
+if [ "X$PRIORITY" = "X" ]
+then
+ CMDNICE=""
+else
+ CMDNICE="nice -$PRIORITY"
+fi
+
+# Build the anchor file clause.
+if [ "X$IGNORE_SIGNALS" = "X" ]
+then
+ ANCHORPROP=
+ IGNOREPROP=
+else
+ ANCHORPROP=wrapper.anchorfile=\"$ANCHORFILE\"
+ IGNOREPROP=wrapper.ignore_signals=TRUE
+fi
+
+# Build the status file clause.
+if [ "X$DETAIL_STATUS" = "X" ]
+then
+ STATUSPROP=
+else
+ STATUSPROP="wrapper.statusfile=\"$STATUSFILE\" wrapper.java.statusfile=\"$JAVASTATUSFILE\""
+fi
+
+# Build the lock file clause. Only create a lock file if the lock directory exists on this platform.
+LOCKPROP=
+if [ -d $LOCKDIR ]
+then
+ if [ -w $LOCKDIR ]
+ then
+ LOCKPROP=wrapper.lockfile=\"$LOCKFILE\"
+ fi
+fi
+
+checkUser() {
+ # $1 touchLock flag
+ # $2 command
+
+ # Check the configured user. If necessary rerun this script as the desired user.
+ if [ "X$RUN_AS_USER" != "X" ]
+ then
+ # Resolve the location of the 'id' command
+ IDEXE="/usr/xpg4/bin/id"
+ if [ ! -x "$IDEXE" ]
+ then
+ IDEXE="/usr/bin/id"
+ if [ ! -x "$IDEXE" ]
+ then
+ echo "Unable to locate 'id'."
+ echo "Please report this message along with the location of the command on your system."
+ exit 1
+ fi
+ fi
+
+ if [ "`$IDEXE -u -n`" = "$RUN_AS_USER" ]
+ then
+ # Already running as the configured user. Avoid password prompts by not calling su.
+ RUN_AS_USER=""
+ fi
+ fi
+ if [ "X$RUN_AS_USER" != "X" ]
+ then
+ # If LOCKPROP and $RUN_AS_USER are defined then the new user will most likely not be
+ # able to create the lock file. The Wrapper will be able to update this file once it
+ # is created but will not be able to delete it on shutdown. If $2 is defined then
+ # the lock file should be created for the current command
+ if [ "X$LOCKPROP" != "X" ]
+ then
+ if [ "X$1" != "X" ]
+ then
+ # Resolve the primary group
+ RUN_AS_GROUP=`groups $RUN_AS_USER | awk '{print $3}' | tail -1`
+ if [ "X$RUN_AS_GROUP" = "X" ]
+ then
+ RUN_AS_GROUP=$RUN_AS_USER
+ fi
+ touch $LOCKFILE
+ chown $RUN_AS_USER:$RUN_AS_GROUP $LOCKFILE
+ fi
+ fi
+
+ # Still want to change users, recurse. This means that the user will only be
+ # prompted for a password once. Variables shifted by 1
+ #
+ # Use "runuser" if this exists. runuser should be used on RedHat in preference to su.
+ #
+ if test -f "/sbin/runuser"
+ then
+ /sbin/runuser - $RUN_AS_USER -c "\"$REALPATH\" $2"
+ else
+ su - $RUN_AS_USER -c "\"$REALPATH\" $2"
+ fi
+
+ # Now that we are the original user again, we may need to clean up the lock file.
+ if [ "X$LOCKPROP" != "X" ]
+ then
+ getpid
+ if [ "X$pid" = "X" ]
+ then
+ # Wrapper is not running so make sure the lock file is deleted.
+ if [ -f "$LOCKFILE" ]
+ then
+ rm "$LOCKFILE"
+ fi
+ fi
+ fi
+
+ exit 0
+ fi
+}
+
+getpid() {
+ pid=""
+ if [ -f "$PIDFILE" ]
+ then
+ if [ -r "$PIDFILE" ]
+ then
+ pid=`cat "$PIDFILE"`
+ if [ "X$pid" != "X" ]
+ then
+ # It is possible that 'a' process with the pid exists but that it is not the
+ # correct process. This can happen in a number of cases, but the most
+ # common is during system startup after an unclean shutdown.
+ # The ps statement below looks for the specific wrapper command running as
+ # the pid. If it is not found then the pid file is considered to be stale.
+ case "$DIST_OS" in
+ 'macosx')
+ pidtest=`$PSEXE -ww -p $pid -o command | grep "$WRAPPER_CMD" | tail -1`
+ ;;
+ 'solaris')
+ pidtest=`$PSEXE -auxww $pid | grep "$WRAPPER_CMD" | tail -1`
+ ;;
+ 'hpux')
+ pidtest=`$PSEXE -p $pid -x -o args | grep "$WRAPPER_CMD" | tail -1`
+ ;;
+ *)
+ pidtest=`$PSEXE -p $pid -o args | grep "$WRAPPER_CMD" | tail -1`
+ ;;
+ esac
+
+ if [ "X$pidtest" = "X" ]
+ then
+ # This is a stale pid file.
+ rm -f "$PIDFILE"
+ echo "Removed stale pid file: $PIDFILE"
+ pid=""
+ fi
+ fi
+ else
+ echo "Cannot read $PIDFILE."
+ exit 1
+ fi
+ fi
+}
+
+getstatus() {
+ STATUS=
+ if [ -f "$STATUSFILE" ]
+ then
+ if [ -r "$STATUSFILE" ]
+ then
+ STATUS=`cat "$STATUSFILE"`
+ fi
+ fi
+ if [ "X$STATUS" = "X" ]
+ then
+ STATUS="Unknown"
+ fi
+
+ JAVASTATUS=
+ if [ -f "$JAVASTATUSFILE" ]
+ then
+ if [ -r "$JAVASTATUSFILE" ]
+ then
+ JAVASTATUS=`cat "$JAVASTATUSFILE"`
+ fi
+ fi
+ if [ "X$JAVASTATUS" = "X" ]
+ then
+ JAVASTATUS="Unknown"
+ fi
+}
+
+testpid() {
+ case "$DIST_OS" in
+ 'solaris')
+ pid=`$PSEXE $pid | grep $pid | grep -v grep | awk '{print $1}' | tail -1`
+ ;;
+ *)
+ pid=`$PSEXE -p $pid | grep $pid | grep -v grep | awk '{print $1}' | tail -1`
+ ;;
+ esac
+ if [ "X$pid" = "X" ]
+ then
+ # Process is gone so remove the pid file.
+ rm -f "$PIDFILE"
+ pid=""
+ fi
+}
+
+console() {
+ echo "Running $APP_LONG_NAME..."
+ getpid
+ if [ "X$pid" = "X" ]
+ then
+ # The string passed to eval must handles spaces in paths correctly.
+ COMMAND_LINE="$CMDNICE \"$WRAPPER_CMD\" \"$WRAPPER_CONF\" wrapper.syslog.ident=\"$APP_NAME\" wrapper.pidfile=\"$PIDFILE\" wrapper.name=\"$APP_NAME\" wrapper.displayname=\"$APP_LONG_NAME\" $ANCHORPROP $STATUSPROP $LOCKPROP"
+ eval $COMMAND_LINE
+ else
+ echo "$APP_LONG_NAME is already running."
+ exit 1
+ fi
+}
+
+start() {
+ echo -n "Starting $APP_LONG_NAME..."
+ getpid
+ if [ "X$pid" = "X" ]
+ then
+ # The string passed to eval must handles spaces in paths correctly.
+ COMMAND_LINE="$CMDNICE \"$WRAPPER_CMD\" \"$WRAPPER_CONF\" wrapper.syslog.ident=\"$APP_NAME\" wrapper.pidfile=\"$PIDFILE\" wrapper.name=\"$APP_NAME\" wrapper.displayname=\"$APP_LONG_NAME\" wrapper.daemonize=TRUE $ANCHORPROP $IGNOREPROP $STATUSPROP $LOCKPROP"
+ eval $COMMAND_LINE
+ else
+ echo "$APP_LONG_NAME is already running."
+ exit 1
+ fi
+
+ # Sleep for a few seconds to allow for intialization if required
+ # then test to make sure we're still running.
+ #
+ i=0
+ while [ $i -lt $WAIT_AFTER_STARTUP ]
+ do
+ sleep 1
+ echo -n "."
+ i=`expr $i + 1`
+ done
+ if [ $WAIT_AFTER_STARTUP -gt 0 ]
+ then
+ getpid
+ if [ "X$pid" = "X" ]
+ then
+ echo " WARNING: $APP_LONG_NAME may have failed to start."
+ exit 1
+ else
+ echo " running ($pid)."
+ fi
+ else
+ echo ""
+ fi
+}
+
+stopit() {
+ # $1 exit if down flag
+
+ echo "Stopping $APP_LONG_NAME..."
+ getpid
+ if [ "X$pid" = "X" ]
+ then
+ echo "$APP_LONG_NAME was not running."
+ if [ "X$1" = "X1" ]
+ then
+ exit 1
+ fi
+ else
+ if [ "X$IGNORE_SIGNALS" = "X" ]
+ then
+ # Running so try to stop it.
+ kill $pid
+ if [ $? -ne 0 ]
+ then
+ # An explanation for the failure should have been given
+ echo "Unable to stop $APP_LONG_NAME."
+ exit 1
+ fi
+ else
+ rm -f "$ANCHORFILE"
+ if [ -f "$ANCHORFILE" ]
+ then
+ # An explanation for the failure should have been given
+ echo "Unable to stop $APP_LONG_NAME."
+ exit 1
+ fi
+ fi
+
+ # We can not predict how long it will take for the wrapper to
+ # actually stop as it depends on settings in wrapper.conf.
+ # Loop until it does.
+ savepid=$pid
+ CNT=0
+ TOTCNT=0
+ while [ "X$pid" != "X" ]
+ do
+ # Show a waiting message every 5 seconds.
+ if [ "$CNT" -lt "5" ]
+ then
+ CNT=`expr $CNT + 1`
+ else
+ echo "Waiting for $APP_LONG_NAME to exit..."
+ CNT=0
+ fi
+ TOTCNT=`expr $TOTCNT + 1`
+
+ sleep 1
+
+ testpid
+ done
+
+ pid=$savepid
+ testpid
+ if [ "X$pid" != "X" ]
+ then
+ echo "Failed to stop $APP_LONG_NAME."
+ exit 1
+ else
+ echo "Stopped $APP_LONG_NAME."
+ fi
+ fi
+}
+
+status() {
+ getpid
+ if [ "X$pid" = "X" ]
+ then
+ echo "$APP_LONG_NAME is not running."
+ exit 1
+ else
+ if [ "X$DETAIL_STATUS" = "X" ]
+ then
+ echo "$APP_LONG_NAME is running (PID:$pid)."
+ else
+ getstatus
+ echo "$APP_LONG_NAME is running (PID:$pid, Wrapper:$STATUS, Java:$JAVASTATUS)"
+ fi
+ exit 0
+ fi
+}
+
+dump() {
+ echo "Dumping $APP_LONG_NAME..."
+ getpid
+ if [ "X$pid" = "X" ]
+ then
+ echo "$APP_LONG_NAME was not running."
+ else
+ kill -3 $pid
+
+ if [ $? -ne 0 ]
+ then
+ echo "Failed to dump $APP_LONG_NAME."
+ exit 1
+ else
+ echo "Dumped $APP_LONG_NAME."
+ fi
+ fi
+}
+
+# Used by HP-UX init scripts.
+startmsg() {
+ getpid
+ if [ "X$pid" = "X" ]
+ then
+ echo "Starting $APP_LONG_NAME... (Wrapper:Stopped)"
+ else
+ if [ "X$DETAIL_STATUS" = "X" ]
+ then
+ echo "Starting $APP_LONG_NAME... (Wrapper:Running)"
+ else
+ getstatus
+ echo "Starting $APP_LONG_NAME... (Wrapper:$STATUS, Java:$JAVASTATUS)"
+ fi
+ fi
+}
+
+# Used by HP-UX init scripts.
+stopmsg() {
+ getpid
+ if [ "X$pid" = "X" ]
+ then
+ echo "Stopping $APP_LONG_NAME... (Wrapper:Stopped)"
+ else
+ if [ "X$DETAIL_STATUS" = "X" ]
+ then
+ echo "Stopping $APP_LONG_NAME... (Wrapper:Running)"
+ else
+ getstatus
+ echo "Stopping $APP_LONG_NAME... (Wrapper:$STATUS, Java:$JAVASTATUS)"
+ fi
+ fi
+}
+
+case "$1" in
+
+ 'console')
+ checkUser touchlock $1
+ console
+ ;;
+
+ 'start')
+ checkUser touchlock $1
+ start
+ ;;
+
+ 'stop')
+ checkUser "" $1
+ stopit "0"
+ ;;
+
+ 'restart')
+ checkUser touchlock $1
+ stopit "0"
+ start
+ ;;
+
+ 'condrestart')
+ checkUser touchlock $1
+ stopit "1"
+ start
+ ;;
+
+ 'status')
+ checkUser "" $1
+ status
+ ;;
+
+ 'dump')
+ checkUser "" $1
+ dump
+ ;;
+
+ 'start_msg')
+ checkUser "" $1
+ startmsg
+ ;;
+
+ 'stop_msg')
+ checkUser "" $1
+ stopmsg
+ ;;
+
+ *)
+ echo "Usage: $0 { console | start | stop | restart | condrestart | status | dump }"
+ exit 1
+ ;;
+esac
+
+exit 0
diff --git a/src/contribs/wrapper/conf/wrapper.conf b/src/contribs/wrapper/conf/wrapper.conf
new file mode 100644
index 0000000..2442d7b
--- /dev/null
+++ b/src/contribs/wrapper/conf/wrapper.conf
@@ -0,0 +1,119 @@
+#********************************************************************
+# Wrapper License Properties (Ignored by Community Edition)
+#********************************************************************
+# Include file problems can be debugged by removing the first '#'
+# from the following line:
+##include.debug
+#include ../conf/wrapper-license.conf
+#include ../conf/wrapper-license-%WRAPPER_HOST_NAME%.conf
+
+#********************************************************************
+# Wrapper Java Properties
+#********************************************************************
+# Java Application
+wrapper.java.command=java
+wrapper.debug=false
+# Tell the Wrapper to log the full generated Java command line.
+#wrapper.java.command.loglevel=INFO
+
+# Java Main class. This class must implement the WrapperListener interface
+# or guarantee that the WrapperManager class is initialized. Helper
+# classes are provided to do this for you. See the Integration section
+# of the documentation for details.
+wrapper.java.mainclass=org.tanukisoftware.wrapper.WrapperSimpleApp
+
+# Java Classpath (include wrapper.jar) Add class path elements as
+# needed starting from 1
+wrapper.java.classpath.1=../lib/wrapper.jar
+wrapper.java.classpath.2=../lib/*.jar
+
+
+# Java Library Path (location of Wrapper.DLL or libwrapper.so)
+wrapper.java.library.path.1=../lib
+
+# Java Bits. On applicable platforms, tells the JVM to run in 32 or 64-bit mode.
+wrapper.java.additional.auto_bits=TRUE
+
+# Java Additional Parameters
+#wrapper.java.additional.1=
+
+# Initial Java Heap Size (in MB)
+wrapper.java.initmemory=3
+
+# Maximum Java Heap Size (in MB)
+wrapper.java.maxmemory=64
+
+# Application parameters. Add parameters as needed starting from 1
+wrapper.app.parameter.1=davmail.DavGateway
+wrapper.app.parameter.2=../conf/davmail.properties
+
+
+#********************************************************************
+# Wrapper Logging Properties
+#********************************************************************
+# Enables Debug output from the Wrapper.
+# wrapper.debug=TRUE
+
+# Format of output for the console. (See docs for formats)
+wrapper.console.format=PM
+
+# Log Level for console output. (See docs for log levels)
+wrapper.console.loglevel=INFO
+
+# Log file to use for wrapper output logging.
+wrapper.logfile=../logs/davmail.log
+
+# Format of output for the log file. (See docs for formats)
+wrapper.logfile.format=LPTM
+
+# Log Level for log file output. (See docs for log levels)
+wrapper.logfile.loglevel=INFO
+
+# Maximum size that the log file will be allowed to grow to before
+# the log is rolled. Size is specified in bytes. The default value
+# of 0, disables log rolling. May abbreviate with the 'k' (kb) or
+# 'm' (mb) suffix. For example: 10m = 10 megabytes.
+wrapper.logfile.maxsize=10m
+
+# Maximum number of rolled log files which will be allowed before old
+# files are deleted. The default value of 0 implies no limit.
+wrapper.logfile.maxfiles=5
+
+# Log Level for sys/event log output. (See docs for log levels)
+wrapper.syslog.loglevel=NONE
+
+#********************************************************************
+# Wrapper General Properties
+#********************************************************************
+# Allow for the use of non-contiguous numbered properties
+wrapper.ignore_sequence_gaps=TRUE
+
+# Title to use when running as a console
+wrapper.console.title=DavMail
+
+#********************************************************************
+# Wrapper Windows NT/2000/XP Service Properties
+#********************************************************************
+# WARNING - Do not modify any of these properties when an application
+# using this configuration file has been installed as a service.
+# Please uninstall the service before modifying this section. The
+# service can then be reinstalled.
+
+# Name of the service
+wrapper.name=davmail
+
+# Display name of the service
+wrapper.displayname=DavMail
+
+# Description of the service
+wrapper.description=DavMail
+
+# Service dependencies. Add dependencies as needed starting from 1
+wrapper.ntservice.dependency.1=
+
+# Mode in which the service is installed. AUTO_START or DEMAND_START
+wrapper.ntservice.starttype=AUTO_START
+
+# Allow the service to interact with the desktop.
+wrapper.ntservice.interactive=false
+
diff --git a/src/contribs/wrapper/conf/wrapper.conf_2 b/src/contribs/wrapper/conf/wrapper.conf_2
new file mode 100644
index 0000000..1a531bf
--- /dev/null
+++ b/src/contribs/wrapper/conf/wrapper.conf_2
@@ -0,0 +1,119 @@
+#********************************************************************
+# Wrapper License Properties (Ignored by Community Edition)
+#********************************************************************
+# Include file problems can be debugged by removing the first '#'
+# from the following line:
+##include.debug
+#include ../conf/wrapper-license.conf
+#include ../conf/wrapper-license-%WRAPPER_HOST_NAME%.conf
+
+#********************************************************************
+# Wrapper Java Properties
+#********************************************************************
+# Java Application
+wrapper.java.command=java
+wrapper.debug=false
+# Tell the Wrapper to log the full generated Java command line.
+#wrapper.java.command.loglevel=INFO
+
+# Java Main class. This class must implement the WrapperListener interface
+# or guarantee that the WrapperManager class is initialized. Helper
+# classes are provided to do this for you. See the Integration section
+# of the documentation for details.
+wrapper.java.mainclass=org.tanukisoftware.wrapper.WrapperSimpleApp
+
+# Java Classpath (include wrapper.jar) Add class path elements as
+# needed starting from 1
+wrapper.java.classpath.1=../lib/wrapper.jar
+wrapper.java.classpath.2=../lib/*.jar
+
+
+# Java Library Path (location of Wrapper.DLL or libwrapper.so)
+wrapper.java.library.path.1=../lib
+
+# Java Bits. On applicable platforms, tells the JVM to run in 32 or 64-bit mode.
+wrapper.java.additional.auto_bits=TRUE
+
+# Java Additional Parameters
+#wrapper.java.additional.1=
+
+# Initial Java Heap Size (in MB)
+wrapper.java.initmemory=3
+
+# Maximum Java Heap Size (in MB)
+wrapper.java.maxmemory=64
+
+# Application parameters. Add parameters as needed starting from 1
+wrapper.app.parameter.1=davmail.DavGateway
+wrapper.app.parameter.2=../conf/davmail.properties_2
+
+
+#********************************************************************
+# Wrapper Logging Properties
+#********************************************************************
+# Enables Debug output from the Wrapper.
+# wrapper.debug=TRUE
+
+# Format of output for the console. (See docs for formats)
+wrapper.console.format=PM
+
+# Log Level for console output. (See docs for log levels)
+wrapper.console.loglevel=INFO
+
+# Log file to use for wrapper output logging.
+wrapper.logfile=../logs/davmail.log_2
+
+# Format of output for the log file. (See docs for formats)
+wrapper.logfile.format=LPTM
+
+# Log Level for log file output. (See docs for log levels)
+wrapper.logfile.loglevel=INFO
+
+# Maximum size that the log file will be allowed to grow to before
+# the log is rolled. Size is specified in bytes. The default value
+# of 0, disables log rolling. May abbreviate with the 'k' (kb) or
+# 'm' (mb) suffix. For example: 10m = 10 megabytes.
+wrapper.logfile.maxsize=10m
+
+# Maximum number of rolled log files which will be allowed before old
+# files are deleted. The default value of 0 implies no limit.
+wrapper.logfile.maxfiles=5
+
+# Log Level for sys/event log output. (See docs for log levels)
+wrapper.syslog.loglevel=NONE
+
+#********************************************************************
+# Wrapper General Properties
+#********************************************************************
+# Allow for the use of non-contiguous numbered properties
+wrapper.ignore_sequence_gaps=TRUE
+
+# Title to use when running as a console
+wrapper.console.title=DavMail
+
+#********************************************************************
+# Wrapper Windows NT/2000/XP Service Properties
+#********************************************************************
+# WARNING - Do not modify any of these properties when an application
+# using this configuration file has been installed as a service.
+# Please uninstall the service before modifying this section. The
+# service can then be reinstalled.
+
+# Name of the service
+wrapper.name=davmail
+
+# Display name of the service
+wrapper.displayname=DavMail
+
+# Description of the service
+wrapper.description=DavMail
+
+# Service dependencies. Add dependencies as needed starting from 1
+wrapper.ntservice.dependency.1=
+
+# Mode in which the service is installed. AUTO_START or DEMAND_START
+wrapper.ntservice.starttype=AUTO_START
+
+# Allow the service to interact with the desktop.
+wrapper.ntservice.interactive=false
+
diff --git a/src/contribs/wrapper/readme.txt b/src/contribs/wrapper/readme.txt
new file mode 100644
index 0000000..8d6a65d
--- /dev/null
+++ b/src/contribs/wrapper/readme.txt
@@ -0,0 +1,39 @@
+From Dustin Hawkins:
+
+I run two instances of DavMail on my linux desktop to connect to work and university exchange servers.
+In order to accomplish this, I used the Java Service Wrapper.
+
+Thought I would share my configs in case any one else wanted to run multi-instance under the java service wrapper.
+
+http://wrapper.tanukisoftware.org/doc/english/download.jsp
+
+
+I created the following directory structure
+
+/opt/davmail
+ bin/
+ wrapper <-- this is provided by java service wrapper. platform specific native executable.
+ davmail
+ davmail_2
+ lib/
+ <davmail jars>
+ <wrapper static objects/dll's for appropriate platform>
+ conf/
+ wrapper.conf
+ wrapper.conf_2
+ davmail.properties
+ davmail.properties_2
+ logs/
+
+
+The bin/davmail* scripts are the linux start/stop scripts for each instance
+the conf/ directory has two files for each instance, a wrapper.conf, and a davmail.properties.
+
+by linking the bin/davmail* scripts into /etc/init.d and /etc/rc.d, I start davmail as a linux service,
+and its easily start/stop/restart-able via the standard linux commands.
+
+If you download the Java Service Wrapper, you can find the appropriate executables and start scripts for
+your platform under <wrapper install dir>/bin
+You can find the correct static object or DLL files under the <wrapper install dir>/lib
+
+I have included a ZIP of my wrapper.conf and davmail startup scripts
diff --git a/src/java/com/ctc/wstx/sr/StreamScanner.java b/src/java/com/ctc/wstx/sr/StreamScanner.java
new file mode 100644
index 0000000..215cc53
--- /dev/null
+++ b/src/java/com/ctc/wstx/sr/StreamScanner.java
@@ -0,0 +1,2444 @@
+/* Woodstox XML processor
+ *
+ * Copyright (c) 2004- Tatu Saloranta, tatu.saloranta at iki.fi
+ *
+ * Licensed under the License specified in file LICENSE, included with
+ * the source code.
+ * You may not use this file except in compliance with the License.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.ctc.wstx.sr;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.URL;
+import java.text.MessageFormat;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.xml.stream.Location;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLReporter;
+import javax.xml.stream.XMLResolver;
+import javax.xml.stream.XMLStreamException;
+
+import org.codehaus.stax2.XMLReporter2;
+import org.codehaus.stax2.XMLStreamLocation2;
+import org.codehaus.stax2.validation.XMLValidationProblem;
+
+import com.ctc.wstx.api.ReaderConfig;
+import com.ctc.wstx.cfg.ErrorConsts;
+import com.ctc.wstx.cfg.InputConfigFlags;
+import com.ctc.wstx.cfg.ParsingErrorMsgs;
+import com.ctc.wstx.cfg.XmlConsts;
+import com.ctc.wstx.dtd.MinimalDTDReader;
+import com.ctc.wstx.ent.EntityDecl;
+import com.ctc.wstx.ent.IntEntity;
+import com.ctc.wstx.exc.*;
+import com.ctc.wstx.io.DefaultInputResolver;
+import com.ctc.wstx.io.WstxInputData;
+import com.ctc.wstx.io.WstxInputLocation;
+import com.ctc.wstx.io.WstxInputSource;
+import com.ctc.wstx.util.ExceptionUtil;
+import com.ctc.wstx.util.SymbolTable;
+import com.ctc.wstx.util.TextBuffer;
+
+/**
+ * Abstract base class that defines some basic functionality that all
+ * Woodstox reader classes (main XML reader, DTD reader) extend from.
+ */
+
+public abstract class StreamScanner
+ extends WstxInputData
+ implements InputProblemReporter,
+ InputConfigFlags, ParsingErrorMsgs
+{
+
+ // // // Some well-known chars:
+
+ /**
+ * Last (highest) char code of the three, LF, CR and NULL
+ */
+ public final static char CHAR_CR_LF_OR_NULL = (char) 13;
+
+ public final static int INT_CR_LF_OR_NULL = 13;
+
+ /**
+ * Character that allows quick check of whether a char can potentially
+ * be some kind of markup, WRT input stream processing;
+ * has to contain linefeeds, &, < and > (">" only matters when
+ * quoting text, as part of "]]>")
+ */
+ protected final static char CHAR_FIRST_PURE_TEXT = (char) ('>' + 1);
+
+
+ /**
+ * First character in Unicode (ie one with lowest id) that is legal
+ * as part of a local name (all valid name chars minus ':'). Used
+ * for doing quick check for local name end; usually name ends in
+ * a whitespace or equals sign.
+ */
+ protected final static char CHAR_LOWEST_LEGAL_LOCALNAME_CHAR = '-';
+
+ /*
+ ///////////////////////////////////////////////////////////
+ // Character validity constants, structs
+ ///////////////////////////////////////////////////////////
+ */
+
+ /**
+ * We will only use validity array for first 256 characters, mostly
+ * because after those characters it's easier to do fairly simple
+ * block checks.
+ */
+ private final static int VALID_CHAR_COUNT = 0x100;
+
+ private final static byte NAME_CHAR_INVALID_B = (byte) 0;
+ private final static byte NAME_CHAR_ALL_VALID_B = (byte) 1;
+ private final static byte NAME_CHAR_VALID_NONFIRST_B = (byte) -1;
+
+ private final static byte[] sCharValidity = new byte[VALID_CHAR_COUNT];
+
+ static {
+ /* First, since all valid-as-first chars are also valid-as-other chars,
+ * we'll initialize common chars:
+ */
+ sCharValidity['_'] = NAME_CHAR_ALL_VALID_B;
+ for (int i = 0, last = ('z' - 'a'); i <= last; ++i) {
+ sCharValidity['A' + i] = NAME_CHAR_ALL_VALID_B;
+ sCharValidity['a' + i] = NAME_CHAR_ALL_VALID_B;
+ }
+ for (int i = 0xC0; i < 0xF6; ++i) { // not all are fully valid, but
+ sCharValidity[i] = NAME_CHAR_ALL_VALID_B;
+ }
+ // ... now we can 'revert' ones not fully valid:
+ sCharValidity[0xD7] = NAME_CHAR_INVALID_B;
+ sCharValidity[0xF7] = NAME_CHAR_INVALID_B;
+
+ /* And then we can proceed with ones only valid-as-other.
+ */
+ sCharValidity['-'] = NAME_CHAR_VALID_NONFIRST_B;
+ sCharValidity['.'] = NAME_CHAR_VALID_NONFIRST_B;
+ sCharValidity[0xB7] = NAME_CHAR_VALID_NONFIRST_B;
+ for (int i = '0'; i <= '9'; ++i) {
+ sCharValidity[i] = NAME_CHAR_VALID_NONFIRST_B;
+ }
+ }
+
+ /**
+ * Public identifiers only use 7-bit ascii range.
+ */
+ private final static int VALID_PUBID_CHAR_COUNT = 0x80;
+ private final static byte[] sPubidValidity = new byte[VALID_PUBID_CHAR_COUNT];
+// private final static byte PUBID_CHAR_INVALID_B = (byte) 0;
+ private final static byte PUBID_CHAR_VALID_B = (byte) 1;
+ static {
+ for (int i = 0, last = ('z' - 'a'); i <= last; ++i) {
+ sPubidValidity['A' + i] = PUBID_CHAR_VALID_B;
+ sPubidValidity['a' + i] = PUBID_CHAR_VALID_B;
+ }
+ for (int i = '0'; i <= '9'; ++i) {
+ sPubidValidity[i] = PUBID_CHAR_VALID_B;
+ }
+
+ // 3 main white space types are valid
+ sPubidValidity[0x0A] = PUBID_CHAR_VALID_B;
+ sPubidValidity[0x0D] = PUBID_CHAR_VALID_B;
+ sPubidValidity[0x20] = PUBID_CHAR_VALID_B;
+
+ // And many of punctuation/separator ascii chars too:
+ sPubidValidity['-'] = PUBID_CHAR_VALID_B;
+ sPubidValidity['\''] = PUBID_CHAR_VALID_B;
+ sPubidValidity['('] = PUBID_CHAR_VALID_B;
+ sPubidValidity[')'] = PUBID_CHAR_VALID_B;
+ sPubidValidity['+'] = PUBID_CHAR_VALID_B;
+ sPubidValidity[','] = PUBID_CHAR_VALID_B;
+ sPubidValidity['.'] = PUBID_CHAR_VALID_B;
+ sPubidValidity['/'] = PUBID_CHAR_VALID_B;
+ sPubidValidity[':'] = PUBID_CHAR_VALID_B;
+ sPubidValidity['='] = PUBID_CHAR_VALID_B;
+ sPubidValidity['?'] = PUBID_CHAR_VALID_B;
+ sPubidValidity[';'] = PUBID_CHAR_VALID_B;
+ sPubidValidity['!'] = PUBID_CHAR_VALID_B;
+ sPubidValidity['*'] = PUBID_CHAR_VALID_B;
+ sPubidValidity['#'] = PUBID_CHAR_VALID_B;
+ sPubidValidity['@'] = PUBID_CHAR_VALID_B;
+ sPubidValidity['$'] = PUBID_CHAR_VALID_B;
+ sPubidValidity['_'] = PUBID_CHAR_VALID_B;
+ sPubidValidity['%'] = PUBID_CHAR_VALID_B;
+ }
+
+ /*
+ ///////////////////////////////////////////////////////////
+ // Basic configuration
+ ///////////////////////////////////////////////////////////
+ */
+
+ /**
+ * Copy of the configuration object passed by the factory.
+ * Contains immutable settings for this reader (or in case
+ * of DTD parsers, reader that uses it)
+ */
+ protected final ReaderConfig mConfig;
+
+ // // // Various extracted settings:
+
+ /**
+ * If true, Reader is namespace aware, and should do basic checks
+ * (usually enforcing limitations on having colons in names)
+ */
+ protected final boolean mCfgNsEnabled;
+
+ // Extracted standard on/off settings:
+
+ /**
+ * note: left non-final on purpose: sub-class may need to modify
+ * the default value after construction.
+ */
+ protected boolean mCfgReplaceEntities;
+
+ /*
+ ///////////////////////////////////////////////////////////
+ // Symbol handling, if applicable
+ ///////////////////////////////////////////////////////////
+ */
+
+ final SymbolTable mSymbols;
+
+ /**
+ * Local full name for the event, if it has one (note: element events
+ * do NOT use this variable; those names are stored in element stack):
+ * target for processing instructions.
+ *<p>
+ * Currently used for proc. instr. target, and entity name (at least
+ * when current entity reference is null).
+ *<p>
+ * Note: this variable is generally not cleared, since it comes from
+ * a symbol table, ie. this won't be the only reference.
+ */
+ protected String mCurrName;
+
+ /*
+ ///////////////////////////////////////////////////////////
+ // Input handling
+ ///////////////////////////////////////////////////////////
+ */
+
+ /**
+ * Currently active input source; contains link to parent (nesting) input
+ * sources, if any.
+ */
+ protected WstxInputSource mInput;
+
+ /**
+ * Top-most input source this reader can use; due to input source
+ * chaining, this is not necessarily the root of all input; for example,
+ * external DTD subset reader's root input still has original document
+ * input as its parent.
+ */
+ protected final WstxInputSource mRootInput;
+
+ /**
+ * Custom resolver used to handle external entities that are to be expanded
+ * by this reader (external param/general entity expander)
+ */
+ XMLResolver mEntityResolver = null;
+
+ /**
+ * This is the current depth of the input stack (same as what input
+ * element stack would return as its depth).
+ * It is used to enforce input scope constraints for nesting of
+ * elements (for xml reader) and dtd declaration (for dtd reader)
+ * with regards to input block (entity expansion) boundaries.
+ *<p>
+ * Basically this value is compared to {@link #mInputTopDepth}, which
+ * indicates what was the depth at the point where the currently active
+ * input scope/block was started.
+ */
+ protected int mCurrDepth = 0;
+
+ protected int mInputTopDepth = 0;
+
+ /**
+ * Flag that indicates whether linefeeds in the input data are to
+ * be normalized or not.
+ * Xml specs mandate that the line feeds are only normalized
+ * when they are from the external entities (main doc, external
+ * general/parsed entities), so normalization has to be
+ * suppressed when expanding internal general/parsed entities.
+ */
+ protected boolean mNormalizeLFs;
+
+ /*
+ ///////////////////////////////////////////////////////////
+ // Buffer(s) for local name(s) and text content
+ ///////////////////////////////////////////////////////////
+ */
+
+ /**
+ * Temporary buffer used if local name can not be just directly
+ * constructed from input buffer (name is on a boundary or such).
+ */
+ protected char[] mNameBuffer = null;
+
+ /*
+ ///////////////////////////////////////////////////////////
+ // Information about starting location of event
+ // Reader is pointing to; updated on-demand
+ ///////////////////////////////////////////////////////////
+ */
+
+ // // // Location info at point when current token was started
+
+ /**
+ * Total number of characters read before start of current token.
+ * For big (gigabyte-sized) sizes are possible, needs to be long,
+ * unlike pointers and sizes related to in-memory buffers.
+ */
+ protected long mTokenInputTotal = 0;
+
+ /**
+ * Input row on which current token starts, 1-based
+ */
+ protected int mTokenInputRow = 1;
+
+ /**
+ * Column on input row that current token starts; 0-based (although
+ * in the end it'll be converted to 1-based)
+ */
+ protected int mTokenInputCol = 0;
+
+ /*
+ ///////////////////////////////////////////////////////////
+ // XML document information (from doc decl if one
+ // was found) common to all entities (main xml
+ // document, external DTD subset)
+ ///////////////////////////////////////////////////////////
+ */
+
+ /**
+ * Input stream encoding, if known (passed in, or determined by
+ * auto-detection); null if not.
+ */
+ String mDocInputEncoding = null;
+
+ /**
+ * Character encoding from xml declaration, if any; null if no
+ * declaration, or it didn't specify encoding.
+ */
+ String mDocXmlEncoding = null;
+
+ /**
+ * XML version as declared by the document; one of constants
+ * from {@link XmlConsts} (like {@link XmlConsts#XML_V_10}).
+ */
+ protected int mDocXmlVersion = XmlConsts.XML_V_UNKNOWN;
+
+ /**
+ * Cache of internal character entities;
+ */
+ protected Map mCachedEntities;
+
+ /**
+ * Flag for whether or not character references should be treated as entities
+ */
+ protected boolean mCfgTreatCharRefsAsEntities;
+
+ /**
+ * Entity reference stream currently points to.
+ */
+ protected EntityDecl mCurrEntity;
+
+ /*
+ ///////////////////////////////////////////////////////////
+ // Life-cycle
+ ///////////////////////////////////////////////////////////
+ */
+
+ /**
+ * Constructor used when creating a complete new (main-level) reader that
+ * does not share its input buffers or state with another reader.
+ */
+ protected StreamScanner(WstxInputSource input, ReaderConfig cfg,
+ XMLResolver res)
+ {
+ super();
+ mInput = input;
+ // 17-Jun-2004, TSa: Need to know root-level input source
+ mRootInput = input;
+
+ mConfig = cfg;
+ mSymbols = cfg.getSymbols();
+ int cf = cfg.getConfigFlags();
+ mCfgNsEnabled = (cf & CFG_NAMESPACE_AWARE) != 0;
+ mCfgReplaceEntities = (cf & CFG_REPLACE_ENTITY_REFS) != 0;
+
+ mNormalizeLFs = mConfig.willNormalizeLFs();
+ mInputBuffer = null;
+ mInputPtr = mInputEnd = 0;
+ mEntityResolver = res;
+
+ mCfgTreatCharRefsAsEntities = mConfig.willTreatCharRefsAsEnts();
+ mCachedEntities = mCfgTreatCharRefsAsEntities ? new HashMap() : Collections.EMPTY_MAP;
+ }
+
+ /*
+ ///////////////////////////////////////////////////////////
+ // Package API
+ ///////////////////////////////////////////////////////////
+ */
+
+ /**
+ * Method that returns location of the last character returned by this
+ * reader; that is, location "one less" than the currently pointed to
+ * location.
+ */
+ protected WstxInputLocation getLastCharLocation()
+ {
+ return mInput.getLocation(mCurrInputProcessed + mInputPtr - 1,
+ mCurrInputRow,
+ mInputPtr - mCurrInputRowStart);
+ }
+
+ protected URL getSource() {
+ return mInput.getSource();
+ }
+
+ protected String getSystemId() {
+ return mInput.getSystemId();
+ }
+
+ /*
+ ///////////////////////////////////////////////////////////
+ // Partial LocationInfo implementation (not implemented
+ // by this base class, but is by some sub-classes)
+ ///////////////////////////////////////////////////////////
+ */
+
+ /**
+ * Returns location of last properly parsed token; as per StAX specs,
+ * apparently needs to be the end of current event, which is the same
+ * as the start of the following event (or EOF if that's next).
+ */
+ public abstract Location getLocation();
+
+ public XMLStreamLocation2 getStartLocation()
+ {
+ // note: +1 is used as columns are 1-based...
+ return mInput.getLocation(mTokenInputTotal, mTokenInputRow,
+ mTokenInputCol + 1);
+ }
+
+ public XMLStreamLocation2 getCurrentLocation()
+ {
+ return mInput.getLocation(mCurrInputProcessed + mInputPtr,
+ mCurrInputRow,
+ mInputPtr - mCurrInputRowStart + 1);
+ }
+
+ /*
+ ///////////////////////////////////////////////////////////
+ // InputProblemReporter implementation
+ ///////////////////////////////////////////////////////////
+ */
+
+ public WstxException throwWfcException(String msg, boolean deferErrors)
+ throws WstxException
+ {
+ WstxException ex = constructWfcException(msg);
+ if (!deferErrors) {
+ throw ex;
+ }
+ return ex;
+ }
+
+ public void throwParseError(String msg) throws XMLStreamException
+ {
+ throwParseError(msg, null, null);
+ }
+
+ /**
+ * Throws generic parse error with specified message and current parsing
+ * location.
+ *<p>
+ * Note: public access only because core code in other packages needs
+ * to access it.
+ */
+ public void throwParseError(String format, Object arg, Object arg2)
+ throws XMLStreamException
+ {
+ String msg = (arg != null || arg2 != null) ?
+ MessageFormat.format(format, new Object[] { arg, arg2 }) : format;
+ throw constructWfcException(msg);
+ }
+
+ public void reportProblem(String probType, String format, Object arg, Object arg2)
+ throws XMLStreamException
+ {
+ XMLReporter rep = mConfig.getXMLReporter();
+ if (rep != null) {
+ _reportProblem(rep, probType,
+ MessageFormat.format(format, new Object[] { arg, arg2 }), null);
+ }
+ }
+
+ public void reportProblem(Location loc, String probType,
+ String format, Object arg, Object arg2)
+ throws XMLStreamException
+ {
+ XMLReporter rep = mConfig.getXMLReporter();
+ if (rep != null) {
+ String msg = (arg != null || arg2 != null) ?
+ MessageFormat.format(format, new Object[] { arg, arg2 }) : format;
+ _reportProblem(rep, probType, msg, loc);
+ }
+ }
+
+ protected void _reportProblem(XMLReporter rep, String probType, String msg, Location loc)
+ throws XMLStreamException
+ {
+ if (loc == null) {
+ loc = getLastCharLocation();
+ }
+ _reportProblem(rep, new XMLValidationProblem(loc, msg, XMLValidationProblem.SEVERITY_ERROR, probType));
+ }
+
+ protected void _reportProblem(XMLReporter rep, XMLValidationProblem prob)
+ throws XMLStreamException
+ {
+ if (rep != null) {
+ Location loc = prob.getLocation();
+ if (loc == null) {
+ loc = getLastCharLocation();
+ prob.setLocation(loc);
+ }
+ // Backwards-compatibility fix: add non-null type, if missing:
+ if (prob.getType() == null) {
+ prob.setType(ErrorConsts.WT_VALIDATION);
+ }
+ // [WSTX-154]: was catching and dropping thrown exception: shouldn't.
+ // [WTSX-157]: need to support XMLReporter2
+ if (rep instanceof XMLReporter2) {
+ ((XMLReporter2) rep).report(prob);
+ } else {
+ rep.report(prob.getMessage(), prob.getType(), prob, loc);
+ }
+ }
+ }
+
+ /**
+ *<p>
+ * Note: this is the base implementation used for implementing
+ * <code>ValidationContext</code>
+ */
+ public void reportValidationProblem(XMLValidationProblem prob)
+ throws XMLStreamException
+ {
+ // !!! TBI: Fail-fast vs. deferred modes?
+ /* For now let's implement basic functionality: warnings get
+ * reported via XMLReporter, errors and fatal errors result in
+ * immediate exceptions.
+ */
+ /* 27-May-2008, TSa: [WSTX-153] Above is incorrect: as per Stax
+ * javadocs for XMLReporter, both warnings and non-fatal errors
+ * (which includes all validation errors) should be reported via
+ * XMLReporter interface, and only fatals should cause an
+ * immediate stream exception (by-passing reporter)
+ */
+ if (prob.getSeverity() > XMLValidationProblem.SEVERITY_ERROR) {
+ throw WstxValidationException.create(prob);
+ }
+ XMLReporter rep = mConfig.getXMLReporter();
+ if (rep != null) {
+ _reportProblem(rep, prob);
+ } else {
+ /* If no reporter, regular non-fatal errors are to be reported
+ * as exceptions as well, for backwards compatibility
+ */
+ if (prob.getSeverity() >= XMLValidationProblem.SEVERITY_ERROR) {
+ throw WstxValidationException.create(prob);
+ }
+ }
+ }
+
+ public void reportValidationProblem(String msg, int severity)
+ throws XMLStreamException
+ {
+ reportValidationProblem(new XMLValidationProblem(getLastCharLocation(),
+ msg, severity));
+ }
+
+ public void reportValidationProblem(String msg)
+ throws XMLStreamException
+ {
+ reportValidationProblem(new XMLValidationProblem(getLastCharLocation(),
+ msg,
+ XMLValidationProblem.SEVERITY_ERROR));
+ }
+
+ public void reportValidationProblem(Location loc, String msg)
+ throws XMLStreamException
+ {
+ reportValidationProblem(new XMLValidationProblem(loc, msg));
+ }
+
+ public void reportValidationProblem(String format, Object arg, Object arg2)
+ throws XMLStreamException
+ {
+ reportValidationProblem(MessageFormat.format(format, new Object[] { arg, arg2 }));
+ }
+
+ /*
+ ///////////////////////////////////////////////////////////
+ // Other error reporting methods
+ ///////////////////////////////////////////////////////////
+ */
+
+ protected WstxException constructWfcException(String msg)
+ {
+ return new WstxParsingException(msg, getLastCharLocation());
+ }
+
+ /**
+ * Construct and return a {@link XMLStreamException} to throw
+ * as a result of a failed Typed Access operation (but one not
+ * caused by a Well-Formedness Constraint or Validation Constraint
+ * problem)
+ */
+ /*
+ protected WstxException _constructTypeException(String msg)
+ {
+ // Hmmh. Should there be a distinct sub-type?
+ return new WstxParsingException(msg, getLastCharLocation());
+ }
+ */
+
+ protected WstxException constructFromIOE(IOException ioe)
+ {
+ return new WstxIOException(ioe);
+ }
+
+ protected WstxException constructNullCharException()
+ {
+ return new WstxUnexpectedCharException("Illegal character (NULL, unicode 0) encountered: not valid in any content",
+ getLastCharLocation(), CHAR_NULL);
+ }
+
+ protected void throwUnexpectedChar(int i, String msg)
+ throws WstxException
+ {
+ char c = (char) i;
+ String excMsg = "Unexpected character "+getCharDesc(c)+msg;
+ throw new WstxUnexpectedCharException(excMsg, getLastCharLocation(), c);
+ }
+
+ protected void throwNullChar()
+ throws WstxException
+ {
+ throw constructNullCharException();
+ }
+
+ protected void throwInvalidSpace(int i)
+ throws WstxException
+ {
+ throwInvalidSpace(i, false);
+ }
+
+ protected WstxException throwInvalidSpace(int i, boolean deferErrors)
+ throws WstxException
+ {
+ char c = (char) i;
+ WstxException ex;
+ if (c == CHAR_NULL) {
+ ex = constructNullCharException();
+ } else {
+ String msg = "Illegal character ("+getCharDesc(c)+")";
+ if (mXml11) {
+ msg += " [note: in XML 1.1, it could be included via entity expansion]";
+ }
+ ex = new WstxUnexpectedCharException(msg, getLastCharLocation(), c);
+ }
+ if (!deferErrors) {
+ throw ex;
+ }
+ return ex;
+ }
+
+ protected void throwUnexpectedEOF(String msg)
+ throws WstxException
+ {
+ throw new WstxEOFException("Unexpected EOF"
+ +(msg == null ? "" : msg),
+ getLastCharLocation());
+ }
+
+ /**
+ * Similar to {@link #throwUnexpectedEOF}, but only indicates ending
+ * of an input block. Used when reading a token that can not span
+ * input block boundaries (ie. can not continue past end of an
+ * entity expansion).
+ */
+ protected void throwUnexpectedEOB(String msg)
+ throws WstxException
+ {
+ throw new WstxEOFException("Unexpected end of input block"
+ +(msg == null ? "" : msg),
+ getLastCharLocation());
+ }
+
+ protected void throwFromIOE(IOException ioe)
+ throws WstxException
+ {
+ throw new WstxIOException(ioe);
+ }
+
+ protected void throwFromStrE(XMLStreamException strex)
+ throws WstxException
+ {
+ if (strex instanceof WstxException) {
+ throw (WstxException) strex;
+ }
+ WstxException newEx = new WstxException(strex);
+ ExceptionUtil.setInitCause(newEx, strex);
+ throw newEx;
+ }
+
+ /**
+ * Method called to report an error, when caller's signature only
+ * allows runtime exceptions to be thrown.
+ */
+ protected void throwLazyError(Exception e)
+ {
+ if (e instanceof XMLStreamException) {
+ WstxLazyException.throwLazily((XMLStreamException) e);
+ }
+ ExceptionUtil.throwRuntimeException(e);
+ }
+
+ protected String tokenTypeDesc(int type)
+ {
+ return ErrorConsts.tokenTypeDesc(type);
+ }
+
+ /*
+ ///////////////////////////////////////////////////////////
+ // Input buffer handling
+ ///////////////////////////////////////////////////////////
+ */
+
+ /**
+ * Returns current input source this source uses.
+ *<p>
+ * Note: public only because some implementations are on different
+ * package.
+ */
+ public final WstxInputSource getCurrentInput() {
+ return mInput;
+ }
+
+ protected final int inputInBuffer() {
+ return mInputEnd - mInputPtr;
+ }
+
+ protected final int getNext()
+ throws XMLStreamException
+ {
+ if (mInputPtr >= mInputEnd) {
+ if (!loadMore()) {
+ return -1;
+ }
+ }
+ return (int) mInputBuffer[mInputPtr++];
+ }
+
+ /**
+ * Similar to {@link #getNext}, but does not advance pointer
+ * in input buffer.
+ *<p>
+ * Note: this method only peeks within current input source;
+ * it does not close it and check nested input source (if any).
+ * This is necessary when checking keywords, since they can never
+ * cross input block boundary.
+ */
+ protected final int peekNext()
+ throws XMLStreamException
+ {
+ if (mInputPtr >= mInputEnd) {
+ if (!loadMoreFromCurrent()) {
+ return -1;
+ }
+ }
+ return (int) mInputBuffer[mInputPtr];
+ }
+
+ protected final char getNextChar(String errorMsg)
+ throws XMLStreamException
+ {
+ if (mInputPtr >= mInputEnd) {
+ loadMore(errorMsg);
+ }
+ return mInputBuffer[mInputPtr++];
+ }
+
+ /**
+ * Similar to {@link #getNextChar}, but will not read more characters
+ * from parent input source(s) if the current input source doesn't
+ * have more content. This is often needed to prevent "runaway" content,
+ * such as comments that start in an entity but do not have matching
+ * close marker inside entity; XML specification specifically states
+ * such markup is not legal.
+ */
+ protected final char getNextCharFromCurrent(String errorMsg)
+ throws XMLStreamException
+ {
+ if (mInputPtr >= mInputEnd) {
+ loadMoreFromCurrent(errorMsg);
+ }
+ return mInputBuffer[mInputPtr++];
+ }
+
+ /**
+ * Method that will skip through zero or more white space characters,
+ * and return either the character following white space, or -1 to
+ * indicate EOF (end of the outermost input source)/
+ */
+ protected final int getNextAfterWS()
+ throws XMLStreamException
+ {
+ if (mInputPtr >= mInputEnd) {
+ if (!loadMore()) {
+ return -1;
+ }
+ }
+ char c = mInputBuffer[mInputPtr++];
+ while (c <= CHAR_SPACE) {
+ // Linefeed?
+ if (c == '\n' || c == '\r') {
+ skipCRLF(c);
+ } else if (c != CHAR_SPACE && c != '\t') {
+ throwInvalidSpace(c);
+ }
+ // Still a white space?
+ if (mInputPtr >= mInputEnd) {
+ if (!loadMore()) {
+ return -1;
+ }
+ }
+ c = mInputBuffer[mInputPtr++];
+ }
+ return (int) c;
+ }
+
+ protected final char getNextCharAfterWS(String errorMsg)
+ throws XMLStreamException
+ {
+ if (mInputPtr >= mInputEnd) {
+ loadMore(errorMsg);
+ }
+
+ char c = mInputBuffer[mInputPtr++];
+ while (c <= CHAR_SPACE) {
+ // Linefeed?
+ if (c == '\n' || c == '\r') {
+ skipCRLF(c);
+ } else if (c != CHAR_SPACE && c != '\t') {
+ throwInvalidSpace(c);
+ }
+
+ // Still a white space?
+ if (mInputPtr >= mInputEnd) {
+ loadMore(errorMsg);
+ }
+ c = mInputBuffer[mInputPtr++];
+ }
+ return c;
+ }
+
+ protected final char getNextInCurrAfterWS(String errorMsg)
+ throws XMLStreamException
+ {
+ return getNextInCurrAfterWS(errorMsg, getNextCharFromCurrent(errorMsg));
+ }
+
+ protected final char getNextInCurrAfterWS(String errorMsg, char c)
+ throws XMLStreamException
+ {
+ while (c <= CHAR_SPACE) {
+ // Linefeed?
+ if (c == '\n' || c == '\r') {
+ skipCRLF(c);
+ } else if (c != CHAR_SPACE && c != '\t') {
+ throwInvalidSpace(c);
+ }
+
+ // Still a white space?
+ if (mInputPtr >= mInputEnd) {
+ loadMoreFromCurrent(errorMsg);
+ }
+ c = mInputBuffer[mInputPtr++];
+ }
+ return c;
+ }
+
+ /**
+ * Method called when a CR has been spotted in input; checks if next
+ * char is LF, and if so, skips it. Note that next character has to
+ * come from the current input source, to qualify; it can never come
+ * from another (nested) input source.
+ *
+ * @return True, if passed in char is '\r' and next one is '\n'.
+ */
+ protected final boolean skipCRLF(char c)
+ throws XMLStreamException
+ {
+ boolean result;
+
+ if (c == '\r' && peekNext() == '\n') {
+ ++mInputPtr;
+ result = true;
+ } else {
+ result = false;
+ }
+ ++mCurrInputRow;
+ mCurrInputRowStart = mInputPtr;
+ return result;
+ }
+
+ protected final void markLF() {
+ ++mCurrInputRow;
+ mCurrInputRowStart = mInputPtr;
+ }
+
+ protected final void markLF(int inputPtr) {
+ ++mCurrInputRow;
+ mCurrInputRowStart = inputPtr;
+ }
+
+ /**
+ * Method to push back last character read; can only be called once,
+ * that is, no more than one char can be guaranteed to be succesfully
+ * returned.
+ */
+ protected final void pushback() { --mInputPtr; }
+
+ /*
+ ///////////////////////////////////////////////////////////
+ // Sub-class overridable input handling methods
+ ///////////////////////////////////////////////////////////
+ */
+
+ /**
+ * Method called when an entity has been expanded (new input source
+ * has been created). Needs to initialize location information and change
+ * active input source.
+ *
+ * @param entityId Name of the entity being expanded
+ */
+ protected void initInputSource(WstxInputSource newInput, boolean isExt,
+ String entityId)
+ throws XMLStreamException
+ {
+ mInput = newInput;
+ // Let's make sure new input will be read next time input is needed:
+ mInputPtr = 0;
+ mInputEnd = 0;
+ /* Plus, reset the input location so that'll be accurate for
+ * error reporting etc.
+ */
+ mInputTopDepth = mCurrDepth;
+ mInput.initInputLocation(this, mCurrDepth);
+
+ /* 21-Feb-2006, TSa: Linefeeds are NOT normalized when expanding
+ * internal entities (XML, 2.11)
+ */
+ if (isExt) {
+ mNormalizeLFs = true;
+ } else {
+ mNormalizeLFs = false;
+ }
+ }
+
+ /**
+ * Method that will try to read one or more characters from currently
+ * open input sources; closing input sources if necessary.
+ *
+ * @return true if reading succeeded (or may succeed), false if
+ * we reached EOF.
+ */
+ protected boolean loadMore()
+ throws XMLStreamException
+ {
+ WstxInputSource input = mInput;
+ do {
+ /* Need to make sure offsets are properly updated for error
+ * reporting purposes, and do this now while previous amounts
+ * are still known.
+ */
+ mCurrInputProcessed += mInputEnd;
+ mCurrInputRowStart -= mInputEnd;
+ int count;
+ try {
+ count = input.readInto(this);
+ if (count > 0) {
+ return true;
+ }
+ input.close();
+ } catch (IOException ioe) {
+ throw constructFromIOE(ioe);
+ }
+ if (input == mRootInput) {
+ /* Note: no need to check entity/input nesting in this
+ * particular case, since it will be handled by higher level
+ * parsing code (results in an unexpected EOF)
+ */
+ return false;
+ }
+ WstxInputSource parent = input.getParent();
+ if (parent == null) { // sanity check!
+ throwNullParent(input);
+ }
+ /* 13-Feb-2006, TSa: Ok, do we violate a proper nesting constraints
+ * with this input block closure?
+ */
+ if (mCurrDepth != input.getScopeId()) {
+ handleIncompleteEntityProblem(input);
+ }
+
+ mInput = input = parent;
+ input.restoreContext(this);
+ mInputTopDepth = input.getScopeId();
+ /* 21-Feb-2006, TSa: Since linefeed normalization needs to be
+ * suppressed for internal entity expansion, we may need to
+ * change the state...
+ */
+ if (!mNormalizeLFs) {
+ mNormalizeLFs = !input.fromInternalEntity();
+ }
+ // Maybe there are leftovers from that input in buffer now?
+ } while (mInputPtr >= mInputEnd);
+
+ return true;
+ }
+
+ protected final boolean loadMore(String errorMsg)
+ throws XMLStreamException
+ {
+ if (!loadMore()) {
+ throwUnexpectedEOF(errorMsg);
+ }
+ return true;
+ }
+
+ protected boolean loadMoreFromCurrent()
+ throws XMLStreamException
+ {
+ // Need to update offsets properly
+ mCurrInputProcessed += mInputEnd;
+ mCurrInputRowStart -= mInputEnd;
+ try {
+ int count = mInput.readInto(this);
+ return (count > 0);
+ } catch (IOException ie) {
+ throw constructFromIOE(ie);
+ }
+ }
+
+ protected final boolean loadMoreFromCurrent(String errorMsg)
+ throws XMLStreamException
+ {
+ if (!loadMoreFromCurrent()) {
+ throwUnexpectedEOB(errorMsg);
+ }
+ return true;
+ }
+
+ /**
+ * Method called to make sure current main-level input buffer has at
+ * least specified number of characters available consequtively,
+ * without having to call {@link #loadMore}. It can only be called
+ * when input comes from main-level buffer; further, call can shift
+ * content in input buffer, so caller has to flush any data still
+ * pending. In short, caller has to know exactly what it's doing. :-)
+ *<p>
+ * Note: method does not check for any other input sources than the
+ * current one -- if current source can not fulfill the request, a
+ * failure is indicated.
+ *
+ * @return true if there's now enough data; false if not (EOF)
+ */
+ protected boolean ensureInput(int minAmount)
+ throws XMLStreamException
+ {
+ int currAmount = mInputEnd - mInputPtr;
+ if (currAmount >= minAmount) {
+ return true;
+ }
+ try {
+ return mInput.readMore(this, minAmount);
+ } catch (IOException ie) {
+ throw constructFromIOE(ie);
+ }
+ }
+
+ protected void closeAllInput(boolean force)
+ throws XMLStreamException
+ {
+ WstxInputSource input = mInput;
+ while (true) {
+ try {
+ if (force) {
+ input.closeCompletely();
+ } else {
+ input.close();
+ }
+ } catch (IOException ie) {
+ throw constructFromIOE(ie);
+ }
+ if (input == mRootInput) {
+ break;
+ }
+ WstxInputSource parent = input.getParent();
+ if (parent == null) { // sanity check!
+ throwNullParent(input);
+ }
+ mInput = input = parent;
+ }
+ }
+
+ protected void throwNullParent(WstxInputSource curr)
+ {
+ throw new IllegalStateException(ErrorConsts.ERR_INTERNAL);
+ //throw new IllegalStateException("Internal error: null parent for input source '"+curr+"'; should never occur (should have stopped at root input '"+mRootInput+"').");
+ }
+
+ /*
+ ///////////////////////////////////////////////////////////
+ // Entity resolution
+ ///////////////////////////////////////////////////////////
+ */
+
+ /**
+ * Method that tries to resolve a character entity, or (if caller so
+ * specifies), a pre-defined internal entity (lt, gt, amp, apos, quot).
+ * It will succeed iff:
+ * <ol>
+ * <li>Entity in question is a simple character entity (either one of
+ * 5 pre-defined ones, or using decimal/hex notation), AND
+ * <li>
+ * <li>Entity fits completely inside current input buffer.
+ * <li>
+ * </ol>
+ * If so, character value of entity is returned. Character 0 is returned
+ * otherwise; if so, caller needs to do full resolution.
+ *<p>
+ * Note: On entry we are guaranteed there are at least 3 more characters
+ * in this buffer; otherwise we shouldn't be called.
+ *
+ * @param checkStd If true, will check pre-defined internal entities
+ * (gt, lt, amp, apos, quot); if false, will only check actual
+ * character entities.
+ *
+ * @return (Valid) character value, if entity is a character reference,
+ * and could be resolved from current input buffer (does not span
+ * buffer boundary); null char (code 0) if not (either non-char
+ * entity, or spans input buffer boundary).
+ */
+ protected int resolveSimpleEntity(boolean checkStd)
+ throws XMLStreamException
+ {
+ char[] buf = mInputBuffer;
+ int ptr = mInputPtr;
+ char c = buf[ptr++];
+
+ // Numeric reference?
+ if (c == '#') {
+ c = buf[ptr++];
+ int value = 0;
+ int inputLen = mInputEnd;
+ if (c == 'x') { // hex
+ while (ptr < inputLen) {
+ c = buf[ptr++];
+ if (c == ';') {
+ break;
+ }
+ value = value << 4;
+ if (c <= '9' && c >= '0') {
+ value += (c - '0');
+ } else if (c >= 'a' && c <= 'f') {
+ value += (10 + (c - 'a'));
+ } else if (c >= 'A' && c <= 'F') {
+ value += (10 + (c - 'A'));
+ } else {
+ mInputPtr = ptr; // so error points to correct char
+ throwUnexpectedChar(c, "; expected a hex digit (0-9a-fA-F).");
+ }
+ /* Need to check for overflow; easiest to do right as
+ * it happens...
+ */
+ if (value > MAX_UNICODE_CHAR) {
+ reportUnicodeOverflow();
+ }
+ }
+ } else { // numeric (decimal)
+ while (c != ';') {
+ if (c <= '9' && c >= '0') {
+ value = (value * 10) + (c - '0');
+ // Overflow?
+ if (value > MAX_UNICODE_CHAR) {
+ reportUnicodeOverflow();
+ }
+ } else {
+ mInputPtr = ptr; // so error points to correct char
+ throwUnexpectedChar(c, "; expected a decimal number.");
+ }
+ if (ptr >= inputLen) {
+ break;
+ }
+ c = buf[ptr++];
+ }
+ }
+ /* We get here either if we got it all, OR if we ran out of
+ * input in current buffer.
+ */
+ if (c == ';') { // got the full thing
+ mInputPtr = ptr;
+ validateChar(value);
+ return value;
+ }
+
+ /* If we ran out of input, need to just fall back, gets
+ * resolved via 'full' resolution mechanism.
+ */
+ } else if (checkStd) {
+ /* Caller may not want to resolve these quite yet...
+ * (when it wants separate events for non-char entities)
+ */
+ if (c == 'a') { // amp or apos?
+ c = buf[ptr++];
+
+ if (c == 'm') { // amp?
+ if (buf[ptr++] == 'p') {
+ if (ptr < mInputEnd && buf[ptr++] == ';') {
+ mInputPtr = ptr;
+ return '&';
+ }
+ }
+ } else if (c == 'p') { // apos?
+ if (buf[ptr++] == 'o') {
+ int len = mInputEnd;
+ if (ptr < len && buf[ptr++] == 's') {
+ if (ptr < len && buf[ptr++] == ';') {
+ mInputPtr = ptr;
+ return '\'';
+ }
+ }
+ }
+ }
+ } else if (c == 'g') { // gt?
+ if (buf[ptr++] == 't' && buf[ptr++] == ';') {
+ mInputPtr = ptr;
+ return '>';
+ }
+ } else if (c == 'l') { // lt?
+ if (buf[ptr++] == 't' && buf[ptr++] == ';') {
+ mInputPtr = ptr;
+ return '<';
+ }
+ } else if (c == 'q') { // quot?
+ if (buf[ptr++] == 'u' && buf[ptr++] == 'o') {
+ int len = mInputEnd;
+ if (ptr < len && buf[ptr++] == 't') {
+ if (ptr < len && buf[ptr++] == ';') {
+ mInputPtr = ptr;
+ return '"';
+ }
+ }
+ }
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Method called to resolve character entities, and only character
+ * entities (except that pre-defined char entities -- amp, apos, lt,
+ * gt, quote -- MAY be "char entities" in this sense, depending on
+ * arguments).
+ * Otherwise it is to return the null char; if so,
+ * the input pointer will point to the same point as when method
+ * entered (char after ampersand), plus the ampersand itself is
+ * guaranteed to be in the input buffer (so caller can just push it
+ * back if necessary).
+ *<p>
+ * Most often this method is called when reader is not to expand
+ * non-char entities automatically, but to return them as separate
+ * events.
+ *<p>
+ * Main complication here is that we need to do 5-char lookahead. This
+ * is problematic if chars are on input buffer boundary. This is ok
+ * for the root level input buffer, but not for some nested buffers.
+ * However, according to XML specs, such split entities are actually
+ * illegal... so we can throw an exception in those cases.
+ *
+ * @param checkStd If true, will check pre-defined internal entities
+ * (gt, lt, amp, apos, quot) as character entities; if false, will only
+ * check actual 'real' character entities.
+ *
+ * @return (Valid) character value, if entity is a character reference,
+ * and could be resolved from current input buffer (does not span
+ * buffer boundary); null char (code 0) if not (either non-char
+ * entity, or spans input buffer boundary).
+ */
+ protected int resolveCharOnlyEntity(boolean checkStd)
+ throws XMLStreamException
+ {
+ //int avail = inputInBuffer();
+ int avail = mInputEnd - mInputPtr;
+ if (avail < 6) {
+ // split entity, or buffer boundary
+ /* Don't want to lose leading '&' (in case we can not expand
+ * the entity), so let's push it back first
+ */
+ --mInputPtr;
+ /* Shortest valid reference would be 3 chars ('&a;'); which
+ * would only be legal from an expanded entity...
+ */
+ if (!ensureInput(6)) {
+ avail = inputInBuffer();
+ if (avail < 3) {
+ throwUnexpectedEOF(SUFFIX_IN_ENTITY_REF);
+ }
+ } else {
+ avail = 6;
+ }
+ // ... and now we can move pointer back as well:
+ ++mInputPtr;
+ }
+
+ /* Ok, now we have one more character to check, and that's enough
+ * to determine type decisively.
+ */
+ char c = mInputBuffer[mInputPtr];
+
+ // A char reference?
+ if (c == '#') { // yup
+ ++mInputPtr;
+ return resolveCharEnt(null);
+ }
+
+ // nope... except may be a pre-def?
+ if (checkStd) {
+ if (c == 'a') {
+ char d = mInputBuffer[mInputPtr+1];
+ if (d == 'm') {
+ if (avail >= 4
+ && mInputBuffer[mInputPtr+2] == 'p'
+ && mInputBuffer[mInputPtr+3] == ';') {
+ mInputPtr += 4;
+ return '&';
+ }
+ } else if (d == 'p') {
+ if (avail >= 5
+ && mInputBuffer[mInputPtr+2] == 'o'
+ && mInputBuffer[mInputPtr+3] == 's'
+ && mInputBuffer[mInputPtr+4] == ';') {
+ mInputPtr += 5;
+ return '\'';
+ }
+ }
+ } else if (c == 'l') {
+ if (avail >= 3
+ && mInputBuffer[mInputPtr+1] == 't'
+ && mInputBuffer[mInputPtr+2] == ';') {
+ mInputPtr += 3;
+ return '<';
+ }
+ } else if (c == 'g') {
+ if (avail >= 3
+ && mInputBuffer[mInputPtr+1] == 't'
+ && mInputBuffer[mInputPtr+2] == ';') {
+ mInputPtr += 3;
+ return '>';
+ }
+ } else if (c == 'q') {
+ if (avail >= 5
+ && mInputBuffer[mInputPtr+1] == 'u'
+ && mInputBuffer[mInputPtr+2] == 'o'
+ && mInputBuffer[mInputPtr+3] == 't'
+ && mInputBuffer[mInputPtr+4] == ';') {
+ mInputPtr += 5;
+ return '"';
+ }
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Reverse of {@link #resolveCharOnlyEntity}; will only resolve entity
+ * if it is NOT a character entity (or pre-defined 'generic' entity;
+ * amp, apos, lt, gt or quot). Only used in cases where entities
+ * are to be separately returned unexpanded (in non-entity-replacing
+ * mode); which means it's never called from dtd handler.
+ */
+ protected EntityDecl resolveNonCharEntity()
+ throws XMLStreamException
+ {
+ //int avail = inputInBuffer();
+ int avail = mInputEnd - mInputPtr;
+ if (avail < 6) {
+ // split entity, or buffer boundary
+ /* Don't want to lose leading '&' (in case we can not expand
+ * the entity), so let's push it back first
+ */
+ --mInputPtr;
+
+ /* Shortest valid reference would be 3 chars ('&a;'); which
+ * would only be legal from an expanded entity...
+ */
+ if (!ensureInput(6)) {
+ avail = inputInBuffer();
+ if (avail < 3) {
+ throwUnexpectedEOF(SUFFIX_IN_ENTITY_REF);
+ }
+ } else {
+ avail = 6;
+ }
+ // ... and now we can move pointer back as well:
+ ++mInputPtr;
+ }
+
+ // We don't care about char entities:
+ char c = mInputBuffer[mInputPtr];
+ if (c == '#') {
+ return null;
+ }
+
+ /* 19-Aug-2004, TSa: Need special handling for pre-defined
+ * entities; they are not counted as 'real' general parsed
+ * entities, but more as character entities...
+ */
+
+ // have chars at least up to mInputPtr+4 by now
+ if (c == 'a') {
+ char d = mInputBuffer[mInputPtr+1];
+ if (d == 'm') {
+ if (avail >= 4
+ && mInputBuffer[mInputPtr+2] == 'p'
+ && mInputBuffer[mInputPtr+3] == ';') {
+ // If not automatically expanding:
+ //return sEntityAmp;
+ // mInputPtr += 4;
+ return null;
+ }
+ } else if (d == 'p') {
+ if (avail >= 5
+ && mInputBuffer[mInputPtr+2] == 'o'
+ && mInputBuffer[mInputPtr+3] == 's'
+ && mInputBuffer[mInputPtr+4] == ';') {
+ return null;
+ }
+ }
+ } else if (c == 'l') {
+ if (avail >= 3
+ && mInputBuffer[mInputPtr+1] == 't'
+ && mInputBuffer[mInputPtr+2] == ';') {
+ return null;
+ }
+ } else if (c == 'g') {
+ if (avail >= 3
+ && mInputBuffer[mInputPtr+1] == 't'
+ && mInputBuffer[mInputPtr+2] == ';') {
+ return null;
+ }
+ } else if (c == 'q') {
+ if (avail >= 5
+ && mInputBuffer[mInputPtr+1] == 'u'
+ && mInputBuffer[mInputPtr+2] == 'o'
+ && mInputBuffer[mInputPtr+3] == 't'
+ && mInputBuffer[mInputPtr+4] == ';') {
+ return null;
+ }
+ }
+
+ // Otherwise, let's just parse in generic way:
+ ++mInputPtr; // since we already read the first letter
+ String id = parseEntityName(c);
+ mCurrName = id;
+
+ return findEntity(id, null);
+ }
+
+ /**
+ * Method that does full resolution of an entity reference, be it
+ * character entity, internal entity or external entity, including
+ * updating of input buffers, and depending on whether result is
+ * a character entity (or one of 5 pre-defined entities), returns
+ * char in question, or null character (code 0) to indicate it had
+ * to change input source.
+ *
+ * @param allowExt If true, is allowed to expand external entities
+ * (expanding text); if false, is not (expanding attribute value).
+ *
+ * @return Either single-character replacement (which is NOT to be
+ * reparsed), or null char (0) to indicate expansion is done via
+ * input source.
+ */
+ protected int fullyResolveEntity(boolean allowExt)
+ throws XMLStreamException
+ {
+ char c = getNextCharFromCurrent(SUFFIX_IN_ENTITY_REF);
+ // Do we have a (numeric) character entity reference?
+ if (c == '#') { // numeric
+ final StringBuffer originalSurface = new StringBuffer("#");
+ int ch = resolveCharEnt(originalSurface);
+ if (mCfgTreatCharRefsAsEntities) {
+ final char[] originalChars = new char[originalSurface.length()];
+ originalSurface.getChars(0, originalSurface.length(), originalChars, 0);
+ mCurrEntity = getIntEntity(ch, originalChars);
+ return 0;
+ }
+ return ch;
+ }
+
+ String id = parseEntityName(c);
+
+ // Perhaps we have a pre-defined char reference?
+ c = id.charAt(0);
+ /*
+ * 16-May-2004, TSa: Should custom entities (or ones defined in int/ext subset) override
+ * pre-defined settings for these?
+ */
+ char d = CHAR_NULL;
+ if (c == 'a') { // amp or apos?
+ if (id.equals("amp")) {
+ d = '&';
+ } else if (id.equals("apos")) {
+ d = '\'';
+ }
+ } else if (c == 'g') { // gt?
+ if (id.length() == 2 && id.charAt(1) == 't') {
+ d = '>';
+ }
+ } else if (c == 'l') { // lt?
+ if (id.length() == 2 && id.charAt(1) == 't') {
+ d = '<';
+ }
+ } else if (c == 'q') { // quot?
+ if (id.equals("quot")) {
+ d = '"';
+ }
+ }
+
+ if (d != CHAR_NULL) {
+ if (mCfgTreatCharRefsAsEntities) {
+ final char[] originalChars = new char[id.length()];
+ id.getChars(0, id.length(), originalChars, 0);
+ mCurrEntity = getIntEntity(d, originalChars);
+ return 0;
+ }
+ return d;
+ }
+
+ final EntityDecl e = expandEntity(id, allowExt, null);
+ if (mCfgTreatCharRefsAsEntities) {
+ mCurrEntity = e;
+ }
+ return 0;
+ }
+
+ /**
+ * Returns an entity (possibly from cache) for the argument character using the encoded
+ * representation in mInputBuffer[entityStartPos ... mInputPtr-1].
+ */
+ protected EntityDecl getIntEntity(int ch, final char[] originalChars)
+ {
+ String cacheKey = new String(originalChars);
+
+ IntEntity entity = (IntEntity) mCachedEntities.get(cacheKey);
+ if (entity == null) {
+ String repl;
+ if (ch <= 0xFFFF) {
+ repl = Character.toString((char) ch);
+ } else {
+ StringBuffer sb = new StringBuffer(2);
+ ch -= 0x10000;
+ sb.append((char) ((ch >> 10) + 0xD800));
+ sb.append((char) ((ch & 0x3FF) + 0xDC00));
+ repl = sb.toString();
+ }
+ entity = IntEntity.create(new String(originalChars), repl);
+ mCachedEntities.put(cacheKey, entity);
+ }
+ return entity;
+ }
+
+
+ /**
+ * Helper method that will try to expand a parsed entity (parameter or
+ * generic entity).
+ *<p>
+ * note: called by sub-classes (dtd parser), needs to be protected.
+ *
+ * @param id Name of the entity being expanded
+ * @param allowExt Whether external entities can be expanded or not; if
+ * not, and the entity to expand would be external one, an exception
+ * will be thrown
+ */
+ protected EntityDecl expandEntity(String id, boolean allowExt,
+ Object extraArg)
+ throws XMLStreamException
+ {
+ mCurrName = id;
+
+ EntityDecl ed = findEntity(id, extraArg);
+
+ if (ed == null) {
+ /* 30-Sep-2005, TSa: As per [WSTX-5], let's only throw exception
+ * if we have to resolve it (otherwise it's just best-effort,
+ * and null is ok)
+ */
+ /* 02-Oct-2005, TSa: Plus, [WSTX-4] adds "undeclared entity
+ * resolver"
+ */
+ if (mCfgReplaceEntities) {
+ mCurrEntity = expandUnresolvedEntity(id);
+ }
+ return null;
+ }
+
+ if (!mCfgTreatCharRefsAsEntities || this instanceof MinimalDTDReader) {
+ expandEntity(ed, allowExt);
+ }
+
+ return ed;
+ }
+
+ /**
+ *
+ *<p>
+ * note: defined as private for documentation, ie. it's just called
+ * from within this class (not sub-classes), from one specific method
+ * (see above)
+ *
+ * @param ed Entity to be expanded
+ * @param allowExt Whether external entities are allowed or not.
+ */
+ private void expandEntity(EntityDecl ed, boolean allowExt)
+ throws XMLStreamException
+ {
+ String id = ed.getName();
+
+ /* Very first thing; we can immediately check if expanding
+ * this entity would result in infinite recursion:
+ */
+ if (mInput.isOrIsExpandedFrom(id)) {
+ throwRecursionError(id);
+ }
+
+ /* Should not refer unparsed entities from attribute values
+ * or text content (except via notation mechanism, but that's
+ * not parsed here)
+ */
+ if (!ed.isParsed()) {
+ throwParseError("Illegal reference to unparsed external entity \"{0}\"", id, null);
+ }
+
+ // 28-Jun-2004, TSa: Do we support external entity expansion?
+ boolean isExt = ed.isExternal();
+ if (isExt) {
+ if (!allowExt) { // never ok in attribute value...
+ 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);
+ }
+ if (!mConfig.willSupportExternalEntities()) {
+ throwParseError("Encountered a reference to external entity \"{0}\", but stream reader has feature \"{1}\" disabled",
+ id, XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES);
+ }
+ }
+
+ // First, let's give current context chance to save its stuff
+ WstxInputSource oldInput = mInput;
+ oldInput.saveContext(this);
+ WstxInputSource newInput = null;
+ try {
+ newInput = ed.expand(oldInput, mEntityResolver, mConfig, mDocXmlVersion);
+ } catch (FileNotFoundException fex) {
+ /* Let's catch and rethrow this just so we get more meaningful
+ * description (with input source position etc)
+ */
+ throwParseError("(was {0}) {1}", fex.getClass().getName(), fex.getMessage());
+ } catch (IOException ioe) {
+ throw constructFromIOE(ioe);
+ }
+ /* And then we'll need to make sure new input comes from the new
+ * input source
+ */
+ initInputSource(newInput, isExt, id);
+ }
+
+ /**
+ *<p>
+ * note: only called from the local expandEntity() method
+ */
+ private EntityDecl expandUnresolvedEntity(String id)
+ throws XMLStreamException
+ {
+ XMLResolver resolver = mConfig.getUndeclaredEntityResolver();
+ if (resolver != null) {
+ /* Ok, we can check for recursion here; but let's only do that
+ * if there is any chance that it might get resolved by
+ * the special resolver (it must have been resolved this way
+ * earlier, too...)
+ */
+ if (mInput.isOrIsExpandedFrom(id)) {
+ throwRecursionError(id);
+ }
+
+ WstxInputSource oldInput = mInput;
+ oldInput.saveContext(this);
+ // null, null -> no public or system ids
+ int xmlVersion = mDocXmlVersion;
+ // 05-Feb-2006, TSa: If xmlVersion not explicitly known, defaults to 1.0
+ if (xmlVersion == XmlConsts.XML_V_UNKNOWN) {
+ xmlVersion = XmlConsts.XML_V_10;
+ }
+ WstxInputSource newInput;
+ try {
+ newInput = DefaultInputResolver.resolveEntityUsing
+ (oldInput, id, null, null, resolver, mConfig, xmlVersion);
+ if (mCfgTreatCharRefsAsEntities) {
+ return new IntEntity(WstxInputLocation.getEmptyLocation(), newInput.getEntityId(),
+ newInput.getSource(), new char[]{}, WstxInputLocation.getEmptyLocation());
+ }
+ } catch (IOException ioe) {
+ throw constructFromIOE(ioe);
+ }
+ if (newInput != null) {
+ // true -> is external
+ initInputSource(newInput, true, id);
+ return null;
+ }
+ }
+ handleUndeclaredEntity(id);
+ return null;
+ }
+
+ /*
+ ///////////////////////////////////////////////////////////
+ // Abstract methods for sub-classes to implement
+ ///////////////////////////////////////////////////////////
+ */
+
+ /**
+ * Abstract method for sub-classes to implement, for finding
+ * a declared general or parsed entity.
+ *
+ * @param id Identifier of the entity to find
+ * @param arg Optional argument passed from caller; needed by DTD
+ * reader.
+ */
+ protected abstract EntityDecl findEntity(String id, Object arg)
+ throws XMLStreamException;
+
+ /**
+ * This method gets called if a declaration for an entity was not
+ * found in entity expanding mode (enabled by default for xml reader,
+ * always enabled for dtd reader).
+ */
+ protected abstract void handleUndeclaredEntity(String id)
+ throws XMLStreamException;
+
+ protected abstract void handleIncompleteEntityProblem(WstxInputSource closing)
+ throws XMLStreamException;
+
+ /*
+ ///////////////////////////////////////////////////////////
+ // Basic tokenization
+ ///////////////////////////////////////////////////////////
+ */
+
+ /**
+ * Method that will parse name token (roughly equivalent to XML specs;
+ * although bit lenier for more efficient handling); either uri prefix,
+ * or local name.
+ *<p>
+ * Much of complexity in this method has to do with the intention to
+ * try to avoid any character copies. In this optimal case algorithm
+ * would be fairly simple. However, this only works if all data is
+ * already in input buffer... if not, copy has to be made halfway
+ * through parsing, and that complicates things.
+ *<p>
+ * One thing to note is that String returned has been canonicalized
+ * and (if necessary) added to symbol table. It can thus be compared
+ * against other such (usually id) Strings, with simple equality operator.
+ *
+ * @param c First character of the name; not yet checked for validity
+ *
+ * @return Canonicalized name String (which may have length 0, if
+ * EOF or non-name-start char encountered)
+ */
+ protected String parseLocalName(char c)
+ throws XMLStreamException
+ {
+ /* Has to start with letter, or '_' (etc); we won't allow ':' as that
+ * is taken as namespace separator; no use trying to optimize
+ * heavily as it's 98% likely it is a valid char...
+ */
+ if (!isNameStartChar(c)) {
+ if (c == ':') {
+ throwUnexpectedChar(c, " (missing namespace prefix?)");
+ }
+ throwUnexpectedChar(c, " (expected a name start character)");
+ }
+
+ int ptr = mInputPtr;
+ int hash = (int) c;
+ final int inputLen = mInputEnd;
+ int startPtr = ptr-1; // already read previous char
+ final char[] inputBuf = mInputBuffer;
+
+ /* After which there may be zero or more name chars
+ * we have to consider
+ */
+ while (true) {
+ if (ptr >= inputLen) {
+ /* Ok, identifier may continue past buffer end, need
+ * to continue with part 2 (separate method, as this is
+ * not as common as having it all in buffer)
+ */
+ mInputPtr = ptr;
+ return parseLocalName2(startPtr, hash);
+ }
+ // Ok, we have the char... is it a name char?
+ c = inputBuf[ptr];
+ if (c < CHAR_LOWEST_LEGAL_LOCALNAME_CHAR) {
+ break;
+ }
+ if (!isNameChar(c)) {
+ break;
+ }
+ hash = (hash * 31) + (int) c;
+ ++ptr;
+ }
+ mInputPtr = ptr;
+ return mSymbols.findSymbol(mInputBuffer, startPtr, ptr - startPtr, hash);
+ }
+
+ /**
+ * Second part of name token parsing; called when name can continue
+ * past input buffer end (so only part was read before calling this
+ * method to read the rest).
+ *<p>
+ * Note that this isn't heavily optimized, on assumption it's not
+ * called very often.
+ */
+ protected String parseLocalName2(int start, int hash)
+ throws XMLStreamException
+ {
+ int ptr = mInputEnd - start;
+ // Let's assume fairly short names
+ char[] outBuf = getNameBuffer(ptr+8);
+
+ if (ptr > 0) {
+ System.arraycopy(mInputBuffer, start, outBuf, 0, ptr);
+ }
+
+ int outLen = outBuf.length;
+ while (true) {
+ // note: names can not cross input block (entity) boundaries...
+ if (mInputPtr >= mInputEnd) {
+ if (!loadMoreFromCurrent()) {
+ break;
+ }
+ }
+ char c = mInputBuffer[mInputPtr];
+ if (c < CHAR_LOWEST_LEGAL_LOCALNAME_CHAR) {
+ break;
+ }
+ if (!isNameChar(c)) {
+ break;
+ }
+ ++mInputPtr;
+ if (ptr >= outLen) {
+ mNameBuffer = outBuf = expandBy50Pct(outBuf);
+ outLen = outBuf.length;
+ }
+ outBuf[ptr++] = c;
+ hash = (hash * 31) + (int) c;
+ }
+ // Still need to canonicalize the name:
+ return mSymbols.findSymbol(outBuf, 0, ptr, hash);
+ }
+
+ /**
+ * Method that will parse 'full' name token; what full means depends on
+ * whether reader is namespace aware or not. If it is, full name means
+ * local name with no namespace prefix (PI target, entity/notation name);
+ * if not, name can contain arbitrary number of colons. Note that
+ * element and attribute names are NOT parsed here, so actual namespace
+ * prefix separation can be handled properly there.
+ *<p>
+ * Similar to {@link #parseLocalName}, much of complexity stems from
+ * trying to avoid copying name characters from input buffer.
+ *<p>
+ * Note that returned String will be canonicalized, similar to
+ * {@link #parseLocalName}, but without separating prefix/local name.
+ *
+ * @return Canonicalized name String (which may have length 0, if
+ * EOF or non-name-start char encountered)
+ */
+ protected String parseFullName()
+ throws XMLStreamException
+ {
+ if (mInputPtr >= mInputEnd) {
+ loadMoreFromCurrent();
+ }
+ return parseFullName(mInputBuffer[mInputPtr++]);
+ }
+
+ protected String parseFullName(char c)
+ throws XMLStreamException
+ {
+ // First char has special handling:
+ if (!isNameStartChar(c)) {
+ if (c == ':') { // no name.... generally an error:
+ if (mCfgNsEnabled) {
+ throwNsColonException(parseFNameForError());
+ }
+ // Ok, that's fine actually
+ } else {
+ if (c <= CHAR_SPACE) {
+ throwUnexpectedChar(c, " (missing name?)");
+ }
+ throwUnexpectedChar(c, " (expected a name start character)");
+ }
+ }
+
+ int ptr = mInputPtr;
+ int hash = (int) c;
+ int inputLen = mInputEnd;
+ int startPtr = ptr-1; // to account for the first char
+
+ /* After which there may be zero or more name chars
+ * we have to consider
+ */
+ while (true) {
+ if (ptr >= inputLen) {
+ /* Ok, identifier may continue past buffer end, need
+ * to continue with part 2 (separate method, as this is
+ * not as common as having it all in buffer)
+ */
+ mInputPtr = ptr;
+ return parseFullName2(startPtr, hash);
+ }
+ c = mInputBuffer[ptr];
+ if (c == ':') { // colon only allowed in non-NS mode
+ if (mCfgNsEnabled) {
+ mInputPtr = ptr;
+ throwNsColonException(new String(mInputBuffer, startPtr, ptr - startPtr) + parseFNameForError());
+ }
+ } else {
+ if (c < CHAR_LOWEST_LEGAL_LOCALNAME_CHAR) {
+ break;
+ }
+ if (!isNameChar(c)) {
+ break;
+ }
+ }
+ hash = (hash * 31) + (int) c;
+ ++ptr;
+ }
+ mInputPtr = ptr;
+ return mSymbols.findSymbol(mInputBuffer, startPtr, ptr - startPtr, hash);
+ }
+
+ protected String parseFullName2(int start, int hash)
+ throws XMLStreamException
+ {
+ int ptr = mInputEnd - start;
+ // Let's assume fairly short names
+ char[] outBuf = getNameBuffer(ptr+8);
+
+ if (ptr > 0) {
+ System.arraycopy(mInputBuffer, start, outBuf, 0, ptr);
+ }
+
+ int outLen = outBuf.length;
+ while (true) {
+ /* 06-Sep-2004, TSa: Name tokens are not allowed to continue
+ * past entity expansion ranges... that is, all characters
+ * have to come from the same input source. Thus, let's only
+ * load things from same input level
+ */
+ if (mInputPtr >= mInputEnd) {
+ if (!loadMoreFromCurrent()) {
+ break;
+ }
+ }
+ char c = mInputBuffer[mInputPtr];
+ if (c == ':') { // colon only allowed in non-NS mode
+ if (mCfgNsEnabled) {
+ throwNsColonException(new String(outBuf, 0, ptr) + c + parseFNameForError());
+ }
+ } else if (c < CHAR_LOWEST_LEGAL_LOCALNAME_CHAR) {
+ break;
+ } else if (!isNameChar(c)) {
+ break;
+ }
+ ++mInputPtr;
+
+ if (ptr >= outLen) {
+ mNameBuffer = outBuf = expandBy50Pct(outBuf);
+ outLen = outBuf.length;
+ }
+ outBuf[ptr++] = c;
+ hash = (hash * 31) + (int) c;
+ }
+
+ // Still need to canonicalize the name:
+ return mSymbols.findSymbol(outBuf, 0, ptr, hash);
+ }
+
+ /**
+ * Method called to read in full name, including unlimited number of
+ * namespace separators (':'), for the purpose of displaying name in
+ * an error message. Won't do any further validations, and parsing
+ * is not optimized: main need is just to get more meaningful error
+ * messages.
+ */
+ protected String parseFNameForError()
+ throws XMLStreamException
+ {
+ StringBuffer sb = new StringBuffer(100);
+ while (true) {
+ char c;
+
+ if (mInputPtr < mInputEnd) {
+ c = mInputBuffer[mInputPtr++];
+ } else { // can't error here, so let's accept EOF for now:
+ int i = getNext();
+ if (i < 0) {
+ break;
+ }
+ c = (char) i;
+ }
+ if (c != ':' && !isNameChar(c)) {
+ --mInputPtr;
+ break;
+ }
+ sb.append(c);
+ }
+ return sb.toString();
+ }
+
+ protected final String parseEntityName(char c)
+ throws XMLStreamException
+ {
+ String id = parseFullName(c);
+ // Needs to be followed by a semi-colon, too.. from same input source:
+ if (mInputPtr >= mInputEnd) {
+ if (!loadMoreFromCurrent()) {
+ throwParseError("Missing semicolon after reference for entity \"{0}\"", id, null);
+ }
+ }
+ c = mInputBuffer[mInputPtr++];
+ if (c != ';') {
+ throwUnexpectedChar(c, "; expected a semi-colon after the reference for entity '"+id+"'");
+ }
+ return id;
+ }
+
+ /**
+ * Note: does not check for number of colons, amongst other things.
+ * Main idea is to skip through what superficially seems like a valid
+ * id, nothing more. This is only done when really skipping through
+ * something we do not care about at all: not even whether names/ids
+ * would be valid (for example, when ignoring internal DTD subset).
+ *
+ * @return Length of skipped name.
+ */
+ protected int skipFullName(char c)
+ throws XMLStreamException
+ {
+ if (!isNameStartChar(c)) {
+ --mInputPtr;
+ return 0;
+ }
+
+ /* After which there may be zero or more name chars
+ * we have to consider
+ */
+ int count = 1;
+ while (true) {
+ c = (mInputPtr < mInputEnd) ?
+ mInputBuffer[mInputPtr++] : getNextChar(SUFFIX_EOF_EXP_NAME);
+ if (c != ':' && !isNameChar(c)) {
+ break;
+ }
+ ++count;
+ }
+ return count;
+ }
+
+ /**
+ * Simple parsing method that parses system ids, which are generally
+ * used in entities (from DOCTYPE declaration to internal/external
+ * subsets).
+ *<p>
+ * NOTE: returned String is not canonicalized, on assumption that
+ * external ids may be longish, and are not shared all that often, as
+ * they are generally just used for resolving paths, if anything.
+ *<br />
+ * Also note that this method is not heavily optimized, as it's not
+ * likely to be a bottleneck for parsing.
+ */
+ protected final String parseSystemId(char quoteChar, boolean convertLFs,
+ String errorMsg)
+ throws XMLStreamException
+ {
+ char[] buf = getNameBuffer(-1);
+ int ptr = 0;
+
+ while (true) {
+ char c = (mInputPtr < mInputEnd) ?
+ mInputBuffer[mInputPtr++] : getNextChar(errorMsg);
+ if (c == quoteChar) {
+ break;
+ }
+ /* ??? 14-Jun-2004, TSa: Should we normalize linefeeds or not?
+ * It seems like we should, for all input... so that's the way it
+ * works.
+ */
+ if (c == '\n') {
+ markLF();
+ } else if (c == '\r') {
+ if (peekNext() == '\n') {
+ ++mInputPtr;
+ if (!convertLFs) {
+ /* The only tricky thing; need to preserve 2-char LF; need to
+ * output one char from here, then can fall back to default:
+ */
+ if (ptr >= buf.length) {
+ buf = expandBy50Pct(buf);
+ }
+ buf[ptr++] = '\r';
+ }
+ c = '\n';
+ } else if (convertLFs) {
+ c = '\n';
+ }
+ }
+
+ // Other than that, let's just append it:
+ if (ptr >= buf.length) {
+ buf = expandBy50Pct(buf);
+ }
+ buf[ptr++] = c;
+ }
+
+ return (ptr == 0) ? "" : new String(buf, 0, ptr);
+ }
+
+ /**
+ * Simple parsing method that parses system ids, which are generally
+ * used in entities (from DOCTYPE declaration to internal/external
+ * subsets).
+ *<p>
+ * As per xml specs, the contents are actually normalized.
+ *<p>
+ * NOTE: returned String is not canonicalized, on assumption that
+ * external ids may be longish, and are not shared all that often, as
+ * they are generally just used for resolving paths, if anything.
+ *<br />
+ * Also note that this method is not heavily optimized, as it's not
+ * likely to be a bottleneck for parsing.
+ */
+ protected final String parsePublicId(char quoteChar, String errorMsg)
+ throws XMLStreamException
+ {
+ char[] buf = getNameBuffer(-1);
+ int ptr = 0;
+ boolean spaceToAdd = false;
+
+ while (true) {
+ char c = (mInputPtr < mInputEnd) ?
+ mInputBuffer[mInputPtr++] : getNextChar(errorMsg);
+ if (c == quoteChar) {
+ break;
+ }
+ if (c == '\n') {
+ markLF();
+ spaceToAdd = true;
+ continue;
+ } else if (c == '\r') {
+ if (peekNext() == '\n') {
+ ++mInputPtr;
+ }
+ spaceToAdd = true;
+ continue;
+ } else if (c == CHAR_SPACE) {
+ spaceToAdd = true;
+ continue;
+ } else {
+ // Verify it's a legal pubid char (see XML spec, #13, from 2.3)
+ if ((c >= VALID_PUBID_CHAR_COUNT)
+ || sPubidValidity[c] != PUBID_CHAR_VALID_B) {
+ throwUnexpectedChar(c, " in public identifier");
+ }
+ }
+
+ // Other than that, let's just append it:
+ if (ptr >= buf.length) {
+ buf = expandBy50Pct(buf);
+ }
+ /* Space-normalization means scrapping leading and trailing
+ * white space, and coalescing remaining ws into single spaces.
+ */
+ if (spaceToAdd) { // pending white space to add?
+ if (c == CHAR_SPACE) { // still a space; let's skip
+ continue;
+ }
+ /* ok: if we have non-space, we'll either forget about
+ * space(s) (if nothing has been output, ie. leading space),
+ * or output a single space (in-between non-white space)
+ */
+ spaceToAdd = false;
+ if (ptr > 0) {
+ buf[ptr++] = CHAR_SPACE;
+ if (ptr >= buf.length) {
+ buf = expandBy50Pct(buf);
+ }
+ }
+ }
+ buf[ptr++] = c;
+ }
+
+ return (ptr == 0) ? "" : new String(buf, 0, ptr);
+ }
+
+ protected final void parseUntil(TextBuffer tb, char endChar, boolean convertLFs,
+ String errorMsg)
+ throws XMLStreamException
+ {
+ // Let's first ensure we have some data in there...
+ if (mInputPtr >= mInputEnd) {
+ loadMore(errorMsg);
+ }
+ while (true) {
+ // Let's loop consequtive 'easy' spans:
+ char[] inputBuf = mInputBuffer;
+ int inputLen = mInputEnd;
+ int ptr = mInputPtr;
+ int startPtr = ptr;
+ while (ptr < inputLen) {
+ char c = inputBuf[ptr++];
+ if (c == endChar) {
+ int thisLen = ptr - startPtr - 1;
+ if (thisLen > 0) {
+ tb.append(inputBuf, startPtr, thisLen);
+ }
+ mInputPtr = ptr;
+ return;
+ }
+ if (c == '\n') {
+ mInputPtr = ptr; // markLF() requires this
+ markLF();
+ } else if (c == '\r') {
+ if (!convertLFs && ptr < inputLen) {
+ if (inputBuf[ptr] == '\n') {
+ ++ptr;
+ }
+ mInputPtr = ptr;
+ markLF();
+ } else {
+ int thisLen = ptr - startPtr - 1;
+ if (thisLen > 0) {
+ tb.append(inputBuf, startPtr, thisLen);
+ }
+ mInputPtr = ptr;
+ c = getNextChar(errorMsg);
+ if (c != '\n') {
+ --mInputPtr; // pusback
+ tb.append(convertLFs ? '\n' : '\r');
+ } else {
+ if (convertLFs) {
+ tb.append('\n');
+ } else {
+ tb.append('\r');
+ tb.append('\n');
+ }
+ }
+ startPtr = ptr = mInputPtr;
+ markLF();
+ }
+ }
+ }
+ int thisLen = ptr - startPtr;
+ if (thisLen > 0) {
+ tb.append(inputBuf, startPtr, thisLen);
+ }
+ loadMore(errorMsg);
+ startPtr = ptr = mInputPtr;
+ inputBuf = mInputBuffer;
+ inputLen = mInputEnd;
+ }
+ }
+
+ /*
+ ///////////////////////////////////////////////////////////
+ // Internal methods
+ ///////////////////////////////////////////////////////////
+ */
+
+ private int resolveCharEnt(StringBuffer originalCharacters)
+ throws XMLStreamException
+ {
+ int value = 0;
+ char c = getNextChar(SUFFIX_IN_ENTITY_REF);
+
+ if (originalCharacters != null) {
+ originalCharacters.append(c);
+ }
+
+ if (c == 'x') { // hex
+ while (true) {
+ c = (mInputPtr < mInputEnd) ? mInputBuffer[mInputPtr++]
+ : getNextCharFromCurrent(SUFFIX_IN_ENTITY_REF);
+ if (c == ';') {
+ break;
+ }
+
+ if (originalCharacters != null) {
+ originalCharacters.append(c);
+ }
+ value = value << 4;
+ if (c <= '9' && c >= '0') {
+ value += (c - '0');
+ } else if (c >= 'a' && c <= 'f') {
+ value += 10 + (c - 'a');
+ } else if (c >= 'A' && c <= 'F') {
+ value += 10 + (c - 'A');
+ } else {
+ throwUnexpectedChar(c, "; expected a hex digit (0-9a-fA-F).");
+ }
+ // Overflow?
+ if (value > MAX_UNICODE_CHAR) {
+ reportUnicodeOverflow();
+ }
+ }
+ } else { // numeric (decimal)
+ while (c != ';') {
+ if (c <= '9' && c >= '0') {
+ value = (value * 10) + (c - '0');
+ // Overflow?
+ if (value > MAX_UNICODE_CHAR) {
+ reportUnicodeOverflow();
+ }
+ } else {
+ throwUnexpectedChar(c, "; expected a decimal number.");
+ }
+ c = (mInputPtr < mInputEnd) ? mInputBuffer[mInputPtr++]
+ : getNextCharFromCurrent(SUFFIX_IN_ENTITY_REF);
+
+ if (originalCharacters != null && c != ';') {
+ originalCharacters.append(c);
+ }
+ }
+ }
+ validateChar(value);
+ return value;
+ }
+
+ /**
+ * Method that will verify that expanded Unicode codepoint is a valid
+ * XML content character.
+ */
+ private final void validateChar(int value)
+ throws XMLStreamException
+ {
+ /* 24-Jan-2006, TSa: Ok, "high" Unicode chars are problematic,
+ * need to be reported by a surrogate pair..
+ */
+ if (value >= 0xD800) {
+ if (value < 0xE000) { // no surrogates via entity expansion
+ reportIllegalChar(value);
+ }
+ if (value > 0xFFFF) {
+ // Within valid range at all?
+ if (value > MAX_UNICODE_CHAR) {
+ reportUnicodeOverflow();
+ }
+ } else if (value >= 0xFFFE) { // 0xFFFE and 0xFFFF are illegal too
+ reportIllegalChar(value);
+ }
+ // Ok, fine as is
+ } else if (value < 32) {
+ if (value == 0) {
+ throwParseError("Invalid character reference: null character not allowed in XML content.");
+ }
+ // XML 1.1 allows most other chars; 1.0 does not: However Exchange sends such chars with XML 1.0
+ //if (!mXml11 &&
+ // (value != 0x9 && value != 0xA && value != 0xD)) {
+ // reportIllegalChar(value);
+ //}
+ }
+ }
+
+ protected final char[] getNameBuffer(int minSize)
+ {
+ char[] buf = mNameBuffer;
+
+ if (buf == null) {
+ mNameBuffer = buf = new char[(minSize > 48) ? (minSize+16) : 64];
+ } else if (minSize >= buf.length) { // let's allow one char extra...
+ int len = buf.length;
+ len += (len >> 1); // grow by 50%
+ mNameBuffer = buf = new char[(minSize >= len) ? (minSize+16) : len];
+ }
+ return buf;
+ }
+
+ protected final char[] expandBy50Pct(char[] buf)
+ {
+ int len = buf.length;
+ char[] newBuf = new char[len + (len >> 1)];
+ System.arraycopy(buf, 0, newBuf, 0, len);
+ return newBuf;
+ }
+
+ /**
+ * Method called to throw an exception indicating that a name that
+ * should not be namespace-qualified (PI target, entity/notation name)
+ * is one, and reader is namespace aware.
+ */
+ private void throwNsColonException(String name)
+ throws XMLStreamException
+ {
+ throwParseError("Illegal name \"{0}\" (PI target, entity/notation name): can not contain a colon (XML Namespaces 1.0#6)", name, null);
+ }
+
+ private void throwRecursionError(String entityName)
+ throws XMLStreamException
+ {
+ throwParseError("Illegal entity expansion: entity \"{0}\" expands itself recursively.", entityName, null);
+ }
+
+ private void reportUnicodeOverflow()
+ throws XMLStreamException
+ {
+ throwParseError("Illegal character entity: value higher than max allowed (0x{0})", Integer.toHexString(MAX_UNICODE_CHAR), null);
+ }
+
+ private void reportIllegalChar(int value)
+ throws XMLStreamException
+ {
+ throwParseError("Illegal character entity: expansion character (code 0x{0}", Integer.toHexString(value), null);
+ }
+}
diff --git a/src/java/davmail/AbstractConnection.java b/src/java/davmail/AbstractConnection.java
new file mode 100644
index 0000000..2c49277
--- /dev/null
+++ b/src/java/davmail/AbstractConnection.java
@@ -0,0 +1,278 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail;
+
+import davmail.exception.DavMailException;
+import davmail.exchange.ExchangeSession;
+import davmail.ui.tray.DavGatewayTray;
+import org.apache.commons.codec.binary.Base64;
+
+import java.io.*;
+import java.net.Socket;
+
+
+/**
+ * Generic connection common to pop3 and smtp implementations
+ */
+public class AbstractConnection extends Thread {
+
+ protected enum State {
+ INITIAL, LOGIN, USER, PASSWORD, AUTHENTICATED, STARTMAIL, RECIPIENT, MAILDATA
+ }
+
+ protected static class LineReaderInputStream extends PushbackInputStream {
+ final String encoding;
+
+ /**
+ * @inheritDoc
+ */
+ protected LineReaderInputStream(InputStream in, String encoding) {
+ super(in);
+ if (encoding == null) {
+ this.encoding = "ASCII";
+ } else {
+ this.encoding = encoding;
+ }
+ }
+
+ public String readLine() throws IOException {
+ ByteArrayOutputStream baos = null;
+ int b;
+ while ((b = read()) > -1) {
+ if (b == '\r') {
+ int next = read();
+ if (next != '\n') {
+ unread(next);
+ }
+ break;
+ } else if (b == '\n') {
+ break;
+ }
+ if (baos == null) {
+ baos = new ByteArrayOutputStream();
+ }
+ baos.write(b);
+ }
+ if (baos != null) {
+ return new String(baos.toByteArray(), encoding);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Read byteSize bytes from inputStream, return content as String.
+ *
+ * @param byteSize content size
+ * @return content
+ * @throws IOException on error
+ */
+ public String readContentAsString(int byteSize) throws IOException {
+ return new String(readContent(byteSize), encoding);
+ }
+
+ /**
+ * Read byteSize bytes from inputStream, return content as byte array.
+ *
+ * @param byteSize content size
+ * @return content
+ * @throws IOException on error
+ */
+ public byte[] readContent(int byteSize) throws IOException {
+ byte[] buffer = new byte[byteSize];
+ int startIndex = 0;
+ int count = 0;
+ while (count >= 0 && startIndex < byteSize) {
+ count = in.read(buffer, startIndex, byteSize - startIndex);
+ startIndex += count;
+ }
+ if (startIndex < byteSize) {
+ throw new DavMailException("EXCEPTION_END_OF_STREAM");
+ }
+
+ return buffer;
+ }
+ }
+
+ protected final Socket client;
+
+ protected LineReaderInputStream in;
+ protected OutputStream os;
+ // user name and password initialized through connection
+ protected String userName;
+ protected String password;
+ // connection state
+ protected State state = State.INITIAL;
+ // Exchange session proxy
+ protected ExchangeSession session;
+
+ /**
+ * Only set the thread name and socket
+ *
+ * @param name thread type name
+ * @param clientSocket client socket
+ */
+ public AbstractConnection(String name, Socket clientSocket) {
+ super(name + '-' + clientSocket.getPort());
+ this.client = clientSocket;
+ setDaemon(true);
+ }
+
+ /**
+ * Initialize the streams and set thread name.
+ *
+ * @param name thread type name
+ * @param clientSocket client socket
+ * @param encoding socket stream encoding
+ */
+ public AbstractConnection(String name, Socket clientSocket, String encoding) {
+ super(name + '-' + clientSocket.getPort());
+ this.client = clientSocket;
+ try {
+ in = new LineReaderInputStream(client.getInputStream(), encoding);
+ os = new BufferedOutputStream(client.getOutputStream());
+ } catch (IOException e) {
+ close();
+ DavGatewayTray.error(new BundleMessage("LOG_EXCEPTION_GETTING_SOCKET_STREAMS"), e);
+ }
+ }
+
+ /**
+ * Send message to client followed by CRLF.
+ *
+ * @param message message
+ * @throws IOException on error
+ */
+ public void sendClient(String message) throws IOException {
+ sendClient(null, message);
+ }
+
+ /**
+ * Send prefix and message to client followed by CRLF.
+ *
+ * @param prefix prefix
+ * @param message message
+ * @throws IOException on error
+ */
+ public void sendClient(String prefix, String message) throws IOException {
+ if (prefix != null) {
+ os.write(prefix.getBytes());
+ DavGatewayTray.debug(new BundleMessage("LOG_SEND_CLIENT_PREFIX_MESSAGE", prefix, message));
+ } else {
+ DavGatewayTray.debug(new BundleMessage("LOG_SEND_CLIENT_MESSAGE", message));
+ }
+ os.write(message.getBytes());
+ os.write((char) 13);
+ os.write((char) 10);
+ os.flush();
+ }
+
+ /**
+ * Send only bytes to client.
+ *
+ * @param messageBytes content
+ * @throws IOException on error
+ */
+ public void sendClient(byte[] messageBytes) throws IOException {
+ sendClient(messageBytes, 0, messageBytes.length);
+ }
+
+ /**
+ * Send only bytes to client.
+ *
+ * @param messageBytes content
+ * @param offset the start offset in the data.
+ * @param length the number of bytes to write.
+ * @throws IOException on error
+ */
+ public void sendClient(byte[] messageBytes, int offset, int length) throws IOException {
+ //StringBuffer logBuffer = new StringBuffer("> ");
+ //logBuffer.append(new String(messageBytes, offset, length));
+ //DavGatewayTray.debug(logBuffer.toString());
+ os.write(messageBytes, offset, length);
+ os.flush();
+ }
+
+ /**
+ * Read a line from the client connection.
+ * Log message to logger
+ *
+ * @return command line or null
+ * @throws IOException when unable to read line
+ */
+ public String readClient() throws IOException {
+ String line = in.readLine();
+ if (line != null) {
+ if (line.startsWith("PASS")) {
+ DavGatewayTray.debug(new BundleMessage("LOG_READ_CLIENT_PASS"));
+ // SMTP LOGIN
+ } else if (line.startsWith("AUTH LOGIN ")) {
+ DavGatewayTray.debug(new BundleMessage("LOG_READ_CLIENT_AUTH_LOGIN"));
+ // IMAP LOGIN
+ } else if (state == State.INITIAL && line.indexOf(' ') >= 0 &&
+ line.substring(line.indexOf(' ') + 1).toUpperCase().startsWith("LOGIN")) {
+ DavGatewayTray.debug(new BundleMessage("LOG_READ_CLIENT_LOGIN"));
+ } else if (state == State.PASSWORD) {
+ DavGatewayTray.debug(new BundleMessage("LOG_READ_CLIENT_PASSWORD"));
+ // HTTP Basic Authentication
+ } else if (line.startsWith("Authorization:")) {
+ DavGatewayTray.debug(new BundleMessage("LOG_READ_CLIENT_AUTHORIZATION"));
+ } else if (line.startsWith("AUTH PLAIN")) {
+ DavGatewayTray.debug(new BundleMessage("LOG_READ_CLIENT_AUTH_PLAIN"));
+ } else {
+ DavGatewayTray.debug(new BundleMessage("LOG_READ_CLIENT_LINE", line));
+ }
+ }
+ DavGatewayTray.switchIcon();
+ return line;
+ }
+
+ /**
+ * Close client connection, streams and Exchange session .
+ */
+ public void close() {
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException e2) {
+ DavGatewayTray.warn(new BundleMessage("LOG_EXCEPTION_CLOSING_CLIENT_INPUT_STREAM"), e2);
+ }
+ }
+ if (os != null) {
+ try {
+ os.close();
+ } catch (IOException e2) {
+ DavGatewayTray.warn(new BundleMessage("LOG_EXCEPTION_CLOSING_CLIENT_OUTPUT_STREAM"), e2);
+ }
+ }
+ try {
+ client.close();
+ } catch (IOException e2) {
+ DavGatewayTray.warn(new BundleMessage("LOG_EXCEPTION_CLOSING_CLIENT_SOCKET"), e2);
+ }
+ }
+
+ protected String base64Encode(String value) {
+ return new String(new Base64().encode(value.getBytes()));
+ }
+
+ protected String base64Decode(String value) {
+ return new String(new Base64().decode(value.getBytes()));
+ }
+}
diff --git a/src/java/davmail/AbstractServer.java b/src/java/davmail/AbstractServer.java
new file mode 100644
index 0000000..7638e4b
--- /dev/null
+++ b/src/java/davmail/AbstractServer.java
@@ -0,0 +1,206 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail;
+
+import davmail.exception.DavMailException;
+import davmail.ui.tray.DavGatewayTray;
+
+import javax.net.ServerSocketFactory;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+
+/**
+ * Generic abstract server common to SMTP and POP3 implementations
+ */
+public abstract class AbstractServer extends Thread {
+ protected boolean nosslFlag; // will cause same behavior as before with unchanged config files
+ private final int port;
+ private ServerSocket serverSocket;
+
+ /**
+ * Get server protocol name (SMTP, POP, IMAP, ...).
+ *
+ * @return server protocol name
+ */
+ public abstract String getProtocolName();
+
+ /**
+ * Server socket TCP port
+ *
+ * @return port
+ */
+ public int getPort() {
+ return port;
+ }
+
+ /**
+ * Create a ServerSocket to listen for connections.
+ * Start the thread.
+ *
+ * @param name thread name
+ * @param port tcp socket chosen port
+ * @param defaultPort tcp socket default port
+ */
+ public AbstractServer(String name, int port, int defaultPort) {
+ super(name);
+ setDaemon(true);
+ if (port == 0) {
+ this.port = defaultPort;
+ } else {
+ this.port = port;
+ }
+ }
+
+ /**
+ * Bind server socket on defined port.
+ *
+ * @throws DavMailException unable to create server socket
+ */
+ public void bind() throws DavMailException {
+ String bindAddress = Settings.getProperty("davmail.bindAddress");
+ String keystoreFile = Settings.getProperty("davmail.ssl.keystoreFile");
+
+ ServerSocketFactory serverSocketFactory;
+ if (keystoreFile == null || keystoreFile.length() == 0 || nosslFlag) {
+ serverSocketFactory = ServerSocketFactory.getDefault();
+ } else {
+ FileInputStream keyStoreInputStream = null;
+ try {
+ keyStoreInputStream = new FileInputStream(keystoreFile);
+ // keystore for keys and certificates
+ // keystore and private keys should be password protected...
+ KeyStore keystore = KeyStore.getInstance(Settings.getProperty("davmail.ssl.keystoreType"));
+ keystore.load(keyStoreInputStream, Settings.getCharArrayProperty("davmail.ssl.keystorePass"));
+
+ // KeyManagerFactory to create key managers
+ KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+
+ // initialize KMF to work with keystore
+ kmf.init(keystore, Settings.getCharArrayProperty("davmail.ssl.keyPass"));
+
+ // SSLContext is environment for implementing JSSE...
+ // create ServerSocketFactory
+ SSLContext sslContext = SSLContext.getInstance("SSLv3");
+
+ // initialize sslContext to work with key managers
+ sslContext.init(kmf.getKeyManagers(), null, null);
+
+ // create ServerSocketFactory from sslContext
+ serverSocketFactory = sslContext.getServerSocketFactory();
+ } catch (IOException ex) {
+ throw new DavMailException("LOG_EXCEPTION_CREATING_SSL_SERVER_SOCKET", getProtocolName(), port, ex.getMessage() == null ? ex.toString() : ex.getMessage());
+ } catch (GeneralSecurityException ex) {
+ throw new DavMailException("LOG_EXCEPTION_CREATING_SSL_SERVER_SOCKET", getProtocolName(), port, ex.getMessage() == null ? ex.toString() : ex.getMessage());
+ } finally {
+ if (keyStoreInputStream != null) {
+ try {
+ keyStoreInputStream.close();
+ } catch (IOException exc) {
+ DavGatewayTray.warn(new BundleMessage("LOG_EXCEPTION_CLOSING_KEYSTORE_INPUT_STREAM"), exc);
+ }
+ }
+ }
+ }
+ try {
+ // create the server socket
+ if (bindAddress == null || bindAddress.length() == 0) {
+ serverSocket = serverSocketFactory.createServerSocket(port);
+ } else {
+ serverSocket = serverSocketFactory.createServerSocket(port, 0, Inet4Address.getByName(bindAddress));
+ }
+ } catch (IOException e) {
+ throw new DavMailException("LOG_SOCKET_BIND_FAILED", getProtocolName(), port);
+ }
+ }
+
+
+ /**
+ * The body of the server thread. Loop forever, listening for and
+ * accepting connections from clients. For each connection,
+ * create a Connection object to handle communication through the
+ * new Socket.
+ */
+ @Override
+ public void run() {
+ Socket clientSocket = null;
+ AbstractConnection connection = null;
+ try {
+ //noinspection InfiniteLoopStatement
+ while (true) {
+ clientSocket = serverSocket.accept();
+ // set default timeout to 5 minutes
+ clientSocket.setSoTimeout(Settings.getIntProperty("davmail.clientSoTimeout", 300)*1000);
+ DavGatewayTray.debug(new BundleMessage("LOG_CONNECTION_FROM", clientSocket.getInetAddress(), port));
+ // only accept localhost connections for security reasons
+ if (Settings.getBooleanProperty("davmail.allowRemote") ||
+ clientSocket.getInetAddress().isLoopbackAddress()) {
+ connection = createConnectionHandler(clientSocket);
+ connection.start();
+ } else {
+ clientSocket.close();
+ DavGatewayTray.warn(new BundleMessage("LOG_EXTERNAL_CONNECTION_REFUSED"));
+ }
+ }
+ } catch (IOException e) {
+ // do not warn if exception on socket close (gateway restart)
+ if (!serverSocket.isClosed()) {
+ DavGatewayTray.warn(new BundleMessage("LOG_EXCEPTION_LISTENING_FOR_CONNECTIONS"), e);
+ }
+ } finally {
+ try {
+ if (clientSocket != null) {
+ clientSocket.close();
+ }
+ } catch (IOException e) {
+ DavGatewayTray.warn(new BundleMessage("LOG_EXCEPTION_CLOSING_CLIENT_SOCKET"), e);
+ }
+ if (connection != null) {
+ connection.close();
+ }
+ }
+ }
+
+ /**
+ * Create a connection handler for the current listener.
+ *
+ * @param clientSocket client socket
+ * @return connection handler
+ */
+ public abstract AbstractConnection createConnectionHandler(Socket clientSocket);
+
+ /**
+ * Close server socket
+ */
+ public void close() {
+ try {
+ if (serverSocket != null) {
+ serverSocket.close();
+ }
+ } catch (IOException e) {
+ DavGatewayTray.warn(new BundleMessage("LOG_EXCEPTION_CLOSING_SERVER_SOCKET"), e);
+ }
+ }
+}
diff --git a/src/java/davmail/BundleMessage.java b/src/java/davmail/BundleMessage.java
new file mode 100644
index 0000000..e020f4f
--- /dev/null
+++ b/src/java/davmail/BundleMessage.java
@@ -0,0 +1,218 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail;
+
+import davmail.exception.DavMailException;
+
+import java.io.Serializable;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Locale;
+import java.util.ResourceBundle;
+
+/**
+ * Internationalization message.
+ */
+public class BundleMessage implements Serializable {
+ /**
+ * Root locale to get english messages for logging.
+ */
+ public static final Locale ROOT_LOCALE = new Locale("", "");
+ protected static final String MESSAGE_BUNDLE_NAME = "davmailmessages";
+ protected final String key;
+ private final Object[] arguments;
+
+ /**
+ * Internationalization message.
+ *
+ * @param key message key in resource bundle
+ * @param arguments message values
+ */
+ public BundleMessage(String key, Object... arguments) {
+ this.key = key;
+ this.arguments = arguments;
+ }
+
+
+ /**
+ * Format message with the default locale.
+ *
+ * @return formatted message
+ */
+ public String format() {
+ return format(null);
+ }
+
+ /**
+ * Format message with the given locale.
+ *
+ * @param locale resource bundle locale
+ * @return formatted message
+ */
+ public String format(Locale locale) {
+ return BundleMessage.format(locale, key, arguments);
+ }
+
+ /**
+ * Format message for logging (with the root locale).
+ * Log file should remain in english
+ *
+ * @return log formatted message
+ */
+ public String formatLog() {
+ return format(ROOT_LOCALE);
+ }
+
+ /**
+ * Format message for logging (with the root locale).
+ * Log file should remain in english
+ *
+ * @return log formatted message
+ */
+ @Override
+ public String toString() {
+ return formatLog();
+ }
+
+ /**
+ * Get bundle for the given locale.
+ * Load the properties file for the given locale in a resource bundle
+ *
+ * @param locale resource bundle locale
+ * @return resource bundle
+ */
+ protected static ResourceBundle getBundle(Locale locale) {
+ if (locale == null) {
+ return ResourceBundle.getBundle(MESSAGE_BUNDLE_NAME);
+ } else {
+ return ResourceBundle.getBundle(MESSAGE_BUNDLE_NAME, locale);
+ }
+ }
+
+ /**
+ * Get formatted message for message key and values with the default locale.
+ *
+ * @param key message key in resource bundle
+ * @param arguments message values
+ * @return formatted message
+ */
+ public static String format(String key, Object... arguments) {
+ return format(null, key, arguments);
+ }
+
+ /**
+ * Get formatted message for message key and values with the given locale.
+ *
+ * @param locale resource bundle locale
+ * @param key message key in resource bundle
+ * @param arguments message values
+ * @return formatted message
+ */
+ public static String format(Locale locale, String key, Object... arguments) {
+ Object[] formattedArguments = null;
+ if (arguments != null) {
+ formattedArguments = new Object[arguments.length];
+ for (int i = 0; i < arguments.length; i++) {
+ if (arguments[i] instanceof BundleMessage) {
+ formattedArguments[i] = ((BundleMessage) arguments[i]).format(locale);
+ } else if (arguments[i] instanceof BundleMessageList) {
+ StringBuilder buffer = new StringBuilder();
+ for (BundleMessage bundleMessage : (BundleMessageList) arguments[i]) {
+ buffer.append(bundleMessage.format(locale));
+ }
+ formattedArguments[i] = buffer.toString();
+ } else if (arguments[i] instanceof DavMailException) {
+ formattedArguments[i] = ((DavMailException) arguments[i]).getMessage(locale);
+ } else if (arguments[i] instanceof Throwable) {
+ formattedArguments[i] = ((Throwable) arguments[i]).getMessage();
+ if (formattedArguments[i] == null) {
+ formattedArguments[i] = arguments[i].toString();
+ }
+ } else {
+ formattedArguments[i] = arguments[i];
+ }
+ }
+ }
+ return MessageFormat.format(getBundle(locale).getString(key), formattedArguments);
+ }
+
+ /**
+ * Get formatted log message for message key and values.
+ * Use the root locale
+ *
+ * @param key message key in resource bundle
+ * @param arguments message values
+ * @return formatted message
+ */
+ public static String formatLog(String key, Object... arguments) {
+ return format(ROOT_LOCALE, key, arguments);
+ }
+
+ /**
+ * Get formatted error message for bundle message and exception for logging.
+ * Use the root locale
+ *
+ * @param message bundle message
+ * @param e exception
+ * @return formatted message
+ */
+ public static String getExceptionLogMessage(BundleMessage message, Exception e) {
+ return getExceptionMessage(message, e, ROOT_LOCALE);
+ }
+
+ /**
+ * Get formatted error message for bundle message and exception with default locale.
+ *
+ * @param message bundle message
+ * @param e exception
+ * @return formatted message
+ */
+ public static String getExceptionMessage(BundleMessage message, Exception e) {
+ return getExceptionMessage(message, e, null);
+ }
+
+ /**
+ * Get formatted error message for bundle message and exception with given locale.
+ *
+ * @param message bundle message
+ * @param e exception
+ * @param locale bundle locale
+ * @return formatted message
+ */
+ public static String getExceptionMessage(BundleMessage message, Exception e, Locale locale) {
+ StringBuilder buffer = new StringBuilder();
+ if (message != null) {
+ buffer.append(message.format(locale)).append(' ');
+ }
+ if (e instanceof DavMailException) {
+ buffer.append(((DavMailException) e).getMessage(locale));
+ } else if (e.getMessage() != null) {
+ buffer.append(e.getMessage());
+ } else {
+ buffer.append(e.toString());
+ }
+ return buffer.toString();
+ }
+
+ /**
+ * Typed bundle message collection
+ */
+ public static class BundleMessageList extends ArrayList<BundleMessage> {
+ }
+}
diff --git a/src/java/davmail/DavGateway.java b/src/java/davmail/DavGateway.java
new file mode 100644
index 0000000..008b356
--- /dev/null
+++ b/src/java/davmail/DavGateway.java
@@ -0,0 +1,246 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail;
+
+import davmail.caldav.CaldavServer;
+import davmail.exception.DavMailException;
+import davmail.exchange.ExchangeSessionFactory;
+import davmail.http.DavGatewayHttpClientFacade;
+import davmail.http.DavGatewaySSLProtocolSocketFactory;
+import davmail.imap.ImapServer;
+import davmail.ldap.LdapServer;
+import davmail.pop.PopServer;
+import davmail.smtp.SmtpServer;
+import davmail.ui.tray.DavGatewayTray;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpStatus;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.apache.log4j.Logger;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+
+/**
+ * DavGateway main class
+ */
+public final class DavGateway {
+ private static final Logger LOGGER = Logger.getLogger(DavGateway.class);
+ private static final String HTTP_DAVMAIL_SOURCEFORGE_NET_VERSION_TXT = "http://davmail.sourceforge.net/version.txt";
+
+ private static boolean stopped;
+
+ private DavGateway() {
+ }
+
+ private static final ArrayList<AbstractServer> SERVER_LIST = new ArrayList<AbstractServer>();
+
+ /**
+ * Start the gateway, listen on specified smtp and pop3 ports
+ *
+ * @param args command line parameter config file path
+ */
+ public static void main(String[] args) {
+
+ if (args.length >= 1) {
+ Settings.setConfigFilePath(args[0]);
+ }
+
+ Settings.load();
+ DavGatewayTray.init();
+
+ start();
+
+ // server mode: all threads are daemon threads, do not let main stop
+ if (Settings.getBooleanProperty("davmail.server")) {
+ Runtime.getRuntime().addShutdownHook(new Thread("Shutdown") {
+ @Override
+ public void run() {
+ DavGatewayTray.debug(new BundleMessage("LOG_GATEWAY_INTERRUPTED"));
+ DavGateway.stop();
+ stopped = true;
+ }
+ });
+
+ try {
+ while (!stopped) {
+ Thread.sleep(1000);
+ }
+ } catch (InterruptedException e) {
+ DavGatewayTray.debug(new BundleMessage("LOG_GATEWAY_INTERRUPTED"));
+ stop();
+ DavGatewayTray.debug(new BundleMessage("LOG_GATEWAY_STOP"));
+ }
+
+ }
+ }
+
+ /**
+ * Start DavMail listeners.
+ */
+ public static void start() {
+ // register custom SSL Socket factory
+ DavGatewaySSLProtocolSocketFactory.register();
+
+ // prepare HTTP connection pool
+ DavGatewayHttpClientFacade.start();
+
+ SERVER_LIST.clear();
+
+ int smtpPort = Settings.getIntProperty("davmail.smtpPort");
+ if (smtpPort != 0) {
+ SERVER_LIST.add(new SmtpServer(smtpPort));
+ }
+ int popPort = Settings.getIntProperty("davmail.popPort");
+ if (popPort != 0) {
+ SERVER_LIST.add(new PopServer(popPort));
+ }
+ int imapPort = Settings.getIntProperty("davmail.imapPort");
+ if (imapPort != 0) {
+ SERVER_LIST.add(new ImapServer(imapPort));
+ }
+ int caldavPort = Settings.getIntProperty("davmail.caldavPort");
+ if (caldavPort != 0) {
+ SERVER_LIST.add(new CaldavServer(caldavPort));
+ }
+ int ldapPort = Settings.getIntProperty("davmail.ldapPort");
+ if (ldapPort != 0) {
+ SERVER_LIST.add(new LdapServer(ldapPort));
+ }
+
+ BundleMessage.BundleMessageList messages = new BundleMessage.BundleMessageList();
+ BundleMessage.BundleMessageList errorMessages = new BundleMessage.BundleMessageList();
+ for (AbstractServer server : SERVER_LIST) {
+ try {
+ server.bind();
+ server.start();
+ messages.add(new BundleMessage("LOG_PROTOCOL_PORT", server.getProtocolName(), server.getPort()));
+ } catch (DavMailException e) {
+ errorMessages.add(e.getBundleMessage());
+ } catch (IOException e) {
+ errorMessages.add(new BundleMessage("LOG_SOCKET_BIND_FAILED", server.getProtocolName(), server.getPort()));
+ }
+ }
+
+ final String currentVersion = getCurrentVersion();
+ boolean showStartupBanner = Settings.getBooleanProperty("davmail.showStartupBanner", true);
+ if (showStartupBanner) {
+ DavGatewayTray.info(new BundleMessage("LOG_DAVMAIL_GATEWAY_LISTENING",
+ currentVersion == null ? "" : currentVersion, messages));
+ }
+ if (!errorMessages.isEmpty()) {
+ DavGatewayTray.error(new BundleMessage("LOG_MESSAGE", errorMessages));
+ }
+
+ // check for new version in a separate thread
+ new Thread("CheckRelease") {
+ @Override
+ public void run() {
+ String releasedVersion = getReleasedVersion();
+ if (currentVersion != null && currentVersion.length() > 0 && releasedVersion != null && currentVersion.compareTo(releasedVersion) < 0) {
+ DavGatewayTray.info(new BundleMessage("LOG_NEW_VERSION_AVAILABLE", releasedVersion));
+ }
+
+ }
+ }.start();
+
+ }
+
+ /**
+ * Stop all listeners, shutdown connection pool and clear session cache.
+ */
+ public static void stop() {
+ DavGateway.stopServers();
+ // close pooled connections
+ DavGatewayHttpClientFacade.stop();
+ // clear session cache
+ ExchangeSessionFactory.reset();
+ DavGatewayTray.info(new BundleMessage("LOG_GATEWAY_STOP"));
+ }
+
+ /**
+ * Stop all listeners and clear session cache.
+ */
+ public static void restart() {
+ DavGateway.stopServers();
+ // clear session cache
+ ExchangeSessionFactory.reset();
+ DavGateway.start();
+ }
+
+ private static void stopServers() {
+ for (AbstractServer server : SERVER_LIST) {
+ server.close();
+ try {
+ server.join();
+ } catch (InterruptedException e) {
+ DavGatewayTray.warn(new BundleMessage("LOG_EXCEPTION_WAITING_SERVER_THREAD_DIE"), e);
+ }
+ }
+ }
+
+ /**
+ * Get current DavMail version.
+ *
+ * @return current version
+ */
+ public static String getCurrentVersion() {
+ Package davmailPackage = DavGateway.class.getPackage();
+ String currentVersion = davmailPackage.getImplementationVersion();
+ if (currentVersion == null) {
+ currentVersion = "";
+ }
+ return currentVersion;
+ }
+
+ /**
+ * Get latest released version from SourceForge.
+ *
+ * @return latest version
+ */
+ public static String getReleasedVersion() {
+ String version = null;
+ if (!Settings.getBooleanProperty("davmail.disableUpdateCheck")) {
+ BufferedReader versionReader = null;
+ GetMethod getMethod = new GetMethod(HTTP_DAVMAIL_SOURCEFORGE_NET_VERSION_TXT);
+ try {
+ HttpClient httpClient = DavGatewayHttpClientFacade.getInstance(HTTP_DAVMAIL_SOURCEFORGE_NET_VERSION_TXT);
+ int status = httpClient.executeMethod(getMethod);
+ if (status == HttpStatus.SC_OK) {
+ versionReader = new BufferedReader(new InputStreamReader(getMethod.getResponseBodyAsStream()));
+ version = versionReader.readLine();
+ LOGGER.debug("DavMail released version: " + version);
+ }
+ } catch (IOException e) {
+ DavGatewayTray.debug(new BundleMessage("LOG_UNABLE_TO_GET_RELEASED_VERSION"));
+ } finally {
+ if (versionReader != null) {
+ try {
+ versionReader.close();
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+ getMethod.releaseConnection();
+ }
+ }
+ return version;
+ }
+}
diff --git a/src/java/davmail/Settings.java b/src/java/davmail/Settings.java
new file mode 100644
index 0000000..7794709
--- /dev/null
+++ b/src/java/davmail/Settings.java
@@ -0,0 +1,493 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail;
+
+import davmail.ui.tray.DavGatewayTray;
+import org.apache.log4j.*;
+
+import java.io.*;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.Properties;
+import java.util.TreeSet;
+
+/**
+ * Settings facade.
+ * DavMail settings are stored in the .davmail.properties file in current
+ * user home directory or in the file specified on the command line.
+ */
+public final class Settings {
+ private Settings() {
+ }
+
+ private static final Properties SETTINGS = new Properties() {
+ @Override
+ public synchronized Enumeration<Object> keys() {
+ Enumeration keysEnumeration = super.keys();
+ TreeSet<String> sortedKeySet = new TreeSet<String>();
+ while (keysEnumeration.hasMoreElements()) {
+ sortedKeySet.add((String) keysEnumeration.nextElement());
+ }
+ final Iterator<String> sortedKeysIterator = sortedKeySet.iterator();
+ return new Enumeration<Object>() {
+
+ public boolean hasMoreElements() {
+ return sortedKeysIterator.hasNext();
+ }
+
+ public Object nextElement() {
+ return sortedKeysIterator.next();
+ }
+ };
+ }
+
+ };
+ private static String configFilePath;
+ private static boolean isFirstStart;
+
+ /**
+ * Set config file path (from command line parameter).
+ *
+ * @param path davmail properties file path
+ */
+ public static synchronized void setConfigFilePath(String path) {
+ configFilePath = path;
+ }
+
+ /**
+ * Detect first launch (properties file does not exist).
+ *
+ * @return true if this is the first start with the current file path
+ */
+ public static synchronized boolean isFirstStart() {
+ return isFirstStart;
+ }
+
+ /**
+ * Load properties from provided stream (used in webapp mode).
+ *
+ * @param inputStream properties stream
+ * @throws IOException on error
+ */
+ public static synchronized void load(InputStream inputStream) throws IOException {
+ SETTINGS.load(inputStream);
+ updateLoggingConfig();
+ }
+
+ /**
+ * Load properties from current file path (command line or default).
+ */
+ public static synchronized void load() {
+ FileInputStream fileInputStream = null;
+ try {
+ if (configFilePath == null) {
+ //noinspection AccessOfSystemProperties
+ configFilePath = System.getProperty("user.home") + "/.davmail.properties";
+ }
+ File configFile = new File(configFilePath);
+ if (configFile.exists()) {
+ fileInputStream = new FileInputStream(configFile);
+ load(fileInputStream);
+ } else {
+ isFirstStart = true;
+
+ // first start : set default values, ports above 1024 for unix/linux
+ setDefaultSettings();
+ save();
+ }
+ } catch (IOException e) {
+ DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_LOAD_SETTINGS"), e);
+ } finally {
+ if (fileInputStream != null) {
+ try {
+ fileInputStream.close();
+ } catch (IOException e) {
+ DavGatewayTray.debug(new BundleMessage("LOG_ERROR_CLOSING_CONFIG_FILE"), e);
+ }
+ }
+ }
+ updateLoggingConfig();
+ }
+
+ /**
+ * Set all settings to default values.
+ * Ports above 1024 for unix/linux
+ */
+ public static void setDefaultSettings() {
+ SETTINGS.put("davmail.url", "https://exchangeServer/exchange/");
+ SETTINGS.put("davmail.popPort", "1110");
+ SETTINGS.put("davmail.imapPort", "1143");
+ SETTINGS.put("davmail.smtpPort", "1025");
+ SETTINGS.put("davmail.caldavPort", "1080");
+ SETTINGS.put("davmail.ldapPort", "1389");
+ SETTINGS.put("davmail.clientSoTimeout", "");
+ SETTINGS.put("davmail.keepDelay", "30");
+ SETTINGS.put("davmail.sentKeepDelay", "90");
+ SETTINGS.put("davmail.caldavPastDelay", "90");
+ SETTINGS.put("davmail.imapIdleDelay", "");
+ SETTINGS.put("davmail.allowRemote", Boolean.FALSE.toString());
+ SETTINGS.put("davmail.bindAddress", "");
+ SETTINGS.put("davmail.useSystemProxies", Boolean.FALSE.toString());
+ SETTINGS.put("davmail.enableProxy", Boolean.FALSE.toString());
+ SETTINGS.put("davmail.enableEws", "auto");
+ SETTINGS.put("davmail.proxyHost", "");
+ SETTINGS.put("davmail.proxyPort", "");
+ SETTINGS.put("davmail.proxyUser", "");
+ SETTINGS.put("davmail.proxyPassword", "");
+ SETTINGS.put("davmail.server", Boolean.FALSE.toString());
+ SETTINGS.put("davmail.server.certificate.hash", "");
+ SETTINGS.put("davmail.caldavAlarmSound", "");
+ SETTINGS.put("davmail.forceActiveSyncUpdate", Boolean.FALSE.toString());
+ SETTINGS.put("davmail.showStartupBanner", Boolean.TRUE.toString());
+ SETTINGS.put("davmail.disableGuiNotifications", Boolean.FALSE.toString());
+ SETTINGS.put("davmail.imapAutoExpunge", Boolean.TRUE.toString());
+ SETTINGS.put("davmail.popMarkReadOnRetr", Boolean.FALSE.toString());
+ SETTINGS.put("davmail.smtpSaveInSent", Boolean.TRUE.toString());
+ SETTINGS.put("davmail.ssl.keystoreType", "");
+ SETTINGS.put("davmail.ssl.keystoreFile", "");
+ SETTINGS.put("davmail.ssl.keystorePass", "");
+ SETTINGS.put("davmail.ssl.keyPass", "");
+ SETTINGS.put("davmail.ssl.clientKeystoreType", "");
+ SETTINGS.put("davmail.ssl.clientKeystoreFile", "");
+ SETTINGS.put("davmail.ssl.clientKeystorePass", "");
+ SETTINGS.put("davmail.ssl.pkcs11Library", "");
+ SETTINGS.put("davmail.ssl.pkcs11Config", "");
+ SETTINGS.put("davmail.ssl.nosecurepop", Boolean.FALSE.toString());
+ SETTINGS.put("davmail.ssl.nosecureimap", Boolean.FALSE.toString());
+ SETTINGS.put("davmail.ssl.nosecuresmtp", Boolean.FALSE.toString());
+ SETTINGS.put("davmail.ssl.nosecurecaldav", Boolean.FALSE.toString());
+ SETTINGS.put("davmail.ssl.nosecureldap", Boolean.FALSE.toString());
+
+ // logging
+ SETTINGS.put("log4j.rootLogger", Level.WARN.toString());
+ SETTINGS.put("log4j.logger.davmail", Level.DEBUG.toString());
+ SETTINGS.put("log4j.logger.httpclient.wire", Level.WARN.toString());
+ SETTINGS.put("log4j.logger.org.apache.commons.httpclient", Level.WARN.toString());
+ SETTINGS.put("davmail.logFilePath", "");
+ }
+
+ /**
+ * Return DavMail log file path
+ *
+ * @return full log file path
+ */
+ public static String getLogFilePath() {
+ String logFilePath = Settings.getProperty("davmail.logFilePath");
+ // set default log file path
+ if ((logFilePath == null || logFilePath.length() == 0)) {
+ if (System.getProperty("os.name").toLowerCase().startsWith("mac os x")) {
+ // store davmail.log in OSX Logs directory
+ logFilePath = System.getProperty("user.home") + "/Library/Logs/DavMail/davmail.log";
+ } else {
+ // store davmail.log in user home folder
+ logFilePath = System.getProperty("user.home") + "/davmail.log";
+ }
+ } else {
+ File logFile = new File(logFilePath);
+ if (logFile.isDirectory()) {
+ logFilePath += "/davmail.log";
+ }
+ }
+ return logFilePath;
+ }
+
+ /**
+ * Return DavMail log file directory
+ *
+ * @return full log file directory
+ */
+ public static String getLogFileDirectory() {
+ String logFilePath = getLogFilePath();
+ if (logFilePath == null || logFilePath.length() == 0) {
+ return ".";
+ }
+ int lastSlashIndex = logFilePath.lastIndexOf('/');
+ if (lastSlashIndex == -1) {
+ lastSlashIndex = logFilePath.lastIndexOf('\\');
+ }
+ if (lastSlashIndex >= 0) {
+ return logFilePath.substring(0, lastSlashIndex);
+ } else {
+ return ".";
+ }
+ }
+
+ /**
+ * Update Log4J config from settings.
+ */
+ private static void updateLoggingConfig() {
+ String logFilePath = getLogFilePath();
+
+ Logger rootLogger = Logger.getRootLogger();
+ try {
+ if (logFilePath != null && logFilePath.length() > 0) {
+ File logFile = new File(logFilePath);
+ // create parent directory if needed
+ File logFileDir = logFile.getParentFile();
+ if (logFileDir != null && !logFileDir.exists()) {
+ if (!logFileDir.mkdirs()) {
+ DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_CREATE_LOG_FILE_DIR"));
+ throw new IOException();
+ }
+ }
+ } else {
+ logFilePath = "davmail.log";
+ }
+ // Build file appender
+ RollingFileAppender fileAppender = ((RollingFileAppender) rootLogger.getAppender("FileAppender"));
+ if (fileAppender == null) {
+ String logFileSize = Settings.getProperty("davmail.logFileSize");
+ if (logFileSize == null || logFileSize.length() == 0) {
+ logFileSize = "1MB";
+ }
+ fileAppender = new RollingFileAppender();
+ fileAppender.setName("FileAppender");
+ fileAppender.setMaxBackupIndex(2);
+ fileAppender.setMaxFileSize(logFileSize);
+ fileAppender.setEncoding("UTF-8");
+ fileAppender.setLayout(new PatternLayout("%d{ISO8601} %-5p [%t] %c %x - %m%n"));
+ }
+ fileAppender.setFile(logFilePath, true, false, 8192);
+ rootLogger.addAppender(fileAppender);
+
+ // disable ConsoleAppender in gui mode
+ if (!Settings.getBooleanProperty("davmail.server")) {
+ ConsoleAppender consoleAppender = (ConsoleAppender) rootLogger.getAppender("ConsoleAppender");
+ if (consoleAppender != null) {
+ consoleAppender.setThreshold(Level.OFF);
+ }
+ }
+
+ } catch (IOException e) {
+ DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_SET_LOG_FILE_PATH"));
+ }
+
+ // update logging levels
+ Settings.setLoggingLevel("rootLogger", Settings.getLoggingLevel("rootLogger"));
+ Settings.setLoggingLevel("davmail", Settings.getLoggingLevel("davmail"));
+ Settings.setLoggingLevel("httpclient.wire", Settings.getLoggingLevel("httpclient.wire"));
+ Settings.setLoggingLevel("org.apache.commons.httpclient", Settings.getLoggingLevel("org.apache.commons.httpclient"));
+ }
+
+ /**
+ * Save settings in current file path (command line or default).
+ */
+ public static synchronized void save() {
+ // configFilePath is null in some test cases
+ if (configFilePath != null) {
+ FileOutputStream fileOutputStream = null;
+ try {
+ fileOutputStream = new FileOutputStream(configFilePath);
+ SETTINGS.store(fileOutputStream, "DavMail settings");
+ } catch (IOException e) {
+ DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_STORE_SETTINGS"), e);
+ } finally {
+ if (fileOutputStream != null) {
+ try {
+ fileOutputStream.close();
+ } catch (IOException e) {
+ DavGatewayTray.debug(new BundleMessage("LOG_ERROR_CLOSING_CONFIG_FILE"), e);
+ }
+ }
+ }
+ }
+ updateLoggingConfig();
+ }
+
+ /**
+ * Get a property value as String.
+ *
+ * @param property property name
+ * @return property value
+ */
+ public static synchronized String getProperty(String property) {
+ String value = SETTINGS.getProperty(property);
+ // return null on empty value
+ if (value != null && value.length() == 0) {
+ value = null;
+ }
+ return value;
+ }
+
+ /**
+ * Get property value or default.
+ *
+ * @param property property name
+ * @param defaultValue default property value
+ * @return property value
+ */
+ public static synchronized String getProperty(String property, String defaultValue) {
+ String value = SETTINGS.getProperty(property);
+ if (value == null) {
+ value = defaultValue;
+ }
+ return value;
+ }
+
+
+ /**
+ * Get a property value as char[].
+ *
+ * @param property property name
+ * @return property value
+ */
+ public static synchronized char[] getCharArrayProperty(String property) {
+ String propertyValue = Settings.getProperty(property);
+ char[] value = null;
+ if (propertyValue != null) {
+ value = propertyValue.toCharArray();
+ }
+ return value;
+ }
+
+ /**
+ * Set a property value.
+ *
+ * @param property property name
+ * @param value property value
+ */
+ public static synchronized void setProperty(String property, String value) {
+ if (value != null) {
+ SETTINGS.setProperty(property, value);
+ } else {
+ SETTINGS.setProperty(property, "");
+ }
+ }
+
+ /**
+ * Get a property value as int.
+ *
+ * @param property property name
+ * @return property value
+ */
+ public static synchronized int getIntProperty(String property) {
+ return getIntProperty(property, 0);
+ }
+
+ /**
+ * Get a property value as int, return default value if null.
+ *
+ * @param property property name
+ * @param defaultValue default property value
+ * @return property value
+ */
+ public static synchronized int getIntProperty(String property, int defaultValue) {
+ int value = defaultValue;
+ try {
+ String propertyValue = SETTINGS.getProperty(property);
+ if (propertyValue != null && propertyValue.length() > 0) {
+ value = Integer.parseInt(propertyValue);
+ }
+ } catch (NumberFormatException e) {
+ DavGatewayTray.error(new BundleMessage("LOG_INVALID_SETTING_VALUE", property), e);
+ }
+ return value;
+ }
+
+ /**
+ * Get a property value as boolean.
+ *
+ * @param property property name
+ * @return property value
+ */
+ public static synchronized boolean getBooleanProperty(String property) {
+ String propertyValue = SETTINGS.getProperty(property);
+ return Boolean.parseBoolean(propertyValue);
+ }
+
+ /**
+ * Get a property value as boolean.
+ *
+ * @param property property name
+ * @param defaultValue default property value
+ * @return property value
+ */
+ public static synchronized boolean getBooleanProperty(String property, boolean defaultValue) {
+ boolean value = defaultValue;
+ String propertyValue = SETTINGS.getProperty(property);
+ if (propertyValue != null && propertyValue.length() > 0) {
+ value = Boolean.parseBoolean(propertyValue);
+ }
+ return value;
+ }
+
+ /**
+ * Build logging properties prefix.
+ *
+ * @param category logging category
+ * @return prefix
+ */
+ private static String getLoggingPrefix(String category) {
+ String prefix;
+ if ("rootLogger".equals(category)) {
+ prefix = "log4j.";
+ } else {
+ prefix = "log4j.logger.";
+ }
+ return prefix;
+ }
+
+ /**
+ * Return Log4J logging level for the category.
+ *
+ * @param category logging category
+ * @return logging level
+ */
+ public static synchronized Level getLoggingLevel(String category) {
+ String prefix = getLoggingPrefix(category);
+ String currentValue = SETTINGS.getProperty(prefix + category);
+
+ if (currentValue != null && currentValue.length() > 0) {
+ return Level.toLevel(currentValue);
+ } else if ("rootLogger".equals(category)) {
+ return Logger.getRootLogger().getLevel();
+ } else {
+ return Logger.getLogger(category).getLevel();
+ }
+ }
+
+ /**
+ * Set Log4J logging level for the category
+ *
+ * @param category logging category
+ * @param level logging level
+ */
+ public static synchronized void setLoggingLevel(String category, Level level) {
+ String prefix = getLoggingPrefix(category);
+ SETTINGS.setProperty(prefix + category, level.toString());
+ if ("rootLogger".equals(category)) {
+ Logger.getRootLogger().setLevel(level);
+ } else {
+ Logger.getLogger(category).setLevel(level);
+ }
+ }
+
+ /**
+ * Change and save a single property.
+ *
+ * @param property property name
+ * @param value property value
+ */
+ public static synchronized void saveProperty(String property, String value) {
+ Settings.load();
+ Settings.setProperty(property, value);
+ Settings.save();
+ }
+
+}
diff --git a/src/java/davmail/caldav/CaldavConnection.java b/src/java/davmail/caldav/CaldavConnection.java
new file mode 100644
index 0000000..ec2f1d9
--- /dev/null
+++ b/src/java/davmail/caldav/CaldavConnection.java
@@ -0,0 +1,1806 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.caldav;
+
+import davmail.AbstractConnection;
+import davmail.BundleMessage;
+import davmail.DavGateway;
+import davmail.Settings;
+import davmail.exception.DavMailAuthenticationException;
+import davmail.exception.DavMailException;
+import davmail.exception.HttpNotFoundException;
+import davmail.exception.HttpPreconditionFailedException;
+import davmail.exchange.ExchangeSession;
+import davmail.exchange.ExchangeSessionFactory;
+import davmail.exchange.ICSBufferedReader;
+import davmail.exchange.XMLStreamUtil;
+import davmail.ui.tray.DavGatewayTray;
+import davmail.util.StringUtil;
+import org.apache.commons.httpclient.HttpException;
+import org.apache.commons.httpclient.HttpStatus;
+import org.apache.commons.httpclient.URI;
+import org.apache.commons.httpclient.URIException;
+import org.apache.commons.httpclient.util.URIUtil;
+import org.apache.log4j.Logger;
+
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import java.io.*;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.net.URL;
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+/**
+ * Handle a caldav connection.
+ */
+public class CaldavConnection extends AbstractConnection {
+ /**
+ * Maximum keep alive time in seconds
+ */
+ protected static final int MAX_KEEP_ALIVE_TIME = 300;
+ protected final Logger wireLogger = Logger.getLogger(this.getClass());
+
+ protected boolean closed;
+
+ /**
+ * custom url encode path set for iCal 5
+ */
+ public static final BitSet ical_allowed_abs_path = new BitSet(256);
+ static {
+ ical_allowed_abs_path.or(URI.allowed_abs_path);
+ ical_allowed_abs_path.clear('@');
+ }
+
+ static String encodePath(CaldavRequest request, String path) throws URIException {
+ if (request.isIcal5()) {
+ return URIUtil.encode(path, ical_allowed_abs_path, "UTF-8");
+ } else {
+ return URIUtil.encodePath(path, "UTF-8");
+ }
+ }
+
+ /**
+ * Initialize the streams and start the thread.
+ *
+ * @param clientSocket Caldav client socket
+ */
+ public CaldavConnection(Socket clientSocket) {
+ super(CaldavConnection.class.getSimpleName(), clientSocket, "UTF-8");
+ // set caldav logging to davmail logging level
+ wireLogger.setLevel(Settings.getLoggingLevel("davmail"));
+ }
+
+ protected Map<String, String> parseHeaders() throws IOException {
+ HashMap<String, String> headers = new HashMap<String, String>();
+ String line;
+ while ((line = readClient()) != null && line.length() > 0) {
+ int index = line.indexOf(':');
+ if (index <= 0) {
+ wireLogger.warn("Invalid header: "+line);
+ throw new DavMailException("EXCEPTION_INVALID_HEADER");
+ }
+ headers.put(line.substring(0, index).toLowerCase(), line.substring(index + 1).trim());
+ }
+ return headers;
+ }
+
+ protected String getContent(String contentLength) throws IOException {
+ if (contentLength == null || contentLength.length() == 0) {
+ return null;
+ } else {
+ int size;
+ try {
+ size = Integer.parseInt(contentLength);
+ } catch (NumberFormatException e) {
+ throw new DavMailException("EXCEPTION_INVALID_CONTENT_LENGTH", contentLength);
+ }
+ String content = in.readContentAsString(size);
+ if (wireLogger.isDebugEnabled()) {
+ wireLogger.debug("< " + content);
+ }
+ return content;
+ }
+ }
+
+ protected void setSocketTimeout(String keepAliveValue) throws IOException {
+ if (keepAliveValue != null && keepAliveValue.length() > 0) {
+ int keepAlive;
+ try {
+ keepAlive = Integer.parseInt(keepAliveValue);
+ } catch (NumberFormatException e) {
+ throw new DavMailException("EXCEPTION_INVALID_KEEPALIVE", keepAliveValue);
+ }
+ if (keepAlive > MAX_KEEP_ALIVE_TIME) {
+ keepAlive = MAX_KEEP_ALIVE_TIME;
+ }
+ client.setSoTimeout(keepAlive * 1000);
+ DavGatewayTray.debug(new BundleMessage("LOG_SET_SOCKET_TIMEOUT", keepAlive));
+ }
+ }
+
+ @Override
+ public void run() {
+ String line;
+ StringTokenizer tokens;
+
+ try {
+ while (!closed) {
+ line = readClient();
+ // unable to read line, connection closed ?
+ if (line == null) {
+ break;
+ }
+ tokens = new StringTokenizer(line);
+ String command = tokens.nextToken();
+ Map<String, String> headers = parseHeaders();
+ String encodedPath = StringUtil.encodePlusSign(tokens.nextToken());
+ String path = URIUtil.decode(encodedPath);
+ String content = getContent(headers.get("content-length"));
+ setSocketTimeout(headers.get("keep-alive"));
+ // client requested connection close
+ closed = "close".equals(headers.get("connection"));
+ if ("OPTIONS".equals(command)) {
+ sendOptions();
+ } else if (!headers.containsKey("authorization")) {
+ sendUnauthorized();
+ } else {
+ decodeCredentials(headers.get("authorization"));
+ // need to check session on each request, credentials may have changed or session expired
+ try {
+ session = ExchangeSessionFactory.getInstance(userName, password);
+ handleRequest(command, path, headers, content);
+ } catch (DavMailAuthenticationException e) {
+ sendUnauthorized();
+ }
+ }
+
+ os.flush();
+ DavGatewayTray.resetIcon();
+ }
+ } catch (SocketTimeoutException e) {
+ DavGatewayTray.debug(new BundleMessage("LOG_CLOSE_CONNECTION_ON_TIMEOUT"));
+ } catch (SocketException e) {
+ DavGatewayTray.debug(new BundleMessage("LOG_CONNECTION_CLOSED"));
+ } catch (Exception e) {
+ if (!(e instanceof HttpNotFoundException)) {
+ DavGatewayTray.log(e);
+ }
+ try {
+ sendErr(e);
+ } catch (IOException e2) {
+ DavGatewayTray.debug(new BundleMessage("LOG_EXCEPTION_SENDING_ERROR_TO_CLIENT"), e2);
+ }
+ } finally {
+ close();
+ }
+ DavGatewayTray.resetIcon();
+ }
+
+ /**
+ * Handle caldav request.
+ *
+ * @param command Http command
+ * @param path request path
+ * @param headers Http headers map
+ * @param body request body
+ * @throws IOException on error
+ */
+ public void handleRequest(String command, String path, Map<String, String> headers, String body) throws IOException {
+ CaldavRequest request = new CaldavRequest(command, path, headers, body);
+ if (request.isOptions()) {
+ sendOptions();
+ } else if (request.isPropFind() && request.isRoot()) {
+ sendRoot(request);
+ } else if (request.isGet() && request.isRoot()) {
+ sendGetRoot();
+ } else if (request.isPath(1, "principals")) {
+ handlePrincipals(request);
+ } else if (request.isPath(1, "users")) {
+ if (request.isPropFind() && request.isPathLength(3)) {
+ sendUserRoot(request);
+ } else {
+ handleFolderOrItem(request);
+ }
+ } else if (request.isPath(1, "public")) {
+ handleFolderOrItem(request);
+ } else if (request.isPath(1, "directory")) {
+ sendDirectory(request);
+ } else if (request.isPath(1, ".well-known")) {
+ sendWellKnown();
+ } else {
+ sendUnsupported(request);
+ }
+ }
+
+ protected void handlePrincipals(CaldavRequest request) throws IOException {
+ if (request.isPath(2, "users")) {
+ if (request.isPropFind() && request.isPathLength(4)) {
+ sendPrincipal(request, "users", URIUtil.decode(request.getPathElement(3)));
+ // send back principal on search
+ } else if (request.isReport() && request.isPathLength(3)) {
+ sendPrincipal(request, "users", session.getEmail());
+ // iCal current-user-principal request
+ } else if (request.isPropFind() && request.isPathLength(3)) {
+ sendPrincipalsFolder(request);
+ } else {
+ sendUnsupported(request);
+ }
+ } else if (request.isPath(2, "public")) {
+ StringBuilder prefixBuffer = new StringBuilder("public");
+ for (int i = 3; i < request.getPathLength() - 1; i++) {
+ prefixBuffer.append('/').append(request.getPathElement(i));
+ }
+ sendPrincipal(request, URIUtil.decode(prefixBuffer.toString()), URIUtil.decode(request.getLastPath()));
+ } else {
+ sendUnsupported(request);
+ }
+ }
+
+ protected void handleFolderOrItem(CaldavRequest request) throws IOException {
+ String lastPath = StringUtil.xmlDecode(request.getLastPath());
+ // folder requests
+ if (request.isPropFind() && "inbox".equals(lastPath)) {
+ sendInbox(request);
+ } else if (request.isPropFind() && "outbox".equals(lastPath)) {
+ sendOutbox(request);
+ } else if (request.isPost() && "outbox".equals(lastPath)) {
+ if (request.isFreeBusy()) {
+ sendFreeBusy(request.getBody());
+ } else {
+ int status = session.sendEvent(request.getBody());
+ // TODO: implement Itip response body
+ sendHttpResponse(status);
+ }
+ } else if (request.isPropFind()) {
+ sendFolderOrItem(request);
+ } else if (request.isPropPatch()) {
+ patchCalendar(request);
+ } else if (request.isReport()) {
+ reportItems(request);
+ // event requests
+ } else if (request.isPut()) {
+ String etag = request.getHeader("if-match");
+ String noneMatch = request.getHeader("if-none-match");
+ ExchangeSession.ItemResult itemResult = session.createOrUpdateItem(request.getFolderPath(), lastPath, request.getBody(), etag, noneMatch);
+ sendHttpResponse(itemResult.status, buildEtagHeader(itemResult.etag), null, "", true);
+
+ } else if (request.isDelete()) {
+ if (request.getFolderPath().endsWith("inbox")) {
+ session.processItem(request.getFolderPath(), lastPath);
+ } else {
+ session.deleteItem(request.getFolderPath(), lastPath);
+ }
+ sendHttpResponse(HttpStatus.SC_OK);
+ } else if (request.isGet()) {
+ if (request.path.endsWith("/")) {
+ // GET request on a folder => build ics content of all folder events
+ String folderPath = request.getFolderPath();
+ ExchangeSession.Folder folder = session.getFolder(folderPath);
+ if (folder.isContact()) {
+ List<ExchangeSession.Contact> contacts = session.getAllContacts(folderPath);
+ ChunkedResponse response = new ChunkedResponse(HttpStatus.SC_OK, "text/vcard;charset=UTF-8");
+
+ for (ExchangeSession.Contact contact : contacts) {
+ String contactBody = contact.getBody();
+ if (contactBody != null) {
+ response.append(contactBody);
+ response.append("\n");
+ }
+ }
+ response.close();
+
+ } else if (folder.isCalendar() || folder.isTask()) {
+ List<ExchangeSession.Event> events = session.getAllEvents(folderPath);
+ ChunkedResponse response = new ChunkedResponse(HttpStatus.SC_OK, "text/calendar;charset=UTF-8");
+ response.append("BEGIN:VCALENDAR\r\n");
+ response.append("VERSION:2.0\r\n");
+ response.append("PRODID:-//davmail.sf.net/NONSGML DavMail Calendar V1.1//EN\r\n");
+ response.append("METHOD:PUBLISH\r\n");
+
+ for (ExchangeSession.Event event : events) {
+ String icsContent = StringUtil.getToken(event.getBody(), "BEGIN:VTIMEZONE", "END:VCALENDAR");
+ if (icsContent != null) {
+ response.append("BEGIN:VTIMEZONE");
+ response.append(icsContent);
+ } else {
+ icsContent = StringUtil.getToken(event.getBody(), "BEGIN:VEVENT", "END:VCALENDAR");
+ if (icsContent != null) {
+ response.append("BEGIN:VEVENT");
+ response.append(icsContent);
+ }
+ }
+ }
+ response.append("END:VCALENDAR");
+ response.close();
+ } else {
+ sendHttpResponse(HttpStatus.SC_OK, buildEtagHeader(folder.etag), "text/html", (byte[]) null, true);
+ }
+ } else {
+ ExchangeSession.Item item = session.getItem(request.getFolderPath(), lastPath);
+ sendHttpResponse(HttpStatus.SC_OK, buildEtagHeader(item.getEtag()), item.getContentType(), item.getBody(), true);
+ }
+ } else if (request.isHead()) {
+ // test event
+ ExchangeSession.Item item = session.getItem(request.getFolderPath(), lastPath);
+ sendHttpResponse(HttpStatus.SC_OK, buildEtagHeader(item.getEtag()), item.getContentType(), (byte[]) null, true);
+ } else if (request.isMkCalendar()) {
+ HashMap<String, String> properties = new HashMap<String, String>();
+ //properties.put("displayname", request.getProperty("displayname"));
+ int status = session.createCalendarFolder(request.getFolderPath(), properties);
+ sendHttpResponse(status, null);
+ } else if (request.isMove()) {
+ String destinationUrl = request.getHeader("destination");
+ session.moveItem(request.path, URIUtil.decode(new URL(destinationUrl).getPath()));
+ sendHttpResponse(HttpStatus.SC_CREATED, null);
+ } else {
+ sendUnsupported(request);
+ }
+
+ }
+
+ protected HashMap<String, String> buildEtagHeader(String etag) {
+ if (etag != null) {
+ HashMap<String, String> etagHeader = new HashMap<String, String>();
+ etagHeader.put("ETag", etag);
+ return etagHeader;
+ } else {
+ return null;
+ }
+ }
+
+ private void appendContactsResponses(CaldavResponse response, CaldavRequest request, List<ExchangeSession.Contact> contacts) throws IOException {
+ int size = contacts.size();
+ int count = 0;
+ for (ExchangeSession.Contact contact : contacts) {
+ DavGatewayTray.debug(new BundleMessage("LOG_LISTING_ITEM", ++count, size));
+ DavGatewayTray.switchIcon();
+ appendItemResponse(response, request, contact);
+ }
+ }
+
+ protected void appendEventsResponses(CaldavResponse response, CaldavRequest request, List<ExchangeSession.Event> events) throws IOException {
+ int size = events.size();
+ int count = 0;
+ for (ExchangeSession.Event event : events) {
+ DavGatewayTray.debug(new BundleMessage("LOG_LISTING_ITEM", ++count, size));
+ DavGatewayTray.switchIcon();
+ appendItemResponse(response, request, event);
+ }
+ }
+
+ protected void appendItemResponse(CaldavResponse response, CaldavRequest request, ExchangeSession.Item item) throws IOException {
+ StringBuilder eventPath = new StringBuilder();
+ eventPath.append(encodePath(request, request.getPath()));
+ if (!(eventPath.charAt(eventPath.length() - 1) == '/')) {
+ eventPath.append('/');
+ }
+ String itemName = StringUtil.xmlEncode(item.getName());
+ eventPath.append(URIUtil.encodeWithinQuery(itemName));
+ response.startResponse(eventPath.toString());
+ response.startPropstat();
+ if (request.hasProperty("calendar-data") && item instanceof ExchangeSession.Event) {
+ response.appendCalendarData(item.getBody());
+ }
+ if (request.hasProperty("address-data") && item instanceof ExchangeSession.Contact) {
+ response.appendContactData(item.getBody());
+ }
+ if (request.hasProperty("getcontenttype")) {
+ if (item instanceof ExchangeSession.Event) {
+ response.appendProperty("D:getcontenttype", "text/calendar; component=vevent");
+ } else if (item instanceof ExchangeSession.Contact) {
+ response.appendProperty("D:getcontenttype", "text/vcard");
+ }
+ }
+ if (request.hasProperty("getetag")) {
+ response.appendProperty("D:getetag", item.getEtag());
+ }
+ if (request.hasProperty("resourcetype")) {
+ response.appendProperty("D:resourcetype");
+ }
+ if (request.hasProperty("displayname")) {
+ response.appendProperty("D:displayname", itemName);
+ }
+ response.endPropStatOK();
+ response.endResponse();
+ }
+
+ /**
+ * Append folder object to Caldav response.
+ *
+ * @param response Caldav response
+ * @param request Caldav request
+ * @param folder folder object
+ * @param subFolder calendar folder path relative to request path
+ * @throws IOException on error
+ */
+ public void appendFolderOrItem(CaldavResponse response, CaldavRequest request, ExchangeSession.Folder folder, String subFolder) throws IOException {
+ response.startResponse(encodePath(request, request.getPath(subFolder)));
+ response.startPropstat();
+
+ if (request.hasProperty("resourcetype")) {
+ if (folder.isContact()) {
+ response.appendProperty("D:resourcetype", "<D:collection/>" +
+ "<E:addressbook/>");
+ } else if (folder.isCalendar() || folder.isTask()) {
+ response.appendProperty("D:resourcetype", "<D:collection/>" + "<C:calendar/>");
+ } else {
+ response.appendProperty("D:resourcetype", "<D:collection/>");
+ }
+
+ }
+ if (request.hasProperty("owner")) {
+ if ("users".equals(request.getPathElement(1))) {
+ response.appendHrefProperty("D:owner", "/principals/users/" + request.getPathElement(2));
+ } else {
+ response.appendHrefProperty("D:owner", "/principals" + request.getPath());
+ }
+ }
+ if (request.hasProperty("getcontenttype")) {
+ if (folder.isContact()) {
+ response.appendProperty("D:getcontenttype", "text/x-vcard");
+ } else if (folder.isCalendar()) {
+ response.appendProperty("D:getcontenttype", "text/calendar; component=vevent");
+ } else if (folder.isTask()) {
+ response.appendProperty("D:getcontenttype", "text/calendar; component=vtodo");
+ }
+ }
+ if (request.hasProperty("getetag")) {
+ response.appendProperty("D:getetag", folder.etag);
+ }
+ if (request.hasProperty("getctag")) {
+ response.appendProperty("CS:getctag", "CS=\"http://calendarserver.org/ns/\"",
+ base64Encode(folder.ctag));
+ }
+ if (request.hasProperty("displayname")) {
+ if (subFolder == null || subFolder.length() == 0) {
+ // use i18n calendar name as display name
+ String displayname = request.getLastPath();
+ if ("calendar".equals(displayname)) {
+ displayname = folder.displayName;
+ }
+ response.appendProperty("D:displayname", displayname);
+ } else {
+ response.appendProperty("D:displayname", subFolder);
+ }
+ }
+ if (request.hasProperty("calendar-description")) {
+ response.appendProperty("C:calendar-description", "");
+ }
+ if (request.hasProperty("supported-calendar-component-set")) {
+ if (folder.isCalendar()) {
+ response.appendProperty("C:supported-calendar-component-set", "<C:comp name=\"VEVENT\"/><C:comp name=\"VTODO\"/>");
+ } else if (folder.isTask()) {
+ response.appendProperty("C:supported-calendar-component-set", "<C:comp name=\"VTODO\"/>");
+ }
+ }
+
+ if (request.hasProperty("current-user-privilege-set")) {
+ response.appendProperty("D:current-user-privilege-set", "<D:privilege><D:read/><D:write/></D:privilege>");
+ }
+
+ response.endPropStatOK();
+ response.endResponse();
+ }
+
+ /**
+ * Append calendar inbox object to Caldav response.
+ *
+ * @param response Caldav response
+ * @param request Caldav request
+ * @param subFolder inbox folder path relative to request path
+ * @throws IOException on error
+ */
+ public void appendInbox(CaldavResponse response, CaldavRequest request, String subFolder) throws IOException {
+ String ctag = "0";
+ String etag = "0";
+ String folderPath = request.getFolderPath(subFolder);
+ // do not try to access inbox on shared calendar
+ if (!session.isSharedFolder(folderPath)) {
+ try {
+ ExchangeSession.Folder folder = session.getFolder(folderPath);
+ ctag = base64Encode(folder.ctag);
+ etag = base64Encode(folder.etag);
+ } catch (HttpException e) {
+ // unauthorized access, probably an inbox on shared calendar
+ DavGatewayTray.debug(new BundleMessage("LOG_ACCESS_FORBIDDEN", folderPath, e.getMessage()));
+ }
+ }
+ response.startResponse(encodePath(request, request.getPath(subFolder)));
+ response.startPropstat();
+
+ if (request.hasProperty("resourcetype")) {
+ response.appendProperty("D:resourcetype", "<D:collection/>" +
+ "<C:schedule-inbox xmlns:C=\"urn:ietf:params:xml:ns:caldav\"/>");
+ }
+ if (request.hasProperty("getcontenttype")) {
+ response.appendProperty("D:getcontenttype", "text/calendar; component=vevent");
+ }
+ if (request.hasProperty("getctag")) {
+ response.appendProperty("CS:getctag", "CS=\"http://calendarserver.org/ns/\"", ctag);
+ }
+ if (request.hasProperty("getetag")) {
+ response.appendProperty("D:getetag", etag);
+ }
+ if (request.hasProperty("displayname")) {
+ response.appendProperty("D:displayname", "inbox");
+ }
+ response.endPropStatOK();
+ response.endResponse();
+ }
+
+ /**
+ * Append calendar outbox object to Caldav response.
+ *
+ * @param response Caldav response
+ * @param request Caldav request
+ * @param subFolder outbox folder path relative to request path
+ * @throws IOException on error
+ */
+ public void appendOutbox(CaldavResponse response, CaldavRequest request, String subFolder) throws IOException {
+ response.startResponse(encodePath(request, request.getPath(subFolder)));
+ response.startPropstat();
+
+ if (request.hasProperty("resourcetype")) {
+ response.appendProperty("D:resourcetype", "<D:collection/>" +
+ "<C:schedule-outbox xmlns:C=\"urn:ietf:params:xml:ns:caldav\"/>");
+ }
+ if (request.hasProperty("getctag")) {
+ response.appendProperty("CS:getctag", "CS=\"http://calendarserver.org/ns/\"",
+ "0");
+ }
+ if (request.hasProperty("getetag")) {
+ response.appendProperty("D:getetag", "0");
+ }
+ if (request.hasProperty("displayname")) {
+ response.appendProperty("D:displayname", "outbox");
+ }
+ response.endPropStatOK();
+ response.endResponse();
+ }
+
+ /**
+ * Send simple html response to GET /.
+ *
+ * @throws IOException on error
+ */
+ public void sendGetRoot() throws IOException {
+ StringBuilder buffer = new StringBuilder();
+ buffer.append("Connected to DavMail<br/>");
+ buffer.append("UserName :").append(userName).append("<br/>");
+ buffer.append("Email :").append(session.getEmail()).append("<br/>");
+ sendHttpResponse(HttpStatus.SC_OK, null, "text/html;charset=UTF-8", buffer.toString(), true);
+ }
+
+ /**
+ * Send inbox response for request.
+ *
+ * @param request Caldav request
+ * @throws IOException on error
+ */
+ public void sendInbox(CaldavRequest request) throws IOException {
+ CaldavResponse response = new CaldavResponse(HttpStatus.SC_MULTI_STATUS);
+ response.startMultistatus();
+ appendInbox(response, request, null);
+ // do not try to access inbox on shared calendar
+ if (!session.isSharedFolder(request.getFolderPath(null)) && request.getDepth() == 1
+ && !request.isLightning()) {
+ try {
+ DavGatewayTray.debug(new BundleMessage("LOG_SEARCHING_CALENDAR_MESSAGES"));
+ List<ExchangeSession.Event> events = session.getEventMessages(request.getFolderPath());
+ DavGatewayTray.debug(new BundleMessage("LOG_FOUND_CALENDAR_MESSAGES", events.size()));
+ appendEventsResponses(response, request, events);
+ } catch (HttpException e) {
+ // unauthorized access, probably an inbox on shared calendar
+ DavGatewayTray.debug(new BundleMessage("LOG_ACCESS_FORBIDDEN", request.getFolderPath(), e.getMessage()));
+ }
+ }
+ response.endMultistatus();
+ response.close();
+ }
+
+ /**
+ * Send outbox response for request.
+ *
+ * @param request Caldav request
+ * @throws IOException on error
+ */
+ public void sendOutbox(CaldavRequest request) throws IOException {
+ CaldavResponse response = new CaldavResponse(HttpStatus.SC_MULTI_STATUS);
+ response.startMultistatus();
+ appendOutbox(response, request, null);
+ response.endMultistatus();
+ response.close();
+ }
+
+ /**
+ * Send calendar response for request.
+ *
+ * @param request Caldav request
+ * @throws IOException on error
+ */
+ public void sendFolderOrItem(CaldavRequest request) throws IOException {
+ String folderPath = request.getFolderPath();
+ // process request before sending response to avoid sending headers twice on error
+ ExchangeSession.Folder folder = session.getFolder(folderPath);
+ List<ExchangeSession.Contact> contacts = null;
+ List<ExchangeSession.Event> events = null;
+ List<ExchangeSession.Folder> folderList = null;
+ if (request.getDepth() == 1) {
+ if (folder.isContact()) {
+ contacts = session.getAllContacts(folderPath);
+ } else if (folder.isCalendar() || folder.isTask()) {
+ events = session.getAllEvents(folderPath);
+ if (!folderPath.startsWith("/public")) {
+ folderList = session.getSubCalendarFolders(folderPath, false);
+ }
+ }
+ }
+
+ CaldavResponse response = new CaldavResponse(HttpStatus.SC_MULTI_STATUS);
+ response.startMultistatus();
+ appendFolderOrItem(response, request, folder, null);
+ if (request.getDepth() == 1) {
+ if (folder.isContact()) {
+ appendContactsResponses(response, request, contacts);
+ } else if (folder.isCalendar() || folder.isTask()) {
+ appendEventsResponses(response, request, events);
+ // Send sub folders for multi-calendar support under iCal, except for public folders
+ if (folderList != null) {
+ for (ExchangeSession.Folder subFolder : folderList) {
+ appendFolderOrItem(response, request, subFolder, subFolder.folderPath.substring(subFolder.folderPath.indexOf('/') + 1));
+ }
+ }
+ }
+ }
+ response.endMultistatus();
+ response.close();
+ }
+
+ /**
+ * Fake PROPPATCH response for request.
+ *
+ * @param request Caldav request
+ * @throws IOException on error
+ */
+ public void patchCalendar(CaldavRequest request) throws IOException {
+ String displayname = request.getProperty("displayname");
+ String folderPath = request.getFolderPath();
+ if (displayname != null) {
+ String targetPath = request.getParentFolderPath() + '/' + displayname;
+ if (!targetPath.equals(folderPath)) {
+ session.moveFolder(folderPath, targetPath);
+ }
+ }
+ CaldavResponse response = new CaldavResponse(HttpStatus.SC_MULTI_STATUS);
+ response.startMultistatus();
+ // ical calendar folder proppatch
+ if (request.hasProperty("calendar-color") || request.hasProperty("calendar-order")) {
+ response.startPropstat();
+ if (request.hasProperty("calendar-color")) {
+ response.appendProperty("x1:calendar-color", "x1=\"http://apple.com/ns/ical/\"", null);
+ }
+ if (request.hasProperty("calendar-order")) {
+ response.appendProperty("x1:calendar-order", "x1=\"http://apple.com/ns/ical/\"", null);
+ }
+ response.endPropStatOK();
+ }
+ response.endMultistatus();
+ response.close();
+ }
+
+ protected String getEventFileNameFromPath(String path) {
+ int index = path.lastIndexOf('/');
+ if (index < 0) {
+ return null;
+ } else {
+ return StringUtil.xmlDecode(path.substring(index + 1));
+ }
+ }
+
+ /**
+ * Report items listed in request.
+ *
+ * @param request Caldav request
+ * @throws IOException on error
+ */
+ public void reportItems(CaldavRequest request) throws IOException {
+ String folderPath = request.getFolderPath();
+ List<ExchangeSession.Event> events;
+ List<String> notFound = new ArrayList<String>();
+
+ CaldavResponse response = new CaldavResponse(HttpStatus.SC_MULTI_STATUS);
+ response.startMultistatus();
+ if (request.isMultiGet()) {
+ int count = 0;
+ int total = request.getHrefs().size();
+ for (String href : request.getHrefs()) {
+ DavGatewayTray.debug(new BundleMessage("LOG_REPORT_ITEM", ++count, total));
+ DavGatewayTray.switchIcon();
+ String eventName = getEventFileNameFromPath(href);
+ try {
+ // ignore cases for Sunbird
+ if (eventName != null && eventName.length() > 0
+ && !"inbox".equals(eventName) && !"calendar".equals(eventName)) {
+ ExchangeSession.Item item;
+ try {
+ item = session.getItem(folderPath, eventName);
+ } catch (HttpNotFoundException e) {
+ // workaround for Lightning bug
+ if (request.isBrokenLightning() && eventName.indexOf('%') >= 0) {
+ item = session.getItem(folderPath, URIUtil.decode(StringUtil.encodePlusSign(eventName)));
+ } else {
+ throw e;
+ }
+
+ }
+ appendItemResponse(response, request, item);
+ }
+ } catch (SocketException e) {
+ // rethrow SocketException (client closed connection)
+ throw e;
+ } catch (Exception e) {
+ wireLogger.debug(e.getMessage(), e);
+ DavGatewayTray.warn(new BundleMessage("LOG_ITEM_NOT_AVAILABLE", eventName, href));
+ notFound.add(href);
+ }
+ }
+ } else if (request.isPath(1, "users") && request.isPath(3, "inbox")) {
+ events = session.getEventMessages(request.getFolderPath());
+ appendEventsResponses(response, request, events);
+ } else {
+ // TODO: handle contacts ?
+ if (request.vTodoOnly) {
+ events = session.searchTasksOnly(request.getFolderPath());
+ } else if (request.vEventOnly) {
+ events = session.searchEventsOnly(request.getFolderPath(), request.timeRangeStart, request.timeRangeEnd);
+ } else {
+ events = session.searchEvents(request.getFolderPath(), request.timeRangeStart, request.timeRangeEnd);
+ }
+ appendEventsResponses(response, request, events);
+ }
+
+ // send not found events errors
+ for (String href : notFound) {
+ response.startResponse(encodePath(request, href));
+ response.appendPropstatNotFound();
+ response.endResponse();
+ }
+ response.endMultistatus();
+ response.close();
+ }
+
+ /**
+ * Send principals folder.
+ *
+ * @param request Caldav request
+ * @throws IOException on error
+ */
+ public void sendPrincipalsFolder(CaldavRequest request) throws IOException {
+ CaldavResponse response = new CaldavResponse(HttpStatus.SC_MULTI_STATUS);
+ response.startMultistatus();
+ response.startResponse(encodePath(request, request.getPath()));
+ response.startPropstat();
+
+ if (request.hasProperty("current-user-principal")) {
+ response.appendHrefProperty("D:current-user-principal", encodePath(request, "/principals/users/" + session.getEmail()));
+ }
+ response.endPropStatOK();
+ response.endResponse();
+ response.endMultistatus();
+ response.close();
+ }
+
+ /**
+ * Send user response for request.
+ *
+ * @param request Caldav request
+ * @throws IOException on error
+ */
+ public void sendUserRoot(CaldavRequest request) throws IOException {
+ CaldavResponse response = new CaldavResponse(HttpStatus.SC_MULTI_STATUS);
+ response.startMultistatus();
+ response.startResponse(encodePath(request, request.getPath()));
+ response.startPropstat();
+
+ if (request.hasProperty("resourcetype")) {
+ response.appendProperty("D:resourcetype", "<D:collection/>");
+ }
+ if (request.hasProperty("displayname")) {
+ response.appendProperty("D:displayname", request.getLastPath());
+ }
+ if (request.hasProperty("getctag")) {
+ ExchangeSession.Folder rootFolder = session.getFolder("");
+ response.appendProperty("CS:getctag", "CS=\"http://calendarserver.org/ns/\"",
+ base64Encode(rootFolder.ctag));
+ }
+ response.endPropStatOK();
+ if (request.getDepth() == 1) {
+ appendInbox(response, request, "inbox");
+ appendOutbox(response, request, "outbox");
+ appendFolderOrItem(response, request, session.getFolder(request.getFolderPath("calendar")), "calendar");
+ appendFolderOrItem(response, request, session.getFolder(request.getFolderPath("contacts")), "contacts");
+ }
+ response.endResponse();
+ response.endMultistatus();
+ response.close();
+ }
+
+ /**
+ * Send caldav response for / request.
+ *
+ * @param request Caldav request
+ * @throws IOException on error
+ */
+ public void sendRoot(CaldavRequest request) throws IOException {
+ CaldavResponse response = new CaldavResponse(HttpStatus.SC_MULTI_STATUS);
+ response.startMultistatus();
+ response.startResponse("/");
+ response.startPropstat();
+
+ if (request.hasProperty("principal-collection-set")) {
+ response.appendHrefProperty("D:principal-collection-set", "/principals/users/");
+ }
+ if (request.hasProperty("displayname")) {
+ response.appendProperty("D:displayname", "ROOT");
+ }
+ if (request.hasProperty("resourcetype")) {
+ response.appendProperty("D:resourcetype", "<D:collection/>");
+ }
+ if (request.hasProperty("current-user-principal")) {
+ response.appendHrefProperty("D:current-user-principal", encodePath(request, "/principals/users/" + session.getEmail()));
+ }
+ response.endPropStatOK();
+ response.endResponse();
+ if (request.depth == 1) {
+ // iPhone workaround: send calendar subfolder
+ response.startResponse("/users/" + session.getEmail() + "/calendar");
+ response.startPropstat();
+ if (request.hasProperty("resourcetype")) {
+ response.appendProperty("D:resourcetype", "<D:collection/>" +
+ "<C:calendar xmlns:C=\"urn:ietf:params:xml:ns:caldav\"/>");
+ }
+ if (request.hasProperty("displayname")) {
+ response.appendProperty("D:displayname", session.getEmail());
+ }
+ if (request.hasProperty("supported-calendar-component-set")) {
+ response.appendProperty("C:supported-calendar-component-set", "<C:comp name=\"VEVENT\"/>");
+ }
+ response.endPropStatOK();
+ response.endResponse();
+
+ response.startResponse("/users");
+ response.startPropstat();
+ if (request.hasProperty("displayname")) {
+ response.appendProperty("D:displayname", "users");
+ }
+ if (request.hasProperty("resourcetype")) {
+ response.appendProperty("D:resourcetype", "<D:collection/>");
+ }
+ response.endPropStatOK();
+ response.endResponse();
+
+ response.startResponse("/principals");
+ response.startPropstat();
+ if (request.hasProperty("displayname")) {
+ response.appendProperty("D:displayname", "principals");
+ }
+ if (request.hasProperty("resourcetype")) {
+ response.appendProperty("D:resourcetype", "<D:collection/>");
+ }
+ response.endPropStatOK();
+ response.endResponse();
+ }
+ response.endMultistatus();
+ response.close();
+ }
+
+ /**
+ * Send caldav response for /directory/ request.
+ *
+ * @param request Caldav request
+ * @throws IOException on error
+ */
+ public void sendDirectory(CaldavRequest request) throws IOException {
+ CaldavResponse response = new CaldavResponse(HttpStatus.SC_MULTI_STATUS);
+ response.startMultistatus();
+ response.startResponse("/directory/");
+ response.startPropstat();
+ if (request.hasProperty("current-user-privilege-set")) {
+ response.appendProperty("D:current-user-privilege-set", "<D:privilege><D:read/></D:privilege>");
+ }
+ response.endPropStatOK();
+ response.endResponse();
+ response.endMultistatus();
+ response.close();
+ }
+
+ /**
+ * Send caldav response for /.well-known/ request.
+ *
+ * @throws IOException on error
+ */
+ public void sendWellKnown() throws IOException {
+ HashMap<String, String> headers = new HashMap<String, String>();
+ headers.put("Location", "/");
+ sendHttpResponse(HttpStatus.SC_MOVED_PERMANENTLY, headers);
+ }
+
+ /**
+ * Send Caldav principal response.
+ *
+ * @param request Caldav request
+ * @param prefix principal prefix (users or public)
+ * @param principal principal name (email address for users)
+ * @throws IOException on error
+ */
+ public void sendPrincipal(CaldavRequest request, String prefix, String principal) throws IOException {
+ // actual principal is email address
+ String actualPrincipal = principal;
+ if ("users".equals(prefix) &&
+ (principal.equalsIgnoreCase(session.getAlias()) || (principal.equalsIgnoreCase(session.getAliasFromLogin())))) {
+ actualPrincipal = session.getEmail();
+ }
+
+ CaldavResponse response = new CaldavResponse(HttpStatus.SC_MULTI_STATUS);
+ response.startMultistatus();
+ response.startResponse(encodePath(request, "/principals/" + prefix + '/' + principal));
+ response.startPropstat();
+
+ if (request.hasProperty("principal-URL") && request.isIcal5()) {
+ response.appendHrefProperty("D:principal-URL", encodePath(request, "/principals/" + prefix + '/' + actualPrincipal));
+ }
+
+
+ if (request.hasProperty("calendar-home-set")) {
+ if ("users".equals(prefix)) {
+ response.appendHrefProperty("C:calendar-home-set", encodePath(request, "/users/" + actualPrincipal + "/calendar/"));
+ } else {
+ response.appendHrefProperty("C:calendar-home-set", encodePath(request, '/' + prefix + '/' + actualPrincipal));
+ }
+ }
+
+ if (request.hasProperty("calendar-user-address-set") && "users".equals(prefix)) {
+ response.appendHrefProperty("C:calendar-user-address-set", "mailto:" + actualPrincipal);
+ }
+
+ if (request.hasProperty("addressbook-home-set")) {
+ if (request.isUserAgent("Address%20Book") || request.isUserAgent("Darwin")) {
+ response.appendHrefProperty("E:addressbook-home-set", encodePath(request, '/' + prefix + '/' + actualPrincipal + '/'));
+ } else if ("users".equals(prefix)) {
+ response.appendHrefProperty("E:addressbook-home-set", encodePath(request, "/users/" + actualPrincipal + "/contacts/"));
+ } else {
+ response.appendHrefProperty("E:addressbook-home-set", encodePath(request, '/' + prefix + '/' + actualPrincipal + '/'));
+ }
+ }
+
+ if ("users".equals(prefix)) {
+ if (request.hasProperty("schedule-inbox-URL")) {
+ response.appendHrefProperty("C:schedule-inbox-URL", encodePath(request, "/users/" + actualPrincipal + "/inbox/"));
+ }
+
+ if (request.hasProperty("schedule-outbox-URL")) {
+ response.appendHrefProperty("C:schedule-outbox-URL", encodePath(request, "/users/" + actualPrincipal + "/outbox/"));
+ }
+ } else {
+ // public calendar, send root href as inbox url (always empty) for Lightning
+ if (request.isLightning() && request.hasProperty("schedule-inbox-URL")) {
+ response.appendHrefProperty("C:schedule-inbox-URL", "/");
+ }
+ // send user outbox
+ if (request.hasProperty("schedule-outbox-URL")) {
+ response.appendHrefProperty("C:schedule-outbox-URL", encodePath(request, "/users/" + session.getEmail() + "/outbox/"));
+ }
+ }
+
+ if (request.hasProperty("displayname")) {
+ response.appendProperty("D:displayname", actualPrincipal);
+ }
+ if (request.hasProperty("resourcetype")) {
+ response.appendProperty("D:resourcetype", "<D:collection/><D:principal/>");
+ }
+ if (request.hasProperty("supported-report-set")) {
+ response.appendProperty("D:supported-report-set", "<D:supported-report><D:report><C:calendar-multiget/></D:report></D:supported-report>");
+ }
+ response.endPropStatOK();
+ response.endResponse();
+ response.endMultistatus();
+ response.close();
+ }
+
+ /**
+ * Send free busy response for body request.
+ *
+ * @param body request body
+ * @throws IOException on error
+ */
+ public void sendFreeBusy(String body) throws IOException {
+ HashMap<String, String> valueMap = new HashMap<String, String>();
+ ArrayList<String> attendees = new ArrayList<String>();
+ HashMap<String, String> attendeeKeyMap = new HashMap<String, String>();
+ ICSBufferedReader reader = new ICSBufferedReader(new StringReader(body));
+ String line;
+ String key;
+ while ((line = reader.readLine()) != null) {
+ int index = line.indexOf(':');
+ if (index <= 0) {
+ throw new DavMailException("EXCEPTION_INVALID_REQUEST", body);
+ }
+ String fullkey = line.substring(0, index);
+ String value = line.substring(index + 1);
+ int semicolonIndex = fullkey.indexOf(';');
+ if (semicolonIndex > 0) {
+ key = fullkey.substring(0, semicolonIndex);
+ } else {
+ key = fullkey;
+ }
+ if ("ATTENDEE".equals(key)) {
+ attendees.add(value);
+ attendeeKeyMap.put(value, fullkey);
+ } else {
+ valueMap.put(key, value);
+ }
+ }
+ // get freebusy for each attendee
+ HashMap<String, ExchangeSession.FreeBusy> freeBusyMap = new HashMap<String, ExchangeSession.FreeBusy>();
+ for (String attendee : attendees) {
+ ExchangeSession.FreeBusy freeBusy = session.getFreebusy(attendee, valueMap.get("DTSTART"), valueMap.get("DTEND"));
+ if (freeBusy != null) {
+ freeBusyMap.put(attendee, freeBusy);
+ }
+ }
+ CaldavResponse response = new CaldavResponse(HttpStatus.SC_OK);
+ response.startScheduleResponse();
+
+ for (Map.Entry<String, ExchangeSession.FreeBusy> entry : freeBusyMap.entrySet()) {
+ String attendee = entry.getKey();
+ response.startRecipientResponse(attendee);
+
+ StringBuilder ics = new StringBuilder();
+ ics.append("BEGIN:VCALENDAR").append((char) 13).append((char) 10)
+ .append("VERSION:2.0").append((char) 13).append((char) 10)
+ .append("PRODID:-//davmail.sf.net/NONSGML DavMail Calendar V1.1//EN").append((char) 13).append((char) 10)
+ .append("METHOD:REPLY").append((char) 13).append((char) 10)
+ .append("BEGIN:VFREEBUSY").append((char) 13).append((char) 10)
+ .append("DTSTAMP:").append(valueMap.get("DTSTAMP")).append("").append((char) 13).append((char) 10)
+ .append("ORGANIZER:").append(valueMap.get("ORGANIZER")).append("").append((char) 13).append((char) 10)
+ .append("DTSTART:").append(valueMap.get("DTSTART")).append("").append((char) 13).append((char) 10)
+ .append("DTEND:").append(valueMap.get("DTEND")).append("").append((char) 13).append((char) 10)
+ .append("UID:").append(valueMap.get("UID")).append("").append((char) 13).append((char) 10)
+ .append(attendeeKeyMap.get(attendee)).append(':').append(attendee).append("").append((char) 13).append((char) 10);
+ entry.getValue().appendTo(ics);
+ ics.append("END:VFREEBUSY").append((char) 13).append((char) 10)
+ .append("END:VCALENDAR");
+ response.appendCalendarData(ics.toString());
+ response.endRecipientResponse();
+
+ }
+ response.endScheduleResponse();
+ response.close();
+
+ }
+
+
+ /**
+ * Send Http error response for exception
+ *
+ * @param e exception
+ * @throws IOException on error
+ */
+ public void sendErr(Exception e) throws IOException {
+ String message = e.getMessage();
+ if (message == null) {
+ message = e.toString();
+ }
+ if (e instanceof HttpNotFoundException) {
+ sendErr(HttpStatus.SC_NOT_FOUND, message);
+ } else if (e instanceof HttpPreconditionFailedException) {
+ sendErr(HttpStatus.SC_PRECONDITION_FAILED, message);
+ } else {
+ sendErr(HttpStatus.SC_SERVICE_UNAVAILABLE, message);
+ }
+ }
+
+ /**
+ * Send 400 bad response for unsupported request.
+ *
+ * @param request Caldav request
+ * @throws IOException on error
+ */
+ public void sendUnsupported(CaldavRequest request) throws IOException {
+ BundleMessage message = new BundleMessage("LOG_UNSUPPORTED_REQUEST", request);
+ DavGatewayTray.error(message);
+ sendErr(HttpStatus.SC_BAD_REQUEST, message.format());
+ }
+
+ /**
+ * Send Http error status and message.
+ *
+ * @param status Http status
+ * @param message error messagee
+ * @throws IOException on error
+ */
+ public void sendErr(int status, String message) throws IOException {
+ sendHttpResponse(status, null, "text/plain;charset=UTF-8", message, false);
+ }
+
+ /**
+ * Send OPTIONS response.
+ *
+ * @throws IOException on error
+ */
+ public void sendOptions() throws IOException {
+ HashMap<String, String> headers = new HashMap<String, String>();
+ headers.put("Allow", "OPTIONS, PROPFIND, HEAD, GET, REPORT, PROPPATCH, PUT, DELETE, POST");
+ sendHttpResponse(HttpStatus.SC_OK, headers);
+ }
+
+ /**
+ * Send 401 Unauthorized response.
+ *
+ * @throws IOException on error
+ */
+ public void sendUnauthorized() throws IOException {
+ HashMap<String, String> headers = new HashMap<String, String>();
+ headers.put("WWW-Authenticate", "Basic realm=\"" + BundleMessage.format("UI_DAVMAIL_GATEWAY") + '\"');
+ sendHttpResponse(HttpStatus.SC_UNAUTHORIZED, headers, null, (byte[]) null, true);
+ }
+
+ /**
+ * Send Http response with given status.
+ *
+ * @param status Http status
+ * @throws IOException on error
+ */
+ public void sendHttpResponse(int status) throws IOException {
+ sendHttpResponse(status, null, null, (byte[]) null, true);
+ }
+
+ /**
+ * Send Http response with given status and headers.
+ *
+ * @param status Http status
+ * @param headers Http headers
+ * @throws IOException on error
+ */
+ public void sendHttpResponse(int status, Map<String, String> headers) throws IOException {
+ sendHttpResponse(status, headers, null, (byte[]) null, true);
+ }
+
+ /**
+ * Send Http response with given status in chunked mode.
+ *
+ * @param status Http status
+ * @param contentType MIME content type
+ * @throws IOException on error
+ */
+ public void sendChunkedHttpResponse(int status, String contentType) throws IOException {
+ HashMap<String, String> headers = new HashMap<String, String>();
+ headers.put("Transfer-Encoding", "chunked");
+ sendHttpResponse(status, headers, contentType, (byte[]) null, true);
+ }
+
+ /**
+ * Send Http response with given status, headers, content type and content.
+ * Close connection if keepAlive is false
+ *
+ * @param status Http status
+ * @param headers Http headers
+ * @param contentType MIME content type
+ * @param content response body as string
+ * @param keepAlive keep connection open
+ * @throws IOException on error
+ */
+ public void sendHttpResponse(int status, Map<String, String> headers, String contentType, String content, boolean keepAlive) throws IOException {
+ sendHttpResponse(status, headers, contentType, content.getBytes("UTF-8"), keepAlive);
+ }
+
+ /**
+ * Send Http response with given status, headers, content type and content.
+ * Close connection if keepAlive is false
+ *
+ * @param status Http status
+ * @param headers Http headers
+ * @param contentType MIME content type
+ * @param content response body as byte array
+ * @param keepAlive keep connection open
+ * @throws IOException on error
+ */
+ public void sendHttpResponse(int status, Map<String, String> headers, String contentType, byte[] content, boolean keepAlive) throws IOException {
+ sendClient("HTTP/1.1 " + status + ' ' + HttpStatus.getStatusText(status));
+ if (status != HttpStatus.SC_UNAUTHORIZED) {
+ sendClient("Server: DavMail Gateway " + DavGateway.getCurrentVersion());
+ sendClient("DAV: 1, calendar-access, calendar-schedule, calendarserver-private-events, addressbook");
+ SimpleDateFormat formatter = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.ENGLISH);
+ // force GMT timezone
+ formatter.setTimeZone(ExchangeSession.GMT_TIMEZONE);
+ String now = formatter.format(new Date());
+ sendClient("Date: " + now);
+ sendClient("Expires: " + now);
+ sendClient("Cache-Control: private, max-age=0");
+ }
+ if (headers != null) {
+ for (Map.Entry<String, String> header : headers.entrySet()) {
+ sendClient(header.getKey() + ": " + header.getValue());
+ }
+ }
+ if (contentType != null) {
+ sendClient("Content-Type: " + contentType);
+ }
+ closed = closed || !keepAlive;
+ sendClient("Connection: " + (closed ? "close" : "keep-alive"));
+ if (content != null && content.length > 0) {
+ sendClient("Content-Length: " + content.length);
+ } else if (headers == null || !"chunked".equals(headers.get("Transfer-Encoding"))) {
+ sendClient("Content-Length: 0");
+ }
+ sendClient("");
+ if (content != null && content.length > 0) {
+ // full debug trace
+ if (wireLogger.isDebugEnabled()) {
+ wireLogger.debug("> " + new String(content, "UTF-8"));
+ }
+ sendClient(content);
+ }
+ }
+
+ /**
+ * Decode HTTP credentials
+ *
+ * @param authorization http authorization header value
+ * @throws IOException if invalid credentials
+ */
+ protected void decodeCredentials(String authorization) throws IOException {
+ int index = authorization.indexOf(' ');
+ if (index > 0) {
+ String mode = authorization.substring(0, index).toLowerCase();
+ if (!"basic".equals(mode)) {
+ throw new DavMailException("EXCEPTION_UNSUPPORTED_AUTHORIZATION_MODE", mode);
+ }
+ String encodedCredentials = authorization.substring(index + 1);
+ String decodedCredentials = base64Decode(encodedCredentials);
+ index = decodedCredentials.indexOf(':');
+ if (index > 0) {
+ userName = decodedCredentials.substring(0, index);
+ password = decodedCredentials.substring(index + 1);
+ } else {
+ throw new DavMailException("EXCEPTION_INVALID_CREDENTIALS");
+ }
+ } else {
+ throw new DavMailException("EXCEPTION_INVALID_CREDENTIALS");
+ }
+
+ }
+
+ protected static class CaldavRequest {
+ protected final String command;
+ protected final String path;
+ protected final String[] pathElements;
+ protected final Map<String, String> headers;
+ protected int depth;
+ protected final String body;
+ protected final HashMap<String, String> properties = new HashMap<String, String>();
+ protected HashSet<String> hrefs;
+ protected boolean isMultiGet;
+ protected String timeRangeStart;
+ protected String timeRangeEnd;
+ protected boolean vTodoOnly;
+ protected boolean vEventOnly;
+
+ protected CaldavRequest(String command, String path, Map<String, String> headers, String body) throws IOException {
+ this.command = command;
+ this.path = path.replaceAll("//", "/");
+ pathElements = this.path.split("/");
+ this.headers = headers;
+ buildDepth();
+ this.body = body;
+
+ if (isPropFind() || isReport() || isMkCalendar() || isPropPatch()) {
+ parseXmlBody();
+ }
+ }
+
+ public boolean isOptions() {
+ return "OPTIONS".equals(command);
+ }
+
+ public boolean isPropFind() {
+ return "PROPFIND".equals(command);
+ }
+
+ public boolean isPropPatch() {
+ return "PROPPATCH".equals(command);
+ }
+
+ public boolean isReport() {
+ return "REPORT".equals(command);
+ }
+
+ public boolean isGet() {
+ return "GET".equals(command);
+ }
+
+ public boolean isHead() {
+ return "HEAD".equals(command);
+ }
+
+ public boolean isPut() {
+ return "PUT".equals(command);
+ }
+
+ public boolean isPost() {
+ return "POST".equals(command);
+ }
+
+ public boolean isDelete() {
+ return "DELETE".equals(command);
+ }
+
+ public boolean isMkCalendar() {
+ return "MKCALENDAR".equals(command);
+ }
+
+ public boolean isMove() {
+ return "MOVE".equals(command);
+ }
+
+ /**
+ * Check if this request is a folder request.
+ *
+ * @return true if this is a folder (not event) request
+ */
+ public boolean isFolder() {
+ return path.endsWith("/") || isPropFind() || isReport() || isPropPatch() || isOptions() || isPost();
+ }
+
+ public boolean isRoot() {
+ return (pathElements.length == 0 || pathElements.length == 1);
+ }
+
+ public boolean isPathLength(int length) {
+ return pathElements.length == length;
+ }
+
+ public int getPathLength() {
+ return pathElements.length;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public String getPath(String subFolder) {
+ String folderPath;
+ if (subFolder == null || subFolder.length() == 0) {
+ folderPath = path;
+ } else if (path.endsWith("/")) {
+ folderPath = path + subFolder;
+ } else {
+ folderPath = path + '/' + subFolder;
+ }
+ if (folderPath.endsWith("/")) {
+ return folderPath;
+ } else {
+ return folderPath + '/';
+ }
+ }
+
+ /**
+ * Check if path element at index is value
+ *
+ * @param index path element index
+ * @param value path value
+ * @return true if path element at index is value
+ */
+ public boolean isPath(int index, String value) {
+ return value != null && value.equals(getPathElement(index));
+ }
+
+ protected String getPathElement(int index) {
+ if (index < pathElements.length) {
+ return pathElements[index];
+ } else {
+ return null;
+ }
+ }
+
+ public String getLastPath() {
+ return getPathElement(getPathLength() - 1);
+ }
+
+ protected boolean isBrokenHrefEncoding() {
+ return isUserAgent("DAVKit/3") || isUserAgent("eM Client/") || isBrokenLightning();
+ }
+
+ protected boolean isBrokenLightning() {
+ return isUserAgent("Lightning/1.0b2");
+ }
+
+ protected boolean isLightning() {
+ return isUserAgent("Lightning/");
+ }
+
+ protected boolean isIcal5() {
+ return isUserAgent("CoreDAV/") || isUserAgent("iOS/5");
+ }
+
+ protected boolean isUserAgent(String key) {
+ String userAgent = headers.get("user-agent");
+ return userAgent != null && userAgent.indexOf(key) >= 0;
+ }
+
+ public boolean isFreeBusy() {
+ return body != null && body.indexOf("VFREEBUSY") >= 0;
+ }
+
+ protected void buildDepth() {
+ String depthValue = headers.get("depth");
+ if ("infinity".equalsIgnoreCase(depthValue)) {
+ depth = Integer.MAX_VALUE;
+ } else if (depthValue != null) {
+ try {
+ depth = Integer.valueOf(depthValue);
+ } catch (NumberFormatException e) {
+ DavGatewayTray.warn(new BundleMessage("LOG_INVALID_DEPTH", depthValue));
+ }
+ }
+ }
+
+ public int getDepth() {
+ return depth;
+ }
+
+ public String getBody() {
+ return body;
+ }
+
+ public String getHeader(String headerName) {
+ return headers.get(headerName);
+ }
+
+ protected void parseXmlBody() throws IOException {
+ XMLStreamReader streamReader = null;
+ try {
+ streamReader = XMLStreamUtil.createXMLStreamReader(body);
+ while (streamReader.hasNext()) {
+ streamReader.next();
+ if (XMLStreamUtil.isStartTag(streamReader)) {
+ String tagLocalName = streamReader.getLocalName();
+ if ("prop".equals(tagLocalName)) {
+ handleProp(streamReader);
+ } else if ("calendar-multiget".equals(tagLocalName)
+ || "addressbook-multiget".equals(tagLocalName)) {
+ isMultiGet = true;
+ } else if ("comp-filter".equals(tagLocalName)) {
+ handleCompFilter(streamReader);
+ } else if ("href".equals(tagLocalName)) {
+ if (hrefs == null) {
+ hrefs = new HashSet<String>();
+ }
+ if (isBrokenHrefEncoding()) {
+ hrefs.add(streamReader.getElementText());
+ } else {
+ hrefs.add(URIUtil.decode(StringUtil.encodePlusSign(streamReader.getElementText())));
+ }
+ }
+ }
+ }
+ } catch (XMLStreamException e) {
+ throw new DavMailException("EXCEPTION_INVALID_CALDAV_REQUEST", e.getMessage());
+ } finally {
+ try {
+ if (streamReader != null) {
+ streamReader.close();
+ }
+ } catch (XMLStreamException e) {
+ DavGatewayTray.error(e);
+ }
+ }
+ }
+
+ protected boolean isEndTag(XMLStreamReader reader, String tagLocalName) {
+ return (reader.getEventType() == XMLStreamConstants.END_ELEMENT) && (reader.getLocalName().equals(tagLocalName));
+ }
+
+ public void handleCompFilter(XMLStreamReader reader) throws XMLStreamException {
+ while (reader.hasNext() && !isEndTag(reader, "comp-filter")) {
+ reader.next();
+ if (XMLStreamUtil.isStartTag(reader, "comp-filter")) {
+ String name = reader.getAttributeValue(null, "name");
+ if ("VEVENT".equals(name)) {
+ vEventOnly = true;
+ } else if ("VTODO".equals(name)) {
+ vTodoOnly = true;
+ }
+ } else if (XMLStreamUtil.isStartTag(reader, "time-range")) {
+ timeRangeStart = reader.getAttributeValue(null, "start");
+ timeRangeEnd = reader.getAttributeValue(null, "end");
+ }
+ }
+ }
+
+ public void handleProp(XMLStreamReader reader) throws XMLStreamException {
+ while (reader.hasNext() && !isEndTag(reader, "prop")) {
+ reader.next();
+ if (XMLStreamUtil.isStartTag(reader)) {
+ String tagLocalName = reader.getLocalName();
+ String tagText = null;
+ if ("displayname".equals(tagLocalName) || reader.hasText()) {
+ tagText = XMLStreamUtil.getElementText(reader);
+ }
+ properties.put(tagLocalName, tagText);
+ }
+ }
+ }
+
+ public boolean hasProperty(String propertyName) {
+ return properties.containsKey(propertyName);
+ }
+
+ public String getProperty(String propertyName) {
+ return properties.get(propertyName);
+ }
+
+ public boolean isMultiGet() {
+ return isMultiGet && hrefs != null;
+ }
+
+ public Set<String> getHrefs() {
+ return hrefs;
+ }
+
+ @Override
+ public String toString() {
+ return command + ' ' + path + " Depth: " + depth + '\n' + body;
+ }
+
+ /**
+ * Get request folder path.
+ *
+ * @return exchange folder path
+ */
+ public String getFolderPath() {
+ return getFolderPath(null);
+ }
+
+ public String getParentFolderPath() {
+ int endIndex;
+ if (isFolder()) {
+ endIndex = getPathLength() - 1;
+ } else {
+ endIndex = getPathLength() - 2;
+ }
+ return getFolderPath(endIndex, null);
+ }
+
+ /**
+ * Get request folder path with subFolder.
+ *
+ * @param subFolder sub folder path
+ * @return folder path
+ */
+ public String getFolderPath(String subFolder) {
+ int endIndex;
+ if (isFolder()) {
+ endIndex = getPathLength();
+ } else {
+ endIndex = getPathLength() - 1;
+ }
+ return getFolderPath(endIndex, subFolder);
+ }
+
+ protected String getFolderPath(int endIndex, String subFolder) {
+
+ StringBuilder calendarPath = new StringBuilder();
+ for (int i = 0; i < endIndex; i++) {
+ if (getPathElement(i).length() > 0) {
+ calendarPath.append('/').append(getPathElement(i));
+ }
+ }
+ if (subFolder != null && subFolder.length() > 0) {
+ calendarPath.append('/').append(subFolder);
+ }
+ if (this.isUserAgent("Address%20Book") || this.isUserAgent("Darwin")) {
+ /* WARNING - This is a kludge -
+ * If your public folder address book path has spaces, then Address Book app just ignores that account
+ * This kludge allows you to specify the path in which spaces are encoded as ___
+ * It'll make Address book to not ignore the account and communicate with DavMail.
+ * Here we replace the ___ in the path with spaces. Be warned if your actual address book path has ___
+ * it'll fail.
+ */
+ String result = calendarPath.toString();
+ // replace unsupported spaces
+ if (result.indexOf(' ') >=0 ) {
+ result = result.replaceAll("___", " ");
+ }
+ // replace /addressbook suffix on public folders
+ if (result.startsWith("/public")) {
+ result = result.replaceAll("/addressbook", "");
+ }
+
+ return result;
+ } else {
+ return calendarPath.toString();
+ }
+ }
+ }
+
+ /**
+ * Http chunked response.
+ */
+ protected class ChunkedResponse {
+ Writer writer;
+
+ protected ChunkedResponse(int status, String contentType) throws IOException {
+ writer = new OutputStreamWriter(new BufferedOutputStream(new OutputStream() {
+ @Override
+ public void write(byte[] data, int offset, int length) throws IOException {
+ sendClient(Integer.toHexString(length));
+ sendClient(data, offset, length);
+ if (wireLogger.isDebugEnabled()) {
+ StringBuilder logBuffer = new StringBuilder("> ");
+ logBuffer.append(new String(data, offset, length, "UTF-8"));
+ wireLogger.debug(logBuffer.toString());
+ }
+ sendClient("");
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void close() throws IOException {
+ sendClient("0");
+ sendClient("");
+ }
+ }), "UTF-8");
+ sendChunkedHttpResponse(status, contentType);
+ }
+
+ public void append(String data) throws IOException {
+ writer.write(data);
+ }
+
+ public void close() throws IOException {
+ writer.close();
+ }
+ }
+
+ /**
+ * Caldav response wrapper, content sent in chunked mode to avoid timeout
+ */
+ protected class CaldavResponse extends ChunkedResponse {
+
+ protected CaldavResponse(int status) throws IOException {
+ super(status, "text/xml;charset=UTF-8");
+ writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
+ }
+
+
+ public void startMultistatus() throws IOException {
+ writer.write("<D:multistatus xmlns:D=\"DAV:\" xmlns:C=\"urn:ietf:params:xml:ns:caldav\" xmlns:E=\"urn:ietf:params:xml:ns:carddav\">");
+ }
+
+ public void startResponse(String href) throws IOException {
+ writer.write("<D:response>");
+ writer.write("<D:href>");
+ writer.write(StringUtil.xmlEncode(href));
+ writer.write("</D:href>");
+ }
+
+ public void startPropstat() throws IOException {
+ writer.write("<D:propstat>");
+ writer.write("<D:prop>");
+ }
+
+ public void appendCalendarData(String ics) throws IOException {
+ if (ics != null && ics.length() > 0) {
+ writer.write("<C:calendar-data xmlns:C=\"urn:ietf:params:xml:ns:caldav\"");
+ writer.write(" C:content-type=\"text/calendar\" C:version=\"2.0\">");
+ writer.write(StringUtil.xmlEncode(ics));
+ writer.write("</C:calendar-data>");
+ }
+ }
+
+ public void appendContactData(String vcard) throws IOException {
+ if (vcard != null && vcard.length() > 0) {
+ writer.write("<E:address-data>");
+ writer.write(StringUtil.xmlEncode(vcard));
+ writer.write("</E:address-data>");
+ }
+ }
+
+ public void appendHrefProperty(String propertyName, String propertyValue) throws IOException {
+ appendProperty(propertyName, null, "<D:href>" + StringUtil.xmlEncode(propertyValue) + "</D:href>");
+ }
+
+ public void appendProperty(String propertyName) throws IOException {
+ appendProperty(propertyName, null);
+ }
+
+ public void appendProperty(String propertyName, String propertyValue) throws IOException {
+ appendProperty(propertyName, null, propertyValue);
+ }
+
+ public void appendProperty(String propertyName, String namespace, String propertyValue) throws IOException {
+ if (propertyValue != null) {
+ writer.write('<');
+ writer.write(propertyName);
+ if (namespace != null) {
+ writer.write(" xmlns:");
+ writer.write(namespace);
+ }
+ writer.write('>');
+ writer.write(propertyValue);
+ writer.write("</");
+ writer.write(propertyName);
+ writer.write('>');
+ } else {
+ writer.write('<');
+ writer.write(propertyName);
+ if (namespace != null) {
+ writer.write(" xmlns:");
+ writer.write(namespace);
+ }
+ writer.write("/>");
+ }
+ }
+
+ public void endPropStatOK() throws IOException {
+ writer.write("</D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat>");
+ }
+
+ public void appendPropstatNotFound() throws IOException {
+ writer.write("<D:propstat><D:status>HTTP/1.1 404 Not Found</D:status></D:propstat>");
+ }
+
+ public void endResponse() throws IOException {
+ writer.write("</D:response>");
+ }
+
+ public void endMultistatus() throws IOException {
+ writer.write("</D:multistatus>");
+ }
+
+ public void startScheduleResponse() throws IOException {
+ writer.write("<C:schedule-response xmlns:D=\"DAV:\" xmlns:C=\"urn:ietf:params:xml:ns:caldav\">");
+ }
+
+ public void startRecipientResponse(String recipient) throws IOException {
+ writer.write("<C:response><C:recipient><D:href>");
+ writer.write(recipient);
+ writer.write("</D:href></C:recipient><C:request-status>2.0;Success</C:request-status>");
+ }
+
+ public void endRecipientResponse() throws IOException {
+ writer.write("</C:response>");
+ }
+
+ public void endScheduleResponse() throws IOException {
+ writer.write("</C:schedule-response>");
+ }
+
+ }
+}
+
diff --git a/src/java/davmail/caldav/CaldavServer.java b/src/java/davmail/caldav/CaldavServer.java
new file mode 100644
index 0000000..d52d2bb
--- /dev/null
+++ b/src/java/davmail/caldav/CaldavServer.java
@@ -0,0 +1,56 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.caldav;
+
+import davmail.AbstractConnection;
+import davmail.AbstractServer;
+import davmail.Settings;
+
+import java.net.Socket;
+
+/**
+ * Calendar server, handle HTTP Caldav requests.
+ */
+public class CaldavServer extends AbstractServer {
+ /**
+ * Default HTTP Caldav port
+ */
+ public static final int DEFAULT_PORT = 80;
+
+ /**
+ * Create a ServerSocket to listen for connections.
+ * Start the thread.
+ *
+ * @param port pop listen port, 80 if not defined (0)
+ */
+ public CaldavServer(int port) {
+ super(CaldavServer.class.getName(), port, CaldavServer.DEFAULT_PORT);
+ nosslFlag = Settings.getBooleanProperty("davmail.ssl.nosecurecaldav");
+ }
+
+ @Override
+ public String getProtocolName() {
+ return "CALDAV";
+ }
+
+ @Override
+ public AbstractConnection createConnectionHandler(Socket clientSocket) {
+ return new CaldavConnection(clientSocket);
+ }
+}
\ No newline at end of file
diff --git a/src/java/davmail/exception/DavMailAuthenticationException.java b/src/java/davmail/exception/DavMailAuthenticationException.java
new file mode 100644
index 0000000..05154a9
--- /dev/null
+++ b/src/java/davmail/exception/DavMailAuthenticationException.java
@@ -0,0 +1,44 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exception;
+
+/**
+ * I18 AuthenticationException subclass.
+ */
+public class DavMailAuthenticationException extends DavMailException {
+ /**
+ * Create a DavMail authentication exception with the given BundleMessage key.
+ *
+ * @param key message key
+ */
+ public DavMailAuthenticationException(String key) {
+ super(key);
+ }
+
+ /**
+ * Create a DavMail authentication exception with the given BundleMessage key and arguments.
+ *
+ * @param key message key
+ * @param arguments message values
+ */
+ public DavMailAuthenticationException(String key, Object... arguments) {
+ super(key, arguments);
+ }
+
+}
diff --git a/src/java/davmail/exception/DavMailException.java b/src/java/davmail/exception/DavMailException.java
new file mode 100644
index 0000000..d97d5a3
--- /dev/null
+++ b/src/java/davmail/exception/DavMailException.java
@@ -0,0 +1,70 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exception;
+
+import davmail.BundleMessage;
+
+import java.io.IOException;
+import java.util.Locale;
+
+/**
+ * I18 IOException subclass.
+ */
+public class DavMailException extends IOException {
+ private final BundleMessage message;
+
+ /**
+ * Create a DavMail exception with the given BundleMessage key and arguments.
+ *
+ * @param key message key
+ * @param arguments message values
+ */
+ public DavMailException(String key, Object... arguments) {
+ this.message = new BundleMessage(key, arguments);
+ }
+
+ /**
+ * Get formatted message
+ *
+ * @return english formatted message
+ */
+ @Override
+ public String getMessage() {
+ return message.formatLog();
+ }
+
+ /**
+ * Get formatted message using locale.
+ *
+ * @param locale locale
+ * @return localized formatted message
+ */
+ public String getMessage(Locale locale) {
+ return message.format(locale);
+ }
+
+ /**
+ * Get internal exception BundleMessage.
+ *
+ * @return unformatted message
+ */
+ public BundleMessage getBundleMessage() {
+ return message;
+ }
+}
diff --git a/src/java/davmail/exception/HttpForbiddenException.java b/src/java/davmail/exception/HttpForbiddenException.java
new file mode 100644
index 0000000..30faa49
--- /dev/null
+++ b/src/java/davmail/exception/HttpForbiddenException.java
@@ -0,0 +1,35 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exception;
+
+import org.apache.commons.httpclient.HttpException;
+
+/**
+ * HttpException with 403 forbidden status.
+ */
+public class HttpForbiddenException extends HttpException {
+ /**
+ * HttpException with 403 forbidden status.
+ *
+ * @param message exception message
+ */
+ public HttpForbiddenException(String message) {
+ super(message);
+ }
+}
diff --git a/src/java/davmail/exception/HttpNotFoundException.java b/src/java/davmail/exception/HttpNotFoundException.java
new file mode 100644
index 0000000..6604d0e
--- /dev/null
+++ b/src/java/davmail/exception/HttpNotFoundException.java
@@ -0,0 +1,35 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exception;
+
+import org.apache.commons.httpclient.HttpException;
+
+/**
+ * HttpException with 404 not found status.
+ */
+public class HttpNotFoundException extends HttpException {
+ /**
+ * HttpException with 404 not found status.
+ *
+ * @param message exception message
+ */
+ public HttpNotFoundException(String message) {
+ super(message);
+ }
+}
diff --git a/src/java/davmail/exception/HttpPreconditionFailedException.java b/src/java/davmail/exception/HttpPreconditionFailedException.java
new file mode 100644
index 0000000..458452c
--- /dev/null
+++ b/src/java/davmail/exception/HttpPreconditionFailedException.java
@@ -0,0 +1,35 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exception;
+
+import org.apache.commons.httpclient.HttpException;
+
+/**
+ * HttpException with 412 precondition failed status.
+ */
+public class HttpPreconditionFailedException extends HttpException {
+ /**
+ * HttpException with 412 precondition failed status.
+ *
+ * @param message exception message
+ */
+ public HttpPreconditionFailedException(String message) {
+ super(message);
+ }
+}
diff --git a/src/java/davmail/exception/HttpServerErrorException.java b/src/java/davmail/exception/HttpServerErrorException.java
new file mode 100644
index 0000000..170b983
--- /dev/null
+++ b/src/java/davmail/exception/HttpServerErrorException.java
@@ -0,0 +1,35 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exception;
+
+import org.apache.commons.httpclient.HttpException;
+
+/**
+ * HttpException with 500 internal server error status.
+ */
+public class HttpServerErrorException extends HttpException {
+ /**
+ * HttpException with 500 internal server error status.
+ *
+ * @param message exception message
+ */
+ public HttpServerErrorException(String message) {
+ super(message);
+ }
+}
diff --git a/src/java/davmail/exception/InsufficientStorageException.java b/src/java/davmail/exception/InsufficientStorageException.java
new file mode 100644
index 0000000..3418087
--- /dev/null
+++ b/src/java/davmail/exception/InsufficientStorageException.java
@@ -0,0 +1,35 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exception;
+
+import org.apache.commons.httpclient.HttpException;
+
+/**
+ * HttpException with 507 Insufficient Storage status.
+ */
+public class InsufficientStorageException extends HttpException {
+ /**
+ * HttpException with 507 Insufficient Storage status.
+ *
+ * @param message exception message
+ */
+ public InsufficientStorageException(String message) {
+ super(message);
+ }
+}
\ No newline at end of file
diff --git a/src/java/davmail/exception/LoginTimeoutException.java b/src/java/davmail/exception/LoginTimeoutException.java
new file mode 100644
index 0000000..3cc9289
--- /dev/null
+++ b/src/java/davmail/exception/LoginTimeoutException.java
@@ -0,0 +1,35 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exception;
+
+import org.apache.commons.httpclient.HttpException;
+
+/**
+ * HttpException with 440 login timeout status.
+ */
+public class LoginTimeoutException extends HttpException {
+ /**
+ * HttpException with 550 login timeout status.
+ *
+ * @param message exception message
+ */
+ public LoginTimeoutException(String message) {
+ super(message);
+ }
+}
diff --git a/src/java/davmail/exception/WebdavNotAvailableException.java b/src/java/davmail/exception/WebdavNotAvailableException.java
new file mode 100644
index 0000000..fabd820
--- /dev/null
+++ b/src/java/davmail/exception/WebdavNotAvailableException.java
@@ -0,0 +1,34 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exception;
+
+/**
+ * Exchange 2007 with Webdav disabled will trigger this exception.
+ */
+public class WebdavNotAvailableException extends DavMailException {
+ /**
+ * Create a DavMail exception with the given BundleMessage key and arguments.
+ *
+ * @param key message key
+ * @param arguments message values
+ */
+ public WebdavNotAvailableException(String key, Object... arguments) {
+ super(key, arguments);
+ }
+}
diff --git a/src/java/davmail/exchange/DoubleDotInputStream.java b/src/java/davmail/exchange/DoubleDotInputStream.java
new file mode 100644
index 0000000..f8c85f2
--- /dev/null
+++ b/src/java/davmail/exchange/DoubleDotInputStream.java
@@ -0,0 +1,85 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PushbackInputStream;
+
+/**
+ * Replace double dot lines with single dot in input stream.
+ * A line with a single dot means end of stream
+ */
+public class DoubleDotInputStream extends PushbackInputStream {
+ final int[] buffer = new int[4];
+ int index = -1;
+
+ /**
+ * @inheritDoc
+ */
+ public DoubleDotInputStream(InputStream in) {
+ super(in, 4);
+ }
+
+ /**
+ * Push current byte to buffer and read next byte.
+ *
+ * @return next byte
+ * @throws IOException on error
+ */
+ protected int readNextByte() throws IOException {
+ int b = super.read();
+ buffer[++index] = b;
+ return b;
+ }
+
+ @Override
+ public int read() throws IOException {
+ int b = super.read();
+ if (b == '\r') {
+ // \r\n
+ if (readNextByte() == '\n') {
+ // \r\n.
+ if (readNextByte() == '.') {
+ // \r\n.\r
+ if (readNextByte() == '\r') {
+ // \r\n.\r\n
+ if (readNextByte() == '\n') {
+ // end of stream
+ index = -1;
+ b = -1;
+ }
+ // \r\n..
+ } else if (buffer[index] == '.') {
+ // replace double dot
+ index--;
+ }
+ }
+ }
+ // push back characters
+ if (index >= 0) {
+ while (index >= 0) {
+ unread(buffer[index--]);
+ }
+ }
+ }
+ return b;
+ }
+
+}
diff --git a/src/java/davmail/exchange/DoubleDotOutputStream.java b/src/java/davmail/exchange/DoubleDotOutputStream.java
new file mode 100644
index 0000000..6973f86
--- /dev/null
+++ b/src/java/davmail/exchange/DoubleDotOutputStream.java
@@ -0,0 +1,76 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * RFC 1939: 3 Basic Operations
+ * [...]
+ * If any line begins with the termination octet, the line is "byte-stuffed" by
+ * pre-pending the termination octet to that line of the response.
+ */
+public class DoubleDotOutputStream extends FilterOutputStream {
+
+ // remember last 2 bytes written
+ final int[] buf = {0, 0};
+
+ /**
+ * @inheritDoc
+ */
+ public DoubleDotOutputStream(OutputStream out) {
+ super(out);
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ if (b == '.' && (buf[0] == '\r' || buf[0] == '\n' || buf[0] == 0)) {
+ // line starts with '.', prepend it with an additional '.'
+ out.write('.');
+ }
+ out.write(b);
+
+ buf[1] = buf[0];
+ buf[0] = b;
+ }
+
+ /**
+ * RFC 1939: 3 Basic Operations
+ * [...]
+ * Hence a multi-line response is terminated with the five octets
+ * "CRLF.CRLF"
+ * <p/>
+ * Do not close actual outputstream
+ *
+ * @throws IOException on error
+ */
+ @Override
+ public void close() throws IOException {
+ if (buf[1] != '\r' || buf[0] != '\n') {
+ out.write('\r');
+ out.write('\n');
+ }
+ out.write('.');
+ out.write('\r');
+ out.write('\n');
+ }
+
+}
diff --git a/src/java/davmail/exchange/ExchangeSession.java b/src/java/davmail/exchange/ExchangeSession.java
new file mode 100644
index 0000000..d3ba208
--- /dev/null
+++ b/src/java/davmail/exchange/ExchangeSession.java
@@ -0,0 +1,3378 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange;
+
+import davmail.BundleMessage;
+import davmail.Settings;
+import davmail.exception.DavMailAuthenticationException;
+import davmail.exception.DavMailException;
+import davmail.exception.WebdavNotAvailableException;
+import davmail.http.DavGatewayHttpClientFacade;
+import davmail.http.DavGatewayOTPPrompt;
+import davmail.ui.NotificationDialog;
+import davmail.util.StringUtil;
+import org.apache.commons.httpclient.*;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.apache.commons.httpclient.methods.PostMethod;
+import org.apache.commons.httpclient.params.HttpClientParams;
+import org.apache.commons.httpclient.util.URIUtil;
+import org.apache.log4j.Logger;
+import org.htmlcleaner.CommentToken;
+import org.htmlcleaner.ContentToken;
+import org.htmlcleaner.HtmlCleaner;
+import org.htmlcleaner.TagNode;
+
+import javax.imageio.ImageIO;
+import javax.mail.MessagingException;
+import javax.mail.internet.*;
+import javax.mail.util.SharedByteArrayInputStream;
+import java.awt.image.BufferedImage;
+import java.io.*;
+import java.net.NoRouteToHostException;
+import java.net.UnknownHostException;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+/**
+ * Exchange session through Outlook Web Access (DAV)
+ */
+public abstract class ExchangeSession {
+
+ protected static final Logger LOGGER = Logger.getLogger("davmail.exchange.ExchangeSession");
+
+ /**
+ * Reference GMT timezone to format dates
+ */
+ public static final SimpleTimeZone GMT_TIMEZONE = new SimpleTimeZone(0, "GMT");
+
+ protected static final Set<String> USER_NAME_FIELDS = new HashSet<String>();
+
+ static {
+ USER_NAME_FIELDS.add("username");
+ USER_NAME_FIELDS.add("txtUserName");
+ USER_NAME_FIELDS.add("userid");
+ USER_NAME_FIELDS.add("SafeWordUser");
+ USER_NAME_FIELDS.add("user_name");
+ }
+
+ protected static final Set<String> PASSWORD_FIELDS = new HashSet<String>();
+
+ static {
+ PASSWORD_FIELDS.add("password");
+ PASSWORD_FIELDS.add("txtUserPass");
+ PASSWORD_FIELDS.add("pw");
+ PASSWORD_FIELDS.add("basicPassword");
+ }
+
+ protected static final Set<String> TOKEN_FIELDS = new HashSet<String>();
+
+ static {
+ TOKEN_FIELDS.add("SafeWordPassword");
+ TOKEN_FIELDS.add("passcode");
+ }
+
+ protected static final int FREE_BUSY_INTERVAL = 15;
+
+ protected static final String PUBLIC_ROOT = "/public/";
+ protected static final String CALENDAR = "calendar";
+ protected static final String TASKS = "tasks";
+ /**
+ * Contacts folder logical name
+ */
+ public static final String CONTACTS = "contacts";
+ protected static final String ADDRESSBOOK = "addressbook";
+ protected static final String INBOX = "INBOX";
+ protected static final String LOWER_CASE_INBOX = "inbox";
+ protected static final String SENT = "Sent";
+ protected static final String SENDMSG = "##DavMailSubmissionURI##";
+ protected static final String DRAFTS = "Drafts";
+ protected static final String TRASH = "Trash";
+ protected static final String JUNK = "Junk";
+ protected static final String UNSENT = "Unsent Messages";
+
+ static {
+ // Adjust Mime decoder settings
+ System.setProperty("mail.mime.ignoreunknownencoding", "true");
+ System.setProperty("mail.mime.decodetext.strict", "false");
+ }
+
+ protected String publicFolderUrl;
+
+ /**
+ * Base user mailboxes path (used to select folder)
+ */
+ protected String mailPath;
+ protected String rootPath;
+ protected String email;
+ protected String alias;
+ /**
+ * Lower case Caldav path to current user mailbox.
+ * /users/<i>email</i>
+ */
+ protected String currentMailboxPath;
+ protected final HttpClient httpClient;
+
+ protected String userName;
+
+ protected String serverVersion;
+
+ protected static final String YYYY_MM_DD_HH_MM_SS = "yyyy/MM/dd HH:mm:ss";
+ private static final String YYYYMMDD_T_HHMMSS_Z = "yyyyMMdd'T'HHmmss'Z'";
+ protected static final String YYYY_MM_DD_T_HHMMSS_Z = "yyyy-MM-dd'T'HH:mm:ss'Z'";
+ private static final String YYYY_MM_DD = "yyyy-MM-dd";
+ private static final String YYYY_MM_DD_T_HHMMSS_SSS_Z = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
+
+ /**
+ * Logon form user name fields.
+ */
+ private final List<String> userNameInputs = new ArrayList<String>();
+ /**
+ * Logon form password field, default is password.
+ */
+ private String passwordInput = "password";
+
+ /**
+ * Create an exchange session for the given URL.
+ * The session is established for given userName and password
+ *
+ * @param url Exchange url
+ * @param userName user login name
+ * @param password user password
+ * @throws IOException on error
+ */
+ public ExchangeSession(String url, String userName, String password) throws IOException {
+ this.userName = userName;
+ try {
+ httpClient = DavGatewayHttpClientFacade.getInstance(url);
+ // set private connection pool
+ DavGatewayHttpClientFacade.createMultiThreadedHttpConnectionManager(httpClient);
+ boolean isBasicAuthentication = isBasicAuthentication(httpClient, url);
+
+ DavGatewayHttpClientFacade.setCredentials(httpClient, userName, password);
+
+ // get webmail root url
+ // providing credentials
+ // manually follow redirect
+ HttpMethod method = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, url);
+
+ if (!this.isAuthenticated()) {
+ if (isBasicAuthentication) {
+ int status = method.getStatusCode();
+
+ if (status == HttpStatus.SC_UNAUTHORIZED) {
+ method.releaseConnection();
+ throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
+ } else if (status != HttpStatus.SC_OK) {
+ method.releaseConnection();
+ throw DavGatewayHttpClientFacade.buildHttpException(method);
+ }
+ // workaround for basic authentication on /exchange and form based authentication at /owa
+ if ("/owa/auth/logon.aspx".equals(method.getPath())) {
+ method = formLogin(httpClient, method, userName, password);
+ }
+ } else {
+ method = formLogin(httpClient, method, userName, password);
+ }
+ }
+
+ // avoid 401 roundtrips, only if NTLM is disabled and basic authentication enabled
+ if (isBasicAuthentication && !DavGatewayHttpClientFacade.hasNTLM(httpClient)) {
+ httpClient.getParams().setParameter(HttpClientParams.PREEMPTIVE_AUTHENTICATION, true);
+ }
+
+ buildSessionInfo(method);
+
+ } catch (DavMailAuthenticationException exc) {
+ LOGGER.error(exc.getMessage());
+ throw exc;
+ } catch (UnknownHostException exc) {
+ BundleMessage message = new BundleMessage("EXCEPTION_CONNECT", exc.getClass().getName(), exc.getMessage());
+ ExchangeSession.LOGGER.error(message);
+ throw new DavMailException("EXCEPTION_DAVMAIL_CONFIGURATION", message);
+ } catch (WebdavNotAvailableException exc) {
+ throw exc;
+ } catch (IOException exc) {
+ LOGGER.error(BundleMessage.formatLog("EXCEPTION_EXCHANGE_LOGIN_FAILED", exc));
+ throw new DavMailException("EXCEPTION_EXCHANGE_LOGIN_FAILED", exc);
+ }
+ LOGGER.debug("Session " + this + " created");
+ }
+
+ /**
+ * Format date to exchange search format.
+ *
+ * @param date date object
+ * @return formatted search date
+ */
+ public abstract String formatSearchDate(Date date);
+
+ /**
+ * Return standard zulu date formatter.
+ *
+ * @return zulu date formatter
+ */
+ public static SimpleDateFormat getZuluDateFormat() {
+ SimpleDateFormat dateFormat = new SimpleDateFormat(YYYYMMDD_T_HHMMSS_Z, Locale.ENGLISH);
+ dateFormat.setTimeZone(GMT_TIMEZONE);
+ return dateFormat;
+ }
+
+ protected static SimpleDateFormat getVcardBdayFormat() {
+ SimpleDateFormat dateFormat = new SimpleDateFormat(YYYY_MM_DD, Locale.ENGLISH);
+ dateFormat.setTimeZone(GMT_TIMEZONE);
+ return dateFormat;
+ }
+
+ protected static SimpleDateFormat getExchangeZuluDateFormat() {
+ SimpleDateFormat dateFormat = new SimpleDateFormat(YYYY_MM_DD_T_HHMMSS_Z, Locale.ENGLISH);
+ dateFormat.setTimeZone(GMT_TIMEZONE);
+ return dateFormat;
+ }
+
+ protected static SimpleDateFormat getExchangeZuluDateFormatMillisecond() {
+ SimpleDateFormat dateFormat = new SimpleDateFormat(YYYY_MM_DD_T_HHMMSS_SSS_Z, Locale.ENGLISH);
+ dateFormat.setTimeZone(GMT_TIMEZONE);
+ return dateFormat;
+ }
+
+ protected static Date parseDate(String dateString) throws ParseException {
+ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
+ dateFormat.setTimeZone(GMT_TIMEZONE);
+ return dateFormat.parse(dateString);
+ }
+
+
+ /**
+ * Test if the session expired.
+ *
+ * @return true this session expired
+ * @throws NoRouteToHostException on error
+ * @throws UnknownHostException on error
+ */
+ public boolean isExpired() throws NoRouteToHostException, UnknownHostException {
+ boolean isExpired = false;
+ try {
+ getFolder("");
+ } catch (UnknownHostException exc) {
+ throw exc;
+ } catch (NoRouteToHostException exc) {
+ throw exc;
+ } catch (IOException e) {
+ isExpired = true;
+ }
+
+ return isExpired;
+ }
+
+ /**
+ * Test authentication mode : form based or basic.
+ *
+ * @param url exchange base URL
+ * @param httpClient httpClient instance
+ * @return true if basic authentication detected
+ */
+ protected boolean isBasicAuthentication(HttpClient httpClient, String url) {
+ return DavGatewayHttpClientFacade.getHttpStatus(httpClient, url) == HttpStatus.SC_UNAUTHORIZED;
+ }
+
+ protected String getAbsoluteUri(HttpMethod method, String path) throws URIException {
+ URI uri = method.getURI();
+ if (path != null) {
+ // reset query string
+ uri.setQuery(null);
+ if (path.startsWith("/")) {
+ // path is absolute, replace method path
+ uri.setPath(path);
+ } else if (path.startsWith("http://") || path.startsWith("https://")) {
+ return path;
+ } else {
+ // relative path, build new path
+ String currentPath = method.getPath();
+ int end = currentPath.lastIndexOf('/');
+ if (end >= 0) {
+ uri.setPath(currentPath.substring(0, end + 1) + path);
+ } else {
+ throw new URIException(uri.getURI());
+ }
+ }
+ }
+ return uri.getURI();
+ }
+
+ protected String getScriptBasedFormURL(HttpMethod initmethod, String pathQuery) throws URIException {
+ URI initmethodURI = initmethod.getURI();
+ int queryIndex = pathQuery.indexOf('?');
+ if (queryIndex >= 0) {
+ if (queryIndex > 0) {
+ // update path
+ String newPath = pathQuery.substring(0, queryIndex);
+ if (newPath.startsWith("/")) {
+ // absolute path
+ initmethodURI.setPath(newPath);
+ } else {
+ String currentPath = initmethodURI.getPath();
+ int folderIndex = currentPath.lastIndexOf('/');
+ if (folderIndex >= 0) {
+ // replace relative path
+ initmethodURI.setPath(currentPath.substring(0, folderIndex + 1) + newPath);
+ } else {
+ // should not happen
+ initmethodURI.setPath('/' + newPath);
+ }
+ }
+ }
+ initmethodURI.setQuery(pathQuery.substring(queryIndex + 1));
+ }
+ return initmethodURI.getURI();
+ }
+
+ /**
+ * Try to find logon method path from logon form body.
+ *
+ * @param httpClient httpClient instance
+ * @param initmethod form body http method
+ * @return logon method
+ * @throws IOException on error
+ */
+ protected HttpMethod buildLogonMethod(HttpClient httpClient, HttpMethod initmethod) throws IOException {
+
+ HttpMethod logonMethod = null;
+
+ // create an instance of HtmlCleaner
+ HtmlCleaner cleaner = new HtmlCleaner();
+
+ try {
+ TagNode node = cleaner.clean(initmethod.getResponseBodyAsStream());
+ List forms = node.getElementListByName("form", true);
+ TagNode logonForm = null;
+ // select form
+ if (forms.size() == 1) {
+ logonForm = (TagNode) forms.get(0);
+ } else if (forms.size() > 1) {
+ for (Object form : forms) {
+ if ("logonForm".equals(((TagNode) form).getAttributeByName("name"))) {
+ logonForm = ((TagNode) form);
+ }
+ }
+ }
+ if (logonForm != null) {
+ String logonMethodPath = logonForm.getAttributeByName("action");
+
+ // workaround for broken form with empty action
+ if (logonMethodPath != null && logonMethodPath.length() == 0) {
+ logonMethodPath = "/owa/auth.owa";
+ }
+
+ logonMethod = new PostMethod(getAbsoluteUri(initmethod, logonMethodPath));
+
+ // retrieve lost inputs attached to body
+ List inputList = node.getElementListByName("input", true);
+
+ for (Object input : inputList) {
+ String type = ((TagNode) input).getAttributeByName("type");
+ String name = ((TagNode) input).getAttributeByName("name");
+ String value = ((TagNode) input).getAttributeByName("value");
+ if ("hidden".equalsIgnoreCase(type) && name != null && value != null) {
+ ((PostMethod) logonMethod).addParameter(name, value);
+ }
+ // custom login form
+ if (USER_NAME_FIELDS.contains(name)) {
+ userNameInputs.add(name);
+ } else if (PASSWORD_FIELDS.contains(name)) {
+ passwordInput = name;
+ } else if ("addr".equals(name)) {
+ // this is not a logon form but a redirect form
+ HttpMethod newInitMethod = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, logonMethod);
+ logonMethod = buildLogonMethod(httpClient, newInitMethod);
+ } else if (TOKEN_FIELDS.contains(name)) {
+ // one time password, ask user
+ ((PostMethod) logonMethod).addParameter(name, DavGatewayOTPPrompt.getOneTimePassword());
+ } else if ("otc".equals(name)) {
+ // captcha image, get image and ask user
+ String pinsafeUser = getAliasFromLogin();
+ if (pinsafeUser == null) {
+ pinsafeUser = userName;
+ }
+ GetMethod getMethod = new GetMethod("/PINsafeISAFilter.dll?username=" + pinsafeUser);
+ try {
+ int status = httpClient.executeMethod(getMethod);
+ if (status != HttpStatus.SC_OK) {
+ throw DavGatewayHttpClientFacade.buildHttpException(getMethod);
+ }
+ BufferedImage captchaImage = ImageIO.read(getMethod.getResponseBodyAsStream());
+ ((PostMethod) logonMethod).addParameter(name, DavGatewayOTPPrompt.getCaptchaValue(captchaImage));
+
+ } finally {
+ getMethod.releaseConnection();
+ }
+ }
+ }
+ } else {
+ List frameList = node.getElementListByName("frame", true);
+ if (frameList.size() == 1) {
+ String src = ((TagNode) frameList.get(0)).getAttributeByName("src");
+ if (src != null) {
+ LOGGER.debug("Frames detected in form page, try frame content");
+ initmethod.releaseConnection();
+ HttpMethod newInitMethod = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, src);
+ logonMethod = buildLogonMethod(httpClient, newInitMethod);
+ }
+ } else {
+ // another failover for script based logon forms (Exchange 2007)
+ List scriptList = node.getElementListByName("script", true);
+ for (Object script : scriptList) {
+ List contents = ((TagNode) script).getChildren();
+ for (Object content : contents) {
+ if (content instanceof CommentToken) {
+ String scriptValue = ((CommentToken) content).getCommentedContent();
+ String sUrl = StringUtil.getToken(scriptValue, "var a_sUrl = \"", "\"");
+ String sLgn = StringUtil.getToken(scriptValue, "var a_sLgnQS = \"", "\"");
+ if (sLgn == null) {
+ sLgn = StringUtil.getToken(scriptValue, "var a_sLgn = \"", "\"");
+ }
+ if (sUrl != null && sLgn != null) {
+ String src = getScriptBasedFormURL(initmethod, sLgn + sUrl);
+ LOGGER.debug("Detected script based logon, redirect to form at " + src);
+ HttpMethod newInitMethod = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, src);
+ logonMethod = buildLogonMethod(httpClient, newInitMethod);
+ }
+
+ } else if (content instanceof ContentToken) {
+ // Microsoft Forefront Unified Access Gateway redirect
+ String scriptValue = ((ContentToken) content).getContent();
+ String location = StringUtil.getToken(scriptValue, "window.location.replace(\"", "\"");
+ if (location != null) {
+ LOGGER.debug("Post logon redirect to: " + location);
+ logonMethod = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, location);
+ }
+ }
+ }
+ }
+ }
+ }
+ } catch (IOException e) {
+ LOGGER.error("Error parsing login form at " + initmethod.getURI());
+ } finally {
+ initmethod.releaseConnection();
+ }
+
+ return logonMethod;
+ }
+
+ protected HttpMethod postLogonMethod(HttpClient httpClient, HttpMethod logonMethod, String userName, String password) throws IOException {
+ String userNameInput;
+ if (userNameInputs.size() == 2) {
+ // multiple username fields, split userid|username on |
+ int pipeIndex = userName.indexOf('|');
+ if (pipeIndex < 0) {
+ LOGGER.warn("Multiple user fields detected, please use userid|username as user name in client");
+ } else {
+ String userid = userName.substring(0, pipeIndex);
+ ((PostMethod) logonMethod).removeParameter("userid");
+ ((PostMethod) logonMethod).addParameter("userid", userid);
+ userName = userName.substring(pipeIndex + 1);
+ // adjust credentials
+ this.userName = userName;
+ DavGatewayHttpClientFacade.setCredentials(httpClient, userName, password);
+ }
+
+ userNameInput = "username";
+ } else if (userNameInputs.size() == 1) {
+ // simple username field
+ userNameInput = userNameInputs.get(0);
+ } else {
+ // should not happen
+ userNameInput = "username";
+ }
+ // make sure username and password fields are empty
+ ((PostMethod) logonMethod).removeParameter(userNameInput);
+ ((PostMethod) logonMethod).removeParameter(passwordInput);
+ ((PostMethod) logonMethod).removeParameter("trusted");
+ ((PostMethod) logonMethod).removeParameter("flags");
+ ((PostMethod) logonMethod).addParameter(userNameInput, userName);
+ ((PostMethod) logonMethod).addParameter(passwordInput, password);
+ ((PostMethod) logonMethod).addParameter("trusted", "4");
+ ((PostMethod) logonMethod).addParameter("flags", "4");
+
+ // add exchange 2010 PBack cookie in compatibility mode
+ httpClient.getState().addCookie(new Cookie(httpClient.getHostConfiguration().getHost(), "PBack", "0", "/", null, false));
+
+ logonMethod = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, logonMethod);
+
+ // test form based authentication
+ checkFormLoginQueryString(logonMethod);
+
+ // workaround for post logon script redirect
+ if (!isAuthenticated()) {
+ // try to get new method from script based redirection
+ logonMethod = buildLogonMethod(httpClient, logonMethod);
+
+ if (logonMethod != null) {
+ // if logonMethod is not null, try to follow redirection
+ logonMethod = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, logonMethod);
+ checkFormLoginQueryString(logonMethod);
+ // also check cookies
+ if (!isAuthenticated()) {
+ throwAuthenticationFailed();
+ }
+ } else {
+ // authentication failed
+ throwAuthenticationFailed();
+ }
+ }
+
+ // check for language selection form
+ if (logonMethod != null && "/owa/languageselection.aspx".equals(logonMethod.getPath())) {
+ // need to submit form
+ logonMethod = submitLanguageSelectionForm(logonMethod);
+ }
+ return logonMethod;
+ }
+
+ protected HttpMethod formLogin(HttpClient httpClient, HttpMethod initmethod, String userName, String password) throws IOException {
+ LOGGER.debug("Form based authentication detected");
+
+ HttpMethod logonMethod = buildLogonMethod(httpClient, initmethod);
+ if (logonMethod == null) {
+ LOGGER.debug("Authentication form not found at " + initmethod.getURI() + ", trying default url");
+ logonMethod = new PostMethod("/owa/auth/owaauth.dll");
+ }
+ logonMethod = postLogonMethod(httpClient, logonMethod, userName, password);
+
+ return logonMethod;
+ }
+
+ protected HttpMethod submitLanguageSelectionForm(HttpMethod logonMethod) throws IOException {
+ PostMethod postLanguageFormMethod;
+ // create an instance of HtmlCleaner
+ HtmlCleaner cleaner = new HtmlCleaner();
+
+ try {
+ TagNode node = cleaner.clean(logonMethod.getResponseBodyAsStream());
+ List forms = node.getElementListByName("form", true);
+ TagNode languageForm;
+ // select form
+ if (forms.size() == 1) {
+ languageForm = (TagNode) forms.get(0);
+ } else {
+ throw new IOException("Form not found");
+ }
+ String languageMethodPath = languageForm.getAttributeByName("action");
+
+ postLanguageFormMethod = new PostMethod(getAbsoluteUri(logonMethod, languageMethodPath));
+
+ List inputList = languageForm.getElementListByName("input", true);
+ for (Object input : inputList) {
+ String name = ((TagNode) input).getAttributeByName("name");
+ String value = ((TagNode) input).getAttributeByName("value");
+ if (name != null && value != null) {
+ postLanguageFormMethod.addParameter(name, value);
+ }
+ }
+ List selectList = languageForm.getElementListByName("select", true);
+ for (Object select : selectList) {
+ String name = ((TagNode) select).getAttributeByName("name");
+ List optionList = ((TagNode) select).getElementListByName("option", true);
+ String value = null;
+ for (Object option : optionList) {
+ if (((TagNode) option).getAttributeByName("selected") != null) {
+ value = ((TagNode) option).getAttributeByName("value");
+ break;
+ }
+ }
+ if (name != null && value != null) {
+ postLanguageFormMethod.addParameter(name, value);
+ }
+ }
+ } catch (IOException e) {
+ String errorMessage = "Error parsing language selection form at " + logonMethod.getURI();
+ LOGGER.error(errorMessage);
+ throw new IOException(errorMessage);
+ } finally {
+ logonMethod.releaseConnection();
+ }
+
+ return DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, postLanguageFormMethod);
+ }
+
+ /**
+ * Look for session cookies.
+ *
+ * @return true if session cookies are available
+ */
+ protected boolean isAuthenticated() {
+ boolean authenticated = false;
+ for (Cookie cookie : httpClient.getState().getCookies()) {
+ // Exchange 2003 cookies
+ if (cookie.getName().startsWith("cadata") || "sessionid".equals(cookie.getName())
+ // Exchange 2007 cookie
+ || "UserContext".equals(cookie.getName())
+ // Direct EWS access
+ || "exchangecookie".equals(cookie.getName())
+ ) {
+ authenticated = true;
+ break;
+ }
+ }
+ return authenticated;
+ }
+
+ protected void checkFormLoginQueryString(HttpMethod logonMethod) throws DavMailAuthenticationException {
+ String queryString = logonMethod.getQueryString();
+ if (queryString != null && (queryString.contains("reason=2") || queryString.contains("reason=4"))) {
+ logonMethod.releaseConnection();
+ throwAuthenticationFailed();
+ }
+ }
+
+ protected void throwAuthenticationFailed() throws DavMailAuthenticationException {
+ if (this.userName != null && this.userName.contains("\\")) {
+ throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
+ } else {
+ throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED_RETRY");
+ }
+ }
+
+ protected abstract void buildSessionInfo(HttpMethod method) throws DavMailException;
+
+ /**
+ * Create message in specified folder.
+ * Will overwrite an existing message with same subject in the same folder
+ *
+ * @param folderPath Exchange folder path
+ * @param messageName message name
+ * @param properties message properties (flags)
+ * @param mimeMessage MIME message
+ * @throws IOException when unable to create message
+ */
+ public abstract void createMessage(String folderPath, String messageName, HashMap<String, String> properties, MimeMessage mimeMessage) throws IOException;
+
+ /**
+ * Update given properties on message.
+ *
+ * @param message Exchange message
+ * @param properties Webdav properties map
+ * @throws IOException on error
+ */
+ public abstract void updateMessage(Message message, Map<String, String> properties) throws IOException;
+
+
+ /**
+ * Delete Exchange message.
+ *
+ * @param message Exchange message
+ * @throws IOException on error
+ */
+ public abstract void deleteMessage(Message message) throws IOException;
+
+ /**
+ * Get raw MIME message content
+ *
+ * @param message Exchange message
+ * @return message body
+ * @throws IOException on error
+ */
+ protected abstract byte[] getContent(Message message) throws IOException;
+
+ protected static final Set<String> POP_MESSAGE_ATTRIBUTES = new HashSet<String>();
+
+ static {
+ POP_MESSAGE_ATTRIBUTES.add("uid");
+ POP_MESSAGE_ATTRIBUTES.add("imapUid");
+ POP_MESSAGE_ATTRIBUTES.add("messageSize");
+ }
+
+ /**
+ * Return folder message list with id and size only (for POP3 listener).
+ *
+ * @param folderName Exchange folder name
+ * @return folder message list
+ * @throws IOException on error
+ */
+ public MessageList getAllMessageUidAndSize(String folderName) throws IOException {
+ return searchMessages(folderName, POP_MESSAGE_ATTRIBUTES, null);
+ }
+
+ protected static final Set<String> IMAP_MESSAGE_ATTRIBUTES = new HashSet<String>();
+
+ static {
+ IMAP_MESSAGE_ATTRIBUTES.add("permanenturl");
+ IMAP_MESSAGE_ATTRIBUTES.add("urlcompname");
+ IMAP_MESSAGE_ATTRIBUTES.add("uid");
+ IMAP_MESSAGE_ATTRIBUTES.add("messageSize");
+ IMAP_MESSAGE_ATTRIBUTES.add("imapUid");
+ IMAP_MESSAGE_ATTRIBUTES.add("junk");
+ IMAP_MESSAGE_ATTRIBUTES.add("flagStatus");
+ IMAP_MESSAGE_ATTRIBUTES.add("messageFlags");
+ IMAP_MESSAGE_ATTRIBUTES.add("lastVerbExecuted");
+ IMAP_MESSAGE_ATTRIBUTES.add("read");
+ IMAP_MESSAGE_ATTRIBUTES.add("deleted");
+ IMAP_MESSAGE_ATTRIBUTES.add("date");
+ IMAP_MESSAGE_ATTRIBUTES.add("lastmodified");
+ // OSX IMAP requests content-class
+ IMAP_MESSAGE_ATTRIBUTES.add("contentclass");
+ }
+
+ protected static final Set<String> UID_MESSAGE_ATTRIBUTES = new HashSet<String>();
+
+ static {
+ UID_MESSAGE_ATTRIBUTES.add("uid");
+ }
+
+ /**
+ * Get all folder messages.
+ *
+ * @param folderPath Exchange folder name
+ * @return message list
+ * @throws IOException on error
+ */
+ public MessageList searchMessages(String folderPath) throws IOException {
+ return searchMessages(folderPath, IMAP_MESSAGE_ATTRIBUTES, null);
+ }
+
+ /**
+ * Search folder for messages matching conditions, with attributes needed by IMAP listener.
+ *
+ * @param folderName Exchange folder name
+ * @param condition search filter
+ * @return message list
+ * @throws IOException on error
+ */
+ public MessageList searchMessages(String folderName, Condition condition) throws IOException {
+ return searchMessages(folderName, IMAP_MESSAGE_ATTRIBUTES, condition);
+ }
+
+ /**
+ * Search folder for messages matching conditions, with given attributes.
+ *
+ * @param folderName Exchange folder name
+ * @param attributes requested Webdav attributes
+ * @param condition search filter
+ * @return message list
+ * @throws IOException on error
+ */
+ public abstract MessageList searchMessages(String folderName, Set<String> attributes, Condition condition) throws IOException;
+
+ /**
+ * Get server version (Exchange2003, Exchange2007 or Exchange2010)
+ *
+ * @return server version
+ */
+ public String getServerVersion() {
+ return serverVersion;
+ }
+
+ @SuppressWarnings({"JavaDoc"})
+ public enum Operator {
+ Or, And, Not, IsEqualTo,
+ IsGreaterThan, IsGreaterThanOrEqualTo,
+ IsLessThan, IsLessThanOrEqualTo,
+ IsNull, IsTrue, IsFalse,
+ Like, StartsWith, Contains
+ }
+
+ /**
+ * Exchange search filter.
+ */
+ public static interface Condition {
+ /**
+ * Append condition to buffer.
+ *
+ * @param buffer search filter buffer
+ */
+ void appendTo(StringBuilder buffer);
+
+ /**
+ * True if condition is empty.
+ *
+ * @return true if condition is empty
+ */
+ boolean isEmpty();
+
+ /**
+ * Test if the contact matches current condition.
+ *
+ * @param contact Exchange Contact
+ * @return true if contact matches condition
+ */
+ boolean isMatch(ExchangeSession.Contact contact);
+ }
+
+ /**
+ * Attribute condition.
+ */
+ public abstract static class AttributeCondition implements Condition {
+ protected final String attributeName;
+ protected final Operator operator;
+ protected final String value;
+
+ protected AttributeCondition(String attributeName, Operator operator, String value) {
+ this.attributeName = attributeName;
+ this.operator = operator;
+ this.value = value;
+ }
+
+ public boolean isEmpty() {
+ return false;
+ }
+
+ /**
+ * Get attribute name.
+ *
+ * @return attribute name
+ */
+ public String getAttributeName() {
+ return attributeName;
+ }
+
+ /**
+ * Condition value.
+ *
+ * @return value
+ */
+ public String getValue() {
+ return value;
+ }
+
+ }
+
+ /**
+ * Multiple condition.
+ */
+ public abstract static class MultiCondition implements Condition {
+ protected final Operator operator;
+ protected final List<Condition> conditions;
+
+ protected MultiCondition(Operator operator, Condition... conditions) {
+ this.operator = operator;
+ this.conditions = new ArrayList<Condition>();
+ for (Condition condition : conditions) {
+ if (condition != null) {
+ this.conditions.add(condition);
+ }
+ }
+ }
+
+ /**
+ * Conditions list.
+ *
+ * @return conditions
+ */
+ public List<Condition> getConditions() {
+ return conditions;
+ }
+
+ /**
+ * Condition operator.
+ *
+ * @return operator
+ */
+ public Operator getOperator() {
+ return operator;
+ }
+
+ /**
+ * Add a new condition.
+ *
+ * @param condition single condition
+ */
+ public void add(Condition condition) {
+ if (condition != null) {
+ conditions.add(condition);
+ }
+ }
+
+ public boolean isEmpty() {
+ boolean isEmpty = true;
+ for (Condition condition : conditions) {
+ if (!condition.isEmpty()) {
+ isEmpty = false;
+ break;
+ }
+ }
+ return isEmpty;
+ }
+
+ public boolean isMatch(ExchangeSession.Contact contact) {
+ if (operator == Operator.And) {
+ for (Condition condition : conditions) {
+ if (!condition.isMatch(contact)) {
+ return false;
+ }
+ }
+ return true;
+ } else if (operator == Operator.Or) {
+ for (Condition condition : conditions) {
+ if (condition.isMatch(contact)) {
+ return true;
+ }
+ }
+ return false;
+ } else {
+ return false;
+ }
+ }
+
+ }
+
+ /**
+ * Not condition.
+ */
+ public abstract static class NotCondition implements Condition {
+ protected final Condition condition;
+
+ protected NotCondition(Condition condition) {
+ this.condition = condition;
+ }
+
+ public boolean isEmpty() {
+ return condition.isEmpty();
+ }
+
+ public boolean isMatch(ExchangeSession.Contact contact) {
+ return !condition.isMatch(contact);
+ }
+ }
+
+ /**
+ * Single search filter condition.
+ */
+ public abstract static class MonoCondition implements Condition {
+ protected final String attributeName;
+ protected final Operator operator;
+
+ protected MonoCondition(String attributeName, Operator operator) {
+ this.attributeName = attributeName;
+ this.operator = operator;
+ }
+
+ public boolean isEmpty() {
+ return false;
+ }
+
+ public boolean isMatch(ExchangeSession.Contact contact) {
+ String actualValue = contact.get(attributeName);
+ return (operator == Operator.IsNull && actualValue == null) ||
+ (operator == Operator.IsFalse && "false".equals(actualValue)) ||
+ (operator == Operator.IsTrue && "true".equals(actualValue));
+ }
+ }
+
+ /**
+ * And search filter.
+ *
+ * @param condition search conditions
+ * @return condition
+ */
+ public abstract MultiCondition and(Condition... condition);
+
+ /**
+ * Or search filter.
+ *
+ * @param condition search conditions
+ * @return condition
+ */
+ public abstract MultiCondition or(Condition... condition);
+
+ /**
+ * Not search filter.
+ *
+ * @param condition search condition
+ * @return condition
+ */
+ public abstract Condition not(Condition condition);
+
+ /**
+ * Equals condition.
+ *
+ * @param attributeName logical Exchange attribute name
+ * @param value attribute value
+ * @return condition
+ */
+ public abstract Condition isEqualTo(String attributeName, String value);
+
+ /**
+ * Equals condition.
+ *
+ * @param attributeName logical Exchange attribute name
+ * @param value attribute value
+ * @return condition
+ */
+ public abstract Condition isEqualTo(String attributeName, int value);
+
+ /**
+ * MIME header equals condition.
+ *
+ * @param headerName MIME header name
+ * @param value attribute value
+ * @return condition
+ */
+ public abstract Condition headerIsEqualTo(String headerName, String value);
+
+ /**
+ * Greater than or equals condition.
+ *
+ * @param attributeName logical Exchange attribute name
+ * @param value attribute value
+ * @return condition
+ */
+ public abstract Condition gte(String attributeName, String value);
+
+ /**
+ * Greater than condition.
+ *
+ * @param attributeName logical Exchange attribute name
+ * @param value attribute value
+ * @return condition
+ */
+ public abstract Condition gt(String attributeName, String value);
+
+ /**
+ * Lower than condition.
+ *
+ * @param attributeName logical Exchange attribute name
+ * @param value attribute value
+ * @return condition
+ */
+ public abstract Condition lt(String attributeName, String value);
+
+ /**
+ * Lower than or equals condition.
+ *
+ * @param attributeName logical Exchange attribute name
+ * @param value attribute value
+ * @return condition
+ */
+ @SuppressWarnings({"UnusedDeclaration"})
+ public abstract Condition lte(String attributeName, String value);
+
+ /**
+ * Contains condition.
+ *
+ * @param attributeName logical Exchange attribute name
+ * @param value attribute value
+ * @return condition
+ */
+ public abstract Condition contains(String attributeName, String value);
+
+ /**
+ * Starts with condition.
+ *
+ * @param attributeName logical Exchange attribute name
+ * @param value attribute value
+ * @return condition
+ */
+ public abstract Condition startsWith(String attributeName, String value);
+
+ /**
+ * Is null condition.
+ *
+ * @param attributeName logical Exchange attribute name
+ * @return condition
+ */
+ public abstract Condition isNull(String attributeName);
+
+ /**
+ * Is true condition.
+ *
+ * @param attributeName logical Exchange attribute name
+ * @return condition
+ */
+ public abstract Condition isTrue(String attributeName);
+
+ /**
+ * Is false condition.
+ *
+ * @param attributeName logical Exchange attribute name
+ * @return condition
+ */
+ public abstract Condition isFalse(String attributeName);
+
+ /**
+ * Search mail and generic folders under given folder.
+ * Exclude calendar and contacts folders
+ *
+ * @param folderName Exchange folder name
+ * @param recursive deep search if true
+ * @return list of folders
+ * @throws IOException on error
+ */
+ public List<Folder> getSubFolders(String folderName, boolean recursive) throws IOException {
+ Condition folderCondition = null;
+ if (!Settings.getBooleanProperty("davmail.imapIncludeSpecialFolders", false)) {
+ folderCondition = or(isEqualTo("folderclass", "IPF.Note"), isNull("folderclass"));
+ }
+ List<Folder> results = getSubFolders(folderName, folderCondition,
+ recursive);
+ // need to include base folder in recursive search, except on root
+ if (recursive && folderName.length() > 0) {
+ results.add(getFolder(folderName));
+ }
+
+ return results;
+ }
+
+ /**
+ * Search calendar folders under given folder.
+ *
+ * @param folderName Exchange folder name
+ * @param recursive deep search if true
+ * @return list of folders
+ * @throws IOException on error
+ */
+ public List<Folder> getSubCalendarFolders(String folderName, boolean recursive) throws IOException {
+ return getSubFolders(folderName, isEqualTo("folderclass", "IPF.Appointment"), recursive);
+ }
+
+ /**
+ * Search folders under given folder matching filter.
+ *
+ * @param folderName Exchange folder name
+ * @param condition search filter
+ * @param recursive deep search if true
+ * @return list of folders
+ * @throws IOException on error
+ */
+ public abstract List<Folder> getSubFolders(String folderName, Condition condition, boolean recursive) throws IOException;
+
+ /**
+ * Delete oldest messages in trash.
+ * keepDelay is the number of days to keep messages in trash before delete
+ *
+ * @throws IOException when unable to purge messages
+ */
+ public void purgeOldestTrashAndSentMessages() throws IOException {
+ int keepDelay = Settings.getIntProperty("davmail.keepDelay");
+ if (keepDelay != 0) {
+ purgeOldestFolderMessages(TRASH, keepDelay);
+ }
+ // this is a new feature, default is : do nothing
+ int sentKeepDelay = Settings.getIntProperty("davmail.sentKeepDelay");
+ if (sentKeepDelay != 0) {
+ purgeOldestFolderMessages(SENT, sentKeepDelay);
+ }
+ }
+
+ protected void purgeOldestFolderMessages(String folderPath, int keepDelay) throws IOException {
+ Calendar cal = Calendar.getInstance();
+ cal.add(Calendar.DAY_OF_MONTH, -keepDelay);
+ LOGGER.debug("Delete messages in " + folderPath + " not modified since " + cal.getTime());
+
+ MessageList messages = searchMessages(folderPath, UID_MESSAGE_ATTRIBUTES,
+ lt("lastmodified", formatSearchDate(cal.getTime())));
+
+ for (Message message : messages) {
+ message.delete();
+ }
+ }
+
+ protected void convertResentHeader(MimeMessage mimeMessage, String headerName) throws MessagingException {
+ String[] resentHeader = mimeMessage.getHeader("Resent-" + headerName);
+ if (resentHeader != null) {
+ mimeMessage.removeHeader("Resent-" + headerName);
+ mimeMessage.removeHeader(headerName);
+ for (String value : resentHeader) {
+ mimeMessage.addHeader(headerName, value);
+ }
+ }
+ }
+
+ protected String lastSentMessageId;
+
+ /**
+ * Send message in reader to recipients.
+ * Detect visible recipients in message body to determine bcc recipients
+ *
+ * @param rcptToRecipients recipients list
+ * @param mimeMessage mime message
+ * @throws IOException on error
+ * @throws MessagingException on error
+ */
+ public void sendMessage(List<String> rcptToRecipients, MimeMessage mimeMessage) throws IOException, MessagingException {
+ // detect duplicate send command
+ String messageId = mimeMessage.getMessageID();
+ if (lastSentMessageId != null && lastSentMessageId.equals(messageId)) {
+ LOGGER.debug("Dropping message id " + messageId + ": already sent");
+ return;
+ }
+ lastSentMessageId = messageId;
+
+ convertResentHeader(mimeMessage, "From");
+ convertResentHeader(mimeMessage, "To");
+ convertResentHeader(mimeMessage, "Cc");
+ convertResentHeader(mimeMessage, "Bcc");
+ convertResentHeader(mimeMessage, "Message-Id");
+
+ mimeMessage.removeHeader("From");
+
+ // remove visible recipients from list
+ Set<String> visibleRecipients = new HashSet<String>();
+ List<InternetAddress> recipients = getAllRecipients(mimeMessage);
+ for (InternetAddress address : recipients) {
+ visibleRecipients.add((address.getAddress().toLowerCase()));
+ }
+ for (String recipient : rcptToRecipients) {
+ if (!visibleRecipients.contains(recipient.toLowerCase())) {
+ mimeMessage.addRecipient(javax.mail.Message.RecipientType.BCC, new InternetAddress(recipient));
+ }
+ }
+ sendMessage(mimeMessage);
+
+ }
+
+ static final String[] RECIPIENT_HEADERS = {"to", "cc", "bcc"};
+
+ protected List<InternetAddress> getAllRecipients(MimeMessage mimeMessage) throws MessagingException {
+ List<InternetAddress> recipientList = new ArrayList<InternetAddress>();
+ for (String recipientHeader : RECIPIENT_HEADERS) {
+ final String recipientHeaderValue = mimeMessage.getHeader(recipientHeader, ",");
+ if (recipientHeaderValue != null) {
+ // parse headers in non strict mode
+ recipientList.addAll(Arrays.asList(InternetAddress.parseHeader(recipientHeaderValue, false)));
+ }
+
+ }
+ return recipientList;
+ }
+
+ /**
+ * Send Mime message.
+ *
+ * @param mimeMessage MIME message
+ * @throws IOException on error
+ * @throws MessagingException on error
+ */
+ public abstract void sendMessage(MimeMessage mimeMessage) throws IOException, MessagingException;
+
+ /**
+ * Get folder object.
+ * Folder name can be logical names INBOX, Drafts, Trash or calendar,
+ * or a path relative to user base folder or absolute path.
+ *
+ * @param folderPath folder path
+ * @return Folder object
+ * @throws IOException on error
+ */
+ public ExchangeSession.Folder getFolder(String folderPath) throws IOException {
+ Folder folder = internalGetFolder(folderPath);
+ if (isMainCalendar(folderPath)) {
+ Folder taskFolder = internalGetFolder(TASKS);
+ folder.ctag += taskFolder.ctag;
+ }
+ return folder;
+ }
+
+ protected abstract Folder internalGetFolder(String folderName) throws IOException;
+
+ /**
+ * Check folder ctag and reload messages as needed.
+ *
+ * @param currentFolder current folder
+ * @return true if folder changed
+ * @throws IOException on error
+ */
+ public boolean refreshFolder(Folder currentFolder) throws IOException {
+ Folder newFolder = getFolder(currentFolder.folderPath);
+ if (currentFolder.ctag == null || !currentFolder.ctag.equals(newFolder.ctag)) {
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("Contenttag changed on " + currentFolder.folderPath + ' '
+ + currentFolder.ctag + " => " + newFolder.ctag + ", reloading messages");
+ }
+ currentFolder.hasChildren = newFolder.hasChildren;
+ currentFolder.noInferiors = newFolder.noInferiors;
+ currentFolder.unreadCount = newFolder.unreadCount;
+ currentFolder.ctag = newFolder.ctag;
+ currentFolder.etag = newFolder.etag;
+ currentFolder.loadMessages();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Create Exchange message folder.
+ *
+ * @param folderName logical folder name
+ * @return status
+ * @throws IOException on error
+ */
+ public int createMessageFolder(String folderName) throws IOException {
+ return createFolder(folderName, "IPF.Note", null);
+ }
+
+ /**
+ * Create Exchange calendar folder.
+ *
+ * @param folderName logical folder name
+ * @param properties folder properties
+ * @return status
+ * @throws IOException on error
+ */
+ public int createCalendarFolder(String folderName, Map<String, String> properties) throws IOException {
+ return createFolder(folderName, "IPF.Appointment", properties);
+ }
+
+ /**
+ * Create Exchange contact folder.
+ *
+ * @param folderName logical folder name
+ * @param properties folder properties
+ * @return status
+ * @throws IOException on error
+ */
+ public int createContactFolder(String folderName, Map<String, String> properties) throws IOException {
+ return createFolder(folderName, "IPF.Contact", properties);
+ }
+
+ /**
+ * Create Exchange folder with given folder class.
+ *
+ * @param folderName logical folder name
+ * @param folderClass folder class
+ * @param properties folder properties
+ * @return status
+ * @throws IOException on error
+ */
+ public abstract int createFolder(String folderName, String folderClass, Map<String, String> properties) throws IOException;
+
+ /**
+ * Update Exchange folder properties.
+ *
+ * @param folderName logical folder name
+ * @param properties folder properties
+ * @return status
+ * @throws IOException on error
+ */
+ public abstract int updateFolder(String folderName, Map<String, String> properties) throws IOException;
+
+ /**
+ * Delete Exchange folder.
+ *
+ * @param folderName logical folder name
+ * @throws IOException on error
+ */
+ public abstract void deleteFolder(String folderName) throws IOException;
+
+ /**
+ * Copy message to target folder
+ *
+ * @param message Exchange message
+ * @param targetFolder target folder
+ * @throws IOException on error
+ */
+ public abstract void copyMessage(Message message, String targetFolder) throws IOException;
+
+ /**
+ * Move message to target folder
+ *
+ * @param message Exchange message
+ * @param targetFolder target folder
+ * @throws IOException on error
+ */
+ public abstract void moveMessage(Message message, String targetFolder) throws IOException;
+
+ /**
+ * Move folder to target name.
+ *
+ * @param folderName current folder name/path
+ * @param targetName target folder name/path
+ * @throws IOException on error
+ */
+ public abstract void moveFolder(String folderName, String targetName) throws IOException;
+
+ /**
+ * Move item from source path to target path.
+ *
+ * @param sourcePath item source path
+ * @param targetPath item target path
+ * @throws IOException on error
+ */
+ public abstract void moveItem(String sourcePath, String targetPath) throws IOException;
+
+ protected abstract void moveToTrash(Message message) throws IOException;
+
+ /**
+ * Exchange folder with IMAP properties
+ */
+ public class Folder {
+ /**
+ * Logical (IMAP) folder path.
+ */
+ public String folderPath;
+
+ /**
+ * Display Name.
+ */
+ public String displayName;
+ /**
+ * Folder class (PR_CONTAINER_CLASS).
+ */
+ public String folderClass;
+ /**
+ * Folder unread message count.
+ */
+ public int unreadCount;
+ /**
+ * true if folder has subfolders (DAV:hassubs).
+ */
+ public boolean hasChildren;
+ /**
+ * true if folder has no subfolders (DAV:nosubs).
+ */
+ public boolean noInferiors;
+ /**
+ * Folder content tag (to detect folder content changes).
+ */
+ public String ctag;
+ /**
+ * Folder etag (to detect folder object changes).
+ */
+ public String etag;
+ /**
+ * Next IMAP uid
+ */
+ public int uidNext;
+ /**
+ * recent count
+ */
+ public int recent;
+
+ /**
+ * Folder message list, empty before loadMessages call.
+ */
+ public ExchangeSession.MessageList messages;
+ /**
+ * Permanent uid (PR_SEARCH_KEY) to IMAP UID map.
+ */
+ private final HashMap<String, Long> permanentUrlToImapUidMap = new HashMap<String, Long>();
+
+ /**
+ * Get IMAP folder flags.
+ *
+ * @return folder flags in IMAP format
+ */
+ public String getFlags() {
+ if (noInferiors) {
+ return "\\NoInferiors";
+ } else if (hasChildren) {
+ return "\\HasChildren";
+ } else {
+ return "\\HasNoChildren";
+ }
+ }
+
+ /**
+ * Load folder messages.
+ *
+ * @throws IOException on error
+ */
+ public void loadMessages() throws IOException {
+ messages = ExchangeSession.this.searchMessages(folderPath, null);
+ fixUids(messages);
+ recent = 0;
+ for (Message message : messages) {
+ if (message.recent) {
+ recent++;
+ }
+ }
+ }
+
+ /**
+ * Search messages in folder matching query.
+ *
+ * @param condition search query
+ * @return message list
+ * @throws IOException on error
+ */
+ public MessageList searchMessages(Condition condition) throws IOException {
+ MessageList localMessages = ExchangeSession.this.searchMessages(folderPath, condition);
+ fixUids(localMessages);
+ return localMessages;
+ }
+
+ /**
+ * Restore previous uids changed by a PROPPATCH (flag change).
+ *
+ * @param messages message list
+ */
+ protected void fixUids(MessageList messages) {
+ boolean sortNeeded = false;
+ for (Message message : messages) {
+ if (permanentUrlToImapUidMap.containsKey(message.getPermanentId())) {
+ long previousUid = permanentUrlToImapUidMap.get(message.getPermanentId());
+ if (message.getImapUid() != previousUid) {
+ LOGGER.debug("Restoring IMAP uid " + message.getImapUid() + " -> " + previousUid + " for message " + message.getPermanentId());
+ message.setImapUid(previousUid);
+ sortNeeded = true;
+ }
+ } else {
+ // add message to uid map
+ permanentUrlToImapUidMap.put(message.getPermanentId(), message.getImapUid());
+ }
+ }
+ if (sortNeeded) {
+ Collections.sort(messages);
+ }
+ }
+
+ /**
+ * Folder message count.
+ *
+ * @return message count
+ */
+ public int count() {
+ return messages.size();
+ }
+
+ /**
+ * Compute IMAP uidnext.
+ *
+ * @return max(messageuids)+1
+ */
+ public long getUidNext() {
+ return messages.get(messages.size() - 1).getImapUid() + 1;
+ }
+
+ /**
+ * Get message at index.
+ *
+ * @param index message index
+ * @return message
+ */
+ public Message get(int index) {
+ return messages.get(index);
+ }
+
+ /**
+ * Get current folder messages imap uids
+ *
+ * @return imap uid list
+ */
+ public List<Long> getImapUidList() {
+ ArrayList<Long> imapUidList = new ArrayList<Long>();
+ for (ExchangeSession.Message message : messages) {
+ imapUidList.add(message.getImapUid());
+ }
+ return imapUidList;
+ }
+
+ /**
+ * Calendar folder flag.
+ *
+ * @return true if this is a calendar folder
+ */
+ public boolean isCalendar() {
+ return "IPF.Appointment".equals(folderClass);
+ }
+
+ /**
+ * Contact folder flag.
+ *
+ * @return true if this is a calendar folder
+ */
+ public boolean isContact() {
+ return "IPF.Contact".equals(folderClass);
+ }
+
+ /**
+ * Task folder flag.
+ *
+ * @return true if this is a task folder
+ */
+ public boolean isTask() {
+ return "IPF.Task".equals(folderClass);
+ }
+
+ /**
+ * drop cached message
+ */
+ public void clearCache() {
+ messages.cachedMimeBody = null;
+ messages.cachedMimeMessage = null;
+ messages.cachedMessageImapUid = 0;
+ }
+ }
+
+ /**
+ * Exchange message.
+ */
+ public abstract class Message implements Comparable<Message> {
+ /**
+ * enclosing message list
+ */
+ public MessageList messageList;
+ /**
+ * Message url.
+ */
+ public String messageUrl;
+ /**
+ * Message permanent url (does not change on message move).
+ */
+ public String permanentUrl;
+ /**
+ * Message uid.
+ */
+ public String uid;
+ /**
+ * Message content class.
+ */
+ public String contentClass;
+ /**
+ * Message IMAP uid, unique in folder (x0e230003).
+ */
+ public long imapUid;
+ /**
+ * MAPI message size.
+ */
+ public int size;
+ /**
+ * Message date (urn:schemas:mailheader:date).
+ */
+ public String date;
+
+ /**
+ * Message flag: read.
+ */
+ public boolean read;
+ /**
+ * Message flag: deleted.
+ */
+ public boolean deleted;
+ /**
+ * Message flag: junk.
+ */
+ public boolean junk;
+ /**
+ * Message flag: flagged.
+ */
+ public boolean flagged;
+ /**
+ * Message flag: recent.
+ */
+ public boolean recent;
+ /**
+ * Message flag: draft.
+ */
+ public boolean draft;
+ /**
+ * Message flag: answered.
+ */
+ public boolean answered;
+ /**
+ * Message flag: fowarded.
+ */
+ public boolean forwarded;
+
+ /**
+ * Unparsed message content.
+ */
+ protected SharedByteArrayInputStream mimeBody;
+
+ /**
+ * Message content parsed in a MIME message.
+ */
+ protected MimeMessage mimeMessage;
+
+ /**
+ * Get permanent message id.
+ * permanentUrl over WebDav or IitemId over EWS
+ *
+ * @return permanent id
+ */
+ public abstract String getPermanentId();
+
+ /**
+ * IMAP uid , unique in folder (x0e230003)
+ *
+ * @return IMAP uid
+ */
+ public long getImapUid() {
+ return imapUid;
+ }
+
+ /**
+ * Set IMAP uid.
+ *
+ * @param imapUid new uid
+ */
+ public void setImapUid(long imapUid) {
+ this.imapUid = imapUid;
+ }
+
+ /**
+ * Exchange uid.
+ *
+ * @return uid
+ */
+ public String getUid() {
+ return uid;
+ }
+
+ /**
+ * Return message flags in IMAP format.
+ *
+ * @return IMAP flags
+ */
+ public String getImapFlags() {
+ StringBuilder buffer = new StringBuilder();
+ if (read) {
+ buffer.append("\\Seen ");
+ }
+ if (deleted) {
+ buffer.append("\\Deleted ");
+ }
+ if (recent) {
+ buffer.append("\\Recent ");
+ }
+ if (flagged) {
+ buffer.append("\\Flagged ");
+ }
+ if (junk) {
+ buffer.append("Junk ");
+ }
+ if (draft) {
+ buffer.append("\\Draft ");
+ }
+ if (answered) {
+ buffer.append("\\Answered ");
+ }
+ if (forwarded) {
+ buffer.append("$Forwarded ");
+ }
+ return buffer.toString().trim();
+ }
+
+ /**
+ * Load message content in a Mime message
+ *
+ * @throws IOException on error
+ * @throws MessagingException on error
+ */
+ protected void loadMimeMessage() throws IOException, MessagingException {
+ if (mimeMessage == null) {
+ // try to get message content from cache
+ if (this.imapUid == messageList.cachedMessageImapUid) {
+ mimeBody = messageList.cachedMimeBody;
+ mimeMessage = messageList.cachedMimeMessage;
+ LOGGER.debug("Got message content for " + imapUid + " from cache");
+ } else {
+ // load and parse message
+ mimeBody = new SharedByteArrayInputStream(getContent(this));
+ mimeMessage = new MimeMessage(null, mimeBody);
+ mimeBody.reset();
+ // workaround for Exchange 2003 ActiveSync bug
+ if (mimeMessage.getHeader("MAIL FROM") != null) {
+ mimeBody = (SharedByteArrayInputStream)mimeMessage.getRawInputStream();
+ mimeMessage = new MimeMessage(null, mimeBody);
+ mimeBody.reset();
+ }
+ LOGGER.debug("Downloaded full message content for IMAP UID " + imapUid + " (" + mimeBody.available() + " bytes)");
+ }
+ }
+ }
+
+ /**
+ * Get message content as a Mime message.
+ *
+ * @return mime message
+ * @throws IOException on error
+ * @throws MessagingException on error
+ */
+ public MimeMessage getMimeMessage() throws IOException, MessagingException {
+ loadMimeMessage();
+ return mimeMessage;
+ }
+
+ public Enumeration getMatchingHeaderLines(String[] headerNames) throws MessagingException, IOException {
+ Enumeration result = null;
+ if (mimeMessage == null) {
+ // message not loaded, try to get headers only
+ InputStream headers = getMimeHeaders();
+ if (headers != null) {
+ result = new InternetHeaders(headers).getMatchingHeaderLines(headerNames);
+ }
+ }
+ if (result == null) {
+ result = getMimeMessage().getMatchingHeaderLines(headerNames);
+ }
+ return result;
+ }
+
+ protected abstract InputStream getMimeHeaders();
+
+ /**
+ * Get message body size.
+ *
+ * @return mime message size
+ * @throws IOException on error
+ * @throws MessagingException on error
+ */
+ public int getMimeMessageSize() throws IOException, MessagingException {
+ loadMimeMessage();
+ mimeBody.reset();
+ return mimeBody.available();
+ }
+
+ /**
+ * Get message body input stream.
+ *
+ * @return mime message InputStream
+ * @throws IOException on error
+ * @throws MessagingException on error
+ */
+ public InputStream getRawInputStream() throws IOException, MessagingException {
+ loadMimeMessage();
+ mimeBody.reset();
+ return mimeBody;
+ }
+
+
+ /**
+ * Drop mime message to avoid keeping message content in memory,
+ * keep a single message in MessageList cache to handle chunked fetch.
+ */
+ public void dropMimeMessage() {
+ // update single message cache
+ if (mimeMessage != null) {
+ messageList.cachedMessageImapUid = imapUid;
+ messageList.cachedMimeBody = mimeBody;
+ messageList.cachedMimeMessage = mimeMessage;
+ }
+ // drop curent message body to save memory
+ mimeMessage = null;
+ mimeBody = null;
+ }
+
+ /**
+ * Delete message.
+ *
+ * @throws IOException on error
+ */
+ public void delete() throws IOException {
+ deleteMessage(this);
+ }
+
+ /**
+ * Move message to trash, mark message read.
+ *
+ * @throws IOException on error
+ */
+ public void moveToTrash() throws IOException {
+ markRead();
+
+ ExchangeSession.this.moveToTrash(this);
+ }
+
+ /**
+ * Mark message as read.
+ *
+ * @throws IOException on error
+ */
+ public void markRead() throws IOException {
+ HashMap<String, String> properties = new HashMap<String, String>();
+ properties.put("read", "1");
+ updateMessage(this, properties);
+ }
+
+ /**
+ * Comparator to sort messages by IMAP uid
+ *
+ * @param message other message
+ * @return imapUid comparison result
+ */
+ public int compareTo(Message message) {
+ long compareValue = (imapUid - message.imapUid);
+ if (compareValue > 0) {
+ return 1;
+ } else if (compareValue < 0) {
+ return -1;
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Override equals, compare IMAP uids
+ *
+ * @param message other message
+ * @return true if IMAP uids are equal
+ */
+ @Override
+ public boolean equals(Object message) {
+ return message instanceof Message && imapUid == ((Message) message).imapUid;
+ }
+
+ /**
+ * Override hashCode, return imapUid hashcode.
+ *
+ * @return imapUid hashcode
+ */
+ @Override
+ public int hashCode() {
+ return (int) (imapUid ^ (imapUid >>> 32));
+ }
+ }
+
+ /**
+ * Message list, includes a single messsage cache
+ */
+ public static class MessageList extends ArrayList<Message> {
+ /**
+ * Cached message content parsed in a MIME message.
+ */
+ protected transient MimeMessage cachedMimeMessage;
+ /**
+ * Cached message uid.
+ */
+ protected transient long cachedMessageImapUid;
+ /**
+ * Cached unparsed message
+ */
+ protected transient SharedByteArrayInputStream cachedMimeBody;
+
+ }
+
+ /**
+ * Generic folder item.
+ */
+ public abstract static class Item extends HashMap<String, String> {
+ protected String folderPath;
+ protected String itemName;
+ protected String permanentUrl;
+ /**
+ * Display name.
+ */
+ public String displayName;
+ /**
+ * item etag
+ */
+ public String etag;
+ protected String noneMatch;
+
+ /**
+ * Build item instance.
+ *
+ * @param folderPath folder path
+ * @param itemName item name class
+ * @param etag item etag
+ * @param noneMatch none match flag
+ */
+ public Item(String folderPath, String itemName, String etag, String noneMatch) {
+ this.folderPath = folderPath;
+ this.itemName = itemName;
+ this.etag = etag;
+ this.noneMatch = noneMatch;
+ }
+
+ /**
+ * Default constructor.
+ */
+ protected Item() {
+ }
+
+ /**
+ * Return item content type
+ *
+ * @return content type
+ */
+ public abstract String getContentType();
+
+ /**
+ * Retrieve item body from Exchange
+ *
+ * @return item body
+ * @throws HttpException on error
+ */
+ public abstract String getBody() throws IOException;
+
+ /**
+ * Get event name (file name part in URL).
+ *
+ * @return event name
+ */
+ public String getName() {
+ return itemName;
+ }
+
+ /**
+ * Get event etag (last change tag).
+ *
+ * @return event etag
+ */
+ public String getEtag() {
+ return etag;
+ }
+
+ /**
+ * Set item href.
+ *
+ * @param href item href
+ */
+ public void setHref(String href) {
+ int index = href.lastIndexOf('/');
+ if (index >= 0) {
+ folderPath = href.substring(0, index);
+ itemName = href.substring(index + 1);
+ } else {
+ throw new IllegalArgumentException(href);
+ }
+ }
+
+ /**
+ * Return item href.
+ *
+ * @return item href
+ */
+ public String getHref() {
+ return folderPath + '/' + itemName;
+ }
+ }
+
+ /**
+ * Contact object
+ */
+ public abstract class Contact extends Item {
+
+ /**
+ * @inheritDoc
+ */
+ public Contact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) {
+ super(folderPath, itemName.endsWith(".vcf") ? itemName.substring(0, itemName.length() - 3) + "EML" : itemName, etag, noneMatch);
+ this.putAll(properties);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected Contact() {
+ }
+
+ /**
+ * Convert EML extension to vcf.
+ *
+ * @return item name
+ */
+ @Override
+ public String getName() {
+ String name = super.getName();
+ if (name.endsWith(".EML")) {
+ name = name.substring(0, name.length() - 3) + "vcf";
+ }
+ return name;
+ }
+
+ /**
+ * Set contact name
+ *
+ * @param name contact name
+ */
+ public void setName(String name) {
+ this.itemName = name;
+ }
+
+ /**
+ * Compute vcard uid from name.
+ *
+ * @return uid
+ * @throws URIException on error
+ */
+ protected String getUid() throws URIException {
+ String uid = getName();
+ int dotIndex = uid.lastIndexOf('.');
+ if (dotIndex > 0) {
+ uid = uid.substring(0, dotIndex);
+ }
+ return URIUtil.encodePath(uid);
+ }
+
+ @Override
+ public String getContentType() {
+ return "text/vcard";
+ }
+
+
+ @Override
+ public String getBody() throws HttpException {
+ // build RFC 2426 VCard from contact information
+ VCardWriter writer = new VCardWriter();
+ writer.startCard();
+ writer.appendProperty("UID", getUid());
+ // common name
+ writer.appendProperty("FN", get("cn"));
+ // RFC 2426: Family Name, Given Name, Additional Names, Honorific Prefixes, and Honorific Suffixes
+ writer.appendProperty("N", get("sn"), get("givenName"), get("middlename"), get("personaltitle"), get("namesuffix"));
+
+ writer.appendProperty("TEL;TYPE=cell", get("mobile"));
+ writer.appendProperty("TEL;TYPE=work", get("telephoneNumber"));
+ writer.appendProperty("TEL;TYPE=home", get("homePhone"));
+ writer.appendProperty("TEL;TYPE=fax", get("facsimiletelephonenumber"));
+ writer.appendProperty("TEL;TYPE=pager", get("pager"));
+ writer.appendProperty("TEL;TYPE=car", get("othermobile"));
+ writer.appendProperty("TEL;TYPE=home,fax", get("homefax"));
+ writer.appendProperty("TEL;TYPE=isdn", get("internationalisdnnumber"));
+ writer.appendProperty("TEL;TYPE=msg", get("otherTelephone"));
+
+ // The structured type value corresponds, in sequence, to the post office box; the extended address;
+ // the street address; the locality (e.g., city); the region (e.g., state or province);
+ // the postal code; the country name
+ writer.appendProperty("ADR;TYPE=home",
+ get("homepostofficebox"), null, get("homeStreet"), get("homeCity"), get("homeState"), get("homePostalCode"), get("homeCountry"));
+ writer.appendProperty("ADR;TYPE=work",
+ get("postofficebox"), get("roomnumber"), get("street"), get("l"), get("st"), get("postalcode"), get("co"));
+ writer.appendProperty("ADR;TYPE=other",
+ get("otherpostofficebox"), null, get("otherstreet"), get("othercity"), get("otherstate"), get("otherpostalcode"), get("othercountry"));
+
+ writer.appendProperty("EMAIL;TYPE=work", get("smtpemail1"));
+ writer.appendProperty("EMAIL;TYPE=home", get("smtpemail2"));
+ writer.appendProperty("EMAIL;TYPE=other", get("smtpemail3"));
+
+ writer.appendProperty("ORG", get("o"), get("department"));
+ writer.appendProperty("URL;TYPE=work", get("businesshomepage"));
+ writer.appendProperty("URL;TYPE=home", get("personalHomePage"));
+ writer.appendProperty("TITLE", get("title"));
+ writer.appendProperty("NOTE", get("description"));
+
+ writer.appendProperty("CUSTOM1", get("extensionattribute1"));
+ writer.appendProperty("CUSTOM2", get("extensionattribute2"));
+ writer.appendProperty("CUSTOM3", get("extensionattribute3"));
+ writer.appendProperty("CUSTOM4", get("extensionattribute4"));
+
+ writer.appendProperty("ROLE", get("profession"));
+ writer.appendProperty("NICKNAME", get("nickname"));
+ writer.appendProperty("X-AIM", get("im"));
+
+ writer.appendProperty("BDAY", convertZuluDateToBday(get("bday")));
+ writer.appendProperty("ANNIVERSARY", convertZuluDateToBday(get("anniversary")));
+
+ String gender = get("gender");
+ if ("1".equals(gender)) {
+ writer.appendProperty("SEX", "2");
+ } else if ("2".equals(gender)) {
+ writer.appendProperty("SEX", "1");
+ }
+
+ writer.appendProperty("CATEGORIES", get("keywords"));
+
+ writer.appendProperty("FBURL", get("fburl"));
+
+ if ("1".equals(get("private"))) {
+ writer.appendProperty("CLASS", "PRIVATE");
+ }
+
+ writer.appendProperty("X-ASSISTANT", get("secretarycn"));
+ writer.appendProperty("X-MANAGER", get("manager"));
+ writer.appendProperty("X-SPOUSE", get("spousecn"));
+
+ writer.appendProperty("REV", get("lastmodified"));
+
+ if ("true".equals(get("haspicture"))) {
+ try {
+ ContactPhoto contactPhoto = getContactPhoto(this);
+ writer.writeLine("PHOTO;BASE64;TYPE=\"" + contactPhoto.contentType + "\";ENCODING=\"b\":");
+ writer.writeLine(contactPhoto.content, true);
+ } catch (IOException e) {
+ LOGGER.warn("Unable to get photo from contact " + this.get("cn"));
+ }
+ }
+
+ writer.endCard();
+ return writer.toString();
+ }
+
+ }
+
+ /**
+ * Calendar event object.
+ */
+ public abstract class Event extends Item {
+ protected String contentClass;
+ protected String subject;
+ protected VCalendar vCalendar;
+
+ /**
+ * @inheritDoc
+ */
+ public Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch) throws IOException {
+ super(folderPath, itemName, etag, noneMatch);
+ this.contentClass = contentClass;
+ fixICS(itemBody.getBytes("UTF-8"), false);
+ // fix task item name
+ if (vCalendar.isTodo() && this.itemName.endsWith(".ics")) {
+ this.itemName = itemName.substring(0, itemName.length() - 3) + "EML";
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected Event() {
+ }
+
+ @Override
+ public String getContentType() {
+ return "text/calendar;charset=UTF-8";
+ }
+
+ @Override
+ public String getBody() throws IOException {
+ if (vCalendar == null) {
+ fixICS(getEventContent(), true);
+ }
+ return vCalendar.toString();
+ }
+
+ protected HttpException buildHttpException(Exception e) {
+ String message = "Unable to get event " + getName() + " subject: " + subject + " at " + permanentUrl + ": " + e.getMessage();
+ LOGGER.warn(message);
+ return new HttpException(message);
+ }
+
+ /**
+ * Retrieve item body from Exchange
+ *
+ * @return item content
+ * @throws HttpException on error
+ */
+ public abstract byte[] getEventContent() throws IOException;
+
+ protected static final String TEXT_CALENDAR = "text/calendar";
+ protected static final String APPLICATION_ICS = "application/ics";
+
+ protected boolean isCalendarContentType(String contentType) {
+ return TEXT_CALENDAR.regionMatches(true, 0, contentType, 0, TEXT_CALENDAR.length()) ||
+ APPLICATION_ICS.regionMatches(true, 0, contentType, 0, APPLICATION_ICS.length());
+ }
+
+ protected MimePart getCalendarMimePart(MimeMultipart multiPart) throws IOException, MessagingException {
+ MimePart bodyPart = null;
+ for (int i = 0; i < multiPart.getCount(); i++) {
+ String contentType = multiPart.getBodyPart(i).getContentType();
+ if (isCalendarContentType(contentType)) {
+ bodyPart = (MimePart) multiPart.getBodyPart(i);
+ break;
+ } else if (contentType.startsWith("multipart")) {
+ Object content = multiPart.getBodyPart(i).getContent();
+ if (content instanceof MimeMultipart) {
+ bodyPart = getCalendarMimePart((MimeMultipart) content);
+ }
+ }
+ }
+
+ return bodyPart;
+ }
+
+ /**
+ * Load ICS content from MIME message input stream
+ *
+ * @param mimeInputStream mime message input stream
+ * @return mime message ics attachment body
+ * @throws IOException on error
+ * @throws MessagingException on error
+ */
+ protected byte[] getICS(InputStream mimeInputStream) throws IOException, MessagingException {
+ byte[] result;
+ MimeMessage mimeMessage = new MimeMessage(null, mimeInputStream);
+ String[] contentClassHeader = mimeMessage.getHeader("Content-class");
+ // task item, return null
+ if (contentClassHeader != null && contentClassHeader.length > 0 && "urn:content-classes:task".equals(contentClassHeader[0])) {
+ return null;
+ }
+ Object mimeBody = mimeMessage.getContent();
+ MimePart bodyPart = null;
+ if (mimeBody instanceof MimeMultipart) {
+ bodyPart = getCalendarMimePart((MimeMultipart) mimeBody);
+ } else if (isCalendarContentType(mimeMessage.getContentType())) {
+ // no multipart, single body
+ bodyPart = mimeMessage;
+ }
+
+ if (bodyPart != null) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ bodyPart.getDataHandler().writeTo(baos);
+ baos.close();
+ result = baos.toByteArray();
+ } else {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ mimeMessage.writeTo(baos);
+ baos.close();
+ throw new DavMailException("EXCEPTION_INVALID_MESSAGE_CONTENT", new String(baos.toByteArray(), "UTF-8"));
+ }
+ return result;
+ }
+
+ protected void fixICS(byte[] icsContent, boolean fromServer) throws IOException {
+ if (LOGGER.isDebugEnabled() && fromServer) {
+ dumpIndex++;
+ String icsBody = new String(icsContent);
+ dumpICS(icsBody, fromServer, false);
+ LOGGER.debug("Vcalendar body received from server:\n" + icsBody);
+ }
+ vCalendar = new VCalendar(icsContent, getEmail(), getVTimezone());
+ vCalendar.fixVCalendar(fromServer);
+ if (LOGGER.isDebugEnabled() && !fromServer) {
+ String resultString = vCalendar.toString();
+ LOGGER.debug("Fixed Vcalendar body to server:\n" + resultString);
+ dumpICS(resultString, fromServer, true);
+ }
+ }
+
+ protected void dumpICS(String icsBody, boolean fromServer, boolean after) {
+ String logFileDirectory = Settings.getLogFileDirectory();
+
+ // additional setting to activate ICS dump (not available in GUI)
+ int dumpMax = Settings.getIntProperty("davmail.dumpICS");
+ if (dumpMax > 0) {
+ if (dumpIndex > dumpMax) {
+ // Delete the oldest dump file
+ final int oldest = dumpIndex - dumpMax;
+ try {
+ File[] oldestFiles = (new File(logFileDirectory)).listFiles(new FilenameFilter() {
+ public boolean accept(File dir, String name) {
+ if (name.endsWith(".ics")) {
+ int dashIndex = name.indexOf('-');
+ if (dashIndex > 0) {
+ try {
+ int fileIndex = Integer.parseInt(name.substring(0, dashIndex));
+ return fileIndex < oldest;
+ } catch (NumberFormatException nfe) {
+ // ignore
+ }
+ }
+ }
+ return false;
+ }
+ });
+ for (File file : oldestFiles) {
+ if (!file.delete()) {
+ LOGGER.warn("Unable to delete " + file.getAbsolutePath());
+ }
+ }
+ } catch (Exception ex) {
+ LOGGER.warn("Error deleting ics dump: " + ex.getMessage());
+ }
+ }
+
+ StringBuilder filePath = new StringBuilder();
+ filePath.append(logFileDirectory).append('/')
+ .append(dumpIndex)
+ .append(after ? "-to" : "-from")
+ .append((after ^ fromServer) ? "-server" : "-client")
+ .append(".ics");
+ if ((icsBody != null) && (icsBody.length() > 0)) {
+ FileWriter fileWriter = null;
+ try {
+ fileWriter = new FileWriter(filePath.toString());
+ fileWriter.write(icsBody);
+ } catch (IOException e) {
+ LOGGER.error(e);
+ } finally {
+ if (fileWriter != null) {
+ try {
+ fileWriter.close();
+ } catch (IOException e) {
+ LOGGER.error(e);
+ }
+ }
+ }
+
+
+ }
+ }
+
+ }
+
+ /**
+ * Build Mime body for event or event message.
+ *
+ * @return mimeContent as byte array or null
+ * @throws IOException on error
+ */
+ public byte[] createMimeContent() throws IOException {
+ String boundary = UUID.randomUUID().toString();
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ MimeOutputStreamWriter writer = new MimeOutputStreamWriter(baos);
+
+ writer.writeHeader("Content-Transfer-Encoding", "7bit");
+ writer.writeHeader("Content-class", contentClass);
+ // append date
+ writer.writeHeader("Date", new Date());
+
+ // Make sure invites have a proper subject line
+ String vEventSubject = vCalendar.getFirstVeventPropertyValue("SUMMARY");
+ if (vEventSubject == null) {
+ vEventSubject = BundleMessage.format("MEETING_REQUEST");
+ }
+
+ // Write a part of the message that contains the
+ // ICS description so that invites contain the description text
+ String description = vCalendar.getFirstVeventPropertyValue("DESCRIPTION");
+
+ // handle notifications
+ if ("urn:content-classes:calendarmessage".equals(contentClass)) {
+ // need to parse attendees and organizer to build recipients
+ VCalendar.Recipients recipients = vCalendar.getRecipients(true);
+ String to;
+ String cc;
+ String notificationSubject;
+ if (email.equalsIgnoreCase(recipients.organizer)) {
+ // current user is organizer => notify all
+ to = recipients.attendees;
+ cc = recipients.optionalAttendees;
+ notificationSubject = subject;
+ } else {
+ String status = vCalendar.getAttendeeStatus();
+ // notify only organizer
+ to = recipients.organizer;
+ cc = null;
+ notificationSubject = (status != null) ? (BundleMessage.format(status) + vEventSubject) : subject;
+ description = "";
+ }
+
+ // Allow end user notification edit
+ if (Settings.getBooleanProperty("davmail.caldavEditNotifications")) {
+ // create notification edit dialog
+ NotificationDialog notificationDialog = new NotificationDialog(to,
+ cc, notificationSubject, description);
+ if (!notificationDialog.getSendNotification()) {
+ LOGGER.debug("Notification canceled by user");
+ return null;
+ }
+ // get description from dialog
+ to = notificationDialog.getTo();
+ cc = notificationDialog.getCc();
+ notificationSubject = notificationDialog.getSubject();
+ description = notificationDialog.getBody();
+ }
+
+ // do not send notification if no recipients found
+ if ((to == null || to.length() == 0) && (cc == null || cc.length() == 0)) {
+ return null;
+ }
+
+ writer.writeHeader("To", to);
+ writer.writeHeader("Cc", cc);
+ writer.writeHeader("Subject", notificationSubject);
+
+
+ if (LOGGER.isDebugEnabled()) {
+ StringBuilder logBuffer = new StringBuilder("Sending notification ");
+ if (to != null) {
+ logBuffer.append("to: ").append(to);
+ }
+ if (cc != null) {
+ logBuffer.append("cc: ").append(cc);
+ }
+ LOGGER.debug(logBuffer.toString());
+ }
+ } else {
+ // need to parse attendees and organizer to build recipients
+ VCalendar.Recipients recipients = vCalendar.getRecipients(false);
+ // storing appointment, full recipients header
+ if (recipients.attendees != null) {
+ writer.writeHeader("To", recipients.attendees);
+ } else {
+ // use current user as attendee
+ writer.writeHeader("To", email);
+ }
+ writer.writeHeader("Cc", recipients.optionalAttendees);
+
+ if (recipients.organizer != null) {
+ writer.writeHeader("From", recipients.organizer);
+ } else {
+ writer.writeHeader("From", email);
+ }
+ }
+ if (vCalendar.getMethod() == null) {
+ vCalendar.setPropertyValue("METHOD", "REQUEST");
+ }
+ writer.writeHeader("MIME-Version", "1.0");
+ writer.writeHeader("Content-Type", "multipart/alternative;\r\n" +
+ "\tboundary=\"----=_NextPart_" + boundary + '\"');
+ writer.writeLn();
+ writer.writeLn("This is a multi-part message in MIME format.");
+ writer.writeLn();
+ writer.writeLn("------=_NextPart_" + boundary);
+
+ if (description != null && description.length() > 0) {
+ writer.writeHeader("Content-Type", "text/plain;\r\n" +
+ "\tcharset=\"utf-8\"");
+ writer.writeHeader("content-transfer-encoding", "8bit");
+ writer.writeLn();
+ writer.flush();
+ baos.write(description.getBytes("UTF-8"));
+ writer.writeLn();
+ writer.writeLn("------=_NextPart_" + boundary);
+ }
+ writer.writeHeader("Content-class", contentClass);
+ writer.writeHeader("Content-Type", "text/calendar;\r\n" +
+ "\tmethod=" + vCalendar.getMethod() + ";\r\n" +
+ "\tcharset=\"utf-8\""
+ );
+ writer.writeHeader("Content-Transfer-Encoding", "8bit");
+ writer.writeLn();
+ writer.flush();
+ baos.write(vCalendar.toString().getBytes("UTF-8"));
+ writer.writeLn();
+ writer.writeLn("------=_NextPart_" + boundary + "--");
+ writer.close();
+ return baos.toByteArray();
+ }
+
+ /**
+ * Create or update item
+ *
+ * @return action result
+ * @throws IOException on error
+ */
+ public abstract ItemResult createOrUpdate() throws IOException;
+
+ }
+
+ protected abstract Set<String> getItemProperties();
+
+ /**
+ * Search contacts in provided folder.
+ *
+ * @param folderPath Exchange folder path
+ * @return list of contacts
+ * @throws IOException on error
+ */
+ public List<ExchangeSession.Contact> getAllContacts(String folderPath) throws IOException {
+ return searchContacts(folderPath, ExchangeSession.CONTACT_ATTRIBUTES, isEqualTo("outlookmessageclass", "IPM.Contact"), 0);
+ }
+
+
+ /**
+ * Search contacts in provided folder matching the search query.
+ *
+ * @param folderPath Exchange folder path
+ * @param attributes requested attributes
+ * @param condition Exchange search query
+ * @param maxCount maximum item count
+ * @return list of contacts
+ * @throws IOException on error
+ */
+ public abstract List<Contact> searchContacts(String folderPath, Set<String> attributes, Condition condition, int maxCount) throws IOException;
+
+ /**
+ * Search calendar messages in provided folder.
+ *
+ * @param folderPath Exchange folder path
+ * @return list of calendar messages as Event objects
+ * @throws IOException on error
+ */
+ public abstract List<Event> getEventMessages(String folderPath) throws IOException;
+
+ /**
+ * Search calendar events in provided folder.
+ *
+ * @param folderPath Exchange folder path
+ * @return list of calendar events
+ * @throws IOException on error
+ */
+ public List<Event> getAllEvents(String folderPath) throws IOException {
+ List<Event> results = searchEvents(folderPath, getCalendarItemCondition(getPastDelayCondition("dtstart")));
+
+ if (!Settings.getBooleanProperty("davmail.caldavDisableTasks", false) && isMainCalendar(folderPath)) {
+ // retrieve tasks from main tasks folder
+ results.addAll(searchTasksOnly(TASKS));
+ }
+
+ return results;
+ }
+
+ protected abstract Condition getCalendarItemCondition(Condition dateCondition);
+
+ protected Condition getPastDelayCondition(String attribute) {
+ int caldavPastDelay = Settings.getIntProperty("davmail.caldavPastDelay");
+ Condition dateCondition = null;
+ if (caldavPastDelay != 0) {
+ Calendar cal = Calendar.getInstance();
+ cal.add(Calendar.DAY_OF_MONTH, -caldavPastDelay);
+ dateCondition = gt(attribute, formatSearchDate(cal.getTime()));
+ }
+ return dateCondition;
+ }
+
+ protected Condition getRangeCondition(String timeRangeStart, String timeRangeEnd) throws IOException {
+ try {
+ SimpleDateFormat parser = getZuluDateFormat();
+ ExchangeSession.MultiCondition andCondition = and();
+ if (timeRangeStart != null) {
+ andCondition.add(gt("dtend", formatSearchDate(parser.parse(timeRangeStart))));
+ }
+ if (timeRangeEnd != null) {
+ andCondition.add(lt("dtstart", formatSearchDate(parser.parse(timeRangeEnd))));
+ }
+ return andCondition;
+ } catch (ParseException e) {
+ throw new IOException(e+" "+e.getMessage());
+ }
+ }
+
+ /**
+ * Search events between start and end.
+ *
+ * @param folderPath Exchange folder path
+ * @param timeRangeStart date range start in zulu format
+ * @param timeRangeEnd date range start in zulu format
+ * @return list of calendar events
+ * @throws IOException on error
+ */
+ public List<Event> searchEvents(String folderPath, String timeRangeStart, String timeRangeEnd) throws IOException {
+ Condition dateCondition = getRangeCondition(timeRangeStart, timeRangeEnd);
+ Condition condition = getCalendarItemCondition(dateCondition);
+
+ return searchEvents(folderPath, condition);
+ }
+
+ /**
+ * Search events between start and end, exclude tasks.
+ *
+ * @param folderPath Exchange folder path
+ * @param timeRangeStart date range start in zulu format
+ * @param timeRangeEnd date range start in zulu format
+ * @return list of calendar events
+ * @throws IOException on error
+ */
+ public List<Event> searchEventsOnly(String folderPath, String timeRangeStart, String timeRangeEnd) throws IOException {
+ Condition dateCondition = getRangeCondition(timeRangeStart, timeRangeEnd);
+ return searchEvents(folderPath, getCalendarItemCondition(dateCondition));
+ }
+
+ /**
+ * Search tasks only (VTODO).
+ *
+ * @param folderPath Exchange folder path
+ * @return list of tasks
+ * @throws IOException on error
+ */
+ public List<Event> searchTasksOnly(String folderPath) throws IOException {
+ return searchEvents(folderPath, and(isEqualTo("outlookmessageclass", "IPM.Task"),
+ or(isNull("datecompleted"), getPastDelayCondition("datecompleted"))));
+ }
+
+ /**
+ * Search calendar events in provided folder.
+ *
+ * @param folderPath Exchange folder path
+ * @param filter search filter
+ * @return list of calendar events
+ * @throws IOException on error
+ */
+ public List<Event> searchEvents(String folderPath, Condition filter) throws IOException {
+
+ Condition privateCondition = null;
+ if (isSharedFolder(folderPath) && Settings.getBooleanProperty("davmail.excludePrivateEvents", true)) {
+ LOGGER.debug("Shared or public calendar: exclude private events");
+ privateCondition = isEqualTo("sensitivity", 0);
+ }
+
+ return searchEvents(folderPath, getItemProperties(),
+ and(filter, privateCondition));
+ }
+
+ /**
+ * Search calendar events or messages in provided folder matching the search query.
+ *
+ * @param folderPath Exchange folder path
+ * @param attributes requested attributes
+ * @param condition Exchange search query
+ * @return list of calendar messages as Event objects
+ * @throws IOException on error
+ */
+ public abstract List<Event> searchEvents(String folderPath, Set<String> attributes, Condition condition) throws IOException;
+
+ /**
+ * convert vcf extension to EML.
+ *
+ * @param itemName item name
+ * @return EML item name
+ */
+ protected String convertItemNameToEML(String itemName) {
+ if (itemName.endsWith(".vcf")) {
+ return itemName.substring(0, itemName.length() - 3) + "EML";
+ } else {
+ return itemName;
+ }
+ }
+
+ /**
+ * Get item named eventName in folder
+ *
+ * @param folderPath Exchange folder path
+ * @param itemName event name
+ * @return event object
+ * @throws IOException on error
+ */
+ public abstract Item getItem(String folderPath, String itemName) throws IOException;
+
+ /**
+ * Contact picture
+ */
+ public static class ContactPhoto {
+ /**
+ * Contact picture content type (always image/jpeg on read)
+ */
+ public String contentType;
+ /**
+ * Base64 encoded picture content
+ */
+ public String content;
+ }
+
+ /**
+ * Retrieve contact photo attached to contact
+ *
+ * @param contact address book contact
+ * @return contact photo
+ * @throws IOException on error
+ */
+ public abstract ContactPhoto getContactPhoto(Contact contact) throws IOException;
+
+
+ /**
+ * Delete event named itemName in folder
+ *
+ * @param folderPath Exchange folder path
+ * @param itemName item name
+ * @throws IOException on error
+ */
+ public abstract void deleteItem(String folderPath, String itemName) throws IOException;
+
+ /**
+ * Mark event processed named eventName in folder
+ *
+ * @param folderPath Exchange folder path
+ * @param itemName item name
+ * @throws IOException on error
+ */
+ public abstract void processItem(String folderPath, String itemName) throws IOException;
+
+
+ private static int dumpIndex;
+
+ /**
+ * Replace iCal4 (Snow Leopard) principal paths with mailto expression
+ *
+ * @param value attendee value or ics line
+ * @return fixed value
+ */
+ protected String replaceIcal4Principal(String value) {
+ if (value != null && value.contains("/principals/__uuids__/")) {
+ return value.replaceAll("/principals/__uuids__/([^/]*)__AT__([^/]*)/", "mailto:$1@$2");
+ } else {
+ return value;
+ }
+ }
+
+ /**
+ * Event result object to hold HTTP status and event etag from an event creation/update.
+ */
+ public static class ItemResult {
+ /**
+ * HTTP status
+ */
+ public int status;
+ /**
+ * Event etag from response HTTP header
+ */
+ public String etag;
+ }
+
+ /**
+ * Build and send the MIME message for the provided ICS event.
+ *
+ * @param icsBody event in iCalendar format
+ * @return HTTP status
+ * @throws IOException on error
+ */
+ public abstract int sendEvent(String icsBody) throws IOException;
+
+ /**
+ * Create or update item (event or contact) on the Exchange server
+ *
+ * @param folderPath Exchange folder path
+ * @param itemName event name
+ * @param itemBody event body in iCalendar format
+ * @param etag previous event etag to detect concurrent updates
+ * @param noneMatch if-none-match header value
+ * @return HTTP response event result (status and etag)
+ * @throws IOException on error
+ */
+ public ItemResult createOrUpdateItem(String folderPath, String itemName, String itemBody, String etag, String noneMatch) throws IOException {
+ if (itemBody.startsWith("BEGIN:VCALENDAR")) {
+ return internalCreateOrUpdateEvent(folderPath, itemName, "urn:content-classes:appointment", itemBody, etag, noneMatch);
+ } else if (itemBody.startsWith("BEGIN:VCARD")) {
+ return createOrUpdateContact(folderPath, itemName, itemBody, etag, noneMatch);
+ } else {
+ throw new IOException(BundleMessage.format("EXCEPTION_INVALID_MESSAGE_CONTENT", itemBody));
+ }
+ }
+
+ static final String[] VCARD_N_PROPERTIES = {"sn", "givenName", "middlename", "personaltitle", "namesuffix"};
+ static final String[] VCARD_ADR_HOME_PROPERTIES = {"homepostofficebox", null, "homeStreet", "homeCity", "homeState", "homePostalCode", "homeCountry"};
+ static final String[] VCARD_ADR_WORK_PROPERTIES = {"postofficebox", "roomnumber", "street", "l", "st", "postalcode", "co"};
+ static final String[] VCARD_ADR_OTHER_PROPERTIES = {"otherpostofficebox", null, "otherstreet", "othercity", "otherstate", "otherpostalcode", "othercountry"};
+ static final String[] VCARD_ORG_PROPERTIES = {"o", "department"};
+
+ protected void convertContactProperties(Map<String, String> properties, String[] contactProperties, List<String> values) {
+ for (int i = 0; i < values.size() && i < contactProperties.length; i++) {
+ if (contactProperties[i] != null) {
+ properties.put(contactProperties[i], values.get(i));
+ }
+ }
+ }
+
+ protected ItemResult createOrUpdateContact(String folderPath, String itemName, String itemBody, String etag, String noneMatch) throws IOException {
+ // parse VCARD body to build contact property map
+ Map<String, String> properties = new HashMap<String, String>();
+ properties.put("outlookmessageclass", "IPM.Contact");
+
+ VObject vcard = new VObject(new ICSBufferedReader(new StringReader(itemBody)));
+ for (VProperty property : vcard.getProperties()) {
+ if ("FN".equals(property.getKey())) {
+ properties.put("cn", property.getValue());
+ properties.put("subject", property.getValue());
+ properties.put("fileas", property.getValue());
+
+ } else if ("N".equals(property.getKey())) {
+ convertContactProperties(properties, VCARD_N_PROPERTIES, property.getValues());
+ } else if ("NICKNAME".equals(property.getKey())) {
+ properties.put("nickname", property.getValue());
+ } else if ("TEL".equals(property.getKey())) {
+ if (property.hasParam("TYPE", "cell") || property.hasParam("X-GROUP", "cell")) {
+ properties.put("mobile", property.getValue());
+ } else if (property.hasParam("TYPE", "work") || property.hasParam("X-GROUP", "work")) {
+ properties.put("telephoneNumber", property.getValue());
+ } else if (property.hasParam("TYPE", "home") || property.hasParam("X-GROUP", "home")) {
+ properties.put("homePhone", property.getValue());
+ } else if (property.hasParam("TYPE", "fax")) {
+ if (property.hasParam("TYPE", "home")) {
+ properties.put("homefax", property.getValue());
+ } else {
+ properties.put("facsimiletelephonenumber", property.getValue());
+ }
+ } else if (property.hasParam("TYPE", "pager")) {
+ properties.put("pager", property.getValue());
+ } else if (property.hasParam("TYPE", "car")) {
+ properties.put("othermobile", property.getValue());
+ } else {
+ properties.put("otherTelephone", property.getValue());
+ }
+ } else if ("ADR".equals(property.getKey())) {
+ // address
+ if (property.hasParam("TYPE", "home")) {
+ convertContactProperties(properties, VCARD_ADR_HOME_PROPERTIES, property.getValues());
+ } else if (property.hasParam("TYPE", "work")) {
+ convertContactProperties(properties, VCARD_ADR_WORK_PROPERTIES, property.getValues());
+ // any other type goes to other address
+ } else {
+ convertContactProperties(properties, VCARD_ADR_OTHER_PROPERTIES, property.getValues());
+ }
+ } else if ("EMAIL".equals(property.getKey())) {
+ if (property.hasParam("TYPE", "home")) {
+ properties.put("email2", property.getValue());
+ properties.put("smtpemail2", property.getValue());
+ } else if (property.hasParam("TYPE", "other")) {
+ properties.put("email3", property.getValue());
+ properties.put("smtpemail3", property.getValue());
+ } else {
+ properties.put("email1", property.getValue());
+ properties.put("smtpemail1", property.getValue());
+ }
+ } else if ("ORG".equals(property.getKey())) {
+ convertContactProperties(properties, VCARD_ORG_PROPERTIES, property.getValues());
+ } else if ("URL".equals(property.getKey())) {
+ if (property.hasParam("TYPE", "work")) {
+ properties.put("businesshomepage", property.getValue());
+ } else if (property.hasParam("TYPE", "home")) {
+ properties.put("personalHomePage", property.getValue());
+ } else {
+ // default: set personal home page
+ properties.put("personalHomePage", property.getValue());
+ }
+ } else if ("TITLE".equals(property.getKey())) {
+ properties.put("title", property.getValue());
+ } else if ("NOTE".equals(property.getKey())) {
+ properties.put("description", property.getValue());
+ } else if ("CUSTOM1".equals(property.getKey())) {
+ properties.put("extensionattribute1", property.getValue());
+ } else if ("CUSTOM2".equals(property.getKey())) {
+ properties.put("extensionattribute2", property.getValue());
+ } else if ("CUSTOM3".equals(property.getKey())) {
+ properties.put("extensionattribute3", property.getValue());
+ } else if ("CUSTOM4".equals(property.getKey())) {
+ properties.put("extensionattribute4", property.getValue());
+ } else if ("ROLE".equals(property.getKey())) {
+ properties.put("profession", property.getValue());
+ } else if ("X-AIM".equals(property.getKey())) {
+ properties.put("im", property.getValue());
+ } else if ("BDAY".equals(property.getKey())) {
+ properties.put("bday", convertBDayToZulu(property.getValue()));
+ } else if ("ANNIVERSARY".equals(property.getKey()) || "X-ANNIVERSARY".equals(property.getKey())) {
+ properties.put("anniversary", convertBDayToZulu(property.getValue()));
+ } else if ("CATEGORIES".equals(property.getKey())) {
+ properties.put("keywords", property.getValue());
+ } else if ("CLASS".equals(property.getKey())) {
+ if ("PUBLIC".equals(property.getValue())) {
+ properties.put("sensitivity", "0");
+ properties.put("private", "false");
+ } else {
+ properties.put("sensitivity", "2");
+ properties.put("private", "true");
+ }
+ } else if ("SEX".equals(property.getKey())) {
+ String propertyValue = property.getValue();
+ if ("1".equals(propertyValue)) {
+ properties.put("gender", "2");
+ } else if ("2".equals(propertyValue)) {
+ properties.put("gender", "1");
+ }
+ } else if ("FBURL".equals(property.getKey())) {
+ properties.put("fburl", property.getValue());
+ } else if ("X-ASSISTANT".equals(property.getKey())) {
+ properties.put("secretarycn", property.getValue());
+ } else if ("X-MANAGER".equals(property.getKey())) {
+ properties.put("manager", property.getValue());
+ } else if ("X-SPOUSE".equals(property.getKey())) {
+ properties.put("spousecn", property.getValue());
+ } else if ("PHOTO".equals(property.getKey())) {
+ properties.put("photo", property.getValue());
+ properties.put("haspicture", "true");
+ }
+ }
+ LOGGER.debug("Create or update contact " + itemName + ": " + properties);
+ // reset missing properties to null
+ for (String key : CONTACT_ATTRIBUTES) {
+ if (!"imapUid".equals(key) && !"etag".equals(key) && !"urlcompname".equals(key)
+ && !"lastmodified".equals(key) && !"sensitivity".equals(key) &&
+ !properties.containsKey(key)) {
+ properties.put(key, null);
+ }
+ }
+ return internalCreateOrUpdateContact(folderPath, itemName, properties, etag, noneMatch);
+ }
+
+ protected String convertZuluDateToBday(String value) {
+ String result = null;
+ if (value != null && value.length() > 0) {
+ try {
+ SimpleDateFormat parser = ExchangeSession.getZuluDateFormat();
+ Calendar cal = Calendar.getInstance();
+ cal.setTime(parser.parse(value));
+ cal.add(Calendar.HOUR_OF_DAY, 12);
+ result = ExchangeSession.getVcardBdayFormat().format(cal.getTime());
+ } catch (ParseException e) {
+ LOGGER.warn("Invalid date: " + value);
+ }
+ }
+ return result;
+ }
+
+ protected String convertBDayToZulu(String value) {
+ String result = null;
+ if (value != null && value.length() > 0) {
+ try {
+ SimpleDateFormat parser;
+ if (value.length() == 10) {
+ parser = ExchangeSession.getVcardBdayFormat();
+ } else if (value.length() == 15) {
+ parser = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ENGLISH);
+ parser.setTimeZone(GMT_TIMEZONE);
+ } else {
+ parser = ExchangeSession.getExchangeZuluDateFormat();
+ }
+ result = ExchangeSession.getExchangeZuluDateFormatMillisecond().format(parser.parse(value));
+ } catch (ParseException e) {
+ LOGGER.warn("Invalid date: " + value);
+ }
+ }
+
+ return result;
+ }
+
+
+ protected abstract ItemResult internalCreateOrUpdateContact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) throws IOException;
+
+ protected abstract ItemResult internalCreateOrUpdateEvent(String folderPath, String itemName, String contentClass, String icsBody, String etag, String noneMatch) throws IOException;
+
+ /**
+ * Get current Exchange alias name from login name
+ *
+ * @return user name
+ */
+ public String getAliasFromLogin() {
+ // login is email, not alias
+ if (this.userName.indexOf('@') >= 0) {
+ return null;
+ }
+ String result = this.userName;
+ // remove domain name
+ int index = Math.max(result.indexOf('\\'), result.indexOf('/'));
+ if (index >= 0) {
+ result = result.substring(index + 1);
+ }
+ return result;
+ }
+
+ protected String getEmailSuffixFromHostname() {
+ String domain = httpClient.getHostConfiguration().getHost();
+ int start = domain.lastIndexOf('.', domain.lastIndexOf('.') - 1);
+ if (start >= 0) {
+ return '@' + domain.substring(start + 1);
+ } else {
+ return '@' + domain;
+ }
+ }
+
+ /**
+ * Test if folderPath is inside user mailbox.
+ *
+ * @param folderPath absolute folder path
+ * @return true if folderPath is a public or shared folder
+ */
+ public abstract boolean isSharedFolder(String folderPath);
+
+ /**
+ * Test if folderPath is main calendar.
+ *
+ * @param folderPath absolute folder path
+ * @return true if folderPath is a public or shared folder
+ */
+ public abstract boolean isMainCalendar(String folderPath);
+
+ static final String MAILBOX_BASE = "/cn=";
+
+ protected void getEmailAndAliasFromOptions() {
+ Cookie[] currentCookies = httpClient.getState().getCookies();
+ // get user mail URL from html body
+ BufferedReader optionsPageReader = null;
+ GetMethod optionsMethod = new GetMethod("/owa/?ae=Options&t=About");
+ try {
+ DavGatewayHttpClientFacade.executeGetMethod(httpClient, optionsMethod, false);
+ optionsPageReader = new BufferedReader(new InputStreamReader(optionsMethod.getResponseBodyAsStream()));
+ String line;
+
+ // find email and alias
+ while ((line = optionsPageReader.readLine()) != null
+ && (line.indexOf('[') == -1
+ || line.indexOf('@') == -1
+ || line.indexOf(']') == -1)
+ && line.toLowerCase().indexOf(MAILBOX_BASE) == -1) {
+ }
+ if (line != null) {
+ int start = line.toLowerCase().lastIndexOf(MAILBOX_BASE) + MAILBOX_BASE.length();
+ int end = line.indexOf('<', start);
+ alias = line.substring(start, end);
+ end = line.lastIndexOf(']');
+ start = line.lastIndexOf('[', end) + 1;
+ email = line.substring(start, end);
+ }
+ } catch (IOException e) {
+ // restore cookies on error
+ httpClient.getState().addCookies(currentCookies);
+ LOGGER.error("Error parsing options page at " + optionsMethod.getPath());
+ } finally {
+ if (optionsPageReader != null) {
+ try {
+ optionsPageReader.close();
+ } catch (IOException e) {
+ LOGGER.error("Error parsing options page at " + optionsMethod.getPath());
+ }
+ }
+ optionsMethod.releaseConnection();
+ }
+ }
+
+ /**
+ * Get current user email
+ *
+ * @return user email
+ */
+ public String getEmail() {
+ return email;
+ }
+
+ /**
+ * Get current user alias
+ *
+ * @return user email
+ */
+ public String getAlias() {
+ return alias;
+ }
+
+ /**
+ * Search global address list
+ *
+ * @param condition search filter
+ * @param returningAttributes returning attributes
+ * @param sizeLimit size limit
+ * @return matching contacts from gal
+ * @throws IOException on error
+ */
+ public abstract Map<String, Contact> galFind(Condition condition, Set<String> returningAttributes, int sizeLimit) throws IOException;
+
+ /**
+ * Full Contact attribute list
+ */
+ public static final Set<String> CONTACT_ATTRIBUTES = new HashSet<String>();
+
+ static {
+ CONTACT_ATTRIBUTES.add("imapUid");
+ CONTACT_ATTRIBUTES.add("etag");
+ CONTACT_ATTRIBUTES.add("urlcompname");
+
+ CONTACT_ATTRIBUTES.add("extensionattribute1");
+ CONTACT_ATTRIBUTES.add("extensionattribute2");
+ CONTACT_ATTRIBUTES.add("extensionattribute3");
+ CONTACT_ATTRIBUTES.add("extensionattribute4");
+ CONTACT_ATTRIBUTES.add("bday");
+ CONTACT_ATTRIBUTES.add("anniversary");
+ CONTACT_ATTRIBUTES.add("businesshomepage");
+ CONTACT_ATTRIBUTES.add("personalHomePage");
+ CONTACT_ATTRIBUTES.add("cn");
+ CONTACT_ATTRIBUTES.add("co");
+ CONTACT_ATTRIBUTES.add("department");
+ CONTACT_ATTRIBUTES.add("smtpemail1");
+ CONTACT_ATTRIBUTES.add("smtpemail2");
+ CONTACT_ATTRIBUTES.add("smtpemail3");
+ CONTACT_ATTRIBUTES.add("facsimiletelephonenumber");
+ CONTACT_ATTRIBUTES.add("givenName");
+ CONTACT_ATTRIBUTES.add("homeCity");
+ CONTACT_ATTRIBUTES.add("homeCountry");
+ CONTACT_ATTRIBUTES.add("homePhone");
+ CONTACT_ATTRIBUTES.add("homePostalCode");
+ CONTACT_ATTRIBUTES.add("homeState");
+ CONTACT_ATTRIBUTES.add("homeStreet");
+ CONTACT_ATTRIBUTES.add("homepostofficebox");
+ CONTACT_ATTRIBUTES.add("l");
+ CONTACT_ATTRIBUTES.add("manager");
+ CONTACT_ATTRIBUTES.add("mobile");
+ CONTACT_ATTRIBUTES.add("namesuffix");
+ CONTACT_ATTRIBUTES.add("nickname");
+ CONTACT_ATTRIBUTES.add("o");
+ CONTACT_ATTRIBUTES.add("pager");
+ CONTACT_ATTRIBUTES.add("personaltitle");
+ CONTACT_ATTRIBUTES.add("postalcode");
+ CONTACT_ATTRIBUTES.add("postofficebox");
+ CONTACT_ATTRIBUTES.add("profession");
+ CONTACT_ATTRIBUTES.add("roomnumber");
+ CONTACT_ATTRIBUTES.add("secretarycn");
+ CONTACT_ATTRIBUTES.add("sn");
+ CONTACT_ATTRIBUTES.add("spousecn");
+ CONTACT_ATTRIBUTES.add("st");
+ CONTACT_ATTRIBUTES.add("street");
+ CONTACT_ATTRIBUTES.add("telephoneNumber");
+ CONTACT_ATTRIBUTES.add("title");
+ CONTACT_ATTRIBUTES.add("description");
+ CONTACT_ATTRIBUTES.add("im");
+ CONTACT_ATTRIBUTES.add("middlename");
+ CONTACT_ATTRIBUTES.add("lastmodified");
+ CONTACT_ATTRIBUTES.add("otherstreet");
+ CONTACT_ATTRIBUTES.add("otherstate");
+ CONTACT_ATTRIBUTES.add("otherpostofficebox");
+ CONTACT_ATTRIBUTES.add("otherpostalcode");
+ CONTACT_ATTRIBUTES.add("othercountry");
+ CONTACT_ATTRIBUTES.add("othercity");
+ CONTACT_ATTRIBUTES.add("haspicture");
+ CONTACT_ATTRIBUTES.add("keywords");
+ CONTACT_ATTRIBUTES.add("othermobile");
+ CONTACT_ATTRIBUTES.add("otherTelephone");
+ CONTACT_ATTRIBUTES.add("gender");
+ CONTACT_ATTRIBUTES.add("private");
+ CONTACT_ATTRIBUTES.add("sensitivity");
+ CONTACT_ATTRIBUTES.add("fburl");
+ }
+
+ /**
+ * Get freebusy data string from Exchange.
+ *
+ * @param attendee attendee email address
+ * @param start start date in Exchange zulu format
+ * @param end end date in Exchange zulu format
+ * @param interval freebusy interval in minutes
+ * @return freebusy data or null
+ * @throws IOException on error
+ */
+ protected abstract String getFreeBusyData(String attendee, String start, String end, int interval) throws IOException;
+
+ /**
+ * Get freebusy info for attendee between start and end date.
+ *
+ * @param attendee attendee email
+ * @param startDateValue start date
+ * @param endDateValue end date
+ * @return FreeBusy info
+ * @throws IOException on error
+ */
+ public FreeBusy getFreebusy(String attendee, String startDateValue, String endDateValue) throws IOException {
+ // replace ical encoded attendee name
+ attendee = replaceIcal4Principal(attendee);
+
+ // then check that email address is valid to avoid InvalidSmtpAddress error
+ if (attendee == null || attendee.indexOf('@') < 0 || attendee.charAt(attendee.length() - 1) == '@') {
+ return null;
+ }
+
+ if (attendee.startsWith("mailto:") || attendee.startsWith("MAILTO:")) {
+ attendee = attendee.substring("mailto:".length());
+ }
+
+ SimpleDateFormat exchangeZuluDateFormat = getExchangeZuluDateFormat();
+ SimpleDateFormat icalDateFormat = getZuluDateFormat();
+
+ Date startDate;
+ Date endDate;
+ try {
+ if (startDateValue.length() == 8) {
+ startDate = parseDate(startDateValue);
+ } else {
+ startDate = icalDateFormat.parse(startDateValue);
+ }
+ if (endDateValue.length() == 8) {
+ endDate = parseDate(endDateValue);
+ } else {
+ endDate = icalDateFormat.parse(endDateValue);
+ }
+ } catch (ParseException e) {
+ throw new DavMailException("EXCEPTION_INVALID_DATES", e.getMessage());
+ }
+
+ FreeBusy freeBusy = null;
+ String fbdata = getFreeBusyData(attendee, exchangeZuluDateFormat.format(startDate), exchangeZuluDateFormat.format(endDate), FREE_BUSY_INTERVAL);
+ if (fbdata != null) {
+ freeBusy = new FreeBusy(icalDateFormat, startDate, fbdata);
+ }
+
+ if (freeBusy != null && freeBusy.knownAttendee) {
+ return freeBusy;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Exchange to iCalendar Free/Busy parser.
+ * Free time returns 0, Tentative returns 1, Busy returns 2, and Out of Office (OOF) returns 3
+ */
+ public static final class FreeBusy {
+ final SimpleDateFormat icalParser;
+ boolean knownAttendee = true;
+ static final HashMap<Character, String> FBTYPES = new HashMap<Character, String>();
+
+ static {
+ FBTYPES.put('1', "BUSY-TENTATIVE");
+ FBTYPES.put('2', "BUSY");
+ FBTYPES.put('3', "BUSY-UNAVAILABLE");
+ }
+
+ final HashMap<String, StringBuilder> busyMap = new HashMap<String, StringBuilder>();
+
+ StringBuilder getBusyBuffer(char type) {
+ String fbType = FBTYPES.get(Character.valueOf(type));
+ StringBuilder buffer = busyMap.get(fbType);
+ if (buffer == null) {
+ buffer = new StringBuilder();
+ busyMap.put(fbType, buffer);
+ }
+ return buffer;
+ }
+
+ void startBusy(char type, Calendar currentCal) {
+ if (type == '4') {
+ knownAttendee = false;
+ } else if (type != '0') {
+ StringBuilder busyBuffer = getBusyBuffer(type);
+ if (busyBuffer.length() > 0) {
+ busyBuffer.append(',');
+ }
+ busyBuffer.append(icalParser.format(currentCal.getTime()));
+ }
+ }
+
+ void endBusy(char type, Calendar currentCal) {
+ if (type != '0' && type != '4') {
+ getBusyBuffer(type).append('/').append(icalParser.format(currentCal.getTime()));
+ }
+ }
+
+ FreeBusy(SimpleDateFormat icalParser, Date startDate, String fbdata) {
+ this.icalParser = icalParser;
+ if (fbdata.length() > 0) {
+ Calendar currentCal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+ currentCal.setTime(startDate);
+
+ startBusy(fbdata.charAt(0), currentCal);
+ for (int i = 1; i < fbdata.length() && knownAttendee; i++) {
+ currentCal.add(Calendar.MINUTE, FREE_BUSY_INTERVAL);
+ char previousState = fbdata.charAt(i - 1);
+ char currentState = fbdata.charAt(i);
+ if (previousState != currentState) {
+ endBusy(previousState, currentCal);
+ startBusy(currentState, currentCal);
+ }
+ }
+ currentCal.add(Calendar.MINUTE, FREE_BUSY_INTERVAL);
+ endBusy(fbdata.charAt(fbdata.length() - 1), currentCal);
+ }
+ }
+
+ /**
+ * Append freebusy information to buffer.
+ *
+ * @param buffer String buffer
+ */
+ public void appendTo(StringBuilder buffer) {
+ for (Map.Entry<String, StringBuilder> entry : busyMap.entrySet()) {
+ buffer.append("FREEBUSY;FBTYPE=").append(entry.getKey())
+ .append(':').append(entry.getValue()).append((char) 13).append((char) 10);
+ }
+ }
+ }
+
+ protected VObject vTimezone;
+
+ /**
+ * Load and return current user OWA timezone.
+ *
+ * @return current timezone
+ */
+ public VObject getVTimezone() {
+ if (vTimezone == null) {
+ // need to load Timezone info from OWA
+ loadVtimezone();
+ }
+ return vTimezone;
+ }
+
+ protected abstract void loadVtimezone();
+
+ /**
+ * Return internal HttpClient instance
+ *
+ * @return http client
+ */
+ public HttpClient getHttpClient() {
+ return httpClient;
+ }
+}
diff --git a/src/java/davmail/exchange/ExchangeSessionFactory.java b/src/java/davmail/exchange/ExchangeSessionFactory.java
new file mode 100644
index 0000000..6094f71
--- /dev/null
+++ b/src/java/davmail/exchange/ExchangeSessionFactory.java
@@ -0,0 +1,317 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange;
+
+import davmail.BundleMessage;
+import davmail.Settings;
+import davmail.exception.DavMailAuthenticationException;
+import davmail.exception.DavMailException;
+import davmail.exception.WebdavNotAvailableException;
+import davmail.exchange.dav.DavExchangeSession;
+import davmail.exchange.ews.EwsExchangeSession;
+import davmail.http.DavGatewayHttpClientFacade;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpStatus;
+import org.apache.commons.httpclient.methods.GetMethod;
+
+import java.io.IOException;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Create ExchangeSession instances.
+ */
+public final class ExchangeSessionFactory {
+ private static final Object LOCK = new Object();
+ private static final Map<PoolKey, ExchangeSession> POOL_MAP = new HashMap<PoolKey, ExchangeSession>();
+ private static boolean configChecked;
+ private static boolean errorSent;
+
+ static class PoolKey {
+ final String url;
+ final String userName;
+ final String password;
+
+ PoolKey(String url, String userName, String password) {
+ this.url = url;
+ this.userName = userName;
+ this.password = password;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ return object == this ||
+ object instanceof PoolKey &&
+ ((PoolKey) object).url.equals(this.url) &&
+ ((PoolKey) object).userName.equals(this.userName) &&
+ ((PoolKey) object).password.equals(this.password);
+ }
+
+ @Override
+ public int hashCode() {
+ return url.hashCode() + userName.hashCode() + password.hashCode();
+ }
+ }
+
+ private ExchangeSessionFactory() {
+ }
+
+ /**
+ * Create authenticated Exchange session
+ *
+ * @param userName user login
+ * @param password user password
+ * @return authenticated session
+ * @throws IOException on error
+ */
+ public static ExchangeSession getInstance(String userName, String password) throws IOException {
+ String baseUrl = Settings.getProperty("davmail.url");
+ if (Settings.getBooleanProperty("davmail.server")) {
+ return getInstance(baseUrl, userName, password);
+ } else {
+ // serialize session creation in workstation mode to avoid multiple OTP requests
+ synchronized (LOCK) {
+ return getInstance(baseUrl, userName, password);
+ }
+ }
+ }
+
+ private static String convertUserName(String userName) {
+ String result = userName;
+ // prepend default windows domain prefix
+ String defaultDomain = Settings.getProperty("davmail.defaultDomain");
+ if (userName.indexOf('\\') < 0 && defaultDomain != null) {
+ result = defaultDomain + '\\' + userName;
+ }
+ return result;
+ }
+
+ /**
+ * Create authenticated Exchange session
+ *
+ * @param baseUrl OWA base URL
+ * @param userName user login
+ * @param password user password
+ * @return authenticated session
+ * @throws IOException on error
+ */
+ public static ExchangeSession getInstance(String baseUrl, String userName, String password) throws IOException {
+ ExchangeSession session = null;
+ try {
+
+ PoolKey poolKey = new PoolKey(baseUrl, convertUserName(userName), password);
+
+ synchronized (LOCK) {
+ session = POOL_MAP.get(poolKey);
+ }
+ if (session != null) {
+ ExchangeSession.LOGGER.debug("Got session " + session + " from cache");
+ }
+
+ if (session != null && session.isExpired()) {
+ ExchangeSession.LOGGER.debug("Session " + session + " expired");
+ session = null;
+ // expired session, remove from cache
+ synchronized (LOCK) {
+ POOL_MAP.remove(poolKey);
+ }
+ }
+
+ if (session == null) {
+ String enableEws = Settings.getProperty("davmail.enableEws", "auto");
+ if ("true".equals(enableEws)) {
+ session = new EwsExchangeSession(poolKey.url, poolKey.userName, poolKey.password);
+ } else {
+ try {
+ session = new DavExchangeSession(poolKey.url, poolKey.userName, poolKey.password);
+ } catch (WebdavNotAvailableException e) {
+ if ("auto".equals(enableEws)) {
+ ExchangeSession.LOGGER.debug(e.getMessage() + ", retry with EWS");
+ session = new EwsExchangeSession(poolKey.url, poolKey.userName, poolKey.password);
+ } else {
+ throw e;
+ }
+ }
+ }
+ ExchangeSession.LOGGER.debug("Created new session: " + session);
+ }
+ // successful login, put session in cache
+ synchronized (LOCK) {
+ POOL_MAP.put(poolKey, session);
+ }
+ // session opened, future failure will mean network down
+ configChecked = true;
+ // Reset so next time an problem occurs message will be sent once
+ errorSent = false;
+ } catch (DavMailAuthenticationException exc) {
+ throw exc;
+ } catch (DavMailException exc) {
+ throw exc;
+ } catch (IllegalStateException exc) {
+ throw exc;
+ } catch (Exception exc) {
+ handleNetworkDown(exc);
+ }
+ return session;
+ }
+
+ /**
+ * Get a non expired session.
+ * If the current session is not expired, return current session, else try to create a new session
+ *
+ * @param currentSession current session
+ * @param userName user login
+ * @param password user password
+ * @return authenticated session
+ * @throws IOException on error
+ */
+ public static ExchangeSession getInstance(ExchangeSession currentSession, String userName, String password)
+ throws IOException {
+ ExchangeSession session = currentSession;
+ try {
+ if (session.isExpired()) {
+ ExchangeSession.LOGGER.debug("Session " + session + " expired, trying to open a new one");
+ session = null;
+ String baseUrl = Settings.getProperty("davmail.url");
+ PoolKey poolKey = new PoolKey(baseUrl, userName, password);
+ // expired session, remove from cache
+ synchronized (LOCK) {
+ POOL_MAP.remove(poolKey);
+ }
+ session = getInstance(userName, password);
+ }
+ } catch (DavMailAuthenticationException exc) {
+ ExchangeSession.LOGGER.debug("Unable to reopen session", exc);
+ throw exc;
+ } catch (Exception exc) {
+ ExchangeSession.LOGGER.debug("Unable to reopen session", exc);
+ handleNetworkDown(exc);
+ }
+ return session;
+ }
+
+ /**
+ * Send a request to Exchange server to check current settings.
+ *
+ * @throws IOException if unable to access Exchange server
+ */
+ public static void checkConfig() throws IOException {
+ String url = Settings.getProperty("davmail.url");
+ if (url == null || (!url.startsWith("http://") && !url.startsWith("https://"))) {
+ throw new DavMailException("LOG_INVALID_URL", url);
+ }
+ HttpClient httpClient = DavGatewayHttpClientFacade.getInstance(url);
+ GetMethod testMethod = new GetMethod(url);
+ try {
+ // get webMail root url (will not follow redirects)
+ int status = DavGatewayHttpClientFacade.executeTestMethod(httpClient, testMethod);
+ ExchangeSession.LOGGER.debug("Test configuration status: " + status);
+ if (status != HttpStatus.SC_OK && status != HttpStatus.SC_UNAUTHORIZED
+ && !DavGatewayHttpClientFacade.isRedirect(status)) {
+ throw new DavMailException("EXCEPTION_CONNECTION_FAILED", url, status);
+ }
+ // session opened, future failure will mean network down
+ configChecked = true;
+ // Reset so next time an problem occurs message will be sent once
+ errorSent = false;
+ } catch (Exception exc) {
+ handleNetworkDown(exc);
+ } finally {
+ testMethod.releaseConnection();
+ }
+
+ }
+
+ private static void handleNetworkDown(Exception exc) throws DavMailException {
+ if (!checkNetwork() || configChecked) {
+ ExchangeSession.LOGGER.warn(BundleMessage.formatLog("EXCEPTION_NETWORK_DOWN"));
+ // log full stack trace for unknown errors
+ if (!((exc instanceof UnknownHostException)||(exc instanceof NetworkDownException))) {
+ ExchangeSession.LOGGER.debug(exc, exc);
+ }
+ throw new NetworkDownException("EXCEPTION_NETWORK_DOWN");
+ } else {
+ BundleMessage message = new BundleMessage("EXCEPTION_CONNECT", exc.getClass().getName(), exc.getMessage());
+ if (errorSent) {
+ ExchangeSession.LOGGER.warn(message);
+ throw new NetworkDownException("EXCEPTION_DAVMAIL_CONFIGURATION", message);
+ } else {
+ // Mark that an error has been sent so you only get one
+ // error in a row (not a repeating string of errors).
+ errorSent = true;
+ ExchangeSession.LOGGER.error(message);
+ throw new DavMailException("EXCEPTION_DAVMAIL_CONFIGURATION", message);
+ }
+ }
+ }
+
+ /**
+ * Get user password from session pool for SASL authentication
+ *
+ * @param userName Exchange user name
+ * @return user password
+ */
+ public static String getUserPassword(String userName) {
+ String fullUserName = convertUserName(userName);
+ for (PoolKey poolKey : POOL_MAP.keySet()) {
+ if (poolKey.userName.equals(fullUserName)) {
+ return poolKey.password;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Check if at least one network interface is up and active (i.e. has an address)
+ *
+ * @return true if network available
+ */
+ static boolean checkNetwork() {
+ boolean up = false;
+ Enumeration<NetworkInterface> enumeration;
+ try {
+ enumeration = NetworkInterface.getNetworkInterfaces();
+ while (!up && enumeration.hasMoreElements()) {
+ NetworkInterface networkInterface = enumeration.nextElement();
+ //noinspection Since15
+ up = networkInterface.isUp() && !networkInterface.isLoopback()
+ && networkInterface.getInetAddresses().hasMoreElements();
+ }
+ } catch (NoSuchMethodError error) {
+ ExchangeSession.LOGGER.debug("Unable to test network interfaces (not available under Java 1.5)");
+ up = true;
+ } catch (SocketException exc) {
+ ExchangeSession.LOGGER.error("DavMail configuration exception: \n Error listing network interfaces " + exc.getMessage(), exc);
+ }
+ return up;
+ }
+
+ /**
+ * Reset config check status and clear session pool.
+ */
+ public static void reset() {
+ configChecked = false;
+ errorSent = false;
+ POOL_MAP.clear();
+ }
+}
diff --git a/src/java/davmail/exchange/ICSBufferedReader.java b/src/java/davmail/exchange/ICSBufferedReader.java
new file mode 100644
index 0000000..c98e573
--- /dev/null
+++ b/src/java/davmail/exchange/ICSBufferedReader.java
@@ -0,0 +1,72 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.Reader;
+
+/**
+ * ICS Buffered Reader.
+ * Read events by line, handle multiple line elements
+ */
+public class ICSBufferedReader extends BufferedReader {
+ protected String nextLine;
+ protected final StringBuilder currentLine = new StringBuilder(75);
+
+ /**
+ * Create an ICS reader on the provided reader
+ *
+ * @param in input reader
+ * @throws IOException on error
+ */
+ public ICSBufferedReader(Reader in) throws IOException {
+ super(in);
+ nextLine = super.readLine();
+ }
+
+ /**
+ * Read a line from input reader, unwrap long lines.
+ */
+ @Override
+ public String readLine() throws IOException {
+ if (nextLine == null) {
+ return null;
+ } else {
+ currentLine.setLength(0);
+ currentLine.append(nextLine);
+ nextLine = super.readLine();
+ while (nextLine != null && !(nextLine.length() == 0) &&
+ (nextLine.charAt(0) == ' ' || nextLine.charAt(0) == '\t'
+ // workaround for broken items with \n as first line character
+ || nextLine.charAt(0) == '\\'
+ // workaround for Exchange 2010 bug
+ || nextLine.charAt(0) == ':')) {
+ // Timezone ends with \n => next line starts with :
+ if (nextLine.charAt(0) == ':') {
+ currentLine.append(nextLine);
+ } else {
+ currentLine.append(nextLine.substring(1));
+ }
+ nextLine = super.readLine();
+ }
+ return currentLine.toString();
+ }
+ }
+}
diff --git a/src/java/davmail/exchange/ICSBufferedWriter.java b/src/java/davmail/exchange/ICSBufferedWriter.java
new file mode 100644
index 0000000..a6cae56
--- /dev/null
+++ b/src/java/davmail/exchange/ICSBufferedWriter.java
@@ -0,0 +1,122 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange;
+
+/**
+ * ICS String writer.
+ * split lines longer than 75 characters
+ */
+public class ICSBufferedWriter {
+ final StringBuilder buffer = new StringBuilder();
+
+ /**
+ * Write content to buffer, do not split lines.
+ *
+ * @param content ics content
+ */
+ public void write(String content) {
+ if (content != null) {
+ buffer.append(content);
+ }
+ }
+
+ /**
+ * Write line to buffer, split lines at 75 characters.
+ *
+ * @param line ics event line
+ */
+ public void writeLine(String line) {
+ writeLine(line, false);
+ }
+
+ /**
+ * Write line with or without continuation prefix.
+ *
+ * @param line line content
+ * @param prefix continuation flag
+ */
+ public void writeLine(String line, boolean prefix) {
+ int maxLength = 77;
+ if (prefix) {
+ maxLength--;
+ buffer.append(' ');
+ }
+ if (line.length() > maxLength) {
+ buffer.append(line.substring(0, maxLength));
+ newLine();
+ writeLine(line.substring(maxLength), true);
+ } else {
+ buffer.append(line);
+ newLine();
+ }
+ }
+
+ /**
+ * Append CRLF.
+ */
+ public void newLine() {
+ buffer.append((char) 13).append((char) 10);
+ }
+
+ /**
+ * Get buffer as String
+ *
+ * @return ICS content as String
+ */
+ @Override
+ public String toString() {
+ return buffer.toString();
+ }
+
+ /**
+ * Append single value property
+ *
+ * @param propertyName property name
+ * @param propertyValue property value
+ */
+ public void appendProperty(String propertyName, String propertyValue) {
+ if ((propertyValue != null) && (propertyValue.length() > 0)) {
+ StringBuilder lineBuffer = new StringBuilder();
+ lineBuffer.append(propertyName);
+ lineBuffer.append(':');
+ appendMultilineEncodedValue(lineBuffer, propertyValue);
+ writeLine(lineBuffer.toString());
+ }
+
+ }
+
+ /**
+ * Append and encode \n to \\n in value.
+ *
+ * @param buffer line buffer
+ * @param value value
+ */
+ protected void appendMultilineEncodedValue(StringBuilder buffer, String value) {
+ for (int i = 0; i < value.length(); i++) {
+ char c = value.charAt(i);
+ if (c == '\n') {
+ buffer.append("\\n");
+ // skip carriage return
+ } else if (c != '\r') {
+ buffer.append(value.charAt(i));
+ }
+ }
+ }
+
+}
diff --git a/src/java/davmail/exchange/MimeOutputStreamWriter.java b/src/java/davmail/exchange/MimeOutputStreamWriter.java
new file mode 100644
index 0000000..609353d
--- /dev/null
+++ b/src/java/davmail/exchange/MimeOutputStreamWriter.java
@@ -0,0 +1,93 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange;
+
+import javax.mail.internet.MimeUtility;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * Mime OutputStreamWriter to build in memory Mime message.
+ */
+public class MimeOutputStreamWriter extends OutputStreamWriter {
+ /**
+ * Build MIME outputStreamWriter
+ *
+ * @param out outputstream
+ * @throws UnsupportedEncodingException on error
+ */
+ public MimeOutputStreamWriter(OutputStream out) throws UnsupportedEncodingException {
+ super(out, "ASCII");
+ }
+
+ /**
+ * Write MIME header
+ *
+ * @param header header name
+ * @param value header value
+ * @throws IOException on error
+ */
+ public void writeHeader(String header, String value) throws IOException {
+ // do not write empty headers
+ if (value != null && value.length() > 0) {
+ write(header);
+ write(": ");
+ write(MimeUtility.encodeText(value, "UTF-8", null));
+ writeLn();
+ }
+ }
+
+ /**
+ * Write MIME header
+ *
+ * @param header header name
+ * @param value header value
+ * @throws IOException on error
+ */
+ public void writeHeader(String header, Date value) throws IOException {
+ SimpleDateFormat formatter = new SimpleDateFormat("EEE, dd MMM yyyy hh:mm:ss Z", Locale.ENGLISH);
+ writeHeader(header, formatter.format(value));
+ }
+
+ /**
+ * Write line.
+ *
+ * @param line line content
+ * @throws IOException on error
+ */
+ public void writeLn(String line) throws IOException {
+ write(line);
+ write("\r\n");
+ }
+
+ /**
+ * Write CRLF.
+ *
+ * @throws IOException on error
+ */
+ public void writeLn() throws IOException {
+ write("\r\n");
+ }
+
+}
diff --git a/src/java/davmail/exchange/NetworkDownException.java b/src/java/davmail/exchange/NetworkDownException.java
new file mode 100644
index 0000000..e5ab7dc
--- /dev/null
+++ b/src/java/davmail/exchange/NetworkDownException.java
@@ -0,0 +1,45 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange;
+
+import davmail.exception.DavMailException;
+
+/**
+ * Custom exception to mark network down case.
+ */
+public class NetworkDownException extends DavMailException {
+ /**
+ * Build a network down exception with the provided BundleMessage key.
+ *
+ * @param key message key
+ */
+ public NetworkDownException(String key) {
+ super(key);
+ }
+
+ /**
+ * Build a network down exception with the provided BundleMessage key.
+ *
+ * @param key message key
+ * @param message detailed message
+ */
+ public NetworkDownException(String key, Object message) {
+ super(key, message);
+ }
+}
diff --git a/src/java/davmail/exchange/VCalendar.java b/src/java/davmail/exchange/VCalendar.java
new file mode 100644
index 0000000..3dfe1d5
--- /dev/null
+++ b/src/java/davmail/exchange/VCalendar.java
@@ -0,0 +1,741 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange;
+
+import davmail.Settings;
+import davmail.util.StringUtil;
+import org.apache.log4j.Logger;
+
+import java.io.*;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+/**
+ * VCalendar object.
+ */
+public class VCalendar extends VObject {
+ protected static final Logger LOGGER = Logger.getLogger(VCalendar.class);
+ protected VObject firstVevent;
+ protected VObject vTimezone;
+ protected String email;
+
+ /**
+ * Create VCalendar object from reader;
+ *
+ * @param reader stream reader
+ * @param email current user email
+ * @param vTimezone user OWA timezone
+ * @throws IOException on error
+ */
+ public VCalendar(BufferedReader reader, String email, VObject vTimezone) throws IOException {
+ super(reader);
+ if (!"VCALENDAR".equals(type)) {
+ throw new IOException("Invalid type: " + type);
+ }
+ this.email = email;
+ // set OWA timezone information
+ if (this.vTimezone == null && vTimezone != null) {
+ setTimezone(vTimezone);
+ }
+ }
+
+ /**
+ * Create VCalendar object from string;
+ *
+ * @param vCalendarBody item body
+ * @param email current user email
+ * @param vTimezone user OWA timezone
+ * @throws IOException on error
+ */
+ public VCalendar(String vCalendarBody, String email, VObject vTimezone) throws IOException {
+ this(new ICSBufferedReader(new StringReader(vCalendarBody)), email, vTimezone);
+ }
+
+ /**
+ * Create VCalendar object from string;
+ *
+ * @param vCalendarContent item content
+ * @param email current user email
+ * @param vTimezone user OWA timezone
+ * @throws IOException on error
+ */
+ public VCalendar(byte[] vCalendarContent, String email, VObject vTimezone) throws IOException {
+ this(new ICSBufferedReader(new InputStreamReader(new ByteArrayInputStream(vCalendarContent), "UTF-8")), email, vTimezone);
+ }
+
+ /**
+ * Empty constructor
+ */
+ public VCalendar() {
+ type = "VCALENDAR";
+ }
+
+ /**
+ * Set timezone on vObject
+ *
+ * @param vTimezone timezone object
+ */
+ public void setTimezone(VObject vTimezone) {
+ if (vObjects == null) {
+ addVObject(vTimezone);
+ } else {
+ vObjects.add(0, vTimezone);
+ }
+ this.vTimezone = vTimezone;
+ }
+
+ @Override
+ public void addVObject(VObject vObject) {
+ super.addVObject(vObject);
+ if (firstVevent == null && ("VEVENT".equals(vObject.type) || "VTODO".equals(vObject.type))) {
+ firstVevent = vObject;
+ }
+ if ("VTIMEZONE".equals(vObject.type)) {
+ vTimezone = vObject;
+ }
+ }
+
+ protected boolean isAllDay(VObject vObject) {
+ VProperty dtstart = vObject.getProperty("DTSTART");
+ return dtstart != null && dtstart.hasParam("VALUE", "DATE");
+ }
+
+ protected boolean isCdoAllDay(VObject vObject) {
+ return "TRUE".equals(vObject.getPropertyValue("X-MICROSOFT-CDO-ALLDAYEVENT"));
+ }
+
+ /**
+ * Check if vCalendar is CDO allday.
+ *
+ * @return true if vCalendar has X-MICROSOFT-CDO-ALLDAYEVENT property set to TRUE
+ */
+ public boolean isCdoAllDay() {
+ return firstVevent != null && isCdoAllDay(firstVevent);
+ }
+
+ /**
+ * Get email from property value.
+ *
+ * @param property property
+ * @return email value
+ */
+ public String getEmailValue(VProperty property) {
+ if (property == null) {
+ return null;
+ }
+ String propertyValue = property.getValue();
+ if (propertyValue != null && (propertyValue.startsWith("MAILTO:") || propertyValue.startsWith("mailto:"))) {
+ return propertyValue.substring(7);
+ } else {
+ return propertyValue;
+ }
+ }
+
+ protected String getMethod() {
+ return getPropertyValue("METHOD");
+ }
+
+ protected void fixVCalendar(boolean fromServer) {
+ // set iCal 4 global X-CALENDARSERVER-ACCESS from CLASS
+ if (fromServer) {
+ setPropertyValue("X-CALENDARSERVER-ACCESS", getCalendarServerAccess());
+ }
+
+ // iCal 4 global X-CALENDARSERVER-ACCESS
+ String calendarServerAccess = getPropertyValue("X-CALENDARSERVER-ACCESS");
+ String now = ExchangeSession.getZuluDateFormat().format(new Date());
+
+ // fix method from iPhone
+ if (!fromServer && getPropertyValue("METHOD") == null) {
+ setPropertyValue("METHOD", "PUBLISH");
+ }
+
+ // rename TZID for maximum iCal/iPhone compatibility
+ String tzid = null;
+ if (fromServer) {
+ // get current tzid
+ VObject vObject = vTimezone;
+ if (vObject != null) {
+ String currentTzid = vObject.getPropertyValue("TZID");
+ // fix TZID with \n (Exchange 2010 bug)
+ if (currentTzid != null && currentTzid.endsWith("\n")) {
+ currentTzid = currentTzid.substring(0, currentTzid.length() - 1);
+ vObject.setPropertyValue("TZID", currentTzid);
+ }
+ if (currentTzid != null && currentTzid.indexOf(' ') >= 0) {
+ try {
+ tzid = ResourceBundle.getBundle("timezones").getString(currentTzid);
+ vObject.setPropertyValue("TZID", tzid);
+ } catch (MissingResourceException e) {
+ LOGGER.debug("Timezone " + currentTzid + " not found in rename table");
+ }
+ }
+ }
+ }
+
+ if (!fromServer) {
+ fixTimezone();
+ }
+
+ // iterate over vObjects
+ for (VObject vObject : vObjects) {
+ if ("VEVENT".equals(vObject.type)) {
+ if (calendarServerAccess != null) {
+ vObject.setPropertyValue("CLASS", getEventClass(calendarServerAccess));
+ // iCal 3, get X-CALENDARSERVER-ACCESS from local VEVENT
+ } else if (vObject.getPropertyValue("X-CALENDARSERVER-ACCESS") != null) {
+ vObject.setPropertyValue("CLASS", getEventClass(vObject.getPropertyValue("X-CALENDARSERVER-ACCESS")));
+ }
+ if (fromServer) {
+ // remove organizer line for event without attendees for iPhone
+ if (vObject.getProperty("ATTENDEE") == null) {
+ vObject.setPropertyValue("ORGANIZER", null);
+ }
+ // detect allday and update date properties
+ if (isCdoAllDay(vObject)) {
+ setClientAllday(vObject.getProperty("DTSTART"));
+ setClientAllday(vObject.getProperty("DTEND"));
+ }
+ String cdoBusyStatus = vObject.getPropertyValue("X-MICROSOFT-CDO-BUSYSTATUS");
+ if (cdoBusyStatus != null) {
+ vObject.setPropertyValue("TRANSP",
+ !"FREE".equals(cdoBusyStatus) ? "OPAQUE" : "TRANSPARENT");
+ }
+
+ // Apple iCal doesn't understand this key, and it's entourage
+ // specific (i.e. not needed by any caldav client): strip it out
+ vObject.removeProperty("X-ENTOURAGE_UUID");
+
+ splitExDate(vObject);
+
+ // remove empty properties
+ if ("".equals(vObject.getPropertyValue("LOCATION"))) {
+ vObject.removeProperty("LOCATION");
+ }
+ if ("".equals(vObject.getPropertyValue("DESCRIPTION"))) {
+ vObject.removeProperty("DESCRIPTION");
+ }
+ if ("".equals(vObject.getPropertyValue("CLASS"))) {
+ vObject.removeProperty("CLASS");
+ }
+ // rename TZID
+ if (tzid != null) {
+ VProperty dtStart = vObject.getProperty("DTSTART");
+ if (dtStart != null && dtStart.getParam("TZID") != null) {
+ dtStart.setParam("TZID", tzid);
+ }
+ VProperty dtEnd = vObject.getProperty("DTEND");
+ if (dtEnd != null && dtStart.getParam("TZID") != null) {
+ dtEnd.setParam("TZID", tzid);
+ }
+ }
+ // remove unsupported attachment reference
+ if (vObject.getProperty("ATTACH") != null) {
+ List<String> toRemoveValues = null;
+ List<String> values = vObject.getProperty("ATTACH").getValues();
+ for (String value : values) {
+ if (value.indexOf("CID:") >= 0) {
+ if (toRemoveValues == null) {
+ toRemoveValues = new ArrayList<String>();
+ }
+ toRemoveValues.add(value);
+ }
+ }
+ if (toRemoveValues != null) {
+ values.removeAll(toRemoveValues);
+ if (values.size() == 0) {
+ vObject.removeProperty("ATTACH");
+ }
+ }
+ }
+ } else {
+ // add organizer line to all events created in Exchange for active sync
+ String organizer = getEmailValue(vObject.getProperty("ORGANIZER"));
+ if (organizer == null) {
+ vObject.setPropertyValue("ORGANIZER", "MAILTO:" + email);
+ } else if (!email.equalsIgnoreCase(organizer) && vObject.getProperty("X-MICROSOFT-CDO-REPLYTIME") == null) {
+ vObject.setPropertyValue("X-MICROSOFT-CDO-REPLYTIME", now);
+ }
+ // set OWA allday flag
+ vObject.setPropertyValue("X-MICROSOFT-CDO-ALLDAYEVENT", isAllDay(vObject) ? "TRUE" : "FALSE");
+ vObject.setPropertyValue("X-MICROSOFT-CDO-BUSYSTATUS",
+ !"TRANSPARENT".equals(vObject.getPropertyValue("TRANSP")) ? "BUSY" : "FREE");
+
+ if (isAllDay(vObject)) {
+ // convert date values to outlook compatible values
+ setServerAllday(vObject.getProperty("DTSTART"));
+ setServerAllday(vObject.getProperty("DTEND"));
+ } else {
+ fixTzid(vObject.getProperty("DTSTART"));
+ fixTzid(vObject.getProperty("DTEND"));
+ }
+ }
+
+ fixAttendees(vObject, fromServer);
+
+ fixAlarm(vObject, fromServer);
+ }
+ }
+
+ }
+
+ private void fixTimezone() {
+ if (vTimezone != null && vTimezone.vObjects != null && vTimezone.vObjects.size() > 2) {
+ VObject standard = null;
+ VObject daylight = null;
+ for (VObject vObject : vTimezone.vObjects) {
+ if ("STANDARD".equals(vObject.type)) {
+ if (standard == null ||
+ (vObject.getPropertyValue("DTSTART").compareTo(standard.getPropertyValue("DTSTART")) > 0)) {
+ standard = vObject;
+ }
+ }
+ if ("DAYLIGHT".equals(vObject.type)) {
+ if (daylight == null ||
+ (vObject.getPropertyValue("DTSTART").compareTo(daylight.getPropertyValue("DTSTART")) > 0)) {
+ daylight = vObject;
+ }
+ }
+ }
+ vTimezone.vObjects.clear();
+ vTimezone.vObjects.add(standard);
+ vTimezone.vObjects.add(daylight);
+ }
+ }
+
+ private void fixTzid(VProperty property) {
+ if (property != null && !property.hasParam("TZID")) {
+ property.addParam("TZID", vTimezone.getPropertyValue("TZID"));
+ }
+ }
+
+ protected void splitExDate(VObject vObject) {
+ List<VProperty> exDateProperties = vObject.getProperties("EXDATE");
+ if (exDateProperties != null) {
+ for (VProperty property : exDateProperties) {
+ String value = property.getValue();
+ if (value.indexOf(',') >= 0) {
+ // split property
+ vObject.removeProperty(property);
+ for (String singleValue : value.split(",")) {
+ VProperty singleProperty = new VProperty("EXDATE", singleValue);
+ singleProperty.setParams(property.getParams());
+ vObject.addProperty(singleProperty);
+ }
+ }
+ }
+ }
+ }
+
+ protected void setServerAllday(VProperty property) {
+ if (vTimezone != null) {
+ // set TZID param
+ if (!property.hasParam("TZID")) {
+ property.addParam("TZID", vTimezone.getPropertyValue("TZID"));
+ }
+ // remove VALUE
+ property.removeParam("VALUE");
+ String value = property.getValue();
+ if (value.length() != 8) {
+ LOGGER.warn("Invalid date value in allday event: " + value);
+ }
+ property.setValue(property.getValue() + "T000000");
+ }
+ }
+
+ protected void setClientAllday(VProperty property) {
+ if (property != null) {
+ // set VALUE=DATE param
+ if (!property.hasParam("VALUE")) {
+ property.addParam("VALUE", "DATE");
+ }
+ // remove TZID
+ property.removeParam("TZID");
+ String value = property.getValue();
+ if (value.length() != 8) {
+ // try to convert datetime value to date value
+ try {
+ Calendar calendar = Calendar.getInstance();
+ SimpleDateFormat dateParser = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
+ calendar.setTime(dateParser.parse(value));
+ calendar.add(Calendar.HOUR_OF_DAY, 12);
+ SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyyMMdd");
+ value = dateFormatter.format(calendar.getTime());
+ } catch (ParseException e) {
+ LOGGER.warn("Invalid date value in allday event: " + value);
+ }
+ }
+ property.setValue(value);
+ }
+ }
+
+ protected void fixAlarm(VObject vObject, boolean fromServer) {
+ if (vObject.vObjects != null) {
+ if (Settings.getBooleanProperty("davmail.caldavDisableReminders", false)) {
+ ArrayList<VObject> vAlarms = null;
+ for (VObject vAlarm : vObject.vObjects) {
+ if ("VALARM".equals(vAlarm.type)) {
+ if (vAlarms == null) {
+ vAlarms = new ArrayList<VObject>();
+ }
+ vAlarms.add(vAlarm);
+ }
+ }
+ // remove all vAlarms
+ if (vAlarms != null) {
+ for (VObject vAlarm : vAlarms) {
+ vObject.vObjects.remove(vAlarm);
+ }
+ }
+
+ } else {
+ for (VObject vAlarm : vObject.vObjects) {
+ if ("VALARM".equals(vAlarm.type)) {
+ String action = vAlarm.getPropertyValue("ACTION");
+ if (fromServer && "DISPLAY".equals(action)
+ // convert DISPLAY to AUDIO only if user defined an alarm sound
+ && Settings.getProperty("davmail.caldavAlarmSound") != null) {
+ // Convert alarm to audio for iCal
+ vAlarm.setPropertyValue("ACTION", "AUDIO");
+
+ if (vAlarm.getPropertyValue("ATTACH") == null) {
+ // Add defined sound into the audio alarm
+ VProperty vProperty = new VProperty("ATTACH", Settings.getProperty("davmail.caldavAlarmSound"));
+ vProperty.addParam("VALUE", "URI");
+ vAlarm.addProperty(vProperty);
+ }
+
+ } else if (!fromServer && "AUDIO".equals(action)) {
+ // Use the alarm action that exchange (and blackberry) understand
+ // (exchange and blackberry don't understand audio actions)
+ vAlarm.setPropertyValue("ACTION", "DISPLAY");
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Replace iCal4 (Snow Leopard) principal paths with mailto expression
+ *
+ * @param value attendee value or ics line
+ * @return fixed value
+ */
+ protected String replaceIcal4Principal(String value) {
+ if (value.contains("/principals/__uuids__/")) {
+ return value.replaceAll("/principals/__uuids__/([^/]*)__AT__([^/]*)/", "mailto:$1@$2");
+ } else {
+ return value;
+ }
+ }
+
+ private void fixAttendees(VObject vObject, boolean fromServer) {
+ if (vObject.properties != null) {
+ for (VProperty property : vObject.properties) {
+ if ("ATTENDEE".equalsIgnoreCase(property.getKey())) {
+ if (fromServer) {
+ // If this is coming from the server, strip out RSVP for this
+ // user as an attendee where the partstat is something other
+ // than PARTSTAT=NEEDS-ACTION since the RSVP confuses iCal4 into
+ // thinking the attendee has not replied
+ if (isCurrentUser(property) && property.hasParam("RSVP", "TRUE")) {
+ VProperty.Param partstat = property.getParam("PARTSTAT");
+ if (partstat == null || !"NEEDS-ACTION".equals(partstat.getValue())) {
+ property.removeParam("RSVP");
+ }
+ }
+ } else {
+ property.setValue(replaceIcal4Principal(property.getValue()));
+ }
+ }
+
+ }
+ }
+
+ }
+
+ private boolean isCurrentUser(VProperty property) {
+ return property.getValue().equalsIgnoreCase("mailto:" + email);
+ }
+
+ /**
+ * Return VTimezone object
+ *
+ * @return VTimezone
+ */
+ public VObject getVTimezone() {
+ return vTimezone;
+ }
+
+ /**
+ * Convert X-CALENDARSERVER-ACCESS to CLASS.
+ * see http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-privateevents.txt
+ *
+ * @param calendarServerAccess X-CALENDARSERVER-ACCESS value
+ * @return CLASS value
+ */
+ protected String getEventClass(String calendarServerAccess) {
+ if ("PRIVATE".equalsIgnoreCase(calendarServerAccess)) {
+ return "CONFIDENTIAL";
+ } else if ("CONFIDENTIAL".equalsIgnoreCase(calendarServerAccess) || "RESTRICTED".equalsIgnoreCase(calendarServerAccess)) {
+ return "PRIVATE";
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Convert CLASS to X-CALENDARSERVER-ACCESS.
+ * see http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-privateevents.txt *
+ *
+ * @return X-CALENDARSERVER-ACCESS value
+ */
+ protected String getCalendarServerAccess() {
+ String eventClass = getFirstVeventPropertyValue("CLASS");
+ if ("PRIVATE".equalsIgnoreCase(eventClass)) {
+ return "CONFIDENTIAL";
+ } else if ("CONFIDENTIAL".equalsIgnoreCase(eventClass)) {
+ return "PRIVATE";
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Get property value from first VEVENT in VCALENDAR.
+ *
+ * @param name property name
+ * @return property value
+ */
+ public String getFirstVeventPropertyValue(String name) {
+ if (firstVevent == null) {
+ return null;
+ } else {
+ return firstVevent.getPropertyValue(name);
+ }
+ }
+
+ protected VProperty getFirstVeventProperty(String name) {
+ if (firstVevent == null) {
+ return null;
+ } else {
+ return firstVevent.getProperty(name);
+ }
+ }
+
+
+ /**
+ * Get properties by name from first VEVENT.
+ *
+ * @param name property name
+ * @return properties
+ */
+ public List<VProperty> getFirstVeventProperties(String name) {
+ if (firstVevent == null) {
+ return null;
+ } else {
+ return firstVevent.getProperties(name);
+ }
+ }
+
+ /**
+ * Remove VAlarm from VCalendar.
+ */
+ public void removeVAlarm() {
+ if (vObjects != null) {
+ for (VObject vObject : vObjects) {
+ if ("VEVENT".equals(vObject.type)) {
+ // As VALARM is the only possible inner object, just drop all objects
+ if (vObject.vObjects != null) {
+ vObject.vObjects = null;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Check if VCalendar has a VALARM item.
+ *
+ * @return true if VCalendar has a VALARM
+ */
+ public boolean hasVAlarm() {
+ if (vObjects != null) {
+ for (VObject vObject : vObjects) {
+ if ("VEVENT".equals(vObject.type)) {
+ if (vObject.vObjects != null) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Check if this VCalendar is a meeting.
+ *
+ * @return true if this VCalendar has attendees
+ */
+ public boolean isMeeting() {
+ return getFirstVeventProperty("ATTENDEE") != null;
+ }
+
+ /**
+ * Check if current user is meeting organizer.
+ *
+ * @return true it user email matched organizer email
+ */
+ public boolean isMeetingOrganizer() {
+ return email.equalsIgnoreCase(getEmailValue(getFirstVeventProperty("ORGANIZER")));
+ }
+
+ /**
+ * Set property value on first VEVENT.
+ *
+ * @param propertyName property name
+ * @param propertyValue property value
+ */
+ public void setFirstVeventPropertyValue(String propertyName, String propertyValue) {
+ firstVevent.setPropertyValue(propertyName, propertyValue);
+ }
+
+ /**
+ * Add property on first VEVENT.
+ *
+ * @param vProperty property object
+ */
+ public void addFirstVeventProperty(VProperty vProperty) {
+ firstVevent.addProperty(vProperty);
+ }
+
+ /**
+ * Check if this item is a VTODO item
+ *
+ * @return true with VTODO items
+ */
+ public boolean isTodo() {
+ return "VTODO".equals(firstVevent.type);
+ }
+
+ /**
+ * VCalendar recipients for notifications
+ */
+ public static class Recipients {
+ /**
+ * attendee list
+ */
+ public String attendees;
+
+ /**
+ * optional attendee list
+ */
+ public String optionalAttendees;
+
+ /**
+ * vCalendar organizer
+ */
+ public String organizer;
+ }
+
+ /**
+ * Build recipients value for VCalendar.
+ *
+ * @param isNotification if true, filter recipients that should receive meeting notifications
+ * @return notification/event recipients
+ */
+ public Recipients getRecipients(boolean isNotification) {
+
+ HashSet<String> attendees = new HashSet<String>();
+ HashSet<String> optionalAttendees = new HashSet<String>();
+
+ // get recipients from first VEVENT
+ List<VProperty> attendeeProperties = getFirstVeventProperties("ATTENDEE");
+ if (attendeeProperties != null) {
+ for (VProperty property : attendeeProperties) {
+ // exclude current user and invalid values from recipients
+ // also exclude no action attendees
+ String attendeeEmail = getEmailValue(property);
+ if (!email.equalsIgnoreCase(attendeeEmail) && attendeeEmail != null && attendeeEmail.indexOf('@') >= 0
+ // return all attendees for user calendar folder, filter for notifications
+ && (!isNotification
+ // notify attendee if reply explicitly requested
+ || (property.hasParam("RSVP", "TRUE"))
+ || (
+ // workaround for iCal bug: do not notify if reply explicitly not requested
+ !(property.hasParam("RSVP", "FALSE")) &&
+ ((property.hasParam("PARTSTAT", "NEEDS-ACTION")
+ // need to include other PARTSTATs participants for CANCEL notifications
+ || property.hasParam("PARTSTAT", "ACCEPTED")
+ || property.hasParam("PARTSTAT", "DECLINED")
+ || property.hasParam("PARTSTAT", "TENTATIVE")))
+ ))) {
+ if (property.hasParam("ROLE", "OPT-PARTICIPANT")) {
+ optionalAttendees.add(attendeeEmail);
+ } else {
+ attendees.add(attendeeEmail);
+ }
+ }
+ }
+ }
+ Recipients recipients = new Recipients();
+ recipients.organizer = getEmailValue(getFirstVeventProperty("ORGANIZER"));
+ recipients.attendees = StringUtil.join(attendees, ", ");
+ recipients.optionalAttendees = StringUtil.join(optionalAttendees, ", ");
+ return recipients;
+ }
+
+ protected String getAttendeeStatus() {
+ String status = null;
+ List<VProperty> attendeeProperties = getFirstVeventProperties("ATTENDEE");
+ if (attendeeProperties != null) {
+ for (VProperty property : attendeeProperties) {
+ String attendeeEmail = getEmailValue(property);
+ if (email.equalsIgnoreCase(attendeeEmail) && property.hasParam("PARTSTAT")) {
+ // found current user attendee line
+ status = property.getParam("PARTSTAT").getValue();
+ break;
+ }
+ }
+ }
+ return status;
+ }
+
+ /**
+ * Get recurring VCalendar occurence exceptions.
+ *
+ * @return event occurences
+ */
+ public List<VObject> getModifiedOccurrences() {
+ boolean first = true;
+ ArrayList<VObject> results = new ArrayList<VObject>();
+ for (VObject vObject : vObjects) {
+ if ("VEVENT".equals(vObject.type)) {
+ if (first) {
+ first = false;
+ } else {
+ results.add(vObject);
+ }
+ }
+ }
+ return results;
+ }
+}
diff --git a/src/java/davmail/exchange/VCardWriter.java b/src/java/davmail/exchange/VCardWriter.java
new file mode 100644
index 0000000..cd67b0c
--- /dev/null
+++ b/src/java/davmail/exchange/VCardWriter.java
@@ -0,0 +1,92 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange;
+
+/**
+ * VCard Writer
+ */
+public class VCardWriter extends ICSBufferedWriter {
+ /**
+ * Begin VCard and version
+ */
+ public void startCard() {
+ writeLine("BEGIN:VCARD");
+ writeLine("VERSION:3.0");
+ }
+
+ /**
+ * Append compound value
+ *
+ * @param propertyName property name
+ * @param propertyValue property values
+ */
+ public void appendProperty(String propertyName, String... propertyValue) {
+ boolean hasValue = false;
+ for (String value : propertyValue) {
+ if ((value != null) && (value.length() > 0)) {
+ hasValue = true;
+ break;
+ }
+ }
+ if (hasValue) {
+ boolean first = true;
+ StringBuilder lineBuffer = new StringBuilder();
+ lineBuffer.append(propertyName);
+ lineBuffer.append(':');
+ for (String value : propertyValue) {
+ if (first) {
+ first = false;
+ } else {
+ lineBuffer.append(';');
+ }
+ appendEncodedValue(lineBuffer, value);
+ }
+ writeLine(lineBuffer.toString());
+ }
+ }
+
+ /**
+ * Encode and append value to buffer
+ *
+ * @param buffer current buffer
+ * @param value property value
+ */
+ private void appendEncodedValue(StringBuilder buffer, String value) {
+ if (value != null) {
+ for (int i = 0; i < value.length(); i++) {
+ char c = value.charAt(i);
+ if (c == ',' || c == ';') {
+ buffer.append('\\');
+ }
+ if (c == '\n') {
+ buffer.append("\\n");
+ } else {
+ buffer.append(value.charAt(i));
+ }
+ }
+ }
+ }
+
+ /**
+ * End VCard
+ */
+ public void endCard() {
+ writeLine("END:VCARD");
+ }
+}
diff --git a/src/java/davmail/exchange/VObject.java b/src/java/davmail/exchange/VObject.java
new file mode 100644
index 0000000..f6fb76f
--- /dev/null
+++ b/src/java/davmail/exchange/VObject.java
@@ -0,0 +1,282 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Base class for VCalendar, VTimezone, VEvent.
+ */
+public class VObject {
+ /**
+ * VObject properties
+ */
+ ArrayList<VProperty> properties;
+ /**
+ * Inner VObjects (e.g. VEVENT, VALARM, ...)
+ */
+ ArrayList<VObject> vObjects;
+ /**
+ * Object base name (VCALENDAR, VEVENT, VCARD...).
+ */
+ public String type;
+
+ /**
+ * Create VObject with given type
+ *
+ * @param beginProperty first line property
+ * @param reader stream reader just after the BEGIN:TYPE line
+ * @throws IOException on error
+ */
+ public VObject(VProperty beginProperty, BufferedReader reader) throws IOException {
+ if (!"BEGIN".equals(beginProperty.getKey())) {
+ throw new IOException("Invalid first line: " + beginProperty);
+ }
+ type = beginProperty.getValue();
+ String endLine = "END:" + type;
+ String line = reader.readLine();
+ while (line != null && !line.startsWith(endLine)) {
+ handleLine(line, reader);
+ line = reader.readLine();
+ }
+ if (line == null) {
+ throw new IOException("Unexpected end of stream");
+ }
+ }
+
+ /**
+ * Create VObject from reader.
+ *
+ * @param reader stream reader just after the BEGIN:TYPE line
+ * @throws IOException on error
+ */
+ public VObject(BufferedReader reader) throws IOException {
+ this(new VProperty(reader.readLine()), reader);
+ }
+
+ /**
+ * Create VCalendar object from string;
+ *
+ * @param itemBody item body
+ * @throws IOException on error
+ */
+ public VObject(String itemBody) throws IOException {
+ this(new ICSBufferedReader(new StringReader(itemBody)));
+ }
+
+ /**
+ * Create empty VCalendar object;
+ */
+ public VObject() {
+ }
+
+
+ protected void handleLine(String line, BufferedReader reader) throws IOException {
+ // skip empty lines
+ if (line.length() > 0) {
+ VProperty property = new VProperty(line);
+ // inner object
+ if ("BEGIN".equals(property.getKey())) {
+ addVObject(new VObject(property, reader));
+ } else if (property.getKey() != null) {
+ addProperty(property);
+ }
+ }
+ }
+
+ /**
+ * Add vObject.
+ *
+ * @param vObject inner object
+ */
+ public void addVObject(VObject vObject) {
+ if (vObjects == null) {
+ vObjects = new ArrayList<VObject>();
+ }
+ vObjects.add(vObject);
+ }
+
+ /**
+ * Add vProperty.
+ *
+ * @param property vProperty
+ */
+ public void addProperty(VProperty property) {
+ if (property.getValue() != null) {
+ if (properties == null) {
+ properties = new ArrayList<VProperty>();
+ }
+ properties.add(property);
+ }
+ }
+
+ /**
+ * Write VObject to writer.
+ *
+ * @param writer buffered writer
+ */
+ public void writeTo(ICSBufferedWriter writer) {
+ writer.write("BEGIN:");
+ writer.writeLine(type);
+ if (properties != null) {
+ for (VProperty property : properties) {
+ writer.writeLine(property.toString());
+ }
+ }
+ if (vObjects != null) {
+ for (VObject object : vObjects) {
+ object.writeTo(writer);
+ }
+ }
+ writer.write("END:");
+ writer.writeLine(type);
+ }
+
+ public String toString() {
+ ICSBufferedWriter writer = new ICSBufferedWriter();
+ writeTo(writer);
+ return writer.toString();
+ }
+
+ /**
+ * Get VObject properties
+ *
+ * @return properties
+ */
+ public List<VProperty> getProperties() {
+ return properties;
+ }
+
+ /**
+ * Get vProperty by name.
+ *
+ * @param name property name
+ * @return property object
+ */
+ public VProperty getProperty(String name) {
+ if (properties != null) {
+ for (VProperty property : properties) {
+ if (property.getKey() != null && property.getKey().equalsIgnoreCase(name)) {
+ return property;
+ }
+ }
+
+ }
+ return null;
+ }
+
+ /**
+ * Get multivalued vProperty by name.
+ *
+ * @param name property name
+ * @return property list
+ */
+ public List<VProperty> getProperties(String name) {
+ List<VProperty> result = null;
+ if (properties != null) {
+ for (VProperty property : properties) {
+ if (property.getKey() != null && property.getKey().equalsIgnoreCase(name)) {
+ if (result == null) {
+ result = new ArrayList<VProperty>();
+ }
+ result.add(property);
+ }
+ }
+
+ }
+ return result;
+ }
+
+ /**
+ * Get vProperty value by name.
+ *
+ * @param name property name
+ * @return property value
+ */
+ public String getPropertyValue(String name) {
+ VProperty property = getProperty(name);
+ if (property != null) {
+ return property.getValue();
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Set vProperty value on vObject, remove property if value is null.
+ *
+ * @param name property name
+ * @param value property value
+ */
+ public void setPropertyValue(String name, String value) {
+ if (value == null) {
+ removeProperty(name);
+ } else {
+ VProperty property = getProperty(name);
+ if (property == null) {
+ property = new VProperty(name, value);
+ addProperty(property);
+ } else {
+ property.setValue(value);
+ }
+ }
+ }
+
+ /**
+ * Add vProperty value on vObject.
+ *
+ * @param name property name
+ * @param value property value
+ */
+ public void addPropertyValue(String name, String value) {
+ if (value != null) {
+ VProperty property = new VProperty(name, value);
+ addProperty(property);
+ }
+ }
+
+ /**
+ * Remove vProperty from vObject.
+ *
+ * @param name property name
+ */
+ public void removeProperty(String name) {
+ if (properties != null) {
+ VProperty property = getProperty(name);
+ if (property != null) {
+ properties.remove(property);
+ }
+ }
+ }
+
+ /**
+ * Remove vProperty object from vObject.
+ *
+ * @param property object
+ */
+ public void removeProperty(VProperty property) {
+ if (properties != null) {
+ properties.remove(property);
+ }
+ }
+}
diff --git a/src/java/davmail/exchange/VProperty.java b/src/java/davmail/exchange/VProperty.java
new file mode 100644
index 0000000..c03cf5b
--- /dev/null
+++ b/src/java/davmail/exchange/VProperty.java
@@ -0,0 +1,452 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * VCard property
+ */
+public class VProperty {
+ protected static enum State {
+ KEY, PARAM_NAME, PARAM_VALUE, QUOTED_PARAM_VALUE, VALUE, BACKSLASH
+ }
+
+ protected static final HashSet<String> MULTIVALUED_PROPERTIES = new HashSet<String>();
+ static {
+ MULTIVALUED_PROPERTIES.add("RESOURCES");
+ MULTIVALUED_PROPERTIES.add("LOCATION");
+ }
+
+ protected static class Param {
+ String name;
+ List<String> values;
+
+ public void addAll(List<String> paramValues) {
+ if (values == null) {
+ values = new ArrayList<String>();
+ }
+ values.addAll(paramValues);
+ }
+
+ public String getValue() {
+ if (values != null && !values.isEmpty()) {
+ return values.get(0);
+ } else {
+ return null;
+ }
+ }
+ }
+
+ protected String key;
+ protected List<Param> params;
+ protected List<String> values;
+
+ /**
+ * Create VProperty for key and value.
+ *
+ * @param name property name
+ * @param value property value
+ */
+ public VProperty(String name, String value) {
+ setKey(name);
+ setValue(value);
+ }
+
+ /**
+ * Create VProperty from line.
+ *
+ * @param line card line
+ */
+ public VProperty(String line) {
+ if (line != null && !"END:VCARD".equals(line)) {
+ State state = State.KEY;
+ String paramName = null;
+ List<String> paramValues = null;
+ int startIndex = 0;
+ for (int i = 0; i < line.length(); i++) {
+ char currentChar = line.charAt(i);
+ if (state == State.KEY) {
+ if (currentChar == ':') {
+ setKey(line.substring(startIndex, i));
+ state = State.VALUE;
+ startIndex = i + 1;
+ } else if (currentChar == ';') {
+ setKey(line.substring(startIndex, i));
+ state = State.PARAM_NAME;
+ startIndex = i + 1;
+ }
+ } else if (state == State.PARAM_NAME) {
+ if (currentChar == '=') {
+ paramName = line.substring(startIndex, i).toUpperCase();
+ state = State.PARAM_VALUE;
+ paramValues = new ArrayList<String>();
+ startIndex = i + 1;
+ } else if (currentChar == ';') {
+ // param with no value
+ paramName = line.substring(startIndex, i).toUpperCase();
+ addParam(paramName);
+ state = State.PARAM_NAME;
+ startIndex = i + 1;
+ } else if (currentChar == ':') {
+ // param with no value
+ paramName = line.substring(startIndex, i).toUpperCase();
+ addParam(paramName);
+ state = State.VALUE;
+ startIndex = i + 1;
+ }
+ } else if (state == State.PARAM_VALUE) {
+ if (currentChar == '"') {
+ state = State.QUOTED_PARAM_VALUE;
+ startIndex = i + 1;
+ } else if (currentChar == ':') {
+ if (startIndex < i) {
+ paramValues.add(line.substring(startIndex, i));
+ }
+ addParam(paramName, paramValues);
+ state = State.VALUE;
+ startIndex = i + 1;
+ } else if (currentChar == ';') {
+ if (startIndex < i) {
+ paramValues.add(line.substring(startIndex, i));
+ }
+ addParam(paramName, paramValues);
+ state = State.PARAM_NAME;
+ startIndex = i + 1;
+ } else if (currentChar == ',') {
+ if (startIndex < i) {
+ paramValues.add(line.substring(startIndex, i));
+ }
+ startIndex = i + 1;
+ }
+ } else if (state == State.QUOTED_PARAM_VALUE) {
+ if (currentChar == '"') {
+ state = State.PARAM_VALUE;
+ paramValues.add(line.substring(startIndex, i));
+ startIndex = i + 1;
+ }
+ } else if (state == State.VALUE) {
+ if (currentChar == '\\') {
+ state = State.BACKSLASH;
+ } else if (currentChar == ';' || (MULTIVALUED_PROPERTIES.contains(key) && currentChar == ',')) {
+ addValue(line.substring(startIndex, i));
+ startIndex = i + 1;
+ }
+ } else if (state == State.BACKSLASH) {
+ state = State.VALUE;
+ }
+ }
+ if (state == State.VALUE) {
+ addValue(line.substring(startIndex));
+ } else {
+ throw new IllegalArgumentException("Invalid property line: " + line);
+ }
+ }
+ }
+
+ /**
+ * Property key, without optional parameters (e.g. TEL).
+ *
+ * @return key
+ */
+ public String getKey() {
+ return key;
+ }
+
+ /**
+ * Property value.
+ *
+ * @return value
+ */
+ public String getValue() {
+ if (values == null || values.isEmpty()) {
+ return null;
+ } else {
+ return values.get(0);
+ }
+ }
+
+ /**
+ * Property values.
+ *
+ * @return values
+ */
+ public List<String> getValues() {
+ return values;
+ }
+
+ /**
+ * Test if the property has a param named paramName with given value.
+ *
+ * @param paramName param name
+ * @param paramValue param value
+ * @return true if property has param name and value
+ */
+ public boolean hasParam(String paramName, String paramValue) {
+ return params != null && getParam(paramName) != null && containsIgnoreCase(getParam(paramName).values, paramValue);
+ }
+
+ /**
+ * Test if the property has a param named paramName.
+ *
+ * @param paramName param name
+ * @return true if property has param name
+ */
+ public boolean hasParam(String paramName) {
+ return params != null && getParam(paramName) != null;
+ }
+
+ /**
+ * Remove param from property.
+ *
+ * @param paramName param name
+ */
+ public void removeParam(String paramName) {
+ if (params != null) {
+ Param param = getParam(paramName);
+ if (param != null) {
+ params.remove(param);
+ }
+ }
+ }
+
+ protected boolean containsIgnoreCase(List<String> stringCollection, String value) {
+ for (String collectionValue : stringCollection) {
+ if (value.equalsIgnoreCase(collectionValue)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ protected void addParam(String paramName) {
+ addParam(paramName, (String) null);
+ }
+
+ /**
+ * Set param value on property.
+ *
+ * @param paramName param name
+ * @param paramValue param value
+ */
+ public void setParam(String paramName, String paramValue) {
+ Param currentParam = getParam(paramName);
+ if (currentParam != null) {
+ getParams().remove(currentParam);
+ }
+ addParam(paramName, paramValue);
+ }
+
+ /**
+ * Add param value on property.
+ *
+ * @param paramName param name
+ * @param paramValue param value
+ */
+ public void addParam(String paramName, String paramValue) {
+ List<String> paramValues = new ArrayList<String>();
+ paramValues.add(paramValue);
+ addParam(paramName, paramValues);
+ }
+
+ protected void addParam(String paramName, List<String> paramValues) {
+ if (params == null) {
+ params = new ArrayList<Param>();
+ }
+ Param currentParam = getParam(paramName);
+ if (currentParam == null) {
+ currentParam = new Param();
+ currentParam.name = paramName;
+ params.add(currentParam);
+ }
+ currentParam.addAll(paramValues);
+ }
+
+ protected Param getParam(String paramName) {
+ if (params != null) {
+ for (Param param : params) {
+ if (paramName.equals(param.name)) {
+ return param;
+ }
+ }
+ }
+ return null;
+ }
+
+ public String getParamValue(String paramName) {
+ Param param = getParam(paramName);
+ if (param != null) {
+ return param.getValue();
+ } else {
+ return null;
+ }
+ }
+
+ protected List<Param> getParams() {
+ return params;
+ }
+
+ protected void setParams(List<Param> params) {
+ this.params = params;
+ }
+
+ protected void setValue(String value) {
+ if (value == null) {
+ values = null;
+ } else {
+ if (values == null) {
+ values = new ArrayList<String>();
+ } else {
+ values.clear();
+ }
+ values.add(decodeValue(value));
+ }
+ }
+
+ protected void addValue(String value) {
+ if (values == null) {
+ values = new ArrayList<String>();
+ }
+ values.add(decodeValue(value));
+ }
+
+ protected String decodeValue(String value) {
+ if (value == null || (value.indexOf('\\') < 0 && value.indexOf(',') < 0)) {
+ return value;
+ } else {
+ // decode value
+ StringBuilder decodedValue = new StringBuilder();
+ for (int i = 0; i < value.length(); i++) {
+ char c = value.charAt(i);
+ if (c == '\\') {
+ //noinspection AssignmentToForLoopParameter
+ i++;
+ if (i == value.length()) {
+ break;
+ }
+ c = value.charAt(i);
+ if (c == 'n' || c == 'N') {
+ c = '\n';
+ } else if (c == 'r') {
+ c = '\r';
+ }
+ }
+ // iPhone encodes category separator
+ if (c == ',' &&
+ // multivalued properties
+ ("N".equals(key) ||
+ "ADR".equals(key) ||
+ "CATEGORIES".equals(key) ||
+ "NICKNAME".equals(key)
+ )) {
+ // convert multiple values to multiline values (e.g. street)
+ c = '\n';
+ }
+ decodedValue.append(c);
+ }
+ return decodedValue.toString();
+ }
+ }
+
+ /**
+ * Set property key.
+ *
+ * @param key property key
+ */
+ public void setKey(String key) {
+ int dotIndex = key.indexOf('.');
+ if (dotIndex < 0) {
+ this.key = key;
+ } else {
+ this.key = key.substring(dotIndex + 1);
+ }
+ }
+
+ public String toString() {
+ StringBuilder buffer = new StringBuilder();
+ buffer.append(key);
+ if (params != null) {
+ for (Param param : params) {
+ buffer.append(';').append(param.name);
+ if (param.values != null) {
+ buffer.append('=');
+ appendParamValues(buffer, param);
+ }
+ }
+ }
+ buffer.append(':');
+ if (values != null) {
+ boolean firstValue = true;
+ for (String value : values) {
+ if (firstValue) {
+ firstValue = false;
+ } else if (MULTIVALUED_PROPERTIES.contains(key)) {
+ buffer.append(',');
+ } else {
+ buffer.append(';');
+ }
+ appendMultilineEncodedValue(buffer, value);
+ }
+ }
+ return buffer.toString();
+ }
+
+ protected void appendParamValues(StringBuilder buffer, Param param) {
+ boolean first = true;
+ for (String value : param.values) {
+ if (first) {
+ first = false;
+ } else {
+ buffer.append(',');
+ }
+ // always quote CN param
+ if ("CN".equalsIgnoreCase(param.name)
+ // quote param values with special characters
+ || value.indexOf(';') >= 0 || value.indexOf(',') >= 0
+ || value.indexOf('(') >= 0 || value.indexOf('/') >= 0
+ || value.indexOf(':') >= 0) {
+ buffer.append('"').append(value).append('"');
+ } else {
+ buffer.append(value);
+ }
+ }
+ }
+
+ /**
+ * Append and encode \n to \\n in value.
+ *
+ * @param buffer line buffer
+ * @param value value
+ */
+ protected void appendMultilineEncodedValue(StringBuilder buffer, String value) {
+ for (int i = 0; i < value.length(); i++) {
+ char c = value.charAt(i);
+ if (c == '\n') {
+ buffer.append("\\n");
+ } else if (MULTIVALUED_PROPERTIES.contains(key) && c==',') {
+ buffer.append('\\').append(',');
+ } else {
+ buffer.append(value.charAt(i));
+ }
+ }
+ }
+
+}
diff --git a/src/java/davmail/exchange/XMLStreamUtil.java b/src/java/davmail/exchange/XMLStreamUtil.java
new file mode 100644
index 0000000..88cb74b
--- /dev/null
+++ b/src/java/davmail/exchange/XMLStreamUtil.java
@@ -0,0 +1,190 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange;
+
+import org.apache.log4j.Logger;
+
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringReader;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * XmlStreamReader utility methods
+ */
+ at SuppressWarnings("Since15")
+public final class XMLStreamUtil {
+ private static final Logger LOGGER = Logger.getLogger(XMLStreamUtil.class);
+
+ private XMLStreamUtil() {
+ }
+
+ /**
+ * Build a new XMLInputFactory.
+ *
+ * @return XML input factory
+ */
+ public static XMLInputFactory getXmlInputFactory() {
+ XMLInputFactory inputFactory = XMLInputFactory.newInstance();
+ inputFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE);
+ inputFactory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, Boolean.TRUE);
+ return inputFactory;
+ }
+
+ /**
+ * Convert the XML stream to a map of entries.
+ * An entry is also a key/value map
+ *
+ * @param inputStream xml input stream
+ * @param rowName xml tag name of entries
+ * @param idName xml tag name of entry attribute used as key in the main map
+ * @return map of entries
+ * @throws IOException on error
+ */
+ public static Map<String, Map<String, String>> getElementContentsAsMap(InputStream inputStream, String rowName, String idName) throws IOException {
+ Map<String, Map<String, String>> results = new HashMap<String, Map<String, String>>();
+ Map<String, String> item = null;
+ String currentElement = null;
+ XMLStreamReader reader = null;
+ try {
+ XMLInputFactory inputFactory = getXmlInputFactory();
+ reader = inputFactory.createXMLStreamReader(inputStream);
+ while (reader.hasNext()) {
+ int event = reader.next();
+ if (event == XMLStreamConstants.START_ELEMENT && rowName.equals(reader.getLocalName())) {
+ item = new HashMap<String, String>();
+ } else if (event == XMLStreamConstants.END_ELEMENT && rowName.equals(reader.getLocalName())) {
+ if (item.containsKey(idName)) {
+ results.put(item.get(idName).toLowerCase(), item);
+ }
+ item = null;
+ } else if (event == XMLStreamConstants.START_ELEMENT && item != null) {
+ currentElement = reader.getLocalName();
+ } else if (event == XMLStreamConstants.CHARACTERS && currentElement != null) {
+ item.put(currentElement, reader.getText());
+ currentElement = null;
+ }
+ }
+ } catch (XMLStreamException e) {
+ throw new IOException(e.getMessage());
+ } finally {
+ try {
+ if (reader != null) {
+ reader.close();
+ }
+ } catch (XMLStreamException e) {
+ ExchangeSession.LOGGER.error(e);
+ }
+ }
+ return results;
+ }
+
+ /**
+ * Test if reader is on a start tag named tagLocalName.
+ *
+ * @param reader xml stream reader
+ * @param tagLocalName tag local name
+ * @return true if reader is on a start tag named tagLocalName
+ */
+ public static boolean isStartTag(XMLStreamReader reader, String tagLocalName) {
+ return (reader.getEventType() == XMLStreamConstants.START_ELEMENT) && (reader.getLocalName().equals(tagLocalName));
+ }
+
+ /**
+ * Test if reader is on a start tag.
+ *
+ * @param reader xml stream reader
+ * @return true if reader is on a start tag
+ */
+ public static boolean isStartTag(XMLStreamReader reader) {
+ return (reader.getEventType() == XMLStreamConstants.START_ELEMENT);
+ }
+
+ /**
+ * Test if reader is on an end tag named tagLocalName.
+ *
+ * @param reader xml stream reader
+ * @param tagLocalName tag local name
+ * @return true if reader is on an end tag named tagLocalName
+ */
+ public static boolean isEndTag(XMLStreamReader reader, String tagLocalName) {
+ return (reader.getEventType() == XMLStreamConstants.END_ELEMENT) && (reader.getLocalName().equals(tagLocalName));
+ }
+
+ /**
+ * Create XML stream reader for byte array
+ *
+ * @param xmlContent xml content as byte array
+ * @return XML stream reader
+ * @throws XMLStreamException on error
+ */
+ public static XMLStreamReader createXMLStreamReader(byte[] xmlContent) throws XMLStreamException {
+ return createXMLStreamReader(new ByteArrayInputStream(xmlContent));
+ }
+
+ /**
+ * Create XML stream reader for string
+ *
+ * @param xmlContent xml content as string
+ * @return XML stream reader
+ * @throws XMLStreamException on error
+ */
+ public static XMLStreamReader createXMLStreamReader(String xmlContent) throws XMLStreamException {
+ XMLInputFactory xmlInputFactory = XMLStreamUtil.getXmlInputFactory();
+ return xmlInputFactory.createXMLStreamReader(new StringReader(xmlContent));
+ }
+
+ /**
+ * Create XML stream reader for inputStream
+ *
+ * @param inputStream xml content inputStream
+ * @return XML stream reader
+ * @throws XMLStreamException on error
+ */
+ public static XMLStreamReader createXMLStreamReader(InputStream inputStream) throws XMLStreamException {
+ XMLInputFactory xmlInputFactory = XMLStreamUtil.getXmlInputFactory();
+ return xmlInputFactory.createXMLStreamReader(inputStream);
+ }
+
+ /**
+ * Get element text.
+ *
+ * @param reader stream reader
+ * @return element text
+ */
+ public static String getElementText(XMLStreamReader reader) {
+ String value = null;
+ try {
+ value = reader.getElementText();
+ } catch (XMLStreamException e) {
+ LOGGER.warn(e.getMessage());
+ } catch (RuntimeException e) {
+ // probably com.ctc.wstx.exc.WstxLazyException on invalid character sequence
+ LOGGER.warn(e.getMessage());
+ }
+ return value;
+ }
+
+}
diff --git a/src/java/davmail/exchange/dav/DavExchangeSession.java b/src/java/davmail/exchange/dav/DavExchangeSession.java
new file mode 100644
index 0000000..8b97101
--- /dev/null
+++ b/src/java/davmail/exchange/dav/DavExchangeSession.java
@@ -0,0 +1,3122 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.dav;
+
+import davmail.BundleMessage;
+import davmail.Settings;
+import davmail.exception.*;
+import davmail.exchange.*;
+import davmail.http.DavGatewayHttpClientFacade;
+import davmail.ui.tray.DavGatewayTray;
+import davmail.util.IOUtil;
+import davmail.util.StringUtil;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.httpclient.*;
+import org.apache.commons.httpclient.methods.*;
+import org.apache.commons.httpclient.util.URIUtil;
+import org.apache.jackrabbit.webdav.DavConstants;
+import org.apache.jackrabbit.webdav.DavException;
+import org.apache.jackrabbit.webdav.MultiStatus;
+import org.apache.jackrabbit.webdav.MultiStatusResponse;
+import org.apache.jackrabbit.webdav.client.methods.CopyMethod;
+import org.apache.jackrabbit.webdav.client.methods.MoveMethod;
+import org.apache.jackrabbit.webdav.client.methods.PropFindMethod;
+import org.apache.jackrabbit.webdav.client.methods.PropPatchMethod;
+import org.apache.jackrabbit.webdav.property.DavProperty;
+import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
+import org.apache.jackrabbit.webdav.property.DavPropertySet;
+import org.w3c.dom.Node;
+
+import javax.mail.MessagingException;
+import javax.mail.Session;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeMessage;
+import javax.mail.internet.MimeMultipart;
+import javax.mail.internet.MimePart;
+import javax.mail.util.SharedByteArrayInputStream;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import java.io.*;
+import java.net.NoRouteToHostException;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.zip.GZIPInputStream;
+
+/**
+ * Webdav Exchange adapter.
+ * Compatible with Exchange 2003 and 2007 with webdav available.
+ */
+public class DavExchangeSession extends ExchangeSession {
+ protected static enum FolderQueryTraversal {
+ Shallow, Deep
+ }
+
+ protected static final DavPropertyNameSet WELL_KNOWN_FOLDERS = new DavPropertyNameSet();
+
+ static {
+ WELL_KNOWN_FOLDERS.add(Field.getPropertyName("inbox"));
+ WELL_KNOWN_FOLDERS.add(Field.getPropertyName("deleteditems"));
+ WELL_KNOWN_FOLDERS.add(Field.getPropertyName("sentitems"));
+ WELL_KNOWN_FOLDERS.add(Field.getPropertyName("sendmsg"));
+ WELL_KNOWN_FOLDERS.add(Field.getPropertyName("drafts"));
+ WELL_KNOWN_FOLDERS.add(Field.getPropertyName("calendar"));
+ WELL_KNOWN_FOLDERS.add(Field.getPropertyName("tasks"));
+ WELL_KNOWN_FOLDERS.add(Field.getPropertyName("contacts"));
+ WELL_KNOWN_FOLDERS.add(Field.getPropertyName("outbox"));
+ }
+
+ static final Map<String, String> vTodoToTaskStatusMap = new HashMap<String, String>();
+ static final Map<String, String> taskTovTodoStatusMap = new HashMap<String, String>();
+
+ static {
+ //taskTovTodoStatusMap.put("0", null);
+ taskTovTodoStatusMap.put("1", "IN-PROCESS");
+ taskTovTodoStatusMap.put("2", "COMPLETED");
+ taskTovTodoStatusMap.put("3", "NEEDS-ACTION");
+ taskTovTodoStatusMap.put("4", "CANCELLED");
+
+ //vTodoToTaskStatusMap.put(null, "0");
+ vTodoToTaskStatusMap.put("IN-PROCESS", "1");
+ vTodoToTaskStatusMap.put("COMPLETED", "2");
+ vTodoToTaskStatusMap.put("NEEDS-ACTION", "3");
+ vTodoToTaskStatusMap.put("CANCELLED", "4");
+ }
+
+ /**
+ * Various standard mail boxes Urls
+ */
+ protected String inboxUrl;
+ protected String deleteditemsUrl;
+ protected String sentitemsUrl;
+ protected String sendmsgUrl;
+ protected String draftsUrl;
+ protected String calendarUrl;
+ protected String tasksUrl;
+ protected String contactsUrl;
+ protected String outboxUrl;
+
+ protected String inboxName;
+ protected String deleteditemsName;
+ protected String sentitemsName;
+ protected String sendmsgName;
+ protected String draftsName;
+ protected String calendarName;
+ protected String tasksName;
+ protected String contactsName;
+ protected String outboxName;
+
+ protected static final String USERS = "/users/";
+
+ @Override
+ public boolean isExpired() throws NoRouteToHostException, UnknownHostException {
+ // experimental: try to reset session timeout
+ if ("Exchange2007".equals(serverVersion)) {
+ GetMethod getMethod = null;
+ try {
+ getMethod = new GetMethod("/owa/");
+ getMethod.setFollowRedirects(false);
+ httpClient.executeMethod(getMethod);
+ } catch (IOException e) {
+ LOGGER.warn(e.getMessage());
+ } finally {
+ if (getMethod != null) {
+ getMethod.releaseConnection();
+ }
+ }
+ }
+
+ return super.isExpired();
+ }
+
+
+ /**
+ * Convert logical or relative folder path to exchange folder path.
+ *
+ * @param folderPath folder name
+ * @return folder path
+ */
+ public String getFolderPath(String folderPath) {
+ String exchangeFolderPath;
+ // IMAP path
+ if (folderPath.startsWith(INBOX)) {
+ exchangeFolderPath = mailPath + inboxName + folderPath.substring(INBOX.length());
+ } else if (folderPath.startsWith(TRASH)) {
+ exchangeFolderPath = mailPath + deleteditemsName + folderPath.substring(TRASH.length());
+ } else if (folderPath.startsWith(DRAFTS)) {
+ exchangeFolderPath = mailPath + draftsName + folderPath.substring(DRAFTS.length());
+ } else if (folderPath.startsWith(SENT)) {
+ exchangeFolderPath = mailPath + sentitemsName + folderPath.substring(SENT.length());
+ } else if (folderPath.startsWith(SENDMSG)) {
+ exchangeFolderPath = mailPath + sendmsgName + folderPath.substring(SENDMSG.length());
+ } else if (folderPath.startsWith(CONTACTS)) {
+ exchangeFolderPath = mailPath + contactsName + folderPath.substring(CONTACTS.length());
+ } else if (folderPath.startsWith(CALENDAR)) {
+ exchangeFolderPath = mailPath + calendarName + folderPath.substring(CALENDAR.length());
+ } else if (folderPath.startsWith(TASKS)) {
+ exchangeFolderPath = mailPath + tasksName + folderPath.substring(TASKS.length());
+ } else if (folderPath.startsWith("public")) {
+ exchangeFolderPath = publicFolderUrl + folderPath.substring("public".length());
+
+ // caldav path
+ } else if (folderPath.startsWith(USERS)) {
+ // get requested principal
+ String principal;
+ String localPath;
+ int principalIndex = folderPath.indexOf('/', USERS.length());
+ if (principalIndex >= 0) {
+ principal = folderPath.substring(USERS.length(), principalIndex);
+ localPath = folderPath.substring(USERS.length() + principal.length() + 1);
+ if (localPath.startsWith(LOWER_CASE_INBOX) || localPath.startsWith(INBOX)) {
+ localPath = inboxName + localPath.substring(LOWER_CASE_INBOX.length());
+ } else if (localPath.startsWith(CALENDAR)) {
+ localPath = calendarName + localPath.substring(CALENDAR.length());
+ } else if (localPath.startsWith(TASKS)) {
+ localPath = tasksName + localPath.substring(TASKS.length());
+ } else if (localPath.startsWith(CONTACTS)) {
+ localPath = contactsName + localPath.substring(CONTACTS.length());
+ } else if (localPath.startsWith(ADDRESSBOOK)) {
+ localPath = contactsName + localPath.substring(ADDRESSBOOK.length());
+ }
+ } else {
+ principal = folderPath.substring(USERS.length());
+ localPath = "";
+ }
+ if (principal.length() == 0) {
+ exchangeFolderPath = rootPath;
+ } else if (alias.equalsIgnoreCase(principal) || email.equalsIgnoreCase(principal)) {
+ exchangeFolderPath = mailPath + localPath;
+ } else {
+ LOGGER.debug("Detected shared path for principal " + principal + ", user principal is " + email);
+ exchangeFolderPath = rootPath + principal + '/' + localPath;
+ }
+
+ // absolute folder path
+ } else if (folderPath.startsWith("/")) {
+ exchangeFolderPath = folderPath;
+ } else {
+ exchangeFolderPath = mailPath + folderPath;
+ }
+ return exchangeFolderPath;
+ }
+
+ /**
+ * Test if folderPath is inside user mailbox.
+ *
+ * @param folderPath absolute folder path
+ * @return true if folderPath is a public or shared folder
+ */
+ @Override
+ public boolean isSharedFolder(String folderPath) {
+ return !getFolderPath(folderPath).toLowerCase().startsWith(mailPath.toLowerCase());
+ }
+
+ /**
+ * Test if folderPath is main calendar.
+ *
+ * @param folderPath absolute folder path
+ * @return true if folderPath is a public or shared folder
+ */
+ @Override
+ public boolean isMainCalendar(String folderPath) {
+ return getFolderPath(folderPath).equalsIgnoreCase(getFolderPath("calendar"));
+ }
+
+ /**
+ * Build base path for cmd commands (galfind, gallookup).
+ *
+ * @return cmd base path
+ */
+ public String getCmdBasePath() {
+ if (("Exchange2003".equals(serverVersion) || PUBLIC_ROOT.equals(publicFolderUrl)) && mailPath != null) {
+ // public folder is not available => try to use mailbox path
+ // Note: This does not work with freebusy, which requires /public/
+ return mailPath;
+ } else {
+ // use public folder url
+ return publicFolderUrl;
+ }
+ }
+
+ /**
+ * LDAP to Exchange Criteria Map
+ */
+ static final HashMap<String, String> GALFIND_CRITERIA_MAP = new HashMap<String, String>();
+
+ static {
+ GALFIND_CRITERIA_MAP.put("imapUid", "AN");
+ GALFIND_CRITERIA_MAP.put("smtpemail1", "EM");
+ GALFIND_CRITERIA_MAP.put("cn", "DN");
+ GALFIND_CRITERIA_MAP.put("givenName", "FN");
+ GALFIND_CRITERIA_MAP.put("sn", "LN");
+ GALFIND_CRITERIA_MAP.put("title", "TL");
+ GALFIND_CRITERIA_MAP.put("o", "CP");
+ GALFIND_CRITERIA_MAP.put("l", "OF");
+ GALFIND_CRITERIA_MAP.put("department", "DP");
+ }
+
+ static final HashSet<String> GALLOOKUP_ATTRIBUTES = new HashSet<String>();
+
+ static {
+ GALLOOKUP_ATTRIBUTES.add("givenName");
+ GALLOOKUP_ATTRIBUTES.add("initials");
+ GALLOOKUP_ATTRIBUTES.add("sn");
+ GALLOOKUP_ATTRIBUTES.add("street");
+ GALLOOKUP_ATTRIBUTES.add("st");
+ GALLOOKUP_ATTRIBUTES.add("postalcode");
+ GALLOOKUP_ATTRIBUTES.add("co");
+ GALLOOKUP_ATTRIBUTES.add("departement");
+ GALLOOKUP_ATTRIBUTES.add("mobile");
+ }
+
+ /**
+ * Exchange to LDAP attribute map
+ */
+ static final HashMap<String, String> GALFIND_ATTRIBUTE_MAP = new HashMap<String, String>();
+
+ static {
+ GALFIND_ATTRIBUTE_MAP.put("uid", "AN");
+ GALFIND_ATTRIBUTE_MAP.put("smtpemail1", "EM");
+ GALFIND_ATTRIBUTE_MAP.put("cn", "DN");
+ GALFIND_ATTRIBUTE_MAP.put("displayName", "DN");
+ GALFIND_ATTRIBUTE_MAP.put("telephoneNumber", "PH");
+ GALFIND_ATTRIBUTE_MAP.put("l", "OFFICE");
+ GALFIND_ATTRIBUTE_MAP.put("o", "CP");
+ GALFIND_ATTRIBUTE_MAP.put("title", "TL");
+
+ GALFIND_ATTRIBUTE_MAP.put("givenName", "first");
+ GALFIND_ATTRIBUTE_MAP.put("initials", "initials");
+ GALFIND_ATTRIBUTE_MAP.put("sn", "last");
+ GALFIND_ATTRIBUTE_MAP.put("street", "street");
+ GALFIND_ATTRIBUTE_MAP.put("st", "state");
+ GALFIND_ATTRIBUTE_MAP.put("postalcode", "zip");
+ GALFIND_ATTRIBUTE_MAP.put("co", "country");
+ GALFIND_ATTRIBUTE_MAP.put("department", "department");
+ GALFIND_ATTRIBUTE_MAP.put("mobile", "mobile");
+ GALFIND_ATTRIBUTE_MAP.put("roomnumber", "office");
+ }
+
+ boolean disableGalFind;
+
+ protected Map<String, Map<String, String>> galFind(String query) throws IOException {
+ Map<String, Map<String, String>> results;
+ String path = getCmdBasePath() + "?Cmd=galfind" + query;
+ GetMethod getMethod = new GetMethod(path);
+ try {
+ DavGatewayHttpClientFacade.executeGetMethod(httpClient, getMethod, true);
+ results = XMLStreamUtil.getElementContentsAsMap(getMethod.getResponseBodyAsStream(), "item", "AN");
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug(path + ": " + results.size() + " result(s)");
+ }
+ } catch (IOException e) {
+ LOGGER.debug("GET " + path + " failed: " + e + ' ' + e.getMessage());
+ disableGalFind = true;
+ throw e;
+ } finally {
+ getMethod.releaseConnection();
+ }
+ return results;
+ }
+
+
+ @Override
+ public Map<String, ExchangeSession.Contact> galFind(Condition condition, Set<String> returningAttributes, int sizeLimit) throws IOException {
+ Map<String, ExchangeSession.Contact> contacts = new HashMap<String, ExchangeSession.Contact>();
+ if (disableGalFind) {
+ // do nothing
+ } else if (condition instanceof MultiCondition) {
+ List<Condition> conditions = ((ExchangeSession.MultiCondition) condition).getConditions();
+ Operator operator = ((ExchangeSession.MultiCondition) condition).getOperator();
+ if (operator == Operator.Or) {
+ for (Condition innerCondition : conditions) {
+ contacts.putAll(galFind(innerCondition, returningAttributes, sizeLimit));
+ }
+ } else if (operator == Operator.And && !conditions.isEmpty()) {
+ Map<String, ExchangeSession.Contact> innerContacts = galFind(conditions.get(0), returningAttributes, sizeLimit);
+ for (ExchangeSession.Contact contact : innerContacts.values()) {
+ if (condition.isMatch(contact)) {
+ contacts.put(contact.getName().toLowerCase(), contact);
+ }
+ }
+ }
+ } else if (condition instanceof AttributeCondition) {
+ String searchAttributeName = ((ExchangeSession.AttributeCondition) condition).getAttributeName();
+ String searchAttribute = GALFIND_CRITERIA_MAP.get(searchAttributeName);
+ if (searchAttribute != null) {
+ String searchValue = ((ExchangeSession.AttributeCondition) condition).getValue();
+ StringBuilder query = new StringBuilder();
+ if ("EM".equals(searchAttribute)) {
+ // mail search, split
+ int atIndex = searchValue.indexOf('@');
+ // remove suffix
+ if (atIndex >= 0) {
+ searchValue = searchValue.substring(0, atIndex);
+ }
+ // split firstname.lastname
+ int dotIndex = searchValue.indexOf('.');
+ if (dotIndex >= 0) {
+ // assume mail starts with firstname
+ query.append("&FN=").append(URIUtil.encodeWithinQuery(searchValue.substring(0, dotIndex)));
+ query.append("&LN=").append(URIUtil.encodeWithinQuery(searchValue.substring(dotIndex + 1)));
+ } else {
+ query.append("&FN=").append(URIUtil.encodeWithinQuery(searchValue));
+ }
+ } else {
+ query.append('&').append(searchAttribute).append('=').append(URIUtil.encodeWithinQuery(searchValue));
+ }
+ Map<String, Map<String, String>> results = galFind(query.toString());
+ for (Map<String, String> result : results.values()) {
+ Contact contact = new Contact();
+ contact.setName(result.get("AN"));
+ contact.put("imapUid", result.get("AN"));
+ buildGalfindContact(contact, result);
+ if (needGalLookup(searchAttributeName, returningAttributes)) {
+ galLookup(contact);
+ // iCal fix to suit both iCal 3 and 4: move cn to sn, remove cn
+ } else if (returningAttributes.contains("apple-serviceslocator")) {
+ if (contact.get("cn") != null && returningAttributes.contains("sn")) {
+ contact.put("sn", contact.get("cn"));
+ contact.remove("cn");
+ }
+ }
+ if (condition.isMatch(contact)) {
+ contacts.put(contact.getName().toLowerCase(), contact);
+ }
+ }
+ }
+
+ }
+ return contacts;
+ }
+
+ protected boolean needGalLookup(String searchAttributeName, Set<String> returningAttributes) {
+ // return all attributes => call gallookup
+ if (returningAttributes == null || returningAttributes.isEmpty()) {
+ return true;
+ // iCal search, do not call gallookup
+ } else if (returningAttributes.contains("apple-serviceslocator")) {
+ return false;
+ // Lightning search, no need to gallookup
+ } else if ("sn".equals(searchAttributeName)) {
+ return returningAttributes.contains("sn");
+ // search attribute is gallookup attribute, need to fetch value for isMatch
+ } else if (GALLOOKUP_ATTRIBUTES.contains(searchAttributeName)) {
+ return true;
+ }
+
+ for (String attributeName : GALLOOKUP_ATTRIBUTES) {
+ if (returningAttributes.contains(attributeName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean disableGalLookup;
+
+ /**
+ * Get extended address book information for person with gallookup.
+ * Does not work with Exchange 2007
+ *
+ * @param contact galfind contact
+ */
+ public void galLookup(Contact contact) {
+ if (!disableGalLookup) {
+ LOGGER.debug("galLookup(" + contact.get("smtpemail1") + ')');
+ GetMethod getMethod = null;
+ try {
+ getMethod = new GetMethod(URIUtil.encodePathQuery(getCmdBasePath() + "?Cmd=gallookup&ADDR=" + contact.get("smtpemail1")));
+ DavGatewayHttpClientFacade.executeGetMethod(httpClient, getMethod, true);
+ Map<String, Map<String, String>> results = XMLStreamUtil.getElementContentsAsMap(getMethod.getResponseBodyAsStream(), "person", "alias");
+ // add detailed information
+ if (!results.isEmpty()) {
+ Map<String, String> personGalLookupDetails = results.get(contact.get("uid").toLowerCase());
+ if (personGalLookupDetails != null) {
+ buildGalfindContact(contact, personGalLookupDetails);
+ }
+ }
+ } catch (IOException e) {
+ LOGGER.warn("Unable to gallookup person: " + contact + ", disable GalLookup");
+ disableGalLookup = true;
+ } finally {
+ if (getMethod != null) {
+ getMethod.releaseConnection();
+ }
+ }
+ }
+ }
+
+ protected void buildGalfindContact(Contact contact, Map<String, String> response) {
+ for (Map.Entry<String, String> entry : GALFIND_ATTRIBUTE_MAP.entrySet()) {
+ String attributeValue = response.get(entry.getValue());
+ if (attributeValue != null) {
+ contact.put(entry.getKey(), attributeValue);
+ }
+ }
+ }
+
+ @Override
+ protected String getFreeBusyData(String attendee, String start, String end, int interval) throws IOException {
+ String freebusyUrl = publicFolderUrl + "/?cmd=freebusy" +
+ "&start=" + start +
+ "&end=" + end +
+ "&interval=" + interval +
+ "&u=SMTP:" + attendee;
+ GetMethod getMethod = new GetMethod(freebusyUrl);
+ getMethod.setRequestHeader("Content-Type", "text/xml");
+ String fbdata = null;
+ try {
+ DavGatewayHttpClientFacade.executeGetMethod(httpClient, getMethod, true);
+ fbdata = StringUtil.getLastToken(getMethod.getResponseBodyAsString(), "<a:fbdata>", "</a:fbdata>");
+ } finally {
+ getMethod.releaseConnection();
+ }
+ return fbdata;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public DavExchangeSession(String url, String userName, String password) throws IOException {
+ super(url, userName, password);
+ }
+
+ @Override
+ protected void buildSessionInfo(HttpMethod method) throws DavMailException {
+ buildMailPath(method);
+
+ // get base http mailbox http urls
+ getWellKnownFolders();
+ }
+
+ static final String BASE_HREF = "<base href=\"";
+
+ /**
+ * Exchange 2003: get mailPath from welcome page
+ *
+ * @param method current http method
+ * @return mail path from body
+ */
+ protected String getMailpathFromWelcomePage(HttpMethod method) {
+ String welcomePageMailPath = null;
+ // get user mail URL from html body (multi frame)
+ BufferedReader mainPageReader = null;
+ try {
+ mainPageReader = new BufferedReader(new InputStreamReader(method.getResponseBodyAsStream()));
+ //noinspection StatementWithEmptyBody
+ String line;
+ while ((line = mainPageReader.readLine()) != null && line.toLowerCase().indexOf(BASE_HREF) == -1) {
+ }
+ if (line != null) {
+ // Exchange 2003
+ int start = line.toLowerCase().indexOf(BASE_HREF) + BASE_HREF.length();
+ int end = line.indexOf('\"', start);
+ String mailBoxBaseHref = line.substring(start, end);
+ URL baseURL = new URL(mailBoxBaseHref);
+ welcomePageMailPath = URIUtil.decode(baseURL.getPath());
+ LOGGER.debug("Base href found in body, mailPath is " + welcomePageMailPath);
+ }
+ } catch (IOException e) {
+ LOGGER.error("Error parsing main page at " + method.getPath(), e);
+ } finally {
+ if (mainPageReader != null) {
+ try {
+ mainPageReader.close();
+ } catch (IOException e) {
+ LOGGER.error("Error parsing main page at " + method.getPath());
+ }
+ }
+ method.releaseConnection();
+ }
+ return welcomePageMailPath;
+ }
+
+ protected void buildMailPath(HttpMethod method) throws DavMailAuthenticationException {
+ // get mailPath from welcome page on Exchange 2003
+ mailPath = getMailpathFromWelcomePage(method);
+
+ //noinspection VariableNotUsedInsideIf
+ if (mailPath != null) {
+ // Exchange 2003
+ serverVersion = "Exchange2003";
+ fixClientHost(method);
+ checkPublicFolder();
+ try {
+ buildEmail(method.getURI().getHost());
+ } catch (URIException uriException) {
+ LOGGER.warn(uriException);
+ }
+ } else {
+ // Exchange 2007 : get alias and email from options page
+ serverVersion = "Exchange2007";
+
+ // Gallookup is an Exchange 2003 only feature
+ disableGalLookup = true;
+ fixClientHost(method);
+ getEmailAndAliasFromOptions();
+
+ checkPublicFolder();
+
+ // failover: try to get email through Webdav and Galfind
+ if (alias == null || email == null) {
+ try {
+ buildEmail(method.getURI().getHost());
+ } catch (URIException uriException) {
+ LOGGER.warn(uriException);
+ }
+ }
+
+ // build standard mailbox link with email
+ mailPath = "/exchange/" + email + '/';
+ }
+
+ if (mailPath == null || email == null) {
+ throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED_PASSWORD_EXPIRED");
+ }
+ LOGGER.debug("Current user email is " + email + ", alias is " + alias + ", mailPath is " + mailPath + " on " + serverVersion);
+ rootPath = mailPath.substring(0, mailPath.lastIndexOf('/', mailPath.length() - 2) + 1);
+ }
+
+ /**
+ * Determine user email through various means.
+ *
+ * @param hostName Exchange server host name for last failover
+ */
+ public void buildEmail(String hostName) {
+ String mailBoxPath = getMailboxPath();
+ // mailPath contains either alias or email
+ if (mailBoxPath != null && mailBoxPath.indexOf('@') >= 0) {
+ email = mailBoxPath;
+ alias = getAliasFromMailboxDisplayName();
+ if (alias == null) {
+ alias = getAliasFromLogin();
+ }
+ } else {
+ // use mailbox name as alias
+ alias = mailBoxPath;
+ email = getEmail(alias);
+ if (email == null) {
+ // failover: try to get email from login name
+ alias = getAliasFromLogin();
+ email = getEmail(alias);
+ }
+ // another failover : get alias from mailPath display name
+ if (email == null) {
+ alias = getAliasFromMailboxDisplayName();
+ email = getEmail(alias);
+ }
+ if (email == null) {
+ LOGGER.debug("Unable to get user email with alias " + mailBoxPath
+ + " or " + getAliasFromLogin()
+ + " or " + alias
+ );
+ // last failover: build email from domain name and mailbox display name
+ StringBuilder buffer = new StringBuilder();
+ // most reliable alias
+ if (mailBoxPath != null) {
+ alias = mailBoxPath;
+ } else {
+ alias = getAliasFromLogin();
+ }
+ buffer.append(alias);
+ if (alias.indexOf('@') < 0) {
+ buffer.append('@');
+ int dotIndex = hostName.indexOf('.');
+ if (dotIndex >= 0) {
+ buffer.append(hostName.substring(dotIndex + 1));
+ }
+ }
+ email = buffer.toString();
+ }
+ }
+ }
+
+ /**
+ * Get user alias from mailbox display name over Webdav.
+ *
+ * @return user alias
+ */
+ public String getAliasFromMailboxDisplayName() {
+ if (mailPath == null) {
+ return null;
+ }
+ String displayName = null;
+ try {
+ Folder rootFolder = getFolder("");
+ if (rootFolder == null) {
+ LOGGER.warn(new BundleMessage("EXCEPTION_UNABLE_TO_GET_MAIL_FOLDER", mailPath));
+ } else {
+ displayName = rootFolder.displayName;
+ }
+ } catch (IOException e) {
+ LOGGER.warn(new BundleMessage("EXCEPTION_UNABLE_TO_GET_MAIL_FOLDER", mailPath));
+ }
+ return displayName;
+ }
+
+ /**
+ * Get current Exchange alias name from mailbox name
+ *
+ * @return user name
+ */
+ protected String getMailboxPath() {
+ if (mailPath == null) {
+ return null;
+ }
+ int index = mailPath.lastIndexOf('/', mailPath.length() - 2);
+ if (index >= 0 && mailPath.endsWith("/")) {
+ return mailPath.substring(index + 1, mailPath.length() - 1);
+ } else {
+ LOGGER.warn(new BundleMessage("EXCEPTION_INVALID_MAIL_PATH", mailPath));
+ return null;
+ }
+ }
+
+ /**
+ * Get user email from global address list (galfind).
+ *
+ * @param alias user alias
+ * @return user email
+ */
+ public String getEmail(String alias) {
+ String emailResult = null;
+ if (alias != null && !disableGalFind) {
+ try {
+ Map<String, Map<String, String>> results = galFind("&AN=" + URIUtil.encodeWithinQuery(alias));
+ Map<String, String> result = results.get(alias.toLowerCase());
+ if (result != null) {
+ emailResult = result.get("EM");
+ }
+ } catch (IOException e) {
+ // galfind not available
+ disableGalFind = true;
+ LOGGER.debug("getEmail(" + alias + ") failed");
+ }
+ }
+ return emailResult;
+ }
+
+ protected String getURIPropertyIfExists(DavPropertySet properties, String alias) throws URIException {
+ DavProperty property = properties.get(Field.getPropertyName(alias));
+ if (property == null) {
+ return null;
+ } else {
+ return URIUtil.decode((String) property.getValue());
+ }
+ }
+
+ // return last folder name from url
+
+ protected String getFolderName(String url) {
+ if (url != null) {
+ if (url.endsWith("/")) {
+ return url.substring(url.lastIndexOf('/', url.length() - 2) + 1, url.length() - 1);
+ } else if (url.indexOf('/') > 0) {
+ return url.substring(url.lastIndexOf('/') + 1);
+ } else {
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+
+ protected void fixClientHost(HttpMethod method) {
+ try {
+ // update client host, workaround for Exchange 2003 mailbox with an Exchange 2007 frontend
+ URI currentUri = method.getURI();
+ if (currentUri != null && currentUri.getHost() != null && currentUri.getScheme() != null) {
+ httpClient.getHostConfiguration().setHost(currentUri.getHost(), currentUri.getPort(), currentUri.getScheme());
+ }
+ } catch (URIException e) {
+ LOGGER.warn("Unable to update http client host:" + e.getMessage(), e);
+ }
+ }
+
+ protected void checkPublicFolder() {
+
+ Cookie[] currentCookies = httpClient.getState().getCookies();
+ // check public folder access
+ try {
+ publicFolderUrl = httpClient.getHostConfiguration().getHostURL() + PUBLIC_ROOT;
+ DavPropertyNameSet davPropertyNameSet = new DavPropertyNameSet();
+ davPropertyNameSet.add(Field.getPropertyName("displayname"));
+ PropFindMethod propFindMethod = new PropFindMethod(publicFolderUrl, davPropertyNameSet, 0);
+ try {
+ DavGatewayHttpClientFacade.executeMethod(httpClient, propFindMethod);
+ } catch (IOException e) {
+ // workaround for NTLM authentication only on /public
+ if (!DavGatewayHttpClientFacade.hasNTLM(httpClient)) {
+ DavGatewayHttpClientFacade.addNTLM(httpClient);
+ DavGatewayHttpClientFacade.executeMethod(httpClient, propFindMethod);
+ }
+ }
+ // update public folder URI
+ publicFolderUrl = propFindMethod.getURI().getURI();
+ } catch (IOException e) {
+ // restore cookies on error
+ httpClient.getState().addCookies(currentCookies);
+ LOGGER.warn("Public folders not available: " + (e.getMessage() == null ? e : e.getMessage()));
+ // default public folder path
+ publicFolderUrl = PUBLIC_ROOT;
+ }
+ }
+
+ protected void getWellKnownFolders() throws DavMailException {
+ // Retrieve well known URLs
+ MultiStatusResponse[] responses;
+ try {
+ responses = DavGatewayHttpClientFacade.executePropFindMethod(
+ httpClient, URIUtil.encodePath(mailPath), 0, WELL_KNOWN_FOLDERS);
+ if (responses.length == 0) {
+ throw new WebdavNotAvailableException("EXCEPTION_UNABLE_TO_GET_MAIL_FOLDER", mailPath);
+ }
+ DavPropertySet properties = responses[0].getProperties(HttpStatus.SC_OK);
+ inboxUrl = getURIPropertyIfExists(properties, "inbox");
+ inboxName = getFolderName(inboxUrl);
+ deleteditemsUrl = getURIPropertyIfExists(properties, "deleteditems");
+ deleteditemsName = getFolderName(deleteditemsUrl);
+ sentitemsUrl = getURIPropertyIfExists(properties, "sentitems");
+ sentitemsName = getFolderName(sentitemsUrl);
+ sendmsgUrl = getURIPropertyIfExists(properties, "sendmsg");
+ sendmsgName = getFolderName(sendmsgUrl);
+ draftsUrl = getURIPropertyIfExists(properties, "drafts");
+ draftsName = getFolderName(draftsUrl);
+ calendarUrl = getURIPropertyIfExists(properties, "calendar");
+ calendarName = getFolderName(calendarUrl);
+ tasksUrl = getURIPropertyIfExists(properties, "tasks");
+ tasksName = getFolderName(tasksUrl);
+ contactsUrl = getURIPropertyIfExists(properties, "contacts");
+ contactsName = getFolderName(contactsUrl);
+ outboxUrl = getURIPropertyIfExists(properties, "outbox");
+ outboxName = getFolderName(outboxUrl);
+ // junk folder not available over webdav
+
+ LOGGER.debug("Inbox URL: " + inboxUrl +
+ " Trash URL: " + deleteditemsUrl +
+ " Sent URL: " + sentitemsUrl +
+ " Send URL: " + sendmsgUrl +
+ " Drafts URL: " + draftsUrl +
+ " Calendar URL: " + calendarUrl +
+ " Tasks URL: " + tasksUrl +
+ " Contacts URL: " + contactsUrl +
+ " Outbox URL: " + outboxUrl +
+ " Public folder URL: " + publicFolderUrl
+ );
+ } catch (IOException e) {
+ LOGGER.error(e.getMessage());
+ throw new WebdavNotAvailableException("EXCEPTION_UNABLE_TO_GET_MAIL_FOLDER", mailPath);
+ }
+ }
+
+ protected static class MultiCondition extends ExchangeSession.MultiCondition {
+ protected MultiCondition(Operator operator, Condition... condition) {
+ super(operator, condition);
+ }
+
+ public void appendTo(StringBuilder buffer) {
+ boolean first = true;
+
+ for (Condition condition : conditions) {
+ if (condition != null && !condition.isEmpty()) {
+ if (first) {
+ buffer.append('(');
+ first = false;
+ } else {
+ buffer.append(' ').append(operator).append(' ');
+ }
+ condition.appendTo(buffer);
+ }
+ }
+ // at least one non empty condition
+ if (!first) {
+ buffer.append(')');
+ }
+ }
+ }
+
+ protected static class NotCondition extends ExchangeSession.NotCondition {
+ protected NotCondition(Condition condition) {
+ super(condition);
+ }
+
+ public void appendTo(StringBuilder buffer) {
+ buffer.append("(Not ");
+ condition.appendTo(buffer);
+ buffer.append(')');
+ }
+ }
+
+ static final Map<Operator, String> OPERATOR_MAP = new HashMap<Operator, String>();
+
+ static {
+ OPERATOR_MAP.put(Operator.IsEqualTo, " = ");
+ OPERATOR_MAP.put(Operator.IsGreaterThanOrEqualTo, " >= ");
+ OPERATOR_MAP.put(Operator.IsGreaterThan, " > ");
+ OPERATOR_MAP.put(Operator.IsLessThanOrEqualTo, " <= ");
+ OPERATOR_MAP.put(Operator.IsLessThan, " < ");
+ OPERATOR_MAP.put(Operator.Like, " like ");
+ OPERATOR_MAP.put(Operator.IsNull, " is null");
+ OPERATOR_MAP.put(Operator.IsFalse, " = false");
+ OPERATOR_MAP.put(Operator.IsTrue, " = true");
+ OPERATOR_MAP.put(Operator.StartsWith, " = ");
+ OPERATOR_MAP.put(Operator.Contains, " = ");
+ }
+
+ protected static class AttributeCondition extends ExchangeSession.AttributeCondition {
+ protected boolean isIntValue;
+
+ protected AttributeCondition(String attributeName, Operator operator, String value) {
+ super(attributeName, operator, value);
+ }
+
+ protected AttributeCondition(String attributeName, Operator operator, int value) {
+ super(attributeName, operator, String.valueOf(value));
+ isIntValue = true;
+ }
+
+ public void appendTo(StringBuilder buffer) {
+ Field field = Field.get(attributeName);
+ buffer.append('"').append(field.getUri()).append('"');
+ buffer.append(OPERATOR_MAP.get(operator));
+ //noinspection VariableNotUsedInsideIf
+ if (field.cast != null) {
+ buffer.append("CAST (\"");
+ } else if (!isIntValue && !field.isIntValue()) {
+ buffer.append('\'');
+ }
+ if (Operator.Like == operator) {
+ buffer.append('%');
+ }
+ if ("urlcompname".equals(field.alias)) {
+ buffer.append(StringUtil.encodeUrlcompname(StringUtil.davSearchEncode(value)));
+ } else if (field.isIntValue()) {
+ // check value
+ try {
+ Integer.parseInt(value);
+ buffer.append(value);
+ } catch (NumberFormatException e) {
+ // invalid value, replace with 0
+ buffer.append('0');
+ }
+ } else {
+ buffer.append(StringUtil.davSearchEncode(value));
+ }
+ if (Operator.Like == operator || Operator.StartsWith == operator) {
+ buffer.append('%');
+ }
+ if (field.cast != null) {
+ buffer.append("\" as '").append(field.cast).append("')");
+ } else if (!isIntValue && !field.isIntValue()) {
+ buffer.append('\'');
+ }
+ }
+
+ public boolean isMatch(ExchangeSession.Contact contact) {
+ String lowerCaseValue = value.toLowerCase();
+ String actualValue = contact.get(attributeName);
+ Operator actualOperator = operator;
+ // patch for iCal or Lightning search without galLookup
+ if (actualValue == null && ("givenName".equals(attributeName) || "sn".equals(attributeName))) {
+ actualValue = contact.get("cn");
+ actualOperator = Operator.Like;
+ }
+ if (actualValue == null) {
+ return false;
+ }
+ actualValue = actualValue.toLowerCase();
+ return (actualOperator == Operator.IsEqualTo && actualValue.equals(lowerCaseValue)) ||
+ (actualOperator == Operator.Like && actualValue.contains(lowerCaseValue)) ||
+ (actualOperator == Operator.StartsWith && actualValue.startsWith(lowerCaseValue));
+ }
+ }
+
+ protected static class HeaderCondition extends AttributeCondition {
+
+ protected HeaderCondition(String attributeName, Operator operator, String value) {
+ super(attributeName, operator, value);
+ }
+
+ @Override
+ public void appendTo(StringBuilder buffer) {
+ buffer.append('"').append(Field.getHeader(attributeName).getUri()).append('"');
+ buffer.append(OPERATOR_MAP.get(operator));
+ buffer.append('\'');
+ if (Operator.Like == operator) {
+ buffer.append('%');
+ }
+ buffer.append(value);
+ if (Operator.Like == operator) {
+ buffer.append('%');
+ }
+ buffer.append('\'');
+ }
+ }
+
+ protected static class MonoCondition extends ExchangeSession.MonoCondition {
+ protected MonoCondition(String attributeName, Operator operator) {
+ super(attributeName, operator);
+ }
+
+ public void appendTo(StringBuilder buffer) {
+ buffer.append('"').append(Field.get(attributeName).getUri()).append('"');
+ buffer.append(OPERATOR_MAP.get(operator));
+ }
+ }
+
+ @Override
+ public ExchangeSession.MultiCondition and(Condition... condition) {
+ return new MultiCondition(Operator.And, condition);
+ }
+
+ @Override
+ public ExchangeSession.MultiCondition or(Condition... condition) {
+ return new MultiCondition(Operator.Or, condition);
+ }
+
+ @Override
+ public Condition not(Condition condition) {
+ if (condition == null) {
+ return null;
+ } else {
+ return new NotCondition(condition);
+ }
+ }
+
+ @Override
+ public Condition isEqualTo(String attributeName, String value) {
+ return new AttributeCondition(attributeName, Operator.IsEqualTo, value);
+ }
+
+ @Override
+ public Condition isEqualTo(String attributeName, int value) {
+ return new AttributeCondition(attributeName, Operator.IsEqualTo, value);
+ }
+
+ @Override
+ public Condition headerIsEqualTo(String headerName, String value) {
+ return new HeaderCondition(headerName, Operator.IsEqualTo, value);
+ }
+
+ @Override
+ public Condition gte(String attributeName, String value) {
+ return new AttributeCondition(attributeName, Operator.IsGreaterThanOrEqualTo, value);
+ }
+
+ @Override
+ public Condition lte(String attributeName, String value) {
+ return new AttributeCondition(attributeName, Operator.IsLessThanOrEqualTo, value);
+ }
+
+ @Override
+ public Condition lt(String attributeName, String value) {
+ return new AttributeCondition(attributeName, Operator.IsLessThan, value);
+ }
+
+ @Override
+ public Condition gt(String attributeName, String value) {
+ return new AttributeCondition(attributeName, Operator.IsGreaterThan, value);
+ }
+
+ @Override
+ public Condition contains(String attributeName, String value) {
+ return new AttributeCondition(attributeName, Operator.Like, value);
+ }
+
+ @Override
+ public Condition startsWith(String attributeName, String value) {
+ return new AttributeCondition(attributeName, Operator.StartsWith, value);
+ }
+
+ @Override
+ public Condition isNull(String attributeName) {
+ return new MonoCondition(attributeName, Operator.IsNull);
+ }
+
+ @Override
+ public Condition isTrue(String attributeName) {
+ if ("Exchange2003".equals(this.serverVersion) && "deleted".equals(attributeName)) {
+ return isEqualTo(attributeName, "1");
+ } else {
+ return new MonoCondition(attributeName, Operator.IsTrue);
+ }
+ }
+
+ @Override
+ public Condition isFalse(String attributeName) {
+ if ("Exchange2003".equals(this.serverVersion) && "deleted".equals(attributeName)) {
+ return or(isEqualTo(attributeName, "0"), isNull(attributeName));
+ } else {
+ return new MonoCondition(attributeName, Operator.IsFalse);
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public class Message extends ExchangeSession.Message {
+
+ @Override
+ public String getPermanentId() {
+ return permanentUrl;
+ }
+
+ @Override
+ protected InputStream getMimeHeaders() {
+ InputStream result = null;
+ try {
+ String messageHeaders = getItemProperty(permanentUrl, "messageheaders");
+ if (messageHeaders != null) {
+ // workaround for messages in Sent folder
+ if (messageHeaders.indexOf("From:") < 0) {
+ String from = getItemProperty(permanentUrl, "from");
+ messageHeaders = "From: "+from+"\n"+messageHeaders;
+ }
+ result = new ByteArrayInputStream(messageHeaders.getBytes("UTF-8"));
+ }
+ } catch (Exception e) {
+ LOGGER.warn(e.getMessage());
+ }
+
+ return result;
+ }
+ }
+
+
+ /**
+ * @inheritDoc
+ */
+ public class Contact extends ExchangeSession.Contact {
+ /**
+ * Build Contact instance from multistatusResponse info
+ *
+ * @param multiStatusResponse response
+ * @throws URIException on error
+ * @throws DavMailException on error
+ */
+ public Contact(MultiStatusResponse multiStatusResponse) throws URIException, DavMailException {
+ setHref(URIUtil.decode(multiStatusResponse.getHref()));
+ DavPropertySet properties = multiStatusResponse.getProperties(HttpStatus.SC_OK);
+ permanentUrl = getURLPropertyIfExists(properties, "permanenturl");
+ etag = getPropertyIfExists(properties, "etag");
+ displayName = getPropertyIfExists(properties, "displayname");
+ for (String attributeName : CONTACT_ATTRIBUTES) {
+ String value = getPropertyIfExists(properties, attributeName);
+ if (value != null) {
+ if ("bday".equals(attributeName) || "anniversary".equals(attributeName)
+ || "lastmodified".equals(attributeName) || "datereceived".equals(attributeName)) {
+ value = convertDateFromExchange(value);
+ } else if ("haspicture".equals(attributeName) || "private".equals(attributeName)) {
+ value = "1".equals(value) ? "true" : "false";
+ }
+ put(attributeName, value);
+ }
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public Contact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) {
+ super(folderPath, itemName, properties, etag, noneMatch);
+ }
+
+ /**
+ * Default constructor for galFind
+ */
+ public Contact() {
+ }
+
+ protected Set<PropertyValue> buildProperties() {
+ Set<PropertyValue> propertyValues = new HashSet<PropertyValue>();
+ for (Map.Entry<String, String> entry : entrySet()) {
+ String key = entry.getKey();
+ if (!"photo".equals(key)) {
+ propertyValues.add(Field.createPropertyValue(key, entry.getValue()));
+ if (key.startsWith("email")) {
+ propertyValues.add(Field.createPropertyValue(key + "type", "SMTP"));
+ }
+ }
+ }
+
+ return propertyValues;
+ }
+
+ protected ExchangePropPatchMethod internalCreateOrUpdate(String encodedHref) throws IOException {
+ ExchangePropPatchMethod propPatchMethod = new ExchangePropPatchMethod(encodedHref, buildProperties());
+ propPatchMethod.setRequestHeader("Translate", "f");
+ if (etag != null) {
+ propPatchMethod.setRequestHeader("If-Match", etag);
+ }
+ if (noneMatch != null) {
+ propPatchMethod.setRequestHeader("If-None-Match", noneMatch);
+ }
+ try {
+ httpClient.executeMethod(propPatchMethod);
+ } finally {
+ propPatchMethod.releaseConnection();
+ }
+ return propPatchMethod;
+ }
+
+ /**
+ * Create or update contact
+ *
+ * @return action result
+ * @throws IOException on error
+ */
+ public ItemResult createOrUpdate() throws IOException {
+ String encodedHref = URIUtil.encodePath(getHref());
+ ExchangePropPatchMethod propPatchMethod = internalCreateOrUpdate(encodedHref);
+ int status = propPatchMethod.getStatusCode();
+ if (status == HttpStatus.SC_MULTI_STATUS) {
+ status = propPatchMethod.getResponseStatusCode();
+ //noinspection VariableNotUsedInsideIf
+ if (status == HttpStatus.SC_CREATED) {
+ LOGGER.debug("Created contact " + encodedHref);
+ } else {
+ LOGGER.debug("Updated contact " + encodedHref);
+ }
+ } else if (status == HttpStatus.SC_NOT_FOUND) {
+ LOGGER.debug("Contact not found at " + encodedHref + ", searching permanenturl by urlcompname");
+ // failover, search item by urlcompname
+ MultiStatusResponse[] responses = searchItems(folderPath, EVENT_REQUEST_PROPERTIES, DavExchangeSession.this.isEqualTo("urlcompname", convertItemNameToEML(itemName)), FolderQueryTraversal.Shallow, 1);
+ if (responses.length == 1) {
+ encodedHref = getPropertyIfExists(responses[0].getProperties(HttpStatus.SC_OK), "permanenturl");
+ LOGGER.warn("Contact found, permanenturl is " + encodedHref);
+ propPatchMethod = internalCreateOrUpdate(encodedHref);
+ status = propPatchMethod.getStatusCode();
+ if (status == HttpStatus.SC_MULTI_STATUS) {
+ status = propPatchMethod.getResponseStatusCode();
+ LOGGER.debug("Updated contact " + encodedHref);
+ } else {
+ LOGGER.warn("Unable to create or update contact " + status + ' ' + propPatchMethod.getStatusLine());
+ }
+ }
+
+ } else {
+ LOGGER.warn("Unable to create or update contact " + status + ' ' + propPatchMethod.getStatusLine());
+ }
+ ItemResult itemResult = new ItemResult();
+ // 440 means forbidden on Exchange
+ if (status == 440) {
+ status = HttpStatus.SC_FORBIDDEN;
+ }
+ itemResult.status = status;
+
+ if (status == HttpStatus.SC_OK || status == HttpStatus.SC_CREATED) {
+ String contactPictureUrl = URIUtil.encodePath(getHref() + "/ContactPicture.jpg");
+ String photo = get("photo");
+ if (photo != null) {
+ // need to update photo
+ byte[] resizedImageBytes = IOUtil.resizeImage(Base64.decodeBase64(photo.getBytes()), 90);
+
+ final PutMethod putmethod = new PutMethod(contactPictureUrl);
+ putmethod.setRequestHeader("Overwrite", "t");
+ putmethod.setRequestHeader("Content-Type", "image/jpeg");
+ putmethod.setRequestEntity(new ByteArrayRequestEntity(resizedImageBytes, "image/jpeg"));
+ try {
+ status = httpClient.executeMethod(putmethod);
+ if (status != HttpStatus.SC_OK && status != HttpStatus.SC_CREATED) {
+ throw new IOException("Unable to update contact picture: " + status + ' ' + putmethod.getStatusLine());
+ }
+ } catch (IOException e) {
+ LOGGER.error("Error in contact photo create or update", e);
+ throw e;
+ } finally {
+ putmethod.releaseConnection();
+ }
+
+ Set<PropertyValue> picturePropertyValues = new HashSet<PropertyValue>();
+ picturePropertyValues.add(Field.createPropertyValue("attachmentContactPhoto", "true"));
+ // picturePropertyValues.add(Field.createPropertyValue("renderingPosition", "-1"));
+ picturePropertyValues.add(Field.createPropertyValue("attachExtension", ".jpg"));
+
+ final ExchangePropPatchMethod attachmentPropPatchMethod = new ExchangePropPatchMethod(contactPictureUrl, picturePropertyValues);
+ try {
+ status = httpClient.executeMethod(attachmentPropPatchMethod);
+ if (status != HttpStatus.SC_MULTI_STATUS) {
+ LOGGER.error("Error in contact photo create or update: " + attachmentPropPatchMethod.getStatusCode());
+ throw new IOException("Unable to update contact picture");
+ }
+ } finally {
+ attachmentPropPatchMethod.releaseConnection();
+ }
+
+ } else {
+ // try to delete picture
+ DeleteMethod deleteMethod = new DeleteMethod(contactPictureUrl);
+ try {
+ status = httpClient.executeMethod(deleteMethod);
+ if (status != HttpStatus.SC_OK && status != HttpStatus.SC_NOT_FOUND) {
+ LOGGER.error("Error in contact photo delete: " + status);
+ throw new IOException("Unable to delete contact picture");
+ }
+ } finally {
+ deleteMethod.releaseConnection();
+ }
+ }
+ // need to retrieve new etag
+ HeadMethod headMethod = new HeadMethod(URIUtil.encodePath(getHref()));
+ try {
+ httpClient.executeMethod(headMethod);
+ if (headMethod.getResponseHeader("ETag") != null) {
+ itemResult.etag = headMethod.getResponseHeader("ETag").getValue();
+ }
+ } finally {
+ headMethod.releaseConnection();
+ }
+ }
+ return itemResult;
+
+ }
+
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public class Event extends ExchangeSession.Event {
+ protected String instancetype;
+
+ /**
+ * Build Event instance from response info.
+ *
+ * @param multiStatusResponse response
+ * @throws URIException on error
+ */
+ public Event(MultiStatusResponse multiStatusResponse) throws URIException {
+ setHref(URIUtil.decode(multiStatusResponse.getHref()));
+ DavPropertySet properties = multiStatusResponse.getProperties(HttpStatus.SC_OK);
+ permanentUrl = getURLPropertyIfExists(properties, "permanenturl");
+ etag = getPropertyIfExists(properties, "etag");
+ displayName = getPropertyIfExists(properties, "displayname");
+ subject = getPropertyIfExists(properties, "subject");
+ instancetype = getPropertyIfExists(properties, "instancetype");
+ contentClass = getPropertyIfExists(properties, "contentclass");
+ }
+
+ protected String getPermanentUrl() {
+ return permanentUrl;
+ }
+
+
+ /**
+ * @inheritDoc
+ */
+ public Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch) throws IOException {
+ super(folderPath, itemName, contentClass, itemBody, etag, noneMatch);
+ }
+
+ protected byte[] getICSFromInternetContentProperty() throws IOException, DavException, MessagingException {
+ byte[] result = null;
+ // PropFind PR_INTERNET_CONTENT
+ String propertyValue = getItemProperty(permanentUrl, "internetContent");
+ if (propertyValue != null) {
+ byte[] byteArray = Base64.decodeBase64(propertyValue.getBytes());
+ result = getICS(new ByteArrayInputStream(byteArray));
+ }
+ return result;
+ }
+
+ /**
+ * Load ICS content from Exchange server.
+ * User Translate: f header to get MIME event content and get ICS attachment from it
+ *
+ * @return ICS (iCalendar) event
+ * @throws HttpException on error
+ */
+ @Override
+ public byte[] getEventContent() throws IOException {
+ byte[] result = null;
+ LOGGER.debug("Get event subject: " + subject + " href: " + getHref() + " permanentUrl: " + permanentUrl);
+ // try to get PR_INTERNET_CONTENT
+ try {
+ result = getICSFromInternetContentProperty();
+ if (result == null) {
+ GetMethod method = new GetMethod(encodeAndFixUrl(permanentUrl));
+ method.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
+ method.setRequestHeader("Translate", "f");
+ try {
+ DavGatewayHttpClientFacade.executeGetMethod(httpClient, method, true);
+ result = getICS(method.getResponseBodyAsStream());
+ } finally {
+ method.releaseConnection();
+ }
+ }
+ } catch (DavException e) {
+ LOGGER.warn(e.getMessage());
+ } catch (IOException e) {
+ LOGGER.warn(e.getMessage());
+ } catch (MessagingException e) {
+ LOGGER.warn(e.getMessage());
+ }
+
+ // failover: rebuild event from MAPI properties
+ if (result == null) {
+ try {
+ result = getICSFromItemProperties();
+ } catch (HttpException e) {
+ deleteBroken();
+ throw e;
+ }
+ }
+ // debug code
+ /*if (new String(result).indexOf("VTODO") < 0) {
+ LOGGER.debug("Original body: " + new String(result));
+ result = getICSFromItemProperties();
+ LOGGER.debug("Rebuilt body: " + new String(result));
+ }*/
+
+ return result;
+ }
+
+ private byte[] getICSFromItemProperties() throws IOException {
+ byte[] result;
+
+ // experimental: build VCALENDAR from properties
+
+ try {
+ //MultiStatusResponse[] responses = DavGatewayHttpClientFacade.executeMethod(httpClient, propFindMethod);
+ Set<String> eventProperties = new HashSet<String>();
+ eventProperties.add("method");
+
+ eventProperties.add("created");
+ eventProperties.add("calendarlastmodified");
+ eventProperties.add("dtstamp");
+ eventProperties.add("calendaruid");
+ eventProperties.add("subject");
+ eventProperties.add("dtstart");
+ eventProperties.add("dtend");
+ eventProperties.add("transparent");
+ eventProperties.add("organizer");
+ eventProperties.add("to");
+ eventProperties.add("description");
+ eventProperties.add("rrule");
+ eventProperties.add("exdate");
+ eventProperties.add("sensitivity");
+ eventProperties.add("alldayevent");
+ eventProperties.add("busystatus");
+ eventProperties.add("reminderset");
+ eventProperties.add("reminderdelta");
+ // task
+ eventProperties.add("importance");
+ eventProperties.add("uid");
+ eventProperties.add("taskstatus");
+ eventProperties.add("percentcomplete");
+ eventProperties.add("keywords");
+ eventProperties.add("startdate");
+ eventProperties.add("duedate");
+ eventProperties.add("datecompleted");
+
+ MultiStatusResponse[] responses = searchItems(folderPath, eventProperties, DavExchangeSession.this.isEqualTo("urlcompname", convertItemNameToEML(itemName)), FolderQueryTraversal.Shallow, 1);
+ if (responses.length == 0) {
+ throw new HttpNotFoundException(permanentUrl + " not found");
+ }
+ DavPropertySet davPropertySet = responses[0].getProperties(HttpStatus.SC_OK);
+ VCalendar localVCalendar = new VCalendar();
+ localVCalendar.setPropertyValue("PRODID", "-//davmail.sf.net/NONSGML DavMail Calendar V1.1//EN");
+ localVCalendar.setPropertyValue("VERSION", "2.0");
+ localVCalendar.setPropertyValue("METHOD", getPropertyIfExists(davPropertySet, "method"));
+ VObject vEvent = new VObject();
+ vEvent.setPropertyValue("CREATED", convertDateFromExchange(getPropertyIfExists(davPropertySet, "created")));
+ vEvent.setPropertyValue("LAST-MODIFIED", convertDateFromExchange(getPropertyIfExists(davPropertySet, "calendarlastmodified")));
+ vEvent.setPropertyValue("DTSTAMP", convertDateFromExchange(getPropertyIfExists(davPropertySet, "dtstamp")));
+
+ String uid = getPropertyIfExists(davPropertySet, "calendaruid");
+ if (uid == null) {
+ uid = getPropertyIfExists(davPropertySet, "uid");
+ }
+ vEvent.setPropertyValue("UID", uid);
+ vEvent.setPropertyValue("SUMMARY", getPropertyIfExists(davPropertySet, "subject"));
+ vEvent.setPropertyValue("DESCRIPTION", getPropertyIfExists(davPropertySet, "description"));
+ vEvent.setPropertyValue("PRIORITY", convertPriorityFromExchange(getPropertyIfExists(davPropertySet, "importance")));
+ vEvent.setPropertyValue("CATEGORIES", getPropertyIfExists(davPropertySet, "keywords"));
+
+ if (instancetype == null) {
+ vEvent.type = "VTODO";
+ double percentComplete = getDoublePropertyIfExists(davPropertySet, "percentcomplete");
+ if (percentComplete > 0) {
+ vEvent.setPropertyValue("PERCENT-COMPLETE", String.valueOf((int) (percentComplete * 100)));
+ }
+ vEvent.setPropertyValue("STATUS", taskTovTodoStatusMap.get(getPropertyIfExists(davPropertySet, "taskstatus")));
+ vEvent.setPropertyValue("DUE;VALUE=DATE", convertDateFromExchangeToTaskDate(getPropertyIfExists(davPropertySet, "duedate")));
+ vEvent.setPropertyValue("DTSTART;VALUE=DATE", convertDateFromExchangeToTaskDate(getPropertyIfExists(davPropertySet, "startdate")));
+ vEvent.setPropertyValue("COMPLETED;VALUE=DATE", convertDateFromExchangeToTaskDate(getPropertyIfExists(davPropertySet, "datecompleted")));
+
+ } else {
+ vEvent.type = "VEVENT";
+ // check mandatory dtstart value
+ String dtstart = getPropertyIfExists(davPropertySet, "dtstart");
+ if (dtstart != null) {
+ vEvent.setPropertyValue("DTSTART", convertDateFromExchange(dtstart));
+ } else {
+ LOGGER.warn("missing dtstart on item, using fake value. Set davmail.deleteBroken=true to delete broken events");
+ vEvent.setPropertyValue("DTSTART", "20000101T000000Z");
+ deleteBroken();
+ }
+ // same on DTEND
+ String dtend = getPropertyIfExists(davPropertySet, "dtend");
+ if (dtend != null) {
+ vEvent.setPropertyValue("DTEND", convertDateFromExchange(dtend));
+ } else {
+ LOGGER.warn("missing dtend on item, using fake value. Set davmail.deleteBroken=true to delete broken events");
+ vEvent.setPropertyValue("DTEND", "20000101T010000Z");
+ deleteBroken();
+ }
+ vEvent.setPropertyValue("TRANSP", getPropertyIfExists(davPropertySet, "transparent"));
+ vEvent.setPropertyValue("RRULE", getPropertyIfExists(davPropertySet, "rrule"));
+ String exdates = getPropertyIfExists(davPropertySet, "exdate");
+ if (exdates != null) {
+ String[] exdatearray = exdates.split(",");
+ for (String exdate : exdatearray) {
+ vEvent.addPropertyValue("EXDATE",
+ StringUtil.convertZuluDateTimeToAllDay(convertDateFromExchange(exdate)));
+ }
+ }
+ String sensitivity = getPropertyIfExists(davPropertySet, "sensitivity");
+ if ("2".equals(sensitivity)) {
+ vEvent.setPropertyValue("CLASS", "PRIVATE");
+ } else if ("3".equals(sensitivity)) {
+ vEvent.setPropertyValue("CLASS", "CONFIDENTIAL");
+ } else if ("0".equals(sensitivity)) {
+ vEvent.setPropertyValue("CLASS", "PUBLIC");
+ }
+ String organizer = getPropertyIfExists(davPropertySet, "organizer");
+ String organizerEmail = null;
+ if (organizer != null) {
+ InternetAddress organizerAddress = new InternetAddress(organizer);
+ organizerEmail = organizerAddress.getAddress();
+ vEvent.setPropertyValue("ORGANIZER", "MAILTO:" + organizerEmail);
+ }
+
+ // Parse attendee list
+ String toHeader = getPropertyIfExists(davPropertySet, "to");
+ if (toHeader != null && !toHeader.equals(organizerEmail)) {
+ InternetAddress[] attendees = InternetAddress.parseHeader(toHeader, false);
+ for (InternetAddress attendee : attendees) {
+ if (!attendee.getAddress().equalsIgnoreCase(organizerEmail)) {
+ VProperty vProperty = new VProperty("ATTENDEE", attendee.getAddress());
+ if (attendee.getPersonal() != null) {
+ vProperty.addParam("CN", attendee.getPersonal());
+ }
+ vEvent.addProperty(vProperty);
+ }
+ }
+
+ }
+ vEvent.setPropertyValue("X-MICROSOFT-CDO-ALLDAYEVENT",
+ "1".equals(getPropertyIfExists(davPropertySet, "alldayevent")) ? "TRUE" : "FALSE");
+ vEvent.setPropertyValue("X-MICROSOFT-CDO-BUSYSTATUS", getPropertyIfExists(davPropertySet, "busystatus"));
+
+ if ("1".equals(getPropertyIfExists(davPropertySet, "reminderset"))) {
+ VObject vAlarm = new VObject();
+ vAlarm.type = "VALARM";
+ vAlarm.setPropertyValue("ACTION", "DISPLAY");
+ vAlarm.setPropertyValue("DISPLAY", "Reminder");
+ String reminderdelta = getPropertyIfExists(davPropertySet, "reminderdelta");
+ VProperty vProperty = new VProperty("TRIGGER", "-PT" + reminderdelta + 'M');
+ vProperty.addParam("VALUE", "DURATION");
+ vAlarm.addProperty(vProperty);
+ vEvent.addVObject(vAlarm);
+ }
+ }
+
+ localVCalendar.addVObject(vEvent);
+ result = localVCalendar.toString().getBytes("UTF-8");
+ } catch (MessagingException e) {
+ LOGGER.warn("Unable to rebuild event content: " + e.getMessage(), e);
+ throw buildHttpException(e);
+ } catch (IOException e) {
+ LOGGER.warn("Unable to rebuild event content: " + e.getMessage(), e);
+ throw buildHttpException(e);
+ }
+
+ return result;
+ }
+
+ protected void deleteBroken() {
+ // try to delete broken event
+ if (Settings.getBooleanProperty("davmail.deleteBroken")) {
+ LOGGER.warn("Deleting broken event at: " + permanentUrl);
+ try {
+ DavGatewayHttpClientFacade.executeDeleteMethod(httpClient, encodeAndFixUrl(permanentUrl));
+ } catch (IOException ioe) {
+ LOGGER.warn("Unable to delete broken event at: " + permanentUrl);
+ }
+ }
+ }
+
+ protected PutMethod internalCreateOrUpdate(String encodedHref, byte[] mimeContent) throws IOException {
+ PutMethod putmethod = new PutMethod(encodedHref);
+ putmethod.setRequestHeader("Translate", "f");
+ putmethod.setRequestHeader("Overwrite", "f");
+ if (etag != null) {
+ putmethod.setRequestHeader("If-Match", etag);
+ }
+ if (noneMatch != null) {
+ putmethod.setRequestHeader("If-None-Match", noneMatch);
+ }
+ putmethod.setRequestHeader("Content-Type", "message/rfc822");
+ putmethod.setRequestEntity(new ByteArrayRequestEntity(mimeContent, "message/rfc822"));
+ try {
+ httpClient.executeMethod(putmethod);
+ } finally {
+ putmethod.releaseConnection();
+ }
+ return putmethod;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public ItemResult createOrUpdate() throws IOException {
+ ItemResult itemResult = new ItemResult();
+ if (vCalendar.isTodo()) {
+ if ((mailPath + calendarName).equals(folderPath)) {
+ folderPath = mailPath + tasksName;
+ }
+ String encodedHref = URIUtil.encodePath(getHref());
+ Set<PropertyValue> propertyValues = new HashSet<PropertyValue>();
+ // set contentclass on create
+ if (noneMatch != null) {
+ propertyValues.add(Field.createPropertyValue("contentclass", "urn:content-classes:task"));
+ propertyValues.add(Field.createPropertyValue("outlookmessageclass", "IPM.Task"));
+ propertyValues.add(Field.createPropertyValue("calendaruid", vCalendar.getFirstVeventPropertyValue("UID")));
+ }
+ propertyValues.add(Field.createPropertyValue("subject", vCalendar.getFirstVeventPropertyValue("SUMMARY")));
+ propertyValues.add(Field.createPropertyValue("description", vCalendar.getFirstVeventPropertyValue("DESCRIPTION")));
+ propertyValues.add(Field.createPropertyValue("importance", convertPriorityToExchange(vCalendar.getFirstVeventPropertyValue("PRIORITY"))));
+ String percentComplete = vCalendar.getFirstVeventPropertyValue("PERCENT-COMPLETE");
+ if (percentComplete == null) {
+ percentComplete = "0";
+ }
+ propertyValues.add(Field.createPropertyValue("percentcomplete", String.valueOf(Double.parseDouble(percentComplete) / 100)));
+ String taskStatus = vTodoToTaskStatusMap.get(vCalendar.getFirstVeventPropertyValue("STATUS"));
+ propertyValues.add(Field.createPropertyValue("taskstatus", taskStatus));
+ propertyValues.add(Field.createPropertyValue("keywords", vCalendar.getFirstVeventPropertyValue("CATEGORIES")));
+ propertyValues.add(Field.createPropertyValue("startdate", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DTSTART"))));
+ propertyValues.add(Field.createPropertyValue("duedate", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DUE"))));
+ propertyValues.add(Field.createPropertyValue("datecompleted", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("COMPLETED"))));
+
+ propertyValues.add(Field.createPropertyValue("iscomplete", "2".equals(taskStatus) ? "true" : "false"));
+ propertyValues.add(Field.createPropertyValue("commonstart", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DTSTART"))));
+ propertyValues.add(Field.createPropertyValue("commonend", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DUE"))));
+
+ ExchangePropPatchMethod propPatchMethod = new ExchangePropPatchMethod(encodedHref, propertyValues);
+ propPatchMethod.setRequestHeader("Translate", "f");
+ if (etag != null) {
+ propPatchMethod.setRequestHeader("If-Match", etag);
+ }
+ if (noneMatch != null) {
+ propPatchMethod.setRequestHeader("If-None-Match", noneMatch);
+ }
+ try {
+ int status = httpClient.executeMethod(propPatchMethod);
+
+ if (status == HttpStatus.SC_MULTI_STATUS) {
+ Item newItem = getItem(folderPath, itemName);
+ itemResult.status = propPatchMethod.getResponseStatusCode();
+ itemResult.etag = newItem.etag;
+ } else {
+ itemResult.status = status;
+ }
+ } finally {
+ propPatchMethod.releaseConnection();
+ }
+
+ } else {
+ String encodedHref = URIUtil.encodePath(getHref());
+ byte[] mimeContent = createMimeContent();
+ PutMethod putMethod = internalCreateOrUpdate(encodedHref, mimeContent);
+ int status = putMethod.getStatusCode();
+
+ if (status == HttpStatus.SC_OK) {
+ LOGGER.debug("Updated event " + encodedHref);
+ } else if (status == HttpStatus.SC_CREATED) {
+ LOGGER.debug("Created event " + encodedHref);
+ } else if (status == HttpStatus.SC_NOT_FOUND) {
+ LOGGER.debug("Event not found at " + encodedHref + ", searching permanenturl by urlcompname");
+ // failover, search item by urlcompname
+ MultiStatusResponse[] responses = searchItems(folderPath, EVENT_REQUEST_PROPERTIES, DavExchangeSession.this.isEqualTo("urlcompname", convertItemNameToEML(itemName)), FolderQueryTraversal.Shallow, 1);
+ if (responses.length == 1) {
+ encodedHref = getPropertyIfExists(responses[0].getProperties(HttpStatus.SC_OK), "permanenturl");
+ LOGGER.warn("Event found, permanenturl is " + encodedHref);
+ putMethod = internalCreateOrUpdate(encodedHref, mimeContent);
+ status = putMethod.getStatusCode();
+ if (status == HttpStatus.SC_OK) {
+ LOGGER.debug("Updated event " + encodedHref);
+ } else {
+ LOGGER.warn("Unable to create or update event " + status + ' ' + putMethod.getStatusLine());
+ }
+ }
+ } else {
+ LOGGER.warn("Unable to create or update event " + status + ' ' + putMethod.getStatusLine());
+ }
+
+ // 440 means forbidden on Exchange
+ if (status == 440) {
+ status = HttpStatus.SC_FORBIDDEN;
+ }
+ itemResult.status = status;
+ if (putMethod.getResponseHeader("GetETag") != null) {
+ itemResult.etag = putMethod.getResponseHeader("GetETag").getValue();
+ }
+
+ // trigger activeSync push event, only if davmail.forceActiveSyncUpdate setting is true
+ if ((status == HttpStatus.SC_OK || status == HttpStatus.SC_CREATED) &&
+ (Settings.getBooleanProperty("davmail.forceActiveSyncUpdate"))) {
+ ArrayList<DavConstants> propertyList = new ArrayList<DavConstants>();
+ // Set contentclass to make ActiveSync happy
+ propertyList.add(Field.createDavProperty("contentclass", contentClass));
+ // ... but also set PR_INTERNET_CONTENT to preserve custom properties
+ propertyList.add(Field.createDavProperty("internetContent", new String(Base64.encodeBase64(mimeContent))));
+ PropPatchMethod propPatchMethod = new PropPatchMethod(encodedHref, propertyList);
+ int patchStatus = DavGatewayHttpClientFacade.executeHttpMethod(httpClient, propPatchMethod);
+ if (patchStatus != HttpStatus.SC_MULTI_STATUS) {
+ LOGGER.warn("Unable to patch event to trigger activeSync push");
+ } else {
+ // need to retrieve new etag
+ Item newItem = getItem(folderPath, itemName);
+ itemResult.etag = newItem.etag;
+ }
+ }
+ }
+ return itemResult;
+ }
+
+
+ }
+
+ protected Folder buildFolder(MultiStatusResponse entity) throws IOException {
+ String href = URIUtil.decode(entity.getHref());
+ Folder folder = new Folder();
+ DavPropertySet properties = entity.getProperties(HttpStatus.SC_OK);
+ folder.displayName = getPropertyIfExists(properties, "displayname");
+ folder.folderClass = getPropertyIfExists(properties, "folderclass");
+ folder.hasChildren = "1".equals(getPropertyIfExists(properties, "hassubs"));
+ folder.noInferiors = "1".equals(getPropertyIfExists(properties, "nosubs"));
+ folder.unreadCount = getIntPropertyIfExists(properties, "unreadcount");
+ folder.ctag = getPropertyIfExists(properties, "contenttag");
+ folder.etag = getPropertyIfExists(properties, "lastmodified");
+
+ folder.uidNext = getIntPropertyIfExists(properties, "uidNext");
+
+ // replace well known folder names
+ if (inboxUrl != null && href.startsWith(inboxUrl)) {
+ folder.folderPath = href.replaceFirst(inboxUrl, INBOX);
+ } else if (sentitemsUrl != null && href.startsWith(sentitemsUrl)) {
+ folder.folderPath = href.replaceFirst(sentitemsUrl, SENT);
+ } else if (draftsUrl != null && href.startsWith(draftsUrl)) {
+ folder.folderPath = href.replaceFirst(draftsUrl, DRAFTS);
+ } else if (deleteditemsUrl != null && href.startsWith(deleteditemsUrl)) {
+ folder.folderPath = href.replaceFirst(deleteditemsUrl, TRASH);
+ } else if (calendarUrl != null && href.startsWith(calendarUrl)) {
+ folder.folderPath = href.replaceFirst(calendarUrl, CALENDAR);
+ } else if (contactsUrl != null && href.startsWith(contactsUrl)) {
+ folder.folderPath = href.replaceFirst(contactsUrl, CONTACTS);
+ } else {
+ int index = href.indexOf(mailPath.substring(0, mailPath.length() - 1));
+ if (index >= 0) {
+ if (index + mailPath.length() > href.length()) {
+ folder.folderPath = "";
+ } else {
+ folder.folderPath = href.substring(index + mailPath.length());
+ }
+ } else {
+ try {
+ URI folderURI = new URI(href, false);
+ folder.folderPath = folderURI.getPath();
+ } catch (URIException e) {
+ throw new DavMailException("EXCEPTION_INVALID_FOLDER_URL", href);
+ }
+ }
+ }
+ if (folder.folderPath.endsWith("/")) {
+ folder.folderPath = folder.folderPath.substring(0, folder.folderPath.length() - 1);
+ }
+ return folder;
+ }
+
+ protected static final Set<String> FOLDER_PROPERTIES = new HashSet<String>();
+
+ static {
+ FOLDER_PROPERTIES.add("displayname");
+ FOLDER_PROPERTIES.add("folderclass");
+ FOLDER_PROPERTIES.add("hassubs");
+ FOLDER_PROPERTIES.add("nosubs");
+ FOLDER_PROPERTIES.add("unreadcount");
+ FOLDER_PROPERTIES.add("contenttag");
+ FOLDER_PROPERTIES.add("lastmodified");
+ FOLDER_PROPERTIES.add("uidNext");
+ }
+
+ protected static final DavPropertyNameSet FOLDER_PROPERTIES_NAME_SET = new DavPropertyNameSet();
+
+ static {
+ for (String attribute : FOLDER_PROPERTIES) {
+ FOLDER_PROPERTIES_NAME_SET.add(Field.getPropertyName(attribute));
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ protected Folder internalGetFolder(String folderPath) throws IOException {
+ MultiStatusResponse[] responses = DavGatewayHttpClientFacade.executePropFindMethod(
+ httpClient, URIUtil.encodePath(getFolderPath(folderPath)), 0, FOLDER_PROPERTIES_NAME_SET);
+ Folder folder = null;
+ if (responses.length > 0) {
+ folder = buildFolder(responses[0]);
+ folder.folderPath = folderPath;
+ }
+ return folder;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public List<Folder> getSubFolders(String folderPath, Condition condition, boolean recursive) throws IOException {
+ boolean isPublic = folderPath.startsWith("/public");
+ FolderQueryTraversal mode = (!isPublic && recursive) ? FolderQueryTraversal.Deep : FolderQueryTraversal.Shallow;
+ List<Folder> folders = new ArrayList<Folder>();
+
+ MultiStatusResponse[] responses = searchItems(folderPath, FOLDER_PROPERTIES, and(isTrue("isfolder"), isFalse("ishidden"), condition), mode, 0);
+
+ for (MultiStatusResponse response : responses) {
+ Folder folder = buildFolder(response);
+ folders.add(buildFolder(response));
+ if (isPublic && recursive) {
+ getSubFolders(folder.folderPath, condition, recursive);
+ }
+ }
+ return folders;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public int createFolder(String folderPath, String folderClass, Map<String, String> properties) throws IOException {
+ Set<PropertyValue> propertyValues = new HashSet<PropertyValue>();
+ if (properties != null) {
+ for (Map.Entry<String, String> entry : properties.entrySet()) {
+ propertyValues.add(Field.createPropertyValue(entry.getKey(), entry.getValue()));
+ }
+ }
+ propertyValues.add(Field.createPropertyValue("folderclass", folderClass));
+
+ // standard MkColMethod does not take properties, override PropPatchMethod instead
+ ExchangePropPatchMethod method = new ExchangePropPatchMethod(URIUtil.encodePath(getFolderPath(folderPath)), propertyValues) {
+ @Override
+ public String getName() {
+ return "MKCOL";
+ }
+ };
+ int status = DavGatewayHttpClientFacade.executeHttpMethod(httpClient, method);
+ if (status == HttpStatus.SC_MULTI_STATUS) {
+ status = method.getResponseStatusCode();
+ }
+ return status;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public int updateFolder(String folderPath, Map<String, String> properties) throws IOException {
+ Set<PropertyValue> propertyValues = new HashSet<PropertyValue>();
+ if (properties != null) {
+ for (Map.Entry<String, String> entry : properties.entrySet()) {
+ propertyValues.add(Field.createPropertyValue(entry.getKey(), entry.getValue()));
+ }
+ }
+
+ // standard MkColMethod does not take properties, override PropPatchMethod instead
+ ExchangePropPatchMethod method = new ExchangePropPatchMethod(URIUtil.encodePath(getFolderPath(folderPath)), propertyValues);
+ int status = DavGatewayHttpClientFacade.executeHttpMethod(httpClient, method);
+ if (status == HttpStatus.SC_MULTI_STATUS) {
+ status = method.getResponseStatusCode();
+ }
+ return status;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public void deleteFolder(String folderPath) throws IOException {
+ DavGatewayHttpClientFacade.executeDeleteMethod(httpClient, URIUtil.encodePath(getFolderPath(folderPath)));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public void moveFolder(String folderPath, String targetPath) throws IOException {
+ MoveMethod method = new MoveMethod(URIUtil.encodePath(getFolderPath(folderPath)),
+ URIUtil.encodePath(getFolderPath(targetPath)), false);
+ try {
+ int statusCode = httpClient.executeMethod(method);
+ if (statusCode == HttpStatus.SC_PRECONDITION_FAILED) {
+ throw new HttpPreconditionFailedException(BundleMessage.format("EXCEPTION_UNABLE_TO_MOVE_FOLDER"));
+ } else if (statusCode != HttpStatus.SC_CREATED) {
+ throw DavGatewayHttpClientFacade.buildHttpException(method);
+ } else if (folderPath.equalsIgnoreCase("/users/" + getEmail() + "/calendar")) {
+ // calendar renamed, need to reload well known folders
+ getWellKnownFolders();
+ }
+ } finally {
+ method.releaseConnection();
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public void moveItem(String sourcePath, String targetPath) throws IOException {
+ MoveMethod method = new MoveMethod(URIUtil.encodePath(getFolderPath(sourcePath)),
+ URIUtil.encodePath(getFolderPath(targetPath)), false);
+ moveItem(method);
+ }
+
+ protected void moveItem(MoveMethod method) throws IOException {
+ try {
+ int statusCode = httpClient.executeMethod(method);
+ if (statusCode == HttpStatus.SC_PRECONDITION_FAILED) {
+ throw new DavMailException("EXCEPTION_UNABLE_TO_MOVE_ITEM");
+ } else if (statusCode != HttpStatus.SC_CREATED && statusCode != HttpStatus.SC_OK) {
+ throw DavGatewayHttpClientFacade.buildHttpException(method);
+ }
+ } finally {
+ method.releaseConnection();
+ }
+ }
+
+ protected String getPropertyIfExists(DavPropertySet properties, String alias) {
+ DavProperty property = properties.get(Field.getResponsePropertyName(alias));
+ if (property == null) {
+ return null;
+ } else {
+ Object value = property.getValue();
+ if (value instanceof Node) {
+ return ((Node) value).getTextContent();
+ } else if (value instanceof List) {
+ StringBuilder buffer = new StringBuilder();
+ for (Object node : (List) value) {
+ if (buffer.length() > 0) {
+ buffer.append(',');
+ }
+ buffer.append(((Node) node).getTextContent());
+ }
+ return buffer.toString();
+ } else {
+ return (String) value;
+ }
+ }
+ }
+
+ protected String getURLPropertyIfExists(DavPropertySet properties, String alias) throws URIException {
+ String result = getPropertyIfExists(properties, alias);
+ if (result != null) {
+ result = URIUtil.decode(result);
+ }
+ return result;
+ }
+
+ protected int getIntPropertyIfExists(DavPropertySet properties, String alias) {
+ DavProperty property = properties.get(Field.getPropertyName(alias));
+ if (property == null) {
+ return 0;
+ } else {
+ return Integer.parseInt((String) property.getValue());
+ }
+ }
+
+ protected long getLongPropertyIfExists(DavPropertySet properties, String alias) {
+ DavProperty property = properties.get(Field.getPropertyName(alias));
+ if (property == null) {
+ return 0;
+ } else {
+ return Long.parseLong((String) property.getValue());
+ }
+ }
+
+ protected double getDoublePropertyIfExists(DavPropertySet properties, String alias) {
+ DavProperty property = properties.get(Field.getResponsePropertyName(alias));
+ if (property == null) {
+ return 0;
+ } else {
+ return Double.parseDouble((String) property.getValue());
+ }
+ }
+
+ protected byte[] getBinaryPropertyIfExists(DavPropertySet properties, String alias) {
+ byte[] property = null;
+ String base64Property = getPropertyIfExists(properties, alias);
+ if (base64Property != null) {
+ try {
+ property = Base64.decodeBase64(base64Property.getBytes("ASCII"));
+ } catch (UnsupportedEncodingException e) {
+ LOGGER.warn(e);
+ }
+ }
+ return property;
+ }
+
+
+ protected Message buildMessage(MultiStatusResponse responseEntity) throws URIException, DavMailException {
+ Message message = new Message();
+ message.messageUrl = URIUtil.decode(responseEntity.getHref());
+ DavPropertySet properties = responseEntity.getProperties(HttpStatus.SC_OK);
+
+ message.permanentUrl = getURLPropertyIfExists(properties, "permanenturl");
+ message.size = getIntPropertyIfExists(properties, "messageSize");
+ message.uid = getPropertyIfExists(properties, "uid");
+ message.contentClass = getPropertyIfExists(properties, "contentclass");
+ message.imapUid = getLongPropertyIfExists(properties, "imapUid");
+ message.read = "1".equals(getPropertyIfExists(properties, "read"));
+ message.junk = "1".equals(getPropertyIfExists(properties, "junk"));
+ message.flagged = "2".equals(getPropertyIfExists(properties, "flagStatus"));
+ message.draft = (getIntPropertyIfExists(properties, "messageFlags") & 8) != 0;
+ String lastVerbExecuted = getPropertyIfExists(properties, "lastVerbExecuted");
+ message.answered = "102".equals(lastVerbExecuted) || "103".equals(lastVerbExecuted);
+ message.forwarded = "104".equals(lastVerbExecuted);
+ message.date = convertDateFromExchange(getPropertyIfExists(properties, "date"));
+ message.deleted = "1".equals(getPropertyIfExists(properties, "deleted"));
+
+ String lastmodified = convertDateFromExchange(getPropertyIfExists(properties, "lastmodified"));
+ message.recent = !message.read && lastmodified != null && lastmodified.equals(message.date);
+
+ if (LOGGER.isDebugEnabled()) {
+ StringBuilder buffer = new StringBuilder();
+ buffer.append("Message");
+ if (message.imapUid != 0) {
+ buffer.append(" IMAP uid: ").append(message.imapUid);
+ }
+ if (message.uid != null) {
+ buffer.append(" uid: ").append(message.uid);
+ }
+ buffer.append(" href: ").append(responseEntity.getHref()).append(" permanenturl:").append(message.permanentUrl);
+ LOGGER.debug(buffer.toString());
+ }
+ return message;
+ }
+
+ @Override
+ public MessageList searchMessages(String folderPath, Set<String> attributes, Condition condition) throws IOException {
+ MessageList messages = new MessageList();
+ MultiStatusResponse[] responses = searchItems(folderPath, attributes, and(isFalse("isfolder"), isFalse("ishidden"), condition), FolderQueryTraversal.Shallow, 0);
+
+ for (MultiStatusResponse response : responses) {
+ Message message = buildMessage(response);
+ message.messageList = messages;
+ messages.add(message);
+ }
+ Collections.sort(messages);
+ return messages;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public List<ExchangeSession.Contact> searchContacts(String folderPath, Set<String> attributes, Condition condition, int maxCount) throws IOException {
+ List<ExchangeSession.Contact> contacts = new ArrayList<ExchangeSession.Contact>();
+ MultiStatusResponse[] responses = searchItems(folderPath, attributes,
+ and(isEqualTo("outlookmessageclass", "IPM.Contact"), isFalse("isfolder"), isFalse("ishidden"), condition),
+ FolderQueryTraversal.Shallow, maxCount);
+ for (MultiStatusResponse response : responses) {
+ contacts.add(new Contact(response));
+ }
+ return contacts;
+ }
+
+ /**
+ * Common item properties
+ */
+ protected static final Set<String> ITEM_PROPERTIES = new HashSet<String>();
+
+ static {
+ ITEM_PROPERTIES.add("etag");
+ ITEM_PROPERTIES.add("displayname");
+ // calendar CdoInstanceType
+ ITEM_PROPERTIES.add("instancetype");
+ ITEM_PROPERTIES.add("urlcompname");
+ ITEM_PROPERTIES.add("subject");
+ }
+
+ protected Set<String> getItemProperties() {
+ return ITEM_PROPERTIES;
+ }
+
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public List<ExchangeSession.Event> getEventMessages(String folderPath) throws IOException {
+ return searchEvents(folderPath, ITEM_PROPERTIES,
+ and(isEqualTo("contentclass", "urn:content-classes:calendarmessage"),
+ or(isNull("processed"), isFalse("processed"))));
+ }
+
+
+ @Override
+ public List<ExchangeSession.Event> searchEvents(String folderPath, Set<String> attributes, Condition condition) throws IOException {
+ List<ExchangeSession.Event> events = new ArrayList<ExchangeSession.Event>();
+ MultiStatusResponse[] responses = searchItems(folderPath, attributes, and(isFalse("isfolder"), isFalse("ishidden"), condition), FolderQueryTraversal.Shallow, 0);
+ for (MultiStatusResponse response : responses) {
+ String instancetype = getPropertyIfExists(response.getProperties(HttpStatus.SC_OK), "instancetype");
+ Event event = new Event(response);
+ //noinspection VariableNotUsedInsideIf
+ if (instancetype == null) {
+ // check ics content
+ try {
+ event.getBody();
+ // getBody success => add event or task
+ events.add(event);
+ } catch (IOException e) {
+ // invalid event: exclude from list
+ LOGGER.warn("Invalid event " + event.displayName + " found at " + response.getHref(), e);
+ }
+ } else {
+ events.add(event);
+ }
+ }
+ return events;
+ }
+
+ @Override
+ protected Condition getCalendarItemCondition(Condition dateCondition) {
+ boolean caldavEnableLegacyTasks = Settings.getBooleanProperty("davmail.caldavEnableLegacyTasks", false);
+ if (caldavEnableLegacyTasks) {
+ // return tasks created in calendar folder
+ return or(isNull("instancetype"),
+ isEqualTo("instancetype", 1),
+ and(isEqualTo("instancetype", 0), dateCondition));
+ } else {
+ // instancetype 0 single appointment / 1 master recurring appointment
+ return and(isEqualTo("outlookmessageclass", "IPM.Appointment"),
+ or(isEqualTo("instancetype", 1),
+ and(isEqualTo("instancetype", 0), dateCondition)));
+ }
+ }
+
+ protected MultiStatusResponse[] searchItems(String folderPath, Set<String> attributes, Condition condition,
+ FolderQueryTraversal folderQueryTraversal, int maxCount) throws IOException {
+ String folderUrl;
+ if (folderPath.startsWith("http")) {
+ folderUrl = folderPath;
+ } else {
+ folderUrl = getFolderPath(folderPath);
+ }
+ StringBuilder searchRequest = new StringBuilder();
+ searchRequest.append("SELECT ")
+ .append(Field.getRequestPropertyString("permanenturl"));
+ if (attributes != null) {
+ for (String attribute : attributes) {
+ searchRequest.append(',').append(Field.getRequestPropertyString(attribute));
+ }
+ }
+ searchRequest.append(" FROM SCOPE('").append(folderQueryTraversal).append(" TRAVERSAL OF \"").append(folderUrl).append("\"')");
+ if (condition != null) {
+ searchRequest.append(" WHERE ");
+ condition.appendTo(searchRequest);
+ }
+ DavGatewayTray.debug(new BundleMessage("LOG_SEARCH_QUERY", searchRequest));
+ MultiStatusResponse[] responses = DavGatewayHttpClientFacade.executeSearchMethod(
+ httpClient, encodeAndFixUrl(folderUrl), searchRequest.toString(), maxCount);
+ DavGatewayTray.debug(new BundleMessage("LOG_SEARCH_RESULT", responses.length));
+ return responses;
+ }
+
+ protected static final Set<String> EVENT_REQUEST_PROPERTIES = new HashSet<String>();
+
+ static {
+ EVENT_REQUEST_PROPERTIES.add("permanenturl");
+ EVENT_REQUEST_PROPERTIES.add("urlcompname");
+ EVENT_REQUEST_PROPERTIES.add("etag");
+ EVENT_REQUEST_PROPERTIES.add("contentclass");
+ EVENT_REQUEST_PROPERTIES.add("displayname");
+ EVENT_REQUEST_PROPERTIES.add("subject");
+ }
+
+ protected static final DavPropertyNameSet EVENT_REQUEST_PROPERTIES_NAME_SET = new DavPropertyNameSet();
+
+ static {
+ for (String attribute : EVENT_REQUEST_PROPERTIES) {
+ EVENT_REQUEST_PROPERTIES_NAME_SET.add(Field.getPropertyName(attribute));
+ }
+
+ }
+
+ @Override
+ public Item getItem(String folderPath, String itemName) throws IOException {
+ String emlItemName = convertItemNameToEML(itemName);
+ String itemPath = getFolderPath(folderPath) + '/' + emlItemName;
+ MultiStatusResponse[] responses = null;
+ try {
+ try {
+ responses = DavGatewayHttpClientFacade.executePropFindMethod(httpClient, URIUtil.encodePath(itemPath), 0, EVENT_REQUEST_PROPERTIES_NAME_SET);
+ } catch (HttpNotFoundException e) {
+ // ignore
+ }
+ if (responses == null || responses.length == 0 && isMainCalendar(folderPath)) {
+ if (itemName.endsWith(".ics")) {
+ itemName = itemName.substring(0, itemName.length() - 3) + "EML";
+ }
+ // look for item in tasks folder
+ responses = DavGatewayHttpClientFacade.executePropFindMethod(httpClient, URIUtil.encodePath(getFolderPath(TASKS) + '/' + emlItemName), 0, EVENT_REQUEST_PROPERTIES_NAME_SET);
+ }
+ if (responses == null || responses.length == 0) {
+ throw new HttpNotFoundException(itemPath + " not found");
+ }
+ } catch (HttpNotFoundException e) {
+ try {
+ LOGGER.debug(itemPath + " not found, searching by urlcompname");
+ // failover: try to get event by displayname
+ responses = searchItems(folderPath, EVENT_REQUEST_PROPERTIES, isEqualTo("urlcompname", emlItemName), FolderQueryTraversal.Shallow, 1);
+ if (responses.length == 0 && isMainCalendar(folderPath)) {
+ responses = searchItems(TASKS, EVENT_REQUEST_PROPERTIES, isEqualTo("urlcompname", emlItemName), FolderQueryTraversal.Shallow, 1);
+ }
+ if (responses.length == 0) {
+ throw new HttpNotFoundException(itemPath + " not found");
+ }
+ } catch (HttpNotFoundException e2) {
+ LOGGER.debug("last failover: search all items");
+ List<ExchangeSession.Event> events = getAllEvents(folderPath);
+ for (ExchangeSession.Event event : events) {
+ if (itemName.equals(event.getName())) {
+ responses = DavGatewayHttpClientFacade.executePropFindMethod(httpClient, encodeAndFixUrl(((DavExchangeSession.Event) event).getPermanentUrl()), 0, EVENT_REQUEST_PROPERTIES_NAME_SET);
+ break;
+ }
+ }
+ if (responses == null || responses.length == 0) {
+ throw new HttpNotFoundException(itemPath + " not found");
+ }
+ LOGGER.warn("search by urlcompname failed, actual value is " + getPropertyIfExists(responses[0].getProperties(HttpStatus.SC_OK), "urlcompname"));
+ }
+ }
+ // build item
+ String contentClass = getPropertyIfExists(responses[0].getProperties(HttpStatus.SC_OK), "contentclass");
+ String urlcompname = getPropertyIfExists(responses[0].getProperties(HttpStatus.SC_OK), "urlcompname");
+ if ("urn:content-classes:person".equals(contentClass)) {
+ // retrieve Contact properties
+ List<ExchangeSession.Contact> contacts = searchContacts(folderPath, CONTACT_ATTRIBUTES,
+ isEqualTo("urlcompname", StringUtil.decodeUrlcompname(urlcompname)), 1);
+ if (contacts.isEmpty()) {
+ LOGGER.warn("Item found, but unable to build contact");
+ throw new HttpNotFoundException(itemPath + " not found");
+ }
+ return contacts.get(0);
+ } else if ("urn:content-classes:appointment".equals(contentClass)
+ || "urn:content-classes:calendarmessage".equals(contentClass)
+ || "urn:content-classes:task".equals(contentClass)) {
+ return new Event(responses[0]);
+ } else {
+ LOGGER.warn("wrong contentclass on item " + itemPath + ": " + contentClass);
+ // return item anyway
+ return new Event(responses[0]);
+ }
+
+ }
+
+ @Override
+ public ExchangeSession.ContactPhoto getContactPhoto(ExchangeSession.Contact contact) throws IOException {
+ ContactPhoto contactPhoto = null;
+ if ("true".equals(contact.get("haspicture"))) {
+ final GetMethod method = new GetMethod(URIUtil.encodePath(contact.getHref()) + "/ContactPicture.jpg");
+ method.setRequestHeader("Translate", "f");
+ method.setRequestHeader("Accept-Encoding", "gzip");
+
+ InputStream inputStream = null;
+ try {
+ DavGatewayHttpClientFacade.executeGetMethod(httpClient, method, true);
+ if (DavGatewayHttpClientFacade.isGzipEncoded(method)) {
+ inputStream = (new GZIPInputStream(method.getResponseBodyAsStream()));
+ } else {
+ inputStream = method.getResponseBodyAsStream();
+ }
+
+ contactPhoto = new ContactPhoto();
+ contactPhoto.contentType = "image/jpeg";
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ InputStream partInputStream = inputStream;
+ byte[] bytes = new byte[8192];
+ int length;
+ while ((length = partInputStream.read(bytes)) > 0) {
+ baos.write(bytes, 0, length);
+ }
+ contactPhoto.content = new String(Base64.encodeBase64(baos.toByteArray()));
+ } finally {
+ if (inputStream != null) {
+ try {
+ inputStream.close();
+ } catch (IOException e) {
+ LOGGER.debug(e);
+ }
+ }
+ method.releaseConnection();
+ }
+ }
+ return contactPhoto;
+ }
+
+ @Override
+ public int sendEvent(String icsBody) throws IOException {
+ String itemName = UUID.randomUUID().toString() + ".EML";
+ byte[] mimeContent = (new Event(getFolderPath(DRAFTS), itemName, "urn:content-classes:calendarmessage", icsBody, null, null)).createMimeContent();
+ if (mimeContent == null) {
+ // no recipients, cancel
+ return HttpStatus.SC_NO_CONTENT;
+ } else {
+ sendMessage(mimeContent);
+ return HttpStatus.SC_OK;
+ }
+ }
+
+ @Override
+ public void deleteItem(String folderPath, String itemName) throws IOException {
+ String eventPath = URIUtil.encodePath(getFolderPath(folderPath) + '/' + convertItemNameToEML(itemName));
+ int status = DavGatewayHttpClientFacade.executeDeleteMethod(httpClient, eventPath);
+ if (status == HttpStatus.SC_NOT_FOUND && isMainCalendar(folderPath)) {
+ // retry in tasks folder
+ eventPath = URIUtil.encodePath(getFolderPath(TASKS) + '/' + convertItemNameToEML(itemName));
+ status = DavGatewayHttpClientFacade.executeDeleteMethod(httpClient, eventPath);
+ }
+ if (status == HttpStatus.SC_NOT_FOUND) {
+ LOGGER.debug("Unable to delete " + itemName + ": item not found");
+ }
+ }
+
+ @Override
+ public void processItem(String folderPath, String itemName) throws IOException {
+ String eventPath = URIUtil.encodePath(getFolderPath(folderPath) + '/' + convertItemNameToEML(itemName));
+ // do not delete calendar messages, mark read and processed
+ ArrayList<DavConstants> list = new ArrayList<DavConstants>();
+ list.add(Field.createDavProperty("processed", "true"));
+ list.add(Field.createDavProperty("read", "1"));
+ PropPatchMethod patchMethod = new PropPatchMethod(eventPath, list);
+ DavGatewayHttpClientFacade.executeMethod(httpClient, patchMethod);
+ }
+
+ @Override
+ public ItemResult internalCreateOrUpdateEvent(String folderPath, String itemName, String contentClass, String icsBody, String etag, String noneMatch) throws IOException {
+ return new Event(getFolderPath(folderPath), itemName, contentClass, icsBody, etag, noneMatch).createOrUpdate();
+ }
+
+ /**
+ * create a fake event to get VTIMEZONE body
+ */
+ @Override
+ protected void loadVtimezone() {
+ try {
+ // create temporary folder
+ String folderPath = getFolderPath("davmailtemp");
+ createCalendarFolder(folderPath, null);
+
+ String fakeEventUrl = null;
+ if ("Exchange2003".equals(serverVersion)) {
+ PostMethod postMethod = new PostMethod(URIUtil.encodePath(folderPath));
+ postMethod.addParameter("Cmd", "saveappt");
+ postMethod.addParameter("FORMTYPE", "appointment");
+ try {
+ // create fake event
+ int statusCode = httpClient.executeMethod(postMethod);
+ if (statusCode == HttpStatus.SC_OK) {
+ fakeEventUrl = StringUtil.getToken(postMethod.getResponseBodyAsString(), "<span id=\"itemHREF\">", "</span>");
+ if (fakeEventUrl != null) {
+ fakeEventUrl = URIUtil.decode(fakeEventUrl);
+ }
+ }
+ } finally {
+ postMethod.releaseConnection();
+ }
+ }
+ // failover for Exchange 2007, use PROPPATCH with forced timezone
+ if (fakeEventUrl == null) {
+ ArrayList<DavConstants> propertyList = new ArrayList<DavConstants>();
+ propertyList.add(Field.createDavProperty("contentclass", "urn:content-classes:appointment"));
+ propertyList.add(Field.createDavProperty("outlookmessageclass", "IPM.Appointment"));
+ propertyList.add(Field.createDavProperty("instancetype", "0"));
+
+ // get forced timezone id from settings
+ String timezoneId = Settings.getProperty("davmail.timezoneId");
+ if (timezoneId == null) {
+ // get timezoneid from OWA settings
+ timezoneId = getTimezoneIdFromExchange();
+ }
+ // without a timezoneId, use Exchange timezone
+ if (timezoneId != null) {
+ propertyList.add(Field.createDavProperty("timezoneid", timezoneId));
+ }
+ String patchMethodUrl = folderPath + '/' + UUID.randomUUID().toString() + ".EML";
+ PropPatchMethod patchMethod = new PropPatchMethod(URIUtil.encodePath(patchMethodUrl), propertyList);
+ try {
+ int statusCode = httpClient.executeMethod(patchMethod);
+ if (statusCode == HttpStatus.SC_MULTI_STATUS) {
+ fakeEventUrl = patchMethodUrl;
+ }
+ } finally {
+ patchMethod.releaseConnection();
+ }
+ }
+ if (fakeEventUrl != null) {
+ // get fake event body
+ GetMethod getMethod = new GetMethod(URIUtil.encodePath(fakeEventUrl));
+ getMethod.setRequestHeader("Translate", "f");
+ try {
+ httpClient.executeMethod(getMethod);
+ this.vTimezone = new VObject("BEGIN:VTIMEZONE" +
+ StringUtil.getToken(getMethod.getResponseBodyAsString(), "BEGIN:VTIMEZONE", "END:VTIMEZONE") +
+ "END:VTIMEZONE\r\n");
+ } finally {
+ getMethod.releaseConnection();
+ }
+ }
+
+ // delete temporary folder
+ deleteFolder("davmailtemp");
+ } catch (IOException e) {
+ LOGGER.warn("Unable to get VTIMEZONE info: " + e, e);
+ }
+ }
+
+ protected String getTimezoneIdFromExchange() {
+ String timezoneId = null;
+ String timezoneName = null;
+ try {
+ Set<String> attributes = new HashSet<String>();
+ attributes.add("roamingdictionary");
+
+ MultiStatusResponse[] responses = searchItems("/users/" + getEmail() + "/NON_IPM_SUBTREE", attributes, isEqualTo("messageclass", "IPM.Configuration.OWA.UserOptions"), DavExchangeSession.FolderQueryTraversal.Deep, 1);
+ if (responses.length == 1) {
+ byte[] roamingdictionary = getBinaryPropertyIfExists(responses[0].getProperties(HttpStatus.SC_OK), "roamingdictionary");
+ if (roamingdictionary != null) {
+ timezoneName = getTimezoneNameFromRoamingDictionary(roamingdictionary);
+ if (timezoneName != null) {
+ timezoneId = ResourceBundle.getBundle("timezoneids").getString(timezoneName);
+ }
+ }
+ }
+ } catch (MissingResourceException e) {
+ LOGGER.warn("Unable to retrieve Exchange timezone id for name " + timezoneName);
+ } catch (UnsupportedEncodingException e) {
+ LOGGER.warn("Unable to retrieve Exchange timezone id: " + e.getMessage(), e);
+ } catch (IOException e) {
+ LOGGER.warn("Unable to retrieve Exchange timezone id: " + e.getMessage(), e);
+ }
+ return timezoneId;
+ }
+
+ protected String getTimezoneNameFromRoamingDictionary(byte[] roamingdictionary) {
+ String timezoneName = null;
+ XMLStreamReader reader;
+ try {
+ reader = XMLStreamUtil.createXMLStreamReader(roamingdictionary);
+ while (reader.hasNext()) {
+ reader.next();
+ if (XMLStreamUtil.isStartTag(reader, "e")
+ && "18-timezone".equals(reader.getAttributeValue(null, "k"))) {
+ String value = reader.getAttributeValue(null, "v");
+ if (value != null && value.startsWith("18-")) {
+ timezoneName = value.substring(3);
+ }
+ }
+ }
+
+ } catch (XMLStreamException e) {
+ LOGGER.error("Error while parsing RoamingDictionary: " + e, e);
+ }
+ return timezoneName;
+ }
+
+ @Override
+ protected ItemResult internalCreateOrUpdateContact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) throws IOException {
+ return new Contact(getFolderPath(folderPath), itemName, properties, etag, noneMatch).createOrUpdate();
+ }
+
+ protected List<DavConstants> buildProperties(Map<String, String> properties) {
+ ArrayList<DavConstants> list = new ArrayList<DavConstants>();
+ if (properties != null) {
+ for (Map.Entry<String, String> entry : properties.entrySet()) {
+ if ("read".equals(entry.getKey())) {
+ list.add(Field.createDavProperty("read", entry.getValue()));
+ } else if ("junk".equals(entry.getKey())) {
+ list.add(Field.createDavProperty("junk", entry.getValue()));
+ } else if ("flagged".equals(entry.getKey())) {
+ list.add(Field.createDavProperty("flagStatus", entry.getValue()));
+ } else if ("answered".equals(entry.getKey())) {
+ list.add(Field.createDavProperty("lastVerbExecuted", entry.getValue()));
+ if ("102".equals(entry.getValue())) {
+ list.add(Field.createDavProperty("iconIndex", "261"));
+ }
+ } else if ("forwarded".equals(entry.getKey())) {
+ list.add(Field.createDavProperty("lastVerbExecuted", entry.getValue()));
+ if ("104".equals(entry.getValue())) {
+ list.add(Field.createDavProperty("iconIndex", "262"));
+ }
+ } else if ("bcc".equals(entry.getKey())) {
+ list.add(Field.createDavProperty("bcc", entry.getValue()));
+ } else if ("deleted".equals(entry.getKey())) {
+ list.add(Field.createDavProperty("deleted", entry.getValue()));
+ } else if ("datereceived".equals(entry.getKey())) {
+ list.add(Field.createDavProperty("datereceived", entry.getValue()));
+ }
+ }
+ }
+ return list;
+ }
+
+ /**
+ * Create message in specified folder.
+ * Will overwrite an existing message with same messageName in the same folder
+ *
+ * @param folderPath Exchange folder path
+ * @param messageName message name
+ * @param properties message properties (flags)
+ * @param mimeMessage MIME message
+ * @throws IOException when unable to create message
+ */
+ @Override
+ public void createMessage(String folderPath, String messageName, HashMap<String, String> properties, MimeMessage mimeMessage) throws IOException {
+ String messageUrl = URIUtil.encodePathQuery(getFolderPath(folderPath) + '/' + messageName);
+ PropPatchMethod patchMethod;
+ List<DavConstants> davProperties = buildProperties(properties);
+
+ if (properties != null && properties.containsKey("draft")) {
+ // note: draft is readonly after create, create the message first with requested messageFlags
+ davProperties.add(Field.createDavProperty("messageFlags", properties.get("draft")));
+ }
+ if (properties != null && properties.containsKey("mailOverrideFormat")) {
+ davProperties.add(Field.createDavProperty("mailOverrideFormat", properties.get("mailOverrideFormat")));
+ }
+ if (properties != null && properties.containsKey("messageFormat")) {
+ davProperties.add(Field.createDavProperty("messageFormat", properties.get("messageFormat")));
+ }
+ if (!davProperties.isEmpty()) {
+ patchMethod = new PropPatchMethod(messageUrl, davProperties);
+ try {
+ // update message with blind carbon copy and other flags
+ int statusCode = httpClient.executeMethod(patchMethod);
+ if (statusCode != HttpStatus.SC_MULTI_STATUS) {
+ throw new DavMailException("EXCEPTION_UNABLE_TO_CREATE_MESSAGE", messageUrl, statusCode, ' ', patchMethod.getStatusLine());
+ }
+
+ } finally {
+ patchMethod.releaseConnection();
+ }
+ }
+
+ // update message body
+ PutMethod putmethod = new PutMethod(messageUrl);
+ putmethod.setRequestHeader("Translate", "f");
+ putmethod.setRequestHeader("Content-Type", "message/rfc822");
+
+ try {
+ // use same encoding as client socket reader
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ mimeMessage.writeTo(baos);
+ baos.close();
+ putmethod.setRequestEntity(new ByteArrayRequestEntity(baos.toByteArray()));
+ int code = httpClient.executeMethod(putmethod);
+
+ // workaround for misconfigured Exchange server
+ if (code == HttpStatus.SC_NOT_ACCEPTABLE) {
+ LOGGER.warn("Draft message creation failed, failover to property update. Note: attachments are lost");
+
+ ArrayList<DavConstants> propertyList = new ArrayList<DavConstants>();
+ propertyList.add(Field.createDavProperty("to", mimeMessage.getHeader("to", ",")));
+ propertyList.add(Field.createDavProperty("cc", mimeMessage.getHeader("cc", ",")));
+ propertyList.add(Field.createDavProperty("message-id", mimeMessage.getHeader("message-id", ",")));
+
+ MimePart mimePart = mimeMessage;
+ if (mimeMessage.getContent() instanceof MimeMultipart) {
+ MimeMultipart multiPart = (MimeMultipart) mimeMessage.getContent();
+ for (int i = 0; i < multiPart.getCount(); i++) {
+ String contentType = multiPart.getBodyPart(i).getContentType();
+ if (contentType.startsWith("text/")) {
+ mimePart = (MimePart) multiPart.getBodyPart(i);
+ break;
+ }
+ }
+ }
+
+ String contentType = mimePart.getContentType();
+
+ if (contentType.startsWith("text/plain")) {
+ propertyList.add(Field.createDavProperty("description", (String) mimePart.getContent()));
+ } else if (contentType.startsWith("text/html")) {
+ propertyList.add(Field.createDavProperty("htmldescription", (String) mimePart.getContent()));
+ } else {
+ LOGGER.warn("Unsupported content type: " + contentType + " message body will be empty");
+ }
+
+ propertyList.add(Field.createDavProperty("subject", mimeMessage.getHeader("subject", ",")));
+ PropPatchMethod propPatchMethod = new PropPatchMethod(messageUrl, propertyList);
+ try {
+ int patchStatus = DavGatewayHttpClientFacade.executeHttpMethod(httpClient, propPatchMethod);
+ if (patchStatus == HttpStatus.SC_MULTI_STATUS) {
+ code = HttpStatus.SC_OK;
+ }
+ } finally {
+ propPatchMethod.releaseConnection();
+ }
+ }
+
+ if (code != HttpStatus.SC_OK && code != HttpStatus.SC_CREATED) {
+
+ // first delete draft message
+ if (!davProperties.isEmpty()) {
+ try {
+ DavGatewayHttpClientFacade.executeDeleteMethod(httpClient, messageUrl);
+ } catch (IOException e) {
+ LOGGER.warn("Unable to delete draft message");
+ }
+ }
+ if (code == HttpStatus.SC_INSUFFICIENT_STORAGE) {
+ throw new InsufficientStorageException(putmethod.getStatusText());
+ } else {
+ throw new DavMailException("EXCEPTION_UNABLE_TO_CREATE_MESSAGE", messageUrl, code, ' ', putmethod.getStatusLine());
+ }
+ }
+ } catch (MessagingException e) {
+ throw new IOException(e.getMessage());
+ } finally {
+ putmethod.releaseConnection();
+ }
+
+ try {
+ // need to update bcc after put
+ if (mimeMessage.getHeader("Bcc") != null) {
+ davProperties = new ArrayList<DavConstants>();
+ davProperties.add(Field.createDavProperty("bcc", mimeMessage.getHeader("Bcc", ",")));
+ patchMethod = new PropPatchMethod(messageUrl, davProperties);
+ try {
+ // update message with blind carbon copy
+ int statusCode = httpClient.executeMethod(patchMethod);
+ if (statusCode != HttpStatus.SC_MULTI_STATUS) {
+ throw new DavMailException("EXCEPTION_UNABLE_TO_CREATE_MESSAGE", messageUrl, statusCode, ' ', patchMethod.getStatusLine());
+ }
+
+ } finally {
+ patchMethod.releaseConnection();
+ }
+ }
+ } catch (MessagingException e) {
+ throw new IOException(e.getMessage());
+ }
+
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public void updateMessage(ExchangeSession.Message message, Map<String, String> properties) throws IOException {
+ PropPatchMethod patchMethod = new PropPatchMethod(encodeAndFixUrl(message.permanentUrl), buildProperties(properties)) {
+ @Override
+ protected void processResponseBody(HttpState httpState, HttpConnection httpConnection) {
+ // ignore response body, sometimes invalid with exchange mapi properties
+ }
+ };
+ try {
+ int statusCode = httpClient.executeMethod(patchMethod);
+ if (statusCode != HttpStatus.SC_MULTI_STATUS) {
+ throw new DavMailException("EXCEPTION_UNABLE_TO_UPDATE_MESSAGE");
+ }
+
+ } finally {
+ patchMethod.releaseConnection();
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public void deleteMessage(ExchangeSession.Message message) throws IOException {
+ LOGGER.debug("Delete " + message.permanentUrl + " (" + message.messageUrl + ')');
+ DavGatewayHttpClientFacade.executeDeleteMethod(httpClient, encodeAndFixUrl(message.permanentUrl));
+ }
+
+ /**
+ * Send message.
+ *
+ * @param messageBody MIME message body
+ * @throws IOException on error
+ */
+ public void sendMessage(byte[] messageBody) throws IOException {
+ try {
+ sendMessage(new MimeMessage(null, new SharedByteArrayInputStream(messageBody)));
+ } catch (MessagingException e) {
+ throw new IOException(e.getMessage());
+ }
+ }
+
+ //protected static final long MAPI_SEND_NO_RICH_INFO = 0x00010000L;
+ protected static final long ENCODING_PREFERENCE = 0x00020000L;
+ protected static final long ENCODING_MIME = 0x00040000L;
+ //protected static final long BODY_ENCODING_HTML = 0x00080000L;
+ protected static final long BODY_ENCODING_TEXT_AND_HTML = 0x00100000L;
+ //protected static final long MAC_ATTACH_ENCODING_UUENCODE = 0x00200000L;
+ //protected static final long MAC_ATTACH_ENCODING_APPLESINGLE = 0x00400000L;
+ //protected static final long MAC_ATTACH_ENCODING_APPLEDOUBLE = 0x00600000L;
+ //protected static final long OOP_DONT_LOOKUP = 0x10000000L;
+
+ @Override
+ public void sendMessage(MimeMessage mimeMessage) throws IOException {
+ try {
+ // need to create draft first
+ String itemName = UUID.randomUUID().toString() + ".EML";
+ HashMap<String, String> properties = new HashMap<String, String>();
+ properties.put("draft", "9");
+ String contentType = mimeMessage.getContentType();
+ if (contentType != null && contentType.startsWith("text/plain")) {
+ properties.put("messageFormat", "1");
+ } else {
+ properties.put("mailOverrideFormat", String.valueOf(ENCODING_PREFERENCE | ENCODING_MIME | BODY_ENCODING_TEXT_AND_HTML));
+ properties.put("messageFormat", "2");
+ }
+ createMessage(DRAFTS, itemName, properties, mimeMessage);
+ MoveMethod method = new MoveMethod(URIUtil.encodePath(getFolderPath(DRAFTS + '/' + itemName)),
+ URIUtil.encodePath(getFolderPath(SENDMSG)), false);
+ // set header if saveInSent is disabled
+ if (!Settings.getBooleanProperty("davmail.smtpSaveInSent", true)) {
+ method.setRequestHeader("Saveinsent", "f");
+ }
+ moveItem(method);
+ } catch (MessagingException e) {
+ throw new IOException(e.getMessage());
+ }
+ }
+
+ // wrong hostname fix flag
+ protected boolean restoreHostName;
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ protected byte[] getContent(ExchangeSession.Message message) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ InputStream contentInputStream;
+ try {
+ try {
+ try {
+ contentInputStream = getContentInputStream(message.messageUrl);
+ } catch (UnknownHostException e) {
+ // failover for misconfigured Exchange server, replace host name in url
+ restoreHostName = true;
+ contentInputStream = getContentInputStream(message.messageUrl);
+ }
+ } catch (HttpNotFoundException e) {
+ LOGGER.debug("Message not found at: " + message.messageUrl + ", retrying with permanenturl");
+ contentInputStream = getContentInputStream(message.permanentUrl);
+ }
+
+ try {
+ IOUtil.write(contentInputStream, baos);
+ } finally {
+ contentInputStream.close();
+ }
+
+ } catch (LoginTimeoutException e) {
+ // throw error on expired session
+ LOGGER.warn(e.getMessage());
+ throw e;
+ } catch (IOException e) {
+ LOGGER.warn("Broken message at: " + message.messageUrl + " permanentUrl: " + message.permanentUrl + ", trying to rebuild from properties");
+
+ try {
+ DavPropertyNameSet messageProperties = new DavPropertyNameSet();
+ messageProperties.add(Field.getPropertyName("contentclass"));
+ messageProperties.add(Field.getPropertyName("message-id"));
+ messageProperties.add(Field.getPropertyName("from"));
+ messageProperties.add(Field.getPropertyName("to"));
+ messageProperties.add(Field.getPropertyName("cc"));
+ messageProperties.add(Field.getPropertyName("subject"));
+ messageProperties.add(Field.getPropertyName("date"));
+ messageProperties.add(Field.getPropertyName("htmldescription"));
+ messageProperties.add(Field.getPropertyName("body"));
+ PropFindMethod propFindMethod = new PropFindMethod(encodeAndFixUrl(message.permanentUrl), messageProperties, 0);
+ DavGatewayHttpClientFacade.executeMethod(httpClient, propFindMethod);
+ MultiStatus responses = propFindMethod.getResponseBodyAsMultiStatus();
+ if (responses.getResponses().length > 0) {
+ MimeMessage mimeMessage = new MimeMessage((Session) null);
+
+ DavPropertySet properties = responses.getResponses()[0].getProperties(HttpStatus.SC_OK);
+ String propertyValue = getPropertyIfExists(properties, "contentclass");
+ if (propertyValue != null) {
+ mimeMessage.addHeader("Content-class", propertyValue);
+ }
+ propertyValue = getPropertyIfExists(properties, "date");
+ if (propertyValue != null) {
+ mimeMessage.setSentDate(parseDateFromExchange(propertyValue));
+ }
+ propertyValue = getPropertyIfExists(properties, "from");
+ if (propertyValue != null) {
+ mimeMessage.addHeader("From", propertyValue);
+ }
+ propertyValue = getPropertyIfExists(properties, "to");
+ if (propertyValue != null) {
+ mimeMessage.addHeader("To", propertyValue);
+ }
+ propertyValue = getPropertyIfExists(properties, "cc");
+ if (propertyValue != null) {
+ mimeMessage.addHeader("Cc", propertyValue);
+ }
+ propertyValue = getPropertyIfExists(properties, "subject");
+ if (propertyValue != null) {
+ mimeMessage.setSubject(propertyValue);
+ }
+ propertyValue = getPropertyIfExists(properties, "htmldescription");
+ if (propertyValue != null) {
+ mimeMessage.setContent(propertyValue, "text/html; charset=UTF-8");
+ } else {
+ propertyValue = getPropertyIfExists(properties, "body");
+ if (propertyValue != null) {
+ mimeMessage.setText(propertyValue);
+ }
+ }
+ mimeMessage.writeTo(baos);
+ }
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("Rebuilt message content: " + new String(baos.toByteArray()));
+ }
+ } catch (IOException e2) {
+ LOGGER.warn(e2);
+ } catch (DavException e2) {
+ LOGGER.warn(e2);
+ } catch (MessagingException e2) {
+ LOGGER.warn(e2);
+ }
+ // other exception
+ if (baos.size() == 0 && Settings.getBooleanProperty("davmail.deleteBroken")) {
+ LOGGER.warn("Deleting broken message at: " + message.messageUrl + " permanentUrl: " + message.permanentUrl);
+ try {
+ message.delete();
+ } catch (IOException ioe) {
+ LOGGER.warn("Unable to delete broken message at: " + message.permanentUrl);
+ }
+ throw e;
+ }
+ }
+
+ return baos.toByteArray();
+ }
+
+ protected String getEscapedUrlFromPath(String escapedPath) throws URIException {
+ URI uri = new URI(httpClient.getHostConfiguration().getHostURL(), true);
+ uri.setEscapedPath(escapedPath);
+ return uri.getEscapedURI();
+ }
+
+ public String encodeAndFixUrl(String url) throws URIException {
+ String originalUrl = URIUtil.encodePath(url);
+ if (restoreHostName && originalUrl.startsWith("http")) {
+ String targetPath = new URI(originalUrl, true).getEscapedPath();
+ originalUrl = getEscapedUrlFromPath(targetPath);
+ }
+ return originalUrl;
+ }
+
+ protected InputStream getContentInputStream(String url) throws IOException {
+ String encodedUrl = encodeAndFixUrl(url);
+
+ final GetMethod method = new GetMethod(encodedUrl);
+ method.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
+ method.setRequestHeader("Translate", "f");
+ method.setRequestHeader("Accept-Encoding", "gzip");
+
+ InputStream inputStream;
+ try {
+ DavGatewayHttpClientFacade.executeGetMethod(httpClient, method, true);
+ if (DavGatewayHttpClientFacade.isGzipEncoded(method)) {
+ inputStream = new GZIPInputStream(method.getResponseBodyAsStream());
+ } else {
+ inputStream = method.getResponseBodyAsStream();
+ }
+ inputStream = new FilterInputStream(inputStream) {
+ int totalCount;
+ int lastLogCount;
+
+ @Override
+ public int read(byte[] buffer, int offset, int length) throws IOException {
+ int count = super.read(buffer, offset, length);
+ totalCount += count;
+ if (totalCount - lastLogCount > 1024 * 128) {
+ DavGatewayTray.debug(new BundleMessage("LOG_DOWNLOAD_PROGRESS", String.valueOf(totalCount / 1024), method.getURI()));
+ DavGatewayTray.switchIcon();
+ lastLogCount = totalCount;
+ }
+ return count;
+ }
+
+ @Override
+ public void close() throws IOException {
+ try {
+ super.close();
+ } finally {
+ method.releaseConnection();
+ }
+ }
+ };
+
+ } catch (HttpException e) {
+ method.releaseConnection();
+ LOGGER.warn("Unable to retrieve message at: " + url);
+ throw e;
+ }
+ return inputStream;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public void moveMessage(ExchangeSession.Message message, String targetFolder) throws IOException {
+ try {
+ moveMessage(message.permanentUrl, targetFolder);
+ } catch (HttpNotFoundException e) {
+ LOGGER.debug("404 not found at permanenturl: " + message.permanentUrl + ", retry with messageurl");
+ moveMessage(message.messageUrl, targetFolder);
+ }
+ }
+
+ protected void moveMessage(String sourceUrl, String targetFolder) throws IOException {
+ String targetPath = URIUtil.encodePath(getFolderPath(targetFolder)) + '/' + UUID.randomUUID().toString();
+ MoveMethod method = new MoveMethod(URIUtil.encodePath(sourceUrl), targetPath, false);
+ // allow rename if a message with the same name exists
+ method.addRequestHeader("Allow-Rename", "t");
+ try {
+ int statusCode = httpClient.executeMethod(method);
+ if (statusCode == HttpStatus.SC_PRECONDITION_FAILED) {
+ throw new DavMailException("EXCEPTION_UNABLE_TO_MOVE_MESSAGE");
+ } else if (statusCode != HttpStatus.SC_CREATED) {
+ throw DavGatewayHttpClientFacade.buildHttpException(method);
+ }
+ } finally {
+ method.releaseConnection();
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public void copyMessage(ExchangeSession.Message message, String targetFolder) throws IOException {
+ try {
+ copyMessage(message.permanentUrl, targetFolder);
+ } catch (HttpNotFoundException e) {
+ LOGGER.debug("404 not found at permanenturl: " + message.permanentUrl + ", retry with messageurl");
+ copyMessage(message.messageUrl, targetFolder);
+ }
+ }
+
+ protected void copyMessage(String sourceUrl, String targetFolder) throws IOException {
+ String targetPath = URIUtil.encodePath(getFolderPath(targetFolder)) + '/' + UUID.randomUUID().toString();
+ CopyMethod method = new CopyMethod(URIUtil.encodePath(sourceUrl), targetPath, false);
+ // allow rename if a message with the same name exists
+ method.addRequestHeader("Allow-Rename", "t");
+ try {
+ int statusCode = httpClient.executeMethod(method);
+ if (statusCode == HttpStatus.SC_PRECONDITION_FAILED) {
+ throw new DavMailException("EXCEPTION_UNABLE_TO_COPY_MESSAGE");
+ } else if (statusCode != HttpStatus.SC_CREATED) {
+ throw DavGatewayHttpClientFacade.buildHttpException(method);
+ }
+ } finally {
+ method.releaseConnection();
+ }
+ }
+
+ @Override
+ protected void moveToTrash(ExchangeSession.Message message) throws IOException {
+ String destination = URIUtil.encodePath(deleteditemsUrl) + '/' + UUID.randomUUID().toString();
+ LOGGER.debug("Deleting : " + message.permanentUrl + " to " + destination);
+ MoveMethod method = new MoveMethod(encodeAndFixUrl(message.permanentUrl), destination, false);
+ method.addRequestHeader("Allow-rename", "t");
+
+ int status = DavGatewayHttpClientFacade.executeHttpMethod(httpClient, method);
+ // do not throw error if already deleted
+ if (status != HttpStatus.SC_CREATED && status != HttpStatus.SC_NOT_FOUND) {
+ throw DavGatewayHttpClientFacade.buildHttpException(method);
+ }
+ if (method.getResponseHeader("Location") != null) {
+ destination = method.getResponseHeader("Location").getValue();
+ }
+
+ LOGGER.debug("Deleted to :" + destination);
+ }
+
+ protected String getItemProperty(String permanentUrl, String propertyName) throws IOException, DavException {
+ String result = null;
+ DavPropertyNameSet davPropertyNameSet = new DavPropertyNameSet();
+ davPropertyNameSet.add(Field.getPropertyName(propertyName));
+ PropFindMethod propFindMethod = new PropFindMethod(encodeAndFixUrl(permanentUrl), davPropertyNameSet, 0);
+ try {
+ try {
+ DavGatewayHttpClientFacade.executeHttpMethod(httpClient, propFindMethod);
+ } catch (UnknownHostException e) {
+ propFindMethod.releaseConnection();
+ // failover for misconfigured Exchange server, replace host name in url
+ restoreHostName = true;
+ propFindMethod = new PropFindMethod(encodeAndFixUrl(permanentUrl), davPropertyNameSet, 0);
+ DavGatewayHttpClientFacade.executeHttpMethod(httpClient, propFindMethod);
+ }
+
+ MultiStatus responses = propFindMethod.getResponseBodyAsMultiStatus();
+ if (responses.getResponses().length > 0) {
+ DavPropertySet properties = responses.getResponses()[0].getProperties(HttpStatus.SC_OK);
+ result = getPropertyIfExists(properties, propertyName);
+ }
+ } finally {
+ propFindMethod.releaseConnection();
+ }
+ return result;
+ }
+
+ protected String convertDateFromExchange(String exchangeDateValue) throws DavMailException {
+ String zuluDateValue = null;
+ if (exchangeDateValue != null) {
+ try {
+ zuluDateValue = getZuluDateFormat().format(getExchangeZuluDateFormatMillisecond().parse(exchangeDateValue));
+ } catch (ParseException e) {
+ throw new DavMailException("EXCEPTION_INVALID_DATE", exchangeDateValue);
+ }
+ }
+ return zuluDateValue;
+ }
+
+ protected static final Map<String, String> importanceToPriorityMap = new HashMap<String, String>();
+
+ static {
+ importanceToPriorityMap.put("high", "1");
+ importanceToPriorityMap.put("normal", "5");
+ importanceToPriorityMap.put("low", "9");
+ }
+
+ protected static final Map<String, String> priorityToImportanceMap = new HashMap<String, String>();
+
+ static {
+ priorityToImportanceMap.put("1", "high");
+ priorityToImportanceMap.put("5", "normal");
+ priorityToImportanceMap.put("9", "low");
+ }
+
+ protected String convertPriorityFromExchange(String exchangeImportanceValue) {
+ String value = null;
+ if (exchangeImportanceValue != null) {
+ value = importanceToPriorityMap.get(exchangeImportanceValue);
+ }
+ return value;
+ }
+
+ protected String convertPriorityToExchange(String vTodoPriorityValue) {
+ String value = null;
+ if (vTodoPriorityValue != null) {
+ value = priorityToImportanceMap.get(vTodoPriorityValue);
+ }
+ return value;
+ }
+
+
+ /**
+ * Format date to exchange search format.
+ *
+ * @param date date object
+ * @return formatted search date
+ */
+ @Override
+ public String formatSearchDate(Date date) {
+ SimpleDateFormat dateFormatter = new SimpleDateFormat(YYYY_MM_DD_HH_MM_SS, Locale.ENGLISH);
+ dateFormatter.setTimeZone(GMT_TIMEZONE);
+ return dateFormatter.format(date);
+ }
+
+ protected String convertTaskDateToZulu(String value) {
+ String result = null;
+ if (value != null && value.length() > 0) {
+ try {
+ SimpleDateFormat parser;
+ if (value.length() == 8) {
+ parser = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH);
+ parser.setTimeZone(GMT_TIMEZONE);
+ } else if (value.length() == 15) {
+ parser = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ENGLISH);
+ parser.setTimeZone(GMT_TIMEZONE);
+ } else if (value.length() == 16) {
+ parser = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.ENGLISH);
+ parser.setTimeZone(GMT_TIMEZONE);
+ } else {
+ parser = ExchangeSession.getExchangeZuluDateFormat();
+ }
+ Calendar calendarValue = Calendar.getInstance(GMT_TIMEZONE);
+ calendarValue.setTime(parser.parse(value));
+ // zulu time: add 12 hours
+ if (value.length() == 16) {
+ calendarValue.add(Calendar.HOUR, 12);
+ }
+ calendarValue.set(Calendar.HOUR, 0);
+ calendarValue.set(Calendar.MINUTE, 0);
+ calendarValue.set(Calendar.SECOND, 0);
+ result = ExchangeSession.getExchangeZuluDateFormatMillisecond().format(calendarValue.getTime());
+ } catch (ParseException e) {
+ LOGGER.warn("Invalid date: " + value);
+ }
+ }
+
+ return result;
+ }
+
+ protected String convertDateFromExchangeToTaskDate(String exchangeDateValue) throws DavMailException {
+ String result = null;
+ if (exchangeDateValue != null) {
+ try {
+ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH);
+ dateFormat.setTimeZone(GMT_TIMEZONE);
+ result = dateFormat.format(getExchangeZuluDateFormatMillisecond().parse(exchangeDateValue));
+ } catch (ParseException e) {
+ throw new DavMailException("EXCEPTION_INVALID_DATE", exchangeDateValue);
+ }
+ }
+ return result;
+ }
+
+ protected Date parseDateFromExchange(String exchangeDateValue) throws DavMailException {
+ Date result = null;
+ if (exchangeDateValue != null) {
+ try {
+ result = getExchangeZuluDateFormatMillisecond().parse(exchangeDateValue);
+ } catch (ParseException e) {
+ throw new DavMailException("EXCEPTION_INVALID_DATE", exchangeDateValue);
+ }
+ }
+ return result;
+ }
+}
diff --git a/src/java/davmail/exchange/dav/ExchangeDavMethod.java b/src/java/davmail/exchange/dav/ExchangeDavMethod.java
new file mode 100644
index 0000000..757a703
--- /dev/null
+++ b/src/java/davmail/exchange/dav/ExchangeDavMethod.java
@@ -0,0 +1,246 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2012 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.dav;
+
+import davmail.exchange.XMLStreamUtil;
+import org.apache.commons.httpclient.*;
+import org.apache.commons.httpclient.methods.PostMethod;
+import org.apache.commons.httpclient.methods.RequestEntity;
+import org.apache.jackrabbit.webdav.MultiStatusResponse;
+import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
+import org.apache.jackrabbit.webdav.xml.Namespace;
+import org.apache.log4j.Logger;
+
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * New stax based implementation to replace DOM based jackrabbit version an support Exchange only extensions.
+ */
+public abstract class ExchangeDavMethod extends PostMethod {
+ protected static final Logger LOGGER = Logger.getLogger(ExchangeDavMethod.class);
+ List<MultiStatusResponse> responses;
+
+ /**
+ * Create PROPPATCH method.
+ *
+ * @param path path
+ */
+ public ExchangeDavMethod(String path) {
+ super(path);
+ setRequestEntity(new RequestEntity() {
+ byte[] content;
+
+ public boolean isRepeatable() {
+ return true;
+ }
+
+ public void writeRequest(OutputStream outputStream) throws IOException {
+ if (content == null) {
+ content = generateRequestContent();
+ }
+ outputStream.write(content);
+ }
+
+ public long getContentLength() {
+ if (content == null) {
+ content = generateRequestContent();
+ }
+ return content.length;
+ }
+
+ public String getContentType() {
+ return "text/xml;charset=UTF-8";
+ }
+ });
+ }
+
+ /**
+ * Generate request content from property values.
+ *
+ * @return request content as byte array
+ */
+ protected abstract byte[] generateRequestContent();
+
+ @Override
+ protected void processResponseBody(HttpState httpState, HttpConnection httpConnection) {
+ Header contentTypeHeader = getResponseHeader("Content-Type");
+ if (contentTypeHeader != null && "text/xml".equals(contentTypeHeader.getValue())) {
+ responses = new ArrayList<MultiStatusResponse>();
+ XMLStreamReader reader;
+ try {
+ reader = XMLStreamUtil.createXMLStreamReader(new FilterInputStream(getResponseBodyAsStream()) {
+ final byte[] lastbytes = new byte[3];
+
+ @Override
+ public int read(byte[] bytes, int off, int len) throws IOException {
+ int count = in.read(bytes, off, len);
+ // patch invalid element name
+ for (int i = 0; i < count; i++) {
+ byte currentByte = bytes[off + i];
+ if ((lastbytes[0] == '<') && (currentByte >= '0' && currentByte <= '9')) {
+ // move invalid first tag char to valid range
+ bytes[off + i] = (byte) (currentByte + 49);
+ }
+ lastbytes[0] = lastbytes[1];
+ lastbytes[1] = lastbytes[2];
+ lastbytes[2] = currentByte;
+ }
+ return count;
+ }
+
+ });
+ while (reader.hasNext()) {
+ reader.next();
+ if (XMLStreamUtil.isStartTag(reader, "response")) {
+ handleResponse(reader);
+ }
+ }
+
+ } catch (IOException e) {
+ LOGGER.error("Error while parsing soap response: " + e, e);
+ } catch (XMLStreamException e) {
+ LOGGER.error("Error while parsing soap response: " + e, e);
+ }
+ }
+ }
+
+ protected void handleResponse(XMLStreamReader reader) throws XMLStreamException {
+ MultiStatusResponse multiStatusResponse = null;
+ String href = null;
+ String responseStatus = "";
+ while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "response")) {
+ reader.next();
+ if (XMLStreamUtil.isStartTag(reader)) {
+ String tagLocalName = reader.getLocalName();
+ if ("href".equals(tagLocalName)) {
+ href = reader.getElementText();
+ } else if ("status".equals(tagLocalName)) {
+ responseStatus = reader.getElementText();
+ } else if ("propstat".equals(tagLocalName)) {
+ if (multiStatusResponse == null) {
+ multiStatusResponse = new MultiStatusResponse(href, responseStatus);
+ }
+ handlePropstat(reader, multiStatusResponse);
+ }
+ }
+ }
+ if (multiStatusResponse != null) {
+ responses.add(multiStatusResponse);
+ }
+ }
+
+ protected void handlePropstat(XMLStreamReader reader, MultiStatusResponse multiStatusResponse) throws XMLStreamException {
+ int propstatStatus = 0;
+ while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "propstat")) {
+ reader.next();
+ if (XMLStreamUtil.isStartTag(reader)) {
+ String tagLocalName = reader.getLocalName();
+ if ("status".equals(tagLocalName)) {
+ if ("HTTP/1.1 200 OK".equals(reader.getElementText())) {
+ propstatStatus = HttpStatus.SC_OK;
+ } else {
+ propstatStatus = 0;
+ }
+ } else if ("prop".equals(tagLocalName) && propstatStatus == HttpStatus.SC_OK) {
+ handleProperty(reader, multiStatusResponse);
+ }
+ }
+ }
+
+ }
+
+ protected void handleProperty(XMLStreamReader reader, MultiStatusResponse multiStatusResponse) throws XMLStreamException {
+ while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "prop")) {
+ reader.next();
+ if (XMLStreamUtil.isStartTag(reader)) {
+ Namespace namespace = Namespace.getNamespace(reader.getNamespaceURI());
+ String tagLocalName = reader.getLocalName();
+ String tagContent = getTagContent(reader);
+ if (tagContent != null) {
+ multiStatusResponse.add(new DefaultDavProperty(tagLocalName, tagContent, namespace));
+ }
+ }
+ }
+ }
+
+ protected String getTagContent(XMLStreamReader reader) throws XMLStreamException {
+ String value = null;
+ String tagLocalName = reader.getLocalName();
+ while (reader.hasNext() &&
+ !((reader.getEventType() == XMLStreamConstants.END_ELEMENT) && tagLocalName.equals(reader.getLocalName()))) {
+ reader.next();
+ if (reader.getEventType() == XMLStreamConstants.CHARACTERS) {
+ value = reader.getText();
+ }
+ }
+ // empty tag
+ if (!reader.hasNext()) {
+ throw new XMLStreamException("End element for " + tagLocalName + " not found");
+ }
+ return value;
+ }
+
+ /**
+ * Get Multistatus responses.
+ *
+ * @return responses
+ * @throws HttpException on error
+ */
+ public MultiStatusResponse[] getResponses() throws HttpException {
+ if (responses == null) {
+ throw new HttpException(getStatusLine().toString());
+ }
+ return responses.toArray(new MultiStatusResponse[responses.size()]);
+ }
+
+ /**
+ * Get single Multistatus response.
+ *
+ * @return response
+ * @throws HttpException on error
+ */
+ public MultiStatusResponse getResponse() throws HttpException {
+ if (responses == null || responses.size() != 1) {
+ throw new HttpException(getStatusLine().toString());
+ }
+ return responses.get(0);
+ }
+
+ /**
+ * Return method http status code.
+ *
+ * @return http status code
+ * @throws HttpException on error
+ */
+ public int getResponseStatusCode() throws HttpException {
+ String responseDescription = getResponse().getResponseDescription();
+ if ("HTTP/1.1 201 Created".equals(responseDescription)) {
+ return HttpStatus.SC_CREATED;
+ } else {
+ return HttpStatus.SC_OK;
+ }
+ }
+}
diff --git a/src/java/davmail/exchange/dav/ExchangePropFindMethod.java b/src/java/davmail/exchange/dav/ExchangePropFindMethod.java
new file mode 100644
index 0000000..9526d1e
--- /dev/null
+++ b/src/java/davmail/exchange/dav/ExchangePropFindMethod.java
@@ -0,0 +1,114 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2011 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.dav;
+
+import org.apache.jackrabbit.webdav.header.DepthHeader;
+import org.apache.jackrabbit.webdav.property.DavPropertyName;
+import org.apache.jackrabbit.webdav.property.DavPropertyNameIterator;
+import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
+import org.apache.log4j.Logger;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Custom Exchange PROPFIND method.
+ * Does not load full DOM in memory.
+ */
+public class ExchangePropFindMethod extends ExchangeDavMethod {
+ protected static final Logger LOGGER = Logger.getLogger(ExchangePropFindMethod.class);
+
+ protected final DavPropertyNameSet propertyNameSet;
+
+ public ExchangePropFindMethod(String uri) {
+ this(uri, null, DepthHeader.DEPTH_INFINITY);
+ }
+
+ public ExchangePropFindMethod(String uri, DavPropertyNameSet propertyNameSet, int depth) {
+ super(uri);
+ this.propertyNameSet = propertyNameSet;
+ DepthHeader dh = new DepthHeader(depth);
+ setRequestHeader(dh.getHeaderName(), dh.getHeaderValue());
+ }
+
+ protected byte[] generateRequestContent() {
+ try {
+ // build namespace map
+ int currentChar = 'e';
+ final Map<String, Integer> nameSpaceMap = new HashMap<String, Integer>();
+ nameSpaceMap.put("DAV:", (int) 'D');
+ if (propertyNameSet != null) {
+ DavPropertyNameIterator propertyNameIterator = propertyNameSet.iterator();
+ while (propertyNameIterator.hasNext()) {
+ DavPropertyName davPropertyName = propertyNameIterator.nextPropertyName();
+
+ davPropertyName.getName();
+ // property namespace
+ String namespaceUri = davPropertyName.getNamespace().getURI();
+ if (!nameSpaceMap.containsKey(namespaceUri)) {
+ nameSpaceMap.put(namespaceUri, currentChar++);
+ }
+ }
+ }
+ // <D:propfind xmlns:D="DAV:"><D:prop><D:displayname/></D:prop></D:propfind>
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ OutputStreamWriter writer = new OutputStreamWriter(baos, "UTF-8");
+ writer.write("<D:propfind ");
+ for (Map.Entry<String, Integer> mapEntry : nameSpaceMap.entrySet()) {
+ writer.write(" xmlns:");
+ writer.write((char) mapEntry.getValue().intValue());
+ writer.write("=\"");
+ writer.write(mapEntry.getKey());
+ writer.write("\"");
+ }
+ writer.write(">");
+ if (propertyNameSet == null || propertyNameSet.isEmpty()) {
+ writer.write("<D:allprop/>");
+ } else {
+ writer.write("<D:prop>");
+ DavPropertyNameIterator propertyNameIterator = propertyNameSet.iterator();
+ while (propertyNameIterator.hasNext()) {
+ DavPropertyName davPropertyName = propertyNameIterator.nextPropertyName();
+ char nameSpaceChar = (char) nameSpaceMap.get(davPropertyName.getNamespace().getURI()).intValue();
+ writer.write('<');
+ writer.write(nameSpaceChar);
+ writer.write(':');
+ writer.write(davPropertyName.getName());
+ writer.write("/>");
+ }
+ writer.write("</D:prop>");
+ }
+ writer.write("</D:propfind>");
+ writer.close();
+ return baos.toByteArray();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ }
+
+ @Override
+ public String getName() {
+ return "PROPFIND";
+ }
+
+}
diff --git a/src/java/davmail/exchange/dav/ExchangePropPatchMethod.java b/src/java/davmail/exchange/dav/ExchangePropPatchMethod.java
new file mode 100644
index 0000000..6f749e8
--- /dev/null
+++ b/src/java/davmail/exchange/dav/ExchangePropPatchMethod.java
@@ -0,0 +1,137 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.dav;
+
+import org.apache.log4j.Logger;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Custom Exchange PROPPATCH method.
+ * Supports extended property update with type.
+ */
+public class ExchangePropPatchMethod extends ExchangeDavMethod {
+ protected static final Logger LOGGER = Logger.getLogger(ExchangePropPatchMethod.class);
+ static final String TYPE_NAMESPACE = "urn:schemas-microsoft-com:datatypes";
+ final Set<PropertyValue> propertyValues;
+
+ /**
+ * Create PROPPATCH method.
+ *
+ * @param path path
+ * @param propertyValues property values
+ */
+ public ExchangePropPatchMethod(String path, Set<PropertyValue> propertyValues) {
+ super(path);
+ this.propertyValues = propertyValues;
+ }
+
+ @Override
+ protected byte[] generateRequestContent() {
+ try {
+ // build namespace map
+ int currentChar = 'e';
+ final Map<String, Integer> nameSpaceMap = new HashMap<String, Integer>();
+ final Set<PropertyValue> setPropertyValues = new HashSet<PropertyValue>();
+ final Set<PropertyValue> deletePropertyValues = new HashSet<PropertyValue>();
+ for (PropertyValue propertyValue : propertyValues) {
+ // data type namespace
+ if (!nameSpaceMap.containsKey(TYPE_NAMESPACE) && propertyValue.getTypeString() != null) {
+ nameSpaceMap.put(TYPE_NAMESPACE, currentChar++);
+ }
+ // property namespace
+ String namespaceUri = propertyValue.getNamespaceUri();
+ if (!nameSpaceMap.containsKey(namespaceUri)) {
+ nameSpaceMap.put(namespaceUri, currentChar++);
+ }
+ if (propertyValue.getXmlEncodedValue() == null) {
+ deletePropertyValues.add(propertyValue);
+ } else {
+ setPropertyValues.add(propertyValue);
+ }
+ }
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ OutputStreamWriter writer = new OutputStreamWriter(baos, "UTF-8");
+ writer.write("<D:propertyupdate xmlns:D=\"DAV:\"");
+ for (Map.Entry<String, Integer> mapEntry : nameSpaceMap.entrySet()) {
+ writer.write(" xmlns:");
+ writer.write((char) mapEntry.getValue().intValue());
+ writer.write("=\"");
+ writer.write(mapEntry.getKey());
+ writer.write("\"");
+ }
+ writer.write(">");
+ if (!setPropertyValues.isEmpty()) {
+ writer.write("<D:set><D:prop>");
+ for (PropertyValue propertyValue : setPropertyValues) {
+ String typeString = propertyValue.getTypeString();
+ char nameSpaceChar = (char) nameSpaceMap.get(propertyValue.getNamespaceUri()).intValue();
+ writer.write('<');
+ writer.write(nameSpaceChar);
+ writer.write(':');
+ writer.write(propertyValue.getName());
+ if (typeString != null) {
+ writer.write(' ');
+ writer.write(nameSpaceMap.get(TYPE_NAMESPACE));
+ writer.write(":dt=\"");
+ writer.write(typeString);
+ writer.write("\"");
+ }
+ writer.write('>');
+ writer.write(propertyValue.getXmlEncodedValue());
+ writer.write("</");
+ writer.write(nameSpaceChar);
+ writer.write(':');
+ writer.write(propertyValue.getName());
+ writer.write('>');
+ }
+ writer.write("</D:prop></D:set>");
+ }
+ if (!deletePropertyValues.isEmpty()) {
+ writer.write("<D:remove><D:prop>");
+ for (PropertyValue propertyValue : deletePropertyValues) {
+ char nameSpaceChar = (char) nameSpaceMap.get(propertyValue.getNamespaceUri()).intValue();
+ writer.write('<');
+ writer.write(nameSpaceChar);
+ writer.write(':');
+ writer.write(propertyValue.getName());
+ writer.write("/>");
+ }
+ writer.write("</D:prop></D:remove>");
+ }
+ writer.write("</D:propertyupdate>");
+ writer.close();
+ return baos.toByteArray();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public String getName() {
+ return "PROPPATCH";
+ }
+
+}
diff --git a/src/java/davmail/exchange/dav/ExchangeSearchMethod.java b/src/java/davmail/exchange/dav/ExchangeSearchMethod.java
new file mode 100644
index 0000000..eeb2ee7
--- /dev/null
+++ b/src/java/davmail/exchange/dav/ExchangeSearchMethod.java
@@ -0,0 +1,72 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2012 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.dav;
+
+import davmail.util.StringUtil;
+import org.apache.log4j.Logger;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+
+/**
+ * Custom Exchange PROPFIND method.
+ * Does not load full DOM in memory.
+ */
+public class ExchangeSearchMethod extends ExchangeDavMethod {
+ protected static final Logger LOGGER = Logger.getLogger(ExchangeSearchMethod.class);
+
+ protected final String searchRequest;
+
+ /**
+ * Create search method.
+ *
+ * @param uri method uri
+ * @param searchRequest Exchange search request
+ */
+ public ExchangeSearchMethod(String uri, String searchRequest) {
+ super(uri);
+ this.searchRequest = searchRequest;
+ }
+
+ protected byte[] generateRequestContent() {
+ try {
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ OutputStreamWriter writer = new OutputStreamWriter(baos, "UTF-8");
+ writer.write("<?xml version=\"1.0\"?>\n");
+ writer.write("<d:searchrequest xmlns:d=\"DAV:\">\n");
+ writer.write(" <d:sql>");
+ writer.write(StringUtil.xmlEncode(searchRequest));
+ writer.write("</d:sql>\n");
+ writer.write("</d:searchrequest>");
+ writer.close();
+ return baos.toByteArray();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ }
+
+ @Override
+ public String getName() {
+ return "SEARCH";
+ }
+
+}
diff --git a/src/java/davmail/exchange/dav/Field.java b/src/java/davmail/exchange/dav/Field.java
new file mode 100644
index 0000000..fabfe59
--- /dev/null
+++ b/src/java/davmail/exchange/dav/Field.java
@@ -0,0 +1,635 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.dav;
+
+import davmail.util.StringUtil;
+import org.apache.jackrabbit.webdav.DavConstants;
+import org.apache.jackrabbit.webdav.property.DavPropertyName;
+import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
+import org.apache.jackrabbit.webdav.xml.DomUtil;
+import org.apache.jackrabbit.webdav.xml.Namespace;
+import org.apache.jackrabbit.webdav.xml.XmlSerializable;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * WebDav Field
+ */
+public class Field {
+
+ protected static final Map<DistinguishedPropertySetType, String> distinguishedPropertySetMap = new HashMap<DistinguishedPropertySetType, String>();
+
+ static {
+ distinguishedPropertySetMap.put(DistinguishedPropertySetType.Meeting, "6ed8da90-450b-101b-98da-00aa003f1305");
+ distinguishedPropertySetMap.put(DistinguishedPropertySetType.Appointment, "00062002-0000-0000-c000-000000000046");
+ distinguishedPropertySetMap.put(DistinguishedPropertySetType.Common, "00062008-0000-0000-c000-000000000046");
+ distinguishedPropertySetMap.put(DistinguishedPropertySetType.PublicStrings, "00020329-0000-0000-c000-000000000046");
+ distinguishedPropertySetMap.put(DistinguishedPropertySetType.Address, "00062004-0000-0000-c000-000000000046");
+ distinguishedPropertySetMap.put(DistinguishedPropertySetType.InternetHeaders, "00020386-0000-0000-c000-000000000046");
+ distinguishedPropertySetMap.put(DistinguishedPropertySetType.UnifiedMessaging, "4442858e-a9e3-4e80-b900-317a210cc15b");
+ distinguishedPropertySetMap.put(DistinguishedPropertySetType.Task, "00062003-0000-0000-c000-000000000046");
+ }
+
+ protected static final Namespace EMPTY = Namespace.getNamespace("");
+ protected static final Namespace XML = Namespace.getNamespace("xml:");
+ protected static final Namespace DAV = Namespace.getNamespace("DAV:");
+ protected static final Namespace URN_SCHEMAS_HTTPMAIL = Namespace.getNamespace("urn:schemas:httpmail:");
+ protected static final Namespace URN_SCHEMAS_MAILHEADER = Namespace.getNamespace("urn:schemas:mailheader:");
+
+ protected static final Namespace SCHEMAS_EXCHANGE = Namespace.getNamespace("http://schemas.microsoft.com/exchange/");
+ protected static final Namespace SCHEMAS_MAPI = Namespace.getNamespace("http://schemas.microsoft.com/mapi/");
+ protected static final Namespace SCHEMAS_MAPI_PROPTAG = Namespace.getNamespace("http://schemas.microsoft.com/mapi/proptag/");
+ protected static final Namespace SCHEMAS_MAPI_ID = Namespace.getNamespace("http://schemas.microsoft.com/mapi/id/");
+ protected static final Namespace SCHEMAS_MAPI_STRING = Namespace.getNamespace("http://schemas.microsoft.com/mapi/string/");
+ protected static final Namespace SCHEMAS_REPL = Namespace.getNamespace("http://schemas.microsoft.com/repl/");
+ protected static final Namespace URN_SCHEMAS_CONTACTS = Namespace.getNamespace("urn:schemas:contacts:");
+ protected static final Namespace URN_SCHEMAS_CALENDAR = Namespace.getNamespace("urn:schemas:calendar:");
+
+ protected static final Namespace SCHEMAS_MAPI_STRING_INTERNET_HEADERS =
+ Namespace.getNamespace(SCHEMAS_MAPI_STRING.getURI() +
+ '{' + distinguishedPropertySetMap.get(DistinguishedPropertySetType.InternetHeaders) + "}/");
+
+ protected static final Map<PropertyType, String> propertyTypeMap = new HashMap<PropertyType, String>();
+
+ static {
+ propertyTypeMap.put(PropertyType.Integer, "0003"); // PT_INT
+ propertyTypeMap.put(PropertyType.Boolean, "000b"); // PT_BOOLEAN
+ propertyTypeMap.put(PropertyType.SystemTime, "0040"); // PT_SYSTIME
+ propertyTypeMap.put(PropertyType.String, "001f"); // 001f is PT_UNICODE_STRING, 001E is PT_STRING
+ propertyTypeMap.put(PropertyType.Binary, "0102"); // PT_BINARY
+ propertyTypeMap.put(PropertyType.Double, "0005"); // PT_DOUBLE
+ }
+
+ @SuppressWarnings({"UnusedDeclaration"})
+ protected static enum DistinguishedPropertySetType {
+ Meeting, Appointment, Common, PublicStrings, Address, InternetHeaders, CalendarAssistant, UnifiedMessaging, Task
+ }
+
+
+ protected static final Map<String, Field> fieldMap = new HashMap<String, Field>();
+
+ static {
+ // well known folders
+ createField(URN_SCHEMAS_HTTPMAIL, "inbox");
+ createField(URN_SCHEMAS_HTTPMAIL, "deleteditems");
+ createField(URN_SCHEMAS_HTTPMAIL, "sentitems");
+ createField(URN_SCHEMAS_HTTPMAIL, "sendmsg");
+ createField(URN_SCHEMAS_HTTPMAIL, "drafts");
+ createField(URN_SCHEMAS_HTTPMAIL, "calendar");
+ createField(URN_SCHEMAS_HTTPMAIL, "tasks");
+ createField(URN_SCHEMAS_HTTPMAIL, "contacts");
+ createField(URN_SCHEMAS_HTTPMAIL, "outbox");
+
+
+ // folder
+ createField("folderclass", SCHEMAS_EXCHANGE, "outlookfolderclass");
+ createField(DAV, "hassubs");
+ createField(DAV, "nosubs");
+ createField(URN_SCHEMAS_HTTPMAIL, "unreadcount");
+ createField(SCHEMAS_REPL, "contenttag");
+
+ createField("uidNext", 0x6751, PropertyType.Integer);// PR_ARTICLE_NUM_NEXT
+ createField("highestUid", 0x6752, PropertyType.Integer);// PR_IMAP_LAST_ARTICLE_ID
+
+ createField(DAV, "isfolder");
+
+ // item uid, do not use as search parameter, see http://support.microsoft.com/kb/320749
+ createField(DAV, "uid"); // based on PR_RECORD_KEY
+
+ // POP and IMAP message
+ createField("messageSize", 0x0e08, PropertyType.Integer);//PR_MESSAGE_SIZE
+ createField("imapUid", 0x0e23, PropertyType.Integer);//PR_INTERNET_ARTICLE_NUMBER
+ createField("junk", 0x1083, PropertyType.Integer);
+ createField("flagStatus", 0x1090, PropertyType.Integer);//PR_FLAG_STATUS
+ createField("messageFlags", 0x0e07, PropertyType.Integer);//PR_MESSAGE_FLAGS
+ createField("lastVerbExecuted", 0x1081, PropertyType.Integer);//PR_LAST_VERB_EXECUTED
+ createField("iconIndex", 0x1080, PropertyType.Integer);//PR_ICON_INDEX
+ createField(URN_SCHEMAS_HTTPMAIL, "read");
+ //createField("read", 0x0e69, PropertyType.Boolean);//PR_READ
+ createField("deleted", DistinguishedPropertySetType.Common, 0x8570, "deleted", PropertyType.String);
+
+ //createField(URN_SCHEMAS_HTTPMAIL, "date");//PR_CLIENT_SUBMIT_TIME, 0x0039
+ createField("date", 0x0e06, PropertyType.SystemTime);//PR_MESSAGE_DELIVERY_TIME
+ createField(URN_SCHEMAS_MAILHEADER, "bcc");//PS_INTERNET_HEADERS/bcc
+ createField(URN_SCHEMAS_HTTPMAIL, "datereceived");//PR_MESSAGE_DELIVERY_TIME, 0x0E06
+
+ // unused: force message encoding
+ createField("messageFormat", 0x5909, PropertyType.Integer);//PR_MSG_EDITOR_FORMAT EDITOR_FORMAT_PLAINTEXT = 1 EDITOR_FORMAT_HTML = 2
+ createField("mailOverrideFormat", 0x5902, PropertyType.Integer);//PR_INETMAIL_OVERRIDE_FORMAT ENCODING_PREFERENCE = 2 BODY_ENCODING_TEXT_AND_HTML = 1 ENCODING_MIME = 4
+
+ // IMAP search
+
+ createField(URN_SCHEMAS_HTTPMAIL, "subject"); // DistinguishedPropertySetType.InternetHeaders/Subject/String
+ //createField("subject", 0x0037, PropertyType.String);//PR_SUBJECT
+ createField("body", 0x1000, PropertyType.String);//PR_BODY
+ createField("messageheaders", 0x007D, PropertyType.String);// PR_TRANSPORT_MESSAGE_HEADERS
+ createField(URN_SCHEMAS_HTTPMAIL, "from");
+ //createField("from", DistinguishedPropertySetType.PublicStrings, 0x001f);//urn:schemas:httpmail:from
+ createField(URN_SCHEMAS_MAILHEADER, "to"); // DistinguishedPropertySetType.InternetHeaders/To/String
+ createField(URN_SCHEMAS_MAILHEADER, "cc"); // DistinguishedPropertySetType.InternetHeaders/To/String
+ createField(URN_SCHEMAS_MAILHEADER, "message-id"); // DistinguishedPropertySetType.InternetHeaders/message-id/String
+ createField(URN_SCHEMAS_MAILHEADER, "htmldescription"); // DistinguishedPropertySetType.InternetHeaders/htmldescription/String
+
+ createField("lastmodified", DAV, "getlastmodified"); // PR_LAST_MODIFICATION_TIME 0x3008 SystemTime
+
+ // failover search
+ createField(DAV, "displayname");
+ createField("urlcompname", 0x10f3, PropertyType.String); //PR_URL_COMP_NAME
+
+ // items
+ createField("etag", DAV, "getetag");
+
+ // calendar
+ createField(SCHEMAS_EXCHANGE, "permanenturl");
+ createField(URN_SCHEMAS_CALENDAR, "instancetype"); // DistinguishedPropertySetType.PublicStrings/urn:schemas:calendar:instancetype/Integer
+ createField(URN_SCHEMAS_CALENDAR, "dtstart"); // 0x10C3 SystemTime
+ createField(URN_SCHEMAS_CALENDAR, "dtend"); // 0x10C4 SystemTime
+
+ //createField(URN_SCHEMAS_CALENDAR, "prodid"); // // DistinguishedPropertySetType.PublicStrings/urn:schemas:calendar:prodid/String
+ createField("calendarversion", URN_SCHEMAS_CALENDAR, "version"); // DistinguishedPropertySetType.PublicStrings/urn:schemas:calendar:version/String
+ createField(URN_SCHEMAS_CALENDAR, "method"); // // DistinguishedPropertySetType.PublicStrings/urn:schemas:calendar:method/String
+
+ createField("calendarlastmodified", URN_SCHEMAS_CALENDAR, "lastmodified"); // DistinguishedPropertySetType.PublicStrings/urn:schemas:calendar:isorganizer/Boolean
+ createField(URN_SCHEMAS_CALENDAR, "dtstamp"); // PidLidOwnerCriticalChange
+ createField("calendaruid", URN_SCHEMAS_CALENDAR, "uid"); // DistinguishedPropertySetType.PublicStrings/urn:schemas:calendar:uid/String
+ createField(URN_SCHEMAS_CALENDAR, "transparent"); // DistinguishedPropertySetType.PublicStrings/urn:schemas:calendar:transparent/String
+
+ createField(URN_SCHEMAS_CALENDAR, "organizer");
+ createField(URN_SCHEMAS_CALENDAR, "created"); // DistinguishedPropertySetType.PublicStrings/urn:schemas:calendar:created/SystemTime
+ createField(URN_SCHEMAS_CALENDAR, "alldayevent"); // DistinguishedPropertySetType.Appointment/0x8215 Boolean
+
+ createField(URN_SCHEMAS_CALENDAR, "rrule"); // DistinguishedPropertySetType.PublicStrings/urn:schemas:calendar:rrule/PtypMultipleString
+ createField(URN_SCHEMAS_CALENDAR, "exdate"); // DistinguishedPropertySetType.PublicStrings/urn:schemas:calendar:exdate/PtypMultipleTime
+
+ createField(SCHEMAS_MAPI, "reminderset"); // PidLidReminderSet
+ createField(SCHEMAS_MAPI, "reminderdelta"); // PidLidReminderDelta
+
+ // TODO
+ createField(SCHEMAS_MAPI, "allattendeesstring"); // PidLidAllAttendeesString
+ createField(SCHEMAS_MAPI, "required_attendees"); // PidLidRequiredAttendees
+ createField(SCHEMAS_MAPI, "apptendtime"); // PidLidAppointmentEndTime
+ createField(SCHEMAS_MAPI, "apptstateflags"); // PidLidAppointmentStateFlags 1: Meeting, 2: Received, 4: Cancelled
+
+ createField(URN_SCHEMAS_CALENDAR, "isorganizer"); // DistinguishedPropertySetType.PublicStrings/urn:schemas:calendar:isorganizer/Boolean
+ createField(URN_SCHEMAS_CALENDAR, "location"); // DistinguishedPropertySetType.Appointment/0x8208 String
+ createField(URN_SCHEMAS_CALENDAR, "attendeerole"); // DistinguishedPropertySetType.PublicStrings/urn:schemas:calendar:attendeerole/Integer
+ createField(URN_SCHEMAS_CALENDAR, "busystatus"); // DistinguishedPropertySetType.PublicStrings/urn:schemas:calendar:busystatus/String
+ createField(URN_SCHEMAS_CALENDAR, "exrule"); // DistinguishedPropertySetType.PublicStrings/urn:schemas:calendar:exrule/PtypMultipleString
+ createField(URN_SCHEMAS_CALENDAR, "recurrenceidrange"); // DistinguishedPropertySetType.PublicStrings/urn:schemas:calendar:recurrenceidrange/String
+ createField(URN_SCHEMAS_CALENDAR, "rdate"); // DistinguishedPropertySetType.PublicStrings/urn:schemas:calendar:rdate/PtypMultipleTime
+ createField(URN_SCHEMAS_CALENDAR, "reminderoffset"); // DistinguishedPropertySetType.PublicStrings/urn:schemas:calendar:reminderoffset/Integer
+ createField(URN_SCHEMAS_CALENDAR, "timezone"); // DistinguishedPropertySetType.PublicStrings/urn:schemas:calendar:timezone/String
+
+
+ createField(SCHEMAS_EXCHANGE, "sensitivity"); // PR_SENSITIVITY 0x0036 Integer
+ createField(URN_SCHEMAS_CALENDAR, "timezoneid"); // DistinguishedPropertySetType.PublicStrings/urn:schemas:calendar:timezoneid/Integer
+ // should use PidLidServerProcessed ?
+ createField("processed", 0x65e8, PropertyType.Boolean);// PR_MESSAGE_PROCESSED
+
+ createField(DAV, "contentclass");
+ createField("internetContent", 0x6659, PropertyType.Binary);
+
+ // contact
+
+ createField(SCHEMAS_EXCHANGE, "outlookmessageclass");
+ createField(URN_SCHEMAS_HTTPMAIL, "subject");
+
+ createField(URN_SCHEMAS_CONTACTS, "middlename"); // PR_MIDDLE_NAME 0x3A44
+ createField(URN_SCHEMAS_CONTACTS, "fileas"); // urn:schemas:contacts:fileas PS_PUBLIC_STRINGS
+
+ //createField("id", 0x0ff6, PropertyType.Binary); // PR_INSTANCE_KEY http://support.microsoft.com/kb/320749
+
+ createField(URN_SCHEMAS_CONTACTS, "homepostaladdress"); // homeAddress DistinguishedPropertySetType.Address/0x0000801A/String
+ createField(URN_SCHEMAS_CONTACTS, "otherpostaladdress"); // otherAddress DistinguishedPropertySetType.Address/0x0000801C/String
+ createField(URN_SCHEMAS_CONTACTS, "mailingaddressid"); // postalAddressId DistinguishedPropertySetType.Address/0x00008022/String
+ createField(URN_SCHEMAS_CONTACTS, "workaddress"); // workAddress DistinguishedPropertySetType.Address/0x0000801B/String
+
+ createField(URN_SCHEMAS_CONTACTS, "alternaterecipient"); // alternaterecipient DistinguishedPropertySetType.PublicStrings/urn:schemas:contacts:alternaterecipient/String
+
+ createField(SCHEMAS_EXCHANGE, "extensionattribute1"); // DistinguishedPropertySetType.Address/0x0000804F/String
+ createField(SCHEMAS_EXCHANGE, "extensionattribute2"); // DistinguishedPropertySetType.Address/0x00008050/String
+ createField(SCHEMAS_EXCHANGE, "extensionattribute3"); // DistinguishedPropertySetType.Address/0x00008051/String
+ createField(SCHEMAS_EXCHANGE, "extensionattribute4"); // DistinguishedPropertySetType.Address/0x00008052/String
+
+ createField(URN_SCHEMAS_CONTACTS, "bday"); // PR_BIRTHDAY 0x3A42 SystemTime
+ createField("anniversary", URN_SCHEMAS_CONTACTS, "weddinganniversary"); // WeddingAnniversary
+ createField(URN_SCHEMAS_CONTACTS, "businesshomepage"); // PR_BUSINESS_HOME_PAGE 0x3A51 String
+ createField(URN_SCHEMAS_CONTACTS, "personalHomePage"); // PR_PERSONAL_HOME_PAGE 0x3A50 String
+ createField(URN_SCHEMAS_CONTACTS, "cn"); // PR_DISPLAY_NAME 0x3001 String
+ createField(URN_SCHEMAS_CONTACTS, "co"); // workAddressCountry DistinguishedPropertySetType.Address/0x00008049/String
+ createField(URN_SCHEMAS_CONTACTS, "department"); // PR_DEPARTMENT_NAME 0x3A18 String
+
+ // smtp email
+ createField("smtpemail1", DistinguishedPropertySetType.Address, 0x8084, "smtpemail1"); // Email1OriginalDisplayName
+ createField("smtpemail2", DistinguishedPropertySetType.Address, 0x8094, "smtpemail2"); // Email2OriginalDisplayName
+ createField("smtpemail3", DistinguishedPropertySetType.Address, 0x80A4, "smtpemail3"); // Email3OriginalDisplayName
+
+ // native email
+ createField("email1", DistinguishedPropertySetType.Address, 0x8083, "email1"); // Email1EmailAddress
+ createField("email2", DistinguishedPropertySetType.Address, 0x8093, "email2"); // Email2EmailAddress
+ createField("email3", DistinguishedPropertySetType.Address, 0x80A3, "email3"); // Email3EmailAddress
+
+ // email type
+ createField("email1type", DistinguishedPropertySetType.Address, 0x8082, "email1type"); // Email1AddressType
+ createField("email2type", DistinguishedPropertySetType.Address, 0x8092, "email2type"); // Email2AddressType
+ createField("email3type", DistinguishedPropertySetType.Address, 0x80A2, "email3type"); // Email3AddressType
+
+ createField(URN_SCHEMAS_CONTACTS, "facsimiletelephonenumber"); // PR_BUSINESS_FAX_NUMBER 0x3A24 String
+ createField(URN_SCHEMAS_CONTACTS, "givenName"); // PR_GIVEN_NAME 0x3A06 String
+ createField(URN_SCHEMAS_CONTACTS, "homepostofficebox"); // PR_HOME_ADDRESS_POST_OFFICE_BOX 0x3A5E String
+ createField(URN_SCHEMAS_CONTACTS, "homeCity"); // PR_HOME_ADDRESS_CITY 0x3A59 String
+ createField(URN_SCHEMAS_CONTACTS, "homeCountry"); // PR_HOME_ADDRESS_COUNTRY 0x3A5A String
+ createField(URN_SCHEMAS_CONTACTS, "homePhone"); // PR_HOME_TELEPHONE_NUMBER 0x3A09 String
+ createField(URN_SCHEMAS_CONTACTS, "homePostalCode"); // PR_HOME_ADDRESS_POSTAL_CODE 0x3A5B String
+ createField(URN_SCHEMAS_CONTACTS, "homeState"); // PR_HOME_ADDRESS_STATE_OR_PROVINCE 0x3A5C String
+ createField(URN_SCHEMAS_CONTACTS, "homeStreet"); // PR_HOME_ADDRESS_STREET 0x3A5D String
+ createField(URN_SCHEMAS_CONTACTS, "l"); // workAddressCity DistinguishedPropertySetType.Address/0x00008046/String
+ createField(URN_SCHEMAS_CONTACTS, "manager"); // PR_MANAGER_NAME 0x3A4E String
+ createField(URN_SCHEMAS_CONTACTS, "mobile"); // PR_MOBILE_TELEPHONE_NUMBER 0x3A1C String
+ createField(URN_SCHEMAS_CONTACTS, "namesuffix"); // PR_GENERATION 0x3A05 String
+ createField(URN_SCHEMAS_CONTACTS, "nickname"); // PR_NICKNAME 0x3A4F String
+ createField(URN_SCHEMAS_CONTACTS, "o"); // PR_COMPANY_NAME 0x3A16 String
+ createField(URN_SCHEMAS_CONTACTS, "pager"); // PR_PAGER_TELEPHONE_NUMBER 0x3A21 String
+ createField(URN_SCHEMAS_CONTACTS, "personaltitle"); // PR_DISPLAY_NAME_PREFIX 0x3A45 String
+ createField(URN_SCHEMAS_CONTACTS, "postalcode"); // workAddressPostalCode DistinguishedPropertySetType.Address/0x00008048/String
+ createField(URN_SCHEMAS_CONTACTS, "postofficebox"); // workAddressPostOfficeBox DistinguishedPropertySetType.Address/0x0000804A/String
+ createField(URN_SCHEMAS_CONTACTS, "profession"); // PR_PROFESSION 0x3A46 String
+ createField(URN_SCHEMAS_CONTACTS, "roomnumber"); // PR_OFFICE_LOCATION 0x3A19 String
+ createField(URN_SCHEMAS_CONTACTS, "secretarycn"); // PR_ASSISTANT 0x3A30 String
+ createField(URN_SCHEMAS_CONTACTS, "sn"); // PR_SURNAME 0x3A11 String
+ createField(URN_SCHEMAS_CONTACTS, "spousecn"); // PR_SPOUSE_NAME 0x3A48 String
+ createField(URN_SCHEMAS_CONTACTS, "st"); // workAddressState DistinguishedPropertySetType.Address/0x00008047/String
+ createField(URN_SCHEMAS_CONTACTS, "street"); // workAddressStreet DistinguishedPropertySetType.Address/0x00008045/String
+ createField(URN_SCHEMAS_CONTACTS, "telephoneNumber"); // PR_BUSINESS_TELEPHONE_NUMBER 0x3A08 String
+ createField(URN_SCHEMAS_CONTACTS, "title"); // PR_TITLE 0x3A17 String
+ createField("description", URN_SCHEMAS_HTTPMAIL, "textdescription"); // PR_BODY 0x1000 String
+ createField("im", SCHEMAS_MAPI, "InstMsg"); // InstantMessagingAddress DistinguishedPropertySetType.Address/0x00008062/String
+ createField(URN_SCHEMAS_CONTACTS, "othermobile"); // PR_CAR_TELEPHONE_NUMBER 0x3A1E String
+ createField(URN_SCHEMAS_CONTACTS, "internationalisdnnumber"); // PR_ISDN_NUMBER 0x3A2D String
+
+ createField(URN_SCHEMAS_CONTACTS, "otherTelephone"); // PR_OTHER_TELEPHONE_NUMBER 0x3A21 String
+ createField(URN_SCHEMAS_CONTACTS, "homefax"); // PR_HOME_FAX_NUMBER 0x3A25 String
+
+ createField(URN_SCHEMAS_CONTACTS, "otherstreet"); // PR_OTHER_ADDRESS_STREET 0x3A63 String
+ createField(URN_SCHEMAS_CONTACTS, "otherstate"); // PR_OTHER_ADDRESS_STATE_OR_PROVINCE 0x3A62 String
+ createField(URN_SCHEMAS_CONTACTS, "otherpostofficebox"); // PR_OTHER_ADDRESS_POST_OFFICE_BOX 0x3A64 String
+ createField(URN_SCHEMAS_CONTACTS, "otherpostalcode"); // PR_OTHER_ADDRESS_POSTAL_CODE 0x3A61 String
+ createField(URN_SCHEMAS_CONTACTS, "othercountry"); // PR_OTHER_ADDRESS_COUNTRY 0x3A60 String
+ createField(URN_SCHEMAS_CONTACTS, "othercity"); // PR_OTHER_ADDRESS_CITY 0x3A5F String
+
+ createField(URN_SCHEMAS_CONTACTS, "gender"); // PR_GENDER 0x3A4D Integer16
+
+ createField("keywords", SCHEMAS_EXCHANGE, "keywords-utf8", PropertyType.StringArray); // PS_PUBLIC_STRINGS Keywords String
+ //createField("keywords", DistinguishedPropertySetType.PublicStrings, "Keywords", ); // PS_PUBLIC_STRINGS Keywords String
+
+ // contact private flags
+ createField("private", DistinguishedPropertySetType.Common, 0x8506, "private", PropertyType.Boolean); // True/False
+ createField("sensitivity", 0x0036, PropertyType.Integer); // PR_SENSITIVITY SENSITIVITY_PRIVATE = 2, SENSITIVITY_PERSONAL = 1, SENSITIVITY_NONE = 0
+
+ createField("haspicture", DistinguishedPropertySetType.Address, 0x8015, "haspicture", PropertyType.Boolean); // True/False
+
+ createField(URN_SCHEMAS_CALENDAR, "fburl"); // freeBusyLocation
+
+ // OWA settings
+ createField("messageclass", 0x001a, PropertyType.String);
+ createField("roamingxmlstream", 0x7c08, PropertyType.Binary);
+ createField("roamingdictionary", 0x7c07, PropertyType.Binary);
+
+ createField(DAV, "ishidden");
+
+ // attachment content
+ createField("attachDataBinary", 0x3701, PropertyType.Binary);
+
+ createField("attachmentContactPhoto", 0x7FFF, PropertyType.Boolean); // PR_ATTACHMENT_CONTACTPHOTO
+ createField("renderingPosition", 0x370B, PropertyType.Integer);// PR_RENDERING_POSITION
+ //createField("attachFilename", 0x3704, PropertyType.String); // PR_ATTACH_FILENAME
+ createField("attachExtension", 0x3703, PropertyType.String); // PR_ATTACH_EXTENSION
+
+ createField("xmozlastack", DistinguishedPropertySetType.PublicStrings);
+ createField("xmozsnoozetime", DistinguishedPropertySetType.PublicStrings);
+ createField("xmozsendinvitations", DistinguishedPropertySetType.PublicStrings);
+
+ // task
+ createField(URN_SCHEMAS_MAILHEADER, "importance");//PS_INTERNET_HEADERS/importance
+ createField("percentcomplete", DistinguishedPropertySetType.Task, 0x8102, "percentcomplete", PropertyType.Double);
+ createField("taskstatus", DistinguishedPropertySetType.Task, 0x8101, "taskstatus", PropertyType.Integer);
+ createField("startdate", DistinguishedPropertySetType.Task, 0x8104, "startdate", PropertyType.SystemTime);
+ createField("duedate", DistinguishedPropertySetType.Task, 0x8105, "duedate", PropertyType.SystemTime);
+ createField("datecompleted", DistinguishedPropertySetType.Task, 0x810F, "datecompleted", PropertyType.SystemTime);
+ createField("iscomplete", DistinguishedPropertySetType.Task, 0x811C, "iscomplete", PropertyType.Boolean);
+
+
+ createField("commonstart", DistinguishedPropertySetType.Common, 0x8516, "commonstart", PropertyType.SystemTime);
+ createField("commonend", DistinguishedPropertySetType.Common, 0x8517, "commonend", PropertyType.SystemTime);
+ }
+
+ protected static String toHexString(int propertyTag) {
+ StringBuilder hexValue = new StringBuilder(Integer.toHexString(propertyTag));
+ while (hexValue.length() < 4) {
+ hexValue.insert(0, '0');
+ }
+ return hexValue.toString();
+ }
+
+ protected static void createField(String alias, int propertyTag, PropertyType propertyType) {
+ String name = 'x' + toHexString(propertyTag) + propertyTypeMap.get(propertyType);
+ Field field;
+ if (propertyType == PropertyType.Binary) {
+ field = new Field(alias, SCHEMAS_MAPI_PROPTAG, name, propertyType, null, "bin.base64", name);
+ } else {
+ field = new Field(alias, SCHEMAS_MAPI_PROPTAG, name, propertyType);
+ }
+ fieldMap.put(field.alias, field);
+ }
+
+ protected static void createField(String alias, DistinguishedPropertySetType propertySetType) {
+ Field field = new Field(Namespace.getNamespace(SCHEMAS_MAPI_STRING.getURI() +
+ '{' + distinguishedPropertySetMap.get(propertySetType) + "}/"), alias);
+ fieldMap.put(field.alias, field);
+ }
+
+ protected static void createField(String alias, DistinguishedPropertySetType propertySetType, int propertyTag, String responseAlias) {
+ createField(alias, propertySetType, propertyTag, responseAlias, null);
+ }
+
+ protected static void createField(String alias, DistinguishedPropertySetType propertySetType, int propertyTag, String responseAlias, PropertyType propertyType) {
+ String name;
+ String updateAlias;
+ if (propertySetType == DistinguishedPropertySetType.Address) {
+ // Address namespace expects integer names
+ name = String.valueOf(propertyTag);
+ updateAlias = "_x0030_x" + toHexString(propertyTag);
+ } else if (propertySetType == DistinguishedPropertySetType.Task) {
+ name = "0x" + toHexString(propertyTag);
+ updateAlias = "0x0000" + toHexString(propertyTag);
+ } else {
+ // Common namespace expects hex names
+ name = "0x" + toHexString(propertyTag);
+ updateAlias = "_x0030_x" + toHexString(propertyTag);
+ }
+ Field field = new Field(alias, Namespace.getNamespace(SCHEMAS_MAPI_ID.getURI() +
+ '{' + distinguishedPropertySetMap.get(propertySetType) + "}/"), name, propertyType, responseAlias, null, updateAlias);
+ fieldMap.put(field.alias, field);
+ }
+
+ protected static void createField(Namespace namespace, String name) {
+ Field field = new Field(namespace, name);
+ fieldMap.put(field.alias, field);
+ }
+
+ protected static void createField(String alias, Namespace namespace, String name) {
+ Field field = new Field(alias, namespace, name, null);
+ fieldMap.put(field.alias, field);
+ }
+
+ protected static void createField(String alias, Namespace namespace, String name, PropertyType propertyType) {
+ Field field = new Field(alias, namespace, name, propertyType);
+ fieldMap.put(field.alias, field);
+ }
+
+ private final DavPropertyName davPropertyName;
+ protected final String alias;
+ protected final String uri;
+ protected final String requestPropertyString;
+ protected final DavPropertyName responsePropertyName;
+ protected final DavPropertyName updatePropertyName;
+ protected final String cast;
+ protected final boolean isIntValue;
+ protected final boolean isMultivalued;
+ protected final boolean isBooleanValue;
+ protected final boolean isFloatValue;
+ protected final boolean isDateValue;
+
+ /**
+ * Create field for namespace and name, use name as alias.
+ *
+ * @param namespace Exchange namespace
+ * @param name Exchange name
+ */
+ protected Field(Namespace namespace, String name) {
+ this(name, namespace, name, null);
+ }
+
+ /**
+ * Create field for namespace and name of type propertyType.
+ *
+ * @param alias logical name in DavMail
+ * @param namespace Exchange namespace
+ * @param name Exchange name
+ * @param propertyType property type
+ */
+ protected Field(String alias, Namespace namespace, String name, PropertyType propertyType) {
+ this(alias, namespace, name, propertyType, null, null, name);
+ }
+
+ /**
+ * Create field for namespace and name of type propertyType.
+ *
+ * @param alias logical name in DavMail
+ * @param namespace Exchange namespace
+ * @param name Exchange name
+ * @param propertyType property type
+ * @param responseAlias property name in SEARCH response (as responsealias in request)
+ * @param cast response cast type (e.g. bin.base64)
+ * @param updateAlias some properties use a different alias in PROPPATCH requests
+ */
+ protected Field(String alias, Namespace namespace, String name, PropertyType propertyType, String responseAlias, String cast, String updateAlias) {
+ this.alias = alias;
+
+ // property name in PROPFIND requests
+ davPropertyName = DavPropertyName.create(name, namespace);
+ // property name in PROPPATCH requests
+ updatePropertyName = DavPropertyName.create(updateAlias, namespace);
+
+ // a few type based flags
+ isMultivalued = propertyType != null && propertyType.toString().endsWith("Array");
+ isIntValue = propertyType == PropertyType.Long || propertyType == PropertyType.Integer || propertyType == PropertyType.Short;
+ isBooleanValue = propertyType == PropertyType.Boolean;
+ isFloatValue = propertyType == PropertyType.Float || propertyType == PropertyType.Double;
+ isDateValue = propertyType == PropertyType.SystemTime;
+
+ this.uri = namespace.getURI() + name;
+ if (responseAlias == null) {
+ this.requestPropertyString = '"' + uri + '"';
+ this.responsePropertyName = davPropertyName;
+ } else {
+ this.requestPropertyString = '"' + uri + "\" as " + responseAlias;
+ this.responsePropertyName = DavPropertyName.create(responseAlias, EMPTY);
+ }
+ this.cast = cast;
+ }
+
+ /**
+ * Property uri.
+ *
+ * @return uri
+ */
+ public String getUri() {
+ return uri;
+ }
+
+ /**
+ * Integer value property type.
+ *
+ * @return true if the field value is integer
+ */
+ public boolean isIntValue() {
+ return isIntValue;
+ }
+
+ /**
+ * Get Field by alias.
+ *
+ * @param alias field alias
+ * @return field
+ */
+ public static Field get(String alias) {
+ Field field = fieldMap.get(alias);
+ if (field == null) {
+ throw new IllegalArgumentException("Unknown field: " + alias);
+ }
+ return field;
+ }
+
+ /**
+ * Get Mime header field.
+ *
+ * @param headerName header name
+ * @return field object
+ */
+ public static Field getHeader(String headerName) {
+ return new Field(SCHEMAS_MAPI_STRING_INTERNET_HEADERS, headerName);
+ }
+
+ /**
+ * Create DavProperty object for field alias and value.
+ *
+ * @param alias DavMail field alias
+ * @param value field value
+ * @return DavProperty with value or DavPropertyName for null values
+ */
+ public static DavConstants createDavProperty(String alias, String value) {
+ Field field = Field.get(alias);
+ if (value == null) {
+ // return DavPropertyName to remove property
+ return field.updatePropertyName;
+ } else if (field.isMultivalued) {
+ // multivalued field, split values separated by \n
+ List<XmlSerializable> valueList = new ArrayList<XmlSerializable>();
+ String[] values = value.split("\n");
+ for (final String singleValue : values) {
+ valueList.add(new XmlSerializable() {
+ public Element toXml(Document document) {
+ return DomUtil.createElement(document, "v", XML, singleValue);
+ }
+ });
+ }
+
+ return new DefaultDavProperty(field.updatePropertyName, valueList);
+ } else if (field.isBooleanValue && !"haspicture".equals(alias)) {
+ if ("true".equals(value)) {
+ return new DefaultDavProperty(field.updatePropertyName, "1");
+ } else if ("false".equals(value)) {
+ return new DefaultDavProperty(field.updatePropertyName, "0");
+ } else {
+ throw new RuntimeException("Invalid value for " + field.alias + ": " + value);
+ }
+ } else {
+ return new DefaultDavProperty(field.updatePropertyName, value);
+ }
+ }
+
+ /**
+ * Create property value object for field and value.
+ *
+ * @param alias field alias
+ * @param value field value
+ * @return property value object
+ * @see ExchangePropPatchMethod
+ */
+ public static PropertyValue createPropertyValue(String alias, String value) {
+ Field field = Field.get(alias);
+ DavPropertyName davPropertyName = field.davPropertyName;
+ if (value == null) {
+ // return DavPropertyName to remove property
+ return new PropertyValue(davPropertyName.getNamespace().getURI(), davPropertyName.getName());
+ } else if (field.isMultivalued) {
+ StringBuilder buffer = new StringBuilder();
+ // multivalued field, split values separated by \n
+ String[] values = value.split("\n");
+ for (final String singleValue : values) {
+ buffer.append("<v>");
+ buffer.append(StringUtil.xmlEncode(singleValue));
+ buffer.append("</v>");
+ }
+ return new PropertyValue(davPropertyName.getNamespace().getURI(), davPropertyName.getName(), buffer.toString());
+ } else if (field.isBooleanValue) {
+ if ("true".equals(value)) {
+ return new PropertyValue(davPropertyName.getNamespace().getURI(), davPropertyName.getName(), "1", "boolean");
+ } else if ("false".equals(value)) {
+ return new PropertyValue(davPropertyName.getNamespace().getURI(), davPropertyName.getName(), "0", "boolean");
+ } else {
+ throw new RuntimeException("Invalid value for " + field.alias + ": " + value);
+ }
+ } else if (field.isFloatValue) {
+ return new PropertyValue(davPropertyName.getNamespace().getURI(), davPropertyName.getName(), StringUtil.xmlEncode(value), "float");
+ } else if (field.isIntValue) {
+ return new PropertyValue(field.updatePropertyName.getNamespace().getURI(), field.updatePropertyName.getName(), StringUtil.xmlEncode(value), "int");
+ } else if (field.isDateValue) {
+ return new PropertyValue(field.updatePropertyName.getNamespace().getURI(), field.updatePropertyName.getName(), StringUtil.xmlEncode(value), "dateTime.tz");
+ } else {
+ return new PropertyValue(davPropertyName.getNamespace().getURI(), davPropertyName.getName(), StringUtil.xmlEncode(value));
+ }
+ }
+
+ /**
+ * SEARCH request property name for alias
+ *
+ * @param alias field alias
+ * @return request property string
+ */
+ public static String getRequestPropertyString(String alias) {
+ return Field.get(alias).requestPropertyString;
+ }
+
+ /**
+ * PROPFIND request property name
+ *
+ * @param alias field alias
+ * @return request property name
+ */
+ public static DavPropertyName getPropertyName(String alias) {
+ return Field.get(alias).davPropertyName;
+ }
+
+ /**
+ * SEARCH response property name
+ *
+ * @param alias field alias
+ * @return response property name
+ */
+ public static DavPropertyName getResponsePropertyName(String alias) {
+ return Field.get(alias).responsePropertyName;
+ }
+}
diff --git a/src/java/davmail/exchange/dav/PropertyType.java b/src/java/davmail/exchange/dav/PropertyType.java
new file mode 100644
index 0000000..cc493fe
--- /dev/null
+++ b/src/java/davmail/exchange/dav/PropertyType.java
@@ -0,0 +1,29 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.dav;
+
+/**
+ * MAPI property types.
+ */
+ at SuppressWarnings({"UnusedDeclaration", "JavaDoc"})
+public enum PropertyType {
+ ApplicationTime, ApplicationTimeArray, Binary, BinaryArray, Boolean, CLSID, CLSIDArray, Currency, CurrencyArray,
+ Double, DoubleArray, Error, Float, FloatArray, Integer, IntegerArray, Long, LongArray, Null, Object,
+ ObjectArray, Short, ShortArray, SystemTime, SystemTimeArray, String, StringArray
+}
diff --git a/src/java/davmail/exchange/dav/PropertyValue.java b/src/java/davmail/exchange/dav/PropertyValue.java
new file mode 100644
index 0000000..2462258
--- /dev/null
+++ b/src/java/davmail/exchange/dav/PropertyValue.java
@@ -0,0 +1,101 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.dav;
+
+/**
+ * Property value.
+ */
+public class PropertyValue {
+ protected final String namespaceUri;
+ protected final String name;
+ protected final String xmlEncodedValue;
+ protected final String typeString;
+
+ /**
+ * Create Dav property value.
+ *
+ * @param namespaceUri property namespace
+ * @param name property name
+ */
+ public PropertyValue(String namespaceUri, String name) {
+ this(namespaceUri, name, null, null);
+ }
+
+ /**
+ * Create Dav property value.
+ *
+ * @param namespaceUri property namespace
+ * @param name property name
+ * @param xmlEncodedValue xml encoded value
+ */
+ public PropertyValue(String namespaceUri, String name, String xmlEncodedValue) {
+ this(namespaceUri, name, xmlEncodedValue, null);
+ }
+
+ /**
+ * Create Dav property value.
+ *
+ * @param namespaceUri property namespace
+ * @param name property name
+ * @param xmlEncodedValue xml encoded value
+ * @param typeString property type
+ */
+ public PropertyValue(String namespaceUri, String name, String xmlEncodedValue, String typeString) {
+ this.namespaceUri = namespaceUri;
+ this.name = name;
+ this.xmlEncodedValue = xmlEncodedValue;
+ this.typeString = typeString;
+ }
+
+ /**
+ * Get property namespace.
+ *
+ * @return property namespace
+ */
+ public String getNamespaceUri() {
+ return namespaceUri;
+ }
+
+ /**
+ * Get xml encoded value.
+ *
+ * @return Xml encoded value
+ */
+ public String getXmlEncodedValue() {
+ return xmlEncodedValue;
+ }
+
+ /**
+ * Get property type.
+ *
+ * @return property type
+ */
+ public String getTypeString() {
+ return typeString;
+ }
+
+ /**
+ * Get property name.
+ *
+ * @return property name
+ */
+ public String getName() {
+ return name;
+ }
+}
diff --git a/src/java/davmail/exchange/ews/AffectedTaskOccurrences.java b/src/java/davmail/exchange/ews/AffectedTaskOccurrences.java
new file mode 100644
index 0000000..546d931
--- /dev/null
+++ b/src/java/davmail/exchange/ews/AffectedTaskOccurrences.java
@@ -0,0 +1,32 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2011 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+/**
+ * Item delete option.
+ */
+ at SuppressWarnings({"JavaDoc", "UnusedDeclaration"})
+public final class AffectedTaskOccurrences extends AttributeOption {
+ private AffectedTaskOccurrences(String value) {
+ super("AffectedTaskOccurrences", value);
+ }
+
+ public static final AffectedTaskOccurrences AllOccurrences = new AffectedTaskOccurrences("AllOccurrences");
+ public static final AffectedTaskOccurrences SpecifiedOccurrenceOnly = new AffectedTaskOccurrences("SpecifiedOccurrenceOnly");
+}
diff --git a/src/java/davmail/exchange/ews/AttributeOption.java b/src/java/davmail/exchange/ews/AttributeOption.java
new file mode 100644
index 0000000..d168e8b
--- /dev/null
+++ b/src/java/davmail/exchange/ews/AttributeOption.java
@@ -0,0 +1,51 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * Generic attribute option.
+ */
+public class AttributeOption extends Option {
+
+ protected AttributeOption(String name, String value) {
+ super(name, value);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public void appendTo(StringBuilder buffer) {
+ buffer.append(' ').append(name).append("=\"").append(value).append('"');
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public void write(Writer writer) throws IOException {
+ writer.write(" ");
+ writer.write(name);
+ writer.write("=\"");
+ writer.write(value);
+ writer.write("\"");
+ }
+}
diff --git a/src/java/davmail/exchange/ews/BaseShape.java b/src/java/davmail/exchange/ews/BaseShape.java
new file mode 100644
index 0000000..8e5757c
--- /dev/null
+++ b/src/java/davmail/exchange/ews/BaseShape.java
@@ -0,0 +1,42 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+/**
+ * Item or folder base shape.
+ */
+ at SuppressWarnings({"UnusedDeclaration"})
+public final class BaseShape extends ElementOption {
+ private BaseShape(String value) {
+ super("t:BaseShape", value);
+ }
+
+ /**
+ * Return id only.
+ */
+ public static final BaseShape ID_ONLY = new BaseShape("IdOnly");
+ /**
+ * Return default properties.
+ */
+ public static final BaseShape DEFAULT = new BaseShape("Default");
+ /**
+ * Return all properties, except MAPI extended properties.
+ */
+ public static final BaseShape ALL_PROPERTIES = new BaseShape("AllProperties");
+}
\ No newline at end of file
diff --git a/src/java/davmail/exchange/ews/ConflictResolution.java b/src/java/davmail/exchange/ews/ConflictResolution.java
new file mode 100644
index 0000000..fd217f8
--- /dev/null
+++ b/src/java/davmail/exchange/ews/ConflictResolution.java
@@ -0,0 +1,33 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+/**
+ * Item update conflict resolution
+ */
+ at SuppressWarnings({"JavaDoc", "UnusedDeclaration"})
+public final class ConflictResolution extends AttributeOption {
+ private ConflictResolution(String value) {
+ super("ConflictResolution", value);
+ }
+
+ public static final ConflictResolution NeverOverwrite = new ConflictResolution("NeverOverwrite");
+ public static final ConflictResolution AutoResolve = new ConflictResolution("AutoResolve");
+ public static final ConflictResolution AlwaysOverwrite = new ConflictResolution("AlwaysOverwrite");
+}
diff --git a/src/java/davmail/exchange/ews/ContainmentComparison.java b/src/java/davmail/exchange/ews/ContainmentComparison.java
new file mode 100644
index 0000000..fb7bc36
--- /dev/null
+++ b/src/java/davmail/exchange/ews/ContainmentComparison.java
@@ -0,0 +1,39 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+/**
+ * Contains comparison mode.
+ */
+ at SuppressWarnings({"UnusedDeclaration", "JavaDoc"})
+public final class ContainmentComparison extends AttributeOption {
+ private ContainmentComparison(String value) {
+ super("ContainmentComparison", value);
+ }
+
+ public static final ContainmentComparison Exact = new ContainmentComparison("Exact");
+ public static final ContainmentComparison IgnoreCase = new ContainmentComparison("IgnoreCase");
+ public static final ContainmentComparison IgnoreNonSpacingCharacters = new ContainmentComparison("IgnoreNonSpacingCharacters");
+ public static final ContainmentComparison Loose = new ContainmentComparison("Loose");
+ public static final ContainmentComparison IgnoreCaseAndNonSpacingCharacters = new ContainmentComparison("IgnoreCaseAndNonSpacingCharacters");
+ public static final ContainmentComparison LooseAndIgnoreCase = new ContainmentComparison("LooseAndIgnoreCase");
+ public static final ContainmentComparison LooseAndIgnoreNonSpace = new ContainmentComparison("LooseAndIgnoreNonSpace");
+ public static final ContainmentComparison LooseAndIgnoreCaseAndIgnoreNonSpace = new ContainmentComparison("LooseAndIgnoreCaseAndIgnoreNonSpace");
+
+}
diff --git a/src/java/davmail/exchange/ews/ContainmentMode.java b/src/java/davmail/exchange/ews/ContainmentMode.java
new file mode 100644
index 0000000..af16832
--- /dev/null
+++ b/src/java/davmail/exchange/ews/ContainmentMode.java
@@ -0,0 +1,49 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+/**
+ * Contains search mode.
+ */
+ at SuppressWarnings({"UnusedDeclaration", "JavaDoc"})
+public final class ContainmentMode extends AttributeOption {
+ private ContainmentMode(String value) {
+ super("ContainmentMode", value);
+ }
+
+ @Override
+ public String toString() {
+ return value;
+ }
+
+ /**
+ * Full String.
+ */
+ public static final ContainmentMode FullString = new ContainmentMode("FullString");
+ /**
+ * Starts with.
+ */
+ public static final ContainmentMode Prefixed = new ContainmentMode("Prefixed");
+ /**
+ * Contains
+ */
+ public static final ContainmentMode Substring = new ContainmentMode("Substring");
+ public static final ContainmentMode PrefixOnWords = new ContainmentMode("PrefixOnWords");
+ public static final ContainmentMode ExactPhrase = new ContainmentMode("ExactPhrase");
+}
diff --git a/src/java/davmail/exchange/ews/CopyItemMethod.java b/src/java/davmail/exchange/ews/CopyItemMethod.java
new file mode 100644
index 0000000..1d0d7f8
--- /dev/null
+++ b/src/java/davmail/exchange/ews/CopyItemMethod.java
@@ -0,0 +1,36 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+/**
+ * Copy item to another folder.
+ */
+public class CopyItemMethod extends EWSMethod {
+ /**
+ * Copy item method.
+ *
+ * @param itemId item id
+ * @param toFolderId target folder id
+ */
+ public CopyItemMethod(ItemId itemId, FolderId toFolderId) {
+ super("Item", "CopyItem");
+ this.itemId = itemId;
+ this.toFolderId = toFolderId;
+ }
+}
diff --git a/src/java/davmail/exchange/ews/CreateAttachmentMethod.java b/src/java/davmail/exchange/ews/CreateAttachmentMethod.java
new file mode 100644
index 0000000..31aa891
--- /dev/null
+++ b/src/java/davmail/exchange/ews/CreateAttachmentMethod.java
@@ -0,0 +1,36 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+/**
+ * Create Attachment Method.
+ */
+public class CreateAttachmentMethod extends EWSMethod {
+ /**
+ * Create attachment method.
+ *
+ * @param parentItemId parent item id
+ * @param attachment attachment object
+ */
+ public CreateAttachmentMethod(ItemId parentItemId, FileAttachment attachment) {
+ super("Item", "CreateAttachment");
+ this.parentItemId = parentItemId;
+ this.attachment = attachment;
+ }
+}
diff --git a/src/java/davmail/exchange/ews/CreateFolderMethod.java b/src/java/davmail/exchange/ews/CreateFolderMethod.java
new file mode 100644
index 0000000..4ea01d8
--- /dev/null
+++ b/src/java/davmail/exchange/ews/CreateFolderMethod.java
@@ -0,0 +1,36 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+/**
+ * Create Folder method.
+ */
+public class CreateFolderMethod extends EWSMethod {
+ /**
+ * Update folder method.
+ *
+ * @param parentFolderId parent folder id
+ * @param item folder item
+ */
+ public CreateFolderMethod(FolderId parentFolderId, Item item) {
+ super("Folder", "CreateFolder");
+ this.parentFolderId = parentFolderId;
+ this.item = item;
+ }
+}
diff --git a/src/java/davmail/exchange/ews/CreateItemMethod.java b/src/java/davmail/exchange/ews/CreateItemMethod.java
new file mode 100644
index 0000000..d486fd0
--- /dev/null
+++ b/src/java/davmail/exchange/ews/CreateItemMethod.java
@@ -0,0 +1,55 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+/**
+ * Create Item method.
+ */
+public class CreateItemMethod extends EWSMethod {
+ /**
+ * Create exchange item.
+ *
+ * @param messageDisposition save or send option
+ * @param savedItemFolderId saved item folder id
+ * @param item item content
+ */
+ public CreateItemMethod(MessageDisposition messageDisposition, FolderId savedItemFolderId, EWSMethod.Item item) {
+ super("Item", "CreateItem");
+ this.savedItemFolderId = savedItemFolderId;
+ this.item = item;
+ addMethodOption(messageDisposition);
+ }
+
+ /**
+ * Create exchange item.
+ *
+ * @param messageDisposition save or send option
+ * @param sendMeetingInvitations send invitation option
+ * @param savedItemFolderId saved item folder id
+ * @param item item content
+ */
+ public CreateItemMethod(MessageDisposition messageDisposition, SendMeetingInvitations sendMeetingInvitations, FolderId savedItemFolderId, EWSMethod.Item item) {
+ super("Item", "CreateItem");
+ this.savedItemFolderId = savedItemFolderId;
+ this.item = item;
+ addMethodOption(messageDisposition);
+ addMethodOption(sendMeetingInvitations);
+ }
+
+}
diff --git a/src/java/davmail/exchange/ews/DeleteAttachmentMethod.java b/src/java/davmail/exchange/ews/DeleteAttachmentMethod.java
new file mode 100644
index 0000000..6bab757
--- /dev/null
+++ b/src/java/davmail/exchange/ews/DeleteAttachmentMethod.java
@@ -0,0 +1,34 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+/**
+ * Delete attachment method.
+ */
+public class DeleteAttachmentMethod extends EWSMethod {
+ /**
+ * Delete attachment method.
+ *
+ * @param attachmentId attachment id
+ */
+ public DeleteAttachmentMethod(String attachmentId) {
+ super("Item", "DeleteAttachment");
+ this.attachmentId = attachmentId;
+ }
+}
diff --git a/src/java/davmail/exchange/ews/DeleteFolderMethod.java b/src/java/davmail/exchange/ews/DeleteFolderMethod.java
new file mode 100644
index 0000000..7207653
--- /dev/null
+++ b/src/java/davmail/exchange/ews/DeleteFolderMethod.java
@@ -0,0 +1,35 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+/**
+ * Delete Folder method.
+ */
+public class DeleteFolderMethod extends EWSMethod {
+ /**
+ * Delete folder method.
+ *
+ * @param folderId folder id
+ */
+ public DeleteFolderMethod(FolderId folderId) {
+ super("Folder", "DeleteFolder");
+ this.folderId = folderId;
+ this.deleteType = Disposal.HardDelete;
+ }
+}
diff --git a/src/java/davmail/exchange/ews/DeleteItemMethod.java b/src/java/davmail/exchange/ews/DeleteItemMethod.java
new file mode 100644
index 0000000..71bfe09
--- /dev/null
+++ b/src/java/davmail/exchange/ews/DeleteItemMethod.java
@@ -0,0 +1,40 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+/**
+ * Delete Item method.
+ */
+public class DeleteItemMethod extends EWSMethod {
+ /**
+ * Delete item method.
+ *
+ * @param itemId item id
+ * @param deleteType delete mode
+ * @param sendMeetingCancellations send meeting cancellation notifications
+ */
+ public DeleteItemMethod(ItemId itemId, DeleteType deleteType, SendMeetingCancellations sendMeetingCancellations) {
+ super("Item", "DeleteItem");
+ addMethodOption(deleteType);
+ addMethodOption(sendMeetingCancellations);
+ addMethodOption(AffectedTaskOccurrences.AllOccurrences);
+ this.itemId = itemId;
+ }
+
+}
diff --git a/src/java/davmail/exchange/ews/DeleteType.java b/src/java/davmail/exchange/ews/DeleteType.java
new file mode 100644
index 0000000..0b54317
--- /dev/null
+++ b/src/java/davmail/exchange/ews/DeleteType.java
@@ -0,0 +1,33 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+/**
+ * DeleteItem disposal type.
+ */
+ at SuppressWarnings({"JavaDoc", "UnusedDeclaration"})
+public final class DeleteType extends AttributeOption {
+ private DeleteType(String value) {
+ super("DeleteType", value);
+ }
+
+ public static final DeleteType HardDelete = new DeleteType("HardDelete");
+ public static final DeleteType SoftDelete = new DeleteType("SoftDelete");
+ public static final DeleteType MoveToDeletedItems = new DeleteType("MoveToDeletedItems");
+}
diff --git a/src/java/davmail/exchange/ews/Disposal.java b/src/java/davmail/exchange/ews/Disposal.java
new file mode 100644
index 0000000..c359311
--- /dev/null
+++ b/src/java/davmail/exchange/ews/Disposal.java
@@ -0,0 +1,33 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+/**
+ * Disposal.
+ */
+ at SuppressWarnings({"JavaDoc", "UnusedDeclaration"})
+public final class Disposal extends AttributeOption {
+ private Disposal(String value) {
+ super("DeleteType", value);
+ }
+
+ public static final Disposal HardDelete = new Disposal("HardDelete");
+ public static final Disposal SoftDelete = new Disposal("SoftDelete");
+ public static final Disposal MoveToDeletedItems = new Disposal("MoveToDeletedItems");
+}
diff --git a/src/java/davmail/exchange/ews/DistinguishedFolderId.java b/src/java/davmail/exchange/ews/DistinguishedFolderId.java
new file mode 100644
index 0000000..0267425
--- /dev/null
+++ b/src/java/davmail/exchange/ews/DistinguishedFolderId.java
@@ -0,0 +1,70 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Distinguished Folder Id.
+ */
+public final class DistinguishedFolderId extends FolderId {
+
+ private DistinguishedFolderId(String value) {
+ super("t:DistinguishedFolderId", value, null);
+ }
+
+ private DistinguishedFolderId(String value, String mailbox) {
+ super("t:DistinguishedFolderId", value, null, mailbox);
+ }
+
+ /**
+ * DistinguishedFolderId names
+ */
+ @SuppressWarnings({"UnusedDeclaration", "JavaDoc"})
+ public static enum Name {
+ calendar, contacts, deleteditems, drafts, inbox, journal, notes, outbox, sentitems, tasks, msgfolderroot,
+ publicfoldersroot, root, junkemail, searchfolders, voicemail,
+ archivemsgfolderroot
+ }
+
+ private static final Map<Name, DistinguishedFolderId> folderIdMap = new HashMap<Name, DistinguishedFolderId>();
+
+ static {
+ for (Name name : Name.values()) {
+ folderIdMap.put(name, new DistinguishedFolderId(name.toString()));
+ }
+ }
+
+ /**
+ * Get DistinguishedFolderId object for mailbox and name.
+ *
+ * @param mailbox mailbox name
+ * @param name folder id name
+ * @return DistinguishedFolderId object
+ */
+ public static DistinguishedFolderId getInstance(String mailbox, Name name) {
+ if (mailbox == null) {
+ return folderIdMap.get(name);
+ } else {
+ return new DistinguishedFolderId(name.toString(), mailbox);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/java/davmail/exchange/ews/EWSException.java b/src/java/davmail/exchange/ews/EWSException.java
new file mode 100644
index 0000000..edad327
--- /dev/null
+++ b/src/java/davmail/exchange/ews/EWSException.java
@@ -0,0 +1,35 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+import java.io.IOException;
+
+/**
+ * EWS Exception
+ */
+public class EWSException extends IOException {
+ /**
+ * Create EWS Exception with detailed error message
+ *
+ * @param message error message
+ */
+ public EWSException(String message) {
+ super(message);
+ }
+}
diff --git a/src/java/davmail/exchange/ews/EWSMethod.java b/src/java/davmail/exchange/ews/EWSMethod.java
new file mode 100644
index 0000000..914853a
--- /dev/null
+++ b/src/java/davmail/exchange/ews/EWSMethod.java
@@ -0,0 +1,1118 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+import davmail.BundleMessage;
+import davmail.Settings;
+import davmail.exchange.XMLStreamUtil;
+import davmail.http.DavGatewayHttpClientFacade;
+import davmail.ui.tray.DavGatewayTray;
+import davmail.util.StringUtil;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.httpclient.*;
+import org.apache.commons.httpclient.methods.PostMethod;
+import org.apache.commons.httpclient.methods.RequestEntity;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.codehaus.stax2.typed.TypedXMLStreamReader;
+
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import java.io.*;
+import java.util.*;
+import java.util.zip.GZIPInputStream;
+
+/**
+ * EWS SOAP method.
+ */
+ at SuppressWarnings("Since15")
+public abstract class EWSMethod extends PostMethod {
+ protected static final Logger LOGGER = Logger.getLogger(EWSMethod.class);
+
+ protected FolderQueryTraversal traversal;
+ protected BaseShape baseShape;
+ protected boolean includeMimeContent;
+ protected FolderId folderId;
+ protected FolderId savedItemFolderId;
+ protected FolderId toFolderId;
+ protected FolderId parentFolderId;
+ protected ItemId itemId;
+ protected ItemId parentItemId;
+ protected Set<FieldURI> additionalProperties;
+ protected Disposal deleteType;
+ protected Set<AttributeOption> methodOptions;
+ protected ElementOption unresolvedEntry;
+
+ // paging request
+ protected int maxCount;
+ protected int offset;
+ // paging response
+ protected boolean includesLastItemInRange;
+
+ protected List<FieldUpdate> updates;
+
+ protected FileAttachment attachment;
+
+ protected String attachmentId;
+
+ protected final String itemType;
+ protected final String methodName;
+ protected final String responseCollectionName;
+
+ protected List<Item> responseItems;
+ protected String errorDetail;
+ protected String errorDescription;
+ protected Item item;
+
+ protected SearchExpression searchExpression;
+
+ protected String serverVersion;
+ protected String timezoneContext;
+
+ /**
+ * Build EWS method
+ *
+ * @param itemType item type
+ * @param methodName method name
+ */
+ public EWSMethod(String itemType, String methodName) {
+ this(itemType, methodName, itemType + 's');
+ }
+
+ /**
+ * Build EWS method
+ *
+ * @param itemType item type
+ * @param methodName method name
+ * @param responseCollectionName item response collection name
+ */
+ public EWSMethod(String itemType, String methodName, String responseCollectionName) {
+ super("/ews/exchange.asmx");
+ this.itemType = itemType;
+ this.methodName = methodName;
+ this.responseCollectionName = responseCollectionName;
+ if (Settings.getBooleanProperty("davmail.acceptEncodingGzip", true) &&
+ !Level.DEBUG.toString().equals(Settings.getBooleanProperty("log4j.logger.httpclient.wire"))) {
+ setRequestHeader("Accept-Encoding", "gzip");
+ }
+ setRequestEntity(new RequestEntity() {
+ byte[] content;
+
+ public boolean isRepeatable() {
+ return true;
+ }
+
+ public void writeRequest(OutputStream outputStream) throws IOException {
+ if (content == null) {
+ content = generateSoapEnvelope();
+ }
+ outputStream.write(content);
+ }
+
+ public long getContentLength() {
+ if (content == null) {
+ content = generateSoapEnvelope();
+ }
+ return content.length;
+ }
+
+ public String getContentType() {
+ return "text/xml; charset=UTF-8";
+ }
+ });
+ }
+
+
+ @Override
+ public String getName() {
+ return "POST";
+ }
+
+ protected void addAdditionalProperty(FieldURI additionalProperty) {
+ if (additionalProperties == null) {
+ additionalProperties = new HashSet<FieldURI>();
+ }
+ additionalProperties.add(additionalProperty);
+ }
+
+ protected void addMethodOption(AttributeOption attributeOption) {
+ if (methodOptions == null) {
+ methodOptions = new HashSet<AttributeOption>();
+ }
+ methodOptions.add(attributeOption);
+ }
+
+ protected void setSearchExpression(SearchExpression searchExpression) {
+ this.searchExpression = searchExpression;
+ }
+
+ protected void writeShape(Writer writer) throws IOException {
+ if (baseShape != null) {
+ writer.write("<m:");
+ writer.write(itemType);
+ writer.write("Shape>");
+ baseShape.write(writer);
+ if (includeMimeContent) {
+ writer.write("<t:IncludeMimeContent>true</t:IncludeMimeContent>");
+ }
+ if (additionalProperties != null) {
+ writer.write("<t:AdditionalProperties>");
+ StringBuilder buffer = new StringBuilder();
+ for (FieldURI fieldURI : additionalProperties) {
+ fieldURI.appendTo(buffer);
+ }
+ writer.write(buffer.toString());
+ writer.write("</t:AdditionalProperties>");
+ }
+ writer.write("</m:");
+ writer.write(itemType);
+ writer.write("Shape>");
+ }
+ }
+
+ protected void writeItemId(Writer writer) throws IOException {
+ if (itemId != null) {
+ if (updates == null) {
+ writer.write("<m:ItemIds>");
+ }
+ itemId.write(writer);
+ if (updates == null) {
+ writer.write("</m:ItemIds>");
+ }
+ }
+ }
+
+ protected void writeParentItemId(Writer writer) throws IOException {
+ if (parentItemId != null) {
+ writer.write("<m:ParentItemId Id=\"");
+ writer.write(parentItemId.id);
+ if (parentItemId.changeKey != null) {
+ writer.write("\" ChangeKey=\"");
+ writer.write(parentItemId.changeKey);
+ }
+ writer.write("\"/>");
+ }
+ }
+
+ protected void writeFolderId(Writer writer) throws IOException {
+ if (folderId != null) {
+ if (updates == null) {
+ writer.write("<m:FolderIds>");
+ }
+ folderId.write(writer);
+ if (updates == null) {
+ writer.write("</m:FolderIds>");
+ }
+ }
+ }
+
+ protected void writeSavedItemFolderId(Writer writer) throws IOException {
+ if (savedItemFolderId != null) {
+ writer.write("<m:SavedItemFolderId>");
+ savedItemFolderId.write(writer);
+ writer.write("</m:SavedItemFolderId>");
+ }
+ }
+
+ protected void writeToFolderId(Writer writer) throws IOException {
+ if (toFolderId != null) {
+ writer.write("<m:ToFolderId>");
+ toFolderId.write(writer);
+ writer.write("</m:ToFolderId>");
+ }
+ }
+
+ protected void writeParentFolderId(Writer writer) throws IOException {
+ if (parentFolderId != null) {
+ writer.write("<m:ParentFolderId");
+ if (item == null) {
+ writer.write("s");
+ }
+ writer.write(">");
+ parentFolderId.write(writer);
+ writer.write("</m:ParentFolderId");
+ if (item == null) {
+ writer.write("s");
+ }
+ writer.write(">");
+ }
+ }
+
+ protected void writeItem(Writer writer) throws IOException {
+ if (item != null) {
+ writer.write("<m:");
+ writer.write(itemType);
+ writer.write("s>");
+ item.write(writer);
+ writer.write("</m:");
+ writer.write(itemType);
+ writer.write("s>");
+ }
+ }
+
+ protected void writeRestriction(Writer writer) throws IOException {
+ if (searchExpression != null) {
+ writer.write("<m:Restriction>");
+ StringBuilder buffer = new StringBuilder();
+ searchExpression.appendTo(buffer);
+ writer.write(buffer.toString());
+ writer.write("</m:Restriction>");
+ }
+ }
+
+ protected void startChanges(Writer writer) throws IOException {
+ //noinspection VariableNotUsedInsideIf
+ if (updates != null) {
+ writer.write("<m:");
+ writer.write(itemType);
+ writer.write("Changes>");
+ writer.write("<t:");
+ writer.write(itemType);
+ writer.write("Change>");
+ }
+ }
+
+ protected void writeUpdates(Writer writer) throws IOException {
+ if (updates != null) {
+ writer.write("<t:Updates>");
+ for (FieldUpdate fieldUpdate : updates) {
+ fieldUpdate.write(itemType, writer);
+ }
+ writer.write("</t:Updates>");
+ }
+ }
+
+ protected void writeUnresolvedEntry(Writer writer) throws IOException {
+ if (unresolvedEntry != null) {
+ unresolvedEntry.write(writer);
+ }
+ }
+
+ protected void endChanges(Writer writer) throws IOException {
+ //noinspection VariableNotUsedInsideIf
+ if (updates != null) {
+ writer.write("</t:");
+ writer.write(itemType);
+ writer.write("Change>");
+ writer.write("</m:");
+ writer.write(itemType);
+ writer.write("Changes>");
+ }
+ }
+
+ protected byte[] generateSoapEnvelope() {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try {
+ OutputStreamWriter writer = new OutputStreamWriter(baos, "UTF-8");
+ writer.write("<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" " +
+ "xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\" " +
+ "xmlns:m=\"http://schemas.microsoft.com/exchange/services/2006/messages\">" +
+ "");
+ writer.write("<soap:Header>");
+ if (serverVersion != null) {
+ writer.write("<t:RequestServerVersion Version=\"");
+ writer.write(serverVersion);
+ writer.write("\"/>");
+ }
+ if (timezoneContext != null) {
+ writer.write("<t:TimeZoneContext><t:TimeZoneDefinition Id=\"");
+ writer.write(timezoneContext);
+ writer.write("\"/></t:TimeZoneContext>");
+ }
+ writer.write("</soap:Header>");
+
+ writer.write("<soap:Body>");
+ writer.write("<m:");
+ writer.write(methodName);
+ if (traversal != null) {
+ traversal.write(writer);
+ }
+ if (deleteType != null) {
+ deleteType.write(writer);
+ }
+ if (methodOptions != null) {
+ for (AttributeOption attributeOption : methodOptions) {
+ attributeOption.write(writer);
+ }
+ }
+ writer.write(">");
+ writeSoapBody(writer);
+ writer.write("</m:");
+ writer.write(methodName);
+ writer.write(">");
+ writer.write("</soap:Body>" +
+ "</soap:Envelope>");
+ writer.flush();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return baos.toByteArray();
+ }
+
+ protected void writeSoapBody(Writer writer) throws IOException {
+ startChanges(writer);
+ writeShape(writer);
+ writeIndexedPageItemView(writer);
+ writeRestriction(writer);
+ writeParentFolderId(writer);
+ writeToFolderId(writer);
+ writeItemId(writer);
+ writeParentItemId(writer);
+ writeAttachments(writer);
+ writeAttachmentId(writer);
+ writeFolderId(writer);
+ writeSavedItemFolderId(writer);
+ writeItem(writer);
+ writeUpdates(writer);
+ writeUnresolvedEntry(writer);
+ endChanges(writer);
+ }
+
+
+ protected void writeIndexedPageItemView(Writer writer) throws IOException {
+ if (maxCount > 0) {
+ writer.write("<m:IndexedPageItemView MaxEntriesReturned=\"");
+ writer.write(String.valueOf(maxCount));
+ writer.write("\" Offset=\"");
+ writer.write(String.valueOf(offset));
+ writer.write("\" BasePoint=\"Beginning\"/>");
+
+ }
+ }
+
+ protected void writeAttachmentId(Writer writer) throws IOException {
+ if (attachmentId != null) {
+ if ("CreateAttachment".equals(methodName)) {
+ writer.write("<m:AttachmentShape>");
+ writer.write("<t:IncludeMimeContent>true</t:IncludeMimeContent>");
+ writer.write("</m:AttachmentShape>");
+ }
+ writer.write("<m:AttachmentIds>");
+ writer.write("<t:AttachmentId Id=\"");
+ writer.write(attachmentId);
+ writer.write("\"/>");
+ writer.write("</m:AttachmentIds>");
+ }
+ }
+
+ protected void writeAttachments(Writer writer) throws IOException {
+ if (attachment != null) {
+ writer.write("<m:Attachments>");
+ attachment.write(writer);
+ writer.write("</m:Attachments>");
+ }
+ }
+
+ /**
+ * Get Exchange server version, Exchange2010 or Exchange2007_SP1
+ *
+ * @return server version
+ */
+ public String getServerVersion() {
+ return serverVersion;
+ }
+
+ /**
+ * Set Exchange server version, Exchange2010 or Exchange2007_SP1
+ *
+ * @param serverVersion server version
+ */
+ public void setServerVersion(String serverVersion) {
+ this.serverVersion = serverVersion;
+ }
+
+ /**
+ * Set Exchange timezone context
+ *
+ * @param timezoneContext user timezone context
+ */
+ public void setTimezoneContext(String timezoneContext) {
+ this.timezoneContext = timezoneContext;
+ }
+
+ /**
+ * Meeting attendee object
+ */
+ public static class Attendee {
+ /**
+ * attendee role
+ */
+ public String role;
+ /**
+ * attendee email address
+ */
+ public String email;
+ /**
+ * attendee participation status
+ */
+ public String partstat;
+ /**
+ * attendee fullname
+ */
+ public String name;
+ }
+
+ /**
+ * Recurring event occurrence
+ */
+ public static class Occurrence {
+ /**
+ * Original occurence start date
+ */
+ public String originalStart;
+ }
+
+ /**
+ * Item
+ */
+ public static class Item extends HashMap<String, String> {
+ /**
+ * Item type.
+ */
+ public String type;
+ protected byte[] mimeContent;
+ protected List<FieldUpdate> fieldUpdates;
+ protected List<FileAttachment> attachments;
+ protected List<Attendee> attendees;
+ protected final List<String> fieldNames = new ArrayList<String>();
+ protected List<Occurrence> occurrences;
+ protected ItemId referenceItemId;
+
+ @Override
+ public String toString() {
+ return "type: " + type + ' ' + super.toString();
+ }
+
+ @Override
+ public String put(String key, String value) {
+ if (value != null) {
+ if (get(key) == null) {
+ fieldNames.add(key);
+ }
+ return super.put(key, value);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Write XML content to writer.
+ *
+ * @param writer writer
+ * @throws IOException on error
+ */
+ public void write(Writer writer) throws IOException {
+ writer.write("<t:");
+ writer.write(type);
+ writer.write(">");
+ if (mimeContent != null) {
+ writer.write("<t:MimeContent>");
+ writer.write(new String(mimeContent));
+ writer.write("</t:MimeContent>");
+ }
+ if (referenceItemId != null) {
+ referenceItemId.write(writer);
+ }
+ // write ordered fields
+ for (String key : fieldNames) {
+ if ("MeetingTimeZone".equals(key)) {
+ writer.write("<t:MeetingTimeZone TimeZoneName=\"");
+ writer.write(StringUtil.xmlEncodeAttribute(get(key)));
+ writer.write("\"></t:MeetingTimeZone>");
+ } else if ("StartTimeZone".equals(key)) {
+ writer.write("<t:StartTimeZone Id=\"");
+ writer.write(StringUtil.xmlEncodeAttribute(get(key)));
+ writer.write("\"></t:StartTimeZone>");
+ } else {
+ writer.write("<t:");
+ writer.write(key);
+ writer.write(">");
+ writer.write(StringUtil.xmlEncode(get(key)));
+ writer.write("</t:");
+ writer.write(key);
+ writer.write(">");
+ }
+ }
+ if (fieldUpdates != null) {
+ for (FieldUpdate fieldUpdate : fieldUpdates) {
+ fieldUpdate.write(null, writer);
+ }
+ }
+ writer.write("</t:");
+ writer.write(type);
+ writer.write(">");
+ }
+
+ /**
+ * Field updates.
+ *
+ * @param fieldUpdates field updates
+ */
+ public void setFieldUpdates(List<FieldUpdate> fieldUpdates) {
+ this.fieldUpdates = fieldUpdates;
+ }
+
+ /**
+ * Get property value as int
+ *
+ * @param key property response name
+ * @return property value
+ */
+ public int getInt(String key) {
+ int result = 0;
+ String value = get(key);
+ if (value != null && value.length() > 0) {
+ result = Integer.parseInt(value);
+ }
+ return result;
+ }
+
+ /**
+ * Get property value as long
+ *
+ * @param key property response name
+ * @return property value
+ */
+ public long getLong(String key) {
+ long result = 0;
+ String value = get(key);
+ if (value != null && value.length() > 0) {
+ result = Long.parseLong(value);
+ }
+ return result;
+ }
+
+
+ /**
+ * Get property value as boolean
+ *
+ * @param key property response name
+ * @return property value
+ */
+ public boolean getBoolean(String key) {
+ boolean result = false;
+ String value = get(key);
+ if (value != null && value.length() > 0) {
+ result = Boolean.parseBoolean(value);
+ }
+ return result;
+ }
+
+ /**
+ * Get file attachment by file name
+ *
+ * @param attachmentName attachment name
+ * @return attachment
+ */
+ public FileAttachment getAttachmentByName(String attachmentName) {
+ FileAttachment result = null;
+ if (attachments != null) {
+ for (FileAttachment attachment : attachments) {
+ if (attachmentName.equals(attachment.name)) {
+ result = attachment;
+ break;
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Get all attendees.
+ *
+ * @return all attendees
+ */
+ public List<Attendee> getAttendees() {
+ return attendees;
+ }
+
+ /**
+ * Add attendee.
+ *
+ * @param attendee attendee object
+ */
+ public void addAttendee(Attendee attendee) {
+ if (attendees == null) {
+ attendees = new ArrayList<Attendee>();
+ }
+ attendees.add(attendee);
+ }
+
+ /**
+ * Add occurrence.
+ *
+ * @param occurrence event occurence
+ */
+ public void addOccurrence(Occurrence occurrence) {
+ if (occurrences == null) {
+ occurrences = new ArrayList<Occurrence>();
+ }
+ occurrences.add(occurrence);
+ }
+
+ /**
+ * Get occurences.
+ *
+ * @return event occurences
+ */
+ public List<Occurrence> getOccurrences() {
+ return occurrences;
+ }
+ }
+
+ /**
+ * Check method success.
+ *
+ * @throws EWSException on error
+ */
+ public void checkSuccess() throws EWSException {
+ if (errorDetail != null) {
+ if (!"ErrorAccessDenied".equals(errorDetail)
+ && !"ErrorMailRecipientNotFound".equals(errorDetail)
+ && !"ErrorItemNotFound".equals(errorDetail)
+ ) {
+ try {
+ throw new EWSException(errorDetail +" "+((errorDescription!=null)?errorDescription:"")+ "\n request: " + new String(generateSoapEnvelope(), "UTF-8"));
+ } catch (UnsupportedEncodingException e) {
+ throw new EWSException(e.getMessage());
+ }
+ }
+ }
+ if (getStatusCode() == HttpStatus.SC_BAD_REQUEST) {
+ throw new EWSException(getStatusText());
+ }
+ }
+
+ @Override
+ public int getStatusCode() {
+ if ("ErrorAccessDenied".equals(errorDetail)) {
+ return HttpStatus.SC_FORBIDDEN;
+ } else {
+ return super.getStatusCode();
+ }
+ }
+
+ /**
+ * Get response items.
+ *
+ * @return response items
+ * @throws EWSException on error
+ */
+ public List<Item> getResponseItems() throws EWSException {
+ checkSuccess();
+ if (responseItems != null) {
+ return responseItems;
+ } else {
+ return new ArrayList<Item>();
+ }
+ }
+
+ /**
+ * Get single response item.
+ *
+ * @return response item
+ * @throws EWSException on error
+ */
+ public Item getResponseItem() throws EWSException {
+ checkSuccess();
+ if (responseItems != null && responseItems.size() > 0) {
+ return responseItems.get(0);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Get response mime content.
+ *
+ * @return mime content
+ * @throws EWSException on error
+ */
+ public byte[] getMimeContent() throws EWSException {
+ checkSuccess();
+ Item responseItem = getResponseItem();
+ if (responseItem != null) {
+ return responseItem.mimeContent;
+ } else {
+ return null;
+ }
+ }
+
+ protected String handleTag(XMLStreamReader reader, String localName) throws XMLStreamException {
+ String result = null;
+ int event = reader.getEventType();
+ if (event == XMLStreamConstants.START_ELEMENT && localName.equals(reader.getLocalName())) {
+ while (reader.hasNext() &&
+ !((event == XMLStreamConstants.END_ELEMENT && localName.equals(reader.getLocalName())))) {
+ event = reader.next();
+ if (event == XMLStreamConstants.CHARACTERS) {
+ result = reader.getText();
+ }
+ }
+ }
+ return result;
+ }
+
+ protected void handleErrors(XMLStreamReader reader) throws XMLStreamException {
+ String result = handleTag(reader, "ResponseCode");
+ // store error description;
+ String messageText = handleTag(reader, "MessageText");
+ if (messageText != null) {
+ errorDescription = messageText;
+ }
+ if (errorDetail == null && result != null
+ && !"NoError".equals(result)
+ && !"ErrorNameResolutionMultipleResults".equals(result)
+ && !"ErrorNameResolutionNoResults".equals(result)
+ && !"ErrorFolderExists".equals(result)
+ ) {
+ errorDetail = result;
+ }
+ if (XMLStreamUtil.isStartTag(reader, "faultstring")) {
+ errorDetail = XMLStreamUtil.getElementText(reader);
+ }
+ }
+
+ protected Item handleItem(XMLStreamReader reader) throws XMLStreamException {
+ Item responseItem = new Item();
+ responseItem.type = reader.getLocalName();
+ while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, responseItem.type)) {
+ reader.next();
+ if (XMLStreamUtil.isStartTag(reader)) {
+ String tagLocalName = reader.getLocalName();
+ String value = null;
+ if ("ExtendedProperty".equals(tagLocalName)) {
+ addExtendedPropertyValue(reader, responseItem);
+ } else if (tagLocalName.endsWith("MimeContent")) {
+ handleMimeContent(reader, responseItem);
+ } else if ("Attachments".equals(tagLocalName)) {
+ responseItem.attachments = handleAttachments(reader);
+ } else if ("EmailAddresses".equals(tagLocalName)) {
+ handleEmailAddresses(reader, responseItem);
+ } else if ("RequiredAttendees".equals(tagLocalName) || "OptionalAttendees".equals(tagLocalName)) {
+ handleAttendees(reader, responseItem, tagLocalName);
+ } else if ("ModifiedOccurrences".equals(tagLocalName)) {
+ handleModifiedOccurrences(reader, responseItem);
+ } else {
+ if (tagLocalName.endsWith("Id")) {
+ value = getAttributeValue(reader, "Id");
+ // get change key
+ responseItem.put("ChangeKey", getAttributeValue(reader, "ChangeKey"));
+ }
+ if (value == null) {
+ value = getTagContent(reader);
+ }
+ if (value != null) {
+ responseItem.put(tagLocalName, value);
+ }
+ }
+ }
+ }
+ return responseItem;
+ }
+
+ protected void handleEmailAddresses(XMLStreamReader reader, Item item) throws XMLStreamException {
+ while (reader.hasNext() && !(XMLStreamUtil.isEndTag(reader, "EmailAddresses"))) {
+ reader.next();
+ if (XMLStreamUtil.isStartTag(reader)) {
+ String tagLocalName = reader.getLocalName();
+ if ("Entry".equals(tagLocalName)) {
+ item.put(reader.getAttributeValue(null, "Key"), XMLStreamUtil.getElementText(reader));
+ }
+ }
+ }
+ }
+
+ protected void handleAttendees(XMLStreamReader reader, Item item, String attendeeType) throws XMLStreamException {
+ while (reader.hasNext() && !(XMLStreamUtil.isEndTag(reader, attendeeType))) {
+ reader.next();
+ if (XMLStreamUtil.isStartTag(reader)) {
+ String tagLocalName = reader.getLocalName();
+ if ("Attendee".equals(tagLocalName)) {
+ handleAttendee(reader, item, attendeeType);
+ }
+ }
+ }
+ }
+
+ protected void handleModifiedOccurrences(XMLStreamReader reader, Item item) throws XMLStreamException {
+ while (reader.hasNext() && !(XMLStreamUtil.isEndTag(reader, "ModifiedOccurrences"))) {
+ reader.next();
+ if (XMLStreamUtil.isStartTag(reader)) {
+ String tagLocalName = reader.getLocalName();
+ if ("Occurrence".equals(tagLocalName)) {
+ handleOccurrence(reader, item);
+ }
+ }
+ }
+ }
+
+ protected void handleOccurrence(XMLStreamReader reader, Item item) throws XMLStreamException {
+ while (reader.hasNext() && !(XMLStreamUtil.isEndTag(reader, "Occurrence"))) {
+ reader.next();
+ if (XMLStreamUtil.isStartTag(reader)) {
+ String tagLocalName = reader.getLocalName();
+ if ("OriginalStart".equals(tagLocalName)) {
+ Occurrence occurrence = new Occurrence();
+ occurrence.originalStart = XMLStreamUtil.getElementText(reader);
+ item.addOccurrence(occurrence);
+ }
+ }
+ }
+ }
+
+ public static String responseTypeToPartstat(String responseType) {
+ if ("Accept".equals(responseType)) {
+ return "ACCEPTED";
+ } else if ("Tentative".equals(responseType)) {
+ return "TENTATIVE";
+ } else if ("Decline".equals(responseType)) {
+ return "DECLINED";
+ } else {
+ return "NEEDS-ACTION";
+ }
+ }
+
+ protected void handleAttendee(XMLStreamReader reader, Item item, String attendeeType) throws XMLStreamException {
+ Attendee attendee = new Attendee();
+ if ("RequiredAttendees".equals(attendeeType)) {
+ attendee.role = "REQ-PARTICIPANT";
+ } else {
+ attendee.role = "OPT-PARTICIPANT";
+ }
+ while (reader.hasNext() && !(XMLStreamUtil.isEndTag(reader, "Attendee"))) {
+ reader.next();
+ if (XMLStreamUtil.isStartTag(reader)) {
+ String tagLocalName = reader.getLocalName();
+ if ("EmailAddress".equals(tagLocalName)) {
+ attendee.email = reader.getElementText();
+ } else if ("Name".equals(tagLocalName)) {
+ attendee.name = XMLStreamUtil.getElementText(reader);
+ } else if ("ResponseType".equals(tagLocalName)) {
+ String responseType = XMLStreamUtil.getElementText(reader);
+ attendee.partstat = responseTypeToPartstat(responseType);
+ }
+ }
+ }
+ item.addAttendee(attendee);
+ }
+
+ protected List<FileAttachment> handleAttachments(XMLStreamReader reader) throws XMLStreamException {
+ List<FileAttachment> attachments = new ArrayList<FileAttachment>();
+ while (reader.hasNext() && !(XMLStreamUtil.isEndTag(reader, "Attachments"))) {
+ reader.next();
+ if (XMLStreamUtil.isStartTag(reader)) {
+ String tagLocalName = reader.getLocalName();
+ if ("FileAttachment".equals(tagLocalName)) {
+ attachments.add(handleFileAttachment(reader));
+ }
+ }
+ }
+ return attachments;
+ }
+
+ protected FileAttachment handleFileAttachment(XMLStreamReader reader) throws XMLStreamException {
+ FileAttachment fileAttachment = new FileAttachment();
+ while (reader.hasNext() && !(XMLStreamUtil.isEndTag(reader, "FileAttachment"))) {
+ reader.next();
+ if (XMLStreamUtil.isStartTag(reader)) {
+ String tagLocalName = reader.getLocalName();
+ if ("AttachmentId".equals(tagLocalName)) {
+ fileAttachment.attachmentId = getAttributeValue(reader, "Id");
+ } else if ("Name".equals(tagLocalName)) {
+ fileAttachment.name = getTagContent(reader);
+ } else if ("ContentType".equals(tagLocalName)) {
+ fileAttachment.contentType = getTagContent(reader);
+ }
+ }
+ }
+ return fileAttachment;
+ }
+
+
+ protected void handleMimeContent(XMLStreamReader reader, Item responseItem) throws XMLStreamException {
+ if (reader instanceof TypedXMLStreamReader) {
+ // Stax2 parser: use enhanced base64 conversion
+ responseItem.mimeContent = ((TypedXMLStreamReader) reader).getElementAsBinary();
+ } else {
+ // failover: slow and memory consuming conversion
+ byte[] base64MimeContent = reader.getElementText().getBytes();
+ responseItem.mimeContent = Base64.decodeBase64(base64MimeContent);
+ }
+ }
+
+ protected void addExtendedPropertyValue(XMLStreamReader reader, Item item) throws XMLStreamException {
+ String propertyTag = null;
+ String propertyValue = null;
+ while (reader.hasNext() && !(XMLStreamUtil.isEndTag(reader, "ExtendedProperty"))) {
+ reader.next();
+ if (XMLStreamUtil.isStartTag(reader)) {
+ String tagLocalName = reader.getLocalName();
+ if ("ExtendedFieldURI".equals(tagLocalName)) {
+ propertyTag = getAttributeValue(reader, "PropertyTag");
+ // property name is in PropertyId or PropertyName with DistinguishedPropertySetId
+ if (propertyTag == null) {
+ propertyTag = getAttributeValue(reader, "PropertyId");
+ }
+ if (propertyTag == null) {
+ propertyTag = getAttributeValue(reader, "PropertyName");
+ }
+ } else if ("Value".equals(tagLocalName)) {
+ propertyValue = XMLStreamUtil.getElementText(reader);
+ } else if ("Values".equals(tagLocalName)) {
+ StringBuilder buffer = new StringBuilder();
+ while (reader.hasNext() && !(XMLStreamUtil.isEndTag(reader, "Values"))) {
+ reader.next();
+ if (XMLStreamUtil.isStartTag(reader)) {
+
+ if (buffer.length() > 0) {
+ buffer.append(',');
+ }
+ String singleValue = XMLStreamUtil.getElementText(reader);
+ if (singleValue != null) {
+ buffer.append(singleValue);
+ }
+ }
+ }
+ propertyValue = buffer.toString();
+ }
+ }
+ }
+ if ((propertyTag != null) && (propertyValue != null)) {
+ item.put(propertyTag, propertyValue);
+ }
+ }
+
+ protected String getTagContent(XMLStreamReader reader) throws XMLStreamException {
+ String tagLocalName = reader.getLocalName();
+ while (reader.hasNext() && !(reader.getEventType() == XMLStreamConstants.END_ELEMENT)) {
+ reader.next();
+ if (reader.getEventType() == XMLStreamConstants.CHARACTERS) {
+ return reader.getText();
+ }
+ }
+ // empty tag
+ if (reader.hasNext()) {
+ return null;
+ } else {
+ throw new XMLStreamException("End element for " + tagLocalName + " not found");
+ }
+ }
+
+ protected String getAttributeValue(XMLStreamReader reader, String attributeName) {
+ for (int i = 0; i < reader.getAttributeCount(); i++) {
+ if (attributeName.equals(reader.getAttributeLocalName(i))) {
+ return reader.getAttributeValue(i);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void processResponseBody(HttpState httpState, HttpConnection httpConnection) {
+ Header contentTypeHeader = getResponseHeader("Content-Type");
+ if (contentTypeHeader != null && "text/xml; charset=utf-8".equals(contentTypeHeader.getValue())) {
+ try {
+ if (DavGatewayHttpClientFacade.isGzipEncoded(this)) {
+ processResponseStream(new GZIPInputStream(getResponseBodyAsStream()));
+ } else {
+ processResponseStream(getResponseBodyAsStream());
+ }
+ } catch (IOException e) {
+ LOGGER.error("Error while parsing soap response: " + e, e);
+ }
+ }
+ }
+
+ protected void processResponseStream(InputStream inputStream) {
+ responseItems = new ArrayList<Item>();
+ XMLStreamReader reader = null;
+ try {
+ inputStream = new FilterInputStream(inputStream) {
+ int totalCount;
+ int lastLogCount;
+
+ @Override
+ public int read(byte[] buffer, int offset, int length) throws IOException {
+ int count = super.read(buffer, offset, length);
+ totalCount += count;
+ if (totalCount - lastLogCount > 1024 * 128) {
+ DavGatewayTray.debug(new BundleMessage("LOG_DOWNLOAD_PROGRESS", String.valueOf(totalCount / 1024), EWSMethod.this.getURI()));
+ DavGatewayTray.switchIcon();
+ lastLogCount = totalCount;
+ }
+ return count;
+ }
+ };
+ reader = XMLStreamUtil.createXMLStreamReader(inputStream);
+ while (reader.hasNext()) {
+ reader.next();
+ handleErrors(reader);
+ if (serverVersion == null && XMLStreamUtil.isStartTag(reader, "ServerVersionInfo")) {
+ String majorVersion = getAttributeValue(reader, "MajorVersion");
+ if ("14".equals(majorVersion)) {
+ String minorVersion = getAttributeValue(reader, "MinorVersion");
+ if ("0".equals(minorVersion)) {
+ serverVersion = "Exchange2010";
+ } else {
+ serverVersion = "Exchange2010_SP1";
+ }
+ } else {
+ serverVersion = "Exchange2007_SP1";
+ }
+ } else if (XMLStreamUtil.isStartTag(reader, "RootFolder")) {
+ includesLastItemInRange = "true".equals(reader.getAttributeValue(null, "IncludesLastItemInRange"));
+ } else if (XMLStreamUtil.isStartTag(reader, responseCollectionName)) {
+ handleItems(reader);
+ } else {
+ handleCustom(reader);
+ }
+ }
+ } catch (XMLStreamException e) {
+ LOGGER.error("Error while parsing soap response: " + e, e);
+ if (reader != null) {
+ try {
+ LOGGER.error("Current text: " + reader.getText());
+ } catch (IllegalStateException ise) {
+ LOGGER.error(e+" "+e.getMessage());
+ }
+ }
+ }
+ if (errorDetail != null) {
+ LOGGER.debug(errorDetail);
+ }
+ }
+
+ @SuppressWarnings({"NoopMethodInAbstractClass"})
+ protected void handleCustom(XMLStreamReader reader) throws XMLStreamException {
+ // override to handle custom content
+ }
+
+ private void handleItems(XMLStreamReader reader) throws XMLStreamException {
+ while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, responseCollectionName)) {
+ reader.next();
+ if (XMLStreamUtil.isStartTag(reader)) {
+ responseItems.add(handleItem(reader));
+ }
+ }
+
+ }
+
+}
diff --git a/src/java/davmail/exchange/ews/ElementOption.java b/src/java/davmail/exchange/ews/ElementOption.java
new file mode 100644
index 0000000..c1bbf75
--- /dev/null
+++ b/src/java/davmail/exchange/ews/ElementOption.java
@@ -0,0 +1,53 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+import davmail.util.StringUtil;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * Generic element option.
+ */
+public class ElementOption extends Option {
+ /**
+ * Create element option.
+ *
+ * @param name element tag name
+ * @param value element value
+ */
+ protected ElementOption(String name, String value) {
+ super(name, value);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public void write(Writer writer) throws IOException {
+ writer.write('<');
+ writer.write(name);
+ writer.write('>');
+ writer.write(StringUtil.xmlEncode(value));
+ writer.write("</");
+ writer.write(name);
+ writer.write('>');
+ }
+}
diff --git a/src/java/davmail/exchange/ews/EwsExchangeSession.java b/src/java/davmail/exchange/ews/EwsExchangeSession.java
new file mode 100644
index 0000000..7e9293a
--- /dev/null
+++ b/src/java/davmail/exchange/ews/EwsExchangeSession.java
@@ -0,0 +1,2409 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+import davmail.Settings;
+import davmail.exception.DavMailAuthenticationException;
+import davmail.exception.DavMailException;
+import davmail.exception.HttpNotFoundException;
+import davmail.exchange.ExchangeSession;
+import davmail.exchange.VCalendar;
+import davmail.exchange.VObject;
+import davmail.exchange.VProperty;
+import davmail.http.DavGatewayHttpClientFacade;
+import davmail.util.IOUtil;
+import davmail.util.StringUtil;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.httpclient.*;
+import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.apache.commons.httpclient.methods.PostMethod;
+import org.apache.commons.httpclient.params.HttpClientParams;
+
+import javax.mail.MessagingException;
+import javax.mail.Session;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeMessage;
+import javax.mail.util.SharedByteArrayInputStream;
+import java.io.*;
+import java.net.HttpURLConnection;
+import java.net.SocketException;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+/**
+ * EWS Exchange adapter.
+ * Compatible with Exchange 2007 and hopefully 2010.
+ */
+public class EwsExchangeSession extends ExchangeSession {
+
+ protected static final int PAGE_SIZE = 100;
+
+ protected static final String ARCHIVE_ROOT = "/archive/";
+
+ /**
+ * Message types.
+ *
+ * @see <a href="http://msdn.microsoft.com/en-us/library/aa565652%28v=EXCHG.140%29.aspx">http://msdn.microsoft.com/en-us/library/aa565652%28v=EXCHG.140%29.aspx</a>
+ */
+ protected static final Set<String> MESSAGE_TYPES = new HashSet<String>();
+
+ static {
+ MESSAGE_TYPES.add("Message");
+ MESSAGE_TYPES.add("CalendarItem");
+
+ MESSAGE_TYPES.add("MeetingMessage");
+ MESSAGE_TYPES.add("MeetingRequest");
+ MESSAGE_TYPES.add("MeetingResponse");
+ MESSAGE_TYPES.add("MeetingCancellation");
+
+ // exclude types from IMAP
+ //MESSAGE_TYPES.add("Item");
+ //MESSAGE_TYPES.add("PostItem");
+ //MESSAGE_TYPES.add("Contact");
+ //MESSAGE_TYPES.add("DistributionList");
+ //MESSAGE_TYPES.add("Task");
+
+ //ReplyToItem
+ //ForwardItem
+ //ReplyAllToItem
+ //AcceptItem
+ //TentativelyAcceptItem
+ //DeclineItem
+ //CancelCalendarItem
+ //RemoveItem
+ //PostReplyItem
+ //SuppressReadReceipt
+ //AcceptSharingInvitation
+ }
+
+ static final Map<String, String> vTodoToTaskStatusMap = new HashMap<String, String>();
+ static final Map<String, String> taskTovTodoStatusMap = new HashMap<String, String>();
+
+ static {
+ //taskTovTodoStatusMap.put("NotStarted", null);
+ taskTovTodoStatusMap.put("InProgress", "IN-PROCESS");
+ taskTovTodoStatusMap.put("Completed", "COMPLETED");
+ taskTovTodoStatusMap.put("WaitingOnOthers", "NEEDS-ACTION");
+ taskTovTodoStatusMap.put("Deferred", "CANCELLED");
+
+ //vTodoToTaskStatusMap.put(null, "NotStarted");
+ vTodoToTaskStatusMap.put("IN-PROCESS", "InProgress");
+ vTodoToTaskStatusMap.put("COMPLETED", "Completed");
+ vTodoToTaskStatusMap.put("NEEDS-ACTION", "WaitingOnOthers");
+ vTodoToTaskStatusMap.put("CANCELLED", "Deferred");
+ }
+
+ protected Map<String, String> folderIdMap;
+
+ protected class Folder extends ExchangeSession.Folder {
+ public FolderId folderId;
+ }
+
+ protected static class FolderPath {
+ protected final String parentPath;
+ protected final String folderName;
+
+ protected FolderPath(String folderPath) {
+ int slashIndex = folderPath.lastIndexOf('/');
+ if (slashIndex < 0) {
+ parentPath = "";
+ folderName = folderPath;
+ } else {
+ parentPath = folderPath.substring(0, slashIndex);
+ folderName = folderPath.substring(slashIndex + 1);
+ }
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public EwsExchangeSession(String url, String userName, String password) throws IOException {
+ super(url, userName, password);
+ }
+
+ @Override
+ protected HttpMethod formLogin(HttpClient httpClient, HttpMethod initmethod, String userName, String password) throws IOException {
+ LOGGER.debug("Form based authentication detected");
+
+ HttpMethod logonMethod = buildLogonMethod(httpClient, initmethod);
+ if (logonMethod == null) {
+ LOGGER.debug("Authentication form not found at " + initmethod.getURI() + ", will try direct EWS access");
+ } else {
+ logonMethod = postLogonMethod(httpClient, logonMethod, userName, password);
+ }
+
+ return logonMethod;
+ }
+
+
+ /**
+ * Check endpoint url.
+ *
+ * @param endPointUrl endpoint url
+ * @throws IOException on error
+ */
+ protected void checkEndPointUrl(String endPointUrl) throws IOException {
+ HttpMethod getMethod = new GetMethod(endPointUrl);
+ getMethod.setFollowRedirects(false);
+ try {
+ int status = DavGatewayHttpClientFacade.executeNoRedirect(httpClient, getMethod);
+ if (status == HttpStatus.SC_UNAUTHORIZED) {
+ throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
+ } else if (status != HttpStatus.SC_MOVED_TEMPORARILY) {
+ throw DavGatewayHttpClientFacade.buildHttpException(getMethod);
+ }
+ // check Location
+ Header locationHeader = getMethod.getResponseHeader("Location");
+ if (locationHeader == null || !"/ews/services.wsdl".equalsIgnoreCase(locationHeader.getValue())) {
+ throw new IOException("Ews endpoint not available at " + getMethod.getURI().toString());
+ }
+ } finally {
+ getMethod.releaseConnection();
+ }
+ }
+
+ @Override
+ protected void buildSessionInfo(HttpMethod method) throws DavMailException {
+ // no need to check logon method body
+ if (method != null) {
+ method.releaseConnection();
+ // need to retrieve email and alias
+ getEmailAndAliasFromOptions();
+ }
+
+ if (email == null || alias == null) {
+ // OWA authentication failed, get email address from login
+ if (userName.indexOf('@') >= 0) {
+ // userName is email address
+ email = userName;
+ alias = userName.substring(0, userName.indexOf('@'));
+ } else {
+ // userName or domain\\username, rebuild email address
+ alias = getAliasFromLogin();
+ email = getAliasFromLogin() + getEmailSuffixFromHostname();
+ }
+ }
+
+ currentMailboxPath = "/users/" + email.toLowerCase();
+
+ // check EWS access
+ try {
+ checkEndPointUrl("/ews/exchange.asmx");
+ // workaround for Exchange bug: send fake request
+ internalGetFolder("");
+ } catch (IOException e) {
+ // first failover: retry with NTLM
+ DavGatewayHttpClientFacade.addNTLM(httpClient);
+ try {
+ checkEndPointUrl("/ews/exchange.asmx");
+ // workaround for Exchange bug: send fake request
+ internalGetFolder("");
+ } catch (IOException e2) {
+ LOGGER.debug(e2.getMessage());
+ try {
+ // failover, try to retrieve EWS url from autodiscover
+ checkEndPointUrl(getEwsUrlFromAutoDiscover());
+ // workaround for Exchange bug: send fake request
+ internalGetFolder("");
+ } catch (IOException e3) {
+ // autodiscover failed and initial exception was authentication failure => throw original exception
+ if (e instanceof DavMailAuthenticationException) {
+ throw (DavMailAuthenticationException) e;
+ }
+ LOGGER.error(e2.getMessage());
+ throw new DavMailAuthenticationException("EXCEPTION_EWS_NOT_AVAILABLE");
+ }
+ }
+ }
+
+ // enable preemptive authentication on non NTLM endpoints
+ if (!DavGatewayHttpClientFacade.hasNTLM(httpClient)) {
+ httpClient.getParams().setParameter(HttpClientParams.PREEMPTIVE_AUTHENTICATION, true);
+ }
+
+ try {
+ folderIdMap = new HashMap<String, String>();
+ // load actual well known folder ids
+ folderIdMap.put(internalGetFolder(INBOX).folderId.value, INBOX);
+ folderIdMap.put(internalGetFolder(CALENDAR).folderId.value, CALENDAR);
+ folderIdMap.put(internalGetFolder(CONTACTS).folderId.value, CONTACTS);
+ folderIdMap.put(internalGetFolder(SENT).folderId.value, SENT);
+ folderIdMap.put(internalGetFolder(DRAFTS).folderId.value, DRAFTS);
+ folderIdMap.put(internalGetFolder(TRASH).folderId.value, TRASH);
+ folderIdMap.put(internalGetFolder(JUNK).folderId.value, JUNK);
+ folderIdMap.put(internalGetFolder(UNSENT).folderId.value, UNSENT);
+ } catch (IOException e) {
+ LOGGER.error(e.getMessage(), e);
+ throw new DavMailAuthenticationException("EXCEPTION_EWS_NOT_AVAILABLE");
+ }
+ LOGGER.debug("Current user email is " + email + ", alias is " + alias + " on " + serverVersion);
+ }
+
+ protected static class AutoDiscoverMethod extends PostMethod {
+ AutoDiscoverMethod(String autodiscoverHost, String userEmail) {
+ super("https://" + autodiscoverHost + "/autodiscover/autodiscover.xml");
+ setAutoDiscoverRequestEntity(userEmail);
+ }
+
+ AutoDiscoverMethod(String userEmail) {
+ super("/autodiscover/autodiscover.xml");
+ setAutoDiscoverRequestEntity(userEmail);
+ }
+
+ void setAutoDiscoverRequestEntity(String userEmail) {
+ String body = "<Autodiscover xmlns=\"http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006\">" +
+ "<Request>" +
+ "<EMailAddress>" + userEmail + "</EMailAddress>" +
+ "<AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>" +
+ "</Request>" +
+ "</Autodiscover>";
+ setRequestEntity(new ByteArrayRequestEntity(body.getBytes(), "text/xml"));
+ }
+
+ String ewsUrl;
+
+ @Override
+ protected void processResponseBody(HttpState httpState, HttpConnection httpConnection) {
+ Header contentTypeHeader = getResponseHeader("Content-Type");
+ if (contentTypeHeader != null &&
+ ("text/xml; charset=utf-8".equals(contentTypeHeader.getValue())
+ || "text/html; charset=utf-8".equals(contentTypeHeader.getValue())
+ )) {
+ BufferedReader autodiscoverReader = null;
+ try {
+ autodiscoverReader = new BufferedReader(new InputStreamReader(getResponseBodyAsStream()));
+ String line;
+ // find ews url
+ while ((line = autodiscoverReader.readLine()) != null
+ && (line.indexOf("<EwsUrl>") == -1)
+ && (line.indexOf("</EwsUrl>") == -1)) {
+ }
+ if (line != null) {
+ ewsUrl = line.substring(line.indexOf("<EwsUrl>") + 8, line.indexOf("</EwsUrl>"));
+ }
+ } catch (IOException e) {
+ LOGGER.debug(e);
+ } finally {
+ if (autodiscoverReader != null) {
+ try {
+ autodiscoverReader.close();
+ } catch (IOException e) {
+ LOGGER.debug(e);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ protected String getEwsUrlFromAutoDiscover() throws DavMailAuthenticationException {
+ String ewsUrl;
+ try {
+ ewsUrl = getEwsUrlFromAutoDiscover(null);
+ } catch (IOException e) {
+ try {
+ ewsUrl = getEwsUrlFromAutoDiscover("autodiscover." + email.substring(email.indexOf('@') + 1));
+ } catch (IOException e2) {
+ LOGGER.error(e2.getMessage());
+ throw new DavMailAuthenticationException("EXCEPTION_EWS_NOT_AVAILABLE");
+ }
+ }
+ return ewsUrl;
+ }
+
+ protected String getEwsUrlFromAutoDiscover(String autodiscoverHostname) throws IOException {
+ String ewsUrl;
+ AutoDiscoverMethod autoDiscoverMethod;
+ if (autodiscoverHostname != null) {
+ autoDiscoverMethod = new AutoDiscoverMethod(autodiscoverHostname, email);
+ } else {
+ autoDiscoverMethod = new AutoDiscoverMethod(email);
+ }
+ try {
+ int status = DavGatewayHttpClientFacade.executeNoRedirect(httpClient, autoDiscoverMethod);
+ if (status != HttpStatus.SC_OK) {
+ throw DavGatewayHttpClientFacade.buildHttpException(autoDiscoverMethod);
+ }
+ ewsUrl = autoDiscoverMethod.ewsUrl;
+
+ // update host name
+ DavGatewayHttpClientFacade.setClientHost(httpClient, ewsUrl);
+
+ if (ewsUrl == null) {
+ throw new IOException("Ews url not found");
+ }
+ } finally {
+ autoDiscoverMethod.releaseConnection();
+ }
+ return ewsUrl;
+ }
+
+ class Message extends ExchangeSession.Message {
+ // message item id
+ ItemId itemId;
+
+ @Override
+ public String getPermanentId() {
+ return itemId.id;
+ }
+
+ @Override
+ protected InputStream getMimeHeaders() {
+ InputStream result = null;
+ try {
+ GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, itemId, false);
+ getItemMethod.addAdditionalProperty(Field.get("messageheaders"));
+ getItemMethod.addAdditionalProperty(Field.get("from"));
+ executeMethod(getItemMethod);
+ EWSMethod.Item item = getItemMethod.getResponseItem();
+
+ String messageHeaders = item.get(Field.get("messageheaders").getResponseName());
+ if (messageHeaders != null) {
+ // workaround for messages in Sent folder
+ if (messageHeaders.indexOf("From:") < 0) {
+ String from = item.get(Field.get("from").getResponseName());
+ messageHeaders = "From: "+from+"\n"+messageHeaders;
+ }
+
+ result = new ByteArrayInputStream(messageHeaders.getBytes("UTF-8"));
+ }
+ } catch (Exception e) {
+ LOGGER.warn(e.getMessage());
+ }
+
+ return result;
+ }
+ }
+
+ /**
+ * Message create/update properties
+ *
+ * @param properties flag values map
+ * @return field values
+ */
+ protected List<FieldUpdate> buildProperties(Map<String, String> properties) {
+ ArrayList<FieldUpdate> list = new ArrayList<FieldUpdate>();
+ for (Map.Entry<String, String> entry : properties.entrySet()) {
+ if ("read".equals(entry.getKey())) {
+ list.add(Field.createFieldUpdate("read", Boolean.toString("1".equals(entry.getValue()))));
+ } else if ("junk".equals(entry.getKey())) {
+ list.add(Field.createFieldUpdate("junk", entry.getValue()));
+ } else if ("flagged".equals(entry.getKey())) {
+ list.add(Field.createFieldUpdate("flagStatus", entry.getValue()));
+ } else if ("answered".equals(entry.getKey())) {
+ list.add(Field.createFieldUpdate("lastVerbExecuted", entry.getValue()));
+ if ("102".equals(entry.getValue())) {
+ list.add(Field.createFieldUpdate("iconIndex", "261"));
+ }
+ } else if ("forwarded".equals(entry.getKey())) {
+ list.add(Field.createFieldUpdate("lastVerbExecuted", entry.getValue()));
+ if ("104".equals(entry.getValue())) {
+ list.add(Field.createFieldUpdate("iconIndex", "262"));
+ }
+ } else if ("draft".equals(entry.getKey())) {
+ // note: draft is readonly after create
+ list.add(Field.createFieldUpdate("messageFlags", entry.getValue()));
+ } else if ("deleted".equals(entry.getKey())) {
+ list.add(Field.createFieldUpdate("deleted", entry.getValue()));
+ } else if ("datereceived".equals(entry.getKey())) {
+ list.add(Field.createFieldUpdate("datereceived", entry.getValue()));
+ }
+ }
+ return list;
+ }
+
+ @Override
+ public void createMessage(String folderPath, String messageName, HashMap<String, String> properties, MimeMessage mimeMessage) throws IOException {
+ EWSMethod.Item item = new EWSMethod.Item();
+ item.type = "Message";
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try {
+ mimeMessage.writeTo(baos);
+ } catch (MessagingException e) {
+ throw new IOException(e.getMessage());
+ }
+ baos.close();
+ item.mimeContent = Base64.encodeBase64(baos.toByteArray());
+
+ List<FieldUpdate> fieldUpdates = buildProperties(properties);
+ if (!properties.containsKey("draft")) {
+ // need to force draft flag to false
+ if (properties.containsKey("read")) {
+ fieldUpdates.add(Field.createFieldUpdate("messageFlags", "1"));
+ } else {
+ fieldUpdates.add(Field.createFieldUpdate("messageFlags", "0"));
+ }
+ }
+ fieldUpdates.add(Field.createFieldUpdate("urlcompname", messageName));
+ item.setFieldUpdates(fieldUpdates);
+ CreateItemMethod createItemMethod = new CreateItemMethod(MessageDisposition.SaveOnly, getFolderId(folderPath), item);
+ executeMethod(createItemMethod);
+ }
+
+ @Override
+ public void updateMessage(ExchangeSession.Message message, Map<String, String> properties) throws IOException {
+ if (properties.containsKey("read") && "urn:content-classes:appointment".equals(message.contentClass)) {
+ properties.remove("read");
+ }
+ if (!properties.isEmpty()) {
+ UpdateItemMethod updateItemMethod = new UpdateItemMethod(MessageDisposition.SaveOnly,
+ ConflictResolution.AlwaysOverwrite,
+ SendMeetingInvitationsOrCancellations.SendToNone,
+ ((EwsExchangeSession.Message) message).itemId, buildProperties(properties));
+ executeMethod(updateItemMethod);
+ }
+ }
+
+ @Override
+ public void deleteMessage(ExchangeSession.Message message) throws IOException {
+ LOGGER.debug("Delete " + message.permanentUrl);
+ DeleteItemMethod deleteItemMethod = new DeleteItemMethod(((EwsExchangeSession.Message) message).itemId, DeleteType.HardDelete, SendMeetingCancellations.SendToNone);
+ executeMethod(deleteItemMethod);
+ }
+
+
+ protected void sendMessage(String itemClass, byte[] messageBody) throws IOException {
+ EWSMethod.Item item = new EWSMethod.Item();
+ item.type = "Message";
+ item.mimeContent = Base64.encodeBase64(messageBody);
+ if (itemClass != null) {
+ item.put("ItemClass", itemClass);
+ }
+
+ MessageDisposition messageDisposition;
+ if (Settings.getBooleanProperty("davmail.smtpSaveInSent", true)) {
+ messageDisposition = MessageDisposition.SendAndSaveCopy;
+ } else {
+ messageDisposition = MessageDisposition.SendOnly;
+ }
+
+ CreateItemMethod createItemMethod = new CreateItemMethod(messageDisposition, getFolderId(SENT), item);
+ executeMethod(createItemMethod);
+ }
+
+ @Override
+ public void sendMessage(MimeMessage mimeMessage) throws IOException, MessagingException {
+ String itemClass = null;
+ if (mimeMessage.getContentType().startsWith("multipart/report")) {
+ itemClass = "REPORT.IPM.Note.IPNRN";
+ }
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try {
+ mimeMessage.writeTo(baos);
+ } catch (MessagingException e) {
+ throw new IOException(e.getMessage());
+ }
+ sendMessage(itemClass, baos.toByteArray());
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ protected byte[] getContent(ExchangeSession.Message message) throws IOException {
+ return getContent(((EwsExchangeSession.Message) message).itemId);
+ }
+
+ /**
+ * Get item content.
+ *
+ * @param itemId EWS item id
+ * @return item content as byte array
+ * @throws IOException on error
+ */
+ protected byte[] getContent(ItemId itemId) throws IOException {
+ GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, itemId, true);
+ byte[] mimeContent = null;
+ try {
+ executeMethod(getItemMethod);
+ mimeContent = getItemMethod.getMimeContent();
+ } catch (EWSException e) {
+ LOGGER.warn("GetItem with MimeContent failed: " + e.getMessage());
+ }
+ if (mimeContent == null) {
+ LOGGER.warn("MimeContent not available, trying to rebuild from properties");
+ try {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, itemId, false);
+ getItemMethod.addAdditionalProperty(Field.get("contentclass"));
+ getItemMethod.addAdditionalProperty(Field.get("message-id"));
+ getItemMethod.addAdditionalProperty(Field.get("from"));
+ getItemMethod.addAdditionalProperty(Field.get("to"));
+ getItemMethod.addAdditionalProperty(Field.get("cc"));
+ getItemMethod.addAdditionalProperty(Field.get("subject"));
+ getItemMethod.addAdditionalProperty(Field.get("date"));
+ getItemMethod.addAdditionalProperty(Field.get("body"));
+ executeMethod(getItemMethod);
+ EWSMethod.Item item = getItemMethod.getResponseItem();
+
+ MimeMessage mimeMessage = new MimeMessage((Session) null);
+ mimeMessage.addHeader("Content-class", item.get(Field.get("contentclass").getResponseName()));
+ mimeMessage.setSentDate(parseDateFromExchange(item.get(Field.get("date").getResponseName())));
+ mimeMessage.addHeader("From", item.get(Field.get("from").getResponseName()));
+ mimeMessage.addHeader("To", item.get(Field.get("to").getResponseName()));
+ mimeMessage.addHeader("Cc", item.get(Field.get("cc").getResponseName()));
+ mimeMessage.setSubject(item.get(Field.get("subject").getResponseName()));
+ String propertyValue = item.get(Field.get("body").getResponseName());
+ if (propertyValue == null) {
+ propertyValue = "";
+ }
+ mimeMessage.setContent(propertyValue, "text/html; charset=UTF-8");
+
+ mimeMessage.writeTo(baos);
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("Rebuilt message content: " + new String(baos.toByteArray()));
+ }
+ mimeContent = baos.toByteArray();
+
+ } catch (IOException e2) {
+ LOGGER.warn(e2);
+ } catch (MessagingException e2) {
+ LOGGER.warn(e2);
+ }
+ if (mimeContent == null) {
+ throw new IOException("GetItem returned null MimeContent");
+ }
+ }
+ return mimeContent;
+ }
+
+ protected Message buildMessage(EWSMethod.Item response) throws DavMailException {
+ Message message = new Message();
+
+ // get item id
+ message.itemId = new ItemId(response);
+
+ message.permanentUrl = response.get(Field.get("permanenturl").getResponseName());
+
+ message.size = response.getInt(Field.get("messageSize").getResponseName());
+ message.uid = response.get(Field.get("uid").getResponseName());
+ message.contentClass = response.get(Field.get("contentclass").getResponseName());
+ message.imapUid = response.getLong(Field.get("imapUid").getResponseName());
+ message.read = response.getBoolean(Field.get("read").getResponseName());
+ message.junk = response.getBoolean(Field.get("junk").getResponseName());
+ message.flagged = "2".equals(response.get(Field.get("flagStatus").getResponseName()));
+ message.draft = (response.getInt(Field.get("messageFlags").getResponseName()) & 8) != 0;
+ String lastVerbExecuted = response.get(Field.get("lastVerbExecuted").getResponseName());
+ message.answered = "102".equals(lastVerbExecuted) || "103".equals(lastVerbExecuted);
+ message.forwarded = "104".equals(lastVerbExecuted);
+ message.date = convertDateFromExchange(response.get(Field.get("date").getResponseName()));
+ message.deleted = "1".equals(response.get(Field.get("deleted").getResponseName()));
+
+ String lastmodified = convertDateFromExchange(response.get(Field.get("lastmodified").getResponseName()));
+ message.recent = !message.read && lastmodified != null && lastmodified.equals(message.date);
+
+ if (LOGGER.isDebugEnabled()) {
+ StringBuilder buffer = new StringBuilder();
+ buffer.append("Message");
+ if (message.imapUid != 0) {
+ buffer.append(" IMAP uid: ").append(message.imapUid);
+ }
+ if (message.uid != null) {
+ buffer.append(" uid: ").append(message.uid);
+ }
+ buffer.append(" ItemId: ").append(message.itemId.id);
+ buffer.append(" ChangeKey: ").append(message.itemId.changeKey);
+ LOGGER.debug(buffer.toString());
+ }
+ return message;
+ }
+
+ @Override
+ public MessageList searchMessages(String folderPath, Set<String> attributes, Condition condition) throws IOException {
+ MessageList messages = new MessageList();
+ List<EWSMethod.Item> responses = searchItems(folderPath, attributes, condition, FolderQueryTraversal.SHALLOW, 0);
+
+ for (EWSMethod.Item response : responses) {
+ if (MESSAGE_TYPES.contains(response.type)) {
+ Message message = buildMessage(response);
+ message.messageList = messages;
+ messages.add(message);
+ }
+ }
+ Collections.sort(messages);
+ return messages;
+ }
+
+ protected List<EWSMethod.Item> searchItems(String folderPath, Set<String> attributes, Condition condition, FolderQueryTraversal folderQueryTraversal, int maxCount) throws IOException {
+ int offset = 0;
+ List<EWSMethod.Item> results = new ArrayList<EWSMethod.Item>();
+ FindItemMethod findItemMethod;
+ do {
+ int fetchCount = PAGE_SIZE;
+ if (maxCount > 0) {
+ fetchCount = Math.min(PAGE_SIZE, maxCount - offset);
+ }
+ findItemMethod = new FindItemMethod(folderQueryTraversal, BaseShape.ID_ONLY, getFolderId(folderPath), offset, fetchCount);
+ for (String attribute : attributes) {
+ findItemMethod.addAdditionalProperty(Field.get(attribute));
+ }
+ if (condition != null && !condition.isEmpty()) {
+ findItemMethod.setSearchExpression((SearchExpression) condition);
+ }
+ executeMethod(findItemMethod);
+ results.addAll(findItemMethod.getResponseItems());
+ offset = results.size();
+ } while (!(findItemMethod.includesLastItemInRange || (maxCount > 0 && offset == maxCount)));
+ return results;
+ }
+
+ protected static class MultiCondition extends ExchangeSession.MultiCondition implements SearchExpression {
+ protected MultiCondition(Operator operator, Condition... condition) {
+ super(operator, condition);
+ }
+
+ public void appendTo(StringBuilder buffer) {
+ int actualConditionCount = 0;
+ for (Condition condition : conditions) {
+ if (!condition.isEmpty()) {
+ actualConditionCount++;
+ }
+ }
+ if (actualConditionCount > 0) {
+ if (actualConditionCount > 1) {
+ buffer.append("<t:").append(operator.toString()).append('>');
+ }
+
+ for (Condition condition : conditions) {
+ condition.appendTo(buffer);
+ }
+
+ if (actualConditionCount > 1) {
+ buffer.append("</t:").append(operator.toString()).append('>');
+ }
+ }
+ }
+ }
+
+ protected static class NotCondition extends ExchangeSession.NotCondition implements SearchExpression {
+ protected NotCondition(Condition condition) {
+ super(condition);
+ }
+
+ public void appendTo(StringBuilder buffer) {
+ buffer.append("<t:Not>");
+ condition.appendTo(buffer);
+ buffer.append("</t:Not>");
+ }
+ }
+
+
+ protected static class AttributeCondition extends ExchangeSession.AttributeCondition implements SearchExpression {
+ protected ContainmentMode containmentMode;
+ protected ContainmentComparison containmentComparison;
+
+ protected AttributeCondition(String attributeName, Operator operator, String value) {
+ super(attributeName, operator, value);
+ }
+
+ protected AttributeCondition(String attributeName, Operator operator, String value,
+ ContainmentMode containmentMode, ContainmentComparison containmentComparison) {
+ super(attributeName, operator, value);
+ this.containmentMode = containmentMode;
+ this.containmentComparison = containmentComparison;
+ }
+
+ protected FieldURI getFieldURI() {
+ FieldURI fieldURI = Field.get(attributeName);
+ if (fieldURI == null) {
+ throw new IllegalArgumentException("Unknown field: " + attributeName);
+ }
+ return fieldURI;
+ }
+
+ protected Operator getOperator() {
+ return operator;
+ }
+
+ public void appendTo(StringBuilder buffer) {
+ buffer.append("<t:").append(operator.toString());
+ if (containmentMode != null) {
+ containmentMode.appendTo(buffer);
+ }
+ if (containmentComparison != null) {
+ containmentComparison.appendTo(buffer);
+ }
+ buffer.append('>');
+ FieldURI fieldURI = getFieldURI();
+ fieldURI.appendTo(buffer);
+
+ if (operator != Operator.Contains) {
+ buffer.append("<t:FieldURIOrConstant>");
+ }
+ buffer.append("<t:Constant Value=\"");
+ // encode urlcompname
+ if (fieldURI instanceof ExtendedFieldURI && "0x10f3".equals(((ExtendedFieldURI) fieldURI).propertyTag)) {
+ buffer.append(StringUtil.xmlEncodeAttribute(StringUtil.encodeUrlcompname(value)));
+ } else if (fieldURI instanceof ExtendedFieldURI
+ && ((ExtendedFieldURI) fieldURI).propertyType == ExtendedFieldURI.PropertyType.Integer) {
+ // check value
+ try {
+ Integer.parseInt(value);
+ buffer.append(value);
+ } catch (NumberFormatException e) {
+ // invalid value, replace with 0
+ buffer.append('0');
+ }
+ } else {
+ buffer.append(StringUtil.xmlEncodeAttribute(value));
+ }
+ buffer.append("\"/>");
+ if (operator != Operator.Contains) {
+ buffer.append("</t:FieldURIOrConstant>");
+ }
+
+ buffer.append("</t:").append(operator.toString()).append('>');
+ }
+
+ public boolean isMatch(ExchangeSession.Contact contact) {
+ String lowerCaseValue = value.toLowerCase();
+
+ String actualValue = contact.get(attributeName);
+ if (actualValue == null) {
+ return false;
+ }
+ actualValue = actualValue.toLowerCase();
+ if (operator == Operator.IsEqualTo) {
+ return lowerCaseValue.equals(actualValue);
+ } else {
+ return operator == Operator.Contains && ((containmentMode.equals(ContainmentMode.Substring) && actualValue.contains(lowerCaseValue)) ||
+ (containmentMode.equals(ContainmentMode.Prefixed) && actualValue.startsWith(lowerCaseValue)));
+ }
+ }
+
+ }
+
+ protected static class HeaderCondition extends AttributeCondition {
+
+ protected HeaderCondition(String attributeName, Operator operator, String value) {
+ super(attributeName, operator, value);
+ }
+
+ @Override
+ protected FieldURI getFieldURI() {
+ return new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.InternetHeaders, attributeName);
+ }
+
+ }
+
+ protected static class IsNullCondition implements ExchangeSession.Condition, SearchExpression {
+ protected final String attributeName;
+
+ protected IsNullCondition(String attributeName) {
+ this.attributeName = attributeName;
+ }
+
+ public void appendTo(StringBuilder buffer) {
+ buffer.append("<t:Not><t:Exists>");
+ Field.get(attributeName).appendTo(buffer);
+ buffer.append("</t:Exists></t:Not>");
+ }
+
+ public boolean isEmpty() {
+ return false;
+ }
+
+ public boolean isMatch(ExchangeSession.Contact contact) {
+ String actualValue = contact.get(attributeName);
+ return actualValue == null;
+ }
+
+ }
+
+ @Override
+ public ExchangeSession.MultiCondition and(Condition... condition) {
+ return new MultiCondition(Operator.And, condition);
+ }
+
+ @Override
+ public ExchangeSession.MultiCondition or(Condition... condition) {
+ return new MultiCondition(Operator.Or, condition);
+ }
+
+ @Override
+ public Condition not(Condition condition) {
+ return new NotCondition(condition);
+ }
+
+ @Override
+ public Condition isEqualTo(String attributeName, String value) {
+ return new AttributeCondition(attributeName, Operator.IsEqualTo, value);
+ }
+
+ @Override
+ public Condition isEqualTo(String attributeName, int value) {
+ return new AttributeCondition(attributeName, Operator.IsEqualTo, String.valueOf(value));
+ }
+
+ @Override
+ public Condition headerIsEqualTo(String headerName, String value) {
+ return new HeaderCondition(headerName, Operator.IsEqualTo, value);
+ }
+
+ @Override
+ public Condition gte(String attributeName, String value) {
+ return new AttributeCondition(attributeName, Operator.IsGreaterThanOrEqualTo, value);
+ }
+
+ @Override
+ public Condition lte(String attributeName, String value) {
+ return new AttributeCondition(attributeName, Operator.IsLessThanOrEqualTo, value);
+ }
+
+ @Override
+ public Condition lt(String attributeName, String value) {
+ return new AttributeCondition(attributeName, Operator.IsLessThan, value);
+ }
+
+ @Override
+ public Condition gt(String attributeName, String value) {
+ return new AttributeCondition(attributeName, Operator.IsGreaterThan, value);
+ }
+
+ @Override
+ public Condition contains(String attributeName, String value) {
+ return new AttributeCondition(attributeName, Operator.Contains, value, ContainmentMode.Substring, ContainmentComparison.IgnoreCase);
+ }
+
+ @Override
+ public Condition startsWith(String attributeName, String value) {
+ return new AttributeCondition(attributeName, Operator.Contains, value, ContainmentMode.Prefixed, ContainmentComparison.IgnoreCase);
+ }
+
+ @Override
+ public Condition isNull(String attributeName) {
+ return new IsNullCondition(attributeName);
+ }
+
+ @Override
+ public Condition isTrue(String attributeName) {
+ return new AttributeCondition(attributeName, Operator.IsEqualTo, "true");
+ }
+
+ @Override
+ public Condition isFalse(String attributeName) {
+ return new AttributeCondition(attributeName, Operator.IsEqualTo, "false");
+ }
+
+ protected static final HashSet<FieldURI> FOLDER_PROPERTIES = new HashSet<FieldURI>();
+
+ static {
+ FOLDER_PROPERTIES.add(Field.get("urlcompname"));
+ FOLDER_PROPERTIES.add(Field.get("folderDisplayName"));
+ FOLDER_PROPERTIES.add(Field.get("lastmodified"));
+ FOLDER_PROPERTIES.add(Field.get("folderclass"));
+ FOLDER_PROPERTIES.add(Field.get("ctag"));
+ FOLDER_PROPERTIES.add(Field.get("unread"));
+ FOLDER_PROPERTIES.add(Field.get("hassubs"));
+ FOLDER_PROPERTIES.add(Field.get("uidNext"));
+ FOLDER_PROPERTIES.add(Field.get("highestUid"));
+ }
+
+ protected Folder buildFolder(EWSMethod.Item item) {
+ Folder folder = new Folder();
+ folder.folderId = new FolderId(item);
+ folder.displayName = item.get(Field.get("folderDisplayName").getResponseName());
+ folder.folderClass = item.get(Field.get("folderclass").getResponseName());
+ folder.etag = item.get(Field.get("lastmodified").getResponseName());
+ folder.ctag = item.get(Field.get("ctag").getResponseName());
+ folder.unreadCount = item.getInt(Field.get("unread").getResponseName());
+ folder.hasChildren = item.getBoolean(Field.get("hassubs").getResponseName());
+ // noInferiors not implemented
+ folder.uidNext = item.getInt(Field.get("uidNext").getResponseName());
+ return folder;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public List<ExchangeSession.Folder> getSubFolders(String folderPath, Condition condition, boolean recursive) throws IOException {
+ String baseFolderPath = folderPath;
+ if (baseFolderPath.startsWith("/users/")) {
+ int index = baseFolderPath.indexOf('/', "/users/".length());
+ if (index >= 0) {
+ baseFolderPath = baseFolderPath.substring(index + 1);
+ }
+ }
+ List<ExchangeSession.Folder> folders = new ArrayList<ExchangeSession.Folder>();
+ appendSubFolders(folders, baseFolderPath, getFolderId(folderPath), condition, recursive);
+ return folders;
+ }
+
+ protected void appendSubFolders(List<ExchangeSession.Folder> folders,
+ String parentFolderPath, FolderId parentFolderId,
+ Condition condition, boolean recursive) throws IOException {
+ FindFolderMethod findFolderMethod = new FindFolderMethod(FolderQueryTraversal.SHALLOW,
+ BaseShape.ID_ONLY, parentFolderId, FOLDER_PROPERTIES, (SearchExpression) condition);
+ executeMethod(findFolderMethod);
+ for (EWSMethod.Item item : findFolderMethod.getResponseItems()) {
+ Folder folder = buildFolder(item);
+ if (parentFolderPath.length() > 0) {
+ if (parentFolderPath.endsWith("/")) {
+ folder.folderPath = parentFolderPath + item.get(Field.get("folderDisplayName").getResponseName());
+ } else {
+ folder.folderPath = parentFolderPath + '/' + item.get(Field.get("folderDisplayName").getResponseName());
+ }
+ } else if (folderIdMap.get(folder.folderId.value) != null) {
+ folder.folderPath = folderIdMap.get(folder.folderId.value);
+ } else {
+ folder.folderPath = item.get(Field.get("folderDisplayName").getResponseName());
+ }
+ folders.add(folder);
+ if (recursive && folder.hasChildren) {
+ appendSubFolders(folders, folder.folderPath, folder.folderId, condition, recursive);
+ }
+ }
+ }
+
+ /**
+ * Get folder by path.
+ *
+ * @param folderPath folder path
+ * @return folder object
+ * @throws IOException on error
+ */
+ @Override
+ protected EwsExchangeSession.Folder internalGetFolder(String folderPath) throws IOException {
+ FolderId folderId = getFolderId(folderPath);
+ GetFolderMethod getFolderMethod = new GetFolderMethod(BaseShape.ID_ONLY, folderId, FOLDER_PROPERTIES);
+ executeMethod(getFolderMethod);
+ EWSMethod.Item item = getFolderMethod.getResponseItem();
+ Folder folder;
+ if (item != null) {
+ folder = buildFolder(item);
+ folder.folderPath = folderPath;
+ } else {
+ throw new HttpNotFoundException("Folder " + folderPath + " not found");
+ }
+ return folder;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public int createFolder(String folderPath, String folderClass, Map<String, String> properties) throws IOException {
+ FolderPath path = new FolderPath(folderPath);
+ EWSMethod.Item folder = new EWSMethod.Item();
+ folder.type = "Folder";
+ folder.put("FolderClass", folderClass);
+ folder.put("DisplayName", path.folderName);
+ // TODO: handle properties
+ CreateFolderMethod createFolderMethod = new CreateFolderMethod(getFolderId(path.parentPath), folder);
+ executeMethod(createFolderMethod);
+ return HttpStatus.SC_CREATED;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public int updateFolder(String folderPath, Map<String, String> properties) throws IOException {
+ ArrayList<FieldUpdate> updates = new ArrayList<FieldUpdate>();
+ for (Map.Entry<String, String> entry : properties.entrySet()) {
+ updates.add(new FieldUpdate(Field.get(entry.getKey()), entry.getValue()));
+ }
+ UpdateFolderMethod updateFolderMethod = new UpdateFolderMethod(internalGetFolder(folderPath).folderId, updates);
+
+ executeMethod(updateFolderMethod);
+ return HttpStatus.SC_CREATED;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public void deleteFolder(String folderPath) throws IOException {
+ FolderId folderId = getFolderIdIfExists(folderPath);
+ if (folderId != null) {
+ DeleteFolderMethod deleteFolderMethod = new DeleteFolderMethod(folderId);
+ executeMethod(deleteFolderMethod);
+ } else {
+ LOGGER.debug("Folder " + folderPath + " not found");
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public void moveMessage(ExchangeSession.Message message, String targetFolder) throws IOException {
+ MoveItemMethod moveItemMethod = new MoveItemMethod(((EwsExchangeSession.Message) message).itemId, getFolderId(targetFolder));
+ executeMethod(moveItemMethod);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public void copyMessage(ExchangeSession.Message message, String targetFolder) throws IOException {
+ CopyItemMethod copyItemMethod = new CopyItemMethod(((EwsExchangeSession.Message) message).itemId, getFolderId(targetFolder));
+ executeMethod(copyItemMethod);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public void moveFolder(String folderPath, String targetFolderPath) throws IOException {
+ FolderPath path = new FolderPath(folderPath);
+ FolderPath targetPath = new FolderPath(targetFolderPath);
+ FolderId folderId = getFolderId(folderPath);
+ FolderId toFolderId = getFolderId(targetPath.parentPath);
+ toFolderId.changeKey = null;
+ // move folder
+ if (!path.parentPath.equals(targetPath.parentPath)) {
+ MoveFolderMethod moveFolderMethod = new MoveFolderMethod(folderId, toFolderId);
+ executeMethod(moveFolderMethod);
+ }
+ // rename folder
+ if (!path.folderName.equals(targetPath.folderName)) {
+ ArrayList<FieldUpdate> updates = new ArrayList<FieldUpdate>();
+ updates.add(new FieldUpdate(Field.get("folderDisplayName"), targetPath.folderName));
+ UpdateFolderMethod updateFolderMethod = new UpdateFolderMethod(folderId, updates);
+ executeMethod(updateFolderMethod);
+ }
+ }
+
+ @Override
+ public void moveItem(String sourcePath, String targetPath) throws IOException {
+ FolderPath sourceFolderPath = new FolderPath(sourcePath);
+ Item item = getItem(sourceFolderPath.parentPath, sourceFolderPath.folderName);
+ FolderPath targetFolderPath = new FolderPath(targetPath);
+ FolderId toFolderId = getFolderId(targetFolderPath.parentPath);
+ MoveItemMethod moveItemMethod = new MoveItemMethod(((Event) item).itemId, toFolderId);
+ executeMethod(moveItemMethod);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ protected void moveToTrash(ExchangeSession.Message message) throws IOException {
+ MoveItemMethod moveItemMethod = new MoveItemMethod(((EwsExchangeSession.Message) message).itemId, getFolderId(TRASH));
+ executeMethod(moveItemMethod);
+ }
+
+ protected class Contact extends ExchangeSession.Contact {
+ // item id
+ ItemId itemId;
+
+ protected Contact(EWSMethod.Item response) throws DavMailException {
+ itemId = new ItemId(response);
+
+ permanentUrl = response.get(Field.get("permanenturl").getResponseName());
+ etag = response.get(Field.get("etag").getResponseName());
+ displayName = response.get(Field.get("displayname").getResponseName());
+ itemName = StringUtil.decodeUrlcompname(response.get(Field.get("urlcompname").getResponseName()));
+ // workaround for missing urlcompname in Exchange 2010
+ if (itemName == null) {
+ itemName = StringUtil.base64ToUrl(itemId.id) + ".EML";
+ }
+ for (String attributeName : CONTACT_ATTRIBUTES) {
+ String value = response.get(Field.get(attributeName).getResponseName());
+ if (value != null && value.length() > 0) {
+ if ("bday".equals(attributeName) || "anniversary".equals(attributeName) || "lastmodified".equals(attributeName) || "datereceived".equals(attributeName)) {
+ value = convertDateFromExchange(value);
+ }
+ put(attributeName, value);
+ }
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected Contact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) {
+ super(folderPath, itemName, properties, etag, noneMatch);
+ }
+
+ /**
+ * Empty constructor for GalFind
+ */
+ protected Contact() {
+ }
+
+ protected void buildProperties(List<FieldUpdate> updates) {
+ for (Map.Entry<String, String> entry : entrySet()) {
+ if ("photo".equals(entry.getKey())) {
+ updates.add(Field.createFieldUpdate("haspicture", "true"));
+ } else if (!entry.getKey().startsWith("email") && !entry.getKey().startsWith("smtpemail")
+ && !entry.getKey().equals("fileas")) {
+ updates.add(Field.createFieldUpdate(entry.getKey(), entry.getValue()));
+ }
+ }
+ if (get("fileas") != null) {
+ updates.add(Field.createFieldUpdate("fileas", get("fileas")));
+ }
+ // handle email addresses
+ IndexedFieldUpdate emailFieldUpdate = null;
+ for (Map.Entry<String, String> entry : entrySet()) {
+ if (entry.getKey().startsWith("smtpemail") && entry.getValue() != null) {
+ if (emailFieldUpdate == null) {
+ emailFieldUpdate = new IndexedFieldUpdate("EmailAddresses");
+ }
+ emailFieldUpdate.addFieldValue(Field.createFieldUpdate(entry.getKey(), entry.getValue()));
+ }
+ }
+ if (emailFieldUpdate != null) {
+ updates.add(emailFieldUpdate);
+ }
+ }
+
+
+ /**
+ * Create or update contact
+ *
+ * @return action result
+ * @throws IOException on error
+ */
+ public ItemResult createOrUpdate() throws IOException {
+ String photo = get("photo");
+
+ ItemResult itemResult = new ItemResult();
+ EWSMethod createOrUpdateItemMethod;
+
+ // first try to load existing event
+ String currentEtag = null;
+ ItemId currentItemId = null;
+ FileAttachment currentFileAttachment = null;
+ EWSMethod.Item currentItem = getEwsItem(folderPath, itemName);
+ if (currentItem != null) {
+ currentItemId = new ItemId(currentItem);
+ currentEtag = currentItem.get(Field.get("etag").getResponseName());
+
+ // load current picture
+ GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, currentItemId, false);
+ getItemMethod.addAdditionalProperty(Field.get("attachments"));
+ executeMethod(getItemMethod);
+ EWSMethod.Item item = getItemMethod.getResponseItem();
+ if (item != null) {
+ currentFileAttachment = item.getAttachmentByName("ContactPicture.jpg");
+ }
+ }
+ if ("*".equals(noneMatch)) {
+ // create requested
+ if (currentItemId != null) {
+ itemResult.status = HttpStatus.SC_PRECONDITION_FAILED;
+ return itemResult;
+ }
+ } else if (etag != null) {
+ // update requested
+ if (currentItemId == null || !etag.equals(currentEtag)) {
+ itemResult.status = HttpStatus.SC_PRECONDITION_FAILED;
+ return itemResult;
+ }
+ }
+
+ List<FieldUpdate> properties = new ArrayList<FieldUpdate>();
+ if (currentItemId != null) {
+ buildProperties(properties);
+ // update
+ createOrUpdateItemMethod = new UpdateItemMethod(MessageDisposition.SaveOnly,
+ ConflictResolution.AlwaysOverwrite,
+ SendMeetingInvitationsOrCancellations.SendToNone,
+ currentItemId, properties);
+ } else {
+ // create
+ EWSMethod.Item newItem = new EWSMethod.Item();
+ newItem.type = "Contact";
+ // force urlcompname on create
+ properties.add(Field.createFieldUpdate("urlcompname", convertItemNameToEML(itemName)));
+ buildProperties(properties);
+ newItem.setFieldUpdates(properties);
+ createOrUpdateItemMethod = new CreateItemMethod(MessageDisposition.SaveOnly, getFolderId(folderPath), newItem);
+ }
+ executeMethod(createOrUpdateItemMethod);
+
+ itemResult.status = createOrUpdateItemMethod.getStatusCode();
+ if (itemResult.status == HttpURLConnection.HTTP_OK) {
+ //noinspection VariableNotUsedInsideIf
+ if (etag == null) {
+ itemResult.status = HttpStatus.SC_CREATED;
+ LOGGER.debug("Created contact " + getHref());
+ } else {
+ LOGGER.debug("Updated contact " + getHref());
+ }
+ } else {
+ return itemResult;
+ }
+
+ ItemId newItemId = new ItemId(createOrUpdateItemMethod.getResponseItem());
+
+ // disable contact picture handling on Exchange 2007
+ if (!"Exchange2007_SP1".equals(serverVersion)) {
+ // first delete current picture
+ if (currentFileAttachment != null) {
+ DeleteAttachmentMethod deleteAttachmentMethod = new DeleteAttachmentMethod(currentFileAttachment.attachmentId);
+ executeMethod(deleteAttachmentMethod);
+ }
+
+ if (photo != null) {
+ // convert image to jpeg
+ byte[] resizedImageBytes = IOUtil.resizeImage(Base64.decodeBase64(photo.getBytes()), 90);
+
+ FileAttachment attachment = new FileAttachment("ContactPicture.jpg", "image/jpeg", new String(Base64.encodeBase64(resizedImageBytes)));
+ attachment.setIsContactPhoto(true);
+
+ // update photo attachment
+ CreateAttachmentMethod createAttachmentMethod = new CreateAttachmentMethod(newItemId, attachment);
+ executeMethod(createAttachmentMethod);
+ }
+ }
+
+ GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, newItemId, false);
+ getItemMethod.addAdditionalProperty(Field.get("etag"));
+ executeMethod(getItemMethod);
+ itemResult.etag = getItemMethod.getResponseItem().get(Field.get("etag").getResponseName());
+
+ return itemResult;
+ }
+ }
+
+ protected class Event extends ExchangeSession.Event {
+ // item id
+ ItemId itemId;
+ String type;
+ boolean isException;
+
+ protected Event(EWSMethod.Item response) {
+ itemId = new ItemId(response);
+
+ type = response.type;
+
+ permanentUrl = response.get(Field.get("permanenturl").getResponseName());
+ etag = response.get(Field.get("etag").getResponseName());
+ displayName = response.get(Field.get("displayname").getResponseName());
+ subject = response.get(Field.get("subject").getResponseName());
+ itemName = StringUtil.decodeUrlcompname(response.get(Field.get("urlcompname").getResponseName()));
+ // workaround for missing urlcompname in Exchange 2010
+ if (itemName == null) {
+ itemName = StringUtil.base64ToUrl(itemId.id) + ".EML";
+ }
+ String instancetype = response.get(Field.get("instancetype").getResponseName());
+ boolean isrecurring = "true".equals(response.get(Field.get("isrecurring").getResponseName()));
+ String calendaritemtype = response.get(Field.get("calendaritemtype").getResponseName());
+ isException = "3".equals(instancetype) || (isrecurring && "Single".equals(calendaritemtype));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch) throws IOException {
+ super(folderPath, itemName, contentClass, itemBody, etag, noneMatch);
+ }
+
+ @Override
+ public ItemResult createOrUpdate() throws IOException {
+ if (vCalendar.isTodo() && isMainCalendar(folderPath)) {
+ // task item, move to tasks folder
+ folderPath = TASKS;
+ }
+
+ ItemResult itemResult = new ItemResult();
+ EWSMethod createOrUpdateItemMethod;
+
+ // first try to load existing event
+ String currentEtag = null;
+ ItemId currentItemId = null;
+ String ownerResponseReply = null;
+
+ EWSMethod.Item currentItem = getEwsItem(folderPath, itemName);
+ if (currentItem != null) {
+ currentItemId = new ItemId(currentItem);
+ currentEtag = currentItem.get(Field.get("etag").getResponseName());
+ LOGGER.debug("Existing item found with etag: " + currentEtag + " client etag: " + etag + " id: " + currentItemId.id);
+ }
+ if ("*".equals(noneMatch)) {
+ // create requested
+ if (currentItemId != null) {
+ itemResult.status = HttpStatus.SC_PRECONDITION_FAILED;
+ return itemResult;
+ }
+ } else if (etag != null) {
+ // update requested
+ if (currentItemId == null || !etag.equals(currentEtag)) {
+ itemResult.status = HttpStatus.SC_PRECONDITION_FAILED;
+ return itemResult;
+ }
+ }
+ if (vCalendar.isTodo()) {
+ // create or update task method
+ EWSMethod.Item newItem = new EWSMethod.Item();
+ newItem.type = "Task";
+ List<FieldUpdate> updates = new ArrayList<FieldUpdate>();
+ updates.add(Field.createFieldUpdate("importance", convertPriorityToExchange(vCalendar.getFirstVeventPropertyValue("PRIORITY"))));
+ updates.add(Field.createFieldUpdate("calendaruid", vCalendar.getFirstVeventPropertyValue("UID")));
+ // force urlcompname
+ updates.add(Field.createFieldUpdate("urlcompname", convertItemNameToEML(itemName)));
+ updates.add(Field.createFieldUpdate("subject", vCalendar.getFirstVeventPropertyValue("SUMMARY")));
+ updates.add(Field.createFieldUpdate("description", vCalendar.getFirstVeventPropertyValue("DESCRIPTION")));
+ updates.add(Field.createFieldUpdate("keywords", vCalendar.getFirstVeventPropertyValue("CATEGORIES")));
+ updates.add(Field.createFieldUpdate("startdate", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DTSTART"))));
+ updates.add(Field.createFieldUpdate("duedate", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DUE"))));
+ updates.add(Field.createFieldUpdate("datecompleted", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("COMPLETED"))));
+
+ updates.add(Field.createFieldUpdate("commonstart", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DTSTART"))));
+ updates.add(Field.createFieldUpdate("commonend", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DUE"))));
+
+ String percentComplete = vCalendar.getFirstVeventPropertyValue("PERCENT-COMPLETE");
+ if (percentComplete == null) {
+ percentComplete = "0";
+ }
+ updates.add(Field.createFieldUpdate("percentcomplete", percentComplete));
+ String vTodoStatus = vCalendar.getFirstVeventPropertyValue("STATUS");
+ if (vTodoStatus == null) {
+ updates.add(Field.createFieldUpdate("taskstatus", "NotStarted"));
+ } else {
+ updates.add(Field.createFieldUpdate("taskstatus", vTodoToTaskStatusMap.get(vTodoStatus)));
+ }
+
+ //updates.add(Field.createFieldUpdate("iscomplete", "COMPLETED".equals(vTodoStatus)?"True":"False"));
+
+ if (currentItemId != null) {
+ // update
+ createOrUpdateItemMethod = new UpdateItemMethod(MessageDisposition.SaveOnly,
+ ConflictResolution.AutoResolve,
+ SendMeetingInvitationsOrCancellations.SendToNone,
+ currentItemId, updates);
+ } else {
+ newItem.setFieldUpdates(updates);
+ // create
+ createOrUpdateItemMethod = new CreateItemMethod(MessageDisposition.SaveOnly, SendMeetingInvitations.SendToNone, getFolderId(folderPath), newItem);
+ }
+
+ } else {
+
+ if (currentItemId != null) {
+ /*Set<FieldUpdate> updates = new HashSet<FieldUpdate>();
+ // TODO: update properties instead of brute force delete/add
+ updates.add(new FieldUpdate(Field.get("mimeContent"), new String(Base64.encodeBase64(itemContent))));
+ // update
+ createOrUpdateItemMethod = new UpdateItemMethod(MessageDisposition.SaveOnly,
+ ConflictResolution.AutoResolve,
+ SendMeetingInvitationsOrCancellations.SendToNone,
+ currentItemId, updates);*/
+ // hard method: delete/create on update
+ DeleteItemMethod deleteItemMethod = new DeleteItemMethod(currentItemId, DeleteType.HardDelete, SendMeetingCancellations.SendToNone);
+ executeMethod(deleteItemMethod);
+ } //else {
+ // create
+ EWSMethod.Item newItem = new EWSMethod.Item();
+ newItem.type = "CalendarItem";
+ newItem.mimeContent = Base64.encodeBase64(vCalendar.toString().getBytes("UTF-8"));
+ ArrayList<FieldUpdate> updates = new ArrayList<FieldUpdate>();
+ if (!vCalendar.hasVAlarm()) {
+ updates.add(Field.createFieldUpdate("reminderset", "false"));
+ }
+ //updates.add(Field.createFieldUpdate("outlookmessageclass", "IPM.Appointment"));
+ // force urlcompname
+ updates.add(Field.createFieldUpdate("urlcompname", convertItemNameToEML(itemName)));
+ if (vCalendar.isMeeting()) {
+ if (vCalendar.isMeetingOrganizer()) {
+ updates.add(Field.createFieldUpdate("apptstateflags", "1"));
+ } else {
+ updates.add(Field.createFieldUpdate("apptstateflags", "3"));
+ }
+ } else {
+ updates.add(Field.createFieldUpdate("apptstateflags", "0"));
+ }
+ // store mozilla invitations option
+ String xMozSendInvitations = vCalendar.getFirstVeventPropertyValue("X-MOZ-SEND-INVITATIONS");
+ if (xMozSendInvitations != null) {
+ updates.add(Field.createFieldUpdate("xmozsendinvitations", xMozSendInvitations));
+ }
+ // handle mozilla alarm
+ String xMozLastack = vCalendar.getFirstVeventPropertyValue("X-MOZ-LASTACK");
+ if (xMozLastack != null) {
+ updates.add(Field.createFieldUpdate("xmozlastack", xMozLastack));
+ }
+ String xMozSnoozeTime = vCalendar.getFirstVeventPropertyValue("X-MOZ-SNOOZE-TIME");
+ if (xMozSnoozeTime != null) {
+ updates.add(Field.createFieldUpdate("xmozsnoozetime", xMozSnoozeTime));
+ }
+
+ if (vCalendar.isMeeting() && "Exchange2007_SP1".equals(serverVersion)) {
+ Set<String> requiredAttendees = new HashSet<String>();
+ Set<String> optionalAttendees = new HashSet<String>();
+ List<VProperty> attendeeProperties = vCalendar.getFirstVeventProperties("ATTENDEE");
+ if (attendeeProperties != null) {
+ for (VProperty property : attendeeProperties) {
+ String attendeeEmail = vCalendar.getEmailValue(property);
+ if (attendeeEmail != null && attendeeEmail.indexOf('@') >= 0) {
+ if (email.equals(attendeeEmail)) {
+ String ownerPartStat = property.getParamValue("PARTSTAT");
+ if ("ACCEPTED".equals(ownerPartStat)) {
+ ownerResponseReply = "AcceptItem";
+ // do not send DeclineItem to avoid deleting target event
+ //} else if ("DECLINED".equals(ownerPartStat)) {
+ // ownerResponseReply = "DeclineItem";
+ } else if ("TENTATIVE".equals(ownerPartStat)) {
+ ownerResponseReply = "TentativelyAcceptItem";
+ }
+ }
+ InternetAddress internetAddress = new InternetAddress(attendeeEmail, property.getParamValue("CN"));
+ String attendeeRole = property.getParamValue("ROLE");
+ if ("REQ-PARTICIPANT".equals(attendeeRole)) {
+ requiredAttendees.add(internetAddress.toString());
+ } else {
+ optionalAttendees.add(internetAddress.toString());
+ }
+ }
+ }
+ }
+ List<VProperty> organizerProperties = vCalendar.getFirstVeventProperties("ORGANIZER");
+ if (organizerProperties != null) {
+ VProperty property = organizerProperties.get(0);
+ String organizerEmail = vCalendar.getEmailValue(property);
+ if (organizerEmail != null && organizerEmail.indexOf('@') >= 0) {
+ updates.add(Field.createFieldUpdate("from", organizerEmail));
+ }
+ }
+
+ if (requiredAttendees.size() > 0) {
+ updates.add(Field.createFieldUpdate("to", StringUtil.join(requiredAttendees, ", ")));
+ }
+ if (optionalAttendees.size() > 0) {
+ updates.add(Field.createFieldUpdate("cc", StringUtil.join(optionalAttendees, ", ")));
+ }
+ }
+
+ // patch allday date values, only on 2007
+ if ("Exchange2007_SP1".equals(serverVersion) && vCalendar.isCdoAllDay()) {
+ updates.add(Field.createFieldUpdate("dtstart", convertCalendarDateToExchange(vCalendar.getFirstVeventPropertyValue("DTSTART"))));
+ updates.add(Field.createFieldUpdate("dtend", convertCalendarDateToExchange(vCalendar.getFirstVeventPropertyValue("DTEND"))));
+ }
+ updates.add(Field.createFieldUpdate("busystatus", "BUSY".equals(vCalendar.getFirstVeventPropertyValue("X-MICROSOFT-CDO-BUSYSTATUS")) ? "Busy" : "Free"));
+ if ("Exchange2007_SP1".equals(serverVersion) && vCalendar.isCdoAllDay()) {
+ updates.add(Field.createFieldUpdate("meetingtimezone", vCalendar.getVTimezone().getPropertyValue("TZID")));
+ }
+
+ newItem.setFieldUpdates(updates);
+ createOrUpdateItemMethod = new CreateItemMethod(MessageDisposition.SaveOnly, SendMeetingInvitations.SendToNone, getFolderId(folderPath), newItem);
+ // force context Timezone on Exchange 2010
+ if (serverVersion != null && serverVersion.startsWith("Exchange2010")) {
+ createOrUpdateItemMethod.setTimezoneContext(EwsExchangeSession.this.getVTimezone().getPropertyValue("TZID"));
+ }
+ //}
+ }
+ executeMethod(createOrUpdateItemMethod);
+
+ itemResult.status = createOrUpdateItemMethod.getStatusCode();
+ if (itemResult.status == HttpURLConnection.HTTP_OK) {
+ //noinspection VariableNotUsedInsideIf
+ if (currentItemId == null) {
+ itemResult.status = HttpStatus.SC_CREATED;
+ LOGGER.debug("Created event " + getHref());
+ } else {
+ LOGGER.warn("Overwritten event " + getHref());
+ }
+ }
+
+ // force responsetype on Exchange 2007
+ if (ownerResponseReply != null) {
+ EWSMethod.Item responseTypeItem = new EWSMethod.Item();
+ responseTypeItem.referenceItemId = new ItemId("ReferenceItemId", createOrUpdateItemMethod.getResponseItem());
+ responseTypeItem.type = ownerResponseReply;
+ createOrUpdateItemMethod = new CreateItemMethod(MessageDisposition.SaveOnly, SendMeetingInvitations.SendToNone, null, responseTypeItem);
+ executeMethod(createOrUpdateItemMethod);
+
+ // force urlcompname again
+ ArrayList<FieldUpdate> updates = new ArrayList<FieldUpdate>();
+ updates.add(Field.createFieldUpdate("urlcompname", convertItemNameToEML(itemName)));
+ createOrUpdateItemMethod = new UpdateItemMethod(MessageDisposition.SaveOnly,
+ ConflictResolution.AlwaysOverwrite,
+ SendMeetingInvitationsOrCancellations.SendToNone,
+ new ItemId(createOrUpdateItemMethod.getResponseItem()),
+ updates);
+ executeMethod(createOrUpdateItemMethod);
+ }
+
+ ItemId newItemId = new ItemId(createOrUpdateItemMethod.getResponseItem());
+ GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, newItemId, false);
+ getItemMethod.addAdditionalProperty(Field.get("etag"));
+ executeMethod(getItemMethod);
+ itemResult.etag = getItemMethod.getResponseItem().get(Field.get("etag").getResponseName());
+
+ return itemResult;
+
+ }
+
+ @Override
+ public byte[] getEventContent() throws IOException {
+ byte[] content;
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("Get event: " + itemName);
+ }
+ try {
+ GetItemMethod getItemMethod;
+ if ("Task".equals(type)) {
+ getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, itemId, false);
+ getItemMethod.addAdditionalProperty(Field.get("importance"));
+ getItemMethod.addAdditionalProperty(Field.get("subject"));
+ getItemMethod.addAdditionalProperty(Field.get("created"));
+ getItemMethod.addAdditionalProperty(Field.get("lastmodified"));
+ getItemMethod.addAdditionalProperty(Field.get("calendaruid"));
+ getItemMethod.addAdditionalProperty(Field.get("description"));
+ getItemMethod.addAdditionalProperty(Field.get("percentcomplete"));
+ getItemMethod.addAdditionalProperty(Field.get("taskstatus"));
+ getItemMethod.addAdditionalProperty(Field.get("startdate"));
+ getItemMethod.addAdditionalProperty(Field.get("duedate"));
+ getItemMethod.addAdditionalProperty(Field.get("datecompleted"));
+ getItemMethod.addAdditionalProperty(Field.get("keywords"));
+
+ } else if (!"Message".equals(type)) {
+ getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, itemId, true);
+ getItemMethod.addAdditionalProperty(Field.get("reminderset"));
+ getItemMethod.addAdditionalProperty(Field.get("calendaruid"));
+ getItemMethod.addAdditionalProperty(Field.get("myresponsetype"));
+ getItemMethod.addAdditionalProperty(Field.get("requiredattendees"));
+ getItemMethod.addAdditionalProperty(Field.get("optionalattendees"));
+ getItemMethod.addAdditionalProperty(Field.get("modifiedoccurrences"));
+ getItemMethod.addAdditionalProperty(Field.get("xmozlastack"));
+ getItemMethod.addAdditionalProperty(Field.get("xmozsnoozetime"));
+ getItemMethod.addAdditionalProperty(Field.get("xmozsendinvitations"));
+ } else {
+ getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, itemId, true);
+ }
+
+ executeMethod(getItemMethod);
+ if ("Task".equals(type)) {
+ VObject vTimezone = getVTimezone();
+ VCalendar localVCalendar = new VCalendar();
+ VObject vTodo = new VObject();
+ vTodo.type = "VTODO";
+ localVCalendar.setTimezone(vTimezone);
+ vTodo.setPropertyValue("LAST-MODIFIED", convertDateFromExchange(getItemMethod.getResponseItem().get(Field.get("lastmodified").getResponseName())));
+ vTodo.setPropertyValue("CREATED", convertDateFromExchange(getItemMethod.getResponseItem().get(Field.get("created").getResponseName())));
+ String calendarUid = getItemMethod.getResponseItem().get(Field.get("calendaruid").getResponseName());
+ if (calendarUid == null) {
+ // use item id as uid for Exchange created tasks
+ calendarUid = itemId.id;
+ }
+ vTodo.setPropertyValue("UID", calendarUid);
+ vTodo.setPropertyValue("SUMMARY", getItemMethod.getResponseItem().get(Field.get("subject").getResponseName()));
+ vTodo.setPropertyValue("DESCRIPTION", getItemMethod.getResponseItem().get(Field.get("description").getResponseName()));
+ vTodo.setPropertyValue("PRIORITY", convertPriorityFromExchange(getItemMethod.getResponseItem().get(Field.get("importance").getResponseName())));
+ vTodo.setPropertyValue("PERCENT-COMPLETE", getItemMethod.getResponseItem().get(Field.get("percentcomplete").getResponseName()));
+ vTodo.setPropertyValue("STATUS", taskTovTodoStatusMap.get(getItemMethod.getResponseItem().get(Field.get("taskstatus").getResponseName())));
+
+ vTodo.setPropertyValue("DUE;VALUE=DATE", convertDateFromExchangeToTaskDate(getItemMethod.getResponseItem().get(Field.get("duedate").getResponseName())));
+ vTodo.setPropertyValue("DTSTART;VALUE=DATE", convertDateFromExchangeToTaskDate(getItemMethod.getResponseItem().get(Field.get("startdate").getResponseName())));
+ vTodo.setPropertyValue("COMPLETED;VALUE=DATE", convertDateFromExchangeToTaskDate(getItemMethod.getResponseItem().get(Field.get("datecompleted").getResponseName())));
+
+ vTodo.setPropertyValue("CATEGORIES", getItemMethod.getResponseItem().get(Field.get("keywords").getResponseName()));
+
+ localVCalendar.addVObject(vTodo);
+ content = localVCalendar.toString().getBytes("UTF-8");
+ } else {
+ content = getItemMethod.getMimeContent();
+ if (content == null) {
+ throw new IOException("empty event body");
+ }
+ if (!"CalendarItem".equals(type)) {
+ content = getICS(new SharedByteArrayInputStream(content));
+ }
+ VCalendar localVCalendar = new VCalendar(content, email, getVTimezone());
+ // remove additional reminder
+ if (!"true".equals(getItemMethod.getResponseItem().get(Field.get("reminderset").getResponseName()))) {
+ localVCalendar.removeVAlarm();
+ }
+ String calendaruid = getItemMethod.getResponseItem().get(Field.get("calendaruid").getResponseName());
+ if (calendaruid != null) {
+ localVCalendar.setFirstVeventPropertyValue("UID", calendaruid);
+ }
+ List<EWSMethod.Attendee> attendees = getItemMethod.getResponseItem().getAttendees();
+ if (attendees != null) {
+ for (EWSMethod.Attendee attendee : attendees) {
+ VProperty attendeeProperty = new VProperty("ATTENDEE", "mailto:" + attendee.email);
+ attendeeProperty.addParam("CN", attendee.name);
+ String myResponseType = getItemMethod.getResponseItem().get(Field.get("myresponsetype").getResponseName());
+ if ("Exchange2007_SP1".equals(serverVersion) && email.equalsIgnoreCase(attendee.email) && myResponseType != null) {
+ attendeeProperty.addParam("PARTSTAT", EWSMethod.responseTypeToPartstat(myResponseType));
+ } else {
+ attendeeProperty.addParam("PARTSTAT", attendee.partstat);
+ }
+ //attendeeProperty.addParam("RSVP", "TRUE");
+ attendeeProperty.addParam("ROLE", attendee.role);
+ localVCalendar.addFirstVeventProperty(attendeeProperty);
+ }
+ }
+ // fix UID and RECURRENCE-ID, broken at least on Exchange 2007
+ List<EWSMethod.Occurrence> occurences = getItemMethod.getResponseItem().getOccurrences();
+ if (occurences != null) {
+ Iterator<VObject> modifiedOccurrencesIterator = localVCalendar.getModifiedOccurrences().iterator();
+ for (EWSMethod.Occurrence occurrence : occurences) {
+ if (modifiedOccurrencesIterator.hasNext()) {
+ VObject modifiedOccurrence = modifiedOccurrencesIterator.next();
+ // fix uid, should be the same as main VEVENT
+ if (calendaruid != null) {
+ modifiedOccurrence.setPropertyValue("UID", calendaruid);
+ }
+ VProperty recurrenceId = modifiedOccurrence.getProperty("RECURRENCE-ID");
+ if (recurrenceId != null) {
+ recurrenceId.removeParam("TZID");
+ recurrenceId.getValues().set(0, convertDateFromExchange(occurrence.originalStart));
+ }
+ }
+ }
+ }
+ // restore mozilla invitations option
+ localVCalendar.setFirstVeventPropertyValue("X-MOZ-SEND-INVITATIONS",
+ getItemMethod.getResponseItem().get(Field.get("xmozsendinvitations").getResponseName()));
+ // restore mozilla alarm status
+ localVCalendar.setFirstVeventPropertyValue("X-MOZ-LASTACK",
+ getItemMethod.getResponseItem().get(Field.get("xmozlastack").getResponseName()));
+ localVCalendar.setFirstVeventPropertyValue("X-MOZ-SNOOZE-TIME",
+ getItemMethod.getResponseItem().get(Field.get("xmozsnoozetime").getResponseName()));
+ // overwrite method
+ // localVCalendar.setPropertyValue("METHOD", "REQUEST");
+ content = localVCalendar.toString().getBytes("UTF-8");
+ }
+ } catch (IOException e) {
+ throw buildHttpException(e);
+ } catch (MessagingException e) {
+ throw buildHttpException(e);
+ }
+ return content;
+ }
+ }
+
+ @Override
+ public List<ExchangeSession.Contact> searchContacts(String folderPath, Set<String> attributes, Condition condition, int maxCount) throws IOException {
+ List<ExchangeSession.Contact> contacts = new ArrayList<ExchangeSession.Contact>();
+ List<EWSMethod.Item> responses = searchItems(folderPath, attributes, condition,
+ FolderQueryTraversal.SHALLOW, maxCount);
+
+ for (EWSMethod.Item response : responses) {
+ contacts.add(new Contact(response));
+ }
+ return contacts;
+ }
+
+ @Override
+ protected Condition getCalendarItemCondition(Condition dateCondition) {
+ // tasks in calendar not supported over EWS => do not look for instancetype null
+ return or(
+ // Exchange 2010
+ or(isTrue("isrecurring"),
+ and(isFalse("isrecurring"), dateCondition)),
+ // Exchange 2007
+ or(isEqualTo("instancetype", 1),
+ and(isEqualTo("instancetype", 0), dateCondition))
+ );
+ }
+
+ @Override
+ public List<ExchangeSession.Event> getEventMessages(String folderPath) throws IOException {
+ return searchEvents(folderPath, ITEM_PROPERTIES,
+ and(startsWith("outlookmessageclass", "IPM.Schedule.Meeting."),
+ or(isNull("processed"), isFalse("processed"))));
+ }
+
+ @Override
+ public List<ExchangeSession.Event> searchEvents(String folderPath, Set<String> attributes, Condition condition) throws IOException {
+ List<ExchangeSession.Event> events = new ArrayList<ExchangeSession.Event>();
+ List<EWSMethod.Item> responses = searchItems(folderPath, attributes,
+ condition,
+ FolderQueryTraversal.SHALLOW, 0);
+ for (EWSMethod.Item response : responses) {
+ Event event = new Event(response);
+ if ("Message".equals(event.type)) {
+ // TODO: just exclude
+ // need to check body
+ try {
+ event.getEventContent();
+ events.add(event);
+ } catch (HttpException e) {
+ LOGGER.warn("Ignore invalid event " + event.getHref());
+ }
+ // exclude exceptions
+ } else if (event.isException) {
+ LOGGER.debug("Exclude recurrence exception " + event.getHref());
+ } else {
+ events.add(event);
+ }
+
+ }
+
+ return events;
+ }
+
+ /**
+ * Common item properties
+ */
+ protected static final Set<String> ITEM_PROPERTIES = new HashSet<String>();
+
+ static {
+ ITEM_PROPERTIES.add("etag");
+ ITEM_PROPERTIES.add("displayname");
+ // calendar CdoInstanceType
+ ITEM_PROPERTIES.add("instancetype");
+ ITEM_PROPERTIES.add("urlcompname");
+ ITEM_PROPERTIES.add("subject");
+
+ ITEM_PROPERTIES.add("calendaritemtype");
+ ITEM_PROPERTIES.add("isrecurring");
+ }
+
+ protected static final HashSet<String> EVENT_REQUEST_PROPERTIES = new HashSet<String>();
+
+ static {
+ EVENT_REQUEST_PROPERTIES.add("permanenturl");
+ EVENT_REQUEST_PROPERTIES.add("etag");
+ EVENT_REQUEST_PROPERTIES.add("displayname");
+ EVENT_REQUEST_PROPERTIES.add("subject");
+ EVENT_REQUEST_PROPERTIES.add("urlcompname");
+ }
+
+ protected Set<String> getItemProperties() {
+ return ITEM_PROPERTIES;
+ }
+
+ protected EWSMethod.Item getEwsItem(String folderPath, String itemName) throws IOException {
+ EWSMethod.Item item = null;
+ String urlcompname = convertItemNameToEML(itemName);
+ // workaround for missing urlcompname in Exchange 2010
+ if (isItemId(urlcompname)) {
+ ItemId itemId = new ItemId(StringUtil.urlToBase64(urlcompname.substring(0, itemName.length() - 4)));
+ GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, itemId, false);
+ for (String attribute : EVENT_REQUEST_PROPERTIES) {
+ getItemMethod.addAdditionalProperty(Field.get(attribute));
+ }
+ executeMethod(getItemMethod);
+ item = getItemMethod.getResponseItem();
+ }
+ // find item by urlcompname
+ if (item == null) {
+ List<EWSMethod.Item> responses = searchItems(folderPath, EVENT_REQUEST_PROPERTIES, isEqualTo("urlcompname", urlcompname), FolderQueryTraversal.SHALLOW, 0);
+ if (!responses.isEmpty()) {
+ item = responses.get(0);
+ }
+ }
+ return item;
+ }
+
+
+ @Override
+ public Item getItem(String folderPath, String itemName) throws IOException {
+ EWSMethod.Item item = getEwsItem(folderPath, itemName);
+ if (item == null && isMainCalendar(folderPath)) {
+ // look for item in task folder, replace extension first
+ if (itemName.endsWith(".ics")) {
+ itemName = itemName.substring(0, itemName.length() - 3) + "EML";
+ }
+ item = getEwsItem(TASKS, itemName);
+ }
+
+ if (item == null) {
+ throw new HttpNotFoundException(itemName + " not found in " + folderPath);
+ }
+
+ String itemType = item.type;
+ if ("Contact".equals(itemType)) {
+ // retrieve Contact properties
+ ItemId itemId = new ItemId(item);
+ GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, itemId, false);
+ for (String attribute : CONTACT_ATTRIBUTES) {
+ getItemMethod.addAdditionalProperty(Field.get(attribute));
+ }
+ executeMethod(getItemMethod);
+ item = getItemMethod.getResponseItem();
+ if (item == null) {
+ throw new HttpNotFoundException(itemName + " not found in " + folderPath);
+ }
+ return new Contact(item);
+ } else if ("CalendarItem".equals(itemType)
+ || "MeetingRequest".equals(itemType)
+ || "Task".equals(itemType)
+ // VTODOs appear as Messages
+ || "Message".equals(itemType)) {
+ return new Event(item);
+ } else {
+ throw new HttpNotFoundException(itemName + " not found in " + folderPath);
+ }
+
+ }
+
+ @Override
+ public ContactPhoto getContactPhoto(ExchangeSession.Contact contact) throws IOException {
+ ContactPhoto contactPhoto = null;
+
+ GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, ((EwsExchangeSession.Contact) contact).itemId, false);
+ getItemMethod.addAdditionalProperty(Field.get("attachments"));
+ executeMethod(getItemMethod);
+ EWSMethod.Item item = getItemMethod.getResponseItem();
+ if (item != null) {
+ FileAttachment attachment = item.getAttachmentByName("ContactPicture.jpg");
+ if (attachment == null) {
+ throw new IOException("Missing contact picture");
+ }
+ // get attachment content
+ GetAttachmentMethod getAttachmentMethod = new GetAttachmentMethod(attachment.attachmentId);
+ executeMethod(getAttachmentMethod);
+
+ contactPhoto = new ContactPhoto();
+ contactPhoto.content = getAttachmentMethod.getResponseItem().get("Content");
+ if (attachment.contentType == null) {
+ contactPhoto.contentType = "image/jpeg";
+ } else {
+ contactPhoto.contentType = attachment.contentType;
+ }
+ }
+
+ return contactPhoto;
+ }
+
+ @Override
+ public void deleteItem(String folderPath, String itemName) throws IOException {
+ EWSMethod.Item item = getEwsItem(folderPath, itemName);
+ if (item == null && isMainCalendar(folderPath)) {
+ // look for item in task folder
+ item = getEwsItem(TASKS, itemName);
+ }
+ if (item != null) {
+ DeleteItemMethod deleteItemMethod = new DeleteItemMethod(new ItemId(item), DeleteType.HardDelete, SendMeetingCancellations.SendToNone);
+ executeMethod(deleteItemMethod);
+ }
+ }
+
+ @Override
+ public void processItem(String folderPath, String itemName) throws IOException {
+ EWSMethod.Item item = getEwsItem(folderPath, itemName);
+ if (item != null) {
+ HashMap<String, String> localProperties = new HashMap<String, String>();
+ localProperties.put("processed", "1");
+ localProperties.put("read", "1");
+ UpdateItemMethod updateItemMethod = new UpdateItemMethod(MessageDisposition.SaveOnly,
+ ConflictResolution.AlwaysOverwrite,
+ SendMeetingInvitationsOrCancellations.SendToNone,
+ new ItemId(item), buildProperties(localProperties));
+ executeMethod(updateItemMethod);
+ }
+ }
+
+ @Override
+ public int sendEvent(String icsBody) throws IOException {
+ String itemName = UUID.randomUUID().toString() + ".EML";
+ byte[] mimeContent = new Event(DRAFTS, itemName, "urn:content-classes:calendarmessage", icsBody, null, null).createMimeContent();
+ if (mimeContent == null) {
+ // no recipients, cancel
+ return HttpStatus.SC_NO_CONTENT;
+ } else {
+ sendMessage(null, mimeContent);
+ return HttpStatus.SC_OK;
+ }
+ }
+
+ @Override
+ protected ItemResult internalCreateOrUpdateContact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) throws IOException {
+ return new Contact(folderPath, itemName, properties, StringUtil.removeQuotes(etag), noneMatch).createOrUpdate();
+ }
+
+ @Override
+ protected ItemResult internalCreateOrUpdateEvent(String folderPath, String itemName, String contentClass, String icsBody, String etag, String noneMatch) throws IOException {
+ return new Event(folderPath, itemName, contentClass, icsBody, StringUtil.removeQuotes(etag), noneMatch).createOrUpdate();
+ }
+
+ @Override
+ public boolean isSharedFolder(String folderPath) {
+ return folderPath.startsWith("/") && !folderPath.toLowerCase().startsWith(currentMailboxPath);
+ }
+
+ @Override
+ public boolean isMainCalendar(String folderPath) {
+ return "calendar".equalsIgnoreCase(folderPath) || (currentMailboxPath + "/calendar").equalsIgnoreCase(folderPath);
+ }
+
+ @Override
+ protected String getFreeBusyData(String attendee, String start, String end, int interval) throws IOException {
+ GetUserAvailabilityMethod getUserAvailabilityMethod = new GetUserAvailabilityMethod(attendee, start, end, interval);
+ executeMethod(getUserAvailabilityMethod);
+ return getUserAvailabilityMethod.getMergedFreeBusy();
+ }
+
+ @Override
+ protected void loadVtimezone() {
+
+ try {
+ String timezoneId = null;
+ if (!"Exchange2007_SP1".equals(serverVersion)) {
+ GetUserConfigurationMethod getUserConfigurationMethod = new GetUserConfigurationMethod();
+ executeMethod(getUserConfigurationMethod);
+ EWSMethod.Item item = getUserConfigurationMethod.getResponseItem();
+ if (item != null) {
+ timezoneId = item.get("timezone");
+ }
+ } else {
+ timezoneId = getTimezoneidFromOptions();
+ }
+ // failover: use timezone id from settings file
+ if (timezoneId == null) {
+ timezoneId = Settings.getProperty("davmail.timezoneId");
+ }
+ // last failover: use GMT
+ if (timezoneId == null) {
+ LOGGER.warn("Unable to get user timezone, using GMT Standard Time. Set davmail.timezoneId setting to override this.");
+ timezoneId = "GMT Standard Time";
+ }
+
+ createCalendarFolder("davmailtemp", null);
+ EWSMethod.Item item = new EWSMethod.Item();
+ item.type = "CalendarItem";
+ if (!"Exchange2007_SP1".equals(serverVersion)) {
+ SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH);
+ dateFormatter.setTimeZone(GMT_TIMEZONE);
+ Calendar cal = Calendar.getInstance();
+ item.put("Start", dateFormatter.format(cal.getTime()));
+ cal.add(Calendar.DAY_OF_MONTH, 1);
+ item.put("End", dateFormatter.format(cal.getTime()));
+ item.put("StartTimeZone", timezoneId);
+ } else {
+ item.put("MeetingTimeZone", timezoneId);
+ }
+ CreateItemMethod createItemMethod = new CreateItemMethod(MessageDisposition.SaveOnly, SendMeetingInvitations.SendToNone, getFolderId("davmailtemp"), item);
+ executeMethod(createItemMethod);
+ item = createItemMethod.getResponseItem();
+ VCalendar vCalendar = new VCalendar(getContent(new ItemId(item)), email, null);
+ this.vTimezone = vCalendar.getVTimezone();
+ // delete temporary folder
+ deleteFolder("davmailtemp");
+ } catch (IOException e) {
+ LOGGER.warn("Unable to get VTIMEZONE info: " + e, e);
+ }
+ }
+
+ protected String getTimezoneidFromOptions() {
+ String result = null;
+ // get user mail URL from html body
+ BufferedReader optionsPageReader = null;
+ GetMethod optionsMethod = new GetMethod("/owa/?ae=Options&t=Regional");
+ try {
+ DavGatewayHttpClientFacade.executeGetMethod(httpClient, optionsMethod, false);
+ optionsPageReader = new BufferedReader(new InputStreamReader(optionsMethod.getResponseBodyAsStream()));
+ String line;
+ // find email
+ //noinspection StatementWithEmptyBody
+ while ((line = optionsPageReader.readLine()) != null
+ && (line.indexOf("tblTmZn") == -1)
+ && (line.indexOf("selTmZn") == -1)) {
+ }
+ if (line != null) {
+ if (line.indexOf("tblTmZn") >= 0) {
+ int start = line.indexOf("oV=\"") + 4;
+ int end = line.indexOf('\"', start);
+ result = line.substring(start, end);
+ } else {
+ int end = line.lastIndexOf("\" selected>");
+ int start = line.lastIndexOf('\"', end - 1);
+ result = line.substring(start + 1, end);
+ }
+ }
+ } catch (IOException e) {
+ LOGGER.error("Error parsing options page at " + optionsMethod.getPath());
+ } finally {
+ if (optionsPageReader != null) {
+ try {
+ optionsPageReader.close();
+ } catch (IOException e) {
+ LOGGER.error("Error parsing options page at " + optionsMethod.getPath());
+ }
+ }
+ optionsMethod.releaseConnection();
+ }
+
+ return result;
+ }
+
+
+ protected FolderId getFolderId(String folderPath) throws IOException {
+ FolderId folderId = getFolderIdIfExists(folderPath);
+ if (folderId == null) {
+ throw new HttpNotFoundException("Folder '" + folderPath + "' not found");
+ }
+ return folderId;
+ }
+
+ protected static final String USERS_ROOT = "/users/";
+
+ protected FolderId getFolderIdIfExists(String folderPath) throws IOException {
+ String lowerCaseFolderPath = folderPath.toLowerCase();
+ if (currentMailboxPath.equals(lowerCaseFolderPath)) {
+ return getSubFolderIdIfExists(null, "");
+ } else if (lowerCaseFolderPath.startsWith(currentMailboxPath + '/')) {
+ return getSubFolderIdIfExists(null, folderPath.substring(currentMailboxPath.length() + 1));
+ } else if (folderPath.startsWith("/users/")) {
+ int slashIndex = folderPath.indexOf('/', USERS_ROOT.length());
+ String mailbox;
+ String subFolderPath;
+ if (slashIndex >= 0) {
+ mailbox = folderPath.substring(USERS_ROOT.length(), slashIndex);
+ subFolderPath = folderPath.substring(slashIndex + 1);
+ } else {
+ mailbox = folderPath.substring(USERS_ROOT.length());
+ subFolderPath = "";
+ }
+ return getSubFolderIdIfExists(mailbox, subFolderPath);
+ } else {
+ return getSubFolderIdIfExists(null, folderPath);
+ }
+ }
+
+ protected FolderId getSubFolderIdIfExists(String mailbox, String folderPath) throws IOException {
+ String[] folderNames;
+ FolderId currentFolderId;
+
+ if (folderPath.startsWith(PUBLIC_ROOT)) {
+ currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.publicfoldersroot);
+ folderNames = folderPath.substring(PUBLIC_ROOT.length()).split("/");
+ } else if (folderPath.startsWith(ARCHIVE_ROOT)) {
+ currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.archivemsgfolderroot);
+ folderNames = folderPath.substring(ARCHIVE_ROOT.length()).split("/");
+ } else if (folderPath.startsWith(INBOX) || folderPath.startsWith(LOWER_CASE_INBOX)) {
+ currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.inbox);
+ folderNames = folderPath.substring(INBOX.length()).split("/");
+ } else if (folderPath.startsWith(CALENDAR)) {
+ currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.calendar);
+ folderNames = folderPath.substring(CALENDAR.length()).split("/");
+ } else if (folderPath.startsWith(TASKS)) {
+ currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.tasks);
+ folderNames = folderPath.substring(TASKS.length()).split("/");
+ } else if (folderPath.startsWith(CONTACTS)) {
+ currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.contacts);
+ folderNames = folderPath.substring(CONTACTS.length()).split("/");
+ } else if (folderPath.startsWith(SENT)) {
+ currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.sentitems);
+ folderNames = folderPath.substring(SENT.length()).split("/");
+ } else if (folderPath.startsWith(DRAFTS)) {
+ currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.drafts);
+ folderNames = folderPath.substring(DRAFTS.length()).split("/");
+ } else if (folderPath.startsWith(TRASH)) {
+ currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.deleteditems);
+ folderNames = folderPath.substring(TRASH.length()).split("/");
+ } else if (folderPath.startsWith(JUNK)) {
+ currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.junkemail);
+ folderNames = folderPath.substring(JUNK.length()).split("/");
+ } else if (folderPath.startsWith(UNSENT)) {
+ currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.outbox);
+ folderNames = folderPath.substring(UNSENT.length()).split("/");
+ } else {
+ currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.msgfolderroot);
+ folderNames = folderPath.split("/");
+ }
+ for (String folderName : folderNames) {
+ if (folderName.length() > 0) {
+ currentFolderId = getSubFolderByName(currentFolderId, folderName);
+ if (currentFolderId == null) {
+ break;
+ }
+ }
+ }
+ return currentFolderId;
+ }
+
+ protected FolderId getSubFolderByName(FolderId parentFolderId, String folderName) throws IOException {
+ FolderId folderId = null;
+ FindFolderMethod findFolderMethod = new FindFolderMethod(
+ FolderQueryTraversal.SHALLOW,
+ BaseShape.ID_ONLY,
+ parentFolderId,
+ FOLDER_PROPERTIES,
+ new TwoOperandExpression(TwoOperandExpression.Operator.IsEqualTo,
+ Field.get("folderDisplayName"), folderName)
+ );
+ executeMethod(findFolderMethod);
+ EWSMethod.Item item = findFolderMethod.getResponseItem();
+ if (item != null) {
+ folderId = new FolderId(item);
+ }
+ return folderId;
+ }
+
+ protected void executeMethod(EWSMethod ewsMethod) throws IOException {
+ try {
+ ewsMethod.setServerVersion(serverVersion);
+ httpClient.executeMethod(ewsMethod);
+ if (serverVersion == null) {
+ serverVersion = ewsMethod.getServerVersion();
+ }
+ ewsMethod.checkSuccess();
+ } catch (SocketException e) {
+ LOGGER.error(e + " " + e.getMessage(), e);
+ throw new EWSException(e + " " + e.getMessage());
+ } finally {
+ ewsMethod.releaseConnection();
+ }
+ }
+
+ protected static final HashMap<String, String> GALFIND_ATTRIBUTE_MAP = new HashMap<String, String>();
+
+ static {
+ GALFIND_ATTRIBUTE_MAP.put("imapUid", "Name");
+ GALFIND_ATTRIBUTE_MAP.put("cn", "DisplayName");
+ GALFIND_ATTRIBUTE_MAP.put("givenName", "GivenName");
+ GALFIND_ATTRIBUTE_MAP.put("sn", "Surname");
+ GALFIND_ATTRIBUTE_MAP.put("smtpemail1", "EmailAddress");
+
+ GALFIND_ATTRIBUTE_MAP.put("roomnumber", "OfficeLocation");
+ GALFIND_ATTRIBUTE_MAP.put("street", "BusinessStreet");
+ GALFIND_ATTRIBUTE_MAP.put("l", "BusinessCity");
+ GALFIND_ATTRIBUTE_MAP.put("o", "CompanyName");
+ GALFIND_ATTRIBUTE_MAP.put("postalcode", "BusinessPostalCode");
+ GALFIND_ATTRIBUTE_MAP.put("st", "BusinessState");
+ GALFIND_ATTRIBUTE_MAP.put("co", "BusinessCountryOrRegion");
+
+ GALFIND_ATTRIBUTE_MAP.put("manager", "Manager");
+ GALFIND_ATTRIBUTE_MAP.put("middlename", "Initials");
+ GALFIND_ATTRIBUTE_MAP.put("title", "JobTitle");
+ GALFIND_ATTRIBUTE_MAP.put("department", "Department");
+
+ GALFIND_ATTRIBUTE_MAP.put("otherTelephone", "OtherTelephone");
+ GALFIND_ATTRIBUTE_MAP.put("telephoneNumber", "BusinessPhone");
+ GALFIND_ATTRIBUTE_MAP.put("mobile", "MobilePhone");
+ GALFIND_ATTRIBUTE_MAP.put("facsimiletelephonenumber", "BusinessFax");
+ GALFIND_ATTRIBUTE_MAP.put("secretarycn", "AssistantName");
+ }
+
+ protected static final HashSet<String> IGNORE_ATTRIBUTE_SET = new HashSet<String>();
+
+ static {
+ IGNORE_ATTRIBUTE_SET.add("ContactSource");
+ IGNORE_ATTRIBUTE_SET.add("Culture");
+ IGNORE_ATTRIBUTE_SET.add("AssistantPhone");
+ }
+
+ protected Contact buildGalfindContact(EWSMethod.Item response) {
+ Contact contact = new Contact();
+ contact.setName(response.get("Name"));
+ contact.put("imapUid", response.get("Name"));
+ contact.put("uid", response.get("Name"));
+ if (LOGGER.isDebugEnabled()) {
+ for (Map.Entry<String, String> entry : response.entrySet()) {
+ String key = entry.getKey();
+ if (!IGNORE_ATTRIBUTE_SET.contains(key) && !GALFIND_ATTRIBUTE_MAP.containsValue(key)) {
+ LOGGER.debug("Unsupported ResolveNames " + contact.getName() + " response attribute: " + key + " value: " + entry.getValue());
+ }
+ }
+ }
+ for (Map.Entry<String, String> entry : GALFIND_ATTRIBUTE_MAP.entrySet()) {
+ String attributeValue = response.get(entry.getValue());
+ if (attributeValue != null) {
+ contact.put(entry.getKey(), attributeValue);
+ }
+ }
+ return contact;
+ }
+
+ @Override
+ public Map<String, ExchangeSession.Contact> galFind(Condition condition, Set<String> returningAttributes, int sizeLimit) throws IOException {
+ Map<String, ExchangeSession.Contact> contacts = new HashMap<String, ExchangeSession.Contact>();
+ if (condition instanceof MultiCondition) {
+ List<Condition> conditions = ((ExchangeSession.MultiCondition) condition).getConditions();
+ Operator operator = ((ExchangeSession.MultiCondition) condition).getOperator();
+ if (operator == Operator.Or) {
+ for (Condition innerCondition : conditions) {
+ contacts.putAll(galFind(innerCondition, returningAttributes, sizeLimit));
+ }
+ } else if (operator == Operator.And && !conditions.isEmpty()) {
+ Map<String, ExchangeSession.Contact> innerContacts = galFind(conditions.get(0), returningAttributes, sizeLimit);
+ for (ExchangeSession.Contact contact : innerContacts.values()) {
+ if (condition.isMatch(contact)) {
+ contacts.put(contact.getName().toLowerCase(), contact);
+ }
+ }
+ }
+ } else if (condition instanceof AttributeCondition) {
+ String mappedAttributeName = GALFIND_ATTRIBUTE_MAP.get(((ExchangeSession.AttributeCondition) condition).getAttributeName());
+ if (mappedAttributeName != null) {
+ String value = ((ExchangeSession.AttributeCondition) condition).getValue().toLowerCase();
+ Operator operator = ((AttributeCondition) condition).getOperator();
+ String searchValue = value;
+ if (mappedAttributeName.startsWith("EmailAddress")) {
+ searchValue = "smtp:" + searchValue;
+ }
+ if (operator == Operator.IsEqualTo) {
+ searchValue = '=' + searchValue;
+ }
+ ResolveNamesMethod resolveNamesMethod = new ResolveNamesMethod(searchValue);
+ executeMethod(resolveNamesMethod);
+ List<EWSMethod.Item> responses = resolveNamesMethod.getResponseItems();
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("ResolveNames(" + searchValue + ") returned " + responses.size() + " results");
+ }
+ for (EWSMethod.Item response : responses) {
+ Contact contact = buildGalfindContact(response);
+ if (condition.isMatch(contact)) {
+ contacts.put(contact.getName().toLowerCase(), contact);
+ }
+ }
+ }
+ }
+ return contacts;
+ }
+
+ protected Date parseDateFromExchange(String exchangeDateValue) throws DavMailException {
+ Date dateValue = null;
+ if (exchangeDateValue != null) {
+ try {
+ dateValue = getExchangeZuluDateFormat().parse(exchangeDateValue);
+ } catch (ParseException e) {
+ throw new DavMailException("EXCEPTION_INVALID_DATE", exchangeDateValue);
+ }
+ }
+ return dateValue;
+ }
+
+ protected String convertDateFromExchange(String exchangeDateValue) throws DavMailException {
+ String zuluDateValue = null;
+ if (exchangeDateValue != null) {
+ try {
+ zuluDateValue = getZuluDateFormat().format(getExchangeZuluDateFormat().parse(exchangeDateValue));
+ } catch (ParseException e) {
+ throw new DavMailException("EXCEPTION_INVALID_DATE", exchangeDateValue);
+ }
+ }
+ return zuluDateValue;
+ }
+
+ protected String convertCalendarDateToExchange(String vcalendarDateValue) throws DavMailException {
+ String zuluDateValue = null;
+ if (vcalendarDateValue != null) {
+ try {
+ SimpleDateFormat dateParser;
+ if (vcalendarDateValue.length() == 8) {
+ dateParser = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH);
+ } else {
+ dateParser = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ENGLISH);
+ }
+ dateParser.setTimeZone(GMT_TIMEZONE);
+ SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH);
+ dateFormatter.setTimeZone(GMT_TIMEZONE);
+ zuluDateValue = dateFormatter.format(dateParser.parse(vcalendarDateValue));
+ } catch (ParseException e) {
+ throw new DavMailException("EXCEPTION_INVALID_DATE", vcalendarDateValue);
+ }
+ }
+ return zuluDateValue;
+ }
+
+ protected String convertDateFromExchangeToTaskDate(String exchangeDateValue) throws DavMailException {
+ String zuluDateValue = null;
+ if (exchangeDateValue != null) {
+ try {
+ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH);
+ dateFormat.setTimeZone(GMT_TIMEZONE);
+ zuluDateValue = dateFormat.format(getExchangeZuluDateFormat().parse(exchangeDateValue));
+ } catch (ParseException e) {
+ throw new DavMailException("EXCEPTION_INVALID_DATE", exchangeDateValue);
+ }
+ }
+ return zuluDateValue;
+ }
+
+ protected String convertTaskDateToZulu(String value) {
+ String result = null;
+ if (value != null && value.length() > 0) {
+ try {
+ SimpleDateFormat parser;
+ if (value.length() == 8) {
+ parser = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH);
+ parser.setTimeZone(GMT_TIMEZONE);
+ } else if (value.length() == 15) {
+ parser = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ENGLISH);
+ parser.setTimeZone(GMT_TIMEZONE);
+ } else if (value.length() == 16) {
+ parser = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.ENGLISH);
+ parser.setTimeZone(GMT_TIMEZONE);
+ } else {
+ parser = ExchangeSession.getExchangeZuluDateFormat();
+ }
+ Calendar calendarValue = Calendar.getInstance(GMT_TIMEZONE);
+ calendarValue.setTime(parser.parse(value));
+ // zulu time: add 12 hours
+ if (value.length() == 16) {
+ calendarValue.add(Calendar.HOUR, 12);
+ }
+ calendarValue.set(Calendar.HOUR, 0);
+ calendarValue.set(Calendar.MINUTE, 0);
+ calendarValue.set(Calendar.SECOND, 0);
+ result = ExchangeSession.getExchangeZuluDateFormat().format(calendarValue.getTime());
+ } catch (ParseException e) {
+ LOGGER.warn("Invalid date: " + value);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Format date to exchange search format.
+ *
+ * @param date date object
+ * @return formatted search date
+ */
+ @Override
+ public String formatSearchDate(Date date) {
+ SimpleDateFormat dateFormatter = new SimpleDateFormat(YYYY_MM_DD_T_HHMMSS_Z, Locale.ENGLISH);
+ dateFormatter.setTimeZone(GMT_TIMEZONE);
+ return dateFormatter.format(date);
+ }
+
+ protected static boolean isItemId(String itemName) {
+ return itemName.length() >= 152;
+ }
+
+
+ protected static final Map<String, String> importanceToPriorityMap = new HashMap<String, String>();
+
+ static {
+ importanceToPriorityMap.put("High", "1");
+ importanceToPriorityMap.put("Normal", "5");
+ importanceToPriorityMap.put("Low", "9");
+ }
+
+ protected static final Map<String, String> priorityToImportanceMap = new HashMap<String, String>();
+
+ static {
+ priorityToImportanceMap.put("1", "High");
+ priorityToImportanceMap.put("5", "Normal");
+ priorityToImportanceMap.put("9", "Low");
+ }
+
+ protected String convertPriorityFromExchange(String exchangeImportanceValue) {
+ String value = null;
+ if (exchangeImportanceValue != null) {
+ value = importanceToPriorityMap.get(exchangeImportanceValue);
+ }
+ return value;
+ }
+
+ protected String convertPriorityToExchange(String vTodoPriorityValue) {
+ String value = null;
+ if (vTodoPriorityValue != null) {
+ value = priorityToImportanceMap.get(vTodoPriorityValue);
+ }
+ return value;
+ }
+}
+
diff --git a/src/java/davmail/exchange/ews/ExtendedFieldURI.java b/src/java/davmail/exchange/ews/ExtendedFieldURI.java
new file mode 100644
index 0000000..9571801
--- /dev/null
+++ b/src/java/davmail/exchange/ews/ExtendedFieldURI.java
@@ -0,0 +1,172 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+import davmail.util.StringUtil;
+
+/**
+ * Extended MAPI property.
+ */
+public class ExtendedFieldURI implements FieldURI {
+
+ @SuppressWarnings({"UnusedDeclaration"})
+ protected enum PropertyType {
+ ApplicationTime, ApplicationTimeArray, Binary, BinaryArray, Boolean, CLSID, CLSIDArray, Currency, CurrencyArray,
+ Double, DoubleArray, Error, Float, FloatArray, Integer, IntegerArray, Long, LongArray, Null, Object,
+ ObjectArray, Short, ShortArray, SystemTime, SystemTimeArray, String, StringArray
+ }
+
+ @SuppressWarnings({"UnusedDeclaration"})
+ protected static enum DistinguishedPropertySetType {
+ Meeting, Appointment, Common, PublicStrings, Address, InternetHeaders, CalendarAssistant, UnifiedMessaging, Task
+ }
+
+
+ protected String propertyTag;
+ protected DistinguishedPropertySetType distinguishedPropertySetId;
+ protected String propertySetId;
+ protected String propertyName;
+ protected int propertyId;
+ protected final PropertyType propertyType;
+
+ /**
+ * Create extended field uri.
+ *
+ * @param intPropertyTag property tag as int
+ * @param propertyType property type
+ */
+ public ExtendedFieldURI(int intPropertyTag, PropertyType propertyType) {
+ this.propertyTag = "0x" + Integer.toHexString(intPropertyTag);
+ this.propertyType = propertyType;
+ }
+
+ /**
+ * Create extended field uri.
+ *
+ * @param distinguishedPropertySetId distinguished property set id
+ * @param propertyId property id
+ * @param propertyType property type
+ */
+ public ExtendedFieldURI(DistinguishedPropertySetType distinguishedPropertySetId, int propertyId, PropertyType propertyType) {
+ this.distinguishedPropertySetId = distinguishedPropertySetId;
+ this.propertyId = propertyId;
+ this.propertyType = propertyType;
+ }
+
+ /**
+ * Create extended field uri.
+ *
+ * @param distinguishedPropertySetId distinguished property set id
+ * @param propertyName property name
+ */
+ public ExtendedFieldURI(DistinguishedPropertySetType distinguishedPropertySetId, String propertyName) {
+ this.distinguishedPropertySetId = distinguishedPropertySetId;
+ this.propertyName = propertyName;
+ this.propertyType = PropertyType.String;
+ }
+
+ /**
+ * Create extended field uri.
+ *
+ * @param distinguishedPropertySetId distinguished property set id
+ * @param propertyName property name
+ * @param propertyType property type
+ */
+ public ExtendedFieldURI(DistinguishedPropertySetType distinguishedPropertySetId, String propertyName, PropertyType propertyType) {
+ this.distinguishedPropertySetId = distinguishedPropertySetId;
+ this.propertyName = propertyName;
+ this.propertyType = propertyType;
+ }
+
+ public void appendTo(StringBuilder buffer) {
+ buffer.append("<t:ExtendedFieldURI");
+ if (propertyTag != null) {
+ buffer.append(" PropertyTag=\"").append(propertyTag).append('"');
+ }
+ if (distinguishedPropertySetId != null) {
+ buffer.append(" DistinguishedPropertySetId=\"").append(distinguishedPropertySetId).append('"');
+ }
+ if (propertySetId != null) {
+ buffer.append(" PropertySetId=\"").append(propertySetId).append('"');
+ }
+ if (propertyName != null) {
+ buffer.append(" PropertyName=\"").append(propertyName).append('"');
+ }
+ if (propertyId != 0) {
+ buffer.append(" PropertyId=\"").append(String.valueOf(propertyId)).append('"');
+ }
+ if (propertyType != null) {
+ buffer.append(" PropertyType=\"").append(propertyType.toString()).append('"');
+ }
+ buffer.append("/>");
+ }
+
+ public void appendValue(StringBuilder buffer, String itemType, String value) {
+ if (itemType != null) {
+ appendTo(buffer);
+ buffer.append("<t:");
+ buffer.append(itemType);
+ buffer.append('>');
+ }
+ buffer.append("<t:ExtendedProperty>");
+ appendTo(buffer);
+ if (propertyType == PropertyType.StringArray) {
+ buffer.append("<t:Values>");
+ String[] values = value.split("\n");
+ for (final String singleValue : values) {
+ buffer.append("<t:Value>");
+ buffer.append(StringUtil.xmlEncode(singleValue));
+ buffer.append("</t:Value>");
+
+ }
+ buffer.append("</t:Values>");
+ } else {
+ buffer.append("<t:Value>");
+ if ("0x10f3".equals(propertyTag)) {
+ buffer.append(StringUtil.xmlEncode(StringUtil.encodeUrlcompname(value)));
+ } else {
+ buffer.append(StringUtil.xmlEncode(value));
+ }
+ buffer.append("</t:Value>");
+ }
+ buffer.append("</t:ExtendedProperty>");
+ if (itemType != null) {
+ buffer.append("</t:");
+ buffer.append(itemType);
+ buffer.append('>');
+ }
+ }
+
+ /**
+ * Field name in EWS response.
+ *
+ * @return field name in response
+ */
+ public String getResponseName() {
+ if (propertyTag != null) {
+ return propertyTag;
+ } else if (propertyName != null) {
+ return propertyName;
+ } else {
+ return String.valueOf(propertyId);
+ }
+ }
+
+}
+
diff --git a/src/java/davmail/exchange/ews/Field.java b/src/java/davmail/exchange/ews/Field.java
new file mode 100644
index 0000000..af9daa4
--- /dev/null
+++ b/src/java/davmail/exchange/ews/Field.java
@@ -0,0 +1,262 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * EWS MAPI fields;
+ */
+public final class Field {
+ private static final Map<String, FieldURI> FIELD_MAP = new HashMap<String, FieldURI>();
+
+ private Field() {
+ }
+
+ static {
+ // items
+ FIELD_MAP.put("etag", new ExtendedFieldURI(0x3008, ExtendedFieldURI.PropertyType.SystemTime));
+ FIELD_MAP.put("displayname", new ExtendedFieldURI(0x3001, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("urlcompname", new ExtendedFieldURI(0x10f3, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("lastmodified", new ExtendedFieldURI(0x3008, ExtendedFieldURI.PropertyType.SystemTime));
+ FIELD_MAP.put("created", new ExtendedFieldURI(0x3007, ExtendedFieldURI.PropertyType.SystemTime));
+
+ // folder
+ FIELD_MAP.put("ctag", new ExtendedFieldURI(0x670a, ExtendedFieldURI.PropertyType.SystemTime)); // PR_LOCAL_COMMIT_TIME_MAX
+ FIELD_MAP.put("unread", new ExtendedFieldURI(0x3603, ExtendedFieldURI.PropertyType.Integer)); // PR_CONTENT_UNREAD
+ FIELD_MAP.put("hassubs", new ExtendedFieldURI(0x360a, ExtendedFieldURI.PropertyType.Boolean)); // PR_SUBFOLDERS
+ FIELD_MAP.put("folderDisplayName", new UnindexedFieldURI("folder:DisplayName"));
+
+ FIELD_MAP.put("uidNext", new ExtendedFieldURI(0x6751, ExtendedFieldURI.PropertyType.Integer)); // PR_ARTICLE_NUM_NEXT
+ FIELD_MAP.put("highestUid", new ExtendedFieldURI(0x6752, ExtendedFieldURI.PropertyType.Integer)); // PR_IMAP_LAST_ARTICLE_ID
+
+ FIELD_MAP.put("permanenturl", new ExtendedFieldURI(0x670E, ExtendedFieldURI.PropertyType.String)); //PR_FLAT_URL_NAME
+ FIELD_MAP.put("instancetype", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.PublicStrings, "urn:schemas:calendar:instancetype", ExtendedFieldURI.PropertyType.Integer));
+ //FIELD_MAP.put("dtstart", new ExtendedFieldURI(0x10C3, ExtendedFieldURI.PropertyType.SystemTime));
+ //FIELD_MAP.put("dtend", new ExtendedFieldURI(0x10C4, ExtendedFieldURI.PropertyType.SystemTime));
+ FIELD_MAP.put("dtstart", new UnindexedFieldURI("calendar:Start"));
+ FIELD_MAP.put("dtend", new UnindexedFieldURI("calendar:End"));
+
+ FIELD_MAP.put("mimeContent", new UnindexedFieldURI("item:MimeContent"));
+
+ // use PR_RECORD_KEY as unique key
+ FIELD_MAP.put("uid", new ExtendedFieldURI(0x0FF9, ExtendedFieldURI.PropertyType.Binary));
+ FIELD_MAP.put("messageFlags", new ExtendedFieldURI(0x0e07, ExtendedFieldURI.PropertyType.Integer));//PR_MESSAGE_FLAGS
+ FIELD_MAP.put("imapUid", new ExtendedFieldURI(0x0e23, ExtendedFieldURI.PropertyType.Integer));
+ FIELD_MAP.put("flagStatus", new ExtendedFieldURI(0x1090, ExtendedFieldURI.PropertyType.Integer));
+ FIELD_MAP.put("lastVerbExecuted", new ExtendedFieldURI(0x1081, ExtendedFieldURI.PropertyType.Integer));
+ FIELD_MAP.put("read", new UnindexedFieldURI("message:IsRead"));
+ FIELD_MAP.put("messageSize", new ExtendedFieldURI(0x0e08, ExtendedFieldURI.PropertyType.Integer));
+ FIELD_MAP.put("date", new ExtendedFieldURI(0x0e06, ExtendedFieldURI.PropertyType.SystemTime));
+ // always empty on Exchange 2007
+ //FIELD_MAP.put("messageSize", new ExtendedFieldURI(0x6746, ExtendedFieldURI.PropertyType.Integer)); // PR_MIME_SIZE
+ //FIELD_MAP.put("date", new ExtendedFieldURI(0x65f5, ExtendedFieldURI.PropertyType.SystemTime)); // PR_IMAP_INTERNAL_DATE
+ FIELD_MAP.put("deleted", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Common, 0x8570, ExtendedFieldURI.PropertyType.Integer)); // PidLidImapDeleted
+ FIELD_MAP.put("junk", new ExtendedFieldURI(0x1083, ExtendedFieldURI.PropertyType.Integer));
+
+ FIELD_MAP.put("iconIndex", new ExtendedFieldURI(0x1080, ExtendedFieldURI.PropertyType.Integer));// PR_ICON_INDEX
+ FIELD_MAP.put("datereceived", new ExtendedFieldURI(0x0e06, ExtendedFieldURI.PropertyType.SystemTime));// PR_MESSAGE_DELIVERY_TIME
+ FIELD_MAP.put("bcc", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.InternetHeaders, "bcc"));
+
+ FIELD_MAP.put("to", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.InternetHeaders, "to"));
+ FIELD_MAP.put("cc", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.InternetHeaders, "cc"));
+ FIELD_MAP.put("from", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.InternetHeaders, "from"));
+
+ FIELD_MAP.put("messageheaders", new ExtendedFieldURI(0x007D, ExtendedFieldURI.PropertyType.String)); // PR_TRANSPORT_MESSAGE_HEADERS
+
+ FIELD_MAP.put("contentclass", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.InternetHeaders, "content-class"));
+ FIELD_MAP.put("message-id", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.InternetHeaders, "message-id"));
+
+ FIELD_MAP.put("body", new UnindexedFieldURI("item:Body"));
+
+ // folder
+ FIELD_MAP.put("folderclass", new ExtendedFieldURI(0x3613, ExtendedFieldURI.PropertyType.String));
+
+ // contact
+
+ FIELD_MAP.put("outlookmessageclass", new ExtendedFieldURI(0x001A, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("subject", new ExtendedFieldURI(0x0037, ExtendedFieldURI.PropertyType.String));
+
+ FIELD_MAP.put("middlename", new ExtendedFieldURI(0x3A44, ExtendedFieldURI.PropertyType.String));
+ //FIELD_MAP.put("fileas", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.PublicStrings, "urn:schemas:contacts:fileas"));
+ FIELD_MAP.put("fileas", new UnindexedFieldURI("contacts:FileAs"));
+
+ FIELD_MAP.put("homepostaladdress", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Address, 0x801A, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("otherpostaladdress", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Address, 0x801C, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("mailingaddressid", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Address, 0x8022, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("workaddress", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Address, 0x801B, ExtendedFieldURI.PropertyType.String));
+
+ FIELD_MAP.put("alternaterecipient", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.PublicStrings, "urn:schemas:contacts:alternaterecipient"));
+
+ FIELD_MAP.put("extensionattribute1", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Address, 0x804F, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("extensionattribute2", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Address, 0x8050, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("extensionattribute3", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Address, 0x8051, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("extensionattribute4", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Address, 0x8052, ExtendedFieldURI.PropertyType.String));
+
+ FIELD_MAP.put("bday", new ExtendedFieldURI(0x3A42, ExtendedFieldURI.PropertyType.SystemTime));
+ FIELD_MAP.put("anniversary", new ExtendedFieldURI(0x3A41, ExtendedFieldURI.PropertyType.SystemTime));
+ FIELD_MAP.put("businesshomepage", new ExtendedFieldURI(0x3A51, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("personalHomePage", new ExtendedFieldURI(0x3A50, ExtendedFieldURI.PropertyType.String));
+
+ FIELD_MAP.put("cn", new ExtendedFieldURI(0x3001, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("co", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Address, 0x8049, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("department", new ExtendedFieldURI(0x3A18, ExtendedFieldURI.PropertyType.String));
+
+ /*
+ FIELD_MAP.put("email1", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Address, 0x8083, ExtendedFieldURI.PropertyType.String)); // Email1EmailAddress
+ FIELD_MAP.put("email2", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Address, 0x8093, ExtendedFieldURI.PropertyType.String)); // Email2EmailAddress
+ FIELD_MAP.put("email3", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Address, 0x80A3, ExtendedFieldURI.PropertyType.String)); // Email3EmailAddress
+
+ FIELD_MAP.put("smtpemail1", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Address, 0x8084, ExtendedFieldURI.PropertyType.String)); // Email1OriginalDisplayName
+ FIELD_MAP.put("smtpemail2", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Address, 0x8094, ExtendedFieldURI.PropertyType.String)); // Email2OriginalDisplayName
+ FIELD_MAP.put("smtpemail3", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Address, 0x80A4, ExtendedFieldURI.PropertyType.String)); // Email3OriginalDisplayName
+
+ FIELD_MAP.put("displayemail1", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Address, 0x8080, ExtendedFieldURI.PropertyType.String)); // Email1DisplayName
+ FIELD_MAP.put("displayemail2", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Address, 0x8090, ExtendedFieldURI.PropertyType.String)); // Email2DisplayName
+ FIELD_MAP.put("displayemail3", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Address, 0x80A0, ExtendedFieldURI.PropertyType.String)); // Email3DisplayName
+ */
+ FIELD_MAP.put("smtpemail1", new IndexedFieldURI("contacts:EmailAddress", "EmailAddress1", "Contact", "EmailAddresses"));
+ FIELD_MAP.put("smtpemail2", new IndexedFieldURI("contacts:EmailAddress", "EmailAddress2", "Contact", "EmailAddresses"));
+ FIELD_MAP.put("smtpemail3", new IndexedFieldURI("contacts:EmailAddress", "EmailAddress3", "Contact", "EmailAddresses"));
+
+ FIELD_MAP.put("facsimiletelephonenumber", new ExtendedFieldURI(0x3A24, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("givenName", new ExtendedFieldURI(0x3A06, ExtendedFieldURI.PropertyType.String));
+
+ FIELD_MAP.put("homepostofficebox", new ExtendedFieldURI(0x3A5E, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("homeCity", new ExtendedFieldURI(0x3A59, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("homeCountry", new ExtendedFieldURI(0x3A5A, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("homePhone", new ExtendedFieldURI(0x3A09, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("homePostalCode", new ExtendedFieldURI(0x3A5B, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("homeState", new ExtendedFieldURI(0x3A5C, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("homeStreet", new ExtendedFieldURI(0x3A5D, ExtendedFieldURI.PropertyType.String));
+
+ FIELD_MAP.put("l", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Address, 0x8046, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("manager", new ExtendedFieldURI(0x3A4E, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("mobile", new ExtendedFieldURI(0x3A1C, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("namesuffix", new ExtendedFieldURI(0x3A05, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("nickname", new ExtendedFieldURI(0x3A4F, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("o", new ExtendedFieldURI(0x3A16, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("pager", new ExtendedFieldURI(0x3A21, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("personaltitle", new ExtendedFieldURI(0x3A45, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("postalcode", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Address, 0x8048, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("postofficebox", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Address, 0x804A, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("profession", new ExtendedFieldURI(0x3A46, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("roomnumber", new ExtendedFieldURI(0x3A19, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("secretarycn", new ExtendedFieldURI(0x3A30, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("sn", new ExtendedFieldURI(0x3A11, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("spousecn", new ExtendedFieldURI(0x3A48, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("st", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Address, 0x8047, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("street", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Address, 0x8045, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("telephoneNumber", new ExtendedFieldURI(0x3A08, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("title", new ExtendedFieldURI(0x3A17, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("description", new ExtendedFieldURI(0x1000, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("im", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Address, 0x8062, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("othermobile", new ExtendedFieldURI(0x3A1E, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("internationalisdnnumber", new ExtendedFieldURI(0x3A2D, ExtendedFieldURI.PropertyType.String));
+
+ FIELD_MAP.put("otherTelephone", new ExtendedFieldURI(0x3A21, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("homefax", new ExtendedFieldURI(0x3A25, ExtendedFieldURI.PropertyType.String));
+
+ FIELD_MAP.put("otherstreet", new ExtendedFieldURI(0x3A63, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("otherstate", new ExtendedFieldURI(0x3A62, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("otherpostofficebox", new ExtendedFieldURI(0x3A64, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("otherpostalcode", new ExtendedFieldURI(0x3A61, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("othercountry", new ExtendedFieldURI(0x3A60, ExtendedFieldURI.PropertyType.String));
+ FIELD_MAP.put("othercity", new ExtendedFieldURI(0x3A5F, ExtendedFieldURI.PropertyType.String));
+
+ FIELD_MAP.put("gender", new ExtendedFieldURI(0x3A4D, ExtendedFieldURI.PropertyType.Short));
+
+ FIELD_MAP.put("keywords", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.PublicStrings, "Keywords", ExtendedFieldURI.PropertyType.StringArray));
+
+ FIELD_MAP.put("private", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Common, 0x8506, ExtendedFieldURI.PropertyType.Boolean));
+ FIELD_MAP.put("sensitivity", new ExtendedFieldURI(0x0036, ExtendedFieldURI.PropertyType.Integer));
+
+ FIELD_MAP.put("haspicture", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Address, 0x8015, ExtendedFieldURI.PropertyType.Boolean));
+
+ FIELD_MAP.put("fburl", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Address, 0x80D8, ExtendedFieldURI.PropertyType.String));
+
+ // calendar
+ FIELD_MAP.put("processed", new ExtendedFieldURI(0x65e8, ExtendedFieldURI.PropertyType.Boolean));
+
+ FIELD_MAP.put("reminderset", new UnindexedFieldURI("item:ReminderIsSet"));
+ FIELD_MAP.put("ismeeting", new UnindexedFieldURI("item:IsMeeting"));
+ FIELD_MAP.put("apptstateflags", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Appointment, 0x8217, ExtendedFieldURI.PropertyType.Integer)); // PidLidAppointmentStateFlags 1: Meeting, 2: Received, 4: Cancelled
+ FIELD_MAP.put("appointmentstate", new UnindexedFieldURI("calendar:AppointmentState"));
+ FIELD_MAP.put("myresponsetype", new UnindexedFieldURI("calendar:MyResponseType"));
+ FIELD_MAP.put("calendaruid", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.PublicStrings, "urn:schemas:calendar:uid", ExtendedFieldURI.PropertyType.String));
+
+ FIELD_MAP.put("meetingtimezone", new UnindexedFieldURI("calendar:MeetingTimeZone"));
+ FIELD_MAP.put("starttimezone", new UnindexedFieldURI("calendar:StartTimeZone"));
+ FIELD_MAP.put("busystatus", new UnindexedFieldURI("calendar:LegacyFreeBusyStatus"));
+
+ FIELD_MAP.put("requiredattendees", new UnindexedFieldURI("calendar:RequiredAttendees"));
+ FIELD_MAP.put("optionalattendees", new UnindexedFieldURI("calendar:OptionalAttendees"));
+ FIELD_MAP.put("modifiedoccurrences", new UnindexedFieldURI("calendar:ModifiedOccurrences"));
+
+ FIELD_MAP.put("isrecurring", new UnindexedFieldURI("calendar:IsRecurring"));
+ FIELD_MAP.put("calendaritemtype", new UnindexedFieldURI("calendar:CalendarItemType"));
+
+ FIELD_MAP.put("xmozlastack", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.PublicStrings, "xmozlastack"));
+ FIELD_MAP.put("xmozsnoozetime", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.PublicStrings, "xmozsnoozetime"));
+ FIELD_MAP.put("xmozsendinvitations", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.PublicStrings, "xmozsendinvitations"));
+
+ // task
+ FIELD_MAP.put("importance", new UnindexedFieldURI("item:Importance"));
+ FIELD_MAP.put("percentcomplete", new UnindexedFieldURI("task:PercentComplete"));
+ FIELD_MAP.put("taskstatus", new UnindexedFieldURI("task:Status"));
+
+ FIELD_MAP.put("startdate", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Task, 0x8104, ExtendedFieldURI.PropertyType.SystemTime));
+ FIELD_MAP.put("duedate", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Task, 0x8105, ExtendedFieldURI.PropertyType.SystemTime));
+ FIELD_MAP.put("datecompleted", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Task, 0x810F, ExtendedFieldURI.PropertyType.SystemTime));
+ FIELD_MAP.put("iscomplete", new UnindexedFieldURI("task:IsComplete"));
+
+ FIELD_MAP.put("commonstart", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Task, 0x8516, ExtendedFieldURI.PropertyType.SystemTime));
+ FIELD_MAP.put("commonend", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.Task, 0x8517, ExtendedFieldURI.PropertyType.SystemTime));
+
+ // attachments
+ FIELD_MAP.put("attachments", new UnindexedFieldURI("item:Attachments"));
+ }
+
+ /**
+ * Get Field by alias.
+ *
+ * @param alias field alias
+ * @return field
+ */
+ public static FieldURI get(String alias) {
+ FieldURI field = FIELD_MAP.get(alias);
+ if (field == null) {
+ throw new IllegalArgumentException("Unknown field: " + alias);
+ }
+ return field;
+ }
+
+ /**
+ * Create property update field
+ *
+ * @param alias property alias
+ * @param value property value
+ * @return field update
+ */
+ public static FieldUpdate createFieldUpdate(String alias, String value) {
+ return new FieldUpdate(Field.get(alias), value);
+ }
+
+}
diff --git a/src/java/davmail/exchange/ews/FieldURI.java b/src/java/davmail/exchange/ews/FieldURI.java
new file mode 100644
index 0000000..2cecb6d
--- /dev/null
+++ b/src/java/davmail/exchange/ews/FieldURI.java
@@ -0,0 +1,49 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+/**
+ * Field URI.
+ */
+public interface FieldURI {
+
+ /**
+ * Append field to buffer
+ *
+ * @param buffer current buffer
+ */
+ void appendTo(StringBuilder buffer);
+
+ /**
+ * Append updated field value to buffer
+ *
+ * @param buffer current buffer
+ * @param itemType item type
+ * @param value field value
+ */
+ void appendValue(StringBuilder buffer, String itemType, String value);
+
+ /**
+ * Property name in EWS response.
+ *
+ * @return property name
+ */
+ String getResponseName();
+
+}
diff --git a/src/java/davmail/exchange/ews/FieldUpdate.java b/src/java/davmail/exchange/ews/FieldUpdate.java
new file mode 100644
index 0000000..6df9a6a
--- /dev/null
+++ b/src/java/davmail/exchange/ews/FieldUpdate.java
@@ -0,0 +1,86 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * Field update
+ */
+public class FieldUpdate {
+ FieldURI fieldURI;
+ String value;
+
+ /**
+ * Create field update with value.
+ *
+ * @param fieldURI target field
+ * @param value field value
+ */
+ public FieldUpdate(FieldURI fieldURI, String value) {
+ this.fieldURI = fieldURI;
+ this.value = value;
+ }
+
+ protected FieldUpdate() {
+ // empty constructor for subclass
+ }
+
+ /**
+ * Write field to request writer.
+ *
+ * @param itemType item type
+ * @param writer request writer
+ * @throws IOException on error
+ */
+ public void write(String itemType, Writer writer) throws IOException {
+ String action;
+ //noinspection VariableNotUsedInsideIf
+ if (value == null) {
+ action = "Delete";
+ } else {
+ action = "Set";
+ }
+ if (itemType != null) {
+ writer.write("<t:");
+ writer.write(action);
+ writer.write(itemType);
+ writer.write("Field>");
+ }
+
+ // do not try to set empty value on create
+ if (itemType != null || value != null) {
+ StringBuilder buffer = new StringBuilder();
+ if (value == null) {
+ fieldURI.appendTo(buffer);
+ } else {
+ fieldURI.appendValue(buffer, itemType, value);
+ }
+ writer.write(buffer.toString());
+ }
+
+ if (itemType != null) {
+ writer.write("</t:");
+ writer.write(action);
+ writer.write(itemType);
+ writer.write("Field>");
+ }
+ }
+}
diff --git a/src/java/davmail/exchange/ews/FileAttachment.java b/src/java/davmail/exchange/ews/FileAttachment.java
new file mode 100644
index 0000000..d7c4f7c
--- /dev/null
+++ b/src/java/davmail/exchange/ews/FileAttachment.java
@@ -0,0 +1,92 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * File Attachment.
+ */
+public class FileAttachment {
+ protected String name;
+ protected String contentType;
+ protected String content;
+ protected String attachmentId;
+ protected boolean isContactPhoto;
+
+ /**
+ * Default constructor
+ */
+ public FileAttachment() {
+ // empty constructor
+ }
+
+ /**
+ * Build file attachment.
+ *
+ * @param name attachment name
+ * @param contentType content type
+ * @param content body as string
+ */
+ public FileAttachment(String name, String contentType, String content) {
+ this.name = name;
+ this.contentType = contentType;
+ this.content = content;
+ }
+
+ /**
+ * Write XML content to writer.
+ *
+ * @param writer writer
+ * @throws IOException on error
+ */
+ public void write(Writer writer) throws IOException {
+ writer.write("<t:FileAttachment>");
+ if (name != null) {
+ writer.write("<t:Name>");
+ writer.write(name);
+ writer.write("</t:Name>");
+ }
+ if (contentType != null) {
+ writer.write("<t:ContentType>");
+ writer.write(contentType);
+ writer.write("</t:ContentType>");
+ }
+ if (isContactPhoto) {
+ writer.write("<t:IsContactPhoto>true</t:IsContactPhoto>");
+ }
+ if (content != null) {
+ writer.write("<t:Content>");
+ writer.write(content);
+ writer.write("</t:Content>");
+ }
+ writer.write("</t:FileAttachment>");
+ }
+
+ /**
+ * Exchange 2010 only: set contact photo flag on attachment.
+ *
+ * @param isContactPhoto contact photo flag
+ */
+ public void setIsContactPhoto(boolean isContactPhoto) {
+ this.isContactPhoto = isContactPhoto;
+ }
+
+}
diff --git a/src/java/davmail/exchange/ews/FindFolderMethod.java b/src/java/davmail/exchange/ews/FindFolderMethod.java
new file mode 100644
index 0000000..3c3ab22
--- /dev/null
+++ b/src/java/davmail/exchange/ews/FindFolderMethod.java
@@ -0,0 +1,58 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+import java.util.Set;
+
+/**
+ * EWS Find Folder.
+ */
+public class FindFolderMethod extends EWSMethod {
+
+ /**
+ * Find Exchange Folder.
+ *
+ * @param traversal traversal type
+ * @param baseShape base shape
+ * @param parentFolderId parent folder id
+ * @param additionalProperties folder properties
+ */
+ public FindFolderMethod(FolderQueryTraversal traversal, BaseShape baseShape, FolderId parentFolderId, Set<FieldURI> additionalProperties) {
+ super("Folder", "FindFolder");
+ this.traversal = traversal;
+ this.baseShape = baseShape;
+ this.parentFolderId = parentFolderId;
+ this.additionalProperties = additionalProperties;
+ }
+
+ /**
+ * Find Exchange Folder.
+ *
+ * @param traversal traversal type
+ * @param baseShape base shape
+ * @param parentFolderId parent folder id
+ * @param additionalProperties folder properties
+ * @param searchExpression search expression
+ */
+ public FindFolderMethod(FolderQueryTraversal traversal, BaseShape baseShape, FolderId parentFolderId,
+ Set<FieldURI> additionalProperties, SearchExpression searchExpression) {
+ this(traversal, baseShape, parentFolderId, additionalProperties);
+ this.searchExpression = searchExpression;
+ }
+}
diff --git a/src/java/davmail/exchange/ews/FindItemMethod.java b/src/java/davmail/exchange/ews/FindItemMethod.java
new file mode 100644
index 0000000..033d287
--- /dev/null
+++ b/src/java/davmail/exchange/ews/FindItemMethod.java
@@ -0,0 +1,43 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+/**
+ * EWS Find Item Method.
+ */
+public class FindItemMethod extends EWSMethod {
+ /**
+ * Find item method.
+ *
+ * @param traversal folder traversal mode
+ * @param baseShape base item shape
+ * @param parentFolderId parent folder id
+ * @param offset start offset
+ * @param maxCount maximum result count
+ */
+ public FindItemMethod(FolderQueryTraversal traversal, BaseShape baseShape, FolderId parentFolderId, int offset, int maxCount) {
+ super("Item", "FindItem");
+ this.traversal = traversal;
+ this.baseShape = baseShape;
+ this.parentFolderId = parentFolderId;
+ this.offset = offset;
+ this.maxCount = maxCount;
+ }
+}
+
diff --git a/src/java/davmail/exchange/ews/FolderId.java b/src/java/davmail/exchange/ews/FolderId.java
new file mode 100644
index 0000000..18da02d
--- /dev/null
+++ b/src/java/davmail/exchange/ews/FolderId.java
@@ -0,0 +1,90 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * Folder Id.
+ */
+public class FolderId extends Option {
+ protected String changeKey;
+ protected String mailbox;
+
+ /**
+ * Create FolderId with specified tag name.
+ *
+ * @param name field tag name
+ * @param value id value
+ * @param changeKey folder change key
+ * @param mailbox shared mailbox name
+ */
+ protected FolderId(String name, String value, String changeKey, String mailbox) {
+ this(name, value, changeKey);
+ this.mailbox = mailbox;
+ }
+
+ /**
+ * Create FolderId with specified tag name.
+ *
+ * @param name field tag name
+ * @param value id value
+ * @param changeKey folder change key
+ */
+ protected FolderId(String name, String value, String changeKey) {
+ super(name, value);
+ this.changeKey = changeKey;
+ }
+
+ /**
+ * Build Folder id from response item.
+ *
+ * @param item response item
+ */
+ public FolderId(EWSMethod.Item item) {
+ this("t:FolderId", item.get("FolderId"), item.get("ChangeKey"));
+ }
+
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public void write(Writer writer) throws IOException {
+ writer.write('<');
+ writer.write(name);
+ writer.write(" Id=\"");
+ writer.write(value);
+ if (changeKey != null) {
+ writer.write("\" ChangeKey=\"");
+ writer.write(changeKey);
+ }
+ if (mailbox == null) {
+ writer.write("\"/>");
+ } else {
+ writer.write("\"><t:Mailbox><t:EmailAddress>");
+ writer.write(mailbox);
+ writer.write("</t:EmailAddress></t:Mailbox></");
+ writer.write(name);
+ writer.write('>');
+ }
+ }
+
+}
diff --git a/src/java/davmail/exchange/ews/FolderQueryTraversal.java b/src/java/davmail/exchange/ews/FolderQueryTraversal.java
new file mode 100644
index 0000000..5677ca7
--- /dev/null
+++ b/src/java/davmail/exchange/ews/FolderQueryTraversal.java
@@ -0,0 +1,39 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+/**
+ * Folder folderQueryTraversalType search mode.
+ */
+public final class FolderQueryTraversal extends AttributeOption {
+
+ private FolderQueryTraversal(String value) {
+ super("Traversal", value);
+ }
+
+ /**
+ * Search only in current folder.
+ */
+ public static final FolderQueryTraversal SHALLOW = new FolderQueryTraversal("Shallow");
+ /**
+ * Recursive search.
+ */
+ @SuppressWarnings({"UnusedDeclaration"})
+ public static final FolderQueryTraversal DEEP = new FolderQueryTraversal("Deep");
+}
\ No newline at end of file
diff --git a/src/java/davmail/exchange/ews/GetAttachmentMethod.java b/src/java/davmail/exchange/ews/GetAttachmentMethod.java
new file mode 100644
index 0000000..64779af
--- /dev/null
+++ b/src/java/davmail/exchange/ews/GetAttachmentMethod.java
@@ -0,0 +1,35 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+/**
+ * Get Attachment Method.
+ */
+public class GetAttachmentMethod extends EWSMethod {
+
+ /**
+ * Get Attachment Method.
+ *
+ * @param attachmentId attachment id
+ */
+ public GetAttachmentMethod(String attachmentId) {
+ super("Attachment", "GetAttachment");
+ this.attachmentId = attachmentId;
+ }
+}
diff --git a/src/java/davmail/exchange/ews/GetFolderMethod.java b/src/java/davmail/exchange/ews/GetFolderMethod.java
new file mode 100644
index 0000000..b93b1e0
--- /dev/null
+++ b/src/java/davmail/exchange/ews/GetFolderMethod.java
@@ -0,0 +1,42 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+import java.util.Set;
+
+/**
+ * EWS GetFolder method.
+ */
+public class GetFolderMethod extends EWSMethod {
+
+ /**
+ * Get folder method.
+ *
+ * @param baseShape base requested shape
+ * @param folderId folder id
+ * @param additionalProperties additional requested properties
+ */
+ public GetFolderMethod(BaseShape baseShape, FolderId folderId, Set<FieldURI> additionalProperties) {
+ super("Folder", "GetFolder");
+ this.baseShape = baseShape;
+ this.folderId = folderId;
+ this.additionalProperties = additionalProperties;
+ }
+
+}
diff --git a/src/java/davmail/exchange/ews/GetItemMethod.java b/src/java/davmail/exchange/ews/GetItemMethod.java
new file mode 100644
index 0000000..86648b2
--- /dev/null
+++ b/src/java/davmail/exchange/ews/GetItemMethod.java
@@ -0,0 +1,39 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+/**
+ * Get Item method.
+ */
+public class GetItemMethod extends EWSMethod {
+ /**
+ * Get item method.
+ *
+ * @param baseShape base requested shape
+ * @param itemId item id
+ * @param includeMimeContent return mime content
+ */
+ public GetItemMethod(BaseShape baseShape, ItemId itemId, boolean includeMimeContent) {
+ super("Item", "GetItem");
+ this.baseShape = baseShape;
+ this.itemId = itemId;
+ this.includeMimeContent = includeMimeContent;
+ }
+
+}
diff --git a/src/java/davmail/exchange/ews/GetUserAvailabilityMethod.java b/src/java/davmail/exchange/ews/GetUserAvailabilityMethod.java
new file mode 100644
index 0000000..68f3e6c
--- /dev/null
+++ b/src/java/davmail/exchange/ews/GetUserAvailabilityMethod.java
@@ -0,0 +1,115 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+import davmail.exchange.XMLStreamUtil;
+
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * GetUserAvailability method.
+ */
+public class GetUserAvailabilityMethod extends EWSMethod {
+ protected final String attendee;
+ protected final String start;
+ protected final String end;
+ protected String mergedFreeBusy;
+ protected final int interval;
+
+ /**
+ * Build EWS method
+ *
+ * @param attendee attendee email address
+ * @param start start date in Exchange zulu format
+ * @param end end date in Exchange zulu format
+ * @param interval freebusy interval in minutes
+ */
+ public GetUserAvailabilityMethod(String attendee, String start, String end, int interval) {
+ super("FreeBusy", "GetUserAvailabilityRequest");
+ this.attendee = attendee;
+ this.start = start;
+ this.end = end;
+ this.interval = interval;
+ }
+
+ @Override
+ protected void writeSoapBody(Writer writer) throws IOException {
+ // write UTC timezone
+ writer.write("<t:TimeZone>" +
+ "<t:Bias>0</t:Bias>" +
+ "<t:StandardTime>" +
+ "<t:Bias>0</t:Bias>" +
+ "<t:Time>02:00:00</t:Time>" +
+ "<t:DayOrder>1</t:DayOrder>" +
+ "<t:Month>3</t:Month>" +
+ "<t:DayOfWeek>Sunday</t:DayOfWeek>" +
+ "</t:StandardTime>" +
+ "<t:DaylightTime>" +
+ "<t:Bias>0</t:Bias>" +
+ "<t:Time>02:00:00</t:Time>" +
+ "<t:DayOrder>1</t:DayOrder>" +
+ "<t:Month>10</t:Month>" +
+ "<t:DayOfWeek>Sunday</t:DayOfWeek>" +
+ "</t:DaylightTime>" +
+ "</t:TimeZone>");
+ // write attendee address
+ writer.write("<m:MailboxDataArray>" +
+ "<t:MailboxData>" +
+ "<t:Email>" +
+ "<t:Address>");
+ writer.write(attendee);
+ writer.write("</t:Address>" +
+ "</t:Email>" +
+ "<t:AttendeeType>Required</t:AttendeeType>" +
+ "</t:MailboxData>" +
+ "</m:MailboxDataArray>");
+ // freebusy range
+ writer.write("<t:FreeBusyViewOptions>" +
+ "<t:TimeWindow>" +
+ "<t:StartTime>");
+ writer.write(start);
+ writer.write("</t:StartTime>" +
+ "<t:EndTime>");
+ writer.write(end);
+ writer.write("</t:EndTime>" +
+ "</t:TimeWindow>" +
+ "<t:MergedFreeBusyIntervalInMinutes>" + interval + "</t:MergedFreeBusyIntervalInMinutes>" +
+ "<t:RequestedView>MergedOnly</t:RequestedView>" +
+ "</t:FreeBusyViewOptions>");
+ }
+
+ @Override
+ protected void handleCustom(XMLStreamReader reader) throws XMLStreamException {
+ if (XMLStreamUtil.isStartTag(reader, "MergedFreeBusy")) {
+ this.mergedFreeBusy = XMLStreamUtil.getElementText(reader);
+ }
+ }
+
+ /**
+ * Get merged freebusy string.
+ *
+ * @return freebusy string
+ */
+ public String getMergedFreeBusy() {
+ return mergedFreeBusy;
+ }
+}
diff --git a/src/java/davmail/exchange/ews/GetUserConfigurationMethod.java b/src/java/davmail/exchange/ews/GetUserConfigurationMethod.java
new file mode 100644
index 0000000..273f20e
--- /dev/null
+++ b/src/java/davmail/exchange/ews/GetUserConfigurationMethod.java
@@ -0,0 +1,87 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+import davmail.exchange.XMLStreamUtil;
+
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * Get User Configuration method.
+ */
+public class GetUserConfigurationMethod extends EWSMethod {
+
+ /**
+ * Get User Configuration method.
+ */
+ public GetUserConfigurationMethod() {
+ super("UserConfiguration", "GetUserConfiguration");
+ folderId = DistinguishedFolderId.getInstance(null, DistinguishedFolderId.Name.root);
+ }
+
+ @Override
+ protected void writeSoapBody(Writer writer) throws IOException {
+ writer.write("<m:UserConfigurationName Name=\"OWA.UserOptions\">");
+ folderId.write(writer);
+ writer.write("</m:UserConfigurationName>");
+ writer.write("<m:UserConfigurationProperties>All</m:UserConfigurationProperties>");
+ }
+
+ @Override
+ protected void handleCustom(XMLStreamReader reader) throws XMLStreamException {
+ if (XMLStreamUtil.isStartTag(reader, "UserConfiguration")) {
+ responseItems.add(handleUserConfiguration(reader));
+ }
+ }
+
+ private Item handleUserConfiguration(XMLStreamReader reader) throws XMLStreamException {
+ Item responseItem = new Item();
+ while (reader.hasNext() && !(XMLStreamUtil.isEndTag(reader, "UserConfiguration"))) {
+ reader.next();
+ if (XMLStreamUtil.isStartTag(reader)) {
+ String tagLocalName = reader.getLocalName();
+ if ("DictionaryEntry".equals(tagLocalName)) {
+ handleDictionaryEntry(reader, responseItem);
+ }
+ }
+ }
+ return responseItem;
+ }
+
+ private void handleDictionaryEntry(XMLStreamReader reader, Item responseItem) throws XMLStreamException {
+ String key = null;
+ while (reader.hasNext() && !(XMLStreamUtil.isEndTag(reader, "DictionaryEntry"))) {
+ reader.next();
+ if (XMLStreamUtil.isStartTag(reader)) {
+ String tagLocalName = reader.getLocalName();
+ if ("Value".equals(tagLocalName)) {
+ if (key == null) {
+ key = reader.getElementText();
+ } else {
+ responseItem.put(key, XMLStreamUtil.getElementText(reader));
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/src/java/davmail/exchange/ews/IndexedFieldURI.java b/src/java/davmail/exchange/ews/IndexedFieldURI.java
new file mode 100644
index 0000000..a30b3d5
--- /dev/null
+++ b/src/java/davmail/exchange/ews/IndexedFieldURI.java
@@ -0,0 +1,72 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+import davmail.util.StringUtil;
+
+/**
+ * Indexed FieldURI
+ */
+public class IndexedFieldURI implements FieldURI {
+ protected final String fieldURI;
+ protected final String fieldIndex;
+ protected final String fieldItemType;
+ protected final String collectionName;
+
+ /**
+ * Create indexed field uri.
+ *
+ * @param fieldURI base field uri
+ * @param fieldIndex field name
+ * @param fieldItemType field item type
+ * @param collectionName collection name
+ */
+ public IndexedFieldURI(String fieldURI, String fieldIndex, String fieldItemType, String collectionName) {
+ this.fieldURI = fieldURI;
+ this.fieldIndex = fieldIndex;
+ this.fieldItemType = fieldItemType;
+ this.collectionName = collectionName;
+ }
+
+ public void appendTo(StringBuilder buffer) {
+ buffer.append("<t:IndexedFieldURI FieldURI=\"").append(fieldURI);
+ buffer.append("\" FieldIndex=\"").append(fieldIndex);
+ buffer.append("\"/>");
+ }
+
+ public void appendValue(StringBuilder buffer, String itemType, String value) {
+ if (itemType != null) {
+ // append IndexedFieldURI
+ appendTo(buffer);
+ buffer.append("<t:").append(fieldItemType).append('>');
+ buffer.append("<t:").append(collectionName).append('>');
+ }
+ buffer.append("<t:Entry Key=\"").append(fieldIndex).append("\">");
+ buffer.append(StringUtil.xmlEncodeAttribute(value));
+ buffer.append("</t:Entry>");
+ if (itemType != null) {
+ buffer.append("</t:").append(collectionName).append('>');
+ buffer.append("</t:").append(fieldItemType).append('>');
+ }
+ }
+
+ public String getResponseName() {
+ return fieldIndex;
+ }
+}
diff --git a/src/java/davmail/exchange/ews/IndexedFieldUpdate.java b/src/java/davmail/exchange/ews/IndexedFieldUpdate.java
new file mode 100644
index 0000000..b340830
--- /dev/null
+++ b/src/java/davmail/exchange/ews/IndexedFieldUpdate.java
@@ -0,0 +1,83 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Field update with multiple values.
+ */
+public class IndexedFieldUpdate extends FieldUpdate {
+ final Set<FieldUpdate> updates = new HashSet<FieldUpdate>();
+ protected final String collectionName;
+
+ /**
+ * Create indexed field update object.
+ *
+ * @param collectionName values collection name e.g. EmailAddresses
+ */
+ public IndexedFieldUpdate(String collectionName) {
+ this.collectionName = collectionName;
+ }
+
+ /**
+ * Add indexed field value.
+ *
+ * @param fieldUpdate field update object
+ */
+ public void addFieldValue(FieldUpdate fieldUpdate) {
+ updates.add(fieldUpdate);
+ }
+
+ /**
+ * Write field to request writer.
+ *
+ * @param itemType item type
+ * @param writer request writer
+ * @throws IOException on error
+ */
+ @Override
+ public void write(String itemType, Writer writer) throws IOException {
+ if (itemType == null) {
+ // use collection name on create
+ writer.write("<t:");
+ writer.write(collectionName);
+ writer.write(">");
+
+ StringBuilder buffer = new StringBuilder();
+ for (FieldUpdate fieldUpdate : updates) {
+ fieldUpdate.fieldURI.appendValue(buffer, null, fieldUpdate.value);
+ }
+ writer.write(buffer.toString());
+
+ writer.write("</t:");
+ writer.write(collectionName);
+ writer.write(">");
+ } else {
+ // on update, write each fieldupdate
+ for (FieldUpdate fieldUpdate : updates) {
+ fieldUpdate.write(itemType, writer);
+ }
+ }
+ }
+
+}
diff --git a/src/java/davmail/exchange/ews/ItemId.java b/src/java/davmail/exchange/ews/ItemId.java
new file mode 100644
index 0000000..86c3d9a
--- /dev/null
+++ b/src/java/davmail/exchange/ews/ItemId.java
@@ -0,0 +1,89 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * Item id.
+ */
+public class ItemId {
+ protected final String name;
+ protected final String id;
+ protected final String changeKey;
+
+ /**
+ * Build Item id from response item.
+ *
+ * @param item response item
+ */
+ public ItemId(EWSMethod.Item item) {
+ this("ItemId", item);
+ }
+
+ /**
+ * Build Item id object from item id.
+ *
+ * @param itemId item id
+ */
+ public ItemId(String itemId) {
+ this("ItemId", itemId);
+ }
+
+ /**
+ * Build Item id from response item.
+ *
+ * @param item response item
+ */
+ public ItemId(String name, EWSMethod.Item item) {
+ this.name = name;
+ this.id = item.get("ItemId");
+ this.changeKey = item.get("ChangeKey");
+ }
+
+ /**
+ * Build Item id object from item id.
+ *
+ * @param itemId item id
+ */
+ public ItemId(String name, String itemId) {
+ this.name = name;
+ this.id = itemId;
+ this.changeKey = null;
+ }
+
+ /**
+ * Write item id as XML.
+ *
+ * @param writer request writer
+ * @throws IOException on error
+ */
+ public void write(Writer writer) throws IOException {
+ writer.write("<t:");
+ writer.write(name);
+ writer.write(" Id=\"");
+ writer.write(id);
+ if (changeKey != null) {
+ writer.write("\" ChangeKey=\"");
+ writer.write(changeKey);
+ }
+ writer.write("\"/>");
+ }
+}
diff --git a/src/java/davmail/exchange/ews/MessageDisposition.java b/src/java/davmail/exchange/ews/MessageDisposition.java
new file mode 100644
index 0000000..651b041
--- /dev/null
+++ b/src/java/davmail/exchange/ews/MessageDisposition.java
@@ -0,0 +1,34 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+/**
+ * MessageDisposition flag.
+ */
+ at SuppressWarnings({"JavaDoc", "UnusedDeclaration"})
+public final class MessageDisposition extends AttributeOption {
+
+ private MessageDisposition(String value) {
+ super("MessageDisposition", value);
+ }
+
+ public static final MessageDisposition SaveOnly = new MessageDisposition("SaveOnly");
+ public static final MessageDisposition SendOnly = new MessageDisposition("SendOnly");
+ public static final MessageDisposition SendAndSaveCopy = new MessageDisposition("SendAndSaveCopy");
+}
diff --git a/src/java/davmail/exchange/ews/MoveFolderMethod.java b/src/java/davmail/exchange/ews/MoveFolderMethod.java
new file mode 100644
index 0000000..19bc4db
--- /dev/null
+++ b/src/java/davmail/exchange/ews/MoveFolderMethod.java
@@ -0,0 +1,36 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+/**
+ * Create Folder method.
+ */
+public class MoveFolderMethod extends EWSMethod {
+ /**
+ * Move folder to target folder.
+ *
+ * @param folderId folder id
+ * @param toFolderId target folder id
+ */
+ public MoveFolderMethod(FolderId folderId, FolderId toFolderId) {
+ super("Folder", "MoveFolder");
+ this.folderId = folderId;
+ this.toFolderId = toFolderId;
+ }
+}
diff --git a/src/java/davmail/exchange/ews/MoveItemMethod.java b/src/java/davmail/exchange/ews/MoveItemMethod.java
new file mode 100644
index 0000000..e22d135
--- /dev/null
+++ b/src/java/davmail/exchange/ews/MoveItemMethod.java
@@ -0,0 +1,36 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+/**
+ * Move Item method.
+ */
+public class MoveItemMethod extends EWSMethod {
+ /**
+ * Move item to target folder.
+ *
+ * @param itemId item id
+ * @param toFolderId target folder id
+ */
+ public MoveItemMethod(ItemId itemId, FolderId toFolderId) {
+ super("Item", "MoveItem");
+ this.itemId = itemId;
+ this.toFolderId = toFolderId;
+ }
+}
diff --git a/src/java/davmail/exchange/ews/Option.java b/src/java/davmail/exchange/ews/Option.java
new file mode 100644
index 0000000..53cfdfa
--- /dev/null
+++ b/src/java/davmail/exchange/ews/Option.java
@@ -0,0 +1,44 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * Generic option.
+ */
+public abstract class Option {
+ protected final String name;
+ protected final String value;
+
+ protected Option(String name, String value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ /**
+ * Write XML content to writer.
+ *
+ * @param writer writer
+ * @throws IOException on error
+ */
+ public abstract void write(Writer writer) throws IOException;
+
+}
diff --git a/src/java/davmail/exchange/ews/ResolveNamesMethod.java b/src/java/davmail/exchange/ews/ResolveNamesMethod.java
new file mode 100644
index 0000000..812da74
--- /dev/null
+++ b/src/java/davmail/exchange/ews/ResolveNamesMethod.java
@@ -0,0 +1,158 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+import davmail.exchange.XMLStreamUtil;
+
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+/**
+ * Resolve Names method.
+ */
+public class ResolveNamesMethod extends EWSMethod {
+ protected static final AttributeOption RETURN_FULL_CONTACT_DATA = new AttributeOption("ReturnFullContactData", "true");
+
+ /**
+ * Build Resolve Names method
+ *
+ * @param value search value
+ */
+ public ResolveNamesMethod(String value) {
+ super("Contact", "ResolveNames", "ResolutionSet");
+ addMethodOption(SearchScope.ActiveDirectory);
+ addMethodOption(RETURN_FULL_CONTACT_DATA);
+ unresolvedEntry = new ElementOption("m:UnresolvedEntry", value);
+ }
+
+ @Override
+ protected Item handleItem(XMLStreamReader reader) throws XMLStreamException {
+ Item responseItem = new Item();
+ responseItem.type = "Contact";
+ // skip to Contact
+ while (reader.hasNext() && !XMLStreamUtil.isStartTag(reader, "Resolution")) {
+ reader.next();
+ }
+ while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "Resolution")) {
+ reader.next();
+ if (XMLStreamUtil.isStartTag(reader)) {
+ String tagLocalName = reader.getLocalName();
+ if ("Mailbox".equals(tagLocalName)) {
+ handleMailbox(reader, responseItem);
+ } else if ("Contact".equals(tagLocalName)) {
+ handleContact(reader, responseItem);
+ }
+ }
+ }
+ return responseItem;
+ }
+
+ protected void handleMailbox(XMLStreamReader reader, Item responseItem) throws XMLStreamException {
+ while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "Mailbox")) {
+ reader.next();
+ if (XMLStreamUtil.isStartTag(reader)) {
+ String tagLocalName = reader.getLocalName();
+ if ("Name".equals(tagLocalName)) {
+ responseItem.put(tagLocalName, XMLStreamUtil.getElementText(reader));
+ } else if ("EmailAddress".equals(tagLocalName)) {
+ responseItem.put(tagLocalName, XMLStreamUtil.getElementText(reader));
+ }
+ }
+ }
+ }
+
+ protected void handleContact(XMLStreamReader reader, Item responseItem) throws XMLStreamException {
+ while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "Contact")) {
+ reader.next();
+ if (XMLStreamUtil.isStartTag(reader)) {
+ String tagLocalName = reader.getLocalName();
+ if ("EmailAddresses".equals(tagLocalName)) {
+ handleEmailAddresses(reader, responseItem);
+ } else if ("PhysicalAddresses".equals(tagLocalName)) {
+ handlePhysicalAddresses(reader, responseItem);
+ } else if ("PhoneNumbers".equals(tagLocalName)) {
+ handlePhoneNumbers(reader, responseItem);
+ } else {
+ responseItem.put(tagLocalName, XMLStreamUtil.getElementText(reader));
+ }
+ }
+ }
+ }
+
+ protected void handlePhysicalAddress(XMLStreamReader reader, Item responseItem, String addressType) throws XMLStreamException {
+ while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "Entry")) {
+ reader.next();
+ if (XMLStreamUtil.isStartTag(reader)) {
+ String tagLocalName = reader.getLocalName();
+ String value = XMLStreamUtil.getElementText(reader);
+ responseItem.put(addressType + tagLocalName, value);
+ }
+ }
+ }
+
+ protected void handlePhysicalAddresses(XMLStreamReader reader, Item responseItem) throws XMLStreamException {
+ while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "PhysicalAddresses")) {
+ reader.next();
+ if (XMLStreamUtil.isStartTag(reader)) {
+ String tagLocalName = reader.getLocalName();
+ if ("Entry".equals(tagLocalName)) {
+ String key = getAttributeValue(reader, "Key");
+ handlePhysicalAddress(reader, responseItem, key);
+ }
+ }
+ }
+ }
+
+ protected void handlePhoneNumbers(XMLStreamReader reader, Item responseItem) throws XMLStreamException {
+ while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "PhoneNumbers")) {
+ reader.next();
+ if (XMLStreamUtil.isStartTag(reader)) {
+ String tagLocalName = reader.getLocalName();
+ if ("Entry".equals(tagLocalName)) {
+ String key = getAttributeValue(reader, "Key");
+ String value = XMLStreamUtil.getElementText(reader);
+ responseItem.put(key, value);
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void handleEmailAddresses(XMLStreamReader reader, Item responseItem) throws XMLStreamException {
+ while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "EmailAddresses")) {
+ reader.next();
+ if (XMLStreamUtil.isStartTag(reader)) {
+ String tagLocalName = reader.getLocalName();
+ if ("Entry".equals(tagLocalName)) {
+ String value = XMLStreamUtil.getElementText(reader);
+ if (value != null) {
+ if (value.startsWith("smtp:") || value.startsWith("SMTP:")) {
+ value = value.substring(5);
+ // get smtp address only if not already available through Mailbox info
+ if (responseItem.get("EmailAddress") == null) {
+ responseItem.put("EmailAddress", value);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/src/java/davmail/exchange/ews/SearchExpression.java b/src/java/davmail/exchange/ews/SearchExpression.java
new file mode 100644
index 0000000..f42e1ec
--- /dev/null
+++ b/src/java/davmail/exchange/ews/SearchExpression.java
@@ -0,0 +1,31 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+/**
+ * EWS Search Expression.
+ */
+public interface SearchExpression {
+ /**
+ * Append search expression to buffer.
+ *
+ * @param buffer search buffer
+ */
+ void appendTo(StringBuilder buffer);
+}
diff --git a/src/java/davmail/exchange/ews/SearchScope.java b/src/java/davmail/exchange/ews/SearchScope.java
new file mode 100644
index 0000000..6be2c33
--- /dev/null
+++ b/src/java/davmail/exchange/ews/SearchScope.java
@@ -0,0 +1,34 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+/**
+ * ResolveNames search scope.
+ */
+ at SuppressWarnings({"JavaDoc", "UnusedDeclaration"})
+public final class SearchScope extends AttributeOption {
+ private SearchScope(String value) {
+ super("SearchScope", value);
+ }
+
+ public static final SearchScope ActiveDirectory = new SearchScope("ActiveDirectory");
+ public static final SearchScope ActiveDirectoryContacts = new SearchScope("ActiveDirectoryContacts");
+ public static final SearchScope Contacts = new SearchScope("Contacts");
+ public static final SearchScope ContactsActiveDirectory = new SearchScope("ContactsActiveDirectory");
+}
diff --git a/src/java/davmail/exchange/ews/SendMeetingCancellations.java b/src/java/davmail/exchange/ews/SendMeetingCancellations.java
new file mode 100644
index 0000000..359826f
--- /dev/null
+++ b/src/java/davmail/exchange/ews/SendMeetingCancellations.java
@@ -0,0 +1,33 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+/**
+ * Item update option.
+ */
+ at SuppressWarnings({"JavaDoc", "UnusedDeclaration"})
+public final class SendMeetingCancellations extends AttributeOption {
+ private SendMeetingCancellations(String value) {
+ super("SendMeetingCancellations", value);
+ }
+
+ public static final SendMeetingCancellations SendToNone = new SendMeetingCancellations("SendToNone");
+ public static final SendMeetingCancellations SendOnlyToAll = new SendMeetingCancellations("SendOnlyToAll");
+ public static final SendMeetingCancellations SendToAllAndSaveCopy = new SendMeetingCancellations("SendToAllAndSaveCopy");
+}
diff --git a/src/java/davmail/exchange/ews/SendMeetingInvitations.java b/src/java/davmail/exchange/ews/SendMeetingInvitations.java
new file mode 100644
index 0000000..6b8d966
--- /dev/null
+++ b/src/java/davmail/exchange/ews/SendMeetingInvitations.java
@@ -0,0 +1,33 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+/**
+ * Item update option.
+ */
+ at SuppressWarnings({"UnusedDeclaration", "JavaDoc"})
+public final class SendMeetingInvitations extends AttributeOption {
+ private SendMeetingInvitations(String value) {
+ super("SendMeetingInvitations", value);
+ }
+
+ public static final SendMeetingInvitations SendToNone = new SendMeetingInvitations("SendToNone");
+ public static final SendMeetingInvitations SendOnlyToAll = new SendMeetingInvitations("SendOnlyToAll");
+ public static final SendMeetingInvitations SendToAllAndSaveCopy = new SendMeetingInvitations("SendToAllAndSaveCopy");
+}
diff --git a/src/java/davmail/exchange/ews/SendMeetingInvitationsOrCancellations.java b/src/java/davmail/exchange/ews/SendMeetingInvitationsOrCancellations.java
new file mode 100644
index 0000000..55973b9
--- /dev/null
+++ b/src/java/davmail/exchange/ews/SendMeetingInvitationsOrCancellations.java
@@ -0,0 +1,33 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+/**
+ * Item update option.
+ */
+ at SuppressWarnings({"JavaDoc", "UnusedDeclaration"})
+public final class SendMeetingInvitationsOrCancellations extends AttributeOption {
+ private SendMeetingInvitationsOrCancellations(String value) {
+ super("SendMeetingInvitationsOrCancellations", value);
+ }
+
+ public static final SendMeetingInvitationsOrCancellations SendToNone = new SendMeetingInvitationsOrCancellations("SendToNone");
+ public static final SendMeetingInvitationsOrCancellations SendOnlyToAll = new SendMeetingInvitationsOrCancellations("SendOnlyToAll");
+ public static final SendMeetingInvitationsOrCancellations SendToAllAndSaveCopy = new SendMeetingInvitationsOrCancellations("SendToAllAndSaveCopy");
+}
diff --git a/src/java/davmail/exchange/ews/TwoOperandExpression.java b/src/java/davmail/exchange/ews/TwoOperandExpression.java
new file mode 100644
index 0000000..6d9ef7c
--- /dev/null
+++ b/src/java/davmail/exchange/ews/TwoOperandExpression.java
@@ -0,0 +1,65 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+import davmail.util.StringUtil;
+
+/**
+ * Two operand expression.
+ */
+public class TwoOperandExpression implements SearchExpression {
+ @SuppressWarnings({"UnusedDeclaration"})
+ protected enum Operator {
+ IsEqualTo, IsNotEqualTo, IsGreaterThan, IsGreaterThanOrEqualTo, IsLessThan, IsLessThanOrEqualTo
+ }
+
+ protected final Operator operator;
+ protected final FieldURI fieldURI;
+ protected final String value;
+
+ /**
+ * Create two operand expression.
+ *
+ * @param operator operator
+ * @param fieldURI field operand
+ * @param value value operand
+ */
+ public TwoOperandExpression(Operator operator, FieldURI fieldURI, String value) {
+ this.operator = operator;
+ this.fieldURI = fieldURI;
+ this.value = value;
+ }
+
+ public void appendTo(StringBuilder buffer) {
+ buffer.append("<t:").append(operator.toString()).append('>');
+ fieldURI.appendTo(buffer);
+
+ buffer.append("<t:FieldURIOrConstant><t:Constant Value=\"");
+ // encode urlcompname
+ if (fieldURI instanceof ExtendedFieldURI && "0x10f3".equals(((ExtendedFieldURI) fieldURI).propertyTag)) {
+ buffer.append(StringUtil.xmlEncodeAttribute(StringUtil.encodeUrlcompname(value)));
+ } else {
+ buffer.append(StringUtil.xmlEncodeAttribute(value));
+ }
+ buffer.append("\"/></t:FieldURIOrConstant>");
+
+ buffer.append("</t:").append(operator.toString()).append('>');
+ }
+
+}
diff --git a/src/java/davmail/exchange/ews/UnindexedFieldURI.java b/src/java/davmail/exchange/ews/UnindexedFieldURI.java
new file mode 100644
index 0000000..0a2e852
--- /dev/null
+++ b/src/java/davmail/exchange/ews/UnindexedFieldURI.java
@@ -0,0 +1,93 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+import davmail.util.StringUtil;
+
+/**
+ * Unindexed Field URI
+ */
+public class UnindexedFieldURI implements FieldURI {
+ protected final String fieldURI;
+ protected final String fieldName;
+
+ /**
+ * Create unindexed field URI.
+ *
+ * @param fieldURI field name
+ */
+ public UnindexedFieldURI(String fieldURI) {
+ this.fieldURI = fieldURI;
+ int colonIndex = fieldURI.indexOf(':');
+ if (colonIndex < 0) {
+ fieldName = fieldURI;
+ } else {
+ fieldName = fieldURI.substring(colonIndex + 1);
+ }
+ }
+
+ public void appendTo(StringBuilder buffer) {
+ buffer.append("<t:FieldURI FieldURI=\"").append(fieldURI).append("\"/>");
+ }
+
+ public void appendValue(StringBuilder buffer, String itemType, String value) {
+ if (fieldURI.startsWith("message") && itemType != null) {
+ itemType = "Message";
+ } else if (fieldURI.startsWith("calendar") && itemType != null) {
+ itemType = "CalendarItem";
+ } else if (fieldURI.startsWith("task") && itemType != null) {
+ itemType = "Task";
+ } else if (fieldURI.startsWith("contacts") && itemType != null) {
+ itemType = "Contact";
+ }
+ if (itemType != null) {
+ appendTo(buffer);
+ buffer.append("<t:");
+ buffer.append(itemType);
+ buffer.append('>');
+ }
+ if ("MeetingTimeZone".equals(fieldName)) {
+ buffer.append("<t:MeetingTimeZone TimeZoneName=\"");
+ buffer.append(StringUtil.xmlEncodeAttribute(value));
+ buffer.append("\"></t:MeetingTimeZone>");
+ } else if ("StartTimeZone".equals(fieldName)) {
+ buffer.append("<t:StartTimeZone Id=\"");
+ buffer.append(StringUtil.xmlEncodeAttribute(value));
+ buffer.append("\"></t:StartTimeZone>");
+ } else {
+ buffer.append("<t:");
+ buffer.append(fieldName);
+ buffer.append('>');
+ buffer.append(StringUtil.xmlEncodeAttribute(value));
+ buffer.append("</t:");
+ buffer.append(fieldName);
+ buffer.append('>');
+ }
+ if (itemType != null) {
+ buffer.append("</t:");
+ buffer.append(itemType);
+ buffer.append('>');
+ }
+ }
+
+ public String getResponseName() {
+ return fieldName;
+ }
+
+}
diff --git a/src/java/davmail/exchange/ews/UpdateFolderMethod.java b/src/java/davmail/exchange/ews/UpdateFolderMethod.java
new file mode 100644
index 0000000..18982b4
--- /dev/null
+++ b/src/java/davmail/exchange/ews/UpdateFolderMethod.java
@@ -0,0 +1,38 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+import java.util.List;
+
+/**
+ * Update Folder method.
+ */
+public class UpdateFolderMethod extends EWSMethod {
+ /**
+ * Update folder options.
+ *
+ * @param folderId folder id
+ * @param updates folder properties updates
+ */
+ public UpdateFolderMethod(FolderId folderId, List<FieldUpdate> updates) {
+ super("Folder", "UpdateFolder");
+ this.folderId = folderId;
+ this.updates = updates;
+ }
+}
\ No newline at end of file
diff --git a/src/java/davmail/exchange/ews/UpdateItemMethod.java b/src/java/davmail/exchange/ews/UpdateItemMethod.java
new file mode 100644
index 0000000..71e4d1d
--- /dev/null
+++ b/src/java/davmail/exchange/ews/UpdateItemMethod.java
@@ -0,0 +1,47 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+import java.util.List;
+
+/**
+ * Update Item method.
+ */
+public class UpdateItemMethod extends EWSMethod {
+ /**
+ * Update exchange item.
+ *
+ * @param messageDisposition save or send option
+ * @param conflictResolution overwrite option
+ * @param sendMeetingInvitationsOrCancellations
+ * send invitations option
+ * @param itemId item id with change key
+ * @param updates field updates
+ */
+ public UpdateItemMethod(MessageDisposition messageDisposition, ConflictResolution conflictResolution,
+ SendMeetingInvitationsOrCancellations sendMeetingInvitationsOrCancellations,
+ ItemId itemId, List<FieldUpdate> updates) {
+ super("Item", "UpdateItem");
+ this.itemId = itemId;
+ this.updates = updates;
+ addMethodOption(messageDisposition);
+ addMethodOption(conflictResolution);
+ addMethodOption(sendMeetingInvitationsOrCancellations);
+ }
+}
\ No newline at end of file
diff --git a/src/java/davmail/http/DavGatewayHttpClientFacade.java b/src/java/davmail/http/DavGatewayHttpClientFacade.java
new file mode 100644
index 0000000..0475b34
--- /dev/null
+++ b/src/java/davmail/http/DavGatewayHttpClientFacade.java
@@ -0,0 +1,751 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.http;
+
+import davmail.BundleMessage;
+import davmail.Settings;
+import davmail.exception.*;
+import davmail.exchange.dav.ExchangeDavMethod;
+import davmail.exchange.dav.ExchangeSearchMethod;
+import davmail.ui.tray.DavGatewayTray;
+import org.apache.commons.httpclient.*;
+import org.apache.commons.httpclient.auth.AuthPolicy;
+import org.apache.commons.httpclient.auth.AuthScope;
+import org.apache.commons.httpclient.cookie.CookiePolicy;
+import org.apache.commons.httpclient.methods.DeleteMethod;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.apache.commons.httpclient.params.HttpClientParams;
+import org.apache.commons.httpclient.params.HttpMethodParams;
+import org.apache.commons.httpclient.util.IdleConnectionTimeoutThread;
+import org.apache.commons.httpclient.util.URIUtil;
+import org.apache.jackrabbit.webdav.DavException;
+import org.apache.jackrabbit.webdav.MultiStatusResponse;
+import org.apache.jackrabbit.webdav.client.methods.CopyMethod;
+import org.apache.jackrabbit.webdav.client.methods.DavMethodBase;
+import org.apache.jackrabbit.webdav.client.methods.MoveMethod;
+import org.apache.jackrabbit.webdav.client.methods.PropFindMethod;
+import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
+import org.apache.log4j.Logger;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Create HttpClient instance according to DavGateway Settings
+ */
+public final class DavGatewayHttpClientFacade {
+ static final Logger LOGGER = Logger.getLogger("davmail.http.DavGatewayHttpClientFacade");
+
+ static final String IE_USER_AGENT = "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0)";
+ static final int MAX_REDIRECTS = 10;
+ static final Object LOCK = new Object();
+ private static boolean needNTLM;
+
+ static final long ONE_MINUTE = 60000;
+
+ private static IdleConnectionTimeoutThread httpConnectionManagerThread;
+
+ static {
+ // workaround for TLS Renegotiation issue see http://java.sun.com/javase/javaseforbusiness/docs/TLSReadme.html
+ System.setProperty("sun.security.ssl.allowUnsafeRenegotiation", "true");
+
+ DavGatewayHttpClientFacade.start();
+
+ // register custom cookie policy
+ CookiePolicy.registerCookieSpec("DavMailCookieSpec", DavMailCookieSpec.class);
+
+ AuthPolicy.registerAuthScheme(AuthPolicy.BASIC, LenientBasicScheme.class);
+
+ }
+
+
+ private DavGatewayHttpClientFacade() {
+ }
+
+ /**
+ * Create basic http client with default params.
+ *
+ * @return HttpClient instance
+ */
+ private static HttpClient getBaseInstance() {
+ HttpClient httpClient = new HttpClient();
+ httpClient.getParams().setParameter(HttpMethodParams.USER_AGENT, IE_USER_AGENT);
+ httpClient.getParams().setParameter(HttpClientParams.MAX_REDIRECTS, MAX_REDIRECTS);
+ httpClient.getParams().setCookiePolicy("DavMailCookieSpec");
+ return httpClient;
+ }
+
+ /**
+ * Create a configured HttpClient instance.
+ *
+ * @param url target url
+ * @return httpClient
+ * @throws DavMailException on error
+ */
+ public static HttpClient getInstance(String url) throws DavMailException {
+ // create an HttpClient instance
+ HttpClient httpClient = getBaseInstance();
+ configureClient(httpClient, url);
+ return httpClient;
+ }
+
+ /**
+ * Set credentials on HttpClient instance.
+ *
+ * @param httpClient httpClient instance
+ * @param userName user name
+ * @param password user password
+ */
+ public static void setCredentials(HttpClient httpClient, String userName, String password) {
+ // some Exchange servers redirect to a different host for freebusy, use wide auth scope
+ AuthScope authScope = new AuthScope(null, -1);
+ httpClient.getState().setCredentials(authScope, new NTCredentials(userName, password, "UNKNOWN", ""));
+ }
+
+ /**
+ * Set http client current host configuration.
+ *
+ * @param httpClient current Http client
+ * @param url target url
+ * @throws DavMailException on error
+ */
+ public static void setClientHost(HttpClient httpClient, String url) throws DavMailException {
+ try {
+ HostConfiguration hostConfig = httpClient.getHostConfiguration();
+ URI httpURI = new URI(url, true);
+ hostConfig.setHost(httpURI);
+ } catch (URIException e) {
+ throw new DavMailException("LOG_INVALID_URL", url);
+ }
+ }
+
+ /**
+ * Update http client configuration (proxy)
+ *
+ * @param httpClient current Http client
+ * @param url target url
+ * @throws DavMailException on error
+ */
+ public static void configureClient(HttpClient httpClient, String url) throws DavMailException {
+ setClientHost(httpClient, url);
+
+ if (!needNTLM) {
+ ArrayList<String> authPrefs = new ArrayList<String>();
+ authPrefs.add(AuthPolicy.DIGEST);
+ authPrefs.add(AuthPolicy.BASIC);
+ // exclude NTLM authentication scheme
+ httpClient.getParams().setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, authPrefs);
+ }
+
+ boolean enableProxy = Settings.getBooleanProperty("davmail.enableProxy");
+ boolean useSystemProxies = Settings.getBooleanProperty("davmail.useSystemProxies", Boolean.FALSE);
+ String proxyHost = null;
+ int proxyPort = 0;
+ String proxyUser = null;
+ String proxyPassword = null;
+
+ if (useSystemProxies) {
+ // get proxy for url from system settings
+ System.setProperty("java.net.useSystemProxies", "true");
+ try {
+ List<Proxy> proxyList = getProxyForURI(new java.net.URI(url));
+ if (!proxyList.isEmpty() && proxyList.get(0).address() != null) {
+ InetSocketAddress inetSocketAddress = (InetSocketAddress) proxyList.get(0).address();
+ proxyHost = inetSocketAddress.getHostName();
+ proxyPort = inetSocketAddress.getPort();
+
+ // we may still need authentication credentials
+ proxyUser = Settings.getProperty("davmail.proxyUser");
+ proxyPassword = Settings.getProperty("davmail.proxyPassword");
+ }
+ } catch (URISyntaxException e) {
+ throw new DavMailException("LOG_INVALID_URL", url);
+ }
+ } else if (enableProxy) {
+ proxyHost = Settings.getProperty("davmail.proxyHost");
+ proxyPort = Settings.getIntProperty("davmail.proxyPort");
+ proxyUser = Settings.getProperty("davmail.proxyUser");
+ proxyPassword = Settings.getProperty("davmail.proxyPassword");
+ }
+
+ // configure proxy
+ if (proxyHost != null && proxyHost.length() > 0) {
+ httpClient.getHostConfiguration().setProxy(proxyHost, proxyPort);
+ if (proxyUser != null && proxyUser.length() > 0) {
+
+ AuthScope authScope = new AuthScope(proxyHost, proxyPort, AuthScope.ANY_REALM);
+
+ // detect ntlm authentication (windows domain name in user name)
+ int backslashindex = proxyUser.indexOf('\\');
+ if (backslashindex > 0) {
+ httpClient.getState().setProxyCredentials(authScope,
+ new NTCredentials(proxyUser.substring(backslashindex + 1),
+ proxyPassword, "UNKNOWN",
+ proxyUser.substring(0, backslashindex)));
+ } else {
+ httpClient.getState().setProxyCredentials(authScope,
+ new NTCredentials(proxyUser, proxyPassword, "UNKNOWN", ""));
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Retrieve Proxy Selector
+ *
+ * @param uri target uri
+ * @return proxy selector
+ */
+ private static List<Proxy> getProxyForURI(java.net.URI uri) {
+ LOGGER.debug("get Default proxy selector");
+ ProxySelector proxySelector = ProxySelector.getDefault();
+ LOGGER.debug("getProxyForURI(" + uri + ')');
+ List<Proxy> proxies = proxySelector.select(uri);
+ LOGGER.debug("got system proxies:" + proxies);
+ return proxies;
+ }
+
+
+ /**
+ * Get Http Status code for the given URL
+ *
+ * @param httpClient httpClient instance
+ * @param url url string
+ * @return HttpStatus code
+ */
+ public static int getHttpStatus(HttpClient httpClient, String url) {
+ int status = 0;
+ HttpMethod testMethod = new GetMethod(url);
+ testMethod.setDoAuthentication(false);
+ try {
+ status = httpClient.executeMethod(testMethod);
+ } catch (IOException e) {
+ LOGGER.warn(e.getMessage(), e);
+ } finally {
+ testMethod.releaseConnection();
+ }
+ return status;
+ }
+
+ /**
+ * Check if status is a redirect (various 30x values).
+ *
+ * @param status Http status
+ * @return true if status is a redirect
+ */
+ public static boolean isRedirect(int status) {
+ return status == HttpStatus.SC_MOVED_PERMANENTLY
+ || status == HttpStatus.SC_MOVED_TEMPORARILY
+ || status == HttpStatus.SC_SEE_OTHER
+ || status == HttpStatus.SC_TEMPORARY_REDIRECT;
+ }
+
+ /**
+ * Execute given url, manually follow redirects.
+ * Workaround for HttpClient bug (GET full URL over HTTPS and proxy)
+ *
+ * @param httpClient HttpClient instance
+ * @param url url string
+ * @return executed method
+ * @throws IOException on error
+ */
+ public static HttpMethod executeFollowRedirects(HttpClient httpClient, String url) throws IOException {
+ HttpMethod method = new GetMethod(url);
+ method.setFollowRedirects(false);
+ return executeFollowRedirects(httpClient, method);
+ }
+
+ private static int checkNTLM(HttpClient httpClient, HttpMethod currentMethod) throws IOException {
+ int status = currentMethod.getStatusCode();
+ if ((status == HttpStatus.SC_UNAUTHORIZED || status == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED)
+ && acceptsNTLMOnly(currentMethod) && !hasNTLM(httpClient)) {
+ LOGGER.debug("Received " + status + " unauthorized at " + currentMethod.getURI() + ", retrying with NTLM");
+ resetMethod(currentMethod);
+ addNTLM(httpClient);
+ status = httpClient.executeMethod(currentMethod);
+ }
+ return status;
+ }
+
+ /**
+ * Execute method with httpClient, follow 30x redirects.
+ *
+ * @param httpClient Http client instance
+ * @param method Http method
+ * @return last http method after redirects
+ * @throws IOException on error
+ */
+ public static HttpMethod executeFollowRedirects(HttpClient httpClient, HttpMethod method) throws IOException {
+ HttpMethod currentMethod = method;
+ try {
+ DavGatewayTray.debug(new BundleMessage("LOG_EXECUTE_FOLLOW_REDIRECTS", currentMethod.getURI()));
+ httpClient.executeMethod(currentMethod);
+ int status = checkNTLM(httpClient, currentMethod);
+
+ Header location = currentMethod.getResponseHeader("Location");
+ int redirectCount = 0;
+ while (redirectCount++ < 10
+ && location != null
+ && isRedirect(status)) {
+ // Novell iChain workaround
+ String locationValue = location.getValue();
+ if (locationValue.indexOf('"') >= 0) {
+ locationValue = URIUtil.encodePath(locationValue);
+ }
+ // workaround for invalid relative location
+ if (locationValue.startsWith("./")) {
+ locationValue = locationValue.substring(1);
+ }
+ currentMethod.releaseConnection();
+ currentMethod = new GetMethod(locationValue);
+ currentMethod.setFollowRedirects(false);
+ DavGatewayTray.debug(new BundleMessage("LOG_EXECUTE_FOLLOW_REDIRECTS_COUNT", currentMethod.getURI(), redirectCount));
+ httpClient.executeMethod(currentMethod);
+ checkNTLM(httpClient, currentMethod);
+ location = currentMethod.getResponseHeader("Location");
+ }
+ if (location != null && isRedirect(currentMethod.getStatusCode())) {
+ currentMethod.releaseConnection();
+ throw new HttpException("Maximum redirections reached");
+ }
+ } catch (IOException e) {
+ currentMethod.releaseConnection();
+ throw e;
+ }
+ // caller will need to release connection
+ return currentMethod;
+ }
+
+ /**
+ * Execute method with httpClient, do not follow 30x redirects.
+ *
+ * @param httpClient Http client instance
+ * @param method Http method
+ * @return status
+ * @throws IOException on error
+ */
+ public static int executeNoRedirect(HttpClient httpClient, HttpMethod method) throws IOException {
+ int status;
+ try {
+ status = httpClient.executeMethod(method);
+ // check NTLM
+ if ((status == HttpStatus.SC_UNAUTHORIZED || status == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED)
+ && acceptsNTLMOnly(method) && !hasNTLM(httpClient)) {
+ LOGGER.debug("Received " + status + " unauthorized at " + method.getURI() + ", retrying with NTLM");
+ resetMethod(method);
+ addNTLM(httpClient);
+ status = httpClient.executeMethod(method);
+ }
+ } finally {
+ method.releaseConnection();
+ }
+ // caller will need to release connection
+ return status;
+ }
+
+ /**
+ * Execute webdav search method.
+ *
+ * @param httpClient http client instance
+ * @param path <i>encoded</i> searched folder path
+ * @param searchRequest (SQL like) search request
+ * @param maxCount max item count
+ * @return Responses enumeration
+ * @throws IOException on error
+ */
+ public static MultiStatusResponse[] executeSearchMethod(HttpClient httpClient, String path, String searchRequest,
+ int maxCount) throws IOException {
+ ExchangeSearchMethod searchMethod = new ExchangeSearchMethod(path, searchRequest);
+ if (maxCount > 0) {
+ searchMethod.addRequestHeader("Range", "rows=0-" + (maxCount - 1));
+ }
+ return executeMethod(httpClient, searchMethod);
+ }
+
+ /**
+ * Execute webdav propfind method.
+ *
+ * @param httpClient http client instance
+ * @param path <i>encoded</i> searched folder path
+ * @param depth propfind request depth
+ * @param properties propfind requested properties
+ * @return Responses enumeration
+ * @throws IOException on error
+ */
+ public static MultiStatusResponse[] executePropFindMethod(HttpClient httpClient, String path, int depth, DavPropertyNameSet properties) throws IOException {
+ PropFindMethod propFindMethod = new PropFindMethod(path, properties, depth);
+ return executeMethod(httpClient, propFindMethod);
+ }
+
+ /**
+ * Execute a delete method on the given path with httpClient.
+ *
+ * @param httpClient Http client instance
+ * @param path Path to be deleted
+ * @throws IOException on error
+ * @return http status
+ */
+ public static int executeDeleteMethod(HttpClient httpClient, String path) throws IOException {
+ DeleteMethod deleteMethod = new DeleteMethod(path);
+ deleteMethod.setFollowRedirects(false);
+
+ int status = executeHttpMethod(httpClient, deleteMethod);
+ // do not throw error if already deleted
+ if (status != HttpStatus.SC_OK && status != HttpStatus.SC_NOT_FOUND) {
+ throw DavGatewayHttpClientFacade.buildHttpException(deleteMethod);
+ }
+ return status;
+ }
+
+ /**
+ * Execute webdav request.
+ *
+ * @param httpClient http client instance
+ * @param method webdav method
+ * @return Responses enumeration
+ * @throws IOException on error
+ */
+ public static MultiStatusResponse[] executeMethod(HttpClient httpClient, DavMethodBase method) throws IOException {
+ MultiStatusResponse[] responses = null;
+ try {
+ int status = httpClient.executeMethod(method);
+
+ // need to follow redirects (once) on public folders
+ if (isRedirect(status)) {
+ method.releaseConnection();
+ URI targetUri = new URI(method.getResponseHeader("Location").getValue(), true);
+ checkExpiredSession(targetUri.getQuery());
+ method.setURI(targetUri);
+ status = httpClient.executeMethod(method);
+ }
+
+ if (status != HttpStatus.SC_MULTI_STATUS) {
+ throw buildHttpException(method);
+ }
+ responses = method.getResponseBodyAsMultiStatus().getResponses();
+
+ } catch (DavException e) {
+ throw new IOException(e.getMessage());
+ } finally {
+ method.releaseConnection();
+ }
+ return responses;
+ }
+
+ /**
+ * Execute webdav request.
+ *
+ * @param httpClient http client instance
+ * @param method webdav method
+ * @return Responses enumeration
+ * @throws IOException on error
+ */
+ public static MultiStatusResponse[] executeMethod(HttpClient httpClient, ExchangeDavMethod method) throws IOException {
+ MultiStatusResponse[] responses = null;
+ try {
+ int status = httpClient.executeMethod(method);
+
+ // need to follow redirects (once) on public folders
+ if (isRedirect(status)) {
+ method.releaseConnection();
+ URI targetUri = new URI(method.getResponseHeader("Location").getValue(), true);
+ checkExpiredSession(targetUri.getQuery());
+ method.setURI(targetUri);
+ status = httpClient.executeMethod(method);
+ }
+
+ if (status != HttpStatus.SC_MULTI_STATUS) {
+ throw buildHttpException(method);
+ }
+ responses = method.getResponses();
+
+ } finally {
+ method.releaseConnection();
+ }
+ return responses;
+ }
+
+ /**
+ * Execute method with httpClient.
+ *
+ * @param httpClient Http client instance
+ * @param method Http method
+ * @return Http status
+ * @throws IOException on error
+ */
+ public static int executeHttpMethod(HttpClient httpClient, HttpMethod method) throws IOException {
+ int status = 0;
+ try {
+ status = httpClient.executeMethod(method);
+ } finally {
+ method.releaseConnection();
+ }
+ return status;
+ }
+
+ /**
+ * Test if NTLM auth scheme is enabled.
+ *
+ * @param httpClient HttpClient instance
+ * @return true if NTLM is enabled
+ */
+ public static boolean hasNTLM(HttpClient httpClient) {
+ Object authPrefs = httpClient.getParams().getParameter(AuthPolicy.AUTH_SCHEME_PRIORITY);
+ return authPrefs == null || (authPrefs instanceof List<?> && ((Collection) authPrefs).contains(AuthPolicy.NTLM));
+ }
+
+ /**
+ * Enable NTLM authentication on http client
+ *
+ * @param httpClient HttpClient instance
+ */
+ public static void addNTLM(HttpClient httpClient) {
+ // disable preemptive authentication
+ httpClient.getParams().setParameter(HttpClientParams.PREEMPTIVE_AUTHENTICATION, false);
+
+ // register the jcifs based NTLMv2 implementation
+ AuthPolicy.registerAuthScheme(AuthPolicy.NTLM, NTLMv2Scheme.class);
+
+ ArrayList<String> authPrefs = new ArrayList<String>();
+ authPrefs.add(AuthPolicy.NTLM);
+ authPrefs.add(AuthPolicy.DIGEST);
+ authPrefs.add(AuthPolicy.BASIC);
+ httpClient.getParams().setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, authPrefs);
+
+ // separate domain from username in credentials
+ AuthScope authScope = new AuthScope(null, -1);
+ NTCredentials credentials = (NTCredentials) httpClient.getState().getCredentials(authScope);
+ String userName = credentials.getUserName();
+ int backSlashIndex = userName.indexOf('\\');
+ if (backSlashIndex >= 0) {
+ String domain = userName.substring(0, backSlashIndex);
+ userName = userName.substring(backSlashIndex + 1);
+ credentials = new NTCredentials(userName, credentials.getPassword(), "UNKNOWN", domain);
+ httpClient.getState().setCredentials(authScope, credentials);
+ }
+
+ // make sure NTLM is always active
+ needNTLM = true;
+ }
+
+ /**
+ * Test method header for supported authentication mode,
+ * return true if Basic authentication is not available
+ *
+ * @param getMethod http method
+ * @return true if only NTLM is enabled
+ */
+ public static boolean acceptsNTLMOnly(HttpMethod getMethod) {
+ Header authenticateHeader = null;
+ if (getMethod.getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
+ authenticateHeader = getMethod.getResponseHeader("WWW-Authenticate");
+ } else if (getMethod.getStatusCode() == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED) {
+ authenticateHeader = getMethod.getResponseHeader("Proxy-Authenticate");
+ }
+ if (authenticateHeader == null) {
+ return false;
+ } else {
+ boolean acceptBasic = false;
+ boolean acceptNTLM = false;
+ HeaderElement[] headerElements = authenticateHeader.getElements();
+ for (HeaderElement headerElement : headerElements) {
+ if ("NTLM".equalsIgnoreCase(headerElement.getName())) {
+ acceptNTLM = true;
+ }
+ if ("Basic realm".equalsIgnoreCase(headerElement.getName())) {
+ acceptBasic = true;
+ }
+ }
+ return acceptNTLM && !acceptBasic;
+
+ }
+ }
+
+ /**
+ * Execute test method from checkConfig, with proxy credentials, but without Exchange credentials.
+ *
+ * @param httpClient Http client instance
+ * @param method Http method
+ * @return Http status
+ * @throws IOException on error
+ */
+ public static int executeTestMethod(HttpClient httpClient, GetMethod method) throws IOException {
+ // do not follow redirects in expired sessions
+ method.setFollowRedirects(false);
+ int status = httpClient.executeMethod(method);
+ if (status == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED
+ && acceptsNTLMOnly(method) && !hasNTLM(httpClient)) {
+ resetMethod(method);
+ LOGGER.debug("Received " + status + " unauthorized at " + method.getURI() + ", retrying with NTLM");
+ addNTLM(httpClient);
+ status = httpClient.executeMethod(method);
+ }
+
+ return status;
+ }
+
+ /**
+ * Execute Get method, do not follow redirects.
+ *
+ * @param httpClient Http client instance
+ * @param method Http method
+ * @param followRedirects Follow redirects flag
+ * @throws IOException on error
+ */
+ public static void executeGetMethod(HttpClient httpClient, GetMethod method, boolean followRedirects) throws IOException {
+ // do not follow redirects in expired sessions
+ method.setFollowRedirects(followRedirects);
+ int status = httpClient.executeMethod(method);
+ if ((status == HttpStatus.SC_UNAUTHORIZED || status == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED)
+ && acceptsNTLMOnly(method) && !hasNTLM(httpClient)) {
+ resetMethod(method);
+ LOGGER.debug("Received " + status + " unauthorized at " + method.getURI() + ", retrying with NTLM");
+ addNTLM(httpClient);
+ status = httpClient.executeMethod(method);
+ }
+ if (status != HttpStatus.SC_OK && (followRedirects || !isRedirect(status))) {
+ LOGGER.warn("GET failed with status " + status + " at " + method.getURI());
+ if (status != HttpStatus.SC_NOT_FOUND && status != HttpStatus.SC_FORBIDDEN) {
+ LOGGER.warn(method.getResponseBodyAsString());
+ }
+ throw DavGatewayHttpClientFacade.buildHttpException(method);
+ }
+ // check for expired session
+ if (followRedirects) {
+ String queryString = method.getQueryString();
+ checkExpiredSession(queryString);
+ }
+ }
+
+ private static void resetMethod(HttpMethod method) {
+ // reset method state
+ method.releaseConnection();
+ method.getHostAuthState().invalidate();
+ method.getProxyAuthState().invalidate();
+ }
+
+ private static void checkExpiredSession(String queryString) throws DavMailAuthenticationException {
+ if (queryString != null && (queryString.contains("reason=2") || queryString.contains("reason=0"))) {
+ LOGGER.warn("Request failed, session expired");
+ throw new DavMailAuthenticationException("EXCEPTION_SESSION_EXPIRED");
+ }
+ }
+
+ /**
+ * Build Http Exception from methode status
+ *
+ * @param method Http Method
+ * @return Http Exception
+ */
+ public static HttpException buildHttpException(HttpMethod method) {
+ int status = method.getStatusCode();
+ StringBuilder message = new StringBuilder();
+ message.append(status).append(' ').append(method.getStatusText());
+ try {
+ message.append(" at ").append(method.getURI().getURI());
+ if (method instanceof CopyMethod || method instanceof MoveMethod) {
+ message.append(" to ").append(method.getRequestHeader("Destination"));
+ }
+ } catch (URIException e) {
+ message.append(method.getPath());
+ }
+ // 440 means forbidden on Exchange
+ if (status == 440) {
+ return new LoginTimeoutException(message.toString());
+ } else if (status == HttpStatus.SC_FORBIDDEN) {
+ return new HttpForbiddenException(message.toString());
+ } else if (status == HttpStatus.SC_NOT_FOUND) {
+ return new HttpNotFoundException(message.toString());
+ } else if (status == HttpStatus.SC_PRECONDITION_FAILED) {
+ return new HttpPreconditionFailedException(message.toString());
+ } else if (status == HttpStatus.SC_INTERNAL_SERVER_ERROR) {
+ return new HttpServerErrorException(message.toString());
+ } else {
+ return new HttpException(message.toString());
+ }
+ }
+
+ /**
+ * Test if the method response is gzip encoded
+ *
+ * @param method http method
+ * @return true if response is gzip encoded
+ */
+ public static boolean isGzipEncoded(HttpMethod method) {
+ Header[] contentEncodingHeaders = method.getResponseHeaders("Content-Encoding");
+ if (contentEncodingHeaders != null) {
+ for (Header header : contentEncodingHeaders) {
+ if ("gzip".equals(header.getValue())) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Stop HttpConnectionManager.
+ */
+ public static void stop() {
+ synchronized (LOCK) {
+ if (httpConnectionManagerThread != null) {
+ httpConnectionManagerThread.interrupt();
+ httpConnectionManagerThread = null;
+ }
+ MultiThreadedHttpConnectionManager.shutdownAll();
+ }
+ }
+
+ /**
+ * Create and set connection pool.
+ *
+ * @param httpClient httpClient instance
+ */
+ public static void createMultiThreadedHttpConnectionManager(HttpClient httpClient) {
+ MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
+ connectionManager.getParams().setDefaultMaxConnectionsPerHost(100);
+ connectionManager.getParams().setConnectionTimeout(10000);
+ synchronized (LOCK) {
+ httpConnectionManagerThread.addConnectionManager(connectionManager);
+ }
+ httpClient.setHttpConnectionManager(connectionManager);
+ }
+
+ /**
+ * Create and start a new HttpConnectionManager, close idle connections every minute.
+ */
+ public static void start() {
+ synchronized (LOCK) {
+ if (httpConnectionManagerThread == null) {
+ httpConnectionManagerThread = new IdleConnectionTimeoutThread();
+ httpConnectionManagerThread.setName(IdleConnectionTimeoutThread.class.getSimpleName());
+ httpConnectionManagerThread.setConnectionTimeout(ONE_MINUTE);
+ httpConnectionManagerThread.setTimeoutInterval(ONE_MINUTE);
+ httpConnectionManagerThread.start();
+ }
+ }
+ }
+}
diff --git a/src/java/davmail/http/DavGatewayOTPPrompt.java b/src/java/davmail/http/DavGatewayOTPPrompt.java
new file mode 100644
index 0000000..0473380
--- /dev/null
+++ b/src/java/davmail/http/DavGatewayOTPPrompt.java
@@ -0,0 +1,53 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.http;
+
+import davmail.BundleMessage;
+import davmail.ui.PasswordPromptDialog;
+
+import java.awt.*;
+
+/**
+ * Ask user one time password.
+ */
+public final class DavGatewayOTPPrompt {
+ private DavGatewayOTPPrompt() {
+ }
+
+ /**
+ * Ask user token password
+ *
+ * @return user provided one time password
+ */
+ public static String getOneTimePassword() {
+ PasswordPromptDialog passwordPromptDialog = new PasswordPromptDialog(BundleMessage.format("UI_OTP_PASSWORD_PROMPT"));
+ return String.valueOf(passwordPromptDialog.getPassword());
+ }
+
+ /**
+ * Ask user captcha value
+ *
+ * @param captchaImage captcha image
+ * @return user provided one time password
+ */
+ public static String getCaptchaValue(Image captchaImage) {
+ PasswordPromptDialog passwordPromptDialog = new PasswordPromptDialog(BundleMessage.format("UI_CAPTCHA_PROMPT"), captchaImage);
+ return String.valueOf(passwordPromptDialog.getPassword());
+ }
+}
diff --git a/src/java/davmail/http/DavGatewaySSLProtocolSocketFactory.java b/src/java/davmail/http/DavGatewaySSLProtocolSocketFactory.java
new file mode 100644
index 0000000..6aa0ab7
--- /dev/null
+++ b/src/java/davmail/http/DavGatewaySSLProtocolSocketFactory.java
@@ -0,0 +1,226 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.http;
+
+import davmail.BundleMessage;
+import davmail.Settings;
+import davmail.ui.PasswordPromptDialog;
+import davmail.ui.tray.DavGatewayTray;
+import org.apache.commons.httpclient.HttpsURL;
+import org.apache.commons.httpclient.params.HttpConnectionParams;
+import org.apache.commons.httpclient.protocol.Protocol;
+import org.apache.commons.httpclient.protocol.ProtocolSocketFactory;
+import org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory;
+
+import javax.net.ssl.*;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import java.io.File;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.MalformedURLException;
+import java.net.Socket;
+import java.net.URL;
+import java.security.*;
+import java.util.ArrayList;
+
+/**
+ * Manual Socket Factory.
+ * Let user choose to accept or reject certificate
+ */
+public class DavGatewaySSLProtocolSocketFactory implements SecureProtocolSocketFactory {
+ /**
+ * Register custom Socket Factory to let user accept or reject certificate
+ */
+ public static void register() {
+ String urlString = Settings.getProperty("davmail.url");
+ try {
+ URL url = new URL(urlString);
+ String protocol = url.getProtocol();
+ if ("https".equals(protocol)) {
+ int port = url.getPort();
+ if (port < 0) {
+ port = HttpsURL.DEFAULT_PORT;
+ }
+ Protocol.registerProtocol(url.getProtocol(),
+ new Protocol(protocol, (ProtocolSocketFactory) new DavGatewaySSLProtocolSocketFactory(), port));
+ }
+ } catch (MalformedURLException e) {
+ DavGatewayTray.error(new BundleMessage("LOG_INVALID_URL", urlString));
+ }
+ }
+
+ private KeyStore.ProtectionParameter getProtectionParameter(String password) {
+ if (password != null && password.length() > 0) {
+ // password provided: create a PasswordProtection
+ return new KeyStore.PasswordProtection(password.toCharArray());
+ } else {
+ // request password at runtime through a callback
+ return new KeyStore.CallbackHandlerProtection(new CallbackHandler() {
+ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+ if (callbacks.length > 0 && callbacks[0] instanceof PasswordCallback) {
+ PasswordPromptDialog passwordPromptDialog = new PasswordPromptDialog(((PasswordCallback) callbacks[0]).getPrompt());
+ ((PasswordCallback) callbacks[0]).setPassword(passwordPromptDialog.getPassword());
+ }
+ }
+ });
+ }
+ }
+
+ private SSLContext sslcontext;
+
+ private SSLContext createSSLContext() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, KeyManagementException, KeyStoreException {
+ // PKCS11 client certificate settings
+ String pkcs11Library = Settings.getProperty("davmail.ssl.pkcs11Library");
+
+ String clientKeystoreType = Settings.getProperty("davmail.ssl.clientKeystoreType");
+ // set default keystore type
+ if (clientKeystoreType == null || clientKeystoreType.length() == 0) {
+ clientKeystoreType = "PKCS11";
+ }
+
+ if (pkcs11Library != null && pkcs11Library.length() > 0 && "PKCS11".equals(clientKeystoreType)) {
+ StringBuilder pkcs11Buffer = new StringBuilder();
+ pkcs11Buffer.append("name=DavMail\n");
+ pkcs11Buffer.append("library=").append(pkcs11Library).append('\n');
+ String pkcs11Config = Settings.getProperty("davmail.ssl.pkcs11Config");
+ if (pkcs11Config != null && pkcs11Config.length() > 0) {
+ pkcs11Buffer.append(pkcs11Config).append('\n');
+ }
+ SunPKCS11ProviderHandler.registerProvider(pkcs11Buffer.toString());
+ }
+
+ KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(/*KeyManagerFactory.getDefaultAlgorithm()*/"NewSunX509");
+
+ ArrayList<KeyStore.Builder> keyStoreBuilders = new ArrayList<KeyStore.Builder>();
+ // PKCS11 (smartcard) keystore with password callback
+ KeyStore.Builder scBuilder = KeyStore.Builder.newInstance("PKCS11", null, getProtectionParameter(null));
+ keyStoreBuilders.add(scBuilder);
+
+ String clientKeystoreFile = Settings.getProperty("davmail.ssl.clientKeystoreFile");
+ String clientKeystorePass = Settings.getProperty("davmail.ssl.clientKeystorePass");
+ if (clientKeystoreFile != null && clientKeystoreFile.length() > 0
+ && ("PKCS12".equals(clientKeystoreType) || "JKS".equals(clientKeystoreType))) {
+ // PKCS12 file based keystore
+ KeyStore.Builder fsBuilder = KeyStore.Builder.newInstance(clientKeystoreType, null,
+ new File(clientKeystoreFile), getProtectionParameter(clientKeystorePass));
+ keyStoreBuilders.add(fsBuilder);
+ }
+
+ ManagerFactoryParameters keyStoreBuilderParameters = new KeyStoreBuilderParameters(keyStoreBuilders);
+ keyManagerFactory.init(keyStoreBuilderParameters);
+
+ // Get a list of key managers
+ KeyManager[] keyManagers = keyManagerFactory.getKeyManagers();
+
+ // Walk through the key managers and replace all X509 Key Managers with
+ // a specialized wrapped DavMail X509 Key Manager
+ for (int i = 0; i < keyManagers.length; i++) {
+ KeyManager keyManager = keyManagers[i];
+ if (keyManager instanceof X509KeyManager) {
+ keyManagers[i] = new DavMailX509KeyManager((X509KeyManager) keyManager);
+ }
+ }
+
+ SSLContext context = SSLContext.getInstance("SSL");
+ context.init(keyManagers, new TrustManager[]{new DavGatewayX509TrustManager()}, null);
+ return context;
+ }
+
+ private SSLContext getSSLContext() throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, InvalidAlgorithmParameterException {
+ if (this.sslcontext == null) {
+ this.sslcontext = createSSLContext();
+ }
+ return this.sslcontext;
+ }
+
+ public Socket createSocket(String host, int port, InetAddress clientHost, int clientPort) throws IOException {
+ try {
+ return getSSLContext().getSocketFactory().createSocket(host, port, clientHost, clientPort);
+ } catch (NoSuchAlgorithmException e) {
+ throw new IOException(e + " " + e.getMessage());
+ } catch (KeyManagementException e) {
+ throw new IOException(e + " " + e.getMessage());
+ } catch (KeyStoreException e) {
+ throw new IOException(e + " " + e.getMessage());
+ } catch (InvalidAlgorithmParameterException e) {
+ throw new IOException(e + " " + e.getMessage());
+ }
+ }
+
+ public Socket createSocket(String host, int port, InetAddress clientHost, int clientPort, HttpConnectionParams params) throws IOException {
+ try {
+ return getSSLContext().getSocketFactory().createSocket(host, port, clientHost, clientPort);
+ } catch (NoSuchAlgorithmException e) {
+ throw new IOException(e + " " + e.getMessage());
+ } catch (KeyManagementException e) {
+ throw new IOException(e + " " + e.getMessage());
+ } catch (KeyStoreException e) {
+ throw new IOException(e + " " + e.getMessage());
+ } catch (InvalidAlgorithmParameterException e) {
+ throw new IOException(e + " " + e.getMessage());
+ }
+ }
+
+ public Socket createSocket(String host, int port) throws IOException {
+ try {
+ return getSSLContext().getSocketFactory().createSocket(host, port);
+ } catch (NoSuchAlgorithmException e) {
+ throw new IOException(e + " " + e.getMessage());
+ } catch (KeyManagementException e) {
+ throw new IOException(e + " " + e.getMessage());
+ } catch (KeyStoreException e) {
+ throw new IOException(e + " " + e.getMessage());
+ } catch (InvalidAlgorithmParameterException e) {
+ throw new IOException(e + " " + e.getMessage());
+ }
+ }
+
+ public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException {
+ try {
+ return getSSLContext().getSocketFactory().createSocket(socket, host, port, autoClose);
+ } catch (NoSuchAlgorithmException e) {
+ throw new IOException(e + " " + e.getMessage());
+ } catch (KeyManagementException e) {
+ throw new IOException(e + " " + e.getMessage());
+ } catch (KeyStoreException e) {
+ throw new IOException(e + " " + e.getMessage());
+ } catch (InvalidAlgorithmParameterException e) {
+ throw new IOException(e + " " + e.getMessage());
+ }
+ }
+
+ /**
+ * All instances of SSLProtocolSocketFactory are the same.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ return ((obj != null) && obj.getClass().equals(this.getClass()));
+ }
+
+ /**
+ * All instances of SSLProtocolSocketFactory have the same hash code.
+ */
+ @Override
+ public int hashCode() {
+ return this.getClass().hashCode();
+ }
+}
diff --git a/src/java/davmail/http/DavGatewayX509TrustManager.java b/src/java/davmail/http/DavGatewayX509TrustManager.java
new file mode 100644
index 0000000..b60509e
--- /dev/null
+++ b/src/java/davmail/http/DavGatewayX509TrustManager.java
@@ -0,0 +1,214 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.http;
+
+import davmail.BundleMessage;
+import davmail.Settings;
+import davmail.ui.AcceptCertificateDialog;
+import davmail.ui.tray.DavGatewayTray;
+
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+import java.awt.*;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.security.*;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.text.SimpleDateFormat;
+
+/**
+ * Custom Trust Manager, let user accept or deny.
+ */
+public class DavGatewayX509TrustManager implements X509TrustManager {
+ private final X509TrustManager standardTrustManager;
+
+ /**
+ * Create a new custom X509 trust manager.
+ *
+ * @throws NoSuchAlgorithmException on error
+ * @throws KeyStoreException on error
+ */
+ public DavGatewayX509TrustManager() throws NoSuchAlgorithmException, KeyStoreException {
+ TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ factory.init((KeyStore) null);
+ TrustManager[] trustManagers = factory.getTrustManagers();
+ if (trustManagers.length == 0) {
+ throw new NoSuchAlgorithmException("No trust manager found");
+ }
+ this.standardTrustManager = (X509TrustManager) trustManagers[0];
+ }
+
+ public void checkServerTrusted(X509Certificate[] x509Certificates, String authType) throws CertificateException {
+ try {
+ // first try standard Trust Manager
+ this.standardTrustManager.checkServerTrusted(x509Certificates, authType);
+ } catch (CertificateException e) {
+ if ((x509Certificates != null) && (x509Certificates.length > 0)) {
+ userCheckServerTrusted(x509Certificates);
+ } else {
+ throw e;
+ }
+
+ }
+ }
+
+ public void checkClientTrusted(X509Certificate[] x509Certificates, String authType) throws CertificateException {
+ this.standardTrustManager.checkClientTrusted(x509Certificates, authType);
+ }
+
+ public X509Certificate[] getAcceptedIssuers() {
+ return this.standardTrustManager.getAcceptedIssuers();
+ }
+
+ protected void userCheckServerTrusted(final X509Certificate[] x509Certificates) throws CertificateException {
+ String acceptedCertificateHash = Settings.getProperty("davmail.server.certificate.hash");
+ String certificateHash = getFormattedHash(x509Certificates[0]);
+ // if user already accepted a certificate,
+ if (acceptedCertificateHash != null && acceptedCertificateHash.length() > 0
+ && acceptedCertificateHash.equals(certificateHash)) {
+ DavGatewayTray.debug(new BundleMessage("LOG_FOUND_ACCEPTED_CERTIFICATE", acceptedCertificateHash));
+ } else {
+ boolean isCertificateTrusted;
+ if (Settings.getBooleanProperty("davmail.server") || GraphicsEnvironment.isHeadless()) {
+ // headless or server mode
+ isCertificateTrusted = isCertificateTrusted(x509Certificates[0]);
+ } else {
+ isCertificateTrusted = AcceptCertificateDialog.isCertificateTrusted(x509Certificates[0]);
+ }
+ if (!isCertificateTrusted) {
+ throw new CertificateException("User rejected certificate");
+ }
+ // certificate accepted, store in settings
+ Settings.saveProperty("davmail.server.certificate.hash", certificateHash);
+ }
+ }
+
+ @SuppressWarnings({"UseOfSystemOutOrSystemErr"})
+ protected boolean isCertificateTrusted(X509Certificate certificate) {
+ BufferedReader inReader = new BufferedReader(new InputStreamReader(System.in));
+ String answer = null;
+ String yes = BundleMessage.format("UI_ANSWER_YES");
+ String no = BundleMessage.format("UI_ANSWER_NO");
+ StringBuilder buffer = new StringBuilder();
+ buffer.append(BundleMessage.format("UI_SERVER_CERTIFICATE")).append(":\n");
+ buffer.append(BundleMessage.format("UI_ISSUED_TO")).append(": ")
+ .append(DavGatewayX509TrustManager.getRDN(certificate.getSubjectDN())).append('\n');
+ buffer.append(BundleMessage.format("UI_ISSUED_BY")).append(": ")
+ .append(getRDN(certificate.getIssuerDN())).append('\n');
+ SimpleDateFormat formatter = new SimpleDateFormat("MM/dd/yyyy");
+ String notBefore = formatter.format(certificate.getNotBefore());
+ buffer.append(BundleMessage.format("UI_VALID_FROM")).append(": ").append(notBefore).append('\n');
+ String notAfter = formatter.format(certificate.getNotAfter());
+ buffer.append(BundleMessage.format("UI_VALID_UNTIL")).append(": ").append(notAfter).append('\n');
+ buffer.append(BundleMessage.format("UI_SERIAL")).append(": ").append(getFormattedSerial(certificate)).append('\n');
+ String sha1Hash = DavGatewayX509TrustManager.getFormattedHash(certificate);
+ buffer.append(BundleMessage.format("UI_FINGERPRINT")).append(": ").append(sha1Hash).append('\n');
+ buffer.append('\n');
+ buffer.append(BundleMessage.format("UI_UNTRUSTED_CERTIFICATE")).append('\n');
+ try {
+ while (!yes.equals(answer) && !no.equals(answer)) {
+ System.out.println(buffer.toString());
+ answer = inReader.readLine();
+ if (answer == null) {
+ answer = no;
+ }
+ answer = answer.toLowerCase();
+ }
+ } catch (IOException e) {
+ System.err.println(e);
+ }
+ return yes.equals(answer);
+ }
+
+ /**
+ * Get rdn value from principal dn.
+ *
+ * @param principal security principal
+ * @return rdn
+ */
+ public static String getRDN(Principal principal) {
+ String dn = principal.getName();
+ int start = dn.indexOf('=');
+ int end = dn.indexOf(',');
+ if (start >= 0 && end >= 0) {
+ return dn.substring(start + 1, end);
+ } else {
+ return dn;
+ }
+ }
+
+ /**
+ * Build a formatted certificate serial string.
+ *
+ * @param certificate X509 certificate
+ * @return formatted serial
+ */
+ public static String getFormattedSerial(X509Certificate certificate) {
+ StringBuilder builder = new StringBuilder();
+ String serial = certificate.getSerialNumber().toString(16);
+ for (int i = 0; i < serial.length(); i++) {
+ if (i > 0 && i % 2 == 0) {
+ builder.append(' ');
+ }
+ builder.append(serial.charAt(i));
+ }
+ return builder.toString().toUpperCase();
+ }
+
+ /**
+ * Build a formatted hash string.
+ *
+ * @param certificate X509 certificate
+ * @return formatted hash
+ */
+ public static String getFormattedHash(X509Certificate certificate) {
+ String sha1Hash;
+ try {
+ MessageDigest md = MessageDigest.getInstance("SHA1");
+ byte[] digest = md.digest(certificate.getEncoded());
+ sha1Hash = formatHash(digest);
+ } catch (NoSuchAlgorithmException nsa) {
+ sha1Hash = nsa.getMessage();
+ } catch (CertificateEncodingException cee) {
+ sha1Hash = cee.getMessage();
+ }
+ return sha1Hash;
+ }
+
+ /**
+ * Format byte buffer to a hexadecimal hash string.
+ *
+ * @param buffer byte array
+ * @return hexadecimal hash string
+ */
+ protected static String formatHash(byte[] buffer) {
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < buffer.length; i++) {
+ if (i > 0) {
+ builder.append(':');
+ }
+ builder.append(Integer.toHexString(buffer[i] & 0xFF));
+ }
+ return builder.toString().toUpperCase();
+ }
+}
diff --git a/src/java/davmail/http/DavMailCookieSpec.java b/src/java/davmail/http/DavMailCookieSpec.java
new file mode 100644
index 0000000..f20537b
--- /dev/null
+++ b/src/java/davmail/http/DavMailCookieSpec.java
@@ -0,0 +1,62 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.http;
+
+import org.apache.commons.httpclient.Cookie;
+import org.apache.commons.httpclient.cookie.MalformedCookieException;
+import org.apache.commons.httpclient.cookie.RFC2109Spec;
+
+/**
+ * Custom CookieSpec to allow extended domain names.
+ */
+public class DavMailCookieSpec extends RFC2109Spec {
+ @Override
+ public void validate(String host, int port, String path,
+ boolean secure, final Cookie cookie) throws MalformedCookieException {
+ // workaround for space in cookie name
+ String cookieName = cookie.getName();
+ if (cookieName != null && cookieName.indexOf(' ') >= 0) {
+ cookie.setName(cookieName.replaceAll(" ", ""));
+ } else {
+ cookieName = null;
+ }
+ // workaround for invalid cookie path
+ String cookiePath = cookie.getPath();
+ if (cookiePath != null && !path.startsWith(cookiePath)) {
+ cookie.setPath(path);
+ } else {
+ cookiePath = null;
+ }
+ String hostWithoutDomain = host.substring(0, host.length()
+ - cookie.getDomain().length());
+ int dotIndex = hostWithoutDomain.indexOf('.');
+ if (dotIndex != -1) {
+ // discard additional host name part
+ super.validate(host.substring(dotIndex + 1), port, path, secure, cookie);
+ } else {
+ super.validate(host, port, path, secure, cookie);
+ }
+ if (cookieName != null) {
+ cookie.setName(cookieName);
+ }
+ if (cookiePath != null) {
+ cookie.setPath(cookiePath);
+ }
+ }
+}
diff --git a/src/java/davmail/http/DavMailX509KeyManager.java b/src/java/davmail/http/DavMailX509KeyManager.java
new file mode 100644
index 0000000..7c3bffb
--- /dev/null
+++ b/src/java/davmail/http/DavMailX509KeyManager.java
@@ -0,0 +1,165 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2011 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.http;
+
+import davmail.ui.SelectCertificateDialog;
+import org.apache.log4j.Logger;
+
+import javax.net.ssl.X509KeyManager;
+import java.net.Socket;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Special X509 Key Manager that handles cases where more than one private key
+ * is sufficient to establish the HTTPs connection by asking the user to
+ * select one.
+ */
+public class DavMailX509KeyManager implements X509KeyManager {
+
+ protected static final Logger LOGGER = Logger.getLogger(DavMailX509KeyManager.class);
+
+ // Wrap an existing key manager to handle most of the interface as a pass through
+ private final X509KeyManager keyManager;
+
+ // Remember selected alias so we don't continually bug the user
+ private String cachedAlias;
+
+ /**
+ * Build the specialized key manager wrapping the default one
+ *
+ * @param keyManager original key manager
+ */
+ public DavMailX509KeyManager(X509KeyManager keyManager) {
+ this.keyManager = keyManager;
+ }
+
+ /**
+ * Get the client aliases, simply pass this through to wrapped key manager
+ */
+ public String[] getClientAliases(String string, Principal[] principals) {
+ return keyManager.getClientAliases(string, principals);
+ }
+
+ /**
+ * Select a client alias. Some servers are misconfigured and claim to accept
+ * any client certificate during the SSL handshake, however OWA only authenticates
+ * using a single certificate.
+ * <p/>
+ * This method allows the user to select the right client certificate
+ */
+ public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
+ // Build a list of all aliases
+ ArrayList<String> aliases = new ArrayList<String>();
+ for (String keyTypeValue : keyType) {
+ String[] keyAliases = keyManager.getClientAliases(keyTypeValue, null);
+
+ if (keyAliases != null) {
+ aliases.addAll(Arrays.asList(keyAliases));
+ }
+ }
+
+ // If there are more than one show a dialog and return the selected alias
+ if (aliases.size() > 1) {
+
+ //If there's a saved pattern try to match it
+ if (cachedAlias != null) {
+ for (String alias : aliases) {
+ if (cachedAlias.equals(stripAlias(alias))) {
+ LOGGER.debug(alias + " matched cached alias: " + cachedAlias);
+ return alias;
+ }
+ }
+
+ // pattern didn't match, clear the pattern and ask user to select an alias
+ cachedAlias = null;
+ }
+
+ String[] aliasesArray = aliases.toArray(new String[aliases.size()]);
+ SelectCertificateDialog selectCertificateDialog = new SelectCertificateDialog(aliasesArray);
+
+ LOGGER.debug("User selected Key Alias: " + selectCertificateDialog.getSelectedAlias());
+
+ cachedAlias = stripAlias(selectCertificateDialog.getSelectedAlias());
+ LOGGER.debug("Stored Key Alias Pattern: " + cachedAlias);
+
+ return selectCertificateDialog.getSelectedAlias();
+
+ // exactly one, simply return that and don't bother the user
+ } else if (aliases.size() == 1) {
+ LOGGER.debug("One Private Key found, returning that");
+ return aliases.get(0);
+
+ // none, return null
+ } else {
+ LOGGER.debug("No Private Keys found");
+ return null;
+ }
+ }
+
+ /**
+ * PKCS11 aliases are in the format: dd.0, dd is incremented
+ * every time the SSL connection is re-negotiated
+ *
+ * @param alias original alias
+ * @return alias without prefix
+ */
+ protected String stripAlias(String alias) {
+ String value = alias;
+ if (value != null && value.length() > 1) {
+ char firstChar = value.charAt(0);
+ int dotIndex = value.indexOf('.');
+ if (firstChar >= '0' && firstChar <= '9' && dotIndex >= 0) {
+ value = value.substring(dotIndex+1);
+ }
+ }
+ return value;
+ }
+
+ /**
+ * Passthrough to wrapped keymanager
+ */
+ public String[] getServerAliases(String string, Principal[] prncpls) {
+ return keyManager.getServerAliases(string, prncpls);
+ }
+
+ /**
+ * Passthrough to wrapped keymanager
+ */
+ public String chooseServerAlias(String string, Principal[] prncpls, Socket socket) {
+ return keyManager.chooseServerAlias(string, prncpls, socket);
+ }
+
+ /**
+ * Passthrough to wrapped keymanager
+ */
+ public X509Certificate[] getCertificateChain(String string) {
+ return keyManager.getCertificateChain(string);
+ }
+
+ /**
+ * Passthrough to wrapped keymanager
+ */
+ public PrivateKey getPrivateKey(String string) {
+ return keyManager.getPrivateKey(string);
+ }
+}
diff --git a/src/java/davmail/http/LenientBasicScheme.java b/src/java/davmail/http/LenientBasicScheme.java
new file mode 100644
index 0000000..95bc204
--- /dev/null
+++ b/src/java/davmail/http/LenientBasicScheme.java
@@ -0,0 +1,36 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2011 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.http;
+
+import org.apache.commons.httpclient.auth.BasicScheme;
+import org.apache.commons.httpclient.auth.MalformedChallengeException;
+
+/**
+ * Workaround for broken servers that send invalid Basic authentication challenge.
+ */
+public class LenientBasicScheme extends BasicScheme {
+ public void processChallenge(String challenge)
+ throws MalformedChallengeException {
+ if ("Basic".equalsIgnoreCase(challenge)) {
+ super.processChallenge("Basic \"default\"");
+ } else {
+ super.processChallenge(challenge);
+ }
+ }
+}
diff --git a/src/java/davmail/http/NTLMv2Scheme.java b/src/java/davmail/http/NTLMv2Scheme.java
new file mode 100644
index 0000000..7193396
--- /dev/null
+++ b/src/java/davmail/http/NTLMv2Scheme.java
@@ -0,0 +1,200 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.http;
+
+import jcifs.ntlmssp.NtlmFlags;
+import jcifs.ntlmssp.Type1Message;
+import jcifs.ntlmssp.Type2Message;
+import jcifs.ntlmssp.Type3Message;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.httpclient.Credentials;
+import org.apache.commons.httpclient.HttpMethod;
+import org.apache.commons.httpclient.NTCredentials;
+import org.apache.commons.httpclient.auth.*;
+import org.apache.commons.httpclient.util.EncodingUtil;
+
+import java.io.IOException;
+
+/**
+ * NTLMv2 scheme implementation.
+ */
+public class NTLMv2Scheme implements AuthScheme {
+ private static final int UNINITIATED = 0;
+ private static final int INITIATED = 1;
+ private static final int TYPE1_MSG_GENERATED = 2;
+ private static final int TYPE2_MSG_RECEIVED = 3;
+ private static final int TYPE3_MSG_GENERATED = 4;
+ private static final int FAILED = Integer.MAX_VALUE;
+
+ private Type2Message type2Message;
+ /**
+ * Authentication process state
+ */
+ private int state;
+
+ /**
+ * Processes the NTLM challenge.
+ *
+ * @param challenge the challenge string
+ * @throws MalformedChallengeException is thrown if the authentication challenge
+ * is malformed
+ */
+ public void processChallenge(final String challenge) throws MalformedChallengeException {
+ String authScheme = AuthChallengeParser.extractScheme(challenge);
+ if (!authScheme.equalsIgnoreCase(getSchemeName())) {
+ throw new MalformedChallengeException("Invalid NTLM challenge: " + challenge);
+ }
+ int spaceIndex = challenge.indexOf(' ');
+ if (spaceIndex != -1) {
+ try {
+ type2Message = new Type2Message(Base64.decodeBase64(EncodingUtil.getBytes(
+ challenge.substring(spaceIndex, challenge.length()).trim(), "ASCII")));
+ } catch (IOException e) {
+ throw new MalformedChallengeException("Invalid NTLM challenge: " + challenge, e);
+ }
+ this.state = TYPE2_MSG_RECEIVED;
+ } else {
+ this.type2Message = null;
+ if (this.state == UNINITIATED) {
+ this.state = INITIATED;
+ } else {
+ this.state = FAILED;
+ }
+ }
+ }
+
+
+ /**
+ * Returns textual designation of the NTLM authentication scheme.
+ *
+ * @return <code>ntlm</code>
+ */
+ public String getSchemeName() {
+ return "ntlm";
+ }
+
+ /**
+ * Not used with NTLM.
+ *
+ * @return null
+ */
+ public String getParameter(String s) {
+ return null;
+ }
+
+ /**
+ * Not used with NTLM.
+ *
+ * @return null
+ */
+ public String getRealm() {
+ return null;
+ }
+
+ /**
+ * Deprecated.
+ */
+ @Deprecated
+ public String getID() {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * NTLM is connection based.
+ *
+ * @return true
+ */
+ public boolean isConnectionBased() {
+ return true;
+ }
+
+ /**
+ * Tests if the NTLM authentication process has been completed.
+ *
+ * @return <tt>true</tt> if authorization has been processed
+ */
+ public boolean isComplete() {
+ return state == TYPE3_MSG_GENERATED || state == FAILED;
+ }
+
+ /**
+ * Not implemented.
+ *
+ * @param credentials user credentials
+ * @param method method name
+ * @param uri URI
+ * @return an NTLM authorization string
+ * @throws InvalidCredentialsException if authentication credentials
+ * are not valid or not applicable for this authentication scheme
+ * @throws AuthenticationException if authorization string cannot
+ * be generated due to an authentication failure
+ */
+ @Deprecated
+ public String authenticate(final Credentials credentials, String method, String uri) throws AuthenticationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Produces NTLM authorization string for the given set of
+ * {@link Credentials}.
+ *
+ * @param credentials The set of credentials to be used for authentication
+ * @param httpMethod The method being authenticated
+ * @return an NTLM authorization string
+ * @throws InvalidCredentialsException if authentication credentials
+ * are not valid or not applicable for this authentication scheme
+ * @throws AuthenticationException if authorization string cannot
+ * be generated due to an authentication failure
+ */
+ public String authenticate(Credentials credentials, HttpMethod httpMethod) throws AuthenticationException {
+ if (this.state == UNINITIATED) {
+ throw new IllegalStateException("NTLM authentication process has not been initiated");
+ }
+
+ NTCredentials ntcredentials;
+ try {
+ ntcredentials = (NTCredentials) credentials;
+ } catch (ClassCastException e) {
+ throw new InvalidCredentialsException(
+ "Credentials cannot be used for NTLM authentication: "
+ + credentials.getClass().getName());
+ }
+ String response;
+ if (this.state == INITIATED || this.state == FAILED) {
+ int flags = NtlmFlags.NTLMSSP_NEGOTIATE_NTLM2 | NtlmFlags.NTLMSSP_NEGOTIATE_ALWAYS_SIGN |
+ NtlmFlags.NTLMSSP_NEGOTIATE_OEM_WORKSTATION_SUPPLIED | NtlmFlags.NTLMSSP_NEGOTIATE_OEM_DOMAIN_SUPPLIED |
+ NtlmFlags.NTLMSSP_NEGOTIATE_NTLM | NtlmFlags.NTLMSSP_REQUEST_TARGET |
+ NtlmFlags.NTLMSSP_NEGOTIATE_OEM | NtlmFlags.NTLMSSP_NEGOTIATE_UNICODE |
+ NtlmFlags.NTLMSSP_NEGOTIATE_56 | NtlmFlags.NTLMSSP_NEGOTIATE_128;
+ Type1Message type1Message = new Type1Message(flags, ntcredentials.getDomain(), ntcredentials.getHost());
+ response = EncodingUtil.getAsciiString(Base64.encodeBase64(type1Message.toByteArray()));
+ this.state = TYPE1_MSG_GENERATED;
+ } else {
+ int flags = NtlmFlags.NTLMSSP_NEGOTIATE_NTLM2 | NtlmFlags.NTLMSSP_NEGOTIATE_ALWAYS_SIGN |
+ NtlmFlags.NTLMSSP_NEGOTIATE_NTLM | NtlmFlags.NTLMSSP_NEGOTIATE_UNICODE;
+ Type3Message type3Message = new Type3Message(type2Message, ntcredentials.getPassword(),
+ ntcredentials.getDomain(), ntcredentials.getUserName(), ntcredentials.getHost(), flags);
+ response = EncodingUtil.getAsciiString(Base64.encodeBase64(type3Message.toByteArray()));
+ this.state = TYPE3_MSG_GENERATED;
+ }
+ return "NTLM " + response;
+ }
+
+
+}
diff --git a/src/java/davmail/http/SunPKCS11ProviderHandler.java b/src/java/davmail/http/SunPKCS11ProviderHandler.java
new file mode 100644
index 0000000..c84d644
--- /dev/null
+++ b/src/java/davmail/http/SunPKCS11ProviderHandler.java
@@ -0,0 +1,45 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.http;
+
+import sun.security.pkcs11.SunPKCS11;
+
+import java.io.ByteArrayInputStream;
+import java.security.Provider;
+import java.security.Security;
+
+/**
+ * Add the SunPKCS11 Provider.
+ */
+public final class SunPKCS11ProviderHandler {
+
+ private SunPKCS11ProviderHandler() {
+ }
+
+ /**
+ * Register PKCS11 provider.
+ *
+ * @param pkcs11Config PKCS11 config string
+ */
+ public static void registerProvider(String pkcs11Config) {
+ Provider p = new SunPKCS11(new ByteArrayInputStream(pkcs11Config.getBytes()));
+ Security.addProvider(p);
+ }
+
+}
diff --git a/src/java/davmail/imap/ImapConnection.java b/src/java/davmail/imap/ImapConnection.java
new file mode 100644
index 0000000..d54ba81
--- /dev/null
+++ b/src/java/davmail/imap/ImapConnection.java
@@ -0,0 +1,1816 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.imap;
+
+import com.sun.mail.imap.protocol.BASE64MailboxDecoder;
+import com.sun.mail.imap.protocol.BASE64MailboxEncoder;
+import davmail.AbstractConnection;
+import davmail.BundleMessage;
+import davmail.DavGateway;
+import davmail.Settings;
+import davmail.exception.DavMailException;
+import davmail.exception.HttpForbiddenException;
+import davmail.exception.HttpNotFoundException;
+import davmail.exception.InsufficientStorageException;
+import davmail.exchange.ExchangeSession;
+import davmail.exchange.ExchangeSessionFactory;
+import davmail.ui.tray.DavGatewayTray;
+import davmail.util.IOUtil;
+import davmail.util.StringUtil;
+import org.apache.commons.httpclient.HttpException;
+import org.apache.log4j.Logger;
+
+import javax.mail.MessagingException;
+import javax.mail.internet.*;
+import javax.mail.util.SharedByteArrayInputStream;
+import java.io.*;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+/**
+ * Dav Gateway smtp connection implementation.
+ * Still alpha code : need to find a way to handle message ids
+ */
+public class ImapConnection extends AbstractConnection {
+ private static final Logger LOGGER = Logger.getLogger(ImapConnection.class);
+
+ protected String baseMailboxPath;
+ ExchangeSession.Folder currentFolder;
+
+ /**
+ * Initialize the streams and start the thread.
+ *
+ * @param clientSocket IMAP client socket
+ */
+ public ImapConnection(Socket clientSocket) {
+ super(ImapConnection.class.getSimpleName(), clientSocket, "UTF-8");
+ }
+
+ @Override
+ public void run() {
+ final String capabilities;
+ int imapIdleDelay = Settings.getIntProperty("davmail.imapIdleDelay") * 60;
+ if (imapIdleDelay > 0) {
+ capabilities = "CAPABILITY IMAP4REV1 AUTH=LOGIN IDLE MOVE";
+ } else {
+ capabilities = "CAPABILITY IMAP4REV1 AUTH=LOGIN MOVE";
+ }
+
+ String line;
+ String commandId = null;
+ IMAPTokenizer tokens;
+ try {
+ ExchangeSessionFactory.checkConfig();
+ sendClient("* OK [" + capabilities + "] IMAP4rev1 DavMail " + DavGateway.getCurrentVersion() + " server ready");
+ for (; ; ) {
+ line = readClient();
+ // unable to read line, connection closed ?
+ if (line == null) {
+ break;
+ }
+
+ tokens = new IMAPTokenizer(line);
+ if (tokens.hasMoreTokens()) {
+ commandId = tokens.nextToken();
+
+ checkInfiniteLoop(line);
+
+ if (tokens.hasMoreTokens()) {
+ String command = tokens.nextToken();
+
+ if ("LOGOUT".equalsIgnoreCase(command)) {
+ sendClient("* BYE Closing connection");
+ sendClient(commandId + " OK LOGOUT completed");
+ break;
+ }
+ if ("capability".equalsIgnoreCase(command)) {
+ sendClient("* " + capabilities);
+ sendClient(commandId + " OK CAPABILITY completed");
+ } else if ("login".equalsIgnoreCase(command)) {
+ parseCredentials(tokens);
+ // detect shared mailbox access
+ splitUserName();
+ try {
+ session = ExchangeSessionFactory.getInstance(userName, password);
+ sendClient(commandId + " OK Authenticated");
+ state = State.AUTHENTICATED;
+ } catch (Exception e) {
+ DavGatewayTray.error(e);
+ sendClient(commandId + " NO LOGIN failed");
+ state = State.INITIAL;
+ }
+ } else if ("AUTHENTICATE".equalsIgnoreCase(command)) {
+ if (tokens.hasMoreTokens()) {
+ String authenticationMethod = tokens.nextToken();
+ if ("LOGIN".equalsIgnoreCase(authenticationMethod)) {
+ try {
+ sendClient("+ " + base64Encode("Username:"));
+ state = State.LOGIN;
+ userName = base64Decode(readClient());
+ // detect shared mailbox access
+ splitUserName();
+ sendClient("+ " + base64Encode("Password:"));
+ state = State.PASSWORD;
+ password = base64Decode(readClient());
+ session = ExchangeSessionFactory.getInstance(userName, password);
+ sendClient(commandId + " OK Authenticated");
+ state = State.AUTHENTICATED;
+ } catch (Exception e) {
+ DavGatewayTray.error(e);
+ sendClient(commandId + " NO LOGIN failed");
+ state = State.INITIAL;
+ }
+ } else {
+ sendClient(commandId + " NO unsupported authentication method");
+ }
+ } else {
+ sendClient(commandId + " BAD authentication method required");
+ }
+ } else {
+ if (state != State.AUTHENTICATED) {
+ sendClient(commandId + " BAD command authentication required");
+ } else {
+ // check for expired session
+ session = ExchangeSessionFactory.getInstance(session, userName, password);
+ if ("lsub".equalsIgnoreCase(command) || "list".equalsIgnoreCase(command)) {
+ if (tokens.hasMoreTokens()) {
+ String folderContext;
+ if (baseMailboxPath == null) {
+ folderContext = BASE64MailboxDecoder.decode(tokens.nextToken());
+ } else {
+ folderContext = baseMailboxPath + BASE64MailboxDecoder.decode(tokens.nextToken());
+ }
+ if (tokens.hasMoreTokens()) {
+ String folderQuery = folderContext + BASE64MailboxDecoder.decode(tokens.nextToken());
+ if (folderQuery.endsWith("%/%") && !"/%/%".equals(folderQuery)) {
+ List<ExchangeSession.Folder> folders = session.getSubFolders(folderQuery.substring(0, folderQuery.length() - 3), false);
+ for (ExchangeSession.Folder folder : folders) {
+ sendClient("* " + command + " (" + folder.getFlags() + ") \"/\" \"" + BASE64MailboxEncoder.encode(folder.folderPath) + '\"');
+ sendSubFolders(command, folder.folderPath, false);
+ }
+ sendClient(commandId + " OK " + command + " completed");
+ } else if (folderQuery.endsWith("%") || folderQuery.endsWith("*")) {
+ if ("/*".equals(folderQuery) || "/%".equals(folderQuery) || "/%/%".equals(folderQuery)) {
+ folderQuery = folderQuery.substring(1);
+ if ("%/%".equals(folderQuery)) {
+ folderQuery = folderQuery.substring(0, folderQuery.length() - 2);
+ }
+ sendClient("* " + command + " (\\HasChildren) \"/\" \"/public\"");
+ }
+ if ("*%".equals(folderQuery)) {
+ folderQuery = "*";
+ }
+ boolean recursive = folderQuery.endsWith("*") && !folderQuery.startsWith("/public");
+ sendSubFolders(command, folderQuery.substring(0, folderQuery.length() - 1), recursive);
+ sendClient(commandId + " OK " + command + " completed");
+ } else {
+ ExchangeSession.Folder folder = null;
+ try {
+ folder = session.getFolder(folderQuery);
+ } catch (HttpForbiddenException e) {
+ // access forbidden, ignore
+ DavGatewayTray.debug(new BundleMessage("LOG_FOLDER_ACCESS_FORBIDDEN", folderQuery));
+ } catch (HttpNotFoundException e) {
+ // not found, ignore
+ DavGatewayTray.debug(new BundleMessage("LOG_FOLDER_NOT_FOUND", folderQuery));
+ } catch (HttpException e) {
+ // other errors, ignore
+ DavGatewayTray.debug(new BundleMessage("LOG_FOLDER_ACCESS_ERROR", folderQuery, e.getMessage()));
+ }
+ if (folder != null) {
+ sendClient("* " + command + " (" + folder.getFlags() + ") \"/\" \"" + BASE64MailboxEncoder.encode(folder.folderPath) + '\"');
+ sendClient(commandId + " OK " + command + " completed");
+ } else {
+ sendClient(commandId + " NO Folder not found");
+ }
+ }
+ } else {
+ sendClient(commandId + " BAD missing folder argument");
+ }
+ } else {
+ sendClient(commandId + " BAD missing folder argument");
+ }
+ } else if ("select".equalsIgnoreCase(command) || "examine".equalsIgnoreCase(command)) {
+ if (tokens.hasMoreTokens()) {
+ @SuppressWarnings({"NonConstantStringShouldBeStringBuffer"})
+ String folderName = BASE64MailboxDecoder.decode(tokens.nextToken());
+ if (baseMailboxPath != null && !folderName.startsWith("/")) {
+ folderName = baseMailboxPath + folderName;
+ }
+ try {
+ currentFolder = session.getFolder(folderName);
+ currentFolder.loadMessages();
+ sendClient("* " + currentFolder.count() + " EXISTS");
+ sendClient("* " + currentFolder.recent + " RECENT");
+ sendClient("* OK [UIDVALIDITY 1]");
+ if (currentFolder.count() == 0) {
+ sendClient("* OK [UIDNEXT 1]");
+ } else {
+ sendClient("* OK [UIDNEXT " + currentFolder.getUidNext() + ']');
+ }
+ sendClient("* FLAGS (\\Answered \\Deleted \\Draft \\Flagged \\Seen $Forwarded Junk)");
+ sendClient("* OK [PERMANENTFLAGS (\\Answered \\Deleted \\Draft \\Flagged \\Seen $Forwarded Junk)]");
+ if ("select".equalsIgnoreCase(command)) {
+ sendClient(commandId + " OK [READ-WRITE] " + command + " completed");
+ } else {
+ sendClient(commandId + " OK [READ-ONLY] " + command + " completed");
+ }
+ } catch (HttpNotFoundException e) {
+ sendClient(commandId + " NO Not found");
+ } catch (HttpForbiddenException e) {
+ sendClient(commandId + " NO Forbidden");
+ }
+ } else {
+ sendClient(commandId + " BAD command unrecognized");
+ }
+ } else if ("expunge".equalsIgnoreCase(command)) {
+ if (expunge(false)) {
+ // need to refresh folder to avoid 404 errors
+ session.refreshFolder(currentFolder);
+ }
+ sendClient(commandId + " OK " + command + " completed");
+ } else if ("close".equalsIgnoreCase(command)) {
+ expunge(true);
+ // deselect folder
+ currentFolder = null;
+ sendClient(commandId + " OK " + command + " completed");
+ } else if ("create".equalsIgnoreCase(command)) {
+ if (tokens.hasMoreTokens()) {
+ String folderName = BASE64MailboxDecoder.decode(tokens.nextToken());
+ session.createMessageFolder(folderName);
+ sendClient(commandId + " OK folder created");
+ } else {
+ sendClient(commandId + " BAD missing create argument");
+ }
+ } else if ("rename".equalsIgnoreCase(command)) {
+ String folderName = BASE64MailboxDecoder.decode(tokens.nextToken());
+ String targetName = BASE64MailboxDecoder.decode(tokens.nextToken());
+ try {
+ session.moveFolder(folderName, targetName);
+ sendClient(commandId + " OK rename completed");
+ } catch (HttpException e) {
+ sendClient(commandId + " NO " + e.getMessage());
+ }
+ } else if ("delete".equalsIgnoreCase(command)) {
+ String folderName = BASE64MailboxDecoder.decode(tokens.nextToken());
+ try {
+ session.deleteFolder(folderName);
+ sendClient(commandId + " OK folder deleted");
+ } catch (HttpException e) {
+ sendClient(commandId + " NO " + e.getMessage());
+ }
+ } else if ("uid".equalsIgnoreCase(command)) {
+ if (tokens.hasMoreTokens()) {
+ String subcommand = tokens.nextToken();
+ if ("fetch".equalsIgnoreCase(subcommand)) {
+ if (currentFolder == null) {
+ sendClient(commandId + " NO no folder selected");
+ } else {
+ String ranges = tokens.nextToken();
+ if (ranges == null) {
+ sendClient(commandId + " BAD missing range parameter");
+ } else {
+ String parameters = null;
+ if (tokens.hasMoreTokens()) {
+ parameters = tokens.nextToken();
+ }
+ UIDRangeIterator uidRangeIterator = new UIDRangeIterator(currentFolder.messages, ranges);
+ while (uidRangeIterator.hasNext()) {
+ DavGatewayTray.switchIcon();
+ ExchangeSession.Message message = uidRangeIterator.next();
+ try {
+ handleFetch(message, uidRangeIterator.currentIndex, parameters);
+ } catch (SocketException e) {
+ // client closed connection
+ throw e;
+ } catch (IOException e) {
+ DavGatewayTray.log(e);
+ sendClient(commandId + " NO Unable to retrieve message: " + e.getMessage());
+ }
+ }
+ sendClient(commandId + " OK UID FETCH completed");
+ }
+ }
+
+ } else if ("search".equalsIgnoreCase(subcommand)) {
+ List<Long> uidList = handleSearch(tokens);
+ StringBuilder buffer = new StringBuilder("* SEARCH");
+ for (long uid : uidList) {
+ buffer.append(' ');
+ buffer.append(uid);
+ }
+ sendClient(buffer.toString());
+ sendClient(commandId + " OK SEARCH completed");
+
+ } else if ("store".equalsIgnoreCase(subcommand)) {
+ UIDRangeIterator uidRangeIterator = new UIDRangeIterator(currentFolder.messages, tokens.nextToken());
+ String action = tokens.nextToken();
+ String flags = tokens.nextToken();
+ handleStore(commandId, uidRangeIterator, action, flags);
+ } else if ("copy".equalsIgnoreCase(subcommand) || "move".equalsIgnoreCase(subcommand)) {
+ try {
+ UIDRangeIterator uidRangeIterator = new UIDRangeIterator(currentFolder.messages, tokens.nextToken());
+ String targetName = BASE64MailboxDecoder.decode(tokens.nextToken());
+ if (!uidRangeIterator.hasNext()) {
+ sendClient(commandId + " NO " + "No message found");
+ } else {
+ while (uidRangeIterator.hasNext()) {
+ DavGatewayTray.switchIcon();
+ ExchangeSession.Message message = uidRangeIterator.next();
+ if ("copy".equalsIgnoreCase(subcommand)) {
+ session.copyMessage(message, targetName);
+ } else {
+ session.moveMessage(message, targetName);
+ }
+ }
+ sendClient(commandId + " OK " + subcommand + " completed");
+ }
+ } catch (HttpException e) {
+ sendClient(commandId + " NO " + e.getMessage());
+ }
+ }
+ } else {
+ sendClient(commandId + " BAD command unrecognized");
+ }
+ } else if ("search".equalsIgnoreCase(command)) {
+ if (currentFolder == null) {
+ sendClient(commandId + " NO no folder selected");
+ } else {
+ List<Long> uidList = handleSearch(tokens);
+ if (uidList.isEmpty()) {
+ sendClient("* SEARCH");
+ } else {
+ int currentIndex = 0;
+ for (ExchangeSession.Message message : currentFolder.messages) {
+ currentIndex++;
+ if (uidList.contains(message.getImapUid())) {
+ sendClient("* SEARCH " + currentIndex);
+ }
+ }
+ }
+ sendClient(commandId + " OK SEARCH completed");
+ }
+ } else if ("fetch".equalsIgnoreCase(command)) {
+ if (currentFolder == null) {
+ sendClient(commandId + " NO no folder selected");
+ } else {
+ RangeIterator rangeIterator = new RangeIterator(currentFolder.messages, tokens.nextToken());
+ String parameters = null;
+ if (tokens.hasMoreTokens()) {
+ parameters = tokens.nextToken();
+ }
+ while (rangeIterator.hasNext()) {
+ DavGatewayTray.switchIcon();
+ ExchangeSession.Message message = rangeIterator.next();
+ try {
+ handleFetch(message, rangeIterator.currentIndex, parameters);
+ } catch (SocketException e) {
+ // client closed connection, rethrow exception
+ throw e;
+ } catch (IOException e) {
+ DavGatewayTray.log(e);
+ sendClient(commandId + " NO Unable to retrieve message: " + e.getMessage());
+ }
+
+ }
+ sendClient(commandId + " OK FETCH completed");
+ }
+
+ } else if ("store".equalsIgnoreCase(command)) {
+ RangeIterator rangeIterator = new RangeIterator(currentFolder.messages, tokens.nextToken());
+ String action = tokens.nextToken();
+ String flags = tokens.nextToken();
+ handleStore(commandId, rangeIterator, action, flags);
+
+ } else if ("copy".equalsIgnoreCase(command) || "move".equalsIgnoreCase(command)) {
+ try {
+ RangeIterator rangeIterator = new RangeIterator(currentFolder.messages, tokens.nextToken());
+ String targetName = BASE64MailboxDecoder.decode(tokens.nextToken());
+ if (!rangeIterator.hasNext()) {
+ sendClient(commandId + " NO " + "No message found");
+ } else {
+ while (rangeIterator.hasNext()) {
+ DavGatewayTray.switchIcon();
+ ExchangeSession.Message message = rangeIterator.next();
+ if ("copy".equalsIgnoreCase(command)) {
+ session.copyMessage(message, targetName);
+ } else {
+ session.moveMessage(message, targetName);
+ }
+ }
+ sendClient(commandId + " OK " + command + " completed");
+ }
+ } catch (HttpException e) {
+ sendClient(commandId + " NO " + e.getMessage());
+ }
+ } else if ("append".equalsIgnoreCase(command)) {
+ String folderName = BASE64MailboxDecoder.decode(tokens.nextToken());
+ HashMap<String, String> properties = new HashMap<String, String>();
+ String flags = null;
+ String date = null;
+ // handle optional flags
+ String nextToken = tokens.nextQuotedToken();
+ if (nextToken.startsWith("(")) {
+ flags = removeQuotes(nextToken);
+ if (tokens.hasMoreTokens()) {
+ nextToken = tokens.nextToken();
+ if (tokens.hasMoreTokens()) {
+ date = nextToken;
+ nextToken = tokens.nextToken();
+ }
+ }
+ } else if (tokens.hasMoreTokens()) {
+ date = removeQuotes(nextToken);
+ nextToken = tokens.nextToken();
+ }
+
+ if (flags != null) {
+ // parse flags, on create read and draft flags are on the
+ // same messageFlags property, 8 means draft and 1 means read
+ StringTokenizer flagtokenizer = new StringTokenizer(flags);
+ while (flagtokenizer.hasMoreTokens()) {
+ String flag = flagtokenizer.nextToken();
+ if ("\\Seen".equals(flag)) {
+ if (properties.containsKey("draft")) {
+ // draft message, add read flag
+ properties.put("draft", "9");
+ } else {
+ // not (yet) draft, set read flag
+ properties.put("draft", "1");
+ }
+ } else if ("\\Flagged".equals(flag)) {
+ properties.put("flagged", "2");
+ } else if ("\\Answered".equals(flag)) {
+ properties.put("answered", "102");
+ } else if ("$Forwarded".equals(flag)) {
+ properties.put("forwarded", "104");
+ } else if ("\\Draft".equals(flag)) {
+ if (properties.containsKey("draft")) {
+ // read message, add draft flag
+ properties.put("draft", "9");
+ } else {
+ // not (yet) read, set draft flag
+ properties.put("draft", "8");
+ }
+ } else if ("Junk".equals(flag)) {
+ properties.put("junk", "1");
+ }
+ }
+ } else {
+ // no flags, force not draft and unread
+ properties.put("draft", "0");
+ }
+ // handle optional date
+ if (date != null) {
+ SimpleDateFormat dateParser = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss Z", Locale.ENGLISH);
+ Date dateReceived = dateParser.parse(date);
+ SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
+ dateFormatter.setTimeZone(ExchangeSession.GMT_TIMEZONE);
+
+ properties.put("datereceived", dateFormatter.format(dateReceived));
+ }
+ int size = Integer.parseInt(removeQuotes(nextToken));
+ sendClient("+ send literal data");
+ byte[] buffer = in.readContent(size);
+ // empty line
+ readClient();
+ MimeMessage mimeMessage = new MimeMessage(null, new SharedByteArrayInputStream(buffer));
+
+ String messageName = UUID.randomUUID().toString() + ".EML";
+ try {
+ session.createMessage(folderName, messageName, properties, mimeMessage);
+ sendClient(commandId + " OK APPEND completed");
+ } catch (InsufficientStorageException e) {
+ sendClient(commandId + " NO " + e.getMessage());
+ }
+ } else if ("idle".equalsIgnoreCase(command) && imapIdleDelay > 0) {
+ if (currentFolder != null) {
+ sendClient("+ idling ");
+ // clear cache before going to idle mode
+ currentFolder.clearCache();
+ DavGatewayTray.resetIcon();
+ try {
+ int count = 0;
+ while (in.available() == 0) {
+ if (++count >= imapIdleDelay) {
+ count = 0;
+ List<Long> previousImapUidList = currentFolder.getImapUidList();
+ if (session.refreshFolder(currentFolder)) {
+ handleRefresh(previousImapUidList, currentFolder.getImapUidList());
+ }
+ }
+ // sleep 1 second
+ Thread.sleep(1000);
+ }
+ // read DONE line
+ line = readClient();
+ if ("DONE".equals(line)) {
+ sendClient(commandId + " OK " + command + " terminated");
+ } else {
+ sendClient(commandId + " BAD command unrecognized");
+ }
+ } catch (IOException e) {
+ // client connection closed
+ throw new SocketException(e.getMessage());
+ }
+ } else {
+ sendClient(commandId + " NO no folder selected");
+ }
+ } else if ("noop".equalsIgnoreCase(command) || "check".equalsIgnoreCase(command)) {
+ if (currentFolder != null) {
+ DavGatewayTray.debug(new BundleMessage("LOG_IMAP_COMMAND", command, currentFolder.folderPath));
+ List<Long> previousImapUidList = currentFolder.getImapUidList();
+ if (session.refreshFolder(currentFolder)) {
+ handleRefresh(previousImapUidList, currentFolder.getImapUidList());
+ }
+ }
+ sendClient(commandId + " OK " + command + " completed");
+ } else if ("subscribe".equalsIgnoreCase(command) || "unsubscribe".equalsIgnoreCase(command)) {
+ sendClient(commandId + " OK " + command + " completed");
+ } else if ("status".equalsIgnoreCase(command)) {
+ try {
+ String encodedFolderName = tokens.nextToken();
+ String folderName = BASE64MailboxDecoder.decode(encodedFolderName);
+ ExchangeSession.Folder folder = session.getFolder(folderName);
+ // must retrieve messages
+ folder.loadMessages();
+ String parameters = tokens.nextToken();
+ StringBuilder answer = new StringBuilder();
+ StringTokenizer parametersTokens = new StringTokenizer(parameters);
+ while (parametersTokens.hasMoreTokens()) {
+ String token = parametersTokens.nextToken();
+ if ("MESSAGES".equalsIgnoreCase(token)) {
+ answer.append("MESSAGES ").append(folder.count()).append(' ');
+ }
+ if ("RECENT".equalsIgnoreCase(token)) {
+ answer.append("RECENT ").append(folder.recent).append(' ');
+ }
+ if ("UIDNEXT".equalsIgnoreCase(token)) {
+ if (folder.count() == 0) {
+ answer.append("UIDNEXT 1 ");
+ } else {
+ if (folder.count() == 0) {
+ answer.append("UIDNEXT 1 ");
+ } else {
+ answer.append("UIDNEXT ").append(folder.getUidNext()).append(' ');
+ }
+ }
+
+ }
+ if ("UIDVALIDITY".equalsIgnoreCase(token)) {
+ answer.append("UIDVALIDITY 1 ");
+ }
+ if ("UNSEEN".equalsIgnoreCase(token)) {
+ answer.append("UNSEEN ").append(folder.unreadCount).append(' ');
+ }
+ }
+ sendClient("* STATUS \"" + encodedFolderName + "\" (" + answer.toString().trim() + ')');
+ sendClient(commandId + " OK " + command + " completed");
+ } catch (HttpException e) {
+ sendClient(commandId + " NO folder not found");
+ }
+ } else {
+ sendClient(commandId + " BAD command unrecognized");
+ }
+ }
+ }
+
+ } else {
+ sendClient(commandId + " BAD missing command");
+ }
+ } else {
+ sendClient("BAD Null command");
+ }
+ DavGatewayTray.resetIcon();
+ }
+
+ os.flush();
+ } catch (SocketTimeoutException e) {
+ DavGatewayTray.debug(new BundleMessage("LOG_CLOSE_CONNECTION_ON_TIMEOUT"));
+ try {
+ sendClient("* BYE Closing connection");
+ } catch (IOException e1) {
+ DavGatewayTray.debug(new BundleMessage("LOG_EXCEPTION_CLOSING_CONNECTION_ON_TIMEOUT"));
+ }
+ } catch (SocketException e) {
+ LOGGER.warn(BundleMessage.formatLog("LOG_CLIENT_CLOSED_CONNECTION"));
+ } catch (Exception e) {
+ DavGatewayTray.log(e);
+ try {
+ String message = ((e.getMessage() == null) ? e.toString() : e.getMessage()).replaceAll("\\n", " ");
+ if (commandId != null) {
+ sendClient(commandId + " BAD unable to handle request: " + message);
+ } else {
+ sendClient("* BAD unable to handle request: " + message);
+ }
+ } catch (IOException e2) {
+ DavGatewayTray.warn(new BundleMessage("LOG_EXCEPTION_SENDING_ERROR_TO_CLIENT"), e2);
+ }
+ } finally {
+ close();
+ }
+ DavGatewayTray.resetIcon();
+ }
+
+ protected String lastCommand;
+ protected int lastCommandCount;
+
+ /**
+ * Detect infinite loop on the client side.
+ *
+ * @param line IMAP command line
+ * @throws IOException on infinite loop
+ */
+ protected void checkInfiniteLoop(String line) throws IOException {
+ int spaceIndex = line.indexOf(' ');
+ if (spaceIndex < 0) {
+ // invalid command line, reset
+ lastCommand = null;
+ lastCommandCount = 0;
+ } else {
+ String command = line.substring(spaceIndex + 1);
+ if (command.equals(lastCommand)) {
+ lastCommandCount++;
+ if (lastCommandCount > 100 && !"NOOP".equalsIgnoreCase(lastCommand) && !"IDLE".equalsIgnoreCase(lastCommand)) {
+ // more than a hundred times the same command => this is a client infinite loop, close connection
+ throw new IOException("Infinite loop on command " + command + " detected");
+ }
+ } else {
+ // new command, reset
+ lastCommand = command;
+ lastCommandCount = 0;
+ }
+ }
+ }
+
+ /**
+ * Detect shared mailbox access.
+ * see http://msexchangeteam.com/archive/2004/03/31/105275.aspx
+ */
+ protected void splitUserName() {
+ String[] tokens = null;
+ if (userName.indexOf('/') >= 0) {
+ tokens = userName.split("/");
+ } else if (userName.indexOf('\\') >= 0) {
+ tokens = userName.split("\\\\");
+ }
+
+ if (tokens != null && tokens.length == 3) {
+ userName = tokens[0] + '\\' + tokens[1];
+ baseMailboxPath = "/users/" + tokens[2] + '/';
+ }
+ }
+
+ /**
+ * Send expunge untagged response for removed IMAP message uids.
+ *
+ * @param previousImapUidList uid list before refresh
+ * @param imapUidList uid list after refresh
+ * @throws IOException on error
+ */
+ private void handleRefresh(List<Long> previousImapUidList, List<Long> imapUidList) throws IOException {
+ //
+ int index = 1;
+ for (long previousImapUid : previousImapUidList) {
+ if (!imapUidList.contains(previousImapUid)) {
+ sendClient("* " + index + " EXPUNGE");
+ } else {
+ index++;
+ }
+ }
+ sendClient("* " + currentFolder.count() + " EXISTS");
+ sendClient("* " + currentFolder.recent + " RECENT");
+ }
+
+ private void handleFetch(ExchangeSession.Message message, int currentIndex, String parameters) throws IOException, MessagingException {
+ StringBuilder buffer = new StringBuilder();
+ buffer.append("* ").append(currentIndex).append(" FETCH (UID ").append(message.getImapUid());
+ if (parameters != null) {
+ StringTokenizer paramTokens = new StringTokenizer(parameters);
+ while (paramTokens.hasMoreTokens()) {
+ @SuppressWarnings({"NonConstantStringShouldBeStringBuffer"})
+ String param = paramTokens.nextToken().toUpperCase();
+ if ("FLAGS".equals(param)) {
+ buffer.append(" FLAGS (").append(message.getImapFlags()).append(')');
+ } else if ("RFC822.SIZE".equals(param)) {
+ int size;
+ if (parameters.indexOf("BODY.PEEK[HEADER.FIELDS (") >= 0) {
+ // Header request, send approximate size
+ size = message.size;
+ } else {
+ size = message.getMimeMessageSize();
+ }
+ buffer.append(" RFC822.SIZE ").append(size);
+ } else if ("ENVELOPE".equals(param)) {
+ appendEnvelope(buffer, message);
+ } else if ("BODYSTRUCTURE".equals(param)) {
+ appendBodyStructure(buffer, message);
+ } else if ("INTERNALDATE".equals(param) && message.date != null && message.date.length() > 0) {
+ try {
+ SimpleDateFormat dateParser = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
+ dateParser.setTimeZone(ExchangeSession.GMT_TIMEZONE);
+ Date date = ExchangeSession.getZuluDateFormat().parse(message.date);
+ SimpleDateFormat dateFormatter = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss Z", Locale.ENGLISH);
+ buffer.append(" INTERNALDATE \"").append(dateFormatter.format(date)).append('\"');
+ } catch (ParseException e) {
+ throw new DavMailException("EXCEPTION_INVALID_DATE", message.date);
+ }
+ } else if (param.equals("RFC822") || param.startsWith("BODY[") || param.startsWith("BODY.PEEK[") || "RFC822.HEADER".equals(param)) {
+ // get full param
+ if (param.indexOf('[') >= 0) {
+ StringBuilder paramBuffer = new StringBuilder(param);
+ while (paramTokens.hasMoreTokens() && paramBuffer.indexOf("]") < 0) {
+ paramBuffer.append(' ').append(paramTokens.nextToken());
+ }
+ param = paramBuffer.toString();
+ }
+ // parse buffer size
+ int startIndex = 0;
+ int maxSize = Integer.MAX_VALUE;
+ int ltIndex = param.indexOf('<');
+ if (ltIndex >= 0) {
+ int dotIndex = param.indexOf('.', ltIndex);
+ if (dotIndex >= 0) {
+ startIndex = Integer.parseInt(param.substring(ltIndex + 1, dotIndex));
+ maxSize = Integer.parseInt(param.substring(dotIndex + 1, param.indexOf('>')));
+ }
+ }
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ InputStream partInputStream = null;
+ OutputStream partOutputStream = null;
+
+ // try to parse message part index
+ String partIndexString = StringUtil.getToken(param, "[", "]");
+ if ("".equals(partIndexString) || partIndexString == null) {
+ // write message with headers
+ partOutputStream = new PartialOutputStream(baos, startIndex, maxSize);
+ partInputStream = message.getRawInputStream();
+ } else if ("TEXT".equals(partIndexString)) {
+ // write message without headers
+ partOutputStream = new PartialOutputStream(baos, startIndex, maxSize);
+ partInputStream = message.getMimeMessage().getRawInputStream();
+ } else if ("RFC822.HEADER".equals(param) || partIndexString.startsWith("HEADER")) {
+ // Header requested fetch headers
+ String[] requestedHeaders = getRequestedHeaders(partIndexString);
+ if (requestedHeaders != null) {
+ // OSX Lion special flags request
+ if (requestedHeaders.length == 1 && "content-class".equals(requestedHeaders[0]) && message.contentClass != null) {
+ baos.write("Content-class: ".getBytes("UTF-8"));
+ baos.write(message.contentClass.getBytes("UTF-8"));
+ baos.write(13);
+ baos.write(10);
+ } else {
+ Enumeration headerEnumeration = message.getMatchingHeaderLines(requestedHeaders);
+ while (headerEnumeration.hasMoreElements()) {
+ baos.write(((String) headerEnumeration.nextElement()).getBytes("UTF-8"));
+ baos.write(13);
+ baos.write(10);
+ }
+ }
+ } else {
+ // write headers only
+ partOutputStream = new PartOutputStream(baos, true, false, startIndex, maxSize);
+ partInputStream = message.getRawInputStream();
+ }
+ } else {
+ MimePart bodyPart = message.getMimeMessage();
+ String[] partIndexStrings = partIndexString.split("\\.");
+ for (String subPartIndexString : partIndexStrings) {
+ // ignore MIME subpart index, will return full part
+ if ("MIME".equals(subPartIndexString)) {
+ break;
+ }
+ int subPartIndex;
+ // try to parse part index
+ try {
+ subPartIndex = Integer.parseInt(subPartIndexString);
+ } catch (NumberFormatException e) {
+ throw new DavMailException("EXCEPTION_INVALID_PARAMETER", param);
+ }
+
+ Object mimeBody = bodyPart.getContent();
+ if (mimeBody instanceof MimeMultipart) {
+ MimeMultipart multiPart = (MimeMultipart) mimeBody;
+ if (subPartIndex - 1 < multiPart.getCount()) {
+ bodyPart = (MimePart) multiPart.getBodyPart(subPartIndex - 1);
+ } else {
+ throw new DavMailException("EXCEPTION_INVALID_PARAMETER", param);
+ }
+ } else if (subPartIndex != 1) {
+ throw new DavMailException("EXCEPTION_INVALID_PARAMETER", param);
+ }
+ }
+
+ // write selected part, without headers
+ partOutputStream = new PartialOutputStream(baos, startIndex, maxSize);
+ if (bodyPart instanceof MimeMessage) {
+ partInputStream = ((MimeMessage) bodyPart).getRawInputStream();
+ } else {
+ partInputStream = ((MimeBodyPart) bodyPart).getRawInputStream();
+ }
+ }
+
+ // copy selected content to baos
+ if (partInputStream != null && partOutputStream != null) {
+ IOUtil.write(partInputStream, partOutputStream);
+ partInputStream.close();
+ partOutputStream.close();
+ }
+ baos.close();
+
+ if ("RFC822.HEADER".equals(param)) {
+ buffer.append(" RFC822.HEADER ");
+ } else {
+ buffer.append(" BODY[").append(partIndexString).append(']');
+ }
+ // partial
+ if (startIndex > 0 || maxSize != Integer.MAX_VALUE) {
+ buffer.append('<').append(startIndex).append('>');
+ }
+ buffer.append(" {").append(baos.size()).append('}');
+ sendClient(buffer.toString());
+ os.write(baos.toByteArray());
+ os.flush();
+ buffer.setLength(0);
+ }
+ }
+ }
+ buffer.append(')');
+ sendClient(buffer.toString());
+ // do not keep message content in memory
+ message.dropMimeMessage();
+ }
+
+ protected String[] getRequestedHeaders(String partIndexString) {
+ if (partIndexString == null) {
+ return null;
+ } else {
+ int startIndex = partIndexString.indexOf('(');
+ int endIndex = partIndexString.indexOf(')');
+ if (startIndex >= 0 && endIndex >= 0) {
+ return partIndexString.substring(startIndex + 1, endIndex).split(" ");
+ } else {
+ return null;
+ }
+ }
+ }
+
+ protected void handleStore(String commandId, AbstractRangeIterator rangeIterator, String action, String flags) throws IOException {
+ while (rangeIterator.hasNext()) {
+ DavGatewayTray.switchIcon();
+ ExchangeSession.Message message = rangeIterator.next();
+ updateFlags(message, action, flags);
+ sendClient("* " + (rangeIterator.getCurrentIndex()) + " FETCH (UID " + message.getImapUid() + " FLAGS (" + (message.getImapFlags()) + "))");
+ }
+ // auto expunge
+ if (Settings.getBooleanProperty("davmail.imapAutoExpunge")) {
+ if (expunge(false)) {
+ session.refreshFolder(currentFolder);
+ }
+ }
+ sendClient(commandId + " OK STORE completed");
+ }
+
+ protected ExchangeSession.Condition buildConditions(SearchConditions conditions, IMAPTokenizer tokens) throws IOException {
+ ExchangeSession.MultiCondition condition = null;
+ while (tokens.hasMoreTokens()) {
+ String token = tokens.nextQuotedToken().toUpperCase();
+ if (token.startsWith("(") && token.endsWith(")")) {
+ // quoted search param
+ if (condition == null) {
+ condition = session.and();
+ }
+ condition.add(buildConditions(conditions, new IMAPTokenizer(token.substring(1, token.length() - 1))));
+ } else if ("OR".equals(token)) {
+ condition = session.or();
+ } else if (token.startsWith("OR ")) {
+ condition = appendOrSearchParams(token, conditions);
+ } else if ("CHARSET".equals(token)) {
+ String charset = tokens.nextQuotedToken().toUpperCase();
+ if (!("ASCII".equals(charset) || "UTF-8".equals(charset))) {
+ throw new IOException("Unsupported charset " + charset);
+ }
+ } else {
+ if (condition == null) {
+ condition = session.and();
+ }
+ condition.add(appendSearchParam(tokens, token, conditions));
+ }
+ }
+ return condition;
+ }
+
+
+ protected List<Long> handleSearch(IMAPTokenizer tokens) throws IOException {
+ List<Long> uidList = new ArrayList<Long>();
+ SearchConditions conditions = new SearchConditions();
+ ExchangeSession.Condition condition = buildConditions(conditions, tokens);
+ ExchangeSession.MessageList localMessages = currentFolder.searchMessages(condition);
+ Iterator<ExchangeSession.Message> iterator;
+ if (conditions.uidRange != null) {
+ iterator = new UIDRangeIterator(localMessages, conditions.uidRange);
+ } else if (conditions.indexRange != null) {
+ iterator = new RangeIterator(localMessages, conditions.indexRange);
+ } else {
+ iterator = localMessages.iterator();
+ }
+ while (iterator.hasNext()) {
+ ExchangeSession.Message message = iterator.next();
+ if ((conditions.flagged == null || message.flagged == conditions.flagged)
+ && (conditions.answered == null || message.answered == conditions.answered)
+ && (conditions.draft == null || message.draft == conditions.draft)) {
+ uidList.add(message.getImapUid());
+ }
+ }
+ return uidList;
+ }
+
+ protected void appendEnvelope(StringBuilder buffer, ExchangeSession.Message message) throws IOException {
+ buffer.append(" ENVELOPE (");
+
+ try {
+ MimeMessage mimeMessage = message.getMimeMessage();
+ // Envelope for date, subject, from, sender, reply-to, to, cc, bcc,in-reply-to, message-id
+ appendEnvelopeHeader(buffer, mimeMessage.getHeader("Date"));
+ appendEnvelopeHeader(buffer, mimeMessage.getHeader("Subject"));
+ appendMailEnvelopeHeader(buffer, mimeMessage.getHeader("From"));
+ appendMailEnvelopeHeader(buffer, mimeMessage.getHeader("Sender"));
+ appendMailEnvelopeHeader(buffer, mimeMessage.getHeader("Reply-To"));
+ appendMailEnvelopeHeader(buffer, mimeMessage.getHeader("To"));
+ appendMailEnvelopeHeader(buffer, mimeMessage.getHeader("CC"));
+ appendMailEnvelopeHeader(buffer, mimeMessage.getHeader("BCC"));
+ appendEnvelopeHeader(buffer, mimeMessage.getHeader("In-Reply-To"));
+ appendEnvelopeHeader(buffer, mimeMessage.getHeader("Message-Id"));
+
+ } catch (MessagingException me) {
+ DavGatewayTray.warn(me);
+ // send fake envelope
+ buffer.append(" NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL");
+ }
+ buffer.append(')');
+ }
+
+ protected void appendEnvelopeHeader(StringBuilder buffer, String[] value) throws UnsupportedEncodingException {
+ if (buffer.charAt(buffer.length() - 1) != '(') {
+ buffer.append(' ');
+ }
+ if (value != null && value.length > 0) {
+ appendEnvelopeHeaderValue(buffer, MimeUtility.unfold(value[0]));
+ } else {
+ buffer.append("NIL");
+ }
+ }
+
+ protected void appendMailEnvelopeHeader(StringBuilder buffer, String[] value) {
+ buffer.append(' ');
+ if (value != null && value.length > 0) {
+ try {
+ String unfoldedValue = MimeUtility.unfold(value[0]);
+ InternetAddress[] addresses = InternetAddress.parseHeader(unfoldedValue, false);
+ if (addresses != null && addresses.length > 1) {
+ buffer.append('(');
+ for (InternetAddress address : addresses) {
+ buffer.append('(');
+ String personal = address.getPersonal();
+ if (personal != null) {
+ appendEnvelopeHeaderValue(buffer, personal);
+ } else {
+ buffer.append("NIL");
+ }
+ buffer.append(" NIL ");
+ String mail = address.getAddress();
+ int atIndex = mail.indexOf('@');
+ if (atIndex >= 0) {
+ buffer.append('"').append(mail.substring(0, atIndex)).append('"');
+ buffer.append(' ');
+ buffer.append('"').append(mail.substring(atIndex + 1)).append('"');
+ } else {
+ buffer.append("NIL NIL");
+ }
+ buffer.append(')');
+ }
+ buffer.append(')');
+ } else {
+ buffer.append("NIL");
+ }
+ } catch (AddressException e) {
+ DavGatewayTray.warn(e);
+ buffer.append("NIL");
+ } catch (UnsupportedEncodingException e) {
+ DavGatewayTray.warn(e);
+ buffer.append("NIL");
+ }
+ } else {
+ buffer.append("NIL");
+ }
+ }
+
+ protected void appendEnvelopeHeaderValue(StringBuilder buffer, String value) throws UnsupportedEncodingException {
+ if (value.indexOf('"') >= 0 || value.indexOf('\\') >= 0) {
+ buffer.append('{');
+ buffer.append(value.length());
+ buffer.append("}\r\n");
+ buffer.append(value);
+ } else {
+ buffer.append('"');
+ buffer.append(MimeUtility.encodeText(value, "UTF-8", null));
+ buffer.append('"');
+ }
+
+ }
+
+ protected void appendBodyStructure(StringBuilder buffer, ExchangeSession.Message message) throws IOException {
+
+ buffer.append(" BODYSTRUCTURE ");
+ try {
+ MimeMessage mimeMessage = message.getMimeMessage();
+ Object mimeBody = mimeMessage.getContent();
+ if (mimeBody instanceof MimeMultipart) {
+ appendBodyStructure(buffer, (MimeMultipart) mimeBody);
+ } else {
+ // no multipart, single body
+ appendBodyStructure(buffer, mimeMessage);
+ }
+ } catch (UnsupportedEncodingException e) {
+ DavGatewayTray.warn(e);
+ // failover: send default bodystructure
+ buffer.append("(\"TEXT\" \"PLAIN\" (\"CHARSET\" \"US-ASCII\") NIL NIL NIL NIL NIL)");
+ } catch (MessagingException me) {
+ DavGatewayTray.warn(me);
+ // failover: send default bodystructure
+ buffer.append("(\"TEXT\" \"PLAIN\" (\"CHARSET\" \"US-ASCII\") NIL NIL NIL NIL NIL)");
+ }
+ }
+
+ protected void appendBodyStructure(StringBuilder buffer, MimeMultipart multiPart) throws IOException, MessagingException {
+ buffer.append('(');
+
+ for (int i = 0; i < multiPart.getCount(); i++) {
+ MimeBodyPart bodyPart = (MimeBodyPart) multiPart.getBodyPart(i);
+ Object mimeBody = bodyPart.getContent();
+ if (mimeBody instanceof MimeMultipart) {
+ appendBodyStructure(buffer, (MimeMultipart) mimeBody);
+ } else {
+ // no multipart, single body
+ appendBodyStructure(buffer, bodyPart);
+ }
+ }
+ int slashIndex = multiPart.getContentType().indexOf('/');
+ if (slashIndex < 0) {
+ throw new DavMailException("EXCEPTION_INVALID_CONTENT_TYPE", multiPart.getContentType());
+ }
+ int semiColonIndex = multiPart.getContentType().indexOf(';');
+ if (semiColonIndex < 0) {
+ buffer.append(" \"").append(multiPart.getContentType().substring(slashIndex + 1).toUpperCase()).append("\")");
+ } else {
+ buffer.append(" \"").append(multiPart.getContentType().substring(slashIndex + 1, semiColonIndex).trim().toUpperCase()).append("\")");
+ }
+ }
+
+ protected void appendBodyStructure(StringBuilder buffer, MimePart bodyPart) throws IOException, MessagingException {
+ String contentType = MimeUtility.unfold(bodyPart.getContentType());
+ int slashIndex = contentType.indexOf('/');
+ if (slashIndex < 0) {
+ throw new DavMailException("EXCEPTION_INVALID_CONTENT_TYPE", contentType);
+ }
+ String type = contentType.substring(0, slashIndex).toUpperCase();
+ buffer.append("(\"").append(type).append("\" \"");
+ int semiColonIndex = contentType.indexOf(';');
+ if (semiColonIndex < 0) {
+ buffer.append(contentType.substring(slashIndex + 1).toUpperCase()).append("\" NIL");
+ } else {
+ // extended content type
+ buffer.append(contentType.substring(slashIndex + 1, semiColonIndex).trim().toUpperCase()).append('\"');
+ int charsetindex = contentType.indexOf("charset=");
+ int nameindex = contentType.indexOf("name=");
+ if (charsetindex >= 0 || nameindex >= 0) {
+ buffer.append(" (");
+
+ if (charsetindex >= 0) {
+ buffer.append("\"CHARSET\" ");
+ int charsetSemiColonIndex = contentType.indexOf(';', charsetindex);
+ int charsetEndIndex;
+ if (charsetSemiColonIndex > 0) {
+ charsetEndIndex = charsetSemiColonIndex;
+ } else {
+ charsetEndIndex = contentType.length();
+ }
+ String charSet = contentType.substring(charsetindex + "charset=".length(), charsetEndIndex);
+ if (!charSet.startsWith("\"")) {
+ buffer.append('"');
+ }
+ buffer.append(charSet.trim().toUpperCase());
+ if (!charSet.endsWith("\"")) {
+ buffer.append('"');
+ }
+ }
+
+ if (nameindex >= 0) {
+ if (charsetindex >= 0) {
+ buffer.append(' ');
+ }
+
+ buffer.append("\"NAME\" ");
+ int nameSemiColonIndex = contentType.indexOf(';', nameindex);
+ int nameEndIndex;
+ if (nameSemiColonIndex > 0) {
+ nameEndIndex = nameSemiColonIndex;
+ } else {
+ nameEndIndex = contentType.length();
+ }
+ String name = contentType.substring(nameindex + "name=".length(), nameEndIndex).trim();
+ if (!name.startsWith("\"")) {
+ buffer.append('"');
+ }
+ buffer.append(name.trim());
+ if (!name.endsWith("\"")) {
+ buffer.append('"');
+ }
+ }
+ buffer.append(')');
+ } else {
+ buffer.append(" NIL");
+ }
+ }
+ appendBodyStructureValue(buffer, bodyPart.getContentID());
+ appendBodyStructureValue(buffer, bodyPart.getDescription());
+ appendBodyStructureValue(buffer, bodyPart.getEncoding());
+ appendBodyStructureValue(buffer, bodyPart.getSize());
+ if ("MESSAGE".equals(type) || "TEXT".equals(type)) {
+ // line count not implemented in JavaMail, return fake line count
+ appendBodyStructureValue(buffer, bodyPart.getSize() / 80);
+ } else {
+ // do not send line count for non text bodyparts
+ appendBodyStructureValue(buffer, -1);
+ }
+ buffer.append(')');
+ }
+
+ protected void appendBodyStructureValue(StringBuilder buffer, String value) {
+ if (value == null) {
+ buffer.append(" NIL");
+ } else {
+ buffer.append(" \"").append(value.toUpperCase()).append('\"');
+ }
+ }
+
+ protected void appendBodyStructureValue(StringBuilder buffer, int value) {
+ if (value < 0) {
+ buffer.append(" NIL");
+ } else {
+ buffer.append(' ').append(value);
+ }
+ }
+
+ protected void sendSubFolders(String command, String folderPath, boolean recursive) throws IOException {
+ try {
+ List<ExchangeSession.Folder> folders = session.getSubFolders(folderPath, recursive);
+ for (ExchangeSession.Folder folder : folders) {
+ sendClient("* " + command + " (" + folder.getFlags() + ") \"/\" \"" + BASE64MailboxEncoder.encode(folder.folderPath) + '\"');
+ }
+ } catch (HttpForbiddenException e) {
+ // access forbidden, ignore
+ DavGatewayTray.debug(new BundleMessage("LOG_SUBFOLDER_ACCESS_FORBIDDEN", folderPath));
+ } catch (HttpNotFoundException e) {
+ // not found, ignore
+ DavGatewayTray.debug(new BundleMessage("LOG_FOLDER_NOT_FOUND", folderPath));
+ } catch (HttpException e) {
+ // other errors, ignore
+ DavGatewayTray.debug(new BundleMessage("LOG_FOLDER_ACCESS_ERROR", folderPath, e.getMessage()));
+ }
+ }
+
+ /**
+ * client side search conditions
+ */
+ static final class SearchConditions {
+ Boolean flagged;
+ Boolean answered;
+ Boolean draft;
+ String indexRange;
+ String uidRange;
+ }
+
+ protected ExchangeSession.MultiCondition appendOrSearchParams(String token, SearchConditions conditions) throws IOException {
+ ExchangeSession.MultiCondition orCondition = session.or();
+ IMAPTokenizer innerTokens = new IMAPTokenizer(token);
+ innerTokens.nextToken();
+ while (innerTokens.hasMoreTokens()) {
+ String innerToken = innerTokens.nextToken();
+ orCondition.add(appendSearchParam(innerTokens, innerToken, conditions));
+ }
+ return orCondition;
+ }
+
+ protected ExchangeSession.Condition appendSearchParam(StringTokenizer tokens, String token, SearchConditions conditions) throws IOException {
+ if ("NOT".equals(token)) {
+ String nextToken = tokens.nextToken();
+ if ("DELETED".equals(nextToken)) {
+ // conditions.deleted = Boolean.FALSE;
+ return session.isNull("deleted");
+ } else {
+ return session.not(appendSearchParam(tokens, nextToken, conditions));
+ }
+ } else if (token.startsWith("OR ")) {
+ return appendOrSearchParams(token, conditions);
+ } else if ("SUBJECT".equals(token)) {
+ return session.contains("subject", tokens.nextToken());
+ } else if ("BODY".equals(token)) {
+ return session.contains("body", tokens.nextToken());
+ } else if ("TEXT".equals(token)) {
+ String value = tokens.nextToken();
+ return session.or(session.contains("body", value),
+ session.contains("subject", value),
+ session.contains("from", value),
+ session.contains("to", value),
+ session.contains("cc", value));
+ } else if ("FROM".equals(token)) {
+ return session.contains("from", tokens.nextToken());
+ } else if ("TO".equals(token)) {
+ return session.contains("to", tokens.nextToken());
+ } else if ("CC".equals(token)) {
+ return session.contains("cc", tokens.nextToken());
+ } else if ("LARGER".equals(token)) {
+ return session.gte("messageSize", tokens.nextToken());
+ } else if ("SMALLER".equals(token)) {
+ return session.lt("messageSize", tokens.nextToken());
+ } else if (token.startsWith("SENT") || "SINCE".equals(token) || "BEFORE".equals(token)) {
+ return appendDateSearchParam(tokens, token);
+ } else if ("SEEN".equals(token)) {
+ return session.isTrue("read");
+ } else if ("UNSEEN".equals(token) || "NEW".equals(token)) {
+ return session.isFalse("read");
+ } else if ("DRAFT".equals(token)) {
+ conditions.draft = Boolean.TRUE;
+ } else if ("UNDRAFT".equals(token)) {
+ conditions.draft = Boolean.FALSE;
+ } else if ("DELETED".equals(token)) {
+ // conditions.deleted = Boolean.TRUE;
+ return session.isEqualTo("deleted", "1");
+ } else if ("UNDELETED".equals(token) || "NOT DELETED".equals(token)) {
+ // conditions.deleted = Boolean.FALSE;
+ return session.isNull("deleted");
+ } else if ("FLAGGED".equals(token)) {
+ conditions.flagged = Boolean.TRUE;
+ } else if ("UNFLAGGED".equals(token) || "NEW".equals(token)) {
+ conditions.flagged = Boolean.FALSE;
+ } else if ("ANSWERED".equals(token)) {
+ conditions.answered = Boolean.TRUE;
+ } else if ("UNANSWERED".equals(token)) {
+ conditions.answered = Boolean.FALSE;
+ } else if ("HEADER".equals(token)) {
+ String headerName = tokens.nextToken().toLowerCase();
+ String value = tokens.nextToken();
+ if ("message-id".equals(headerName) && !value.startsWith("<")) {
+ value = '<' + value + '>';
+ }
+ return session.headerIsEqualTo(headerName, value);
+ } else if ("UID".equals(token)) {
+ String range = tokens.nextToken();
+ if ("1:*".equals(range)) {
+ // ignore: this is a noop filter
+ } else {
+ conditions.uidRange = range;
+ }
+ } else if ("OLD".equals(token) || "RECENT".equals(token) || "ALL".equals(token)) {
+ // ignore
+ } else if (token.indexOf(':') >= 0 || token.matches("\\d+")) {
+ // range search
+ conditions.indexRange = token;
+ } else {
+ throw new DavMailException("EXCEPTION_INVALID_SEARCH_PARAMETERS", token);
+ }
+ // client side search token
+ return null;
+ }
+
+ protected ExchangeSession.Condition appendDateSearchParam(StringTokenizer tokens, String token) throws IOException {
+ Date startDate;
+ Date endDate;
+ SimpleDateFormat parser = new SimpleDateFormat("dd-MMM-yyyy", Locale.ENGLISH);
+ parser.setTimeZone(ExchangeSession.GMT_TIMEZONE);
+ String dateToken = tokens.nextToken();
+ try {
+ startDate = parser.parse(dateToken);
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTime(startDate);
+ calendar.add(Calendar.DAY_OF_MONTH, 1);
+ endDate = calendar.getTime();
+ } catch (ParseException e) {
+ throw new DavMailException("EXCEPTION_INVALID_SEARCH_PARAMETERS", dateToken);
+ }
+ String searchAttribute;
+ if (token.startsWith("SENT")) {
+ searchAttribute = "date";
+ } else {
+ searchAttribute = "lastmodified";
+ }
+
+ if (token.endsWith("ON")) {
+ return session.and(session.gt(searchAttribute, session.formatSearchDate(startDate)),
+ session.lt(searchAttribute, session.formatSearchDate(endDate)));
+ } else if (token.endsWith("BEFORE")) {
+ return session.lt(searchAttribute, session.formatSearchDate(startDate));
+ } else if (token.endsWith("SINCE")) {
+ return session.gte(searchAttribute, session.formatSearchDate(startDate));
+ } else {
+ throw new DavMailException("EXCEPTION_INVALID_SEARCH_PARAMETERS", dateToken);
+ }
+ }
+
+ protected boolean expunge(boolean silent) throws IOException {
+ boolean hasDeleted = false;
+ if (currentFolder.messages != null) {
+ int index = 1;
+ for (ExchangeSession.Message message : currentFolder.messages) {
+ if (message.deleted) {
+ message.delete();
+ hasDeleted = true;
+ if (!silent) {
+ sendClient("* " + index + " EXPUNGE");
+ }
+ } else {
+ index++;
+ }
+ }
+ }
+ return hasDeleted;
+ }
+
+ protected void updateFlags(ExchangeSession.Message message, String action, String flags) throws IOException {
+ HashMap<String, String> properties = new HashMap<String, String>();
+ if ("-Flags".equalsIgnoreCase(action) || "-FLAGS.SILENT".equalsIgnoreCase(action)) {
+ StringTokenizer flagtokenizer = new StringTokenizer(flags);
+ while (flagtokenizer.hasMoreTokens()) {
+ String flag = flagtokenizer.nextToken();
+ if ("\\Seen".equalsIgnoreCase(flag) && message.read) {
+ properties.put("read", "0");
+ message.read = false;
+ } else if ("\\Flagged".equalsIgnoreCase(flag) && message.flagged) {
+ properties.put("flagged", "0");
+ message.flagged = false;
+ } else if ("\\Deleted".equalsIgnoreCase(flag) && message.deleted) {
+ properties.put("deleted", null);
+ message.deleted = false;
+ } else if ("Junk".equalsIgnoreCase(flag) && message.junk) {
+ properties.put("junk", "0");
+ message.junk = false;
+ } else if ("$Forwarded".equalsIgnoreCase(flag) && message.forwarded) {
+ properties.put("forwarded", null);
+ message.forwarded = false;
+ } else if ("\\Answered".equalsIgnoreCase(flag) && message.answered) {
+ properties.put("answered", null);
+ message.answered = false;
+ }
+ }
+ } else if ("+Flags".equalsIgnoreCase(action) || "+FLAGS.SILENT".equalsIgnoreCase(action)) {
+ StringTokenizer flagtokenizer = new StringTokenizer(flags);
+ while (flagtokenizer.hasMoreTokens()) {
+ String flag = flagtokenizer.nextToken();
+ if ("\\Seen".equalsIgnoreCase(flag) && !message.read) {
+ properties.put("read", "1");
+ message.read = true;
+ } else if ("\\Deleted".equalsIgnoreCase(flag) && !message.deleted) {
+ message.deleted = true;
+ properties.put("deleted", "1");
+ } else if ("\\Flagged".equalsIgnoreCase(flag) && !message.flagged) {
+ properties.put("flagged", "2");
+ message.flagged = true;
+ } else if ("\\Answered".equalsIgnoreCase(flag) && !message.answered) {
+ properties.put("answered", "102");
+ message.answered = true;
+ } else if ("$Forwarded".equalsIgnoreCase(flag) && !message.forwarded) {
+ properties.put("forwarded", "104");
+ message.forwarded = true;
+ } else if ("Junk".equalsIgnoreCase(flag) && !message.junk) {
+ properties.put("junk", "1");
+ message.junk = true;
+ }
+ }
+ } else if ("FLAGS".equalsIgnoreCase(action) || "FLAGS.SILENT".equalsIgnoreCase(action)) {
+ // flag list with default values
+ boolean read = false;
+ boolean deleted = false;
+ boolean junk = false;
+ boolean flagged = false;
+ boolean answered = false;
+ boolean forwarded = false;
+ // set flags from new flag list
+ StringTokenizer flagtokenizer = new StringTokenizer(flags);
+ while (flagtokenizer.hasMoreTokens()) {
+ String flag = flagtokenizer.nextToken();
+ if ("\\Seen".equalsIgnoreCase(flag)) {
+ read = true;
+ } else if ("\\Deleted".equalsIgnoreCase(flag)) {
+ deleted = true;
+ } else if ("\\Flagged".equalsIgnoreCase(flag)) {
+ flagged = true;
+ } else if ("\\Answered".equalsIgnoreCase(flag)) {
+ answered = true;
+ } else if ("$Forwarded".equalsIgnoreCase(flag)) {
+ forwarded = true;
+ } else if ("Junk".equalsIgnoreCase(flag)) {
+ junk = true;
+ }
+ }
+ if (read != message.read) {
+ message.read = read;
+ if (message.read) {
+ properties.put("read", "1");
+ } else {
+ properties.put("read", "0");
+ }
+ }
+ if (deleted != message.deleted) {
+ message.deleted = deleted;
+ if (message.deleted) {
+ properties.put("deleted", "1");
+ } else {
+ properties.put("deleted", null);
+ }
+ }
+ if (flagged != message.flagged) {
+ message.flagged = flagged;
+ if (message.flagged) {
+ properties.put("flagged", "2");
+ } else {
+ properties.put("flagged", "0");
+ }
+ }
+ if (answered != message.answered) {
+ message.answered = answered;
+ if (message.answered) {
+ properties.put("answered", "102");
+ } else if (!forwarded) {
+ // remove property only if not forwarded
+ properties.put("answered", null);
+ }
+ }
+ if (forwarded != message.forwarded) {
+ message.forwarded = forwarded;
+ if (message.forwarded) {
+ properties.put("forwarded", "104");
+ } else if (!answered) {
+ // remove property only if not answered
+ properties.put("forwarded", null);
+ }
+ }
+ if (junk != message.junk) {
+ message.junk = junk;
+ if (message.junk) {
+ properties.put("junk", "1");
+ } else {
+ properties.put("junk", "0");
+ }
+ }
+ }
+ if (!properties.isEmpty()) {
+ session.updateMessage(message, properties);
+ // message is no longer recent
+ message.recent = false;
+ }
+ }
+
+ /**
+ * Decode IMAP credentials
+ *
+ * @param tokens tokens
+ * @throws IOException on error
+ */
+ protected void parseCredentials(StringTokenizer tokens) throws IOException {
+ if (tokens.hasMoreTokens()) {
+ userName = tokens.nextToken();
+ } else {
+ throw new DavMailException("EXCEPTION_INVALID_CREDENTIALS");
+ }
+
+ if (tokens.hasMoreTokens()) {
+ password = tokens.nextToken();
+ } else {
+ throw new DavMailException("EXCEPTION_INVALID_CREDENTIALS");
+ }
+ int backslashindex = userName.indexOf('\\');
+ if (backslashindex > 0) {
+ userName = userName.substring(0, backslashindex) + userName.substring(backslashindex + 1);
+ }
+ }
+
+ protected String removeQuotes(String value) {
+ String result = value;
+ if (result.startsWith("\"") || result.startsWith("{") || result.startsWith("(")) {
+ result = result.substring(1);
+ }
+ if (result.endsWith("\"") || result.endsWith("}") || result.endsWith(")")) {
+ result = result.substring(0, result.length() - 1);
+ }
+ return result;
+ }
+
+ /**
+ * Filter to output only headers, also count full size
+ */
+ private static final class PartOutputStream extends FilterOutputStream {
+ private static final int START = 0;
+ private static final int CR = 1;
+ private static final int CRLF = 2;
+ private static final int CRLFCR = 3;
+ private static final int BODY = 4;
+
+ private int state = START;
+ private int size;
+ private int bufferSize;
+ private final boolean writeHeaders;
+ private final boolean writeBody;
+ private final int startIndex;
+ private final int maxSize;
+
+ private PartOutputStream(OutputStream os, boolean writeHeaders, boolean writeBody,
+ int startIndex, int maxSize) {
+ super(os);
+ this.writeHeaders = writeHeaders;
+ this.writeBody = writeBody;
+ this.startIndex = startIndex;
+ this.maxSize = maxSize;
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ size++;
+ if (((state != BODY && writeHeaders) || (state == BODY && writeBody)) &&
+ (size > startIndex) && (bufferSize < maxSize)
+ ) {
+ super.write(b);
+ bufferSize++;
+ }
+ if (state == START) {
+ if (b == '\r') {
+ state = CR;
+ }
+ } else if (state == CR) {
+ if (b == '\n') {
+ state = CRLF;
+ } else {
+ state = START;
+ }
+ } else if (state == CRLF) {
+ if (b == '\r') {
+ state = CRLFCR;
+ } else {
+ state = START;
+ }
+ } else if (state == CRLFCR) {
+ if (b == '\n') {
+ state = BODY;
+ } else {
+ state = START;
+ }
+ }
+ }
+ }
+
+ /**
+ * Partial output stream, start at startIndex and write maxSize bytes.
+ */
+ private static final class PartialOutputStream extends FilterOutputStream {
+ private int size;
+ private int bufferSize;
+ private final int startIndex;
+ private final int maxSize;
+
+ private PartialOutputStream(OutputStream os, int startIndex, int maxSize) {
+ super(os);
+ this.startIndex = startIndex;
+ this.maxSize = maxSize;
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ size++;
+ if ((size > startIndex) && (bufferSize < maxSize)) {
+ super.write(b);
+ bufferSize++;
+ }
+ }
+ }
+
+ protected abstract static class AbstractRangeIterator implements Iterator<ExchangeSession.Message> {
+ ExchangeSession.MessageList messages;
+ int currentIndex;
+
+ protected int getCurrentIndex() {
+ return currentIndex;
+ }
+ }
+
+ protected static class UIDRangeIterator extends AbstractRangeIterator {
+ final String[] ranges;
+ int currentRangeIndex;
+ long startUid;
+ long endUid;
+
+ protected UIDRangeIterator(ExchangeSession.MessageList messages, String value) {
+ this.messages = messages;
+ ranges = value.split(",");
+ }
+
+ protected long convertToLong(String value) {
+ if ("*".equals(value)) {
+ return Long.MAX_VALUE;
+ } else {
+ return Long.parseLong(value);
+ }
+ }
+
+ protected void skipToNextRangeStartUid() {
+ if (currentRangeIndex < ranges.length) {
+ String currentRange = ranges[currentRangeIndex++];
+ int colonIndex = currentRange.indexOf(':');
+ if (colonIndex > 0) {
+ startUid = convertToLong(currentRange.substring(0, colonIndex));
+ endUid = convertToLong(currentRange.substring(colonIndex + 1));
+ if (endUid < startUid) {
+ long swap = endUid;
+ endUid = startUid;
+ startUid = swap;
+ }
+ } else if ("*".equals(currentRange)) {
+ startUid = endUid = messages.get(messages.size() - 1).getImapUid();
+ } else {
+ startUid = endUid = convertToLong(currentRange);
+ }
+ while (currentIndex < messages.size() && messages.get(currentIndex).getImapUid() < startUid) {
+ currentIndex++;
+ }
+ } else {
+ currentIndex = messages.size();
+ }
+ }
+
+ protected boolean hasNextInRange() {
+ return hasNextIndex() && messages.get(currentIndex).getImapUid() <= endUid;
+ }
+
+ protected boolean hasNextIndex() {
+ return currentIndex < messages.size();
+ }
+
+ protected boolean hasNextRange() {
+ return currentRangeIndex < ranges.length;
+ }
+
+ public boolean hasNext() {
+ boolean hasNextInRange = hasNextInRange();
+ // if has next range and current index after current range end, reset index
+ if (hasNextRange() && !hasNextInRange) {
+ currentIndex = 0;
+ }
+ while (hasNextIndex() && !hasNextInRange) {
+ skipToNextRangeStartUid();
+ hasNextInRange = hasNextInRange();
+ }
+ return hasNextIndex();
+ }
+
+ public ExchangeSession.Message next() {
+ ExchangeSession.Message message = messages.get(currentIndex++);
+ long uid = message.getImapUid();
+ if (uid < startUid || uid > endUid) {
+ throw new RuntimeException("Message uid " + uid + " not in range " + startUid + ':' + endUid);
+ }
+ return message;
+ }
+
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ protected static class RangeIterator extends AbstractRangeIterator {
+ final String[] ranges;
+ int currentRangeIndex;
+ long startUid;
+ long endUid;
+
+ protected RangeIterator(ExchangeSession.MessageList messages, String value) {
+ this.messages = messages;
+ ranges = value.split(",");
+ }
+
+ protected long convertToLong(String value) {
+ if ("*".equals(value)) {
+ return Long.MAX_VALUE;
+ } else {
+ return Long.parseLong(value);
+ }
+ }
+
+ protected void skipToNextRangeStart() {
+ if (currentRangeIndex < ranges.length) {
+ String currentRange = ranges[currentRangeIndex++];
+ int colonIndex = currentRange.indexOf(':');
+ if (colonIndex > 0) {
+ startUid = convertToLong(currentRange.substring(0, colonIndex));
+ endUid = convertToLong(currentRange.substring(colonIndex + 1));
+ if (endUid < startUid) {
+ long swap = endUid;
+ endUid = startUid;
+ startUid = swap;
+ }
+ } else if ("*".equals(currentRange)) {
+ startUid = endUid = messages.size();
+ } else {
+ startUid = endUid = convertToLong(currentRange);
+ }
+ while (currentIndex < messages.size() && (currentIndex + 1) < startUid) {
+ currentIndex++;
+ }
+ } else {
+ currentIndex = messages.size();
+ }
+ }
+
+ protected boolean hasNextInRange() {
+ return hasNextIndex() && currentIndex < endUid;
+ }
+
+ protected boolean hasNextIndex() {
+ return currentIndex < messages.size();
+ }
+
+ protected boolean hasNextRange() {
+ return currentRangeIndex < ranges.length;
+ }
+
+ public boolean hasNext() {
+ boolean hasNextInRange = hasNextInRange();
+ // if has next range and current index after current range end, reset index
+ if (hasNextRange() && !hasNextInRange) {
+ currentIndex = 0;
+ }
+ while (hasNextIndex() && !hasNextInRange) {
+ skipToNextRangeStart();
+ hasNextInRange = hasNextInRange();
+ }
+ return hasNextIndex();
+ }
+
+ public ExchangeSession.Message next() {
+ return messages.get(currentIndex++);
+ }
+
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ class IMAPTokenizer extends StringTokenizer {
+ IMAPTokenizer(String value) {
+ super(value);
+ }
+
+ @Override
+ public String nextToken() {
+ return removeQuotes(nextQuotedToken());
+ }
+
+ public String nextQuotedToken() {
+ StringBuilder nextToken = new StringBuilder();
+ nextToken.append(super.nextToken());
+ while (hasMoreTokens() && nextToken.length() > 0 && nextToken.charAt(0) == '"'
+ && (nextToken.charAt(nextToken.length() - 1) != '"' || nextToken.length() == 1)) {
+ nextToken.append(' ').append(super.nextToken());
+ }
+ while (hasMoreTokens() && nextToken.length() > 0 && nextToken.charAt(0) == '('
+ && nextToken.charAt(nextToken.length() - 1) != ')') {
+ nextToken.append(' ').append(super.nextToken());
+ }
+ while (hasMoreTokens() && nextToken.length() > 0 && nextToken.indexOf("[") != -1
+ && nextToken.charAt(nextToken.length() - 1) != ']') {
+ nextToken.append(' ').append(super.nextToken());
+ }
+ return nextToken.toString();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/java/davmail/imap/ImapServer.java b/src/java/davmail/imap/ImapServer.java
new file mode 100644
index 0000000..06e8616
--- /dev/null
+++ b/src/java/davmail/imap/ImapServer.java
@@ -0,0 +1,58 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.imap;
+
+
+import davmail.AbstractConnection;
+import davmail.AbstractServer;
+import davmail.Settings;
+
+import java.net.Socket;
+
+/**
+ * Pop3 server
+ */
+public class ImapServer extends AbstractServer {
+ /**
+ * Default IMAP port
+ */
+ public static final int DEFAULT_PORT = 143;
+
+ /**
+ * Create a ServerSocket to listen for connections.
+ * Start the thread.
+ *
+ * @param port imap listen port, 143 if not defined (0)
+ */
+ public ImapServer(int port) {
+ super(ImapServer.class.getName(), port, ImapServer.DEFAULT_PORT);
+ nosslFlag = Settings.getBooleanProperty("davmail.ssl.nosecureimap");
+ }
+
+ @Override
+ public String getProtocolName() {
+ return "IMAP";
+ }
+
+ @Override
+ public AbstractConnection createConnectionHandler(Socket clientSocket) {
+ return new ImapConnection(clientSocket);
+ }
+
+}
diff --git a/src/java/davmail/ldap/LdapConnection.java b/src/java/davmail/ldap/LdapConnection.java
new file mode 100644
index 0000000..960d77d
--- /dev/null
+++ b/src/java/davmail/ldap/LdapConnection.java
@@ -0,0 +1,1687 @@
+/*
+* DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.ldap;
+
+import com.sun.jndi.ldap.Ber;
+import com.sun.jndi.ldap.BerDecoder;
+import com.sun.jndi.ldap.BerEncoder;
+import davmail.AbstractConnection;
+import davmail.BundleMessage;
+import davmail.Settings;
+import davmail.exception.DavMailException;
+import davmail.exchange.ExchangeSession;
+import davmail.exchange.ExchangeSessionFactory;
+import davmail.exchange.dav.DavExchangeSession;
+import davmail.ui.tray.DavGatewayTray;
+import org.apache.log4j.Logger;
+
+import javax.security.auth.callback.*;
+import javax.security.sasl.AuthorizeCallback;
+import javax.security.sasl.Sasl;
+import javax.security.sasl.SaslServer;
+import java.io.*;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.*;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+/**
+ * Handle a caldav connection.
+ */
+public class LdapConnection extends AbstractConnection {
+ private static final Logger LOGGER = Logger.getLogger(LdapConnection.class);
+ /**
+ * Davmail base context
+ */
+ static final String BASE_CONTEXT = "ou=people";
+ /**
+ * OSX server (OpenDirectory) base context
+ */
+ static final String OD_BASE_CONTEXT = "o=od";
+ static final String OD_USER_CONTEXT = "cn=users, o=od";
+ static final String OD_CONFIG_CONTEXT = "cn=config, o=od";
+ static final String COMPUTER_CONTEXT = "cn=computers, o=od";
+ static final String OD_GROUP_CONTEXT = "cn=groups, o=od";
+
+ // TODO: adjust Directory Utility settings
+ static final String COMPUTER_CONTEXT_LION = "cn=computers,o=od";
+ static final String OD_USER_CONTEXT_LION = "cn=users, ou=people";
+
+ /**
+ * Root DSE naming contexts (default and OpenDirectory)
+ */
+ static final List<String> NAMING_CONTEXTS = new ArrayList<String>();
+
+ static {
+ NAMING_CONTEXTS.add(BASE_CONTEXT);
+ NAMING_CONTEXTS.add(OD_BASE_CONTEXT);
+ }
+
+ static final List<String> PERSON_OBJECT_CLASSES = new ArrayList<String>();
+
+ static {
+ PERSON_OBJECT_CLASSES.add("top");
+ PERSON_OBJECT_CLASSES.add("person");
+ PERSON_OBJECT_CLASSES.add("organizationalPerson");
+ PERSON_OBJECT_CLASSES.add("inetOrgPerson");
+ // OpenDirectory class for iCal
+ PERSON_OBJECT_CLASSES.add("apple-user");
+ }
+
+ /**
+ * Map Exchange contact attribute names to LDAP attributes.
+ * Used only when returningAttributes is empty in LDAP request (return all available attributes)
+ */
+ static final HashMap<String, String> CONTACT_TO_LDAP_ATTRIBUTE_MAP = new HashMap<String, String>();
+
+ static {
+ CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("imapUid", "uid");
+ CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("co", "countryname");
+ CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("extensionattribute1", "custom1");
+ CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("extensionattribute2", "custom2");
+ CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("extensionattribute3", "custom3");
+ CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("extensionattribute4", "custom4");
+ CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("smtpemail1", "mail");
+ CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("smtpemail2", "xmozillasecondemail");
+ CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("homeCountry", "mozillahomecountryname");
+ CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("homeCity", "mozillahomelocalityname");
+ CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("homePostalCode", "mozillahomepostalcode");
+ CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("homeState", "mozillahomestate");
+ CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("homeStreet", "mozillahomestreet");
+ CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("businesshomepage", "mozillaworkurl");
+ CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("nickname", "mozillanickname");
+ }
+
+ /**
+ * OSX constant computer guid (used by iCal attendee completion)
+ */
+ static final String COMPUTER_GUID = "52486C30-F0AB-48E3-9C37-37E9B28CDD7B";
+ /**
+ * OSX constant virtual host guid (used by iCal attendee completion)
+ */
+ static final String VIRTUALHOST_GUID = "D6DD8A10-1098-11DE-8C30-0800200C9A66";
+
+ /**
+ * OSX constant value for attribute apple-serviceslocator
+ */
+ static final HashMap<String, String> STATIC_ATTRIBUTE_MAP = new HashMap<String, String>();
+
+ static {
+ STATIC_ATTRIBUTE_MAP.put("apple-serviceslocator", COMPUTER_GUID + ':' + VIRTUALHOST_GUID + ":calendar");
+ }
+
+ /**
+ * LDAP to Exchange Criteria Map
+ */
+ // TODO: remove
+ static final HashMap<String, String> CRITERIA_MAP = new HashMap<String, String>();
+
+ static {
+ // assume mail starts with firstname
+ CRITERIA_MAP.put("uid", "AN");
+ CRITERIA_MAP.put("mail", "FN");
+ CRITERIA_MAP.put("displayname", "DN");
+ CRITERIA_MAP.put("cn", "DN");
+ CRITERIA_MAP.put("givenname", "FN");
+ CRITERIA_MAP.put("sn", "LN");
+ CRITERIA_MAP.put("title", "TL");
+ CRITERIA_MAP.put("company", "CP");
+ CRITERIA_MAP.put("o", "CP");
+ CRITERIA_MAP.put("l", "OF");
+ CRITERIA_MAP.put("department", "DP");
+ CRITERIA_MAP.put("apple-group-realname", "DP");
+ }
+
+ /**
+ * LDAP to Exchange contact attribute map.
+ */
+ static final HashMap<String, String> LDAP_TO_CONTACT_ATTRIBUTE_MAP = new HashMap<String, String>();
+
+ static {
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("uid", "imapUid");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mail", "smtpemail1");
+
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("displayname", "cn");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("commonname", "cn");
+
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("givenname", "givenName");
+
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("surname", "sn");
+
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("company", "o");
+
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("apple-group-realname", "department");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillahomelocalityname", "homeCity");
+
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("c", "co");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("countryname", "co");
+
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("custom1", "extensionattribute1");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("custom2", "extensionattribute2");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("custom3", "extensionattribute3");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("custom4", "extensionattribute4");
+
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillacustom1", "extensionattribute1");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillacustom2", "extensionattribute2");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillacustom3", "extensionattribute3");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillacustom4", "extensionattribute4");
+
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("telephonenumber", "telephoneNumber");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("orgunit", "department");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("departmentnumber", "department");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("ou", "department");
+
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillaworkstreet2", null);
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillahomestreet", "homeStreet");
+
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("xmozillanickname", "nickname");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillanickname", "nickname");
+
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("cellphone", "mobile");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("homeurl", "personalHomePage");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillahomeurl", "personalHomePage");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("apple-user-homeurl", "personalHomePage");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillahomepostalcode", "homePostalCode");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("fax", "facsimiletelephonenumber");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillahomecountryname", "homeCountry");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("streetaddress", "street");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillaworkurl", "businesshomepage");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("workurl", "businesshomepage");
+
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("region", "st");
+
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("birthmonth", "bday");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("birthday", "bday");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("birthyear", "bday");
+
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("carphone", "othermobile");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("nsaimid", "im");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("nscpaimscreenname", "im");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("apple-imhandle", "im");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("imhandle", "im");
+
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("xmozillasecondemail", "smtpemail2");
+
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("notes", "description");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("pagerphone", "pager");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("pager", "pager");
+
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("locality", "l");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("homephone", "homePhone");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillasecondemail", "smtpemail2");
+
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("zip", "postalcode");
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillahomestate", "homeState");
+
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("modifytimestamp", "lastmodified");
+
+ // ignore attribute
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("objectclass", null);
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillausehtmlmail", null);
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("xmozillausehtmlmail", null);
+
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillahomestreet2", null);
+
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("labeleduri", null);
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("apple-generateduid", null);
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("uidnumber", null);
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("gidnumber", null);
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("jpegphoto", null);
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("apple-emailcontacts", null);
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("apple-user-picture", null);
+
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("_writers_usercertificate", null);
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("_writers_realname", null);
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("_writers_jpegphoto", null);
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("_guest", null);
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("_writers_linkedidentity", null);
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("_defaultlanguage", null);
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("_writers_hint", null);
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("_writers__defaultlanguage", null);
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("_writers_picture", null);
+
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("apple-user-authenticationhint", null);
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("external", null);
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("userpassword", null);
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("linkedidentity", null);
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("homedirectory", null);
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("authauthority", null);
+
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("applefloor", null);
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("buildingname", null);
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("destinationindicator", null);
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("postaladdress", null);
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("homepostaladdress", null);
+
+ // iCal search attribute
+ LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("apple-serviceslocator", "apple-serviceslocator");
+ }
+
+ /**
+ * LDAP filter attributes ignore map
+ */
+ // TODO remove
+ static final HashSet<String> IGNORE_MAP = new HashSet<String>();
+
+ static {
+ IGNORE_MAP.add("objectclass");
+ IGNORE_MAP.add("apple-generateduid");
+ IGNORE_MAP.add("augmentconfiguration");
+ IGNORE_MAP.add("ou");
+ IGNORE_MAP.add("apple-realname");
+ IGNORE_MAP.add("apple-group-nestedgroup");
+ IGNORE_MAP.add("apple-group-memberguid");
+ IGNORE_MAP.add("macaddress");
+ IGNORE_MAP.add("memberuid");
+ }
+
+ // LDAP version
+ // static final int LDAP_VERSION2 = 0x02;
+ static final int LDAP_VERSION3 = 0x03;
+
+ // LDAP request operations
+ static final int LDAP_REQ_BIND = 0x60;
+ static final int LDAP_REQ_SEARCH = 0x63;
+ static final int LDAP_REQ_UNBIND = 0x42;
+ static final int LDAP_REQ_ABANDON = 0x50;
+
+ // LDAP response operations
+ static final int LDAP_REP_BIND = 0x61;
+ static final int LDAP_REP_SEARCH = 0x64;
+ static final int LDAP_REP_RESULT = 0x65;
+
+ static final int LDAP_SASL_BIND_IN_PROGRESS = 0x0E;
+
+ // LDAP return codes
+ static final int LDAP_OTHER = 80;
+ static final int LDAP_SUCCESS = 0;
+ static final int LDAP_SIZE_LIMIT_EXCEEDED = 4;
+ static final int LDAP_INVALID_CREDENTIALS = 49;
+
+ // LDAP filter code
+ static final int LDAP_FILTER_AND = 0xa0;
+ static final int LDAP_FILTER_OR = 0xa1;
+ static final int LDAP_FILTER_NOT = 0xa2;
+
+ // LDAP filter operators
+ static final int LDAP_FILTER_SUBSTRINGS = 0xa4;
+ //static final int LDAP_FILTER_GE = 0xa5;
+ //static final int LDAP_FILTER_LE = 0xa6;
+ static final int LDAP_FILTER_PRESENT = 0x87;
+ //static final int LDAP_FILTER_APPROX = 0xa8;
+ static final int LDAP_FILTER_EQUALITY = 0xa3;
+
+ // LDAP filter mode
+ static final int LDAP_SUBSTRING_INITIAL = 0x80;
+ static final int LDAP_SUBSTRING_ANY = 0x81;
+ static final int LDAP_SUBSTRING_FINAL = 0x82;
+
+ // BER data types
+ static final int LBER_ENUMERATED = 0x0a;
+ static final int LBER_SET = 0x31;
+ static final int LBER_SEQUENCE = 0x30;
+
+ // LDAP search scope
+ static final int SCOPE_BASE_OBJECT = 0;
+ //static final int SCOPE_ONE_LEVEL = 1;
+ //static final int SCOPE_SUBTREE = 2;
+
+ /**
+ * For some unknown reason parseIntWithTag is private !
+ */
+ static final Method PARSE_INT_WITH_TAG_METHOD;
+
+ static {
+ try {
+ PARSE_INT_WITH_TAG_METHOD = BerDecoder.class.getDeclaredMethod("parseIntWithTag", int.class);
+ PARSE_INT_WITH_TAG_METHOD.setAccessible(true);
+ } catch (NoSuchMethodException e) {
+ DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_GET_PARSEINTWITHTAG"));
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Sasl server for DIGEST-MD5 authentication
+ */
+ protected SaslServer saslServer;
+
+ /**
+ * raw connection inputStream
+ */
+ protected BufferedInputStream is;
+
+ /**
+ * reusable BER encoder
+ */
+ protected final BerEncoder responseBer = new BerEncoder();
+
+ /**
+ * Current LDAP version (used for String encoding)
+ */
+ int ldapVersion = LDAP_VERSION3;
+
+ /**
+ * Search threads map
+ */
+ protected final HashMap<Integer, SearchRunnable> searchThreadMap = new HashMap<Integer, SearchRunnable>();
+
+ /**
+ * Initialize the streams and start the thread.
+ *
+ * @param clientSocket LDAP client socket
+ */
+ public LdapConnection(Socket clientSocket) {
+ super(LdapConnection.class.getSimpleName(), clientSocket);
+ try {
+ is = new BufferedInputStream(client.getInputStream());
+ os = new BufferedOutputStream(client.getOutputStream());
+ } catch (IOException e) {
+ close();
+ DavGatewayTray.error(new BundleMessage("LOG_EXCEPTION_GETTING_SOCKET_STREAMS"), e);
+ }
+ }
+
+ protected boolean isLdapV3() {
+ return ldapVersion == LDAP_VERSION3;
+ }
+
+ @Override
+ public void run() {
+ byte[] inbuf = new byte[2048]; // Buffer for reading incoming bytes
+ int bytesread; // Number of bytes in inbuf
+ int bytesleft; // Number of bytes that need to read for completing resp
+ int br; // Temp; number of bytes read from stream
+ int offset; // Offset of where to store bytes in inbuf
+ boolean eos; // End of stream
+
+ try {
+ ExchangeSessionFactory.checkConfig();
+ while (true) {
+ offset = 0;
+
+ // check that it is the beginning of a sequence
+ bytesread = is.read(inbuf, offset, 1);
+ if (bytesread < 0) {
+ break; // EOF
+ }
+
+ if (inbuf[offset++] != (Ber.ASN_SEQUENCE | Ber.ASN_CONSTRUCTOR)) {
+ continue;
+ }
+
+ // get length of sequence
+ bytesread = is.read(inbuf, offset, 1);
+ if (bytesread < 0) {
+ break; // EOF
+ }
+ int seqlen = inbuf[offset++]; // Length of ASN sequence
+
+ // if high bit is on, length is encoded in the
+ // subsequent length bytes and the number of length bytes
+ // is equal to & 0x80 (i.e. length byte with high bit off).
+ if ((seqlen & 0x80) == 0x80) {
+ int seqlenlen = seqlen & 0x7f; // number of length bytes
+
+ bytesread = 0;
+ eos = false;
+
+ // Read all length bytes
+ while (bytesread < seqlenlen) {
+ br = is.read(inbuf, offset + bytesread,
+ seqlenlen - bytesread);
+ if (br < 0) {
+ eos = true;
+ break; // EOF
+ }
+ bytesread += br;
+ }
+
+ // end-of-stream reached before length bytes are read
+ if (eos) {
+ break; // EOF
+ }
+
+ // Add contents of length bytes to determine length
+ seqlen = 0;
+ for (int i = 0; i < seqlenlen; i++) {
+ seqlen = (seqlen << 8) + (inbuf[offset + i] & 0xff);
+ }
+ offset += bytesread;
+ }
+
+ // read in seqlen bytes
+ bytesleft = seqlen;
+ if ((offset + bytesleft) > inbuf.length) {
+ byte[] nbuf = new byte[offset + bytesleft];
+ System.arraycopy(inbuf, 0, nbuf, 0, offset);
+ inbuf = nbuf;
+ }
+ while (bytesleft > 0) {
+ bytesread = is.read(inbuf, offset, bytesleft);
+ if (bytesread < 0) {
+ break; // EOF
+ }
+ offset += bytesread;
+ bytesleft -= bytesread;
+ }
+
+ DavGatewayTray.switchIcon();
+
+ handleRequest(inbuf, offset);
+ }
+
+ } catch (SocketException e) {
+ DavGatewayTray.debug(new BundleMessage("LOG_CONNECTION_CLOSED"));
+ } catch (SocketTimeoutException e) {
+ DavGatewayTray.debug(new BundleMessage("LOG_CLOSE_CONNECTION_ON_TIMEOUT"));
+ } catch (Exception e) {
+ DavGatewayTray.log(e);
+ try {
+ sendErr(0, LDAP_REP_BIND, e);
+ } catch (IOException e2) {
+ DavGatewayTray.warn(new BundleMessage("LOG_EXCEPTION_SENDING_ERROR_TO_CLIENT"), e2);
+ }
+ } finally {
+ // cancel all search threads
+ synchronized (searchThreadMap) {
+ for (SearchRunnable searchRunnable : searchThreadMap.values()) {
+ searchRunnable.abandon();
+ }
+ }
+ close();
+ }
+ DavGatewayTray.resetIcon();
+ }
+
+ protected static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+
+ protected void handleRequest(byte[] inbuf, int offset) throws IOException {
+ //dumpBer(inbuf, offset);
+ BerDecoder reqBer = new BerDecoder(inbuf, 0, offset);
+ int currentMessageId = 0;
+ try {
+ reqBer.parseSeq(null);
+ currentMessageId = reqBer.parseInt();
+ int requestOperation = reqBer.peekByte();
+
+ if (requestOperation == LDAP_REQ_BIND) {
+ reqBer.parseSeq(null);
+ ldapVersion = reqBer.parseInt();
+ userName = reqBer.parseString(isLdapV3());
+ if (reqBer.peekByte() == (Ber.ASN_CONTEXT | Ber.ASN_CONSTRUCTOR | 3)) {
+ // SASL authentication
+ reqBer.parseSeq(null);
+ // Get mechanism, usually DIGEST-MD5
+ String mechanism = reqBer.parseString(isLdapV3());
+
+ byte[] serverResponse;
+ CallbackHandler callbackHandler = new CallbackHandler() {
+ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+ // look for username in callbacks
+ for (Callback callback : callbacks) {
+ if (callback instanceof NameCallback) {
+ userName = ((NameCallback) callback).getDefaultName();
+ // get password from session pool
+ password = ExchangeSessionFactory.getUserPassword(userName);
+ }
+ }
+ // handle other callbacks
+ for (Callback callback : callbacks) {
+ if (callback instanceof AuthorizeCallback) {
+ ((AuthorizeCallback) callback).setAuthorized(true);
+ } else if (callback instanceof PasswordCallback) {
+ if (password != null) {
+ ((PasswordCallback) callback).setPassword(password.toCharArray());
+ }
+ }
+ }
+ }
+ };
+ int status;
+ if (reqBer.bytesLeft() > 0 && saslServer != null) {
+ byte[] clientResponse = reqBer.parseOctetString(Ber.ASN_OCTET_STR, null);
+ serverResponse = saslServer.evaluateResponse(clientResponse);
+ status = LDAP_SUCCESS;
+
+ DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_BIND_USER", currentMessageId, userName));
+ try {
+ session = ExchangeSessionFactory.getInstance(userName, password);
+ DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_BIND_SUCCESS"));
+ } catch (IOException e) {
+ serverResponse = EMPTY_BYTE_ARRAY;
+ status = LDAP_INVALID_CREDENTIALS;
+ DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_BIND_INVALID_CREDENTIALS"));
+ }
+
+ } else {
+ Map<String, String> properties = new HashMap<String, String>();
+ properties.put("javax.security.sasl.qop", "auth,auth-int");
+ saslServer = Sasl.createSaslServer(mechanism, "ldap", client.getLocalAddress().getHostAddress(), properties, callbackHandler);
+ serverResponse = saslServer.evaluateResponse(EMPTY_BYTE_ARRAY);
+ status = LDAP_SASL_BIND_IN_PROGRESS;
+ }
+
+ responseBer.beginSeq(Ber.ASN_SEQUENCE | Ber.ASN_CONSTRUCTOR);
+ responseBer.encodeInt(currentMessageId);
+ responseBer.beginSeq(LDAP_REP_BIND);
+ responseBer.encodeInt(status, LBER_ENUMERATED);
+ // server credentials
+ responseBer.encodeString("", isLdapV3());
+ responseBer.encodeString("", isLdapV3());
+ // challenge or response
+ if (serverResponse != null) {
+ responseBer.encodeOctetString(serverResponse, 0x87);
+ }
+ responseBer.endSeq();
+ responseBer.endSeq();
+ sendResponse();
+
+ } else {
+ password = reqBer.parseStringWithTag(Ber.ASN_CONTEXT, isLdapV3(), null);
+
+ if (userName.length() > 0 && password.length() > 0) {
+ DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_BIND_USER", currentMessageId, userName));
+ try {
+ session = ExchangeSessionFactory.getInstance(userName, password);
+ DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_BIND_SUCCESS"));
+ sendClient(currentMessageId, LDAP_REP_BIND, LDAP_SUCCESS, "");
+ } catch (IOException e) {
+ DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_BIND_INVALID_CREDENTIALS"));
+ sendClient(currentMessageId, LDAP_REP_BIND, LDAP_INVALID_CREDENTIALS, "");
+ }
+ } else {
+ DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_BIND_ANONYMOUS", currentMessageId));
+ // anonymous bind
+ sendClient(currentMessageId, LDAP_REP_BIND, LDAP_SUCCESS, "");
+ }
+ }
+
+ } else if (requestOperation == LDAP_REQ_UNBIND) {
+ DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_UNBIND", currentMessageId));
+ if (session != null) {
+ session = null;
+ }
+ } else if (requestOperation == LDAP_REQ_SEARCH) {
+ reqBer.parseSeq(null);
+ String dn = reqBer.parseString(isLdapV3());
+ int scope = reqBer.parseEnumeration();
+ /*int derefAliases =*/
+ reqBer.parseEnumeration();
+ int sizeLimit = reqBer.parseInt();
+ if (sizeLimit > 100 || sizeLimit == 0) {
+ sizeLimit = 100;
+ }
+ int timelimit = reqBer.parseInt();
+ /*boolean typesOnly =*/
+ reqBer.parseBoolean();
+ LdapFilter ldapFilter = parseFilter(reqBer);
+ Set<String> returningAttributes = parseReturningAttributes(reqBer);
+ SearchRunnable searchRunnable = new SearchRunnable(currentMessageId, dn, scope, sizeLimit, timelimit, ldapFilter, returningAttributes);
+ if (BASE_CONTEXT.equalsIgnoreCase(dn) || OD_USER_CONTEXT.equalsIgnoreCase(dn) || OD_USER_CONTEXT_LION.equalsIgnoreCase(dn)) {
+ // launch search in a separate thread
+ synchronized (searchThreadMap) {
+ searchThreadMap.put(currentMessageId, searchRunnable);
+ }
+ Thread searchThread = new Thread(searchRunnable);
+ searchThread.setName(getName() + "-Search-" + currentMessageId);
+ searchThread.start();
+ } else {
+ // no need to create a separate thread, just run
+ searchRunnable.run();
+ }
+
+ } else if (requestOperation == LDAP_REQ_ABANDON) {
+ int abandonMessageId = 0;
+ try {
+ abandonMessageId = (Integer) PARSE_INT_WITH_TAG_METHOD.invoke(reqBer, LDAP_REQ_ABANDON);
+ synchronized (searchThreadMap) {
+ SearchRunnable searchRunnable = searchThreadMap.get(abandonMessageId);
+ if (searchRunnable != null) {
+ searchRunnable.abandon();
+ searchThreadMap.remove(currentMessageId);
+ }
+ }
+ } catch (IllegalAccessException e) {
+ DavGatewayTray.error(e);
+ } catch (InvocationTargetException e) {
+ DavGatewayTray.error(e);
+ }
+ DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_ABANDON_SEARCH", currentMessageId, abandonMessageId));
+ } else {
+ DavGatewayTray.debug(new BundleMessage("LOG_LDAP_UNSUPPORTED_OPERATION", requestOperation));
+ sendClient(currentMessageId, LDAP_REP_RESULT, LDAP_OTHER, "Unsupported operation");
+ }
+ } catch (IOException e) {
+ dumpBer(inbuf, offset);
+ try {
+ sendErr(currentMessageId, LDAP_REP_RESULT, e);
+ } catch (IOException e2) {
+ DavGatewayTray.debug(new BundleMessage("LOG_EXCEPTION_SENDING_ERROR_TO_CLIENT"), e2);
+ }
+ throw e;
+ }
+ }
+
+ protected void dumpBer(byte[] inbuf, int offset) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ Ber.dumpBER(baos, "LDAP request buffer\n", inbuf, 0, offset);
+ try {
+ LOGGER.debug(new String(baos.toByteArray(), "UTF-8"));
+ } catch (UnsupportedEncodingException e) {
+ // should not happen
+ LOGGER.error(e);
+ }
+ }
+
+ protected LdapFilter parseFilter(BerDecoder reqBer) throws IOException {
+ LdapFilter ldapFilter;
+ if (reqBer.peekByte() == LDAP_FILTER_PRESENT) {
+ String attributeName = reqBer.parseStringWithTag(LDAP_FILTER_PRESENT, isLdapV3(), null).toLowerCase();
+ ldapFilter = new SimpleFilter(attributeName);
+ } else {
+ int[] seqSize = new int[1];
+ int ldapFilterType = reqBer.parseSeq(seqSize);
+ int end = reqBer.getParsePosition() + seqSize[0];
+
+ ldapFilter = parseNestedFilter(reqBer, ldapFilterType, end);
+ }
+
+ return ldapFilter;
+ }
+
+ protected LdapFilter parseNestedFilter(BerDecoder reqBer, int ldapFilterType, int end) throws IOException {
+ LdapFilter nestedFilter;
+
+ if ((ldapFilterType == LDAP_FILTER_OR) || (ldapFilterType == LDAP_FILTER_AND)
+ || ldapFilterType == LDAP_FILTER_NOT) {
+ nestedFilter = new CompoundFilter(ldapFilterType);
+
+ while (reqBer.getParsePosition() < end && reqBer.bytesLeft() > 0) {
+ if (reqBer.peekByte() == LDAP_FILTER_PRESENT) {
+ String attributeName = reqBer.parseStringWithTag(LDAP_FILTER_PRESENT, isLdapV3(), null).toLowerCase();
+ nestedFilter.add(new SimpleFilter(attributeName));
+ } else {
+ int[] seqSize = new int[1];
+ int ldapFilterOperator = reqBer.parseSeq(seqSize);
+ int subEnd = reqBer.getParsePosition() + seqSize[0];
+ nestedFilter.add(parseNestedFilter(reqBer, ldapFilterOperator, subEnd));
+ }
+ }
+ } else {
+ // simple filter
+ nestedFilter = parseSimpleFilter(reqBer, ldapFilterType);
+ }
+
+ return nestedFilter;
+ }
+
+ protected LdapFilter parseSimpleFilter(BerDecoder reqBer, int ldapFilterOperator) throws IOException {
+ String attributeName = reqBer.parseString(isLdapV3()).toLowerCase();
+ int ldapFilterMode = 0;
+
+ StringBuilder value = new StringBuilder();
+ if (ldapFilterOperator == LDAP_FILTER_SUBSTRINGS) {
+ // Thunderbird sends values with space as separate strings, rebuild value
+ int[] seqSize = new int[1];
+ /*LBER_SEQUENCE*/
+ reqBer.parseSeq(seqSize);
+ int end = reqBer.getParsePosition() + seqSize[0];
+ while (reqBer.getParsePosition() < end && reqBer.bytesLeft() > 0) {
+ ldapFilterMode = reqBer.peekByte();
+ if (value.length() > 0) {
+ value.append(' ');
+ }
+ value.append(reqBer.parseStringWithTag(ldapFilterMode, isLdapV3(), null));
+ }
+ } else if (ldapFilterOperator == LDAP_FILTER_EQUALITY) {
+ value.append(reqBer.parseString(isLdapV3()));
+ } else {
+ DavGatewayTray.warn(new BundleMessage("LOG_LDAP_UNSUPPORTED_FILTER_VALUE"));
+ }
+
+ String sValue = value.toString();
+
+ if ("uid".equalsIgnoreCase(attributeName) && sValue.equals(userName)) {
+ // replace with actual alias instead of login name search, only in Dav mode
+ if (sValue.equals(userName) && session instanceof DavExchangeSession) {
+ sValue = session.getAlias();
+ DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REPLACED_UID_FILTER", userName, sValue));
+ }
+ }
+
+ return new SimpleFilter(attributeName, sValue, ldapFilterOperator, ldapFilterMode);
+ }
+
+ protected Set<String> parseReturningAttributes(BerDecoder reqBer) throws IOException {
+ Set<String> returningAttributes = new HashSet<String>();
+ int[] seqSize = new int[1];
+ reqBer.parseSeq(seqSize);
+ int end = reqBer.getParsePosition() + seqSize[0];
+ while (reqBer.getParsePosition() < end && reqBer.bytesLeft() > 0) {
+ returningAttributes.add(reqBer.parseString(isLdapV3()).toLowerCase());
+ }
+ return returningAttributes;
+ }
+
+ /**
+ * Send Root DSE
+ *
+ * @param currentMessageId current message id
+ * @throws IOException on error
+ */
+ protected void sendRootDSE(int currentMessageId) throws IOException {
+ DavGatewayTray.debug(new BundleMessage("LOG_LDAP_SEND_ROOT_DSE"));
+
+ Map<String, Object> attributes = new HashMap<String, Object>();
+ attributes.put("objectClass", "top");
+ attributes.put("namingContexts", NAMING_CONTEXTS);
+ //attributes.put("supportedsaslmechanisms", "PLAIN");
+
+ sendEntry(currentMessageId, "Root DSE", attributes);
+ }
+
+ protected void addIf(Map<String, Object> attributes, Set<String> returningAttributes, String name, Object value) {
+ if ((returningAttributes.isEmpty()) || returningAttributes.contains(name)) {
+ attributes.put(name, value);
+ }
+ }
+
+ protected String currentHostName;
+
+ protected String getCurrentHostName() throws UnknownHostException {
+ if (currentHostName == null) {
+ if (client.getInetAddress().isLoopbackAddress()) {
+ // local address, probably using localhost in iCal URL
+ currentHostName = "localhost";
+ } else {
+ // remote address, send fully qualified domain name
+ currentHostName = InetAddress.getLocalHost().getCanonicalHostName();
+ }
+ }
+ return currentHostName;
+ }
+
+ /**
+ * Cache serviceInfo string value
+ */
+ protected String serviceInfo;
+
+ protected String getServiceInfo() throws UnknownHostException {
+ if (serviceInfo == null) {
+ StringBuilder buffer = new StringBuilder();
+ buffer.append("<?xml version='1.0' encoding='UTF-8'?>" +
+ "<!DOCTYPE plist PUBLIC '-//Apple//DTD PLIST 1.0//EN' 'http://www.apple.com/DTDs/PropertyList-1.0.dtd'>" +
+ "<plist version='1.0'>" +
+ "<dict>" +
+ "<key>com.apple.macosxserver.host</key>" +
+ "<array>" +
+ "<string>localhost</string>" + // NOTE: Will be replaced by real hostname
+ "</array>" +
+ "<key>com.apple.macosxserver.virtualhosts</key>" +
+ "<dict>" +
+ "<key>" + VIRTUALHOST_GUID + "</key>" +
+ "<dict>" +
+ "<key>hostDetails</key>" +
+ "<dict>" +
+ "<key>http</key>" +
+ "<dict>" +
+ "<key>enabled</key>" +
+ "<true/>" +
+ "<key>port</key>" +
+ "<integer>");
+ buffer.append(Settings.getProperty("davmail.caldavPort"));
+ buffer.append("</integer>" +
+ "</dict>" +
+ "<key>https</key>" +
+ "<dict>" +
+ "<key>disabled</key>" +
+ "<false/>" +
+ "<key>port</key>" +
+ "<integer>0</integer>" +
+ "</dict>" +
+ "</dict>" +
+ "<key>hostname</key>" +
+ "<string>");
+ buffer.append(getCurrentHostName());
+ buffer.append("</string>" +
+ "<key>serviceInfo</key>" +
+ "<dict>" +
+ "<key>calendar</key>" +
+ "<dict>" +
+ "<key>enabled</key>" +
+ "<true/>" +
+ "<key>templates</key>" +
+ "<dict>" +
+ "<key>calendarUserAddresses</key>" +
+ "<array>" +
+ "<string>%(principaluri)s</string>" +
+ "<string>mailto:%(email)s</string>" +
+ "<string>urn:uuid:%(guid)s</string>" +
+ "</array>" +
+ "<key>principalPath</key>" +
+ "<string>/principals/__uuids__/%(guid)s/</string>" +
+ "</dict>" +
+ "</dict>" +
+ "</dict>" +
+ "<key>serviceType</key>" +
+ "<array>" +
+ "<string>calendar</string>" +
+ "</array>" +
+ "</dict>" +
+ "</dict>" +
+ "</dict>" +
+ "</plist>");
+ serviceInfo = buffer.toString();
+ }
+ return serviceInfo;
+ }
+
+ /**
+ * Send ComputerContext
+ *
+ * @param currentMessageId current message id
+ * @param returningAttributes attributes to return
+ * @throws IOException on error
+ */
+ protected void sendComputerContext(int currentMessageId, Set<String> returningAttributes) throws IOException {
+ List<String> objectClasses = new ArrayList<String>();
+ objectClasses.add("top");
+ objectClasses.add("apple-computer");
+ Map<String, Object> attributes = new HashMap<String, Object>();
+ addIf(attributes, returningAttributes, "objectClass", objectClasses);
+ addIf(attributes, returningAttributes, "apple-generateduid", COMPUTER_GUID);
+ addIf(attributes, returningAttributes, "apple-serviceinfo", getServiceInfo());
+ // TODO: remove ?
+ addIf(attributes, returningAttributes, "apple-xmlplist", getServiceInfo());
+ addIf(attributes, returningAttributes, "apple-serviceslocator", "::anyService");
+ addIf(attributes, returningAttributes, "cn", getCurrentHostName());
+
+ String dn = "cn=" + getCurrentHostName() + ", " + COMPUTER_CONTEXT;
+ DavGatewayTray.debug(new BundleMessage("LOG_LDAP_SEND_COMPUTER_CONTEXT", dn, attributes));
+
+ sendEntry(currentMessageId, dn, attributes);
+ }
+
+ /**
+ * Send Base Context
+ *
+ * @param currentMessageId current message id
+ * @throws IOException on error
+ */
+ protected void sendBaseContext(int currentMessageId) throws IOException {
+ List<String> objectClasses = new ArrayList<String>();
+ objectClasses.add("top");
+ objectClasses.add("organizationalUnit");
+ Map<String, Object> attributes = new HashMap<String, Object>();
+ attributes.put("objectClass", objectClasses);
+ attributes.put("description", "DavMail Gateway LDAP for " + Settings.getProperty("davmail.url"));
+ sendEntry(currentMessageId, BASE_CONTEXT, attributes);
+ }
+
+ protected void sendEntry(int currentMessageId, String dn, Map<String, Object> attributes) throws IOException {
+ // synchronize on responseBer
+ synchronized (responseBer) {
+ responseBer.reset();
+ responseBer.beginSeq(Ber.ASN_SEQUENCE | Ber.ASN_CONSTRUCTOR);
+ responseBer.encodeInt(currentMessageId);
+ responseBer.beginSeq(LDAP_REP_SEARCH);
+ responseBer.encodeString(dn, isLdapV3());
+ responseBer.beginSeq(LBER_SEQUENCE);
+ for (Map.Entry<String, Object> entry : attributes.entrySet()) {
+ responseBer.beginSeq(LBER_SEQUENCE);
+ responseBer.encodeString(entry.getKey(), isLdapV3());
+ responseBer.beginSeq(LBER_SET);
+ Object values = entry.getValue();
+ if (values instanceof String) {
+ responseBer.encodeString((String) values, isLdapV3());
+ } else if (values instanceof List) {
+ for (Object value : (List) values) {
+ responseBer.encodeString((String) value, isLdapV3());
+ }
+ } else {
+ throw new DavMailException("EXCEPTION_UNSUPPORTED_VALUE", values);
+ }
+ responseBer.endSeq();
+ responseBer.endSeq();
+ }
+ responseBer.endSeq();
+ responseBer.endSeq();
+ responseBer.endSeq();
+ sendResponse();
+ }
+ }
+
+ protected void sendErr(int currentMessageId, int responseOperation, Exception e) throws IOException {
+ String message = e.getMessage();
+ if (message == null) {
+ message = e.toString();
+ }
+ sendClient(currentMessageId, responseOperation, LDAP_OTHER, message);
+ }
+
+ protected void sendClient(int currentMessageId, int responseOperation, int status, String message) throws IOException {
+ responseBer.reset();
+
+ responseBer.beginSeq(Ber.ASN_SEQUENCE | Ber.ASN_CONSTRUCTOR);
+ responseBer.encodeInt(currentMessageId);
+ responseBer.beginSeq(responseOperation);
+ responseBer.encodeInt(status, LBER_ENUMERATED);
+ // dn
+ responseBer.encodeString("", isLdapV3());
+ // error message
+ responseBer.encodeString(message, isLdapV3());
+ responseBer.endSeq();
+ responseBer.endSeq();
+ sendResponse();
+ }
+
+ protected void sendResponse() throws IOException {
+ //Ber.dumpBER(System.out, ">\n", responseBer.getBuf(), 0, responseBer.getDataLen());
+ os.write(responseBer.getBuf(), 0, responseBer.getDataLen());
+ os.flush();
+ }
+
+ static interface LdapFilter {
+ ExchangeSession.Condition getContactSearchFilter();
+
+ Map<String, ExchangeSession.Contact> findInGAL(ExchangeSession session, Set<String> returningAttributes, int sizeLimit) throws IOException;
+
+ void add(LdapFilter filter);
+
+ boolean isFullSearch();
+
+ boolean isMatch(Map<String, String> person);
+ }
+
+ class CompoundFilter implements LdapFilter {
+ final Set<LdapFilter> criteria = new HashSet<LdapFilter>();
+ final int type;
+
+ CompoundFilter(int filterType) {
+ type = filterType;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder buffer = new StringBuilder();
+
+ if (type == LDAP_FILTER_OR) {
+ buffer.append("(|");
+ } else if (type == LDAP_FILTER_AND) {
+ buffer.append("(&");
+ } else {
+ buffer.append("(!");
+ }
+
+ for (LdapFilter child : criteria) {
+ buffer.append(child.toString());
+ }
+
+ buffer.append(')');
+
+ return buffer.toString();
+ }
+
+ /**
+ * Add child filter
+ *
+ * @param filter inner filter
+ */
+ public void add(LdapFilter filter) {
+ criteria.add(filter);
+ }
+
+ /**
+ * This is only a full search if every child
+ * is also a full search
+ *
+ * @return true if full search filter
+ */
+ public boolean isFullSearch() {
+ for (LdapFilter child : criteria) {
+ if (!child.isFullSearch()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Build search filter for Contacts folder search.
+ * Use Exchange SEARCH syntax
+ *
+ * @return contact search filter
+ */
+ public ExchangeSession.Condition getContactSearchFilter() {
+ ExchangeSession.MultiCondition condition;
+
+ if (type == LDAP_FILTER_OR) {
+ condition = session.or();
+ } else {
+ condition = session.and();
+ }
+
+ for (LdapFilter child : criteria) {
+ condition.add(child.getContactSearchFilter());
+ }
+
+ return condition;
+ }
+
+ /**
+ * Test if person matches the current filter.
+ *
+ * @param person person attributes map
+ * @return true if filter match
+ */
+ public boolean isMatch(Map<String, String> person) {
+ if (type == LDAP_FILTER_OR) {
+ for (LdapFilter child : criteria) {
+ if (!child.isFullSearch()) {
+ if (child.isMatch(person)) {
+ // We've found a match
+ return true;
+ }
+ }
+ }
+
+ // No subconditions are met
+ return false;
+ } else if (type == LDAP_FILTER_AND) {
+ for (LdapFilter child : criteria) {
+ if (!child.isFullSearch()) {
+ if (!child.isMatch(person)) {
+ // We've found a miss
+ return false;
+ }
+ }
+ }
+
+ // All subconditions are met
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Find persons in Exchange GAL matching filter.
+ * Iterate over child filters to build results.
+ *
+ * @param session Exchange session
+ * @return persons map
+ * @throws IOException on error
+ */
+ public Map<String, ExchangeSession.Contact> findInGAL(ExchangeSession session, Set<String> returningAttributes, int sizeLimit) throws IOException {
+ Map<String, ExchangeSession.Contact> persons = null;
+
+ for (LdapFilter child : criteria) {
+ int currentSizeLimit = sizeLimit;
+ if (persons != null) {
+ currentSizeLimit -= persons.size();
+ }
+ Map<String, ExchangeSession.Contact> childFind = child.findInGAL(session, returningAttributes, currentSizeLimit);
+
+ if (childFind != null) {
+ if (persons == null) {
+ persons = childFind;
+ } else if (type == LDAP_FILTER_OR) {
+ // Create the union of the existing results and the child found results
+ persons.putAll(childFind);
+ } else if (type == LDAP_FILTER_AND) {
+ // Append current child filter results that match all child filters to persons.
+ // The hard part is that, due to the 100-item-returned galFind limit
+ // we may catch new items that match all child filters in each child search.
+ // Thus, instead of building the intersection, we check each result against
+ // all filters.
+
+ for (ExchangeSession.Contact result : childFind.values()) {
+ if (isMatch(result)) {
+ // This item from the child result set matches all sub-criteria, add it
+ persons.put(result.get("uid"), result);
+ }
+ }
+ }
+ }
+ }
+
+ if ((persons == null) && !isFullSearch()) {
+ // return an empty map (indicating no results were found)
+ return new HashMap<String, ExchangeSession.Contact>();
+ }
+
+ return persons;
+ }
+ }
+
+ class SimpleFilter implements LdapFilter {
+ static final String STAR = "*";
+ final String attributeName;
+ final String value;
+ final int mode;
+ final int operator;
+ final boolean canIgnore;
+
+ SimpleFilter(String attributeName) {
+ this.attributeName = attributeName;
+ this.value = SimpleFilter.STAR;
+ this.operator = LDAP_FILTER_SUBSTRINGS;
+ this.mode = 0;
+ this.canIgnore = checkIgnore();
+ }
+
+ SimpleFilter(String attributeName, String value, int ldapFilterOperator, int ldapFilterMode) {
+ this.attributeName = attributeName;
+ this.value = value;
+ this.operator = ldapFilterOperator;
+ this.mode = ldapFilterMode;
+ this.canIgnore = checkIgnore();
+ }
+
+ private boolean checkIgnore() {
+ if ("objectclass".equals(attributeName) && STAR.equals(value)) {
+ // ignore cases where any object class can match
+ return true;
+ } else if (IGNORE_MAP.contains(attributeName)) {
+ // Ignore this specific attribute
+ return true;
+ } else if (CRITERIA_MAP.get(attributeName) == null && getContactAttributeName(attributeName) == null) {
+ DavGatewayTray.debug(new BundleMessage("LOG_LDAP_UNSUPPORTED_FILTER_ATTRIBUTE",
+ attributeName, value));
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public boolean isFullSearch() {
+ // only (objectclass=*) is a full search
+ return "objectclass".equals(attributeName) && STAR.equals(value);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder buffer = new StringBuilder();
+ buffer.append('(');
+ buffer.append(attributeName);
+ buffer.append('=');
+ if (SimpleFilter.STAR.equals(value)) {
+ buffer.append(SimpleFilter.STAR);
+ } else if (operator == LDAP_FILTER_SUBSTRINGS) {
+ if (mode == LDAP_SUBSTRING_FINAL || mode == LDAP_SUBSTRING_ANY) {
+ buffer.append(SimpleFilter.STAR);
+ }
+ buffer.append(value);
+ if (mode == LDAP_SUBSTRING_INITIAL || mode == LDAP_SUBSTRING_ANY) {
+ buffer.append(SimpleFilter.STAR);
+ }
+ } else {
+ buffer.append(value);
+ }
+
+ buffer.append(')');
+ return buffer.toString();
+ }
+
+ public ExchangeSession.Condition getContactSearchFilter() {
+ String contactAttributeName = getContactAttributeName(attributeName);
+
+ if (canIgnore || (contactAttributeName == null)) {
+ return null;
+ }
+
+ ExchangeSession.Condition condition = null;
+
+ if (operator == LDAP_FILTER_EQUALITY) {
+ condition = session.isEqualTo(contactAttributeName, value);
+ } else if ("*".equals(value)) {
+ condition = session.not(session.isNull(contactAttributeName));
+ // do not allow substring search on integer field imapUid
+ } else if (!"imapUid".equals(contactAttributeName)) {
+ // endsWith not supported by exchange, convert to contains
+ if (mode == LDAP_SUBSTRING_FINAL || mode == LDAP_SUBSTRING_ANY) {
+ condition = session.contains(contactAttributeName, value);
+ } else {
+ condition = session.startsWith(contactAttributeName, value);
+ }
+ }
+ return condition;
+ }
+
+ public boolean isMatch(Map<String, String> person) {
+ if (canIgnore) {
+ // Ignore this filter
+ return true;
+ }
+
+ String personAttributeValue = person.get(attributeName);
+
+ if (personAttributeValue == null) {
+ // No value to allow for filter match
+ return false;
+ } else if (value == null) {
+ // This is a presence filter: found
+ return true;
+ } else if ((operator == LDAP_FILTER_EQUALITY) && personAttributeValue.equalsIgnoreCase(value)) {
+ // Found an exact match
+ return true;
+ } else if ((operator == LDAP_FILTER_SUBSTRINGS) && (personAttributeValue.toLowerCase().indexOf(value.toLowerCase()) >= 0)) {
+ // Found a substring match
+ return true;
+ }
+
+ return false;
+ }
+
+ public Map<String, ExchangeSession.Contact> findInGAL(ExchangeSession session, Set<String> returningAttributes, int sizeLimit) throws IOException {
+ if (canIgnore) {
+ return null;
+ }
+
+ String contactAttributeName = getContactAttributeName(attributeName);
+
+ if (contactAttributeName != null) {
+ // quick fix for cn=* filter
+ Map<String, ExchangeSession.Contact> galPersons = session.galFind(session.startsWith(contactAttributeName, "*".equals(value) ? "A" : value),
+ convertLdapToContactReturningAttributes(returningAttributes), sizeLimit);
+
+ if (operator == LDAP_FILTER_EQUALITY) {
+ // Make sure only exact matches are returned
+
+ Map<String, ExchangeSession.Contact> results = new HashMap<String, ExchangeSession.Contact>();
+
+ for (ExchangeSession.Contact person : galPersons.values()) {
+ if (isMatch(person)) {
+ // Found an exact match
+ results.put(person.get("uid"), person);
+ }
+ }
+
+ return results;
+ } else {
+ return galPersons;
+ }
+ }
+
+ return null;
+ }
+
+ public void add(LdapFilter filter) {
+ // Should never be called
+ DavGatewayTray.error(new BundleMessage("LOG_LDAP_UNSUPPORTED_FILTER", "nested simple filters"));
+ }
+ }
+
+ /**
+ * Convert contact attribute name to LDAP attribute name.
+ *
+ * @param ldapAttributeName ldap attribute name
+ * @return contact attribute name
+ */
+ protected static String getContactAttributeName(String ldapAttributeName) {
+ String contactAttributeName = null;
+ // first look in contact attributes
+ if (ExchangeSession.CONTACT_ATTRIBUTES.contains(ldapAttributeName)) {
+ contactAttributeName = ldapAttributeName;
+ } else if (LDAP_TO_CONTACT_ATTRIBUTE_MAP.containsKey(ldapAttributeName)) {
+ String mappedAttribute = LDAP_TO_CONTACT_ATTRIBUTE_MAP.get(ldapAttributeName);
+ if (mappedAttribute != null) {
+ contactAttributeName = mappedAttribute;
+ }
+ } else {
+ DavGatewayTray.debug(new BundleMessage("UNKNOWN_ATTRIBUTE", ldapAttributeName));
+ }
+ return contactAttributeName;
+ }
+
+ /**
+ * Convert LDAP attribute name to contact attribute name.
+ *
+ * @param contactAttributeName ldap attribute name
+ * @return contact attribute name
+ */
+ protected static String getLdapAttributeName(String contactAttributeName) {
+ String mappedAttributeName = CONTACT_TO_LDAP_ATTRIBUTE_MAP.get(contactAttributeName);
+ if (mappedAttributeName != null) {
+ return mappedAttributeName;
+ } else {
+ return contactAttributeName;
+ }
+ }
+
+ protected Set<String> convertLdapToContactReturningAttributes(Set<String> returningAttributes) {
+ Set<String> contactReturningAttributes;
+ if (returningAttributes != null && !returningAttributes.isEmpty()) {
+ contactReturningAttributes = new HashSet<String>();
+ // always return uid
+ contactReturningAttributes.add("imapUid");
+ for (String attribute : returningAttributes) {
+ String contactAttributeName = getContactAttributeName(attribute);
+ if (contactAttributeName != null) {
+ contactReturningAttributes.add(contactAttributeName);
+ }
+ }
+ } else {
+ contactReturningAttributes = ExchangeSession.CONTACT_ATTRIBUTES;
+ }
+ return contactReturningAttributes;
+ }
+
+ protected class SearchRunnable implements Runnable {
+ private final int currentMessageId;
+ private final String dn;
+ private final int scope;
+ private final int sizeLimit;
+ private final int timelimit;
+ private final LdapFilter ldapFilter;
+ private final Set<String> returningAttributes;
+ private boolean abandon;
+
+ protected SearchRunnable(int currentMessageId, String dn, int scope, int sizeLimit, int timelimit, LdapFilter ldapFilter, Set<String> returningAttributes) {
+ this.currentMessageId = currentMessageId;
+ this.dn = dn;
+ this.scope = scope;
+ this.sizeLimit = sizeLimit;
+ this.timelimit = timelimit;
+ this.ldapFilter = ldapFilter;
+ this.returningAttributes = returningAttributes;
+ }
+
+ /**
+ * Abandon search.
+ */
+ protected void abandon() {
+ abandon = true;
+ }
+
+ public void run() {
+ try {
+ int size = 0;
+ DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_SEARCH", currentMessageId, dn, scope, sizeLimit, timelimit, ldapFilter.toString(), returningAttributes));
+
+ if (scope == SCOPE_BASE_OBJECT) {
+ if ("".equals(dn)) {
+ size = 1;
+ sendRootDSE(currentMessageId);
+ } else if (BASE_CONTEXT.equals(dn)) {
+ size = 1;
+ // root
+ sendBaseContext(currentMessageId);
+ } else if (dn.startsWith("uid=") && dn.indexOf(',') > 0) {
+ if (session != null) {
+ // single user request
+ String uid = dn.substring("uid=".length(), dn.indexOf(','));
+ Map<String, ExchangeSession.Contact> persons = null;
+
+ // first search in contact
+ try {
+ // check if this is a contact uid
+ Integer.parseInt(uid);
+ persons = contactFind(session.isEqualTo("imapUid", uid), returningAttributes, sizeLimit);
+ } catch (NumberFormatException e) {
+ // ignore, this is not a contact uid
+ }
+
+ // then in GAL
+ if (persons == null || persons.isEmpty()) {
+ persons = session.galFind(session.isEqualTo("imapUid", uid),
+ convertLdapToContactReturningAttributes(returningAttributes), sizeLimit);
+
+ ExchangeSession.Contact person = persons.get(uid.toLowerCase());
+ // filter out non exact results
+ if (persons.size() > 1 || person == null) {
+ persons = new HashMap<String, ExchangeSession.Contact>();
+ if (person != null) {
+ persons.put(uid.toLowerCase(), person);
+ }
+ }
+ }
+ size = persons.size();
+ sendPersons(currentMessageId, dn.substring(dn.indexOf(',')), persons, returningAttributes);
+ } else {
+ DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_SEARCH_ANONYMOUS_ACCESS_FORBIDDEN", currentMessageId, dn));
+ }
+ } else {
+ DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_SEARCH_INVALID_DN", currentMessageId, dn));
+ }
+ } else if (COMPUTER_CONTEXT.equals(dn) || COMPUTER_CONTEXT_LION.equals(dn)) {
+ size = 1;
+ // computer context for iCal
+ sendComputerContext(currentMessageId, returningAttributes);
+ } else if ((BASE_CONTEXT.equalsIgnoreCase(dn) || OD_USER_CONTEXT.equalsIgnoreCase(dn)) || OD_USER_CONTEXT_LION.equalsIgnoreCase(dn)) {
+ if (session != null) {
+ Map<String, ExchangeSession.Contact> persons = new HashMap<String, ExchangeSession.Contact>();
+ if (ldapFilter.isFullSearch()) {
+ // append personal contacts first
+ for (ExchangeSession.Contact person : contactFind(null, returningAttributes, sizeLimit).values()) {
+ persons.put(person.get("imapUid"), person);
+ if (persons.size() == sizeLimit) {
+ break;
+ }
+ }
+ // full search
+ for (char c = 'A'; c <= 'Z'; c++) {
+ if (!abandon && persons.size() < sizeLimit) {
+ for (ExchangeSession.Contact person : session.galFind(session.startsWith("cn", String.valueOf(c)),
+ convertLdapToContactReturningAttributes(returningAttributes), sizeLimit).values()) {
+ persons.put(person.get("uid"), person);
+ if (persons.size() == sizeLimit) {
+ break;
+ }
+ }
+ }
+ if (persons.size() == sizeLimit) {
+ break;
+ }
+ }
+ } else {
+ // append personal contacts first
+ ExchangeSession.Condition filter = ldapFilter.getContactSearchFilter();
+
+ // if ldapfilter is not a full search and filter is null,
+ // ignored all attribute filters => return empty results
+ if (ldapFilter.isFullSearch() || filter != null) {
+ for (ExchangeSession.Contact person : contactFind(filter, returningAttributes, sizeLimit).values()) {
+ persons.put(person.get("imapUid"), person);
+
+ if (persons.size() == sizeLimit) {
+ break;
+ }
+ }
+ if (!abandon && persons.size() < sizeLimit) {
+ for (ExchangeSession.Contact person : ldapFilter.findInGAL(session, returningAttributes, sizeLimit - persons.size()).values()) {
+ if (persons.size() == sizeLimit) {
+ break;
+ }
+
+ persons.put(person.get("uid"), person);
+ }
+ }
+ }
+ }
+
+ size = persons.size();
+ DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_SEARCH_FOUND_RESULTS", currentMessageId, size));
+ sendPersons(currentMessageId, ", " + dn, persons, returningAttributes);
+ DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_SEARCH_END", currentMessageId));
+ } else {
+ DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_SEARCH_ANONYMOUS_ACCESS_FORBIDDEN", currentMessageId, dn));
+ }
+ } else if (dn != null && dn.length() > 0 && !OD_CONFIG_CONTEXT.equals(dn) && !OD_GROUP_CONTEXT.equals(dn)) {
+ DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_SEARCH_INVALID_DN", currentMessageId, dn));
+ }
+
+ // iCal: do not send LDAP_SIZE_LIMIT_EXCEEDED on apple-computer search by cn with sizelimit 1
+ if (size > 1 && size == sizeLimit) {
+ DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_SEARCH_SIZE_LIMIT_EXCEEDED", currentMessageId));
+ sendClient(currentMessageId, LDAP_REP_RESULT, LDAP_SIZE_LIMIT_EXCEEDED, "");
+ } else {
+ DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_SEARCH_SUCCESS", currentMessageId));
+ sendClient(currentMessageId, LDAP_REP_RESULT, LDAP_SUCCESS, "");
+ }
+ } catch (SocketException e) {
+ // client closed connection
+ LOGGER.warn(BundleMessage.formatLog("LOG_CLIENT_CLOSED_CONNECTION"));
+ } catch (IOException e) {
+ DavGatewayTray.log(e);
+ try {
+ sendErr(currentMessageId, LDAP_REP_RESULT, e);
+ } catch (IOException e2) {
+ DavGatewayTray.debug(new BundleMessage("LOG_EXCEPTION_SENDING_ERROR_TO_CLIENT"), e2);
+ }
+ } finally {
+ synchronized (searchThreadMap) {
+ searchThreadMap.remove(currentMessageId);
+ }
+ }
+
+ }
+
+ /**
+ * Search users in contacts folder
+ *
+ * @param condition search filter
+ * @param returningAttributes requested attributes
+ * @param maxCount maximum item count
+ * @return List of users
+ * @throws IOException on error
+ */
+ public Map<String, ExchangeSession.Contact> contactFind(ExchangeSession.Condition condition, Set<String> returningAttributes, int maxCount) throws IOException {
+ Map<String, ExchangeSession.Contact> results = new HashMap<String, ExchangeSession.Contact>();
+
+ Set<String> contactReturningAttributes = convertLdapToContactReturningAttributes(returningAttributes);
+ contactReturningAttributes.remove("apple-serviceslocator");
+ List<ExchangeSession.Contact> contacts = session.searchContacts(ExchangeSession.CONTACTS, contactReturningAttributes, condition, maxCount);
+
+ for (ExchangeSession.Contact contact : contacts) {
+ // use imapUid as uid
+ String imapUid = contact.get("imapUid");
+ if (imapUid != null) {
+ results.put(imapUid, contact);
+ }
+ }
+
+ return results;
+ }
+
+
+ /**
+ * Convert to LDAP attributes and send entry
+ *
+ * @param currentMessageId current Message Id
+ * @param baseContext request base context (BASE_CONTEXT or OD_BASE_CONTEXT)
+ * @param persons persons Map
+ * @param returningAttributes returning attributes
+ * @throws IOException on error
+ */
+ protected void sendPersons(int currentMessageId, String baseContext, Map<String, ExchangeSession.Contact> persons, Set<String> returningAttributes) throws IOException {
+ boolean needObjectClasses = returningAttributes.contains("objectclass") || returningAttributes.isEmpty();
+ boolean returnAllAttributes = returningAttributes.isEmpty();
+
+ for (ExchangeSession.Contact person : persons.values()) {
+ if (abandon) {
+ break;
+ }
+
+ Map<String, Object> ldapPerson = new HashMap<String, Object>();
+
+ // convert Contact entries
+ if (returnAllAttributes) {
+ // just convert contact attributes to default ldap names
+ for (Map.Entry<String, String> entry : person.entrySet()) {
+ String ldapAttribute = getLdapAttributeName(entry.getKey());
+ String value = entry.getValue();
+ if (value != null) {
+ ldapPerson.put(ldapAttribute, value);
+ }
+ }
+ } else {
+ // always map uid
+ ldapPerson.put("uid", person.get("imapUid"));
+ // iterate over requested attributes
+ for (String ldapAttribute : returningAttributes) {
+ String contactAttribute = getContactAttributeName(ldapAttribute);
+ String value = person.get(contactAttribute);
+ if (value != null) {
+ if (ldapAttribute.startsWith("birth")) {
+ SimpleDateFormat parser = ExchangeSession.getZuluDateFormat();
+ Calendar calendar = Calendar.getInstance();
+ try {
+ calendar.setTime(parser.parse(value));
+ } catch (ParseException e) {
+ throw new IOException(e + " " + e.getMessage());
+ }
+ if ("birthday".equals(ldapAttribute)) {
+ value = String.valueOf(calendar.get(Calendar.DAY_OF_MONTH));
+ } else if ("birthmonth".equals(ldapAttribute)) {
+ value = String.valueOf(calendar.get(Calendar.MONTH) + 1);
+ } else if ("birthyear".equals(ldapAttribute)) {
+ value = String.valueOf(calendar.get(Calendar.YEAR));
+ }
+ }
+ ldapPerson.put(ldapAttribute, value);
+ }
+ }
+ }
+
+ // Process all attributes which have static mappings
+ for (Map.Entry<String, String> entry : STATIC_ATTRIBUTE_MAP.entrySet()) {
+ String ldapAttribute = entry.getKey();
+ String value = entry.getValue();
+
+ if (value != null
+ && (returnAllAttributes || returningAttributes.contains(ldapAttribute))) {
+ ldapPerson.put(ldapAttribute, value);
+ }
+ }
+
+ if (needObjectClasses) {
+ ldapPerson.put("objectClass", PERSON_OBJECT_CLASSES);
+ }
+
+ // iCal: copy email to apple-generateduid, encode @
+ if (returnAllAttributes || returningAttributes.contains("apple-generateduid")) {
+ String mail = (String) ldapPerson.get("mail");
+ if (mail != null) {
+ ldapPerson.put("apple-generateduid", mail.replaceAll("@", "__AT__"));
+ } else {
+ // failover, should not happen
+ ldapPerson.put("apple-generateduid", ldapPerson.get("uid"));
+ }
+ }
+
+ // iCal: replace current user alias with login name
+ if (session.getAlias().equals(ldapPerson.get("uid"))) {
+ if (returningAttributes.contains("uidnumber")) {
+ ldapPerson.put("uidnumber", userName);
+ }
+ }
+ DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_SEARCH_SEND_PERSON", currentMessageId, ldapPerson.get("uid"), baseContext, ldapPerson));
+ sendEntry(currentMessageId, "uid=" + ldapPerson.get("uid") + baseContext, ldapPerson);
+ }
+
+ }
+
+ }
+}
diff --git a/src/java/davmail/ldap/LdapServer.java b/src/java/davmail/ldap/LdapServer.java
new file mode 100644
index 0000000..d562712
--- /dev/null
+++ b/src/java/davmail/ldap/LdapServer.java
@@ -0,0 +1,56 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.ldap;
+
+import davmail.AbstractConnection;
+import davmail.AbstractServer;
+import davmail.Settings;
+
+import java.net.Socket;
+
+/**
+ * LDAP server, handle LDAP directory requests.
+ */
+public class LdapServer extends AbstractServer {
+ /**
+ * Default LDAP port
+ */
+ public static final int DEFAULT_PORT = 389;
+
+ /**
+ * Create a ServerSocket to listen for connections.
+ * Start the thread.
+ *
+ * @param port pop listen port, 389 if not defined (0)
+ */
+ public LdapServer(int port) {
+ super(LdapServer.class.getName(), port, LdapServer.DEFAULT_PORT);
+ nosslFlag = Settings.getBooleanProperty("davmail.ssl.nosecureldap");
+ }
+
+ @Override
+ public String getProtocolName() {
+ return "LDAP";
+ }
+
+ @Override
+ public AbstractConnection createConnectionHandler(Socket clientSocket) {
+ return new LdapConnection(clientSocket);
+ }
+}
\ No newline at end of file
diff --git a/src/java/davmail/pop/PopConnection.java b/src/java/davmail/pop/PopConnection.java
new file mode 100644
index 0000000..2792694
--- /dev/null
+++ b/src/java/davmail/pop/PopConnection.java
@@ -0,0 +1,354 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.pop;
+
+import davmail.AbstractConnection;
+import davmail.BundleMessage;
+import davmail.DavGateway;
+import davmail.Settings;
+import davmail.exchange.DoubleDotOutputStream;
+import davmail.exchange.ExchangeSession;
+import davmail.exchange.ExchangeSessionFactory;
+import davmail.ui.tray.DavGatewayTray;
+import davmail.util.IOUtil;
+import org.apache.log4j.Logger;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.net.SocketException;
+import java.util.Date;
+import java.util.List;
+import java.util.StringTokenizer;
+
+/**
+ * Dav Gateway pop connection implementation
+ */
+public class PopConnection extends AbstractConnection {
+ private static final Logger LOGGER = Logger.getLogger(PopConnection.class);
+
+ private List<ExchangeSession.Message> messages;
+
+ /**
+ * Initialize the streams and start the thread.
+ *
+ * @param clientSocket POP client socket
+ */
+ public PopConnection(Socket clientSocket) {
+ super(PopConnection.class.getSimpleName(), clientSocket, null);
+ }
+
+ protected long getTotalMessagesLength() {
+ int result = 0;
+ for (ExchangeSession.Message message : messages) {
+ result += message.size;
+ }
+ return result;
+ }
+
+ protected void printCapabilities() throws IOException {
+ sendClient("TOP");
+ sendClient("USER");
+ sendClient("UIDL");
+ sendClient(".");
+ }
+
+ protected void printList() throws IOException {
+ int i = 1;
+ for (ExchangeSession.Message message : messages) {
+ sendClient(i++ + " " + message.size);
+ }
+ sendClient(".");
+ }
+
+ protected void printUidList() throws IOException {
+ int i = 1;
+ for (ExchangeSession.Message message : messages) {
+ sendClient(i++ + " " + message.getUid());
+ }
+ sendClient(".");
+ }
+
+
+ @Override
+ public void run() {
+ String line;
+ StringTokenizer tokens;
+
+ try {
+ ExchangeSessionFactory.checkConfig();
+ sendOK("DavMail " + DavGateway.getCurrentVersion() + " POP ready at " + new Date());
+
+ for (; ;) {
+ line = readClient();
+ // unable to read line, connection closed ?
+ if (line == null) {
+ break;
+ }
+
+ tokens = new StringTokenizer(line);
+ if (tokens.hasMoreTokens()) {
+ String command = tokens.nextToken();
+
+ if ("QUIT".equalsIgnoreCase(command)) {
+ // delete messages before quit
+ if (session != null) {
+ session.purgeOldestTrashAndSentMessages();
+ }
+ sendOK("Bye");
+ break;
+ } else if ("USER".equalsIgnoreCase(command)) {
+ userName = null;
+ password = null;
+ session = null;
+ if (tokens.hasMoreTokens()) {
+ userName = line.substring("USER ".length());
+ sendOK("USER : " + userName);
+ state = State.USER;
+ } else {
+ sendERR("invalid syntax");
+ state = State.INITIAL;
+ }
+ } else if ("PASS".equalsIgnoreCase(command)) {
+ if (state != State.USER) {
+ sendERR("invalid state");
+ state = State.INITIAL;
+ } else if (!tokens.hasMoreTokens()) {
+ sendERR("invalid syntax");
+ } else {
+ // bug 2194492 : allow space in password
+ password = line.substring("PASS".length() + 1);
+ try {
+ session = ExchangeSessionFactory.getInstance(userName, password);
+ sendOK("PASS");
+ state = State.AUTHENTICATED;
+ } catch (SocketException e) {
+ // can not send error to client after a socket exception
+ LOGGER.warn(BundleMessage.formatLog("LOG_CLIENT_CLOSED_CONNECTION"));
+ } catch (Exception e) {
+ DavGatewayTray.error(e);
+ sendERR(e);
+ }
+ }
+ } else if ("CAPA".equalsIgnoreCase(command)) {
+ sendOK("Capability list follows");
+ printCapabilities();
+ } else if (state != State.AUTHENTICATED) {
+ sendERR("Invalid state not authenticated");
+ } else {
+ // load messages (once)
+ if (messages == null) {
+ messages = session.getAllMessageUidAndSize("INBOX");
+ }
+ if ("STAT".equalsIgnoreCase(command)) {
+ sendOK(messages.size() + " " +
+ getTotalMessagesLength());
+ } else if ("NOOP".equalsIgnoreCase(command)) {
+ sendOK("");
+ } else if ("LIST".equalsIgnoreCase(command)) {
+ if (tokens.hasMoreTokens()) {
+ String token = tokens.nextToken();
+ try {
+ int messageNumber = Integer.valueOf(token);
+ ExchangeSession.Message message = messages.get(messageNumber - 1);
+ sendOK("" + messageNumber + ' ' + message.size);
+ } catch (NumberFormatException e) {
+ sendERR("Invalid message index: " + token);
+ } catch (IndexOutOfBoundsException e) {
+ sendERR("Invalid message index: " + token);
+ }
+ } else {
+ sendOK(messages.size() +
+ " messages (" + getTotalMessagesLength() +
+ " octets)");
+ printList();
+ }
+ } else if ("UIDL".equalsIgnoreCase(command)) {
+ if (tokens.hasMoreTokens()) {
+ String token = tokens.nextToken();
+ try {
+ int messageNumber = Integer.valueOf(token);
+ sendOK(messageNumber + " " + messages.get(messageNumber - 1).getUid());
+ } catch (NumberFormatException e) {
+ sendERR("Invalid message index: " + token);
+ } catch (IndexOutOfBoundsException e) {
+ sendERR("Invalid message index: " + token);
+ }
+ } else {
+ sendOK(messages.size() +
+ " messages (" + getTotalMessagesLength() +
+ " octets)");
+ printUidList();
+ }
+ } else if ("RETR".equalsIgnoreCase(command)) {
+ if (tokens.hasMoreTokens()) {
+ try {
+ int messageNumber = Integer.valueOf(tokens.nextToken()) - 1;
+ sendOK("");
+ DoubleDotOutputStream doubleDotOutputStream = new DoubleDotOutputStream(os);
+ ExchangeSession.Message message = messages.get(messageNumber);
+ IOUtil.write(message.getRawInputStream(), doubleDotOutputStream);
+ doubleDotOutputStream.close();
+ if (Settings.getBooleanProperty("davmail.popMarkReadOnRetr")) {
+ message.markRead();
+ }
+ } catch (SocketException e) {
+ // can not send error to client after a socket exception
+ LOGGER.warn(BundleMessage.formatLog("LOG_CLIENT_CLOSED_CONNECTION"));
+ } catch (Exception e) {
+ DavGatewayTray.error(new BundleMessage("LOG_ERROR_RETRIEVING_MESSAGE"), e);
+ sendERR("error retrieving message " + e + ' ' + e.getMessage());
+ }
+ } else {
+ sendERR("invalid message index");
+ }
+ } else if ("DELE".equalsIgnoreCase(command)) {
+ if (tokens.hasMoreTokens()) {
+ ExchangeSession.Message message;
+ try {
+ int messageNumber = Integer.valueOf(tokens.
+ nextToken()) - 1;
+ message = messages.get(messageNumber);
+ message.moveToTrash();
+ sendOK("DELETE");
+ } catch (NumberFormatException e) {
+ sendERR("invalid message index");
+ } catch (IndexOutOfBoundsException e) {
+ sendERR("invalid message index");
+ }
+ } else {
+ sendERR("invalid message index");
+ }
+ } else if ("TOP".equalsIgnoreCase(command)) {
+ int message = 0;
+ try {
+ message = Integer.valueOf(tokens.nextToken());
+ int lines = Integer.valueOf(tokens.nextToken());
+ ExchangeSession.Message m = messages.get(message - 1);
+ sendOK("");
+ DoubleDotOutputStream doubleDotOutputStream = new DoubleDotOutputStream(os);
+ IOUtil.write(m.getRawInputStream(), new TopOutputStream(doubleDotOutputStream, lines));
+ doubleDotOutputStream.close();
+ } catch (NumberFormatException e) {
+ sendERR("invalid command");
+ } catch (IndexOutOfBoundsException e) {
+ sendERR("invalid message index: " + message);
+ } catch (Exception e) {
+ sendERR("error retreiving top of messages");
+ DavGatewayTray.error(e);
+ }
+ } else if ("RSET".equalsIgnoreCase(command)) {
+ sendOK("RSET");
+ } else {
+ sendERR("unknown command");
+ }
+ }
+
+ } else {
+ sendERR("unknown command");
+ }
+
+ os.flush();
+ }
+ } catch (SocketException e) {
+ DavGatewayTray.debug(new BundleMessage("LOG_CONNECTION_CLOSED"));
+ } catch (Exception e) {
+ DavGatewayTray.log(e);
+ try {
+ sendERR(e.getMessage());
+ } catch (IOException e2) {
+ DavGatewayTray.debug(new BundleMessage("LOG_EXCEPTION_SENDING_ERROR_TO_CLIENT"), e2);
+ }
+ } finally {
+ close();
+ }
+ DavGatewayTray.resetIcon();
+ }
+
+ protected void sendOK(String message) throws IOException {
+ sendClient("+OK ", message);
+ }
+
+ protected void sendERR(Exception e) throws IOException {
+ String message = e.getMessage();
+ if (message == null) {
+ message = e.toString();
+ }
+ sendERR(message);
+ }
+
+ protected void sendERR(String message) throws IOException {
+ sendClient("-ERR ", message.replaceAll("\\n", " "));
+ }
+
+ /**
+ * Filter to limit output lines to max body lines after header
+ */
+ private static final class TopOutputStream extends FilterOutputStream {
+ private static final int START = 0;
+ private static final int CR = 1;
+ private static final int CRLF = 2;
+ private static final int CRLFCR = 3;
+ private static final int BODY = 4;
+
+ private int maxLines;
+ private int state = START;
+
+ private TopOutputStream(OutputStream os, int maxLines) {
+ super(os);
+ this.maxLines = maxLines;
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ if (state != BODY || maxLines > 0) {
+ super.write(b);
+ }
+ if (state == BODY) {
+ if (b == '\n') {
+ maxLines--;
+ }
+ } else if (state == START) {
+ if (b == '\r') {
+ state = CR;
+ }
+ } else if (state == CR) {
+ if (b == '\n') {
+ state = CRLF;
+ } else {
+ state = START;
+ }
+ } else if (state == CRLF) {
+ if (b == '\r') {
+ state = CRLFCR;
+ } else {
+ state = START;
+ }
+ } else if (state == CRLFCR) {
+ if (b == '\n') {
+ state = BODY;
+ } else {
+ state = START;
+ }
+ }
+ }
+ }
+
+}
diff --git a/src/java/davmail/pop/PopServer.java b/src/java/davmail/pop/PopServer.java
new file mode 100644
index 0000000..7fa9cdd
--- /dev/null
+++ b/src/java/davmail/pop/PopServer.java
@@ -0,0 +1,58 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.pop;
+
+
+import davmail.AbstractConnection;
+import davmail.AbstractServer;
+import davmail.Settings;
+
+import java.net.Socket;
+
+/**
+ * Pop3 server
+ */
+public class PopServer extends AbstractServer {
+ /**
+ * Default POP port
+ */
+ public static final int DEFAULT_PORT = 110;
+
+ /**
+ * Create a ServerSocket to listen for connections.
+ * Start the thread.
+ *
+ * @param port pop listen port, 110 if not defined (0)
+ */
+ public PopServer(int port) {
+ super(PopServer.class.getName(), port, PopServer.DEFAULT_PORT);
+ nosslFlag = Settings.getBooleanProperty("davmail.ssl.nosecurepop");
+ }
+
+ @Override
+ public String getProtocolName() {
+ return "POP";
+ }
+
+ @Override
+ public AbstractConnection createConnectionHandler(Socket clientSocket) {
+ return new PopConnection(clientSocket);
+ }
+
+}
diff --git a/src/java/davmail/service/DavService.java b/src/java/davmail/service/DavService.java
new file mode 100644
index 0000000..6cd64e9
--- /dev/null
+++ b/src/java/davmail/service/DavService.java
@@ -0,0 +1,71 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.service;
+
+import davmail.BundleMessage;
+import davmail.DavGateway;
+import davmail.Settings;
+import davmail.ui.tray.DavGatewayTray;
+import org.boris.winrun4j.AbstractService;
+import org.boris.winrun4j.ServiceException;
+
+/**
+ * WinRun4J DavMail service.
+ */
+public class DavService extends AbstractService {
+
+ /**
+ * Perform a service request.
+ *
+ * @param control service control.
+ * @return return code.
+ * @throws ServiceException on error.
+ */
+ @Override
+ public int serviceRequest(int control) throws ServiceException {
+ switch (control) {
+ case SERVICE_CONTROL_STOP:
+ case SERVICE_CONTROL_SHUTDOWN:
+ DavGatewayTray.debug(new BundleMessage("LOG_STOPPING_DAVMAIL"));
+ DavGateway.stop();
+ }
+ return 0;
+ }
+
+ /**
+ * Run the service.
+ *
+ * @param args command line arguments
+ * @return return code
+ * @throws ServiceException on error
+ */
+ public int serviceMain(String[] args) throws ServiceException {
+ if (args.length >= 1) {
+ Settings.setConfigFilePath(args[0]);
+ }
+
+ Settings.load();
+ DavGatewayTray.init();
+
+ DavGateway.start();
+ DavGatewayTray.debug(new BundleMessage("LOG_DAVMAIL_STARTED"));
+
+ return 0;
+ }
+}
diff --git a/src/java/davmail/smtp/SmtpConnection.java b/src/java/davmail/smtp/SmtpConnection.java
new file mode 100644
index 0000000..3d3fc4d
--- /dev/null
+++ b/src/java/davmail/smtp/SmtpConnection.java
@@ -0,0 +1,262 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.smtp;
+
+import davmail.AbstractConnection;
+import davmail.BundleMessage;
+import davmail.DavGateway;
+import davmail.exception.DavMailException;
+import davmail.exchange.DoubleDotInputStream;
+import davmail.exchange.ExchangeSessionFactory;
+import davmail.ui.tray.DavGatewayTray;
+
+import javax.mail.internet.AddressException;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeMessage;
+import javax.mail.util.SharedByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.Socket;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.StringTokenizer;
+
+/**
+ * Dav Gateway smtp connection implementation
+ */
+public class SmtpConnection extends AbstractConnection {
+
+ /**
+ * Initialize the streams and start the thread.
+ *
+ * @param clientSocket SMTP client socket
+ */
+ public SmtpConnection(Socket clientSocket) {
+ super(SmtpConnection.class.getSimpleName(), clientSocket, null);
+ }
+
+
+ @Override
+ public void run() {
+ String line;
+ StringTokenizer tokens;
+ List<String> recipients = new ArrayList<String>();
+
+ try {
+ ExchangeSessionFactory.checkConfig();
+ sendClient("220 DavMail " + DavGateway.getCurrentVersion() + " SMTP ready at " + new Date());
+ for (; ;) {
+ line = readClient();
+ // unable to read line, connection closed ?
+ if (line == null) {
+ break;
+ }
+
+ tokens = new StringTokenizer(line);
+ if (tokens.hasMoreTokens()) {
+ String command = tokens.nextToken();
+
+ if (state == State.LOGIN) {
+ // AUTH LOGIN, read userName
+ userName = base64Decode(line);
+ sendClient("334 " + base64Encode("Password:"));
+ state = State.PASSWORD;
+ } else if (state == State.PASSWORD) {
+ // AUTH LOGIN, read password
+ password = base64Decode(line);
+ authenticate();
+ } else if ("QUIT".equalsIgnoreCase(command)) {
+ sendClient("221 Closing connection");
+ break;
+ } else if ("NOOP".equalsIgnoreCase(command)) {
+ sendClient("250 OK");
+ } else if ("EHLO".equalsIgnoreCase(command)) {
+ sendClient("250-" + tokens.nextToken());
+ // inform server that AUTH is supported
+ // actually it is mandatory (only way to get credentials)
+ sendClient("250-AUTH LOGIN PLAIN");
+ sendClient("250-8BITMIME");
+ sendClient("250 Hello");
+ } else if ("HELO".equalsIgnoreCase(command)) {
+ sendClient("250 Hello");
+ } else if ("AUTH".equalsIgnoreCase(command)) {
+ if (tokens.hasMoreElements()) {
+ String authType = tokens.nextToken();
+ if ("PLAIN".equalsIgnoreCase(authType) && tokens.hasMoreElements()) {
+ decodeCredentials(tokens.nextToken());
+ authenticate();
+ } else if ("LOGIN".equalsIgnoreCase(authType)) {
+ if (tokens.hasMoreTokens()) {
+ // user name sent on auth line
+ userName = base64Decode(tokens.nextToken());
+ sendClient("334 " + base64Encode("Password:"));
+ state = State.PASSWORD;
+ } else {
+ sendClient("334 " + base64Encode("Username:"));
+ state = State.LOGIN;
+ }
+ } else {
+ sendClient("451 Error : unknown authentication type");
+ }
+ } else {
+ sendClient("451 Error : authentication type not specified");
+ }
+ } else if ("MAIL".equalsIgnoreCase(command)) {
+ if (state == State.AUTHENTICATED) {
+ state = State.STARTMAIL;
+ recipients.clear();
+ sendClient("250 Sender OK");
+ } else if (state == State.INITIAL) {
+ sendClient("503 Authentication required");
+ } else {
+ state = State.INITIAL;
+ sendClient("503 Bad sequence of commands");
+ }
+ } else if ("RCPT".equalsIgnoreCase(command)) {
+ if (state == State.STARTMAIL || state == State.RECIPIENT) {
+ if (line.toUpperCase().startsWith("RCPT TO:")) {
+ state = State.RECIPIENT;
+ try {
+ InternetAddress internetAddress = new InternetAddress(line.substring("RCPT TO:".length()));
+ recipients.add(internetAddress.getAddress());
+ } catch (AddressException e) {
+ throw new DavMailException("EXCEPTION_INVALID_RECIPIENT", line);
+ }
+ sendClient("250 Recipient OK");
+ } else {
+ sendClient("500 Unrecognized command");
+ }
+
+ } else {
+ state = State.AUTHENTICATED;
+ sendClient("503 Bad sequence of commands");
+ }
+ } else if ("DATA".equalsIgnoreCase(command)) {
+ if (state == State.RECIPIENT) {
+ state = State.MAILDATA;
+ sendClient("354 Start mail input; end with <CRLF>.<CRLF>");
+
+ try {
+ // read message in buffer
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ DoubleDotInputStream doubleDotInputStream = new DoubleDotInputStream(in);
+ int b;
+ while ((b = doubleDotInputStream.read()) >= 0) {
+ baos.write(b);
+ }
+ MimeMessage mimeMessage = new MimeMessage(null, new SharedByteArrayInputStream(baos.toByteArray()));
+ session.sendMessage(recipients, mimeMessage);
+ state = State.AUTHENTICATED;
+ sendClient("250 Queued mail for delivery");
+ } catch (Exception e) {
+ DavGatewayTray.error(e);
+ state = State.AUTHENTICATED;
+ sendClient("451 Error : " + e + ' ' + e.getMessage());
+ }
+
+ } else {
+ state = State.AUTHENTICATED;
+ sendClient("503 Bad sequence of commands");
+ }
+ } else if ("RSET".equalsIgnoreCase(command)) {
+ recipients.clear();
+
+ if (state == State.STARTMAIL ||
+ state == State.RECIPIENT ||
+ state == State.MAILDATA ||
+ state == State.AUTHENTICATED) {
+ state = State.AUTHENTICATED;
+ } else {
+ state = State.INITIAL;
+ }
+ sendClient("250 OK Reset");
+ } else {
+ sendClient("500 Unrecognized command");
+ }
+ } else {
+ sendClient("500 Unrecognized command");
+ }
+
+ os.flush();
+ }
+
+ } catch (SocketException e) {
+ DavGatewayTray.debug(new BundleMessage("LOG_CONNECTION_CLOSED"));
+ } catch (Exception e) {
+ DavGatewayTray.log(e);
+ try {
+ sendClient("500 " + ((e.getMessage() == null) ? e : e.getMessage()));
+ } catch (IOException e2) {
+ DavGatewayTray.debug(new BundleMessage("LOG_EXCEPTION_SENDING_ERROR_TO_CLIENT"), e2);
+ }
+ } finally {
+ close();
+ }
+ DavGatewayTray.resetIcon();
+ }
+
+ /**
+ * Create authenticated session with Exchange server
+ *
+ * @throws IOException on error
+ */
+ protected void authenticate() throws IOException {
+ try {
+ session = ExchangeSessionFactory.getInstance(userName, password);
+ sendClient("235 OK Authenticated");
+ state = State.AUTHENTICATED;
+ } catch (Exception e) {
+ DavGatewayTray.error(e);
+ String message = e.getMessage();
+ if (message == null) {
+ message = e.toString();
+ }
+ message = message.replaceAll("\\n", " ");
+ sendClient("554 Authenticated failed " + message);
+ state = State.INITIAL;
+ }
+
+ }
+
+ /**
+ * Decode SMTP credentials
+ *
+ * @param encodedCredentials smtp encoded credentials
+ * @throws IOException if invalid credentials
+ */
+ protected void decodeCredentials(String encodedCredentials) throws IOException {
+ String decodedCredentials = base64Decode(encodedCredentials);
+ int startIndex = decodedCredentials.indexOf((char) 0);
+ if (startIndex >=0) {
+ int endIndex = decodedCredentials.indexOf((char) 0, startIndex+1);
+ if (endIndex >=0) {
+ userName = decodedCredentials.substring(startIndex+1, endIndex);
+ password = decodedCredentials.substring(endIndex + 1);
+ } else {
+ throw new DavMailException("EXCEPTION_INVALID_CREDENTIALS");
+ }
+ } else {
+ throw new DavMailException("EXCEPTION_INVALID_CREDENTIALS");
+ }
+ }
+
+}
+
diff --git a/src/java/davmail/smtp/SmtpServer.java b/src/java/davmail/smtp/SmtpServer.java
new file mode 100644
index 0000000..1629d80
--- /dev/null
+++ b/src/java/davmail/smtp/SmtpServer.java
@@ -0,0 +1,57 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.smtp;
+
+import davmail.AbstractConnection;
+import davmail.AbstractServer;
+import davmail.Settings;
+
+import java.net.Socket;
+
+/**
+ * SMTP server, handle message send requests.
+ */
+public class SmtpServer extends AbstractServer {
+ /**
+ * Default SMTP Caldav port
+ */
+ public static final int DEFAULT_PORT = 25;
+
+ /**
+ * Create a ServerSocket to listen for connections.
+ * Start the thread.
+ *
+ * @param port smtp port
+ */
+ public SmtpServer(int port) {
+ super(SmtpServer.class.getName(), port, SmtpServer.DEFAULT_PORT);
+ nosslFlag = Settings.getBooleanProperty("davmail.ssl.nosecuresmtp");
+ }
+
+ @Override
+ public String getProtocolName() {
+ return "SMTP";
+ }
+
+ @Override
+ public AbstractConnection createConnectionHandler(Socket clientSocket) {
+ return new SmtpConnection(clientSocket);
+ }
+
+}
diff --git a/src/java/davmail/ui/AboutFrame.java b/src/java/davmail/ui/AboutFrame.java
new file mode 100644
index 0000000..ce01d4f
--- /dev/null
+++ b/src/java/davmail/ui/AboutFrame.java
@@ -0,0 +1,144 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.ui;
+
+import davmail.BundleMessage;
+import davmail.DavGateway;
+import davmail.ui.browser.DesktopBrowser;
+import davmail.ui.tray.DavGatewayTray;
+
+import javax.imageio.ImageIO;
+import javax.swing.*;
+import javax.swing.event.HyperlinkEvent;
+import javax.swing.event.HyperlinkListener;
+import javax.swing.text.html.HTMLEditorKit;
+import javax.swing.text.html.StyleSheet;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+
+/**
+ * About frame
+ */
+public class AboutFrame extends JFrame {
+ private final JEditorPane jEditorPane;
+
+ /**
+ * About frame.
+ */
+ public AboutFrame() {
+ setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
+ setTitle(BundleMessage.format("UI_ABOUT_DAVMAIL"));
+ try {
+ setIconImage(DavGatewayTray.getFrameIcon());
+ } catch (NoSuchMethodError error) {
+ DavGatewayTray.debug(new BundleMessage("LOG_UNABLE_TO_SET_ICON_IMAGE"));
+ }
+ try {
+ JLabel imageLabel = new JLabel();
+ ClassLoader classloader = this.getClass().getClassLoader();
+ URL imageUrl = classloader.getResource("tray32.png");
+ Image iconImage = ImageIO.read(imageUrl);
+ ImageIcon icon = new ImageIcon(iconImage);
+ imageLabel.setIcon(icon);
+ JPanel imagePanel = new JPanel();
+ imagePanel.add(imageLabel);
+ add(BorderLayout.WEST, imagePanel);
+ } catch (IOException e) {
+ DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_CREATE_ICON"), e);
+ }
+
+ jEditorPane = new JEditorPane();
+ HTMLEditorKit htmlEditorKit = new HTMLEditorKit();
+ StyleSheet stylesheet = htmlEditorKit.getStyleSheet();
+ stylesheet.addRule("body { font-size:small;font-family: " + jEditorPane.getFont().getFamily() + '}');
+ jEditorPane.setEditorKit(htmlEditorKit);
+ jEditorPane.setContentType("text/html");
+ jEditorPane.setText(getContent(null));
+
+ jEditorPane.setEditable(false);
+ jEditorPane.setOpaque(false);
+ jEditorPane.addHyperlinkListener(new HyperlinkListener() {
+ public void hyperlinkUpdate(HyperlinkEvent hle) {
+ if (HyperlinkEvent.EventType.ACTIVATED.equals(hle.getEventType())) {
+ try {
+ DesktopBrowser.browse(hle.getURL().toURI());
+ } catch (URISyntaxException e) {
+ DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_OPEN_LINK"), e);
+ }
+ setVisible(false);
+ }
+ }
+ });
+
+
+ JPanel mainPanel = new JPanel();
+ mainPanel.add(jEditorPane);
+ add(BorderLayout.CENTER, mainPanel);
+
+ JPanel buttonPanel = new JPanel();
+ JButton ok = new JButton(BundleMessage.format("UI_BUTTON_OK"));
+ ActionListener close = new ActionListener() {
+ public void actionPerformed(ActionEvent evt) {
+ setVisible(false);
+ }
+ };
+ ok.addActionListener(close);
+
+ buttonPanel.add(ok);
+
+ add(BorderLayout.SOUTH, buttonPanel);
+
+ pack();
+ setResizable(false);
+ // center frame
+ setLocation(getToolkit().getScreenSize().width / 2 -
+ getSize().width / 2,
+ getToolkit().getScreenSize().height / 2 -
+ getSize().height / 2);
+ }
+
+ String getContent(String releasedVersion) {
+ Package davmailPackage = DavGateway.class.getPackage();
+ StringBuilder buffer = new StringBuilder();
+ buffer.append(BundleMessage.format("UI_ABOUT_DAVMAIL_AUTHOR"));
+ String currentVersion = davmailPackage.getImplementationVersion();
+ if (currentVersion != null) {
+ buffer.append(BundleMessage.format("UI_CURRENT_VERSION", currentVersion));
+ }
+ if (currentVersion != null && releasedVersion != null && currentVersion.compareTo(releasedVersion) < 0) {
+ buffer.append(BundleMessage.format("UI_LATEST_VERSION", releasedVersion));
+ }
+ buffer.append(BundleMessage.format("UI_HELP_INSTRUCTIONS"));
+ return buffer.toString();
+ }
+
+
+ /**
+ * Update about frame content with current released version.
+ */
+ public void update() {
+ jEditorPane.setText(getContent(DavGateway.getReleasedVersion()));
+ pack();
+ }
+
+}
diff --git a/src/java/davmail/ui/AcceptCertificateDialog.java b/src/java/davmail/ui/AcceptCertificateDialog.java
new file mode 100644
index 0000000..c33484c
--- /dev/null
+++ b/src/java/davmail/ui/AcceptCertificateDialog.java
@@ -0,0 +1,169 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.ui;
+
+import davmail.BundleMessage;
+import davmail.http.DavGatewayX509TrustManager;
+import davmail.ui.tray.DavGatewayTray;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.lang.reflect.InvocationTargetException;
+import java.security.cert.X509Certificate;
+import java.text.DateFormat;
+import java.util.Date;
+
+/**
+ * Accept certificate dialog
+ */
+public class AcceptCertificateDialog extends JDialog {
+ protected boolean accepted;
+
+ /**
+ * Accept status.
+ *
+ * @return true if user accepted certificate
+ */
+ public boolean isAccepted() {
+ return accepted;
+ }
+
+ /**
+ * Add a new JLabel to panel with <b>label</b>: value text.
+ *
+ * @param panel certificate details panel
+ * @param label certificate attribute label
+ * @param value certificate attribute value
+ */
+ protected void addFieldValue(JPanel panel, String label, String value) {
+ JPanel fieldPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+ StringBuilder buffer = new StringBuilder();
+ buffer.append("<html><b>");
+ buffer.append(label);
+ buffer.append(":</b></html>");
+ fieldPanel.add(new JLabel(buffer.toString()));
+ fieldPanel.add(new JLabel(value));
+ panel.add(fieldPanel);
+ }
+
+ /**
+ * Accept certificate dialog.
+ *
+ * @param certificate certificate sent by server
+ */
+ public AcceptCertificateDialog(X509Certificate certificate) {
+ setAlwaysOnTop(true);
+ String sha1Hash = DavGatewayX509TrustManager.getFormattedHash(certificate);
+ DateFormat formatter = DateFormat.getDateInstance(DateFormat.MEDIUM);
+
+ setTitle(BundleMessage.format("UI_ACCEPT_CERTIFICATE"));
+ try {
+ setIconImage(DavGatewayTray.getFrameIcon());
+ } catch (NoSuchMethodError error) {
+ DavGatewayTray.debug(new BundleMessage("LOG_UNABLE_TO_SET_ICON_IMAGE"));
+ }
+
+ JPanel subjectPanel = new JPanel();
+ subjectPanel.setLayout(new BoxLayout(subjectPanel, BoxLayout.Y_AXIS));
+ subjectPanel.setBorder(BorderFactory.createTitledBorder(BundleMessage.format("UI_SERVER_CERTIFICATE")));
+ addFieldValue(subjectPanel, BundleMessage.format("UI_ISSUED_TO"), DavGatewayX509TrustManager.getRDN(certificate.getSubjectDN()));
+ addFieldValue(subjectPanel, BundleMessage.format("UI_ISSUED_BY"), DavGatewayX509TrustManager.getRDN(certificate.getIssuerDN()));
+ Date now = new Date();
+ String notBefore = formatter.format(certificate.getNotBefore());
+ if (now.before(certificate.getNotBefore())) {
+ notBefore = "<html><font color=\"#FF0000\">" + notBefore + "</font></html>";
+ }
+ addFieldValue(subjectPanel, BundleMessage.format("UI_VALID_FROM"), notBefore);
+ String notAfter = formatter.format(certificate.getNotAfter());
+ if (now.after(certificate.getNotAfter())) {
+ notAfter = "<html><font color=\"#FF0000\">" + notAfter + "</font></html>";
+ }
+ addFieldValue(subjectPanel, BundleMessage.format("UI_VALID_UNTIL"), notAfter);
+ addFieldValue(subjectPanel, BundleMessage.format("UI_SERIAL"), DavGatewayX509TrustManager.getFormattedSerial(certificate));
+ addFieldValue(subjectPanel, BundleMessage.format("UI_FINGERPRINT"), sha1Hash);
+
+ JPanel warningPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+ JLabel imageLabel = new JLabel();
+ imageLabel.setIcon(UIManager.getIcon("OptionPane.warningIcon"));
+ imageLabel.setText(BundleMessage.format("UI_UNTRUSTED_CERTIFICATE_HTML"));
+ warningPanel.add(imageLabel);
+ add(warningPanel, BorderLayout.NORTH);
+ add(subjectPanel, BorderLayout.CENTER);
+ add(getButtonPanel(), BorderLayout.SOUTH);
+ setModal(true);
+
+ pack();
+ // center frame
+ setLocation(getToolkit().getScreenSize().width / 2 -
+ getSize().width / 2,
+ getToolkit().getScreenSize().height / 2 -
+ getSize().height / 2);
+ setAlwaysOnTop(true);
+ setVisible(true);
+ }
+
+ protected JPanel getButtonPanel() {
+ JPanel buttonPanel = new JPanel();
+ JButton accept = new JButton(BundleMessage.format("UI_BUTTON_ACCEPT"));
+ JButton deny = new JButton(BundleMessage.format("UI_BUTTON_DENY"));
+ accept.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent evt) {
+ accepted = true;
+ setVisible(false);
+ }
+ });
+ deny.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent evt) {
+ accepted = false;
+ setVisible(false);
+ }
+ });
+
+ buttonPanel.add(accept);
+ buttonPanel.add(deny);
+ return buttonPanel;
+ }
+
+
+ /**
+ * Display certificate accept dialog and get user answer.
+ *
+ * @param certificate certificate sent by server
+ * @return true if user accepted certificate
+ */
+ public static boolean isCertificateTrusted(final X509Certificate certificate) {
+ final boolean[] answer = new boolean[1];
+ try {
+ SwingUtilities.invokeAndWait(new Runnable() {
+ public void run() {
+ AcceptCertificateDialog certificateDialog = new AcceptCertificateDialog(certificate);
+ answer[0] = certificateDialog.isAccepted();
+ }
+ });
+ } catch (InterruptedException ie) {
+ DavGatewayTray.error(new BundleMessage("UI_ERROR_WAITING_FOR_CERTIFICATE_CHECK"), ie);
+ } catch (InvocationTargetException ite) {
+ DavGatewayTray.error(new BundleMessage("UI_ERROR_WAITING_FOR_CERTIFICATE_CHECK"), ite);
+ }
+
+ return answer[0];
+ }
+}
diff --git a/src/java/davmail/ui/NotificationDialog.java b/src/java/davmail/ui/NotificationDialog.java
new file mode 100644
index 0000000..fb3d38e
--- /dev/null
+++ b/src/java/davmail/ui/NotificationDialog.java
@@ -0,0 +1,215 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.ui;
+
+import davmail.BundleMessage;
+import davmail.ui.browser.DesktopBrowser;
+import davmail.ui.tray.DavGatewayTray;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+/**
+ * Edit Caldav scheduling notifications.
+ */
+public class NotificationDialog extends JDialog {
+
+ protected boolean sendNotification;
+
+ protected JTextField toField;
+ protected JTextField ccField;
+ protected JTextField subjectField;
+ protected JEditorPane bodyField;
+
+ protected void addRecipientComponent(JPanel panel, String label, JTextField textField, String toolTipText) {
+ JLabel fieldLabel = new JLabel(label);
+ fieldLabel.setHorizontalAlignment(SwingConstants.RIGHT);
+ fieldLabel.setVerticalAlignment(SwingConstants.CENTER);
+ textField.setMaximumSize(textField.getPreferredSize());
+ JPanel innerPanel = new JPanel();
+ innerPanel.setLayout(new BoxLayout(innerPanel, BoxLayout.X_AXIS));
+ innerPanel.setAlignmentX(Component.RIGHT_ALIGNMENT);
+ innerPanel.add(fieldLabel);
+ innerPanel.add(textField);
+ panel.add(innerPanel);
+ if (toolTipText != null) {
+ fieldLabel.setToolTipText(toolTipText);
+ textField.setToolTipText(toolTipText);
+ }
+ }
+
+ /**
+ * Notification dialog to let user edit message body or cancel notification.
+ *
+ * @param to main recipients
+ * @param cc copy recipients
+ * @param subject notification subject
+ */
+ public NotificationDialog(String to, String cc, String subject, String description) {
+ setModal(true);
+ setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
+ setTitle(BundleMessage.format("UI_CALDAV_NOTIFICATION"));
+ try {
+ setIconImage(DavGatewayTray.getFrameIcon());
+ } catch (NoSuchMethodError error) {
+ DavGatewayTray.debug(new BundleMessage("LOG_UNABLE_TO_SET_ICON_IMAGE"));
+ }
+
+ JPanel mainPanel = new JPanel();
+ // add help (F1 handler)
+ mainPanel.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke("F1"),
+ "help");
+ mainPanel.getActionMap().put("help", new AbstractAction() {
+ public void actionPerformed(ActionEvent e) {
+ DesktopBrowser.browse("http://davmail.sourceforge.net");
+ }
+ });
+
+ mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
+ mainPanel.add(getRecipientsPanel());
+ mainPanel.add(getBodyPanel(description));
+
+ JPanel recipientsPanel = getRecipientsPanel();
+ if (to != null) {
+ toField.setText(to);
+ }
+ if (cc != null) {
+ ccField.setText(cc);
+ }
+ if (subject != null) {
+ subjectField.setText(subject);
+ }
+ add(BorderLayout.NORTH, recipientsPanel);
+ JPanel bodyPanel = getBodyPanel(description);
+ add(BorderLayout.CENTER, bodyPanel);
+ bodyField.setPreferredSize(recipientsPanel.getPreferredSize());
+
+ JPanel buttonPanel = new JPanel();
+ JButton cancel = new JButton(BundleMessage.format("UI_BUTTON_CANCEL"));
+ JButton send = new JButton(BundleMessage.format("UI_BUTTON_SEND"));
+
+ send.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent evt) {
+ sendNotification = true;
+ setVisible(false);
+ }
+ });
+
+ cancel.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent evt) {
+ // nothing to do, just hide
+ setVisible(false);
+ }
+ });
+
+ buttonPanel.add(send);
+ buttonPanel.add(cancel);
+
+ add(BorderLayout.SOUTH, buttonPanel);
+
+ pack();
+ setResizable(true);
+ // center frame
+ setLocation(getToolkit().getScreenSize().width / 2 -
+ getSize().width / 2,
+ getToolkit().getScreenSize().height / 2 -
+ getSize().height / 2);
+ setAlwaysOnTop(true);
+ setVisible(true);
+ }
+
+ protected JPanel getRecipientsPanel() {
+ JPanel recipientsPanel = new JPanel();
+ recipientsPanel.setLayout(new BoxLayout(recipientsPanel, BoxLayout.Y_AXIS));
+ recipientsPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
+
+ toField = new JTextField("", 40);
+ ccField = new JTextField("", 40);
+ subjectField = new JTextField("", 40);
+
+ addRecipientComponent(recipientsPanel, BundleMessage.format("UI_TO"), toField,
+ BundleMessage.format("UI_TO_HELP"));
+ addRecipientComponent(recipientsPanel, BundleMessage.format("UI_CC"), ccField,
+ BundleMessage.format("UI_CC"));
+ addRecipientComponent(recipientsPanel, BundleMessage.format("UI_SUBJECT"), subjectField,
+ BundleMessage.format("UI_SUBJECT_HELP"));
+ return recipientsPanel;
+ }
+
+ protected JPanel getBodyPanel(String description) {
+ JPanel bodyPanel = new JPanel();
+ bodyPanel.setBorder(BorderFactory.createTitledBorder(BundleMessage.format("UI_NOTIFICATION_BODY")));
+
+ bodyField = new JEditorPane();
+ bodyField.setText(description);
+ //HTMLEditorKit htmlEditorKit = new HTMLEditorKit();
+ //bodyField.setEditorKit(htmlEditorKit);
+ //bodyField.setContentType("text/html");
+
+ bodyPanel.add(new JScrollPane(bodyField));
+ return bodyPanel;
+ }
+
+ /**
+ * Cancel notification flag.
+ *
+ * @return false if user chose to cancel notification
+ */
+ public boolean getSendNotification() {
+ return sendNotification;
+ }
+
+ /**
+ * Get edited recipients.
+ *
+ * @return recipients string
+ */
+ public String getTo() {
+ return toField.getText();
+ }
+
+ /**
+ * Get edited copy recipients.
+ *
+ * @return copy recipients string
+ */
+ public String getCc() {
+ return ccField.getText();
+ }
+
+ /**
+ * Get edited subject.
+ *
+ * @return subject
+ */
+ public String getSubject() {
+ return subjectField.getText();
+ }
+
+ /**
+ * Get edited body.
+ *
+ * @return edited notification body
+ */
+ public String getBody() {
+ return bodyField.getText();
+ }
+}
diff --git a/src/java/davmail/ui/OSXAdapter.java b/src/java/davmail/ui/OSXAdapter.java
new file mode 100644
index 0000000..3b458b7
--- /dev/null
+++ b/src/java/davmail/ui/OSXAdapter.java
@@ -0,0 +1,231 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.ui;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+
+/**
+ * Reflection based MacOS handler
+ */
+public class OSXAdapter implements InvocationHandler {
+
+ protected final Object targetObject;
+ protected final Method targetMethod;
+ protected final String proxySignature;
+
+ static Object macOSXApplication;
+
+ /**
+ * Pass this method an Object and Method equipped to perform application shutdown logic.
+ * The method passed should return a boolean stating whether or not the quit should occur
+ *
+ * @param target target object
+ * @param quitHandler quit method
+ * @throws InvocationTargetException on error
+ * @throws ClassNotFoundException on error
+ * @throws NoSuchMethodException on error
+ * @throws InstantiationException on error
+ * @throws IllegalAccessException on error
+ */
+ public static void setQuitHandler(Object target, Method quitHandler) throws InvocationTargetException, ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException {
+ setHandler(new OSXAdapter("handleQuit", target, quitHandler));
+ }
+
+ /**
+ * Pass this method an Object and Method equipped to display application info
+ * They will be called when the About menu item is selected from the application menu
+ *
+ * @param target target object
+ * @param aboutHandler about method
+ * @throws InvocationTargetException on error
+ * @throws ClassNotFoundException on error
+ * @throws NoSuchMethodException on error
+ * @throws InstantiationException on error
+ * @throws IllegalAccessException on error
+ */
+ public static void setAboutHandler(Object target, Method aboutHandler) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, ClassNotFoundException, InstantiationException {
+ boolean enableAboutMenu = (target != null && aboutHandler != null);
+ if (enableAboutMenu) {
+ setHandler(new OSXAdapter("handleAbout", target, aboutHandler));
+ }
+ Method enableAboutMethod = macOSXApplication.getClass().getDeclaredMethod("setEnabledAboutMenu", new Class[]{boolean.class});
+ enableAboutMethod.invoke(macOSXApplication, enableAboutMenu);
+ }
+
+ /**
+ * Pass this method an Object and a Method equipped to display application options.
+ * They will be called when the Preferences menu item is selected from the application menu
+ *
+ * @param target target object
+ * @param prefsHandler preferences method
+ * @throws InvocationTargetException on error
+ * @throws ClassNotFoundException on error
+ * @throws NoSuchMethodException on error
+ * @throws InstantiationException on error
+ * @throws IllegalAccessException on error
+ */
+ public static void setPreferencesHandler(Object target, Method prefsHandler) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, ClassNotFoundException, InstantiationException {
+ boolean enablePrefsMenu = (target != null && prefsHandler != null);
+ if (enablePrefsMenu) {
+ setHandler(new OSXAdapter("handlePreferences", target, prefsHandler));
+ }
+ Method enablePrefsMethod = macOSXApplication.getClass().getDeclaredMethod("setEnabledPreferencesMenu", new Class[]{boolean.class});
+ enablePrefsMethod.invoke(macOSXApplication, enablePrefsMenu);
+ }
+
+ /**
+ * Pass this method an Object and a Method equipped to handle document events from the Finder.
+ * Documents are registered with the Finder via the CFBundleDocumentTypes dictionary in the
+ * application bundle's Info.plist
+ *
+ * @param target target object
+ * @param fileHandler file method
+ * @throws InvocationTargetException on error
+ * @throws ClassNotFoundException on error
+ * @throws NoSuchMethodException on error
+ * @throws InstantiationException on error
+ * @throws IllegalAccessException on error
+ */
+ @SuppressWarnings({"UnusedDeclaration"})
+ public static void setFileHandler(Object target, Method fileHandler) throws InvocationTargetException, ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException {
+ setHandler(new OSXAdapter("handleOpenFile", target, fileHandler) {
+ // Override OSXAdapter.callTarget to send information on the
+ // file to be opened
+ @Override
+ public boolean callTarget(Object appleEvent) {
+ if (appleEvent != null) {
+ try {
+ Method getFilenameMethod = appleEvent.getClass().getDeclaredMethod("getFilename", (Class[]) null);
+ String filename = (String) getFilenameMethod.invoke(appleEvent, (Object[]) null);
+ this.targetMethod.invoke(this.targetObject, filename);
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+ return true;
+ }
+ });
+ }
+
+ /**
+ * setHandler creates a Proxy object from the passed OSXAdapter and adds it as an ApplicationListener.
+ *
+ * @param adapter OSX adapter
+ * @throws InvocationTargetException on error
+ * @throws ClassNotFoundException on error
+ * @throws NoSuchMethodException on error
+ * @throws InstantiationException on error
+ * @throws IllegalAccessException on error
+ */
+ public static void setHandler(OSXAdapter adapter) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
+ Class<?> applicationClass = Class.forName("com.apple.eawt.Application");
+ if (macOSXApplication == null) {
+ macOSXApplication = applicationClass.getConstructor((Class[]) null).newInstance((Object[]) null);
+ }
+ Class applicationListenerClass = Class.forName("com.apple.eawt.ApplicationListener");
+ Method addListenerMethod = applicationClass.getDeclaredMethod("addApplicationListener", new Class[]{applicationListenerClass});
+ // Create a proxy object around this handler that can be reflectively added as an Apple ApplicationListener
+ Object osxAdapterProxy = Proxy.newProxyInstance(OSXAdapter.class.getClassLoader(), new Class[]{applicationListenerClass}, adapter);
+ addListenerMethod.invoke(macOSXApplication, osxAdapterProxy);
+ }
+
+ /**
+ * Each OSXAdapter has the name of the EAWT method it intends to listen for (handleAbout, for example),
+ * the Object that will ultimately perform the task, and the Method to be called on that Object
+ *
+ * @param proxySignature proxy signature
+ * @param target target object
+ * @param handler handler method
+ */
+ protected OSXAdapter(String proxySignature, Object target, Method handler) {
+ this.proxySignature = proxySignature;
+ this.targetObject = target;
+ this.targetMethod = handler;
+ }
+
+ /**
+ * Override this method to perform any operations on the event
+ * that comes with the various callbacks.
+ * See setFileHandler above for an example
+ *
+ * @param appleEvent apple event object
+ * @return true on success
+ * @throws InvocationTargetException on error
+ * @throws IllegalAccessException on error
+ */
+ public boolean callTarget(Object appleEvent) throws InvocationTargetException, IllegalAccessException {
+ Object result = targetMethod.invoke(targetObject, (Object[]) null);
+ return result == null || Boolean.valueOf(result.toString());
+ }
+
+ /**
+ * InvocationHandler implementation.
+ * This is the entry point for our proxy object; it is called every time an ApplicationListener method is invoked
+ *
+ * @param proxy proxy object
+ * @param method handler method
+ * @param args method arguments
+ * @return null
+ * @throws Throwable on error
+ */
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ if (isCorrectMethod(method, args)) {
+ boolean handled = callTarget(args[0]);
+ setApplicationEventHandled(args[0], handled);
+ }
+ // All of the ApplicationListener methods are void; return null regardless of what happens
+ return null;
+ }
+
+ //
+ //
+
+ /**
+ * Compare the method that was called to the intended method when the OSXAdapter instance was created
+ * (e.g. handleAbout, handleQuit, handleOpenFile, etc.).
+ *
+ * @param method handler method
+ * @param args method arguments
+ * @return true if method is correct
+ */
+ protected boolean isCorrectMethod(Method method, Object[] args) {
+ return (targetMethod != null && proxySignature.equals(method.getName()) && args.length == 1);
+ }
+
+ /**
+ * It is important to mark the ApplicationEvent as handled and cancel the default behavior.
+ * This method checks for a boolean result from the proxy method and sets the event accordingly
+ *
+ * @param event event object
+ * @param handled true if event handled
+ * @throws NoSuchMethodException on error
+ * @throws InvocationTargetException on error
+ * @throws IllegalAccessException on error
+ */
+ protected void setApplicationEventHandled(Object event, boolean handled) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+ if (event != null) {
+ Method setHandledMethod = event.getClass().getDeclaredMethod("setHandled", new Class[]{boolean.class});
+ // If the target method returns a boolean, use that as a hint
+ setHandledMethod.invoke(event, handled);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/java/davmail/ui/OSXInfoPlist.java b/src/java/davmail/ui/OSXInfoPlist.java
new file mode 100644
index 0000000..932e28a
--- /dev/null
+++ b/src/java/davmail/ui/OSXInfoPlist.java
@@ -0,0 +1,85 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2012 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.ui;
+
+import davmail.util.IOUtil;
+import org.apache.log4j.Logger;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * Handle OSX Info.plist file access
+ */
+public class OSXInfoPlist {
+ protected static final Logger LOGGER = Logger.getLogger(OSXInfoPlist.class);
+ protected static final String INFO_PLIST_PATH = "Contents/Info.plist";
+
+ protected static boolean isOSX() {
+ return System.getProperty("os.name").toLowerCase().startsWith("mac os x");
+ }
+
+ protected static String getInfoPlistContent() throws IOException {
+ FileInputStream fileInputStream = new FileInputStream(INFO_PLIST_PATH);
+ return new String(IOUtil.readFully(fileInputStream));
+ }
+
+ /**
+ * Test current LSUIElement (hide from dock) value
+ *
+ * @return true if application is hidden from dock
+ */
+ public static boolean isHideFromDock() {
+ boolean result = false;
+ try {
+ result = isOSX() && getInfoPlistContent().indexOf("<key>LSUIElement</key><string>1</string>") >= 0;
+ } catch (IOException e) {
+ LOGGER.warn("Unable to update Info.plist", e);
+ }
+ return result;
+ }
+
+ /**
+ * Update LSUIElement (hide from dock) value
+ *
+ * @param hideFromDock new hide from dock value
+ */
+ public static void setOSXHideFromDock(boolean hideFromDock) {
+ try {
+ if (isOSX()) {
+ boolean currentHideFromDock = isHideFromDock();
+ if (currentHideFromDock != hideFromDock) {
+ String content = getInfoPlistContent();
+ FileOutputStream fileOutputStream = new FileOutputStream(INFO_PLIST_PATH);
+ try {
+ fileOutputStream.write(content.replaceFirst(
+ "<key>LSUIElement</key><string>" + (currentHideFromDock ? "1" : "0") + "</string>",
+ "<key>LSUIElement</key><string>" + (hideFromDock ? "1" : "0") + "</string>").getBytes("UTF-8"));
+ } finally {
+ fileOutputStream.close();
+ }
+ fileOutputStream.close();
+ }
+ }
+ } catch (IOException e) {
+ LOGGER.warn("Unable to update Info.plist", e);
+ }
+ }
+}
diff --git a/src/java/davmail/ui/PasswordPromptDialog.java b/src/java/davmail/ui/PasswordPromptDialog.java
new file mode 100644
index 0000000..cb7cee3
--- /dev/null
+++ b/src/java/davmail/ui/PasswordPromptDialog.java
@@ -0,0 +1,138 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.ui;
+
+import davmail.BundleMessage;
+import davmail.ui.tray.DavGatewayTray;
+
+import javax.swing.*;
+import javax.swing.border.EmptyBorder;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+/**
+ * Get smartcard password
+ */
+public class PasswordPromptDialog extends JDialog {
+ final JPasswordField passwordField = new JPasswordField(20);
+ protected char[] password;
+
+ /**
+ * Get user password.
+ *
+ * @return user password as char array
+ */
+ public char[] getPassword() {
+ if (password != null) {
+ return password.clone();
+ } else {
+ return "".toCharArray();
+ }
+ }
+
+ /**
+ * Get smartcard password.
+ *
+ * @param prompt password prompt from PKCS11 module
+ */
+ public PasswordPromptDialog(String prompt) {
+ this(prompt, null);
+ }
+
+ /**
+ * Get smartcard password.
+ *
+ * @param prompt password prompt from PKCS11 module
+ * @param captchaImage ISA filter pinsafe image
+ */
+ public PasswordPromptDialog(String prompt, Image captchaImage) {
+ setAlwaysOnTop(true);
+
+ setTitle(BundleMessage.format("UI_PASSWORD_PROMPT"));
+ try {
+ //noinspection Since15
+ setIconImage(DavGatewayTray.getFrameIcon());
+ } catch (NoSuchMethodError error) {
+ DavGatewayTray.debug(new BundleMessage("LOG_UNABLE_TO_SET_ICON_IMAGE"));
+ }
+
+
+ JPanel questionPanel = new JPanel();
+ questionPanel.setLayout(new BoxLayout(questionPanel, BoxLayout.Y_AXIS));
+ JLabel imageLabel = new JLabel();
+ imageLabel.setIcon(UIManager.getIcon("OptionPane.questionIcon"));
+ imageLabel.setText(prompt);
+ questionPanel.add(imageLabel);
+
+ passwordField.setMaximumSize(passwordField.getPreferredSize());
+ passwordField.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ password = passwordField.getPassword();
+ setVisible(false);
+ }
+ });
+ JPanel passwordPanel = new JPanel();
+ passwordPanel.setLayout(new BoxLayout(passwordPanel, BoxLayout.Y_AXIS));
+ if (captchaImage != null) {
+ JLabel captchaLabel = new JLabel(new ImageIcon(captchaImage));
+ captchaLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
+ captchaLabel.setBorder(new EmptyBorder(10,10,10,10));
+ passwordPanel.add(captchaLabel);
+ }
+ passwordPanel.add(passwordField);
+
+ add(questionPanel, BorderLayout.NORTH);
+ add(passwordPanel, BorderLayout.CENTER);
+ add(getButtonPanel(), BorderLayout.SOUTH);
+ setModal(true);
+
+ pack();
+ // center frame
+ setLocation(getToolkit().getScreenSize().width / 2 -
+ getSize().width / 2,
+ getToolkit().getScreenSize().height / 2 -
+ getSize().height / 2);
+ setAlwaysOnTop(true);
+ setVisible(true);
+ }
+
+ protected JPanel getButtonPanel() {
+ JPanel buttonPanel = new JPanel();
+ JButton okButton = new JButton(BundleMessage.format("UI_BUTTON_OK"));
+ JButton cancelButton = new JButton(BundleMessage.format("UI_BUTTON_CANCEL"));
+ okButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent evt) {
+ password = passwordField.getPassword();
+ setVisible(false);
+ }
+ });
+ cancelButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent evt) {
+ password = null;
+ setVisible(false);
+ }
+ });
+
+ buttonPanel.add(okButton);
+ buttonPanel.add(cancelButton);
+ return buttonPanel;
+ }
+
+}
diff --git a/src/java/davmail/ui/SelectCertificateDialog.java b/src/java/davmail/ui/SelectCertificateDialog.java
new file mode 100644
index 0000000..d20bb0b
--- /dev/null
+++ b/src/java/davmail/ui/SelectCertificateDialog.java
@@ -0,0 +1,111 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2011 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.ui;
+
+import davmail.BundleMessage;
+import davmail.ui.tray.DavGatewayTray;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+/**
+ * Let user select a client certificate
+ */
+public class SelectCertificateDialog extends JDialog {
+ protected final JList aliasListBox;
+ protected String selectedAlias;
+
+ /**
+ * Gets user selected alias.
+ *
+ * @return user selected alias
+ */
+ public String getSelectedAlias() {
+ return this.selectedAlias;
+ }
+
+ /**
+ * Select a client certificate
+ *
+ * @param aliases An array of certificate aliases for the user to pick from
+ */
+ public SelectCertificateDialog(String[] aliases) {
+ setAlwaysOnTop(true);
+
+ setTitle(BundleMessage.format("UI_CERTIFICATE_ALIAS_PROMPT"));
+ try {
+ setIconImage(DavGatewayTray.getFrameIcon());
+ } catch (NoSuchMethodError error) {
+ DavGatewayTray.debug(new BundleMessage("LOG_UNABLE_TO_SET_ICON_IMAGE"));
+ }
+
+ JPanel questionPanel = new JPanel();
+ questionPanel.setLayout(new BoxLayout(questionPanel, BoxLayout.Y_AXIS));
+ JLabel imageLabel = new JLabel();
+ imageLabel.setIcon(UIManager.getIcon("OptionPane.questionIcon"));
+ imageLabel.setText(BundleMessage.format("UI_CERTIFICATE_ALIAS_PROMPT"));
+ questionPanel.add(imageLabel);
+
+ aliasListBox = new JList(aliases);
+ aliasListBox.setMaximumSize(aliasListBox.getPreferredSize());
+
+ JPanel aliasPanel = new JPanel();
+ aliasPanel.setLayout(new BoxLayout(aliasPanel, BoxLayout.Y_AXIS));
+ aliasPanel.add(aliasListBox);
+
+ add(questionPanel, BorderLayout.NORTH);
+ add(aliasPanel, BorderLayout.CENTER);
+ add(getButtonPanel(), BorderLayout.SOUTH);
+ setModal(true);
+
+ pack();
+ // center frame
+ setLocation(getToolkit().getScreenSize().width / 2 -
+ getSize().width / 2,
+ getToolkit().getScreenSize().height / 2 -
+ getSize().height / 2);
+ setAlwaysOnTop(true);
+ setVisible(true);
+ }
+
+ protected JPanel getButtonPanel() {
+ JPanel buttonPanel = new JPanel();
+ JButton okButton = new JButton(BundleMessage.format("UI_BUTTON_OK"));
+ JButton cancelButton = new JButton(BundleMessage.format("UI_BUTTON_CANCEL"));
+ okButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent evt) {
+ selectedAlias = aliasListBox.getSelectedValue().toString();
+ setVisible(false);
+ }
+ });
+ cancelButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent evt) {
+ selectedAlias = null;
+ setVisible(false);
+ }
+ });
+
+ buttonPanel.add(okButton);
+ buttonPanel.add(cancelButton);
+ return buttonPanel;
+ }
+
+}
diff --git a/src/java/davmail/ui/SettingsFrame.java b/src/java/davmail/ui/SettingsFrame.java
new file mode 100644
index 0000000..3ee4e63
--- /dev/null
+++ b/src/java/davmail/ui/SettingsFrame.java
@@ -0,0 +1,821 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.ui;
+
+import davmail.BundleMessage;
+import davmail.DavGateway;
+import davmail.Settings;
+import davmail.ui.browser.DesktopBrowser;
+import davmail.ui.tray.DavGatewayTray;
+import org.apache.log4j.Level;
+
+import javax.swing.*;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+
+/**
+ * DavMail settings frame
+ */
+public class SettingsFrame extends JFrame {
+ static final Level[] LOG_LEVELS = {Level.OFF, Level.FATAL, Level.ERROR, Level.WARN, Level.INFO, Level.DEBUG, Level.ALL};
+
+ protected JTextField urlField;
+ protected JTextField popPortField;
+ protected JCheckBox popPortCheckBox;
+ protected JCheckBox popNoSSLCheckBox;
+ protected JTextField imapPortField;
+ protected JCheckBox imapPortCheckBox;
+ protected JCheckBox imapNoSSLCheckBox;
+ protected JTextField smtpPortField;
+ protected JCheckBox smtpPortCheckBox;
+ protected JCheckBox smtpNoSSLCheckBox;
+ protected JTextField caldavPortField;
+ protected JCheckBox caldavPortCheckBox;
+ protected JCheckBox caldavNoSSLCheckBox;
+ protected JTextField ldapPortField;
+ protected JCheckBox ldapPortCheckBox;
+ protected JCheckBox ldapNoSSLCheckBox;
+ protected JTextField keepDelayField;
+ protected JTextField sentKeepDelayField;
+ protected JTextField caldavPastDelayField;
+ protected JTextField imapIdleDelayField;
+
+ JCheckBox useSystemProxiesField;
+ JCheckBox enableProxyField;
+ JTextField httpProxyField;
+ JTextField httpProxyPortField;
+ JTextField httpProxyUserField;
+ JTextField httpProxyPasswordField;
+
+ JCheckBox allowRemoteField;
+ JTextField bindAddressField;
+ JTextField clientSoTimeoutField;
+ JTextField certHashField;
+ JCheckBox disableUpdateCheck;
+
+ JComboBox keystoreTypeCombo;
+ JTextField keystoreFileField;
+ JPasswordField keystorePassField;
+ JPasswordField keyPassField;
+
+ JComboBox clientKeystoreTypeCombo;
+ JTextField clientKeystoreFileField;
+ JPasswordField clientKeystorePassField;
+ JTextField pkcs11LibraryField;
+ JTextArea pkcs11ConfigField;
+
+ JComboBox rootLoggingLevelField;
+ JComboBox davmailLoggingLevelField;
+ JComboBox httpclientLoggingLevelField;
+ JComboBox wireLoggingLevelField;
+ JTextField logFilePathField;
+ JTextField logFileSizeField;
+
+ JCheckBox caldavEditNotificationsField;
+ JTextField caldavAlarmSoundField;
+ JCheckBox forceActiveSyncUpdateCheckBox;
+ JTextField defaultDomainField;
+ JCheckBox showStartupBannerCheckBox;
+ JCheckBox disableGuiNotificationsCheckBox;
+ JCheckBox imapAutoExpungeCheckBox;
+ JCheckBox popMarkReadOnRetrCheckBox;
+ JComboBox enableEwsComboBox;
+ JCheckBox smtpSaveInSentCheckBox;
+
+ JCheckBox osxHideFromDockCheckBox;
+
+ protected void addSettingComponent(JPanel panel, String label, JComponent component) {
+ addSettingComponent(panel, label, component, null);
+ }
+
+ protected void addSettingComponent(JPanel panel, String label, JComponent component, String toolTipText) {
+ JLabel fieldLabel = new JLabel(label);
+ fieldLabel.setHorizontalAlignment(SwingConstants.RIGHT);
+ fieldLabel.setVerticalAlignment(SwingConstants.CENTER);
+ panel.add(fieldLabel);
+ component.setMaximumSize(component.getPreferredSize());
+ JPanel innerPanel = new JPanel();
+ innerPanel.setLayout(new BoxLayout(innerPanel, BoxLayout.X_AXIS));
+ innerPanel.add(component);
+ panel.add(innerPanel);
+ if (toolTipText != null) {
+ fieldLabel.setToolTipText(toolTipText);
+ component.setToolTipText(toolTipText);
+ }
+ }
+
+ protected void addPortSettingComponent(JPanel panel, String label, JComponent component, JComponent checkboxComponent, JComponent checkboxSSLComponent, String toolTipText) {
+ JLabel fieldLabel = new JLabel(label);
+ fieldLabel.setHorizontalAlignment(SwingConstants.RIGHT);
+ fieldLabel.setVerticalAlignment(SwingConstants.CENTER);
+ panel.add(fieldLabel);
+ component.setMaximumSize(component.getPreferredSize());
+ JPanel innerPanel = new JPanel();
+ innerPanel.setLayout(new BoxLayout(innerPanel, BoxLayout.X_AXIS));
+ innerPanel.add(checkboxComponent);
+ innerPanel.add(component);
+ innerPanel.add(checkboxSSLComponent);
+ panel.add(innerPanel);
+ if (toolTipText != null) {
+ fieldLabel.setToolTipText(toolTipText);
+ component.setToolTipText(toolTipText);
+ }
+ }
+
+ protected JPanel getSettingsPanel() {
+ JPanel settingsPanel = new JPanel(new GridLayout(6, 2));
+ settingsPanel.setBorder(BorderFactory.createTitledBorder(BundleMessage.format("UI_GATEWAY")));
+
+ urlField = new JTextField(Settings.getProperty("davmail.url"), 17);
+ popPortField = new JTextField(Settings.getProperty("davmail.popPort"), 4);
+ popPortCheckBox = new JCheckBox();
+ popNoSSLCheckBox = new JCheckBox(BundleMessage.format("UI_NO_SSL"), Settings.getBooleanProperty("davmail.ssl.nosecurepop"));
+ popPortCheckBox.setSelected(Settings.getProperty("davmail.popPort") != null && Settings.getProperty("davmail.popPort").length() > 0);
+ popPortField.setEnabled(popPortCheckBox.isSelected());
+ popNoSSLCheckBox.setEnabled(popPortCheckBox.isSelected() && isSslEnabled());
+ popPortCheckBox.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent evt) {
+ popPortField.setEnabled(popPortCheckBox.isSelected());
+ popNoSSLCheckBox.setEnabled(popPortCheckBox.isSelected() && isSslEnabled());
+ }
+ });
+
+ imapPortField = new JTextField(Settings.getProperty("davmail.imapPort"), 4);
+ imapPortCheckBox = new JCheckBox();
+ imapNoSSLCheckBox = new JCheckBox(BundleMessage.format("UI_NO_SSL"), Settings.getBooleanProperty("davmail.ssl.nosecureimap"));
+ imapPortCheckBox.setSelected(Settings.getProperty("davmail.imapPort") != null && Settings.getProperty("davmail.imapPort").length() > 0);
+ imapPortField.setEnabled(imapPortCheckBox.isSelected());
+ imapNoSSLCheckBox.setEnabled(imapPortCheckBox.isSelected() && isSslEnabled());
+ imapPortCheckBox.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent evt) {
+ imapPortField.setEnabled(imapPortCheckBox.isSelected());
+ imapNoSSLCheckBox.setEnabled(imapPortCheckBox.isSelected() && isSslEnabled());
+ }
+ });
+
+ smtpPortField = new JTextField(Settings.getProperty("davmail.smtpPort"), 4);
+ smtpPortCheckBox = new JCheckBox();
+ smtpNoSSLCheckBox = new JCheckBox(BundleMessage.format("UI_NO_SSL"), Settings.getBooleanProperty("davmail.ssl.nosecuresmtp"));
+ smtpPortCheckBox.setSelected(Settings.getProperty("davmail.smtpPort") != null && Settings.getProperty("davmail.smtpPort").length() > 0);
+ smtpPortField.setEnabled(smtpPortCheckBox.isSelected());
+ smtpNoSSLCheckBox.setEnabled(smtpPortCheckBox.isSelected() && isSslEnabled());
+ smtpPortCheckBox.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent evt) {
+ smtpPortField.setEnabled(smtpPortCheckBox.isSelected());
+ smtpNoSSLCheckBox.setEnabled(smtpPortCheckBox.isSelected() && isSslEnabled());
+ }
+ });
+
+ caldavPortField = new JTextField(Settings.getProperty("davmail.caldavPort"), 4);
+ caldavPortCheckBox = new JCheckBox();
+ caldavNoSSLCheckBox = new JCheckBox(BundleMessage.format("UI_NO_SSL"), Settings.getBooleanProperty("davmail.ssl.nosecurecaldav"));
+ caldavPortCheckBox.setSelected(Settings.getProperty("davmail.caldavPort") != null && Settings.getProperty("davmail.caldavPort").length() > 0);
+ caldavPortField.setEnabled(caldavPortCheckBox.isSelected());
+ caldavNoSSLCheckBox.setEnabled(caldavPortCheckBox.isSelected() && isSslEnabled());
+ caldavPortCheckBox.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent evt) {
+ caldavPortField.setEnabled(caldavPortCheckBox.isSelected());
+ caldavNoSSLCheckBox.setEnabled(caldavPortCheckBox.isSelected() && isSslEnabled());
+ }
+ });
+
+ ldapPortField = new JTextField(Settings.getProperty("davmail.ldapPort"), 4);
+ ldapPortCheckBox = new JCheckBox();
+ ldapNoSSLCheckBox = new JCheckBox(BundleMessage.format("UI_NO_SSL"), Settings.getBooleanProperty("davmail.ssl.nosecureldap"));
+ ldapPortCheckBox.setSelected(Settings.getProperty("davmail.ldapPort") != null && Settings.getProperty("davmail.ldapPort").length() > 0);
+ ldapPortField.setEnabled(ldapPortCheckBox.isSelected());
+ ldapNoSSLCheckBox.setEnabled(ldapPortCheckBox.isSelected() && isSslEnabled());
+ ldapPortCheckBox.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent evt) {
+ ldapPortField.setEnabled(ldapPortCheckBox.isSelected());
+ ldapNoSSLCheckBox.setEnabled(ldapPortCheckBox.isSelected() && isSslEnabled());
+ }
+ });
+
+ addSettingComponent(settingsPanel, BundleMessage.format("UI_OWA_URL"), urlField, BundleMessage.format("UI_OWA_URL_HELP"));
+ addPortSettingComponent(settingsPanel, BundleMessage.format("UI_POP_PORT"), popPortField, popPortCheckBox,
+ popNoSSLCheckBox, BundleMessage.format("UI_POP_PORT_HELP"));
+ addPortSettingComponent(settingsPanel, BundleMessage.format("UI_IMAP_PORT"), imapPortField, imapPortCheckBox,
+ imapNoSSLCheckBox, BundleMessage.format("UI_IMAP_PORT_HELP"));
+ addPortSettingComponent(settingsPanel, BundleMessage.format("UI_SMTP_PORT"), smtpPortField, smtpPortCheckBox,
+ smtpNoSSLCheckBox, BundleMessage.format("UI_SMTP_PORT_HELP"));
+ addPortSettingComponent(settingsPanel, BundleMessage.format("UI_CALDAV_PORT"), caldavPortField, caldavPortCheckBox,
+ caldavNoSSLCheckBox, BundleMessage.format("UI_CALDAV_PORT_HELP"));
+ addPortSettingComponent(settingsPanel, BundleMessage.format("UI_LDAP_PORT"), ldapPortField, ldapPortCheckBox,
+ ldapNoSSLCheckBox, BundleMessage.format("UI_LDAP_PORT_HELP"));
+ return settingsPanel;
+ }
+
+ protected JPanel getDelaysPanel() {
+ JPanel delaysPanel = new JPanel(new GridLayout(4, 2));
+ delaysPanel.setBorder(BorderFactory.createTitledBorder(BundleMessage.format("UI_DELAYS")));
+
+ keepDelayField = new JTextField(Settings.getProperty("davmail.keepDelay"), 4);
+ sentKeepDelayField = new JTextField(Settings.getProperty("davmail.sentKeepDelay"), 4);
+ caldavPastDelayField = new JTextField(Settings.getProperty("davmail.caldavPastDelay"), 4);
+ imapIdleDelayField = new JTextField(Settings.getProperty("davmail.imapIdleDelay"), 4);
+
+ addSettingComponent(delaysPanel, BundleMessage.format("UI_KEEP_DELAY"), keepDelayField,
+ BundleMessage.format("UI_KEEP_DELAY_HELP"));
+ addSettingComponent(delaysPanel, BundleMessage.format("UI_SENT_KEEP_DELAY"), sentKeepDelayField,
+ BundleMessage.format("UI_SENT_KEEP_DELAY_HELP"));
+ addSettingComponent(delaysPanel, BundleMessage.format("UI_CALENDAR_PAST_EVENTS"), caldavPastDelayField,
+ BundleMessage.format("UI_CALENDAR_PAST_EVENTS_HELP"));
+ addSettingComponent(delaysPanel, BundleMessage.format("UI_IMAP_IDLE_DELAY"), imapIdleDelayField,
+ BundleMessage.format("UI_IMAP_IDLE_DELAY_HELP"));
+ return delaysPanel;
+ }
+
+ protected JPanel getProxyPanel() {
+ JPanel proxyPanel = new JPanel(new GridLayout(6, 2));
+ proxyPanel.setBorder(BorderFactory.createTitledBorder(BundleMessage.format("UI_PROXY")));
+
+ boolean useSystemProxies = Settings.getBooleanProperty("davmail.useSystemProxies", Boolean.FALSE);
+ boolean enableProxy = Settings.getBooleanProperty("davmail.enableProxy");
+ useSystemProxiesField = new JCheckBox();
+ useSystemProxiesField.setSelected(useSystemProxies);
+ enableProxyField = new JCheckBox();
+ enableProxyField.setSelected(enableProxy);
+ httpProxyField = new JTextField(Settings.getProperty("davmail.proxyHost"), 15);
+ httpProxyPortField = new JTextField(Settings.getProperty("davmail.proxyPort"), 4);
+ httpProxyUserField = new JTextField(Settings.getProperty("davmail.proxyUser"), 10);
+ httpProxyPasswordField = new JPasswordField(Settings.getProperty("davmail.proxyPassword"), 10);
+
+ enableProxyField.setEnabled(!useSystemProxies);
+ httpProxyField.setEnabled(enableProxy);
+ httpProxyPortField.setEnabled(enableProxy);
+ httpProxyUserField.setEnabled(enableProxy || useSystemProxies);
+ httpProxyPasswordField.setEnabled(enableProxy || useSystemProxies);
+
+ useSystemProxiesField.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent evt) {
+ boolean newUseSystemProxies = useSystemProxiesField.isSelected();
+ boolean newEnableProxy = enableProxyField.isSelected();
+ enableProxyField.setEnabled(!newUseSystemProxies);
+ httpProxyField.setEnabled(!newUseSystemProxies && newEnableProxy);
+ httpProxyPortField.setEnabled(!newUseSystemProxies && newEnableProxy);
+ httpProxyUserField.setEnabled(newUseSystemProxies || newEnableProxy);
+ httpProxyPasswordField.setEnabled(newUseSystemProxies || newEnableProxy);
+ }
+ });
+ enableProxyField.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent evt) {
+ boolean newEnableProxy = enableProxyField.isSelected();
+ httpProxyField.setEnabled(newEnableProxy);
+ httpProxyPortField.setEnabled(newEnableProxy);
+ httpProxyUserField.setEnabled(newEnableProxy);
+ httpProxyPasswordField.setEnabled(newEnableProxy);
+ }
+ });
+
+ addSettingComponent(proxyPanel, BundleMessage.format("UI_USE_SYSTEM_PROXIES"), useSystemProxiesField);
+ addSettingComponent(proxyPanel, BundleMessage.format("UI_ENABLE_PROXY"), enableProxyField);
+ addSettingComponent(proxyPanel, BundleMessage.format("UI_PROXY_SERVER"), httpProxyField);
+ addSettingComponent(proxyPanel, BundleMessage.format("UI_PROXY_PORT"), httpProxyPortField);
+ addSettingComponent(proxyPanel, BundleMessage.format("UI_PROXY_USER"), httpProxyUserField);
+ addSettingComponent(proxyPanel, BundleMessage.format("UI_PROXY_PASSWORD"), httpProxyPasswordField);
+ updateMaximumSize(proxyPanel);
+ return proxyPanel;
+ }
+
+ protected JPanel getKeystorePanel() {
+ JPanel keyStorePanel = new JPanel(new GridLayout(4, 2));
+ keyStorePanel.setBorder(BorderFactory.createTitledBorder(BundleMessage.format("UI_DAVMAIL_SERVER_CERTIFICATE")));
+
+ keystoreTypeCombo = new JComboBox(new String[]{"JKS", "PKCS12"});
+ keystoreTypeCombo.setSelectedItem(Settings.getProperty("davmail.ssl.keystoreType"));
+ keystoreFileField = new JTextField(Settings.getProperty("davmail.ssl.keystoreFile"), 17);
+ keystorePassField = new JPasswordField(Settings.getProperty("davmail.ssl.keystorePass"), 15);
+ keyPassField = new JPasswordField(Settings.getProperty("davmail.ssl.keyPass"), 15);
+
+ addSettingComponent(keyStorePanel, BundleMessage.format("UI_KEY_STORE_TYPE"), keystoreTypeCombo,
+ BundleMessage.format("UI_KEY_STORE_TYPE_HELP"));
+ addSettingComponent(keyStorePanel, BundleMessage.format("UI_KEY_STORE"), keystoreFileField,
+ BundleMessage.format("UI_KEY_STORE_HELP"));
+ addSettingComponent(keyStorePanel, BundleMessage.format("UI_KEY_STORE_PASSWORD"), keystorePassField,
+ BundleMessage.format("UI_KEY_STORE_PASSWORD_HELP"));
+ addSettingComponent(keyStorePanel, BundleMessage.format("UI_KEY_PASSWORD"), keyPassField,
+ BundleMessage.format("UI_KEY_PASSWORD_HELP"));
+ updateMaximumSize(keyStorePanel);
+ return keyStorePanel;
+ }
+
+ protected JPanel getSmartCardPanel() {
+ JPanel clientKeystorePanel = new JPanel(new GridLayout(2, 1));
+ clientKeystorePanel.setLayout(new BoxLayout(clientKeystorePanel, BoxLayout.Y_AXIS));
+ clientKeystorePanel.setBorder(BorderFactory.createTitledBorder(BundleMessage.format("UI_CLIENT_CERTIFICATE")));
+
+ clientKeystoreTypeCombo = new JComboBox(new String[]{"PKCS11", "JKS", "PKCS12"});
+ clientKeystoreTypeCombo.setSelectedItem(Settings.getProperty("davmail.ssl.clientKeystoreType"));
+ clientKeystoreFileField = new JTextField(Settings.getProperty("davmail.ssl.clientKeystoreFile"), 17);
+ clientKeystorePassField = new JPasswordField(Settings.getProperty("davmail.ssl.clientKeystorePass"), 15);
+
+ pkcs11LibraryField = new JTextField(Settings.getProperty("davmail.ssl.pkcs11Library"), 17);
+ pkcs11ConfigField = new JTextArea(2, 17);
+ pkcs11ConfigField.setText(Settings.getProperty("davmail.ssl.pkcs11Config"));
+ pkcs11ConfigField.setBorder(pkcs11LibraryField.getBorder());
+ pkcs11ConfigField.setFont(pkcs11LibraryField.getFont());
+
+ JPanel clientKeystoreTypePanel = new JPanel(new GridLayout(1, 2));
+ addSettingComponent(clientKeystoreTypePanel, BundleMessage.format("UI_CLIENT_KEY_STORE_TYPE"), clientKeystoreTypeCombo,
+ BundleMessage.format("UI_CLIENT_KEY_STORE_TYPE_HELP"));
+ clientKeystorePanel.add(clientKeystoreTypePanel);
+
+ final JPanel cardPanel = new JPanel(new CardLayout());
+ clientKeystorePanel.add(cardPanel);
+
+ JPanel clientKeystoreFilePanel = new JPanel(new GridLayout(2, 2));
+ addSettingComponent(clientKeystoreFilePanel, BundleMessage.format("UI_CLIENT_KEY_STORE"), clientKeystoreFileField,
+ BundleMessage.format("UI_CLIENT_KEY_STORE_HELP"));
+ addSettingComponent(clientKeystoreFilePanel, BundleMessage.format("UI_CLIENT_KEY_STORE_PASSWORD"), clientKeystorePassField,
+ BundleMessage.format("UI_CLIENT_KEY_STORE_PASSWORD_HELP"));
+ cardPanel.add(clientKeystoreFilePanel, "FILE");
+
+ JPanel pkcs11Panel = new JPanel(new GridLayout(2, 2));
+ addSettingComponent(pkcs11Panel, BundleMessage.format("UI_PKCS11_LIBRARY"), pkcs11LibraryField,
+ BundleMessage.format("UI_PKCS11_LIBRARY_HELP"));
+ addSettingComponent(pkcs11Panel, BundleMessage.format("UI_PKCS11_CONFIG"), pkcs11ConfigField,
+ BundleMessage.format("UI_PKCS11_CONFIG_HELP"));
+ cardPanel.add(pkcs11Panel, "PKCS11");
+
+ ((CardLayout) cardPanel.getLayout()).show(cardPanel, (String) clientKeystoreTypeCombo.getSelectedItem());
+
+ clientKeystoreTypeCombo.addItemListener(new ItemListener() {
+ public void itemStateChanged(ItemEvent event) {
+ CardLayout cardLayout = (CardLayout) (cardPanel.getLayout());
+ if ("PKCS11".equals(event.getItem())) {
+ cardLayout.show(cardPanel, "PKCS11");
+ } else {
+ cardLayout.show(cardPanel, "FILE");
+ }
+ }
+ });
+ updateMaximumSize(clientKeystorePanel);
+ return clientKeystorePanel;
+ }
+
+ protected JPanel getNetworkSettingsPanel() {
+ JPanel networkSettingsPanel = new JPanel(new GridLayout(4, 2));
+ networkSettingsPanel.setBorder(BorderFactory.createTitledBorder(BundleMessage.format("UI_NETWORK")));
+
+ allowRemoteField = new JCheckBox();
+ allowRemoteField.setSelected(Settings.getBooleanProperty("davmail.allowRemote"));
+
+ bindAddressField = new JTextField(Settings.getProperty("davmail.bindAddress"), 15);
+ clientSoTimeoutField = new JTextField(Settings.getProperty("davmail.clientSoTimeout"), 15);
+
+ certHashField = new JTextField(Settings.getProperty("davmail.server.certificate.hash"), 15);
+
+ addSettingComponent(networkSettingsPanel, BundleMessage.format("UI_BIND_ADDRESS"), bindAddressField,
+ BundleMessage.format("UI_BIND_ADDRESS_HELP"));
+ addSettingComponent(networkSettingsPanel, BundleMessage.format("UI_CLIENT_SO_TIMEOUT"), clientSoTimeoutField,
+ BundleMessage.format("UI_CLIENT_SO_TIMEOUT_HELP"));
+ addSettingComponent(networkSettingsPanel, BundleMessage.format("UI_ALLOW_REMOTE_CONNECTION"), allowRemoteField,
+ BundleMessage.format("UI_ALLOW_REMOTE_CONNECTION_HELP"));
+ addSettingComponent(networkSettingsPanel, BundleMessage.format("UI_SERVER_CERTIFICATE_HASH"), certHashField,
+ BundleMessage.format("UI_SERVER_CERTIFICATE_HASH_HELP"));
+ updateMaximumSize(networkSettingsPanel);
+ return networkSettingsPanel;
+ }
+
+ protected static final String WEBDAV = "WebDav";
+ protected static final String EWS = "EWS";
+ protected static final String AUTO = "Auto";
+
+ protected void setEwsModeSelectedItem(String ewsMode) {
+ if ("true".equals(ewsMode)) {
+ enableEwsComboBox.setSelectedItem(EWS);
+ } else if ("false".equals(ewsMode)) {
+ enableEwsComboBox.setSelectedItem(WEBDAV);
+ } else {
+ enableEwsComboBox.setSelectedItem(AUTO);
+ }
+ }
+
+ protected JPanel getOtherSettingsPanel() {
+ JPanel otherSettingsPanel = new JPanel(new GridLayout(11, 2));
+ otherSettingsPanel.setBorder(BorderFactory.createTitledBorder(BundleMessage.format("UI_OTHER")));
+
+ enableEwsComboBox = new JComboBox(new String[]{WEBDAV, EWS, AUTO});
+ setEwsModeSelectedItem(Settings.getProperty("davmail.enableEws", "auto"));
+ caldavEditNotificationsField = new JCheckBox();
+ caldavEditNotificationsField.setSelected(Settings.getBooleanProperty("davmail.caldavEditNotifications"));
+ caldavAlarmSoundField = new JTextField(Settings.getProperty("davmail.caldavAlarmSound"), 15);
+ forceActiveSyncUpdateCheckBox = new JCheckBox();
+ forceActiveSyncUpdateCheckBox.setSelected(Settings.getBooleanProperty("davmail.forceActiveSyncUpdate"));
+ defaultDomainField = new JTextField(Settings.getProperty("davmail.defaultDomain"), 15);
+ showStartupBannerCheckBox = new JCheckBox();
+ showStartupBannerCheckBox.setSelected(Settings.getBooleanProperty("davmail.showStartupBanner", true));
+ disableGuiNotificationsCheckBox = new JCheckBox();
+ disableGuiNotificationsCheckBox.setSelected(Settings.getBooleanProperty("davmail.disableGuiNotifications", false));
+ imapAutoExpungeCheckBox = new JCheckBox();
+ imapAutoExpungeCheckBox.setSelected(Settings.getBooleanProperty("davmail.imapAutoExpunge", true));
+ popMarkReadOnRetrCheckBox = new JCheckBox();
+ popMarkReadOnRetrCheckBox.setSelected(Settings.getBooleanProperty("davmail.popMarkReadOnRetr", false));
+ smtpSaveInSentCheckBox = new JCheckBox();
+ smtpSaveInSentCheckBox.setSelected(Settings.getBooleanProperty("davmail.smtpSaveInSent", true));
+ disableUpdateCheck = new JCheckBox();
+ disableUpdateCheck.setSelected(Settings.getBooleanProperty("davmail.disableUpdateCheck"));
+
+ addSettingComponent(otherSettingsPanel, BundleMessage.format("UI_ENABLE_EWS"), enableEwsComboBox,
+ BundleMessage.format("UI_ENABLE_EWS_HELP"));
+ addSettingComponent(otherSettingsPanel, BundleMessage.format("UI_CALDAV_EDIT_NOTIFICATIONS"), caldavEditNotificationsField,
+ BundleMessage.format("UI_CALDAV_EDIT_NOTIFICATIONS_HELP"));
+ addSettingComponent(otherSettingsPanel, BundleMessage.format("UI_CALDAV_ALARM_SOUND"), caldavAlarmSoundField,
+ BundleMessage.format("UI_CALDAV_ALARM_SOUND_HELP"));
+ addSettingComponent(otherSettingsPanel, BundleMessage.format("UI_FORCE_ACTIVESYNC_UPDATE"), forceActiveSyncUpdateCheckBox,
+ BundleMessage.format("UI_FORCE_ACTIVESYNC_UPDATE_HELP"));
+ addSettingComponent(otherSettingsPanel, BundleMessage.format("UI_DEFAULT_DOMAIN"), defaultDomainField,
+ BundleMessage.format("UI_DEFAULT_DOMAIN_HELP"));
+ addSettingComponent(otherSettingsPanel, BundleMessage.format("UI_SHOW_STARTUP_BANNER"), showStartupBannerCheckBox,
+ BundleMessage.format("UI_SHOW_STARTUP_BANNER_HELP"));
+ addSettingComponent(otherSettingsPanel, BundleMessage.format("UI_DISABLE_GUI_NOTIFICATIONS"), disableGuiNotificationsCheckBox,
+ BundleMessage.format("UI_DISABLE_GUI_NOTIFICATIONS_HELP"));
+ addSettingComponent(otherSettingsPanel, BundleMessage.format("UI_IMAP_AUTO_EXPUNGE"), imapAutoExpungeCheckBox,
+ BundleMessage.format("UI_IMAP_AUTO_EXPUNGE_HELP"));
+ addSettingComponent(otherSettingsPanel, BundleMessage.format("UI_POP_MARK_READ"), popMarkReadOnRetrCheckBox,
+ BundleMessage.format("UI_POP_MARK_READ_HELP"));
+ addSettingComponent(otherSettingsPanel, BundleMessage.format("UI_SAVE_IN_SENT"), smtpSaveInSentCheckBox,
+ BundleMessage.format("UI_SAVE_IN_SENT_HELP"));
+ addSettingComponent(otherSettingsPanel, BundleMessage.format("UI_DISABLE_UPDATE_CHECK"), disableUpdateCheck,
+ BundleMessage.format("UI_DISABLE_UPDATE_CHECK_HELP"));
+
+ updateMaximumSize(otherSettingsPanel);
+ return otherSettingsPanel;
+ }
+
+ protected JPanel getOSXPanel() {
+ JPanel osxSettingsPanel = new JPanel(new GridLayout(1, 2));
+ osxSettingsPanel.setBorder(BorderFactory.createTitledBorder(BundleMessage.format("UI_OSX")));
+
+ osxHideFromDockCheckBox = new JCheckBox();
+ osxHideFromDockCheckBox.setSelected(OSXInfoPlist.isHideFromDock());
+
+ addSettingComponent(osxSettingsPanel, BundleMessage.format("UI_OSX_HIDE_FROM_DOCK"), osxHideFromDockCheckBox,
+ BundleMessage.format("UI_OSX_HIDE_FROM_DOCK_HELP"));
+
+ updateMaximumSize(osxSettingsPanel);
+ return osxSettingsPanel;
+ }
+
+ protected JPanel getLoggingSettingsPanel() {
+ JPanel loggingLevelPanel = new JPanel();
+ JPanel leftLoggingPanel = new JPanel(new GridLayout(2, 2));
+ JPanel rightLoggingPanel = new JPanel(new GridLayout(2, 2));
+ loggingLevelPanel.add(leftLoggingPanel);
+ loggingLevelPanel.add(rightLoggingPanel);
+
+ rootLoggingLevelField = new JComboBox(LOG_LEVELS);
+ davmailLoggingLevelField = new JComboBox(LOG_LEVELS);
+ httpclientLoggingLevelField = new JComboBox(LOG_LEVELS);
+ wireLoggingLevelField = new JComboBox(LOG_LEVELS);
+ logFilePathField = new JTextField(Settings.getProperty("davmail.logFilePath"), 15);
+ logFileSizeField = new JTextField(Settings.getProperty("davmail.logFileSize"), 15);
+
+ rootLoggingLevelField.setSelectedItem(Settings.getLoggingLevel("rootLogger"));
+ davmailLoggingLevelField.setSelectedItem(Settings.getLoggingLevel("davmail"));
+ httpclientLoggingLevelField.setSelectedItem(Settings.getLoggingLevel("org.apache.commons.httpclient"));
+ wireLoggingLevelField.setSelectedItem(Settings.getLoggingLevel("httpclient.wire"));
+
+ addSettingComponent(leftLoggingPanel, BundleMessage.format("UI_LOG_DEFAULT"), rootLoggingLevelField);
+ addSettingComponent(leftLoggingPanel, BundleMessage.format("UI_LOG_DAVMAIL"), davmailLoggingLevelField);
+ addSettingComponent(rightLoggingPanel, BundleMessage.format("UI_LOG_HTTPCLIENT"), httpclientLoggingLevelField);
+ addSettingComponent(rightLoggingPanel, BundleMessage.format("UI_LOG_WIRE"), wireLoggingLevelField);
+
+ JPanel logFilePathPanel = new JPanel(new GridLayout(2, 2));
+ addSettingComponent(logFilePathPanel, BundleMessage.format("UI_LOG_FILE_PATH"), logFilePathField);
+ addSettingComponent(logFilePathPanel, BundleMessage.format("UI_LOG_FILE_SIZE"), logFileSizeField);
+
+ JButton defaultButton = new JButton(BundleMessage.format("UI_BUTTON_DEFAULT"));
+ defaultButton.setToolTipText(BundleMessage.format("UI_BUTTON_DEFAULT_HELP"));
+ defaultButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ rootLoggingLevelField.setSelectedItem(Level.WARN);
+ davmailLoggingLevelField.setSelectedItem(Level.DEBUG);
+ httpclientLoggingLevelField.setSelectedItem(Level.WARN);
+ wireLoggingLevelField.setSelectedItem(Level.WARN);
+ }
+ });
+
+ JPanel loggingPanel = new JPanel();
+ loggingPanel.setLayout(new BoxLayout(loggingPanel, BoxLayout.Y_AXIS));
+ loggingPanel.setBorder(BorderFactory.createTitledBorder(BundleMessage.format("UI_LOGGING_LEVELS")));
+ loggingPanel.add(logFilePathPanel);
+ loggingPanel.add(loggingLevelPanel);
+ loggingPanel.add(defaultButton);
+
+ updateMaximumSize(loggingPanel);
+ return loggingPanel;
+ }
+
+ protected void updateMaximumSize(JPanel panel) {
+ Dimension preferredSize = panel.getPreferredSize();
+ preferredSize.width = Integer.MAX_VALUE;
+ panel.setMaximumSize(preferredSize);
+ }
+
+ /**
+ * Reload settings from properties.
+ */
+ public void reload() {
+ // reload settings in form
+ urlField.setText(Settings.getProperty("davmail.url"));
+ popPortField.setText(Settings.getProperty("davmail.popPort"));
+ popPortCheckBox.setSelected(Settings.getProperty("davmail.popPort") != null && Settings.getProperty("davmail.popPort").length() > 0);
+ popNoSSLCheckBox.setSelected(Settings.getBooleanProperty("davmail.ssl.nosecurepop"));
+ imapPortField.setText(Settings.getProperty("davmail.imapPort"));
+ imapPortCheckBox.setSelected(Settings.getProperty("davmail.imapPort") != null && Settings.getProperty("davmail.imapPort").length() > 0);
+ imapNoSSLCheckBox.setSelected(Settings.getBooleanProperty("davmail.ssl.nosecureimap"));
+ smtpPortField.setText(Settings.getProperty("davmail.smtpPort"));
+ smtpPortCheckBox.setSelected(Settings.getProperty("davmail.smtpPort") != null && Settings.getProperty("davmail.smtpPort").length() > 0);
+ smtpNoSSLCheckBox.setSelected(Settings.getBooleanProperty("davmail.ssl.nosecuresmtp"));
+ caldavPortField.setText(Settings.getProperty("davmail.caldavPort"));
+ caldavPortCheckBox.setSelected(Settings.getProperty("davmail.caldavPort") != null && Settings.getProperty("davmail.caldavPort").length() > 0);
+ caldavNoSSLCheckBox.setSelected(Settings.getBooleanProperty("davmail.ssl.nosecurecaldav"));
+ ldapPortField.setText(Settings.getProperty("davmail.ldapPort"));
+ ldapPortCheckBox.setSelected(Settings.getProperty("davmail.ldapPort") != null && Settings.getProperty("davmail.ldapPort").length() > 0);
+ ldapNoSSLCheckBox.setSelected(Settings.getBooleanProperty("davmail.ssl.nosecureldap"));
+ keepDelayField.setText(Settings.getProperty("davmail.keepDelay"));
+ sentKeepDelayField.setText(Settings.getProperty("davmail.sentKeepDelay"));
+ caldavPastDelayField.setText(Settings.getProperty("davmail.caldavPastDelay"));
+ imapIdleDelayField.setText(Settings.getProperty("davmail.imapIdleDelay"));
+ boolean useSystemProxies = Settings.getBooleanProperty("davmail.useSystemProxies", Boolean.FALSE);
+ useSystemProxiesField.setSelected(useSystemProxies);
+ boolean enableProxy = Settings.getBooleanProperty("davmail.enableProxy");
+ enableProxyField.setSelected(enableProxy);
+ enableProxyField.setEnabled(!useSystemProxies);
+ httpProxyField.setEnabled(!useSystemProxies && enableProxy);
+ httpProxyPortField.setEnabled(!useSystemProxies && enableProxy);
+ httpProxyUserField.setEnabled(useSystemProxies || enableProxy);
+ httpProxyPasswordField.setEnabled(useSystemProxies || enableProxy);
+ httpProxyField.setText(Settings.getProperty("davmail.proxyHost"));
+ httpProxyPortField.setText(Settings.getProperty("davmail.proxyPort"));
+ httpProxyUserField.setText(Settings.getProperty("davmail.proxyUser"));
+ httpProxyPasswordField.setText(Settings.getProperty("davmail.proxyPassword"));
+
+ bindAddressField.setText(Settings.getProperty("davmail.bindAddress"));
+ allowRemoteField.setSelected(Settings.getBooleanProperty(("davmail.allowRemote")));
+ certHashField.setText(Settings.getProperty("davmail.server.certificate.hash"));
+ disableUpdateCheck.setSelected(Settings.getBooleanProperty(("davmail.disableUpdateCheck")));
+
+ caldavEditNotificationsField.setSelected(Settings.getBooleanProperty("davmail.caldavEditNotifications"));
+ clientSoTimeoutField.setText(Settings.getProperty("davmail.clientSoTimeout"));
+ caldavAlarmSoundField.setText(Settings.getProperty("davmail.caldavAlarmSound"));
+ forceActiveSyncUpdateCheckBox.setSelected(Settings.getBooleanProperty("davmail.forceActiveSyncUpdate"));
+ defaultDomainField.setText(Settings.getProperty("davmail.defaultDomain"));
+ showStartupBannerCheckBox.setSelected(Settings.getBooleanProperty("davmail.showStartupBanner", true));
+ disableGuiNotificationsCheckBox.setSelected(Settings.getBooleanProperty("davmail.disableGuiNotifications", false));
+ imapAutoExpungeCheckBox.setSelected(Settings.getBooleanProperty("davmail.imapAutoExpunge", true));
+ popMarkReadOnRetrCheckBox.setSelected(Settings.getBooleanProperty("davmail.popMarkReadOnRetrCheckBox", false));
+ setEwsModeSelectedItem(Settings.getProperty("davmail.enableEws", "auto"));
+ smtpSaveInSentCheckBox.setSelected(Settings.getBooleanProperty("davmail.smtpSaveInSent", true));
+
+ keystoreTypeCombo.setSelectedItem(Settings.getProperty("davmail.ssl.keystoreType"));
+ keystoreFileField.setText(Settings.getProperty("davmail.ssl.keystoreFile"));
+ keystorePassField.setText(Settings.getProperty("davmail.ssl.keystorePass"));
+ keyPassField.setText(Settings.getProperty("davmail.ssl.keyPass"));
+
+ clientKeystoreTypeCombo.setSelectedItem(Settings.getProperty("davmail.ssl.clientKeystoreType"));
+ pkcs11LibraryField.setText(Settings.getProperty("davmail.ssl.pkcs11Library"));
+ pkcs11ConfigField.setText(Settings.getProperty("davmail.ssl.pkcs11Config"));
+
+ rootLoggingLevelField.setSelectedItem(Settings.getLoggingLevel("rootLogger"));
+ davmailLoggingLevelField.setSelectedItem(Settings.getLoggingLevel("davmail"));
+ httpclientLoggingLevelField.setSelectedItem(Settings.getLoggingLevel("org.apache.commons.httpclient"));
+ wireLoggingLevelField.setSelectedItem(Settings.getLoggingLevel("httpclient.wire"));
+ logFilePathField.setText(Settings.getProperty("davmail.logFilePath"));
+ logFileSizeField.setText(Settings.getProperty("davmail.logFileSize"));
+
+ if (osxHideFromDockCheckBox != null) {
+ osxHideFromDockCheckBox.setSelected(OSXInfoPlist.isHideFromDock());
+ }
+ }
+
+ protected boolean isSslEnabled() {
+ if (keystoreFileField != null) {
+ return keystoreFileField.getText().length() > 0;
+ } else {
+ return Settings.getProperty("davmail.ssl.keystoreFile") != null &&
+ (Settings.getProperty("davmail.ssl.keystoreFile").length() > 0);
+ }
+ }
+
+ /**
+ * DavMail settings frame.
+ */
+ public SettingsFrame() {
+ setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
+ setTitle(BundleMessage.format("UI_DAVMAIL_SETTINGS"));
+ try {
+ setIconImage(DavGatewayTray.getFrameIcon());
+ } catch (NoSuchMethodError error) {
+ DavGatewayTray.debug(new BundleMessage("LOG_UNABLE_TO_SET_ICON_IMAGE"));
+ }
+
+ JTabbedPane tabbedPane = new JTabbedPane();
+ // add help (F1 handler)
+ tabbedPane.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke("F1"),
+ "help");
+ tabbedPane.getActionMap().put("help", new AbstractAction() {
+ public void actionPerformed(ActionEvent e) {
+ DesktopBrowser.browse("http://davmail.sourceforge.net");
+ }
+ });
+ tabbedPane.addChangeListener(new ChangeListener() {
+ public void stateChanged(ChangeEvent e) {
+ boolean isSslEnabled = isSslEnabled();
+ popNoSSLCheckBox.setEnabled(Settings.getProperty("davmail.popPort") != null && isSslEnabled);
+ imapNoSSLCheckBox.setEnabled(imapPortCheckBox.isSelected() && isSslEnabled);
+ smtpNoSSLCheckBox.setEnabled(smtpPortCheckBox.isSelected() && isSslEnabled);
+ caldavNoSSLCheckBox.setEnabled(caldavPortCheckBox.isSelected() && isSslEnabled);
+ ldapNoSSLCheckBox.setEnabled(ldapPortCheckBox.isSelected() && isSslEnabled);
+ }
+ });
+
+ JPanel mainPanel = new JPanel();
+ mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
+ mainPanel.add(getSettingsPanel());
+ mainPanel.add(getDelaysPanel());
+ mainPanel.add(Box.createVerticalGlue());
+
+ tabbedPane.add(BundleMessage.format("UI_TAB_MAIN"), mainPanel);
+
+ JPanel proxyPanel = new JPanel();
+ proxyPanel.setLayout(new BoxLayout(proxyPanel, BoxLayout.Y_AXIS));
+ proxyPanel.add(getProxyPanel());
+ proxyPanel.add(getNetworkSettingsPanel());
+ tabbedPane.add(BundleMessage.format("UI_TAB_NETWORK"), proxyPanel);
+
+ JPanel encryptionPanel = new JPanel();
+ encryptionPanel.setLayout(new BoxLayout(encryptionPanel, BoxLayout.Y_AXIS));
+ encryptionPanel.add(getKeystorePanel());
+ encryptionPanel.add(getSmartCardPanel());
+ // empty panel
+ encryptionPanel.add(new JPanel());
+ tabbedPane.add(BundleMessage.format("UI_TAB_ENCRYPTION"), encryptionPanel);
+
+ JPanel loggingPanel = new JPanel();
+ loggingPanel.setLayout(new BoxLayout(loggingPanel, BoxLayout.Y_AXIS));
+ loggingPanel.add(getLoggingSettingsPanel());
+ // empty panel
+ loggingPanel.add(new JPanel());
+
+ tabbedPane.add(BundleMessage.format("UI_TAB_LOGGING"), loggingPanel);
+
+ JPanel advancedPanel = new JPanel();
+ advancedPanel.setLayout(new BoxLayout(advancedPanel, BoxLayout.Y_AXIS));
+
+ advancedPanel.add(getOtherSettingsPanel());
+ // empty panel
+ advancedPanel.add(new JPanel());
+
+ tabbedPane.add(BundleMessage.format("UI_TAB_ADVANCED"), advancedPanel);
+
+ if (OSXInfoPlist.isOSX()) {
+ JPanel osxPanel = new JPanel();
+ osxPanel.setLayout(new BoxLayout(osxPanel, BoxLayout.Y_AXIS));
+ osxPanel.add(getOSXPanel());
+ // empty panel
+ osxPanel.add(new JPanel());
+
+ tabbedPane.add(BundleMessage.format("UI_TAB_OSX"), osxPanel);
+ }
+
+ add(BorderLayout.CENTER, tabbedPane);
+
+ JPanel buttonPanel = new JPanel();
+ JButton cancel = new JButton(BundleMessage.format("UI_BUTTON_CANCEL"));
+ JButton ok = new JButton(BundleMessage.format("UI_BUTTON_SAVE"));
+ JButton help = new JButton(BundleMessage.format("UI_BUTTON_HELP"));
+ ActionListener save = new ActionListener() {
+ public void actionPerformed(ActionEvent evt) {
+ // save options
+ Settings.setProperty("davmail.url", urlField.getText());
+ Settings.setProperty("davmail.popPort", popPortCheckBox.isSelected() ? popPortField.getText() : "");
+ Settings.setProperty("davmail.ssl.nosecurepop", String.valueOf(popNoSSLCheckBox.isSelected()));
+ Settings.setProperty("davmail.imapPort", imapPortCheckBox.isSelected() ? imapPortField.getText() : "");
+ Settings.setProperty("davmail.ssl.nosecureimap", String.valueOf(imapNoSSLCheckBox.isSelected()));
+ Settings.setProperty("davmail.smtpPort", smtpPortCheckBox.isSelected() ? smtpPortField.getText() : "");
+ Settings.setProperty("davmail.ssl.nosecuresmtp", String.valueOf(smtpNoSSLCheckBox.isSelected()));
+ Settings.setProperty("davmail.caldavPort", caldavPortCheckBox.isSelected() ? caldavPortField.getText() : "");
+ Settings.setProperty("davmail.ssl.nosecurecaldav", String.valueOf(caldavNoSSLCheckBox.isSelected()));
+ Settings.setProperty("davmail.ldapPort", ldapPortCheckBox.isSelected() ? ldapPortField.getText() : "");
+ Settings.setProperty("davmail.ssl.nosecureldap", String.valueOf(ldapNoSSLCheckBox.isSelected()));
+ Settings.setProperty("davmail.keepDelay", keepDelayField.getText());
+ Settings.setProperty("davmail.sentKeepDelay", sentKeepDelayField.getText());
+ Settings.setProperty("davmail.caldavPastDelay", caldavPastDelayField.getText());
+ Settings.setProperty("davmail.imapIdleDelay", imapIdleDelayField.getText());
+ Settings.setProperty("davmail.useSystemProxies", String.valueOf(useSystemProxiesField.isSelected()));
+ Settings.setProperty("davmail.enableProxy", String.valueOf(enableProxyField.isSelected()));
+ Settings.setProperty("davmail.proxyHost", httpProxyField.getText());
+ Settings.setProperty("davmail.proxyPort", httpProxyPortField.getText());
+ Settings.setProperty("davmail.proxyUser", httpProxyUserField.getText());
+ Settings.setProperty("davmail.proxyPassword", httpProxyPasswordField.getText());
+
+ Settings.setProperty("davmail.bindAddress", bindAddressField.getText());
+ Settings.setProperty("davmail.clientSoTimeout", String.valueOf(clientSoTimeoutField.getText()));
+ Settings.setProperty("davmail.allowRemote", String.valueOf(allowRemoteField.isSelected()));
+ Settings.setProperty("davmail.server.certificate.hash", certHashField.getText());
+ Settings.setProperty("davmail.disableUpdateCheck", String.valueOf(disableUpdateCheck.isSelected()));
+
+ Settings.setProperty("davmail.caldavEditNotifications", String.valueOf(caldavEditNotificationsField.isSelected()));
+ Settings.setProperty("davmail.caldavAlarmSound", String.valueOf(caldavAlarmSoundField.getText()));
+ Settings.setProperty("davmail.forceActiveSyncUpdate", String.valueOf(forceActiveSyncUpdateCheckBox.isSelected()));
+ Settings.setProperty("davmail.defaultDomain", String.valueOf(defaultDomainField.getText()));
+ Settings.setProperty("davmail.showStartupBanner", String.valueOf(showStartupBannerCheckBox.isSelected()));
+ Settings.setProperty("davmail.disableGuiNotifications", String.valueOf(disableGuiNotificationsCheckBox.isSelected()));
+ Settings.setProperty("davmail.imapAutoExpunge", String.valueOf(imapAutoExpungeCheckBox.isSelected()));
+ Settings.setProperty("davmail.popMarkReadOnRetr", String.valueOf(popMarkReadOnRetrCheckBox.isSelected()));
+ String selectedEwsMode = (String) enableEwsComboBox.getSelectedItem();
+ String enableEws;
+ if (EWS.equals(selectedEwsMode)) {
+ enableEws = "true";
+ } else if (WEBDAV.equals(selectedEwsMode)) {
+ enableEws = "false";
+ } else {
+ enableEws = "auto";
+ }
+ Settings.setProperty("davmail.enableEws", enableEws);
+ Settings.setProperty("davmail.smtpSaveInSent", String.valueOf(smtpSaveInSentCheckBox.isSelected()));
+
+ Settings.setProperty("davmail.ssl.keystoreType", (String) keystoreTypeCombo.getSelectedItem());
+ Settings.setProperty("davmail.ssl.keystoreFile", keystoreFileField.getText());
+ Settings.setProperty("davmail.ssl.keystorePass", String.valueOf(keystorePassField.getPassword()));
+ Settings.setProperty("davmail.ssl.keyPass", String.valueOf(keyPassField.getPassword()));
+
+ Settings.setProperty("davmail.ssl.clientKeystoreType", (String) clientKeystoreTypeCombo.getSelectedItem());
+ Settings.setProperty("davmail.ssl.clientKeystoreFile", clientKeystoreFileField.getText());
+ Settings.setProperty("davmail.ssl.clientKeystorePass", String.valueOf(clientKeystorePassField.getPassword()));
+ Settings.setProperty("davmail.ssl.pkcs11Library", pkcs11LibraryField.getText());
+ Settings.setProperty("davmail.ssl.pkcs11Config", pkcs11ConfigField.getText());
+
+ Settings.setLoggingLevel("rootLogger", (Level) rootLoggingLevelField.getSelectedItem());
+ Settings.setLoggingLevel("davmail", (Level) davmailLoggingLevelField.getSelectedItem());
+ Settings.setLoggingLevel("org.apache.commons.httpclient", (Level) httpclientLoggingLevelField.getSelectedItem());
+ Settings.setLoggingLevel("httpclient.wire", (Level) wireLoggingLevelField.getSelectedItem());
+ Settings.setProperty("davmail.logFilePath", logFilePathField.getText());
+ Settings.setProperty("davmail.logFileSize", logFileSizeField.getText());
+
+ setVisible(false);
+ Settings.save();
+
+ if (osxHideFromDockCheckBox != null) {
+ OSXInfoPlist.setOSXHideFromDock(osxHideFromDockCheckBox.isSelected());
+ }
+
+ // restart listeners with new config
+ DavGateway.restart();
+ }
+ };
+ ok.addActionListener(save);
+
+ cancel.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent evt) {
+ reload();
+ setVisible(false);
+ }
+ });
+
+ help.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ DesktopBrowser.browse("http://davmail.sourceforge.net");
+ }
+ });
+
+ buttonPanel.add(ok);
+ buttonPanel.add(cancel);
+ buttonPanel.add(help);
+
+ add(BorderLayout.SOUTH, buttonPanel);
+
+ pack();
+ //setResizable(false);
+ // center frame
+ setLocation(getToolkit().getScreenSize().width / 2 -
+ getSize().width / 2,
+ getToolkit().getScreenSize().height / 2 -
+ getSize().height / 2);
+ urlField.requestFocus();
+ }
+}
diff --git a/src/java/davmail/ui/browser/AwtDesktopBrowser.java b/src/java/davmail/ui/browser/AwtDesktopBrowser.java
new file mode 100644
index 0000000..250dcbd
--- /dev/null
+++ b/src/java/davmail/ui/browser/AwtDesktopBrowser.java
@@ -0,0 +1,44 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.ui.browser;
+
+import java.awt.*;
+import java.io.IOException;
+import java.net.URI;
+
+/**
+ * Wrapper class to call Java6 Desktop class to launch default browser.
+ */
+public final class AwtDesktopBrowser {
+ private AwtDesktopBrowser() {
+ }
+
+ /**
+ * Open default browser at location URI.
+ * User Java 6 Desktop class
+ *
+ * @param location location URI
+ * @throws IOException on error
+ */
+ public static void browse(URI location) throws IOException {
+ Desktop desktop = Desktop.getDesktop();
+ desktop.browse(location);
+ }
+
+}
diff --git a/src/java/davmail/ui/browser/DesktopBrowser.java b/src/java/davmail/ui/browser/DesktopBrowser.java
new file mode 100644
index 0000000..b0626b0
--- /dev/null
+++ b/src/java/davmail/ui/browser/DesktopBrowser.java
@@ -0,0 +1,90 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.ui.browser;
+
+import davmail.BundleMessage;
+import davmail.ui.AboutFrame;
+import davmail.ui.tray.DavGatewayTray;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+/**
+ * Open default browser.
+ */
+public final class DesktopBrowser {
+ private DesktopBrowser() {
+ }
+
+ /**
+ * Open default browser at location URI.
+ * User Java 6 Desktop class, OSX open command or SWT program launch
+ *
+ * @param location location URI
+ */
+ public static void browse(URI location) {
+ try {
+ // trigger ClassNotFoundException
+ ClassLoader classloader = AboutFrame.class.getClassLoader();
+ classloader.loadClass("java.awt.Desktop");
+
+ // Open link in default browser
+ AwtDesktopBrowser.browse(location);
+ } catch (ClassNotFoundException e) {
+ DavGatewayTray.debug(new BundleMessage("LOG_JAVA6_DESKTOP_UNAVAILABLE"));
+ // failover for MacOSX
+ if (System.getProperty("os.name").toLowerCase().startsWith("mac os x")) {
+ try {
+ OSXDesktopBrowser.browse(location);
+ } catch (Exception e2) {
+ DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_OPEN_LINK"), e2);
+ }
+ } else {
+ // failover : try SWT
+ try {
+ // trigger ClassNotFoundException
+ ClassLoader classloader = AboutFrame.class.getClassLoader();
+ classloader.loadClass("org.eclipse.swt.program.Program");
+ SwtDesktopBrowser.browse(location);
+ } catch (ClassNotFoundException e2) {
+ DavGatewayTray.error(new BundleMessage("LOG_OPEN_LINK_NOT_SUPPORTED"));
+ } catch (Exception e2) {
+ DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_OPEN_LINK"), e2);
+ }
+ }
+ } catch (Exception e) {
+ DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_OPEN_LINK"), e);
+ }
+ }
+
+ /**
+ * Open default browser at location.
+ * User Java 6 Desktop class, OSX open command or SWT program launch
+ *
+ * @param location target location
+ */
+ public static void browse(String location) {
+ try {
+ DesktopBrowser.browse(new URI(location));
+ } catch (URISyntaxException e) {
+ DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_OPEN_LINK"), e);
+ }
+ }
+
+}
diff --git a/src/java/davmail/ui/browser/OSXDesktopBrowser.java b/src/java/davmail/ui/browser/OSXDesktopBrowser.java
new file mode 100644
index 0000000..250e5cb
--- /dev/null
+++ b/src/java/davmail/ui/browser/OSXDesktopBrowser.java
@@ -0,0 +1,41 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.ui.browser;
+
+import java.io.IOException;
+import java.net.URI;
+
+/**
+ * Failover: Runtime.exec open URL
+ */
+public final class OSXDesktopBrowser {
+ private OSXDesktopBrowser() {
+ }
+
+ /**
+ * Open default browser at location URI.
+ * User OSX open command
+ *
+ * @param location location URI
+ * @throws IOException on error
+ */
+ public static void browse(URI location) throws IOException {
+ Runtime.getRuntime().exec("open " + location.toString());
+ }
+}
diff --git a/src/java/davmail/ui/browser/SwtDesktopBrowser.java b/src/java/davmail/ui/browser/SwtDesktopBrowser.java
new file mode 100644
index 0000000..d488400
--- /dev/null
+++ b/src/java/davmail/ui/browser/SwtDesktopBrowser.java
@@ -0,0 +1,42 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.ui.browser;
+
+import org.eclipse.swt.program.Program;
+
+import java.net.URI;
+
+/**
+ * Wrapper class to call SWT Program class to launch default browser.
+ */
+public final class SwtDesktopBrowser {
+ private SwtDesktopBrowser() {
+ }
+
+ /**
+ * Open default browser at location URI.
+ * User SWT program launch
+ *
+ * @param location location URI
+ */
+ public static void browse(URI location) {
+ Program.launch(location.toString());
+ }
+
+}
diff --git a/src/java/davmail/ui/tray/AwtGatewayTray.java b/src/java/davmail/ui/tray/AwtGatewayTray.java
new file mode 100644
index 0000000..a0303da
--- /dev/null
+++ b/src/java/davmail/ui/tray/AwtGatewayTray.java
@@ -0,0 +1,296 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.ui.tray;
+
+import davmail.BundleMessage;
+import davmail.DavGateway;
+import davmail.Settings;
+import davmail.ui.AboutFrame;
+import davmail.ui.SettingsFrame;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.apache.log4j.lf5.LF5Appender;
+import org.apache.log4j.lf5.LogLevel;
+import org.apache.log4j.lf5.viewer.LogBrokerMonitor;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+/**
+ * Tray icon handler based on java 1.6
+ */
+public class AwtGatewayTray implements DavGatewayTrayInterface {
+ protected static final String TRAY_ACTIVE_PNG = "tray2.png";
+ protected static final String TRAY_PNG = "tray.png";
+ protected static final String TRAY_INACTIVE_PNG = "trayinactive.png";
+
+ protected AwtGatewayTray() {
+ }
+
+ static AboutFrame aboutFrame;
+ static SettingsFrame settingsFrame;
+ ActionListener settingsListener;
+
+ static TrayIcon trayIcon;
+ private static Image image;
+ private static Image image2;
+ private static Image inactiveImage;
+ static LogBrokerMonitor logBrokerMonitor;
+ private boolean isActive = true;
+
+ /**
+ * Return AWT Image icon for frame title.
+ *
+ * @return frame icon
+ */
+ public Image getFrameIcon() {
+ return image;
+ }
+
+ /**
+ * Switch tray icon between active and standby icon.
+ */
+ public void switchIcon() {
+ isActive = true;
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ if (trayIcon.getImage().equals(image)) {
+ trayIcon.setImage(image2);
+ } else {
+ trayIcon.setImage(image);
+ }
+ }
+ });
+ }
+
+ /**
+ * Set tray icon to inactive (network down)
+ */
+ public void resetIcon() {
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ trayIcon.setImage(image);
+ }
+ });
+ }
+
+ /**
+ * Set tray icon to inactive (network down)
+ */
+ public void inactiveIcon() {
+ isActive = false;
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ trayIcon.setImage(inactiveImage);
+ }
+ });
+ }
+
+ /**
+ * Check if current tray status is inactive (network down).
+ *
+ * @return true if inactive
+ */
+ public boolean isActive() {
+ return isActive;
+ }
+
+ /**
+ * Display balloon message for log level.
+ *
+ * @param message text message
+ * @param level log level
+ */
+ public void displayMessage(final String message, final Level level) {
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ if (trayIcon != null) {
+ TrayIcon.MessageType messageType = null;
+ if (level.equals(Level.INFO)) {
+ messageType = TrayIcon.MessageType.INFO;
+ } else if (level.equals(Level.WARN)) {
+ messageType = TrayIcon.MessageType.WARNING;
+ } else if (level.equals(Level.ERROR)) {
+ messageType = TrayIcon.MessageType.ERROR;
+ }
+ if (messageType != null) {
+ trayIcon.displayMessage(BundleMessage.format("UI_DAVMAIL_GATEWAY"), message, messageType);
+ }
+ trayIcon.setToolTip(BundleMessage.format("UI_DAVMAIL_GATEWAY") + '\n' + message);
+ }
+ }
+ });
+ }
+
+ /**
+ * Open about window
+ */
+ public void about() {
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ aboutFrame.update();
+ aboutFrame.setVisible(true);
+ aboutFrame.toFront();
+ aboutFrame.requestFocus();
+ }
+ });
+ }
+
+ /**
+ * Open settings window
+ */
+ public void preferences() {
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ settingsFrame.reload();
+ settingsFrame.setVisible(true);
+ settingsFrame.toFront();
+ settingsFrame.requestFocus();
+ }
+ });
+ }
+
+ /**
+ * Create tray icon and register frame listeners.
+ */
+ public void init() {
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ createAndShowGUI();
+ }
+ });
+ }
+
+ protected void createAndShowGUI() {
+ System.setProperty("swing.defaultlaf", UIManager.getSystemLookAndFeelClassName());
+
+ // get the SystemTray instance
+ SystemTray tray = SystemTray.getSystemTray();
+ image = DavGatewayTray.loadImage(getTrayIconPath());
+ image2 = DavGatewayTray.loadImage(getTrayIconActivePath());
+ inactiveImage = DavGatewayTray.loadImage(getTrayIconInactivePath());
+
+ // create a popup menu
+ PopupMenu popup = new PopupMenu();
+
+ aboutFrame = new AboutFrame();
+ // create an action settingsListener to listen for settings action executed on the tray icon
+ ActionListener aboutListener = new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ about();
+ }
+ };
+ // create menu item for the default action
+ MenuItem aboutItem = new MenuItem(BundleMessage.format("UI_ABOUT"));
+ aboutItem.addActionListener(aboutListener);
+ popup.add(aboutItem);
+
+ settingsFrame = new SettingsFrame();
+ // create an action settingsListener to listen for settings action executed on the tray icon
+ settingsListener = new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ preferences();
+ }
+ };
+ // create menu item for the default action
+ MenuItem defaultItem = new MenuItem(BundleMessage.format("UI_SETTINGS"));
+ defaultItem.addActionListener(settingsListener);
+ popup.add(defaultItem);
+
+ MenuItem logItem = new MenuItem(BundleMessage.format("UI_SHOW_LOGS"));
+ logItem.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ Logger rootLogger = Logger.getRootLogger();
+ LF5Appender lf5Appender = (LF5Appender) rootLogger.getAppender("LF5Appender");
+ if (lf5Appender == null) {
+ logBrokerMonitor = new LogBrokerMonitor(LogLevel.getLog4JLevels()) {
+ @Override
+ protected void closeAfterConfirm() {
+ hide();
+ }
+ };
+ lf5Appender = new LF5Appender(logBrokerMonitor);
+ lf5Appender.setName("LF5Appender");
+ rootLogger.addAppender(lf5Appender);
+ }
+ lf5Appender.getLogBrokerMonitor().show();
+ }
+ });
+ popup.add(logItem);
+
+ // create an action exitListener to listen for exit action executed on the tray icon
+ ActionListener exitListener = new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ try {
+ DavGateway.stop();
+ SystemTray.getSystemTray().remove(trayIcon);
+
+ // dispose frames
+ settingsFrame.dispose();
+ aboutFrame.dispose();
+ if (logBrokerMonitor != null) {
+ logBrokerMonitor.dispose();
+ }
+ } catch (Exception exc) {
+ DavGatewayTray.error(exc);
+ }
+ // make sure we do exit
+ System.exit(0);
+ }
+ };
+ // create menu item for the exit action
+ MenuItem exitItem = new MenuItem(BundleMessage.format("UI_EXIT"));
+ exitItem.addActionListener(exitListener);
+ popup.add(exitItem);
+
+ /// ... add other items
+ // construct a TrayIcon
+ trayIcon = new TrayIcon(image, BundleMessage.format("UI_DAVMAIL_GATEWAY"), popup);
+ // set the TrayIcon properties
+ trayIcon.addActionListener(settingsListener);
+ // ...
+ // add the tray image
+ try {
+ tray.add(trayIcon);
+ } catch (AWTException e) {
+ DavGatewayTray.warn(new BundleMessage("LOG_UNABLE_TO_CREATE_TRAY"), e);
+ }
+
+ // display settings frame on first start
+ if (Settings.isFirstStart()) {
+ settingsFrame.setVisible(true);
+ settingsFrame.toFront();
+ settingsFrame.requestFocus();
+ }
+ }
+
+ protected String getTrayIconPath() {
+ return AwtGatewayTray.TRAY_PNG;
+ }
+
+ protected String getTrayIconActivePath() {
+ return AwtGatewayTray.TRAY_ACTIVE_PNG;
+ }
+
+ protected String getTrayIconInactivePath() {
+ return AwtGatewayTray.TRAY_INACTIVE_PNG;
+ }
+}
diff --git a/src/java/davmail/ui/tray/DavGatewayTray.java b/src/java/davmail/ui/tray/DavGatewayTray.java
new file mode 100644
index 0000000..1e1dd6a
--- /dev/null
+++ b/src/java/davmail/ui/tray/DavGatewayTray.java
@@ -0,0 +1,293 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.ui.tray;
+
+import davmail.BundleMessage;
+import davmail.Settings;
+import davmail.exchange.NetworkDownException;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.io.IOException;
+import java.net.URL;
+
+
+/**
+ * Tray icon handler
+ */
+public final class DavGatewayTray {
+ private static final Logger LOGGER = Logger.getLogger("davmail");
+ private static final long ICON_SWITCH_MINIMUM_DELAY = 250;
+ private static long lastIconSwitch;
+
+ private DavGatewayTray() {
+ }
+
+ static DavGatewayTrayInterface davGatewayTray;
+
+ /**
+ * Return AWT Image icon for frame title.
+ *
+ * @return frame icon
+ */
+ public static Image getFrameIcon() {
+ Image icon = null;
+ if (davGatewayTray != null) {
+ icon = davGatewayTray.getFrameIcon();
+ }
+ return icon;
+ }
+
+ /**
+ * Switch tray icon between active and standby icon.
+ */
+ public static void switchIcon() {
+ if (davGatewayTray != null) {
+ if (System.currentTimeMillis() - lastIconSwitch > ICON_SWITCH_MINIMUM_DELAY) {
+ davGatewayTray.switchIcon();
+ lastIconSwitch = System.currentTimeMillis();
+ }
+ }
+ }
+
+ /**
+ * Set tray icon to inactive (network down)
+ */
+ public static void resetIcon() {
+ if (davGatewayTray != null && isActive()) {
+ davGatewayTray.resetIcon();
+ }
+ }
+
+ /**
+ * Check if current tray status is inactive (network down).
+ *
+ * @return true if inactive
+ */
+ public static boolean isActive() {
+ return davGatewayTray == null || davGatewayTray.isActive();
+ }
+
+ /**
+ * Log and display balloon message according to log level.
+ *
+ * @param message text message
+ * @param level log level
+ */
+ private static void displayMessage(BundleMessage message, Level level) {
+ LOGGER.log(level, message.formatLog());
+ if (davGatewayTray != null && !Settings.getBooleanProperty("davmail.disableGuiNotifications")) {
+ davGatewayTray.displayMessage(message.format(), level);
+ }
+ }
+
+ /**
+ * Log and display balloon message and exception according to log level.
+ *
+ * @param message text message
+ * @param e exception
+ * @param level log level
+ */
+ private static void displayMessage(BundleMessage message, Exception e, Level level) {
+ if (e instanceof NetworkDownException) {
+ LOGGER.log(level, BundleMessage.getExceptionLogMessage(message, e));
+ } else {
+ LOGGER.log(level, BundleMessage.getExceptionLogMessage(message, e), e);
+ }
+ if (davGatewayTray != null && !Settings.getBooleanProperty("davmail.disableGuiNotifications")
+ && (!(e instanceof NetworkDownException))) {
+ davGatewayTray.displayMessage(BundleMessage.getExceptionMessage(message, e), level);
+ }
+ if (davGatewayTray != null && e instanceof NetworkDownException) {
+ davGatewayTray.inactiveIcon();
+ }
+ }
+
+ /**
+ * Log message at level DEBUG.
+ *
+ * @param message bundle message
+ */
+ public static void debug(BundleMessage message) {
+ displayMessage(message, Level.DEBUG);
+ }
+
+ /**
+ * Log message at level INFO.
+ *
+ * @param message bundle message
+ */
+ public static void info(BundleMessage message) {
+ displayMessage(message, Level.INFO);
+ }
+
+ /**
+ * Log message at level WARN.
+ *
+ * @param message bundle message
+ */
+ public static void warn(BundleMessage message) {
+ displayMessage(message, Level.WARN);
+ }
+
+ /**
+ * Log exception at level WARN.
+ *
+ * @param e exception
+ */
+ public static void warn(Exception e) {
+ displayMessage(null, e, Level.WARN);
+ }
+
+ /**
+ * Log message at level ERROR.
+ *
+ * @param message bundle message
+ */
+ public static void error(BundleMessage message) {
+ displayMessage(message, Level.ERROR);
+ }
+
+ /**
+ * Log exception at level WARN for NetworkDownException,
+ * ERROR for other exceptions.
+ *
+ * @param e exception
+ */
+ public static void log(Exception e) {
+ // only warn on network down
+ if (e instanceof NetworkDownException) {
+ warn(e);
+ } else {
+ error(e);
+ }
+ }
+
+ /**
+ * Log exception at level ERROR.
+ *
+ * @param e exception
+ */
+ public static void error(Exception e) {
+ displayMessage(null, e, Level.ERROR);
+ }
+
+ /**
+ * Log message and exception at level DEBUG.
+ *
+ * @param message bundle message
+ * @param e exception
+ */
+ public static void debug(BundleMessage message, Exception e) {
+ displayMessage(message, e, Level.DEBUG);
+ }
+
+ /**
+ * Log message and exception at level WARN.
+ *
+ * @param message bundle message
+ * @param e exception
+ */
+ public static void warn(BundleMessage message, Exception e) {
+ displayMessage(message, e, Level.WARN);
+ }
+
+ /**
+ * Log message and exception at level ERROR.
+ *
+ * @param message bundle message
+ * @param e exception
+ */
+ public static void error(BundleMessage message, Exception e) {
+ displayMessage(message, e, Level.ERROR);
+ }
+
+ /**
+ * Create tray icon and register frame listeners.
+ */
+ public static void init() {
+ if (!Settings.getBooleanProperty("davmail.server")) {
+ // first try to load SWT before with Java
+ ClassLoader classloader = DavGatewayTray.class.getClassLoader();
+ try {
+ // trigger ClassNotFoundException
+ classloader.loadClass("org.eclipse.swt.SWT");
+ // SWT available, create tray
+ davGatewayTray = new SwtGatewayTray();
+ davGatewayTray.init();
+ } catch (ClassNotFoundException e) {
+ DavGatewayTray.info(new BundleMessage("LOG_SWT_NOT_AVAILABLE"));
+ }
+ // try java6 tray support
+ if (davGatewayTray == null) {
+ try {
+ if (SystemTray.isSupported()) {
+ if (isOSX()) {
+ davGatewayTray = new OSXAwtGatewayTray();
+ } else {
+ davGatewayTray = new AwtGatewayTray();
+ }
+ davGatewayTray.init();
+ }
+ } catch (NoClassDefFoundError e) {
+ DavGatewayTray.info(new BundleMessage("LOG_SYSTEM_TRAY_NOT_AVAILABLE"));
+ }
+ }
+ if (davGatewayTray == null) {
+ if (isOSX()) {
+ // MacOS
+ davGatewayTray = new OSXFrameGatewayTray();
+ } else {
+ davGatewayTray = new FrameGatewayTray();
+ }
+ davGatewayTray.init();
+ }
+ }
+ }
+
+ /**
+ * Test if running on OSX
+ *
+ * @return true on Mac OS X
+ */
+ private static boolean isOSX() {
+ return System.getProperty("os.name").toLowerCase().startsWith("mac os x");
+ }
+
+ /**
+ * Load image with current class loader.
+ *
+ * @param fileName image resource file name
+ * @return image
+ */
+ public static Image loadImage(String fileName) {
+ Image result = null;
+ try {
+ ClassLoader classloader = DavGatewayTray.class.getClassLoader();
+ URL imageUrl = classloader.getResource(fileName);
+ result = ImageIO.read(imageUrl);
+ } catch (IOException e) {
+ DavGatewayTray.warn(new BundleMessage("LOG_UNABLE_TO_LOAD_IMAGE"), e);
+ }
+ return result;
+ }
+
+}
diff --git a/src/java/davmail/ui/tray/DavGatewayTrayInterface.java b/src/java/davmail/ui/tray/DavGatewayTrayInterface.java
new file mode 100644
index 0000000..7220cea
--- /dev/null
+++ b/src/java/davmail/ui/tray/DavGatewayTrayInterface.java
@@ -0,0 +1,71 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.ui.tray;
+
+import org.apache.log4j.Level;
+
+import java.awt.*;
+
+/**
+ * Gateway tray interface common to SWT and pure java implementations
+ */
+public interface DavGatewayTrayInterface {
+ /**
+ * Switch tray icon between active and standby icon.
+ */
+ void switchIcon();
+
+ /**
+ * Reset tray icon to standby
+ */
+ void resetIcon();
+
+ /**
+ * Set tray icon to inactive (network down)
+ */
+ void inactiveIcon();
+
+ /**
+ * Check if current tray status is inactive (network down).
+ *
+ * @return true if inactive
+ */
+ boolean isActive();
+
+ /**
+ * Return AWT Image icon for frame title.
+ *
+ * @return frame icon
+ */
+ Image getFrameIcon();
+
+ /**
+ * Display balloon message for log level.
+ *
+ * @param message text message
+ * @param level log level
+ */
+ void displayMessage(String message, Level level);
+
+ /**
+ * Create tray icon and register frame listeners.
+ */
+ void init();
+
+}
diff --git a/src/java/davmail/ui/tray/FrameGatewayTray.java b/src/java/davmail/ui/tray/FrameGatewayTray.java
new file mode 100644
index 0000000..622abcb
--- /dev/null
+++ b/src/java/davmail/ui/tray/FrameGatewayTray.java
@@ -0,0 +1,325 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.ui.tray;
+
+import davmail.BundleMessage;
+import davmail.DavGateway;
+import davmail.Settings;
+import davmail.ui.AboutFrame;
+import davmail.ui.SettingsFrame;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.apache.log4j.lf5.LF5Appender;
+import org.apache.log4j.lf5.LogLevel;
+import org.apache.log4j.lf5.viewer.LogBrokerMonitor;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+/**
+ * Failover GUI for Java 1.5 without SWT
+ */
+public class FrameGatewayTray implements DavGatewayTrayInterface {
+ protected FrameGatewayTray() {
+ }
+
+ static JFrame mainFrame;
+ static AboutFrame aboutFrame;
+ static SettingsFrame settingsFrame;
+ LogBrokerMonitor logBrokerMonitor;
+ private static JEditorPane errorArea;
+ private static JLabel errorLabel;
+ private static JEditorPane messageArea;
+ private static Image image;
+ private static Image image2;
+ private static Image inactiveImage;
+ private boolean isActive = true;
+
+ /**
+ * Return AWT Image icon for frame title.
+ *
+ * @return frame icon
+ */
+ public Image getFrameIcon() {
+ return image;
+ }
+
+ /**
+ * Switch tray icon between active and standby icon.
+ */
+ public void switchIcon() {
+ isActive = true;
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ if (mainFrame.getIconImage().equals(image)) {
+ mainFrame.setIconImage(image2);
+ } else {
+ mainFrame.setIconImage(image);
+ }
+ }
+ });
+ }
+
+ /**
+ * Set tray icon to inactive (network down)
+ */
+ public void resetIcon() {
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ mainFrame.setIconImage(image);
+ }
+ });
+ }
+
+ /**
+ * Set tray icon to inactive (network down)
+ */
+ public void inactiveIcon() {
+ isActive = false;
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ mainFrame.setIconImage(inactiveImage);
+ }
+ });
+ }
+
+ /**
+ * Check if current tray status is inactive (network down).
+ *
+ * @return true if inactive
+ */
+ public boolean isActive() {
+ return isActive;
+ }
+
+ /**
+ * Log and display balloon message according to log level.
+ *
+ * @param message text message
+ * @param level log level
+ */
+ public void displayMessage(final String message, final Level level) {
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ if (errorArea != null && messageArea != null) {
+ if (level.equals(Level.INFO)) {
+ errorLabel.setIcon(UIManager.getIcon("OptionPane.informationIcon"));
+ errorArea.setText(message);
+ } else if (level.equals(Level.WARN)) {
+ errorLabel.setIcon(UIManager.getIcon("OptionPane.warningIcon"));
+ errorArea.setText(message);
+ } else if (level.equals(Level.ERROR)) {
+ errorLabel.setIcon(UIManager.getIcon("OptionPane.errorIcon"));
+ errorArea.setText(message);
+ }
+ messageArea.setText(message);
+ }
+ }
+ });
+ }
+
+ /**
+ * Open about window
+ */
+ public void about() {
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ aboutFrame.update();
+ aboutFrame.setVisible(true);
+ aboutFrame.toFront();
+ aboutFrame.requestFocus();
+ }
+ });
+ }
+
+ /**
+ * Open settings window
+ */
+ public void preferences() {
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ settingsFrame.reload();
+ settingsFrame.setVisible(true);
+ settingsFrame.toFront();
+ settingsFrame.repaint();
+ settingsFrame.requestFocus();
+ }
+ });
+ }
+
+ /**
+ * Open logging window.
+ */
+ public void showLogs() {
+ Logger rootLogger = Logger.getRootLogger();
+ LF5Appender lf5Appender = (LF5Appender) rootLogger.getAppender("LF5Appender");
+ if (lf5Appender == null) {
+ logBrokerMonitor = new LogBrokerMonitor(LogLevel.getLog4JLevels()) {
+ @Override
+ protected void closeAfterConfirm() {
+ hide();
+ }
+ };
+ lf5Appender = new LF5Appender(logBrokerMonitor);
+ lf5Appender.setName("LF5Appender");
+ rootLogger.addAppender(lf5Appender);
+ }
+ lf5Appender.getLogBrokerMonitor().show();
+ }
+
+ /**
+ * Create tray icon and register frame listeners.
+ */
+ public void init() {
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ createAndShowGUI();
+ }
+ });
+ }
+
+ protected void buildMenu() {
+ // create a popup menu
+ JMenu menu = new JMenu(BundleMessage.format("UI_DAVMAIL_GATEWAY"));
+ JMenuBar menuBar = new JMenuBar();
+ menuBar.add(menu);
+ mainFrame.setJMenuBar(menuBar);
+
+ // create an action settingsListener to listen for settings action executed on the tray icon
+ ActionListener aboutListener = new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ about();
+ }
+ };
+ // create menu item for the default action
+ JMenuItem aboutItem = new JMenuItem(BundleMessage.format("UI_ABOUT"));
+ aboutItem.addActionListener(aboutListener);
+ menu.add(aboutItem);
+
+
+ // create an action settingsListener to listen for settings action executed on the tray icon
+ ActionListener settingsListener = new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ preferences();
+ }
+ };
+ // create menu item for the default action
+ JMenuItem defaultItem = new JMenuItem(BundleMessage.format("UI_SETTINGS"));
+ defaultItem.addActionListener(settingsListener);
+ menu.add(defaultItem);
+
+ JMenuItem logItem = new JMenuItem(BundleMessage.format("UI_SHOW_LOGS"));
+ logItem.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ showLogs();
+ }
+ });
+ menu.add(logItem);
+
+ // create an action exitListener to listen for exit action executed on the tray icon
+ ActionListener exitListener = new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ try {
+ DavGateway.stop();
+ // dispose frames
+ settingsFrame.dispose();
+ aboutFrame.dispose();
+ if (logBrokerMonitor != null) {
+ logBrokerMonitor.dispose();
+ }
+ } catch (Exception exc) {
+ DavGatewayTray.error(exc);
+ }
+ // make sure we do exit
+ System.exit(0);
+ }
+ };
+ // create menu item for the exit action
+ JMenuItem exitItem = new JMenuItem(BundleMessage.format("UI_EXIT"));
+ exitItem.addActionListener(exitListener);
+ menu.add(exitItem);
+ }
+
+ protected void createAndShowGUI() {
+ System.setProperty("swing.defaultlaf", UIManager.getSystemLookAndFeelClassName());
+
+ image = DavGatewayTray.loadImage("tray.png");
+ image2 = DavGatewayTray.loadImage(AwtGatewayTray.TRAY_ACTIVE_PNG);
+ inactiveImage = DavGatewayTray.loadImage(AwtGatewayTray.TRAY_INACTIVE_PNG);
+
+ mainFrame = new JFrame();
+ mainFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
+ mainFrame.setTitle(BundleMessage.format("UI_DAVMAIL_GATEWAY"));
+ mainFrame.setIconImage(image);
+
+ JPanel errorPanel = new JPanel();
+ errorPanel.setBorder(BorderFactory.createTitledBorder(BundleMessage.format("UI_LAST_MESSAGE")));
+ errorPanel.setLayout(new BoxLayout(errorPanel, BoxLayout.X_AXIS));
+ errorArea = new JTextPane();
+ errorArea.setEditable(false);
+ errorArea.setBackground(mainFrame.getBackground());
+ errorLabel = new JLabel();
+ errorPanel.add(errorLabel);
+ errorPanel.add(errorArea);
+
+ JPanel messagePanel = new JPanel();
+ messagePanel.setBorder(BorderFactory.createTitledBorder(BundleMessage.format("UI_LAST_LOG")));
+ messagePanel.setLayout(new BoxLayout(messagePanel, BoxLayout.X_AXIS));
+
+ messageArea = new JTextPane();
+ messageArea.setText(BundleMessage.format("LOG_STARTING_DAVMAIL"));
+ messageArea.setEditable(false);
+ messageArea.setBackground(mainFrame.getBackground());
+ messagePanel.add(messageArea);
+
+ JPanel mainPanel = new JPanel();
+ mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
+ mainPanel.add(errorPanel);
+ mainPanel.add(messagePanel);
+ mainFrame.add(mainPanel);
+
+ aboutFrame = new AboutFrame();
+ settingsFrame = new SettingsFrame();
+ buildMenu();
+
+ mainFrame.setMinimumSize(new Dimension(400, 180));
+ mainFrame.pack();
+ // workaround MacOSX
+ if (mainFrame.getSize().width < 400 || mainFrame.getSize().height < 180) {
+ mainFrame.setSize(Math.max(mainFrame.getSize().width, 400),
+ Math.max(mainFrame.getSize().height, 180));
+ }
+ // center frame
+ mainFrame.setLocation(mainFrame.getToolkit().getScreenSize().width / 2 -
+ mainFrame.getSize().width / 2,
+ mainFrame.getToolkit().getScreenSize().height / 2 -
+ mainFrame.getSize().height / 2);
+ mainFrame.setVisible(true);
+
+ // display settings frame on first start
+ if (Settings.isFirstStart()) {
+ settingsFrame.setVisible(true);
+ settingsFrame.toFront();
+ settingsFrame.repaint();
+ settingsFrame.requestFocus();
+ }
+ }
+}
diff --git a/src/java/davmail/ui/tray/OSXAwtGatewayTray.java b/src/java/davmail/ui/tray/OSXAwtGatewayTray.java
new file mode 100644
index 0000000..9eff9c0
--- /dev/null
+++ b/src/java/davmail/ui/tray/OSXAwtGatewayTray.java
@@ -0,0 +1,133 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.ui.tray;
+
+import davmail.BundleMessage;
+import davmail.DavGateway;
+import davmail.ui.OSXAdapter;
+import info.growl.Growl;
+import info.growl.GrowlException;
+import info.growl.GrowlUtils;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.awt.image.RenderedImage;
+
+/**
+ * Extended Awt tray with OSX extensions.
+ */
+public class OSXAwtGatewayTray extends AwtGatewayTray {
+ protected static final String OSX_TRAY_ACTIVE_PNG = "osxtray2.png";
+ protected static final String OSX_TRAY_PNG = "osxtray.png";
+ protected static final String OSX_TRAY_INACTIVE_PNG = "osxtrayinactive.png";
+
+ private static final Logger LOGGER = Logger.getLogger(OSXAwtGatewayTray.class);
+
+ /**
+ * Exit DavMail Gateway.
+ *
+ * @return true
+ */
+ @SuppressWarnings({"SameReturnValue", "UnusedDeclaration"})
+ public boolean quit() {
+ DavGateway.stop();
+ // dispose frames
+ settingsFrame.dispose();
+ aboutFrame.dispose();
+ if (logBrokerMonitor != null) {
+ logBrokerMonitor.dispose();
+ }
+ return true;
+ }
+
+ @Override
+ protected void createAndShowGUI() {
+ System.setProperty("apple.laf.useScreenMenuBar", "true");
+ super.createAndShowGUI();
+ trayIcon.removeActionListener(settingsListener);
+ try {
+ OSXAdapter.setAboutHandler(this, AwtGatewayTray.class.getDeclaredMethod("about", (Class[]) null));
+ OSXAdapter.setPreferencesHandler(this, AwtGatewayTray.class.getDeclaredMethod("preferences", (Class[]) null));
+ OSXAdapter.setQuitHandler(this, OSXAwtGatewayTray.class.getDeclaredMethod("quit", (Class[]) null));
+ } catch (Exception e) {
+ DavGatewayTray.error(new BundleMessage("LOG_ERROR_LOADING_OSXADAPTER"), e);
+ }
+ }
+
+ @Override
+ protected String getTrayIconPath() {
+ return OSXAwtGatewayTray.OSX_TRAY_PNG;
+ }
+
+ @Override
+ protected String getTrayIconActivePath() {
+ return OSXAwtGatewayTray.OSX_TRAY_ACTIVE_PNG;
+ }
+
+ @Override
+ protected String getTrayIconInactivePath() {
+ return OSXAwtGatewayTray.OSX_TRAY_INACTIVE_PNG;
+ }
+
+ @Override
+ public void displayMessage(final String message, final Level level) {
+ if (!GrowlUtils.isGrowlLoaded()) {
+ super.displayMessage(message, level);
+ } else {
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ if (trayIcon != null) {
+ Icon icon = null;
+ if (level.equals(Level.INFO)) {
+ icon = UIManager.getIcon("OptionPane.informationIcon");
+ } else if (level.equals(Level.WARN)) {
+ icon = UIManager.getIcon("OptionPane.warningIcon");
+ } else if (level.equals(Level.ERROR)) {
+ icon = UIManager.getIcon("OptionPane.errorIcon");
+ }
+
+ if (icon != null && message != null && message.length() > 0) {
+ try {
+ String title = BundleMessage.format("UI_DAVMAIL_GATEWAY");
+ Growl growl = GrowlUtils.getGrowlInstance("DavMail");
+ growl.addNotification(title, true);
+ growl.register();
+ growl.sendNotification(title, title, message, (RenderedImage) getImageForIcon(icon));
+ } catch (GrowlException growlException) {
+ LOGGER.error(growlException);
+ }
+ }
+ trayIcon.setToolTip(BundleMessage.format("UI_DAVMAIL_GATEWAY") + '\n' + message);
+ }
+ }
+ });
+ }
+ }
+
+ protected Image getImageForIcon(Icon icon) {
+ BufferedImage bufferedimage = new BufferedImage(icon.getIconWidth(), icon.getIconHeight(), BufferedImage.TYPE_INT_ARGB);
+ Graphics g = bufferedimage.getGraphics();
+ icon.paintIcon(null, g, 0, 0);
+ g.dispose();
+ return bufferedimage;
+ }
+}
diff --git a/src/java/davmail/ui/tray/OSXFrameGatewayTray.java b/src/java/davmail/ui/tray/OSXFrameGatewayTray.java
new file mode 100644
index 0000000..c84058e
--- /dev/null
+++ b/src/java/davmail/ui/tray/OSXFrameGatewayTray.java
@@ -0,0 +1,81 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.ui.tray;
+
+import davmail.BundleMessage;
+import davmail.DavGateway;
+import davmail.ui.OSXAdapter;
+
+import javax.swing.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+/**
+ * MacOSX specific frame to handle menu
+ */
+public class OSXFrameGatewayTray extends FrameGatewayTray {
+
+ /**
+ * Exit DavMail Gateway.
+ *
+ * @return true
+ */
+ @SuppressWarnings({"SameReturnValue", "UnusedDeclaration"})
+ public boolean quit() {
+ DavGateway.stop();
+ // dispose frames
+ settingsFrame.dispose();
+ aboutFrame.dispose();
+ if (logBrokerMonitor != null) {
+ logBrokerMonitor.dispose();
+ }
+ return true;
+ }
+
+ @Override
+ protected void buildMenu() {
+ // create a popup menu
+ JMenu menu = new JMenu(BundleMessage.format("UI_LOGS"));
+ JMenuBar menuBar = new JMenuBar();
+ menuBar.add(menu);
+ mainFrame.setJMenuBar(menuBar);
+
+ JMenuItem logItem = new JMenuItem(BundleMessage.format("UI_SHOW_LOGS"));
+ logItem.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ showLogs();
+ }
+ });
+ menu.add(logItem);
+ }
+
+
+ @Override
+ protected void createAndShowGUI() {
+ System.setProperty("apple.laf.useScreenMenuBar", "true");
+ super.createAndShowGUI();
+ try {
+ OSXAdapter.setAboutHandler(this, FrameGatewayTray.class.getDeclaredMethod("about", (Class[]) null));
+ OSXAdapter.setPreferencesHandler(this, FrameGatewayTray.class.getDeclaredMethod("preferences", (Class[]) null));
+ OSXAdapter.setQuitHandler(this, OSXFrameGatewayTray.class.getDeclaredMethod("quit", (Class[]) null));
+ } catch (Exception e) {
+ DavGatewayTray.error(new BundleMessage("LOG_ERROR_LOADING_OSXADAPTER"), e);
+ }
+ }
+}
diff --git a/src/java/davmail/ui/tray/SwtGatewayTray.java b/src/java/davmail/ui/tray/SwtGatewayTray.java
new file mode 100644
index 0000000..09b9d9e
--- /dev/null
+++ b/src/java/davmail/ui/tray/SwtGatewayTray.java
@@ -0,0 +1,393 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.ui.tray;
+
+import davmail.BundleMessage;
+import davmail.DavGateway;
+import davmail.Settings;
+import davmail.ui.AboutFrame;
+import davmail.ui.SettingsFrame;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.apache.log4j.lf5.LF5Appender;
+import org.apache.log4j.lf5.LogLevel;
+import org.apache.log4j.lf5.viewer.LogBrokerMonitor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.internal.gtk.OS;
+import org.eclipse.swt.widgets.*;
+
+import javax.swing.*;
+import java.io.IOException;
+import java.net.URL;
+
+/**
+ * Tray icon handler based on SWT
+ */
+public class SwtGatewayTray implements DavGatewayTrayInterface {
+ protected SwtGatewayTray() {
+ }
+
+ SettingsFrame settingsFrame;
+ AboutFrame aboutFrame;
+
+ private static TrayItem trayItem;
+ private static java.awt.Image awtImage;
+ private static Image image;
+ private static Image image2;
+ private static Image inactiveImage;
+ private static Display display;
+ private static Shell shell;
+ private LogBrokerMonitor logBrokerMonitor;
+ private boolean isActive = true;
+ private boolean isReady;
+
+ private final Thread mainThread = Thread.currentThread();
+
+ /**
+ * Return AWT Image icon for frame title.
+ *
+ * @return frame icon
+ */
+ public java.awt.Image getFrameIcon() {
+ return awtImage;
+ }
+
+ /**
+ * Switch tray icon between active and standby icon.
+ */
+ public void switchIcon() {
+ isActive = true;
+ display.syncExec(new Runnable() {
+ public void run() {
+ if (trayItem.getImage().equals(image)) {
+ trayItem.setImage(image2);
+ } else {
+ trayItem.setImage(image);
+ }
+ }
+ });
+
+ }
+
+ /**
+ * Set tray icon to inactive (network down)
+ */
+ public void resetIcon() {
+ display.syncExec(new Runnable() {
+ public void run() {
+ trayItem.setImage(image);
+ }
+ });
+ }
+
+ /**
+ * Set tray icon to inactive (network down)
+ */
+ public void inactiveIcon() {
+ isActive = false;
+ display.syncExec(new Runnable() {
+ public void run() {
+ trayItem.setImage(inactiveImage);
+ }
+ });
+ }
+
+ /**
+ * Check if current tray status is inactive (network down).
+ *
+ * @return true if inactive
+ */
+ public boolean isActive() {
+ return isActive;
+ }
+
+ /**
+ * Log and display balloon message according to log level.
+ *
+ * @param message text message
+ * @param level log level
+ */
+ public void displayMessage(final String message, final Level level) {
+ if (trayItem != null) {
+ display.asyncExec(new Runnable() {
+ public void run() {
+ int messageType = 0;
+ if (level.equals(Level.INFO)) {
+ messageType = SWT.ICON_INFORMATION;
+ } else if (level.equals(Level.WARN)) {
+ messageType = SWT.ICON_WARNING;
+ } else if (level.equals(Level.ERROR)) {
+ messageType = SWT.ICON_ERROR;
+ }
+ if (messageType != 0) {
+ final ToolTip toolTip = new ToolTip(shell, SWT.BALLOON | messageType);
+ toolTip.setText(BundleMessage.format("UI_DAVMAIL_GATEWAY"));
+ toolTip.setMessage(message);
+ trayItem.setToolTip(toolTip);
+ toolTip.setVisible(true);
+ }
+ trayItem.setToolTipText(BundleMessage.format("UI_DAVMAIL_GATEWAY") + '\n' + message);
+ }
+ });
+ }
+ }
+
+ /**
+ * Load image with current class loader.
+ *
+ * @param fileName image resource file name
+ * @return image
+ */
+ public static Image loadSwtImage(String fileName) {
+ Image result = null;
+ try {
+ ClassLoader classloader = DavGatewayTray.class.getClassLoader();
+ URL imageUrl = classloader.getResource(fileName);
+ result = new Image(display, imageUrl.openStream());
+ } catch (IOException e) {
+ DavGatewayTray.warn(new BundleMessage("LOG_UNABLE_TO_LOAD_IMAGE"), e);
+ }
+ return result;
+ }
+
+ /**
+ * Create tray icon and register frame listeners.
+ */
+ public void init() {
+ // register error handler to avoid application crash on concurrent X access from SWT and AWT
+ try {
+ OS.gdk_error_trap_push();
+ } catch (NoClassDefFoundError e) {
+ // ignore
+ }
+ final String systemLookAndFeelClassName = UIManager.getSystemLookAndFeelClassName();
+ try {
+ // workaround for bug when SWT and AWT both try to access Gtk
+ if (systemLookAndFeelClassName.indexOf("gtk") >= 0) {
+ System.setProperty("swing.defaultlaf", UIManager.getCrossPlatformLookAndFeelClassName());
+ } else {
+ System.setProperty("swing.defaultlaf", systemLookAndFeelClassName);
+ }
+ } catch (Exception e) {
+ DavGatewayTray.warn(new BundleMessage("LOG_UNABLE_TO_SET_LOOK_AND_FEEL"));
+ }
+
+ new Thread("SWT") {
+ @Override
+ public void run() {
+ try {
+ display = new Display();
+ shell = new Shell(display);
+
+ final Tray tray = display.getSystemTray();
+ if (tray != null) {
+
+ trayItem = new TrayItem(tray, SWT.NONE);
+ trayItem.setToolTipText(BundleMessage.format("UI_DAVMAIL_GATEWAY"));
+
+ awtImage = DavGatewayTray.loadImage(AwtGatewayTray.TRAY_PNG);
+ image = loadSwtImage(AwtGatewayTray.TRAY_PNG);
+ image2 = loadSwtImage(AwtGatewayTray.TRAY_ACTIVE_PNG);
+ inactiveImage = loadSwtImage(AwtGatewayTray.TRAY_INACTIVE_PNG);
+
+ trayItem.setImage(image);
+
+ // create a popup menu
+ final Menu popup = new Menu(shell, SWT.POP_UP);
+ trayItem.addListener(SWT.MenuDetect, new Listener() {
+ public void handleEvent(Event event) {
+ display.asyncExec(
+ new Runnable() {
+ public void run() {
+ popup.setVisible(true);
+ }
+ });
+ }
+ });
+
+ MenuItem aboutItem = new MenuItem(popup, SWT.PUSH);
+ aboutItem.setText(BundleMessage.format("UI_ABOUT"));
+ aboutItem.addListener(SWT.Selection, new Listener() {
+ public void handleEvent(Event event) {
+ display.asyncExec(
+ new Runnable() {
+ public void run() {
+ if (aboutFrame == null) {
+ aboutFrame = new AboutFrame();
+ }
+ aboutFrame.update();
+ aboutFrame.setVisible(true);
+ aboutFrame.toFront();
+ aboutFrame.requestFocus();
+ }
+ });
+ }
+ });
+
+ trayItem.addListener(SWT.DefaultSelection, new Listener() {
+ public void handleEvent(Event event) {
+ display.asyncExec(
+ new Runnable() {
+ public void run() {
+ // create frame on first call
+ if (settingsFrame == null) {
+ settingsFrame = new SettingsFrame();
+ }
+ settingsFrame.reload();
+ settingsFrame.setVisible(true);
+ settingsFrame.toFront();
+ settingsFrame.requestFocus();
+ }
+ });
+ }
+ });
+
+ // create menu item for the default action
+ MenuItem defaultItem = new MenuItem(popup, SWT.PUSH);
+ defaultItem.setText(BundleMessage.format("UI_SETTINGS"));
+ defaultItem.addListener(SWT.Selection, new Listener() {
+ public void handleEvent(Event event) {
+ display.asyncExec(
+ new Runnable() {
+ public void run() {
+ // create frame on first call
+ if (settingsFrame == null) {
+ settingsFrame = new SettingsFrame();
+ }
+ settingsFrame.reload();
+ settingsFrame.setVisible(true);
+ settingsFrame.toFront();
+ settingsFrame.requestFocus();
+ }
+ });
+ }
+ });
+
+ MenuItem logItem = new MenuItem(popup, SWT.PUSH);
+ logItem.setText(BundleMessage.format("UI_SHOW_LOGS"));
+ logItem.addListener(SWT.Selection, new Listener() {
+ public void handleEvent(Event event) {
+ display.asyncExec(
+ new Runnable() {
+ public void run() {
+
+ Logger rootLogger = Logger.getRootLogger();
+ LF5Appender lf5Appender = (LF5Appender) rootLogger.getAppender("LF5Appender");
+ if (lf5Appender == null) {
+ logBrokerMonitor = new LogBrokerMonitor(LogLevel.getLog4JLevels()) {
+ @Override
+ protected void closeAfterConfirm() {
+ hide();
+ }
+ };
+ lf5Appender = new LF5Appender(logBrokerMonitor);
+ lf5Appender.setName("LF5Appender");
+ rootLogger.addAppender(lf5Appender);
+ }
+ lf5Appender.getLogBrokerMonitor().show();
+ }
+ });
+ }
+ });
+
+ MenuItem exitItem = new MenuItem(popup, SWT.PUSH);
+ exitItem.setText(BundleMessage.format("UI_EXIT"));
+ exitItem.addListener(SWT.Selection, new Listener() {
+ public void handleEvent(Event event) {
+ DavGateway.stop();
+ shell.dispose();
+ }
+ });
+
+ // display settings frame on first start
+ if (Settings.isFirstStart()) {
+ // create frame on first call
+ if (settingsFrame == null) {
+ settingsFrame = new SettingsFrame();
+ }
+ settingsFrame.setVisible(true);
+ settingsFrame.toFront();
+ settingsFrame.requestFocus();
+ }
+
+ synchronized (mainThread) {
+ // ready
+ isReady = true;
+ mainThread.notifyAll();
+ }
+
+ while (!shell.isDisposed()) {
+ if (!display.readAndDispatch()) {
+ display.sleep();
+ }
+ }
+
+ if (trayItem != null) {
+ trayItem.dispose();
+ trayItem = null;
+ }
+
+ if (image != null) {
+ image.dispose();
+ }
+ if (image2 != null) {
+ image2.dispose();
+ }
+ try {
+ if (!display.isDisposed()) {
+ display.dispose();
+ }
+ } catch (Exception e) {
+ // already disposed
+ }
+ // dispose AWT frames
+ if (settingsFrame != null) {
+ settingsFrame.dispose();
+ }
+ if (aboutFrame != null) {
+ aboutFrame.dispose();
+ }
+ if (logBrokerMonitor != null) {
+ logBrokerMonitor.dispose();
+ }
+ }
+ } catch (Exception exc) {
+ DavGatewayTray.error(exc);
+ }
+ // make sure we do exit
+ System.exit(0);
+ }
+ }.start();
+ while (true) {
+ // wait for SWT init
+ try {
+ synchronized (mainThread) {
+ if (isReady) {
+ break;
+ }
+ mainThread.wait(1000);
+ }
+ } catch (InterruptedException e) {
+ DavGatewayTray.error(new BundleMessage("LOG_ERROR_WAITING_FOR_SWT_INIT"), e);
+ }
+ }
+ }
+
+}
diff --git a/src/java/davmail/util/IOUtil.java b/src/java/davmail/util/IOUtil.java
new file mode 100644
index 0000000..5a9f060
--- /dev/null
+++ b/src/java/davmail/util/IOUtil.java
@@ -0,0 +1,101 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.util;
+
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.*;
+
+/**
+ * Input output functions.
+ */
+public final class IOUtil {
+ private IOUtil() {
+ }
+
+ /**
+ * Write all inputstream content to outputstream.
+ *
+ * @param inputStream input stream
+ * @param outputStream output stream
+ * @throws IOException on error
+ */
+ public static void write(InputStream inputStream, OutputStream outputStream) throws IOException {
+ byte[] bytes = new byte[8192];
+ int length;
+ while ((length = inputStream.read(bytes)) > 0) {
+ outputStream.write(bytes, 0, length);
+ }
+ }
+
+
+ /**
+ * Resize image bytes to a max width or height image size.
+ *
+ * @param inputBytes input image bytes
+ * @param max max size
+ * @return scaled image bytes
+ * @throws IOException on error
+ */
+ public static byte[] resizeImage(byte[] inputBytes, int max) throws IOException {
+ BufferedImage inputImage = ImageIO.read(new ByteArrayInputStream(inputBytes));
+ BufferedImage outputImage = resizeImage(inputImage, max);
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ ImageIO.write(outputImage, "jpg", baos);
+ return baos.toByteArray();
+ }
+
+ /**
+ * Resize image to a max width or height image size.
+ *
+ * @param inputImage input image
+ * @param max max size
+ * @return scaled image
+ */
+ public static BufferedImage resizeImage(BufferedImage inputImage, int max) {
+ int width = inputImage.getWidth();
+ int height = inputImage.getHeight();
+ int targetWidth;
+ int targetHeight;
+ if (width <= max && height <= max) {
+ return inputImage;
+ } else if (width > height) {
+ targetWidth = max;
+ targetHeight = targetWidth * height / width;
+ } else {
+ targetHeight = max;
+ targetWidth = targetHeight * width / height;
+ }
+ Image scaledImage = inputImage.getScaledInstance(targetWidth, targetHeight, Image.SCALE_SMOOTH);
+ BufferedImage targetImage = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
+ targetImage.getGraphics().drawImage(scaledImage, 0, 0, null);
+ return targetImage;
+ }
+
+ public static byte[] readFully(InputStream inputStream) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ byte[] bytes = new byte[8192];
+ int length;
+ while ((length = inputStream.read(bytes)) > 0) {
+ baos.write(bytes, 0, length);
+ }
+ return baos.toByteArray();
+ }
+}
diff --git a/src/java/davmail/util/StringUtil.java b/src/java/davmail/util/StringUtil.java
new file mode 100644
index 0000000..9e6a4e4
--- /dev/null
+++ b/src/java/davmail/util/StringUtil.java
@@ -0,0 +1,507 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.util;
+
+import org.apache.commons.codec.DecoderException;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.binary.Hex;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * Various string handling methods
+ */
+public final class StringUtil {
+ private StringUtil() {
+ }
+
+ /**
+ * Return the sub string between startDelimiter and endDelimiter or null.
+ *
+ * @param value String value
+ * @param startDelimiter start delimiter
+ * @param endDelimiter end delimiter
+ * @return token value
+ */
+ public static String getToken(String value, String startDelimiter, String endDelimiter) {
+ String token = null;
+ if (value != null) {
+ int startIndex = value.indexOf(startDelimiter);
+ if (startIndex >= 0) {
+ startIndex += startDelimiter.length();
+ int endIndex = value.indexOf(endDelimiter, startIndex);
+ if (endIndex >= 0) {
+ token = value.substring(startIndex, endIndex);
+ }
+ }
+ }
+ return token;
+ }
+
+ /**
+ * Return the sub string between startDelimiter and endDelimiter or null,
+ * look for last token in string.
+ *
+ * @param value String value
+ * @param startDelimiter start delimiter
+ * @param endDelimiter end delimiter
+ * @return token value
+ */
+ public static String getLastToken(String value, String startDelimiter, String endDelimiter) {
+ String token = null;
+ if (value != null) {
+ int startIndex = value.lastIndexOf(startDelimiter);
+ if (startIndex >= 0) {
+ startIndex += startDelimiter.length();
+ int endIndex = value.indexOf(endDelimiter, startIndex);
+ if (endIndex >= 0) {
+ token = value.substring(startIndex, endIndex);
+ }
+ }
+ }
+ return token;
+ }
+
+ /**
+ * Return the sub string between startDelimiter and endDelimiter with newToken.
+ *
+ * @param value String value
+ * @param startDelimiter start delimiter
+ * @param endDelimiter end delimiter
+ * @param newToken new token value
+ * @return token value
+ */
+ public static String replaceToken(String value, String startDelimiter, String endDelimiter, String newToken) {
+ String result = null;
+ if (value != null) {
+ int startIndex = value.indexOf(startDelimiter);
+ if (startIndex >= 0) {
+ startIndex += startDelimiter.length();
+ int endIndex = value.indexOf(endDelimiter, startIndex);
+ if (endIndex >= 0) {
+ result = value.substring(0, startIndex) + newToken + value.substring(endIndex);
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Join values with given separator.
+ *
+ * @param values value set
+ * @param separator separator
+ * @return joined values
+ */
+ public static String join(Set<String> values, String separator) {
+ if (values != null && !values.isEmpty()) {
+ StringBuilder result = new StringBuilder();
+ for (String value : values) {
+ if (result.length() > 0) {
+ result.append(separator);
+ }
+ result.append(value);
+ }
+ return result.toString();
+ } else {
+ return null;
+ }
+ }
+
+ private static final Pattern AMP_PATTERN = Pattern.compile("&");
+ private static final Pattern LT_PATTERN = Pattern.compile("<");
+ private static final Pattern GT_PATTERN = Pattern.compile(">");
+ private static final Pattern PERCENT_PATTERN = Pattern.compile("%");
+ private static final Pattern HASH_PATTERN = Pattern.compile("#");
+ private static final Pattern STAR_PATTERN = Pattern.compile("\\*");
+
+ private static final Pattern QUOTE_PATTERN = Pattern.compile("\"");
+ private static final Pattern CR_PATTERN = Pattern.compile("\r");
+ private static final Pattern LF_PATTERN = Pattern.compile("\n");
+
+ private static final Pattern URLENCODED_F8FF_PATTERN = Pattern.compile(String.valueOf((char) 0xF8FF));
+ private static final Pattern URLENCODED_AMP_PATTERN = Pattern.compile("%26");
+ private static final Pattern URLENCODED_PLUS_PATTERN = Pattern.compile("%2B");
+ private static final Pattern URLENCODED_COLON_PATTERN = Pattern.compile("%3A");
+ private static final Pattern URLENCODED_LT_PATTERN = Pattern.compile("%3C");
+ private static final Pattern URLENCODED_GT_PATTERN = Pattern.compile("%3E");
+ private static final Pattern URLENCODED_QUOTE_PATTERN = Pattern.compile("%22");
+ private static final Pattern URLENCODED_X0D0A_PATTERN = Pattern.compile("\n");
+ private static final Pattern URLENCODED_PERCENT_PATTERN = Pattern.compile("%25");
+ private static final Pattern URLENCODED_HASH_PATTERN = Pattern.compile("%23");
+ private static final Pattern URLENCODED_STAR_PATTERN = Pattern.compile("%2A");
+ private static final Pattern URLENCODED_PIPE_PATTERN = Pattern.compile("%7C");
+ private static final Pattern URLENCODED_QUESTION_PATTERN = Pattern.compile("%3F");
+
+ private static final Pattern ENCODED_AMP_PATTERN = Pattern.compile("&");
+ private static final Pattern ENCODED_LT_PATTERN = Pattern.compile("<");
+ private static final Pattern ENCODED_GT_PATTERN = Pattern.compile(">");
+
+ private static final Pattern F8FF_PATTERN = Pattern.compile("_xF8FF_");
+ private static final Pattern X0D0A_PATTERN = Pattern.compile("_x000D__x000A_");
+
+ private static final Pattern PLUS_PATTERN = Pattern.compile("\\+");
+ private static final Pattern COLON_PATTERN = Pattern.compile(":");
+ private static final Pattern SLASH_PATTERN = Pattern.compile("/");
+ private static final Pattern UNDERSCORE_PATTERN = Pattern.compile("_");
+ private static final Pattern DASH_PATTERN = Pattern.compile("-");
+ private static final Pattern PIPE_PATTERN = Pattern.compile("\\|");
+ private static final Pattern QUESTION_PATTERN = Pattern.compile("\\?");
+
+ // WebDav search parameter encode
+ private static final Pattern APOS_PATTERN = Pattern.compile("'");
+
+ /**
+ * Encode & to %26 for urlcompname.
+ *
+ * @param name decoded name
+ * @return name encoded name
+ */
+ public static String urlEncodeAmpersand(String name) {
+ String result = name;
+ if (name.indexOf('&') >= 0) {
+ result = AMP_PATTERN.matcher(result).replaceAll("%26");
+ }
+ return result;
+ }
+
+ /**
+ * Decode %26 to & for urlcompname.
+ *
+ * @param name decoded name
+ * @return name encoded name
+ */
+ public static String urlDecodeAmpersand(String name) {
+ String result = name;
+ if (name != null && name.indexOf("%26") >= 0) {
+ result = URLENCODED_AMP_PATTERN.matcher(result).replaceAll("&");
+ }
+ return result;
+ }
+
+ /**
+ * Xml encode content.
+ *
+ * @param name decoded name
+ * @return name encoded name
+ */
+ public static String xmlEncode(String name) {
+ String result = name;
+ if (name != null) {
+ if (name.indexOf('&') >= 0) {
+ result = AMP_PATTERN.matcher(result).replaceAll("&");
+ }
+ if (name.indexOf('<') >= 0) {
+ result = LT_PATTERN.matcher(result).replaceAll("<");
+ }
+ if (name.indexOf('>') >= 0) {
+ result = GT_PATTERN.matcher(result).replaceAll(">");
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Xml encode inside attribute.
+ *
+ * @param name decoded name
+ * @return name encoded name
+ */
+ public static String xmlEncodeAttribute(String name) {
+ String result = xmlEncode(name);
+ if (result != null) {
+ if (result.indexOf('"') >= 0) {
+ result = QUOTE_PATTERN.matcher(result).replaceAll(""");
+ }
+ if (result.indexOf('\r') >= 0) {
+ result = CR_PATTERN.matcher(result).replaceAll("
");
+ }
+ if (result.indexOf('\n') >= 0) {
+ result = LF_PATTERN.matcher(result).replaceAll("
");
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Need to decode xml for iCal
+ *
+ * @param name encoded name
+ * @return name decoded name
+ */
+ public static String xmlDecode(String name) {
+ String result = name;
+ if (name.indexOf("&") >= 0) {
+ result = ENCODED_AMP_PATTERN.matcher(result).replaceAll("&");
+ }
+ if (name.indexOf("<") >= 0) {
+ result = ENCODED_LT_PATTERN.matcher(result).replaceAll("<");
+ }
+ if (name.indexOf(">") >= 0) {
+ result = ENCODED_GT_PATTERN.matcher(result).replaceAll(">");
+ }
+ return result;
+ }
+
+ /**
+ * Convert base64 value to hex.
+ *
+ * @param value base64 value
+ * @return hex value
+ */
+ public static String base64ToHex(String value) {
+ String hexValue = null;
+ if (value != null) {
+ hexValue = new String(Hex.encodeHex(Base64.decodeBase64(value.getBytes())));
+ }
+ return hexValue;
+ }
+
+ /**
+ * Convert hex value to base64.
+ *
+ * @param value hex value
+ * @return base64 value
+ * @throws DecoderException on error
+ */
+ public static String hexToBase64(String value) throws DecoderException {
+ String base64Value = null;
+ if (value != null) {
+ base64Value = new String(Base64.encodeBase64(Hex.decodeHex(value.toCharArray())));
+ }
+ return base64Value;
+ }
+
+ /**
+ * Encode item name to get actual value stored in urlcompname MAPI property.
+ *
+ * @param value decoded value
+ * @return urlcompname encoded value
+ */
+ public static String encodeUrlcompname(String value) {
+ String result = value;
+ if (result.indexOf('%') >= 0) {
+ result = PERCENT_PATTERN.matcher(result).replaceAll("%25");
+ }
+ if (result.indexOf("_xF8FF_") >= 0) {
+ result = F8FF_PATTERN.matcher(result).replaceAll(String.valueOf((char) 0xF8FF));
+ }
+ if (result.indexOf('&') >= 0) {
+ result = AMP_PATTERN.matcher(result).replaceAll("%26");
+ }
+ if (result.indexOf('+') >= 0) {
+ result = PLUS_PATTERN.matcher(result).replaceAll("%2B");
+ }
+ if (result.indexOf(':') >= 0) {
+ result = COLON_PATTERN.matcher(result).replaceAll("%3A");
+ }
+ if (result.indexOf('<') >= 0) {
+ result = LT_PATTERN.matcher(result).replaceAll("%3C");
+ }
+ if (result.indexOf('>') >= 0) {
+ result = GT_PATTERN.matcher(result).replaceAll("%3E");
+ }
+ if (result.indexOf('"') >= 0) {
+ result = QUOTE_PATTERN.matcher(result).replaceAll("%22");
+ }
+ if (result.indexOf('#') >= 0) {
+ result = HASH_PATTERN.matcher(result).replaceAll("%23");
+ }
+ if (result.indexOf('*') >= 0) {
+ result = STAR_PATTERN.matcher(result).replaceAll("%2A");
+ }
+ if (result.indexOf("_x000D__x000A_") >= 0) {
+ result = X0D0A_PATTERN.matcher(result).replaceAll("\r\n");
+ }
+ if (result.indexOf('|') >= 0) {
+ result = PIPE_PATTERN.matcher(result).replaceAll("%7C");
+ }
+ if (result.indexOf('?') >= 0) {
+ result = QUESTION_PATTERN.matcher(result).replaceAll("%3F");
+ }
+ return result;
+ }
+
+ /**
+ * Decode urlcompname to get item name.
+ *
+ * @param urlcompname encoded value
+ * @return decoded value
+ */
+ public static String decodeUrlcompname(String urlcompname) {
+ String result = urlcompname;
+ if (result != null) {
+ if (result.indexOf((char) 0xF8FF) >= 0) {
+ result = URLENCODED_F8FF_PATTERN.matcher(result).replaceAll("_xF8FF_");
+ }
+ if (result.indexOf("%26") >= 0) {
+ result = URLENCODED_AMP_PATTERN.matcher(result).replaceAll("&");
+ }
+ if (result.indexOf("%2B") >= 0) {
+ result = URLENCODED_PLUS_PATTERN.matcher(result).replaceAll("+");
+ }
+ if (result.indexOf("%3A") >= 0) {
+ result = URLENCODED_COLON_PATTERN.matcher(result).replaceAll(":");
+ }
+ if (result.indexOf("%3C") >= 0) {
+ result = URLENCODED_LT_PATTERN.matcher(result).replaceAll("<");
+ }
+ if (result.indexOf("%3E") >= 0) {
+ result = URLENCODED_GT_PATTERN.matcher(result).replaceAll(">");
+ }
+ if (result.indexOf("%22") >= 0) {
+ result = URLENCODED_QUOTE_PATTERN.matcher(result).replaceAll("\"");
+ }
+ // CRLF is replaced with LF in response
+ if (result.indexOf('\n') >= 0) {
+ result = URLENCODED_X0D0A_PATTERN.matcher(result).replaceAll("_x000D__x000A_");
+ }
+ if (result.indexOf("%23") >= 0) {
+ result = URLENCODED_HASH_PATTERN.matcher(result).replaceAll("#");
+ }
+ if (result.indexOf("%2A") >= 0) {
+ result = URLENCODED_STAR_PATTERN.matcher(result).replaceAll("*");
+ }
+ if (result.indexOf("%7C") >= 0) {
+ result = URLENCODED_PIPE_PATTERN.matcher(result).replaceAll("|");
+ }
+ if (result.indexOf("%3F") >= 0) {
+ result = URLENCODED_QUESTION_PATTERN.matcher(result).replaceAll("?");
+ }
+ // last replace %
+ if (result.indexOf("%25") >= 0) {
+ result = URLENCODED_PERCENT_PATTERN.matcher(result).replaceAll("%");
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Urlencode plus sign in encoded href.
+ * '+' is decoded as ' ' by URIUtil.decode, the workaround is to force urlencoding to '%2B' first
+ *
+ * @param value encoded href
+ * @return encoded href
+ */
+ public static String encodePlusSign(String value) {
+ String result = value;
+ if (result.indexOf('+') >= 0) {
+ result = PLUS_PATTERN.matcher(result).replaceAll("%2B");
+ }
+ return result;
+ }
+
+ /**
+ * Encode EWS base64 itemId to url compatible value.
+ *
+ * @param value base64 value
+ * @return url compatible value
+ */
+ public static String base64ToUrl(String value) {
+ String result = value;
+ if (result != null) {
+ if (result.indexOf('+') >= 0) {
+ result = PLUS_PATTERN.matcher(result).replaceAll("-");
+ }
+ if (result.indexOf('/') >= 0) {
+ result = SLASH_PATTERN.matcher(result).replaceAll("_");
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Encode EWS url compatible itemId back to base64 value.
+ *
+ * @param value url compatible value
+ * @return base64 value
+ */
+ public static String urlToBase64(String value) {
+ String result = value;
+ if (result.indexOf('-') >= 0) {
+ result = DASH_PATTERN.matcher(result).replaceAll("+");
+ }
+ if (result.indexOf('_') >= 0) {
+ result = UNDERSCORE_PATTERN.matcher(result).replaceAll("/");
+ }
+ return result;
+ }
+
+ /**
+ * Encode quotes in Dav search parameter.
+ *
+ * @param value search parameter
+ * @return escaped value
+ */
+ public static String davSearchEncode(String value) {
+ String result = value;
+ if (result.indexOf('\'') >= 0) {
+ result = APOS_PATTERN.matcher(result).replaceAll("''");
+ }
+ return result;
+ }
+
+ /**
+ * Get allday date value from zulu timestamp.
+ *
+ * @param value zulu datetime
+ * @return yyyyMMdd allday date value
+ */
+ public static String convertZuluDateTimeToAllDay(String value) {
+ String result = value;
+ if (value != null && value.length() != 8) {
+ // try to convert datetime value to date value
+ try {
+ Calendar calendar = Calendar.getInstance();
+ SimpleDateFormat dateParser = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
+ calendar.setTime(dateParser.parse(value));
+ calendar.add(Calendar.HOUR_OF_DAY, 12);
+ SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyyMMdd");
+ result = dateFormatter.format(calendar.getTime());
+ } catch (ParseException e) {
+ // ignore
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Remove quotes if present on value.
+ *
+ * @param value input value
+ * @return unquoted string
+ */
+ public static String removeQuotes(String value) {
+ String result = value;
+ if (result != null) {
+ if (result.startsWith("\"") || result.startsWith("{") || result.startsWith("(")) {
+ result = result.substring(1);
+ }
+ if (result.endsWith("\"") || result.endsWith("}") || result.endsWith(")")) {
+ result = result.substring(0, result.length() - 1);
+ }
+ }
+ return result;
+ }
+
+}
diff --git a/src/java/davmail/web/DavGatewayServletContextListener.java b/src/java/davmail/web/DavGatewayServletContextListener.java
new file mode 100644
index 0000000..e626b11
--- /dev/null
+++ b/src/java/davmail/web/DavGatewayServletContextListener.java
@@ -0,0 +1,59 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.web;
+
+import davmail.BundleMessage;
+import davmail.DavGateway;
+import davmail.Settings;
+import davmail.ui.tray.DavGatewayTray;
+
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Context Listener to start/stop DavMail
+ */
+public class DavGatewayServletContextListener implements ServletContextListener {
+ public void contextInitialized(ServletContextEvent event) {
+ InputStream settingInputStream = null;
+ try {
+ settingInputStream = DavGatewayServletContextListener.class.getClassLoader().getResourceAsStream("davmail.properties");
+ Settings.load(settingInputStream);
+ DavGateway.start();
+ } catch (IOException e) {
+ DavGatewayTray.error(new BundleMessage("LOG_ERROR_LOADING_SETTINGS"), e);
+ } finally {
+ if (settingInputStream != null) {
+ try {
+ settingInputStream.close();
+ } catch (IOException e) {
+ DavGatewayTray.debug(new BundleMessage("LOG_ERROR_CLOSING_CONFIG_FILE"), e);
+ }
+ }
+ }
+ DavGatewayTray.debug(new BundleMessage("LOG_DAVMAIL_STARTED"));
+ }
+
+ public void contextDestroyed(ServletContextEvent event) {
+ DavGatewayTray.debug(new BundleMessage("LOG_STOPPING_DAVMAIL"));
+ DavGateway.stop();
+ }
+}
diff --git a/src/java/davmailmessages.properties b/src/java/davmailmessages.properties
new file mode 100644
index 0000000..9dacdfe
--- /dev/null
+++ b/src/java/davmailmessages.properties
@@ -0,0 +1,296 @@
+EXCEPTION_AUTHENTICATION_FAILED=Authentication failed: invalid user or password
+EXCEPTION_AUTHENTICATION_FAILED_PASSWORD_EXPIRED=Authentication failed: password expired ?
+EXCEPTION_AUTHENTICATION_FAILED_RETRY=Authentication failed: invalid user or password, retry with domain\\user or use default domain setting
+EXCEPTION_CONNECTION_FAILED=Unable to connect to OWA at {0}, status code {1}, check configuration
+EXCEPTION_DAVMAIL_CONFIGURATION=DavMail configuration exception:\n{0}
+EXCEPTION_END_OF_STREAM=End of stream reached reading content
+EXCEPTION_ITEM_NOT_FOUND=Item not found
+EXCEPTION_EXCHANGE_LOGIN_FAILED=Exchange login exception: {0}
+EXCEPTION_SESSION_EXPIRED=Exchange session expired
+EXCEPTION_INVALID_CALDAV_REQUEST=Invalid Caldav request: {0}
+EXCEPTION_INVALID_CONTENT_LENGTH=Invalid content length: {0}
+EXCEPTION_INVALID_CONTENT_TYPE=Invalid content type: {0}
+EXCEPTION_INVALID_CREDENTIALS=Invalid credentials
+EXCEPTION_INVALID_DATE=Invalid date: {0}
+EXCEPTION_INVALID_DATES=Invalid dates: {0}
+EXCEPTION_INVALID_FOLDER_URL=Invalid folder URL: {0}
+EXCEPTION_INVALID_HEADER=Invalid header, HTTPS connection to an HTTP listener ?
+EXCEPTION_INVALID_KEEPALIVE=Invalid Keep-Alive: {0}
+EXCEPTION_INVALID_MAIL_PATH=Invalid mail path: {0}
+EXCEPTION_INVALID_MESSAGE_CONTENT=Invalid message content: {0}
+EXCEPTION_INVALID_MESSAGE_URL=Invalid message URL: {0}
+EXCEPTION_INVALID_RECIPIENT=Invalid recipient: {0}
+EXCEPTION_INVALID_REQUEST=Invalid request: {0}
+EXCEPTION_INVALID_SEARCH_PARAMETERS=Invalid search parameters: {0}
+EXCEPTION_UNSUPPORTED_PARAMETER=Unsupported parameter: {0}
+EXCEPTION_INVALID_PARAMETER=Invalid parameter: {0}
+EXCEPTION_NETWORK_DOWN=All network interfaces down or host unreachable !
+EXCEPTION_UNABLE_TO_CREATE_MESSAGE=Unable to create message {0}: {1}{2}{3}
+EXCEPTION_UNABLE_TO_GET_FOLDER=Unable to get folder at {0}
+EXCEPTION_UNABLE_TO_GET_MAIL_FOLDER=Unable to get mail folder at {0}, Webdav not available on Exchange server
+EXCEPTION_UNABLE_TO_MOVE_FOLDER=Unable to move folder, target already exists
+EXCEPTION_UNABLE_TO_MOVE_MESSAGE=Unable to move message, target already exists
+EXCEPTION_UNABLE_TO_COPY_MESSAGE=Unable to copy message, target already exists
+EXCEPTION_UNABLE_TO_PATCH_MESSAGE=Unable to patch message {0}: {1}{2}{3}
+EXCEPTION_UNABLE_TO_UPDATE_MESSAGE=Unable to update message properties
+EXCEPTION_CONNECT=Connect exception: {0} {1}
+EXCEPTION_UNSUPPORTED_AUTHORIZATION_MODE=Unsupported authorization mode: {0}
+EXCEPTION_UNSUPPORTED_VALUE=Unsupported value: {0}
+LOG_CLIENT_CLOSED_CONNECTION=Client closed connection
+LOG_CLOSE_CONNECTION_ON_TIMEOUT=Closing connection on timeout
+LOG_CONNECTION_CLOSED=Connection closed
+LOG_CONNECTION_FROM=Connection from {0} on port {1,number,#}
+LOG_DAVMAIL_GATEWAY_LISTENING=DavMail Gateway {0} listening on {1}
+LOG_DAVMAIL_STARTED=DavMail Gateway started
+LOG_ERROR_CLOSING_CONFIG_FILE=Error closing configuration file
+LOG_ERROR_LOADING_OSXADAPTER=Error while loading the OSXAdapter
+LOG_ERROR_LOADING_SETTINGS=Error loading settings
+LOG_ERROR_RETRIEVING_MESSAGE=Error retreiving message
+LOG_ERROR_WAITING_FOR_SWT_INIT=Error waiting for SWT init
+LOG_ITEM_NOT_AVAILABLE=Item {0} not available: {1}
+LOG_EXCEPTION_CLOSING_CLIENT_INPUT_STREAM=Exception closing client input stream
+LOG_EXCEPTION_CLOSING_CLIENT_OUTPUT_STREAM=Exception closing client output stream
+LOG_EXCEPTION_CLOSING_CLIENT_SOCKET=Exception closing client socket
+LOG_EXCEPTION_CLOSING_CONNECTION_ON_TIMEOUT=Exception closing connection on timeout
+LOG_EXCEPTION_CLOSING_SERVER_SOCKET=Exception closing server socket
+LOG_EXCEPTION_CREATING_SERVER_SOCKET=Exception creating server socket
+LOG_EXCEPTION_CREATING_SSL_SERVER_SOCKET=Unable to bind server socket for {0} on port {1,number,#}: Exception creating secured server socket : {2}
+LOG_EXCEPTION_GETTING_SOCKET_STREAMS=Exception while getting socket streams
+LOG_EXCEPTION_LISTENING_FOR_CONNECTIONS=Exception while listening for connections
+LOG_EXCEPTION_SENDING_ERROR_TO_CLIENT=Exception sending error to client
+LOG_EXCEPTION_WAITING_SERVER_THREAD_DIE=Exception waiting for server thread to die
+LOG_EXECUTE_FOLLOW_REDIRECTS=executeFollowRedirects({0})
+LOG_EXECUTE_FOLLOW_REDIRECTS_COUNT=executeFollowRedirects: {0} redirectCount:{1}
+LOG_EXTERNAL_CONNECTION_REFUSED=Connection from external client refused
+LOG_FOUND_ACCEPTED_CERTIFICATE=Found permanently accepted certificate, hash {0}
+LOG_FOUND_CALENDAR_MESSAGES=Found {0} calendar messages
+LOG_IMAP_COMMAND={0} on {1}
+LOG_INVALID_DEPTH=Invalid depth value: {0}
+LOG_INVALID_SETTING_VALUE=Invalid setting value in {0}
+LOG_INVALID_URL=Invalid URL: {0}
+LOG_JAVA6_DESKTOP_UNAVAILABLE=Java 6 Desktop class not available
+LOG_LDAP_IGNORE_FILTER_ATTRIBUTE=Ignoring filter attribute: {0}= {1}
+LOG_LDAP_REPLACED_UID_FILTER=Replaced {0} with {1} in uid filter
+LOG_LDAP_REQ_ABANDON_SEARCH=LDAP_REQ_ABANDON {0} for search {1}
+LOG_LDAP_REQ_UNBIND=LDAP_REQ_UNBIND {0}
+LOG_LDAP_REQ_BIND_ANONYMOUS=LDAP_REQ_BIND {0} anonymous
+LOG_LDAP_REQ_BIND_USER=LDAP_REQ_BIND {0} {1}
+LOG_LDAP_REQ_BIND_SUCCESS=LOG_LDAP_REQ_BIND Success
+LOG_LDAP_REQ_BIND_INVALID_CREDENTIALS=LDAP_REQ_BIND Invalid credentials
+LOG_LDAP_REQ_SEARCH=LDAP_REQ_SEARCH {0} base={1} scope: {2} sizelimit: {3} timelimit: {4} filter: {5} returning attributes: {6}
+LOG_LDAP_REQ_SEARCH_ANONYMOUS_ACCESS_FORBIDDEN=LDAP_REQ_SEARCH {0} Anonymous access to {1} forbidden
+LOG_LDAP_REQ_SEARCH_END=LDAP_REQ_SEARCH {0} end
+LOG_LDAP_REQ_SEARCH_FOUND_RESULTS=LDAP_REQ_SEARCH {0} found {1} results
+LOG_LDAP_REQ_SEARCH_INVALID_DN=LDAP_REQ_SEARCH {0} unrecognized dn {1}, use base context ou=people or o=od
+LOG_LDAP_REQ_SEARCH_SEND_PERSON=LDAP_REQ_SEARCH {0} send uid={1}{2} {3}
+LOG_LDAP_REQ_SEARCH_SIZE_LIMIT_EXCEEDED=LDAP_REQ_SEARCH {0} size limit exceeded
+LOG_LDAP_REQ_SEARCH_SUCCESS=LDAP_REQ_SEARCH {0} success
+LOG_LDAP_SEND_COMPUTER_CONTEXT=Sending computer context {0} {1}
+LOG_LDAP_SEND_ROOT_DSE=Sending root DSE
+LOG_LDAP_UNSUPPORTED_FILTER=Unsupported filter: {0}
+LOG_LDAP_UNSUPPORTED_FILTER_ATTRIBUTE=Unsupported filter attribute: {0}= {1}
+LOG_LDAP_UNSUPPORTED_FILTER_VALUE=Unsupported filter value
+LOG_LDAP_UNSUPPORTED_OPERATION=Unsupported operation: {0}
+LOG_LISTING_ITEM=Listing item {0}/{1}
+LOG_MESSAGE={0}
+LOG_NEW_VERSION_AVAILABLE=A new version ({0}) of DavMail Gateway is available !
+LOG_OPEN_LINK_NOT_SUPPORTED=Open link not supported (tried AWT Desktop and SWT Program)
+LOG_PROTOCOL_PORT={0} port {1,number,# }
+LOG_READ_CLIENT_AUTHORIZATION=< Authorization: ********
+LOG_READ_CLIENT_AUTH_PLAIN=< AUTH PLAIN ********
+LOG_READ_CLIENT_AUTH_LOGIN=< AUTH LOGIN ********
+LOG_READ_CLIENT_LINE=< {0}
+LOG_READ_CLIENT_LOGIN=< LOGIN ********
+LOG_READ_CLIENT_PASS=< PASS ********
+LOG_READ_CLIENT_PASSWORD=< ********
+LOG_REPORT_ITEM=Report item {0}/{1}
+LOG_GATEWAY_INTERRUPTED=Stopping DavMail gateway
+LOG_GATEWAY_STOP=DavMail gateway stopped
+LOG_SEARCHING_CALENDAR_MESSAGES=Searching calendar messages...
+LOG_SEARCH_QUERY=Search: {0}
+LOG_SEND_CLIENT_MESSAGE=> {0}
+LOG_SEND_CLIENT_PREFIX_MESSAGE=> {0}{1}
+LOG_SET_SOCKET_TIMEOUT=Set socket timeout to {0} seconds
+LOG_SOCKET_BIND_FAILED=Unable to bind server socket for {0} on port {1,number,#}: port not allowed or in use by another process\n
+LOG_STARTING_DAVMAIL=Starting DavMail Gateway...
+LOG_STOPPING_DAVMAIL=Stopping DavMail Gateway...
+LOG_SWT_NOT_AVAILABLE=SWT not available, fallback to JDK 1.6 system tray support
+LOG_SYSTEM_TRAY_NOT_AVAILABLE=JDK 1.6 needed for system tray support
+LOG_UNABLE_TO_CREATE_ICON=Unable to create icon
+LOG_UNABLE_TO_CREATE_LOG_FILE_DIR=Unable to create log file directory
+LOG_UNABLE_TO_CREATE_TRAY=Unable to create tray
+LOG_UNABLE_TO_GET_PARSEINTWITHTAG=Unable to get BerDecoder.parseIntWithTag method
+LOG_UNABLE_TO_GET_RELEASED_VERSION=Unable to get released version
+LOG_UNABLE_TO_LOAD_IMAGE=Unable to load image
+LOG_UNABLE_TO_LOAD_SETTINGS=Unable to load settings:
+LOG_UNABLE_TO_OPEN_LINK=Unable to open link
+LOG_UNABLE_TO_SET_ICON_IMAGE=Unable to set JDialog icon image (not available under Java 1.5)
+LOG_UNABLE_TO_SET_LOG_FILE_PATH=Unable to set log file path
+LOG_UNABLE_TO_SET_LOOK_AND_FEEL=Unable to set look and feel
+LOG_UNABLE_TO_SET_SYSTEM_LOOK_AND_FEEL=Unable to set system look and feel
+LOG_UNABLE_TO_STORE_SETTINGS=Unable to store settings:
+LOG_UNSUPPORTED_REQUEST=Unsupported request: {0}
+LOG_INVALID_TIMEZONE=Invalid timezone: {0}
+LOG_ACCESS_FORBIDDEN=Access to {0} forbidden: {1}
+LOG_DOWNLOAD_PROGRESS=Downloaded {0} KBytes from {1}
+LOG_WEBDAV_NOT_AVAILABLE=WebDav not available, retry with EWS mode
+UI_ABOUT=About...
+UI_ABOUT_DAVMAIL=About DavMail Gateway
+UI_ABOUT_DAVMAIL_AUTHOR=<html><b>DavMail Gateway</b><br>By Mickaël Guessant<br><br>
+UI_ACCEPT_CERTIFICATE=DavMail: Accept certificate ?
+UI_ALLOW_REMOTE_CONNECTION=Allow Remote Connections:
+UI_ALLOW_REMOTE_CONNECTION_HELP=Allow remote connections to the gateway (server mode)
+UI_PASSWORD_PROMPT=DavMail: Enter password
+UI_ANSWER_NO=n
+UI_ANSWER_YES=y
+UI_BIND_ADDRESS=Bind address:
+UI_BIND_ADDRESS_HELP=Bind only to the specified network address
+UI_BUTTON_ACCEPT=Accept
+UI_BUTTON_CANCEL=Cancel
+UI_BUTTON_DENY=Deny
+UI_BUTTON_HELP=Help
+UI_BUTTON_OK=OK
+UI_BUTTON_SAVE=Save
+UI_BUTTON_DEFAULT=Default
+UI_BUTTON_DEFAULT_HELP=Reset default logging levels
+UI_CALDAV_PORT=Caldav HTTP port:
+UI_CALDAV_PORT_HELP=Local Caldav server port to configure in Caldav (calendar) client
+UI_CALENDAR_PAST_EVENTS=Calendar past events (Caldav):
+UI_CALENDAR_PAST_EVENTS_HELP=Get events in the past not older than specified days count, leave empty for no limits
+UI_CURRENT_VERSION=Current version: {0}<br>
+UI_DAVMAIL_GATEWAY=DavMail Gateway
+UI_DAVMAIL_SETTINGS=DavMail Gateway Settings
+UI_DELAYS=Delays
+UI_DISABLE_UPDATE_CHECK=Disable update check:
+UI_DISABLE_UPDATE_CHECK_HELP=Disable DavMail check for new version
+UI_ENABLE_PROXY=Enable proxy:
+UI_ERROR_WAITING_FOR_CERTIFICATE_CHECK=Error waiting for certificate check
+UI_EXIT=Exit
+UI_FINGERPRINT=FingerPrint
+UI_GATEWAY=Gateway
+UI_HELP_INSTRUCTIONS=<br>Help and setup instructions available at:<br><a href=\"http://davmail.sourceforge.net\">http://davmail.sourceforge.net</a><br><br>To send comments or report bugs, <br>use <a href=\"http://sourceforge.net/tracker/?group_id=184600\">DavMail Sourceforge trackers</a><br>or contact me at <a href=\"mailto:mguessan at free.fr\">mguessan at free.fr</a></html>
+UI_IMAP_PORT=Local IMAP port:
+UI_IMAP_PORT_HELP=Local IMAP server port to configure in mail client
+UI_ISSUED_BY=Issued by
+UI_ISSUED_TO=Issued to
+UI_KEEP_DELAY=Trash keep delay (POP):
+UI_KEEP_DELAY_HELP=Number of days to keep messages in trash
+UI_KEY_PASSWORD=Key password:
+UI_KEY_PASSWORD_HELP=SSL key password inside key store
+UI_KEY_STORE=Key store:
+UI_KEY_STORE_HELP=SSL certificate key store file path
+UI_KEY_STORE_PASSWORD=Key store password:
+UI_KEY_STORE_PASSWORD_HELP=Key store password
+UI_KEY_STORE_TYPE=Key store type:
+UI_KEY_STORE_TYPE_HELP=Choose key store type
+UI_CLIENT_SO_TIMEOUT=Client connection timeout:
+UI_CLIENT_SO_TIMEOUT_HELP=Client connection timeout in seconds, 0 to disable timeout, empty for 5 minutes
+UI_CLIENT_KEY_STORE=Client key store:
+UI_CLIENT_KEY_STORE_HELP=SSL client certificate key store file path
+UI_CLIENT_KEY_STORE_PASSWORD=Client key store password:
+UI_CLIENT_KEY_STORE_PASSWORD_HELP=Client key store password, leave empty for runtime prompt
+UI_CLIENT_KEY_STORE_TYPE=Client key store type:
+UI_CLIENT_KEY_STORE_TYPE_HELP=Choose client certificate key store type, choose PKCS11 for smartcard
+UI_CLIENT_CERTIFICATE=Client Certificate (DavMail to Exchange)
+UI_PKCS11_LIBRARY=PKCS11 library:
+UI_PKCS11_LIBRARY_HELP=PKCS11 (smartcard) library path (.so or .dll)
+UI_PKCS11_CONFIG=PKCS11 config:
+UI_PKCS11_CONFIG_HELP=Optional additional PKCS11 settings (slot, nssArgs, ...)
+UI_LAST_LOG=Last log
+UI_LAST_MESSAGE=Last message
+UI_LATEST_VERSION=Latest version available: {0} <br>A new version of DavMail Gateway is available.<br><a href=\"http://sourceforge.net/project/platformdownload.php?group_id=184600\">Download latest version</a><br>
+UI_LDAP_PORT=Local LDAP port:
+UI_LDAP_PORT_HELP=Local LDAP server port to configure in directory (addresse book) client
+UI_LOGGING_LEVELS=Logging levels
+UI_LOGS=Logs
+UI_LOG_DAVMAIL=DavMail:
+UI_LOG_DEFAULT=Default:
+UI_LOG_HTTPCLIENT=HttpClient:
+UI_LOG_WIRE=Wire:
+UI_LOG_FILE_PATH=Log file path:
+UI_NETWORK=Network
+UI_NO_SSL=No SSL
+UI_OWA_URL=OWA (Exchange) URL:
+UI_OWA_URL_HELP=Base Outlook Web Access URL
+UI_POP_PORT=Local POP port:
+UI_POP_PORT_HELP=Local POP server port to configure in mail client
+UI_PROXY=Proxy
+UI_PROXY_PASSWORD=Proxy password:
+UI_PROXY_PORT=Proxy port:
+UI_PROXY_SERVER=Proxy server:
+UI_PROXY_USER=Proxy user:
+UI_SENT_KEEP_DELAY=Sent keep delay (POP):
+UI_SENT_KEEP_DELAY_HELP=Number of days to keep messages in sent folder
+UI_SERIAL=Serial
+UI_DAVMAIL_SERVER_CERTIFICATE=Server Certificate (Client to DavMail)
+UI_SERVER_CERTIFICATE=Exchange Server Certificate
+UI_SERVER_CERTIFICATE_HASH=Server certificate hash:
+UI_SERVER_CERTIFICATE_HASH_HELP=Manually accepted server certificate hash
+UI_SETTINGS=Settings...
+UI_SHOW_LOGS=Show logs...
+UI_SMTP_PORT=Local SMTP port:
+UI_SMTP_PORT_HELP=Local SMTP server port to configure in mail client
+UI_TAB_ADVANCED=Advanced
+UI_TAB_OSX=OSX
+UI_OSX=OSX
+UI_OSX_HIDE_FROM_DOCK=Hide from Dock
+UI_OSX_HIDE_FROM_DOCK_HELP=Hide application from Dock (restart needed)
+UI_TAB_ENCRYPTION=Encryption
+UI_TAB_MAIN=Main
+UI_TAB_NETWORK=Network
+UI_UNTRUSTED_CERTIFICATE=Server provided an untrusted certificate,\n you can choose to accept or deny access.\n Accept certificate (y/n)?
+UI_UNTRUSTED_CERTIFICATE_HTML=<html><b>Server provided an untrusted certificate,<br> you can choose to accept or deny access</b></html>
+UI_VALID_FROM=Valid from
+UI_VALID_UNTIL=Valid until
+MEETING_REQUEST=Meeting request
+LOG_EXCEPTION_CLOSING_KEYSTORE_INPUT_STREAM=Exception closing keystore input stream
+LOG_SUBFOLDER_ACCESS_FORBIDDEN=Subfolder access forbidden to {0}
+LOG_FOLDER_NOT_FOUND=Folder {0} not found
+LOG_FOLDER_ACCESS_FORBIDDEN=Folder access to {0} forbidden
+LOG_FOLDER_ACCESS_ERROR=Folder access to {0} error: {1}
+UI_OTP_PASSWORD_PROMPT=One Time (token) Password:
+UI_CAPTCHA_PROMPT=Enter pin value:
+UI_TAB_LOGGING=Logging
+UI_OTHER=Other
+UI_CALDAV_ALARM_SOUND=Caldav alarm sound:
+UI_CALDAV_ALARM_SOUND_HELP=Convert Caldav alarm to sound alarm supported by iCal, e.g. Basso
+UI_FORCE_ACTIVESYNC_UPDATE=Force ActiveSync update:
+UI_FORCE_ACTIVESYNC_UPDATE_HELP=Force update of Caldav events for ActiveSync connected devices
+UI_DEFAULT_DOMAIN=Default windows domain:
+UI_DEFAULT_DOMAIN_HELP=Default windows domain name
+UI_USE_SYSTEM_PROXIES=Use system proxy settings:
+UI_SHOW_STARTUP_BANNER=Display startup banner
+UI_SHOW_STARTUP_BANNER_HELP=Whether to show the initial startup notification window or not
+UI_DISABLE_GUI_NOTIFICATIONS=Disable balloon notifications
+UI_DISABLE_GUI_NOTIFICATIONS_HELP=Disable all graphical notifications
+UI_IMAP_AUTO_EXPUNGE=IMAP auto expunge:
+UI_IMAP_AUTO_EXPUNGE_HELP=Delete messages immediately on the server over IMAP
+UI_POP_MARK_READ=POP mark read:
+UI_POP_MARK_READ_HELP=Mark messages read on server immediately after retrieval
+UI_IMAP_IDLE_DELAY=IDLE folder monitor delay (IMAP):
+UI_IMAP_IDLE_DELAY_HELP=IMAP folder idle monitor delay in minutes, leave empty to disable IDLE support
+EXCEPTION_EWS_NOT_AVAILABLE=EWS end point not available
+EXCEPTION_FOLDER_NOT_FOUND=Folder {0} not found
+UNKNOWN_ATTRIBUTE=Unknown attribute: {0}
+NEEDS-ACTION=
+ACCEPTED=Accepted:
+TENTATIVE=Tentative:
+DECLINED=Declined:
+UI_ENABLE_EWS=Exchange Protocol
+UI_ENABLE_EWS_HELP=Choose EWS on Exchange 2010 or Exchange 2007 with Webdav disabled
+UI_CALDAV_NOTIFICATION=DavMail: Caldav scheduling notification
+UI_BUTTON_SEND=Send
+UI_TO=To:
+UI_TO_HELP=Recipients
+UI_CC=Cc:
+UI_CC_HELP=Copy recipients
+UI_SUBJECT=Subject:
+UI_SUBJECT_HELP=Caldav notification subject
+UI_NOTIFICATION_BODY=Caldav notification comment
+UI_CALDAV_EDIT_NOTIFICATIONS=Edit Caldav notifications:
+UI_CALDAV_EDIT_NOTIFICATIONS_HELP=Enable interactive Caldav edit notification window
+LOG_SEARCH_RESULT=Found {0} item(s)
+UI_LOG_FILE_SIZE=Log file size:
+UI_SAVE_IN_SENT=SMTP save in sent:
+UI_SAVE_IN_SENT_HELP=Save messages sent over SMTP in server Sent folder
+UI_CERTIFICATE_ALIAS_PROMPT=Select a certificate
\ No newline at end of file
diff --git a/src/java/davmailmessages_fr.properties b/src/java/davmailmessages_fr.properties
new file mode 100644
index 0000000..e9c10b3
--- /dev/null
+++ b/src/java/davmailmessages_fr.properties
@@ -0,0 +1,295 @@
+EXCEPTION_AUTHENTICATION_FAILED=Echec d''authentification : identifiant ou mot de passe invalide
+EXCEPTION_AUTHENTICATION_FAILED_PASSWORD_EXPIRED=Echec d''authentification : mot de passe expiré ?
+EXCEPTION_AUTHENTICATION_FAILED_RETRY=Echec d''authentification : identifiant ou mot de passe invalide, réessayer avec domaine\\utilisateur ou utiliser le paramètre domaine par défaut
+EXCEPTION_CONNECTION_FAILED=Connection OWA à {0} impossible, code retour {1}, vérifier la configuration
+EXCEPTION_DAVMAIL_CONFIGURATION=Erreur de configuration DavMail :\n{0}
+EXCEPTION_END_OF_STREAM=Fin de flux âtteint pendant la lecture du contenu
+EXCEPTION_ITEM_NOT_FOUND=Elément non trouvé
+EXCEPTION_EXCHANGE_LOGIN_FAILED=Exception lors de la connexion Exchange : {0}
+EXCEPTION_INVALID_CALDAV_REQUEST=Reuqête Caldav invalide : {0}
+EXCEPTION_INVALID_CONTENT_LENGTH=Longueur du contenu invalide : {0}
+EXCEPTION_INVALID_CONTENT_TYPE=Type de contenu invalide : {0}
+EXCEPTION_INVALID_CREDENTIALS=Identifiant ou mot de passe invalide
+EXCEPTION_INVALID_DATE=Date invalide {0}
+EXCEPTION_INVALID_DATES=Dates invalides : {0}
+EXCEPTION_INVALID_FOLDER_URL=URL du dossier invalide : {0}
+EXCEPTION_INVALID_HEADER=Entête invalide, connexion HTTPS sur le service HTTP ?
+EXCEPTION_INVALID_KEEPALIVE=Keep-Alive invalide : {0}
+EXCEPTION_INVALID_MAIL_PATH=Chemin de messagerie invalide : {0}
+EXCEPTION_INVALID_MESSAGE_CONTENT=Contenu du message invalide : {0}
+EXCEPTION_INVALID_MESSAGE_URL=URL de message invalide : {0}
+EXCEPTION_INVALID_RECIPIENT=Destinataire invalide : {0}
+EXCEPTION_INVALID_REQUEST=Requête invalide {0}
+EXCEPTION_INVALID_SEARCH_PARAMETERS=Paramètres de recherche invalides : {0}
+EXCEPTION_NETWORK_DOWN=Toutes les interfaces réseaux sont indisponibles ou serveur non joignable !
+EXCEPTION_UNABLE_TO_CREATE_MESSAGE=Impossible de créer le message {0} : {1}{2}{3}
+EXCEPTION_UNABLE_TO_GET_FOLDER=Impossible d''obtenir le dossier {0}
+EXCEPTION_UNABLE_TO_GET_MAIL_FOLDER=Impossible d''obtenir le dossier de messagerie à l''adresse {0}, Webdav non disponible sur le serveur Exchange
+EXCEPTION_UNABLE_TO_MOVE_FOLDER=Impossible de déplacer le dossier, la cible existe
+EXCEPTION_UNABLE_TO_MOVE_MESSAGE=Impossible de déplacer le message, la cible existe
+EXCEPTION_UNABLE_TO_COPY_MESSAGE=Impossible de copier le message, la cible existe
+EXCEPTION_UNABLE_TO_PATCH_MESSAGE=Impossible de mettre ) jour le message {0} : {1}{2}{3}
+EXCEPTION_UNABLE_TO_UPDATE_MESSAGE=Impossible de mettre à jour les propriétés du message
+EXCEPTION_CONNECT=Exception lors de la connexion : {0} {1}
+EXCEPTION_UNSUPPORTED_AUTHORIZATION_MODE=Mode d'authentification invalide : {0}
+EXCEPTION_UNSUPPORTED_VALUE=Valeur non supportée : {0}
+LOG_CLIENT_CLOSED_CONNECTION=Connection fermée par le client
+LOG_CLOSE_CONNECTION_ON_TIMEOUT=Connection fermée sur expiration
+LOG_CONNECTION_CLOSED=Connection fermée
+LOG_CONNECTION_FROM=Connection de {0} sur le port {1,number,#}
+LOG_DAVMAIL_GATEWAY_LISTENING=Passerelle DavMail {0} en écoute sur {1}
+LOG_DAVMAIL_STARTED=Passerelle DavMail démarrée
+LOG_ERROR_CLOSING_CONFIG_FILE=Erreur à la fermeture du fichier de configuration
+LOG_ERROR_LOADING_OSXADAPTER=Erreur au chargement de OSXAdapter
+LOG_ERROR_LOADING_SETTINGS=Erreur de chargement de la configuration
+LOG_ERROR_RETRIEVING_MESSAGE=Erreur lors la récupération du message
+LOG_ERROR_WAITING_FOR_SWT_INIT=Erreur d''initialisation SWT
+LOG_ITEM_NOT_AVAILABLE=Evènement {0} non disponible : {1}
+LOG_EXCEPTION_CLOSING_CLIENT_INPUT_STREAM=Erreur à la fermeture du flux d''entrée client
+LOG_EXCEPTION_CLOSING_CLIENT_OUTPUT_STREAM=Erreur à la fermeture du flux de sortie client
+LOG_EXCEPTION_CLOSING_CLIENT_SOCKET=Erreur à la fermeture de la connection client
+LOG_EXCEPTION_CLOSING_CONNECTION_ON_TIMEOUT=Erreur à la fermeture de la connexion sur expiration
+LOG_EXCEPTION_CLOSING_SERVER_SOCKET=Erreur à la fermeture du port d''écoute serveur
+LOG_EXCEPTION_CREATING_SERVER_SOCKET=Erreur lors de la création du port d''écoute serveur
+LOG_EXCEPTION_CREATING_SSL_SERVER_SOCKET=Impossible d''ouvrir le port d''écoute {1,number,#} pour {0} : Erreur lors de la création du port d''écoute serveur sécurisé : {2}
+LOG_EXCEPTION_GETTING_SOCKET_STREAMS=Erreur lors de l''établissement des flux de la connexion
+LOG_EXCEPTION_LISTENING_FOR_CONNECTIONS=Erreur pendant l''attente des connexion entrantes
+LOG_EXCEPTION_SENDING_ERROR_TO_CLIENT=Erreur d''envoi du message d''erreur au client
+LOG_EXCEPTION_WAITING_SERVER_THREAD_DIE=Erreur pendant l''attente de fin du processus serveur
+LOG_EXECUTE_FOLLOW_REDIRECTS=executeFollowRedirects({0})
+LOG_EXECUTE_FOLLOW_REDIRECTS_COUNT=executeFollowRedirects: {0} redirectCount: {1}
+LOG_EXTERNAL_CONNECTION_REFUSED=Connexion du client distant refusée
+LOG_FOUND_ACCEPTED_CERTIFICATE=Certificat définitivement accepté trouvé, hash {0}
+LOG_FOUND_CALENDAR_MESSAGES={0} messages trouvés dans le calendrier
+LOG_IMAP_COMMAND={0} sur {1}
+LOG_INVALID_DEPTH=Profondeur invalide : {0}
+LOG_INVALID_SETTING_VALUE=Paramètre de configuration {0} invalide
+LOG_INVALID_URL=URL invalide : {0}
+LOG_JAVA6_DESKTOP_UNAVAILABLE=Classe Java 6 Desktop non disponible
+LOG_LDAP_IGNORE_FILTER_ATTRIBUTE=Filtre d''attribut ignoré : {0}= {1}
+LOG_LDAP_REPLACED_UID_FILTER=Remplacé {0} par {1} dans le filtre uid
+LOG_LDAP_REQ_ABANDON_SEARCH=LDAP_REQ_ABANDON {0} pour la recherche {1}
+LOG_LDAP_REQ_UNBIND=LDAP_REQ_UNBIND {0}
+LOG_LDAP_REQ_BIND_ANONYMOUS=LDAP_REQ_BIND {0} anonyme
+LOG_LDAP_REQ_BIND_USER=LOG_LDAP_REQ_BIND_USER
+LOG_LDAP_REQ_SEARCH=LDAP_REQ_SEARCH {0} base={1} scope: {2} sizelimit: {3} timelimit: {4} filter: {5} returning attributes: {6}
+LOG_LDAP_REQ_SEARCH_ANONYMOUS_ACCESS_FORBIDDEN=LDAP_REQ_SEARCH {0} Accès anonyme à {1} interdit
+LOG_LDAP_REQ_SEARCH_END=LDAP_REQ_SEARCH {0} fin
+LOG_LDAP_REQ_SEARCH_FOUND_RESULTS=LDAP_REQ_SEARCH {0} retourne {1} résultats
+LOG_LDAP_REQ_SEARCH_INVALID_DN=LDAP_REQ_SEARCH {0} dn non reconnu {1}, utiliser le contexte ou=people ou o=od
+LOG_LDAP_REQ_SEARCH_SEND_PERSON=LDAP_REQ_SEARCH {0} envoi uid={1}{2} {3}
+LOG_LDAP_REQ_SEARCH_SIZE_LIMIT_EXCEEDED=LDAP_REQ_SEARCH {0} limite de taille dépassée
+LOG_LDAP_REQ_SEARCH_SUCCESS=LDAP_REQ_SEARCH {0} succès
+LOG_LDAP_SEND_COMPUTER_CONTEXT=Envoi contexte computer {0} {1}
+LOG_LDAP_SEND_ROOT_DSE=Envoi racine DSE
+LOG_LDAP_UNSUPPORTED_FILTER=Filtre non supporté : {0}
+LOG_LDAP_UNSUPPORTED_FILTER_ATTRIBUTE=Attribut de filtre non supporté : {0}= {1}
+LOG_LDAP_UNSUPPORTED_FILTER_VALUE=Valeur de filtre non supportée
+LOG_LDAP_UNSUPPORTED_OPERATION=Opération non supportée : {0}
+LOG_LISTING_ITEM=Liste élément {0}/{1}
+LOG_MESSAGE={0}
+LOG_NEW_VERSION_AVAILABLE=Une nouvelle version ({0}) de la Passerelle DavMail est disponible !
+LOG_OPEN_LINK_NOT_SUPPORTED=Ouverture de lien impossible (avec AWT Desktop et SWT Program)
+LOG_PROTOCOL_PORT=port {0} : {1,number,# }
+LOG_READ_CLIENT_AUTHORIZATION=< Authorization: ********
+LOG_READ_CLIENT_AUTH_PLAIN=< AUTH PLAIN ********
+LOG_READ_CLIENT_LINE=< {0}
+LOG_READ_CLIENT_LOGIN=< LOGIN ********
+LOG_READ_CLIENT_PASS=< PASS ********
+LOG_READ_CLIENT_PASSWORD=< ********
+LOG_REPORT_ITEM=Envoi élément {0}/{1}
+LOG_SEARCHING_CALENDAR_MESSAGES=Recherche des messages de calendrier...
+LOG_SEARCH_QUERY=Recherche : {0}
+LOG_SEND_CLIENT_MESSAGE=> {0}
+LOG_SEND_CLIENT_PREFIX_MESSAGE=> {0}{1}
+LOG_SET_SOCKET_TIMEOUT=Expiration de lecture de la connection positionnée à {0} secondes
+LOG_SOCKET_BIND_FAILED=Impossible d''ouvrir le port d''écoute {1,number,#} pour {0} : port non autorisé ou utilisé par un autre processus\n
+LOG_STARTING_DAVMAIL=Démarrage de la passerelle DavMail...
+LOG_STOPPING_DAVMAIL=Arrêt de la passerelle DavMail...
+LOG_SWT_NOT_AVAILABLE=SWT non disponible, bascule vers le support icône de notification de Java 1.6
+LOG_SYSTEM_TRAY_NOT_AVAILABLE=JDK 1.6 nécessaire pour le support de l''icône de notification
+LOG_UNABLE_TO_CREATE_ICON=Impossible de créer l''icône
+LOG_UNABLE_TO_CREATE_LOG_FILE_DIR=Impossible de créer le répertoire de traces
+LOG_UNABLE_TO_CREATE_TRAY=Impossible de créer l''icône de notification
+LOG_UNABLE_TO_GET_PARSEINTWITHTAG=Erreur d''accès à la méthode BerDecoder.parseIntWithTag
+LOG_UNABLE_TO_GET_RELEASED_VERSION=Impossible de récupérer le numéro de dernière version
+LOG_UNABLE_TO_LOAD_IMAGE=Impossible de charger l''image
+LOG_UNABLE_TO_LOAD_SETTINGS=Impossible de charger la configuration :
+LOG_UNABLE_TO_OPEN_LINK=Impossible d''ouvrir le lien
+LOG_UNABLE_TO_SET_ICON_IMAGE=Impossible de positionner l''icône de JDialog (non disponible en Java 1.5)
+LOG_UNABLE_TO_SET_LOG_FILE_PATH=Echec à la mise à jour du chemin du fichier de traces
+LOG_UNABLE_TO_SET_LOOK_AND_FEEL=Impossible de définir le style de l''interface
+LOG_UNABLE_TO_SET_SYSTEM_LOOK_AND_FEEL=Impossible de définir le style natif sur l''interface
+LOG_UNABLE_TO_STORE_SETTINGS=Impossible d''enregistrer la configuration
+LOG_UNSUPPORTED_REQUEST=Requête non supportée : {0}
+LOG_DOWNLOAD_PROGRESS={0} KOctets téléchargés de {1}
+LOG_WEBDAV_NOT_AVAILABLE=WebDav non disponible, réessayer en activant EWS
+UI_ABOUT=A propos...
+UI_ABOUT_DAVMAIL=A propos de la Passerelle DavMail
+UI_ABOUT_DAVMAIL_AUTHOR=<html><b>Passerelle DavMail</b><br>Par Mickaël Guessant<br><br>
+UI_ACCEPT_CERTIFICATE=DavMail : Accepter le certificat ?
+UI_ALLOW_REMOTE_CONNECTION=Autoriser connexions distantes :
+UI_ALLOW_REMOTE_CONNECTION_HELP=Autoriser les connexions distantes à la passerelle (mode serveur)
+UI_ANSWER_NO=n
+UI_ANSWER_YES=o
+UI_BIND_ADDRESS=Adresse d''écoute :
+UI_BIND_ADDRESS_HELP=Ecouter seulement sur l''adresse définie
+UI_BUTTON_ACCEPT=Accepter
+UI_BUTTON_CANCEL=Annuler
+UI_BUTTON_DENY=Refuser
+UI_BUTTON_HELP=Aide
+UI_BUTTON_OK=OK
+UI_BUTTON_SAVE=Enregistrer
+UI_BUTTON_DEFAULT=Défaut
+UI_BUTTON_DEFAULT_HELP=Réinitialiser les niveaux de trace par défaut
+UI_CALDAV_PORT=Port HTTP Caldav :
+UI_CALDAV_PORT_HELP=Port local Caldav à configurer dans le client Caldav (agenda)
+UI_CALENDAR_PAST_EVENTS=Jours passés du calendrier (Caldav) :
+UI_CALENDAR_PAST_EVENTS_HELP=Limiter les évènements remontés
+UI_CURRENT_VERSION=Version actuelle : {0}<br>
+UI_DAVMAIL_GATEWAY=Passerelle DavMail
+UI_DAVMAIL_SETTINGS=Configuration Passerelle DavMail
+UI_DELAYS=Délais
+UI_DISABLE_UPDATE_CHECK=Désactivation contrôle version :
+UI_DISABLE_UPDATE_CHECK_HELP=Désactiver le contrôle de nouvelle version disponible
+UI_ENABLE_PROXY=Activer proxy :
+UI_ERROR_WAITING_FOR_CERTIFICATE_CHECK=Erreur lors de l''attente de validation du certificat
+UI_EXIT=Quitter
+UI_FINGERPRINT=Empreinte
+UI_GATEWAY=Passerelle
+UI_HELP_INSTRUCTIONS=<br>Aide et instructions disponibles sur :<br><a href=\"http://davmail.sourceforge.net\">http://davmail.sourceforge.net</a><br><br>Pour envoyer des commentaires ou signaler des anomalies, <br>utiliser <a href=\"http://sourceforge.net/tracker/?group_id=184600\">DavMail Sourceforge trackers</a><br>ou me contacter à l''adresse <a href=\"mailto:mguessan at free.fr\">mguessan at free.fr</a></html>
+UI_IMAP_PORT=Port IMAP local :
+UI_IMAP_PORT_HELP=Port IMAP local à configurer dans le client de messagerie
+UI_ISSUED_BY=Emis par
+UI_ISSUED_TO=Emis pour
+UI_KEEP_DELAY=Délai de rétention corbeille (POP) :
+UI_KEEP_DELAY_HELP=Nombre de jours de conservation des messages dans la corbeille
+UI_KEY_PASSWORD=Mot de passe clé :
+UI_KEY_PASSWORD_HELP=Mot de passe clé SSL contenue dans le fichier des clés
+UI_KEY_STORE=Fichier clés :
+UI_KEY_STORE_HELP=Chemin du fichier contenant les clés et certificats SSL
+UI_KEY_STORE_PASSWORD=Mot de passe fichier clés :
+UI_KEY_STORE_PASSWORD_HELP=Mot de passe du fichier des clés
+UI_KEY_STORE_TYPE=Type de fichier clés :
+UI_KEY_STORE_TYPE_HELP=Choix du type de fichier de clés
+UI_LAST_LOG=Dernière trace
+UI_LAST_MESSAGE=Dernier message
+UI_LATEST_VERSION=Dernière version disponible : {0} <br>Une nouvelle version de la Passerelle DavMail est disponible.<br><a href=\"http://sourceforge.net/project/platformdownload.php?group_id=184600\">Télécharcher la dernière version</a><br>
+UI_LDAP_PORT=Port LDAP local :
+UI_LDAP_PORT_HELP=Port LDAP local à configurer dans le client annuaire (carnet d''adresse)
+UI_LOGGING_LEVELS=Niveaux de traces
+UI_LOGS=Traces
+UI_LOG_DAVMAIL=DavMail :
+UI_LOG_DEFAULT=Défaut :
+UI_LOG_HTTPCLIENT=HttpClient :
+UI_LOG_WIRE=Réseau :
+UI_NETWORK=Réseau
+UI_NO_SSL=Pas de SSL
+UI_OWA_URL=URL OWA (Exchange) :
+UI_OWA_URL_HELP=URL de connexion Outlook Web Access
+UI_POP_PORT=Port POP local :
+UI_POP_PORT_HELP=Port POP local à configurer dans le client de messagerie
+UI_PROXY=Proxy
+UI_PROXY_PASSWORD=Mot de passe proxy :
+UI_PROXY_PORT=Port du serveur proxy :
+UI_PROXY_SERVER=Serveur proxy :
+UI_PROXY_USER=Identifiant proxy :
+UI_SENT_KEEP_DELAY=Délai de rétention envoyés (POP) :
+UI_SENT_KEEP_DELAY_HELP=Nombre de jours de conservation des messages dans le dossier des messages envoyés
+UI_SERIAL=Numéro de série
+UI_DAVMAIL_SERVER_CERTIFICATE=Certificat Serveur (Client vers DavMail)
+UI_SERVER_CERTIFICATE=Certificat Serveur Exchange
+UI_SERVER_CERTIFICATE_HASH=Hash du certificat serveur :
+UI_SERVER_CERTIFICATE_HASH_HELP=Hash du certificat accepté manuellement
+UI_SETTINGS=Configuration...
+UI_SHOW_LOGS=Afficher les traces...
+UI_SMTP_PORT=Port SMTP local :
+UI_SMTP_PORT_HELP=Port SMTP local à configurer dans le client de messagerie
+UI_TAB_ADVANCED=Avancé
+UI_TAB_ENCRYPTION=Chiffrement
+UI_TAB_MAIN=Général
+UI_TAB_NETWORK=Réseau
+UI_UNTRUSTED_CERTIFICATE=Le certificat fourni par le serveur n''est certifié par aucune autorité de confiance,\n vous pouvez choisir d''accepter ou de rejeter l''accès
+UI_UNTRUSTED_CERTIFICATE_HTML=<html><b>Le certificat fourni par le serveur n''est certifié par aucune autorité de confiance,<br> vous pouvez choisir d''accepter ou de rejeter l''accès</b></html>
+UI_VALID_FROM=Emis le
+UI_VALID_UNTIL=Expire le
+UI_PASSWORD_PROMPT=DavMail : Entrer le mot de passe
+UI_PKCS11_LIBRARY_HELP=Chemin de la librarie PKCS11 (carte à puce) (.so or .dll)
+UI_PKCS11_LIBRARY=Librairie PKCS11 :
+UI_PKCS11_CONFIG_HELP=Configuration PKCS11 complémentaire optionnelle (slot, nssArgs, ...)
+UI_PKCS11_CONFIG=Configuration PKCS11 :
+UI_CLIENT_SO_TIMEOUT=Délai d'attente client:
+UI_CLIENT_SO_TIMEOUT_HELP=Délai d'attente client en secondes, 0 pour désactiver, vide pour 5 minutes
+UI_CLIENT_CERTIFICATE=Certificat client (DavMail vers Exchange)
+UI_LOG_FILE_PATH=Chemin du fichier de traces :
+UI_LOG_FILE_SIZE=Taille du fichier de traces :
+LOG_GATEWAY_INTERRUPTED=Arrêt de la passerelle DavMail en cours
+LOG_GATEWAY_STOP=Passerelle DavMail arrêtée
+LOG_INVALID_TIMEZONE=Fuseau horaire invalide : {0}
+MEETING_REQUEST=Invitation
+LOG_ACCESS_FORBIDDEN=Accès à {0} non autorisé: {1}
+LOG_LDAP_REQ_BIND_INVALID_CREDENTIALS=LDAP_REQ_BIND Utilisateur ou mot de passe invalide
+LOG_LDAP_REQ_BIND_SUCCESS=LOG_LDAP_REQ_BIND Authentification réussie
+LOG_EXCEPTION_CLOSING_KEYSTORE_INPUT_STREAM=Erreur à la fermeture du flux d''entrée du fichier de clés
+LOG_SUBFOLDER_ACCESS_FORBIDDEN=Accès interdit au sous dossiers de {0}
+LOG_FOLDER_ACCESS_FORBIDDEN=Accès interdit au dossier {0}
+LOG_FOLDER_NOT_FOUND=Dossier {0} introuvable
+LOG_FOLDER_ACCESS_ERROR=Erreur lors de l''accès au dossier {0} : {1}
+UI_OTP_PASSWORD_PROMPT=Mot de passe du jeton :
+UI_CAPTCHA_PROMPT=Code PIN :
+EXCEPTION_SESSION_EXPIRED=Session Exchange expirée
+UI_CLIENT_KEY_STORE_TYPE=Type de stockage :
+UI_CLIENT_KEY_STORE_TYPE_HELP=Choisir le type de stockage du certificat client, PKCS11 pour une carte à puce
+UI_CLIENT_KEY_STORE=Fichier certificat client :
+UI_CLIENT_KEY_STORE_HELP=Chemin du fichier contenant le certificat client SSL
+UI_CLIENT_KEY_STORE_PASSWORD=Mot de passe certificat client :
+UI_CLIENT_KEY_STORE_PASSWORD_HELP=Mot de passe du certificat client, laisser vide pour fournir le mot de passe mode interactif
+UI_TAB_LOGGING=Traces
+UI_TAB_OSX=OSX
+UI_OSX=OSX
+UI_OSX_HIDE_FROM_DOCK=Supprimer du Dock
+UI_OSX_HIDE_FROM_DOCK_HELP=Supprimer l''application du Dock (redémarrage nécessaire)
+UI_OTHER=Autres
+UI_CALDAV_ALARM_SOUND=Son des alarmes Caldav :
+UI_CALDAV_ALARM_SOUND_HELP=Convertir les alarmes Caldav en alarmes sonores supportées par iCal, par exemple Basso
+UI_FORCE_ACTIVESYNC_UPDATE=Forcer ActiveSync :
+UI_FORCE_ACTIVESYNC_UPDATE_HELP=Forcer la mise à jour des évènements Caldav pour les appareils connectés via ActiveSync
+UI_DEFAULT_DOMAIN=Domaine windows par défaut :
+UI_DEFAULT_DOMAIN_HELP=Nom du domaine windows par défaut
+EXCEPTION_UNSUPPORTED_PARAMETER=Paramètre non supporté : {0}
+EXCEPTION_INVALID_PARAMETER=Paramètre invalide : {0}
+UI_USE_SYSTEM_PROXIES=Utiliser la configuration système :
+UI_SHOW_STARTUP_BANNER=Notification au lancement :
+UI_SHOW_STARTUP_BANNER_HELP=Afficher ou non la fenêtre de notification au démarrage
+UI_DISABLE_GUI_NOTIFICATIONS=Désactiver notifications graphiques :
+UI_DISABLE_GUI_NOTIFICATIONS_HELP=Supprimer toutes les notifications graphiques
+LOG_READ_CLIENT_AUTH_LOGIN=< AUTH LOGIN ********
+UI_IMAP_IDLE_DELAY=Délai de surveillance dossier (IMAP) :
+UI_IMAP_IDLE_DELAY_HELP=Délai de surveillance du dossier IMAP en minutes, laisser vide pour désactiver le support IDLE
+UI_IMAP_AUTO_EXPUNGE=IMAP suppression immédiate :
+UI_IMAP_AUTO_EXPUNGE_HELP=Supprimer immédiatement les messages du serveur via IMAP
+UI_POP_MARK_READ=POP marquer lu :
+UI_POP_MARK_READ_HELP=Marquer les messages lus sur le serveur immédiatement après chargement
+EXCEPTION_EWS_NOT_AVAILABLE=Point d''accès EWS non disponible
+EXCEPTION_FOLDER_NOT_FOUND=Dossier {0} non trouvé
+UNKNOWN_ATTRIBUTE=Attribut inconnu: {0}
+ACCEPTED=Accepté :
+TENTATIVE=Provisoire :
+DECLINED=Refusé :
+UI_ENABLE_EWS=Protocole Exchange :
+UI_ENABLE_EWS_HELP=Activer EWS sur Exchange 2010 ou Exchange 2007 sans support Webdav
+UI_CALDAV_NOTIFICATION=DavMail : Notification Caldav
+UI_BUTTON_SEND=Envoyer
+UI_SUBJECT=Sujet :
+UI_SUBJECT_HELP=Sujet notification Caldav
+UI_TO=Pour :
+UI_TO_HELP=Destinataires
+UI_NOTIFICATION_BODY=Commentaire notification Caldav
+UI_CC=Copie à :
+UI_CC_HELP=Destinataires en copie
+UI_CALDAV_EDIT_NOTIFICATIONS=Edition notifications Caldav :
+UI_CALDAV_EDIT_NOTIFICATIONS_HELP=Activer le fenêtre d'édition interactive des notifications
+LOG_SEARCH_RESULT={0} élément(s) trouvé(s)
+UI_SAVE_IN_SENT=SMTP copie dans Envoyés :
+UI_SAVE_IN_SENT_HELP=Créer une copie des messages envoyés via SMTP dans le dossier serveur "Envoyés"
+UI_CERTIFICATE_ALIAS_PROMPT=Choisir un certificat
\ No newline at end of file
diff --git a/src/java/log4j.properties b/src/java/log4j.properties
new file mode 100644
index 0000000..6d3b4cc
--- /dev/null
+++ b/src/java/log4j.properties
@@ -0,0 +1,19 @@
+# Warning : actual log levels set in davmail.properties
+log4j.rootLogger=WARN, ConsoleAppender
+log4j.logger.davmail=DEBUG
+log4j.logger.httpclient.wire=WARN
+log4j.logger.org.apache.commons.httpclient=WARN
+
+# ConsoleAppender is set to be a ConsoleAppender.
+log4j.appender.ConsoleAppender=org.apache.log4j.ConsoleAppender
+
+# ConsoleAppender uses PatternLayout.
+log4j.appender.ConsoleAppender.layout=org.apache.log4j.PatternLayout
+log4j.appender.ConsoleAppender.layout.ConversionPattern=%d{ISO8601} %-5p [%t] %c %x - %m%n
+
+#log4j.logger.org.apache.commons.httpclient.util.IdleConnectionHandler=DEBUG
+#log4j.logger.org.apache.commons.httpclient.util.MultiThreadedHttpConnectionManager=DEBUG
+
+#log4j.appender.defaultSocketAppender=org.apache.log4j.net.SocketAppender
+#log4j.appender.defaultSocketAppender.RemoteHost=localhost
+#log4j.appender.defaultSocketAppender.port=4560
diff --git a/src/java/osxtray.png b/src/java/osxtray.png
new file mode 100644
index 0000000..fe1f313
Binary files /dev/null and b/src/java/osxtray.png differ
diff --git a/src/java/osxtray2.png b/src/java/osxtray2.png
new file mode 100644
index 0000000..18ee2b7
Binary files /dev/null and b/src/java/osxtray2.png differ
diff --git a/src/java/osxtrayinactive.png b/src/java/osxtrayinactive.png
new file mode 100644
index 0000000..dbf9b5e
Binary files /dev/null and b/src/java/osxtrayinactive.png differ
diff --git a/src/java/timezoneids.properties b/src/java/timezoneids.properties
new file mode 100644
index 0000000..8a54d6b
--- /dev/null
+++ b/src/java/timezoneids.properties
@@ -0,0 +1,102 @@
+# Timezone name to id table
+Morocco\ Standard\ Time=88
+GMT\ Standard\ Time=1
+Greenwich\ Standard\ Time=31
+W.\ Europe\ Standard\ Time=4
+Romance\ Standard\ Time=3
+Central\ Europe\ Standard\ Time=6
+Central\ European\ Standard\ Time=2
+W.\ Central\ Africa\ Standard\ Time=69
+Jordan\ Standard\ Time=83
+GTB\ Standard\ Time=7
+Egypt\ Standard\ Time=49
+South\ Africa\ Standard\ Time=50
+FLE\ Standard\ Time=59
+Israel\ Standard\ Time=27
+E.\ Europe\ Standard\ Time=5
+Arabic\ Standard\ Time=26
+E.\ Africa\ Standard\ Time=56
+Arab\ Standard\ Time=74
+Russian\ Standard\ Time=51
+Iran\ Standard\ Time=25
+Arabian\ Standard\ Time=24
+Azerbaijan\ Standard\ Time=84
+Caucasus\ Standard\ Time=54
+Mauritius\ Standard\ Time=91
+Georgian\ Standard\ Time=86
+Armenian\ Standard\ Time=85
+Afghanistan\ Standard\ Time=48
+Ekaterinburg\ Standard\ Time=58
+Pakistan\ Standard\ Time=89
+West\ Asia\ Standard\ Time=47
+India\ Standard\ Time=23
+Sri\ Lanka\ Standard\ Time=66
+Nepal\ Standard\ Time=62
+N.\ Central\ Asia\ Standard\ Time=46
+Central\ Asia\ Standard\ Time=71
+Myanmar\ Standard\ Time=61
+SE\ Asia\ Standard\ Time=22
+North\ Asia\ Standard\ Time=64
+China\ Standard\ Time=45
+North\ Asia\ East\ Standard\ Time=63
+Singapore\ Standard\ Time=21
+W.\ Australia\ Standard\ Time=73
+Taipei\ Standard\ Time=75
+Korea\ Standard\ Time=72
+Tokyo\ Standard\ Time=20
+Yakutsk\ Standard\ Time=70
+Cen.\ Australia\ Standard\ Time=19
+AUS\ Central\ Standard\ Time=44
+E.\ Australia\ Standard\ Time=18
+AUS\ Eastern\ Standard\ Time=78
+West\ Pacific\ Standard\ Time=43
+Tasmania\ Standard\ Time=42
+Vladivostok\ Standard\ Time=68
+Central\ Pacific\ Standard\ Time=41
+Fiji\ Standard\ Time=40
+New\ Zealand\ Standard\ Time=17
+Tonga\ Standard\ Time=67
+Azores\ Standard\ Time=29
+Cape\ Verde\ Standard\ Time=53
+Mid-Atlantic\ Standard\ Time=30
+E.\ South\ America\ Standard\ Time=8
+Argentina\ Standard\ Time=87
+SA\ Eastern\ Standard\ Time=32
+Greenland\ Standard\ Time=60
+Montevideo\ Standard\ Time=92
+Newfoundland\ Standard\ Time=28
+Atlantic\ Standard\ Time=9
+SA\ Western\ Standard\ Time=33
+Central\ Brazilian\ Standard\ Time=90
+Pacific\ SA\ Standard\ Time=65
+Venezuela\ Standard\ Time=82
+SA\ Pacific\ Standard\ Time=35
+Eastern\ Standard\ Time=10
+US\ Eastern\ Standard\ Time=34
+Central\ America\ Standard\ Time=55
+Central\ Standard\ Time=11
+Central\ Standard\ Time\ (Mexico)=37
+Canada\ Central\ Standard\ Time=36
+US\ Mountain\ Standard\ Time=38
+Mountain\ Standard\ Time\ (Mexico)=77
+Mountain\ Standard\ Time=12
+Pacific\ Standard\ Time=13
+Pacific\ Standard\ Time\ (Mexico)=81
+Alaskan\ Standard\ Time=14
+Hawaiian\ Standard\ Time=15
+Samoa\ Standard\ Time=16
+Dateline\ Standard\ Time=39
+# Additional
+UTC=31
+Middle\ East\ Standard\ Time=7
+Bangladesh\ Standard\ Time=71
+Ulaanbaatar\ Standard\ Time=63
+Kamchatka\ Standard\ Time=40
+UTC-02=30
+Paraguay\ Standard\ Time=65
+Mexico\ Standard\ Time=37
+Mexico\ Standard\ Time\ 2=77
+UTC-11=16
+# Unknown
+#UTC+12=N/A
+#Namibia\ Standard\ Time=N/A
diff --git a/src/java/timezones.properties b/src/java/timezones.properties
new file mode 100644
index 0000000..f264624
--- /dev/null
+++ b/src/java/timezones.properties
@@ -0,0 +1,148 @@
+# Timezone rename table to help iCal find the correct timezone
+(GMT)\ Monrovia/Casablanca=Africa/Casablanca
+(GMT)\ Monrovia/Reykjavik=Africa/Casablanca
+(GMT)\ Casablanca=Africa/Casablanca
+(GMT)\ Greenwich\ Mean\ Time/Dublin/Edinburgh/London=Europe/London
+(GMT+01.00)\ Berlin/Stockholm/Rome/Bern/Vienna=Europe/Berlin
+(GMT+01.00)\ Paris/Madrid/Brussels/Copenhagen=Europe/Paris
+(GMT+01\:00)\ Brussels,\ Copenhagen,\ Madrid,\ Paris=Europe/Paris
+(GMT+01.00)\ Prague/Central\ Europe=Europe/Prague
+(GMT+01.00)\ Sarajevo/Warsaw/Zagreb=Europe/Sarajevo
+(GMT+01.00)\ West\ Central\ Africa=Africa/Algiers
+(GMT+02.00)\ Athens/Istanbul/Minsk=Europe/Athens
+(GMT+02.00)\ Athens/Bucharest/Istanbul=Europe/Athens
+(GMT+02.00)\ Bucharest/Eastern\ Europe=Europe/Bucharest
+(GMT+02.00)\ Minsk/Eastern\ Europe=Europe/Bucharest
+(GMT+02.00)\ Cairo=Africa/Cairo
+(GMT+02.00)\ Harare/Pretoria=Africa/Harare
+(GMT+02.00)\ Helsinki/Riga/Tallinn=Europe/Helsinki
+(GMT+02.00)\ Israel/Jerusalem\ Standard\ Time=Asia/Jerusalem
+(GMT+02.00)\ Amman=Asia/Amman
+(GMT+02.00)\ Athens/Bucharest=Europe/Athens
+(GMT+02.00)\ Damascus=Asia/Damascus
+(GMT+02.00)\ Istanbul=Asia/Istanbul
+(GMT+02.00)\ Windhoek=Africa/Windhoek
+(GMT+03.00)\ Baghdad=Asia/Baghdad
+(GMT+03.00)\ Arab/Kuwait/Riyadh=Asia/Kuwait
+(GMT+03.00)\ Moscow/St.\ Petersburg/Volgograd=Europe/Moscow
+(GMT+03.00)\ East\ Africa/Nairobi=Africa/Nairobi
+(GMT+03.00)\ Kaliningrad=Europe/Kaliningrad
+(GMT+03.00)\ Kuwait/Riyadh=Asia/Kuwait
+(GMT+03.00)\ Tbilisi=Asia/Tbilisi
+(GMT+03.30)\ Tehran=Asia/Tehran
+(GMT+04.00)\ Abu\ Dhabi/Muscat=Asia/Dubai
+(GMT+04.00)\ Caucasus/Baku/Tbilisi/Yerevan=Asia/Tbilisi
+(GMT+04.00)\ Baku=Asia/Baku
+(GMT+04.00)\ Caucasus\ Standard\ Time=Asia/Baku
+(GMT+04.00)\ Port\ Louis=Asia/Baku
+(GMT+04.00)\ Yerevan=Asia/Baku
+(GMT+04.30)\ Kabul=Asia/Kabul
+(GMT+05.00)\ Ekaterinburg=Asia/Yekaterinburg
+(GMT+05.00)\ Islamabad/Karachi/Sverdlovsk/Tashkent=Asia/Tashkent
+(GMT+05.00)\ Islamabad/Karachi=Asia/Karachi
+(GMT+05.00)\ Sverdlovsk/Tashkent=Asia/Tashkent
+(GMT+05.30)\ Calcutta/Chennai/Mumbai/New\ Delhi/India\ Standard\ Time=Asia/Kolkata
+(GMT+05.30)\ Kolkata/Chennai/Mumbai/New\ Delhi/India\ Standard\ Time=Asia/Kolkata
+(GMT+05.30)\ Sri\ Jayawardenepura/Sri\ Lanka=Asia/Kolkata
+(GMT+05.45)\ Kathmandu/Nepal=Asia/Katmandu
+(GMT+06.00)\ Almaty/North\ Central\ Asia/Novosibirsk=Asia/Almaty
+(GMT+06.00)\ Astana/Dhaka=Asia/Dhaka
+(GMT+06.00)\ Sri\ Jayawardenepura/Sri\ Lanka=Asia/Kolkata
+(GMT+06.00)\ Astana=Asia/Dhaka
+(GMT+06.00)\ Dhaka=Asia/Dhaka
+(GMT+06.00)\ Novosibirsk=Asia/Novosibirsk
+(GMT+06.30)\ Rangoon=Asia/Rangoon
+(GMT+07.00)\ Bangkok/Jakarta/Hanoi=Asia/Bangkok
+(GMT+07.00)\ Krasnoyarsk/North\ Asia=Asia/Krasnoyarsk
+(GMT+08.00)\ Beijing/Chongqing/Hong\ Kong/Urumqi=Asia/Hong_Kong
+(GMT+08.00)\ Irkutsk/Ulaan\ Bataar=Asia/Irkutsk
+(GMT+08.00)\ Kuala\ Lumpur/Singapore=Asia/Kuala_Lumpur
+(GMT+08.00)\ Perth/Western\ Australia=Australia/Perth
+(GMT+08.00)\ Taipei=Asia/Taipei
+(GMT+08.00)\ Beijing/Chongqing/Hong\ Kong\ SAR/Urumqi=Asia/Beijing
+(GMT+08.00)\ Irkutsk=Asia/Irkutsk
+(GMT+08.00)\ Ulaanbaatar=Asia/Ulaanbaatar
+(GMT+09.00)\ Tokyo/Osaka/Sapporo=Asia/Tokyo
+(GMT+09.00)\ Seoul/Korea\ Standard\ Time=Asia/Seoul
+(GMT+09.00)\ Yakutsk=Asia/Yakutsk
+(GMT+09.30)\ Adelaide/Central\ Australia=Australia/Adelaide
+(GMT+09.30)\ Darwin=Australia/Darwin
+(GMT+09.30)\ Adelaide\ (Commonwealth\ Games)=Australia/Adelaide
+(GMT+10.00)\ Canberra,\ Melbourne,\ Sydney,\ Hobart\ (Year\ 2000\ only)=Australia/Melbourne
+(GMT+10.00)\ Canberra,\ Melbourne,\ Sydney\ (Commonwealth\ Games)=Australia/Melbourne
+(GMT+10.00)\ Hobart\ (Commonwealth\ Games)=Australia/Melbourne
+(GMT+10.00)\ Brisbane/East\ Australia=Australia/Brisbane
+(GMT+10.00)\ Canberra/Melbourne/Sydney/Hobart\ (Year\ 2000\ only)=Australia/Melbourne
+(GMT+10.00)\ Guam/Port\ Moresby=Pacific/Guam
+(GMT+10.00)\ Hobart/Tasmania=Australia/Hobart
+(GMT+10.00)\ Vladivostok=Asia/Vladivostok
+(GMT+10.00)\ Melbourne/Sydney=Australia/Melbourne
+(GMT+11.00)\ Magadan/Solomon\ Is./New\ Caledonia=Asia/Magadan
+(GMT+11.00)\ Solomon\ Is./New\ Caledonia=Pacific/Noumea
+(GMT+12.00)\ Wellington/Auckland=Pacific/Auckland
+(GMT+12.00)\ Fiji/Kamchatka/Marshall\ Is.=Pacific/Fiji
+(GMT+12.00)\ Coordinated\ Universal\ Time+12=Pacific/Fiji
+(GMT+12.00)\ Fiji=Pacific/Fiji
+(GMT+12.00)\ Magadan=Asia/Magadan
+(GMT+12.00)\ Petropavlovsk-Kamchatsky\ -\ Old=
+(GMT+13.00)\ Tonga/Nuku'alofa=Pacific/Tongatapu
+(GMT+13.00)\ Nuku'alofa=Pacific/Tongatapu
+(GMT-01.00)\ Azores=Atlantic/Azores
+(GMT-01.00)\ Cape\ Verde\ Is.=Atlantic/Cape_Verde
+(GMT-02.00)\ Mid-Atlantic=Atlantic/South_Georgia
+(GMT-02.00)\ Coordinated\ Universal\ Time-02=Atlantic/South_Georgia
+(GMT-03.00)\ Brasilia=America/Sao_Paulo
+(GMT-03.00)\ Georgetown=America/Argentina/Buenos_Aires
+(GMT-03.00)\ Buenos\ Aires/Georgetown=America/Argentina/Buenos_Aires
+(GMT-03.00)\ Greenland=America/Godthab
+(GMT-03.00)\ Buenos\ Aires=America/Argentina/Buenos_Aires
+(GMT-03.00)\ Cayenne/Fortaleza=America/Cayenne
+(GMT-03.00)\ Montevideo=America/Montevideo
+(GMT-03.30)\ Newfoundland=America/St_Johns
+(GMT-04.00)\ Atlantic\ Time\ (Canada)=America/Halifax
+(GMT-04.00)\ Caracas/La\ Paz=America/Caracas
+(GMT-04.00)\ Santiago=America/Santiago
+(GMT-04.00)\ Asuncion=America/Asuncion
+(GMT-04.00)\ Cuiaba=America/Cuiaba
+(GMT-04.00)\ Georgetown/La\ Paz/Manaus/San\ Juan=America/Manaus
+(GMT-04.30)\ Caracas=America/Caracas
+(GMT-05.00)\ Bogota/Lima=America/Lima
+(GMT-05.00)\ Bogota/Lima/Quito=America/Lima
+SA\ Pacific\ Standard\ Time=America/Lima
+(GMT-05.00)\ Eastern\ Time\ (US\ &\ Canada)=America/New_York
+(GMT-05.00)\ Indiana\ (East)=America/Indiana/Knox
+(GMT-06.00)\ Central\ America=America/Chicago
+(GMT-06.00)\ Central\ Time\ (US\ &\ Canada)=America/Chicago
+(GMT-06.00)\ Mexico\ City/Tegucigalpa=America/Mexico_City
+(GMT-06.00)\ Saskatchewan=America/Regina
+(GMT-07.00)\ Arizona=America/Phoenix
+(GMT-07.00)\ Mountain\ Time\ (US\ &\ Canada)=America/Denver
+(GMT-07.00)\ Chihuahua/La\ Paza/Mazatlan=America/Chihuahua
+(GMT-08.00)\ Pacific\ Time\ (US\ &\ Canada)/Tijuana=America/Los_Angeles
+(GMT-08.00)\ Pacific\ Time\ (US\ &\ Canada)=America/Los_Angeles
+(GMT-08.00)\ Baja\ California=America/Los_Angeles
+(GMT-0800)\ Pacific\ Standard\ Time=America/Los_Angeles
+Pacific\ Standard\ Time=America/Los_Angeles
+(GMT-09.00)\ Alaska=America/Anchorage
+(GMT-10.00)\ Hawaii=Pacific/Honolulu
+(GMT-11.00)\ Midway\ Island/Samoa=Pacific/Pago_Pago
+(GMT-11.00)\ Coordinated\ Universal\ Time-11=Pacific/Samoa
+(GMT-11.00)\ Samoa=Pacific/Samoa
+(GMT-12.00)\ Eniwetok/Kwajalein/Dateline\ Time=Pacific/Kwajalein
+GMT\ -0000\ (Standard)\ /\ GMT\ -0000\ (Daylight)=Africa/Casablanca
+GMT\ -0000\ (Standard)\ /\ GMT\ +0100\ (Daylight)=Europe/London
+GMT\ +0100\ (Standard)\ /\ GMT\ +0200\ (Daylight)=Europe/Paris
+GMT\ +0200\ (Standard)\ /\ GMT\ +0200\ (Daylight)=Africa/Harare
+GMT\ +0200\ (Standard)\ /\ GMT\ +0300\ (Daylight)=Europe/Bucharest
+GMT\ +0400\ (Standard)\ /\ GMT\ +0400\ (Daylight)=Asia/Dubai
+GMT\ +0800\ (Standard)\ /\ GMT\ +0900\ (Daylight)=Asia/Kuala_Lumpur
+GMT\ +1000\ (Standard)\ /\ GMT\ +1100\ (Daylight)=Australia/Melbourne
+GMT\ -0300\ (Standard)\ /\ GMT\ -0200\ (Daylight)=America/Sao_Paulo
+GMT\ -0500\ (Standard)\ /\ GMT\ -0400\ (Daylight)=America/New_York
+GMT\ -0600\ (Standard)\ /\ GMT\ -0500\ (Daylight)=America/Chicago
+GMT\ -0700\ (Standard)\ /\ GMT\ -0600\ (Daylight)=America/Denver
+GMT\ -0800\ (Standard)\ /\ GMT\ -0700\ (Daylight)=America/Los_Angeles
+GMT\ Standard\ Time=GMT Standard Time
+Greenwich\ Standard\ Time=Greenwich Standard Time
+Mexico\ Standard\ Time=America/Mexico_City
+(GMT)\ Coordinated\ Universal\ Time=Greenwich Standard Time
diff --git a/src/java/tray.png b/src/java/tray.png
new file mode 100644
index 0000000..9d3f243
Binary files /dev/null and b/src/java/tray.png differ
diff --git a/src/java/tray2.png b/src/java/tray2.png
new file mode 100644
index 0000000..9168427
Binary files /dev/null and b/src/java/tray2.png differ
diff --git a/src/java/tray32.png b/src/java/tray32.png
new file mode 100644
index 0000000..0ecb943
Binary files /dev/null and b/src/java/tray32.png differ
diff --git a/src/java/tray48.png b/src/java/tray48.png
new file mode 100644
index 0000000..7129e47
Binary files /dev/null and b/src/java/tray48.png differ
diff --git a/src/java/trayinactive.png b/src/java/trayinactive.png
new file mode 100644
index 0000000..71b79c2
Binary files /dev/null and b/src/java/trayinactive.png differ
diff --git a/src/jsmooth/skeletons/autodownload-wrapper/autodownload.exe b/src/jsmooth/skeletons/autodownload-wrapper/autodownload.exe
new file mode 100644
index 0000000..d97c73d
Binary files /dev/null and b/src/jsmooth/skeletons/autodownload-wrapper/autodownload.exe differ
diff --git a/src/jsmooth/skeletons/autodownload-wrapper/autodownload.skel b/src/jsmooth/skeletons/autodownload-wrapper/autodownload.skel
new file mode 100644
index 0000000..c764afe
--- /dev/null
+++ b/src/jsmooth/skeletons/autodownload-wrapper/autodownload.skel
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<jsmoothskeleton>
+<debug>false</debug>
+<description>SKEL_AUTODOWNLOAD_PROPERTY_DESCRIPTION</description>
+<executableName>autodownload.exe</executableName>
+<resourceCategory>JAVA</resourceCategory>
+<resourceJarId>102</resourceJarId>
+<resourcePropsId>103</resourcePropsId>
+<shortName>Autodownload Wrapper</shortName>
+<skeletonProperties>
+<description>SKEL_AUTODOWNLOAD_PROPERTY_MESSAGE_DESCRIPTION</description>
+<idName>Message</idName>
+<label>SKEL_AUTODOWNLOAD_PROPERTY_MESSAGE_LABEL</label>
+<type>textarea</type>
+<value>Java has not been found on your computer. Do you want to download it?</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_AUTODOWNLOAD_PROPERTY_DLURL_DESCRIPTION</description>
+<idName>DownloadURL</idName>
+<label>SKEL_AUTODOWNLOAD_PROPERTY_DLURL_LABEL</label>
+<type>autodownloadurl</type>
+<value>http://java.sun.com/update/1.5.0/jinstall-1_5_0_11-windows-i586.cab</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_PROPERTY_SINGLEPROCESS_DESCRIPTION</description>
+<idName>SingleProcess</idName>
+<label>SKEL_GENERIC_PROPERTY_SINGLEPROCESS_LABEL</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_SINGLEINSTANCE_DESCRIPTION</description>
+<idName>SingleInstance</idName>
+<label>SKEL_GENERIC_SINGLEINSTANCE</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_JNISMOOTH_DESCRIPTION</description>
+<idName>JniSmooth</idName>
+<label>SKEL_GENERIC_JNISMOOTH</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_PROPERTY_DEBUG_DESCRIPTION</description>
+<idName>Debug</idName>
+<label>SKEL_GENERIC_PROPERTY_DEBUG_LABEL</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+</jsmoothskeleton>
diff --git a/src/jsmooth/skeletons/autodownload-wrapper/customdownload.skel b/src/jsmooth/skeletons/autodownload-wrapper/customdownload.skel
new file mode 100644
index 0000000..9ee12a1
--- /dev/null
+++ b/src/jsmooth/skeletons/autodownload-wrapper/customdownload.skel
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<jsmoothskeleton>
+<debug>false</debug>
+<description>SKEL_CUSTOMDOWNLOAD_DESCRIPTION</description>
+<executableName>autodownload.exe</executableName>
+<resourceCategory>JAVA</resourceCategory>
+<resourceJarId>102</resourceJarId>
+<resourcePropsId>103</resourcePropsId>
+<shortName>Custom Web Download Wrapper</shortName>
+<skeletonProperties>
+<description>SKEL_CUSTOMDOWNLOAD_PROPERTY_MESSAGE_DESCRIPTION</description>
+<idName>Message</idName>
+<label>SKEL_CUSTOMDOWNLOAD_PROPERTY_MESSAGE_LABEL</label>
+<type>textarea</type>
+<value>Java has not been found on your computer. Do you want to download it?</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_CUSTOMDOWNLOAD_PROPERTY_URL_DESCRIPTION</description>
+<idName>DownloadURL</idName>
+<label>SKEL_CUSTOMDOWNLOAD_PROPERTY_URL_LABEL</label>
+<type>string</type>
+<value>http://</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_PROPERTY_SINGLEPROCESS_DESCRIPTION</description>
+<idName>SingleProcess</idName>
+<label>SKEL_GENERIC_PROPERTY_SINGLEPROCESS_LABEL</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_SINGLEINSTANCE_DESCRIPTION</description>
+<idName>SingleInstance</idName>
+<label>SKEL_GENERIC_SINGLEINSTANCE</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_JNISMOOTH_DESCRIPTION</description>
+<idName>JniSmooth</idName>
+<label>SKEL_GENERIC_JNISMOOTH</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_PROPERTY_DEBUG_DESCRIPTION</description>
+<idName>Debug</idName>
+<label>SKEL_GENERIC_PROPERTY_DEBUG_LABEL</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+</jsmoothskeleton>
diff --git a/src/jsmooth/skeletons/console-wrapper/consolewrapper.exe b/src/jsmooth/skeletons/console-wrapper/consolewrapper.exe
new file mode 100644
index 0000000..abc3ac9
Binary files /dev/null and b/src/jsmooth/skeletons/console-wrapper/consolewrapper.exe differ
diff --git a/src/jsmooth/skeletons/console-wrapper/description.skel b/src/jsmooth/skeletons/console-wrapper/description.skel
new file mode 100644
index 0000000..4a8b8ac
--- /dev/null
+++ b/src/jsmooth/skeletons/console-wrapper/description.skel
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<jsmoothskeleton>
+<debug>false</debug>
+<description>SKEL_CONSOLEWRAPPER_DESCRIPTION</description>
+<executableName>consolewrapper.exe</executableName>
+<resourceCategory>JAVA</resourceCategory>
+<resourceJarId>102</resourceJarId>
+<resourcePropsId>103</resourcePropsId>
+<shortName>Console Wrapper</shortName>
+<skeletonProperties>
+<description>SKEL_CUSTOMWRAPPER_PROPERTY_MESSAGE_DESCRIPTION</description>
+<idName>Message</idName>
+<label>SKEL_CUSTOMWRAPPER_PROPERTY_MESSAGE_LABEL</label>
+<type>textarea</type>
+<value>This program needs Java to run.
+Please download it at http://www.java.com</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_CUSTOMWRAPPER_PROPERTY_KEYPRESS_DESCRIPTION</description>
+<idName>PressKey</idName>
+<label>SKEL_CUSTOMWRAPPER_PROPERTY_KEYPRESS_LABEL</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_PROPERTY_DEBUG_DESCRIPTION</description>
+<idName>Debug</idName>
+<label>SKEL_GENERIC_PROPERTY_DEBUG_LABEL</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+</jsmoothskeleton>
diff --git a/src/jsmooth/skeletons/windowed-wrapper64/description64.skel b/src/jsmooth/skeletons/windowed-wrapper64/description64.skel
new file mode 100644
index 0000000..6772244
--- /dev/null
+++ b/src/jsmooth/skeletons/windowed-wrapper64/description64.skel
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<jsmoothskeleton>
+<debug>false</debug>
+<description>SKEL_SIMPLEWRAPPER_DESCRIPTION</description>
+<executableName>windowed-wrapper64.exe</executableName>
+<resourceCategory>JAVA</resourceCategory>
+<resourceJarId>102</resourceJarId>
+<resourcePropsId>103</resourcePropsId>
+<shortName>Windowed Wrapper x64</shortName>
+<skeletonProperties>
+<description>SKEL_SIMPLEWRAPPER_PROPERTY_MESSAGE_DESCRIPTION</description>
+<idName>Message</idName>
+<label>SKEL_SIMPLEWRAPPER_PROPERTY_MESSAGE_LABEL</label>
+<type>textarea</type>
+<value>Java has not been found on your computer. Do you want to download it?</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_SIMPLEWRAPPER_PROPERTY_URL_DESCRIPTION</description>
+<idName>URL</idName>
+<label>SKEL_SIMPLEWRAPPER_PROPERTY_URL_LABEL</label>
+<type>string</type>
+<value>http://www.java.com</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_PROPERTY_SINGLEPROCESS_DESCRIPTION</description>
+<idName>SingleProcess</idName>
+<label>SKEL_GENERIC_PROPERTY_SINGLEPROCESS_LABEL</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_SINGLEINSTANCE_DESCRIPTION</description>
+<idName>SingleInstance</idName>
+<label>SKEL_GENERIC_SINGLEINSTANCE</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_JNISMOOTH_DESCRIPTION</description>
+<idName>JniSmooth</idName>
+<label>SKEL_GENERIC_JNISMOOTH</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_PROPERTY_DEBUG_DESCRIPTION</description>
+<idName>Debug</idName>
+<label>SKEL_GENERIC_PROPERTY_DEBUG_LABEL</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+</jsmoothskeleton>
diff --git a/src/jsmooth/skeletons/windowed-wrapper64/windowed-wrapper64.exe b/src/jsmooth/skeletons/windowed-wrapper64/windowed-wrapper64.exe
new file mode 100644
index 0000000..3078cb9
Binary files /dev/null and b/src/jsmooth/skeletons/windowed-wrapper64/windowed-wrapper64.exe differ
diff --git a/src/jsmooth/skeletons/winservice-wrapper/description.skel b/src/jsmooth/skeletons/winservice-wrapper/description.skel
new file mode 100644
index 0000000..11bfa94
--- /dev/null
+++ b/src/jsmooth/skeletons/winservice-wrapper/description.skel
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<jsmoothskeleton>
+<debug>false</debug>
+<description>SKEL_WINSERVICE_DESCRIPTION</description>
+<executableName>winservice.exe</executableName>
+<resourceCategory>JAVA</resourceCategory>
+<resourceJarId>102</resourceJarId>
+<resourcePropsId>103</resourcePropsId>
+<shortName>WinService Wrapper</shortName>
+<skeletonProperties>
+<description>SKEL_WINSERVICE_NAME_DESCRIPTION</description>
+<idName>ServiceName</idName>
+<label>SKEL_WINSERVICE_NAME_LABEL</label>
+<type>string</type>
+<value>myservice</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_WINSERVICE_DISPLAYNAME_DESCRIPTION</description>
+<idName>ServiceDisplayName</idName>
+<label>SKEL_WINSERVICE_DISPLAYNAME_LABEL</label>
+<type>string</type>
+<value>My Service</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_WINSERVICE_PROPERTY_MESSAGE_DESCRIPTION</description>
+<idName>Message</idName>
+<label>SKEL_WINSERVICE_PROPERTY_MESSAGE_LABEL</label>
+<type>textarea</type>
+<value>This program needs Java to run. Please download it at http://www.java.comf</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_WINSERVICE_AUTOSTART_DESCRIPTION</description>
+<idName>Autostart</idName>
+<label>SKEL_WINSERVICE_AUTOSTART_LABEL</label>
+<type>boolean</type>
+<value>1</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_WINSERVICE_LOGFILE_DESCRIPTION</description>
+<idName>Logfile</idName>
+<label>SKEL_WINSERVICE_LOGFILE_LABEL</label>
+<type>string</type>
+<value>service.log</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_WINSERVICE_INTERACTIVE_DESCRIPTION</description>
+<idName>Interactive</idName>
+<label>SKEL_WINSERVICE_INTERACTIVE_LABEL</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_JNISMOOTH_DESCRIPTION</description>
+<idName>JniSmooth</idName>
+<label>SKEL_GENERIC_JNISMOOTH</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+<skeletonProperties>
+<description>SKEL_GENERIC_PROPERTY_DEBUG_DESCRIPTION</description>
+<idName>Debug</idName>
+<label>SKEL_GENERIC_PROPERTY_DEBUG_LABEL</label>
+<type>boolean</type>
+<value>0</value>
+</skeletonProperties>
+</jsmoothskeleton>
diff --git a/src/jsmooth/skeletons/winservice-wrapper/winservice.exe b/src/jsmooth/skeletons/winservice-wrapper/winservice.exe
new file mode 100644
index 0000000..de5c0ac
Binary files /dev/null and b/src/jsmooth/skeletons/winservice-wrapper/winservice.exe differ
diff --git a/src/license.txt b/src/license.txt
new file mode 100644
index 0000000..d511905
--- /dev/null
+++ b/src/license.txt
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/src/osx/davmail b/src/osx/davmail
new file mode 100644
index 0000000..2fe00d3
Binary files /dev/null and b/src/osx/davmail differ
diff --git a/src/osx/tray.icns b/src/osx/tray.icns
new file mode 100644
index 0000000..c0b3093
Binary files /dev/null and b/src/osx/tray.icns differ
diff --git a/src/site/resources/css/site.css b/src/site/resources/css/site.css
new file mode 100644
index 0000000..7c9be4d
--- /dev/null
+++ b/src/site/resources/css/site.css
@@ -0,0 +1,78 @@
+/* You can override this file with your own styles */
+body {
+ background:#F1FAFF url(../images/inner-gd.png) repeat-x scroll 0 0;
+}
+#breadcrumbs {
+ background-color: white;
+ border:0;
+}
+.externalLink img {
+ vertical-align: middle;
+}
+.section h2, .section h3 {
+ border:0;
+ background: transparent;
+ color:black;
+ font-size:17px;
+}
+.section h2 {
+ border-bottom:1px solid;
+ padding:1px;
+}
+.section h3 {
+ margin-top: 0;
+ margin-bottom: 0;
+ font-size:16px;
+ font-weight:bold;
+}
+.section p {
+ padding-left:15px;
+}
+
+
+
+.section table {
+ margin-left:10px;
+ border-collapse: collapse;
+}
+.small {
+ font-size:9px;
+}
+.source {
+ overflow:auto;
+}
+.floatLeft {
+ float:left;
+}
+.notype {
+ list-style-type:none;
+ margin-left:10px;
+ padding-left:0;
+}
+.about td{
+ vertical-align: top;
+}
+a.download {
+ display: block;
+ background: #5ACE1D url(../images/download.png) 95% center no-repeat;
+ color: white;
+ font-weight: bold;
+ padding: 8px;
+ -moz-border-radius: 8px;
+ -webkit-border-radius: 10px;
+ margin: 5px;
+}
+a.donate {
+ display: block;
+ background: #5ACE1D url(../images/donate.png) 95% center no-repeat;
+ color: white;
+ font-weight: bold;
+ padding: 8px;
+ -moz-border-radius: 8px;
+ -webkit-border-radius: 10px;
+ margin: 5px;
+}
+.about p {
+ padding: 0;
+ margin-top: 0;
+}
diff --git a/src/site/resources/favicon.ico b/src/site/resources/favicon.ico
new file mode 100644
index 0000000..508c444
Binary files /dev/null and b/src/site/resources/favicon.ico differ
diff --git a/src/site/resources/images/davmailArchitecture.png b/src/site/resources/images/davmailArchitecture.png
new file mode 100644
index 0000000..ba3580c
Binary files /dev/null and b/src/site/resources/images/davmailArchitecture.png differ
diff --git a/src/site/resources/images/davmailLogo.png b/src/site/resources/images/davmailLogo.png
new file mode 100644
index 0000000..c4c4ea7
Binary files /dev/null and b/src/site/resources/images/davmailLogo.png differ
diff --git a/src/site/resources/images/davmailSettings.png b/src/site/resources/images/davmailSettings.png
new file mode 100644
index 0000000..290af23
Binary files /dev/null and b/src/site/resources/images/davmailSettings.png differ
diff --git a/src/site/resources/images/donate.png b/src/site/resources/images/donate.png
new file mode 100644
index 0000000..98caae4
Binary files /dev/null and b/src/site/resources/images/donate.png differ
diff --git a/src/site/resources/images/download.png b/src/site/resources/images/download.png
new file mode 100644
index 0000000..27d3999
Binary files /dev/null and b/src/site/resources/images/download.png differ
diff --git a/src/site/resources/images/inner-gd.png b/src/site/resources/images/inner-gd.png
new file mode 100644
index 0000000..0e711e7
Binary files /dev/null and b/src/site/resources/images/inner-gd.png differ
diff --git a/src/site/resources/images/iphone/iphoneAccount1.png b/src/site/resources/images/iphone/iphoneAccount1.png
new file mode 100644
index 0000000..e246cb3
Binary files /dev/null and b/src/site/resources/images/iphone/iphoneAccount1.png differ
diff --git a/src/site/resources/images/iphone/iphoneAccount2.png b/src/site/resources/images/iphone/iphoneAccount2.png
new file mode 100644
index 0000000..07d9db3
Binary files /dev/null and b/src/site/resources/images/iphone/iphoneAccount2.png differ
diff --git a/src/site/resources/images/iphone/iphoneAccount3.png b/src/site/resources/images/iphone/iphoneAccount3.png
new file mode 100644
index 0000000..633e43d
Binary files /dev/null and b/src/site/resources/images/iphone/iphoneAccount3.png differ
diff --git a/src/site/resources/images/iphone/iphoneCaldav1.png b/src/site/resources/images/iphone/iphoneCaldav1.png
new file mode 100644
index 0000000..2db45c2
Binary files /dev/null and b/src/site/resources/images/iphone/iphoneCaldav1.png differ
diff --git a/src/site/resources/images/iphone/iphoneCaldav2.png b/src/site/resources/images/iphone/iphoneCaldav2.png
new file mode 100644
index 0000000..6de5eea
Binary files /dev/null and b/src/site/resources/images/iphone/iphoneCaldav2.png differ
diff --git a/src/site/resources/images/iphone/iphoneCaldav3.png b/src/site/resources/images/iphone/iphoneCaldav3.png
new file mode 100644
index 0000000..259f3f3
Binary files /dev/null and b/src/site/resources/images/iphone/iphoneCaldav3.png differ
diff --git a/src/site/resources/images/iphone/iphoneCaldav4.png b/src/site/resources/images/iphone/iphoneCaldav4.png
new file mode 100644
index 0000000..9b52137
Binary files /dev/null and b/src/site/resources/images/iphone/iphoneCaldav4.png differ
diff --git a/src/site/resources/images/iphone/iphoneCarddav1.png b/src/site/resources/images/iphone/iphoneCarddav1.png
new file mode 100644
index 0000000..c991c93
Binary files /dev/null and b/src/site/resources/images/iphone/iphoneCarddav1.png differ
diff --git a/src/site/resources/images/iphone/iphoneCarddav2.png b/src/site/resources/images/iphone/iphoneCarddav2.png
new file mode 100644
index 0000000..280bdc7
Binary files /dev/null and b/src/site/resources/images/iphone/iphoneCarddav2.png differ
diff --git a/src/site/resources/images/iphone/iphoneCarddav3.png b/src/site/resources/images/iphone/iphoneCarddav3.png
new file mode 100644
index 0000000..3d6c484
Binary files /dev/null and b/src/site/resources/images/iphone/iphoneCarddav3.png differ
diff --git a/src/site/resources/images/iphone/iphoneCarddav4.png b/src/site/resources/images/iphone/iphoneCarddav4.png
new file mode 100644
index 0000000..f3fa88b
Binary files /dev/null and b/src/site/resources/images/iphone/iphoneCarddav4.png differ
diff --git a/src/site/resources/images/iphone/iphoneMail01.png b/src/site/resources/images/iphone/iphoneMail01.png
new file mode 100644
index 0000000..6154d79
Binary files /dev/null and b/src/site/resources/images/iphone/iphoneMail01.png differ
diff --git a/src/site/resources/images/iphone/iphoneMail02.png b/src/site/resources/images/iphone/iphoneMail02.png
new file mode 100644
index 0000000..e5f8f57
Binary files /dev/null and b/src/site/resources/images/iphone/iphoneMail02.png differ
diff --git a/src/site/resources/images/iphone/iphoneMail03.png b/src/site/resources/images/iphone/iphoneMail03.png
new file mode 100644
index 0000000..4950fbf
Binary files /dev/null and b/src/site/resources/images/iphone/iphoneMail03.png differ
diff --git a/src/site/resources/images/iphone/iphoneMail04.png b/src/site/resources/images/iphone/iphoneMail04.png
new file mode 100644
index 0000000..20e8aa4
Binary files /dev/null and b/src/site/resources/images/iphone/iphoneMail04.png differ
diff --git a/src/site/resources/images/iphone/iphoneMail05.png b/src/site/resources/images/iphone/iphoneMail05.png
new file mode 100644
index 0000000..0a49c60
Binary files /dev/null and b/src/site/resources/images/iphone/iphoneMail05.png differ
diff --git a/src/site/resources/images/iphone/iphoneMail06.png b/src/site/resources/images/iphone/iphoneMail06.png
new file mode 100644
index 0000000..f659bc4
Binary files /dev/null and b/src/site/resources/images/iphone/iphoneMail06.png differ
diff --git a/src/site/resources/images/iphone/iphoneMail07.png b/src/site/resources/images/iphone/iphoneMail07.png
new file mode 100644
index 0000000..d81ec0a
Binary files /dev/null and b/src/site/resources/images/iphone/iphoneMail07.png differ
diff --git a/src/site/resources/images/iphone/iphoneMail08.png b/src/site/resources/images/iphone/iphoneMail08.png
new file mode 100644
index 0000000..83deb84
Binary files /dev/null and b/src/site/resources/images/iphone/iphoneMail08.png differ
diff --git a/src/site/resources/images/iphone/iphoneMail09.png b/src/site/resources/images/iphone/iphoneMail09.png
new file mode 100644
index 0000000..b3b58ec
Binary files /dev/null and b/src/site/resources/images/iphone/iphoneMail09.png differ
diff --git a/src/site/resources/images/iphone/iphoneMail10.png b/src/site/resources/images/iphone/iphoneMail10.png
new file mode 100644
index 0000000..b391d4e
Binary files /dev/null and b/src/site/resources/images/iphone/iphoneMail10.png differ
diff --git a/src/site/resources/images/iphone/iphoneMail11.png b/src/site/resources/images/iphone/iphoneMail11.png
new file mode 100644
index 0000000..5062d5d
Binary files /dev/null and b/src/site/resources/images/iphone/iphoneMail11.png differ
diff --git a/src/site/resources/images/iphone/iphoneMail12.png b/src/site/resources/images/iphone/iphoneMail12.png
new file mode 100644
index 0000000..4d40323
Binary files /dev/null and b/src/site/resources/images/iphone/iphoneMail12.png differ
diff --git a/src/site/resources/images/iphone/iphoneMail13.png b/src/site/resources/images/iphone/iphoneMail13.png
new file mode 100644
index 0000000..e9804bd
Binary files /dev/null and b/src/site/resources/images/iphone/iphoneMail13.png differ
diff --git a/src/site/resources/images/iphone/iphoneMail14.png b/src/site/resources/images/iphone/iphoneMail14.png
new file mode 100644
index 0000000..ee54ea5
Binary files /dev/null and b/src/site/resources/images/iphone/iphoneMail14.png differ
diff --git a/src/site/resources/images/iphone/iphoneMail15.png b/src/site/resources/images/iphone/iphoneMail15.png
new file mode 100644
index 0000000..99302df
Binary files /dev/null and b/src/site/resources/images/iphone/iphoneMail15.png differ
diff --git a/src/site/resources/images/iphone/iphoneMail16.png b/src/site/resources/images/iphone/iphoneMail16.png
new file mode 100644
index 0000000..637c7c8
Binary files /dev/null and b/src/site/resources/images/iphone/iphoneMail16.png differ
diff --git a/src/site/resources/images/osxDirectoryUtility1.png b/src/site/resources/images/osxDirectoryUtility1.png
new file mode 100644
index 0000000..ada8994
Binary files /dev/null and b/src/site/resources/images/osxDirectoryUtility1.png differ
diff --git a/src/site/resources/images/osxDirectoryUtility2.png b/src/site/resources/images/osxDirectoryUtility2.png
new file mode 100644
index 0000000..9a09371
Binary files /dev/null and b/src/site/resources/images/osxDirectoryUtility2.png differ
diff --git a/src/site/resources/images/osxDirectoryUtility3.png b/src/site/resources/images/osxDirectoryUtility3.png
new file mode 100644
index 0000000..7291f28
Binary files /dev/null and b/src/site/resources/images/osxDirectoryUtility3.png differ
diff --git a/src/site/resources/images/osxDirectoryUtility4.png b/src/site/resources/images/osxDirectoryUtility4.png
new file mode 100644
index 0000000..c392433
Binary files /dev/null and b/src/site/resources/images/osxDirectoryUtility4.png differ
diff --git a/src/site/resources/images/osxDirectoryUtility5.png b/src/site/resources/images/osxDirectoryUtility5.png
new file mode 100644
index 0000000..f9a5540
Binary files /dev/null and b/src/site/resources/images/osxDirectoryUtility5.png differ
diff --git a/src/site/resources/images/osxDirectoryUtility6.png b/src/site/resources/images/osxDirectoryUtility6.png
new file mode 100644
index 0000000..b6c47d8
Binary files /dev/null and b/src/site/resources/images/osxDirectoryUtility6.png differ
diff --git a/src/site/resources/images/osxDirectoryUtility7.png b/src/site/resources/images/osxDirectoryUtility7.png
new file mode 100644
index 0000000..0d77abd
Binary files /dev/null and b/src/site/resources/images/osxDirectoryUtility7.png differ
diff --git a/src/site/resources/images/osxDirectoryUtility8.png b/src/site/resources/images/osxDirectoryUtility8.png
new file mode 100644
index 0000000..6def3e3
Binary files /dev/null and b/src/site/resources/images/osxDirectoryUtility8.png differ
diff --git a/src/site/resources/images/osxDirectoryUtility9.png b/src/site/resources/images/osxDirectoryUtility9.png
new file mode 100644
index 0000000..737e376
Binary files /dev/null and b/src/site/resources/images/osxDirectoryUtility9.png differ
diff --git a/src/site/resources/images/osxIcal1.png b/src/site/resources/images/osxIcal1.png
new file mode 100644
index 0000000..91b06c8
Binary files /dev/null and b/src/site/resources/images/osxIcal1.png differ
diff --git a/src/site/resources/images/osxIcal2.png b/src/site/resources/images/osxIcal2.png
new file mode 100644
index 0000000..41ecd2c
Binary files /dev/null and b/src/site/resources/images/osxIcal2.png differ
diff --git a/src/site/resources/images/osxIcal3.png b/src/site/resources/images/osxIcal3.png
new file mode 100644
index 0000000..c0eab88
Binary files /dev/null and b/src/site/resources/images/osxIcal3.png differ
diff --git a/src/site/resources/images/osxMailImap1.png b/src/site/resources/images/osxMailImap1.png
new file mode 100644
index 0000000..42e1cc9
Binary files /dev/null and b/src/site/resources/images/osxMailImap1.png differ
diff --git a/src/site/resources/images/osxMailImap10.png b/src/site/resources/images/osxMailImap10.png
new file mode 100644
index 0000000..ffaeec3
Binary files /dev/null and b/src/site/resources/images/osxMailImap10.png differ
diff --git a/src/site/resources/images/osxMailImap11.png b/src/site/resources/images/osxMailImap11.png
new file mode 100644
index 0000000..6b28662
Binary files /dev/null and b/src/site/resources/images/osxMailImap11.png differ
diff --git a/src/site/resources/images/osxMailImap12.png b/src/site/resources/images/osxMailImap12.png
new file mode 100644
index 0000000..5aa7ce7
Binary files /dev/null and b/src/site/resources/images/osxMailImap12.png differ
diff --git a/src/site/resources/images/osxMailImap13.png b/src/site/resources/images/osxMailImap13.png
new file mode 100644
index 0000000..1592a06
Binary files /dev/null and b/src/site/resources/images/osxMailImap13.png differ
diff --git a/src/site/resources/images/osxMailImap2.png b/src/site/resources/images/osxMailImap2.png
new file mode 100644
index 0000000..9c0c00d
Binary files /dev/null and b/src/site/resources/images/osxMailImap2.png differ
diff --git a/src/site/resources/images/osxMailImap3.png b/src/site/resources/images/osxMailImap3.png
new file mode 100644
index 0000000..0d4a504
Binary files /dev/null and b/src/site/resources/images/osxMailImap3.png differ
diff --git a/src/site/resources/images/osxMailImap4.png b/src/site/resources/images/osxMailImap4.png
new file mode 100644
index 0000000..4fd5619
Binary files /dev/null and b/src/site/resources/images/osxMailImap4.png differ
diff --git a/src/site/resources/images/osxMailImap5.png b/src/site/resources/images/osxMailImap5.png
new file mode 100644
index 0000000..54154d8
Binary files /dev/null and b/src/site/resources/images/osxMailImap5.png differ
diff --git a/src/site/resources/images/osxMailImap6.png b/src/site/resources/images/osxMailImap6.png
new file mode 100644
index 0000000..7866f48
Binary files /dev/null and b/src/site/resources/images/osxMailImap6.png differ
diff --git a/src/site/resources/images/osxMailImap7.png b/src/site/resources/images/osxMailImap7.png
new file mode 100644
index 0000000..12c9803
Binary files /dev/null and b/src/site/resources/images/osxMailImap7.png differ
diff --git a/src/site/resources/images/osxMailImap8.png b/src/site/resources/images/osxMailImap8.png
new file mode 100644
index 0000000..bb0fd4c
Binary files /dev/null and b/src/site/resources/images/osxMailImap8.png differ
diff --git a/src/site/resources/images/osxMailImap9.png b/src/site/resources/images/osxMailImap9.png
new file mode 100644
index 0000000..6abe5c9
Binary files /dev/null and b/src/site/resources/images/osxMailImap9.png differ
diff --git a/src/site/resources/images/osxconfirm.png b/src/site/resources/images/osxconfirm.png
new file mode 100644
index 0000000..7686f29
Binary files /dev/null and b/src/site/resources/images/osxconfirm.png differ
diff --git a/src/site/resources/images/osxdesktop.jpg b/src/site/resources/images/osxdesktop.jpg
new file mode 100644
index 0000000..baaad18
Binary files /dev/null and b/src/site/resources/images/osxdesktop.jpg differ
diff --git a/src/site/resources/images/osxdownload.png b/src/site/resources/images/osxdownload.png
new file mode 100644
index 0000000..d1dd969
Binary files /dev/null and b/src/site/resources/images/osxdownload.png differ
diff --git a/src/site/resources/images/osxgrowl.png b/src/site/resources/images/osxgrowl.png
new file mode 100644
index 0000000..9b1b5b3
Binary files /dev/null and b/src/site/resources/images/osxgrowl.png differ
diff --git a/src/site/resources/images/osxsettings.png b/src/site/resources/images/osxsettings.png
new file mode 100644
index 0000000..c3b0c29
Binary files /dev/null and b/src/site/resources/images/osxsettings.png differ
diff --git a/src/site/resources/images/setup1.png b/src/site/resources/images/setup1.png
new file mode 100644
index 0000000..3de60de
Binary files /dev/null and b/src/site/resources/images/setup1.png differ
diff --git a/src/site/resources/images/setup2.png b/src/site/resources/images/setup2.png
new file mode 100644
index 0000000..452ab01
Binary files /dev/null and b/src/site/resources/images/setup2.png differ
diff --git a/src/site/resources/images/setup3.png b/src/site/resources/images/setup3.png
new file mode 100644
index 0000000..ba074d2
Binary files /dev/null and b/src/site/resources/images/setup3.png differ
diff --git a/src/site/resources/images/setup4.png b/src/site/resources/images/setup4.png
new file mode 100644
index 0000000..3bb9d1a
Binary files /dev/null and b/src/site/resources/images/setup4.png differ
diff --git a/src/site/resources/images/setup5.png b/src/site/resources/images/setup5.png
new file mode 100644
index 0000000..2344140
Binary files /dev/null and b/src/site/resources/images/setup5.png differ
diff --git a/src/site/resources/images/thunderbirdAccount1.png b/src/site/resources/images/thunderbirdAccount1.png
new file mode 100644
index 0000000..0fcd42f
Binary files /dev/null and b/src/site/resources/images/thunderbirdAccount1.png differ
diff --git a/src/site/resources/images/thunderbirdAccount2.png b/src/site/resources/images/thunderbirdAccount2.png
new file mode 100644
index 0000000..7bbc947
Binary files /dev/null and b/src/site/resources/images/thunderbirdAccount2.png differ
diff --git a/src/site/resources/images/thunderbirdAccount3.png b/src/site/resources/images/thunderbirdAccount3.png
new file mode 100644
index 0000000..7376b5c
Binary files /dev/null and b/src/site/resources/images/thunderbirdAccount3.png differ
diff --git a/src/site/resources/images/thunderbirdAccount4.png b/src/site/resources/images/thunderbirdAccount4.png
new file mode 100644
index 0000000..7410258
Binary files /dev/null and b/src/site/resources/images/thunderbirdAccount4.png differ
diff --git a/src/site/resources/images/thunderbirdAccountPop3.png b/src/site/resources/images/thunderbirdAccountPop3.png
new file mode 100644
index 0000000..60798b9
Binary files /dev/null and b/src/site/resources/images/thunderbirdAccountPop3.png differ
diff --git a/src/site/resources/images/thunderbirdCalendar1.png b/src/site/resources/images/thunderbirdCalendar1.png
new file mode 100644
index 0000000..c94ada6
Binary files /dev/null and b/src/site/resources/images/thunderbirdCalendar1.png differ
diff --git a/src/site/resources/images/thunderbirdCalendar2.png b/src/site/resources/images/thunderbirdCalendar2.png
new file mode 100644
index 0000000..146242a
Binary files /dev/null and b/src/site/resources/images/thunderbirdCalendar2.png differ
diff --git a/src/site/resources/images/thunderbirdCalendar3.png b/src/site/resources/images/thunderbirdCalendar3.png
new file mode 100644
index 0000000..75968bf
Binary files /dev/null and b/src/site/resources/images/thunderbirdCalendar3.png differ
diff --git a/src/site/resources/images/thunderbirdCalendar4.png b/src/site/resources/images/thunderbirdCalendar4.png
new file mode 100644
index 0000000..2c2b242
Binary files /dev/null and b/src/site/resources/images/thunderbirdCalendar4.png differ
diff --git a/src/site/resources/images/thunderbirdCalendar5.png b/src/site/resources/images/thunderbirdCalendar5.png
new file mode 100644
index 0000000..13be2be
Binary files /dev/null and b/src/site/resources/images/thunderbirdCalendar5.png differ
diff --git a/src/site/resources/images/thunderbirdCarddav1.png b/src/site/resources/images/thunderbirdCarddav1.png
new file mode 100644
index 0000000..60d0f16
Binary files /dev/null and b/src/site/resources/images/thunderbirdCarddav1.png differ
diff --git a/src/site/resources/images/thunderbirdCarddav2.png b/src/site/resources/images/thunderbirdCarddav2.png
new file mode 100644
index 0000000..9aa162f
Binary files /dev/null and b/src/site/resources/images/thunderbirdCarddav2.png differ
diff --git a/src/site/resources/images/thunderbirdCarddav3.png b/src/site/resources/images/thunderbirdCarddav3.png
new file mode 100644
index 0000000..4f11860
Binary files /dev/null and b/src/site/resources/images/thunderbirdCarddav3.png differ
diff --git a/src/site/resources/images/thunderbirdDirectory1.png b/src/site/resources/images/thunderbirdDirectory1.png
new file mode 100644
index 0000000..0b41d8a
Binary files /dev/null and b/src/site/resources/images/thunderbirdDirectory1.png differ
diff --git a/src/site/resources/images/thunderbirdDirectory2.png b/src/site/resources/images/thunderbirdDirectory2.png
new file mode 100644
index 0000000..6ff3519
Binary files /dev/null and b/src/site/resources/images/thunderbirdDirectory2.png differ
diff --git a/src/site/resources/images/thunderbirdSmtp.png b/src/site/resources/images/thunderbirdSmtp.png
new file mode 100644
index 0000000..76335fa
Binary files /dev/null and b/src/site/resources/images/thunderbirdSmtp.png differ
diff --git a/src/site/resources/images/ubuntutray.png b/src/site/resources/images/ubuntutray.png
new file mode 100644
index 0000000..0d01199
Binary files /dev/null and b/src/site/resources/images/ubuntutray.png differ
diff --git a/src/site/resources/images/ubuntutraysettings.png b/src/site/resources/images/ubuntutraysettings.png
new file mode 100644
index 0000000..05160f4
Binary files /dev/null and b/src/site/resources/images/ubuntutraysettings.png differ
diff --git a/src/site/resources/setup.html b/src/site/resources/setup.html
new file mode 100644
index 0000000..46de401
--- /dev/null
+++ b/src/site/resources/setup.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+ <head>
+ <title>Maven -
+ Setup DavMail</title>
+
+ <meta http-equiv="refresh" content="0; url=windowssetup.html" />
+ <body>
+ Redirecting to <a href="windowssetup.html" />...
+ </body>
+</html>
diff --git a/src/site/site.xml b/src/site/site.xml
new file mode 100644
index 0000000..2d6fc13
--- /dev/null
+++ b/src/site/site.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<project name="Maven" xmlns="http://maven.apache.org/DECORATION/1.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/DECORATION/1.0.0 http://maven.apache.org/xsd/decoration-1.0.0.xsd">
+ <version position="left"/>
+
+ <bannerLeft>
+ <name>DavMail Gateway</name>
+ <src>images/davmailLogo.png</src>
+ <href>http://davmail.sourceforge.net</href>
+ </bannerLeft>
+ <bannerRight>
+ <name>SourceForge</name>
+ <src>http://sflogo.sourceforge.net/sflogo.php?group_id=184600&type=13</src>
+ <href>http://sourceforge.net/projects/davmail</href>
+ </bannerRight>
+ <poweredBy>
+ <logo name="Donate" href="https://sourceforge.net/donate/index.php?group_id=184600"
+ img="https://sourceforge.net/images/project-support.jpg"/>
+ </poweredBy>
+ <skin>
+ <groupId>org.apache.maven.skins</groupId>
+ <artifactId>maven-default-skin</artifactId>
+ </skin>
+ <body>
+ <head>
+ <link href="/favicon.ico" rel="shortcut icon"/>
+ </head>
+ <menu name="Quick Links">
+ <item name="Home" href="/index.html"/>
+ <item name="Download" href="/download.html"/>
+ <item name="Roadmap" href="/roadmap.html"/>
+ <item name="Build" href="/build.html"/>
+ <item name="FAQ" href="/faq.html"/>
+ <item name="SourceForge site" href="http://sourceforge.net/projects/davmail"/>
+ <item name="Reviews" href="/reviews.html"/>
+ </menu>
+
+ <menu name="DavMail Setup">
+ <item name="Windows Setup" href="/windowssetup.html"/>
+ <item name="Linux Setup" href="/linuxsetup.html"/>
+ <item name="Mac OS X" href="/macosxsetup.html"/>
+ <item name="Server Setup" href="/serversetup.html"/>
+ <item name="Getting Started" href="/gettingstarted.html"/>
+ <item name="Advanced Settings" href="/advanced.html"/>
+ <item name="SSL Setup" href="/sslsetup.html"/>
+ </menu>
+
+ <menu name="Thunderbird Setup">
+ <item name="POP Mail setup" href="/thunderbirdmailsetup.html"/>
+ <item name="IMAP Mail setup" href="/thunderbirdimapmailsetup.html"/>
+ <item name="Calendar setup" href="/thunderbirdcalendarsetup.html"/>
+ <item name="Directory setup" href="/thunderbirddirectorysetup.html"/>
+ <item name="Carddav setup" href="/thunderbirdcarddavsetup.html"/>
+ </menu>
+ <menu name="OSX Setup">
+ <item name="iCal setup" href="/osxicalsetup.html"/>
+ <item name="Directory setup" href="/osxdirectorysetup.html"/>
+ <item name="IMAP Mail setup" href="/osximapmailsetup.html"/>
+ <item name="Address book setup" href="/osxaddressbooksetup.html"/>
+ </menu>
+ <menu name="iPhone Setup">
+ <item name="iPhone setup" href="/iphonesetup.html"/>
+ <item name="Mail setup" href="/iphonemailsetup.html"/>
+ <item name="Contact setup" href="/iphonecarddavsetup.html"/>
+ <item name="Calendar setup" href="/iphonecaldavsetup.html"/>
+ </menu>
+ <menu ref="reports"/>
+ </body>
+</project>
diff --git a/src/site/xdoc/advanced.xml b/src/site/xdoc/advanced.xml
new file mode 100644
index 0000000..0e0fb8a
--- /dev/null
+++ b/src/site/xdoc/advanced.xml
@@ -0,0 +1,270 @@
+<?xml version="1.0"?>
+<document xmlns="http://maven.apache.org/XDOC/2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+
+ <properties>
+ <title>DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway - Advanced settings</title>
+ <author email="mguessan at free.fr">Mickael Guessant</author>
+ </properties>
+
+ <body>
+
+ <section name="Advanced settings">
+ <p>In addition to basic settings described in
+ <a href="gettingstarted.html">Getting started</a>
+ DavMail also supports the following settings:
+ </p>
+ <subsection name="Global settings">
+ <table>
+ <tr>
+ <th>Parameter</th>
+ <th>Description</th>
+ <th>Sample value</th>
+ </tr>
+ <tr>
+ <td>Enable EWS</td>
+ <td>Enable EWS mode on Exchange 2010 or Exchange 2007 with Webdav disabled</td>
+ <td>false</td>
+ </tr>
+ <tr>
+ <td>Default domain</td>
+ <td>Default windows domain name</td>
+ <td>DOMAIN</td>
+ </tr>
+ <tr>
+ <td>Display startup banner</td>
+ <td>Whether to show the initial startup notification window or not</td>
+ <td>true</td>
+ </tr>
+ <tr>
+ <td>Disable balloon notifications</td>
+ <td>Disable all graphical notifications</td>
+ <td>false</td>
+ </tr>
+ <tr>
+ <td>Disable update check</td>
+ <td>Disable DavMail check for new version</td>
+ <td>false</td>
+ </tr>
+ </table>
+ </subsection>
+ <subsection name="Network">
+ <table>
+ <tr>
+ <th>Parameter</th>
+ <th>Description</th>
+ <th>Sample value</th>
+ </tr>
+ <tr>
+ <td>Allow remote connections</td>
+ <td>Allow remote connections to the gateway (server mode)</td>
+ <td>false</td>
+ </tr>
+ <tr>
+ <td>Bind Address</td>
+ <td>Bind only to the specified network address, leave empty to listen on all network
+ interfaces
+ </td>
+ <td>10.0.1.2</td>
+ </tr>
+ <tr>
+ <td>Client connection timeout</td>
+ <td>Client connection timeout in seconds, 0 to disable timeout, empty for 5 minutes</td>
+ <td>300</td>
+ </tr>
+ </table>
+ </subsection>
+ <subsection name="Encryption">
+ <table>
+ <tr>
+ <th>Parameter</th>
+ <th>Description</th>
+ <th>Sample value</th>
+ </tr>
+ <tr>
+ <td>Server certificate hash</td>
+ <td>Manually accepted server certificate hash, contains the SHA1 hash of
+ a manually accepted certificate (invalid or self signed)
+ </td>
+ <td>9F:CC:59:82:1F:C:CD:29:7C:70:F0:D8:37:B1:77:3F:48:84:AE:C4</td>
+ </tr>
+ <tr>
+ <td>Key store type</td>
+ <td>To encrypt communication between client and DavMail, create a server certificate,
+ choose key store type and set key store path
+ </td>
+ <td>JKS</td>
+ </tr>
+ <tr>
+ <td>Key store</td>
+ <td>SSL certificate key store file path</td>
+ <td>path/to/keystore</td>
+ </tr>
+ <tr>
+ <td>Key store password</td>
+ <td>Key store password</td>
+ <td>password</td>
+ </tr>
+ <tr>
+ <td>Key password</td>
+ <td>SSL key password inside key store</td>
+ <td>password</td>
+ </tr>
+ <tr>
+ <td>Client key store type</td>
+ <td>When the Exchange server requires mutual authentication,
+ choose client certificate key store type, PKCS11 for smartcard,
+ PKCS12 or JKS for certificate file
+ </td>
+ <td>PKCS11</td>
+ </tr>
+ <tr>
+ <td>Client key store</td>
+ <td>SSL client certificate key store file path</td>
+ <td>path/to/keystore</td>
+ </tr>
+ <tr>
+ <td>Client key store password</td>
+ <td>Client key store password, leave empty for runtime prompt</td>
+ <td>password</td>
+ </tr>
+ <tr>
+ <td>PKCS11 library</td>
+ <td>PKCS11 (smartcard) library path (.so or .dll)</td>
+ <td>softokn3.dll</td>
+ </tr>
+ <tr>
+ <td>PKCS11 config</td>
+ <td>Optional additional PKCS11 settings (slot, nssArgs, ...)</td>
+ <td>slot=2</td>
+ </tr>
+ </table>
+ </subsection>
+ <subsection name="POP">
+ <table>
+ <tr>
+ <th>Parameter</th>
+ <th>Description</th>
+ <th>Sample value</th>
+ </tr>
+ <tr>
+ <td>Keep Delay (POP)</td>
+ <td>Number of days to keep messages in Exchange trash folder before actual deletion,
+ only for POP service
+ </td>
+ <td>30</td>
+ </tr>
+ <tr>
+ <td>Sent Keep Delay (POP)</td>
+ <td>Number of days to keep sent messages in Exchange sent folder,
+ only for POP service
+ </td>
+ <td>90</td>
+ </tr>
+ <tr>
+ <td>POP mark read</td>
+ <td>Mark messages read on server immediately after retrieval</td>
+ <td>true</td>
+ </tr>
+ </table>
+ </subsection>
+ <subsection name="Caldav (Calendar)">
+ <table>
+ <tr>
+ <th>Parameter</th>
+ <th>Description</th>
+ <th>Sample value</th>
+ </tr>
+ <tr>
+ <td>Calendar past events (Caldav)</td>
+ <td>Get events in the past not older than specified days count, leave empty for no limits</td>
+ <td>90</td>
+ </tr>
+ <tr>
+ <td>Edit Caldav notifications</td>
+ <td>Enable interactive Caldav edit notification window</td>
+ <td>false</td>
+ </tr>
+ <tr>
+ <td>Force Active Sync update</td>
+ <td>Use double event update to trigger ActiveSync mobile phones sync, only in WebDav mode</td>
+ <td>false</td>
+ </tr>
+ <tr>
+ <td>Caldav alarm sound</td>
+ <td>Convert Caldav alarm to sound alarm supported by iCal, e.g. Basso. Leave empty for no
+ conversion
+ </td>
+ <td>Basso</td>
+ </tr>
+ </table>
+ </subsection>
+ <subsection name="IMAP">
+ <table>
+ <tr>
+ <th>Parameter</th>
+ <th>Description</th>
+ <th>Sample value</th>
+ </tr>
+ <tr>
+ <td>IDLE folder monitor delay (IMAP):</td>
+ <td>IMAP folder idle monitor delay in minutes, leave empty to disable IDLE support</td>
+ <td>1</td>
+ </tr>
+ <tr>
+ <td>IMAP auto expunge</td>
+ <td>Delete messages immediately on the server over IMAP, i.e. expunge message on \Deleted flag set</td>
+ <td>true</td>
+ </tr>
+ </table>
+ </subsection>
+ <subsection name="SMTP">
+ <table>
+ <tr>
+ <th>Parameter</th>
+ <th>Description</th>
+ <th>Sample value</th>
+ </tr>
+ <tr>
+ <td>SMTP save in sent:</td>
+ <td>Save messages sent over SMTP in server Sent folder</td>
+ <td>true</td>
+ </tr>
+ </table>
+ </subsection>
+ <subsection name="Logging">
+ <table>
+ <tr>
+ <th>Parameter</th>
+ <th>Description</th>
+ <th>Sample value</th>
+ </tr>
+ <tr>
+ <td>Logging levels</td>
+ <td>Default, DavMail and HttpClient logging levels, see Log4J documentation for more details
+ </td>
+ <td>WARN</td>
+ </tr>
+ <tr>
+ <td>Log file Path</td>
+ <td>DavMail log file path (default is davmail.log in working directory on Unix and Windows,
+ ~/Library/Logs/DavMail/davmail.log on OSX)
+ </td>
+ <td>davmail.log</td>
+ </tr>
+ <tr>
+ <td>Log file Size</td>
+ <td>Maximum log file size, use Log4J syntax, see
+ <a href="http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/RollingFileAppender.html#setMaxFileSize%28java.lang.String%29">
+ RollingFileAppender
+ </a>
+ </td>
+ <td>1MB</td>
+ </tr>
+ </table>
+ </subsection>
+ </section>
+
+ </body>
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/build.xml b/src/site/xdoc/build.xml
new file mode 100644
index 0000000..41547c4
--- /dev/null
+++ b/src/site/xdoc/build.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0"?>
+<document xmlns="http://maven.apache.org/XDOC/2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+
+ <properties>
+ <title>DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway - Frequently asked questions</title>
+ <author email="mguessan at free.fr">Mickael Guessant</author>
+ </properties>
+
+ <body>
+
+ <section name="Building DavMail from source">
+ <p>Building DavMail is quite simple:</p>
+ <ul>
+ <li>Make sure you have Java 6 or 7 installed and set as current version by checking
+ JAVA_HOME:
+ <br/>
+ <ul>
+ <li>Unix/OSX:
+ <source>echo $JAVA_HOME</source>
+ </li>
+ <li>Windows:
+ <source>echo %JAVA_HOME%</source>
+ </li>
+ </ul>
+ </li>
+ <li>Check if you already have Apache Ant installed :<br/>
+ <source>ant -version</source><br/>
+ If you don't, download and unzip ant from <a href="http://ant.apache.org/">ant.apache.org</a>,
+ add ant/bin full path to the PATH environment variable.
+ </li>
+ <li>Get DavMail source package from sourceforge and uncompress it:
+ <source>tar xvzf davmail-src-*.tgz</source>
+ <br/>
+ or checkout trunk:
+ <source>svn co http://davmail.svn.sourceforge.net/svnroot/davmail/trunk</source>
+ <br/>
+ or get and uncompress tarball from
+ <a href="http://davmail.svn.sourceforge.net/viewvc/davmail/trunk">
+ http://davmail.svn.sourceforge.net/viewvc/davmail/trunk
+ </a>
+ </li>
+ <li>Additional steps on Windows:
+ <ul>
+ <li>Download and install NSIS from
+ <a href="http://nsis.sourceforge.net">http://nsis.sourceforge.net</a>
+ </li>
+ <li>Make sure NSIS install path (C:\Program Files\NSIS) is available in system path</li>
+ <li>Then get KillProcess plugin from
+ <a href="http://www.esanu.name/software/index.php/nsis-kill-process/">
+ http://www.esanu.name/software/index.php/nsis-kill-process/
+ </a>
+ or the nsis directory in DavMail source.
+ </li>
+ <li>Copy the processwork.dll library to NSIS plugin directory.</li>
+ </ul>
+ </li>
+ <li>Go into davmail directory root directory (which contains the build.xml file) and
+ type:
+ <source>ant</source>
+ </li>
+ </ul>
+ <p>Packages are then available under dist
+ </p>
+ </section>
+ </body>
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/download.xml b/src/site/xdoc/download.xml
new file mode 100644
index 0000000..a9b6202
--- /dev/null
+++ b/src/site/xdoc/download.xml
@@ -0,0 +1,122 @@
+<?xml version="1.0"?>
+<document xmlns="http://maven.apache.org/XDOC/2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+
+ <properties>
+ <title>DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway - Download</title>
+ <author email="mguessan at free.fr">Mickael Guessant</author>
+ </properties>
+
+ <body>
+
+ <section name="Download latest DavMail Gateway release">
+ <p>The right DavMail package choice depends on your operating system and
+ deployment (server or workstation).
+ </p>
+ <p>Please consider making a donation to help this project at
+ <a href="https://sourceforge.net/donate/index.php?group_id=184600"
+ title="Donate">
+ SourceForge Donation page
+ </a>
+ </p>
+
+ <subsection name="Workstation (personal) mode">
+ <ul>
+ <li>
+ <strong>Windows:</strong>
+ Download installation package
+ <a href="http://sourceforge.net/projects/davmail/files/">davmail-<em>version</em>-setup.exe
+ </a>
+ <br/>
+ and follow instructions at
+ <a href="windowssetup.html">DavMail Setup on windows</a>
+ </li>
+ <li>
+ <strong>Mac OS X:</strong>
+ Download
+ <a href="http://sourceforge.net/projects/davmail/files/">DavMail-MacOSX-<em>version</em>.app.zip
+ </a>
+ <br/>
+ and follow instructions at
+ <a href="macosxsetup.html">DavMail Setup on Mac OS X</a>
+ </li>
+ <li>
+ <strong>Debian Linux (Ubuntu):</strong>
+ Download debian package
+ <a href="http://sourceforge.net/projects/davmail/files/">davmail_<em>version</em>-1_all.deb
+ </a>
+ <br/>
+ and follow instructions at
+ <a href="linuxsetup.html">DavMail Setup on linux</a>
+ </li>
+ <li>
+ <strong>Linux (other):</strong>
+ Download linux package
+ <a href="http://sourceforge.net/projects/davmail/files/">davmail-linux-x86-<em>version</em>.tgz
+ </a>
+ , or 64 bits package
+ <a href="http://sourceforge.net/projects/davmail/files/">davmail-linux-x86_64-
+ <em>version</em>
+ .tgz
+ </a>
+ <br/>
+ and follow instructions at
+ <a href="linuxsetup.html">DavMail Setup on linux</a>
+ <br/>
+ Note: the only difference between 32 and 64 bits packages is the included SWT library.
+ </li>
+ <li>
+ <strong>Other:</strong>
+ Download platform independent package
+ <a href="http://sourceforge.net/projects/davmail/files/">davmail-<em>version</em>.zip
+ </a>
+ <br/>
+ and follow instructions at
+ <a href="linuxsetup.html">DavMail Setup on linux</a>
+ <br/>
+ </li>
+ </ul>
+ </subsection>
+
+ <subsection name="Server (shared) mode">
+ <ul>
+ <li>
+ <strong>Standalone:</strong>
+ Download platform independent package
+ <a href="http://sourceforge.net/projects/davmail/files/">davmail-<em>version</em>.zip
+ </a>
+ <br/>
+ and follow instructions at
+ <a href="serversetup.html">DavMail Setup as a standalone server</a>
+ <br/>
+ </li>
+ <li>
+ <strong>Webapp:</strong>
+ If you intend to run DavMail inside an application server, download webapp package
+ <a href="http://sourceforge.net/projects/davmail/files/">davmail-<em>version</em>.war
+ </a>
+ <br/>
+ and follow instructions at
+ <a href="serversetup.html">DavMail Setup as a JEE Web Application</a>
+ (last section)
+ <br/>
+ </li>
+ </ul>
+ </subsection>
+
+ <subsection name="Build from source">
+ <p>
+ Download source tarball
+ <a href="http://sourceforge.net/projects/davmail/files/">davmail-src-<em>version</em>.tgz
+ </a>
+ <br/>
+ and follow instructions at
+ <a href="build.html">Building DavMail from source</a>
+ <br/>
+ </p>
+ </subsection>
+ </section>
+
+ </body>
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/faq.xml b/src/site/xdoc/faq.xml
new file mode 100644
index 0000000..08754e3
--- /dev/null
+++ b/src/site/xdoc/faq.xml
@@ -0,0 +1,318 @@
+<?xml version="1.0"?>
+<document xmlns="http://maven.apache.org/XDOC/2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+
+ <properties>
+ <title>DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway - Frequently asked questions</title>
+ <author email="mguessan at free.fr">Mickael Guessant</author>
+ </properties>
+
+ <body>
+
+ <section name="Frequently asked questions">
+ <p>Frequently asked questions on DavMail forums, mailing list or direct email.
+ </p>
+ <subsection name="Feedback">
+ <p>
+ <strong>How do I create a WIRE debug log ?</strong>
+ </p>
+ <p>In DavMail trackers and forums, I often ask for a WIRE debug log. To create this
+ log file, you will need to change DavMail settings under the Logging tab.
+ Set the WIRE logging category to DEBUG level.
+ </p>
+ <p>You can then reproduce your issue and send the log file to
+ <a href="mailto:mguessan at free.fr">mguessan at free.fr</a>
+ </p>
+ <p>The davmail.log file is available in DavMail working directory on Unix and Windows,
+ ~/Library/Logs/DavMail/davmail.log on OSX. This default path can be customized with
+ the
+ <code>Log File Path</code>
+ setting.
+ </p>
+ <p>Note: on OSX Lion the Library folder is hidden, a simple way to access it is to launch
+ Console (Finder, Go to Utilities), right click on davmail.log and choose Reveal in Finder.</p>
+ </subsection>
+ <subsection name="General">
+ <p>
+ <strong>EWS endpoint not available</strong>
+ </p>
+ <p>Unfortunately, this probably means your Exchange administrators blocked EWS access.</p>
+ <p>To check this, try to connect to
+ <code>https://<i>mail.company.com</i>/ews/exchange.asmx
+ </code>
+ in your favorite web browser: you should get an authentication popup. On authentication success,
+ you should get the EWS wsdl definition. If you don't, please contact the Exchange administrators
+ to let them fix the broken Exchange server setup.
+ </p>
+ <p>Note: Microsoft Outlook 2011 on OSX and native OSX applications also rely on EWS to connect to
+ Exchange. Mobile devices (Android phones, iPhones, Windows mobile) use ActiveSync on a different
+ endpoint.
+ </p>
+ <p>
+ <strong>Authentication fails with invalid user or password message</strong>
+ </p>
+ <p>Authentication fails with the following message:
+ <code>Authentication failed: invalid user or password, retry with domain\user</code>
+ </p>
+ <p>Exchange before 2007 expects domain qualified user name, you may not have
+ to provide the domain name on the logon form if javascript is used to add it,
+ but DavMail can not execute javascript. Thus you need to prefix your user name
+ with the Active Directory domain followed by \
+ </p>
+ <p>Note to Mac users: OSX applications do not like username with backslash, you have to set windows
+ domain name in DavMail advanced settings and use the simple username in client application.
+ </p>
+
+ <p>
+ <strong>Authentication fails with error in parsing the status line</strong>
+ </p>
+ <p>Authentication fails with the following message:
+ <code>error in parsing the status line from the response: unable to find line starting with "HTTP"
+ </code>
+ </p>
+ <p>Your Exchange server expects HTTPS (secured) connections, use https instead of http in
+ OWA url.
+ </p>
+ <p>
+ <strong>Where can I find DavMail settings file ?</strong>
+ </p>
+ <p>The default location for DavMail settings is a file named .davmail.properties in user home
+ folder. This file is hidden under Linux and OSX. On windows, the user home folder is under
+ <code>Document and Settings</code>
+ </p>
+ <p>
+ <strong>How to run multiple instances of DavMail ?</strong>
+ </p>
+ <p>Just create multiple configuration files and provide them as a command line option, see previous
+ question:
+ </p>
+ <source>
+davmail server1.properties
+davmail server2.properties
+ </source>
+
+ </subsection>
+
+ <subsection name="Exchange setup">
+ <p>
+ <strong>What are the prerequisites on Exchange server side ?</strong>
+ </p>
+ <p>With Exchange 2003, you only need to have access to OWA (Outlook Web Access), Webdav, Address Book
+ and public folders are always available. Unfortunately, this is not the case with Exchange 2007.
+ You must make sure Webdav support is enabled on your server. You can test this by accessing
+ https://mail.company.com/exchange, which should redirect to /owa. Another important service is the
+ galfind (address book) access at /public/?Cmd=galfind
+ </p>
+ <p>Additional information for Exchange administrators to enable WebDav:</p>
+ <p>After running through the pre-requisite checker for each server (Front End CAS server and
+ Back End Mailbox server) and adding the appropriate roles and features, add the following
+ via the ServerManager:
+ </p>
+ <ul>
+ <li>Select the IIS role, then add Role Services</li>
+ <li>Add WebDav Publishing (both Cas server and Mailbox Server)</li>
+ <li>On the Mailbox server, add the ISAPI extensions Role Service to the IIS role</li>
+ <li>On the Default Web Site (each server), enable WebDav</li>
+ <li>Should not have to enable any additional WebDav rules</li>
+ <li>Install the server roles</li>
+ <li>Create a test mailbox (see Exchange Docs)</li>
+ </ul>
+ <p>To Test:</p>
+ <ul>
+ <li>Ensure that Outlook Web Access works by using the https://<i>exchangeServer</i>/owa URL
+ </li>
+ <li>Try using the pass through legacy URL: https://<i>exchangeServer</i>/exchange,
+ if you receive an error 500 âInternal Server Errorâ, then WebDav is not working.
+ </li>
+ </ul>
+ <p>
+ <strong>Is Exchange 2010 supported ?</strong>
+ </p>
+ <p>As Microsoft decided to drop WebDav support in Exchange 2010, I had to implement a new backend from
+ scratch to support the new Exchange Web Services interface. This support is still experimental,
+ but is working quite well. However, you still need to enable it manually in .davmail.properties
+ with the following line:
+ <br/>
+ <source>davmail.enableEws=true</source>
+ <br/>
+ </p>
+ <p>
+ <strong>Is DavMail based on Outlook protocol ?</strong>
+ </p>
+ <p>Outlook anywhere uses MAPI RPC over HTTPS to access company LAN through the firewall.
+ DavMail does not use MAPI at all but relies on WebDav (Exchange 2003/2007) or EWS (Exchange
+ 2007/2010) to access Exchange. This means you need direct access over HTTPS to either the OWA WebDav
+ url (/exchange/mail at company.com) or EWS (/ews/exchange.asmx) to access Exchange with DavMail.
+ </p>
+ </subsection>
+
+ <subsection name="OSX">
+ <p>
+ <strong>How do I make DavMail start automatically ?</strong>
+ </p>
+ <p>Add DavMail.app to "Login Items", see
+ <a href="http://docs.info.apple.com/article.html?path=Mac/10.6/en/15189.html">
+ Opening items automatically when you log in
+ </a>
+ </p>
+ </subsection>
+
+ <subsection name="Linux">
+ <p>
+ <strong>Why do I get black on black notifications on Ubuntu ?</strong>
+ </p>
+ <p>This is an SWT bug, upgrade to SWT 3.6, see
+ <a href="https://sourceforge.net/tracker/?func=detail&atid=909904&aid=3138023&group_id=184600">
+ On Ubuntu, notify text conflicts with default theme.
+ </a>
+ http://users.ox.ac.uk/~richardc/guides/LinuxNexus.html
+ </p>
+ <p>
+ <strong>DavMail hangs on 64-bit Linux</strong>
+ </p>
+ <p>Disable use system proxies option in DavMail settings
+ </p>
+ </subsection>
+
+
+ <subsection name="Security">
+ <p>
+ <strong>How do I secure DavMail connections ?</strong>
+ </p>
+ <p>Communication between DavMail and your Exchange server is secured by default as long as you access
+ OWA over HTTPS.
+ </p>
+ <p>Communication between the messaging client and DavMail is
+ <em>not</em>
+ secured by default. This is not an issue in standalone mode as all communication is local, but you
+ may want to improve this in server (shared) mode.
+ </p>
+ <p>This is quite simple: you just need to get a server certificate in PKCS12 or JKS format and add it in
+ the key store section in DavMail settings, see <a href="sslsetup.html">SSL Setup</a>.
+ </p>
+
+ <p>
+ <strong>Are my credentials safe ?</strong>
+ </p>
+ <p>DavMail does not store Exchange username or password, they are provided by the messaging client over
+ IMAP, HTTP, POP, SMTP or LDAP
+ </p>
+ </subsection>
+
+ <subsection name="Caldav (calendar)">
+ <p>
+ <strong>How do I detect errors in Thunderbird/Lightning calendar ?</strong>
+ </p>
+ <p>Unfortunately, Lightning does not provide much feedback of what is going
+ on. To detect errors at this level, you may activate calendar log settings
+ under Tools/Options/Advanced:
+ </p>
+ <source><![CDATA[calendar.debug.log=true
+calendar.debug.log.verbose=true]]></source>
+
+ <p>
+ <strong>Invalid notifications</strong>
+ </p>
+ <p>Lightning 0.9 mail notifications are broken, activate
+ Caldav notifications to outbox instead:
+ </p>
+ <p>Check the following parameter in Thunderbird, it should be set to true
+ </p>
+ <source>calendar.caldav.sched.enabled=true</source>
+
+ <p>
+ <strong>Public calendars</strong>
+ </p>
+ <p>To access public folders in Lightning, the URL scheme is:
+ </p>
+ <source>http://localhost:<i>1080</i>/public/<i>path/to/calendar</i></source>
+ <p>To access public folders in iCal, the URL scheme is:
+ </p>
+ <source>http://localhost:<i>1080</i>/principals/public/<i>path/to/calendar</i></source>
+ <p>Note: iCal does not support folder names with spaces or special characters</p>
+
+ <p>
+ <strong>Shared calendars</strong>
+ </p>
+ <p>To access a shared calendar in Lightning, the URL scheme is:
+ </p>
+ <source>http://localhost:<i>1080</i>/users/<i>user at company.com</i></source>
+ <p>To access a shared calendar in iCal, the URL scheme is:
+ </p>
+ <source>http://localhost:<i>1080</i>/principals/users/<i>user at company.com</i></source>
+
+ </subsection>
+
+ <subsection name="Mail">
+ <p>
+ <strong>Timeout errors during IMAP sync (broken pipe)</strong>
+ </p>
+ <p>If you Exchange server is too slow, you may get timeout errors during
+ message fetch.
+ </p>
+ <p>To fix the problem in OSX Mail, go to your account setup under preferences.
+ On the IMAP account having difficulty, go to the advanced tab. Where it says
+ "keep copies for offline viewing", change it to "only messages I've read" or
+ don't keep copies at all.
+ </p>
+ <p>Under Thunderbird, you can try to change IMAP timeout values
+ (
+ <code>mail.server.server<em>index</em>.timeout
+ </code>
+ and <code>mailnews.tcptimeout</code>) or avoid full
+ mailbox sync: disable global search and indexer under advanced options and change
+ Synchronization & Storage options under Account Settings.
+ </p>
+ <p>
+ <strong>Message deleted over IMAP still visible through OWA</strong>
+ </p>
+ <p>Message move does not exist in IMAP, thus the move is a copy followed by a delete.
+ And a delete is not immediate with IMAP: you need to EXPUNGE the folder to actually
+ delete a message.
+ </p>
+ <p>A workaround with Thunderbird is to set the property
+ <code>mail.imap.expunge_after_delete=true</code>
+ </p>
+ <p>New: a new setting is available in DavMail to delete messages immediately over IMAP:
+ <code>davmail.imapAutoExpunge=true</code>
+ </p>
+ <p>
+ <strong>Duplicate messages in Sent folder</strong>
+ </p>
+ <p>Messages sent through DavMail appear twice in Sent folder when the IMAP client is setup
+ to store sent messages on the server as Exchange already saves sent messages in this folder.
+ Change IMAP client settings or change Save in Sent option in DavMail settings to avoid this issue.
+ </p>
+ <p>
+ <strong>Sent messages encoding issue with Thunderbird</strong>
+ </p>
+ <p>If special characters appear replaced with ? in sent messages, set
+ <code>mail.strictly_mime</code>
+ advanced option.
+ Go to Tools/Options/Advanced/General/Config Editor and look for a preference
+ with name<code>mail.strictly_mime</code>, set value to
+ <code>true</code>
+ to send messages as quoted-printable.
+ </p>
+ <p>
+ <strong>Access another user mailbox</strong>
+ </p>
+ <p>Either set base IMAP path in client settings or use the following username:
+ <code>domain\user\otheruser at company.com</code> where otheruser at company.com it target mailbox
+ name.
+ </p>
+
+ </subsection>
+
+ <subsection name="Build">
+ <p>
+ <strong>How to build DavMail ?</strong>
+ </p>
+ <p>See
+ <a href="build.html">Building DavMail from source</a>
+ </p>
+ </subsection>
+ </section>
+ </body>
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/gettingstarted.xml b/src/site/xdoc/gettingstarted.xml
new file mode 100644
index 0000000..23024a2
--- /dev/null
+++ b/src/site/xdoc/gettingstarted.xml
@@ -0,0 +1,84 @@
+<?xml version="1.0"?>
+<document xmlns="http://maven.apache.org/XDOC/2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+
+ <properties>
+ <title>DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway - Getting started</title>
+ <author email="mguessan at free.fr">Mickael Guessant</author>
+ </properties>
+
+ <body>
+
+ <section name="DavMail configuration">
+ <p>In order to change DavMail settings, double click on the tray icon or
+ right click on it and choose
+ <code>Settings...</code>
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/davmailSettings.png" alt=""/>
+ </div>
+ <p>The following table describes the main options :</p>
+ <table>
+ <tr>
+ <th>Parameter</th>
+ <th>Description</th>
+ <th>Sample value</th>
+ </tr>
+ <tr>
+ <td>OWA url</td>
+ <td>Outlook Web Access URL to access the exchange server, i.e. the base webmail URL
+ <br/>
+ The path depends on Exchange version and configuration, with or without a reverse proxy.
+ DavMail must be able to find the authentication form at the provided URL.<br/>
+ Usual paths for different Exchange versions:<br/>
+ <ul>
+ <li>Exchange 2003: <code>https://<i>mail.company.com</i>/exchange/</code></li>
+ <li>Exchange 2007 Webdav mode: <code>https://<i>mail.company.com</i>/owa/</code></li>
+ <li>Exchange 2007 EWS mode: <code>https://<i>mail.company.com</i>/owa/</code></li>
+ <li>Exchange 2010 EWS mode: <code>https://<i>mail.company.com</i>/owa/</code></li>
+ <li>Exchange 2010 EWS mode with unsupported authentication form e.g. Windows Live login:
+ <code>https://<i>mail.company.com</i>/ews/exchange.asmx</code></li>
+ </ul>
+ </td>
+ <td>https://exchangeServer/owa/</td>
+ </tr>
+ <tr>
+ <td>Local POP port</td>
+ <td>Local POP server port to configure in POP client</td>
+ <td>110</td>
+ </tr>
+ <tr>
+ <td>Local IMAP port</td>
+ <td>Local IMAP server port to configure in IMAP client</td>
+ <td>143</td>
+ </tr>
+ <tr>
+ <td>Local SMTP port</td>
+ <td>Local SMTP server port to configure in SMTP client</td>
+ <td>25</td>
+ </tr>
+ <tr>
+ <td>Local Caldav/Carddav HTTP port</td>
+ <td>Local Caldav/Carddav server port to configure in Caldav (calendar)
+ or Carddav (address book) client</td>
+ <td>80</td>
+ </tr>
+ <tr>
+ <td>Local LDAP port</td>
+ <td>Local LDAP server port to configure in directory (address book) client</td>
+ <td>389</td>
+ </tr>
+ </table>
+ <p>Uncheck a port to disable matching service.</p>
+ <p>Activate panel under Proxy tab to set an HTTP proxy and associated credentials if needed</p>
+ <p>If you need to adjust other advanced settings, check <a href="advanced.html">Advanced settings</a></p>
+ <p>Proceed to
+ <a href="thunderbirdmailsetup.html">Thunderbird mail setup</a>
+ or
+ <a href="osxicalsetup.html">OSX iCal setup</a>
+ </p>
+ </section>
+
+ </body>
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/index.xml b/src/site/xdoc/index.xml
new file mode 100644
index 0000000..b07f980
--- /dev/null
+++ b/src/site/xdoc/index.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0"?>
+<document xmlns="http://maven.apache.org/XDOC/2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+
+ <properties>
+ <title>DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway</title>
+ <author email="mguessan at free.fr">Mickael Guessant</author>
+ </properties>
+
+ <body>
+ <section name="DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway">
+ <table class="about" border="0" style="width: 98%">
+ <tr>
+ <td>
+ <p>Ever wanted to get rid of Outlook ? DavMail is a POP/IMAP/SMTP/Caldav/Carddav/LDAP exchange
+ gateway allowing users to use any mail/calendar client (e.g. Thunderbird with Lightning or
+ Apple iCal) with an Exchange server, even from the internet or behind a firewall through
+ Outlook Web Access.
+ DavMail now includes an LDAP gateway to Exchange global address book and user personal
+ contacts to allow recipient address completion in mail compose window and full calendar
+ support with attendees free/busy display.
+ </p>
+ <p>DavMail also supports the CardDav protocol to sync address books. This new feature
+ is sponsored by
+ <a href="http://www.defense.gouv.fr/dga/">French Defense / DGA</a>
+ through project
+ <a href="http://www.trustedbird.org">Trustedbird</a>
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/davmailArchitecture.png" alt="DavMail Architecture" width="550"
+ height="364"/>
+ </div>
+ <p>The main goal of DavMail is to provide standard compliant protocols in front of proprietary
+ Exchange. This means LDAP for global address book, SMTP to send messages, IMAP to browse
+ messages on the server in any folder, POP to retrieve inbox messages only, Caldav for
+ calendar support and Carddav for personal contacts sync.
+ Thus any standard compliant client can be used with Microsoft Exchange.
+ </p>
+ <p>DavMail gateway is implemented in java and should run on any platform. Releases are tested
+ on Windows, Linux (Ubuntu) and Mac OSX. Tested successfully with the Iphone
+ (gateway running on a server).
+ </p>
+ </td>
+ <td>
+ <div style="text-align: center">
+ <script type="text/javascript"
+ src="http://www.ohloh.net/p/106265/widgets/project_thin_badge.js">
+ </script>
+ </div>
+ <a class="donate" href="https://sourceforge.net/donate/index.php?group_id=184600"
+ title="Donate">
+ Donate
+ </a>
+ <a class="download" href="download.html">
+ Download DavMail Gateway
+ </a>
+ </td>
+ </tr>
+ </table>
+ </section>
+ </body>
+</document>
+
\ No newline at end of file
diff --git a/src/site/xdoc/iphonecaldavsetup.xml b/src/site/xdoc/iphonecaldavsetup.xml
new file mode 100644
index 0000000..ddcd578
--- /dev/null
+++ b/src/site/xdoc/iphonecaldavsetup.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0"?>
+<document xmlns="http://maven.apache.org/XDOC/2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+
+ <properties>
+ <title>DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway - iPhone Caldav setup</title>
+ <author email="mguessan at free.fr">Mickael Guessant</author>
+ </properties>
+
+ <body>
+
+ <section name="iPhone Caldav setup">
+ <p>Note: you can't install DavMail directly on an iPhone, you need a separate DavMail server,
+ see <a href="iphonesetup.html">iPhone setup</a>.
+ </p>
+
+ <subsection name="Create a new account">
+ <p>Open iPhone Settings application and choose Mail, Contacts, Calendar:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneAccount1.png" alt=""/>
+ </div>
+
+ <p>Then select Add Account...:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneAccount2.png" alt=""/>
+ </div>
+
+ <p>DavMail is not an Exchange ActiveSync server, press Other:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneAccount3.png" alt=""/>
+ </div>
+
+ <p>Choose Add CalDAV Account:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneCaldav1.png" alt=""/>
+ </div>
+
+ <p>Enter your DavMail server hostname in the Server field,
+ append ':' and port if DavMail HTTP (Caldav and Carddav) port is not 80,
+ provide your username and password then press Next:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneCaldav2.png" alt=""/>
+ </div>
+
+ <p>On the account page, open Advanced Settings:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneCaldav3.png" alt=""/>
+ </div>
+
+ <p>Enable SSL and check port, make sure Account URL is
+ <em>https://davmailhost:1080/principals/users/username@company.com</em>:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneCaldav4.png" alt=""/>
+ </div>
+
+ </subsection>
+ </section>
+ </body>
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/iphonecarddavsetup.xml b/src/site/xdoc/iphonecarddavsetup.xml
new file mode 100644
index 0000000..3d7ed53
--- /dev/null
+++ b/src/site/xdoc/iphonecarddavsetup.xml
@@ -0,0 +1,69 @@
+<?xml version="1.0"?>
+<document xmlns="http://maven.apache.org/XDOC/2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+
+ <properties>
+ <title>DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway - iPhone Carddav setup</title>
+ <author email="mguessan at free.fr">Mickael Guessant</author>
+ </properties>
+
+ <body>
+
+ <section name="iPhone Carddav setup">
+ <p>Note: you can't install DavMail directly on an iPhone, you need a separate DavMail server,
+ see <a href="iphonesetup.html">iPhone setup</a>.
+ </p>
+
+ <subsection name="Create a new account">
+ <p>Open iPhone Settings application and choose Mail, Contacts, Calendar:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneAccount1.png" alt=""/>
+ </div>
+
+ <p>Then select Add Account...:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneAccount2.png" alt=""/>
+ </div>
+
+ <p>DavMail is not an Exchange ActiveSync server, press Other:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneAccount3.png" alt=""/>
+ </div>
+
+ <p>Choose Add CardDAV Account:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneCarddav1.png" alt=""/>
+ </div>
+
+ <p>Enter your DavMail server hostname in the Server field,
+ append ':' and port if DavMail HTTP (Caldav and Carddav) port is not 80,
+ provide your username and password then press Next:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneCarddav2.png" alt=""/>
+ </div>
+
+ <p>On the account page, open Advanced Settings:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneCarddav3.png" alt=""/>
+ </div>
+
+ <p>Enable SSL and check port:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneCarddav4.png" alt=""/>
+ </div>
+
+ <p>Contact setup is complete, proceed to
+ <a href="iphonecaldavsetup.html">iPhone Caldav setup</a>
+ </p>
+ </subsection>
+ </section>
+ </body>
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/iphonemailsetup.xml b/src/site/xdoc/iphonemailsetup.xml
new file mode 100644
index 0000000..10145b7
--- /dev/null
+++ b/src/site/xdoc/iphonemailsetup.xml
@@ -0,0 +1,145 @@
+<?xml version="1.0"?>
+<document xmlns="http://maven.apache.org/XDOC/2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+
+ <properties>
+ <title>DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway - iPhone Mail setup</title>
+ <author email="mguessan at free.fr">Mickael Guessant</author>
+ </properties>
+
+ <body>
+
+ <section name="iPhone Mail setup">
+ <p>Note: you can't install DavMail directly on an iPhone, you need a separate DavMail server,
+ see <a href="iphonesetup.html">iPhone setup</a>.
+ </p>
+
+ <subsection name="Create a new account">
+ <p>Open iPhone Settings application and choose Mail, Contacts, Calendar:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneAccount1.png" alt=""/>
+ </div>
+
+ <p>Then select Add Account...:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneAccount2.png" alt=""/>
+ </div>
+
+ <p>DavMail is not an Exchange ActiveSync server, press Other:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneAccount3.png" alt=""/>
+ </div>
+
+ <p>Choose Add Mail Account:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneMail01.png" alt=""/>
+ </div>
+
+ <p>Enter your name, email address and password, then press Next:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneMail02.png" alt=""/>
+ </div>
+
+ <p>Select IMAP account type:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneMail03.png" alt=""/>
+ </div>
+
+ <p>In Incoming Mail Server section, enter your DavMail server hostname,
+ append ':' and port if DavMail IMAP port is not 143:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneMail04.png" alt=""/>
+ </div>
+
+ <p>In Outgoing Mail Server section, enter your DavMail server hostname,
+ <em>do not append port, this is not supported by current iOS version</em>, then press Next:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneMail05.png" alt=""/>
+ </div>
+
+ <p>Wait until your iPhone has finished verifying all possible port/protocol combination:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneMail06.png" alt=""/>
+ </div>
+
+ <p>If you get the
+ <em>Cannot Connect Using SSL</em>
+ message, just select Yes,
+ you will just need to adjust account settings later.
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneMail07.png" alt=""/>
+ </div>
+
+ <p>Save the account:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneMail08.png" alt=""/>
+ </div>
+
+ <p>Confirm:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneMail09.png" alt=""/>
+ </div>
+
+ <p>Back to account settings page, still need to fix SMTP port
+ if DavMail is not listening on default port 25,
+ open account settings:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneMail10.png" alt=""/>
+ </div>
+
+ <p>Open Account Info:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneMail11.png" alt=""/>
+ </div>
+ <p>Open SMTP page:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneMail12.png" alt=""/>
+ </div>
+ <p>Choose primary server:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneMail13.png" alt=""/>
+ </div>
+ <p>Adjust SMTP port:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneMail14.png" alt=""/>
+ </div>
+ <p>Back to Account Info, open Advanced settings:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneMail15.png" alt=""/>
+ </div>
+ <p>Back to Account Info, open Advanced settings:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneMail15.png" alt=""/>
+ </div>
+ <p>Enable SSL on IMAP port:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/iphone/iphoneMail16.png" alt=""/>
+ </div>
+ <p>Mail setup is complete, proceed to
+ <a href="iphonecarddavsetup.html">iPhone Carddav setup</a>
+ </p>
+ </subsection>
+ </section>
+ </body>
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/iphonesetup.xml b/src/site/xdoc/iphonesetup.xml
new file mode 100644
index 0000000..e3d9578
--- /dev/null
+++ b/src/site/xdoc/iphonesetup.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0"?>
+<document xmlns="http://maven.apache.org/XDOC/2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+
+ <properties>
+ <title>DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway - iPhone setup</title>
+ <author email="mguessan at free.fr">Mickael Guessant</author>
+ </properties>
+
+ <body>
+
+ <section name="iPhone setup">
+ <p>In order to use DavMail with an iPhone, you need to install DavMail in server mode.
+ This server should be available directly on the internet or through a Wifi connection.
+ See
+ <a href="serversetup.html">Server setup</a>
+ for more information.
+ </p>
+
+ <p>As mail data between your iPhone and DavMail travels on the internet, you probably want
+ to switch DavMail to encrypted mode, see
+ <a href="sslsetup.html">SSL setup</a>
+ </p>
+
+ <p>Once you have a working DavMail server, proceed to:
+ <a href="iphonemailsetup.html">iPhone Mail setup</a>
+ </p>
+ <p>Then sync your Exchange contacts:
+ <a href="iphonecarddavsetup.html">iPhone Carddav setup</a>
+ </p>
+ <p>And add your calendar:
+ <a href="iphonecaldavsetup.html">iPhone Caldav setup</a>
+ </p>
+ </section>
+
+ </body>
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/linuxsetup.xml b/src/site/xdoc/linuxsetup.xml
new file mode 100644
index 0000000..f69e643
--- /dev/null
+++ b/src/site/xdoc/linuxsetup.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0"?>
+<document xmlns="http://maven.apache.org/XDOC/2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+
+ <properties>
+ <title>DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway - Linux setup</title>
+ <author email="mguessan at free.fr">Mickael Guessant</author>
+ </properties>
+
+ <body>
+
+ <section name="DavMail Setup on linux">
+ <subsection name="Debian package">
+ <p>Download current debian package from Sourceforge, double click on it
+ to launch package installer.
+ </p>
+ <p>
+ Select Install Package, installer will automatically download and
+ install Java and the appropriate SWT package. After installation,
+ DavMail is available in the Applications menu.
+ </p>
+ <p>Note to Ubuntu 12 Natty users: they eventually fixed Oneiric issue, use gsettings command below</p>
+ <p>Note to Ubuntu 11.10 Oneiric users: Unity does not support system tray defined by FreeDesktop, you will need
+ to switch to standard Gnome Panel to use DavMail (and any other application using system tray)</p>
+ <p>Note to Ubuntu 10.04 users: all tray icons are hidden by default, use the following command to restore normal mode:</p>
+ <code>gsettings set com.canonical.Unity.Panel systray-whitelist "['all']"</code>
+ </subsection>
+ <subsection name="Manual setup">
+ <p>Prerequisite: OpenJDK 6 or 7 or Sun JRE 6. Tray icon is now implemented with SWT and compatible with
+ Java 5.
+ </p>
+ <p>Note: some users reported issues with OpenJDK 6, please upgrade to OpenJDK 7 in this case.</p>
+
+ <p>You should first download and install Java, with the graphical package manager or through
+ command line.
+ </p>
+ <p>Under Ubuntu, launch System/Administration/Synaptic Package Manager, quick search
+ default-jre, mark for installation and click Apply
+ </p>
+ <p>Or use the following command:</p>
+ <code>sudo apt-get install default-jre</code>
+
+ <p>Download the linux x86 DavMail package from Sourceforge and uncompress it with
+ your favorite tool. The standard package will run natively on x86, to use DavMail
+ on any other hardware platform, replace the SWT with the right one from
+ <a href="http://www.eclipse.org/swt/">http://www.eclipse.org/swt/</a>
+ or use the platform independent package.
+ </p>
+ <p>On Ubuntu and other Gnome or Kde distributions, just use the desktop launcher.
+ On other distributions, try
+ <code>davmail.sh</code>.
+ You should now see the DavMail gateway icon in the tray :
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img align="middle" src="images/ubuntutray.png" alt=""/>
+ </div>
+ <p>Adjust DavMail settings :
+ <a href="gettingstarted.html">Getting started</a>
+ </p>
+ </subsection>
+ </section>
+
+ </body>
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/macosxsetup.xml b/src/site/xdoc/macosxsetup.xml
new file mode 100644
index 0000000..20c294b
--- /dev/null
+++ b/src/site/xdoc/macosxsetup.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0"?>
+<document xmlns="http://maven.apache.org/XDOC/2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+
+ <properties>
+ <title>DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway - Mac OS X setup</title>
+ <author email="mguessan at free.fr">Mickael Guessant</author>
+ </properties>
+
+ <body>
+
+ <section name="DavMail Setup on Mac OS X">
+ <p>Older OSX releases include Java 5, this version does not support Tray icon,
+ and DavMail uses a dedicated frame instead. An upgrade to Java 1.6 will enable tray icon.
+ Recent OSX versions either include Java 6 or can install it automatically on first Java application
+ launch.
+ </p>
+
+ <p>Download DavMail Mac OSX package from Sourceforge:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/osxdownload.png" alt="OSX Download"/>
+ </div>
+ <p>Safari should automatically extract the application archive. If not, just
+ double click on the zip file to trigger decompression. The extracted
+ DavMail.app directory is then recognised as a valid OSX application.
+ </p>
+ <p>Just double click on the application icon to launch DavMail. On
+ first start the application displays the Settings (Preferences) frame.
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/osxdesktop.jpg" alt="OSX Desktop"/>
+ </div>
+ <p>In order to get balloon notifications, you will need to install Growl 1.2.2 from
+ <a href="http://growl.info/">http://growl.info/</a>:</p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/osxgrowl.png" alt="OSX Growl"/>
+ </div>
+ <p>Adjust DavMail settings:
+ <a href="gettingstarted.html">Getting started</a>
+ </p>
+ </section>
+
+ </body>
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/osxaddressbooksetup.xml b/src/site/xdoc/osxaddressbooksetup.xml
new file mode 100644
index 0000000..eb0c899
--- /dev/null
+++ b/src/site/xdoc/osxaddressbooksetup.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+<document xmlns="http://maven.apache.org/XDOC/2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+
+ <properties>
+ <title>DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway - OSX Address Book setup</title>
+ <author email="mguessan at free.fr">Mickael Guessant</author>
+ </properties>
+
+ <body>
+
+ <section name="OSX Address Book setup">
+ <p>Launch OSX Address Book and create a new Carddav directory.
+ </p>
+
+ <p>Enter your credentials and
+ <code>localhost:1080</code>
+ as server location (Snow Leopard) or <code>http://localhost:1080</code> (OSX Lion)
+ to sync with the default personal address book.
+ </p>
+ <p>
+ <em>Note: uncheck SSL mode if DavMail is in cleartext (default) mode.</em>
+ </p>
+
+ </section>
+ </body>
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/osxdirectorysetup.xml b/src/site/xdoc/osxdirectorysetup.xml
new file mode 100644
index 0000000..4a530c6
--- /dev/null
+++ b/src/site/xdoc/osxdirectorysetup.xml
@@ -0,0 +1,86 @@
+<?xml version="1.0"?>
+<document xmlns="http://maven.apache.org/XDOC/2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+
+ <properties>
+ <title>DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway - OSX directory setup</title>
+ <author email="mguessan at free.fr">Mickael Guessant</author>
+ </properties>
+
+ <body>
+
+ <section name="OSX directory setup">
+ <p>DavMail Directory support is now available to access Exchange address book through LDAP.
+ A special OpenDirectory naming context was implemented to enable iCal attendee completion.
+ </p>
+
+ <subsection name="Open Directory Utility">
+ <p>In OSX Finder, go to Utilities and launch Directory Utility. Under OSX 10.6
+ Snow Leopard, Directory Utility is tucked away in /System/Library/CoreServices.
+ </p>
+ <p>Leopard only: choose Show Advanced Settings, switch to Services tab</p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/osxDirectoryUtility1.png" alt=""/>
+ </div>
+
+ <p>Select LDAPv3 and click the pencil icon (you may need to unlock settings with your
+ admin password first):
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/osxDirectoryUtility2.png" alt=""/>
+ </div>
+
+ <p>Click New...:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/osxDirectoryUtility3.png" alt=""/>
+ </div>
+
+ <p>Enter DavMail server name (localhost) and click Manual:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/osxDirectoryUtility4.png" alt=""/>
+ </div>
+
+ <p>Enter Configuration Name and choose OpenDirectory in LDAP Mappings:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/osxDirectoryUtility5.png" alt=""/>
+ </div>
+
+ <p>Enter o=od as Search Base Suffix and click Ok:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/osxDirectoryUtility6.png" alt=""/>
+ </div>
+
+ <p>Now click Edit...:</p>
+
+ <p>If DavMail LDAP listen port is not 389, check Use custom port and enter actual port (default is 1389):
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/osxDirectoryUtility7.png" alt=""/>
+ </div>
+
+ <p>Then activate authentication under the Security tab and
+ enter your credentials, then press OK twice to close the settings:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/osxDirectoryUtility8.png" alt=""/>
+ </div>
+
+ <p>Under the Search Policy tab, choose Contacts, set Search to Custom Path
+ and click the + to add /LDAPV3/localhost to the list of Directory Domains:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/osxDirectoryUtility9.png" alt=""/>
+ </div>
+
+ <p>Proceed to
+ <a href="osximapmailsetup.html">OSX IMAP Mail setup</a>
+ </p>
+ </subsection>
+ </section>
+ </body>
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/osxicalsetup.xml b/src/site/xdoc/osxicalsetup.xml
new file mode 100644
index 0000000..780b663
--- /dev/null
+++ b/src/site/xdoc/osxicalsetup.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0"?>
+<document xmlns="http://maven.apache.org/XDOC/2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+
+ <properties>
+ <title>DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway - OSX iCal setup</title>
+ <author email="mguessan at free.fr">Mickael Guessant</author>
+ </properties>
+
+ <body>
+
+ <section name="OSX iCal setup">
+ <p>DavMail Calendar support is now available, tested with Thunderbird/Lightning and Apple iCal.
+ This should also work with other Caldav clients.
+ </p>
+
+ <subsection name="Open iCal Preferences">
+ <p>Open iCal Preferences, go to the Accounts tab and click the plus button:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/osxIcal1.png" alt=""/>
+ </div>
+
+ <p>On Snow Leopard, choose CalDAV as account type, enter you Exchange username and password and server
+ address without path:
+ </p>
+ <code>http://localhost:1080
+ </code>
+ <p>On 10.5 choose a name, enter you Exchange username and password.
+ Open the server options section to define the calendar URL (adjust port to your settings):
+ </p>
+ <code>http://localhost:1080/
+ </code>
+ <p>Note that iCal will build the full principal path automatically.
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/osxIcal2.png" alt=""/>
+ </div>
+
+ <p>Click Add, choose Connect Anyway in warning dialog:</p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/osxIcal3.png" alt=""/>
+ </div>
+
+ <p>Proceed to
+ <a href="osxdirectorysetup.html">OSX directory setup</a>
+ to enable attendee completion.
+ </p>
+ </subsection>
+ </section>
+ </body>
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/osximapmailsetup.xml b/src/site/xdoc/osximapmailsetup.xml
new file mode 100644
index 0000000..333ef26
--- /dev/null
+++ b/src/site/xdoc/osximapmailsetup.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0"?>
+<document xmlns="http://maven.apache.org/XDOC/2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+
+ <properties>
+ <title>DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway - OSX IMAP Mail setup</title>
+ <author email="mguessan at free.fr">Mickael Guessant</author>
+ </properties>
+
+ <body>
+
+ <section name="OSX IMAP Mail setup">
+ <p>There are two ways to access Exchange emails through DavMail Gateway: the good old
+ and efficient POP protocol, limited to Inbox access and the IMAP protocol that enables
+ full message folder tree access. IMAP support in DavMail is more recent and less optimised
+ than POP. This page describes OSX IMAP Mail setup.
+ </p>
+
+ <subsection name="Create a new account">
+ <p>Launch Apple Mail, open Preferences and click the plus button to create a new
+ account. Enter you full name, email address and password:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/osxMailImap1.png" alt=""/>
+ </div>
+
+ <p>Choose IMAP account type, enter description, DavMail gateway address (localhost).
+ Check username and password:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/osxMailImap2.png" alt=""/>
+ </div>
+
+ <p>Click continue, if DavMail is not listening on the default IMAP port (143), you
+ will get the following error message, click Continue again to proceed:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/osxMailImap3.png" alt=""/>
+ </div>
+
+ <p>Leave SSL unchecked and click Continue:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/osxMailImap4.png" alt=""/>
+ </div>
+
+ <p>Enter Outgoing Mail Server (SMTP) description and DavMail gateway address (localhost).
+ Check Use Authentication and enter your Exchange username and password:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/osxMailImap5.png" alt=""/>
+ </div>
+
+ <p>Click continue, if DavMail is not listening on the default SMTP port (25), you
+ will get the following error message, click Continue again to proceed:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/osxMailImap6.png" alt=""/>
+ </div>
+
+ <p>Leave SSL unchecked and click Continue:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/osxMailImap7.png" alt=""/>
+ </div>
+
+ <p>Check summary and click Create:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/osxMailImap8.png" alt=""/>
+ </div>
+
+ </subsection>
+ <subsection name="Additional settings">
+ <p>Open the Mailbox Behaviors tab and uncheck Store sent messages on server, as Exchange already
+ stores each sent message in this folder:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/osxMailImap9.png" alt=""/>
+ </div>
+ <p>Under the advanced tab, enter actual DavMail IMAP port:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/osxMailImap10.png" alt=""/>
+ </div>
+ <p>Under the Account Information tab, choose Edit Server List in Outgoing Mail Server (SMTP) field:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/osxMailImap11.png" alt=""/>
+ </div>
+ <p>Open the advanced tab, check use custom port and enter actual DavMail SMTP port:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/osxMailImap12.png" alt=""/>
+ </div>
+ <p>You may also want to set the Add invitations to iCal parameter to Never:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/osxMailImap13.png" alt=""/>
+ </div>
+
+ <p>Proceed to
+ <a href="osxaddressbooksetup.html">OSX Address Book setup</a>
+ </p>
+
+ </subsection>
+ </section>
+ </body>
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/reviews.xml b/src/site/xdoc/reviews.xml
new file mode 100644
index 0000000..c7e113e
--- /dev/null
+++ b/src/site/xdoc/reviews.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0"?>
+<document xmlns="http://maven.apache.org/XDOC/2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+
+ <properties>
+ <title>DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway - Reviews</title>
+ <author email="mguessan at free.fr">Mickael Guessant</author>
+ </properties>
+
+ <body>
+
+ <section name="Reviews and additional documentation">
+ <p>In this page you find some independent reviews of DavMail and additional documentation on DavMail setup.
+ </p>
+
+ <ul>
+ <li>
+ <a href="http://olex.openlogic.com/wazi/2009/escaping-microsoft-exchange-via-davmail-fetchmail-postfix-courier-imap/">
+ Escaping Microsoft Exchange via Davmail + Fetchmail + Postfix + Courier IMAP
+ </a>
+ </li>
+ <li>
+ <a href="http://www.intrasection.com/pjmorr/2010/01/21/snow-leopard-and-exchange-2003/">
+ Getting mac mail, ical and address book to talk to Exchange 2003
+ </a>
+ </li>
+ <li>
+ <a href="http://www.oucs.ox.ac.uk/nexus/calendar/iCal/davmail.xml">
+ Nexus Calendaring using Davmail and iCal
+ </a>
+ </li>
+ <li>
+ <a href="http://wiki.rcs.manchester.ac.uk/community/ExchangeThunderbird">
+ Exchange Email/Calendars via DavMail with Thunderbird
+ </a>
+ </li>
+ <li>
+ <a href="http://www.ronakg.in/2010/07/thunderbird_lightning_davmail_ubuntu/">
+ How to setup Thunderbird + Lightning + DavMail on Ubuntu 10.04 (Lucid Lynx)
+ </a>
+ </li>
+ <li>
+ <a href="http://karuppuswamy.com/wordpress/2010/05/13/how-to-integrate-thunderbird-with-ms-exchange-to-replace-ms-outlook/">
+ How to use Thunderbird to send and receive Exchange email replacing MS-Outlook
+ </a>
+ </li>
+ <li>
+ <a href="http://guycoen.wordpress.com/2010/12/09/outlook-2011-on-mac-and-exchange-2003/">
+ Outlook 2011 on Mac and Exchange 2003
+ </a>
+ </li>
+ <li>
+ <a href="http://techruminations.blogspot.com/2010/01/using-davmail-gateway-to-allow.html">
+ Using DavMail Gateway To Allow Thunderbird To Access Microsoft Exchange Server 2007 & Earlier
+ </a>
+ </li>
+ <li>
+ <a href="http://www.linux.com/learn/tutorials/265449-escaping-microsoft-exchange-via-davmail-fetchmail-postfix-courier-imap">
+ Escaping Microsoft Exchange via DavMail Fetchmail Postfix Courier-imap
+ </a>
+ </li>
+ <li>
+ <a href="http://guzaho.wordpress.com/2011/10/12/thunderbird-as-client-for-microsoft-exchange-2010-server/">
+ Thunderbird as client for Microsoft Exchange 2010 server
+ </a>
+ </li>
+ <li>
+ <a href="http://roger-almeida.blogspot.fr/2012/04/thunderbird-with-microsoft-owa-in.html">
+ Thunderbird with Microsoft OWA in Ubuntu 11.10
+ </a>
+ </li>
+ <li>
+ <a href="http://www.docgreen.fr/?p=4">
+ Utiliser Thunderbird avec un serveur Microsoft Exchange 2007 (French)
+ </a>
+ </li>
+ </ul>
+ </section>
+ </body>
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/roadmap.xml b/src/site/xdoc/roadmap.xml
new file mode 100644
index 0000000..0bb78ad
--- /dev/null
+++ b/src/site/xdoc/roadmap.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0"?>
+<document xmlns="http://maven.apache.org/XDOC/2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+
+ <properties>
+ <title>DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway - RoadMap</title>
+ <author email="mguessan at free.fr">Mickael Guessant</author>
+ </properties>
+
+ <body>
+
+ <section name="DavMail RoadMap">
+ <p>DavMail features are now almost complete, yet there remains a lot of place
+ for improvement. The following section lists the expected new features
+ and enhancements in next DavMail versions.
+ </p>
+ <subsection name="3.9.9">
+ <p>
+ <strong>Next minor release</strong>
+ </p>
+ <ul>
+ <li>Improve IMAP sync performance</li>
+ </ul>
+ </subsection>
+ <subsection name="4.0">
+ <p>
+ <strong>Next major version</strong>
+ </p>
+ <ul>
+ <li>Implement split and kerberos authentication</li>
+ <li>Implement optional ICS Todo/Task conversion to Outlook tasks: Done</li>
+ <li>Exchange 2010 support through EWS: Done</li>
+ </ul>
+ </subsection>
+ <subsection name="4.0.1">
+ <p>
+ <strong>Next minor version</strong>
+ </p>
+ <ul>
+ <li>Switch from registry to link on windows</li>
+ <li>Test windows install on seven without admin rights</li>
+ </ul>
+ </subsection>
+ <subsection name="4.x">
+ <p>
+ <strong>Future version</strong>
+ </p>
+ <ul>
+ <li>Implement Caldav attachments</li>
+ <li>Test the RPM package</li>
+ <li>Switch wrappers to WinRun4J</li>
+ <li>Implement ActiveSync backend</li>
+ <li>Implement EWS frontend for Exchange 2003 backend and Outlook 2011</li>
+ <li>Implement custom authenticator interface to handle specific authentications
+ (e.g. token based)
+ </li>
+ </ul>
+ </subsection>
+ </section>
+ </body>
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/serversetup.xml b/src/site/xdoc/serversetup.xml
new file mode 100644
index 0000000..9f25547
--- /dev/null
+++ b/src/site/xdoc/serversetup.xml
@@ -0,0 +1,124 @@
+<?xml version="1.0"?>
+<document xmlns="http://maven.apache.org/XDOC/2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+
+ <properties>
+ <title>DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway - Server setup</title>
+ <author email="mguessan at free.fr">Mickael Guessant</author>
+ </properties>
+
+ <body>
+
+ <section name="DavMail Setup as a standalone server">
+ <p>Prerequisite : Sun JRE 5, 6 or 7 or OpenJDK 6 or 7.
+ </p>
+
+ <p>Davmail Gateway can run in server mode as a gateway between the mail
+ client and the Outlook Web Access (Exchange) server.
+ In server mode Davmail can run on any Java supported platform.
+ This mode was tested successfully with the Iphone and should work with
+ any phone with POP/IMAP/SMTP/LDAP/Caldav/Carddav client.
+ In this mode many users can share the same DavMail instance.
+ </p>
+
+ <p>Download the generic DavMail package from Sourceforge and uncompress it with
+ your favorite tool, e.g. on Linux: <code>unzip davmail-*.zip</code>.
+ </p>
+ <p>Prepare a davmail.properties file according to your local needs :
+ </p>
+ <source><![CDATA[
+davmail.url=http://exchangeServer/exchange/
+davmail.enableEws=false
+davmail.popPort=1110
+davmail.imapPort=1143
+davmail.smtpPort=1025
+davmail.caldavPort=1080
+davmail.ldapPort=1389
+davmail.keepDelay=30
+davmail.sentKeepDelay=90
+davmail.caldavPastDelay=90
+davmail.imapIdleDelay=
+davmail.useSystemProxies=false
+davmail.enableProxy=false
+davmail.proxyHost=
+davmail.proxyPort=
+davmail.proxyUser=
+davmail.proxyPassword=
+davmail.ssl.keystoreType=JKS
+davmail.ssl.keyPass=
+davmail.ssl.keystoreFile=
+davmail.ssl.keystorePass=
+davmail.smtpSaveInSent=true
+davmail.server=true
+davmail.server.certificate.hash=
+davmail.bindAddress=
+davmail.clientSoTimeout=
+davmail.allowRemote=true
+davmail.disableUpdateCheck=false
+log4j.rootLogger=WARN
+log4j.logger.davmail=DEBUG
+log4j.logger.org.apache.commons.httpclient=WARN
+log4j.logger.httpclient.wire=WARN
+davmail.logFilePath=/var/log/davmail.log]]>
+ </source>
+ <p>See
+ <a href="gettingstarted.html">Getting started</a>
+ for more information on
+ the options. Make sure davmail.server is set to true (no icon tray) and allow
+ remote connections: davmail.allowRemote=true.
+ </p>
+ <p>To disable a service, set an empty port value.</p>
+ <p>Launch Davmail with the following command:
+ <source>nohup davmail.sh davmail.properties &</source>.
+ </p>
+ <p>Then check messages:
+ <source>tail -f nohup.out</source>
+ </p>
+ </section>
+
+ <section name="Register DavMail windows service">
+ <p>A new WinRun4J davmailservice.exe wrapper is available in the windows
+ package.
+ </p>
+ <p>To register DavMail as a windows service, use:
+ <source>davmailservice --WinRun4J:RegisterService</source>
+ or
+ <source>sc create DavMail binPath= \path\to\davmailservice.exe type= own start= auto</source>
+ </p>
+ </section>
+
+ <section name="DavMail Setup as a JEE Web Application">
+ <p>Prerequisites : Sun JRE 5, 6 or 7 or OpenJDK 6 or 7 and any JEE compliant web container
+ </p>
+
+ <p>Davmail Gateway can now be deployed in any JEE application server using
+ the war package. In this mode, DavMail listener threads run inside the
+ application server and follow the web application lifecycle (start,
+ stop, deploy, undeploy). The following items describe Tomcat deployment,
+ details will vary according to the specific application server available.
+ </p>
+
+ <p>Download the war DavMail package from Sourceforge<code>davmail-*.war</code>,
+ and deploy it inside the application server. In Tomcat, this means copy the
+ war file to the webapps directory. If Tomcat is started and automatic
+ deployment enabled (this is the default configuration), the package is
+ automatically uncompressed and started.
+ </p>
+ <p>The davmail.properties configuration file is then available under
+ <code>davmail-*/WEB-INF/classes</code>.
+ </p>
+
+ <p>Note: DavMail does not use the standard Tomcat HTTP connector and uses the same listeners
+ in war and server modes. This means the HTTP port for Caldav url is specified in davmail.properties
+ </p>
+
+ <p>See above and
+ <a href="gettingstarted.html">Getting started</a>
+ for more information on
+ the options. Make sure davmail.server is set to true (no icon tray) and allow
+ remote connections: davmail.allowRemote=true.
+ </p>
+ </section>
+ </body>
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/sslsetup.xml b/src/site/xdoc/sslsetup.xml
new file mode 100644
index 0000000..fa555f4
--- /dev/null
+++ b/src/site/xdoc/sslsetup.xml
@@ -0,0 +1,116 @@
+<?xml version="1.0"?>
+<document xmlns="http://maven.apache.org/XDOC/2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+
+ <properties>
+ <title>DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway - SSL setup</title>
+ <author email="mguessan at free.fr">Mickael Guessant</author>
+ </properties>
+
+ <body>
+
+ <section name="SSL setup">
+ <subsection name="Server keystore (Client to DavMail)">
+ <p>SSL is not necessary when DavMail is used in workstation mode, as communication between clients and
+ DavMail remain local. However, in server (shared) mode e.g. with a smartphone connecting to DavMail
+ over the internet, you should make sure encryption is enabled.
+ </p>
+
+ <p>The simplest way to secure communication between mail/calendar clients and DavMail is to create a
+ self signed certificate:
+ </p>
+ <source>keytool -genkey -keyalg rsa -keysize 2048 -storepass password -keystore davmail.p12 -storetype
+ pkcs12 -validity 3650 -dname cn=davmailhostname.company.com,ou=davmail,o=sf,o=net
+ </source>
+
+ <p>Note to iPhone users: iOS does not support the default DSA algorithm, make sure you use an RSA key
+ pair
+ </p>
+
+ <p>Another note : do not use blank passwords, both keystore and key passwords must be set</p>
+
+ <p>If you have an official certificate in PEM form, convert it to PKCS12 with the following command:</p>
+ <source>openssl pkcs12 -export -in cert-davmail.pem -inkey privatekey-davmail.key -certfile
+ chain-davmail.pem -out davmail.p12
+ </source>
+
+ <p>Then add this keystore to DavMail settings:
+ </p>
+ <source><![CDATA[
+davmail.ssl.keystoreType=PKCS12
+davmail.ssl.keyPass=password
+davmail.ssl.keystoreFile=davmail.p12
+davmail.ssl.keystorePass=password]]></source>
+ <p>If your already have your keystore in JKS format, just set keystoreType to JKS in DavMail
+ settings. keystorePass is the password used to open the KeyStore, keyPass protects the private key
+ inside the KeyStore. With PKCS12, keyPass and keystorePass are often identical.
+ </p>
+ <p>Restart DavMail, all DavMail listeners will switch to secure mode: POP3S/IMAPS/SMTPS/HTTPS/LDAPS.
+ You will also need to enable SSL in client applications and manually accept the certificate as it's
+ not signed by a trusted Certification Authority.
+ </p>
+ </subsection>
+
+ <subsection name="DavMail to Exchange">
+ <p>
+ <strong>Custom certificate authority</strong>
+ </p>
+ <p>Most users rely on the interactive accept certificate dialog to handle non public certificate authorities.
+ However, this will not work with an Exchange server cluster with a different certificate on each server.
+ In this case, you need to update global Java truststore with the custom certificate authority:
+ </p>
+ <source>keytool -import -alias root -keystore /path/to/jre/lib/security/cacerts -trustcacerts -file rootca.crt -storepass changeit -noprompt</source>
+ <p>
+ <strong>Client certificate</strong>
+ </p>
+ <p>In most cases, using https in OWA url is enough to secure communication between DavMail and Exchange.
+ However, with Exchange servers setup to require mutual authentication, you will have to register
+ your client certificate in DavMail settings, either through PKCS11 (smartcard) or file certificate.
+ </p>
+ <p>To use a client certificate provided as a PKCS12 file, set the following keys in DavMail:</p>
+ <source><![CDATA[
+davmail.ssl.clientKeystoreType=PKCS12
+davmail.ssl.clientKeystoreFile=client.p12
+davmail.ssl.clientKeystorePass=password]]></source>
+ <p>For a smartcard, first make sure you PKCS11 module is correctly installed by testing mutual
+ authentication through a browser. Then set the following properties in DavMail:</p>
+ <source><![CDATA[
+davmail.ssl.clientKeystoreType=PKCS11
+davmail.ssl.pkcs11Library=/full/path/to/pkcs11Module
+davmail.ssl.pkcs11Config=]]></source>
+ <p>PKCS11 library is the full path to the PKCS11 module (.so on Unix, .dll on windows) or simple
+ library name if PATH (Windows) or LD_LIBRARY_PATH (Unix) already contains the full path. Add any
+ additional PKCS11 parameter in PKCS11 Config parameter, e.g. <code>slot = 2</code>.
+ </p>
+ <p>To adjust your settings, you can try to access the smartcard with java keytool. First create a file
+ named pkcs11.config with the following lines:</p>
+ <source><![CDATA[
+name = moduleName
+library = /path/to/pkcs11module]]></source>
+ <p>and list certificates with keytool:</p>
+ <source>keytool -keystore NONE -storetype PKCS11 -providerClass sun.security.pkcs11.SunPKCS11 -providerArg pkcs11.config -list -v</source>
+
+ <p>Sample pkcs11.config for NSS Soft token (Thunderbird/Firefox):</p>
+<source><![CDATA[
+name=NSS
+library=softokn3
+nssArgs="configdir='/path/to/firefox/profile' certPrefix='' keyPrefix='' secmod='secmod.db' flags=readOnly"
+slot = 2
+]]></source>
+
+ <p>Another one for Coolkey (see <a href="http://pkg-coolkey.alioth.debian.org/">Coolkey for Debian</a>
+ and <a href="http://www7320.nrlssc.navy.mil/pubs/2006/CommonAccessCardLinux.pdf">United States Department of Defense Common Access Cards</a>):</p>
+<source><![CDATA[
+name=CoolKey
+library=/usr/cac/lib/pkcs11/libcoolkeypk11.so
+]]></source>
+
+ <p>More details on java PKCS11 setup in
+ <a href="http://java.sun.com/javase/6/docs/technotes/guides/security/p11guide.html">Sun PKCS11 guide</a>
+ </p>
+
+ </subsection>
+ </section>
+ </body>
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/thunderbirdcalendarsetup.xml b/src/site/xdoc/thunderbirdcalendarsetup.xml
new file mode 100644
index 0000000..e1f6eeb
--- /dev/null
+++ b/src/site/xdoc/thunderbirdcalendarsetup.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0"?>
+<document xmlns="http://maven.apache.org/XDOC/2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+
+ <properties>
+ <title>DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway - Thunderbird calendar setup</title>
+ <author email="mguessan at free.fr">Mickael Guessant</author>
+ </properties>
+
+ <body>
+
+ <section name="Thunderbird calendar setup">
+ <p>DavMail Calendar support is now available, tested with Thunderbird/Lightning and Apple iCal.
+ This should also work with other Caldav clients.
+ </p>
+
+ <subsection name="Create a new network calendar">
+ <p>Obviously, you need to install the Thunderbird Lightning extension. You can
+ then create a new network calendar:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/thunderbirdCalendar1.png" alt=""/>
+ </div>
+
+ <p>Choose CalDav format and specify location (adjust port to your settings):</p>
+ <code>http://localhost:1080/users/mail@company.com/calendar
+ </code>
+ <p>Replace mail at company.com with actual user email address.
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/thunderbirdCalendar2.png" alt=""/>
+ </div>
+
+ <p>Choose a name, color and set the associated email address (as set in mail setup).
+ </p>
+ <p>Note that Caldav notifications will not work if this email address field is empty.</p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/thunderbirdCalendar3.png" alt=""/>
+ </div>
+
+
+ <p>Provide your credentials, login is Active Directory account name, i.e. domain\account
+ or email address with Exchange 2007
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/thunderbirdCalendar4.png" alt=""/>
+ </div>
+
+
+ <p>Finish:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/thunderbirdCalendar5.png" alt=""/>
+ </div>
+
+ <p>You may want to enable the (experimental) Lightning calendar cache feature to avoid
+ the possibly long calendar load task delay on Thunderbird startup: double click
+ on the calendar in Lightning left pane and activate cache. Restart Thunderbird.
+ </p>
+
+ <p>Proceed to
+ <a href="thunderbirddirectorysetup.html">Thunderbird directory setup</a>
+ </p>
+ </subsection>
+ </section>
+ </body>
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/thunderbirdcarddavsetup.xml b/src/site/xdoc/thunderbirdcarddavsetup.xml
new file mode 100644
index 0000000..f1ee8d6
--- /dev/null
+++ b/src/site/xdoc/thunderbirdcarddavsetup.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0"?>
+<document xmlns="http://maven.apache.org/XDOC/2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+
+ <properties>
+ <title>DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway - Thunderbird Carddav setup</title>
+ <author email="mguessan at free.fr">Mickael Guessant</author>
+ </properties>
+
+ <body>
+
+ <section name="Thunderbird Carddav setup">
+ <p>Thunderbird does not support Carddav natively, you will need to download and install the SOGo Connector
+ plugin from
+ <a href="http://www.sogo.nu/fr/downloads/frontends.html">SOGo Connector Thunderbird extension</a>.
+ </p>
+
+ <subsection name="Create a new remote address book">
+ <p>After plugin installation, restart Thunderbird, open address book and choose
+ File / New / Remote Address Book:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/thunderbirdCarddav1.png" alt=""/>
+ </div>
+
+ <p>Choose a name and enter contact folder location (adjust port to your settings):</p>
+ <code>http://localhost:1080/users/mail@company.com/contacts
+ </code>
+ <p>Replace mail at company.com with actual user email address.
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/thunderbirdCarddav2.png" alt=""/>
+ </div>
+
+ <p>Press OK and launch synchronisation with the toolbar
+ <code>Synchronize</code>
+ button.
+ </p>
+
+ <p>Provide your credentials, login is Active Directory account name, i.e. domain\account
+ or email address with Exchange 2007
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/thunderbirdCarddav3.png" alt=""/>
+ </div>
+ </subsection>
+ </section>
+ </body>
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/thunderbirddirectorysetup.xml b/src/site/xdoc/thunderbirddirectorysetup.xml
new file mode 100644
index 0000000..34eb472
--- /dev/null
+++ b/src/site/xdoc/thunderbirddirectorysetup.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0"?>
+<document xmlns="http://maven.apache.org/XDOC/2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+
+ <properties>
+ <title>DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway - Thunderbird directory setup</title>
+ <author email="mguessan at free.fr">Mickael Guessant</author>
+ </properties>
+
+ <body>
+
+ <section name="Thunderbird directory setup">
+ <p>DavMail Directory support is now available to access Exchange address book through LDAP.
+ </p>
+
+ <subsection name="Create a new LDAP directory">
+ <p>Open Thunderbird address book and choose File/New/LDAP Directory:</p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/thunderbirdDirectory1.png" alt=""/>
+ </div>
+
+ <p>Choose a name, host is always localhost, LDAP base context <code>ou=people</code>,
+ adjust port to your settings and set user to your Active Directory account name,
+ i.e. domain\account. Leave other options to default values.
+ </p>
+ <p>You may also want to use this directory for mail completion in compose window :
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/thunderbirdDirectory2.png" alt=""/>
+ </div>
+
+ <p>Proceed to
+ <a href="thunderbirdcarddavsetup.html">Thunderbird Carddav setup</a>
+ </p>
+ </subsection>
+ </section>
+ </body>
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/thunderbirdimapmailsetup.xml b/src/site/xdoc/thunderbirdimapmailsetup.xml
new file mode 100644
index 0000000..f911ce5
--- /dev/null
+++ b/src/site/xdoc/thunderbirdimapmailsetup.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0"?>
+<document xmlns="http://maven.apache.org/XDOC/2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+
+ <properties>
+ <title>DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway - Thunderbird mail setup</title>
+ <author email="mguessan at free.fr">Mickael Guessant</author>
+ </properties>
+
+ <body>
+
+ <section name="IMAP Thunderbird mail setup">
+ <p>There are two ways to access Exchange emails through DavMail Gateway: the good old
+ and efficient POP protocol, limited to Inbox access and the IMAP protocol that enables
+ full message folder tree access. DavMail IMAP listener has a higher memory footprint.
+ </p>
+
+ <p>This page describes IMAP setup, if you don't need multiple folders access, proceed to
+ <a href="thunderbirdmailsetup.html">
+ POP Thunderbird mail setup
+ </a>
+ instead.
+ </p>
+
+ <p>DavMail can be used with any IMAP/SMTP client by adjusting the following description designed for
+ Thunderbird. DavMail IMAP support is tested with Thunderbird, Outlook and Apple Mail.
+ </p>
+
+ <subsection name="Create a new account">
+ <p>Choose <code>Add Mail Account...</code> under Account settings and enter name, email address
+ and password:</p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/thunderbirdAccount1.png" alt=""/>
+ </div>
+ <p>Click <code>Continue</code> and <code>Manual config</code> without waiting for automatic config:</p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/thunderbirdAccount2.png" alt=""/>
+ </div>
+
+ <p>Incoming server type is IMAP, server hostname is localhost, default port is 1143, no SSL and normal
+ password authentication. Outgoing server is localhost, default port is 1025, no SSL and normal
+ password authentication. Set your username (windows account name or email address) and click
+ <code>Re-test</code> to validate account settings, then <code>Create Account</code>:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/thunderbirdAccountPop3.png" alt=""/>
+ </div>
+
+
+ <p>Without SSL you will get the following warning, check box and confirm account creation:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/thunderbirdAccount4.png" alt=""/>
+ </div>
+ <p>Note: as communication between Thunderbird and DavMail is local, cleartext mode is not an issue,
+ except on shared machines (e.g. Terminal server).</p>
+
+ <p>Proceed to
+ <a href="thunderbirdcalendarsetup.html">Thunderbird calendar setup</a>
+ </p>
+ </subsection>
+ </section>
+ </body>
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/thunderbirdmailsetup.xml b/src/site/xdoc/thunderbirdmailsetup.xml
new file mode 100644
index 0000000..243b7d2
--- /dev/null
+++ b/src/site/xdoc/thunderbirdmailsetup.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0"?>
+<document xmlns="http://maven.apache.org/XDOC/2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+
+ <properties>
+ <title>DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway - Thunderbird mail setup</title>
+ <author email="mguessan at free.fr">Mickael Guessant</author>
+ </properties>
+
+ <body>
+
+ <section name="POP Thunderbird mail setup">
+ <p>There are two ways to access Exchange emails through DavMail Gateway: the good old
+ and efficient POP protocol, limited to Inbox access and the IMAP protocol that enables
+ full message folder tree access. IMAP support in DavMail is more recent and less optimised
+ than POP.
+ </p>
+
+ <p>This page describes POP3 setup, if you need multiple folders access, proceed to
+ <a href="thunderbirdimapmailsetup.html">
+ IMAP Thunderbird mail setup
+ </a>
+ instead.
+ </p>
+
+ <p>DavMail can be used with any POP3/SMTP client by adjusting the following description designed for
+ Thunderbird
+ </p>
+
+ <subsection name="Create a new account">
+ <p>Choose <code>Add Mail Account...</code> under Account settings and enter name, email address
+ and password:</p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/thunderbirdAccount1.png" alt=""/>
+ </div>
+
+ <p>Click <code>Continue</code> and <code>Manual config</code> without waiting for automatic config:</p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/thunderbirdAccount2.png" alt=""/>
+ </div>
+
+ <p>Incoming server type is POP, server hostname is localhost, default port is 1110, no SSL and normal
+ password authentication. Outgoing server is localhost, default port is 1025, no SSL and normal
+ password authentication. Set your username (windows account name or email address) and click
+ <code>Re-test</code> to validate account settings, then <code>Create Account</code>:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/thunderbirdAccount3.png" alt=""/>
+ </div>
+
+
+ <p>Without SSL you will get the following warning, check box and confirm account creation:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/thunderbirdAccount4.png" alt=""/>
+ </div>
+ <p>Note: as communication between Thunderbird and DavMail is local, cleartext mode is not an issue,
+ except on shared machines (e.g. Terminal server).</p>
+
+ <p>Proceed to
+ <a href="thunderbirdcalendarsetup.html">Thunderbird calendar setup</a>
+ </p>
+ </subsection>
+ </section>
+ </body>
+</document>
\ No newline at end of file
diff --git a/src/site/xdoc/windowssetup.xml b/src/site/xdoc/windowssetup.xml
new file mode 100644
index 0000000..3395689
--- /dev/null
+++ b/src/site/xdoc/windowssetup.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0"?>
+<document xmlns="http://maven.apache.org/XDOC/2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
+
+ <properties>
+ <title>DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway - Windows setup</title>
+ <author email="mguessan at free.fr">Mickael Guessant</author>
+ </properties>
+
+ <body>
+
+ <section name="DavMail Setup on windows">
+ <p>Prerequisite : Sun JRE 5, 6 or 7. Tray icon is now implemented with SWT and compatible with
+ Java 5. You may use DavMail with an older version, but the gateway will run as a
+ command line application.
+ </p>
+
+ <p>If Java is not available, DavMail Jsmooth launcher will trigger java download and
+ setup.
+ </p>
+
+ <p>DavMail setup is straightforward, just follow the setup wizard.
+ </p>
+ <p>Choose language:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/setup1.png" alt=""/>
+ </div>
+ <p>Click Next:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/setup2.png" alt=""/>
+ </div>
+ <p>Accept GPL licence:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/setup3.png" alt=""/>
+ </div>
+ <p>Choose path:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/setup4.png" alt=""/>
+ </div>
+ <p>Launch installation and start gateway:
+ </p>
+ <div style="width: 100%;text-align: center">
+ <img src="images/setup5.png" alt=""/>
+ </div>
+ <p>Adjust DavMail settings :
+ <a href="gettingstarted.html">Getting started</a>
+ </p>
+ </section>
+
+ </body>
+</document>
\ No newline at end of file
diff --git a/src/test/davmail/AbstractDavMailTestCase.java b/src/test/davmail/AbstractDavMailTestCase.java
new file mode 100644
index 0000000..e69a446
--- /dev/null
+++ b/src/test/davmail/AbstractDavMailTestCase.java
@@ -0,0 +1,91 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail;
+
+import davmail.exchange.ExchangeSession;
+import davmail.http.DavGatewaySSLProtocolSocketFactory;
+import junit.framework.TestCase;
+
+import javax.mail.MessagingException;
+import javax.mail.Session;
+import javax.mail.internet.MimeMessage;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * DavMail generic test case.
+ * Loads DavMail settings
+ */
+public class AbstractDavMailTestCase extends TestCase {
+ protected static boolean loaded;
+ protected static String url;
+ protected static String certificateHash;
+ protected static String username;
+ protected static String password;
+ protected static ExchangeSession session;
+
+ @Override
+ public void setUp() throws IOException {
+ if (!loaded) {
+ loaded = true;
+
+ if (url == null) {
+ // try to load settings from current folder davmail.properties
+ File file = new File("davmail.properties");
+ if (file.exists()) {
+ Settings.setConfigFilePath("davmail.properties");
+ }
+ // Load current settings
+ Settings.load();
+ } else {
+ Settings.setDefaultSettings();
+ Settings.setProperty("davmail.url", url);
+ Settings.setProperty("davmail.server.certificate.hash", certificateHash);
+ Settings.setProperty("davmail.username", username);
+ Settings.setProperty("davmail.password", password);
+ }
+
+
+ DavGatewaySSLProtocolSocketFactory.register();
+ // force server mode
+ Settings.setProperty("davmail.server", "true");
+
+ // enable WIRE debug log
+ //Settings.setLoggingLevel("httpclient.wire", Level.DEBUG);
+ // enable EWS support
+ //Settings.setProperty("davmail.enableEws", "false");
+
+ }
+ }
+
+ protected MimeMessage createMimeMessage() throws MessagingException {
+ MimeMessage mimeMessage = new MimeMessage((Session) null);
+ mimeMessage.addHeader("To", "test at test.local");
+ mimeMessage.setText("Test message\n");
+ mimeMessage.setSubject("Test subject");
+ return mimeMessage;
+ }
+
+ protected byte[] getMimeBody(MimeMessage mimeMessage) throws IOException, MessagingException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ mimeMessage.writeTo(baos);
+ return baos.toByteArray();
+ }
+}
diff --git a/src/test/davmail/caldav/TestCaldav.java b/src/test/davmail/caldav/TestCaldav.java
new file mode 100644
index 0000000..a0a1846
--- /dev/null
+++ b/src/test/davmail/caldav/TestCaldav.java
@@ -0,0 +1,393 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.caldav;
+
+import davmail.AbstractDavMailTestCase;
+import davmail.DavGateway;
+import davmail.Settings;
+import davmail.exchange.ExchangeSession;
+import davmail.exchange.ExchangeSessionFactory;
+import davmail.util.StringUtil;
+import org.apache.commons.httpclient.*;
+import org.apache.commons.httpclient.auth.AuthScope;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.apache.commons.httpclient.methods.RequestEntity;
+import org.apache.commons.httpclient.params.HttpMethodParams;
+import org.apache.commons.httpclient.util.URIUtil;
+import org.apache.jackrabbit.webdav.DavException;
+import org.apache.jackrabbit.webdav.MultiStatus;
+import org.apache.jackrabbit.webdav.MultiStatusResponse;
+import org.apache.jackrabbit.webdav.client.methods.DavMethodBase;
+import org.apache.jackrabbit.webdav.client.methods.MoveMethod;
+import org.apache.jackrabbit.webdav.client.methods.PropFindMethod;
+import org.apache.jackrabbit.webdav.property.DavPropertyName;
+import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
+import org.apache.jackrabbit.webdav.xml.Namespace;
+import org.apache.log4j.Level;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Test Caldav listener.
+ */
+public class TestCaldav extends AbstractDavMailTestCase {
+
+ class SearchReportMethod extends DavMethodBase {
+ SearchReportMethod(String path, String stringContent) throws UnsupportedEncodingException {
+ this(path, stringContent.getBytes("UTF-8"));
+ }
+
+ SearchReportMethod(String path, final byte[] content) {
+ super(path);
+ setRequestEntity(new RequestEntity() {
+
+ public boolean isRepeatable() {
+ return true;
+ }
+
+ public void writeRequest(OutputStream outputStream) throws IOException {
+ outputStream.write(content);
+ }
+
+ public long getContentLength() {
+ return content.length;
+ }
+
+ public String getContentType() {
+ return "text/xml;charset=UTF-8";
+ }
+ });
+ }
+
+ @Override
+ public String getName() {
+ return "REPORT";
+ }
+
+ @Override
+ protected boolean isSuccess(int statusCode) {
+ return statusCode == HttpStatus.SC_MULTI_STATUS;
+ }
+ }
+
+ HttpClient httpClient;
+
+ @Override
+ public void setUp() throws IOException {
+ super.setUp();
+ if (httpClient == null) {
+ // start gateway
+ DavGateway.start();
+ httpClient = new HttpClient();
+ HostConfiguration hostConfig = httpClient.getHostConfiguration();
+ URI httpURI = new URI("http://localhost:" + Settings.getProperty("davmail.caldavPort"), true);
+ hostConfig.setHost(httpURI);
+ AuthScope authScope = new AuthScope(null, -1);
+ httpClient.getState().setCredentials(authScope, new NTCredentials(Settings.getProperty("davmail.username"), Settings.getProperty("davmail.password"), "", ""));
+ }
+ if (session == null) {
+ session = ExchangeSessionFactory.getInstance(Settings.getProperty("davmail.username"), Settings.getProperty("davmail.password"));
+ }
+ }
+
+ public void testGetRoot() throws IOException {
+ GetMethod method = new GetMethod("/");
+ httpClient.executeMethod(method);
+ assertEquals(HttpStatus.SC_OK, method.getStatusCode());
+ }
+
+ public void testGetUserRoot() throws IOException {
+ GetMethod method = new GetMethod("/users/" + session.getEmail() + '/');
+ httpClient.executeMethod(method);
+ assertEquals(HttpStatus.SC_OK, method.getStatusCode());
+ }
+
+ public void testGetCalendar() throws IOException {
+ GetMethod method = new GetMethod("/users/" + session.getEmail() + "/calendar/");
+ httpClient.executeMethod(method);
+ assertEquals(HttpStatus.SC_OK, method.getStatusCode());
+ }
+
+ public void testGetInbox() throws IOException {
+ GetMethod method = new GetMethod("/users/" + session.getEmail() + "/inbox/");
+ httpClient.executeMethod(method);
+ assertEquals(HttpStatus.SC_OK, method.getStatusCode());
+ }
+
+ public void testGetContacts() throws IOException {
+ GetMethod method = new GetMethod("/users/" + session.getEmail() + "/contacts/");
+ httpClient.executeMethod(method);
+ assertEquals(HttpStatus.SC_OK, method.getStatusCode());
+ }
+
+ public void testPropfindCalendar() throws IOException {
+ Settings.setLoggingLevel("httpclient.wire", Level.DEBUG);
+ PropFindMethod method = new PropFindMethod("/users/" + session.getEmail() + "/calendar/", null, 1);
+ httpClient.executeMethod(method);
+ assertEquals(HttpStatus.SC_MULTI_STATUS, method.getStatusCode());
+ }
+
+
+ public void testGetOtherUserCalendar() throws IOException {
+ Settings.setLoggingLevel("httpclient.wire", Level.DEBUG);
+ PropFindMethod method = new PropFindMethod("/principals/users/" + Settings.getProperty("davmail.to") + "/calendar/");
+ httpClient.executeMethod(method);
+ assertEquals(HttpStatus.SC_OK, method.getStatusCode());
+ }
+
+ public void testReportCalendar() throws IOException, DavException {
+ SimpleDateFormat formatter = ExchangeSession.getZuluDateFormat();
+ Calendar cal = Calendar.getInstance();
+ Date end = cal.getTime();
+ cal.add(Calendar.MONTH, -1);
+ Date start = cal.getTime();
+
+ StringBuilder buffer = new StringBuilder();
+ buffer.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
+ buffer.append("<C:calendar-query xmlns:C=\"urn:ietf:params:xml:ns:caldav\" xmlns:D=\"DAV:\">");
+ buffer.append("<D:prop>");
+ buffer.append("<C:calendar-data/>");
+ buffer.append("</D:prop>");
+ buffer.append("<C:comp-filter name=\"VCALENDAR\">");
+ buffer.append("<C:comp-filter name=\"VEVENT\">");
+ buffer.append("<C:time-range start=\"").append(formatter.format(start)).append("\" end=\"").append(formatter.format(end)).append("\"/>");
+ //buffer.append("<C:time-range start=\"").append(formatter.format(start)).append("\"/>");
+ buffer.append("</C:comp-filter>");
+ buffer.append("</C:comp-filter>");
+ buffer.append("<C:filter>");
+ buffer.append("</C:filter>");
+ buffer.append("</C:calendar-query>");
+ SearchReportMethod method = new SearchReportMethod("/users/" + session.getEmail() + "/calendar/", buffer.toString());
+ httpClient.executeMethod(method);
+ assertEquals(HttpStatus.SC_MULTI_STATUS, method.getStatusCode());
+ MultiStatus multiStatus = method.getResponseBodyAsMultiStatus();
+ MultiStatusResponse[] responses = multiStatus.getResponses();
+ ExchangeSession.Condition dateCondition = session.and(
+ session.gt("dtstart", session.formatSearchDate(start)),
+ session.lt("dtend", session.formatSearchDate(end))
+ );
+ List<ExchangeSession.Event> events = session.searchEvents("/users/" + session.getEmail() + "/calendar/",
+ session.or(session.isEqualTo("instancetype", 1),
+ session.and(session.isEqualTo("instancetype", 0), dateCondition))
+
+ );
+
+ assertEquals(events.size(), responses.length);
+ }
+
+ public void testReportInbox() throws IOException, DavException {
+
+ StringBuilder buffer = new StringBuilder();
+ buffer.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
+ buffer.append("<C:calendar-query xmlns:C=\"urn:ietf:params:xml:ns:caldav\" xmlns:D=\"DAV:\">");
+ buffer.append("<D:prop>");
+ buffer.append("<C:calendar-data/>");
+ buffer.append("</D:prop>");
+ buffer.append("<C:filter>");
+ buffer.append("</C:filter>");
+ buffer.append("</C:calendar-query>");
+ SearchReportMethod method = new SearchReportMethod("/users/" + session.getEmail() + "/inbox/", buffer.toString());
+ httpClient.executeMethod(method);
+ assertEquals(HttpStatus.SC_MULTI_STATUS, method.getStatusCode());
+ MultiStatus multiStatus = method.getResponseBodyAsMultiStatus();
+ MultiStatusResponse[] responses = multiStatus.getResponses();
+ /*List<ExchangeSession.Event> events = session.searchEvents("/users/" + session.getEmail() + "/calendar/",
+ session.or(session.isEqualTo("instancetype", 1),
+ session.and(session.isEqualTo("instancetype", 0), dateCondition))
+
+ );*/
+
+ //assertEquals(events.size(), responses.length);
+ }
+
+ public void testReportTasks() throws IOException, DavException {
+ StringBuilder buffer = new StringBuilder();
+ buffer.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
+ buffer.append("<C:calendar-query xmlns:C=\"urn:ietf:params:xml:ns:caldav\" xmlns:D=\"DAV:\">");
+ buffer.append("<D:prop>");
+ buffer.append("<C:calendar-data/>");
+ buffer.append("</D:prop>");
+ buffer.append("<C:comp-filter name=\"VCALENDAR\">");
+ buffer.append("<C:comp-filter name=\"VTODO\"/>");
+ buffer.append("</C:comp-filter>");
+ buffer.append("<C:filter>");
+ buffer.append("</C:filter>");
+ buffer.append("</C:calendar-query>");
+ SearchReportMethod method = new SearchReportMethod("/users/" + session.getEmail() + "/calendar/", buffer.toString());
+ httpClient.executeMethod(method);
+ assertEquals(HttpStatus.SC_MULTI_STATUS, method.getStatusCode());
+ MultiStatus multiStatus = method.getResponseBodyAsMultiStatus();
+ MultiStatusResponse[] responses = multiStatus.getResponses();
+ }
+
+ public void testReportEventsOnly() throws IOException, DavException {
+ StringBuilder buffer = new StringBuilder();
+ buffer.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
+ buffer.append("<C:calendar-query xmlns:C=\"urn:ietf:params:xml:ns:caldav\" xmlns:D=\"DAV:\">");
+ buffer.append("<D:prop>");
+ buffer.append("<C:calendar-data/>");
+ buffer.append("</D:prop>");
+ buffer.append("<C:comp-filter name=\"VCALENDAR\">");
+ buffer.append("<C:comp-filter name=\"VEVENT\"/>");
+ buffer.append("</C:comp-filter>");
+ buffer.append("<C:filter>");
+ buffer.append("</C:filter>");
+ buffer.append("</C:calendar-query>");
+ SearchReportMethod method = new SearchReportMethod("/users/" + session.getEmail() + "/calendar/", buffer.toString());
+ httpClient.executeMethod(method);
+ assertEquals(HttpStatus.SC_MULTI_STATUS, method.getStatusCode());
+ MultiStatus multiStatus = method.getResponseBodyAsMultiStatus();
+ MultiStatusResponse[] responses = multiStatus.getResponses();
+ }
+
+ public void testCreateCalendar() throws IOException {
+ String folderName = "test & accentué";
+ String encodedFolderpath = URIUtil.encodePath("/users/" + session.getEmail() + "/calendar/" + folderName + '/');
+ // first delete calendar
+ session.deleteFolder("calendar/" + folderName);
+ String body =
+ "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
+ " <C:mkcalendar xmlns:D=\"DAV:\"\n" +
+ " xmlns:C=\"urn:ietf:params:xml:ns:caldav\">\n" +
+ " <D:set>\n" +
+ " <D:prop>\n" +
+ " <D:displayname>" + StringUtil.xmlEncode(folderName) + "</D:displayname>\n" +
+ " <C:calendar-description xml:lang=\"en\">Calendar description</C:calendar-description>\n" +
+ " <C:supported-calendar-component-set>\n" +
+ " <C:comp name=\"VEVENT\"/>\n" +
+ " </C:supported-calendar-component-set>\n" +
+ " </D:prop>\n" +
+ " </D:set>\n" +
+ " </C:mkcalendar>";
+
+ SearchReportMethod method = new SearchReportMethod(encodedFolderpath, body) {
+ @Override
+ public String getName() {
+ return "MKCALENDAR";
+ }
+ };
+ httpClient.executeMethod(method);
+ assertEquals(HttpStatus.SC_CREATED, method.getStatusCode());
+
+ GetMethod getMethod = new GetMethod(encodedFolderpath);
+ httpClient.executeMethod(getMethod);
+ assertEquals(HttpStatus.SC_OK, getMethod.getStatusCode());
+ }
+
+ public void testPropfindPrincipal() throws IOException, DavException {
+ //Settings.setLoggingLevel("httpclient.wire", Level.DEBUG);
+
+ DavPropertyNameSet davPropertyNameSet = new DavPropertyNameSet();
+ davPropertyNameSet.add(DavPropertyName.create("calendar-home-set", Namespace.getNamespace("urn:ietf:params:xml:ns:caldav")));
+ davPropertyNameSet.add(DavPropertyName.create("calendar-user-address-set", Namespace.getNamespace("urn:ietf:params:xml:ns:caldav")));
+ davPropertyNameSet.add(DavPropertyName.create("schedule-inbox-URL", Namespace.getNamespace("urn:ietf:params:xml:ns:caldav")));
+ davPropertyNameSet.add(DavPropertyName.create("schedule-outbox-URL", Namespace.getNamespace("urn:ietf:params:xml:ns:caldav")));
+ PropFindMethod method = new PropFindMethod("/principals/users/" + session.getEmail() + "/", davPropertyNameSet, 0);
+ httpClient.executeMethod(method);
+ assertEquals(HttpStatus.SC_MULTI_STATUS, method.getStatusCode());
+ MultiStatus multiStatus = method.getResponseBodyAsMultiStatus();
+ MultiStatusResponse[] responses = multiStatus.getResponses();
+ assertEquals(1, responses.length);
+ }
+
+ public void testRenameCalendar() throws IOException {
+ String folderName = "testcalendarfolder";
+ String encodedFolderpath = URIUtil.encodePath("/users/" + session.getEmail() + "/calendar/" + folderName + '/');
+ // first delete calendar
+ session.deleteFolder("calendar/" + folderName);
+ String body =
+ "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
+ " <C:mkcalendar xmlns:D=\"DAV:\"\n" +
+ " xmlns:C=\"urn:ietf:params:xml:ns:caldav\">\n" +
+ " <D:set>\n" +
+ " <D:prop>\n" +
+ " <D:displayname>" + StringUtil.xmlEncode(folderName) + "</D:displayname>\n" +
+ " <C:calendar-description xml:lang=\"en\">Calendar description</C:calendar-description>\n" +
+ " <C:supported-calendar-component-set>\n" +
+ " <C:comp name=\"VEVENT\"/>\n" +
+ " </C:supported-calendar-component-set>\n" +
+ " </D:prop>\n" +
+ " </D:set>\n" +
+ " </C:mkcalendar>";
+
+ SearchReportMethod method = new SearchReportMethod(encodedFolderpath, body) {
+ @Override
+ public String getName() {
+ return "MKCALENDAR";
+ }
+ };
+ httpClient.executeMethod(method);
+ assertEquals(HttpStatus.SC_CREATED, method.getStatusCode());
+ MoveMethod moveMethod = new MoveMethod(encodedFolderpath, "http://localhost:" + Settings.getProperty("davmail.caldavPort")+"/users/" + session.getEmail() + "/movedcalendarfolder", true);
+ httpClient.executeMethod(moveMethod);
+ }
+
+ public void testRenameMainCalendar() throws IOException {
+ MoveMethod moveMethod = new MoveMethod("/users/" + session.getEmail() + "/Calendrierzzz", "http://localhost:" + Settings.getProperty("davmail.caldavPort")+"/users/" + session.getEmail() + "/Calendrier", true);
+ httpClient.executeMethod(moveMethod);
+ }
+
+ public void testWellKnown() throws IOException, DavException {
+ DavPropertyNameSet davPropertyNameSet = new DavPropertyNameSet();
+ davPropertyNameSet.add(DavPropertyName.create("current-user-principal", Namespace.getNamespace("DAV:")));
+ davPropertyNameSet.add(DavPropertyName.create("principal-URL", Namespace.getNamespace("DAV:")));
+ davPropertyNameSet.add(DavPropertyName.create("resourcetype", Namespace.getNamespace("DAV:")));
+ PropFindMethod method = new PropFindMethod("/.well-known/caldav", davPropertyNameSet, 0);
+ httpClient.executeMethod(method);
+ assertEquals(HttpStatus.SC_MOVED_PERMANENTLY, method.getStatusCode());
+ }
+
+ public void testPrincipalUrl() throws IOException, DavException {
+ DavPropertyNameSet davPropertyNameSet = new DavPropertyNameSet();
+ davPropertyNameSet.add(DavPropertyName.create("principal-URL", Namespace.getNamespace("DAV:")));
+ PropFindMethod method = new PropFindMethod("/principals/users/"+session.getEmail(), davPropertyNameSet, 0);
+ httpClient.executeMethod(method);
+ method.getResponseBodyAsMultiStatus();
+ assertEquals(HttpStatus.SC_MULTI_STATUS, method.getStatusCode());
+ }
+
+ public void testPropfindRoot() throws IOException, DavException {
+ DavPropertyNameSet davPropertyNameSet = new DavPropertyNameSet();
+ davPropertyNameSet.add(DavPropertyName.create("current-user-principal", Namespace.getNamespace("DAV:")));
+ davPropertyNameSet.add(DavPropertyName.create("principal-URL", Namespace.getNamespace("DAV:")));
+ davPropertyNameSet.add(DavPropertyName.create("resourcetype", Namespace.getNamespace("DAV:")));
+ PropFindMethod method = new PropFindMethod("/", davPropertyNameSet, 0);
+ httpClient.executeMethod(method);
+ assertEquals(HttpStatus.SC_MULTI_STATUS, method.getStatusCode());
+ method.getResponseBodyAsMultiStatus();
+ }
+
+ public void testPropfindAddressBook() throws IOException, DavException {
+ DavPropertyNameSet davPropertyNameSet = new DavPropertyNameSet();
+ //davPropertyNameSet.add(DavPropertyName.create("getctag", Namespace.getNamespace("http://calendarserver.org/ns/")));
+ davPropertyNameSet.add(DavPropertyName.create("getetag", Namespace.getNamespace("DAV:")));
+ PropFindMethod method = new PropFindMethod("/users/" + session.getEmail()+"/addressbook/", davPropertyNameSet, 1);
+ httpClient.getParams().setParameter(HttpMethodParams.USER_AGENT, "Address%20Book/883 CFNetwork/454.12.4 Darwin/10.8.0 (i386) (MacBookPro3%2C1)");
+ httpClient.executeMethod(method);
+ assertEquals(HttpStatus.SC_MULTI_STATUS, method.getStatusCode());
+ method.getResponseBodyAsMultiStatus();
+ }
+
+}
diff --git a/src/test/davmail/exchange/AbstractExchangeSessionTestCase.java b/src/test/davmail/exchange/AbstractExchangeSessionTestCase.java
new file mode 100644
index 0000000..9c765f6
--- /dev/null
+++ b/src/test/davmail/exchange/AbstractExchangeSessionTestCase.java
@@ -0,0 +1,50 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange;
+
+import davmail.AbstractDavMailTestCase;
+import davmail.Settings;
+import davmail.exchange.dav.DavExchangeSession;
+import davmail.exchange.ews.EwsExchangeSession;
+
+import java.io.IOException;
+
+/**
+ * Exchange session test case.
+ * Open a session to default DavMail server as found in user davmail.properties,
+ * except if url is not null
+ */
+public class AbstractExchangeSessionTestCase extends AbstractDavMailTestCase {
+
+ @Override
+ public void setUp() throws IOException {
+ super.setUp();
+ if (session == null) {
+ // open session, get username and password from davmail.properties
+ // Note: those properties should *not* exist in normal production mode,
+ // they are not used by DavMail, just by this test case
+ if (Settings.getBooleanProperty("davmail.enableEws")) {
+ session = new EwsExchangeSession(Settings.getProperty("davmail.url"), Settings.getProperty("davmail.username"), Settings.getProperty("davmail.password"));
+ } else {
+ session = new DavExchangeSession(Settings.getProperty("davmail.url"), Settings.getProperty("davmail.username"), Settings.getProperty("davmail.password"));
+ }
+ }
+ }
+
+}
diff --git a/src/test/davmail/exchange/TestDoubleDotInputStream.java b/src/test/davmail/exchange/TestDoubleDotInputStream.java
new file mode 100644
index 0000000..04025a1
--- /dev/null
+++ b/src/test/davmail/exchange/TestDoubleDotInputStream.java
@@ -0,0 +1,107 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange;
+
+import junit.framework.TestCase;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+/**
+ * Test double dot input stream.
+ */
+public class TestDoubleDotInputStream extends TestCase {
+ static final String END_OF_STREAM = "\r\n.\r\n";
+
+ protected String doubleDotRead(String value) throws IOException {
+ DoubleDotInputStream doubleDotInputStream = new DoubleDotInputStream(new ByteArrayInputStream(value.getBytes()));
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ int b;
+ while ((b = doubleDotInputStream.read()) != -1) {
+ baos.write(b);
+ }
+ return new String(baos.toByteArray());
+ }
+
+ protected String doubleDotWrite(String value) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ DoubleDotOutputStream doubleDotOutputStream = new DoubleDotOutputStream(baos);
+ doubleDotOutputStream.write(value.getBytes());
+ doubleDotOutputStream.close();
+ return new String(baos.toByteArray());
+ }
+
+ public void testSimple() throws IOException {
+ String value = "simple test";
+ assertEquals(value, doubleDotRead(value + END_OF_STREAM));
+ }
+
+ public void testNoEof() throws IOException {
+ String value = "simple test";
+ assertEquals(value, doubleDotRead(value));
+ }
+
+ public void testMultiLine() throws IOException {
+ String value = "simple test\r\nsecond line";
+ assertEquals(value, doubleDotRead(value+END_OF_STREAM));
+ }
+
+ public void testDoubleDot() throws IOException {
+ String value = "simple test\r\n..\r\nsecond line";
+ assertEquals(value.replaceAll("\\.\\.", "."), doubleDotRead(value+END_OF_STREAM));
+ }
+
+ public void testDoubleDotEnd() throws IOException {
+ String value = "simple test\r\n..";
+ assertEquals(value.replaceAll("\\.\\.", "."), doubleDotRead(value+END_OF_STREAM));
+ assertEquals("..", doubleDotRead(".."+END_OF_STREAM));
+ }
+
+ public void testWriteCRLF() throws IOException {
+ String value = "simple test\r\n.\r\nsecond line";
+ assertEquals(value.replaceAll("\\.", "..")+END_OF_STREAM, doubleDotWrite(value));
+ }
+
+ public void testEndsWithCRLF() throws IOException {
+ String value = "simple test\r\n";
+ assertEquals("simple test"+END_OF_STREAM, doubleDotWrite(value));
+ }
+
+ public void testEndsWithLF() throws IOException {
+ String value = "simple test\n";
+ assertEquals("simple test\n"+END_OF_STREAM, doubleDotWrite(value));
+ }
+
+ public void testWriteOSXCR() throws IOException {
+ String value = "simple test\r.\rsecond line";
+ assertEquals(value.replaceAll("\\.", "..")+END_OF_STREAM, doubleDotWrite(value));
+ }
+
+ public void testWriteUnixLF() throws IOException {
+ String value = "simple test\n.\nsecond line";
+ assertEquals(value.replaceAll("\\.", "..")+END_OF_STREAM, doubleDotWrite(value));
+ }
+
+ public void testAnotherTest() throws IOException {
+ String value = "foo\r\n..bar";
+ assertEquals(value.replaceAll("\\.\\.", "."), doubleDotRead(value));
+ }
+
+}
diff --git a/src/test/davmail/exchange/TestExchangeAdapter.java b/src/test/davmail/exchange/TestExchangeAdapter.java
new file mode 100644
index 0000000..478d096
--- /dev/null
+++ b/src/test/davmail/exchange/TestExchangeAdapter.java
@@ -0,0 +1,122 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange;
+
+import davmail.Settings;
+import davmail.exchange.dav.DavExchangeSession;
+import davmail.exchange.ews.EwsExchangeSession;
+import davmail.http.DavGatewaySSLProtocolSocketFactory;
+import junit.framework.TestCase;
+import org.apache.log4j.Level;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Test Exchange adapter methods.
+ */
+public class TestExchangeAdapter extends TestCase {
+ ExchangeSession davSession;
+ ExchangeSession ewsSession;
+
+ @Override
+ public void setUp() throws IOException {
+ if (davSession == null) {
+ Settings.setConfigFilePath("davmail.properties");
+ Settings.load();
+ DavGatewaySSLProtocolSocketFactory.register();
+ davSession = new DavExchangeSession(Settings.getProperty("davmail.url"),
+ Settings.getProperty("davmail.username"), Settings.getProperty("davmail.password"));
+ ewsSession = new EwsExchangeSession(Settings.getProperty("davmail.url"),
+ Settings.getProperty("davmail.username"), Settings.getProperty("davmail.password"));
+ Settings.setLoggingLevel("httpclient.wire", Level.INFO);
+ }
+ }
+
+ public void assertEquals(ExchangeSession.Folder davFolder, ExchangeSession.Folder ewsFolder) {
+ assertNotNull(ewsFolder);
+ assertEquals(davFolder.folderPath, ewsFolder.folderPath);
+ assertEquals(davFolder.folderClass, ewsFolder.folderClass);
+ assertEquals(davFolder.hasChildren, ewsFolder.hasChildren);
+ assertEquals(davFolder.unreadCount, ewsFolder.unreadCount);
+
+ assertEquals(davFolder.isCalendar(), false);
+ assertEquals(ewsFolder.isCalendar(), false);
+
+ assertEquals(davFolder.isContact(), false);
+ assertEquals(ewsFolder.isContact(), false);
+
+ assertEquals(davFolder.noInferiors, false);
+ assertEquals(ewsFolder.noInferiors, false);
+
+ assertEquals(davFolder.getFlags(), ewsFolder.getFlags());
+ assertEquals(davFolder.etag.substring(0, ewsFolder.ctag.length()-1)+ 'Z', ewsFolder.etag);
+
+
+ assertNotNull(davFolder.ctag);
+ assertNotNull(ewsFolder.ctag);
+ // dav and ews ctags are still different: dav contentag has milliseconds info
+ assertEquals(davFolder.ctag.substring(0, ewsFolder.ctag.length()-1)+ 'Z', ewsFolder.ctag);
+
+ }
+
+ public void testGetInbox() throws IOException {
+ ExchangeSession.Folder davFolder = davSession.getFolder("INBOX");
+ ExchangeSession.Folder ewsFolder = ewsSession.getFolder("INBOX");
+ assertEquals(davFolder, ewsFolder);
+ }
+
+ public void testGetSubFolder() throws IOException {
+ ExchangeSession.Folder ewsFolder = ewsSession.getFolder("INBOX/bbbb");
+ }
+
+ public void testFindFolder() throws IOException {
+ List<ExchangeSession.Folder> davFolders = davSession.getSubFolders("", false);
+ Settings.setLoggingLevel("httpclient.wire", Level.DEBUG);
+ List<ExchangeSession.Folder> ewsFolders = ewsSession.getSubFolders("", false);
+ assertEquals(davFolders.size(), ewsFolders.size());
+ }
+
+ public void testFindPublicFolder() throws IOException {
+ List<ExchangeSession.Folder> davFolders = davSession.getSubFolders("/public", false);
+ Settings.setLoggingLevel("httpclient.wire", Level.DEBUG);
+ List<ExchangeSession.Folder> ewsFolders = ewsSession.getSubFolders("/public", false);
+ assertEquals(davFolders.size(), ewsFolders.size());
+ }
+
+ public void testFindFolders() throws IOException {
+ List<ExchangeSession.Folder> davFolders = davSession.getSubFolders("/public", null, true);
+ System.out.println(davFolders);
+ }
+
+ public void testSearchMessages() throws IOException {
+ ExchangeSession.MessageList messages = davSession.searchMessages("INBOX");
+ for (ExchangeSession.Message message:messages) {
+ System.out.println(message);
+ }
+ }
+
+ public void testSearchEvents() throws IOException {
+ List<ExchangeSession.Event> events = davSession.getAllEvents("calendar");
+ for (ExchangeSession.Event event:events) {
+ System.out.println(event);
+ }
+ }
+
+}
diff --git a/src/test/davmail/exchange/TestExchangeSession.java b/src/test/davmail/exchange/TestExchangeSession.java
new file mode 100644
index 0000000..2306830
--- /dev/null
+++ b/src/test/davmail/exchange/TestExchangeSession.java
@@ -0,0 +1,41 @@
+package davmail.exchange;
+
+import davmail.Settings;
+import davmail.http.DavGatewaySSLProtocolSocketFactory;
+
+/**
+ * Test Exchange session
+ */
+public class TestExchangeSession {
+
+ private TestExchangeSession() {
+ }
+
+ /**
+ * main method
+ * @param argv command line arg
+ */
+ public static void main(String[] argv) {
+ // register custom SSL Socket factory
+ int currentArg = 0;
+ Settings.setConfigFilePath(argv[currentArg++]);
+ Settings.load();
+
+ DavGatewaySSLProtocolSocketFactory.register();
+
+ ExchangeSession session;
+ // test auth
+ try {
+ ExchangeSessionFactory.checkConfig();
+ session = ExchangeSessionFactory.getInstance(argv[currentArg++], argv[currentArg]);
+
+ ExchangeSession.Folder folder = session.getFolder("INBOX");
+ folder.loadMessages();
+
+ //session.purgeOldestTrashAndSentMessages();
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/test/davmail/exchange/TestExchangeSessionCalendar.java b/src/test/davmail/exchange/TestExchangeSessionCalendar.java
new file mode 100644
index 0000000..f0ddf02
--- /dev/null
+++ b/src/test/davmail/exchange/TestExchangeSessionCalendar.java
@@ -0,0 +1,409 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange;
+
+import davmail.Settings;
+import davmail.exchange.ews.EwsExchangeSession;
+import davmail.exchange.ews.FolderQueryTraversal;
+import org.apache.log4j.Level;
+
+import javax.mail.MessagingException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+/**
+ * Test Exchange session calendar features .
+ */
+ at SuppressWarnings({"UseOfSystemOutOrSystemErr"})
+public class TestExchangeSessionCalendar extends AbstractExchangeSessionTestCase {
+
+ public void testGetVtimezone() {
+ VObject timezone = session.getVTimezone();
+ assertNotNull(timezone);
+ assertNotNull(timezone.getPropertyValue("TZID"));
+ }
+
+ public void testDumpVtimezones() throws IOException {
+ Properties properties = new Properties() {
+ @Override
+ public synchronized Enumeration<Object> keys() {
+ Enumeration keysEnumeration = super.keys();
+ TreeSet<String> sortedKeySet = new TreeSet<String>();
+ while (keysEnumeration.hasMoreElements()) {
+ sortedKeySet.add((String) keysEnumeration.nextElement());
+ }
+ final Iterator<String> sortedKeysIterator = sortedKeySet.iterator();
+ return new Enumeration<Object>() {
+
+ public boolean hasMoreElements() {
+ return sortedKeysIterator.hasNext();
+ }
+
+ public Object nextElement() {
+ return sortedKeysIterator.next();
+ }
+ };
+ }
+
+ };
+ for (int i = 1; i < 100; i++) {
+ Settings.setProperty("davmail.timezoneId", String.valueOf(i));
+ VObject timezone = session.getVTimezone();
+ if (timezone.getProperty("TZID") != null) {
+ properties.put(timezone.getPropertyValue("TZID").replaceAll("\\\\", ""), String.valueOf(i));
+ System.out.println(timezone.getPropertyValue("TZID") + '=' + i);
+ }
+ session.vTimezone = null;
+ }
+ FileOutputStream fileOutputStream = null;
+ try {
+ fileOutputStream = new FileOutputStream("timezoneids.properties");
+ properties.store(fileOutputStream, "Timezone ids");
+ } finally {
+ if (fileOutputStream != null) {
+ try {
+ fileOutputStream.close();
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+ }
+ }
+
+ public void testSearchCalendar() throws IOException {
+ List<ExchangeSession.Event> events = null;
+ try {
+ events = session.getAllEvents("/users/" + session.getEmail() + "/calendar");
+ for (ExchangeSession.Event event : events) {
+ System.out.println(event.getBody());
+ }
+ } catch (IOException e) {
+ System.out.println(e.getMessage());
+ throw e;
+ }
+ }
+
+ public void testReportCalendar() throws IOException {
+ List<ExchangeSession.Event> events = null;
+ try {
+ events = session.getAllEvents("/users/" + session.getEmail() + "/calendar");
+ for (ExchangeSession.Event event : events) {
+ System.out.println(event.subject);
+ ExchangeSession.Item item = session.getItem("/users/" + session.getEmail() + "/calendar", event.itemName);
+ System.out.println(item.getBody());
+ }
+ } catch (IOException e) {
+ System.out.println(e.getMessage());
+ throw e;
+ }
+ }
+
+ public void testGetFreeBusyData() throws IOException, MessagingException {
+ Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+ cal.set(Calendar.MONTH, 7);
+ cal.set(Calendar.DAY_OF_MONTH, 1);
+ cal.set(Calendar.HOUR_OF_DAY, 0);
+ cal.set(Calendar.MINUTE, 0);
+ cal.set(Calendar.SECOND, 0);
+ Date startDate = cal.getTime();
+ cal.set(Calendar.HOUR_OF_DAY, 23);
+ cal.set(Calendar.MINUTE, 59);
+ cal.set(Calendar.SECOND, 59);
+ Date endDate = cal.getTime();
+ SimpleDateFormat formatter = ExchangeSession.getExchangeZuluDateFormat();
+ // personal fbdata
+ String fbdata = session.getFreeBusyData(session.getEmail(), formatter.format(startDate),
+ formatter.format(endDate), 60);
+ assertNotNull(fbdata);
+ // other user data
+ fbdata = session.getFreeBusyData(Settings.getProperty("davmail.to"), formatter.format(startDate),
+ formatter.format(endDate), 60);
+ assertNotNull(fbdata);
+ // unknown user data
+ fbdata = session.getFreeBusyData("unknown at company.org", formatter.format(startDate),
+ formatter.format(endDate), 60);
+ assertNull(fbdata);
+ }
+
+ public void testCreateEvent() throws IOException {
+ String itemBody = "BEGIN:VCALENDAR\n" +
+ "PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\n" +
+ "VERSION:2.0\n" +
+ "BEGIN:VTIMEZONE\n" +
+ "TZID:Pacific Time (US & Canada)\\; Tijuana\n" +
+ "BEGIN:STANDARD\n" +
+ "DTSTART:16010101T020000\n" +
+ "TZOFFSETFROM:-0700\n" +
+ "TZOFFSETTO:-0800\n" +
+ "RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=11;BYDAY=1SU\n" +
+ "END:STANDARD\n" +
+ "BEGIN:DAYLIGHT\n" +
+ "DTSTART:16010101T020000\n" +
+ "TZOFFSETFROM:-0800\n" +
+ "TZOFFSETTO:-0700\n" +
+ "RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=3;BYDAY=2SU\n" +
+ "END:DAYLIGHT\n" +
+ "END:VTIMEZONE\n" +
+ "BEGIN:VEVENT\n" +
+ "CREATED:20100829T204658Z\n" +
+ "LAST-MODIFIED:20100829T204829Z\n" +
+ "DTSTAMP:20100829T204829Z\n" +
+ "UID:701b9d8f-ab64-4a7c-a75d-251cc8687cd9\n" +
+ "SUMMARY:testzz\n" +
+ "DTSTART;TZID=\"Pacific Time (US & Canada); Tijuana\":20100830T230000\n" +
+ "DTEND;TZID=\"Pacific Time (US & Canada); Tijuana\":20100831T000000\n" +
+ "X-MICROSOFT-CDO-ALLDAYEVENT:FALSE\n" +
+ "X-MICROSOFT-CDO-BUSYSTATUS:BUSY\n" +
+ "TRANSP:OPAQUE\n" +
+ "X-MOZ-GENERATION:1\n" +
+ "END:VEVENT\n" +
+ "END:VCALENDAR";
+ String itemName = UUID.randomUUID().toString() + ".EML";
+ session.createOrUpdateItem("calendar", itemName, itemBody, null, null);
+ }
+
+ public void testGetEvent() throws IOException {
+ ExchangeSession.Item item = session.getItem("calendar", "19083675-f8ce-4d81-8ac8-096fa0bd0e13.EML");
+ item.getBody();
+ }
+
+ public void testGetInbox() throws IOException {
+ List<ExchangeSession.Event> items = session.getEventMessages("INBOX");
+ for (ExchangeSession.Item item : items) {
+ System.out.println(item.getBody());
+ }
+ }
+
+ public void testSearchEventCount() throws IOException {
+ Settings.setLoggingLevel("davmail", Level.WARN);
+ System.out.println("Item count: " + session.searchEvents("calendar", null).size());
+ System.out.println("InstanceType null: " + session.searchEvents("calendar", session.isNull("instancetype")).size());
+ System.out.println("InstanceType not null: " + session.searchEvents("calendar", session.not(session.isNull("instancetype"))).size());
+ System.out.println("InstanceType 0: " + session.searchEvents("calendar", session.isEqualTo("instancetype", 0)).size());
+ System.out.println("InstanceType 1: " + session.searchEvents("calendar", session.isEqualTo("instancetype", 1)).size());
+ System.out.println("InstanceType 2: " + session.searchEvents("calendar", session.isEqualTo("instancetype", 2)).size());
+ System.out.println("InstanceType 3: " + session.searchEvents("calendar", session.isEqualTo("instancetype", 3)).size());
+
+ if (session instanceof EwsExchangeSession) {
+ System.out.println("Recurring: " + session.searchEvents("calendar", session.isTrue("isrecurring")).size());
+ System.out.println("Non recurring: " + session.searchEvents("calendar", session.isFalse("isrecurring")).size());
+ System.out.println("Null recurring: " + session.searchEvents("calendar", session.isNull("isrecurring")).size());
+ }
+
+ }
+
+
+ public void testCreateEventTZ() throws IOException {
+ String itemBody = "BEGIN:VCALENDAR\n" +
+ "PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\n" +
+ "VERSION:2.0\n" +
+ "BEGIN:VTIMEZONE\n" +
+ "TZID:America/Bogota\n" +
+ "BEGIN:DAYLIGHT\n" +
+ "TZOFFSETFROM:-0500\n" +
+ "DTSTART:19920503T000000\n" +
+ "TZNAME:COT\n" +
+ "TZOFFSETTO:-0400\n" +
+ "RDATE:19920503T000000\n" +
+ "END:DAYLIGHT\n" +
+ "BEGIN:STANDARD\n" +
+ "TZOFFSETFROM:-0400\n" +
+ "DTSTART:19930404T000000\n" +
+ "TZNAME:COT\n" +
+ "TZOFFSETTO:-0500\n" +
+ "RDATE:19930404T000000\n" +
+ "END:STANDARD\n" +
+ "END:VTIMEZONE\n" +
+ "BEGIN:VEVENT\n" +
+ "CREATED:20110804T203742Z\n" +
+ "UID:1E17151D-92DA-4D2E-9747-60B489DE56F4\n" +
+ "DTEND;TZID=America/Bogota:20110805T090000\n" +
+ "TRANSP:OPAQUE\n" +
+ "SUMMARY:New Event 2\n" +
+ "DTSTART;TZID=America/Bogota:20110805T080000\n" +
+ "DTSTAMP:20110804T203742Z\n" +
+ "SEQUENCE:0\n" +
+ "END:VEVENT\n" +
+ "END:VCALENDAR";
+ String itemName = UUID.randomUUID().toString() + ".EML";
+ session.createOrUpdateItem("calendar", itemName, itemBody, null, null);
+ }
+
+ public void testCreateEventBrokenTZ() throws IOException {
+ String itemBody = "BEGIN:VCALENDAR\n" +
+ "PRODID:-//K Desktop Environment//NONSGML libkcal 4.3//EN\n" +
+ "VERSION:2.0\n" +
+ "METHOD:PUBLISH\n" +
+ "BEGIN:VTIMEZONE\n" +
+ "TZID:Europe/Amsterdam\n" +
+ "BEGIN:DAYLIGHT\n" +
+ "TZNAME:NST\n" +
+ "TZOFFSETFROM:+001932\n" +
+ "TZOFFSETTO:+011932\n" +
+ "DTSTART:19160501T234028\n" +
+ "RDATE;VALUE=DATE-TIME:19160501T234028\n" +
+ "RDATE;VALUE=DATE-TIME:19170417T014028\n" +
+ "RDATE;VALUE=DATE-TIME:19180402T014028\n" +
+ "RDATE;VALUE=DATE-TIME:19190408T014028\n" +
+ "RDATE;VALUE=DATE-TIME:19200406T014028\n" +
+ "RDATE;VALUE=DATE-TIME:19210405T014028\n" +
+ "RDATE;VALUE=DATE-TIME:19220327T014028\n" +
+ "RDATE;VALUE=DATE-TIME:19230602T014028\n" +
+ "RDATE;VALUE=DATE-TIME:19240331T014028\n" +
+ "RDATE;VALUE=DATE-TIME:19250606T014028\n" +
+ "RDATE;VALUE=DATE-TIME:19260516T014028\n" +
+ "RDATE;VALUE=DATE-TIME:19270516T014028\n" +
+ "RDATE;VALUE=DATE-TIME:19280516T014028\n" +
+ "RDATE;VALUE=DATE-TIME:19290516T014028\n" +
+ "RDATE;VALUE=DATE-TIME:19300516T014028\n" +
+ "RDATE;VALUE=DATE-TIME:19310516T014028\n" +
+ "RDATE;VALUE=DATE-TIME:19320523T014028\n" +
+ "RDATE;VALUE=DATE-TIME:19330516T014028\n" +
+ "RDATE;VALUE=DATE-TIME:19340516T014028\n" +
+ "RDATE;VALUE=DATE-TIME:19350516T014028\n" +
+ "RDATE;VALUE=DATE-TIME:19360516T014028\n" +
+ "RDATE;VALUE=DATE-TIME:19370523T014028\n" +
+ "END:DAYLIGHT\n" +
+ "BEGIN:STANDARD\n" +
+ "TZNAME:AMT\n" +
+ "TZOFFSETFROM:+011932\n" +
+ "TZOFFSETTO:+001932\n" +
+ "DTSTART:19161001T224028\n" +
+ "RDATE;VALUE=DATE-TIME:19161001T224028\n" +
+ "RDATE;VALUE=DATE-TIME:19170918T024028\n" +
+ "RDATE;VALUE=DATE-TIME:19181001T024028\n" +
+ "RDATE;VALUE=DATE-TIME:19190930T024028\n" +
+ "RDATE;VALUE=DATE-TIME:19200928T024028\n" +
+ "RDATE;VALUE=DATE-TIME:19210927T024028\n" +
+ "RDATE;VALUE=DATE-TIME:19221009T024028\n" +
+ "RDATE;VALUE=DATE-TIME:19231008T024028\n" +
+ "RDATE;VALUE=DATE-TIME:19241006T024028\n" +
+ "RDATE;VALUE=DATE-TIME:19251005T024028\n" +
+ "RDATE;VALUE=DATE-TIME:19261004T024028\n" +
+ "RDATE;VALUE=DATE-TIME:19271003T024028\n" +
+ "RDATE;VALUE=DATE-TIME:19281008T024028\n" +
+ "RDATE;VALUE=DATE-TIME:19291007T024028\n" +
+ "RDATE;VALUE=DATE-TIME:19301006T024028\n" +
+ "RDATE;VALUE=DATE-TIME:19311005T024028\n" +
+ "RDATE;VALUE=DATE-TIME:19321003T024028\n" +
+ "RDATE;VALUE=DATE-TIME:19331009T024028\n" +
+ "RDATE;VALUE=DATE-TIME:19341008T024028\n" +
+ "RDATE;VALUE=DATE-TIME:19351007T024028\n" +
+ "RDATE;VALUE=DATE-TIME:19361005T024028\n" +
+ "END:STANDARD\n" +
+ "BEGIN:DAYLIGHT\n" +
+ "TZNAME:NEST\n" +
+ "TZOFFSETFROM:+011932\n" +
+ "TZOFFSETTO:+0120\n" +
+ "DTSTART:19370701T224028\n" +
+ "RDATE;VALUE=DATE-TIME:19370701T224028\n" +
+ "END:DAYLIGHT\n" +
+ "BEGIN:STANDARD\n" +
+ "TZNAME:NET\n" +
+ "TZOFFSETFROM:+0120\n" +
+ "TZOFFSETTO:+0020\n" +
+ "DTSTART:19371004T024028\n" +
+ "RDATE;VALUE=DATE-TIME:19371004T024028\n" +
+ "RDATE;VALUE=DATE-TIME:19381003T024000\n" +
+ "RDATE;VALUE=DATE-TIME:19391009T024000\n" +
+ "END:STANDARD\n" +
+ "BEGIN:DAYLIGHT\n" +
+ "TZNAME:NEST\n" +
+ "TZOFFSETFROM:+0020\n" +
+ "TZOFFSETTO:+0120\n" +
+ "DTSTART:19380516T014000\n" +
+ "RDATE;VALUE=DATE-TIME:19380516T014000\n" +
+ "RDATE;VALUE=DATE-TIME:19390516T014000\n" +
+ "END:DAYLIGHT\n" +
+ "BEGIN:DAYLIGHT\n" +
+ "TZNAME:CEST\n" +
+ "TZOFFSETFROM:+0020\n" +
+ "TZOFFSETTO:+0200\n" +
+ "DTSTART:19400516T234000\n" +
+ "RDATE;VALUE=DATE-TIME:19400516T234000\n" +
+ "END:DAYLIGHT\n" +
+ "BEGIN:STANDARD\n" +
+ "TZNAME:CET\n" +
+ "TZOFFSETFROM:+0200\n" +
+ "TZOFFSETTO:+0100\n" +
+ "DTSTART:19790930T030000\n" +
+ "RRULE:FREQ=YEARLY;COUNT=17;BYDAY=-1SU;BYMONTH=9\n" +
+ "END:STANDARD\n" +
+ "BEGIN:STANDARD\n" +
+ "TZNAME:CET\n" +
+ "TZOFFSETFROM:+0200\n" +
+ "TZOFFSETTO:+0100\n" +
+ "DTSTART:19961027T030000\n" +
+ "RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\n" +
+ "END:STANDARD\n" +
+ "BEGIN:STANDARD\n" +
+ "TZNAME:CET\n" +
+ "TZOFFSETFROM:+0200\n" +
+ "TZOFFSETTO:+0100\n" +
+ "DTSTART:19421103T024000\n" +
+ "RDATE;VALUE=DATE-TIME:19421103T024000\n" +
+ "RDATE;VALUE=DATE-TIME:19431004T020000\n" +
+ "RDATE;VALUE=DATE-TIME:19441002T020000\n" +
+ "RDATE;VALUE=DATE-TIME:19450916T020000\n" +
+ "RDATE;VALUE=DATE-TIME:19770925T030000\n" +
+ "RDATE;VALUE=DATE-TIME:19781001T030000\n" +
+ "END:STANDARD\n" +
+ "BEGIN:DAYLIGHT\n" +
+ "TZNAME:CEST\n" +
+ "TZOFFSETFROM:+0100\n" +
+ "TZOFFSETTO:+0200\n" +
+ "DTSTART:19810329T020000\n" +
+ "RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3\n" +
+ "END:DAYLIGHT\n" +
+ "BEGIN:DAYLIGHT\n" +
+ "TZNAME:CEST\n" +
+ "TZOFFSETFROM:+0100\n" +
+ "TZOFFSETTO:+0200\n" +
+ "DTSTART:19430329T010000\n" +
+ "RDATE;VALUE=DATE-TIME:19430329T010000\n" +
+ "RDATE;VALUE=DATE-TIME:19440403T010000\n" +
+ "RDATE;VALUE=DATE-TIME:19450402T010000\n" +
+ "RDATE;VALUE=DATE-TIME:19770403T020000\n" +
+ "RDATE;VALUE=DATE-TIME:19780402T020000\n" +
+ "RDATE;VALUE=DATE-TIME:19790401T020000\n" +
+ "RDATE;VALUE=DATE-TIME:19800406T020000\n" +
+ "END:DAYLIGHT\n" +
+ "END:VTIMEZONE\n" +
+ "BEGIN:VEVENT\n" +
+ "DTSTAMP:20111022T175835Z\n" +
+ "CREATED:20111022T175832Z\n" +
+ "UID:libkcal-797112054.882\n" +
+ "LAST-MODIFIED:20111022T175832Z\n" +
+ "SUMMARY:Test Event 000\n" +
+ "DTSTART;TZID=\"Europe/Amsterdam\":20111027T120000\n" +
+ "DTEND;TZID=\"Europe/Amsterdam\":20111027T174500\n" +
+ "TRANSP:OPAQUE\n" +
+ "X-MICROSOFT-CDO-REPLYTIME:20111022T175835Z\n" +
+ "X-MICROSOFT-CDO-ALLDAYEVENT:FALSE\n" +
+ "X-MICROSOFT-CDO-BUSYSTATUS:BUSY\n" +
+ "END:VEVENT\n" +
+ "END:VCALENDAR";
+ String itemName = UUID.randomUUID().toString() + ".EML";
+ session.createOrUpdateItem("calendar", itemName, itemBody, null, null);
+ }
+
+}
+
diff --git a/src/test/davmail/exchange/TestExchangeSessionContact.java b/src/test/davmail/exchange/TestExchangeSessionContact.java
new file mode 100644
index 0000000..ea615d5
--- /dev/null
+++ b/src/test/davmail/exchange/TestExchangeSessionContact.java
@@ -0,0 +1,496 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange;
+
+import davmail.Settings;
+import davmail.util.IOUtil;
+import org.apache.commons.codec.binary.Base64;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Test ExchangeSession contact features.
+ */
+ at SuppressWarnings({"UseOfSystemOutOrSystemErr"})
+public class TestExchangeSessionContact extends AbstractExchangeSessionTestCase {
+ static String itemName;
+
+ protected ExchangeSession.Contact getCurrentContact() throws IOException {
+ if (itemName != null) {
+ return (ExchangeSession.Contact) session.getItem("testcontactfolder", itemName);
+ } else {
+ List<ExchangeSession.Contact> contacts = session.searchContacts("testcontactfolder", ExchangeSession.CONTACT_ATTRIBUTES, null, 0);
+ itemName = contacts.get(0).itemName;
+ return contacts.get(0);
+ }
+ }
+
+ public void testCreateFolder() throws IOException {
+ // recreate empty folder
+ session.deleteFolder("testcontactfolder");
+ session.createContactFolder("testcontactfolder", null);
+ }
+
+ public void testCreateContact() throws IOException {
+ itemName = UUID.randomUUID().toString() + ".vcf";
+ VCardWriter vCardWriter = new VCardWriter();
+ vCardWriter.startCard();
+ vCardWriter.appendProperty("N", "sn", "givenName", "middlename", "personaltitle", "namesuffix");
+ vCardWriter.appendProperty("FN", "common name");
+ vCardWriter.appendProperty("NICKNAME", "nickname");
+
+ vCardWriter.appendProperty("TEL;TYPE=cell", "mobile");
+ vCardWriter.appendProperty("TEL;TYPE=work", "telephoneNumber");
+ vCardWriter.appendProperty("TEL;TYPE=home,voice", "homePhone");
+ vCardWriter.appendProperty("TEL;TYPE=fax", "facsimiletelephonenumber");
+ vCardWriter.appendProperty("TEL;TYPE=pager", "pager");
+
+ vCardWriter.appendProperty("ADR;TYPE=home", "homepostofficebox", null, "homeStreet", "homeCity", "homeState", "homePostalCode", "homeCountry");
+ vCardWriter.appendProperty("ADR;TYPE=work", "postofficebox", "roomnumber", "street", "l", "st", "postalcode", "co");
+ vCardWriter.appendProperty("ADR;TYPE=other", "otherpostofficebox", null, "otherstreet", "othercity", "otherstate", "otherpostalcode", "othercountry");
+
+ vCardWriter.appendProperty("EMAIL;TYPE=work", "email1 at local.net");
+ vCardWriter.appendProperty("EMAIL;TYPE=home", "email2 at local.net");
+ vCardWriter.appendProperty("EMAIL;TYPE=other", "email3 at local.net");
+
+ vCardWriter.appendProperty("ORG", "o", "department");
+
+ vCardWriter.appendProperty("URL;TYPE=work", "http://local.net");
+ vCardWriter.appendProperty("TITLE", "title");
+ vCardWriter.appendProperty("NOTE", "description");
+
+ vCardWriter.appendProperty("CUSTOM1", "extensionattribute1");
+ vCardWriter.appendProperty("CUSTOM2", "extensionattribute2");
+ vCardWriter.appendProperty("CUSTOM3", "extensionattribute3");
+ vCardWriter.appendProperty("CUSTOM4", "extensionattribute4");
+
+ vCardWriter.appendProperty("ROLE", "profession");
+ vCardWriter.appendProperty("X-AIM", "im");
+ vCardWriter.appendProperty("BDAY", "2000-01-02T00:00:00Z");
+ vCardWriter.appendProperty("CATEGORIES", "keyword1,keyword2");
+
+ vCardWriter.appendProperty("FBURL", "http://fburl");
+
+ vCardWriter.appendProperty("X-ASSISTANT", "secretarycn");
+ vCardWriter.appendProperty("X-MANAGER", "manager");
+ vCardWriter.appendProperty("X-SPOUSE", "spousecn");
+
+ vCardWriter.appendProperty("CLASS", "PRIVATE");
+
+ // add photo
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ InputStream fileInputStream = new FileInputStream("src/data/anonymous.jpg");
+ IOUtil.write(fileInputStream, baos);
+ vCardWriter.appendProperty("PHOTO;ENCODING=b;TYPE=JPEG", new String(Base64.encodeBase64(baos.toByteArray())));
+
+ vCardWriter.endCard();
+
+ ExchangeSession.ItemResult result = session.createOrUpdateContact("testcontactfolder", itemName, vCardWriter.toString(), null, null);
+ assertEquals(201, result.status);
+
+ }
+
+ public void testGetContact() throws IOException {
+ ExchangeSession.Contact contact = (ExchangeSession.Contact) session.getItem("testcontactfolder", itemName);
+ assertEquals("common name", contact.get("cn"));
+ assertEquals("sn", contact.get("sn"));
+ assertEquals("givenName", contact.get("givenName"));
+ assertEquals("middlename", contact.get("middlename"));
+ assertEquals("personaltitle", contact.get("personaltitle"));
+ assertEquals("namesuffix", contact.get("namesuffix"));
+ assertNotNull("lastmodified");
+ assertEquals("nickname", contact.get("nickname"));
+
+ assertEquals("mobile", contact.get("mobile"));
+ assertEquals("telephoneNumber", contact.get("telephoneNumber"));
+ assertEquals("homePhone", contact.get("homePhone"));
+ assertEquals("facsimiletelephonenumber", contact.get("facsimiletelephonenumber"));
+ assertEquals("pager", contact.get("pager"));
+
+ assertEquals("homepostofficebox", contact.get("homepostofficebox"));
+ assertEquals("homeStreet", contact.get("homeStreet"));
+ assertEquals("homeCity", contact.get("homeCity"));
+ assertEquals("homeState", contact.get("homeState"));
+ assertEquals("homePostalCode", contact.get("homePostalCode"));
+ assertEquals("homeCountry", contact.get("homeCountry"));
+
+ assertEquals("postofficebox", contact.get("postofficebox"));
+ assertEquals("roomnumber", contact.get("roomnumber"));
+ assertEquals("street", contact.get("street"));
+ assertEquals("l", contact.get("l"));
+ assertEquals("st", contact.get("st"));
+ assertEquals("postalcode", contact.get("postalcode"));
+ assertEquals("co", contact.get("co"));
+
+ assertEquals("email1 at local.net", contact.get("smtpemail1"));
+ assertEquals("email2 at local.net", contact.get("smtpemail2"));
+ assertEquals("email3 at local.net", contact.get("smtpemail3"));
+
+ assertEquals("o", contact.get("o"));
+ assertEquals("department", contact.get("department"));
+
+ assertEquals("http://local.net", contact.get("businesshomepage"));
+ assertEquals("title", contact.get("title"));
+ assertEquals("description", contact.get("description"));
+
+ assertEquals("extensionattribute1", contact.get("extensionattribute1"));
+ assertEquals("extensionattribute2", contact.get("extensionattribute2"));
+ assertEquals("extensionattribute3", contact.get("extensionattribute3"));
+ assertEquals("extensionattribute4", contact.get("extensionattribute4"));
+
+ assertEquals("profession", contact.get("profession"));
+ assertEquals("im", contact.get("im"));
+ assertEquals("20000102T000000Z", contact.get("bday"));
+
+ assertEquals("otherpostofficebox", contact.get("otherpostofficebox"));
+ assertEquals("otherstreet", contact.get("otherstreet"));
+ assertEquals("othercity", contact.get("othercity"));
+ assertEquals("otherstate", contact.get("otherstate"));
+ assertEquals("otherpostalcode", contact.get("otherpostalcode"));
+ assertEquals("othercountry", contact.get("othercountry"));
+
+ assertEquals("secretarycn", contact.get("secretarycn"));
+ assertEquals("manager", contact.get("manager"));
+ assertEquals("spousecn", contact.get("spousecn"));
+ assertEquals("keyword1,keyword2", contact.get("keywords"));
+
+ assertEquals("true", contact.get("private"));
+
+ assertEquals("http://fburl", contact.get("fburl"));
+
+ assertEquals("true", contact.get("haspicture"));
+ if (!Settings.getBooleanProperty("davmail.enableEws") || "Exchange2010".equals(session.getServerVersion())) {
+ assertNotNull(session.getContactPhoto(contact));
+ }
+ }
+
+ public void testUpdateContact() throws IOException {
+ ExchangeSession.Contact contact = getCurrentContact();
+
+ VCardWriter vCardWriter = new VCardWriter();
+ vCardWriter.startCard();
+
+ vCardWriter.endCard();
+
+ ExchangeSession.ItemResult result = session.createOrUpdateContact("testcontactfolder", itemName, vCardWriter.toString(), contact.etag, null);
+ assertEquals(200, result.status);
+
+ contact = getCurrentContact();
+ assertNull(contact.get("cn"));
+ assertNull(contact.get("sn"));
+ assertNull(contact.get("givenName"));
+ assertNull(contact.get("middlename"));
+ assertNull(contact.get("personaltitle"));
+ assertNull(contact.get("namesuffix"));
+ assertNotNull("lastmodified");
+ assertNull(contact.get("nickname"));
+
+ assertNull(contact.get("mobile"));
+ assertNull(contact.get("telephoneNumber"));
+ assertNull(contact.get("homePhone"));
+ assertNull(contact.get("facsimiletelephonenumber"));
+ assertNull(contact.get("pager"));
+
+ assertNull(contact.get("homepostofficebox"));
+ assertNull(contact.get("homeStreet"));
+ assertNull(contact.get("homeCity"));
+ assertNull(contact.get("homeState"));
+ assertNull(contact.get("homePostalCode"));
+ assertNull(contact.get("homeCountry"));
+
+ assertNull(contact.get("postofficebox"));
+ assertNull(contact.get("roomnumber"));
+ assertNull(contact.get("street"));
+ assertNull(contact.get("l"));
+ assertNull(contact.get("st"));
+ assertNull(contact.get("postalcode"));
+ assertNull(contact.get("co"));
+
+ assertNull(contact.get("email1"));
+ assertNull(contact.get("email2"));
+ assertNull(contact.get("email3"));
+
+ assertNull(contact.get("o"));
+ assertNull(contact.get("department"));
+
+ assertNull(contact.get("businesshomepage"));
+ assertNull(contact.get("title"));
+ assertNull(contact.get("description"));
+
+ assertNull(contact.get("extensionattribute1"));
+ assertNull(contact.get("extensionattribute2"));
+ assertNull(contact.get("extensionattribute3"));
+ assertNull(contact.get("extensionattribute4"));
+
+ assertNull(contact.get("profession"));
+ assertNull(contact.get("im"));
+ assertNull(contact.get("bday"));
+
+ assertNull(contact.get("otherpostofficebox"));
+ assertNull(contact.get("otherstreet"));
+ assertNull(contact.get("othercity"));
+ assertNull(contact.get("otherstate"));
+ assertNull(contact.get("otherpostalcode"));
+ assertNull(contact.get("othercountry"));
+
+ assertNull(contact.get("secretarycn"));
+ assertNull(contact.get("manager"));
+ assertNull(contact.get("spousecn"));
+ assertNull(contact.get("keywords"));
+
+ assertNull(contact.get("private"));
+
+ assertTrue(contact.get("haspicture") == null || "false".equals(contact.get("haspicture")));
+
+ assertNull(session.getContactPhoto(contact));
+ }
+
+
+ public void testUpdateEmail() throws IOException {
+ ExchangeSession.Contact contact = getCurrentContact();
+
+ VCardWriter vCardWriter = new VCardWriter();
+ vCardWriter.startCard();
+ vCardWriter.appendProperty("EMAIL;TYPE=work", "email1.test at local.net");
+ vCardWriter.endCard();
+
+ ExchangeSession.ItemResult result = session.createOrUpdateContact("testcontactfolder", itemName, vCardWriter.toString(), contact.etag, null);
+ assertEquals(200, result.status);
+
+ contact = getCurrentContact();
+
+ assertEquals("email1.test at local.net", contact.get("smtpemail1"));
+
+ }
+
+ public void testUpperCaseParamName() throws IOException {
+ ExchangeSession.Contact contact = getCurrentContact();
+
+ VCardWriter vCardWriter = new VCardWriter();
+ vCardWriter.startCard();
+ vCardWriter.appendProperty("TEL;TYPE=CELL", "mobile");
+ vCardWriter.endCard();
+
+ ExchangeSession.ItemResult result = session.createOrUpdateContact("testcontactfolder", itemName, vCardWriter.toString(), contact.etag, null);
+ assertEquals(200, result.status);
+
+ contact = getCurrentContact();
+
+ assertEquals("mobile", contact.get("mobile"));
+
+ }
+
+ public void testMultipleTypesParamName() throws IOException {
+ ExchangeSession.Contact contact = (ExchangeSession.Contact) session.getItem("testcontactfolder", itemName);
+
+ VCardWriter vCardWriter = new VCardWriter();
+ vCardWriter.startCard();
+ vCardWriter.appendProperty("TEL;TYPE=CELL;TYPE=pref", "another mobile");
+ vCardWriter.endCard();
+
+ ExchangeSession.ItemResult result = session.createOrUpdateContact("testcontactfolder", itemName, vCardWriter.toString(), contact.etag, null);
+ assertEquals(200, result.status);
+
+ contact = (ExchangeSession.Contact) session.getItem("testcontactfolder", itemName);
+
+ assertEquals("another mobile", contact.get("mobile"));
+
+ }
+
+ public void testLowerCaseTypesParamName() throws IOException {
+ ExchangeSession.Contact contact = (ExchangeSession.Contact) session.getItem("testcontactfolder", itemName);
+
+ VCardWriter vCardWriter = new VCardWriter();
+ vCardWriter.startCard();
+ vCardWriter.appendProperty("TEL;type=HOME;type=pref", "5 68 99 3");
+ vCardWriter.endCard();
+
+ ExchangeSession.ItemResult result = session.createOrUpdateContact("testcontactfolder", itemName, vCardWriter.toString(), contact.etag, null);
+ assertEquals(200, result.status);
+
+ contact = (ExchangeSession.Contact) session.getItem("testcontactfolder", itemName);
+
+ assertEquals("5 68 99 3", contact.get("homePhone"));
+
+ }
+
+ public void testKeyPrefix() throws IOException {
+ ExchangeSession.Contact contact = (ExchangeSession.Contact) session.getItem("testcontactfolder", itemName);
+
+ VCardWriter vCardWriter = new VCardWriter();
+ vCardWriter.startCard();
+ vCardWriter.appendProperty("ITEM1.TEL;TYPE=CELL;TYPE=pref", "mobile with prefix");
+ vCardWriter.endCard();
+
+ ExchangeSession.ItemResult result = session.createOrUpdateContact("testcontactfolder", itemName, vCardWriter.toString(), contact.etag, null);
+ assertEquals(200, result.status);
+
+ contact = (ExchangeSession.Contact) session.getItem("testcontactfolder", itemName);
+
+ assertEquals("mobile with prefix", contact.get("mobile"));
+
+ }
+
+ public void testIphonePersonalHomePage() throws IOException {
+ ExchangeSession.Contact contact = (ExchangeSession.Contact) session.getItem("testcontactfolder", itemName);
+
+ VCardWriter vCardWriter = new VCardWriter();
+ vCardWriter.startCard();
+ vCardWriter.appendProperty("ITEM1.URL", "http://www.myhomepage.org");
+ vCardWriter.endCard();
+
+ ExchangeSession.ItemResult result = session.createOrUpdateContact("testcontactfolder", itemName, vCardWriter.toString(), contact.etag, null);
+ assertEquals(200, result.status);
+
+ contact = (ExchangeSession.Contact) session.getItem("testcontactfolder", itemName);
+
+ assertEquals("http://www.myhomepage.org", contact.get("personalHomePage"));
+
+ }
+
+
+ public void testIphoneEncodedCategories() throws IOException {
+ ExchangeSession.Contact contact = getCurrentContact();
+
+ VCardWriter vCardWriter = new VCardWriter();
+ vCardWriter.startCard();
+ vCardWriter.appendProperty("CATEGORIES", "rouge,vert");
+ vCardWriter.endCard();
+
+ ExchangeSession.ItemResult result = session.createOrUpdateContact("testcontactfolder", itemName, vCardWriter.toString(), contact.etag, null);
+ assertEquals(200, result.status);
+
+ contact = getCurrentContact();
+
+ assertEquals("rouge,vert", contact.get("keywords"));
+
+ }
+
+ public void testSemiColonInCompoundValue() throws IOException {
+ ExchangeSession.Contact contact = getCurrentContact();
+ String itemBody = "BEGIN:VCARD\n" +
+ "VERSION:3.0\n" +
+ "item1.ADR;type=WORK;type=pref:;;line1\\nline 2 \\; with semicolon;;;;\n" +
+ "END:VCARD";
+
+ ExchangeSession.ItemResult result = session.createOrUpdateContact("testcontactfolder", itemName, itemBody, contact.etag, null);
+ assertEquals(200, result.status);
+ }
+
+ public void testIphoneEncodedComma() throws IOException {
+ ExchangeSession.Contact contact = getCurrentContact();
+
+ VCardWriter vCardWriter = new VCardWriter();
+ vCardWriter.startCard();
+ vCardWriter.appendProperty("ITEM1.TEL;TYPE=CELL;TYPE=pref", "mobile\\, with comma");
+ vCardWriter.endCard();
+
+ ExchangeSession.ItemResult result = session.createOrUpdateContact("testcontactfolder", itemName, vCardWriter.toString(), contact.etag, null);
+ assertEquals(200, result.status);
+
+ contact = getCurrentContact();
+
+ assertEquals("mobile, with comma", contact.get("mobile"));
+
+ }
+
+ public void testAmpersAndValue() throws IOException {
+ ExchangeSession.Contact contact = getCurrentContact();
+
+ VCardWriter vCardWriter = new VCardWriter();
+ vCardWriter.startCard();
+ vCardWriter.appendProperty("FN", "common & name");
+ vCardWriter.endCard();
+
+ ExchangeSession.ItemResult result = session.createOrUpdateContact("testcontactfolder", itemName, vCardWriter.toString(), contact.etag, null);
+ assertEquals(200, result.status);
+
+ contact = getCurrentContact();
+
+ assertEquals("common & name", contact.get("cn"));
+
+ }
+
+ public void testDateValue() throws IOException {
+ ExchangeSession.Contact contact = getCurrentContact();
+
+ VCardWriter vCardWriter = new VCardWriter();
+ vCardWriter.startCard();
+ vCardWriter.appendProperty("BDAY", "2000-01-02");
+ vCardWriter.endCard();
+
+ ExchangeSession.ItemResult result = session.createOrUpdateContact("testcontactfolder", itemName, vCardWriter.toString(), contact.etag, null);
+ assertEquals(200, result.status);
+
+ contact = getCurrentContact();
+
+ assertEquals("20000102T000000Z", contact.get("bday"));
+ System.out.println(contact.getBody());
+ }
+
+ public void testAnniversary() throws IOException {
+ ExchangeSession.Contact contact = getCurrentContact();
+
+ VCardWriter vCardWriter = new VCardWriter();
+ vCardWriter.startCard();
+ vCardWriter.appendProperty("X-ANNIVERSARY", "2000-01-02");
+ vCardWriter.endCard();
+
+ ExchangeSession.ItemResult result = session.createOrUpdateContact("testcontactfolder", itemName, vCardWriter.toString(), contact.etag, null);
+ assertEquals(200, result.status);
+
+ contact = getCurrentContact();
+
+ assertEquals("20000102T000000Z", contact.get("anniversary"));
+ System.out.println(contact.getBody());
+ }
+
+ public void testSpecialUrlCharacters() throws IOException {
+ testCreateFolder();
+
+ VCardWriter vCardWriter = new VCardWriter();
+ vCardWriter.startCard();
+ vCardWriter.appendProperty("N", "sn", "givenName", "middlename", "personaltitle", "namesuffix");
+ vCardWriter.appendProperty("FN", "common name");
+ vCardWriter.endCard();
+
+ itemName = "test {<:&'>} \"accentué.vcf";
+
+ ExchangeSession.ItemResult result = session.createOrUpdateContact("testcontactfolder", itemName, vCardWriter.toString(), null, null);
+ assertEquals(201, result.status);
+
+ ExchangeSession.Contact contact = getCurrentContact();
+
+ assertEquals("common name", contact.get("cn"));
+ }
+
+ public void testPagingSearchContacts() throws IOException {
+ int maxCount = 0;
+ List<ExchangeSession.Contact> contacts = session.searchContacts(ExchangeSession.CONTACTS, ExchangeSession.CONTACT_ATTRIBUTES, null, maxCount);
+ int folderSize = contacts.size();
+ assertEquals(50, session.searchContacts(ExchangeSession.CONTACTS, ExchangeSession.CONTACT_ATTRIBUTES, null, 50).size());
+ assertEquals(folderSize, session.searchContacts(ExchangeSession.CONTACTS, ExchangeSession.CONTACT_ATTRIBUTES, null, folderSize+1).size());
+ }
+
+}
diff --git a/src/test/davmail/exchange/TestExchangeSessionEvent.java b/src/test/davmail/exchange/TestExchangeSessionEvent.java
new file mode 100644
index 0000000..7a38068
--- /dev/null
+++ b/src/test/davmail/exchange/TestExchangeSessionEvent.java
@@ -0,0 +1,388 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange;
+
+import davmail.BundleMessage;
+import junit.framework.TestCase;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+/**
+ * Test ExchangeSession event conversion.
+ */
+ at SuppressWarnings({"UseOfSystemOutOrSystemErr"})
+public class TestExchangeSessionEvent extends TestCase {
+ static String email = "user at company.com";
+ static VObject vTimeZone;
+
+ static {
+ try {
+ vTimeZone = new VObject(new ICSBufferedReader(new StringReader("BEGIN:VTIMEZONE\n" +
+ "TZID:(GMT+01.00) Paris/Madrid/Brussels/Copenhagen\n" +
+ "X-MICROSOFT-CDO-TZID:3\n" +
+ "BEGIN:STANDARD\n" +
+ "DTSTART:16010101T030000\n" +
+ "TZOFFSETFROM:+0200\n" +
+ "TZOFFSETTO:+0100\n" +
+ "RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=10;BYDAY=-1SU\n" +
+ "END:STANDARD\n" +
+ "BEGIN:DAYLIGHT\n" +
+ "DTSTART:16010101T020000\n" +
+ "TZOFFSETFROM:+0100\n" +
+ "TZOFFSETTO:+0200\n" +
+ "RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=3;BYDAY=-1SU\n" +
+ "END:DAYLIGHT\n" +
+ "END:VTIMEZONE")));
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+
+ protected String fixICS(String icsBody, boolean fromServer) throws IOException {
+ VCalendar vCalendar = new VCalendar(icsBody, email, vTimeZone);
+ vCalendar.fixVCalendar(fromServer);
+ return vCalendar.toString();
+ }
+
+ public void testNoClass() throws IOException {
+ String itemBody = "BEGIN:VCALENDAR\n" +
+ "BEGIN:VEVENT\n" +
+ "END:VEVENT\n" +
+ "END:VCALENDAR";
+ String toServer = fixICS(itemBody, false);
+ String toClient = fixICS(itemBody, true);
+ System.out.println(toServer);
+ System.out.println(toClient);
+ assertTrue(toServer.indexOf("CLASS") < 0);
+ assertTrue(toClient.indexOf("CLASS") < 0);
+ assertTrue(toClient.indexOf("X-CALENDARSERVER-ACCESS") < 0);
+ }
+
+ public void testPublicClass() throws IOException {
+ String itemBody = "BEGIN:VCALENDAR\n" +
+ "BEGIN:VEVENT\n" +
+ "CLASS:PUBLIC\n" +
+ "END:VEVENT\n" +
+ "END:VCALENDAR";
+ String toServer = fixICS(itemBody, false);
+ String toClient = fixICS(itemBody, true);
+ System.out.println(toServer);
+ System.out.println(toClient);
+ assertTrue(toServer.indexOf("CLASS:PUBLIC") >= 0);
+ assertTrue(toClient.indexOf("CLASS:PUBLIC") >= 0);
+ assertTrue(toClient.indexOf("X-CALENDARSERVER-ACCESS:PUBLIC") >= 0);
+ }
+
+ public void testPrivateClass() throws IOException {
+ String itemBody = "BEGIN:VCALENDAR\n" +
+ "BEGIN:VEVENT\n" +
+ "CLASS:PRIVATE\n" +
+ "END:VEVENT\n" +
+ "END:VCALENDAR";
+ String toServer = fixICS(itemBody, false);
+ String toClient = fixICS(itemBody, true);
+ System.out.println(toServer);
+ System.out.println(toClient);
+ assertTrue(toServer.indexOf("CLASS:PRIVATE") >= 0);
+ assertTrue(toClient.indexOf("CLASS:PRIVATE") >= 0);
+ assertTrue(toClient.indexOf("X-CALENDARSERVER-ACCESS:CONFIDENTIAL") >= 0);
+ }
+
+ public void testConfidentialClass() throws IOException {
+ String itemBody = "BEGIN:VCALENDAR\n" +
+ "BEGIN:VEVENT\n" +
+ "CLASS:CONFIDENTIAL\n" +
+ "END:VEVENT\n" +
+ "END:VCALENDAR";
+ String toServer = fixICS(itemBody, false);
+ String toClient = fixICS(itemBody, true);
+ System.out.println(toServer);
+ System.out.println(toClient);
+ assertTrue(toServer.indexOf("CLASS:CONFIDENTIAL") >= 0);
+ assertTrue(toClient.indexOf("CLASS:CONFIDENTIAL") >= 0);
+ assertTrue(toClient.indexOf("X-CALENDARSERVER-ACCESS:PRIVATE") >= 0);
+ }
+
+ public void testCalendarServerAccessPrivate() throws IOException {
+ String itemBody = "BEGIN:VCALENDAR\n" +
+ "PRODID:-//Apple Inc.//iCal 4.0.3//EN\n" +
+ "BEGIN:VEVENT\n" +
+ "X-CALENDARSERVER-ACCESS:PRIVATE\n" +
+ "END:VEVENT\n" +
+ "END:VCALENDAR";
+ String toServer = fixICS(itemBody, false);
+ System.out.println(toServer);
+ assertTrue(toServer.indexOf("CLASS:CONFIDENTIAL") >= 0);
+ }
+
+ public void testCalendarServerAccessConfidential() throws IOException {
+ String itemBody = "BEGIN:VCALENDAR\n" +
+ "PRODID:-//Apple Inc.//iCal 4.0.3//EN\n" +
+ "BEGIN:VEVENT\n" +
+ "X-CALENDARSERVER-ACCESS:CONFIDENTIAL\n" +
+ "END:VEVENT\n" +
+ "END:VCALENDAR";
+ String toServer = fixICS(itemBody, false);
+ System.out.println(toServer);
+ assertTrue(toServer.indexOf("CLASS:PRIVATE") >= 0);
+ }
+
+ public void testCalendarServerAccessPublic() throws IOException {
+ String itemBody = "BEGIN:VCALENDAR\n" +
+ "PRODID:-//Apple Inc.//iCal 4.0.3//EN\n" +
+ "BEGIN:VEVENT\n" +
+ "X-CALENDARSERVER-ACCESS:PUBLIC\n" +
+ "END:VEVENT\n" +
+ "END:VCALENDAR";
+ String toServer = fixICS(itemBody, false);
+ System.out.println(toServer);
+ assertTrue(toServer.indexOf("CLASS:PUBLIC") >= 0);
+ }
+
+ public void testCalendarServerAccessNone() throws IOException {
+ String itemBody = "BEGIN:VCALENDAR\n" +
+ "PRODID:-//Apple Inc.//iCal 4.0.3//EN\n" +
+ "BEGIN:VEVENT\n" +
+ "END:VEVENT\n" +
+ "END:VCALENDAR";
+ String toServer = fixICS(itemBody, false);
+ System.out.println(toServer);
+ assertFalse(toServer.contains("CLASS"));
+ }
+
+ public void testNoOrganizer() throws IOException {
+ String itemBody = "BEGIN:VCALENDAR\n" +
+ "BEGIN:VEVENT\n" +
+ "END:VEVENT\n" +
+ "END:VCALENDAR";
+ String toServer = fixICS(itemBody, false);
+ System.out.println(toServer);
+ assertTrue(toServer.contains("ORGANIZER"));
+ }
+
+
+ public void testValarm() throws IOException {
+ String itemBody = "BEGIN:VCALENDAR\n" +
+ "BEGIN:VEVENT\n" +
+ "BEGIN:VALARM\n" +
+ "TRIGGER:-PT15M\n" +
+ "ATTACH;VALUE=URI:Basso\n" +
+ "ACTION:AUDIO\n" +
+ "END:VALARM\n" +
+ "END:VEVENT\n" +
+ "END:VCALENDAR";
+ String toServer = fixICS(itemBody, false);
+ System.out.println(toServer);
+ assertTrue(toServer.contains("ACTION:DISPLAY"));
+ }
+
+ public void testReceiveAllDay() throws IOException {
+ String itemBody = "BEGIN:VCALENDAR\n" +
+ vTimeZone +
+ "BEGIN:VEVENT\n" +
+ "DTSTART;TZID=\"(GMT+01.00) Paris/Madrid/Brussels/Copenhagen\":20100615T000000\n" +
+ "DTEND;TZID=\"(GMT+01.00) Paris/Madrid/Brussels/Copenhagen\":20100616T000000\n" +
+ "X-MICROSOFT-CDO-ALLDAYEVENT:TRUE\n" +
+ "END:VEVENT\n" +
+ "END:VCALENDAR";
+ String toClient = fixICS(itemBody, true);
+ System.out.println(toClient);
+ // OWA created allday events have the X-MICROSOFT-CDO-ALLDAYEVENT set to true and always 000000 in event time
+ // just remove the TZID, add VALUE=DATE param and set a date only value
+ assertTrue(toClient.contains("DTSTART;VALUE=DATE:20100615"));
+ assertTrue(toClient.contains("DTEND;VALUE=DATE:20100616"));
+ }
+
+ public void testSendAllDay() throws IOException {
+ String itemBody = "BEGIN:VCALENDAR\n" +
+ "BEGIN:VEVENT\n" +
+ "DTSTART;VALUE=DATE:20100615\n" +
+ "DTEND;VALUE=DATE:20100616\n" +
+ "END:VEVENT\n" +
+ "END:VCALENDAR";
+ String toServer = fixICS(itemBody, false);
+ System.out.println(toServer);
+ // Client created allday event have no timezone and no time information in date values
+ // first set the X-MICROSOFT-CDO-ALLDAYEVENT flag for OWA
+ assertTrue(toServer.contains("X-MICROSOFT-CDO-ALLDAYEVENT:TRUE"));
+ // then patch TZID for Outlook (need to retrieve OWA TZID
+ assertTrue(toServer.contains("BEGIN:VTIMEZONE"));
+ assertTrue(toServer.contains("TZID:" + vTimeZone.getPropertyValue("TZID")));
+ assertTrue(toServer.contains("DTSTART;TZID=\"" + vTimeZone.getPropertyValue("TZID") + "\":20100615T000000"));
+ assertTrue(toServer.contains("DTEND;TZID=\"" + vTimeZone.getPropertyValue("TZID") + "\":20100616T000000"));
+ }
+
+ public void testRsvp() throws IOException {
+ String itemBody = "BEGIN:VCALENDAR\n" +
+ "BEGIN:VEVENT\n" +
+ "ATTENDEE;PARTSTAT=ACCEPTED;RSVP=TRUE:MAILTO:" + email + "\n" +
+ "END:VEVENT\n" +
+ "END:VCALENDAR";
+ String toClient = fixICS(itemBody, true);
+ System.out.println(toClient);
+ assertTrue(toClient.contains("ATTENDEE;PARTSTAT=ACCEPTED:MAILTO:" + email));
+ }
+
+ public void testExdate() throws IOException {
+ String itemBody = "BEGIN:VCALENDAR\n" +
+ "BEGIN:VEVENT\n" +
+ "EXDATE;TZID=\"Europe/Paris\":20100809T150000,20100823T150000\n" +
+ "END:VEVENT\n" +
+ "END:VCALENDAR";
+ String toClient = fixICS(itemBody, true);
+ System.out.println(toClient);
+ assertTrue(toClient.contains("EXDATE;TZID=\"Europe/Paris\":20100823T150000"));
+
+ }
+
+ public void testEmptyLine() throws IOException {
+ String itemBody = "BEGIN:VCALENDAR\n" +
+ "BEGIN:VEVENT\n" +
+ "\n" +
+ "END:VEVENT\n" +
+ "END:VCALENDAR";
+ String toClient = fixICS(itemBody, true);
+ System.out.println(toClient);
+ }
+
+ public void testAttendeeStatus() throws IOException {
+ String itemBody = "BEGIN:VCALENDAR\n" +
+ "BEGIN:VEVENT\n" +
+ "ATTENDEE;PARTSTAT=ACCEPTED;RSVP=FALSE:MAILTO:" + email + "\n" +
+ "END:VEVENT\n" +
+ "END:VCALENDAR";
+ VCalendar vCalendar = new VCalendar(itemBody, email, vTimeZone);
+ vCalendar.fixVCalendar(false);
+ String status = vCalendar.getAttendeeStatus();
+ assertEquals("ACCEPTED", status);
+ System.out.println("'" + BundleMessage.format(status) + "'");
+ }
+
+ public void testMissingTzid() throws IOException {
+ String itemBody = "BEGIN:VCALENDAR\n" +
+ "BEGIN:VEVENT\n" +
+ "DTSTART:20100101T000000\n" +
+ "DTEND:20100102T000000\n" +
+ "END:VEVENT\n" +
+ "END:VCALENDAR";
+ String toServer = fixICS(itemBody, false);
+ System.out.println(toServer);
+ assertTrue(toServer.contains("DTSTART;TZID="));
+ assertTrue(toServer.contains("DTEND;TZID="));
+ }
+
+ public void testBroken() throws IOException {
+ String itemBody = "BEGIN:VCALENDAR\n" +
+ "PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\n" +
+ "VERSION:2.0\n" +
+ "BEGIN:VEVENT\n" +
+ "CREATED:20100916T115132Z\n" +
+ "LAST-MODIFIED:20100916T115138Z\n" +
+ "DTSTAMP:20100916T115138Z\n" +
+ "UID:d72ff8cc-f3ee-4fbc-b44d-1aaf78d92847\n" +
+ "SUMMARY:New Event\n" +
+ "DTSTART;VALUE=DATE:20100929\n" +
+ "DTEND;VALUE=DATE:20100930\n" +
+ "TRANSP:TRANSPARENT\n" +
+ "END:VEVENT\n" +
+ "END:VCALENDAR";
+ String toServer = fixICS(itemBody, true);
+ System.out.println(toServer);
+ }
+
+ public void testFloatingTimezone() throws IOException {
+ String itemBody = "BEGIN:VCALENDAR\n" +
+ "PRODID:Microsoft CDO for Microsoft Exchange\n" +
+ "VERSION:2.0\n" +
+ "BEGIN:VTIMEZONE\n" +
+ "TZID:Pacific Time (US & Canada)\\; Tijuana\n" +
+ "BEGIN:STANDARD\n" +
+ "DTSTART:16010101T030000\n" +
+ "TZOFFSETFROM:-0700\n" +
+ "TZOFFSETTO:-0800\n" +
+ "RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=11;BYDAY=1SU\n" +
+ "END:STANDARD\n" +
+ "BEGIN:DAYLIGHT\n" +
+ "DTSTART:16010101T010000\n" +
+ "TZOFFSETFROM:-0800\n" +
+ "TZOFFSETTO:-0700\n" +
+ "RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=3;BYDAY=2SU\n" +
+ "END:DAYLIGHT\n" +
+ "END:VTIMEZONE" +
+ "BEGIN:VEVENT\n" +
+ "END:VEVENT\n" +
+ "END:VCALENDAR";
+ String toServer = fixICS(itemBody, true);
+ System.out.println(toServer);
+ }
+
+ public void testAnotherBroken() throws IOException {
+ String icsBody = "BEGIN:VCALENDAR\n" +
+ "METHOD:PUBLISH\n" +
+ "PRODID:Microsoft Exchange Server 2010\n" +
+ "VERSION:2.0\n" +
+ "BEGIN:VTIMEZONE\n" +
+ "TZID:GMT -0800 (Standard) / GMT -0700 (Daylight)\\n\n" +
+ "BEGIN:STANDARD\n" +
+ "DTSTART:16010101T020000\n" +
+ "TZOFFSETFROM:-0700\n" +
+ "TZOFFSETTO:-0800\n" +
+ "RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11\n" +
+ "END:STANDARD\n" +
+ "BEGIN:DAYLIGHT\n" +
+ "DTSTART:16010101T020000\n" +
+ "TZOFFSETFROM:-0800\n" +
+ "TZOFFSETTO:-0700\n" +
+ "RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3\n" +
+ "END:DAYLIGHT\n" +
+ "END:VTIMEZONE\n" +
+ "BEGIN:VEVENT\n" +
+ "ORGANIZER;CN=John Doe:MAILTO:aTargetAddress at dummy.com\n" +
+ "DESCRIPTION;LANGUAGE=en-US:Look over broken timezone.\\n\n" +
+ "SUMMARY;LANGUAGE=en-US:meeting\n" +
+ "DTSTART;TZID=GMT -0800 (Standard) / GMT -0700 (Daylight)\n" +
+ ":20060210T130000\n" +
+ "DTEND;TZID=GMT -0800 (Standard) / GMT -0700 (Daylight)\n" +
+ ":20060210T143000\n" +
+ "UID:040000008200E00074C5B7101A82E00800000000D01FF309972CC601000000000000000\n" +
+ " 010000000B389A3C5092D7640A06D2EF5A2125577\n" +
+ "CLASS:PUBLIC\n" +
+ "PRIORITY:5\n" +
+ "DTSTAMP:20060208T180425Z\n" +
+ "TRANSP:OPAQUE\n" +
+ "STATUS:CONFIRMED\n" +
+ "SEQUENCE:0\n" +
+ "LOCATION;LANGUAGE=en-US:not sure\n" +
+ "X-MICROSOFT-CDO-APPT-SEQUENCE:0\n" +
+ "X-MICROSOFT-CDO-OWNERAPPTID:1602758614\n" +
+ "X-MICROSOFT-CDO-BUSYSTATUS:BUSY\n" +
+ "X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY\n" +
+ "X-MICROSOFT-CDO-ALLDAYEVENT:FALSE\n" +
+ "X-MICROSOFT-CDO-IMPORTANCE:1\n" +
+ "X-MICROSOFT-CDO-INSTTYPE:0\n" +
+ "X-MICROSOFT-DISALLOW-COUNTER:FALSE\n" +
+ "END:VEVENT\n" +
+ "END:VCALENDAR";
+ String toClient = fixICS(icsBody, true);
+ System.out.println(toClient);
+
+ }
+}
diff --git a/src/test/davmail/exchange/TestExchangeSessionFolder.java b/src/test/davmail/exchange/TestExchangeSessionFolder.java
new file mode 100644
index 0000000..8668b01
--- /dev/null
+++ b/src/test/davmail/exchange/TestExchangeSessionFolder.java
@@ -0,0 +1,136 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange;
+
+import davmail.Settings;
+
+import javax.mail.MessagingException;
+import java.io.IOException;
+
+/**
+ * Test folder methods.
+ */
+public class TestExchangeSessionFolder extends AbstractExchangeSessionTestCase {
+ public void testCreateFolder() throws IOException {
+ session.createMessageFolder("test");
+ }
+
+ public void testGetFolder() throws IOException {
+ ExchangeSession.Folder folder = session.getFolder("test");
+ assertNotNull(folder);
+ assertEquals("test", folder.folderPath);
+ assertEquals("test", folder.displayName);
+ assertEquals("IPF.Note", folder.folderClass);
+ assertEquals(0, folder.unreadCount);
+ assertFalse(folder.hasChildren);
+ assertFalse(folder.noInferiors);
+ assertNotNull(folder.ctag);
+ assertNotNull(folder.etag);
+ }
+
+ public void testSubFolder() throws IOException {
+ session.createMessageFolder("test/subfolder");
+ ExchangeSession.Folder folder = session.getFolder("test/subfolder");
+ assertNotNull(folder);
+ assertEquals("test/subfolder", folder.folderPath);
+ assertEquals("subfolder", folder.displayName);
+ session.deleteFolder("test/subfolder");
+ }
+
+ public void testUpdateFolder() throws IOException {
+ // TODO: implement
+ }
+
+ public void testMoveFolder() throws IOException {
+ session.deleteFolder("target");
+ session.deleteFolder("tomove");
+ session.createMessageFolder("tomove");
+ session.createMessageFolder("target");
+ session.moveFolder("tomove", "target/moved");
+ session.deleteFolder("target");
+ }
+
+ public void testDeleteFolder() throws IOException {
+ session.deleteFolder("test");
+ }
+
+ public void testCalendarFolder() throws IOException {
+ String folderName = "testcalendar";
+ session.deleteFolder(folderName);
+ session.createCalendarFolder(folderName, null);
+ ExchangeSession.Folder folder = session.getFolder(folderName);
+ assertNotNull(folder);
+ assertEquals("IPF.Appointment", folder.folderClass);
+ session.deleteFolder(folderName);
+ }
+
+ public void testContactFolder() throws IOException {
+ String folderName = "testcontact";
+ session.deleteFolder(folderName);
+ session.createContactFolder(folderName, null);
+ ExchangeSession.Folder folder = session.getFolder(folderName);
+ assertNotNull(folder);
+ assertEquals("IPF.Contact", folder.folderClass);
+ session.deleteFolder(folderName);
+ }
+
+
+ public void testFolderAccent() throws IOException {
+ String folderName = "testé";
+ session.deleteFolder(folderName);
+ session.createMessageFolder(folderName);
+ ExchangeSession.Folder folder = session.getFolder(folderName);
+ assertNotNull(folder);
+ assertEquals(folderName, folder.displayName);
+ assertEquals(folderName, folder.folderPath);
+ session.deleteFolder(folderName);
+ }
+
+ public void testFolderSpace() throws IOException {
+ String folderName = "test space";
+ session.deleteFolder(folderName);
+ session.createMessageFolder(folderName);
+ ExchangeSession.Folder folder = session.getFolder(folderName);
+ assertNotNull(folder);
+ assertEquals(folderName, folder.displayName);
+ assertEquals(folderName, folder.folderPath);
+ session.deleteFolder(folderName);
+ }
+
+ public void testSpecialFolderCharacter() throws IOException {
+ String folderName = "Special & accenté";
+ session.deleteFolder(folderName);
+ session.createMessageFolder(folderName);
+ ExchangeSession.Folder folder = session.getFolder(folderName);
+ assertNotNull(folder);
+ assertEquals(folderName, folder.displayName);
+ assertEquals(folderName, folder.folderPath);
+ session.deleteFolder(folderName);
+ }
+
+ public void testGetSharedFolder() throws IOException, MessagingException {
+ ExchangeSession.Folder folder = session.getFolder("/users/"+ Settings.getProperty("davmail.to")+"/inbox");
+ ExchangeSession.MessageList messages = session.searchMessages("/users/"+ Settings.getProperty("davmail.to")+"/inbox");
+ for (ExchangeSession.Message message:messages) {
+ System.out.println(message.getMimeMessage());
+ }
+ assertNotNull(folder);
+ }
+
+}
diff --git a/src/test/davmail/exchange/TestExchangeSessionMessage.java b/src/test/davmail/exchange/TestExchangeSessionMessage.java
new file mode 100644
index 0000000..02e5c2c
--- /dev/null
+++ b/src/test/davmail/exchange/TestExchangeSessionMessage.java
@@ -0,0 +1,150 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange;
+
+import javax.mail.MessagingException;
+import javax.mail.internet.MimeMessage;
+import javax.mail.util.SharedByteArrayInputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.UUID;
+
+/**
+ * Test message handling features.
+ */
+public class TestExchangeSessionMessage extends AbstractExchangeSessionTestCase {
+ static ExchangeSession.Message message;
+ static String messageName;
+
+ public void testCreateMessage() throws IOException, MessagingException {
+ session.deleteFolder("testfolder");
+ session.createMessageFolder("testfolder");
+ MimeMessage mimeMessage = createMimeMessage();
+ messageName = UUID.randomUUID().toString()+".EML";
+ HashMap<String, String> properties = new HashMap<String, String>();
+ properties.put("draft", "0");
+ session.createMessage("testfolder", messageName, properties, mimeMessage);
+ }
+
+ public void testSearchInbox() throws IOException, MessagingException {
+ ExchangeSession.MessageList messageList = session.searchMessages("INBOX");
+ assertNotNull(messageList);
+ }
+
+ public void testSearchMessage() throws IOException, MessagingException {
+ ExchangeSession.MessageList messageList = session.searchMessages("testfolder");
+ assertNotNull(messageList);
+ assertEquals(1, messageList.size());
+ message = messageList.get(0);
+ assertFalse(message.answered);
+ assertFalse(message.forwarded);
+ assertFalse(message.flagged);
+ assertFalse(message.draft);
+ assertTrue(message.size > 0);
+ assertFalse(message.deleted);
+ assertFalse(message.read);
+ assertNotNull(message.date);
+ }
+
+ public void testFlagMessage() throws IOException, MessagingException {
+ ExchangeSession.Folder testFolder = session.getFolder("testfolder");
+ testFolder.loadMessages();
+ HashMap<String,String> properties = new HashMap<String,String>();
+ properties.put("flagged", "2");
+ session.updateMessage(message, properties);
+
+ // refresh folder
+ testFolder.loadMessages();
+ assertNotNull(testFolder.get(0));
+ assertTrue(testFolder.get(0).flagged);
+ assertEquals(message.getImapUid(), testFolder.get(0).getImapUid());
+ }
+
+ public void testGetContent() throws IOException, MessagingException {
+ byte[] content = session.getContent(message);
+ assertNotNull(content);
+ MimeMessage mimeMessage = new MimeMessage(null, new SharedByteArrayInputStream(content));
+ assertTrue(mimeMessage.getHeader("To")[0].indexOf("test at test.local") >= 0);
+ assertEquals("Test subject", mimeMessage.getSubject());
+ assertEquals("Test message\n", mimeMessage.getContent());
+ }
+
+ public void testProcessMessage() throws IOException, MessagingException {
+ session.processItem("testfolder", messageName);
+ }
+
+ public void testFolderUidNext() throws IOException, MessagingException {
+ ExchangeSession.Folder folder = session.getFolder("testfolder");
+ assertTrue(folder.uidNext > 0);
+ }
+
+ public void testDeleteMessage() throws IOException {
+ session.deleteMessage(message);
+ ExchangeSession.MessageList messageList = session.searchMessages("testfolder");
+ assertNotNull(messageList);
+ assertEquals(0, messageList.size());
+ }
+
+ public void testSpecialMessageCharacter() throws IOException, MessagingException {
+ session.deleteFolder("testfolder");
+ session.createMessageFolder("testfolder");
+ MimeMessage mimeMessage = createMimeMessage();
+ messageName = "Special & accenté.EML";
+ HashMap<String, String> properties = new HashMap<String, String>();
+ properties.put("draft", "0");
+ session.createMessage("testfolder", messageName, properties, mimeMessage);
+ ExchangeSession.MessageList messageList = session.searchMessages("testfolder", session.isEqualTo("urlcompname", messageName));
+ assertNotNull(messageList);
+ assertEquals(1, messageList.size());
+ }
+
+ public void testSlashMessageName() throws IOException, MessagingException {
+ session.deleteFolder("testfolder");
+ session.createMessageFolder("testfolder");
+ MimeMessage mimeMessage = createMimeMessage();
+ messageName = "test _xF8FF_ slash.EML";
+ HashMap<String, String> properties = new HashMap<String, String>();
+ properties.put("draft", "0");
+ session.createMessage("testfolder", messageName, properties, mimeMessage);
+ ExchangeSession.MessageList messageList = session.searchMessages("testfolder", session.isEqualTo("urlcompname", messageName));
+ assertNotNull(messageList);
+ assertEquals(1, messageList.size());
+ }
+
+ public void testPlusMessageName() throws IOException, MessagingException {
+ // fails on Exchange 2003
+ session.deleteFolder("testfolder");
+ session.createMessageFolder("testfolder");
+ MimeMessage mimeMessage = createMimeMessage();
+ messageName = "test + plus.EML";
+ HashMap<String, String> properties = new HashMap<String, String>();
+ properties.put("draft", "0");
+ session.createMessage("testfolder", messageName, properties, mimeMessage);
+ ExchangeSession.MessageList messageList = session.searchMessages("testfolder", session.isEqualTo("urlcompname", messageName));
+ assertNotNull(messageList);
+ assertEquals(1, messageList.size());
+ }
+
+ /**
+ * Cleanup
+ */
+ public void testDeleteFolder() throws IOException {
+ session.deleteFolder("testfolder");
+ }
+}
diff --git a/src/test/davmail/exchange/TestExchangeSessionMessageFlags.java b/src/test/davmail/exchange/TestExchangeSessionMessageFlags.java
new file mode 100644
index 0000000..4987922
--- /dev/null
+++ b/src/test/davmail/exchange/TestExchangeSessionMessageFlags.java
@@ -0,0 +1,115 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange;
+
+import davmail.Settings;
+import org.apache.log4j.Level;
+
+import javax.mail.MessagingException;
+import javax.mail.internet.MimeMessage;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.UUID;
+
+/**
+ * Test message flag update
+ */
+public class TestExchangeSessionMessageFlags extends AbstractExchangeSessionTestCase {
+
+ @Override
+ public void setUp() throws IOException {
+ super.setUp();
+ // recreate empty folder
+ session.deleteFolder("testfolder");
+ session.createMessageFolder("testfolder");
+ }
+
+ public void testCreateDraftMessage() throws MessagingException, IOException {
+ MimeMessage mimeMessage = createMimeMessage();
+ String messageName = UUID.randomUUID().toString()+".EML";
+ HashMap<String, String> properties = new HashMap<String, String>();
+ properties.put("draft", "9");
+ session.createMessage("testfolder", messageName, properties, mimeMessage);
+ ExchangeSession.MessageList messageList = session.searchMessages("testfolder");
+ assertNotNull(messageList);
+ assertEquals(1, messageList.size());
+ assertTrue(messageList.get(0).draft);
+ }
+
+ public void testCreateDraftReadMessage() throws MessagingException, IOException {
+ MimeMessage mimeMessage = createMimeMessage();
+ String messageName = UUID.randomUUID().toString();
+ HashMap<String, String> properties = new HashMap<String, String>();
+ properties.put("draft", "9");
+ session.createMessage("testfolder", messageName, properties, mimeMessage);
+ ExchangeSession.MessageList messageList = session.searchMessages("testfolder");
+ assertNotNull(messageList);
+ assertEquals(1, messageList.size());
+ assertTrue(messageList.get(0).draft);
+ assertTrue(messageList.get(0).read);
+ }
+
+ public void testCreateReadMessage() throws MessagingException, IOException {
+ MimeMessage mimeMessage = createMimeMessage();
+ String messageName = UUID.randomUUID().toString();
+ HashMap<String, String> properties = new HashMap<String, String>();
+ properties.put("draft", "1");
+ session.createMessage("testfolder", messageName, properties, mimeMessage);
+ ExchangeSession.MessageList messageList = session.searchMessages("testfolder");
+ assertNotNull(messageList);
+ assertEquals(1, messageList.size());
+ assertFalse(messageList.get(0).draft);
+ assertTrue(messageList.get(0).read);
+ }
+
+ public void testCreateBccMessage() throws MessagingException, IOException {
+ Settings.setLoggingLevel("httpclient.wire", Level.DEBUG);
+ MimeMessage mimeMessage = createMimeMessage();
+ String messageName = UUID.randomUUID().toString();
+ HashMap<String, String> properties = new HashMap<String, String>();
+ properties.put("draft", "8");
+ properties.put("bcc", "testbcc at test.local");
+ session.createMessage("testfolder", messageName, properties, mimeMessage);
+ ExchangeSession.MessageList messageList = session.searchMessages("testfolder");
+ assertNotNull(messageList);
+ assertEquals(1, messageList.size());
+ }
+
+ public void testCreateDateReceivedMessage() throws MessagingException, IOException {
+ MimeMessage mimeMessage = createMimeMessage();
+ String messageName = UUID.randomUUID().toString();
+ HashMap<String, String> properties = new HashMap<String, String>();
+ SimpleDateFormat dateFormatter = ExchangeSession.getExchangeZuluDateFormat();
+ dateFormatter.setTimeZone(ExchangeSession.GMT_TIMEZONE);
+ Calendar cal = Calendar.getInstance();
+ cal.add(Calendar.MONTH, -1);
+ properties.put("datereceived", dateFormatter.format(cal.getTime()));
+ session.createMessage("testfolder", messageName, properties, mimeMessage);
+ ExchangeSession.MessageList messageList = session.searchMessages("testfolder");
+ assertNotNull(messageList);
+ assertEquals(1, messageList.size());
+ assertNotNull(messageList);
+ // TODO: use same format for date read/write
+ assertEquals(ExchangeSession.getZuluDateFormat().format(cal.getTime()), messageList.get(0).date);
+ }
+
+
+}
diff --git a/src/test/davmail/exchange/TestExchangeSessionSearchContact.java b/src/test/davmail/exchange/TestExchangeSessionSearchContact.java
new file mode 100644
index 0000000..23c1b51
--- /dev/null
+++ b/src/test/davmail/exchange/TestExchangeSessionSearchContact.java
@@ -0,0 +1,104 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange;
+
+import davmail.Settings;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Test contact search.
+ */
+ at SuppressWarnings({"UseOfSystemOutOrSystemErr"})
+public class TestExchangeSessionSearchContact extends AbstractExchangeSessionTestCase {
+ public void testSearchPublicContacts() throws IOException {
+ String folderPath = Settings.getProperty("davmail.publicContactFolder");
+ List<ExchangeSession.Contact> contacts = session.searchContacts(folderPath, ExchangeSession.CONTACT_ATTRIBUTES, null, 0);
+ int count = 0;
+ for (ExchangeSession.Contact contact : contacts) {
+ System.out.println("Contact " + (++count) + '/' + contacts.size() + session.getItem(folderPath, contact.getName()));
+ }
+ }
+
+ public void testSearchPublicContactsRange() throws IOException {
+ String folderPath = Settings.getProperty("davmail.publicContactFolder");
+ List<ExchangeSession.Contact> contacts = session.searchContacts(folderPath, ExchangeSession.CONTACT_ATTRIBUTES, null, 10);
+ assertEquals(10, contacts.size());
+ }
+
+ public void testSearchPublicContactsWithPicture() throws IOException {
+ String folderPath = Settings.getProperty("davmail.publicContactFolder");
+ List<ExchangeSession.Contact> contacts = session.searchContacts(folderPath, ExchangeSession.CONTACT_ATTRIBUTES, session.isTrue("haspicture"), 0);
+ int count = 0;
+ for (ExchangeSession.Contact contact : contacts) {
+ System.out.println("Contact " + (++count) + '/' + contacts.size() + contact.getBody());
+ assertNotNull(session.getContactPhoto(contact));
+ }
+ }
+
+ public void testSearchContacts() throws IOException {
+ List<ExchangeSession.Contact> contacts = session.searchContacts(ExchangeSession.CONTACTS, ExchangeSession.CONTACT_ATTRIBUTES, null, 0);
+ for (ExchangeSession.Contact contact : contacts) {
+ System.out.println(session.getItem(ExchangeSession.CONTACTS, contact.getName()));
+ }
+ }
+
+ public void testSearchContactsUidOnly() throws IOException {
+ Set<String> attributes = new HashSet<String>();
+ attributes.add("uid");
+ List<ExchangeSession.Contact> contacts = session.searchContacts(ExchangeSession.CONTACTS, attributes, null, 0);
+ for (ExchangeSession.Contact contact : contacts) {
+ System.out.println(contact);
+ }
+ }
+
+ public void testSearchContactsByUid() throws IOException {
+ Set<String> attributes = new HashSet<String>();
+ attributes.add("uid");
+ List<ExchangeSession.Contact> contacts = session.searchContacts(ExchangeSession.CONTACTS, attributes, null, 0);
+ for (ExchangeSession.Contact contact : contacts) {
+ System.out.println(session.searchContacts(ExchangeSession.CONTACTS, attributes, session.isEqualTo("uid", contact.get("uid")), 0));
+ }
+ }
+
+ public void testGalFind() throws IOException {
+ // find a set of contacts
+ Map<String, ExchangeSession.Contact> contacts = session.galFind(session.startsWith("cn", "a"), null, 100);
+ for (ExchangeSession.Contact contact : contacts.values()) {
+ System.out.println(contact);
+ }
+ if (!contacts.isEmpty()) {
+ ExchangeSession.Contact testContact = contacts.values().iterator().next();
+ contacts = session.galFind(session.isEqualTo("cn", testContact.get("cn")), null, 100);
+ assertEquals(1, contacts.size());
+ contacts = session.galFind(session.isEqualTo("smtpemail1", testContact.get("smtpemail1")), null, 100);
+ assertEquals(1, contacts.size());
+ contacts = session.galFind(session.startsWith("smtpemail1", testContact.get("smtpemail1")), null, 100);
+ assertEquals(1, contacts.size());
+ contacts = session.galFind(session.and(session.isEqualTo("cn", testContact.get("cn")),
+ session.startsWith("smtpemail1", testContact.get("smtpemail1"))), null, 100);
+ assertEquals(1, contacts.size());
+ }
+ }
+
+}
diff --git a/src/test/davmail/exchange/TestICSBufferedReader.java b/src/test/davmail/exchange/TestICSBufferedReader.java
new file mode 100644
index 0000000..d223f30
--- /dev/null
+++ b/src/test/davmail/exchange/TestICSBufferedReader.java
@@ -0,0 +1,82 @@
+package davmail.exchange;
+
+import junit.framework.TestCase;
+
+import java.io.StringReader;
+import java.io.IOException;
+
+/**
+ * Test ICSBufferedReader
+ */
+public class TestICSBufferedReader extends TestCase {
+ public void testSimpleRead() throws IOException {
+ String value = "test\nmultiline\nstring";
+ ICSBufferedReader reader = new ICSBufferedReader(new StringReader(value));
+ assertEquals("test", reader.readLine());
+ assertEquals("multiline", reader.readLine());
+ assertEquals("string", reader.readLine());
+ assertNull(reader.readLine());
+ }
+
+ public void testContinuationRead() throws IOException {
+ String value = "test\nmultiline\n string";
+ ICSBufferedReader reader = new ICSBufferedReader(new StringReader(value));
+ assertEquals("test", reader.readLine());
+ assertEquals("multilinestring", reader.readLine());
+ assertNull(reader.readLine());
+ }
+
+ public void testEventWithEmptyLine() throws IOException {
+ String value = "BEGIN:VCALENDAR\n" +
+ "CALSCALE:GREGORIAN\n" +
+ "METHOD:REQUEST\n" +
+ "PRODID:Microsoft CDO for Microsoft Exchange\n" +
+ "VERSION:2.0\n" +
+ "BEGIN:VTIMEZONE\n" +
+ "TZID:Africa/Lagos\n" +
+ "X-MICROSOFT-CDO-TZID:69\n" +
+ "BEGIN:STANDARD\n" +
+ "DTSTART:16010101T000000\n" +
+ "TZOFFSETFROM:+0100\n" +
+ "TZOFFSETTO:+0100\n" +
+ "END:STANDARD\n" +
+ "BEGIN:DAYLIGHT\n" +
+ "DTSTART:16010101T000000\n" +
+ "TZOFFSETFROM:+0100\n" +
+ "TZOFFSETTO:+0100\n" +
+ "END:DAYLIGHT\n" +
+ "END:VTIMEZONE\n" +
+ "BEGIN:VEVENT\n" +
+ "DTSTART;TZID=\"Africa/Lagos\":20070326T070000\n" +
+ "DTEND;TZID=\"Africa/Lagos\":20070326T083000\n" +
+ "DTSTAMP:20070217T231150Z\n" +
+ "SUMMARY:My meeting\n" +
+ "CATEGORIES:Groupcal,iCal:user\n" +
+ "UID:com.apple.syncservices:5C1BCD60-8C8E-4FCE-B2CA-C99DE0BE81EB\n" +
+ "RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,FR\n" +
+ "ORGANIZER:MAILTO:user at domain\n" +
+ "\n" +
+ "X-GROUPCAL-ALARM:PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0\n" +
+ "NUWVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUgQ29tcHV0ZXIvL0RURCBQTElTVCAxLjAvL0VOI\n" +
+ "iAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4wLmR0ZCI+CjxwbGlz\n" +
+ "dCB2ZXJzaW9uPSIxLjAiPgo8YXJyYXkvPgo8L3BsaXN0Pgo=\n" +
+ "CLASS:\n" +
+ "STATUS:TENTATIVE\n" +
+ "TRANSP:OPAQUE\n" +
+ "X-MICROSOFT-CDO-BUSYSTATUS:OOF\n" +
+ "X-MICROSOFT-CDO-INSTTYPE:1\n" +
+ "X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY\n" +
+ "X-MICROSOFT-CDO-ALLDAYEVENT:FALSE\n" +
+ "X-MICROSOFT-CDO-IMPORTANCE:1\n" +
+ "X-MICROSOFT-CDO-OWNERAPPTID:-1\n" +
+ "END:VEVENT\n" +
+ "END:VCALENDAR";
+ ICSBufferedReader reader = new ICSBufferedReader(new StringReader(value));
+ String line;
+ String lastLine = null;
+ while ((line =reader.readLine())!= null) {
+ lastLine = line;
+ }
+ assertEquals("END:VCALENDAR", lastLine);
+ }
+}
diff --git a/src/test/davmail/exchange/TestVProperty.java b/src/test/davmail/exchange/TestVProperty.java
new file mode 100644
index 0000000..3ef262a
--- /dev/null
+++ b/src/test/davmail/exchange/TestVProperty.java
@@ -0,0 +1,40 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2011 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange;
+
+import junit.framework.TestCase;
+
+/**
+ * Test VProperty.
+ */
+public class TestVProperty extends TestCase {
+ public void testMultivaluedParam() {
+ String line = "TEL;TYPE=home,voice:homePhone";
+ VProperty vProperty = new VProperty(line);
+ assertNotNull(vProperty);
+ assertEquals(line, vProperty.toString());
+ }
+
+ public void testQuoteCn() {
+ String line = "ATTENDEE;CN=\"test\":MAILTO:test at company.com";
+ VProperty vProperty = new VProperty(line);
+ assertNotNull(vProperty);
+ assertEquals(line, vProperty.toString());
+ }
+}
diff --git a/src/test/davmail/exchange/dav/TestDavExchangeSession.java b/src/test/davmail/exchange/dav/TestDavExchangeSession.java
new file mode 100644
index 0000000..d9cf457
--- /dev/null
+++ b/src/test/davmail/exchange/dav/TestDavExchangeSession.java
@@ -0,0 +1,191 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.dav;
+
+import davmail.exchange.AbstractExchangeSessionTestCase;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.httpclient.HttpStatus;
+import org.apache.jackrabbit.webdav.MultiStatusResponse;
+import org.apache.jackrabbit.webdav.property.DavProperty;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Webdav specific unit tests
+ */
+ at SuppressWarnings({"UseOfSystemOutOrSystemErr"})
+public class TestDavExchangeSession extends AbstractExchangeSessionTestCase {
+ DavExchangeSession davSession;
+
+ /**
+ * @inheritDoc
+ */
+ @Override
+ public void setUp() throws IOException {
+ super.setUp();
+ davSession = ((DavExchangeSession) session);
+ }
+
+ /**
+ * Test exchange folder path mapping
+ */
+ public void testGetFolderPath() {
+ String mailPath = davSession.getFolderPath("");
+ String rootPath = davSession.getFolderPath("/users/");
+
+ assertEquals(mailPath + davSession.inboxName, davSession.getFolderPath("INBOX"));
+ assertEquals(mailPath + davSession.deleteditemsName, davSession.getFolderPath("Trash"));
+ assertEquals(mailPath + davSession.sentitemsName, davSession.getFolderPath("Sent"));
+ assertEquals(mailPath + davSession.draftsName, davSession.getFolderPath("Drafts"));
+
+ assertEquals(mailPath + davSession.contactsName, davSession.getFolderPath("contacts"));
+ assertEquals(mailPath + davSession.calendarName, davSession.getFolderPath("calendar"));
+
+ assertEquals(mailPath + davSession.inboxName + "/test", davSession.getFolderPath("INBOX/test"));
+ assertEquals(mailPath + davSession.deleteditemsName + "/test", davSession.getFolderPath("Trash/test"));
+ assertEquals(mailPath + davSession.sentitemsName + "/test", davSession.getFolderPath("Sent/test"));
+ assertEquals(mailPath + davSession.draftsName + "/test", davSession.getFolderPath("Drafts/test"));
+
+ // TODO: may be wrong, should return full url, public folders may be located on another server
+ assertEquals("/public", davSession.getFolderPath("/public"));
+ assertEquals("/public/test", davSession.getFolderPath("/public/test"));
+
+ // caldav folder paths
+ assertEquals(mailPath, davSession.getFolderPath("/users/" + davSession.getEmail()));
+ assertEquals(mailPath, davSession.getFolderPath("/users/" + davSession.getEmail() + '/'));
+ assertEquals(mailPath, davSession.getFolderPath("/users/" + davSession.getAlias()));
+ assertEquals(mailPath, davSession.getFolderPath("/users/" + davSession.getAlias() + '/'));
+
+ assertEquals(mailPath, davSession.getFolderPath("/users/" + davSession.getEmail().toUpperCase()));
+ assertEquals(mailPath, davSession.getFolderPath("/users/" + davSession.getEmail().toLowerCase()));
+ assertEquals(mailPath, davSession.getFolderPath("/users/" + davSession.getAlias().toUpperCase()));
+ assertEquals(mailPath, davSession.getFolderPath("/users/" + davSession.getAlias().toLowerCase()));
+
+
+ assertEquals(mailPath + "subfolder", davSession.getFolderPath("/users/" + davSession.getAlias() + "/subfolder"));
+ assertEquals(mailPath + "subfolder/", davSession.getFolderPath("/users/" + davSession.getAlias() + "/subfolder/"));
+
+ assertEquals(rootPath + "anotheruser/", davSession.getFolderPath("/users/anotheruser"));
+ assertEquals(rootPath + "anotheruser/subfolder", davSession.getFolderPath("/users/anotheruser/subfolder"));
+
+ assertEquals(mailPath + davSession.inboxName, davSession.getFolderPath("/users/" + davSession.getEmail() + "/inbox"));
+ assertEquals(mailPath + davSession.inboxName + "/subfolder", davSession.getFolderPath("/users/" + davSession.getEmail() + "/inbox/subfolder"));
+
+ assertEquals(mailPath + davSession.calendarName, davSession.getFolderPath("/users/" + davSession.getEmail() + "/calendar"));
+ assertEquals(mailPath + davSession.contactsName, davSession.getFolderPath("/users/" + davSession.getEmail() + "/contacts"));
+ assertEquals(mailPath + davSession.contactsName, davSession.getFolderPath("/users/" + davSession.getEmail() + "/addressbook"));
+
+ assertEquals(rootPath + "anotherUser/" + davSession.inboxName, davSession.getFolderPath("/users/anotherUser/inbox"));
+ assertEquals(rootPath + "anotherUser/" + davSession.calendarName, davSession.getFolderPath("/users/anotherUser/calendar"));
+ assertEquals(rootPath + "anotherUser/" + davSession.contactsName, davSession.getFolderPath("/users/anotherUser/contacts"));
+
+ // do not replace i18n names
+ assertEquals(mailPath + "Inbox", davSession.getFolderPath("/users/" + davSession.getEmail() + "/Inbox"));
+ assertEquals(mailPath + "Calendar", davSession.getFolderPath("/users/" + davSession.getEmail() + "/Calendar"));
+ assertEquals(mailPath + "Contacts", davSession.getFolderPath("/users/" + davSession.getEmail() + "/Contacts"));
+ }
+
+ /**
+ * Get main category list
+ *
+ * @throws IOException on error
+ */
+ public void testGetCategoryList() throws IOException {
+ Set<String> attributes = new HashSet<String>();
+ attributes.add("permanenturl");
+ attributes.add("roamingxmlstream");
+ MultiStatusResponse[] responses = davSession.searchItems("/users/" + davSession.getEmail() + "/calendar", attributes, davSession.and(davSession.isFalse("isfolder"), davSession.isEqualTo("messageclass", "IPM.Configuration.CategoryList")), DavExchangeSession.FolderQueryTraversal.Shallow, 0);
+ String value = (String) responses[0].getProperties(HttpStatus.SC_OK).get(Field.getPropertyName("roamingxmlstream")).getValue();
+ String propertyList = new String(Base64.decodeBase64(value.getBytes()), "UTF-8");
+ System.out.println(propertyList);
+ }
+
+ /**
+ * Find calendar options
+ *
+ * @throws IOException on error
+ */
+ public void testGetCalendarOptions() throws IOException {
+ Set<String> attributes = new HashSet<String>();
+ attributes.add("permanenturl");
+ attributes.add("roamingxmlstream");
+ MultiStatusResponse[] responses = davSession.searchItems("/users/" + davSession.getEmail() + "/calendar", attributes, davSession.and(davSession.isFalse("isfolder"), davSession.isEqualTo("messageclass", "IPM.Configuration.Calendar")), DavExchangeSession.FolderQueryTraversal.Shallow, 0);
+ String value = (String) responses[0].getProperties(HttpStatus.SC_OK).get(Field.getPropertyName("roamingxmlstream")).getValue();
+ String propertyList = new String(Base64.decodeBase64(value.getBytes()), "UTF-8");
+ System.out.println(propertyList);
+ }
+
+ /**
+ * Retrieve all hidden items
+ *
+ * @throws IOException on error
+ */
+ public void testAllHidden() throws IOException {
+ Set<String> attributes = new HashSet<String>();
+ attributes.add("messageclass");
+ attributes.add("permanenturl");
+ attributes.add("roamingxmlstream");
+ attributes.add("displayname");
+
+ MultiStatusResponse[] responses = davSession.searchItems("/users/" + davSession.getEmail() + '/', attributes, davSession.and(davSession.isTrue("ishidden")), DavExchangeSession.FolderQueryTraversal.Deep, 0);
+ for (MultiStatusResponse response : responses) {
+ System.out.println(response.getProperties(HttpStatus.SC_OK).get(Field.getPropertyName("messageclass")).getValue() + ": "
+ + response.getProperties(HttpStatus.SC_OK).get(Field.getPropertyName("displayname")).getValue());
+
+ DavProperty roamingxmlstreamProperty = response.getProperties(HttpStatus.SC_OK).get(Field.getPropertyName("roamingxmlstream"));
+ if (roamingxmlstreamProperty != null) {
+ System.out.println(new String(Base64.decodeBase64(((String) roamingxmlstreamProperty.getValue()).getBytes()), "UTF-8"));
+ }
+
+ }
+ }
+
+ /**
+ * Search in non ipm subtree
+ *
+ * @throws IOException on error
+ */
+ public void testNonIpmSubtree() throws IOException {
+ Set<String> attributes = new HashSet<String>();
+ attributes.add("messageclass");
+ attributes.add("permanenturl");
+ attributes.add("roamingxmlstream");
+ attributes.add("roamingdictionary");
+ attributes.add("displayname");
+
+ MultiStatusResponse[] responses = davSession.searchItems("/users/" + davSession.getEmail() + "/non_ipm_subtree", attributes, davSession.and(davSession.isTrue("ishidden")), DavExchangeSession.FolderQueryTraversal.Deep, 0);
+ for (MultiStatusResponse response : responses) {
+ System.out.println(response.getHref() + ' ' + response.getProperties(HttpStatus.SC_OK).get(Field.getPropertyName("messageclass")).getValue() + ": "
+ + response.getProperties(HttpStatus.SC_OK).get(Field.getPropertyName("displayname")).getValue());
+
+ DavProperty roamingxmlstreamProperty = response.getProperties(HttpStatus.SC_OK).get(Field.getPropertyName("roamingxmlstream"));
+ if (roamingxmlstreamProperty != null) {
+ System.out.println("roamingxmlstream: " + new String(Base64.decodeBase64(((String) roamingxmlstreamProperty.getValue()).getBytes()), "UTF-8"));
+ }
+
+ DavProperty roamingdictionaryProperty = response.getProperties(HttpStatus.SC_OK).get(Field.getPropertyName("roamingdictionary"));
+ if (roamingdictionaryProperty != null) {
+ System.out.println("roamingdictionary: " + new String(Base64.decodeBase64(((String) roamingdictionaryProperty.getValue()).getBytes()), "UTF-8"));
+ }
+ }
+ }
+
+}
diff --git a/src/test/davmail/exchange/ews/TestEwsExchangeSession.java b/src/test/davmail/exchange/ews/TestEwsExchangeSession.java
new file mode 100644
index 0000000..894998b
--- /dev/null
+++ b/src/test/davmail/exchange/ews/TestEwsExchangeSession.java
@@ -0,0 +1,80 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.exchange.ews;
+
+import davmail.exchange.AbstractExchangeSessionTestCase;
+import davmail.exchange.ExchangeSession;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Webdav specific unit tests
+ */
+ at SuppressWarnings({"UseOfSystemOutOrSystemErr"})
+public class TestEwsExchangeSession extends AbstractExchangeSessionTestCase {
+ EwsExchangeSession ewsSession;
+
+ public void setUp() throws IOException {
+ super.setUp();
+ ewsSession = ((EwsExchangeSession) session);
+ }
+
+ public void testResolveNames() throws IOException {
+ ResolveNamesMethod resolveNamesMethod = new ResolveNamesMethod("smtp:g");
+ ewsSession.executeMethod(resolveNamesMethod);
+ List<EWSMethod.Item> items = resolveNamesMethod.getResponseItems();
+ for (EWSMethod.Item item : items) {
+ System.out.println(item);
+ }
+ }
+
+ public void testGalFind() throws IOException {
+ // find a set of contacts
+ Map<String, ExchangeSession.Contact> contacts = ewsSession.galFind(ewsSession.startsWith("cn", "a"), null, 100);
+ for (ExchangeSession.Contact contact : contacts.values()) {
+ System.out.println(contact);
+ }
+ if (!contacts.isEmpty()) {
+ ExchangeSession.Contact testContact = contacts.values().iterator().next();
+ contacts = ewsSession.galFind(ewsSession.isEqualTo("cn", testContact.get("cn")), null, 100);
+ assertEquals(1, contacts.size());
+ contacts = ewsSession.galFind(ewsSession.isEqualTo("email1", testContact.get("email1")), null, 100);
+ assertEquals(1, contacts.size());
+ contacts = ewsSession.galFind(ewsSession.startsWith("email1", testContact.get("email1")), null, 100);
+ assertEquals(1, contacts.size());
+ contacts = ewsSession.galFind(ewsSession.and(ewsSession.isEqualTo("cn", testContact.get("cn")),
+ ewsSession.startsWith("email1", testContact.get("email1"))), null, 100);
+ assertEquals(1, contacts.size());
+ }
+ }
+
+ public void testGetUserConfiguration() throws IOException {
+ GetUserConfigurationMethod getUserConfigurationMethod = new GetUserConfigurationMethod();
+ ewsSession.executeMethod(getUserConfigurationMethod);
+ EWSMethod.Item item = getUserConfigurationMethod.getResponseItem();
+ assertNotNull(item);
+ }
+
+ public void testTimezone() {
+ ewsSession.loadVtimezone();
+ }
+
+}
diff --git a/src/test/davmail/imap/AbstractImapTestCase.java b/src/test/davmail/imap/AbstractImapTestCase.java
new file mode 100644
index 0000000..1eef370
--- /dev/null
+++ b/src/test/davmail/imap/AbstractImapTestCase.java
@@ -0,0 +1,82 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2011 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.imap;
+
+import davmail.AbstractDavMailTestCase;
+import davmail.DavGateway;
+import davmail.Settings;
+
+import java.io.*;
+import java.net.Socket;
+
+/**
+ * Abstract IMAP test case.
+ */
+public class AbstractImapTestCase extends AbstractDavMailTestCase {
+ static Socket clientSocket;
+ static BufferedWriter socketWriter;
+ static BufferedReader socketReader;
+
+ static String messageUid;
+
+ protected void write(String line) throws IOException {
+ socketWriter.write(line);
+ socketWriter.flush();
+ }
+
+ protected void writeLine(String line) throws IOException {
+ socketWriter.write(line);
+ socketWriter.newLine();
+ socketWriter.flush();
+ }
+
+ protected String readLine() throws IOException {
+ return socketReader.readLine();
+ }
+
+ protected String readFullAnswer(String prefix) throws IOException {
+ String line = socketReader.readLine();
+ while (!line.startsWith(prefix)) {
+ line = socketReader.readLine();
+ }
+ return line;
+ }
+
+ @Override
+ public void setUp() throws IOException {
+ boolean needStart = !loaded;
+ super.setUp();
+ if (needStart) {
+ // start gateway
+ DavGateway.start();
+ }
+ if (clientSocket == null) {
+ clientSocket = new Socket("localhost", Settings.getIntProperty("davmail.imapPort"));
+ socketWriter = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()));
+ socketReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
+
+ String banner = socketReader.readLine();
+ assertNotNull(banner);
+
+ writeLine(". LOGIN " + Settings.getProperty("davmail.username").replaceAll("\\\\", "\\\\\\\\") + ' ' + Settings.getProperty("davmail.password"));
+ assertEquals(". OK Authenticated", socketReader.readLine());
+ }
+ }
+
+}
diff --git a/src/test/davmail/imap/TestImap.java b/src/test/davmail/imap/TestImap.java
new file mode 100644
index 0000000..ef10471
--- /dev/null
+++ b/src/test/davmail/imap/TestImap.java
@@ -0,0 +1,397 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.imap;
+
+import davmail.Settings;
+
+import javax.mail.MessagingException;
+import javax.mail.Session;
+import javax.mail.internet.MimeMessage;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Random;
+
+/**
+ * IMAP tests, an instance of DavMail Gateway must be available
+ */
+ at SuppressWarnings({"JavaDoc", "UseOfSystemOutOrSystemErr"})
+public class TestImap extends AbstractImapTestCase {
+
+ public void testListFolders() throws IOException {
+ writeLine(". LSUB \"\" \"*\"");
+ assertEquals(". OK LSUB completed", readFullAnswer("."));
+ }
+
+ public void testListSubFolders() throws IOException {
+ writeLine(". LIST \"\" \"INBOX*\"");
+ assertEquals(". OK LIST completed", readFullAnswer("."));
+ }
+
+ public void testSelectInbox() throws IOException {
+ writeLine(". SELECT INBOX");
+ assertEquals(". OK [READ-WRITE] SELECT completed", readFullAnswer("."));
+ }
+
+ public void testSelectRoot() throws IOException {
+ writeLine(". SELECT \"\"");
+ assertEquals(". OK [READ-WRITE] SELECT completed", readFullAnswer("."));
+ }
+
+ public void testFetchFlags() throws IOException {
+ writeLine(". UID FETCH 1:* (FLAGS)");
+ assertEquals(". OK UID FETCH completed", readFullAnswer("."));
+ }
+
+ public void testUidSearchUnDeleted() throws IOException {
+ writeLine(". UID SEARCH UNDELETED");
+ assertEquals(". OK SEARCH completed", readFullAnswer("."));
+ writeLine(". UID SEARCH NOT DELETED");
+ assertEquals(". OK SEARCH completed", readFullAnswer("."));
+ }
+
+ public void testUidSearchdeleted() throws IOException {
+ writeLine(". UID SEARCH DELETED");
+ assertEquals(". OK SEARCH completed", readFullAnswer("."));
+ }
+
+ public void testStoreUndelete() throws IOException {
+ writeLine(". UID STORE 10 -FLAGS (\\Deleted)");
+ readFullAnswer(".");
+ }
+
+ public void testCreateFolder() throws IOException {
+ writeLine(". DELETE testfolder");
+ readFullAnswer(".");
+ writeLine(". CREATE testfolder");
+ assertEquals(". OK folder created", readFullAnswer("."));
+ writeLine(". SELECT testfolder");
+ assertEquals(". OK [READ-WRITE] SELECT completed", readFullAnswer("."));
+ }
+
+ public void testFetchBigMessage() throws IOException, MessagingException {
+ testCreateFolder();
+ // create 10MB message
+ MimeMessage mimeMessage = new MimeMessage((Session) null);
+ mimeMessage.addHeader("to", Settings.getProperty("davmail.to"));
+ mimeMessage.addHeader("bcc", Settings.getProperty("davmail.bcc"));
+ Random random = new Random();
+ StringBuilder randomText = new StringBuilder();
+ for (int i = 0; i < 10 * 1024 * 1024; i++) {
+ randomText.append((char) ('a' + random.nextInt(26)));
+ }
+ mimeMessage.setText(randomText.toString());
+ mimeMessage.setSubject("Big subject");
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ mimeMessage.writeTo(baos);
+ byte[] content = baos.toByteArray();
+ long start = System.currentTimeMillis();
+ writeLine(". APPEND testfolder (\\Seen \\Draft) {" + content.length + '}');
+ assertEquals("+ send literal data", readLine());
+ writeLine(new String(content));
+ assertEquals(". OK APPEND completed", readFullAnswer("."));
+ System.out.println("Create time: " + (System.currentTimeMillis() - start) + " ms");
+ writeLine(". NOOP");
+ assertEquals(". OK NOOP completed", readFullAnswer("."));
+ start = System.currentTimeMillis();
+ writeLine(". UID FETCH 1:* (RFC822.SIZE BODY.TEXT)");
+ readFullAnswer(".");
+ System.out.println("Fetch time: " + (System.currentTimeMillis() - start) + " ms");
+
+ }
+
+
+ public void testSelectFolder() throws IOException {
+ writeLine(". SELECT testfolder");
+ assertEquals(". OK [READ-WRITE] SELECT completed", readFullAnswer("."));
+ }
+
+ public void testCreateMessage() throws IOException, MessagingException {
+
+ MimeMessage mimeMessage = new MimeMessage((Session) null);
+ mimeMessage.addHeader("to", Settings.getProperty("davmail.to"));
+ mimeMessage.addHeader("bcc", Settings.getProperty("davmail.bcc"));
+ mimeMessage.setText("Test message");
+ mimeMessage.setSubject("Test subject");
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ mimeMessage.writeTo(baos);
+ byte[] content = baos.toByteArray();
+ writeLine(". APPEND testfolder (\\Seen \\Draft) {" + content.length + '}');
+ assertEquals("+ send literal data", readLine());
+ writeLine(new String(content));
+ assertEquals(". OK APPEND completed", readFullAnswer("."));
+ writeLine(". NOOP");
+ assertEquals(". OK NOOP completed", readFullAnswer("."));
+
+ // fetch message uid
+ writeLine(". UID FETCH 1:* (FLAGS)");
+ String messageLine = readLine();
+ int uidIndex = messageLine.indexOf("UID ") + 4;
+ messageUid = messageLine.substring(uidIndex, messageLine.indexOf(' ', uidIndex));
+ assertEquals(". OK UID FETCH completed", readFullAnswer("."));
+ assertNotNull(messageUid);
+ }
+
+ public void testUidStoreDeletedFlag() throws IOException {
+
+ // test deleted flag
+ writeLine(". UID STORE " + messageUid + " +FLAGS (\\Deleted)");
+ assertEquals(". OK STORE completed", readFullAnswer("."));
+ writeLine(". UID FETCH " + messageUid + " (FLAGS)");
+ assertEquals("* 1 FETCH (UID " + messageUid + " FLAGS (\\Seen \\Deleted \\Draft))", readLine());
+ assertEquals(". OK UID FETCH completed", readFullAnswer("."));
+
+ // remove deleted flag
+ writeLine(". UID STORE " + messageUid + " -FLAGS (\\Deleted)");
+ assertEquals(". OK STORE completed", readFullAnswer("."));
+ writeLine(". UID FETCH " + messageUid + " (FLAGS)");
+ assertEquals("* 1 FETCH (UID " + messageUid + " FLAGS (\\Seen \\Draft))", readLine());
+ assertEquals(". OK UID FETCH completed", readFullAnswer("."));
+
+ }
+
+ public void testUidRemoveSeenFlag() throws IOException {
+ // remove seen flag
+ writeLine(". UID STORE " + messageUid + " FLAGS (\\Draft)");
+ assertEquals(". OK STORE completed", readFullAnswer("."));
+ writeLine(". UID FETCH " + messageUid + " (FLAGS)");
+ assertEquals("* 1 FETCH (UID " + messageUid + " FLAGS (\\Draft))", readLine());
+ assertEquals(". OK UID FETCH completed", readFullAnswer("."));
+ }
+
+ public void testUidStoreForwardedFlag() throws IOException {
+ // add forwarded flag
+ writeLine(". UID STORE " + messageUid + " +FLAGS ($Forwarded)");
+ assertEquals(". OK STORE completed", readFullAnswer("."));
+ writeLine(". UID FETCH " + messageUid + " (FLAGS)");
+ assertEquals("* 1 FETCH (UID " + messageUid + " FLAGS (\\Draft $Forwarded))", readLine());
+ assertEquals(". OK UID FETCH completed", readFullAnswer("."));
+
+ // remove forwarded flag
+ writeLine(". UID STORE " + messageUid + " -FLAGS ($Forwarded)");
+ assertEquals(". OK STORE completed", readFullAnswer("."));
+ writeLine(". UID FETCH " + messageUid + " (FLAGS)");
+ assertEquals("* 1 FETCH (UID " + messageUid + " FLAGS (\\Draft))", readLine());
+ assertEquals(". OK UID FETCH completed", readFullAnswer("."));
+ }
+
+ public void testUidStoreAnsweredFlag() throws IOException {
+ // add answered flag
+ writeLine(". UID STORE " + messageUid + " +FLAGS (\\Answered)");
+ assertEquals(". OK STORE completed", readFullAnswer("."));
+ writeLine(". UID FETCH " + messageUid + " (FLAGS)");
+ assertEquals("* 1 FETCH (UID " + messageUid + " FLAGS (\\Draft \\Answered))", readLine());
+ assertEquals(". OK UID FETCH completed", readFullAnswer("."));
+
+ // remove answered flag
+ writeLine(". UID STORE " + messageUid + " -FLAGS (\\Answered)");
+ assertEquals(". OK STORE completed", readFullAnswer("."));
+ writeLine(". UID FETCH " + messageUid + " (FLAGS)");
+ assertEquals("* 1 FETCH (UID " + messageUid + " FLAGS (\\Draft))", readLine());
+ assertEquals(". OK UID FETCH completed", readFullAnswer("."));
+ }
+
+ public void testUidStoreJunkFlag() throws IOException {
+ // add Junk flag
+ writeLine(". UID STORE " + messageUid + " +FLAGS (Junk)");
+ assertEquals(". OK STORE completed", readFullAnswer("."));
+ writeLine(". UID FETCH " + messageUid + " (FLAGS)");
+ assertEquals("* 1 FETCH (UID " + messageUid + " FLAGS (Junk \\Draft))", readLine());
+ assertEquals(". OK UID FETCH completed", readFullAnswer("."));
+
+ // remove Junk flag
+ writeLine(". UID STORE " + messageUid + " -FLAGS (Junk)");
+ assertEquals(". OK STORE completed", readFullAnswer("."));
+ writeLine(". UID FETCH " + messageUid + " (FLAGS)");
+ assertEquals("* 1 FETCH (UID " + messageUid + " FLAGS (\\Draft))", readLine());
+ assertEquals(". OK UID FETCH completed", readFullAnswer("."));
+ }
+
+ public void testUidStoreSeenFlag() throws IOException {
+ // add Junk flag
+ writeLine(". UID STORE " + messageUid + " +FLAGS (\\Seen)");
+ assertEquals(". OK STORE completed", readFullAnswer("."));
+ writeLine(". UID FETCH " + messageUid + " (FLAGS)");
+ assertEquals("* 1 FETCH (UID " + messageUid + " FLAGS (\\Seen \\Draft))", readLine());
+ assertEquals(". OK UID FETCH completed", readFullAnswer("."));
+
+ // remove Junk flag
+ writeLine(". UID STORE " + messageUid + " -FLAGS (\\Seen)");
+ assertEquals(". OK STORE completed", readFullAnswer("."));
+ writeLine(". UID FETCH " + messageUid + " (FLAGS)");
+ assertEquals("* 1 FETCH (UID " + messageUid + " FLAGS (\\Draft))", readLine());
+ assertEquals(". OK UID FETCH completed", readFullAnswer("."));
+ }
+
+ public void testPartialFetch() throws IOException {
+ writeLine(". UID FETCH " + messageUid + " (BODY.PEEK[1.MIME])");
+ assertEquals(". OK UID FETCH completed", readFullAnswer("."));
+ }
+
+ public void testHeaderFetch() throws IOException {
+ writeLine(". UID FETCH " + messageUid + " (BODY[HEADER.FIELDS (DATE SUBJECT FROM CONTENT-TYPE TO CC BCC MESSAGE-ID IN-REPLY-TO REFERENCES)])");
+ assertEquals(". OK UID FETCH completed", readFullAnswer("."));
+ }
+
+ public void testHeaderBodyFetch() throws IOException {
+ writeLine(". UID FETCH " + messageUid + " (UID BODY.PEEK[HEADER.FIELDS (Content-Type Content-Transfer-Encoding)] BODY.PEEK[TEXT]<0.2048>)");
+ assertEquals(". OK UID FETCH completed", readFullAnswer("."));
+ }
+
+ public void testFetchInternalDate() throws IOException {
+ writeLine(". UID FETCH " + messageUid + " (INTERNALDATE)");
+ assertEquals(". OK UID FETCH completed", readFullAnswer("."));
+ }
+
+
+ public void testDeleteFolder() throws IOException {
+ writeLine(". DELETE testfolder");
+ assertEquals(". OK folder deleted", readFullAnswer("."));
+ }
+
+ public void testLogout() throws IOException {
+ writeLine(". LOGOUT");
+ assertEquals("* BYE Closing connection", socketReader.readLine());
+ assertEquals(". OK LOGOUT completed", socketReader.readLine());
+ clientSocket = null;
+ }
+
+ public void testCopyMessage() throws IOException, InterruptedException, MessagingException {
+ testCreateFolder();
+ testCreateMessage();
+ writeLine(". UID FETCH 1:* (FLAGS)");
+ String messageLine = readLine();
+ int uidIndex = messageLine.indexOf("UID ") + 4;
+ messageUid = messageLine.substring(uidIndex, messageLine.indexOf(' ', uidIndex));
+ assertEquals(". OK UID FETCH completed", readFullAnswer("."));
+
+ writeLine(". UID COPY " + messageUid + " Trash");
+ assertEquals(". OK copy completed", readFullAnswer("."));
+
+ writeLine(". COPY 1 Trash");
+ assertEquals(". OK copy completed", readFullAnswer("."));
+
+ testDeleteFolder();
+ }
+
+ public void testFetchInboxEnvelope() throws IOException {
+ testSelectInbox();
+ writeLine(". UID FETCH 1:* (ENVELOPE)");
+ assertEquals(". OK UID FETCH completed", readFullAnswer("."));
+ }
+
+ public void testFetchInboxBodyStructure() throws IOException {
+ testSelectInbox();
+ writeLine(". UID FETCH 1:* (BODYSTRUCTURE)");
+ assertEquals(". OK UID FETCH completed", readFullAnswer("."));
+ }
+
+ public void testFetchRfc822Header() throws IOException {
+ testSelectInbox();
+ writeLine(". UID FETCH 1:* (UID RFC822.HEADER RFC822.SIZE FLAGS)");
+ assertEquals(". OK UID FETCH completed", readFullAnswer("."));
+ }
+
+ public void testThunderbirdHeaderFetch() throws IOException {
+ testSelectInbox();
+ writeLine(". UID FETCH 1:* (UID RFC822.SIZE FLAGS BODY.PEEK[HEADER.FIELDS (From To Cc Bcc Subject Date Message-ID Priority X-Priority References Newsgroups In-Reply-To Content-Type)])");
+ assertEquals(". OK UID FETCH completed", readFullAnswer("."));
+ }
+
+ public void testSearchHeader() throws IOException {
+ testSelectInbox();
+ writeLine(". UID SEARCH HEADER X-TUID testvalue");
+ assertEquals(". OK SEARCH completed", readFullAnswer("."));
+ writeLine(". UID SEARCH HEADER X-OfflineIMAP \"testvalue\"");
+ assertEquals(". OK SEARCH completed", readFullAnswer("."));
+ }
+
+ public void testSearchUndraft() throws IOException {
+ testSelectInbox();
+ writeLine(". UID SEARCH UNDRAFT");
+ assertEquals(". OK SEARCH completed", readFullAnswer("."));
+ writeLine(". UID SEARCH DRAFT");
+ assertEquals(". OK SEARCH completed", readFullAnswer("."));
+ }
+
+ public void testBrokenPipe() throws IOException, InterruptedException {
+ testSelectInbox();
+ writeLine(". UID FETCH 1:* (RFC822.SIZE BODY.TEXT)");
+ socketReader.readLine();
+ // force close connection
+ clientSocket.close();
+ Thread.sleep(5000);
+ }
+
+ public void testSearchCharset() throws IOException {
+ testSelectInbox();
+ writeLine("UID SEARCH CHARSET UTF-8 (HEADER SUBJECT testé)");
+ assertEquals(". OK SEARCH completed", readFullAnswer("."));
+ }
+
+ public void testWanderLust() throws IOException {
+ testSelectInbox();
+ writeLine(". uid fetch 1:* (body.peek[header.fields (Subject From To Cc Date Message-Id References In-Reply-To Delivered-To)] rfc822.size flags)");
+ assertEquals(". OK UID FETCH completed", readFullAnswer("."));
+ }
+
+ public void testSearchSince() throws IOException {
+ testSelectInbox();
+ writeLine(". UID SEARCH SINCE 1-Jan-2012 UNDELETED");
+ assertEquals(". OK SEARCH completed", readFullAnswer("."));
+ }
+
+ public void testSearchText() throws IOException {
+ testSelectInbox();
+ writeLine(". UID SEARCH TEXT test");
+ assertEquals(". OK SEARCH completed", readFullAnswer("."));
+ }
+
+
+ public void testDraftMessageMessageId() throws IOException, InterruptedException, MessagingException {
+ testCreateFolder();
+ MimeMessage mimeMessage = new MimeMessage((Session) null);
+ mimeMessage.addHeader("to", Settings.getProperty("davmail.to"));
+ mimeMessage.setText("Test message");
+ mimeMessage.setSubject("Test subject");
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ mimeMessage.writeTo(baos);
+ byte[] content = baos.toByteArray();
+ writeLine(". APPEND testfolder (\\Seen \\Draft) {" + content.length + '}');
+ assertEquals("+ send literal data", readLine());
+ writeLine(new String(content));
+ assertEquals(". OK APPEND completed", readFullAnswer("."));
+
+ writeLine(". UID SEARCH UNDELETED (HEADER Message-ID "+mimeMessage.getMessageID().substring(1, mimeMessage.getMessageID().length()-1)+")");
+ assertEquals(". OK SEARCH completed", readFullAnswer("."));
+
+ testDeleteFolder();
+ }
+
+ public void testFetchOSX() throws IOException {
+ testSelectInbox();
+ writeLine(". FETCH 1:* (FLAGS UID BODY.PEEK[HEADER.FIELDS (content-class)])");
+ assertEquals(". OK FETCH completed", readFullAnswer("."));
+ }
+
+ public void testFetchHeadersThunderbird() throws IOException {
+ testSelectInbox();
+ writeLine(". FETCH 1:* (UID RFC822.SIZE FLAGS BODY.PEEK[HEADER.FIELDS (From To Cc Bcc Subject Date Message-ID Priority X-Priority References Newsgroups In-Reply-To Content-Type)])");
+ assertEquals(". OK FETCH completed", readFullAnswer("."));
+ }
+}
diff --git a/src/test/davmail/imap/TestImapFullSync.java b/src/test/davmail/imap/TestImapFullSync.java
new file mode 100644
index 0000000..da5126c
--- /dev/null
+++ b/src/test/davmail/imap/TestImapFullSync.java
@@ -0,0 +1,34 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2011 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.imap;
+
+import java.io.IOException;
+
+/**
+ * Test IMAP full sync.
+ */
+public class TestImapFullSync extends AbstractImapTestCase {
+ public void testSelectTrash() throws IOException {
+ writeLine(". SELECT Trash");
+ assertEquals(". OK [READ-WRITE] SELECT completed", readFullAnswer("."));
+ writeLine(". UID FETCH 1:* (UID RFC822.SIZE FLAGS BODY.PEEK[HEADER.FIELDS (From To Cc Bcc Subject Date Message-ID Priority X-Priority References Newsgroups In-Reply-To Content-Type)])");
+ assertEquals(". OK UID FETCH completed", readFullAnswer("."));
+ }
+
+}
diff --git a/src/test/davmail/ldap/TestLdap.java b/src/test/davmail/ldap/TestLdap.java
new file mode 100644
index 0000000..b894371
--- /dev/null
+++ b/src/test/davmail/ldap/TestLdap.java
@@ -0,0 +1,171 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.ldap;
+
+import davmail.DavGateway;
+import davmail.Settings;
+import davmail.exchange.AbstractExchangeSessionTestCase;
+import davmail.exchange.ExchangeSessionFactory;
+
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+import javax.naming.ldap.InitialLdapContext;
+import java.io.IOException;
+import java.util.Hashtable;
+
+/**
+ * Test LDAP.
+ */
+ at SuppressWarnings({"JavaDoc"})
+public class TestLdap extends AbstractExchangeSessionTestCase {
+ InitialLdapContext ldapContext;
+
+ @Override
+ public void setUp() throws IOException {
+ boolean needStart = !loaded;
+ super.setUp();
+ if (needStart) {
+ // start gateway
+ DavGateway.start();
+ }
+ if (ldapContext == null) {
+ Hashtable<String, String> env = new Hashtable<String, String>();
+ env.put("java.naming.security.authentication", "simple");
+ env.put("java.naming.security.principal", Settings.getProperty("davmail.username"));
+ env.put("java.naming.security.credentials", Settings.getProperty("davmail.password"));
+
+ env.put("com.sun.jndi.ldap.connect.pool", "true");
+ env.put("java.naming.factory.initial", "com.sun.jndi.ldap.LdapCtxFactory");
+ env.put("java.naming.provider.url", "ldap://localhost:" + Settings.getIntProperty("davmail.ldapPort"));
+ env.put("java.naming.referral", "follow");
+
+ try {
+ ldapContext = new InitialLdapContext(env, null);
+ } catch (NamingException e) {
+ throw new IOException(e);
+ }
+ }
+ if (session == null) {
+ session = ExchangeSessionFactory.getInstance(Settings.getProperty("davmail.username"), Settings.getProperty("davmail.password"));
+ }
+ }
+
+ public void testSearchOneLevel() throws NamingException {
+ SearchControls searchControls = new SearchControls();
+ searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
+ NamingEnumeration<SearchResult> searchResults = ldapContext.search("ou=people", "(objectclass=*)", searchControls);
+ }
+
+ public void testSearchMail() throws NamingException {
+ SearchControls searchControls = new SearchControls();
+ searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
+ searchControls.setReturningAttributes(new String[]{"mail"});
+ NamingEnumeration<SearchResult> searchResults = ldapContext.search("ou=people", "(objectclass=*)", searchControls);
+ }
+
+ public void testMozillaSearchAttributes() throws NamingException {
+ SearchControls searchControls = new SearchControls();
+ searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
+ searchControls.setReturningAttributes(new String[]{"custom1", "mozillausehtmlmail", "postalcode", "custom2", "custom3", "custom4", "street", "surname", "telephonenumber", "mozillahomelocalityname", "orgunit", "mozillaworkstreet2", "xmozillanickname", "mozillahomestreet", "description", "cellphone", "homeurl", "mozillahomepostalcode", "departmentnumber", "postofficebox", "st", "objectclass", "sn", "ou", "fax", "mozillahomeurl", "mozillahomecountryname", "streetaddress", "cn", "company", "mozillaworkurl", "mobile", "region", "birthmonth", "birthday", "labeleduri", "carphone", "department", "xmozillausehtmlmail", "givenname", "nsaimid", "workurl", "facsimiletelephonenumber", "mozillanickname", "title", "nscpaimscreenname", "xmozillasecondemail", "mozillacustom3", "countryname", "mozillacustom4", "mozillacustom1", "mozillacustom2", "homephone", "mozillasecondemail", "pager", "zip", "mail", "c", "mozillahomestate", "o", "l", "birthyear", "modifytimestamp", "locality", "commonname", "notes", "pagerphone", "mozillahomestreet2"});
+ NamingEnumeration<SearchResult> searchResults = ldapContext.search("ou=people", "(objectclass=*)", searchControls);
+ }
+
+ public void testGalfind() throws NamingException {
+ SearchControls searchControls = new SearchControls();
+ searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
+ NamingEnumeration<SearchResult> searchResults = ldapContext.search("ou=people", "(uid="+session.getAlias()+ ')', searchControls);
+ assertTrue(searchResults.hasMore());
+ SearchResult searchResult = searchResults.next();
+ Attributes attributes = searchResult.getAttributes();
+ Attribute attribute = attributes.get("uid");
+ assertEquals(session.getAlias(), attribute.get());
+ // given name not available on Exchange 2007 over Dav (no gallookup)
+ //assertNotNull(attributes.get("givenName"));
+ }
+
+ public void testOSXSearch() throws NamingException {
+ SearchControls searchControls = new SearchControls();
+ searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
+ searchControls.setReturningAttributes(new String[]{"uid", "jpegphoto", "postalcode", "mail", "sn", "apple-emailcontacts", "c", "street", "givenname", "l", "apple-user-picture", "telephonenumber", "cn", "st", "apple-imhandle"});
+ NamingEnumeration<SearchResult> searchResults = ldapContext.search("cn=users, o=od", "(&(objectclass=inetOrgPerson)(|(givenname=Charles*)(|(uid=Ch*)(cn=Ch*))(sn=Ch*))(objectclass=shadowAccount)(objectclass=extensibleObject)(objectclass=posixAccount)(objectclass=apple-user))", searchControls);
+ assertTrue(searchResults.hasMore());
+ }
+
+ public void testOSXICalSearch() throws NamingException {
+ SearchControls searchControls = new SearchControls();
+ searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
+ searchControls.setReturningAttributes(new String[]{"uid", "mail", "sn", "cn", "description", "apple-generateduid", "givenname", "apple-serviceslocator", "uidnumber"});
+ NamingEnumeration<SearchResult> searchResults = ldapContext.search("cn=users, o=od",
+ "(&(objectclass=inetOrgPerson)(objectclass=extensibleObject)(objectclass=apple-user)(|(|(uid=fair*)(cn=fair*))(givenname=fair*)(sn=fair*)(cn=fair*)(mail=fair*))(objectclass=posixAccount)(objectclass=shadowAccount))", searchControls);
+ }
+
+ public void testSearchByGivenNameWithoutReturningAttributes() throws NamingException {
+ SearchControls searchControls = new SearchControls();
+ searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
+ searchControls.setReturningAttributes(new String[]{"uid"});
+ NamingEnumeration<SearchResult> searchResults = ldapContext.search("ou=people", "(givenName=mic*)", searchControls);
+ }
+
+ public void testSearchByGalfindUnsupportedAttribute() throws NamingException {
+ SearchControls searchControls = new SearchControls();
+ searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
+ NamingEnumeration<SearchResult> searchResults = ldapContext.search("ou=people", "(postalcode=N18 1ZF)", searchControls);
+ }
+
+ public void testSearchByCnReturnSn() throws NamingException {
+ SearchControls searchControls = new SearchControls();
+ searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
+ searchControls.setReturningAttributes(new String[]{"sn"});
+ NamingEnumeration<SearchResult> searchResults = ldapContext.search("ou=people", "(cn=*)", searchControls);
+ }
+
+ public void testSearchByCnReturnGivenName() throws NamingException {
+ SearchControls searchControls = new SearchControls();
+ searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
+ searchControls.setReturningAttributes(new String[]{"givenName"});
+ NamingEnumeration<SearchResult> searchResults = ldapContext.search("ou=people", "(cn=*a*)", searchControls);
+ }
+
+ public void testSearchIPad() throws NamingException {
+ SearchControls searchControls = new SearchControls();
+ searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
+ searchControls.setReturningAttributes(new String[]{"postalcode", "labeleduri", "street", "givenname", "telephonenumber", "facsimiletelephonenumber", "title", "imhandle", "homepostaladdress", "st", "homephone", "applefloor", "jpegphoto", "pager", "mail", "sn", "buildingname", "ou", "destinationindicator", "c", "o", "l", "co", "postaladdress", "cn", "mobile"});
+ NamingEnumeration<SearchResult> searchResults = ldapContext.search("ou=people", "(|(mail=Test*)(cn=Test*)(givenname=Test*)(sn=Test*))", searchControls);
+ }
+
+ public void testThunderbird() throws NamingException {
+ String filter = "(|(sn=*stocker*)(givenname=*stocker*)(mail=*stocker*)(cn=*stocker*))";
+ String[] returningAttributes = new String[]{"custom1", "mozillausehtmlmail", "postalcode", "custom2", "custom3", "custom4", "street", "surname", "telephonenumber", "mozillahomelocalityname", "orgunit", "mozillaworkstreet2", "xmozillanickname", "mozillahomestreet", "description", "cellphone", "homeurl", "mozillahomepostalcode", "departmentnumber", "postofficebox", "st", "objectclass", "sn", "ou", "fax", "mozillahomeurl", "mozillahomecountryname", "streetaddress", "cn", "company", "mozillaworkurl", "mobile", "region", "birthmonth", "birthday", "labeleduri", "carphone", "department", "xmozillausehtmlmail", "givenname", "nsaimid", "workurl", "facsimiletelephonenumber", "mozillanickname", "title", "nscpaimscreenname", "xmozillasecondemail", "mozillacustom3", "countryname", "mozillacustom4", "mozillacustom1", "mozillacustom2", "homephone", "mozillasecondemail", "pager", "zip", "mail", "c", "mozillahomestate", "o", "l", "birthyear", "modifytimestamp", "locality", "commonname", "notes", "pagerphone", "mozillahomestreet2"};
+ SearchControls searchControls = new SearchControls();
+ searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
+ searchControls.setReturningAttributes(returningAttributes);
+ NamingEnumeration<SearchResult> searchResults = ldapContext.search("ou=people", filter, searchControls);
+ }
+
+ public void testSearchNotFilter() throws NamingException {
+ SearchControls searchControls = new SearchControls();
+ searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
+ searchControls.setReturningAttributes(new String[]{"mail"});
+ NamingEnumeration<SearchResult> searchResults = ldapContext.search("ou=people", "(!(objectclass=test))", searchControls);
+ }
+}
diff --git a/src/test/davmail/smtp/TestSmtp.java b/src/test/davmail/smtp/TestSmtp.java
new file mode 100644
index 0000000..88d0ca9
--- /dev/null
+++ b/src/test/davmail/smtp/TestSmtp.java
@@ -0,0 +1,254 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.smtp;
+
+import davmail.AbstractDavMailTestCase;
+import davmail.DavGateway;
+import davmail.Settings;
+import davmail.exchange.DoubleDotOutputStream;
+import davmail.exchange.ExchangeSession;
+import davmail.exchange.ExchangeSessionFactory;
+import org.apache.commons.codec.binary.Base64;
+
+import javax.mail.MessagingException;
+import javax.mail.Session;
+import javax.mail.internet.MimeMessage;
+import java.io.*;
+import java.net.Socket;
+
+/**
+ * Test smtp send message.
+ */
+public class TestSmtp extends AbstractDavMailTestCase {
+ static Socket clientSocket;
+ static BufferedOutputStream socketOutputStream;
+ static BufferedInputStream socketInputStream;
+
+ enum State {
+ CHAR, CR, CRLF
+ }
+
+ protected void write(String line) throws IOException {
+ socketOutputStream.write(line.getBytes("ASCII"));
+ socketOutputStream.flush();
+ }
+
+ protected void writeLine(String line) throws IOException {
+ write(line);
+ socketOutputStream.write(13);
+ socketOutputStream.write(10);
+ socketOutputStream.flush();
+ }
+
+ protected String readLine() throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ State state = State.CHAR;
+ while (!(state == State.CRLF)) {
+ int character = socketInputStream.read();
+ if (state == State.CHAR) {
+ if (character == 13) {
+ state = State.CR;
+ }
+ } else {
+ if (character == 10) {
+ state = State.CRLF;
+ } else {
+ state = State.CHAR;
+ }
+ }
+ if (state == State.CHAR) {
+ baos.write(character);
+ }
+ }
+ return new String(baos.toByteArray(), "ASCII");
+ }
+
+ @Override
+ public void setUp() throws IOException {
+ super.setUp();
+ if (clientSocket == null) {
+ // start gateway
+ DavGateway.start();
+ clientSocket = new Socket("localhost", Settings.getIntProperty("davmail.smtpPort"));
+ socketOutputStream = new BufferedOutputStream(clientSocket.getOutputStream());
+ socketInputStream = new BufferedInputStream(clientSocket.getInputStream());
+
+ String banner = readLine();
+ assertNotNull(banner);
+ //String credentials = (char) 0 + Settings.getProperty("davmail.username") + (char) 0 + Settings.getProperty("davmail.password");
+ String credentials = Settings.getProperty("davmail.username") + (char) 0 + Settings.getProperty("davmail.username") + (char) 0 + Settings.getProperty("davmail.password");
+ writeLine("AUTH PLAIN " + new String(new Base64().encode(credentials.getBytes())));
+ assertEquals("235 OK Authenticated", readLine());
+ }
+ if (session == null) {
+ session = ExchangeSessionFactory.getInstance(Settings.getProperty("davmail.username"), Settings.getProperty("davmail.password"));
+ }
+ }
+
+ public void sendAndCheckMessage(MimeMessage mimeMessage) throws IOException, MessagingException, InterruptedException {
+ sendAndCheckMessage(mimeMessage, null);
+ }
+
+ public void sendAndCheckMessage(MimeMessage mimeMessage, String bcc) throws IOException, MessagingException, InterruptedException {
+ sendAndCheckMessage(mimeMessage, null, bcc);
+ }
+
+ public void sendAndCheckMessage(MimeMessage mimeMessage, String from, String bcc) throws IOException, MessagingException, InterruptedException {
+ // generate message id
+ mimeMessage.saveChanges();
+ // mimeMessage.writeTo(System.out);
+
+ // copy Message-id to references header
+ mimeMessage.addHeader("references", mimeMessage.getHeader("message-id")[0]);
+ if (from != null) {
+ writeLine("MAIL FROM:" + from);
+ } else {
+ writeLine("MAIL FROM:" + session.getEmail());
+ }
+ readLine();
+ if (bcc != null) {
+ writeLine("RCPT TO:" + bcc);
+ readLine();
+ }
+ writeLine("RCPT TO:" + Settings.getProperty("davmail.to"));
+ readLine();
+ writeLine("DATA");
+ assertEquals("354 Start mail input; end with <CRLF>.<CRLF>", readLine());
+ mimeMessage.writeTo(new DoubleDotOutputStream(socketOutputStream));
+ writeLine("");
+ writeLine(".");
+ assertEquals("250 Queued mail for delivery", readLine());
+ // wait for asynchronous message send
+ ExchangeSession.MessageList messages = null;
+ for (int i = 0; i < 5; i++) {
+ messages = session.searchMessages("Sent", session.headerIsEqualTo("references", mimeMessage.getMessageID()));
+ if (messages.size() > 0) {
+ break;
+ }
+ Thread.sleep(1000);
+ }
+ assertEquals(1, messages.size());
+ ExchangeSession.Message sentMessage = messages.get(0);
+ sentMessage.getMimeMessage().writeTo(System.out);
+ assertEquals(mimeMessage.getDataHandler().getContent(), sentMessage.getMimeMessage().getDataHandler().getContent());
+ }
+
+ public void testSendSimpleMessage() throws IOException, MessagingException, InterruptedException {
+ String body = "Test message";
+ MimeMessage mimeMessage = new MimeMessage((Session) null);
+ mimeMessage.addHeader("To", Settings.getProperty("davmail.to"));
+ mimeMessage.setSubject("Test subject");
+ mimeMessage.setText(body);
+ sendAndCheckMessage(mimeMessage);
+ }
+
+ public void testInvalidFrom() throws IOException, MessagingException, InterruptedException {
+ String body = "Test message";
+ MimeMessage mimeMessage = new MimeMessage((Session) null);
+ mimeMessage.addHeader("From", "guessant at loca.net");
+ mimeMessage.addHeader("To", Settings.getProperty("davmail.to"));
+ mimeMessage.setSubject("Test subject");
+ mimeMessage.setText(body);
+ sendAndCheckMessage(mimeMessage, "guessant at loca.net", null);
+ }
+
+ public void testSendMessage() throws IOException, MessagingException, InterruptedException {
+ String body = "Test message\r\n" +
+ "Special characters: éèçà \r\n" +
+ "Chinese: " + ((char) 0x604F) + ((char) 0x7D59);
+ MimeMessage mimeMessage = new MimeMessage((Session) null);
+ mimeMessage.addHeader("To", Settings.getProperty("davmail.to"));
+ mimeMessage.setSubject("Test subject");
+ mimeMessage.setText(body, "UTF-8");
+ sendAndCheckMessage(mimeMessage);
+ }
+
+ public void testBccMessage() throws IOException, MessagingException, InterruptedException {
+ MimeMessage mimeMessage = new MimeMessage((Session) null);
+ mimeMessage.addHeader("to", Settings.getProperty("davmail.to"));
+ mimeMessage.setSubject("Test subject dav");
+ mimeMessage.setText("Test message");
+ sendAndCheckMessage(mimeMessage, Settings.getProperty("davmail.bcc"));
+ }
+
+ public void testDotMessage() throws IOException, MessagingException, InterruptedException {
+ String body = "First line\r\n.\r\nSecond line";
+ MimeMessage mimeMessage = new MimeMessage((Session) null);
+ mimeMessage.addHeader("to", Settings.getProperty("davmail.to"));
+ mimeMessage.setSubject("Test subject");
+ mimeMessage.setText(body);
+ sendAndCheckMessage(mimeMessage);
+ }
+
+ public void testSendMessageTwice() throws IOException, MessagingException, InterruptedException {
+ Settings.setProperty("davmail.smtpCheckDuplicates", "true");
+ String body = "First line\r\n.\r\nSecond line";
+ MimeMessage mimeMessage = new MimeMessage((Session) null);
+ mimeMessage.addHeader("to", Settings.getProperty("davmail.to"));
+ mimeMessage.setSubject("Test subject");
+ mimeMessage.setText(body);
+ sendAndCheckMessage(mimeMessage);
+ sendAndCheckMessage(mimeMessage);
+ }
+
+ public void testComplexToMessage() throws IOException, MessagingException, InterruptedException {
+ String body = "Test message";
+ MimeMessage mimeMessage = new MimeMessage((Session) null);
+ mimeMessage.addHeader("To", "nickname <" + Settings.getProperty("davmail.to") + '>');
+ mimeMessage.setSubject("Test subject");
+ mimeMessage.setText(body);
+ sendAndCheckMessage(mimeMessage);
+ }
+
+ public void testSendPlainTextMessage() throws IOException, MessagingException, InterruptedException {
+ String body = "Test plain text message";
+ MimeMessage mimeMessage = new MimeMessage((Session) null);
+ mimeMessage.addHeader("To", Settings.getProperty("davmail.to"));
+ mimeMessage.setSubject("Test text/plain message");
+ mimeMessage.setText(body);
+ sendAndCheckMessage(mimeMessage);
+ }
+
+ public void testSendHtmlMessage() throws IOException, MessagingException, InterruptedException {
+ String body = "Test html message <font color=\"#ff0000\">red</font>";
+ MimeMessage mimeMessage = new MimeMessage((Session) null);
+ mimeMessage.addHeader("To", Settings.getProperty("davmail.to"));
+ mimeMessage.setSubject("Test html message");
+ mimeMessage.setContent(body, "text/html");
+ sendAndCheckMessage(mimeMessage);
+ }
+
+ public void testQuit() throws IOException {
+ writeLine("QUIT");
+ assertEquals("221 Closing connection", readLine());
+ }
+
+
+ public void testBrokenMessage() throws MessagingException, IOException, InterruptedException {
+ MimeMessage mimeMessage = new MimeMessage(null, new FileInputStream("test.eml"));
+ sendAndCheckMessage(mimeMessage);
+ }
+
+ public void testBrokenMessage2() throws MessagingException, IOException, InterruptedException {
+ MimeMessage mimeMessage = new MimeMessage(null, new org.apache.commons.codec.binary.Base64InputStream(new FileInputStream("broken64.txt")));
+ mimeMessage.addHeader("To", Settings.getProperty("davmail.to"));
+ sendAndCheckMessage(mimeMessage);
+ }
+
+}
diff --git a/src/test/davmail/ui/TestNotificationDialog.java b/src/test/davmail/ui/TestNotificationDialog.java
new file mode 100644
index 0000000..1d19c3d
--- /dev/null
+++ b/src/test/davmail/ui/TestNotificationDialog.java
@@ -0,0 +1,36 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2010 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.ui;
+
+import junit.framework.TestCase;
+
+import javax.swing.*;
+import java.io.IOException;
+
+/**
+ * Test Notification Frame
+ */
+public class TestNotificationDialog extends TestCase {
+ public void testCreateNotificationFrame() throws IOException, ClassNotFoundException, UnsupportedLookAndFeelException, IllegalAccessException, InstantiationException {
+ UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+ NotificationDialog notificationDialog = new NotificationDialog("to", "cc", "subject", "description");
+ notificationDialog.setVisible(true);
+ System.out.println(notificationDialog.getSendNotification());
+ }
+}
diff --git a/src/test/davmail/util/StringUtilTest.java b/src/test/davmail/util/StringUtilTest.java
new file mode 100644
index 0000000..7a7612a
--- /dev/null
+++ b/src/test/davmail/util/StringUtilTest.java
@@ -0,0 +1,122 @@
+/*
+ * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
+ * Copyright (C) 2009 Mickael Guessant
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package davmail.util;
+
+import junit.framework.TestCase;
+import org.apache.commons.httpclient.URIException;
+import org.apache.commons.httpclient.util.URIUtil;
+
+/**
+ * Test StringUtil.
+ */
+public class StringUtilTest extends TestCase {
+ /**
+ * Test get token
+ */
+ public void testGetToken() {
+ assertNull(StringUtil.getToken(null, null, null));
+ assertNull(StringUtil.getToken(null, "\'", "\'"));
+ assertNull(StringUtil.getToken("'test", "\'", "\'"));
+ assertEquals("test", StringUtil.getToken("'test'", "'", "'"));
+ assertEquals("test", StringUtil.getToken("value=\"test\"", "value=\"", "\""));
+ }
+
+ /**
+ * Test replace token
+ */
+ public void testReplaceToken() {
+ assertNull(StringUtil.replaceToken(null, null, null, null));
+ assertNull(StringUtil.replaceToken(null, null, null, "new"));
+ assertEquals("'new'", StringUtil.replaceToken("'old'", "'", "'", "new"));
+ assertEquals("value=\"new\"", StringUtil.replaceToken("value=\"old\"", "value=\"", "\"", "new"));
+ }
+
+ /**
+ * Test Xml Encode
+ */
+ public void testXmlEncode() {
+ assertEquals("&", StringUtil.xmlEncode("&"));
+ assertEquals("<", StringUtil.xmlEncode("<"));
+ assertEquals(">", StringUtil.xmlEncode(">"));
+ assertEquals("&", StringUtil.xmlDecode("&"));
+ assertEquals("<", StringUtil.xmlDecode("<"));
+ assertEquals(">", StringUtil.xmlDecode(">"));
+ assertEquals("<test>", StringUtil.xmlEncode("<test>"));
+ }
+
+ public void testUrlEncodeAmpersand() {
+ assertEquals("%26", StringUtil.urlEncodeAmpersand("&"));
+ assertEquals("&", StringUtil.urlDecodeAmpersand("%26"));
+ }
+
+ public void testPerf() {
+ String value = "dqsdqs+dsqds+dsqdqs";
+ for (int j = 0; j < 5; j++) {
+ long startTime = System.currentTimeMillis();
+ for (int i = 0; i < 1000000; i++) {
+ //String result = StringUtil.encodePlusSign(value);
+ //String result = StringUtil.replaceAll(value, '+', "%2B");
+
+ /*int length = value.length();
+ StringBuilder buffer = new StringBuilder(length);
+ int startIndex = 0;
+ int endIndex = value.indexOf('+');
+ while (endIndex >= 0) {
+ buffer.append(value.substring(startIndex, endIndex));
+ buffer.append("%2B");
+ startIndex = endIndex + 1;
+ endIndex = value.indexOf('+', startIndex);
+ }
+ buffer.append(value.substring(startIndex)); */
+ /*
+ for (int k = 0; k < length; k++) {
+ char c = value.charAt(k);
+ if (c == '+') {
+ buffer.append("%2B");
+ } else {
+ buffer.append(c);
+ }
+ }*/
+ //String result = buffer.toString();
+ //String result = value.replaceAll("\\+", "%2B");
+ }
+ System.out.println("Elapsed: " + (System.currentTimeMillis() - startTime) + " ms");
+ }
+ }
+
+ public void testRemoveQuotes() {
+ assertEquals("test", StringUtil.removeQuotes("test"));
+ assertEquals("test", StringUtil.removeQuotes("\"test\""));
+ }
+
+ public void testEncodePipe() {
+ assertEquals("test %7C", StringUtil.encodeUrlcompname("test |"));
+ assertEquals("test |", StringUtil.decodeUrlcompname("test %7C"));
+ }
+
+ public void testEncodeQuestion() {
+ try {
+ URIUtil.encodeWithinQuery("test ?");
+ } catch (URIException e) {
+ e.printStackTrace();
+ }
+ assertEquals("test %3F", StringUtil.encodeUrlcompname("test ?"));
+ assertEquals("test ?", StringUtil.decodeUrlcompname("test %3F"));
+ }
+}
diff --git a/src/web/WEB-INF/classes/davmail.properties b/src/web/WEB-INF/classes/davmail.properties
new file mode 100644
index 0000000..ad0e495
--- /dev/null
+++ b/src/web/WEB-INF/classes/davmail.properties
@@ -0,0 +1,23 @@
+davmail.url=http://exchangeServer/exchange/
+davmail.popPort=1110
+davmail.smtpPort=1025
+davmail.keepDelay=30
+davmail.sentKeepDelay=90
+davmail.caldavPastDelay=90
+
+davmail.useSystemProxies=false
+davmail.enableProxy=false
+davmail.proxyHost=
+davmail.proxyPort=
+davmail.proxyUser=
+davmail.proxyPassword=
+
+davmail.allowRemote=true
+davmail.bindAddress=
+davmail.server=true
+davmail.disableUpdateCheck=false
+
+log4j.logger.davmail=DEBUG
+log4j.rootLogger=WARN
+log4j.logger.httpclient.wire=WARN
+log4j.logger.org.apache.commons.httpclient=WARN
diff --git a/src/web/WEB-INF/web.xml b/src/web/WEB-INF/web.xml
new file mode 100644
index 0000000..ccb57ad
--- /dev/null
+++ b/src/web/WEB-INF/web.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<web-app xmlns="http://java.sun.com/xml/ns/javaee"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
+ http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
+ version="2.5">
+ <description>
+ Ever wanted to get rid of Outlook ? DavMail is a POP/IMAP/SMTP/Caldav/Carddav/LDAP exchange
+ gateway allowing users to use any mail/calendar client (e.g. Thunderbird with Lightning or
+ Apple iCal) with an Exchange server, even from the internet or behind a firewall through
+ Outlook Web Access.
+ DavMail now includes an LDAP gateway to Exchange global address book and user personal
+ contacts to allow recipient address completion in mail compose window and full calendar
+ support with attendees free/busy display.
+ The main goal of DavMail is to provide standard compliant protocols in front of proprietary
+ Exchange. This means LDAP for global address book, SMTP to send messages, IMAP to browse
+ messages on the server in any folder, POP to retrieve inbox messages only, Caldav for
+ calendar support and Carddav for personal contacts sync.
+ Thus any standard compliant client can be used with Microsoft Exchange.
+ DavMail gateway is implemented in java and should run on any platform. Releases are tested
+ on Windows, Linux (Ubuntu) and Mac OSX. Tested successfully with the Iphone
+ (gateway running on a server).
+
+ http://davmail.sourceforge.net
+ </description>
+ <listener>
+ <listener-class>davmail.web.DavGatewayServletContextListener</listener-class>
+ </listener>
+</web-app>
diff --git a/src/winrun4j/davmailservice.exe b/src/winrun4j/davmailservice.exe
new file mode 100644
index 0000000..e8428aa
Binary files /dev/null and b/src/winrun4j/davmailservice.exe differ
diff --git a/src/winrun4j/davmailservice.ini b/src/winrun4j/davmailservice.ini
new file mode 100644
index 0000000..bf5b70a
--- /dev/null
+++ b/src/winrun4j/davmailservice.ini
@@ -0,0 +1,15 @@
+service.startup=auto
+service.class=davmail.service.DavService
+service.id=DavMail
+service.name=DavMail Gateway
+service.description=DavMail POP/IMAP/SMTP/Caldav/Carddav/LDAP Exchange Gateway
+
+classpath.1=davmail.jar
+classpath.2=lib/*
+
+vm.version.min=1.6
+vmarg.1=-Dsun.net.inetaddr.ttl=60
+vmarg.2=-Xrs
+
+
+arg.1=davmail.properties
diff --git a/src/winrun4j/rcedit.exe b/src/winrun4j/rcedit.exe
new file mode 100644
index 0000000..642e955
Binary files /dev/null and b/src/winrun4j/rcedit.exe differ
diff --git a/user.properties b/user.properties
new file mode 100644
index 0000000..1d12c7a
--- /dev/null
+++ b/user.properties
@@ -0,0 +1,3 @@
+username=mguessan
+keyfile=C:/mguessan/.ssh/id_dsa
+passphrase=
--
davmail packaging
More information about the pkg-java-commits
mailing list