Skip to content

Commit

Permalink
Merge pull request #30 from itz-Amethyst/main
Browse files Browse the repository at this point in the history
refactor: (IMAPClient) improvement on `Imapclient` to minimize dependency on ctx
  • Loading branch information
sepehr-akbarzadeh authored Jul 25, 2024
2 parents 62c02df + c4a9f59 commit cd8637e
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 25 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
30 changes: 28 additions & 2 deletions docs/source/getting_started/examples/example1.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
-----------

Expand All @@ -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.

Expand Down
66 changes: 43 additions & 23 deletions sage_imap/services/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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()

0 comments on commit cd8637e

Please sign in to comment.