feat(email): add pgp support (#1138)
This commit is contained in:
@@ -42,6 +42,7 @@
|
|||||||
"node-schedule": "^2.0.0",
|
"node-schedule": "^2.0.0",
|
||||||
"nodemailer": "^6.5.0",
|
"nodemailer": "^6.5.0",
|
||||||
"nookies": "^2.5.2",
|
"nookies": "^2.5.2",
|
||||||
|
"openpgp": "^5.0.0-1",
|
||||||
"plex-api": "^5.3.1",
|
"plex-api": "^5.3.1",
|
||||||
"pug": "^3.0.2",
|
"pug": "^3.0.2",
|
||||||
"react": "17.0.1",
|
"react": "17.0.1",
|
||||||
|
|||||||
@@ -37,4 +37,7 @@ export class UserSettings {
|
|||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
public originalLanguage?: string;
|
public originalLanguage?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
public pgpKey?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,4 +10,5 @@ export interface UserSettingsNotificationsResponse {
|
|||||||
discordId?: string;
|
discordId?: string;
|
||||||
telegramChatId?: string;
|
telegramChatId?: string;
|
||||||
telegramSendSilently?: boolean;
|
telegramSendSilently?: boolean;
|
||||||
|
pgpKey?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import nodemailer from 'nodemailer';
|
import nodemailer from 'nodemailer';
|
||||||
import Email from 'email-templates';
|
import Email from 'email-templates';
|
||||||
import { getSettings } from '../settings';
|
import { getSettings } from '../settings';
|
||||||
|
import { openpgpEncrypt } from './openpgpEncrypt';
|
||||||
class PreparedEmail extends Email {
|
class PreparedEmail extends Email {
|
||||||
public constructor() {
|
public constructor(pgpKey?: string) {
|
||||||
const settings = getSettings().notifications.agents.email;
|
const settings = getSettings().notifications.agents.email;
|
||||||
|
|
||||||
const transport = nodemailer.createTransport({
|
const transport = nodemailer.createTransport({
|
||||||
@@ -22,6 +23,16 @@ class PreparedEmail extends Email {
|
|||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
|
if (pgpKey) {
|
||||||
|
transport.use(
|
||||||
|
'stream',
|
||||||
|
openpgpEncrypt({
|
||||||
|
signingKey: settings.options.pgpPrivateKey,
|
||||||
|
password: settings.options.pgpPassword,
|
||||||
|
encryptionKeys: [pgpKey],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
super({
|
super({
|
||||||
message: {
|
message: {
|
||||||
from: {
|
from: {
|
||||||
|
|||||||
181
server/lib/email/openpgpEncrypt.ts
Normal file
181
server/lib/email/openpgpEncrypt.ts
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import * as openpgp from 'openpgp';
|
||||||
|
import { Transform, TransformCallback } from 'stream';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
|
interface EncryptorOptions {
|
||||||
|
signingKey?: string;
|
||||||
|
password?: string;
|
||||||
|
encryptionKeys: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
class PGPEncryptor extends Transform {
|
||||||
|
private _messageChunks: Uint8Array[] = [];
|
||||||
|
private _messageLength = 0;
|
||||||
|
private _signingKey?: string;
|
||||||
|
private _password?: string;
|
||||||
|
|
||||||
|
private _encryptionKeys: string[];
|
||||||
|
|
||||||
|
constructor(options: EncryptorOptions) {
|
||||||
|
super();
|
||||||
|
this._signingKey = options.signingKey;
|
||||||
|
this._password = options.password;
|
||||||
|
this._encryptionKeys = options.encryptionKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
// just save the whole message
|
||||||
|
_transform = (
|
||||||
|
chunk: any,
|
||||||
|
_encoding: BufferEncoding,
|
||||||
|
callback: TransformCallback
|
||||||
|
): void => {
|
||||||
|
this._messageChunks.push(chunk);
|
||||||
|
this._messageLength += chunk.length;
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Actually do stuff
|
||||||
|
_flush = async (callback: TransformCallback): Promise<void> => {
|
||||||
|
// Reconstruct message as buffer
|
||||||
|
const message = Buffer.concat(this._messageChunks, this._messageLength);
|
||||||
|
const validPublicKeys = await Promise.all(
|
||||||
|
this._encryptionKeys.map((armoredKey) => openpgp.readKey({ armoredKey }))
|
||||||
|
);
|
||||||
|
let privateKey: openpgp.Key | undefined;
|
||||||
|
|
||||||
|
// Just return the message if there is no one to encrypt for
|
||||||
|
if (!validPublicKeys.length) {
|
||||||
|
this.push(message);
|
||||||
|
return callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only sign the message if private key and password exist
|
||||||
|
if (this._signingKey && this._password) {
|
||||||
|
privateKey = await openpgp.readKey({
|
||||||
|
armoredKey: this._signingKey,
|
||||||
|
});
|
||||||
|
await privateKey.decrypt(this._password);
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailPartDelimiter = '\r\n\r\n';
|
||||||
|
const messageParts = message.toString().split(emailPartDelimiter);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In this loop original headers are split up into two parts,
|
||||||
|
* one for the email that is sent
|
||||||
|
* and one for the encrypted content
|
||||||
|
*/
|
||||||
|
const header = messageParts.shift() as string;
|
||||||
|
const emailHeaders: string[][] = [];
|
||||||
|
const contentHeaders: string[][] = [];
|
||||||
|
const linesInHeader = header.split('\r\n');
|
||||||
|
let previousHeader: string[] = [];
|
||||||
|
for (let i = 0; i < linesInHeader.length; i++) {
|
||||||
|
const line = linesInHeader[i];
|
||||||
|
/**
|
||||||
|
* If it is a multi-line header (current line starts with whitespace)
|
||||||
|
* or it's the first line in the iteration
|
||||||
|
* add the current line with previous header and move on
|
||||||
|
*/
|
||||||
|
if (/^\s/.test(line) || i === 0) {
|
||||||
|
previousHeader.push(line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is done to prevent the last header
|
||||||
|
* from being missed
|
||||||
|
*/
|
||||||
|
if (i === linesInHeader.length - 1) {
|
||||||
|
previousHeader.push(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We need to seperate the actual content headers
|
||||||
|
* so that we can add it as a header for the encrypted content
|
||||||
|
* So that the content will be displayed properly after decryption
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
/^(content-type|content-transfer-encoding):/i.test(previousHeader[0])
|
||||||
|
) {
|
||||||
|
contentHeaders.push(previousHeader);
|
||||||
|
} else {
|
||||||
|
emailHeaders.push(previousHeader);
|
||||||
|
}
|
||||||
|
previousHeader = [line];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a new boundary for the email content
|
||||||
|
const boundary = 'nm_' + crypto.randomBytes(14).toString('hex');
|
||||||
|
/**
|
||||||
|
* Concatenate everything into single strings
|
||||||
|
* and add pgp headers to the email headers
|
||||||
|
*/
|
||||||
|
const emailHeadersRaw =
|
||||||
|
emailHeaders.map((line) => line.join('\r\n')).join('\r\n') +
|
||||||
|
'\r\n' +
|
||||||
|
'Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";' +
|
||||||
|
'\r\n' +
|
||||||
|
' boundary="' +
|
||||||
|
boundary +
|
||||||
|
'"' +
|
||||||
|
'\r\n' +
|
||||||
|
'Content-Description: OpenPGP encrypted message' +
|
||||||
|
'\r\n' +
|
||||||
|
'Content-Transfer-Encoding: 7bit';
|
||||||
|
const contentHeadersRaw = contentHeaders
|
||||||
|
.map((line) => line.join('\r\n'))
|
||||||
|
.join('\r\n');
|
||||||
|
|
||||||
|
const encryptedMessage = await openpgp.encrypt({
|
||||||
|
message: openpgp.Message.fromText(
|
||||||
|
contentHeadersRaw +
|
||||||
|
emailPartDelimiter +
|
||||||
|
messageParts.join(emailPartDelimiter)
|
||||||
|
),
|
||||||
|
publicKeys: validPublicKeys,
|
||||||
|
privateKeys: privateKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
const body =
|
||||||
|
'--' +
|
||||||
|
boundary +
|
||||||
|
'\r\n' +
|
||||||
|
'Content-Type: application/pgp-encrypted\r\n' +
|
||||||
|
'Content-Transfer-Encoding: 7bit\r\n' +
|
||||||
|
'\r\n' +
|
||||||
|
'Version: 1\r\n' +
|
||||||
|
'\r\n' +
|
||||||
|
'--' +
|
||||||
|
boundary +
|
||||||
|
'\r\n' +
|
||||||
|
'Content-Type: application/octet-stream; name=encrypted.asc\r\n' +
|
||||||
|
'Content-Disposition: inline; filename=encrypted.asc\r\n' +
|
||||||
|
'Content-Transfer-Encoding: 7bit\r\n' +
|
||||||
|
'\r\n' +
|
||||||
|
encryptedMessage +
|
||||||
|
'\r\n--' +
|
||||||
|
boundary +
|
||||||
|
'--\r\n';
|
||||||
|
|
||||||
|
this.push(Buffer.from(emailHeadersRaw + emailPartDelimiter + body));
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const openpgpEncrypt = (options: EncryptorOptions) => {
|
||||||
|
return function (mail: any, callback: () => unknown): void {
|
||||||
|
if (!options.encryptionKeys.length) {
|
||||||
|
setImmediate(callback);
|
||||||
|
}
|
||||||
|
mail.message.transform(
|
||||||
|
() =>
|
||||||
|
new PGPEncryptor({
|
||||||
|
signingKey: options.signingKey,
|
||||||
|
password: options.password,
|
||||||
|
encryptionKeys: options.encryptionKeys,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setImmediate(callback);
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -47,7 +47,7 @@ class EmailAgent
|
|||||||
users
|
users
|
||||||
.filter((user) => user.hasPermission(Permission.MANAGE_REQUESTS))
|
.filter((user) => user.hasPermission(Permission.MANAGE_REQUESTS))
|
||||||
.forEach((user) => {
|
.forEach((user) => {
|
||||||
const email = new PreparedEmail();
|
const email = new PreparedEmail(payload.notifyUser.settings?.pgpKey);
|
||||||
|
|
||||||
email.send({
|
email.send({
|
||||||
template: path.join(
|
template: path.join(
|
||||||
@@ -97,7 +97,7 @@ class EmailAgent
|
|||||||
users
|
users
|
||||||
.filter((user) => user.hasPermission(Permission.MANAGE_REQUESTS))
|
.filter((user) => user.hasPermission(Permission.MANAGE_REQUESTS))
|
||||||
.forEach((user) => {
|
.forEach((user) => {
|
||||||
const email = new PreparedEmail();
|
const email = new PreparedEmail(payload.notifyUser.settings?.pgpKey);
|
||||||
|
|
||||||
email.send({
|
email.send({
|
||||||
template: path.join(
|
template: path.join(
|
||||||
@@ -142,7 +142,7 @@ class EmailAgent
|
|||||||
// This is getting main settings for the whole app
|
// This is getting main settings for the whole app
|
||||||
const { applicationUrl, applicationTitle } = getSettings().main;
|
const { applicationUrl, applicationTitle } = getSettings().main;
|
||||||
try {
|
try {
|
||||||
const email = new PreparedEmail();
|
const email = new PreparedEmail(payload.notifyUser.settings?.pgpKey);
|
||||||
|
|
||||||
await email.send({
|
await email.send({
|
||||||
template: path.join(
|
template: path.join(
|
||||||
@@ -234,7 +234,7 @@ class EmailAgent
|
|||||||
// This is getting main settings for the whole app
|
// This is getting main settings for the whole app
|
||||||
const { applicationUrl, applicationTitle } = getSettings().main;
|
const { applicationUrl, applicationTitle } = getSettings().main;
|
||||||
try {
|
try {
|
||||||
const email = new PreparedEmail();
|
const email = new PreparedEmail(payload.notifyUser.settings?.pgpKey);
|
||||||
|
|
||||||
await email.send({
|
await email.send({
|
||||||
template: path.join(
|
template: path.join(
|
||||||
@@ -276,7 +276,7 @@ class EmailAgent
|
|||||||
// This is getting main settings for the whole app
|
// This is getting main settings for the whole app
|
||||||
const { applicationUrl, applicationTitle } = getSettings().main;
|
const { applicationUrl, applicationTitle } = getSettings().main;
|
||||||
try {
|
try {
|
||||||
const email = new PreparedEmail();
|
const email = new PreparedEmail(payload.notifyUser.settings?.pgpKey);
|
||||||
|
|
||||||
await email.send({
|
await email.send({
|
||||||
template: path.join(
|
template: path.join(
|
||||||
@@ -318,7 +318,7 @@ class EmailAgent
|
|||||||
// This is getting main settings for the whole app
|
// This is getting main settings for the whole app
|
||||||
const { applicationUrl, applicationTitle } = getSettings().main;
|
const { applicationUrl, applicationTitle } = getSettings().main;
|
||||||
try {
|
try {
|
||||||
const email = new PreparedEmail();
|
const email = new PreparedEmail(payload.notifyUser.settings?.pgpKey);
|
||||||
|
|
||||||
await email.send({
|
await email.send({
|
||||||
template: path.join(__dirname, '../../../templates/email/test-email'),
|
template: path.join(__dirname, '../../../templates/email/test-email'),
|
||||||
|
|||||||
@@ -117,6 +117,8 @@ export interface NotificationAgentEmail extends NotificationAgentConfig {
|
|||||||
authPass?: string;
|
authPass?: string;
|
||||||
allowSelfSigned: boolean;
|
allowSelfSigned: boolean;
|
||||||
senderName: string;
|
senderName: string;
|
||||||
|
pgpPrivateKey?: string;
|
||||||
|
pgpPassword?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
31
server/migration/1615333940450-AddPGPToUserSettings.ts
Normal file
31
server/migration/1615333940450-AddPGPToUserSettings.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddPGPToUserSettings1615333940450 implements MigrationInterface {
|
||||||
|
name = 'AddPGPToUserSettings1615333940450';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "enableNotifications" boolean NOT NULL DEFAULT (1), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "temporary_user_settings"("id", "enableNotifications", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently") SELECT "id", "enableNotifications", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently" FROM "user_settings"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "user_settings"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "temporary_user_settings" RENAME TO "user_settings"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "user_settings" RENAME TO "temporary_user_settings"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "enableNotifications" boolean NOT NULL DEFAULT (1), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "user_settings"("id", "enableNotifications", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently") SELECT "id", "enableNotifications", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently" FROM "temporary_user_settings"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "temporary_user_settings"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -225,6 +225,7 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>(
|
|||||||
discordId: user.settings?.discordId,
|
discordId: user.settings?.discordId,
|
||||||
telegramChatId: user.settings?.telegramChatId,
|
telegramChatId: user.settings?.telegramChatId,
|
||||||
telegramSendSilently: user?.settings?.telegramSendSilently,
|
telegramSendSilently: user?.settings?.telegramSendSilently,
|
||||||
|
pgpKey: user?.settings?.pgpKey,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
next({ status: 500, message: e.message });
|
next({ status: 500, message: e.message });
|
||||||
@@ -263,12 +264,14 @@ userSettingsRoutes.post<
|
|||||||
discordId: req.body.discordId,
|
discordId: req.body.discordId,
|
||||||
telegramChatId: req.body.telegramChatId,
|
telegramChatId: req.body.telegramChatId,
|
||||||
telegramSendSilently: req.body.telegramSendSilently,
|
telegramSendSilently: req.body.telegramSendSilently,
|
||||||
|
pgpKey: req.body.pgpKey,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
user.settings.enableNotifications = req.body.enableNotifications;
|
user.settings.enableNotifications = req.body.enableNotifications;
|
||||||
user.settings.discordId = req.body.discordId;
|
user.settings.discordId = req.body.discordId;
|
||||||
user.settings.telegramChatId = req.body.telegramChatId;
|
user.settings.telegramChatId = req.body.telegramChatId;
|
||||||
user.settings.telegramSendSilently = req.body.telegramSendSilently;
|
user.settings.telegramSendSilently = req.body.telegramSendSilently;
|
||||||
|
user.settings.pgpKey = req.body.pgpKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
@@ -278,6 +281,7 @@ userSettingsRoutes.post<
|
|||||||
discordId: user.settings.discordId,
|
discordId: user.settings.discordId,
|
||||||
telegramChatId: user.settings.telegramChatId,
|
telegramChatId: user.settings.telegramChatId,
|
||||||
telegramSendSilently: user.settings.telegramSendSilently,
|
telegramSendSilently: user.settings.telegramSendSilently,
|
||||||
|
pgpKey: user.settings.pgpKey,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
next({ status: 500, message: e.message });
|
next({ status: 500, message: e.message });
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import * as Yup from 'yup';
|
|||||||
import { useToasts } from 'react-toast-notifications';
|
import { useToasts } from 'react-toast-notifications';
|
||||||
import NotificationTypeSelector from '../../NotificationTypeSelector';
|
import NotificationTypeSelector from '../../NotificationTypeSelector';
|
||||||
import Alert from '../../Common/Alert';
|
import Alert from '../../Common/Alert';
|
||||||
|
import Badge from '../../Common/Badge';
|
||||||
|
import globalMessages from '../../../i18n/globalMessages';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
save: 'Save Changes',
|
save: 'Save Changes',
|
||||||
@@ -39,8 +41,27 @@ const messages = defineMessages({
|
|||||||
emailNotificationTypesAlertDescriptionPt2:
|
emailNotificationTypesAlertDescriptionPt2:
|
||||||
'<strong>Media Approved</strong>, <strong>Media Declined</strong>, and <strong>Media Available</strong>\
|
'<strong>Media Approved</strong>, <strong>Media Declined</strong>, and <strong>Media Available</strong>\
|
||||||
email notifications are sent to the user who submitted the request.',
|
email notifications are sent to the user who submitted the request.',
|
||||||
|
pgpPrivateKey: '<PgpLink>PGP</PgpLink> Private Key',
|
||||||
|
pgpPrivateKeyTip:
|
||||||
|
'Sign encrypted email messages (PGP password is also required)',
|
||||||
|
pgpPassword: '<PgpLink>PGP</PgpLink> Password',
|
||||||
|
pgpPasswordTip:
|
||||||
|
'Sign encrypted email messages (PGP private key is also required)',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function PgpLink(msg: string): JSX.Element {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href="https://www.openpgp.org/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="text-gray-100 underline transition duration-300 hover:text-white"
|
||||||
|
>
|
||||||
|
{msg}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const NotificationsEmail: React.FC = () => {
|
const NotificationsEmail: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { addToast } = useToasts();
|
const { addToast } = useToasts();
|
||||||
@@ -77,6 +98,8 @@ const NotificationsEmail: React.FC = () => {
|
|||||||
authPass: data.options.authPass,
|
authPass: data.options.authPass,
|
||||||
allowSelfSigned: data.options.allowSelfSigned,
|
allowSelfSigned: data.options.allowSelfSigned,
|
||||||
senderName: data.options.senderName,
|
senderName: data.options.senderName,
|
||||||
|
pgpPrivateKey: data.options.pgpPrivateKey,
|
||||||
|
pgpPassword: data.options.pgpPassword,
|
||||||
}}
|
}}
|
||||||
validationSchema={NotificationsEmailSchema}
|
validationSchema={NotificationsEmailSchema}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
@@ -93,6 +116,8 @@ const NotificationsEmail: React.FC = () => {
|
|||||||
authPass: values.authPass,
|
authPass: values.authPass,
|
||||||
allowSelfSigned: values.allowSelfSigned,
|
allowSelfSigned: values.allowSelfSigned,
|
||||||
senderName: values.senderName,
|
senderName: values.senderName,
|
||||||
|
pgpPrivateKey: values.pgpPrivateKey,
|
||||||
|
pgpPassword: values.pgpPassword,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
addToast(intl.formatMessage(messages.emailsettingssaved), {
|
addToast(intl.formatMessage(messages.emailsettingssaved), {
|
||||||
@@ -122,6 +147,8 @@ const NotificationsEmail: React.FC = () => {
|
|||||||
authUser: values.authUser,
|
authUser: values.authUser,
|
||||||
authPass: values.authPass,
|
authPass: values.authPass,
|
||||||
senderName: values.senderName,
|
senderName: values.senderName,
|
||||||
|
pgpPrivateKey: values.pgpPrivateKey,
|
||||||
|
pgpPassword: values.pgpPassword,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -291,6 +318,56 @@ const NotificationsEmail: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label htmlFor="pgpPrivateKey" className="text-label">
|
||||||
|
<span className="mr-2">
|
||||||
|
{intl.formatMessage(messages.pgpPrivateKey, {
|
||||||
|
PgpLink: PgpLink,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<Badge badgeType="danger">
|
||||||
|
{intl.formatMessage(globalMessages.advanced)}
|
||||||
|
</Badge>
|
||||||
|
<span className="label-tip">
|
||||||
|
{intl.formatMessage(messages.pgpPrivateKeyTip)}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div className="form-input">
|
||||||
|
<div className="form-input-field">
|
||||||
|
<Field
|
||||||
|
id="pgpPrivateKey"
|
||||||
|
name="pgpPrivateKey"
|
||||||
|
as="textarea"
|
||||||
|
rows="3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label htmlFor="pgpPassword" className="text-label">
|
||||||
|
<span className="mr-2">
|
||||||
|
{intl.formatMessage(messages.pgpPassword, {
|
||||||
|
PgpLink: PgpLink,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<Badge badgeType="danger">
|
||||||
|
{intl.formatMessage(globalMessages.advanced)}
|
||||||
|
</Badge>
|
||||||
|
<span className="label-tip">
|
||||||
|
{intl.formatMessage(messages.pgpPasswordTip)}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div className="form-input">
|
||||||
|
<div className="form-input-field">
|
||||||
|
<Field
|
||||||
|
id="pgpPassword"
|
||||||
|
name="pgpPassword"
|
||||||
|
type="password"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
role="group"
|
role="group"
|
||||||
aria-labelledby="group-label"
|
aria-labelledby="group-label"
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import Button from '../../../Common/Button';
|
|||||||
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
||||||
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
|
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
|
import Badge from '../../../Common/Badge';
|
||||||
|
import globalMessages from '../../../../i18n/globalMessages';
|
||||||
|
import { PgpLink } from '../../../Settings/Notifications/NotificationsEmail';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
notificationsettings: 'Notification Settings',
|
notificationsettings: 'Notification Settings',
|
||||||
@@ -32,6 +35,8 @@ const messages = defineMessages({
|
|||||||
localuser: 'Local User',
|
localuser: 'Local User',
|
||||||
toastSettingsSuccess: 'Settings successfully saved!',
|
toastSettingsSuccess: 'Settings successfully saved!',
|
||||||
toastSettingsFailure: 'Something went wrong while saving settings.',
|
toastSettingsFailure: 'Something went wrong while saving settings.',
|
||||||
|
pgpKey: '<PgpLink>PGP</PgpLink> Public Key',
|
||||||
|
pgpKeyTip: 'Encrypt email messages',
|
||||||
});
|
});
|
||||||
|
|
||||||
const UserNotificationSettings: React.FC = () => {
|
const UserNotificationSettings: React.FC = () => {
|
||||||
@@ -76,6 +81,7 @@ const UserNotificationSettings: React.FC = () => {
|
|||||||
discordId: data?.discordId,
|
discordId: data?.discordId,
|
||||||
telegramChatId: data?.telegramChatId,
|
telegramChatId: data?.telegramChatId,
|
||||||
telegramSendSilently: data?.telegramSendSilently,
|
telegramSendSilently: data?.telegramSendSilently,
|
||||||
|
pgpKey: data?.pgpKey,
|
||||||
}}
|
}}
|
||||||
validationSchema={UserNotificationSettingsSchema}
|
validationSchema={UserNotificationSettingsSchema}
|
||||||
enableReinitialize
|
enableReinitialize
|
||||||
@@ -88,6 +94,7 @@ const UserNotificationSettings: React.FC = () => {
|
|||||||
discordId: values.discordId,
|
discordId: values.discordId,
|
||||||
telegramChatId: values.telegramChatId,
|
telegramChatId: values.telegramChatId,
|
||||||
telegramSendSilently: values.telegramSendSilently,
|
telegramSendSilently: values.telegramSendSilently,
|
||||||
|
pgpKey: values.pgpKey,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -123,6 +130,29 @@ const UserNotificationSettings: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label htmlFor="pgpKey" className="text-label">
|
||||||
|
<span className="mr-2">
|
||||||
|
{intl.formatMessage(messages.pgpKey, {
|
||||||
|
PgpLink: PgpLink,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<Badge badgeType="danger">
|
||||||
|
{intl.formatMessage(globalMessages.advanced)}
|
||||||
|
</Badge>
|
||||||
|
<span className="label-tip">
|
||||||
|
{intl.formatMessage(messages.pgpKeyTip)}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div className="form-input">
|
||||||
|
<div className="form-input-field">
|
||||||
|
<Field id="pgpKey" name="pgpKey" as="textarea" rows="3" />
|
||||||
|
</div>
|
||||||
|
{errors.pgpKey && touched.pgpKey && (
|
||||||
|
<div className="error">{errors.pgpKey}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label htmlFor="discordId" className="text-label">
|
<label htmlFor="discordId" className="text-label">
|
||||||
<span>{intl.formatMessage(messages.discordId)}</span>
|
<span>{intl.formatMessage(messages.discordId)}</span>
|
||||||
|
|||||||
@@ -328,6 +328,10 @@
|
|||||||
"components.Settings.Notifications.emailsettingssaved": "Email notification settings saved successfully!",
|
"components.Settings.Notifications.emailsettingssaved": "Email notification settings saved successfully!",
|
||||||
"components.Settings.Notifications.enableSsl": "Enable SSL",
|
"components.Settings.Notifications.enableSsl": "Enable SSL",
|
||||||
"components.Settings.Notifications.notificationtypes": "Notification Types",
|
"components.Settings.Notifications.notificationtypes": "Notification Types",
|
||||||
|
"components.Settings.Notifications.pgpPassword": "<PgpLink>PGP</PgpLink> Password",
|
||||||
|
"components.Settings.Notifications.pgpPasswordTip": "Sign encrypted email messages (PGP private key is also required)",
|
||||||
|
"components.Settings.Notifications.pgpPrivateKey": "<PgpLink>PGP</PgpLink> Private Key",
|
||||||
|
"components.Settings.Notifications.pgpPrivateKeyTip": "Sign encrypted email messages (PGP password is also required)",
|
||||||
"components.Settings.Notifications.save": "Save Changes",
|
"components.Settings.Notifications.save": "Save Changes",
|
||||||
"components.Settings.Notifications.saving": "Saving…",
|
"components.Settings.Notifications.saving": "Saving…",
|
||||||
"components.Settings.Notifications.sendSilently": "Send Silently",
|
"components.Settings.Notifications.sendSilently": "Send Silently",
|
||||||
@@ -736,6 +740,8 @@
|
|||||||
"components.UserProfile.UserSettings.UserNotificationSettings.enableNotifications": "Enable Notifications",
|
"components.UserProfile.UserSettings.UserNotificationSettings.enableNotifications": "Enable Notifications",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.localuser": "Local User",
|
"components.UserProfile.UserSettings.UserNotificationSettings.localuser": "Local User",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.notificationsettings": "Notification Settings",
|
"components.UserProfile.UserSettings.UserNotificationSettings.notificationsettings": "Notification Settings",
|
||||||
|
"components.UserProfile.UserSettings.UserNotificationSettings.pgpKey": "<PgpLink>PGP</PgpLink> Public Key",
|
||||||
|
"components.UserProfile.UserSettings.UserNotificationSettings.pgpKeyTip": "Encrypt email messages",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.plexuser": "Plex User",
|
"components.UserProfile.UserSettings.UserNotificationSettings.plexuser": "Plex User",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.save": "Save Changes",
|
"components.UserProfile.UserSettings.UserNotificationSettings.save": "Save Changes",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.saving": "Saving…",
|
"components.UserProfile.UserSettings.UserNotificationSettings.saving": "Saving…",
|
||||||
@@ -782,6 +788,7 @@
|
|||||||
"components.UserProfile.UserSettings.settings": "User Settings",
|
"components.UserProfile.UserSettings.settings": "User Settings",
|
||||||
"components.UserProfile.UserSettings.unauthorized": "Unauthorized",
|
"components.UserProfile.UserSettings.unauthorized": "Unauthorized",
|
||||||
"components.UserProfile.UserSettings.unauthorizedDescription": "You do not have permission to modify this user's settings.",
|
"components.UserProfile.UserSettings.unauthorizedDescription": "You do not have permission to modify this user's settings.",
|
||||||
|
"components.UserProfile.norequests": "No Requests",
|
||||||
"components.UserProfile.recentrequests": "Recent Requests",
|
"components.UserProfile.recentrequests": "Recent Requests",
|
||||||
"i18n.advanced": "Advanced",
|
"i18n.advanced": "Advanced",
|
||||||
"i18n.approve": "Approve",
|
"i18n.approve": "Approve",
|
||||||
|
|||||||
@@ -94,6 +94,10 @@ img.avatar-sm {
|
|||||||
@apply flex max-w-lg rounded-md shadow-sm;
|
@apply flex max-w-lg rounded-md shadow-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
@apply flex-1 block w-full min-w-0 text-white transition duration-150 ease-in-out bg-gray-700 border border-gray-500 rounded-md form-input sm:text-sm sm:leading-5;
|
||||||
|
}
|
||||||
|
|
||||||
.label-required {
|
.label-required {
|
||||||
@apply text-red-500;
|
@apply text-red-500;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3040,7 +3040,7 @@ asap@^2.0.0, asap@~2.0.3:
|
|||||||
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
|
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
|
||||||
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
|
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
|
||||||
|
|
||||||
asn1.js@^5.2.0:
|
asn1.js@^5.0.0, asn1.js@^5.2.0:
|
||||||
version "5.4.1"
|
version "5.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07"
|
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07"
|
||||||
integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==
|
integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==
|
||||||
@@ -10288,6 +10288,13 @@ opener@^1.5.1:
|
|||||||
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
|
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
|
||||||
integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==
|
integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==
|
||||||
|
|
||||||
|
openpgp@^5.0.0-1:
|
||||||
|
version "5.0.0-1"
|
||||||
|
resolved "https://registry.yarnpkg.com/openpgp/-/openpgp-5.0.0-1.tgz#28456d930895483770f099e373045e3ce9dbf9f9"
|
||||||
|
integrity sha512-yfmRStdmOQPZbNbvwyQrqjLOTGW4QO0/aok/Vt08Zhf4UB9w0tGA5c6zBxDefxq+SmXlqEsmdNu+AtYx5G8D6A==
|
||||||
|
dependencies:
|
||||||
|
asn1.js "^5.0.0"
|
||||||
|
|
||||||
optionator@^0.9.1:
|
optionator@^0.9.1:
|
||||||
version "0.9.1"
|
version "0.9.1"
|
||||||
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
|
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
|
||||||
|
|||||||
Reference in New Issue
Block a user