Mailcow.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. import requests
  2. import urllib3
  3. import sys
  4. import os
  5. import subprocess
  6. import tempfile
  7. import mysql.connector
  8. from contextlib import contextmanager
  9. from datetime import datetime
  10. from modules.Docker import Docker
  11. class Mailcow:
  12. def __init__(self):
  13. self.apiUrl = "/api/v1"
  14. self.ignore_ssl_errors = True
  15. self.baseUrl = f"https://{os.getenv('IPv4_NETWORK', '172.22.1')}.247:{os.getenv('HTTPS_PORT', '443')}"
  16. self.host = os.getenv("MAILCOW_HOSTNAME", "")
  17. self.apiKey = ""
  18. if self.ignore_ssl_errors:
  19. urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
  20. self.db_config = {
  21. 'user': os.getenv('DBUSER'),
  22. 'password': os.getenv('DBPASS'),
  23. 'database': os.getenv('DBNAME'),
  24. 'unix_socket': '/var/run/mysqld/mysqld.sock',
  25. }
  26. self.docker = Docker()
  27. # API Functions
  28. def addDomain(self, domain):
  29. """
  30. Add a domain to the mailcow instance.
  31. :param domain: Dictionary containing domain details.
  32. :return: Response from the mailcow API.
  33. """
  34. return self.post('/add/domain', domain)
  35. def addMailbox(self, mailbox):
  36. """
  37. Add a mailbox to the mailcow instance.
  38. :param mailbox: Dictionary containing mailbox details.
  39. :return: Response from the mailcow API.
  40. """
  41. return self.post('/add/mailbox', mailbox)
  42. def addAlias(self, alias):
  43. """
  44. Add an alias to the mailcow instance.
  45. :param alias: Dictionary containing alias details.
  46. :return: Response from the mailcow API.
  47. """
  48. return self.post('/add/alias', alias)
  49. def addSyncjob(self, syncjob):
  50. """
  51. Add a sync job to the mailcow instance.
  52. :param syncjob: Dictionary containing sync job details.
  53. :return: Response from the mailcow API.
  54. """
  55. return self.post('/add/syncjob', syncjob)
  56. def addDomainadmin(self, domainadmin):
  57. """
  58. Add a domain admin to the mailcow instance.
  59. :param domainadmin: Dictionary containing domain admin details.
  60. :return: Response from the mailcow API.
  61. """
  62. return self.post('/add/domain-admin', domainadmin)
  63. def deleteDomain(self, domain):
  64. """
  65. Delete a domain from the mailcow instance.
  66. :param domain: Name of the domain to delete.
  67. :return: Response from the mailcow API.
  68. """
  69. items = [domain]
  70. return self.post('/delete/domain', items)
  71. def deleteAlias(self, id):
  72. """
  73. Delete an alias from the mailcow instance.
  74. :param id: ID of the alias to delete.
  75. :return: Response from the mailcow API.
  76. """
  77. items = [id]
  78. return self.post('/delete/alias', items)
  79. def deleteSyncjob(self, id):
  80. """
  81. Delete a sync job from the mailcow instance.
  82. :param id: ID of the sync job to delete.
  83. :return: Response from the mailcow API.
  84. """
  85. items = [id]
  86. return self.post('/delete/syncjob', items)
  87. def deleteMailbox(self, mailbox):
  88. """
  89. Delete a mailbox from the mailcow instance.
  90. :param mailbox: Name of the mailbox to delete.
  91. :return: Response from the mailcow API.
  92. """
  93. items = [mailbox]
  94. return self.post('/delete/mailbox', items)
  95. def deleteDomainadmin(self, username):
  96. """
  97. Delete a domain admin from the mailcow instance.
  98. :param username: Username of the domain admin to delete.
  99. :return: Response from the mailcow API.
  100. """
  101. items = [username]
  102. return self.post('/delete/domain-admin', items)
  103. def post(self, endpoint, data):
  104. """
  105. Make a POST request to the mailcow API.
  106. :param endpoint: The API endpoint to post to.
  107. :param data: Data to be sent in the POST request.
  108. :return: Response from the mailcow API.
  109. """
  110. url = f"{self.baseUrl}{self.apiUrl}/{endpoint.lstrip('/')}"
  111. headers = {
  112. "Content-Type": "application/json",
  113. "Host": self.host
  114. }
  115. if self.apiKey:
  116. headers["X-Api-Key"] = self.apiKey
  117. response = requests.post(
  118. url,
  119. json=data,
  120. headers=headers,
  121. verify=not self.ignore_ssl_errors
  122. )
  123. response.raise_for_status()
  124. return response.json()
  125. def getDomain(self, domain):
  126. """
  127. Get a domain from the mailcow instance.
  128. :param domain: Name of the domain to get.
  129. :return: Response from the mailcow API.
  130. """
  131. return self.get(f'/get/domain/{domain}')
  132. def getMailbox(self, username):
  133. """
  134. Get a mailbox from the mailcow instance.
  135. :param mailbox: Dictionary containing mailbox details (e.g. {"username": "user@example.com"})
  136. :return: Response from the mailcow API.
  137. """
  138. return self.get(f'/get/mailbox/{username}')
  139. def getAlias(self, id):
  140. """
  141. Get an alias from the mailcow instance.
  142. :param alias: Dictionary containing alias details (e.g. {"address": "alias@example.com"})
  143. :return: Response from the mailcow API.
  144. """
  145. return self.get(f'/get/alias/{id}')
  146. def getSyncjob(self, id):
  147. """
  148. Get a sync job from the mailcow instance.
  149. :param syncjob: Dictionary containing sync job details (e.g. {"id": "123"})
  150. :return: Response from the mailcow API.
  151. """
  152. return self.get(f'/get/syncjobs/{id}')
  153. def getDomainadmin(self, username):
  154. """
  155. Get a domain admin from the mailcow instance.
  156. :param username: Username of the domain admin to get.
  157. :return: Response from the mailcow API.
  158. """
  159. return self.get(f'/get/domain-admin/{username}')
  160. def getStatusVersion(self):
  161. """
  162. Get the version of the mailcow instance.
  163. :return: Response from the mailcow API.
  164. """
  165. return self.get('/get/status/version')
  166. def getStatusVmail(self):
  167. """
  168. Get the vmail status from the mailcow instance.
  169. :return: Response from the mailcow API.
  170. """
  171. return self.get('/get/status/vmail')
  172. def getStatusContainers(self):
  173. """
  174. Get the status of containers from the mailcow instance.
  175. :return: Response from the mailcow API.
  176. """
  177. return self.get('/get/status/containers')
  178. def get(self, endpoint, params=None):
  179. """
  180. Make a GET request to the mailcow API.
  181. :param endpoint: The API endpoint to get from.
  182. :param params: Parameters to be sent in the GET request.
  183. :return: Response from the mailcow API.
  184. """
  185. url = f"{self.baseUrl}{self.apiUrl}/{endpoint.lstrip('/')}"
  186. headers = {
  187. "Content-Type": "application/json",
  188. "Host": self.host
  189. }
  190. if self.apiKey:
  191. headers["X-Api-Key"] = self.apiKey
  192. response = requests.get(
  193. url,
  194. params=params,
  195. headers=headers,
  196. verify=not self.ignore_ssl_errors
  197. )
  198. response.raise_for_status()
  199. return response.json()
  200. def editDomain(self, domain, attributes):
  201. """
  202. Edit an existing domain in the mailcow instance.
  203. :param domain: Name of the domain to edit
  204. :param attributes: Dictionary containing the new domain attributes.
  205. """
  206. items = [domain]
  207. return self.edit('/edit/domain', items, attributes)
  208. def editMailbox(self, mailbox, attributes):
  209. """
  210. Edit an existing mailbox in the mailcow instance.
  211. :param mailbox: Name of the mailbox to edit
  212. :param attributes: Dictionary containing the new mailbox attributes.
  213. """
  214. items = [mailbox]
  215. return self.edit('/edit/mailbox', items, attributes)
  216. def editAlias(self, alias, attributes):
  217. """
  218. Edit an existing alias in the mailcow instance.
  219. :param alias: Name of the alias to edit
  220. :param attributes: Dictionary containing the new alias attributes.
  221. """
  222. items = [alias]
  223. return self.edit('/edit/alias', items, attributes)
  224. def editSyncjob(self, syncjob, attributes):
  225. """
  226. Edit an existing sync job in the mailcow instance.
  227. :param syncjob: Name of the sync job to edit
  228. :param attributes: Dictionary containing the new sync job attributes.
  229. """
  230. items = [syncjob]
  231. return self.edit('/edit/syncjob', items, attributes)
  232. def editDomainadmin(self, username, attributes):
  233. """
  234. Edit an existing domain admin in the mailcow instance.
  235. :param username: Username of the domain admin to edit
  236. :param attributes: Dictionary containing the new domain admin attributes.
  237. """
  238. items = [username]
  239. return self.edit('/edit/domain-admin', items, attributes)
  240. def edit(self, endpoint, items, attributes):
  241. """
  242. Make a POST request to edit items in the mailcow API.
  243. :param items: List of items to edit.
  244. :param attributes: Dictionary containing the new attributes for the items.
  245. :return: Response from the mailcow API.
  246. """
  247. url = f"{self.baseUrl}{self.apiUrl}/{endpoint.lstrip('/')}"
  248. headers = {
  249. "Content-Type": "application/json",
  250. "Host": self.host
  251. }
  252. if self.apiKey:
  253. headers["X-Api-Key"] = self.apiKey
  254. data = {
  255. "items": items,
  256. "attr": attributes
  257. }
  258. response = requests.post(
  259. url,
  260. json=data,
  261. headers=headers,
  262. verify=not self.ignore_ssl_errors
  263. )
  264. response.raise_for_status()
  265. return response.json()
  266. # System Functions
  267. def runSyncjob(self, id, force=False):
  268. """
  269. Run a sync job.
  270. :param id: ID of the sync job to run.
  271. :return: Response from the imapsync script.
  272. """
  273. creds_path = "/app/sieve.creds"
  274. conn = mysql.connector.connect(**self.db_config)
  275. cursor = conn.cursor(dictionary=True)
  276. with open(creds_path, 'r') as file:
  277. master_user, master_pass = file.read().strip().split(':')
  278. query = ("SELECT * FROM imapsync WHERE id = %s")
  279. cursor.execute(query, (id,))
  280. success = False
  281. syncjob = cursor.fetchone()
  282. if not syncjob:
  283. cursor.close()
  284. conn.close()
  285. return f"Sync job with ID {id} not found."
  286. if syncjob['active'] == 0 and not force:
  287. cursor.close()
  288. conn.close()
  289. return f"Sync job with ID {id} is not active."
  290. enc1_flag = "--tls1" if syncjob['enc1'] == "TLS" else "--ssl1" if syncjob['enc1'] == "SSL" else None
  291. passfile1_path = f"/tmp/passfile1_{id}.txt"
  292. passfile2_path = f"/tmp/passfile2_{id}.txt"
  293. passfile1_cmd = [
  294. "sh", "-c",
  295. f"echo {syncjob['password1']} > {passfile1_path}"
  296. ]
  297. passfile2_cmd = [
  298. "sh", "-c",
  299. f"echo {master_pass} > {passfile2_path}"
  300. ]
  301. self.docker.exec_command("dovecot-mailcow", passfile1_cmd)
  302. self.docker.exec_command("dovecot-mailcow", passfile2_cmd)
  303. imapsync_cmd = [
  304. "/usr/local/bin/imapsync",
  305. "--tmpdir", "/tmp",
  306. "--nofoldersizes",
  307. "--addheader"
  308. ]
  309. if int(syncjob['timeout1']) > 0:
  310. imapsync_cmd.extend(['--timeout1', str(syncjob['timeout1'])])
  311. if int(syncjob['timeout2']) > 0:
  312. imapsync_cmd.extend(['--timeout2', str(syncjob['timeout2'])])
  313. if syncjob['exclude']:
  314. imapsync_cmd.extend(['--exclude', syncjob['exclude']])
  315. if syncjob['subfolder2']:
  316. imapsync_cmd.extend(['--subfolder2', syncjob['subfolder2']])
  317. if int(syncjob['maxage']) > 0:
  318. imapsync_cmd.extend(['--maxage', str(syncjob['maxage'])])
  319. if int(syncjob['maxbytespersecond']) > 0:
  320. imapsync_cmd.extend(['--maxbytespersecond', str(syncjob['maxbytespersecond'])])
  321. if int(syncjob['delete2duplicates']) == 1:
  322. imapsync_cmd.append("--delete2duplicates")
  323. if int(syncjob['subscribeall']) == 1:
  324. imapsync_cmd.append("--subscribeall")
  325. if int(syncjob['delete1']) == 1:
  326. imapsync_cmd.append("--delete")
  327. if int(syncjob['delete2']) == 1:
  328. imapsync_cmd.append("--delete2")
  329. if int(syncjob['automap']) == 1:
  330. imapsync_cmd.append("--automap")
  331. if int(syncjob['skipcrossduplicates']) == 1:
  332. imapsync_cmd.append("--skipcrossduplicates")
  333. if enc1_flag:
  334. imapsync_cmd.append(enc1_flag)
  335. imapsync_cmd.extend([
  336. "--host1", syncjob['host1'],
  337. "--user1", syncjob['user1'],
  338. "--passfile1", passfile1_path,
  339. "--port1", str(syncjob['port1']),
  340. "--host2", "localhost",
  341. "--user2", f"{syncjob['user2']}*{master_user}",
  342. "--passfile2", passfile2_path
  343. ])
  344. if syncjob['dry'] == 1:
  345. imapsync_cmd.append("--dry")
  346. imapsync_cmd.extend([
  347. "--no-modulesversion",
  348. "--noreleasecheck"
  349. ])
  350. try:
  351. cursor.execute("UPDATE imapsync SET is_running = 1, success = NULL, exit_status = NULL WHERE id = %s", (id,))
  352. conn.commit()
  353. result = self.docker.exec_command("dovecot-mailcow", imapsync_cmd)
  354. print(result)
  355. success = result['status'] == "success" and result['exit_code'] == 0
  356. cursor.execute(
  357. "UPDATE imapsync SET returned_text = %s, success = %s, exit_status = %s WHERE id = %s",
  358. (result['output'], int(success), result['exit_code'], id)
  359. )
  360. conn.commit()
  361. except Exception as e:
  362. cursor.execute(
  363. "UPDATE imapsync SET returned_text = %s, success = 0 WHERE id = %s",
  364. (str(e), id)
  365. )
  366. conn.commit()
  367. finally:
  368. cursor.execute("UPDATE imapsync SET last_run = NOW(), is_running = 0 WHERE id = %s", (id,))
  369. conn.commit()
  370. delete_passfile1_cmd = [
  371. "sh", "-c",
  372. f"rm -f {passfile1_path}"
  373. ]
  374. delete_passfile2_cmd = [
  375. "sh", "-c",
  376. f"rm -f {passfile2_path}"
  377. ]
  378. self.docker.exec_command("dovecot-mailcow", delete_passfile1_cmd)
  379. self.docker.exec_command("dovecot-mailcow", delete_passfile2_cmd)
  380. cursor.close()
  381. conn.close()
  382. return "Sync job completed successfully." if success else "Sync job failed."