Skip to content

Commit

Permalink
Add partial support for exchange
Browse files Browse the repository at this point in the history
  • Loading branch information
gauravjuvekar committed Sep 15, 2022
1 parent 1416d91 commit a709fd2
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 13 deletions.
11 changes: 6 additions & 5 deletions src/remote_email_filtering/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,23 +46,24 @@ def start(remote,
:param stop_event: Event object that safely exits from a loop before
`count` expires
"""
validity = dict((k, False) for k in dir_actions.keys())
watermarks = dict((k, None) for k in dir_actions.keys())

while count > 0 and not stop_event.is_set():
for dir_ in remote.list_dirs():
if stop_event.is_set():
break

if dir_ not in validity:
if dir_ not in watermarks:
continue

new_validity = remote.dir_validity(dir_)
if validity[dir_] == new_validity:
updated, new_watermark = remote.is_dir_updated(dir_,
watermarks[dir_])
watermarks[dir_] = new_watermark
if not updated:
log.debug(f"No new messages in {dir_}")
continue
else:
log.debug(f"{dir_} has new messages")
validity[dir_] = new_validity

for message in remote.get_messages(dir_):
if stop_event.is_set():
Expand Down
94 changes: 87 additions & 7 deletions src/remote_email_filtering/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
import logging
import typing

import exchangelib
import imapclient
import imapclient.response_types
import oauthlib
import oauthlib.oauth2

from . import message, types

Expand All @@ -16,12 +20,10 @@

class Remote(abc.ABC):
@abc.abstractmethod
def dir_validity(self, dir_: types.Directory):
def is_dir_updated(self, dir_: types.Directory, watermark):
"""
Get an object that represents if a given dir_ on the remote is valid
If this value does not match with a previously returned value, a new
message was received in the directory.
Returns (True/False, new watermark) if there are changes in dir_
compared to the watermark.
"""
pass

Expand Down Expand Up @@ -124,9 +126,10 @@ def __init__(self, host, user, token, **kwargs):
self.connection = imapclient.IMAPClient(host)
self.connection.oauth2_login(user, access_token=token)

def dir_validity(self, dir_):
def is_dir_updated(self, dir_, watermark=None):
ret = self.connection.select_folder('/'.join(dir_))
return (ret[b'UIDVALIDITY'], ret[b'UIDNEXT'])
new_watermark = (ret[b'UIDVALIDITY'], ret[b'UIDNEXT'])
return watermark != new_watermark, new_watermark

def list_dirs(self):
for flags, delim, name in self.connection.list_folders():
Expand Down Expand Up @@ -182,3 +185,80 @@ def remove_flags(self, msg_id, flags):
dir_, uid = msg_id
self.connection.select_folder('/'.join(dir_))
return set(self.connection.remove_flags([uid], flags)[uid])


class Ews(Remote):
def __init__(self, host, user, token, **kwargs):
super().__init__(**kwargs)

self.connection = exchangelib.Account(
primary_smtp_address=user,
config=exchangelib.Configuration(
server=host,
credentials=exchangelib.OAuth2AuthorizationCodeCredentials(access_token=token)),
autodiscover=False,
access_type=exchangelib.DELEGATE)

self.toplevel = self.connection.msg_folder_root

def _resolve_dir(self, parts):
start = self.connection.msg_folder_root
for part in parts:
start = start / part
return start

def is_dir_updated(self, dir_, watermark=None):
dir_ = self._resolve_dir(dir_)
l = list(dir_.sync_items())
return bool(l), None

def list_dirs(self):
toplevel_strip = len(self.toplevel.parts)
for dir_ in self.toplevel.walk():
yield tuple((x.name for x in dir_.parts[toplevel_strip:]))

def list_messages(self, dir_):
dir_obj = self._resolve_dir(dir_)
for msgid in dir_obj.all().values('id', 'changekey'):
yield (dir_, msgid)

def fetch_envelope(self, msg_id):
dir_, msg_id = msg_id
dir_obj = self._resolve_dir(dir_)
msg = dir_obj.get(**msg_id)
envelope = imapclient.response_types.Envelope(
date=msg.datetime_received,
subject=msg.subject.encode('utf-8'),
from_=tuple([types.Address.from_exchangelib(msg.author)]),
sender=(tuple([types.Address.from_exchangelib(msg.sender)])
if msg.sender else None),
reply_to=tuple([types.Address.from_exchangelib(x) for x in
(msg.reply_to if msg.reply_to else [])]),
to=tuple([types.Address.from_exchangelib(x) for x in
(msg.to_recipients if msg.to_recipients else [])]),
cc=tuple([types.Address.from_exchangelib(x) for x in
(msg.cc_recipients if msg.cc_recipients else [])]),
bcc=tuple([types.Address.from_exchangelib(x) for x in
(msg.bcc_recipients if msg.bcc_recipients else [])]),
in_reply_to=msg.in_reply_to,
message_id=msg.message_id)
return envelope

def fetch_multiple_envelopes(self, msg_ids):
for msg_id in msg_ids:
yield self.fetch_envelope(msg_id)

def fetch_body(self, msg_id):
raise NotImplementedError()

def move_message_id(self, msg_id, target_dir):
raise NotImplementedError()

def fetch_flags(self, msg_id):
raise NotImplementedError()

def add_flags(self, msg_id, flags):
raise NotImplementedError()

def remove_flags(self, msg_id, flags):
raise NotImplementedError()
5 changes: 5 additions & 0 deletions src/remote_email_filtering/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ class Address(collections.namedtuple('Address', 'name mailbox host',
def from_imapclient(cls, addr):
return cls(name=addr.name, mailbox=addr.mailbox, host=addr.host)

@classmethod
def from_exchangelib(cls, addr):
mailbox, _, host = addr.email_address.rpartition('@')
return cls(name=addr.name, mailbox=mailbox, host=host)

def re_match(self, addr):
sname, smbox, shost = self
if sname is None:
Expand Down
2 changes: 1 addition & 1 deletion utils/authorized_oauth2_msal.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,4 @@ def main(params, cache_path, username):

result = main(params, args.SECRET_JSON, args.USERNAME)
if result is None or 'access_token' not in result:
raise Exception(msg="Failed to refresh")
raise Exception("Failed to refresh")

0 comments on commit a709fd2

Please sign in to comment.