Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Email Listener #17

Merged
merged 4 commits into from
Jun 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[flake8]
application-import-names = promail,tests
ignore = E203,W503,E402,S107,S101
ignore = E203,W503,E402,S107,S101,S403,S303,S301
max-line-length = 88
select = B,B9,BLK,C,D,DAR,E,F,I,S,W
docstring-convention = google
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
[tool.poetry]
name = "promail"
version = "0.4.3"
version = "0.5.0"
authors = ["Antoine Wood <antoinewood@gmail.com>"]
description = "The Python Email Automation Framework"
description = "Promail: The Python Email Automation Framework"
license = "GNU"
readme = "README.md"
homepage = "https://github.com/trafire/promail"
repository = "https://github.com/trafire/promail"
keywords = ["promail", "email", "automation"]
include = ["src/promail/.credentials/gmail_credentials.json"]
documentation = "https://promail.readthedocs.io"

[tool.poetry.dependencies]
python = "^3.8"
Expand Down
85 changes: 78 additions & 7 deletions src/promail/clients/email_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,27 @@
import mimetypes
import os
from email.message import EmailMessage
from typing import List, Optional
from typing import Callable, List, Optional

from promail.core.embedded_attachments import EmbeddedAttachments
from promail.core.messages.messages import Message
from promail.filters.email_filter import EmailFilter


class OutBoundManager(abc.ABC):
class EmailManager:
"""Super class inherited by OutBoundInbound manager."""

def __init__(self, account):
"""Initializes email manager."""
self._account = account


class OutBoundManager(abc.ABC, EmailManager):
"""Outbound Mail class template."""

def __init__(self, account):
"""Initializes OutBoundManager."""
self._account = account
super(OutBoundManager, self).__init__(account)

def send_email(
self,
Expand Down Expand Up @@ -69,7 +79,7 @@ def create_message(
plaintext: str = "",
embedded_attachments: Optional[List[EmbeddedAttachments]] = None,
attachements: Optional[list] = None,
):
) -> EmailMessage:
"""Create Email Message."""
if attachements is None:
attachements = []
Expand Down Expand Up @@ -97,12 +107,73 @@ def create_message(


class InBoundManager(abc.ABC):
"""Outbound Mail class template."""
"""InBound Mail class template."""

def retrieve_last_items(self: object, max_items: int) -> list:
def __init__(self, account):
"""Initializes Inbound Email manager."""
self._registered_functions: dict = {}
self._last_email = None
self._filter_class = None
super(InBoundManager, self).__init__(account)

@property
def registered_functions(self) -> dict:
"""Get Dictionary of (key) filters and (value) registered functions."""
return self._registered_functions

def retrieve_last_items(self: object, max_items: int = 100) -> List[Message]:
"""Get a list of last n items received in inbox.

Args:
max_items: The Maximum number of items to return

Raises:
NotImplementedError: If not implemented by subclass.
"""
pass
raise NotImplementedError(__name__ + " not Implemented")

def _process_filter_messages(
self,
email_filter: EmailFilter,
page_size: int = 100,
page_token: Optional[str] = None,
):
"""Queries Email Server for new messages.

That match filter requisites passes each matched message
to their registered functions.

Args:
email_filter: Email Filter Object, must be a key
of self._registered_functions.
page_size: Number of emails to pull per query,
max number platform dependent (GMAIL: 500).
page_token: Pagenation Token
(may not be used on all platforms).

Raises:
NotImplementedError: If not implemented by subclass.
"""
raise NotImplementedError

def process(self, page_size: int = 100) -> None:
"""Process all filters."""
for email_filter in self._registered_functions:
self._process_filter_messages(
email_filter, page_size=page_size, page_token=None
)

def register(self, **filter_args):
"""Registers a listener function."""

def decorator(func: Callable):
def wrapper(email: EmailMessage):
func(email)

f = self._filter_class(**filter_args)

if f not in self._registered_functions:
self._registered_functions[f] = set()
self._registered_functions[f].add(wrapper)

return decorator
78 changes: 76 additions & 2 deletions src/promail/clients/gmail.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@
from googleapiclient.discovery import build # type: ignore
from googleapiclient.errors import HttpError # type: ignore

from promail.clients.email_manager import InBoundManager, OutBoundManager
from promail.clients.email_manager import (
InBoundManager,
OutBoundManager,
)
from promail.core.embedded_attachments import EmbeddedAttachments
from promail.core.messages.messages import Message
from promail.filters.gmail_filter import GmailFilter


class GmailClient(OutBoundManager, InBoundManager):
Expand Down Expand Up @@ -51,6 +56,7 @@ def __init__(

"""
super(GmailClient, self).__init__(account)
self._filter_class = GmailFilter
sanitized_account: str = "".join(x for x in account if x.isalnum())
self._token_path: str = token_path or os.path.join(
os.getcwd(),
Expand All @@ -64,7 +70,7 @@ def __init__(
".credentials",
"gmail_credentials.json",
)
self.login()
self.service = self.login()
self._clear_token = clear_token

def login(self):
Expand Down Expand Up @@ -137,7 +143,75 @@ def send_email(
except HttpError as error:
print("An error occurred: %s" % error)

# inbound

def _process_filter_messages(self, email_filter, page_size=100, page_token=None):
results = (
self.service.users()
.messages()
.list(
userId="me",
maxResults=page_size,
q=email_filter.get_filter_string(),
pageToken=page_token,
)
.execute()
)

messages = email_filter.filter_results(results["messages"])

for message in messages:
current_message = (
self.service.users()
.messages()
.get(userId="me", id=message["id"], format="raw", metadataHeaders=None)
.execute()
)
email_message = Message(current_message)
for func in self._registered_functions[email_filter]:
func(email_message)
email_filter.add_processed(message["id"])

next_page = results.get("nextPageToken")
if next_page:
self.process_filter_items(email_filter, page_size=100, page_token=next_page)

# gmail specific functionality
def mailboxes(self):
"""Labels that we can filter by."""
return [
label["id"]
for label in self.service.users()
.labels()
.list(userId="me")
.execute()["labels"]
]

def __exit__(self, exc_type, exc_val, exc_tb):
"""If clear token flag has been set will delete token on exit."""
if self._clear_token:
os.remove(self._token_path)


#
# client = GmailClient("antoinewood@gmail.com")
#
#
# @client.register(
# name="search",
# sender=("antoine",),
# newer_than="4d",
# version="10",
# run_completed=True,
# )
# def print_subjects1(email):
# if email.cc:
# print(dir(email.cc))
# print("2", type(email.cc), email.attachments)
#
#
# client.process(100)
# #
# # @client.register(name="search", sender=("antoine",), newer_than="100d")
# # def print_subjects2(email):
# # print("1", email.subject)
1 change: 1 addition & 0 deletions src/promail/core/attachements/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Module for email Attachments."""
39 changes: 39 additions & 0 deletions src/promail/core/attachements/attachments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Email Attachments."""
import email
from email.message import EmailMessage
from io import BytesIO


class Attachments:
"""Email Attachment Reader."""

manager = email.contentmanager.raw_data_manager

def __init__(self, email_attachment: EmailMessage):
"""Initializes Email Attachment."""
self.email_attachment = email_attachment

@property
def filename(self):
"""Get filename."""
return self.email_attachment.get_filename()

def save_file(self, path):
"""Saves file to path provided."""
with open(path, "wb") as file:
file.write(self.email_attachment.get_content(content_manager=self.manager))

def get_data(self) -> BytesIO:
"""Get file data as an inmemory as a BytesIO file-like object."""
obj = BytesIO()
obj.write(self.email_attachment.get_content(content_manager=self.manager))
obj.seek(0)
return obj

def __str__(self):
"""Get string representation."""
return self.filename

def __repr__(self):
"""Get repr representation."""
return f"Attachments({self.__str__()})"
1 change: 1 addition & 0 deletions src/promail/core/messages/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Module for reading received emails."""
84 changes: 84 additions & 0 deletions src/promail/core/messages/messages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Email Message Reader."""
import base64
import email
from email.message import EmailMessage
from email.policy import default

from promail.core.attachements.attachments import Attachments


class Message:
"""Email Message reader."""

def __init__(self, msg: dict) -> None:
"""Initalises Message object.

Args:
msg: email message data containing id
"""
self.msg = email.message_from_bytes(
base64.urlsafe_b64decode(msg["raw"]), _class=EmailMessage, policy=default
)
self._attachments = None

@property
def html_body(self) -> str:
"""Get HTML Body of email."""
return self.msg.get_body(preferencelist=["html"]) # type: ignore

@property
def plain_text(self):
"""Get Plain text body of email."""
return self.msg.get_body(preferencelist=["plain"]) # type: ignore

@property
def sender(self) -> str:
"""Get sender of email."""
return self.msg.get("from")

@property
def cc(self) -> str:
"""Get emails cc'd."""
return self.msg.get("cc")

@property
def bcc(self) -> str:
"""Get emails ccc'd."""
return self.msg.get("bcc")

@property
def message_id(self) -> str:
"""Get Message ID of email."""
return self.msg.get("message-id")

@property
def to(self) -> str:
"""Get to field of email."""
return self.msg.get("to")

@property
def subject(self) -> str:
"""Get Subject of email."""
return self.msg.get("subject")

@property
def date(self):
"""Get Date of Email."""
return self.msg.get("date")

@property
def attachments(self):
"""Get Email Attachments."""
if self._attachments is None:
self._attachments = {}
for email_message_attachment in self.msg.iter_attachments():
print(type(email_message_attachment))
if email_message_attachment.is_attachment():
self._attachments[
email_message_attachment.get_filename()
] = Attachments(email_message_attachment)
return self._attachments

def __str__(self) -> str:
"""String representation."""
return self.subject
1 change: 1 addition & 0 deletions src/promail/filters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Email Filters."""
Loading