From a437ceea0ad91a2d5c83058d0a31bb585d6085c8 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Fri, 3 May 2024 00:32:08 +0530 Subject: [PATCH 1/8] add: attachments workflow --- Dockerfile | 2 +- src/generics/helpers/email-notifications.js | 40 +++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 09dab55e..a71d09a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:16 +FROM node:20 #Set working directory WORKDIR /var/src/ diff --git a/src/generics/helpers/email-notifications.js b/src/generics/helpers/email-notifications.js index 16e6586a..c6890e3f 100644 --- a/src/generics/helpers/email-notifications.js +++ b/src/generics/helpers/email-notifications.js @@ -9,6 +9,7 @@ const sgMail = require('@sendgrid/mail') sgMail.setApiKey(process.env.SENDGRID_API_KEY) const logQueries = require('../../database/queries/log') +const request = require('request') /** * Send Email @@ -22,9 +23,46 @@ const logQueries = require('../../database/queries/log') * @param {String} params.cc - contains the cc of the email * @returns {JSON} Returns response of the email sending information */ + +async function fetchFileByUrl(fileUrl) { + try { + const response = await new Promise((resolve, reject) => { + request(fileUrl.url, { encoding: null }, (err, res, body) => { + if (err) { + reject(err) + } else { + resolve({ content: body, filename: fileUrl.filename }) + } + }) + }) + return response + } catch (error) { + throw new Error('Error fetching file: ' + error.message) + } +} + async function sendEmail(params) { try { + let attachments = [] + + if (params.attachments && params.attachments.length > 0) { + const processAttachment = async (attachment) => { + const attachmentContent = await fetchFileByUrl(attachment) + return { + content: Buffer.from(attachmentContent.content).toString('base64'), + filename: attachment.filename, + type: attachment.type, + } + } + + if (params.attachments.length === 1) { + attachments.push(await processAttachment(params.attachments[0])) + } else { + attachments = await Promise.all(params.attachments.map(processAttachment)) + } + } let fromMail = process.env.SENDGRID_FROM_MAIL + if (params.from) { fromMail = params.from } @@ -35,6 +73,7 @@ async function sendEmail(params) { to: to, // list of receivers subject: params.subject, // Subject line html: params.body, + attachments: attachments, } if (params.cc) { message['cc'] = params.cc.split(',') @@ -66,6 +105,7 @@ async function sendEmail(params) { message: 'successfully mail sent', } } catch (error) { + console.log(error) return { status: 'failed', message: 'Mail server is down, please try after some time', From b8bf7b0d573aea69da38b84085e7583b4ad359c1 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Fri, 3 May 2024 10:15:30 +0530 Subject: [PATCH 2/8] updated validators --- src/validators/v1/email.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/validators/v1/email.js b/src/validators/v1/email.js index b4401c53..0dc3c496 100644 --- a/src/validators/v1/email.js +++ b/src/validators/v1/email.js @@ -9,9 +9,28 @@ module.exports = { req.checkBody('email.to') .notEmpty() - .withMessage('email field is empty') + .withMessage('email.to field is empty') .custom((emailIds) => emailValidation(emailIds)) .withMessage('invalid email ids') + req.checkBody('email.attachments').optional().notEmpty().withMessage('email.attachments field is empty') + if (emailValidation.attachments) { + req.checkBody('email.attachments.*.url') + .notEmpty() + .withMessage('attachments.url field is empty') + .isURL() + .withMessage('attachments.url is invalid') + + req.checkBody('email.attachments.*.filename') + .notEmpty() + .withMessage('attachments.filename field is empty') + .isAlphanumeric('en-US', { ignore: '-_' }) + .withMessage('attachments.filename is invalid') + req.checkBody('email.attachments.*.filename') + .notEmpty() + .withMessage('attachments.type field is empty') + .isAlphanumeric('en-US', { ignore: '/' }) + .withMessage('attachments.type is invalid') + } }, } From 87090cb22788a213c10b4e9084a972fefb44c635 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Fri, 3 May 2024 14:38:21 +0530 Subject: [PATCH 3/8] add: error handling --- src/generics/helpers/email-notifications.js | 47 +++++++++++++++------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/src/generics/helpers/email-notifications.js b/src/generics/helpers/email-notifications.js index c6890e3f..781122d1 100644 --- a/src/generics/helpers/email-notifications.js +++ b/src/generics/helpers/email-notifications.js @@ -24,12 +24,23 @@ const request = require('request') * @returns {JSON} Returns response of the email sending information */ +/** + * Fetches a file from a given URL. + * @param {Object} fileUrl - The URL object containing information about the file. + * @param {string} fileUrl.url - The URL of the file to fetch. + * @param {string} fileUrl.filename - The name of the file. + * @returns {Promise} A promise that resolves with an object containing the file content and filename. + * @throws {Error} If an error occurs during the file fetch operation. + */ async function fetchFileByUrl(fileUrl) { try { const response = await new Promise((resolve, reject) => { request(fileUrl.url, { encoding: null }, (err, res, body) => { if (err) { reject(err) + } else if (res.statusCode === 400 || res.statusCode >= 500) { + // Handle 400 Bad Request and server errors + reject(new Error(`Request failed with status code ${res.statusCode}`)) } else { resolve({ content: body, filename: fileUrl.filename }) } @@ -44,21 +55,27 @@ async function fetchFileByUrl(fileUrl) { async function sendEmail(params) { try { let attachments = [] + let errorMeta = {} + try { + if (params.attachments && params.attachments.length > 0) { + const processAttachment = async (attachment) => { + const attachmentContent = await fetchFileByUrl(attachment) + return { + content: Buffer.from(attachmentContent.content).toString('base64'), + filename: attachment.filename, + type: attachment.type, + } + } - if (params.attachments && params.attachments.length > 0) { - const processAttachment = async (attachment) => { - const attachmentContent = await fetchFileByUrl(attachment) - return { - content: Buffer.from(attachmentContent.content).toString('base64'), - filename: attachment.filename, - type: attachment.type, + if (params.attachments.length === 1) { + attachments.push(await processAttachment(params.attachments[0])) + } else { + attachments = await Promise.all(params.attachments.map(processAttachment)) } } - - if (params.attachments.length === 1) { - attachments.push(await processAttachment(params.attachments[0])) - } else { - attachments = await Promise.all(params.attachments.map(processAttachment)) + } catch (error) { + errorMeta = { + attachments: { message: error.message }, } } let fromMail = process.env.SENDGRID_FROM_MAIL @@ -83,17 +100,19 @@ async function sendEmail(params) { } try { const res = await sgMail.send(message) - const errorResponse = { + errorResponse = { email: to, response_code: Number(res[0].statusCode), + meta: errorMeta, } await logQueries.createLog(errorResponse) } catch (error) { - const errorResponse = { + errorResponse = { email: to, response_code: Number(error?.code), error: error?.response, status: 'FAILED', + meta: errorMeta, } await logQueries.createLog(errorResponse) if (error.response) { From c98fbf6b3fcbf04d2040410f0bca44b66d0b6c49 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Fri, 3 May 2024 14:42:33 +0530 Subject: [PATCH 4/8] add: api docs and postman collection --- ...torED-Notification.postman_collection.json | 22 ++++++---- src/api-doc/api-doc.yaml | 43 +++++++++++++------ 2 files changed, 45 insertions(+), 20 deletions(-) diff --git a/src/api-doc/MentorED-Notification.postman_collection.json b/src/api-doc/MentorED-Notification.postman_collection.json index 1d1a7da3..00707648 100644 --- a/src/api-doc/MentorED-Notification.postman_collection.json +++ b/src/api-doc/MentorED-Notification.postman_collection.json @@ -1,25 +1,25 @@ { "info": { - "_postman_id": "c208eafb-8d59-4414-8b02-41bde6580ca9", + "_postman_id": "fa88f778-f705-47c6-8d27-7b5f068ced12", "name": "MentorED-Notification", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "7997930" + "_exporter_id": "21498549" }, "item": [ { - "name": "Send Email", + "name": "SendEmail", "request": { "method": "POST", "header": [ { "key": "internal_access_token", - "value": "bsj82AHBxahusub12yexlashsbxAXADHBlaj", + "value": "{{internal_access_token}}", "type": "text" } ], "body": { "mode": "raw", - "raw": "{\n \"type\":\"email\",\n \"email\":{\n \"to\": \"ankitstar00786@gmail.com\",\n\t\t\"subject\": \"MentorED - Reset Otp\",\n\t \"body\": \"

Dear Ankit,

Your OTP to reset your password is 123456. Please enter the OTP to reset your password. For your security, please do not share this OTP with anyone.\"\n }\n}", + "raw": "{\n \"type\": \"email\",\n \"email\": {\n \"to\": \"nevil@tunerlabs.com\",\n \"subject\": \"Testing email logs\",\n \"body\": \"Sample Data\",\n \"attachments\": [\n {\n \"url\": \"https://www.clickdimensions.com/links/TestPDFfile.pdf\",\n \"filename\": \"some-pdf.pdf\",\n \"type\": \"application/pdf\",\n \"disposition\": \"attachment\",\n \"content_id\": \"mytext\"\n },\n {\n \"url\": \"https://sample-videos.com/csv/Sample-Spreadsheet-10-rows.csv\",\n \"filename\": \"some-csv.csv\",\n \"type\": \"application/csv\",\n \"disposition\": \"attachment\",\n \"content_id\": \"mytext\"\n }\n ]\n }\n}", "options": { "raw": { "language": "json" @@ -28,11 +28,17 @@ }, "url": { "raw": "{{notificationBaseUrl}}notification/v1/email/send", - "host": ["{{notificationBaseUrl}}notification"], - "path": ["v1", "email", "send"] + "host": [ + "{{notificationBaseUrl}}notification" + ], + "path": [ + "v1", + "email", + "send" + ] } }, "response": [] } ] -} +} \ No newline at end of file diff --git a/src/api-doc/api-doc.yaml b/src/api-doc/api-doc.yaml index 4d138447..bf2758e9 100644 --- a/src/api-doc/api-doc.yaml +++ b/src/api-doc/api-doc.yaml @@ -1,14 +1,14 @@ ---- openapi: 3.0.0 info: title: Elevate Notification version: 1.0.0 - termsOfService: 'https://github.com/project-sunbird/sunbird-commons/blob/master/LICENSE' + termsOfService: https://github.com/project-sunbird/sunbird-commons/blob/master/LICENSE description: >- - - The Notification Service is a centralized Service to support other services. Apis perform operations related to sending email notification etc + - The Notification Service is a centralized Service to support other + services. Apis perform operations related to sending email notification etc - - The URL for Users API(s) is `{context}/notification/v1` - - Note: These resources can be used in other services + - The URL for Users API(s) is `{context}/notification/v1` - Note: + These resources can be used in other services contact: email: tech-infra@shikshalokam.org servers: @@ -16,9 +16,8 @@ servers: description: local server url - url: https://dev.elevate-apis.shikshalokam.org description: dev server url - paths: - '/notification/v1/email/send': + /notification/v1/email/send: post: summary: Send Email tags: @@ -28,7 +27,6 @@ paths: - Endpoint for sending email `/notification/v1/email/send` - It is mandatory to provide values for parameters marked as `required`. - Mandatory parameters cannot be empty or null. - parameters: - name: internal_access_token in: header @@ -41,20 +39,39 @@ paths: content: application.json: schema: - '$ref': '#/components/schemas/email/emailSendRequest' + $ref: '#/components/schemas/email/emailSendRequest' + examples: + example1: + value: + type: email + email: + to: example@mail.com + cc: ccexample@mail.com + subject: Subject of email + body: |- + Dear Jhon, + Welcome to Notification + attachments: + - url: https://www.clickdimensions.com/links/TestPDFfile.pdf + filename: some-pdf.pdf + type: application/pdf + - url: >- + https://sample-videos.com/csv/Sample-Spreadsheet-10-rows.csv + filename: some-csv.csv + type: application/csv responses: '200': description: OK. Email sent successfully. content: application.json: schema: - '$ref': '#/components/schemas/email/emailSendResponse200' + $ref: '#/components/schemas/email/emailSendResponse200' '400': description: Bad Request. content: application.json: schema: - '$ref': '#/components/schemas/email/emailSendResponse400' + $ref: '#/components/schemas/email/emailSendResponse400' components: schemas: email: @@ -94,7 +111,9 @@ components: body: type: string description: Body of Email. It will accept Html too. - example: "Dear Jhon, \n Welcome to Notification" + example: |- + Dear Jhon, + Welcome to Notification required: true emailSendResponse200: description: Email Sent response From 262091b6a9482b2372c0d6fd3d824683493aef57 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Fri, 3 May 2024 14:44:45 +0530 Subject: [PATCH 5/8] update: jsdoc --- src/generics/helpers/email-notifications.js | 27 ++++++++++----------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/generics/helpers/email-notifications.js b/src/generics/helpers/email-notifications.js index 781122d1..155b397a 100644 --- a/src/generics/helpers/email-notifications.js +++ b/src/generics/helpers/email-notifications.js @@ -11,26 +11,13 @@ sgMail.setApiKey(process.env.SENDGRID_API_KEY) const logQueries = require('../../database/queries/log') const request = require('request') -/** - * Send Email - * @method - * @name sendEmail - * @param {Object} params - contains email information for sending email - * @param {String} params.from - email id of the sender - * @param {String} params.to - email id of the receiver - * @param {String} params.subject - subject of the email - * @param {String} params.body - contains email content - * @param {String} params.cc - contains the cc of the email - * @returns {JSON} Returns response of the email sending information - */ - /** * Fetches a file from a given URL. * @param {Object} fileUrl - The URL object containing information about the file. * @param {string} fileUrl.url - The URL of the file to fetch. * @param {string} fileUrl.filename - The name of the file. * @returns {Promise} A promise that resolves with an object containing the file content and filename. - * @throws {Error} If an error occurs during the file fetch operation. + * @throws {Error} If an error occurs during the file fetch operation, or if the response status code is 400 or greater than or equal to 500. */ async function fetchFileByUrl(fileUrl) { try { @@ -52,6 +39,18 @@ async function fetchFileByUrl(fileUrl) { } } +/** + * Send Email + * @method + * @name sendEmail + * @param {Object} params - contains email information for sending email + * @param {String} params.from - email id of the sender + * @param {String} params.to - email id of the receiver + * @param {String} params.subject - subject of the email + * @param {String} params.body - contains email content + * @param {String} params.cc - contains the cc of the email + * @returns {JSON} Returns response of the email sending information + */ async function sendEmail(params) { try { let attachments = [] From 1714329c7f93d07b636807eb5b4f191c2401927c Mon Sep 17 00:00:00 2001 From: rakeshSgr Date: Wed, 15 May 2024 17:08:38 +0530 Subject: [PATCH 6/8] ansible fix --- deployment/ansible.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/deployment/ansible.yml b/deployment/ansible.yml index 57f9de4a..a8367e07 100644 --- a/deployment/ansible.yml +++ b/deployment/ansible.yml @@ -43,6 +43,7 @@ - name: Delete Old Folder & create folder shell: rm -rf {{ current_path }} && cd {{ project_path }} && mkdir notification + state: absent - name: Move code from release to service folder shell: mv "{{ release_path }}"/* {{ current_path }}/ @@ -64,6 +65,7 @@ - name: Delete release folder shell: rm -rf {{ release_path }} + state: absent - name: Start pm2 command: "chdir={{current_path}}/src pm2 start app.js -i 2 --name elevate-notification" From 776baf8bc1db75c06212e3629410ac7a06ffb7ba Mon Sep 17 00:00:00 2001 From: rakeshSgr Date: Wed, 15 May 2024 17:11:32 +0530 Subject: [PATCH 7/8] ansible fix --- deployment/ansible.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deployment/ansible.yml b/deployment/ansible.yml index a8367e07..2cc3a90a 100644 --- a/deployment/ansible.yml +++ b/deployment/ansible.yml @@ -43,7 +43,6 @@ - name: Delete Old Folder & create folder shell: rm -rf {{ current_path }} && cd {{ project_path }} && mkdir notification - state: absent - name: Move code from release to service folder shell: mv "{{ release_path }}"/* {{ current_path }}/ @@ -64,8 +63,9 @@ ignore_errors: yes - name: Delete release folder - shell: rm -rf {{ release_path }} - state: absent + file: + path: "{{ release_path }}" + state: absent - name: Start pm2 command: "chdir={{current_path}}/src pm2 start app.js -i 2 --name elevate-notification" From 7071c97794d372b746e49b19c5d94e273464cb2b Mon Sep 17 00:00:00 2001 From: rakeshSgr Date: Wed, 15 May 2024 17:37:45 +0530 Subject: [PATCH 8/8] ansible changes --- deployment/ansible.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/deployment/ansible.yml b/deployment/ansible.yml index 2cc3a90a..9ad9fc30 100644 --- a/deployment/ansible.yml +++ b/deployment/ansible.yml @@ -41,8 +41,13 @@ - name: Update npm shell: cd {{release_path}}/src && npm i - - name: Delete Old Folder & create folder - shell: rm -rf {{ current_path }} && cd {{ project_path }} && mkdir notification + - name: Delete Old Folder + file: + path: "{{ current_path }}" + state: absent + + - name: create folder + shell: cd {{ project_path }} && mkdir notification - name: Move code from release to service folder shell: mv "{{ release_path }}"/* {{ current_path }}/