diff --git a/.cron b/.cron new file mode 100644 index 0000000..f22bfe6 --- /dev/null +++ b/.cron @@ -0,0 +1,3 @@ + +0 * * * * /root/temp_mail/scripts/delete_messages_24h.sh >> /var/log/temp_mail/cleanup.log 2>&1 +0 0 * * * /root/temp_mail/scripts/delete_emails_14d.sh >> /var/log/temp_mail/cleanup.log 2>&1 diff --git a/.env-example b/.env-example new file mode 100644 index 0000000..d7be63c --- /dev/null +++ b/.env-example @@ -0,0 +1,2 @@ +MYSQL_ADMIN_USER=admin +MYSQL_ADMIN_PASSWORD= \ No newline at end of file diff --git a/config/database-example.js b/config/database-example.js new file mode 100644 index 0000000..d003e19 --- /dev/null +++ b/config/database-example.js @@ -0,0 +1,15 @@ +module.exports = { + development: { + client: 'mysql2', + connection: { + database: 'email_api', + user: 'email_api', + password: '', + host: 'localhost', + port: 3306 + }, + migrations: { + directory: '../db/migrations' + } + } + }; \ No newline at end of file diff --git a/package.json b/package.json index 9288095..f7dc145 100644 --- a/package.json +++ b/package.json @@ -3,30 +3,43 @@ "version": "1.0.0", "main": "app.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "node app.js", - "dev": "nodemon app.js" + "start": "npm run setup && node app.js", + "dev": "npm run setup && nodemon app.js", + "lint": "eslint .", + "lint:fix": "eslint --fix .", + "setup": "bash setup.sh" }, "keywords": [], - "author": "", + "author": "Ryahn", "license": "ISC", - "description": "", "dependencies": { "bcrypt": "^5.1.1", + "chokidar": "^4.0.3", "cors": "^2.8.5", "express": "^4.21.2", "favicon": "^0.0.2", "haraka": "^0.0.33", "haraka-plugin-dkim": "^1.0.9", + "html-to-text": "^9.0.5", "jsonwebtoken": "^9.0.2", "knex": "^3.1.0", "moniker": "^0.1.2", "mysql2": "^3.12.0", "node-cron": "^3.0.3", + "nodemailer": "^6.10.0", "objection": "^3.1.5", + "sanitize-html": "^2.14.0", "serve-favicon": "^2.5.0", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", "unique-names-generator": "^4.7.1" + }, + "repository": { + "type": "git", + "url": "git@git.zonies.xyz:Ryahn/Temp-Email-Service.git" + }, + "description": "", + "devDependencies": { + "eslint": "^9.21.0" } } diff --git a/scripts/delete_user.js b/scripts/delete_user.js index 2f61561..53902ca 100644 --- a/scripts/delete_user.js +++ b/scripts/delete_user.js @@ -1,27 +1,28 @@ -const User = require('../src/db/models/User'); -const { Model } = require('objection'); -const Knex = require('knex'); -const knexConfig = require('../src/config/database'); +const User = require("../src/db/models/User"); +const { Model } = require("objection"); +const Knex = require("knex"); +const knexConfig = require("../src/config/database"); const knex = Knex(knexConfig.development); Model.knex(knex); +const Logger = require("../src/utils/logger"); +const log = new Logger(); const email = process.argv[2]; const id = process.argv[3]; if (!email && !id) { - console.error('Usage: node delete_user.js || '); - process.exit(1); + log.error("Usage: node delete_user.js || "); + process.exit(1); } async function main() { - let user; - if (id) { - user = await User.query().where('id', id).delete(); - } else { - user = await User.query().where('email', email).delete(); - } - console.log(`User deleted`); - process.exit(0); + if (id) { + await User.query().where("id", id).delete(); + } else { + await User.query().where("email", email).delete(); + } + log.info(`User deleted`); + process.exit(0); } -main(); \ No newline at end of file +main(); diff --git a/scripts/generate_token.js b/scripts/generate_token.js index 351431e..51583fc 100644 --- a/scripts/generate_token.js +++ b/scripts/generate_token.js @@ -29,7 +29,7 @@ async function main() { ); }; - token = generateToken(); + const token = generateToken(); await User.query().where("id", user.id).update({ api_key: token }); console.log("Token generated and updated for user", user.email); console.log("Token:", token); diff --git a/setup.sh b/setup.sh index a290179..bebb6f6 100755 --- a/setup.sh +++ b/setup.sh @@ -1,21 +1,242 @@ #! /bin/bash + +# Color definitions +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Logger functions +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + CURRENT_DIR=$(pwd) +SERVICE_FILE="haraka.service" +SERVICE_PATH="/etc/systemd/system/$SERVICE_FILE" +LOCAL_SERVICE_PATH="$CURRENT_DIR/$SERVICE_FILE" + +# Load environment variables +if [ -f "$CURRENT_DIR/.env" ]; then + export $(cat "$CURRENT_DIR/.env" | grep -v '^#' | xargs) +else + log_error ".env file not found" + exit 1 +fi + +# Check if MYSQL_ADMIN_PASSWORD is set +if [ -z "$MYSQL_ADMIN_PASSWORD" ]; then + log_error "MYSQL_ADMIN_PASSWORD not found in .env file" + exit 1 +fi + +if [ -z "$MYSQL_ADMIN_USER" ]; then + log_error "MYSQL_ADMIN_USER not found in .env file" + exit 1 +fi + +# Database configuration +DB_NAME="email_api1" +DB_USER="email_api1" +DB_PASS=$(openssl rand -base64 12) # Generate random password + +# Check if mysql-server is installed +if ! command -v mysql &> /dev/null; then + log_warn "MySQL Server not found. Installing..." + sudo apt-get update >> /dev/null 2>&1 + sudo apt-get install -y mysql-server >> /dev/null 2>&1 +else + log_info "MySQL Server is already installed" +fi + +# Wait for MySQL to be ready +log_info "Waiting for MySQL to be ready..." +while ! mysqladmin ping -h "localhost" -u admin -p"${MYSQL_ADMIN_PASSWORD}" --silent; do + sleep 1 +done + +# Create database and user +log_info "Setting up MySQL database and user..." +mysql -u admin -p"${MYSQL_ADMIN_PASSWORD}" -e "CREATE DATABASE IF NOT EXISTS ${DB_NAME};" + +# Check if user exists +USER_EXISTS=$(mysql -u "${MYSQL_ADMIN_USER}" -p"${MYSQL_ADMIN_PASSWORD}" -s -N -e "SELECT COUNT(*) FROM mysql.user WHERE User = '${DB_USER}' AND Host = 'localhost';") + +if [ "$USER_EXISTS" -eq 0 ]; then + log_info "Creating new database user..." + mysql -u "${MYSQL_ADMIN_USER}" -p"${MYSQL_ADMIN_PASSWORD}" -e "CREATE USER '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}';" + mysql -u "${MYSQL_ADMIN_USER}" -p"${MYSQL_ADMIN_PASSWORD}" -e "GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'localhost';" + mysql -u "${MYSQL_ADMIN_USER}" -p"${MYSQL_ADMIN_PASSWORD}" -e "FLUSH PRIVILEGES;" + + # Update database config file with new values + log_info "Updating database configuration..." + cp "$CURRENT_DIR/src/config/database-example.js" "$CURRENT_DIR/src/config/database.js" + CONFIG_FILE="$CURRENT_DIR/src/config/database.js" + sed -i "s/database: \"\"/database: '${DB_NAME}'/" "$CONFIG_FILE" + sed -i "s/user: \"\"/user: '${DB_USER}'/" "$CONFIG_FILE" + sed -i "s/password: \"\"/password: '${DB_PASS}'/" "$CONFIG_FILE" + log_info "Database password has been updated in the configuration file" +else + log_warn "Database user already exists. Skipping user creation and configuration update." +fi # Create group and user -sudo groupadd smtp -sudo useradd -r -g smtp smtp +log_info "Creating group and user..." +sudo groupadd smtp 2>/dev/null || true +sudo useradd -r -g smtp smtp 2>/dev/null || true # Create required directories +log_info "Creating required directories..." sudo mkdir -p /var/spool/haraka /var/log/haraka sudo chown -R smtp:smtp /var/spool/haraka /var/log/haraka # Set permissions +log_info "Setting permissions..." sudo chmod 755 /var/spool/haraka sudo chmod 644 /var/log/haraka # Install Haraka -npm install -g haraka +log_info "Installing Haraka..." +if ! command -v haraka &> /dev/null; then + npm install -g Haraka +else + log_warn "Haraka is already installed" +fi + +# Check if service file exists and handle accordingly +log_info "Checking if service file exists..." +if [ -f "$SERVICE_PATH" ]; then + # Calculate checksums + REMOTE_CHECKSUM=$(sha1sum "$SERVICE_PATH" | awk '{print $1}') + LOCAL_CHECKSUM=$(sha1sum "$LOCAL_SERVICE_PATH" | awk '{print $1}') + + if [ "$REMOTE_CHECKSUM" != "$LOCAL_CHECKSUM" ]; then + log_warn "Service file differs from local version. Updating..." + sudo cp "$LOCAL_SERVICE_PATH" "$SERVICE_PATH" + sudo systemctl daemon-reload + sudo systemctl enable haraka + sudo systemctl restart haraka + else + log_info "Service file is up to date" + fi +else + log_info "Service file not found. Installing..." + sudo cp "$LOCAL_SERVICE_PATH" "$SERVICE_PATH" + sudo systemctl daemon-reload + log_info "Enabling and starting Haraka..." + sudo systemctl enable haraka +fi # Install Haraka plugins -mkdir -p $CURRENT_DIR/src/email_server/plugins/queue -ln -s $CURRENT_DIR/src/haraka-plugins/queue/store_message.js $CURRENT_DIR/src/email_server/plugins/queue/store_message.js \ No newline at end of file +log_info "Installing Haraka plugins..." +mkdir -p "$CURRENT_DIR/src/email_server/plugins/queue" +rm -f "$CURRENT_DIR/src/email_server/plugins/queue/store_message.js" +ln -s "$CURRENT_DIR/src/haraka-plugins/queue/store_message.js" "$CURRENT_DIR/src/email_server/plugins/queue/store_message.js" + +log_info "Installing dependencies..." +npm install >> /dev/null 2>&1 + +log_info "Running migrations..." +npx knex migrate:latest + +CRON_FILE="$CURRENT_DIR/.cron" +echo "" > "$CRON_FILE" +echo "0 * * * * $CURRENT_DIR/scripts/delete_messages_24h.sh >> /var/log/temp_mail/cleanup.log 2>&1" >> "$CRON_FILE" +echo "0 0 * * * $CURRENT_DIR/scripts/delete_emails_14d.sh >> /var/log/temp_mail/cleanup.log 2>&1" >> "$CRON_FILE" + +TEMP_CRON="/tmp/temp_cron" + +mkdir -p /var/log/temp_mail + +# Check if .cron file exists +if [ ! -f "$CRON_FILE" ]; then + log_error "Cron file not found at $CRON_FILE" + exit 1 +fi + +# Function to normalize cron entry (remove extra spaces) +normalize_cron() { + echo "$1" | sed -e 's/^[ \t]*//' -e 's/[ \t]*$//' -e 's/[ \t]\+/ /g' +} + +# Function to check if a cron job exists +check_cron_job() { + local cron_entry="$1" + local normalized_entry=$(normalize_cron "$cron_entry") + + # Get current user's crontab + crontab -l > "$TEMP_CRON" 2>/dev/null + + # Check if the normalized entry exists in current crontab + if grep -Fq "$normalized_entry" "$TEMP_CRON"; then + return 0 # Found + else + return 1 # Not found + fi +} + +# Function to install cron jobs +install_cron_jobs() { + log_info "Installing cron jobs..." + + # Get current crontab + crontab -l > "$TEMP_CRON" 2>/dev/null + + # Read each line from .cron file + while IFS= read -r line || [ -n "$line" ]; do + # Skip comments and empty lines + [[ $line =~ ^#.*$ ]] || [ -z "$line" ] && continue + + normalized_line=$(normalize_cron "$line") + + if ! grep -Fq "$normalized_line" "$TEMP_CRON"; then + log_warn "Cron job not found, adding: $normalized_line" + echo "$normalized_line" >> "$TEMP_CRON" + else + log_info "Cron job already exists: $normalized_line" + fi + done < "$CRON_FILE" + + # Install new crontab + crontab "$TEMP_CRON" + + # Clean up + rm -f "$TEMP_CRON" +} + +# Main execution +log_info "Checking cron jobs..." + +# Read .cron file and check each entry +missing_jobs=0 +while IFS= read -r line || [ -n "$line" ]; do + # Skip comments and empty lines + [[ $line =~ ^#.*$ ]] || [ -z "$line" ] && continue + + if ! check_cron_job "$line"; then + log_warn "Missing cron job: $line" + missing_jobs=$((missing_jobs + 1)) + else + log_info "Found cron job: $line" + fi +done < "$CRON_FILE" + +# If any jobs are missing, offer to install them +if [ $missing_jobs -gt 0 ]; then + log_warn "Found $missing_jobs missing cron job(s)" + install_cron_jobs + log_info "Cron jobs have been updated" +else + log_info "All cron jobs are up to date" +fi + +log_info "Installation complete!" +log_info "You can now start the email server with: sudo systemctl start haraka" \ No newline at end of file diff --git a/src/config/database-example.js b/src/config/database-example.js index d003e19..5dea436 100644 --- a/src/config/database-example.js +++ b/src/config/database-example.js @@ -1,15 +1,15 @@ module.exports = { - development: { - client: 'mysql2', - connection: { - database: 'email_api', - user: 'email_api', - password: '', - host: 'localhost', - port: 3306 - }, - migrations: { - directory: '../db/migrations' - } - } - }; \ No newline at end of file + development: { + client: "mysql2", + connection: { + database: "", + user: "", + password: "", + host: "localhost", + port: 3306, + }, + migrations: { + directory: "../db/migrations", + }, + }, +}; \ No newline at end of file diff --git a/src/routes/email.js b/src/routes/email.js index 5bd0d97..abb8587 100644 --- a/src/routes/email.js +++ b/src/routes/email.js @@ -3,7 +3,6 @@ const router = express.Router(); const { authenticateToken } = require('../middleware/auth'); const TempEmail = require('../db/models/TempEmail'); router.use(authenticateToken); -const Domain = require('../db/models/Domain'); const DailyStats = require('../db/models/DailyStats'); const { generateUniqueName, getRandomDomain, mysqlSafeTimestamp } = require('../utils/functions'); diff --git a/src/routes/messages.js b/src/routes/messages.js index 0ab935d..0298e01 100644 --- a/src/routes/messages.js +++ b/src/routes/messages.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); const { authenticateToken } = require('../middleware/auth'); const Message = require('../db/models/Message'); - +const TempEmail = require("../db/models/TempEmail"); router.use(authenticateToken); /** diff --git a/src/utils/logger.js b/src/utils/logger.js new file mode 100644 index 0000000..2a3e976 --- /dev/null +++ b/src/utils/logger.js @@ -0,0 +1,51 @@ +class Logger { + constructor() { + this.levels = { + INFO: "\x1b[36m", // Cyan + WARNING: "\x1b[33m", // Yellow + ERROR: "\x1b[31m", // Red + CRITICAL: "\x1b[31m\x1b[1m", // Bold Red + DEBUG: "\x1b[32m", // Green + RESET: "\x1b[0m", // Reset color + }; + } + + _getTimestamp() { + const now = new Date(); + return now.toISOString().replace(/T/, " ").replace(/\..+/, ""); + } + + _log(level, message) { + const timestamp = this._getTimestamp(); + const color = this.levels[level] || ""; + console.log( + `${color}[${timestamp}] [${level}] ${message}${this.levels.RESET}` + ); + } + + info(message) { + this._log("INFO", message); + } + + warning(message) { + this._log("WARNING", message); + } + + error(message) { + this._log("ERROR", message); + } + + critical(message) { + this._log("CRITICAL", message); + } + + debug(message) { + this._log("DEBUG", message); + } +} + +// Usage +// const log = new Logger(); +// log.info("This is an info message"); + +module.exports = Logger;