diff --git a/python/src/deltachat/__init__.py b/python/src/deltachat/__init__.py index cda2864c..1e4bbaac 100644 --- a/python/src/deltachat/__init__.py +++ b/python/src/deltachat/__init__.py @@ -1,8 +1,9 @@ from deltachat import capi +from deltachat.capi import ffi +from deltachat.account import Account _DC_CALLBACK_MAP = {} -_DC_EVENTNAME_MAP = {} @capi.ffi.def_extern() @@ -13,14 +14,35 @@ def py_dc_callback(ctx, evt, data1, data2): looks up the correct event handler for the given context. """ callback = _DC_CALLBACK_MAP.get(ctx, lambda *a: 0) + # the following code relates to the deltachat/_build.py's helper + # function which provides us signature info of an event call + event_sig_types = capi.lib.dc_get_event_signature_types(evt) + if data1 and event_sig_types & 1: + data1 = ffi.string(ffi.cast('char*', data1)) + if data2 and event_sig_types & 2: + data2 = ffi.string(ffi.cast('char*', data2)) + evt_name = get_dc_event_name(evt) try: - ret = callback(ctx, evt, data1, data2) + ret = callback(ctx, evt_name, data1, data2) + if event_sig_types & 4: + return ffi.cast('uintptr_t', ret) + elif event_sig_types & 8: + return ffi.cast('int', ret) except: + raise ret = 0 return ret -def get_dc_event_name(integer): +def set_context_callback(dc_context, func): + _DC_CALLBACK_MAP[dc_context] = func + + +def clear_context_callback(dc_context): + _DC_CALLBACK_MAP.pop(dc_context, None) + + +def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}): if not _DC_EVENTNAME_MAP: for name, val in vars(capi.lib).items(): if name.startswith("DC_EVENT_"): diff --git a/python/src/deltachat/account.py b/python/src/deltachat/account.py new file mode 100644 index 00000000..6da3883f --- /dev/null +++ b/python/src/deltachat/account.py @@ -0,0 +1,106 @@ +from __future__ import print_function +import threading +import requests +from . import capi +import deltachat +from .capi import ffi + + +class Account: + def __init__(self, db_path, logcallback=None): + self.dc_context = ctx = capi.lib.dc_context_new( + capi.lib.py_dc_callback, + capi.ffi.NULL, capi.ffi.NULL) + capi.lib.dc_open(ctx, db_path, capi.ffi.NULL) + self._logcallback = logcallback + + def set_config(self, **kwargs): + for name, value in kwargs.items(): + capi.lib.dc_set_config(self.dc_context, name, value) + + def start(self): + deltachat.set_context_callback(self.dc_context, self.process_event) + capi.lib.dc_configure(self.dc_context) + self._threads = IOThreads(self.dc_context) + self._threads.start() + + def shutdown(self): + deltachat.clear_context_callback(self.dc_context) + self._threads.stop(wait=False) + # XXX actually we'd like to wait but the smtp/imap + # interrupt idle calls do not seem to release the + # blocking call to smtp|imap idle. This means we + # also can't now close the database because the + # threads might still need it + # capi.lib.dc_close(self.dc_context) + + def process_event(self, ctx, evt_name, data1, data2): + assert ctx == self.dc_context + if self._logcallback is not None: + self._logcallback((evt_name, data1, data2)) + callname = evt_name[3:].lower() + method = getattr(self, callname, None) + if method is not None: + return method(data1, data2) or 0 + # print ("dropping event: no handler for", evt_name) + return 0 + + def read_url(self, url): + try: + r = requests.get(url) + except requests.ConnectionError: + return '' + else: + return r.content + + def event_http_get(self, data1, data2): + url = data1.decode("utf-8") + content = self.read_url(url) + s = content.encode("utf-8") + # we need to return a fresh pointer that the core owns + return capi.lib.dupstring_helper(s) + + def event_is_offline(self, data1, data2): + return 0 # always online + + +class IOThreads: + def __init__(self, dc_context): + self.dc_context = dc_context + self._thread_quitflag = False + self._name2thread = {} + + def start(self, imap=True, smtp=True): + assert not self._name2thread + if imap: + self._start_one_thread("imap", self.imap_thread_run) + if smtp: + self._start_one_thread("smtp", self.smtp_thread_run) + + def _start_one_thread(self, name, func): + self._name2thread[name] = t = threading.Thread(target=func, name=name) + t.setDaemon(1) + t.start() + + def stop(self, wait=False): + self._thread_quitflag = True + # XXX interrupting does not quite work yet, the threads keep idling + print("interrupting smtp and idle") + capi.lib.dc_interrupt_imap_idle(self.dc_context) + capi.lib.dc_interrupt_smtp_idle(self.dc_context) + if wait: + for name, thread in self._name2thread.items(): + thread.join() + + def imap_thread_run(self): + print ("starting imap thread") + while not self._thread_quitflag: + capi.lib.dc_perform_imap_jobs(self.dc_context) + capi.lib.dc_perform_imap_fetch(self.dc_context) + capi.lib.dc_perform_imap_idle(self.dc_context) + + def smtp_thread_run(self): + print ("starting smtp thread") + while not self._thread_quitflag: + capi.lib.dc_perform_smtp_jobs(self.dc_context) + capi.lib.dc_perform_smtp_idle(self.dc_context) diff --git a/python/tests/conftest.py b/python/tests/conftest.py index cc8039c0..06a7ce33 100644 --- a/python/tests/conftest.py +++ b/python/tests/conftest.py @@ -1,20 +1,6 @@ import pytest import deltachat -from deltachat import capi -import threading - - -@pytest.fixture -def register_dc_callback(monkeypatch): - """Register a callback for a given context. - - This is a function-scoped fixture and the function will be - unregisterd automatically on fixture teardown. - """ - def register_dc_callback(ctx, func): - monkeypatch.setitem(deltachat._DC_CALLBACK_MAP, ctx, func) - return register_dc_callback def pytest_addoption(parser): @@ -32,39 +18,6 @@ def userpassword(pytestconfig): pytest.skip("specify a test account with --user and --password options") - -def imap_thread(context, quitflag): - print ("starting imap thread") - while not quitflag.is_set(): - capi.lib.dc_perform_imap_jobs(context) - capi.lib.dc_perform_imap_fetch(context) - capi.lib.dc_perform_imap_idle(context) - - -def smtp_thread(context, quitflag): - print ("starting smtp thread") - while not quitflag.is_set(): - capi.lib.dc_perform_smtp_jobs(context) - capi.lib.dc_perform_smtp_idle(context) - - @pytest.fixture -def dc_context(): - ctx = capi.lib.dc_context_new(capi.lib.py_dc_callback, - capi.ffi.NULL, capi.ffi.NULL) - yield ctx - capi.lib.dc_close(ctx) - - -@pytest.fixture -def dc_threads(dc_context): - quitflag = threading.Event() - t1 = threading.Thread(target=imap_thread, name="imap", args=[dc_context, quitflag]) - t1.setDaemon(1) - t1.start() - t2 = threading.Thread(target=smtp_thread, name="smtp", args=[dc_context, quitflag]) - t2.setDaemon(1) - t2.start() - yield - quitflag.set() - +def tmp_db_path(tmpdir): + return tmpdir.join("test.db").strpath diff --git a/python/tests/test_lowlevel.py b/python/tests/test_lowlevel.py index 0f1717b5..69e1169f 100644 --- a/python/tests/test_lowlevel.py +++ b/python/tests/test_lowlevel.py @@ -1,8 +1,7 @@ from __future__ import print_function import deltachat import re -import requests -from deltachat import capi, get_dc_event_name +from deltachat import capi from deltachat.capi import ffi import queue @@ -16,70 +15,21 @@ def test_event_defines(): assert capi.lib.DC_EVENT_INFO == 100 -def test_cb(register_dc_callback): - def cb(ctx, evt, data1, data2): - return 0 - ctx = capi.lib.dc_context_new(capi.lib.py_dc_callback, - capi.ffi.NULL, capi.ffi.NULL) - register_dc_callback(ctx, cb) - capi.lib.dc_close(ctx) - assert deltachat._DC_CALLBACK_MAP[ctx] is cb - - -def test_basic_events(dc_context, dc_threads, register_dc_callback, tmpdir, userpassword): - q = queue.Queue() - def cb(dc_context, evt, data1, data2): - # the following code relates to the deltachat/_build.py's helper - # function which provides us signature info of an event call - event_sig_types = capi.lib.dc_get_event_signature_types(evt) - if data1 and event_sig_types & 1: - data1 = ffi.string(ffi.cast('char*', data1)) - if data2 and event_sig_types & 2: - data2 = ffi.string(ffi.cast('char*', data2)) - evt_name = get_dc_event_name(evt) - print (evt_name, data1, data2) - if evt_name == "DC_EVENT_HTTP_GET": - content = read_url(data1) - s = content.encode("utf-8") - # we need to return a pointer that the core owns - dupped = capi.lib.dupstring_helper(s) - return ffi.cast('uintptr_t', dupped) - elif evt_name == "DC_EVENT_IS_OFFLINE": - return 0 - elif event_sig_types & (4|8): # returning string or int means it's a sync event - print ("dropping sync event: no handler for", evt_name) - return 0 - # async event - q.put((evt_name, data1, data2)) - return 0 - - register_dc_callback(dc_context, cb) - - dbfile = tmpdir.join("test.db") - capi.lib.dc_open(dc_context, dbfile.strpath, capi.ffi.NULL) - capi.lib.dc_set_config(dc_context, "addr", userpassword[0]) - capi.lib.dc_set_config(dc_context, "mail_pw", userpassword[1]) - capi.lib.dc_configure(dc_context) - - imap_ok = smtp_ok = False - while not imap_ok or not smtp_ok: - evt_name, data1, data2 = q.get(timeout=5.0) - if evt_name == "DC_EVENT_ERROR": - assert 0 - if evt_name == "DC_EVENT_INFO": - if re.match("imap-login.*ok.", data2.lower()): - imap_ok = True - if re.match("smtp-login.*ok.", data2.lower()): - smtp_ok = True - assert 0 - # assert capi.lib.dc_imap_is_connected(dc_context) - # assert capi.lib.dc_smtp_is_connected(dc_context) - - -def read_url(url): - try: - r = requests.get(url) - except requests.ConnectionError: - return '' - else: - return r.content +class TestLive: + def test_basic_configure_login_ok(self, request, tmp_db_path, userpassword): + q = queue.Queue() + dc = deltachat.Account(tmp_db_path, logcallback=q.put) + dc.set_config(addr=userpassword[0], mail_pw=userpassword[1]) + dc.start() + request.addfinalizer(dc.shutdown) + imap_ok = smtp_ok = False + while not imap_ok or not smtp_ok: + evt_name, data1, data2 = q.get(timeout=5.0) + print(evt_name, data1, data2) + if evt_name == "DC_EVENT_ERROR": + assert 0 + if evt_name == "DC_EVENT_INFO": + if re.match("imap-login.*ok.", data2.lower()): + imap_ok = True + if re.match("smtp-login.*ok.", data2.lower()): + smtp_ok = True