From 5faf8817de8b0bedcbc3745e44cad1504327473c Mon Sep 17 00:00:00 2001 From: cproudlock Date: Tue, 30 Dec 2025 13:15:05 -0500 Subject: [PATCH] Initial commit: Python email notification API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Flask-based email API for manufacturing notifications - SMTP integration with configurable settings 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .gitignore | 4 + app.py | 337 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 + start.sh | 31 +++++ 4 files changed, 374 insertions(+) create mode 100644 .gitignore create mode 100644 app.py create mode 100644 requirements.txt create mode 100755 start.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2b91c72 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +venv/ +__pycache__/ +*.pyc +.env diff --git a/app.py b/app.py new file mode 100644 index 0000000..f98a556 --- /dev/null +++ b/app.py @@ -0,0 +1,337 @@ +""" +ShopDB Email API +================ +Flask API for sending emails via SMTP with STARTTLS support. +Designed to be called from Classic ASP pages that cannot handle STARTTLS. + +Environment Variables: + SMTP_HOST - SMTP server hostname + SMTP_PORT - SMTP port (default: 587) + SMTP_USER - SMTP username + SMTP_PASS - SMTP password + SMTP_FROM - Default from address (optional) + API_KEY - Simple API key for authentication (optional) + DB_HOST - MySQL host for distribution groups (default: 192.168.122.1) + DB_USER - MySQL username (default: 570005354) + DB_PASS - MySQL password + DB_NAME - MySQL database (default: shopdb) + +Usage from ASP: + Set http = Server.CreateObject("MSXML2.ServerXMLHTTP.6.0") + http.open "POST", "http://localhost:5000/api/send", False + http.setRequestHeader "Content-Type", "application/json" + http.send "{""to"":""user@example.com"",""subject"":""Test"",""message"":""Hello""}" +""" + +import os +import smtplib +import logging +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from flask import Flask, request, jsonify +from functools import wraps + +# Optional MySQL support for distribution groups +try: + import mysql.connector + MYSQL_AVAILABLE = True +except ImportError: + MYSQL_AVAILABLE = False + +app = Flask(__name__) + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +# Configuration from environment +SMTP_HOST = os.environ.get('SMTP_HOST', '') +SMTP_PORT = int(os.environ.get('SMTP_PORT', '587')) +SMTP_USER = os.environ.get('SMTP_USER', '') +SMTP_PASS = os.environ.get('SMTP_PASS', '') +SMTP_FROM = os.environ.get('SMTP_FROM', SMTP_USER) +API_KEY = os.environ.get('API_KEY', '') + +# Database config for distribution groups +DB_HOST = os.environ.get('DB_HOST', '192.168.122.1') +DB_PORT = int(os.environ.get('DB_PORT', '3306')) +DB_USER = os.environ.get('DB_USER', '570005354') +DB_PASS = os.environ.get('DB_PASS', '570005354') +DB_NAME = os.environ.get('DB_NAME', 'shopdb') + + +def require_api_key(f): + """Optional API key authentication decorator.""" + @wraps(f) + def decorated(*args, **kwargs): + if API_KEY: + provided_key = request.headers.get('X-API-Key') or request.args.get('api_key') + if provided_key != API_KEY: + return jsonify({'success': False, 'error': 'Invalid or missing API key'}), 401 + return f(*args, **kwargs) + return decorated + + +def get_db_connection(): + """Get MySQL database connection.""" + if not MYSQL_AVAILABLE: + return None + try: + return mysql.connector.connect( + host=DB_HOST, + port=DB_PORT, + user=DB_USER, + password=DB_PASS, + database=DB_NAME + ) + except Exception as e: + logger.error(f"Database connection failed: {e}") + return None + + +def send_email(to_addresses, subject, message, html=False, from_addr=None): + """ + Send email via SMTP with STARTTLS. + + Args: + to_addresses: Single email or list of emails + subject: Email subject + message: Email body (plain text or HTML) + html: If True, send as HTML email + from_addr: Optional from address (defaults to SMTP_FROM) + + Returns: + tuple: (success: bool, error_message: str or None) + """ + if not SMTP_HOST or not SMTP_USER or not SMTP_PASS: + return False, "SMTP not configured. Set SMTP_HOST, SMTP_USER, SMTP_PASS environment variables." + + # Normalize to list + if isinstance(to_addresses, str): + to_addresses = [to_addresses] + + # Filter empty addresses + to_addresses = [addr.strip() for addr in to_addresses if addr and addr.strip()] + + if not to_addresses: + return False, "No valid recipient addresses provided" + + from_addr = from_addr or SMTP_FROM + + try: + # Create message + if html: + msg = MIMEMultipart('alternative') + msg.attach(MIMEText(message, 'html')) + else: + msg = MIMEText(message, 'plain') + + msg['Subject'] = subject + msg['From'] = from_addr + msg['To'] = ', '.join(to_addresses) + + # Connect and send + logger.info(f"Connecting to {SMTP_HOST}:{SMTP_PORT}") + + with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=30) as server: + server.ehlo() + server.starttls() + server.ehlo() + server.login(SMTP_USER, SMTP_PASS) + server.sendmail(from_addr, to_addresses, msg.as_string()) + + logger.info(f"Email sent successfully to {to_addresses}") + return True, None + + except smtplib.SMTPAuthenticationError as e: + error = f"SMTP authentication failed: {e}" + logger.error(error) + return False, error + except smtplib.SMTPException as e: + error = f"SMTP error: {e}" + logger.error(error) + return False, error + except Exception as e: + error = f"Failed to send email: {e}" + logger.error(error) + return False, error + + +@app.route('/api/health', methods=['GET']) +def health_check(): + """Health check endpoint.""" + smtp_configured = bool(SMTP_HOST and SMTP_USER and SMTP_PASS) + return jsonify({ + 'status': 'ok', + 'smtp_configured': smtp_configured, + 'mysql_available': MYSQL_AVAILABLE + }) + + +@app.route('/api/send', methods=['POST']) +@require_api_key +def send_email_endpoint(): + """ + Send an email. + + Request JSON: + { + "to": "email@example.com" or ["email1@example.com", "email2@example.com"], + "subject": "Email subject", + "message": "Email body", + "html": false, // optional, default false + "from": "sender@example.com" // optional + } + + Response JSON: + {"success": true} or {"success": false, "error": "error message"} + """ + try: + data = request.get_json() + + if not data: + return jsonify({'success': False, 'error': 'No JSON data provided'}), 400 + + to_addresses = data.get('to') + subject = data.get('subject', '') + message = data.get('message', '') + html = data.get('html', False) + from_addr = data.get('from') + + if not to_addresses: + return jsonify({'success': False, 'error': 'Missing "to" field'}), 400 + + if not subject: + return jsonify({'success': False, 'error': 'Missing "subject" field'}), 400 + + if not message: + return jsonify({'success': False, 'error': 'Missing "message" field'}), 400 + + success, error = send_email(to_addresses, subject, message, html, from_addr) + + if success: + return jsonify({'success': True}) + else: + return jsonify({'success': False, 'error': error}), 500 + + except Exception as e: + logger.error(f"Error in send endpoint: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + +@app.route('/api/send-to-group', methods=['POST']) +@require_api_key +def send_to_distribution_group(): + """ + Send email to a distribution group from the database. + + Request JSON: + { + "group_id": 1, // or "group_name": "IT Support" + "subject": "Email subject", + "message": "Email body", + "html": false + } + """ + if not MYSQL_AVAILABLE: + return jsonify({'success': False, 'error': 'MySQL connector not installed'}), 500 + + try: + data = request.get_json() + + if not data: + return jsonify({'success': False, 'error': 'No JSON data provided'}), 400 + + group_id = data.get('group_id') + group_name = data.get('group_name') + subject = data.get('subject', '') + message = data.get('message', '') + html = data.get('html', False) + + if not group_id and not group_name: + return jsonify({'success': False, 'error': 'Missing group_id or group_name'}), 400 + + # Look up distribution group + conn = get_db_connection() + if not conn: + return jsonify({'success': False, 'error': 'Database connection failed'}), 500 + + try: + cursor = conn.cursor(dictionary=True) + + if group_id: + cursor.execute( + "SELECT email FROM distributiongroups WHERE distributiongroupid = %s AND isactive = 1", + (group_id,) + ) + else: + cursor.execute( + "SELECT email FROM distributiongroups WHERE name = %s AND isactive = 1", + (group_name,) + ) + + result = cursor.fetchone() + + if not result: + return jsonify({'success': False, 'error': 'Distribution group not found or inactive'}), 404 + + to_address = result['email'] + + finally: + cursor.close() + conn.close() + + success, error = send_email(to_address, subject, message, html) + + if success: + return jsonify({'success': True, 'sent_to': to_address}) + else: + return jsonify({'success': False, 'error': error}), 500 + + except Exception as e: + logger.error(f"Error in send-to-group endpoint: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + +@app.route('/api/groups', methods=['GET']) +@require_api_key +def list_distribution_groups(): + """List all active distribution groups.""" + if not MYSQL_AVAILABLE: + return jsonify({'success': False, 'error': 'MySQL connector not installed'}), 500 + + conn = get_db_connection() + if not conn: + return jsonify({'success': False, 'error': 'Database connection failed'}), 500 + + try: + cursor = conn.cursor(dictionary=True) + cursor.execute( + "SELECT distributiongroupid, name, email FROM distributiongroups WHERE isactive = 1 ORDER BY name" + ) + groups = cursor.fetchall() + return jsonify({'success': True, 'groups': groups}) + except Exception as e: + logger.error(f"Error listing groups: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + finally: + cursor.close() + conn.close() + + +if __name__ == '__main__': + # Check configuration + if not SMTP_HOST: + logger.warning("SMTP_HOST not set - email sending will fail") + if not SMTP_USER: + logger.warning("SMTP_USER not set - email sending will fail") + if not SMTP_PASS: + logger.warning("SMTP_PASS not set - email sending will fail") + + logger.info(f"Starting Email API on port 5000") + logger.info(f"SMTP: {SMTP_HOST}:{SMTP_PORT}") + + app.run(host='0.0.0.0', port=5000, debug=False) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..cd1aaaf --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +flask>=2.0.0 +mysql-connector-python>=8.0.0 diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..a549834 --- /dev/null +++ b/start.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Start the Email API service +# +# Required environment variables: +# SMTP_HOST - SMTP server hostname +# SMTP_USER - SMTP username +# SMTP_PASS - SMTP password +# +# Optional: +# SMTP_PORT - SMTP port (default: 587) +# SMTP_FROM - Default from address +# API_KEY - API key for authentication +# DB_HOST - MySQL host (default: 192.168.122.1) +# DB_USER - MySQL user (default: 570005354) +# DB_PASS - MySQL password + +cd "$(dirname "$0")" + +# Check for virtual environment +if [ ! -d "venv" ]; then + echo "Creating virtual environment..." + python3 -m venv venv + source venv/bin/activate + pip install -r requirements.txt +else + source venv/bin/activate +fi + +# Start the API +echo "Starting Email API on port 5000..." +python app.py