diff --git a/.emails-version b/.emails-version new file mode 100644 index 0000000..0ea3a94 --- /dev/null +++ b/.emails-version @@ -0,0 +1 @@ +0.2.0 diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml new file mode 100644 index 0000000..e47b34a --- /dev/null +++ b/.github/workflows/CI.yaml @@ -0,0 +1,88 @@ +# 🐻‍❄️💌 email-service: charted's email service built in Rust that can be connected via gRPC +# Copyright 2023 Noelware, LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: CI +on: + workflow_dispatch: {} + pull_request: + types: [opened, synchronize] + branches: + - main + paths-ignore: + - '.vscode/**' + - 'assets/**' + - .gitignore + - 'README.md' + - LICENSE + push: + branches: + - 'issue/gh-**' + - 'feat/**' + - main + paths-ignore: + - '.github/**' + - '.vscode/**' + - 'assets/**' + - .gitignore + - 'README.md' + - LICENSE +jobs: + rust: + name: Rust CI on ${{matrix.runner == 'ubuntu-latest' && 'Linux (x86_64)' || matrix.runner == 'self-hosted' && 'Linux (arm64)' || matrix.runner == 'macos-latest' && 'macOS (x86_64)' || matrix.runner == 'windows-latest' && 'Windows' || 'Unknown'}} with toolchain ${{matrix.toolchain}} + runs-on: ${{matrix.runner}} + strategy: + fail-fast: true + matrix: + runner: [ubuntu-latest, self-hosted, windows-latest, macos-latest] + toolchain: [nightly, stable] + steps: + - name: Checkout source + uses: actions/checkout@v4 + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{matrix.toolchain}} + components: clippy, rustfmt + + - name: Setup Rust cache + uses: Swatinem/rust-cache@v2 + + - name: Pull dependencies + run: cargo build --all-features + + - name: Run tests + run: cargo test --all-features + clippy: + name: Clippy! + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + components: clippy + + - name: Setup Rust cache + uses: Swatinem/rust-cache@v2 + + - name: Clippy! + uses: auguwu/clippy-action@1.2.2 + with: + all-features: true + token: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/Release.yaml b/.github/workflows/Release.yaml index cec6755..3a2b850 100644 --- a/.github/workflows/Release.yaml +++ b/.github/workflows/Release.yaml @@ -13,343 +13,370 @@ # See the License for the specific language governing permissions and # limitations under the License. -name: Release a new version +name: Release Pipeline on: - release: - types: [released] + release: + types: [released] jobs: - build: - name: Build [${{matrix.dist.prettyName}}] - runs-on: ${{matrix.dist.runner}} - strategy: - matrix: - dist: - - { runner: ubuntu-latest, target: 'x86_64-unknown-linux-gnu', os: 'linux', arch: 'amd64', prettyName: 'Linux (x64)', ext: '' } - - { runner: self-hosted, target: 'aarch64-unknown-linux-gnu', os: 'linux', arch: 'arm64', prettyName: 'Linux (arm64)', ext: '' } - - { runner: macos-latest, target: 'x86_64-apple-darwin', os: 'darwin', arch: 'amd64', prettyName: 'macOS (x64)', ext: '' } - - { runner: macos-latest, target: 'aarch64-apple-darwin', os: 'darwin', arch: 'arm64', prettyName: 'macOS (arm64)', ext: '' } - - { runner: windows-latest, target: 'x86_64-pc-windows-msvc', os: 'windows', arch: 'amd64', prettyName: 'Windows (x64)', ext: '.exe' } - steps: - - name: Checkout repository - uses: actions/checkout@v3 - with: - submodules: recursive - - - name: Get release tag - id: tag - uses: auguwu/git-tag-action@master - - - name: Setup Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: nightly - override: true - target: ${{matrix.dist.target}} - - - name: Setup Rust cache - uses: Swatinem/rust-cache@v2 - - - name: Install Protoc - uses: Noelware/setup-protoc@1.0.2 - with: - repo-token: ${{secrets.GITHUB_TOKEN}} - - - name: Build release binary - run: cargo build --release --target=${{matrix.dist.target}} - - - name: Upload artifact to workflow - uses: actions/upload-artifact@v3 - with: - name: emails-${{matrix.dist.os}}-${{matrix.dist.arch}}${{matrix.dist.ext}} - path: ./target/${{matrix.dist.target}}/release/emails${{matrix.dist.ext}} - - - name: Create appropriate path name for release artifact - run: cp ./target/${{matrix.dist.target}}/release/emails${{matrix.dist.ext}} ./target/${{matrix.dist.target}}/release/emails-${{matrix.dist.os}}-${{matrix.dist.arch}}${{matrix.dist.ext}} - - - name: Upload artifact to release - uses: softprops/action-gh-release@v1 - with: - files: ./target/${{matrix.dist.target}}/release/emails-${{matrix.dist.os}}-${{matrix.dist.arch}}${{matrix.dist.ext}} - - - name: Upload artifacts to Noelware's Artifact Registry - uses: Noelware/s3-action@2.1.0 - with: - enforce-path-access-style: true - files: ./target/${{matrix.dist.target}}/release/emails${{matrix.dist.ext}} - path-format: $(prefix)/charted/emails/${{steps.tag.outputs.version}}/email-service-${{matrix.dist.os}}-${{matrix.dist.arch}}${{matrix.dist.ext}} - access-key-id: ${{secrets.S3_ACCESS_KEY}} - secret-key: ${{secrets.S3_SECRET_KEY}} - endpoint: ${{secrets.S3_ENDPOINT}} - prefix: /noelware/artifacts - bucket: august - - - name: Upload artifacts to Noelware's Artifact Registry [latest] - uses: Noelware/s3-action@2.1.0 - with: - enforce-path-access-style: true - files: ./target/${{matrix.dist.target}}/release/emails${{matrix.dist.ext}} - path-format: $(prefix)/charted/emails/latest/email-service-${{matrix.dist.os}}-${{matrix.dist.arch}}${{matrix.dist.ext}} - access-key-id: ${{secrets.S3_ACCESS_KEY}} - secret-key: ${{secrets.S3_SECRET_KEY}} - endpoint: ${{secrets.S3_ENDPOINT}} - prefix: /noelware/artifacts - bucket: august - docker-x64: - name: Build Docker image (x86_64) - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v3 - with: - submodules: recursive - - - name: Get current release tag - uses: auguwu/git-tag-action@master - id: tag - - - name: Get commit hash - id: commit-hash - run: echo "::set-output name=commit::$(git rev-parse --short=8 $GITHUB_SHA)" - - - name: Setup Docker buildx - uses: docker/setup-buildx-action@v2 - - - name: Setup QEMU - uses: docker/setup-qemu-action@v2 - - - name: Login into GitHub Container Registry - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: charted-dev - password: ${{secrets.GITHUB_TOKEN}} - - - name: Build and push [Debian, linux/amd64] - uses: docker/build-push-action@v4 - with: - platforms: linux/amd64 - context: . - file: ./distribution/docker/debian/Dockerfile - cache-from: type=gha,scope=debian-amd64 - cache-to: type=gha,scope=debian-amd64 - provenance: false - push: true - labels: | - org.opencontainers.image.title=charted-emails - org.opencontainers.image.description=🐻‍❄️💌 charted's email service built in Rust that can be connected via gRPC - org.opencontainers.image.version=${{steps.tag.outputs.version}} - org.opencontainers.image.revision=${{steps.commit-hash.outputs.commit}} - org.opencontainers.image.licenses=MIT - org.opencontainers.image.documentation=https://charts.noelware.org/docs/services/emails/${{steps.tag.outputs.version}} - org.opencontainers.image.source=https://github.com/charted-dev/email-service - tags: | - ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}.${{steps.tag.outputs.minor}}-amd64, - ghcr.io/charted-dev/email-service:${{steps.tag.outputs.version}}-amd64, - ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}-amd64, - ghcr.io/charted-dev/email-service:latest-amd64 - - - name: Build and push [Alpine, linux/amd64] - uses: docker/build-push-action@v4 - with: - platforms: linux/amd64 - context: . - file: ./distribution/docker/alpine/Dockerfile - push: true - cache-from: type=gha,scope=alpine-amd64 - cache-to: type=gha,scope=alpine-amd64 - provenance: false - labels: | - org.opencontainers.image.title=charted-emails - org.opencontainers.image.description=🐻‍❄️💌 charted's email service built in Rust that can be connected via gRPC - org.opencontainers.image.version=${{steps.tag.outputs.version}} - org.opencontainers.image.revision=${{steps.commit-hash.outputs.commit}} - org.opencontainers.image.licenses=MIT - org.opencontainers.image.documentation=https://charts.noelware.org/docs/services/emails/${{steps.tag.outputs.version}} - org.opencontainers.image.source=https://github.com/charted-dev/email-service - tags: | - ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}.${{steps.tag.outputs.minor}}-alpine-amd64, - ghcr.io/charted-dev/email-service:${{steps.tag.outputs.version}}-alpine-amd64, - ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}-alpine-amd64, - ghcr.io/charted-dev/email-service:latest-alpine-amd64 - docker-arm: - name: Build Docker image (ARM64) - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v3 - with: - submodules: recursive - - - name: Get current release tag - uses: auguwu/git-tag-action@master - id: tag - - - name: Get commit hash - id: commit-hash - run: echo "::set-output name=commit::$(git rev-parse --short=8 $GITHUB_SHA)" - - - name: Setup Docker buildx - uses: docker/setup-buildx-action@v2 - - - name: Setup QEMU - uses: docker/setup-qemu-action@v2 - - - name: Login into GitHub Container Registry - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: charted-dev - password: ${{secrets.GITHUB_TOKEN}} - - - name: Build and push [Debian, linux/arm64] - uses: docker/build-push-action@v4 - with: - platforms: linux/arm64 - context: . - file: ./distribution/docker/debian/Dockerfile - push: true - cache-from: type=gha,scope=debian-arm64 - cache-to: type=gha,scope=debian-arm64 - provenance: false - labels: | - org.opencontainers.image.title=charted-emails - org.opencontainers.image.description=🐻‍❄️💌 charted's email service built in Rust that can be connected via gRPC - org.opencontainers.image.version=${{steps.tag.outputs.version}} - org.opencontainers.image.revision=${{steps.commit-hash.outputs.commit}} - org.opencontainers.image.licenses=MIT - org.opencontainers.image.documentation=https://charts.noelware.org/docs/services/emails/${{steps.tag.outputs.version}} - org.opencontainers.image.source=https://github.com/charted-dev/email-service - tags: | - ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}.${{steps.tag.outputs.minor}}-arm64, - ghcr.io/charted-dev/email-service:${{steps.tag.outputs.version}}-arm64, - ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}-arm64, - ghcr.io/charted-dev/email-service:latest-arm64 - - - name: Build and push [Alpine, linux/arm64] - uses: docker/build-push-action@v4 - with: - platforms: linux/arm64 - context: . - file: ./distribution/docker/alpine/Dockerfile - push: true - cache-from: type=gha,scope=alpine-arm64 - cache-to: type=gha,scope=alpine-arm64 - provenance: false - labels: | - org.opencontainers.image.title=charted-emails - org.opencontainers.image.description=🐻‍❄️💌 charted's email service built in Rust that can be connected via gRPC - org.opencontainers.image.version=${{steps.tag.outputs.version}} - org.opencontainers.image.revision=${{steps.commit-hash.outputs.commit}} - org.opencontainers.image.licenses=MIT - org.opencontainers.image.documentation=https://charts.noelware.org/docs/services/emails/${{steps.tag.outputs.version}} - org.opencontainers.image.source=https://github.com/charted-dev/email-service - tags: | - ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}.${{steps.tag.outputs.minor}}-alpine-arm64, - ghcr.io/charted-dev/email-service:${{steps.tag.outputs.version}}-alpine-arm64, - ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}-alpine-arm64, - ghcr.io/charted-dev/email-service:latest-alpine-arm64 - manifests: - name: Merge manifests - runs-on: ubuntu-latest - needs: [docker-x64, docker-arm] - steps: - - name: Get current release tag - uses: auguwu/git-tag-action@master - id: tag - - - name: Login into GitHub Container Registry - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: charted-dev - password: ${{secrets.GITHUB_TOKEN}} - - - name: Merge Debian [latest] images - uses: Noelware/docker-manifest-action@0.3.1 - with: - push: true - inputs: ghcr.io/charted-dev/email-service:latest - images: | - ghcr.io/charted-dev/email-service:latest-amd64, - ghcr.io/charted-dev/email-service:latest-arm64 - - - name: Merge Debian [${{steps.tag.outputs.major}}] images - uses: Noelware/docker-manifest-action@0.3.1 - with: - push: true - inputs: ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}} - images: | - ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}-amd64, - ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}-arm64 - - - name: Merge Debian [${{steps.tag.outputs.major}}.${{steps.tag.outputs.minor}}] images - uses: Noelware/docker-manifest-action@0.3.1 - with: - push: true - inputs: ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}.${{steps.tag.outputs.minor}} - images: | - ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}.${{steps.tag.outputs.minor}}-amd64, - ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}.${{steps.tag.outputs.minor}}-arm64 - - - name: Merge Debian [{{steps.tag.outputs.version}}] images - uses: Noelware/docker-manifest-action@0.3.1 - with: - push: true - inputs: ghcr.io/charted-dev/email-service:${{steps.tag.outputs.version}} - images: | - ghcr.io/charted-dev/email-service:${{steps.tag.outputs.version}}-amd64, - ghcr.io/charted-dev/email-service:${{steps.tag.outputs.version}}-arm64 - - - name: Merge Alpine [latest] images - uses: Noelware/docker-manifest-action@0.3.1 - with: - push: true - inputs: ghcr.io/charted-dev/email-service:alpine - images: | - ghcr.io/charted-dev/email-service:alpine-amd64, - ghcr.io/charted-dev/email-service:alpine-arm64 - - - name: Merge Alpine [${{steps.tag.outputs.major}}] images - uses: Noelware/docker-manifest-action@0.3.1 - with: - push: true - inputs: ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}-alpine - images: | - ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}-alpine-amd64, - ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}-alpine-arm64 - - - name: Merge Debian [${{steps.tag.outputs.major}}.${{steps.tag.outputs.minor}}] images - uses: Noelware/docker-manifest-action@0.3.1 - with: - push: true - inputs: ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}.${{steps.tag.outputs.minor}}-alpine - images: | - ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}.${{steps.tag.outputs.minor}}-alpine-amd64, - ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}.${{steps.tag.outputs.minor}}-alpine-arm64 - - - name: Merge Debian [{{steps.tag.outputs.version}}] images - uses: Noelware/docker-manifest-action@0.3.1 - with: - push: true - inputs: ghcr.io/charted-dev/email-service:${{steps.tag.outputs.version}}-alpine - images: | - ghcr.io/charted-dev/email-service:${{steps.tag.outputs.version}}-alpine-amd64, - ghcr.io/charted-dev/email-service:${{steps.tag.outputs.version}}-alpine-arm64 - deploy: - name: Deploy to Noelware's infrastructure - runs-on: ubuntu-latest - needs: [manifests] - steps: - - name: Get current release tag - uses: auguwu/git-tag-action@master - id: tag - - - name: Create kubeconfig - run: mkdir ~/.kube && echo "${{secrets.KUBECONFIG}}" >> ~/.kube/config - - - name: Rollout image - run: kubectl set image deployment/charted-emails emails=ghcr.io/charted-dev/email-service:{{steps.tag.outputs.version}}-alpine --namespace noelware - - - name: Wait for completion - run: kubectl rollout status deployment/charted-emails + archives: + name: Archives [${{matrix.prettyName}}] + runs-on: ${{matrix.runner}} + strategy: + fail-fast: true + matrix: + include: + - runner: ubuntu-latest + prettyName: "Linux (x64)" + os: linux + arch: x86_64 + target: "x86_64-unknown-linux-gnu" + extension: "" + - runner: self-hosted + prettyName: "Linux (aarch64)" + os: linux + arch: aarch64 + target: "aarch64-unknown-linux-gnu" + extension: "" + - runner: macos-latest + prettyName: "macOS (x64)" + os: darwin + arch: x86_64 + target: "x86_64-apple-darwin" + extension: "" + - runner: macos-latest + prettyName: "macOS (aarch64)" + os: darwin + arch: aarch64 + target: "aarch64-apple-darwin" + extension: "" + - runner: windows-latest + prettyName: "Windows (x64)" + os: windows + arch: x86_64 + target: "x86_64-pc-windows-msvc" + extension: ".exe" + steps: + - name: Checkout source code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + target: ${{matrix.target}} + + - name: Setup Rust cache + uses: Swatinem/rust-cache@v2 + + - name: Setup protoc + uses: Noelware/setup-protoc@1.1.0 + + - name: Build release binary + run: cargo build --release --target=${{matrix.target}} + + - name: Upload artifact to workflow + uses: actions/upload-artifact@v3 + with: + path: ./target/${{matrix.target}}/release/emails${{matrix.extension}} + name: emails-${{matrix.os}}-${{matrix.arch}}${{matrix.extension}} + +# - name: Build release binary +# run: cargo build --release --target=${{matrix.dist.target}} + +# - name: Upload artifact to workflow +# uses: actions/upload-artifact@v3 +# with: +# name: emails-${{matrix.dist.os}}-${{matrix.dist.arch}}${{matrix.dist.ext}} +# path: ./target/${{matrix.dist.target}}/release/emails${{matrix.dist.ext}} + +# - name: Create appropriate path name for release artifact +# run: cp ./target/${{matrix.dist.target}}/release/emails${{matrix.dist.ext}} ./target/${{matrix.dist.target}}/release/emails-${{matrix.dist.os}}-${{matrix.dist.arch}}${{matrix.dist.ext}} + +# - name: Upload artifact to release +# uses: softprops/action-gh-release@v1 +# with: +# files: ./target/${{matrix.dist.target}}/release/emails-${{matrix.dist.os}}-${{matrix.dist.arch}}${{matrix.dist.ext}} + +# - name: Upload artifacts to Noelware's Artifact Registry +# uses: Noelware/s3-action@2.1.0 +# with: +# enforce-path-access-style: true +# files: ./target/${{matrix.dist.target}}/release/emails${{matrix.dist.ext}} +# path-format: $(prefix)/charted/emails/${{steps.tag.outputs.version}}/email-service-${{matrix.dist.os}}-${{matrix.dist.arch}}${{matrix.dist.ext}} +# access-key-id: ${{secrets.S3_ACCESS_KEY}} +# secret-key: ${{secrets.S3_SECRET_KEY}} +# endpoint: ${{secrets.S3_ENDPOINT}} +# prefix: /noelware/artifacts +# bucket: august + +# - name: Upload artifacts to Noelware's Artifact Registry [latest] +# uses: Noelware/s3-action@2.1.0 +# with: +# enforce-path-access-style: true +# files: ./target/${{matrix.dist.target}}/release/emails${{matrix.dist.ext}} +# path-format: $(prefix)/charted/emails/latest/email-service-${{matrix.dist.os}}-${{matrix.dist.arch}}${{matrix.dist.ext}} +# access-key-id: ${{secrets.S3_ACCESS_KEY}} +# secret-key: ${{secrets.S3_SECRET_KEY}} +# endpoint: ${{secrets.S3_ENDPOINT}} +# prefix: /noelware/artifacts +# bucket: august +# docker-x64: +# name: Build Docker image (x86_64) +# runs-on: ubuntu-latest +# steps: +# - name: Checkout repository +# uses: actions/checkout@v3 +# with: +# submodules: recursive + +# - name: Get current release tag +# uses: auguwu/git-tag-action@master +# id: tag + +# - name: Get commit hash +# id: commit-hash +# run: echo "::set-output name=commit::$(git rev-parse --short=8 $GITHUB_SHA)" + +# - name: Setup Docker buildx +# uses: docker/setup-buildx-action@v2 + +# - name: Setup QEMU +# uses: docker/setup-qemu-action@v2 + +# - name: Login into GitHub Container Registry +# uses: docker/login-action@v2 +# with: +# registry: ghcr.io +# username: charted-dev +# password: ${{secrets.GITHUB_TOKEN}} + +# - name: Build and push [Debian, linux/amd64] +# uses: docker/build-push-action@v4 +# with: +# platforms: linux/amd64 +# context: . +# file: ./distribution/docker/debian/Dockerfile +# cache-from: type=gha,scope=debian-amd64 +# cache-to: type=gha,scope=debian-amd64 +# provenance: false +# push: true +# labels: | +# org.opencontainers.image.title=charted-emails +# org.opencontainers.image.description=🐻‍❄️💌 charted's email service built in Rust that can be connected via gRPC +# org.opencontainers.image.version=${{steps.tag.outputs.version}} +# org.opencontainers.image.revision=${{steps.commit-hash.outputs.commit}} +# org.opencontainers.image.licenses=MIT +# org.opencontainers.image.documentation=https://charts.noelware.org/docs/services/emails/${{steps.tag.outputs.version}} +# org.opencontainers.image.source=https://github.com/charted-dev/email-service +# tags: | +# ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}.${{steps.tag.outputs.minor}}-amd64, +# ghcr.io/charted-dev/email-service:${{steps.tag.outputs.version}}-amd64, +# ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}-amd64, +# ghcr.io/charted-dev/email-service:latest-amd64 + +# - name: Build and push [Alpine, linux/amd64] +# uses: docker/build-push-action@v4 +# with: +# platforms: linux/amd64 +# context: . +# file: ./distribution/docker/alpine/Dockerfile +# push: true +# cache-from: type=gha,scope=alpine-amd64 +# cache-to: type=gha,scope=alpine-amd64 +# provenance: false +# labels: | +# org.opencontainers.image.title=charted-emails +# org.opencontainers.image.description=🐻‍❄️💌 charted's email service built in Rust that can be connected via gRPC +# org.opencontainers.image.version=${{steps.tag.outputs.version}} +# org.opencontainers.image.revision=${{steps.commit-hash.outputs.commit}} +# org.opencontainers.image.licenses=MIT +# org.opencontainers.image.documentation=https://charts.noelware.org/docs/services/emails/${{steps.tag.outputs.version}} +# org.opencontainers.image.source=https://github.com/charted-dev/email-service +# tags: | +# ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}.${{steps.tag.outputs.minor}}-alpine-amd64, +# ghcr.io/charted-dev/email-service:${{steps.tag.outputs.version}}-alpine-amd64, +# ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}-alpine-amd64, +# ghcr.io/charted-dev/email-service:latest-alpine-amd64 +# docker-arm: +# name: Build Docker image (ARM64) +# runs-on: ubuntu-latest +# steps: +# - name: Checkout repository +# uses: actions/checkout@v3 +# with: +# submodules: recursive + +# - name: Get current release tag +# uses: auguwu/git-tag-action@master +# id: tag + +# - name: Get commit hash +# id: commit-hash +# run: echo "::set-output name=commit::$(git rev-parse --short=8 $GITHUB_SHA)" + +# - name: Setup Docker buildx +# uses: docker/setup-buildx-action@v2 + +# - name: Setup QEMU +# uses: docker/setup-qemu-action@v2 + +# - name: Login into GitHub Container Registry +# uses: docker/login-action@v2 +# with: +# registry: ghcr.io +# username: charted-dev +# password: ${{secrets.GITHUB_TOKEN}} + +# - name: Build and push [Debian, linux/arm64] +# uses: docker/build-push-action@v4 +# with: +# platforms: linux/arm64 +# context: . +# file: ./distribution/docker/debian/Dockerfile +# push: true +# cache-from: type=gha,scope=debian-arm64 +# cache-to: type=gha,scope=debian-arm64 +# provenance: false +# labels: | +# org.opencontainers.image.title=charted-emails +# org.opencontainers.image.description=🐻‍❄️💌 charted's email service built in Rust that can be connected via gRPC +# org.opencontainers.image.version=${{steps.tag.outputs.version}} +# org.opencontainers.image.revision=${{steps.commit-hash.outputs.commit}} +# org.opencontainers.image.licenses=MIT +# org.opencontainers.image.documentation=https://charts.noelware.org/docs/services/emails/${{steps.tag.outputs.version}} +# org.opencontainers.image.source=https://github.com/charted-dev/email-service +# tags: | +# ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}.${{steps.tag.outputs.minor}}-arm64, +# ghcr.io/charted-dev/email-service:${{steps.tag.outputs.version}}-arm64, +# ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}-arm64, +# ghcr.io/charted-dev/email-service:latest-arm64 + +# - name: Build and push [Alpine, linux/arm64] +# uses: docker/build-push-action@v4 +# with: +# platforms: linux/arm64 +# context: . +# file: ./distribution/docker/alpine/Dockerfile +# push: true +# cache-from: type=gha,scope=alpine-arm64 +# cache-to: type=gha,scope=alpine-arm64 +# provenance: false +# labels: | +# org.opencontainers.image.title=charted-emails +# org.opencontainers.image.description=🐻‍❄️💌 charted's email service built in Rust that can be connected via gRPC +# org.opencontainers.image.version=${{steps.tag.outputs.version}} +# org.opencontainers.image.revision=${{steps.commit-hash.outputs.commit}} +# org.opencontainers.image.licenses=MIT +# org.opencontainers.image.documentation=https://charts.noelware.org/docs/services/emails/${{steps.tag.outputs.version}} +# org.opencontainers.image.source=https://github.com/charted-dev/email-service +# tags: | +# ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}.${{steps.tag.outputs.minor}}-alpine-arm64, +# ghcr.io/charted-dev/email-service:${{steps.tag.outputs.version}}-alpine-arm64, +# ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}-alpine-arm64, +# ghcr.io/charted-dev/email-service:latest-alpine-arm64 +# manifests: +# name: Merge manifests +# runs-on: ubuntu-latest +# needs: [docker-x64, docker-arm] +# steps: +# - name: Get current release tag +# uses: auguwu/git-tag-action@master +# id: tag + +# - name: Login into GitHub Container Registry +# uses: docker/login-action@v2 +# with: +# registry: ghcr.io +# username: charted-dev +# password: ${{secrets.GITHUB_TOKEN}} + +# - name: Merge Debian [latest] images +# uses: Noelware/docker-manifest-action@0.3.1 +# with: +# push: true +# inputs: ghcr.io/charted-dev/email-service:latest +# images: | +# ghcr.io/charted-dev/email-service:latest-amd64, +# ghcr.io/charted-dev/email-service:latest-arm64 + +# - name: Merge Debian [${{steps.tag.outputs.major}}] images +# uses: Noelware/docker-manifest-action@0.3.1 +# with: +# push: true +# inputs: ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}} +# images: | +# ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}-amd64, +# ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}-arm64 + +# - name: Merge Debian [${{steps.tag.outputs.major}}.${{steps.tag.outputs.minor}}] images +# uses: Noelware/docker-manifest-action@0.3.1 +# with: +# push: true +# inputs: ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}.${{steps.tag.outputs.minor}} +# images: | +# ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}.${{steps.tag.outputs.minor}}-amd64, +# ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}.${{steps.tag.outputs.minor}}-arm64 + +# - name: Merge Debian [{{steps.tag.outputs.version}}] images +# uses: Noelware/docker-manifest-action@0.3.1 +# with: +# push: true +# inputs: ghcr.io/charted-dev/email-service:${{steps.tag.outputs.version}} +# images: | +# ghcr.io/charted-dev/email-service:${{steps.tag.outputs.version}}-amd64, +# ghcr.io/charted-dev/email-service:${{steps.tag.outputs.version}}-arm64 + +# - name: Merge Alpine [latest] images +# uses: Noelware/docker-manifest-action@0.3.1 +# with: +# push: true +# inputs: ghcr.io/charted-dev/email-service:alpine +# images: | +# ghcr.io/charted-dev/email-service:alpine-amd64, +# ghcr.io/charted-dev/email-service:alpine-arm64 + +# - name: Merge Alpine [${{steps.tag.outputs.major}}] images +# uses: Noelware/docker-manifest-action@0.3.1 +# with: +# push: true +# inputs: ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}-alpine +# images: | +# ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}-alpine-amd64, +# ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}-alpine-arm64 + +# - name: Merge Debian [${{steps.tag.outputs.major}}.${{steps.tag.outputs.minor}}] images +# uses: Noelware/docker-manifest-action@0.3.1 +# with: +# push: true +# inputs: ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}.${{steps.tag.outputs.minor}}-alpine +# images: | +# ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}.${{steps.tag.outputs.minor}}-alpine-amd64, +# ghcr.io/charted-dev/email-service:${{steps.tag.outputs.major}}.${{steps.tag.outputs.minor}}-alpine-arm64 + +# - name: Merge Debian [{{steps.tag.outputs.version}}] images +# uses: Noelware/docker-manifest-action@0.3.1 +# with: +# push: true +# inputs: ghcr.io/charted-dev/email-service:${{steps.tag.outputs.version}}-alpine +# images: | +# ghcr.io/charted-dev/email-service:${{steps.tag.outputs.version}}-alpine-amd64, +# ghcr.io/charted-dev/email-service:${{steps.tag.outputs.version}}-alpine-arm64 +# deploy: +# name: Deploy to Noelware's infrastructure +# runs-on: ubuntu-latest +# needs: [manifests] +# steps: +# - name: Get current release tag +# uses: auguwu/git-tag-action@master +# id: tag + +# - name: Create kubeconfig +# run: mkdir ~/.kube && echo "${{secrets.KUBECONFIG}}" >> ~/.kube/config + +# - name: Rollout image +# run: kubectl set image deployment/charted-emails emails=ghcr.io/charted-dev/email-service:{{steps.tag.outputs.version}}-alpine --namespace noelware + +# - name: Wait for completion +# run: kubectl rollout status deployment/charted-emails diff --git a/.github/workflows/RustCI.yaml b/.github/workflows/RustCI.yaml deleted file mode 100644 index 4e0b56a..0000000 --- a/.github/workflows/RustCI.yaml +++ /dev/null @@ -1,85 +0,0 @@ -# 🐻‍❄️💌 email-service: charted's email service built in Rust that can be connected via gRPC -# Copyright 2023 Noelware, LLC. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: Rust CI -on: - workflow_dispatch: {} - pull_request: - types: [opened, synchronize] - branches: - - 'issue/gh-**' - - 'feat/**' - - main - paths-ignore: - - '.github/**' - - '.vscode/**' - - 'assets/**' - - .gitignore - - '**.md' - - LICENSE - - renovate.json - push: - branches: - - 'issue/gh-**' - - 'feat/**' - - main - paths-ignore: - - '.github/**' - - '.vscode/**' - - 'assets/**' - - .gitignore - - '**.md' - - LICENSE - - renovate.json -jobs: - ci: - name: Rust CI [${{matrix.toolchain}}, ${{matrix.runner}}] - runs-on: ${{matrix.runner}} - strategy: - matrix: - runner: [windows-latest, macos-latest, ubuntu-latest, self-hosted] - toolchain: [nightly, stable] - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Setup Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: ${{matrix.toolchain}} - override: true - components: rustfmt, clippy - - - name: Setup Rust cache - uses: Swatinem/rust-cache@v2 - - - name: Install Protoc - uses: Noelware/setup-protoc@1.0.2 - with: - repo-token: ${{secrets.GITHUB_TOKEN}} - - - name: Check for compile errors - run: cargo build - env: - CARGO_INCREMENTAL: 1 - - # Only run Clippy in the nightly toolchain and Ubuntu, im biased ok - - name: Clippy - if: ${{matrix.runner == 'ubuntu-latest' && matrix.toolchain == 'nightly'}} - uses: actions-rs/clippy-check@v1 - with: - token: ${{secrets.GITHUB_TOKEN}} - args: --all-features diff --git a/.gitignore b/.gitignore index 53349f8..932ac9f 100644 --- a/.gitignore +++ b/.gitignore @@ -512,3 +512,6 @@ FodyWeavers.xsd # Additional files built by Visual Studio config.yml + +# not finalized yet! +docs/ diff --git a/.vscode/settings.json b/.vscode/settings.json index 0d2a9be..34fdbfa 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "editor.tabSize": 4, "rust-analyzer.check.command": "clippy", "rust-analyzer.cargo.buildScripts.enable": true, + "rust-analyzer.showUnlinkedFileNotification": false, "[rust]": { "editor.defaultFormatter": "rust-lang.rust-analyzer" } diff --git a/Cargo.toml b/Cargo.toml index 2df46a8..f42f2e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,49 +14,45 @@ # limitations under the License. [package] -name = "emails" +name = "charted-emails" description = "🐻‍❄️💌 charted's email service built in Rust that can be connected via gRPC" homepage = "https://charts.noelware.org/docs/services/emails/latest" -version = "0.1.1" +version = "0.0.0-devel.0" edition = "2021" -authors = ["Noel Miko ", "Noelware Team "] +authors = ["Noel Towa ", "Noelware Team "] license = "Apache-2.0" [dependencies] -ansi_term = "0.12.1" async-trait = "0.1.73" -chrono = { version = "0.4.28", features = ["serde"] } +chrono = "0.4.28" +color-eyre = "0.6.2" dotenv = "0.15.0" -fern = "0.6.2" -futures = { version = "0.3.28", default-features = false, features = ["std"] } -futures-util = "0.3.28" -lazy_static = "1.4.0" -lettre = { version = "0.10.4", features = ["tracing", "hostname", "tokio1-native-tls", "tokio1"] } -log = "0.4.20" +eyre = "0.6.8" +git2 = "0.18.0" mustache = "0.9.0" once_cell = "1.18.0" -prost = "0.11.9" -prost-types = "0.11.9" -regex = "1.9.4" -sentry = { version = "0.31.6", features = ["backtrace", "log", "panic", "tracing"] } -sentry-log = "0.31.6" +owo-colors = { version = "3.5.0", features = ["supports-colors"] } +prost = "0.12.0" +prost-types = "0.12.0" +regex = "1.9.5" +sentry = "0.31.6" sentry-tower = "0.31.6" sentry-tracing = "0.31.6" serde = { version = "1.0.188", features = ["derive"] } -serde_json = "1.0.105" +serde_json = "1.0.106" serde_yaml = "0.9.25" -thiserror = "1.0.47" -tokio = { version = "1.32.0", features = ["full"] } -tonic = "0.8.3" -tonic-health = "0.8.0" +tokio = "1.32.0" +tonic = "0.10.0" +tonic-health = "0.10.0" +tracing = "0.1.37" +tracing-log = "0.1.3" +tracing-subscriber = "0.3.17" +url = "2.4.1" [build-dependencies] chrono = "0.4.28" -tonic-build = "0.8.4" - -[[bin]] -path = "src/main.rs" -name = "emails" +rustc_version = "0.4.0" +tonic-build = "0.10.0" [profile.release] strip = true diff --git a/README.md b/README.md index 8013f3b..5fdf2bf 100644 --- a/README.md +++ b/README.md @@ -3,42 +3,60 @@ **email-service** is a small microservice to help transfer emails towards other people without trying to implement it in different languages. This is used in [charted-server](https://github.com/charted-dev/charted) for member invitations, passwordless authentication, and more. -The service also comes with pre-made templates that you can override easily from the `./templates` directory to suite your needs. Since this is a microservice that anyone can use, the templates can be customized to your liking. +## Templates +Starting in v0.2.0, you can now use Git to host your templates and the service will pull them into the filesystem and keep track of them once you start sending users emails! -This repository also comes with the templates what we built for charted via the [react-email](https://www.npmjs.com/package/react-email) NPM library, which is available in [./template-builder](./template-builder). +> Note +> You can still host your templates on the filesystem, just use the `templates.fs` object instead. + +To use Git, you must need to have it installed on your system (as the service will require [`libgit2`](https://libgit2.github.com) to pull them), and you can set the `templates.git.repository` to `git://[server]/[owner]/[repo]`: + +```yaml +templates: + git: + repository: git://github.com/charted-dev/email-templates + directory: ./dist + branch: main +``` + +The server will pull the repository in `/var/lib/noelware/charted/emails/templates` (if on Docker if `templates.directory` is not on the disk), or in the `templates.directory` directory. + +### SSH +To use the SSH protocol for Git, you will need to have the keys available on the filesystem. You can use the `templates.git.ssh` object to do so: + +```yaml +templates: + git: + repository: git://github.com/charted-dev/email-templates + directory: ./dist + branch: main + ssh: + username: noel # some other username... + keys: + - ~/.ssh/id_rsa +``` ## Installation ### Docker -To use the microservice with Docker, you will need to have the [Docker Engine](https://docker.com) or [Docker Desktop](https://docker.com/products/docker-desktop) installed on your machine. Once you have Docker installed, you can pull the Docker image from Noelware or GitHub's container registry, depends what you want to run: - -- If you wish to run ***only stable builds***, you can use [Noelware's Container Registry](https://cr.noelware.cloud). -- If you really want to run the most cutting edge version of this service, you can do so with the Nightly channel. All nightly builds are only available on [GitHub's container registry](https://github.com/orgs/charted-dev/packages) to not clutter Noelware's registry. +To use the microservice with Docker, you will need to have the [Docker Engine](https://docker.com) or [Docker Desktop](https://docker.com/products/docker-desktop) installed on your machine. Once you have Docker installed, you can pull the Docker image from Noelware's container registry. The image consists around multiple tags that are suited for your environment. We build the images with the `linux/amd64` and `linux/arm64` architectures. - `latest`, `nightly` - The latest versions for each channel (`latest` for the **stable** channel, `nightly` for the **nightly** channel) -- `alpine` - This tag runs this service with the [Alpine](https://hub.docker.com/_/alpine) image instead of [Ubuntu](https://hub.docker.com/_/ubuntu), which is recommended for production environments since it's more compat and smaller. +- `alpine` - This tag runs this service with the [Alpine](https://hub.docker.com/_/alpine) image instead of [Debian](https://hub.docker.com/_/debian), which is recommended for production environments since it's more compat and smaller. - `{version}`, `{version}-nightly` - The **{version}** placeholder is for any specific version of this service to run. - `{version}-alpine` - Similarly to the stock `alpine` image tag, but uses a specific version of this microservice to run. As this service doesn't hold any persistence, we will not be requiring it and we do not need any external databases or any other service. Now, we can begin pulling the image from the respected registry: ```shell -# Noelware's Container Registry $ docker pull cr.noelware.cloud/charted/emails - -# GitHub's Container Registry -$ docker pull ghcr.io/charted-dev/email-service ``` Now, we can run the container: ```shell -# Noelware's Container Registry $ docker run -d -p 32121:32121 --name emails cr.noelware.cloud/charted/emails - -# GitHub's Container Registry -$ docker run -d -p 32121:32121 --name emails ghcr.io/charted-dev/email-service ``` ### Docker Compose diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..0506b8e --- /dev/null +++ b/clippy.toml @@ -0,0 +1,16 @@ +# 🐻‍❄️💌 email-service: charted's email service built in Rust that can be connected via gRPC +# Copyright 2023 Noelware, LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +msrv = "1.64.0" diff --git a/distribution/config/emails.yaml b/distribution/config/emails.yaml new file mode 100644 index 0000000..191e2d6 --- /dev/null +++ b/distribution/config/emails.yaml @@ -0,0 +1,17 @@ +# 🐻‍❄️💌 email-service: charted's email service built in Rust that can be connected via gRPC +# Copyright 2023 Noelware, LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This is the default configuration file. You can edit this to your heart's desires!~ +# Read up more on the configuration: https://charts.noelware.org/docs/emails/latest/self-hosting/configuration diff --git a/distribution/docker/alpine.Dockerfile b/distribution/docker/alpine.Dockerfile new file mode 100644 index 0000000..619d1da --- /dev/null +++ b/distribution/docker/alpine.Dockerfile @@ -0,0 +1,63 @@ +# 🐻‍❄️💌 email-service: charted's email service built in Rust that can be connected via gRPC +# Copyright 2023 Noelware, LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM rust:1.72-alpine3.18 AS build + +RUN apk update && apk add --no-cache git ca-certificates curl musl-dev libc6-compat gcompat pkgconfig openssl-dev libgit2 + +ENV CARGO_INCREMENTAL=1 +WORKDIR /build + +COPY . . +RUN cargo build --release + +FROM alpine:3.18 + +RUN apk update && apk add --no-cache bash tini curl libgit2 +WORKDIR /app/noelware/charted/emails + +COPY --from=build /build/target/release/emails /app/noelware/charted/emails/bin/emails +COPY distribution/docker/scripts /app/noelware/charted/emails/scripts +COPY distribution/config /app/noelware/charted/emails/config + +# # renovate: datasource=github-tags name=grpc-ecosystem/grpc-health-probe +ENV GRPC_HEALTH_PROBE_VERSION="v0.4.15" +RUN set -eux; \ + arch="$(uname -m)"; \ + case "${arch}" in \ + aarch64|arm64) \ + HEALTHPROBE_DOWNLOAD_URL="https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/${GRPC_HEALTH_PROBE_VERSION}/grpc_health_probe-linux-arm64"; \ + ;; \ + amd64|x86_64) \ + HEALTHPROBE_DOWNLOAD_URL="https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/${GRPC_HEALTH_PROBE_VERSION}/grpc_health_probe-linux-amd64"; \ + ;; \ + esac; \ + curl -fsSL -o /usr/local/bin/grpc-healthprobe ${HEALTHPROBE_DOWNLOAD_URL}; \ + chmod +x /usr/local/bin/grpc-healthprobe; + +RUN mkdir -p /var/lib/noelware/charted/emails +RUN addgroup -g 1001 noelware && \ + adduser -DSH -u 1001 -G noelware noelware && \ + chown 1001:1001 /app/noelware/charted/emails && \ + chown 1001:1001 /var/lib/noelware/charted/emails && \ + chmod +x /app/noelware/charted/emails/scripts/docker-entrypoint.sh + +ENV EMAILS_DISTRIBUTION_KIND=docker +EXPOSE 32121 +VOLUME /var/lib/noelware/charted/emails + +USER noelware +ENTRYPOINT ["/app/noelware/charted/emails/scripts/docker-entrypoint.sh"] +CMD ["/app/noelware/charted/emails/bin/emails"] diff --git a/distribution/docker/alpine/Dockerfile b/distribution/docker/alpine/Dockerfile deleted file mode 100644 index 22fbb30..0000000 --- a/distribution/docker/alpine/Dockerfile +++ /dev/null @@ -1,67 +0,0 @@ -# 🐻‍❄️💌 email-service: charted's email service built in Rust that can be connected via gRPC -# Copyright 2023 Noelware, LLC. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -FROM rust:1.72-alpine AS builder - -RUN apk update && apk add --no-cache build-base openssl-dev gcompat libc6-compat && apk add --no-cache protobuf-dev --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main - -ENV CARGO_INCREMENTAL=1 -WORKDIR /build - -COPY Cargo.toml . -RUN echo "fn main() {}" >> dummy.rs -RUN sed -i 's#src/main.rs#dummy.rs#' Cargo.toml -ENV RUSTFLAGS=-Ctarget-feature=-crt-static -RUN cargo build --release -RUN rm dummy.rs && sed -i 's#dummy.rs#src/main.rs#' Cargo.toml - -COPY . . -RUN cargo build --release - -FROM alpine:3.18 - -RUN apk update && apk add --no-cache bash tini openssl-dev gcompat libc6-compat libgcc openssl1.1-compat curl -WORKDIR /app/noelware/charted/emails - -COPY --from=builder /build/target/release/emails /app/noelware/charted/emails/bin/emails -COPY distribution/docker/scripts /app/noelware/charted/emails/scripts - -EXPOSE 32121 -RUN addgroup -g 1001 noelware && \ - adduser -DSH -u 1001 -G noelware noelware && \ - chown 1001:1001 /app/noelware/charted/emails && \ - chmod +x /app/noelware/charted/emails/bin/emails /app/noelware/charted/emails/scripts/docker-entrypoint.sh - -# Install gRPC Health Probe so you can health-check this application -ENV GRPC_HEALTH_PROBE_VERSION="0.4.15" -RUN set -eux; \ - arch="$(uname -m)"; \ - case "${arch}" in \ - aarch64|arm64) \ - HEALTHPROBE_DOWNLOAD_URL=" https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/v${GRPC_HEALTH_PROBE_VERSION}/grpc_health_probe-linux-arm64"; \ - ;; \ - amd64|x86_64) \ - HEALTHPROBE_DOWNLOAD_URL=" https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/v${GRPC_HEALTH_PROBE_VERSION}/grpc_health_probe-linux-amd64"; \ - ;; \ - esac; \ - mkdir -p /opt/healthprobe; \ - curl -fsSL -o /opt/healthprobe/healthprobe ${HEALTHPROBE_DOWNLOAD_URL}; \ - chmod +x /opt/healthprobe/healthprobe; - -RUN ln -s /opt/healthprobe/healthprobe /usr/bin/grpc-healthprobe - -USER noelware -ENTRYPOINT ["/app/noelware/charted/emails/scripts/docker-entrypoint.sh"] -CMD ["/app/noelware/charted/emails/bin/emails"] diff --git a/distribution/docker/config/emails.yaml b/distribution/docker/config/emails.yaml new file mode 100644 index 0000000..9dff87f --- /dev/null +++ b/distribution/docker/config/emails.yaml @@ -0,0 +1,22 @@ +# 🐻‍❄️💌 email-service: charted's email service built in Rust that can be connected via gRPC +# Copyright 2023 Noelware, LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This is the default configuration file. You can edit this to your heart's desires!~ +# Read up more on the configuration: https://charts.noelware.org/docs/emails/latest/self-hosting/configuration +# +# This file just sets the template directory to /var/lib/noelware/charted/emails/templates + +templates: + directory: /var/lib/noelware/charted/emails/templates diff --git a/distribution/docker/debian.Dockerfile b/distribution/docker/debian.Dockerfile new file mode 100644 index 0000000..10ee118 --- /dev/null +++ b/distribution/docker/debian.Dockerfile @@ -0,0 +1,64 @@ +# 🐻‍❄️💌 email-service: charted's email service built in Rust that can be connected via gRPC +# Copyright 2023 Noelware, LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM rust:1.72-slim-bullseye AS build + +ENV DEBIAN_FRONTEND=noninteractive +RUN apt update && apt upgrade -y && apt install -y curl libssl-dev libarchive-tools pkg-config protobuf-compiler libgit2 + +ENV CARGO_INCREMENTAL=1 +WORKDIR /build + +COPY . . +RUN cargo build --release + +FROM debian:bullseye-slim + +RUN DEBIAN_FRONTEND=noninteractive apt update && DEBIAN_FRONTEND=noninteractive apt install -y bash tini curl libssl-dev libgit2 +WORKDIR /app/noelware/charted/emails + +COPY --from=build /build/target/release/emails /app/noelware/charted/emails/bin/emails +COPY distribution/docker/scripts /app/noelware/charted/emails/scripts +COPY distribution/config /app/noelware/charted/emails/config + +# renovate: datasource=github-tags name=grpc-ecosystem/grpc-health-probe +ENV GRPC_HEALTH_PROBE_VERSION="v0.4.15" +RUN set -eux; \ + arch="$(uname -m)"; \ + case "${arch}" in \ + aarch64|arm64) \ + HEALTHPROBE_DOWNLOAD_URL="https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/${GRPC_HEALTH_PROBE_VERSION}/grpc_health_probe-linux-arm64"; \ + ;; \ + amd64|x86_64) \ + HEALTHPROBE_DOWNLOAD_URL="https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/${GRPC_HEALTH_PROBE_VERSION}/grpc_health_probe-linux-amd64"; \ + ;; \ + esac; \ + curl -fsSL -o /usr/local/bin/grpc-healthprobe ${HEALTHPROBE_DOWNLOAD_URL}; \ + chmod +x /usr/local/bin/grpc-healthprobe; + +RUN mkdir -p /var/lib/noelware/charted/emails +RUN groupadd -g 1001 noelware && \ + useradd -rm -s /bin/bash -g noelware -u 1001 noelware && \ + chown 1001:1001 /app/noelware/charted/emails && \ + chown 1001:1001 /var/lib/noelware/charted/emails && \ + chmod +x /app/noelware/charted/emails/scripts/docker-entrypoint.sh + +ENV EMAILS_DISTRIBUTION_KIND=docker +EXPOSE 32121 +VOLUME /var/lib/noelware/charted/emails + +USER noelware +ENTRYPOINT ["/app/noelware/charted/emails/scripts/docker-entrypoint.sh"] +CMD ["/app/noelware/charted/emails/bin/emails"] diff --git a/distribution/docker/debian/Dockerfile b/distribution/docker/debian/Dockerfile deleted file mode 100644 index 6cac5a9..0000000 --- a/distribution/docker/debian/Dockerfile +++ /dev/null @@ -1,87 +0,0 @@ -# 🐻‍❄️💌 email-service: charted's email service built in Rust that can be connected via gRPC -# Copyright 2023 Noelware, LLC. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -FROM rustlang/rust:nightly-slim AS builder - -# We need to install some packages first -ENV DEBIAN_FRONTEND=noninteractive -RUN apt update && apt upgrade -y && apt install -y curl libssl-dev libarchive-tools pkg-config - -# First, we need to install protoc -ENV PROTOC_VERSION="22.0" -RUN set -eux; \ - arch="$(dpkg --print-architecture)"; \ - case "${arch}" in \ - aarch64|arm64) \ - PROTOC_DOWNLOAD_URL="https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-aarch_64.zip"; \ - ;; \ - amd64|x86_64) \ - PROTOC_DOWNLOAD_URL="https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-x86_64.zip"; \ - ;; \ - esac; \ - mkdir -p /opt/protoc; \ - curl -L ${PROTOC_DOWNLOAD_URL} | bsdtar xfz - --strip-components=1 -C /opt/protoc; \ - chmod +x /opt/protoc/protoc; - -ENV PROTOC=/opt/protoc/protoc -ENV CARGO_INCREMENTAL=1 -WORKDIR /build - -COPY Cargo.toml . -RUN echo "fn main() {}" >> dummy.rs -RUN sed -i 's#src/main.rs#dummy.rs#' Cargo.toml -ENV RUSTFLAGS=-Ctarget-feature=-crt-static -RUN cargo build --release -RUN rm dummy.rs && sed -i 's#dummy.rs#src/main.rs#' Cargo.toml - -COPY . . -RUN cargo build --release - -FROM debian:11-slim - -ENV DEBIAN_FRONTEND=noninteractive -RUN apt update && apt upgrade -y && apt install -y bash tini curl -WORKDIR /app/noelware/charted/emails - -COPY --from=builder /build/target/release/emails /app/noelware/charted/emails/bin/emails -COPY distribution/docker/scripts /app/noelware/charted/emails/scripts - -EXPOSE 32121 -RUN groupadd -g 1001 noelware && \ - useradd -rm -s /bin/bash -g noelware -u 1001 noelware && \ - chown 1001:1001 /app/noelware/charted/emails && \ - chmod +x /app/noelware/charted/emails/bin/emails /app/noelware/charted/emails/scripts/docker-entrypoint.sh - -# Install gRPC Health Probe so you can health-check this application -ENV GRPC_HEALTH_PROBE_VERSION="0.4.15" -RUN set -eux; \ - arch="$(uname -m)"; \ - case "${arch}" in \ - aarch64|arm64) \ - HEALTHPROBE_DOWNLOAD_URL=" https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/v${GRPC_HEALTH_PROBE_VERSION}/grpc_health_probe-linux-arm64"; \ - ;; \ - amd64|x86_64) \ - HEALTHPROBE_DOWNLOAD_URL=" https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/v${GRPC_HEALTH_PROBE_VERSION}/grpc_health_probe-linux-amd64"; \ - ;; \ - esac; \ - mkdir -p /opt/healthprobe; \ - curl -fsSL -o /opt/healthprobe/healthprobe ${HEALTHPROBE_DOWNLOAD_URL}; \ - chmod +x /opt/healthprobe/healthprobe; - -RUN ln -s /opt/healthprobe/healthprobe /usr/bin/grpc-healthprobe - -USER noelware -ENTRYPOINT ["/app/noelware/charted/emails/scripts/docker-entrypoint.sh"] -CMD ["/app/noelware/charted/emails/bin/emails"] diff --git a/distribution/helm/.charted.yaml b/distribution/helm/.charted.yaml new file mode 100644 index 0000000..b22c02d --- /dev/null +++ b/distribution/helm/.charted.yaml @@ -0,0 +1,18 @@ +# 🐻‍❄️💌 email-service: charted's email service built in Rust that can be connected via gRPC +# Copyright 2023 Noelware, LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +repository: charted/emails +registry: https://charts.noelware.org/api +readme: ./README.md diff --git a/distribution/helm/Chart.yaml b/distribution/helm/Chart.yaml new file mode 100644 index 0000000..6afc314 --- /dev/null +++ b/distribution/helm/Chart.yaml @@ -0,0 +1,40 @@ +# 🐻‍❄️💌 email-service: charted's email service built in Rust that can be connected via gRPC +# Copyright 2023 Noelware, LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: v2 +appVersion: 0.2.0 +kubeVersion: ">=1.23" +version: 0.2.0 +name: emails +home: https://charts.noelware.org +icon: https://cdn.noelware.cloud/branding/charted.png +sources: + - https://github.com/charted-dev/emails/tree/main/distribution/helm + - https://github.com/charted-dev/emails + - https://charts.noelware.org +maintainers: + - name: Noel Towa + email: cutie@floofy.dev + url: https://floofy.dev + - name: Noelware, LLC. + email: team@noelware.org + url: https://noelware.org +annotations: + charts.noelware.org/maintainers: user/noel,org/noelware + charts.noelware.org/type: application +dependencies: + - name: common + version: ~2.9.2 + repository: https://charts.bitnami.com/bitnami diff --git a/distribution/helm/README.md b/distribution/helm/README.md new file mode 100644 index 0000000..38ed271 --- /dev/null +++ b/distribution/helm/README.md @@ -0,0 +1,13 @@ +# Helm Chart for [charted-emails](https://github.com/charted-dev/emails) +This is the canonical source for the Helm chart distribution for [charted-emails](https://github.com/charted-dev/emails), by [Noelware, LLC.](https://noelware.org). + +## Installation +```sh +$ helm repo add charted https://charts.noelware.org/~/charted +$ helm install charted-emails charted/emails --set global.smtp.host=localhost --set global.smtp.port=25 +``` + +## Parameters + + + diff --git a/distribution/helm/templates/NOTES.txt b/distribution/helm/templates/NOTES.txt new file mode 100644 index 0000000..e69de29 diff --git a/distribution/helm/templates/_helpers.tpl b/distribution/helm/templates/_helpers.tpl new file mode 100644 index 0000000..d7afcfd --- /dev/null +++ b/distribution/helm/templates/_helpers.tpl @@ -0,0 +1,16 @@ +{{/* +~ 🐻‍❄️💌 email-service: charted's email service built in Rust that can be connected via gRPC +~ Copyright 2023 Noelware, LLC. +~ +~ Licensed under the Apache License, Version 2.0 (the "License"); +~ you may not use this file except in compliance with the License. +~ You may obtain a copy of the License at +~ +~ http://www.apache.org/licenses/LICENSE-2.0 +~ +~ Unless required by applicable law or agreed to in writing, software +~ distributed under the License is distributed on an "AS IS" BASIS, +~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +~ See the License for the specific language governing permissions and +~ limitations under the License. +*/}} diff --git a/distribution/helm/templates/configmap.yaml b/distribution/helm/templates/configmap.yaml new file mode 100644 index 0000000..6bde41e --- /dev/null +++ b/distribution/helm/templates/configmap.yaml @@ -0,0 +1,14 @@ +# 🐻‍❄️💌 email-service: charted's email service built in Rust that can be connected via gRPC +# Copyright 2023 Noelware, LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/distribution/helm/templates/deployment.yaml b/distribution/helm/templates/deployment.yaml new file mode 100644 index 0000000..6bde41e --- /dev/null +++ b/distribution/helm/templates/deployment.yaml @@ -0,0 +1,14 @@ +# 🐻‍❄️💌 email-service: charted's email service built in Rust that can be connected via gRPC +# Copyright 2023 Noelware, LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/distribution/helm/templates/service.yaml b/distribution/helm/templates/service.yaml new file mode 100644 index 0000000..6bde41e --- /dev/null +++ b/distribution/helm/templates/service.yaml @@ -0,0 +1,14 @@ +# 🐻‍❄️💌 email-service: charted's email service built in Rust that can be connected via gRPC +# Copyright 2023 Noelware, LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/distribution/helm/templates/serviceAccount.yaml b/distribution/helm/templates/serviceAccount.yaml new file mode 100644 index 0000000..6bde41e --- /dev/null +++ b/distribution/helm/templates/serviceAccount.yaml @@ -0,0 +1,14 @@ +# 🐻‍❄️💌 email-service: charted's email service built in Rust that can be connected via gRPC +# Copyright 2023 Noelware, LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/distribution/helm/values.yaml b/distribution/helm/values.yaml new file mode 100644 index 0000000..6bde41e --- /dev/null +++ b/distribution/helm/values.yaml @@ -0,0 +1,14 @@ +# 🐻‍❄️💌 email-service: charted's email service built in Rust that can be connected via gRPC +# Copyright 2023 Noelware, LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/protos/emails.proto b/protos/emails.proto index 2158708..846cd91 100644 --- a/protos/emails.proto +++ b/protos/emails.proto @@ -60,9 +60,18 @@ message SendEmailResponse { // will be available, and to see if you can retry with the `should_retry` property. bool success = 1; - // If the email should be retried due to an error. - bool should_retry = 2; + // Any errors that might've occured. + repeated Error errors = 2; +} + +message Error { + // A machine-readable error code that you can look up for more information + // A list of codes can be found in the [documentation](https://charts.noelware.org/docs/services/emails/latest/api#error-codes). + string code = 1; + + // Human-readable message to indicate on why it failed. + string message = 2; - // Optional error message of the reason why it was not successful. - optional string error_message = 3; + // Any extra details that might help on why it failed. + optional google.protobuf.Struct details = 3; } diff --git a/renovate.json b/renovate.json index 403825d..1d91004 100644 --- a/renovate.json +++ b/renovate.json @@ -1,16 +1,17 @@ { + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:base", "docker:disableMajor"], "automerge": true, - "extends": ["config:base", "default:timezone(America/Phoenix)", "docker:disableMajor"], + "automergeSchedule": ["at any time"], + "timezone": "America/Los_Angeles", "vulnerabilityAlerts": { "labels": ["security"] }, - "java": { - "packageRules": [ - { - "matchPackagePatterns": ["^org\\.noelware[.:]"], - "matchManagers": ["gradle"], - "registryUrls": ["https://maven.noelware.org", "https://maven.noelware.org/snapshots"] - } - ] - } + "regexManagers": [ + { + "description": "Allow updating software versions in Dockerfiles", + "fileMatch": ["distribution/docker/alpine.Dockerfile", "distribution/docker/debian.Dockerfile"], + "matchStrings": ["datasource=(?.*?) name=(?.*?)\\sENV \\w+_VERSION=\"(?.*)\""] + } + ] } diff --git a/rustfmt.toml b/rustfmt.toml index d6639c4..724e130 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -13,5 +13,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -max_width = 90 +use_field_init_shorthand = true +match_arm_leading_pipes = "Preserve" +use_try_shorthand = true +newline_style = "Unix" # keep it consistent +max_width = 120 edition = "2021" diff --git a/src/config/logging.rs b/src/config/logging.rs index 76bed10..3520bfa 100644 --- a/src/config/logging.rs +++ b/src/config/logging.rs @@ -12,90 +12,3 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - -use crate::{error::Error, generate_config_struct}; -use log::LevelFilter; -use serde::{Deserialize, Serialize}; - -use std::str::FromStr; - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum LogLevel { - Off, - #[default] - Info, - Warn, - Error, - Debug, - Trace, -} - -impl LogLevel { - pub fn to_level_filter(&self) -> LevelFilter { - match self { - LogLevel::Trace => LevelFilter::Trace, - LogLevel::Debug => LevelFilter::Debug, - LogLevel::Error => LevelFilter::Error, - LogLevel::Warn => LevelFilter::Warn, - LogLevel::Info => LevelFilter::Info, - LogLevel::Off => LevelFilter::Off, - } - } -} - -impl FromStr for LogLevel { - type Err = Error; - - fn from_str(s: &str) -> std::result::Result { - match s { - "off" => Ok(LogLevel::Off), - "info" => Ok(LogLevel::Info), - "warn" => Ok(LogLevel::Warn), - "error" => Ok(LogLevel::Error), - "debug" => Ok(LogLevel::Debug), - "trace" => Ok(LogLevel::Trace), - _ => Err(Error::UnknownLogLevel { - level: s.to_owned(), - }), - } - } -} - -generate_config_struct!(LogConfig { - /// - /// URL to connect to Logstash via TCP that outputs all logs into Logstash. - #[serde(skip_serializing_if = "Option::is_none")] - logstash_uri: Option => { - on_env -> ::std::env::var("EMAILS_LOGSTASH_URI").ok(); - on_default -> None; - }, - - /// - /// The level to use when configuring the logger. By default, all information logging - /// will be outputted. - #[serde(default)] - pub level: LogLevel => { - on_env -> ::std::env::var("EMAILS_LOG_LEVEL").map(|p| p.parse::().expect("unable to parse into log level")).unwrap_or_default(); - on_default -> LogLevel::default(); - }, - - /// - /// If the server should print out JSON logging instead of the default, prettier - /// logging. - #[serde(default = "default_json")] - pub json: bool => { - on_env -> ::std::env::var("EMAILS_LOG_IN_JSON").map(|f| f.parse::().expect("Unable to parse into boolean")).unwrap_or(default_json()); - on_default -> default_json(); - } -}); - -fn default_json() -> bool { - false -} - -impl LogConfig { - pub fn logstash_uri(&self) -> Option { - self.logstash_uri.clone() - } -} diff --git a/src/config/mod.rs b/src/config/mod.rs index 11689a5..3520bfa 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -12,172 +12,3 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - -mod logging; -mod server; -mod smtp; - -use crate::{error::*, to_dyn_error}; -use std::{ - fs::File, - net::SocketAddr, - path::{Path, PathBuf}, -}; - -pub use logging::*; -use once_cell::sync::OnceCell; -pub use server::*; -pub use smtp::*; - -static CONFIG: OnceCell = OnceCell::new(); - -pub trait FromEnv { - fn from_env() -> T; -} - -#[macro_export] -macro_rules! generate_config_struct { - ($name:ident { - $( - #[doc = $help:expr] - $(#[$meta:meta])* - $vis:vis $key:ident: $ty:ty => { - on_env -> $env_block:expr; - on_default -> $default_block:expr; - }$(,)? - ),+ $(,)? - }) => { - #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] - pub struct $name { - $( - $(#[$meta])* - $vis $key: $ty, - )* - } - - impl $crate::FromEnv<$name> for $name { - fn from_env() -> $name { - $name { - $( - $key: $env_block, - )* - } - } - } - - impl Default for $name { - fn default() -> $name { - $name { - $( - $key: $default_block, - )* - } - } - } - }; -} - -generate_config_struct!(Config { - /// - /// The DSN for using Sentry for tracing and tracking down errors when it happens when you use this service - #[serde(skip_serializing_if = "Option::is_none")] - pub sentry_dsn: Option => { - on_env -> ::std::env::var("EMAILS_SENTRY_DSN").ok(); - on_default -> None; - }, - - /// - /// The directory to load up templates from - #[serde(default = "default_templates_dir")] - pub templates: PathBuf => { - on_env -> ::std::env::var("EMAILS_TEMPLATE_DIR").map(|p| p.parse().expect("unable to parse into PathBuf")).unwrap_or(default_templates_dir()); - on_default -> default_templates_dir(); - }, - - /// - /// Logging configuration - #[serde(default)] - pub logging: LogConfig => { - on_env -> LogConfig::from_env(); - on_default -> LogConfig::default(); - }, - - /// - /// Server configuration - #[serde(default)] - pub server: ServerConfig => { - on_env -> ServerConfig::from_env(); - on_default -> ServerConfig::default(); - }, - - /// - /// SMTP configuration - #[serde(default)] - pub smtp: SmtpConfig => { - on_env -> SmtpConfig::from_env(); - on_default -> SmtpConfig::default(); - } -}); - -fn default_templates_dir() -> PathBuf { - "./templates".parse().expect("unable to parse into PathBuf") -} - -impl Config { - fn from_file>(path: P) -> Result { - let fd = File::open(path.as_ref()).map_err(Error::Io)?; - serde_yaml::from_reader(fd).map_err(Error::YamlSerialization) - } - - pub fn load>(path: Option

) -> Result { - if CONFIG.get().is_some() { - warn!("Configuration file was already loaded!"); - return Ok(()); - } - - if path.is_none() { - return Config::load(Some("./config.yml")); - } - - let path = path.unwrap(); - let (path, path_string) = { - let p = path.as_ref(); - (p, p.to_str().unwrap()) - }; - - info!("attempting to load config in [{path_string}]"); - match Config::from_file(path) { - Ok(config) => { - info!("...successfully loaded from path [{path_string}]"); - CONFIG.set(config).unwrap(); - Ok(()) - } - - Err(e) => match e { - Error::Io(err) if err.kind() == std::io::ErrorKind::NotFound => { - CONFIG.set(Config::from_env()).unwrap(); - Ok(()) - } - - Error::Io(err) => { - println!("[preinit::error] Unable to load configuration from path [{path_string}] due to IO error: {err} -- opting to use system environment variables instead!"); - CONFIG.set(Config::from_env()).unwrap(); - Ok(()) - } - - err => Err(err), - }, - } - } - - pub fn get<'a>() -> &'a Config { - CONFIG.get().unwrap() - } - - pub fn http_addr(&self) -> Result { - let server = self.server.clone(); - format!("{}:{}", server.host, server.port) - .parse() - .map_err(|e| to_dyn_error!(e)) - } -} diff --git a/src/config/server.rs b/src/config/server.rs index 111b83f..3520bfa 100644 --- a/src/config/server.rs +++ b/src/config/server.rs @@ -12,33 +12,3 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - -use crate::generate_config_struct; - -generate_config_struct!(ServerConfig { - /// - /// Server port to bind to. By default, the email service will bind to - /// `32121`. - #[serde(default = "default_port")] - pub port: u16 => { - on_env -> ::std::env::var("EMAILS_SERVER_PORT").map(|p| p.parse::().expect("unable to parse value to u16")).unwrap_or(default_port()); - on_default -> default_port(); - }, - - /// - /// The host string to bind to. By default, the email service will bind to - /// `0.0.0.0`. - #[serde(default = "default_host")] - pub host: String => { - on_env -> ::std::env::var("EMAILS_SERVER_HOST").unwrap_or(default_host()); - on_default -> default_host(); - } -}); - -fn default_host() -> String { - "0.0.0.0".into() -} - -fn default_port() -> u16 { - 32121 -} diff --git a/src/config/smtp.rs b/src/config/smtp.rs index 8d9bf3b..3520bfa 100644 --- a/src/config/smtp.rs +++ b/src/config/smtp.rs @@ -12,79 +12,3 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - -use crate::generate_config_struct; - -generate_config_struct!(SmtpConfig { - /// - /// Username for authenticating with the SMTP server - #[serde(skip_serializing_if = "Option::is_none")] - pub username: Option => { - on_env -> ::std::env::var("EMAILS_SMTP_USERNAME").ok(); - on_default -> None; - }, - - /// - /// Password for authenticating with the SMTP server - #[serde(skip_serializing_if = "Option::is_none")] - pub password: Option => { - on_env -> ::std::env::var("EMAILS_SMTP_PASSWORD").ok(); - on_default -> None; - }, - - /// - /// The from address when sending out the email to - #[serde(default = "default_smtp_from")] - pub from: String => { - on_env -> ::std::env::var("EMAILS_SMTP_FROM").unwrap_or(default_smtp_from()); - on_default -> default_smtp_from(); - }, - - /// - /// The SMTP host to connect to - #[serde(default = "localhost")] - pub host: String => { - on_env -> ::std::env::var("EMAILS_SMTP_HOST").unwrap_or(localhost()); - on_default -> localhost(); - }, - - /// - /// The SMTP port to connect to - #[serde(default = "default_smtp_port")] - pub port: u16 => { - on_env -> ::std::env::var("EMAILS_SMTP_PORT").map(|p| p.parse::().expect("unable to parse value to u16")).unwrap_or(default_smtp_port()); - on_default -> default_smtp_port(); - }, - - /// - /// If starttls should be enabled when establishing a connection - #[serde(default = "false_variant")] - pub tls: bool => { - on_env -> ::std::env::var("EMAILS_SMTP_STARTTLS").map(|f| f.parse::().expect("Unable to parse into boolean")).unwrap_or(false_variant()); - on_default -> false_variant(); - }, - - /// - /// If SSL should be enabled when establishing a connection - #[serde(default = "false_variant")] - pub ssl: bool => { - on_env -> ::std::env::var("EMAILS_SMTP_SSL").map(|f| f.parse::().expect("Unable to parse into boolean")).unwrap_or(false_variant()); - on_default -> false_variant(); - } -}); - -fn localhost() -> String { - "127.0.0.1".into() -} - -fn default_smtp_port() -> u16 { - 587 -} - -fn default_smtp_from() -> String { - "from@example.com".into() -} - -fn false_variant() -> bool { - false -} diff --git a/src/config/templates.rs b/src/config/templates.rs new file mode 100644 index 0000000..3520bfa --- /dev/null +++ b/src/config/templates.rs @@ -0,0 +1,14 @@ +// 🐻‍❄️💌 email-service: charted's email service built in Rust that can be connected via gRPC +// Copyright 2023 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index 7007c01..0000000 --- a/src/error.rs +++ /dev/null @@ -1,67 +0,0 @@ -// 🐻‍❄️💌 email-service: charted's email service built in Rust that can be connected via gRPC -// Copyright 2023 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::{error::Error as _, net::AddrParseError, path::PathBuf}; - -use tonic::Status; - -#[macro_export] -macro_rules! to_dyn_error { - ($e:expr) => { - $crate::error::Error::Unknown(Box::new($e)) - }; -} - -pub type Result = std::result::Result; - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("I/O: {0}")] - Io(#[from] std::io::Error), - - #[error("Lettre error: {0}")] - Lettre(#[from] lettre::error::Error), - - #[error("{0}")] - Message(String), - - #[error("SMTP error: {0}")] - SmtpError(#[from] lettre::transport::smtp::Error), - - #[error("Mustache error: {0}")] - Mustache(#[from] mustache::Error), - - #[error("{0}")] - Unknown(#[from] Box), - - #[error("YAML serialization: {0}")] - YamlSerialization(#[from] serde_yaml::Error), - - #[error("gRPC error ({}): {}", status.code(), status.message())] - Grpc { status: Status }, - - #[error("Level {level} is not a valid log level")] - UnknownLogLevel { level: String }, - - #[error("Unable to parse [{value}] as a socket address: {}", error.description())] - ParseSocketError { - #[source] - error: AddrParseError, - value: String, - }, - - #[error("Template [{template:?}] was not found")] - TemplateNotFound { template: PathBuf }, -} diff --git a/src/lib.rs b/src/lib.rs index d584b51..3520bfa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,33 +12,3 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - -pub const VERSION: &str = env!("CARGO_PKG_VERSION"); -pub const COMMIT_HASH: &str = env!("SERVICE_COMMIT_HASH"); -pub const BUILD_DATE: &str = env!("SERVICE_BUILD_DATE"); - -#[macro_use] -extern crate log; - -#[macro_use] -extern crate lazy_static; - -#[macro_use] -extern crate async_trait; - -pub mod error; -pub mod setup_utils; - -mod config; -mod service; -mod template_engine; - -pub use config::*; -pub use service::*; -pub use template_engine::*; - -mod protos { - tonic::include_proto!("noelware.charted.emails"); -} - -pub use protos::*; diff --git a/src/logging.rs b/src/logging.rs new file mode 100644 index 0000000..3520bfa --- /dev/null +++ b/src/logging.rs @@ -0,0 +1,14 @@ +// 🐻‍❄️💌 email-service: charted's email service built in Rust that can be connected via gRPC +// Copyright 2023 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. diff --git a/src/logging/mod.rs b/src/logging/mod.rs new file mode 100644 index 0000000..3520bfa --- /dev/null +++ b/src/logging/mod.rs @@ -0,0 +1,14 @@ +// 🐻‍❄️💌 email-service: charted's email service built in Rust that can be connected via gRPC +// Copyright 2023 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. diff --git a/src/logging/visitor.rs b/src/logging/visitor.rs new file mode 100644 index 0000000..3520bfa --- /dev/null +++ b/src/logging/visitor.rs @@ -0,0 +1,14 @@ +// 🐻‍❄️💌 email-service: charted's email service built in Rust that can be connected via gRPC +// Copyright 2023 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. diff --git a/src/main.rs b/src/main.rs index 85fa294..6b32c73 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,43 +13,4 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::env::var; - -use emails::{ - emails_server::EmailsServer, error::Result, setup_utils, to_dyn_error, Config, - EmailService, COMMIT_HASH, VERSION, -}; -use log::*; -use sentry_tower::NewSentryLayer; -use tonic::transport::Server; -use tonic_health::server::health_reporter; - -#[tokio::main] -async fn main() -> Result { - dotenv::dotenv().unwrap_or_default(); - match var("EMAILS_CONFIG_FILE") { - Ok(p) => Config::load(Some(p))?, - Err(_) => Config::load::(None)?, - }; - - let config = Config::get(); - setup_utils::logging(config)?; - setup_utils::sentry(config)?; - - info!("email service v{}+{} - initializing", VERSION, COMMIT_HASH); - - let (mut reporter, health_service) = health_reporter(); - info!("created healthcheck reporter!"); - reporter.set_serving::>().await; - - let http_addr = config.http_addr()?; - info!("listening on http addr {http_addr}!"); - - Server::builder() - .layer(NewSentryLayer::new_from_top()) - .add_service(health_service) - .add_service(EmailsServer::new(EmailService::new().await?)) - .serve(http_addr) - .await - .map_err(|e| to_dyn_error!(e)) -} +fn main() {} diff --git a/src/service.rs b/src/service.rs index d28e499..3520bfa 100644 --- a/src/service.rs +++ b/src/service.rs @@ -12,293 +12,3 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - -use std::collections::HashMap; - -use lettre::{ - message::Mailbox, transport::smtp::authentication::Credentials, Address, - AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor, -}; -use mustache::Data; -use prost_types::{value::Kind, ListValue}; -use tonic::{Request, Response, Status}; - -use crate::{ - error::{Error, Result}, - protos::{ - emails_server::Emails, PingRequest, PingResponse, SendEmailRequest, - SendEmailResponse, - }, - Config, TemplateEngine, COMMIT_HASH, VERSION, -}; - -#[derive(Clone)] -pub struct EmailService { - templates: TemplateEngine, - mailer: AsyncSmtpTransport, -} - -impl EmailService { - pub async fn new() -> Result { - info!("creating smtp mailer..."); - - let config = Config::get(); - let mut mailer = - AsyncSmtpTransport::::relay(config.smtp.host.as_str()) - .map_err(Error::SmtpError)? - .port(config.smtp.port); - - match (config.smtp.username.clone(), config.smtp.password.clone()) { - (Some(username), Some(password)) => { - // so the credentials can be in the final transport - // u dont know how much time it took to realise that i had - // to do this - // - // end me - mailer = mailer - .clone() - .credentials(Credentials::new(username, password)); - } - - (Some(_), None) => { - return Err(Error::Message( - "Missing `smtp.password` configuration key".into(), - )) - } - - (None, Some(_)) => { - return Err(Error::Message( - "Missing `smtp.username` configuration key".into(), - )) - } - - (None, None) => {} - } - - let mailer = mailer.build(); - let success = mailer.test_connection().await?; - if !success { - warn!("unable to send NOOP request to email server, things might break D:"); - } - - let templates = TemplateEngine::new(config.templates.clone()); - templates.init()?; - - info!("created smtp mailer!"); - Ok(EmailService { templates, mailer }) - } -} - -#[async_trait] -impl Emails for EmailService { - async fn ping( - &self, - _request: Request, - ) -> tonic::Result, Status> { - Ok(Response::new(PingResponse { pong: true })) - } - - async fn send( - &self, - request: Request, - ) -> Result, Status> { - let config = Config::get(); - let to = request.get_ref().to.clone(); - debug!("sending email to address [{to}]"); - - let request = request.get_ref(); - if let Some(content) = &request.content { - debug!("...with content\n{content}"); - - let from_addr = match config.smtp.from.parse::

() { - Ok(addr) => addr, - Err(e) => { - error!("Unable to parse from address [{}]: {e}", config.smtp.from); - sentry::capture_error(&e); - - return Err(Status::internal("Internal Server Error")); - } - }; - - let to_addr = match to.parse::
() { - Ok(addr) => addr, - Err(e) => { - error!("Unable to parse to address [{}]: {e}", to); - sentry::capture_error(&e); - - return Err(Status::internal("Internal Server Error")); - } - }; - - let message = Message::builder() - .from(Mailbox::new(None, from_addr)) - .to(Mailbox::new(None, to_addr)) - .subject(request.subject.clone()) - .date_now() - .user_agent(format!("Noelware/charted-email (+https://github.com/charted-dev/email-service; v{VERSION}+{COMMIT_HASH}")) - .body(content.clone()) - .map_err(|e| { - Status::internal(format!("Unable to create message payload: {e}")) - })?; - - match self.mailer.send(message).await { - Ok(_) => { - return Ok(Response::new(SendEmailResponse { - success: true, - should_retry: false, - error_message: None, - })); - } - - Err(e) => { - error!("Unable to send email to [{to}]: {e}"); - sentry::capture_error(&e); - - return Ok(Response::new(SendEmailResponse { - success: false, - should_retry: false, - error_message: Some(format!( - "Unable to send email to [{to}]: {e}" - )), - })); - } - } - } - - // First, we need to find the template that we need to - // render. - if request.template.is_none() { - return Ok(Response::new(SendEmailResponse { - success: false, - should_retry: false, - error_message: Some("Missing template to use".into()), - })); - } - - let template = request.template.as_ref().unwrap(); - debug!("...with template {template}!"); - match self.templates.find(template).await { - Ok(true) => {} - Ok(false) => { - return Ok(Response::new(SendEmailResponse { - success: false, - should_retry: false, - error_message: Some(format!("Template {template} was not found.")), - })) - } - - Err(e) => { - error!("Received i/o error when trying to find template {template}: {e}"); - sentry::capture_error(&e); - - return Err(Status::internal("Internal server error")); - } - } - - let context = match request.context.clone() { - Some(s) => prost_value_to_data(Kind::StructValue(s)), - None => Data::Map(HashMap::::new()), - }; - - let rendered = self - .templates - .render(template, context) - .await - .map_err(|e| { - error!("Unable to render template {template}: {e}"); - sentry::capture_error(&e); - - Status::internal(format!("Unable to render template {template}: {e}")) - })?; - - trace!("rendered result:\n{rendered}"); - let from_addr = match config.smtp.from.parse::
() { - Ok(addr) => addr, - Err(e) => { - error!("Unable to parse from address [{}]: {e}", config.smtp.from); - sentry::capture_error(&e); - - return Err(Status::internal("Internal Server Error")); - } - }; - - let to_addr = match to.parse::
() { - Ok(addr) => addr, - Err(e) => { - error!("Unable to parse to address [{}]: {e}", to); - sentry::capture_error(&e); - - return Err(Status::internal("Internal Server Error")); - } - }; - - let message = Message::builder() - .from(Mailbox::new(None, from_addr)) - .to(Mailbox::new(None, to_addr)) - .subject(request.subject.clone()) - .date_now() - .user_agent(format!("Noelware/charted-email (+https://github.com/charted-dev/email-service; v{VERSION}+{COMMIT_HASH}")) - .body(rendered) - .map_err(|e| { - Status::internal(format!("Unable to create message payload: {e}")) - })?; - - match self.mailer.send(message).await { - Ok(_) => Ok(Response::new(SendEmailResponse { - success: true, - should_retry: false, - error_message: None, - })), - - Err(e) => { - error!("Unable to send email to [{to}]: {e}"); - sentry::capture_error(&e); - - Ok(Response::new(SendEmailResponse { - success: false, - should_retry: false, - error_message: Some(format!("Unable to send email to [{to}]: {e}")), - })) - } - } - } -} - -fn prost_value_to_data(value: Kind) -> Data { - match value { - Kind::BoolValue(b) => Data::Bool(b), - Kind::NullValue(_) => Data::Null, - Kind::NumberValue(float) => Data::String(float.to_string()), - Kind::StringValue(s) => Data::String(s), - Kind::StructValue(s) => { - let mut res = HashMap::new(); - for (key, value) in s.fields { - if value.kind.is_none() { - warn!( - "key [{key}] with value kind {:?} couldn't be determined, skipping!", value.kind - ); - - continue; - } - - res.insert(key, prost_value_to_data(value.kind.clone().unwrap())); - } - - Data::Map(res) - } - - Kind::ListValue(ListValue { values }) => { - let mut res: Vec = vec![]; - for (index, val) in values.iter().enumerate() { - if val.kind.is_none() { - warn!("value kind in index #{index} ({:?}) couldn't be determined, will be skipped", val.kind); - continue; - } - - res.push(prost_value_to_data(val.kind.clone().unwrap())); - } - - Data::Vec(res) - } - } -} diff --git a/src/setup_utils.rs b/src/setup_utils.rs deleted file mode 100644 index 7e370c0..0000000 --- a/src/setup_utils.rs +++ /dev/null @@ -1,218 +0,0 @@ -// 🐻‍❄️💌 email-service: charted's email service built in Rust that can be connected via gRPC -// Copyright 2023 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::{ - borrow::Cow, - io::{Result as IoResult, Write}, - net::{SocketAddr, TcpStream}, - str::FromStr, - sync::Arc, -}; - -use fern::Dispatch; -use once_cell::sync::OnceCell; -use sentry::{ - integrations::{backtrace::AttachStacktraceIntegration, panic::PanicIntegration}, - types::Dsn, -}; - -use ansi_term::Colour::RGB; -use chrono::Local; -use log::Log; -use regex::Regex; -use sentry_log::{NoopLogger, SentryLogger}; -use serde_json::json; - -use crate::{ - error::{Error, Result}, - to_dyn_error, Config, COMMIT_HASH, VERSION, -}; - -lazy_static! { - static ref ANSI_TERM_REGEX: Regex = Regex::new(r#"\u001b\[.*?m"#).unwrap(); - static ref LOG_INIT: OnceCell = OnceCell::new(); -} - -/// Represents a writer that does nothing and discards information that is being written -/// into this writer. -#[derive(Debug, Default)] -pub struct NullWriter; - -impl Write for NullWriter { - fn flush(&mut self) -> IoResult<()> { - Ok(()) - } - - fn write(&mut self, buf: &[u8]) -> IoResult { - Ok(buf.len()) - } -} - -unsafe impl Send for NullWriter {} - -/// Returns a `bool` if [logging] was called when we are running -/// the server. -pub fn log_was_init() -> bool { - *LOG_INIT.get().unwrap_or(&false) -} - -pub fn sentry(config: &Config) -> Result { - if let Some(dsn) = &config.sentry_dsn { - debug!("configuring sentry with DSN [{dsn}]!"); - let _ = sentry::init(sentry::ClientOptions { - dsn: Some(Dsn::from_str(dsn.as_str()).map_err(|e| to_dyn_error!(e))?), - release: Some(Cow::Owned(format!("{VERSION}+{COMMIT_HASH}"))), - traces_sample_rate: 1.0, - attach_stacktrace: true, - integrations: vec![ - Arc::new(AttachStacktraceIntegration::default()), - Arc::new(PanicIntegration::default()), - ], - - ..Default::default() - }); - } - - Ok(()) -} - -pub fn logging(config: &Config) -> Result { - let logging = &config.logging; - let level = logging.clone().level; - let filter = level.to_level_filter(); - let log_in_json = logging.json; - let console = Dispatch::new() - .format(move |out, message, record| { - let thread = std::thread::current(); - let name = thread.name().unwrap_or("main"); - let pid = std::process::id(); - let disable_colours = std::env::var("MAKUTU_DISABLE_COLOURS").is_ok(); - - if disable_colours { - out.finish(format_args!( - "{} {:<5} [{} <{}> ({})] :: {}", - Local::now().format("[%B %d, %G | %H:%M:%S %p]"), - record.level(), - record.target(), - pid, - name, - message - )); - } else if log_in_json { - let level_name = record.level().as_str(); - let msg = format_args!("{message}").to_string(); - let data = json!({ - "@timestamp": Local::now().to_rfc3339(), - "@version": "1", - "message": ANSI_TERM_REGEX.replace(msg.as_str(), ""), - "source": format!("Noelware/makutu v{VERSION}+{COMMIT_HASH}"), - "module": record.target(), - "thread": name, - "level": level_name, - "pid": pid, - "file": json!({ - "path": record.file(), - "line": record.line() - }) - }); - - out.finish(format_args!("{data}")); - } else { - let color = match record.level() { - log::Level::Error => RGB(153, 75, 104).bold(), - log::Level::Debug => RGB(163, 182, 138).bold(), - log::Level::Info => RGB(178, 157, 243).bold(), - log::Level::Trace => RGB(163, 182, 138).bold(), - log::Level::Warn => RGB(243, 243, 134).bold(), - }; - - let pid = std::process::id(); - let time = RGB(134, 134, 134).paint(format!( - "{}", - Local::now().format("[%B %d, %G | %H:%M:%S %p]") - )); - - let level = color.paint(format!("{:<5}", record.level())); - let (b1, b2) = (RGB(134, 134, 134).paint("["), RGB(134, 134, 134).paint("]")); - let (p1, p2) = (RGB(134, 134, 134).paint("("), RGB(134, 134, 134).paint(")")); - let target = RGB(120, 231, 255).paint(format!("{:<25}", record.target())); - let thread_name = RGB(255, 105, 189).paint(format!("{name:>25}")); - let pid_colour = RGB(169, 147, 227).paint(pid.to_string()); - - out.finish(format_args!( - "{time} {level} {b1}{target} {thread_name} {p1}{pid_colour}{p2}{b2} :: {message}" - )); - } - }) - .chain(std::io::stdout()) - .level(filter) - .into_shared(); - - let logstash = Dispatch::new() - .format(move |out, message, record| { - let thread = std::thread::current(); - let name = thread.name().unwrap_or("main"); - let pid = std::process::id(); - let level_name = record.level().as_str(); - let msg = format_args!("{message}").to_string(); - let data = json!({ - "@timestamp": Local::now().to_rfc3339(), - "@version": "1", - "message": ANSI_TERM_REGEX.replace(msg.as_str(), ""), - "source": format!("Noelware/makutu v{VERSION}+{COMMIT_HASH}"), - "module": record.target(), - "thread": name, - "level": level_name, - "pid": pid, - "file": json!({ - "path": record.file(), - "line": record.line() - }) - }); - - out.finish(format_args!("{data}")); - }) - .level(filter) - .chain(if let Some(url) = logging.logstash_uri() { - let addr = url - .parse::() - .map_err(|e| Error::ParseSocketError { - error: e, - value: url, - }) - .expect("unable to parse to socket addr"); - - Box::new( - TcpStream::connect(addr) - .expect("Unable to connect to Logstash TCP stream!"), - ) as Box - } else { - Box::::default() as Box - }) - .into_shared(); - - let dispatch = Dispatch::new().chain(console).chain(logstash).chain( - if config.sentry_dsn.is_some() { - Box::new(SentryLogger::new()) - } else { - Box::new(NoopLogger) as Box - }, - ); - - dispatch.apply().map_err(|e| to_dyn_error!(e)).map(|_| { - LOG_INIT.set(true).unwrap(); - () - }) -} diff --git a/src/template_engine.rs b/src/template_engine.rs deleted file mode 100644 index b77e499..0000000 --- a/src/template_engine.rs +++ /dev/null @@ -1,83 +0,0 @@ -// 🐻‍❄️💌 email-service: charted's email service built in Rust that can be connected via gRPC -// Copyright 2023 Noelware, LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::{ - fs::create_dir, - path::{Path, PathBuf}, -}; - -use crate::error::{Error, Result}; -use tokio::{fs::File, io::AsyncReadExt}; - -/// Represents the engine for converting templates in a directory into what the service -/// can send. This is an abstraction over Mustache templates. -#[derive(Debug, Clone)] -pub struct TemplateEngine { - templates_dir: PathBuf, -} - -impl TemplateEngine { - /// Creates a new [`TemplateEngine`] with the directory of the template. - pub fn new>(path: P) -> TemplateEngine { - TemplateEngine { - templates_dir: path.as_ref().to_path_buf(), - } - } - - pub fn init(&self) -> Result { - info!("initializing template engine..."); - - if !self.templates_dir.exists() { - warn!( - "templates in directory [{:?}] doesn't exist", - self.templates_dir - ); - - create_dir(self.templates_dir.clone()).map_err(Error::Io)?; - } - - Ok(()) - } - - /// Could the file in the path be found? - pub async fn find>(&self, path: P) -> Result { - match File::open(self.templates_dir.join(path.as_ref())).await { - Ok(_) => Ok(true), - Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false), - Err(e) => Err(Error::Io(e)), - } - } - - /// Renders a template with the given path and context object - pub async fn render, C: Into>( - &self, - path: P, - context: C, - ) -> Result { - let template = self.templates_dir.join(path.as_ref()); - if !template.exists() { - return Err(Error::TemplateNotFound { template }); - } - - debug!("found template [{:?}]", template.display()); - let mut file = File::open(template).await?; - let mut buf = String::new(); - file.read_to_string(&mut buf).await?; - - // Now, let's put it into Mustache - let compiled_template = mustache::compile_str(buf.as_str())?; - Ok(compiled_template.render_data_to_string(&context.into())?) - } -} diff --git a/src/templates.rs b/src/templates.rs new file mode 100644 index 0000000..3520bfa --- /dev/null +++ b/src/templates.rs @@ -0,0 +1,14 @@ +// 🐻‍❄️💌 email-service: charted's email service built in Rust that can be connected via gRPC +// Copyright 2023 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. diff --git a/src/templates/resolver.rs b/src/templates/resolver.rs new file mode 100644 index 0000000..3520bfa --- /dev/null +++ b/src/templates/resolver.rs @@ -0,0 +1,14 @@ +// 🐻‍❄️💌 email-service: charted's email service built in Rust that can be connected via gRPC +// Copyright 2023 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. diff --git a/src/templates/resolver/filesystem.rs b/src/templates/resolver/filesystem.rs new file mode 100644 index 0000000..3520bfa --- /dev/null +++ b/src/templates/resolver/filesystem.rs @@ -0,0 +1,14 @@ +// 🐻‍❄️💌 email-service: charted's email service built in Rust that can be connected via gRPC +// Copyright 2023 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. diff --git a/src/templates/resolver/git.rs b/src/templates/resolver/git.rs new file mode 100644 index 0000000..3520bfa --- /dev/null +++ b/src/templates/resolver/git.rs @@ -0,0 +1,14 @@ +// 🐻‍❄️💌 email-service: charted's email service built in Rust that can be connected via gRPC +// Copyright 2023 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..3520bfa --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,14 @@ +// 🐻‍❄️💌 email-service: charted's email service built in Rust that can be connected via gRPC +// Copyright 2023 Noelware, LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. diff --git a/template_builder/.gitkeep b/template_builder/.gitkeep deleted file mode 100644 index 9ed4a44..0000000 --- a/template_builder/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# woof woof