import os import json import threading import time from datetime import datetime, timedelta from uuid import uuid4 import random import string import tempfile import smtplib from email.message import EmailMessage import mimetypes import dns.resolver from flask import Flask, render_template_string, request, redirect, url_for, flash, jsonify, session, Response from huggingface_hub import HfApi, hf_hub_download from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError from dotenv import load_dotenv import requests load_dotenv() app = Flask(__name__) app.secret_key = 'super_secret_key_mail_app_123_direct_smtp' app.config.update( SESSION_COOKIE_SAMESITE='None', SESSION_COOKIE_SECURE=True, PERMANENT_SESSION_LIFETIME=timedelta(days=30) ) DATA_FILE = 'data.json' SYNC_FILES = [DATA_FILE] REPO_ID = os.getenv("REPO_ID", "Kgshop/mail") HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") data_lock = threading.Lock() def get_almaty_time(): return (datetime.utcnow() + timedelta(hours=5)).strftime('%Y-%m-%d %H:%M:%S') def download_db_from_hf(specific_file=None, retries=3, delay=5): token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE files_to_download = [specific_file] if specific_file else SYNC_FILES all_successful = True for file_name in files_to_download: success = False for attempt in range(retries + 1): try: hf_hub_download( repo_id=REPO_ID, filename=file_name, repo_type="dataset", token=token_to_use, local_dir=".", local_dir_use_symlinks=False, force_download=True, resume_download=False ) success = True break except RepositoryNotFoundError: return False except HfHubHTTPError as e: if e.response.status_code == 404: if attempt == 0 and not os.path.exists(file_name): try: if file_name == DATA_FILE: with data_lock: fd, temp_path = tempfile.mkstemp(dir=os.path.dirname(os.path.abspath(DATA_FILE)) or '.', text=True) with os.fdopen(fd, 'w', encoding='utf-8') as f: json.dump({}, f) os.replace(temp_path, DATA_FILE) except Exception: pass success = False break except requests.exceptions.RequestException: pass except Exception: pass if attempt < retries: time.sleep(delay) if not success: all_successful = False return all_successful def upload_db_to_hf(specific_file=None): if not HF_TOKEN_WRITE: return try: api = HfApi() files_to_upload = [specific_file] if specific_file else SYNC_FILES for file_name in files_to_upload: if os.path.exists(file_name): try: api.upload_file( path_or_fileobj=file_name, path_in_repo=file_name, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"Sync {file_name} {get_almaty_time()}" ) except Exception: pass except Exception: pass def upload_attachment(file_obj, repo_path): if not HF_TOKEN_WRITE: return None try: ext = os.path.splitext(file_obj.filename)[1].lower() filename = f"{uuid4().hex}{ext}" fd, temp_path = tempfile.mkstemp() with os.fdopen(fd, 'wb') as f: f.write(file_obj.read()) file_obj.seek(0) api = HfApi() api.upload_file( path_or_fileobj=temp_path, path_in_repo=f"{repo_path}/{filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE ) if os.path.exists(temp_path): os.remove(temp_path) return filename except Exception: return None def periodic_backup(): while True: time.sleep(1800) upload_db_to_hf() def load_data(): with data_lock: data = {} try: with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file) if not isinstance(data, dict): raise FileNotFoundError except (FileNotFoundError, json.JSONDecodeError): if download_db_from_hf(specific_file=DATA_FILE): try: with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file) except Exception: data = {} else: data = {} changed = False for env_id, env_data in data.items(): if 'emails' not in env_data: env_data['emails'] = []; changed = True if 'contacts' not in env_data: env_data['contacts'] = []; changed = True if 'settings' not in env_data: env_data['settings'] = {} changed = True settings = env_data['settings'] if 'organization_name' not in settings: settings['organization_name'] = f'Mail Client {env_id}'; changed = True if 'admin_password_enabled' not in settings: settings['admin_password_enabled'] = False; changed = True if 'admin_password' not in settings: settings['admin_password'] = ''; changed = True if 'smtp_user' not in settings: settings['smtp_user'] = ''; changed = True if 'sender_name' not in settings: settings['sender_name'] = ''; changed = True if 'is_deleted' not in settings: settings['is_deleted'] = False; changed = True if 'smtp_host' in settings: del settings['smtp_host']; changed = True if 'smtp_port' in settings: del settings['smtp_port']; changed = True if 'smtp_pass' in settings: del settings['smtp_pass']; changed = True if changed or not os.path.exists(DATA_FILE): try: fd, temp_path = tempfile.mkstemp(dir=os.path.dirname(os.path.abspath(DATA_FILE)) or '.', text=True) with os.fdopen(fd, 'w', encoding='utf-8') as f: json.dump(data, f) os.replace(temp_path, DATA_FILE) except Exception: pass return data def save_data(data): try: if not isinstance(data, dict): return with data_lock: fd, temp_path = tempfile.mkstemp(dir=os.path.dirname(os.path.abspath(DATA_FILE)) or '.', text=True) with os.fdopen(fd, 'w', encoding='utf-8') as file: json.dump(data, file, ensure_ascii=False, indent=4) os.replace(temp_path, DATA_FILE) upload_db_to_hf(specific_file=DATA_FILE) except Exception: pass def get_env_data(env_id): all_data = load_data() if env_id not in all_data: all_data[env_id] = { 'emails': [], 'contacts': [], 'settings': { 'organization_name': f'Mail Client {env_id}', 'admin_password_enabled': False, 'admin_password': '', 'smtp_user': '', 'sender_name': '', 'is_deleted': False } } save_data(all_data) return all_data[env_id] def save_env_data(env_id, env_data): all_data = load_data() all_data[env_id] = env_data save_data(all_data) @app.before_request def check_deleted_env(): if request.endpoint and request.view_args and 'env_id' in request.view_args: env_id = request.view_args['env_id'] if env_id in ['admhosto']: return all_data = load_data() if env_id in all_data and all_data[env_id].get('settings', {}).get('is_deleted', False): return "

Клиент отключен

", 403 def send_email_direct(settings, to_email, subject, body, files): msg = EmailMessage() msg['Subject'] = subject sender_name = settings.get('sender_name', '').strip() smtp_user = settings.get('smtp_user', '').strip() if not smtp_user: raise Exception("Email отправителя не настроен в админ панели.") if sender_name: msg['From'] = f"{sender_name} <{smtp_user}>" else: msg['From'] = smtp_user msg['To'] = to_email msg.set_content(body) for f in files: if f and f.filename: file_data = f.read() ctype, encoding = mimetypes.guess_type(f.filename) if ctype is None or encoding is not None: ctype = 'application/octet-stream' maintype, subtype = ctype.split('/', 1) msg.add_attachment(file_data, maintype=maintype, subtype=subtype, filename=f.filename) f.seek(0) try: domain = to_email.split('@')[1] records = dns.resolver.resolve(domain, 'MX') mx_record = sorted(records, key=lambda x: x.preference)[0].exchange.to_text() except Exception as e: raise Exception(f"Не удалось определить MX сервер для домена {to_email.split('@')[-1]}: {str(e)}") try: with smtplib.SMTP(mx_record, 25, timeout=15) as server: server.ehlo() if server.has_extn('STARTTLS'): server.starttls() server.ehlo() server.send_message(msg) except Exception as e: raise Exception(f"Ошибка при доставке письма на сервер {mx_record}: {str(e)}") LOGIN_TEMPLATE = ''' Вход

Вход

{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endif %} {% endwith %}
''' ADMHOSTO_TEMPLATE = ''' Управление клиентами

Почтовые клиенты

{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endif %} {% endwith %}

Активные клиенты

Архив

''' ADMIN_TEMPLATE = ''' Настройки клиента

Настройки ({{ env_id }})

К почте {% if settings.admin_password_enabled %} Выход {% endif %}
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endif %} {% endwith %}

Основные настройки

Настройки отправителя (Встроенный SMTP)

Безопасность

''' MAIL_TEMPLATE = ''' {{ settings.organization_name }}
Отправка письма...

{{ settings.organization_name }}

{% if settings.admin_password_enabled %} {% endif %}

Новое письмо

Отправленные

Адресная книга

{% for contact in contacts %}
{{ contact.name }} {{ contact.email }}
{% else %}
Адресная книга пуста
{% endfor %}
''' @app.route('/') def index(): return redirect(url_for('admhosto')) @app.route('/admhosto', methods=['GET']) def admhosto(): data = load_data() active_envs = [] archived_envs = [] for env_id, env_data in data.items(): settings = env_data.get('settings', {}) org_name = settings.get("organization_name", f"Client {env_id}") env_info = { "id": env_id, "org_name": org_name, "pwd_enabled": settings.get("admin_password_enabled", False), "password": settings.get("admin_password", "") } if settings.get('is_deleted', False): archived_envs.append(env_info) else: active_envs.append(env_info) active_envs.sort(key=lambda x: x['id']) archived_envs.sort(key=lambda x: x['id']) return render_template_string(ADMHOSTO_TEMPLATE, active_envs=active_envs, archived_envs=archived_envs) @app.route('/admhosto/create', methods=['POST']) def create_environment(): all_data = load_data() while True: new_id = ''.join(random.choices(string.digits, k=6)) if new_id not in all_data: break all_data[new_id] = { 'emails': [], 'contacts': [], 'settings': { "organization_name": f"Mail Client {new_id}", "admin_password_enabled": False, "admin_password": "", "smtp_user": "", "sender_name": "", "is_deleted": False } } save_data(all_data) flash(f'Новый клиент с ID {new_id} успешно создан.', 'success') return redirect(url_for('admhosto')) @app.route('/admhosto/update_pwd/', methods=['POST']) def update_env_pwd(env_id): all_data = load_data() if env_id in all_data: pwd_enabled = 'pwd_enabled' in request.form password = request.form.get('password', '').strip() all_data[env_id]['settings']['admin_password_enabled'] = pwd_enabled all_data[env_id]['settings']['admin_password'] = password save_data(all_data) flash(f'Пароль для клиента {env_id} обновлен.', 'success') return redirect(url_for('admhosto')) @app.route('/admhosto/delete/', methods=['POST']) def delete_environment(env_id): all_data = load_data() if env_id in all_data: all_data[env_id]['settings']['is_deleted'] = True save_data(all_data) flash(f'Клиент {env_id} отключен.', 'success') return redirect(url_for('admhosto')) @app.route('/admhosto/restore/', methods=['POST']) def restore_environment(env_id): all_data = load_data() if env_id in all_data: all_data[env_id]['settings']['is_deleted'] = False save_data(all_data) flash(f'Клиент {env_id} восстановлен.', 'success') return redirect(url_for('admhosto')) @app.route('/admhosto/hard_delete/', methods=['POST']) def hard_delete_environment(env_id): all_data = load_data() if env_id in all_data: del all_data[env_id] save_data(all_data) flash(f'Клиент {env_id} удален окончательно.', 'success') return redirect(url_for('admhosto')) @app.route('//login', methods=['GET', 'POST']) def admin_login(env_id): data = get_env_data(env_id) settings = data.get('settings', {}) if not settings.get('admin_password_enabled'): return redirect(url_for('mail_client', env_id=env_id)) if request.method == 'POST': pwd = request.form.get('password', '') if pwd == settings.get('admin_password', ''): session.permanent = True session[f'admin_auth_{env_id}'] = True return redirect(url_for('mail_client', env_id=env_id)) else: flash('Неверный пароль', 'error') return render_template_string(LOGIN_TEMPLATE, env_id=env_id) @app.route('//logout') def admin_logout(env_id): session.pop(f'admin_auth_{env_id}', None) return redirect(url_for('admin_login', env_id=env_id)) @app.route('//admin', methods=['GET', 'POST']) def admin(env_id): data = get_env_data(env_id) settings = data.get('settings', {}) if settings.get('admin_password_enabled') and not session.get(f'admin_auth_{env_id}'): return redirect(url_for('admin_login', env_id=env_id)) if request.method == 'POST': if request.form.get('action') == 'update_settings': settings['organization_name'] = request.form.get('organization_name', '').strip() settings['sender_name'] = request.form.get('sender_name', '').strip() settings['smtp_user'] = request.form.get('smtp_user', '').strip() settings['admin_password_enabled'] = 'admin_password_enabled' in request.form settings['admin_password'] = request.form.get('admin_password', '').strip() data['settings'] = settings save_env_data(env_id, data) flash('Настройки сохранены', 'success') return redirect(url_for('admin', env_id=env_id)) return render_template_string(ADMIN_TEMPLATE, env_id=env_id, settings=settings) @app.route('//mail', methods=['GET']) def mail_client(env_id): data = get_env_data(env_id) settings = data.get('settings', {}) if settings.get('admin_password_enabled') and not session.get(f'admin_auth_{env_id}'): return redirect(url_for('admin_login', env_id=env_id)) emails = data.get('emails', []) contacts = data.get('contacts', []) return render_template_string( MAIL_TEMPLATE, env_id=env_id, settings=settings, emails=emails, contacts=contacts, emails_json=json.dumps(emails), repo_id=REPO_ID ) @app.route('//contact_action', methods=['POST']) def contact_action(env_id): data = get_env_data(env_id) settings = data.get('settings', {}) if settings.get('admin_password_enabled') and not session.get(f'admin_auth_{env_id}'): return redirect(url_for('admin_login', env_id=env_id)) action = request.form.get('action') contacts = data.get('contacts', []) if action == 'add': name = request.form.get('name', '').strip() email = request.form.get('email', '').strip() if name and email: contacts.append({'id': uuid4().hex, 'name': name, 'email': email}) data['contacts'] = contacts save_env_data(env_id, data) elif action == 'delete': cid = request.form.get('contact_id') data['contacts'] = [c for c in contacts if c['id'] != cid] save_env_data(env_id, data) return redirect(url_for('mail_client', env_id=env_id)) @app.route('//send_mail', methods=['POST']) def send_mail_route(env_id): data = get_env_data(env_id) settings = data.get('settings', {}) if settings.get('admin_password_enabled') and not session.get(f'admin_auth_{env_id}'): return jsonify({'success': False, 'error': 'Unauthorized'}), 401 to_email = request.form.get('to_email', '').strip() subject = request.form.get('subject', '').strip() body = request.form.get('body', '').strip() files = request.files.getlist('attachments') if not settings.get('smtp_user'): return jsonify({'success': False, 'error': 'Email отправителя не настроен в админ панели.'}), 400 try: send_email_direct(settings, to_email, subject, body, files) uploaded_attachments = [] for f in files: if f and f.filename: f.seek(0) filename = upload_attachment(f, 'attachments') if filename: uploaded_attachments.append({'original': f.filename, 'filename': filename}) email_record = { 'id': uuid4().hex, 'date': get_almaty_time(), 'to': to_email, 'subject': subject, 'body': body, 'attachments': uploaded_attachments } data.setdefault('emails', []).append(email_record) save_env_data(env_id, data) return jsonify({'success': True}) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 if __name__ == '__main__': download_db_from_hf() load_data() if HF_TOKEN_WRITE: threading.Thread(target=periodic_backup, daemon=True).start() port = int(os.environ.get('PORT', 7860)) app.run(host='0.0.0.0', port=port)