mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-03 05:43:24 +00:00
UBERF-9542: Add mail service with SMTP and SES support (#8130)
Some checks are pending
CI / uitest-pg (push) Waiting to run
CI / uitest (push) Waiting to run
CI / build (push) Waiting to run
CI / svelte-check (push) Blocked by required conditions
CI / formatting (push) Blocked by required conditions
CI / test (push) Blocked by required conditions
CI / uitest-qms (push) Waiting to run
CI / uitest-workspaces (push) Waiting to run
CI / docker-build (push) Blocked by required conditions
CI / dist-build (push) Blocked by required conditions
Some checks are pending
CI / uitest-pg (push) Waiting to run
CI / uitest (push) Waiting to run
CI / build (push) Waiting to run
CI / svelte-check (push) Blocked by required conditions
CI / formatting (push) Blocked by required conditions
CI / test (push) Blocked by required conditions
CI / uitest-qms (push) Waiting to run
CI / uitest-workspaces (push) Waiting to run
CI / docker-build (push) Blocked by required conditions
CI / dist-build (push) Blocked by required conditions
Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
parent
3b7ef214e0
commit
1e134664e7
@ -246,7 +246,7 @@
|
||||
"summary": "Build docker with platform",
|
||||
"description": "use to build all docker containers required for platform",
|
||||
"safeForSimultaneousRushProcesses": true,
|
||||
"shellCommand": "rush docker:build -p 20 --to @hcengineering/pod-server --to @hcengineering/pod-front --to @hcengineering/prod --to @hcengineering/pod-account --to @hcengineering/pod-workspace --to @hcengineering/pod-collaborator --to @hcengineering/tool --to @hcengineering/pod-print --to @hcengineering/pod-sign --to @hcengineering/pod-analytics-collector --to @hcengineering/rekoni-service --to @hcengineering/pod-ai-bot --to @hcengineering/import-tool --to @hcengineering/pod-stats --to @hcengineering/pod-fulltext --to @hcengineering/pod-love --to @hcengineering/green"
|
||||
"shellCommand": "rush docker:build -p 20 --to @hcengineering/pod-server --to @hcengineering/pod-front --to @hcengineering/prod --to @hcengineering/pod-account --to @hcengineering/pod-workspace --to @hcengineering/pod-collaborator --to @hcengineering/tool --to @hcengineering/pod-print --to @hcengineering/pod-sign --to @hcengineering/pod-analytics-collector --to @hcengineering/rekoni-service --to @hcengineering/pod-ai-bot --to @hcengineering/import-tool --to @hcengineering/pod-stats --to @hcengineering/pod-fulltext --to @hcengineering/pod-love --to @hcengineering/green --to @hcengineering/pod-mail"
|
||||
},
|
||||
{
|
||||
"commandKind": "global",
|
||||
|
@ -700,6 +700,9 @@ importers:
|
||||
'@rush-temp/pod-love':
|
||||
specifier: file:./projects/pod-love.tgz
|
||||
version: file:projects/pod-love.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(utf-8-validate@6.0.4)
|
||||
'@rush-temp/pod-mail':
|
||||
specifier: file:./projects/pod-mail.tgz
|
||||
version: file:projects/pod-mail.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))
|
||||
'@rush-temp/pod-print':
|
||||
specifier: file:./projects/pod-print.tgz
|
||||
version: file:projects/pod-print.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(utf-8-validate@6.0.4)
|
||||
@ -1420,6 +1423,9 @@ importers:
|
||||
'@types/node-fetch':
|
||||
specifier: ~2.6.2
|
||||
version: 2.6.11
|
||||
'@types/nodemailer':
|
||||
specifier: ~6.4.17
|
||||
version: 6.4.17
|
||||
'@types/otp-generator':
|
||||
specifier: ^4.0.2
|
||||
version: 4.0.2
|
||||
@ -1834,6 +1840,9 @@ importers:
|
||||
node-loader:
|
||||
specifier: ~2.0.0
|
||||
version: 2.0.0(webpack@5.97.1)
|
||||
nodemailer:
|
||||
specifier: ~6.10.0
|
||||
version: 6.10.0
|
||||
octokit:
|
||||
specifier: ^3.1.1
|
||||
version: 3.2.1
|
||||
@ -4016,7 +4025,7 @@ packages:
|
||||
version: 0.0.0
|
||||
|
||||
'@rush-temp/api-tests@file:projects/api-tests.tgz':
|
||||
resolution: {integrity: sha512-gleyNro85I24sFWGaNjqqz1AF2c8sugQ4vZX/UD/OVu+It6mI96P2Zd8pDl9FqPaIUvtYng8ePj0ThjwI5uGFw==, tarball: file:projects/api-tests.tgz}
|
||||
resolution: {integrity: sha512-sZvswWPvNBNUJ3rmWnDInxFbuHO494V1BWc786PDGXXlzeOA+KyuAw1LjuDH68VCatzruASrxcKjtekhMWHzNg==, tarball: file:projects/api-tests.tgz}
|
||||
version: 0.0.0
|
||||
|
||||
'@rush-temp/attachment-assets@file:projects/attachment-assets.tgz':
|
||||
@ -4803,6 +4812,10 @@ packages:
|
||||
resolution: {integrity: sha512-m+9dPMR45FJpQB+M5ka4PiJcTpV8COlQA5F36is2LkMLwnsDjcB94Sx+TFdIyjhVFpfB8l+t886mSPveHql4ag==, tarball: file:projects/pod-love.tgz}
|
||||
version: 0.0.0
|
||||
|
||||
'@rush-temp/pod-mail@file:projects/pod-mail.tgz':
|
||||
resolution: {integrity: sha512-XO6I8SkpJ2CFVPWBZFflMCDjKbH1uQpKXhS/DPdGmneb8u+8PB3QbYRZT5R01+GPZIvIcLw11q5Q4psD60nDOQ==, tarball: file:projects/pod-mail.tgz}
|
||||
version: 0.0.0
|
||||
|
||||
'@rush-temp/pod-print@file:projects/pod-print.tgz':
|
||||
resolution: {integrity: sha512-i0Y9ShIeieNqCNMCXax1CZTStiDTw9M156m7fCDSrNS6VV4v2e5jy/HyNLtpyXz/ZRlOKndBdyiKsls+5pDL8g==, tarball: file:projects/pod-print.tgz}
|
||||
version: 0.0.0
|
||||
@ -6394,6 +6407,9 @@ packages:
|
||||
'@types/node@20.11.19':
|
||||
resolution: {integrity: sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==}
|
||||
|
||||
'@types/nodemailer@6.4.17':
|
||||
resolution: {integrity: sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==}
|
||||
|
||||
'@types/oauth@0.9.4':
|
||||
resolution: {integrity: sha512-qk9orhti499fq5XxKCCEbd0OzdPZuancneyse3KtR+vgMiHRbh+mn8M4G6t64ob/Fg+GZGpa565MF/2dKWY32A==}
|
||||
|
||||
@ -10993,6 +11009,10 @@ packages:
|
||||
node-releases@2.0.19:
|
||||
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
|
||||
|
||||
nodemailer@6.10.0:
|
||||
resolution: {integrity: sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
nopt@1.0.10:
|
||||
resolution: {integrity: sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==}
|
||||
hasBin: true
|
||||
@ -21495,6 +21515,47 @@ snapshots:
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
'@rush-temp/pod-mail@file:projects/pod-mail.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))':
|
||||
dependencies:
|
||||
'@aws-sdk/client-ses': 3.738.0
|
||||
'@tsconfig/node16': 1.0.4
|
||||
'@types/cors': 2.8.17
|
||||
'@types/express': 4.17.21
|
||||
'@types/jest': 29.5.12
|
||||
'@types/node': 20.11.19
|
||||
'@types/nodemailer': 6.4.17
|
||||
'@types/web-push': 3.6.4
|
||||
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.7.3)
|
||||
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.7.3)
|
||||
cors: 2.8.5
|
||||
cross-env: 7.0.3
|
||||
dotenv: 16.0.3
|
||||
esbuild: 0.24.2
|
||||
eslint: 8.56.0
|
||||
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.7.3)
|
||||
eslint-plugin-import: 2.29.1(eslint@8.56.0)
|
||||
eslint-plugin-n: 15.7.0(eslint@8.56.0)
|
||||
eslint-plugin-node: 11.1.0(eslint@8.56.0)
|
||||
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
|
||||
express: 4.21.2
|
||||
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))
|
||||
nodemailer: 6.10.0
|
||||
prettier: 3.2.5
|
||||
ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.11.19)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3)))(typescript@5.7.3)
|
||||
ts-node: 10.9.2(@types/node@20.11.19)(typescript@5.7.3)
|
||||
typescript: 5.7.3
|
||||
web-push: 3.6.7
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- '@jest/types'
|
||||
- '@swc/core'
|
||||
- '@swc/wasm'
|
||||
- aws-crt
|
||||
- babel-jest
|
||||
- babel-plugin-macros
|
||||
- node-notifier
|
||||
- supports-color
|
||||
|
||||
'@rush-temp/pod-print@file:projects/pod-print.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(bufferutil@4.0.8)(utf-8-validate@6.0.4)':
|
||||
dependencies:
|
||||
'@tsconfig/node16': 1.0.4
|
||||
@ -27696,6 +27757,10 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 5.26.5
|
||||
|
||||
'@types/nodemailer@6.4.17':
|
||||
dependencies:
|
||||
'@types/node': 20.11.19
|
||||
|
||||
'@types/oauth@0.9.4':
|
||||
dependencies:
|
||||
'@types/node': 20.11.19
|
||||
@ -33502,6 +33567,8 @@ snapshots:
|
||||
|
||||
node-releases@2.0.19: {}
|
||||
|
||||
nodemailer@6.10.0: {}
|
||||
|
||||
nopt@1.0.10:
|
||||
dependencies:
|
||||
abbrev: 1.1.1
|
||||
@ -35764,6 +35831,24 @@ snapshots:
|
||||
v8-compile-cache-lib: 3.0.1
|
||||
yn: 3.1.1
|
||||
|
||||
ts-node@10.9.2(@types/node@20.11.19)(typescript@5.7.3):
|
||||
dependencies:
|
||||
'@cspotcode/source-map-support': 0.8.1
|
||||
'@tsconfig/node10': 1.0.9
|
||||
'@tsconfig/node12': 1.0.11
|
||||
'@tsconfig/node14': 1.0.3
|
||||
'@tsconfig/node16': 1.0.4
|
||||
'@types/node': 20.11.19
|
||||
acorn: 8.11.3
|
||||
acorn-walk: 8.3.2
|
||||
arg: 4.1.3
|
||||
create-require: 1.1.1
|
||||
diff: 4.0.2
|
||||
make-error: 1.3.6
|
||||
typescript: 5.7.3
|
||||
v8-compile-cache-lib: 3.0.1
|
||||
yn: 3.1.1
|
||||
|
||||
ts-node@7.0.1:
|
||||
dependencies:
|
||||
arrify: 1.0.1
|
||||
|
@ -2325,6 +2325,11 @@
|
||||
"packageName": "@hcengineering/model-my-space",
|
||||
"projectFolder": "models/my-space",
|
||||
"shouldPublish": false
|
||||
},
|
||||
{
|
||||
"packageName": "@hcengineering/pod-mail",
|
||||
"projectFolder": "services/mail/pod-mail",
|
||||
"shouldPublish": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
7
services/mail/pod-mail/.eslintrc.js
Normal file
7
services/mail/pod-mail/.eslintrc.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
1
services/mail/pod-mail/.gitignore
vendored
Normal file
1
services/mail/pod-mail/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.env
|
4
services/mail/pod-mail/.npmignore
Normal file
4
services/mail/pod-mail/.npmignore
Normal file
@ -0,0 +1,4 @@
|
||||
*
|
||||
!/lib/**
|
||||
!CHANGELOG.md
|
||||
/lib/**/__tests__/
|
7
services/mail/pod-mail/Dockerfile
Normal file
7
services/mail/pod-mail/Dockerfile
Normal file
@ -0,0 +1,7 @@
|
||||
FROM hardcoreeng/base:v20250113a
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY bundle/bundle.js ./
|
||||
|
||||
EXPOSE 8097
|
||||
CMD [ "node", "bundle.js" ]
|
60
services/mail/pod-mail/README.md
Normal file
60
services/mail/pod-mail/README.md
Normal file
@ -0,0 +1,60 @@
|
||||
# Mail Service
|
||||
|
||||
## Overview
|
||||
|
||||
The Mail Service is responsible for sending emails using SMTP or SES transfer.
|
||||
It supports sending emails with multiple recipients, along with optional CC, BCC, and HTML content.
|
||||
|
||||
### Configuration
|
||||
|
||||
Environment variables should be set to configure the Mail Service:
|
||||
- `PORT`: The port on which the mail service listens for incoming HTTP requests.
|
||||
|
||||
Settings for SMTP or SES email service should be specified, simultaneous use of both protocols is not supported
|
||||
|
||||
SMTP settings:
|
||||
- `SMTP_HOST`: Hostname of the SMTP server used for sending emails.
|
||||
- `SMTP_PORT`: Port number of the SMTP server.
|
||||
- `SMTP_USERNAME`: Username for authenticating with the SMTP server. Refer to your SMTP server documentation for the appropriate format.
|
||||
- `SMTP_PASSWORD`: Password for authenticating with the SMTP server. Refer to your SMTP server documentation for the appropriate format.
|
||||
|
||||
SES settings:
|
||||
- `SES_ACCESS_KEY`: AWS SES access key for authentication.
|
||||
- `SES_SECRET_KEY`: AWS SES secret key for authentication.
|
||||
- `SES_REGION`: AWS SES region where your SES service is hosted.
|
||||
|
||||
### Running the Service
|
||||
|
||||
Add .env file to the root of the project with the following content to add integration with fake SMTP server:
|
||||
```
|
||||
PORT=8097
|
||||
SMTP_HOST="mail.smtpbucket.com"
|
||||
SMTP_PORT=8025
|
||||
```
|
||||
To use the real SMTP server it is required to register an account in some email service provider and specify settings and credentials for it.
|
||||
|
||||
Start the service locally using:
|
||||
```bash
|
||||
rushx run-local
|
||||
```
|
||||
|
||||
The service will run and listen for incoming requests on the configured port.
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### POST /send
|
||||
|
||||
Send an email message.
|
||||
|
||||
#### Request Body
|
||||
|
||||
- `to`: Required. String or array of strings containing recipient email addresses.
|
||||
- `text`: Required. String containing the plain text message body.
|
||||
- `subject`: Required. String containing the email subject.
|
||||
- `html`: Optional. String containing HTML message body.
|
||||
- `from`: Optional. Sender's email address.
|
||||
|
||||
#### Response
|
||||
|
||||
- `200 OK` on success.
|
||||
- `400 Bad Request` if required fields are missing.
|
19
services/mail/pod-mail/build.sh
Executable file
19
services/mail/pod-mail/build.sh
Executable file
@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Copyright © 2025 Hardcore Engineering Inc.
|
||||
#
|
||||
# Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License. You may
|
||||
# obtain a copy of the License at https://www.eclipse.org/legal/epl-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.
|
||||
#
|
||||
|
||||
rushx bundle
|
||||
rushx docker:build
|
||||
rushx docker:push
|
4
services/mail/pod-mail/config/rig.json
Normal file
4
services/mail/pod-mail/config/rig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig"
|
||||
}
|
7
services/mail/pod-mail/jest.config.js
Normal file
7
services/mail/pod-mail/jest.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
|
||||
roots: ["./src"],
|
||||
coverageReporters: ["text-summary", "html"]
|
||||
}
|
63
services/mail/pod-mail/package.json
Normal file
63
services/mail/pod-mail/package.json
Normal file
@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "@hcengineering/pod-mail",
|
||||
"version": "0.6.0",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
"files": [
|
||||
"lib/**/*",
|
||||
"types/**/*",
|
||||
"tsconfig.json"
|
||||
],
|
||||
"author": "Hardcore Engineering Inc.",
|
||||
"scripts": {
|
||||
"build": "compile",
|
||||
"build:watch": "compile",
|
||||
"test": "jest --passWithNoTests --silent",
|
||||
"_phase:bundle": "rushx bundle",
|
||||
"_phase:docker-build": "rushx docker:build",
|
||||
"_phase:docker-staging": "rushx docker:staging",
|
||||
"bundle": "node ../../../common/scripts/esbuild.js",
|
||||
"docker:build": "../../../common/scripts/docker_build.sh hardcoreeng/mail",
|
||||
"docker:staging": "../../../common/scripts/docker_tag.sh hardcoreeng/mail staging",
|
||||
"docker:abuild": "docker build -t hardcoreeng/mail . --platform=linux/arm64 && ../../../common/scripts/docker_tag_push.sh hardcoreeng/mail",
|
||||
"docker:push": "../../../common/scripts/docker_tag.sh hardcoreeng/mail",
|
||||
"run-local": "ts-node src/index.ts",
|
||||
"format": "format src",
|
||||
"_phase:build": "compile transpile src",
|
||||
"_phase:test": "jest --passWithNoTests --silent",
|
||||
"_phase:format": "format src",
|
||||
"_phase:validate": "compile validate"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "~7.0.3",
|
||||
"@hcengineering/platform-rig": "^0.6.0",
|
||||
"@types/node": "~20.11.16",
|
||||
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
||||
"@typescript-eslint/parser": "^6.11.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-n": "^15.4.0",
|
||||
"eslint": "^8.54.0",
|
||||
"esbuild": "^0.24.2",
|
||||
"prettier": "^3.1.0",
|
||||
"ts-node": "^10.8.0",
|
||||
"typescript": "^5.3.3",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5",
|
||||
"@tsconfig/node16": "^1.0.4",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/express": "^4.17.13",
|
||||
"eslint-plugin-node": "^11.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-ses": "^3.738.0",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "~16.0.0",
|
||||
"express": "^4.21.2",
|
||||
"nodemailer": "^6.10.0"
|
||||
}
|
||||
}
|
83
services/mail/pod-mail/src/__tests__/config.test.ts
Normal file
83
services/mail/pod-mail/src/__tests__/config.test.ts
Normal file
@ -0,0 +1,83 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-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.
|
||||
//
|
||||
|
||||
describe('Config', () => {
|
||||
const originalEnv = process.env
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules()
|
||||
process.env = { ...originalEnv }
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
process.env = originalEnv
|
||||
})
|
||||
|
||||
test('should throw an error if PORT is missing', () => {
|
||||
process.env.PORT = undefined
|
||||
expect(() => require('../config')).toThrow('Missing env variable: Port')
|
||||
})
|
||||
|
||||
test('should load default SMTP config if all required env variables are set', () => {
|
||||
process.env.PORT = '1025'
|
||||
process.env.SMTP_HOST = 'smtp.example.com'
|
||||
process.env.SMTP_PORT = '587'
|
||||
process.env.SMTP_USERNAME = 'user'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const loadedConfig = require('../config').default
|
||||
|
||||
expect(loadedConfig.port).toBe(1025)
|
||||
expect(loadedConfig.smtpConfig).toEqual({
|
||||
Host: 'smtp.example.com',
|
||||
Port: 587,
|
||||
Username: 'user',
|
||||
Password: undefined
|
||||
})
|
||||
})
|
||||
|
||||
test('should load SES config if all required env variables are set', () => {
|
||||
process.env.SMTP_HOST = undefined
|
||||
process.env.PORT = '1025'
|
||||
process.env.SES_ACCESS_KEY = 'access_key'
|
||||
process.env.SES_SECRET_KEY = 'secret_key'
|
||||
process.env.SES_REGION = 'us-west-2'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const loadedConfig = require('../config').default
|
||||
|
||||
expect(loadedConfig.sesConfig).toEqual({
|
||||
AccessKey: 'access_key',
|
||||
SecretKey: 'secret_key',
|
||||
Region: 'us-west-2'
|
||||
})
|
||||
})
|
||||
|
||||
test('should throw an error if both SES and SMTP configs are missing', () => {
|
||||
process.env.PORT = '1025'
|
||||
process.env.SES_ACCESS_KEY = undefined
|
||||
process.env.SMTP_HOST = undefined
|
||||
|
||||
expect(() => require('../config')).toThrow('Please specify SES or SMTP configuration')
|
||||
})
|
||||
|
||||
test('should throw an error if both SES and SMTP are configured', () => {
|
||||
process.env.PORT = '1025'
|
||||
process.env.SES_ACCESS_KEY = 'access_key'
|
||||
process.env.SMTP_HOST = 'smtp.example.com'
|
||||
|
||||
expect(() => require('../config')).toThrow('Both SMTP and SES configuration are specified, please specify only one')
|
||||
})
|
||||
})
|
119
services/mail/pod-mail/src/config.ts
Normal file
119
services/mail/pod-mail/src/config.ts
Normal file
@ -0,0 +1,119 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-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.
|
||||
//
|
||||
import { config as dotenvConfig } from 'dotenv'
|
||||
|
||||
dotenvConfig()
|
||||
|
||||
export interface Config {
|
||||
port: number
|
||||
source?: string
|
||||
sesConfig?: SesConfig
|
||||
smtpConfig?: SmtpConfig
|
||||
}
|
||||
|
||||
export interface SesConfig {
|
||||
AccessKey: string
|
||||
SecretKey: string
|
||||
Region: string
|
||||
}
|
||||
|
||||
export interface SmtpConfig {
|
||||
Host: string
|
||||
Port: number
|
||||
Username: string | undefined
|
||||
Password: string | undefined
|
||||
}
|
||||
|
||||
const envMap = {
|
||||
Port: 'PORT',
|
||||
Source: 'SOURCE',
|
||||
DefaultProtocol: 'DEFAULT_PROTOCOL',
|
||||
SesAccessKey: 'SES_ACCESS_KEY',
|
||||
SesSecretKey: 'SES_SECRET_KEY',
|
||||
SesRegion: 'SES_REGION',
|
||||
SmtpHost: 'SMTP_HOST',
|
||||
SmtpPort: 'SMTP_PORT',
|
||||
SmtpUsername: 'SMTP_USERNAME',
|
||||
SmtpPassword: 'SMTP_PASSWORD'
|
||||
}
|
||||
|
||||
const parseNumber = (str: string | undefined): number | undefined => (str !== undefined ? Number(str) : undefined)
|
||||
const isEmpty = (str: string | undefined): boolean => str === undefined || str.trim().length === 0
|
||||
|
||||
const buildSesConfig = (): SesConfig => {
|
||||
const accessKey = process.env[envMap.SesAccessKey]
|
||||
const secretKey = process.env[envMap.SesSecretKey]
|
||||
const region = process.env[envMap.SesRegion]
|
||||
|
||||
if (isEmpty(accessKey) || isEmpty(secretKey) || isEmpty(region)) {
|
||||
const missingKeys = [
|
||||
isEmpty(accessKey) && 'SES_ACCESS_KEY',
|
||||
isEmpty(secretKey) && 'SES_SECRET_KEY',
|
||||
isEmpty(region) && 'SES_REGION'
|
||||
].filter(Boolean)
|
||||
throw Error(`Missing env variables for SES configuration: ${missingKeys.join(', ')}`)
|
||||
}
|
||||
|
||||
return {
|
||||
AccessKey: accessKey as string,
|
||||
SecretKey: secretKey as string,
|
||||
Region: region as string
|
||||
}
|
||||
}
|
||||
|
||||
const buildSmtpConfig = (): SmtpConfig => {
|
||||
const host = process.env[envMap.SmtpHost]
|
||||
const port = parseNumber(process.env[envMap.SmtpPort])
|
||||
const username = process.env[envMap.SmtpUsername]
|
||||
const password = process.env[envMap.SmtpPassword]
|
||||
|
||||
if (isEmpty(host) || port === undefined) {
|
||||
const missingKeys = [isEmpty(host) && 'SMTP_HOST', port === undefined && 'SMTP_PORT'].filter(Boolean)
|
||||
|
||||
throw Error(`Missing env variables for SMTP configuration: ${missingKeys.join(', ')}`)
|
||||
}
|
||||
|
||||
return {
|
||||
Host: host as string,
|
||||
Port: port,
|
||||
Username: username,
|
||||
Password: password
|
||||
}
|
||||
}
|
||||
|
||||
const config: Config = (() => {
|
||||
const port = parseNumber(process.env[envMap.Port])
|
||||
if (port === undefined) {
|
||||
throw Error('Missing env variable: Port')
|
||||
}
|
||||
const isSmtpConfig = !isEmpty(process.env[envMap.SmtpHost])
|
||||
const isSesConfig = !isEmpty(process.env[envMap.SesAccessKey])
|
||||
if (isSmtpConfig && isSesConfig) {
|
||||
throw Error('Both SMTP and SES configuration are specified, please specify only one')
|
||||
}
|
||||
if (!isSmtpConfig && !isSesConfig) {
|
||||
throw Error('Please specify SES or SMTP configuration')
|
||||
}
|
||||
const params: Config = {
|
||||
port,
|
||||
source: process.env[envMap.Source],
|
||||
sesConfig: isSesConfig ? buildSesConfig() : undefined,
|
||||
smtpConfig: isSmtpConfig ? buildSmtpConfig() : undefined
|
||||
}
|
||||
|
||||
return params
|
||||
})()
|
||||
|
||||
export default config
|
23
services/mail/pod-mail/src/error.ts
Normal file
23
services/mail/pod-mail/src/error.ts
Normal file
@ -0,0 +1,23 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-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.
|
||||
//
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor (
|
||||
readonly code: string,
|
||||
readonly message: string
|
||||
) {
|
||||
super(message)
|
||||
}
|
||||
}
|
22
services/mail/pod-mail/src/index.ts
Normal file
22
services/mail/pod-mail/src/index.ts
Normal file
@ -0,0 +1,22 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-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.
|
||||
//
|
||||
|
||||
import { main } from './main'
|
||||
|
||||
void main().catch((err) => {
|
||||
if (err != null) {
|
||||
console.error(err)
|
||||
}
|
||||
})
|
53
services/mail/pod-mail/src/mail.ts
Normal file
53
services/mail/pod-mail/src/mail.ts
Normal file
@ -0,0 +1,53 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-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.
|
||||
//
|
||||
import { type SendMailOptions, type Transporter } from 'nodemailer'
|
||||
|
||||
import config from './config'
|
||||
import { Message, Receivers } from './types'
|
||||
import { getTransport } from './transport'
|
||||
|
||||
export class MailClient {
|
||||
private readonly transporter: Transporter
|
||||
|
||||
constructor () {
|
||||
this.transporter = getTransport(config)
|
||||
}
|
||||
|
||||
async sendMessage (message: Message, receivers: Receivers, from?: string): Promise<void> {
|
||||
const mailOptions: SendMailOptions = {
|
||||
from: from ?? config.source,
|
||||
to: receivers.to,
|
||||
text: message.text,
|
||||
subject: message.subject
|
||||
}
|
||||
if (receivers.cc !== undefined) {
|
||||
mailOptions.cc = receivers.cc
|
||||
}
|
||||
if (receivers.bcc !== undefined) {
|
||||
mailOptions.bcc = receivers.bcc
|
||||
}
|
||||
if (message.html !== undefined) {
|
||||
mailOptions.html = message.html
|
||||
}
|
||||
|
||||
this.transporter.sendMail(mailOptions, (err, info) => {
|
||||
if (err !== null) {
|
||||
console.error('Failed to send email: ', err.message)
|
||||
} else {
|
||||
console.log(`Email request ${info?.messageId} sent: ${info?.response}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
78
services/mail/pod-mail/src/main.ts
Normal file
78
services/mail/pod-mail/src/main.ts
Normal file
@ -0,0 +1,78 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-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.
|
||||
//
|
||||
|
||||
import config from './config'
|
||||
import { createServer, listen } from './server'
|
||||
import { MailClient } from './mail'
|
||||
import { Endpoint } from './types'
|
||||
|
||||
export const main = async (): Promise<void> => {
|
||||
const client = new MailClient()
|
||||
console.log('Mail service has been started')
|
||||
|
||||
const endpoints: Endpoint[] = [
|
||||
{
|
||||
endpoint: '/send',
|
||||
type: 'post',
|
||||
handler: async (req, res) => {
|
||||
// Skip auth check, since service should be internal
|
||||
const text = req.body?.text
|
||||
if (text === undefined) {
|
||||
res.status(400).send({ err: "'text' is missing" })
|
||||
return
|
||||
}
|
||||
const subject = req.body?.subject
|
||||
if (subject === undefined) {
|
||||
res.status(400).send({ err: "'subject' is missing" })
|
||||
return
|
||||
}
|
||||
const html = req.body?.html
|
||||
const to = req.body?.to
|
||||
if (to === undefined) {
|
||||
res.status(400).send({ err: "'to' is missing" })
|
||||
return
|
||||
}
|
||||
const receivers = {
|
||||
to: Array.isArray(to) ? to : [to]
|
||||
}
|
||||
const from = req.body?.from
|
||||
try {
|
||||
await client.sendMessage({ text, subject, html }, receivers, from)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
|
||||
res.send()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const server = listen(createServer(endpoints), config.port)
|
||||
|
||||
const shutdown = (): void => {
|
||||
server.close(() => {
|
||||
process.exit()
|
||||
})
|
||||
}
|
||||
|
||||
process.on('SIGINT', shutdown)
|
||||
process.on('SIGTERM', shutdown)
|
||||
process.on('uncaughtException', (e) => {
|
||||
console.error(e)
|
||||
})
|
||||
process.on('unhandledRejection', (e) => {
|
||||
console.error(e)
|
||||
})
|
||||
}
|
69
services/mail/pod-mail/src/server.ts
Normal file
69
services/mail/pod-mail/src/server.ts
Normal file
@ -0,0 +1,69 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-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.
|
||||
//
|
||||
|
||||
import cors from 'cors'
|
||||
import express, { Express, NextFunction, Request, Response } from 'express'
|
||||
import { Server } from 'http'
|
||||
|
||||
import { Endpoint, RequestHandler } from './types'
|
||||
import { ApiError } from './error'
|
||||
|
||||
const catchError = (fn: RequestHandler) => (req: Request, res: Response, next: NextFunction) => {
|
||||
void (async () => {
|
||||
try {
|
||||
await fn(req, res, next)
|
||||
} catch (err: unknown) {
|
||||
next(err)
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
||||
export function createServer (endpoints: Endpoint[]): Express {
|
||||
const app = express()
|
||||
|
||||
app.use(cors())
|
||||
app.use(express.json())
|
||||
|
||||
endpoints.forEach((endpoint) => {
|
||||
if (endpoint.type === 'get') {
|
||||
app.get(endpoint.endpoint, catchError(endpoint.handler))
|
||||
} else if (endpoint.type === 'post') {
|
||||
app.post(endpoint.endpoint, catchError(endpoint.handler))
|
||||
}
|
||||
})
|
||||
|
||||
app.use((_req, res, _next) => {
|
||||
res.status(404).send({ message: 'Not found' })
|
||||
})
|
||||
|
||||
app.use((err: any, _req: any, res: any, _next: any) => {
|
||||
if (err instanceof ApiError) {
|
||||
res.status(400).send({ code: err.code, message: err.message })
|
||||
return
|
||||
}
|
||||
|
||||
res.status(500).send({ message: err.message })
|
||||
})
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
export function listen (e: Express, port: number, host?: string): Server {
|
||||
const cb = (): void => {
|
||||
console.log(`Mail service has been started at ${host ?? '*'}:${port}`)
|
||||
}
|
||||
|
||||
return host !== undefined ? e.listen(port, host, cb) : e.listen(port, cb)
|
||||
}
|
57
services/mail/pod-mail/src/transport.ts
Normal file
57
services/mail/pod-mail/src/transport.ts
Normal file
@ -0,0 +1,57 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-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.
|
||||
//
|
||||
import nodemailer, { type Transporter } from 'nodemailer'
|
||||
import aws from '@aws-sdk/client-ses'
|
||||
|
||||
import type { Config, SmtpConfig, SesConfig } from './config'
|
||||
|
||||
function smtp (config: SmtpConfig): Transporter {
|
||||
const auth =
|
||||
config.Username !== undefined && config.Password !== undefined
|
||||
? {
|
||||
user: config.Username,
|
||||
pass: config.Password
|
||||
}
|
||||
: undefined
|
||||
return nodemailer.createTransport({
|
||||
host: config.Host,
|
||||
port: config.Port,
|
||||
auth
|
||||
})
|
||||
}
|
||||
|
||||
function ses (config: SesConfig): Transporter {
|
||||
const ses = new aws.SES({
|
||||
region: config.Region,
|
||||
credentials: {
|
||||
accessKeyId: config.AccessKey,
|
||||
secretAccessKey: config.SecretKey
|
||||
}
|
||||
})
|
||||
|
||||
return nodemailer.createTransport({
|
||||
SES: { ses, aws }
|
||||
})
|
||||
}
|
||||
|
||||
export function getTransport (config: Config): Transporter {
|
||||
if (config.smtpConfig !== undefined) {
|
||||
return smtp(config.smtpConfig)
|
||||
}
|
||||
if (config.sesConfig !== undefined) {
|
||||
return ses(config.sesConfig)
|
||||
}
|
||||
throw new Error('No transport protocol is configured')
|
||||
}
|
38
services/mail/pod-mail/src/types.ts
Normal file
38
services/mail/pod-mail/src/types.ts
Normal file
@ -0,0 +1,38 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-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.
|
||||
//
|
||||
|
||||
import { NextFunction, Request, Response } from 'express'
|
||||
|
||||
export interface Receivers {
|
||||
to: string[]
|
||||
cc?: string[]
|
||||
bcc?: string[]
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
text: string
|
||||
subject: string
|
||||
html?: string
|
||||
}
|
||||
|
||||
export type RequestType = 'get' | 'post'
|
||||
|
||||
export type RequestHandler = (req: Request, res: Response, next?: NextFunction) => Promise<void>
|
||||
|
||||
export interface Endpoint {
|
||||
endpoint: string
|
||||
type: RequestType
|
||||
handler: RequestHandler
|
||||
}
|
10
services/mail/pod-mail/tsconfig.json
Normal file
10
services/mail/pod-mail/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"declarationDir": "./types",
|
||||
"tsBuildInfoFile": ".build/build.tsbuildinfo"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user