From 0d73d88f35b03e993f305873dc72672003c7d9e5 Mon Sep 17 00:00:00 2001 From: sct Date: Tue, 24 Nov 2020 14:36:31 +0000 Subject: [PATCH] feat: other email notifications for approved/available also adds UI to configure email notifications to frontend --- overseerr-api.yml | 61 +++++ server/lib/notifications/agents/email.ts | 79 +++++- server/lib/settings.ts | 2 + server/routes/settings.ts | 15 ++ .../templates/email/media-request/subject.pug | 2 +- .../Notifications/NotificationsEmail.tsx | 228 ++++++++++++++++++ src/components/Settings/SettingsLayout.tsx | 2 +- .../Settings/SettingsNotifications.tsx | 6 +- .../notifications/{index.tsx => email.tsx} | 5 +- 9 files changed, 390 insertions(+), 10 deletions(-) create mode 100644 src/components/Settings/Notifications/NotificationsEmail.tsx rename src/pages/settings/notifications/{index.tsx => email.tsx} (65%) diff --git a/overseerr-api.yml b/overseerr-api.yml index 2a5d8deb..9921e09f 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -770,6 +770,36 @@ components: properties: webhookUrl: type: string + NotificationEmailSettings: + type: object + properties: + enabled: + type: boolean + example: false + types: + type: number + example: 2 + options: + type: object + properties: + emailFrom: + type: string + example: no-reply@example.com + smtpHost: + type: string + example: 127.0.0.1 + smtpPort: + type: number + example: 465 + secure: + type: boolean + example: false + authUser: + type: string + nullable: true + authPass: + type: string + nullable: true securitySchemes: cookieAuth: @@ -1225,6 +1255,37 @@ paths: nextExecutionTime: type: string example: '2020-09-02T05:02:23.000Z' + /settings/notifications/email: + get: + summary: Return current email notification settings + description: Returns current email notification settings in JSON format + tags: + - settings + responses: + '200': + description: Returned email settings + content: + application/json: + schema: + $ref: '#/components/schemas/NotificationEmailSettings' + post: + summary: Update email notification settings + description: Update current email notification settings with provided values + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/NotificationEmailSettings' + responses: + '200': + description: 'Values were sucessfully updated' + content: + application/json: + schema: + $ref: '#/components/schemas/NotificationEmailSettings' /settings/notifications/discord: get: summary: Return current discord notification settings diff --git a/server/lib/notifications/agents/email.ts b/server/lib/notifications/agents/email.ts index c291af5d..779eee21 100644 --- a/server/lib/notifications/agents/email.ts +++ b/server/lib/notifications/agents/email.ts @@ -35,9 +35,10 @@ class EmailAgent implements NotificationAgent { } private getNewEmail() { + const settings = getSettings().notifications.agents.email; return new Email({ message: { - from: 'no-reply@os.sct.dev', + from: settings.options.emailFrom, }, send: true, transport: this.getSmtpTransport(), @@ -55,9 +56,6 @@ class EmailAgent implements NotificationAgent { .filter((user) => user.hasPermission(Permission.MANAGE_REQUESTS)) .forEach((user) => { const email = this.getNewEmail(); - logger.debug('Sending email notification', { - label: 'Notifications', - }); email.send({ template: path.join( @@ -74,6 +72,7 @@ class EmailAgent implements NotificationAgent { timestamp: new Date().toTimeString(), requestedBy: payload.notifyUser.username, actionUrl: settings.applicationUrl, + requestType: 'New Request', }, }); }); @@ -87,6 +86,72 @@ class EmailAgent implements NotificationAgent { } } + private async sendMediaApprovedEmail(payload: NotificationPayload) { + const settings = getSettings().main; + try { + const email = this.getNewEmail(); + + email.send({ + template: path.join( + __dirname, + '../../../templates/email/media-request' + ), + message: { + to: payload.notifyUser.email, + }, + locals: { + body: 'Your request for the following media has been approved:', + mediaName: payload.subject, + imageUrl: payload.image, + timestamp: new Date().toTimeString(), + requestedBy: payload.notifyUser.username, + actionUrl: settings.applicationUrl, + requestType: 'Request Approved', + }, + }); + return true; + } catch (e) { + logger.error('Mail notification failed to send', { + label: 'Notifications', + message: e.message, + }); + return false; + } + } + + private async sendMediaAvailableEmail(payload: NotificationPayload) { + const settings = getSettings().main; + try { + const email = this.getNewEmail(); + + email.send({ + template: path.join( + __dirname, + '../../../templates/email/media-request' + ), + message: { + to: payload.notifyUser.email, + }, + locals: { + body: 'Your requsested media is now available!', + mediaName: payload.subject, + imageUrl: payload.image, + timestamp: new Date().toTimeString(), + requestedBy: payload.notifyUser.username, + actionUrl: settings.applicationUrl, + requestType: 'Now Available', + }, + }); + return true; + } catch (e) { + logger.error('Mail notification failed to send', { + label: 'Notifications', + message: e.message, + }); + return false; + } + } + public async send( type: Notification, payload: NotificationPayload @@ -97,6 +162,12 @@ class EmailAgent implements NotificationAgent { case Notification.MEDIA_PENDING: this.sendMediaRequestEmail(payload); break; + case Notification.MEDIA_APPROVED: + this.sendMediaApprovedEmail(payload); + break; + case Notification.MEDIA_AVAILABLE: + this.sendMediaAvailableEmail(payload); + break; } return true; diff --git a/server/lib/settings.ts b/server/lib/settings.ts index 00f43048..a490aebe 100644 --- a/server/lib/settings.ts +++ b/server/lib/settings.ts @@ -65,6 +65,7 @@ interface NotificationAgentDiscord extends NotificationAgent { interface NotificationAgentEmail extends NotificationAgent { options: { + emailFrom: string; smtpHost: string; smtpPort: number; secure: boolean; @@ -120,6 +121,7 @@ class Settings { enabled: false, types: 0, options: { + emailFrom: '', smtpHost: '127.0.0.1', smtpPort: 465, secure: false, diff --git a/server/routes/settings.ts b/server/routes/settings.ts index 5cdce2d9..fe082600 100644 --- a/server/routes/settings.ts +++ b/server/routes/settings.ts @@ -364,4 +364,19 @@ settingsRoutes.post('/notifications/discord', (req, res) => { res.status(200).json(settings.notifications.agents.discord); }); +settingsRoutes.get('/notifications/email', (req, res) => { + const settings = getSettings(); + + res.status(200).json(settings.notifications.agents.email); +}); + +settingsRoutes.post('/notifications/email', (req, res) => { + const settings = getSettings(); + + settings.notifications.agents.email = req.body; + settings.save(); + + res.status(200).json(settings.notifications.agents.email); +}); + export default settingsRoutes; diff --git a/server/templates/email/media-request/subject.pug b/server/templates/email/media-request/subject.pug index 4ff70bff..02046bd5 100644 --- a/server/templates/email/media-request/subject.pug +++ b/server/templates/email/media-request/subject.pug @@ -1 +1 @@ -= `New Request: ${mediaName} - Overseerr` += `${requestType}: ${mediaName} - Overseerr` diff --git a/src/components/Settings/Notifications/NotificationsEmail.tsx b/src/components/Settings/Notifications/NotificationsEmail.tsx new file mode 100644 index 00000000..8a42657a --- /dev/null +++ b/src/components/Settings/Notifications/NotificationsEmail.tsx @@ -0,0 +1,228 @@ +import React, { useState } from 'react'; +import { Field, Form, Formik } from 'formik'; +import useSWR from 'swr'; +import LoadingSpinner from '../../Common/LoadingSpinner'; +import Button from '../../Common/Button'; +import { defineMessages, useIntl } from 'react-intl'; +import Axios from 'axios'; +import * as Yup from 'yup'; + +const messages = defineMessages({ + save: 'Save Changes', + saving: 'Saving...', +}); + +const NotificationsEmail: React.FC = () => { + const intl = useIntl(); + const { data, error, revalidate } = useSWR( + '/api/v1/settings/notifications/email' + ); + + const NotificationsDiscordSchema = Yup.object().shape({ + emailFrom: Yup.string().required( + 'You must provide an email sender address' + ), + smtpHost: Yup.string().required('You must provide an SMTP host'), + smtpPort: Yup.number().required('You must provide an SMTP port'), + }); + + if (!data && !error) { + return ; + } + + return ( + { + try { + await Axios.post('/api/v1/settings/notifications/email', { + enabled: values.enabled, + types: values.types, + options: { + emailFrom: values.emailFrom, + smtpHost: values.smtpHost, + smtpPort: values.smtpPort, + secure: values.secure, + authUser: values.authUser, + authPass: values.authPass, + }, + }); + } catch (e) { + // TODO show error + } finally { + revalidate(); + } + }} + > + {({ errors, touched, isSubmitting }) => { + return ( +
+
+ +
+ +
+
+
+ +
+
+ +
+ {errors.emailFrom && touched.emailFrom && ( +
{errors.emailFrom}
+ )} +
+
+
+ +
+
+ +
+ {errors.smtpHost && touched.smtpHost && ( +
{errors.smtpHost}
+ )} +
+
+
+ +
+
+ +
+ {errors.smtpPort && touched.smtpPort && ( +
{errors.smtpPort}
+ )} +
+
+
+ +
+ +
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+
+ + + +
+
+
+ ); + }} +
+ ); +}; + +export default NotificationsEmail; diff --git a/src/components/Settings/SettingsLayout.tsx b/src/components/Settings/SettingsLayout.tsx index 94cda2a3..650720fa 100644 --- a/src/components/Settings/SettingsLayout.tsx +++ b/src/components/Settings/SettingsLayout.tsx @@ -26,7 +26,7 @@ const settingsRoutes: SettingsRoute[] = [ }, { text: 'Notifications', - route: '/settings/notifications', + route: '/settings/notifications/email', regex: /^\/settings\/notifications/, }, { diff --git a/src/components/Settings/SettingsNotifications.tsx b/src/components/Settings/SettingsNotifications.tsx index bd533d39..7b06772f 100644 --- a/src/components/Settings/SettingsNotifications.tsx +++ b/src/components/Settings/SettingsNotifications.tsx @@ -10,9 +10,9 @@ interface SettingsRoute { const settingsRoutes: SettingsRoute[] = [ { - text: 'General', - route: '/settings/notifications', - regex: /^\/settings\/notifications$/, + text: 'Email', + route: '/settings/notifications/email', + regex: /^\/settings\/notifications\/email/, }, { text: 'Discord', diff --git a/src/pages/settings/notifications/index.tsx b/src/pages/settings/notifications/email.tsx similarity index 65% rename from src/pages/settings/notifications/index.tsx rename to src/pages/settings/notifications/email.tsx index 10061c71..8f2fcd92 100644 --- a/src/pages/settings/notifications/index.tsx +++ b/src/pages/settings/notifications/email.tsx @@ -2,11 +2,14 @@ import { NextPage } from 'next'; import React from 'react'; import SettingsLayout from '../../../components/Settings/SettingsLayout'; import SettingsNotifications from '../../../components/Settings/SettingsNotifications'; +import NotificationsEmail from '../../../components/Settings/Notifications/NotificationsEmail'; const NotificationsPage: NextPage = () => { return ( - N/A + + + ); };