File size: 11,083 Bytes
d63f324 1d047ce 26944da 065c62e 26944da a866cec b999f01 d63f324 26944da 1d047ce d63f324 67e6d6c ed2d335 67e6d6c 1d047ce ed2d335 26944da d63f324 ed2d335 1d047ce 67e6d6c 26944da d63f324 26944da a866cec ed2d335 a866cec d63f324 26944da d63f324 ed2d335 67e6d6c d63f324 ed2d335 67e6d6c d63f324 ed2d335 d63f324 67e6d6c d63f324 ed2d335 b999f01 ed2d335 065c62e ed2d335 065c62e d63f324 ed2d335 d63f324 9532261 d63f324 9532261 ed2d335 9532261 ed2d335 b999f01 d63f324 ed2d335 9532261 d63f324 ed2d335 9532261 ed2d335 9532261 ed2d335 065c62e ed2d335 065c62e d63f324 065c62e ed2d335 d63f324 ed2d335 d63f324 ed2d335 d63f324 ed2d335 d63f324 ed2d335 d63f324 ed2d335 d63f324 ed2d335 d63f324 26944da ed2d335 d63f324 26944da a866cec ed2d335 a866cec ed2d335 a866cec 67e6d6c d63f324 67e6d6c ed2d335 d63f324 ed2d335 67e6d6c 26944da ed2d335 d63f324 ed2d335 26944da ed2d335 26944da d63f324 26944da d63f324 26944da d63f324 0ffddf8 a866cec ed2d335 a866cec 065c62e 1d047ce 26944da | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 | """
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 # Pustaka sanitasi HTML standard 2026 (menggantikan bleach)
from flask import Flask, request, jsonify, Response
from huggingface_hub import HfApi, hf_hub_download
# βββββββββββββββββββββββββββββββββββββββββ
# SETUP & LOGGING
# βββββββββββββββββββββββββββββββββββββββββ
app = Flask(__name__)
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s'
)
logger = logging.getLogger(__name__)
# Executor untuk tugasan latar (Background Tasks)
executor = ThreadPoolExecutor(max_workers=5)
# Environment Variables
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 HF v1.0+ kini dikuasakan oleh `httpx` untuk operasi pantas
api = HfApi(token=HF_TOKEN)
# βββββββββββββββββββββββββββββββββββββββββ
# HELPERS (SANITIZATION & EXTRACTION)
# βββββββββββββββββββββββββββββββββββββββββ
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()) # Buang semua tag HTML
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))
# Carian di dalam teks biasa
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)
# Carian di dalam tag HTML <a>
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
# βββββββββββββββββββββββββββββββββββββββββ
# CORE LOGIC (BACKGROUND TASKS)
# βββββββββββββββββββββββββββββββββββββββββ
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)
# 1. Simpan HTML ke HF Dataset (v1.0 API) & Jana Preview URL
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}")
# 2. Simpan Log Teks ke HF (v1.0 API)
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}")
# 3. Hantar Notifikasi ke Discord (Webhooks 2026 Layout)
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],
# Anda boleh tambah "flags": 4096 di sini untuk ciri 'Silent Message' baharu Discord
# "flags": 4096
}
if content_parts:
payload["content"] = "\n".join(content_parts)
# Timeout di-set pada 10 untuk menghalang sambungan HTTP yang tergantung
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")
# βββββββββββββββββββββββββββββββββββββββββ
# ROUTES
# βββββββββββββββββββββββββββββββββββββββββ
@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():
# 1. Auth Check (Standard Werkzeug 3.x+)
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
# 2. Parse Data
data = request.get_json(silent=True)
if not data:
return jsonify(error="Invalid JSON"), 400
# 3. Masukkan Tugasan Dalam Pool Moden
executor.submit(process_email_background, data, request.host_url)
# 4. Beri Maklum Balas Pantas (202 Accepted)
return jsonify(status="Accepted", message="Tugasan dimasukkan ke Executor"), 202
# βββββββββββββββββββββββββββββββββββββββββ
# RUN (Gunicorn will use 'app')
# βββββββββββββββββββββββββββββββββββββββββ
if __name__ == "__main__":
app.run(host="0.0.0.0", port=7860) |