Skip to content

Commit faa8747

Browse files
authored
feat: send email from gmail account (#173)
* feat: send email from gmail account * refactor: new middleware `getCurrentConnectedService` this middleware will get the current service from the email present in `req.params` * feat: sanitize html before sending email in gmail
1 parent 3b6726e commit faa8747

9 files changed

+6000
-5701
lines changed

.vscode/settings.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"cSpell.words": ["markverified"],
2+
"cSpell.words": ["dompurify", "markverified"],
33
"docwriter.style": "Auto-detect",
44
"editor.tabSize": 4,
55
"editor.comments.insertSpace": true,

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"express-rate-limit": "^6.6.0",
3030
"helmet": "^6.0.0",
3131
"http-status-codes": "^2.2.0",
32+
"isomorphic-dompurify": "^0.23.0",
3233
"jsonwebtoken": "^8.5.1",
3334
"lodash": "^4.17.21",
3435
"mongoose": "^6.6.1",

src/app.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import adminRouter from "./routes/admin.routes";
1313
import mailRouter from "./routes/mail.routes";
1414
import deserializeUser from "./middleware/deserializeUser.middleware";
1515
import requireAdminRole from "./middleware/requireAdminRole.middleware";
16+
import requireSameUser from "./middleware/requireSameUser.middleware";
17+
import getCurrentConnectedService from "./middleware/getCurrentConnectedService.middleware";
1618

1719
config();
1820

@@ -27,7 +29,7 @@ app.use(express.json());
2729
app.use("/api", authRouter);
2830
app.use("/api", userRouter);
2931
app.use("/api", ticketRouter);
30-
app.use("/api", mailRouter);
32+
app.use("/api", deserializeUser, mailRouter);
3133
app.use("/api", deserializeUser, requireAdminRole, adminRouter);
3234

3335
logger.info("Current Environment: " + process.env.NODE_ENV);

src/controllers/auth.controller.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export const signupHandler = async (req: Request<{}, {}, SignupSchema["body"]>,
5959

6060
sendEmail(
6161
email,
62-
"OTP for Multi Email",
62+
"Verify your Multi Email account",
6363
`<h2>Welcome to Multi Email</h2>
6464
<h4>please visit this URL and enter your OTP</h4>
6565
<p>

src/controllers/mail.controller.ts

+59-19
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { Request, Response } from "express";
22
import axios from "axios";
33
import { StatusCodes } from "http-status-codes";
4-
import { GetEmailsFromGmailSchema } from "../schemas/mail.schema";
5-
import { ConnectedServices, User } from "../models/user.model";
4+
import { GetEmailsFromGmailSchema, PostSendGmailSchema } from "../schemas/mail.schema";
5+
import { ConnectedServices } from "../models/user.model";
66
import logger from "../utils/logger.util";
77
import { URLSearchParams } from "url";
8+
import { createTransport, SendMailOptions, Transporter } from "nodemailer";
9+
import DOMPurify from "isomorphic-dompurify";
810

911
/**
1012
* This function will fetch all the emails from gmail
@@ -18,20 +20,8 @@ export const getEmailsFromGmailHandler = async (
1820
res: Response,
1921
) => {
2022
try {
21-
const { email } = req.params;
2223
const { maxResults, pageToken, q, includeSpamTrash } = req.query;
23-
const user = res.locals.user as User;
24-
25-
// get email accessToken
26-
const foundService = user.connected_services.find(
27-
(service: ConnectedServices) => service.email === email,
28-
);
29-
30-
if (!foundService) {
31-
return res.status(StatusCodes.NOT_FOUND).json({
32-
error: "Account not connected",
33-
});
34-
}
24+
const currentConnectedService = res.locals.currentConnectedService as ConnectedServices;
3525

3626
const fetchEmailsQueryURL = new URLSearchParams({
3727
maxResults: maxResults || "100",
@@ -41,10 +31,12 @@ export const getEmailsFromGmailHandler = async (
4131
});
4232

4333
const response = await axios.get(
44-
`https://gmail.googleapis.com/gmail/v1/users/${email}/messages?${fetchEmailsQueryURL.toString()}`,
34+
`https://gmail.googleapis.com/gmail/v1/users/${
35+
currentConnectedService.email
36+
}/messages?${fetchEmailsQueryURL.toString()}`,
4537
{
4638
headers: {
47-
Authorization: `Bearer ${foundService.access_token}`,
39+
Authorization: `Bearer ${currentConnectedService.access_token}`,
4840
"Content-type": "application/json",
4941
},
5042
},
@@ -56,11 +48,59 @@ export const getEmailsFromGmailHandler = async (
5648
size: response.data.messages.length,
5749
nextPageToken: response.data.nextPageToken,
5850
});
59-
} catch (err) {
60-
logger.error(err);
51+
} catch (err: any) {
52+
logger.error(err.response);
6153

6254
return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
6355
error: "Internal Server Error",
6456
});
6557
}
6658
};
59+
60+
/**
61+
* This controller will send a email from users gmail account
62+
* @param req express request
63+
* @param res express response
64+
*
65+
* @author aayushchugh
66+
*/
67+
export const postSendGmailHandler = async (
68+
req: Request<PostSendGmailSchema["params"], {}, PostSendGmailSchema["body"]>,
69+
res: Response,
70+
) => {
71+
const { to, subject, html } = req.body;
72+
const currentConnectedService = res.locals.currentConnectedService as ConnectedServices;
73+
74+
try {
75+
const cleanedHTML = DOMPurify.sanitize(html);
76+
77+
const transporter: Transporter = createTransport({
78+
service: "gmail",
79+
auth: {
80+
type: "OAuth2",
81+
user: currentConnectedService.email,
82+
clientId: process.env.GOOGLE_CLIENT_ID,
83+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
84+
refreshToken: currentConnectedService.refresh_token,
85+
accessToken: currentConnectedService.access_token,
86+
},
87+
});
88+
89+
const mailOptions: SendMailOptions = {
90+
from: currentConnectedService.email,
91+
to,
92+
subject,
93+
html: cleanedHTML,
94+
};
95+
96+
await transporter.sendMail(mailOptions);
97+
98+
return res.status(StatusCodes.OK).json({
99+
message: "Email sent successfully",
100+
});
101+
} catch (err) {
102+
return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
103+
error: "Internal Server Error",
104+
});
105+
}
106+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { NextFunction, Request, Response } from "express";
2+
import { StatusCodes } from "http-status-codes";
3+
import { User } from "../models/user.model";
4+
5+
/**
6+
* This middleware will find current connected service from current user
7+
* and save that to `res.locals.currentConnectedService`
8+
*
9+
* @param req express request
10+
* @param res express response
11+
* @param next express next function
12+
*
13+
* @author aayushchugh
14+
*/
15+
const getCurrentConnectedService = (req: Request, res: Response, next: NextFunction) => {
16+
try {
17+
const user = res.locals.user as User;
18+
const { email } = req.params;
19+
20+
const currentConnectedService = user.connected_services.find(
21+
(service) => service.email === email,
22+
);
23+
24+
if (!currentConnectedService) {
25+
return res.status(StatusCodes.NOT_FOUND).json({
26+
error: "Account not connected",
27+
});
28+
}
29+
30+
res.locals.currentConnectedService = currentConnectedService;
31+
next();
32+
} catch (err) {
33+
return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
34+
error: "Internal Server Error",
35+
});
36+
}
37+
};
38+
39+
export default getCurrentConnectedService;

src/routes/mail.routes.ts

+18-8
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
import { Router } from "express";
2-
import { getEmailsFromGmailHandler } from "../controllers/mail.controller";
3-
import deserializeUser from "../middleware/deserializeUser.middleware";
2+
import { getEmailsFromGmailHandler, postSendGmailHandler } from "../controllers/mail.controller";
3+
import getCurrentConnectedService from "../middleware/getCurrentConnectedService.middleware";
44
import requireSameUser from "../middleware/requireSameUser.middleware";
5+
import validateRequest from "../middleware/validateRequest.middleware";
6+
import { getEmailsFromGmailSchema, postSendGmailSchema } from "../schemas/mail.schema";
57

68
const mailRouter = Router();
79

8-
mailRouter.get(
9-
"/mail/:id/gmail/:email",
10-
deserializeUser,
11-
requireSameUser,
12-
getEmailsFromGmailHandler,
13-
);
10+
mailRouter
11+
.route("/mail/:id/gmail/:email")
12+
.get(
13+
requireSameUser,
14+
validateRequest(getEmailsFromGmailSchema),
15+
getCurrentConnectedService,
16+
getEmailsFromGmailHandler,
17+
)
18+
.post(
19+
requireSameUser,
20+
validateRequest(postSendGmailSchema),
21+
getCurrentConnectedService,
22+
postSendGmailHandler,
23+
);
1424

1525
export default mailRouter;

src/schemas/mail.schema.ts

+26-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { z } from "zod";
22

33
/**
4-
* This schema will validate `/mail/gmail/:email` route
4+
* This schema will validate `GET /mail/gmail/:email` route
55
*
66
* @author aayushchugh
77
*/
@@ -23,3 +23,28 @@ export const getEmailsFromGmailSchema = z.object({
2323
* @author aayushchugh
2424
*/
2525
export type GetEmailsFromGmailSchema = z.TypeOf<typeof getEmailsFromGmailSchema>;
26+
27+
/**
28+
* This schema will validate `POST /mail/gmail/:email` route
29+
*
30+
* @author aayushchugh
31+
*/
32+
export const postSendGmailSchema = z.object({
33+
body: z.object({
34+
to: z.string({ required_error: "to is required" }).email("to must be a valid email"),
35+
subject: z.string({ required_error: "subject is required" }),
36+
html: z.string({ required_error: "html is required" }),
37+
}),
38+
params: z.object({
39+
email: z
40+
.string({ required_error: "email param is required" })
41+
.email("please enter a valid email param"),
42+
}),
43+
});
44+
45+
/**
46+
* This type is generated from `sendGmailSchema`
47+
*
48+
* @author aayushchugh
49+
*/
50+
export type PostSendGmailSchema = z.TypeOf<typeof postSendGmailSchema>;

0 commit comments

Comments
 (0)