Skip to content

Commit

Permalink
Tons of changes to get rudimentary Move() working
Browse files Browse the repository at this point in the history
  • Loading branch information
gauravjuvekar committed Aug 2, 2022
1 parent 2243e41 commit c1c74d1
Show file tree
Hide file tree
Showing 14 changed files with 296 additions and 71 deletions.
1 change: 1 addition & 0 deletions doc/src/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
_autosummary/
11 changes: 11 additions & 0 deletions doc/src/_templates/autosummary/class.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{{ fullname | escape | underline }}

.. currentmodule:: {{ module }}

.. autoclass:: {{ objname }}
:members:
:undoc-members:
:special-members:
:show-inheritance:

.. autoclasstoc::
8 changes: 8 additions & 0 deletions doc/src/api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
API reference
=============

.. autosummary::
:toctree: _autosummary
:recursive:

remote_email_filtering
13 changes: 12 additions & 1 deletion doc/src/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,21 @@

extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.autosummary',
'sphinx.ext.doctest',
'sphinx.ext.todo',
'sphinx.ext.coverage',
]
autosummary_generate = True
autosummary_imported_members = True

autoclass_content = 'both'
autodoc_default_options = {
'members': True,
'special-members': '__init__,__call__',
'undoc-members': False,
'show-inheritance': True
}

templates_path = ['_templates']
exclude_patterns = []
Expand All @@ -37,7 +48,7 @@
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output

html_theme = 'alabaster'
html_theme = 'sphinx_rtd_theme'
html_static_path = ['_static']

# -- Options for todo extension ----------------------------------------------
Expand Down
4 changes: 4 additions & 0 deletions doc/src/getting_started.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Getting started
---------------

Just do it!
24 changes: 4 additions & 20 deletions doc/src/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,9 @@ Welcome to remote-email-filtering's documentation!
==================================================

.. toctree::
:maxdepth: 2
:caption: Contents:
:maxdepth: 4



Remote
------
.. automodule:: remote_email_filtering.remote
:members:

Action
------
.. automodule:: remote_email_filtering.action
:members:


Indices and tables
==================

* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
introduction
getting_started
api
10 changes: 10 additions & 0 deletions doc/src/introduction.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Introduction
============

``remote_email_filtering`` is a email client library that lets you use arbitrary
Python code for automation. You can write rules or filters as code on any
machine instead of being restricted to limited rulsets in hosted webmail or GUI
email clients.

As these actions are Python code, you can trigger other automation based on
emails.
3 changes: 2 additions & 1 deletion src/remote_email_filtering/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
__version__ = '0.1.0'

from .main import *
from .types import *
from .main import start
45 changes: 43 additions & 2 deletions src/remote_email_filtering/action.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,53 @@
"""
Basic building blocks for doing stuff with a
:class:`~remote_email_filtering.message.Message`
"""

import abc


class Action(abc.ABC):
"""A callable that does something with a Message"""
"""
A callable that does something with a :class:`~.message.Message`
Instances will be called with :class:`~.message.Message` instances.
The :attr:`remote` attribute will be set to a :class:`Remote` before
calling.
"""

def __init__(self):
self.remote = None

@abc.abstractmethod
def __call__(self, message):
def __call__(self, msg) -> 'Iterable[Action]':
"""
When called, an :class:`Action` should return an iterable of other
:class:`Action` s that will be applied to the ``msg`` being processed.
"""
pass


class Stop(Action):
"""
Stop processing any futher :class:`Action` for the current
:class:`~.message.Message`
"""

def __call__(self, msg):
raise StopIteration()


class Move(Action):
def __init__(self, destination: tuple[str]):
"""
Move the :class:`~.message.Message` to ``destination`` directory.
:param tuple[str] destination: the destination directory on the server
"""
super().__init__()
self.destination = destination

def __call__(self, msg):
self.remote.move_message(msg, self.destination)
return []
50 changes: 39 additions & 11 deletions src/remote_email_filtering/main.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,47 @@
import datetime
import itertools
import time
from pprint import pp
import typing

from . import types

def filter_message(message, filters):
pass


def start_filtering(remote,
filter_map=dict(),
interval=datetime.timedelta(seconds=5)):
def pipeline(message, actions):
actions = iter(actions)
while True:
for dir in remote.list_dirs():
if dir in filter_map:
for message in remote.get_messages(dir):
filter_message(message, filter_map[dir])
try:
action = next(actions)
except StopIteration:
break

action.remote = message.remote
try:
further = action(message)
if further is None:
raise Exception(msg=f'Action: {action} returned None')
except StopIteration:
return
actions = itertools.chain(further, actions)


def start(remote,
dir_actions: typing.Dict[types.Directory, typing.List['Action']] = dict(),
interval=datetime.timedelta(seconds=5),
count=float('inf')):
"""
Start applying :class:`~.action.Action` s to all messages in specified
directories.
:param Dict[Directory, List[Action]] dir_actions:
:class:`~.action.Action` to apply to all messages in the directory
:param interval: duration to wait after each pass
:param count: number of times to loop through all directories
"""
while count > 0:
for dir_ in remote.list_dirs():
if dir_ in dir_actions:
for message in remote.get_messages(dir_):
pipeline(message, dir_actions[dir_])
count -= 1

time.sleep(interval.seconds)
64 changes: 49 additions & 15 deletions src/remote_email_filtering/message.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,73 @@
import email
import email.header
import email.policy
import typing

from . import types

class Message(object):
def __init__(self, rfc822_bytes):
"""
An email message with convenient properties
"""
def __init__(self, uid: types.Uid,
envelope, remote, dir_=None, rfc822_bytes=None):
"""
:param uid: A unique identifier for a message within ``dir_``
:param envelope: The envelope structure parsed from headers
:param remote: A :class:`~.remote.Remote` used to lazy-load the body
:param tuple[str] dir_: the mailbox directory that this email is in
"""
self.uid = uid
self.envelope = envelope._asdict()
for field in ('cc', 'bcc', 'from_', 'reply_to', 'sender', 'to'):
if not self.envelope[field]:
self.envelope[field] = []
self.envelope[field] = tuple((types.Address.from_imapclient(x)
for x in self.envelope[field]))

self.remote = remote
self.dir_ = dir_
self.raw = rfc822_bytes
self.mail = email.message_from_bytes(self.raw)
self._body = None
if rfc822_bytes is not None:
self._body = email.message_from_bytes(self.raw,
policy=email.policy.default)

@property
def body(self):
if self._body is None:
self.raw = self.remote.fetch_body(self.uid)
self._body = email.message_from_bytes(self.raw,
policy=email.policy.default)
return self._body

@property
def To(self):
ret = self.mail['To']
if ret is None:
ret = ''
return ret
return self.envelope['to']

@property
def Cc(self):
return self.envelope['cc']

@property
def From(self):
return self.envelope['from_']

@property
def Recipients(self):
return ', '.join(_ for _ in (self.mail['To'], self.mail['CC'])
if _ is not None)
return self.To + self.Cc

@property
def Subject(self):
return self.mail['Subject']
return self.envelope['subject']

@property
def SaneSubject(self):
ret = self.mail['Subject']
def _SaneSubject(self):
ret = self.body['Subject']
ret = email.header.decode_header(ret)[0]
if ret[1] is not None:
ret = ret[0].decode('ascii', errors='replace')
else:
ret = ret[0]
ret = ret.replace('\n', '')
return ret

@property
def From(self):
return self.mail['From']
Loading

0 comments on commit c1c74d1

Please sign in to comment.