From 7d11b590d8ffabf37e17e8edff75ce9086654a14 Mon Sep 17 00:00:00 2001 From: zevav Date: Thu, 26 Jul 2018 15:48:31 -0400 Subject: [PATCH 1/5] added basic structure for vendors module, including mostly blank GmailMessages class. 'vendor' is now a kwarg for Imbox, but there's also a lookup dict created in vendors/__init__.py for 'magic' determination of the vendor from the hostname. --- imbox/__init__.py | 1 + imbox/imbox.py | 21 ++++++++++++++++----- imbox/vendors/__init__.py | 7 +++++++ imbox/vendors/gmail.py | 6 ++++++ 4 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 imbox/vendors/__init__.py create mode 100644 imbox/vendors/gmail.py diff --git a/imbox/__init__.py b/imbox/__init__.py index 5e02e04..232f7a2 100644 --- a/imbox/__init__.py +++ b/imbox/__init__.py @@ -6,3 +6,4 @@ __version__ = '.'.join([str(x) for x in __version_info__]) __all__ = ['Imbox'] + diff --git a/imbox/imbox.py b/imbox/imbox.py index e06d467..f8dfc14 100644 --- a/imbox/imbox.py +++ b/imbox/imbox.py @@ -3,16 +3,20 @@ from imbox.messages import Messages import logging +from imbox.vendors import GmailMessages, hostname_vendorname_dict + logger = logging.getLogger(__name__) class Imbox: def __init__(self, hostname, username=None, password=None, ssl=True, - port=None, ssl_context=None, policy=None, starttls=False): + port=None, ssl_context=None, policy=None, starttls=False, + vendor=None): self.server = ImapTransport(hostname, ssl=ssl, port=port, ssl_context=ssl_context, starttls=starttls) + self.hostname = hostname self.username = username self.password = password @@ -20,6 +24,7 @@ class Imbox: self.connection = self.server.connect(username, password) logger.info("Connected to IMAP Server with user {username} on {hostname}{ssl}".format( hostname=hostname, username=username, ssl=(" over SSL" if ssl or starttls else ""))) + self.vendor = vendor or hostname_vendorname_dict.get(self.hostname) def __enter__(self): return self @@ -64,9 +69,15 @@ class Imbox: msg = " from inbox" logger.info("Fetch list of messages{}".format(msg)) - return Messages(connection=self.connection, - parser_policy=self.parser_policy, - **kwargs) + + messages_class = Messages + + if self.vendor == 'gmail': + messages_class = GmailMessages + + return messages_class(connection=self.connection, + parser_policy=self.parser_policy, + **kwargs) def folders(self): - return self.connection.list() \ No newline at end of file + return self.connection.list() diff --git a/imbox/vendors/__init__.py b/imbox/vendors/__init__.py new file mode 100644 index 0000000..f983626 --- /dev/null +++ b/imbox/vendors/__init__.py @@ -0,0 +1,7 @@ +from imbox.vendors.gmail import GmailMessages + + +hostname_vendorname_dict = {GmailMessages.hostname: GmailMessages.name} + +__all__ = ['GmailMessages', + 'hostname_vendorname_dict'] \ No newline at end of file diff --git a/imbox/vendors/gmail.py b/imbox/vendors/gmail.py new file mode 100644 index 0000000..a9dfa4b --- /dev/null +++ b/imbox/vendors/gmail.py @@ -0,0 +1,6 @@ +from imbox.messages import Messages + + +class GmailMessages(Messages): + hostname = 'imap.gmail.com' + name = 'gmail' From 1fbb8511b97c4d230d0d1cf8b6a9b031053f204d Mon Sep 17 00:00:00 2001 From: zevav Date: Fri, 27 Jul 2018 14:31:21 -0400 Subject: [PATCH 2/5] added `authentication_error_message` as a class attribute to `Imbox`. If `Imbox.vendor` is not None, look for a custom authentication error string, and raise it if there's a problem logging in. The default error value is raised if there is no custom string. created `name_authentication_string_dict` from all subclasses of `Messages` in `vendors/__init__.py`, for the lookup mentioned above. Implement architecture discussed in #131, deciding whether to use `Messages` or a subclass of it in `Imbox.messages` based on the `Imbox.vendor` value. Use the `folder_lookup` dict, present on `Messages` but only filled in on its subclasses, to select the right folder based on a lowercased "standard" name given as the value of `folder`. created a blank `folder_lookup` on `Messages`. filled in the first vendor that subclasses `Messages`, `GmailMessages`. It contains class attributes `authentication_error_message`, `hostname`, `name`, used for purposes described above, as well as `folder_lookup` which is a dict containing aliases, sometimes several, for Gmail's unique folder naming scheme. These aliases are meant to be "human-friendly" for intuitive `folder` selection in calls to `Imbox.messages`. fixes #131. --- imbox/imbox.py | 32 +++++++++++++++++++++++--------- imbox/messages.py | 2 ++ imbox/vendors/__init__.py | 7 +++++-- imbox/vendors/gmail.py | 23 +++++++++++++++++++++++ 4 files changed, 53 insertions(+), 11 deletions(-) diff --git a/imbox/imbox.py b/imbox/imbox.py index f8dfc14..89fa616 100644 --- a/imbox/imbox.py +++ b/imbox/imbox.py @@ -1,15 +1,19 @@ +import imaplib + from imbox.imap import ImapTransport from imbox.messages import Messages import logging -from imbox.vendors import GmailMessages, hostname_vendorname_dict +from imbox.vendors import GmailMessages, hostname_vendorname_dict, name_authentication_string_dict logger = logging.getLogger(__name__) class Imbox: + authentication_error_message = None + def __init__(self, hostname, username=None, password=None, ssl=True, port=None, ssl_context=None, policy=None, starttls=False, vendor=None): @@ -21,10 +25,20 @@ class Imbox: self.username = username self.password = password self.parser_policy = policy - self.connection = self.server.connect(username, password) + self.vendor = vendor or hostname_vendorname_dict.get(self.hostname) + + if self.vendor is not None: + self.authentication_error_message = name_authentication_string_dict.get(self.vendor) + + try: + self.connection = self.server.connect(username, password) + except imaplib.IMAP4.error as e: + if self.authentication_error_message is None: + raise + raise imaplib.IMAP4.error(self.authentication_error_message + '\n' + str(e)) + logger.info("Connected to IMAP Server with user {username} on {hostname}{ssl}".format( hostname=hostname, username=username, ssl=(" over SSL" if ssl or starttls else ""))) - self.vendor = vendor or hostname_vendorname_dict.get(self.hostname) def __enter__(self): return self @@ -62,19 +76,19 @@ class Imbox: def messages(self, **kwargs): folder = kwargs.get('folder', False) + messages_class = Messages + + if self.vendor == 'gmail': + messages_class = GmailMessages + if folder: - self.connection.select(folder) + self.connection.select(messages_class.folder_lookup.get((folder.lower())) or folder) msg = " from folder '{}'".format(folder) else: msg = " from inbox" logger.info("Fetch list of messages{}".format(msg)) - messages_class = Messages - - if self.vendor == 'gmail': - messages_class = GmailMessages - return messages_class(connection=self.connection, parser_policy=self.parser_policy, **kwargs) diff --git a/imbox/messages.py b/imbox/messages.py index 3d44f42..baefae7 100644 --- a/imbox/messages.py +++ b/imbox/messages.py @@ -8,6 +8,8 @@ logger = logging.getLogger(__name__) class Messages: + folder_lookup = {} + def __init__(self, connection, parser_policy, diff --git a/imbox/vendors/__init__.py b/imbox/vendors/__init__.py index f983626..da8a6b9 100644 --- a/imbox/vendors/__init__.py +++ b/imbox/vendors/__init__.py @@ -1,7 +1,10 @@ from imbox.vendors.gmail import GmailMessages +vendors = [GmailMessages] -hostname_vendorname_dict = {GmailMessages.hostname: GmailMessages.name} +hostname_vendorname_dict = {vendor.hostname: vendor.name for vendor in vendors} +name_authentication_string_dict = {vendor.name: vendor.authentication_error_message for vendor in vendors} __all__ = ['GmailMessages', - 'hostname_vendorname_dict'] \ No newline at end of file + 'hostname_vendorname_dict', + 'name_authentication_string_dict'] \ No newline at end of file diff --git a/imbox/vendors/gmail.py b/imbox/vendors/gmail.py index a9dfa4b..97c97d9 100644 --- a/imbox/vendors/gmail.py +++ b/imbox/vendors/gmail.py @@ -2,5 +2,28 @@ from imbox.messages import Messages class GmailMessages(Messages): + authentication_error_message = ('If you\'re not using an app-specific password, grab one here: ' + 'https://myaccount.google.com/apppasswords') hostname = 'imap.gmail.com' name = 'gmail' + folder_lookup = { + + 'all_mail': '"[Gmail]/All Mail"', + 'all': '"[Gmail]/All Mail"', + 'all mail': '"[Gmail]/All Mail"', + 'sent': '"[Gmail]/Sent Mail"', + 'sent mail': '"[Gmail]/Sent Mail"', + 'sent_mail': '"[Gmail]/Sent Mail"', + 'drafts': '"[Gmail]/Drafts"', + 'important': '"[Gmail]/Important"', + 'spam': '"[Gmail]/Spam"', + 'starred': '"[Gmail]/Starred"', + 'trash': '"[Gmail]/Trash"', + + } + + def __init__(self, + connection, + parser_policy, + **kwargs): + super().__init__(connection, parser_policy, **kwargs) From 6f3cc1ab92c4b40a94635d88ff2a311fbb879f7e Mon Sep 17 00:00:00 2001 From: zevav Date: Fri, 27 Jul 2018 14:44:35 -0400 Subject: [PATCH 3/5] prevent too much bookkeeping in vendors/__init__.py by adding each vendor.__name__ to __all__ from the vendors list. --- imbox/vendors/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/imbox/vendors/__init__.py b/imbox/vendors/__init__.py index da8a6b9..ed26aef 100644 --- a/imbox/vendors/__init__.py +++ b/imbox/vendors/__init__.py @@ -5,6 +5,7 @@ vendors = [GmailMessages] hostname_vendorname_dict = {vendor.hostname: vendor.name for vendor in vendors} name_authentication_string_dict = {vendor.name: vendor.authentication_error_message for vendor in vendors} -__all__ = ['GmailMessages', - 'hostname_vendorname_dict', - 'name_authentication_string_dict'] \ No newline at end of file +__all__ = [v.__name__ for v in vendors] + +__all__ += ['hostname_vendorname_dict', + 'name_authentication_string_dict'] From 86cf1fbf9b38fb664c2207667eccf7caff658885 Mon Sep 17 00:00:00 2001 From: zevav Date: Thu, 26 Jul 2018 15:48:31 -0400 Subject: [PATCH 4/5] added basic structure for vendors module, including mostly blank GmailMessages class. 'vendor' is now a kwarg for Imbox, but there's also a lookup dict created in vendors/__init__.py for 'magic' determination of the vendor from the hostname. added `authentication_error_message` as a class attribute to `Imbox`. If `Imbox.vendor` is not None, look for a custom authentication error string, and raise it if there's a problem logging in. The default error value is raised if there is no custom string. created `name_authentication_string_dict` from all subclasses of `Messages` in `vendors/__init__.py`, for the lookup mentioned above. Implement architecture discussed in #131, deciding whether to use `Messages` or a subclass of it in `Imbox.messages` based on the `Imbox.vendor` value. Use the `folder_lookup` dict, present on `Messages` but only filled in on its subclasses, to select the right folder based on a lowercased "standard" name given as the value of `folder`. created a blank `folder_lookup` on `Messages`. filled in the first vendor that subclasses `Messages`, `GmailMessages`. It contains class attributes `authentication_error_message`, `hostname`, `name`, used for purposes described above, as well as `folder_lookup` which is a dict containing aliases, sometimes several, for Gmail's unique folder naming scheme. These aliases are meant to be "human-friendly" for intuitive `folder` selection in calls to `Imbox.messages`. fixes #131. prevent too much bookkeeping in vendors/__init__.py by adding each vendor.__name__ to __all__ from the vendors list. --- imbox/__init__.py | 1 + imbox/imbox.py | 39 ++++++++++++++++++++++++++++++++------- imbox/messages.py | 2 ++ imbox/vendors/__init__.py | 11 +++++++++++ imbox/vendors/gmail.py | 29 +++++++++++++++++++++++++++++ 5 files changed, 75 insertions(+), 7 deletions(-) create mode 100644 imbox/vendors/__init__.py create mode 100644 imbox/vendors/gmail.py diff --git a/imbox/__init__.py b/imbox/__init__.py index 5e02e04..232f7a2 100644 --- a/imbox/__init__.py +++ b/imbox/__init__.py @@ -6,3 +6,4 @@ __version__ = '.'.join([str(x) for x in __version_info__]) __all__ = ['Imbox'] + diff --git a/imbox/imbox.py b/imbox/imbox.py index e06d467..89fa616 100644 --- a/imbox/imbox.py +++ b/imbox/imbox.py @@ -1,23 +1,42 @@ +import imaplib + from imbox.imap import ImapTransport from imbox.messages import Messages import logging +from imbox.vendors import GmailMessages, hostname_vendorname_dict, name_authentication_string_dict + logger = logging.getLogger(__name__) class Imbox: + authentication_error_message = None + def __init__(self, hostname, username=None, password=None, ssl=True, - port=None, ssl_context=None, policy=None, starttls=False): + port=None, ssl_context=None, policy=None, starttls=False, + vendor=None): self.server = ImapTransport(hostname, ssl=ssl, port=port, ssl_context=ssl_context, starttls=starttls) + self.hostname = hostname self.username = username self.password = password self.parser_policy = policy - self.connection = self.server.connect(username, password) + self.vendor = vendor or hostname_vendorname_dict.get(self.hostname) + + if self.vendor is not None: + self.authentication_error_message = name_authentication_string_dict.get(self.vendor) + + try: + self.connection = self.server.connect(username, password) + except imaplib.IMAP4.error as e: + if self.authentication_error_message is None: + raise + raise imaplib.IMAP4.error(self.authentication_error_message + '\n' + str(e)) + logger.info("Connected to IMAP Server with user {username} on {hostname}{ssl}".format( hostname=hostname, username=username, ssl=(" over SSL" if ssl or starttls else ""))) @@ -57,16 +76,22 @@ class Imbox: def messages(self, **kwargs): folder = kwargs.get('folder', False) + messages_class = Messages + + if self.vendor == 'gmail': + messages_class = GmailMessages + if folder: - self.connection.select(folder) + self.connection.select(messages_class.folder_lookup.get((folder.lower())) or folder) msg = " from folder '{}'".format(folder) else: msg = " from inbox" logger.info("Fetch list of messages{}".format(msg)) - return Messages(connection=self.connection, - parser_policy=self.parser_policy, - **kwargs) + + return messages_class(connection=self.connection, + parser_policy=self.parser_policy, + **kwargs) def folders(self): - return self.connection.list() \ No newline at end of file + return self.connection.list() diff --git a/imbox/messages.py b/imbox/messages.py index 3d44f42..baefae7 100644 --- a/imbox/messages.py +++ b/imbox/messages.py @@ -8,6 +8,8 @@ logger = logging.getLogger(__name__) class Messages: + folder_lookup = {} + def __init__(self, connection, parser_policy, diff --git a/imbox/vendors/__init__.py b/imbox/vendors/__init__.py new file mode 100644 index 0000000..ed26aef --- /dev/null +++ b/imbox/vendors/__init__.py @@ -0,0 +1,11 @@ +from imbox.vendors.gmail import GmailMessages + +vendors = [GmailMessages] + +hostname_vendorname_dict = {vendor.hostname: vendor.name for vendor in vendors} +name_authentication_string_dict = {vendor.name: vendor.authentication_error_message for vendor in vendors} + +__all__ = [v.__name__ for v in vendors] + +__all__ += ['hostname_vendorname_dict', + 'name_authentication_string_dict'] diff --git a/imbox/vendors/gmail.py b/imbox/vendors/gmail.py new file mode 100644 index 0000000..97c97d9 --- /dev/null +++ b/imbox/vendors/gmail.py @@ -0,0 +1,29 @@ +from imbox.messages import Messages + + +class GmailMessages(Messages): + authentication_error_message = ('If you\'re not using an app-specific password, grab one here: ' + 'https://myaccount.google.com/apppasswords') + hostname = 'imap.gmail.com' + name = 'gmail' + folder_lookup = { + + 'all_mail': '"[Gmail]/All Mail"', + 'all': '"[Gmail]/All Mail"', + 'all mail': '"[Gmail]/All Mail"', + 'sent': '"[Gmail]/Sent Mail"', + 'sent mail': '"[Gmail]/Sent Mail"', + 'sent_mail': '"[Gmail]/Sent Mail"', + 'drafts': '"[Gmail]/Drafts"', + 'important': '"[Gmail]/Important"', + 'spam': '"[Gmail]/Spam"', + 'starred': '"[Gmail]/Starred"', + 'trash': '"[Gmail]/Trash"', + + } + + def __init__(self, + connection, + parser_policy, + **kwargs): + super().__init__(connection, parser_policy, **kwargs) From f82ce522209d2d3dcbd1a871ec3bfccd1d7173cb Mon Sep 17 00:00:00 2001 From: zevav Date: Fri, 27 Jul 2018 16:00:17 -0400 Subject: [PATCH 5/5] added type hints for new stuff --- imbox/parser.py | 2 ++ imbox/parser.pyi | 4 ++++ imbox/vendors/__init__.pyi | 9 +++++++++ imbox/vendors/gmail.pyi | 14 ++++++++++++++ 4 files changed, 29 insertions(+) create mode 100644 imbox/vendors/__init__.pyi create mode 100644 imbox/vendors/gmail.pyi diff --git a/imbox/parser.py b/imbox/parser.py index 070d730..3351452 100644 --- a/imbox/parser.py +++ b/imbox/parser.py @@ -138,6 +138,8 @@ def fetch_email_by_uid(uid, connection, parser_policy): email_object = parse_email(raw_email, policy=parser_policy) flags = parse_flags(raw_headers.decode()) + if len(flags) > 0: + print(type(flags[0])) email_object.__dict__['flags'] = flags return email_object diff --git a/imbox/parser.pyi b/imbox/parser.pyi index bebbb21..355061e 100644 --- a/imbox/parser.pyi +++ b/imbox/parser.pyi @@ -26,5 +26,9 @@ def parse_attachment(message_part: Message) -> Optional[Dict[str, Union[int, str def decode_content(message: Message) -> str: ... def fetch_email_by_uid(uid: bytes, connection: IMAP4_SSL, parser_policy: Optional[Policy]) -> Struct: ... + raw_headers: bytes + raw_email: bytes + +def parse_flags(headers: str) -> Union[list, List[bytes]]: ... def parse_email(raw_email: bytes, policy: Optional[Policy]) -> Struct: ... diff --git a/imbox/vendors/__init__.pyi b/imbox/vendors/__init__.pyi new file mode 100644 index 0000000..d66c4c6 --- /dev/null +++ b/imbox/vendors/__init__.pyi @@ -0,0 +1,9 @@ +from typing import List, Dict, Optional + +from imbox.messages import Messages + +vendors: List[Messages] + +hostname_vendorname_dict: Dict[str, str] +name_authentication_string_dict: Dict[str, Optional[str]] + diff --git a/imbox/vendors/gmail.pyi b/imbox/vendors/gmail.pyi new file mode 100644 index 0000000..e8c7ade --- /dev/null +++ b/imbox/vendors/gmail.pyi @@ -0,0 +1,14 @@ +import datetime +from email._policybase import Policy +from imaplib import IMAP4, IMAP4_SSL +from typing import Union + +from imbox.messages import Messages + + +class GmailMessages(Messages): + + def __init__(self, + connection: Union[IMAP4, IMAP4_SSL], + parser_policy: Policy, + **kwargs: Union[bool, str, datetime.date]) -> None: ...