#!/usr/bin/env python
# coding:utf-8

import os
import re
import time
import ast
import imaplib, socket

import cplImaplib
from cplCommand import LocalClient
from .sshfs import SSHFS
from .tar import TAR


msg1001 = 'unable to get imap host from ldap data'
msg1002 = 'configuration error, no imap host provided'
msg1003 = 'configuration error, unable to find mail server config'
msg1004 = 'configuration error, no proxy user/password provided'
msg1005 = 'imap host configuration: {0}'
msg1006 = 'no configured imap connection for user: \'{0}\''
msg1007 = 'user \'{0}\' not connected to imap'
msg1008 = 'no mail attibute, nothing to do for: \'{0}\''
msg1009 = 'nothing to do for: \'{0}\''
msg1010 = 'unknown mail archive mode configuration: \'{0}\''
msg1011 = 'archive mailbox does not exist: \'{0}\''
msg1012 = 'mailbox does not exist: \'{0}\''
msg1013 = 'move \'{0}\' to \'{1}\' failed'
msg1014 = 'refuse to remove non empty mailbox \'{0}\''
msg1015 = 'something went wrong getting an acl list from \'{0}\''
msg1016 = 'mailbox \'{0}\' is excluded'
msg1017 = 'saved mail users string: {0}'
msg1018 = 'saved mail users: {0}'
msg1019 = 'no orig mail users provided in command call'
msg1020 = 'unable to coerce sting of mail users to list: \'{0}\''
msg1021 = 'closing all imap connections'
msg1022 = 'unable to determin mail archive base path'
msg1023 = 'downloading IMAP mailbox: {0}'
msg1024 = 'unable to create mail archive \'{0}\''
msg1025 = 'removed temp mail directory: \'{0}\''
msg1026 = 'failed to remove temp mail directory: \'{0}\''
msg1027 = 'not handling groupmail autosubscription for server \'{0}\''
msg1028 = 'performing groupmail autosubscription for group \'{0}\''
msg1029 = 'successfully moved mailbox \'{0}\' to \'{1}\''
msg1030 = 'successfully written mailbox \'{0}\' to archive \'{1}\''
msg1031 = 'deleted mailbox: \'{0}\''
msg1032 = 'skipping mailbox deletion of: \'{0}\''


class ImapMail(SSHFS, TAR):

	def __init__(self, postcmd):

		super().__init__()

		self.client_l = LocalClient(self.logger)
		self.imap_hosts = {}
		self.imap_conns = {}


	def __del__(self):

		# disconnect and remove all imap connections
		self._imapDisConnAll()
		# disconnect all sshfs connections
		self._umountAllSSHFS()


	def __imapGetConn(self, conf, user=None):

		user = user or conf['proxy_user']

		try:
			return self.imap_conns[user]
		except KeyError:
			pass

		imap_name = self.ldap_data['gosaMailServer'][0].decode()
		imap_config = self._getImapConfig(imap_name)
		imap = cplImaplib.Imap(self.logger, skipspecial=imap_config['skipspecial'])

		# connect
		imap.connSrv(
			host=conf['host'],
			user=user and user or conf['proxy_user'],
			auser=conf['proxy_user'],
			passwd=conf['proxy_pwd'],
			usessl=conf['sec_b'],
			port=conf['port'],
		)

		self.imap_conns[user] = imap
		return self.imap_conns[user]


	def __imapGetConnConf(self, imap_name):

		try:
			return self.imap_hosts[imap_name]
		except KeyError:
			pass

		i_con_re_str = r'^\{(?P<host>[\w.-]+):?(?P<port>\d+)?/?(?P<sec>ssl|(?:no)?tls|(?:no)?validate-cert)?\}$'
		sec_imap_lst = ('ssl', 'tls', 'novalidate-cert',
			'validate-cert', None)

		# get imap host, admin, pwd etc. from ldap
		res = self.gosa_ldap.searchLdap(
			self.searchbase,
			'sub',
			'(&(objectClass=goImapServer)(goImapName={0}))'.
				format(imap_name),
			('goImapAdmin', 'goImapPassword',
				'cyrusImapSSL', 'goImapConnect', ),
		)

		if not res:
			self.exit(253, msg1003)

		# parse goImapConnect string
		try:
			self.imap_hosts[imap_name] = re.match(
				i_con_re_str,
				res[0][1]['goImapConnect'][0].decode()
			).groupdict()

		except (AttributeError, IndexError):
			self.exit(253, msg1001)

		if not self.imap_hosts[imap_name]['host']:
			self.exit(253, msg1002)

		self.imap_hosts[imap_name]['sec_b'] = \
			self.imap_hosts[imap_name]['sec'] in \
			sec_imap_lst and True or False

		self.imap_hosts[imap_name]['port'] = \
			self.imap_hosts[imap_name]['port'] and \
			self.imap_hosts[imap_name]['port'] or '993'

		try:
			self.imap_hosts[imap_name]['proxy_user'] = \
				res[0][1]['goImapAdmin'][0].decode()
			self.imap_hosts[imap_name]['proxy_pwd'] = \
				res[0][1]['goImapPassword'][0].decode()
		except KeyError:
			self.exit(253, msg1004)

		self.logger.debug(msg1005.format(self.imap_hosts[imap_name]))

		return self.imap_hosts[imap_name]


	def _imapDisConn(self, user):

		try:
			imap = self.imap_conns[user]
		except KeyError:
			self.logger.info(msg1006.format(user))
			return

		if not imap.connected:
			self.logger.info(msg1007.format(user))

		self.logger.debug('closing imap connection for: \'{0}\''.format(user))

		try:
			imap.disconnSrv()
			del self.imap_conns[user]

		except (cplImaplib.CfgError,
			cplImaplib.ConnectError,
			cplImaplib.ResponseError,
			cplImaplib.MiscError) as e:
			self.logger.error(str(e))

		except socket.error as e:
			self.logger.info('ERROR: {0} {1}'.format(*e))

		except (imaplib.IMAP4.error, imaplib.IMAP4.abort) as e:
			self.logger.info('ERROR: {0}'.format(e))


	def _imapDisConnAll(self):

		self.logger.debug(msg1021)
		try:
			for u in [ x for x in self.imap_conns.keys() ]:

				self._imapDisConn(u)
		except AttributeError:
			pass


	def modGroupMail(self):

		if not b'gosaMailAccount' in self.ldap_data['objectClass']:
			self.logger.info(msg1008.format(self.dn))
			return True

		imap_name = self.ldap_data['gosaMailServer'][0].decode()
		imap_config = self._getImapConfig(imap_name)

		if not imap_config['groupmailautosubscribe']:
			self.logger.info(msg1027.format(imap_name))
			return True

		self.logger.info(msg1028.format(self.cn))

		mailbox = self.cn
		mailbox_blacklist = imap_config['mailboxblacklist']

		# return here for blacklisted mailboxes
		if mailbox in mailbox_blacklist:
			self.logger.info(msg1016.format(mailbox))
			return True

		# get saved, original group member users
		try:
			if not self.opt[0]:
				omusers = []
			else:
				omusers_str = self.opt[0]
				self.logger.debug(msg1017.format(omusers_str))
				omusers = ast.literal_eval(omusers_str)
				self.logger.debug(msg1018.format(omusers))

		except IndexError:
			self.logger.info(msg1019)
			omusers = []

		except SyntaxError:
			self.logger.error(msg1020.format(omusers_str))
			omusers = []

		imap_conn_conf = self.__imapGetConnConf(imap_name)

		mailbox_base = imap_config['mailboxbase']
		mailbox_sh_base = imap_config['mailboxsharebase']

		bl_user = list(imap_config['mailuserblacklist'])
		bl_user.extend((imap_conn_conf['proxy_user'], mailbox, 'anyone', ))

		mb_list = list(imap_config['mailboxlist'])
		mb_list.append('')

		try:
			imap = self.__imapGetConn(imap_conn_conf)
			mailbox_path = imap.mbPath(mailbox_base + (mailbox, ))
			mailbox_share_path = imap.mbPath(mailbox_sh_base + (mailbox, ))

			# get recursive acl list of mailboxes
			acl_lst = imap.getacl(mailbox_path, True)

			# generate list of all mailboxes to create
			mb_list_fullp = [ imap.mbPath(mailbox_base +
				(mailbox, ) + (x, )) for x in mb_list ]

			# get user list from acl list excluding blacklisted users
			# for simplicity, we only use the first (main) mailbox to determin the user list 
			user_list = [ x for x in acl_lst[0][1].keys()
				if x not in bl_user ]

			for mb_acl in acl_lst:

				# test/fix acl's of all sub mailboxes
				for user in user_list:
					if user not in mb_acl[1].keys() or \
					   mb_acl[1][user] != acl_lst[0][1][user]:

						imap.setacl(mb_acl[0], user, acl_lst[0][1][user])

				# remove residual acl's from removed users
				for user in mb_acl[1].keys():
					if user not in user_list + bl_user:
						imap.setacl(mb_acl[0], user, '')

				# remove existing mailboxes from create list
				if mb_acl[0] in mb_list_fullp:

					mb_list_fullp.remove(mb_acl[0])

			# create missing mailboxes and set acl's
			if mb_list_fullp:

				if not imap.createl(mb_list_fullp): return False
				if not imap.setacll( [ (x, acl_lst[0][1], )
					for x in mb_list_fullp ] ): return False

			# connect as users, authenticating as proxyuser
			# to subscribe mailboxes
			try:
				for user in user_list:

					u_imap = self.__imapGetConn(imap_conn_conf, user)
					u_imap.subscribel(mb_list, mailbox_share_path)

			except (AttributeError, IndexError):
				self.logger.info(msg1009.format(mailbox_share_path))

			# connect as users, authenticating as proxyuser
			# to unsubscribe mailboxes for removed users

			# gather removed group members
			try:
				user_list = [ x for x in omusers if x.encode() not in self.ldap_data['memberUid'] ]

			except KeyError:
				# in case of a KeyError for memberUid in self.ldap_data
				# we assume all users have been removed from the group
				# and remove all users from the original users list
				user_list = [ x for x in omusers ]

			try:
				for user in user_list:

					u_imap = self.__imapGetConn(imap_conn_conf, user)
					u_imap.unsubscribe(mailbox_share_path, sub=True)

			except (AttributeError, IndexError):
				self.logger.info(msg1009.format(mailbox_share_path))

		except (cplImaplib.CfgError,
			cplImaplib.ConnectError,
			cplImaplib.ResponseError,
			cplImaplib.MiscError) as e:
			self.logger.error(str(e))
			return False

		except socket.error as e:
			self.logger.error('ERROR: {0} {1}'.format(*e))
			return False

		except (imaplib.IMAP4.error, imaplib.IMAP4.abort) as e:
			self.logger.error('ERROR: {0}'.format(e))
			return False

		return True


	def delMailbox(self):

		if not b'gosaMailAccount' in self.ldap_data['objectClass']:
			self.logger.info(msg1008.format(self.dn))
			return True

		imap_name = self.ldap_data['gosaMailServer'][0].decode()
		imap_config = self._getImapConfig(imap_name)

		mailbox = self.cn

		# verify archive mode
		if imap_config['mailarchivemode'] not in ('', 'move', 'tgz', 'tbz2'):
			self.logger.error(msg1010.format(imap_config['mailarchivemode']))
			return False

		imap_conn_conf = self.__imapGetConnConf(imap_name)

		mailbox_base = imap_config['mailboxbase']
		mailbox_sh_base = imap_config['mailboxsharebase']

		bl_user = (imap_conn_conf['proxy_user'], 'anyone', )

		try:
			imap = self.__imapGetConn(imap_conn_conf)
			mailbox_path = imap.mbPath(mailbox_base + (mailbox, ))
			mailbox_share_path = imap.mbPath(mailbox_sh_base + (mailbox, ))

			# get recursive acl list of mailboxes
			acl_lst = imap.getacl(mailbox_path, True)

			# exit for empty acl list as mailbox doesn't exist
			if not acl_lst:
				self.logger.info(msg1012.format(mailbox_path))
				return True

			# get user list from acl list excluding blacklisted users
			# for simplicity, we only use the first (root)
			# mailbox to determin the user list 
			user_list = [ x for x in acl_lst[0][1].keys()
				if x not in bl_user ]

			# unsubscribe mailboxes for all users
			try:
				for user in user_list:

					u_imap = self.__imapGetConn(imap_conn_conf, user)
					if user == mailbox:
						u_imap.unsubscribe(mailbox, sub=True)
					else:
						u_imap.unsubscribe(mailbox_share_path, sub=True)

			except (AttributeError, IndexError):
				self.logger.info(msg1009.format(mailbox_share_path))

			# replace acl's with proxy_user full access
			try:
				imap.applyacl(mailbox_path,
					{imap_conn_conf['proxy_user']:imap_config['proxyacl']}, True)

			except TypeError as e:
				self.logger.warn(e)
				return False

			if imap_config['mailarchivemode'] == 'move':

				# assure we have an archive base
				ma_path = imap.mbPath(imap_config['mailarchive'])
				typ, data = imap.server.select(imap.quoteMpPath(ma_path), readonly=True)
				if data == b'Mailbox does not exist':
					self.logger.error(msg1011.format(ma_path))
					return True

				# determin destination path
				dst = imap.mbPath(imap_config['mailarchive'] + [self.cn, ])
				timestamp = time.strftime('%Y-%m-%d_%H-%M-%S')

				if imap_config['appendtimestamp']:
					dst += '_' + timestamp
				else:
					# open/close dst mailbox just to check existence
					# possibly add timestamp to avoid conflict
					typ, data = imap.server.select(imap.quoteMpPath(dst), readonly=True)

					if data != b'Mailbox does not exist':
						dst += '_' + timestamp

					if typ == 'OK':
						imap.server.close()

				if not imap.copyMailbox(mailbox_path, dst, move=imap_config['deletemailbox'],
					sub=True, create=True, setacl=True):

					self.logger.error(msg1013.format(mailbox_path, dst))

				elif imap_config['deletemailbox'] and \
					not imap.delete(mailbox_path, sub=True, empty=True):

					self.logger.error(msg1014.format(mailbox_path))

				# force acls from the archive mailbox to the destination
				try:
					ma_acls = imap.getacl(ma_path)[0][1]

				except IndexError:
					self.logger.warn(msg1015.format(mailbox_path))
					return False

				try:
					imap.applyacl(dst, ma_acls, True)

				except TypeError as e:
					self.logger.warn(e)
					return False

				self.logger.info(msg1029.format(mailbox_path, dst))

			elif imap_config['mailarchivemode'] in ('tgz', 'tbz2'):

				# constuct config for _preparePaths
				arc_cfg = {
					'mailarchive' : imap_config,
					}

				# get/mount the archive path
				path = self._preparePaths(arc_cfg, imap_config['mountbase'])

				# ensure success
				try:
					archive_base = path['mailarchive']

				except KeyError:
					self.logger.error(msg1022)
					return False

				# download mailbox to a temp directory
				tmpdir = os.path.join(archive_base, mailbox + '_mail')
				self.logger.debug(msg1023.format(mailbox_path))

				imap.downloadToFile(
					path=mailbox_path,
					thunderbird=True,
					overwrite=False,
					destination=tmpdir
				)

				# tar temp directory
				timestamp = time.strftime('%Y-%m-%d_%H-%M-%S')
				archive = os.path.join(archive_base, mailbox + '_mail_' + timestamp)

				if not self.tarPath(tmpdir, archive, comp=imap_config['mailarchivemode']):
					self.logger.warn(msg1024.format(archive))
					return False

				self.logger.info(msg1030.format(mailbox_path, archive))

				# remove temp directory
				if self.client_l.rm(tmpdir, True):
					self.logger.debug(msg1025.format(tmpdir))
				else:
					self.logger.warn(msg1026.format(tmpdir))
					return False

				# final delete of the remaining mailbox
				if imap_config['deletemailbox']:
					imap.delete(mailbox_path, sub=True, empty=False)
					self.logger.info(msg1031.format(mailbox_path))
				else:
					self.logger.info(msg1032.format(mailbox_path))
			else:
				# just delete the mailbox
				if imap_config['deletemailbox']:
					imap.delete(mailbox_path, sub=True, empty=False)
					self.logger.info(msg1031.format(mailbox_path))
				else:
					self.logger.info(msg1032.format(mailbox_path))

		except (cplImaplib.CfgError,
			cplImaplib.ConnectError,
			cplImaplib.ResponseError,
			cplImaplib.MiscError) as e:
			self.logger.error(str(e))
			return False

		except socket.error as e:
			self.logger.error('ERROR: {0} {1}'.format(*e))
			return False

		except (imaplib.IMAP4.error, imaplib.IMAP4.abort) as e:
			self.logger.error('ERROR: {0}'.format(e))
			return False

		return True

