diff --git a/apps/smtp-server/package.json b/apps/smtp-server/package.json index 2afb1a8..91b76f5 100644 --- a/apps/smtp-server/package.json +++ b/apps/smtp-server/package.json @@ -5,7 +5,7 @@ "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "build": "tsc", + "build": "tsup", "start": "node dist/server.js" }, "keywords": [], @@ -23,6 +23,7 @@ "devDependencies": { "@types/node": "^22.1.0", "@types/nodemailer": "^6.4.15", + "tsup": "^8.0.2", "typescript": "^5.5.4" } -} +} \ No newline at end of file diff --git a/apps/smtp-server/src/server.ts b/apps/smtp-server/src/server.ts index f62ae05..d8c0cbe 100644 --- a/apps/smtp-server/src/server.ts +++ b/apps/smtp-server/src/server.ts @@ -1,98 +1,130 @@ -import { SMTPServer, SMTPServerOptions } from 'smtp-server'; -import { Readable } from 'stream'; -import dotenv from 'dotenv'; -import { simpleParser } from 'mailparser'; +import { SMTPServer, SMTPServerOptions, SMTPServerSession } from "smtp-server"; +import { Readable } from "stream"; +import dotenv from "dotenv"; +import { simpleParser } from "mailparser"; +import { readFileSync } from "fs"; dotenv.config(); -const AUTH_USERNAME = process.env.SMTP_AUTH_USERNAME!; -const UNSEND_BASE_URL = process.env.UNSEND_BASE_URL!; -let API_KEY = ''; +const AUTH_USERNAME = process.env.SMTP_AUTH_USERNAME ?? "unsend"; +const UNSEND_BASE_URL = process.env.UNSEND_BASE_URL ?? "https://app.unsend.dev"; +const SSL_KEY_PATH = process.env.UNSEND_API_KEY_PATH; +const SSL_CERT_PATH = process.env.UNSEND_API_CERT_PATH; -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) { +async function sendEmailToUnsend(emailData: any, apiKey: string) { try { - const apiEndpoint = '/api/v1/emails'; + 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 + console.log("Sending email to Unsend API at:", url.href); // Debug statement const response = await fetch(url.href, { - method: 'POST', + method: "POST", headers: { - 'Authorization': `Bearer ${API_KEY}`, - 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + "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'}`); + console.error("Unsend API error response:", errorData); + throw new Error( + `Failed to send email: ${errorData.error.message || "Unknown error from server"}` + ); } const responseData = await response.json(); - console.log('Unsend API response:', responseData); + console.log("Unsend API response:", responseData); } catch (error) { if (error instanceof Error) { - console.error('Error message:', error.message); + 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'); + console.error("Unexpected error:", error); + throw new Error("Failed to send email: Unexpected error occurred"); } } } +const serverOptions: SMTPServerOptions = { + secure: false, + key: SSL_KEY_PATH ? readFileSync(SSL_KEY_PATH) : undefined, + cert: SSL_CERT_PATH ? readFileSync(SSL_CERT_PATH) : undefined, + onData( + stream: Readable, + session: SMTPServerSession, + 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); + } + + if (!session.user) { + console.error("No API key found in session"); + return callback(new Error("No API key found in session")); + } + + const emailObject = { + 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, session.user) + .then(() => callback()) + .catch((error) => { + console.error("Failed to send email:", error.message); + callback(error); + }); + }); + }, + onAuth(auth, session: any, callback: (error?: Error, user?: any) => void) { + if (auth.username === AUTH_USERNAME && auth.password) { + console.log("Authenticated successfully"); // Debug statement + callback(undefined, { user: auth.password }); + } else { + console.error("Invalid username or password"); + callback(new Error("Invalid username or password")); + } + }, + size: 10485760, +}; + function startServers() { - ports.forEach(port => { + // Implicit SSL/TLS for ports 465 and 2465 + [465, 2465].forEach((port) => { + const server = new SMTPServer({ ...serverOptions, secure: true }); + + server.listen(port, () => { + console.log(`Implicit SSL/TLS SMTP server is listening on port ${port}`); + }); + + server.on("error", (err) => { + console.error(`Error occurred on port ${port}:`, err); + }); + }); + + // STARTTLS for ports 25, 587, and 2587 + [25, 587, 2587].forEach((port) => { const server = new SMTPServer(serverOptions); server.listen(port, () => { - console.log(`SMTP server is listening on port ${port}`); + console.log(`STARTTLS SMTP server is listening on port ${port}`); }); - server.on('error', (err) => { + server.on("error", (err) => { console.error(`Error occurred on port ${port}:`, err); }); }); diff --git a/apps/smtp-server/src/usage.js b/apps/smtp-server/src/usage.js index 5e91071..eedd2e1 100644 --- a/apps/smtp-server/src/usage.js +++ b/apps/smtp-server/src/usage.js @@ -1,37 +1,30 @@ -const nodemailer = require('nodemailer'); +const nodemailer = require("nodemailer"); const transporter = nodemailer.createTransport({ - host: "localhost", - port: 2587, - secure: false, - auth: { - user: "unsend", - pass: "us_38de56vwa7_cc90a91b01a402de0c15516b3554adc1", - }, - tls: { - rejectUnauthorized: false, - } - }); - - + 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: "THIS IS USING SMTP,

Unsend is the best open source sending platform

check out unsend.dev", - text: "hello,\n\nUnsend is the best open source sending platform", + to: "harsh121102@gmail.com", + from: "hello@support.harshbhat.me", + subject: "Testing SMTP", + html: "THIS IS USING SMTP,

Unsend is the best open source sending platform

check out unsend.dev", + text: "hello,\n\nUnsend is the best open source sending platform", }; - transporter.sendMail(mailOptions, (error, info) => { if (error) { - console.error('Error sending email:', error); + console.error("Error sending email:", error); } else { - console.log('Email sent successfully:', info.response); + console.log("Email sent successfully:", info.response); } }); - - - - diff --git a/apps/smtp-server/tsup.config.ts b/apps/smtp-server/tsup.config.ts new file mode 100644 index 0000000..e7d1815 --- /dev/null +++ b/apps/smtp-server/tsup.config.ts @@ -0,0 +1,13 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { defineConfig, Options } from "tsup"; + +// eslint-disable-next-line import/no-default-export +export default defineConfig((options: Options) => ({ + entry: ["src/server.ts"], + format: ["cjs"], + dts: true, + minify: true, + clean: true, + injectStyle: true, + ...options, +})); diff --git a/package.json b/package.json index df4a780..c8ef2e3 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "build:editor": "turbo build --filter=@unsend/email-editor", "build:web": "turbo build --filter=@unsend/email-editor --filter=web ", "build:marketing": "turbo build --filter=@unsend/email-editor --filter=marketing", + "build:smtp": "turbo build --filter=smtp-server", "dev": "pnpm load-env -- turbo dev", "dev:docs": "cd apps/docs && mintlify dev", "lint": "turbo lint", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bb8ab58..3f41ba4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -122,6 +122,9 @@ importers: '@types/nodemailer': specifier: ^6.4.15 version: 6.4.15 + tsup: + specifier: ^8.0.2 + version: 8.0.2(typescript@5.5.4) typescript: specifier: ^5.5.4 version: 5.5.4 @@ -7493,12 +7496,13 @@ packages: /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + /balanced-match@2.0.0: + resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} + dev: false + /base32.js@0.1.0: resolution: {integrity: sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==} engines: {node: '>=0.12.0'} - - /balanced-match@2.0.0: - resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} dev: false /base64-js@1.5.1: @@ -11491,6 +11495,12 @@ packages: engines: {node: '>=12'} dev: false + /magic-string@0.30.10: + resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: false + /mailparser@3.7.1: resolution: {integrity: sha512-RCnBhy5q8XtB3mXzxcAfT1huNqN93HTYYyL6XawlIKycfxM/rXPg9tXoZ7D46+SgCS1zxKzw+BayDQSvncSTTw==} dependencies: @@ -11512,10 +11522,6 @@ packages: libbase64: 1.2.1 libmime: 5.2.0 libqp: 2.0.1 - /magic-string@0.30.10: - resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} - dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 dev: false /map-age-cleaner@0.1.3: @@ -13764,7 +13770,6 @@ packages: engines: {node: '>=6'} dev: false - /punycode@1.4.1: resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} dev: false @@ -15491,10 +15496,6 @@ packages: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} dev: false - /tlds@1.252.0: - resolution: {integrity: sha512-GA16+8HXvqtfEnw/DTcwB0UU354QE1n3+wh08oFjr6Znl7ZLAeUgYzCcK+/CCrOyE0vnHR8/pu3XXG3vDijXpQ==} - hasBin: true - /tippy.js@6.3.7: resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} dependencies: @@ -15510,6 +15511,11 @@ packages: engines: {node: '>=18'} dev: false + /tlds@1.252.0: + resolution: {integrity: sha512-GA16+8HXvqtfEnw/DTcwB0UU354QE1n3+wh08oFjr6Znl7ZLAeUgYzCcK+/CCrOyE0vnHR8/pu3XXG3vDijXpQ==} + hasBin: true + dev: false + /tldts-core@6.1.16: resolution: {integrity: sha512-rxnuCux+zn3hMF57nBzr1m1qGZH7Od2ErbDZjVm04fk76cEynTg3zqvHjx5BsBl8lvRTjpzIhsEGMHDH/Hr2Vw==} dev: false @@ -15662,6 +15668,45 @@ packages: - ts-node dev: true + /tsup@8.0.2(typescript@5.5.4): + resolution: {integrity: sha512-NY8xtQXdH7hDUAZwcQdY/Vzlw9johQsaqf7iwZ6g1DOUlFYQ5/AtVAjTvihhEyeRlGo4dLRVHtrRaL35M1daqQ==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + dependencies: + bundle-require: 4.1.0(esbuild@0.19.12) + cac: 6.7.14 + chokidar: 3.6.0 + debug: 4.3.4 + esbuild: 0.19.12 + execa: 5.1.1 + globby: 11.1.0 + joycon: 3.1.1 + postcss-load-config: 4.0.2(postcss@8.4.38) + resolve-from: 5.0.0 + rollup: 4.18.0 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tree-kill: 1.2.2 + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + - ts-node + dev: true + /tsutils@3.21.0(typescript@5.4.2): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'}