Exemple #1
0
class PxEmail(object):
    '''
        python helper for email manipulation
        - retrieve email using imap connection
        - sending email using smtp
        - support for ssl connection
    '''
    
    def __init__(self):
        '''
            create imap object constructor
            with no parameter
            imap_user is ImapUser object
        '''
        
        self.__imap_entity = IMAPEntity()
        self.__smtp_entity = SMTPEntity()
        
        self.__imap_local_dir = os.getcwd() + os.path.sep + 'pxemail_cache'
        
        # this will be override when call imap_set_active(host, username)
        self.__active_imap_user = {'host':'', 'username':''}
        self.__active_smtp_user = {'host':'', 'username':''}
        
    ####################################
    ####### IMAP4 FUNCTIONALITY #########
    ####################################
    def imap_add(self, host, username, password, port=imaplib.IMAP4_PORT,
        connection_type=EntityFlag.CONNECTION_PLAIN, keyfile=None, certfile=None, ssl_context=None, force=False):
        '''
            add imap user to __imap_user
            host = '*****@*****.**'
            username = '******'
            password = '******'
            port = 143 (default is imap port or custom port depend on connection preference)
            force = False (default is false, to not override existing user), if exist will return false
                True will force existing user with new configuration
                    
            connection_type = EntityFlag.CONNECTION_PLAIN (if connection using ssl, may need keyfile, certfile, ssl_context parameter)
                value should be one of EntityFlag.CONNECTION_PLAIN|CONNECTION_SSL
        '''
        
        return self.__imap_entity.add(host, username, password, port,
            connection_type, keyfile, certfile, ssl_context, force)
    
    def imap_serialize(self, filename):
        '''
            serialize imap object
            save it as pickle object
        '''
            
        try:
            imap_entity_dump = {}
            imap_entity = self.__imap_entity.get_all()
            
            for entity in imap_entity:
                imap_entity_item = copy.copy(imap_entity.get(entity))
                imap_entity_dump[entity] = {}
                for imap_user in imap_entity_item:
                    imap_entity_dump[entity][imap_user] = imap_entity_item.get(imap_user)
                    imap_entity_dump.get(entity).get(imap_user)['imap'] = None
            
            # serialize imap user         
            Pickler(open(filename + '.imap.entity', 'wb'), protocol=pickle.HIGHEST_PROTOCOL).dump(imap_entity_dump)
            # serialize imap directory
            Pickler(open(filename + '.imap.maildir', 'wb'), protocol=pickle.HIGHEST_PROTOCOL).dump(self.__imap_local_dir)
        except Exception as e:
            print(e)
            
    def imap_unserialize(self, filename):
        '''
            unserialize imap object
            load and init __imap_entity
        '''
        
        try:
            return {'entity':Unpickler(open(filename + '.imap.entity', 'rb')).load(),
                'maildir':Unpickler(open(filename + '.imap.maildir', 'rb')).load()}
        except Exception as e:
            print(e)
            
        return None
        
    def imap_get_user(self, host, username=None):
        '''
            get user imap configuration
            depend on host and username selector
            if configuration not exist will return None
            if username nost set will return list of imap user
        '''
        
        return self.__imap_entity.get(host, username)
        
    def imap_set_active(self, host, username):
        '''
            set current active user
            all imap operation and modification will depend on current active user
        '''
        
        self.__active_imap_user['host'] = host
        self.__active_imap_user['username'] = username
        
    def imap_get(self):
        '''
            get user imap object
            depend on host and username selector
        '''
        
        host = self.imap_get_active().get('host')
        username = self.imap_get_active().get('username')
        return self.__imap_entity.get_imap(host, username)
        
    def imap_is_connected(self):
        '''
            check if conneected to server or not
            online or not
        '''
        
        try:
            imap_get().noop()
            return True
        except Exception:
            return False
            
    def imap_reconnect(self):
        '''
            reconnect imap
            this is force to re login
            and init imap object with new one
            
            but check with imap_is_connected before do force connect
        '''
        
        self.imap_logout()
        
        host = self.imap_get_active().get('host')
        username = self.imap_get_active().get('username')
        imap_user = self.imap_get_user(host, username)
        
        self.imap_add(
            host,
            username,
            imap_user.get('password'),
            imap_user.get('port'),
            imap_user.get('connection_type'),
            imap_user.get('keyfile'),
            imap_user.get('certfile'),
            imap_user.get('ssl_context'),
            force=True)
            
        return self.imap_login()
        
    def imap_get_active(self):
        '''
            get active user
            will return host and user name as dictionary
        '''
        
        return self.__active_imap_user
            
    def imap_is_login(self):
        '''
            check if current active user already execure login command
            if already login return true
        '''
        
        host = self.imap_get_active().get('host')
        username = self.imap_get_active().get('username')
        return self.imap_get_user(host, username).get('is_login')
        
    def imap_login(self):
        '''
            login to imap using host and username
            will search in imap user list
            if not exist will return imap user not exist error code
            if success will return SUCCESS_USER_LOGIN flag
        '''
        
        imap = self.imap_get()
        host = self.imap_get_active().get('host')
        username = self.imap_get_active().get('username')
        password = self.__imap_entity.get(host, username).get('password')
        
        # do login if imap user exist
        if imap:
            try:
                imap.login(username, password)
                self.imap_get_user(host, username)['is_login'] = True
                return EntityFlag.SUCCESS_USER_LOGIN
                
            except imaplib.IMAP4.error as e:
                print(e)
                return EntityFlag.ERROR_USER_LOGIN
            
        return EntityFlag.ERROR_USER_NOT_EXIST
        
    def imap_logout(self):
        '''
            logout from specific imap object
            return
            EntityFlag.SUCCESS_USER_LOGOUT|EntityFlag.SUCCESS_USER_LOGOUT
        '''
        
        try:
            host = self.imap_get_active().get('host')
            username = self.imap_get_active().get('username')
            self.imap_get_user(host, username)['is_login'] = False
            self.imap_get().close()
            self.imap_get().logout()
            self.imap_get().shutdown()
            return EntityFlag.SUCCESS_USER_LOGOUT
        except Exception:
            return EntityFlag.ERROR_USER_LOGOUT
        
        
    def imap_get_list(self):
        '''
            return list of mailbox list
            directory default to top level
            pattern default match to any
        '''
        
        status, msg = self.imap_get().list()
        
        try:
            msg = [mailbox.decode('UTF-8') for mailbox in msg]
        except Exception as e:
            print(e)
        
        return {'status':status, 'msg':msg}
            
    def imap_mailbox_select(self, mailbox='INBOX', readonly=False):
        '''
            select mailbox from imap object
            default is INBOX
        '''
            
        status, msg = self.imap_get().select(mailbox, readonly)
        
        return {'status':status, 'msg':msg}
    
    def imap_get_search(self, *criterion):
        '''
            do search in imap
            get email from imap object
            *criterion is for search criterion ex: 'FROM', '"LDJ"' or '(FROM "LDJ")'
        '''
        
        status, msg = self.imap_get().uid('search', None, *criterion)
        
        email_ids = []
        if len(msg):
            msg_ids = msg[0].decode('UTF-8')
            if msg_ids != '':
                email_ids = msg_ids.split(' ')
        
        return {'status':status, 'msg':email_ids}
        
    def imap_get_fetch(self, email_id, *criterion):
        '''
            do fetch email information
            for specific email_id
        '''
        
        status, msg = self.imap_get().uid('fetch', email_id, *criterion)
        
        return {'status':status, 'msg':msg}
        
    def imap_store_command(self, message_id, command, flag_list):
        '''
            store imap flags
            command should be 'FLAGS', '+FLAGS', '-FLAGS', optionaly with suffix of ."SILENT".
            flag_list must be valid flag '\\Deleted', '\\Seen' etc
        '''
        
        status, msg = self.imap_get().uid('STORE', message_id, command, flag_list)
        
        return {'status':status, 'msg':msg}
    
    def imap_expunge(self):
        '''
            commit all change command to email
            should call this after change email flag
        '''
        
        status, msg = self.imap_get().expunge()
        
        return {'status':status, 'msg':msg}
        
    def imap_get_fetch_header(self, email_id):
        '''
            get header of messages
            return parsed header messages
            parameter is email_id returned from serach result
        '''
        
        # check serialize cache
        email_cache = self.imap_unserialize_email_from_file(email_id)
        if email_cache:
            return {'status':'OK', 'msg':email_cache}
        
        email_info = self.imap_get_fetch(email_id, '(BODY[HEADER])')
        if email_info.get('status').lower() != 'ok':
            return None
        
        header = email_info.get('msg')[0][1].decode('UTF-8') #get header string then decode
        parser = HeaderParser()
        
        # serialize
        parsed_header = parser.parsestr(header)
        serialized_eml = {}
        serialized_eml['ID'] = email_id
        serialized_eml['From'] = parsed_header.get('From')
        serialized_eml['To'] = parsed_header.get('To')
        serialized_eml['CC'] = parsed_header.get('CC')
        serialized_eml['BCC'] = parsed_header.get('BCC')
        serialized_eml['Subject'] = parsed_header.get('Subject')
        serialized_eml['Date'] = parsed_header.get('Date')
        self.imap_serialize_email_to_file(serialized_eml)
            
        return {'status':'OK', 'msg':parsed_header}
        
    def imap_get_fetch_content(self, email_id, download_attachment=False):
        '''
            get email content
            return email content with attachment filename
            format
            {
                'content':'',
                attachment:[
                    {'name':attachment01, 'mime':''},
                    {'name':attachment01, 'mime':''}
                ],
                inline_attachment:[
                    {'name':attachment01, 'mime':''},
                    {'name':attachment01, 'mime':''}
                ]
            }
        '''
        
        cache_dir = self.imap_init_serialize_dir(email_id)
        
        # check serialize cache
        email_cache = self.imap_get_fetch_header(email_id)
        if email_cache and email_cache.get('msg') and email_cache.get('msg').get('Message'):
            return email_cache
        
        email_cache = email_cache.get('msg')
        
        #email_content = {'content':[], 'attachment':[], 'inline_attachment':[]}
        email_cache['Message'] = []
        email_cache['Attachment'] = []
        email_cache['InlineAttachment'] = []
        email_cache['ID'] = email_id
        
        body = self.imap_get_fetch(email_id, '(BODY[])')
        status = body.get('status')
        data = body.get('msg')[0][1]
        email_msg = email.message_from_bytes(data)
        
        if status.lower() != 'ok':
            return None
        
        # check if download_attachment is set
        for part in email_msg.walk():
            if part.get('Content-Disposition') and download_attachment:
                # save attachment
                filename = part.get_filename()
                fp = open(filename, 'wb')
                fp.write(part.get_payload(decode=True))
                fp.close()
                # add attachment filename
                #email_content.get('attachment').append({'name':filename, 'mime':part.get_content_type()})
                email_cache.get('Attachment').append({'name':filename, 'mime':part.get_content_type()})
                
            # if plain text or html and not disposition
            elif part.get_content_type() == 'text/plain' or part.get_content_type() == 'text/html':
                body = part.get_payload(decode=True)
                body = body.decode()
                #email_content.get('content').append(body)
                email_cache.get('Message').append(body)
                    
            # else save as inline attachment
            # elif part.get_content_type().find('image/') != -1 or part.get_content_type().find('video/') != -1 or part.get_content_type().find('application/') != -1 or part.get('Content-Disposition'):
            elif download_attachment:
                # save attachment
                filename = part.get_filename()
                fp = open(cache_dir + os.path.sep + filename, 'wb')
                fp.write(part.get_payload(decode=True))
                fp.close()
                # add attachment filename
                #email_content.get('inline_attachment').append({'name':filename, 'mime':part.get_content_type()})
                email_cache.get('InlineAttachment').append({'name':filename, 'mime':part.get_content_type()})
                # append inline attachment value to content
                #email_content.get('content').append('[pxemail:inline' + filename + ']')
                email_cache.get('Message').append('[pxemail:inline' + filename + ']')
        
        # make sure content is in text
        #email_content['content'] = ''.join(email_content.get('content'))
        email_cache['Message'] = ''.join(email_cache.get('Message'))
        
        #return {'status':status, 'msg':email_content}
        self.imap_serialize_email_to_file(email_cache)
        return {'status':status, 'msg':email_cache}
    
    def imap_set_directory(self, directory):
        '''
            set imap directory
            retrieved email will save to the directory
        '''
        
        self.__imap_local_dir = directory
    
    def imap_init_serialize_dir(self, email_id):
        '''
            check if local directory for serialize already exists
            if not create it
        '''
        
        dir_path = self.__imap_local_dir + os.path.sep + self.__active_imap_user.get('username') + os.path.sep + email_id
        if not os.path.isdir(dir_path):
            os.makedirs(dir_path)
            
        return dir_path
    
    def imap_unserialize_email_from_file(self, email_id):
        '''
            unserialize json from email cache to variable
        '''
        
        dir_path = self.imap_init_serialize_dir(email_id)
        file = dir_path + os.path.sep + email_id
        
        content = None
        
        '''if os.path.exists(file):
            f = open(file, 'r')
            content = f.readline()
            f.close()
            
            if content:
                content = json.loads(content)
        ''' 
        
        try:
            return Unpickler(open(file, 'rb')).load()
            
        except Exception as e:
            print(e)
               
        return content
        
    def imap_serialize_email_to_file(self, email_data):
        '''
            email_data = {
                'Subject':'',
                'CC':'',
                'BCC':'',
                'Attachment':[],
                'InlineAttachment':[],
                'ID':''
            }
        '''
        
        try:
            dir_path = self.imap_init_serialize_dir(email_data.get('ID'))
            file = dir_path + os.path.sep + email_data.get('ID')
            Pickler(open(file, 'wb'), protocol=pickle.HIGHEST_PROTOCOL).dump(email_data)
            
        except Exception as e:
            print(e)
                
    def is_email_serialized(self, email_id):
        '''
            check email data is alrady exists or not
        '''
        
        return os.path.exists(self.__imap_local_dir + os.path.sep + self.__active_imap_user.get('username') + os.path.sep + email_id)
        
    ####################################
    ####### SMTP FUNCTIONALITY #########
    ####################################
    def smtp_add(self, host, username, password, port=smtplib.SMTP_PORT, local_hostname=None, source_address=None,
        connection_type=EntityFlag.CONNECTION_PLAIN, keyfile=None, certfile=None, context=None, force=False):
        '''
            add smtp user to __smtp_user
            host = '*****@*****.**'
            username = '******'
            password = '******'
            port = 25 (default is imap port or custom port depend on connection preference)
            local_hostname = If specified, local_hostname is used as the FQDN of the local host in the HELO/EHLO command.
                Otherwise, the local hostname is found using socket.getfqdn().
            source_address = If the connect() call returns anything other than a success code, an SMTPConnectError is raised
                The optional source_address parameter allows to bind to some specific source address in a machine with multiple network interfaces, and/or to some specific source TCP port.
                It takes a 2-tuple (host, port), for the socket to bind to as its source address before connecting.
                If omitted (or if host or port are '' and/or 0 respectively) the OS default behavior will be used.    
            force = False (default is false, to not override existing user), if exist will return false
                    True will force existing user with new configuration
                    
            connection_type = EntityFlag.CONNECTION_PLAIN (if connection using ssl, may need keyfile, certfile, ssl_context parameter)
                value should be one of connection_type=EntityFlag.CONNECTION_PLAIN|connection_type=EntityFlag.CONNECTION_SSL|connection_type=EntityFlag.CONNECTION_LMTP
        '''
        
        return self.__smtp_entity.add(host, username, password, port, local_hostname, source_address,
            connection_type, keyfile, certfile, context, force)
    
    def smtp_serialize(self, filename):
        '''
            serialize smtp object
            save it as pickle object
        '''
            
        try:
            smtp_entity_dump = {}
            smtp_entity = self.__smtp_entity.get_all()
            
            for entity in smtp_entity:
                smtp_entity_item = copy.copy(smtp_entity.get(entity))
                smtp_entity_dump[entity] = {}
                for smtp_user in smtp_entity_item:
                    smtp_entity_dump[entity][smtp_user] = smtp_entity_item.get(smtp_user)
                    smtp_entity_dump.get(entity).get(smtp_user)['smtp'] = None
            
            # serialize smtp user         
            Pickler(open(filename + '.smtp.entity', 'wb'), protocol=pickle.HIGHEST_PROTOCOL).dump(smtp_entity_dump)
        except Exception as e:
            print(e)
            
    def smtp_unserialize(self, filename):
        '''
            unserialize smtp object
            load and init __smtp_entity
        '''
        
        try:
            return {'entity':Unpickler(open(filename + '.smtp.entity', 'rb')).load()}
        except Exception as e:
            print(e)
            
        return None
            
    def smtp_get_user(self, host, username=None):
        '''
            get user smtp configuration
            depend on host and username selector
            if configuration not exist will return None
            if username host set will return list of smtp user
        '''
        
        return self.__smtp_entity.get(host, username)
        
    def smtp_get(self):
        '''
            get user smtp object
            depend on host and username selector
        '''
        
        host = self.smtp_get_active().get('host')
        username = self.smtp_get_active().get('username')
        return self.__smtp_entity.get_smtp(host, username)
        
    def smtp_set_active(self, host, username):
        '''
            set current active user
            all smtp operation and modification will depend on current active user
        '''
        
        self.__active_smtp_user['host'] = host
        self.__active_smtp_user['username'] = username
        
    def smtp_get_active(self):
        '''
            get active user
            will return host and user name as dictionary
        '''
        
        return self.__active_smtp_user
        
    def smtp_login(self):
        '''
            login to smtp using host and username
            will search in imap user list
            if not exist will return smtp user not exist error code
            if success will return SUCCESS_USER_LOGIN flag
        '''
        
        smtp = self.smtp_get()
        
        host = self.smtp_get_active().get('host')
        username = self.smtp_get_active().get('username')
        password = self.__smtp_entity.get(host, username).get('password')
        
        # do login if imap user exist
        if smtp:
            try:
                smtp.login(username, password)
                self.smtp_get_user(host, username)['is_login'] = True
                return EntityFlag.SUCCESS_USER_LOGIN
                
            except smtplib.SMTPException as e:
                print(e)
                return EntityFlag.ERROR_USER_LOGIN
            
        return EntityFlag.ERROR_USER_NOT_EXIST
        
    def smtp_logout(self):
        '''
            logout from specific smtp object
            return
            EntityFlag.SUCCESS_USER_LOGOUT|EntityFlag.SUCCESS_USER_LOGOUT
        '''
        
        try:
            host = self.smtp_get_active().get('host')
            username = self.smtp_get_active().get('username')
            self.smtp_get_user(host, username)['is_login'] = False
            self.smtp_get().close()
            self.smtp_get().logout()
            self.smtp_get().shutdown()
            return EntityFlag.SUCCESS_USER_LOGOUT
        except Exception:
            return EntityFlag.ERROR_USER_LOGOUT
            
    def smtp_send_message(self, message):
        '''
            send message with option
            message = implementation of MessageBuilder object
            else is optional message which is like send_message method from smptlib
        '''
        
        self.smtp_get().send_message(message.generate(), from_addr=None, to_addrs=None, mail_options=message.get_mail_options(), rcpt_options=message.get_rcpt_options())