PK ! K K imapy/__init__.py# -*- coding: utf-8 -*-
"""
API hook
"""
from .connector import (connect)
PK ! Y imapy/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 ! if imapy/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 ! TqX X
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 ! C C imapy/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 ! Ԯu u imapy/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 ! j j imapy/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 ! E imapy/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-W Y imapy-1.2.0.dist-info/WHEEL
A
н#Z;/"
bFF]xzwK;<*mTֻ0*Ri.4Vm0[H,JPK !Hx1 T imapy-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웓XSڤj66#|cLp XrY`>!xP_ܒfnXװ&z&o%V-kc;cqO(tq(Η1sU zPjIekxN|2}s˶mǀ)Ւ:ƞ.5ƖT1oHGQ.;4M>'xmS-kD{