Skip to content

Commit

Permalink
Core Mailer (#494)
Browse files Browse the repository at this point in the history
  • Loading branch information
alllenshibu authored Oct 12, 2024
1 parent 6042486 commit d8e7918
Show file tree
Hide file tree
Showing 16 changed files with 410 additions and 14,723 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,29 @@ jobs:
curl -X GET ${{ secrets.DEV_CORE_ADMIN_DEPLOY_HOOK }}
curl -X GET ${{ secrets.PROD_CORE_ADMIN_DEPLOY_HOOK }}
build-and-push-core-mailer:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- run: |
docker build -t techno-event-core-mailer -f apps/core-mailer/Dockerfile .
- run: |
docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASSWORD }}
- run: |
docker tag techno-event-core-mailer ${{ secrets.DOCKER_USER }}/techno-event-core-mailer:latest
docker push ${{ secrets.DOCKER_USER }}/techno-event-core-mailer:latest
redeploy-core-mailer:
needs: build-and-push-core-mailer
runs-on: ubuntu-latest
steps:
- name: Call deploy hook
run: |
curl -X GET ${{ secrets.DEV_CORE_MAILER_DEPLOY_HOOK }}
curl -X GET ${{ secrets.PROD_CORE_MAILER_DEPLOY_HOOK }}
build-and-push-core-auth0-actions:
runs-on: ubuntu-latest

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ node_modules
.pnp
.pnp.js

# docs
docs

# testing
coverage

Expand Down
7 changes: 5 additions & 2 deletions apps/core-mailer/.dockerignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
node_modules
build
.turbo
dist
build
node_modules
.env*
13 changes: 13 additions & 0 deletions apps/core-mailer/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
REDIS_URL
RABBITMQ_URL

ACCESS_TOKEN
PORT=3000

EMAIL_ID
GMAIL_API_CLIENT_ID
GMAIL_API_CLIENT_SECRET
GMAIL_API_REFRESH_TOKEN
GMAIL_EMAIL_ADDRESS

GMAIL_API_ACCESS_TOKEN
14 changes: 10 additions & 4 deletions apps/core-mailer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main": "build/index.js",
"scripts": {
"build": "npm run clean & webpack --config webpack.config.js",
"clean": "rimraf dist & rimraf build",
"clean": "rimraf dist & rimraf build & rimraf docs",
"dev": "concurrently \"npx tsc --watch\" \"nodemon -q build/index.js\"",
"start": "node dist/app.js",
"lint": "eslint --ext .ts src"
Expand All @@ -14,18 +14,23 @@
"author": "",
"license": "ISC",
"dependencies": {
"amqplib": "^0.10.4",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"database": "workspace:eventsync-core-mailer-database",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"nodemailer": "^6.9.9"
"ioredis": "^5.4.1",
"nodemailer": "^6.9.9",
"uuid": "^9.0.0"
},
"devDependencies": {
"@types/nodemailer": "^6.4.14",
"@types/chai": "^4.3.5",
"@types/amqplib": "^0.10.5",
"@types/express": "^4.17.17",
"@types/node": "^20.10.5",
"@types/nodemailer": "^6.4.14",
"@types/pg": "^8.10.1",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^6.7.3",
"concurrently": "^8.0.1",
"eslint": "^8.56.0",
Expand All @@ -40,6 +45,7 @@
"rimraf": "^5.0.1",
"ts-loader": "^9.5.1",
"tsconfig": "workspace:*",
"typedoc": "^0.26.6",
"typescript": "^5.0.4",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4"
Expand Down
18 changes: 18 additions & 0 deletions apps/core-mailer/src/config/NodeMailer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Mail from 'nodemailer/lib/mailer';

import nodemailer from 'nodemailer';

export const createTransport = (): Mail => {
return nodemailer.createTransport({
host: 'smtp.gmail.com',
port: 587,
secure: false,
auth: {
type: 'OAuth2',
user: process.env.GMAIL_EMAIL_ADDRESS,
clientId: process.env.GMAIL_API_CLIENT_ID,
clientSecret: process.env.GMAIL_API_CLIENT_SECRET,
refreshToken: process.env.GMAIL_API_REFRESH_TOKEN,
},
});
};
13 changes: 13 additions & 0 deletions apps/core-mailer/src/config/Redis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Redis from 'ioredis';

// Right now the entries do not have a TTL, so they will remain in Redis indefinitely.
// This is not a problem for this application, but it is something to keep in mind.
// Ideally, we would want to set a TTL for each entry, so that they are automatically removed after a certain amount of time.
// The application sending the email job should check whether the email has been sent or failed and update in its database.

export const createRedisClient = (): Redis => {
return new Redis(process.env.REDIS_URL as string, {
maxRetriesPerRequest: null,
keyPrefix: 'core-mailer:',
});
};
28 changes: 0 additions & 28 deletions apps/core-mailer/src/controller/mail.ts

This file was deleted.

38 changes: 38 additions & 0 deletions apps/core-mailer/src/controllers/MailController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { UUID } from 'node:crypto';

import { Mail } from '../models/Mail';
import { MailService } from '../services/MailService';

// Add email to the queue. This will be picked up by the worker and sent.
export const sendEmailController: any = async (
name: string,
to: string,
subject: string,
text: string,
html: string,
) => {
try {
const mail: Mail = new Mail(process.env.EMAIL_ID as string, name, to, subject, text, html);

const mailService: MailService = new MailService();
await mailService.initialize();

await mailService.publish(mail);

return mail.jobId;
} catch (error) {
console.error(error);
}
};

// Check the status of an email job.
export const fetchEmailJobStatus: any = async (jobId: UUID) => {
try {
const mailService: MailService = new MailService();
await mailService.initialize();

return await mailService.fetchJobStatus(jobId);
} catch (error) {
console.error(error);
}
};
79 changes: 62 additions & 17 deletions apps/core-mailer/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,92 @@
import express, { Request, Response } from 'express';
import dotenv from 'dotenv';
import { sendMail } from './controller/mail';
import { UUID } from 'node:crypto';

import express, { Express, Request, Response } from 'express';

import { fetchEmailJobStatus, sendEmailController } from './controllers/MailController';
import { MailService } from './services/MailService';
import { authorize } from './middlewares/auth';

const bodyParser = require('body-parser');
const cors = require('cors');

dotenv.config();

const port = process.env.PORT || 80;

const app = express();
app.use(
cors({
origin: '*',
}),
);
const app: Express = express();

app.use(cors({ origin: '*' }));

app.use(bodyParser.json());

app.get('/health', (req: Request, res: Response) => {
const healthcheck: any = {
resource: 'Techno Mailer Server',
resource: 'Core Mailer',
uptime: process.uptime(),
responseTime: process.hrtime(),
message: 'OK',
timestamp: Date.now(),
};
try {
res.send(healthcheck);
return res.send(healthcheck);
} catch (error) {
healthcheck.message = error;
res.status(503).send();
return res.status(503).send();
}
});

app.post('/send-mail', async (req: Request, res: Response) => {
const { email, subject, text } = req.body;
// Add email to queue
app.post('/mail', authorize, async (req: Request, res: Response) => {
try {
await sendMail(email, subject, text);
res.status(200).send('Email sent successfully');
const { name, to, subject, text, html } = req.body;

if (!name || !to || !subject || !text || !html)
return res.status(400).send({ message: 'Missing required fields' });

const jobId: UUID = await sendEmailController(name, to, subject, text, html);

return res.status(200).send({
jobId: jobId,
});
} catch (error) {
res.status(500).send('Email failed to send');
console.error(error);
return res.status(500).send({ message: 'Internal Server Error' });
}
});

// Fetch email status
app.get('/mail', authorize, async (req: Request, res: Response) => {
try {
const { jobId } = req.query;

if (!jobId) return res.status(400).send({ message: 'Missing required fields' });

const status = await fetchEmailJobStatus(jobId as UUID);

return res.status(200).send({
status: status,
});
} catch (error) {
console.error(error);
return res.status(500).send({ message: 'Internal Server Error' });
}
});

app.listen(port, () => {
console.log(`Server is running at http://localhost:${port}`);
console.log(`Core Mailer is running on ${port}`);
});

// Check queue for new emails and send them
async function startMailSub() {
try {
const rmqInstance: MailService = new MailService();
await rmqInstance.initialize();
await rmqInstance.subscribe();
console.log('Subscribed to RabbitMQ email queue');
} catch (error) {
console.error('Failed to subscribe to RabbitMQ email queue: ', error);
}
}

startMailSub();
24 changes: 24 additions & 0 deletions apps/core-mailer/src/middlewares/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { NextFunction, Request, Response } from 'express';

export const authorize = (req: Request, res: Response, next: NextFunction) => {
try {
const ACCESS_TOKEN = process.env.ACCESS_TOKEN;

if (!ACCESS_TOKEN) {
console.error('Access token not found on the server');
return res.status(500).json({ message: 'Internal Server Error' });
}

const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];

if (!token) return res.status(401).json({ message: 'Unauthorized' });

if (token !== ACCESS_TOKEN) return res.status(403).json({ message: 'Unauthorized' });

next();
} catch (error) {
console.error(error);
return res.status(500).json({ message: 'Internal Server Error' });
}
};
54 changes: 54 additions & 0 deletions apps/core-mailer/src/models/Mail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { randomUUID, UUID } from 'node:crypto';

export class Mail {
readonly jobId: UUID;
accessor from: string;
accessor name: string;
accessor to: string;
accessor subject: string;
accessor text: string;
accessor html: string;

constructor(
from: string,
name: string,
to: string,
subject: string,
text: string,
html: string,
jobId: UUID = randomUUID(),
) {
this.jobId = jobId;
this.from = from;
this.name = name;
this.to = to;
this.subject = subject;
this.text = text;
this.html = html;
}

toString(): string {
return `{
jobId: "${this.jobId}",
from: "${this.from}",
name: "${this.name}",
to: "${this.to}",
subject: "${this.subject}",
text: "${this.text}",
html: "${this.html}"
}`;
}
}

export function parseMail(str: string): Mail {
const parsed = JSON.parse(str.replace(/(\w+):/g, '"$1":'));
return new Mail(
parsed.from,
parsed.name,
parsed.to,
parsed.subject,
parsed.text,
parsed.html,
parsed.jobId,
);
}
Loading

0 comments on commit d8e7918

Please sign in to comment.