feat: SMTP server (#47)
* WIP: SMTP server * Card and minor changes in Server --------- Co-authored-by: KM Koushik <koushikmohan1996@gmail.com>
This commit is contained in:
committed by
GitHub
parent
d74b20bac8
commit
0b82eb2266
28
apps/smtp-server/package.json
Normal file
28
apps/smtp-server/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "smtp-server",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "tsc",
|
||||
"start": "node dist/server.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@types/dotenv": "^8.2.0",
|
||||
"@types/mailparser": "^3.4.4",
|
||||
"@types/smtp-server": "^3.5.10",
|
||||
"dotenv": "^16.4.5",
|
||||
"mailparser": "^3.7.1",
|
||||
"nodemailer": "^6.9.14",
|
||||
"smtp-server": "^3.13.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.1.0",
|
||||
"@types/nodemailer": "^6.4.15",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
101
apps/smtp-server/src/server.ts
Normal file
101
apps/smtp-server/src/server.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { SMTPServer, SMTPServerOptions } from 'smtp-server';
|
||||
import { Readable } from 'stream';
|
||||
import dotenv from 'dotenv';
|
||||
import { simpleParser } from 'mailparser';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const AUTH_USERNAME = process.env.SMTP_AUTH_USERNAME!;
|
||||
const UNSEND_BASE_URL = process.env.UNSEND_BASE_URL!;
|
||||
let API_KEY = '';
|
||||
|
||||
const ports = [25, 465, 2465, 587, 2587]; // Array of ports to listen on
|
||||
|
||||
const serverOptions: SMTPServerOptions = {
|
||||
onData(stream: Readable, session: any, callback: (error?: Error) => void) {
|
||||
console.log('Receiving email data...'); // Debug statement
|
||||
simpleParser(stream, (err, parsed) => {
|
||||
if (err) {
|
||||
console.error('Failed to parse email data:', err.message);
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const emailObject: any = {
|
||||
to: Array.isArray(parsed.to) ? parsed.to.map(addr => addr.text).join(', ') : parsed.to?.text,
|
||||
from: Array.isArray(parsed.from) ? parsed.from.map(addr => addr.text).join(', ') : parsed.from?.text,
|
||||
subject: parsed.subject,
|
||||
text: parsed.text,
|
||||
html: parsed.html,
|
||||
};
|
||||
|
||||
console.log('Parsed email data:', emailObject); // Debug statement
|
||||
|
||||
sendEmailToUnsend(emailObject)
|
||||
.then(() => callback())
|
||||
.catch((error) => {
|
||||
console.error('Failed to send email:', error.message);
|
||||
callback(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
onAuth(auth: any, session: any, callback: (error?: Error, user?: any) => void) {
|
||||
API_KEY = auth.password;
|
||||
if (auth.username === AUTH_USERNAME) {
|
||||
console.log('Authenticated successfully'); // Debug statement
|
||||
callback(undefined, { user: AUTH_USERNAME });
|
||||
} else {
|
||||
console.error('Invalid username or password');
|
||||
callback(new Error('Invalid username or password'));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
async function sendEmailToUnsend(emailData: any) {
|
||||
try {
|
||||
const apiEndpoint = '/api/v1/emails';
|
||||
const url = new URL(apiEndpoint, UNSEND_BASE_URL); // Combine base URL with endpoint
|
||||
console.log('Sending email to Unsend API at:', url.href); // Debug statement
|
||||
|
||||
const response = await fetch(url.href, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(emailData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
console.error('Unsend API error response:', errorData);
|
||||
throw new Error(`Failed to send email: ${errorData.message || 'Unknown error from server'}`);
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
console.log('Unsend API response:', responseData);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error('Error message:', error.message);
|
||||
throw new Error(`Failed to send email: ${error.message}`);
|
||||
} else {
|
||||
console.error('Unexpected error:', error);
|
||||
throw new Error('Failed to send email: Unexpected error occurred');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function startServers() {
|
||||
ports.forEach(port => {
|
||||
const server = new SMTPServer(serverOptions);
|
||||
|
||||
server.listen(port, () => {
|
||||
console.log(`SMTP server is listening on port ${port}`);
|
||||
});
|
||||
|
||||
server.on('error', (err) => {
|
||||
console.error(`Error occurred on port ${port}:`, err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
startServers();
|
37
apps/smtp-server/src/usage.js
Normal file
37
apps/smtp-server/src/usage.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const nodemailer = require('nodemailer');
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: "localhost",
|
||||
port: 2587,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: "unsend",
|
||||
pass: "us_38de56vwa7_cc90a91b01a402de0c15516b3554adc1",
|
||||
},
|
||||
tls: {
|
||||
rejectUnauthorized: false,
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
const mailOptions = {
|
||||
to: "harsh121102@gmail.com",
|
||||
from: "hello@support.harshbhat.me",
|
||||
subject: "Testing SMTP",
|
||||
html: "<strong>THIS IS USING SMTP,</strong><p>Unsend is the best open source sending platform<p><p>check out <a href='https://unsend.dev'>unsend.dev</a>",
|
||||
text: "hello,\n\nUnsend is the best open source sending platform",
|
||||
};
|
||||
|
||||
|
||||
transporter.sendMail(mailOptions, (error, info) => {
|
||||
if (error) {
|
||||
console.error('Error sending email:', error);
|
||||
} else {
|
||||
console.log('Email sent successfully:', info.response);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
24
apps/smtp-server/tsconfig.json
Normal file
24
apps/smtp-server/tsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Language and Environment */
|
||||
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
"module": "commonjs", /* Specify what module code is generated. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Modules */
|
||||
"rootDir": "./src", /* Specify the root folder within your source files. */
|
||||
"outDir": "./dist", /* Specify an output folder for all emitted files. */
|
||||
|
||||
/* Emit */
|
||||
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
"removeComments": true, /* Disable emitting comments. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
},
|
||||
"include": ["src/**/*.ts"], /* Include all TypeScript files in the src directory. */
|
||||
"exclude": ["node_modules"] /* Exclude node_modules from compilation. */
|
||||
}
|
@@ -2,15 +2,43 @@
|
||||
|
||||
import ApiList from "./api-list";
|
||||
import AddApiKey from "./add-api-key";
|
||||
import Smtp from "./smtp";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@unsend/ui/src/tabs";
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function ApiKeysPage() {
|
||||
const [activeTab, setActiveTab] = useState("apiKeys");
|
||||
const disableSmtp = true;
|
||||
const handleTabChange = (value: any) => {
|
||||
if (value === "smtp" && disableSmtp) {
|
||||
return;
|
||||
}
|
||||
setActiveTab(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="font-bold text-lg">API Keys</h1>
|
||||
<AddApiKey />
|
||||
</div>
|
||||
<ApiList />
|
||||
<Tabs defaultValue="apiKeys" value={activeTab} onValueChange={handleTabChange}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="apiKeys">API keys</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="smtp"
|
||||
className={`cursor-pointer ${disableSmtp ? 'opacity-50 pointer-events-none' : ''}`}
|
||||
>
|
||||
SMTP
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="apiKeys">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="font-bold text-lg">API Keys</h1>
|
||||
<AddApiKey />
|
||||
</div>
|
||||
<ApiList />
|
||||
</TabsContent>
|
||||
<TabsContent value="smtp">
|
||||
<Smtp />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
54
apps/web/src/app/(dashboard)/api-keys/smtp.tsx
Normal file
54
apps/web/src/app/(dashboard)/api-keys/smtp.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
"use client";
|
||||
import * as React from "react";
|
||||
import { Code } from "@unsend/ui/src/code";
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@unsend/ui/src/card";
|
||||
import { TextWithCopyButton } from "@unsend/ui/src/text-with-copy";
|
||||
|
||||
export default function ExampleCard() {
|
||||
const smtpDetails = {
|
||||
smtp: "smtp.example.com",
|
||||
port: "587",
|
||||
user: "user@example.com",
|
||||
password: "supersecretpassword"
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="mt-9">
|
||||
<CardHeader>
|
||||
<CardTitle>SMTP</CardTitle>
|
||||
<CardDescription>
|
||||
Send emails using SMTP instead of the REST API. See documentation for more information.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<strong>Host:</strong>
|
||||
<TextWithCopyButton className="ml-1 text-zinc-500 rounded-lg mt-1 p-2 w-full bg-gray-900" value={"smtp.unsend.dev"}></TextWithCopyButton>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Port:</strong>
|
||||
<TextWithCopyButton className="ml-1 text-zinc-500 rounded-lg mt-1 p-2 w-full bg-gray-900" value={"465"}></TextWithCopyButton>
|
||||
<p className="ml-1 mt-1 text-zinc-500 ">For encrypted/TLS connections use <strong>2465</strong>, <strong>587</strong> or <strong>2587</strong></p>
|
||||
</div>
|
||||
<div>
|
||||
<strong>User:</strong>
|
||||
<TextWithCopyButton className="ml-1 text-zinc-500 rounded-lg mt-1 p-2 w-full bg-gray-900" value={"unsend"}></TextWithCopyButton>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Password:</strong>
|
||||
<TextWithCopyButton className="ml-1 text-zinc-500 rounded-lg mt-1 p-2 w-full bg-gray-900" value={"YOUR_API_KEY"}></TextWithCopyButton>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user