From 3df2af5b3647216cd99cc49c5e08640b337c9c56 Mon Sep 17 00:00:00 2001 From: "v.kaukin" Date: Mon, 18 May 2020 14:50:32 +0500 Subject: [PATCH] mailbox.MailBox splitted to: BaseMailBox, MailBox, MailBoxUnencrypted MailBox ssl argument deleted mailbox.MessageFlags class moved to utils.MessageFlags Add PySocks proxy examples --- examples/pysocks_proxy.py | 183 ++++++++++++++++++++++++++++++++++++++ imap_tools/__init__.py | 2 +- imap_tools/mailbox.py | 95 ++++++++++---------- imap_tools/utils.py | 13 +++ release_notes.rst | 7 ++ todo.txt | 2 - 6 files changed, 253 insertions(+), 49 deletions(-) create mode 100644 examples/pysocks_proxy.py diff --git a/examples/pysocks_proxy.py b/examples/pysocks_proxy.py new file mode 100644 index 0000000..e63e7ac --- /dev/null +++ b/examples/pysocks_proxy.py @@ -0,0 +1,183 @@ +""" +MailBox traffic through proxy servers using https://github.com/Anorov/PySocks +NOTE: examples NOT checked! +""" +import ssl +import socks +from imaplib import IMAP4 + + +class Imap4Proxy(IMAP4): + def __init__(self, + host: str = "", + port: int = 143, + p_timeout: int = None, + p_source_address: tuple = None, + p_proxy_type: socks.PROXY_TYPES = "HTTP", + p_proxy_addr: str = None, + p_proxy_port: int = None, + p_proxy_rdns=True, + p_proxy_username: str = None, + p_proxy_password: str = None, + p_socket_options: iter = None, + ): + self._host = host + self._port = port + self._p_timeout = p_timeout + self._p_source_address = p_source_address + self._p_proxy_type = p_proxy_type + self._p_proxy_addr = p_proxy_addr + self._p_proxy_port = p_proxy_port + self._p_proxy_rdns = p_proxy_rdns + self._p_proxy_username = p_proxy_username + self._p_proxy_password = p_proxy_password + self._p_socket_options = p_socket_options + super().__init__(host, port) + + def _create_socket(self): + return socks.create_connection( + dest_pair=(self._host, self._port), + timeout=self._p_timeout, + source_address=self._p_source_address, + proxy_type=self._p_proxy_type, + proxy_addr=self._p_proxy_addr, + proxy_port=self._p_proxy_port, + proxy_rdns=self._p_proxy_rdns, + proxy_username=self._p_proxy_username, + proxy_password=self._p_proxy_password, + socket_options=self._p_socket_options, + ) + + +class Imap4SslProxy(Imap4Proxy): + def __init__(self, + host: str = "", + port: int = 993, + keyfile=None, + certfile=None, + ssl_context=None, + p_timeout: int = None, + p_source_address: tuple = None, + p_proxy_type: socks.PROXY_TYPES = "HTTP", + p_proxy_addr: str = None, + p_proxy_port: int = None, + p_proxy_rdns=True, + p_proxy_username: str = None, + p_proxy_password: str = None, + p_socket_options: iter = None, + ): + self._host = host + self._port = port + self._p_timeout = p_timeout + self._p_source_address = p_source_address + self._p_proxy_type = p_proxy_type + self._p_proxy_addr = p_proxy_addr + self._p_proxy_port = p_proxy_port + self._p_proxy_rdns = p_proxy_rdns + self._p_proxy_username = p_proxy_username + self._p_proxy_password = p_proxy_password + self._p_socket_options = p_socket_options + + if ssl_context is not None and keyfile is not None: + raise ValueError("ssl_context and keyfile arguments are mutually exclusive") + if ssl_context is not None and certfile is not None: + raise ValueError("ssl_context and certfile arguments are mutually exclusive") + if keyfile is not None or certfile is not None: + import warnings + warnings.warn("keyfile and certfile are deprecated, use ssl_context instead", DeprecationWarning, 2) + + if ssl_context is None: + ssl_context = ssl._create_stdlib_context(certfile=certfile, keyfile=keyfile) # noqa + + self.keyfile = keyfile + self.certfile = certfile + self.ssl_context = ssl_context + + super().__init__(host, port, p_timeout, p_source_address, p_proxy_type, p_proxy_addr, p_proxy_port, + p_proxy_rdns, p_proxy_username, p_proxy_password, p_socket_options) + + def _create_socket(self): + sock = super()._create_socket() + server_hostname = self.host if ssl.HAS_SNI else None + return self.ssl_context.wrap_socket(sock, server_hostname=server_hostname) + + def open(self, host='', port=993): + super().open(host, port) + + +class MailBoxUnencryptedProxy: + """Working with the email box through IMAP4 through proxy""" + + def __init__(self, + host: str = "", + port: int = 143, + p_timeout: int = None, + p_source_address: tuple = None, + p_proxy_type: socks.PROXY_TYPES = "HTTP", + p_proxy_addr: str = None, + p_proxy_port: int = None, + p_proxy_rdns=True, + p_proxy_username: str = None, + p_proxy_password: str = None, + p_socket_options: iter = None, + ): + self._host = host + self._port = port + self._p_timeout = p_timeout + self._p_source_address = p_source_address + self._p_proxy_type = p_proxy_type + self._p_proxy_addr = p_proxy_addr + self._p_proxy_port = p_proxy_port + self._p_proxy_rdns = p_proxy_rdns + self._p_proxy_username = p_proxy_username + self._p_proxy_password = p_proxy_password + self._p_socket_options = p_socket_options + super().__init__() + + def _get_mailbox_client(self): + return Imap4Proxy( + self._host, self._port, + self._p_timeout, self._p_source_address, self._p_proxy_type, self._p_proxy_addr, self._p_proxy_port, + self._p_proxy_rdns, self._p_proxy_username, self._p_proxy_password, self._p_socket_options) + + +class MailBoxProxy: + """Working with the email box through IMAP4 over SSL connection through proxy""" + + def __init__(self, + host: str = "", + port: int = 993, + keyfile=None, + certfile=None, + ssl_context=None, + p_timeout: int = None, + p_source_address: tuple = None, + p_proxy_type: socks.PROXY_TYPES = "HTTP", + p_proxy_addr: str = None, + p_proxy_port: int = None, + p_proxy_rdns=True, + p_proxy_username: str = None, + p_proxy_password: str = None, + p_socket_options: iter = None, + ): + self._host = host + self._port = port + self._keyfile = keyfile + self._certfile = certfile + self._ssl_context = ssl_context + self._p_timeout = p_timeout + self._p_source_address = p_source_address + self._p_proxy_type = p_proxy_type + self._p_proxy_addr = p_proxy_addr + self._p_proxy_port = p_proxy_port + self._p_proxy_rdns = p_proxy_rdns + self._p_proxy_username = p_proxy_username + self._p_proxy_password = p_proxy_password + self._p_socket_options = p_socket_options + super().__init__() + + def _get_mailbox_client(self): + return Imap4SslProxy( + self._host, self._port, self._keyfile, self._certfile, self._ssl_context, + self._p_timeout, self._p_source_address, self._p_proxy_type, self._p_proxy_addr, self._p_proxy_port, + self._p_proxy_rdns, self._p_proxy_username, self._p_proxy_password, self._p_socket_options) diff --git a/imap_tools/__init__.py b/imap_tools/__init__.py index 6d1531f..a741c2c 100644 --- a/imap_tools/__init__.py +++ b/imap_tools/__init__.py @@ -4,4 +4,4 @@ from .folder import * from .utils import * -__version__ = '0.14.3' +__version__ = '0.15.0' diff --git a/imap_tools/mailbox.py b/imap_tools/mailbox.py index c226938..64965e0 100644 --- a/imap_tools/mailbox.py +++ b/imap_tools/mailbox.py @@ -2,66 +2,31 @@ from .message import MailMessage from .folder import MailBoxFolderManager -from .utils import cleaned_uid_set, check_command_status +from .utils import cleaned_uid_set, check_command_status, MessageFlags # Maximal line length when calling readline(). This is to prevent reading arbitrary length lines. imaplib._MAXLINE = 4 * 1024 * 1024 # 4Mb -class MessageFlags: - """Standard email message flags""" - SEEN = 'SEEN' - ANSWERED = 'ANSWERED' - FLAGGED = 'FLAGGED' - DELETED = 'DELETED' - DRAFT = 'DRAFT' - RECENT = 'RECENT' - all = ( - SEEN, ANSWERED, FLAGGED, DELETED, DRAFT, RECENT - ) - - -class MailBox: - """Working with the email box through IMAP4""" +class BaseMailBox: + """Working with the email box""" email_message_class = MailMessage folder_manager_class = MailBoxFolderManager - def __init__(self, host='', port=None, ssl=True, keyfile=None, certfile=None, ssl_context=None): - """ - :param host: host's name (default: localhost) - :param port: port number (default: standard IMAP4 SSL port) - :param ssl: use client class over SSL connection (IMAP4_SSL) if True, else use IMAP4 - :param keyfile: PEM formatted file that contains your private key (default: None) - :param certfile: PEM formatted certificate chain file (default: None) - :param ssl_context: SSLContext object that contains your certificate chain and private key (default: None) - Note: if ssl_context is provided, then parameters keyfile or - certfile should not be set otherwise ValueError is raised. - """ - self._host = host - self._port = port - self._keyfile = keyfile - self._certfile = certfile - self._ssl_context = ssl_context - if ssl: - self.box = imaplib.IMAP4_SSL( - host, port or imaplib.IMAP4_SSL_PORT, keyfile, certfile, ssl_context) - else: - self.box = imaplib.IMAP4(host, port or imaplib.IMAP4_PORT) - self._username = None - self._password = None - self._initial_folder = None - self.folder = None + def __init__(self): + self.folder = None # folder manager self.login_result = None + self.box = self._get_mailbox_client() + + def _get_mailbox_client(self) -> imaplib.IMAP4: + raise NotImplementedError def login(self, username: str, password: str, initial_folder: str = 'INBOX'): - self._username = username - self._password = password - self._initial_folder = initial_folder - result = self.box.login(self._username, self._password) + result = self.box.login(username, password) check_command_status('box.login', result) self.folder = self.folder_manager_class(self) - self.folder.set(self._initial_folder) + self.folder.set(initial_folder) self.login_result = result return self # return self in favor of context manager @@ -182,3 +147,41 @@ def __enter__(self): def __exit__(self, exc_type, exc_value, exc_traceback): self.logout() + + +class MailBoxUnencrypted(BaseMailBox): + """Working with the email box through IMAP4""" + + def __init__(self, host='', port=143): + """ + :param host: host's name (default: localhost) + :param port: port number + """ + self._host = host + self._port = port + super().__init__() + + def _get_mailbox_client(self): + return imaplib.IMAP4(self._host, self._port) + + +class MailBox(BaseMailBox): + """Working with the email box through IMAP4 over SSL connection""" + + def __init__(self, host='', port=993, keyfile=None, certfile=None, ssl_context=None): + """ + :param host: host's name (default: localhost) + :param port: port number + :param keyfile: PEM formatted file that contains your private key (deprecated) + :param certfile: PEM formatted certificate chain file (deprecated) + :param ssl_context: SSLContext object that contains your certificate chain and private key + """ + self._host = host + self._port = port + self._keyfile = keyfile + self._certfile = certfile + self._ssl_context = ssl_context + super().__init__() + + def _get_mailbox_client(self): + return imaplib.IMAP4_SSL(self._host, self._port, self._keyfile, self._certfile, self._ssl_context) diff --git a/imap_tools/utils.py b/imap_tools/utils.py index 3666417..850faab 100644 --- a/imap_tools/utils.py +++ b/imap_tools/utils.py @@ -118,3 +118,16 @@ def pairs_to_dict(items: list) -> dict: if len(items) % 2 != 0: raise ValueError('An even-length array is expected') return dict((items[i * 2], items[i * 2 + 1]) for i in range(len(items) // 2)) + + +class MessageFlags: + """Standard email message flags""" + SEEN = 'SEEN' + ANSWERED = 'ANSWERED' + FLAGGED = 'FLAGGED' + DELETED = 'DELETED' + DRAFT = 'DRAFT' + RECENT = 'RECENT' + all = ( + SEEN, ANSWERED, FLAGGED, DELETED, DRAFT, RECENT + ) diff --git a/release_notes.rst b/release_notes.rst index e387026..baf03a0 100644 --- a/release_notes.rst +++ b/release_notes.rst @@ -1,3 +1,10 @@ +0.15.0 +====== +* mailbox.MailBox splitted to: BaseMailBox, MailBox, MailBoxUnencrypted +* MailBox ssl argument deleted +* mailbox.MessageFlags class moved to utils.MessageFlags +* Add PySocks proxy examples + 0.14.3 ====== * Fixed multiple encodings case for attachment name diff --git a/todo.txt b/todo.txt index 364fdd0..ded5f5f 100644 --- a/todo.txt +++ b/todo.txt @@ -1,5 +1,3 @@ try to imitate long poll by NOOP check https://docs.python.org/release/3.8.1/library/email.utils.html - -check possibility to implement SOCKS without/with dependencies