Skip to content

Commit

Permalink
feat(core): email relay (#2741)
Browse files Browse the repository at this point in the history
  • Loading branch information
szkl authored Oct 26, 2023
1 parent 0584678 commit 498aa96
Show file tree
Hide file tree
Showing 15 changed files with 199 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/main-core.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ jobs:
INTERNAL_DKIM_SELECTOR
SECRET_RELAY_DKIM_PRIVATE_KEY
INTERNAL_GOOGLE_OAUTH_CLIENT_ID
SECRET_GOOGLE_OAUTH_CLIENT_SECRET
Expand Down Expand Up @@ -92,6 +94,8 @@ jobs:

INTERNAL_DKIM_SELECTOR: ${{ secrets.INTERNAL_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 }}

Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/next-core.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ jobs:
INTERNAL_DKIM_SELECTOR
SECRET_RELAY_DKIM_PRIVATE_KEY
INTERNAL_GOOGLE_OAUTH_CLIENT_ID
SECRET_GOOGLE_OAUTH_CLIENT_SECRET
Expand Down Expand Up @@ -92,6 +94,8 @@ jobs:

INTERNAL_DKIM_SELECTOR: ${{ secrets.INTERNAL_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 }}

Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/release-core.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ jobs:
INTERNAL_DKIM_SELECTOR
SECRET_RELAY_DKIM_PRIVATE_KEY
INTERNAL_GOOGLE_OAUTH_CLIENT_ID
SECRET_GOOGLE_OAUTH_CLIENT_SECRET
Expand Down Expand Up @@ -91,6 +93,8 @@ jobs:

INTERNAL_DKIM_SELECTOR: ${{ secrets.INTERNAL_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 }}

Expand Down
Binary file not shown.
1 change: 1 addition & 0 deletions packages/types/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export enum OAuthAccountType {

export enum EmailAccountType {
Email = 'email',
Mask = 'mask',
}

export enum WebauthnAccountType {
Expand Down
4 changes: 4 additions & 0 deletions platform/core/.dev.vars.example
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ""
Expand Down
3 changes: 3 additions & 0 deletions platform/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
14 changes: 14 additions & 0 deletions platform/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -36,6 +37,19 @@ export default {
},
})
},
async email(message: CloudflareEmailMessage, env: Environment) {
const decoder = new TextDecoder()
const reader = message.raw.getReader()

let content = ''
let { done, value } = await reader.read()
while (!done) {
content += decoder.decode(value)
;({ done, value } = await reader.read())
}

return relay(content, env)
},
}

export { router, type Context, type Environment }
139 changes: 139 additions & 0 deletions platform/core/src/relay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
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<Uint8Array>
readonly rawSize: number

setReject(reason: string): void
forward(rcptTo: string, headers?: Headers): Promise<void>
}

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<Address>()
.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<AccountURN>(
'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)
}
}
}
4 changes: 4 additions & 0 deletions platform/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions platform/core/wrangler.current.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions platform/core/wrangler.dev.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions platform/core/wrangler.next.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions platform/core/wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit 498aa96

Please sign in to comment.