├── .gitattributes ├── .gitignore ├── LICENSE ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── kotlin │ ├── common-build-script.gradle.kts │ ├── java-version-script.gradle.kts │ └── maven-publish-script.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── guide ├── docs │ ├── client.md │ ├── convert.md │ ├── core.md │ ├── html.md │ ├── img │ │ ├── favicon.png │ │ └── logo.png │ ├── index.md │ ├── server.md │ ├── setup_gradle.md │ ├── setup_maven.md │ └── tls.md └── mkdocs.yml ├── readme.md ├── settings.gradle.kts ├── simplekotlinmail-client ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── net │ └── axay │ └── simplekotlinmail │ └── delivery │ ├── EmailDelivery.kt │ ├── MailerBuilder.kt │ └── MailerManager.kt ├── simplekotlinmail-core ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── net │ └── axay │ └── simplekotlinmail │ ├── data │ └── SMTPLoginInfo.kt │ └── email │ ├── EmailBuilder.kt │ └── EmailConverter.kt ├── simplekotlinmail-html ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── net │ └── axay │ └── simplekotlinmail │ └── html │ └── EmailBuilderHtml.kt ├── simplekotlinmail-server ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── net │ └── axay │ └── simplekotlinmail │ └── server │ ├── MessageListener.kt │ ├── SMTPServer.kt │ ├── SMTPServerBuilder.kt │ ├── exchange │ ├── IncomingFrom.kt │ ├── IncomingMail.kt │ ├── IncomingMailExchange.kt │ └── IncomingRecipient.kt │ ├── tls │ ├── SMTPServerTLS.kt │ ├── TLSContext.kt │ └── TLSVersions.kt │ └── utils │ └── SubEthaExtensions.kt ├── simplekotlinmail-test ├── build.gradle.kts └── src │ └── test │ ├── kotlin │ └── net │ │ └── axay │ │ └── simplekotlinmail │ │ └── test │ │ ├── ServerClientTest.kt │ │ └── ServerTest.kt │ └── resources │ ├── keystore │ └── truststore ├── simplekotlinmail_logo.afdesign └── simplekotlinmail_logo.svg /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle 2 | .gradle/ 3 | */.gradle/ 4 | build/ 5 | */build/ 6 | 7 | # IntelliJ IDEA 8 | .idea/ 9 | 10 | # MkDocs 11 | guide/site/ 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | } 8 | 9 | val kotlinVersion = "1.6.10" 10 | 11 | dependencies { 12 | implementation(kotlin("gradle-plugin", kotlinVersion)) 13 | implementation("org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion") 14 | } 15 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/common-build-script.gradle.kts: -------------------------------------------------------------------------------- 1 | group = "net.axay" 2 | version = "1.4.0" 3 | 4 | repositories { 5 | mavenCentral() 6 | } 7 | 8 | plugins { 9 | kotlin("jvm") 10 | } 11 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/java-version-script.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | `java-library` 5 | } 6 | 7 | tasks.withType { 8 | options.release.set(11) 9 | } 10 | 11 | tasks.withType { 12 | kotlinOptions.jvmTarget = "11" 13 | } 14 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/maven-publish-script.gradle.kts: -------------------------------------------------------------------------------- 1 | val githubProject = "jakobkmar/SimpleKotlinMail" 2 | 3 | description = "A simple coroutine based Kotlin Email API for client- and server-side projects" 4 | 5 | plugins { 6 | kotlin("jvm") 7 | `java-library` 8 | `maven-publish` 9 | signing 10 | } 11 | 12 | signing { 13 | sign(publishing.publications) 14 | } 15 | 16 | java { 17 | withSourcesJar() 18 | withJavadocJar() 19 | } 20 | 21 | publishing { 22 | repositories { 23 | maven("https://oss.sonatype.org/service/local/staging/deploy/maven2") { 24 | name = "ossrh" 25 | credentials(PasswordCredentials::class) 26 | } 27 | } 28 | 29 | publications { 30 | create(project.name) { 31 | from(components["java"]) 32 | 33 | this.groupId = project.group.toString() 34 | this.artifactId = project.name 35 | this.version = project.version.toString() 36 | 37 | pom { 38 | name.set(project.name) 39 | description.set(project.description) 40 | 41 | developers { 42 | developer { 43 | name.set("jakobkmar") 44 | } 45 | } 46 | 47 | licenses { 48 | license { 49 | name.set("The Apache License, Version 2.0") 50 | url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") 51 | } 52 | } 53 | 54 | url.set("https://github.com/$githubProject") 55 | 56 | scm { 57 | connection.set("scm:git:git://github.com/$githubProject.git") 58 | url.set("https://github.com/$githubProject/tree/main") 59 | } 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | 3 | kotlinSerialization = "1.3.2" 4 | kotlinCoroutines = "1.6.0" 5 | kotlinHtml = "0.7.3" 6 | 7 | simpleJavaMail = "7.0.0" 8 | 9 | subethasmtp = "6.0.1" 10 | 11 | [libraries] 12 | 13 | simpleJavaMail = { module = "org.simplejavamail:simple-java-mail", version.ref = "simpleJavaMail" } 14 | simpleJavaMail-batch = { module = "org.simplejavamail:batch-module", version.ref = "simpleJavaMail" } 15 | simpleJavaMail-smime = { module = "org.simplejavamail:smime-module", version.ref = "simpleJavaMail" } 16 | kt-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinCoroutines" } 17 | kt-coroutines-jdk8 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8", version.ref = "kotlinCoroutines" } 18 | kt-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinSerialization" } 19 | kt-html = { module = "org.jetbrains.kotlinx:kotlinx-html-jvm", version.ref = "kotlinHtml" } 20 | subethasmtp = { module = "com.github.davidmoten:subethasmtp", version.ref = "subethasmtp" } 21 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakobkmar/SimpleKotlinMail/0f4018a5a37d225e69bd278012c2a08dc8cf8c2d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /guide/docs/client.md: -------------------------------------------------------------------------------- 1 | # Client 2 | 3 | ## Mailer 4 | 5 | A mailer instance is connected to an SMTP server capable of sending emails (e.g. [Postfix](http://www.postfix.org/), paid services or Gmail). 6 | 7 | For a list of free SMTP servers have a look into [this list by mailtrap](https://blog.mailtrap.io/free-smtp-servers/). If you want to configure a send-only postfix server, read [this tutorial](https://blog.mailtrap.io/setup-smtp-server/). 8 | 9 | #### Create a Mailer 10 | 11 | ```kotlin 12 | val mailer = mailerBuilder(host = "your_hostname", port = 25) 13 | ``` 14 | 15 | #### Set the global default Mailer 16 | 17 | The default mailer will be used for sending emails if no specific mailer is passed as a parameter. 18 | 19 | ```kotlin 20 | MailerManager.defaultMailer = mailer 21 | ``` 22 | 23 | #### Shutdown all Mailers 24 | 25 | The mailer instances keep your program alive. In order to exit your program safely, you have to shut down the mailer instances. 26 | 27 | ```kotlin 28 | MailerManager.shutdownMailers() 29 | ``` 30 | 31 | ## Send emails 32 | 33 | Easily send your [previously built emails](core.md). 34 | 35 | ##### Using coroutines (Asynchronously) 36 | 37 | This approach allows you to send emails without blocking the current thread. These methods only work within a `CoroutineScope`. 38 | 39 | ```kotlin 40 | email.send() // using the defult Mailer instance 41 | // or 42 | email.send(mailer) 43 | ``` 44 | 45 | ###### Suspend until completion 46 | 47 | The send function returns a `Job`, therefore you can just join it. 48 | 49 | ```kotlin 50 | email.send().join() 51 | ``` 52 | 53 | This can thrown an exception of the delivery failed, you can handle that like this: 54 | 55 | ```kotlin 56 | kotlin.runCatching { email.send().join() } 57 | .onFailure { it.printStackTrace() } 58 | .onSuccess { println("I just sent an email!") } 59 | ``` 60 | 61 | ##### Synchronously 62 | 63 | If you need to send your emails synchronously for some reason, you can do that. 64 | This function will throw an exception if the action fails, otherwise (on success) it will just pass. 65 | 66 | ```kotlin 67 | email.sendSync() // using the default Mailer instance 68 | // or 69 | email.sendSync(mailer) 70 | ``` 71 | -------------------------------------------------------------------------------- /guide/docs/convert.md: -------------------------------------------------------------------------------- 1 | # Convert 2 | 3 | It is easy to convert between different email representations. 4 | 5 | ## To an email object 6 | 7 | EML `String` to `Email` 8 | ```kotlin 9 | string.toEmail() 10 | ``` 11 | 12 | `MimeMessage` to `Email` 13 | ```kotlin 14 | mimeMessage.email 15 | ``` 16 | 17 | ## From an email object 18 | 19 | `Email` to `MimeMessage` 20 | ```kotlin 21 | email.mimeMessage 22 | ``` 23 | 24 | `Email` to EML `String` 25 | ```kotlin 26 | email.eml 27 | ``` 28 | -------------------------------------------------------------------------------- /guide/docs/core.md: -------------------------------------------------------------------------------- 1 | # Email API 2 | 3 | ## Create an email 4 | 5 | You can create an email using the `emailBuilder` function. Inside an email builder you set everything you need. 6 | 7 | ```kotlin 8 | val email = emailBuilder { 9 | from("foo@bar.com") 10 | to("info@example.org") 11 | 12 | withSubject("Important question") 13 | withPlainText("Hey, how are you today?") 14 | 15 | // and much more 16 | } 17 | ``` 18 | 19 | A built email object is immutable. 20 | 21 | ### Send that email 22 | 23 | Go to the [client page](client.md). 24 | 25 | ## Copy an email 26 | 27 | If you don't want to start blank with your email builder, you can copy another email and change it to your liking. 28 | 29 | ```kotlin 30 | val copiedEmail = email.copy { 31 | // modify the email 32 | } 33 | ``` 34 | 35 | ## Forward an email 36 | 37 | ```kotlin 38 | val forwardEmail = email.forward(from = "forwardaddress@example.org") { 39 | prependText("This is a forwarded message.") 40 | } 41 | ``` 42 | 43 | ## Reply to an email 44 | 45 | ```kotlin 46 | val replyEmail = email.reply(from = "replyaddress@example.org", toAll = false) { 47 | prependText("This is a reply message.") 48 | } 49 | ``` 50 | -------------------------------------------------------------------------------- /guide/docs/html.md: -------------------------------------------------------------------------------- 1 | # HTML 2 | 3 | SimpleKotlinMail provides a convenient way for setting the HTML content of a message. Instead of just passing in the 4 | HTML text, you can use the HTML DSL of [kotlinx.html](https://github.com/Kotlin/kotlinx.html) to create the HTML 5 | content. 6 | 7 | ## Set HTML content 8 | 9 | Inside your email builder, use the `withHTML` function. 10 | 11 | ```kotlin 12 | emailBuilder { 13 | withHTML { 14 | body { 15 | h1 { +"Really important question:" } 16 | p { +"Hey, how are you today?" } 17 | } 18 | } 19 | } 20 | ``` 21 | 22 | #### What are the advantages of using kotlinx.html? 23 | 24 | In the above example, there isn't any real advantage apart from the better readability. But as soon as you want to 25 | customize the messages, for example for a specific user, kotlinx.html shows its full power. 26 | 27 | Let's say that we want to greet the user with his name. Additionally, we want to list the items he bought: 28 | 29 | ```kotlin 30 | val username = "foo" 31 | val itemsBought = listOf("banana", "apple") 32 | 33 | emailBuilder { 34 | withHTML { 35 | body { 36 | h1 { +"We have received your order." } 37 | p { +"Thanks for shopping with us $username!" } 38 | 39 | p { +"You have bought the following items:" } 40 | ul { 41 | for (item in itemsBought) 42 | li { +item } 43 | } 44 | } 45 | } 46 | } 47 | ``` 48 | 49 | Here kotlinx.html allows us to use the existing data we have (`username` and `itemsBought`) without the need for an 50 | additional templating language. 51 | -------------------------------------------------------------------------------- /guide/docs/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakobkmar/SimpleKotlinMail/0f4018a5a37d225e69bd278012c2a08dc8cf8c2d/guide/docs/img/favicon.png -------------------------------------------------------------------------------- /guide/docs/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakobkmar/SimpleKotlinMail/0f4018a5a37d225e69bd278012c2a08dc8cf8c2d/guide/docs/img/logo.png -------------------------------------------------------------------------------- /guide/docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to SimpleKotlinMail 2 | 3 | SimpleKotlinMail is a Kotlin Email API - coroutine based, easy to use and modern! 4 | You can use it for both clientside and serverside projects, meaning you can **send** and **receive** emails. 5 | 6 | ### Coroutines 7 | 8 | Send emails without blocking the current thread. 9 | 10 | ### Easy to use 11 | 12 | Create emails, mailers and SMTP servers with the help of intuitive Kotlin builders. 13 | 14 | ### Modern 15 | 16 | SimpleKotlinMail hides unnecessary and overly complicated parts of the mail protocol from you. 17 | Additionally, the API is typesafe. 18 | 19 | ## Get Started 20 | 21 | Follow the [setup guide](setup_gradle.md) to get started quickly. 22 | 23 | ## Links 24 | 25 | - [GitHub](https://github.com/bluefireoly/SimpleKotlinMail) repository 26 | -------------------------------------------------------------------------------- /guide/docs/server.md: -------------------------------------------------------------------------------- 1 | # SMTP Server 2 | 3 | This SMTP server can be used to **receive** emails. 4 | 5 | ## Setup 6 | 7 | To set up a new SMTP server you can use the `smtpServer` function. 8 | 9 | ```kotlin 10 | val smtpServer = smtpServer(port = 25) { 11 | // access the SMTP server builder in here (more information below) 12 | } 13 | ``` 14 | 15 | Now you can start the SMTPServer 16 | ```kotlin 17 | // this function does not block, 18 | // but it keeps the current thread alive until you call stop() 19 | smtpServer.start() 20 | ``` 21 | 22 | It is good practice to stop the SMTPServer 23 | ```kotlin 24 | smtpServer.stop() 25 | ``` 26 | 27 | ___ 28 | 29 | *The following code samples all are inside an SMTP server builder.* 30 | 31 | ## Optional Configuration 32 | 33 | #### Configuration variables 34 | 35 | Inside the SMTP server builder, you have access to the following configuration variables: 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 50 | 51 | 52 | 53 | 56 | 57 | 58 | 59 | 62 | 63 | 64 | 65 | 72 | 73 | 74 |
OptionDescription
maxRecipients 48 | The maximum amount of recipients the server accepts per message.
default = 1000 49 |
maxConnections 54 | The maximum amount of connections the server allows at once.
default = 1000 55 |
prefferedMaxMessageSize 60 | The maximum size of a message. This won't be enforced, this is just an information for the connected client.
default = null (no limit) 61 |
connectionTimeout 66 | The timeout for waiting for data on a connection.
default = 1 to TimeUnit.MINUTES (1 minute)

67 | The best way to set this is the following 68 | ```kotlin 69 | connectionTimeout = 2 to TimeUnit.MINUTES 70 | ``` 71 |
75 | 76 | #### TLS (Secure connections) 77 | 78 | Go to the dedicated [TLS page](tls.md) for more details. 79 | 80 | ## Listeners 81 | 82 | With listeners, you can receive and **process emails**. 83 | 84 | ### Commands 85 | 86 | #### Mail (easiest) 87 | 88 | Listen to the `DATA` command (called last, therefore has the most information): 89 | ```kotlin 90 | mailListener { 91 | // get envelope data 92 | it.envelopeFrom 93 | it.recipients 94 | 95 | // get the email 96 | it.email 97 | 98 | // optional response 99 | it.respondText("OK message received") 100 | 101 | // if the client sent too much data 102 | it.tooMuchData() 103 | } 104 | ``` 105 | 106 | Earlier available commands are: 107 | 108 | #### From 109 | Listen to the `MAIL FROM` command (called first, can only be called once): 110 | ```kotlin 111 | fromListener { 112 | // get the envelope from 113 | it.envelopeFrom 114 | } 115 | ``` 116 | 117 | #### Recipients 118 | Listen to the `RCPT TO` command (can be called multiple times): 119 | ```kotlin 120 | recipientListener { 121 | // current envelope data 122 | it.envelopeFrom 123 | it.currentRecipients // all recipients known so far (including the one responsible for this call) 124 | 125 | // get the recipient responsible for this call 126 | it.recipient 127 | } 128 | ``` 129 | 130 | ### Reject Connections 131 | 132 | You can reject connections after receiving any command. Both functions have parameters for a custom reponse and status code. 133 | ```kotlin 134 | // reject the connection 135 | it.reject() 136 | // drop the connection 137 | it.dropConnection() 138 | ``` 139 | 140 | ### MessageContext 141 | 142 | The context gives you more information about the current connection. 143 | ```kotlin 144 | // get the MessageContext 145 | it.context 146 | // get the session 147 | it.context.session 148 | // example usage of session 149 | it.context.session?.socket is SSLSocket 150 | ``` 151 | -------------------------------------------------------------------------------- /guide/docs/setup_gradle.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | ## Repository 4 | 5 | SimpleKotlinMail is available on Maven Central. 6 | 7 | ```kotlin 8 | repositories { 9 | mavenCentral() 10 | } 11 | ``` 12 | 13 | ## Dependencies 14 | 15 | Kotlin DSL 16 | 17 | ```kotlin 18 | dependencies { 19 | implementation("net.axay:MODULE:VERSION") 20 | } 21 | ``` 22 | 23 | or Groovy DSL 24 | 25 | ```groovy 26 | dependencies { 27 | implementation 'net.axay:MODULE:VERSION' 28 | } 29 | ``` 30 | 31 | **Replace**: 32 | 33 | - `VERSION` with the version you wish to use (you 34 | can [find the latest version on github](https://github.com/bluefireoly/SimpleKotlinMail/releases)) 35 | 36 | - `MODULE` with the names of the following modules: 37 | 38 | ### Modules 39 | 40 | - **`simplekotlinmail-core`** **(required)** 41 | - **`simplekotlinmail-client`** if you want to send emails 42 | - **`simplekotlinmail-server`** if you want to receive emails 43 | - **`simplekotlinmail-html`** if you want to use kotlinx.html inside your email builders 44 | 45 | ## JVM Version 46 | 47 | To be able to use the inline functions of the API, you have to configure the JVM version (if you have not done that 48 | already). 49 | 50 | ```kotlin 51 | tasks.withType { 52 | kotlinOptions.jvmTarget = jvmVersionString // <- e.g. 11 53 | } 54 | ``` 55 | -------------------------------------------------------------------------------- /guide/docs/setup_maven.md: -------------------------------------------------------------------------------- 1 | # Setup (Maven) 2 | 3 | Currently, there is no setup guide for Maven. 4 | 5 | For the repository and dependencies, you can have a look at the [Gradle](setup_gradle.md) setup guide (or switch to 6 | Gradle entirely). 7 | -------------------------------------------------------------------------------- /guide/docs/tls.md: -------------------------------------------------------------------------------- 1 | # TLS 2 | 3 | TLS ([Transport Layer Security](https://en.wikipedia.org/wiki/Transport_Layer_Security)) allows you to secure the Simple 4 | Mail Transfer Protocol. 5 | 6 | **You probably want to [let your proxy handle TLS](https://docs.nginx.com/nginx/admin-guide/mail-proxy/mail-proxy/), 7 | this guide is for TLS in tests.** 8 | 9 | ## TLSContext (SSLContext) 10 | 11 | SimpleKotlinMail provides a utility function allowing you to easily create a new TLSContext (actually an SSLContext). 12 | 13 | ```kotlin 14 | val tlsContext = TLSContext( 15 | File("path/to/keystore"), keyStorePassphrase = "passphrase", 16 | File("path/to/truststore"), trustStorePassphrase = "passphrase" 17 | ) 18 | ``` 19 | 20 | If you need a keystore and truststore for **testing purposes**, you can download 21 | both [from the OpenJDK repository](https://github.com/openjdk/jdk/tree/master/test/jdk/javax/net/ssl/etc). 22 | 23 | ## Secure the SMTP server 24 | 25 | Use the `setupTLS` function inside an SMTP server builder. 26 | 27 | ```kotlin 28 | smtpServer { 29 | setupTLS(tlsContext) 30 | } 31 | ``` 32 | 33 | _Currently, TLSv1.3 and TLSv1.2 are enabled by default, but you can change that (example below)._ 34 | 35 | or if you need more options 36 | 37 | ```kotlin 38 | setupTLS( 39 | tlsContext, 40 | requireTLS = true, 41 | protocolVersions = listOf(TLSVersions.TLS_1_3), 42 | requireClientAuth = true 43 | ) { 44 | // configure the SSLSocket to your liking 45 | } 46 | ``` 47 | 48 | ## Secure the Mailer 49 | 50 | Inside a mailer builder, you can set the transport strategy: 51 | 52 | ```kotlin 53 | mailerBuilder { 54 | // STARTTLS 55 | withTransportStrategy(TransportStrategy.SMTP_TLS) 56 | // or complete TLS encryption 57 | withTransportStrategy(TransportStrategy.SMTPS) 58 | } 59 | ``` 60 | -------------------------------------------------------------------------------- /guide/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: SimpleKotlinMail 2 | site_description: A simple, modern and coroutine based Kotlin Email API, supporting both clientside and serverside projects 3 | site_author: Jakob K 4 | 5 | theme: 6 | name: material 7 | language: en 8 | favicon: img/logo.png 9 | icon: 10 | repo: fontawesome/brands/github 11 | logo: img/logo.png 12 | palette: 13 | scheme: preference 14 | primary: deep orange 15 | accent: amber 16 | 17 | markdown_extensions: 18 | - pymdownx.highlight 19 | - pymdownx.superfences 20 | - toc: 21 | permalink: true 22 | toc_depth: 4 23 | 24 | repo_url: https://github.com/jakobkmar/SimpleKotlinMail 25 | repo_name: jakobkmar/SimpleKotlinMail 26 | 27 | edit_uri: edit/main/guide/docs/ 28 | 29 | nav: 30 | - Home: index.md 31 | - Setup: 32 | - Setup using Gradle: setup_gradle.md 33 | - Setup using Maven: setup_maven.md 34 | - Core: 35 | - Email API: core.md 36 | - Kotlin HTML DSL: html.md 37 | - Convert: convert.md 38 | - Server (receive): server.md 39 | - Client (send): client.md 40 | - TLS: tls.md 41 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | SimpleKotlinMail Logo 2 | 3 | _SimpleKotlinMail is a Kotlin Mail API, using coroutines and providing DSLs._ 4 | 5 | This project is **not actively being worked on anymore**, for an explanation and alternatives see https://github.com/jakobkmar/SimpleKotlinMail/issues/6#issuecomment-1082247403 . 6 | 7 | ## Features 8 | 9 | - build emails 10 | - send emails (using an external SMTP server) 11 | - receive and process emails 12 | - TLS support 13 | 14 | ## To get started, visit the **[Documentation](https://jakobkmar.github.io/SimpleKotlinMail/)**. 15 | 16 | ## Examples 17 | 18 | The purpose of the following code snippets is to provide an insight into the API. However, they are not suitable for learning the API, you should use the actual documentation for this. 19 | 20 | ### Build 21 | 22 | Build an email: 23 | 24 | ```kotlin 25 | val email = emailBuilder { 26 | from("no-reply@example.com") 27 | to("foo@bar.com") 28 | 29 | withSubject("Important question") 30 | withPlainText("Hey, how are you doing?") 31 | } 32 | ``` 33 | 34 | ### Send 35 | 36 | Send that email: 37 | 38 | ```kotlin 39 | suspend fun main() = email.send() 40 | ``` 41 | 42 | ### Server / Receive 43 | 44 | Create a custom SMTPServer: 45 | 46 | ```kotlin 47 | val smtpServer = smtpServer { 48 | mailListener { 49 | println(it.email.plainText) 50 | } 51 | }.start(keepAlive = true) 52 | ``` 53 | 54 | ### Convert 55 | 56 | ```kotlin 57 | // EML String -> Email 58 | string.toEmail() 59 | // MimeMessage -> Email 60 | mimeMessage.email 61 | ``` 62 | 63 | ### HTML 64 | 65 | Inside the email builder, you can easily access kotlinx.html: 66 | ```kotlin 67 | emailBuilder { 68 | withHTML { 69 | div { 70 | h1 { +"Really important question!" } 71 | p { +"Hey, how are you doing?" } 72 | } 73 | } 74 | } 75 | ``` 76 | 77 | ### And more 78 | 79 | To learn more about SimpleKotlinMail, visit the **[Documentation](https://jakobkmar.github.io/SimpleKotlinMail/)**. 80 | 81 | ## Project information 82 | 83 | This project uses [SimpleJavaMail](https://www.simplejavamail.org/) to deal with java MimeMessages in a more elegant 84 | way. On the server side, this projects depends on a fork of [SubEthaSMTP](https://github.com/davidmoten/subethasmtp). 85 | 86 | If you use the documented functionality of SimpleKotlinMail, everything will make use 87 | of [kotlinx.coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html). 88 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | val projectName = "simplekotlinmail" 2 | 3 | rootProject.name = projectName 4 | 5 | include("$projectName-core") 6 | 7 | include("$projectName-server") 8 | include("$projectName-client") 9 | include("$projectName-html") 10 | 11 | include("$projectName-test") 12 | 13 | enableFeaturePreview("VERSION_CATALOGS") 14 | -------------------------------------------------------------------------------- /simplekotlinmail-client/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `common-build-script` 3 | `java-version-script` 4 | `maven-publish-script` 5 | } 6 | 7 | dependencies { 8 | api(project(":${rootProject.name}-core")) 9 | 10 | api(libs.kt.coroutines) 11 | api(libs.kt.coroutines.jdk8) 12 | 13 | api(libs.simpleJavaMail.batch) 14 | } 15 | -------------------------------------------------------------------------------- /simplekotlinmail-client/src/main/kotlin/net/axay/simplekotlinmail/delivery/EmailDelivery.kt: -------------------------------------------------------------------------------- 1 | package net.axay.simplekotlinmail.delivery 2 | 3 | import kotlinx.coroutines.* 4 | import kotlinx.coroutines.future.asDeferred 5 | import org.simplejavamail.api.email.Email 6 | import org.simplejavamail.api.mailer.Mailer 7 | 8 | private val sendScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) 9 | 10 | /** 11 | * Send this email. 12 | * @param mailer the mailer which should be used to deliver the message 13 | */ 14 | suspend fun Email.send(mailer: Mailer = MailerManager.defaultMailer): Job { 15 | return sendScope.launch { 16 | mailer.sendMail(this@send, true) 17 | .asDeferred().await() 18 | } 19 | } 20 | 21 | /** 22 | * Send this email synchronously. 23 | * @param mailer the mailer which should be used to deliver the message 24 | */ 25 | fun Email.sendSync(mailer: Mailer = MailerManager.defaultMailer) = 26 | mailer.sendMail(this, false) 27 | -------------------------------------------------------------------------------- /simplekotlinmail-client/src/main/kotlin/net/axay/simplekotlinmail/delivery/MailerBuilder.kt: -------------------------------------------------------------------------------- 1 | package net.axay.simplekotlinmail.delivery 2 | 3 | import net.axay.simplekotlinmail.data.SMTPLoginInfo 4 | import org.simplejavamail.api.mailer.Mailer 5 | import org.simplejavamail.mailer.MailerBuilder 6 | import org.simplejavamail.mailer.internal.MailerRegularBuilderImpl 7 | 8 | /** 9 | * Open a mailer builder. 10 | * This function automatically builds the email and 11 | * returns it. 12 | */ 13 | inline fun mailerBuilder(smtpLoginInfo: SMTPLoginInfo, builder: MailerRegularBuilderImpl.() -> Unit = {}): Mailer = 14 | mailerBuilder(smtpLoginInfo.host, smtpLoginInfo.port, smtpLoginInfo.username, smtpLoginInfo.password, builder) 15 | 16 | /** 17 | * Open a mailer builder. 18 | * This function automatically builds the email and 19 | * returns it. 20 | */ 21 | inline fun mailerBuilder( 22 | host: String = "localhost", 23 | port: Int = 25, 24 | username: String? = null, 25 | password: String? = null, 26 | builder: MailerRegularBuilderImpl.() -> Unit = {} 27 | ): Mailer = MailerBuilder 28 | .withSMTPServer(host, port, username, password) 29 | .apply(builder) 30 | .buildMailer().apply { 31 | MailerManager.registerMailer(this) 32 | } 33 | -------------------------------------------------------------------------------- /simplekotlinmail-client/src/main/kotlin/net/axay/simplekotlinmail/delivery/MailerManager.kt: -------------------------------------------------------------------------------- 1 | package net.axay.simplekotlinmail.delivery 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.Job 5 | import kotlinx.coroutines.coroutineScope 6 | import kotlinx.coroutines.launch 7 | import org.simplejavamail.api.mailer.Mailer 8 | 9 | object MailerManager { 10 | private val LOCAL_MAILER by lazy { mailerBuilder("localhost", 25) } 11 | 12 | private var DEFAULT_MAILER: Mailer? = null 13 | 14 | /** 15 | * The default mailer instance, that is used if 16 | * the no other instance is provided. 17 | */ 18 | var defaultMailer: Mailer 19 | get() = DEFAULT_MAILER ?: LOCAL_MAILER 20 | set(value) { 21 | DEFAULT_MAILER = value 22 | } 23 | 24 | private val registeredMailers = HashSet() 25 | 26 | /** 27 | * Register this mailer instance. 28 | * 29 | * Note that mailer instances built with the mailerBuilder 30 | * are registered automatically. 31 | */ 32 | fun registerMailer(mailer: Mailer) = registeredMailers.add(mailer) 33 | 34 | /** 35 | * Shutdown all registered mailers. 36 | */ 37 | suspend fun shutdownMailers() { 38 | coroutineScope { 39 | val shutdownJobs = mutableSetOf() 40 | registeredMailers.removeAll { 41 | shutdownJobs += launch(Dispatchers.IO) { 42 | it.shutdownConnectionPool().get() 43 | } 44 | true 45 | } 46 | shutdownJobs.forEach { it.join() } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /simplekotlinmail-core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `common-build-script` 3 | `java-version-script` 4 | `maven-publish-script` 5 | 6 | kotlin("plugin.serialization") 7 | } 8 | 9 | dependencies { 10 | api(libs.simpleJavaMail) 11 | api(libs.simpleJavaMail.smime) 12 | 13 | api(libs.kt.serialization) 14 | } 15 | -------------------------------------------------------------------------------- /simplekotlinmail-core/src/main/kotlin/net/axay/simplekotlinmail/data/SMTPLoginInfo.kt: -------------------------------------------------------------------------------- 1 | package net.axay.simplekotlinmail.data 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class SMTPLoginInfo( 7 | val host: String, 8 | val port: Int, 9 | val username: String?, 10 | val password: String? 11 | ) 12 | -------------------------------------------------------------------------------- /simplekotlinmail-core/src/main/kotlin/net/axay/simplekotlinmail/email/EmailBuilder.kt: -------------------------------------------------------------------------------- 1 | package net.axay.simplekotlinmail.email 2 | 3 | import org.simplejavamail.api.email.Email 4 | import org.simplejavamail.api.email.EmailPopulatingBuilder 5 | import org.simplejavamail.email.EmailBuilder 6 | 7 | /** 8 | * Open a new email builder. 9 | * This function automatically builds the email and 10 | * returns it. 11 | */ 12 | inline fun emailBuilder(builder: EmailPopulatingBuilder.() -> Unit): Email = 13 | EmailBuilder.startingBlank().apply(builder).buildEmail() 14 | 15 | /** 16 | * Copy this email and open a new email builder. 17 | * This function automatically builds the new email and 18 | * returns it. 19 | */ 20 | inline fun Email.copy(builder: EmailPopulatingBuilder.() -> Unit): Email = 21 | EmailBuilder.copying(this).apply(builder).buildEmail() 22 | 23 | /** 24 | * Reply to this email. 25 | * This functions opens a new email builder, automatically builds 26 | * the new email and returns it. 27 | * @param from optional from recipient address 28 | * @param toAll 29 | */ 30 | inline fun Email.reply( 31 | from: String? = null, 32 | toAll: Boolean = false, 33 | builder: EmailPopulatingBuilder.() -> Unit 34 | ): Email = (if (toAll) EmailBuilder.replyingToAll(this) else EmailBuilder.replyingTo(this)) 35 | .apply { if (from != null) from(from) }.apply(builder).buildEmail() 36 | 37 | /** 38 | * Forward this email. 39 | * This functions opens a new email builder, automatically builds 40 | * the new email and returns it. 41 | * @param from optional from recipient address 42 | */ 43 | inline fun Email.forward(from: String? = null, builder: EmailPopulatingBuilder.() -> Unit = {}): Email = 44 | EmailBuilder.forwarding(this).apply { if (from != null) from(from) }.apply(builder).buildEmail() 45 | -------------------------------------------------------------------------------- /simplekotlinmail-core/src/main/kotlin/net/axay/simplekotlinmail/email/EmailConverter.kt: -------------------------------------------------------------------------------- 1 | package net.axay.simplekotlinmail.email 2 | 3 | import jakarta.mail.internet.MimeMessage 4 | import org.simplejavamail.api.email.Email 5 | import org.simplejavamail.converter.EmailConverter 6 | 7 | // TO EMAIL 8 | 9 | /** 10 | * Convert this MimeMessage to an Email instance. 11 | */ 12 | val MimeMessage.email: Email get() = EmailConverter.mimeMessageToEmail(this) 13 | 14 | /** 15 | * Convert this string to an Email instance. 16 | */ 17 | fun String.toEmail(): Email = EmailConverter.emlToEmail(this) 18 | 19 | // FROM EMAIL 20 | 21 | /** 22 | * Convert this Email instance to a MimeMessage 23 | */ 24 | val Email.mimeMessage: MimeMessage get() = EmailConverter.emailToMimeMessage(this) 25 | 26 | /** 27 | * Convert this Email instance to an eml string 28 | */ 29 | val Email.eml: String get() = EmailConverter.emailToEML(this) 30 | -------------------------------------------------------------------------------- /simplekotlinmail-html/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `common-build-script` 3 | `java-version-script` 4 | `maven-publish-script` 5 | } 6 | 7 | dependencies { 8 | api(project(":${rootProject.name}-core")) 9 | 10 | api(libs.kt.html) 11 | } 12 | -------------------------------------------------------------------------------- /simplekotlinmail-html/src/main/kotlin/net/axay/simplekotlinmail/html/EmailBuilderHtml.kt: -------------------------------------------------------------------------------- 1 | package net.axay.simplekotlinmail.html 2 | 3 | import kotlinx.html.TagConsumer 4 | import kotlinx.html.stream.appendHTML 5 | import org.simplejavamail.api.email.EmailPopulatingBuilder 6 | 7 | private typealias ContextlessHtmlBuilder = TagConsumer.() -> StringBuilder 8 | 9 | /** 10 | * Set the html of the message. 11 | */ 12 | inline fun EmailPopulatingBuilder.withHTML(builder: ContextlessHtmlBuilder): EmailPopulatingBuilder = 13 | withHTMLText(builder.build()) 14 | 15 | /** 16 | * Append html to the message. 17 | */ 18 | inline fun EmailPopulatingBuilder.appendHTML(builder: ContextlessHtmlBuilder): EmailPopulatingBuilder = 19 | appendTextHTML(builder.build()) 20 | 21 | /** 22 | * Prepend html to the message. 23 | */ 24 | inline fun EmailPopulatingBuilder.prependHTML(builder: ContextlessHtmlBuilder): EmailPopulatingBuilder = 25 | prependTextHTML(builder.build()) 26 | 27 | @Suppress("NOTHING_TO_INLINE") 28 | inline fun ContextlessHtmlBuilder.build() = this(StringBuilder().appendHTML()).toString() 29 | -------------------------------------------------------------------------------- /simplekotlinmail-server/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `common-build-script` 3 | `java-version-script` 4 | `maven-publish-script` 5 | } 6 | 7 | dependencies { 8 | api(project(":${rootProject.name}-core")) 9 | 10 | api(libs.kt.coroutines) 11 | 12 | api(libs.subethasmtp) 13 | } 14 | -------------------------------------------------------------------------------- /simplekotlinmail-server/src/main/kotlin/net/axay/simplekotlinmail/server/MessageListener.kt: -------------------------------------------------------------------------------- 1 | package net.axay.simplekotlinmail.server 2 | 3 | import net.axay.simplekotlinmail.server.exchange.IncomingFrom 4 | import net.axay.simplekotlinmail.server.exchange.IncomingMail 5 | import net.axay.simplekotlinmail.server.exchange.IncomingRecipient 6 | import org.simplejavamail.converter.EmailConverter 7 | import org.subethamail.smtp.MessageContext 8 | import org.subethamail.smtp.MessageHandler 9 | import org.subethamail.smtp.MessageHandlerFactory 10 | import java.io.InputStream 11 | 12 | class MailHandlerFactory( 13 | private val fromListener: FromListener? = null, 14 | private val recipientListener: RecipientListener? = null, 15 | private val mailListener: MailListener? = null 16 | ) : MessageHandlerFactory { 17 | override fun create(context: MessageContext) = MailHandler(context, 18 | fromListener, 19 | recipientListener, 20 | mailListener 21 | ) 22 | } 23 | 24 | class MailHandler( 25 | private val context: MessageContext, 26 | 27 | private val fromListener: FromListener?, 28 | private val recipientListener: RecipientListener?, 29 | private val mailListener: MailListener? 30 | ) : MessageHandler { 31 | private lateinit var from: String 32 | private val recipients = mutableListOf() 33 | 34 | override fun from(from: String) { 35 | this.from = from 36 | 37 | if (fromListener != null) { 38 | val incomingFrom = IncomingFrom(from, context) 39 | fromListener.invoke(incomingFrom) 40 | } 41 | } 42 | 43 | override fun recipient(recipient: String) { 44 | recipients += recipient 45 | 46 | if (recipientListener != null) { 47 | val incomingRecipient = IncomingRecipient(from, recipient, recipients, context) 48 | recipientListener.invoke(incomingRecipient) 49 | } 50 | } 51 | 52 | override fun data(data: InputStream): String? { 53 | return if (mailListener != null) { 54 | val incomingMail = IncomingMail(from, recipients, EmailConverter.emlToEmail(data), context) 55 | mailListener.invoke(incomingMail) 56 | 57 | incomingMail.response 58 | } else null 59 | } 60 | 61 | override fun done() = Unit 62 | } 63 | 64 | /** 65 | * This listener will be invoked after the MAIL FROM command 66 | * during a SMTP exchange. 67 | * 68 | * Throw an exception to reject the mail, 69 | * see [MessageHandler.from] for a list of supported 70 | * exceptions. 71 | */ 72 | typealias FromListener = (from: IncomingFrom) -> Unit 73 | 74 | /** 75 | * This listener will be invoked when a mail 76 | * was received. 77 | * 78 | * Throw an exception to reject the mail, 79 | * see [MessageHandler.data] for a list of supported 80 | * exceptions. 81 | */ 82 | typealias RecipientListener = (mail: IncomingRecipient) -> Unit 83 | 84 | /** 85 | * This listener will be invoked when a mail 86 | * was received. 87 | * 88 | * Throw an exception to reject the mail, 89 | * see [MessageHandler.data] for a list of supported 90 | * exceptions. 91 | */ 92 | typealias MailListener = (mail: IncomingMail) -> Unit 93 | -------------------------------------------------------------------------------- /simplekotlinmail-server/src/main/kotlin/net/axay/simplekotlinmail/server/SMTPServer.kt: -------------------------------------------------------------------------------- 1 | package net.axay.simplekotlinmail.server 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.launch 6 | import kotlinx.coroutines.runBlocking 7 | import org.subethamail.smtp.server.SMTPServer 8 | 9 | /** 10 | * Create a new SMTPServer instance. 11 | * In order to start this instance, call [SMTPServer.start] 12 | * @see SMTPServer 13 | */ 14 | fun smtpServer(port: Int = 25, builder: SMTPServerBuilder.() -> Unit = {}): SMTPServer = 15 | SMTPServerBuilder(port).apply(builder).build() 16 | 17 | private val serverScope = CoroutineScope(Dispatchers.IO) 18 | 19 | /** 20 | * @param keepAlive true, if the current thread should be kept alive until 21 | * the SMTPServer was shut down 22 | * @see SMTPServer.start 23 | */ 24 | fun SMTPServer.start(keepAlive: Boolean): SMTPServer { 25 | if (keepAlive) 26 | this@start.start() 27 | else runBlocking { 28 | serverScope.launch { 29 | this@start.start() 30 | }.join() 31 | } 32 | return this 33 | } 34 | -------------------------------------------------------------------------------- /simplekotlinmail-server/src/main/kotlin/net/axay/simplekotlinmail/server/SMTPServerBuilder.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("MemberVisibilityCanBePrivate") 2 | 3 | package net.axay.simplekotlinmail.server 4 | 5 | import org.subethamail.smtp.server.SMTPServer 6 | import java.util.concurrent.TimeUnit 7 | 8 | class SMTPServerBuilder internal constructor(port: Int) { 9 | private var fromListener: FromListener? = null 10 | private var recipientListener: RecipientListener? = null 11 | private var mailListener: MailListener? = null 12 | 13 | internal val builder: SMTPServer.Builder = SMTPServer.port(port) 14 | 15 | /** 16 | * The maximum amount of recipients the server accepts per message. 17 | */ 18 | var maxRecipients = 1000 19 | 20 | /** 21 | * The maximum amount of connections the server allows at once. 22 | */ 23 | var maxConnections = 1000 24 | 25 | /** 26 | * The maximum size of a message. 27 | * **This won't be enforced,** this is just an information for the connected client. 28 | */ 29 | var prefferedMaxMessageSize: Int? = null 30 | 31 | /** 32 | * The timeout for waiting for data on a connection. 33 | */ 34 | var connectionTimeout: Pair = 1000 * 60 * 1 to TimeUnit.MILLISECONDS 35 | 36 | /** 37 | * Set the [FromListener] for this SMTPServer. 38 | */ 39 | fun fromListener(listener: FromListener) { 40 | fromListener = listener 41 | } 42 | 43 | /** 44 | * Set the [RecipientListener] for this SMTPServer. 45 | */ 46 | fun recipientListener(listener: RecipientListener) { 47 | recipientListener = listener 48 | } 49 | 50 | /** 51 | * Set the [MailListener] for this SMTPServer. 52 | */ 53 | fun mailListener(listener: MailListener) { 54 | mailListener = listener 55 | } 56 | 57 | internal fun build(): SMTPServer { 58 | if (fromListener != null || recipientListener != null || mailListener != null) 59 | builder.messageHandlerFactory(MailHandlerFactory(fromListener, recipientListener, mailListener)) 60 | 61 | builder.apply { 62 | maxRecipients(maxRecipients) 63 | maxConnections(maxConnections) 64 | maxMessageSize(prefferedMaxMessageSize ?: 0) 65 | connectionTimeout(connectionTimeout.first, connectionTimeout.second) 66 | } 67 | 68 | return builder.build() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /simplekotlinmail-server/src/main/kotlin/net/axay/simplekotlinmail/server/exchange/IncomingFrom.kt: -------------------------------------------------------------------------------- 1 | package net.axay.simplekotlinmail.server.exchange 2 | 3 | import org.subethamail.smtp.MessageContext 4 | 5 | class IncomingFrom internal constructor( 6 | val envelopeFrom: String, 7 | context: MessageContext 8 | ) : IncomingMailExchange(context) 9 | -------------------------------------------------------------------------------- /simplekotlinmail-server/src/main/kotlin/net/axay/simplekotlinmail/server/exchange/IncomingMail.kt: -------------------------------------------------------------------------------- 1 | package net.axay.simplekotlinmail.server.exchange 2 | 3 | import org.simplejavamail.api.email.Email 4 | import org.subethamail.smtp.MessageContext 5 | import org.subethamail.smtp.TooMuchDataException 6 | 7 | /** 8 | * This class represents an incoming mail, received by a SMTP server. 9 | */ 10 | class IncomingMail internal constructor( 11 | val envelopeFrom: String, 12 | val recipients: List, 13 | val email: Email, 14 | context: MessageContext 15 | ) : IncomingMailExchange(context) { 16 | internal var response: String? = null 17 | 18 | /** 19 | * Set a custom response success message. 20 | */ 21 | fun respondText(message: String) { 22 | response = message 23 | } 24 | 25 | /** 26 | * Reject the message if an input stream provides 27 | * more data than the listener can handle 28 | */ 29 | fun tooMuchData(message: String? = null): Nothing = 30 | if (message != null) throw TooMuchDataException(message) else throw TooMuchDataException() 31 | } 32 | -------------------------------------------------------------------------------- /simplekotlinmail-server/src/main/kotlin/net/axay/simplekotlinmail/server/exchange/IncomingMailExchange.kt: -------------------------------------------------------------------------------- 1 | package net.axay.simplekotlinmail.server.exchange 2 | 3 | import org.subethamail.smtp.DropConnectionException 4 | import org.subethamail.smtp.MessageContext 5 | import org.subethamail.smtp.RejectException 6 | 7 | abstract class IncomingMailExchange(val context: MessageContext) { 8 | /** 9 | * Reject this message. 10 | */ 11 | fun reject(message: String? = null, statusCode: Int? = null): Nothing = 12 | throw RejectException( 13 | statusCode ?: RejectException.DEFAULT_CODE, 14 | message ?: RejectException.DEFAULT_MESSAGE 15 | ) 16 | 17 | /** 18 | * Drop the connection. 19 | */ 20 | fun dropConnection(message: String? = null, statusCode: Int? = null): Nothing = 21 | throw DropConnectionException( 22 | statusCode ?: DropConnectionException.DEFAULT_CODE, 23 | message ?: DropConnectionException.DEFAULT_MESSAGE 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /simplekotlinmail-server/src/main/kotlin/net/axay/simplekotlinmail/server/exchange/IncomingRecipient.kt: -------------------------------------------------------------------------------- 1 | package net.axay.simplekotlinmail.server.exchange 2 | 3 | import org.subethamail.smtp.MessageContext 4 | 5 | class IncomingRecipient internal constructor( 6 | val envelopeFrom: String, 7 | val recipient: String, 8 | val currentRecipients: List, 9 | context: MessageContext 10 | ) : IncomingMailExchange(context) 11 | -------------------------------------------------------------------------------- /simplekotlinmail-server/src/main/kotlin/net/axay/simplekotlinmail/server/tls/SMTPServerTLS.kt: -------------------------------------------------------------------------------- 1 | package net.axay.simplekotlinmail.server.tls 2 | 3 | import net.axay.simplekotlinmail.server.SMTPServerBuilder 4 | import java.net.InetSocketAddress 5 | import javax.net.ssl.SSLContext 6 | import javax.net.ssl.SSLSocket 7 | 8 | /** 9 | * Setup TLS for this smtp server. 10 | * 11 | * @param tlsContext the SSLContext for creating a SSLSocket - use [TLSContext] to easily create a new context 12 | * @param requireTLS if true, TLS required for every mail exchange 13 | * @param protocolVersions the TLS protocol versions you wish to enable 14 | * @param overrideCipherSuites if true, the default enabled cipher suites will be replaced with 15 | * the cipher suites chosen by SimpleKotlinMail - you can see them here: [TLSVersions] 16 | * @param requireClientAuth [SSLSocket.setNeedClientAuth] 17 | * @param starttlsSocketBuilder use this builder to configure the STARTTLS socket 18 | */ 19 | fun SMTPServerBuilder.setupTLS( 20 | tlsContext: SSLContext, 21 | requireTLS: Boolean = false, 22 | protocolVersions: Array = arrayOf(TLSVersions.TLS_1_3, TLSVersions.TLS_1_2), 23 | overrideCipherSuites: Boolean = false, 24 | requireClientAuth: Boolean = true, 25 | starttlsSocketBuilder: (SSLSocket.() -> Unit)? = null, 26 | ) { 27 | if (requireTLS) builder.requireTLS() else builder.enableTLS() 28 | 29 | builder.startTlsSocketFactory { socketIn -> 30 | (tlsContext.socketFactory.createSocket( 31 | socketIn, (socketIn.remoteSocketAddress as InetSocketAddress).hostString, socketIn.port, true 32 | ) as SSLSocket).apply { 33 | useClientMode = false 34 | 35 | enabledProtocols = ( 36 | protocolVersions 37 | .mapTo(LinkedHashSet()) { it.protocolVersion } intersect supportedProtocols.toList() 38 | ).toTypedArray() 39 | 40 | if (overrideCipherSuites) { 41 | enabledCipherSuites = ( 42 | protocolVersions 43 | .flatMapTo(LinkedHashSet()) { it.cipherSuites } intersect supportedCipherSuites.toList() 44 | ).toTypedArray() 45 | } 46 | 47 | needClientAuth = requireClientAuth 48 | 49 | starttlsSocketBuilder?.invoke(this) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /simplekotlinmail-server/src/main/kotlin/net/axay/simplekotlinmail/server/tls/TLSContext.kt: -------------------------------------------------------------------------------- 1 | package net.axay.simplekotlinmail.server.tls 2 | 3 | import java.io.File 4 | import java.security.KeyStore 5 | import javax.net.ssl.KeyManagerFactory 6 | import javax.net.ssl.SSLContext 7 | import javax.net.ssl.TrustManagerFactory 8 | 9 | /** 10 | * Create a new SSLContext (only using TLS) from the given key and trust store. 11 | */ 12 | @Suppress("FunctionName") 13 | fun TLSContext( 14 | keyStore: File, 15 | keyStorePassphrase: String, 16 | trustStore: File, 17 | trustStorePassphrase: String 18 | ): SSLContext = SSLContext.getInstance("TLS").apply { 19 | val keyStorePassphraseArray = keyStorePassphrase.toCharArray() 20 | 21 | init( 22 | KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()).apply { 23 | init( 24 | KeyStore.getInstance(KeyStore.getDefaultType()).apply { 25 | load(keyStore.inputStream(), keyStorePassphraseArray) 26 | }, 27 | keyStorePassphraseArray 28 | ) 29 | }.keyManagers, 30 | 31 | TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).apply { 32 | init( 33 | KeyStore.getInstance(KeyStore.getDefaultType()).apply { 34 | load(trustStore.inputStream(), trustStorePassphrase.toCharArray()) 35 | } 36 | ) 37 | }.trustManagers, 38 | 39 | null 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /simplekotlinmail-server/src/main/kotlin/net/axay/simplekotlinmail/server/tls/TLSVersions.kt: -------------------------------------------------------------------------------- 1 | package net.axay.simplekotlinmail.server.tls 2 | 3 | /** 4 | * This enum lists the different TLS versions 5 | * which can be used together with [javax.net.ssl]. 6 | * 7 | * @param protocolVersion The string representing the TLS version. 8 | * @param cipherSuites A list of strings representing the cipher suites which should be used 9 | * for that version. The names refer to the names of the enum values from [sun.security.ssl.CipherSuite]. 10 | * 11 | * See: https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml 12 | */ 13 | enum class TLSVersions( 14 | val protocolVersion: String, 15 | val cipherSuites: List 16 | ) { 17 | TLS_1_3( 18 | "TLSv1.3", 19 | listOf( 20 | "TLS_AES_256_GCM_SHA384", 21 | "TLS_AES_128_GCM_SHA256", 22 | "TLS_CHACHA20_POLY1305_SHA256" 23 | ) 24 | ), 25 | TLS_1_2( 26 | "TLSv1.2", 27 | listOf( 28 | "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", 29 | "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", 30 | "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256" 31 | ) 32 | ), 33 | @Deprecated("Support is running out. Since TLS 1.1 uses the non-collision-resistant hash function SHA-1 to create the signature, the BSI advises against its use.") 34 | TLS_1_1( 35 | "TLSv1.1", 36 | listOf( 37 | "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", 38 | "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA" 39 | ) 40 | ), 41 | @Deprecated("Support is running out. No longer complies with the Payment Card Industry Data Security Standard (PCI DSS) in payment transactions as of June 30, 2018.") 42 | TLS_1_0( 43 | "TLSv1", 44 | listOf( 45 | "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", 46 | "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA" 47 | ) 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /simplekotlinmail-server/src/main/kotlin/net/axay/simplekotlinmail/server/utils/SubEthaExtensions.kt: -------------------------------------------------------------------------------- 1 | package net.axay.simplekotlinmail.server.utils 2 | 3 | import org.subethamail.smtp.MessageContext 4 | import org.subethamail.smtp.server.Session 5 | import javax.net.ssl.SSLSocket 6 | 7 | /** 8 | * @return The session. This will probably never be null. 9 | */ 10 | val MessageContext.session get() = (this as? Session) 11 | 12 | /** 13 | * @return The [SSLSocket], or null if the socket is not an SSLSocket. 14 | */ 15 | val Session.sslSocket get() = (socket as? SSLSocket) 16 | -------------------------------------------------------------------------------- /simplekotlinmail-test/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `common-build-script` 3 | `java-version-script` 4 | } 5 | 6 | dependencies { 7 | implementation(project(":${rootProject.name}-core")) 8 | implementation(project(":${rootProject.name}-client")) 9 | implementation(project(":${rootProject.name}-server")) 10 | implementation(project(":${rootProject.name}-html")) 11 | 12 | testImplementation("org.slf4j:slf4j-simple:1.7.32") 13 | testImplementation("org.junit.jupiter:junit-jupiter:5.8.2") 14 | } 15 | 16 | tasks.test { 17 | useJUnitPlatform() 18 | } 19 | -------------------------------------------------------------------------------- /simplekotlinmail-test/src/test/kotlin/net/axay/simplekotlinmail/test/ServerClientTest.kt: -------------------------------------------------------------------------------- 1 | package net.axay.simplekotlinmail.test 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import net.axay.simplekotlinmail.delivery.MailerManager 5 | import net.axay.simplekotlinmail.delivery.mailerBuilder 6 | import net.axay.simplekotlinmail.delivery.send 7 | import net.axay.simplekotlinmail.delivery.sendSync 8 | import net.axay.simplekotlinmail.email.emailBuilder 9 | import net.axay.simplekotlinmail.server.smtpServer 10 | import net.axay.simplekotlinmail.server.start 11 | import net.axay.simplekotlinmail.server.tls.TLSContext 12 | import net.axay.simplekotlinmail.server.tls.setupTLS 13 | import net.axay.simplekotlinmail.server.utils.session 14 | import net.axay.simplekotlinmail.server.utils.sslSocket 15 | import org.junit.jupiter.api.Assertions.assertEquals 16 | import org.junit.jupiter.api.Test 17 | import org.simplejavamail.api.mailer.config.TransportStrategy 18 | import java.io.File 19 | 20 | class ServerClientTest { 21 | private val plainText = "Hey, how are you?" 22 | 23 | private val email = emailBuilder { 24 | from("foo@bar.com") 25 | to("info@example.org") 26 | 27 | withSubject("This is an important message!") 28 | withPlainText(plainText) 29 | } 30 | 31 | @Test 32 | fun testServer() = runBlocking { 33 | val smtpServer = smtpServer(2500) { 34 | mailListener { 35 | println("received email: ${it.email.plainText}") 36 | assertEquals(it.email.plainText?.trim(), plainText) 37 | } 38 | } 39 | 40 | smtpServer.start(keepAlive = true) 41 | 42 | MailerManager.defaultMailer = mailerBuilder(port = 2500) 43 | 44 | email.sendSync() 45 | email.send().join() 46 | 47 | MailerManager.shutdownMailers() 48 | 49 | smtpServer.stop() 50 | } 51 | 52 | @Test 53 | fun testServerTLS() = runBlocking { 54 | val smtpServer = smtpServer(2500) { 55 | setupTLS( 56 | TLSContext( 57 | File("./src/test/resources/keystore"), "passphrase", 58 | File("./src/test/resources/truststore"), "passphrase" 59 | ), 60 | true, 61 | requireClientAuth = false 62 | ) 63 | 64 | mailListener { 65 | println("received email: ${it.email.plainText}") 66 | assertEquals(it.email.plainText?.trim(), plainText) 67 | assert(it.context.session?.sslSocket != null) 68 | } 69 | } 70 | 71 | smtpServer.start(keepAlive = true) 72 | 73 | MailerManager.defaultMailer = mailerBuilder(port = 2500) { 74 | verifyingServerIdentity(false) 75 | trustingAllHosts(true) 76 | withTransportStrategy(TransportStrategy.SMTP_TLS) 77 | } 78 | 79 | email.sendSync() 80 | email.send().join() 81 | 82 | MailerManager.shutdownMailers() 83 | 84 | smtpServer.stop() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /simplekotlinmail-test/src/test/kotlin/net/axay/simplekotlinmail/test/ServerTest.kt: -------------------------------------------------------------------------------- 1 | package net.axay.simplekotlinmail.test 2 | 3 | import net.axay.simplekotlinmail.server.smtpServer 4 | import net.axay.simplekotlinmail.server.start 5 | import org.junit.jupiter.api.Test 6 | 7 | class ServerTest { 8 | @Test 9 | fun startServerKeepAliveTrue() { 10 | val smtpServer = smtpServer(2500) 11 | assert(smtpServer.start(keepAlive = true).isRunning) 12 | smtpServer.stop() 13 | } 14 | 15 | @Test 16 | fun startServerKeepAliveFalse() { 17 | val smtpServer = smtpServer(2500) 18 | assert(smtpServer.start(keepAlive = false).isRunning) 19 | smtpServer.stop() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /simplekotlinmail-test/src/test/resources/keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakobkmar/SimpleKotlinMail/0f4018a5a37d225e69bd278012c2a08dc8cf8c2d/simplekotlinmail-test/src/test/resources/keystore -------------------------------------------------------------------------------- /simplekotlinmail-test/src/test/resources/truststore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakobkmar/SimpleKotlinMail/0f4018a5a37d225e69bd278012c2a08dc8cf8c2d/simplekotlinmail-test/src/test/resources/truststore -------------------------------------------------------------------------------- /simplekotlinmail_logo.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakobkmar/SimpleKotlinMail/0f4018a5a37d225e69bd278012c2a08dc8cf8c2d/simplekotlinmail_logo.afdesign --------------------------------------------------------------------------------