Merge branch 'upstream-master' into json-pending-command-rebase
1
.gitignore
vendored
|
@ -5,6 +5,7 @@
|
|||
.settings
|
||||
.classpath
|
||||
bin
|
||||
captures
|
||||
coverage
|
||||
coverage.ec
|
||||
coverage.em
|
||||
|
|
|
@ -2,10 +2,10 @@ language: android
|
|||
android:
|
||||
components:
|
||||
- tools
|
||||
- build-tools-23.0.3
|
||||
- android-23
|
||||
- build-tools-24.0.3
|
||||
- android-24
|
||||
- extra-android-m2repository
|
||||
|
||||
jdk: oraclejdk7
|
||||
jdk: oraclejdk8
|
||||
|
||||
script: ./gradlew testDebug
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
HtmlCleaner is distributed under BSD License. It gives the freedom for
|
||||
anyone to use, explore, modify, and distribute HtmlCleaner, but without any
|
||||
warranty.
|
||||
|
||||
Copyright (c) 2006-2011, HtmlCleaner team.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use of this software in source and binary forms,
|
||||
with or without modification, are permitted provided that the
|
||||
following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above
|
||||
copyright notice, this list of conditions and the
|
||||
following disclaimer.
|
||||
|
||||
* 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.
|
||||
|
||||
* The name of HtmlCleaner may not be used to endorse or promote
|
||||
products derived from this software without specific prior
|
||||
written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT
|
||||
OWNER 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.
|
193
NOTICE
|
@ -1,190 +1,3 @@
|
|||
|
||||
Copyright (c) 2005-2008, The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
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.
|
||||
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
K-9 Mail
|
||||
Copyright 2008-2016, K-9 Mail Developers
|
||||
Copyright 2005-2016, The Android Open Source Project
|
||||
|
|
|
@ -4,23 +4,20 @@ buildscript {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:2.0.0'
|
||||
classpath 'com.android.tools.build:gradle:2.2.2'
|
||||
}
|
||||
}
|
||||
|
||||
project.ext {
|
||||
preDexLibs = !project.hasProperty('disablePreDex')
|
||||
testCoverage = project.hasProperty('testCoverage')
|
||||
|
||||
compileSdkVersion = 23
|
||||
buildToolsVersion = '23.0.3'
|
||||
optimizeForDevelopment = project.hasProperty('optimizeForDevelopment') && optimizeForDevelopment == 'true'
|
||||
}
|
||||
|
||||
subprojects {
|
||||
project.plugins.whenPluginAdded { plugin ->
|
||||
if ("com.android.build.gradle.AppPlugin".equals(plugin.class.name) ||
|
||||
"com.android.build.gradle.LibraryPlugin".equals(plugin.class.name)) {
|
||||
project.android.dexOptions.preDexLibraries = rootProject.ext.preDexLibs
|
||||
project.android.dexOptions.preDexLibraries = !rootProject.hasProperty('disablePreDex')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
14
docs/google-play/full_description.txt
Normal file
|
@ -0,0 +1,14 @@
|
|||
K-9 Mail is an open source email client with support for multiple accounts, search, IMAP push email, multi-folder sync, flagging, filing, signatures, BCC-self, PGP/MIME & more!
|
||||
|
||||
K-9 supports IMAP, POP3 and Exchange 2003/2007 (with WebDAV).
|
||||
|
||||
Install the app "OpenKeychain: Easy PGP" to encrypt/decrypt your emails using OpenPGP.
|
||||
|
||||
K-9 is a community developed project. If you're interested in helping to make the most popular open source email client on Android even better, please join us! You can find our bug tracker, source code, mailing list and wiki at https://github.com/k9mail/k-9
|
||||
We're always happy to welcome new developers, designers, documenters, bug triagers and friends.
|
||||
|
||||
If you're having trouble with K-9, please report a bug at https://github.com/k9mail/k-9 rather than just leaving a one-star review. We don't mind you telling the world that you're frustrated, but if you use our bug tracker, we have a better chance of fixing whatever's giving you a hard time.
|
||||
|
||||
You can find K-9's release notes at: http://bit.ly/new-k9
|
||||
|
||||
(People sometimes call K-9: K9, K9 Mail, K-9 Email, K9 Email, K9 E-Mail, k9mail or k9email.)
|
1
docs/google-play/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
|||
K-9 Mail is a 100% free and open source email client for Android.
|
8
gradle.properties
Normal file
|
@ -0,0 +1,8 @@
|
|||
androidCompileSdkVersion=24
|
||||
androidBuildToolsVersion=24.0.3
|
||||
androidSupportLibraryVersion=23.1.1
|
||||
|
||||
robolectricVersion=3.1.1
|
||||
junitVersion=4.12
|
||||
mockitoVersion=1.10.19
|
||||
okioVersion=1.11.0
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +1,6 @@
|
|||
#Mon Dec 21 02:24:01 CET 2015
|
||||
#Sun Oct 09 01:26:59 BST 2016
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.12-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-3.1-all.zip
|
||||
|
|
51
gradlew
vendored
|
@ -6,12 +6,30 @@
|
|||
##
|
||||
##############################################################################
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
|
@ -30,6 +48,7 @@ die ( ) {
|
|||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
|
@ -40,26 +59,11 @@ case "`uname`" in
|
|||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
|
@ -85,7 +89,7 @@ location of your Java installation."
|
|||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
|
@ -157,4 +161,9 @@ function splitJvmOpts() {
|
|||
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
|
||||
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
|
||||
|
||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||
if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then
|
||||
cd "$(dirname "$0")"
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
|
||||
|
|
14
gradlew.bat
vendored
|
@ -8,14 +8,14 @@
|
|||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
|
@ -46,10 +46,9 @@ echo location of your Java installation.
|
|||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windowz variants
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
if "%@eval[2+2]" == "4" goto 4NT_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
|
@ -60,11 +59,6 @@ set _SKIP=2
|
|||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
goto execute
|
||||
|
||||
:4NT_args
|
||||
@rem Get arguments from the 4NT Shell from JP Software
|
||||
set CMD_LINE_ARGS=%$
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
|
2580
images/K-9_Mail.ai
1148
images/K-9_Mail.svg
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 42 KiB |
3
images/drawables-pgp/24x24/bullet_point_neutral.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg id="Capa_1" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" style="enable-background:new 0 0 457.47 457.469;" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" height="457.47px" viewBox="0 0 457.47 457.469" width="457.47px" version="1.1" y="0px" x="0px" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata id="metadata41"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><dc:title/></cc:Work></rdf:RDF></metadata><g id="g3" transform="matrix(0.57627118,0,0,0.57627118,96.921399,96.921399)"><path id="path5" d="M228.75,96.906c-72.8,0-131.84,59.044-131.84,131.84,0,72.8,59.044,131.81,131.84,131.81,72.8,0,131.81-59.01,131.81-131.81s-59.01-131.84-131.81-131.84zm0,76.594c30.51,0,55.22,24.74,55.22,55.25s-24.71,55.22-55.22,55.22-55.25-24.71-55.25-55.22,24.74-55.25,55.25-55.25z" transform="matrix(1.7352942,0,0,1.7352942,-168.18714,-168.18714)"/></g></svg>
|
After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 68 KiB |
|
@ -1,3 +1,5 @@
|
|||
Please search to check for an existing issue (including closed issues, for which the fix may not have yet been released) before opening a new issue: https://github.com/k9mail/k-9/issues?q=is%3Aissue
|
||||
|
||||
### Expected behavior
|
||||
Tell us what should happen
|
||||
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
apply plugin: 'com.android.library'
|
||||
apply from: '../gradle/plugins/checkstyle-android.gradle'
|
||||
apply from: '../gradle/plugins/findbugs-android.gradle'
|
||||
apply plugin: 'jacoco'
|
||||
|
||||
if (!rootProject.optimizeForDevelopment) {
|
||||
apply from: '../gradle/plugins/checkstyle-android.gradle'
|
||||
apply from: '../gradle/plugins/findbugs-android.gradle'
|
||||
}
|
||||
|
||||
if (rootProject.testCoverage) {
|
||||
apply plugin: 'jacoco'
|
||||
}
|
||||
|
||||
repositories {
|
||||
jcenter()
|
||||
|
@ -13,20 +19,20 @@ dependencies {
|
|||
compile 'commons-io:commons-io:2.4'
|
||||
compile 'com.jcraft:jzlib:1.0.7'
|
||||
compile 'com.beetstra.jutf7:jutf7:1.0.0'
|
||||
compile 'com.android.support:support-annotations:23.3.0'
|
||||
compile "com.android.support:support-annotations:${androidSupportLibraryVersion}"
|
||||
|
||||
androidTestCompile 'com.android.support.test:runner:0.4.1'
|
||||
androidTestCompile 'com.madgag.spongycastle:pg:1.51.0.0'
|
||||
|
||||
testCompile 'com.squareup.okio:okio:1.6.0'
|
||||
testCompile 'org.robolectric:robolectric:3.0'
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile 'org.mockito:mockito-core:1.10.19'
|
||||
testCompile "com.squareup.okio:okio:${okioVersion}"
|
||||
testCompile "org.robolectric:robolectric:${robolectricVersion}"
|
||||
testCompile "junit:junit:${junitVersion}"
|
||||
testCompile "org.mockito:mockito-core:${mockitoVersion}"
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion rootProject.compileSdkVersion
|
||||
buildToolsVersion rootProject.buildToolsVersion
|
||||
compileSdkVersion androidCompileSdkVersion.toInteger()
|
||||
buildToolsVersion androidBuildToolsVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 15
|
||||
|
|
|
@ -221,7 +221,7 @@ public class PgpMimeMessageTest {
|
|||
InputStream messageInputStream = new ByteArrayInputStream(messageSource.getBytes());
|
||||
MimeMessage message;
|
||||
try {
|
||||
message = new MimeMessage(messageInputStream, true);
|
||||
message = MimeMessage.parseMimeMessage(messageInputStream, true);
|
||||
} finally {
|
||||
messageInputStream.close();
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ import java.io.InputStream;
|
|||
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
import android.test.AndroidTestCase;
|
||||
|
||||
import com.fsck.k9.mail.internet.BinaryTempFileBody;
|
||||
import com.fsck.k9.mail.internet.MimeMessage;
|
||||
|
@ -58,7 +57,7 @@ public class ReconstructMessageTest {
|
|||
InputStream messageInputStream = new ByteArrayInputStream(messageSource.getBytes());
|
||||
MimeMessage message;
|
||||
try {
|
||||
message = new MimeMessage(messageInputStream, true);
|
||||
message = MimeMessage.parseMimeMessage(messageInputStream, true);
|
||||
} finally {
|
||||
messageInputStream.close();
|
||||
}
|
||||
|
|
|
@ -84,7 +84,7 @@ public class Address implements Serializable {
|
|||
return null;
|
||||
}
|
||||
|
||||
return mAddress.substring(hostIdx+1);
|
||||
return mAddress.substring(hostIdx + 1);
|
||||
}
|
||||
|
||||
public void setAddress(String address) {
|
||||
|
@ -95,7 +95,8 @@ public class Address implements Serializable {
|
|||
return mPersonal;
|
||||
}
|
||||
|
||||
public void setPersonal(String personal) {
|
||||
public void setPersonal(String newPersonal) {
|
||||
String personal = newPersonal;
|
||||
if ("".equals(personal)) {
|
||||
personal = null;
|
||||
}
|
||||
|
@ -144,7 +145,7 @@ public class Address implements Serializable {
|
|||
for (int i = 0, count = parsedList.size(); i < count; i++) {
|
||||
org.apache.james.mime4j.dom.address.Address address = parsedList.get(i);
|
||||
if (address instanceof Mailbox) {
|
||||
Mailbox mailbox = (Mailbox)address;
|
||||
Mailbox mailbox = (Mailbox) address;
|
||||
addresses.add(new Address(mailbox.getLocalPart() + "@" + mailbox.getDomain(), mailbox.getName(), false));
|
||||
} else {
|
||||
Log.e(LOG_TAG, "Unknown address type from Mime4J: "
|
||||
|
@ -161,19 +162,29 @@ public class Address implements Serializable {
|
|||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o instanceof Address) {
|
||||
Address other = (Address) o;
|
||||
if (mPersonal != null && other.mPersonal != null && !mPersonal.equals(other.mPersonal)) {
|
||||
return false;
|
||||
}
|
||||
return mAddress.equals(other.mAddress);
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
return super.equals(o);
|
||||
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Address address = (Address) o;
|
||||
|
||||
if (mAddress != null ? !mAddress.equals(address.mAddress) : address.mAddress != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return mPersonal != null ? mPersonal.equals(address.mPersonal) : address.mPersonal == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = mAddress.hashCode();
|
||||
int hash = 0;
|
||||
if (mAddress != null) {
|
||||
hash += mAddress.hashCode();
|
||||
}
|
||||
if (mPersonal != null) {
|
||||
hash += 3 * mPersonal.hashCode();
|
||||
}
|
||||
|
@ -299,8 +310,9 @@ public class Address implements Serializable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Ensures that the given string starts and ends with the double quote character. The string is not modified in any way except to add the
|
||||
* double quote character to start and end if it's not already there.
|
||||
* Ensures that the given string starts and ends with the double quote character.
|
||||
* The string is not modified in any way except to add the double quote character to start
|
||||
* and end if it's not already there.
|
||||
* sample -> "sample"
|
||||
* "sample" -> "sample"
|
||||
* ""sample"" -> ""sample""
|
||||
|
|
|
@ -16,6 +16,13 @@ public enum AuthType {
|
|||
CRAM_MD5,
|
||||
EXTERNAL,
|
||||
|
||||
/**
|
||||
* XOAUTH2 is an OAuth2.0 protocol designed/used by GMail.
|
||||
*
|
||||
* https://developers.google.com/gmail/xoauth2_protocol#the_sasl_xoauth2_mechanism
|
||||
*/
|
||||
XOAUTH2,
|
||||
|
||||
/*
|
||||
* The following are obsolete authentication settings that were used with
|
||||
* SMTP. They are no longer presented to the user as options, but they may
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.fsck.k9.mail;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
import com.fsck.k9.mail.filter.Base64;
|
||||
|
@ -7,6 +8,7 @@ import com.fsck.k9.mail.filter.Hex;
|
|||
|
||||
public class Authentication {
|
||||
private static final String US_ASCII = "US-ASCII";
|
||||
private static final String XOAUTH_FORMAT = "user=%1s\001auth=Bearer %2s\001\001";
|
||||
|
||||
/**
|
||||
* Computes the response for CRAM-MD5 authentication mechanism given the user credentials and
|
||||
|
@ -82,4 +84,12 @@ public class Authentication {
|
|||
throw new MessagingException("Something went wrong during CRAM-MD5 computation", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String computeXoauth(String username, String authToken) throws UnsupportedEncodingException {
|
||||
String formattedAuthenticationString = String.format(XOAUTH_FORMAT, username, authToken);
|
||||
byte[] base64encodedAuthenticationString =
|
||||
Base64.encodeBase64(formattedAuthenticationString.getBytes());
|
||||
|
||||
return new String(base64encodedAuthenticationString, US_ASCII);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
package com.fsck.k9.mail;
|
||||
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
import android.support.annotation.VisibleForTesting;
|
||||
|
||||
|
||||
public class BoundaryGenerator {
|
||||
private static final BoundaryGenerator INSTANCE = new BoundaryGenerator(new Random());
|
||||
|
||||
private static final int BOUNDARY_CHARACTER_COUNT = 30;
|
||||
private static final char[] BASE36_MAP = {
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
|
||||
'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
|
||||
'U', 'V', 'W', 'X', 'Y', 'Z'
|
||||
};
|
||||
|
||||
|
||||
private final Random random;
|
||||
|
||||
|
||||
public static BoundaryGenerator getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
BoundaryGenerator(Random random) {
|
||||
this.random = random;
|
||||
}
|
||||
|
||||
public String generateBoundary() {
|
||||
StringBuilder builder = new StringBuilder(4 + BOUNDARY_CHARACTER_COUNT);
|
||||
builder.append("----");
|
||||
|
||||
for (int i = 0; i < BOUNDARY_CHARACTER_COUNT; i++) {
|
||||
builder.append(BASE36_MAP[random.nextInt(36)]);
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package com.fsck.k9.mail;
|
||||
|
||||
|
||||
/**
|
||||
* A CompositeBody is a {@link Body} extension that can contain subparts that
|
||||
* may require recursing through or iterating over when converting the
|
||||
* CompositeBody from 8bit to 7bit encoding. The {@link Part} to which a
|
||||
* CompositeBody belongs is only permitted to use 8bit or 7bit content transfer
|
||||
* encoding for the CompositeBody.
|
||||
*
|
||||
*/
|
||||
public interface CompositeBody extends Body {
|
||||
|
||||
/**
|
||||
* Called just prior to transmission, once the type of transport is known to
|
||||
* be 7bit.
|
||||
* <p>
|
||||
* All subparts that are 8bit and of type {@link CompositeBody} will be
|
||||
* converted to 7bit and recursed. All supbparts that are 8bit but not
|
||||
* of type CompositeBody will be converted to quoted-printable. Bodies with
|
||||
* encodings other than 8bit remain unchanged.
|
||||
*
|
||||
* @throws MessagingException
|
||||
*
|
||||
*/
|
||||
public abstract void setUsing7bitTransport() throws MessagingException;
|
||||
|
||||
}
|
|
@ -15,7 +15,7 @@ import com.fsck.k9.mail.filter.EOLConvertingOutputStream;
|
|||
|
||||
import static com.fsck.k9.mail.K9MailLib.LOG_TAG;
|
||||
|
||||
public abstract class Message implements Part, CompositeBody {
|
||||
public abstract class Message implements Part, Body {
|
||||
|
||||
public enum RecipientType {
|
||||
TO, CC, BCC,
|
||||
|
@ -145,7 +145,7 @@ public abstract class Message implements Part, CompositeBody {
|
|||
|
||||
public abstract boolean hasAttachments();
|
||||
|
||||
public abstract int getSize();
|
||||
public abstract long getSize();
|
||||
|
||||
public void delete(String trashFolderName) throws MessagingException {}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import org.apache.james.mime4j.util.MimeUtil;
|
|||
import com.fsck.k9.mail.internet.CharsetSupport;
|
||||
import com.fsck.k9.mail.internet.TextBody;
|
||||
|
||||
public abstract class Multipart implements CompositeBody {
|
||||
public abstract class Multipart implements Body {
|
||||
private Part mParent;
|
||||
|
||||
private final List<BodyPart> mParts = new ArrayList<BodyPart>();
|
||||
|
@ -48,8 +48,7 @@ public abstract class Multipart implements CompositeBody {
|
|||
public void setEncoding(String encoding) throws MessagingException {
|
||||
if (!MimeUtil.ENC_7BIT.equalsIgnoreCase(encoding)
|
||||
&& !MimeUtil.ENC_8BIT.equalsIgnoreCase(encoding)) {
|
||||
throw new MessagingException(
|
||||
"Incompatible content-transfer-encoding applied to a CompositeBody");
|
||||
throw new MessagingException("Incompatible content-transfer-encoding for a multipart/* body");
|
||||
}
|
||||
|
||||
/* Nothing else to do. Each subpart has its own separate encoding */
|
||||
|
|
|
@ -41,20 +41,6 @@ public interface Part {
|
|||
|
||||
void writeHeaderTo(OutputStream out) throws IOException, MessagingException;
|
||||
|
||||
/**
|
||||
* Called just prior to transmission, once the type of transport is known to
|
||||
* be 7bit.
|
||||
* <p>
|
||||
* All bodies that are 8bit will be converted to 7bit and recursed if of
|
||||
* type {@link CompositeBody}, or will be converted to quoted-printable in all other
|
||||
* cases. Bodies with encodings other than 8bit remain unchanged.
|
||||
*
|
||||
* @throws MessagingException
|
||||
*
|
||||
*/
|
||||
//TODO perhaps it would be clearer to use a flag "force7bit" in writeTo
|
||||
void setUsing7bitTransport() throws MessagingException;
|
||||
|
||||
String getServerExtra();
|
||||
|
||||
void setServerExtra(String serverExtra);
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.fsck.k9.mail;
|
|||
|
||||
import android.content.Context;
|
||||
|
||||
import com.fsck.k9.mail.oauth.OAuth2TokenProvider;
|
||||
import com.fsck.k9.mail.ssl.DefaultTrustedSocketFactory;
|
||||
import com.fsck.k9.mail.store.StoreConfig;
|
||||
import com.fsck.k9.mail.ServerSettings.Type;
|
||||
|
@ -19,10 +20,12 @@ public abstract class Transport {
|
|||
// RFC 1047
|
||||
protected static final int SOCKET_READ_TIMEOUT = 300000;
|
||||
|
||||
public synchronized static Transport getInstance(Context context, StoreConfig storeConfig) throws MessagingException {
|
||||
public static synchronized Transport getInstance(Context context, StoreConfig storeConfig)
|
||||
throws MessagingException {
|
||||
String uri = storeConfig.getTransportUri();
|
||||
if (uri.startsWith("smtp")) {
|
||||
return new SmtpTransport(storeConfig, new DefaultTrustedSocketFactory(context));
|
||||
OAuth2TokenProvider oauth2TokenProvider = null;
|
||||
return new SmtpTransport(storeConfig, new DefaultTrustedSocketFactory(context), oauth2TokenProvider);
|
||||
} else if (uri.startsWith("webdav")) {
|
||||
return new WebDavTransport(storeConfig);
|
||||
} else {
|
||||
|
|
|
@ -69,4 +69,10 @@ public class FixedLengthInputStream extends InputStream {
|
|||
public String toString() {
|
||||
return String.format(Locale.US, "FixedLengthInputStream(in=%s, length=%d)", mIn.toString(), mLength);
|
||||
}
|
||||
|
||||
public void skipRemaining() throws IOException {
|
||||
while (available() > 0) {
|
||||
skip(available());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,16 @@
|
|||
package com.fsck.k9.mail.internet;
|
||||
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.fsck.k9.mail.K9MailLib;
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
import com.fsck.k9.mail.filter.Base64OutputStream;
|
||||
|
@ -7,10 +18,6 @@ import org.apache.commons.io.IOUtils;
|
|||
import org.apache.james.mime4j.codec.QuotedPrintableOutputStream;
|
||||
import org.apache.james.mime4j.util.MimeUtil;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
|
||||
/**
|
||||
* A Body that is backed by a temp file. The Body exposes a getOutputStream method that allows
|
||||
|
|
|
@ -1,19 +1,14 @@
|
|||
package com.fsck.k9.mail.internet;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.james.mime4j.util.MimeUtil;
|
||||
|
||||
import com.fsck.k9.mail.CompositeBody;
|
||||
import com.fsck.k9.mail.Body;
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
import org.apache.james.mime4j.util.MimeUtil;
|
||||
|
||||
/**
|
||||
* A {@link BinaryTempFileBody} extension containing a body of type message/rfc822.
|
||||
*/
|
||||
public class BinaryTempFileMessageBody extends BinaryTempFileBody implements CompositeBody {
|
||||
public class BinaryTempFileMessageBody extends BinaryTempFileBody implements Body {
|
||||
|
||||
public BinaryTempFileMessageBody(String encoding) {
|
||||
super(encoding);
|
||||
|
@ -23,41 +18,8 @@ public class BinaryTempFileMessageBody extends BinaryTempFileBody implements Com
|
|||
public void setEncoding(String encoding) throws MessagingException {
|
||||
if (!MimeUtil.ENC_7BIT.equalsIgnoreCase(encoding)
|
||||
&& !MimeUtil.ENC_8BIT.equalsIgnoreCase(encoding)) {
|
||||
throw new MessagingException(
|
||||
"Incompatible content-transfer-encoding applied to a CompositeBody");
|
||||
throw new MessagingException("Incompatible content-transfer-encoding for a message/rfc822 body");
|
||||
}
|
||||
mEncoding = encoding;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(OutputStream out) throws IOException, MessagingException {
|
||||
InputStream in = getInputStream();
|
||||
try {
|
||||
if (MimeUtil.ENC_7BIT.equalsIgnoreCase(mEncoding)) {
|
||||
/*
|
||||
* If we knew the message was already 7bit clean, then it
|
||||
* could be sent along without processing. But since we
|
||||
* don't know, we recursively parse it.
|
||||
*/
|
||||
MimeMessage message = new MimeMessage(in, true);
|
||||
message.setUsing7bitTransport();
|
||||
message.writeTo(out);
|
||||
} else {
|
||||
IOUtils.copy(in, out);
|
||||
}
|
||||
} finally {
|
||||
IOUtils.closeQuietly(in);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUsing7bitTransport() throws MessagingException {
|
||||
/*
|
||||
* There's nothing to recurse into here, so there's nothing to do.
|
||||
* The enclosing BodyPart already called setEncoding(MimeUtil.ENC_7BIT). Once
|
||||
* writeTo() is called, the file with the rfc822 body will be opened
|
||||
* for reading and will then be recursed.
|
||||
*/
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,11 +20,13 @@ import com.fsck.k9.mail.Message;
|
|||
import com.fsck.k9.mail.MessagingException;
|
||||
import com.fsck.k9.mail.Multipart;
|
||||
import com.fsck.k9.mail.Part;
|
||||
import com.fsck.k9.mail.internet.Viewable.Flowed;
|
||||
import org.apache.commons.io.input.BoundedInputStream;
|
||||
|
||||
import static com.fsck.k9.mail.K9MailLib.LOG_TAG;
|
||||
import static com.fsck.k9.mail.internet.CharsetSupport.fixupCharset;
|
||||
import static com.fsck.k9.mail.internet.MimeUtility.getHeaderParameter;
|
||||
import static com.fsck.k9.mail.internet.MimeUtility.isFormatFlowed;
|
||||
import static com.fsck.k9.mail.internet.MimeUtility.isSameMimeType;
|
||||
import static com.fsck.k9.mail.internet.Viewable.Alternative;
|
||||
import static com.fsck.k9.mail.internet.Viewable.Html;
|
||||
|
@ -47,17 +49,17 @@ public class MessageExtractor {
|
|||
if ((part != null) && (part.getBody() != null)) {
|
||||
final Body body = part.getBody();
|
||||
if (body instanceof TextBody) {
|
||||
return ((TextBody)body).getText();
|
||||
return ((TextBody) body).getRawText();
|
||||
}
|
||||
final String mimeType = part.getMimeType();
|
||||
if (mimeType != null && MimeUtility.mimeTypeMatches(mimeType, "text/*") ||
|
||||
part.isMimeType("application/pgp")) {
|
||||
return getTextFromTextPart(part, body, mimeType, textSizeLimit);
|
||||
} else {
|
||||
throw new MessagingException("Provided non-text part: " + part);
|
||||
throw new MessagingException("Provided non-text part: " + mimeType);
|
||||
}
|
||||
} else {
|
||||
throw new MessagingException("Provided invalid part: " + part);
|
||||
throw new MessagingException("Provided invalid part");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(LOG_TAG, "Unable to getTextFromPart", e);
|
||||
|
@ -188,13 +190,17 @@ public class MessageExtractor {
|
|||
return;
|
||||
}
|
||||
String mimeType = part.getMimeType();
|
||||
Viewable viewable;
|
||||
if (isSameMimeType(mimeType, "text/plain")) {
|
||||
Text text = new Text(part);
|
||||
outputViewableParts.add(text);
|
||||
if (isFormatFlowed(part.getContentType())) {
|
||||
viewable = new Flowed(part);
|
||||
} else {
|
||||
viewable = new Text(part);
|
||||
}
|
||||
} else {
|
||||
Html html = new Html(part);
|
||||
outputViewableParts.add(html);
|
||||
viewable = new Html(part);
|
||||
}
|
||||
outputViewableParts.add(viewable);
|
||||
} else if (isSameMimeType(part.getMimeType(), "application/pgp-signature")) {
|
||||
// ignore this type explicitly
|
||||
} else {
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
package com.fsck.k9.mail.internet;
|
||||
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
import android.support.annotation.VisibleForTesting;
|
||||
|
||||
import com.fsck.k9.mail.Address;
|
||||
import com.fsck.k9.mail.Message;
|
||||
|
||||
|
||||
public class MessageIdGenerator {
|
||||
public static MessageIdGenerator getInstance() {
|
||||
return new MessageIdGenerator();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
MessageIdGenerator() {
|
||||
}
|
||||
|
||||
public String generateMessageId(Message message) {
|
||||
String hostname = null;
|
||||
|
||||
Address[] from = message.getFrom();
|
||||
if (from != null && from.length >= 1) {
|
||||
hostname = from[0].getHostname();
|
||||
}
|
||||
|
||||
if (hostname == null) {
|
||||
Address[] replyTo = message.getReplyTo();
|
||||
if (replyTo != null && replyTo.length >= 1) {
|
||||
hostname = replyTo[0].getHostname();
|
||||
}
|
||||
}
|
||||
|
||||
if (hostname == null) {
|
||||
hostname = "email.android.com";
|
||||
}
|
||||
|
||||
String uuid = generateUuid();
|
||||
return "<" + uuid + "@" + hostname + ">";
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected String generateUuid() {
|
||||
// We use upper case here to match Apple Mail Message-ID format (for privacy)
|
||||
return UUID.randomUUID().toString().toUpperCase(Locale.US);
|
||||
}
|
||||
}
|
|
@ -3,8 +3,8 @@ package com.fsck.k9.mail.internet;
|
|||
|
||||
import com.fsck.k9.mail.Body;
|
||||
import com.fsck.k9.mail.BodyPart;
|
||||
import com.fsck.k9.mail.CompositeBody;
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
import com.fsck.k9.mail.Multipart;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
|
@ -13,7 +13,6 @@ import java.io.OutputStreamWriter;
|
|||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.apache.james.mime4j.util.MimeUtil;
|
||||
|
||||
/**
|
||||
* TODO this is a close approximation of Message, need to update along with
|
||||
|
@ -95,7 +94,14 @@ public class MimeBodyPart extends BodyPart {
|
|||
@Override
|
||||
public String getContentType() {
|
||||
String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
|
||||
return (contentType == null) ? "text/plain" : MimeUtility.unfoldAndDecode(contentType);
|
||||
if (contentType != null) {
|
||||
return MimeUtility.unfoldAndDecode(contentType);
|
||||
}
|
||||
Multipart parent = getParent();
|
||||
if (parent != null && "multipart/digest".equals(parent.getMimeType())) {
|
||||
return "message/rfc822";
|
||||
}
|
||||
return "text/plain";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -147,44 +153,4 @@ public class MimeBodyPart extends BodyPart {
|
|||
mHeader.writeTo(out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUsing7bitTransport() throws MessagingException {
|
||||
String type = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
|
||||
/*
|
||||
* We don't trust that a multipart/* will properly have an 8bit encoding
|
||||
* header if any of its subparts are 8bit, so we automatically recurse
|
||||
* (as long as its not multipart/signed).
|
||||
*/
|
||||
if (mBody instanceof CompositeBody && !MimeUtility.isSameMimeType(type, "multipart/signed")) {
|
||||
setEncoding(MimeUtil.ENC_7BIT);
|
||||
// recurse
|
||||
((CompositeBody) mBody).setUsing7bitTransport();
|
||||
} else if (!MimeUtil.ENC_8BIT
|
||||
.equalsIgnoreCase(getFirstHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING))) {
|
||||
return;
|
||||
} else if (type != null &&
|
||||
(MimeUtility.isSameMimeType(type, "multipart/signed") || MimeUtility.isMessage(type))) {
|
||||
/*
|
||||
* This shouldn't happen. In any case, it would be wrong to convert
|
||||
* them to some other encoding for 7bit transport.
|
||||
*
|
||||
* RFC 1847 says multipart/signed must be 7bit. It also says their
|
||||
* bodies must be treated as opaque, so we must not change the
|
||||
* encoding.
|
||||
*
|
||||
* We've dealt with (CompositeBody) type message/rfc822 above. Here
|
||||
* we must deal with all other message/* types. RFC 2045 says
|
||||
* message/* can only be 7bit or 8bit. RFC 2046 says unknown
|
||||
* message/* types must be treated as application/octet-stream,
|
||||
* which means we can't recurse into them. It also says that
|
||||
* existing subtypes message/partial and message/external must only
|
||||
* be 7bit, and that future subtypes "should be" 7bit.
|
||||
*/
|
||||
throw new MessagingException(
|
||||
"Unable to convert 8bit body part to 7bit");
|
||||
} else {
|
||||
setEncoding(MimeUtil.ENC_QUOTED_PRINTABLE);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -170,10 +170,6 @@ public class MimeHeader implements Cloneable {
|
|||
if (raw == null) {
|
||||
throw new NullPointerException("Argument 'raw' cannot be null");
|
||||
}
|
||||
if (name != null && !raw.startsWith(name + ":")) {
|
||||
throw new IllegalArgumentException("The value of 'raw' needs to start with the supplied field name " +
|
||||
"followed by a colon");
|
||||
}
|
||||
|
||||
return new Field(name, null, raw);
|
||||
}
|
||||
|
|
|
@ -14,14 +14,12 @@ import java.util.LinkedList;
|
|||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.TimeZone;
|
||||
import java.util.UUID;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import com.fsck.k9.mail.Address;
|
||||
import com.fsck.k9.mail.Body;
|
||||
import com.fsck.k9.mail.BodyPart;
|
||||
import com.fsck.k9.mail.CompositeBody;
|
||||
import com.fsck.k9.mail.Message;
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
import com.fsck.k9.mail.Multipart;
|
||||
|
@ -36,7 +34,7 @@ import org.apache.james.mime4j.parser.MimeStreamParser;
|
|||
import org.apache.james.mime4j.stream.BodyDescriptor;
|
||||
import org.apache.james.mime4j.stream.Field;
|
||||
import org.apache.james.mime4j.stream.MimeConfig;
|
||||
import org.apache.james.mime4j.util.MimeUtil;
|
||||
|
||||
|
||||
/**
|
||||
* An implementation of Message that stores all of it's metadata in RFC 822 and
|
||||
|
@ -61,20 +59,14 @@ public class MimeMessage extends Message {
|
|||
protected int mSize;
|
||||
private String serverExtra;
|
||||
|
||||
public MimeMessage() {
|
||||
|
||||
public static MimeMessage parseMimeMessage(InputStream in, boolean recurse) throws IOException, MessagingException {
|
||||
MimeMessage mimeMessage = new MimeMessage();
|
||||
mimeMessage.parse(in, recurse);
|
||||
return mimeMessage;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parse the given InputStream using Apache Mime4J to build a MimeMessage.
|
||||
*
|
||||
* @param in
|
||||
* @param recurse A boolean indicating to recurse through all nested MimeMessage subparts.
|
||||
* @throws IOException
|
||||
* @throws MessagingException
|
||||
*/
|
||||
public MimeMessage(InputStream in, boolean recurse) throws IOException, MessagingException {
|
||||
parse(in, recurse);
|
||||
public MimeMessage() {
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -190,7 +182,7 @@ public class MimeMessage extends Message {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int getSize() {
|
||||
public long getSize() {
|
||||
return mSize;
|
||||
}
|
||||
|
||||
|
@ -319,27 +311,6 @@ public class MimeMessage extends Message {
|
|||
return mMessageId;
|
||||
}
|
||||
|
||||
public void generateMessageId() throws MessagingException {
|
||||
String hostname = null;
|
||||
|
||||
if (mFrom != null && mFrom.length >= 1) {
|
||||
hostname = mFrom[0].getHostname();
|
||||
}
|
||||
|
||||
if (hostname == null && mReplyTo != null && mReplyTo.length >= 1) {
|
||||
hostname = mReplyTo[0].getHostname();
|
||||
}
|
||||
|
||||
if (hostname == null) {
|
||||
hostname = "email.android.com";
|
||||
}
|
||||
|
||||
/* We use upper case here to match Apple Mail Message-ID format (for privacy) */
|
||||
String messageId = "<" + UUID.randomUUID().toString().toUpperCase(Locale.US) + "@" + hostname + ">";
|
||||
|
||||
setMessageId(messageId);
|
||||
}
|
||||
|
||||
public void setMessageId(String messageId) {
|
||||
setHeader("Message-ID", messageId);
|
||||
mMessageId = messageId;
|
||||
|
@ -459,7 +430,7 @@ public class MimeMessage extends Message {
|
|||
|
||||
@Override
|
||||
public InputStream getInputStream() throws MessagingException {
|
||||
return null;
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -529,15 +500,11 @@ public class MimeMessage extends Message {
|
|||
expect(Part.class);
|
||||
|
||||
Part e = (Part)stack.peek();
|
||||
try {
|
||||
String mimeType = bd.getMimeType();
|
||||
String boundary = bd.getBoundary();
|
||||
MimeMultipart multiPart = new MimeMultipart(mimeType, boundary);
|
||||
e.setBody(multiPart);
|
||||
stack.addFirst(multiPart);
|
||||
} catch (MessagingException me) {
|
||||
throw new MimeException(me.getMessage(), me);
|
||||
}
|
||||
String mimeType = bd.getMimeType();
|
||||
String boundary = bd.getBoundary();
|
||||
MimeMultipart multiPart = new MimeMultipart(mimeType, boundary);
|
||||
e.setBody(multiPart);
|
||||
stack.addFirst(multiPart);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -658,47 +625,6 @@ public class MimeMessage extends Message {
|
|||
return false;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setUsing7bitTransport() throws MessagingException {
|
||||
String type = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
|
||||
/*
|
||||
* We don't trust that a multipart/* will properly have an 8bit encoding
|
||||
* header if any of its subparts are 8bit, so we automatically recurse
|
||||
* (as long as its not multipart/signed).
|
||||
*/
|
||||
if (mBody instanceof CompositeBody && !MimeUtility.isSameMimeType(type, "multipart/signed")) {
|
||||
setEncoding(MimeUtil.ENC_7BIT);
|
||||
// recurse
|
||||
((CompositeBody) mBody).setUsing7bitTransport();
|
||||
} else if (!MimeUtil.ENC_8BIT
|
||||
.equalsIgnoreCase(getFirstHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING))) {
|
||||
return;
|
||||
} else if (type != null &&
|
||||
(MimeUtility.isSameMimeType(type, "multipart/signed") || MimeUtility.isMessage(type))) {
|
||||
/*
|
||||
* This shouldn't happen. In any case, it would be wrong to convert
|
||||
* them to some other encoding for 7bit transport.
|
||||
*
|
||||
* RFC 1847 says multipart/signed must be 7bit. It also says their
|
||||
* bodies must be treated as opaque, so we must not change the
|
||||
* encoding.
|
||||
*
|
||||
* We've dealt with (CompositeBody) type message/rfc822 above. Here
|
||||
* we must deal with all other message/* types. RFC 2045 says
|
||||
* message/* can only be 7bit or 8bit. RFC 2046 says unknown
|
||||
* message/* types must be treated as application/octet-stream,
|
||||
* which means we can't recurse into them. It also says that
|
||||
* existing subtypes message/partial and message/external must only
|
||||
* be 7bit, and that future subtypes "should be" 7bit.
|
||||
*/
|
||||
throw new MessagingException(
|
||||
"Unable to convert 8bit body part to 7bit");
|
||||
} else {
|
||||
setEncoding(MimeUtil.ENC_QUOTED_PRINTABLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServerExtra() {
|
||||
return serverExtra;
|
||||
|
|
|
@ -26,11 +26,8 @@ public class MimeMessageHelper {
|
|||
String mimeType = multipart.getMimeType();
|
||||
String contentType = String.format("%s; boundary=\"%s\"", mimeType, multipart.getBoundary());
|
||||
part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType);
|
||||
if (MimeUtility.isSameMimeType(mimeType, "multipart/signed")) {
|
||||
setEncoding(part, MimeUtil.ENC_7BIT);
|
||||
} else {
|
||||
setEncoding(part, MimeUtil.ENC_8BIT);
|
||||
}
|
||||
// note: if this is ever changed to 8bit, multipart/signed parts must always be 7bit!
|
||||
setEncoding(part, MimeUtil.ENC_7BIT);
|
||||
} else if (body instanceof TextBody) {
|
||||
String contentType;
|
||||
if (MimeUtility.mimeTypeMatches(part.getMimeType(), "text/*")) {
|
||||
|
@ -44,7 +41,10 @@ public class MimeMessageHelper {
|
|||
}
|
||||
part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType);
|
||||
|
||||
setEncoding(part, MimeUtil.ENC_8BIT);
|
||||
setEncoding(part, MimeUtil.ENC_QUOTED_PRINTABLE);
|
||||
} else if (body instanceof RawDataBody) {
|
||||
String encoding = ((RawDataBody) body).getEncoding();
|
||||
part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,26 +1,35 @@
|
|||
|
||||
package com.fsck.k9.mail.internet;
|
||||
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
|
||||
import com.fsck.k9.mail.BodyPart;
|
||||
import com.fsck.k9.mail.BoundaryGenerator;
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
import com.fsck.k9.mail.Multipart;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.Locale;
|
||||
import java.util.Random;
|
||||
|
||||
public class MimeMultipart extends Multipart {
|
||||
private String mimeType;
|
||||
private byte[] preamble;
|
||||
private byte[] epilogue;
|
||||
private final String boundary;
|
||||
|
||||
public MimeMultipart() throws MessagingException {
|
||||
boundary = generateBoundary();
|
||||
setSubType("mixed");
|
||||
|
||||
public static MimeMultipart newInstance() {
|
||||
String boundary = BoundaryGenerator.getInstance().generateBoundary();
|
||||
return new MimeMultipart(boundary);
|
||||
}
|
||||
|
||||
public MimeMultipart(String mimeType, String boundary) throws MessagingException {
|
||||
public MimeMultipart(String boundary) {
|
||||
this("multipart/mixed", boundary);
|
||||
}
|
||||
|
||||
public MimeMultipart(String mimeType, String boundary) {
|
||||
if (mimeType == null) {
|
||||
throw new IllegalArgumentException("mimeType can't be null");
|
||||
}
|
||||
|
@ -32,16 +41,6 @@ public class MimeMultipart extends Multipart {
|
|||
this.boundary = boundary;
|
||||
}
|
||||
|
||||
public static String generateBoundary() {
|
||||
Random random = new Random();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("----");
|
||||
for (int i = 0; i < 30; i++) {
|
||||
sb.append(Integer.toString(random.nextInt(36), 36));
|
||||
}
|
||||
return sb.toString().toUpperCase(Locale.US);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBoundary() {
|
||||
return boundary;
|
||||
|
@ -107,13 +106,6 @@ public class MimeMultipart extends Multipart {
|
|||
|
||||
@Override
|
||||
public InputStream getInputStream() throws MessagingException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUsing7bitTransport() throws MessagingException {
|
||||
for (BodyPart part : getBodyParts()) {
|
||||
part.setUsing7bitTransport();
|
||||
}
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,32 +1,36 @@
|
|||
|
||||
package com.fsck.k9.mail.internet;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import com.fsck.k9.mail.Body;
|
||||
import com.fsck.k9.mail.BodyPart;
|
||||
import com.fsck.k9.mail.Message;
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
import com.fsck.k9.mail.Multipart;
|
||||
import com.fsck.k9.mail.Part;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.io.input.BoundedInputStream;
|
||||
import org.apache.james.mime4j.codec.Base64InputStream;
|
||||
import org.apache.james.mime4j.codec.QuotedPrintableInputStream;
|
||||
import org.apache.james.mime4j.util.MimeUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import static com.fsck.k9.mail.K9MailLib.LOG_TAG;
|
||||
|
||||
|
||||
public class MimeUtility {
|
||||
public static final String DEFAULT_ATTACHMENT_MIME_TYPE = "application/octet-stream";
|
||||
public static final String K9_SETTINGS_MIME_TYPE = "application/x-k9settings";
|
||||
private static final String TEXT_PLAIN = "text/plain";
|
||||
private static final String HEADER_PARAM_FORMAT = "format";
|
||||
private static final String HEADER_FORMAT_FLOWED = "flowed";
|
||||
|
||||
/*
|
||||
* http://www.w3schools.com/media/media_mimeref.asp
|
||||
|
@ -221,6 +225,7 @@ public class MimeUtility {
|
|||
{ "epub", "application/epub+zip"},
|
||||
{ "es3", "application/vnd.eszigno3+xml"},
|
||||
{ "esf", "application/vnd.epson.esf"},
|
||||
{ "espass", "application/vnd.espass-espass+zip"},
|
||||
{ "et3", "application/vnd.eszigno3+xml"},
|
||||
{ "etx", "text/x-setext"},
|
||||
{ "evy", "application/envoy"},
|
||||
|
@ -1045,7 +1050,8 @@ public class MimeUtility {
|
|||
}
|
||||
};
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Encoding for RawDataBody not supported: " + encoding);
|
||||
Log.w(LOG_TAG, "Unsupported encoding: " + encoding);
|
||||
inputStream = rawInputStream;
|
||||
}
|
||||
} else {
|
||||
inputStream = body.getInputStream();
|
||||
|
@ -1138,4 +1144,13 @@ public class MimeUtility {
|
|||
public static boolean isSameMimeType(String mimeType, String otherMimeType) {
|
||||
return mimeType != null && mimeType.equalsIgnoreCase(otherMimeType);
|
||||
}
|
||||
|
||||
static boolean isFormatFlowed(String contentType) {
|
||||
String mimeType = getHeaderParameter(contentType, null);
|
||||
if (isSameMimeType(TEXT_PLAIN, mimeType)) {
|
||||
String formatParameter = getHeaderParameter(contentType, HEADER_PARAM_FORMAT);
|
||||
return HEADER_FORMAT_FLOWED.equalsIgnoreCase(formatParameter);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
|
||||
package com.fsck.k9.mail.internet;
|
||||
|
||||
import com.fsck.k9.mail.Body;
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
|
@ -10,109 +8,114 @@ import java.io.InputStream;
|
|||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import com.fsck.k9.mail.Body;
|
||||
import com.fsck.k9.mail.K9MailLib;
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
import com.fsck.k9.mail.filter.CountingOutputStream;
|
||||
import com.fsck.k9.mail.filter.SignSafeOutputStream;
|
||||
|
||||
import org.apache.james.mime4j.codec.QuotedPrintableOutputStream;
|
||||
import org.apache.james.mime4j.util.MimeUtil;
|
||||
|
||||
public class TextBody implements Body, SizeAware {
|
||||
|
||||
/**
|
||||
* Immutable empty byte array
|
||||
*/
|
||||
private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
|
||||
|
||||
private final String mBody;
|
||||
private String mEncoding;
|
||||
private String mCharset = "UTF-8";
|
||||
|
||||
private final String text;
|
||||
private String encoding;
|
||||
private String charset = "UTF-8";
|
||||
// Length of the message composed (as opposed to quoted). I don't like the name of this variable and am open to
|
||||
// suggestions as to what it should otherwise be. -achen 20101207
|
||||
private Integer mComposedMessageLength;
|
||||
@Nullable
|
||||
private Integer composedMessageLength;
|
||||
// Offset from position 0 where the composed message begins.
|
||||
private Integer mComposedMessageOffset;
|
||||
@Nullable
|
||||
private Integer composedMessageOffset;
|
||||
|
||||
public TextBody(String body) {
|
||||
this.mBody = body;
|
||||
this.text = body;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(OutputStream out) throws IOException, MessagingException {
|
||||
if (mBody != null) {
|
||||
byte[] bytes = mBody.getBytes(mCharset);
|
||||
if (MimeUtil.ENC_8BIT.equalsIgnoreCase(mEncoding)) {
|
||||
if (text != null) {
|
||||
byte[] bytes = text.getBytes(charset);
|
||||
if (MimeUtil.ENC_QUOTED_PRINTABLE.equalsIgnoreCase(encoding)) {
|
||||
writeSignSafeQuotedPrintable(out, bytes);
|
||||
} else if (MimeUtil.ENC_8BIT.equalsIgnoreCase(encoding)) {
|
||||
out.write(bytes);
|
||||
} else {
|
||||
SignSafeOutputStream signSafeOutputStream = new SignSafeOutputStream(out);
|
||||
QuotedPrintableOutputStream signSafeQuotedPrintableOutputStream =
|
||||
new QuotedPrintableOutputStream(signSafeOutputStream, false);
|
||||
signSafeQuotedPrintableOutputStream.write(bytes);
|
||||
signSafeQuotedPrintableOutputStream.flush();
|
||||
signSafeOutputStream.flush();
|
||||
throw new IllegalStateException("Cannot get size for encoding!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text of the body in it's unencoded format.
|
||||
* @return
|
||||
*/
|
||||
public String getText() {
|
||||
return mBody;
|
||||
public String getRawText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an InputStream that reads this body's text.
|
||||
*/
|
||||
@Override
|
||||
public InputStream getInputStream() throws MessagingException {
|
||||
try {
|
||||
byte[] b;
|
||||
if (mBody != null) {
|
||||
b = mBody.getBytes(mCharset);
|
||||
if (text != null) {
|
||||
b = text.getBytes(charset);
|
||||
} else {
|
||||
b = EMPTY_BYTE_ARRAY;
|
||||
}
|
||||
return new ByteArrayInputStream(b);
|
||||
} catch (UnsupportedEncodingException uee) {
|
||||
Log.e(K9MailLib.LOG_TAG, "Unsupported charset: " + charset, uee);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEncoding(String encoding) {
|
||||
mEncoding = encoding;
|
||||
boolean isSupportedEncoding = MimeUtil.ENC_QUOTED_PRINTABLE.equalsIgnoreCase(encoding) ||
|
||||
MimeUtil.ENC_8BIT.equalsIgnoreCase(encoding);
|
||||
if (!isSupportedEncoding) {
|
||||
throw new IllegalArgumentException("Cannot encode to " + encoding);
|
||||
}
|
||||
|
||||
this.encoding = encoding;
|
||||
}
|
||||
|
||||
public void setCharset(String charset) {
|
||||
mCharset = charset;
|
||||
this.charset = charset;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Integer getComposedMessageLength() {
|
||||
return mComposedMessageLength;
|
||||
return composedMessageLength;
|
||||
}
|
||||
|
||||
public void setComposedMessageLength(Integer composedMessageLength) {
|
||||
this.mComposedMessageLength = composedMessageLength;
|
||||
public void setComposedMessageLength(@Nullable Integer composedMessageLength) {
|
||||
this.composedMessageLength = composedMessageLength;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Integer getComposedMessageOffset() {
|
||||
return mComposedMessageOffset;
|
||||
return composedMessageOffset;
|
||||
}
|
||||
|
||||
public void setComposedMessageOffset(Integer composedMessageOffset) {
|
||||
this.mComposedMessageOffset = composedMessageOffset;
|
||||
public void setComposedMessageOffset(@Nullable Integer composedMessageOffset) {
|
||||
this.composedMessageOffset = composedMessageOffset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSize() {
|
||||
try {
|
||||
byte[] bytes = mBody.getBytes(mCharset);
|
||||
byte[] bytes = text.getBytes(charset);
|
||||
|
||||
if (MimeUtil.ENC_8BIT.equalsIgnoreCase(mEncoding)) {
|
||||
if (MimeUtil.ENC_QUOTED_PRINTABLE.equalsIgnoreCase(encoding)) {
|
||||
return getLengthWhenQuotedPrintableEncoded(bytes);
|
||||
} else if (MimeUtil.ENC_8BIT.equalsIgnoreCase(encoding)) {
|
||||
return bytes.length;
|
||||
} else {
|
||||
return getLengthWhenQuotedPrintableEncoded(bytes);
|
||||
throw new IllegalStateException("Cannot get size for encoding!");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Couldn't get body size", e);
|
||||
|
@ -121,19 +124,26 @@ public class TextBody implements Body, SizeAware {
|
|||
|
||||
private long getLengthWhenQuotedPrintableEncoded(byte[] bytes) throws IOException {
|
||||
CountingOutputStream countingOutputStream = new CountingOutputStream();
|
||||
OutputStream quotedPrintableOutputStream = new QuotedPrintableOutputStream(countingOutputStream, false);
|
||||
try {
|
||||
quotedPrintableOutputStream.write(bytes);
|
||||
} finally {
|
||||
try {
|
||||
quotedPrintableOutputStream.close();
|
||||
} catch (IOException e) { /* ignore */ }
|
||||
}
|
||||
|
||||
writeSignSafeQuotedPrintable(countingOutputStream, bytes);
|
||||
return countingOutputStream.getCount();
|
||||
}
|
||||
|
||||
private void writeSignSafeQuotedPrintable(OutputStream out, byte[] bytes) throws IOException {
|
||||
SignSafeOutputStream signSafeOutputStream = new SignSafeOutputStream(out);
|
||||
try {
|
||||
QuotedPrintableOutputStream signSafeQuotedPrintableOutputStream =
|
||||
new QuotedPrintableOutputStream(signSafeOutputStream, false);
|
||||
try {
|
||||
signSafeQuotedPrintableOutputStream.write(bytes);
|
||||
} finally {
|
||||
signSafeQuotedPrintableOutputStream.close();
|
||||
}
|
||||
} finally {
|
||||
signSafeOutputStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
public String getEncoding() {
|
||||
return mEncoding;
|
||||
return encoding;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,12 @@ public interface Viewable {
|
|||
}
|
||||
}
|
||||
|
||||
class Flowed extends Textual {
|
||||
public Flowed(Part part) {
|
||||
super(part);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class representing a {@code text/html} part of a message.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
package com.fsck.k9.mail.oauth;
|
||||
|
||||
public class AuthorizationException extends Exception {
|
||||
public AuthorizationException(String detailMessage, Throwable throwable) {
|
||||
super(detailMessage, throwable);
|
||||
}
|
||||
|
||||
public AuthorizationException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package com.fsck.k9.mail.oauth;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import com.fsck.k9.mail.AuthenticationFailedException;
|
||||
|
||||
|
||||
public interface OAuth2TokenProvider {
|
||||
/**
|
||||
* A default timeout value to use when fetching tokens.
|
||||
*/
|
||||
int OAUTH2_TIMEOUT = 30000;
|
||||
|
||||
|
||||
/**
|
||||
* @return Accounts suitable for OAuth 2.0 token provision.
|
||||
*/
|
||||
List<String> getAccounts();
|
||||
|
||||
/**
|
||||
* Request API authorization. This is a foreground action that may produce a dialog to interact with.
|
||||
*
|
||||
* @param username
|
||||
* Username
|
||||
* @param activity
|
||||
* The responsible activity
|
||||
* @param callback
|
||||
* A callback to process the asynchronous response
|
||||
*/
|
||||
void authorizeApi(String username, Activity activity, OAuth2TokenProviderAuthCallback callback);
|
||||
|
||||
/**
|
||||
* Fetch a token. No guarantees are provided for validity.
|
||||
*/
|
||||
String getToken(String username, long timeoutMillis) throws AuthenticationFailedException;
|
||||
|
||||
/**
|
||||
* Invalidate the token for this username.
|
||||
*
|
||||
* <p>
|
||||
* Note that the token should always be invalidated on credential failure. However invalidating a token every
|
||||
* single time is not recommended.
|
||||
* <p>
|
||||
* Invalidating a token and then failure with a new token should be treated as a permanent failure.
|
||||
*/
|
||||
void invalidateToken(String username);
|
||||
|
||||
|
||||
/**
|
||||
* Provides an asynchronous response to an
|
||||
* {@link OAuth2TokenProvider#authorizeApi(String, Activity, OAuth2TokenProviderAuthCallback)} request.
|
||||
*/
|
||||
interface OAuth2TokenProviderAuthCallback {
|
||||
void success();
|
||||
void failure(AuthorizationException e);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package com.fsck.k9.mail.oauth;
|
||||
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.fsck.k9.mail.K9MailLib;
|
||||
import com.fsck.k9.mail.filter.Base64;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import static com.fsck.k9.mail.K9MailLib.LOG_TAG;
|
||||
|
||||
|
||||
/**
|
||||
* Parses Google's Error/Challenge responses
|
||||
* See: https://developers.google.com/gmail/xoauth2_protocol#error_response
|
||||
*/
|
||||
public class XOAuth2ChallengeParser {
|
||||
public static final String BAD_RESPONSE = "400";
|
||||
|
||||
|
||||
public static boolean shouldRetry(String response, String host) {
|
||||
String decodedResponse = Base64.decode(response);
|
||||
|
||||
if (K9MailLib.isDebug()) {
|
||||
Log.v(LOG_TAG, "Challenge response: " + decodedResponse);
|
||||
}
|
||||
|
||||
try {
|
||||
JSONObject json = new JSONObject(decodedResponse);
|
||||
String status = json.getString("status");
|
||||
if (!BAD_RESPONSE.equals(status)) {
|
||||
return false;
|
||||
}
|
||||
} catch (JSONException jsonException) {
|
||||
Log.e(LOG_TAG, "Error decoding JSON response from: " + host + ". Response was: " + decodedResponse);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ import java.util.List;
|
|||
|
||||
import android.content.Context;
|
||||
import android.net.SSLCertificateSocketFactory;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
|
@ -25,7 +26,11 @@ import static com.fsck.k9.mail.K9MailLib.LOG_TAG;
|
|||
|
||||
|
||||
/**
|
||||
* Filter and reorder list of cipher suites and TLS versions.
|
||||
* Prior to API 21 (and notably from API 10 - 2.3.4) Android weakened it's cipher list
|
||||
* by ordering them badly such that RC4-MD5 was preferred. To work around this we
|
||||
* remove the insecure ciphers and reorder them so the latest more secure ciphers are at the top.
|
||||
*
|
||||
* On more modern versions of Android we keep the system configuration.
|
||||
*/
|
||||
public class DefaultTrustedSocketFactory implements TrustedSocketFactory {
|
||||
protected static final String[] ENABLED_CIPHERS;
|
||||
|
@ -75,6 +80,11 @@ public class DefaultTrustedSocketFactory implements TrustedSocketFactory {
|
|||
"TLS_ECDH_ECDSA_WITH_RC4_128_SHA",
|
||||
"SSL_RSA_WITH_RC4_128_SHA",
|
||||
"SSL_RSA_WITH_RC4_128_MD5",
|
||||
"TLS_ECDH_RSA_WITH_NULL_SHA",
|
||||
"TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA",
|
||||
"TLS_ECDH_anon_WITH_NULL_SHA",
|
||||
"TLS_ECDH_anon_WITH_RC4_128_SHA",
|
||||
"TLS_RSA_WITH_NULL_SHA256"
|
||||
};
|
||||
|
||||
protected static final String[] ORDERED_KNOWN_PROTOCOLS = {
|
||||
|
@ -107,17 +117,28 @@ public class DefaultTrustedSocketFactory implements TrustedSocketFactory {
|
|||
"protocols", e);
|
||||
}
|
||||
|
||||
ENABLED_CIPHERS = (enabledCiphers == null) ? null :
|
||||
reorder(enabledCiphers, ORDERED_KNOWN_CIPHERS, BLACKLISTED_CIPHERS);
|
||||
if (hasWeakSslImplementation()) {
|
||||
ENABLED_CIPHERS = (enabledCiphers == null) ? null :
|
||||
reorder(enabledCiphers, ORDERED_KNOWN_CIPHERS, BLACKLISTED_CIPHERS);
|
||||
ENABLED_PROTOCOLS = (supportedProtocols == null) ? null :
|
||||
reorder(supportedProtocols, ORDERED_KNOWN_PROTOCOLS, BLACKLISTED_PROTOCOLS);
|
||||
} else {
|
||||
ENABLED_CIPHERS = (enabledCiphers == null) ? null :
|
||||
remove(enabledCiphers, BLACKLISTED_CIPHERS);
|
||||
ENABLED_PROTOCOLS = (supportedProtocols == null) ? null :
|
||||
remove(supportedProtocols, BLACKLISTED_PROTOCOLS);
|
||||
}
|
||||
|
||||
ENABLED_PROTOCOLS = (supportedProtocols == null) ? null :
|
||||
reorder(supportedProtocols, ORDERED_KNOWN_PROTOCOLS, BLACKLISTED_PROTOCOLS);
|
||||
}
|
||||
|
||||
public DefaultTrustedSocketFactory(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
private static boolean hasWeakSslImplementation() {
|
||||
return android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP;
|
||||
}
|
||||
|
||||
protected static String[] reorder(String[] enabled, String[] known, String[] blacklisted) {
|
||||
List<String> unknown = new ArrayList<String>();
|
||||
Collections.addAll(unknown, enabled);
|
||||
|
@ -144,6 +165,20 @@ public class DefaultTrustedSocketFactory implements TrustedSocketFactory {
|
|||
return result.toArray(new String[result.size()]);
|
||||
}
|
||||
|
||||
protected static String[] remove(String[] enabled, String[] blacklisted) {
|
||||
List<String> items = new ArrayList<String>();
|
||||
Collections.addAll(items, enabled);
|
||||
|
||||
// Remove blacklisted items
|
||||
if (blacklisted != null) {
|
||||
for (String item : blacklisted) {
|
||||
items.remove(item);
|
||||
}
|
||||
}
|
||||
|
||||
return items.toArray(new String[items.size()]);
|
||||
}
|
||||
|
||||
private Context context;
|
||||
|
||||
public Socket createSocket(Socket socket, String host, int port, String clientCertificateAlias)
|
||||
|
@ -166,7 +201,9 @@ public class DefaultTrustedSocketFactory implements TrustedSocketFactory {
|
|||
}
|
||||
|
||||
SSLSocket sslSocket = (SSLSocket) trustedSocket;
|
||||
|
||||
hardenSocket(sslSocket);
|
||||
|
||||
setSniHost(socketFactory, sslSocket, host);
|
||||
|
||||
return trustedSocket;
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
package com.fsck.k9.mail.store;
|
||||
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
|
||||
|
@ -7,6 +11,7 @@ import com.fsck.k9.mail.MessagingException;
|
|||
import com.fsck.k9.mail.ServerSettings;
|
||||
import com.fsck.k9.mail.ServerSettings.Type;
|
||||
import com.fsck.k9.mail.Store;
|
||||
import com.fsck.k9.mail.oauth.OAuth2TokenProvider;
|
||||
import com.fsck.k9.mail.ssl.DefaultTrustedSocketFactory;
|
||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory;
|
||||
import com.fsck.k9.mail.store.imap.ImapStore;
|
||||
|
@ -14,8 +19,6 @@ import com.fsck.k9.mail.store.pop3.Pop3Store;
|
|||
import com.fsck.k9.mail.store.webdav.WebDavHttpClient;
|
||||
import com.fsck.k9.mail.store.webdav.WebDavStore;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public abstract class RemoteStore extends Store {
|
||||
public static final int SOCKET_CONNECT_TIMEOUT = 30000;
|
||||
|
@ -38,8 +41,7 @@ public abstract class RemoteStore extends Store {
|
|||
/**
|
||||
* Get an instance of a remote mail store.
|
||||
*/
|
||||
public synchronized static Store getInstance(Context context,
|
||||
StoreConfig storeConfig) throws MessagingException {
|
||||
public static synchronized Store getInstance(Context context, StoreConfig storeConfig) throws MessagingException {
|
||||
String uri = storeConfig.getStoreUri();
|
||||
|
||||
if (uri.startsWith("local")) {
|
||||
|
@ -49,12 +51,14 @@ public abstract class RemoteStore extends Store {
|
|||
Store store = sStores.get(uri);
|
||||
if (store == null) {
|
||||
if (uri.startsWith("imap")) {
|
||||
store = new ImapStore(storeConfig,
|
||||
OAuth2TokenProvider oAuth2TokenProvider = null;
|
||||
store = new ImapStore(
|
||||
storeConfig,
|
||||
new DefaultTrustedSocketFactory(context),
|
||||
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE));
|
||||
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE),
|
||||
oAuth2TokenProvider);
|
||||
} else if (uri.startsWith("pop3")) {
|
||||
store = new Pop3Store(storeConfig,
|
||||
new DefaultTrustedSocketFactory(context));
|
||||
store = new Pop3Store(storeConfig, new DefaultTrustedSocketFactory(context));
|
||||
} else if (uri.startsWith("webdav")) {
|
||||
store = new WebDavStore(storeConfig, new WebDavHttpClient.WebDavHttpClientFactory());
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ package com.fsck.k9.mail.store.imap;
|
|||
|
||||
class Capabilities {
|
||||
public static final String IDLE = "IDLE";
|
||||
public static final String SASL_IR = "SASL-IR";
|
||||
public static final String AUTH_XOAUTH2 = "AUTH=XOAUTH2";
|
||||
public static final String AUTH_CRAM_MD5 = "AUTH=CRAM-MD5";
|
||||
public static final String AUTH_PLAIN = "AUTH=PLAIN";
|
||||
public static final String AUTH_EXTERNAL = "AUTH=EXTERNAL";
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package com.fsck.k9.mail.store.imap;
|
||||
|
||||
|
||||
class Commands {
|
||||
public static final String IDLE = "IDLE";
|
||||
public static final String NAMESPACE = "NAMESPACE";
|
||||
public static final String CAPABILITY = "CAPABILITY";
|
||||
public static final String COMPRESS_DEFLATE = "COMPRESS DEFLATE";
|
||||
public static final String STARTTLS = "STARTTLS";
|
||||
public static final String AUTHENTICATE_XOAUTH2 = "AUTHENTICATE XOAUTH2";
|
||||
public static final String AUTHENTICATE_CRAM_MD5 = "AUTHENTICATE CRAM-MD5";
|
||||
public static final String AUTHENTICATE_PLAIN = "AUTHENTICATE PLAIN";
|
||||
public static final String AUTHENTICATE_EXTERNAL = "AUTHENTICATE EXTERNAL";
|
||||
|
|
|
@ -39,6 +39,8 @@ import com.fsck.k9.mail.MessagingException;
|
|||
import com.fsck.k9.mail.NetworkType;
|
||||
import com.fsck.k9.mail.filter.Base64;
|
||||
import com.fsck.k9.mail.filter.PeekableInputStream;
|
||||
import com.fsck.k9.mail.oauth.OAuth2TokenProvider;
|
||||
import com.fsck.k9.mail.oauth.XOAuth2ChallengeParser;
|
||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory;
|
||||
import com.jcraft.jzlib.JZlib;
|
||||
import com.jcraft.jzlib.ZOutputStream;
|
||||
|
@ -61,6 +63,7 @@ class ImapConnection {
|
|||
|
||||
|
||||
private final ConnectivityManager connectivityManager;
|
||||
private final OAuth2TokenProvider oauthTokenProvider;
|
||||
private final TrustedSocketFactory socketFactory;
|
||||
private final int socketConnectTimeout;
|
||||
private final int socketReadTimeout;
|
||||
|
@ -74,22 +77,26 @@ class ImapConnection {
|
|||
private ImapSettings settings;
|
||||
private Exception stacktraceForClose;
|
||||
private boolean open = false;
|
||||
private boolean retryXoauth2WithNewToken = true;
|
||||
|
||||
|
||||
public ImapConnection(ImapSettings settings, TrustedSocketFactory socketFactory,
|
||||
ConnectivityManager connectivityManager) {
|
||||
ConnectivityManager connectivityManager, OAuth2TokenProvider oauthTokenProvider) {
|
||||
this.settings = settings;
|
||||
this.socketFactory = socketFactory;
|
||||
this.connectivityManager = connectivityManager;
|
||||
this.oauthTokenProvider = oauthTokenProvider;
|
||||
this.socketConnectTimeout = SOCKET_CONNECT_TIMEOUT;
|
||||
this.socketReadTimeout = SOCKET_READ_TIMEOUT;
|
||||
}
|
||||
|
||||
ImapConnection(ImapSettings settings, TrustedSocketFactory socketFactory, ConnectivityManager connectivityManager,
|
||||
ImapConnection(ImapSettings settings, TrustedSocketFactory socketFactory,
|
||||
ConnectivityManager connectivityManager, OAuth2TokenProvider oauthTokenProvider,
|
||||
int socketConnectTimeout, int socketReadTimeout) {
|
||||
this.settings = settings;
|
||||
this.socketFactory = socketFactory;
|
||||
this.connectivityManager = connectivityManager;
|
||||
this.oauthTokenProvider = oauthTokenProvider;
|
||||
this.socketConnectTimeout = socketConnectTimeout;
|
||||
this.socketReadTimeout = socketReadTimeout;
|
||||
}
|
||||
|
@ -322,6 +329,15 @@ class ImapConnection {
|
|||
@SuppressWarnings("EnumSwitchStatementWhichMissesCases")
|
||||
private void authenticate() throws MessagingException, IOException {
|
||||
switch (settings.getAuthType()) {
|
||||
case XOAUTH2:
|
||||
if (oauthTokenProvider == null) {
|
||||
throw new MessagingException("No OAuthToken Provider available.");
|
||||
} else if (hasCapability(Capabilities.AUTH_XOAUTH2) && hasCapability(Capabilities.SASL_IR)) {
|
||||
authXoauth2withSASLIR();
|
||||
} else {
|
||||
throw new MessagingException("Server doesn't support SASL XOAUTH2.");
|
||||
}
|
||||
break;
|
||||
case CRAM_MD5: {
|
||||
if (hasCapability(Capabilities.AUTH_CRAM_MD5)) {
|
||||
authCramMD5();
|
||||
|
@ -356,6 +372,71 @@ class ImapConnection {
|
|||
}
|
||||
}
|
||||
|
||||
private void authXoauth2withSASLIR() throws IOException, MessagingException {
|
||||
retryXoauth2WithNewToken = true;
|
||||
try {
|
||||
attemptXOAuth2();
|
||||
} catch (NegativeImapResponseException e) {
|
||||
oauthTokenProvider.invalidateToken(settings.getUsername());
|
||||
|
||||
if (!retryXoauth2WithNewToken) {
|
||||
handlePermanentXoauth2Failure(e);
|
||||
} else {
|
||||
handleTemporaryXoauth2Failure(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePermanentXoauth2Failure(NegativeImapResponseException e) throws AuthenticationFailedException {
|
||||
Log.v(LOG_TAG, "Permanent failure during XOAUTH2", e);
|
||||
throw new AuthenticationFailedException(e.getMessage(), e);
|
||||
}
|
||||
|
||||
private void handleTemporaryXoauth2Failure(NegativeImapResponseException e) throws IOException, MessagingException {
|
||||
//We got a response indicating a retry might suceed after token refresh
|
||||
//We could avoid this if we had a reasonable chance of knowing
|
||||
//if a token was invalid before use (e.g. due to expiry). But we don't
|
||||
//This is the intended behaviour per AccountManager
|
||||
|
||||
Log.v(LOG_TAG, "Temporary failure - retrying with new token", e);
|
||||
try {
|
||||
attemptXOAuth2();
|
||||
} catch (NegativeImapResponseException e2) {
|
||||
//Okay, we failed on a new token.
|
||||
//Invalidate the token anyway but assume it's permanent.
|
||||
Log.v(LOG_TAG, "Authentication exception for new token, permanent error assumed", e);
|
||||
oauthTokenProvider.invalidateToken(settings.getUsername());
|
||||
handlePermanentXoauth2Failure(e2);
|
||||
}
|
||||
}
|
||||
|
||||
private void attemptXOAuth2() throws MessagingException, IOException {
|
||||
String token = oauthTokenProvider.getToken(settings.getUsername(), OAuth2TokenProvider.OAUTH2_TIMEOUT);
|
||||
String authString = Authentication.computeXoauth(settings.getUsername(), token);
|
||||
String tag = sendSaslIrCommand(Commands.AUTHENTICATE_XOAUTH2, authString, true);
|
||||
|
||||
List<ImapResponse> responses = responseParser.readStatusResponse(tag, Commands.AUTHENTICATE_XOAUTH2, getLogId(),
|
||||
new UntaggedHandler() {
|
||||
@Override
|
||||
public void handleAsyncUntaggedResponse(ImapResponse response) throws IOException {
|
||||
handleXOAuthUntaggedResponse(response);
|
||||
}
|
||||
});
|
||||
|
||||
extractCapabilities(responses);
|
||||
}
|
||||
|
||||
private void handleXOAuthUntaggedResponse(ImapResponse response) throws IOException {
|
||||
if (response.isString(0)) {
|
||||
retryXoauth2WithNewToken = XOAuth2ChallengeParser.shouldRetry(response.getString(0), settings.getHost());
|
||||
}
|
||||
|
||||
if (response.isContinuationRequested()) {
|
||||
outputStream.write("\r\n".getBytes());
|
||||
outputStream.flush();
|
||||
}
|
||||
}
|
||||
|
||||
private void authCramMD5() throws MessagingException, IOException {
|
||||
String command = Commands.AUTHENTICATE_CRAM_MD5;
|
||||
String tag = sendCommand(command, false);
|
||||
|
@ -384,6 +465,10 @@ class ImapConnection {
|
|||
try {
|
||||
saslAuthPlain();
|
||||
} catch (AuthenticationFailedException e) {
|
||||
if (!isConnected()) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
login();
|
||||
}
|
||||
}
|
||||
|
@ -405,6 +490,10 @@ class ImapConnection {
|
|||
try {
|
||||
extractCapabilities(responseParser.readStatusResponse(tag, command, getLogId(), null));
|
||||
} catch (NegativeImapResponseException e) {
|
||||
if (e.wasByeResponseReceived()) {
|
||||
close();
|
||||
}
|
||||
|
||||
throw new AuthenticationFailedException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
@ -635,6 +724,31 @@ class ImapConnection {
|
|||
return responseParser.readStatusResponse(tag, commandToLog, getLogId(), untaggedHandler);
|
||||
}
|
||||
|
||||
public String sendSaslIrCommand(String command, String initialClientResponse, boolean sensitive)
|
||||
throws IOException, MessagingException {
|
||||
try {
|
||||
open();
|
||||
|
||||
String tag = Integer.toString(nextCommandTag++);
|
||||
String commandToSend = tag + " " + command + " " + initialClientResponse + "\r\n";
|
||||
outputStream.write(commandToSend.getBytes());
|
||||
outputStream.flush();
|
||||
|
||||
if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP) {
|
||||
if (sensitive && !K9MailLib.isDebugSensitive()) {
|
||||
Log.v(LOG_TAG, getLogId() + ">>> [Command Hidden, Enable Sensitive Debug Logging To Show]");
|
||||
} else {
|
||||
Log.v(LOG_TAG, getLogId() + ">>> " + tag + " " + command + " " + initialClientResponse);
|
||||
}
|
||||
}
|
||||
|
||||
return tag;
|
||||
} catch (IOException | MessagingException e) {
|
||||
close();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public String sendCommand(String command, boolean sensitive) throws MessagingException, IOException {
|
||||
try {
|
||||
open();
|
||||
|
|
|
@ -496,7 +496,7 @@ class ImapFolder extends Folder<ImapMessage> {
|
|||
|
||||
@Override
|
||||
public void delete(boolean recurse) throws MessagingException {
|
||||
throw new Error("ImapStore.delete() not yet implemented");
|
||||
throw new Error("ImapFolder.delete() not yet implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -986,7 +986,7 @@ class ImapFolder extends Folder<ImapMessage> {
|
|||
/*
|
||||
* This is a multipart/*
|
||||
*/
|
||||
MimeMultipart mp = new MimeMultipart();
|
||||
MimeMultipart mp = MimeMultipart.newInstance();
|
||||
for (int i = 0, count = bs.size(); i < count; i++) {
|
||||
if (bs.get(i) instanceof ImapList) {
|
||||
/*
|
||||
|
|
|
@ -58,7 +58,7 @@ class ImapList extends ArrayList<Object> {
|
|||
|
||||
private Date getDate(String value) throws MessagingException {
|
||||
try {
|
||||
if (value == null) {
|
||||
if (value == null || "NIL".equals(value)) {
|
||||
return null;
|
||||
}
|
||||
return parseDate(value);
|
||||
|
|
|
@ -23,7 +23,6 @@ class ImapMessage extends MimeMessage {
|
|||
super.setFlag(flag, set);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setFlag(Flag flag, boolean set) throws MessagingException {
|
||||
super.setFlag(flag, set);
|
||||
|
|
|
@ -45,7 +45,7 @@ class ImapResponseParser {
|
|||
}
|
||||
|
||||
if (exception != null) {
|
||||
throw new RuntimeException("readResponse(): Exception in callback method", exception);
|
||||
throw new ImapResponseParserException("readResponse(): Exception in callback method", exception);
|
||||
}
|
||||
|
||||
return response;
|
||||
|
@ -109,7 +109,7 @@ class ImapResponseParser {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (untaggedHandler != null) {
|
||||
if (response.getTag() == null && untaggedHandler != null) {
|
||||
untaggedHandler.handleAsyncUntaggedResponse(response);
|
||||
}
|
||||
|
||||
|
@ -118,8 +118,7 @@ class ImapResponseParser {
|
|||
|
||||
if (response.size() < 1 || !equalsIgnoreCase(response.get(0), Responses.OK)) {
|
||||
String message = "Command: " + commandToLog + "; response: " + response.toString();
|
||||
String alertText = AlertResponse.getAlertText(response);
|
||||
throw new NegativeImapResponseException(message, alertText);
|
||||
throw new NegativeImapResponseException(message, responses);
|
||||
}
|
||||
|
||||
return responses;
|
||||
|
@ -194,8 +193,7 @@ class ImapResponseParser {
|
|||
expect(' ');
|
||||
parseList(response, '(', ')');
|
||||
expect(' ');
|
||||
//TODO: Add support for NIL
|
||||
String delimiter = parseQuoted();
|
||||
String delimiter = parseQuotedOrNil();
|
||||
response.add(delimiter);
|
||||
expect(' ');
|
||||
String name = parseString();
|
||||
|
@ -354,26 +352,32 @@ class ImapResponseParser {
|
|||
if (response.getCallback() != null) {
|
||||
FixedLengthInputStream fixed = new FixedLengthInputStream(inputStream, size);
|
||||
|
||||
Exception callbackException = null;
|
||||
Object result = null;
|
||||
try {
|
||||
result = response.getCallback().foundLiteral(response, fixed);
|
||||
} catch (IOException e) {
|
||||
// Pass IOExceptions through
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
// Catch everything else and save it for later.
|
||||
exception = e;
|
||||
callbackException = e;
|
||||
}
|
||||
|
||||
// Check if only some of the literal data was read
|
||||
int available = fixed.available();
|
||||
if (available > 0 && available != size) {
|
||||
// If so, skip the rest
|
||||
while (fixed.available() > 0) {
|
||||
fixed.skip(fixed.available());
|
||||
boolean someDataWasRead = fixed.available() != size;
|
||||
if (someDataWasRead) {
|
||||
if (result == null && callbackException == null) {
|
||||
throw new AssertionError("Callback consumed some data but returned no result");
|
||||
}
|
||||
|
||||
fixed.skipRemaining();
|
||||
}
|
||||
|
||||
if (callbackException != null) {
|
||||
if (exception == null) {
|
||||
exception = callbackException;
|
||||
}
|
||||
return "EXCEPTION";
|
||||
}
|
||||
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
|
@ -412,6 +416,22 @@ class ImapResponseParser {
|
|||
throw new IOException("parseQuoted(): end of stream reached");
|
||||
}
|
||||
|
||||
private String parseQuotedOrNil() throws IOException {
|
||||
int peek = inputStream.peek();
|
||||
if (peek == '"') {
|
||||
return parseQuoted();
|
||||
} else {
|
||||
parseNil();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void parseNil() throws IOException {
|
||||
expect('N');
|
||||
expect('I');
|
||||
expect('L');
|
||||
}
|
||||
|
||||
private String readStringUntil(char end) throws IOException {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
package com.fsck.k9.mail.store.imap;
|
||||
|
||||
|
||||
public class ImapResponseParserException extends RuntimeException {
|
||||
public ImapResponseParserException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import android.accounts.AccountManager;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.util.Log;
|
||||
|
||||
|
@ -26,6 +27,7 @@ import com.fsck.k9.mail.NetworkType;
|
|||
import com.fsck.k9.mail.PushReceiver;
|
||||
import com.fsck.k9.mail.Pusher;
|
||||
import com.fsck.k9.mail.ServerSettings;
|
||||
import com.fsck.k9.mail.oauth.OAuth2TokenProvider;
|
||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory;
|
||||
import com.fsck.k9.mail.store.RemoteStore;
|
||||
import com.fsck.k9.mail.store.StoreConfig;
|
||||
|
@ -42,6 +44,7 @@ import static com.fsck.k9.mail.K9MailLib.LOG_TAG;
|
|||
public class ImapStore extends RemoteStore {
|
||||
private Set<Flag> permanentFlagsIndex = EnumSet.noneOf(Flag.class);
|
||||
private ConnectivityManager connectivityManager;
|
||||
private OAuth2TokenProvider oauthTokenProvider;
|
||||
|
||||
private String host;
|
||||
private int port;
|
||||
|
@ -74,7 +77,7 @@ public class ImapStore extends RemoteStore {
|
|||
}
|
||||
|
||||
public ImapStore(StoreConfig storeConfig, TrustedSocketFactory trustedSocketFactory,
|
||||
ConnectivityManager connectivityManager) throws MessagingException {
|
||||
ConnectivityManager connectivityManager, OAuth2TokenProvider oauthTokenProvider) throws MessagingException {
|
||||
super(storeConfig, trustedSocketFactory);
|
||||
|
||||
ImapStoreSettings settings;
|
||||
|
@ -89,6 +92,7 @@ public class ImapStore extends RemoteStore {
|
|||
|
||||
connectionSecurity = settings.connectionSecurity;
|
||||
this.connectivityManager = connectivityManager;
|
||||
this.oauthTokenProvider = oauthTokenProvider;
|
||||
|
||||
authType = settings.authenticationType;
|
||||
username = settings.username;
|
||||
|
@ -340,7 +344,11 @@ public class ImapStore extends RemoteStore {
|
|||
}
|
||||
|
||||
ImapConnection createImapConnection() {
|
||||
return new ImapConnection(new StoreImapSettings(), mTrustedSocketFactory, connectivityManager);
|
||||
return new ImapConnection(
|
||||
new StoreImapSettings(),
|
||||
mTrustedSocketFactory,
|
||||
connectivityManager,
|
||||
oauthTokenProvider);
|
||||
}
|
||||
|
||||
FolderNameCodec getFolderNameCodec() {
|
||||
|
|
|
@ -83,14 +83,26 @@ class ImapStoreUriDecoder {
|
|||
String[] userInfoParts = userinfo.split(":");
|
||||
|
||||
if (userinfo.endsWith(":")) {
|
||||
// Password is empty. This can only happen after an account was imported.
|
||||
authenticationType = AuthType.valueOf(userInfoParts[0]);
|
||||
username = decodeUtf8(userInfoParts[1]);
|
||||
// Last field (password/certAlias) is empty.
|
||||
// For imports e.g.: PLAIN:username: or username:
|
||||
// Or XOAUTH2 where it's a valid config - XOAUTH:username:
|
||||
if (userInfoParts.length > 1) {
|
||||
authenticationType = AuthType.valueOf(userInfoParts[0]);
|
||||
username = decodeUtf8(userInfoParts[1]);
|
||||
} else {
|
||||
authenticationType = AuthType.PLAIN;
|
||||
username = decodeUtf8(userInfoParts[0]);
|
||||
}
|
||||
} else if (userInfoParts.length == 2) {
|
||||
// Old/standard style of encoding - PLAIN auth only:
|
||||
// username:password
|
||||
authenticationType = AuthType.PLAIN;
|
||||
username = decodeUtf8(userInfoParts[0]);
|
||||
password = decodeUtf8(userInfoParts[1]);
|
||||
} else if (userInfoParts.length == 3) {
|
||||
// Standard encoding
|
||||
// PLAIN:username:password
|
||||
// EXTERNAL:username:certAlias
|
||||
authenticationType = AuthType.valueOf(userInfoParts[0]);
|
||||
username = decodeUtf8(userInfoParts[1]);
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@ import java.util.ArrayList;
|
|||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import static com.fsck.k9.mail.store.imap.ImapResponseParser.equalsIgnoreCase;
|
||||
|
||||
class ListResponse {
|
||||
|
@ -52,9 +54,8 @@ class ListResponse {
|
|||
return null;
|
||||
}
|
||||
|
||||
//TODO: Add support for NIL. Needs modifications in ImapResponseParser
|
||||
String hierarchyDelimiter = response.getString(2);
|
||||
if (hierarchyDelimiter.length() != 1) {
|
||||
if (hierarchyDelimiter != null && hierarchyDelimiter.length() != 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -93,6 +94,7 @@ class ListResponse {
|
|||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getHierarchyDelimiter() {
|
||||
return hierarchyDelimiter;
|
||||
}
|
||||
|
|
|
@ -1,20 +1,39 @@
|
|||
package com.fsck.k9.mail.store.imap;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
|
||||
import static com.fsck.k9.mail.store.imap.ImapResponseParser.equalsIgnoreCase;
|
||||
|
||||
|
||||
class NegativeImapResponseException extends MessagingException {
|
||||
private static final long serialVersionUID = 3725007182205882394L;
|
||||
|
||||
private final List<ImapResponse> responses;
|
||||
private String alertText;
|
||||
|
||||
private final String alertText;
|
||||
|
||||
|
||||
public NegativeImapResponseException(String message, String alertText) {
|
||||
public NegativeImapResponseException(String message, List<ImapResponse> responses) {
|
||||
super(message, true);
|
||||
this.alertText = alertText;
|
||||
this.responses = responses;
|
||||
}
|
||||
|
||||
public String getAlertText() {
|
||||
if (alertText == null) {
|
||||
ImapResponse lastResponse = responses.get(responses.size() - 1);
|
||||
alertText = AlertResponse.getAlertText(lastResponse);
|
||||
}
|
||||
|
||||
return alertText;
|
||||
}
|
||||
|
||||
public boolean wasByeResponseReceived() {
|
||||
for (ImapResponse response : responses) {
|
||||
if (response.getTag() == null && response.size() >= 1 && equalsIgnoreCase(response.get(0), Responses.BYE)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package com.fsck.k9.mail.store.imap;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
interface UntaggedHandler {
|
||||
void handleAsyncUntaggedResponse(ImapResponse response);
|
||||
void handleAsyncUntaggedResponse(ImapResponse response) throws IOException;
|
||||
}
|
||||
|
|
|
@ -1184,7 +1184,7 @@ public class Pop3Store extends RemoteStore {
|
|||
}//Pop3Folder
|
||||
|
||||
static class Pop3Message extends MimeMessage {
|
||||
public Pop3Message(String uid, Pop3Folder folder) {
|
||||
Pop3Message(String uid, Pop3Folder folder) {
|
||||
mUid = uid;
|
||||
mFolder = folder;
|
||||
mSize = -1;
|
||||
|
|
|
@ -655,7 +655,11 @@ class WebDavFolder extends Folder<WebDavMessage> {
|
|||
try {
|
||||
ByteArrayOutputStream out;
|
||||
|
||||
out = new ByteArrayOutputStream(message.getSize());
|
||||
long size = message.getSize();
|
||||
if (size > Integer.MAX_VALUE) {
|
||||
throw new MessagingException("message size > Integer.MAX_VALUE!");
|
||||
}
|
||||
out = new ByteArrayOutputStream((int) size);
|
||||
|
||||
open(Folder.OPEN_MODE_RW);
|
||||
EOLConvertingOutputStream msgOut = new EOLConvertingOutputStream(
|
||||
|
|
|
@ -21,6 +21,7 @@ import static com.fsck.k9.mail.helper.UrlEncodingHelper.encodeUtf8;
|
|||
class WebDavMessage extends MimeMessage {
|
||||
private String mUrl = "";
|
||||
|
||||
|
||||
WebDavMessage(String uid, Folder folder) {
|
||||
this.mUid = uid;
|
||||
this.mFolder = folder;
|
||||
|
|
|
@ -8,7 +8,6 @@ import com.fsck.k9.mail.CertificateValidationException;
|
|||
import com.fsck.k9.mail.store.RemoteStore;
|
||||
import com.fsck.k9.mail.store.StoreConfig;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.http.*;
|
||||
import org.apache.http.client.CookieStore;
|
||||
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
||||
|
@ -34,10 +33,7 @@ import java.net.URI;
|
|||
import java.net.URISyntaxException;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
import static com.fsck.k9.mail.K9MailLib.DEBUG_PROTOCOL_WEBDAV;
|
||||
import static com.fsck.k9.mail.K9MailLib.LOG_TAG;
|
||||
|
@ -53,152 +49,12 @@ import static com.fsck.k9.mail.helper.UrlEncodingHelper.encodeUtf8;
|
|||
*/
|
||||
public class WebDavStore extends RemoteStore {
|
||||
|
||||
/**
|
||||
* Decodes a WebDavStore URI.
|
||||
* <p/>
|
||||
* <p>Possible forms:</p>
|
||||
* <pre>
|
||||
* webdav://user:password@server:port ConnectionSecurity.NONE
|
||||
* webdav+ssl+://user:password@server:port ConnectionSecurity.SSL_TLS_REQUIRED
|
||||
* </pre>
|
||||
*/
|
||||
public static WebDavStoreSettings decodeUri(String uri) {
|
||||
String host;
|
||||
int port;
|
||||
ConnectionSecurity connectionSecurity;
|
||||
String username = null;
|
||||
String password = null;
|
||||
String alias = null;
|
||||
String path = null;
|
||||
String authPath = null;
|
||||
String mailboxPath = null;
|
||||
|
||||
|
||||
URI webDavUri;
|
||||
try {
|
||||
webDavUri = new URI(uri);
|
||||
} catch (URISyntaxException use) {
|
||||
throw new IllegalArgumentException("Invalid WebDavStore URI", use);
|
||||
}
|
||||
|
||||
String scheme = webDavUri.getScheme();
|
||||
/*
|
||||
* Currently available schemes are:
|
||||
* webdav
|
||||
* webdav+ssl+
|
||||
*
|
||||
* The following are obsolete schemes that may be found in pre-existing
|
||||
* settings from earlier versions or that may be found when imported. We
|
||||
* continue to recognize them and re-map them appropriately:
|
||||
* webdav+tls
|
||||
* webdav+tls+
|
||||
* webdav+ssl
|
||||
*/
|
||||
if (scheme.equals("webdav")) {
|
||||
connectionSecurity = ConnectionSecurity.NONE;
|
||||
} else if (scheme.startsWith("webdav+")) {
|
||||
connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported protocol (" + scheme + ")");
|
||||
}
|
||||
|
||||
host = webDavUri.getHost();
|
||||
if (host.startsWith("http")) {
|
||||
String[] hostParts = host.split("://", 2);
|
||||
if (hostParts.length > 1) {
|
||||
host = hostParts[1];
|
||||
}
|
||||
}
|
||||
|
||||
port = webDavUri.getPort();
|
||||
|
||||
String userInfo = webDavUri.getUserInfo();
|
||||
if (userInfo != null) {
|
||||
String[] userInfoParts = userInfo.split(":");
|
||||
username = decodeUtf8(userInfoParts[0]);
|
||||
String userParts[] = username.split("\\\\", 2);
|
||||
|
||||
if (userParts.length > 1) {
|
||||
alias = userParts[1];
|
||||
} else {
|
||||
alias = username;
|
||||
}
|
||||
if (userInfoParts.length > 1) {
|
||||
password = decodeUtf8(userInfoParts[1]);
|
||||
}
|
||||
}
|
||||
|
||||
String[] pathParts = webDavUri.getPath().split("\\|");
|
||||
for (int i = 0, count = pathParts.length; i < count; i++) {
|
||||
if (i == 0) {
|
||||
if (pathParts[0] != null &&
|
||||
pathParts[0].length() > 1) {
|
||||
path = pathParts[0];
|
||||
}
|
||||
} else if (i == 1) {
|
||||
if (pathParts[1] != null &&
|
||||
pathParts[1].length() > 1) {
|
||||
authPath = pathParts[1];
|
||||
}
|
||||
} else if (i == 2) {
|
||||
if (pathParts[2] != null &&
|
||||
pathParts[2].length() > 1) {
|
||||
mailboxPath = pathParts[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new WebDavStoreSettings(host, port, connectionSecurity, null, username, password,
|
||||
null, alias, path, authPath, mailboxPath);
|
||||
return WebDavStoreUriDecoder.decode(uri);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a WebDavStore URI with the supplied settings.
|
||||
*
|
||||
* @param server The {@link ServerSettings} object that holds the server settings.
|
||||
* @return A WebDavStore URI that holds the same information as the {@code server} parameter.
|
||||
* @see StoreConfig#getStoreUri()
|
||||
* @see WebDavStore#decodeUri(String)
|
||||
*/
|
||||
public static String createUri(ServerSettings server) {
|
||||
String userEnc = encodeUtf8(server.username);
|
||||
String passwordEnc = (server.password != null) ?
|
||||
encodeUtf8(server.password) : "";
|
||||
|
||||
String scheme;
|
||||
switch (server.connectionSecurity) {
|
||||
case SSL_TLS_REQUIRED:
|
||||
scheme = "webdav+ssl+";
|
||||
break;
|
||||
default:
|
||||
case NONE:
|
||||
scheme = "webdav";
|
||||
break;
|
||||
}
|
||||
|
||||
String userInfo = userEnc + ":" + passwordEnc;
|
||||
|
||||
String uriPath;
|
||||
Map<String, String> extra = server.getExtra();
|
||||
if (extra != null) {
|
||||
String path = extra.get(WebDavStoreSettings.PATH_KEY);
|
||||
path = (path != null) ? path : "";
|
||||
String authPath = extra.get(WebDavStoreSettings.AUTH_PATH_KEY);
|
||||
authPath = (authPath != null) ? authPath : "";
|
||||
String mailboxPath = extra.get(WebDavStoreSettings.MAILBOX_PATH_KEY);
|
||||
mailboxPath = (mailboxPath != null) ? mailboxPath : "";
|
||||
uriPath = "/" + path + "|" + authPath + "|" + mailboxPath;
|
||||
} else {
|
||||
uriPath = "/||";
|
||||
}
|
||||
|
||||
try {
|
||||
return new URI(scheme, userInfo, server.host, server.port, uriPath,
|
||||
null, null).toString();
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IllegalArgumentException("Can't create WebDavStore URI", e);
|
||||
}
|
||||
return WebDavStoreUriCreator.create(server);
|
||||
}
|
||||
|
||||
private ConnectionSecurity mConnectionSecurity;
|
||||
|
@ -1023,6 +879,8 @@ public class WebDavStore extends RemoteStore {
|
|||
} else {
|
||||
throw new MessagingException("Authentication failure in sendRequest().");
|
||||
}
|
||||
} else if (statusCode == 302) {
|
||||
handleUnexpectedRedirect(response, url);
|
||||
} else if (statusCode < 200 || statusCode >= 300) {
|
||||
throw new IOException("Error with code " + statusCode + " during request processing: " +
|
||||
response.getStatusLine().toString());
|
||||
|
@ -1042,6 +900,18 @@ public class WebDavStore extends RemoteStore {
|
|||
return null;
|
||||
}
|
||||
|
||||
private void handleUnexpectedRedirect(HttpResponse response, String url) throws IOException {
|
||||
if (response.getFirstHeader("Location") != null) {
|
||||
// TODO: This may indicate lack of authentication or may alternatively be something we should follow
|
||||
throw new IOException("Unexpected redirect during request processing. " +
|
||||
"Expected response from: "+url+" but told to redirect to:" +
|
||||
response.getFirstHeader("Location").getValue());
|
||||
} else {
|
||||
throw new IOException("Unexpected redirect during request processing. " +
|
||||
"Expected response from: " + url + " but not told where to redirect to");
|
||||
}
|
||||
}
|
||||
|
||||
public String getAuthString() {
|
||||
return mAuthString;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
package com.fsck.k9.mail.store.webdav;
|
||||
|
||||
import com.fsck.k9.mail.ServerSettings;
|
||||
import com.fsck.k9.mail.store.StoreConfig;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.fsck.k9.mail.helper.UrlEncodingHelper.encodeUtf8;
|
||||
|
||||
public class WebDavStoreUriCreator {
|
||||
|
||||
/**
|
||||
* Creates a WebDavStore URI with the supplied settings.
|
||||
*
|
||||
* @param server The {@link ServerSettings} object that holds the server settings.
|
||||
* @return A WebDavStore URI that holds the same information as the {@code server} parameter.
|
||||
* @see StoreConfig#getStoreUri()
|
||||
* @see WebDavStore#decodeUri(String)
|
||||
*/
|
||||
public static String create(ServerSettings server) {
|
||||
String userEnc = encodeUtf8(server.username);
|
||||
String passwordEnc = (server.password != null) ?
|
||||
encodeUtf8(server.password) : "";
|
||||
|
||||
String scheme;
|
||||
switch (server.connectionSecurity) {
|
||||
case SSL_TLS_REQUIRED:
|
||||
scheme = "webdav+ssl+";
|
||||
break;
|
||||
default:
|
||||
case NONE:
|
||||
scheme = "webdav";
|
||||
break;
|
||||
}
|
||||
|
||||
String userInfo = userEnc + ":" + passwordEnc;
|
||||
|
||||
String uriPath;
|
||||
Map<String, String> extra = server.getExtra();
|
||||
if (extra != null) {
|
||||
String path = extra.get(WebDavStoreSettings.PATH_KEY);
|
||||
path = (path != null) ? path : "";
|
||||
String authPath = extra.get(WebDavStoreSettings.AUTH_PATH_KEY);
|
||||
authPath = (authPath != null) ? authPath : "";
|
||||
String mailboxPath = extra.get(WebDavStoreSettings.MAILBOX_PATH_KEY);
|
||||
mailboxPath = (mailboxPath != null) ? mailboxPath : "";
|
||||
uriPath = "/" + path + "|" + authPath + "|" + mailboxPath;
|
||||
} else {
|
||||
uriPath = "/||";
|
||||
}
|
||||
|
||||
try {
|
||||
return new URI(scheme, userInfo, server.host, server.port, uriPath,
|
||||
null, null).toString();
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IllegalArgumentException("Can't create WebDavStore URI", e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
package com.fsck.k9.mail.store.webdav;
|
||||
|
||||
|
||||
import com.fsck.k9.mail.ConnectionSecurity;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import static com.fsck.k9.mail.helper.UrlEncodingHelper.decodeUtf8;
|
||||
|
||||
public class WebDavStoreUriDecoder {
|
||||
|
||||
/**
|
||||
* Decodes a WebDavStore URI.
|
||||
* <p/>
|
||||
* <p>Possible forms:</p>
|
||||
* <pre>
|
||||
* webdav://user:password@server:port ConnectionSecurity.NONE
|
||||
* webdav+ssl+://user:password@server:port ConnectionSecurity.SSL_TLS_REQUIRED
|
||||
* </pre>
|
||||
*/
|
||||
public static WebDavStoreSettings decode(String uri) {
|
||||
String host;
|
||||
int port;
|
||||
ConnectionSecurity connectionSecurity;
|
||||
String username = null;
|
||||
String password = null;
|
||||
String alias = null;
|
||||
String path = null;
|
||||
String authPath = null;
|
||||
String mailboxPath = null;
|
||||
|
||||
|
||||
URI webDavUri;
|
||||
try {
|
||||
webDavUri = new URI(uri);
|
||||
} catch (URISyntaxException use) {
|
||||
throw new IllegalArgumentException("Invalid WebDavStore URI", use);
|
||||
}
|
||||
|
||||
String scheme = webDavUri.getScheme();
|
||||
/*
|
||||
* Currently available schemes are:
|
||||
* webdav
|
||||
* webdav+ssl+
|
||||
*
|
||||
* The following are obsolete schemes that may be found in pre-existing
|
||||
* settings from earlier versions or that may be found when imported. We
|
||||
* continue to recognize them and re-map them appropriately:
|
||||
* webdav+tls
|
||||
* webdav+tls+
|
||||
* webdav+ssl
|
||||
*/
|
||||
if (scheme.equals("webdav")) {
|
||||
connectionSecurity = ConnectionSecurity.NONE;
|
||||
} else if (scheme.startsWith("webdav+")) {
|
||||
connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported protocol (" + scheme + ")");
|
||||
}
|
||||
|
||||
host = webDavUri.getHost();
|
||||
if (host.startsWith("http")) {
|
||||
String[] hostParts = host.split("://", 2);
|
||||
if (hostParts.length > 1) {
|
||||
host = hostParts[1];
|
||||
}
|
||||
}
|
||||
|
||||
port = webDavUri.getPort();
|
||||
|
||||
String userInfo = webDavUri.getUserInfo();
|
||||
if (userInfo != null) {
|
||||
String[] userInfoParts = userInfo.split(":");
|
||||
username = decodeUtf8(userInfoParts[0]);
|
||||
String userParts[] = username.split("\\\\", 2);
|
||||
|
||||
if (userParts.length > 1) {
|
||||
alias = userParts[1];
|
||||
} else {
|
||||
alias = username;
|
||||
}
|
||||
if (userInfoParts.length > 1) {
|
||||
password = decodeUtf8(userInfoParts[1]);
|
||||
}
|
||||
}
|
||||
|
||||
String[] pathParts = webDavUri.getPath().split("\\|");
|
||||
for (int i = 0, count = pathParts.length; i < count; i++) {
|
||||
if (i == 0) {
|
||||
if (pathParts[0] != null &&
|
||||
pathParts[0].length() > 1) {
|
||||
path = pathParts[0];
|
||||
}
|
||||
} else if (i == 1) {
|
||||
if (pathParts[1] != null &&
|
||||
pathParts[1].length() > 1) {
|
||||
authPath = pathParts[1];
|
||||
}
|
||||
} else if (i == 2) {
|
||||
if (pathParts[2] != null &&
|
||||
pathParts[2].length() > 1) {
|
||||
mailboxPath = pathParts[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new WebDavStoreSettings(host, port, connectionSecurity, null, username, password,
|
||||
null, alias, path, authPath, mailboxPath);
|
||||
}
|
||||
}
|
|
@ -1,36 +1,65 @@
|
|||
|
||||
package com.fsck.k9.mail.transport;
|
||||
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.SocketException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import android.support.annotation.VisibleForTesting;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.fsck.k9.mail.*;
|
||||
import com.fsck.k9.mail.Address;
|
||||
import com.fsck.k9.mail.AuthType;
|
||||
import com.fsck.k9.mail.Authentication;
|
||||
import com.fsck.k9.mail.AuthenticationFailedException;
|
||||
import com.fsck.k9.mail.CertificateValidationException;
|
||||
import com.fsck.k9.mail.ConnectionSecurity;
|
||||
import com.fsck.k9.mail.K9MailLib;
|
||||
import com.fsck.k9.mail.Message;
|
||||
import com.fsck.k9.mail.Message.RecipientType;
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
import com.fsck.k9.mail.ServerSettings;
|
||||
import com.fsck.k9.mail.Transport;
|
||||
import com.fsck.k9.mail.filter.Base64;
|
||||
import com.fsck.k9.mail.filter.EOLConvertingOutputStream;
|
||||
import com.fsck.k9.mail.filter.LineWrapOutputStream;
|
||||
import com.fsck.k9.mail.filter.PeekableInputStream;
|
||||
import com.fsck.k9.mail.filter.SmtpDataStuffing;
|
||||
import com.fsck.k9.mail.internet.CharsetSupport;
|
||||
import com.fsck.k9.mail.CertificateValidationException;
|
||||
import com.fsck.k9.mail.oauth.OAuth2TokenProvider;
|
||||
import com.fsck.k9.mail.oauth.XOAuth2ChallengeParser;
|
||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory;
|
||||
import com.fsck.k9.mail.store.StoreConfig;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.*;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.*;
|
||||
|
||||
import static com.fsck.k9.mail.CertificateValidationException.Reason.MissingCapability;
|
||||
import static com.fsck.k9.mail.K9MailLib.DEBUG_PROTOCOL_SMTP;
|
||||
import static com.fsck.k9.mail.K9MailLib.LOG_TAG;
|
||||
import static com.fsck.k9.mail.CertificateValidationException.Reason.MissingCapability;
|
||||
|
||||
public class SmtpTransport extends Transport {
|
||||
public static final int SMTP_CONTINUE_REQUEST = 334;
|
||||
public static final int SMTP_AUTHENTICATION_FAILURE_ERROR_CODE = 535;
|
||||
|
||||
private TrustedSocketFactory mTrustedSocketFactory;
|
||||
private OAuth2TokenProvider oauthTokenProvider;
|
||||
|
||||
/**
|
||||
* Decodes a SmtpTransport URI.
|
||||
|
@ -102,7 +131,7 @@ public class SmtpTransport extends Transport {
|
|||
username = decodeUtf8(userInfoParts[0]);
|
||||
password = decodeUtf8(userInfoParts[1]);
|
||||
} else if (userInfoParts.length == 3) {
|
||||
// NOTE: In SmptTransport URIs, the authType comes last!
|
||||
// NOTE: In SmtpTransport URIs, the authType comes last!
|
||||
authType = AuthType.valueOf(userInfoParts[2]);
|
||||
username = decodeUtf8(userInfoParts[0]);
|
||||
if (authType == AuthType.EXTERNAL) {
|
||||
|
@ -183,9 +212,10 @@ public class SmtpTransport extends Transport {
|
|||
private OutputStream mOut;
|
||||
private boolean m8bitEncodingAllowed;
|
||||
private int mLargestAcceptableMessage;
|
||||
private boolean retryXoauthWithNewToken;
|
||||
|
||||
public SmtpTransport(StoreConfig storeConfig, TrustedSocketFactory trustedSocketFactory)
|
||||
throws MessagingException {
|
||||
public SmtpTransport(StoreConfig storeConfig, TrustedSocketFactory trustedSocketFactory,
|
||||
OAuth2TokenProvider oauth2TokenProvider) throws MessagingException {
|
||||
ServerSettings settings;
|
||||
try {
|
||||
settings = decodeUri(storeConfig.getTransportUri());
|
||||
|
@ -203,6 +233,7 @@ public class SmtpTransport extends Transport {
|
|||
mPassword = settings.password;
|
||||
mClientCertificateAlias = settings.clientCertificateAlias;
|
||||
mTrustedSocketFactory = trustedSocketFactory;
|
||||
oauthTokenProvider = oauth2TokenProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -241,7 +272,7 @@ public class SmtpTransport extends Transport {
|
|||
executeSimpleCommand(null);
|
||||
|
||||
InetAddress localAddress = mSocket.getLocalAddress();
|
||||
String localHost = localAddress.getCanonicalHostName();
|
||||
String localHost = getCanonicalHostName(localAddress);
|
||||
String ipAddr = localAddress.getHostAddress();
|
||||
|
||||
if (localHost.equals("") || localHost.equals(ipAddr) || localHost.contains("_")) {
|
||||
|
@ -300,18 +331,21 @@ public class SmtpTransport extends Transport {
|
|||
boolean authPlainSupported = false;
|
||||
boolean authCramMD5Supported = false;
|
||||
boolean authExternalSupported = false;
|
||||
boolean authXoauth2Supported = false;
|
||||
if (extensions.containsKey("AUTH")) {
|
||||
List<String> saslMech = Arrays.asList(extensions.get("AUTH").split(" "));
|
||||
authLoginSupported = saslMech.contains("LOGIN");
|
||||
authPlainSupported = saslMech.contains("PLAIN");
|
||||
authCramMD5Supported = saslMech.contains("CRAM-MD5");
|
||||
authExternalSupported = saslMech.contains("EXTERNAL");
|
||||
authXoauth2Supported = saslMech.contains("XOAUTH2");
|
||||
}
|
||||
parseOptionalSizeValue(extensions);
|
||||
|
||||
if (mUsername != null
|
||||
&& mUsername.length() > 0
|
||||
&& (mPassword != null && mPassword.length() > 0 || AuthType.EXTERNAL == mAuthType)) {
|
||||
if (!TextUtils.isEmpty(mUsername)
|
||||
&& (!TextUtils.isEmpty(mPassword) ||
|
||||
AuthType.EXTERNAL == mAuthType ||
|
||||
AuthType.XOAUTH2 == mAuthType)) {
|
||||
|
||||
switch (mAuthType) {
|
||||
|
||||
|
@ -339,7 +373,13 @@ public class SmtpTransport extends Transport {
|
|||
throw new MessagingException("Authentication method CRAM-MD5 is unavailable.");
|
||||
}
|
||||
break;
|
||||
|
||||
case XOAUTH2:
|
||||
if (authXoauth2Supported && oauthTokenProvider != null) {
|
||||
saslXoauth2(mUsername);
|
||||
} else {
|
||||
throw new MessagingException("Authentication method XOAUTH2 is unavailable.");
|
||||
}
|
||||
break;
|
||||
case EXTERNAL:
|
||||
if (authExternalSupported) {
|
||||
saslAuthExternal(mUsername);
|
||||
|
@ -498,12 +538,11 @@ public class SmtpTransport extends Transport {
|
|||
|
||||
private void sendMessageTo(List<String> addresses, Message message)
|
||||
throws MessagingException {
|
||||
|
||||
close();
|
||||
open();
|
||||
|
||||
if (!m8bitEncodingAllowed) {
|
||||
message.setUsing7bitTransport();
|
||||
Log.d(LOG_TAG, "Server does not support 8bit transfer encoding");
|
||||
}
|
||||
// If the message has attachments and our server has told us about a limit on
|
||||
// the size of messages, count the message's size before sending it
|
||||
|
@ -637,30 +676,90 @@ public class SmtpTransport extends Transport {
|
|||
|
||||
throw new NegativeSmtpReplyException(replyCode, message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@Deprecated
|
||||
private List<String> executeSimpleCommand(String command) throws IOException, MessagingException {
|
||||
return executeSimpleCommand(command, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: All responses should be checked to confirm that they start with a valid
|
||||
* reply code, and that the reply code is appropriate for the command being executed.
|
||||
* That means it should either be a 2xx code (generally) or a 3xx code in special cases
|
||||
* (e.g., DATA & AUTH LOGIN commands). Reply codes should be made available as part of
|
||||
* the returned object.
|
||||
*
|
||||
* This should be done using the non-deprecated API below.
|
||||
*/
|
||||
@Deprecated
|
||||
private List<String> executeSimpleCommand(String command, boolean sensitive)
|
||||
throws IOException, MessagingException {
|
||||
List<String> results = new ArrayList<String>();
|
||||
List<String> results = new ArrayList<>();
|
||||
if (command != null) {
|
||||
writeLine(command, sensitive);
|
||||
}
|
||||
|
||||
/*
|
||||
* Read lines as long as the length is 4 or larger, e.g. "220-banner text here".
|
||||
* Shorter lines are either errors of contain only a reply code. Those cases will
|
||||
* be handled by checkLine() below.
|
||||
*
|
||||
* TODO: All responses should be checked to confirm that they start with a valid
|
||||
* reply code, and that the reply code is appropriate for the command being executed.
|
||||
* That means it should either be a 2xx code (generally) or a 3xx code in special cases
|
||||
* (e.g., DATA & AUTH LOGIN commands). Reply codes should be made available as part of
|
||||
* the returned object.
|
||||
*/
|
||||
String line = readCommandResponseLine(results);
|
||||
|
||||
// Check if the reply code indicates an error.
|
||||
checkLine(line);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static class CommandResponse {
|
||||
|
||||
private final int replyCode;
|
||||
private final String message;
|
||||
|
||||
public CommandResponse(int replyCode, String message) {
|
||||
this.replyCode = replyCode;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
private CommandResponse executeSimpleCommandWithResponse(String command, boolean sensitive) throws IOException, MessagingException {
|
||||
List<String> results = new ArrayList<>();
|
||||
if (command != null) {
|
||||
writeLine(command, sensitive);
|
||||
}
|
||||
|
||||
String line = readCommandResponseLine(results);
|
||||
|
||||
int length = line.length();
|
||||
if (length < 1) {
|
||||
throw new MessagingException("SMTP response is 0 length");
|
||||
}
|
||||
|
||||
int replyCode = -1;
|
||||
String message = line;
|
||||
if (length >= 3) {
|
||||
try {
|
||||
replyCode = Integer.parseInt(line.substring(0, 3));
|
||||
} catch (NumberFormatException e) { /* ignore */ }
|
||||
|
||||
if (length > 4) {
|
||||
message = line.substring(4);
|
||||
} else {
|
||||
message = "";
|
||||
}
|
||||
}
|
||||
|
||||
char c = line.charAt(0);
|
||||
if ((c == '4') || (c == '5')) {
|
||||
throw new NegativeSmtpReplyException(replyCode, message);
|
||||
}
|
||||
|
||||
return new CommandResponse(replyCode, message);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Read lines as long as the length is 4 or larger, e.g. "220-banner text here".
|
||||
* Shorter lines are either errors of contain only a reply code.
|
||||
*/
|
||||
private String readCommandResponseLine(List<String> results) throws IOException {
|
||||
String line = readLine();
|
||||
while (line.length() >= 4) {
|
||||
if (line.length() > 4) {
|
||||
|
@ -674,11 +773,7 @@ public class SmtpTransport extends Transport {
|
|||
}
|
||||
line = readLine();
|
||||
}
|
||||
|
||||
// Check if the reply code indicates an error.
|
||||
checkLine(line);
|
||||
|
||||
return results;
|
||||
return line;
|
||||
}
|
||||
|
||||
|
||||
|
@ -706,7 +801,7 @@ public class SmtpTransport extends Transport {
|
|||
executeSimpleCommand(Base64.encode(username), true);
|
||||
executeSimpleCommand(Base64.encode(password), true);
|
||||
} catch (NegativeSmtpReplyException exception) {
|
||||
if (exception.getReplyCode() == 535) {
|
||||
if (exception.getReplyCode() == SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) {
|
||||
// Authentication credentials invalid
|
||||
throw new AuthenticationFailedException("AUTH LOGIN failed ("
|
||||
+ exception.getMessage() + ")");
|
||||
|
@ -722,7 +817,7 @@ public class SmtpTransport extends Transport {
|
|||
try {
|
||||
executeSimpleCommand("AUTH PLAIN " + data, true);
|
||||
} catch (NegativeSmtpReplyException exception) {
|
||||
if (exception.getReplyCode() == 535) {
|
||||
if (exception.getReplyCode() == SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) {
|
||||
// Authentication credentials invalid
|
||||
throw new AuthenticationFailedException("AUTH PLAIN failed ("
|
||||
+ exception.getMessage() + ")");
|
||||
|
@ -746,7 +841,7 @@ public class SmtpTransport extends Transport {
|
|||
try {
|
||||
executeSimpleCommand(b64CRAMString, true);
|
||||
} catch (NegativeSmtpReplyException exception) {
|
||||
if (exception.getReplyCode() == 535) {
|
||||
if (exception.getReplyCode() == SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) {
|
||||
// Authentication credentials invalid
|
||||
throw new AuthenticationFailedException(exception.getMessage(), exception);
|
||||
} else {
|
||||
|
@ -755,12 +850,80 @@ public class SmtpTransport extends Transport {
|
|||
}
|
||||
}
|
||||
|
||||
private void saslXoauth2(String username) throws MessagingException, IOException {
|
||||
retryXoauthWithNewToken = true;
|
||||
try {
|
||||
attemptXoauth2(username);
|
||||
} catch (NegativeSmtpReplyException negativeResponse) {
|
||||
if (negativeResponse.getReplyCode() != SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) {
|
||||
throw negativeResponse;
|
||||
}
|
||||
|
||||
oauthTokenProvider.invalidateToken(username);
|
||||
|
||||
if (!retryXoauthWithNewToken) {
|
||||
handlePermanentFailure(negativeResponse);
|
||||
} else {
|
||||
handleTemporaryFailure(username, negativeResponse);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePermanentFailure(NegativeSmtpReplyException negativeResponse) throws AuthenticationFailedException {
|
||||
throw new AuthenticationFailedException(negativeResponse.getMessage(), negativeResponse);
|
||||
}
|
||||
|
||||
private void handleTemporaryFailure(String username, NegativeSmtpReplyException negativeResponseFromOldToken)
|
||||
throws IOException, MessagingException {
|
||||
// Token was invalid
|
||||
|
||||
//We could avoid this double check if we had a reasonable chance of knowing
|
||||
//if a token was invalid before use (e.g. due to expiry). But we don't
|
||||
//This is the intended behaviour per AccountManager
|
||||
|
||||
Log.v(LOG_TAG, "Authentication exception, re-trying with new token", negativeResponseFromOldToken);
|
||||
try {
|
||||
attemptXoauth2(username);
|
||||
} catch (NegativeSmtpReplyException negativeResponseFromNewToken) {
|
||||
if (negativeResponseFromNewToken.getReplyCode() != SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) {
|
||||
throw negativeResponseFromNewToken;
|
||||
}
|
||||
|
||||
//Okay, we failed on a new token.
|
||||
//Invalidate the token anyway but assume it's permanent.
|
||||
Log.v(LOG_TAG, "Authentication exception for new token, permanent error assumed",
|
||||
negativeResponseFromNewToken);
|
||||
|
||||
oauthTokenProvider.invalidateToken(username);
|
||||
|
||||
handlePermanentFailure(negativeResponseFromNewToken);
|
||||
}
|
||||
}
|
||||
|
||||
private void attemptXoauth2(String username) throws MessagingException, IOException {
|
||||
String token = oauthTokenProvider.getToken(username, OAuth2TokenProvider.OAUTH2_TIMEOUT);
|
||||
String authString = Authentication.computeXoauth(username, token);
|
||||
CommandResponse response = executeSimpleCommandWithResponse("AUTH XOAUTH2 " + authString, true);
|
||||
|
||||
if (response.replyCode == SMTP_CONTINUE_REQUEST) {
|
||||
retryXoauthWithNewToken = XOAuth2ChallengeParser.shouldRetry(response.message, mHost);
|
||||
|
||||
//Per Google spec, respond to challenge with empty response
|
||||
executeSimpleCommandWithResponse("", false);
|
||||
}
|
||||
}
|
||||
|
||||
private void saslAuthExternal(String username) throws MessagingException, IOException {
|
||||
executeSimpleCommand(
|
||||
String.format("AUTH EXTERNAL %s",
|
||||
Base64.encode(username)), false);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected String getCanonicalHostName(InetAddress localAddress) {
|
||||
return localAddress.getCanonicalHostName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception that is thrown when the server sends a negative reply (reply codes 4xx or 5xx).
|
||||
*/
|
||||
|
|
|
@ -7,6 +7,9 @@ import org.robolectric.RobolectricTestRunner;
|
|||
import org.robolectric.annotation.Config;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
|
@ -89,4 +92,62 @@ public class AddressTest {
|
|||
assertEquals("\"sa\"mp\"le\"", Address.quoteString("\"sa\"mp\"le\""));
|
||||
assertEquals("\"\"\"", Address.quoteString("\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hashCode_withoutAddress() throws Exception {
|
||||
Address address = Address.parse("name only")[0];
|
||||
assertNull(address.getAddress());
|
||||
|
||||
address.hashCode();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hashCode_withoutPersonal() throws Exception {
|
||||
Address address = Address.parse("alice@example.org")[0];
|
||||
assertNull(address.getPersonal());
|
||||
|
||||
address.hashCode();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equals_withoutAddress_matchesSame() throws Exception {
|
||||
Address address = Address.parse("name only")[0];
|
||||
Address address2 = Address.parse("name only")[0];
|
||||
assertNull(address.getAddress());
|
||||
|
||||
boolean result = address.equals(address2);
|
||||
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equals_withoutAddress_doesNotMatchWithAddress() throws Exception {
|
||||
Address address = Address.parse("name only")[0];
|
||||
Address address2 = Address.parse("name <alice.example.com>")[0];
|
||||
|
||||
boolean result = address.equals(address2);
|
||||
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equals_withoutPersonal_matchesSame() throws Exception {
|
||||
Address address = Address.parse("alice@example.org")[0];
|
||||
Address address2 = Address.parse("alice@example.org")[0];
|
||||
assertNull(address.getPersonal());
|
||||
|
||||
boolean result = address.equals(address2);
|
||||
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equals_withoutPersonal_doesNotMatchWithAddress() throws Exception {
|
||||
Address address = Address.parse("alice@example.org")[0];
|
||||
Address address2 = Address.parse("Alice <alice@example.org>")[0];
|
||||
|
||||
boolean result = address.equals(address2);
|
||||
|
||||
assertFalse(result);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
package com.fsck.k9.mail;
|
||||
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.mockito.stubbing.OngoingStubbing;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
|
||||
public class BoundaryGeneratorTest {
|
||||
@Test
|
||||
public void generateBoundary_allZeros() throws Exception {
|
||||
Random random = createRandom(0);
|
||||
BoundaryGenerator boundaryGenerator = new BoundaryGenerator(random);
|
||||
|
||||
String result = boundaryGenerator.generateBoundary();
|
||||
|
||||
assertEquals("----000000000000000000000000000000", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateBoundary() throws Exception {
|
||||
Random random = createRandom(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
|
||||
23, 24, 25, 26, 27, 28, 35);
|
||||
BoundaryGenerator boundaryGenerator = new BoundaryGenerator(random);
|
||||
|
||||
String result = boundaryGenerator.generateBoundary();
|
||||
|
||||
assertEquals("----0123456789ABCDEFGHIJKLMNOPQRSZ", result);
|
||||
}
|
||||
|
||||
private Random createRandom(int... values) {
|
||||
Random random = mock(Random.class);
|
||||
|
||||
OngoingStubbing<Integer> ongoingStubbing = when(random.nextInt(36));
|
||||
for (int value : values) {
|
||||
ongoingStubbing = ongoingStubbing.thenReturn(value);
|
||||
}
|
||||
|
||||
return random;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package com.fsck.k9.mail;
|
||||
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
@ -8,8 +9,7 @@ import java.io.OutputStream;
|
|||
import java.util.Date;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
import android.content.Context;
|
||||
|
||||
import com.fsck.k9.mail.Message.RecipientType;
|
||||
import com.fsck.k9.mail.internet.BinaryTempFileBody;
|
||||
|
@ -26,137 +26,27 @@ import org.apache.james.mime4j.util.MimeUtil;
|
|||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(manifest = Config.NONE, sdk = 21)
|
||||
public class MessageTest {
|
||||
|
||||
private Context context;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Tokyo"));
|
||||
BinaryTempFileBody.setTempDirectory(InstrumentationRegistry.getTargetContext().getCacheDir());
|
||||
}
|
||||
context = RuntimeEnvironment.application;
|
||||
|
||||
private static final String EIGHT_BIT_RESULT =
|
||||
"From: from@example.com\r\n"
|
||||
+ "To: to@example.com\r\n"
|
||||
+ "Subject: Test Message\r\n"
|
||||
+ "Date: Wed, 28 Aug 2013 08:51:09 -0400\r\n"
|
||||
+ "MIME-Version: 1.0\r\n"
|
||||
+ "Content-Type: multipart/mixed; boundary=\"----Boundary103\"\r\n"
|
||||
+ "Content-Transfer-Encoding: 8bit\r\n"
|
||||
+ "\r\n"
|
||||
+ "------Boundary103\r\n"
|
||||
+ "Content-Type: text/plain;\r\n"
|
||||
+ " charset=utf-8\r\n"
|
||||
+ "Content-Transfer-Encoding: 8bit\r\n"
|
||||
+ "\r\n"
|
||||
+ "Testing.\r\n"
|
||||
+ "This is a text body with some greek characters.\r\n"
|
||||
+ "αβγδεζηθ\r\n"
|
||||
+ "End of test.\r\n"
|
||||
+ "\r\n"
|
||||
+ "------Boundary103\r\n"
|
||||
+ "Content-Type: text/plain;\r\n"
|
||||
+ " charset=utf-8\r\n"
|
||||
+ "Content-Transfer-Encoding: quoted-printable\r\n"
|
||||
+ "\r\n"
|
||||
+ "Testing=2E\r\n"
|
||||
+ "This is a text body with some greek characters=2E\r\n"
|
||||
+ "=CE=B1=CE=B2=CE=B3=CE=B4=CE=B5=CE=B6=CE=B7=CE=B8\r\n"
|
||||
+ "End of test=2E\r\n"
|
||||
+ "\r\n"
|
||||
+ "------Boundary103\r\n"
|
||||
+ "Content-Type: application/octet-stream\r\n"
|
||||
+ "Content-Transfer-Encoding: base64\r\n"
|
||||
+ "\r\n"
|
||||
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\r\n"
|
||||
+ "\r\n"
|
||||
+ "------Boundary103\r\n"
|
||||
+ "Content-Type: message/rfc822\r\n"
|
||||
+ "Content-Disposition: attachment\r\n"
|
||||
+ "Content-Transfer-Encoding: 8bit\r\n"
|
||||
+ "\r\n"
|
||||
+ "From: from@example.com\r\n"
|
||||
+ "To: to@example.com\r\n"
|
||||
+ "Subject: Test Message\r\n"
|
||||
+ "Date: Wed, 28 Aug 2013 08:51:09 -0400\r\n"
|
||||
+ "MIME-Version: 1.0\r\n"
|
||||
+ "Content-Type: multipart/mixed; boundary=\"----Boundary102\"\r\n"
|
||||
+ "Content-Transfer-Encoding: 8bit\r\n"
|
||||
+ "\r\n"
|
||||
+ "------Boundary102\r\n"
|
||||
+ "Content-Type: text/plain;\r\n"
|
||||
+ " charset=utf-8\r\n"
|
||||
+ "Content-Transfer-Encoding: 8bit\r\n"
|
||||
+ "\r\n"
|
||||
+ "Testing.\r\n"
|
||||
+ "This is a text body with some greek characters.\r\n"
|
||||
+ "αβγδεζηθ\r\n"
|
||||
+ "End of test.\r\n"
|
||||
+ "\r\n"
|
||||
+ "------Boundary102\r\n"
|
||||
+ "Content-Type: text/plain;\r\n"
|
||||
+ " charset=utf-8\r\n"
|
||||
+ "Content-Transfer-Encoding: quoted-printable\r\n"
|
||||
+ "\r\n"
|
||||
+ "Testing=2E\r\n"
|
||||
+ "This is a text body with some greek characters=2E\r\n"
|
||||
+ "=CE=B1=CE=B2=CE=B3=CE=B4=CE=B5=CE=B6=CE=B7=CE=B8\r\n"
|
||||
+ "End of test=2E\r\n"
|
||||
+ "\r\n"
|
||||
+ "------Boundary102\r\n"
|
||||
+ "Content-Type: application/octet-stream\r\n"
|
||||
+ "Content-Transfer-Encoding: base64\r\n"
|
||||
+ "\r\n"
|
||||
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\r\n"
|
||||
+ "\r\n"
|
||||
+ "------Boundary102\r\n"
|
||||
+ "Content-Type: message/rfc822\r\n"
|
||||
+ "Content-Disposition: attachment\r\n"
|
||||
+ "Content-Transfer-Encoding: 8bit\r\n"
|
||||
+ "\r\n"
|
||||
+ "From: from@example.com\r\n"
|
||||
+ "To: to@example.com\r\n"
|
||||
+ "Subject: Test Message\r\n"
|
||||
+ "Date: Wed, 28 Aug 2013 08:51:09 -0400\r\n"
|
||||
+ "MIME-Version: 1.0\r\n"
|
||||
+ "Content-Type: multipart/mixed; boundary=\"----Boundary101\"\r\n"
|
||||
+ "Content-Transfer-Encoding: 8bit\r\n"
|
||||
+ "\r\n"
|
||||
+ "------Boundary101\r\n"
|
||||
+ "Content-Type: text/plain;\r\n"
|
||||
+ " charset=utf-8\r\n"
|
||||
+ "Content-Transfer-Encoding: 8bit\r\n"
|
||||
+ "\r\n"
|
||||
+ "Testing.\r\n"
|
||||
+ "This is a text body with some greek characters.\r\n"
|
||||
+ "αβγδεζηθ\r\n"
|
||||
+ "End of test.\r\n"
|
||||
+ "\r\n"
|
||||
+ "------Boundary101\r\n"
|
||||
+ "Content-Type: text/plain;\r\n"
|
||||
+ " charset=utf-8\r\n"
|
||||
+ "Content-Transfer-Encoding: quoted-printable\r\n"
|
||||
+ "\r\n"
|
||||
+ "Testing=2E\r\n"
|
||||
+ "This is a text body with some greek characters=2E\r\n"
|
||||
+ "=CE=B1=CE=B2=CE=B3=CE=B4=CE=B5=CE=B6=CE=B7=CE=B8\r\n"
|
||||
+ "End of test=2E\r\n"
|
||||
+ "\r\n"
|
||||
+ "------Boundary101\r\n"
|
||||
+ "Content-Type: application/octet-stream\r\n"
|
||||
+ "Content-Transfer-Encoding: base64\r\n"
|
||||
+ "\r\n"
|
||||
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\r\n"
|
||||
+ "\r\n"
|
||||
+ "------Boundary101--\r\n"
|
||||
+ "\r\n"
|
||||
+ "------Boundary102--\r\n"
|
||||
+ "\r\n"
|
||||
+ "------Boundary103--\r\n";
|
||||
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Tokyo"));
|
||||
BinaryTempFileBody.setTempDirectory(context.getCacheDir());
|
||||
}
|
||||
|
||||
private static final String SEVEN_BIT_RESULT =
|
||||
"From: from@example.com\r\n"
|
||||
|
@ -168,19 +58,9 @@ public class MessageTest {
|
|||
+ "Content-Transfer-Encoding: 7bit\r\n"
|
||||
+ "\r\n"
|
||||
+ "------Boundary103\r\n"
|
||||
+ "Content-Transfer-Encoding: quoted-printable\r\n"
|
||||
+ "Content-Type: text/plain;\r\n"
|
||||
+ " charset=utf-8\r\n"
|
||||
+ "Content-Transfer-Encoding: quoted-printable\r\n"
|
||||
+ "\r\n"
|
||||
+ "Testing=2E\r\n"
|
||||
+ "This is a text body with some greek characters=2E\r\n"
|
||||
+ "=CE=B1=CE=B2=CE=B3=CE=B4=CE=B5=CE=B6=CE=B7=CE=B8\r\n"
|
||||
+ "End of test=2E\r\n"
|
||||
+ "\r\n"
|
||||
+ "------Boundary103\r\n"
|
||||
+ "Content-Type: text/plain;\r\n"
|
||||
+ " charset=utf-8\r\n"
|
||||
+ "Content-Transfer-Encoding: quoted-printable\r\n"
|
||||
+ "\r\n"
|
||||
+ "Testing=2E\r\n"
|
||||
+ "This is a text body with some greek characters=2E\r\n"
|
||||
|
@ -207,19 +87,9 @@ public class MessageTest {
|
|||
+ "Content-Transfer-Encoding: 7bit\r\n"
|
||||
+ "\r\n"
|
||||
+ "------Boundary102\r\n"
|
||||
+ "Content-Transfer-Encoding: quoted-printable\r\n"
|
||||
+ "Content-Type: text/plain;\r\n"
|
||||
+ " charset=utf-8\r\n"
|
||||
+ "Content-Transfer-Encoding: quoted-printable\r\n"
|
||||
+ "\r\n"
|
||||
+ "Testing=2E\r\n"
|
||||
+ "This is a text body with some greek characters=2E\r\n"
|
||||
+ "=CE=B1=CE=B2=CE=B3=CE=B4=CE=B5=CE=B6=CE=B7=CE=B8\r\n"
|
||||
+ "End of test=2E\r\n"
|
||||
+ "\r\n"
|
||||
+ "------Boundary102\r\n"
|
||||
+ "Content-Type: text/plain;\r\n"
|
||||
+ " charset=utf-8\r\n"
|
||||
+ "Content-Transfer-Encoding: quoted-printable\r\n"
|
||||
+ "\r\n"
|
||||
+ "Testing=2E\r\n"
|
||||
+ "This is a text body with some greek characters=2E\r\n"
|
||||
|
@ -246,19 +116,9 @@ public class MessageTest {
|
|||
+ "Content-Transfer-Encoding: 7bit\r\n"
|
||||
+ "\r\n"
|
||||
+ "------Boundary101\r\n"
|
||||
+ "Content-Transfer-Encoding: quoted-printable\r\n"
|
||||
+ "Content-Type: text/plain;\r\n"
|
||||
+ " charset=utf-8\r\n"
|
||||
+ "Content-Transfer-Encoding: quoted-printable\r\n"
|
||||
+ "\r\n"
|
||||
+ "Testing=2E\r\n"
|
||||
+ "This is a text body with some greek characters=2E\r\n"
|
||||
+ "=CE=B1=CE=B2=CE=B3=CE=B4=CE=B5=CE=B6=CE=B7=CE=B8\r\n"
|
||||
+ "End of test=2E\r\n"
|
||||
+ "\r\n"
|
||||
+ "------Boundary101\r\n"
|
||||
+ "Content-Type: text/plain;\r\n"
|
||||
+ " charset=utf-8\r\n"
|
||||
+ "Content-Transfer-Encoding: quoted-printable\r\n"
|
||||
+ "\r\n"
|
||||
+ "Testing=2E\r\n"
|
||||
+ "This is a text body with some greek characters=2E\r\n"
|
||||
|
@ -279,22 +139,12 @@ public class MessageTest {
|
|||
|
||||
private static final String TO_BODY_PART_RESULT =
|
||||
"Content-Type: multipart/mixed; boundary=\"----Boundary103\"\r\n"
|
||||
+ "Content-Transfer-Encoding: 8bit\r\n"
|
||||
+ "Content-Transfer-Encoding: 7bit\r\n"
|
||||
+ "\r\n"
|
||||
+ "------Boundary103\r\n"
|
||||
+ "Content-Type: text/plain;\r\n"
|
||||
+ " charset=utf-8\r\n"
|
||||
+ "Content-Transfer-Encoding: 8bit\r\n"
|
||||
+ "\r\n"
|
||||
+ "Testing.\r\n"
|
||||
+ "This is a text body with some greek characters.\r\n"
|
||||
+ "αβγδεζηθ\r\n"
|
||||
+ "End of test.\r\n"
|
||||
+ "\r\n"
|
||||
+ "------Boundary103\r\n"
|
||||
+ "Content-Type: text/plain;\r\n"
|
||||
+ " charset=utf-8\r\n"
|
||||
+ "Content-Transfer-Encoding: quoted-printable\r\n"
|
||||
+ "Content-Type: text/plain;\r\n"
|
||||
+ " charset=utf-8\r\n"
|
||||
+ "\r\n"
|
||||
+ "Testing=2E\r\n"
|
||||
+ "This is a text body with some greek characters=2E\r\n"
|
||||
|
@ -310,7 +160,7 @@ public class MessageTest {
|
|||
+ "------Boundary103\r\n"
|
||||
+ "Content-Type: message/rfc822\r\n"
|
||||
+ "Content-Disposition: attachment\r\n"
|
||||
+ "Content-Transfer-Encoding: 8bit\r\n"
|
||||
+ "Content-Transfer-Encoding: 7bit\r\n"
|
||||
+ "\r\n"
|
||||
+ "From: from@example.com\r\n"
|
||||
+ "To: to@example.com\r\n"
|
||||
|
@ -318,22 +168,12 @@ public class MessageTest {
|
|||
+ "Date: Wed, 28 Aug 2013 08:51:09 -0400\r\n"
|
||||
+ "MIME-Version: 1.0\r\n"
|
||||
+ "Content-Type: multipart/mixed; boundary=\"----Boundary102\"\r\n"
|
||||
+ "Content-Transfer-Encoding: 8bit\r\n"
|
||||
+ "Content-Transfer-Encoding: 7bit\r\n"
|
||||
+ "\r\n"
|
||||
+ "------Boundary102\r\n"
|
||||
+ "Content-Type: text/plain;\r\n"
|
||||
+ " charset=utf-8\r\n"
|
||||
+ "Content-Transfer-Encoding: 8bit\r\n"
|
||||
+ "\r\n"
|
||||
+ "Testing.\r\n"
|
||||
+ "This is a text body with some greek characters.\r\n"
|
||||
+ "αβγδεζηθ\r\n"
|
||||
+ "End of test.\r\n"
|
||||
+ "\r\n"
|
||||
+ "------Boundary102\r\n"
|
||||
+ "Content-Type: text/plain;\r\n"
|
||||
+ " charset=utf-8\r\n"
|
||||
+ "Content-Transfer-Encoding: quoted-printable\r\n"
|
||||
+ "Content-Type: text/plain;\r\n"
|
||||
+ " charset=utf-8\r\n"
|
||||
+ "\r\n"
|
||||
+ "Testing=2E\r\n"
|
||||
+ "This is a text body with some greek characters=2E\r\n"
|
||||
|
@ -349,7 +189,7 @@ public class MessageTest {
|
|||
+ "------Boundary102\r\n"
|
||||
+ "Content-Type: message/rfc822\r\n"
|
||||
+ "Content-Disposition: attachment\r\n"
|
||||
+ "Content-Transfer-Encoding: 8bit\r\n"
|
||||
+ "Content-Transfer-Encoding: 7bit\r\n"
|
||||
+ "\r\n"
|
||||
+ "From: from@example.com\r\n"
|
||||
+ "To: to@example.com\r\n"
|
||||
|
@ -357,22 +197,12 @@ public class MessageTest {
|
|||
+ "Date: Wed, 28 Aug 2013 08:51:09 -0400\r\n"
|
||||
+ "MIME-Version: 1.0\r\n"
|
||||
+ "Content-Type: multipart/mixed; boundary=\"----Boundary101\"\r\n"
|
||||
+ "Content-Transfer-Encoding: 8bit\r\n"
|
||||
+ "Content-Transfer-Encoding: 7bit\r\n"
|
||||
+ "\r\n"
|
||||
+ "------Boundary101\r\n"
|
||||
+ "Content-Type: text/plain;\r\n"
|
||||
+ " charset=utf-8\r\n"
|
||||
+ "Content-Transfer-Encoding: 8bit\r\n"
|
||||
+ "\r\n"
|
||||
+ "Testing.\r\n"
|
||||
+ "This is a text body with some greek characters.\r\n"
|
||||
+ "αβγδεζηθ\r\n"
|
||||
+ "End of test.\r\n"
|
||||
+ "\r\n"
|
||||
+ "------Boundary101\r\n"
|
||||
+ "Content-Type: text/plain;\r\n"
|
||||
+ " charset=utf-8\r\n"
|
||||
+ "Content-Transfer-Encoding: quoted-printable\r\n"
|
||||
+ "Content-Type: text/plain;\r\n"
|
||||
+ " charset=utf-8\r\n"
|
||||
+ "\r\n"
|
||||
+ "Testing=2E\r\n"
|
||||
+ "This is a text body with some greek characters=2E\r\n"
|
||||
|
@ -423,19 +253,12 @@ public class MessageTest {
|
|||
MimeMessage message;
|
||||
ByteArrayOutputStream out;
|
||||
|
||||
BinaryTempFileBody.setTempDirectory(InstrumentationRegistry.getTargetContext().getCacheDir());
|
||||
BinaryTempFileBody.setTempDirectory(context.getCacheDir());
|
||||
|
||||
mMimeBoundary = 101;
|
||||
message = nestedMessage(nestedMessage(sampleMessage()));
|
||||
out = new ByteArrayOutputStream();
|
||||
message.writeTo(out);
|
||||
assertEquals(EIGHT_BIT_RESULT, out.toString());
|
||||
|
||||
mMimeBoundary = 101;
|
||||
message = nestedMessage(nestedMessage(sampleMessage()));
|
||||
message.setUsing7bitTransport();
|
||||
out = new ByteArrayOutputStream();
|
||||
message.writeTo(out);
|
||||
assertEquals(SEVEN_BIT_RESULT, out.toString());
|
||||
}
|
||||
|
||||
|
@ -452,7 +275,7 @@ public class MessageTest {
|
|||
|
||||
MimeBodyPart bodyPart = new MimeBodyPart(tempMessageBody, "message/rfc822");
|
||||
bodyPart.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, "attachment");
|
||||
bodyPart.setEncoding(MimeUtil.ENC_8BIT);
|
||||
bodyPart.setEncoding(MimeUtil.ENC_7BIT);
|
||||
|
||||
MimeMessage parentMessage = sampleMessage();
|
||||
((Multipart) parentMessage.getBody()).addBodyPart(bodyPart);
|
||||
|
@ -466,11 +289,10 @@ public class MessageTest {
|
|||
message.setRecipient(RecipientType.TO, new Address("to@example.com"));
|
||||
message.setSubject("Test Message");
|
||||
message.setHeader("Date", "Wed, 28 Aug 2013 08:51:09 -0400");
|
||||
message.setEncoding(MimeUtil.ENC_8BIT);
|
||||
message.setEncoding(MimeUtil.ENC_7BIT);
|
||||
|
||||
MimeMultipart multipartBody = new MimeMultipart("multipart/mixed", generateBoundary());
|
||||
multipartBody.addBodyPart(textBodyPart(MimeUtil.ENC_8BIT));
|
||||
multipartBody.addBodyPart(textBodyPart(MimeUtil.ENC_QUOTED_PRINTABLE));
|
||||
multipartBody.addBodyPart(textBodyPart());
|
||||
multipartBody.addBodyPart(binaryBodyPart());
|
||||
MimeMessageHelper.setBody(message, multipartBody);
|
||||
|
||||
|
@ -501,17 +323,17 @@ public class MessageTest {
|
|||
return bodyPart;
|
||||
}
|
||||
|
||||
private MimeBodyPart textBodyPart(String encoding)
|
||||
throws MessagingException {
|
||||
private MimeBodyPart textBodyPart() throws MessagingException {
|
||||
TextBody textBody = new TextBody(
|
||||
"Testing.\r\n"
|
||||
+ "This is a text body with some greek characters.\r\n"
|
||||
+ "αβγδεζηθ\r\n"
|
||||
+ "End of test.\r\n");
|
||||
textBody.setCharset("utf-8");
|
||||
MimeBodyPart bodyPart = new MimeBodyPart(textBody, "text/plain");
|
||||
|
||||
MimeBodyPart bodyPart = new MimeBodyPart();
|
||||
MimeMessageHelper.setBody(bodyPart, textBody);
|
||||
CharsetSupport.setCharset("utf-8", bodyPart);
|
||||
bodyPart.setEncoding(encoding);
|
||||
return bodyPart;
|
||||
}
|
||||
|
||||
|
@ -524,7 +346,7 @@ public class MessageTest {
|
|||
MimeMessage message;
|
||||
ByteArrayOutputStream out;
|
||||
|
||||
BinaryTempFileBody.setTempDirectory(InstrumentationRegistry.getTargetContext().getCacheDir());
|
||||
BinaryTempFileBody.setTempDirectory(context.getCacheDir());
|
||||
|
||||
mMimeBoundary = 101;
|
||||
message = nestedMessage(nestedMessage(sampleMessage()));
|
|
@ -0,0 +1,15 @@
|
|||
package com.fsck.k9.mail;
|
||||
|
||||
import com.fsck.k9.mail.filter.Base64;
|
||||
|
||||
|
||||
public class XOAuth2ChallengeParserTest {
|
||||
public static final String STATUS_400_RESPONSE = Base64.encode(
|
||||
"{\"status\":\"400\",\"schemes\":\"bearer mac\",\"scope\":\"https://mail.google.com/\"}");
|
||||
public static final String STATUS_401_RESPONSE = Base64.encode(
|
||||
"{\"status\":\"401\",\"schemes\":\"bearer mac\",\"scope\":\"https://mail.google.com/\"}");
|
||||
public static final String MISSING_STATUS_RESPONSE = Base64.encode(
|
||||
"{\"schemes\":\"bearer mac\",\"scope\":\"https://mail.google.com/\"}");
|
||||
public static final String INVALID_RESPONSE = Base64.encode(
|
||||
"{\"status\":\"401\",\"schemes\":\"bearer mac\",\"scope\":\"https://mail.google.com/\"");
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package com.fsck.k9.mail.helpers;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import com.fsck.k9.mail.Address;
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
import com.fsck.k9.mail.internet.MimeMessage;
|
||||
import okio.BufferedSink;
|
||||
import okio.Okio;
|
||||
|
||||
|
||||
class TestMessage extends MimeMessage {
|
||||
private final long messageSize;
|
||||
private final Address[] from;
|
||||
private final Address[] to;
|
||||
private final boolean hasAttachments;
|
||||
|
||||
|
||||
TestMessage(TestMessageBuilder builder) {
|
||||
from = toAddressArray(builder.from);
|
||||
to = toAddressArray(builder.to);
|
||||
hasAttachments = builder.hasAttachments;
|
||||
messageSize = builder.messageSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address[] getFrom() {
|
||||
return from;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address[] getRecipients(RecipientType type) {
|
||||
switch (type) {
|
||||
case TO:
|
||||
return to;
|
||||
case CC:
|
||||
return new Address[0];
|
||||
case BCC:
|
||||
return new Address[0];
|
||||
}
|
||||
|
||||
throw new AssertionError("Missing switch case: " + type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasAttachments() {
|
||||
return hasAttachments;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long calculateSize() {
|
||||
return messageSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(OutputStream out) throws IOException, MessagingException {
|
||||
BufferedSink bufferedSink = Okio.buffer(Okio.sink(out));
|
||||
bufferedSink.writeUtf8("[message data]");
|
||||
bufferedSink.emit();
|
||||
}
|
||||
|
||||
private static Address[] toAddressArray(String email) {
|
||||
return email == null ? new Address[0] : new Address[] { new Address(email) };
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package com.fsck.k9.mail.helpers;
|
||||
|
||||
|
||||
import com.fsck.k9.mail.Message;
|
||||
|
||||
|
||||
public class TestMessageBuilder {
|
||||
String from;
|
||||
String to;
|
||||
boolean hasAttachments;
|
||||
long messageSize;
|
||||
|
||||
|
||||
public TestMessageBuilder from(String email) {
|
||||
from = email;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestMessageBuilder to(String email) {
|
||||
to = email;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestMessageBuilder setHasAttachments(boolean hasAttachments) {
|
||||
this.hasAttachments = hasAttachments;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestMessageBuilder messageSize(long messageSize) {
|
||||
this.messageSize = messageSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Message build() {
|
||||
return new TestMessage(this);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package com.fsck.k9.mail.helpers;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
|
||||
|
||||
public class TestTrustedSocketFactory implements TrustedSocketFactory {
|
||||
@Override
|
||||
public Socket createSocket(Socket socket, String host, int port, String clientCertificateAlias)
|
||||
throws NoSuchAlgorithmException, KeyManagementException, MessagingException, IOException {
|
||||
|
||||
TrustManager[] trustManagers = new TrustManager[] { new VeryTrustingTrustManager() };
|
||||
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null, trustManagers, null);
|
||||
|
||||
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
|
||||
return sslSocketFactory.createSocket(
|
||||
socket,
|
||||
socket.getInetAddress().getHostAddress(),
|
||||
socket.getPort(),
|
||||
true);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package com.fsck.k9.mail.helpers;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
|
||||
@SuppressLint("TrustAllX509TrustManager")
|
||||
class VeryTrustingTrustManager implements X509TrustManager {
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||
// Accept all certificates
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||
// Accept all certificates
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return new X509Certificate[0];
|
||||
}
|
||||
}
|
||||
|
|
@ -72,13 +72,16 @@ public class MessageExtractorTest {
|
|||
assertNull(result);
|
||||
}
|
||||
|
||||
@Test(expected = UnsupportedOperationException.class)
|
||||
public void getTextFromPart_withUnknownEncoding_shouldThrowRuntimeException() throws Exception {
|
||||
@Test
|
||||
public void getTextFromPart_withUnknownEncoding_shouldReturnUnmodifiedBodyContents() throws Exception {
|
||||
part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "text/plain");
|
||||
BinaryMemoryBody body = new BinaryMemoryBody("Sample text body".getBytes(), "unknown encoding");
|
||||
String bodyText = "Sample text body";
|
||||
BinaryMemoryBody body = new BinaryMemoryBody(bodyText.getBytes(), "unknown encoding");
|
||||
part.setBody(body);
|
||||
|
||||
MessageExtractor.getTextFromPart(part);
|
||||
String result = MessageExtractor.getTextFromPart(part);
|
||||
|
||||
assertEquals(bodyText, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
package com.fsck.k9.mail.internet;
|
||||
|
||||
|
||||
import com.fsck.k9.mail.Address;
|
||||
import com.fsck.k9.mail.Message;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(manifest = Config.NONE, sdk = 21)
|
||||
public class MessageIdGeneratorTest {
|
||||
private MessageIdGenerator messageIdGenerator;
|
||||
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
messageIdGenerator = new MessageIdGenerator() {
|
||||
@Override
|
||||
protected String generateUuid() {
|
||||
return "00000000-0000-4000-0000-000000000000";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateMessageId_withFromAndReplyToAddress() throws Exception {
|
||||
Message message = new MimeMessage();
|
||||
message.setFrom(new Address("alice@example.org"));
|
||||
message.setReplyTo(Address.parse("bob@example.com"));
|
||||
|
||||
String result = messageIdGenerator.generateMessageId(message);
|
||||
|
||||
assertEquals("<00000000-0000-4000-0000-000000000000@example.org>", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateMessageId_withReplyToAddress() throws Exception {
|
||||
Message message = new MimeMessage();
|
||||
message.setReplyTo(Address.parse("bob@example.com"));
|
||||
|
||||
String result = messageIdGenerator.generateMessageId(message);
|
||||
|
||||
assertEquals("<00000000-0000-4000-0000-000000000000@example.com>", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateMessageId_withoutRelevantHeaders() throws Exception {
|
||||
Message message = new MimeMessage();
|
||||
|
||||
String result = messageIdGenerator.generateMessageId(message);
|
||||
|
||||
assertEquals("<00000000-0000-4000-0000-000000000000@email.android.com>", result);
|
||||
}
|
||||
}
|
|
@ -52,6 +52,16 @@ public class MimeMessageParseTest {
|
|||
assertEquals("this is some test text.", streamToString(MimeUtility.decodeBody(msg.getBody())));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void headerFieldNameWithSpace() throws Exception {
|
||||
MimeMessage msg = parseWithoutRecurse(toStream("" +
|
||||
"From : <adam@example.org>\r\n" +
|
||||
"\r\n" +
|
||||
"Body"));
|
||||
|
||||
assertEquals("<adam@example.org>", msg.getHeader("From")[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSinglePart8BitRecurse() throws Exception {
|
||||
MimeMessage msg = parseWithRecurse(toStream(
|
||||
|
@ -128,6 +138,23 @@ public class MimeMessageParseTest {
|
|||
"");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeBody_withUnknownEncoding_shouldReturnUnmodifiedBodyContents() throws Exception {
|
||||
MimeMessage msg = parseWithoutRecurse(toStream(
|
||||
"From: <adam@example.org>\r\n" +
|
||||
"To: <eva@example.org>\r\n" +
|
||||
"Subject: Testmail\r\n" +
|
||||
"MIME-Version: 1.0\r\n" +
|
||||
"Content-type: text/plain\r\n" +
|
||||
"Content-Transfer-Encoding: utf-8\r\n" +
|
||||
"\r\n" +
|
||||
"dGhpcyBpcyBzb21lIG1vcmUgdGVzdCB0ZXh0Lg==\r\n"));
|
||||
|
||||
InputStream inputStream = MimeUtility.decodeBody(msg.getBody());
|
||||
|
||||
assertEquals("dGhpcyBpcyBzb21lIG1vcmUgdGVzdCB0ZXh0Lg==\r\n", streamToString(inputStream));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipartSingleLayerRecurse() throws Exception {
|
||||
MimeMessage msg = parseWithRecurse(toStream(
|
||||
|
@ -210,11 +237,11 @@ public class MimeMessageParseTest {
|
|||
}
|
||||
|
||||
private static MimeMessage parseWithoutRecurse(InputStream data) throws Exception {
|
||||
return new MimeMessage(data, false);
|
||||
return MimeMessage.parseMimeMessage(data, false);
|
||||
}
|
||||
|
||||
private static MimeMessage parseWithRecurse(InputStream data) throws Exception {
|
||||
return new MimeMessage(data, true);
|
||||
return MimeMessage.parseMimeMessage(data, true);
|
||||
}
|
||||
|
||||
private static void checkAddresses(Address[] actual, String... expected) {
|
||||
|
|
|
@ -133,4 +133,19 @@ public class MimeUtilityTest {
|
|||
public void isSameMimeType_withSecondArgumentBeingNull_shouldReturnFalse() throws Exception {
|
||||
assertFalse(MimeUtility.isSameMimeType("text/html", null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isFormatFlowed_withTextPlainFormatFlowed__shouldReturnTrue() throws Exception {
|
||||
assertTrue(MimeUtility.isFormatFlowed("text/plain; format=flowed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isFormatFlowed_withTextPlain__shouldReturnFalse() throws Exception {
|
||||
assertFalse(MimeUtility.isFormatFlowed("text/plain"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isFormatFlowed_withTextHtmlFormatFlowed__shouldReturnFalse() throws Exception {
|
||||
assertFalse(MimeUtility.isFormatFlowed("text/html; format=flowed"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
package com.fsck.k9.mail.internet;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
import okio.Buffer;
|
||||
import org.apache.james.mime4j.util.MimeUtil;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
|
||||
public class TextBodyTest {
|
||||
@Test
|
||||
public void getSize_withSignUnsafeData_shouldReturnCorrectValue() throws Exception {
|
||||
TextBody textBody = new TextBody("From Bernd");
|
||||
textBody.setEncoding(MimeUtil.ENC_QUOTED_PRINTABLE);
|
||||
|
||||
long result = textBody.getSize();
|
||||
|
||||
int outputSize = getSizeOfSerializedBody(textBody);
|
||||
assertEquals(outputSize, result);
|
||||
}
|
||||
|
||||
private int getSizeOfSerializedBody(TextBody textBody) throws IOException, MessagingException {
|
||||
Buffer buffer = new Buffer();
|
||||
textBody.writeTo(buffer.outputStream());
|
||||
return buffer.readByteString().size();
|
||||
}
|
||||
}
|
|
@ -2,15 +2,8 @@ package com.fsck.k9.mail.store.imap;
|
|||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.Socket;
|
||||
import java.net.UnknownHostException;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.net.ConnectivityManager;
|
||||
|
||||
import com.fsck.k9.mail.AuthType;
|
||||
|
@ -20,16 +13,16 @@ import com.fsck.k9.mail.CertificateValidationException.Reason;
|
|||
import com.fsck.k9.mail.ConnectionSecurity;
|
||||
import com.fsck.k9.mail.K9MailLib;
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
import com.fsck.k9.mail.XOAuth2ChallengeParserTest;
|
||||
import com.fsck.k9.mail.helpers.TestTrustedSocketFactory;
|
||||
import com.fsck.k9.mail.oauth.OAuth2TokenProvider;
|
||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory;
|
||||
import com.fsck.k9.mail.store.imap.mockserver.MockImapServer;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
import okio.ByteString;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InOrder;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.shadows.ShadowLog;
|
||||
|
@ -37,11 +30,12 @@ import org.robolectric.shadows.ShadowLog;
|
|||
import static org.hamcrest.core.StringContains.containsString;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
|
@ -51,18 +45,24 @@ public class ImapConnectionTest {
|
|||
|
||||
private static final String USERNAME = "user";
|
||||
private static final String PASSWORD = "123456";
|
||||
private static final int SOCKET_CONNECT_TIMEOUT = 2000;
|
||||
private static final int SOCKET_READ_TIMEOUT = 1000;
|
||||
private static final int SOCKET_CONNECT_TIMEOUT = 10000;
|
||||
private static final int SOCKET_READ_TIMEOUT = 10000;
|
||||
private static final String XOAUTH_STRING = ByteString.encodeUtf8(
|
||||
"user=" + USERNAME + "\001auth=Bearer token\001\001").base64();
|
||||
private static final String XOAUTH_STRING_RETRY = ByteString.encodeUtf8(
|
||||
"user=" + USERNAME + "\001auth=Bearer token2\001\001").base64();
|
||||
|
||||
|
||||
private TrustedSocketFactory socketFactory;
|
||||
private ConnectivityManager connectivityManager;
|
||||
private OAuth2TokenProvider oAuth2TokenProvider;
|
||||
private SimpleImapSettings settings;
|
||||
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
connectivityManager = mock(ConnectivityManager.class);
|
||||
oAuth2TokenProvider = mock(OAuth2TokenProvider.class);
|
||||
socketFactory = new TestTrustedSocketFactory();
|
||||
|
||||
settings = new SimpleImapSettings();
|
||||
|
@ -83,7 +83,7 @@ public class ImapConnectionTest {
|
|||
server.output("* OK [CAPABILITY IMAP4 IMAP4REV1 AUTH=PLAIN]");
|
||||
server.expect("1 AUTHENTICATE PLAIN");
|
||||
server.output("+");
|
||||
server.expect(ByteString.encodeUtf8("\000" + USERNAME+ "\000" + PASSWORD).base64());
|
||||
server.expect(ByteString.encodeUtf8("\000" + USERNAME + "\000" + PASSWORD).base64());
|
||||
server.output("1 OK Success");
|
||||
server.expect("2 LIST \"\" \"\"");
|
||||
server.output("* LIST () \"/\" foo/bar");
|
||||
|
@ -103,7 +103,7 @@ public class ImapConnectionTest {
|
|||
preAuthenticationDialog(server, "AUTH=PLAIN");
|
||||
server.expect("2 AUTHENTICATE PLAIN");
|
||||
server.output("+");
|
||||
server.expect(ByteString.encodeUtf8("\000" + USERNAME+ "\000" + PASSWORD).base64());
|
||||
server.expect(ByteString.encodeUtf8("\000" + USERNAME + "\000" + PASSWORD).base64());
|
||||
server.output("2 OK Success");
|
||||
simplePostAuthenticationDialog(server);
|
||||
ImapConnection imapConnection = startServerAndCreateImapConnection(server);
|
||||
|
@ -161,7 +161,7 @@ public class ImapConnectionTest {
|
|||
preAuthenticationDialog(server, "AUTH=PLAIN");
|
||||
server.expect("2 AUTHENTICATE PLAIN");
|
||||
server.output("+");
|
||||
server.expect(ByteString.encodeUtf8("\000" + USERNAME+ "\000" + PASSWORD).base64());
|
||||
server.expect(ByteString.encodeUtf8("\000" + USERNAME + "\000" + PASSWORD).base64());
|
||||
server.output("2 NO Login Failure");
|
||||
server.expect("3 LOGIN \"" + USERNAME + "\" \"" + PASSWORD + "\"");
|
||||
server.output("3 OK LOGIN completed");
|
||||
|
@ -181,7 +181,7 @@ public class ImapConnectionTest {
|
|||
preAuthenticationDialog(server, "AUTH=PLAIN");
|
||||
server.expect("2 AUTHENTICATE PLAIN");
|
||||
server.output("+");
|
||||
server.expect(ByteString.encodeUtf8("\000" + USERNAME+ "\000" + PASSWORD).base64());
|
||||
server.expect(ByteString.encodeUtf8("\000" + USERNAME + "\000" + PASSWORD).base64());
|
||||
server.output("2 NO Login Failure");
|
||||
server.expect("3 LOGIN \"" + USERNAME + "\" \"" + PASSWORD + "\"");
|
||||
server.output("3 NO Go away");
|
||||
|
@ -192,7 +192,33 @@ public class ImapConnectionTest {
|
|||
fail("Expected exception");
|
||||
} catch (AuthenticationFailedException e) {
|
||||
//FIXME: improve exception message
|
||||
assertThat(e.getMessage(), containsString("response: #3# [NO, Go away]"));
|
||||
assertThat(e.getMessage(), containsString("Go away"));
|
||||
}
|
||||
|
||||
server.verifyConnectionClosed();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_authPlainWithByeResponseAndConnectionClose_shouldThrowAuthenticationFailedException()
|
||||
throws Exception {
|
||||
settings.setAuthType(AuthType.PLAIN);
|
||||
MockImapServer server = new MockImapServer();
|
||||
preAuthenticationDialog(server, "AUTH=PLAIN");
|
||||
server.expect("2 AUTHENTICATE PLAIN");
|
||||
server.output("+");
|
||||
server.expect(ByteString.encodeUtf8("\000" + USERNAME + "\000" + PASSWORD).base64());
|
||||
server.output("* BYE Go away");
|
||||
server.output("2 NO Login Failure");
|
||||
server.closeConnection();
|
||||
ImapConnection imapConnection = startServerAndCreateImapConnection(server);
|
||||
|
||||
try {
|
||||
imapConnection.open();
|
||||
fail("Expected exception");
|
||||
} catch (AuthenticationFailedException e) {
|
||||
//FIXME: improve exception message
|
||||
assertThat(e.getMessage(), containsString("Login Failure"));
|
||||
}
|
||||
|
||||
server.verifyConnectionClosed();
|
||||
|
@ -249,7 +275,7 @@ public class ImapConnectionTest {
|
|||
fail("Expected exception");
|
||||
} catch (AuthenticationFailedException e) {
|
||||
//FIXME: improve exception message
|
||||
assertThat(e.getMessage(), containsString("response: #2# [NO, Who are you?]"));
|
||||
assertThat(e.getMessage(), containsString("Who are you?"));
|
||||
}
|
||||
|
||||
server.verifyConnectionClosed();
|
||||
|
@ -274,6 +300,169 @@ public class ImapConnectionTest {
|
|||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_authXoauthWithSaslIr() throws Exception {
|
||||
settings.setAuthType(AuthType.XOAUTH2);
|
||||
when(oAuth2TokenProvider.getToken("user", OAuth2TokenProvider.OAUTH2_TIMEOUT)).thenReturn("token");
|
||||
MockImapServer server = new MockImapServer();
|
||||
preAuthenticationDialog(server, "SASL-IR AUTH=XOAUTH AUTH=XOAUTH2");
|
||||
server.expect("2 AUTHENTICATE XOAUTH2 " + XOAUTH_STRING);
|
||||
server.output("2 OK Success");
|
||||
simplePostAuthenticationDialog(server);
|
||||
ImapConnection imapConnection = startServerAndCreateImapConnection(server);
|
||||
|
||||
imapConnection.open();
|
||||
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_authXoauthWithSaslIrThrowsExeptionOn401Response() throws Exception {
|
||||
settings.setAuthType(AuthType.XOAUTH2);
|
||||
when(oAuth2TokenProvider.getToken("user", OAuth2TokenProvider.OAUTH2_TIMEOUT))
|
||||
.thenReturn("token").thenReturn("token2");
|
||||
MockImapServer server = new MockImapServer();
|
||||
preAuthenticationDialog(server, "SASL-IR AUTH=XOAUTH AUTH=XOAUTH2");
|
||||
server.expect("2 AUTHENTICATE XOAUTH2 " + XOAUTH_STRING);
|
||||
server.output("+ " + XOAuth2ChallengeParserTest.STATUS_401_RESPONSE);
|
||||
server.expect("");
|
||||
server.output("2 NO SASL authentication failed");
|
||||
ImapConnection imapConnection = startServerAndCreateImapConnection(server);
|
||||
|
||||
try {
|
||||
imapConnection.open();
|
||||
fail();
|
||||
} catch (AuthenticationFailedException e) {
|
||||
assertEquals(
|
||||
"Command: AUTHENTICATE XOAUTH2; response: #2# [NO, SASL authentication failed]",
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_authXoauthWithSaslIrInvalidatesAndRetriesNewTokenOn400Response() throws Exception {
|
||||
settings.setAuthType(AuthType.XOAUTH2);
|
||||
when(oAuth2TokenProvider.getToken("user", OAuth2TokenProvider.OAUTH2_TIMEOUT))
|
||||
.thenReturn("token").thenReturn("token2");
|
||||
MockImapServer server = new MockImapServer();
|
||||
preAuthenticationDialog(server, "SASL-IR AUTH=XOAUTH AUTH=XOAUTH2");
|
||||
server.expect("2 AUTHENTICATE XOAUTH2 " + XOAUTH_STRING);
|
||||
server.output("+ " + XOAuth2ChallengeParserTest.STATUS_400_RESPONSE);
|
||||
server.expect("");
|
||||
server.output("2 NO SASL authentication failed");
|
||||
server.expect("3 AUTHENTICATE XOAUTH2 " + XOAUTH_STRING_RETRY);
|
||||
server.output("3 OK Success");
|
||||
simplePostAuthenticationDialog(server, "4");
|
||||
ImapConnection imapConnection = startServerAndCreateImapConnection(server);
|
||||
|
||||
imapConnection.open();
|
||||
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
InOrder inOrder = inOrder(oAuth2TokenProvider);
|
||||
inOrder.verify(oAuth2TokenProvider).getToken("user", OAuth2TokenProvider.OAUTH2_TIMEOUT);
|
||||
inOrder.verify(oAuth2TokenProvider).invalidateToken("user");
|
||||
inOrder.verify(oAuth2TokenProvider).getToken("user", OAuth2TokenProvider.OAUTH2_TIMEOUT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_authXoauthWithSaslIrInvalidatesAndRetriesNewTokenOnInvalidJsonResponse() throws Exception {
|
||||
settings.setAuthType(AuthType.XOAUTH2);
|
||||
when(oAuth2TokenProvider.getToken("user", OAuth2TokenProvider.OAUTH2_TIMEOUT))
|
||||
.thenReturn("token").thenReturn("token2");
|
||||
MockImapServer server = new MockImapServer();
|
||||
preAuthenticationDialog(server, "SASL-IR AUTH=XOAUTH AUTH=XOAUTH2");
|
||||
server.expect("2 AUTHENTICATE XOAUTH2 " + XOAUTH_STRING);
|
||||
server.output("+ " + XOAuth2ChallengeParserTest.INVALID_RESPONSE);
|
||||
server.expect("");
|
||||
server.output("2 NO SASL authentication failed");
|
||||
server.expect("3 AUTHENTICATE XOAUTH2 " + XOAUTH_STRING_RETRY);
|
||||
server.output("3 OK Success");
|
||||
simplePostAuthenticationDialog(server, "4");
|
||||
ImapConnection imapConnection = startServerAndCreateImapConnection(server);
|
||||
|
||||
imapConnection.open();
|
||||
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
InOrder inOrder = inOrder(oAuth2TokenProvider);
|
||||
inOrder.verify(oAuth2TokenProvider).getToken("user", OAuth2TokenProvider.OAUTH2_TIMEOUT);
|
||||
inOrder.verify(oAuth2TokenProvider).invalidateToken("user");
|
||||
inOrder.verify(oAuth2TokenProvider).getToken("user", OAuth2TokenProvider.OAUTH2_TIMEOUT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_authXoauthWithSaslIrInvalidatesAndRetriesNewTokenOnMissingStatusJsonResponse() throws Exception {
|
||||
settings.setAuthType(AuthType.XOAUTH2);
|
||||
when(oAuth2TokenProvider.getToken("user", OAuth2TokenProvider.OAUTH2_TIMEOUT))
|
||||
.thenReturn("token").thenReturn("token2");
|
||||
MockImapServer server = new MockImapServer();
|
||||
preAuthenticationDialog(server, "SASL-IR AUTH=XOAUTH AUTH=XOAUTH2");
|
||||
server.expect("2 AUTHENTICATE XOAUTH2 " + XOAUTH_STRING);
|
||||
server.output("+ " + XOAuth2ChallengeParserTest.MISSING_STATUS_RESPONSE);
|
||||
server.expect("");
|
||||
server.output("2 NO SASL authentication failed");
|
||||
server.expect("3 AUTHENTICATE XOAUTH2 " + XOAUTH_STRING_RETRY);
|
||||
server.output("3 OK Success");
|
||||
simplePostAuthenticationDialog(server, "4");
|
||||
ImapConnection imapConnection = startServerAndCreateImapConnection(server);
|
||||
|
||||
imapConnection.open();
|
||||
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
InOrder inOrder = inOrder(oAuth2TokenProvider);
|
||||
inOrder.verify(oAuth2TokenProvider).getToken("user", OAuth2TokenProvider.OAUTH2_TIMEOUT);
|
||||
inOrder.verify(oAuth2TokenProvider).invalidateToken("user");
|
||||
inOrder.verify(oAuth2TokenProvider).getToken("user", OAuth2TokenProvider.OAUTH2_TIMEOUT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_authXoauthWithSaslIrWithOldTokenThrowsExceptionIfRetryFails() throws Exception {
|
||||
settings.setAuthType(AuthType.XOAUTH2);
|
||||
when(oAuth2TokenProvider.getToken("user", OAuth2TokenProvider.OAUTH2_TIMEOUT))
|
||||
.thenReturn("token").thenReturn("token2");
|
||||
MockImapServer server = new MockImapServer();
|
||||
preAuthenticationDialog(server, "SASL-IR AUTH=XOAUTH AUTH=XOAUTH2");
|
||||
server.expect("2 AUTHENTICATE XOAUTH2 " + XOAUTH_STRING);
|
||||
server.output("+ r3j3krj3irj3oir3ojo");
|
||||
server.expect("");
|
||||
server.output("2 NO SASL authentication failed");
|
||||
server.expect("3 AUTHENTICATE XOAUTH2 " + XOAUTH_STRING_RETRY);
|
||||
server.output("+ 433ba3a3a");
|
||||
server.expect("");
|
||||
server.output("3 NO SASL authentication failed");
|
||||
simplePostAuthenticationDialog(server);
|
||||
ImapConnection imapConnection = startServerAndCreateImapConnection(server);
|
||||
|
||||
try {
|
||||
imapConnection.open();
|
||||
fail();
|
||||
} catch (AuthenticationFailedException e) {
|
||||
assertEquals(
|
||||
"Command: AUTHENTICATE XOAUTH2; response: #3# [NO, SASL authentication failed]",
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_authXoauthWithSaslIrParsesCapabilities() throws Exception {
|
||||
settings.setAuthType(AuthType.XOAUTH2);
|
||||
when(oAuth2TokenProvider.getToken("user", OAuth2TokenProvider.OAUTH2_TIMEOUT))
|
||||
.thenReturn("token");
|
||||
MockImapServer server = new MockImapServer();
|
||||
preAuthenticationDialog(server, "SASL-IR AUTH=XOAUTH AUTH=XOAUTH2");
|
||||
server.expect("2 AUTHENTICATE XOAUTH2 " + XOAUTH_STRING);
|
||||
server.output("2 OK [CAPABILITY IMAP4REV1 IDLE XM-GM-EXT-1]");
|
||||
simplePostAuthenticationDialog(server);
|
||||
ImapConnection imapConnection = startServerAndCreateImapConnection(server);
|
||||
imapConnection.open();
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
assertTrue(imapConnection.hasCapability("XM-GM-EXT-1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_authExternal() throws Exception {
|
||||
settings.setAuthType(AuthType.EXTERNAL);
|
||||
|
@ -296,7 +485,7 @@ public class ImapConnectionTest {
|
|||
MockImapServer server = new MockImapServer();
|
||||
preAuthenticationDialog(server, "AUTH=EXTERNAL");
|
||||
server.expect("2 AUTHENTICATE EXTERNAL " + ByteString.encodeUtf8(USERNAME).base64());
|
||||
server.output("2 NO");
|
||||
server.output("2 NO Bad certificate");
|
||||
ImapConnection imapConnection = startServerAndCreateImapConnection(server);
|
||||
|
||||
try {
|
||||
|
@ -304,7 +493,7 @@ public class ImapConnectionTest {
|
|||
fail("Expected exception");
|
||||
} catch (CertificateValidationException e) {
|
||||
//FIXME: improve exception message
|
||||
assertThat(e.getMessage(), containsString("response: #2# [NO]"));
|
||||
assertThat(e.getMessage(), containsString("Bad certificate"));
|
||||
}
|
||||
|
||||
server.verifyConnectionClosed();
|
||||
|
@ -348,16 +537,15 @@ public class ImapConnectionTest {
|
|||
public void open_withConnectionError_shouldThrow() throws Exception {
|
||||
settings.setHost("127.1.2.3");
|
||||
settings.setPort(143);
|
||||
ImapConnection imapConnection = createImapConnection(settings, socketFactory, connectivityManager);
|
||||
ImapConnection imapConnection = createImapConnection(
|
||||
settings, socketFactory, connectivityManager, oAuth2TokenProvider);
|
||||
|
||||
try {
|
||||
imapConnection.open();
|
||||
fail("Expected exception");
|
||||
} catch (MessagingException e) {
|
||||
//FIXME: Throw ConnectException
|
||||
assertEquals("Cannot connect to host", e.getMessage());
|
||||
assertNotNull(e.getCause());
|
||||
assertEquals(ConnectException.class, e.getCause().getClass());
|
||||
assertTrue(e.getCause() instanceof IOException);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -365,7 +553,8 @@ public class ImapConnectionTest {
|
|||
public void open_withInvalidHostname_shouldThrow() throws Exception {
|
||||
settings.setHost("host name");
|
||||
settings.setPort(143);
|
||||
ImapConnection imapConnection = createImapConnection(settings, socketFactory, connectivityManager);
|
||||
ImapConnection imapConnection = createImapConnection(
|
||||
settings, socketFactory, connectivityManager, oAuth2TokenProvider);
|
||||
|
||||
try {
|
||||
imapConnection.open();
|
||||
|
@ -434,7 +623,7 @@ public class ImapConnectionTest {
|
|||
imapConnection.open();
|
||||
fail("Expected exception");
|
||||
} catch (NegativeImapResponseException e) {
|
||||
assertThat(e.getMessage(), containsString("response: #2# [NO]"));
|
||||
assertEquals(e.getMessage(), "Command: STARTTLS; response: #2# [NO]");
|
||||
}
|
||||
|
||||
server.verifyConnectionClosed();
|
||||
|
@ -533,7 +722,8 @@ public class ImapConnectionTest {
|
|||
|
||||
@Test
|
||||
public void isConnected_withoutPreviousOpen_shouldReturnFalse() throws Exception {
|
||||
ImapConnection imapConnection = createImapConnection(settings, socketFactory, connectivityManager);
|
||||
ImapConnection imapConnection = createImapConnection(
|
||||
settings, socketFactory, connectivityManager, oAuth2TokenProvider);
|
||||
|
||||
boolean result = imapConnection.isConnected();
|
||||
|
||||
|
@ -569,7 +759,8 @@ public class ImapConnectionTest {
|
|||
|
||||
@Test
|
||||
public void close_withoutOpen_shouldNotThrow() throws Exception {
|
||||
ImapConnection imapConnection = createImapConnection(settings, socketFactory, connectivityManager);
|
||||
ImapConnection imapConnection = createImapConnection(
|
||||
settings, socketFactory, connectivityManager, oAuth2TokenProvider);
|
||||
|
||||
imapConnection.close();
|
||||
}
|
||||
|
@ -631,16 +822,16 @@ public class ImapConnectionTest {
|
|||
}
|
||||
|
||||
private ImapConnection createImapConnection(ImapSettings settings, TrustedSocketFactory socketFactory,
|
||||
ConnectivityManager connectivityManager) {
|
||||
return new ImapConnection(settings, socketFactory, connectivityManager, SOCKET_CONNECT_TIMEOUT,
|
||||
SOCKET_READ_TIMEOUT);
|
||||
ConnectivityManager connectivityManager, OAuth2TokenProvider oAuth2TokenProvider) {
|
||||
return new ImapConnection(settings, socketFactory, connectivityManager, oAuth2TokenProvider,
|
||||
SOCKET_CONNECT_TIMEOUT, SOCKET_READ_TIMEOUT);
|
||||
}
|
||||
|
||||
private ImapConnection startServerAndCreateImapConnection(MockImapServer server) throws IOException {
|
||||
server.start();
|
||||
settings.setHost(server.getHost());
|
||||
settings.setPort(server.getPort());
|
||||
return createImapConnection(settings, socketFactory, connectivityManager);
|
||||
return createImapConnection(settings, socketFactory, connectivityManager, oAuth2TokenProvider);
|
||||
}
|
||||
|
||||
private ImapConnection simpleOpen(MockImapServer server) throws Exception {
|
||||
|
@ -690,41 +881,4 @@ public class ImapConnectionTest {
|
|||
server.expect("2 LOGIN \"" + USERNAME + "\" \"" + PASSWORD + "\"");
|
||||
server.output("2 OK LOGIN completed");
|
||||
}
|
||||
|
||||
private static class TestTrustedSocketFactory implements TrustedSocketFactory {
|
||||
@Override
|
||||
public Socket createSocket(Socket socket, String host, int port, String clientCertificateAlias)
|
||||
throws NoSuchAlgorithmException, KeyManagementException, MessagingException, IOException {
|
||||
|
||||
TrustManager[] trustManagers = new TrustManager[] { new VeryTrustingTrustManager() };
|
||||
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null, trustManagers, null);
|
||||
|
||||
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
|
||||
return sslSocketFactory.createSocket(
|
||||
socket,
|
||||
socket.getInetAddress().getHostAddress(),
|
||||
socket.getPort(),
|
||||
true);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("TrustAllX509TrustManager")
|
||||
private static class VeryTrustingTrustManager implements X509TrustManager {
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||
// Accept all certificates
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||
// Accept all certificates
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return new X509Certificate[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import java.util.Map;
|
|||
import java.util.Set;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import com.fsck.k9.mail.Body;
|
||||
import com.fsck.k9.mail.FetchProfile;
|
||||
import com.fsck.k9.mail.FetchProfile.Item;
|
||||
import com.fsck.k9.mail.Flag;
|
||||
|
@ -20,11 +21,19 @@ import com.fsck.k9.mail.Message;
|
|||
import com.fsck.k9.mail.MessageRetrievalListener;
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
import com.fsck.k9.mail.Part;
|
||||
import com.fsck.k9.mail.internet.BinaryTempFileBody;
|
||||
import com.fsck.k9.mail.internet.MimeHeader;
|
||||
import com.fsck.k9.mail.store.StoreConfig;
|
||||
import okio.Buffer;
|
||||
import org.apache.james.mime4j.util.MimeUtil;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import static com.fsck.k9.mail.Folder.OPEN_MODE_RO;
|
||||
|
@ -49,7 +58,6 @@ import static org.mockito.Mockito.when;
|
|||
import static org.mockito.internal.util.collections.Sets.newSet;
|
||||
|
||||
|
||||
//TODO: Increase test coverage e.g. for fetch() and fetchPart()
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(manifest = Config.NONE, sdk = 21)
|
||||
public class ImapFolderTest {
|
||||
|
@ -57,9 +65,9 @@ public class ImapFolderTest {
|
|||
private ImapConnection imapConnection;
|
||||
private StoreConfig storeConfig;
|
||||
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
BinaryTempFileBody.setTempDirectory(RuntimeEnvironment.application.getCacheDir());
|
||||
imapStore = mock(ImapStore.class);
|
||||
storeConfig = mock(StoreConfig.class);
|
||||
when(storeConfig.getInboxFolderName()).thenReturn("INBOX");
|
||||
|
@ -225,9 +233,9 @@ public class ImapFolderTest {
|
|||
ImapFolder imapFolder = createFolder("Folder");
|
||||
when(imapStore.getConnection()).thenReturn(imapConnection);
|
||||
|
||||
boolean result = imapFolder.exists();
|
||||
boolean folderExists = imapFolder.exists();
|
||||
|
||||
assertTrue(result);
|
||||
assertTrue(folderExists);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -237,9 +245,9 @@ public class ImapFolderTest {
|
|||
doThrow(NegativeImapResponseException.class).when(imapConnection)
|
||||
.executeSimpleCommand("STATUS \"Folder\" (UIDVALIDITY)");
|
||||
|
||||
boolean result = imapFolder.exists();
|
||||
boolean folderExists = imapFolder.exists();
|
||||
|
||||
assertFalse(result);
|
||||
assertFalse(folderExists);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -257,9 +265,9 @@ public class ImapFolderTest {
|
|||
ImapFolder imapFolder = createFolder("Folder");
|
||||
when(imapStore.getConnection()).thenReturn(imapConnection);
|
||||
|
||||
boolean result = imapFolder.create(FolderType.HOLDS_MESSAGES);
|
||||
boolean success = imapFolder.create(FolderType.HOLDS_MESSAGES);
|
||||
|
||||
assertTrue(result);
|
||||
assertTrue(success);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -268,9 +276,9 @@ public class ImapFolderTest {
|
|||
when(imapStore.getConnection()).thenReturn(imapConnection);
|
||||
doThrow(NegativeImapResponseException.class).when(imapConnection).executeSimpleCommand("CREATE \"Folder\"");
|
||||
|
||||
boolean result = imapFolder.create(FolderType.HOLDS_MESSAGES);
|
||||
boolean success = imapFolder.create(FolderType.HOLDS_MESSAGES);
|
||||
|
||||
assertFalse(result);
|
||||
assertFalse(success);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -293,9 +301,9 @@ public class ImapFolderTest {
|
|||
ImapFolder destinationFolder = createFolder("Destination");
|
||||
List<ImapMessage> messages = Collections.emptyList();
|
||||
|
||||
Map<String, String> result = sourceFolder.copyMessages(messages, destinationFolder);
|
||||
Map<String, String> uidMapping = sourceFolder.copyMessages(messages, destinationFolder);
|
||||
|
||||
assertNull(result);
|
||||
assertNull(uidMapping);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -326,10 +334,10 @@ public class ImapFolderTest {
|
|||
when(imapConnection.executeSimpleCommand("UID COPY 1 \"Destination\"")).thenReturn(copyResponses);
|
||||
sourceFolder.open(OPEN_MODE_RW);
|
||||
|
||||
Map<String, String> result = sourceFolder.copyMessages(messages, destinationFolder);
|
||||
Map<String, String> uidMapping = sourceFolder.copyMessages(messages, destinationFolder);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals("101", result.get("1"));
|
||||
assertNotNull(uidMapping);
|
||||
assertEquals("101", uidMapping.get("1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -344,10 +352,10 @@ public class ImapFolderTest {
|
|||
when(imapConnection.executeSimpleCommand("UID COPY 1 \"Destination\"")).thenReturn(copyResponses);
|
||||
sourceFolder.open(OPEN_MODE_RW);
|
||||
|
||||
Map<String, String> result = sourceFolder.moveMessages(messages, destinationFolder);
|
||||
Map<String, String> uidMapping = sourceFolder.moveMessages(messages, destinationFolder);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals("101", result.get("1"));
|
||||
assertNotNull(uidMapping);
|
||||
assertEquals("101", uidMapping.get("1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -373,9 +381,9 @@ public class ImapFolderTest {
|
|||
ImapFolder destinationFolder = createFolder("Destination");
|
||||
List<ImapMessage> messages = Collections.emptyList();
|
||||
|
||||
Map<String, String> result = sourceFolder.moveMessages(messages, destinationFolder);
|
||||
Map<String, String> uidMapping = sourceFolder.moveMessages(messages, destinationFolder);
|
||||
|
||||
assertNull(result);
|
||||
assertNull(uidMapping);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -451,6 +459,21 @@ public class ImapFolderTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getUnreadMessageCount_connectionThrowsIOException_shouldThrowMessagingException() throws Exception {
|
||||
ImapFolder folder = createFolder("Folder");
|
||||
prepareImapFolderForOpen(OPEN_MODE_RW);
|
||||
when(imapConnection.executeSimpleCommand("SEARCH 1:* UNSEEN NOT DELETED")).thenThrow(new IOException());
|
||||
folder.open(OPEN_MODE_RW);
|
||||
|
||||
try {
|
||||
folder.getUnreadMessageCount();
|
||||
fail("Expected exception");
|
||||
} catch (MessagingException e) {
|
||||
assertEquals("IO Error", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getUnreadMessageCount() throws Exception {
|
||||
ImapFolder folder = createFolder("Folder");
|
||||
|
@ -459,9 +482,9 @@ public class ImapFolderTest {
|
|||
when(imapConnection.executeSimpleCommand("SEARCH 1:* UNSEEN NOT DELETED")).thenReturn(imapResponses);
|
||||
folder.open(OPEN_MODE_RW);
|
||||
|
||||
int result = folder.getUnreadMessageCount();
|
||||
int unreadMessageCount = folder.getUnreadMessageCount();
|
||||
|
||||
assertEquals(3, result);
|
||||
assertEquals(3, unreadMessageCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -488,9 +511,9 @@ public class ImapFolderTest {
|
|||
when(imapConnection.executeSimpleCommand("SEARCH 1:* FLAGGED NOT DELETED")).thenReturn(imapResponses);
|
||||
folder.open(OPEN_MODE_RW);
|
||||
|
||||
int result = folder.getFlaggedMessageCount();
|
||||
int flaggedMessageCount = folder.getFlaggedMessageCount();
|
||||
|
||||
assertEquals(4, result);
|
||||
assertEquals(4, flaggedMessageCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -501,9 +524,36 @@ public class ImapFolderTest {
|
|||
when(imapConnection.executeSimpleCommand("UID SEARCH *:*")).thenReturn(imapResponses);
|
||||
folder.open(OPEN_MODE_RW);
|
||||
|
||||
long result = folder.getHighestUid();
|
||||
long highestUid = folder.getHighestUid();
|
||||
|
||||
assertEquals(42L, result);
|
||||
assertEquals(42L, highestUid);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getHighestUid_imapConnectionThrowsNegativesResponse_shouldReturnMinusOne() throws Exception {
|
||||
ImapFolder folder = createFolder("Folder");
|
||||
prepareImapFolderForOpen(OPEN_MODE_RW);
|
||||
doThrow(NegativeImapResponseException.class).when(imapConnection).executeSimpleCommand("UID SEARCH *:*");
|
||||
folder.open(OPEN_MODE_RW);
|
||||
|
||||
long highestUid = folder.getHighestUid();
|
||||
|
||||
assertEquals(-1L, highestUid);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getHighestUid_imapConnectionThrowsIOException_shouldThrowMessagingException() throws Exception {
|
||||
ImapFolder folder = createFolder("Folder");
|
||||
prepareImapFolderForOpen(OPEN_MODE_RW);
|
||||
doThrow(IOException.class).when(imapConnection).executeSimpleCommand("UID SEARCH *:*");
|
||||
folder.open(OPEN_MODE_RW);
|
||||
|
||||
try {
|
||||
folder.getHighestUid();
|
||||
fail("Expected MessagingException");
|
||||
} catch (MessagingException e) {
|
||||
assertEquals("IO Error", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -533,7 +583,8 @@ public class ImapFolderTest {
|
|||
createImapResponse("* SEARCH 47"),
|
||||
createImapResponse("* SEARCH 18")
|
||||
);
|
||||
when(imapConnection.executeSimpleCommand("UID SEARCH 1:10 SINCE 06-Feb-2016 NOT DELETED")).thenReturn(imapResponses);
|
||||
when(imapConnection.executeSimpleCommand("UID SEARCH 1:10 SINCE 06-Feb-2016 NOT DELETED"))
|
||||
.thenReturn(imapResponses);
|
||||
folder.open(OPEN_MODE_RW);
|
||||
|
||||
List<ImapMessage> messages = folder.getMessages(1, 10, new Date(1454719826000L), null);
|
||||
|
@ -551,9 +602,9 @@ public class ImapFolderTest {
|
|||
folder.open(OPEN_MODE_RW);
|
||||
MessageRetrievalListener<ImapMessage> listener = createMessageRetrievalListener();
|
||||
|
||||
List<ImapMessage> result = folder.getMessages(1, 10, null, listener);
|
||||
List<ImapMessage> messages = folder.getMessages(1, 10, null, listener);
|
||||
|
||||
ImapMessage message = result.get(0);
|
||||
ImapMessage message = messages.get(0);
|
||||
verify(listener).messageStarted("99", 0, 1);
|
||||
verify(listener).messageFinished(message, 0, 1);
|
||||
verifyNoMoreInteractions(listener);
|
||||
|
@ -648,9 +699,9 @@ public class ImapFolderTest {
|
|||
folder.open(OPEN_MODE_RW);
|
||||
MessageRetrievalListener<ImapMessage> listener = createMessageRetrievalListener();
|
||||
|
||||
List<ImapMessage> result = folder.getMessages(singletonList(1L), true, listener);
|
||||
List<ImapMessage> messages = folder.getMessages(singletonList(1L), true, listener);
|
||||
|
||||
ImapMessage message = result.get(0);
|
||||
ImapMessage message = messages.get(0);
|
||||
verify(listener).messageStarted("99", 0, 1);
|
||||
verify(listener).messageFinished(message, 0, 1);
|
||||
verifyNoMoreInteractions(listener);
|
||||
|
@ -708,9 +759,9 @@ public class ImapFolderTest {
|
|||
when(imapConnection.executeSimpleCommand("SEARCH 1:9 NOT DELETED")).thenReturn(imapResponses);
|
||||
folder.open(OPEN_MODE_RW);
|
||||
|
||||
boolean result = folder.areMoreMessagesAvailable(10, null);
|
||||
boolean areMoreMessagesAvailable = folder.areMoreMessagesAvailable(10, null);
|
||||
|
||||
assertTrue(result);
|
||||
assertTrue(areMoreMessagesAvailable);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -719,9 +770,22 @@ public class ImapFolderTest {
|
|||
prepareImapFolderForOpen(OPEN_MODE_RW);
|
||||
folder.open(OPEN_MODE_RW);
|
||||
|
||||
boolean result = folder.areMoreMessagesAvailable(600, null);
|
||||
boolean areMoreMessagesAvailable = folder.areMoreMessagesAvailable(600, null);
|
||||
|
||||
assertFalse(result);
|
||||
assertFalse(areMoreMessagesAvailable);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void areMoreMessagesAvailable_withIndexOfOne_shouldReturnFalseWithoutPerformingSearch() throws Exception {
|
||||
ImapFolder folder = createFolder("Folder");
|
||||
prepareImapFolderForOpen(OPEN_MODE_RW);
|
||||
folder.open(OPEN_MODE_RW);
|
||||
|
||||
boolean areMoreMessagesAvailable = folder.areMoreMessagesAvailable(1, null);
|
||||
|
||||
assertFalse(areMoreMessagesAvailable);
|
||||
//SELECT during OPEN and no more
|
||||
verify(imapConnection, times(1)).executeSimpleCommand(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -891,6 +955,25 @@ public class ImapFolderTest {
|
|||
verify(imapConnection).sendCommand("UID FETCH 1 (UID BODY.PEEK[1.1])", false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fetchPart_withTextSection_shouldProcessImapResponses() throws Exception {
|
||||
ImapFolder folder = createFolder("Folder");
|
||||
prepareImapFolderForOpen(OPEN_MODE_RO);
|
||||
folder.open(OPEN_MODE_RO);
|
||||
ImapMessage message = createImapMessage("1");
|
||||
Part part = createPlainTextPart("1.1");
|
||||
setupSingleFetchResponseToCallback();
|
||||
|
||||
folder.fetchPart(message, part, null);
|
||||
|
||||
ArgumentCaptor<Body> bodyArgumentCaptor = ArgumentCaptor.forClass(Body.class);
|
||||
verify(part).setBody(bodyArgumentCaptor.capture());
|
||||
Body body = bodyArgumentCaptor.getValue();
|
||||
Buffer buffer = new Buffer();
|
||||
body.writeTo(buffer.outputStream());
|
||||
assertEquals("text", buffer.readUtf8());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void appendMessages_shouldIssueRespectiveCommand() throws Exception {
|
||||
ImapFolder folder = createFolder("Folder");
|
||||
|
@ -910,9 +993,9 @@ public class ImapFolderTest {
|
|||
ImapMessage message = createImapMessage("2");
|
||||
when(message.getHeader("Message-ID")).thenReturn(new String[0]);
|
||||
|
||||
String result = folder.getUidFromMessageId(message);
|
||||
String uid = folder.getUidFromMessageId(message);
|
||||
|
||||
assertNull(result);
|
||||
assertNull(uid);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -938,9 +1021,9 @@ public class ImapFolderTest {
|
|||
when(imapConnection.executeSimpleCommand("UID SEARCH HEADER MESSAGE-ID \"<00000000.0000000@example.org>\""))
|
||||
.thenReturn(singletonList(createImapResponse("* SEARCH 23")));
|
||||
|
||||
String result = folder.getUidFromMessageId(message);
|
||||
String uid = folder.getUidFromMessageId(message);
|
||||
|
||||
assertEquals("23", result);
|
||||
assertEquals("23", uid);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -969,9 +1052,9 @@ public class ImapFolderTest {
|
|||
prepareImapFolderForOpen(OPEN_MODE_RW);
|
||||
ImapMessage message = createImapMessage("2");
|
||||
|
||||
String result = folder.getNewPushState("uidNext=2", message);
|
||||
String newPushState = folder.getNewPushState("uidNext=2", message);
|
||||
|
||||
assertEquals("uidNext=3", result);
|
||||
assertEquals("uidNext=3", newPushState);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -980,9 +1063,9 @@ public class ImapFolderTest {
|
|||
prepareImapFolderForOpen(OPEN_MODE_RW);
|
||||
ImapMessage message = createImapMessage("1");
|
||||
|
||||
String result = folder.getNewPushState("uidNext=2", message);
|
||||
String newPushState = folder.getNewPushState("uidNext=2", message);
|
||||
|
||||
assertNull(result);
|
||||
assertNull(newPushState);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -1022,6 +1105,66 @@ public class ImapFolderTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test(expected = Error.class)
|
||||
public void delete_notImplemented() throws Exception {
|
||||
ImapFolder folder = createFolder("Folder");
|
||||
|
||||
folder.delete(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMessageByUid_returnsNewImapMessageWithUidInFolder() throws Exception {
|
||||
ImapFolder folder = createFolder("Folder");
|
||||
|
||||
ImapMessage message = folder.getMessage("uid");
|
||||
|
||||
assertEquals("uid", message.getUid());
|
||||
assertEquals(folder, message.getFolder());
|
||||
}
|
||||
|
||||
private Part createPlainTextPart(String serverExtra) {
|
||||
Part part = createPart(serverExtra);
|
||||
when(part.getHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING)).thenReturn(
|
||||
new String[] { MimeUtil.ENC_7BIT }
|
||||
);
|
||||
when(part.getHeader(MimeHeader.HEADER_CONTENT_TYPE)).thenReturn(
|
||||
new String[] { "text/plain" }
|
||||
);
|
||||
return part;
|
||||
}
|
||||
|
||||
private void setupSingleFetchResponseToCallback() throws IOException {
|
||||
when(imapConnection.readResponse(any(ImapResponseCallback.class)))
|
||||
.thenAnswer(new Answer<ImapResponse>() {
|
||||
@Override
|
||||
public ImapResponse answer(InvocationOnMock invocation) throws Throwable {
|
||||
ImapResponseCallback callback = (ImapResponseCallback) invocation.getArguments()[0];
|
||||
return buildImapFetchResponse(callback);
|
||||
}
|
||||
})
|
||||
.thenAnswer(new Answer<ImapResponse>() {
|
||||
@Override
|
||||
public ImapResponse answer(InvocationOnMock invocation) throws Throwable {
|
||||
ImapResponseCallback callback = (ImapResponseCallback) invocation.getArguments()[0];
|
||||
return ImapResponse.newTaggedResponse(callback, "TAG");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ImapResponse buildImapFetchResponse(ImapResponseCallback callback) {
|
||||
ImapResponse response = ImapResponse.newContinuationRequest(callback);
|
||||
response.add("1");
|
||||
response.add("FETCH");
|
||||
ImapList fetchList = new ImapList();
|
||||
fetchList.add("UID");
|
||||
fetchList.add("1");
|
||||
fetchList.add("BODY");
|
||||
fetchList.add("1.1");
|
||||
fetchList.add("text");
|
||||
response.add(fetchList);
|
||||
return response;
|
||||
}
|
||||
|
||||
private Set<String> extractMessageUids(List<ImapMessage> messages) {
|
||||
Set<String> result = new HashSet<>();
|
||||
for (Message message : messages) {
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
package com.fsck.k9.mail.store.imap;
|
||||
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Calendar;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
@ -11,33 +14,129 @@ import static org.junit.Assert.assertTrue;
|
|||
import static org.junit.Assert.fail;
|
||||
|
||||
public class ImapListTest {
|
||||
@Test public void testImapListMethods() throws IOException {
|
||||
|
||||
private ImapList buildSampleList() {
|
||||
ImapList list = new ImapList();
|
||||
list.add("ONE");
|
||||
list.add("TWO");
|
||||
list.add("THREE");
|
||||
return list;
|
||||
}
|
||||
|
||||
@Test public void containsKey_returnsTrueForKeys() throws IOException {
|
||||
ImapList list = buildSampleList();
|
||||
|
||||
assertTrue(list.containsKey("ONE"));
|
||||
assertTrue(list.containsKey("TWO"));
|
||||
assertFalse(list.containsKey("THREE"));
|
||||
assertFalse(list.containsKey("nonexistent"));
|
||||
}
|
||||
|
||||
@Test public void containsKey_returnsFalseForStringThatCantBeKey() throws IOException {
|
||||
ImapList list = buildSampleList();
|
||||
|
||||
assertFalse(list.containsKey("THREE"));
|
||||
}
|
||||
|
||||
@Test public void containsKey_returnsFalseForStringNotInList() throws IOException {
|
||||
ImapList list = buildSampleList();
|
||||
|
||||
assertFalse(list.containsKey("nonexistent"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getKeyedValue_providesCorrespondingValues() {
|
||||
ImapList list = buildSampleList();
|
||||
|
||||
assertEquals("TWO", list.getKeyedValue("ONE"));
|
||||
assertEquals("THREE", list.getKeyedValue("TWO"));
|
||||
assertNull(list.getKeyedValue("THREE"));
|
||||
assertNull(list.getKeyedValue("nonexistent"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getKeyIndex_providesIndexForKeys() {
|
||||
ImapList list = buildSampleList();
|
||||
|
||||
assertEquals(0, list.getKeyIndex("ONE"));
|
||||
assertEquals(1, list.getKeyIndex("TWO"));
|
||||
}
|
||||
|
||||
try {
|
||||
list.getKeyIndex("THREE");
|
||||
fail("IllegalArgumentException should have been thrown");
|
||||
} catch (IllegalArgumentException e) { /* do nothing */ }
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void getKeyIndex_throwsExceptionForValue() {
|
||||
ImapList list = buildSampleList();
|
||||
|
||||
try {
|
||||
list.getKeyIndex("nonexistent");
|
||||
fail("IllegalArgumentException should have been thrown");
|
||||
} catch (IllegalArgumentException e) { /* do nothing */ }
|
||||
list.getKeyIndex("THREE");
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void getKeyIndex_throwsExceptionForNonExistantKey() {
|
||||
ImapList list = buildSampleList();
|
||||
|
||||
list.getKeyIndex("nonexistent");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getDate_returnsCorrectDateForValidString() throws MessagingException {
|
||||
ImapList list = new ImapList();
|
||||
list.add("INTERNALDATE");
|
||||
list.add("10-Mar-2000 12:02:01 GMT");
|
||||
|
||||
Calendar c = Calendar.getInstance();
|
||||
c.setTime(list.getDate(1));
|
||||
|
||||
assertEquals(2000, c.get(Calendar.YEAR));
|
||||
assertEquals(Calendar.MARCH, c.get(Calendar.MONTH));
|
||||
assertEquals(10, c.get(Calendar.DAY_OF_MONTH));
|
||||
}
|
||||
|
||||
@Test(expected = MessagingException.class)
|
||||
public void getDate_throwsExceptionForInvalidDate() throws MessagingException {
|
||||
ImapList list = new ImapList();
|
||||
list.add("INTERNALDATE");
|
||||
list.add("InvalidDate");
|
||||
|
||||
list.getDate(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getDate_returnsNullForNIL() throws MessagingException {
|
||||
ImapList list = new ImapList();
|
||||
list.add("INTERNALDATE");
|
||||
list.add("NIL");
|
||||
|
||||
assertNull(list.getDate(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getKeyedDate_returnsCorrectDateForValidString() throws MessagingException {
|
||||
ImapList list = new ImapList();
|
||||
list.add("INTERNALDATE");
|
||||
list.add("10-Mar-2000 12:02:01 GMT");
|
||||
|
||||
Calendar c = Calendar.getInstance();
|
||||
c.setTime(list.getKeyedDate("INTERNALDATE"));
|
||||
|
||||
assertEquals(2000, c.get(Calendar.YEAR));
|
||||
assertEquals(Calendar.MARCH, c.get(Calendar.MONTH));
|
||||
assertEquals(10, c.get(Calendar.DAY_OF_MONTH));
|
||||
}
|
||||
|
||||
@Test(expected = MessagingException.class)
|
||||
public void getKeyedDate_throwsExceptionForInvalidDate() throws MessagingException {
|
||||
ImapList list = new ImapList();
|
||||
list.add("INTERNALDATE");
|
||||
list.add("InvalidDate");
|
||||
|
||||
list.getKeyedDate("INTERNALDATE");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getKeyedDate_returnsNullForNIL() throws MessagingException {
|
||||
ImapList list = new ImapList();
|
||||
list.add("INTERNALDATE");
|
||||
list.add("NIL");
|
||||
|
||||
assertNull(list.getKeyedDate("INTERNALDATE"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,8 @@ import static org.junit.Assert.fail;
|
|||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(manifest = Config.NONE, sdk = 21)
|
||||
public class ImapResponseParserTest {
|
||||
private PeekableInputStream peekableInputStream;
|
||||
|
||||
|
||||
@Test
|
||||
public void testSimpleOkResponse() throws IOException {
|
||||
|
@ -84,7 +86,8 @@ public class ImapResponseParserTest {
|
|||
|
||||
@Test
|
||||
public void testReadStatusResponseWithOKResponse() throws Exception {
|
||||
ImapResponseParser parser = createParser("* COMMAND BAR\tBAZ\r\nTAG OK COMMAND completed\r\n");
|
||||
ImapResponseParser parser = createParser("* COMMAND BAR\tBAZ\r\n" +
|
||||
"TAG OK COMMAND completed\r\n");
|
||||
|
||||
List<ImapResponse> responses = parser.readStatusResponse("TAG", null, null, null);
|
||||
|
||||
|
@ -93,6 +96,19 @@ public class ImapResponseParserTest {
|
|||
assertEquals(asList("OK", "COMMAND completed"), responses.get(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadStatusResponseUntaggedHandlerGetsUntaggedOnly() throws Exception {
|
||||
ImapResponseParser parser = createParser(
|
||||
"* UNTAGGED\r\n" +
|
||||
"A2 OK COMMAND completed\r\n");
|
||||
TestUntaggedHandler untaggedHandler = new TestUntaggedHandler();
|
||||
|
||||
parser.readStatusResponse("A2", null, null, untaggedHandler);
|
||||
|
||||
assertEquals(1, untaggedHandler.responses.size());
|
||||
assertEquals(asList("UNTAGGED"), untaggedHandler.responses.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadStatusResponseSkippingWrongTag() throws Exception {
|
||||
ImapResponseParser parser = createParser("* UNTAGGED\r\n" +
|
||||
|
@ -113,6 +129,23 @@ public class ImapResponseParserTest {
|
|||
assertEquals(responses.get(1), untaggedHandler.responses.get(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadStatusResponseUntaggedHandlerStillCalledOnNegativeReply() throws Exception {
|
||||
ImapResponseParser parser = createParser(
|
||||
"+ text\r\n" +
|
||||
"A2 NO Bad response\r\n");
|
||||
TestUntaggedHandler untaggedHandler = new TestUntaggedHandler();
|
||||
|
||||
try {
|
||||
List<ImapResponse> responses = parser.readStatusResponse("A2", null, null, untaggedHandler);
|
||||
} catch (NegativeImapResponseException e) {
|
||||
}
|
||||
|
||||
assertEquals(1, untaggedHandler.responses.size());
|
||||
assertEquals(asList("text"), untaggedHandler.responses.get(0));
|
||||
|
||||
}
|
||||
|
||||
@Test(expected = NegativeImapResponseException.class)
|
||||
public void testReadStatusResponseWithErrorResponse() throws Exception {
|
||||
ImapResponseParser parser = createParser("* COMMAND BAR BAZ\r\nTAG ERROR COMMAND errored\r\n");
|
||||
|
@ -200,7 +233,7 @@ public class ImapResponseParserTest {
|
|||
@Test
|
||||
public void testParseLiteralWithConsumingCallbackReturningNull() throws Exception {
|
||||
ImapResponseParser parser = createParser("* {4}\r\ntest\r\n");
|
||||
TestImapResponseCallback callback = new TestImapResponseCallback(4, "cheeseburger");
|
||||
TestImapResponseCallback callback = TestImapResponseCallback.readBytesAndReturn(4, "cheeseburger");
|
||||
|
||||
ImapResponse response = parser.readResponse(callback);
|
||||
|
||||
|
@ -211,37 +244,88 @@ public class ImapResponseParserTest {
|
|||
@Test
|
||||
public void testParseLiteralWithNonConsumingCallbackReturningNull() throws Exception {
|
||||
ImapResponseParser parser = createParser("* {4}\r\ntest\r\n");
|
||||
TestImapResponseCallback callback = new TestImapResponseCallback(0, null);
|
||||
TestImapResponseCallback callback = TestImapResponseCallback.readBytesAndReturn(0, null);
|
||||
|
||||
ImapResponse response = parser.readResponse(callback);
|
||||
|
||||
assertEquals(1, response.size());
|
||||
assertEquals("test", response.getString(0));
|
||||
assertTrue(callback.foundLiteralCalled);
|
||||
assertAllInputConsumed();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readResponse_withPartlyConsumingCallbackReturningNull_shouldThrow() throws Exception {
|
||||
ImapResponseParser parser = createParser("* {4}\r\ntest\r\n");
|
||||
TestImapResponseCallback callback = TestImapResponseCallback.readBytesAndReturn(2, null);
|
||||
|
||||
try {
|
||||
parser.readResponse(callback);
|
||||
fail();
|
||||
} catch (AssertionError e) {
|
||||
assertEquals("Callback consumed some data but returned no result", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readResponse_withPartlyConsumingCallbackThatThrows_shouldReadAllDataAndThrow() throws Exception {
|
||||
ImapResponseParser parser = createParser("* {4}\r\ntest\r\n");
|
||||
TestImapResponseCallback callback = TestImapResponseCallback.readBytesAndThrow(2);
|
||||
|
||||
try {
|
||||
parser.readResponse(callback);
|
||||
fail();
|
||||
} catch (ImapResponseParserException e) {
|
||||
assertEquals("readResponse(): Exception in callback method", e.getMessage());
|
||||
assertEquals(ImapResponseParserTestException.class, e.getCause().getClass());
|
||||
}
|
||||
|
||||
assertAllInputConsumed();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readResponse_withCallbackThatThrowsRepeatedly_shouldConsumeAllInputAndThrowFirstException()
|
||||
throws Exception {
|
||||
ImapResponseParser parser = createParser("* {3}\r\none {3}\r\ntwo\r\n");
|
||||
TestImapResponseCallback callback = TestImapResponseCallback.readBytesAndThrow(3);
|
||||
|
||||
try {
|
||||
parser.readResponse(callback);
|
||||
fail();
|
||||
} catch (ImapResponseParserException e) {
|
||||
assertEquals("readResponse(): Exception in callback method", e.getMessage());
|
||||
assertEquals(ImapResponseParserTestException.class, e.getCause().getClass());
|
||||
assertEquals(0, ((ImapResponseParserTestException) e.getCause()).instanceNumber);
|
||||
}
|
||||
|
||||
assertAllInputConsumed();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseLiteralWithIncompleteConsumingCallbackReturningString() throws Exception {
|
||||
ImapResponseParser parser = createParser("* {4}\r\ntest\r\n");
|
||||
TestImapResponseCallback callback = new TestImapResponseCallback(2, "ninja");
|
||||
TestImapResponseCallback callback = TestImapResponseCallback.readBytesAndReturn(2, "ninja");
|
||||
|
||||
ImapResponse response = parser.readResponse(callback);
|
||||
|
||||
assertEquals(1, response.size());
|
||||
assertEquals("ninja", response.getString(0));
|
||||
assertAllInputConsumed();
|
||||
}
|
||||
|
||||
@Test(expected = RuntimeException.class)
|
||||
@Test
|
||||
public void testParseLiteralWithThrowingCallback() throws Exception {
|
||||
ImapResponseParser parser = createParser("* {4}\r\ntest\r\n");
|
||||
ImapResponseCallback callback = new ImapResponseCallback() {
|
||||
@Override
|
||||
public Object foundLiteral(ImapResponse response, FixedLengthInputStream literal) throws Exception {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
};
|
||||
ImapResponseCallback callback = TestImapResponseCallback.readBytesAndThrow(0);
|
||||
|
||||
parser.readResponse(callback);
|
||||
try {
|
||||
parser.readResponse(callback);
|
||||
fail();
|
||||
} catch (ImapResponseParserException e) {
|
||||
assertEquals("readResponse(): Exception in callback method", e.getMessage());
|
||||
}
|
||||
|
||||
assertAllInputConsumed();
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
|
@ -313,6 +397,20 @@ public class ImapResponseParserTest {
|
|||
assertEquals("TAG", responseTwo.getTag());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readResponse_withListResponseContainingNil() throws Exception {
|
||||
ImapResponseParser parser = createParser("* LIST (\\NoInferiors) NIL INBOX\r\n");
|
||||
|
||||
ImapResponse response = parser.readResponse();
|
||||
|
||||
assertEquals(4, response.size());
|
||||
assertEquals("LIST", response.get(0));
|
||||
assertEquals(1, response.getList(1).size());
|
||||
assertEquals("\\NoInferiors", response.getList(1).getString(0));
|
||||
assertEquals(null, response.get(2));
|
||||
assertEquals("INBOX", response.get(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readResponse_withListAsFirstToken_shouldThrow() throws Exception {
|
||||
ImapResponseParser parser = createParser("* [1 2] 3\r\n");
|
||||
|
@ -384,19 +482,34 @@ public class ImapResponseParserTest {
|
|||
|
||||
private ImapResponseParser createParser(String response) {
|
||||
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(response.getBytes());
|
||||
PeekableInputStream peekableInputStream = new PeekableInputStream(byteArrayInputStream);
|
||||
peekableInputStream = new PeekableInputStream(byteArrayInputStream);
|
||||
return new ImapResponseParser(peekableInputStream);
|
||||
}
|
||||
|
||||
private void assertAllInputConsumed() throws IOException {
|
||||
assertEquals(0, peekableInputStream.available());
|
||||
}
|
||||
|
||||
private class TestImapResponseCallback implements ImapResponseCallback {
|
||||
|
||||
static class TestImapResponseCallback implements ImapResponseCallback {
|
||||
private final int readNumberOfBytes;
|
||||
private final Object returnValue;
|
||||
private final boolean throwException;
|
||||
private int exceptionCount = 0;
|
||||
public boolean foundLiteralCalled = false;
|
||||
|
||||
TestImapResponseCallback(int readNumberOfBytes, Object returnValue) {
|
||||
public static TestImapResponseCallback readBytesAndReturn(int readNumberOfBytes, Object returnValue) {
|
||||
return new TestImapResponseCallback(readNumberOfBytes, returnValue, false);
|
||||
}
|
||||
|
||||
public static TestImapResponseCallback readBytesAndThrow(int readNumberOfBytes) {
|
||||
return new TestImapResponseCallback(readNumberOfBytes, null, true);
|
||||
}
|
||||
|
||||
private TestImapResponseCallback(int readNumberOfBytes, Object returnValue, boolean throwException) {
|
||||
this.readNumberOfBytes = readNumberOfBytes;
|
||||
this.returnValue = returnValue;
|
||||
this.throwException = throwException;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -404,17 +517,28 @@ public class ImapResponseParserTest {
|
|||
foundLiteralCalled = true;
|
||||
|
||||
int skipBytes = readNumberOfBytes;
|
||||
long skippedBytes;
|
||||
do {
|
||||
skippedBytes = literal.skip(skipBytes);
|
||||
while (skipBytes > 0) {
|
||||
long skippedBytes = literal.skip(skipBytes);
|
||||
skipBytes -= skippedBytes;
|
||||
} while (skippedBytes > 0);
|
||||
}
|
||||
|
||||
if (throwException) {
|
||||
throw new ImapResponseParserTestException(exceptionCount++);
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
}
|
||||
|
||||
private class TestUntaggedHandler implements UntaggedHandler {
|
||||
static class ImapResponseParserTestException extends RuntimeException {
|
||||
public final int instanceNumber;
|
||||
|
||||
public ImapResponseParserTestException(int instanceNumber) {
|
||||
this.instanceNumber = instanceNumber;
|
||||
}
|
||||
}
|
||||
|
||||
static class TestUntaggedHandler implements UntaggedHandler {
|
||||
public final List<ImapResponse> responses = new ArrayList<ImapResponse>();
|
||||
|
||||
@Override
|
||||
|
|
|
@ -14,6 +14,7 @@ import android.net.ConnectivityManager;
|
|||
|
||||
import com.fsck.k9.mail.Folder;
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
import com.fsck.k9.mail.oauth.OAuth2TokenProvider;
|
||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory;
|
||||
import com.fsck.k9.mail.store.StoreConfig;
|
||||
import org.junit.Before;
|
||||
|
@ -44,8 +45,9 @@ public class ImapStoreTest {
|
|||
storeConfig = createStoreConfig();
|
||||
TrustedSocketFactory trustedSocketFactory = mock(TrustedSocketFactory.class);
|
||||
ConnectivityManager connectivityManager = mock(ConnectivityManager.class);
|
||||
OAuth2TokenProvider oauth2TokenProvider = mock(OAuth2TokenProvider.class);
|
||||
|
||||
imapStore = new TestImapStore(storeConfig, trustedSocketFactory, connectivityManager);
|
||||
imapStore = new TestImapStore(storeConfig, trustedSocketFactory, connectivityManager, oauth2TokenProvider);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -313,8 +315,8 @@ public class ImapStoreTest {
|
|||
private Deque<ImapConnection> imapConnections = new ArrayDeque<>();
|
||||
|
||||
public TestImapStore(StoreConfig storeConfig, TrustedSocketFactory trustedSocketFactory,
|
||||
ConnectivityManager connectivityManager) throws MessagingException {
|
||||
super(storeConfig, trustedSocketFactory, connectivityManager);
|
||||
ConnectivityManager connectivityManager, OAuth2TokenProvider oauth2TokenProvider) throws MessagingException {
|
||||
super(storeConfig, trustedSocketFactory, connectivityManager, oauth2TokenProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -16,9 +16,97 @@ import static org.junit.Assert.assertNull;
|
|||
|
||||
|
||||
public class ImapStoreUriTest {
|
||||
@Test
|
||||
public void testDecodeStoreUriImapNoAuth() {
|
||||
String uri = "imap://user:pass@server/";
|
||||
|
||||
ServerSettings settings = RemoteStore.decodeStoreUri(uri);
|
||||
|
||||
assertEquals(AuthType.PLAIN, settings.authenticationType);
|
||||
assertEquals("user", settings.username);
|
||||
assertEquals("pass", settings.password);
|
||||
assertEquals("server", settings.host);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecodeStoreUriImapNoPassword() {
|
||||
String uri = "imap://user:@server/";
|
||||
|
||||
ServerSettings settings = RemoteStore.decodeStoreUri(uri);
|
||||
|
||||
assertEquals(AuthType.PLAIN, settings.authenticationType);
|
||||
assertEquals("user", settings.username);
|
||||
assertEquals(null, settings.password);
|
||||
assertEquals("server", settings.host);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecodeStoreUriImapPlainNoPassword() {
|
||||
String uri = "imap://PLAIN:user:@server/";
|
||||
|
||||
ServerSettings settings = RemoteStore.decodeStoreUri(uri);
|
||||
|
||||
assertEquals(AuthType.PLAIN, settings.authenticationType);
|
||||
assertEquals("user", settings.username);
|
||||
assertEquals(null, settings.password);
|
||||
assertEquals("server", settings.host);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecodeStoreUriImapExternalAuth() {
|
||||
String uri = "imap://EXTERNAL:user:clientCertAlias@server/";
|
||||
|
||||
ServerSettings settings = RemoteStore.decodeStoreUri(uri);
|
||||
|
||||
assertEquals(AuthType.EXTERNAL, settings.authenticationType);
|
||||
assertEquals("user", settings.username);
|
||||
assertEquals(null, settings.password);
|
||||
assertEquals("clientCertAlias", settings.clientCertificateAlias);
|
||||
assertEquals("server", settings.host);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecodeStoreUriImapXOAuth2() {
|
||||
String uri = "imap://XOAUTH2:user:@server/";
|
||||
|
||||
ServerSettings settings = RemoteStore.decodeStoreUri(uri);
|
||||
|
||||
assertEquals(AuthType.XOAUTH2, settings.authenticationType);
|
||||
assertEquals("user", settings.username);
|
||||
assertEquals(null, settings.password);
|
||||
assertEquals(null, settings.clientCertificateAlias);
|
||||
assertEquals("server", settings.host);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecodeStoreUriImapSSL() {
|
||||
String uri = "imap+tls+://PLAIN:user:pass@server/";
|
||||
ServerSettings settings = RemoteStore.decodeStoreUri(uri);
|
||||
|
||||
assertEquals(ConnectionSecurity.STARTTLS_REQUIRED, settings.connectionSecurity);
|
||||
assertEquals(AuthType.PLAIN, settings.authenticationType);
|
||||
assertEquals("user", settings.username);
|
||||
assertEquals("pass", settings.password);
|
||||
assertEquals("server", settings.host);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecodeStoreUriImapTLS() {
|
||||
String uri = "imap+ssl+://PLAIN:user:pass@server/";
|
||||
|
||||
ServerSettings settings = RemoteStore.decodeStoreUri(uri);
|
||||
|
||||
assertEquals(ConnectionSecurity.SSL_TLS_REQUIRED, settings.connectionSecurity);
|
||||
assertEquals(AuthType.PLAIN, settings.authenticationType);
|
||||
assertEquals("user", settings.username);
|
||||
assertEquals("pass", settings.password);
|
||||
assertEquals("server", settings.host);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecodeStoreUriImapAllExtras() {
|
||||
String uri = "imap://PLAIN:user:pass@server:143/0%7CcustomPathPrefix";
|
||||
|
||||
ServerSettings settings = RemoteStore.decodeStoreUri(uri);
|
||||
|
||||
assertEquals(AuthType.PLAIN, settings.authenticationType);
|
||||
|
@ -33,6 +121,7 @@ public class ImapStoreUriTest {
|
|||
@Test
|
||||
public void testDecodeStoreUriImapNoExtras() {
|
||||
String uri = "imap://PLAIN:user:pass@server:143/";
|
||||
|
||||
ServerSettings settings = RemoteStore.decodeStoreUri(uri);
|
||||
|
||||
assertEquals(AuthType.PLAIN, settings.authenticationType);
|
||||
|
@ -46,6 +135,7 @@ public class ImapStoreUriTest {
|
|||
@Test
|
||||
public void testDecodeStoreUriImapPrefixOnly() {
|
||||
String uri = "imap://PLAIN:user:pass@server:143/customPathPrefix";
|
||||
|
||||
ServerSettings settings = RemoteStore.decodeStoreUri(uri);
|
||||
|
||||
assertEquals(AuthType.PLAIN, settings.authenticationType);
|
||||
|
@ -60,6 +150,7 @@ public class ImapStoreUriTest {
|
|||
@Test
|
||||
public void testDecodeStoreUriImapEmptyPrefix() {
|
||||
String uri = "imap://PLAIN:user:pass@server:143/0%7C";
|
||||
|
||||
ServerSettings settings = RemoteStore.decodeStoreUri(uri);
|
||||
|
||||
assertEquals(AuthType.PLAIN, settings.authenticationType);
|
||||
|
@ -74,6 +165,7 @@ public class ImapStoreUriTest {
|
|||
@Test
|
||||
public void testDecodeStoreUriImapAutodetectAndPrefix() {
|
||||
String uri = "imap://PLAIN:user:pass@server:143/1%7CcustomPathPrefix";
|
||||
|
||||
ServerSettings settings = RemoteStore.decodeStoreUri(uri);
|
||||
|
||||
assertEquals(AuthType.PLAIN, settings.authenticationType);
|
||||
|
@ -90,7 +182,6 @@ public class ImapStoreUriTest {
|
|||
Map<String, String> extra = new HashMap<String, String>();
|
||||
extra.put("autoDetectNamespace", "false");
|
||||
extra.put("pathPrefix", "customPathPrefix");
|
||||
|
||||
ServerSettings settings = new ServerSettings(ServerSettings.Type.IMAP, "server", 143,
|
||||
ConnectionSecurity.NONE, AuthType.PLAIN, "user", "pass", null, extra);
|
||||
|
||||
|
@ -104,7 +195,6 @@ public class ImapStoreUriTest {
|
|||
Map<String, String> extra = new HashMap<String, String>();
|
||||
extra.put("autoDetectNamespace", "false");
|
||||
extra.put("pathPrefix", "");
|
||||
|
||||
ServerSettings settings = new ServerSettings(ServerSettings.Type.IMAP, "server", 143,
|
||||
ConnectionSecurity.NONE, AuthType.PLAIN, "user", "pass", null, extra);
|
||||
|
||||
|
@ -146,6 +236,7 @@ public class ImapStoreUriTest {
|
|||
assertEquals("imap://PLAIN:user%2540doma%253An:p%2540ssw%253Ard%2525@server:143/1%7C", uri);
|
||||
|
||||
ServerSettings outSettings = RemoteStore.decodeStoreUri(uri);
|
||||
|
||||
assertEquals("user@doma:n", outSettings.username);
|
||||
assertEquals("p@ssw:rd%", outSettings.password);
|
||||
}
|
||||
|
|
|
@ -20,15 +20,17 @@ public class ListResponseTest {
|
|||
createImapResponse("* LIST () \"/\" blurdybloop"),
|
||||
createImapResponse("* LIST (\\Noselect) \"/\" foo"),
|
||||
createImapResponse("* LIST () \"/\" foo/bar"),
|
||||
createImapResponse("* LIST (\\NoInferiors) NIL INBOX"),
|
||||
createImapResponse("X OK LIST completed")
|
||||
);
|
||||
|
||||
List<ListResponse> result = ListResponse.parseList(responses);
|
||||
|
||||
assertEquals(3, result.size());
|
||||
assertEquals(4, result.size());
|
||||
assertListResponseEquals(noAttributes(), "/", "blurdybloop", result.get(0));
|
||||
assertListResponseEquals(singletonList("\\Noselect"), "/", "foo", result.get(1));
|
||||
assertListResponseEquals(noAttributes(), "/", "foo/bar", result.get(2));
|
||||
assertListResponseEquals(singletonList("\\NoInferiors"), null, "INBOX", result.get(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -1,121 +1,678 @@
|
|||
package com.fsck.k9.mail.transport;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
|
||||
import com.fsck.k9.mail.AuthType;
|
||||
import com.fsck.k9.mail.AuthenticationFailedException;
|
||||
import com.fsck.k9.mail.CertificateValidationException;
|
||||
import com.fsck.k9.mail.ConnectionSecurity;
|
||||
import com.fsck.k9.mail.Message;
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
import com.fsck.k9.mail.ServerSettings;
|
||||
import com.fsck.k9.mail.ServerSettings.Type;
|
||||
import com.fsck.k9.mail.XOAuth2ChallengeParserTest;
|
||||
import com.fsck.k9.mail.filter.Base64;
|
||||
import com.fsck.k9.mail.helpers.TestMessageBuilder;
|
||||
import com.fsck.k9.mail.helpers.TestTrustedSocketFactory;
|
||||
import com.fsck.k9.mail.internet.MimeMessage;
|
||||
import com.fsck.k9.mail.oauth.OAuth2TokenProvider;
|
||||
import com.fsck.k9.mail.oauth.XOAuth2ChallengeParser;
|
||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory;
|
||||
import com.fsck.k9.mail.store.StoreConfig;
|
||||
import com.fsck.k9.mail.transport.mockServer.MockSmtpServer;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InOrder;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import static junit.framework.Assert.assertTrue;
|
||||
import static junit.framework.Assert.fail;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.anyInt;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(manifest = Config.NONE, sdk = 21)
|
||||
public class SmtpTransportTest {
|
||||
private static final String LOCALHOST_NAME = "localhost";
|
||||
private static final String USERNAME = "user";
|
||||
private static final String PASSWORD = "password";
|
||||
private static final String CLIENT_CERTIFICATE_ALIAS = null;
|
||||
|
||||
@Test
|
||||
public void decodeUri_canDecodeAuthType() {
|
||||
String storeUri = "smtp://user:password:PLAIN@server:123456";
|
||||
|
||||
private TrustedSocketFactory socketFactory;
|
||||
private OAuth2TokenProvider oAuth2TokenProvider;
|
||||
|
||||
ServerSettings result = SmtpTransport.decodeUri(storeUri);
|
||||
|
||||
assertEquals(AuthType.PLAIN, result.authenticationType);
|
||||
|
||||
@Before
|
||||
public void before() throws AuthenticationFailedException {
|
||||
socketFactory = new TestTrustedSocketFactory();
|
||||
oAuth2TokenProvider = mock(OAuth2TokenProvider.class);
|
||||
when(oAuth2TokenProvider.getToken(eq(USERNAME), anyInt()))
|
||||
.thenReturn("oldToken").thenReturn("newToken");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeUri_canDecodeUsername() {
|
||||
String storeUri = "smtp://user:password:PLAIN@server:123456";
|
||||
public void SmtpTransport_withValidTransportUri() throws Exception {
|
||||
StoreConfig storeConfig = createStoreConfigWithTransportUri("smtp://user:password:CRAM_MD5@server:123456");
|
||||
|
||||
ServerSettings result = SmtpTransport.decodeUri(storeUri);
|
||||
new SmtpTransport(storeConfig, socketFactory, oAuth2TokenProvider);
|
||||
}
|
||||
|
||||
assertEquals("user", result.username);
|
||||
@Test(expected = MessagingException.class)
|
||||
public void SmtpTransport_withInvalidTransportUri_shouldThrow() throws Exception {
|
||||
StoreConfig storeConfig = createStoreConfigWithTransportUri("smpt://");
|
||||
|
||||
new SmtpTransport(storeConfig, socketFactory, oAuth2TokenProvider);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeUri_canDecodePassword() {
|
||||
String storeUri = "smtp://user:password:PLAIN@server:123456";
|
||||
public void open_withoutAuthLoginExtension_shouldConnectWithoutAuthentication() throws Exception {
|
||||
MockSmtpServer server = new MockSmtpServer();
|
||||
server.output("220 localhost Simple Mail Transfer Service Ready");
|
||||
server.expect("EHLO localhost");
|
||||
server.output("250-localhost Hello client.localhost");
|
||||
server.output("250 OK");
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransportWithoutPassword(server);
|
||||
|
||||
ServerSettings result = SmtpTransport.decodeUri(storeUri);
|
||||
transport.open();
|
||||
|
||||
assertEquals("password", result.password);
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeUri_canDecodeHost() {
|
||||
String storeUri = "smtp://user:password:PLAIN@server:123456";
|
||||
public void open_withAuthPlainExtension() throws Exception {
|
||||
MockSmtpServer server = new MockSmtpServer();
|
||||
server.output("220 localhost Simple Mail Transfer Service Ready");
|
||||
server.expect("EHLO localhost");
|
||||
server.output("250-localhost Hello client.localhost");
|
||||
server.output("250 AUTH PLAIN LOGIN");
|
||||
server.expect("AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=");
|
||||
server.output("235 2.7.0 Authentication successful");
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.PLAIN, ConnectionSecurity.NONE);
|
||||
|
||||
ServerSettings result = SmtpTransport.decodeUri(storeUri);
|
||||
transport.open();
|
||||
|
||||
assertEquals("server", result.host);
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeUri_canDecodePort() {
|
||||
String storeUri = "smtp://user:password:PLAIN@server:123456";
|
||||
public void open_withAuthLoginExtension() throws Exception {
|
||||
MockSmtpServer server = new MockSmtpServer();
|
||||
server.output("220 localhost Simple Mail Transfer Service Ready");
|
||||
server.expect("EHLO localhost");
|
||||
server.output("250-localhost Hello client.localhost");
|
||||
server.output("250 AUTH LOGIN");
|
||||
server.expect("AUTH LOGIN");
|
||||
server.output("250 OK");
|
||||
server.expect("dXNlcg==");
|
||||
server.output("250 OK");
|
||||
server.expect("cGFzc3dvcmQ=");
|
||||
server.output("235 2.7.0 Authentication successful");
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.PLAIN, ConnectionSecurity.NONE);
|
||||
|
||||
ServerSettings result = SmtpTransport.decodeUri(storeUri);
|
||||
transport.open();
|
||||
|
||||
assertEquals(123456, result.port);
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeUri_canDecodeTLS() {
|
||||
String storeUri = "smtp+tls+://user:password:PLAIN@server:123456";
|
||||
public void open_withoutLoginAndPlainAuthExtensions_shouldThrow() throws Exception {
|
||||
MockSmtpServer server = new MockSmtpServer();
|
||||
server.output("220 localhost Simple Mail Transfer Service Ready");
|
||||
server.expect("EHLO localhost");
|
||||
server.output("250-localhost Hello client.localhost");
|
||||
server.output("250 AUTH");
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.PLAIN, ConnectionSecurity.NONE);
|
||||
|
||||
ServerSettings result = SmtpTransport.decodeUri(storeUri);
|
||||
try {
|
||||
transport.open();
|
||||
fail("Exception expected");
|
||||
} catch (MessagingException e) {
|
||||
assertEquals("Authentication methods SASL PLAIN and LOGIN are unavailable.", e.getMessage());
|
||||
}
|
||||
|
||||
assertEquals(ConnectionSecurity.STARTTLS_REQUIRED, result.connectionSecurity);
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeUri_canDecodeSSL() {
|
||||
String storeUri = "smtp+ssl+://user:password:PLAIN@server:123456";
|
||||
public void open_withCramMd5AuthExtension() throws Exception {
|
||||
MockSmtpServer server = new MockSmtpServer();
|
||||
server.output("220 localhost Simple Mail Transfer Service Ready");
|
||||
server.expect("EHLO localhost");
|
||||
server.output("250-localhost Hello client.localhost");
|
||||
server.output("250 AUTH CRAM-MD5");
|
||||
server.expect("AUTH CRAM-MD5");
|
||||
server.output(Base64.encode("<24609.1047914046@localhost>"));
|
||||
server.expect("dXNlciA3NmYxNWEzZmYwYTNiOGI1NzcxZmNhODZlNTcyMDk2Zg==");
|
||||
server.output("235 2.7.0 Authentication successful");
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.CRAM_MD5, ConnectionSecurity.NONE);
|
||||
|
||||
ServerSettings result = SmtpTransport.decodeUri(storeUri);
|
||||
transport.open();
|
||||
|
||||
assertEquals(ConnectionSecurity.SSL_TLS_REQUIRED, result.connectionSecurity);
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeUri_canDecodeClientCert() {
|
||||
String storeUri = "smtp+ssl+://user:clientCert:EXTERNAL@server:123456";
|
||||
public void open_withoutCramMd5AuthExtension_shouldThrow() throws Exception {
|
||||
MockSmtpServer server = new MockSmtpServer();
|
||||
server.output("220 localhost Simple Mail Transfer Service Ready");
|
||||
server.expect("EHLO localhost");
|
||||
server.output("250-localhost Hello client.localhost");
|
||||
server.output("250 AUTH PLAIN LOGIN");
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.CRAM_MD5, ConnectionSecurity.NONE);
|
||||
|
||||
ServerSettings result = SmtpTransport.decodeUri(storeUri);
|
||||
try {
|
||||
transport.open();
|
||||
fail("Exception expected");
|
||||
} catch (MessagingException e) {
|
||||
assertEquals("Authentication method CRAM-MD5 is unavailable.", e.getMessage());
|
||||
}
|
||||
|
||||
assertEquals("clientCert", result.clientCertificateAlias);
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createUri_canEncodeSmtpSslUri() {
|
||||
public void open_withXoauth2Extension() throws Exception {
|
||||
MockSmtpServer server = new MockSmtpServer();
|
||||
server.output("220 localhost Simple Mail Transfer Service Ready");
|
||||
server.expect("EHLO localhost");
|
||||
server.output("250-localhost Hello client.localhost");
|
||||
server.output("250 AUTH XOAUTH2");
|
||||
server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE=");
|
||||
server.output("235 2.7.0 Authentication successful");
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.XOAUTH2, ConnectionSecurity.NONE);
|
||||
|
||||
transport.open();
|
||||
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_withXoauth2Extension_shouldThrowOn401Response() throws Exception {
|
||||
MockSmtpServer server = new MockSmtpServer();
|
||||
server.output("220 localhost Simple Mail Transfer Service Ready");
|
||||
server.expect("EHLO localhost");
|
||||
server.output("250-localhost Hello client.localhost");
|
||||
server.output("250 AUTH XOAUTH2");
|
||||
server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE=");
|
||||
server.output("334 "+ XOAuth2ChallengeParserTest.STATUS_401_RESPONSE);
|
||||
server.expect("");
|
||||
server.output("535-5.7.1 Username and Password not accepted. Learn more at");
|
||||
server.output("535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68");
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.XOAUTH2, ConnectionSecurity.NONE);
|
||||
|
||||
try {
|
||||
transport.open();
|
||||
fail("Exception expected");
|
||||
} catch (AuthenticationFailedException e) {
|
||||
assertEquals(
|
||||
"Negative SMTP reply: 535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68",
|
||||
e.getMessage());
|
||||
}
|
||||
|
||||
InOrder inOrder = inOrder(oAuth2TokenProvider);
|
||||
inOrder.verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyInt());
|
||||
inOrder.verify(oAuth2TokenProvider).invalidateToken(USERNAME);
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_withXoauth2Extension_shouldInvalidateAndRetryOn400Response() throws Exception {
|
||||
MockSmtpServer server = new MockSmtpServer();
|
||||
server.output("220 localhost Simple Mail Transfer Service Ready");
|
||||
server.expect("EHLO localhost");
|
||||
server.output("250-localhost Hello client.localhost");
|
||||
server.output("250 AUTH XOAUTH2");
|
||||
server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE=");
|
||||
server.output("334 "+ XOAuth2ChallengeParserTest.STATUS_400_RESPONSE);
|
||||
server.expect("");
|
||||
server.output("535-5.7.1 Username and Password not accepted. Learn more at");
|
||||
server.output("535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68");
|
||||
server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG5ld1Rva2VuAQE=");
|
||||
server.output("235 2.7.0 Authentication successful");
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.XOAUTH2, ConnectionSecurity.NONE);
|
||||
|
||||
transport.open();
|
||||
|
||||
InOrder inOrder = inOrder(oAuth2TokenProvider);
|
||||
inOrder.verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyInt());
|
||||
inOrder.verify(oAuth2TokenProvider).invalidateToken(USERNAME);
|
||||
inOrder.verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyInt());
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_withXoauth2Extension_shouldInvalidateAndRetryOnInvalidJsonResponse() throws Exception {
|
||||
MockSmtpServer server = new MockSmtpServer();
|
||||
server.output("220 localhost Simple Mail Transfer Service Ready");
|
||||
server.expect("EHLO localhost");
|
||||
server.output("250-localhost Hello client.localhost");
|
||||
server.output("250 AUTH XOAUTH2");
|
||||
server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE=");
|
||||
server.output("334 "+ XOAuth2ChallengeParserTest.INVALID_RESPONSE);
|
||||
server.expect("");
|
||||
server.output("535-5.7.1 Username and Password not accepted. Learn more at");
|
||||
server.output("535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68");
|
||||
server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG5ld1Rva2VuAQE=");
|
||||
server.output("235 2.7.0 Authentication successful");
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.XOAUTH2, ConnectionSecurity.NONE);
|
||||
|
||||
transport.open();
|
||||
|
||||
InOrder inOrder = inOrder(oAuth2TokenProvider);
|
||||
inOrder.verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyInt());
|
||||
inOrder.verify(oAuth2TokenProvider).invalidateToken(USERNAME);
|
||||
inOrder.verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyInt());
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_withXoauth2Extension_shouldInvalidateAndRetryOnMissingStatusJsonResponse() throws Exception {
|
||||
MockSmtpServer server = new MockSmtpServer();
|
||||
server.output("220 localhost Simple Mail Transfer Service Ready");
|
||||
server.expect("EHLO localhost");
|
||||
server.output("250-localhost Hello client.localhost");
|
||||
server.output("250 AUTH XOAUTH2");
|
||||
server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE=");
|
||||
server.output("334 "+ XOAuth2ChallengeParserTest.MISSING_STATUS_RESPONSE);
|
||||
server.expect("");
|
||||
server.output("535-5.7.1 Username and Password not accepted. Learn more at");
|
||||
server.output("535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68");
|
||||
server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG5ld1Rva2VuAQE=");
|
||||
server.output("235 2.7.0 Authentication successful");
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.XOAUTH2, ConnectionSecurity.NONE);
|
||||
|
||||
transport.open();
|
||||
|
||||
InOrder inOrder = inOrder(oAuth2TokenProvider);
|
||||
inOrder.verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyInt());
|
||||
inOrder.verify(oAuth2TokenProvider).invalidateToken(USERNAME);
|
||||
inOrder.verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyInt());
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_withXoauth2Extension_shouldThrowOnMultipleFailure() throws Exception {
|
||||
MockSmtpServer server = new MockSmtpServer();
|
||||
server.output("220 localhost Simple Mail Transfer Service Ready");
|
||||
server.expect("EHLO localhost");
|
||||
server.output("250-localhost Hello client.localhost");
|
||||
server.output("250 AUTH XOAUTH2");
|
||||
server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE=");
|
||||
server.output("334 " + XOAuth2ChallengeParserTest.STATUS_400_RESPONSE);
|
||||
server.expect("");
|
||||
server.output("535-5.7.1 Username and Password not accepted. Learn more at");
|
||||
server.output("535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68");
|
||||
server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG5ld1Rva2VuAQE=");
|
||||
server.output("334 " + XOAuth2ChallengeParserTest.STATUS_400_RESPONSE);
|
||||
server.expect("");
|
||||
server.output("535-5.7.1 Username and Password not accepted. Learn more at");
|
||||
server.output("535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68");
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.XOAUTH2, ConnectionSecurity.NONE);
|
||||
|
||||
try {
|
||||
transport.open();
|
||||
fail("Exception expected");
|
||||
} catch (AuthenticationFailedException e) {
|
||||
assertEquals(
|
||||
"Negative SMTP reply: 535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68",
|
||||
e.getMessage());
|
||||
}
|
||||
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_withXoauth2Extension_shouldThrowOnFailure_fetchingToken() throws Exception {
|
||||
MockSmtpServer server = new MockSmtpServer();
|
||||
server.output("220 localhost Simple Mail Transfer Service Ready");
|
||||
server.expect("EHLO localhost");
|
||||
server.output("250-localhost Hello client.localhost");
|
||||
server.output("250 AUTH XOAUTH2");
|
||||
when(oAuth2TokenProvider.getToken(anyString(), anyInt())).thenThrow(new AuthenticationFailedException("Failed to fetch token"));
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.XOAUTH2, ConnectionSecurity.NONE);
|
||||
|
||||
try {
|
||||
transport.open();
|
||||
fail("Exception expected");
|
||||
} catch (AuthenticationFailedException e) {
|
||||
assertEquals("Failed to fetch token", e.getMessage());
|
||||
}
|
||||
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void open_withoutXoauth2Extension_shouldThrow() throws Exception {
|
||||
MockSmtpServer server = new MockSmtpServer();
|
||||
server.output("220 localhost Simple Mail Transfer Service Ready");
|
||||
server.expect("EHLO localhost");
|
||||
server.output("250-localhost Hello client.localhost");
|
||||
server.output("250 AUTH PLAIN LOGIN");
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.XOAUTH2, ConnectionSecurity.NONE);
|
||||
|
||||
try {
|
||||
transport.open();
|
||||
fail("Exception expected");
|
||||
} catch (MessagingException e) {
|
||||
assertEquals("Authentication method XOAUTH2 is unavailable.", e.getMessage());
|
||||
}
|
||||
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_withAuthExternalExtension() throws Exception {
|
||||
MockSmtpServer server = new MockSmtpServer();
|
||||
server.output("220 localhost Simple Mail Transfer Service Ready");
|
||||
server.expect("EHLO localhost");
|
||||
server.output("250-localhost Hello client.localhost");
|
||||
server.output("250 AUTH EXTERNAL");
|
||||
server.expect("AUTH EXTERNAL dXNlcg==");
|
||||
server.output("235 2.7.0 Authentication successful");
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.EXTERNAL, ConnectionSecurity.NONE);
|
||||
|
||||
transport.open();
|
||||
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_withoutAuthExternalExtension_shouldThrow() throws Exception {
|
||||
MockSmtpServer server = new MockSmtpServer();
|
||||
server.output("220 localhost Simple Mail Transfer Service Ready");
|
||||
server.expect("EHLO localhost");
|
||||
server.output("250-localhost Hello client.localhost");
|
||||
server.output("250 AUTH");
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.EXTERNAL, ConnectionSecurity.NONE);
|
||||
|
||||
try {
|
||||
transport.open();
|
||||
fail("Exception expected");
|
||||
} catch (CertificateValidationException e) {
|
||||
assertEquals(CertificateValidationException.Reason.MissingCapability, e.getReason());
|
||||
}
|
||||
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_withAutomaticAuthAndNoTransportSecurityAndAuthCramMd5Extension_shouldUseAuthCramMd5()
|
||||
throws Exception {
|
||||
MockSmtpServer server = new MockSmtpServer();
|
||||
server.output("220 localhost Simple Mail Transfer Service Ready");
|
||||
server.expect("EHLO localhost");
|
||||
server.output("250-localhost Hello client.localhost");
|
||||
server.output("250 AUTH CRAM-MD5");
|
||||
server.expect("AUTH CRAM-MD5");
|
||||
server.output(Base64.encode("<24609.1047914046@localhost>"));
|
||||
server.expect("dXNlciA3NmYxNWEzZmYwYTNiOGI1NzcxZmNhODZlNTcyMDk2Zg==");
|
||||
server.output("235 2.7.0 Authentication successful");
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.AUTOMATIC,
|
||||
ConnectionSecurity.NONE);
|
||||
|
||||
transport.open();
|
||||
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_withAutomaticAuthAndNoTransportSecurityAndAuthPlainExtension_shouldThrow() throws Exception {
|
||||
MockSmtpServer server = new MockSmtpServer();
|
||||
server.output("220 localhost Simple Mail Transfer Service Ready");
|
||||
server.expect("EHLO localhost");
|
||||
server.output("250-localhost Hello client.localhost");
|
||||
server.output("250 AUTH PLAIN LOGIN");
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.AUTOMATIC,
|
||||
ConnectionSecurity.NONE);
|
||||
|
||||
try {
|
||||
transport.open();
|
||||
fail("Exception expected");
|
||||
} catch (MessagingException e) {
|
||||
assertEquals("Update your outgoing server authentication setting. AUTOMATIC auth. is unavailable.",
|
||||
e.getMessage());
|
||||
}
|
||||
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_withEhloFailing_shouldTryHelo() throws Exception {
|
||||
MockSmtpServer server = new MockSmtpServer();
|
||||
server.output("220 localhost Simple Mail Transfer Service Ready");
|
||||
server.expect("EHLO localhost");
|
||||
server.output("502 5.5.1, Unrecognized command.");
|
||||
server.expect("HELO localhost");
|
||||
server.output("250 localhost");
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransportWithoutPassword(server);
|
||||
|
||||
transport.open();
|
||||
|
||||
server.verifyConnectionStillOpen();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendMessage_withoutAddressToSendTo_shouldNotOpenConnection() throws Exception {
|
||||
MimeMessage message = new MimeMessage();
|
||||
MockSmtpServer server = createServerAndSetupForPlainAuthentication();
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server);
|
||||
|
||||
transport.sendMessage(message);
|
||||
|
||||
server.verifyConnectionNeverCreated();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendMessage_withSingleRecipient() throws Exception {
|
||||
Message message = getDefaultMessage();
|
||||
MockSmtpServer server = createServerAndSetupForPlainAuthentication();
|
||||
server.expect("MAIL FROM:<user@localhost>");
|
||||
server.output("250 OK");
|
||||
server.expect("RCPT TO:<user2@localhost>");
|
||||
server.output("250 OK");
|
||||
server.expect("DATA");
|
||||
server.output("354 End data with <CR><LF>.<CR><LF>");
|
||||
server.expect("[message data]");
|
||||
server.expect(".");
|
||||
server.output("250 OK: queued as 12345");
|
||||
server.expect("QUIT");
|
||||
server.output("221 BYE");
|
||||
server.closeConnection();
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server);
|
||||
|
||||
transport.sendMessage(message);
|
||||
|
||||
server.verifyConnectionClosed();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendMessage_with8BitEncoding() throws Exception {
|
||||
Message message = getDefaultMessage();
|
||||
MockSmtpServer server = createServerAndSetupForPlainAuthentication("8BITMIME");
|
||||
server.expect("MAIL FROM:<user@localhost> BODY=8BITMIME");
|
||||
server.output("250 OK");
|
||||
server.expect("RCPT TO:<user2@localhost>");
|
||||
server.output("250 OK");
|
||||
server.expect("DATA");
|
||||
server.output("354 End data with <CR><LF>.<CR><LF>");
|
||||
server.expect("[message data]");
|
||||
server.expect(".");
|
||||
server.output("250 OK: queued as 12345");
|
||||
server.expect("QUIT");
|
||||
server.output("221 BYE");
|
||||
server.closeConnection();
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server);
|
||||
|
||||
transport.sendMessage(message);
|
||||
|
||||
server.verifyConnectionClosed();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendMessage_withMessageTooLarge_shouldThrow() throws Exception {
|
||||
Message message = getDefaultMessageBuilder()
|
||||
.setHasAttachments(true)
|
||||
.messageSize(1234L)
|
||||
.build();
|
||||
MockSmtpServer server = createServerAndSetupForPlainAuthentication("SIZE 1000");
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server);
|
||||
|
||||
try {
|
||||
transport.sendMessage(message);
|
||||
fail("Expected message too large error");
|
||||
} catch (MessagingException e) {
|
||||
assertTrue(e.isPermanentFailure());
|
||||
assertEquals("Message too large for server", e.getMessage());
|
||||
}
|
||||
|
||||
//FIXME: Make sure connection was closed
|
||||
//server.verifyConnectionClosed();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendMessage_withNegativeReply_shouldThrow() throws Exception {
|
||||
Message message = getDefaultMessage();
|
||||
MockSmtpServer server = createServerAndSetupForPlainAuthentication();
|
||||
server.expect("MAIL FROM:<user@localhost>");
|
||||
server.output("250 OK");
|
||||
server.expect("RCPT TO:<user2@localhost>");
|
||||
server.output("250 OK");
|
||||
server.expect("DATA");
|
||||
server.output("354 End data with <CR><LF>.<CR><LF>");
|
||||
server.expect("[message data]");
|
||||
server.expect(".");
|
||||
server.output("421 4.7.0 Temporary system problem");
|
||||
server.expect("QUIT");
|
||||
server.output("221 BYE");
|
||||
server.closeConnection();
|
||||
SmtpTransport transport = startServerAndCreateSmtpTransport(server);
|
||||
|
||||
try {
|
||||
transport.sendMessage(message);
|
||||
fail("Expected exception");
|
||||
} catch (SmtpTransport.NegativeSmtpReplyException e) {
|
||||
assertEquals(421, e.getReplyCode());
|
||||
assertEquals("4.7.0 Temporary system problem", e.getReplyText());
|
||||
}
|
||||
|
||||
server.verifyConnectionClosed();
|
||||
server.verifyInteractionCompleted();
|
||||
}
|
||||
|
||||
private SmtpTransport startServerAndCreateSmtpTransport(MockSmtpServer server) throws IOException,
|
||||
MessagingException {
|
||||
return startServerAndCreateSmtpTransport(server, AuthType.PLAIN, ConnectionSecurity.NONE);
|
||||
}
|
||||
|
||||
private SmtpTransport startServerAndCreateSmtpTransportWithoutPassword(MockSmtpServer server) throws IOException,
|
||||
MessagingException {
|
||||
return startServerAndCreateSmtpTransport(server, AuthType.PLAIN, ConnectionSecurity.NONE, null);
|
||||
}
|
||||
|
||||
private SmtpTransport startServerAndCreateSmtpTransport(MockSmtpServer server, AuthType authenticationType,
|
||||
ConnectionSecurity connectionSecurity) throws IOException, MessagingException {
|
||||
return startServerAndCreateSmtpTransport(server, authenticationType, connectionSecurity, PASSWORD);
|
||||
}
|
||||
|
||||
private SmtpTransport startServerAndCreateSmtpTransport(MockSmtpServer server, AuthType authenticationType,
|
||||
ConnectionSecurity connectionSecurity, String password) throws IOException, MessagingException {
|
||||
server.start();
|
||||
|
||||
String host = server.getHost();
|
||||
int port = server.getPort();
|
||||
ServerSettings serverSettings = new ServerSettings(
|
||||
ServerSettings.Type.SMTP, "server", 123456,
|
||||
ConnectionSecurity.SSL_TLS_REQUIRED, AuthType.EXTERNAL,
|
||||
"user", "password", "clientCert");
|
||||
Type.SMTP,
|
||||
host,
|
||||
port,
|
||||
connectionSecurity,
|
||||
authenticationType,
|
||||
USERNAME,
|
||||
password,
|
||||
CLIENT_CERTIFICATE_ALIAS);
|
||||
String uri = SmtpTransport.createUri(serverSettings);
|
||||
StoreConfig storeConfig = createStoreConfigWithTransportUri(uri);
|
||||
|
||||
String result = SmtpTransport.createUri(serverSettings);
|
||||
|
||||
assertEquals("smtp+ssl+://user:clientCert:EXTERNAL@server:123456", result);
|
||||
return new TestSmtpTransport(storeConfig, socketFactory, oAuth2TokenProvider);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createUri_canEncodeSmtpTlsUri() {
|
||||
ServerSettings serverSettings = new ServerSettings(
|
||||
ServerSettings.Type.SMTP, "server", 123456,
|
||||
ConnectionSecurity.STARTTLS_REQUIRED, AuthType.PLAIN,
|
||||
"user", "password", "clientCert");
|
||||
|
||||
String result = SmtpTransport.createUri(serverSettings);
|
||||
|
||||
assertEquals("smtp+tls+://user:password:PLAIN@server:123456", result);
|
||||
private StoreConfig createStoreConfigWithTransportUri(String value) {
|
||||
StoreConfig storeConfig = mock(StoreConfig.class);
|
||||
when(storeConfig.getTransportUri()).thenReturn(value);
|
||||
return storeConfig;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createUri_canEncodeSmtpUri() {
|
||||
ServerSettings serverSettings = new ServerSettings(
|
||||
ServerSettings.Type.SMTP, "server", 123456,
|
||||
ConnectionSecurity.NONE, AuthType.CRAM_MD5,
|
||||
"user", "password", "clientCert");
|
||||
private TestMessageBuilder getDefaultMessageBuilder() {
|
||||
return new TestMessageBuilder()
|
||||
.from("user@localhost")
|
||||
.to("user2@localhost");
|
||||
}
|
||||
|
||||
String result = SmtpTransport.createUri(serverSettings);
|
||||
private Message getDefaultMessage() {
|
||||
return getDefaultMessageBuilder().build();
|
||||
}
|
||||
|
||||
assertEquals("smtp://user:password:CRAM_MD5@server:123456", result);
|
||||
private MockSmtpServer createServerAndSetupForPlainAuthentication(String... extensions) {
|
||||
MockSmtpServer server = new MockSmtpServer();
|
||||
|
||||
server.output("220 localhost Simple Mail Transfer Service Ready");
|
||||
server.expect("EHLO localhost");
|
||||
server.output("250-localhost Hello client.localhost");
|
||||
|
||||
for (String extension : extensions) {
|
||||
server.output("250-" + extension);
|
||||
}
|
||||
|
||||
server.output("250 AUTH LOGIN PLAIN CRAM-MD5");
|
||||
server.expect("AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=");
|
||||
server.output("235 2.7.0 Authentication successful");
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
|
||||
static class TestSmtpTransport extends SmtpTransport {
|
||||
TestSmtpTransport(StoreConfig storeConfig, TrustedSocketFactory trustedSocketFactory, OAuth2TokenProvider oAuth2TokenProvider)
|
||||
throws MessagingException {
|
||||
super(storeConfig, trustedSocketFactory, oAuth2TokenProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getCanonicalHostName(InetAddress localAddress) {
|
||||
return LOCALHOST_NAME;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
package com.fsck.k9.mail.transport;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
import com.fsck.k9.mail.AuthType;
|
||||
import com.fsck.k9.mail.ConnectionSecurity;
|
||||
import com.fsck.k9.mail.ServerSettings;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@SuppressLint("AuthLeak")
|
||||
public class SmtpTransportUriTest {
|
||||
|
||||
@Test
|
||||
public void decodeUri_canDecodeAuthType() {
|
||||
String storeUri = "smtp://user:password:PLAIN@server:123456";
|
||||
|
||||
ServerSettings result = SmtpTransport.decodeUri(storeUri);
|
||||
|
||||
assertEquals(AuthType.PLAIN, result.authenticationType);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeUri_canDecodeUsername() {
|
||||
String storeUri = "smtp://user:password:PLAIN@server:123456";
|
||||
|
||||
ServerSettings result = SmtpTransport.decodeUri(storeUri);
|
||||
|
||||
assertEquals("user", result.username);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeUri_canDecodePassword() {
|
||||
String storeUri = "smtp://user:password:PLAIN@server:123456";
|
||||
|
||||
ServerSettings result = SmtpTransport.decodeUri(storeUri);
|
||||
|
||||
assertEquals("password", result.password);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeUri_canDecodeUsername_withNoAuthType() {
|
||||
String storeUri = "smtp://user:password@server:123456";
|
||||
|
||||
ServerSettings result = SmtpTransport.decodeUri(storeUri);
|
||||
|
||||
assertEquals("user", result.username);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeUri_canDecodeUsername_withNoPasswordOrAuthType() {
|
||||
String storeUri = "smtp://user@server:123456";
|
||||
|
||||
ServerSettings result = SmtpTransport.decodeUri(storeUri);
|
||||
|
||||
assertEquals("user", result.username);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeUri_canDecodeAuthType_withEmptyPassword() {
|
||||
String storeUri = "smtp://user::PLAIN@server:123456";
|
||||
|
||||
ServerSettings result = SmtpTransport.decodeUri(storeUri);
|
||||
|
||||
assertEquals(AuthType.PLAIN, result.authenticationType);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeUri_canDecodeHost() {
|
||||
String storeUri = "smtp://user:password:PLAIN@server:123456";
|
||||
|
||||
ServerSettings result = SmtpTransport.decodeUri(storeUri);
|
||||
|
||||
assertEquals("server", result.host);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeUri_canDecodePort() {
|
||||
String storeUri = "smtp://user:password:PLAIN@server:123456";
|
||||
|
||||
ServerSettings result = SmtpTransport.decodeUri(storeUri);
|
||||
|
||||
assertEquals(123456, result.port);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeUri_canDecodeTLS() {
|
||||
String storeUri = "smtp+tls+://user:password:PLAIN@server:123456";
|
||||
|
||||
ServerSettings result = SmtpTransport.decodeUri(storeUri);
|
||||
|
||||
assertEquals(ConnectionSecurity.STARTTLS_REQUIRED, result.connectionSecurity);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeUri_canDecodeSSL() {
|
||||
String storeUri = "smtp+ssl+://user:password:PLAIN@server:123456";
|
||||
|
||||
ServerSettings result = SmtpTransport.decodeUri(storeUri);
|
||||
|
||||
assertEquals(ConnectionSecurity.SSL_TLS_REQUIRED, result.connectionSecurity);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeUri_canDecodeClientCert() {
|
||||
String storeUri = "smtp+ssl+://user:clientCert:EXTERNAL@server:123456";
|
||||
|
||||
ServerSettings result = SmtpTransport.decodeUri(storeUri);
|
||||
|
||||
assertEquals("clientCert", result.clientCertificateAlias);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void decodeUri_forUnknownSchema_throwsIllegalArgumentException() {
|
||||
String storeUri = "unknown://user:clientCert:EXTERNAL@server:123456";
|
||||
|
||||
ServerSettings result = SmtpTransport.decodeUri(storeUri);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createUri_canEncodeSmtpSslUri() {
|
||||
ServerSettings serverSettings = new ServerSettings(
|
||||
ServerSettings.Type.SMTP, "server", 123456,
|
||||
ConnectionSecurity.SSL_TLS_REQUIRED, AuthType.EXTERNAL,
|
||||
"user", "password", "clientCert");
|
||||
|
||||
String result = SmtpTransport.createUri(serverSettings);
|
||||
|
||||
assertEquals("smtp+ssl+://user:clientCert:EXTERNAL@server:123456", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createUri_canEncodeSmtpTlsUri() {
|
||||
ServerSettings serverSettings = new ServerSettings(
|
||||
ServerSettings.Type.SMTP, "server", 123456,
|
||||
ConnectionSecurity.STARTTLS_REQUIRED, AuthType.PLAIN,
|
||||
"user", "password", "clientCert");
|
||||
|
||||
String result = SmtpTransport.createUri(serverSettings);
|
||||
|
||||
assertEquals("smtp+tls+://user:password:PLAIN@server:123456", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createUri_canEncodeSmtpUri() {
|
||||
ServerSettings serverSettings = new ServerSettings(
|
||||
ServerSettings.Type.SMTP, "server", 123456,
|
||||
ConnectionSecurity.NONE, AuthType.CRAM_MD5,
|
||||
"user", "password", "clientCert");
|
||||
|
||||
String result = SmtpTransport.createUri(serverSettings);
|
||||
|
||||
assertEquals("smtp://user:password:CRAM_MD5@server:123456", result);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,429 @@
|
|||
package com.fsck.k9.mail.transport.mockServer;
|
||||
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.Deque;
|
||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.zip.Inflater;
|
||||
import java.util.zip.InflaterInputStream;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
import com.jcraft.jzlib.JZlib;
|
||||
import com.jcraft.jzlib.ZOutputStream;
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import okio.BufferedSink;
|
||||
import okio.BufferedSource;
|
||||
import okio.Okio;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
public class MockSmtpServer {
|
||||
private static final String KEYSTORE_PASSWORD = "password";
|
||||
private static final String KEYSTORE_RESOURCE = "/keystore.jks";
|
||||
|
||||
private static final byte[] CRLF = { '\r', '\n' };
|
||||
|
||||
|
||||
private final Deque<SmtpInteraction> interactions = new ConcurrentLinkedDeque<>();
|
||||
private final CountDownLatch waitForConnectionClosed = new CountDownLatch(1);
|
||||
private final CountDownLatch waitForAllExpectedCommands = new CountDownLatch(1);
|
||||
private final Logger logger;
|
||||
|
||||
private MockServerThread mockServerThread;
|
||||
private String host;
|
||||
private int port;
|
||||
|
||||
|
||||
public MockSmtpServer() {
|
||||
this(new DefaultLogger());
|
||||
}
|
||||
|
||||
public MockSmtpServer(Logger logger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public void output(String response) {
|
||||
checkServerNotRunning();
|
||||
interactions.add(new CannedResponse(response));
|
||||
}
|
||||
|
||||
public void expect(String command) {
|
||||
checkServerNotRunning();
|
||||
interactions.add(new ExpectedCommand(command));
|
||||
}
|
||||
|
||||
public void closeConnection() {
|
||||
checkServerNotRunning();
|
||||
interactions.add(new CloseConnection());
|
||||
}
|
||||
|
||||
public void start() throws IOException {
|
||||
checkServerNotRunning();
|
||||
|
||||
InetAddress localAddress = InetAddress.getByName(null);
|
||||
ServerSocket serverSocket = new ServerSocket(0, 1, localAddress);
|
||||
InetSocketAddress localSocketAddress = (InetSocketAddress) serverSocket.getLocalSocketAddress();
|
||||
host = localSocketAddress.getHostString();
|
||||
port = serverSocket.getLocalPort();
|
||||
|
||||
mockServerThread = new MockServerThread(serverSocket, interactions, waitForConnectionClosed,
|
||||
waitForAllExpectedCommands, logger);
|
||||
mockServerThread.start();
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
checkServerRunning();
|
||||
|
||||
mockServerThread.shouldStop();
|
||||
waitForMockServerThread();
|
||||
}
|
||||
|
||||
private void waitForMockServerThread() {
|
||||
try {
|
||||
mockServerThread.join(500L);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
checkServerRunning();
|
||||
|
||||
return host;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
checkServerRunning();
|
||||
|
||||
return port;
|
||||
}
|
||||
|
||||
public void waitForInteractionToComplete() {
|
||||
checkServerRunning();
|
||||
|
||||
try {
|
||||
waitForAllExpectedCommands.await(1000L, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public void verifyInteractionCompleted() {
|
||||
shutdown();
|
||||
|
||||
if (!interactions.isEmpty()) {
|
||||
throw new AssertionError("Interactions left: " + interactions.size());
|
||||
}
|
||||
|
||||
UnexpectedCommandException unexpectedCommandException = mockServerThread.getUnexpectedCommandException();
|
||||
if (unexpectedCommandException != null) {
|
||||
throw new AssertionError(unexpectedCommandException.getMessage(), unexpectedCommandException);
|
||||
}
|
||||
}
|
||||
|
||||
public void verifyConnectionNeverCreated() {
|
||||
checkServerRunning();
|
||||
if (mockServerThread.clientConnectionCreated()) {
|
||||
throw new AssertionError("Connection created when it shouldn't have been");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void verifyConnectionStillOpen() {
|
||||
checkServerRunning();
|
||||
|
||||
if (mockServerThread.isClientConnectionClosed()) {
|
||||
throw new AssertionError("Connection closed when it shouldn't be");
|
||||
}
|
||||
}
|
||||
|
||||
public void verifyConnectionClosed() {
|
||||
checkServerRunning();
|
||||
|
||||
try {
|
||||
waitForConnectionClosed.await(300L, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
|
||||
if (!mockServerThread.isClientConnectionClosed()) {
|
||||
throw new AssertionError("Connection open when is shouldn't be");
|
||||
}
|
||||
}
|
||||
|
||||
private void checkServerRunning() {
|
||||
if (mockServerThread == null) {
|
||||
throw new IllegalStateException("Server was never started");
|
||||
}
|
||||
}
|
||||
|
||||
private void checkServerNotRunning() {
|
||||
if (mockServerThread != null) {
|
||||
throw new IllegalStateException("Server was already started");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface Logger {
|
||||
void log(String message);
|
||||
|
||||
void log(String format, Object... args);
|
||||
}
|
||||
|
||||
private interface SmtpInteraction {
|
||||
}
|
||||
|
||||
private static class ExpectedCommand implements SmtpInteraction {
|
||||
private final String command;
|
||||
|
||||
|
||||
public ExpectedCommand(String command) {
|
||||
this.command = command;
|
||||
}
|
||||
|
||||
public String getCommand() {
|
||||
return command;
|
||||
}
|
||||
}
|
||||
|
||||
private static class CannedResponse implements SmtpInteraction {
|
||||
private final String response;
|
||||
|
||||
|
||||
public CannedResponse(String response) {
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
public String getResponse() {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
private static class CloseConnection implements SmtpInteraction {
|
||||
}
|
||||
|
||||
private static class UnexpectedCommandException extends Exception {
|
||||
public UnexpectedCommandException(String expectedCommand, String receivedCommand) {
|
||||
super("Expected <" + expectedCommand + ">, but received <" + receivedCommand + ">");
|
||||
}
|
||||
}
|
||||
|
||||
private static class MockServerThread extends Thread {
|
||||
private final ServerSocket serverSocket;
|
||||
private final Deque<SmtpInteraction> interactions;
|
||||
private final CountDownLatch waitForConnectionClosed;
|
||||
private final CountDownLatch waitForAllExpectedCommands;
|
||||
private final Logger logger;
|
||||
|
||||
private volatile boolean shouldStop = false;
|
||||
private volatile Socket clientSocket;
|
||||
|
||||
private BufferedSource input;
|
||||
private BufferedSink output;
|
||||
private volatile UnexpectedCommandException unexpectedCommandException;
|
||||
|
||||
|
||||
public MockServerThread(ServerSocket serverSocket, Deque<SmtpInteraction> interactions,
|
||||
CountDownLatch waitForConnectionClosed, CountDownLatch waitForAllExpectedCommands, Logger logger) {
|
||||
super("MockSmtpServer");
|
||||
this.serverSocket = serverSocket;
|
||||
this.interactions = interactions;
|
||||
this.waitForConnectionClosed = waitForConnectionClosed;
|
||||
this.waitForAllExpectedCommands = waitForAllExpectedCommands;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
String hostAddress = serverSocket.getInetAddress().getHostAddress();
|
||||
int port = serverSocket.getLocalPort();
|
||||
logger.log("Listening on %s:%d", hostAddress, port);
|
||||
|
||||
Socket socket = null;
|
||||
try {
|
||||
socket = acceptConnectionAndCloseServerSocket();
|
||||
clientSocket = socket;
|
||||
|
||||
String remoteHostAddress = socket.getInetAddress().getHostAddress();
|
||||
int remotePort = socket.getPort();
|
||||
logger.log("Accepted connection from %s:%d", remoteHostAddress, remotePort);
|
||||
|
||||
input = Okio.buffer(Okio.source(socket));
|
||||
output = Okio.buffer(Okio.sink(socket));
|
||||
|
||||
while (!shouldStop && !interactions.isEmpty()) {
|
||||
handleInteractions(socket);
|
||||
}
|
||||
|
||||
waitForAllExpectedCommands.countDown();
|
||||
|
||||
while (!shouldStop) {
|
||||
readAdditionalCommands();
|
||||
}
|
||||
|
||||
waitForConnectionClosed.countDown();
|
||||
} catch (UnexpectedCommandException e) {
|
||||
unexpectedCommandException = e;
|
||||
} catch (IOException e) {
|
||||
if (!shouldStop) {
|
||||
logger.log("Exception: %s", e);
|
||||
}
|
||||
} catch (KeyStoreException | CertificateException | UnrecoverableKeyException |
|
||||
NoSuchAlgorithmException | KeyManagementException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
IOUtils.closeQuietly(socket);
|
||||
|
||||
logger.log("Exiting");
|
||||
}
|
||||
|
||||
private void handleInteractions(Socket socket) throws IOException, KeyStoreException,
|
||||
NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException, KeyManagementException,
|
||||
UnexpectedCommandException {
|
||||
|
||||
SmtpInteraction interaction = interactions.pop();
|
||||
if (interaction instanceof ExpectedCommand) {
|
||||
readExpectedCommand((ExpectedCommand) interaction);
|
||||
} else if (interaction instanceof CannedResponse) {
|
||||
writeCannedResponse((CannedResponse) interaction);
|
||||
} else if (interaction instanceof CloseConnection) {
|
||||
clientSocket.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void readExpectedCommand(ExpectedCommand expectedCommand) throws IOException,
|
||||
UnexpectedCommandException {
|
||||
|
||||
String command = input.readUtf8Line();
|
||||
if (command == null) {
|
||||
throw new EOFException();
|
||||
}
|
||||
|
||||
logger.log("C: %s", command);
|
||||
|
||||
String expected = expectedCommand.getCommand();
|
||||
if (!command.equals(expected)) {
|
||||
logger.log("EXPECTED: %s", expected);
|
||||
logger.log("ACTUAL: %s", command);
|
||||
throw new UnexpectedCommandException(expected, command);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeCannedResponse(CannedResponse cannedResponse) throws IOException {
|
||||
String response = cannedResponse.getResponse();
|
||||
logger.log("S: %s", response);
|
||||
|
||||
output.writeUtf8(response);
|
||||
output.write(CRLF);
|
||||
output.flush();
|
||||
}
|
||||
|
||||
private void enableCompression(Socket socket) throws IOException {
|
||||
InputStream inputStream = new InflaterInputStream(socket.getInputStream(), new Inflater(true));
|
||||
input = Okio.buffer(Okio.source(inputStream));
|
||||
|
||||
ZOutputStream outputStream = new ZOutputStream(socket.getOutputStream(), JZlib.Z_BEST_SPEED, true);
|
||||
outputStream.setFlushMode(JZlib.Z_PARTIAL_FLUSH);
|
||||
output = Okio.buffer(Okio.sink(outputStream));
|
||||
}
|
||||
|
||||
private void upgradeToTls(Socket socket) throws KeyStoreException, IOException, NoSuchAlgorithmException,
|
||||
CertificateException, UnrecoverableKeyException, KeyManagementException {
|
||||
|
||||
KeyStore keyStore = loadKeyStore();
|
||||
|
||||
String defaultAlgorithm = KeyManagerFactory.getDefaultAlgorithm();
|
||||
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(defaultAlgorithm);
|
||||
keyManagerFactory.init(keyStore, KEYSTORE_PASSWORD.toCharArray());
|
||||
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(keyManagerFactory.getKeyManagers(), null, null);
|
||||
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
|
||||
|
||||
SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(
|
||||
socket, socket.getInetAddress().getHostAddress(), socket.getPort(), true);
|
||||
sslSocket.setUseClientMode(false);
|
||||
sslSocket.startHandshake();
|
||||
|
||||
input = Okio.buffer(Okio.source(sslSocket.getInputStream()));
|
||||
output = Okio.buffer(Okio.sink(sslSocket.getOutputStream()));
|
||||
}
|
||||
|
||||
private KeyStore loadKeyStore() throws KeyStoreException, IOException, NoSuchAlgorithmException,
|
||||
CertificateException {
|
||||
KeyStore keyStore = KeyStore.getInstance("JKS");
|
||||
|
||||
InputStream keyStoreInputStream = getClass().getResourceAsStream(KEYSTORE_RESOURCE);
|
||||
try {
|
||||
keyStore.load(keyStoreInputStream, KEYSTORE_PASSWORD.toCharArray());
|
||||
} finally {
|
||||
keyStoreInputStream.close();
|
||||
}
|
||||
|
||||
return keyStore;
|
||||
}
|
||||
|
||||
private void readAdditionalCommands() throws IOException {
|
||||
String command = input.readUtf8Line();
|
||||
if (command == null) {
|
||||
throw new EOFException();
|
||||
}
|
||||
|
||||
logger.log("Received additional command: %s", command);
|
||||
}
|
||||
|
||||
private Socket acceptConnectionAndCloseServerSocket() throws IOException {
|
||||
Socket socket = serverSocket.accept();
|
||||
serverSocket.close();
|
||||
|
||||
return socket;
|
||||
}
|
||||
|
||||
public void shouldStop() {
|
||||
shouldStop = true;
|
||||
|
||||
IOUtils.closeQuietly(clientSocket);
|
||||
}
|
||||
|
||||
public boolean clientConnectionCreated() {
|
||||
return clientSocket != null;
|
||||
}
|
||||
|
||||
public boolean isClientConnectionClosed() {
|
||||
return clientSocket.isClosed();
|
||||
}
|
||||
|
||||
public UnexpectedCommandException getUnexpectedCommandException() {
|
||||
return unexpectedCommandException;
|
||||
}
|
||||
}
|
||||
|
||||
private static class DefaultLogger implements Logger {
|
||||
@Override
|
||||
public void log(String message) {
|
||||
System.out.println("MockSmtpServer: " + message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String format, Object... args) {
|
||||
log(String.format(format, args));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,31 +1,33 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply from: '../gradle/plugins/checkstyle-android.gradle'
|
||||
apply from: '../gradle/plugins/findbugs-android.gradle'
|
||||
apply plugin: 'jacoco'
|
||||
|
||||
if (!rootProject.optimizeForDevelopment) {
|
||||
apply from: '../gradle/plugins/checkstyle-android.gradle'
|
||||
apply from: '../gradle/plugins/findbugs-android.gradle'
|
||||
}
|
||||
|
||||
if (rootProject.testCoverage) {
|
||||
apply plugin: 'jacoco'
|
||||
}
|
||||
|
||||
repositories {
|
||||
jcenter()
|
||||
}
|
||||
|
||||
ext {
|
||||
supportLibraryVersion = '23.1.1'
|
||||
}
|
||||
|
||||
//noinspection GroovyAssignabilityCheck
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
force "com.android.support:support-annotations:${project.supportLibraryVersion}"
|
||||
force "com.android.support:support-annotations:${androidSupportLibraryVersion}"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':k9mail-library')
|
||||
compile project(':plugins:Android-PullToRefresh:library')
|
||||
compile project(':plugins:HoloColorPicker')
|
||||
compile project(':plugins:openpgp-api-lib:openpgp-api')
|
||||
compile "com.squareup.okio:okio:${okioVersion}"
|
||||
compile 'commons-io:commons-io:2.4'
|
||||
compile "com.android.support:support-v4:${project.supportLibraryVersion}"
|
||||
compile 'net.sourceforge.htmlcleaner:htmlcleaner:2.16'
|
||||
compile "com.android.support:support-v4:${androidSupportLibraryVersion}"
|
||||
compile 'net.sourceforge.htmlcleaner:htmlcleaner:2.18'
|
||||
compile 'de.cketti.library.changelog:ckchangelog:1.2.1'
|
||||
compile 'com.github.bumptech.glide:glide:3.6.1'
|
||||
compile 'com.splitwise:tokenautocomplete:2.0.7'
|
||||
|
@ -35,21 +37,22 @@ dependencies {
|
|||
|
||||
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'
|
||||
|
||||
testCompile 'org.robolectric:robolectric:3.0'
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile 'org.mockito:mockito-core:1.10.19'
|
||||
testCompile "org.robolectric:robolectric:${robolectricVersion}"
|
||||
testCompile "junit:junit:${junitVersion}"
|
||||
testCompile "org.mockito:mockito-core:${mockitoVersion}"
|
||||
testCompile 'org.jsoup:jsoup:1.10.2'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion rootProject.compileSdkVersion
|
||||
buildToolsVersion rootProject.buildToolsVersion
|
||||
compileSdkVersion androidCompileSdkVersion.toInteger()
|
||||
buildToolsVersion androidBuildToolsVersion
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.fsck.k9"
|
||||
testApplicationId "com.fsck.k9.tests"
|
||||
|
||||
versionCode 23100
|
||||
versionName '5.110'
|
||||
versionCode 23114
|
||||
versionName '5.115'
|
||||
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 22
|
||||
|
|
|
@ -128,7 +128,7 @@ public class ReconstructMessageFromDatabaseTest extends ApplicationTestCase<K9>
|
|||
protected MimeMessage parseMessage() throws IOException, MessagingException {
|
||||
InputStream messageInputStream = new ByteArrayInputStream(MESSAGE_SOURCE.getBytes());
|
||||
try {
|
||||
return new MimeMessage(messageInputStream, true);
|
||||
return MimeMessage.parseMimeMessage(messageInputStream, true);
|
||||
} finally {
|
||||
messageInputStream.close();
|
||||
}
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
package com.fsck.k9.provider;
|
||||
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.GregorianCalendar;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.database.SQLException;
|
||||
import android.net.Uri;
|
||||
|
@ -12,14 +17,10 @@ import com.fsck.k9.Preferences;
|
|||
import com.fsck.k9.mail.Message;
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
import com.fsck.k9.mail.internet.MimeMessage;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.GregorianCalendar;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class EmailProviderTest extends ProviderTestCase2<EmailProvider> {
|
||||
|
@ -28,6 +29,7 @@ public class EmailProviderTest extends ProviderTestCase2<EmailProvider> {
|
|||
private MimeMessage reply;
|
||||
private MimeMessage replyAtSameTime;
|
||||
|
||||
|
||||
public EmailProviderTest() {
|
||||
super(EmailProvider.class, EmailProvider.AUTHORITY);
|
||||
}
|
||||
|
@ -53,7 +55,6 @@ public class EmailProviderTest extends ProviderTestCase2<EmailProvider> {
|
|||
replyAtSameTime.setSentDate(new GregorianCalendar(2016, 1, 2).getTime(), false);
|
||||
replyAtSameTime.setMessageId("<uid002@email.com>");
|
||||
replyAtSameTime.setInReplyTo("<uid001@email.com>");
|
||||
|
||||
}
|
||||
|
||||
@Before
|
||||
|
@ -66,29 +67,32 @@ public class EmailProviderTest extends ProviderTestCase2<EmailProvider> {
|
|||
|
||||
@Test
|
||||
public void onCreate_shouldReturnTrue() {
|
||||
assertNotNull(this.getProvider());
|
||||
boolean returnValue = this.getProvider().onCreate();
|
||||
assertNotNull(getProvider());
|
||||
|
||||
boolean returnValue = getProvider().onCreate();
|
||||
|
||||
assertEquals(true, returnValue);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void query_withInvalidURI_throwsIllegalArgumentException() {
|
||||
this.getProvider().query(
|
||||
getProvider().query(
|
||||
Uri.parse("content://com.google.www"),
|
||||
new String[]{},
|
||||
new String[] {},
|
||||
"",
|
||||
new String[]{},
|
||||
new String[] {},
|
||||
"");
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void query_forMessagesWithInvalidAccount_throwsIllegalArgumentException() {
|
||||
Cursor cursor = this.getProvider().query(
|
||||
Uri.parse("content://"+EmailProvider.AUTHORITY+"/account/1/messages"),
|
||||
new String[]{},
|
||||
Cursor cursor = getProvider().query(
|
||||
Uri.parse("content://" + EmailProvider.AUTHORITY + "/account/1/messages"),
|
||||
new String[] {},
|
||||
"",
|
||||
new String[]{},
|
||||
new String[] {},
|
||||
"");
|
||||
|
||||
assertNotNull(cursor);
|
||||
}
|
||||
|
||||
|
@ -97,13 +101,13 @@ public class EmailProviderTest extends ProviderTestCase2<EmailProvider> {
|
|||
Account account = Preferences.getPreferences(getContext()).newAccount();
|
||||
account.getUuid();
|
||||
|
||||
Cursor cursor = this.getProvider().query(
|
||||
Uri.parse("content://"+EmailProvider.AUTHORITY
|
||||
+"/account/"+account.getUuid()+"/messages"),
|
||||
new String[]{},
|
||||
Cursor cursor = getProvider().query(
|
||||
Uri.parse("content://" + EmailProvider.AUTHORITY + "/account/" + account.getUuid() + "/messages"),
|
||||
new String[] {},
|
||||
"",
|
||||
new String[]{},
|
||||
new String[] {},
|
||||
"");
|
||||
|
||||
assertNotNull(cursor);
|
||||
assertTrue(cursor.isAfterLast());
|
||||
}
|
||||
|
@ -113,17 +117,17 @@ public class EmailProviderTest extends ProviderTestCase2<EmailProvider> {
|
|||
Account account = Preferences.getPreferences(getContext()).newAccount();
|
||||
account.getUuid();
|
||||
|
||||
Cursor cursor = this.getProvider().query(
|
||||
Uri.parse("content://"+EmailProvider.AUTHORITY
|
||||
+"/account/"+account.getUuid()+"/messages"),
|
||||
new String[]{
|
||||
Cursor cursor = getProvider().query(
|
||||
Uri.parse("content://" + EmailProvider.AUTHORITY + "/account/" + account.getUuid() + "/messages"),
|
||||
new String[] {
|
||||
EmailProvider.MessageColumns.ID,
|
||||
EmailProvider.MessageColumns.FOLDER_ID,
|
||||
EmailProvider.ThreadColumns.ROOT
|
||||
},
|
||||
"",
|
||||
new String[]{},
|
||||
new String[] {},
|
||||
"");
|
||||
|
||||
assertNotNull(cursor);
|
||||
assertTrue(cursor.isAfterLast());
|
||||
}
|
||||
|
@ -133,68 +137,62 @@ public class EmailProviderTest extends ProviderTestCase2<EmailProvider> {
|
|||
Account account = Preferences.getPreferences(getContext()).newAccount();
|
||||
account.getUuid();
|
||||
|
||||
Cursor cursor = this.getProvider().query(
|
||||
Uri.parse("content://"+EmailProvider.AUTHORITY
|
||||
+"/account/"+account.getUuid()+"/messages"),
|
||||
new String[]{
|
||||
Cursor cursor = getProvider().query(
|
||||
Uri.parse("content://" + EmailProvider.AUTHORITY + "/account/" + account.getUuid() + "/messages"),
|
||||
new String[] {
|
||||
EmailProvider.MessageColumns.ID,
|
||||
EmailProvider.MessageColumns.FOLDER_ID,
|
||||
EmailProvider.ThreadColumns.ROOT,
|
||||
EmailProvider.MessageColumns.SUBJECT
|
||||
},
|
||||
"",
|
||||
new String[]{},
|
||||
new String[] {},
|
||||
EmailProvider.MessageColumns.DATE);
|
||||
|
||||
assertNotNull(cursor);
|
||||
assertFalse(cursor.moveToFirst());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void query_forMessagesWithAccountAndRequiredFieldsAndOrderBy_providesResult()
|
||||
throws MessagingException {
|
||||
public void query_forMessagesWithAccountAndRequiredFieldsAndOrderBy_providesResult() throws MessagingException {
|
||||
Account account = Preferences.getPreferences(getContext()).newAccount();
|
||||
account.getUuid();
|
||||
account.getLocalStore().getFolder("Inbox").appendMessages(Collections.singletonList(message));
|
||||
|
||||
account.getLocalStore().getFolder("Inbox")
|
||||
.appendMessages(Collections.singletonList(message));
|
||||
|
||||
Cursor cursor = this.getProvider().query(
|
||||
Uri.parse("content://"+EmailProvider.AUTHORITY
|
||||
+"/account/"+account.getUuid()+"/messages"),
|
||||
new String[]{
|
||||
Cursor cursor = getProvider().query(
|
||||
Uri.parse("content://" + EmailProvider.AUTHORITY + "/account/" + account.getUuid() + "/messages"),
|
||||
new String[] {
|
||||
EmailProvider.MessageColumns.ID,
|
||||
EmailProvider.MessageColumns.FOLDER_ID,
|
||||
EmailProvider.ThreadColumns.ROOT,
|
||||
EmailProvider.MessageColumns.SUBJECT},
|
||||
EmailProvider.MessageColumns.SUBJECT },
|
||||
"",
|
||||
new String[]{},
|
||||
new String[] {},
|
||||
EmailProvider.MessageColumns.DATE);
|
||||
|
||||
assertNotNull(cursor);
|
||||
assertTrue(cursor.moveToFirst());
|
||||
assertEquals(message.getSubject(), cursor.getString(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void query_forMessagesWithAccountAndRequiredFieldsAndOrderBy_sortsCorrectly()
|
||||
throws MessagingException {
|
||||
public void query_forMessagesWithAccountAndRequiredFieldsAndOrderBy_sortsCorrectly() throws MessagingException {
|
||||
Account account = Preferences.getPreferences(getContext()).newAccount();
|
||||
account.getUuid();
|
||||
account.getLocalStore().getFolder("Inbox").appendMessages(Arrays.asList(message, laterMessage));
|
||||
|
||||
account.getLocalStore().getFolder("Inbox")
|
||||
.appendMessages(Arrays.asList(message, laterMessage));
|
||||
|
||||
Cursor cursor = this.getProvider().query(
|
||||
Uri.parse("content://"+EmailProvider.AUTHORITY
|
||||
+"/account/"+account.getUuid()+"/messages"),
|
||||
new String[]{
|
||||
Cursor cursor = getProvider().query(
|
||||
Uri.parse("content://" + EmailProvider.AUTHORITY + "/account/" + account.getUuid() + "/messages"),
|
||||
new String[] {
|
||||
EmailProvider.MessageColumns.ID,
|
||||
EmailProvider.MessageColumns.FOLDER_ID,
|
||||
EmailProvider.ThreadColumns.ROOT,
|
||||
EmailProvider.MessageColumns.SUBJECT
|
||||
},
|
||||
"",
|
||||
new String[]{},
|
||||
EmailProvider.MessageColumns.DATE+" DESC");
|
||||
new String[] {},
|
||||
EmailProvider.MessageColumns.DATE + " DESC");
|
||||
|
||||
assertNotNull(cursor);
|
||||
assertTrue(cursor.moveToFirst());
|
||||
assertEquals(laterMessage.getSubject(), cursor.getString(3));
|
||||
|
@ -203,18 +201,15 @@ public class EmailProviderTest extends ProviderTestCase2<EmailProvider> {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void query_forThreadedMessages_sortsCorrectly()
|
||||
throws MessagingException {
|
||||
public void query_forThreadedMessages_sortsCorrectly() throws MessagingException {
|
||||
Account account = Preferences.getPreferences(getContext()).newAccount();
|
||||
account.getUuid();
|
||||
account.getLocalStore().getFolder("Inbox").appendMessages(Arrays.asList(message, laterMessage));
|
||||
|
||||
account.getLocalStore().getFolder("Inbox")
|
||||
.appendMessages(Arrays.asList(message, laterMessage));
|
||||
|
||||
Cursor cursor = this.getProvider().query(
|
||||
Uri.parse("content://"+EmailProvider.AUTHORITY
|
||||
+"/account/"+account.getUuid()+"/messages/threaded"),
|
||||
new String[]{
|
||||
Cursor cursor = getProvider().query(
|
||||
Uri.parse("content://" + EmailProvider.AUTHORITY + "/account/" + account.getUuid() +
|
||||
"/messages/threaded"),
|
||||
new String[] {
|
||||
EmailProvider.MessageColumns.ID,
|
||||
EmailProvider.MessageColumns.FOLDER_ID,
|
||||
EmailProvider.ThreadColumns.ROOT,
|
||||
|
@ -222,8 +217,8 @@ public class EmailProviderTest extends ProviderTestCase2<EmailProvider> {
|
|||
EmailProvider.MessageColumns.DATE
|
||||
},
|
||||
"",
|
||||
new String[]{},
|
||||
EmailProvider.MessageColumns.DATE+" DESC");
|
||||
new String[] {},
|
||||
EmailProvider.MessageColumns.DATE + " DESC");
|
||||
assertNotNull(cursor);
|
||||
assertTrue(cursor.moveToFirst());
|
||||
assertEquals(laterMessage.getSubject(), cursor.getString(3));
|
||||
|
@ -232,21 +227,16 @@ public class EmailProviderTest extends ProviderTestCase2<EmailProvider> {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void query_forThreadedMessages_showsThreadOfEmailOnce()
|
||||
throws MessagingException {
|
||||
public void query_forThreadedMessages_showsThreadOfEmailOnce() throws MessagingException {
|
||||
Account account = Preferences.getPreferences(getContext()).newAccount();
|
||||
account.getUuid();
|
||||
account.getLocalStore().getFolder("Inbox").appendMessages(Collections.singletonList(message));
|
||||
account.getLocalStore().getFolder("Inbox").appendMessages(Collections.singletonList(reply));
|
||||
|
||||
account.getLocalStore().getFolder("Inbox")
|
||||
.appendMessages(Collections.singletonList(message));
|
||||
|
||||
account.getLocalStore().getFolder("Inbox")
|
||||
.appendMessages(Collections.singletonList(reply));
|
||||
|
||||
Cursor cursor = this.getProvider().query(
|
||||
Uri.parse("content://"+EmailProvider.AUTHORITY
|
||||
+"/account/"+account.getUuid()+"/messages/threaded"),
|
||||
new String[]{
|
||||
Cursor cursor = getProvider().query(
|
||||
Uri.parse("content://" + EmailProvider.AUTHORITY + "/account/" + account.getUuid() +
|
||||
"/messages/threaded"),
|
||||
new String[] {
|
||||
EmailProvider.MessageColumns.ID,
|
||||
EmailProvider.MessageColumns.FOLDER_ID,
|
||||
EmailProvider.ThreadColumns.ROOT,
|
||||
|
@ -255,8 +245,9 @@ public class EmailProviderTest extends ProviderTestCase2<EmailProvider> {
|
|||
EmailProvider.SpecialColumns.THREAD_COUNT
|
||||
},
|
||||
"",
|
||||
new String[]{},
|
||||
EmailProvider.MessageColumns.DATE+" DESC");
|
||||
new String[] {},
|
||||
EmailProvider.MessageColumns.DATE + " DESC");
|
||||
|
||||
assertNotNull(cursor);
|
||||
assertTrue(cursor.moveToFirst());
|
||||
assertEquals(2, cursor.getInt(5));
|
||||
|
@ -264,21 +255,16 @@ public class EmailProviderTest extends ProviderTestCase2<EmailProvider> {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void query_forThreadedMessages_showsThreadOfEmailWithSameSendTimeOnce()
|
||||
throws MessagingException {
|
||||
public void query_forThreadedMessages_showsThreadOfEmailWithSameSendTimeOnce() throws MessagingException {
|
||||
Account account = Preferences.getPreferences(getContext()).newAccount();
|
||||
account.getUuid();
|
||||
account.getLocalStore().getFolder("Inbox").appendMessages(Collections.singletonList(message));
|
||||
account.getLocalStore().getFolder("Inbox").appendMessages(Collections.singletonList(replyAtSameTime));
|
||||
|
||||
account.getLocalStore().getFolder("Inbox")
|
||||
.appendMessages(Collections.singletonList(message));
|
||||
|
||||
account.getLocalStore().getFolder("Inbox")
|
||||
.appendMessages(Collections.singletonList(replyAtSameTime));
|
||||
|
||||
Cursor cursor = this.getProvider().query(
|
||||
Uri.parse("content://"+EmailProvider.AUTHORITY
|
||||
+"/account/"+account.getUuid()+"/messages/threaded"),
|
||||
new String[]{
|
||||
Cursor cursor = getProvider().query(
|
||||
Uri.parse("content://" + EmailProvider.AUTHORITY + "/account/" + account.getUuid() +
|
||||
"/messages/threaded"),
|
||||
new String[] {
|
||||
EmailProvider.MessageColumns.ID,
|
||||
EmailProvider.MessageColumns.FOLDER_ID,
|
||||
EmailProvider.ThreadColumns.ROOT,
|
||||
|
@ -287,8 +273,9 @@ public class EmailProviderTest extends ProviderTestCase2<EmailProvider> {
|
|||
EmailProvider.SpecialColumns.THREAD_COUNT
|
||||
},
|
||||
"",
|
||||
new String[]{},
|
||||
EmailProvider.MessageColumns.DATE+" DESC");
|
||||
new String[] {},
|
||||
EmailProvider.MessageColumns.DATE + " DESC");
|
||||
|
||||
assertNotNull(cursor);
|
||||
assertTrue(cursor.moveToFirst());
|
||||
assertEquals(2, cursor.getInt(5));
|
||||
|
@ -296,29 +283,24 @@ public class EmailProviderTest extends ProviderTestCase2<EmailProvider> {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void query_forAThreadOfMessages_returnsMessage()
|
||||
throws MessagingException {
|
||||
public void query_forAThreadOfMessages_returnsMessage() throws MessagingException {
|
||||
Account account = Preferences.getPreferences(getContext()).newAccount();
|
||||
account.getUuid();
|
||||
|
||||
Message message = new MimeMessage();
|
||||
message.setSubject("Test Subject");
|
||||
message.setSentDate(new GregorianCalendar(2016, 1, 2).getTime(), false);
|
||||
|
||||
account.getLocalStore().getFolder("Inbox")
|
||||
.appendMessages(Collections.singletonList(message));
|
||||
account.getLocalStore().getFolder("Inbox").appendMessages(Collections.singletonList(message));
|
||||
|
||||
//Now get the thread id we just put in.
|
||||
Cursor cursor = this.getProvider().query(
|
||||
Uri.parse("content://"+EmailProvider.AUTHORITY
|
||||
+"/account/"+account.getUuid()+"/messages"),
|
||||
new String[]{
|
||||
Cursor cursor = getProvider().query(
|
||||
Uri.parse("content://" + EmailProvider.AUTHORITY + "/account/" + account.getUuid() + "/messages"),
|
||||
new String[] {
|
||||
EmailProvider.MessageColumns.ID,
|
||||
EmailProvider.MessageColumns.FOLDER_ID,
|
||||
EmailProvider.ThreadColumns.ROOT,
|
||||
},
|
||||
"",
|
||||
new String[]{},
|
||||
new String[] {},
|
||||
EmailProvider.MessageColumns.DATE);
|
||||
|
||||
assertNotNull(cursor);
|
||||
|
@ -326,11 +308,10 @@ public class EmailProviderTest extends ProviderTestCase2<EmailProvider> {
|
|||
String threadId = cursor.getString(2);
|
||||
|
||||
//Now check the message is listed under that thread
|
||||
|
||||
Cursor threadCursor = this.getProvider().query(
|
||||
Uri.parse("content://"+EmailProvider.AUTHORITY
|
||||
+"/account/"+account.getUuid()+"/thread/"+threadId),
|
||||
new String[]{
|
||||
Cursor threadCursor = getProvider().query(
|
||||
Uri.parse("content://" + EmailProvider.AUTHORITY + "/account/" + account.getUuid() +
|
||||
"/thread/" + threadId),
|
||||
new String[] {
|
||||
EmailProvider.MessageColumns.ID,
|
||||
EmailProvider.MessageColumns.FOLDER_ID,
|
||||
EmailProvider.ThreadColumns.ROOT,
|
||||
|
@ -338,8 +319,9 @@ public class EmailProviderTest extends ProviderTestCase2<EmailProvider> {
|
|||
EmailProvider.MessageColumns.DATE
|
||||
},
|
||||
"",
|
||||
new String[]{},
|
||||
new String[] {},
|
||||
EmailProvider.MessageColumns.DATE);
|
||||
|
||||
assertNotNull(threadCursor);
|
||||
assertTrue(threadCursor.moveToFirst());
|
||||
assertEquals(message.getSubject(), threadCursor.getString(3));
|
||||
|
|
|
@ -422,7 +422,17 @@
|
|||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/decrypted_file_provider_paths" />
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name=".provider.AttachmentTempFileProvider"
|
||||
android:authorities="${applicationId}.tempfileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/temp_file_provider_paths" />
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
|
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 4.2 KiB |
|
@ -58,7 +58,7 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
/**
|
||||
* Default value for the inbox folder (never changes for POP3 and IMAP)
|
||||
*/
|
||||
public static final String INBOX = "INBOX";
|
||||
private static final String INBOX = "INBOX";
|
||||
|
||||
/**
|
||||
* This local folder is used to store messages to be sent.
|
||||
|
@ -119,7 +119,7 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
* http://developer.android.com/design/style/color.html
|
||||
* Note: Order does matter, it's the order in which they will be picked.
|
||||
*/
|
||||
public static final Integer[] PREDEFINED_COLORS = new Integer[] {
|
||||
private static final Integer[] PREDEFINED_COLORS = new Integer[] {
|
||||
Color.parseColor("#0099CC"), // blue
|
||||
Color.parseColor("#669900"), // green
|
||||
Color.parseColor("#FF8800"), // orange
|
||||
|
@ -176,7 +176,6 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
private int mAutomaticCheckIntervalMinutes;
|
||||
private int mDisplayCount;
|
||||
private int mChipColor;
|
||||
private long mLastAutomaticCheckTime;
|
||||
private long mLatestOldMessageSeenTime;
|
||||
private boolean mNotifyNewMail;
|
||||
private FolderMode mFolderNotifyNewMailMode;
|
||||
|
@ -197,14 +196,14 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
private boolean mPushPollOnConnect;
|
||||
private boolean mNotifySync;
|
||||
private SortType mSortType;
|
||||
private Map<SortType, Boolean> mSortAscending = new HashMap<SortType, Boolean>();
|
||||
private Map<SortType, Boolean> mSortAscending = new HashMap<>();
|
||||
private ShowPictures mShowPictures;
|
||||
private boolean mIsSignatureBeforeQuotedText;
|
||||
private Expunge mExpungePolicy = Expunge.EXPUNGE_IMMEDIATELY;
|
||||
private int mMaxPushFolders;
|
||||
private int mIdleRefreshMinutes;
|
||||
private boolean goToUnreadMessageSearch;
|
||||
private final Map<NetworkType, Boolean> compressionMap = new ConcurrentHashMap<NetworkType, Boolean>();
|
||||
private final Map<NetworkType, Boolean> compressionMap = new ConcurrentHashMap<>();
|
||||
private Searchable searchableFolders;
|
||||
private boolean subscribedFoldersOnly;
|
||||
private int maximumPolledMessageAge;
|
||||
|
@ -222,7 +221,9 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
private boolean mStripSignature;
|
||||
private boolean mSyncRemoteDeletions;
|
||||
private String mCryptoApp;
|
||||
private boolean mCryptoAppIsDeprecatedApg;
|
||||
private long mCryptoKey;
|
||||
private boolean mCryptoSupportSignOnly;
|
||||
private boolean mMarkMessageAsReadOnView;
|
||||
private boolean mAlwaysShowCcBcc;
|
||||
private boolean mAllowRemoteSearch;
|
||||
|
@ -319,6 +320,7 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
mSyncRemoteDeletions = true;
|
||||
mCryptoApp = NO_OPENPGP_PROVIDER;
|
||||
mCryptoKey = NO_OPENPGP_KEY;
|
||||
mCryptoSupportSignOnly = false;
|
||||
mAllowRemoteSearch = false;
|
||||
mRemoteSearchFullText = false;
|
||||
mRemoteSearchNumResults = DEFAULT_REMOTE_SEARCH_NUM_RESULTS;
|
||||
|
@ -328,7 +330,7 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
|
||||
searchableFolders = Searchable.ALL;
|
||||
|
||||
identities = new ArrayList<Identity>();
|
||||
identities = new ArrayList<>();
|
||||
|
||||
Identity identity = new Identity();
|
||||
identity.setSignatureUse(true);
|
||||
|
@ -353,7 +355,7 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
private int pickColor(Context context) {
|
||||
List<Account> accounts = Preferences.getPreferences(context).getAccounts();
|
||||
|
||||
List<Integer> availableColors = new ArrayList<Integer>(PREDEFINED_COLORS.length);
|
||||
List<Integer> availableColors = new ArrayList<>(PREDEFINED_COLORS.length);
|
||||
Collections.addAll(availableColors, PREDEFINED_COLORS);
|
||||
|
||||
for (Account account : accounts) {
|
||||
|
@ -393,7 +395,6 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
if (mDisplayCount < 0) {
|
||||
mDisplayCount = K9.DEFAULT_VISIBLE_LIMIT;
|
||||
}
|
||||
mLastAutomaticCheckTime = storage.getLong(mUuid + ".lastAutomaticCheckTime", 0);
|
||||
mLatestOldMessageSeenTime = storage.getLong(mUuid + ".latestOldMessageSeenTime", 0);
|
||||
mNotifyNewMail = storage.getBoolean(mUuid + ".notifyNewMail", false);
|
||||
|
||||
|
@ -470,6 +471,7 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
String cryptoApp = storage.getString(mUuid + ".cryptoApp", NO_OPENPGP_PROVIDER);
|
||||
setCryptoApp(cryptoApp);
|
||||
mCryptoKey = storage.getLong(mUuid + ".cryptoKey", NO_OPENPGP_KEY);
|
||||
mCryptoSupportSignOnly = storage.getBoolean(mUuid + ".cryptoSupportSignOnly", false);
|
||||
mAllowRemoteSearch = storage.getBoolean(mUuid + ".allowRemoteSearch", false);
|
||||
mRemoteSearchFullText = storage.getBoolean(mUuid + ".remoteSearchFullText", false);
|
||||
mRemoteSearchNumResults = storage.getInt(mUuid + ".remoteSearchNumResults", DEFAULT_REMOTE_SEARCH_NUM_RESULTS);
|
||||
|
@ -487,11 +489,13 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
}
|
||||
|
||||
protected synchronized void delete(Preferences preferences) {
|
||||
deleteCertificates();
|
||||
|
||||
// Get the list of account UUIDs
|
||||
String[] uuids = preferences.getStorage().getString("accountUuids", "").split(",");
|
||||
|
||||
// Create a list of all account UUIDs excluding this account
|
||||
List<String> newUuids = new ArrayList<String>(uuids.length);
|
||||
List<String> newUuids = new ArrayList<>(uuids.length);
|
||||
for (String uuid : uuids) {
|
||||
if (!uuid.equals(mUuid)) {
|
||||
newUuids.add(uuid);
|
||||
|
@ -559,6 +563,9 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
editor.remove(mUuid + ".cryptoApp");
|
||||
editor.remove(mUuid + ".cryptoAutoSignature");
|
||||
editor.remove(mUuid + ".cryptoAutoEncrypt");
|
||||
editor.remove(mUuid + ".cryptoApp");
|
||||
editor.remove(mUuid + ".cryptoKey");
|
||||
editor.remove(mUuid + ".cryptoSupportSignOnly");
|
||||
editor.remove(mUuid + ".enabled");
|
||||
editor.remove(mUuid + ".markMessageAsReadOnView");
|
||||
editor.remove(mUuid + ".alwaysShowCcBcc");
|
||||
|
@ -581,7 +588,7 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
editor.commit();
|
||||
}
|
||||
|
||||
public static int findNewAccountNumber(List<Integer> accountNumbers) {
|
||||
private static int findNewAccountNumber(List<Integer> accountNumbers) {
|
||||
int newAccountNumber = -1;
|
||||
Collections.sort(accountNumbers);
|
||||
for (int accountNumber : accountNumbers) {
|
||||
|
@ -594,9 +601,9 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
return newAccountNumber;
|
||||
}
|
||||
|
||||
public static List<Integer> getExistingAccountNumbers(Preferences preferences) {
|
||||
private static List<Integer> getExistingAccountNumbers(Preferences preferences) {
|
||||
List<Account> accounts = preferences.getAccounts();
|
||||
List<Integer> accountNumbers = new ArrayList<Integer>(accounts.size());
|
||||
List<Integer> accountNumbers = new ArrayList<>(accounts.size());
|
||||
for (Account a : accounts) {
|
||||
accountNumbers.add(a.getAccountNumber());
|
||||
}
|
||||
|
@ -682,7 +689,6 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
editor.putInt(mUuid + ".idleRefreshMinutes", mIdleRefreshMinutes);
|
||||
editor.putBoolean(mUuid + ".pushPollOnConnect", mPushPollOnConnect);
|
||||
editor.putInt(mUuid + ".displayCount", mDisplayCount);
|
||||
editor.putLong(mUuid + ".lastAutomaticCheckTime", mLastAutomaticCheckTime);
|
||||
editor.putLong(mUuid + ".latestOldMessageSeenTime", mLatestOldMessageSeenTime);
|
||||
editor.putBoolean(mUuid + ".notifyNewMail", mNotifyNewMail);
|
||||
editor.putString(mUuid + ".folderNotifyNewMailMode", mFolderNotifyNewMailMode.name());
|
||||
|
@ -733,6 +739,7 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
editor.putBoolean(mUuid + ".stripSignature", mStripSignature);
|
||||
editor.putString(mUuid + ".cryptoApp", mCryptoApp);
|
||||
editor.putLong(mUuid + ".cryptoKey", mCryptoKey);
|
||||
editor.putBoolean(mUuid + ".cryptoSupportSignOnly", mCryptoSupportSignOnly);
|
||||
editor.putBoolean(mUuid + ".allowRemoteSearch", mAllowRemoteSearch);
|
||||
editor.putBoolean(mUuid + ".remoteSearchFullText", mRemoteSearchFullText);
|
||||
editor.putInt(mUuid + ".remoteSearchNumResults", mRemoteSearchNumResults);
|
||||
|
@ -760,7 +767,7 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
|
||||
}
|
||||
|
||||
public void resetVisibleLimits() {
|
||||
private void resetVisibleLimits() {
|
||||
try {
|
||||
getLocalStore().resetVisibleLimits(getDisplayCount());
|
||||
} catch (MessagingException e) {
|
||||
|
@ -770,7 +777,6 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param context
|
||||
* @return <code>null</code> if not available
|
||||
* @throws MessagingException
|
||||
* @see {@link #isAvailable(Context)}
|
||||
|
@ -800,7 +806,7 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
|
||||
// Use the LocalSearch instance to create a WHERE clause to query the content provider
|
||||
StringBuilder query = new StringBuilder();
|
||||
List<String> queryArgs = new ArrayList<String>();
|
||||
List<String> queryArgs = new ArrayList<>();
|
||||
ConditionsTreeNode conditions = search.getConditions();
|
||||
SqlQueryBuilder.buildWhereClause(this, conditions, query, queryArgs);
|
||||
|
||||
|
@ -809,12 +815,12 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
|
||||
Cursor cursor = cr.query(uri, projection, selection, selectionArgs, null);
|
||||
try {
|
||||
if (cursor.moveToFirst()) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
stats.unreadMessageCount = cursor.getInt(0);
|
||||
stats.flaggedMessageCount = cursor.getInt(1);
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
Utility.closeQuietly(cursor);
|
||||
}
|
||||
|
||||
LocalStore localStore = getLocalStore();
|
||||
|
@ -831,7 +837,7 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
cacheChips();
|
||||
}
|
||||
|
||||
public synchronized void cacheChips() {
|
||||
private synchronized void cacheChips() {
|
||||
mReadColorChip = new ColorChip(mChipColor, true, ColorChip.CIRCULAR);
|
||||
mUnreadColorChip = new ColorChip(mChipColor, false, ColorChip.CIRCULAR);
|
||||
mFlaggedReadColorChip = new ColorChip(mChipColor, true, ColorChip.STAR);
|
||||
|
@ -843,8 +849,7 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
}
|
||||
|
||||
|
||||
public ColorChip generateColorChip(boolean messageRead, boolean toMe, boolean ccMe,
|
||||
boolean fromMe, boolean messageFlagged) {
|
||||
public ColorChip generateColorChip(boolean messageRead, boolean messageFlagged) {
|
||||
ColorChip chip;
|
||||
|
||||
if (messageRead) {
|
||||
|
@ -869,10 +874,6 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
return mUuid;
|
||||
}
|
||||
|
||||
public Uri getContentUri() {
|
||||
return Uri.parse("content://accounts/" + getUuid());
|
||||
}
|
||||
|
||||
public synchronized String getStoreUri() {
|
||||
return mStoreUri;
|
||||
}
|
||||
|
@ -965,11 +966,11 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
} catch (MessagingException e) {
|
||||
Log.e(K9.LOG_TAG, "Switching local storage provider from " +
|
||||
mLocalStorageProviderId + " to " + id + " failed.", e);
|
||||
} finally {
|
||||
// if migration to/from SD-card failed once, it will fail again.
|
||||
if (!successful) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// if migration to/from SD-card failed once, it will fail again.
|
||||
if (!successful) {
|
||||
return;
|
||||
}
|
||||
|
||||
mLocalStorageProviderId = id;
|
||||
|
@ -1007,14 +1008,6 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
resetVisibleLimits();
|
||||
}
|
||||
|
||||
public synchronized long getLastAutomaticCheckTime() {
|
||||
return mLastAutomaticCheckTime;
|
||||
}
|
||||
|
||||
public synchronized void setLastAutomaticCheckTime(long lastAutomaticCheckTime) {
|
||||
this.mLastAutomaticCheckTime = lastAutomaticCheckTime;
|
||||
}
|
||||
|
||||
public synchronized long getLatestOldMessageSeenTime() {
|
||||
return mLatestOldMessageSeenTime;
|
||||
}
|
||||
|
@ -1330,9 +1323,9 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
}
|
||||
|
||||
private synchronized List<Identity> loadIdentities(Storage storage) {
|
||||
List<Identity> newIdentities = new ArrayList<Identity>();
|
||||
List<Identity> newIdentities = new ArrayList<>();
|
||||
int ident = 0;
|
||||
boolean gotOne = false;
|
||||
boolean gotOne;
|
||||
do {
|
||||
gotOne = false;
|
||||
String name = storage.getString(mUuid + "." + IDENTITY_NAME_KEY + "." + ident, null);
|
||||
|
@ -1374,7 +1367,7 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
|
||||
private synchronized void deleteIdentities(Storage storage, StorageEditor editor) {
|
||||
int ident = 0;
|
||||
boolean gotOne = false;
|
||||
boolean gotOne;
|
||||
do {
|
||||
gotOne = false;
|
||||
String email = storage.getString(mUuid + "." + IDENTITY_EMAIL_KEY + "." + ident, null);
|
||||
|
@ -1411,7 +1404,7 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
}
|
||||
|
||||
public synchronized void setIdentities(List<Identity> newIdentities) {
|
||||
identities = new ArrayList<Identity>(newIdentities);
|
||||
identities = new ArrayList<>(newIdentities);
|
||||
}
|
||||
|
||||
public synchronized Identity getIdentity(int i) {
|
||||
|
@ -1482,7 +1475,7 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
* Never <code>null</code>.
|
||||
* @throws MessagingException
|
||||
*/
|
||||
public void switchLocalStorage(final String newStorageProviderId) throws MessagingException {
|
||||
private void switchLocalStorage(final String newStorageProviderId) throws MessagingException {
|
||||
if (!mLocalStorageProviderId.equals(newStorageProviderId)) {
|
||||
getLocalStore().switchLocalStorage(newStorageProviderId);
|
||||
}
|
||||
|
@ -1615,13 +1608,19 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
}
|
||||
|
||||
public void setCryptoApp(String cryptoApp) {
|
||||
if (cryptoApp == null || cryptoApp.equals("apg")) {
|
||||
boolean isApgCryptoProvider = "apg".equals(cryptoApp);
|
||||
if (cryptoApp == null || isApgCryptoProvider) {
|
||||
mCryptoAppIsDeprecatedApg = isApgCryptoProvider;
|
||||
mCryptoApp = NO_OPENPGP_PROVIDER;
|
||||
} else {
|
||||
mCryptoApp = cryptoApp;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isCryptoAppDeprecatedApg() {
|
||||
return mCryptoAppIsDeprecatedApg;
|
||||
}
|
||||
|
||||
public long getCryptoKey() {
|
||||
return mCryptoKey;
|
||||
}
|
||||
|
@ -1630,6 +1629,14 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
mCryptoKey = keyId;
|
||||
}
|
||||
|
||||
public boolean getCryptoSupportSignOnly() {
|
||||
return mCryptoSupportSignOnly;
|
||||
}
|
||||
|
||||
public void setCryptoSupportSignOnly(boolean cryptoSupportSignOnly) {
|
||||
mCryptoSupportSignOnly = cryptoSupportSignOnly;
|
||||
}
|
||||
|
||||
public boolean allowRemoteSearch() {
|
||||
return mAllowRemoteSearch;
|
||||
}
|
||||
|
@ -1691,10 +1698,8 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
*/
|
||||
public boolean isAvailable(Context context) {
|
||||
String localStorageProviderId = getLocalStorageProviderId();
|
||||
if (localStorageProviderId == null) {
|
||||
return true; // defaults to internal memory
|
||||
}
|
||||
return StorageManager.getInstance(context).isReady(localStorageProviderId);
|
||||
boolean storageProviderIsInternalMemory = localStorageProviderId == null;
|
||||
return storageProviderIsInternalMemory || StorageManager.getInstance(context).isReady(localStorageProviderId);
|
||||
}
|
||||
|
||||
public synchronized boolean isEnabled() {
|
||||
|
@ -1843,8 +1848,7 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
/**
|
||||
* Add a new certificate for the incoming or outgoing server to the local key store.
|
||||
*/
|
||||
public void addCertificate(CheckDirection direction,
|
||||
X509Certificate certificate) throws CertificateException {
|
||||
public void addCertificate(CheckDirection direction, X509Certificate certificate) throws CertificateException {
|
||||
Uri uri;
|
||||
if (direction == CheckDirection.INCOMING) {
|
||||
uri = Uri.parse(getStoreUri());
|
||||
|
@ -1860,8 +1864,7 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
* new host/port, then try and delete any (possibly non-existent) certificate stored for the
|
||||
* old host/port.
|
||||
*/
|
||||
public void deleteCertificate(String newHost, int newPort,
|
||||
CheckDirection direction) {
|
||||
public void deleteCertificate(String newHost, int newPort, CheckDirection direction) {
|
||||
Uri uri;
|
||||
if (direction == CheckDirection.INCOMING) {
|
||||
uri = Uri.parse(getStoreUri());
|
||||
|
@ -1884,7 +1887,7 @@ public class Account implements BaseAccount, StoreConfig {
|
|||
* Examine the settings for the account and attempt to delete (possibly non-existent)
|
||||
* certificates for the incoming and outgoing servers.
|
||||
*/
|
||||
public void deleteCertificates() {
|
||||
private void deleteCertificates() {
|
||||
LocalKeyStore localKeyStore = LocalKeyStore.getInstance();
|
||||
|
||||
String storeUri = getStoreUri();
|
||||
|
|