diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml new file mode 100644 index 00000000..61432a46 --- /dev/null +++ b/.github/actions/build-image/action.yaml @@ -0,0 +1,58 @@ +name: build-image +description: "Builds and pushes a docker image" + +inputs: + registry: + description: "Image registry to push image to" + required: true + default: ghcr.io + registry-username: + description: "Username to authenticate against image registry" + required: true + registry-password: + description: "Password to authenticate against image registry" + required: true + path: + description: "Path to the Dockerfile to build image from" + required: true + image-name: + description: "Name to give the image" + required: true + image-tags: + description: "Tags to tag image with" + required: false + default: | + type=raw,value=latest + image-labels: + description: "Labels to add to image" + required: false + default: | + org.opencontainers.image.description=See ${{ github.server_url }}/${{ github.repository }} + +runs: + using: "composite" + steps: + - name: Download a single artifact + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: target + - name: Login to Registry + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + with: + registry: ${{ inputs.registry }} + username: ${{ inputs.registry-username }} + password: ${{ inputs.registry-password }} + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1 + with: + images: "${{ inputs.registry }}/${{ github.repository }}/${{ inputs.image-name }}" + tags: ${{inputs.image-tags}} + labels: ${{inputs.image-labels}} + - name: Build and push image + uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0 + with: + context: ./${{ inputs.path }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/actions/build-maven/action.yaml b/.github/actions/build-maven/action.yaml new file mode 100644 index 00000000..20ff38a7 --- /dev/null +++ b/.github/actions/build-maven/action.yaml @@ -0,0 +1,101 @@ +name: "" +description: "" + +inputs: + module: + description: 'Module to build' + required: true + release: + description: 'Release?' + default: 'false' + release-version: + description: 'Release version' + required: false + default: "X.Y.Z" + next-version: + description: "Next version to use after release" + required: false + default: "X.Y.Z-SNAPSHOT" + java-version: + description: "Jave version used for building" + required: false + default: "21" + gpg-private-key: + description: "Gpg private key used for signing packages for maven central release" + required: false + gpg-passphrase: + description: "Gpg passphrase for private key" + required: false + sonatype-username: + description: "Sonatype username for maven central release" + required: false + sonatype-password: + description: "Sonatype password for maven central release" + required: false + +runs: + using: "composite" + steps: + - name: Set up JDK + uses: actions/setup-java@6a0805fcefea3d4657a47ac4c165951e33482018 # v4.2.2 + with: + java-version: ${{ inputs.java-version }} + distribution: "temurin" + cache: "maven" + cache-dependency-path: "./${{ inputs.module }}/pom.xml" + server-id: "central" + server-username: CENTRAL_USERNAME + server-password: CENTRAL_PASSWORD + gpg-private-key: ${{ inputs.gpg-private-key }} + gpg-passphrase: SIGN_KEY_PASS + - name: Maven build + if: ${{ inputs.release != 'true' }} + shell: bash + run: mvn -f ./${{ inputs.module }}/pom.xml --batch-mode clean install + - name: Maven release + if: ${{ inputs.release == 'true' }} + shell: bash + run: | + git config --global user.email "github-actions@github.com" + git config --global user.name "GitHub Actions" + mvn release:prepare -f ./${{ inputs.module }}/pom.xml -B -DreleaseVersion=${{ inputs.release-version }} -DdevelopmentVersion=${{ inputs.next-version }} -DpushChanges=false -DremoteTagging=false + mvn release:perform -f ./${{ inputs.module }}/pom.xml -DlocalCheckout=true + env: + SIGN_KEY_PASS: ${{ inputs.gpg-passphrase }} + CENTRAL_USERNAME: ${{ inputs.sonatype-username }} + CENTRAL_PASSWORD: ${{ inputs.sonatype-password }} + - name: "Upload target artifacts" + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + with: + name: target + path: "**/target" + retention-days: 5 + - name: Push changes to new branch + if: ${{ inputs.release == 'true' }} + shell: bash + run: | + git checkout -b ${{ github.ref_name }}-version-bump + git push --force origin ${{ github.ref_name }}-version-bump + - name: Create pull request + if: ${{ inputs.release == 'true' }} + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const { repo, owner } = context.repo; + const pullResult = await github.rest.pulls.create({ + title: 'chore: bump release version ${{ github.ref_name }}', + owner, + repo, + head: '${{ github.ref_name }}-version-bump', + base: '${{ github.ref_name }}', + body: [ + 'This PR is auto-generated' + ].join('\n') + }); + await github.rest.issues.addAssignees({ + owner, + repo, + issue_number: pullResult.data.number, + assignees: ['${{ github.actor }}'], + }); + console.log(`Pull Request created: ${pullResult.data.html_url}`); diff --git a/.github/labeler.yml b/.github/labeler.yml index 0ba12742..533d4f2a 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -3,21 +3,21 @@ "Type: Bug": - head-branch: [ '^bug', 'bug' ] "Type: Maintenance": - - head-branch: [ '^maint', 'maintenance' ] + - head-branch: [ '^maint', 'maintenance', '^refact' ] "Type: Documentation": - head-branch: [ '^docs', "documentation" ] - changed-files: - - any-glob-to-any-file: ['docs/**'] + - any-glob-to-any-file: [ 'docs/**' ] "Type: Dependency": - head-branch: [ '^dep', 'dependency' ] "Type: Security": - head-branch: [ '^sec', 'security' ] "Component: API-Gateway": - changed-files: - - any-glob-to-any-file: ['refarch-gateway/**'] -"Component: Integration": + - any-glob-to-any-file: [ 'refarch-gateway/**' ] +"Component: Integrations": - changed-files: - - any-glob-to-any-file: ['refarch-integrations/**'] + - any-glob-to-any-file: [ 'refarch-integrations/**' ] "Component: CLI": - changed-files: - - any-glob-to-any-file: ['refarch-cli/**'] \ No newline at end of file + - any-glob-to-any-file: [ 'refarch-cli/**' ] diff --git a/.github/release.yml b/.github/release.yml index b6b1cf92..9d6c4f8f 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -1,23 +1,154 @@ changelog: categories: - - title: 🎉 New Features + # refarch-gateway + - title: Gateway - 🎉 New Features labels: - "Type: Feature" - - title: 🐞 Bug Fixes + exclude: + labels: + - "Component: Integrations" + - "Component: CLI" + - title: Gateway - 🐞 Bug Fixes labels: - "Type: Bug" - - title: 🔨 Maintenance + exclude: + labels: + - "Component: Integrations" + - "Component: CLI" + - title: Gateway - 🔨 Maintenance labels: - "Type: Maintenance" - - title: 📔 Documentation + exclude: + labels: + - "Component: Integrations" + - "Component: CLI" + - title: Gateway - 📔 Documentation labels: - "Type: Documentation" - - title: ⬆️ Dependency Upgrades + exclude: + labels: + - "Component: Integrations" + - "Component: CLI" + - title: Gateway - ⬆️ Dependency Upgrades labels: - "Type: Dependency" - - title: 🔒️ Security + exclude: + labels: + - "Component: Integrations" + - "Component: CLI" + - title: Gateway - 🔒️ Security labels: - "Type: Security" - - title: 💥 Breaking Changes + exclude: + labels: + - "Component: Integrations" + - "Component: CLI" + - title: Gateway - 💥 Breaking Changes labels: - - BREAKING \ No newline at end of file + - BREAKING + exclude: + labels: + - "Component: Integrations" + - "Component: CLI" + + # refarch-integrations + - title: Integrations - 🎉 New Features + labels: + - "Type: Feature" + exclude: + labels: + - "Component: API-Gateway" + - "Component: CLI" + - title: Integrations - 🐞 Bug Fixes + labels: + - "Type: Bug" + exclude: + labels: + - "Component: API-Gateway" + - "Component: CLI" + - title: Integrations - 🔨 Maintenance + labels: + - "Type: Maintenance" + exclude: + labels: + - "Component: API-Gateway" + - "Component: CLI" + - title: Integrations - 📔 Documentation + labels: + - "Type: Documentation" + exclude: + labels: + - "Component: API-Gateway" + - "Component: CLI" + - title: Integrations - ⬆️ Dependency Upgrades + labels: + - "Type: Dependency" + exclude: + labels: + - "Component: API-Gateway" + - "Component: CLI" + - title: Integrations - 🔒️ Security + labels: + - "Type: Security" + exclude: + labels: + - "Component: API-Gateway" + - "Component: CLI" + - title: Integrations - 💥 Breaking Changes + labels: + - BREAKING + exclude: + labels: + - "Component: API-Gateway" + - "Component: CLI" + + # refarch-cli + - title: CLI - 🎉 New Features + labels: + - "Type: Feature" + exclude: + labels: + - "Component: API-Gateway" + - "Component: Integrations" + - title: CLI - 🐞 Bug Fixes + labels: + - "Type: Bug" + exclude: + labels: + - "Component: API-Gateway" + - "Component: Integrations" + - title: CLI - 🔨 Maintenance + labels: + - "Type: Maintenance" + exclude: + labels: + - "Component: API-Gateway" + - "Component: Integrations" + - title: CLI - 📔 Documentation + labels: + - "Type: Documentation" + exclude: + labels: + - "Component: API-Gateway" + - "Component: Integrations" + - title: CLI - ⬆️ Dependency Upgrades + labels: + - "Type: Dependency" + exclude: + labels: + - "Component: API-Gateway" + - "Component: Integrations" + - title: CLI - 🔒️ Security + labels: + - "Type: Security" + exclude: + labels: + - "Component: API-Gateway" + - "Component: Integrations" + - title: CLI - 💥 Breaking Changes + labels: + - BREAKING + exclude: + labels: + - "Component: API-Gateway" + - "Component: Integrations" diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml deleted file mode 100644 index 7bc4e489..00000000 --- a/.github/workflows/build.yaml +++ /dev/null @@ -1,15 +0,0 @@ -name: build - -on: - pull_request: - push: - branches: - - main - -jobs: - maven: - uses: ./.github/workflows/maven.yaml - with: - build-images: ${{ github.ref_name == 'main' }} - release-version: dev - secrets: inherit diff --git a/.github/workflows/build_gateway.yaml b/.github/workflows/build_gateway.yaml new file mode 100644 index 00000000..0cb42e60 --- /dev/null +++ b/.github/workflows/build_gateway.yaml @@ -0,0 +1,35 @@ +name: build-gateway + +on: + pull_request: + push: + branches: + - main + +jobs: + build-maven: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: Maven build + uses: ./.github/actions/build-maven + with: + module: refarch-gateway + + build-images: + if: github.ref_name == 'main' + needs: build-maven + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: Build and push image + uses: ./.github/actions/build-image + with: + registry-username: ${{ github.actor }} + registry-password: ${{ secrets.GITHUB_TOKEN }} + path: ./refarch-gateway + image-name: refarch-gateway + image-tags: | + type=raw,value=dev diff --git a/.github/workflows/build_integrations.yaml b/.github/workflows/build_integrations.yaml new file mode 100644 index 00000000..f21c1c66 --- /dev/null +++ b/.github/workflows/build_integrations.yaml @@ -0,0 +1,40 @@ +name: build-integrations + +on: + pull_request: + push: + branches: + - main + +jobs: + build-maven: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: Maven build + uses: ./.github/actions/build-maven + with: + module: refarch-integrations + + build-images: + if: github.ref_name == 'main' + needs: build-maven + runs-on: ubuntu-latest + strategy: + matrix: + include: + - name: s3-integration-rest-service + path: ./refarch-integrations/refarch-s3-integration/refarch-s3-integration-rest/refarch-s3-integration-rest-service + steps: + - name: Checkout code + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: Build and push image + uses: ./.github/actions/build-image + with: + registry-username: ${{ github.actor }} + registry-password: ${{ secrets.GITHUB_TOKEN }} + path: ${{ matrix.path }} + image-name: ${{ matrix.name }} + image-tags: | + type=raw,value=dev diff --git a/.github/workflows/maven.yaml b/.github/workflows/maven.yaml deleted file mode 100644 index 432956ce..00000000 --- a/.github/workflows/maven.yaml +++ /dev/null @@ -1,132 +0,0 @@ -env: - JAVA_VERSION: 21 - REGISTRY: ghcr.io - TZ: Europe/Berlin - -on: - workflow_call: - inputs: - snapshot-release: - description: 'Snapshot release?' - type: boolean - default: true - build-images: - description: 'Build and push images?' - type: boolean - default: false - release-version: - description: 'Release version' - type: string - required: false - default: "X.Y.Z" - next-version: - description: "Next version to use after release." - type: string - required: false - default: "X.Y.Z-SNAPSHOT" - -jobs: - build-maven: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - name: Set up JDK - uses: actions/setup-java@6a0805fcefea3d4657a47ac4c165951e33482018 # v4.2.2 - with: - java-version: ${{ env.JAVA_VERSION }} - distribution: "temurin" - cache: "maven" - server-id: "central" - server-username: CENTRAL_USERNAME - server-password: CENTRAL_PASSWORD - gpg-private-key: ${{ secrets.gpg_private_key }} - gpg-passphrase: SIGN_KEY_PASS - - name: Maven build - if: ${{ inputs.snapshot-release != false }} - run: mvn --batch-mode clean install - - name: Maven release - if: ${{ inputs.snapshot-release == false }} - run: | - git config --global user.email "github-actions@github.com" - git config --global user.name "GitHub Actions" - mvn release:prepare -B -DreleaseVersion=${{ inputs.release-version }} -DdevelopmentVersion=${{ inputs.next-version }} -DpushChanges=false -DremoteTagging=false - mvn release:perform -DlocalCheckout=true - env: - SIGN_KEY_PASS: ${{ secrets.gpg_passphrase }} - CENTRAL_USERNAME: ${{ secrets.sonatype_username }} - CENTRAL_PASSWORD: ${{ secrets.sonatype_password }} - - name: "Upload target artifacts" - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 - with: - name: target - path: "**/target" - retention-days: 5 - - name: Push changes to new branch - if: ${{ inputs.snapshot-release == false }} - run: | - git checkout -b ${{ github.ref_name }}-version-bump - git push --force origin ${{ github.ref_name }}-version-bump - - name: Create pull request - if: ${{ inputs.snapshot-release == false }} - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 - with: - script: | - const { repo, owner } = context.repo; - const pullResult = await github.rest.pulls.create({ - title: 'chore: bump release version ${{ github.ref_name }}', - owner, - repo, - head: '${{ github.ref_name }}-version-bump', - base: '${{ github.ref_name }}', - body: [ - 'This PR is auto-generated' - ].join('\n') - }); - await github.rest.issues.addAssignees({ - owner, - repo, - issue_number: pullResult.data.number, - assignees: ['${{ github.actor }}'], - }); - console.log(`Pull Request created: ${pullResult.data.html_url}`); - - build-images: - if: inputs.build-images == true - needs: build-maven - runs-on: ubuntu-latest - strategy: - matrix: - include: - - name: refarch-gateway - path: ./refarch-gateway - - name: s3-integration-rest-service - path: ./refarch-integrations/refarch-s3-integration/refarch-s3-integration-rest/refarch-s3-integration-rest-service - steps: - - name: Checkout code - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - name: Download target artifacts - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - name: target - - name: Login to Registry - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract metadata (tags, labels) for image - id: meta - uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1 - with: - images: "${{ env.REGISTRY }}/${{ github.repository }}/${{ matrix.name }}" - tags: | - type=raw,value=${{ inputs.release-version }} - type=raw,value=latest,enable=${{ inputs.snapshot-release == false }} - - name: Build and push image - uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0 - with: - context: ${{ matrix.path }} - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/pr_labeler.yaml b/.github/workflows/pr_labeler.yaml index ee167c15..547536d4 100644 --- a/.github/workflows/pr_labeler.yaml +++ b/.github/workflows/pr_labeler.yaml @@ -11,5 +11,3 @@ jobs: steps: - name: "Label PR" uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5 - with: - sync-labels: true \ No newline at end of file diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 743fcada..7bb35ce8 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -3,8 +3,15 @@ name: release on: workflow_dispatch: inputs: - snapshot-release: - description: 'Snapshot release?' + module: + description: 'Module to release' + type: choice + required: true + options: + - refarch-gateway + - refarch-integrations + release: + description: 'Release?' type: boolean default: true release-version: @@ -13,30 +20,80 @@ on: required: true default: "X.Y.Z" next-version: - description: "Next version to use after release." + description: "Next version to use after release" type: string required: true default: "X.Y.Z-SNAPSHOT" jobs: - build: - uses: ./.github/workflows/maven.yaml - with: - snapshot-release: ${{ inputs.snapshot-release != false }} - build-images: true - release-version: ${{ inputs.release-version }} - secrets: inherit + build-maven: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: Maven build and release + uses: ./.github/actions/build-maven + with: + module: ${{ inputs.module }} + release: ${{ inputs.release }} + release-version: ${{ inputs.release-version }} + next-version: ${{ inputs.next-version }} + gpg-private-key: ${{ secrets.gpg_private_key }} + gpg-passphrase: ${{ secrets.gpg_passphrase }} + sonatype-username: ${{ secrets.sonatype_username }} + sonatype-password: ${{ secrets.sonatype_password }} + + build-images-gateway: + if: inputs.module == 'refarch-gateway' + needs: build-maven + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: Build and push image + uses: ./.github/actions/build-image + with: + registry-username: ${{ github.actor }} + registry-password: ${{ secrets.GITHUB_TOKEN }} + path: ./refarch-gateway + image-name: refarch-gateway + image-tags: | + type=raw,value=${{ inputs.release-version }} + type=raw,value=latest,enable=${{ inputs.release == true }} + + build-images-integrations: + if: inputs.module == 'refarch-integrations' + needs: build-maven + runs-on: ubuntu-latest + strategy: + matrix: + include: + - name: s3-integration-rest-service + path: ./refarch-integrations/refarch-s3-integration/refarch-s3-integration-rest/refarch-s3-integration-rest-service + steps: + - name: Checkout code + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: Build and push images + uses: ./.github/actions/build-image + with: + registry-username: ${{ github.actor }} + registry-password: ${{ secrets.GITHUB_TOKEN }} + path: ${{ matrix.path }} + image-name: ${{ matrix.name }} + image-tags: | + type=raw,value=${{ inputs.release-version }} + type=raw,value=latest,enable=${{ inputs.release == true }} create-github-release: - if: ${{ inputs.snapshot-release == false }} - needs: build + if: ${{ inputs.release == true }} + needs: build-maven runs-on: ubuntu-latest steps: - name: Create GitHub Release id: create_release uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v2.0.8 with: - tag_name: ${{ github.event.inputs.release-version }} + tag_name: "${{ inputs.module }}_${{ github.event.inputs.release-version }}" draft: false prerelease: false generate_release_notes: true diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 692ed95d..00000000 --- a/pom.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - 4.0.0 - - de.muenchen - refarch - refarch - Collection of different ready to use RefArch components - 1.1.0-SNAPSHOT - pom - - - refarch-gateway - refarch-integrations - - - - https://github.com/it-at-m/refarch.git - scm:git:https://github.com/it-at-m/refarch.git - scm:git:https://github.com/it-at-m/refarch.git - HEAD - - - - MIT - - - - - it@M - opensource@muenchen.de - https://github.com/it-at-m - - - diff --git a/refarch-gateway/pom.xml b/refarch-gateway/pom.xml index 38786fea..985f0e29 100644 --- a/refarch-gateway/pom.xml +++ b/refarch-gateway/pom.xml @@ -1,20 +1,19 @@ - + 4.0.0 org.springframework.boot spring-boot-starter-parent - 3.3.2 - + 3.3.3 + de.muenchen.refarch refarch-gateway refarch-gateway Ready to use RefArch gateway based on Spring Cloud Gateway - 1.1.0-SNAPSHOT + 1.2.0-SNAPSHOT jar @@ -37,6 +36,9 @@ 0.8.12 + + 3.1.1 + 3.17.0 @@ -181,6 +183,22 @@ + + + org.apache.maven.plugins + maven-release-plugin + ${maven-release-plugin.version} + + true + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + @@ -203,8 +221,8 @@ itm-java-codeformat/java_codestyle_formatter.xml - - + + diff --git a/refarch-integrations/README.md b/refarch-integrations/README.md index db260585..280a3158 100644 --- a/refarch-integrations/README.md +++ b/refarch-integrations/README.md @@ -6,6 +6,8 @@ Collection of different integration which can be used as is in RefArch projects. - [s3-integration](./refarch-s3-integration/README.md): For CRUD operations on a s3 storage. Also used for file handling in other integrations. +- [email-integration](./refarch-email-integration/README.md): For sending text and html emails with attachments. Uses + s3-integration for file handling. ## Naming conventions diff --git a/refarch-integrations/pom.xml b/refarch-integrations/pom.xml index 00b5f3e8..49763297 100644 --- a/refarch-integrations/pom.xml +++ b/refarch-integrations/pom.xml @@ -1,13 +1,12 @@ - + 4.0.0 org.springframework.boot spring-boot-starter-parent - 3.3.2 - + 3.3.3 + de.muenchen.refarch @@ -19,6 +18,7 @@ refarch-s3-integration + refarch-email-integration @@ -232,8 +232,8 @@ itm-java-codeformat/java_codestyle_formatter.xml - - + + diff --git a/refarch-integrations/refarch-email-integration/README.md b/refarch-integrations/refarch-email-integration/README.md new file mode 100644 index 00000000..bfb2d331 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/README.md @@ -0,0 +1,34 @@ +# RefArch email integration + +Integration for sending text and html emails with attachments. Uses [s3-integration](../refarch-s3-integration) for file +handling. + +## Usage + +```xml + + + + de.muenchen.refarch + refarch-email-integration-starter + ... + + +``` + +and a [s3-integration starter](../refarch-s3-integration/README.md#usage). + +## Configuration + +### refarch-email-integration-starter + +| Property | Description | Example | +|-----------------------------------------|---------------------------------------------------|------------------------| +| `spring.mail.host` | Host of smtp server used for sending mails. | `mail.example.com` | +| `spring.mail.port` | Host of smtp server used for sending mails. | `1025` | +| `spring.mail.username` | Username of smtp server. | | +| `spring.mail.password` | Password of smtp server. | | +| `refarch.mail.from-address` | Default from address used when sending mails. | `test@example.com` | +| `refarch.mail.default-reply-to-address` | Default reply to address used when sending mails. | `no_reply@example.com` | + +In addition, properties of selected [s3-integration starter](../refarch-s3-integration/README.md#usage). diff --git a/refarch-integrations/refarch-email-integration/pom.xml b/refarch-integrations/refarch-email-integration/pom.xml new file mode 100644 index 00000000..36756574 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + + de.muenchen.refarch + refarch-integrations + 1.1.0-SNAPSHOT + + + refarch-email-integration + refarch-email-integration + 1.1.0-SNAPSHOT + pom + + + 3.0.4 + 1.4.0 + + + + refarch-email + refarch-email-integration-core + refarch-email-integration-starter + refarch-email-integration-rest-example + refarch-email-integration-java-example + + diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/pom.xml b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/pom.xml new file mode 100644 index 00000000..235f7950 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + refarch-email-integration + de.muenchen.refarch + 1.1.0-SNAPSHOT + + + refarch-email-integration-core + + + + de.muenchen.refarch + refarch-s3-integration-client-core + ${project.version} + + + de.muenchen.refarch + refarch-email-starter + ${project.version} + + + org.springframework + spring-web + + + org.apache.pdfbox + jbig2-imageio + ${jbig2-imageio.version} + + + com.github.jai-imageio + jai-imageio-jpeg2000 + ${jai-imageio-jpeg2000.version} + + + + diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/adapter/out/mail/MailAdapter.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/adapter/out/mail/MailAdapter.java new file mode 100644 index 00000000..ab4c8ecc --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/adapter/out/mail/MailAdapter.java @@ -0,0 +1,26 @@ +package de.muenchen.refarch.email.integration.adapter.out.mail; + +import de.muenchen.refarch.email.api.EmailApi; +import de.muenchen.refarch.email.integration.application.port.out.MailOutPort; +import de.muenchen.refarch.email.model.Mail; +import freemarker.template.TemplateException; +import jakarta.mail.MessagingException; +import java.io.IOException; +import java.util.Map; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class MailAdapter implements MailOutPort { + + private final EmailApi emailApi; + + @Override + public void sendMail(Mail mail, String logoPath) throws MessagingException { + this.emailApi.sendMail(mail, logoPath); + } + + @Override + public String getBodyFromTemplate(String templateName, Map content) throws TemplateException, IOException { + return this.emailApi.getBodyFromTemplate(templateName, content); + } +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/adapter/out/s3/S3Adapter.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/adapter/out/s3/S3Adapter.java new file mode 100644 index 00000000..35714783 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/adapter/out/s3/S3Adapter.java @@ -0,0 +1,73 @@ +package de.muenchen.refarch.email.integration.adapter.out.s3; + +import de.muenchen.refarch.email.integration.application.port.out.LoadMailAttachmentOutPort; +import de.muenchen.refarch.email.integration.domain.exception.LoadAttachmentError; +import de.muenchen.refarch.email.model.FileAttachment; +import de.muenchen.refarch.integration.s3.client.exception.DocumentStorageClientErrorException; +import de.muenchen.refarch.integration.s3.client.exception.DocumentStorageException; +import de.muenchen.refarch.integration.s3.client.exception.DocumentStorageServerErrorException; +import de.muenchen.refarch.integration.s3.client.repository.DocumentStorageFileRepository; +import de.muenchen.refarch.integration.s3.client.repository.DocumentStorageFolderRepository; +import de.muenchen.refarch.integration.s3.client.service.FileValidationService; +import jakarta.mail.util.ByteArrayDataSource; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FilenameUtils; + +@Slf4j +@RequiredArgsConstructor +public class S3Adapter implements LoadMailAttachmentOutPort { + + private final DocumentStorageFileRepository documentStorageFileRepository; + private final DocumentStorageFolderRepository documentStorageFolderRepository; + private final FileValidationService fileValidationService; + + @Override + public List loadAttachments(final List filePaths) { + final List attachments = new ArrayList<>(); + filePaths.forEach(path -> { + if (path.endsWith("/")) { + attachments.addAll(getFilesFromFolder(path)); + } else { + attachments.add(getFile(path)); + } + }); + return attachments; + } + + private List getFilesFromFolder(final String folderPath) { + try { + final List contents = new ArrayList<>(); + final Set filepath; + filepath = documentStorageFolderRepository.getAllFilesInFolderRecursively(folderPath); + if (Objects.isNull(filepath)) + throw new LoadAttachmentError("An folder could not be loaded from url: " + folderPath); + filepath.forEach(file -> contents.add(getFile(file))); + return contents; + } catch (final DocumentStorageException | DocumentStorageServerErrorException | DocumentStorageClientErrorException e) { + throw new LoadAttachmentError("An folder could not be loaded from path: " + folderPath); + } + } + + private FileAttachment getFile(final String filePath) { + try { + final byte[] bytes; + bytes = this.documentStorageFileRepository.getFile(filePath, 3); + final String mimeType = fileValidationService.detectFileType(bytes); + final String filename = FilenameUtils.getName(filePath); + final ByteArrayDataSource file = new ByteArrayDataSource(bytes, mimeType); + + // check if mimeType exists + if (!fileValidationService.isSupported(mimeType)) + throw new LoadAttachmentError("The type of this file is not supported: " + filePath); + + return new FileAttachment(filename, file); + } catch (final DocumentStorageException | DocumentStorageServerErrorException | DocumentStorageClientErrorException e) { + throw new LoadAttachmentError("An file could not be loaded from path: " + filePath); + } + } +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/application/port/in/SendMailInPort.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/application/port/in/SendMailInPort.java new file mode 100644 index 00000000..33bd8a94 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/application/port/in/SendMailInPort.java @@ -0,0 +1,13 @@ +package de.muenchen.refarch.email.integration.application.port.in; + +import de.muenchen.refarch.email.integration.domain.model.TemplateMail; +import de.muenchen.refarch.email.integration.domain.model.TextMail; +import jakarta.validation.Valid; + +public interface SendMailInPort { + + void sendMailWithText(@Valid final TextMail mail); + + void sendMailWithTemplate(@Valid final TemplateMail mail); + +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/application/port/out/LoadMailAttachmentOutPort.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/application/port/out/LoadMailAttachmentOutPort.java new file mode 100644 index 00000000..f56157a4 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/application/port/out/LoadMailAttachmentOutPort.java @@ -0,0 +1,8 @@ +package de.muenchen.refarch.email.integration.application.port.out; + +import de.muenchen.refarch.email.model.FileAttachment; +import java.util.List; + +public interface LoadMailAttachmentOutPort { + List loadAttachments(final List filePaths); +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/application/port/out/MailOutPort.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/application/port/out/MailOutPort.java new file mode 100644 index 00000000..20f8ecb3 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/application/port/out/MailOutPort.java @@ -0,0 +1,15 @@ +package de.muenchen.refarch.email.integration.application.port.out; + +import de.muenchen.refarch.email.model.Mail; +import freemarker.template.TemplateException; +import jakarta.mail.MessagingException; +import java.io.IOException; +import java.util.Map; + +public interface MailOutPort { + + void sendMail(Mail mail, String logoPath) throws MessagingException; + + String getBodyFromTemplate(String templateName, Map content) throws TemplateException, IOException; + +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/application/usecase/SendMailUseCase.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/application/usecase/SendMailUseCase.java new file mode 100644 index 00000000..7b460417 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/application/usecase/SendMailUseCase.java @@ -0,0 +1,88 @@ +package de.muenchen.refarch.email.integration.application.usecase; + +import de.muenchen.refarch.email.integration.application.port.in.SendMailInPort; +import de.muenchen.refarch.email.integration.application.port.out.LoadMailAttachmentOutPort; +import de.muenchen.refarch.email.integration.application.port.out.MailOutPort; +import de.muenchen.refarch.email.integration.domain.exception.TemplateError; +import de.muenchen.refarch.email.integration.domain.model.BasicMail; +import de.muenchen.refarch.email.integration.domain.model.TemplateMail; +import de.muenchen.refarch.email.integration.domain.model.TextMail; +import de.muenchen.refarch.email.model.FileAttachment; +import de.muenchen.refarch.email.model.Mail; +import freemarker.template.TemplateException; +import jakarta.mail.MessagingException; +import jakarta.validation.Valid; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.mail.MailSendException; +import org.springframework.validation.annotation.Validated; + +@Slf4j +@RequiredArgsConstructor +@Validated +public class SendMailUseCase implements SendMailInPort { + + private final LoadMailAttachmentOutPort loadAttachmentOutPort; + private final MailOutPort mailOutPort; + + /** + * Send a mail. + * + * @param mail mail that is sent + */ + @Override + public void sendMailWithText(@Valid final TextMail mail) { + Mail mailModel = createMail(mail, mail.getBody(), false); + + this.sendMail(mailModel, null); + } + + @Override + public void sendMailWithTemplate(@Valid final TemplateMail mail) throws TemplateError { + // get body from template + try { + Map content = new HashMap<>(mail.getContent()); + String body = this.mailOutPort.getBodyFromTemplate(mail.getTemplate(), content); + + Mail mailModel = createMail(mail, body, true); + + this.sendMail(mailModel, "templates/email-logo.png"); + + } catch (IOException ioException) { + throw new TemplateError("The template " + mail.getTemplate() + " could not be loaded"); + } catch (TemplateException templateException) { + throw new TemplateError(templateException.getMessage()); + } + } + + private Mail createMail(final BasicMail mail, final String body, final boolean htmlBody) { + // load Attachments + List attachments = loadAttachmentOutPort.loadAttachments(mail.getFilePaths()); + + // send mail + return new Mail( + mail.getReceivers(), + mail.getReceiversCc(), + mail.getReceiversBcc(), + mail.getSubject(), + body, + htmlBody, + null, + mail.getReplyTo(), + attachments); + } + + private void sendMail(Mail mailModel, String logoPath) throws MailSendException { + try { + this.mailOutPort.sendMail(mailModel, logoPath); + } catch (final MessagingException ex) { + log.error("Sending mail failed with exception: {}", ex.getMessage()); + throw new MailSendException(ex.getMessage()); + } + } + +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/exception/LoadAttachmentError.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/exception/LoadAttachmentError.java new file mode 100644 index 00000000..ba854d6b --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/exception/LoadAttachmentError.java @@ -0,0 +1,7 @@ +package de.muenchen.refarch.email.integration.domain.exception; + +public class LoadAttachmentError extends Error { + public LoadAttachmentError(final String message) { + super(message); + } +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/exception/TemplateError.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/exception/TemplateError.java new file mode 100644 index 00000000..7189207f --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/exception/TemplateError.java @@ -0,0 +1,7 @@ +package de.muenchen.refarch.email.integration.domain.exception; + +public class TemplateError extends Error { + public TemplateError(final String message) { + super(message); + } +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/model/BasicMail.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/model/BasicMail.java new file mode 100644 index 00000000..824e9720 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/model/BasicMail.java @@ -0,0 +1,41 @@ +package de.muenchen.refarch.email.integration.domain.model; + +import jakarta.validation.constraints.NotBlank; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class BasicMail { + /** + * Receiver addresses of the mail, comma separated. + */ + @NotBlank(message = "No receivers given") + private String receivers; + + /** + * CC-Receiver addresses of the mail, comma separated. + */ + private String receiversCc; + + /** + * BCC-Receiver addresses of the mail, comma separated. + */ + private String receiversBcc; + + /** + * Subject of the mail. + */ + @NotBlank(message = "No subject given") + private String subject; + + /** + * Reply to address + */ + private String replyTo; + /** + * List of attachment paths. + */ + private List filePaths; +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/model/TemplateMail.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/model/TemplateMail.java new file mode 100644 index 00000000..56ca2683 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/model/TemplateMail.java @@ -0,0 +1,32 @@ +package de.muenchen.refarch.email.integration.domain.model; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import java.util.List; +import java.util.Map; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) +@Getter +public class TemplateMail extends BasicMail { + + /** + * Template of the mail. + */ + @NotBlank(message = "No template given") + private final String template; + + /** + * Bottom body of the mail. + */ + @NotEmpty(message = "No content given") + private final Map content; + + public TemplateMail(String receivers, String receiversCc, String receiversBcc, String subject, String replyTo, List filePaths, + String template, Map content) { + super(receivers, receiversCc, receiversBcc, subject, replyTo, filePaths); + this.template = template; + this.content = content; + } +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/model/TextMail.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/model/TextMail.java new file mode 100644 index 00000000..315a381c --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/main/java/de/muenchen/refarch/email/integration/domain/model/TextMail.java @@ -0,0 +1,27 @@ +package de.muenchen.refarch.email.integration.domain.model; + +import jakarta.validation.constraints.NotBlank; +import java.util.List; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +/** + * Object contains all the information needed to send a mail. + */ +@EqualsAndHashCode(callSuper = true) +@Getter +public class TextMail extends BasicMail { + + /** + * Body of the mail. + */ + @NotBlank(message = "No body given") + private final String body; + + public TextMail(String receivers, String receiversCc, String receiversBcc, String subject, String body, String replyTo, + List filePaths) { + super(receivers, receiversCc, receiversBcc, subject, replyTo, filePaths); + this.body = body; + } + +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/java/de/muenchen/refarch/email/integration/adapter/out/mail/MailAdapterTest.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/java/de/muenchen/refarch/email/integration/adapter/out/mail/MailAdapterTest.java new file mode 100644 index 00000000..d39bc472 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/java/de/muenchen/refarch/email/integration/adapter/out/mail/MailAdapterTest.java @@ -0,0 +1,49 @@ +package de.muenchen.refarch.email.integration.adapter.out.mail; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.anyMap; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import de.muenchen.refarch.email.api.EmailApi; +import de.muenchen.refarch.email.model.Mail; +import freemarker.template.TemplateException; +import jakarta.mail.MessagingException; +import java.io.IOException; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class MailAdapterTest { + + private final EmailApi emailApi = mock(EmailApi.class); + + @Test + void sendMail() throws MessagingException { + final MailAdapter mailAdapter = new MailAdapter(emailApi); + final Mail mail = new Mail( + "receivers", + "receiversCc", + "receiversBcc", + "subject", + "body", + false, + null, + "replyTo", + null); + mailAdapter.sendMail(mail, "logoPath"); + verify(emailApi).sendMail(mail, "logoPath"); + } + + @Test + void getBodyFromTemplate() throws TemplateException, IOException { + final MailAdapter mailAdapter = new MailAdapter(emailApi); + when(emailApi.getBodyFromTemplate(anyString(), anyMap())).thenReturn("generated body"); + String body = mailAdapter.getBodyFromTemplate("template", Map.of("key", "value")); + + assertThat(body).isEqualTo("generated body"); + verify(emailApi).getBodyFromTemplate("template", Map.of("key", "value")); + } + +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/java/de/muenchen/refarch/email/integration/adapter/out/s3/S3AdapterTest.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/java/de/muenchen/refarch/email/integration/adapter/out/s3/S3AdapterTest.java new file mode 100644 index 00000000..cff7d114 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/java/de/muenchen/refarch/email/integration/adapter/out/s3/S3AdapterTest.java @@ -0,0 +1,80 @@ +package de.muenchen.refarch.email.integration.adapter.out.s3; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import de.muenchen.refarch.email.integration.domain.exception.LoadAttachmentError; +import de.muenchen.refarch.email.model.FileAttachment; +import de.muenchen.refarch.integration.s3.client.exception.DocumentStorageClientErrorException; +import de.muenchen.refarch.integration.s3.client.exception.DocumentStorageException; +import de.muenchen.refarch.integration.s3.client.exception.DocumentStorageServerErrorException; +import de.muenchen.refarch.integration.s3.client.repository.DocumentStorageFileRepository; +import de.muenchen.refarch.integration.s3.client.repository.DocumentStorageFolderRepository; +import de.muenchen.refarch.integration.s3.client.service.FileValidationService; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.core.io.ClassPathResource; +import org.springframework.util.unit.DataSize; + +@Slf4j +class S3AdapterTest { + private final DocumentStorageFileRepository documentStorageFileRepository = mock(DocumentStorageFileRepository.class); + private final DocumentStorageFolderRepository documentStorageFolderRepository = mock(DocumentStorageFolderRepository.class); + private final FileValidationService fileValidationService = new FileValidationService(null, DataSize.ofMegabytes(50), DataSize.ofMegabytes(110)); + + private S3Adapter s3Adapter; + + @BeforeEach + void setup() { + s3Adapter = new S3Adapter(documentStorageFileRepository, documentStorageFolderRepository, fileValidationService); + } + + @Test + void testLoadAttachment_DocumentStorageException() + throws DocumentStorageException, DocumentStorageClientErrorException, DocumentStorageServerErrorException { + final String path = "path/to/some-file.txt"; + + // DocumentStorageException + when(documentStorageFileRepository.getFile(eq(path), anyInt())) + .thenThrow(new DocumentStorageException("Some error", new RuntimeException("Some error"))); + assertThatThrownBy(() -> s3Adapter.loadAttachments(List.of(path))) + .isInstanceOf(LoadAttachmentError.class); + } + + @Test + void testLoadAttachment_Success() throws DocumentStorageException, DocumentStorageClientErrorException, DocumentStorageServerErrorException { + final Map files = Map.of( + "test-logo.png", "image/png", + "test-pdf.pdf", "application/pdf", + "test-word.docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"); + + for (final Map.Entry file : files.entrySet()) { + try { + final String path = "files/" + file.getKey(); + final byte[] testFile = new ClassPathResource(path).getInputStream().readAllBytes(); + when(documentStorageFileRepository.getFile(anyString(), anyInt())).thenReturn(testFile); + + final List fileAttachment = this.s3Adapter.loadAttachments(List.of(path)); + + assertThat(Arrays.equals(testFile, fileAttachment.getFirst().file().getInputStream().readAllBytes())).isTrue(); + assertThat(file.getKey()).isEqualTo(fileAttachment.getFirst().fileName()); + assertThat(file.getValue()).isEqualTo(fileAttachment.getFirst().file().getContentType()); + } catch (final IOException e) { + log.warn("Could not read file: {}", file); + fail(e.getMessage()); + } + } + } + +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/java/de/muenchen/refarch/email/integration/application/usecase/SendMailUseCaseTest.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/java/de/muenchen/refarch/email/integration/application/usecase/SendMailUseCaseTest.java new file mode 100644 index 00000000..f43cda9f --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/java/de/muenchen/refarch/email/integration/application/usecase/SendMailUseCaseTest.java @@ -0,0 +1,142 @@ +package de.muenchen.refarch.email.integration.application.usecase; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.catchThrowableOfType; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyMap; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import de.muenchen.refarch.email.integration.application.port.in.SendMailInPort; +import de.muenchen.refarch.email.integration.application.port.out.LoadMailAttachmentOutPort; +import de.muenchen.refarch.email.integration.application.port.out.MailOutPort; +import de.muenchen.refarch.email.integration.domain.exception.TemplateError; +import de.muenchen.refarch.email.integration.domain.model.TemplateMail; +import de.muenchen.refarch.email.integration.domain.model.TextMail; +import de.muenchen.refarch.email.model.FileAttachment; +import de.muenchen.refarch.email.model.Mail; +import freemarker.template.TemplateException; +import jakarta.mail.MessagingException; +import jakarta.mail.util.ByteArrayDataSource; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.mail.MailSendException; + +class SendMailUseCaseTest { + + private final LoadMailAttachmentOutPort loadMailAttachmentOutPort = mock(LoadMailAttachmentOutPort.class); + private final MailOutPort mailOutPort = mock(MailOutPort.class); + private final TextMail mail = new TextMail( + "mailReceiver1@muenchen.de,mailReceiver2@muenchen.de", + "receiverCC@muenchen.de", + "receiverBCC@muenchen.de", + "Test Mail", + "This is a test mail", + "test@muenchen.de", + List.of("folder/file.txt")); + private final TemplateMail templateMail = new TemplateMail( + "mailReceiver1@muenchen.de,mailReceiver2@muenchen.de", + "receiverCC@muenchen.de", + "receiverBCC@muenchen.de", + "Test Mail", + "test@muenchen.de", + List.of("folder/file.txt"), + "template", + Map.of("mail", Map.of())); + private SendMailInPort sendMailInPort; + + @BeforeEach + void setUp() { + this.sendMailInPort = new SendMailUseCase(loadMailAttachmentOutPort, mailOutPort); + } + + @Test + void sendMail() throws MessagingException { + sendMailInPort.sendMailWithText(mail); + final Mail mailOutModel = new Mail( + mail.getReceivers(), + mail.getReceiversCc(), + mail.getReceiversBcc(), + mail.getSubject(), + mail.getBody(), + false, + null, + mail.getReplyTo(), + List.of()); + verify(mailOutPort).sendMail(mailOutModel, null); + } + + @Test + void sendMailWithAttachments() throws MessagingException { + final FileAttachment fileAttachment = new FileAttachment("test.txt", new ByteArrayDataSource("Anhang Inhalt".getBytes(), "text/plain")); + when(loadMailAttachmentOutPort.loadAttachments(List.of("folder/file.txt"))).thenReturn(List.of(fileAttachment)); + + sendMailInPort.sendMailWithText(mail); + final Mail mailOutModel = new Mail( + mail.getReceivers(), + mail.getReceiversCc(), + mail.getReceiversBcc(), + mail.getSubject(), + mail.getBody(), + false, + null, + mail.getReplyTo(), + List.of(fileAttachment)); + verify(mailOutPort).sendMail(mailOutModel, null); + } + + @Test + void sendMailThrowsMailSendException() throws MessagingException { + doThrow(new MessagingException("Test Exception")).when(mailOutPort).sendMail(any(), any()); + assertThatThrownBy(() -> sendMailInPort.sendMailWithText(mail)).isInstanceOf(MailSendException.class); + } + + @Test + void sendMailWithTemplate() throws MessagingException, TemplateException, IOException { + when(mailOutPort.getBodyFromTemplate(anyString(), anyMap())).thenReturn("generated body"); + sendMailInPort.sendMailWithTemplate(templateMail); + final Mail mailOutModel = new Mail( + mail.getReceivers(), + mail.getReceiversCc(), + mail.getReceiversBcc(), + mail.getSubject(), + "generated body", + true, + null, + mail.getReplyTo(), + List.of()); + verify(mailOutPort).sendMail(mailOutModel, "templates/email-logo.png"); + } + + @Test + void sendMailWithTemplateThrowsIOException() throws TemplateException, IOException { + doThrow(new IOException("IO Exception")).when(mailOutPort).getBodyFromTemplate(anyString(), anyMap()); + TemplateError error = catchThrowableOfType(() -> sendMailInPort.sendMailWithTemplate(templateMail), TemplateError.class); + + String expectedMessage = "The template " + templateMail.getTemplate() + " could not be loaded"; + String actualMessage = error.getMessage(); + + assertThat(actualMessage).isEqualTo(expectedMessage); + } + + @Test + void sendMailWithTemplateThrowsTemplateException() throws TemplateException, IOException { + TemplateException templateException = mock(TemplateException.class); + when(templateException.getMessage()).thenReturn("Template Exception Message"); + doThrow(templateException).when(mailOutPort).getBodyFromTemplate(anyString(), anyMap()); + TemplateError error = catchThrowableOfType(() -> sendMailInPort.sendMailWithTemplate(templateMail), TemplateError.class); + + String expectedMessage = "Template Exception Message"; + String actualMessage = error.getMessage(); + + assertThat(actualMessage).isEqualTo(expectedMessage); + } + +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/resources/files/test-logo.png b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/resources/files/test-logo.png new file mode 100644 index 00000000..1db73ea0 Binary files /dev/null and b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/resources/files/test-logo.png differ diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/resources/files/test-pdf.pdf b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/resources/files/test-pdf.pdf new file mode 100644 index 00000000..9ee1f262 Binary files /dev/null and b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/resources/files/test-pdf.pdf differ diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/resources/files/test-word.docx b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/resources/files/test-word.docx new file mode 100644 index 00000000..43f35986 Binary files /dev/null and b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/resources/files/test-word.docx differ diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/resources/logback-test.xml b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/resources/logback-test.xml new file mode 100644 index 00000000..7beed25b --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-core/src/test/resources/logback-test.xml @@ -0,0 +1,29 @@ + + + + + + + + + + %date{yyyy.MM.dd HH:mm:ss.SSS} | %highlight(%level) | [%thread] | %cyan(%logger{0}) | [%file : %line] - + %msg%n + + + + + + + + + + + + + + + diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/pom.xml b/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/pom.xml new file mode 100644 index 00000000..37096c3a --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + de.muenchen.refarch + refarch-email-integration + 1.1.0-SNAPSHOT + + + refarch-email-integration-java-example + + + de.muenchen.refarch + refarch-email-integration-starter + ${project.version} + + + de.muenchen.refarch + refarch-s3-integration-java-client-starter + ${project.version} + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/src/main/java/de/muenchen/refarch/email/integration/EmailJavaExampleApplication.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/src/main/java/de/muenchen/refarch/email/integration/EmailJavaExampleApplication.java new file mode 100644 index 00000000..ceb02d27 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/src/main/java/de/muenchen/refarch/email/integration/EmailJavaExampleApplication.java @@ -0,0 +1,23 @@ +package de.muenchen.refarch.email.integration; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; + +@SpringBootApplication +@RequiredArgsConstructor +public class EmailJavaExampleApplication { + private final TestService testService; + + public static void main(final String[] args) { + SpringApplication.run(EmailJavaExampleApplication.class, args); + } + + @EventListener(ApplicationReadyEvent.class) + void sendTestMail() { + this.testService.testSendMail(); + System.exit(0); + } +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/src/main/java/de/muenchen/refarch/email/integration/TestService.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/src/main/java/de/muenchen/refarch/email/integration/TestService.java new file mode 100644 index 00000000..eebbf175 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/src/main/java/de/muenchen/refarch/email/integration/TestService.java @@ -0,0 +1,40 @@ +package de.muenchen.refarch.email.integration; + +import de.muenchen.refarch.email.integration.application.port.in.SendMailInPort; +import de.muenchen.refarch.email.integration.domain.model.TextMail; +import de.muenchen.refarch.integration.s3.client.repository.DocumentStorageFileRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class TestService { + private final SendMailInPort sendMailInPort; + private final DocumentStorageFileRepository documentStorageFileRepository; + + void testSendMail() { + this.uploadTestFile(); + TextMail mail = new TextMail( + "test.receiver@muenchen.de", + null, + null, + "Test", + "This is a test", + null, + List.of("/test/test-pdf.pdf")); + sendMailInPort.sendMailWithText(mail); + log.info("Test mail sent"); + } + + @SneakyThrows + void uploadTestFile() { + ClassPathResource resource = new ClassPathResource("/files/test-pdf.pdf"); + documentStorageFileRepository.updateFile("/test/test-pdf.pdf", resource.getContentAsByteArray(), 1); + log.info("Test file uploaded"); + } +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/src/main/resources/application-local.yml b/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/src/main/resources/application-local.yml new file mode 100644 index 00000000..7af049c8 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/src/main/resources/application-local.yml @@ -0,0 +1,17 @@ +server: + port: 8087 +spring: + mail: + host: localhost + port: 1025 + username: test@muenchen.de + password: secret +refarch: + mail: + from-address: test@muenchen.de + default-reply-to-address: no_reply@muenchen.de + s3: + bucket-name: test-bucket + access-key: minio + secret-key: Test1234 + url: http://localhost:9000 diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/src/main/resources/files/test-pdf.pdf b/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/src/main/resources/files/test-pdf.pdf new file mode 100644 index 00000000..9ee1f262 Binary files /dev/null and b/refarch-integrations/refarch-email-integration/refarch-email-integration-java-example/src/main/resources/files/test-pdf.pdf differ diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/pom.xml b/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/pom.xml new file mode 100644 index 00000000..47a5bbea --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + + de.muenchen.refarch + refarch-email-integration + 1.1.0-SNAPSHOT + + + refarch-email-integration-rest-example + + + de.muenchen.refarch + refarch-email-integration-starter + ${project.version} + + + de.muenchen.refarch + refarch-s3-integration-rest-client-starter + ${project.version} + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/src/main/java/de/muenchen/refarch/email/integration/EmailRestExampleApplication.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/src/main/java/de/muenchen/refarch/email/integration/EmailRestExampleApplication.java new file mode 100644 index 00000000..fee5c8b9 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/src/main/java/de/muenchen/refarch/email/integration/EmailRestExampleApplication.java @@ -0,0 +1,23 @@ +package de.muenchen.refarch.email.integration; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; + +@SpringBootApplication +@RequiredArgsConstructor +public class EmailRestExampleApplication { + private final TestService testService; + + public static void main(final String[] args) { + SpringApplication.run(EmailRestExampleApplication.class, args); + } + + @EventListener(ApplicationReadyEvent.class) + void sendTestMail() { + this.testService.testSendMail(); + System.exit(0); + } +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/src/main/java/de/muenchen/refarch/email/integration/TestService.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/src/main/java/de/muenchen/refarch/email/integration/TestService.java new file mode 100644 index 00000000..eebbf175 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/src/main/java/de/muenchen/refarch/email/integration/TestService.java @@ -0,0 +1,40 @@ +package de.muenchen.refarch.email.integration; + +import de.muenchen.refarch.email.integration.application.port.in.SendMailInPort; +import de.muenchen.refarch.email.integration.domain.model.TextMail; +import de.muenchen.refarch.integration.s3.client.repository.DocumentStorageFileRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class TestService { + private final SendMailInPort sendMailInPort; + private final DocumentStorageFileRepository documentStorageFileRepository; + + void testSendMail() { + this.uploadTestFile(); + TextMail mail = new TextMail( + "test.receiver@muenchen.de", + null, + null, + "Test", + "This is a test", + null, + List.of("/test/test-pdf.pdf")); + sendMailInPort.sendMailWithText(mail); + log.info("Test mail sent"); + } + + @SneakyThrows + void uploadTestFile() { + ClassPathResource resource = new ClassPathResource("/files/test-pdf.pdf"); + documentStorageFileRepository.updateFile("/test/test-pdf.pdf", resource.getContentAsByteArray(), 1); + log.info("Test file uploaded"); + } +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/src/main/resources/application-local.yml b/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/src/main/resources/application-local.yml new file mode 100644 index 00000000..f7438700 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/src/main/resources/application-local.yml @@ -0,0 +1,19 @@ +server: + port: 8087 +spring: + mail: + host: localhost + port: 1025 + username: test@muenchen.de + password: secret +refarch: + mail: + from-address: test@muenchen.de + default-reply-to-address: no_reply@muenchen.de + s3: + client: + document-storage-url: http://localhost:8086 + enable-security: true +SSO_ISSUER_URL: http://keycloak:8100/auth/realms/local_realm +SSO_S3_CLIENT_ID: local +SSO_S3_CLIENT_SECRET: client_secret diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/src/main/resources/files/test-pdf.pdf b/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/src/main/resources/files/test-pdf.pdf new file mode 100644 index 00000000..9ee1f262 Binary files /dev/null and b/refarch-integrations/refarch-email-integration/refarch-email-integration-rest-example/src/main/resources/files/test-pdf.pdf differ diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-starter/pom.xml b/refarch-integrations/refarch-email-integration/refarch-email-integration-starter/pom.xml new file mode 100644 index 00000000..0069d0d8 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-starter/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + de.muenchen.refarch + refarch-email-integration + 1.1.0-SNAPSHOT + + + refarch-email-integration-starter + + + de.muenchen.refarch + refarch-email-integration-core + ${project.version} + + + org.springframework.boot + spring-boot + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-starter + + + diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-starter/src/main/java/de/muenchen/refarch/email/integration/configuration/MailAutoConfiguration.java b/refarch-integrations/refarch-email-integration/refarch-email-integration-starter/src/main/java/de/muenchen/refarch/email/integration/configuration/MailAutoConfiguration.java new file mode 100644 index 00000000..f4200f60 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-starter/src/main/java/de/muenchen/refarch/email/integration/configuration/MailAutoConfiguration.java @@ -0,0 +1,45 @@ +package de.muenchen.refarch.email.integration.configuration; + +import de.muenchen.refarch.email.api.EmailApi; +import de.muenchen.refarch.email.integration.adapter.out.mail.MailAdapter; +import de.muenchen.refarch.email.integration.adapter.out.s3.S3Adapter; +import de.muenchen.refarch.email.integration.application.port.in.SendMailInPort; +import de.muenchen.refarch.email.integration.application.port.out.LoadMailAttachmentOutPort; +import de.muenchen.refarch.email.integration.application.port.out.MailOutPort; +import de.muenchen.refarch.email.integration.application.usecase.SendMailUseCase; +import de.muenchen.refarch.integration.s3.client.repository.DocumentStorageFileRepository; +import de.muenchen.refarch.integration.s3.client.repository.DocumentStorageFolderRepository; +import de.muenchen.refarch.integration.s3.client.service.FileValidationService; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.mail.MailProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@RequiredArgsConstructor +@EnableConfigurationProperties({ MailProperties.class }) +public class MailAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public SendMailInPort getSendMailPathsInPort(final LoadMailAttachmentOutPort loadAttachmentPort, final MailOutPort mailOutPort) { + return new SendMailUseCase(loadAttachmentPort, mailOutPort); + } + + @Bean + @ConditionalOnMissingBean + public LoadMailAttachmentOutPort getLoadMailAttachmentPort( + final DocumentStorageFileRepository documentStorageFileRepository, + final DocumentStorageFolderRepository documentStorageFolderRepository, + final FileValidationService fileValidationService) { + return new S3Adapter(documentStorageFileRepository, documentStorageFolderRepository, fileValidationService); + } + + @Bean + @ConditionalOnMissingBean + public MailOutPort getMailPort(final EmailApi emailApi) { + return new MailAdapter(emailApi); + } +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email-integration-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/refarch-integrations/refarch-email-integration/refarch-email-integration-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..c5a1177d --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email-integration-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +de.muenchen.refarch.email.integration.configuration.MailAutoConfiguration diff --git a/refarch-integrations/refarch-email-integration/refarch-email/pom.xml b/refarch-integrations/refarch-email-integration/refarch-email/pom.xml new file mode 100644 index 00000000..b265a47d --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email/pom.xml @@ -0,0 +1,18 @@ + + + 4.0.0 + + de.muenchen.refarch + refarch-email-integration + 1.1.0-SNAPSHOT + + + refarch-email + pom + + + refarch-email-api + refarch-email-starter + + + diff --git a/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/pom.xml b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/pom.xml new file mode 100644 index 00000000..f0ecbd87 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + + de.muenchen.refarch + refarch-email + 1.1.0-SNAPSHOT + + + refarch-email-api + + + + org.springframework.boot + spring-boot-starter-mail + + + org.apache.commons + commons-lang3 + + + jakarta.validation + jakarta.validation-api + 3.1.0 + + + com.googlecode.owasp-java-html-sanitizer + owasp-java-html-sanitizer + 20240325.1 + + + + org.springframework.boot + spring-boot-starter-freemarker + + + org.springframework + spring-webmvc + + + + + + diff --git a/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/main/java/de/muenchen/refarch/email/api/EmailApi.java b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/main/java/de/muenchen/refarch/email/api/EmailApi.java new file mode 100644 index 00000000..f94ad523 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/main/java/de/muenchen/refarch/email/api/EmailApi.java @@ -0,0 +1,22 @@ +package de.muenchen.refarch.email.api; + +import de.muenchen.refarch.email.model.Mail; +import freemarker.template.TemplateException; +import jakarta.mail.MessagingException; +import jakarta.validation.Valid; +import java.io.IOException; +import java.util.Map; + +public interface EmailApi { + + void sendMail(@Valid Mail mail) throws MessagingException; + + void sendMailWithDefaultLogo(@Valid Mail mail) throws MessagingException; + + void sendMail(@Valid Mail mail, String logoPath) throws MessagingException; + + String getBodyFromTemplate(String templateName, Map content) throws IOException, TemplateException; + + String getEmailBodyFromTemplate(String templatePath, Map content); + +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/main/java/de/muenchen/refarch/email/impl/EmailApiImpl.java b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/main/java/de/muenchen/refarch/email/impl/EmailApiImpl.java new file mode 100644 index 00000000..c8eb2121 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/main/java/de/muenchen/refarch/email/impl/EmailApiImpl.java @@ -0,0 +1,133 @@ +package de.muenchen.refarch.email.impl; + +import de.muenchen.refarch.email.api.EmailApi; +import de.muenchen.refarch.email.model.Mail; +import freemarker.template.Template; +import freemarker.template.TemplateException; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; +import jakarta.validation.Valid; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.owasp.html.PolicyFactory; +import org.owasp.html.Sanitizers; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.ui.freemarker.FreeMarkerTemplateUtils; +import org.springframework.util.FileCopyUtils; +import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; + +@Slf4j +@RequiredArgsConstructor +public class EmailApiImpl implements EmailApi { + + private final JavaMailSender mailSender; + private final ResourceLoader resourceLoader; + private final FreeMarkerConfigurer freeMarkerConfigurer; + private final String fromAddress; + private final String defaultReplyToAddress; + // use a prepackaged sanitizer to prevent XSS + private final PolicyFactory policy = Sanitizers.BLOCKS + .and(Sanitizers.FORMATTING) + .and(Sanitizers.LINKS) + .and(Sanitizers.TABLES); + + @Override + public void sendMail(@Valid Mail mail) throws MessagingException { + this.sendMail(mail, null); + } + + @Override + public void sendMailWithDefaultLogo(@Valid Mail mail) throws MessagingException { + this.sendMail(mail, "bausteine/mail/email-logo.png"); + } + + @Override + public void sendMail(@Valid Mail mail, String logoPath) throws MessagingException { + final MimeMessage mimeMessage = this.mailSender.createMimeMessage(); + + mimeMessage.setRecipients(Message.RecipientType.TO, InternetAddress.parse(mail.receivers())); + + if (mail.hasReceiversCc()) { + mimeMessage.setRecipients(Message.RecipientType.CC, InternetAddress.parse(mail.receiversCc())); + } + if (mail.hasReceiversBcc()) { + mimeMessage.setRecipients(Message.RecipientType.BCC, InternetAddress.parse(mail.receiversBcc())); + } + + if (mail.hasReplyTo()) { + mimeMessage.setReplyTo(InternetAddress.parse(mail.replyTo())); + } else if (defaultReplyToAddress != null) { + mimeMessage.setReplyTo(InternetAddress.parse(defaultReplyToAddress)); + } + + final var helper = new MimeMessageHelper(mimeMessage, true, StandardCharsets.UTF_8.name()); + + helper.setSubject(mail.subject()); + helper.setText(mail.body(), mail.htmlBody()); + // use custom sender + helper.setFrom(mail.hasSender() ? mail.sender() : this.fromAddress); + + // mail attachments + if (mail.hasAttachment()) { + for (val attachment : mail.attachments()) { + helper.addAttachment(attachment.fileName(), attachment.file()); + } + } + + // logo + if (logoPath != null) { + final Resource logo = this.getRessourceFromClassPath(logoPath); + helper.addInline("logo", logo); + } + + this.mailSender.send(mimeMessage); + log.info("Mail {} sent to {}.", mail.subject(), mail.receivers()); + } + + @Override + public String getBodyFromTemplate(String templateName, Map content) throws IOException, TemplateException { + Template template = freeMarkerConfigurer.getConfiguration().getTemplate(templateName); + return FreeMarkerTemplateUtils.processTemplateIntoString(template, content); + } + + @Override + public String getEmailBodyFromTemplate(String templatePath, Map content) { + String mailTemplate = this.getTemplate(templatePath); + for (val entry : content.entrySet()) { + // make sure inputs are sanitized to prevent XSS + final String value = policy.sanitize(entry.getValue()); + // Make sure new lines are converted to
tags + mailTemplate = mailTemplate.replaceAll(entry.getKey(), value.replaceAll("(\r\n|\n\r|\r|\n)", "
")); + } + return mailTemplate; + } + + private String getTemplate(String templatePath) { + final Resource resource = this.getRessourceFromClassPath(templatePath); + if (!resource.exists()) { + log.error("Email Template not found: {}", templatePath); + throw new RuntimeException("Email Template not found: " + templatePath); + } + + try { + byte[] byteArray = FileCopyUtils.copyToByteArray(resource.getInputStream()); + return new String(byteArray, StandardCharsets.UTF_8); + } catch (Exception e) { + log.warn("Failed to load file: {}", templatePath); + throw new RuntimeException("Failed to load file: " + templatePath, e); + } + } + + private Resource getRessourceFromClassPath(String path) { + return resourceLoader.getResource("classpath:" + path); + } +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/main/java/de/muenchen/refarch/email/model/FileAttachment.java b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/main/java/de/muenchen/refarch/email/model/FileAttachment.java new file mode 100644 index 00000000..284e839b --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/main/java/de/muenchen/refarch/email/model/FileAttachment.java @@ -0,0 +1,8 @@ +package de.muenchen.refarch.email.model; + +import jakarta.mail.util.ByteArrayDataSource; + +public record FileAttachment( + String fileName, + ByteArrayDataSource file) { +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/main/java/de/muenchen/refarch/email/model/Mail.java b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/main/java/de/muenchen/refarch/email/model/Mail.java new file mode 100644 index 00000000..7bdb35d3 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/main/java/de/muenchen/refarch/email/model/Mail.java @@ -0,0 +1,37 @@ +package de.muenchen.refarch.email.model; + +import jakarta.validation.constraints.NotBlank; +import java.util.List; +import org.apache.commons.lang3.StringUtils; + +public record Mail( + @NotBlank String receivers, + String receiversCc, + String receiversBcc, + @NotBlank String subject, + @NotBlank String body, + boolean htmlBody, + String sender, + String replyTo, + List attachments) { + + public boolean hasAttachment() { + return attachments != null && !attachments.isEmpty(); + } + + public boolean hasReplyTo() { + return StringUtils.isNotBlank(this.replyTo); + } + + public boolean hasReceiversCc() { + return StringUtils.isNotBlank(this.receiversCc); + } + + public boolean hasReceiversBcc() { + return StringUtils.isNotBlank(this.receiversBcc); + } + + public boolean hasSender() { + return StringUtils.isNotBlank(this.sender); + } +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/test/java/de/muenchen/refarch/email/EmailApiImplTest.java b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/test/java/de/muenchen/refarch/email/EmailApiImplTest.java new file mode 100644 index 00000000..c622ed23 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/test/java/de/muenchen/refarch/email/EmailApiImplTest.java @@ -0,0 +1,356 @@ +package de.muenchen.refarch.email; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import de.muenchen.refarch.email.api.EmailApi; +import de.muenchen.refarch.email.impl.EmailApiImpl; +import de.muenchen.refarch.email.model.FileAttachment; +import de.muenchen.refarch.email.model.Mail; +import freemarker.template.Configuration; +import freemarker.template.TemplateException; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.MimeMultipart; +import jakarta.mail.util.ByteArrayDataSource; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; + +class EmailApiImplTest { + + private final JavaMailSender javaMailSender = mock(JavaMailSender.class); + private final ResourceLoader resourceLoader = mock(ResourceLoader.class); + private final FreeMarkerConfigurer freeMarkerConfigurer = mock(FreeMarkerConfigurer.class); + // test data + private final String receiver = "mailReceiver1@muenchen.de,mailReceiver2@muenchen.de"; + private final String receiverCC = "receiverCC@muenchen.de"; + private final String receiverBCC = "receiverBCC@muenchen.de"; + private final String subject = "Test Mail"; + private final String body = "This is a test mail"; + private final String replyTo = "test@muenchen.de"; + private final String defaultReplyTo = "noreply@muenchen.de"; + private final String sender = "some-custom-sender@muenchen.de"; + private EmailApi emailApi; + + @BeforeEach + void setUp() { + when(this.javaMailSender.createMimeMessage()).thenReturn(new MimeMessage((Session) null)); + this.emailApi = new EmailApiImpl(this.javaMailSender, this.resourceLoader, freeMarkerConfigurer, "test@muenchen.de", defaultReplyTo); + } + + @Test + void testSendMail() throws MessagingException, IOException { + final Mail mail = new Mail( + this.receiver, + null, + null, + this.subject, + this.body, + false, + null, + null, + null); + this.emailApi.sendMail(mail); + + final ArgumentCaptor messageArgumentCaptor = ArgumentCaptor.forClass(MimeMessage.class); + verify(this.javaMailSender).send(messageArgumentCaptor.capture()); + + assertThat(messageArgumentCaptor.getValue().getAllRecipients()).hasSize(2); + assertThat(messageArgumentCaptor.getValue().getSubject()).isEqualTo(this.subject); + assertThat(messageArgumentCaptor.getValue().getReplyTo()).hasSize(1); + assertThat(messageArgumentCaptor.getValue().getReplyTo()).contains(new InternetAddress(this.defaultReplyTo)); + final MimeMultipart content = (MimeMultipart) messageArgumentCaptor.getValue().getContent(); + assertThat(content.getContentType()).contains("multipart/mixed"); + } + + @Test + void testSendMailNoDefaultReplyTo() throws MessagingException, IOException { + var customAddress = new InternetAddress("custom.test@muenchen.de"); + + final Mail mail = new Mail( + this.receiver, + null, + null, + this.subject, + this.body, + false, + null, + null, + null); + new EmailApiImpl(this.javaMailSender, this.resourceLoader, freeMarkerConfigurer, customAddress.getAddress(), null).sendMail(mail); + + final ArgumentCaptor messageArgumentCaptor = ArgumentCaptor.forClass(MimeMessage.class); + verify(this.javaMailSender).send(messageArgumentCaptor.capture()); + + assertThat(messageArgumentCaptor.getValue().getAllRecipients()).hasSize(2); + assertThat(messageArgumentCaptor.getValue().getSubject()).isEqualTo(this.subject); + assertThat(messageArgumentCaptor.getValue().getReplyTo()).hasSize(1); + assertThat(messageArgumentCaptor.getValue().getReplyTo()).contains(new InternetAddress(customAddress.getAddress())); + final MimeMultipart content = (MimeMultipart) messageArgumentCaptor.getValue().getContent(); + assertThat(content.getContentType()).contains("multipart/mixed"); + } + + @Test + void testSendMailWithOptions() throws MessagingException, IOException { + final Mail mail = new Mail( + this.receiver, + this.receiverCC, + this.receiverBCC, + this.subject, + this.body, + false, + this.sender, + this.replyTo, + null); + this.emailApi.sendMail(mail); + + final ArgumentCaptor messageArgumentCaptor = ArgumentCaptor.forClass(MimeMessage.class); + verify(this.javaMailSender).send(messageArgumentCaptor.capture()); + + assertThat(messageArgumentCaptor.getValue().getAllRecipients()).hasSize(4); + assertThat(messageArgumentCaptor.getValue().getAllRecipients()).containsAll( + List.of(new InternetAddress("mailReceiver1@muenchen.de"), new InternetAddress("mailReceiver2@muenchen.de"), + new InternetAddress(this.receiverCC), new InternetAddress(this.receiverBCC))); + assertThat(messageArgumentCaptor.getValue().getSubject()).isEqualTo(this.subject); + assertThat(messageArgumentCaptor.getValue().getReplyTo()).hasSize(1); + assertThat(messageArgumentCaptor.getValue().getReplyTo()).contains(new InternetAddress(this.replyTo)); + assertThat(messageArgumentCaptor.getValue().getFrom()).contains(new InternetAddress(this.sender)); + final MimeMultipart content = (MimeMultipart) messageArgumentCaptor.getValue().getContent(); + assertThat(content.getContentType()).contains("multipart/mixed"); + } + + @Test + void sendMailWithAttachments() throws MessagingException, IOException { + final Mail mail = new Mail( + this.receiver, + null, + null, + this.subject, + this.body, + false, + null, + null, + List.of( + new FileAttachment( + "Testanhang", + new ByteArrayDataSource("FooBar".getBytes(), "text/plain")))); + this.emailApi.sendMail(mail); + + final ArgumentCaptor messageArgumentCaptor = ArgumentCaptor.forClass(MimeMessage.class); + verify(this.javaMailSender).send(messageArgumentCaptor.capture()); + + assertThat(messageArgumentCaptor.getValue().getAllRecipients()).hasSize(2); + assertThat(messageArgumentCaptor.getValue().getSubject()).isEqualTo(this.subject); + final MimeMultipart content = (MimeMultipart) messageArgumentCaptor.getValue().getContent(); + assertThat(content.getContentType()).contains("multipart/mixed"); + } + + @Test + void sendMailWithMultipleReplyToAddresses() throws MessagingException, IOException { + var reply1 = new InternetAddress("address1@muenchen.de"); + var reply2 = new InternetAddress("address2@muenchen.de"); + + final Mail mail = new Mail( + this.receiver, + null, + null, + this.subject, + this.body, + false, + null, + reply1.getAddress() + "," + reply2.getAddress(), + null); + this.emailApi.sendMail(mail); + + final ArgumentCaptor messageArgumentCaptor = ArgumentCaptor.forClass(MimeMessage.class); + verify(this.javaMailSender).send(messageArgumentCaptor.capture()); + + assertThat(messageArgumentCaptor.getValue().getAllRecipients()).hasSize(2); + assertThat(messageArgumentCaptor.getValue().getSubject()).isEqualTo(this.subject); + assertThat(messageArgumentCaptor.getValue().getReplyTo()).hasSize(2); + assertThat(messageArgumentCaptor.getValue().getReplyTo()).containsAll(List.of(reply1, reply2)); + final MimeMultipart content = (MimeMultipart) messageArgumentCaptor.getValue().getContent(); + assertThat(content.getContentType()).contains("multipart/mixed"); + } + + @Test + void sendMailWithDefaultLogo() throws MessagingException, IOException { + when(this.resourceLoader.getResource(anyString())).thenReturn(this.getResourceForText("Default Logo", true)); + + final Mail mail = new Mail( + this.receiver, + null, + null, + this.subject, + this.body, + false, + null, + null, + null); + this.emailApi.sendMailWithDefaultLogo(mail); + + final ArgumentCaptor messageArgumentCaptor = ArgumentCaptor.forClass(MimeMessage.class); + verify(this.javaMailSender).send(messageArgumentCaptor.capture()); + verify(this.resourceLoader).getResource("classpath:bausteine/mail/email-logo.png"); + + assertThat(messageArgumentCaptor.getValue().getAllRecipients()).hasSize(2); + assertThat(messageArgumentCaptor.getValue().getSubject()).isEqualTo(this.subject); + final MimeMultipart content = (MimeMultipart) messageArgumentCaptor.getValue().getContent(); + assertThat(content.getContentType()).contains("multipart/mixed"); + } + + @Test + void sendMailWithCustomLogo() throws MessagingException, IOException { + when(this.resourceLoader.getResource(anyString())).thenReturn(this.getResourceForText("Custom Logo", true)); + + final Mail mail = new Mail( + this.receiver, + null, + null, + this.subject, + this.body, + false, + null, + null, + null); + this.emailApi.sendMail(mail, "some/random/path/on/classpath"); + + final ArgumentCaptor messageArgumentCaptor = ArgumentCaptor.forClass(MimeMessage.class); + verify(this.javaMailSender).send(messageArgumentCaptor.capture()); + verify(this.resourceLoader).getResource("classpath:some/random/path/on/classpath"); + + assertThat(messageArgumentCaptor.getValue().getAllRecipients()).hasSize(2); + assertThat(messageArgumentCaptor.getValue().getSubject()).isEqualTo(this.subject); + final MimeMultipart content = (MimeMultipart) messageArgumentCaptor.getValue().getContent(); + assertThat(content.getContentType()).contains("multipart/mixed"); + } + + @Test + void testGetBodyFromFreemarkerTemplate() throws IOException, TemplateException { + final String templateName = "test-template.ftl"; + Map content = Map.of("data", "test"); + Configuration configuration = new Configuration(Configuration.VERSION_2_3_30); + configuration.setClassForTemplateLoading(this.getClass(), "/templates/"); + when(this.freeMarkerConfigurer.getConfiguration()).thenReturn(configuration); + + final String result = this.emailApi.getBodyFromTemplate(templateName, content); + + assertThat(result.contains("test")).isTrue(); + } + + @Test + void testGetEmailBodyFromTemplate() { + when(this.resourceLoader.getResource(anyString())).thenReturn(this.getResourceForText("This is a test mail", true)); + + final String templatePath = "bausteine/mail/email-logo.png"; + final String result = this.emailApi.getEmailBodyFromTemplate(templatePath, Map.of()); + + assertThat(result).isEqualTo("This is a test mail"); + } + + @Test + void testGetEmailBodyFromTemplateWithContent() { + when(this.resourceLoader.getResource(anyString())).thenReturn(this.getResourceForText("This is a test mail with content", true)); + + final String templatePath = "bausteine/mail/email-logo.png"; + final String result = this.emailApi.getEmailBodyFromTemplate(templatePath, Map.of("content", "some content")); + + assertThat(result).isEqualTo("This is a test mail with some content"); + } + + @Test + void testGetEmailBodyFromTemplateWithContentAndNewLines() { + when(this.resourceLoader.getResource(anyString())).thenReturn(this.getResourceForText("This is a test mail with content", true)); + + final String templatePath = "bausteine/mail/email-logo.png"; + final String result = this.emailApi.getEmailBodyFromTemplate(templatePath, Map.of("content", "some content \n with new line")); + + assertThat(result).isEqualTo("This is a test mail with some content
with new line"); + } + + @Test + void testGetEmailBodyFromTemplateWithContentFailsIfTemplateDoesNotExist() { + when(this.resourceLoader.getResource(anyString())).thenReturn(this.getResourceForText("foo bar", false)); + + final String templatePath = "some/temlate/that/does/not/exist"; + assertThatThrownBy(() -> { + this.emailApi.getEmailBodyFromTemplate(templatePath, Map.of("content", "some content")); + }) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("Email Template not found: " + templatePath); + } + + private Resource getResourceForText(final String text, final boolean resourceExists) { + return new Resource() { + @Override + public boolean exists() { + return resourceExists; + } + + @Override + public URL getURL() throws IOException { + return null; + } + + @Override + public URI getURI() throws IOException { + return null; + } + + @Override + public File getFile() throws IOException { + return null; + } + + @Override + public long contentLength() throws IOException { + return 0; + } + + @Override + public long lastModified() throws IOException { + return 0; + } + + @Override + public Resource createRelative(String relativePath) throws IOException { + return null; + } + + @Override + public String getFilename() { + return "test.txt"; + } + + @Override + public String getDescription() { + return null; + } + + @Override + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(text.getBytes()); + } + }; + } + +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/test/resources/templates/test-template.ftl b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/test/resources/templates/test-template.ftl new file mode 100644 index 00000000..1e47f8a2 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-api/src/test/resources/templates/test-template.ftl @@ -0,0 +1,9 @@ + + + + Titel + + +

Zweck dieser E-Mail ist ein ${data}

+ + diff --git a/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-starter/pom.xml b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-starter/pom.xml new file mode 100644 index 00000000..a4251084 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-starter/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + de.muenchen.refarch + refarch-email + 1.1.0-SNAPSHOT + + + refarch-email-starter + + + + de.muenchen.refarch + refarch-email-api + ${project.version} + + + org.springframework.boot + spring-boot + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + diff --git a/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-starter/src/main/java/de/muenchen/refarch/email/configuration/EmailAutoConfiguration.java b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-starter/src/main/java/de/muenchen/refarch/email/configuration/EmailAutoConfiguration.java new file mode 100644 index 00000000..976b08cd --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-starter/src/main/java/de/muenchen/refarch/email/configuration/EmailAutoConfiguration.java @@ -0,0 +1,63 @@ +package de.muenchen.refarch.email.configuration; + +import de.muenchen.refarch.email.api.EmailApi; +import de.muenchen.refarch.email.impl.EmailApiImpl; +import de.muenchen.refarch.email.properties.CustomMailProperties; +import jakarta.mail.MessagingException; +import java.util.Properties; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.mail.MailProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.core.io.ResourceLoader; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; +import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; + +@RequiredArgsConstructor +@EnableConfigurationProperties({ MailProperties.class, CustomMailProperties.class }) +public class EmailAutoConfiguration { + + private final MailProperties mailProperties; + private final CustomMailProperties customMailProperties; + + /** + * Configures the {@link JavaMailSender} + * + * @return configured JavaMailSender + */ + @Bean + @ConditionalOnMissingBean + public JavaMailSender getJavaMailSender() throws MessagingException { + final JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); + mailSender.setHost(this.mailProperties.getHost()); + mailSender.setPort(this.mailProperties.getPort()); + mailSender.setProtocol(this.mailProperties.getProtocol()); + mailSender.setUsername(this.mailProperties.getUsername()); + mailSender.setPassword(this.mailProperties.getPassword()); + + final Properties props = mailSender.getJavaMailProperties(); + props.putAll(this.mailProperties.getProperties()); + mailSender.setJavaMailProperties(props); + mailSender.testConnection(); + return mailSender; + } + + @ConditionalOnMissingBean + @Bean + public EmailApi emailApi(final ResourceLoader resourceLoader, final JavaMailSender javaMailSender, + final FreeMarkerConfigurer freeMarkerConfigurer) { + return new EmailApiImpl(javaMailSender, resourceLoader, freeMarkerConfigurer, this.customMailProperties.getFromAddress(), + this.customMailProperties.getDefaultReplyToAddress()); + } + + @Bean + @ConditionalOnMissingBean + public FreeMarkerConfigurer freemarkerConfig() { + FreeMarkerConfigurer freeMarkerConfigurer = new FreeMarkerConfigurer(); + freeMarkerConfigurer.setTemplateLoaderPath("classpath:templates/"); + return freeMarkerConfigurer; + } + +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-starter/src/main/java/de/muenchen/refarch/email/properties/CustomMailProperties.java b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-starter/src/main/java/de/muenchen/refarch/email/properties/CustomMailProperties.java new file mode 100644 index 00000000..af903421 --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-starter/src/main/java/de/muenchen/refarch/email/properties/CustomMailProperties.java @@ -0,0 +1,20 @@ +package de.muenchen.refarch.email.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Data +@ConfigurationProperties(prefix = "refarch.mail") +public class CustomMailProperties { + + /** + * Sender mail address. + */ + private String fromAddress; + + /** + * Default Reply-to mail address, e.g. no-reply@domain + */ + private String defaultReplyToAddress; + +} diff --git a/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..e67a5bac --- /dev/null +++ b/refarch-integrations/refarch-email-integration/refarch-email/refarch-email-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +de.muenchen.refarch.email.configuration.EmailAutoConfiguration diff --git a/refarch-integrations/refarch-s3-integration/pom.xml b/refarch-integrations/refarch-s3-integration/pom.xml index 3842928e..a94fa86f 100644 --- a/refarch-integrations/refarch-s3-integration/pom.xml +++ b/refarch-integrations/refarch-s3-integration/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 diff --git a/refarch-integrations/refarch-s3-integration/refarch-s3-integration-client-core/pom.xml b/refarch-integrations/refarch-s3-integration/refarch-s3-integration-client-core/pom.xml index b9540802..0999d145 100644 --- a/refarch-integrations/refarch-s3-integration/refarch-s3-integration-client-core/pom.xml +++ b/refarch-integrations/refarch-s3-integration/refarch-s3-integration-client-core/pom.xml @@ -1,6 +1,5 @@ - + 4.0.0 diff --git a/refarch-integrations/refarch-s3-integration/refarch-s3-integration-core/pom.xml b/refarch-integrations/refarch-s3-integration/refarch-s3-integration-core/pom.xml index 794be141..b23d2b3e 100644 --- a/refarch-integrations/refarch-s3-integration/refarch-s3-integration-core/pom.xml +++ b/refarch-integrations/refarch-s3-integration/refarch-s3-integration-core/pom.xml @@ -1,6 +1,5 @@ - + 4.0.0 diff --git a/refarch-integrations/refarch-s3-integration/refarch-s3-integration-java/pom.xml b/refarch-integrations/refarch-s3-integration/refarch-s3-integration-java/pom.xml index 626362c2..62a59c94 100644 --- a/refarch-integrations/refarch-s3-integration/refarch-s3-integration-java/pom.xml +++ b/refarch-integrations/refarch-s3-integration/refarch-s3-integration-java/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 diff --git a/refarch-integrations/refarch-s3-integration/refarch-s3-integration-java/refarch-s3-integration-java-client-starter/pom.xml b/refarch-integrations/refarch-s3-integration/refarch-s3-integration-java/refarch-s3-integration-java-client-starter/pom.xml index 1c4a5cd8..f8b1c71a 100644 --- a/refarch-integrations/refarch-s3-integration/refarch-s3-integration-java/refarch-s3-integration-java-client-starter/pom.xml +++ b/refarch-integrations/refarch-s3-integration/refarch-s3-integration-java/refarch-s3-integration-java-client-starter/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 diff --git a/refarch-integrations/refarch-s3-integration/refarch-s3-integration-java/refarch-s3-integration-java-client/pom.xml b/refarch-integrations/refarch-s3-integration/refarch-s3-integration-java/refarch-s3-integration-java-client/pom.xml index 8b9d9ef2..ddbb1cec 100644 --- a/refarch-integrations/refarch-s3-integration/refarch-s3-integration-java/refarch-s3-integration-java-client/pom.xml +++ b/refarch-integrations/refarch-s3-integration/refarch-s3-integration-java/refarch-s3-integration-java-client/pom.xml @@ -1,6 +1,5 @@ - + 4.0.0 diff --git a/refarch-integrations/refarch-s3-integration/refarch-s3-integration-rest/pom.xml b/refarch-integrations/refarch-s3-integration/refarch-s3-integration-rest/pom.xml index e3e2b057..745d983a 100644 --- a/refarch-integrations/refarch-s3-integration/refarch-s3-integration-rest/pom.xml +++ b/refarch-integrations/refarch-s3-integration/refarch-s3-integration-rest/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 diff --git a/refarch-integrations/refarch-s3-integration/refarch-s3-integration-rest/refarch-s3-integration-rest-client-starter/pom.xml b/refarch-integrations/refarch-s3-integration/refarch-s3-integration-rest/refarch-s3-integration-rest-client-starter/pom.xml index e64f703c..9983ae1d 100644 --- a/refarch-integrations/refarch-s3-integration/refarch-s3-integration-rest/refarch-s3-integration-rest-client-starter/pom.xml +++ b/refarch-integrations/refarch-s3-integration/refarch-s3-integration-rest/refarch-s3-integration-rest-client-starter/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 diff --git a/refarch-integrations/refarch-s3-integration/refarch-s3-integration-rest/refarch-s3-integration-rest-client/pom.xml b/refarch-integrations/refarch-s3-integration/refarch-s3-integration-rest/refarch-s3-integration-rest-client/pom.xml index dd5eb796..03bc4722 100644 --- a/refarch-integrations/refarch-s3-integration/refarch-s3-integration-rest/refarch-s3-integration-rest-client/pom.xml +++ b/refarch-integrations/refarch-s3-integration/refarch-s3-integration-rest/refarch-s3-integration-rest-client/pom.xml @@ -1,6 +1,5 @@ - + 4.0.0 diff --git a/refarch-integrations/refarch-s3-integration/refarch-s3-integration-rest/refarch-s3-integration-rest-core/pom.xml b/refarch-integrations/refarch-s3-integration/refarch-s3-integration-rest/refarch-s3-integration-rest-core/pom.xml index 97f0d0fd..d7e77e38 100644 --- a/refarch-integrations/refarch-s3-integration/refarch-s3-integration-rest/refarch-s3-integration-rest-core/pom.xml +++ b/refarch-integrations/refarch-s3-integration/refarch-s3-integration-rest/refarch-s3-integration-rest-core/pom.xml @@ -1,6 +1,5 @@ - + 4.0.0 diff --git a/refarch-integrations/refarch-s3-integration/refarch-s3-integration-rest/refarch-s3-integration-rest-service/pom.xml b/refarch-integrations/refarch-s3-integration/refarch-s3-integration-rest/refarch-s3-integration-rest-service/pom.xml index ead10a9c..4faf9536 100644 --- a/refarch-integrations/refarch-s3-integration/refarch-s3-integration-rest/refarch-s3-integration-rest-service/pom.xml +++ b/refarch-integrations/refarch-s3-integration/refarch-s3-integration-rest/refarch-s3-integration-rest-service/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 diff --git a/refarch-integrations/refarch-s3-integration/refarch-s3-integration-rest/refarch-s3-integration-rest-starter/pom.xml b/refarch-integrations/refarch-s3-integration/refarch-s3-integration-rest/refarch-s3-integration-rest-starter/pom.xml index 7c2205e7..78b56ead 100644 --- a/refarch-integrations/refarch-s3-integration/refarch-s3-integration-rest/refarch-s3-integration-rest-starter/pom.xml +++ b/refarch-integrations/refarch-s3-integration/refarch-s3-integration-rest/refarch-s3-integration-rest-starter/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 diff --git a/refarch-integrations/refarch-s3-integration/refarch-s3-integration-starter/pom.xml b/refarch-integrations/refarch-s3-integration/refarch-s3-integration-starter/pom.xml index 8394b713..1820a0eb 100644 --- a/refarch-integrations/refarch-s3-integration/refarch-s3-integration-starter/pom.xml +++ b/refarch-integrations/refarch-s3-integration/refarch-s3-integration-starter/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 diff --git a/stack/docker-compose-refarch-base.yml b/stack/docker-compose-refarch-base.yml index 1dbb07d1..617882fa 100644 --- a/stack/docker-compose-refarch-base.yml +++ b/stack/docker-compose-refarch-base.yml @@ -98,7 +98,7 @@ services: refarch-gateway: container_name: refarch-gateway - image: ghcr.io/it-at-m/refarch/refarch-gateway:1.0.0@sha256:78af746de560bae737e2ea476bd1759f4dd360ff9f2bb7647b8ed27599ed66ed + image: ghcr.io/it-at-m/refarch/refarch-gateway:1.1.0@sha256:a1313ada8f2a5cfc73fe2adb2bd11f0db5717f3a07dc0482b6ad7d00a9509bb1 depends_on: init-keycloak: condition: service_completed_successfully