From c5a6f642455e69b014bda9ca728adad3b0aeed6e Mon Sep 17 00:00:00 2001 From: javiersuweijie Date: Wed, 23 Oct 2024 17:08:57 +0800 Subject: [PATCH 1/5] feat: added sample email flow when creating patterns --- .../src/app/api/download/[[...slug]]/route.ts | 5 +- .../app/src/app/edit/[[...slug]]/content.tsx | 4 +- .../app/src/app/edit/[[...slug]]/page.tsx | 4 +- packages/app/src/app/submit/action.ts | 8 +- packages/app/src/app/submit/email/action.ts | 144 ++++ packages/app/src/app/submit/page.tsx | 27 +- packages/app/src/components/entry-form.tsx | 701 ++++++++++-------- packages/app/src/lib/code-gen/gen.ts | 6 +- 8 files changed, 559 insertions(+), 340 deletions(-) create mode 100644 packages/app/src/app/submit/email/action.ts diff --git a/packages/app/src/app/api/download/[[...slug]]/route.ts b/packages/app/src/app/api/download/[[...slug]]/route.ts index f0667ea..e32aeee 100644 --- a/packages/app/src/app/api/download/[[...slug]]/route.ts +++ b/packages/app/src/app/api/download/[[...slug]]/route.ts @@ -1,4 +1,4 @@ -import { generateCodeLibrary } from "@/lib/code-gen/gen"; +import { generateCodeLibrary, generateZip } from "@/lib/code-gen/gen"; import { NextRequest, NextResponse } from "next/server"; import { getEntryBySlug } from "@/lib/models/entry"; import { readFileSync, statSync } from "fs"; @@ -13,7 +13,8 @@ export async function GET(request: NextRequest, { params }: { params: { slug: st status: 404 }) } - const output = await generateCodeLibrary(entry.parameters, entry.slug, entry.status); + await generateCodeLibrary(entry.parameters, entry.slug, entry.status); + const output = await generateZip(entry.slug); const stats = statSync(output); const fileContent = readFileSync(output) diff --git a/packages/app/src/app/edit/[[...slug]]/content.tsx b/packages/app/src/app/edit/[[...slug]]/content.tsx index b60cb70..0a2bd61 100644 --- a/packages/app/src/app/edit/[[...slug]]/content.tsx +++ b/packages/app/src/app/edit/[[...slug]]/content.tsx @@ -4,7 +4,7 @@ import { submit } from "./action"; import { Entry } from "@prisma/client"; import { EntryForm } from "@/components/entry-form"; -export default function Content({entry}: {entry: Entry}) { +export default function Content({entry, sampleEmail}: {entry: Entry, sampleEmail: string}) { return (
@@ -13,7 +13,7 @@ export default function Content({entry}: {entry: Entry}) {

Edit pattern

{entry.title}

{entry.description}

- +
diff --git a/packages/app/src/app/edit/[[...slug]]/page.tsx b/packages/app/src/app/edit/[[...slug]]/page.tsx index 0e01b00..8556fc1 100644 --- a/packages/app/src/app/edit/[[...slug]]/page.tsx +++ b/packages/app/src/app/edit/[[...slug]]/page.tsx @@ -1,12 +1,12 @@ import { getEntryBySlug } from "@/lib/models/entry"; import Content from "./content"; -export default async function EditPage({params}: {params: {slug: string[]}}) { +export default async function EditPage({params, searchParams: {sampleEmail}}: {params: {slug: string[]}, searchParams: {sampleEmail: string}}) { const slug = params.slug.join('/'); const entry = await getEntryBySlug(slug); if (!entry) { return (

Entry not found

) } - return () + return () } \ No newline at end of file diff --git a/packages/app/src/app/submit/action.ts b/packages/app/src/app/submit/action.ts index bb89dd6..08703d5 100644 --- a/packages/app/src/app/submit/action.ts +++ b/packages/app/src/app/submit/action.ts @@ -3,8 +3,7 @@ import { z } from 'zod'; import { formSchema } from './form'; import prisma from '@/lib/prisma'; -export async function createEntry(values: z.infer) { - +export async function createEntry(values: z.infer, draft = false) { const entry = { title: values.title, slug: values.slug, @@ -15,11 +14,12 @@ export async function createEntry(values: z.infer) { ...values.parameters, version: values.useNewSdk ? "v2" : "v1", }, - emailQuery: values.emailQuery + emailQuery: values.emailQuery, + status: draft ? "DRAFT" : "PENDING", } try { - const createdEntry = await prisma.entry.create({data: entry}) + const createdEntry = await prisma.entry.create({ data: entry }) return { error: false, message: createdEntry diff --git a/packages/app/src/app/submit/email/action.ts b/packages/app/src/app/submit/email/action.ts new file mode 100644 index 0000000..efefacc --- /dev/null +++ b/packages/app/src/app/submit/email/action.ts @@ -0,0 +1,144 @@ +'use server'; +import { z } from 'zod'; +import { formSchema } from '../form'; +import { generateCodeLibrary } from '@/lib/code-gen/gen'; +import { verifyDKIMSignature } from "@zk-email/helpers/dist/dkim"; + +export interface ProcessEmailResult { + error: boolean, + message: string, + inputs?: any, + parameters?: { + maxHeaderLength: number, + maxBodyLength?: number, + domain: string, + selector: string, + }, + matches: { + name: string, + match: string, + }[], +} + +export async function processEmail(values: z.infer, email: string): Promise { + let res; + let result; + let bodyString; + let headerString; + + try { + result = await verifyDKIMSignature(email) + } catch (e: any) { + return { + error: true, + matches: [], + message: "Error verifying DKIM signature: " + e.toString() + } + } + headerString = result.headers.toString(); + const headerLength = result.headers.length; + const maxHeaderLength = Math.ceil(headerLength / 64) * 64; + const domain = result.signingDomain; + const selector = result.selector; + res = { + maxHeaderLength, + domain, + selector, + } + if (!values.parameters.ignoreBodyHashCheck) { + bodyString = result.body.toString(); + if (values.parameters.shaPrecomputeSelector) { + const split = bodyString.split(values.parameters.shaPrecomputeSelector); + if (split.length > 2) { + return { + error: true, + matches: [], + message: "Non-unique email body cut-off value. Use something that can split the email into strictly two parts." + } + } + if (split.length == 1) { + return { + error: true, + matches: [], + message: "Email body cut-off value is not found in the email body." + } + } + const bodyLength = split[1].length; + const maxBodyLength = Math.ceil(bodyLength / 64) * 64; + res = { + ...res, + maxBodyLength, + } + } + } + + // Apply regex + const regexes = values.parameters.values.map((v: any) => { + let publicGroups: number[] = []; + let index = 1; + const regex = v.parts.reduce((acc: any, part: any) => { + if (part.is_public) { + publicGroups.push(index); + index++; + return acc + "(" + part.regex_def + ")" + } + return acc + part.regex_def + }, ""); + return { + regex, + publicGroups, + name: v.name, + location: v.location, + } + }) + + let matches = [] + + for (const regex of regexes) { + if (regex.location == "body" && bodyString) { + const match = bodyString.match(regex.regex) + if (match) { + for (const group of regex.publicGroups) { + matches.push({ + name: regex.name, + match: match[group], + }) + } + } + } else { + const match = headerString.match(regex.regex) + if (match) { + for (const group of regex.publicGroups) { + matches.push({ + name: regex.name, + match: match[group], + }) + } + } + } + } + + const parameters = { + ...values.parameters, + version: values.useNewSdk ? "v2" : "v1", + } + try { + await generateCodeLibrary(parameters, "drafts/" + values.slug, "DRAFT"); + } catch (e: any) { + return { + error: true, + matches: [], + message: "Error generating code: " + e.toString() + } + } + const inputGeneration = await import(`/Users/javiersuweijie/Projects/zk/zk-regex-registry-app/packages/app/output/code/drafts/${values.slug}/lib/generate_inputs.js`) + const inputs = await inputGeneration.generateCircuitInputs(email) + console.log("inputs", inputs) + return { + error: false, + message: "Email processed successfully", + inputs, + parameters: res, + matches, + } +} \ No newline at end of file diff --git a/packages/app/src/app/submit/page.tsx b/packages/app/src/app/submit/page.tsx index 94d27ce..5746746 100644 --- a/packages/app/src/app/submit/page.tsx +++ b/packages/app/src/app/submit/page.tsx @@ -1,24 +1,15 @@ 'use client'; -import { z } from 'zod'; -import { zodResolver } from "@hookform/resolvers/zod" -import { useForm, useFieldArray } from "react-hook-form" -import { Button } from '@/components/ui/button'; -import { Form, FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage } from '@/components/ui/form'; -import { Input } from '@/components/ui/input'; -import { Textarea } from '@/components/ui/textarea'; -import { Checkbox } from '@/components/ui/checkbox'; -import { Plus, Trash } from 'lucide-react'; -import { formSchema } from './form'; -import { createEntry } from './action'; -import { Dialog, DialogTrigger, DialogContent, DialogTitle, DialogDescription, DialogHeader } from '@/components/ui/dialog'; -import { useEffect, useState } from 'react'; -import { FromAddressPattern, SubjectPattern, TimestampPattern, ToAddressPattern } from './patterns'; -import { Select, SelectTrigger, SelectValue, SelectContent, SelectGroup, SelectLabel, SelectItem } from '@/components/ui/select'; import { EntryForm } from '@/components/entry-form'; +import { Button } from '@/components/ui/button'; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import { Entry } from '@prisma/client'; import { JsonValue } from '@prisma/client/runtime/library'; +import { useState } from 'react'; +import { z } from 'zod'; +import { createEntry } from './action'; +import { formSchema } from './form'; -export default function Submit() { +export default function Submit({searchParams: {sampleEmail}}: {searchParams: {sampleEmail: string}}) { const [modal, setModal] = useState(false); const [modalMessage, setModalMessage] = useState(""); @@ -69,7 +60,6 @@ export default function Submit() { setModal(true) } - return (
@@ -77,13 +67,14 @@ export default function Submit() {

Submit new pattern

- +
+ {modalError ? "Error" : "Success"} {modalMessage} diff --git a/packages/app/src/components/entry-form.tsx b/packages/app/src/components/entry-form.tsx index 4dde8dc..6fa5d26 100644 --- a/packages/app/src/components/entry-form.tsx +++ b/packages/app/src/components/entry-form.tsx @@ -12,14 +12,23 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { formSchema } from "@/app/submit/form"; import { BodyPattern, FromAddressPattern, HeaderPattern, SubjectPattern, TimestampPattern, ToAddressPattern } from "@/app/submit/patterns"; import { Entry } from "@prisma/client"; -import { useEffect, useState } from "react"; +import { BaseSyntheticEvent, FormEvent, useEffect, useState } from "react"; +import PostalMime from "postal-mime"; +import { processEmail, ProcessEmailResult } from "@/app/submit/email/action"; interface EntryFormProps { onFormSubmit: (values: z.infer) => void, entry?: Entry, + sampleEmailFlag?: boolean, } -export function EntryForm( {onFormSubmit, entry}: EntryFormProps) { +interface Email { + contents: string, + internalDate: number, + subject: string, +} + +export function EntryForm({ onFormSubmit, entry, sampleEmailFlag }: EntryFormProps) { const form = useForm>({ resolver: zodResolver(formSchema), @@ -46,8 +55,10 @@ export function EntryForm( {onFormSubmit, entry}: EntryFormProps) { }, }) - const { fields, append, remove } = useFieldArray({control: form.control, name: "parameters.values"}) - const { fields: externalInputs, append: appendInput, remove: removeInput } = useFieldArray({control: form.control, name: "parameters.externalInputs"}) + const { fields, append, remove } = useFieldArray({ control: form.control, name: "parameters.values" }) + const { fields: externalInputs, append: appendInput, remove: removeInput } = useFieldArray({ control: form.control, name: "parameters.externalInputs" }) + const [email, setEmail] = useState(null); + const [processedResult, setProcessedResult] = useState(null); useEffect(() => { if (entry) { @@ -240,314 +251,384 @@ export function EntryForm( {onFormSubmit, entry}: EntryFormProps) { } else { } } - + + function uploadEmail(e: FormEvent) { + if (e.currentTarget.files) { + for (let i = 0; i < e.currentTarget.files.length; i++) { + const file = e.currentTarget.files[i]; + const reader = new FileReader(); + reader.onload = async (e) => { + const contents = e.target?.result; + if (typeof contents === "string") { + const parsed = await PostalMime.parse(contents) + console.log(parsed); + const email = { + contents, + internalDate: (parsed.date ? Date.parse(parsed.date) : file.lastModified), + subject: parsed.subject || file.name, + } + setEmail(email); + } + } + reader.readAsText(file); + } + } + } + + function process(e: BaseSyntheticEvent) { + if (email) { + console.log("Processing email") + form.handleSubmit(async (e) => { + const result = await processEmail(e, email.contents) + console.log("result", result, processedResult); + setProcessedResult(result) + })(); + } + } + + function setProcessedParameters() { + if (processedResult?.parameters) { + // set domain, selector, maxBodyLength, maxHeaderLength in form + if (processedResult.parameters.domain) form.setValue("parameters.senderDomain", processedResult.parameters.domain) + if (processedResult.parameters.selector) form.setValue("parameters.dkimSelector", processedResult.parameters.selector) + if (processedResult.parameters.maxBodyLength) form.setValue("parameters.emailBodyMaxLength", processedResult.parameters.maxBodyLength) + if (processedResult.parameters.maxHeaderLength) form.setValue("parameters.emailHeaderMaxLength", processedResult.parameters.maxHeaderLength) + } + } + return (
- - ( - - Pattern Title - - - - - - - )} - /> - ( - - Description - -