| """ |
| Emailpadu Server V2 - 2026 Edition |
| Dikemaskini untuk menyokong Hugging Face Hub v1.0+ dan nh3 v0.3+ |
| Gunakan sekurang-kurangnya Python 3.12+ |
| """ |
|
|
| import os |
| import logging |
| import re |
| from uuid import uuid4 |
| from datetime import datetime, timezone |
| from email.utils import parseaddr |
| from html import unescape |
| from concurrent.futures import ThreadPoolExecutor |
|
|
| import requests |
| import nh3 |
| from flask import Flask, request, jsonify, Response |
| from huggingface_hub import HfApi, hf_hub_download |
|
|
| |
| |
| |
|
|
| app = Flask(__name__) |
| logging.basicConfig( |
| level=logging.INFO, |
| format='%(asctime)s [%(levelname)s] %(message)s' |
| ) |
| logger = logging.getLogger(__name__) |
|
|
| |
| executor = ThreadPoolExecutor(max_workers=5) |
|
|
| |
| DISCORD_WEBHOOK_URL = os.getenv("DISCORD_WEBHOOK_URL") |
| HF_TOKEN = os.getenv("HF_TOKEN") |
| DATASET_ID = os.getenv("DATASET_ID") |
| WEBHOOK_USER = os.getenv("WEBHOOK_USER") |
| WEBHOOK_PASS = os.getenv("WEBHOOK_PASS") |
|
|
| |
| api = HfApi(token=HF_TOKEN) |
|
|
| |
| |
| |
|
|
| def truncate(value: str | None, max_len: int) -> str: |
| value = str(value or "") |
| return value if len(value) <= max_len else value[:max_len - 1] + "β¦" |
|
|
| def clean_possible_link(link: str | None) -> str | None: |
| if not link: return None |
| link = unescape(str(link).strip()) |
| link = link.replace("=\r\n", "").replace("=\n", "").replace("=3D", "=") |
| return link.strip(' \t\r\n<>"\'.,()[]') |
|
|
| def sanitize_email_html(html_content: str | None) -> str: |
| """Guna pustaka nh3 (v0.3.x ke atas) untuk hapus tag XSS. 20x ganda lebih pantas.""" |
| if not html_content: |
| return "<p>(No HTML Content)</p>" |
| return nh3.clean(str(html_content)) |
|
|
| def html_to_text(html: str | None) -> str: |
| """Tukar HTML ke plain text untuk description Discord menggunakan enjin nh3.""" |
| if not html: return "" |
| text = nh3.clean(str(html), tags=set()) |
| text = unescape(text) |
| text = re.sub(r"[ \t]+", " ", text) |
| text = re.sub(r"\n{3,}", "\n\n", text) |
| return text.strip() |
|
|
| def extract_sender(data: dict) -> str: |
| """Ekstrak maklumat penghantar dari JSON CloudMailin.""" |
| headers = data.get("headers") or {} |
| envelope = data.get("envelope") or {} |
|
|
| from_header = ( |
| headers.get("From") or |
| headers.get("from") or |
| envelope.get("from") or |
| data.get("from") |
| ) |
| |
| if not from_header: |
| return "Unknown Sender" |
|
|
| if isinstance(from_header, list): |
| from_header = from_header[0] |
|
|
| name, addr = parseaddr(str(from_header)) |
| if addr: |
| return f"{name} <{addr}>" if name else addr |
| return str(from_header) |
|
|
| def extract_important_link(plain_body: str = "", html_body: str = "", reply_plain: str = "") -> str | None: |
| """Ekstrak pautan pengesahan (verify) daripada kandungan emel.""" |
| action_words = {"verify", "confirm", "activate", "reset", "password", "auth", "magic"} |
| bad_words = {"unsubscribe", "privacy", "help", "redditinc", "twitter", "facebook"} |
| |
| candidates: list[tuple[int, str]] =[] |
|
|
| def add_candidate(url: str, score: int = 0): |
| clean = clean_possible_link(url) |
| if not clean: return |
| lower = clean.lower() |
| if any(bad in lower for bad in bad_words): score -= 30 |
| if any(word in lower for word in action_words): score += 50 |
| candidates.append((score, clean)) |
|
|
| |
| for source in[reply_plain, plain_body]: |
| if not source: continue |
| found = re.findall(r'https?://[^\s<>"\']+', str(source)) |
| for url in found: add_candidate(url, 10) |
|
|
| |
| if html_body: |
| anchors = re.findall(r'<a\b[^>]*href\s*=\s*["\'](https?://[^"\']+)["\'][^>]*>(.*?)</a>', str(html_body), re.I|re.S) |
| for href, inner in anchors: |
| anchor_text = html_to_text(inner).lower() |
| score = 20 |
| if any(word in anchor_text for word in action_words): score += 100 |
| add_candidate(href, score) |
|
|
| if not candidates: return None |
| candidates.sort(key=lambda x: x[0], reverse=True) |
| return candidates[0][1] if candidates[0][0] >= 0 else None |
|
|
| |
| |
| |
|
|
| def process_email_background(data: dict, host_url: str): |
| """ |
| Fungsi pemprosesan berat yang kini berjalan secara stabil menggunakan ThreadPoolExecutor. |
| """ |
| try: |
| headers = data.get("headers") if isinstance(data.get("headers"), dict) else {} |
| subject = str(headers.get("subject") or "No Subject") |
| sender = extract_sender(data) |
| plain_body = str(data.get("plain") or "") |
| html_body = str(data.get("html") or "") |
| reply_plain = str(data.get("reply_plain") or "") |
| |
| body_clean = (plain_body or reply_plain or html_to_text(html_body) or "Kosong").split("On ")[0].strip() |
| important_link = extract_important_link(plain_body, html_body, reply_plain) |
|
|
| |
| html_preview_url = None |
| if html_body and HF_TOKEN and DATASET_ID: |
| html_id = uuid4().hex[:16] |
| try: |
| safe_html = sanitize_email_html(html_body) |
| api.upload_file( |
| path_or_fileobj=safe_html.encode("utf-8"), |
| path_in_repo=f"html/{html_id}.html", |
| repo_id=DATASET_ID, |
| repo_type="dataset", |
| ) |
| html_preview_url = f"{host_url.rstrip('/')}/html/{html_id}" |
| except Exception as e: |
| logger.error(f"HF HTML Upload Fail: {e}") |
|
|
| |
| if HF_TOKEN and DATASET_ID: |
| ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S") |
| filename = f"logs/email_{ts}_{uuid4().hex[:5]}.txt" |
| log_content = f"From: {sender}\nSubject: {subject}\nLink: {important_link}\n\nPreview: {html_preview_url}\n\n---BODY---\n{body_clean}" |
| try: |
| api.upload_file( |
| path_or_fileobj=log_content.encode("utf-8"), |
| path_in_repo=filename, |
| repo_id=DATASET_ID, |
| repo_type="dataset", |
| ) |
| except Exception as e: |
| logger.error(f"HF Log Upload Fail: {e}") |
|
|
| |
| if DISCORD_WEBHOOK_URL: |
| embed = { |
| "title": f"π© {truncate(subject, 250)}", |
| "description": truncate(body_clean, 1700), |
| "color": 3447003, |
| "timestamp": datetime.now(timezone.utc).isoformat(), |
| "fields":[{"name": "π§ Dari", "value": truncate(sender, 500)}] |
| } |
| |
| content_parts = [] |
| if important_link: |
| embed["url"] = important_link |
| embed["fields"].append({"name": "π Link Penting", "value": important_link}) |
| content_parts.append(f"π <{important_link}>") |
| |
| if html_preview_url: |
| embed["fields"].append({"name": "πΌοΈ Preview Email", "value": html_preview_url}) |
| content_parts.append(f"πΌοΈ <{html_preview_url}>") |
|
|
| payload = { |
| "embeds": [embed], |
| |
| |
| } |
| if content_parts: |
| payload["content"] = "\n".join(content_parts) |
|
|
| |
| requests.post(DISCORD_WEBHOOK_URL, json=payload, timeout=10) |
|
|
| logger.info(f"Selesai memproses e-mel daripada {sender}") |
|
|
| except Exception: |
| logger.exception("Ralat kritikal dalam tugasan latar") |
|
|
| |
| |
| |
|
|
| @app.route("/", methods=["GET"]) |
| def home(): |
| return "Emailpadu Server V2 (2026) is Running β
", 200 |
|
|
| @app.route("/health", methods=["GET"]) |
| def health(): |
| return jsonify(status="OK", timestamp=datetime.now(timezone.utc).isoformat()), 200 |
|
|
| @app.route("/html/<doc_id>", methods=["GET"]) |
| def view_html(doc_id: str): |
| """Paparkan HTML yang telah dibersihkan secara selamat (XSS safe).""" |
| if not re.fullmatch(r"[a-f0-9]{16}", doc_id): |
| return "Invalid ID", 400 |
| |
| try: |
| file_path = hf_hub_download( |
| repo_id=DATASET_ID, |
| repo_type="dataset", |
| filename=f"html/{doc_id}.html", |
| token=HF_TOKEN |
| ) |
| with open(file_path, "r", encoding="utf-8") as f: |
| content = f.read() |
| return Response(content, mimetype="text/html") |
| except Exception as e: |
| logger.warning(f"File not found: {e}") |
| return "Email not found", 404 |
|
|
| @app.route("/webhook", methods=["POST"]) |
| def handle_webhook(): |
| |
| auth = request.authorization |
| if WEBHOOK_USER and WEBHOOK_PASS: |
| if not auth or auth.username != WEBHOOK_USER or auth.password != WEBHOOK_PASS: |
| return jsonify(error="Unauthorized"), 401 |
|
|
| |
| data = request.get_json(silent=True) |
| if not data: |
| return jsonify(error="Invalid JSON"), 400 |
|
|
| |
| executor.submit(process_email_background, data, request.host_url) |
|
|
| |
| return jsonify(status="Accepted", message="Tugasan dimasukkan ke Executor"), 202 |
|
|
| |
| |
| |
|
|
| if __name__ == "__main__": |
| app.run(host="0.0.0.0", port=7860) |