diff --git a/README.md b/README.md index 8f3a43f..7554885 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,8 @@ logging.basicConfig(level=logging.DEBUG) This example demonstrates how to create an IMAP client using the `IMAPClient` class. +The `IMAPClient` class can also be used without a context manager; simply call `connect()` to establish the connection and `disconnect()` to close it + ```python from sage_imap.services import IMAPClient diff --git a/docs/source/getting_started/examples/example1.rst b/docs/source/getting_started/examples/example1.rst index 6a3e4b2..9594bba 100644 --- a/docs/source/getting_started/examples/example1.rst +++ b/docs/source/getting_started/examples/example1.rst @@ -3,6 +3,10 @@ Example 1: Creating an IMAP Client This example demonstrates how to create an IMAP client using the `IMAPClient` class. +The IMAPClient class can be used both with and without a context manager. + +# **With Context Manager** + .. code-block:: python from sage_imap.services.client import IMAPClient @@ -15,6 +19,24 @@ This example demonstrates how to create an IMAP client using the `IMAPClient` cl status, messages = client.select("INBOX") print(f"Selected INBOX with status: {status}") +# **Without Context Manager** + +.. code-block:: python + + from sage_imap.services.client import IMAPClient + + # Initialize and use without context manager + client = IMAPClient('imap.example.com', 'username', 'password') + try: + client.connect() + capabilities = client.connection.capability() + print(f"Server capabilities: {capabilities}") + + status, messages = client.connection.select("INBOX") + print(f"Selected INBOX with status: {status}") + finally: + client.disconnect() + Explanation ----------- @@ -25,11 +47,15 @@ This example illustrates a low-level approach to working with IMAP. If you want - When the `with` block is entered, the connection to the IMAP server is established, and the user is authenticated. - When the `with` block is exited, the connection is automatically closed, ensuring that resources are cleaned up properly. -2. **Why Use IMAPClient**: +2. **IMAPClient Without Context Manager**: + - You can also use the `IMAPClient` class without a context manager. In this case, you need to manually call `connect()` to establish the connection and `disconnect()` to close it. + - This approach provides explicit control over when the connection is opened and closed but requires careful handling to ensure resources are properly released. + +3. **Why Use IMAPClient**: - The `IMAPClient` exists to simplify the management of IMAP connections. By using it as a context manager, you don't have to worry about manually opening and closing the connection. This reduces the risk of resource leaks and makes your code cleaner and more maintainable. - Within the context manager, you have access to the `imaplib` capabilities directly through the `client` object. This allows you to perform various IMAP operations seamlessly. -3. **Capabilities and Select Methods**: +4. **Capabilities and Select Methods**: - The `.capability()` method is called to retrieve the server's capabilities, providing information about what commands and features the server supports. - The `.select("INBOX")` method is used to select the "INBOX" mailbox for further operations. It returns the status of the selection and the number of messages in the mailbox. diff --git a/sage_imap/services/client.py b/sage_imap/services/client.py index d3c803f..4433c23 100644 --- a/sage_imap/services/client.py +++ b/sage_imap/services/client.py @@ -13,14 +13,14 @@ class IMAPClient: - """ - A context manager class for managing IMAP connections. + """A class for managing IMAP connections. Purpose ------- This class provides a convenient way to establish and manage a connection to an IMAP server, handling the connection, login, and logout processes - within a context manager. It ensures proper cleanup and error handling. + either within a context manager or through explicit connection management. + It ensures proper cleanup and error handling. Parameters ---------- @@ -44,17 +44,30 @@ class IMAPClient: Methods ------- - __enter__() + connect() Establishes an IMAP connection and logs in. - __exit__(exc_type, exc_value, traceback) + disconnect() Logs out from the IMAP server and closes the connection. + __enter__() + Establishes an IMAP connection and logs in (for context manager). + __exit__(exc_type, exc_value, traceback) + Logs out from the IMAP server and closes the connection (for context manager). Example ------- + Using as context manager: >>> with IMAPClient('imap.example.com', 'username', 'password') as client: ... status, messages = client.select("INBOX") ... # Process messages + + Using without context manager: + >>> client = IMAPClient('imap.example.com', 'username', 'password') + >>> client.connect() + >>> status, messages = client.connection.select("INBOX") + >>> # Process messages + >>> client.disconnect() """ + def __init__(self, host: str, username: str, password: str): self.host: str = host @@ -63,12 +76,12 @@ def __init__(self, host: str, username: str, password: str): self.connection: Optional[imaplib.IMAP4_SSL] = None logger.debug("IMAPClient initialized with host: %s", self.host) - def __enter__(self) -> imaplib.IMAP4_SSL: + def connect(self) -> imaplib.IMAP4_SSL: """ Establishes an IMAP connection and logs in. This method resolves the IMAP server hostname, establishes a secure IMAP - connection, + connection, ensures that no existing connection is open, and logs in using the provided username and password. If any error occurs during these steps, appropriate custom exceptions are raised. @@ -85,6 +98,10 @@ def __enter__(self) -> imaplib.IMAP4_SSL: imaplib.IMAP4_SSL The established IMAP connection object. """ + if self.connection is not None: + logger.warning("Already connected to the IMAP server.") + return self.connection + try: logger.debug("Resolving IMAP server hostname: %s", self.host) resolved_host = socket.gethostbyname(self.host) @@ -109,29 +126,21 @@ def __enter__(self) -> imaplib.IMAP4_SSL: raise IMAPAuthenticationError("IMAP login failed.") from e return self.connection + + def __enter__(self) -> imaplib.IMAP4_SSL: + """Establishes an IMAP connection and logs in (for context manager).""" + return self.connect() - def __exit__( - self, - exc_type: Optional[type], - exc_value: Optional[BaseException], - traceback: Optional[object], - ) -> None: + def disconnect(self) -> None: """ Logs out from the IMAP server and closes the connection. This method logs out from the IMAP server and ensures that the connection is properly closed. If an error occurs during logout, an appropriate custom exception is raised. - - Parameters - ---------- - exc_type : type - The exception type, if an exception was raised. - exc_value : Exception - The exception instance, if an exception was raised. - traceback : traceback - The traceback object, if an exception was raised. - + After performing logout operation , the connection is set to None to point out that it + has been closed. + Raises ------ IMAPUnexpectedError @@ -145,5 +154,16 @@ def __exit__( except imaplib.IMAP4.error as e: logger.error("IMAP logout failed: %s", e) raise IMAPUnexpectedError("IMAP logout failed.") from e + finally: + self.connection = None else: logger.debug("No connection to logout from.") + + def __exit__( + self, + exc_type: Optional[type], + exc_value: Optional[BaseException], + traceback: Optional[object], + ) -> None: + """Logs out from the IMAP server and closes the connection (for context manager).""" + self.disconnect() \ No newline at end of file