Skip to content

Commit

Permalink
Merge pull request #17
Browse files Browse the repository at this point in the history
Add Email Listener
  • Loading branch information
Trafire authored Jun 5, 2022
2 parents 96bee33 + 0449dc6 commit 7ba203e
Show file tree
Hide file tree
Showing 11 changed files with 839 additions and 12 deletions.
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

0 comments on commit 7ba203e

Please sign in to comment.