diff --git a/src/app/mail/index.ts b/src/app/mail/index.ts index f11550f4..9d4644f8 100644 --- a/src/app/mail/index.ts +++ b/src/app/mail/index.ts @@ -4,9 +4,8 @@ import {Address, DELETE, makeEvent, makeHttpAuth, makeHttpAuthHeader} from "@wel import {Relays, User} from "@welshman/app" import {buildUrl} from "@lib/util" import {app, thunks} from "@app/core" -import {device} from "@app/device" import {EMAIL_NOTIFICATION_SERVER, PUSH_BRIDGE} from "@app/env" -import {userSettingsValues} from "@app/settings" +import {ensureEmailSalt, userSettingsValues} from "@app/settings" import {emailNotificationState, syncRelaySubscriptions} from "@app/push/adapters/common" export class MailNotifications { @@ -138,8 +137,11 @@ export class MailNotifications { ) } - _getSubscriptionIdentifier = (relay: string, key: string) => - String(hash(relay + key + device.get() + "email")) + _getSubscriptionIdentifier = async (relay: string, key: string) => { + const salt = await ensureEmailSalt() + + return String(hash(relay + key + salt + "email")) + } _getPushUrl = async (url: string) => { for (const candidate of [url, PUSH_BRIDGE]) { @@ -161,7 +163,7 @@ export class MailNotifications { return } - const identifier = this._getSubscriptionIdentifier(relay, key) + const identifier = await this._getSubscriptionIdentifier(relay, key) const thunk = thunks.get().publish({ relays: [url], @@ -188,7 +190,7 @@ export class MailNotifications { if (!url) return const $pubkey = User.require(app.get()).pubkey - const identifier = this._getSubscriptionIdentifier(relay, key) + const identifier = await this._getSubscriptionIdentifier(relay, key) const address = new Address(30390, $pubkey, identifier).toString() const event = makeEvent(DELETE, {tags: [["a", address]]}) const error = await thunks diff --git a/src/app/settings.ts b/src/app/settings.ts index 0d7c8b91..5aa96b88 100644 --- a/src/app/settings.ts +++ b/src/app/settings.ts @@ -1,5 +1,5 @@ import {derived, writable} from "svelte/store" -import {append, remove, spec} from "@welshman/lib" +import {append, call, randomId, remove, spec} from "@welshman/lib" import {APP_DATA} from "@welshman/util" import {withGetter} from "@welshman/store" import {AppData} from "@welshman/domain" @@ -10,6 +10,8 @@ import {app, fromApp, usePlugin} from "@app/core" export const SETTINGS = "flotilla/settings" +const emailSaltsByPubkey = new Map>() + export enum RelayAuthMode { Aggressive = "aggressive", Conservative = "conservative", @@ -34,6 +36,7 @@ export type SettingsValues = { email: boolean emailAddress: string emailFrequency: string + emailSalt: string } export const defaultSettings: SettingsValues = { @@ -49,6 +52,7 @@ export const defaultSettings: SettingsValues = { email: false, emailAddress: "", emailFrequency: "daily", + emailSalt: "", } export class Settings extends DerivedPlugin { @@ -121,6 +125,31 @@ export const publishSettings = async (params: Partial) => { return command.publish() } +// A per-account salt for building deterministic identifiers (e.g. kind 30390 email +// subscription ids) that reproduce on any device of the user, stored with the settings. +export const ensureEmailSalt = () => { + const $pubkey = User.require(app.get()).pubkey + const existing = emailSaltsByPubkey.get($pubkey) + if (existing) return existing + + // Force-load from the network so a salt synced by another device wins over a fresh one, + // and memoize the in-flight promise so parallel syncs (one per space relay) share a salt. + const promise = call(async () => { + await settings.get().forceLoad($pubkey) + const salt = userSettingsValues.get().emailSalt + if (salt) return salt + + const emailSalt = randomId() + await publishSettings({emailSalt}) + + return emailSalt + }) + + emailSaltsByPubkey.set($pubkey, promise) + + return promise +} + export const addTrustedRelay = (url: string) => publishSettings({trusted_relays: append(url, getSetting("trusted_relays"))})