diff --git a/src/remote_email_filtering/main.py b/src/remote_email_filtering/main.py index 9ed1eb2..ade96d2 100644 --- a/src/remote_email_filtering/main.py +++ b/src/remote_email_filtering/main.py @@ -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(): diff --git a/src/remote_email_filtering/remote.py b/src/remote_email_filtering/remote.py index c3daae1..5ebc40e 100644 --- a/src/remote_email_filtering/remote.py +++ b/src/remote_email_filtering/remote.py @@ -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 @@ -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 @@ -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(): @@ -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() diff --git a/src/remote_email_filtering/types.py b/src/remote_email_filtering/types.py index 5936504..84b4eeb 100644 --- a/src/remote_email_filtering/types.py +++ b/src/remote_email_filtering/types.py @@ -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: diff --git a/utils/authorized_oauth2_msal.py b/utils/authorized_oauth2_msal.py index 947aeb1..32f44b6 100755 --- a/utils/authorized_oauth2_msal.py +++ b/utils/authorized_oauth2_msal.py @@ -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")