Compare commits

...

19 Commits

Author SHA1 Message Date
Alexander Krotov
ed092fe1ce test_encrypt_decrypt_fuzz_alice: load secret key directly from file 2020-02-29 17:46:55 +03:00
Alexander Krotov
56f589db4a test_encrypt_decrypt_fuzz_alice: load alice public key directly 2020-02-29 17:19:51 +03:00
Alexander Krotov
49b39b42bf Add test_encrypt_decrypt_fuzz_alice
Simplified test using pregenerated key without signature.
2020-02-28 21:28:24 +03:00
Alexander Krotov
7e4951e691 Replace test Alice key with ed25519 key 2020-02-28 05:17:48 +03:00
B. Petersen
f50874acf1 create a basic failing test for the ecc encryption bug 2020-02-27 19:56:50 +01:00
B. Petersen
4f8c5965ac get fix wrt aol from provider-database
aol is not proken, it just needs some prepararions :)

command for updating:
./src/provider/update.py ../provider-db/_providers/ > src/provider/data.rs
2020-02-24 23:39:40 +03:00
holger krekel
900a17fc00 another fix, thanks @adbenitez 2020-02-22 17:11:37 +01:00
holger krekel
78f36aaa0d another bug fix 2020-02-22 17:11:37 +01:00
holger krekel
e064e02794 fix eventlogger 2020-02-22 17:11:37 +01:00
holger krekel
e22e5045f1 add missing file, some streamlining 2020-02-22 17:11:37 +01:00
holger krekel
087f35482b factor out imex tracking 2020-02-22 17:11:37 +01:00
holger krekel
23ff5fea28 move towards pluggy 2020-02-22 17:11:37 +01:00
holger krekel
34347ccaf5 strike get_infostring 2020-02-22 17:11:37 +01:00
holger krekel
e704eb6cef move eventlogging to own module, start distinguishing ll events 2020-02-22 17:11:37 +01:00
holger krekel
bf63423fec strike footer and refine index page 2020-02-22 17:11:37 +01:00
holger krekel
f6d71ed8ef strike one Account parameter, always do eventlogging 2020-02-22 17:11:37 +01:00
Alexander Krotov
3c342339a1 imap: use parse_message_id from mimeparser 2020-02-22 16:41:07 +01:00
Alexander Krotov
33463856c5 Use mailparse::msgidparse to parse Message-IDs 2020-02-22 16:41:07 +01:00
holger krekel
a18f4c9b1b prepare py-0.800.0 2020-02-21 14:09:05 +01:00
18 changed files with 310 additions and 222 deletions

View File

@@ -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
--------- ---------

View File

@@ -15,3 +15,7 @@ div.globaltoc {
img.logo { img.logo {
height: 120px; height: 120px;
} }
div.footer {
display: none;
}

View File

@@ -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

View File

@@ -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
.. ..

View File

@@ -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'],

View File

@@ -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)

View 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))

View 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. """

View File

@@ -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'],

View File

@@ -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()

View File

@@ -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]

View File

@@ -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()
} }

View File

@@ -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"
);
}
}

View File

@@ -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 {

View File

@@ -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(())
}
} }

View File

@@ -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![
], ],

View File

@@ -1 +1 @@
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

View File

@@ -1 +1 @@
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