Skip to content

Commit 633f93d

Browse files
committed
feat: add upload function & disable email sending for now
1 parent a2d6807 commit 633f93d

File tree

8 files changed

+3962
-3040
lines changed

8 files changed

+3962
-3040
lines changed

apps/mailtools/drizzle.config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ dotenv.config();
55
export default {
66
schema: './src/lib/db/schema.ts',
77
out: './drizzle/migrations',
8+
dialect: 'sqlite',
89
driver: 'turso',
910
dbCredentials: {
1011
url: process.env.TURSO_URI!,

apps/mailtools/package.json

+30-30
Original file line numberDiff line numberDiff line change
@@ -14,43 +14,43 @@
1414
"db:studio": "drizzle-kit studio"
1515
},
1616
"devDependencies": {
17-
"@sveltejs/adapter-auto": "^3.0.0",
18-
"@sveltejs/kit": "^2.0.0",
19-
"@sveltejs/vite-plugin-svelte": "^3.0.0",
20-
"@types/eslint": "^8.56.0",
17+
"@sveltejs/adapter-auto": "^3.2.4",
18+
"@sveltejs/kit": "^2.5.22",
19+
"@sveltejs/vite-plugin-svelte": "^3.1.1",
20+
"@types/eslint": "^9.6.0",
2121
"@types/mailparser": "^3.4.4",
22-
"@typescript-eslint/eslint-plugin": "^7.0.0",
23-
"@typescript-eslint/parser": "^7.0.0",
24-
"autoprefixer": "^10.4.16",
22+
"@typescript-eslint/eslint-plugin": "^8.1.0",
23+
"@typescript-eslint/parser": "^8.1.0",
24+
"autoprefixer": "^10.4.20",
2525
"dotenv": "^16.4.5",
26-
"drizzle-kit": "^0.20.14",
27-
"eslint": "^8.56.0",
28-
"eslint-plugin-svelte": "^2.35.1",
29-
"mailparser": "^3.6.9",
30-
"postcss": "^8.4.32",
31-
"postcss-load-config": "^5.0.2",
32-
"svelte": "^4.2.7",
33-
"svelte-check": "^3.6.0",
34-
"tailwindcss": "^3.3.6",
35-
"tslib": "^2.4.1",
36-
"typescript": "^5.0.0",
37-
"vite": "^5.0.3"
26+
"drizzle-kit": "^0.24.0",
27+
"eslint": "^9.9.0",
28+
"eslint-plugin-svelte": "^2.43.0",
29+
"mailparser": "^3.7.1",
30+
"postcss": "^8.4.41",
31+
"postcss-load-config": "^6.0.1",
32+
"svelte": "^4.2.18",
33+
"svelte-check": "^3.8.5",
34+
"tailwindcss": "^3.4.10",
35+
"tslib": "^2.6.3",
36+
"typescript": "^5.5.4",
37+
"vite": "^5.4.1"
3838
},
3939
"type": "module",
4040
"dependencies": {
41-
"@libsql/client": "^0.6.0",
41+
"@libsql/client": "^0.9.0",
4242
"@types/qrcode": "^1.5.5",
4343
"@u22n/mailtools": "workspace:^",
44-
"bits-ui": "^0.21.1",
45-
"clsx": "^2.1.0",
46-
"drizzle-orm": "^0.30.6",
47-
"lucide-svelte": "^0.363.0",
48-
"mode-watcher": "^0.3.0",
49-
"nanoid": "^5.0.6",
50-
"qrcode": "^1.5.3",
51-
"svelte-sonner": "^0.3.20",
52-
"svelte-turnstile": "^0.5.0",
53-
"tailwind-merge": "^2.2.2",
44+
"bits-ui": "^0.21.13",
45+
"clsx": "^2.1.1",
46+
"drizzle-orm": "^0.33.0",
47+
"lucide-svelte": "^0.427.0",
48+
"mode-watcher": "^0.4.1",
49+
"nanoid": "^5.0.7",
50+
"qrcode": "^1.5.4",
51+
"svelte-sonner": "^0.3.27",
52+
"svelte-turnstile": "^0.8.0",
53+
"tailwind-merge": "^2.5.2",
5454
"tailwind-variants": "^0.2.1"
5555
}
5656
}
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,27 @@
11
<script lang="ts">
2+
import { toast } from 'svelte-sonner';
3+
4+
export let onFileAdded: (file: File) => void;
5+
6+
let form: HTMLFormElement;
7+
function handleFileAdded(event: Event) {
8+
const file = (event.target as HTMLInputElement).files?.[0];
9+
if (file?.type !== 'message/rfc822') {
10+
toast.error('Invalid file type. Please upload a .eml file');
11+
form.reset();
12+
return;
13+
}
14+
if (file) onFileAdded(file);
15+
}
216
</script>
317

4-
<div
5-
class="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-32 w-full rounded-md border px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
6-
role="form">
18+
<form
19+
class="flex w-full flex-col gap-1"
20+
bind:this={form}>
21+
<h2 class="font-bold">Drop a file here to upload</h2>
722
<input
823
type="file"
9-
class="hidden" />
10-
</div>
24+
accept="message/rfc822"
25+
on:change={handleFileAdded}
26+
class="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-32 w-full rounded-md border border-dashed px-3 py-2 text-sm file:hidden focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" />
27+
</form>

apps/mailtools/src/routes/+page.svelte

+103-20
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
import { toast } from 'svelte-sonner';
1313
import { Turnstile } from 'svelte-turnstile';
1414
import { PUBLIC_TURNSTILE_SITE_KEY } from '$env/static/public';
15+
import Dropzone from '@components/custom/Dropzone.svelte';
16+
import { Badge } from '@components/ui/badge';
17+
import { Textarea } from '@components/ui/textarea';
18+
import Error from './+error.svelte';
1519
1620
export let data: PageData;
1721
let copied = false;
@@ -66,6 +70,58 @@
6670
data.email = email.email;
6771
}
6872
}
73+
74+
let file: File | null = null;
75+
let fileContents = '';
76+
async function uploadFile() {
77+
if (!turnstileResponse) return;
78+
if (file && fileContents) {
79+
toast.warning(
80+
'Please Either upload a file or paste the contents, not both, refresh the page to try again'
81+
);
82+
return;
83+
}
84+
85+
pending = true;
86+
const formData = new FormData();
87+
if (fileContents) {
88+
formData.append(
89+
'file',
90+
new Blob([fileContents], { type: 'message/rfc822' }),
91+
'email.eml'
92+
);
93+
} else if (file) {
94+
formData.append('file', file, 'email.eml');
95+
} else {
96+
toast.warning('Please upload a file or paste the contents');
97+
return;
98+
}
99+
formData.append('token', turnstileResponse);
100+
101+
const response = await fetch('/api/upload-file', {
102+
method: 'POST',
103+
body: formData
104+
})
105+
.then((res) => res.json())
106+
.catch((err) => {
107+
toast(
108+
err instanceof Error
109+
? err.message
110+
: 'Failed to upload email. Please try again '
111+
);
112+
return null;
113+
})
114+
.finally(() => {
115+
pending = false;
116+
});
117+
118+
if (response.success) {
119+
toast('Email uploaded successfully. Redirecting to the result page...');
120+
goto('/result');
121+
} else {
122+
toast(response.message);
123+
}
124+
}
69125
</script>
70126

71127
<main
@@ -88,11 +144,21 @@
88144
target="_blank">UnInbox</a> to clean up emails before displaying them to users.
89145
</p>
90146
<h2 class="pt-6 text-3xl font-bold">Get a Live Demo</h2>
91-
<Tabs.Root value="email">
147+
<Tabs.Root value="file">
92148
<Tabs.List>
93-
<Tabs.Trigger value="email">
94-
<span class="flex gap-1">Send us an Email</span>
95-
</Tabs.Trigger>
149+
<Tooltip.Root>
150+
<Tooltip.Trigger>
151+
<Tabs.Trigger
152+
value="email"
153+
disabled>
154+
<span class="flex gap-1">Send us an Email</span>
155+
</Tabs.Trigger>
156+
</Tooltip.Trigger>
157+
<Tooltip.Content>
158+
Disabled for now as we have disabled the spare email server
159+
temporarily.
160+
</Tooltip.Content>
161+
</Tooltip.Root>
96162
<Tabs.Trigger value="file">
97163
<span class="flex gap-1">
98164
<code>.eml</code> File
@@ -174,12 +240,7 @@
174240
We will generate a unique email address for you to send your
175241
mail.
176242
</h2>
177-
<Turnstile
178-
siteKey={PUBLIC_TURNSTILE_SITE_KEY}
179-
theme="auto"
180-
on:turnstile-callback={(e) => {
181-
turnstileResponse = e.detail.token;
182-
}} />
243+
183244
<Button
184245
variant="default"
185246
disabled={!turnstileResponse || pending}
@@ -213,13 +274,11 @@
213274
</Card.Description>
214275
</Card.Header>
215276
<Card.Content>
216-
<div class="flex flex-col">
217-
<h2 class="mx-auto text-2xl font-bold">
218-
<span class="animate-pulse">🚧</span>
219-
<span>Work in Progress</span>
220-
<span class="animate-pulse">🚧</span>
221-
</h2>
222-
<!-- <Dropzone />
277+
<div class="flex flex-col items-center justify-center gap-2">
278+
<Dropzone
279+
onFileAdded={(f) => {
280+
file = f;
281+
}} />
223282
<div
224283
class="relative mx-auto flex w-3/4 items-center justify-center px-2 py-8">
225284
<hr class="w-full" />
@@ -229,10 +288,34 @@
229288
</div>
230289
<Textarea
231290
placeholder="Paste file contents here"
232-
class="font-mono" />
233-
</div> -->
234-
</div></Card.Content>
291+
class="font-mono"
292+
bind:value={fileContents} />
293+
294+
<Button
295+
variant="default"
296+
disabled={!turnstileResponse ||
297+
pending ||
298+
(!file && !fileContents)}
299+
on:click={uploadFile}>
300+
{#if !turnstileResponse}
301+
Waiting for Captcha...
302+
{:else if pending}
303+
Uploading...
304+
{:else}
305+
Upload
306+
{/if}
307+
</Button>
308+
</div>
309+
</Card.Content>
235310
</Card.Root>
236311
</Tabs.Content>
237312
</Tabs.Root>
313+
<div class="mx-auto flex w-fit flex-col items-center justify-center gap-2">
314+
<Turnstile
315+
siteKey={PUBLIC_TURNSTILE_SITE_KEY}
316+
theme="auto"
317+
on:turnstile-callback={(e) => {
318+
turnstileResponse = e.detail.token;
319+
}} />
320+
</div>
238321
</main>

apps/mailtools/src/routes/api/generate-email/+server.ts

+4
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@ import { error, json, type RequestHandler } from '@sveltejs/kit';
77
import verifyTurnstileToken from '@lib/turnstile';
88
import { TURNSTILE_SECRET_KEY } from '$env/static/private';
99

10+
const emailDisabled = true;
11+
1012
export const POST: RequestHandler = async ({
1113
cookies,
1214
request,
1315
getClientAddress
1416
}) => {
17+
if (emailDisabled) return error(404, 'Email generation is disabled');
18+
1519
const body = await request.json();
1620
if (!body.token) {
1721
error(400, 'Missing turnstile response');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { db } from '@lib/db';
2+
import { emails, emailStorage } from '@lib/db/schema';
3+
import { nanoid } from 'nanoid';
4+
import { error, json, type RequestHandler } from '@sveltejs/kit';
5+
import verifyTurnstileToken from '@lib/turnstile';
6+
import { TURNSTILE_SECRET_KEY } from '$env/static/private';
7+
import { simpleParser } from 'mailparser';
8+
9+
export const POST: RequestHandler = async ({
10+
cookies,
11+
request,
12+
getClientAddress
13+
}) => {
14+
const body = await request.formData();
15+
const cloudflareToken = body.get('token');
16+
17+
if (typeof cloudflareToken !== 'string' || !cloudflareToken) {
18+
error(400, 'Missing turnstile response');
19+
}
20+
const valid = await verifyTurnstileToken({
21+
response: cloudflareToken,
22+
secretKey: TURNSTILE_SECRET_KEY,
23+
remoteIp: getClientAddress()
24+
});
25+
if (!valid) {
26+
error(400, 'Invalid turnstile response');
27+
}
28+
const email = body.get('file');
29+
if (!(email instanceof File)) {
30+
error(400, 'Missing email file');
31+
}
32+
33+
const rawEmail = await email.text();
34+
35+
const parsedEmail = await simpleParser(rawEmail).catch((err) => {
36+
if (err instanceof Error) {
37+
return { error: err.message };
38+
} else {
39+
return { error: 'Unknown error' };
40+
}
41+
});
42+
43+
if ('error' in parsedEmail) {
44+
error(400, parsedEmail.error);
45+
}
46+
console.log(parsedEmail);
47+
48+
const bodyHtml = parsedEmail.html || parsedEmail.textAsHtml;
49+
if (!bodyHtml) {
50+
error(400, 'Email has no body');
51+
}
52+
53+
const token = nanoid();
54+
const insert = await db.insert(emails).values({
55+
emailAddress: 'uploaded-email',
56+
token,
57+
emailReceived: true
58+
});
59+
60+
await db.insert(emailStorage).values({
61+
emailId: Number(insert.lastInsertRowid ?? 0),
62+
emailFrom: parsedEmail.from?.value[0].address || 'unknown',
63+
emailContent: bodyHtml
64+
});
65+
66+
cookies.set('token', token, { path: '/' });
67+
return json({ success: true });
68+
};

apps/mailtools/src/routes/result/+page.server.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { error } from '@sveltejs/kit';
77
export const load: PageServerLoad = async ({ cookies, url }) => {
88
const token = url.searchParams.get('token') || cookies.get('token');
99
const email = await db.query.emails.findFirst({
10-
where: and(eq(emails.token, token || '')),
10+
where: and(eq(emails.token, token || 'none')),
1111
columns: {
1212
emailReceived: true
1313
},

0 commit comments

Comments
 (0)