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 %}
Активные клиенты
{% for env in active_envs %}
-
{% endfor %}
Архив
{% for env in archived_envs %}
-
{% endfor %}
'''
ADMIN_TEMPLATE = '''
Настройки клиента
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
{{ message }}
{% endfor %}
{% endif %}
{% endwith %}
'''
MAIL_TEMPLATE = '''
{{ settings.organization_name }}
Отправка письма...
Написать
Отправленные
Контакты
Отправленные
{% for email in emails|reverse %}
{{ email.subject }}
{{ email.body[:100] }}...
{% if email.attachments %}
Вложений: {{ email.attachments|length }}
{% endif %}
{% 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)