Exemple #1
0
class EmailOperation(object):
    def __init__(self, user_name, pwd):
        print("使用imapclient,操作邮件...")
        self.server = IMAPClient('imap.139.com', use_uid=True, ssl=False)
        self.username = user_name
        self.password = pwd

    def logout(self):
        self.server.logout()

    # 删除邮件
    def _del(self, messages):
        #         print('del message')
        self.server.delete_messages(messages)
        self.server.expunge()

    def _mv(self, message, folder):
        self.server.copy(message, folder)
        time.sleep(1)
        self._del(message)

    def _into(self, box):
        # 'INBOX','草稿箱','已发送','已删除','100'
        self.server.select_folder(box)

    def _get_uids(self):
        # 获取序列
        uids = self.server.search(['NOT', 'DELETED'])
        return uids

    def email_body(self, mss):
        # 获取某序列id的原始信息
        raw_messages = self.server.fetch([mss], ['BODY[]', 'FLAGS'])
        #         pprint.pprint(rawMessages)
        # 使用pyzmail,返回主体信息
        message = pyzmail.PyzMessage.factory(raw_messages[mss][b'BODY[]'])

        # 如果信息为空,返回None
        if not message:
            return None
        # 获取正文内容
        if message.text_part != None:
            lines = message.text_part.get_payload().decode(
                message.text_part.charset)

        str = ''
        # 去除内容中的回车
        for line in lines:
            if line not in ['\n', '\r']:
                str += line

        print(str)
        # body自动
        body = {
            'subject': message.get_subject(),
            'from': message.get_address('from'),
            'to': message.get_address('to'),
            'mainbody': str
        }
        return body

    def del_all_message(self, folder):
        '''清空某个文件夹下的所有邮件'''
        self._into(folder)

        uids = self._get_uids()
        if len(uids) == 0:
            return
        else:
            self._del(uids)

    def del_new_message(self, fforlder):
        '''把最近一封新邮件已到目的目录'''
        self._into(fforlder)

        uids = self._get_uids()

        if len(uids) == 0:
            return
        else:
            self._del(uids[-1])

    def move_all_message_to_folder(self, fforlder, tforlder):
        '''把maessage/messages 从 ffolder 移动到 tforlder'''

        self._into(fforlder)

        uids = self._get_uids()
        # 邮件数量为空
        if len(uids) == 0:
            print("%s 数量为:0,该操作无效" % fforlder)
            return True
        self._mv(uids, tforlder)

    def check_new_message(self):
        '''判断最新一封邮件,是否包含某个字段,显示邮件数量'''
        self._into("INBOX")

        uids = self._get_uids()

        print('current INBOX email: %s' % str(len(uids)))

        if len(uids) in [0, 1]:
            print("不执行,目前邮件数量为:%r" % len(uids))
            return

        if self.email_body(uids[-1])['subject'] == 'testReceive':
            self._del(uids[-1])

    def delete_newest_mail(self):
        '''删除最新的一封邮件'''
        try:
            is_true = False
            self.server.login(self.username, self.password)
            self.del_new_message('INBOX')
            is_true = True
        except BaseException as error:
            print(error)
            print("删除邮件可能出现错误")
        finally:
            self.logout()
            return is_true

    def clear_forlder(self, l=[]):
        '''清空邮箱某个文件夹'''
        '''
        sample:
        clearForlder(['100', 'INBOX'])
        '''
        is_true = False
        if len(l) == 0:
            return is_true
        try:
            self.server.login(self.username, self.password)
            for f in l:
                print("clear Forlder: %s" % f)
                self.del_all_message(f)
                time.sleep(1)

            is_true = True
        except BaseException as error:
            print(error)
            print("删除邮件可能出现错误")
        finally:
            self.logout()
            return is_true

    def move_forlder(self, l=[]):
        '''移动邮件
        sample:
            moveForlder(['100', 'INBOX'])
        '''
        is_true = False
        if len(l) == 0:
            return is_true
        try:
            self.server.login(self.username, self.password)
            self.move_all_message_to_folder(l[0], l[1])
            print("移动邮件成功:%s => %s" % (l[0], l[1]))
            is_true = True
        except BaseException as error:
            print(error)
            print("清空邮箱某个文件夹可能出现错误")
        finally:
            self.logout()
            return is_true

    def check_inbox_cnt(self):
        '''获取邮件数量'''
        try:
            is_true = 0
            self.server.login(self.username, self.password)

            self._into("INBOX")
            uids = self._get_uids()
            # 数量为 0
            if len(uids) == 0:
                return 0
            # 判断
            if len(uids) == 100:
                print("100封邮件")
                return 0
            elif len(uids) < 100:
                print('邮件数量少于100封')
                return 0
            else:
                cnt = len(uids) - 100
                print('需要删除邮件数量为:%d' % cnt)
                is_true = cnt
        except BaseException as error:
            print(error)
            print("删除邮件可能出现错误")
        finally:
            self.logout()
            return is_true

    def check_inbox(self):
        '''确保收件箱有100封邮件'''
        try:
            is_true = True
            self.server.login(self.username, self.password)

            self._into("INBOX")
            uids = self._get_uids()
            all = len(uids)
            # 数量为 0
            if all == 0:
                return False
            # 判断
            if all == 100:
                print("100封邮件")
                return is_true
            elif all < 100:
                print('邮件数量少于100封')
                return False
            else:
                print('需要删除邮件数量为:%d' % (all - 100))
                #                 print(Uids[100:])
                self._del(uids[100:])
                return is_true
        except BaseException as error:
            print(error)
            print("删除邮件可能出现错误")
            is_true = False
        finally:
            self.logout()
            return is_true

    def seen(self):
        '''将收件箱邮件,标记已读'''
        self.server.login(self.username, self.password)
        self._into("INBOX")
        # 搜索 未读邮件
        typ = self.server.search([u'UNSEEN'])
        # 把邮件改为已读
        for num in typ:
            print(num)
            self.server.set_flags(num, [u'Seen'])
Exemple #2
0
class EmailClient(object):
    def __init__(self, config):
        """
        Create an email client abstraction configured for an account.

        config is a dict with keys:
            HOST
            USERNAME
            PASSWORD
            USE_SSL default=True
            FOLDER default='INBOX'
            SELECTORS default=['NOT DELETED']
        """
        self.config = {
            'USE_SLL': True,
            'FOLDER': 'INBOX',
            'SELECTORS': ['NOT DELETED']
        }
        self.config.update(config)

    def connect(self):
        self.server = IMAPClient(self.config['HOST'],
                                 use_uid=True,
                                 ssl=self.config['USE_SSL'])
        self.server.login(self.config['USERNAME'], self.config['PASSWORD'])
        select_info = self.server.select_folder(self.config['FOLDER'])

    def process_new_messages(self, debug=False):
        """
        Get mesages from the server.

        Note: new information on existing uids will be ignored.
        For example, if the rfc_size changes (which is a strangely common occurrence),
        the new value will be ignored.
        """
        if debug: print "searching"
        messages = self.server.search(self.config['SELECTORS'])
        if debug: print "done searching"
        self._process_messages(messages, debug=debug)

    def _process_messages(self, uids, move=False, debug=False):
        if debug: print "fetching"
        response = self.server.fetch(uids, ['FLAGS', 'RFC822', 'RFC822.SIZE'])
        for msg_uid, data in response.iteritems():
            if debug: print "processing %s" % msg_uid
            with transaction.commit_on_success():
                # extract data
                flags = data['FLAGS']
                rfc_size = data['RFC822.SIZE']
                raw_body = data['RFC822']
                seq = data['SEQ']

                # save objects
                email, _ = EmailMessage.objects.get_or_create(
                    uid=msg_uid,
                    username=self.config['USERNAME'],
                    host=self.config['HOST'],
                    defaults={
                        'raw_body': raw_body,
                        'rfc_size': rfc_size,
                        'seq': data['SEQ'],
                    })

                email.save()

                for flag in flags:
                    EmailFlag.objects.get_or_create(email=email,
                                                    flag=flag)[0].save()

                # move message
                if move:
                    self.server.copy([msg_uid], 'auto_processed')
                    self.server.delete_messages([msg_uid])
Exemple #3
0
class IMAP():
    """
    Central class for IMAP server communication
    """
    Retval = namedtuple('Retval', 'code data')

    def __init__(self, logger, username, password,
                 server='localhost',
                 port=143,
                 starttls=False,
                 imaps=False,
                 tlsverify=True,
                 test=False,
                 timeout=None):
        self.logger = logger
        self.username = username
        self.password = password
        self.server = server
        self.port = port
        self.imaps = imaps
        self.starttls = starttls
        self.timeout = timeout

        self.sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)  # TODO add proto arg
        if tlsverify:
            self.sslcontext.verify_mode = ssl.CERT_REQUIRED
        else:
            self.sslcontext.verify_mode = ssl.CERT_NONE  # TODO improve?

        self.test = test

        self.conn = None

    def do_select_mailbox(func):
        """
        Decorator to do a fresh mailbox SELECT
        """

        def wrapper(*args, **kwargs):
            if len(args) != 1:
                raise AttributeError(
                    'Size of *args tuple "{0}" isn\'t 1. It looks like you haven\'t specified all '
                    'method arguments as named arguments!'.format(
                        args))

            mailbox = None
            for key in ['mailbox', 'source']:
                if key in kwargs.keys():
                    mailbox = kwargs[key]
                    break

            if mailbox is None:
                raise KeyError('Unable to SELECT a mailbox, kwargs "{0}" doesn\'t contain a mailbox name'.format(kwargs))

            result = args[0].select_mailbox(mailbox)
            if not result.code:
                raise RuntimeError(result.data)
            return func(*args, **kwargs)

        return wrapper

    def process_error(self, exception, simple_return=False):
        """
        Process Python exception by logging a message and optionally showing traceback
        """
        trace_info = exc_info()
        err_msg = str(exception)

        if isinstance(exception, IMAPClient.Error):
            err_msg = Helper().byte_to_str(exception.args[0])

        self.logger.error('Catching IMAP exception %s: %s', type(exception), err_msg)

        if self.logger.isEnabledFor(loglevel_DEBUG):
            print_exception(*trace_info)
        del trace_info

        if simple_return:
            return exception
        else:
            return self.Retval(False, err_msg)

    def connect(self, retry=True, logout=False):
        """
        Connect to IMAP server and login
        """
        if self.starttls:
            self.logger.debug('Establishing IMAP connection using STARTTLS/%s to %s and logging in with user %s', self.port, self.server,
                              self.username)
        elif self.imaps:
            self.logger.debug('Establishing IMAP connection using SSL/%s (imaps) to %s and logging in with user %s', self.port, self.server,
                              self.username)

        login = ''
        err_return = None
        try:
            self.conn = IMAPClient(host=self.server,
                                   port=self.port,
                                   use_uid=True,
                                   ssl=self.imaps,
                                   ssl_context=self.sslcontext,
                                   timeout=self.timeout)

            if self.starttls:
                self.conn.starttls(ssl_context=self.sslcontext)

            login = self.conn.login(self.username, self.password)
            login_response = Helper().byte_to_str(login)

            # Test login/auth status
            login_success = False
            noop = self.noop()
            if noop.code and noop.data:
                login_success = True

            if logout:
                return self.disconnect()
            elif login_success:
                return self.Retval(True, login_response)
            else:
                return self.Retval(False, login_response)  # pragma: no cover

        except Exception as e:
            err_return = self.process_error(e)

            if err_return.data == '[AUTHENTICATIONFAILED] Authentication failed.':
                return err_return

            if retry:
                self.logger.error('Trying one more time to login')
                sleep(2)
                return self.connect(retry=False, logout=logout)
            return err_return

    def noop(self):
        """
        Do a noop to test login status
        """
        try:
            noop = self.conn.noop()
            noop_response = Helper().byte_to_str(noop[0])
            noop_resp_pattern_re = regex_compile('^(Success|NOOP completed)')
            login_success = noop_resp_pattern_re.match(noop_response)
            return self.Retval(True, login_success)
        except IMAPClient.Error as e:
            return self.process_error(e)

    def disconnect(self):
        """
        Disconnect from IMAP server
        """
        result = self.conn.logout()
        response = Helper().byte_to_str(result)
        return self.Retval(response == 'Logging out', response)

    def list_mailboxes(self, directory='', pattern='*'):
        """
        Get a listing of folders (mailboxes) on the server
        """
        try:
            raw_list = self.conn.list_folders(directory, pattern)
            nice_list = []

            for mailbox in raw_list:
                flags = []
                for flag in mailbox[0]:
                    flags.append(flag.decode('utf-8'))

                nice_list.append({'name': mailbox[2], 'flags': flags, 'delimiter': mailbox[1].decode("utf-8")})
            return self.Retval(True, nice_list)
        except IMAPClient.Error as e:
            return self.process_error(e)

    def select_mailbox(self, mailbox):
        """
        Select a mailbox to work on
        """
        self.logger.debug('Switching to mailbox %s', mailbox)
        try:
            result = self.conn.select_folder(mailbox)
            response = {}
            for key, value in result.items():
                unicode_key = Helper().byte_to_str(key)
                if unicode_key == 'FLAGS':
                    flags = []
                    for flag in value:
                        flags.append(Helper().byte_to_str(flag))
                    response[unicode_key] = tuple(flags)
                else:
                    response[unicode_key] = value
            return self.Retval(True, response)
        except IMAPClient.Error as e:
            return self.process_error(e)

    def add_mail(self, mailbox, message, flags=(), msg_time=None):
        """
        Add/append a mail to a mailbox
        """
        self.logger.debug('Adding a mail into mailbox %s', mailbox)
        try:
            if not isinstance(message, Mail):
                message = Mail(logger=self.logger, mail_native=message)
            message_native = message.get_native()

            #self.conn.append(mailbox, message, flags, msg_time)
            self._append(mailbox, str(message_native), flags, msg_time)

            # According to rfc4315 we must not return the UID from the response, so we are fetching it ourselves
            uids = self.search_mails(mailbox=mailbox, criteria='HEADER Message-Id "{0}"'.format(message.get_header('Message-Id'))).data[0]

            return self.Retval(True, uids)
        except IMAPClient.Error as e:
            return self.process_error(e)

    @do_select_mailbox
    def search_mails(self, mailbox, criteria='ALL', autocreate_mailbox=False):
        """
        Search for mails in a mailbox
        """
        self.logger.debug('Searching for mails in mailbox %s and criteria=\'%s\'', mailbox, criteria)
        try:
            return self.Retval(True, list(self.conn.search(criteria=criteria)))
        except IMAPClient.Error as e:
            return self.process_error(e)

    @do_select_mailbox
    def fetch_mails(self, uids, mailbox, return_fields=None):
        """
        Retrieve mails from a mailbox
        """
        self.logger.debug('Fetching mails with uids %s', uids)

        return_raw = True
        if return_fields is None:
            return_raw = False
            return_fields = [b'RFC822']

        mails = {}
        try:
            for uid in uids:
                result = self.conn.fetch(uid, return_fields)

                if not result:
                    continue

                if return_raw:
                    mails[uid] = result[uid]
                else:
                    #mails[uid] = Mail(logger=self.logger, uid=uid, mail_native=email.message_from_bytes(result[uid][b'RFC822']))
                    mails[uid] = Mail(logger=self.logger, mail_native=email.message_from_bytes(result[uid][b'RFC822']))
            return self.Retval(True, mails)

        except IMAPClient.Error as e:
            return self.process_error(e)

    @do_select_mailbox
    def get_mailflags(self, uids, mailbox):
        """
        Retrieve flags from mails
        """
        try:
            result = self.conn.get_flags(uids)
            flags = {}

            for uid in uids:
                flags[uid] = []
                if uid not in result.keys():
                    self.logger.error('Failed to get flags for mail with uid=%s: %s', uid, result)
                    return self.Retval(False, None)
                for flag in result[uid]:
                    flags[uid].append(flag.decode('utf-8'))
            return self.Retval(True, flags)

        except IMAPClient.Error as e:
            return self.process_error(e)

    @do_select_mailbox
    def set_mailflags(self, uids, mailbox, flags=[]):
        """
        Set and retrieve flags from mails
        """
        if self.test:
            self.logger.info('Would have set mail flags on message uids "%s"', str(uids))
            return self.Retval(True, None)
        else:
            self.logger.debug('Setting flags=%s on mails uid=%s', flags, uids)
            try:
                result = self.conn.set_flags(uids, flags)

                _flags = {}
                for uid in uids:
                    _flags[uid] = []
                    if uid not in result.keys():
                        self.logger.error('Failed to set and get flags for mail with uid=%s: %s', uid, result)
                        return self.Retval(False, None)
                    for flag in result[uid]:
                        _flags[uid].append(flag.decode('utf-8'))
                return self.Retval(True, _flags)
            except IMAPClient.Error as e:
                return self.process_error(e)

    @do_select_mailbox
    def add_mailflags(self, uids, mailbox, flags=[]):
        """
        Add and retrieve flags from mails
        """
        if self.test:
            self.logger.info('Would have added mail flags on message uids "%s"', str(uids))
            return self.Retval(True, None)
        else:
            self.logger.debug('Adding flags=%s on mails uid=%s', flags, uids)
            try:
                result = self.conn.add_flags(uids, flags)

                _flags = {}
                for uid in uids:
                    _flags[uid] = []
                    if uid not in result.keys():
                        self.logger.error('Failed to add and get flags for mail with uid=%s: %s', uid, result)
                        return self.Retval(False, None)
                    for flag in result[uid]:
                        _flags[uid].append(flag.decode('utf-8'))
                return self.Retval(True, _flags)
            except IMAPClient.Error as e:
                return self.process_error(e)

    @do_select_mailbox
    def move_mail(self, message_ids, source, destination, delete_old=True, expunge=True, add_flags=None, set_flags=None):
        """
        Move a mail from a mailbox to another
        """
        return self.copy_mails(message_ids=message_ids,
                               source=source,
                               destination=destination,
                               delete_old=delete_old,
                               expunge=expunge,
                               add_flags=add_flags,
                               set_flags=set_flags)

    @do_select_mailbox
    def copy_mails(self, source, destination, message_ids=None, delete_old=False, expunge=False, add_flags=None, set_flags=None):
        """
        Copies one or more mails from a mailbox into another
        """
        if self.test:
            if delete_old:
                self.logger.info('Would have moved mail Message-Ids="%s" from "%s" to "%s", skipping because of beeing in testmode',
                                 message_ids, source, destination)
            else:
                self.logger.info('Would have copied mails with Message-Ids="%s" from "%s" to "%s", skipping because of beeing in testmode',
                                 message_ids, source, destination)
            return self.Retval(True, None)
        else:
            try:
                if delete_old:
                    self.logger.debug('Moving mail Message-Ids="%s" from "%s" to "%s"', message_ids, source, destination)
                else:
                    self.logger.debug('Copying mail Message-Ids="%s" from "%s" to "%s"', message_ids, source, destination)

                #if message_ids is None:
                #    message_ids = []
                #    result = self.fetch_mails(uids=uids, mailbox=source)
                #    if not result.code:
                #        self.logger.error('Failed to determine Message-Id by uids for mail with uids "%s"', uids)
                #        return result
                #    message_ids.append(result.data.keys())

                if not self.mailbox_exists(destination).data:
                    self.logger.info('Destination mailbox %s doesn\'t exist, creating it for you', destination)

                    result = self.create_mailbox(mailbox=destination)
                    if not result.code:
                        self.logger.error('Failed to create the mailbox %s: %s', source, result.data)  # pragma: no cover
                        return result  # pragma: no cover

                uids = []
                for message_id in message_ids:
                    result = self.search_mails(mailbox=source, criteria='HEADER Message-Id "{0}"'.format(message_id))

                    if not result.code or len(result.data) == 0:
                        self.logger.error('Failed to determine uid by Message-Id for mail with Message-Id "%s"', message_id)
                        return self.Retval(False, result.data)
                    uids.append(result.data[0])

                result = self.select_mailbox(source)
                if not result.code:
                    return result  # pragma: no cover

                self.conn.copy(uids, destination)

                if delete_old:
                    result = self.delete_mails(uids=uids, mailbox=source)
                    if not result.code:
                        self.logger.error('Failed to remove old mail with Message-Id="%s"/uids="%s": %s', message_ids, uids,
                                          result.data)  # pragma: no cover
                        return result  # pragma: no cover

                    if expunge:  # TODO don't expunge by default
                        result = self.expunge(mailbox=source)
                        if not result.code:
                            self.logger.error('Failed to expunge on mailbox %s: %s', source, result.data)  # pragma: no cover
                            return result  # pragma: no cover

                dest_uids = []
                for message_id in message_ids:
                    result = self.search_mails(mailbox=destination, criteria='HEADER Message-Id "{0}"'.format(message_id))
                    if not result.code:
                        self.logger.error('Failed to determine uid by Message-Id for mail with Message-Id "%s"',
                                          message_id)  # pragma: no cover
                        return result  # pragma: no cover
                    dest_uids.append(result.data[0])

                if isinstance(set_flags, list):
                    self.set_mailflags(uids=dest_uids, mailbox=destination, flags=set_flags)
                if add_flags:
                    self.add_mailflags(uids=dest_uids, mailbox=destination, flags=add_flags)

                return self.Retval(True, dest_uids)

            except IMAPClient.Error as e:
                return self.process_error(e)

    def _append(self, folder, msg, flags=(), msg_time=None):  # TODO
        """
        FORKED FORM IMAPCLIENT
        """
        if msg_time:
            if not msg_time.tzinfo:  # pragma: no cover
                msg_time = msg_time.replace(tzinfo=FixedOffset.for_system())  # pragma: no cover

            time_val = '"{0}"'.format(msg_time.strftime("%d-%b-%Y %H:%M:%S %z"))
            time_val = imapclient.imapclient.to_unicode(time_val)
        else:
            time_val = None

        return self.conn._command_and_check('append', self.conn._normalise_folder(folder), imapclient.imapclient.seq_to_parenstr(flags),
                                            time_val, Helper.str_to_bytes(msg),
                                            unpack=True)

    @do_select_mailbox
    def expunge(self, mailbox):
        """
        Expunge mails form a mailbox
        """
        self.logger.debug('Expunge mails from mailbox %s', mailbox)
        try:
            return self.Retval(True, b'Expunge completed.' in self.conn.expunge())
        except IMAPClient.Error as e:  # pragma: no cover
            return self.process_error(e)  # pragma: no cover

    def create_mailbox(self, mailbox):
        """
        Create a mailbox
        """
        self.logger.debug('Creating mailbox %s', mailbox)
        try:
            return self.Retval(True, self.conn.create_folder(mailbox) == b'Create completed.')
        except IMAPClient.Error as e:
            return self.process_error(e)

    def mailbox_exists(self, mailbox):
        """
        Check whether a mailbox exists
        """
        try:
            return self.Retval(True, self.conn.folder_exists(mailbox))
        except IMAPClient.Error as e:  # pragma: no cover
            return self.process_error(e)  # pragma: no cover

    @do_select_mailbox
    def delete_mails(self, uids, mailbox):
        """
        Delete mails
        """
        self.logger.debug('Deleting mails with uid="%s"', uids)
        try:
            result = self.conn.delete_messages(uids)
            flags = {}

            for uid in uids:
                flags[uid] = []
                if uid not in result.keys():
                    self.logger.error('Failed to get flags for mail with uid=%s after deleting it: %s', uid, result)
                    return self.Retval(False, None)
                for flag in result[uid]:
                    flags[uid].append(flag.decode('utf-8'))
            return self.Retval(True, flags)
        except IMAPClient.Error as e:
            return self.process_error(e)
by_size = sorted(list(response.viewitems()), key=lambda m: m[1]['RFC822.SIZE'])

sizes = [ m[1]['RFC822.SIZE'] for m in by_size ]
# select msgs that are bigger than threshold
bigger = bisect.bisect_right(sizes, options.threshold) # index of the first message bigger than threshold
big_messages = by_size[bigger:]

print "There are %d messages bigger than %s" % (len(big_messages), options.threshold)

big_uids = [ msg[0] for msg in big_messages ]

def print_messages():
    print "\n--- Messages bigger than {}: ".format(options.threshold).ljust(80, '-')
    print "Format: <date> | <size> | <from> | <subject>"
    for msgid, data in big_messages:
        headers = email.message_from_string(data['RFC822.HEADER'])
        size = data['RFC822.SIZE']
        date = data['INTERNALDATE']
        print "{} | {} | {} | {}".format(date, size, headers['from'], headers['subject'])

if not options.no_label:
    print "Labeling big messages with label %s" % options.label
    server.create_folder(options.label)
    server.copy(big_uids, options.label)
    print "Your messages larger than {} bytes have been labeled with label {}".format(options.threshold, options.label)

if options.print_msgs or options.no_label:
    print_messages()


class SourceDriverMail(SourceDriver):
    def __init__(self, instanceName: str, settings: Settings,
                 parser: MessageParser) -> None:
        super().__init__("mail", instanceName, settings, parser)
        # Settings
        self.__server = self.getSettingString("server", "")
        self.__user = self.getSettingString("user", "")
        self.__password = self.getSettingString("password", "")
        self.__ssl = self.getSettingBoolean("ssl", True)
        self.__fix_weak_dh = self.getSettingBoolean("fix_weak_dh", False)
        self.__allowlist = self.getSettingList("allowlist", [])
        self.__denylist = self.getSettingList("denylist", [])
        self.__cleanup = self.getSettingString("cleanup", "")
        self.__archive_folder = self.getSettingString("archive_folder",
                                                      "Archive")

        if self.__cleanup == "Delete":
            self.print("Cleanup strategy is delete")
        elif self.__cleanup == "Archive":
            self.print("Cleanup strategy is archive")
        elif self.__cleanup == "":
            self.print("Cleanup is disabled")
        else:
            self.fatal("Unknown cleanup strategy")

        # Internal
        self.__healthy = False

        self.__connect()

    def retrieveEvent(self) -> Optional[SourceEvent]:
        try:
            if self.isDebug():
                self.print("Checking for new mails")
            messages = self.__imap_client.search('UNSEEN')
            for uid, message_data in self.__imap_client.fetch(
                    messages, "RFC822").items():
                message = email.message_from_bytes(message_data[b"RFC822"],
                                                   policy=policy.default)
                sender = parseaddr(message.get("From"))[1]
                sourceEvent = SourceEvent()
                sourceEvent.source = SourceEvent.SOURCE_MAIL
                sourceEvent.timestamp = datetime.datetime.strptime(
                    message.get('Date'), "%a, %d %b %Y %H:%M:%S %z").strftime(
                        SourceEvent.TIMESTAMP_FORMAT)
                sourceEvent.sender = sender
                sourceEvent.raw = message.get_body(('plain', )).get_content()
                if self.isSenderAllowed(allowlist=self.__allowlist,
                                        denylist=self.__denylist,
                                        sender=sender):
                    parsedSourceEvent = self.parser.parseMessage(
                        sourceEvent, None)  # type: ignore[union-attr]
                    self.__do_cleanup(uid)
                    return parsedSourceEvent
                else:
                    self.error("Received unhandled message (ignored sender)")
                    return UnhandledEvent.fromSourceEvent(
                        sourceEvent, UnhandledEvent.CAUSE_IGNORED_SENDER)
        except (timeout, OSError) as e:
            self.error("Connection to mailserver timed out")
            self.__healthy = False
            self.__connect()

    def getSourceState(self) -> SourceState:
        if self.__healthy:
            return SourceState.OK
        return SourceState.ERROR

    def __connect(self):
        if self.__fix_weak_dh:
            context = ssl.SSLContext(
                ssl.PROTOCOL_TLSv1_2)  # Workaround for weak dh key
            context.set_ciphers('DEFAULT@SECLEVEL=1')
        else:
            context = ssl.SSLContext()

        try:
            if self.isDebug():
                self.print("Connecting to server {}".format(self.__server))
            self.__imap_client = IMAPClient(self.__server,
                                            use_uid=True,
                                            ssl=self.__ssl,
                                            ssl_context=context,
                                            timeout=1.0)
        except gaierror:
            self.error("Failed to connect to Mail Server")
        except ssl.SSLError:
            self.fatal("Failed to connect to Mail Server (TLS Error)")
        else:
            try:
                if self.isDebug():
                    self.print("Login as user {}".format(self.__user))
                self.__imap_client.login(self.__user, self.__password)
            except LoginError:
                self.error("Mail Server login failed")
            else:
                if self.isDebug():
                    self.print("Login successful")
                self.__healthy = True
                self.__imap_client.select_folder('INBOX', readonly=False)

    def __create_imap_folder(self, folder):
        if not self.__imap_client.folder_exists(folder):
            self.print("Folder {} does not exist creating")
            self.__imap_client.create_folder(folder)

    def __do_cleanup(self, uid):
        if self.__cleanup == "Archive":
            self.__create_imap_folder(self.__archive_folder)
            self.__imap_client.copy(uid, self.__archive_folder)
        if self.__cleanup == "Delete" or self.__cleanup == "Archive":
            self.__imap_client.delete_messages(uid)
            self.__imap_client.expunge(uid)
Exemple #6
0
class MailToolbox(SourceFactory):
    def __init__(self,
                 hote_imap,
                 nom_utilisateur,
                 mot_de_passe,
                 dossier_cible='INBOX',
                 verify_peer=True,
                 use_secure_socket=True,
                 legacy_secure_protocol=False):

        super().__init__('IMAPFactory via {}'.format(hote_imap))

        self._ssl_context = SSLContext(
            protocol=PROTOCOL_TLS) if use_secure_socket else None
        self._use_secure_socket = use_secure_socket

        if verify_peer is False and use_secure_socket is True:
            # don't check if certificate hostname doesn't match target hostname
            self._ssl_context.check_hostname = False
            # don't check if the certificate is trusted by a certificate authority
            self._ssl_context.verify_mode = CERT_NONE

        if legacy_secure_protocol and use_secure_socket:
            self._ssl_context.options = OP_ALL

        self._hote_imap = Session.UNIVERSELLE.retranscrire(hote_imap)
        self._client = IMAPClient(host=self._hote_imap,
                                  port=993 if self._use_secure_socket else 143,
                                  ssl=self._use_secure_socket,
                                  ssl_context=self._ssl_context)
        self._nom_utilisateur = Session.UNIVERSELLE.retranscrire(
            nom_utilisateur)
        self._verify_peer = verify_peer
        self._dossier_cible = dossier_cible
        self._mot_de_passe = mot_de_passe

        self._client.login(
            self._nom_utilisateur,
            Session.UNIVERSELLE.retranscrire(self._mot_de_passe))

        self._echec = False

        MailToolbox.INSTANCES.append(self)

    @property
    def est_hors_service(self):
        return self._echec

    def reset(self):

        try:
            self._client.logout()
        except IMAPClientError as e:
            pass
        except OSError as e:
            pass

        try:
            self._client = IMAPClient(
                host=self._hote_imap,
                port=993 if self._use_secure_socket else 143,
                ssl=self._use_secure_socket,
                ssl_context=self._ssl_context)
            self._client.login(
                self._nom_utilisateur,
                Session.UNIVERSELLE.retranscrire(self._mot_de_passe))
        except IMAPClientError as e:
            logger.error(
                "Une erreur IMAP critique est survenue lors de la reconnexion. {msg_err}",
                msg_err=str(e))
            self._echec = True
            return

        self._echec = False

    @property
    def dossier_cible(self):
        return self._dossier_cible

    @dossier_cible.setter
    def dossier_cible(self, nouveau_dossier_cible):
        if isinstance(nouveau_dossier_cible, str):
            self._dossier_cible = nouveau_dossier_cible

    @property
    def hote_imap(self):
        return self._hote_imap

    @property
    def nom_utilisateur(self):
        return self._nom_utilisateur

    @staticmethod
    def fetch_instance(hote_imap, nom_utilisateur):
        hote_imap, nom_utilisateur = Session.UNIVERSELLE.retranscrire(
            hote_imap), Session.UNIVERSELLE.retranscrire(nom_utilisateur)

        for inst in MailToolbox.INSTANCES:
            if isinstance(
                    inst, MailToolbox
            ) is True and inst.nom_utilisateur == nom_utilisateur and inst.hote_imap == hote_imap:
                return inst
        return None

    def extraire(self, no_progress_bar=True):

        if self.est_hors_service is True:
            self.reset()

        try:
            self._client.select_folder(self._dossier_cible)
        except IMAPClientError as e:
            raise ExtractionSourceException('IMAPClientError: ' + str(e))
        except IMAPClientAbortError as e:
            raise ExtractionSourceException('IMAPClientAbortError: ' + str(e))
        except IMAPClientReadOnlyError as e:
            raise ExtractionSourceException('IMAPClientReadOnlyError: ' +
                                            str(e))
        finally:
            self._echec = True

        # fetch selectors are passed as a simple list of strings.
        responses = self._client.fetch(self._client.search(['NOT', 'DELETED']),
                                       ['UID', 'ENVELOPE', 'BODY', 'RFC822'])

        extractions = list()  # type: list[Mail]

        for id_response in tqdm(responses.keys(
        ), unit=' message') if no_progress_bar is False else responses.keys():

            email_message = email.message_from_bytes(
                responses[id_response][b'RFC822'])

            mail = Mail.from_message(email_message)

            mail.folder = self._dossier_cible
            mail.flags = responses[id_response][
                b'FLAGS'] if b'FLAGS' in responses[id_response].keys(
                ) else tuple()
            mail.bal_internal_id = id_response

            extractions.append(mail)

            mail.factory = self

        return sorted(extractions,
                      key=lambda x: x.date_received or datetime.now())

    def copier(self, mail, dossier_dest):
        """

        :param Mail mail:
        :param str dossier_dest:
        :return:
        """
        if self.est_hors_service is True:
            self.reset()

        try:
            if self._client.folder_exists(dossier_dest) is False:
                raise FileNotFoundError(
                    'Le dossier "{}" n\'existe pas sur le serveur IMAP distant !'
                )

            self._client.select_folder(mail.folder)

            self._client.copy([mail.bal_internal_id], dossier_dest)
        except IMAPClientError as e:
            raise ManipulationSourceException('IMAPClientError: ' + str(e))
        except IMAPClientAbortError as e:
            raise ManipulationSourceException('IMAPClientAbortError: ' +
                                              str(e))
        except IMAPClientReadOnlyError as e:
            raise ManipulationSourceException('IMAPClientReadOnlyError: ' +
                                              str(e))
        finally:
            self._echec = True

    def deplacer(self, mail, dossier_dest):

        if self.est_hors_service is True:
            self.reset()

        try:
            if self._client.folder_exists(dossier_dest) is False:
                raise FileNotFoundError(
                    'Le dossier "{}" n\'existe pas sur le serveur IMAP distant !'
                )

            self._client.select_folder(mail.folder)

            try:
                self._client.move([mail.bal_internal_id], dossier_dest)
            except CapabilityError as e:
                self.copier(mail, dossier_dest)
                self.supprimer(mail)
        except IMAPClientError as e:
            raise ManipulationSourceException('IMAPClientError: ' + str(e))
        except IMAPClientAbortError as e:
            raise ManipulationSourceException('IMAPClientAbortError: ' +
                                              str(e))
        except IMAPClientReadOnlyError as e:
            raise ManipulationSourceException('IMAPClientReadOnlyError: ' +
                                              str(e))
        finally:
            self._echec = True

    def supprimer(self, mail):

        if self.est_hors_service is True:
            self.reset()

        try:
            self._client.select_folder(mail.folder)

            self._client.delete_messages([mail.bal_internal_id], silent=True)
            self._client.expunge([mail.bal_internal_id])
        except IMAPClientError as e:
            raise ManipulationSourceException('IMAPClientError: ' + str(e))
        except IMAPClientAbortError as e:
            raise ManipulationSourceException('IMAPClientAbortError: ' +
                                              str(e))
        except IMAPClientReadOnlyError as e:
            raise ManipulationSourceException('IMAPClientReadOnlyError: ' +
                                              str(e))
        finally:
            self._echec = True
Exemple #7
0
class Miner:
    """create a new email miner instance that can be used to traverse mails"""

    imap: IMAPClient = None

    def __init__(self,
                 hostname: str,
                 username: str,
                 password: str,
                 port: int = imaplib.IMAP4_SSL_PORT,
                 use_ssl: bool = True,
                 verify: bool = True,
                 log_level: int = None):
        """
        Create a new instance of the miner.

        :param hostname: the hostname of the imap server to connect to
        :param username: the user to login as
        :param password:
        :param port: the port to connect to. (defaults to 993)
        :param use_ssl: whether to use SSL to connect (defaults to True)
        :param verify: whether to verify the SSL certificates (defaults to False)
        """

        if log_level is not None:
            logging.basicConfig(
                format='%(asctime)s - %(levelname)s: %(message)s',
                level=log_level)

        ssl_context = ssl.create_default_context()

        if not verify:
            # disable hostname check. certificate may not match hostname.
            ssl_context.check_hostname = False
            # disable certificate authority verification. certificate maybe issued by unknown CA
            ssl_context.verify_mode = ssl.CERT_NONE

        self.imap = IMAPClient(host=hostname,
                               port=port,
                               ssl=use_ssl,
                               ssl_context=ssl_context)
        self.imap.login(username, password)

    @contextlib.contextmanager
    def folder(self, folder_name: str, read_only: bool = True):
        """
        Switch to a specific folder.

        :param folder_name: name of the folder to switch to
        :param read_only: read-only mode will not mark emails as read even after retrieval
        :return:
        """
        try:
            yield self.imap.select_folder(folder_name, read_only)
        finally:
            self.imap.close_folder()

    @contextlib.contextmanager
    def inbox(self, read_only: bool = True):
        """
        Switch to the inbox folder.

        :param read_only: read-only mode will not mark emails as read even after retrieval
        :return:
        """
        try:
            yield self.imap.select_folder('inbox', read_only)
        finally:
            self.imap.close_folder()

    def mark_as_unread(self, message_ids: List[int]):
        """
        Mark the given message IDs as unread by removing the SEEN flag.

        :param message_ids:
        :return:
        """
        self.imap.remove_flags(message_ids, [SEEN])

    def mark_as_read(self, message_ids: List[int]):
        """
        Mark the given message IDs as read by adding the SEEN flag.

        :param message_ids:
        :return:
        """
        self.imap.add_flags(message_ids, [SEEN])

    def delete(self, message_ids: List[int]):
        """
        Delete the given message IDs

        :param message_ids:
        :return:
        """
        self.imap.delete_messages(message_ids, True)

    def archive(self, message_ids: List[int]):
        """
        Archive the given message IDs

        :param message_ids:
        :return:
        """
        self.imap.copy(message_ids, br'\Archive')
        self.delete(message_ids)

    def get_emails(self,
                   unread_only: bool = True,
                   with_body: bool = False,
                   keep_as_unread: bool = False,
                   in_memory: bool = True) -> List[Email]:
        """
        Get emails from the selected folder.

        :param keep_as_unread: keep any retrieved emails as unread in the mailbox.
        :param unread_only: choose only to retrieve unread mails
        :param with_body: read-only mode will not mark emails as read even after retrieval
        :param in_memory: store the parsed attachments in-memory as bytes or to a temp file locally
        :return:
        """
        ids = self.imap.search('(UNSEEN)' if unread_only else 'ALL')
        flags = ['ENVELOPE', 'FLAGS', 'UID', 'INTERNALDATE']

        if with_body:
            flags.append('BODY[]')

        response = self.imap.fetch(ids, flags)

        try:
            if keep_as_unread:
                self.mark_as_unread(ids)
            else:
                self.mark_as_read(ids)
        except Exception:
            # will throw an exception if folder in read-only mode. so ignore.
            pass

        return parse_emails(response, in_memory)

    def __enter__(self):
        """
        return the instance of the miner for use as a context manager.

        :return:
        """
        return self

    def __exit__(self, *args):
        """
        Close folder and logout on exit when used as a context manager.

        :param args:
        :return:
        """
        if self.imap is not None:
            try:
                self.imap.close_folder()
            except:
                pass
            self.imap.logout()
Exemple #8
0
class Mail:
    """
    Wrapper for all needed methods to process the mails on the IMAP-server.

    Attributes
    ----------
    config : config.Config
        Configuration manager wrapping around the yaml-config-file.
    """

    # List of all Content-types of attachments that should be saved seperatly
    valid_ctypes = ['application/pdf']

    # Regexp that matches all allowed characters in a filename
    filesafe = re.compile(r'[\w\-\. ]')

    def __init__(self, config):
        config.checkParams('server', 'port', 'username', 'password', 'inbox',
                           'outbox', 'basepath', 'eml-to-pdf-path')
        host = config.get('server')
        port = config.get('port')
        username = config.get('username')
        password = config.get('password')
        inbox = config.get('inbox')
        self.outbox = config.get('outbox')
        self.basepath = config.get('basepath')
        if not os.path.exists(self.basepath): os.makedirs(self.basepath)
        self.emltopdf = config.get('eml-to-pdf-path')

        self.server = IMAPClient(host, port=port)
        result = self.server.login(username, password)
        print(result.decode('UTF-8'))
        self.server.select_folder(inbox)

    def check(self):
        """
        Checks for new mail to process on the server.
        """
        messages = self.server.search(['ALL'])
        if len(messages) < 1: return

        for id, data in self.server.fetch(messages, 'RFC822').items():
            mail = email.message_from_bytes(data[b'RFC822'])
            self.processMail(mail)

        self.server.copy(messages, self.outbox)
        self.server.delete_messages(messages)
        self.server.expunge()

    def getDate(self, mail):
        """
        Extracts the date from the mail header and rewrites it for use as the
        beginning of a filename.

        Parameters
        ----------
        mail : email
            Mail from which to extract the date.

        Returns
        -------
        The date reformatted to be used in a filename.
        """
        date = mail.get('Date')
        date = datetime.strptime(date[:-6], '%a, %d %b %Y %H:%M:%S')
        return date.strftime('%Y-%m-%d-%H-%M-%S')

    def processMail(self, mail):
        """
        Processes a specific mail by saving it.

        Parameters
        ----------
        mail : email
            The mail to process.
        """
        filename = '%s-%s.eml' % (self.getDate(mail), mail.get('Subject'))
        filename = self.validateFilename(filename)
        filename = os.path.join(self.basepath, filename)
        with open(filename, 'w') as f:
            generator = email.generator.Generator(f)
            generator.flatten(mail)

        if mail.is_multipart(): self.processAttachments(mail)

    def processAttachments(self, mail):
        """
        Processes all attachments of a mail.

        Parameters
        ----------
        mail : email
            The mail to process.
        """
        date = self.getDate(mail)
        for part in mail.walk():
            ctype = part.get_content_type()
            if not ctype in Mail.valid_ctypes: continue
            filename = '%s-attachment-%s' % (date, part.get_filename())
            filename = self.validateFilename(filename)
            filename = os.path.join(self.basepath, filename)
            with open(filename, 'wb') as f:
                f.write(part.get_payload(decode=True))

    def validateFilename(self, filename):
        """
        Makes a filename safe.

        Parameters
        ----------
        filename : str
            Filename to reformat.

        Returns
        -------
        The new filename, stripped of unsafe characters.
        """
        return ''.join([c for c in filename if Mail.filesafe.match(c)])

    def __del__(self):
        """
        Deconstructor disconnects from the IMAP-Server.
        """
        self.server.logout()
Exemple #9
0
class ImapDB(BaseDB):
    def __init__(self,
                 username,
                 password='******',
                 host='localhost',
                 port=143,
                 *args,
                 **kwargs):
        super().__init__(username, *args, **kwargs)
        self.imap = IMAPClient(host, port, use_uid=True, ssl=False)
        res = self.imap.login(username, password)
        self.cursor.execute(
            "SELECT lowModSeq,highModSeq,highModSeqMailbox,highModSeqThread,highModSeqEmail FROM account LIMIT 1"
        )
        row = self.cursor.fetchone()
        self.lastfoldersync = 0
        if row:
            self.lowModSeq,
            self.highModSeq,
            self.highModSeqMailbox,
            self.highModSeqThread,
            self.highModSeqEmail = row
        else:
            self.lowModSeq = 0
            self.highModSeq = 1
            self.highModSeqMailbox = 1
            self.highModSeqThread = 1
            self.highModSeqEmail = 1

        # (imapname, readonly)
        self.selected_folder = (None, False)
        self.mailboxes = {}
        self.sync_mailboxes()
        self.messages = {}

    def get_messages_cached(self, properties=(), id__in=()):
        messages = []
        if not self.messages:
            return messages, id__in, properties
        fetch_props = set()
        fetch_ids = set(id__in)
        for id in id__in:
            msg = self.messages.get(id, None)
            if msg:
                found = True
                for prop in properties:
                    try:
                        msg[prop]
                    except (KeyError, AttributeError):
                        found = False
                        fetch_props.add(prop)
                if found:
                    fetch_ids.remove(id)
                    messages.append(msg)
        # if one messages is missing, need to fetch all properties
        if len(messages) < len(id__in):
            fetch_props = properties
        return messages, fetch_ids, fetch_props

    def get_messages(self,
                     properties=(),
                     sort={},
                     inMailbox=None,
                     inMailboxOtherThan=(),
                     id__in=None,
                     threadId__in=None,
                     **criteria):
        # XXX: id == threadId for now
        if id__in is None and threadId__in is not None:
            id__in = [id[1:] for id in threadId__in]
        if id__in is None:
            messages = []
        else:
            # try get everything from cache
            messages, id__in, properties = self.get_messages_cached(
                properties, id__in=id__in)

        fetch_fields = {
            f
            for prop, f in FIELDS_MAP.items() if prop in properties
        }
        if 'RFC822' in fetch_fields:
            # remove redundand fields
            fetch_fields.discard('RFC822.HEADER')
            fetch_fields.discard('RFC822.SIZE')

        if inMailbox:
            mailbox = self.mailboxes.get(inMailbox, None)
            if not mailbox:
                raise errors.notFound(f'Mailbox {inMailbox} not found')
            mailboxes = [mailbox]
        elif inMailboxOtherThan:
            mailboxes = [
                m for m in self.mailboxes.values()
                if m['id'] not in inMailboxOtherThan
            ]
        else:
            mailboxes = self.mailboxes.values()

        search_criteria = as_imap_search(criteria)
        sort_criteria = as_imap_sort(sort) or '' if sort else None

        mailbox_uids = {}
        if id__in is not None:
            if len(id__in) == 0:
                return messages  # no messages matches empty ids
            if not fetch_fields and not sort_criteria:
                # when we don't need anything new from IMAP, create empty messages
                # useful when requested conditions can be calculated from id (threadId)
                messages.extend(
                    self.messages.get(id, 0) or ImapMessage(id=id)
                    for id in id__in)
                return messages

            for id in id__in:
                # TODO: check uidvalidity
                mailboxid, uidvalidity, uid = parse_message_id(id)
                uids = mailbox_uids.get(mailboxid, [])
                if not uids:
                    mailbox_uids[mailboxid] = uids
                uids.append(uid)
            # filter out unnecessary mailboxes
            mailboxes = [m for m in mailboxes if m['id'] in mailbox_uids]

        for mailbox in mailboxes:
            imapname = mailbox['imapname']
            if self.selected_folder[0] != imapname:
                self.imap.select_folder(imapname, readonly=True)
                self.selected_folder = (imapname, True)

            uids = mailbox_uids.get(mailbox['id'], None)
            # uids are now None or not empty
            # fetch all
            if sort_criteria:
                if uids:
                    search = f'{",".join(map(str, uids))} {search_criteria}'
                else:
                    search = search_criteria or 'ALL'
                uids = self.imap.sort(sort_criteria, search)
            elif search_criteria:
                if uids:
                    search = f'{",".join(map(str, uids))} {search_criteria}'
                uids = self.imap.search(search)
            if uids is None:
                uids = '1:*'
            fetch_fields.add('UID')
            fetches = self.imap.fetch(uids, fetch_fields)

            for uid, data in fetches.items():
                id = format_message_id(mailbox['id'], mailbox['uidvalidity'],
                                       uid)
                msg = self.messages.get(id, None)
                if not msg:
                    msg = ImapMessage(id=id, mailboxIds=[mailbox['id']])
                    self.messages[id] = msg
                for k, v in data.items():
                    msg[k.decode()] = v
                messages.append(msg)
        return messages

    def changed_record(self, ifolderid, uid, flags=(), labels=()):
        res = self.dmaybeupdate(
            'imessages', {
                'flags': json.dumps(sorted(flags)),
                'labels': json.dumps(sorted(labels)),
            }, {
                'ifolderid': ifolderid,
                'uid': uid
            })
        if res:
            msgid = self.dgefield('imessages', {
                'ifolderid': ifolderid,
                'uid': uid
            }, 'msgid')
            self.mark_sync(msgid)

    def import_message(self, rfc822, mailboxIds, keywords):
        folderdata = self.dget('ifolders')
        foldermap = {f['ifolderid']: f for f in folderdata}
        jmailmap = {
            f['jmailboxid']: f
            for f in folderdata if f.get('jmailboxid', False)
        }
        # store to the first named folder - we can use labels on gmail to add to other folders later.
        id, others = mailboxIds
        imapname = jmailmap[id][imapname]
        flags = set(keywords)
        for kw in flags:
            if kw in KEYWORD2FLAG:
                flags.remove(kw)
                flags.add(KEYWORD2FLAG[kw])
        appendres = self.imap.append('imapname', '(' + ' '.join(flags) + ')',
                                     datetime.now(), rfc822)
        # TODO: compare appendres[2] with uidvalidity
        uid = appendres[3]
        fdata = jmailmap[mailboxIds[0]]
        self.do_folder(fdata['ifolderid'], fdata['label'])
        ifolderid = fdata['ifolderid']
        msgdata = self.dgetone('imessages', {
            'ifolderid': ifolderid,
            'uid': uid,
        }, 'msgid,thrid,size')

        # XXX - did we fail to sync this back?  Annoying
        if not msgdata:
            raise Exception(
                'Failed to get back stored message from imap server')
        # save us having to download it again - drop out of transaction so we don't wait on the parse
        message = parse.parse(rfc822, msgdata['msgid'])
        self.begin()
        self.dinsert(
            'jrawmessage', {
                'msgid': msgdata['msgid'],
                'parsed': json.dumps('message'),
                'hasAttachment': message['hasattachment'],
            })
        self.commit()
        return msgdata

    def update_messages(self, changes, idmap):
        if not changes:
            return {}, {}

        changed = {}
        notchanged = {}
        map = {}
        msgids = set(changes.keys())
        sql = 'SELECT msgid,ifolderid,uid FROM imessages WHERE msgid IN (' + (
            ('?,' * len(msgids))[:-1]) + ')'
        self.cursor.execute(sql, list(msgids))
        for msgid, ifolderid, uid in self.cursor:
            if not msgid in map:
                map[msgid] = {ifolderid: {uid}}
            elif not ifolderid in map[msgid]:
                map[msgid][ifolderid] = {uid}
            else:
                map[msgid][ifolderid].add(uid)
            msgids.discard(msgid)

        for msgid in msgids:
            notchanged[msgid] = {
                'type': 'notFound',
                'description': 'No such message on server',
            }

        folderdata = self.dget('ifolders')
        foldermap = {f['ifolderid']: f for f in folderdata}
        jmailmap = {
            f['jmailboxid']: f
            for f in folderdata if 'jmailboxid' in f
        }
        jmapdata = self.dget('jmailboxes')
        jidmap = {d['jmailboxid']: (d['role'] or '') for d in jmapdata}
        jrolemap = {
            d['role']: d['jmailboxid']
            for d in jmapdata if 'role' in d
        }

        for msgid in map.keys():
            action = changes[msgid]
            try:
                for ifolderid, uids in map[msgid].items():
                    # TODO: merge similar actions?
                    imapname = foldermap[ifolderid]['imapname']
                    uidvalidity = foldermap[ifolderid]['uidvalidity']
                    if self.selected_folder != (imapname, False):
                        self.imap.select_folder(imapname)
                        self.selected_folder = (imapname, False)
                    if imapname and uidvalidity and 'keywords' in action:
                        flags = set(action['keywords'])
                        for kw in flags:
                            if kw in KEYWORD2FLAG:
                                flags.remove(kw)
                                flags.add(KEYWORD2FLAG[kw])
                        self.imap.set_flags(uids, flags, silent=True)

                if 'mailboxIds' in action:
                    mboxes = [idmap(k) for k in action['mailboxIds'].keys()]
                    # existing ifolderids containing this message
                    # identify a source message to work from
                    ifolderid = sorted(map[msgid])[0]
                    uid = sorted(map[msgid][ifolderid])[0]
                    imapname = foldermap[ifolderid]['imapname']
                    uidvalidity = foldermap[ifolderid]['uidvalidity']

                    # existing ifolderids with this message
                    current = set(map[msgid].keys())
                    # new ifolderids that should contain this message
                    new = set(jmailmap[x]['ifolderid'] for x in mboxes)
                    for ifolderid in new:
                        # unless there's already a matching message in it
                        if current.pop(ifolderid):
                            continue
                        # copy from the existing message
                        newfolder = foldermap[ifolderid]['imapname']
                        self.imap.copy(imapname, uidvalidity, uid, newfolder)
                    for ifolderid in current:
                        # these ifolderids didn't exist in new, so delete all matching UIDs from these folders
                        self.imap.move(
                            foldermap[ifolderid]['imapname'],
                            foldermap[ifolderid]['uidvalidity'],
                            map[msgid][ifolderid],  # uids
                        )
            except Exception as e:
                notchanged[msgid] = {'type': 'error', 'description': str(e)}
                raise e
            else:
                changed[msgid] = None

        return changed, notchanged

    def destroy_messages(self, ids):
        if not ids:
            return [], {}
        destroymap = defaultdict(dict)
        notdestroyed = {}
        idset = set(ids)
        rows = self.dget('imessages', {'msgid': ('IN', idset)},
                         'msgid,ifolderid,uid')
        for msgid, ifolderid, uid in rows:
            idset.discard(msgid)
            destroymap[ifolderid][uid] = msgid
        for msgid in idset:
            notdestroyed[msgid] = {
                'type': 'notFound',
                'description': "No such message on server",
            }

        folderdata = self.dget('ifolders')
        foldermap = {d['ifolderid']: d for d in folderdata}
        jmailmap = {
            d['jmailboxid']: d
            for d in folderdata if 'jmailboxid' in d
        }
        destroyed = []
        for ifolderid, ifolder in destroymap.items():
            #TODO: merge similar actions?
            if not ifolder['imapname']:
                for msgid in destroymap[ifolderid]:
                    notdestroyed[msgid] = \
                        {'type': 'notFound', 'description': "No folder"}
            self.imap.move(ifolder['imapname'], ifolder['uidvalidity'],
                           destroymap[ifolderid].keys(), None)
            destroyed.extend(destroymap[ifolderid].values())

        return destroyed, notdestroyed

    def deleted_record(self, ifolderid, uid):
        msgid = self.dgetfield('imessages', {
            'ifolderid': ifolderid,
            'uid': uid
        }, 'msgid')
        if msgid:
            self.ddelete('imessages', {'ifolderid': ifolderid, 'uid': uid})
            self.mark_sync(msgid)

    def get_raw_message(self, msgid, part=None):
        self.cursor.execute(
            'SELECT imapname,uidvalidity,uid FROM ifolders JOIN imessages USING (ifolderid) WHERE msgid=?',
            [msgid])
        imapname, uidvalidity, uid = self.cursor.fetchone()
        if not imapname:
            return None
        typ = 'message/rfc822'
        if part:
            parsed = self.fill_messages([msgid])
            typ = find_type(parsed[msgid], part)

        res = self.imap.getpart(imapname, uidvalidity, uid, part)
        return typ, res['data']

    def get_mailboxes(self, fields=None, **criteria):
        byimapname = {}
        # TODO: LIST "" % RETURN (STATUS (UNSEEN MESSAGES HIGHESTMODSEQ MAILBOXID))
        for flags, sep, imapname in self.imap.list_folders():
            status = self.imap.folder_status(imapname, ([
                'MESSAGES', 'UIDVALIDITY', 'UIDNEXT', 'HIGHESTMODSEQ', 'X-GUID'
            ]))
            flags = [f.lower() for f in flags]
            roles = [f for f in flags if f not in KNOWN_SPECIALS]
            label = roles[0].decode() if roles else imapname
            role = ROLE_MAP.get(label.lower(), None)
            can_select = b'\\noselect' not in flags
            byimapname[imapname] = {
                # Dovecot can fetch X-GUID
                'id': status[b'X-GUID'].decode(),
                'parentId': None,
                'name': imapname,
                'role': role,
                'sortOrder': 2 if role else (1 if role == 'inbox' else 3),
                'isSubscribed': True,  # TODO: use LSUB
                'totalEmails': status[b'MESSAGES'],
                'unreadEmails': 0,
                'totalThreads': 0,
                'unreadThreads': 0,
                'myRights': {
                    'mayReadItems': can_select,
                    'mayAddItems': can_select,
                    'mayRemoveItems': can_select,
                    'maySetSeen': can_select,
                    'maySetKeywords': can_select,
                    'mayCreateChild': True,
                    'mayRename': False if role else True,
                    'mayDelete': False if role else True,
                    'maySubmit': can_select,
                },
                'imapname': imapname,
                'sep': sep.decode(),
                'uidvalidity': status[b'UIDVALIDITY'],
                'uidnext': status[b'UIDNEXT'],
                # Data sync properties
                'createdModSeq': status[b'UIDVALIDITY'],  # TODO: persist
                'updatedModSeq':
                status[b'UIDVALIDITY'],  # TODO: from persistent storage
                'updatedNotCountsModSeq':
                status[b'UIDVALIDITY'],  # TODO: from persistent storage
                'emailHighestModSeq': status[b'HIGHESTMODSEQ'],
                'deleted': 0,
            }

        # set name and parentId for child folders
        for imapname, mailbox in byimapname.items():
            names = imapname.rsplit(mailbox['sep'], maxsplit=1)
            if len(names) == 2:
                mailbox['parentId'] = byimapname[names[0]]['id']
                mailbox['name'] = names[1]

        # update cache
        self.mailboxes = {mbox['id']: mbox for mbox in byimapname.values()}
        return byimapname.values()

    def sync_mailboxes(self):
        self.get_mailboxes()

    def mailbox_imapname(self, parentId, name):
        parent = self.mailboxes.get(parentId, None)
        if not parent:
            raise errors.notFound('parent folder not found')
        return parent['imapname'] + parent['sep'] + name

    def create_mailbox(self,
                       name=None,
                       parentId=None,
                       isSubscribed=True,
                       **kwargs):
        if not name:
            raise errors.invalidProperties('name is required')
        imapname = self.mailbox_imapname(parentId, name)
        # TODO: parse returned MAILBOXID
        try:
            res = self.imap.create_folder(imapname)
        except IMAPClientError as e:
            desc = str(e)
            if '[ALREADYEXISTS]' in desc:
                raise errors.invalidArguments(desc)
        except Exception:
            raise errors.serverFail(res.decode())

        if not isSubscribed:
            self.imap.unsubscribe_folder(imapname)

        status = self.imap.folder_status(imapname, ['UIDVALIDITY'])
        self.sync_mailboxes()
        return f"f{status[b'UIDVALIDITY']}"

    def update_mailbox(self,
                       id,
                       name=None,
                       parentId=None,
                       isSubscribed=None,
                       sortOrder=None,
                       **update):
        mailbox = self.mailboxes.get(id, None)
        if not mailbox:
            raise errors.notFound('mailbox not found')
        imapname = mailbox['imapname']

        if (name is not None and name != mailbox['name']) or \
           (parentId is not None and parentId != mailbox['parentId']):
            if not name:
                raise errors.invalidProperties('name is required')
            newimapname = self.mailbox_imapname(parentId, name)
            res = self.imap.rename_folder(imapname, newimapname)
            if b'NO' in res or b'BAD' in res:
                raise errors.serverFail(res.encode())

        if isSubscribed is not None and isSubscribed != mailbox['isSubscribed']:
            if isSubscribed:
                res = self.imap.subscribe_folder(imapname)
            else:
                res = self.imap.unsubscribe_folder(imapname)
            if b'NO' in res or b'BAD' in res:
                raise errors.serverFail(res.encode())

        if sortOrder is not None and sortOrder != mailbox['sortOrder']:
            # TODO: update in persistent storage
            mailbox['sortOrder'] = sortOrder
        self.sync_mailboxes()

    def destroy_mailbox(self, id):
        mailbox = self.mailboxes.get(id, None)
        if not mailbox:
            raise errors.notFound('mailbox not found')
        res = self.imap.delete_folder(mailbox['imapname'])
        if b'NO' in res or b'BAD' in res:
            raise errors.serverFail(res.encode())
        mailbox['deleted'] = datetime.now().timestamp()
        self.sync_mailboxes()

    def create_submission(self, new, idmap):
        if not new:
            return {}, {}

        todo = {}
        createmap = {}
        notcreated = {}
        for cid, sub in new.items():
            msgid = idmap.get(sub['emailId'], sub['emailId'])
            if not msgid:
                notcreated[cid] = {'error': 'nos msgid provided'}
                continue
            thrid = self.dgetfield('jmessages', {
                'msgid': msgid,
                'deleted': 0
            }, 'thrid')
            if not thrid:
                notcreated[cid] = {'error': 'message does not exist'}
                continue
            id = self.dmake(
                'jsubmission', {
                    'sendat':
                    datetime.fromisoformat()(sub['sendAt']).isoformat()
                    if sub['sendAt'] else datetime.now().isoformat(),
                    'msgid':
                    msgid,
                    'thrid':
                    thrid,
                    'envelope':
                    json.dumps(sub['envelope']) if 'envelope' in sub else None,
                })
            createmap[cid] = {'id': id}
            todo[cid] = msgid
        self.commit()

        for cid, sub in todo.items():
            type, rfc822 = self.get_raw_message(todo[cid])
            self.imap.send_mail(rfc822, sub['envelope'])

        return createmap, notcreated

    def update_submission(self, changed, idmap):
        return {}, {x: 'change not supported' for x in changed.keys()}

    def destroy_submission(self, destroy):
        if not destroy:
            return [], {}
        destroyed = []
        notdestroyed = {}
        namemap = {}
        for subid in destroy:
            deleted = self.dgetfield('jsubmission', {'jsubid': subid},
                                     'deleted')
            if deleted:
                destroy.append(subid)
                self.ddelete('jsubmission', {'jsubid': subid})
            else:
                notdestroyed[subid] = {
                    'type': 'notFound',
                    'description': 'submission not found'
                }
        self.commit()
        return destroyed, notdestroyed

    def _initdb(self):
        super()._initdb()

        self.dbh.execute("""
            CREATE TABLE IF NOT EXISTS ifolders (
            ifolderid INTEGER PRIMARY KEY NOT NULL,
            jmailboxid INTEGER,
            sep TEXT NOT NULL,
            imapname TEXT NOT NULL,
            label TEXT,
            uidvalidity INTEGER,
            uidfirst INTEGER,
            uidnext INTEGER,
            highestmodseq INTEGER,
            uniqueid TEXT,
            mtime DATE NOT NULL
            )""")
        self.dbh.execute(
            "CREATE INDEX IF NOT EXISTS ifolderj ON ifolders (jmailboxid)")
        self.dbh.execute(
            "CREATE INDEX IF NOT EXISTS ifolderlabel ON ifolders (label)")

        self.dbh.execute("""
            CREATE TABLE IF NOT EXISTS imessages (
            imessageid INTEGER PRIMARY KEY NOT NULL,
            ifolderid INTEGER,
            uid INTEGER,
            internaldate DATE,
            modseq INTEGER,
            flags TEXT,
            labels TEXT,
            thrid TEXT,
            msgid TEXT,
            envelope TEXT,
            bodystructure TEXT,
            size INTEGER,
            mtime DATE NOT NULL
            )""")
        self.dbh.execute(
            "CREATE UNIQUE INDEX IF NOT EXISTS imsgfrom ON imessages (ifolderid, uid)"
        )
        self.dbh.execute(
            "CREATE INDEX IF NOT EXISTS imessageid ON imessages (msgid)")
        self.dbh.execute(
            "CREATE INDEX IF NOT EXISTS imessagethrid ON imessages (thrid)")

        self.dbh.execute("""
            CREATE TABLE IF NOT EXISTS ithread (
            messageid TEXT PRIMARY KEY,
            sortsubject TEXT,
            thrid TEXT
            )""")
        self.dbh.execute(
            "CREATE INDEX IF NOT EXISTS ithrid ON ithread (thrid)")
Exemple #10
0
class IMAPBot(object):
    IMAPBotError = IMAPBotError

    def __init__(self, host, username, password, ssl=True):
        self.server = IMAPClient(host, use_uid=True, ssl=ssl)
        self.server.login(username, password)

        if 'IDLE' not in self.server.capabilities():
            raise IMAPBotError('Sorry, this IMAP server does not support IDLE.')

        # the folder where processed emails go
        self.processed_folder = 'imapbot_processed'

        self.idle_timeout = 5  # seconds

        self._is_idle = False
        self._run = True

        self._create_folder(self.processed_folder)

    def check_mail(self):
        select_info = self.server.select_folder('INBOX')
        print '%d messages in INBOX' % select_info['EXISTS']

        messages = self.server.search(['UNSEEN'])
        messages = self.server.search(['NOT DELETED'])
        print "%d messages that haven't been seen" % len(messages)

        if not messages:
            return

        #response = self.server.fetch(messages, ['FLAGS', 'INTERNALDATE', 'RFC822.SIZE', 'ENVELOPE', 'RFC822.TEXT'])
        response = self.server.fetch(messages, ['FLAGS', 'ENVELOPE', 'RFC822.TEXT'])
        for message_id, data in response.iteritems():
            message = Message(message_id, data['ENVELOPE'], data['RFC822.TEXT'], data['FLAGS'])

            self.process(message)

    def complete(self, message):
        message_ids = [message.id]

        self.server.copy(message_ids, self.processed_folder)
        self.server.delete_messages(message_ids)
        self.server.expunge()

    def _create_folder(self, name):
        # make sure the folder doesn't already exist
        if self.server.folder_exists(name):
            return

        self.server.create_folder(name)

    def handle_message(self, message):
        print 'message id: {}, from: {}:'.format(message.id, message.envelope.get_email('from'))
        with open('message.txt', 'ab') as fh:
            fh.write('{}\n\n'.format(message.text))

        print message.plain or message.html or 'no message'

    def idle(self):
        if self._is_idle:
            return

        self.server.idle()

        self._is_idle = True

        return True  # this actually changed state

    def unidle(self):
        if not self._is_idle:
            return

        self.server.idle_done()

        self._is_idle = False

        return True  # this call actually changed state

    def process(self, message):
        self.handle_message(message)
        self.complete(message)

    def run(self):
        # process any mail that was in the inbox before coming online
        self.check_mail()

        # put the connection in idle mode so we get notifications
        self.idle()

        # loop forever looking for stuff
        while self._run:
            for message in self.server.idle_check(timeout=self.idle_timeout):
                if message[0] == 'OK':
                    continue

                with Unidle(self):
                    self.check_mail()

    def quit(self):
        self._run = False

        self.unidle()

        print self.server.logout()
Exemple #11
0
def main():
	# -options-
	parser = OptionParser("python %prog [options] CONFIG_FILE")
	parser.add_option('-v', '--verbose', action='store_true', dest='verbose', default=False)
	(options, args) = parser.parse_args()

	# -settings-
	if len(args) < 1:
		parser.print_help()
		exit()

	if not os.path.exists(args[0]):
		raise IOError(ENOENT, 'Archivo de configuracion no encontrado', args[0])

	config = RawConfigParser()
	config.read(args[0])
	# put read config options (from the .ini) into global namespace and in uppercase
	for name, value in config.items('chkemail'):
		globals()[name.upper()] = value

	# -workflow-
	# create, connect, and login to server
	server = IMAPClient(HOST, use_uid=True)
	try:
		server.login(USER, PASSWORD, port=PORT)
	except NameError: # if PORT is not defined
		server.login(USER, PASSWORD)

	inbox = server.select_folder('INBOX')
	if options.verbose:
		print '%d messages in INBOX (included deleted)' % inbox['EXISTS']

	messages = server.search(['NOT DELETED', 'HEADER Content-Type mixed'])
	if options.verbose:
		print "%d messages with possible attch" % len(messages)

	# fetch data from messages, put each message (Mess object) into the msgs list
	scan = server.fetch(messages, ['BODYSTRUCTURE', 'ENVELOPE'])
	msgs = dict()
	for mid, response in scan.iteritems():
		# Mess class only works with mulipart messages
		if response['BODYSTRUCTURE'].is_multipart:
			msgs[mid] =  Mess(mid, response['ENVELOPE'], response['BODYSTRUCTURE'])

	# Select the messages with attachements I want, put them in group_msgs
	group_msgs = dict()
	for msg in msgs.itervalues():
		group_msgs[msg.id] = list()
		for part_num, part_info in msg.parts.iteritems():
			if part_info.filename:
				filename = part_info.filename.lower()
				if filename.endswith('.pdf') or filename.endswith('.xml') or \
					filename.endswith('.zip'):
					group_msgs[msg.id] += [part_num]
		if not group_msgs[msg.id]:
			del group_msgs[msg.id]

	# fetch all interesting parts
	for msg_id, parts in group_msgs.iteritems():
		request = ['BODY['+str(part)+']' for part in parts]
		response = server.fetch(msg_id, request)
		for body_part in response[msg_id].iterkeys():
			if 'BODY' in body_part:
				msgs[msg_id].parts[body_part[5:-1]].data = response[msg_id][body_part]

	# move messages to trash
	if len(group_msgs.keys()) > 0:
		server.copy(group_msgs.keys(), 'INBOX.Trash')
		server.delete_messages(group_msgs.keys())
	server.logout()

	# ensure there's an OUTPUT_DIR directory
	pdf_dir = os.path.join(OUTPUT_PDF, strftime('%Y-%m'))
	ensure_dir(pdf_dir)
	ensure_dir(OUTPUT_DIR)

	# decode and write data to file
	num_attch = 0
	for msg in msgs.itervalues():
		for part in msg.parts:
			if part.data:
				filename = part.filename.lower()
				if filename.endswith('.pdf') or filename.endswith('.xml'):
					if filename.endswith('.pdf'):
						ensure_dir(os.path.join(pdf_dir, str(msg.envelope)))
						attachment_filename = generate_filename(os.path.join(pdf_dir, str(msg.envelope), os.path.basename(part.filename)))
					else:
						attachment_filename = generate_filename(os.path.join(OUTPUT_DIR, os.path.basename(part.filename)))
					with open(attachment_filename, 'wb') as file_:
						if part.encoding == 'quoted-printable':
							file_.write(decodestring(part.data))
						elif part.encoding == 'base64':
							file_.write(b64decode(part.data))
					num_attch += 1
				elif filename.endswith('.zip') and part.encoding == 'base64':
					with tempfile.TemporaryFile() as tmp_zip:
						tmp_zip.write(b64decode(part.data))
						zip_file = ZipFile(tmp_zip)
						for f_info in zip_file.infolist():
							if f_info.filename.lower().endswith('.xml'):
								attachment_filename = generate_filename(os.path.join(OUTPUT_DIR, os.path.basename(f_info.filename)))
							elif f_info.filename.lower().endswith('.pdf'):
								ensure_dir(os.path.join(pdf_dir, str(msg.envelope)))
								attachment_filename = generate_filename(os.path.join(pdf_dir, str(msg.envelope), os.path.basename(f_info.filename)))
							else:
								continue
							with open(attachment_filename, 'wb') as file_:
								file_.write(zip_file.read(f_info))
							num_attch += 1

	if options.verbose:
		print '%d files extracted' % num_attch
Exemple #12
0
class IMAP():
    """
    Central class for IMAP server communication
    """
    Retval = namedtuple('Retval', 'code data')

    def __init__(self,
                 logger,
                 username,
                 password,
                 server='localhost',
                 port=143,
                 starttls=False,
                 imaps=False,
                 tlsverify=True,
                 test=False,
                 timeout=None):
        self.logger = logger
        self.username = username
        self.password = password
        self.server = server
        self.port = port
        self.imaps = imaps
        self.starttls = starttls
        self.timeout = timeout

        self.sslcontext = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
        if tlsverify:
            self.sslcontext.verify_mode = ssl.CERT_REQUIRED
        else:
            self.sslcontext.check_hostname = False
            self.sslcontext.verify_mode = ssl.CERT_NONE

        self.test = test
        self.conn = None

    def do_select_mailbox(func):
        """
        Decorator to do a fresh mailbox SELECT
        """
        def wrapper(*args, **kwargs):
            if len(args) != 1:
                raise AttributeError(
                    'Size of *args tuple "{0}" isn\'t 1. It looks like you haven\'t specified all '
                    'method arguments as named arguments!'.format(args))

            mailbox = None
            for key in ['mailbox', 'source']:
                if key in kwargs.keys():
                    mailbox = kwargs[key]
                    break

            if mailbox is None:
                raise KeyError(
                    'Unable to SELECT a mailbox, kwargs "{0}" doesn\'t contain a mailbox name'
                    .format(kwargs))

            result = args[0].select_mailbox(mailbox)
            if not result.code:
                raise RuntimeError(result.data)
            return func(*args, **kwargs)

        return wrapper

    def process_error(self, exception, simple_return=False):
        """
        Process Python exception by logging a message and optionally showing traceback
        """
        trace_info = exc_info()
        err_msg = str(exception)

        if isinstance(exception, IMAPClient.Error):
            err_msg = Helper().byte_to_str(exception.args[0])

        self.logger.error("Catching IMAP exception {}: {}".format(
            type(exception), err_msg))

        if self.logger.isEnabledFor(loglevel_DEBUG):
            print_exception(*trace_info)
        del trace_info

        if simple_return:
            return exception
        else:
            return self.Retval(False, err_msg)

    def connect(self, retry=True, logout=False):
        """
        Connect to IMAP server and login
        """
        if self.starttls:
            self.logger.debug(
                'Establishing IMAP connection using STARTTLS/{} to {} and logging in with user {}'
                .format(self.port, self.server, self.username))
        elif self.imaps:
            self.logger.debug(
                'Establishing IMAP connection using SSL/{} (imaps) to {} and logging in with user {}'
                .format(self.port, self.server, self.username))
        try:
            self.conn = IMAPClient(host=self.server,
                                   port=self.port,
                                   use_uid=True,
                                   ssl=self.imaps,
                                   ssl_context=self.sslcontext,
                                   timeout=self.timeout)

            if self.starttls:
                self.conn.starttls(ssl_context=self.sslcontext)

            login = self.conn.login(self.username, self.password)
            login_response = Helper().byte_to_str(login)

            # Test login/auth status
            login_success = False
            noop = self.noop()
            if noop.code and noop.data:
                login_success = True

            if logout:
                return self.disconnect()
            elif login_success:
                return self.Retval(True, login_response)
            else:
                return self.Retval(False, login_response)  # pragma: no cover

        except exceptions.LoginError as e:
            return self.process_error(e)

        except Exception as e:
            err_return = self.process_error(e)

            if retry:
                self.logger.error('Trying one more time to login')
                sleep(2)
                return self.connect(retry=False, logout=logout)
            return err_return

    def noop(self):
        """
        Do a noop to test login status
        """
        try:
            noop = self.conn.noop()
            noop_response = Helper().byte_to_str(noop[0])
            noop_resp_pattern_re = regex_compile('^(Success|NOOP completed)')
            login_success = noop_resp_pattern_re.match(noop_response)
            return self.Retval(True, login_success)
        except IMAPClient.Error as e:
            return self.process_error(e)

    def disconnect(self):
        """
        Disconnect from IMAP server
        """
        result = self.conn.logout()
        response = Helper().byte_to_str(result)
        return self.Retval(response == 'Logging out', response)

    def list_mailboxes(self, directory='', pattern='*'):
        """
        Get a listing of folders (mailboxes) on the server
        """
        try:
            raw_list = self.conn.list_folders(directory, pattern)
            nice_list = []

            for mailbox in raw_list:
                flags = []
                for flag in mailbox[0]:
                    flags.append(flag.decode('utf-8'))

                nice_list.append({
                    'name': mailbox[2],
                    'flags': flags,
                    'delimiter': mailbox[1].decode("utf-8")
                })
            return self.Retval(True, nice_list)
        except IMAPClient.Error as e:
            return self.process_error(e)

    def select_mailbox(self, mailbox):
        """
        Select a mailbox to work on
        """
        self.logger.debug('Switching to mailbox {}'.format(mailbox))
        try:
            result = self.conn.select_folder(mailbox)
            response = {}
            for key, value in result.items():
                unicode_key = Helper().byte_to_str(key)
                if unicode_key == 'FLAGS':
                    flags = []
                    for flag in value:
                        flags.append(Helper().byte_to_str(flag))
                    response[unicode_key] = tuple(flags)
                else:
                    response[unicode_key] = value
            return self.Retval(True, response)
        except IMAPClient.Error as e:
            return self.process_error(e)

    def add_mail(self, mailbox, message, flags=(), msg_time=None):
        """
        Add/append a mail to a mailbox
        """
        self.logger.debug('Adding a mail into mailbox {}'.format(mailbox))
        try:
            if not isinstance(message, Mail):
                message = Mail(logger=self.logger, mail_native=message)

            self.conn.append(mailbox, str(message.get_native()), flags,
                             msg_time)

            # According to rfc4315 we must not return the UID from the response, so we are fetching it ourselves
            uids = self.search_mails(mailbox=mailbox,
                                     criteria='HEADER Message-Id "{}"'.format(
                                         message.get_message_id())).data[0]

            return self.Retval(True, uids)
        except IMAPClient.Error as e:
            return self.process_error(e)

    @do_select_mailbox
    def search_mails(self, mailbox, criteria='ALL', autocreate_mailbox=False):
        """
        Search for mails in a mailbox
        """
        self.logger.debug(
            'Searching for mails in mailbox {} and criteria=\'{}\''.format(
                mailbox, criteria))
        try:
            return self.Retval(True, list(self.conn.search(criteria=criteria)))
        except IMAPClient.Error as e:
            return self.process_error(e)

    @do_select_mailbox
    def fetch_mails(self, uids, mailbox, return_fields=None):
        """
        Retrieve mails from a mailbox
        """
        self.logger.debug('Fetching mails with uids {}'.format(uids))

        return_raw = True
        if return_fields is None:
            return_raw = False
            return_fields = [b'RFC822']

        mails = {}
        try:
            for uid in uids:
                result = self.conn.fetch(uid, return_fields)

                if not result:
                    continue

                if return_raw:
                    mails[uid] = result[uid]
                else:
                    # mails[uid] = Mail(logger=self.logger, uid=uid, mail_native=email.message_from_bytes(result[uid][b'RFC822']))
                    mails[uid] = Mail(logger=self.logger,
                                      mail_native=email.message_from_bytes(
                                          result[uid][b'RFC822']))
            return self.Retval(True, mails)

        except IMAPClient.Error as e:
            return self.process_error(e)

    @do_select_mailbox
    def get_mailflags(self, uids, mailbox):
        """
        Retrieve flags from mails
        """
        try:
            result = self.conn.get_flags(uids)
            flags = {}

            for uid in uids:
                flags[uid] = []
                if uid not in result.keys():
                    self.logger.error(
                        'Failed to get flags for mail with uid={}: {}'.format(
                            uid, result))
                    return self.Retval(False, None)
                for flag in result[uid]:
                    flags[uid].append(flag.decode('utf-8'))
            return self.Retval(True, flags)

        except IMAPClient.Error as e:
            return self.process_error(e)

    @do_select_mailbox
    def set_mailflags(self, uids, mailbox, flags=[]):
        """
        Set and retrieve flags from mails
        """
        if self.test:
            self.logger.info(
                'Would have set mail flags on message uids "{}"'.format(
                    str(uids)))
            return self.Retval(True, None)
        else:
            self.logger.debug('Setting flags={} on mails uid={}', flags, uids)
            try:
                result = self.conn.set_flags(uids, flags)

                _flags = {}
                for uid in uids:
                    _flags[uid] = []
                    if uid not in result.keys():
                        self.logger.error(
                            'Failed to set and get flags for mail with uid={}: {}'
                            .format(uid, result))
                        return self.Retval(False, None)
                    for flag in result[uid]:
                        _flags[uid].append(flag.decode('utf-8'))
                return self.Retval(True, _flags)
            except IMAPClient.Error as e:
                return self.process_error(e)

    @do_select_mailbox
    def add_mailflags(self, uids, mailbox, flags=[]):
        """
        Add and retrieve flags from mails
        """
        if self.test:
            self.logger.info(
                'Would have added mail flags on message uids "{}"'.format(
                    str(uids)))
            return self.Retval(True, None)
        else:
            self.logger.debug('Adding flags={} on mails uid={}', flags, uids)
            try:
                result = self.conn.add_flags(uids, flags)

                _flags = {}
                for uid in uids:
                    _flags[uid] = []
                    if uid not in result.keys():
                        self.logger.error(
                            'Failed to add and get flags for mail with uid={}: {}'
                            .format(uid, result))
                        return self.Retval(False, None)
                    for flag in result[uid]:
                        _flags[uid].append(flag.decode('utf-8'))
                return self.Retval(True, _flags)
            except IMAPClient.Error as e:
                return self.process_error(e)

    @do_select_mailbox
    def move_mail(self,
                  message_ids,
                  source,
                  destination,
                  delete_old=True,
                  expunge=True,
                  add_flags=None,
                  set_flags=None):
        """
        Move a mail from a mailbox to another
        """
        return self.copy_mails(message_ids=message_ids,
                               source=source,
                               destination=destination,
                               delete_old=delete_old,
                               expunge=expunge,
                               add_flags=add_flags,
                               set_flags=set_flags)

    @do_select_mailbox
    def copy_mails(self,
                   source,
                   destination,
                   message_ids=None,
                   delete_old=False,
                   expunge=False,
                   add_flags=None,
                   set_flags=None):
        """
        Copies one or more mails from a mailbox into another
        """
        if self.test:
            if delete_old:
                self.logger.info(
                    'Would have moved mail Message-Ids="{}" from "{}" to "{}", skipping because of beeing in testmode'
                    .format(message_ids, source, destination))
            else:
                self.logger.info(
                    'Would have copied mails with Message-Ids="{}" from "{}" to "{}", skipping because of beeing in testmode'
                    .format(message_ids, source, destination))
            return self.Retval(True, None)
        else:
            try:
                if delete_old:
                    self.logger.debug(
                        'Moving mail Message-Ids="{}" from "{}" to "{}"'.
                        format(message_ids, source, destination))
                else:
                    self.logger.debug(
                        'Copying mail Message-Ids="{}" from "{}" to "{}"'.
                        format(message_ids, source, destination))

                # if message_ids is None:
                #    message_ids = []
                #    result = self.fetch_mails(uids=uids, mailbox=source)
                #    if not result.code:
                #        self.logger.error('Failed to determine Message-Id by uids for mail with uids "{}"', uids)
                #        return result
                #    message_ids.append(result.data.keys())

                if not self.mailbox_exists(destination).data:
                    self.logger.info(
                        'Destination mailbox {} doesn\'t exist, creating it for you'
                        .format(destination))

                    result = self.create_mailbox(mailbox=destination)
                    if not result.code:
                        self.logger.error(
                            'Failed to create the mailbox {}: {}'.format(
                                source, result.data))  # pragma: no cover
                        return result  # pragma: no cover

                uids = []
                for message_id in message_ids:
                    result = self.search_mails(
                        mailbox=source,
                        criteria='HEADER Message-Id "{}"'.format(message_id))

                    if not result.code or len(result.data) == 0:
                        self.logger.error(
                            'Failed to determine uid by Message-Id for mail with Message-Id "{}"'
                            .format(message_id))
                        return self.Retval(False, result.data)
                    uids.append(result.data[0])

                result = self.select_mailbox(source)
                if not result.code:
                    return result  # pragma: no cover

                self.conn.copy(uids, destination)

                if delete_old:
                    result = self.delete_mails(uids=uids, mailbox=source)
                    if not result.code:
                        self.logger.error(
                            'Failed to remove old mail with Message-Id="{}"/uids="{}": {}'
                            .format(message_ids, uids,
                                    result.data))  # pragma: no cover
                        return result  # pragma: no cover

                    if expunge:  # TODO don't expunge by default
                        result = self.expunge(mailbox=source)
                        if not result.code:
                            self.logger.error(
                                'Failed to expunge on mailbox {}: {}'.format(
                                    source, result.data))  # pragma: no cover
                            return result  # pragma: no cover

                dest_uids = []
                for message_id in message_ids:
                    result = self.search_mails(
                        mailbox=destination,
                        criteria='HEADER Message-Id "{}"'.format(message_id))
                    if not result.code:
                        self.logger.error(
                            'Failed to determine uid by Message-Id for mail with Message-Id "{}"'
                            .format(message_id))  # pragma: no cover
                        return result  # pragma: no cover
                    dest_uids.append(result.data[0])

                if isinstance(set_flags, list):
                    self.set_mailflags(uids=dest_uids,
                                       mailbox=destination,
                                       flags=set_flags)
                if add_flags:
                    self.add_mailflags(uids=dest_uids,
                                       mailbox=destination,
                                       flags=add_flags)

                return self.Retval(True, dest_uids)

            except IMAPClient.Error as e:
                return self.process_error(e)

    @do_select_mailbox
    def expunge(self, mailbox):
        """
        Expunge mails form a mailbox
        """
        self.logger.debug('Expunge mails from mailbox {}'.format(mailbox))
        try:
            return self.Retval(True, b'Expunge completed.'
                               in self.conn.expunge())
        except IMAPClient.Error as e:  # pragma: no cover
            return self.process_error(e)  # pragma: no cover

    def create_mailbox(self, mailbox):
        """
        Create a mailbox
        """
        self.logger.debug('Creating mailbox {}'.format(mailbox))
        try:
            return self.Retval(
                True,
                self.conn.create_folder(mailbox) == b'Create completed.')
        except IMAPClient.Error as e:
            return self.process_error(e)

    def mailbox_exists(self, mailbox):
        """
        Check whether a mailbox exists
        """
        try:
            return self.Retval(True, self.conn.folder_exists(mailbox))
        except IMAPClient.Error as e:  # pragma: no cover
            return self.process_error(e)  # pragma: no cover

    @do_select_mailbox
    def delete_mails(self, uids, mailbox):
        """
        Delete mails
        """
        self.logger.debug('Deleting mails with uid="{}"'.format(uids))
        try:
            result = self.conn.delete_messages(uids)
            flags = {}

            for uid in uids:
                flags[uid] = []
                if uid not in result.keys():
                    self.logger.error(
                        'Failed to get flags for mail with uid={} after deleting it: {}'
                        .format(uid, result))
                    return self.Retval(False, None)
                for flag in result[uid]:
                    flags[uid].append(flag.decode('utf-8'))
            return self.Retval(True, flags)
        except IMAPClient.Error as e:
            return self.process_error(e)
Exemple #13
0
class EmailClient(object):
    def __init__(self, config):
        """
        Create an email client abstraction configured for an account.

        config is a dict with keys:
            HOST
            USERNAME
            PASSWORD
            USE_SSL default=True
            FOLDER default='INBOX'
            SELECTORS default=['NOT DELETED']
        """
        self.config = {'USE_SLL': True, 'FOLDER': 'INBOX', 'SELECTORS': ['NOT DELETED']}
        self.config.update(config)

    def connect(self):
        self.server = IMAPClient(self.config['HOST'], use_uid=True, ssl=self.config['USE_SSL'])
        self.server.login(self.config['USERNAME'], self.config['PASSWORD'])
        select_info = self.server.select_folder(self.config['FOLDER'])

    def process_new_messages(self, debug=False):
        """
        Get mesages from the server.

        Note: new information on existing uids will be ignored.
        For example, if the rfc_size changes (which is a strangely common occurrence),
        the new value will be ignored.
        """
        if debug: print "searching"
        messages = self.server.search(self.config['SELECTORS'])
        if debug: print "done searching"
        self._process_messages(messages, debug=debug)

    def _process_messages(self, uids, move=False, debug=False):
        if debug: print "fetching"
        response = self.server.fetch(uids, ['FLAGS', 'RFC822', 'RFC822.SIZE'])
        for msg_uid, data in response.iteritems():
            if debug: print "processing %s" % msg_uid
            with transaction.commit_on_success():
                # extract data
                flags    = data['FLAGS']
                rfc_size = data['RFC822.SIZE']
                raw_body = data['RFC822']
                seq      = data['SEQ']

                # save objects
                email, _ = EmailMessage.objects.get_or_create(
                    uid      = msg_uid,
                    username = self.config['USERNAME'],
                    host     = self.config['HOST'],
                    defaults = {
                        'raw_body': raw_body,
                        'rfc_size': rfc_size,
                        'seq':      data['SEQ'],
                    } )

                email.save()

                for flag in flags:
                    EmailFlag.objects.get_or_create(email=email, flag=flag)[0].save()

                # move message
                if move:
                    self.server.copy([msg_uid], 'auto_processed')
                    self.server.delete_messages([msg_uid])
Exemple #14
0
class CuckooRequest(object):

    def __init__(self, message):
        self.message = message
        
        '''cuckooinbox config variables'''
        config = Config(cfg=os.path.join(CUCKOO_ROOT,"cuckooinbox","cuckooinbox.conf"))
        config = config.get('cuckooinbox')
        self.username = config['username']
        self.passwd = config['passwd']
        self.imap = config['imap']
        self.imap_ssl = config['imap_ssl']
        self.smtp_server = config['smtp']
        self.interval = config['interval']
        self.archive_folder = config['archive_folder']
        self.email_whitelist = config['email_whitelist']
        self.url_limit = config['url_limit']
        self.attachment_limit = config['attachment_limit'] 
        self.zip_reports = config['zip_reports']
        self.zip_password = config['zip_password']
        self.url_blacklist = config['url_blacklist']
        self.url_file_backlist = config['url_file_backlist']
        self.machine = config['machine']
        
        '''imap variables'''
        self.server = IMAPClient(self.imap, use_uid=True, ssl=self.imap_ssl)
        self.server.login(self.username, self.passwd)
        self.attachment_counter = 0
        
        '''message variables'''
        self.msg = MIMEMultipart()
        self.response_msg = MIMEMultipart()
        self.response_urls = []
        self.response_attachments = []
        self.sender = ''
        self.subject = ''
        self.cc_list = []

        '''logging object'''
        self.log_entry = Logger('cuckooinbox.log')

        '''cuckoo variables'''
        self.taskids = []
        self.db  = Database()
        self.url_counter = 0 # tracks url count to not exceed url_limit
    
    def fetch(self, message):
        
        '''set retrieve folder'''
        select_info = self.server.select_folder('INBOX')
        '''fetch mail'''
        response = self.server.fetch(self.message, ['RFC822'])
    
        '''parse received email'''
        for msgid, data in response.iteritems():
            msg_string = data['RFC822']
            self.msg = email.message_from_string(msg_string)
            
            '''parse 'Name <*****@*****.**>' format'''
            if '<' in self.msg['From']: self.sender = self.msg['From'].split('<'[0])[-1][:-1]            
            else: self.sender = self.msg['From']
            self.subject = self.msg['Subject']
            
            '''print and log successful receive'''
            self.log_entry.logEvent('[+] Received email ID: %d from %s [%s]' % (msgid, self.msg['From'], self.msg['Subject']))
            
            '''save CC info for reply later'''
            if self.msg['Cc']:
                for address in  self.msg['Cc'].split(', '): self.cc_list.append(address)
                self.log_entry.logEvent( '[*] Email \"%s\" from %s cc\'d the following addresses: %s' % (self.msg['Subject'],self.msg['From'],', '.join(str(copies) for copies in self.cc_list)))
            
            file_whitelist = ['exe', 'doc', 'docx', 'xls', 'xlsx', 'pdf', 'zip']
            
            '''parse message elements'''
            for part in self.msg.walk():
                if part.get_content_type() == 'text/plain':
                    self.log_entry.logEvent( '[*] Email ID: %d has a plain text object.' % msgid)
                    content = part.get_payload()
                    self.processText(content)
                elif part.get_content_type() == 'text/html':
                    self.log_entry.logEvent('[*] Email ID: %d has a html object.' % msgid)
                    content = part.get_payload()
                    self.processText(content)
                elif 'application' in part.get_content_type():
                    # email attachment has no filename
                    if not part.get_param('name'): return 0
                    # cuckoo file analysis whitelist
                    if not part.get_param('name').split('.'[0])[-1] in file_whitelist: break
                    # increment and break if limit is reached 
                    if (self.attachment_limit != 0 and self.attachment_counter == self.attachment_limit): break
                    self.attachment_counter += 1
                    self.log_entry.logEvent('[*] Email ID: %d has an attachment object.' % msgid)
                    content = part.get_payload()
                    file_name = part.get_param('name')
                    self.processAttachment(content, file_name)
                    
            '''archive email when done submitting cuckoo tasks'''
            try:
                self.archive(self.message)
            except:
                self.server.delete_messages(self.message)
                self.server.expunge()

    def processText(self, content):
        
        '''reformat quoted string mail to plain html'''
        body = quopri.decodestring(content)
        soup = BeautifulSoup(body)
        # todo analyze href spoof
        
        '''parse and analyze hyperlinks'''
        for url in soup.findAll('a'):
            # strip mailto links
            if url['href'].split(':'[0])[0] == 'mailto' : continue
            # strip blacklist links
            for item in self.url_blacklist.split(','):
                if item in url['href']: return 0
            # strip blacklist link filetypes
            for item in self.url_file_backlist.split(','):
                if item in url['href'].split('.'[0])[-1]: return 0
            else:
                self.response_urls.append(url['href'])
		if self.machine:
                    task_id = self.db.add_url(url['href'], package="ie", timeout=15, machine=self.machine)
                else: task_id = self.db.add_url(url['href'], package="ie", timeout=15)
                if task_id:
                    self.taskids.append(task_id)
                    self.log_entry.logEvent('[+] URL \"%s\" added as task with ID %d' % (url['href'],task_id))
                    # increment counter and exit loop if limit is reached
                    self.url_counter += 1
                    if (self.url_limit != 0 and self.url_counter == self.url_limit): return 0
                else:
                    self.log_entry.logEvent("[!] Error: adding task to database" % (url['href'],task_id))
                    break

    def processAttachment(self, content, filename):
        '''create temp file for analysis'''
        temp_file = tempfile.NamedTemporaryFile(prefix=filename.split('.'[0])[0], suffix='.' + filename.split('.'[0])[1])
        temp_file.write(content)
        self.response_attachments.append(filename)
        
        '''add to cuckoo tasks'''
        task_id = self.db.add_path(temp_file.name, timeout=10, package=filename.split('.'[0])[1])
        temp_file.flush()
        if task_id:
            self.taskids.append(task_id)
            self.log_entry.logEvent('[+] File \"%s\" added as task with ID %d' % (filename,task_id))
        else:
            self.taskids.append(task_id)
            self.log_entry.logEvent("[!] Error adding task to database")
            
        '''make sure file gets submitted before we toss it'''
        timeout = time.time() + 120
        while time.time() < timeout:
            if os.path.exists(os.path.join(CUCKOO_ROOT,"storage","analyses",str(task_id),"reports","report.html")): continue
            time.sleep(.25)
        temp_file.close()


    def archive(self, message):

        select_info = self.server.select_folder('INBOX')
        '''cleanup mailbox'''
        self.server.copy(self.message,self.archive_folder)
        self.server.delete_messages(self.message)
        self.server.expunge()
        
    def expunge(self, message):

        select_info = self.server.select_folder('INBOX')
        '''expunge cuckooinbox request'''
        self.server.delete_messages(self.message)
        self.server.expunge()


    def zipResults(self,):
        
        '''create temporary zip file'''
        temp_zip = tempfile.TemporaryFile(prefix='report',suffix='.zip')
        zip_file = zipfile.ZipFile(temp_zip, 'w')
        if self.zip_password: zip_file.setpassword(self.zip_password)
        
        '''set zip to compress'''
        try:
            import zlib
            compression = zipfile.ZIP_DEFLATED
        except:
            compression = zipfile.ZIP_STORED
        modes = { zipfile.ZIP_DEFLATED: 'deflated',
                zipfile.ZIP_STORED:   'stored',}
        
        '''wait for reports to finish then add to list'''
        for id in self.taskids:
            # timeout error handling
            if not os.path.exists(os.path.join(CUCKOO_ROOT,"storage","analyses",str(id),"reports","report.html")):
                self.log_entry.logEvent('cuckooinbox error: report timeout reached on task ID %d.' % id)
            else: 
                zip_file.write(os.path.join(CUCKOO_ROOT,"storage","analyses",str(id),"reports","report.html"),\
                arcname = 'report' + str(id) + '.html', compress_type=compression)
        zip_file.close()
            
        '''attach zip to email message'''
        temp_zip.seek(0)
        email_file = MIMEBase('application', 'zip')
        email_file.set_payload(temp_zip.read())
        Encoders.encode_base64(email_file)
        email_file.add_header('Content-Disposition', 'attachment; filename="report.zip"')
        self.response_msg.attach(email_file)

    def sendReport(self,):

        '''create email header'''
        assert type(self.cc_list)==list
        assert type(self.taskids)==list
        self.response_msg['From'] = self.username
        self.response_msg['To'] = self.sender
        self.response_msg['Cc'] = ", ".join(self.cc_list)
        self.response_msg['Date'] = formatdate(localtime=True)
        self.response_msg['Subject'] = 'cuckooinbox report: ' + self.subject

        '''attach cuckooinbox email body'''
        for id in self.taskids:
	    '''wait for reports to finish before sending'''
	    timeout = time.time() + 120
            while time.time() < timeout:
                if os.path.exists(os.path.join(CUCKOO_ROOT,"storage","analyses",str(id),"reports","report.html")): continue
            	time.sleep(.25)
            if os.path.exists(os.path.join(CUCKOO_ROOT,"storage","analyses",str(id),"reports","inbox.html")):
                file = open(os.path.join(CUCKOO_ROOT,"storage","analyses",str(id),"reports","inbox.html"))
                body = '<html>' + \
                    '<div class="section-title">'+ \
                    '<h2>Task ID %d <small></small></h2>' % id + \
                    '</div>'+ \
                    '<table class="table table-striped table-bordered">'+ \
                    file.read() + \
                    '</html>'
                file.close()
                response_text = ''.join(body)
                self.response_msg.attach(MIMEText(response_text,'html'))
            else: print '[!] Could not find cuckoobox report files.'

        '''wait for analysis to finish and zip the reports'''
        self.zipResults()
        
        '''send the message'''
	if '@gmail.com' in self.username:
	    smtp = smtplib.SMTP('smtp.gmail.com',587)
	    smtp.starttls()
	    smtp.login(self.username, self.passwd)
	else:
            smtp = smtplib.SMTP(self.smtp_server)
            try: smtp.login(self.username,self.passwd)
            except:
                self.log_entry.logEvent('[!] SMTP login failed.')
        try: smtp.sendmail(self.username, self.sender, self.response_msg.as_string())
        except:
            self.log_entry.logEvent('SMTP message %s failed to send.' % self.subject)
        smtp.close()
        self.log_entry.logEvent('[-] Sent "%s" report to %s' % (self.subject, self.sender))
        self.server.logout()
Exemple #15
0
def main():
    # -options-
    parser = OptionParser("python %prog [options] CONFIG_FILE")
    parser.add_option('-v',
                      '--verbose',
                      action='store_true',
                      dest='verbose',
                      default=False)
    (options, args) = parser.parse_args()

    # -settings-
    if len(args) < 1:
        parser.print_help()
        exit()

    if not os.path.exists(args[0]):
        raise IOError(ENOENT, 'Archivo de configuracion no encontrado',
                      args[0])

    config = RawConfigParser()
    config.read(args[0])
    # put read config options (from the .ini) into global namespace and in uppercase
    for name, value in config.items('chkemail'):
        globals()[name.upper()] = value

    # -workflow-
    # create, connect, and login to server
    server = IMAPClient(HOST, use_uid=True)
    try:
        server.login(USER, PASSWORD, port=PORT)
    except NameError:  # if PORT is not defined
        server.login(USER, PASSWORD)

    inbox = server.select_folder('INBOX')
    if options.verbose:
        print '%d messages in INBOX (included deleted)' % inbox['EXISTS']

    messages = server.search(['NOT DELETED', 'HEADER Content-Type mixed'])
    if options.verbose:
        print "%d messages with possible attch" % len(messages)

    # fetch data from messages, put each message (Mess object) into the msgs list
    scan = server.fetch(messages, ['BODYSTRUCTURE', 'ENVELOPE'])
    msgs = dict()
    for mid, response in scan.iteritems():
        # Mess class only works with mulipart messages
        if response['BODYSTRUCTURE'].is_multipart:
            msgs[mid] = Mess(mid, response['ENVELOPE'],
                             response['BODYSTRUCTURE'])

    # Select the messages with attachements I want, put them in group_msgs
    group_msgs = dict()
    for msg in msgs.itervalues():
        group_msgs[msg.id] = list()
        for part_num, part_info in msg.parts.iteritems():
            if part_info.filename:
                filename = part_info.filename.lower()
                if filename.endswith('.pdf') or filename.endswith('.xml') or \
                 filename.endswith('.zip'):
                    group_msgs[msg.id] += [part_num]
        if not group_msgs[msg.id]:
            del group_msgs[msg.id]

    # fetch all interesting parts
    for msg_id, parts in group_msgs.iteritems():
        request = ['BODY[' + str(part) + ']' for part in parts]
        response = server.fetch(msg_id, request)
        for body_part in response[msg_id].iterkeys():
            if 'BODY' in body_part:
                msgs[msg_id].parts[
                    body_part[5:-1]].data = response[msg_id][body_part]

    # move messages to trash
    if len(group_msgs.keys()) > 0:
        server.copy(group_msgs.keys(), 'INBOX.Trash')
        server.delete_messages(group_msgs.keys())
    server.logout()

    # ensure there's an OUTPUT_DIR directory
    pdf_dir = os.path.join(OUTPUT_PDF, strftime('%Y-%m'))
    ensure_dir(pdf_dir)
    ensure_dir(OUTPUT_DIR)

    # decode and write data to file
    num_attch = 0
    for msg in msgs.itervalues():
        for part in msg.parts:
            if part.data:
                filename = part.filename.lower()
                if filename.endswith('.pdf') or filename.endswith('.xml'):
                    if filename.endswith('.pdf'):
                        ensure_dir(os.path.join(pdf_dir, str(msg.envelope)))
                        attachment_filename = generate_filename(
                            os.path.join(pdf_dir, str(msg.envelope),
                                         os.path.basename(part.filename)))
                    else:
                        attachment_filename = generate_filename(
                            os.path.join(OUTPUT_DIR,
                                         os.path.basename(part.filename)))
                    with open(attachment_filename, 'wb') as file_:
                        if part.encoding == 'quoted-printable':
                            file_.write(decodestring(part.data))
                        elif part.encoding == 'base64':
                            file_.write(b64decode(part.data))
                    num_attch += 1
                elif filename.endswith('.zip') and part.encoding == 'base64':
                    with tempfile.TemporaryFile() as tmp_zip:
                        tmp_zip.write(b64decode(part.data))
                        zip_file = ZipFile(tmp_zip)
                        for f_info in zip_file.infolist():
                            if f_info.filename.lower().endswith('.xml'):
                                attachment_filename = generate_filename(
                                    os.path.join(
                                        OUTPUT_DIR,
                                        os.path.basename(f_info.filename)))
                            elif f_info.filename.lower().endswith('.pdf'):
                                ensure_dir(
                                    os.path.join(pdf_dir, str(msg.envelope)))
                                attachment_filename = generate_filename(
                                    os.path.join(
                                        pdf_dir, str(msg.envelope),
                                        os.path.basename(f_info.filename)))
                            else:
                                continue
                            with open(attachment_filename, 'wb') as file_:
                                file_.write(zip_file.read(f_info))
                            num_attch += 1

    if options.verbose:
        print '%d files extracted' % num_attch
Exemple #16
0
class gmailPy(object):
    def __init__(self):
        self.IMAP_SERVER = 'imap.gmail.com'
        self.ssl = True
        self.myIMAPc = None
        self.response = None
        self.folders = []

    def login(self, username, password):
        self.myIMAPc = IMAPClient(self.IMAP_SERVER, ssl=self.ssl)
        self.myIMAPc.login(username, password)

    # Returns a list of all the folders for a particular account
    def get_folders(self):
        self.response = self.myIMAPc.list_folders()
        for item in self.response:
            self.folders.append(item[2].strip('u'))
        return self.folders

    # Returns the total number of messages in a folder
    def get_mail_count(self, folder='Inbox'):
        self.response = self.myIMAPc.select_folder(folder, True)
        return self.response['EXISTS']

    # Method to delete messages based on their size
    def delete_bigmail(self, folder='Inbox'):
        self.myIMAPc.select_folder(folder, False)
        # Gets all the message ids of the messages which are not deleted in the folder
        messages = self.myIMAPc.search(['NOT DELETED'])
        print "%d messages that aren't deleted" % len(messages)
        if len(messages) > 0:
            print "You can exit by entering 0 or pressing CTRL+C \n"
        else: print "There are no messages in the folder"
        # Gets the message sizes for all the message ids returned in previous step
        # Note: Just sends one request for all message ids with a return time < 10 ms
        self.response = self.myIMAPc.fetch(messages, ['RFC822.SIZE'])
        # Sorts the dictionary returned by fetch by size in descending order
        sorted_response = sorted(self.response.iteritems(), key=operator.itemgetter(1), reverse=True)
        count = 1
        try:
            for item in sorted_response:
                # Gets the biggest message including headers, body, etc.
                big_message = self.myIMAPc.fetch(item[0], ['RFC822'])
                for msgid, data in big_message.iteritems():
                    msg_string = data['RFC822']
                    # Parses the message string using email library
                    msg = email.message_from_string(msg_string)
                    val = dict(self.response[msgid])['RFC822.SIZE']
                    print 'ID %d: From: %s Date: %s' % (msgid, msg['From'], msg['date'])
                    print 'To: %s' % (msg['To'])
                    print 'Subject: %s' % (msg['Subject'])
                    print 'Size: %d bytes \n' % (val)
                    user_del = raw_input("Do you want to delete this message?(Y/N): ")
                    if user_del == 'Y':
                        self.delete_message(msgid)
                        if count == len(sorted_response):
                            print "There are no more messages"
                        else:
                            print "\nMoving on to the next biggest message >>> \n"
                    elif user_del == '0':
                        print "Program exiting"
                        sys.exit()
                    else:
                        if count == len(sorted_response):
                            print "There are no more messages"
                        else:
                            print "\nMoving on to the next biggest message >>> \n"
                    count += 1
        except KeyboardInterrupt:
            print "Program exiting"
            sys.exit()

    # Method to delete messages based on their size with a search criteria
    def delete_bigmail_search(self, folder='Inbox', command='', criteria=''):
        self.myIMAPc.select_folder(folder, False)
        # Gets all the message ids from the server based on the search criteria
        messages = self.myIMAPc.search('%s "%s"' % (command, criteria))
        print "%d messages that match --> %s: %s" % (len(messages), command, criteria)
        if len(messages) > 0:
            print "You can exit by entering 0 or pressing CTRL+C \n"
        else: print "There are no messages in that matched your search criteria"
        # Gets the message sizes for all the message ids returned in previous step
        # Note: Just sends one request for all message ids with a return time < 10 ms
        self.response = self.myIMAPc.fetch(messages, ['RFC822.SIZE'])
        # Sorts the messages in decending order of their sizes
        sorted_response = sorted(self.response.iteritems(), key=operator.itemgetter(1), reverse=True)
        count = 1
        try:
            for item in sorted_response:
                # Gets the entire content for the biggest message identified
                big_message = self.myIMAPc.fetch(item[0], ['RFC822'])
                for msgid, data in big_message.iteritems():
                    msg_string = data['RFC822']
                    msg = email.message_from_string(msg_string)
                    val = dict(self.response[msgid])['RFC822.SIZE']
                    print 'ID %d: From: %s Date: %s' % (msgid, msg['From'], msg['date'])
                    print 'To: %s' % (msg['To'])
                    print 'Subject: %s' % (msg['Subject'])
                    print 'Size: %d bytes \n' % (val)
                    user_del = raw_input("Do you want to delete this message?(Y/N): ")
                    if user_del == 'Y':
                        self.delete_message(msgid)
                        if count == len(sorted_response):
                            print "There are no more messages"
                        else:
                            print "\nMoving on to the next biggest message >>> \n"
                    elif user_del == '0':
                        print "Program exiting"
                        sys.exit()
                    else:
                        if count == len(sorted_response):
                            print "There are no more messages"
                        else:
                            print "\nMoving on to the next biggest message >>> \n"
                    count += 1

        except KeyboardInterrupt:
            print "Program exiting"
            sys.exit()

    # Deletes a message in the current folder based on msg id
    def delete_message(self, id):
        try:
            self.myIMAPc.delete_messages([id])
            self.myIMAPc.expunge()
            print "Message deleted"
        except IMAPClient.Error as err:
            print "Message deletion failed"
            print err

    # Renames a folder
    def rename_folder(self, oldfolder, newfolder):
        try:
            self.myIMAPc.rename_folder(oldfolder, newfolder)
            print "Folder %s renamed to %s" % (oldfolder, newfolder)
        except IMAPClient.Error as err:
            print "Folder renaming failed"
            print err

    # Creates a new folder
    def create_folder(self, folder):
        try:
            self.myIMAPc.create_folder(folder)
            print "New folder %s created" % folder
        except IMAPClient.Error as err:
            print "Folder creation failed"
            print err

    # Deletes a folder
    def delete_folder(self, folder):
        try:
            self.myIMAPc.delete_folder(folder)
            print "Folder %s deleted" % folder
        except IMAPClient.Error as err:
            print "Folder deletion failed"
            print err

    # Creates a new folder and copies the content from the two folders that need to be merged
    # Then deletes the old folders
    def merge_folders(self, merged_folder, folder_1, folder_2):
        try:
            self.create_folder(merged_folder)
            # Selects the folder with read/write permission
            self.myIMAPc.select_folder(folder_1, True)
            messages = self.myIMAPc.search(['NOT DELETED'])
            print "Moving %d messages from %s to %s" % (len(messages), folder_1, merged_folder)
            self.myIMAPc.copy(messages, merged_folder)
            self.myIMAPc.select_folder(folder_2, True)
            messages = self.myIMAPc.search(['NOT DELETED'])
            print "Moving %d messages from %s to %s" % (len(messages), folder_2, merged_folder)
            self.myIMAPc.copy(messages, merged_folder)
            print "Deleting %s and %s..." % (folder_1, folder_2)
            self.delete_folder(folder_1)
            self.delete_folder(folder_2)
            print "Merge folder operation succeeded"
        except IMAPClient.Error as err:
            print "Merge operation failed"
            print err

    def logout(self):
        self.myIMAPc.logout()