emailproxy-ui.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. #!/usr/bin/env python3
  2. import os
  3. import sys
  4. import subprocess
  5. import tempfile
  6. import shutil
  7. import time
  8. import socket
  9. import json
  10. import re
  11. # load le config file (edit this to your needs)
  12. CONFIG_FILE = "/home/crt/helper-emailproxy/config/config.env"
  13. def load_config():
  14. """Load configuration from the environment file."""
  15. config = {}
  16. with open(CONFIG_FILE, "r") as f:
  17. for line in f:
  18. # dont read comments or empty lines perhaps
  19. if line.strip().startswith("#") or "=" not in line:
  20. continue
  21. key, value = line.strip().split("=", 1)
  22. config[key.strip()] = value.strip()
  23. return config
  24. def create_temp_config(auth_base_config, imap_port, smtp_port):
  25. """Create a temporary configuration file for the session."""
  26. temp_dir = tempfile.mkdtemp()
  27. temp_config_path = os.path.join(temp_dir, "emailproxy-auth-temp.config")
  28. with open(auth_base_config, "r") as base_config, open(temp_config_path, "w") as temp_config:
  29. for line in base_config:
  30. temp_config.write(
  31. line.replace("IMAP-2993", f"IMAP-{imap_port}")
  32. .replace("SMTP-2465", f"SMTP-{smtp_port}")
  33. )
  34. return temp_config_path, temp_dir
  35. def launch_emailproxy(temp_config_path, emailproxy_exec):
  36. """Launch the emailproxy with the temporary configuration."""
  37. # launch a temporary emailproxy process for getting 2fa piss token
  38. config = load_config()
  39. log_file = config.get("EMAILPROXY_LOG_FILE", "/tmp/emailproxy.log")
  40. print(f"[+] Starting temporary emailproxy, writing output to {log_file}")
  41. working_dir = os.path.dirname(emailproxy_exec) if "/" in emailproxy_exec else os.getcwd()
  42. cmd = f"stdbuf -oL {emailproxy_exec} --config-file '{temp_config_path}' --no-gui --external-auth"
  43. # this is a security risk, but we need to write the piss token to one file for php to read it because i am dumb
  44. with open(log_file, "a") as lf:
  45. proc = subprocess.Popen(
  46. cmd,
  47. shell=True,
  48. cwd=working_dir,
  49. stdin=subprocess.PIPE,
  50. stdout=lf, # thanks ai for fixing my crap
  51. stderr=lf, # same here
  52. text=True
  53. )
  54. # return the process
  55. return proc
  56. def validate_new_user(temp_config_path, email):
  57. """Check if a new user was successfully added to the temporary config."""
  58. if not os.path.exists(temp_config_path):
  59. return False
  60. with open(temp_config_path, "r") as temp_config:
  61. return f"[{email}]" in temp_config.read()
  62. def merge_configs(main_config_path, temp_config_path):
  63. """Merge the new user configuration into the main config."""
  64. with open(main_config_path, "r") as main_config:
  65. main_config_content = main_config.read()
  66. # ocd newline check
  67. ensure_newline = "" if main_config_content.endswith("\n") else "\n"
  68. with open(temp_config_path, "r") as temp_config:
  69. temp_config_content = temp_config.read()
  70. # get new successful users from temp config
  71. user_sections = []
  72. current_section = []
  73. in_user_section = False
  74. for line in temp_config_content.splitlines():
  75. if line.startswith("[") and "@" in line:
  76. if current_section: # i am bad at python
  77. user_sections.append("\n".join(current_section))
  78. current_section = []
  79. in_user_section = True
  80. current_section.append(line)
  81. elif line.startswith("["):
  82. in_user_section = False
  83. current_section = []
  84. elif in_user_section:
  85. current_section.append(line)
  86. # add the last user section because thats 99% of the time what we want
  87. if current_section and in_user_section:
  88. user_sections.append("\n".join(current_section))
  89. # maybe dont create conflicts yk (send help)
  90. merged_any = False
  91. for user_section in user_sections:
  92. user_email = user_section.split("[", 1)[1].split("]", 1)[0]
  93. if f"[{user_email}]" in main_config_content:
  94. print(f"[!] User {user_email} already exists in the main config. Skipping merge.")
  95. continue
  96. # append new user and ENSURE NEW LINE I HATE GRUSSIGI CONFIG !!!
  97. with open(main_config_path, "a") as main_config:
  98. main_config.write(f"{ensure_newline}\n{user_section}\n")
  99. # mmm emailproxy deletes config and replaces with its cached one when stopped so uhm yeah this is here for that
  100. main_config.flush()
  101. os.fsync(main_config.fileno())
  102. print(f"[+] Successfully added {user_email} to the main config.")
  103. merged_any = True
  104. return merged_any
  105. def restart_main_proxy(emailproxy_exec, emailproxy_config, working_dir, log_file, debug):
  106. """Restart the main emailproxy process."""
  107. print("[+] Stopping the current emailproxy process...")
  108. # kill it with fire
  109. kill_cmd = f"pkill -f '{emailproxy_exec} --config-file {emailproxy_config}'"
  110. subprocess.run(kill_cmd, shell=True)
  111. # make sure the process burnt alive and died
  112. time.sleep(2)
  113. # return before starting the new one
  114. return working_dir, log_file, debug
  115. # thenks ai for fixing !
  116. def handle_new_user(email, imap_port, smtp_port, emailproxy_exec, auth_base_config):
  117. """Handle a new user authentication request."""
  118. temp_config_path, temp_dir = create_temp_config(auth_base_config, imap_port, smtp_port)
  119. try:
  120. # launch temp emailproxy
  121. print(f"[+] Launching temporary emailproxy for {email} on ports IMAP:{imap_port}, SMTP:{smtp_port}")
  122. temp_proxy_proc = launch_emailproxy(temp_config_path, emailproxy_exec)
  123. # gibs returned values for php
  124. return temp_config_path, temp_dir, temp_proxy_proc
  125. except Exception as e:
  126. print(f"[!] Error launching temporary emailproxy: {e}")
  127. shutil.rmtree(temp_dir)
  128. return None, None, None
  129. def start_command_server():
  130. """Start a socket server to receive commands from PHP."""
  131. server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  132. server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  133. server_socket.bind(('localhost', COMMAND_PORT))
  134. server_socket.listen(5)
  135. return server_socket
  136. # security risk most likely <3 i love it
  137. def remove_user_from_config(config_file, email):
  138. """Remove a user section from the config file."""
  139. if not os.path.exists(config_file):
  140. return False
  141. with open(config_file, "r") as f:
  142. content = f.read()
  143. # thanks ai for the regex
  144. pattern = r'\[' + re.escape(email) + r'\].*?(?=\n\[|\Z)'
  145. new_content = re.sub(pattern, '', content, flags=re.DOTALL)
  146. # ICH HASSE GRUSSIGI CONFIG
  147. new_content = re.sub(r'\n\n\n+', '\n\n', new_content)
  148. # updaten das filet
  149. with open(config_file, "w") as f:
  150. f.write(new_content)
  151. f.flush()
  152. os.fsync(f.fileno())
  153. print(f"[+] Successfully removed user {email} from the config.")
  154. return True
  155. def main():
  156. """Main function to handle the authentication process."""
  157. config = load_config()
  158. # load em vars yehea boi
  159. emailproxy_exec = config.get("EMAILPROXY_EXECUTABLE", "emailproxy")
  160. main_config = config.get("EMAILPROXY_CONFIG_FILE", "/home/crt/helper-emailproxy/config/emailproxy.config")
  161. auth_base_config = config.get("EMAILPROXY_AUTH_CONFIG", "/home/crt/helper-emailproxy/config/emailproxy-auth.config")
  162. log_file = config.get("EMAILPROXY_LOG_FILE", "/tmp/emailproxy.log")
  163. command_port = int(config.get("COMMAND_PORT", "8765"))
  164. # get da working dir
  165. working_dir = os.path.dirname(emailproxy_exec) if "/" in emailproxy_exec else os.getcwd()
  166. # launch main proxii
  167. print("[+] Launching the main emailproxy process...")
  168. cmd = f"stdbuf -oL {emailproxy_exec} --config-file '{main_config}' --no-gui"
  169. # commadn line debug (sorry for the mess below emailproxy was acting weird)
  170. debug = "--debug" in sys.argv
  171. if debug:
  172. main_proxy_proc = subprocess.Popen(
  173. cmd,
  174. shell=True,
  175. cwd=working_dir,
  176. stdin=subprocess.PIPE,
  177. stdout=sys.stdout,
  178. stderr=sys.stderr,
  179. text=True
  180. )
  181. else:
  182. with open(log_file, "a") as lf:
  183. main_proxy_proc = subprocess.Popen(
  184. cmd,
  185. shell=True,
  186. cwd=working_dir,
  187. stdin=subprocess.PIPE,
  188. stdout=lf,
  189. stderr=lf,
  190. text=True
  191. )
  192. print(f"[+] emailproxy started with PID {main_proxy_proc.pid}")
  193. # maybe benutzi machi das richtigi porti
  194. server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  195. server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  196. server_socket.bind(('localhost', command_port))
  197. server_socket.listen(5)
  198. print(f"[+] Command server started on port {command_port}, waiting for commands...")
  199. active_sessions = {} # store active sessions for cleanup so no mess and stinky
  200. try:
  201. while True:
  202. # accept le connections from PHP <3
  203. client_socket, address = server_socket.accept()
  204. data = client_socket.recv(4096).decode('utf-8')
  205. try:
  206. command = json.loads(data)
  207. cmd_type = command.get("type")
  208. if cmd_type == "new_user":
  209. # thanks ai for fixing my crap
  210. email = command.get("email", "")
  211. imap_port = int(command.get("imap_port", 2993))
  212. smtp_port = int(command.get("smtp_port", 2465))
  213. session_id = command.get("session_id")
  214. temp_config_path, temp_dir, temp_proc = handle_new_user(
  215. email, imap_port, smtp_port, emailproxy_exec, auth_base_config
  216. )
  217. if temp_config_path:
  218. active_sessions[session_id] = (temp_config_path, temp_dir, temp_proc)
  219. client_socket.send(json.dumps({"success": True}).encode('utf-8'))
  220. else:
  221. client_socket.send(json.dumps({"success": False, "error": "Failed to start temporary proxy"}).encode('utf-8'))
  222. elif cmd_type == "check_auth":
  223. # check if the user was successfully added to the temporary config
  224. session_id = command.get("session_id")
  225. email = command.get("email")
  226. if session_id in active_sessions:
  227. temp_config_path, _, _ = active_sessions[session_id]
  228. success = validate_new_user(temp_config_path, email)
  229. client_socket.send(json.dumps({"success": success}).encode('utf-8'))
  230. else:
  231. client_socket.send(json.dumps({"success": False, "error": "Invalid session"}).encode('utf-8'))
  232. elif cmd_type == "merge_config":
  233. # merge the config and brutally restart the proxy (bad bad bad but works so so yeah uhm idk)
  234. session_id = command.get("session_id")
  235. if session_id in active_sessions:
  236. temp_config_path, temp_dir, temp_proc = active_sessions[session_id]
  237. # murder temp
  238. if temp_proc and temp_proc.poll() is None:
  239. temp_proc.terminate()
  240. temp_proc.wait(timeout=5)
  241. # stop main proxy
  242. working_dir, log_file, debug = restart_main_proxy(emailproxy_exec, main_config, working_dir, log_file, debug)
  243. # merge while stopped
  244. print("[+] Merging configuration files...")
  245. merge_success = merge_configs(main_config, temp_config_path)
  246. # revive again
  247. print("[+] Starting a new emailproxy process...")
  248. cmd = f"stdbuf -oL {emailproxy_exec} --config-file '{main_config}' --no-gui"
  249. if debug:
  250. main_proxy_proc = subprocess.Popen(
  251. cmd,
  252. shell=True,
  253. cwd=working_dir,
  254. stdin=subprocess.PIPE,
  255. stdout=sys.stdout,
  256. stderr=sys.stderr,
  257. text=True
  258. )
  259. else:
  260. with open(log_file, "a") as lf:
  261. main_proxy_proc = subprocess.Popen(
  262. cmd,
  263. shell=True,
  264. cwd=working_dir,
  265. stdin=subprocess.PIPE,
  266. stdout=lf,
  267. stderr=lf,
  268. text=True
  269. )
  270. print(f"[+] New emailproxy process started with PID {main_proxy_proc.pid}")
  271. # no more temp
  272. shutil.rmtree(temp_dir)
  273. del active_sessions[session_id]
  274. client_socket.send(json.dumps({"success": merge_success}).encode('utf-8'))
  275. else:
  276. client_socket.send(json.dumps({"success": False, "error": "Invalid session"}).encode('utf-8'))
  277. elif cmd_type == "cleanup":
  278. # clean up abandoned fatherless session (sad violin)
  279. session_id = command.get("session_id")
  280. if session_id in active_sessions:
  281. _, temp_dir, temp_proc = active_sessions[session_id]
  282. # more murdering of the orphans
  283. if temp_proc and temp_proc.poll() is None:
  284. temp_proc.terminate()
  285. temp_proc.wait(timeout=5)
  286. # hide the bodies
  287. shutil.rmtree(temp_dir)
  288. del active_sessions[session_id]
  289. client_socket.send(json.dumps({"success": True}).encode('utf-8'))
  290. else:
  291. client_socket.send(json.dumps({"success": False, "error": "Invalid session"}).encode('utf-8'))
  292. elif cmd_type == "redirect_url":
  293. # process the actual redirect url
  294. session_id = command.get("session_id")
  295. email = command.get("email")
  296. redirect_url = command.get("redirect_url")
  297. if session_id in active_sessions:
  298. temp_config_path, temp_dir, temp_proc = active_sessions[session_id]
  299. print(f"[+] Received redirect URL for {email}")
  300. if temp_proc and temp_proc.poll() is None:
  301. try:
  302. # canz we haz writez?
  303. if temp_proc.stdin:
  304. temp_proc.stdin.write(redirect_url + "\n")
  305. temp_proc.stdin.flush()
  306. time.sleep(5) # oop wait a bit for the process
  307. # ignoring the problems just like irl and trying again
  308. success = False
  309. for _ in range(3):
  310. if validate_new_user(temp_config_path, email):
  311. success = True
  312. break
  313. time.sleep(2)
  314. client_socket.send(json.dumps({"success": success}).encode('utf-8'))
  315. else:
  316. client_socket.send(json.dumps({"success": False, "error": "Process stdin not available"}).encode('utf-8'))
  317. except Exception as e:
  318. print(f"[!] Error sending redirect URL: {e}")
  319. client_socket.send(json.dumps({"success": False, "error": str(e)}).encode('utf-8'))
  320. else:
  321. client_socket.send(json.dumps({"success": False, "error": "Temporary process not running"}).encode('utf-8'))
  322. else:
  323. client_socket.send(json.dumps({"success": False, "error": "Invalid session"}).encode('utf-8'))
  324. elif cmd_type == "remove_user":
  325. # remove a user from the main config
  326. email = command.get("email")
  327. if not email:
  328. client_socket.send(json.dumps({"success": False, "error": "Email address required"}).encode('utf-8'))
  329. continue
  330. # stop the main proxy before removing the user
  331. working_dir, log_file, debug = restart_main_proxy(emailproxy_exec, main_config, working_dir, log_file, debug)
  332. # tschau tschau user
  333. print(f"[+] Removing user {email} from configuration...")
  334. remove_success = remove_user_from_config(main_config, email)
  335. # prxi is back
  336. print("[+] Starting a new emailproxy process...")
  337. cmd = f"stdbuf -oL {emailproxy_exec} --config-file '{main_config}' --no-gui"
  338. if debug:
  339. main_proxy_proc = subprocess.Popen(
  340. cmd,
  341. shell=True,
  342. cwd=working_dir,
  343. stdin=subprocess.PIPE,
  344. stdout=sys.stdout,
  345. stderr=sys.stderr,
  346. text=True
  347. )
  348. else:
  349. with open(log_file, "a") as lf:
  350. main_proxy_proc = subprocess.Popen(
  351. cmd,
  352. shell=True,
  353. cwd=working_dir,
  354. stdin=subprocess.PIPE,
  355. stdout=lf,
  356. stderr=lf,
  357. text=True
  358. )
  359. print(f"[+] New emailproxy process started with PID {main_proxy_proc.pid}")
  360. client_socket.send(json.dumps({"success": remove_success}).encode('utf-8'))
  361. else:
  362. client_socket.send(json.dumps({"success": False, "error": "Unknown command"}).encode('utf-8'))
  363. except json.JSONDecodeError:
  364. client_socket.send(json.dumps({"success": False, "error": "Invalid JSON"}).encode('utf-8'))
  365. except Exception as e:
  366. client_socket.send(json.dumps({"success": False, "error": str(e)}).encode('utf-8'))
  367. finally:
  368. client_socket.close()
  369. # apparently we need to clean up the mess (thanks ai)
  370. current_time = time.time()
  371. timed_out_sessions = []
  372. for session_id, (_, temp_dir, temp_proc) in active_sessions.items():
  373. session_time = int(session_id.split('_')[0])
  374. if current_time - session_time > 300: # 5 mins
  375. if temp_proc and temp_proc.poll() is None:
  376. temp_proc.terminate()
  377. temp_proc.wait(timeout=5)
  378. shutil.rmtree(temp_dir)
  379. timed_out_sessions.append(session_id)
  380. for session_id in timed_out_sessions:
  381. del active_sessions[session_id]
  382. print(f"[!] Session {session_id} timed out and was cleaned up.")
  383. except KeyboardInterrupt:
  384. print("[!] Shutting down...")
  385. finally:
  386. # bye bye sessions
  387. for session_id, (_, temp_dir, temp_proc) in active_sessions.items():
  388. if temp_proc and temp_proc.poll() is None:
  389. temp_proc.terminate()
  390. temp_proc.wait(timeout=5)
  391. shutil.rmtree(temp_dir)
  392. # bye bye main proxy
  393. if main_proxy_proc.poll() is None:
  394. main_proxy_proc.terminate()
  395. main_proxy_proc.wait(timeout=5)
  396. print("[+] All processes terminated and temporary files probably cleaned up.")
  397. if __name__ == "__main__":
  398. main()