diff --git a/public/locales/de-DE/settings.json b/public/locales/de-DE/settings.json
index bc710ff..227caf4 100644
--- a/public/locales/de-DE/settings.json
+++ b/public/locales/de-DE/settings.json
@@ -2,6 +2,6 @@
"title": "Einstellungen",
"actions": {
"enable2fa": "Zwei-Faktor-Authentifizierung",
- "aliasPreferences": "Alias-Präferenzen"
+ "aliasPreferences": "Alias-Präferenzen",
}
}
diff --git a/public/locales/en-US/settings-email-pgp.json b/public/locales/en-US/settings-email-pgp.json
new file mode 100644
index 0000000..4f86dc5
--- /dev/null
+++ b/public/locales/en-US/settings-email-pgp.json
@@ -0,0 +1,20 @@
+{
+ "title": "Set up PGP encryption",
+ "description": "By providing your public key, we will encrypt all emails sent to you. This will ensure that only you can read the emails we send you. Your email provider will not be able to read your emails anymore.",
+ "form": {
+ "fields": {
+ "publicKey": {
+ "label": "Your public key",
+ "helperText": "Paste your raw public key in armored format here."
+ }
+ },
+ "continueActionLabel": "Enable PGP encryption"
+ },
+ "findPublicKey": {
+ "label": "Find public key automatically",
+ "title": "Use this key?",
+ "description": "We found a public key for your email! Would you like to use it? The key has been created on {{createdAt}} and is of type {{type}}. This is the fingerprint:",
+ "continueActionLabel": "Use Key"
+ },
+ "alreadyConfigured": "PGP encryption is activated. You are using a public key with this fingerprint:"
+}
diff --git a/public/locales/en-US/settings.json b/public/locales/en-US/settings.json
index 756d4aa..2b4d23b 100644
--- a/public/locales/en-US/settings.json
+++ b/public/locales/en-US/settings.json
@@ -3,6 +3,7 @@
"actions": {
"enable2fa": "Two-Factor-Authentication",
"aliasPreferences": "Alias Preferences",
- "apiKeys": "Manage API Keys"
+ "apiKeys": "Manage API Keys",
+ "emailPgp": "PGP Encryption for Emails"
}
}
diff --git a/src/App.tsx b/src/App.tsx
index 48a0768..8a44dad 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -32,6 +32,7 @@ import RootRoute from "~/routes/Root"
import Settings2FARoute from "~/routes/Settings2FARoute"
import SettingsAPIKeysRoute from "~/routes/SettingsAPIKeysRoute"
import SettingsAliasPreferencesRoute from "~/routes/SettingsAliasPreferencesRoute"
+import SettingsEmailPGPRoute from "~/routes/SettingsEmailPGPRoute"
import SettingsRoute from "~/routes/SettingsRoute"
import SignupRoute from "~/routes/SignupRoute"
import VerifyEmailRoute from "~/routes/VerifyEmailRoute"
@@ -113,6 +114,10 @@ const router = createBrowserRouter([
loader: getServerSettings,
element: ,
},
+ {
+ path: "/settings/email-pgp",
+ element: ,
+ },
{
path: "/reports",
loader: getServerSettings,
diff --git a/src/route-widgets/SettingsEmailPGPRoute/ImportKeyDialog.tsx b/src/route-widgets/SettingsEmailPGPRoute/ImportKeyDialog.tsx
new file mode 100644
index 0000000..386f14b
--- /dev/null
+++ b/src/route-widgets/SettingsEmailPGPRoute/ImportKeyDialog.tsx
@@ -0,0 +1,71 @@
+import {ReactElement} from "react"
+import {useTranslation} from "react-i18next"
+import {HiKey} from "react-icons/hi"
+import {TiCancel} from "react-icons/ti"
+import {useAsync} from "react-use"
+import {readKey} from "openpgp"
+
+import {
+ Box,
+ Button,
+ CircularProgress,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogContentText,
+ DialogTitle,
+} from "@mui/material"
+
+import {FindPublicKeyResponse} from "~/apis"
+
+export interface ImportKeyDialogProps {
+ open: boolean
+ publicKeyResult: FindPublicKeyResponse | null
+ onClose: () => void
+ onImport: () => void
+}
+
+export default function ImportKeyDialog({
+ open,
+ publicKeyResult,
+ onClose,
+ onImport,
+}: ImportKeyDialogProps): ReactElement {
+ const {t} = useTranslation(["settings-email-pgp", "common"])
+ const {value: fingerprint, loading: isLoadingFingerprint} = useAsync(async () => {
+ if (publicKeyResult === null) {
+ return
+ }
+
+ const key = await readKey({
+ armoredKey: publicKeyResult!.publicKey,
+ })
+
+ return key.getFingerprint()
+ }, [publicKeyResult])
+
+ return (
+
+ )
+}
diff --git a/src/route-widgets/SettingsEmailPGPRoute/SetupPGPEncryptionForm.tsx b/src/route-widgets/SettingsEmailPGPRoute/SetupPGPEncryptionForm.tsx
new file mode 100644
index 0000000..f44a887
--- /dev/null
+++ b/src/route-widgets/SettingsEmailPGPRoute/SetupPGPEncryptionForm.tsx
@@ -0,0 +1,149 @@
+import * as yup from "yup"
+import {ReactElement, useContext, useState} from "react"
+import {useFormik} from "formik"
+import {useMutation} from "@tanstack/react-query"
+import {Box, FormGroup, FormHelperText, Grid, TextField} from "@mui/material"
+import {
+ FindPublicKeyResponse,
+ UpdatePreferencesData,
+ findPublicKey,
+ updatePreferences,
+} from "~/apis"
+import {HiSearch} from "react-icons/hi"
+import {LoadingButton} from "@mui/lab"
+import {parseFastAPIError} from "~/utils"
+import {AxiosError} from "axios"
+import {RiLockFill} from "react-icons/ri"
+import {SimpleDetailResponse, User} from "~/server-types"
+import {useTranslation} from "react-i18next"
+import {useErrorSuccessSnacks} from "~/hooks"
+import {AuthContext} from "~/components"
+import ImportKeyDialog from "./ImportKeyDialog"
+
+interface Form {
+ publicKey: string
+
+ detail?: string
+}
+
+export default function SetupPGPEncryptionForm(): ReactElement {
+ const {t} = useTranslation(["settings-email-pgp", "common"])
+ const {showSuccess, showError} = useErrorSuccessSnacks()
+
+ const [publicKeyResult, setPublicKeyResult] = useState(null)
+
+ const schema = yup.object().shape({
+ publicKey: yup.string().label(t("form.publicKey.label")),
+ })
+ const {user, _updateUser} = useContext(AuthContext)
+
+ const {mutateAsync} = useMutation(
+ updatePreferences,
+ {
+ onSuccess: (response, values) => {
+ const newUser = {
+ ...user,
+ preferences: {
+ ...user!.preferences,
+ ...values,
+ },
+ } as User
+
+ if (response.detail) {
+ showSuccess(response?.detail)
+ }
+
+ _updateUser(newUser)
+ },
+ },
+ )
+
+ const formik = useFormik
+ setPublicKeyResult(null)}
+ onImport={() => {
+ formik.setFieldValue("publicKey", publicKeyResult!.publicKey || "")
+ setPublicKeyResult(null)
+ }}
+ />
+ >
+ )
+}
diff --git a/src/routes/SettingsEmailPGPRoute.tsx b/src/routes/SettingsEmailPGPRoute.tsx
new file mode 100644
index 0000000..3b9a1ae
--- /dev/null
+++ b/src/routes/SettingsEmailPGPRoute.tsx
@@ -0,0 +1,29 @@
+import {ReactElement} from "react"
+import {useTranslation} from "react-i18next"
+
+import {Alert} from "@mui/material"
+
+import {SimplePage} from "~/components"
+import {useUser} from "~/hooks"
+import SetupPGPEncryptionForm from "~/route-widgets/SettingsEmailPGPRoute/SetupPGPEncryptionForm"
+
+export default function SettingsEmailPGPRoute(): ReactElement {
+ const {t} = useTranslation(["settings-email-pgp", "common"])
+ const user = useUser()
+
+ if (!user?.preferences.emailGpgPublicKey) {
+ return (
+
+
+
+ )
+ } else {
+ return (
+
+
+ {t("alreadyConfigured")}
+
+
+ )
+ }
+}
diff --git a/src/routes/SettingsRoute.tsx b/src/routes/SettingsRoute.tsx
index a103fe6..9b1f293 100644
--- a/src/routes/SettingsRoute.tsx
+++ b/src/routes/SettingsRoute.tsx
@@ -8,6 +8,7 @@ import React, {ReactElement} from "react"
import {List, ListItemButton, ListItemIcon, ListItemText} from "@mui/material"
import {SimplePageBuilder} from "~/components"
+import {RiLockFill} from "react-icons/ri"
export default function SettingsRoute(): ReactElement {
const {t} = useTranslation("settings")
@@ -33,6 +34,12 @@ export default function SettingsRoute(): ReactElement {
+
+
+
+
+
+
)
diff --git a/src/server-types.ts b/src/server-types.ts
index ca6ca57..b0efbcf 100644
--- a/src/server-types.ts
+++ b/src/server-types.ts
@@ -54,6 +54,7 @@ export interface ServerUser {
aliasProxyUserAgent: ProxyUserAgentType
aliasExpandUrlShorteners: boolean
aliasRejectOnPrivacyLeak: boolean
+ emailGpgPublicKey: string | null
}
}