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/deployment/ansible.yml b/deployment/ansible.yml index 57f9de4a..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 }}/ @@ -63,7 +68,9 @@ ignore_errors: yes - name: Delete release folder - shell: rm -rf {{ release_path }} + file: + path: "{{ release_path }}" + state: absent - name: Start pm2 command: "chdir={{current_path}}/src pm2 start app.js -i 2 --name elevate-notification" 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 diff --git a/src/generics/helpers/email-notifications.js b/src/generics/helpers/email-notifications.js index 16e6586a..155b397a 100644 --- a/src/generics/helpers/email-notifications.js +++ b/src/generics/helpers/email-notifications.js @@ -9,6 +9,35 @@ const sgMail = require('@sendgrid/mail') sgMail.setApiKey(process.env.SENDGRID_API_KEY) const logQueries = require('../../database/queries/log') +const request = require('request') + +/** + * 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, or if the response status code is 400 or greater than or equal to 500. + */ +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 }) + } + }) + }) + return response + } catch (error) { + throw new Error('Error fetching file: ' + error.message) + } +} /** * Send Email @@ -24,7 +53,32 @@ const logQueries = require('../../database/queries/log') */ 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.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 + if (params.from) { fromMail = params.from } @@ -35,6 +89,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(',') @@ -44,17 +99,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) { @@ -66,6 +123,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', 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') + } }, }