diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 32bd65e..cdf71d7 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -17,7 +17,6 @@ jobs:
GOOGLE_USER_EMAIL: ${{ secrets.GOOGLE_USER_EMAIL }}
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
- GOOGLE_REDIRECT_URI: ${{ secrets.GOOGLE_REDIRECT_URI }}
GOOGLE_REFRESH_TOKEN: ${{ secrets.GOOGLE_REFRESH_TOKEN }}
steps:
- name: Checkout the repository
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 8f9bb6a..e1a04f8 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -45,7 +45,6 @@ jobs:
GOOGLE_USER_EMAIL: ${{ secrets.GOOGLE_USER_EMAIL }}
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
- GOOGLE_REDIRECT_URI: ${{ secrets.GOOGLE_REDIRECT_URI }}
GOOGLE_REFRESH_TOKEN: ${{ secrets.GOOGLE_REFRESH_TOKEN }}
steps:
diff --git a/CLAUDE.md b/CLAUDE.md
index 7df72ae..227e380 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -15,7 +15,6 @@ All source code lives in the `/app` directory. Run all npm commands from `/app`.
GOOGLE_USER_EMAIL
GOOGLE_CLIENT_ID
GOOGLE_CLIENT_SECRET
-GOOGLE_REDIRECT_URI # https://developers.google.com/oauthplayground
GOOGLE_REFRESH_TOKEN
```
@@ -63,27 +62,26 @@ EmailTransport (lib/email/transport.ts)
└── EmailSender (lib/email/sender.ts)
```
-- **`GmailOAuthClient`** (`lib/google/oauth2client.ts`) — Manages Google OAuth2 credentials; validates env vars via Zod schema on instantiation. Accepts optional constructor params; falls back to env vars.
-- **`EmailTransport`** — Base class; initializes Nodemailer transporter via `createTransport3LO()`, which fetches a fresh access token from `GmailOAuthClient` if one hasn't been pre-generated.
+- **`EmailTransport`** — Base class; initializes Nodemailer transporter via `createTransport3LO(options?)`. Reads Google OAuth2 credentials from `options` fields, falling back to `GOOGLE_*` env vars. Nodemailer handles token refresh internally via the `refreshToken` field.
- **`EmailSender`** — Extends `EmailTransport`; exposes `sendEmail()` method; validates email params with Zod schema. Supports a single `recipient` string or a `recipients[]` array (max 20 total).
- **`SchemaValidator`** (`lib/validator/schemavalidator.ts`) — Generic Zod validation wrapper; handles both `ZodObject` and `ZodEffects` (`.refine()`) schemas; supports partial validation via `pick`.
### Key Data Flow
-1. **Library usage**: `send()` (`lib/email/send.ts`) → creates `GmailOAuthClient` + `EmailSender` → `createTransport3LO()` → `sendEmail()`
+1. **Library usage**: `send(params, oauth2?)` (`lib/email/send.ts`) → creates `EmailSender` → `createTransport3LO(oauth2?)` → `sendEmail()`
2. **CLI text**: Commander.js (`scripts/cli/send.ts`) → `handleSendTextEmail` → `send()`
3. **CLI HTML**: Commander.js → `handleSendHtmlEmail` → `buildHtml()` (renders EJS template, sanitizes HTML) → `send()` with `isHtml: true`
4. **SEA builds**: `build.ts` checks `IS_BUILD_SEA=true` to import the EJS template via `import()` (baked into the binary) rather than reading it from disk.
### Public API (`src/index.ts`)
-Exports: `send`, `buildHtml`, `EmailSender`, `EmailTransport`, `GmailOAuthClient`, `SchemaValidator`, plus all types from `src/types/`.
+Exports: `send`, `buildHtml`, `EmailSender`, `EmailTransport`, `SchemaValidator`, plus all types from `src/types/`.
### Validation
All input validation uses Zod schemas in `src/types/`:
- `email.schema.ts` — `EmailSchema` for `send()` params; `HtmlBuildSchema` for `buildHtml()` params; `EmailTextOptions` / `EmailHtmlOptions` interfaces for CLI handlers
-- `oauth2client.schema.ts` — Google OAuth2 env vars
+- `transport.schema.ts` — `TransportOath2Schema` / `TransportOath2SchemaType` for optional OAuth2 credentials passed to `send()` and `createTransport3LO()`
### ESM Compatibility
diff --git a/README.md b/README.md
index 3ac4179..fb01d51 100644
--- a/README.md
+++ b/README.md
@@ -133,7 +133,6 @@ We welcome contributions! Please see [CONTRIBUTING.md](/CONTRIBUTING.md) and the
| GOOGLE_USER_EMAIL | Your Google email that you've configured for Gmail SMTP and Google OAuth2. |
| GOOGLE_CLIENT_ID | Google OAuth2 client ID linked with your Google Cloud Platform project. |
| GOOGLE_CLIENT_SECRET | Google OAuth2 client secret associated with the `GOOGLE_CLIENT_ID`. |
- | GOOGLE_REDIRECT_URI | Allowed Google API redirect URI. Its value is `https://developers.google.com/oauthplayground` by default. |
| GOOGLE_REFRESH_TOKEN | The initial (or any) refresh token obtained from the [OAuthPlayground](https://developers.google.com/oauthplayground).
- Read on [Using the OAuth 2.0 Playground](https://github.com/weaponsforge/email-sender?tab=readme-ov-file#using-the-oauth-20-playground) for more information about generating a refresh token using the Google OAuth Playground.
_(⚠️ **INFO:** This is an older note; some steps may vary this 2025)_
|
diff --git a/app/.env.example b/app/.env.example
index da024e0..52eb6a3 100644
--- a/app/.env.example
+++ b/app/.env.example
@@ -1,5 +1,4 @@
GOOGLE_USER_EMAIL=YOUR_GMAIL_OAUTH_EMAIL_HERE
GOOGLE_CLIENT_ID=YOUR_GOOGLE_PROJECT_CLIENT_ID_HERE
GOOGLE_CLIENT_SECRET=YOUR_GOOGLE_PROJECT_CLIENT_SECRET_HERE
-GOOGLE_REDIRECT_URI=https://developers.google.com/oauthplayground
GOOGLE_REFRESH_TOKEN=ANY_REFRESH_TOKEN_GENERATED_FROM_OAUTHPLAYGROUND_HERE
\ No newline at end of file
diff --git a/app/package-lock.json b/app/package-lock.json
index 4ceb334..d94c363 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -1,18 +1,17 @@
{
"name": "@weaponsforge/sendemail",
- "version": "1.2.7",
+ "version": "2.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@weaponsforge/sendemail",
- "version": "1.2.7",
+ "version": "2.0.0",
"license": "MIT",
"dependencies": {
"commander": "^14.0.3",
"dotenv": "^17.4.2",
"ejs": "^5.0.2",
- "googleapis": "^171.4.0",
"nodemailer": "^8.0.5",
"sanitize-html": "^2.17.3",
"zod": "^3.24.2"
@@ -1061,6 +1060,29 @@
"node": ">=14.0.0"
}
},
+ "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
+ "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.1",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
+ "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz",
@@ -1639,15 +1661,6 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
- "node_modules/agent-base": {
- "version": "7.1.4",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
- "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 14"
- }
- },
"node_modules/ajv": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
@@ -1721,35 +1734,6 @@
"node": "18 || 20 || >=22"
}
},
- "node_modules/base64-js": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
- "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT"
- },
- "node_modules/bignumber.js": {
- "version": "9.3.1",
- "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
- "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
- "license": "MIT",
- "engines": {
- "node": "*"
- }
- },
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -1789,41 +1773,6 @@
"node": ">=8"
}
},
- "node_modules/buffer-equal-constant-time": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
- "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
- "license": "BSD-3-Clause"
- },
- "node_modules/call-bind-apply-helpers": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
- "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/call-bound": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
- "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.2",
- "get-intrinsic": "^1.3.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/chai": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
@@ -1890,19 +1839,11 @@
"node": ">= 8"
}
},
- "node_modules/data-uri-to-buffer": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
- "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
- "license": "MIT",
- "engines": {
- "node": ">= 12"
- }
- },
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -2034,29 +1975,6 @@
"url": "https://dotenvx.com"
}
},
- "node_modules/dunder-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
- "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.1",
- "es-errors": "^1.3.0",
- "gopd": "^1.2.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/ecdsa-sig-formatter": {
- "version": "1.0.11",
- "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
- "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "safe-buffer": "^5.0.1"
- }
- },
"node_modules/ejs": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-5.0.2.tgz",
@@ -2081,24 +1999,6 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
- "node_modules/es-define-property": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
- "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-errors": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
- "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
"node_modules/es-module-lexer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
@@ -2106,18 +2006,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/es-object-atoms": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
- "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
"node_modules/esbuild": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
@@ -2386,12 +2274,6 @@
"node": ">=12.0.0"
}
},
- "node_modules/extend": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
- "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
- "license": "MIT"
- },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -2440,29 +2322,6 @@
"reusify": "^1.0.4"
}
},
- "node_modules/fetch-blob": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
- "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/jimmywarting"
- },
- {
- "type": "paypal",
- "url": "https://paypal.me/jimmywarting"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "node-domexception": "^1.0.0",
- "web-streams-polyfill": "^3.0.3"
- },
- "engines": {
- "node": "^12.20 || >= 14.13"
- }
- },
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
@@ -2550,18 +2409,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/formdata-polyfill": {
- "version": "4.0.10",
- "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
- "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
- "license": "MIT",
- "dependencies": {
- "fetch-blob": "^3.1.2"
- },
- "engines": {
- "node": ">=12.20.0"
- }
- },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -2577,80 +2424,6 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
- "node_modules/function-bind": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
- "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/gaxios": {
- "version": "7.1.4",
- "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz",
- "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==",
- "license": "Apache-2.0",
- "dependencies": {
- "extend": "^3.0.2",
- "https-proxy-agent": "^7.0.1",
- "node-fetch": "^3.3.2"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/gcp-metadata": {
- "version": "8.1.2",
- "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz",
- "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==",
- "license": "Apache-2.0",
- "dependencies": {
- "gaxios": "^7.0.0",
- "google-logging-utils": "^1.0.0",
- "json-bigint": "^1.0.0"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/get-intrinsic": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
- "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.2",
- "es-define-property": "^1.0.1",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.1.1",
- "function-bind": "^1.1.2",
- "get-proto": "^1.0.1",
- "gopd": "^1.2.0",
- "has-symbols": "^1.1.0",
- "hasown": "^2.0.2",
- "math-intrinsics": "^1.1.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/get-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
- "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
- "license": "MIT",
- "dependencies": {
- "dunder-proto": "^1.0.1",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
"node_modules/get-stream": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
@@ -2728,73 +2501,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/google-auth-library": {
- "version": "10.6.2",
- "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz",
- "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==",
- "license": "Apache-2.0",
- "dependencies": {
- "base64-js": "^1.3.0",
- "ecdsa-sig-formatter": "^1.0.11",
- "gaxios": "^7.1.4",
- "gcp-metadata": "8.1.2",
- "google-logging-utils": "1.1.3",
- "jws": "^4.0.0"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/google-logging-utils": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz",
- "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/googleapis": {
- "version": "171.4.0",
- "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-171.4.0.tgz",
- "integrity": "sha512-xybFL2SmmUgIifgsbsRQYRdNrSAYwxWZDmkZTGjUIaRnX5jPqR8el/cEvo6rCqh7iaZx6MfEPS/lrDgZ0bymkg==",
- "license": "Apache-2.0",
- "dependencies": {
- "google-auth-library": "^10.2.0",
- "googleapis-common": "^8.0.0"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/googleapis-common": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-8.0.1.tgz",
- "integrity": "sha512-eCzNACUXPb1PW5l0ULTzMHaL/ltPRADoPgjBlT8jWsTbxkCp6siv+qKJ/1ldaybCthGwsYFYallF7u9AkU4L+A==",
- "license": "Apache-2.0",
- "dependencies": {
- "extend": "^3.0.2",
- "gaxios": "^7.0.0-rc.4",
- "google-auth-library": "^10.1.0",
- "qs": "^6.7.0",
- "url-template": "^2.0.8"
- },
- "engines": {
- "node": ">=18.0.0"
- }
- },
- "node_modules/gopd": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
- "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -2805,30 +2511,6 @@
"node": ">=8"
}
},
- "node_modules/has-symbols": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
- "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/hasown": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
- "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
- "license": "MIT",
- "dependencies": {
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
"node_modules/html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
@@ -2855,19 +2537,6 @@
"entities": "^7.0.1"
}
},
- "node_modules/https-proxy-agent": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
- "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
- "license": "MIT",
- "dependencies": {
- "agent-base": "^7.1.2",
- "debug": "4"
- },
- "engines": {
- "node": ">= 14"
- }
- },
"node_modules/human-signals": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz",
@@ -3045,15 +2714,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/json-bigint": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
- "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
- "license": "MIT",
- "dependencies": {
- "bignumber.js": "^9.0.0"
- }
- },
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@@ -3075,27 +2735,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/jwa": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
- "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
- "license": "MIT",
- "dependencies": {
- "buffer-equal-constant-time": "^1.0.1",
- "ecdsa-sig-formatter": "1.0.11",
- "safe-buffer": "^5.0.1"
- }
- },
- "node_modules/jws": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
- "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
- "license": "MIT",
- "dependencies": {
- "jwa": "^2.0.1",
- "safe-buffer": "^5.0.1"
- }
- },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -3435,15 +3074,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/math-intrinsics": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
- "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -3498,6 +3128,7 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
"license": "MIT"
},
"node_modules/mylas": {
@@ -3539,44 +3170,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/node-domexception": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
- "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
- "deprecated": "Use your platform's native DOMException instead",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/jimmywarting"
- },
- {
- "type": "github",
- "url": "https://paypal.me/jimmywarting"
- }
- ],
- "license": "MIT",
- "engines": {
- "node": ">=10.5.0"
- }
- },
- "node_modules/node-fetch": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
- "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
- "license": "MIT",
- "dependencies": {
- "data-uri-to-buffer": "^4.0.0",
- "fetch-blob": "^3.1.4",
- "formdata-polyfill": "^4.0.10"
- },
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/node-fetch"
- }
- },
"node_modules/nodemailer": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz",
@@ -3626,18 +3219,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/object-inspect": {
- "version": "1.13.4",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
- "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/obug": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
@@ -3877,21 +3458,6 @@
"node": ">=6"
}
},
- "node_modules/qs": {
- "version": "6.15.0",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
- "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "side-channel": "^1.1.0"
- },
- "engines": {
- "node": ">=0.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/queue-lit": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.2.tgz",
@@ -4015,26 +3581,6 @@
"queue-microtask": "^1.2.2"
}
},
- "node_modules/safe-buffer": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
- "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT"
- },
"node_modules/sanitize-html": {
"version": "2.17.3",
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.3.tgz",
@@ -4085,78 +3631,6 @@
"node": ">=8"
}
},
- "node_modules/side-channel": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
- "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "object-inspect": "^1.13.3",
- "side-channel-list": "^1.0.0",
- "side-channel-map": "^1.0.1",
- "side-channel-weakmap": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/side-channel-list": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
- "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "object-inspect": "^1.13.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/side-channel-map": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
- "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.5",
- "object-inspect": "^1.13.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/side-channel-weakmap": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
- "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.5",
- "object-inspect": "^1.13.3",
- "side-channel-map": "^1.0.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/siginfo": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
@@ -4989,12 +4463,6 @@
"punycode": "^2.1.0"
}
},
- "node_modules/url-template": {
- "version": "2.0.8",
- "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
- "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==",
- "license": "BSD"
- },
"node_modules/vite": {
"version": "8.0.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz",
@@ -5191,15 +4659,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
- "node_modules/web-streams-polyfill": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
- "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
- "license": "MIT",
- "engines": {
- "node": ">= 8"
- }
- },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
diff --git a/app/package.json b/app/package.json
index 8b553c5..f1a5cca 100644
--- a/app/package.json
+++ b/app/package.json
@@ -1,6 +1,6 @@
{
"name": "@weaponsforge/sendemail",
- "version": "1.2.7",
+ "version": "2.0.0",
"description": "Sends emails using Gmail SMTP with username/pw or Google OAuth2",
"main": "dist/index.js",
"types": "./dist/index.d.ts",
@@ -68,7 +68,6 @@
"commander": "^14.0.3",
"dotenv": "^17.4.2",
"ejs": "^5.0.2",
- "googleapis": "^171.4.0",
"nodemailer": "^8.0.5",
"sanitize-html": "^2.17.3",
"zod": "^3.24.2"
diff --git a/app/src/__tests__/oauth2client.test.ts b/app/src/__tests__/oauth2client.test.ts
deleted file mode 100644
index 5a7316a..0000000
--- a/app/src/__tests__/oauth2client.test.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { z } from 'zod'
-import { describe, expect, it } from 'vitest'
-import { GmailOAuthClient } from '@/lib/index.js'
-import type { GetAccessTokenResponse } from '@/types/oauth2client.types.js'
-
-describe('Google OAuth2 Client class test', () => {
- it('should generate an access token', async () => {
- const oauthClient = new GmailOAuthClient()
- const token = await oauthClient.getAccessToken()
-
- expect(token).toHaveProperty('token')
- expect(token).toHaveProperty('res')
- })
-
- it('should generate and store a new access token', async () => {
- const oauthClient = new GmailOAuthClient()
- await oauthClient.generateAccessToken()
-
- expect(oauthClient.accessToken).toHaveProperty('token')
- expect(oauthClient.accessToken).toHaveProperty('res')
- })
-
- it ('should generate an access token using class constructor parameters', async () => {
- const oauthClient = new GmailOAuthClient({
- clientId: process.env.GOOGLE_CLIENT_ID,
- clientSecret: process.env.GOOGLE_CLIENT_SECRET,
- redirectURI: process.env.GOOGLE_REDIRECT_URI,
- refreshToken: process.env.GOOGLE_REFRESH_TOKEN,
- userEmail: process.env.GOOGLE_USER_EMAIL,
- })
-
- const token = await oauthClient.getAccessToken()
-
- expect(token).toHaveProperty('token')
- expect(token).toHaveProperty('res')
- })
-
- it ('should throw an error if incorrect schema is provided', async () => {
- // See @/types/oauth2client.schema.ts for the correct schema
- const wrongSchema = z.object({
- hello: z.string(),
- world: z.number(),
- })
-
- expect(() => new GmailOAuthClient(null, wrongSchema)).toThrow()
- })
-
- it ('should throw an error when manually setting an incorrect access token', async () => {
- const oauthClient = new GmailOAuthClient()
-
- const accessToken = {
- token: 123, // Expected to be a string
- res: 'hello', // Expected to be an object
- } as unknown as GetAccessTokenResponse
-
- expect(() =>
- oauthClient.accessToken = accessToken,
- ).toThrow()
- })
-})
diff --git a/app/src/__tests__/send.test.ts b/app/src/__tests__/send.test.ts
index 17b67b4..780502f 100644
--- a/app/src/__tests__/send.test.ts
+++ b/app/src/__tests__/send.test.ts
@@ -1,20 +1,29 @@
-import { beforeAll, describe, expect, test } from 'vitest'
+import { describe, expect, test } from 'vitest'
import { send } from '@/lib/index.js'
-import { GmailOAuthClient } from '@/lib/index.js'
const MAX_TIMEOUT = 10000
const TEST_RECIPIENT = 'tester@gmail.com'
describe('Send email test', () => {
- const oauthClient = new GmailOAuthClient()
+ test('Accept OAuth2 settings from parameters', async () => {
+ const auth = {
+ googleUserEmail: process.env.GOOGLE_USER_EMAIL || '',
+ googleClientId: process.env.GOOGLE_CLIENT_ID || '',
+ googleClientSecret: process.env.GOOGLE_CLIENT_SECRET || '',
+ googleRereshToken: process.env.GOOGLE_REFRESH_TOKEN || '',
+ }
- // Generates an access token to use for the succeeding tests
- beforeAll(async () => {
- await oauthClient.generateAccessToken()
- })
+ await expect(
+ send({
+ recipient: TEST_RECIPIENT,
+ subject: 'Test Simple Message',
+ content: 'Henlo!',
+ }, auth),
+ ).resolves.toBeUndefined()
+ }, MAX_TIMEOUT)
+ // Ensure .env file contains the required GOOGLE_* variables before running tests below
test('Send a text email', async () => {
- // Sending email this way auto-generates a fresh access token
await expect(send({
recipient: TEST_RECIPIENT,
subject: 'Test Simple Message',
@@ -22,42 +31,24 @@ describe('Send email test', () => {
})).resolves.toBeUndefined()
}, MAX_TIMEOUT)
- test('Send a text email using pre-generated OAuth2 access token', async () => {
- // Sending email this way does not regenerate an access token
- await expect(
- send(
- {
- recipient: TEST_RECIPIENT,
- subject: 'Test Simple Message',
- content: 'Henlo!',
- }, oauthClient,
- ),
- ).resolves.toBeUndefined()
- }, MAX_TIMEOUT)
-
test('Send a text email to multiple recipients[]', async () => {
await expect(
- send(
- {
- recipients: ['student1@gmail.com', 'student2@gmail.com'],
- subject: 'Test Multiple Message',
- content: 'Henlo, hello!',
- }, oauthClient,
- ),
+ send({
+ recipients: ['student1@gmail.com', 'student2@gmail.com'],
+ subject: 'Test Multiple Message',
+ content: 'Henlo, hello!',
+ }),
).resolves.toBeUndefined()
}, MAX_TIMEOUT)
- test('should send email to both recipient and recipients[]', async () => {
+ test('should send email to recipient and recipients[]', async () => {
await expect(
- send(
- {
- recipients: ['person1@gmail.com', 'person2@gmail.com'],
- recipient: TEST_RECIPIENT,
- subject: 'Test Multiple Message 2',
- content: 'Hello there',
- },
- oauthClient,
- ),
+ send({
+ recipients: ['person1@gmail.com', 'person2@gmail.com'],
+ recipient: TEST_RECIPIENT,
+ subject: 'Test Multiple Message 2',
+ content: 'Hello there',
+ }),
).resolves.toBeUndefined()
}, MAX_TIMEOUT)
})
diff --git a/app/src/__tests__/sendFormat.test.ts b/app/src/__tests__/sendFormat.test.ts
index 2065c38..1f96e6f 100644
--- a/app/src/__tests__/sendFormat.test.ts
+++ b/app/src/__tests__/sendFormat.test.ts
@@ -1,6 +1,6 @@
-import { beforeAll, describe, expect, it } from 'vitest'
+import { describe, expect, it } from 'vitest'
-import { GmailOAuthClient, send } from '@/lib/index.js'
+import { send } from '@/lib/index.js'
import { createLongString, createRandomTextArray } from '@/utils/helpers.js'
import { EmailSchemaMessages } from '@/types/email.schema.js'
@@ -10,103 +10,72 @@ const TEST_RECIPIENT = 'tester@gmail.com'
const TEST_SUBJECT = 'Test Simple Message'
describe('Email format test', () => {
- const oauthClient = new GmailOAuthClient()
-
- // Generates an access token to use for the succeeding tests
- beforeAll(async () => {
- await oauthClient.generateAccessToken()
- })
-
- // Testing email formats
it('should reject invalid email address format', async () => {
const invalidEmail = 'tester!#5@.4/'
await expect(
- send(
- {
- recipient: invalidEmail,
- subject: TEST_SUBJECT,
- content: 'Henlo!',
- },
- oauthClient,
- ),
+ send({
+ recipient: invalidEmail,
+ subject: TEST_SUBJECT,
+ content: 'Henlo!',
+ }),
).rejects.toThrow(EmailSchemaMessages.RECIPIENT_EMAIL)
}, MAX_TIMEOUT)
- // Testing long email addresses
it('should reject email addresses longer than 150 characters', async () => {
const longRecipientEmail = createLongString(150) + '@gmail.com'
await expect(
- send(
- {
- recipient: longRecipientEmail,
- subject: TEST_SUBJECT,
- content: 'Henlo!',
- },
- oauthClient,
- ),
+ send({
+ recipient: longRecipientEmail,
+ subject: TEST_SUBJECT,
+ content: 'Henlo!',
+ }),
).rejects.toThrow(EmailSchemaMessages.RECIPIENT_EMAIL_LENGTH)
}, MAX_TIMEOUT)
- // Testing long subject/title content
it('should reject subject/title longer that 200 characters', async () => {
const longTitle = createLongString(201)
await expect(
- send(
- {
- recipient: TEST_RECIPIENT,
- subject: longTitle,
- content: 'Henlo!',
- },
- oauthClient,
- ),
+ send({
+ recipient: TEST_RECIPIENT,
+ subject: longTitle,
+ content: 'Henlo!',
+ }),
).rejects.toThrow(EmailSchemaMessages.SUBJECT)
}, MAX_TIMEOUT)
- // Testing long email message content
it('should reject email message content longer than 1500 characters', async () => {
const longContent = createLongString(1501)
await expect(
- send(
- {
- recipient: TEST_RECIPIENT,
- subject: TEST_SUBJECT,
- content: longContent,
- },
- oauthClient,
- ),
+ send({
+ recipient: TEST_RECIPIENT,
+ subject: TEST_SUBJECT,
+ content: longContent,
+ }),
).rejects.toThrow(EmailSchemaMessages.CONTENT)
}, MAX_TIMEOUT)
- // Testing missing recipient AND recipients[]
it('should reject if recipient and recipients[] are missing', async () => {
await expect(
- send(
- {
- subject: TEST_SUBJECT,
- content: 'Hello there',
- },
- oauthClient,
- ),
+ send({
+ subject: TEST_SUBJECT,
+ content: 'Hello there',
+ }),
).rejects.toThrow(EmailSchemaMessages.RECIPIENT_REQUIRED)
}, MAX_TIMEOUT)
- // Test maximum 20 recipients
it('should reject if recipients[] are more than 20', async () => {
const emailList = createRandomTextArray({ length: 21, suffix: '@gmail.com' })
await expect(
- send(
- {
- recipients: emailList,
- subject: TEST_SUBJECT,
- content: 'Hello there',
- },
- oauthClient,
- ),
+ send({
+ recipients: emailList,
+ subject: TEST_SUBJECT,
+ content: 'Hello there',
+ }),
).rejects.toThrow(EmailSchemaMessages.RECIPIENT_EMAIL_MAX)
}, MAX_TIMEOUT)
@@ -114,15 +83,12 @@ describe('Email format test', () => {
const emailList = createRandomTextArray({ length: 20, suffix: '@gmail.com' })
await expect(
- send(
- {
- recipients: emailList,
- recipient: TEST_RECIPIENT,
- subject: TEST_SUBJECT,
- content: 'Hello there',
- },
- oauthClient,
- ),
+ send({
+ recipients: emailList,
+ recipient: TEST_RECIPIENT,
+ subject: TEST_SUBJECT,
+ content: 'Hello there',
+ }),
).rejects.toThrow(EmailSchemaMessages.RECIPIENT_EMAIL_MAX)
}, MAX_TIMEOUT)
})
diff --git a/app/src/index.ts b/app/src/index.ts
index de0bd2d..942ce8e 100644
--- a/app/src/index.ts
+++ b/app/src/index.ts
@@ -1,7 +1,6 @@
export {
EmailSender,
EmailTransport,
- GmailOAuthClient,
SchemaValidator,
buildHtml,
send,
diff --git a/app/src/lib/email/send.ts b/app/src/lib/email/send.ts
index 10ee793..53809c2 100644
--- a/app/src/lib/email/send.ts
+++ b/app/src/lib/email/send.ts
@@ -1,15 +1,18 @@
import EmailSender from '@/lib/email/sender.js'
-import GmailOAuthClient from '@/lib/google/oauth2client.js'
import { TRANSPORT_AUTH_TYPES, TRANSPORT_SMTP_HOSTS } from '@/types/transport.types.js'
import type { EmailType } from '@/types/email.schema.js'
+import type { TransportOath2SchemaType } from '@/types/transport.schema.js'
/**
* Sends an email to a recipient (text or HTML)
* @param {EmailType} params Email sending input parameters
+ * @param {oauth2} options Optional Google OAuth2 settings. Uses the `GOOGLE_*` env variables by default.
*/
-export const send = async (params: EmailType, client?: GmailOAuthClient): Promise => {
- const oauthClient = client || new GmailOAuthClient()
+export const send = async (
+ params: EmailType,
+ oauth2?: TransportOath2SchemaType,
+): Promise => {
const { recipient, recipients, subject, content, isHtml = false } = params
const handler = new EmailSender({
@@ -18,7 +21,7 @@ export const send = async (params: EmailType, client?: GmailOAuthClient): Promis
})
try {
- await handler.createTransport3LO(oauthClient)
+ await handler.createTransport3LO(oauth2)
} catch (err: unknown) {
if (err instanceof Error) {
throw err
diff --git a/app/src/lib/email/transport.ts b/app/src/lib/email/transport.ts
index 956a6ca..25b7e46 100644
--- a/app/src/lib/email/transport.ts
+++ b/app/src/lib/email/transport.ts
@@ -1,6 +1,7 @@
import nodemailer from 'nodemailer'
-import GmailOAuthClient from '@/lib/google/oauth2client.js'
+import SchemaValidator from '@/lib/validator/schemavalidator.js'
+import { TransportOath2Schema, type TransportOath2SchemaType } from '@/types/transport.schema.js'
import { TRANSPORT_SMTP_HOSTS, TRANSPORT_AUTH_TYPES } from '@/types/transport.types.js'
import type { IEmailTransportAuth, SMTPTransport } from '@/types/transport.types.js'
import type { IEmailTransport } from '@/types/transport.interface.js'
@@ -11,6 +12,8 @@ import type { IEmailTransport } from '@/types/transport.interface.js'
* and initiates sending emails using the Gmail SMTP
*/
class EmailTransport implements IEmailTransport {
+ /** Zod schema wrapper methods and functions */
+ #schema: SchemaValidator | null = null
/** Nodemailer tansport */
#transporter: nodemailer.Transporter | null = null
@@ -27,29 +30,30 @@ class EmailTransport implements IEmailTransport {
constructor (params?: IEmailTransportAuth) {
this.#host = params?.host || TRANSPORT_SMTP_HOSTS.GMAIL
this.#type = params?.type || TRANSPORT_AUTH_TYPES.OAUTH2
+ this.#schema = new SchemaValidator(TransportOath2Schema)
}
- async createTransport3LO (oauth2Client: GmailOAuthClient): Promise {
+ async createTransport3LO (options?: TransportOath2SchemaType): Promise {
try {
- let token = oauth2Client.accessToken
-
- // Generate and retrieve a fresh access token
- if (!token) {
- token = await oauth2Client.getAccessToken()
+ const inputData = {
+ googleUserEmail: options?.googleUserEmail || process.env.GOOGLE_USER_EMAIL,
+ googleClientId: options?.googleClientId || process.env.GOOGLE_CLIENT_ID,
+ googleClientSecret: options?.googleClientSecret || process.env.GOOGLE_CLIENT_SECRET,
+ googleRereshToken: options?.googleRereshToken || process.env.GOOGLE_REFRESH_TOKEN,
}
- // Initialize the nodemailer transport with a fresh access token
+ this.#schema?.validate({ data: inputData })
+
this.#transporter = nodemailer.createTransport({
host: this.#host,
port: 465,
secure: true,
auth: {
type: this.#type,
- user: oauth2Client.email,
- clientId: oauth2Client.client?._clientId,
- clientSecret: oauth2Client.client?._clientSecret,
- refreshToken: oauth2Client?.refreshToken,
- accessToken: token,
+ user: inputData.googleUserEmail,
+ clientId: inputData.googleClientId,
+ clientSecret: inputData.googleClientSecret,
+ refreshToken: inputData.googleRereshToken,
},
})
} catch (err: unknown) {
diff --git a/app/src/lib/google/oauth2client.ts b/app/src/lib/google/oauth2client.ts
deleted file mode 100644
index 9e7deee..0000000
--- a/app/src/lib/google/oauth2client.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-import { google } from 'googleapis'
-import dotenv from 'dotenv'
-dotenv.config({ quiet: true })
-
-import SchemaValidator from '@/lib/validator/schemavalidator.js'
-import type { IGmailOAuthClient } from '@/types/oauth2client.interface.js'
-import type { ZodObjectBasicType } from '@/types/schemavalidator.interface.js'
-import { GmailOAuthClientSchema } from '@/types/oauth2client.schema.js'
-
-import {
- type GetAccessTokenResponse,
- type IOauthClient,
- type OAuth2Client,
-} from '@/types/oauth2client.types.js'
-
-/**
- * @class GmailOAuthClient
- * @description Manages relevant API keys, methods and properties of the Google `OAuth2Client`
- */
-class GmailOAuthClient implements IGmailOAuthClient {
- /** Local instance of the `OAuth2Client` */
- #client: OAuth2Client | null = null
-
- /** Email sender associated with the API keys of the `OAuth2Client` */
- #email: string | null = null
-
- /** Google OAuth2 refresh token from the Google OAuth2 Playground */
- #refreshToken: string | null = null
-
- /** Google OAuth2 access token generated using the #refreshToken */
- #accessToken: GetAccessTokenResponse | null = null
-
- /** Zod schema wrapper methods and functions */
- #schema: SchemaValidator | null = null
-
- /**
- * @constructor
- * @param {Partial} params (Optional) constructor parameters. The corresponding `.env` variables
- * expect to have correct values for the default values.
- * @param {ZodObjectBasicType} [schema] (Optional) zod Schema. Defaults to the `ZodSchemaTypes` if not provided.
- */
- constructor (params?: Partial | null, schema?: ZodObjectBasicType) {
- const clientId = params?.clientId || process.env.GOOGLE_CLIENT_ID
- const clientSecret = params?.clientSecret || process.env.GOOGLE_CLIENT_SECRET
- const redirectURI = params?.redirectURI || process.env.GOOGLE_REDIRECT_URI
- const refreshToken = params?.refreshToken || process.env.GOOGLE_REFRESH_TOKEN
- const userEmail = params?.userEmail || process.env.GOOGLE_USER_EMAIL
-
- this.#schema = new SchemaValidator(schema || GmailOAuthClientSchema)
-
- this.init({
- clientId,
- clientSecret,
- redirectURI,
- refreshToken,
- userEmail,
- })
- }
-
- init (params: IOauthClient): void {
- try {
- this.#schema?.validate({ data: { ...params } })
-
- const { clientId, clientSecret, redirectURI, refreshToken, userEmail } = params
-
- this.#client = new google.auth.OAuth2(
- clientId,
- clientSecret,
- redirectURI,
- )
-
- this.#client.setCredentials({ refresh_token: refreshToken })
-
- this.#email = userEmail
- this.#refreshToken = refreshToken
- } catch (err: unknown) {
- if (err instanceof Error) {
- throw err
- } else {
- throw new Error(`Unexpected error type: ${String(err)}`, { cause: err })
- }
- }
- }
-
- async getAccessToken (): Promise {
- this.checkClient()
-
- return await this.#client!.getAccessToken()
- }
-
- async generateAccessToken (): Promise {
- this.checkClient()
- this.#accessToken = await this.getAccessToken()
- }
-
- checkClient (): void {
- if (!this.#client) {
- throw new Error('Undefined OAuth2 client')
- }
- }
-
- set accessToken (accessToken: GetAccessTokenResponse | null) {
- this.#schema?.checkSchema()
-
- this.#schema?.validate({
- data: { accessToken },
- pick: true,
- })
-
- this.#accessToken = accessToken
- }
-
- get schema (): SchemaValidator | null {
- return this.#schema
- }
-
- get client (): OAuth2Client | null {
- return this.#client
- }
-
- get email (): string | null {
- return this.#email
- }
-
- get refreshToken (): string | null {
- return this.#refreshToken
- }
-
- get accessToken (): GetAccessTokenResponse | null {
- return this.#accessToken
- }
-}
-
-export default GmailOAuthClient
diff --git a/app/src/lib/index.ts b/app/src/lib/index.ts
index fa06f26..a89a795 100644
--- a/app/src/lib/index.ts
+++ b/app/src/lib/index.ts
@@ -1,6 +1,5 @@
import EmailTransport from '@/lib/email/transport.js'
import EmailSender from '@/lib/email/sender.js'
-import GmailOAuthClient from '@/lib/google/oauth2client.js'
import SchemaValidator from '@/lib/validator/schemavalidator.js'
export { send } from '@/lib/email/send.js'
@@ -9,6 +8,5 @@ export { buildHtml } from '@/lib/email/build.js'
export {
EmailSender,
EmailTransport,
- GmailOAuthClient,
SchemaValidator,
}
diff --git a/app/src/types/oauth2client.interface.ts b/app/src/types/oauth2client.interface.ts
deleted file mode 100644
index c887fd9..0000000
--- a/app/src/types/oauth2client.interface.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import SchemaValidator from '@/lib/validator/schemavalidator.js'
-
-import type {
- GetAccessTokenResponse,
- IOauthClient,
- OAuth2Client,
-} from '@/types/oauth2client.types.js'
-
-/**
- * Public properties and methods types of the `GmailOAuthClient` class
- * @interface IGmailOAuthClient
- */
-export interface IGmailOAuthClient {
- /**
- * Initializes the Google OAuth2 local client.
- * Keeps track of the client email and initial refresh token from the `.env` file.
- * @param {IOauthClient} params Parsed and formatted constructor parameters
- * @returns {void}
- */
- init (params: IOauthClient): void;
-
- /**
- * Generates a new Google OAuth2 access token using the refresh token
- * @returns {Promise} Promise that resolves to the `GetAccessTokenResponse` Google OAuth2 access token
- */
- getAccessToken (): Promise;
-
- /**
- * Generates a Google OAuth2 access token, storing it in the local `this.#accessToken`
- * @returns {Promise}
- */
- generateAccessToken (): Promise;
-
- /**
- * Validates that the OAuth2 client is initialized
- * @throws {Error} When the OAuth2 client is null
- */
- checkClient (): void;
-
- /**
- * Sets the value of the local access token
- */
- set accessToken (accessToken: GetAccessTokenResponse | null);
-
- /**
- * Retrieves the local zod schema wrapper
- * @returns {SchemaValidator | null} Local zod schema validator and wrapper
- */
- get schema (): SchemaValidator | null;
-
- /**
- * Retrieves the local-initialized Google OAuth2 client
- * @returns {OAuth2Client | null} local Google OAuth2 client
- */
- get client (): OAuth2Client | null;
-
- /**
- * Retrieves the email associated with the Google OAuth2 client
- * @returns {string | null} Google OAuth2 client email
- */
- get email (): string | null;
-
- /**
- * Retrieves the refresh token originally generated from the Google OAuth2 Playground at
- * https://developers.google.com/oauthplayground
- * @returns {string | null} Google OAuth2 refresh token
- */
- get refreshToken (): string | null;
-
- /**
- * Retrieves the local access token if initialized
- * @returns {GetAccessTokenResponse | null} Google Oauth2 access token
- */
- get accessToken (): GetAccessTokenResponse | null;
-}
-
diff --git a/app/src/types/oauth2client.schema.ts b/app/src/types/oauth2client.schema.ts
deleted file mode 100644
index 8a117cc..0000000
--- a/app/src/types/oauth2client.schema.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { z } from 'zod'
-
-export const GmailOAuthAccessTokenSchema = z.object({
- token: z.string(),
- res: z.record(z.any()),
-})
-
-export const GmailOAuthClientSchema = z.object({
- clientId: z.string().max(200),
- clientSecret: z.string().max(200),
- redirectURI: z.string().max(300),
- refreshToken: z.string().max(500),
- userEmail: z.string().email().max(150),
- accessToken: GmailOAuthAccessTokenSchema.optional(),
-})
diff --git a/app/src/types/oauth2client.types.ts b/app/src/types/oauth2client.types.ts
deleted file mode 100644
index 0add10e..0000000
--- a/app/src/types/oauth2client.types.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-export type { OAuth2Client } from 'google-auth-library'
-export type { GetAccessTokenResponse } from 'google-auth-library/build/src/auth/authclient.js'
-
-/**
- * Optional properties of the `OAuthClient` class constructor.
- * The class constructor uses the values in the .env file by default.
- * @interface IOauth2Client
- * @param {string} [clientId] Google OAuth2 client ID
- * @param {string} [clientSecret] Google OAuth2 secret
- * @param {string} [redirectURI] Google OAuth2 redirect URI. This defaults to the Google OAuth2 Playground at "https://developers.google.com/oauthplayground."
- * @param {string} [refreshToken] Google OAuth2 refresh token retrieved from the Google OAuth2 Playground.
- * @param {string} [userEmail] Google email account that created the `clientId` and `clientSecret`
- */
-export interface IOauthClient {
- clientId: string;
- clientSecret: string;
- redirectURI: string;
- refreshToken:string;
- userEmail: string;
-}
diff --git a/app/src/types/transport.interface.ts b/app/src/types/transport.interface.ts
index a83475a..9c26580 100644
--- a/app/src/types/transport.interface.ts
+++ b/app/src/types/transport.interface.ts
@@ -1,19 +1,20 @@
-import nodemailer from 'nodemailer'
-import GmailOAuthClient from '@/lib/google/oauth2client.js'
+import type { Transporter } from 'nodemailer'
import type { SMTPTransport } from './transport.types.js'
+import type { TransportOath2SchemaType } from './transport.schema.js'
/**
- * Public properties and methods types of the `GmailOAuthClient` class
+ * Public properties and methods types of the `EmailTransport` class
* @interface IEmailTransport
*/
export interface IEmailTransport {
/**
* Initializes `this.#transporter` with a Nodemailer transport using the 3-Legged OAuth (3LO) authentication.
- * @param {GmailOAuthClient} oauth2Client Instance of the `GmailOAuthClient` class
- * @returns {Promise} A completed Promise that intialized `this.#transporter`with a Nodemailer transport
+ * Reads Google OAuth2 credentials from environment variables.
+ * @param {TransportOath2SchemaType} options Google OAuth2 settings
+ * @returns {Promise} A completed Promise that initialized `this.#transporter` with a Nodemailer transport
* @see https://nodemailer.com/smtp/oauth2/#example-3
*/
- createTransport3LO (oauth2Client: GmailOAuthClient): Promise;
+ createTransport3LO (options?: TransportOath2SchemaType): Promise;
/**
* Retrieves the options used to initialize the Nodemailer transport
@@ -26,5 +27,5 @@ export interface IEmailTransport {
* Retrieves the local Nodemailer transporter instance
* @returns {nodemailer.Transporter | null} Nodemailer instance
*/
- get transporter (): nodemailer.Transporter | null;
+ get transporter (): Transporter | null;
}
diff --git a/app/src/types/transport.schema.ts b/app/src/types/transport.schema.ts
new file mode 100644
index 0000000..53e4d52
--- /dev/null
+++ b/app/src/types/transport.schema.ts
@@ -0,0 +1,14 @@
+import { z } from 'zod'
+
+/**
+ * Google OAuth2 settings
+ * Uses the `GOOGLE_*` values in the .env file by default.
+ */
+export const TransportOath2Schema = z.object({
+ googleUserEmail: z.string().email().max(150),
+ googleClientId: z.string().max(200),
+ googleClientSecret: z.string().max(200),
+ googleRereshToken: z.string().max(500),
+})
+
+export type TransportOath2SchemaType = z.infer;
diff --git a/docs/README_NPM.md b/docs/README_NPM.md
index 75d1acb..88139ff 100644
--- a/docs/README_NPM.md
+++ b/docs/README_NPM.md
@@ -72,7 +72,6 @@ We welcome contributions! Please see [CONTRIBUTING.md](/CONTRIBUTING.md) and the
| GOOGLE_USER_EMAIL | Your Google email that you've configured for Gmail SMTP and Google OAuth2. |
| GOOGLE_CLIENT_ID | Google OAuth2 client ID linked with your Google Cloud Platform project. |
| GOOGLE_CLIENT_SECRET | Google OAuth2 client secret associated with the `GOOGLE_CLIENT_ID`. |
- | GOOGLE_REDIRECT_URI | Allowed Google API redirect URI. Its value is `https://developers.google.com/oauthplayground` by default. |
| GOOGLE_REFRESH_TOKEN | The initial (or any) refresh token obtained from the [OAuthPlayground](https://developers.google.com/oauthplayground).
Read on [Using the OAuth 2.0 Playground](https://github.com/weaponsforge/email-sender?tab=readme-ov-file#using-the-oauth-20-playground) for more information about generating a refresh token using the Google OAuth Playground.
_(⚠️ **INFO:** This is an older note; some steps may vary this 2025)_
|