├── .github └── dependabot.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── babel.config.cjs ├── index.html ├── jest.config.cjs ├── package-lock.json ├── package.json ├── public ├── logo.svg └── stormkit-logo.svg ├── redirects.json ├── src ├── App.tsx ├── api │ ├── _assets │ │ └── default_template.ts │ ├── _db │ │ ├── datastore.ts │ │ ├── index.ts │ │ ├── mock-sqlite3.ts │ │ ├── sqlite3.ts │ │ └── store.d.ts │ ├── _mailer │ │ └── smtp.ts │ ├── _utils │ │ ├── http.ts │ │ ├── index.ts │ │ ├── string.ts │ │ └── test_utils.ts │ ├── login.post.ts │ ├── mail.post.ts │ ├── session.post.ts │ ├── subscriber │ │ ├── index.delete.ts │ │ ├── index.patch.spec.ts │ │ ├── index.patch.ts │ │ ├── index.post.spec.ts │ │ ├── index.post.ts │ │ └── upload.post.ts │ ├── subscribers.get.spec.ts │ ├── subscribers.get.ts │ ├── template.delete.ts │ ├── template.patch.ts │ ├── template.post.ts │ └── templates.get.ts ├── assets │ └── react.svg ├── components │ ├── Async │ │ ├── Async.tsx │ │ └── index.ts │ ├── CrudMenu │ │ ├── CrudMenu.tsx │ │ └── index.ts │ ├── Layout │ │ ├── Layout.tsx │ │ └── index.ts │ ├── Prompt │ │ ├── Prompt.tsx │ │ └── index.ts │ ├── TemplateDialog │ │ ├── TemplateDialog.tsx │ │ └── index.ts │ ├── TemplatePreview │ │ ├── TemplatePreview.tsx │ │ └── index.ts │ ├── TemplatePreviewDrawer │ │ ├── TemplatePreviewDrawer.tsx │ │ └── index.ts │ ├── UploadDialog │ │ ├── UploadDialog.tsx │ │ └── index.ts │ └── UserDialog │ │ ├── UserDialog.tsx │ │ └── index.ts ├── context.ts ├── entry-client.tsx ├── entry-server.tsx ├── index.css ├── mui-theme.tsx ├── pages │ ├── index.actions.ts │ ├── index.tsx │ ├── login.tsx │ ├── subscribers.actions.ts │ ├── subscribers.tsx │ ├── templates.actions.ts │ └── templates.tsx ├── prerender.ts ├── routes.tsx ├── types │ ├── fetch-data.d.ts │ ├── mui.d.ts │ ├── seo.d.ts │ ├── template.d.ts │ └── user.d.ts ├── utils │ ├── fetcher.ts │ └── index.ts ├── vite-env.d.ts └── vite-server.ts ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.api.ts ├── vite.config.ssr.ts └── vite.config.ts /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | .stormkit 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | .env 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine AS builder 2 | WORKDIR /app 3 | COPY . . 4 | RUN npm install 5 | 6 | FROM gcr.io/distroless/nodejs18-debian11 AS mailer 7 | COPY --from=builder /app /app 8 | WORKDIR /app 9 | 10 | ENV NODE_NO_WARNINGS=1 11 | 12 | EXPOSE 5173 13 | 14 | CMD [ "--loader", "ts-node/esm", "/app/src/vite-server.ts" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Savas Vedova , Twitter: @savasvedova 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stormkit Mailer (under active development) 2 | 3 | Send automated emails, or launch campaigns through a simple API or an intuitive UI. 4 | 5 | ## Live Demo 6 | 7 | Visit [https://mailer-demo.stormkit.dev](https://mailer-demo.stormkit.dev). 8 | 9 | - Username: `root` 10 | - Password: `123456` 11 | 12 | It's a limited, read-only version. 13 | 14 | ## Features 15 | 16 | ☑️  **SES Mailer:** Send emails through your own Amazon SES account. 17 | 18 | ✅  **SMTP:** Send emails through SMTP, such as your own Gmail account. 19 | 20 | ☑️  **Gmail API** Send emails through Gmail API. 21 | 22 | ✅  **Minimal UI:** Simple, intuitive UI to configure your templates 23 | 24 | ☑️  **Subscribers:** Upload your subscribers either through API or manually 25 | 26 | ☑️  **Unsubscribe:** Users can unsubscribe 27 | 28 | ☑️  **API:** Send emails to your users through a simple API 29 | 30 | ✅  **Free Forever:** Using Stormkit Mailer is free of charge 31 | 32 | **Legend** 33 | 34 | ✅ Ready to use 35 | 36 | ☑️ Incomplete or not yet started 37 | 38 | ## Configuration 39 | 40 | The Mailer is configured through environment variables. You can configure these 41 | variables either by providing an `.env` file or by making these variables available 42 | to your process. 43 | 44 | | Variable | Description | 45 | | -------- | ----------- | 46 | | ADMIN_USERNAME | The user name that is used to login the Mailer app. | 47 | | ADMIN_PASSWORD | The password that is used to login the Mailer app. | 48 | | SMTP_USERNAME | The user name that is used to login your SMTP provider. | 49 | | SMTP_PASSWORD | The password that is used to login your SMTP provider. | 50 | | JWT_SECRET | A random string that is used to encrypt your JWT tokens. | 51 | | MAILER_FROM_ADDR | The address that will be used to send emails. | 52 | 53 | Note that some of these variables will be moved to the configuration page once the page is implemented. See https://github.com/stormkit-io/mailer/issues/2 for more details. 54 | 55 | ## Local development 56 | 57 | See [Docker](/#Docker) for containarized environments. 58 | 59 | ```bash 60 | $ git clone git@github.com:stormkit-io/mailer.git 61 | $ cd mailer 62 | $ npm install 63 | $ npm run dev 64 | ``` 65 | 66 | Create an `.env` file on the root level of the repository and configure the environment variables mentioned in the [Configuration](#configuration) section. 67 | 68 | ✅ HMR enabled 69 | 70 | ✅ To force restarting the server, type `rs` and hit Enter on the terminal 71 | 72 | ## Docker 73 | 74 | ```bash 75 | $ docker build -t mailer . 76 | $ docker run -t mailer 77 | ``` 78 | 79 | Currently, the container does not stop when a Stop Signal is sent. See https://github.com/stormkit-io/mailer/issues/12 for more context. 80 | 81 | To stop the container, you can execute: 82 | 83 | ```bash 84 | $ docker stop $(docker ps -q --filter ancestor=mailer) 85 | ``` 86 | 87 | ## License 88 | 89 | MIT 90 | -------------------------------------------------------------------------------- /babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { presets: ["@babel/preset-env"] }; 2 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | transform: { 4 | "^.+\\.(ts|tsx)?$": "ts-jest", 5 | "^.+\\.(js|jsx)$": "babel-jest", 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorepo-template", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "author": { 7 | "email": "hello@stormkit.io", 8 | "name": "Stormkit", 9 | "url": "https://www.stormkit.io" 10 | }, 11 | "scripts": { 12 | "dev": "NODE_NO_WARNINGS=1 nodemon --watch './src/vite-server.ts' --exec 'node --loader ts-node/esm' src/vite-server.ts", 13 | "build": "npm run build:spa && npm run build:ssr && npm run build:ssg && npm run build:api && rm -rf .stormkit/server", 14 | "build:spa": "tsc && vite build", 15 | "build:ssg": "tsc && SSG=true node --loader ts-node/esm ./src/vite-server.ts", 16 | "build:ssr": "tsc && vite build -c vite.config.ssr.ts", 17 | "build:api": "rm -rf .stormkit/api && node --loader ts-node/esm vite.config.api.ts", 18 | "test": "jest" 19 | }, 20 | "dependencies": { 21 | "@emotion/react": "^11.11.1", 22 | "@emotion/styled": "^11.11.0", 23 | "@mui/icons-material": "^5.11.16", 24 | "@mui/lab": "^5.0.0-alpha.133", 25 | "@mui/material": "^5.13.5", 26 | "@stormkit/ds": "^1.4.2", 27 | "formidable": "^3.5.0", 28 | "http-status-codes": "^2.2.0", 29 | "jose": "^4.14.4", 30 | "nodemailer": "^6.9.3", 31 | "react": "^18.2.0", 32 | "react-dom": "^18.2.0", 33 | "squirrelly": "^9.0.0" 34 | }, 35 | "devDependencies": { 36 | "@babel/preset-env": "^7.22.5", 37 | "@stormkit/serverless": "^1.1.4", 38 | "@types/express": "^4.17.17", 39 | "@types/formidable": "^2.0.6", 40 | "@types/glob": "^8.1.0", 41 | "@types/jest": "^29.5.2", 42 | "@types/node": "^20.3.1", 43 | "@types/nodemailer": "^6.4.8", 44 | "@types/react": "^18.2.13", 45 | "@types/react-dom": "^18.2.6", 46 | "@vitejs/plugin-react": "^4.0.1", 47 | "babel-jest": "^29.5.0", 48 | "dotenv": "^16.3.1", 49 | "express": "^4.18.2", 50 | "glob": "^10.2.7", 51 | "jest": "^29.5.0", 52 | "nodemon": "^2.0.22", 53 | "react-router": "^6.6.2", 54 | "react-router-dom": "^6.13.0", 55 | "sqlite3": "^5.1.6", 56 | "ts-jest": "^29.1.0", 57 | "ts-node": "^10.9.1", 58 | "typescript": "^5.1.3", 59 | "vite": "^4.3.9", 60 | "vite-plugin-static-copy": "^0.16.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/stormkit-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /redirects.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "from": "/*", 4 | "to": "/index.html", 5 | "assets": false 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import type { RouteProps } from "react-router-dom"; 2 | import { useEffect, useState } from "react"; 3 | import { useLocation, Routes, Route, useNavigate } from "react-router-dom"; 4 | import { StyledEngineProvider, ThemeProvider } from "@mui/material/styles"; 5 | import Box from "@mui/material/Box"; 6 | import CssBaseline from "@mui/material/CssBaseline"; 7 | import LinearProgress from "@mui/material/LinearProgress"; 8 | import Layout from "~/components/Layout"; 9 | import { fetcher } from "./utils"; 10 | import theme from "./mui-theme"; 11 | import Context from "./context"; 12 | 13 | interface Props { 14 | routes: RouteProps[]; 15 | } 16 | 17 | export default function App({ routes }: Props) { 18 | const token = 19 | typeof localStorage !== "undefined" ? localStorage.getItem("login") : null; 20 | 21 | const [isLoggedIn, setIsLoggedIn] = useState(false); 22 | const [isLoading, setIsLoading] = useState(true); 23 | const navigate = useNavigate(); 24 | const location = useLocation(); 25 | 26 | useEffect(() => { 27 | if (!token) { 28 | setIsLoading(false); 29 | return; 30 | } 31 | 32 | fetcher("/api/session", { 33 | method: "POST", 34 | body: { token }, 35 | withAuth: false, 36 | }) 37 | .then(async (res) => { 38 | const data: { ok: boolean } = await res.json(); 39 | 40 | if (data.ok) { 41 | setIsLoggedIn(true); 42 | 43 | if (location.pathname === "/login") { 44 | navigate("/"); 45 | } 46 | } 47 | }) 48 | .finally(() => { 49 | setIsLoading(false); 50 | }); 51 | }, [location.pathname, token]); 52 | 53 | useEffect(() => { 54 | if (!isLoading && !isLoggedIn) { 55 | navigate("/login"); 56 | } 57 | }, [isLoading, isLoggedIn]); 58 | 59 | return ( 60 | 61 | 62 | 63 | 66 | 76 | {isLoading && } 77 | {!isLoading && ( 78 | 79 | 80 | {routes.map((route) => ( 81 | 82 | ))} 83 | 84 | 85 | )} 86 | 87 | 88 | 89 | 90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /src/api/_assets/default_template.ts: -------------------------------------------------------------------------------- 1 | // Template from: https://github.com/leemunroe/responsive-html-email-template/blob/master/email.html 2 | const template: Template = { 3 | name: "Standard Template", 4 | description: "Default template that will be used to send emails.", 5 | isDefault: true, 6 | html: ` 7 | 8 | 9 | 10 | 11 | Simple Transactional Email 12 | 342 | 343 | 344 | {{ @if (it.preheader) }} 345 | {{ it.preheader }} 346 | {{ /if }} 347 | 354 | 355 | 356 | 428 | 429 | 430 | 431 | 432 | `, 433 | }; 434 | 435 | export default template; 436 | -------------------------------------------------------------------------------- /src/api/_db/datastore.ts: -------------------------------------------------------------------------------- 1 | import type { Store } from "./store.d"; 2 | import ds from "@stormkit/ds"; 3 | 4 | const store: Store = { 5 | templates: { 6 | async list() { 7 | return ds.fetch