Skip to content

Commit

Permalink
First implementation of syncing subset of gmail flags
Browse files Browse the repository at this point in the history
  • Loading branch information
naspeh committed Apr 18, 2020
1 parent b0cd4ab commit 52109d0
Show file tree
Hide file tree
Showing 7 changed files with 321 additions and 80 deletions.
26 changes: 14 additions & 12 deletions mailur/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from docopt import docopt
from gevent import joinall, sleep, spawn

from . import conf, local, lock, log, remote
from . import conf, local, log, remote

root = pathlib.Path(__file__).resolve().parent.parent

Expand Down Expand Up @@ -66,7 +66,9 @@ def process(args):
elif args['parse']:
local.parse(args.get('<criteria>'), **opts)
elif args['sync']:
sync(int(args['--timeout']))
timeout = args.get('--timeout')
params = [int(timeout)] if timeout else []
sync(*params)
elif args['sync-flags']:
if args['--reverse']:
local.sync_flags_to_src()
Expand Down Expand Up @@ -106,25 +108,25 @@ def inner(*a, **kw):


def sync(timeout=1200):
def sync_remote(res=None):
try:
remote.fetch()
local.parse()
except lock.Error as e:
log.warn(e)

@run_forever
def idle_remote(params):
with remote.client(**params) as c:
c.idle({'EXISTS': sync_remote}, timeout=timeout)
handlers = {
'EXISTS': lambda res: remote.sync(),
'FETCH': lambda res: remote.sync(only_flags=True),
}
c.idle(handlers, timeout=timeout)

@run_forever
def sync_flags():
local.sync_flags_to_all()
local.sync_flags(timeout=timeout)
local.sync_flags(
post_handler=lambda res: remote.sync(only_flags=True),
timeout=timeout
)

try:
sync_remote()
remote.sync()
jobs = [spawn(sync_flags)]
for params in remote.get_folders():
jobs.append(spawn(idle_remote, params))
Expand Down
20 changes: 18 additions & 2 deletions mailur/imap.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,10 @@ def uidnext(self):
def uidvalidity(self):
return self._con.uidvalidity

@property
def highestmodseq(self):
return self._con.highestmodseq

def __enter__(self):
return self

Expand Down Expand Up @@ -394,6 +398,11 @@ def inner(tag):
return


@command(lock=False)
def enable(con, capability):
return check(con.enable(capability))


@command()
def logout(con, timeout=1):
with Timeout(timeout):
Expand All @@ -415,11 +424,12 @@ def select(con, box, readonly=True):
con.flags = con.untagged_responses['FLAGS'][0].decode()[1:-1].split()
con.uidnext = int(con.untagged_responses['UIDNEXT'][0].decode())
con.uidvalidity = con.untagged_responses['UIDVALIDITY'][0].decode()
con.highestmodseq = int(con.untagged_responses['HIGHESTMODSEQ'][0].decode())
return res


@command(lock=False)
def select_tag(con, tag, readonly=True, exc=True):
@ft.lru_cache(None)
def find_folder(con, tag):
if isinstance(tag, str):
tag = tag.encode()
folder = None
Expand All @@ -429,6 +439,12 @@ def select_tag(con, tag, readonly=True, exc=True):
continue
folder = f.rsplit(b' "/" ', 1)[1]
break
return folder


@command(lock=False)
def select_tag(con, tag, readonly=True, exc=True):
folder = find_folder(con, tag)
if folder is None:
if exc:
raise Error('No folder with tag: %s\n%s' % (tag, folders))
Expand Down
33 changes: 19 additions & 14 deletions mailur/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def _create_socket(self):
def connect(username, password):
con = Local(username)
imap.login(con, username, password)
imap.enable(con, 'CONDSTORE')

# For searching with non ascii symbols (Dovecot understands this)
con._encoding = 'utf-8'
Expand Down Expand Up @@ -182,6 +183,11 @@ def data_uidnext(value):
return value


@setting('modseq')
def data_modseq(value):
return value


@setting('links', lambda: [])
def data_links(links):
return links
Expand Down Expand Up @@ -740,20 +746,21 @@ def sync_flags_to_src(con_src=None, con_all=None):


@fn_time
@using(None, reuse=False)
def sync_flags(con=None, timeout=None):
@using(SRC, reuse=False)
def sync_flags(con=None, post_handler=None, timeout=None):
@using(SRC, name='con_src', reuse=False)
@using(ALL, name='con_all', readonly=False, reuse=False)
def handler(res, con_src=None, con_all=None):
modseq0 = modseq[0]
modseq_ = re.search(r'MODSEQ \((\d+)\)', res[0].decode()).group(1)
if int(modseq_) < int(modseq0):
cur_modseq = con.highestmodseq
new_modseq = re.search(r'MODSEQ \((\d+)\)', res[0].decode()).group(1)
new_modseq = int(new_modseq)
if new_modseq < cur_modseq:
return
modseq[0] = modseq_
res = con_src.fetch('1:*', '(UID FLAGS) (CHANGEDSINCE %s)' % modseq0)
res = con_src.fetch('1:*', '(UID FLAGS) (CHANGEDSINCE %s)' % cur_modseq)
cur_modseq = new_modseq
src_flags = {}
for line in res:
val = re.search(r'UID (\d+) FLAGS \(([^)]*)\)', line.decode())
val = re.search(r'UID (\d+) FLAGS \(([^)]*)\) MODSEQ \(\d+\)', line.decode())
if not val:
continue
uid, flags = val.groups()
Expand Down Expand Up @@ -781,15 +788,13 @@ def handler(res, con_src=None, con_all=None):
key = ('-FLAGS.SILENT', ' '.join(val))
actions.setdefault(key, [])
actions[key].append(uid)
log.debug('sync: MODSEQ=%s %s', modseq_, actions)
log.debug('sync: MODSEQ=%s %s', cur_modseq, actions)
for action, uids in actions.items():
con_all.store(uids, *action)
if post_handler:
post_handler(res)

res = con.status(SRC, '(UIDVALIDITY HIGHESTMODSEQ)')
pair = re.search(r'UIDVALIDITY (\d+) HIGHESTMODSEQ (\d+)', res[0].decode())
uidval, modseq = pair.groups()
log.info('%s UIDVALIDITY=%s HIGHESTMODSEQ=%s', con, uidval, modseq)
modseq = [modseq]
log.info('%s UIDVALIDITY=%s HIGHESTMODSEQ=%s', con, con.uidvalidity, con.highestmodseq)
con.select(SRC)
con.idle({'FETCH': handler}, timeout=timeout)

Expand Down
Loading

0 comments on commit 52109d0

Please sign in to comment.