From 46e6e2ba506da09e32731cf55f75d7d1b7125062 Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Tue, 11 Jul 2023 12:04:05 +0200 Subject: [PATCH 01/23] project init --- .editorconfig | 17 + .gitattributes | 1 + .gitignore | 39 +++ checkstyle-suppressions.xml | 16 + checkstyle.xml | 205 ++++++++++++ mvnw | 308 ++++++++++++++++++ mvnw.cmd | 205 ++++++++++++ pom.xml | 67 ++++ readme.md | 0 .../springboot/emailservice/Application.java | 13 + src/main/resources/application.properties | 1 + .../emailservice/ApplicationTests.java | 13 + 12 files changed, 885 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 checkstyle-suppressions.xml create mode 100644 checkstyle.xml create mode 100755 mvnw create mode 100644 mvnw.cmd create mode 100644 pom.xml create mode 100644 readme.md create mode 100644 src/main/java/it/aboutbits/springboot/emailservice/Application.java create mode 100644 src/main/resources/application.properties create mode 100644 src/test/java/it/aboutbits/springboot/emailservice/ApplicationTests.java diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d12ced7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +max_line_length = 120 +tab_width = 4 +ij_continuation_indent_size = 8 + +[*.yml] +indent_size = 2 + +[*.md] +max_line_length = off diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b8afbf6 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf encoding=utf-8 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..427ac49 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac ### +.DS_Store + +### Local Development ### +application-local.yml diff --git a/checkstyle-suppressions.xml b/checkstyle-suppressions.xml new file mode 100644 index 0000000..5a91b4c --- /dev/null +++ b/checkstyle-suppressions.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 0000000..2a4ed8d --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,205 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..66df285 --- /dev/null +++ b/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# 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 Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..95ba6f5 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,205 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.2.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..52d764c --- /dev/null +++ b/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.1.1 + + + it.aboutbits + springboot.emailservice + 0.0.1-SNAPSHOT + springboot.emailservice + Spring Boot Email Service + + 17 + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-mail + + + org.liquibase + liquibase-core + + + + org.postgresql + postgresql + runtime + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/it/aboutbits/springboot/emailservice/Application.java b/src/main/java/it/aboutbits/springboot/emailservice/Application.java new file mode 100644 index 0000000..bd6b234 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/emailservice/Application.java @@ -0,0 +1,13 @@ +package it.aboutbits.springboot.emailservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/src/test/java/it/aboutbits/springboot/emailservice/ApplicationTests.java b/src/test/java/it/aboutbits/springboot/emailservice/ApplicationTests.java new file mode 100644 index 0000000..6fb02a5 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/emailservice/ApplicationTests.java @@ -0,0 +1,13 @@ +package it.aboutbits.springboot.emailservice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ApplicationTests { + + @Test + void contextLoads() { + } + +} From eb6bf255104255501c2e7f3f3a77cd9aff591291 Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Thu, 13 Jul 2023 16:51:28 +0200 Subject: [PATCH 02/23] add initial version --- .github/workflows/main.yml | 41 ++++++ checkstyle.xml | 2 +- license.md | 7 + pom.xml | 136 +++++++++-------- readme.md | 85 +++++++++++ .../springboot/emailservice/Application.java | 13 -- .../EmailServiceAutoConfiguration.java | 60 ++++++++ .../lib/AttachmentDataSource.java | 9 ++ .../lib/EmailSchedulerCallback.java | 12 ++ .../emailservice/lib/EmailState.java | 7 + .../lib/application/EmailParameter.java | 62 ++++++++ .../lib/application/ManageEmail.java | 138 ++++++++++++++++++ .../lib/application/QueryEmail.java | 42 ++++++ .../lib/application/SendScheduledEmails.java | 50 +++++++ .../UnavailableAttachmentDataSource.java | 17 +++ .../lib/application/package-info.java | 6 + .../emailservice/lib/jpa/EmailRepository.java | 27 ++++ .../emailservice/lib/model/Email.java | 75 ++++++++++ .../lib/model/EmailAttachment.java | 31 ++++ ...itional-spring-configuration-metadata.json | 16 ++ src/main/resources/application.properties | 1 - src/main/resources/application.yml | 14 ++ .../2023-01-30-create-email-tables.xml | 99 +++++++++++++ .../db/changelog/emailservice/main.xml | 9 ++ .../emailservice/ApplicationTests.java | 13 -- 25 files changed, 883 insertions(+), 89 deletions(-) create mode 100644 .github/workflows/main.yml create mode 100644 license.md delete mode 100644 src/main/java/it/aboutbits/springboot/emailservice/Application.java create mode 100644 src/main/java/it/aboutbits/springboot/emailservice/EmailServiceAutoConfiguration.java create mode 100644 src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentDataSource.java create mode 100644 src/main/java/it/aboutbits/springboot/emailservice/lib/EmailSchedulerCallback.java create mode 100644 src/main/java/it/aboutbits/springboot/emailservice/lib/EmailState.java create mode 100644 src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailParameter.java create mode 100644 src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java create mode 100644 src/main/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmail.java create mode 100644 src/main/java/it/aboutbits/springboot/emailservice/lib/application/SendScheduledEmails.java create mode 100644 src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java create mode 100644 src/main/java/it/aboutbits/springboot/emailservice/lib/application/package-info.java create mode 100644 src/main/java/it/aboutbits/springboot/emailservice/lib/jpa/EmailRepository.java create mode 100644 src/main/java/it/aboutbits/springboot/emailservice/lib/model/Email.java create mode 100644 src/main/java/it/aboutbits/springboot/emailservice/lib/model/EmailAttachment.java create mode 100644 src/main/resources/META-INF/additional-spring-configuration-metadata.json delete mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/db/changelog/emailservice/2023-01-30-create-email-tables.xml create mode 100644 src/main/resources/db/changelog/emailservice/main.xml delete mode 100644 src/test/java/it/aboutbits/springboot/emailservice/ApplicationTests.java diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..b9b965a --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,41 @@ +name: Publish package to GitHub Packages +on: + workflow_dispatch: + inputs: + version: + description: "Version name to publish (eg: x.x.x)" + type: string + required: true + +jobs: + publish: + timeout-minutes: 15 + runs-on: ubuntu-22.04 + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v3 + + - uses: aboutbits/github-actions-java/setup-and-update-dependencies@v1 + + - name: Set Version + run: sed -i 's/BUILD-SNAPSHOT<\/version>/${{ github.event.inputs.version }}<\/version>/g' pom.xml + + - name: Publish package + run: mvn --batch-mode deploy + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + tag: + timeout-minutes: 5 + runs-on: ubuntu-22.04 + needs: publish + steps: + - name: Tag Deployment + uses: aboutbits/github-actions-git/create-or-update-tag@v1 + with: + tag-name: ${{ github.event.inputs.version }} + user-name: 'AboutBits' + user-email: 'info@aboutbits.it' diff --git a/checkstyle.xml b/checkstyle.xml index 2a4ed8d..7bdca34 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -191,7 +191,7 @@ - + diff --git a/license.md b/license.md new file mode 100644 index 0000000..21586f5 --- /dev/null +++ b/license.md @@ -0,0 +1,7 @@ +Copyright About Bits GmbH + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/pom.xml b/pom.xml index 52d764c..82a2957 100644 --- a/pom.xml +++ b/pom.xml @@ -1,67 +1,81 @@ - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.1.1 - - - it.aboutbits - springboot.emailservice - 0.0.1-SNAPSHOT - springboot.emailservice - Spring Boot Email Service - - 17 - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-mail - - - org.liquibase - liquibase-core - + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 - - org.postgresql - postgresql - runtime - - - org.projectlombok - lombok - true - - - org.springframework.boot - spring-boot-starter-test - test - - + + org.springframework.boot + spring-boot-starter-parent + 3.1.1 + + - - - - org.springframework.boot - spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - - - - + it.aboutbits.springboot + emailservice + BUILD-SNAPSHOT + Spring Boot Email Service + + 17 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-mail + + + org.springframework.boot + spring-boot-starter-validation + + + org.liquibase + liquibase-core + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + + + github + GitHub Packages + https://maven.pkg.github.com/aboutbits/spring-boot-sendEmailCommand-service + + diff --git a/readme.md b/readme.md index e69de29..8c14a01 100644 --- a/readme.md +++ b/readme.md @@ -0,0 +1,85 @@ +# Spring Boot EMail Service + +A reusable mailer service to send emails. + +## Setup + +Add the mailer service to the classpath by adding the following maven dependeny: + +```xml + + + it.aboutbits.springboot + emailservice + x.x.x + +``` + +If you want to use attachments, you will have to create a bean implementing this interface: [AttachmentDataSource.java](src%2Fmain%2Fjava%2Fit%2Faboutbits%2Fspringboot%2Femailservice%2Flib%2FAttachmentDataSource.java) +This step is optional. + +## Usage + +Use the `ManageEmail` service to schedule sending an email. + +### Example + +```java +// @formatter:off +public void sendMail(final String to,final String subject,final String htmlBody,final String plainTextBody) { + manageEmail.schedule( + EmailParameter.builder() + .scheduleAt(OffsetDateTime.now()) + .email(EmailParameter.Email.builder() + .fromName(fromName) + .fromAddress(fromAddress) + .recipient(to) + .subject(subject) + .textBody(plainTextBody) + .htmlBody(htmlBody) + .build()) + .build() + ); +} +// @formatter:on +``` + +To read email datasets from the database use this class: [QueryEmail.java](src%2Fmain%2Fjava%2Fit%2Faboutbits%2Fspringboot%2Femailservice%2Flib%2Fapplication%2FQueryEmail.java) + +If you want to receive a report after each run of the scheduler, create a Bean implementing [EmailSchedulerCallback.java](src%2Fmain%2Fjava%2Fit%2Faboutbits%2Fspringboot%2Femailservice%2Flib%2FEmailSchedulerCallback.java) + +## Configuration + +You will have to enable `EntityScanning` for the package as well as your main package. You must also add it to the base packages for Spring to scan. + +```java +@SpringBootApplication(scanBasePackages = {"the.main.package", "it.aboutbits.springboot.emailservice"}) +@EntityScan({"the.main.package", "it.aboutbits.springboot.emailservice"}) +``` + +Available configuration options are: + +```yaml +lib.emailservice.scheduling.enabled: true +lib.emailservice.scheduling.interval: 30000 +``` + +## Building and releasing a new version: + +To create a new version of this package and push it to the maven registry, you will have to use the GitHub actions workflow and manually trigger it. + +## Information + +About Bits is a company based in South Tyrol, Italy. You can find more information about us on [our website](https://aboutbits.it). + +### Support + +For support, please contact [info@aboutbits.it](mailto:info@aboutbits.it). + +### Credits + +- [All Contributors](../../contributors) + +### License + +The MIT License (MIT). Please see the [license file](license.md) for more information. diff --git a/src/main/java/it/aboutbits/springboot/emailservice/Application.java b/src/main/java/it/aboutbits/springboot/emailservice/Application.java deleted file mode 100644 index bd6b234..0000000 --- a/src/main/java/it/aboutbits/springboot/emailservice/Application.java +++ /dev/null @@ -1,13 +0,0 @@ -package it.aboutbits.springboot.emailservice; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class Application { - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } - -} diff --git a/src/main/java/it/aboutbits/springboot/emailservice/EmailServiceAutoConfiguration.java b/src/main/java/it/aboutbits/springboot/emailservice/EmailServiceAutoConfiguration.java new file mode 100644 index 0000000..a7726b7 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/emailservice/EmailServiceAutoConfiguration.java @@ -0,0 +1,60 @@ +package it.aboutbits.springboot.emailservice; + +import it.aboutbits.springboot.emailservice.lib.AttachmentDataSource; +import it.aboutbits.springboot.emailservice.lib.EmailSchedulerCallback; +import it.aboutbits.springboot.emailservice.lib.application.ManageEmail; +import it.aboutbits.springboot.emailservice.lib.application.QueryEmail; +import it.aboutbits.springboot.emailservice.lib.application.SendScheduledEmails; +import it.aboutbits.springboot.emailservice.lib.application.UnavailableAttachmentDataSource; +import it.aboutbits.springboot.emailservice.lib.jpa.EmailRepository; +import jakarta.persistence.EntityManager; +import liquibase.integration.spring.SpringLiquibase; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; +import org.springframework.mail.javamail.JavaMailSender; + +import javax.sql.DataSource; +import java.util.List; + +@Configuration +public class EmailServiceAutoConfiguration { + @Bean(name = "emailServiceLiquibase") + public SpringLiquibase emailServiceLiquibase(DataSource dataSource) { + var liquibase = new SpringLiquibase(); + liquibase.setDataSource(dataSource); + liquibase.setChangeLog("classpath:/db/changelog/emailservice/main.xml"); + liquibase.setShouldRun(true); + return liquibase; + } + + @Bean + public EmailRepository emailRepository(EntityManager entityManager) { + var factory = new JpaRepositoryFactory(entityManager); + return factory.getRepository(EmailRepository.class); + } + + @Bean + public QueryEmail queryEmail(EmailRepository emailRepository) { + return new QueryEmail(emailRepository); + } + + @Bean + public ManageEmail manageEmail(EmailRepository emailRepository, JavaMailSender javaMailSender, AttachmentDataSource attachmentDataSource) { + return new ManageEmail(emailRepository, javaMailSender, attachmentDataSource); + } + + @Bean + @ConditionalOnProperty(value = "lib.emailservice.scheduling.enabled", matchIfMissing = true) + public SendScheduledEmails sendScheduledEmails(QueryEmail queryEmail, ManageEmail manageEmail, List callbacks) { + return new SendScheduledEmails(queryEmail, manageEmail, callbacks); + } + + @Bean + @ConditionalOnMissingBean(AttachmentDataSource.class) + public AttachmentDataSource attachmentDataSource() { + return new UnavailableAttachmentDataSource(); + } +} diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentDataSource.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentDataSource.java new file mode 100644 index 0000000..a95b289 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentDataSource.java @@ -0,0 +1,9 @@ +package it.aboutbits.springboot.emailservice.lib; + +import java.io.InputStream; + +public interface AttachmentDataSource { + InputStream getAttachmentPayload(String reference); + + void releaseAttachment(String reference); +} diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailSchedulerCallback.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailSchedulerCallback.java new file mode 100644 index 0000000..6cba011 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailSchedulerCallback.java @@ -0,0 +1,12 @@ +package it.aboutbits.springboot.emailservice.lib; + +public interface EmailSchedulerCallback { + void report(Report report); + + record Report( + int totalAttempted, + int sent, + int errors + ) { + } +} diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailState.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailState.java new file mode 100644 index 0000000..f053425 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailState.java @@ -0,0 +1,7 @@ +package it.aboutbits.springboot.emailservice.lib; + +public enum EmailState { + PENDING, + SENT, + ERROR +} diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailParameter.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailParameter.java new file mode 100644 index 0000000..320b8cd --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailParameter.java @@ -0,0 +1,62 @@ +package it.aboutbits.springboot.emailservice.lib.application; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import lombok.Builder; +import lombok.NonNull; +import lombok.Singular; +import org.springframework.lang.Nullable; + +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Set; + +@Builder +public record EmailParameter( + @NonNull OffsetDateTime scheduleAt, + + @NonNull Email email, + + @Nullable String reference +) { + + @Builder + public record Email( + @NonNull + @NotBlank + String subject, + + @Singular + @NonNull + @NotEmpty + List recipients, + + @NonNull String textBody, + @NonNull String htmlBody, + + @NonNull + @NotBlank + String fromAddress, + @NonNull + @NotBlank + String fromName, + + @Singular + @NonNull + Set attachments + ) { + @Builder + public record Attachment( + @NonNull + @NotBlank + String reference, + @NonNull + @NotBlank + String fileName, + @NonNull + @NotBlank + String contentType + ) { + } + } +} diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java new file mode 100644 index 0000000..73f24ab --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java @@ -0,0 +1,138 @@ +package it.aboutbits.springboot.emailservice.lib.application; + + +import it.aboutbits.springboot.emailservice.lib.AttachmentDataSource; +import it.aboutbits.springboot.emailservice.lib.EmailState; +import it.aboutbits.springboot.emailservice.lib.jpa.EmailRepository; +import it.aboutbits.springboot.emailservice.lib.model.Email; +import it.aboutbits.springboot.emailservice.lib.model.EmailAttachment; +import jakarta.mail.MessagingException; +import jakarta.validation.Valid; +import lombok.NonNull; +import lombok.extern.log4j.Log4j2; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.validation.annotation.Validated; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + + +@Validated +@Log4j2 +public class ManageEmail { + private final EmailRepository emailNotificationRepository; + private final JavaMailSender mailSender; + private final AttachmentDataSource attachmentDataSource; + + public ManageEmail( + EmailRepository emailNotificationRepository, + JavaMailSender mailSender, + AttachmentDataSource attachmentDataSource + ) { + + this.emailNotificationRepository = emailNotificationRepository; + this.mailSender = mailSender; + this.attachmentDataSource = attachmentDataSource; + } + + public Email schedule(@NonNull @Valid EmailParameter parameter) { + var emailData = parameter.email(); + + var email = new Email(); + email.setState(EmailState.PENDING); + email.setSendingScheduledAt(parameter.scheduleAt()); + email.setReference(parameter.reference()); + email.setSubject(emailData.subject()); + email.setTextBody(emailData.textBody()); + email.setHtmlBody(emailData.htmlBody()); + email.setRecipients(emailData.recipients()); + email.setFromAddress(emailData.fromAddress()); + email.setFromName(emailData.fromName()); + email.setAttachments(emailData.attachments().stream() + .map(a -> { + var attachment = new EmailAttachment(); + attachment.setReference(a.reference()); + attachment.setContentType(a.contentType()); + attachment.setFileName(a.fileName()); + return attachment; + }) + .collect(Collectors.toSet()) + ); + + email = emailNotificationRepository.save(email); + + return email; + } + + Email send(Email notification) { + if (EmailState.SENT.equals(notification.getState())) { + return notification; + } + + try { + sendMail(notification); + notification.setState(EmailState.SENT); + notification.setErrorMessage(""); + notification.setSentAt(OffsetDateTime.now()); + } catch (MessagingException | IOException e) { + log.error("Failed to send email", e); + notification.setErrorMessage(e.getMessage()); + notification.setState(EmailState.ERROR); + } + + var updatedNotification = emailNotificationRepository.save(notification); + + if (updatedNotification.isSent()) { + cleanupAttachments(notification); + } + + return updatedNotification; + } + + private void cleanupAttachments(final Email notification) { + notification.getAttachments().forEach(a -> attachmentDataSource.releaseAttachment(a.getReference())); + } + + + private void sendMail(Email notification) throws MessagingException, IOException { + sendMail( + notification.getFromAddress(), + notification.getFromName(), + notification.getRecipients(), + notification.getSubject(), + notification.getHtmlBody(), + notification.getTextBody(), + notification.getAttachments() + ); + } + + private void sendMail(String fromAddress, String fromName, List recipients, String subject, String htmlBody, String plainTextBody, Set attachments) throws MessagingException, IOException { + var message = mailSender.createMimeMessage(); + var helper = new MimeMessageHelper(message, true, "UTF-8"); + + helper.setFrom(fromAddress, fromName); + helper.setTo(recipients.toArray(String[]::new)); + helper.setSubject(subject); + + if (!htmlBody.isBlank()) { + helper.setText(plainTextBody, htmlBody); + } else { + helper.setText(plainTextBody); + } + + if (!attachments.isEmpty()) { + for (var attachment : attachments) { + var payload = attachmentDataSource.getAttachmentPayload(attachment.getReference()); + helper.addAttachment(attachment.getFileName(), new ByteArrayResource(payload.readAllBytes())); + payload.close(); + } + } + + mailSender.send(message); + } +} diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmail.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmail.java new file mode 100644 index 0000000..a315ca5 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmail.java @@ -0,0 +1,42 @@ +package it.aboutbits.springboot.emailservice.lib.application; + + +import it.aboutbits.springboot.emailservice.lib.EmailState; +import it.aboutbits.springboot.emailservice.lib.jpa.EmailRepository; +import it.aboutbits.springboot.emailservice.lib.model.Email; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; + +import java.time.OffsetDateTime; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +@RequiredArgsConstructor +public class QueryEmail { + private final EmailRepository emailNotificationRepository; + + public Page paginatedByState(final EmailState state, final PageRequest pageParameter) { + var pageRequest = PageRequest.of(pageParameter.getPageNumber(), pageParameter.getPageSize(), Sort.by("updatedAt")); + + return emailNotificationRepository.findByState(state, pageRequest); + } + + public List byIds(final Collection ids) { + if (ids.isEmpty()) { + return Collections.emptyList(); + } + return emailNotificationRepository.findByIdIn(ids); + } + + public List readyToSend() { + return emailNotificationRepository.findReadyToSend(OffsetDateTime.now()); + } + + public Optional byId(final long id) { + return emailNotificationRepository.findById(id); + } +} diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/SendScheduledEmails.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/SendScheduledEmails.java new file mode 100644 index 0000000..fbe5fba --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/SendScheduledEmails.java @@ -0,0 +1,50 @@ +package it.aboutbits.springboot.emailservice.lib.application; + + +import it.aboutbits.springboot.emailservice.lib.EmailSchedulerCallback; +import lombok.AllArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.scheduling.annotation.Scheduled; + +import java.util.List; + +@AllArgsConstructor +@Log4j2 +public class SendScheduledEmails { + private static final String JOB_DESCRIPTION = "Sending open and failed email notifications."; + + private final QueryEmail queryEmail; + private final ManageEmail manageEmail; + private final List callbacks; + + @Scheduled(initialDelayString = "${lib.emailservice.scheduling.interval:30000}", fixedDelayString = "${lib.emailservice.scheduling.interval:30000}") + void sendEmailNotifications() { + log.info("Start: " + JOB_DESCRIPTION); + + var emailsToSend = queryEmail.readyToSend(); + + var countSent = 0; + var countError = 0; + for (var email : emailsToSend) { + var updatedEmail = manageEmail.send(email); + switch (updatedEmail.getState()) { + case ERROR -> countError++; + case SENT -> countSent++; + default -> log.warn( + "Email job produced an invalid notification result state: {}.", + updatedEmail.getState().name()); + } + } + + log.info("Finished: " + JOB_DESCRIPTION); + log.info("Sent: {}, Errors: {}", countSent, countError); + + for (var callback : callbacks) { + callback.report(new EmailSchedulerCallback.Report( + emailsToSend.size(), + countSent, + countError + )); + } + } +} diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java new file mode 100644 index 0000000..3308e68 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java @@ -0,0 +1,17 @@ +package it.aboutbits.springboot.emailservice.lib.application; + +import it.aboutbits.springboot.emailservice.lib.AttachmentDataSource; + +import java.io.InputStream; + +public final class UnavailableAttachmentDataSource implements AttachmentDataSource { + @Override + public InputStream getAttachmentPayload(final String reference) { + throw new RuntimeException("attachments not available"); + } + + @Override + public void releaseAttachment(final String reference) { + throw new RuntimeException("attachments not available"); + } +} diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/package-info.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/package-info.java new file mode 100644 index 0000000..20c8675 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/package-info.java @@ -0,0 +1,6 @@ +@NonNullApi +@NonNullFields +package it.aboutbits.springboot.emailservice.lib.application; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/jpa/EmailRepository.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/jpa/EmailRepository.java new file mode 100644 index 0000000..b2efee1 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/jpa/EmailRepository.java @@ -0,0 +1,27 @@ +package it.aboutbits.springboot.emailservice.lib.jpa; + + +import it.aboutbits.springboot.emailservice.lib.EmailState; +import it.aboutbits.springboot.emailservice.lib.model.Email; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.time.OffsetDateTime; +import java.util.Collection; +import java.util.List; + +public interface EmailRepository extends JpaRepository { + Page findByState(EmailState state, PageRequest pageRequest); + + @Query(""" + SELECT e from Email e WHERE e.sendingScheduledAt < :scheduledBefore AND e.state IN ( + it.aboutbits.springboot.emailservice.lib.EmailState.PENDING, + it.aboutbits.springboot.emailservice.lib.EmailState.ERROR + ) + """) + List findReadyToSend(OffsetDateTime scheduledBefore); + + List findByIdIn(Collection ids); +} diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/model/Email.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/model/Email.java new file mode 100644 index 0000000..030fd7e --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/model/Email.java @@ -0,0 +1,75 @@ +package it.aboutbits.springboot.emailservice.lib.model; + +import it.aboutbits.springboot.emailservice.lib.EmailState; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.NamedAttributeNode; +import jakarta.persistence.NamedEntityGraph; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.annotations.UpdateTimestamp; +import org.hibernate.type.SqlTypes; + +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Set; + +@NamedEntityGraph( + name = "email_service_emails-entity-graph", + attributeNodes = { + @NamedAttributeNode("recipients") + } +) +@Entity +@Getter +@Setter +@Table(name = "email_service_emails") +public class Email { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + private EmailState state; + + private String subject; + + private String fromAddress; + private String fromName; + + @JdbcTypeCode(SqlTypes.JSON) + private List recipients; + + private String textBody; + private String htmlBody; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "email", orphanRemoval = true) + private Set attachments; + + private String reference; + + private OffsetDateTime sendingScheduledAt; + private OffsetDateTime sentAt; + + private OffsetDateTime errorAt; + private String errorMessage; + + @CreationTimestamp + private OffsetDateTime createdAt; + + @UpdateTimestamp + private OffsetDateTime updatedAt; + + public boolean isSent() { + return EmailState.SENT.equals(state); + } +} diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/model/EmailAttachment.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/model/EmailAttachment.java new file mode 100644 index 0000000..021784a --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/model/EmailAttachment.java @@ -0,0 +1,31 @@ +package it.aboutbits.springboot.emailservice.lib.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Getter +@Setter +@Table(name = "email_service_email_attachments") +public class EmailAttachment { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "email_id") + private Email email; + + private String fileName; + + private String contentType; + + private String reference; +} diff --git a/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 0000000..128d011 --- /dev/null +++ b/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,16 @@ +{ + "properties": [ + { + "name": "lib.emailservice.scheduling.enabled", + "type": "java.lang.Boolean", + "description": "Enables the scheduler sending the emails.", + "defaultValue": true + }, + { + "name": "lib.emailservice.scheduling.interval", + "type": "java.lang.Long", + "description": "Specifies the milliseconds delay between runs of the scheduler.", + "defaultValue": 30000 + } + ] +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 8b13789..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..3ece23f --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,14 @@ +spring: + mail: + default-encoding: UTF-8 + host: ${MAIL_HOST:localhost} + username: ${MAIL_USERNAME:null} + password: ${MAIL_PASSWORD:null} + port: ${MAIL_PORT:1025} + protocol: smtp + properties: + mail: + smtp: + auth: ${MAIL_SSL:false} + starttls: + enable: ${MAIL_SSL:false} diff --git a/src/main/resources/db/changelog/emailservice/2023-01-30-create-email-tables.xml b/src/main/resources/db/changelog/emailservice/2023-01-30-create-email-tables.xml new file mode 100644 index 0000000..93bec1f --- /dev/null +++ b/src/main/resources/db/changelog/emailservice/2023-01-30-create-email-tables.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ALTER TABLE email_service_emails + ADD CONSTRAINT email_service_emails_one_body_not_empty CHECK ( + (text_body != '') + OR (html_body != '') + ) + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/db/changelog/emailservice/main.xml b/src/main/resources/db/changelog/emailservice/main.xml new file mode 100644 index 0000000..271ffbc --- /dev/null +++ b/src/main/resources/db/changelog/emailservice/main.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/src/test/java/it/aboutbits/springboot/emailservice/ApplicationTests.java b/src/test/java/it/aboutbits/springboot/emailservice/ApplicationTests.java deleted file mode 100644 index 6fb02a5..0000000 --- a/src/test/java/it/aboutbits/springboot/emailservice/ApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package it.aboutbits.springboot.emailservice; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class ApplicationTests { - - @Test - void contextLoads() { - } - -} From 6188b1d4f4dcf02d07fa34c8d50727b446a89e81 Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Thu, 13 Jul 2023 16:56:52 +0200 Subject: [PATCH 03/23] add missing mvnw --- .mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .mvn/wrapper/maven-wrapper.properties | 2 ++ 2 files changed, 2 insertions(+) create mode 100644 .mvn/wrapper/maven-wrapper.jar create mode 100644 .mvn/wrapper/maven-wrapper.properties diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..462686e --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.3/apache-maven-3.9.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar From f92b598ff3d5d2d3d466a70a4b898e8e27c3a37f Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Thu, 13 Jul 2023 17:01:05 +0200 Subject: [PATCH 04/23] fix pom --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 82a2957..7d49dd0 100644 --- a/pom.xml +++ b/pom.xml @@ -75,7 +75,7 @@ github GitHub Packages - https://maven.pkg.github.com/aboutbits/spring-boot-sendEmailCommand-service + https://maven.pkg.github.com/aboutbits/spring-boot-email-service From 19dc1c3698a862564cec778c0108ee273cd5554b Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Thu, 13 Jul 2023 17:03:19 +0200 Subject: [PATCH 05/23] fix tagging --- .github/workflows/main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b9b965a..1408f81 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -33,6 +33,8 @@ jobs: runs-on: ubuntu-22.04 needs: publish steps: + - uses: actions/checkout@v3 + - name: Tag Deployment uses: aboutbits/github-actions-git/create-or-update-tag@v1 with: From 57517be1bbf860c4b05c759bdaad09a04c76b9b2 Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Fri, 14 Jul 2023 10:55:39 +0200 Subject: [PATCH 06/23] update workflow to use renamed action --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1408f81..36b83bc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,7 +18,7 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: aboutbits/github-actions-java/setup-and-update-dependencies@v1 + - uses: aboutbits/github-actions-java/setup-and-install@v1 - name: Set Version run: sed -i 's/BUILD-SNAPSHOT<\/version>/${{ github.event.inputs.version }}<\/version>/g' pom.xml From c1affa71bf89ae947bfbc9e2764aa8debc439e35 Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Fri, 14 Jul 2023 12:49:49 +0200 Subject: [PATCH 07/23] update readme --- readme.md | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/readme.md b/readme.md index 8c14a01..1c9d0f6 100644 --- a/readme.md +++ b/readme.md @@ -4,7 +4,7 @@ A reusable mailer service to send emails. ## Setup -Add the mailer service to the classpath by adding the following maven dependeny: +Add the mailer service to the classpath by adding the following maven dependency. Versions haven be found [here](../../packages) ```xml @@ -15,14 +15,18 @@ Add the mailer service to the classpath by adding the following maven dependeny: ``` +### Attachments + If you want to use attachments, you will have to create a bean implementing this interface: [AttachmentDataSource.java](src%2Fmain%2Fjava%2Fit%2Faboutbits%2Fspringboot%2Femailservice%2Flib%2FAttachmentDataSource.java) This step is optional. ## Usage -Use the `ManageEmail` service to schedule sending an email. +### Sending an Email + +Use the `ManageEmail` service to schedule an email. -### Example +#### Example ```java // @formatter:off @@ -44,11 +48,15 @@ public void sendMail(final String to,final String subject,final String htmlBody, // @formatter:on ``` +### Querying Emails + To read email datasets from the database use this class: [QueryEmail.java](src%2Fmain%2Fjava%2Fit%2Faboutbits%2Fspringboot%2Femailservice%2Flib%2Fapplication%2FQueryEmail.java) +### Reporting callback + If you want to receive a report after each run of the scheduler, create a Bean implementing [EmailSchedulerCallback.java](src%2Fmain%2Fjava%2Fit%2Faboutbits%2Fspringboot%2Femailservice%2Flib%2FEmailSchedulerCallback.java) -## Configuration +### Configuration You will have to enable `EntityScanning` for the package as well as your main package. You must also add it to the base packages for Spring to scan. @@ -57,12 +65,12 @@ You will have to enable `EntityScanning` for the package as well as your main pa @EntityScan({"the.main.package", "it.aboutbits.springboot.emailservice"}) ``` -Available configuration options are: +The following configuration options are available: -```yaml -lib.emailservice.scheduling.enabled: true -lib.emailservice.scheduling.interval: 30000 -``` +| Name | Default | Description | +|--------------------------------------------|-------------|-------------------------------------------------------------------------| +| `lib.emailservice.scheduling.enabled` | true | Enables the scheduler sending the emails. | +| `lib.emailservice.scheduling.interval` | 30000 | Specifies the milliseconds delay between runs of the scheduler. | ## Building and releasing a new version: From d4df49fce7fcf17304feb87c0d93e1c6c057421f Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Fri, 14 Jul 2023 18:04:26 +0200 Subject: [PATCH 08/23] update for better usage --- pom.xml | 73 +++++++- readme.md | 22 ++- ...on.java => EmailServiceConfiguration.java} | 36 ++-- .../emailservice/EnableEmailService.java | 14 ++ .../lib/AttachmentDataSource.java | 6 +- .../emailservice/lib/EmailAttachmentDto.java | 16 ++ .../springboot/emailservice/lib/EmailDto.java | 35 ++++ .../application/EmailAttachmentMapper.java | 14 ++ .../lib/application/EmailMapper.java | 24 +++ .../lib/application/ManageEmail.java | 65 ++++--- .../lib/application/QueryEmail.java | 36 +++- .../UnavailableAttachmentDataSource.java | 9 +- .../lib/exception/AttachmentException.java | 23 +++ .../emailservice/lib/jpa/EmailRepository.java | 10 +- .../emailservice/lib/model/Email.java | 5 +- .../emailservice/lib/package-info.java | 6 + ...itional-spring-configuration-metadata.json | 6 + .../2023-01-30-create-email-tables.xml | 99 ----------- .../2023-01-30-create-email-tables.yml | 160 ++++++++++++++++++ .../db/changelog/emailservice/main.xml | 9 - .../db/changelog/emailservice/main.yml | 6 + 21 files changed, 483 insertions(+), 191 deletions(-) rename src/main/java/it/aboutbits/springboot/emailservice/{EmailServiceAutoConfiguration.java => EmailServiceConfiguration.java} (65%) create mode 100644 src/main/java/it/aboutbits/springboot/emailservice/EnableEmailService.java create mode 100644 src/main/java/it/aboutbits/springboot/emailservice/lib/EmailAttachmentDto.java create mode 100644 src/main/java/it/aboutbits/springboot/emailservice/lib/EmailDto.java create mode 100644 src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailAttachmentMapper.java create mode 100644 src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailMapper.java create mode 100644 src/main/java/it/aboutbits/springboot/emailservice/lib/exception/AttachmentException.java create mode 100644 src/main/java/it/aboutbits/springboot/emailservice/lib/package-info.java delete mode 100644 src/main/resources/db/changelog/emailservice/2023-01-30-create-email-tables.xml create mode 100644 src/main/resources/db/changelog/emailservice/2023-01-30-create-email-tables.yml delete mode 100644 src/main/resources/db/changelog/emailservice/main.xml create mode 100644 src/main/resources/db/changelog/emailservice/main.yml diff --git a/pom.xml b/pom.xml index 7d49dd0..2cfa401 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,7 @@ 17 + 1.5.3.Final @@ -51,25 +52,79 @@ spring-boot-configuration-processor true + + org.mapstruct + mapstruct + ${mapstruct.version} + - + true + true + false + + + + validate + validate + + check + + + - <!– overwrite dependency on spotbugs if you want to specify the version of spotbugs –> - com.github.spotbugs - spotbugs - 4.5.3 + com.puppycrawl.tools + checkstyle + 10.3.4 - --> + diff --git a/readme.md b/readme.md index 1c9d0f6..5c9c171 100644 --- a/readme.md +++ b/readme.md @@ -58,19 +58,27 @@ If you want to receive a report after each run of the scheduler, create a Bean i ### Configuration -You will have to enable `EntityScanning` for the package as well as your main package. You must also add it to the base packages for Spring to scan. +To enable this service just add `@EnableEmailService` to your main class. You must also enable `@EnableScheduling` to allow the email queue to be processed. ```java -@SpringBootApplication(scanBasePackages = {"the.main.package", "it.aboutbits.springboot.emailservice"}) -@EntityScan({"the.main.package", "it.aboutbits.springboot.emailservice"}) + +@SpringBootApplication +@EnableEmailService +@EnableScheduling +public class App { + public static void main(final String[] args) { + SpringApplication.run(App.class, args); + } +} ``` The following configuration options are available: -| Name | Default | Description | -|--------------------------------------------|-------------|-------------------------------------------------------------------------| -| `lib.emailservice.scheduling.enabled` | true | Enables the scheduler sending the emails. | -| `lib.emailservice.scheduling.interval` | 30000 | Specifies the milliseconds delay between runs of the scheduler. | +| Name | Default | Description | +|--------------------------------------------|-------------|--------------------------------------------------------------------------------------------------------------------------| +| `lib.emailservice.liquibase.enabled` | true | Enables liquibase migrations. **attention** `classpath:db/changelog/master.yml` will be run first and must be present! | +| `lib.emailservice.scheduling.enabled` | true | Enables the scheduler sending the emails. | +| `lib.emailservice.scheduling.interval` | 30000 | Specifies the milliseconds delay between runs of the scheduler. | ## Building and releasing a new version: diff --git a/src/main/java/it/aboutbits/springboot/emailservice/EmailServiceAutoConfiguration.java b/src/main/java/it/aboutbits/springboot/emailservice/EmailServiceConfiguration.java similarity index 65% rename from src/main/java/it/aboutbits/springboot/emailservice/EmailServiceAutoConfiguration.java rename to src/main/java/it/aboutbits/springboot/emailservice/EmailServiceConfiguration.java index a7726b7..e7363c6 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/EmailServiceAutoConfiguration.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/EmailServiceConfiguration.java @@ -2,6 +2,10 @@ import it.aboutbits.springboot.emailservice.lib.AttachmentDataSource; import it.aboutbits.springboot.emailservice.lib.EmailSchedulerCallback; +import it.aboutbits.springboot.emailservice.lib.application.EmailAttachmentMapper; +import it.aboutbits.springboot.emailservice.lib.application.EmailAttachmentMapperImpl; +import it.aboutbits.springboot.emailservice.lib.application.EmailMapper; +import it.aboutbits.springboot.emailservice.lib.application.EmailMapperImpl; import it.aboutbits.springboot.emailservice.lib.application.ManageEmail; import it.aboutbits.springboot.emailservice.lib.application.QueryEmail; import it.aboutbits.springboot.emailservice.lib.application.SendScheduledEmails; @@ -9,41 +13,45 @@ import it.aboutbits.springboot.emailservice.lib.jpa.EmailRepository; import jakarta.persistence.EntityManager; import liquibase.integration.spring.SpringLiquibase; +import org.springframework.boot.autoconfigure.AutoConfigurationPackage; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; import org.springframework.mail.javamail.JavaMailSender; import javax.sql.DataSource; import java.util.List; -@Configuration -public class EmailServiceAutoConfiguration { - @Bean(name = "emailServiceLiquibase") - public SpringLiquibase emailServiceLiquibase(DataSource dataSource) { +@AutoConfigurationPackage +public class EmailServiceConfiguration { + @Bean + @ConditionalOnProperty(value = "lib.emailservice.liquibase.enabled", matchIfMissing = true) + public SpringLiquibase springLiquibase(DataSource dataSource) { var liquibase = new SpringLiquibase(); liquibase.setDataSource(dataSource); - liquibase.setChangeLog("classpath:/db/changelog/emailservice/main.xml"); + liquibase.setChangeLog("classpath:/db/changelog/emailservice/main.yml"); liquibase.setShouldRun(true); return liquibase; } @Bean - public EmailRepository emailRepository(EntityManager entityManager) { - var factory = new JpaRepositoryFactory(entityManager); - return factory.getRepository(EmailRepository.class); + public EmailAttachmentMapper emailAttachmentMapper() { + return new EmailAttachmentMapperImpl(); + } + + @Bean + public EmailMapper emailMapper() { + return new EmailMapperImpl(); } @Bean - public QueryEmail queryEmail(EmailRepository emailRepository) { - return new QueryEmail(emailRepository); + public QueryEmail queryEmail(EmailRepository emailRepository, EmailMapper emailMapper, EntityManager entityManager) { + return new QueryEmail(emailRepository, emailMapper, entityManager); } @Bean - public ManageEmail manageEmail(EmailRepository emailRepository, JavaMailSender javaMailSender, AttachmentDataSource attachmentDataSource) { - return new ManageEmail(emailRepository, javaMailSender, attachmentDataSource); + public ManageEmail manageEmail(EmailRepository emailRepository, JavaMailSender javaMailSender, AttachmentDataSource attachmentDataSource, EmailMapper emailMapper) { + return new ManageEmail(emailRepository, javaMailSender, attachmentDataSource, emailMapper); } @Bean diff --git a/src/main/java/it/aboutbits/springboot/emailservice/EnableEmailService.java b/src/main/java/it/aboutbits/springboot/emailservice/EnableEmailService.java new file mode 100644 index 0000000..eb9f9bc --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/emailservice/EnableEmailService.java @@ -0,0 +1,14 @@ +package it.aboutbits.springboot.emailservice; + +import org.springframework.context.annotation.Import; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Import({EmailServiceConfiguration.class}) +public @interface EnableEmailService { +} diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentDataSource.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentDataSource.java index a95b289..c4b16b6 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentDataSource.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentDataSource.java @@ -1,9 +1,11 @@ package it.aboutbits.springboot.emailservice.lib; +import it.aboutbits.springboot.emailservice.lib.exception.AttachmentException; + import java.io.InputStream; public interface AttachmentDataSource { - InputStream getAttachmentPayload(String reference); + InputStream getAttachmentPayload(String reference) throws AttachmentException; - void releaseAttachment(String reference); + void releaseAttachment(String reference) throws AttachmentException; } diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailAttachmentDto.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailAttachmentDto.java new file mode 100644 index 0000000..435d194 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailAttachmentDto.java @@ -0,0 +1,16 @@ +package it.aboutbits.springboot.emailservice.lib; + +import it.aboutbits.springboot.emailservice.lib.model.Email; + +public record EmailAttachmentDto( + long id, + + Email email, + + String fileName, + + String contentType, + + String reference +) { +} diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailDto.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailDto.java new file mode 100644 index 0000000..fe3adb4 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailDto.java @@ -0,0 +1,35 @@ +package it.aboutbits.springboot.emailservice.lib; + +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Set; + +public record EmailDto( + long id, + + EmailState state, + + String subject, + + String fromAddress, + String fromName, + + List recipients, + + String textBody, + String htmlBody, + + Set attachments, + + String reference, + + OffsetDateTime sendingScheduledAt, + OffsetDateTime sentAt, + + OffsetDateTime errorAt, + String errorMessage, + + OffsetDateTime createdAt, + OffsetDateTime updatedAt +) { +} diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailAttachmentMapper.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailAttachmentMapper.java new file mode 100644 index 0000000..e8d572a --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailAttachmentMapper.java @@ -0,0 +1,14 @@ +package it.aboutbits.springboot.emailservice.lib.application; + +import it.aboutbits.springboot.emailservice.lib.EmailAttachmentDto; +import it.aboutbits.springboot.emailservice.lib.model.EmailAttachment; +import org.mapstruct.Mapper; + +import java.util.List; + +@Mapper +public interface EmailAttachmentMapper { + EmailAttachmentDto toDto(EmailAttachment model); + + List toDto(List model); +} diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailMapper.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailMapper.java new file mode 100644 index 0000000..c5aa008 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailMapper.java @@ -0,0 +1,24 @@ +package it.aboutbits.springboot.emailservice.lib.application; + +import it.aboutbits.springboot.emailservice.lib.EmailDto; +import it.aboutbits.springboot.emailservice.lib.model.Email; +import org.mapstruct.Mapper; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; + +import java.util.List; + +@Mapper(uses = EmailAttachmentMapper.class) +public interface EmailMapper { + EmailDto toDto(Email model); + + List toDto(List model); + + default Page toDto(Page model) { + return new PageImpl<>( + toDto(model.getContent()), + model.getPageable(), + model.getTotalElements() + ); + } +} diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java index 73f24ab..df6d093 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java @@ -2,7 +2,9 @@ import it.aboutbits.springboot.emailservice.lib.AttachmentDataSource; +import it.aboutbits.springboot.emailservice.lib.EmailDto; import it.aboutbits.springboot.emailservice.lib.EmailState; +import it.aboutbits.springboot.emailservice.lib.exception.AttachmentException; import it.aboutbits.springboot.emailservice.lib.jpa.EmailRepository; import it.aboutbits.springboot.emailservice.lib.model.Email; import it.aboutbits.springboot.emailservice.lib.model.EmailAttachment; @@ -25,25 +27,27 @@ @Validated @Log4j2 public class ManageEmail { - private final EmailRepository emailNotificationRepository; + private final EmailRepository emailRepository; private final JavaMailSender mailSender; private final AttachmentDataSource attachmentDataSource; + private final EmailMapper emailMapper; public ManageEmail( - EmailRepository emailNotificationRepository, + EmailRepository emailRepository, JavaMailSender mailSender, - AttachmentDataSource attachmentDataSource - ) { + AttachmentDataSource attachmentDataSource, + final EmailMapper emailMapper) { - this.emailNotificationRepository = emailNotificationRepository; + this.emailRepository = emailRepository; this.mailSender = mailSender; this.attachmentDataSource = attachmentDataSource; + this.emailMapper = emailMapper; } - public Email schedule(@NonNull @Valid EmailParameter parameter) { + public EmailDto schedule(@NonNull @Valid EmailParameter parameter) { var emailData = parameter.email(); - var email = new Email(); + final var email = new Email(); email.setState(EmailState.PENDING); email.setSendingScheduledAt(parameter.scheduleAt()); email.setReference(parameter.reference()); @@ -56,6 +60,7 @@ public Email schedule(@NonNull @Valid EmailParameter parameter) { email.setAttachments(emailData.attachments().stream() .map(a -> { var attachment = new EmailAttachment(); + attachment.setEmail(email); attachment.setReference(a.reference()); attachment.setContentType(a.contentType()); attachment.setFileName(a.fileName()); @@ -64,42 +69,48 @@ public Email schedule(@NonNull @Valid EmailParameter parameter) { .collect(Collectors.toSet()) ); - email = emailNotificationRepository.save(email); + var savedEmail = emailRepository.save(email); - return email; + return emailMapper.toDto(savedEmail); } - Email send(Email notification) { - if (EmailState.SENT.equals(notification.getState())) { - return notification; + Email send(Email email) { + if (EmailState.SENT.equals(email.getState())) { + return email; } try { - sendMail(notification); - notification.setState(EmailState.SENT); - notification.setErrorMessage(""); - notification.setSentAt(OffsetDateTime.now()); - } catch (MessagingException | IOException e) { + sendMail(email); + email.setState(EmailState.SENT); + email.setErrorMessage(""); + email.setSentAt(OffsetDateTime.now()); + } catch (MessagingException | AttachmentException | IOException e) { log.error("Failed to send email", e); - notification.setErrorMessage(e.getMessage()); - notification.setState(EmailState.ERROR); + email.setErrorMessage(e.getMessage()); + email.setState(EmailState.ERROR); } - var updatedNotification = emailNotificationRepository.save(notification); + var updatedEmail = emailRepository.save(email); - if (updatedNotification.isSent()) { - cleanupAttachments(notification); + if (updatedEmail.isSent()) { + try { + cleanupAttachments(email); + } catch (AttachmentException e) { + throw new IllegalStateException(e); + } } - return updatedNotification; + return updatedEmail; } - private void cleanupAttachments(final Email notification) { - notification.getAttachments().forEach(a -> attachmentDataSource.releaseAttachment(a.getReference())); + private void cleanupAttachments(final Email email) throws AttachmentException { + for (var attachment : email.getAttachments()) { + attachmentDataSource.releaseAttachment(attachment.getReference()); + } } - private void sendMail(Email notification) throws MessagingException, IOException { + private void sendMail(Email notification) throws MessagingException, IOException, AttachmentException { sendMail( notification.getFromAddress(), notification.getFromName(), @@ -111,7 +122,7 @@ private void sendMail(Email notification) throws MessagingException, IOException ); } - private void sendMail(String fromAddress, String fromName, List recipients, String subject, String htmlBody, String plainTextBody, Set attachments) throws MessagingException, IOException { + private void sendMail(String fromAddress, String fromName, List recipients, String subject, String htmlBody, String plainTextBody, Set attachments) throws MessagingException, IOException, AttachmentException { var message = mailSender.createMimeMessage(); var helper = new MimeMessageHelper(message, true, "UTF-8"); diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmail.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmail.java index a315ca5..7f63231 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmail.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmail.java @@ -1,9 +1,11 @@ package it.aboutbits.springboot.emailservice.lib.application; +import it.aboutbits.springboot.emailservice.lib.EmailDto; import it.aboutbits.springboot.emailservice.lib.EmailState; import it.aboutbits.springboot.emailservice.lib.jpa.EmailRepository; import it.aboutbits.springboot.emailservice.lib.model.Email; +import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -17,26 +19,42 @@ @RequiredArgsConstructor public class QueryEmail { - private final EmailRepository emailNotificationRepository; + private final EmailRepository emailRepository; + private final EmailMapper emailMapper; + private final EntityManager entityManager; - public Page paginatedByState(final EmailState state, final PageRequest pageParameter) { + public Page paginatedByState(final EmailState state, final PageRequest pageParameter) { var pageRequest = PageRequest.of(pageParameter.getPageNumber(), pageParameter.getPageSize(), Sort.by("updatedAt")); - return emailNotificationRepository.findByState(state, pageRequest); + return emailMapper.toDto(emailRepository.findByState(state, pageRequest)); } - public List byIds(final Collection ids) { + public List byIds(final Collection ids) { if (ids.isEmpty()) { return Collections.emptyList(); } - return emailNotificationRepository.findByIdIn(ids); + return emailMapper.toDto(emailRepository.findByIdIn(ids)); } - public List readyToSend() { - return emailNotificationRepository.findReadyToSend(OffsetDateTime.now()); + List readyToSend() { + var entityGraph = entityManager.getEntityGraph("email_service_emails-entity-graph"); + return entityManager.createQuery(""" + SELECT e from Email e WHERE e.sendingScheduledAt < :scheduledBefore AND e.state IN ( + it.aboutbits.springboot.emailservice.lib.EmailState.PENDING, + it.aboutbits.springboot.emailservice.lib.EmailState.ERROR + ) + """, Email.class) + .setParameter("scheduledBefore", OffsetDateTime.now()) + .setHint("jakarta.persistence.fetchgraph", entityGraph) + .getResultList(); + //return emailRepository.findReadyToSend(OffsetDateTime.now()); } - public Optional byId(final long id) { - return emailNotificationRepository.findById(id); + public Optional byId(final long id) { + return emailRepository.findById(id).map(emailMapper::toDto); + } + + public List byReference(final String reference) { + return emailMapper.toDto(emailRepository.findByReference(reference)); } } diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java index 3308e68..7960e57 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java @@ -1,17 +1,18 @@ package it.aboutbits.springboot.emailservice.lib.application; import it.aboutbits.springboot.emailservice.lib.AttachmentDataSource; +import it.aboutbits.springboot.emailservice.lib.exception.AttachmentException; import java.io.InputStream; public final class UnavailableAttachmentDataSource implements AttachmentDataSource { @Override - public InputStream getAttachmentPayload(final String reference) { - throw new RuntimeException("attachments not available"); + public InputStream getAttachmentPayload(final String reference) throws AttachmentException { + throw new AttachmentException("attachments not available"); } @Override - public void releaseAttachment(final String reference) { - throw new RuntimeException("attachments not available"); + public void releaseAttachment(final String reference) throws AttachmentException { + throw new AttachmentException("attachments not available"); } } diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/exception/AttachmentException.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/exception/AttachmentException.java new file mode 100644 index 0000000..f836e33 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/exception/AttachmentException.java @@ -0,0 +1,23 @@ +package it.aboutbits.springboot.emailservice.lib.exception; + +public class AttachmentException extends Exception { + public AttachmentException() { + super(); + } + + public AttachmentException(final String message) { + super(message); + } + + public AttachmentException(final String message, final Throwable cause) { + super(message, cause); + } + + public AttachmentException(final Throwable cause) { + super(cause); + } + + protected AttachmentException(final String message, final Throwable cause, final boolean enableSuppression, final boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/jpa/EmailRepository.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/jpa/EmailRepository.java index b2efee1..d939953 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/jpa/EmailRepository.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/jpa/EmailRepository.java @@ -6,22 +6,14 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import java.time.OffsetDateTime; import java.util.Collection; import java.util.List; public interface EmailRepository extends JpaRepository { Page findByState(EmailState state, PageRequest pageRequest); - @Query(""" - SELECT e from Email e WHERE e.sendingScheduledAt < :scheduledBefore AND e.state IN ( - it.aboutbits.springboot.emailservice.lib.EmailState.PENDING, - it.aboutbits.springboot.emailservice.lib.EmailState.ERROR - ) - """) - List findReadyToSend(OffsetDateTime scheduledBefore); + List findByReference(String reference); List findByIdIn(Collection ids); } diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/model/Email.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/model/Email.java index 030fd7e..a818023 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/model/Email.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/model/Email.java @@ -5,6 +5,7 @@ import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -26,7 +27,7 @@ @NamedEntityGraph( name = "email_service_emails-entity-graph", attributeNodes = { - @NamedAttributeNode("recipients") + @NamedAttributeNode("attachments") } ) @Entity @@ -52,7 +53,7 @@ public class Email { private String textBody; private String htmlBody; - @OneToMany(cascade = CascadeType.ALL, mappedBy = "email", orphanRemoval = true) + @OneToMany(cascade = CascadeType.PERSIST, mappedBy = "email", orphanRemoval = true, fetch = FetchType.EAGER) private Set attachments; private String reference; diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/package-info.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/package-info.java new file mode 100644 index 0000000..48744f3 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/package-info.java @@ -0,0 +1,6 @@ +@NonNullApi +@NonNullFields +package it.aboutbits.springboot.emailservice.lib; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 128d011..e5852d7 100644 --- a/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -1,5 +1,11 @@ { "properties": [ + { + "name": "lib.emailservice.liquibase.enabled", + "type": "java.lang.Boolean", + "description": "Enables liquibase migrations.", + "defaultValue": true + }, { "name": "lib.emailservice.scheduling.enabled", "type": "java.lang.Boolean", diff --git a/src/main/resources/db/changelog/emailservice/2023-01-30-create-email-tables.xml b/src/main/resources/db/changelog/emailservice/2023-01-30-create-email-tables.xml deleted file mode 100644 index 93bec1f..0000000 --- a/src/main/resources/db/changelog/emailservice/2023-01-30-create-email-tables.xml +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ALTER TABLE email_service_emails - ADD CONSTRAINT email_service_emails_one_body_not_empty CHECK ( - (text_body != '') - OR (html_body != '') - ) - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/db/changelog/emailservice/2023-01-30-create-email-tables.yml b/src/main/resources/db/changelog/emailservice/2023-01-30-create-email-tables.yml new file mode 100644 index 0000000..111b8e3 --- /dev/null +++ b/src/main/resources/db/changelog/emailservice/2023-01-30-create-email-tables.yml @@ -0,0 +1,160 @@ +databaseChangeLog: + - changeSet: + id: 2023-01-30-create-email-tables + author: Andreas Hufler + changes: + - createTable: + tableName: email_service_emails + columns: + - column: + name: id + type: BIGSERIAL + autoIncrement: true + constraints: + primaryKey: true + nullable: false + - column: + name: state + type: text + constraints: + nullable: false + - column: + name: subject + type: text + constraints: + nullable: false + - column: + name: recipients + type: jsonb + constraints: + nullable: false + - column: + name: from_address + type: text + constraints: + nullable: false + - column: + name: from_name + type: text + constraints: + nullable: false + - column: + name: text_body + type: text + constraints: + nullable: false + - column: + name: html_body + type: text + constraints: + nullable: false + - column: + name: sending_scheduled_at + type: TIMESTAMP WITH TIMEZONE + constraints: + nullable: false + - column: + name: reference + type: text + constraints: + nullable: false + - column: + name: sent_at + type: TIMESTAMP WITH TIMEZONE + constraints: + nullable: false + - column: + name: error_at + type: TIMESTAMP WITH TIMEZONE + constraints: + nullable: false + - column: + name: error_message + type: text + constraints: + nullable: false + - column: + name: created_at + type: TIMESTAMP WITH TIMEZONE + constraints: + nullable: false + - column: + name: updated_at + type: TIMESTAMP WITH TIMEZONE + constraints: + nullable: false + + - createIndex: + tableName: email_service_emails + indexName: email_service_emails_reference_index + columns: + - column: + name: reference + + - createIndex: + tableName: email_service_emails + indexName: email_service_emails_state_index + columns: + - column: + name: state + + - createIndex: + tableName: email_service_emails + indexName: email_service_emails_sending_scheduled_at_index + columns: + - column: + name: sending_scheduled_at + + - sql: + splitStatements: false + sql: | + ALTER TABLE email_service_emails + ADD CONSTRAINT email_service_emails_one_body_not_empty CHECK ( + (text_body != '') + OR (html_body != '') + ) + + - createTable: + tableName: email_service_email_attachments + columns: + - column: + name: id + type: BIGSERIAL + autoIncrement: true + constraints: + primaryKey: true + nullable: false + - column: + name: email_id + type: bigint + constraints: + nullable: false + - column: + name: file_name + type: text + constraints: + nullable: false + - column: + name: content_type + type: text + constraints: + nullable: false + - column: + name: reference + type: text + constraints: + nullable: false + + - addForeignKeyConstraint: + baseTableName: email_service_email_attachments + baseColumnNames: email_id + constraintName: email_service_email_attachments_email_id + referencedTableName: email_service_emails + referencedColumnNames: id + + - createIndex: + tableName: email_service_email_attachments + indexName: email_service_email_attachments_email_id_index + columns: + - column: + name: email_id diff --git a/src/main/resources/db/changelog/emailservice/main.xml b/src/main/resources/db/changelog/emailservice/main.xml deleted file mode 100644 index 271ffbc..0000000 --- a/src/main/resources/db/changelog/emailservice/main.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/src/main/resources/db/changelog/emailservice/main.yml b/src/main/resources/db/changelog/emailservice/main.yml new file mode 100644 index 0000000..197a786 --- /dev/null +++ b/src/main/resources/db/changelog/emailservice/main.yml @@ -0,0 +1,6 @@ +databaseChangeLog: + - include: + file: classpath:db/changelog/master.yml + - include: + file: 2023-01-30-create-email-tables.yml + relativeToChangelogFile: true From 1d9e45d02803a71d9e36a1b71d4d3ae001343ad7 Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Mon, 17 Jul 2023 14:31:48 +0200 Subject: [PATCH 09/23] update checkstyle --- checkstyle-suppressions.xml | 8 ++++---- checkstyle.xml | 12 +++++++----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/checkstyle-suppressions.xml b/checkstyle-suppressions.xml index 5a91b4c..c315367 100644 --- a/checkstyle-suppressions.xml +++ b/checkstyle-suppressions.xml @@ -6,11 +6,11 @@ - - - + + + - + diff --git a/checkstyle.xml b/checkstyle.xml index 7bdca34..8621aa5 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -60,7 +60,7 @@ - + @@ -73,9 +73,9 @@ - + @@ -182,11 +182,13 @@ - + - + + + From c3ca1c5f7f0b8c7dfc7e2b43deef070498d72bd5 Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Mon, 17 Jul 2023 14:32:02 +0200 Subject: [PATCH 10/23] update action name --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 36b83bc..c7b7433 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -36,7 +36,7 @@ jobs: - uses: actions/checkout@v3 - name: Tag Deployment - uses: aboutbits/github-actions-git/create-or-update-tag@v1 + uses: aboutbits/github-actions-base/git-create-or-update-tag@v1 with: tag-name: ${{ github.event.inputs.version }} user-name: 'AboutBits' From f1ac2ec67a4099d197639650a62511db400fdaaf Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Mon, 17 Jul 2023 14:32:16 +0200 Subject: [PATCH 11/23] replace liquibase with a custom migrator --- pom.xml | 4 - readme.md | 10 +- .../EmailServiceConfiguration.java | 16 +- .../lib/application/EmailServiceMigrator.java | 70 ++++++++ .../lib/application/QueryEmail.java | 1 - ...itional-spring-configuration-metadata.json | 4 +- .../2023-01-30-create-email-tables.yml | 160 ------------------ .../db/changelog/emailservice/main.yml | 6 - 8 files changed, 83 insertions(+), 188 deletions(-) create mode 100644 src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailServiceMigrator.java delete mode 100644 src/main/resources/db/changelog/emailservice/2023-01-30-create-email-tables.yml delete mode 100644 src/main/resources/db/changelog/emailservice/main.yml diff --git a/pom.xml b/pom.xml index 2cfa401..18d51dc 100644 --- a/pom.xml +++ b/pom.xml @@ -33,10 +33,6 @@ org.springframework.boot spring-boot-starter-validation - - org.liquibase - liquibase-core - org.projectlombok lombok diff --git a/readme.md b/readme.md index 5c9c171..5adf7b4 100644 --- a/readme.md +++ b/readme.md @@ -74,11 +74,11 @@ public class App { The following configuration options are available: -| Name | Default | Description | -|--------------------------------------------|-------------|--------------------------------------------------------------------------------------------------------------------------| -| `lib.emailservice.liquibase.enabled` | true | Enables liquibase migrations. **attention** `classpath:db/changelog/master.yml` will be run first and must be present! | -| `lib.emailservice.scheduling.enabled` | true | Enables the scheduler sending the emails. | -| `lib.emailservice.scheduling.interval` | 30000 | Specifies the milliseconds delay between runs of the scheduler. | +| Name | Default | Description | +|----------------------------------------|-------------|-----------------------------------------------------------------------| +| `lib.emailservice.migrations.enabled` | true | Enables database migrations. | +| `lib.emailservice.scheduling.enabled` | true | Enables the scheduler sending the emails. | +| `lib.emailservice.scheduling.interval` | 30000 | Specifies the milliseconds delay between runs of the scheduler. | ## Building and releasing a new version: diff --git a/src/main/java/it/aboutbits/springboot/emailservice/EmailServiceConfiguration.java b/src/main/java/it/aboutbits/springboot/emailservice/EmailServiceConfiguration.java index e7363c6..ba67340 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/EmailServiceConfiguration.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/EmailServiceConfiguration.java @@ -6,32 +6,28 @@ import it.aboutbits.springboot.emailservice.lib.application.EmailAttachmentMapperImpl; import it.aboutbits.springboot.emailservice.lib.application.EmailMapper; import it.aboutbits.springboot.emailservice.lib.application.EmailMapperImpl; +import it.aboutbits.springboot.emailservice.lib.application.EmailServiceMigrator; import it.aboutbits.springboot.emailservice.lib.application.ManageEmail; import it.aboutbits.springboot.emailservice.lib.application.QueryEmail; import it.aboutbits.springboot.emailservice.lib.application.SendScheduledEmails; import it.aboutbits.springboot.emailservice.lib.application.UnavailableAttachmentDataSource; import it.aboutbits.springboot.emailservice.lib.jpa.EmailRepository; import jakarta.persistence.EntityManager; -import liquibase.integration.spring.SpringLiquibase; import org.springframework.boot.autoconfigure.AutoConfigurationPackage; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.mail.javamail.JavaMailSender; -import javax.sql.DataSource; import java.util.List; @AutoConfigurationPackage public class EmailServiceConfiguration { - @Bean - @ConditionalOnProperty(value = "lib.emailservice.liquibase.enabled", matchIfMissing = true) - public SpringLiquibase springLiquibase(DataSource dataSource) { - var liquibase = new SpringLiquibase(); - liquibase.setDataSource(dataSource); - liquibase.setChangeLog("classpath:/db/changelog/emailservice/main.yml"); - liquibase.setShouldRun(true); - return liquibase; + @Bean(initMethod = "init") + @ConditionalOnProperty(value = "lib.emailservice.migrations.enabled", matchIfMissing = true) + public EmailServiceMigrator springLiquibase(JdbcTemplate jdbcTemplate) { + return new EmailServiceMigrator(jdbcTemplate); } @Bean diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailServiceMigrator.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailServiceMigrator.java new file mode 100644 index 0000000..38d24c4 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailServiceMigrator.java @@ -0,0 +1,70 @@ +package it.aboutbits.springboot.emailservice.lib.application; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.jdbc.core.JdbcTemplate; + +@Slf4j +public class EmailServiceMigrator { + private final JdbcTemplate jdbcTemplate; + + public EmailServiceMigrator(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + void init() { + log.info("EmailService: running DB migrations..."); + + jdbcTemplate.execute(""" + + create table if not exists email_service_emails + ( + id bigint generated by default as identity + primary key, + state text default 'PENDING'::text not null, + subject text not null, + recipients jsonb not null, + from_address text not null, + from_name text not null, + text_body text not null, + html_body text not null, + attachments jsonb default '[]'::jsonb, + sending_scheduled_at timestamp with time zone default now() not null, + reference text, + sent_at timestamp with time zone, + error_at timestamp with time zone, + error_message text, + created_at timestamp with time zone default now() not null, + updated_at timestamp with time zone default now() not null, + constraint email_service_emails_one_body_not_empty + check ((text_body <> ''::text) OR (html_body <> ''::text)) + ); + + create index if not exists email_service_emails_reference_index + on email_service_emails (reference); + + create index if not exists email_service_emails_state_index + on email_service_emails (state); + + create index if not exists email_service_emails_sending_scheduled_at_index + on email_service_emails (sending_scheduled_at); + + create table if not exists email_service_email_attachments + ( + id bigint generated by default as identity + primary key, + email_id bigint not null + constraint email_service_email_attachments_email_id + references email_service_emails, + file_name text not null, + content_type text not null, + reference text not null + ); + + create index if not exists email_service_email_attachments_email_id_index + on email_service_email_attachments (email_id); + + """); + + log.info("EmailService: done!"); + } +} diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmail.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmail.java index 7f63231..91a04dd 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmail.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmail.java @@ -47,7 +47,6 @@ List readyToSend() { .setParameter("scheduledBefore", OffsetDateTime.now()) .setHint("jakarta.persistence.fetchgraph", entityGraph) .getResultList(); - //return emailRepository.findReadyToSend(OffsetDateTime.now()); } public Optional byId(final long id) { diff --git a/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/src/main/resources/META-INF/additional-spring-configuration-metadata.json index e5852d7..edbf786 100644 --- a/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -1,9 +1,9 @@ { "properties": [ { - "name": "lib.emailservice.liquibase.enabled", + "name": "lib.emailservice.migrations.enabled", "type": "java.lang.Boolean", - "description": "Enables liquibase migrations.", + "description": "Enables database migrations.", "defaultValue": true }, { diff --git a/src/main/resources/db/changelog/emailservice/2023-01-30-create-email-tables.yml b/src/main/resources/db/changelog/emailservice/2023-01-30-create-email-tables.yml deleted file mode 100644 index 111b8e3..0000000 --- a/src/main/resources/db/changelog/emailservice/2023-01-30-create-email-tables.yml +++ /dev/null @@ -1,160 +0,0 @@ -databaseChangeLog: - - changeSet: - id: 2023-01-30-create-email-tables - author: Andreas Hufler - changes: - - createTable: - tableName: email_service_emails - columns: - - column: - name: id - type: BIGSERIAL - autoIncrement: true - constraints: - primaryKey: true - nullable: false - - column: - name: state - type: text - constraints: - nullable: false - - column: - name: subject - type: text - constraints: - nullable: false - - column: - name: recipients - type: jsonb - constraints: - nullable: false - - column: - name: from_address - type: text - constraints: - nullable: false - - column: - name: from_name - type: text - constraints: - nullable: false - - column: - name: text_body - type: text - constraints: - nullable: false - - column: - name: html_body - type: text - constraints: - nullable: false - - column: - name: sending_scheduled_at - type: TIMESTAMP WITH TIMEZONE - constraints: - nullable: false - - column: - name: reference - type: text - constraints: - nullable: false - - column: - name: sent_at - type: TIMESTAMP WITH TIMEZONE - constraints: - nullable: false - - column: - name: error_at - type: TIMESTAMP WITH TIMEZONE - constraints: - nullable: false - - column: - name: error_message - type: text - constraints: - nullable: false - - column: - name: created_at - type: TIMESTAMP WITH TIMEZONE - constraints: - nullable: false - - column: - name: updated_at - type: TIMESTAMP WITH TIMEZONE - constraints: - nullable: false - - - createIndex: - tableName: email_service_emails - indexName: email_service_emails_reference_index - columns: - - column: - name: reference - - - createIndex: - tableName: email_service_emails - indexName: email_service_emails_state_index - columns: - - column: - name: state - - - createIndex: - tableName: email_service_emails - indexName: email_service_emails_sending_scheduled_at_index - columns: - - column: - name: sending_scheduled_at - - - sql: - splitStatements: false - sql: | - ALTER TABLE email_service_emails - ADD CONSTRAINT email_service_emails_one_body_not_empty CHECK ( - (text_body != '') - OR (html_body != '') - ) - - - createTable: - tableName: email_service_email_attachments - columns: - - column: - name: id - type: BIGSERIAL - autoIncrement: true - constraints: - primaryKey: true - nullable: false - - column: - name: email_id - type: bigint - constraints: - nullable: false - - column: - name: file_name - type: text - constraints: - nullable: false - - column: - name: content_type - type: text - constraints: - nullable: false - - column: - name: reference - type: text - constraints: - nullable: false - - - addForeignKeyConstraint: - baseTableName: email_service_email_attachments - baseColumnNames: email_id - constraintName: email_service_email_attachments_email_id - referencedTableName: email_service_emails - referencedColumnNames: id - - - createIndex: - tableName: email_service_email_attachments - indexName: email_service_email_attachments_email_id_index - columns: - - column: - name: email_id diff --git a/src/main/resources/db/changelog/emailservice/main.yml b/src/main/resources/db/changelog/emailservice/main.yml deleted file mode 100644 index 197a786..0000000 --- a/src/main/resources/db/changelog/emailservice/main.yml +++ /dev/null @@ -1,6 +0,0 @@ -databaseChangeLog: - - include: - file: classpath:db/changelog/master.yml - - include: - file: 2023-01-30-create-email-tables.yml - relativeToChangelogFile: true From 4008c1131c5fee8c6eb174dab074091c8a321203 Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Mon, 17 Jul 2023 15:25:06 +0200 Subject: [PATCH 12/23] use an explicitly typed object for attachment references --- .../emailservice/lib/AttachmentDataSource.java | 4 ++-- .../emailservice/lib/AttachmentReference.java | 11 +++++++++++ .../emailservice/lib/EmailAttachmentDto.java | 2 +- .../emailservice/lib/application/EmailParameter.java | 4 ++-- .../emailservice/lib/application/ManageEmail.java | 7 ++++--- .../application/UnavailableAttachmentDataSource.java | 5 +++-- 6 files changed, 23 insertions(+), 10 deletions(-) create mode 100644 src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentReference.java diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentDataSource.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentDataSource.java index c4b16b6..f66f8c9 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentDataSource.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentDataSource.java @@ -5,7 +5,7 @@ import java.io.InputStream; public interface AttachmentDataSource { - InputStream getAttachmentPayload(String reference) throws AttachmentException; + InputStream getAttachmentPayload(AttachmentReference reference) throws AttachmentException; - void releaseAttachment(String reference) throws AttachmentException; + void releaseAttachment(AttachmentReference reference) throws AttachmentException; } diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentReference.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentReference.java new file mode 100644 index 0000000..8874886 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentReference.java @@ -0,0 +1,11 @@ +package it.aboutbits.springboot.emailservice.lib; + +import jakarta.validation.constraints.NotBlank; +import lombok.NonNull; + +public record AttachmentReference( + @NonNull + @NotBlank + String value +) { +} diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailAttachmentDto.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailAttachmentDto.java index 435d194..1ad2ef7 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailAttachmentDto.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailAttachmentDto.java @@ -11,6 +11,6 @@ public record EmailAttachmentDto( String contentType, - String reference + AttachmentReference reference ) { } diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailParameter.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailParameter.java index 320b8cd..e1fd948 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailParameter.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailParameter.java @@ -1,5 +1,6 @@ package it.aboutbits.springboot.emailservice.lib.application; +import it.aboutbits.springboot.emailservice.lib.AttachmentReference; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import lombok.Builder; @@ -48,8 +49,7 @@ public record Email( @Builder public record Attachment( @NonNull - @NotBlank - String reference, + AttachmentReference reference, @NonNull @NotBlank String fileName, diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java index df6d093..ada373a 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java @@ -2,6 +2,7 @@ import it.aboutbits.springboot.emailservice.lib.AttachmentDataSource; +import it.aboutbits.springboot.emailservice.lib.AttachmentReference; import it.aboutbits.springboot.emailservice.lib.EmailDto; import it.aboutbits.springboot.emailservice.lib.EmailState; import it.aboutbits.springboot.emailservice.lib.exception.AttachmentException; @@ -61,7 +62,7 @@ public EmailDto schedule(@NonNull @Valid EmailParameter parameter) { .map(a -> { var attachment = new EmailAttachment(); attachment.setEmail(email); - attachment.setReference(a.reference()); + attachment.setReference(a.reference().value()); attachment.setContentType(a.contentType()); attachment.setFileName(a.fileName()); return attachment; @@ -105,7 +106,7 @@ Email send(Email email) { private void cleanupAttachments(final Email email) throws AttachmentException { for (var attachment : email.getAttachments()) { - attachmentDataSource.releaseAttachment(attachment.getReference()); + attachmentDataSource.releaseAttachment(new AttachmentReference(attachment.getReference())); } } @@ -138,7 +139,7 @@ private void sendMail(String fromAddress, String fromName, List recipien if (!attachments.isEmpty()) { for (var attachment : attachments) { - var payload = attachmentDataSource.getAttachmentPayload(attachment.getReference()); + var payload = attachmentDataSource.getAttachmentPayload(new AttachmentReference(attachment.getReference())); helper.addAttachment(attachment.getFileName(), new ByteArrayResource(payload.readAllBytes())); payload.close(); } diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java index 7960e57..565dc30 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java @@ -1,18 +1,19 @@ package it.aboutbits.springboot.emailservice.lib.application; import it.aboutbits.springboot.emailservice.lib.AttachmentDataSource; +import it.aboutbits.springboot.emailservice.lib.AttachmentReference; import it.aboutbits.springboot.emailservice.lib.exception.AttachmentException; import java.io.InputStream; public final class UnavailableAttachmentDataSource implements AttachmentDataSource { @Override - public InputStream getAttachmentPayload(final String reference) throws AttachmentException { + public InputStream getAttachmentPayload(AttachmentReference reference) throws AttachmentException { throw new AttachmentException("attachments not available"); } @Override - public void releaseAttachment(final String reference) throws AttachmentException { + public void releaseAttachment(AttachmentReference reference) throws AttachmentException { throw new AttachmentException("attachments not available"); } } From 96ac53168d89e3b3d5c073340a520d0150bfdebb Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Mon, 17 Jul 2023 15:26:59 +0200 Subject: [PATCH 13/23] remove final from parameters --- .../emailservice/lib/application/QueryEmail.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmail.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmail.java index 91a04dd..55e7b8e 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmail.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmail.java @@ -10,6 +10,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; +import org.springframework.lang.Nullable; import java.time.OffsetDateTime; import java.util.Collection; @@ -23,13 +24,13 @@ public class QueryEmail { private final EmailMapper emailMapper; private final EntityManager entityManager; - public Page paginatedByState(final EmailState state, final PageRequest pageParameter) { + public Page paginatedByState(EmailState state, PageRequest pageParameter) { var pageRequest = PageRequest.of(pageParameter.getPageNumber(), pageParameter.getPageSize(), Sort.by("updatedAt")); return emailMapper.toDto(emailRepository.findByState(state, pageRequest)); } - public List byIds(final Collection ids) { + public List byIds(Collection ids) { if (ids.isEmpty()) { return Collections.emptyList(); } @@ -49,11 +50,11 @@ List readyToSend() { .getResultList(); } - public Optional byId(final long id) { + public Optional byId(long id) { return emailRepository.findById(id).map(emailMapper::toDto); } - public List byReference(final String reference) { + public List byReference(@Nullable String reference) { return emailMapper.toDto(emailRepository.findByReference(reference)); } } From 82d5ca7ddc2d414277ecaeb8c374bbe84407e986 Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Tue, 18 Jul 2023 13:59:16 +0200 Subject: [PATCH 14/23] add tests --- checkstyle-suppressions.xml | 4 - pom.xml | 39 ++++++ .../application/EmailAttachmentMapper.java | 5 + .../emailservice/lib/model/Email.java | 6 + .../emailservice/TestApplication.java | 13 ++ .../lib/application/ManageEmailTest.java | 91 ++++++++++++++ .../lib/application/QueryEmailTest.java | 110 +++++++++++++++++ .../database/PostgresTestcontainer.java | 112 ++++++++++++++++++ .../support/database/WithPostgres.java | 14 +++ .../database/factory/EmailFactory.java | 29 +++++ 10 files changed, 419 insertions(+), 4 deletions(-) create mode 100644 src/test/java/it/aboutbits/springboot/emailservice/TestApplication.java create mode 100644 src/test/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmailTest.java create mode 100644 src/test/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmailTest.java create mode 100644 src/test/java/it/aboutbits/springboot/emailservice/support/database/PostgresTestcontainer.java create mode 100644 src/test/java/it/aboutbits/springboot/emailservice/support/database/WithPostgres.java create mode 100644 src/test/java/it/aboutbits/springboot/emailservice/support/database/factory/EmailFactory.java diff --git a/checkstyle-suppressions.xml b/checkstyle-suppressions.xml index c315367..15eaff5 100644 --- a/checkstyle-suppressions.xml +++ b/checkstyle-suppressions.xml @@ -6,11 +6,7 @@ - - - - diff --git a/pom.xml b/pom.xml index 18d51dc..8869aa3 100644 --- a/pom.xml +++ b/pom.xml @@ -53,6 +53,45 @@ mapstruct ${mapstruct.version} + + org.postgresql + postgresql + + + com.vladmihalcea + hibernate-types-60 + 2.21.1 + + + + com.fasterxml.jackson.core + jackson-databind + + + + com.github.javafaker + javafaker + 1.0.2 + test + + + org.testcontainers + testcontainers + 1.16.0 + test + + + org.testcontainers + junit-jupiter + 1.16.0 + test + + + org.testcontainers + postgresql + 1.16.3 + test + diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailAttachmentMapper.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailAttachmentMapper.java index e8d572a..8b0c108 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailAttachmentMapper.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailAttachmentMapper.java @@ -1,5 +1,6 @@ package it.aboutbits.springboot.emailservice.lib.application; +import it.aboutbits.springboot.emailservice.lib.AttachmentReference; import it.aboutbits.springboot.emailservice.lib.EmailAttachmentDto; import it.aboutbits.springboot.emailservice.lib.model.EmailAttachment; import org.mapstruct.Mapper; @@ -10,5 +11,9 @@ public interface EmailAttachmentMapper { EmailAttachmentDto toDto(EmailAttachment model); + default AttachmentReference map(String value) { + return new AttachmentReference(value); + } + List toDto(List model); } diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/model/Email.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/model/Email.java index a818023..e8272d1 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/model/Email.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/model/Email.java @@ -13,7 +13,10 @@ import jakarta.persistence.NamedEntityGraph; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.JdbcTypeCode; @@ -33,6 +36,9 @@ @Entity @Getter @Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor @Table(name = "email_service_emails") public class Email { @Id diff --git a/src/test/java/it/aboutbits/springboot/emailservice/TestApplication.java b/src/test/java/it/aboutbits/springboot/emailservice/TestApplication.java new file mode 100644 index 0000000..20454aa --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/emailservice/TestApplication.java @@ -0,0 +1,13 @@ +package it.aboutbits.springboot.emailservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SuppressWarnings("checkstyle:HideUtilityClassConstructor") +@SpringBootApplication +@EnableEmailService +public class TestApplication { + public static void main(final String[] args) { + SpringApplication.run(TestApplication.class, args); + } +} diff --git a/src/test/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmailTest.java b/src/test/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmailTest.java new file mode 100644 index 0000000..a726390 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmailTest.java @@ -0,0 +1,91 @@ +package it.aboutbits.springboot.emailservice.lib.application; + +import it.aboutbits.springboot.emailservice.lib.AttachmentReference; +import it.aboutbits.springboot.emailservice.lib.EmailState; +import it.aboutbits.springboot.emailservice.support.database.WithPostgres; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.mail.javamail.JavaMailSender; + +import java.time.OffsetDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@WithPostgres +class ManageEmailTest { + @MockBean + JavaMailSender javaMailSender; + + @Autowired + private ManageEmail manageEmail; + + @Test + void givenRequiredParameters_schedule_shouldCreateNewNotification() { + var parameter = EmailParameter.builder() + .scheduleAt(OffsetDateTime.now()) + .email(EmailParameter.Email.builder() + .subject("Example email subject") + .textBody("Email body") + .htmlBody("

Html email body

") + .recipient("person1@example.com") + .recipient("person2@example.com") + .fromAddress("somebody@aboutbits.it") + .fromName("somebody") + .build() + ).build(); + + var result = manageEmail.schedule(parameter); + + assertThat(result.id()).isPositive(); + assertThat(result.state()).isEqualTo(EmailState.PENDING); + assertThat(result.subject()).isEqualTo(parameter.email().subject()); + assertThat(result.fromAddress()).isEqualTo(parameter.email().fromAddress()); + assertThat(result.fromName()).isEqualTo(parameter.email().fromName()); + assertThat(result.recipients()).containsAll(parameter.email().recipients()); + assertThat(result.textBody()).isEqualTo(parameter.email().textBody()); + assertThat(result.htmlBody()).isEqualTo(parameter.email().htmlBody()); + assertThat(result.reference()).isEqualTo(parameter.reference()); + assertThat(result.attachments()).isEmpty(); + assertThat(result.sendingScheduledAt()).isEqualTo(parameter.scheduleAt()); + } + + @Test + void givenRequiredParameterWithAttachedFiles_schedule_shouldCreateNewNotification() { + var parameter = EmailParameter.builder() + .scheduleAt(OffsetDateTime.now()) + .email(EmailParameter.Email.builder() + .subject("Example email subject") + .textBody("Email body") + .htmlBody("

Html email body

") + .recipient("person1@example.com") + .recipient("person2@example.com") + .attachment( + EmailParameter.Email.Attachment.builder() + .contentType("image/png") + .fileName("x.png") + .reference(new AttachmentReference("something")) + .build() + ) + .fromAddress("somebody@aboutbits.it") + .fromName("somebody") + .build() + ).build(); + + var result = manageEmail.schedule(parameter); + + assertThat(result.id()).isPositive(); + assertThat(result.state()).isEqualTo(EmailState.PENDING); + assertThat(result.subject()).isEqualTo(parameter.email().subject()); + assertThat(result.fromAddress()).isEqualTo(parameter.email().fromAddress()); + assertThat(result.fromName()).isEqualTo(parameter.email().fromName()); + assertThat(result.recipients()).containsAll(parameter.email().recipients()); + assertThat(result.textBody()).isEqualTo(parameter.email().textBody()); + assertThat(result.htmlBody()).isEqualTo(parameter.email().htmlBody()); + assertThat(result.reference()).isEqualTo(parameter.reference()); + assertThat(result.attachments()).hasSize(1); + assertThat(result.sendingScheduledAt()).isEqualTo(parameter.scheduleAt()); + } +} diff --git a/src/test/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmailTest.java b/src/test/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmailTest.java new file mode 100644 index 0000000..79a2126 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmailTest.java @@ -0,0 +1,110 @@ +package it.aboutbits.springboot.emailservice.lib.application; + +import it.aboutbits.springboot.emailservice.lib.EmailDto; +import it.aboutbits.springboot.emailservice.lib.EmailState; +import it.aboutbits.springboot.emailservice.lib.jpa.EmailRepository; +import it.aboutbits.springboot.emailservice.support.database.WithPostgres; +import it.aboutbits.springboot.emailservice.support.database.factory.EmailFactory; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.PageRequest; +import org.springframework.mail.javamail.JavaMailSender; + +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + + +@SpringBootTest +@WithPostgres +class QueryEmailTest { + @MockBean + JavaMailSender javaMailSender; + + @Autowired + EmailRepository emailRepository; + + @Autowired + QueryEmail queryEmail; + + @Test + void givenEmailNotification_byState_shouldSuccess() { + emailRepository.saveAll(Set.of( + EmailFactory.once().build(), + EmailFactory.once().state(EmailState.SENT).build(), + EmailFactory.once().state(EmailState.SENT).build() + )); + + var result = queryEmail.paginatedByState( + EmailState.SENT, + PageRequest.of(0, 20) + ); + + assertThat(result.getTotalElements()).isEqualTo(2L); + } + + @Test + void givenEmailNotificationWithPagination_byState_shouldSuccess() { + emailRepository.saveAll(Set.of( + EmailFactory.once().build(), + EmailFactory.once().state(EmailState.SENT).build(), + EmailFactory.once().state(EmailState.SENT).build() + )); + + var result = queryEmail.paginatedByState( + EmailState.SENT, + PageRequest.of(1, 1) + ); + + assertThat(result.getTotalPages()).isEqualTo(2); + assertThat(result.getTotalElements()).isEqualTo(2L); + assertThat(result.getContent()).hasSize(1); + } + + @Test + void givenEmailNotificationWithPagination_byReference_shouldSuccess() { + emailRepository.saveAll(Set.of( + EmailFactory.once().reference("something").build(), + EmailFactory.once().reference("something else").build(), + EmailFactory.once().reference("something").build() + )); + + var result = queryEmail.byReference( + "something" + ); + + assertThat(result).hasSize(2); + } + + @Test + void givenEmailNotification_byIds_shouldSuccess() { + var notificationA = emailRepository.save(EmailFactory.once().build()); + var notificationB = emailRepository.save(EmailFactory.once().build()); + emailRepository.save(EmailFactory.once().build()); + + var result = queryEmail.byIds(Set.of(notificationA.getId(), notificationB.getId())); + + assertThat(result) + .hasSize(2) + .map(EmailDto::id) + .containsExactlyInAnyOrder(notificationA.getId(), notificationB.getId()); + + } + + @Test + void givenEmailNotification_byIdOrFail_shouldSuccess() { + var notification = emailRepository.save(EmailFactory.once().build()); + + var result = queryEmail.byId(notification.getId()); + + assertThat(result).isPresent(); + assertThat(result.get().id()).isEqualTo(notification.getId()); + } + + @Test + void givenNoEmailNotification_byIdOrFail_shouldFail() { + assertThat(queryEmail.byId(123L)).isNotPresent(); + } +} diff --git a/src/test/java/it/aboutbits/springboot/emailservice/support/database/PostgresTestcontainer.java b/src/test/java/it/aboutbits/springboot/emailservice/support/database/PostgresTestcontainer.java new file mode 100644 index 0000000..90bc89e --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/emailservice/support/database/PostgresTestcontainer.java @@ -0,0 +1,112 @@ +package it.aboutbits.springboot.emailservice.support.database; + +import lombok.Data; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.utility.DockerImageName; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.StringJoiner; + +@Log4j2 +public class PostgresTestcontainer implements BeforeAllCallback, AfterEachCallback { + public static final PostgreSQLContainer POSTGRES_CONTAINER; + private static final Set TABLES_TO_IGNORE = Set.of( + + ); + + static { + POSTGRES_CONTAINER = new PostgreSQLContainer<>( + DockerImageName.parse("public.ecr.aws/ubuntu/postgres:14-22.04_edge") + .asCompatibleSubstituteFor("postgres")) + .withDatabaseName("app") + // See https://www.postgresql.org/docs/current/non-durability.html for details + .withCommand("postgres -c max_connections=1000 -c fsync=off -c synchronous_commit=off -c full_page_writes=off -c max_wal_size=2GB -c checkpoint_timeout=20min") + .withUsername("admin") + .withPassword("password") + .withEnv("TZ", "UTC") // Container's time zone + .withEnv("PGTZ", "UTC") // PostgreSQL's time zone + .withReuse(true); + + POSTGRES_CONTAINER.start(); + } + + @Override + public void beforeAll(ExtensionContext extensionContext) { + System.setProperty("spring.datasource.driver-class-name", "org.postgresql.Driver"); + System.setProperty("spring.datasource.url", POSTGRES_CONTAINER.getJdbcUrl()); + System.setProperty("spring.datasource.username", POSTGRES_CONTAINER.getUsername()); + System.setProperty("spring.datasource.password", POSTGRES_CONTAINER.getPassword()); + } + + @Override + public void afterEach(ExtensionContext extensionContext) throws Exception { + var connection = DriverManager.getConnection( + POSTGRES_CONTAINER.getJdbcUrl(), + POSTGRES_CONTAINER.getUsername(), + POSTGRES_CONTAINER.getPassword() + ); + + try { + connection.setAutoCommit(false); + var tablesToClean = loadTablesToClean(connection); + cleanTablesData(tablesToClean, connection); + connection.commit(); + } catch (SQLException exception) { + exception.printStackTrace(); + } + } + + private List loadTablesToClean(final Connection connection) throws SQLException { + var databaseMetaData = connection.getMetaData(); + var resultSet = databaseMetaData.getTables( + connection.getCatalog(), null, null, new String[]{"TABLE"}); + + var tablesToClean = new ArrayList(); + while (resultSet.next()) { + var table = new TableData( + resultSet.getString("TABLE_SCHEM"), + resultSet.getString("TABLE_NAME") + ); + + if (!TABLES_TO_IGNORE.contains(table.getName())) { + tablesToClean.add(table); + } + } + + return tablesToClean; + } + + private void cleanTablesData(List tablesToClean, Connection connection) throws SQLException { + if (tablesToClean.isEmpty()) { + return; + } + + log.debug("Cleaning Database tables {}", tablesToClean); + + var allTables = new StringJoiner(", "); + for (TableData table : tablesToClean) { + allTables.add(table.getFullyQualifiedName()); + } + String statement = String.format("TRUNCATE %s RESTART IDENTITY CASCADE", allTables); + connection.prepareStatement(statement).execute(); + } + + @Data + private static class TableData { + private final String schema; + private final String name; + + public String getFullyQualifiedName() { + return schema + "." + name; + } + } +} diff --git a/src/test/java/it/aboutbits/springboot/emailservice/support/database/WithPostgres.java b/src/test/java/it/aboutbits/springboot/emailservice/support/database/WithPostgres.java new file mode 100644 index 0000000..6fdf3a8 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/emailservice/support/database/WithPostgres.java @@ -0,0 +1,14 @@ +package it.aboutbits.springboot.emailservice.support.database; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith({PostgresTestcontainer.class}) +@ResourceLock(value = "Database", mode = ResourceAccessMode.READ_WRITE) +public @interface WithPostgres { +} diff --git a/src/test/java/it/aboutbits/springboot/emailservice/support/database/factory/EmailFactory.java b/src/test/java/it/aboutbits/springboot/emailservice/support/database/factory/EmailFactory.java new file mode 100644 index 0000000..4c1182e --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/emailservice/support/database/factory/EmailFactory.java @@ -0,0 +1,29 @@ +package it.aboutbits.springboot.emailservice.support.database.factory; + +import com.github.javafaker.Faker; +import it.aboutbits.springboot.emailservice.lib.EmailState; +import it.aboutbits.springboot.emailservice.lib.model.Email; + +import java.time.OffsetDateTime; +import java.util.List; + +public final class EmailFactory { + private static final Faker FAKER = new Faker(); + + private EmailFactory() { + } + + public static Email.EmailBuilder once() { + var body = FAKER.lorem().paragraph(); + + return Email.builder() + .state(EmailState.PENDING) + .subject("Email subject") + .textBody(body) + .htmlBody("

" + body + "

") + .sendingScheduledAt(OffsetDateTime.now()) + .fromAddress(FAKER.internet().emailAddress()) + .fromName(FAKER.name().fullName()) + .recipients(List.of(FAKER.internet().emailAddress(), FAKER.internet().emailAddress())); + } +} From 1f8300b197652c38563e1905796123d4342ab643 Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Tue, 25 Jul 2023 09:47:41 +0200 Subject: [PATCH 15/23] remove reference field from email --- .../springboot/emailservice/lib/EmailDto.java | 2 -- .../lib/application/EmailParameter.java | 5 +---- .../lib/application/EmailServiceMigrator.java | 4 ---- .../emailservice/lib/application/ManageEmail.java | 1 - .../emailservice/lib/application/QueryEmail.java | 5 ----- .../emailservice/lib/jpa/EmailRepository.java | 2 -- .../springboot/emailservice/lib/model/Email.java | 2 -- .../lib/application/ManageEmailTest.java | 2 -- .../lib/application/QueryEmailTest.java | 15 --------------- 9 files changed, 1 insertion(+), 37 deletions(-) diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailDto.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailDto.java index fe3adb4..4f0d398 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailDto.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailDto.java @@ -21,8 +21,6 @@ public record EmailDto( Set attachments, - String reference, - OffsetDateTime sendingScheduledAt, OffsetDateTime sentAt, diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailParameter.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailParameter.java index e1fd948..cdf25ae 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailParameter.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailParameter.java @@ -6,7 +6,6 @@ import lombok.Builder; import lombok.NonNull; import lombok.Singular; -import org.springframework.lang.Nullable; import java.time.OffsetDateTime; import java.util.List; @@ -16,9 +15,7 @@ public record EmailParameter( @NonNull OffsetDateTime scheduleAt, - @NonNull Email email, - - @Nullable String reference + @NonNull Email email ) { @Builder diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailServiceMigrator.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailServiceMigrator.java index 38d24c4..9f59ccf 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailServiceMigrator.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailServiceMigrator.java @@ -29,7 +29,6 @@ void init() { html_body text not null, attachments jsonb default '[]'::jsonb, sending_scheduled_at timestamp with time zone default now() not null, - reference text, sent_at timestamp with time zone, error_at timestamp with time zone, error_message text, @@ -39,9 +38,6 @@ updated_at timestamp with time zone default now() not null, check ((text_body <> ''::text) OR (html_body <> ''::text)) ); - create index if not exists email_service_emails_reference_index - on email_service_emails (reference); - create index if not exists email_service_emails_state_index on email_service_emails (state); diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java index ada373a..be6424e 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java @@ -51,7 +51,6 @@ public EmailDto schedule(@NonNull @Valid EmailParameter parameter) { final var email = new Email(); email.setState(EmailState.PENDING); email.setSendingScheduledAt(parameter.scheduleAt()); - email.setReference(parameter.reference()); email.setSubject(emailData.subject()); email.setTextBody(emailData.textBody()); email.setHtmlBody(emailData.htmlBody()); diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmail.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmail.java index 55e7b8e..1ebfdb1 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmail.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmail.java @@ -10,7 +10,6 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; -import org.springframework.lang.Nullable; import java.time.OffsetDateTime; import java.util.Collection; @@ -53,8 +52,4 @@ List readyToSend() { public Optional byId(long id) { return emailRepository.findById(id).map(emailMapper::toDto); } - - public List byReference(@Nullable String reference) { - return emailMapper.toDto(emailRepository.findByReference(reference)); - } } diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/jpa/EmailRepository.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/jpa/EmailRepository.java index d939953..8b85d87 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/jpa/EmailRepository.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/jpa/EmailRepository.java @@ -13,7 +13,5 @@ public interface EmailRepository extends JpaRepository { Page findByState(EmailState state, PageRequest pageRequest); - List findByReference(String reference); - List findByIdIn(Collection ids); } diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/model/Email.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/model/Email.java index e8272d1..8b66625 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/model/Email.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/model/Email.java @@ -62,8 +62,6 @@ public class Email { @OneToMany(cascade = CascadeType.PERSIST, mappedBy = "email", orphanRemoval = true, fetch = FetchType.EAGER) private Set attachments; - private String reference; - private OffsetDateTime sendingScheduledAt; private OffsetDateTime sentAt; diff --git a/src/test/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmailTest.java b/src/test/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmailTest.java index a726390..53c517b 100644 --- a/src/test/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmailTest.java +++ b/src/test/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmailTest.java @@ -47,7 +47,6 @@ void givenRequiredParameters_schedule_shouldCreateNewNotification() { assertThat(result.recipients()).containsAll(parameter.email().recipients()); assertThat(result.textBody()).isEqualTo(parameter.email().textBody()); assertThat(result.htmlBody()).isEqualTo(parameter.email().htmlBody()); - assertThat(result.reference()).isEqualTo(parameter.reference()); assertThat(result.attachments()).isEmpty(); assertThat(result.sendingScheduledAt()).isEqualTo(parameter.scheduleAt()); } @@ -84,7 +83,6 @@ void givenRequiredParameterWithAttachedFiles_schedule_shouldCreateNewNotificatio assertThat(result.recipients()).containsAll(parameter.email().recipients()); assertThat(result.textBody()).isEqualTo(parameter.email().textBody()); assertThat(result.htmlBody()).isEqualTo(parameter.email().htmlBody()); - assertThat(result.reference()).isEqualTo(parameter.reference()); assertThat(result.attachments()).hasSize(1); assertThat(result.sendingScheduledAt()).isEqualTo(parameter.scheduleAt()); } diff --git a/src/test/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmailTest.java b/src/test/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmailTest.java index 79a2126..ab7e5e4 100644 --- a/src/test/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmailTest.java +++ b/src/test/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmailTest.java @@ -63,21 +63,6 @@ void givenEmailNotificationWithPagination_byState_shouldSuccess() { assertThat(result.getContent()).hasSize(1); } - @Test - void givenEmailNotificationWithPagination_byReference_shouldSuccess() { - emailRepository.saveAll(Set.of( - EmailFactory.once().reference("something").build(), - EmailFactory.once().reference("something else").build(), - EmailFactory.once().reference("something").build() - )); - - var result = queryEmail.byReference( - "something" - ); - - assertThat(result).hasSize(2); - } - @Test void givenEmailNotification_byIds_shouldSuccess() { var notificationA = emailRepository.save(EmailFactory.once().build()); From 583e546684fb188c90368ba2d86c80b77f9da96a Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Tue, 25 Jul 2023 09:51:41 +0200 Subject: [PATCH 16/23] rename field --- .../aboutbits/springboot/emailservice/lib/EmailDto.java | 2 +- .../emailservice/lib/application/EmailParameter.java | 2 +- .../lib/application/EmailServiceMigrator.java | 4 ++-- .../emailservice/lib/application/ManageEmail.java | 2 +- .../springboot/emailservice/lib/model/Email.java | 2 +- .../emailservice/lib/application/ManageEmailTest.java | 8 ++++---- .../support/database/factory/EmailFactory.java | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailDto.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailDto.java index 4f0d398..82b818b 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailDto.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailDto.java @@ -21,7 +21,7 @@ public record EmailDto( Set attachments, - OffsetDateTime sendingScheduledAt, + OffsetDateTime scheduledAt, OffsetDateTime sentAt, OffsetDateTime errorAt, diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailParameter.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailParameter.java index cdf25ae..971cb66 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailParameter.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailParameter.java @@ -13,7 +13,7 @@ @Builder public record EmailParameter( - @NonNull OffsetDateTime scheduleAt, + @NonNull OffsetDateTime scheduledAt, @NonNull Email email ) { diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailServiceMigrator.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailServiceMigrator.java index 9f59ccf..2d894f9 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailServiceMigrator.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailServiceMigrator.java @@ -28,7 +28,7 @@ void init() { text_body text not null, html_body text not null, attachments jsonb default '[]'::jsonb, - sending_scheduled_at timestamp with time zone default now() not null, + scheduled_at timestamp with time zone default now() not null, sent_at timestamp with time zone, error_at timestamp with time zone, error_message text, @@ -42,7 +42,7 @@ updated_at timestamp with time zone default now() not null, on email_service_emails (state); create index if not exists email_service_emails_sending_scheduled_at_index - on email_service_emails (sending_scheduled_at); + on email_service_emails (scheduled_at); create table if not exists email_service_email_attachments ( diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java index be6424e..2624bfc 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java @@ -50,7 +50,7 @@ public EmailDto schedule(@NonNull @Valid EmailParameter parameter) { final var email = new Email(); email.setState(EmailState.PENDING); - email.setSendingScheduledAt(parameter.scheduleAt()); + email.setScheduledAt(parameter.scheduledAt()); email.setSubject(emailData.subject()); email.setTextBody(emailData.textBody()); email.setHtmlBody(emailData.htmlBody()); diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/model/Email.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/model/Email.java index 8b66625..e23f0f6 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/model/Email.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/model/Email.java @@ -62,7 +62,7 @@ public class Email { @OneToMany(cascade = CascadeType.PERSIST, mappedBy = "email", orphanRemoval = true, fetch = FetchType.EAGER) private Set attachments; - private OffsetDateTime sendingScheduledAt; + private OffsetDateTime scheduledAt; private OffsetDateTime sentAt; private OffsetDateTime errorAt; diff --git a/src/test/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmailTest.java b/src/test/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmailTest.java index 53c517b..822c634 100644 --- a/src/test/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmailTest.java +++ b/src/test/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmailTest.java @@ -25,7 +25,7 @@ class ManageEmailTest { @Test void givenRequiredParameters_schedule_shouldCreateNewNotification() { var parameter = EmailParameter.builder() - .scheduleAt(OffsetDateTime.now()) + .scheduledAt(OffsetDateTime.now()) .email(EmailParameter.Email.builder() .subject("Example email subject") .textBody("Email body") @@ -48,13 +48,13 @@ void givenRequiredParameters_schedule_shouldCreateNewNotification() { assertThat(result.textBody()).isEqualTo(parameter.email().textBody()); assertThat(result.htmlBody()).isEqualTo(parameter.email().htmlBody()); assertThat(result.attachments()).isEmpty(); - assertThat(result.sendingScheduledAt()).isEqualTo(parameter.scheduleAt()); + assertThat(result.scheduledAt()).isEqualTo(parameter.scheduledAt()); } @Test void givenRequiredParameterWithAttachedFiles_schedule_shouldCreateNewNotification() { var parameter = EmailParameter.builder() - .scheduleAt(OffsetDateTime.now()) + .scheduledAt(OffsetDateTime.now()) .email(EmailParameter.Email.builder() .subject("Example email subject") .textBody("Email body") @@ -84,6 +84,6 @@ void givenRequiredParameterWithAttachedFiles_schedule_shouldCreateNewNotificatio assertThat(result.textBody()).isEqualTo(parameter.email().textBody()); assertThat(result.htmlBody()).isEqualTo(parameter.email().htmlBody()); assertThat(result.attachments()).hasSize(1); - assertThat(result.sendingScheduledAt()).isEqualTo(parameter.scheduleAt()); + assertThat(result.scheduledAt()).isEqualTo(parameter.scheduledAt()); } } diff --git a/src/test/java/it/aboutbits/springboot/emailservice/support/database/factory/EmailFactory.java b/src/test/java/it/aboutbits/springboot/emailservice/support/database/factory/EmailFactory.java index 4c1182e..bb8566e 100644 --- a/src/test/java/it/aboutbits/springboot/emailservice/support/database/factory/EmailFactory.java +++ b/src/test/java/it/aboutbits/springboot/emailservice/support/database/factory/EmailFactory.java @@ -21,7 +21,7 @@ public static Email.EmailBuilder once() { .subject("Email subject") .textBody(body) .htmlBody("

" + body + "

") - .sendingScheduledAt(OffsetDateTime.now()) + .scheduledAt(OffsetDateTime.now()) .fromAddress(FAKER.internet().emailAddress()) .fromName(FAKER.name().fullName()) .recipients(List.of(FAKER.internet().emailAddress(), FAKER.internet().emailAddress())); From 291be303b32853eae3e40d498d8971d3376463c8 Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Tue, 25 Jul 2023 09:54:02 +0200 Subject: [PATCH 17/23] rename a few things --- .../springboot/emailservice/EmailServiceConfiguration.java | 4 ++-- .../emailservice/lib/application/EmailServiceMigrator.java | 4 ++-- .../emailservice/lib/application/SendScheduledEmails.java | 2 +- .../META-INF/additional-spring-configuration-metadata.json | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/it/aboutbits/springboot/emailservice/EmailServiceConfiguration.java b/src/main/java/it/aboutbits/springboot/emailservice/EmailServiceConfiguration.java index ba67340..caa686f 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/EmailServiceConfiguration.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/EmailServiceConfiguration.java @@ -25,7 +25,7 @@ @AutoConfigurationPackage public class EmailServiceConfiguration { @Bean(initMethod = "init") - @ConditionalOnProperty(value = "lib.emailservice.migrations.enabled", matchIfMissing = true) + @ConditionalOnProperty(value = "aboutbits.emailservice.migrations.enabled", matchIfMissing = true) public EmailServiceMigrator springLiquibase(JdbcTemplate jdbcTemplate) { return new EmailServiceMigrator(jdbcTemplate); } @@ -51,7 +51,7 @@ public ManageEmail manageEmail(EmailRepository emailRepository, JavaMailSender j } @Bean - @ConditionalOnProperty(value = "lib.emailservice.scheduling.enabled", matchIfMissing = true) + @ConditionalOnProperty(value = "aboutbits.emailservice.scheduling.enabled", matchIfMissing = true) public SendScheduledEmails sendScheduledEmails(QueryEmail queryEmail, ManageEmail manageEmail, List callbacks) { return new SendScheduledEmails(queryEmail, manageEmail, callbacks); } diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailServiceMigrator.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailServiceMigrator.java index 2d894f9..f95932a 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailServiceMigrator.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailServiceMigrator.java @@ -11,7 +11,7 @@ public EmailServiceMigrator(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } - void init() { + void migrate() { log.info("EmailService: running DB migrations..."); jdbcTemplate.execute(""" @@ -61,6 +61,6 @@ updated_at timestamp with time zone default now() not null, """); - log.info("EmailService: done!"); + log.info("EmailService: migrations done!"); } } diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/SendScheduledEmails.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/SendScheduledEmails.java index fbe5fba..bb0d4c8 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/SendScheduledEmails.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/SendScheduledEmails.java @@ -17,7 +17,7 @@ public class SendScheduledEmails { private final ManageEmail manageEmail; private final List callbacks; - @Scheduled(initialDelayString = "${lib.emailservice.scheduling.interval:30000}", fixedDelayString = "${lib.emailservice.scheduling.interval:30000}") + @Scheduled(initialDelayString = "${aboutbits.emailservice.scheduling.interval:30000}", fixedDelayString = "${aboutbits.emailservice.scheduling.interval:30000}") void sendEmailNotifications() { log.info("Start: " + JOB_DESCRIPTION); diff --git a/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/src/main/resources/META-INF/additional-spring-configuration-metadata.json index edbf786..ef1c36e 100644 --- a/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -1,19 +1,19 @@ { "properties": [ { - "name": "lib.emailservice.migrations.enabled", + "name": "aboutbits.emailservice.migrations.enabled", "type": "java.lang.Boolean", "description": "Enables database migrations.", "defaultValue": true }, { - "name": "lib.emailservice.scheduling.enabled", + "name": "aboutbits.emailservice.scheduling.enabled", "type": "java.lang.Boolean", "description": "Enables the scheduler sending the emails.", "defaultValue": true }, { - "name": "lib.emailservice.scheduling.interval", + "name": "aboutbits.emailservice.scheduling.interval", "type": "java.lang.Long", "description": "Specifies the milliseconds delay between runs of the scheduler.", "defaultValue": 30000 From 3756b9412c05170a2a0f6cd6865251f44a248c82 Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Tue, 25 Jul 2023 09:55:17 +0200 Subject: [PATCH 18/23] improve sed command --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c7b7433..37e1824 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,7 +21,7 @@ jobs: - uses: aboutbits/github-actions-java/setup-and-install@v1 - name: Set Version - run: sed -i 's/BUILD-SNAPSHOT<\/version>/${{ github.event.inputs.version }}<\/version>/g' pom.xml + run: sed -i 's|BUILD-SNAPSHOT|${{ github.event.inputs.version }}|g' pom.xml - name: Publish package run: mvn --batch-mode deploy From 6caa15a71b182930133308cf22db7d9e5caf5d8a Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Tue, 25 Jul 2023 09:57:38 +0200 Subject: [PATCH 19/23] rename and formatting --- .../emailservice/lib/EmailSchedulerCallback.java | 2 +- .../emailservice/lib/application/EmailParameter.java | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailSchedulerCallback.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailSchedulerCallback.java index 6cba011..675b6ca 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailSchedulerCallback.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailSchedulerCallback.java @@ -4,7 +4,7 @@ public interface EmailSchedulerCallback { void report(Report report); record Report( - int totalAttempted, + int total, int sent, int errors ) { diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailParameter.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailParameter.java index 971cb66..b970e06 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailParameter.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailParameter.java @@ -13,9 +13,10 @@ @Builder public record EmailParameter( - @NonNull OffsetDateTime scheduledAt, - - @NonNull Email email + @NonNull + OffsetDateTime scheduledAt, + @NonNull + Email email ) { @Builder From c54fc3b830e9a3914707c960926bca91010ab1ef Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Tue, 25 Jul 2023 11:15:08 +0200 Subject: [PATCH 20/23] refactor attachment storage logic --- .../EmailServiceConfiguration.java | 12 ++- .../lib/AttachmentCleanerCallback.java | 12 +++ .../lib/AttachmentDataSource.java | 2 + .../application/CleanupAttachmentFiles.java | 51 +++++++++ .../lib/application/EmailParameter.java | 4 +- .../lib/application/EmailServiceMigrator.java | 8 +- .../lib/application/ManageEmail.java | 102 +++++++++++------- .../lib/application/QueryEmail.java | 11 +- .../lib/application/SendScheduledEmails.java | 2 +- .../UnavailableAttachmentDataSource.java | 5 + .../lib/exception/EmailException.java | 23 ++++ .../emailservice/lib/model/Email.java | 7 ++ ...itional-spring-configuration-metadata.json | 6 ++ .../lib/application/ManageEmailTest.java | 17 ++- 14 files changed, 212 insertions(+), 50 deletions(-) create mode 100644 src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentCleanerCallback.java create mode 100644 src/main/java/it/aboutbits/springboot/emailservice/lib/application/CleanupAttachmentFiles.java create mode 100644 src/main/java/it/aboutbits/springboot/emailservice/lib/exception/EmailException.java diff --git a/src/main/java/it/aboutbits/springboot/emailservice/EmailServiceConfiguration.java b/src/main/java/it/aboutbits/springboot/emailservice/EmailServiceConfiguration.java index caa686f..ae58265 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/EmailServiceConfiguration.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/EmailServiceConfiguration.java @@ -1,7 +1,9 @@ package it.aboutbits.springboot.emailservice; +import it.aboutbits.springboot.emailservice.lib.AttachmentCleanerCallback; import it.aboutbits.springboot.emailservice.lib.AttachmentDataSource; import it.aboutbits.springboot.emailservice.lib.EmailSchedulerCallback; +import it.aboutbits.springboot.emailservice.lib.application.CleanupAttachmentFiles; import it.aboutbits.springboot.emailservice.lib.application.EmailAttachmentMapper; import it.aboutbits.springboot.emailservice.lib.application.EmailAttachmentMapperImpl; import it.aboutbits.springboot.emailservice.lib.application.EmailMapper; @@ -24,9 +26,9 @@ @AutoConfigurationPackage public class EmailServiceConfiguration { - @Bean(initMethod = "init") + @Bean(initMethod = "migrate") @ConditionalOnProperty(value = "aboutbits.emailservice.migrations.enabled", matchIfMissing = true) - public EmailServiceMigrator springLiquibase(JdbcTemplate jdbcTemplate) { + public EmailServiceMigrator emailServiceMigrator(JdbcTemplate jdbcTemplate) { return new EmailServiceMigrator(jdbcTemplate); } @@ -56,6 +58,12 @@ public SendScheduledEmails sendScheduledEmails(QueryEmail queryEmail, ManageEmai return new SendScheduledEmails(queryEmail, manageEmail, callbacks); } + @Bean + @ConditionalOnProperty(value = "aboutbits.emailservice.scheduling.cleanup.enabled", matchIfMissing = true) + public CleanupAttachmentFiles cleanupAttachments(QueryEmail queryEmail, ManageEmail manageEmail, List callbacks) { + return new CleanupAttachmentFiles(queryEmail, manageEmail, callbacks); + } + @Bean @ConditionalOnMissingBean(AttachmentDataSource.class) public AttachmentDataSource attachmentDataSource() { diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentCleanerCallback.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentCleanerCallback.java new file mode 100644 index 0000000..680804e --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentCleanerCallback.java @@ -0,0 +1,12 @@ +package it.aboutbits.springboot.emailservice.lib; + +public interface AttachmentCleanerCallback { + void report(Report report); + + record Report( + int total, + int cleaned, + int errors + ) { + } +} diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentDataSource.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentDataSource.java index f66f8c9..3076c0b 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentDataSource.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentDataSource.java @@ -7,5 +7,7 @@ public interface AttachmentDataSource { InputStream getAttachmentPayload(AttachmentReference reference) throws AttachmentException; + AttachmentReference storeAttachmentPayload(InputStream payload) throws AttachmentException; + void releaseAttachment(AttachmentReference reference) throws AttachmentException; } diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/CleanupAttachmentFiles.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/CleanupAttachmentFiles.java new file mode 100644 index 0000000..af3954d --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/CleanupAttachmentFiles.java @@ -0,0 +1,51 @@ +package it.aboutbits.springboot.emailservice.lib.application; + + +import it.aboutbits.springboot.emailservice.lib.AttachmentCleanerCallback; +import it.aboutbits.springboot.emailservice.lib.exception.AttachmentException; +import lombok.AllArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.scheduling.annotation.Scheduled; + +import java.util.List; + +@AllArgsConstructor +@Log4j2 +public class CleanupAttachmentFiles { + private static final String JOB_DESCRIPTION = "Cleanup attachments of sent Emails."; + + private final QueryEmail queryEmail; + private final ManageEmail manageEmail; + private final List callbacks; + + @Scheduled(initialDelayString = "${aboutbits.emailservice.scheduling.interval:30000}", fixedDelayString = "${aboutbits.emailservice.scheduling.interval:30000}") + void cleanupAttachments() { + log.info("Start: " + JOB_DESCRIPTION); + + var emailsToCleanup = queryEmail.readyToCleanup(); + + var countCleaned = 0; + var countError = 0; + for (var email : emailsToCleanup) { + try { + manageEmail.cleanupAttachments(email); + countCleaned++; + } catch (AttachmentException e) { + countError++; + } + } + + log.info("Finished: " + JOB_DESCRIPTION); + log.info("Cleaned: {}, Errors: {}", countCleaned, countError); + + for (var callback : callbacks) { + callback.report(new AttachmentCleanerCallback.Report( + emailsToCleanup.size(), + countCleaned, + countError + )); + } + } + + +} diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailParameter.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailParameter.java index b970e06..d6e1856 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailParameter.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailParameter.java @@ -1,12 +1,12 @@ package it.aboutbits.springboot.emailservice.lib.application; -import it.aboutbits.springboot.emailservice.lib.AttachmentReference; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import lombok.Builder; import lombok.NonNull; import lombok.Singular; +import java.io.InputStream; import java.time.OffsetDateTime; import java.util.List; import java.util.Set; @@ -47,7 +47,7 @@ public record Email( @Builder public record Attachment( @NonNull - AttachmentReference reference, + InputStream payload, @NonNull @NotBlank String fileName, diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailServiceMigrator.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailServiceMigrator.java index f95932a..3657267 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailServiceMigrator.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailServiceMigrator.java @@ -28,7 +28,8 @@ void migrate() { text_body text not null, html_body text not null, attachments jsonb default '[]'::jsonb, - scheduled_at timestamp with time zone default now() not null, + attachments_cleaned bool default false not null, + scheduled_at timestamp with time zone default now() not null, sent_at timestamp with time zone, error_at timestamp with time zone, error_message text, @@ -41,8 +42,11 @@ updated_at timestamp with time zone default now() not null, create index if not exists email_service_emails_state_index on email_service_emails (state); - create index if not exists email_service_emails_sending_scheduled_at_index + create index if not exists email_service_emails_scheduled_at_index on email_service_emails (scheduled_at); + + create index if not exists email_service_emails_attachments_cleaned_index + on email_service_emails (attachments_cleaned); create table if not exists email_service_email_attachments ( diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java index 2624bfc..68484b7 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java @@ -6,13 +6,14 @@ import it.aboutbits.springboot.emailservice.lib.EmailDto; import it.aboutbits.springboot.emailservice.lib.EmailState; import it.aboutbits.springboot.emailservice.lib.exception.AttachmentException; +import it.aboutbits.springboot.emailservice.lib.exception.EmailException; import it.aboutbits.springboot.emailservice.lib.jpa.EmailRepository; import it.aboutbits.springboot.emailservice.lib.model.Email; import it.aboutbits.springboot.emailservice.lib.model.EmailAttachment; import jakarta.mail.MessagingException; import jakarta.validation.Valid; import lombok.NonNull; -import lombok.extern.log4j.Log4j2; +import lombok.extern.slf4j.Slf4j; import org.springframework.core.io.ByteArrayResource; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; @@ -20,13 +21,13 @@ import java.io.IOException; import java.time.OffsetDateTime; +import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.stream.Collectors; @Validated -@Log4j2 +@Slf4j public class ManageEmail { private final EmailRepository emailRepository; private final JavaMailSender mailSender; @@ -45,35 +46,36 @@ public ManageEmail( this.emailMapper = emailMapper; } - public EmailDto schedule(@NonNull @Valid EmailParameter parameter) { - var emailData = parameter.email(); - - final var email = new Email(); - email.setState(EmailState.PENDING); - email.setScheduledAt(parameter.scheduledAt()); - email.setSubject(emailData.subject()); - email.setTextBody(emailData.textBody()); - email.setHtmlBody(emailData.htmlBody()); - email.setRecipients(emailData.recipients()); - email.setFromAddress(emailData.fromAddress()); - email.setFromName(emailData.fromName()); - email.setAttachments(emailData.attachments().stream() - .map(a -> { - var attachment = new EmailAttachment(); - attachment.setEmail(email); - attachment.setReference(a.reference().value()); - attachment.setContentType(a.contentType()); - attachment.setFileName(a.fileName()); - return attachment; - }) - .collect(Collectors.toSet()) - ); + public EmailDto schedule(@NonNull @Valid EmailParameter parameter) throws EmailException { + Email email; + try { + email = fromParameter(parameter); + } catch (AttachmentException e) { + throw new EmailException(e); + } var savedEmail = emailRepository.save(email); return emailMapper.toDto(savedEmail); } + public EmailDto sendOrFail(@NonNull @Valid EmailParameter parameter) throws EmailException { + Email email; + try { + email = fromParameter(parameter); + } catch (AttachmentException e) { + throw new EmailException(e); + } + + var savedEmail = send(email); + + if (savedEmail.hasFailed()) { + throw new EmailException(savedEmail.getErrorMessage()); + } + + return emailMapper.toDto(savedEmail); + } + Email send(Email email) { if (EmailState.SENT.equals(email.getState())) { return email; @@ -85,30 +87,52 @@ Email send(Email email) { email.setErrorMessage(""); email.setSentAt(OffsetDateTime.now()); } catch (MessagingException | AttachmentException | IOException e) { - log.error("Failed to send email", e); + log.error("Failed to send email: " + email.getId(), e); email.setErrorMessage(e.getMessage()); email.setState(EmailState.ERROR); } - var updatedEmail = emailRepository.save(email); - - if (updatedEmail.isSent()) { - try { - cleanupAttachments(email); - } catch (AttachmentException e) { - throw new IllegalStateException(e); - } - } - - return updatedEmail; + return emailRepository.save(email); } - private void cleanupAttachments(final Email email) throws AttachmentException { + void cleanupAttachments(final Email email) throws AttachmentException { for (var attachment : email.getAttachments()) { attachmentDataSource.releaseAttachment(new AttachmentReference(attachment.getReference())); } + email.setAttachmentsCleaned(true); + emailRepository.save(email); } + private Email fromParameter(EmailParameter parameter) throws AttachmentException { + var emailData = parameter.email(); + + final var email = new Email(); + email.setState(EmailState.PENDING); + email.setScheduledAt(parameter.scheduledAt()); + email.setSubject(emailData.subject()); + email.setTextBody(emailData.textBody()); + email.setHtmlBody(emailData.htmlBody()); + email.setRecipients(emailData.recipients()); + email.setFromAddress(emailData.fromAddress()); + email.setFromName(emailData.fromName()); + + var attachments = new HashSet(); + for (var attachment : parameter.email().attachments()) { + var reference = attachmentDataSource.storeAttachmentPayload(attachment.payload()); + + var emailAttachment = new EmailAttachment(); + emailAttachment.setEmail(email); + emailAttachment.setReference(reference.value()); + emailAttachment.setContentType(attachment.contentType()); + emailAttachment.setFileName(attachment.fileName()); + + attachments.add(emailAttachment); + } + + email.setAttachments(attachments); + + return email; + } private void sendMail(Email notification) throws MessagingException, IOException, AttachmentException { sendMail( diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmail.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmail.java index 1ebfdb1..9714345 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmail.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/QueryEmail.java @@ -39,7 +39,7 @@ public List byIds(Collection ids) { List readyToSend() { var entityGraph = entityManager.getEntityGraph("email_service_emails-entity-graph"); return entityManager.createQuery(""" - SELECT e from Email e WHERE e.sendingScheduledAt < :scheduledBefore AND e.state IN ( + SELECT e from Email e WHERE e.scheduledAt < :scheduledBefore AND e.state IN ( it.aboutbits.springboot.emailservice.lib.EmailState.PENDING, it.aboutbits.springboot.emailservice.lib.EmailState.ERROR ) @@ -49,6 +49,15 @@ List readyToSend() { .getResultList(); } + List readyToCleanup() { + var entityGraph = entityManager.getEntityGraph("email_service_emails-entity-graph"); + return entityManager.createQuery(""" + SELECT e from Email e WHERE e.attachmentsCleaned=false AND e.state=it.aboutbits.springboot.emailservice.lib.EmailState.SENT + """, Email.class) + .setHint("jakarta.persistence.fetchgraph", entityGraph) + .getResultList(); + } + public Optional byId(long id) { return emailRepository.findById(id).map(emailMapper::toDto); } diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/SendScheduledEmails.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/SendScheduledEmails.java index bb0d4c8..4f6a767 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/SendScheduledEmails.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/SendScheduledEmails.java @@ -18,7 +18,7 @@ public class SendScheduledEmails { private final List callbacks; @Scheduled(initialDelayString = "${aboutbits.emailservice.scheduling.interval:30000}", fixedDelayString = "${aboutbits.emailservice.scheduling.interval:30000}") - void sendEmailNotifications() { + void sendEmails() { log.info("Start: " + JOB_DESCRIPTION); var emailsToSend = queryEmail.readyToSend(); diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java index 565dc30..c9ea6ef 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java @@ -12,6 +12,11 @@ public InputStream getAttachmentPayload(AttachmentReference reference) throws At throw new AttachmentException("attachments not available"); } + @Override + public AttachmentReference storeAttachmentPayload(InputStream payload) throws AttachmentException { + throw new AttachmentException("attachments not available"); + } + @Override public void releaseAttachment(AttachmentReference reference) throws AttachmentException { throw new AttachmentException("attachments not available"); diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/exception/EmailException.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/exception/EmailException.java new file mode 100644 index 0000000..9bc9855 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/exception/EmailException.java @@ -0,0 +1,23 @@ +package it.aboutbits.springboot.emailservice.lib.exception; + +public class EmailException extends Exception { + public EmailException() { + super(); + } + + public EmailException(String message) { + super(message); + } + + public EmailException(String message, Throwable cause) { + super(message, cause); + } + + public EmailException(Throwable cause) { + super(cause); + } + + protected EmailException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/model/Email.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/model/Email.java index e23f0f6..da336ad 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/model/Email.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/model/Email.java @@ -62,6 +62,9 @@ public class Email { @OneToMany(cascade = CascadeType.PERSIST, mappedBy = "email", orphanRemoval = true, fetch = FetchType.EAGER) private Set attachments; + @Builder.Default + private boolean attachmentsCleaned = false; + private OffsetDateTime scheduledAt; private OffsetDateTime sentAt; @@ -77,4 +80,8 @@ public class Email { public boolean isSent() { return EmailState.SENT.equals(state); } + + public boolean hasFailed() { + return EmailState.ERROR.equals(state); + } } diff --git a/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/src/main/resources/META-INF/additional-spring-configuration-metadata.json index ef1c36e..6653b27 100644 --- a/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -12,6 +12,12 @@ "description": "Enables the scheduler sending the emails.", "defaultValue": true }, + { + "name": "aboutbits.emailservice.scheduling.cleanup.enabled", + "type": "java.lang.Boolean", + "description": "Enables cleanup of attachment files after sending.", + "defaultValue": true + }, { "name": "aboutbits.emailservice.scheduling.interval", "type": "java.lang.Long", diff --git a/src/test/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmailTest.java b/src/test/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmailTest.java index 822c634..f7dd3c7 100644 --- a/src/test/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmailTest.java +++ b/src/test/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmailTest.java @@ -1,7 +1,10 @@ package it.aboutbits.springboot.emailservice.lib.application; +import it.aboutbits.springboot.emailservice.lib.AttachmentDataSource; import it.aboutbits.springboot.emailservice.lib.AttachmentReference; import it.aboutbits.springboot.emailservice.lib.EmailState; +import it.aboutbits.springboot.emailservice.lib.exception.AttachmentException; +import it.aboutbits.springboot.emailservice.lib.exception.EmailException; import it.aboutbits.springboot.emailservice.support.database.WithPostgres; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -9,9 +12,12 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.mail.javamail.JavaMailSender; +import java.io.ByteArrayInputStream; import java.time.OffsetDateTime; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; @SpringBootTest @WithPostgres @@ -19,11 +25,14 @@ class ManageEmailTest { @MockBean JavaMailSender javaMailSender; + @MockBean + AttachmentDataSource attachmentDataSource; + @Autowired private ManageEmail manageEmail; @Test - void givenRequiredParameters_schedule_shouldCreateNewNotification() { + void givenRequiredParameters_schedule_shouldCreateNewNotification() throws EmailException { var parameter = EmailParameter.builder() .scheduledAt(OffsetDateTime.now()) .email(EmailParameter.Email.builder() @@ -52,7 +61,9 @@ void givenRequiredParameters_schedule_shouldCreateNewNotification() { } @Test - void givenRequiredParameterWithAttachedFiles_schedule_shouldCreateNewNotification() { + void givenRequiredParameterWithAttachedFiles_schedule_shouldCreateNewNotification() throws EmailException, AttachmentException { + when(attachmentDataSource.storeAttachmentPayload(any())).thenReturn(new AttachmentReference("ref")); + var parameter = EmailParameter.builder() .scheduledAt(OffsetDateTime.now()) .email(EmailParameter.Email.builder() @@ -65,7 +76,7 @@ void givenRequiredParameterWithAttachedFiles_schedule_shouldCreateNewNotificatio EmailParameter.Email.Attachment.builder() .contentType("image/png") .fileName("x.png") - .reference(new AttachmentReference("something")) + .payload(new ByteArrayInputStream(new byte[0])) .build() ) .fromAddress("somebody@aboutbits.it") From ba9e577478e7744e00ab0ed7e2c0828739a5f390 Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Tue, 25 Jul 2023 11:16:52 +0200 Subject: [PATCH 21/23] remove unnecessary check --- .../emailservice/lib/application/ManageEmail.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java index 68484b7..93f87e0 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java @@ -160,12 +160,10 @@ private void sendMail(String fromAddress, String fromName, List recipien helper.setText(plainTextBody); } - if (!attachments.isEmpty()) { - for (var attachment : attachments) { - var payload = attachmentDataSource.getAttachmentPayload(new AttachmentReference(attachment.getReference())); - helper.addAttachment(attachment.getFileName(), new ByteArrayResource(payload.readAllBytes())); - payload.close(); - } + for (var attachment : attachments) { + var payload = attachmentDataSource.getAttachmentPayload(new AttachmentReference(attachment.getReference())); + helper.addAttachment(attachment.getFileName(), new ByteArrayResource(payload.readAllBytes())); + payload.close(); } mailSender.send(message); From 6f498090431e286c1b09fba5ed6b53035f15d262 Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Tue, 25 Jul 2023 11:48:56 +0200 Subject: [PATCH 22/23] add tests and improve errror handling --- .../lib/application/ManageEmail.java | 4 +- .../lib/application/ManageEmailTest.java | 154 +++++++++++++++--- 2 files changed, 130 insertions(+), 28 deletions(-) diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java index 93f87e0..2766abe 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java @@ -15,6 +15,7 @@ import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.springframework.core.io.ByteArrayResource; +import org.springframework.mail.MailException; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.validation.annotation.Validated; @@ -86,7 +87,7 @@ Email send(Email email) { email.setState(EmailState.SENT); email.setErrorMessage(""); email.setSentAt(OffsetDateTime.now()); - } catch (MessagingException | AttachmentException | IOException e) { + } catch (MailException | MessagingException | AttachmentException | IOException e) { log.error("Failed to send email: " + email.getId(), e); email.setErrorMessage(e.getMessage()); email.setState(EmailState.ERROR); @@ -166,6 +167,7 @@ private void sendMail(String fromAddress, String fromName, List recipien payload.close(); } + mailSender.send(message); } } diff --git a/src/test/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmailTest.java b/src/test/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmailTest.java index f7dd3c7..81d40fc 100644 --- a/src/test/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmailTest.java +++ b/src/test/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmailTest.java @@ -6,23 +6,30 @@ import it.aboutbits.springboot.emailservice.lib.exception.AttachmentException; import it.aboutbits.springboot.emailservice.lib.exception.EmailException; import it.aboutbits.springboot.emailservice.support.database.WithPostgres; +import jakarta.mail.internet.MimeMessage; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.mail.MailSendException; import org.springframework.mail.javamail.JavaMailSender; import java.io.ByteArrayInputStream; import java.time.OffsetDateTime; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @SpringBootTest @WithPostgres class ManageEmailTest { - @MockBean + @SpyBean JavaMailSender javaMailSender; @MockBean @@ -33,18 +40,7 @@ class ManageEmailTest { @Test void givenRequiredParameters_schedule_shouldCreateNewNotification() throws EmailException { - var parameter = EmailParameter.builder() - .scheduledAt(OffsetDateTime.now()) - .email(EmailParameter.Email.builder() - .subject("Example email subject") - .textBody("Email body") - .htmlBody("

Html email body

") - .recipient("person1@example.com") - .recipient("person2@example.com") - .fromAddress("somebody@aboutbits.it") - .fromName("somebody") - .build() - ).build(); + var parameter = getValidParameterWithoutAttachment(); var result = manageEmail.schedule(parameter); @@ -64,7 +60,124 @@ void givenRequiredParameters_schedule_shouldCreateNewNotification() throws Email void givenRequiredParameterWithAttachedFiles_schedule_shouldCreateNewNotification() throws EmailException, AttachmentException { when(attachmentDataSource.storeAttachmentPayload(any())).thenReturn(new AttachmentReference("ref")); - var parameter = EmailParameter.builder() + var parameter = getValidParameterWithAttachment(); + + var result = manageEmail.schedule(parameter); + + assertThat(result.id()).isPositive(); + assertThat(result.state()).isEqualTo(EmailState.PENDING); + assertThat(result.subject()).isEqualTo(parameter.email().subject()); + assertThat(result.fromAddress()).isEqualTo(parameter.email().fromAddress()); + assertThat(result.fromName()).isEqualTo(parameter.email().fromName()); + assertThat(result.recipients()).containsAll(parameter.email().recipients()); + assertThat(result.textBody()).isEqualTo(parameter.email().textBody()); + assertThat(result.htmlBody()).isEqualTo(parameter.email().htmlBody()); + assertThat(result.attachments()).hasSize(1); + assertThat(result.scheduledAt()).isEqualTo(parameter.scheduledAt()); + } + + @Test + void givenRequiredParameters_sendOrFail_shouldCreateNewNotification() throws EmailException { + var parameter = getValidParameterWithoutAttachment(); + + var result = manageEmail.sendOrFail(parameter); + + assertThat(result.id()).isPositive(); + assertThat(result.state()).isEqualTo(EmailState.SENT); + assertThat(result.subject()).isEqualTo(parameter.email().subject()); + assertThat(result.fromAddress()).isEqualTo(parameter.email().fromAddress()); + assertThat(result.fromName()).isEqualTo(parameter.email().fromName()); + assertThat(result.recipients()).containsAll(parameter.email().recipients()); + assertThat(result.textBody()).isEqualTo(parameter.email().textBody()); + assertThat(result.htmlBody()).isEqualTo(parameter.email().htmlBody()); + assertThat(result.attachments()).isEmpty(); + assertThat(result.scheduledAt()).isEqualTo(parameter.scheduledAt()); + } + + @Test + void givenRequiredParameterWithAttachedFiles_sendOrFail_shouldCreateNewNotification() throws EmailException, AttachmentException { + when(attachmentDataSource.storeAttachmentPayload(any())).thenReturn(new AttachmentReference("ref")); + when(attachmentDataSource.getAttachmentPayload(any())).thenReturn(new ByteArrayInputStream(new byte[0])); + + var parameter = getValidParameterWithAttachment(); + + var result = manageEmail.sendOrFail(parameter); + + assertThat(result.id()).isPositive(); + assertThat(result.state()).isEqualTo(EmailState.SENT); + assertThat(result.subject()).isEqualTo(parameter.email().subject()); + assertThat(result.fromAddress()).isEqualTo(parameter.email().fromAddress()); + assertThat(result.fromName()).isEqualTo(parameter.email().fromName()); + assertThat(result.recipients()).containsAll(parameter.email().recipients()); + assertThat(result.textBody()).isEqualTo(parameter.email().textBody()); + assertThat(result.htmlBody()).isEqualTo(parameter.email().htmlBody()); + assertThat(result.attachments()).hasSize(1); + assertThat(result.scheduledAt()).isEqualTo(parameter.scheduledAt()); + } + + @Test + void givenRequiredParameters_sendOrFail_shouldSendImmediately() throws EmailException { + EmailParameter parameter = getValidParameterWithoutAttachment(); + + manageEmail.sendOrFail(parameter); + + verify(javaMailSender, times(1)).send(any(MimeMessage.class)); + } + + @Test + void givenRequiredParameterWithAttachedFiles_sendOrFail_shouldSendImmediately() throws EmailException, AttachmentException { + when(attachmentDataSource.storeAttachmentPayload(any())).thenReturn(new AttachmentReference("ref")); + when(attachmentDataSource.getAttachmentPayload(any())).thenReturn(new ByteArrayInputStream(new byte[0])); + + var parameter = getValidParameterWithAttachment(); + + manageEmail.sendOrFail(parameter); + + verify(javaMailSender, times(1)).send(any(MimeMessage.class)); + } + + @Test + void givenAttachmentError_sendOrFail_shouldFail() throws EmailException, AttachmentException { + when(attachmentDataSource.storeAttachmentPayload(any())).thenThrow(new AttachmentException()); + + var parameter = getValidParameterWithAttachment(); + + assertThatExceptionOfType(EmailException.class).isThrownBy( + () -> manageEmail.sendOrFail(parameter) + ); + } + + @Test + void givenMailSenderError_sendOrFail_shouldFail() throws EmailException, AttachmentException { + when(attachmentDataSource.storeAttachmentPayload(any())).thenReturn(new AttachmentReference("ref")); + when(attachmentDataSource.getAttachmentPayload(any())).thenReturn(new ByteArrayInputStream(new byte[0])); + + doThrow(new MailSendException("any")).when(javaMailSender).send(any(MimeMessage.class)); + + var parameter = getValidParameterWithAttachment(); + + assertThatExceptionOfType(EmailException.class).isThrownBy( + () -> manageEmail.sendOrFail(parameter) + ); + } + + private static EmailParameter getValidParameterWithoutAttachment() { + return EmailParameter.builder() + .scheduledAt(OffsetDateTime.now()) + .email(EmailParameter.Email.builder() + .subject("Example email subject") + .textBody("Email body") + .htmlBody("

Html email body

") + .recipient("person1@example.com") + .recipient("person2@example.com") + .fromAddress("somebody@aboutbits.it") + .fromName("somebody") + .build() + ).build(); + } + + private static EmailParameter getValidParameterWithAttachment() { + return EmailParameter.builder() .scheduledAt(OffsetDateTime.now()) .email(EmailParameter.Email.builder() .subject("Example email subject") @@ -83,18 +196,5 @@ void givenRequiredParameterWithAttachedFiles_schedule_shouldCreateNewNotificatio .fromName("somebody") .build() ).build(); - - var result = manageEmail.schedule(parameter); - - assertThat(result.id()).isPositive(); - assertThat(result.state()).isEqualTo(EmailState.PENDING); - assertThat(result.subject()).isEqualTo(parameter.email().subject()); - assertThat(result.fromAddress()).isEqualTo(parameter.email().fromAddress()); - assertThat(result.fromName()).isEqualTo(parameter.email().fromName()); - assertThat(result.recipients()).containsAll(parameter.email().recipients()); - assertThat(result.textBody()).isEqualTo(parameter.email().textBody()); - assertThat(result.htmlBody()).isEqualTo(parameter.email().htmlBody()); - assertThat(result.attachments()).hasSize(1); - assertThat(result.scheduledAt()).isEqualTo(parameter.scheduledAt()); } } From c430bcc0f52fa0cb0b0db4725b3e2098f8fa1f9c Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Thu, 19 Oct 2023 16:36:10 +0200 Subject: [PATCH 23/23] use entity graph and rework attachment file reference --- pom.xml | 2 +- .../lib/AttachmentDataSource.java | 6 +++--- .../emailservice/lib/AttachmentReference.java | 11 ---------- .../emailservice/lib/EmailAttachmentDto.java | 2 +- .../application/EmailAttachmentMapper.java | 5 ----- .../lib/application/EmailServiceMigrator.java | 4 ++-- .../lib/application/ManageEmail.java | 7 +++---- .../UnavailableAttachmentDataSource.java | 7 +++---- .../emailservice/lib/jpa/EmailRepository.java | 20 +++++++++++++++++++ .../emailservice/lib/model/Email.java | 8 ++++++-- .../lib/model/EmailAttachment.java | 2 +- .../lib/application/ManageEmailTest.java | 16 +++++++-------- 12 files changed, 48 insertions(+), 42 deletions(-) delete mode 100644 src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentReference.java diff --git a/pom.xml b/pom.xml index 8869aa3..aab3aae 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 3.1.1 + 3.1.4 diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentDataSource.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentDataSource.java index 3076c0b..f05b85a 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentDataSource.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentDataSource.java @@ -5,9 +5,9 @@ import java.io.InputStream; public interface AttachmentDataSource { - InputStream getAttachmentPayload(AttachmentReference reference) throws AttachmentException; + InputStream getAttachmentPayload(long fileReference) throws AttachmentException; - AttachmentReference storeAttachmentPayload(InputStream payload) throws AttachmentException; + long storeAttachmentPayload(InputStream payload) throws AttachmentException; - void releaseAttachment(AttachmentReference reference) throws AttachmentException; + void releaseAttachment(long fileReference) throws AttachmentException; } diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentReference.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentReference.java deleted file mode 100644 index 8874886..0000000 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentReference.java +++ /dev/null @@ -1,11 +0,0 @@ -package it.aboutbits.springboot.emailservice.lib; - -import jakarta.validation.constraints.NotBlank; -import lombok.NonNull; - -public record AttachmentReference( - @NonNull - @NotBlank - String value -) { -} diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailAttachmentDto.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailAttachmentDto.java index 1ad2ef7..3f138df 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailAttachmentDto.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/EmailAttachmentDto.java @@ -11,6 +11,6 @@ public record EmailAttachmentDto( String contentType, - AttachmentReference reference + long fileReference ) { } diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailAttachmentMapper.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailAttachmentMapper.java index 8b0c108..e8d572a 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailAttachmentMapper.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailAttachmentMapper.java @@ -1,6 +1,5 @@ package it.aboutbits.springboot.emailservice.lib.application; -import it.aboutbits.springboot.emailservice.lib.AttachmentReference; import it.aboutbits.springboot.emailservice.lib.EmailAttachmentDto; import it.aboutbits.springboot.emailservice.lib.model.EmailAttachment; import org.mapstruct.Mapper; @@ -11,9 +10,5 @@ public interface EmailAttachmentMapper { EmailAttachmentDto toDto(EmailAttachment model); - default AttachmentReference map(String value) { - return new AttachmentReference(value); - } - List toDto(List model); } diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailServiceMigrator.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailServiceMigrator.java index 3657267..1223d40 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailServiceMigrator.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/EmailServiceMigrator.java @@ -44,7 +44,7 @@ updated_at timestamp with time zone default now() not null, create index if not exists email_service_emails_scheduled_at_index on email_service_emails (scheduled_at); - + create index if not exists email_service_emails_attachments_cleaned_index on email_service_emails (attachments_cleaned); @@ -57,7 +57,7 @@ updated_at timestamp with time zone default now() not null, references email_service_emails, file_name text not null, content_type text not null, - reference text not null + file_reference bigint not null ); create index if not exists email_service_email_attachments_email_id_index diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java index 2766abe..6b0f224 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmail.java @@ -2,7 +2,6 @@ import it.aboutbits.springboot.emailservice.lib.AttachmentDataSource; -import it.aboutbits.springboot.emailservice.lib.AttachmentReference; import it.aboutbits.springboot.emailservice.lib.EmailDto; import it.aboutbits.springboot.emailservice.lib.EmailState; import it.aboutbits.springboot.emailservice.lib.exception.AttachmentException; @@ -98,7 +97,7 @@ Email send(Email email) { void cleanupAttachments(final Email email) throws AttachmentException { for (var attachment : email.getAttachments()) { - attachmentDataSource.releaseAttachment(new AttachmentReference(attachment.getReference())); + attachmentDataSource.releaseAttachment(attachment.getFileReference()); } email.setAttachmentsCleaned(true); emailRepository.save(email); @@ -123,9 +122,9 @@ private Email fromParameter(EmailParameter parameter) throws AttachmentException var emailAttachment = new EmailAttachment(); emailAttachment.setEmail(email); - emailAttachment.setReference(reference.value()); emailAttachment.setContentType(attachment.contentType()); emailAttachment.setFileName(attachment.fileName()); + emailAttachment.setFileReference(reference); attachments.add(emailAttachment); } @@ -162,7 +161,7 @@ private void sendMail(String fromAddress, String fromName, List recipien } for (var attachment : attachments) { - var payload = attachmentDataSource.getAttachmentPayload(new AttachmentReference(attachment.getReference())); + var payload = attachmentDataSource.getAttachmentPayload(attachment.getFileReference()); helper.addAttachment(attachment.getFileName(), new ByteArrayResource(payload.readAllBytes())); payload.close(); } diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java index c9ea6ef..6d7bd52 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java @@ -1,24 +1,23 @@ package it.aboutbits.springboot.emailservice.lib.application; import it.aboutbits.springboot.emailservice.lib.AttachmentDataSource; -import it.aboutbits.springboot.emailservice.lib.AttachmentReference; import it.aboutbits.springboot.emailservice.lib.exception.AttachmentException; import java.io.InputStream; public final class UnavailableAttachmentDataSource implements AttachmentDataSource { @Override - public InputStream getAttachmentPayload(AttachmentReference reference) throws AttachmentException { + public InputStream getAttachmentPayload(long fileReference) throws AttachmentException { throw new AttachmentException("attachments not available"); } @Override - public AttachmentReference storeAttachmentPayload(InputStream payload) throws AttachmentException { + public long storeAttachmentPayload(InputStream payload) throws AttachmentException { throw new AttachmentException("attachments not available"); } @Override - public void releaseAttachment(AttachmentReference reference) throws AttachmentException { + public void releaseAttachment(long fileReference) throws AttachmentException { throw new AttachmentException("attachments not available"); } } diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/jpa/EmailRepository.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/jpa/EmailRepository.java index 8b85d87..718edf8 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/jpa/EmailRepository.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/jpa/EmailRepository.java @@ -5,13 +5,33 @@ import it.aboutbits.springboot.emailservice.lib.model.Email; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.lang.NonNull; import java.util.Collection; import java.util.List; +import java.util.Optional; public interface EmailRepository extends JpaRepository { + @Override + @NonNull + @EntityGraph(value = Email.DEFAULT_ENTITY_GRAPH) + Optional findById(@NonNull Long id); + + @Override + @NonNull + @EntityGraph(value = Email.DEFAULT_ENTITY_GRAPH) + List findAllById(@NonNull Iterable ids); + + @Override + @NonNull + @EntityGraph(value = Email.DEFAULT_ENTITY_GRAPH) + List findAll(); + + @EntityGraph(value = Email.DEFAULT_ENTITY_GRAPH) Page findByState(EmailState state, PageRequest pageRequest); + @EntityGraph(value = Email.DEFAULT_ENTITY_GRAPH) List findByIdIn(Collection ids); } diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/model/Email.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/model/Email.java index da336ad..a8ee7f9 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/model/Email.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/model/Email.java @@ -5,7 +5,6 @@ import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -27,6 +26,8 @@ import java.util.List; import java.util.Set; +import static it.aboutbits.springboot.emailservice.lib.model.Email.DEFAULT_ENTITY_GRAPH; + @NamedEntityGraph( name = "email_service_emails-entity-graph", attributeNodes = { @@ -40,7 +41,10 @@ @AllArgsConstructor @NoArgsConstructor @Table(name = "email_service_emails") +@NamedEntityGraph(name = DEFAULT_ENTITY_GRAPH, attributeNodes = @NamedAttributeNode("attachments")) public class Email { + public static final String DEFAULT_ENTITY_GRAPH = "graph.EmailServiceEmail.default"; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -59,7 +63,7 @@ public class Email { private String textBody; private String htmlBody; - @OneToMany(cascade = CascadeType.PERSIST, mappedBy = "email", orphanRemoval = true, fetch = FetchType.EAGER) + @OneToMany(cascade = CascadeType.PERSIST, mappedBy = "email", orphanRemoval = true) private Set attachments; @Builder.Default diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/model/EmailAttachment.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/model/EmailAttachment.java index 021784a..9de7abb 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/model/EmailAttachment.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/model/EmailAttachment.java @@ -27,5 +27,5 @@ public class EmailAttachment { private String contentType; - private String reference; + private long fileReference; } diff --git a/src/test/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmailTest.java b/src/test/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmailTest.java index 81d40fc..eeb62f6 100644 --- a/src/test/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmailTest.java +++ b/src/test/java/it/aboutbits/springboot/emailservice/lib/application/ManageEmailTest.java @@ -1,7 +1,6 @@ package it.aboutbits.springboot.emailservice.lib.application; import it.aboutbits.springboot.emailservice.lib.AttachmentDataSource; -import it.aboutbits.springboot.emailservice.lib.AttachmentReference; import it.aboutbits.springboot.emailservice.lib.EmailState; import it.aboutbits.springboot.emailservice.lib.exception.AttachmentException; import it.aboutbits.springboot.emailservice.lib.exception.EmailException; @@ -21,6 +20,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -58,7 +58,7 @@ void givenRequiredParameters_schedule_shouldCreateNewNotification() throws Email @Test void givenRequiredParameterWithAttachedFiles_schedule_shouldCreateNewNotification() throws EmailException, AttachmentException { - when(attachmentDataSource.storeAttachmentPayload(any())).thenReturn(new AttachmentReference("ref")); + when(attachmentDataSource.storeAttachmentPayload(any())).thenReturn(33L); var parameter = getValidParameterWithAttachment(); @@ -96,8 +96,8 @@ void givenRequiredParameters_sendOrFail_shouldCreateNewNotification() throws Ema @Test void givenRequiredParameterWithAttachedFiles_sendOrFail_shouldCreateNewNotification() throws EmailException, AttachmentException { - when(attachmentDataSource.storeAttachmentPayload(any())).thenReturn(new AttachmentReference("ref")); - when(attachmentDataSource.getAttachmentPayload(any())).thenReturn(new ByteArrayInputStream(new byte[0])); + when(attachmentDataSource.storeAttachmentPayload(any())).thenReturn(33L); + when(attachmentDataSource.getAttachmentPayload(anyLong())).thenReturn(new ByteArrayInputStream(new byte[0])); var parameter = getValidParameterWithAttachment(); @@ -126,8 +126,8 @@ void givenRequiredParameters_sendOrFail_shouldSendImmediately() throws EmailExce @Test void givenRequiredParameterWithAttachedFiles_sendOrFail_shouldSendImmediately() throws EmailException, AttachmentException { - when(attachmentDataSource.storeAttachmentPayload(any())).thenReturn(new AttachmentReference("ref")); - when(attachmentDataSource.getAttachmentPayload(any())).thenReturn(new ByteArrayInputStream(new byte[0])); + when(attachmentDataSource.storeAttachmentPayload(any())).thenReturn(33L); + when(attachmentDataSource.getAttachmentPayload(anyLong())).thenReturn(new ByteArrayInputStream(new byte[0])); var parameter = getValidParameterWithAttachment(); @@ -149,8 +149,8 @@ void givenAttachmentError_sendOrFail_shouldFail() throws EmailException, Attachm @Test void givenMailSenderError_sendOrFail_shouldFail() throws EmailException, AttachmentException { - when(attachmentDataSource.storeAttachmentPayload(any())).thenReturn(new AttachmentReference("ref")); - when(attachmentDataSource.getAttachmentPayload(any())).thenReturn(new ByteArrayInputStream(new byte[0])); + when(attachmentDataSource.storeAttachmentPayload(any())).thenReturn(33L); + when(attachmentDataSource.getAttachmentPayload(anyLong())).thenReturn(new ByteArrayInputStream(new byte[0])); doThrow(new MailSendException("any")).when(javaMailSender).send(any(MimeMessage.class));