PK!KKimapy/__init__.py# -*- coding: utf-8 -*- """ API hook """ from .connector import (connect) PK!Yimapy/connector.py# -*- coding: utf-8 -*- """ imapy.connector ~~~~~~~~~~~~~~~ This module contains alias function which passes connection variables to imapy.connect() method. :copyright: (c) 2015 by Vladimir Goncharov. :license: MIT, see LICENSE for more details. """ from . import imap def connect(**kwargs): """Alias function which passes connection variables to imapy.connect() method. """ imapy = imap.IMAP() return imapy.connect(**kwargs) PK!%D!!imapy/email_message.py# -*- coding: utf-8 -*- """ imapy.email_message ~~~~~~~~~~~~~~~~~~~ This module contains EmailMessage class used for parsing email messages and passing calls which modify email state to imapy.IMAP() class. :copyright: (c) 2015 by Vladimir Goncharov. :license: MIT, see LICENSE for more details. """ import re from email.header import decode_header from . import utils from .structures import CaseInsensitiveDict from .packages import six from .exceptions import ( EmailParsingError, ) class EmailMessage(CaseInsensitiveDict): """Class for parsing email message""" def __init__(self, **kwargs): super(EmailMessage, self).__init__() # inject connections self.uid = kwargs.pop('uid', None) self.folder = kwargs.pop('folder', None) self.email_obj = kwargs.pop('email_obj', None) self.imap_obj = kwargs.pop('imap_obj', None) # init self.update(kwargs) self['to'] = [] self['subject'] = '' self['cc'] = [] self['text'] = [] self['html'] = [] self['headers'] = CaseInsensitiveDict() self['flags'] = kwargs.pop('flags', None) self['attachments'] = [] self.parse() def clean_value(self, value, encoding): """Converts value to utf-8 encoding""" if six.PY2: if encoding not in ['utf-8', None]: return value.decode(encoding).encode('utf-8') elif six.PY3: # in PY3 'decode_headers' may return both byte and unicode if isinstance(value, bytes): if encoding in ['utf-8', None]: return utils.b_to_str(value) else: return value.decode(encoding) return value def _normalize_string(self, text): '''Removes excessive spaces, tabs, newlines, etc.''' conversion = { # newlines '\r\n\t': ' ', # replace excessive empty spaces '\s+': ' ' } for find, replace in six.iteritems(conversion): text = re.sub(find, replace, text, re.UNICODE) return text def _get_links(self, text): links = [] """Returns list of found links in text""" matches = re.findall( '(?<=[\s^\<])(?Phttps?\:\/\/.*?)(?=[\s\>$])', text, re.I) if(matches): for match in matches: links.append(match) return list(set(links)) def mark(self, flags): """Alias function for imapy.mark()""" if not isinstance(flags, list): flags = [flags] # update self['flags'] for t in flags: if t[:2] == 'un': if t[2:] in self['flags']: self['flags'].remove(t[2:]) else: if t not in self['flags']: self['flags'].append(t) return self.imap_obj.mark(flags, self.uid) def delete(self): """Alias function for imapy.delete_message""" return self.imap_obj.delete_message(self.uid, self.folder) def copy(self, new_mailbox): """Alias function for imapy.copy_message""" return self.imap_obj.copy_message(self.uid, new_mailbox, self) def move(self, new_mailbox): """Alias function for imapy.copy_message""" return self.imap_obj.move_message(self.uid, new_mailbox, self) def parse(self): """Parses email object and stores data so that email parts can be access with a dictionary syntax like msg['from'], msg['to'] """ # check main body if not self.email_obj.is_multipart(): text = utils.b_to_str(self.email_obj.get_payload(decode=True)) self['text'].append( { 'text': text, 'text_normalized': self._normalize_string(text), 'links': self._get_links(text) } ) # check attachments else: for part in self.email_obj.walk(): # multipart/* are just containers if part.get_content_maintype() == 'multipart': continue content_type = part.get_content_type() if content_type == 'text/plain': # convert text text = utils.b_to_str(part.get_payload(decode=True)) self['text'].append( { 'text': text, 'text_normalized': self._normalize_string(text), 'links': self._get_links(text) } ) elif content_type == 'text/html': # convert html html = utils.b_to_str(part.get_payload(decode=True)) self['html'].append(html) else: try: data = part.get_payload(decode=True) # rare cases when we get decoding error except AssertionError: data = None attachment_fname = decode_header(part.get_filename() or '') filename = self.clean_value( attachment_fname[0][0], attachment_fname[0][1] ) attachment = { 'filename': filename, 'data': data, 'content_type': content_type } self['attachments'].append(attachment) # subject if 'subject' in self.email_obj: msg_subject = decode_header(self.email_obj['subject']) self['subject'] = self.clean_value( msg_subject[0][0], msg_subject[0][1]) # from # cleanup header from_header_cleaned = re.sub('[\n\r\t]+', ' ', self.email_obj['from'] or '') msg_from = decode_header(from_header_cleaned) msg_txt = '' for part in msg_from: msg_txt += self.clean_value(part[0], part[1]) if '<' in msg_txt and '>' in msg_txt: result = re.match('(?P.*)?(?P\<.*\>)', msg_txt, re.U) self['from_whom'] = result.group('from').strip() self['from_email'] = result.group('email').strip('<>') self['from'] = msg_txt else: self['from_whom'] = '' self['from_email'] = self['from'] = msg_txt.strip() # to if 'to' in self.email_obj: msg_to = decode_header(self.email_obj['to']) self['to'] = self.clean_value( msg_to[0][0], msg_to[0][1]).strip('<>') # cc msg_cc = decode_header(str(self.email_obj['cc'])) cc_clean = self.clean_value(msg_cc[0][0], msg_cc[0][1]) if cc_clean and cc_clean.lower() != 'none': # split recepients recepients = cc_clean.split(',') for recepient in recepients: if '<' in recepient and '>' in recepient: # (name)? + email matches = re.findall('((?P.*)?(?P\<.*\>))', recepient, re.U) if matches: for match in matches: self['cc'].append( { 'cc': match[0], 'cc_to': match[1].strip(" \n\r\t"), 'cc_email': match[2].strip("<>"), } ) else: raise EmailParsingError( "Error parsing CC message header. " "Header value: {header}".format(header=cc_clean) ) else: # email only self['cc'].append( { 'cc': recepient, 'cc_to': '', 'cc_email': recepient, } ) # Date self['date'] = self.email_obj['Date'] # message headers for header, val in self.email_obj.items(): if header in self['headers']: self['headers'][header].append(val) else: self['headers'][header] = [val] PK!ifimapy/exceptions.py# -*- coding: utf-8 -*- """ imapy.exceptions ~~~~~~~~~~~~~~~~ This module contains imapy exceptions. :copyright: (c) 2015 by Vladimir Goncharov. :license: MIT, see LICENSE for more details. """ class ImapyException(Exception): def __init__(self, *args, **kwargs): super(ImapyException, self).__init__(*args, **kwargs) """ Imapy Exceptions """ class ImapyLoggedOut(ImapyException): """Raised when user tries to communicate with server after log out""" class WrongDateFormat(ImapyException): """Raised when wrong date format is used in imap search function""" class UnknownEmailMessageType(ImapyException): """Raised when user tries to use email message of unknown type""" class InvalidFolderName(ImapyException): """Raised when user tries to create folder name containing invalid characters""" class InvalidSearchQuery(ImapyException): """Raised when user tries to search for email messages without using the query_builder Q class""" class SearchSyntaxNotSupported(ImapyException): """Raised when user tries to search for email messages using more than 1 parameter containing non-ascii characters""" class TagNotSupported(ImapyException): """Raised when user tries to mark email message with non-standard tag""" class EmailParsingError(ImapyException): """Raised when we cannot correctly parse email field""" class NonexistentFolderError(ImapyException): """Raised when selecting non-existing email folder""" """ MailFolder Exceptions """ class EmailFolderParsingError(ImapyException): """Raised when MailFolder cannot parse folder details""" """ QueryBuilder Exceptions """ class SizeParsingError(ImapyException): """Raised when email size is specified in an unknown format""" """ Third-party Exceptions """ class ConnectionRefused(ImapyException): """ Connection refused """ class InvalidHost(ImapyException): """ Invalid host """ PK!TqXX imapy/imap.py# -*- coding: utf-8 -*- """ imapy.imap ~~~~~~~~~~ Core Imapy module which encapsulates most of its functionality. :copyright: (c) 2015 by Vladimir Goncharov. :license: MIT, see LICENSE for more details. """ import imaplib import socket import email import re from email.mime.base import MIMEBase from . import utils from .mail_folder import MailFolder from .email_message import EmailMessage from .query_builder import Q from .exceptions import ( ImapyLoggedOut, UnknownEmailMessageType, InvalidFolderName, InvalidSearchQuery, TagNotSupported, NonexistentFolderError, ConnectionRefused, InvalidHost ) def is_logged(func): '''Decorator used to check whether the user is logged in while sending commands to IMAP server ''' def wrapper(*args, **kwargs): if args[0].logged_in: return func(*args, **kwargs) else: raise ImapyLoggedOut( 'Trying to send commands after logging out.') return wrapper def refresh_folders(func): '''Decorator used to refresh folder tree during operations involving changing folder name(s) or structure ''' def wrapper(*args, **kwargs): f = func(*args, **kwargs) args[0]._update_folder_info() return f return wrapper class IMAP(): """Class used for interfacing between""" def __enter__(self): return self def __exit__(self, type, value, traceback): self.logout() def __init__(self): """Initialize vars""" self.capabilities = self.separator = None self.folder_capabilities = {} # email flags self.standard_rw_flags = ['Seen', 'Answered', 'Flagged', 'Deleted', 'Draft'] self.standard_r_flags = ['Recent'] self.standard_flags = self.standard_rw_flags + self.standard_r_flags # folders self.selected_folder = self.selected_folder_utf7 = None self.mail_folder_class = MailFolder() # email parsing self.msg_class = EmailMessage ''' Stores folder name which is being operated on. Used in situations when it's required to change folder name temporary to perform some task but return to folder later. For example: emails = box.folder('INBOX').emails(-5) for email in emails: email.copy('Important').mark('Flagged') Folder should be changed 2 times: .copy('Important').mark('Flagged') folder changed from 'Inbox' to 'Important' -------^ ^ folder changed from 'Important' to 'Inbox' ---------------------' ''' self.operating_folder = None def logout(self): """Log out""" # expunge selected folder if selected if self.selected_folder: self.imap.close() self.imap.logout() # cleanup vars self.mail_folders = self.selected_folder = \ self.selected_folder_utf7 = None self.logged_in = False def log_out(self): """Log out alias function""" self.logout() @is_logged def folders(self, search_string=None): """Return list of email all folders or folder names matching the search string """ if search_string: # search folders folders regexp = '' parts = re.split('(? 4 outputs all commands self.debug_level = kwargs.pop('debug_level', 0) if self.ssl: self.lib = imaplib.IMAP4_SSL default_port = imaplib.IMAP4_SSL_PORT else: self.lib = imaplib.IMAP4 default_port = imaplib.IMAP4_PORT self.port = kwargs.pop('port', default_port) try: self.imap = self.lib(self.host, port=self.port) self.imap.debug = self.debug_level self.imap.login(self.username, self.password) # socket errors except socket.error as e: raise ConnectionRefused(e) except socket.gaierror as e: raise InvalidHost(e) self.logged_in = True self.capabilities = self.imap.capabilities # create folder info self._update_folder_info() return self @is_logged def append(self, message, **kwargs): """Append message to the end of mailbox folder.""" # create flags string '(\Seen \Flagged)' flags = kwargs.pop('flags', None) if flags: flags = [f.capitalize() for f in flags] good_flags = list(set(flags) & set(self.standard_rw_flags)) if good_flags: flags = "(\\" + " \\".join(good_flags).strip() + ")" else: flags = None ''' Notice from RFC3501: If a date-time is specified, the internal date SHOULD be set in the resulting message; otherwise, the internal date of the resulting message is set to the current date and time by default. ''' date_time = kwargs.pop('date_time', None) # detect message type if isinstance(message, MIMEBase): msg = utils.b(message.as_string()) else: raise UnknownEmailMessageType( 'Message should be a subclass of email.mime.base.MIMEBase') self.imap.append( '"' + utils.b_to_str(self.selected_folder_utf7) + '"', flags, date_time, msg) return self def email(self, sequence_number): """Helper function for self.emails(). Returns email by its sequence number """ emails = self.emails(sequence_number, sequence_number) if len(emails): return emails[0] return False @is_logged def emails(self, *args, **kwargs): ids_only = kwargs['ids_only'] if 'ids_only' in kwargs else None """Returns emails based on search criteria or sequence set""" if len(args) > 2: raise InvalidSearchQuery( "Emails() method accepts maximum 2 parameters.") elif len(args) == 2: if not isinstance(args[0], int) or not isinstance(args[1], int): raise InvalidSearchQuery( "Emails() method accepts 2 integers as parameters.") if args[1] < 0: raise InvalidSearchQuery( "Emails() method second parameter cannot be negative.") return self._get_emails_by_sequence(args[0], args[1], ids_only=ids_only) elif len(args) == 1: if isinstance(args[0], Q): query = args[0] query.capabilities = self.folder_capabilities[ self.selected_folder] use_query = query.get_query() # call search if len(query.non_ascii_params): # search using charset old_literal = self.imap.literal self.imap.literal = utils.str_to_b( query.non_ascii_params[0]) result, data = self.imap.uid('SEARCH', *use_query) self.imap.literal = old_literal else: result, data = self.imap.uid('SEARCH', *use_query) if data and data[0]: uids = utils.b_to_str(data[0]).split() return self._fetch_emails_info(uids) else: return [] elif isinstance(args[0], int): return self._get_emails_by_sequence(args[0], ids_only=ids_only) else: raise InvalidSearchQuery("Please construct query using query_" "builder Q class or call emails() " "method with integers as parameters.") else: # no parameters - fetch all emails in folder return self._get_emails_by_sequence(ids_only=ids_only) def _get_emails_by_sequence(self, from_id=None, to_id=None, ids_only=False): """Returns emails fetched by their sequence numbers. Sequence number indicates the place of email in folder where 0 is the first email (the oldest) and N-th is the newest in a folder containing N emails. """ from_seq = from_id to_seq = to_id status = self.info() if not status['total']: return False if from_id and from_id < 0: from_seq = status['total'] + from_id + 1 if to_id: raise InvalidSearchQuery( "Invalid use of parameters: accepting only 1 parameter " "when sequence start is negative.") if not from_id: from_seq = 1 if not to_id: to_seq = status['total'] result, data = self.imap.fetch( '{fr}:{to}'.format( fr=max(from_seq, 1), to=max(to_seq, 1)), '(UID)') if isinstance(data, list) and data[0]: uids = [] for inputs in data: match = re.search('UID ([0-9]+)', utils.b_to_str(inputs)) if match: uids.append(match.group(1)) if ids_only: return uids else: return self._fetch_emails_info(uids) return False @is_logged def _fetch_emails_info(self, email_uids): """Fetches email info from server and returns as parsed email objects """ emails = [] uids = ','.join(email_uids) # fetch email without changing 'Seen' state result, data = self.imap.uid('FETCH', uids, '(FLAGS BODY.PEEK[])') if data: total = len(data) for i, inputs in enumerate(data): if type(inputs) is tuple: email_id, raw_email = inputs # Check for email flags/uid added after email contents if (i + 1) < total: email_id += b' ' + data[i + 1] email_id = utils.b_to_str(email_id) raw_email = utils.b_to_str(raw_email) # get UID uid_match = re.match('.*UID (?P[0-9]+)', email_id) uid = uid_match.group('uid') # get FLAGS flags = [] flags_match = re.match('.*FLAGS \((?P.*?)\)', email_id) # cleanup standard tags if flags_match: for f in flags_match.group('flags').split(): if f.title().lstrip('\\') in\ self.standard_rw_flags: flags.append(f.lower().lstrip('\\')) else: flags.append(f) email_obj = email.message_from_string(raw_email) email_parsed = self.msg_class( folder=self.selected_folder, uid=uid, flags=flags, email_obj=email_obj, imap_obj=self) emails.append(email_parsed) return emails @is_logged def mark(self, tags, uid): """Adds or removes standard IMAP flags to message identified by UID""" add_tags = [] remove_tags = [] if not isinstance(tags, list): tags = [tags] for t in tags: tag_clean = t.title() if tag_clean[:2] == 'Un': compare_tag = tag_clean[2:].title() remove_tags.append(compare_tag) else: compare_tag = tag_clean add_tags.append(tag_clean) if compare_tag not in self.standard_rw_flags: allowed_mark = self.standard_rw_flags allowed_unmark = ['Un' + t.lower() for t in self.standard_rw_flags] allowed = ', '.join([str(i) for i in allowed_mark + allowed_unmark]) raise TagNotSupported( 'Using "{tag}" tag to mark email ' 'message is not supported. Please use one ' 'of the following: {allowed}'. format(tag=t, allowed=allowed)) # add tags if add_tags: tag_list = ' '.join(['\\' + t for t in add_tags]) self.imap.uid('STORE', uid, '+FLAGS', '(' + tag_list + ')') # remove tags if remove_tags: tag_list = ' '.join(['\\' + t for t in remove_tags]) self.imap.uid('STORE', uid, '-FLAGS', '(' + tag_list + ')') self._restore_operating_folder() return def _restore_operating_folder(self): ''' Selects operating folder ''' if self.operating_folder: self.folder(self.operating_folder) self.operating_folder = None return def make(self, folder_name): """Alias for make_folder() function""" return self.make_folder() @refresh_folders @is_logged def make_folder(self, folder_name): """ Creates mailbox subfolder with a given name under currently selected folder. """ if type(folder_name) is not list: names = [folder_name] else: names = folder_name for n in names: if self.separator in n: raise InvalidFolderName( "Folder name cannot contain separator symbol: {separator}". format(separator=self.separator)) parent_path = '' if self.selected_folder: parent_path = self.selected_folder + self.separator name = utils.u_to_utf7( '"' + utils.u(parent_path) + utils.u(n) + '"' ) self.imap.create(name) return self @is_logged def copy_message(self, uid, mailbox, msg_instance): """Copy message with specified UID onto end of new_mailbox.""" self.imap.uid('COPY', uid, utils.b('"') + utils.u_to_utf7(utils.u(mailbox)) + utils.b('"')) """ get new UID from imaplib.untagged_responses having format: { ... 'COPYUID': ['1431590004 1 72', '1431590004 1 73', '1431590004 1 74'], ... } """ copy_uid_data = self.imap.untagged_responses['COPYUID'] for i, val in reversed(list(enumerate(copy_uid_data[:]))): _, original_uid, target_uid = val.split() if int(uid) == int(original_uid): del copy_uid_data[i] # update message instance msg_instance.uid = target_uid msg_instance.folder = mailbox # update folder self.operating_folder = self.selected_folder self.folder(mailbox) break return msg_instance @is_logged def move_message(self, uid, mailbox, msg_instance): """Move message with specified UID onto end of new_mailbox.""" msg_folder = self.selected_folder self.copy_message(uid, mailbox, msg_instance) self.delete_message(uid, msg_folder) return msg_instance @is_logged def delete_message(self, uid, folder): """Deletes message with specified UID and folder""" # check email's folder and change it if required if folder != self.selected_folder: self.folder(folder) self.imap.uid('STORE', uid, '+FLAGS', '(\Deleted)') self._restore_operating_folder() return @is_logged def info(self): """Request named status conditions for mailbox.""" info = {'total': None, 'recent': None, 'unseen': None, 'uidnext': None, 'uidvalidity': None} status, result = self.imap.status( utils.b('"') + self.selected_folder_utf7 + utils.b('"'), '(MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN)' ) if result: """Sample response: '"INBOX" (MESSAGES 7527 RECENT 0 UIDNEXT 21264 UIDVALIDITY 2 UNSEEN 1)' """ where = utils.b_to_str(result[0]) messages = re.search('MESSAGES ([0-9]+)', where) if messages: info['total'] = int(messages.group(1)) recent = re.search('RECENT ([0-9]+)', where) if recent: info['recent'] = int(recent.group(1)) unseen = re.search('UNSEEN ([0-9]+)', where) if unseen: info['unseen'] = int(unseen.group(1)) uidnext = re.search('UIDNEXT ([0-9]+)', where) if uidnext: info['uidnext'] = int(uidnext.group(1)) uidvalidity = re.search('UIDVALIDITY ([0-9]+)', where) if uidvalidity: info['uidvalidity'] = int(uidvalidity.group(1)) return info @refresh_folders @is_logged def rename(self, folder_name): """Renames currently selected folder""" sep = self.separator folder_name = utils.u(folder_name) if (sep in self.selected_folder) and (sep not in folder_name): folder_path = self.selected_folder.split(sep)[:-1] folder_name = sep.join(folder_path) + sep + folder_name folder_to_rename = self.selected_folder_utf7 new_name = utils.u_to_utf7(folder_name) ''' Return to authenticated state. That's because some imap servers (like outlook.com) cannot rename currently selected folder & return "NO [CANNOT] Cannot rename selected folder." response ''' self.folder() self.imap.rename( utils.b('"') + folder_to_rename + utils.b('"'), utils.b('"') + new_name + utils.b('"') ) self._update_folder_info() # select new folder self.folder(utils.b_to_str(new_name)) return self @refresh_folders @is_logged def delete(self, folder_names=None): """Deletes list of specified folder names or currently selected folder and returns to authenticated state if currently selected folder is being deleted. """ if folder_names: if not isinstance(folder_names, list): folder_names = [folder_names] # return to authenticated state if self.selected_folder in folder_names: self.folder() for f_name in folder_names: self.imap.delete( utils.b('"') + utils.u_to_utf7( utils.u(f_name) ) + utils.b('"') ) else: # return to authenticated state current_folder = self.selected_folder_utf7 self.folder() self.imap.delete( utils.b('"') + current_folder + utils.b('"')) return self PK!CCimapy/mail_folder.py# -*- coding: utf-8 -*- """ imapy.mail_folder ~~~~~~~~~~~~~~~~~ This module contains MailFolder class used to encapsulate some of functionality for parsing and representing email folder(s) structure. :copyright: (c) 2015 by Vladimir Goncharov. :license: MIT, see LICENSE for more details. """ import re from . import utils from .exceptions import (EmailFolderParsingError) class MailFolder(): """Class for email folder operations""" def __init__(self): """Initialize vars""" # list holding folder names self.folders = [] self.folders_tree = {} self.children = {} # prepare regexp # (\\HasNoChildren) "/" "Bulk Mail" # (\HasNoChildren \Drafts) "." Drafts self.folder_parts = re.compile( r''' # name attributes \(?(?P.*?)?(?.*?)\"\s # inbox name with/without separator \"?(?P.*?)\"?$ ''', re.VERBOSE) def get_folders(self, *args): """Return list of found folders""" self.raw_folders = args[0][1] # get dictionary holding folders info self.folders_tree, self.children = self._get_folders_tree_and_children( self.raw_folders) return self.folders def get_children(self, folder_name): """Returns list of subfolders for current folder""" if folder_name in self.children: return self.children[folder_name] return [] def get_separator(self): """Return hierarchy separator """ return self.separator def _get_folders_tree_and_children(self, raw_folders): """Construct Folders tree and dictionary holding folder children""" obj_list = {} self.folders = [] """setup maximum depth for a folder (used later while merging objects into one) """ max_depth = 0 for raw_folder in raw_folders: # get name attributes, hierarchy delimiter, name if not raw_folder: continue if isinstance(raw_folder, tuple): len_suffix = '{%d}' % len(raw_folder[1]) raw_folder = raw_folder[0][:-len(len_suffix)] + raw_folder[1] # decode folder name raw_folder = utils.utf7_to_unicode(raw_folder) match = self.folder_parts.match(raw_folder) if not match: raise EmailFolderParsingError("Couldn't parse folder info.") # create objects """ Example of format we use: "[Gmail]": # name without path 'name':'[Gmail]', # name with path 'full_name':'[Gmail]', # only attributes defined in RFC3501 # '\' is skipped from the start of attributes 'standard_attributes': ['HasChildren', 'Noselect'], # custom attributes (all the attributes we've found) 'full_attributes':['HasChildren', 'Noselect'] 'children':{}, # empty dict or dict with objects like this 'parent_name':False, 'depth':0, # how deep the folder is (zero is the root) """ # box attributes attributes = match.group('attributes').split() attributes = [a.lstrip('\\') for a in attributes] # separator self.separator = utils.to_unescaped_str(match.group('separator')) # full name (unique identifier for mailbox) full_name = match.group('name') # full name with no path part name = full_name.split(self.separator).pop() # parent's name parent_name = False if full_name.count(self.separator): parent_parts = full_name.split(self.separator) parent_name = self.separator.join( [i for i in parent_parts[:-1]] ) # depth depth = full_name.count(self.separator) if depth > max_depth: max_depth = depth # add folder name to list of folders self.folders.append(utils.to_str(full_name)) # add to object dictionary obj_list[full_name] = { 'full_name': full_name, 'name': name, # TODO -- add only standard values 'standard_attributes': [attr for attr in attributes], 'full_attributes': attributes, # this later is updated while merging objects later 'children': {}, 'parent_name': parent_name, 'depth': depth } # Merge objects into one and create children dict tree, children = self._create_tree_and_children( max_depth, obj_list) return tree, children def get_parent_name(self, folder_name): """Returns name of a parent folder or itself if it is already topmost folder. """ if self.separator not in folder_name: return folder_name parent_parts = folder_name.split(self.separator)[:-1] return self.separator.join([str(i) for i in parent_parts]) def _create_tree_and_children(self, max_depth, obj_list): """Returns 2 things: 1) Folders merged into tree-like dictionary 2) Dictionary containing the children of each folder """ current_depth = 0 result = {} children = {} while current_depth <= max_depth: for folder_name in obj_list: # add only folders from current depth if obj_list[folder_name]['depth'] == current_depth: # does it has a parent ? parent_name = obj_list[folder_name]['parent_name'] if parent_name: # create subtree in result name = '' path = result for i, name in enumerate( folder_name.split(self.separator)): local_name_parts = folder_name.split( self.separator)[:i + 1] local_name = self.separator.join( [s for s in local_name_parts]) if name != self.separator: if name not in path: """ Additional check for Dovecot which can have subfolder without a parent folder """ if local_name in obj_list: path[name] = obj_list[local_name] # nonexistent parent check (Dovecot) if name in path: path = path[name]['children'] # nonexistent parent check (Dovecot) if parent_name in children: # add to children children[parent_name].append(folder_name) children[folder_name] = [] else: # just add result[folder_name] = obj_list[folder_name] # add to children children[folder_name] = [] current_depth += 1 return result, children def __repr__(self): return str(self.folders) PK!imapy/packages/__init__.pyPK!W~\Uf f imapy/packages/imap_utf7.py# This file is from an IMAPClient Python library which # was created and is maintained by Menno Smits . # and is licensed under BSD license. # # The contents of this file has been derived code from the Twisted project # (http://twistedmatrix.com/). The original author is Jp Calderone. # Twisted project license follows: # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. from __future__ import unicode_literals from .six import binary_type, text_type, byte2int, iterbytes, unichr PRINTABLE = set(range(0x20, 0x26)) | set(range(0x27, 0x7f)) def encode(s): """Encode a folder name using IMAP modified UTF-7 encoding. Input is unicode; output is bytes (Python 3) or str (Python 2). If non-unicode input is provided, the input is returned unchanged. """ if not isinstance(s, text_type): return s r = [] _in = [] def extend_result_if_chars_buffered(): if _in: r.extend([b'&', modified_utf7(''.join(_in)), b'-']) del _in[:] for c in s: if ord(c) in PRINTABLE: extend_result_if_chars_buffered() r.append(c.encode('latin-1')) elif c == '&': extend_result_if_chars_buffered() r.append(b'&-') else: _in.append(c) extend_result_if_chars_buffered() return b''.join(r) AMPERSAND_ORD = byte2int(b'&') DASH_ORD = byte2int(b'-') def decode(s): """Decode a folder name from IMAP modified UTF-7 encoding to unicode. Input is bytes (Python 3) or str (Python 2); output is always unicode. If non-bytes/str input is provided, the input is returned unchanged. """ if not isinstance(s, binary_type): return s r = [] _in = bytearray() for c in iterbytes(s): if c == AMPERSAND_ORD and not _in: _in.append(c) elif c == DASH_ORD and _in: if len(_in) == 1: r.append('&') else: r.append(modified_deutf7(_in[1:])) _in = bytearray() elif _in: _in.append(c) else: r.append(unichr(c)) if _in: r.append(modified_deutf7(_in[1:])) return ''.join(r) def modified_utf7(s): s_utf7 = s.encode('utf-7') return s_utf7[1:-1].replace(b'/', b',') def modified_deutf7(s): s_utf7 = b'+' + s.replace(b',', b'/') + b'-' return s_utf7.decode('utf-7') PK!Ԯuuimapy/packages/six.py"""Utilities for writing code that runs on Python 2 and 3""" # Copyright (c) 2010-2015 Benjamin Peterson # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import absolute_import import functools import itertools import operator import sys import types __author__ = "Benjamin Peterson " __version__ = "1.9.0" # Useful for very coarse version differentiation. PY2 = sys.version_info[0] == 2 PY3 = sys.version_info[0] == 3 PY34 = sys.version_info[0:2] >= (3, 4) if PY3: string_types = str, integer_types = int, class_types = type, text_type = str binary_type = bytes MAXSIZE = sys.maxsize else: string_types = basestring, integer_types = (int, long) class_types = (type, types.ClassType) text_type = unicode binary_type = str if sys.platform.startswith("java"): # Jython always uses 32 bits. MAXSIZE = int((1 << 31) - 1) else: # It's possible to have sizeof(long) != sizeof(Py_ssize_t). class X(object): def __len__(self): return 1 << 31 try: len(X()) except OverflowError: # 32-bit MAXSIZE = int((1 << 31) - 1) else: # 64-bit MAXSIZE = int((1 << 63) - 1) del X def _add_doc(func, doc): """Add documentation to a function.""" func.__doc__ = doc def _import_module(name): """Import module, returning the module after the last dot.""" __import__(name) return sys.modules[name] class _LazyDescr(object): def __init__(self, name): self.name = name def __get__(self, obj, tp): result = self._resolve() setattr(obj, self.name, result) # Invokes __set__. try: # This is a bit ugly, but it avoids running this again by # removing this descriptor. delattr(obj.__class__, self.name) except AttributeError: pass return result class MovedModule(_LazyDescr): def __init__(self, name, old, new=None): super(MovedModule, self).__init__(name) if PY3: if new is None: new = name self.mod = new else: self.mod = old def _resolve(self): return _import_module(self.mod) def __getattr__(self, attr): _module = self._resolve() value = getattr(_module, attr) setattr(self, attr, value) return value class _LazyModule(types.ModuleType): def __init__(self, name): super(_LazyModule, self).__init__(name) self.__doc__ = self.__class__.__doc__ def __dir__(self): attrs = ["__doc__", "__name__"] attrs += [attr.name for attr in self._moved_attributes] return attrs # Subclasses should override this _moved_attributes = [] class MovedAttribute(_LazyDescr): def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): super(MovedAttribute, self).__init__(name) if PY3: if new_mod is None: new_mod = name self.mod = new_mod if new_attr is None: if old_attr is None: new_attr = name else: new_attr = old_attr self.attr = new_attr else: self.mod = old_mod if old_attr is None: old_attr = name self.attr = old_attr def _resolve(self): module = _import_module(self.mod) return getattr(module, self.attr) class _SixMetaPathImporter(object): """ A meta path importer to import six.moves and its submodules. This class implements a PEP302 finder and loader. It should be compatible with Python 2.5 and all existing versions of Python3 """ def __init__(self, six_module_name): self.name = six_module_name self.known_modules = {} def _add_module(self, mod, *fullnames): for fullname in fullnames: self.known_modules[self.name + "." + fullname] = mod def _get_module(self, fullname): return self.known_modules[self.name + "." + fullname] def find_module(self, fullname, path=None): if fullname in self.known_modules: return self return None def __get_module(self, fullname): try: return self.known_modules[fullname] except KeyError: raise ImportError("This loader does not know module " + fullname) def load_module(self, fullname): try: # in case of a reload return sys.modules[fullname] except KeyError: pass mod = self.__get_module(fullname) if isinstance(mod, MovedModule): mod = mod._resolve() else: mod.__loader__ = self sys.modules[fullname] = mod return mod def is_package(self, fullname): """ Return true, if the named module is a package. We need this method to get correct spec objects with Python 3.4 (see PEP451) """ return hasattr(self.__get_module(fullname), "__path__") def get_code(self, fullname): """Return None Required, if is_package is implemented""" self.__get_module(fullname) # eventually raises ImportError return None get_source = get_code # same as get_code _importer = _SixMetaPathImporter(__name__) class _MovedItems(_LazyModule): """Lazy loading of moved objects""" __path__ = [] # mark as package _moved_attributes = [ MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"), MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), MovedAttribute("intern", "__builtin__", "sys"), MovedAttribute("map", "itertools", "builtins", "imap", "map"), MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"), MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"), MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"), MovedAttribute("reduce", "__builtin__", "functools"), MovedAttribute("shlex_quote", "pipes", "shlex", "quote"), MovedAttribute("StringIO", "StringIO", "io"), MovedAttribute("UserDict", "UserDict", "collections"), MovedAttribute("UserList", "UserList", "collections"), MovedAttribute("UserString", "UserString", "collections"), MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"), MovedModule("builtins", "__builtin__"), MovedModule("configparser", "ConfigParser"), MovedModule("copyreg", "copy_reg"), MovedModule("dbm_gnu", "gdbm", "dbm.gnu"), MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"), MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), MovedModule("http_cookies", "Cookie", "http.cookies"), MovedModule("html_entities", "htmlentitydefs", "html.entities"), MovedModule("html_parser", "HTMLParser", "html.parser"), MovedModule("http_client", "httplib", "http.client"), MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"), MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), MovedModule("cPickle", "cPickle", "pickle"), MovedModule("queue", "Queue"), MovedModule("reprlib", "repr"), MovedModule("socketserver", "SocketServer"), MovedModule("_thread", "thread", "_thread"), MovedModule("tkinter", "Tkinter"), MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), MovedModule("tkinter_tix", "Tix", "tkinter.tix"), MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"), MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), MovedModule("tkinter_colorchooser", "tkColorChooser", "tkinter.colorchooser"), MovedModule("tkinter_commondialog", "tkCommonDialog", "tkinter.commondialog"), MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), MovedModule("tkinter_font", "tkFont", "tkinter.font"), MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", "tkinter.simpledialog"), MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"), MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"), MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"), MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"), MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"), ] # Add windows specific modules. if sys.platform == "win32": _moved_attributes += [ MovedModule("winreg", "_winreg"), ] for attr in _moved_attributes: setattr(_MovedItems, attr.name, attr) if isinstance(attr, MovedModule): _importer._add_module(attr, "moves." + attr.name) del attr _MovedItems._moved_attributes = _moved_attributes moves = _MovedItems(__name__ + ".moves") _importer._add_module(moves, "moves") class Module_six_moves_urllib_parse(_LazyModule): """Lazy loading of moved objects in six.moves.urllib_parse""" _urllib_parse_moved_attributes = [ MovedAttribute("ParseResult", "urlparse", "urllib.parse"), MovedAttribute("SplitResult", "urlparse", "urllib.parse"), MovedAttribute("parse_qs", "urlparse", "urllib.parse"), MovedAttribute("parse_qsl", "urlparse", "urllib.parse"), MovedAttribute("urldefrag", "urlparse", "urllib.parse"), MovedAttribute("urljoin", "urlparse", "urllib.parse"), MovedAttribute("urlparse", "urlparse", "urllib.parse"), MovedAttribute("urlsplit", "urlparse", "urllib.parse"), MovedAttribute("urlunparse", "urlparse", "urllib.parse"), MovedAttribute("urlunsplit", "urlparse", "urllib.parse"), MovedAttribute("quote", "urllib", "urllib.parse"), MovedAttribute("quote_plus", "urllib", "urllib.parse"), MovedAttribute("unquote", "urllib", "urllib.parse"), MovedAttribute("unquote_plus", "urllib", "urllib.parse"), MovedAttribute("urlencode", "urllib", "urllib.parse"), MovedAttribute("splitquery", "urllib", "urllib.parse"), MovedAttribute("splittag", "urllib", "urllib.parse"), MovedAttribute("splituser", "urllib", "urllib.parse"), MovedAttribute("uses_fragment", "urlparse", "urllib.parse"), MovedAttribute("uses_netloc", "urlparse", "urllib.parse"), MovedAttribute("uses_params", "urlparse", "urllib.parse"), MovedAttribute("uses_query", "urlparse", "urllib.parse"), MovedAttribute("uses_relative", "urlparse", "urllib.parse"), ] for attr in _urllib_parse_moved_attributes: setattr(Module_six_moves_urllib_parse, attr.name, attr) del attr Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes _importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"), "moves.urllib_parse", "moves.urllib.parse") class Module_six_moves_urllib_error(_LazyModule): """Lazy loading of moved objects in six.moves.urllib_error""" _urllib_error_moved_attributes = [ MovedAttribute("URLError", "urllib2", "urllib.error"), MovedAttribute("HTTPError", "urllib2", "urllib.error"), MovedAttribute("ContentTooShortError", "urllib", "urllib.error"), ] for attr in _urllib_error_moved_attributes: setattr(Module_six_moves_urllib_error, attr.name, attr) del attr Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes _importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"), "moves.urllib_error", "moves.urllib.error") class Module_six_moves_urllib_request(_LazyModule): """Lazy loading of moved objects in six.moves.urllib_request""" _urllib_request_moved_attributes = [ MovedAttribute("urlopen", "urllib2", "urllib.request"), MovedAttribute("install_opener", "urllib2", "urllib.request"), MovedAttribute("build_opener", "urllib2", "urllib.request"), MovedAttribute("pathname2url", "urllib", "urllib.request"), MovedAttribute("url2pathname", "urllib", "urllib.request"), MovedAttribute("getproxies", "urllib", "urllib.request"), MovedAttribute("Request", "urllib2", "urllib.request"), MovedAttribute("OpenerDirector", "urllib2", "urllib.request"), MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"), MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"), MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"), MovedAttribute("ProxyHandler", "urllib2", "urllib.request"), MovedAttribute("BaseHandler", "urllib2", "urllib.request"), MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"), MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"), MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"), MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"), MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"), MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"), MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"), MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"), MovedAttribute("HTTPHandler", "urllib2", "urllib.request"), MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"), MovedAttribute("FileHandler", "urllib2", "urllib.request"), MovedAttribute("FTPHandler", "urllib2", "urllib.request"), MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"), MovedAttribute("UnknownHandler", "urllib2", "urllib.request"), MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"), MovedAttribute("urlretrieve", "urllib", "urllib.request"), MovedAttribute("urlcleanup", "urllib", "urllib.request"), MovedAttribute("URLopener", "urllib", "urllib.request"), MovedAttribute("FancyURLopener", "urllib", "urllib.request"), MovedAttribute("proxy_bypass", "urllib", "urllib.request"), ] for attr in _urllib_request_moved_attributes: setattr(Module_six_moves_urllib_request, attr.name, attr) del attr Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes _importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"), "moves.urllib_request", "moves.urllib.request") class Module_six_moves_urllib_response(_LazyModule): """Lazy loading of moved objects in six.moves.urllib_response""" _urllib_response_moved_attributes = [ MovedAttribute("addbase", "urllib", "urllib.response"), MovedAttribute("addclosehook", "urllib", "urllib.response"), MovedAttribute("addinfo", "urllib", "urllib.response"), MovedAttribute("addinfourl", "urllib", "urllib.response"), ] for attr in _urllib_response_moved_attributes: setattr(Module_six_moves_urllib_response, attr.name, attr) del attr Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes _importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"), "moves.urllib_response", "moves.urllib.response") class Module_six_moves_urllib_robotparser(_LazyModule): """Lazy loading of moved objects in six.moves.urllib_robotparser""" _urllib_robotparser_moved_attributes = [ MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"), ] for attr in _urllib_robotparser_moved_attributes: setattr(Module_six_moves_urllib_robotparser, attr.name, attr) del attr Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes _importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"), "moves.urllib_robotparser", "moves.urllib.robotparser") class Module_six_moves_urllib(types.ModuleType): """Create a six.moves.urllib namespace that resembles the Python 3 namespace""" __path__ = [] # mark as package parse = _importer._get_module("moves.urllib_parse") error = _importer._get_module("moves.urllib_error") request = _importer._get_module("moves.urllib_request") response = _importer._get_module("moves.urllib_response") robotparser = _importer._get_module("moves.urllib_robotparser") def __dir__(self): return ['parse', 'error', 'request', 'response', 'robotparser'] _importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"), "moves.urllib") def add_move(move): """Add an item to six.moves.""" setattr(_MovedItems, move.name, move) def remove_move(name): """Remove item from six.moves.""" try: delattr(_MovedItems, name) except AttributeError: try: del moves.__dict__[name] except KeyError: raise AttributeError("no such move, %r" % (name,)) if PY3: _meth_func = "__func__" _meth_self = "__self__" _func_closure = "__closure__" _func_code = "__code__" _func_defaults = "__defaults__" _func_globals = "__globals__" else: _meth_func = "im_func" _meth_self = "im_self" _func_closure = "func_closure" _func_code = "func_code" _func_defaults = "func_defaults" _func_globals = "func_globals" try: advance_iterator = next except NameError: def advance_iterator(it): return it.next() next = advance_iterator try: callable = callable except NameError: def callable(obj): return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) if PY3: def get_unbound_function(unbound): return unbound create_bound_method = types.MethodType def create_unbound_method(func, cls): return func Iterator = object else: def get_unbound_function(unbound): return unbound.im_func def create_bound_method(func, obj): return types.MethodType(func, obj, obj.__class__) def create_unbound_method(func, cls): return types.MethodType(func, None, cls) class Iterator(object): def next(self): return type(self).__next__(self) callable = callable _add_doc(get_unbound_function, """Get the function out of a possibly unbound function""") get_method_function = operator.attrgetter(_meth_func) get_method_self = operator.attrgetter(_meth_self) get_function_closure = operator.attrgetter(_func_closure) get_function_code = operator.attrgetter(_func_code) get_function_defaults = operator.attrgetter(_func_defaults) get_function_globals = operator.attrgetter(_func_globals) if PY3: def iterkeys(d, **kw): return iter(d.keys(**kw)) def itervalues(d, **kw): return iter(d.values(**kw)) def iteritems(d, **kw): return iter(d.items(**kw)) def iterlists(d, **kw): return iter(d.lists(**kw)) viewkeys = operator.methodcaller("keys") viewvalues = operator.methodcaller("values") viewitems = operator.methodcaller("items") else: def iterkeys(d, **kw): return d.iterkeys(**kw) def itervalues(d, **kw): return d.itervalues(**kw) def iteritems(d, **kw): return d.iteritems(**kw) def iterlists(d, **kw): return d.iterlists(**kw) viewkeys = operator.methodcaller("viewkeys") viewvalues = operator.methodcaller("viewvalues") viewitems = operator.methodcaller("viewitems") _add_doc(iterkeys, "Return an iterator over the keys of a dictionary.") _add_doc(itervalues, "Return an iterator over the values of a dictionary.") _add_doc(iteritems, "Return an iterator over the (key, value) pairs of a dictionary.") _add_doc(iterlists, "Return an iterator over the (key, [values]) pairs of a dictionary.") if PY3: def b(s): return s.encode("latin-1") def u(s): return s unichr = chr import struct int2byte = struct.Struct(">B").pack del struct byte2int = operator.itemgetter(0) indexbytes = operator.getitem iterbytes = iter import io StringIO = io.StringIO BytesIO = io.BytesIO _assertCountEqual = "assertCountEqual" if sys.version_info[1] <= 1: _assertRaisesRegex = "assertRaisesRegexp" _assertRegex = "assertRegexpMatches" else: _assertRaisesRegex = "assertRaisesRegex" _assertRegex = "assertRegex" else: def b(s): return s # Workaround for standalone backslash def u(s): return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape") unichr = unichr int2byte = chr def byte2int(bs): return ord(bs[0]) def indexbytes(buf, i): return ord(buf[i]) iterbytes = functools.partial(itertools.imap, ord) import StringIO StringIO = BytesIO = StringIO.StringIO _assertCountEqual = "assertItemsEqual" _assertRaisesRegex = "assertRaisesRegexp" _assertRegex = "assertRegexpMatches" _add_doc(b, """Byte literal""") _add_doc(u, """Text literal""") def assertCountEqual(self, *args, **kwargs): return getattr(self, _assertCountEqual)(*args, **kwargs) def assertRaisesRegex(self, *args, **kwargs): return getattr(self, _assertRaisesRegex)(*args, **kwargs) def assertRegex(self, *args, **kwargs): return getattr(self, _assertRegex)(*args, **kwargs) if PY3: exec_ = getattr(moves.builtins, "exec") def reraise(tp, value, tb=None): if value is None: value = tp() if value.__traceback__ is not tb: raise value.with_traceback(tb) raise value else: def exec_(_code_, _globs_=None, _locs_=None): """Execute code in a namespace.""" if _globs_ is None: frame = sys._getframe(1) _globs_ = frame.f_globals if _locs_ is None: _locs_ = frame.f_locals del frame elif _locs_ is None: _locs_ = _globs_ exec("""exec _code_ in _globs_, _locs_""") exec_("""def reraise(tp, value, tb=None): raise tp, value, tb """) if sys.version_info[:2] == (3, 2): exec_("""def raise_from(value, from_value): if from_value is None: raise value raise value from from_value """) elif sys.version_info[:2] > (3, 2): exec_("""def raise_from(value, from_value): raise value from from_value """) else: def raise_from(value, from_value): raise value print_ = getattr(moves.builtins, "print", None) if print_ is None: def print_(*args, **kwargs): """The new-style print function for Python 2.4 and 2.5.""" fp = kwargs.pop("file", sys.stdout) if fp is None: return def write(data): if not isinstance(data, basestring): data = str(data) # If the file has an encoding, encode unicode with it. if (isinstance(fp, file) and isinstance(data, unicode) and fp.encoding is not None): errors = getattr(fp, "errors", None) if errors is None: errors = "strict" data = data.encode(fp.encoding, errors) fp.write(data) want_unicode = False sep = kwargs.pop("sep", None) if sep is not None: if isinstance(sep, unicode): want_unicode = True elif not isinstance(sep, str): raise TypeError("sep must be None or a string") end = kwargs.pop("end", None) if end is not None: if isinstance(end, unicode): want_unicode = True elif not isinstance(end, str): raise TypeError("end must be None or a string") if kwargs: raise TypeError("invalid keyword arguments to print()") if not want_unicode: for arg in args: if isinstance(arg, unicode): want_unicode = True break if want_unicode: newline = unicode("\n") space = unicode(" ") else: newline = "\n" space = " " if sep is None: sep = space if end is None: end = newline for i, arg in enumerate(args): if i: write(sep) write(arg) write(end) if sys.version_info[:2] < (3, 3): _print = print_ def print_(*args, **kwargs): fp = kwargs.get("file", sys.stdout) flush = kwargs.pop("flush", False) _print(*args, **kwargs) if flush and fp is not None: fp.flush() _add_doc(reraise, """Reraise an exception.""") if sys.version_info[0:2] < (3, 4): def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, updated=functools.WRAPPER_UPDATES): def wrapper(f): f = functools.wraps(wrapped, assigned, updated)(f) f.__wrapped__ = wrapped return f return wrapper else: wraps = functools.wraps def with_metaclass(meta, *bases): """Create a base class with a metaclass.""" # This requires a bit of explanation: the basic idea is to make a dummy # metaclass for one level of class instantiation that replaces itself with # the actual metaclass. class metaclass(meta): def __new__(cls, name, this_bases, d): return meta(name, bases, d) return type.__new__(metaclass, 'temporary_class', (), {}) def add_metaclass(metaclass): """Class decorator for creating a class with a metaclass.""" def wrapper(cls): orig_vars = cls.__dict__.copy() slots = orig_vars.get('__slots__') if slots is not None: if isinstance(slots, str): slots = [slots] for slots_var in slots: orig_vars.pop(slots_var) orig_vars.pop('__dict__', None) orig_vars.pop('__weakref__', None) return metaclass(cls.__name__, cls.__bases__, orig_vars) return wrapper def python_2_unicode_compatible(klass): """ A decorator that defines __unicode__ and __str__ methods under Python 2. Under Python 3 it does nothing. To support Python 2 and 3 with a single code base, define a __str__ method returning text and apply this decorator to the class. """ if PY2: if '__str__' not in klass.__dict__: raise ValueError("@python_2_unicode_compatible cannot be applied " "to %s because it doesn't define __str__()." % klass.__name__) klass.__unicode__ = klass.__str__ klass.__str__ = lambda self: self.__unicode__().encode('utf-8') return klass # Complete the moves implementation. # This code is at the end of this module to speed up module loading. # Turn this module into a package. __path__ = [] # required for PEP 302 and PEP 451 __package__ = __name__ # see PEP 366 @ReservedAssignment if globals().get("__spec__") is not None: __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable # Remove other six meta path importers, since they cause problems. This can # happen if six is removed from sys.modules and then reloaded. (Setuptools does # this for some reason.) if sys.meta_path: for i, importer in enumerate(sys.meta_path): # Here's some real nastiness: Another "instance" of the six module might # be floating around. Therefore, we can't use isinstance() to check for # the six meta path importer, since the other six instance will have # inserted an importer with different class. if (type(importer).__name__ == "_SixMetaPathImporter" and importer.name == __name__): del sys.meta_path[i] break del i, importer # Finally, add the importer to the meta path import hook. sys.meta_path.append(_importer) PK!x++imapy/query_builder.py# -*- coding: utf-8 -*- """ imapy.query_builder ~~~~~~~~~~~~~~~~~~~ This module contains Q class for constructing queries for IMAP search function. Note: this class allows to create simple, non-nested AND queries. It means that all search conditions specified by user should be met in search results. Class doesn't currently support joining query conditions with OR and NOT statements. :copyright: (c) 2015 by Vladimir Goncharov. :license: MIT, see LICENSE for more details. """ import re from datetime import datetime from datetime import date from .exceptions import ( SearchSyntaxNotSupported, WrongDateFormat, SizeParsingError ) def convert_units(func): """Decorator used to convert units (KB, MB, B) from string representation to a number. """ def wrapper(*args): what = args[1] if isinstance(what, int): size = what else: what = what.strip('" \'').lower() # Units: B, KB, MB, GB # Byte, Bytes, KiloByte, KiloBytes, MegaByte, MegaBytes, GigaByte, # Gigabytes multiplicator = 1 if ('giga' in what or 'gb' in what): multiplicator = 1000000000 elif ('mega' in what or 'mb' in what): multiplicator = 1000000 elif ('kilo' in what or 'kb' in what): multiplicator = 1000 # clean string what = re.sub( ( "(((giga)|(mega)|(kilo))(bytes?))|" "(gb)|(mb)|(kb)|(bytes?)|(byte)|(b)" ), '', what, count=1) try: what = int(what.strip()) except ValueError: raise SizeParsingError( "Incorrect format used to define message size: {what}." "Please use integer number + one of the following: " "B, Byte, Bytes, Megabyte, Megabytes, Gigabyte, Gigabytes". format(what=args[1]) ) size = multiplicator * what f = func(*[args[0], size]) return f return wrapper def check_date(func): """Decorator used to check the validity of supplied date.""" def wrapper(*args, **kwargs): date_str = args[-1] try: # 1-Feb-2027 datetime.strptime(date_str, '%d-%b-%Y') except ValueError: raise WrongDateFormat( "Wrong date format used. Please " "use \"en-US\" date format. For example: \"2-Nov-{year}\"". format(year=date.today().year)) f = func(*args, **kwargs) return f return wrapper def quote(func): """Decorator used to quote query parameters.""" '''It is here because imaplib v2.58 quotes params differently under different python versions so running UID search under Python 2.7 actually calls: UID SEARCH FROM "Test Account" under Python 3.4: UID SEARCH FROM Test Account (no quotes in later example) ''' def wrapper(*args, **kwargs): # quote last argument if needed last_arg = str(args[-1]).strip('"') if " " in last_arg: last_arg = '"' + last_arg + '"' new_args = args[:-1] + (last_arg,) f = func(*new_args, **kwargs) return f return wrapper class Q: """Class for constructing queries for IMAP search function.""" def __init__(self, **kwargs): self.queries = [] self.capabilities = None self.non_ascii_params = [] def is_ascii(self, txt): """Returns True if string consists of ASCII characters only, False otherwise """ try: txt.encode('utf-8').decode('ASCII') except UnicodeDecodeError: return False return True def get_query(self): """Returns list containing queries""" non_ascii = self._get_non_ascii_params() if len(non_ascii) > 1: raise SearchSyntaxNotSupported( "Searching using more than 1 parameter " "containing non-ascii characters is " "not supported") # do we have non-ascii characters in query ? if len(non_ascii): for k, v in enumerate(self.queries[:]): if v == non_ascii[0]: del self.queries[k] # put list member to the end of list self.queries.append(self.queries.pop(k - 1)) if 'CHARSET' not in self.queries: self.queries = ['CHARSET', 'UTF-8'] + self.queries return self.queries def _get_non_ascii_params(self): """Checks how much query parameters have non-ascii symbols and returns them""" if not self.non_ascii_params: for q in self.queries: if not isinstance(q, int) and not self.is_ascii(q): self.non_ascii_params.append(q) return self.non_ascii_params @quote def sender(self, what): """Messages that contain the specified string in FROM field.""" self.queries += ['FROM', what] return self def answered(self): r"""Messages with the \Answered flag set.""" self.queries += ['ANSWERED'] return self @quote def bcc(self, what): """Messages that contain the specified string in the envelope structure's BCC field.""" self.queries += ['BCC', what] return self @quote def before(self, what): """Messages whose internal date (disregarding time and timezone) is earlier than the specified date.""" self.queries += ['BEFORE', what] return self @quote def body(self, what): """Messages that contain the specified string in the body of the message.""" self.queries += ['BODY', what] return self @quote def cc(self, what): """Messages that contain the specified string in CC field.""" self.queries += ['CC', what] return self def deleted(self): r"""Messages with the \Deleted flag set.""" self.queries += ['DELETED'] return self def draft(self): r"""Messages with the \Draft flag set.""" self.queries.append('DRAFT') return self def flagged(self): r"""Messages with the \Flagged flag set.""" self.queries += ['FLAGGED'] return self @quote def header(self, header, what): """Messages that have a header with the specified field-name (as defined in [RFC-2822]) and that contains the specified string in the text of the header""" self.queries += ['HEADER', header, what] return self @quote def keyword(self, what): """Messages with the specified keyword flag set.""" self.queries += ['KEYWORD', what] return self @convert_units @quote def larger(self, what): """Messages with an [RFC-2822] size larger than the specified number of octets (1 Octet = 1 Byte)""" self.queries += ['LARGER', what] return self def new(self): r"""Messages that have the \Recent flag set but not the \Seen flag. This is functionally equivalent to "(RECENT UNSEEN)".""" self.queries += ['NEW'] return self def old(self): r"""Messages that do not have the \Recent flag set. This is functionally equivalent to "NOT RECENT" (as opposed to "NOT NEW").""" self.queries += ['OLD'] return self @quote @check_date def on(self, what): """Messages whose internal date (disregarding time and timezone) is within the specified date.""" self.queries += ['ON', what] return self def recent(self): r"""Messages that have the \Recent flag set.""" self.queries += ['RECENT'] return self def seen(self): r"""Messages that have the \Seen flag set.""" self.queries += ['SEEN'] return self @quote @check_date def sent_before(self, what): """Messages whose [RFC-2822] Date: header (disregarding time and timezone) is earlier than the specified date""" self.queries += ['SENTBEFORE', what] return self @quote @check_date def sent_on(self, what): """Messages whose [RFC-2822] Date: header (disregarding time and timezone) is within the specified date.""" self.queries += ['SENTON', what] return self @quote @check_date def sent_since(self, what): """Messages whose [RFC-2822] Date: header (disregarding time and timezone) is within or later than the specified date.""" self.queries += ['SENTSINCE', what] return self @quote @check_date def since(self, what): """Messages whose internal date (disregarding time and timezone) is within or later than the specified date.""" self.queries += ['SINCE', what] return self @convert_units @quote def smaller(self, what): """Messages with an [RFC-2822] size smaller than the specified number of octets (1 Octet = 1 Byte)""" self.queries += ['SMALLER', what] return self @quote def subject(self, what): """Messages that contain the specified string in the envelope structure's SUBJECT field.""" self.queries += ['SUBJECT', what] return self @quote def text(self, what): """Messages that contain the specified string in the header or body of the message.""" self.queries += ['TEXT', what] return self @quote def recipient(self, what): """Messages that contain the specified string in the envelope structure's TO field.""" self.queries += ['TO', what] return self @quote def uid(self, what): """Messages with unique identifiers corresponding to the specified unique identifier set. Sequence set ranges are permitted.""" self.queries += ['UID', what] return self def unanswered(self): r"""Messages that do not have the \Answered flag set.""" self.queries += ['UNANSWERED'] return self def undeleted(self): r"""Messages that do not have the \Deleted flag set.""" self.queries += ['UNDELETED'] return self def undraft(self): r"""Messages that do not have the \Draft flag set.""" self.queries += ['UNDRAFT'] return self def unflagged(self): r"""Messages that do not have the \Flagged flag set.""" self.queries += ['UNFLAGGED'] return self @quote def unkeyword(self, what): """Messages that do not have the specified keyword flag set.""" self.queries += ['UNKEYWORD', what] return self def unseen(self): r"""Messages that do not have the \Seen flag set.""" self.queries += ['UNSEEN'] return self PK!jjimapy/structures.py# -*- coding: utf-8 -*- """ imapy.structures ~~~~~~~~~~~~~~~~ This module contains data structures used by Imapy :copyright: (c) 2015 by Vladimir Goncharov. :license: MIT, see LICENSE for more details. """ class CaseInsensitiveDict(dict): """Case-insensitive dictionary object""" def __init__(self, **kwargs): super(CaseInsensitiveDict, self).__init__(self) def __setitem__(self, key, value): super(CaseInsensitiveDict, self).__setitem__(key.lower(), value) def __getitem__(self, key): return super(CaseInsensitiveDict, self).__getitem__(key.lower()) PK!Eimapy/utils.py# -*- coding: utf-8 -*- """ imapy.utils ~~~~~~~~~~~ This module contains utilities used mostly to make imapy work correctly in Python 2 and 3 :copyright: (c) 2015 by Vladimir Goncharov. :license: MIT, see LICENSE for more details. """ from .packages import six from .packages import imap_utf7 if six.PY2: def utf7_to_unicode(text): """Convert string in utf-7 to unicode""" return imap_utf7.decode(text) def str_to_utf7(text): """Convert string to UTF-7""" return imap_utf7.encode(u(text)) def u(text): """Convert to Unicode""" return text.decode('utf-8', 'ignore') def to_str(text): """Convert to UTF-8""" return text.encode('utf-8') def to_unescaped_str(text): """Convert escaped string to string""" return text.decode('string_escape') def b_to_str(text): """Convert to string""" return text def str_to_b(text): """Convert string to bytes""" return text elif six.PY3: def utf7_to_unicode(text): """Convert string in utf-7 to unicode""" return imap_utf7.decode(text) def str_to_utf7(text): """Convert string to UTF-7""" return imap_utf7.encode(text) def u(text): """Convert to Unicode""" return text def to_str(text): """Convert to UTF-8""" return text def to_unescaped_str(text): """Convert escaped string to string""" return text.encode('utf-8').decode('unicode_escape') def b_to_str(text): """Convert to string""" return text.decode('utf-8', 'ignore') def str_to_b(text): """Convert string to bytes""" return text.encode('utf-8') def u_to_utf7(text): """Convert to UTF-7""" return imap_utf7.encode(text) def b(text): if isinstance(text, six.text_type): return text.encode('utf-8') return text PK!H|n-WYimapy-1.2.0.dist-info/WHEEL A н#Z;/" bFF]xzwK;<*mTֻ0*Ri.4Vm0[H, JPK!Hx1Timapy-1.2.0.dist-info/METADATAMN0>\  J@T,ڈ6((qӑ2vJ=6-H=cΓ5 r9%V@ ~ft"`K:밉{Rn>JJMCNŚ,"eZo: I+YY3>S=>r&[;P 6]:1:8d& Mb??I]>/uv_NGwL|PK!H~]imapy-1.2.0.dist-info/RECORDuǎP| bi~ ˜d2 Gh¦ުMkW]mXL(ϗ'd: îwF^ɗ3FuH`<}e) t ?cPw3!R4웓X Sڤj66#|cLp XrY `>!xP_ܒfnXװ&z&o%V-kc;cqO(tq(Η1sUzPjIekxN|2}s˶mǀ)Ւ :ƞ.5ƖT1oHGQ.;4M>'xmS-kD{