BootstrapPhpfpm.py 6.5 KB

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