├── public ├── chunk.js ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── index.html └── 404.html ├── run.sh ├── src ├── interfaces │ ├── domain.interface.ts │ ├── dns.interface.ts │ └── mail.interface.ts ├── main.ts ├── types │ ├── index.ts │ └── ics.interface.ts ├── lib │ ├── types │ │ └── index.d.ts │ ├── logs │ │ └── index.ts │ └── helpers │ │ ├── index.ts │ │ └── utils.ts ├── start.ts ├── server │ ├── index.ts │ ├── config │ │ ├── MailConfig.ts │ │ ├── DnsChecker.ts │ │ ├── DnsRecordGenrator.ts │ │ └── SpamFilteration.ts │ ├── IncomingServer.ts │ ├── OutgoingServer.ts │ └── mta │ │ ├── node-mta.ts │ │ └── go-mta.go └── services │ ├── MeetingService.ts │ ├── IcsEvents.ts │ ├── IncomingMailHandler.ts │ └── OutgoingMailHandler.ts ├── .gitignore ├── bun.lockb ├── image.png ├── image-1.png ├── image-2.png ├── image-3.png ├── imap ├── image.png └── readme.md ├── .env ├── tsconfig.json ├── biome.json ├── package.json ├── setup.js ├── send-mail.js └── README.md /public/chunk.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | node ./setup.js -------------------------------------------------------------------------------- /src/interfaces/domain.interface.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # deps 2 | build 3 | dist 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/problem/node-mail-server/main/bun.lockb -------------------------------------------------------------------------------- /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/problem/node-mail-server/main/image.png -------------------------------------------------------------------------------- /image-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/problem/node-mail-server/main/image-1.png -------------------------------------------------------------------------------- /image-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/problem/node-mail-server/main/image-2.png -------------------------------------------------------------------------------- /image-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/problem/node-mail-server/main/image-3.png -------------------------------------------------------------------------------- /imap/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/problem/node-mail-server/main/imap/image.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/problem/node-mail-server/main/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/problem/node-mail-server/main/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/problem/node-mail-server/main/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/problem/node-mail-server/main/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/problem/node-mail-server/main/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/problem/node-mail-server/main/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | INCOMING_MAIL_HOST=mx.cirrusmail.cloud 2 | OUTGOING_MAIL_HOST=mail.cirrusmail.cloud 3 | MAIL_SERVER_IP=185.193.19.118 4 | MAX_EMAILS_PER_MINUTE=5 5 | TLS_PRIVATE_KEY_PATH= 6 | TLS_CERTFICATE_PATH= -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import "reflect-metadata"; 3 | import { SMTP_SERVICE } from "./server"; 4 | 5 | function main() { 6 | SMTP_SERVICE.Initialize(); 7 | } 8 | 9 | export default main(); 10 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | type ENV = { 2 | INCOMING_MAIL_HOST: string; 3 | OUTGOING_MAIL_HOST: string; 4 | MAIL_SERVER_IP: string; 5 | MAX_EMAILS_PER_MINUTE: string; 6 | TLS_PRIVATE_KEY_PATH: string; 7 | TLS_CERTFICATE_PATH: string; 8 | [key: string]: string; 9 | }; 10 | declare global { 11 | namespace NodeJS { 12 | interface ProcessEnv extends ENV {} 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/types/ics.interface.ts: -------------------------------------------------------------------------------- 1 | export interface GenreateICSOptions { 2 | uid: string, 3 | method: "REQUEST" | "CANCEL", 4 | title: string, 5 | description: string, 6 | location: string, 7 | start: string, 8 | end: string, 9 | organizer: { 10 | name: string, 11 | email: string}, 12 | attendees: string[], 13 | sequence: number 14 | } -------------------------------------------------------------------------------- /src/lib/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { SMTPServer, SMTPServerDataStream, SMTPServerOptions, Session } from 'smtp-server'; 2 | 3 | declare module 'smtp-server' { 4 | interface SMTPServerOptions { 5 | onData( 6 | stream: SMTPServerDataStream, 7 | session: Session, // Use `Session` instead of `SMTPServerSession` 8 | callback: (err?: Error | null | undefined, message?: string) => void 9 | ): void; 10 | } 11 | } -------------------------------------------------------------------------------- /src/start.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import { writeFileSync } from "fs"; 3 | import { DNSRecordGenerator } from "./server/config/DnsRecordGenrator"; 4 | import { Logging } from "./lib/logs"; 5 | 6 | async function createRecords() { 7 | Logging.dev("Creating DNS Records for your Domain", "notice"); 8 | 9 | const records = await new DNSRecordGenerator( 10 | process.env.DOMAIN_NAME as string, 11 | ).generateAllRecords(); 12 | writeFileSync("records.json", JSON.stringify(records)); 13 | Logging.dev("DNS Records Created", "info"); 14 | } 15 | createRecords(); 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2023", 4 | "experimentalDecorators": true, 5 | "emitDecoratorMetadata": true, 6 | "module": "NodeNext", 7 | "moduleResolution": "NodeNext", 8 | "baseUrl": "./", 9 | "paths": { 10 | "@/*": ["./src/*"] 11 | }, 12 | "resolveJsonModule": true, 13 | "outDir": "./build", 14 | "esModuleInterop": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "strict": true, 17 | "skipLibCheck": true 18 | }, 19 | "include": ["src/**/*.ts", "src/**/*.ini"], 20 | "exclude": ["node_modules", "build"] 21 | } 22 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "ignore": [] 11 | }, 12 | "formatter": { 13 | "enabled": true, 14 | "indentStyle": "tab" 15 | }, 16 | "organizeImports": { 17 | "enabled": true 18 | }, 19 | "linter": { 20 | "enabled": true, 21 | "rules": { 22 | "recommended": true 23 | } 24 | }, 25 | "javascript": { 26 | "formatter": { 27 | "quoteStyle": "double" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodemailserver", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "build/main.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "dev": "ts-node-dev --respawn --transpile-only ./src/main.ts", 9 | "start": "node build/main.js", 10 | "test": "node test.js", 11 | "lint": "biome format --write .", 12 | "build": "tsc -p ." 13 | }, 14 | "dependencies": { 15 | "colorette": "^2.0.20", 16 | "dkim-signer": "^0.2.2", 17 | "dotenv": "^16.4.7", 18 | "glob": "^11.0.1", 19 | "googleapis": "^146.0.0", 20 | "ics": "^3.8.1", 21 | "mailauth": "^4.8.2", 22 | "mailparser": "^3.7.2", 23 | "mime-types": "^2.1.35", 24 | "moment": "^2.30.1", 25 | "node-forge": "^1.3.1", 26 | "nodemailer-smtp-transport": "^2.7.4", 27 | "reflect-metadata": "^0.2.2", 28 | "smtp-server": "^3.13.6", 29 | "ts-node-dev": "^2.0.0" 30 | }, 31 | "devDependencies": { 32 | "@biomejs/biome": "1.9.4", 33 | "@types/dkim-signer": "^0.2.5", 34 | "@types/mailparser": "^3.4.5", 35 | "@types/mime-types": "^2.1.4", 36 | "@types/node": "^22.13.1", 37 | "@types/node-forge": "^1.3.11", 38 | "@types/nodemailer-express-handlebars": "^4.0.5", 39 | "@types/smtp-server": "^3.5.10", 40 | "readline": "^1.3.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/interfaces/dns.interface.ts: -------------------------------------------------------------------------------- 1 | export type DNSRecordType = 2 | | "A" // IPv4 address 3 | | "AAAA" // IPv6 address 4 | | "MX" // Mail Exchange 5 | | "SPF" // Sender Policy Framework 6 | | "DMARC" // Domain-based Message Authentication, Reporting & Conformance 7 | | "TXT" // Text Record 8 | | "CNAME" // Canonical Name 9 | | "PTR" // Pointer Record (reverse DNS) 10 | | "NS" // Name Server 11 | | "SRV" // Service Locator 12 | | "SOA" // Start of Authority 13 | | "CAA" // Certification Authority Authorization 14 | | "DNSSEC" // DNS Security Extensions 15 | | "TLSA" // TLSA (DANE) 16 | | "DS" // Delegation Signer 17 | | "HINFO" // Host Information 18 | | "LOC"; 19 | export interface DNSResponse { 20 | Status: number; 21 | TC: boolean; 22 | RD: boolean; 23 | RA: boolean; 24 | AD: boolean; 25 | CD: boolean; 26 | Question: Question[]; 27 | Answer: Answer[]; 28 | Authority: Answer[]; 29 | Comment: string; 30 | } 31 | 32 | export interface Answer { 33 | name: string; 34 | type: number; 35 | TTL: number; 36 | data: string; 37 | } 38 | 39 | export interface Question { 40 | name: string; 41 | type: number; 42 | } 43 | export type RecordTypeRespose = { 44 | type: number; 45 | name: string; 46 | ttl: number; 47 | data: string; 48 | }; 49 | -------------------------------------------------------------------------------- /src/server/index.ts: -------------------------------------------------------------------------------- 1 | import { IncomingServerConfig } from "./IncomingServer"; 2 | import { OutgoingServerConfig } from "./OutgoingServer"; 3 | 4 | /** More Resources for Further Research 5 | * https:*github.com/devmehq/email-validator-js 6 | * https:*github.com/forwardemail/free-email-forwarding/tree/master 7 | * https:*iredmail.org/ 8 | * https:*github.com/zone-eu/zone-mta 9 | * https:*devsrealm.com/posts/b52728be7a1dbe83/send-only-mail-server-with-haraka 10 | * https:*iaziz786.com/blog/self-hosted-smtp-haraka/ 11 | * https:*www.jetmore.org/john/code/swaks/installation.html 12 | * https:*github.com/guileen/node-sendmail/blob/master/examples/meetingRequest.js 13 | * https:*datatracker.ietf.org/doc/html/rfc8461 14 | */ 15 | 16 | export class SMTP_SERVICE { 17 | private static _instance: SMTP_SERVICE | null = null; 18 | private readonly INCOMING_MAIL_HOST = process.env.INCOMING_MAIL_HOST 19 | private readonly OUTGOING_MAIL_HOST = process.env.OUTGOING_MAIL_HOST || process.env.INCOMING_MAIL_HOST 20 | constructor() { 21 | const incomingServer = new IncomingServerConfig(this.INCOMING_MAIL_HOST); 22 | const outgoingServer = new OutgoingServerConfig(this.OUTGOING_MAIL_HOST); 23 | 24 | incomingServer.start(); 25 | outgoingServer.start(); 26 | } 27 | static Initialize() { 28 | if (!SMTP_SERVICE._instance) { 29 | SMTP_SERVICE._instance = new SMTP_SERVICE(); 30 | } 31 | return SMTP_SERVICE._instance; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/services/MeetingService.ts: -------------------------------------------------------------------------------- 1 | import { google } from "googleapis"; 2 | import utils from "@/lib/helpers/utils"; 3 | 4 | const SCOPES = ["https://www.googleapis.com/auth/calendar"]; 5 | 6 | const oauth2Client = new google.auth.GoogleAuth({ 7 | apiKey: "your_api_key", 8 | scopes: SCOPES, 9 | }) 10 | 11 | const calendar = google.calendar({ version: "v3", auth: oauth2Client }); 12 | export class MeetingService { 13 | async createGoogleMeetEvent(title: string, description: string, startTime: string, endTime: string, attendees: string[]) { 14 | const event = { 15 | summary: title || "Team Meeting", 16 | description: description || "Discuss project updates", 17 | start: { 18 | dateTime: startTime, 19 | timeZone: "UTC", 20 | }, 21 | end: { 22 | dateTime: endTime, 23 | timeZone: "UTC", 24 | }, 25 | conferenceData: { 26 | createRequest: { 27 | requestId: utils.uuid_v4(), 28 | conferenceSolutionKey: { type: "hangoutsMeet" }, 29 | }, 30 | }, 31 | attendees: attendees.map((email) => ({ email })), 32 | }; 33 | const response = await calendar.events.insert({ 34 | sendNotifications: true, 35 | calendarId: "primary", 36 | requestBody: event, 37 | conferenceDataVersion: 1, 38 | }); 39 | 40 | return response.data.hangoutLink as string; 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /src/interfaces/mail.interface.ts: -------------------------------------------------------------------------------- 1 | import { HeaderLines } from "mailparser"; 2 | import { TemplateOptions } from "nodemailer-express-handlebars"; 3 | export interface MailDataDto { 4 | message_id: string; // Unique identifier for each mail 5 | from: string; // Sender email 6 | receipients: string[] | []; 7 | to: string; 8 | subject: string | undefined; // Subject of the mail 9 | hasAttachment: any; // Attachment field, can be a boolean or an object with content and related flag 10 | isRead: boolean; // Whether the mail is read or not 11 | timestamp: string | undefined; // Timestamp of the mail 12 | title: string; // Title of the mail 13 | shortContent: string; // Preview content of the mail 14 | toWithName?: string | null | undefined; // Preview content of the mail 15 | headersLine?: { key: string; line: string } | HeaderLines; // Optional headers as key-value pairs 16 | headers?: object; // Optional additional headers as key-value pairs 17 | contents?: string; 18 | isStarred?: boolean; 19 | synced: boolean; 20 | } 21 | export interface FileAttachmentInterface { 22 | type: string; 23 | content: Buffer; 24 | contentType: string; 25 | partId: string; 26 | release: string | null; 27 | contentDisposition: string; 28 | filename: string; 29 | headers: Map; 30 | checksum: string; 31 | size: number; 32 | } 33 | export interface MailOptions { 34 | from?: string; 35 | to: string | string[]; 36 | cc?: string | string[]; 37 | bcc?: string | string[]; 38 | subject: string; 39 | text?: string; 40 | html: string; 41 | } 42 | export type MailOptionsWithTemplate = MailOptions & TemplateOptions; 43 | -------------------------------------------------------------------------------- /src/services/IcsEvents.ts: -------------------------------------------------------------------------------- 1 | 2 | import moment from 'moment'; 3 | 4 | import * as ics from 'ics' 5 | import { MeetingService } from './MeetingService'; 6 | import { GenreateICSOptions } from '@/types/ics.interface'; 7 | 8 | const meetServiceLink = new MeetingService() 9 | export default class IcalService { 10 | public static async createEvent(data: GenreateICSOptions) { 11 | const startMoment = moment(data.start); 12 | const endMoment = moment(data.end); 13 | let start = startMoment.format('YYYY-M-D-H-m').split("-").map((a) => parseInt(a)) as ics.DateArray 14 | let end = endMoment.add().format("YYYY-M-D-H-m").split("-").map((a) => parseInt(a)) as ics.DateArray 15 | const duration = moment.duration(endMoment.diff(startMoment)) 16 | 17 | const url = await meetServiceLink.createGoogleMeetEvent(data.title, data.description, data.start, data.end, data.attendees) 18 | const attendeeStr = data.attendees.map((email) => ({ email, rsvp: true, role: 'REQ-PARTICIPANT' })) as ics.Attendee[] 19 | 20 | const event: ics.EventAttributes = { 21 | start, 22 | end, 23 | duration: { 24 | hours: Math.floor(duration.asHours()), 25 | minutes: duration.minutes(), 26 | }, 27 | title: data.title, 28 | description: data.description, 29 | // location: 'Folsom Field, University of Colorado (finish line)', 30 | url, 31 | status: 'CONFIRMED', 32 | busyStatus: 'FREE', 33 | organizer: data.organizer, 34 | attendees: attendeeStr 35 | } 36 | return ics.createEvent(event); 37 | 38 | } 39 | } -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | Welcome 13 | 52 | 53 | 54 |
55 |

Welcome to HonoJs - ENJOYS

56 |

We're glad to have you here!

57 |
58 | 59 | 60 | -------------------------------------------------------------------------------- /src/server/config/MailConfig.ts: -------------------------------------------------------------------------------- 1 | import nodemailer from "nodemailer"; 2 | import MailComposer from "nodemailer/lib/mail-composer"; 3 | import Mail from "nodemailer/lib/mailer"; 4 | import { 5 | MailParserOptions, 6 | ParsedMail, 7 | simpleParser, 8 | Source, 9 | MailParser, 10 | } from "mailparser"; 11 | import { DNSChecker } from "./DnsChecker"; 12 | import { Logging } from "../../lib/logs"; 13 | 14 | export let parser = new MailParser(); 15 | 16 | export class MailConfig { 17 | static composeNewMail = (options: Mail.Options): MailComposer => { 18 | const mail = new MailComposer({}); 19 | const stream = mail.compile().createReadStream(); 20 | return mail; 21 | }; 22 | static async ParseEmail( 23 | source: Source, 24 | options?: MailParserOptions | undefined, 25 | ): Promise { 26 | return simpleParser(source, options); 27 | } 28 | static createtransporter(host: string, port: number = 25) { 29 | return nodemailer.createTransport({ 30 | host, 31 | port, 32 | secure: false, 33 | }); 34 | } 35 | static async checkConnections( 36 | host: string, 37 | ): Promise<{ host: string; port: number } | null> { 38 | for (const port of [25, 587, 465]) { 39 | try { 40 | const exchange = await DNSChecker.getMXRecords(host); 41 | if (!exchange) { 42 | return null; 43 | } 44 | return { host: exchange, port }; 45 | } catch (error) { 46 | Logging.dev(`❌ Failed to connect to ${host}:${port}`, "error"); 47 | } 48 | } 49 | return null; 50 | } 51 | static groupRecipientsByDomain( 52 | recipients: string[], 53 | ): Record { 54 | const domainGroups: Record = {}; 55 | recipients.forEach((email) => { 56 | const domain = email.split("@")[1]; 57 | if (!domainGroups[domain]) { 58 | domainGroups[domain] = []; 59 | } 60 | domainGroups[domain].push(email); 61 | }); 62 | 63 | return domainGroups; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | Page Not Found 13 | 51 | 52 | 53 |
54 |

404

55 |

Oops! The page you're looking for doesn't exist.

56 | Go Back Home 57 |
58 | 59 | 60 | -------------------------------------------------------------------------------- /setup.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const dns = require('dns'); 3 | const readline = require('readline'); 4 | const { exec } = require('child_process'); 5 | 6 | const rl = readline.createInterface({ 7 | input: process.stdin, 8 | output: process.stdout 9 | }); 10 | 11 | function askQuestion(query) { 12 | return new Promise((resolve) => { 13 | rl.question(query, (answer) => { 14 | if (!answer.trim()) { 15 | console.log("Input required. Exiting..."); 16 | process.exit(1); 17 | } 18 | resolve(answer.trim()); 19 | }); 20 | }); 21 | } 22 | 23 | 24 | (async function () { 25 | try { 26 | const SERVER_DOMAIN_NAME = await askQuestion("Enter the Mail Server Domain name: "); 27 | const SERVER_IP = await askQuestion("Enter the Mail Server IP: "); 28 | const MAIL_DOMAIN_NAME = await askQuestion("Enter that domain to handle mails: "); 29 | 30 | console.log("Mail Server Domain:", SERVER_DOMAIN_NAME); 31 | console.log("Mail Handling Domain:", MAIL_DOMAIN_NAME); 32 | 33 | 34 | 35 | const envContent = `INCOMING_MAIL_HOST=mx.${SERVER_DOMAIN_NAME}\nOUTGOING_MAIL_HOST=mail.${SERVER_DOMAIN_NAME}\nMAIL_SERVER_IP=${SERVER_IP}\nMAX_EMAILS_PER_MINUTE=5\nTLS_PRIVATE_KEY_PATH=\nTLS_CERTFICATE_PATH=`; 36 | 37 | fs.writeFileSync('.env', envContent, 'utf8'); 38 | console.log("Environment Configuration file created successfully."); 39 | 40 | console.log("Creating DNS Records for your Domain"); 41 | 42 | 43 | exec(`tsc -p . && DOMAIN_NAME=${MAIL_DOMAIN_NAME} node ./build/start.js`,{ 44 | shell: "bash", 45 | }, (error, stdout, stderr) => { 46 | if (error) { 47 | console.error(`Execution error: ${error.message}`); 48 | return; 49 | } 50 | if (stderr) { 51 | console.error(`stderr: ${stderr}`); 52 | return; 53 | } 54 | console.log(`stdout: ${stdout}`); 55 | }); 56 | } catch (error) { 57 | console.error("Error:", error); 58 | } finally { 59 | rl.close(); 60 | } 61 | })(); 62 | -------------------------------------------------------------------------------- /send-mail.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const nodemailer = require('nodemailer'); 3 | const readline = require('readline'); 4 | const dns = require('dns').promises; 5 | 6 | // Function to prompt user input 7 | const rl = readline.createInterface({ 8 | input: process.stdin, 9 | output: process.stdout 10 | }); 11 | 12 | async function askQuestion(query) { 13 | return new Promise(resolve => rl.question(query, resolve)); 14 | } 15 | 16 | async function getMXRecord(email) { 17 | const domain = email.split('@')[1]; 18 | try { 19 | const mxRecords = await dns.resolveMx(domain); 20 | return mxRecords.sort((a, b) => a.priority - b.priority)[0].exchange; 21 | } catch (error) { 22 | 23 | return null; 24 | } 25 | } 26 | 27 | async function sendMail(host, fromEmail, toEmail,pvt_key) { 28 | const transporter = nodemailer.createTransport({ 29 | host, 30 | port: 25, 31 | logger: true, 32 | secure: false, 33 | tls: { 34 | rejectUnauthorized: false 35 | } 36 | }); 37 | 38 | try { 39 | await transporter.verify(); 40 | console.log("Transporter verified successfully"); 41 | 42 | const info = await transporter.sendMail({ 43 | from: fromEmail, 44 | to: toEmail, 45 | subject: 'Hello World', 46 | html: '

Test Email

', 47 | dkim:{ 48 | domainName: fromEmail.split('@')[1], 49 | keySelector: "default", 50 | privateKey: pvt_key 51 | } 52 | }); 53 | 54 | console.log("Email sent:", info); 55 | } catch (error) { 56 | console.error("Error:", error); 57 | } 58 | } 59 | 60 | async function main() { 61 | try { 62 | const fromEmail = await askQuestion("Enter mail from address: "); 63 | const toEmail = await askQuestion("Enter recipient address (only single email): "); 64 | const pvt_key = await askQuestion("Enter your private key: "); 65 | rl.close(); 66 | 67 | const mxServer = await getMXRecord(toEmail); 68 | if (!mxServer) { 69 | throw new Error("MX record not found"); 70 | } 71 | await sendMail(mxServer, fromEmail, toEmail,pvt_key); 72 | } catch (error) { 73 | console.error("Error:", error); 74 | } 75 | } 76 | 77 | main(); 78 | -------------------------------------------------------------------------------- /src/lib/logs/index.ts: -------------------------------------------------------------------------------- 1 | import { bold, greenBright, magenta, red, white, yellow } from "colorette"; 2 | import moment from "moment"; 3 | type LoggingLevel = "info" | "error" | "debug" | "alert" | "notice"; 4 | 5 | class Logger { 6 | public static appName = "NodeMailServer"; 7 | constructor() { 8 | console.log("\u001b[2J\u001b[0;0H"); 9 | console.log( 10 | greenBright( 11 | `[${Logger.appName}] ${yellow(process.pid)} - ${white(moment().format("DD/MM/YYYY hh:mm:ss A"))}, [INFO] ${Logger.name} Service Iniatialized`, 12 | ), 13 | ); 14 | } 15 | setName(name: string) { 16 | return (Logger.appName = name); 17 | } 18 | 19 | /** 20 | * Logs a message to the console. 21 | * 22 | * @param {string} text - The message to be logged. 23 | */ 24 | log(text: string) { 25 | console.log(yellow(`----------- ${text} -------------`)); 26 | } 27 | dev(text: string, type: LoggingLevel = "info") { 28 | if (type === "info") { 29 | return console.log( 30 | greenBright( 31 | `[${Logger.appName}] ${yellow(process.pid)} - ${white(moment().format("DD/MM/YYYY hh:mm:ss A"))}, [${(type).toUpperCase()}] ${text}`, 32 | ), 33 | ); 34 | } 35 | if (type === "error") { 36 | return console.log( 37 | red( 38 | `[${Logger.appName}] ${process.pid} - ${white(moment().format("DD/MM/YYYY hh:mm:ss A"))}, [${(type).toUpperCase()}] ${text}`, 39 | ), 40 | ); 41 | } 42 | if (type === "debug") { 43 | console.log( 44 | bold( 45 | `[${Logger.appName}] ${process.pid} - ${white(moment().format("DD/MM/YYYY hh:mm:ss A"))}, [${(type).toUpperCase()}] ${text}`, 46 | ), 47 | ); 48 | return process.exit(1); 49 | } 50 | if (type === "alert") { 51 | console.log( 52 | magenta( 53 | `[${Logger.appName}] ${yellow(process.pid)} - ${white(moment().format("DD/MM/YYYY hh:mm:ss A"))}, [${(type).toUpperCase()}] ${text}`, 54 | ), 55 | ); 56 | } 57 | if (type === "notice") { 58 | return console.log( 59 | yellow( 60 | `[${Logger.appName}] ${process.pid} - ${white(moment().format("DD/MM/YYYY hh:mm:ss A"))}, [${(type).toUpperCase()}] ${text}`, 61 | ), 62 | ); 63 | } 64 | } 65 | /** 66 | * Prints the given text in the console with a formatted alert message. 67 | * 68 | * @param {string} text - The text to be displayed in the alert message. 69 | */ 70 | alert(text: string) { 71 | console.log(magenta(`----------- ${text} -------------`)); 72 | } 73 | } 74 | 75 | const Logging = new Logger(); 76 | export { Logging }; 77 | -------------------------------------------------------------------------------- /src/server/IncomingServer.ts: -------------------------------------------------------------------------------- 1 | import { SMTPServer, SMTPServerOptions } from "smtp-server"; 2 | import { NewMailHandler } from "../services/IncomingMailHandler"; 3 | import { Logging } from "../lib/logs"; 4 | import { white } from "colorette"; 5 | import { SpamFilteration } from "./config/SpamFilteration"; 6 | 7 | export class IncomingServerConfig { 8 | constructor(private host: string) {} 9 | private INCOMING_SERVER_PORT = 25; 10 | private getOptions(handlers: SMTPServerOptions): SMTPServerOptions { 11 | return { 12 | allowInsecureAuth: false, 13 | logger: false, 14 | secure: false, 15 | name: this.host, 16 | banner: `220 ${this.host} ESMTP NodeSMTP Server is Ready`, 17 | disabledCommands: [""], 18 | authMethods: ["PLAIN", "LOGIN", "CRAM-MD5"], 19 | size: 10 * 1024 * 1024, 20 | disableReverseLookup: true, 21 | useXClient: false, 22 | hidePIPELINING: true, 23 | needsUpgrade: false, 24 | authOptional: true, 25 | useProxy: false, 26 | handshakeTimeout: 60000, 27 | socketTimeout: 60000, // Increase socket timeout to 60 seconds 28 | closeTimeout: 60000, // Increase connection timeout to 60 seconds 29 | maxClients: 1000, 30 | enableTrace: true, 31 | ...handlers, 32 | }; 33 | } 34 | private eventListener(server: SMTPServer) { 35 | server.on("error", (err) => 36 | Logging.dev("incoming Mail Server has error " + err, "error"), 37 | ); 38 | server.on("close", () => Logging.dev("incoming Mail Server closed")); 39 | } 40 | private incomingHandlers(): SMTPServerOptions { 41 | return { 42 | async onConnect(session, callback) { 43 | Logging.dev("Client Connected " + session.id); 44 | return callback(null); 45 | }, 46 | onClose(session, callback) {}, 47 | async onMailFrom(address, session, callback) { 48 | try { 49 | Logging.dev("Mail Recived From " + address.address); 50 | 51 | await SpamFilteration.checkBlackListIp(session.remoteAddress, 3); 52 | return NewMailHandler.HandleMailFrom(address, session, callback); 53 | } catch (error) {} 54 | }, 55 | onRcptTo(address, session, callback) { 56 | Logging.dev("Mail Recived To " + address.address); 57 | 58 | return NewMailHandler.HandleRcptTo(address, session, callback); 59 | }, 60 | onAuth(auth, session, callback) { 61 | 62 | 63 | return callback(null); 64 | }, 65 | onData(stream, session, callback) { 66 | return NewMailHandler.HandleNewMail(stream, session, callback); 67 | }, 68 | }; 69 | } 70 | public start() { 71 | const options = this.getOptions(this.incomingHandlers()); 72 | const server = new SMTPServer(options); 73 | this.eventListener(server); 74 | server.listen(this.INCOMING_SERVER_PORT); 75 | Logging.dev( 76 | white( 77 | `Incoming Mail Server started Host: ${this.host} Port: ` + this.INCOMING_SERVER_PORT, 78 | ), 79 | ); 80 | return server; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/server/OutgoingServer.ts: -------------------------------------------------------------------------------- 1 | import { SMTPServer, SMTPServerOptions } from "smtp-server"; 2 | import { NewOutgoingMailHandler } from "../services/OutgoingMailHandler"; 3 | 4 | import { Logging } from "../lib/logs"; 5 | import { white } from "colorette"; 6 | import { readFileSync } from "fs"; 7 | import { SpamFilteration } from "./config/SpamFilteration"; 8 | 9 | // Make Sure to use TLS 10 | // const options = { 11 | // key: readFileSync(process.env.TLS_PRIVATE_KEY_PATH as string,"utf8"), 12 | // cert: readFileSync(process.env.TLS_CERTFICATE_PATH as string,"utf8") 13 | // } 14 | 15 | export class OutgoingServerConfig { 16 | private readonly MAX_EMAILS_PER_MINUTE = 17 | Number(process.env.MAX_EMAILS_PER_MINUTE) || 5; 18 | 19 | constructor(private host: string) {} 20 | private OUTGOING_SERVER_PORT = 587; 21 | private getOptions(handlers: SMTPServerOptions): SMTPServerOptions { 22 | return { 23 | allowInsecureAuth: false, 24 | logger: false, 25 | secure: false, 26 | name: this.host, 27 | //Enable TLS 28 | // key: options.key, 29 | // cert:options.cert, 30 | banner: `220 ${this.host} ESMTP NodeSMTP Server is Ready`, 31 | disabledCommands: [""], 32 | authMethods: ["PLAIN", "LOGIN", "CRAM-MD5"], 33 | size: 10 * 1024 * 1024, 34 | disableReverseLookup: true, 35 | useXClient: false, 36 | hidePIPELINING: true, 37 | needsUpgrade: false, 38 | authOptional: true, 39 | useProxy: false, 40 | handshakeTimeout: 60000, 41 | socketTimeout: 60000, // Increase socket timeout to 60 seconds 42 | closeTimeout: 60000, // Increase connection timeout to 60 seconds 43 | maxClients: 1000, 44 | enableTrace: true, 45 | ...handlers, 46 | }; 47 | } 48 | private eventListener(server: SMTPServer) { 49 | server.on("error", (err) => 50 | Logging.dev("incoming Mail Server has error " + err, "error"), 51 | ); 52 | server.on("close", () => Logging.dev("incoming Mail Server closed")); 53 | } 54 | private outgoingHandlers(): SMTPServerOptions { 55 | const self = this; 56 | return { 57 | async onConnect(session, callback) { 58 | try { 59 | await SpamFilteration.checkBlackListIp( 60 | session.remoteAddress, 61 | self.MAX_EMAILS_PER_MINUTE, 62 | ); 63 | return callback(null); 64 | } catch (error: any) { 65 | return callback(error.message); 66 | } 67 | }, 68 | onClose(session, callback) {}, 69 | onMailFrom(address, session, callback) { 70 | return NewOutgoingMailHandler.HandleMailFrom( 71 | address, 72 | session, 73 | callback, 74 | ); 75 | }, 76 | onRcptTo(address, session, callback) { 77 | return NewOutgoingMailHandler.HandleRcptTo(address, session, callback); 78 | }, 79 | onAuth(auth, session, callback) { 80 | return NewOutgoingMailHandler.HandleAuthenticate( 81 | auth, 82 | session, 83 | callback, 84 | ); 85 | }, 86 | onData(stream, session, callback) { 87 | return NewOutgoingMailHandler.HandleNewMail(stream, session, callback); 88 | }, 89 | }; 90 | } 91 | public start() { 92 | const options = this.getOptions(this.outgoingHandlers()); 93 | const server = new SMTPServer(options); 94 | this.eventListener(server); 95 | server.listen(this.OUTGOING_SERVER_PORT); 96 | Logging.dev( 97 | white( 98 | `Outgoing Mail Server started Host: ${this.host} Port: ` + this.OUTGOING_SERVER_PORT, 99 | ), 100 | ); 101 | return server; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/server/mta/node-mta.ts: -------------------------------------------------------------------------------- 1 | import net from "net"; 2 | import dns from "dns"; 3 | import { MailConfig } from "../config/MailConfig"; 4 | 5 | // Not Recommended 6 | export class NodeMailerMTA { 7 | private mxCache = new Map(); 8 | 9 | async DnsBasedDelivery( 10 | to: string, 11 | from: string, 12 | subject: string, 13 | body: string, 14 | ) { 15 | return new Promise((resolve, reject) => { 16 | const domain = to.split("@")[1]; 17 | 18 | // Lookup MX records for the recipient's domain 19 | dns.resolveMx(domain, (err, addresses) => { 20 | if (err || addresses.length === 0) { 21 | return reject(new Error(`Failed to resolve MX for ${domain}`)); 22 | } 23 | // Sort MX records by priority (lowest first) 24 | addresses.sort((a, b) => a.priority - b.priority); 25 | const mxHost = addresses[0].exchange; 26 | console.log(`Connecting to MX host: ${mxHost}:${25}`); 27 | const client = net.createConnection(25, mxHost, () => { 28 | console.log(`Connected to recipient's mail server ${mxHost}:${25}`); 29 | }); 30 | 31 | let response = ""; 32 | 33 | client.on("data", async (data) => { 34 | response += data.toString(); 35 | 36 | // Automatically proceed with SMTP handshake 37 | if (response.includes("220")) { 38 | client.write("EHLO test.example.com\r\n"); 39 | } else if (response.includes("250-STARTTLS")) { 40 | await sendCommand(`MAIL FROM:<${from}>`); 41 | } else if (response.includes("250 Sender")) { 42 | await sendCommand(`RCPT TO:<${to}>`); 43 | } else if (response.includes("250 OK")) { 44 | await sendCommand(`DATA`); 45 | await sendCommand( 46 | `Subject: ${subject}\r\nFrom: ${from}\r\nTo: ${to}\r\n\r\n${body}\r\n.`, 47 | ); 48 | await sendCommand(`QUIT`); 49 | client.end(); // Close connection after sending email 50 | resolve(response); 51 | } else if (response.includes("553")) { 52 | await sendCommand(`QUIT`); 53 | client.end(); // Close connection after getting relay error 54 | resolve(response); 55 | } 56 | }); 57 | function sendCommand(command: string) { 58 | return new Promise((res) => { 59 | client.write(command + "\r\n", "utf-8", () => res()); 60 | }); 61 | } 62 | client.on("error", (err) => { 63 | console.error("Connection error:", err); 64 | reject(err); 65 | }); 66 | 67 | client.on("end", () => { 68 | console.log("SMTP Connection closed"); 69 | }); 70 | }); 71 | }); 72 | } 73 | async useTransport(totalRecipients: string[]) { 74 | const groupedRecipients = 75 | MailConfig.groupRecipientsByDomain(totalRecipients); 76 | for await (const [domain, recipients] of Object.entries( 77 | groupedRecipients, 78 | )) { 79 | try { 80 | let mxServer = this.mxCache.get(domain) || null; 81 | 82 | if (!mxServer) { 83 | mxServer = await MailConfig.checkConnections(domain); 84 | if (!mxServer) { 85 | continue; 86 | } 87 | this.mxCache.set(domain, mxServer); 88 | } 89 | // send to recipients mx server 90 | for (const recipient in recipients) { 91 | const response = await MailConfig.createtransporter( 92 | mxServer.host, 93 | mxServer.port, 94 | ).sendMail({ 95 | text: recipient, 96 | // extra data like from,body, aattachment 97 | 98 | // Dkim Part is required otherwise mail will rejected, 99 | // dkim: { 100 | // domainName: domain, 101 | // keySelector: "default", 102 | // privateKey: PVT_KEY 103 | // } 104 | }) 105 | return response 106 | } 107 | 108 | console.log(`Mail Successfully Delivered`); 109 | } catch (error: any) { 110 | console.log(`Delivery Attempt Failed` + error.message); 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /imap/readme.md: -------------------------------------------------------------------------------- 1 | # IMAP SERVER GUIDE 2 | 3 | `maildir` is a standard format for storing emails in a filesystem. It organizes emails in a directory structure and ensures safe message delivery by avoiding file locking issues. Here’s how you can use `maildir` to store emails: 4 | 5 | ## 1. Maildir Directory Structure 6 | 7 | ```bash 8 | /var/mail/demo_user/ 9 | ├── cur/ # Processed emails (read) After being read, emails are moved here. 10 | ├── new/ # New emails (unread) When a new email arrives, it is placed here. 11 | ├── tmp/ # Temporary storage for emails during delivery Emails are written here first before being moved to new/ to prevent corruption. 12 | ``` 13 | 14 | `demo_user` This is a test user for testing Imap server it can be dynamic for that you can check DoveCot Config `/etc/dovecot/conf.d/10-mail.conf` 15 | 16 | ```bash 17 | # %u - username 18 | # %n - user part in user@domain, same as %u if there's no domain 19 | # %d - domain part in user@domain, empty if there's no domain 20 | # %h - home directory 21 | ``` 22 | 23 | 24 | 25 | ## 2. Accessing Mail via IMAP, Install Dovecot 26 | 27 | #### You can also install DB recommended are PGSQL/MYSQL for user auth and storing emails 28 | 29 | ```bash 30 | sudo apt install dovecot-imapd 31 | sudo systemctl status dovecot #Ensure Dovecot is Installed & Running 32 | sudo systemctl enable dovecot 33 | ``` 34 | 35 | ## 3. Creating a Maildir or use default `/var/mail 36 | 37 | ```bash 38 | sudo mkdir -p /var/mail/demo_user/{cur,new,tmp} 39 | sudo chown -R 5000:5000 /var/mail/demo_user 40 | sudo chown demo_user:mail /var/mail/demo_user 41 | sudo chmod -R 700 /var/mail/demo_user 42 | 43 | ``` 44 | 45 | # 4. Enable STARTTLS in Dovecot 46 | 47 | ```bash 48 | sudo nano /etc/dovecot/conf.d/10-ssl.conf 49 | # Find this line ssl = no Change it to: ssl = yes 50 | ssl = yes 51 | ``` 52 | 53 | # 5. Set Up SSL Certificates 54 | 55 | Use Certbot or use your SMTP Server key files/ Self Signed Certificate are not recommended 56 | 57 | ```bash 58 | ssl_cert = ) egid=5000() missing +x perm: /var/mail/demo_user, dir owner missing perms)``` 169 | If the issue persists, check logs again: 170 | ```bash 171 | sudo mkdir -p /var/mail/demo_user/{new,cur,tmp} 172 | sudo chmod -R 700 /var/mail/demo_user 173 | sudo chmod 755 /var/mail 174 | # and still facing error then use /* udo chmod -R 700 /var/mail/demo_user/* 175 | sudo journalctl -u dovecot --no-pager | tail -n 50 176 | ``` -------------------------------------------------------------------------------- /src/services/IncomingMailHandler.ts: -------------------------------------------------------------------------------- 1 | import { MiscellaneousHelper } from "../lib/helpers"; 2 | import { DNSChecker } from "../server/config/DnsChecker"; 3 | import { MailConfig } from "../server/config/MailConfig"; 4 | import { SpamFilteration } from "../server/config/SpamFilteration"; 5 | import { 6 | SMTPServerAddress, 7 | SMTPServerDataStream, 8 | SMTPServerSession, 9 | } from "smtp-server"; 10 | 11 | const regex = /^[^\s@]+\.temp@[^\s@]+\.[^\s@]+$/; 12 | class IncomingMailHandler { 13 | async HandleMailFrom( 14 | address: SMTPServerAddress, 15 | session: SMTPServerSession, 16 | callback: (err?: Error | null | undefined) => void, 17 | ): Promise { 18 | try { 19 | const mailFrom = 20 | MiscellaneousHelper.extractEmail(address.address) || address.address; 21 | 22 | if (mailFrom.match(regex)) { 23 | return callback(new Error("The email address you used is invalid")); 24 | } 25 | return callback(); // Accept the address 26 | } catch (error: any) { 27 | if (error instanceof Error) { 28 | return callback(new Error(error.message)); 29 | } 30 | return callback(null); 31 | } 32 | } 33 | async HandleRcptTo( 34 | address: SMTPServerAddress, 35 | session: SMTPServerSession, 36 | callback: (err?: Error | null | undefined) => void, 37 | ): Promise { 38 | try { 39 | let message = ""; 40 | /** NOTE : 41 | * Check the RCPT TO address is exist or not in your System 42 | * Check the DNS records like MX, SPF, DMARC, TXT of the Sender and validate them and based on them allow or reject the email 43 | * Spam Detection 44 | * Store All Logs in Database, Redis etc 45 | */ 46 | 47 | if (session.envelope && session.envelope.mailFrom) { 48 | const receiverMail = 49 | MiscellaneousHelper.extractEmail(address.address) || address.address; 50 | const receiverDomain = receiverMail.split("@")[1]; 51 | const mailFrom = 52 | MiscellaneousHelper.extractEmail(session.envelope.mailFrom.address) || 53 | session.envelope.mailFrom.address; 54 | const mailFromDomain = mailFrom.split("@")[1]; 55 | 56 | const { mxRecords, spfRecords, dmarcRecords, txtRecords } = 57 | await DNSChecker.resolveRecords(mailFromDomain); 58 | 59 | // Check for MX record 60 | if (mxRecords.length === 0) { 61 | message = `No MX records found for domain ${mailFromDomain}. Rejecting the email.`; 62 | 63 | return callback(new Error(message)); 64 | } 65 | message = `MX record check passed localPort = ${session.localPort}, remoteIp = ${session.remoteAddress}, remotePort = ${session.remotePort}, from = ${mailFrom} to ${receiverMail}`; 66 | 67 | // Check for SPF record 68 | const spfRecord = spfRecords.find((record: string[]) => 69 | record.join(" ").toUpperCase().includes("SPF"), 70 | ); 71 | if (!spfRecord) { 72 | message = `No SPF record found for domain ${mailFromDomain}.SPF record not passed, Rejecting the email.`; 73 | 74 | return callback(new Error(message)); 75 | } 76 | message = `SPF record passed ,localPort = ${session.localPort}, remoteIp = ${session.remoteAddress}, remotePort = ${session.remotePort}, from = ${mailFrom} to ${receiverMail}`; 77 | 78 | if (dmarcRecords.length === 0) { 79 | const message = `No DMARC record found for domain ${mailFromDomain}.DMARC record not passed, Forwarding to Spam Folder`; 80 | } else { 81 | message = `DMARC record passed, localPort = ${session.localPort}, remoteIp = ${session.remoteAddress}, remotePort = ${session.remotePort}, from = ${mailFrom} to ${receiverMail}, `; 82 | } 83 | 84 | // Check for TXT record 85 | 86 | if (txtRecords.length === 0) { 87 | const message = `No TXT records found for domain ${mailFromDomain}. Forwarding to Spam Folder`; 88 | } else { 89 | message = `TXT record passed, localPort = ${session.localPort}, remoteIp = ${session.remoteAddress}, remotePort = ${session.remotePort}, from = ${mailFrom} to ${receiverMail}`; 90 | } 91 | 92 | const successMessage = `Incoming mail accepted localPort = ${session.localPort}, remoteIp = ${session.remoteAddress}, remotePort = ${session.remotePort}, from = ${mailFrom} to ${receiverMail}`; 93 | 94 | // Dont Forget to check the RCPT TO domain is exist or not in your System 95 | 96 | return callback(null); 97 | } 98 | 99 | return callback(null); 100 | } catch (error) { 101 | if (error instanceof Error) { 102 | return callback(new Error(error.message)); 103 | } 104 | return callback(null); 105 | } 106 | } 107 | async HandleNewMail( 108 | stream: SMTPServerDataStream, 109 | session: SMTPServerSession, 110 | callback: (err?: Error | null | undefined) => void, 111 | ): Promise { 112 | let mailchunks = ""; 113 | stream.on("data", (chunk) => (mailchunks += chunk.toString())); 114 | stream.on("end", async () => { 115 | const parsedEmailData = await MailConfig.ParseEmail(mailchunks); 116 | 117 | /** NOTE : 118 | * CHECK FOR TXT RECORD i.e DKIM 119 | * Check for DKIM signature 120 | * Extract DKIM-Signature Header from the Email. 121 | * new SpamFilteration().verifyDkimSignature(parsedEmailData.headerLines, parsedEmailData.dkimSignature); 122 | * Map HeaderLines according to requirement of function 123 | * Verify the DKIM signature and reject the email if not valid, forward to spam folder 124 | */ 125 | 126 | // Do something with the parsed email data (e.g., save to database, etc.) 127 | stream.pipe(process.stdout); 128 | return callback(null); 129 | }); 130 | } 131 | } 132 | export const NewMailHandler = new IncomingMailHandler(); 133 | -------------------------------------------------------------------------------- /src/lib/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export class MiscellaneousHelper { 2 | public static filterId(inputId: string): string { 3 | let filteredId = inputId.split("@")[0]; 4 | 5 | filteredId = filteredId.replace(/[^a-zA-Z0-9-]/g, ""); 6 | 7 | return filteredId; 8 | } 9 | public static async delay(ms: number) { 10 | return new Promise((resolve) => setTimeout(resolve, ms)); 11 | } 12 | static getStringSizeInBytes(str: string): number { 13 | return Buffer.byteLength(str, "utf8"); 14 | } 15 | static extractEmail(input: string): string | null { 16 | const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/; 17 | const match = input.match(emailRegex); 18 | return match ? match[0] : null; 19 | } 20 | 21 | static extractNameAndEmail(input: string): { 22 | name: string | null; 23 | email: string; 24 | } { 25 | const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/; 26 | const nameRegex = /"([^"]+)"/; // Matches the name inside double quotes 27 | 28 | const emailMatch = input.match(emailRegex); 29 | const nameMatch = input.match(nameRegex); 30 | 31 | const email = emailMatch ? emailMatch[0] : input; 32 | const name = nameMatch ? nameMatch[1] : null; 33 | 34 | return { name, email }; 35 | } 36 | public static extractDomainFromEmail(email: string): string { 37 | const emailParts = email.split("@"); 38 | return emailParts[1]; 39 | } 40 | public static getDomainUrl(request: Request) { 41 | const host = 42 | request.headers.get("X-Forwarded-Host") ?? request.headers.get("Host"); 43 | if (!host) return null; 44 | 45 | const protocol = host.includes("localhost") ? "http" : "https"; 46 | return `${protocol}://${host}`; 47 | } 48 | public static getDomainPathname(request: Request) { 49 | const pathname = new URL(request.url).pathname; 50 | if (!pathname) return null; 51 | return pathname; 52 | } 53 | public static combineHeaders( 54 | ...headers: Array 55 | ) { 56 | const combined = new Headers(); 57 | for (const header of headers) { 58 | if (!header) continue; 59 | for (const [key, value] of new Headers(header).entries()) { 60 | combined.append(key, value); 61 | } 62 | } 63 | return combined; 64 | } 65 | static singleton(name: string, value: () => Value): Value { 66 | const globalStore = global as any; 67 | 68 | globalStore.__singletons ??= {}; 69 | globalStore.__singletons[name] ??= value(); 70 | 71 | return globalStore.__singletons[name]; 72 | } 73 | static getImageBase64() { 74 | return `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAsTAAALEwEAmpwYAAAJo0lEQVR4nO1YW1Nb5xXloZPJtB47xk7iawwYMEgICUmAuAl0QQiQETdZmJvBRjh2TFzsxpnMtL+hnTbT/oE8JP0FeTAgjCGQEGxz011C4tK3XMiEPMTj1VnfOQLRaTMDwW2mw55Zc76z99pr7X10uKalHcZhHMZh/F+EY6bSUvNZ4Sflk3mRkomcTb0367nOm/mC0I9nPWeONdOU+uOGJ+WmtF9CVMeqXzVPq/9aMpH9delENkzTatR/WQrnUyPa5s1oX7AI8Mwca+SQW/oo+2vLTNGHrkXFK/+T4S0zRb/XPcr8oWpKgYa5MrgWLOj2NaI/2IyBUBtuRq7gZsQt44rIsUYOueypmlJAN561ZZnWfPBfG9wxqztpmMjxGSZy0TBngHvJhr5AEwbDLtyOXsXQSg/uJq5hONGHe4l+AZ6ZY40cctnjXrIJDWoZHucu2aYM6S91ePu0RqEbz9ysnlahZb4GPQEHBqMuvBPrxN1EL4ZX+3F//QZ+t3YD76178N6GjHWPyLFGDrnsGYy6hAa1qKnzZm7aZnV5L2V487RGUTSesWX+XAPXkhV9QSduxdwYinfjt6t9uL92He+tDeDBhgcP1j14sDGI9zc8AjxLOY/gkMueoXi30KAWNalNjwNfgq+N1puxKYZftOJ6qBlvxzowlOjBsDy8ePIbA+Jpi4HF0x8UEEvxzE9iY0Bw2TPMJRI9Qoua1KaHdixj0/lE/dqBLVA8cdFXNV2AtkUz+kJO3Fxx406iG3fXrmF4vR/3Nvpxf+OGBLHIDdz/xwDubdwQ4FnkOLjMY8/wer/QoBY1qU0PepU8yl48kOGrphUflExcRNOzSnQHGuHhOx/vxLurPbi71ovhdS7Rt4ONPtwT5/6UfL+U20jhCVwTGtSiJrXpQS96Vn2W94efNXx17MKrmvHzP9TO6uD22XA90oK34268s9qJodVuvLvWg7vrvXh3rXfnutazfU3Frtqunh6hRU1q04Ne9NR439pS/JyfE2WTOX8rm8xB84IRvSEHPLE23Fpx450EF+jE0FqXwJ3V5H0n7qx2yVfpLGF3jdw7Kf1Dq51Ck9r0oBc96V02mfvnfS+gGT//jXVWiys+K/rCTnhW2vF24gpur7pxa7UDt3fBjdtrbtxKdOCWqPMsY/u+Q3AEN6X3ltxPbXrQi570Lhp/66t9DV85mW/Rjl9A47MyXA3Y0R9thifejsG4CzdXr+wgQbgwuOrCIK9xGYl/QWp+1SV6brI3RYt1etCLnvTWjmegbFJl3PMCuseZn5RP5aF50YiuYD2uc4GVNngSbfDEU5BoFRiIt2JgRb7+FGROss+zS6tNeNCLnvTmDPqJrI/2vIBm/Fyk5gsV2pZr0BVuQN+KEzfiLdu4nmjG9Xgz+okVwon+eApWnKIuOP+mJnriUp1aqdr0oie9OYNm/Fx4zwsUes99Z/lSi3afCd2RBvRFL6NvpUkeogn9K7y/jL74ZVxbceBazCFdVxxSXoDnnftkPcnti0t5agnNuFN40Iue9OYMau+5b/e8gGrszHPbnA7tfjO6wnb0Rh3olQfgtSfWiJ5YA7oF6ncQrZfzKbmYnIvuznXHGkSeWqna9KInvTmDynv2x70vMHrmhe2JDq0+E66G6tAlm3fF6tEZtaMzWichVoerERuuRiVI+Z37nbxU285FbKJ3WydqF9rCI1ovPOnNGVRjZ17sa4HaJ1q0+KrREZIHi9lxlYja0BGtlRCrhTtihTsqQcrv3O/kpdp2LmIVvUkdsVTMLjzoRU96c4Z9LVAwdvq5Za4Izb4quEIWdEQ4tA1uDhGzwhWxwBUxo13AtIOwSc6n5CJyLrw71x4xizy1qEltetCLnvTmDMrR03t/hZSjp7+rnlXh8lI52oImuEJmuMLJoU1oi9SgNVyD1kg1WsJGtISMaAnzXI3WbRhlSPfJusQ1il5qUCu5pPAImYUnvTlDgff03r+IFWNvRipm8tCwUIpmvxFtIRNcNCPkAVpCVWgOVQo4gxVw8ppEsDKllpKXuclaS6hKaFGT2sIjZBKe9OYMirFTe/82WuB94+PiySzYnunQ5KtAS9CIVg4eqUJzuBJN4QoZZQKOkAGOoAGOUNlPQ3AM231Nsg41qS08gkbhSW/OoPSe2vsPMvX0WVOh9yxMc4VoXDLAGZCemjNUAWe4HJdDZbgshi6FI1iCxlAJGoLFaAjICJbsxna+WHDZ4wiWCg1qUZPawiNQITzpzRkKJ85Xpu0n8kZf/7p85hLqFnRwLJehKVCBpqA0fGPQgIZgKeqDxagPEHrUB/WwB/SoC+gk+GXI93aZI7jsCXKhUqFFTWrTg170pHf+6Ov7+2WOoXz0xodFE+dheloI+3IJGv0GOAIGefgS1IeKYQ/pURfUwRbQwhYsQu32tQi1vMrn1Bq57LGH9EKDWtQU2n6D8KInvfO9b/5x3wvwj4lLoye3ymZyYZ3XoN7H16AUDYES8QTF0w5qYeOwAQ2sAmpY/f8BrAU0gsueuqBW+lTE68XXrFR40Iuel0ZOfv+z/qBh5HlPvF/oPYOq2XxY54tQt6yH3VcMu18Pm18a3urXwBJQw+wvlKGCSaBAhkrkknVy2WPjJ+PXCi1qUpse9KLnpYkT99MOInJGjy8VPb4A45wS1gUNbMta1Pl0wrzWzwXUsHAwnwpmX4EAB6/xKQV4TubJIZc9tX5pAaG1rBXa9KBXzlj6fNpBhWLqaHrOw/RN3VQGjHMKWBYKYV0qgtWnQa1PGl56wsknXoAav3IXknlyyGVPrU8tNKhFTWrTI3ck/Rv1k2MH928VRsHsibyckfQt7dQFVM7lo+aZCpYlNSzLaph9RCHMyyqYfHzySUifQPKeNXIEl4svq4UGtahJ7ZzR9K28mZO5aS8jcie5xPFvNY/Po+yLXBifKlCzoIJpUQXTkgwusVQA03IBapaVAjxLuRTeokr0UoNa1MwZOfYNPdJeZig+PZqeNXp0Uek9Bf10JsrnLqHqaT6M80pULyhRvViA6iVCiRoZPIscawtKwWVP+dwloUGtrNGj8wf+2vxUZHuPPLg48tqW6tFp6KczYPgiGxVzuah8yoXyYHyWjyoZ4vw0T9TIIZc9qkencfHha99nen9zMN9t9hqKv6e9kjF65C8XR459lT/2BgonzkL72VvQz2Si5PMslH5+UYBn5lgjh1z2ZI4d+VPaWNqv0n4JcW78iPHCyK8/ynh4JJw1cnQzZ+T489yHJ14Q2SPHf8waOfota+ScHzuyv99tDuMwDuMw0n5p8U+n9eV9/DHUWwAAAABJRU5ErkJggg==`; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/server/config/DnsChecker.ts: -------------------------------------------------------------------------------- 1 | import { DNSRecordType, DNSResponse } from "../../interfaces/dns.interface"; 2 | import dns from "dns/promises"; 3 | import moment from "moment"; 4 | import net from "net"; 5 | 6 | export class DNSChecker { 7 | private domain: string; 8 | private dkimSelector: string; 9 | 10 | constructor( 11 | domain: string, 12 | dkimSelector: string = moment().format("YYYYMM"), 13 | ) { 14 | this.domain = domain; 15 | this.dkimSelector = dkimSelector; 16 | } 17 | static async getMXRecords(domain: string) { 18 | try { 19 | const records = await dns.resolveMx(domain); 20 | records.sort((a, b) => a.priority - b.priority); // Sort by priority 21 | return records[0].exchange; // Return primary mail server 22 | } catch (error) { 23 | console.error(`❌ Failed to fetch MX records for ${domain}:`, error); 24 | return null; 25 | } 26 | } 27 | 28 | /** 29 | * Resolves the DNS records for a given hostname using DNS over HTTPS. 30 | * @param hostname - The hostname to resolve. 31 | * @returns A promise that resolves to an array of IP addresses as strings. 32 | */ 33 | static async resolveRecords(domain: string): Promise { 34 | try { 35 | const mxRecords = await dns.resolveMx(domain); 36 | const spfRecords = await dns.resolveTxt(domain); 37 | const dmarcRecords = await dns.resolveTxt("_dmarc." + domain); 38 | const txtRecords = await dns.resolveTxt(domain); 39 | const nsRecords = await dns.resolveNs(domain); 40 | // const srvRecords = await dns.resolveSrv(domain); 41 | const validDomain = /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(domain); 42 | return { 43 | mxRecords, 44 | spfRecords, 45 | dmarcRecords, 46 | txtRecords, 47 | validDomain, 48 | nsRecords, 49 | }; 50 | } catch (error) { 51 | console.log(error); 52 | } 53 | } 54 | static resolveDNS( 55 | hostname: string, 56 | type: DNSRecordType = "A", 57 | ): Promise { 58 | return new Promise((resolve, reject) => { 59 | fetch(`https://dns.google/resolve?name=${hostname}&type=${type}`, { 60 | method: "GET", 61 | headers: { 62 | Accept: 63 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", 64 | "User-Agentr": 65 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", 66 | }, 67 | }) 68 | .then((res) => res.json()) 69 | .then((result: any) => resolve(result)) 70 | .catch((error) => reject(error)); 71 | }); 72 | } 73 | static checkRecordsExistOrNot( 74 | hostname: string, 75 | type: DNSRecordType = "A", 76 | ): Promise { 77 | return this.resolveDNS(hostname, type).then( 78 | (result) => result.Answer.length > 0, 79 | ); 80 | } 81 | static filterMXRecord(mxRecord: string): string { 82 | return mxRecord.replace(/^\d+\s*/, "").trim(); 83 | } 84 | static tryConnect(host: string, port: number): Promise { 85 | return new Promise((resolve, reject) => { 86 | const socket = net.createConnection(port, host, () => { 87 | socket.destroy(); // Close the connection 88 | resolve(true); 89 | }); 90 | socket.on("error", (err) => { 91 | socket.destroy(); 92 | reject(false); 93 | }); 94 | 95 | socket.setTimeout(5000, () => { 96 | socket.destroy(); 97 | reject(false); 98 | }); 99 | }); 100 | } 101 | /** 102 | * Check SPF record 103 | */ 104 | async checkSPF(expectedSPF: string): Promise { 105 | try { 106 | const records = await dns.resolveTxt(this.domain); 107 | const spfRecord = records.flat().find((txt) => txt.startsWith("v=spf1")); 108 | return spfRecord === expectedSPF; 109 | } catch (error) { 110 | console.error("SPF check failed:", error); 111 | return false; 112 | } 113 | } 114 | 115 | /** 116 | * Check DKIM record 117 | */ 118 | async checkDKIM(expectedDKIM: string): Promise { 119 | try { 120 | const dkimRecordName = `${this.dkimSelector}._domainkey.${this.domain}`; 121 | const records = await dns.resolveTxt(dkimRecordName); 122 | const dkimRecord = records.flat().join(""); 123 | return dkimRecord.includes(expectedDKIM); 124 | } catch (error) { 125 | console.error("DKIM check failed:", error); 126 | return false; 127 | } 128 | } 129 | 130 | /** 131 | * Check DMARC record 132 | */ 133 | async checkDMARC(expectedDMARC: string): Promise { 134 | try { 135 | const dmarcRecordName = `_dmarc.${this.domain}`; 136 | const records = await dns.resolveTxt(dmarcRecordName); 137 | const dmarcRecord = records.flat().join(""); 138 | return dmarcRecord === expectedDMARC; 139 | } catch (error) { 140 | console.error("DMARC check failed:", error); 141 | return false; 142 | } 143 | } 144 | 145 | /** 146 | * Check MX records 147 | */ 148 | async checkMX(expectedMX: string): Promise { 149 | try { 150 | const mxRecords = await dns.resolveMx(this.domain); 151 | return mxRecords.some((record) => record.exchange === expectedMX); 152 | } catch (error) { 153 | console.error("MX check failed:", error); 154 | return false; 155 | } 156 | } 157 | 158 | /** 159 | * Check custom TXT record 160 | */ 161 | async checkTXT(name: string, expectedValue: string): Promise { 162 | try { 163 | const records = await dns.resolveTxt(`${name}.${this.domain}`); 164 | const txtRecord = records.flat().join(""); 165 | return txtRecord.includes(expectedValue); 166 | } catch (error) { 167 | console.error("TXT check failed:", error); 168 | return false; 169 | } 170 | } 171 | 172 | /** 173 | * Run all checks and return results 174 | */ 175 | async checkAllRecords( 176 | expectedRecords: Record, 177 | ): Promise> { 178 | const results: Record = {}; 179 | 180 | results["SPF"] = await this.checkSPF(expectedRecords["SPF"]); 181 | results["DKIM"] = await this.checkDKIM(expectedRecords["DKIM"]); 182 | results["DMARC"] = await this.checkDMARC(expectedRecords["DMARC"]); 183 | results["MX"] = await this.checkMX(expectedRecords["MX"]); 184 | 185 | return results; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This Project is Under Develepment 2 | 3 | [Setup Video](https://media.cm/u/H5U7) 4 | 5 | This project started as a fun experiment but has grown into something more. Right now, it's a simple mail server for sending and receiving emails. Future updates will add: 6 | 7 | - **Mail Exchanger (IMAP)** – Use Gmail, Outlook, Yahoo, or Hotmail to send and receive emails. 8 | - **Extra Features** – SMTP relay, IP/domain whitelisting & blacklisting. 9 | - **AI Email Optimization** – Smart suggestions to improve email performance. 10 | - **Security & Anti-Spam** – AI-driven spam filtering and fraud detection. 11 | - **User Controls** – Rate limits, storage alerts, email forwarding, and aliases. 12 | - **Calendar Integration** – Works with Google Meet, Teams, and Cal.com. 13 | - **Developer API** – Easy-to-use API for automating emails in your apps. 14 | # SMTP Server Setup Guide 15 | 16 | ### Prerequisites 17 | 18 | - A registered domain name 19 | 20 | - A server with a static IP address 21 | 22 | - SSL/TLS certificate for secure email transmission 23 | 24 | ### Basic Server Setup 25 | 26 | - Configure your server with a static IP address 27 | 28 | - Ensure your firewall allows SMTP traffic (ports 25, 465, and 587) 29 | 30 | - Set up reverse DNS (PTR record) for your mail server IP (- Most Important) 31 | 32 | #### Clone the Repository 33 | 34 | Use Any Package Manager, I'm just using bun, to install check out `bun.sh` 35 | 36 | 1. Install Dependencies First (Recommended) `bun install` 37 | 38 | 2. Build the Applications `bun run build`, dont use `bun build` 39 | 40 | 3. Start with command `bun run dev` (Development) and `bun run 41 | start` with root permissions (Incase not able to start then use 42 | `sudo node ./build/main.js`) 43 | 44 | ## NOTE 45 | 46 | You will find two server file config (Incoming and Outgoing) and 2 server listening at 25 and 587. 47 | We can do it one, but we have to manage incoming and outgoing in same file, which is a mess, complicates the code. Moreover you have full controll to run multiple diffrente mail server like `mx.domain.com`, `mx2.domain.com` etc and for sending `{smtp,mail}.domain.com` (which ever is suitable). 48 | 49 | # DNS Records Configuration 50 | Please Setup PTR(Reverse Lookup) and `MAIL_HOST`, `MAIL_SERVER_IP` first 51 | ### How to genrate DNS Records 52 | 1. 53 | - ```bash chmod +x run.sh``` 54 | - ```bash ./run.sh``` 55 | 2 . 56 | - Go to src -> start.ts 57 | 58 | - Open and replace `your_domain` with your domain name 59 | 60 | ```ts 61 | const records = new DNSRecordGenerator("cirrusmail.cloud").generateAllRecords(); 62 | ``` 63 | 64 | and Run this File, Records will create in `records.json` File in root directory. 65 | 66 | ### Essential DNS Records 67 | 68 | ![DNS Record Setup](image-1.png) 69 | 70 | 1. A Record 71 | 72 | - Create mail.yourdomain.com pointing to your server IP 73 | - Ensure the hostname matches SSL certificate 74 | 75 | 2 .PTR Record 76 | 77 | - Contact your hosting provider to set up reverse DNS 78 | - Should match your mail server's hostname 79 | 80 | Share your Mailserver hostname, who ever wants to use your server and tell them to create MX Record pointing your server. 81 | 82 | - Point to your mail server hostname 83 | - Set priority (e.g., 10 mail.yourdomain.com) 84 | ![MX Record Setup](image.png) 85 | 86 | # Anti-Spam DNS Records 87 | 88 | ![Anti-Spam DNS Records Setup](image-2.png) 89 | 90 | 1. SPF Record (TXT) 91 | 92 | - Add record: v=spf1 ip4:YOUR_IP mx -all 93 | - Prevents email spoofing 94 | - Specifies authorized IPs/servers 95 | 96 | 97 | 2. DKIM Record 98 | 99 | - Generate DKIM keys 100 | - Add public key to DNS as TXT record 101 | - Format: default._domainkey.yourdomain.com 102 | 103 | 104 | 3. DMARC Record 105 | 106 | - Add TXT record: _dmarc.yourdomain.com 107 | - Define policy for failed authentications 108 | - Set up reporting (optional) 109 | 110 | 111 | ### Note: Required Ports => 25, 587 112 | 113 | #### Port 25 is used for receiving mails and outbound traffice to send mail from your server to another mail server 114 | 115 | #### Port 587 is used for to connection to your mail server and create transport which send mail and process the mail , how it is going to deliver via Direct or Relay 116 | 117 | ### Generate SSL Ceritificate for using STARTTLS to prevent spam. Dont use Self Signed Certificate 118 | 119 | Mail Server SSL Certificate Setup 120 | 121 | Simple guide to secure your mail server with Let's Encrypt/ZeroSSL certificates using Certbot. This setup enables encrypted SMTP connections and works with any transport method (relay or direct delivery). 122 | 123 | ### Prerequisites 124 | 125 | Linux server with root access 126 | 127 | Domain with DNS A record pointing to your server 128 | 129 | Port 80 temporarily available for verification 130 | 131 | ### Installation Steps 132 | 133 | Install Certbot: 134 | 135 | ```bash 136 | 137 | sudo apt update 138 | 139 | sudo apt install certbot 140 | 141 | ``` 142 | 143 | Generate certificate: 144 | 145 | ```bash 146 | 147 | sudo certbot certonly --standalone --preferred-challenges http -d mail.domain.com 148 | 149 | ``` 150 | 151 | Certificate locations: 152 | 153 | Certificate: /etc/letsencrypt/live/mail.domain.com/fullchain.pem 154 | 155 | Private key: /etc/letsencrypt/live/mail.domain.com/privkey.pem 156 | 157 | 158 | 159 | ``` 160 | # Dont Touch it 161 | INCOMING_MAIL_HOST= 162 | 163 | # Dont Touch it 164 | 165 | OUTGOING_MAIL_HOST= 166 | 167 | # Dont Touch it 168 | MAIL_SERVER_IP= 169 | 170 | MAX_EMAILS_PER_MINUTE=5 171 | 172 | TLS_PRIVATE_KEY_PATH="/etc/letsencrypt/live//privkey.pem" 173 | 174 | TLS_CERTFICATE_PATH="/etc/letsencrypt/live//fullchain.pem" 175 | 176 | ``` 177 | ### Testing 178 | 179 | - Run mail server tests: 180 | - Verify all DNS records 181 | - Test SMTP authentication 182 | - Check TLS encryption 183 | - Verify reverse DNS 184 | - Test sending/receiving 185 | ### Use external tools: 186 | Go and Test Your Mail Server here , it should be like in given Image https://mxtoolbox.com/diagnostic.aspx 187 | ![See image](image-3.png) 188 | 189 | 190 | # iCalendar event for Nodemailer - Resources 191 | ### Step 1: Enable Google Calendar API 192 | - Go to the Google Cloud Console. 193 | - Create a new project or select an existing one. 194 | - Enable the Google Calendar API for the project. 195 | - Create OAuth 2.0 credentials or a service account for authentication. 196 | - Download the credentials JSON file. 197 | 198 | ```bash 199 | npm install googleapis nodemailer ics 200 | ``` 201 | ## Integration 202 | - [x] Google Meet/Calender 203 | - [ ] Cal.com 204 | - [ ] Zoho Calender 205 | - [ ] Zoom Meetings 206 | 207 | 208 | ### Calender UI 209 | - https://shadcn-cal-com.vercel.app/?date=2025-03-02 210 | - https://github.com/Mina-Massoud/next-ui-full-calendar 211 | - https://github.com/schedule-x/schedule-x 212 | - https://synergy-platform.vercel.app/calendar 213 | - https://github.com/charlietlamb/calendar 214 | - https://github.com/list-jonas/shadcn-ui-big-calendar 215 | 216 | -------------------------------------------------------------------------------- /src/services/OutgoingMailHandler.ts: -------------------------------------------------------------------------------- 1 | import dns from "dns/promises"; 2 | import { DKIMSign } from "dkim-signer"; 3 | import { MiscellaneousHelper } from "../lib/helpers"; 4 | import { Logging } from "../lib/logs"; 5 | import { DNSChecker } from "../server/config/DnsChecker"; 6 | import { MailConfig } from "../server/config/MailConfig"; 7 | import { SpamFilteration } from "../server/config/SpamFilteration"; 8 | import { 9 | SMTPServerAddress, 10 | SMTPServerAuthentication, 11 | SMTPServerAuthenticationResponse, 12 | SMTPServerDataStream, 13 | SMTPServerSession, 14 | } from "smtp-server"; 15 | import { NodeMailerMTA } from "@/server/mta/node-mta"; 16 | const spam = new SpamFilteration(); 17 | 18 | class OutgoingMailHandler { 19 | private readonly AVIALBLE_PORTS = [25, 465, 587]; 20 | async HandleAuthenticate( 21 | auth: SMTPServerAuthentication, 22 | session: SMTPServerSession, 23 | callback: ( 24 | err: Error | null | undefined, 25 | response?: SMTPServerAuthenticationResponse | undefined, 26 | ) => void, 27 | ): Promise { 28 | try { 29 | if (auth.method === "XOAUTH2") { 30 | return callback( 31 | new Error( 32 | "XOAUTH2 method is not allowed,Expecting LOGIN authentication", 33 | ), 34 | ); 35 | } 36 | 37 | // Note: You should check the username and password here before accepting the connection 38 | // This can be using Database and File system or any other method 39 | if ( 40 | auth.username === "your_username" && 41 | auth.password === "your_password" 42 | ) { 43 | Logging.dev("Client Authenticated " + auth.username); 44 | callback(null, { user: auth.username }); 45 | } else { 46 | callback(new Error("Invalid username or password")); 47 | } 48 | } catch (error) { 49 | return callback(new Error("Invalid username or password")); 50 | } 51 | } 52 | HandleConnection( 53 | session: SMTPServerSession, 54 | callback: (err?: Error | null | undefined) => void, 55 | ): void { 56 | // Must be disbabled in Production, can be allowed in development for testing Purpose 57 | if (session.remoteAddress === "127.0.0.1") { 58 | callback(new Error("No connections from localhost allowed")); 59 | return; 60 | } 61 | 62 | Logging.dev("Client Connected " + session.id); 63 | 64 | return callback(null); // Accept the connection 65 | } 66 | HandleConnectionClose( 67 | session: SMTPServerSession, 68 | callback: (err?: Error | null | undefined) => void, 69 | ): void { 70 | // Must be disbabled in Production, can be allowed in development for testing Purpose 71 | if (session.remoteAddress === "127.0.0.1") { 72 | callback(new Error("No connections from localhost allowed")); 73 | return; 74 | } 75 | Logging.dev("Client Disonnected " + session.id); 76 | } 77 | 78 | async HandleMailFrom( 79 | address: SMTPServerAddress, 80 | session: SMTPServerSession, 81 | callback: (err?: Error | null | undefined) => void, 82 | ): Promise { 83 | try { 84 | let message = ""; 85 | Logging.dev("Sending Mail From " + address.address); 86 | 87 | const mailFrom = 88 | MiscellaneousHelper.extractEmail(address.address) || address.address; 89 | const mailFromDomain = mailFrom.split("@")[1]; 90 | // Check For Relay, Dont Allow Relay 91 | 92 | return callback(); // Accept the address 93 | } catch (error: any) { 94 | if (error instanceof Error) { 95 | return callback(new Error(error.message)); 96 | } 97 | return callback(null); 98 | } 99 | } 100 | async HandleRcptTo( 101 | address: SMTPServerAddress, 102 | session: SMTPServerSession, 103 | callback: (err?: Error | null | undefined) => void, 104 | ): Promise { 105 | try { 106 | let message = ""; 107 | Logging.dev("Sending Mail To " + address.address); 108 | 109 | if (session.envelope && session.envelope.mailFrom) { 110 | const recipientMail = 111 | MiscellaneousHelper.extractEmail(address.address) || address.address; 112 | 113 | const mailFrom = 114 | MiscellaneousHelper.extractEmail(session.envelope.mailFrom.address) || 115 | session.envelope.mailFrom.address; 116 | const mailFromDomain = mailFrom.split("@")[1]; 117 | 118 | message = `Outgoing mail localPort = ${session.localPort}, remoteIp = ${session.remoteAddress}, remotePort = ${session.remotePort}, from = ${mailFrom} to ${recipientMail}`; 119 | // use MAX_EMAILS_PER_MINUTE , prevent Spam Protection ,using of bulk mails can down your server and IP Reputation 120 | return callback(); 121 | } 122 | 123 | return callback(); 124 | } catch (error) { 125 | if (error instanceof Error) { 126 | return callback(new Error(error.message)); 127 | } 128 | return callback(null); 129 | } 130 | } 131 | async HandleNewMail( 132 | stream: SMTPServerDataStream, 133 | session: SMTPServerSession, 134 | callback: (err?: Error | null | undefined) => void, 135 | ): Promise { 136 | let mailchunks = ""; 137 | stream.on("data", (chunk) => (mailchunks += chunk.toString())); 138 | stream.on("end", async () => { 139 | // Handle Incoming mail 140 | const parsedEmailData = await MailConfig.ParseEmail(mailchunks); 141 | 142 | const MAIL_FROM = (session.envelope.mailFrom && 143 | session.envelope.mailFrom.address) as string; 144 | const RCPT_TO = session.envelope.rcptTo.map((v) => v.address); 145 | 146 | const successMessage = `Outgoing mail Added To Queue localPort = ${session.localPort}, remoteIp = ${session.remoteAddress}, remotePort = ${session.remotePort}, from = ${MAIL_FROM} to ${session.envelope.rcptTo.map((v) => v.address).join(",")}`; 147 | 148 | // Send to Queue which processes the mail or You can Use Relay 149 | // Use processOutgoingWithQueueMailDirectDelivery or processOutgoingMailWithTransporterDirectDelivery function or use your own 150 | // use parsedEmailData as emailData and your required details to send the mail 151 | 152 | // EXAMPLE, filter all reciepeint and send the mail, remove duplicates 153 | 154 | // let totalRecipients = [...data.to, ...(data.cc || []), ...(data.bcc || [])]; 155 | // totalRecipients = Array.from(new Set(totalRecipients)); 156 | 157 | // const sentInfo = await new NodeMailerMTA().useTransport(totalRecipients) 158 | // return callback(null,sentInfo?.response); // Send the response back to the client (recommended) 159 | 160 | return callback(null); // if you don't want to send any response 161 | }); 162 | } 163 | 164 | private async checkConnections( 165 | hosts: string[], 166 | ): Promise<{ host: string; port: number } | null> { 167 | // You Can implement your own logic here for better Approach, It's just a simple example and It will work in most cases 168 | // I'm Noob 🙂 👉👈 169 | for (const host of hosts) { 170 | for (const port of this.AVIALBLE_PORTS) { 171 | try { 172 | const isConnected = await DNSChecker.tryConnect(host, port); 173 | if (isConnected) { 174 | Logging.dev(`✅ Connected to ${host}:${port}`); 175 | return { host, port }; 176 | } 177 | } catch (error) { 178 | Logging.dev(`❌ Failed to connect to ${host}:${port}`, "error"); 179 | } 180 | } 181 | } 182 | return null; // No connection was successful 183 | } 184 | 185 | } 186 | export const NewOutgoingMailHandler = new OutgoingMailHandler(); 187 | -------------------------------------------------------------------------------- /src/server/config/DnsRecordGenrator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DNSRecordType, 3 | RecordTypeRespose, 4 | } from "../../interfaces/dns.interface"; 5 | import crypto from "crypto"; 6 | import dns from "dns/promises"; 7 | 8 | export class DNSRecordGenerator { 9 | private domain: string; 10 | private dkimSelector: string; 11 | private mailServer: string; 12 | private dnsRecordsValue: { [key: number]: string } = { 13 | 1: "A", // Address record 14 | 2: "NS", // Name server 15 | 5: "CNAME", // Canonical name 16 | 6: "SOA", // Start of authority 17 | 12: "PTR", // Pointer record 18 | 15: "MX", // Mail exchange 19 | 16: "TXT", // Text record 20 | 28: "AAAA", // IPv6 address record 21 | 33: "SRV", // Service locator 22 | 41: "OPT", // Option (EDNS0) 23 | 43: "DS", // Delegation signer 24 | 46: "RRSIG", // DNSSEC signature 25 | 47: "NSEC", // Next secure record 26 | 48: "DNSKEY", // DNSSEC key 27 | 257: "CAA", // Certification Authority Authorization 28 | 52: "TLSA", 29 | }; 30 | 31 | constructor(domain: string, dkimSelector: string = "default") { 32 | this.domain = domain; 33 | this.mailServer = process.env.INCOMING_MAIL_HOST; 34 | this.dkimSelector = dkimSelector; 35 | } 36 | private formatDkimPublicKey(publicKey: string): string { 37 | return publicKey 38 | .replace(/-----BEGIN PUBLIC KEY-----/g, "") // Remove header 39 | .replace(/-----END PUBLIC KEY-----/g, "") // Remove footer 40 | .replace(/\s+/g, "") // Remove spaces and newlines 41 | .trim(); // Trim any extra spaces 42 | } 43 | private getKeyFromValue(value: string) { 44 | let num: number; 45 | for (const key in this.dnsRecordsValue) { 46 | if (this.dnsRecordsValue[key as unknown as number] === value) { 47 | num = Number(key); 48 | return num; 49 | } 50 | } 51 | } 52 | private formatRecords( 53 | type: DNSRecordType, 54 | name: string, 55 | data: string, 56 | ): RecordTypeRespose { 57 | return { 58 | type: this.getKeyFromValue(type) as number, 59 | name, 60 | ttl: 300, 61 | data, 62 | }; 63 | } 64 | /** 65 | * Generates an ARC-Seal header. 66 | * @param domain - The signing domain. 67 | * @param selector - The ARC selector. 68 | * @param privateKey - The private key for signing. 69 | * @param headerData - The data to sign. 70 | * @returns The ARC-Seal header. 71 | */ 72 | generateARCSeal(privateKey: string, headerData: string): string { 73 | const sign = crypto.createSign("RSA-SHA256"); 74 | sign.update(headerData); 75 | const signature = sign.sign(privateKey, "base64"); 76 | 77 | return `ARC-Seal: a=rsa-sha256; d=${this.domain}; s=${this.dkimSelector}; b=${signature}`; 78 | } 79 | generateARCHeaders(headers: Record): Record { 80 | const timestamp = Math.floor(Date.now() / 1000); 81 | const arcSeal = `ARC-Seal: i=1; a=rsa-sha256; t=${timestamp}; cv=none; 82 | d=${this.domain}; s=arc-20240605;`; 83 | 84 | const messageHash = crypto 85 | .createHash("sha256") 86 | .update(arcSeal) 87 | .digest("base64"); 88 | const arcMessageSignature = `ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=${this.domain}; s=arc-20240605; 89 | bh=${messageHash};`; 90 | 91 | return { arcSeal, arcMessageSignature }; 92 | } 93 | private isIPAddress(value: string): boolean { 94 | return /^(\d{1,3}\.){3}\d{1,3}$/.test(value); 95 | } 96 | /** 97 | * Generates SPF record allowing only the mail server to send emails. 98 | */ 99 | async generateSPF() { 100 | let spfRecord = "v=spf1"; 101 | if (this.isIPAddress(process.env.MAIL_SERVER_IP as string)) { 102 | spfRecord += ` a mx ip4:${process.env.MAIL_SERVER_IP}`; 103 | }else{ 104 | try { 105 | await dns.lookup(this.mailServer).then(({ address }) => { 106 | spfRecord += ` a mx ip4:${address}`; 107 | }); 108 | } catch (error) { 109 | spfRecord += ` include:${this.mailServer}`; 110 | } 111 | } 112 | 113 | 114 | spfRecord += " ~all"; 115 | return this.formatRecords("TXT", this.domain, spfRecord); 116 | } 117 | 118 | /** 119 | * Generates DKIM public/private key pair and returns the DKIM DNS record. 120 | */ 121 | generateDKIM(): { publicKey: string; privateKey: string; dkimRecord: any } { 122 | const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", { 123 | modulusLength: 2048, 124 | publicKeyEncoding: { type: "spki", format: "pem" }, 125 | privateKeyEncoding: { type: "pkcs8", format: "pem" }, 126 | }); 127 | 128 | const publicKeyFiltered = this.formatDkimPublicKey(publicKey); 129 | const string = `v=DKIM1; k=rsa; p=${publicKeyFiltered}`; 130 | const dkimRecord = this.formatRecords( 131 | "TXT", 132 | `${this.dkimSelector}._domainkey.${this.domain}`, 133 | string, 134 | ); 135 | return { publicKey: publicKeyFiltered, privateKey, dkimRecord }; 136 | } 137 | 138 | /** 139 | * Generates DMARC record. 140 | */ 141 | generateDMARC() { 142 | const DMARCRecord = this.formatRecords( 143 | "TXT", 144 | `_dmarc.${this.domain}`, 145 | `v=DMARC1; p=quarantine; pct=100; rua=mailto:dmarc-reports@${this.domain}; ruf=mailto:dmarc-failures@${this.domain}`, 146 | ); 147 | 148 | return DMARCRecord; 149 | } 150 | 151 | /** 152 | * Generates an MX record pointing to the mail server. 153 | */ 154 | generateMX() { 155 | const mxRecord = this.formatRecords( 156 | "MX", 157 | `${this.domain}`, 158 | `10 ${this.mailServer}`, 159 | ); 160 | 161 | return mxRecord; 162 | } 163 | 164 | /** 165 | * Generates an SRV record (useful for mail services like IMAP/SMTP). 166 | */ 167 | generateSRV(): string { 168 | return `_submission._tcp.${this.domain} 10 10 587 ${this.mailServer}`; 169 | } 170 | 171 | /** 172 | * Generates a custom TXT record. 173 | */ 174 | generateTXT(name: string, value: string): RecordTypeRespose { 175 | return this.formatRecords("TXT", name, value); 176 | } 177 | autodiscoverHandler = (domain: string): string => { 178 | return ` 179 | 180 | 181 | 182 | email 183 | settings 184 | 185 | IMAP 186 | ${domain} 187 | 993 188 | true 189 | 190 | 191 | 192 | 193 | `; 194 | }; 195 | /** 196 | * Generates all records for a domain. 197 | */ 198 | async generateAllRecords(): Promise<{ 199 | SPF: RecordTypeRespose; 200 | DKIM: { 201 | privateKey: string; 202 | selector: string; 203 | dkimRecord: RecordTypeRespose; 204 | }; 205 | DMARC: RecordTypeRespose; 206 | MX: RecordTypeRespose; 207 | }> { 208 | const { dkimRecord, privateKey } = this.generateDKIM(); 209 | 210 | return { 211 | SPF: await this.generateSPF(), 212 | DKIM: { 213 | privateKey, 214 | selector: this.dkimSelector, 215 | dkimRecord, 216 | }, 217 | DMARC: this.generateDMARC(), 218 | MX: this.generateMX(), 219 | }; 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/server/mta/go-mta.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/tls" 7 | "encoding/json" 8 | "fmt" 9 | "log" 10 | "runtime" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | "net/mail" 16 | 17 | "github.com/redis/go-redis/v9" 18 | "github.com/rovergulf/mta" 19 | ) 20 | 21 | type LoggingLevel string 22 | type EmailType string 23 | type LogType string 24 | 25 | // Define allowed values as constants 26 | const ( 27 | Alert LoggingLevel = "alert" 28 | Crit LoggingLevel = "crit" 29 | Error LoggingLevel = "error" 30 | Notice LoggingLevel = "notice" 31 | Info LoggingLevel = "info" 32 | Debug LoggingLevel = "debug" 33 | EmailTypeOutgoing EmailType = "outgoing" 34 | EmailTypeIncoming EmailType = "incoming" 35 | ) 36 | 37 | type IRedisClient struct { 38 | rdb *redis.Client 39 | } 40 | type BaseLogEntry struct { 41 | Message string `json:"message"` 42 | DomainName string `json:"domain_name"` 43 | Timestamp string `json:"timestamp"` 44 | } 45 | type LogMailsEntry struct { 46 | BaseLogEntry 47 | Type LoggingLevel `json:"type"` 48 | Event string `json:"event"` 49 | } 50 | type LogEmailEntry struct { 51 | BaseLogEntry 52 | Type EmailType `json:"type"` 53 | Status string `json:"status"` 54 | Email string `json:"email"` 55 | } 56 | 57 | var ctx = context.Background() 58 | var subscriptionChannel = "OUTGOING_MAILS" 59 | 60 | var queue = make(chan string, 1000) 61 | 62 | func MakeNewRedisClient(uri string) *IRedisClient { 63 | client, err := newRedisClient(uri) 64 | if err != nil { 65 | log.Fatalf("failed to create Redis client: %v", err) 66 | } 67 | return client 68 | } 69 | 70 | func newRedisClient(uri string) (*IRedisClient, error) { 71 | options, err := redis.ParseURL(uri) 72 | if err != nil { 73 | return nil, fmt.Errorf("invalid Redis URI: %v", err) 74 | } 75 | 76 | client := redis.NewClient(options) 77 | if err := client.Ping(ctx).Err(); err != nil { 78 | return nil, fmt.Errorf("redis connection failed: %v", err) 79 | } 80 | 81 | return &IRedisClient{rdb: client}, nil 82 | } 83 | 84 | func main() { 85 | 86 | client := MakeNewRedisClient("redis://localhost:6379") 87 | fmt.Println("Connected to Redis") 88 | sub := client.rdb.Subscribe(ctx, subscriptionChannel) 89 | status := sub.Ping(ctx) 90 | fmt.Println("status:", status) 91 | ch := sub.Channel() 92 | 93 | var wg sync.WaitGroup 94 | 95 | for i := 1; i <= runtime.NumCPU(); i++ { 96 | wg.Add(1) 97 | go client.Worker(&wg) 98 | } 99 | 100 | for msg := range ch { 101 | select { 102 | case queue <- msg.Payload: 103 | 104 | default: 105 | log.Println("queue full") 106 | } 107 | } 108 | 109 | close(queue) 110 | wg.Wait() 111 | } 112 | 113 | func (r *IRedisClient) Worker(wg *sync.WaitGroup) { 114 | defer wg.Done() 115 | 116 | for msg := range queue { 117 | r.processBatch(msg) 118 | } 119 | } 120 | 121 | func (r *IRedisClient) processBatch(batch string) { 122 | 123 | var emailData Email 124 | if err := json.Unmarshal([]byte(batch), &emailData); err != nil { 125 | fmt.Println("error parsing email job data:: %w", err) 126 | } 127 | fmt.Println("📧 Processing Email:", emailData.To) 128 | groupedRecipients := groupRecipientsByDomain(emailData.To) 129 | for domain, recipients := range groupedRecipients { 130 | mxRecords, err := r.getValue(domain) 131 | if err != nil { 132 | mxRecords, err = LookupMX(domain) 133 | if err != nil { 134 | logs := createFormattedLogMessage(LogType(Error), emailData.From, fmt.Sprintf("error getting MX records for domain %s: %s", domain, err.Error()), "SMTP Delivery Error") 135 | r.publishMailLogs(logs) 136 | } 137 | r.setValue(domain, mxRecords) 138 | } 139 | 140 | for _, recipient := range recipients { 141 | if err := SendMail(mxRecords, 25, emailData); err != nil { 142 | logs := createFormattedLogMessage(LogType(Error), emailData.From, fmt.Sprintf("error sending email to %s: %s", recipient, err.Error()), "SMTP Delivery Error") 143 | r.publishMailLogs(logs) 144 | 145 | r.publisEmailDeliveryLogs(createFormattedLogMessage(LogType(EmailTypeOutgoing), emailData.From, fmt.Sprintf("error sending email to %s: %s", recipient, err.Error()), "SMTP Delivery Error")) 146 | } 147 | } 148 | 149 | } 150 | 151 | } 152 | func createFormattedLogMessage(logType LogType, email string, message string, extra string) string { 153 | var logEntry interface{} 154 | if logType != LogType(EmailTypeIncoming) || logType != LogType(EmailTypeOutgoing) { 155 | logEntry = LogMailsEntry{ 156 | BaseLogEntry: BaseLogEntry{ 157 | Message: message, 158 | Timestamp: time.Now().Format(time.RFC3339), 159 | DomainName: email[strings.LastIndex(email, "@")+1:], 160 | }, 161 | Type: LoggingLevel(logType), 162 | Event: extra, 163 | } 164 | } else { 165 | logEntry = LogEmailEntry{ 166 | BaseLogEntry: BaseLogEntry{ 167 | Message: message, 168 | Timestamp: time.Now().Format(time.RFC3339), 169 | DomainName: email[strings.LastIndex(email, "@")+1:], 170 | }, 171 | Type: EmailType(logType), 172 | Status: extra, 173 | } 174 | } 175 | 176 | // Marshal struct to JSON 177 | logs, err := json.Marshal(logEntry) 178 | if err != nil { 179 | log.Fatalf("Failed to marshal logs: %v", err) 180 | } 181 | 182 | return string(logs) 183 | } 184 | func (r *IRedisClient) publishMailLogs(message string) { 185 | 186 | if err := r.rdb.Publish(ctx, "::channel_for_mail:logs", message).Err(); err != nil { 187 | log.Printf("Failed to publish step: %v", err) 188 | } 189 | } 190 | func (r *IRedisClient) publisEmailDeliveryLogs(message string) { 191 | 192 | if err := r.rdb.Publish(ctx, "::email:logs", message).Err(); err != nil { 193 | log.Printf("Failed to publish step: %v", err) 194 | } 195 | } 196 | func (r *IRedisClient) getValue(key string) (string, error) { 197 | val, err := r.rdb.Get(ctx, key).Result() 198 | if err == redis.Nil { 199 | return "", nil 200 | } else if err != nil { 201 | return "", err 202 | } 203 | return val, nil 204 | } 205 | 206 | func (r *IRedisClient) setValue(key string, value string) error { 207 | err := r.rdb.Set(ctx, key, value, 0).Err() 208 | return err 209 | } 210 | 211 | type Email struct { 212 | From string `json:"from"` 213 | To []string `json:"to"` 214 | Subject string `json:"subject"` 215 | Body string `json:"body"` 216 | } 217 | type SMTPClient struct { 218 | Host string 219 | Port int 220 | Username string 221 | Password string 222 | } 223 | 224 | func MailParser(rawEmail string) { 225 | msg, err := mail.ReadMessage(strings.NewReader(rawEmail)) 226 | if err != nil { 227 | log.Fatal("Error reading email:", err) 228 | } 229 | body := new(bytes.Buffer) 230 | _, err = body.ReadFrom(msg.Body) 231 | if err != nil { 232 | log.Fatal("Error reading body:", err) 233 | } 234 | 235 | } 236 | func SendMail(Host string, port int, emailData Email) error { 237 | 238 | d := mta.Dialer{Host: Host, Port: port} 239 | d.TLSConfig = &tls.Config{InsecureSkipVerify: true} 240 | s, err := d.Dial() 241 | if err != nil { 242 | log.Fatal(err) 243 | } 244 | s.Close() 245 | 246 | msg := mta.NewMessage() 247 | msg.SetAddressHeader("From", emailData.From, "Test MTA Sender") 248 | for _, to := range emailData.To { 249 | msg.SetAddressHeader("To", to, "") 250 | } 251 | msg.SetHeader("Subject", emailData.Subject) 252 | msg.SetHeader("MIME-version: 1.0") 253 | msg.SetBody("text/plain", emailData.Body) 254 | if err := d.DialAndSend(msg); err != nil { 255 | panic(err) 256 | } 257 | return nil 258 | } 259 | 260 | // GOOS=linux GOARCH=amd64 go build -o mta 261 | -------------------------------------------------------------------------------- /src/server/config/SpamFilteration.ts: -------------------------------------------------------------------------------- 1 | import dns from "dns"; 2 | import crypto from "crypto"; 3 | import forge from "node-forge"; 4 | 5 | const ipRequestCounts: Record = 6 | {}; 7 | const TEMP_BLOCK_DURATION = 60 * 1000; // 1-minute temporary block 8 | const tempBlockedIPs: Record = {}; // Stores temporarily blocked IPs with unblock timestamps 9 | const blockedIps = new Map(); 10 | export class SpamFilteration { 11 | static async checkBlackListIp(ip: string, thresold: number) { 12 | // Check if the IP is permanently blocked 13 | if (blockedIps.has(ip)) { 14 | throw new Error( 15 | "Your IP is permanently blocked due to excessive requests.", 16 | ); 17 | } 18 | const now = Date.now(); 19 | 20 | // Check if the IP is temporarily blocked 21 | if (tempBlockedIPs[ip] && tempBlockedIPs[ip] > now) { 22 | // If the IP still sends messages while blocked, mark it as permanently blocked 23 | 24 | blockedIps.set(ip, now); 25 | delete tempBlockedIPs[ip]; // Remove from temporary block list 26 | delete ipRequestCounts[ip]; // Reset any stored request data 27 | throw new Error( 28 | "Your IP has been permanently blocked due to continued abuse.", 29 | ); 30 | } else if (tempBlockedIPs[ip]) { 31 | // If the temporary block duration has passed, remove the IP from temp block list 32 | delete tempBlockedIPs[ip]; 33 | } 34 | 35 | // Rate limit check 36 | if (!ipRequestCounts[ip]) { 37 | ipRequestCounts[ip] = { count: 1, timestamp: now }; 38 | } else { 39 | const { count, timestamp } = ipRequestCounts[ip]; 40 | 41 | if (now - timestamp < 1000) { 42 | ipRequestCounts[ip].count++; 43 | 44 | if (ipRequestCounts[ip].count > thresold) { 45 | tempBlockedIPs[ip] = now + TEMP_BLOCK_DURATION; // Temporarily block IP 46 | delete ipRequestCounts[ip]; // Reset request count 47 | throw new Error( 48 | "Your IP has been temporarily blocked due to excessive requests.", 49 | ); 50 | } 51 | } else { 52 | ipRequestCounts[ip] = { count: 1, timestamp: now }; // Reset count for a new time window 53 | } 54 | } 55 | } 56 | checkRBL(ip: string): Promise { 57 | return new Promise((resolve) => { 58 | const reversedIP = ip.split(".").reverse().join("."); 59 | const rblDomain = `${reversedIP}.zen.spamhaus.org`; 60 | 61 | dns.resolve4(rblDomain, (err) => { 62 | if (err) 63 | resolve(false); // Not blacklisted 64 | else resolve(true); // Blacklisted 65 | }); 66 | }); 67 | } 68 | checkSPF(domain: string, senderIP: string): Promise { 69 | return new Promise((resolve) => { 70 | dns.resolveTxt(domain, (err, records) => { 71 | if (err || !records.length) return resolve(false); 72 | const spfRecord = records.flat().find((r) => r.startsWith("v=spf1")); 73 | if (!spfRecord) return resolve(false); 74 | resolve(spfRecord.includes(`ip4:${senderIP}`)); 75 | }); 76 | }); 77 | } 78 | 79 | detectSpam(content: string): boolean { 80 | const spamKeywords = [ 81 | "viagra", 82 | "lottery", 83 | "free money", 84 | "million dollars", 85 | "crypto scam", 86 | "click here", 87 | ]; 88 | return spamKeywords.some((keyword) => 89 | content.toLowerCase().includes(keyword), 90 | ); 91 | } 92 | private parseDkimSignature(header: string) { 93 | const fields = Object.fromEntries( 94 | header 95 | .split(";") 96 | .map((field) => field.trim().split("=")) 97 | .filter((kv) => kv.length === 2), 98 | ); 99 | 100 | return { 101 | v: fields["v"], // DKIM version 102 | a: fields["a"], // Algorithm (e.g., rsa-sha256) 103 | d: fields["d"], // Domain (e.g., example.com) 104 | s: fields["s"], // Selector (e.g., default) 105 | bh: fields["bh"], // Body hash 106 | b: fields["b"], // Signature 107 | h: fields["h"].split(":").map((h: string) => h.trim()), // Signed headers 108 | }; 109 | } 110 | /** 111 | * Fetches the DKIM public key from DNS 112 | */ 113 | async fetchDkimPublicKey(domain: string, selector: string): Promise { 114 | const dnsRecord = `${selector}._domainkey.${domain}`; 115 | return new Promise((resolve, reject) => { 116 | dns.resolveTxt(dnsRecord, (err, records) => { 117 | if (err) return reject(err); 118 | const keyRecord = records.flat().find((r) => r.startsWith("v=DKIM1;")); 119 | if (!keyRecord) return reject(new Error("DKIM record not found")); 120 | const match = keyRecord.match(/p=([^;]+)/); 121 | if (!match) return reject(new Error("Public key not found")); 122 | resolve(match[1]); 123 | }); 124 | }); 125 | } 126 | /** 127 | * Recreate the canonicalized signed headers 128 | */ 129 | private reconstructSignedHeaders( 130 | emailHeaders: string, 131 | signedHeaderFields: string[], 132 | ): string { 133 | const headerMap = Object.fromEntries( 134 | emailHeaders 135 | .split("\n") 136 | .map((line) => line.trim()) 137 | .filter((line) => line.includes(":")) 138 | .map((line) => { 139 | const [key, ...value] = line.split(":"); 140 | return [key.toLowerCase(), value.join(":").trim()]; 141 | }), 142 | ); 143 | 144 | return signedHeaderFields 145 | .map((field) => `${field}:${headerMap[field.toLowerCase()]}`) 146 | .join("\r\n"); 147 | } 148 | /** 149 | * Verify DKIM Signature 150 | */ 151 | async verifyDkimSignature(emailHeaders: string, dkimHeader: string) { 152 | const dkim = this.parseDkimSignature(dkimHeader); 153 | const publicKey = await this.fetchDkimPublicKey(dkim.d, dkim.s); 154 | const signedHeaders = this.reconstructSignedHeaders(emailHeaders, dkim.h); 155 | const signedData = signedHeaders + "\r\n"; 156 | const signatureBuffer = Buffer.from(dkim.b, "base64"); 157 | const pemKey = `-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`; 158 | 159 | const verifier = crypto.createVerify("RSA-SHA256"); 160 | verifier.update(signedData); 161 | const isValid = verifier.verify(pemKey, signatureBuffer); 162 | 163 | return isValid; 164 | } 165 | /** 166 | * Canonicalize email body and compute the SHA-256 hash 167 | */ 168 | private hashEmailBody(body: string): string { 169 | const canonicalBody = body.trim() + "\r\n"; // DKIM relaxed body canonicalization 170 | return crypto.createHash("sha256").update(canonicalBody).digest("base64"); 171 | } 172 | /** 173 | * Canonicalize headers based on DKIM rules 174 | */ 175 | canonicalizeHeaders( 176 | headers: Record, 177 | signedHeaders: string[], 178 | ): string { 179 | return signedHeaders 180 | .map((field) => `${field}:${headers[field.toLowerCase()].trim()}`) 181 | .join("\r\n"); 182 | } 183 | /** 184 | * Generate a DKIM signature 185 | */ 186 | signDkim( 187 | headers: Record, 188 | body: string, 189 | domain: string, 190 | selector: string, 191 | privateKeyPem: string, 192 | ): string { 193 | const signedHeadersList = [ 194 | "from", 195 | "subject", 196 | "date", 197 | "message-id", 198 | "content-type", 199 | "content-transfer-encoding", 200 | "mime-version", 201 | ]; // Fields to sign 202 | const bh = this.hashEmailBody(body); 203 | 204 | const dkimHeader = 205 | `v=1; a=rsa-sha256; c=relaxed/relaxed; d=${domain}; s=${selector}; ` + 206 | `h=${signedHeadersList.join(":")}; bh=${bh}; b=`; 207 | 208 | const canonicalHeaders = 209 | this.canonicalizeHeaders(headers, signedHeadersList) + 210 | "\r\n" + 211 | dkimHeader; 212 | 213 | const signer = crypto.createSign("RSA-SHA256"); 214 | signer.update(canonicalHeaders); 215 | const signature = signer.sign(privateKeyPem, "base64"); 216 | 217 | return `DKIM-Signature: ${dkimHeader}${signature}`; 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/lib/helpers/utils.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from "crypto"; 2 | import moment from "moment"; 3 | import { join } from "path"; 4 | 5 | export let Tokens = new Map(); 6 | export let BlacklistedTokens: string[] = []; 7 | export const SetAppRoutes = new Map(); 8 | type Units = "d" | "h" | "m" | "w"; 9 | export type TTL = `${number}${Units}`; 10 | class Utils { 11 | ParseTTL(ttl: TTL | number): number { 12 | if (typeof ttl === "number") { 13 | return ttl; 14 | } 15 | 16 | const timeUnits: { [key: string]: number } = { 17 | d: 86400, 18 | h: 3600, 19 | m: 60, 20 | w: 604800, 21 | }; 22 | 23 | const regex = /^(\d+)([dhmw])$/i; // Regex to capture number and unit 24 | const match = ttl.match(regex); 25 | 26 | if (match) { 27 | const value = Number.parseInt(match[1], 10); // Extract number part 28 | const unit = match[2].toLowerCase(); // Extract unit (d, h, m, w) 29 | 30 | if (timeUnits[unit]) { 31 | return value * timeUnits[unit]; // Convert to seconds 32 | } 33 | } 34 | 35 | return 60; 36 | } 37 | CreatePath = (currentPath: string) => { 38 | const currentPathArray = currentPath.split("/"); 39 | return join(process.cwd(), ...currentPathArray); 40 | }; 41 | RandomToken(length: number = 64): string { 42 | return crypto.randomBytes(length).toString("hex"); 43 | } 44 | /** 45 | * Generates a random number within a specified range. 46 | * 47 | * @param {number} min - The minimum value of the range (default: 100000). 48 | * @param {number} max - The maximum value of the range (default: 999999). 49 | * @return {number} - The randomly generated number. 50 | */ 51 | RandomNumber(min: number = 100000, max: number = 999999): number { 52 | return Math.floor(Math.random() * (max - min + 1) + min); 53 | } 54 | /** 55 | * Generates a unique user ID. 56 | * 57 | * @return {string} The generated user ID. 58 | */ 59 | CreateUserID(): string { 60 | const id = Math.floor(Math.random() * 10000000).toString(); 61 | return id; 62 | } 63 | /** 64 | * Generates a token of random bytes with the specified byte length. 65 | * 66 | * @param {number} byteLength - The length of the token in bytes. Defaults to 48. 67 | * @return {string} - The generated token as a base64-encoded string. 68 | */ 69 | GenerateToken(byteLength: number = 48): string { 70 | return crypto.randomBytes(byteLength).toString("base64"); 71 | } 72 | /** 73 | * Generates a refresh token of a specified length. 74 | * 75 | * @param {number} byteLength - The length of the refresh token in bytes. Defaults to 32. 76 | * @return {string} - The generated refresh token. 77 | */ 78 | CreateRefreshToken(byteLength: number = 32): string { 79 | return crypto.randomBytes(byteLength).toString("base64"); 80 | } 81 | /** 82 | * Generates a random request ID with the specified byte length. 83 | * 84 | * @param {number} byteLength - The length of the byte array used to generate the request ID. Defaults to 16. 85 | * @return {string} - The generated request ID as a base64 encoded string. 86 | */ 87 | RequestId(byteLength: number = 16): string { 88 | return crypto.randomBytes(byteLength).toString("base64"); 89 | } 90 | /** 91 | * Generates a new refresh token for the given ID and stores it in the Tokens map. 92 | * 93 | * @param {string} id - The ID of the user for whom the refresh token is generated. 94 | * @return {string} - The newly generated refresh token. 95 | */ 96 | HandleRefreshToken(id: string): string { 97 | const RefreshToken = this.CreateRefreshToken(); 98 | Tokens.set(id, RefreshToken); 99 | return RefreshToken; 100 | } 101 | /** 102 | * Converts a string to a number. 103 | * 104 | * 105 | * @return {number} The converted number. 106 | */ 107 | CreateOTP(min: number = 100000, max: number = 999999): number { 108 | return Math.floor(Math.random() * (max - min + 1) + min); 109 | } 110 | /** 111 | * Converts a query string to an object. 112 | * 113 | * @param {string} query - The query string to convert. 114 | * @return {object} The resulting object. 115 | */ 116 | QueryToObject(query: string): object { 117 | let NewObject = {}; 118 | query.split("&").map((item) => { 119 | const [key, value] = item.split("="); 120 | Object.assign(NewObject, Object.fromEntries([[key, value]])); 121 | return Object.fromEntries([[key, value]]); 122 | }); 123 | 124 | return NewObject; 125 | } 126 | ObjectToQuery(obj: Record): string { 127 | return Object.entries(obj) 128 | .map(([key, value]) => { 129 | if (typeof value === "object") { 130 | return `${key}=${JSON.stringify(value)}`; 131 | } 132 | return `${key}=${value}`; 133 | }) 134 | .join("&"); 135 | } 136 | /** 137 | * Converts a string representation of a date word into a formatted date string. 138 | * 139 | * @param {string} str - The string representation of the date word. 140 | * @return {string} - The formatted date string. 141 | */ 142 | ConvertDateWordsToDate(str: string): string { 143 | let newDate; 144 | if (str === "Latest") { 145 | newDate = moment().subtract(10, "minutes").toDate(); 146 | } else if (str === "LastHour") { 147 | newDate = moment().subtract(1, "hour").toDate(); 148 | } else if (str === "Last24hour") { 149 | newDate = moment().subtract(24, "hour").toDate(); 150 | } else { 151 | const number = str.replace(/[^0-9]/g, ""); 152 | newDate = moment().subtract(number, `days`).toDate(); 153 | } 154 | 155 | const date = this.SimpleDateStr(newDate); 156 | return date; 157 | } 158 | /** 159 | * Generates a string representation of a date. 160 | * 161 | * @param {Date} newDate - The date to convert to a string. Defaults to the current date. 162 | * @return {string} The string representation of the date. 163 | */ 164 | SimpleDateStr(newDate: Date = new Date()): string { 165 | const newDateStr = newDate.toISOString().split("T"); 166 | const date = (newDateStr[0] + " " + newDateStr[1].split(".")[0]) 167 | .trim() 168 | .toString(); 169 | 170 | return date; 171 | } 172 | /** 173 | * Cleans and purifies a string by converting it to lowercase, removing leading and trailing whitespace, 174 | * removing all spaces, replacing multiple spaces or underscores with a single hyphen, 175 | * and removing leading and trailing hyphens. 176 | * 177 | * @param {string} str - The string to be purified. 178 | * @return {string} The purified string. 179 | */ 180 | PurifyString(str: string): string { 181 | return str 182 | .toLowerCase() 183 | .trim() 184 | .replace(/\s/g, "") 185 | .replace(/[\s_-]+/g, "-") 186 | .replace(/^-+|-+$/g, ""); 187 | } 188 | /** 189 | * Generates the keys and values of an object. 190 | * 191 | * @param {string} obj - the object to generate keys and values for 192 | * @return {string[]} an array containing the keys and values of the object 193 | */ 194 | ObjectKeysAndValues(obj: string): string[] { 195 | let keys = Object.keys(JSON.parse(obj)); 196 | const PureObject = keys.map((key) => { 197 | return JSON.parse(JSON.parse(obj)[key]); 198 | }); 199 | return PureObject; 200 | } 201 | /** 202 | * Formats the salary to a string with comma-separated thousands and no decimal places. 203 | * 204 | * @param {number} salary - The salary to be formatted. 205 | * @return {string} - The formatted salary as a string. 206 | */ 207 | IndianNumberFormat(salary: number): string { 208 | return salary.toLocaleString("en-IN", { maximumFractionDigits: 0 }); 209 | } 210 | 211 | /** 212 | * Slugify a given string. 213 | * 214 | * @param {string} str - The string to be slugified. 215 | * @return {string} The slugified string. 216 | */ 217 | Slugify(str: string): string { 218 | return str 219 | .toLowerCase() 220 | .trim() 221 | .replace(/[^\w\s-]/g, "") 222 | .replace(/[\s_-]+/g, "-") 223 | .replace(/^-+|-+$/g, ""); 224 | } 225 | uuid_v4() { 226 | return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace( 227 | /[xy]/g, 228 | function (c) { 229 | var r = (Math.random() * 16) | 0, 230 | v = c == "x" ? r : (r & 0x3) | 0x8; 231 | return v.toString(16); 232 | }, 233 | ); 234 | } 235 | Md5Checksum(content: string): string { 236 | return crypto.createHash("md5").update(content).digest("hex"); 237 | } 238 | 239 | SimpleHash(): string { 240 | return crypto.randomBytes(32).toString("hex"); 241 | } 242 | } 243 | export default new Utils(); 244 | --------------------------------------------------------------------------------