mirror of
https://github.com/chatmail/core.git
synced 2026-07-23 03:13:06 +03:00
Compare commits
19 Commits
1.25.0
...
test-ecc-b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed092fe1ce | ||
|
|
56f589db4a | ||
|
|
49b39b42bf | ||
|
|
7e4951e691 | ||
|
|
f50874acf1 | ||
|
|
4f8c5965ac | ||
|
|
900a17fc00 | ||
|
|
78f36aaa0d | ||
|
|
e064e02794 | ||
|
|
e22e5045f1 | ||
|
|
087f35482b | ||
|
|
23ff5fea28 | ||
|
|
34347ccaf5 | ||
|
|
e704eb6cef | ||
|
|
bf63423fec | ||
|
|
f6d71ed8ef | ||
|
|
3c342339a1 | ||
|
|
33463856c5 | ||
|
|
a18f4c9b1b |
@@ -1,3 +1,10 @@
|
|||||||
|
0.800.0
|
||||||
|
-------
|
||||||
|
|
||||||
|
- use latest core 1.25.0
|
||||||
|
|
||||||
|
- refine tests and some internal changes to core bindings
|
||||||
|
|
||||||
0.700.0
|
0.700.0
|
||||||
---------
|
---------
|
||||||
|
|
||||||
|
|||||||
4
python/doc/_static/custom.css
vendored
4
python/doc/_static/custom.css
vendored
@@ -15,3 +15,7 @@ div.globaltoc {
|
|||||||
img.logo {
|
img.logo {
|
||||||
height: 120px;
|
height: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.footer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ master_doc = 'index'
|
|||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = u'deltachat'
|
project = u'deltachat'
|
||||||
copyright = u'2018, holger krekel and contributors'
|
copyright = u'2020, holger krekel and contributors'
|
||||||
|
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
deltachat python bindings
|
deltachat python bindings
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
The ``deltachat`` Python package provides two bindings for the core Rust-library
|
The ``deltachat`` Python package provides two layers of bindings for the
|
||||||
of the https://delta.chat messaging ecosystem:
|
core Rust-library of the https://delta.chat messaging ecosystem:
|
||||||
|
|
||||||
- :doc:`api` is a high level interface to deltachat-core which aims
|
- :doc:`api` is a high level interface to deltachat-core which aims
|
||||||
to be memory safe and thoroughly tested through continous tox/pytest runs.
|
to be memory safe and thoroughly tested through continous tox/pytest runs.
|
||||||
|
|
||||||
- :doc:`capi` is a lowlevel CFFI-binding to the previous
|
- :doc:`lapi` is a lowlevel CFFI-binding to the `Rust Core
|
||||||
`deltachat-core C-API <https://c.delta.chat>`_ (so far the Rust library
|
<https://github.com/deltachat/deltachat-core-rust>`_.
|
||||||
replicates exactly the same C-level API).
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -28,7 +27,6 @@ getting started
|
|||||||
links
|
links
|
||||||
changelog
|
changelog
|
||||||
api
|
api
|
||||||
capi
|
|
||||||
lapi
|
lapi
|
||||||
|
|
||||||
..
|
..
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ def main():
|
|||||||
description='Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat',
|
description='Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat',
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
author='holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors',
|
author='holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors',
|
||||||
install_requires=['cffi>=1.0.0', 'six'],
|
install_requires=['cffi>=1.0.0', 'pluggy'],
|
||||||
packages=setuptools.find_packages('src'),
|
packages=setuptools.find_packages('src'),
|
||||||
package_dir={'': 'src'},
|
package_dir={'': 'src'},
|
||||||
cffi_modules=['src/deltachat/_build.py:ffibuilder'],
|
cffi_modules=['src/deltachat/_build.py:ffibuilder'],
|
||||||
|
|||||||
@@ -4,13 +4,9 @@ from __future__ import print_function
|
|||||||
import atexit
|
import atexit
|
||||||
import threading
|
import threading
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import time
|
import time
|
||||||
from array import array
|
from array import array
|
||||||
try:
|
from queue import Queue
|
||||||
from queue import Queue, Empty
|
|
||||||
except ImportError:
|
|
||||||
from Queue import Queue, Empty
|
|
||||||
|
|
||||||
import deltachat
|
import deltachat
|
||||||
from . import const
|
from . import const
|
||||||
@@ -19,6 +15,8 @@ from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array, DCLot
|
|||||||
from .chat import Chat
|
from .chat import Chat
|
||||||
from .message import Message
|
from .message import Message
|
||||||
from .contact import Contact
|
from .contact import Contact
|
||||||
|
from .eventlogger import EventLogger
|
||||||
|
from .hookspec import get_plugin_manager, hookimpl
|
||||||
|
|
||||||
|
|
||||||
class Account(object):
|
class Account(object):
|
||||||
@@ -26,14 +24,13 @@ class Account(object):
|
|||||||
by the underlying deltachat core library. All public Account methods are
|
by the underlying deltachat core library. All public Account methods are
|
||||||
meant to be memory-safe and return memory-safe objects.
|
meant to be memory-safe and return memory-safe objects.
|
||||||
"""
|
"""
|
||||||
def __init__(self, db_path, logid=None, eventlogging=True, os_name=None, debug=True):
|
def __init__(self, db_path, logid=None, os_name=None, debug=True):
|
||||||
""" initialize account object.
|
""" initialize account object.
|
||||||
|
|
||||||
:param db_path: a path to the account database. The database
|
:param db_path: a path to the account database. The database
|
||||||
will be created if it doesn't exist.
|
will be created if it doesn't exist.
|
||||||
:param logid: an optional logging prefix that should be used with
|
:param logid: an optional logging prefix that should be used with
|
||||||
the default internal logging.
|
the default internal logging.
|
||||||
:param eventlogging: if False no eventlogging and no context callback will be configured
|
|
||||||
:param os_name: this will be put to the X-Mailer header in outgoing messages
|
:param os_name: this will be put to the X-Mailer header in outgoing messages
|
||||||
:param debug: turn on debug logging for events.
|
:param debug: turn on debug logging for events.
|
||||||
"""
|
"""
|
||||||
@@ -41,19 +38,26 @@ class Account(object):
|
|||||||
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, as_dc_charpointer(os_name)),
|
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, as_dc_charpointer(os_name)),
|
||||||
_destroy_dc_context,
|
_destroy_dc_context,
|
||||||
)
|
)
|
||||||
if eventlogging:
|
self._evlogger = EventLogger(self, logid, debug)
|
||||||
self._evlogger = EventLogger(self._dc_context, logid, debug)
|
self._threads = IOThreads(self._dc_context, self._evlogger._log_event)
|
||||||
deltachat.set_context_callback(self._dc_context, self._process_event)
|
|
||||||
self._threads = IOThreads(self._dc_context, self._evlogger._log_event)
|
|
||||||
else:
|
|
||||||
self._threads = IOThreads(self._dc_context)
|
|
||||||
|
|
||||||
|
# register event call back and initialize plugin system
|
||||||
|
def _ll_event(ctx, evt_name, data1, data2):
|
||||||
|
assert ctx == self._dc_context
|
||||||
|
self.pluggy.hook.process_low_level_event(
|
||||||
|
account=self, event_name=evt_name, data1=data1, data2=data2
|
||||||
|
)
|
||||||
|
|
||||||
|
self.pluggy = get_plugin_manager()
|
||||||
|
self.pluggy.register(self._evlogger)
|
||||||
|
deltachat.set_context_callback(self._dc_context, _ll_event)
|
||||||
|
|
||||||
|
# open database
|
||||||
if hasattr(db_path, "encode"):
|
if hasattr(db_path, "encode"):
|
||||||
db_path = db_path.encode("utf8")
|
db_path = db_path.encode("utf8")
|
||||||
if not lib.dc_open(self._dc_context, db_path, ffi.NULL):
|
if not lib.dc_open(self._dc_context, db_path, ffi.NULL):
|
||||||
raise ValueError("Could not dc_open: {}".format(db_path))
|
raise ValueError("Could not dc_open: {}".format(db_path))
|
||||||
self._configkeys = self.get_config("sys.config_keys").split()
|
self._configkeys = self.get_config("sys.config_keys").split()
|
||||||
self._imex_events = Queue()
|
|
||||||
atexit.register(self.shutdown)
|
atexit.register(self.shutdown)
|
||||||
|
|
||||||
# def __del__(self):
|
# def __del__(self):
|
||||||
@@ -177,11 +181,6 @@ class Account(object):
|
|||||||
raise ValueError("no flags set")
|
raise ValueError("no flags set")
|
||||||
lib.dc_empty_server(self._dc_context, flags)
|
lib.dc_empty_server(self._dc_context, flags)
|
||||||
|
|
||||||
def get_infostring(self):
|
|
||||||
""" return info of the configured account. """
|
|
||||||
self.check_is_configured()
|
|
||||||
return from_dc_charpointer(lib.dc_get_info(self._dc_context))
|
|
||||||
|
|
||||||
def get_latest_backupfile(self, backupdir):
|
def get_latest_backupfile(self, backupdir):
|
||||||
""" return the latest backup file in a given directory.
|
""" return the latest backup file in a given directory.
|
||||||
"""
|
"""
|
||||||
@@ -382,27 +381,12 @@ class Account(object):
|
|||||||
raise RuntimeError("found more than one new file")
|
raise RuntimeError("found more than one new file")
|
||||||
return export_files[0]
|
return export_files[0]
|
||||||
|
|
||||||
def _imex_events_clear(self):
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
self._imex_events.get_nowait()
|
|
||||||
except Empty:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _export(self, path, imex_cmd):
|
def _export(self, path, imex_cmd):
|
||||||
self._imex_events_clear()
|
with ImexTracker(self) as imex_tracker:
|
||||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
||||||
if not self._threads.is_started():
|
if not self._threads.is_started():
|
||||||
lib.dc_perform_imap_jobs(self._dc_context)
|
lib.dc_perform_imap_jobs(self._dc_context)
|
||||||
files_written = []
|
return imex_tracker.wait_finish()
|
||||||
while True:
|
|
||||||
ev = self._imex_events.get()
|
|
||||||
if isinstance(ev, str):
|
|
||||||
files_written.append(ev)
|
|
||||||
elif isinstance(ev, bool):
|
|
||||||
if not ev:
|
|
||||||
raise ValueError("export failed, exp-files: {}".format(files_written))
|
|
||||||
return files_written
|
|
||||||
|
|
||||||
def import_self_keys(self, path):
|
def import_self_keys(self, path):
|
||||||
""" Import private keys found in the `path` directory.
|
""" Import private keys found in the `path` directory.
|
||||||
@@ -420,12 +404,11 @@ class Account(object):
|
|||||||
self._import(path, imex_cmd=12)
|
self._import(path, imex_cmd=12)
|
||||||
|
|
||||||
def _import(self, path, imex_cmd):
|
def _import(self, path, imex_cmd):
|
||||||
self._imex_events_clear()
|
with ImexTracker(self) as imex_tracker:
|
||||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
||||||
if not self._threads.is_started():
|
if not self._threads.is_started():
|
||||||
lib.dc_perform_imap_jobs(self._dc_context)
|
lib.dc_perform_imap_jobs(self._dc_context)
|
||||||
if not self._imex_events.get():
|
imex_tracker.wait_finish()
|
||||||
raise ValueError("import from path '{}' failed".format(path))
|
|
||||||
|
|
||||||
def initiate_key_transfer(self):
|
def initiate_key_transfer(self):
|
||||||
"""return setup code after a Autocrypt setup message
|
"""return setup code after a Autocrypt setup message
|
||||||
@@ -528,24 +511,7 @@ class Account(object):
|
|||||||
deltachat.clear_context_callback(self._dc_context)
|
deltachat.clear_context_callback(self._dc_context)
|
||||||
del self._dc_context
|
del self._dc_context
|
||||||
atexit.unregister(self.shutdown)
|
atexit.unregister(self.shutdown)
|
||||||
|
self.pluggy.unregister(self._evlogger)
|
||||||
def _process_event(self, ctx, evt_name, data1, data2):
|
|
||||||
assert ctx == self._dc_context
|
|
||||||
if hasattr(self, "_evlogger"):
|
|
||||||
self._evlogger(evt_name, data1, data2)
|
|
||||||
method = getattr(self, "on_" + evt_name.lower(), None)
|
|
||||||
if method is not None:
|
|
||||||
method(data1, data2)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def on_dc_event_imex_progress(self, data1, data2):
|
|
||||||
if data1 == 1000:
|
|
||||||
self._imex_events.put(True)
|
|
||||||
elif data1 == 0:
|
|
||||||
self._imex_events.put(False)
|
|
||||||
|
|
||||||
def on_dc_event_imex_file_written(self, data1, data2):
|
|
||||||
self._imex_events.put(data1)
|
|
||||||
|
|
||||||
def set_location(self, latitude=0.0, longitude=0.0, accuracy=0.0):
|
def set_location(self, latitude=0.0, longitude=0.0, accuracy=0.0):
|
||||||
"""set a new location. It effects all chats where we currently
|
"""set a new location. It effects all chats where we currently
|
||||||
@@ -562,6 +528,41 @@ class Account(object):
|
|||||||
raise ValueError("no chat is streaming locations")
|
raise ValueError("no chat is streaming locations")
|
||||||
|
|
||||||
|
|
||||||
|
class ImexTracker:
|
||||||
|
def __init__(self, account):
|
||||||
|
self._imex_events = Queue()
|
||||||
|
self.account = account
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.account.pluggy.register(self)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *args):
|
||||||
|
self.account.pluggy.unregister(self)
|
||||||
|
|
||||||
|
@hookimpl
|
||||||
|
def process_low_level_event(self, account, event_name, data1, data2):
|
||||||
|
# there could be multiple accounts instantiated
|
||||||
|
if self.account is not account:
|
||||||
|
return
|
||||||
|
if event_name == "DC_EVENT_IMEX_PROGRESS":
|
||||||
|
self._imex_events.put(data1)
|
||||||
|
elif event_name == "DC_EVENT_IMEX_FILE_WRITTEN":
|
||||||
|
self._imex_events.put(data1)
|
||||||
|
|
||||||
|
def wait_finish(self, progress_timeout=60):
|
||||||
|
""" Return list of written files, raise ValueError if ExportFailed. """
|
||||||
|
files_written = []
|
||||||
|
while True:
|
||||||
|
ev = self._imex_events.get(timeout=progress_timeout)
|
||||||
|
if isinstance(ev, str):
|
||||||
|
files_written.append(ev)
|
||||||
|
elif ev == 0:
|
||||||
|
raise ValueError("export failed, exp-files: {}".format(files_written))
|
||||||
|
elif ev == 1000:
|
||||||
|
return files_written
|
||||||
|
|
||||||
|
|
||||||
class IOThreads:
|
class IOThreads:
|
||||||
def __init__(self, dc_context, log_event=lambda *args: None):
|
def __init__(self, dc_context, log_event=lambda *args: None):
|
||||||
self._dc_context = dc_context
|
self._dc_context = dc_context
|
||||||
@@ -642,80 +643,6 @@ class IOThreads:
|
|||||||
self._log_event("py-bindings-info", 0, "SMTP THREAD FINISHED")
|
self._log_event("py-bindings-info", 0, "SMTP THREAD FINISHED")
|
||||||
|
|
||||||
|
|
||||||
class EventLogger:
|
|
||||||
_loglock = threading.RLock()
|
|
||||||
|
|
||||||
def __init__(self, dc_context, logid=None, debug=True):
|
|
||||||
self._dc_context = dc_context
|
|
||||||
self._event_queue = Queue()
|
|
||||||
self._debug = debug
|
|
||||||
if logid is None:
|
|
||||||
logid = str(self._dc_context).strip(">").split()[-1]
|
|
||||||
self.logid = logid
|
|
||||||
self._timeout = None
|
|
||||||
self.init_time = time.time()
|
|
||||||
|
|
||||||
def __call__(self, evt_name, data1, data2):
|
|
||||||
self._log_event(evt_name, data1, data2)
|
|
||||||
self._event_queue.put((evt_name, data1, data2))
|
|
||||||
|
|
||||||
def set_timeout(self, timeout):
|
|
||||||
self._timeout = timeout
|
|
||||||
|
|
||||||
def consume_events(self, check_error=True):
|
|
||||||
while not self._event_queue.empty():
|
|
||||||
self.get()
|
|
||||||
|
|
||||||
def get(self, timeout=None, check_error=True):
|
|
||||||
timeout = timeout or self._timeout
|
|
||||||
ev = self._event_queue.get(timeout=timeout)
|
|
||||||
if check_error and ev[0] == "DC_EVENT_ERROR":
|
|
||||||
raise ValueError("{}({!r},{!r})".format(*ev))
|
|
||||||
return ev
|
|
||||||
|
|
||||||
def ensure_event_not_queued(self, event_name_regex):
|
|
||||||
__tracebackhide__ = True
|
|
||||||
rex = re.compile("(?:{}).*".format(event_name_regex))
|
|
||||||
while 1:
|
|
||||||
try:
|
|
||||||
ev = self._event_queue.get(False)
|
|
||||||
except Empty:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
assert not rex.match(ev[0]), "event found {}".format(ev)
|
|
||||||
|
|
||||||
def get_matching(self, event_name_regex, check_error=True, timeout=None):
|
|
||||||
self._log("-- waiting for event with regex: {} --".format(event_name_regex))
|
|
||||||
rex = re.compile("(?:{}).*".format(event_name_regex))
|
|
||||||
while 1:
|
|
||||||
ev = self.get(timeout=timeout, check_error=check_error)
|
|
||||||
if rex.match(ev[0]):
|
|
||||||
return ev
|
|
||||||
|
|
||||||
def get_info_matching(self, regex):
|
|
||||||
rex = re.compile("(?:{}).*".format(regex))
|
|
||||||
while 1:
|
|
||||||
ev = self.get_matching("DC_EVENT_INFO")
|
|
||||||
if rex.match(ev[2]):
|
|
||||||
return ev
|
|
||||||
|
|
||||||
def _log_event(self, evt_name, data1, data2):
|
|
||||||
# don't show events that are anyway empty impls now
|
|
||||||
if evt_name == "DC_EVENT_GET_STRING":
|
|
||||||
return
|
|
||||||
if self._debug:
|
|
||||||
evpart = "{}({!r},{!r})".format(evt_name, data1, data2)
|
|
||||||
self._log(evpart)
|
|
||||||
|
|
||||||
def _log(self, msg):
|
|
||||||
t = threading.currentThread()
|
|
||||||
tname = getattr(t, "name", t)
|
|
||||||
if tname == "MainThread":
|
|
||||||
tname = "MAIN"
|
|
||||||
with self._loglock:
|
|
||||||
print("{:2.2f} [{}-{}] {}".format(time.time() - self.init_time, tname, self.logid, msg))
|
|
||||||
|
|
||||||
|
|
||||||
def _destroy_dc_context(dc_context, dc_context_unref=lib.dc_context_unref):
|
def _destroy_dc_context(dc_context, dc_context_unref=lib.dc_context_unref):
|
||||||
# destructor for dc_context
|
# destructor for dc_context
|
||||||
dc_context_unref(dc_context)
|
dc_context_unref(dc_context)
|
||||||
|
|||||||
81
python/src/deltachat/eventlogger.py
Normal file
81
python/src/deltachat/eventlogger.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import threading
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
from queue import Queue, Empty
|
||||||
|
from .hookspec import hookimpl
|
||||||
|
|
||||||
|
|
||||||
|
class EventLogger:
|
||||||
|
_loglock = threading.RLock()
|
||||||
|
|
||||||
|
def __init__(self, account, logid=None, debug=True):
|
||||||
|
self.account = account
|
||||||
|
self._event_queue = Queue()
|
||||||
|
self._debug = debug
|
||||||
|
if logid is None:
|
||||||
|
logid = str(self.account._dc_context).strip(">").split()[-1]
|
||||||
|
self.logid = logid
|
||||||
|
self._timeout = None
|
||||||
|
self.init_time = time.time()
|
||||||
|
|
||||||
|
@hookimpl
|
||||||
|
def process_low_level_event(self, account, event_name, data1, data2):
|
||||||
|
if self.account == account:
|
||||||
|
self._log_event(event_name, data1, data2)
|
||||||
|
self._event_queue.put((event_name, data1, data2))
|
||||||
|
|
||||||
|
def set_timeout(self, timeout):
|
||||||
|
self._timeout = timeout
|
||||||
|
|
||||||
|
def consume_events(self, check_error=True):
|
||||||
|
while not self._event_queue.empty():
|
||||||
|
self.get(check_error=check_error)
|
||||||
|
|
||||||
|
def get(self, timeout=None, check_error=True):
|
||||||
|
timeout = timeout or self._timeout
|
||||||
|
ev = self._event_queue.get(timeout=timeout)
|
||||||
|
if check_error and ev[0] == "DC_EVENT_ERROR":
|
||||||
|
raise ValueError("{}({!r},{!r})".format(*ev))
|
||||||
|
return ev
|
||||||
|
|
||||||
|
def ensure_event_not_queued(self, event_name_regex):
|
||||||
|
__tracebackhide__ = True
|
||||||
|
rex = re.compile("(?:{}).*".format(event_name_regex))
|
||||||
|
while 1:
|
||||||
|
try:
|
||||||
|
ev = self._event_queue.get(False)
|
||||||
|
except Empty:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
assert not rex.match(ev[0]), "event found {}".format(ev)
|
||||||
|
|
||||||
|
def get_matching(self, event_name_regex, check_error=True, timeout=None):
|
||||||
|
self._log("-- waiting for event with regex: {} --".format(event_name_regex))
|
||||||
|
rex = re.compile("(?:{}).*".format(event_name_regex))
|
||||||
|
while 1:
|
||||||
|
ev = self.get(timeout=timeout, check_error=check_error)
|
||||||
|
if rex.match(ev[0]):
|
||||||
|
return ev
|
||||||
|
|
||||||
|
def get_info_matching(self, regex):
|
||||||
|
rex = re.compile("(?:{}).*".format(regex))
|
||||||
|
while 1:
|
||||||
|
ev = self.get_matching("DC_EVENT_INFO")
|
||||||
|
if rex.match(ev[2]):
|
||||||
|
return ev
|
||||||
|
|
||||||
|
def _log_event(self, evt_name, data1, data2):
|
||||||
|
# don't show events that are anyway empty impls now
|
||||||
|
if evt_name == "DC_EVENT_GET_STRING":
|
||||||
|
return
|
||||||
|
if self._debug:
|
||||||
|
evpart = "{}({!r},{!r})".format(evt_name, data1, data2)
|
||||||
|
self._log(evpart)
|
||||||
|
|
||||||
|
def _log(self, msg):
|
||||||
|
t = threading.currentThread()
|
||||||
|
tname = getattr(t, "name", t)
|
||||||
|
if tname == "MainThread":
|
||||||
|
tname = "MAIN"
|
||||||
|
with self._loglock:
|
||||||
|
print("{:2.2f} [{}-{}] {}".format(time.time() - self.init_time, tname, self.logid, msg))
|
||||||
25
python/src/deltachat/hookspec.py
Normal file
25
python/src/deltachat/hookspec.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
""" Hooks for python bindings """
|
||||||
|
|
||||||
|
import pluggy
|
||||||
|
|
||||||
|
name = "deltachat"
|
||||||
|
|
||||||
|
hookspec = pluggy.HookspecMarker(name)
|
||||||
|
hookimpl = pluggy.HookimplMarker(name)
|
||||||
|
_plugin_manager = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_plugin_manager():
|
||||||
|
global _plugin_manager
|
||||||
|
if _plugin_manager is None:
|
||||||
|
_plugin_manager = pluggy.PluginManager(name)
|
||||||
|
_plugin_manager.add_hookspecs(DeltaChatHookSpecs)
|
||||||
|
return _plugin_manager
|
||||||
|
|
||||||
|
|
||||||
|
class DeltaChatHookSpecs:
|
||||||
|
""" Plugin Hook specifications for Python bindings to Delta Chat CFFI. """
|
||||||
|
|
||||||
|
@hookspec
|
||||||
|
def process_low_level_event(self, account, event_name, data1, data2):
|
||||||
|
""" process a CFFI low level events for a given account. """
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import py
|
import py
|
||||||
import pytest
|
import pytest
|
||||||
import requests
|
import requests
|
||||||
@@ -7,6 +8,7 @@ import time
|
|||||||
from deltachat import Account
|
from deltachat import Account
|
||||||
from deltachat import const
|
from deltachat import const
|
||||||
from deltachat.capi import lib
|
from deltachat.capi import lib
|
||||||
|
from _pytest.monkeypatch import MonkeyPatch
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
@@ -43,11 +45,14 @@ def pytest_report_header(config, startdir):
|
|||||||
summary = []
|
summary = []
|
||||||
|
|
||||||
t = tempfile.mktemp()
|
t = tempfile.mktemp()
|
||||||
|
m = MonkeyPatch()
|
||||||
try:
|
try:
|
||||||
ac = Account(t, eventlogging=False)
|
m.setattr(sys.stdout, "write", lambda x: len(x))
|
||||||
|
ac = Account(t)
|
||||||
info = ac.get_info()
|
info = ac.get_info()
|
||||||
ac.shutdown()
|
ac.shutdown()
|
||||||
finally:
|
finally:
|
||||||
|
m.undo()
|
||||||
os.remove(t)
|
os.remove(t)
|
||||||
summary.extend(['Deltachat core={} sqlite={}'.format(
|
summary.extend(['Deltachat core={} sqlite={}'.format(
|
||||||
info['deltachat_core_version'],
|
info['deltachat_core_version'],
|
||||||
|
|||||||
@@ -65,11 +65,6 @@ class TestOfflineAccountBasic:
|
|||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
ac1.get_self_contact()
|
ac1.get_self_contact()
|
||||||
|
|
||||||
def test_get_info(self, acfactory):
|
|
||||||
ac1 = acfactory.get_configured_offline_account()
|
|
||||||
out = ac1.get_infostring()
|
|
||||||
assert "number_of_chats=0" in out
|
|
||||||
|
|
||||||
def test_selfcontact_configured(self, acfactory):
|
def test_selfcontact_configured(self, acfactory):
|
||||||
ac1 = acfactory.get_configured_offline_account()
|
ac1 = acfactory.get_configured_offline_account()
|
||||||
me = ac1.get_self_contact()
|
me = ac1.get_self_contact()
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from __future__ import print_function
|
|||||||
from deltachat import capi, cutil, const, set_context_callback, clear_context_callback
|
from deltachat import capi, cutil, const, set_context_callback, clear_context_callback
|
||||||
from deltachat.capi import ffi
|
from deltachat.capi import ffi
|
||||||
from deltachat.capi import lib
|
from deltachat.capi import lib
|
||||||
from deltachat.account import EventLogger
|
|
||||||
|
|
||||||
|
|
||||||
def test_empty_context():
|
def test_empty_context():
|
||||||
@@ -18,21 +17,13 @@ def test_callback_None2int():
|
|||||||
|
|
||||||
|
|
||||||
def test_dc_close_events(tmpdir):
|
def test_dc_close_events(tmpdir):
|
||||||
ctx = ffi.gc(
|
from deltachat.account import Account
|
||||||
capi.lib.dc_context_new(capi.lib.py_dc_callback, ffi.NULL, ffi.NULL),
|
|
||||||
lib.dc_context_unref,
|
|
||||||
)
|
|
||||||
evlog = EventLogger(ctx)
|
|
||||||
evlog.set_timeout(5)
|
|
||||||
set_context_callback(
|
|
||||||
ctx,
|
|
||||||
lambda ctx, evt_name, data1, data2: evlog(evt_name, data1, data2)
|
|
||||||
)
|
|
||||||
p = tmpdir.join("hello.db")
|
p = tmpdir.join("hello.db")
|
||||||
lib.dc_open(ctx, p.strpath.encode("ascii"), ffi.NULL)
|
ac1 = Account(p.strpath)
|
||||||
capi.lib.dc_close(ctx)
|
ac1.shutdown()
|
||||||
|
|
||||||
def find(info_string):
|
def find(info_string):
|
||||||
|
evlog = ac1._evlogger
|
||||||
while 1:
|
while 1:
|
||||||
ev = evlog.get_matching("DC_EVENT_INFO", check_error=False)
|
ev = evlog.get_matching("DC_EVENT_INFO", check_error=False)
|
||||||
data2 = ev[2]
|
data2 = ev[2]
|
||||||
|
|||||||
@@ -1464,7 +1464,7 @@ fn is_known_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(ids) = mailparse::addrparse(mid_list) {
|
if let Ok(ids) = mailparse::msgidparse(mid_list) {
|
||||||
for id in ids.iter() {
|
for id in ids.iter() {
|
||||||
if is_known_rfc724_mid(context, id) {
|
if is_known_rfc724_mid(context, id) {
|
||||||
return true;
|
return true;
|
||||||
@@ -1476,8 +1476,7 @@ fn is_known_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a message is a reply to a known message (messenger or non-messenger).
|
/// Check if a message is a reply to a known message (messenger or non-messenger).
|
||||||
fn is_known_rfc724_mid(context: &Context, rfc724_mid: &mailparse::MailAddr) -> bool {
|
fn is_known_rfc724_mid(context: &Context, rfc724_mid: &str) -> bool {
|
||||||
let addr = extract_single_from_addr(rfc724_mid);
|
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.exists(
|
.exists(
|
||||||
@@ -1485,7 +1484,7 @@ fn is_known_rfc724_mid(context: &Context, rfc724_mid: &mailparse::MailAddr) -> b
|
|||||||
LEFT JOIN chats c ON m.chat_id=c.id \
|
LEFT JOIN chats c ON m.chat_id=c.id \
|
||||||
WHERE m.rfc724_mid=? \
|
WHERE m.rfc724_mid=? \
|
||||||
AND m.chat_id>9 AND c.blocked=0;",
|
AND m.chat_id>9 AND c.blocked=0;",
|
||||||
params![addr],
|
params![rfc724_mid],
|
||||||
)
|
)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
@@ -1512,7 +1511,7 @@ fn is_reply_to_messenger_message(context: &Context, mime_parser: &MimeMessage) -
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_msgrmsg_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool {
|
pub(crate) fn is_msgrmsg_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool {
|
||||||
if let Ok(ids) = mailparse::addrparse(mid_list) {
|
if let Ok(ids) = mailparse::msgidparse(mid_list) {
|
||||||
for id in ids.iter() {
|
for id in ids.iter() {
|
||||||
if is_msgrmsg_rfc724_mid(context, id) {
|
if is_msgrmsg_rfc724_mid(context, id) {
|
||||||
return true;
|
return true;
|
||||||
@@ -1522,21 +1521,13 @@ pub(crate) fn is_msgrmsg_rfc724_mid_in_list(context: &Context, mid_list: &str) -
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_single_from_addr(addr: &mailparse::MailAddr) -> &String {
|
|
||||||
match addr {
|
|
||||||
mailparse::MailAddr::Group(infos) => &infos.addrs[0].addr,
|
|
||||||
mailparse::MailAddr::Single(info) => &info.addr,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if a message is a reply to any messenger message.
|
/// Check if a message is a reply to any messenger message.
|
||||||
fn is_msgrmsg_rfc724_mid(context: &Context, rfc724_mid: &mailparse::MailAddr) -> bool {
|
fn is_msgrmsg_rfc724_mid(context: &Context, rfc724_mid: &str) -> bool {
|
||||||
let addr = extract_single_from_addr(rfc724_mid);
|
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.exists(
|
.exists(
|
||||||
"SELECT id FROM msgs WHERE rfc724_mid=? AND msgrmsg!=0 AND chat_id>9;",
|
"SELECT id FROM msgs WHERE rfc724_mid=? AND msgrmsg!=0 AND chat_id>9;",
|
||||||
params![addr],
|
params![rfc724_mid],
|
||||||
)
|
)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1306,17 +1306,6 @@ fn precheck_imf(context: &Context, rfc724_mid: &str, server_folder: &str, server
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_message_id(value: &str) -> crate::error::Result<String> {
|
|
||||||
let addrs = mailparse::addrparse(value)
|
|
||||||
.map_err(|err| format_err!("failed to parse message id {:?}", err))?;
|
|
||||||
|
|
||||||
if let Some(info) = addrs.extract_single_info() {
|
|
||||||
return Ok(info.addr);
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!("could not parse message_id: {}", value);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader>> {
|
fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader>> {
|
||||||
let header_bytes = match prefetch_msg.header() {
|
let header_bytes = match prefetch_msg.header() {
|
||||||
Some(header_bytes) => header_bytes,
|
Some(header_bytes) => header_bytes,
|
||||||
@@ -1328,7 +1317,7 @@ fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader>>
|
|||||||
|
|
||||||
fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Result<String> {
|
fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Result<String> {
|
||||||
if let Some(message_id) = headers.get_header_value(HeaderDef::MessageId)? {
|
if let Some(message_id) = headers.get_header_value(HeaderDef::MessageId)? {
|
||||||
Ok(parse_message_id(&message_id)?)
|
Ok(crate::mimeparser::parse_message_id(&message_id)?)
|
||||||
} else {
|
} else {
|
||||||
Err(Error::Other("prefetch: No message ID found".to_string()))
|
Err(Error::Other("prefetch: No message ID found".to_string()))
|
||||||
}
|
}
|
||||||
@@ -1384,20 +1373,3 @@ fn prefetch_should_download(
|
|||||||
let show = show && !blocked_contact;
|
let show = show && !blocked_contact;
|
||||||
Ok(show)
|
Ok(show)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_message_id() {
|
|
||||||
assert_eq!(
|
|
||||||
parse_message_id("Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org").unwrap(),
|
|
||||||
"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
parse_message_id("<Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org>").unwrap(),
|
|
||||||
"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -741,7 +741,7 @@ impl MimeMessage {
|
|||||||
|
|
||||||
pub fn get_rfc724_mid(&self) -> Option<String> {
|
pub fn get_rfc724_mid(&self) -> Option<String> {
|
||||||
self.get(HeaderDef::MessageId)
|
self.get(HeaderDef::MessageId)
|
||||||
.and_then(|msgid| parse_message_id(msgid))
|
.and_then(|msgid| parse_message_id(msgid).ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn merge_headers(headers: &mut HashMap<String, String>, fields: &[mailparse::MailHeader<'_>]) {
|
fn merge_headers(headers: &mut HashMap<String, String>, fields: &[mailparse::MailHeader<'_>]) {
|
||||||
@@ -779,14 +779,16 @@ impl MimeMessage {
|
|||||||
.get_header_value(HeaderDef::OriginalMessageId)
|
.get_header_value(HeaderDef::OriginalMessageId)
|
||||||
.ok()
|
.ok()
|
||||||
.flatten()
|
.flatten()
|
||||||
.and_then(|v| parse_message_id(&v))
|
.and_then(|v| parse_message_id(&v).ok())
|
||||||
{
|
{
|
||||||
let additional_message_ids = report_fields
|
let additional_message_ids = report_fields
|
||||||
.get_header_value(HeaderDef::AdditionalMessageIds)
|
.get_header_value(HeaderDef::AdditionalMessageIds)
|
||||||
.ok()
|
.ok()
|
||||||
.flatten()
|
.flatten()
|
||||||
.map_or_else(Vec::new, |v| {
|
.map_or_else(Vec::new, |v| {
|
||||||
v.split(' ').filter_map(parse_message_id).collect()
|
v.split(' ')
|
||||||
|
.filter_map(|s| parse_message_id(s).ok())
|
||||||
|
.collect()
|
||||||
});
|
});
|
||||||
|
|
||||||
return Ok(Some(Report {
|
return Ok(Some(Report {
|
||||||
@@ -909,14 +911,20 @@ pub(crate) struct Report {
|
|||||||
additional_message_ids: Vec<String>,
|
additional_message_ids: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_message_id(field: &str) -> Option<String> {
|
pub(crate) fn parse_message_id(value: &str) -> crate::error::Result<String> {
|
||||||
if let Ok(addrs) = mailparse::addrparse(field) {
|
let ids = mailparse::msgidparse(value)
|
||||||
// Assume the message id is a single id in the form of <id>
|
.map_err(|err| format_err!("failed to parse message id {:?}", err))?;
|
||||||
if let mailparse::MailAddr::Single(mailparse::SingleInfo { ref addr, .. }) = addrs[0] {
|
|
||||||
return Some(addr.clone());
|
if ids.len() == 1 {
|
||||||
|
let id = &ids[0];
|
||||||
|
if id.starts_with('<') && id.ends_with('>') {
|
||||||
|
Ok(id.chars().skip(1).take(id.len() - 2).collect())
|
||||||
|
} else {
|
||||||
|
bail!("message-ID {} is not enclosed in < and >", value);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
bail!("could not parse message_id: {}", value);
|
||||||
}
|
}
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_known(key: &str) -> bool {
|
fn is_known(key: &str) -> bool {
|
||||||
|
|||||||
84
src/pgp.rs
84
src/pgp.rs
@@ -573,4 +573,88 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(plain, CLEARTEXT);
|
assert_eq!(plain, CLEARTEXT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn test_encrypt_decrypt_fuzz(key_type: KeyGenType) {
|
||||||
|
// sending messages from Alice to Bob
|
||||||
|
let alice = create_keypair(EmailAddress::new("a@e.org").unwrap(), key_type).unwrap();
|
||||||
|
let bob = create_keypair(EmailAddress::new("b@e.org").unwrap(), key_type).unwrap();
|
||||||
|
|
||||||
|
let alice_secret = Key::from(alice.secret.clone());
|
||||||
|
let alice_public = Key::from(alice.public.clone());
|
||||||
|
let bob_secret = Key::from(bob.secret.clone());
|
||||||
|
let bob_public = Key::from(bob.public.clone());
|
||||||
|
|
||||||
|
let plain: &[u8] = b"just a test";
|
||||||
|
|
||||||
|
let mut encr_keyring = Keyring::default();
|
||||||
|
encr_keyring.add_ref(&bob_public);
|
||||||
|
|
||||||
|
let mut decr_keyring = Keyring::default();
|
||||||
|
decr_keyring.add_ref(&bob_secret);
|
||||||
|
let mut validate_keyring = Keyring::default();
|
||||||
|
validate_keyring.add_ref(&alice_public);
|
||||||
|
|
||||||
|
for _ in 0..1000 {
|
||||||
|
let ctext = pk_encrypt(plain.as_ref(), &encr_keyring, Some(&alice_secret)).unwrap();
|
||||||
|
let plain2 =
|
||||||
|
pk_decrypt(ctext.as_ref(), &decr_keyring, &validate_keyring, None).unwrap();
|
||||||
|
assert_eq!(plain2, plain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_encrypt_decrypt_fuzz_rsa() {
|
||||||
|
test_encrypt_decrypt_fuzz(KeyGenType::Rsa2048)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_encrypt_decrypt_fuzz_ecc() {
|
||||||
|
test_encrypt_decrypt_fuzz(KeyGenType::Ed25519)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_encrypt_decrypt_fuzz_alice() -> Result<()> {
|
||||||
|
let plain: &[u8] = b"just a test";
|
||||||
|
let lit_msg = Message::new_literal_bytes("", plain);
|
||||||
|
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
|
||||||
|
let enc_key =
|
||||||
|
SignedPublicKey::from_base64(include_str!("../test-data/key/alice-public.asc"))
|
||||||
|
.unwrap();
|
||||||
|
let enc_subkey = &enc_key.public_subkeys[0];
|
||||||
|
assert!(enc_subkey.is_encryption_key());
|
||||||
|
let pkeys: Vec<&SignedPublicSubKey> = vec![&enc_subkey];
|
||||||
|
|
||||||
|
let skey = SignedSecretKey::from_base64(include_str!("../test-data/key/alice-secret.asc"))
|
||||||
|
.unwrap();
|
||||||
|
let skeys: Vec<&SignedSecretKey> = vec![&skey];
|
||||||
|
|
||||||
|
for _ in 0..1000 {
|
||||||
|
let msg = lit_msg.encrypt_to_keys(
|
||||||
|
&mut rng,
|
||||||
|
pgp::crypto::sym::SymmetricKeyAlgorithm::AES128,
|
||||||
|
&pkeys[..],
|
||||||
|
)?;
|
||||||
|
let ctext = msg.to_armored_string(None)?;
|
||||||
|
|
||||||
|
println!("{}", ctext);
|
||||||
|
|
||||||
|
let (decryptor, _) = msg.decrypt(|| "".into(), || "".into(), &skeys[..])?;
|
||||||
|
let msgs = decryptor.collect::<pgp::errors::Result<Vec<_>>>()?;
|
||||||
|
ensure!(!msgs.is_empty(), "No valid messages found");
|
||||||
|
|
||||||
|
let dec_msg = &msgs[0];
|
||||||
|
|
||||||
|
let plain2: Vec<u8> = match dec_msg.get_content()? {
|
||||||
|
Some(content) => content,
|
||||||
|
None => bail!("Decrypted message is empty"),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(plain2, plain);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ lazy_static::lazy_static! {
|
|||||||
|
|
||||||
// aol.md: aol.com
|
// aol.md: aol.com
|
||||||
static ref P_AOL: Provider = Provider {
|
static ref P_AOL: Provider = Provider {
|
||||||
status: Status::BROKEN,
|
status: Status::PREPARATION,
|
||||||
before_login_hint: "You can't use Delta Chat to login to AOL.",
|
before_login_hint: "To log in to AOL with Delta Chat, you need to set up an app password in the AOL web interface.",
|
||||||
after_login_hint: "Seems like you logged in anyway, congratulations! 🎉 Feel free to tell us at https://github.com/deltachat/provider-db/issues that AOL works again.",
|
after_login_hint: "",
|
||||||
overview_page: "https://providers.delta.chat/aol",
|
overview_page: "https://providers.delta.chat/aol",
|
||||||
server: vec![
|
server: vec![
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
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
|
||||||
@@ -1 +1 @@
|
|||||||
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
|
||||||
Reference in New Issue
Block a user