diff --git a/.github/workflows/main-core.yaml b/.github/workflows/main-core.yaml index d868789e23..00b9bc0a4f 100644 --- a/.github/workflows/main-core.yaml +++ b/.github/workflows/main-core.yaml @@ -62,6 +62,10 @@ jobs: INTERNAL_DKIM_SELECTOR + INTERNAL_RELAY_DKIM_DOMAIN + INTERNAL_RELAY_DKIM_SELECTOR + SECRET_RELAY_DKIM_PRIVATE_KEY + INTERNAL_GOOGLE_OAUTH_CLIENT_ID SECRET_GOOGLE_OAUTH_CLIENT_SECRET @@ -92,6 +96,10 @@ jobs: INTERNAL_DKIM_SELECTOR: ${{ secrets.INTERNAL_DKIM_SELECTOR }} + INTERNAL_RELAY_DKIM_DOMAIN: ${{ secrets.INTERNAL_RELAY_DKIM_DOMAIN }} + INTERNAL_RELAY_DKIM_SELECTOR: ${{ secrets.INTERNAL_RELAY_DKIM_SELECTOR }} + SECRET_RELAY_DKIM_PRIVATE_KEY: ${{ secrets.SECRET_RELAY_DKIM_PRIVATE_KEY }} + INTERNAL_GOOGLE_OAUTH_CLIENT_ID: ${{ vars.INTERNAL_GOOGLE_OAUTH_CLIENT_ID_DEV }} SECRET_GOOGLE_OAUTH_CLIENT_SECRET: ${{ secrets.SECRET_GOOGLE_OAUTH_CLIENT_SECRET_DEV }} diff --git a/.github/workflows/next-core.yaml b/.github/workflows/next-core.yaml index f35a4b7af1..617ec5442d 100644 --- a/.github/workflows/next-core.yaml +++ b/.github/workflows/next-core.yaml @@ -62,6 +62,10 @@ jobs: INTERNAL_DKIM_SELECTOR + INTERNAL_RELAY_DKIM_DOMAIN + INTERNAL_RELAY_DKIM_SELECTOR + SECRET_RELAY_DKIM_PRIVATE_KEY + INTERNAL_GOOGLE_OAUTH_CLIENT_ID SECRET_GOOGLE_OAUTH_CLIENT_SECRET @@ -92,6 +96,10 @@ jobs: INTERNAL_DKIM_SELECTOR: ${{ secrets.INTERNAL_DKIM_SELECTOR }} + INTERNAL_RELAY_DKIM_DOMAIN: ${{ secrets.INTERNAL_RELAY_DKIM_DOMAIN }} + INTERNAL_RELAY_DKIM_SELECTOR: ${{ secrets.INTERNAL_RELAY_DKIM_SELECTOR }} + SECRET_RELAY_DKIM_PRIVATE_KEY: ${{ secrets.SECRET_RELAY_DKIM_PRIVATE_KEY }} + INTERNAL_GOOGLE_OAUTH_CLIENT_ID: ${{ vars.INTERNAL_GOOGLE_OAUTH_CLIENT_ID_TEST }} SECRET_GOOGLE_OAUTH_CLIENT_SECRET: ${{ secrets.SECRET_GOOGLE_OAUTH_CLIENT_SECRET_TEST }} diff --git a/.github/workflows/release-core.yaml b/.github/workflows/release-core.yaml index 08a8f403b9..6a58b2cfa7 100644 --- a/.github/workflows/release-core.yaml +++ b/.github/workflows/release-core.yaml @@ -61,6 +61,10 @@ jobs: INTERNAL_DKIM_SELECTOR + INTERNAL_RELAY_DKIM_DOMAIN + INTERNAL_RELAY_DKIM_SELECTOR + SECRET_RELAY_DKIM_PRIVATE_KEY + INTERNAL_GOOGLE_OAUTH_CLIENT_ID SECRET_GOOGLE_OAUTH_CLIENT_SECRET @@ -91,6 +95,10 @@ jobs: INTERNAL_DKIM_SELECTOR: ${{ secrets.INTERNAL_DKIM_SELECTOR }} + INTERNAL_RELAY_DKIM_DOMAIN: ${{ secrets.INTERNAL_RELAY_DKIM_DOMAIN }} + INTERNAL_RELAY_DKIM_SELECTOR: ${{ secrets.INTERNAL_RELAY_DKIM_SELECTOR }} + SECRET_RELAY_DKIM_PRIVATE_KEY: ${{ secrets.SECRET_RELAY_DKIM_PRIVATE_KEY }} + INTERNAL_GOOGLE_OAUTH_CLIENT_ID: ${{ vars.INTERNAL_GOOGLE_OAUTH_CLIENT_ID_PROD }} SECRET_GOOGLE_OAUTH_CLIENT_SECRET: ${{ secrets.SECRET_GOOGLE_OAUTH_CLIENT_SECRET_PROD }} diff --git a/.yarn/cache/postal-mime-npm-1.0.16-5981a9ed49-adcfd1432a.zip b/.yarn/cache/postal-mime-npm-1.0.16-5981a9ed49-adcfd1432a.zip new file mode 100644 index 0000000000..8730f51517 Binary files /dev/null and b/.yarn/cache/postal-mime-npm-1.0.16-5981a9ed49-adcfd1432a.zip differ diff --git a/packages/types/account.ts b/packages/types/account.ts index 70d3a41718..188ef2268a 100644 --- a/packages/types/account.ts +++ b/packages/types/account.ts @@ -24,6 +24,7 @@ export enum OAuthAccountType { export enum EmailAccountType { Email = 'email', + Mask = 'mask', } export enum WebauthnAccountType { diff --git a/platform/core/.dev.vars.example b/platform/core/.dev.vars.example index 490c128615..9185e1e08d 100644 --- a/platform/core/.dev.vars.example +++ b/platform/core/.dev.vars.example @@ -3,6 +3,10 @@ SECRET_JWKS = '[]' INTERNAL_DKIM_SELECTOR = "" +INTERNAL_RELAY_DKIM_DOMAIN = "" +INTERNAL_RELAY_DKIM_SELECTOR = "" +SECRET_RELAY_DKIM_PRIVATE_KEY = "" + INTERNAL_PASSPORT_SERVICE_NAME = "" INTERNAL_CLOUDFLARE_ZONE_ID = "" diff --git a/platform/core/package.json b/platform/core/package.json index 39291ca2a5..31da26b486 100644 --- a/platform/core/package.json +++ b/platform/core/package.json @@ -39,9 +39,12 @@ "@proofzero/platform.edges": "workspace:*", "@proofzero/platform.identity": "workspace:*", "@proofzero/platform.starbase": "workspace:*", + "@proofzero/types": "workspace:*", + "@proofzero/urns": "workspace:*", "@trpc/server": "10.8.1", "do-proxy": "1.3.4", "jose": "4.14.4", + "postal-mime": "1.0.16", "zod": "3.22.4" } } diff --git a/platform/core/src/index.ts b/platform/core/src/index.ts index f739d013bd..1e5aa469ae 100644 --- a/platform/core/src/index.ts +++ b/platform/core/src/index.ts @@ -7,6 +7,7 @@ import { serverOnError as onError } from '@proofzero/utils/trpc' import { createContext, type Context } from './context' import router from './router' +import relay, { type CloudflareEmailMessage } from './relay' import type { Environment } from './types' export { Account } from '@proofzero/platform.account' @@ -36,6 +37,17 @@ export default { }, }) }, + async email(message: CloudflareEmailMessage, env: Environment) { + const decoder = new TextDecoder() + const reader = message.raw.getReader() + let content = '' + while (true) { + const { done, value } = await reader.read() + content += decoder.decode(value) + if (done) break + } + return relay(content, env) + }, } export { router, type Context, type Environment } diff --git a/platform/core/src/relay.ts b/platform/core/src/relay.ts new file mode 100644 index 0000000000..b3e84065f3 --- /dev/null +++ b/platform/core/src/relay.ts @@ -0,0 +1,138 @@ +import PostalMime, { type Address, Email } from 'postal-mime' + +import { AccountURN, AccountURNSpace } from '@proofzero/urns/account' +import { generateHashedIDRef } from '@proofzero/urns/idref' +import { EmailAccountType } from '@proofzero/types/account' +import { initAccountNodeByName } from '@proofzero/platform.account/src/nodes' + +import type { Environment } from './types' + +export interface CloudflareEmailMessage { + readonly from: string + readonly to: string + readonly headers: Headers + readonly raw: ReadableStream + readonly rawSize: number + + setReject(reason: string): void + forward(rcptTo: string, headers?: Headers): Promise +} + +interface MailChannelAddress { + name: string + email: string +} + +interface DKIM { + dkim_domain: string + dkim_selector: string + dkim_private_key: string +} + +export default async (message: string, env: Environment) => { + const postalMime = new PostalMime() + const email = await postalMime.parse(message) + + const dkim: DKIM = { + dkim_domain: env.INTERNAL_RELAY_DKIM_DOMAIN, + dkim_selector: env.INTERNAL_RELAY_DKIM_SELECTOR, + dkim_private_key: env.SECRET_RELAY_DKIM_PRIVATE_KEY, + } + + const recipients = new Array
() + .concat(email.to || []) + .concat(email.cc || []) + .filter((recipient) => + recipient.address.endsWith(`@${env.INTERNAL_RELAY_DKIM_DOMAIN}`) + ) + + for (const recipient of recipients) { + const nss = generateHashedIDRef(EmailAccountType.Mask, recipient.address) + const urn = AccountURNSpace.componentizedUrn(nss) + const node = initAccountNodeByName(urn, env.Account) + + const sourceAccountURN = + await node.storage.get('source-account') + if (!sourceAccountURN) continue + + const sourceAccountNode = initAccountNodeByName( + sourceAccountURN, + env.Account + ) + + const name = (await sourceAccountNode.class.getNickname()) || '' + const address = await sourceAccountNode.class.getAddress() + if (!address) continue + + const from: MailChannelAddress = { + name: email.from.name, + email: recipient.address, + } + + const to: MailChannelAddress[] = [ + { + name, + email: address, + }, + ] + + await send(email, from, to, dkim) + } +} + +const send = async ( + email: Email, + from: MailChannelAddress, + to: MailChannelAddress[], + dkim: DKIM +) => { + const { subject } = email + + const content = [] + if (email.text) + content.push({ + type: 'text/plain', + value: email.text, + }) + if (email.html) { + content.push({ + type: 'text/html', + value: email.html, + }) + } + + const personalizations = [ + { + to, + ...dkim, + }, + ] + + const request = new Request('https://api.mailchannels.net/tx/v1/send', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify( + { + from, + subject, + content, + personalizations, + }, + null, + 2 + ), + }) + + const response = await fetch(request) + if (!response.ok) { + const responseBody = await response.text() + try { + console.error( + 'MailChannels', + JSON.stringify(JSON.parse(responseBody), null, 2) + ) + } catch (err) { + console.error('MailChannels', responseBody) + } + } +} diff --git a/platform/core/src/types.ts b/platform/core/src/types.ts index ff9944051b..0725fbbbbd 100644 --- a/platform/core/src/types.ts +++ b/platform/core/src/types.ts @@ -49,6 +49,10 @@ export interface Environment { INTERNAL_DKIM_SELECTOR: string + INTERNAL_RELAY_DKIM_DOMAIN: string + INTERNAL_RELAY_DKIM_SELECTOR: string + SECRET_RELAY_DKIM_PRIVATE_KEY: string + INTERNAL_CLOUDFLARE_ZONE_ID: string TOKEN_CLOUDFLARE_API: string diff --git a/platform/core/wrangler.current.toml b/platform/core/wrangler.current.toml index 21e82bb78f..a83dbaffd5 100644 --- a/platform/core/wrangler.current.toml +++ b/platform/core/wrangler.current.toml @@ -39,6 +39,9 @@ ENVIRONMENT = "current" INTERNAL_PASSPORT_SERVICE_NAME = "passport-current" +INTERNAL_RELAY_DKIM_DOMAIN = "rollup.email" +INTERNAL_RELAY_DKIM_SELECTOR = "mailchannels" + PASSPORT_URL = "https://passport.rollup.id" MINTPFP_CONTRACT_ADDRESS = "0x3ebfaFE60F3Ac34f476B2f696Fc2779ff1B03193" TTL_IN_MS = 300_000 diff --git a/platform/core/wrangler.dev.toml b/platform/core/wrangler.dev.toml index 12f09466f0..062b0a4352 100644 --- a/platform/core/wrangler.dev.toml +++ b/platform/core/wrangler.dev.toml @@ -39,6 +39,9 @@ ENVIRONMENT = "dev" INTERNAL_PASSPORT_SERVICE_NAME = "passport-dev" +INTERNAL_RELAY_DKIM_DOMAIN = "rollup.email" +INTERNAL_RELAY_DKIM_SELECTOR = "mailchannels" + PASSPORT_URL = "https://passport-dev.rollup.id" MINTPFP_CONTRACT_ADDRESS = "0x028aE75Bb01eef2A581172607b93af8D24F50643" TTL_IN_MS = 25_000 diff --git a/platform/core/wrangler.next.toml b/platform/core/wrangler.next.toml index 11268d1682..af356d4a36 100644 --- a/platform/core/wrangler.next.toml +++ b/platform/core/wrangler.next.toml @@ -39,6 +39,9 @@ ENVIRONMENT = "next" INTERNAL_PASSPORT_SERVICE_NAME = "passport-next" +INTERNAL_RELAY_DKIM_DOMAIN = "rollup.email" +INTERNAL_RELAY_DKIM_SELECTOR = "mailchannels" + PASSPORT_URL = "https://passport-next.rollup.id" MINTPFP_CONTRACT_ADDRESS = "0x028aE75Bb01eef2A581172607b93af8D24F50643" TTL_IN_MS = 300_000 diff --git a/platform/core/wrangler.toml b/platform/core/wrangler.toml index 8337428da4..a681b2ea3e 100644 --- a/platform/core/wrangler.toml +++ b/platform/core/wrangler.toml @@ -35,6 +35,9 @@ migrations_dir = "../edges/migrations" [vars] ENVIRONMENT = "local" +INTERNAL_RELAY_DKIM_DOMAIN = "rollup.email" +INTERNAL_RELAY_DKIM_SELECTOR = "mailchannels" + PASSPORT_URL = "http://localhost:10001" MINTPFP_CONTRACT_ADDRESS = "0x028aE75Bb01eef2A581172607b93af8D24F50643" TTL_IN_MS = 25_000 diff --git a/yarn.lock b/yarn.lock index a7b8309f2e..2144de5025 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6543,6 +6543,8 @@ __metadata: "@proofzero/platform.edges": "workspace:*" "@proofzero/platform.identity": "workspace:*" "@proofzero/platform.starbase": "workspace:*" + "@proofzero/types": "workspace:*" + "@proofzero/urns": "workspace:*" "@trpc/server": 10.8.1 "@types/node": 20.3.1 "@typescript-eslint/eslint-plugin": 5.59.11 @@ -6553,6 +6555,7 @@ __metadata: eslint-config-prettier: 8.8.0 jose: 4.14.4 npm-run-all: 4.1.5 + postal-mime: 1.0.16 prettier: 2.8.8 typescript: 5.1.3 wrangler: 3.2.0 @@ -31285,6 +31288,13 @@ __metadata: languageName: node linkType: hard +"postal-mime@npm:1.0.16": + version: 1.0.16 + resolution: "postal-mime@npm:1.0.16" + checksum: adcfd1432add9601ca68d98a4c54c695bcdf6ad723b1c58ced36f18fc1778f20261c6dbd2c4a9a9c972166b590ab60330ec9fa1fc51ed1252148176a92a95b96 + languageName: node + linkType: hard + "postcss-attribute-case-insensitive@npm:^5.0.2": version: 5.0.2 resolution: "postcss-attribute-case-insensitive@npm:5.0.2"