BootstrapPhpfpm.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. from jinja2 import Environment, FileSystemLoader
  2. from modules.BootstrapBase import BootstrapBase
  3. from pathlib import Path
  4. import os
  5. import ipaddress
  6. import sys
  7. import time
  8. import platform
  9. import subprocess
  10. class Bootstrap(BootstrapBase):
  11. def bootstrap(self):
  12. self.connect_mysql()
  13. self.connect_redis()
  14. # Setup Jinja2 Environment and load vars
  15. self.env = Environment(
  16. loader=FileSystemLoader([
  17. '/php-conf/custom_templates',
  18. '/php-conf/config_templates'
  19. ]),
  20. keep_trailing_newline=True,
  21. lstrip_blocks=True,
  22. trim_blocks=True
  23. )
  24. extra_vars = {
  25. }
  26. self.env_vars = self.prepare_template_vars('/overwrites.json', extra_vars)
  27. print("Set Timezone")
  28. self.set_timezone()
  29. # Prepare Redis and MySQL Database
  30. # TODO: move to dockerapi
  31. if self.isYes(os.getenv("MASTER", "")):
  32. print("We are master, preparing...")
  33. self.prepare_redis()
  34. self.setup_apikeys(
  35. os.getenv("API_ALLOW_FROM", "").strip(),
  36. os.getenv("API_KEY", "").strip(),
  37. os.getenv("API_KEY_READ_ONLY", "").strip()
  38. )
  39. self.setup_mysql_events()
  40. print("Render config")
  41. self.render_config("/php-conf/config.json")
  42. self.copy_file("/usr/local/etc/php/conf.d/opcache-recommended.ini", "/php-conf/opcache-recommended.ini")
  43. self.copy_file("/usr/local/etc/php-fpm.d/z-pools.conf", "/php-conf/pools.conf")
  44. self.copy_file("/usr/local/etc/php/conf.d/zzz-other.ini", "/php-conf/other.ini")
  45. self.copy_file("/usr/local/etc/php/conf.d/upload.ini", "/php-conf/upload.ini")
  46. self.copy_file("/usr/local/etc/php/conf.d/session_store.ini", "/php-conf/session_store.ini")
  47. self.set_owner("/global_sieve", 82, 82, recursive=True)
  48. self.set_owner("/web/templates/cache", 82, 82, recursive=True)
  49. self.remove("/web/templates/cache", wipe_contents=True, exclude=[".gitkeep"])
  50. print("Running DB init...")
  51. self.run_command(["php", "-c", "/usr/local/etc/php", "-f", "/web/inc/init_db.inc.php"], check=False)
  52. def prepare_redis(self):
  53. print("Setting default Redis keys if missing...")
  54. # Q_RELEASE_FORMAT
  55. if self.redis_conn.get("Q_RELEASE_FORMAT") is None:
  56. self.redis_conn.set("Q_RELEASE_FORMAT", "raw")
  57. # Q_MAX_AGE
  58. if self.redis_conn.get("Q_MAX_AGE") is None:
  59. self.redis_conn.set("Q_MAX_AGE", 365)
  60. # PASSWD_POLICY hash defaults
  61. if self.redis_conn.hget("PASSWD_POLICY", "length") is None:
  62. self.redis_conn.hset("PASSWD_POLICY", mapping={
  63. "length": 6,
  64. "chars": 0,
  65. "special_chars": 0,
  66. "lowerupper": 0,
  67. "numbers": 0
  68. })
  69. # DOMAIN_MAP
  70. print("Rebuilding DOMAIN_MAP from MySQL...")
  71. self.redis_conn.delete("DOMAIN_MAP")
  72. domains = set()
  73. try:
  74. cursor = self.mysql_conn.cursor()
  75. cursor.execute("SELECT domain FROM domain")
  76. domains.update(row[0] for row in cursor.fetchall())
  77. cursor.execute("SELECT alias_domain FROM alias_domain")
  78. domains.update(row[0] for row in cursor.fetchall())
  79. cursor.close()
  80. if domains:
  81. for domain in domains:
  82. self.redis_conn.hset("DOMAIN_MAP", domain, 1)
  83. print(f"{len(domains)} domains added to DOMAIN_MAP.")
  84. else:
  85. print("No domains found to insert into DOMAIN_MAP.")
  86. except Exception as e:
  87. print(f"Failed to rebuild DOMAIN_MAP: {e}")
  88. def setup_apikeys(self, api_allow_from, api_key_rw, api_key_ro):
  89. if not api_allow_from or api_allow_from == "invalid":
  90. return
  91. print("Validating API_ALLOW_FROM IPs...")
  92. ip_list = [ip.strip() for ip in api_allow_from.split(",")]
  93. validated_ips = []
  94. for ip in ip_list:
  95. try:
  96. ipaddress.ip_network(ip, strict=False)
  97. validated_ips.append(ip)
  98. except ValueError:
  99. continue
  100. if not validated_ips:
  101. print("No valid IPs found in API_ALLOW_FROM")
  102. return
  103. allow_from_str = ",".join(validated_ips)
  104. cursor = self.mysql_conn.cursor()
  105. try:
  106. if api_key_rw and api_key_rw != "invalid":
  107. print("Setting RW API key...")
  108. cursor.execute("DELETE FROM api WHERE access = 'rw'")
  109. cursor.execute(
  110. "INSERT INTO api (api_key, active, allow_from, access) VALUES (%s, %s, %s, %s)",
  111. (api_key_rw, 1, allow_from_str, "rw")
  112. )
  113. if api_key_ro and api_key_ro != "invalid":
  114. print("Setting RO API key...")
  115. cursor.execute("DELETE FROM api WHERE access = 'ro'")
  116. cursor.execute(
  117. "INSERT INTO api (api_key, active, allow_from, access) VALUES (%s, %s, %s, %s)",
  118. (api_key_ro, 1, allow_from_str, "ro")
  119. )
  120. self.mysql_conn.commit()
  121. print("API key(s) set successfully.")
  122. except Exception as e:
  123. print(f"Failed to configure API keys: {e}")
  124. self.mysql_conn.rollback()
  125. finally:
  126. cursor.close()
  127. def setup_mysql_events(self):
  128. print("Creating scheduled MySQL EVENTS...")
  129. queries = [
  130. "DROP EVENT IF EXISTS clean_spamalias;",
  131. """
  132. CREATE EVENT clean_spamalias
  133. ON SCHEDULE EVERY 1 DAY
  134. DO
  135. DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP();
  136. """,
  137. "DROP EVENT IF EXISTS clean_oauth2;",
  138. """
  139. CREATE EVENT clean_oauth2
  140. ON SCHEDULE EVERY 1 DAY
  141. DO
  142. BEGIN
  143. DELETE FROM oauth_refresh_tokens WHERE expires < NOW();
  144. DELETE FROM oauth_access_tokens WHERE expires < NOW();
  145. DELETE FROM oauth_authorization_codes WHERE expires < NOW();
  146. END;
  147. """,
  148. "DROP EVENT IF EXISTS clean_sasl_log;",
  149. """
  150. CREATE EVENT clean_sasl_log
  151. ON SCHEDULE EVERY 1 DAY
  152. DO
  153. BEGIN
  154. DELETE sasl_log.* FROM sasl_log
  155. LEFT JOIN (
  156. SELECT username, service, MAX(datetime) AS lastdate
  157. FROM sasl_log
  158. GROUP BY username, service
  159. ) AS last
  160. ON sasl_log.username = last.username AND sasl_log.service = last.service
  161. WHERE datetime < DATE_SUB(NOW(), INTERVAL 31 DAY)
  162. AND datetime < lastdate;
  163. DELETE FROM sasl_log
  164. WHERE username NOT IN (SELECT username FROM mailbox)
  165. AND datetime < DATE_SUB(NOW(), INTERVAL 31 DAY);
  166. END;
  167. """
  168. ]
  169. try:
  170. cursor = self.mysql_conn.cursor()
  171. for query in queries:
  172. cursor.execute(query)
  173. self.mysql_conn.commit()
  174. cursor.close()
  175. print("MySQL EVENTS created successfully.")
  176. except Exception as e:
  177. print(f"Failed to create MySQL EVENTS: {e}")
  178. self.mysql_conn.rollback()