Compare commits

..

1 Commits

Author SHA1 Message Date
holger krekel
9b6d7bde82 bring back remote_rust tests but comment them 2020-06-09 15:00:31 +02:00
99 changed files with 2806 additions and 6861 deletions

View File

@@ -138,6 +138,12 @@ jobs:
- py-docs
- wheelhouse
remote_tests_rust:
machine: true
steps:
- checkout
- run: ci_scripts/remote_tests_rust.sh
remote_tests_python:
machine: true
steps:
@@ -172,6 +178,11 @@ workflows:
jobs:
# - cargo_fetch
#- remote_tests_rust
# filters:
# tags:
# only: /.*/
- remote_tests_python:
filters:
tags:
@@ -180,6 +191,7 @@ workflows:
- remote_python_packaging:
requires:
- remote_tests_python
# - remote_tests_rust
filters:
branches:
only: master

View File

@@ -18,7 +18,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.45.0
toolchain: 1.43.1
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
@@ -32,34 +32,21 @@ jobs:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
toolchain: 1.45.0
toolchain: 1.43.1
components: clippy
override: true
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all
build_and_test:
name: Build and test
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
rust: [1.45.0]
experimental: [false]
include:
- os: ubuntu-latest
rust: nightly
experimental: true
- os: windows-latest
rust: nightly
experimental: true
- os: macOS-latest
rust: nightly
experimental: true
rust: [nightly, 1.43.1]
steps:
- uses: actions/checkout@master
@@ -92,10 +79,11 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: check
args: --all --bins --examples --tests --features repl
args: --workspace --all --bins --examples --tests
- name: tests
uses: actions-rs/cargo@v1
with:
command: test
args: --all
args: --workspace

54
ASYNC-API-TODO.txt Normal file
View File

@@ -0,0 +1,54 @@
Delta Chat ASYNC (friedel, bjoern, floris, friedel)
- smtp fake-idle/load jobs gerade noch alle fuenf sekunden , sollte alle zehn minuten (oder gar nicht)
APIs:
dc_context_new # opens the database
dc_open # FFI only
-> drop it and move parameters to dc_context_new()
dc_configure # note: dc_start_jobs() is NOT allowed to run concurrently
dc_imex NEVER goes through the job system
dc_imex import_backup needs to ensure dc_stop_jobs()
dc_start_io # start smtp/imap and job handling subsystems
dc_stop_io # stop smtp/imap and job handling subsystems
dc_is_io_running # return 1 if smtp/imap/jobs susbystem is running
dc_close # FFI only
-> can be dropped
dc_context_unref
for ios share-extension:
Int dc_direct_send() -> try send out without going through jobs system, but queue a job in db if it needs to be retried on failure
0: message was sent
1: message failed to go out, is queued as a job to be retried later
2: message permanently failed?
EVENT handling:
start a callback thread and call get_next_event() which is BLOCKING
it's fine to start this callback thread later, it will see all events.
Note that the core infinitely fills the internal queue if you never drain it.
FFI-get_next_event() returns NULL if the context is unrefed already?
sidenote: how python's callback thread does it currently:
CB-thread runs this while loop:
while not QUITFLAG:
ev = context.get_next_event( )
...
So in order to shutdown properly one has to set QUITFLAG
before calling dc_stop_jobs() and dc_context_unref
event API:
get_data1_int
get_data2_int
get_data3_str
- userdata likely only used for the callbacks, likely can be dropped, needs verification
- iOS needs for the share app to call "try_send_smtp" wihtout a full dc_context_run and without going

View File

@@ -1,164 +1,5 @@
# Changelog
## 1.44.0
- fix peerstate issues #1800 #1805
- fix a crash related to muted chats #1803
- fix incorrect dimensions sometimes reported for images #1806
- fixed `dc_chat_get_remaining_mute_duration` function #1807
- handle empty tags (e.g. `<br/>`) in HTML mails #1810
- always translate the message about disappearing messages timer change #1813
- improve footer detection in plain text email #1812
- update device chat icon to fix warnings in iOS logs #1802
- fix deletion of multiple messages #1795
## 1.43.0
- improve using own jitsi-servers #1785
- fix smtp-timeout tweaks for larger mails #1797
- more bug fixes and updates #1794 #1792 #1789 #1787
## 1.42.0
- new qr-code type `DC_QR_WEBRTC` #1779
- new `dc_chatlist_get_summary2()` api #1771
- tweak smtp-timeout for larger mails #1782
- optimize read-receipts #1765
- Allow http scheme for DCACCOUNT URLs #1770
- improve tests #1769
- bug fixes #1766 #1772 #1773 #1775 #1776 #1777
## 1.41.0
- new apis to initiate video chats #1718 #1735
- new apis `dc_msg_get_ephemeral_timer()`
and `dc_msg_get_ephemeral_timestamp()`
- new api `dc_chatlist_get_summary2()` #1771
- improve IMAP handling #1703 #1704
- improve ephemeral messages #1696 #1705
- mark location-messages as auto-generated #1715
- multi-device avatar-sync #1716 #1717
- improve python bindings #1732 #1733 #1738 #1769
- Allow http scheme for DCACCOUNT urls #1770
- more fixes #1702 #1706 #1707 #1710 #1719 #1721
#1723 #1734 #1740 #1744 #1748 #1760 #1766 #1773 #1765
- refactorings #1712 #1714 #1757
- update toolchains and dependencies #1726 #1736 #1737 #1742 #1743 #1746
## 1.40.0
- introduce ephemeral messages #1540 #1680 #1683 #1684 #1691 #1692
- `DC_MSG_ID_DAYMARKER` gets timestamp attached #1677 #1685
- improve idle #1690 #1688
- fix message processing issues by sequential processing #1694
- refactorings #1670 #1673
## 1.39.0
- fix handling of `mvbox_watch`, `sentbox_watch`, `inbox_watch` #1654 #1658
- fix potential panics, update dependencies #1650 #1655
## 1.38.0
- fix sorting, esp. for multi-device
## 1.37.0
- improve ndn heuristics #1630
- get oauth2 authorizer from provider-db #1641
- removed linebreaks and spaces from generated qr-code #1631
- more fixes #1633 #1635 #1636 #1637
## 1.36.0
- parse ndn (network delivery notification) reports
and report failed messages as such #1552 #1622 #1630
- add oauth2 support for gsuite domains #1626
- read image orientation from exif before recoding #1619
- improve logging #1593 #1598
- improve python and bot bindings #1583 #1609
- improve imap logout #1595
- fix sorting #1600 #1604
- fix qr code generation #1631
- update rustcrypto releases #1603
- refactorings #1617
## 1.35.0
- enable strict-tls from a new provider-db setting #1587
- new subject 'Message from USER' for one-to-one chats #1395
- recode images #1563
- improve reconnect handling #1549 #1580
- improve importing addresses #1544
- improve configure and folder detection #1539 #1548
- improve test suite #1559 #1564 #1580 #1581 #1582 #1584 #1588:
- fix ad-hoc groups #1566
- preventions against being marked as spam #1575
- refactorings #1542 #1569
## 1.34.0
- new api for io, thread and event handling #1356,

1535
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,66 +1,66 @@
[package]
name = "deltachat"
version = "1.44.0"
version = "1.34.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"
[profile.release]
lto = true
# lto = true
[dependencies]
deltachat_derive = { path = "./deltachat_derive" }
libc = "0.2.51"
pgp = { version = "0.6.0", default-features = false }
pgp = { version = "0.5.1", default-features = false }
hex = "0.4.0"
sha2 = "0.9.0"
sha2 = "0.8.0"
rand = "0.7.0"
smallvec = "1.0.0"
surf = { version = "2.0.0-alpha.4", default-features = false, features = ["h1-client"] }
surf = { version = "2.0.0-alpha.2", default-features = false, features = ["h1-client"] }
num-derive = "0.3.0"
num-traits = "0.2.6"
async-smtp = "0.3"
async-smtp = { version = "0.3" }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
async-imap = "0.3.1"
async-native-tls = { version = "0.3.3" }
async-std = { version = "1.6.1", features = ["unstable"] }
base64 = "0.12"
async-std = { version = "1.6.0", features = ["unstable"] }
base64 = "0.11"
charset = "0.1"
percent-encoding = "2.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = "0.4.6"
indexmap = "1.3.0"
kamadak-exif = "0.5"
lazy_static = "1.4.0"
regex = "1.1.6"
rusqlite = { version = "0.23", features = ["bundled"] }
r2d2_sqlite = "0.16.0"
rusqlite = { version = "0.22", features = ["bundled"] }
r2d2_sqlite = "0.15.0"
r2d2 = "0.8.5"
strum = "0.18.0"
strum_macros = "0.18.0"
strum = "0.16.0"
strum_macros = "0.16.0"
backtrace = "0.3.33"
byteorder = "1.3.1"
itertools = "0.9.0"
quick-xml = "0.18.1"
itertools = "0.8.0"
image-meta = "0.1.0"
quick-xml = "0.17.1"
escaper = "0.1.0"
bitflags = "1.1.0"
debug_stub_derive = "0.3.0"
sanitize-filename = "0.2.1"
stop-token = { version = "0.1.1", features = ["unstable"] }
mailparse = "0.12.1"
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
native-tls = "0.2.3"
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
image = { version = "0.22.4", default-features=false, features = ["gif_codec", "jpeg", "ico", "png_codec", "pnm", "webp", "bmp"] }
futures = "0.3.4"
thiserror = "1.0.14"
anyhow = "1.0.28"
async-trait = "0.1.31"
url = "2.1.1"
async-std-resolver = "0.19.5"
pretty_env_logger = { version = "0.4.0", optional = true }
pretty_env_logger = { version = "0.3.1", optional = true }
log = {version = "0.4.8", optional = true }
rustyline = { version = "4.1.0", optional = true }
ansi_term = { version = "0.12.1", optional = true }
@@ -69,8 +69,8 @@ ansi_term = { version = "0.12.1", optional = true }
[dev-dependencies]
tempfile = "3.0"
pretty_assertions = "0.6.1"
pretty_env_logger = "0.4.0"
proptest = "0.10"
pretty_env_logger = "0.3.0"
proptest = "0.9.4"
async-std = { version = "1.6.0", features = ["unstable", "attributes"] }
smol = "0.1.10"

View File

@@ -123,7 +123,6 @@ Language bindings are available for:
- [Node.js](https://www.npmjs.com/package/deltachat-node)
- [Python](https://py.delta.chat)
- [Go](https://github.com/hugot/go-deltachat/)
- [Free Pascal](https://github.com/deltachat/deltachat-fp/)
- **Java** and **Swift** (contained in the Android/iOS repos)
The following "frontend" projects make use of the Rust-library

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -16,6 +16,6 @@ ENV PIP_DISABLE_PIP_VERSION_CHECK 1
ADD deps/build_python.sh /builder/build_python.sh
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_python.sh && cd .. && rm -r tmp1
# Install Rust
# Install Rust nightly
ADD deps/build_rust.sh /builder/build_rust.sh
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_rust.sh && cd .. && rm -r tmp1

View File

@@ -3,9 +3,9 @@
set -e -x
# Install Rust
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.45.0-x86_64-unknown-linux-gnu -y
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.43.1-x86_64-unknown-linux-gnu -y
export PATH=/root/.cargo/bin:$PATH
rustc --version
# remove some 300-400 MB that we don't need for automated builds
rm -rf /root/.rustup/toolchains/1.45.0-x86_64-unknown-linux-gnu/share
rm -rf /root/.rustup/toolchains/1.43.1-x86_64-unknown-linux-gnu/share

View File

@@ -15,7 +15,6 @@ cargo build --release -p deltachat_ffi
# Statically link against libdeltachat.a.
export DCC_RS_DEV=$(pwd)
export DCC_RS_TARGET=release
# Configure access to a base python and to several python interpreters
# needed by tox below.

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.44.0"
version = "1.34.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"

View File

@@ -19,10 +19,10 @@ fn main() {
include_str!("deltachat.pc.in"),
name = "deltachat",
description = env::var("CARGO_PKG_DESCRIPTION").unwrap(),
url = env::var("CARGO_PKG_HOMEPAGE").unwrap_or_else(|_| "".to_string()),
url = env::var("CARGO_PKG_HOMEPAGE").unwrap_or("".to_string()),
version = env::var("CARGO_PKG_VERSION").unwrap(),
libs_priv = libs_priv,
prefix = env::var("PREFIX").unwrap_or_else(|_| "/usr/local".to_string()),
prefix = env::var("PREFIX").unwrap_or("/usr/local".to_string()),
);
fs::create_dir_all(target_path.join("pkgconfig")).unwrap();

View File

@@ -262,25 +262,17 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `selfavatar` = File containing avatar. Will immediately be copied to the
* `blobdir`; the original image will not be needed anymore.
* NULL to remove the avatar.
* As for `displayname` and `selfstatus`, also the avatar is sent to the recipients.
* To save traffic, however, the avatar is attached only as needed
* and also recoded to a reasonable size.
* It is planned for future versions
* to send this image together with the next messages.
* - `e2ee_enabled` = 0=no end-to-end-encryption, 1=prefer end-to-end-encryption (default)
* - `mdns_enabled` = 0=do not send or request read receipts,
* 1=send and request read receipts (default)
* - `bcc_self` = 0=do not send a copy of outgoing messages to self (default),
* 1=send a copy of outgoing messages to self.
* Sending messages to self is needed for a proper multi-account setup,
* however, on the other hand, may lead to unwanted notifications in non-delta clients.
* - `inbox_watch` = 1=watch `INBOX`-folder for changes (default),
* 0=do not watch the `INBOX`-folder,
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
* 0=do not watch the `INBOX`-folder
* - `sentbox_watch`= 1=watch `Sent`-folder for changes (default),
* 0=do not watch the `Sent`-folder,
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
* 0=do not watch the `Sent`-folder
* - `mvbox_watch` = 1=watch `DeltaChat`-folder for changes (default),
* 0=do not watch the `DeltaChat`-folder,
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
* 0=do not watch the `DeltaChat`-folder
* - `mvbox_move` = 1=heuristically detect chat-messages
* and move them to the `DeltaChat`-folder,
* 0=do not move chat-messages
@@ -315,15 +307,9 @@ char* dc_get_blobdir (const dc_context_t* context);
* DC_MEDIA_QUALITY_WORSE (1)
* allow worse images/videos/voice quality to gain smaller sizes,
* suitable for providers or areas known to have a bad connection.
* The library uses the `media_quality` setting to use different defaults
* for recoding images sent with type DC_MSG_IMAGE.
* If needed, recoding other file types is up to the UI.
* - `webrtc_instance` = webrtc instance to use for videochats in the form
* `[basicwebrtc:|jitsi:]https://example.com/subdir#roomname=$ROOM`
* if the url is prefixed by `basicwebrtc`, the server is assumed to be of the type
* https://github.com/cracker0dks/basicwebrtc which some UIs have native support for.
* The type `jitsi:` may be handled by external apps.
* If no type is prefixed, the videochat is handled completely in a browser.
* In contrast to other options, the implementation of this option is currently up to the UIs;
* this may change in future, however,
* having the option in the core allows provider-specific-defaults already today.
*
* If you want to retrieve a value, use dc_get_config().
*
@@ -376,13 +362,12 @@ int dc_set_stock_translation(dc_context_t* context, uint32_t stock_i
/**
* Set configuration values from a QR code.
* Set configuration values from a QR code containing an account.
* Before this function is called, dc_check_qr() should confirm the type of the
* QR code is DC_QR_ACCOUNT or DC_QR_WEBRTC_INSTANCE.
* QR code is DC_QR_ACCOUNT.
*
* Internally, the function will call dc_set_config() with the appropriate keys,
* eg. `addr` and `mail_pw` for DC_QR_ACCOUNT
* or `webrtc_instance` for DC_QR_WEBRTC_INSTANCE.
* Internally, the function will call dc_set_config()
* at least with the keys `addr` and `mail_pw`.
*
* @memberof dc_context_t
* @param context The context object
@@ -543,20 +528,9 @@ int dc_is_io_running(const dc_context_t* context);
void dc_stop_io(dc_context_t* context);
/**
* This function should be called when there is a hint
* that the network is available again,
* eg. as a response to system event reporting network availability.
* The library will try to send pending messages out immediately.
*
* Moreover, to have a reliable state
* when the app comes to foreground with network available,
* it may be reasonable to call the function also at that moment.
*
* It is okay to call the function unconditionally when there is
* network available, however, calling the function
* _without_ having network may interfere with the backoff algorithm
* and will led to let the jobs fail faster, with fewer retries
* and may avoid messages being sent out.
* This function can be called whenever there is a hint
* that the network is available again.
* The library will try to send pending messages out.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
@@ -780,15 +754,6 @@ uint32_t dc_prepare_msg (dc_context_t* context, uint32_t ch
* dc_msg_unref(msg);
* ~~~
*
* If you send images with the DC_MSG_IMAGE type,
* they will be recoded to a reasonable size before sending, if possible
* (cmp the dc_set_config()-option `media_quality`).
* If that fails, is not possible, or the image is already small enough, the image is sent as original.
* If you want images to be always sent as the original file, use the DC_MSG_FILE type.
*
* Videos and other file types are currently not recoded by the library,
* with dc_prepare_msg(), however, you can do that from the UI.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id Chat ID to send the message to.
@@ -841,42 +806,6 @@ uint32_t dc_send_msg_sync (dc_context_t* context, uint32
uint32_t dc_send_text_msg (dc_context_t* context, uint32_t chat_id, const char* text_to_send);
/**
* Send invitation to a videochat.
*
* This function reads the `webrtc_instance` config value,
* may check that the server is working in some way
* and creates a unique room for this chat, if needed doing a TOKEN roundtrip for that.
*
* After that, the function sends out a message that contains information to join the room:
*
* - To allow non-delta-clients to join the chat,
* the message contains a text-area with some descriptive text
* and a url that can be opened in a supported browser to join the videochat
*
* - delta-clients can get all information needed from
* the message object, using eg.
* dc_msg_get_videochat_url() and check dc_msg_get_viewtype() for #DC_MSG_VIDEOCHAT_INVITATION
*
* dc_send_videochat_invitation() is blocking and may take a while,
* so the UIs will typically call the function from within a thread.
* Moreover, UIs will typically enter the room directly without an additional click on the message,
* for this purpose, the function returns the message-id directly.
*
* As for other messages sent, this function
* sends the event #DC_EVENT_MSGS_CHANGED on succcess, the message has a delivery state, and so on.
* The recipient will get noticed by the call as usual by #DC_EVENT_INCOMING_MSG or #DC_EVENT_MSGS_CHANGED,
* However, UIs might some things differently, eg. play a different sound.
*
* @memberof dc_context_t
* @param context The context object.
* @param chat_id The chat to start a videochat for.
* @return The id if the message sent out
* or 0 for errors.
*/
uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
/**
* Save a draft for a chat in the database.
*
@@ -1020,7 +949,6 @@ dc_msg_t* dc_get_draft (dc_context_t* context, uint32_t ch
* @param chat_id The chat ID of which the messages IDs should be queried.
* @param flags If set to DC_GCM_ADDDAYMARKER, the marker DC_MSG_ID_DAYMARKER will
* be added before each day (regarding the local timezone). Set this to 0 if you do not want this behaviour.
* To get the concrete time of the marker, use dc_array_get_timestamp().
* @param marker1before An optional message ID. If set, the id DC_MSG_ID_MARKER1 will be added just
* before the given ID in the returned array. Set this to 0 if you do not want this behaviour.
* @return Array of message IDs, must be dc_array_unref()'d when no longer used.
@@ -1215,16 +1143,6 @@ void dc_delete_chat (dc_context_t* context, uint32_t ch
*/
dc_array_t* dc_get_chat_contacts (dc_context_t* context, uint32_t chat_id);
/**
* Get the chat's ephemeral message timer.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @param chat_id The chat ID.
*
* @return ephemeral timer value in seconds, 0 if the timer is disabled or if there is an error
*/
uint32_t dc_get_chat_ephemeral_timer (dc_context_t* context, uint32_t chat_id);
/**
* Search messages containing the given query string.
@@ -1357,21 +1275,6 @@ int dc_remove_contact_from_chat (dc_context_t* context, uint32_t ch
*/
int dc_set_chat_name (dc_context_t* context, uint32_t chat_id, const char* name);
/**
* Set the chat's ephemeral message timer.
*
* This timer is applied to all messages in a chat and starts when the
* message is read. The setting is synchronized to all clients
* participating in a chat.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @param chat_id The chat ID to set the ephemeral message timer for.
* @param timer The timer value in seconds or 0 to disable the timer.
*
* @return 1=success, 0=error
*/
int dc_set_chat_ephemeral_timer (dc_context_t* context, uint32_t chat_id, uint32_t timer);
/**
* Set group profile image.
@@ -1929,7 +1832,6 @@ void dc_stop_ongoing_process (dc_context_t* context);
#define DC_QR_FPR_MISMATCH 220 // id=contact
#define DC_QR_FPR_WITHOUT_ADDR 230 // test1=formatted fingerprint
#define DC_QR_ACCOUNT 250 // text1=domain
#define DC_QR_WEBRTC_INSTANCE 260 // text1=domain
#define DC_QR_ADDR 320 // id=contact
#define DC_QR_TEXT 330 // text1=text
#define DC_QR_URL 332 // text1=URL
@@ -1948,9 +1850,6 @@ void dc_stop_ongoing_process (dc_context_t* context);
* - DC_QR_FPR_MISMATCH with dc_lot_t::id=Contact ID
* - DC_QR_FPR_WITHOUT_ADDR with dc_lot_t::test1=Formatted fingerprint
* - DC_QR_ACCOUNT allows creation of an account, dc_lot_t::text1=domain
* - DC_QR_WEBRTC_INSTANCE - a shared webrtc-instance
* that will be set if dc_set_config_from_qr() is called with the qr-code,
* dc_lot_t::text1=domain could be used to ask the user
* - DC_QR_ADDR with dc_lot_t::id=Contact ID
* - DC_QR_TEXT with dc_lot_t::text1=Text
* - DC_QR_URL with dc_lot_t::text1=URL
@@ -2358,6 +2257,17 @@ int dc_array_is_independent (const dc_array_t* array, size_t in
int dc_array_search_id (const dc_array_t* array, uint32_t needle, size_t* ret_index);
/**
* Get raw pointer to the data.
*
* @memberof dc_array_t
* @param array The array object.
* @return Raw pointer to the array. You MUST NOT free the data. You MUST NOT access the data beyond the current item count.
* It is not possible to enlarge the array this way. Calling any other dc_array*()-function may discard the returned pointer.
*/
const uint32_t* dc_array_get_raw (const dc_array_t* array);
/**
* @class dc_chatlist_t
*
@@ -2476,28 +2386,6 @@ uint32_t dc_chatlist_get_msg_id (const dc_chatlist_t* chatlist, siz
dc_lot_t* dc_chatlist_get_summary (const dc_chatlist_t* chatlist, size_t index, dc_chat_t* chat);
/**
* Create a chatlist summary item when the chatlist object is already unref()'d.
*
* This function is similar to dc_chatlist_get_summary(), however,
* takes the chat-id and message-id as returned by dc_chatlist_get_chat_id() and dc_chatlist_get_msg_id()
* as arguments. The chatlist object itself is not needed directly.
*
* This maybe useful if you convert the complete object into a different represenation
* as done eg. in the node-bindings.
* If you have access to the chatlist object in some way, using this function is not recommended,
* use dc_chatlist_get_summary() in this case instead.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new()
* @param chat_id Chat to get a summary for.
* @param msg_id Messasge to get a summary for.
* @return The summary as an dc_lot_t object, see dc_chatlist_get_summary() for details.
* Must be freed using dc_lot_unref(). NULL is never returned.
*/
dc_lot_t* dc_chatlist_get_summary2 (dc_context_t* context, uint32_t chat_id, uint32_t msg_id);
/**
* Helper function to get the associated context object.
*
@@ -2894,9 +2782,6 @@ int dc_msg_get_viewtype (const dc_msg_t* msg);
* If a sent message changes to this state, you'll receive the event #DC_EVENT_MSG_DELIVERED.
* - DC_STATE_OUT_MDN_RCVD (28) - Outgoing message read by the recipient (two checkmarks; this requires goodwill on the receiver's side)
* If a sent message changes to this state, you'll receive the event #DC_EVENT_MSG_READ.
* Also messages already read by some recipients
* may get into the state DC_STATE_OUT_FAILED at a later point,
* eg. when in a group, delivery fails for some recipients.
*
* If you just want to check if a message is sent or not, please use dc_msg_is_sent() which regards all states accordingly.
*
@@ -3095,32 +2980,6 @@ int dc_msg_get_duration (const dc_msg_t* msg);
int dc_msg_get_showpadlock (const dc_msg_t* msg);
/**
* Get ephemeral timer duration for message.
*
* To check if the timer is started and calculate remaining time,
* use dc_msg_get_ephemeral_timestamp().
*
* @memberof dc_msg_t
* @param msg The message object.
* @return Duration in seconds, or 0 if no timer is set.
*/
uint32_t dc_msg_get_ephemeral_timer (const dc_msg_t* msg);
/**
* Get timestamp of ephemeral message removal.
*
* If returned value is non-zero, you can calculate the * fraction of
* time remaining by divinding the difference between the current timestamp
* and this timestamp by dc_msg_get_ephemeral_timer().
*
* @memberof dc_msg_t
* @param msg The message object.
* @return Time of message removal, 0 if the timer is not started.
*/
int64_t dc_msg_get_ephemeral_timestamp (const dc_msg_t* msg);
/**
* Get a summary for a message.
*
@@ -3304,57 +3163,6 @@ int dc_msg_is_setupmessage (const dc_msg_t* msg);
char* dc_msg_get_setupcodebegin (const dc_msg_t* msg);
/**
* Get url of a videochat invitation.
*
* Videochat invitations are sent out using dc_send_videochat_invitation()
* and dc_msg_get_viewtype() returns #DC_MSG_VIDEOCHAT_INVITATION for such invitations.
*
* @memberof dc_msg_t
* @param msg The message object.
* @return If the message contains a videochat invitation,
* the url of the invitation is returned.
* If the message is no videochat invitation, NULL is returned.
* Must be released using dc_str_unref() when done.
*/
char* dc_msg_get_videochat_url (const dc_msg_t* msg);
/**
* Get type of videochat.
*
* Calling this functions only makes sense for messages of type #DC_MSG_VIDEOCHAT_INVITATION,
* in this case, if `basicwebrtc:` as of https://github.com/cracker0dks/basicwebrtc or `jitsi`
* were used to initiate the videochat,
* dc_msg_get_videochat_type() returns the corresponding type.
*
* The videochat-url can be retrieved using dc_msg_get_videochat_url().
* To check if a message is a videochat invitation at all, check the message type for #DC_MSG_VIDEOCHAT_INVITATION.
*
* @memberof dc_msg_t
* @param msg The message object.
* @return Type of the videochat as of DC_VIDEOCHATTYPE_BASICWEBRTC, DC_VIDEOCHATTYPE_JITSI or DC_VIDEOCHATTYPE_UNKNOWN.
*
* Example:
* ~~~
* if (dc_msg_get_viewtype(msg) == DC_MSG_VIDEOCHAT_INVITATION) {
* if (dc_msg_get_videochat_type(msg) == DC_VIDEOCHATTYPE_BASICWEBRTC) {
* // videochat invitation that we ship a client for
* } else {
* // use browser for videochat - or add an additional check for DC_VIDEOCHATTYPE_JITSI
* }
* } else {
* // not a videochat invitation
* }
* ~~~
*/
int dc_msg_get_videochat_type (const dc_msg_t* msg);
#define DC_VIDEOCHATTYPE_UNKNOWN 0
#define DC_VIDEOCHATTYPE_BASICWEBRTC 1
#define DC_VIDEOCHATTYPE_JITSI 2
/**
* Set the text of a message object.
* This does not alter any information in the database; this may be done by dc_send_msg() later.
@@ -3890,18 +3698,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*/
#define DC_MSG_FILE 60
/**
* Message indicating an incoming or outgoing videochat.
* The message was created via dc_send_videochat_invitation() on this or a remote device.
*
* Typically, such messages are rendered differently by the UIs,
* eg. contain a button to join the videochat.
* The url for joining can be retrieved using dc_msg_get_videochat_url().
*/
#define DC_MSG_VIDEOCHAT_INVITATION 70
/**
* @}
*/
@@ -4259,12 +4055,14 @@ void dc_event_unref(dc_event_t* event);
* Network errors should be reported to users in a non-disturbing way,
* however, as network errors may come in a sequence,
* it is not useful to raise each an every error to the user.
* For this purpose, data1 is set to 1 if the error is probably worth reporting.
*
* Moreover, if the UI detects that the device is offline,
* it is probably more useful to report this to the user
* instead of the string from data2.
*
* @param data1 0
* @param data1 (int) 1=first/new network error, should be reported the user;
* 0=subsequent network error, should be logged only
* @param data2 (char*) Error string, always set, never NULL.
*/
#define DC_EVENT_ERROR_NETWORK 401
@@ -4319,9 +4117,8 @@ void dc_event_unref(dc_event_t* event);
/**
* A single message could not be sent.
* State changed from DC_STATE_OUT_PENDING, DC_STATE_OUT_DELIVERED or DC_STATE_OUT_MDN_RCVD
* to DC_STATE_OUT_FAILED, see dc_msg_get_state().
* A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
* DC_STATE_OUT_FAILED, see dc_msg_get_state().
*
* @param data1 (int) chat_id
* @param data2 (int) msg_id
@@ -4350,11 +4147,6 @@ void dc_event_unref(dc_event_t* event);
*/
#define DC_EVENT_CHAT_MODIFIED 2020
/**
* Chat ephemeral timer changed.
*/
#define DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED 2021
/**
* Contact(s) created, renamed, verified, blocked or deleted.
@@ -4491,7 +4283,7 @@ void dc_event_unref(dc_event_t* event);
*/
/**
* Provider works out-of-the-box.
* Prover works out-of-the-box.
* This provider status is returned for provider where the login
* works by just entering the name or the email-address.
*
@@ -4636,30 +4428,12 @@ void dc_event_unref(dc_event_t* event);
#define DC_STR_WELCOME_MESSAGE 71
#define DC_STR_UNKNOWN_SENDER_FOR_CHAT 72
#define DC_STR_SUBJECT_FOR_NEW_CONTACT 73
#define DC_STR_FAILED_SENDING_TO 74
#define DC_STR_EPHEMERAL_DISABLED 75
#define DC_STR_EPHEMERAL_SECONDS 76
#define DC_STR_EPHEMERAL_MINUTE 77
#define DC_STR_EPHEMERAL_HOUR 78
#define DC_STR_EPHEMERAL_DAY 79
#define DC_STR_EPHEMERAL_WEEK 80
#define DC_STR_EPHEMERAL_FOUR_WEEKS 81
#define DC_STR_VIDEOCHAT_INVITATION 82
#define DC_STR_VIDEOCHAT_INVITE_MSG_BODY 83
#define DC_STR_COUNT 83
#define DC_STR_COUNT 73
/*
* @}
*/
#ifdef PY_CFFI_INC
/* Helper utility to locate the header file when building python bindings. */
char* _dc_header_file_location(void) {
return __FILE__;
}
#endif
#ifdef __cplusplus
}

View File

@@ -1,56 +1,46 @@
use crate::chat::ChatItem;
use crate::constants::{DC_MSG_ID_DAYMARKER, DC_MSG_ID_MARKER1};
use crate::location::Location;
use crate::message::MsgId;
/* * the structure behind dc_array_t */
#[derive(Debug, Clone)]
pub enum dc_array_t {
MsgIds(Vec<MsgId>),
Chat(Vec<ChatItem>),
Locations(Vec<Location>),
Uint(Vec<u32>),
}
impl dc_array_t {
pub(crate) fn get_id(&self, index: usize) -> u32 {
pub fn new(capacity: usize) -> Self {
dc_array_t::Uint(Vec::with_capacity(capacity))
}
/// Constructs a new, empty `dc_array_t` holding locations with specified `capacity`.
pub fn new_locations(capacity: usize) -> Self {
dc_array_t::Locations(Vec::with_capacity(capacity))
}
pub fn add_id(&mut self, item: u32) {
if let Self::Uint(array) = self {
array.push(item);
} else {
panic!("Attempt to add id to array of other type");
}
}
pub fn add_location(&mut self, location: Location) {
if let Self::Locations(array) = self {
array.push(location)
} else {
panic!("Attempt to add a location to array of other type");
}
}
pub fn get_id(&self, index: usize) -> u32 {
match self {
Self::MsgIds(array) => array[index].to_u32(),
Self::Chat(array) => match array[index] {
ChatItem::Message { msg_id } => msg_id.to_u32(),
ChatItem::Marker1 => DC_MSG_ID_MARKER1,
ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
},
Self::Locations(array) => array[index].location_id,
Self::Uint(array) => array[index],
Self::Uint(array) => array[index] as u32,
}
}
pub(crate) fn get_timestamp(&self, index: usize) -> Option<i64> {
match self {
Self::MsgIds(_) => None,
Self::Chat(array) => array.get(index).and_then(|item| match item {
ChatItem::Message { .. } => None,
ChatItem::Marker1 { .. } => None,
ChatItem::DayMarker { timestamp } => Some(*timestamp),
}),
Self::Locations(array) => array.get(index).map(|location| location.timestamp),
Self::Uint(_) => None,
}
}
pub(crate) fn get_marker(&self, index: usize) -> Option<&str> {
match self {
Self::MsgIds(_) => None,
Self::Chat(_) => None,
Self::Locations(array) => array
.get(index)
.and_then(|location| location.marker.as_deref()),
Self::Uint(_) => None,
}
}
pub(crate) fn get_location(&self, index: usize) -> &Location {
pub fn get_location(&self, index: usize) -> &Location {
if let Self::Locations(array) = self {
&array[index]
} else {
@@ -58,18 +48,55 @@ impl dc_array_t {
}
}
/// Returns the number of elements in the array.
pub(crate) fn len(&self) -> usize {
pub fn is_empty(&self) -> bool {
match self {
Self::Locations(array) => array.is_empty(),
Self::Uint(array) => array.is_empty(),
}
}
/// Returns the number of elements in the array.
pub fn len(&self) -> usize {
match self {
Self::MsgIds(array) => array.len(),
Self::Chat(array) => array.len(),
Self::Locations(array) => array.len(),
Self::Uint(array) => array.len(),
}
}
pub(crate) fn search_id(&self, needle: u32) -> Option<usize> {
(0..self.len()).find(|i| self.get_id(*i) == needle)
pub fn clear(&mut self) {
match self {
Self::Locations(array) => array.clear(),
Self::Uint(array) => array.clear(),
}
}
pub fn search_id(&self, needle: u32) -> Option<usize> {
if let Self::Uint(array) = self {
for (i, &u) in array.iter().enumerate() {
if u == needle {
return Some(i);
}
}
None
} else {
panic!("Attempt to search for id in array of other type");
}
}
pub fn sort_ids(&mut self) {
if let dc_array_t::Uint(v) = self {
v.sort();
} else {
panic!("Attempt to sort array of something other than uints");
}
}
pub fn as_ptr(&self) -> *const u32 {
if let dc_array_t::Uint(v) = self {
v.as_ptr()
} else {
panic!("Attempt to convert array of something other than uints to raw");
}
}
}
@@ -79,18 +106,6 @@ impl From<Vec<u32>> for dc_array_t {
}
}
impl From<Vec<MsgId>> for dc_array_t {
fn from(array: Vec<MsgId>) -> Self {
dc_array_t::MsgIds(array)
}
}
impl From<Vec<ChatItem>> for dc_array_t {
fn from(array: Vec<ChatItem>) -> Self {
dc_array_t::Chat(array)
}
}
impl From<Vec<Location>> for dc_array_t {
fn from(array: Vec<Location>) -> Self {
dc_array_t::Locations(array)
@@ -103,11 +118,12 @@ mod tests {
#[test]
fn test_dc_array() {
let arr: dc_array_t = Vec::<u32>::new().into();
assert!(arr.len() == 0);
let mut arr = dc_array_t::new(7);
assert!(arr.is_empty());
let ids: Vec<u32> = (2..1002).collect();
let arr: dc_array_t = ids.into();
for i in 0..1000 {
arr.add_id(i + 2);
}
assert_eq!(arr.len(), 1000);
@@ -115,15 +131,31 @@ mod tests {
assert_eq!(arr.get_id(i), (i + 2) as u32);
}
assert_eq!(arr.search_id(10), Some(8));
assert_eq!(arr.search_id(1), None);
arr.clear();
assert!(arr.is_empty());
arr.add_id(13);
arr.add_id(7);
arr.add_id(666);
arr.add_id(0);
arr.add_id(5000);
arr.sort_ids();
assert_eq!(arr.get_id(0), 0);
assert_eq!(arr.get_id(1), 7);
assert_eq!(arr.get_id(2), 13);
assert_eq!(arr.get_id(3), 666);
}
#[test]
#[should_panic]
fn test_dc_array_out_of_bounds() {
let ids: Vec<u32> = (2..1002).collect();
let arr: dc_array_t = ids.into();
let mut arr = dc_array_t::new(7);
for i in 0..1000 {
arr.add_id(i + 2);
}
arr.get_id(1000);
}
}

View File

@@ -1,4 +1,3 @@
#![deny(clippy::all)]
#![allow(
non_camel_case_types,
non_snake_case,
@@ -28,7 +27,6 @@ use deltachat::chat::{ChatId, ChatVisibility, MuteDuration};
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
use deltachat::contact::{Contact, Origin};
use deltachat::context::Context;
use deltachat::ephemeral::Timer as EphemeralTimer;
use deltachat::key::DcKey;
use deltachat::message::MsgId;
use deltachat::stock::StockMessage;
@@ -38,7 +36,6 @@ mod dc_array;
mod string;
use self::string::*;
use deltachat::chatlist::Chatlist;
// as C lacks a good and portable error handling,
// in general, the C Interface is forgiving wrt to bad parameters.
@@ -129,7 +126,7 @@ pub unsafe extern "C" fn dc_set_config(
// When ctx.set_config() fails it already logged the error.
// TODO: Context::set_config() should not log this
Ok(key) => block_on(async move {
ctx.set_config(key, to_opt_string_lossy(value).as_deref())
ctx.set_config(key, to_opt_string_lossy(value).as_ref().map(|x| x.as_str()))
.await
.is_ok() as libc::c_int
}),
@@ -288,7 +285,7 @@ pub unsafe extern "C" fn dc_start_io(context: *mut dc_context_t) {
}
let ctx = &*context;
block_on(ctx.start_io())
block_on({ ctx.start_io() })
}
#[no_mangle]
@@ -298,7 +295,7 @@ pub unsafe extern "C" fn dc_is_io_running(context: *mut dc_context_t) -> libc::c
}
let ctx = &*context;
block_on(ctx.is_io_running()) as libc::c_int
block_on({ ctx.is_io_running() }) as libc::c_int
}
#[no_mangle]
@@ -352,8 +349,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| Event::MsgDelivered { chat_id, .. }
| Event::MsgFailed { chat_id, .. }
| Event::MsgRead { chat_id, .. }
| Event::ChatModified(chat_id)
| Event::ChatEphemeralTimerModified { chat_id, .. } => chat_id.to_u32() as libc::c_int,
| Event::ChatModified(chat_id) => chat_id.to_u32() as libc::c_int,
Event::ContactsChanged(id) | Event::LocationChanged(id) => {
let id = id.unwrap_or_default();
id as libc::c_int
@@ -403,7 +399,6 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| Event::MsgRead { msg_id, .. } => msg_id.to_u32() as libc::c_int,
Event::SecurejoinInviterProgress { progress, .. }
| Event::SecurejoinJoinerProgress { progress, .. } => *progress as libc::c_int,
Event::ChatEphemeralTimerModified { timer, .. } => timer.to_u32() as libc::c_int,
}
}
@@ -444,8 +439,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| Event::ConfigureProgress(_)
| Event::ImexProgress(_)
| Event::SecurejoinInviterProgress { .. }
| Event::SecurejoinJoinerProgress { .. }
| Event::ChatEphemeralTimerModified { .. } => ptr::null_mut(),
| Event::SecurejoinJoinerProgress { .. } => ptr::null_mut(),
Event::ImexFileWritten(file) => {
let data2 = file.to_c_string().unwrap_or_default();
data2.into_raw()
@@ -488,7 +482,7 @@ pub unsafe extern "C" fn dc_get_next_event(events: *mut dc_event_emitter_t) -> *
events
.recv_sync()
.map(|ev| Box::into_raw(Box::new(ev)))
.unwrap_or_else(ptr::null_mut)
.unwrap_or_else(|| ptr::null_mut())
}
#[no_mangle]
@@ -560,7 +554,14 @@ pub unsafe extern "C" fn dc_get_chatlist(
let qi = if query_id == 0 { None } else { Some(query_id) };
block_on(async move {
match chatlist::Chatlist::try_load(&ctx, flags as usize, qs.as_deref(), qi).await {
match chatlist::Chatlist::try_load(
&ctx,
flags as usize,
qs.as_ref().map(|x| x.as_str()),
qi,
)
.await
{
Ok(list) => {
let ffi_list = ChatlistWrapper { context, list };
Box::into_raw(Box::new(ffi_list))
@@ -711,25 +712,6 @@ pub unsafe extern "C" fn dc_send_text_msg(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_send_videochat_invitation(
context: *mut dc_context_t,
chat_id: u32,
) -> u32 {
if context.is_null() {
eprintln!("ignoring careless call to dc_send_videochat_invitation()");
return 0;
}
let ctx = &*context;
block_on(async move {
chat::send_videochat_invitation(&ctx, ChatId::new(chat_id))
.await
.map(|msg_id| msg_id.to_u32())
.unwrap_or_log_default(&ctx, "Failed to send video chat invitation")
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_draft(
context: *mut dc_context_t,
@@ -770,9 +752,13 @@ pub unsafe extern "C" fn dc_add_device_msg(
};
block_on(async move {
chat::add_device_msg(&ctx, to_opt_string_lossy(label).as_deref(), msg)
.await
.unwrap_or_log_default(&ctx, "Failed to add device message")
chat::add_device_msg(
&ctx,
to_opt_string_lossy(label).as_ref().map(|x| x.as_str()),
msg,
)
.await
.unwrap_or_log_default(&ctx, "Failed to add device message")
})
.to_u32()
}
@@ -855,11 +841,14 @@ pub unsafe extern "C" fn dc_get_chat_msgs(
};
block_on(async move {
Box::into_raw(Box::new(
let arr = dc_array_t::from(
chat::get_chat_msgs(&ctx, ChatId::new(chat_id), flags, marker_flag)
.await
.into(),
))
.iter()
.map(|msg_id| msg_id.to_u32())
.collect::<Vec<u32>>(),
);
Box::into_raw(Box::new(arr))
})
}
@@ -988,7 +977,7 @@ pub unsafe extern "C" fn dc_get_chat_media(
from_prim(or_msg_type3).expect(&format!("incorrect or_msg_type3 = {}", or_msg_type3));
block_on(async move {
Box::into_raw(Box::new(
let arr = dc_array_t::from(
chat::get_chat_media(
&ctx,
ChatId::new(chat_id),
@@ -997,8 +986,11 @@ pub unsafe extern "C" fn dc_get_chat_media(
or_msg_type3,
)
.await
.into(),
))
.iter()
.map(|msg_id| msg_id.to_u32())
.collect::<Vec<u32>>(),
);
Box::into_raw(Box::new(arr))
})
}
@@ -1286,9 +1278,7 @@ pub unsafe extern "C" fn dc_set_chat_mute_duration(
let muteDuration = match duration {
0 => MuteDuration::NotMuted,
-1 => MuteDuration::Forever,
n if n > 0 => SystemTime::now()
.checked_add(Duration::from_secs(duration as u64))
.map_or(MuteDuration::Forever, MuteDuration::Until),
n if n > 0 => MuteDuration::Until(SystemTime::now() + Duration::from_secs(duration as u64)),
_ => {
warn!(
ctx,
@@ -1306,49 +1296,6 @@ pub unsafe extern "C" fn dc_set_chat_mute_duration(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_chat_ephemeral_timer(
context: *mut dc_context_t,
chat_id: u32,
) -> u32 {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_chat_ephemeral_timer()");
return 0;
}
let ctx = &*context;
// Timer value 0 is returned in the rare case of a database error,
// but it is not dangerous since it is only meant to be used as a
// default when changing the value. Such errors should not be
// ignored when ephemeral timer value is used to construct
// message headers.
block_on(async move { ChatId::new(chat_id).get_ephemeral_timer(ctx).await })
.log_err(ctx, "Failed to get ephemeral timer")
.unwrap_or_default()
.to_u32()
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_chat_ephemeral_timer(
context: *mut dc_context_t,
chat_id: u32,
timer: u32,
) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_set_chat_ephemeral_timer()");
return 0;
}
let ctx = &*context;
block_on(async move {
ChatId::new(chat_id)
.set_ephemeral_timer(ctx, EphemeralTimer::from_u32(timer))
.await
.log_err(ctx, "Failed to set ephemeral timer")
.is_ok() as libc::c_int
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_msg_info(
context: *mut dc_context_t,
@@ -1882,11 +1829,7 @@ pub unsafe extern "C" fn dc_send_locations_to_chat(
}
let ctx = &*context;
block_on(location::send_locations_to_chat(
&ctx,
ChatId::new(chat_id),
seconds as i64,
));
block_on({ location::send_locations_to_chat(&ctx, ChatId::new(chat_id), seconds as i64) });
}
#[no_mangle]
@@ -2044,7 +1987,7 @@ pub unsafe extern "C" fn dc_array_get_timestamp(
return 0;
}
(*array).get_timestamp(index).unwrap_or_default()
(*array).get_location(index).timestamp
}
#[no_mangle]
pub unsafe extern "C" fn dc_array_get_chat_id(
@@ -2091,7 +2034,7 @@ pub unsafe extern "C" fn dc_array_get_marker(
return std::ptr::null_mut(); // NULL explicitly defined as "no markers"
}
if let Some(s) = (*array).get_marker(index) {
if let Some(s) = &(*array).get_location(index).marker {
s.strdup()
} else {
std::ptr::null_mut()
@@ -2119,6 +2062,16 @@ pub unsafe extern "C" fn dc_array_search_id(
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_array_get_raw(array: *const dc_array_t) -> *const u32 {
if array.is_null() {
eprintln!("ignoring careless call to dc_array_get_raw()");
return ptr::null_mut();
}
(*array).as_ptr()
}
// Return the independent-state of the location at the given index.
// Independent locations do not belong to the track of the user.
// Returns 1 if location belongs to the track of the user,
@@ -2230,24 +2183,6 @@ pub unsafe extern "C" fn dc_chatlist_get_summary(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_chatlist_get_summary2(
context: *mut dc_context_t,
chat_id: u32,
msg_id: u32,
) -> *mut dc_lot_t {
if context.is_null() {
eprintln!("ignoring careless call to dc_chatlist_get_summary2()");
return ptr::null_mut();
}
let ctx = &*context;
block_on(async move {
let lot =
Chatlist::get_summary2(&ctx, ChatId::new(chat_id), MsgId::new(msg_id), None).await;
Box::into_raw(Box::new(lot))
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_chatlist_get_context(
chatlist: *mut dc_chatlist_t,
@@ -2445,7 +2380,7 @@ pub unsafe extern "C" fn dc_chat_get_remaining_mute_duration(chat: *mut dc_chat_
MuteDuration::NotMuted => 0,
MuteDuration::Forever => -1,
MuteDuration::Until(when) => when
.duration_since(SystemTime::now())
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0),
}
@@ -2716,26 +2651,6 @@ pub unsafe extern "C" fn dc_msg_get_showpadlock(msg: *mut dc_msg_t) -> libc::c_i
ffi_msg.message.get_showpadlock() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_ephemeral_timer(msg: *mut dc_msg_t) -> u32 {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_ephemeral_timer()");
return 0;
}
let ffi_msg = &*msg;
ffi_msg.message.get_ephemeral_timer()
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_ephemeral_timestamp(msg: *mut dc_msg_t) -> i64 {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_ephemeral_timer()");
return 0;
}
let ffi_msg = &*msg;
ffi_msg.message.get_ephemeral_timestamp()
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_summary(
msg: *mut dc_msg_t,
@@ -2860,31 +2775,6 @@ pub unsafe extern "C" fn dc_msg_is_setupmessage(msg: *mut dc_msg_t) -> libc::c_i
ffi_msg.message.is_setupmessage().into()
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_videochat_url(msg: *mut dc_msg_t) -> *mut libc::c_char {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_videochat_url()");
return "".strdup();
}
let ffi_msg = &*msg;
ffi_msg
.message
.get_videochat_url()
.unwrap_or_default()
.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_videochat_type(msg: *mut dc_msg_t) -> libc::c_int {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_videochat_type()");
return 0;
}
let ffi_msg = &*msg;
ffi_msg.message.get_videochat_type().unwrap_or_default() as i32
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_setupcodebegin(msg: *mut dc_msg_t) -> *mut libc::c_char {
if msg.is_null() {
@@ -2922,7 +2812,7 @@ pub unsafe extern "C" fn dc_msg_set_file(
let ffi_msg = &mut *msg;
ffi_msg.message.set_file(
to_string_lossy(file),
to_opt_string_lossy(filemime).as_deref(),
to_opt_string_lossy(filemime).as_ref().map(|x| x.as_str()),
)
}

View File

@@ -105,9 +105,8 @@ impl<T: AsRef<std::ffi::OsStr>> OsStrExt for T {
#[cfg(not(target_os = "windows"))]
fn to_c_string(&self) -> Result<CString, CStringError> {
use std::os::unix::ffi::OsStrExt;
CString::new(self.as_ref().as_bytes()).map_err(|err| {
let std::ffi::NulError { .. } = err;
CStringError::InteriorNullByte
CString::new(self.as_ref().as_bytes()).map_err(|err| match err {
std::ffi::NulError { .. } => CStringError::InteriorNullByte,
})
}
@@ -123,9 +122,8 @@ fn os_str_to_c_string_unicode(
os_str: &dyn AsRef<std::ffi::OsStr>,
) -> Result<CString, CStringError> {
match os_str.as_ref().to_str() {
Some(val) => CString::new(val.as_bytes()).map_err(|err| {
let std::ffi::NulError { .. } = err;
CStringError::InteriorNullByte
Some(val) => CString::new(val.as_bytes()).map_err(|err| match err {
std::ffi::NulError { .. } => CStringError::InteriorNullByte,
}),
None => Err(CStringError::NotUnicode),
}

View File

@@ -1,66 +0,0 @@
simplify/streamline mark-seen/delete/move/send-mdn job handling
---------------------------------------------------------------
Idea: Introduce a new "msgwork" sql table that looks very
much like the jobs table but has a primary key "msgid"
and no job id and no foreign-id anymore. This opens up
bulk-processing by looking at the whole table and combining
flag-setting to reduce imap-roundtrips and select-folder calls.
Concretely, these IMAP jobs:
DeleteMsgOnImap
MarkseenMsgOnImap
MoveMsg
Would be replaced by a few per-message columns in the new msgwork table:
- needs_mark_seen: (bool) message shall be marked as seen on imap
- needs_to_move: (bool) message should be moved to mvbox_folder
- deletion_time: (target_time or 0) message shall be deleted at specified time
- needs_send_mdn: (bool) MDN shall be sent
The various places that currently add the (replaced) jobs
would now add/modify the respective message record in the message-work table.
Looking at a single message-work entry conceptually looks like this::
if msg.server_uid==0:
return RetryLater # nothing can be done without server_uid
if msg.deletion_time > current_time:
imap.mark_delete(msg) # might trigger early exit with a RetryLater/Failed
clear(needs_deletion)
clear(mark_seen)
if needs_mark_seen:
imap.mark_seen(msg) # might trigger early exit with a RetryLater/Failed
clear(needs_mark_seen)
if needs_send_mdn:
schedule_smtp_send_mdn(msg)
clear(needs_send_mdn)
if any_flag_set():
retrylater
# remove msgwork entry from table
Notes/Questions:
- it's unclear how much we need per-message retry-time tracking/backoff
- drafting bulk processing algo is useful before
going for the implementation, i.e. including select_folder calls etc.
- maybe it's better to not have bools for the flags but
0 (no change)
1 (set the imap flag)
2 (clear the imap flag)
and design such that we can cover all imap flags.
- It might not be neccessary to keep needs_send_mdn state in this table
if this can be decided rather when we succeed with mark_seen/mark_delete.

View File

@@ -2,7 +2,7 @@ use std::str::FromStr;
use anyhow::{bail, ensure};
use async_std::path::Path;
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility};
use deltachat::chat::{self, Chat, ChatId, ChatVisibility};
use deltachat::chatlist::*;
use deltachat::constants::*;
use deltachat::contact::*;
@@ -183,7 +183,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
let temp2 = dc_timestamp_to_str(msg.get_timestamp());
let msgtext = msg.get_text();
println!(
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{}{} [{}]",
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{} [{}]",
prefix.as_ref(),
msg.get_id(),
if msg.get_showpadlock() { "🔒" } else { "" },
@@ -202,15 +202,6 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
"[FRESH]"
},
if msg.is_info() { "[INFO]" } else { "" },
if msg.get_viewtype() == Viewtype::VideochatInvitation {
format!(
"[VIDEOCHAT-INVITATION: {}, type={}]",
msg.get_videochat_url().unwrap_or_default(),
msg.get_videochat_type().unwrap_or_default()
)
} else {
"".to_string()
},
if msg.is_forwarded() {
"[FORWARDED]"
} else {
@@ -224,7 +215,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<(), Error> {
let mut lines_out = 0;
for &msg_id in msglist {
if msg_id == MsgId::new(DC_MSG_ID_DAYMARKER) {
if msg_id.is_daymarker() {
println!(
"--------------------------------------------------------------------------------"
);
@@ -284,9 +275,7 @@ async fn log_contactlist(context: &Context, contacts: &[u32]) {
"addr unset"
}
);
let peerstate = Peerstate::from_addr(context, &addr)
.await
.expect("peerstate error");
let peerstate = Peerstate::from_addr(context, &addr).await;
if peerstate.is_some() && contact_id != 1 as libc::c_uint {
line2 = format!(
", prefer-encrypt={}",
@@ -370,7 +359,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
send-garbage\n\
sendimage <file> [<text>]\n\
sendfile <file> [<text>]\n\
videochat\n\
draft [<text>]\n\
devicemsg <text>\n\
listmedia\n\
@@ -500,7 +488,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"listchats" | "listarchived" | "chats" => {
let listflags = if arg0 == "listarchived" { 0x01 } else { 0 };
let time_start = std::time::SystemTime::now();
let chatlist = Chatlist::try_load(
&context,
listflags,
@@ -508,9 +495,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
None,
)
.await?;
let time_needed = std::time::SystemTime::now()
.duration_since(time_start)
.unwrap_or_default();
let cnt = chatlist.len();
if cnt > 0 {
@@ -569,7 +553,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!("Location streaming enabled.");
}
println!("{} chats", cnt);
println!("{:?} to create this list", time_needed);
}
"chat" => {
if sel_chat.is_none() && arg1.is_empty() {
@@ -586,15 +569,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let sel_chat = sel_chat.as_ref().unwrap();
let msglist = chat::get_chat_msgs(&context, sel_chat.get_id(), 0x1, None).await;
let msglist: Vec<MsgId> = msglist
.into_iter()
.map(|x| match x {
ChatItem::Message { msg_id } => msg_id,
ChatItem::Marker1 => MsgId::new(DC_MSG_ID_MARKER1),
ChatItem::DayMarker { .. } => MsgId::new(DC_MSG_ID_DAYMARKER),
})
.collect();
let members = chat::get_chat_contacts(&context, sel_chat.id).await;
let subtitle = if sel_chat.is_device_talk() {
"device-talk".to_string()
@@ -820,10 +794,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
}
"videochat" => {
ensure!(sel_chat.is_some(), "No chat selected.");
chat::send_videochat_invitation(&context, sel_chat.as_ref().unwrap().get_id()).await?;
}
"listmsgs" => {
ensure!(!arg1.is_empty(), "Argument <query> missing.");

View File

@@ -158,7 +158,7 @@ const DB_COMMANDS: [&str; 9] = [
"housekeeping",
];
const CHAT_COMMANDS: [&str; 27] = [
const CHAT_COMMANDS: [&str; 26] = [
"listchats",
"listarchived",
"chat",
@@ -178,7 +178,6 @@ const CHAT_COMMANDS: [&str; 27] = [
"send",
"sendimage",
"sendfile",
"videochat",
"draft",
"listmedia",
"archive",

View File

@@ -1,22 +1,6 @@
1.44.0
------
- fix Chat.get_mute_duration()
1.40.1
0.900.0 (DRAFT)
---------------
- emit "ac_member_removed" event (with 'actor' being the removed contact)
for when a user leaves a group.
- fix create_contact(addr) when addr is the self-contact.
1.40.0
---------------
- uses latest 1.40+ Delta Chat core
- refactored internals to use plugin-approach
- introduced PerAccount and Global hooks that plugins can implement
@@ -26,7 +10,6 @@
- introduced two documented examples for an echo and a group-membership
tracking plugin.
0.800.0
-------

View File

@@ -7,14 +7,76 @@ which implements IMAP/SMTP/MIME/PGP e-mail standards and offers
a low-level Chat/Contact/Message API to user interfaces and bots.
Installing bindings from source (Updated: 20-Jan-2020)
=========================================================
Install Rust and Cargo first. Deltachat needs a specific nightly
version, the easiest is probably to first install Rust stable from
rustup and then use this to install the correct nightly version.
Bootstrap Rust and Cargo by using rustup::
curl https://sh.rustup.rs -sSf | sh
Then GIT clone the deltachat-core-rust repo and get the actual
rust- and cargo-toolchain needed by deltachat::
git clone https://github.com/deltachat/deltachat-core-rust
cd deltachat-core-rust
rustup show
To install the Delta Chat Python bindings make sure you have Python3 installed.
E.g. on Debian-based systems `apt install python3 python3-pip
python3-venv` should give you a usable python installation.
Ensure you are in the deltachat-core-rust/python directory, create the
virtual environment and activate it in your shell::
cd python
python3 -m venv venv # or: virtualenv venv
source venv/bin/activate
You should now be able to build the python bindings using the supplied script::
./install_python_bindings.py
The installation might take a while, depending on your machine.
The bindings will be installed in release mode but with debug symbols.
The release mode is currently necessary because some tests generate RSA keys
which is prohibitively slow in non-release mode.
After successful binding installation you can install a few more
Python packages before running the tests::
python -m pip install pytest pytest-timeout pytest-rerunfailures requests
pytest -v tests
running "live" tests with temporary accounts
---------------------------------------------
If you want to run "liveconfig" functional tests you can set
``DCC_NEW_TMP_EMAIL`` to:
- a particular https-url that you can ask for from the delta
chat devs. This is implemented on the server side via
the [mailadm](https://github.com/deltachat/mailadm) command line tool.
- or the path of a file that contains two lines, each describing
via "addr=... mail_pw=..." a test account login that will
be used for the live tests.
With ``DCC_NEW_TMP_EMAIL`` set pytest invocations will use real
e-mail accounts and run through all functional "liveconfig" tests.
Installing pre-built packages (Linux-only)
========================================================
If you have a Linux system you may try to install the ``deltachat`` binary "wheel" packages
without any "build-from-source" steps. Otherwise you need to `compile the Delta Chat bindings
yourself <sourceinstall>`_.
without any "build-from-source" steps.
We recommend to first `install virtualenv <https://virtualenv.pypa.io/en/stable/installation/>`_,
We suggest to `Install virtualenv <https://virtualenv.pypa.io/en/stable/installation/>`_,
then create a fresh Python virtual environment and activate it in your shell::
virtualenv venv # or: python -m venv
@@ -41,78 +103,6 @@ To verify it worked::
`in contact with us <https://delta.chat/en/contribute>`_.
Running tests
=============
After successful binding installation you can install a few more
Python packages before running the tests::
python -m pip install pytest pytest-xdist pytest-timeout pytest-rerunfailures requests
pytest -v tests
This will run all "offline" tests and skip all functional
end-to-end tests that require accounts on real e-mail servers.
.. _livetests:
running "live" tests with temporary accounts
---------------------------------------------
If you want to run live functional tests you can set ``DCC_NEW_TMP_EMAIL``::
export DCC_NEW_TMP_EMAIL=https://testrun.org/new_email?t=1h_4w4r8h7y9nmcdsy
With this, pytest runs create ephemeral e-mail accounts on the http://testrun.org server.
These accounts exists for one 1hour and then are removed completely.
One hour is enough to invoke pytest and run all offline and online tests:
pytest
# or if you have installed pytest-xdist for parallel test execution
pytest -n6
Each test run creates new accounts.
.. _sourceinstall:
Installing bindings from source (Updated: July 2020)
=========================================================
Install Rust and Cargo first.
The easiest is probably to use `rustup <https://rustup.rs/>`_.
Bootstrap Rust and Cargo by using rustup::
curl https://sh.rustup.rs -sSf | sh
Then clone the deltachat-core-rust repo::
git clone https://github.com/deltachat/deltachat-core-rust
cd deltachat-core-rust
To install the Delta Chat Python bindings make sure you have Python3 installed.
E.g. on Debian-based systems `apt install python3 python3-pip
python3-venv` should give you a usable python installation.
Ensure you are in the deltachat-core-rust/python directory, create the
virtual environment and activate it in your shell::
cd python
python3 -m venv venv # or: virtualenv venv
source venv/bin/activate
You should now be able to build the python bindings using the supplied script::
python install_python_bindings.py
The core compilation and bindings building might take a while,
depending on the speed of your machine.
The bindings will be installed in release mode but with debug symbols.
The release mode is currently necessary because some tests generate RSA keys
which is prohibitively slow in non-release mode.
Code examples
=============

View File

@@ -9,7 +9,8 @@
</ul>
<b>external links:</b>
<ul>
<li><a href="https://github.com/deltachat/deltachat-core-rust">github repository</a></li>
<li><a href="https://github.com/deltachat/deltachat-core">github repository</a></li>
<!-- <li><a href="https://lists.codespeak.net/postorius/lists/muacrypt.lists.codespeak.net">Mailing list</></li> <-->
<li><a href="https://pypi.python.org/pypi/deltachat">pypi: deltachat</a></li>
</ul>

View File

@@ -32,16 +32,16 @@ class GroupTrackingPlugin:
print("chat member: {}".format(member.addr))
@account_hookimpl
def ac_member_added(self, chat, contact, actor, message):
def ac_member_added(self, chat, contact, message):
print("ac_member_added {} to chat {} from {}".format(
contact.addr, chat.id, actor or message.get_sender_contact().addr))
contact.addr, chat.id, message.get_sender_contact().addr))
for member in chat.get_contacts():
print("chat member: {}".format(member.addr))
@account_hookimpl
def ac_member_removed(self, chat, contact, actor, message):
def ac_member_removed(self, chat, contact, message):
print("ac_member_removed {} from chat {} by {}".format(
contact.addr, chat.id, actor or message.get_sender_contact().addr))
contact.addr, chat.id, message.get_sender_contact().addr))
def main(argv=None):

View File

@@ -69,11 +69,11 @@ def test_group_tracking_plugin(acfactory, lp):
lp.sec("now looking at what the bot received")
botproc.fnmatch_lines("""
*ac_member_added {}*from*{}*
""".format(contact3.addr, ac1.get_config("addr")))
*ac_member_added {}*
""".format(contact3.addr))
lp.sec("contact successfully added, now removing")
ch.remove_contact(contact3)
botproc.fnmatch_lines("""
*ac_member_removed {}*from*{}*
""".format(contact3.addr, ac1.get_config("addr")))
*ac_member_removed {}*
""".format(contact3.addr))

View File

@@ -17,12 +17,8 @@ if __name__ == "__main__":
os.environ["DCC_RS_DEV"] = dn
cmd = ["cargo", "build", "-p", "deltachat_ffi"]
if target == 'release':
extra = " -C lto=on -C embed-bitcode=yes"
os.environ["RUSTFLAGS"] = os.environ.get("RUSTFLAGS", "") + extra
cmd.append("--release")
print("running:", " ".join(cmd))
subprocess.check_call(cmd)
subprocess.check_call("rm -rf build/ src/deltachat/*.so" , shell=True)

View File

@@ -77,8 +77,8 @@ def run_cmdline(argv=None, account_plugins=None):
ac.set_config("mvbox_move", "0")
ac.set_config("mvbox_watch", "0")
ac.set_config("sentbox_watch", "0")
configtracker = ac.configure()
configtracker.wait_finish()
ac.configure()
ac.wait_configure_finish()
# start IO threads and configure if neccessary
ac.start_io()

View File

@@ -1,64 +1,65 @@
import distutils.ccompiler
import distutils.log
import distutils.sysconfig
import os
import platform
import re
import shutil
import subprocess
import tempfile
import textwrap
import types
from os.path import abspath
from os.path import dirname as dn
import platform
import os
import cffi
import shutil
from os.path import dirname as dn
from os.path import abspath
def local_build_flags(projdir, target):
"""Construct build flags for building against a checkout.
:param projdir: The root directory of the deltachat-core-rust project.
:param target: The rust build target, `debug` or `release`.
"""
flags = types.SimpleNamespace()
if platform.system() == 'Darwin':
flags.libs = ['resolv', 'dl']
flags.extra_link_args = [
'-framework', 'CoreFoundation',
'-framework', 'CoreServices',
'-framework', 'Security',
]
elif platform.system() == 'Linux':
flags.libs = ['rt', 'dl', 'm']
flags.extra_link_args = []
def ffibuilder():
projdir = os.environ.get('DCC_RS_DEV')
if not projdir:
p = dn(dn(dn(dn(abspath(__file__)))))
projdir = os.environ["DCC_RS_DEV"] = p
target = os.environ.get('DCC_RS_TARGET', 'release')
if projdir:
if platform.system() == 'Darwin':
libs = ['resolv', 'dl']
extra_link_args = [
'-framework', 'CoreFoundation',
'-framework', 'CoreServices',
'-framework', 'Security',
]
elif platform.system() == 'Linux':
libs = ['rt', 'dl', 'm']
extra_link_args = []
else:
raise NotImplementedError("Compilation not supported yet on Windows, can you help?")
target_dir = os.environ.get("CARGO_TARGET_DIR")
if target_dir is None:
target_dir = os.path.join(projdir, 'target')
objs = [os.path.join(target_dir, target, 'libdeltachat.a')]
assert os.path.exists(objs[0]), objs
incs = [os.path.join(projdir, 'deltachat-ffi')]
else:
raise NotImplementedError("Compilation not supported yet on Windows, can you help?")
target_dir = os.environ.get("CARGO_TARGET_DIR")
if target_dir is None:
target_dir = os.path.join(projdir, 'target')
flags.objs = [os.path.join(target_dir, target, 'libdeltachat.a')]
assert os.path.exists(flags.objs[0]), flags.objs
flags.incs = [os.path.join(projdir, 'deltachat-ffi')]
return flags
def system_build_flags():
"""Construct build flags for building against an installed libdeltachat."""
flags = types.SimpleNamespace()
flags.libs = ['deltachat']
flags.objs = []
flags.incs = []
flags.extra_link_args = []
def extract_functions(flags):
"""Extract the function definitions from deltachat.h.
This creates a .h file with a single `#include <deltachat.h>` line
in it. It then runs the C preprocessor to create an output file
which contains all function definitions found in `deltachat.h`.
"""
libs = ['deltachat']
objs = []
incs = []
extra_link_args = []
builder = cffi.FFI()
builder.set_source(
'deltachat.capi',
"""
#include <deltachat.h>
int dc_event_has_string_data(int e)
{
return DC_EVENT_DATA2_IS_STRING(e);
}
""",
include_dirs=incs,
libraries=libs,
extra_objects=objs,
extra_link_args=extra_link_args,
)
builder.cdef("""
typedef int... time_t;
void free(void *ptr);
extern int dc_event_has_string_data(int);
""")
distutils.log.set_verbosity(distutils.log.INFO)
cc = distutils.ccompiler.new_compiler(force=True)
distutils.sysconfig.customize_compiler(cc)
@@ -70,133 +71,13 @@ def extract_functions(flags):
src_fp.write('#include <deltachat.h>')
cc.preprocess(source=src_name,
output_file=dst_name,
include_dirs=flags.incs,
include_dirs=incs,
macros=[('PY_CFFI', '1')])
with open(dst_name, "r") as dst_fp:
return dst_fp.read()
builder.cdef(dst_fp.read())
finally:
shutil.rmtree(tmpdir)
def find_header(flags):
"""Use the compiler to find the deltachat.h header location.
This uses a small utility in deltachat.h to find the location of
the header file location.
"""
distutils.log.set_verbosity(distutils.log.INFO)
cc = distutils.ccompiler.new_compiler(force=True)
distutils.sysconfig.customize_compiler(cc)
tmpdir = tempfile.mkdtemp()
try:
src_name = os.path.join(tmpdir, "where.c")
obj_name = os.path.join(tmpdir, "where.o")
dst_name = os.path.join(tmpdir, "where")
with open(src_name, "w") as src_fp:
src_fp.write(textwrap.dedent("""
#include <stdio.h>
#include <deltachat.h>
int main(void) {
printf("%s", _dc_header_file_location());
return 0;
}
"""))
cwd = os.getcwd()
try:
os.chdir(tmpdir)
cc.compile(sources=["where.c"],
include_dirs=flags.incs,
macros=[("PY_CFFI_INC", "1")])
finally:
os.chdir(cwd)
cc.link_executable(objects=[obj_name],
output_progname="where",
output_dir=tmpdir)
return subprocess.check_output(dst_name)
finally:
shutil.rmtree(tmpdir)
def extract_defines(flags):
"""Extract the required #DEFINEs from deltachat.h.
Since #DEFINEs are interpreted by the C preprocessor we can not
use the compiler to extract these and need to parse the header
file ourselves.
The defines are returned in a string that can be passed to CFFIs
cdef() method.
"""
header = find_header(flags)
defines_re = re.compile(r"""
\#define\s+ # The start of a define.
( # Begin capturing group which captures the define name.
(?: # A nested group which is not captured, this allows us
# to build the list of prefixes to extract without
# creation another capture group.
DC_EVENT
| DC_QR
| DC_MSG
| DC_LP
| DC_EMPTY
| DC_CERTCK
| DC_STATE
| DC_STR
| DC_CONTACT_ID
| DC_GCL
| DC_CHAT
| DC_PROVIDER
| DC_KEY_GEN
) # End of prefix matching
_[\w_]+ # Match the suffix, e.g. _RSA2048 in DC_KEY_GEN_RSA2048
) # Close the capturing group, this contains
# the entire name e.g. DC_MSG_TEXT.
\s+\S+ # Ensure there is whitespace followed by a value.
""", re.VERBOSE)
defines = []
with open(header) as fp:
for line in fp:
match = defines_re.match(line)
if match:
defines.append(match.group(1))
return '\n'.join('#define {} ...'.format(d) for d in defines)
def ffibuilder():
projdir = os.environ.get('DCC_RS_DEV')
if not projdir:
p = dn(dn(dn(dn(abspath(__file__)))))
projdir = os.environ["DCC_RS_DEV"] = p
target = os.environ.get('DCC_RS_TARGET', 'release')
if projdir:
flags = local_build_flags(projdir, target)
else:
flags = system_build_flags()
builder = cffi.FFI()
builder.set_source(
'deltachat.capi',
"""
#include <deltachat.h>
int dc_event_has_string_data(int e)
{
return DC_EVENT_DATA2_IS_STRING(e);
}
""",
include_dirs=flags.incs,
libraries=flags.libs,
extra_objects=flags.objs,
extra_link_args=flags.extra_link_args,
)
builder.cdef("""
typedef int... time_t;
void free(void *ptr);
extern int dc_event_has_string_data(int);
""")
function_defs = extract_functions(flags)
defines = extract_defines(flags)
builder.cdef(function_defs)
builder.cdef(defines)
return builder

View File

@@ -246,6 +246,7 @@ class Account(object):
addr = as_dc_charpointer(addr)
name = as_dc_charpointer(name)
contact_id = lib.dc_create_contact(self._dc_context, name, addr)
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL, contact_id
return Contact(self, contact_id)
def delete_contact(self, contact):
@@ -560,24 +561,29 @@ class Account(object):
:raises MissingCredentials: if `addr` and `mail_pw` values are not set.
:raises ConfigureFailed: if the account could not be configured.
:returns: None
:returns: None (account is configured and with io-scheduling running)
"""
if not self.is_configured():
raise ValueError("account not configured, cannot start io")
lib.dc_start_io(self._dc_context)
def configure(self):
""" Start configuration process and return a Configtracker instance
on which you can block with wait_finish() to get a True/False success
value for the configuration process.
"""
assert not self.is_configured()
assert not hasattr(self, "_configtracker")
if not self.get_config("addr") or not self.get_config("mail_pw"):
raise MissingCredentials("addr or mail_pwd not set in config")
configtracker = ConfigureTracker(self)
self.add_account_plugin(configtracker)
if hasattr(self, "_configtracker"):
self.remove_account_plugin(self._configtracker)
self._configtracker = ConfigureTracker(self)
self.add_account_plugin(self._configtracker)
lib.dc_configure(self._dc_context)
return configtracker
def wait_configure_finish(self):
try:
self._configtracker.wait_finish()
finally:
self.remove_account_plugin(self._configtracker)
del self._configtracker
def is_started(self):
return self._event_thread.is_alive() and bool(lib.dc_is_io_running(self._dc_context))
@@ -606,24 +612,12 @@ class Account(object):
self.stop_io()
self.log("remove dc_context references")
# if _dc_context is unref'ed the event thread should quickly
# receive the termination signal. However, some python code might
# still hold a reference and so we use a secondary signal
# to make sure the even thread terminates if it receives any new
# event, indepedently from waiting for the core to send NULL to
# get_next_event().
self._event_thread.mark_shutdown()
# the dc_context_unref triggers get_next_event to return ffi.NULL
# which in turns makes the event thread finish execution
self._dc_context = None
self.log("wait for event thread to finish")
try:
self._event_thread.wait(timeout=2)
except RuntimeError as e:
self.log("Waiting for event thread failed: {}".format(e))
if self._event_thread.is_alive():
self.log("WARN: event thread did not terminate yet, ignoring.")
self._event_thread.wait()
self._shutdown_event.set()

View File

@@ -137,23 +137,7 @@ class Chat(object):
:param duration:
:returns: Returns the number of seconds the chat is still muted for. (0 for not muted, -1 forever muted)
"""
return lib.dc_chat_get_remaining_mute_duration(self._dc_chat)
def get_ephemeral_timer(self):
""" get ephemeral timer.
:returns: ephemeral timer value in seconds
"""
return lib.dc_get_chat_ephemeral_timer(self.account._dc_context, self.id)
def set_ephemeral_timer(self, timer):
""" set ephemeral timer.
:param: timer value in seconds
:returns: None
"""
return lib.dc_set_chat_ephemeral_timer(self.account._dc_context, self.id, timer)
return bool(lib.dc_chat_get_remaining_mute_duration(self.id))
def get_type(self):
""" (deprecated) return type of this chat.

View File

@@ -1,7 +1,197 @@
from .capi import lib
import sys
import re
import os
from os.path import dirname, abspath
from os.path import join as joinpath
# the following const are generated from deltachat.h
# this works well when you in a git-checkout
# run "python deltachat/const.py" to regenerate events
# begin const generated
DC_GCL_ARCHIVED_ONLY = 0x01
DC_GCL_NO_SPECIALS = 0x02
DC_GCL_ADD_ALLDONE_HINT = 0x04
DC_GCL_FOR_FORWARDING = 0x08
DC_GCL_VERIFIED_ONLY = 0x01
DC_GCL_ADD_SELF = 0x02
DC_QR_ASK_VERIFYCONTACT = 200
DC_QR_ASK_VERIFYGROUP = 202
DC_QR_FPR_OK = 210
DC_QR_FPR_MISMATCH = 220
DC_QR_FPR_WITHOUT_ADDR = 230
DC_QR_ACCOUNT = 250
DC_QR_ADDR = 320
DC_QR_TEXT = 330
DC_QR_URL = 332
DC_QR_ERROR = 400
DC_CHAT_ID_DEADDROP = 1
DC_CHAT_ID_TRASH = 3
DC_CHAT_ID_MSGS_IN_CREATION = 4
DC_CHAT_ID_STARRED = 5
DC_CHAT_ID_ARCHIVED_LINK = 6
DC_CHAT_ID_ALLDONE_HINT = 7
DC_CHAT_ID_LAST_SPECIAL = 9
DC_CHAT_TYPE_UNDEFINED = 0
DC_CHAT_TYPE_SINGLE = 100
DC_CHAT_TYPE_GROUP = 120
DC_CHAT_TYPE_VERIFIED_GROUP = 130
DC_MSG_ID_MARKER1 = 1
DC_MSG_ID_DAYMARKER = 9
DC_MSG_ID_LAST_SPECIAL = 9
DC_STATE_UNDEFINED = 0
DC_STATE_IN_FRESH = 10
DC_STATE_IN_NOTICED = 13
DC_STATE_IN_SEEN = 16
DC_STATE_OUT_PREPARING = 18
DC_STATE_OUT_DRAFT = 19
DC_STATE_OUT_PENDING = 20
DC_STATE_OUT_FAILED = 24
DC_STATE_OUT_DELIVERED = 26
DC_STATE_OUT_MDN_RCVD = 28
DC_CONTACT_ID_SELF = 1
DC_CONTACT_ID_INFO = 2
DC_CONTACT_ID_DEVICE = 5
DC_CONTACT_ID_LAST_SPECIAL = 9
DC_MSG_TEXT = 10
DC_MSG_IMAGE = 20
DC_MSG_GIF = 21
DC_MSG_STICKER = 23
DC_MSG_AUDIO = 40
DC_MSG_VOICE = 41
DC_MSG_VIDEO = 50
DC_MSG_FILE = 60
DC_LP_AUTH_OAUTH2 = 0x2
DC_LP_AUTH_NORMAL = 0x4
DC_LP_IMAP_SOCKET_STARTTLS = 0x100
DC_LP_IMAP_SOCKET_SSL = 0x200
DC_LP_IMAP_SOCKET_PLAIN = 0x400
DC_LP_SMTP_SOCKET_STARTTLS = 0x10000
DC_LP_SMTP_SOCKET_SSL = 0x20000
DC_LP_SMTP_SOCKET_PLAIN = 0x40000
DC_CERTCK_AUTO = 0
DC_CERTCK_STRICT = 1
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3
DC_EMPTY_MVBOX = 0x01
DC_EMPTY_INBOX = 0x02
DC_EVENT_INFO = 100
DC_EVENT_SMTP_CONNECTED = 101
DC_EVENT_IMAP_CONNECTED = 102
DC_EVENT_SMTP_MESSAGE_SENT = 103
DC_EVENT_IMAP_MESSAGE_DELETED = 104
DC_EVENT_IMAP_MESSAGE_MOVED = 105
DC_EVENT_IMAP_FOLDER_EMPTIED = 106
DC_EVENT_NEW_BLOB_FILE = 150
DC_EVENT_DELETED_BLOB_FILE = 151
DC_EVENT_WARNING = 300
DC_EVENT_ERROR = 400
DC_EVENT_ERROR_NETWORK = 401
DC_EVENT_ERROR_SELF_NOT_IN_GROUP = 410
DC_EVENT_MSGS_CHANGED = 2000
DC_EVENT_INCOMING_MSG = 2005
DC_EVENT_MSG_DELIVERED = 2010
DC_EVENT_MSG_FAILED = 2012
DC_EVENT_MSG_READ = 2015
DC_EVENT_CHAT_MODIFIED = 2020
DC_EVENT_CONTACTS_CHANGED = 2030
DC_EVENT_LOCATION_CHANGED = 2035
DC_EVENT_CONFIGURE_PROGRESS = 2041
DC_EVENT_IMEX_PROGRESS = 2051
DC_EVENT_IMEX_FILE_WRITTEN = 2052
DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060
DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061
DC_EVENT_FILE_COPIED = 2055
DC_EVENT_IS_OFFLINE = 2081
DC_EVENT_GET_STRING = 2091
DC_STR_SELFNOTINGRP = 21
DC_KEY_GEN_DEFAULT = 0
DC_KEY_GEN_RSA2048 = 1
DC_KEY_GEN_ED25519 = 2
DC_PROVIDER_STATUS_OK = 1
DC_PROVIDER_STATUS_PREPARATION = 2
DC_PROVIDER_STATUS_BROKEN = 3
DC_CHAT_VISIBILITY_NORMAL = 0
DC_CHAT_VISIBILITY_ARCHIVED = 1
DC_CHAT_VISIBILITY_PINNED = 2
DC_STR_NOMESSAGES = 1
DC_STR_SELF = 2
DC_STR_DRAFT = 3
DC_STR_VOICEMESSAGE = 7
DC_STR_DEADDROP = 8
DC_STR_IMAGE = 9
DC_STR_VIDEO = 10
DC_STR_AUDIO = 11
DC_STR_FILE = 12
DC_STR_STATUSLINE = 13
DC_STR_NEWGROUPDRAFT = 14
DC_STR_MSGGRPNAME = 15
DC_STR_MSGGRPIMGCHANGED = 16
DC_STR_MSGADDMEMBER = 17
DC_STR_MSGDELMEMBER = 18
DC_STR_MSGGROUPLEFT = 19
DC_STR_GIF = 23
DC_STR_ENCRYPTEDMSG = 24
DC_STR_E2E_AVAILABLE = 25
DC_STR_ENCR_TRANSP = 27
DC_STR_ENCR_NONE = 28
DC_STR_CANTDECRYPT_MSG_BODY = 29
DC_STR_FINGERPRINTS = 30
DC_STR_READRCPT = 31
DC_STR_READRCPT_MAILBODY = 32
DC_STR_MSGGRPIMGDELETED = 33
DC_STR_E2E_PREFERRED = 34
DC_STR_CONTACT_VERIFIED = 35
DC_STR_CONTACT_NOT_VERIFIED = 36
DC_STR_CONTACT_SETUP_CHANGED = 37
DC_STR_ARCHIVEDCHATS = 40
DC_STR_STARREDMSGS = 41
DC_STR_AC_SETUP_MSG_SUBJECT = 42
DC_STR_AC_SETUP_MSG_BODY = 43
DC_STR_CANNOT_LOGIN = 60
DC_STR_SERVER_RESPONSE = 61
DC_STR_MSGACTIONBYUSER = 62
DC_STR_MSGACTIONBYME = 63
DC_STR_MSGLOCATIONENABLED = 64
DC_STR_MSGLOCATIONDISABLED = 65
DC_STR_LOCATION = 66
DC_STR_STICKER = 67
DC_STR_DEVICE_MESSAGES = 68
DC_STR_COUNT = 68
# end const generated
for name in dir(lib):
if name.startswith("DC_"):
globals()[name] = getattr(lib, name)
del name
def read_event_defines(f):
rex = re.compile(r'#define\s+((?:DC_EVENT|DC_QR|DC_MSG|DC_LP|DC_EMPTY|DC_CERTCK|DC_STATE|DC_STR|'
r'DC_CONTACT_ID|DC_GCL|DC_CHAT|DC_PROVIDER|DC_KEY_GEN)_\S+)\s+([x\d]+).*')
for line in f:
m = rex.match(line)
if m:
yield m.groups()
if __name__ == "__main__":
here = abspath(__file__).rstrip("oc")
here_dir = dirname(here)
if len(sys.argv) >= 2:
deltah = sys.argv[1]
else:
deltah = joinpath(dirname(dirname(dirname(here_dir))), "deltachat-ffi", "deltachat.h")
assert os.path.exists(deltah)
lines = []
skip_to_end = False
for orig_line in open(here):
if skip_to_end:
if not orig_line.startswith("# end const"):
continue
skip_to_end = False
lines.append(orig_line)
if orig_line.startswith("# begin const"):
with open(deltah) as f:
for name, item in read_event_defines(f):
lines.append("{} = {}\n".format(name, item))
skip_to_end = True
tmpname = here + ".tmp"
with open(tmpname, "w") as f:
f.write("".join(lines))
os.rename(tmpname, here)

View File

@@ -108,15 +108,6 @@ class DirectImap:
def get_all_messages(self):
assert not self._idling
# Flush unsolicited responses. IMAPClient has problems
# dealing with them: https://github.com/mjs/imapclient/issues/334
# When this NOOP was introduced, next FETCH returned empty
# result instead of a single message, even though IMAP server
# can only return more untagged responses than required, not
# less.
self.conn.noop()
return self.conn.fetch(ALL, [FLAGS])
def get_unread_messages(self):
@@ -168,13 +159,9 @@ class DirectImap:
log("---------", imapfolder, len(messages), "messages ---------")
# get message content without auto-marking it as seen
# fetching 'RFC822' would mark it as seen.
requested = [b'BODY.PEEK[]', FLAGS]
requested = [b'BODY.PEEK[HEADER]', FLAGS]
for uid, data in self.conn.fetch(messages, requested).items():
body_bytes = data[b'BODY[]']
if not body_bytes:
log("Message", uid, "has empty body")
continue
body_bytes = data[b'BODY[HEADER]']
flags = data[FLAGS]
path = pathlib.Path(str(dir)).joinpath("IMAP", self.logid, imapfolder)
path.mkdir(parents=True, exist_ok=True)
@@ -200,12 +187,9 @@ class DirectImap:
""" (blocking) wait for next idle message from server. """
assert self._idling
self.account.log("imap-direct: calling idle_check")
res = self.conn.idle_check(timeout=30)
if len(res) == 0:
raise TimeoutError
res = self.conn.idle_check()
if terminate:
self.idle_done()
self.account.log("imap-direct: idle_check returned {!r}".format(res))
return res
def idle_wait_for_seen(self):

View File

@@ -86,11 +86,11 @@ class FFIEventTracker:
if rex.match(ev.name):
return ev
def get_info_contains(self, regex):
rex = re.compile(regex)
def get_info_matching(self, regex):
rex = re.compile("(?:{}).*".format(regex))
while 1:
ev = self.get_matching("DC_EVENT_INFO")
if rex.search(ev.data2):
if rex.match(ev.data2):
return ev
def ensure_event_not_queued(self, event_name_regex):
@@ -139,7 +139,6 @@ class EventThread(threading.Thread):
self.account = account
super(EventThread, self).__init__(name="events")
self.setDaemon(True)
self._marked_for_shutdown = False
self.start()
@contextmanager
@@ -148,15 +147,12 @@ class EventThread(threading.Thread):
yield
self.account.log(message + " FINISHED")
def mark_shutdown(self):
self._marked_for_shutdown = True
def wait(self, timeout=None):
def wait(self):
if self == threading.current_thread():
# we are in the callback thread and thus cannot
# wait for the thread-loop to finish.
return
self.join(timeout=timeout)
self.join()
def run(self):
""" get and run events until shutdown. """
@@ -168,12 +164,10 @@ class EventThread(threading.Thread):
lib.dc_get_event_emitter(self.account._dc_context),
lib.dc_event_emitter_unref,
)
while not self._marked_for_shutdown:
while 1:
event = lib.dc_get_next_event(event_emitter)
if event == ffi.NULL:
break
if self._marked_for_shutdown:
break
evt = lib.dc_event_get_id(event)
data1 = lib.dc_event_get_data1_int(event)
# the following code relates to the deltachat/_build.py's helper

View File

@@ -16,7 +16,7 @@ class PerAccount:
""" per-Account-instance hook specifications.
All hooks are executed in a dedicated Event thread.
Hooks are generally not allowed to block/last long as this
Hooks are not allowed to block/last long as this
blocks overall event processing on the python side.
"""
@classmethod
@@ -31,6 +31,10 @@ class PerAccount:
ffi_event has "name", "data1", "data2" values as specified
with `DC_EVENT_* <https://c.delta.chat/group__DC__EVENT.html>`_.
DANGER: this hook is executed from the callback invoked by core.
Hook implementations need to be short running and can typically
not call back into core because this would easily cause recursion issues.
"""
@account_hookspec
@@ -51,37 +55,19 @@ class PerAccount:
@account_hookspec
def ac_message_delivered(self, message):
""" Called when an outgoing message has been delivered to SMTP.
:param message: Message that was just delivered.
"""
""" Called when an outgoing message has been delivered to SMTP. """
@account_hookspec
def ac_chat_modified(self, chat):
""" Chat was created or modified regarding membership, avatar, title.
:param chat: Chat which was modified.
"""
""" Chat was created or modified regarding membership, avatar, title. """
@account_hookspec
def ac_member_added(self, chat, contact, actor, message):
""" Called for each contact added to an accepted chat.
:param chat: Chat where contact was added.
:param contact: Contact that was added.
:param actor: Who added the contact (None if it was our self-addr)
:param message: The original system message that reports the addition.
"""
def ac_member_added(self, chat, contact, message):
""" Called for each contact added to an accepted chat. """
@account_hookspec
def ac_member_removed(self, chat, contact, actor, message):
""" Called for each contact removed from a chat.
:param chat: Chat where contact was removed.
:param contact: Contact that was removed.
:param actor: Who removed the contact (None if it was our self-addr)
:param message: The original system message that reports the removal.
"""
def ac_member_removed(self, chat, contact, message):
""" Called for each contact removed from a chat. """
class Global:

View File

@@ -1,7 +1,6 @@
""" The Message object. """
import os
import re
from . import props
from .cutil import from_dc_charpointer, as_dc_charpointer
from .capi import lib, ffi
@@ -155,26 +154,6 @@ class Message(object):
if ts:
return datetime.utcfromtimestamp(ts)
@props.with_doc
def ephemeral_timer(self):
"""Ephemeral timer in seconds
:returns: timer in seconds or None if there is no timer
"""
timer = lib.dc_msg_get_ephemeral_timer(self._dc_msg)
if timer:
return timer
@props.with_doc
def ephemeral_timestamp(self):
"""UTC time when the message will be deleted.
:returns: naive datetime.datetime() object or None if the timer is not started.
"""
ts = lib.dc_msg_get_ephemeral_timestamp(self._dc_msg)
if ts:
return datetime.utcfromtimestamp(ts)
def get_mime_headers(self):
""" return mime-header object for an incoming message.
@@ -357,43 +336,20 @@ def get_viewtype_code_from_name(view_type_name):
def map_system_message(msg):
if msg.is_system_message():
res = parse_system_add_remove(msg.text)
if not res:
return
action, affected, actor = res
affected = msg.account.get_contact_by_addr(affected)
if actor == "me":
actor = None
else:
actor = msg.account.get_contact_by_addr(actor)
d = dict(chat=msg.chat, contact=affected, actor=actor, message=msg)
return "ac_member_" + res[0], d
def extract_addr(text):
m = re.match(r'.*\((.+@.+)\)', text)
if m:
text = m.group(1)
text = text.rstrip(".")
return text.strip()
if res:
contact = msg.account.get_contact_by_addr(res[1])
if contact:
d = dict(chat=msg.chat, contact=contact, message=msg)
return "ac_member_" + res[0], d
def parse_system_add_remove(text):
""" return add/remove info from parsing the given system message text.
returns a (action, affected, actor) triple """
# Member Me (x@y) removed by a@b.
# Member x@y added by a@b
# Member With space (tmp1@x.org) removed by tmp2@x.org.
# Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
# Group left by some one (tmp1@x.org).
# Group left by tmp1@x.org.
# Member x@y removed by a@b
text = text.lower()
m = re.match(r'member (.+) (removed|added) by (.+)', text)
if m:
affected, action, actor = m.groups()
return action, extract_addr(affected), extract_addr(actor)
if text.startswith("group left by "):
addr = extract_addr(text[13:])
if addr:
return "removed", addr, addr
parts = text.split()
if parts[0] == "member":
if parts[2] in ("removed", "added"):
return parts[2], parts[1]
if parts[3] in ("removed", "added"):
return parts[3], parts[2].strip("()")

View File

@@ -32,10 +32,6 @@ def pytest_addoption(parser):
"--ignored", action="store_true",
help="Also run tests marked with the ignored marker",
)
parser.addoption(
"--strict-tls", action="store_true",
help="Never accept invalid TLS certificates for test accounts",
)
def pytest_configure(config):
@@ -156,7 +152,7 @@ class SessionLiveConfigFromURL:
assert index == len(self.configlist), index
res = requests.post(self.url)
if res.status_code != 200:
pytest.skip("creating newtmpuser failed with code {}: '{}'".format(res.status_code, res.text))
pytest.skip("creating newtmpuser failed {!r}".format(res))
d = res.json()
config = dict(addr=d["email"], mail_pw=d["password"])
self.configlist.append(config)
@@ -235,13 +231,10 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
def make_account(self, path, logid, quiet=False):
ac = Account(path, logging=self._logging)
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
ac._evtracker.set_timeout(30)
ac.addr = ac.get_self_contact().addr
ac.set_config("displayname", logid)
if not quiet:
logger = FFIEventLogger(ac)
logger.init_time = self.init_time
ac.add_account_plugin(logger)
ac.add_account_plugin(FFIEventLogger(ac))
self._accounts.append(ac)
return ac
@@ -251,7 +244,10 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
def get_unconfigured_account(self):
self.offline_count += 1
tmpdb = tmpdir.join("offlinedb%d" % self.offline_count)
return self.make_account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
ac._evtracker.init_time = self.init_time
ac._evtracker.set_timeout(2)
return ac
def _preconfigure_key(self, account, addr):
# Only set a key if we haven't used it yet for another account.
@@ -286,15 +282,16 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
if "e2ee_enabled" not in configdict:
configdict["e2ee_enabled"] = "1"
if pytestconfig.getoption("--strict-tls"):
# Enable strict certificate checks for online accounts
configdict["imap_certificate_checks"] = str(const.DC_CERTCK_STRICT)
configdict["smtp_certificate_checks"] = str(const.DC_CERTCK_STRICT)
# Enable strict certificate checks for online accounts
configdict["imap_certificate_checks"] = str(const.DC_CERTCK_STRICT)
configdict["smtp_certificate_checks"] = str(const.DC_CERTCK_STRICT)
tmpdb = tmpdir.join("livedb%d" % self.live_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count), quiet=quiet)
if pre_generated_key:
self._preconfigure_key(ac, configdict['addr'])
ac._evtracker.init_time = self.init_time
ac._evtracker.set_timeout(30)
return ac, dict(configdict)
def get_online_configuring_account(self, mvbox=False, sentbox=False, move=False,
@@ -306,25 +303,33 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
configdict["mvbox_move"] = str(int(move))
configdict["sentbox_watch"] = str(int(sentbox))
ac.update_config(configdict)
ac._configtracker = ac.configure()
ac.configure()
return ac
def get_one_online_account(self, pre_generated_key=True, mvbox=False, move=False):
ac1 = self.get_online_configuring_account(
pre_generated_key=pre_generated_key, mvbox=mvbox, move=move)
self.wait_configure_and_start_io()
ac1.wait_configure_finish()
ac1.start_io()
return ac1
def get_two_online_accounts(self, move=False, quiet=False):
ac1 = self.get_online_configuring_account(move=move, quiet=quiet)
ac2 = self.get_online_configuring_account(quiet=quiet)
self.wait_configure_and_start_io()
ac1.wait_configure_finish()
ac1.start_io()
ac2.wait_configure_finish()
ac2.start_io()
return ac1, ac2
def get_many_online_accounts(self, num, move=True):
accounts = [self.get_online_configuring_account(move=move, quiet=True)
for i in range(num)]
self.wait_configure_and_start_io()
for acc in accounts:
acc._configtracker.wait_finish()
acc.start_io()
print("{}: {} account was successfully setup".format(
acc.get_config("displayname"), acc.get_config("addr")))
for acc in accounts:
acc.add_account_plugin(FFIEventLogger(acc))
return accounts
@@ -335,6 +340,8 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
if pre_generated_key:
self._preconfigure_key(ac, account.get_config("addr"))
ac._evtracker.init_time = self.init_time
ac._evtracker.set_timeout(30)
ac.update_config(dict(
addr=account.get_config("addr"),
mail_pw=account.get_config("mail_pw"),
@@ -342,29 +349,14 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
mvbox_move=account.get_config("mvbox_move"),
sentbox_watch=account.get_config("sentbox_watch"),
))
ac._configtracker = ac.configure()
ac.configure()
return ac
def wait_configure_and_start_io(self):
for acc in self._accounts:
if hasattr(acc, "_configtracker"):
acc._configtracker.wait_finish()
del acc._configtracker
acc.set_config("bcc_self", "0")
if acc.is_configured() and not acc.is_started():
acc.start_io()
print("{}: {} account was successfully setup".format(
acc.get_config("displayname"), acc.get_config("addr")))
def run_bot_process(self, module, ffi=True):
fn = module.__file__
bot_ac, bot_cfg = self.get_online_config()
# Avoid starting ac so we don't interfere with the bot operating on
# the same database.
self._accounts.remove(bot_ac)
args = [
sys.executable,
"-u",

View File

@@ -64,7 +64,6 @@ class ConfigureTracker:
if success:
self._gm.hook.dc_account_extra_configure(account=self.account)
self._configure_events.put(success)
self.account.remove_account_plugin(self)
def wait_smtp_connected(self):
""" wait until smtp is configured. """

View File

@@ -11,21 +11,8 @@ from datetime import datetime, timedelta
@pytest.mark.parametrize("msgtext,res", [
("Member Me (tmp1@x.org) removed by tmp2@x.org.",
("removed", "tmp1@x.org", "tmp2@x.org")),
("Member With space (tmp1@x.org) removed by tmp2@x.org.",
("removed", "tmp1@x.org", "tmp2@x.org")),
("Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
("removed", "tmp1@x.org", "tmp2@x.org")),
("Member With space (tmp1@x.org) removed by me",
("removed", "tmp1@x.org", "me")),
("Group left by some one (tmp1@x.org).",
("removed", "tmp1@x.org", "tmp1@x.org")),
("Group left by tmp1@x.org.",
("removed", "tmp1@x.org", "tmp1@x.org")),
("Member tmp1@x.org added by tmp2@x.org.", ("added", "tmp1@x.org", "tmp2@x.org")),
("Member nothing bla bla", None),
("Another unknown system message", None),
("Member Me (tmp1@x.org) removed by tmp2@x.org.", ("removed", "tmp1@x.org")),
("Member tmp1@x.org added by tmp2@x.org.", ("added", "tmp1@x.org")),
])
def test_parse_system_add_remove(msgtext, res):
from deltachat.message import parse_system_add_remove
@@ -129,11 +116,6 @@ class TestOfflineContact:
assert not contact1.is_blocked()
assert not contact1.is_verified()
def test_create_self_contact(self, acfactory):
ac1 = acfactory.get_configured_offline_account()
contact1 = ac1.create_contact(ac1.get_config("addr"))
assert contact1.id == 1
def test_get_contacts_and_delete(self, acfactory):
ac1 = acfactory.get_configured_offline_account()
contact1 = ac1.create_contact("some1@example.org", name="some1")
@@ -276,23 +258,15 @@ class TestOfflineChat:
def test_mute(self, ac1):
chat = ac1.create_group_chat(name="title1")
assert not chat.is_muted()
assert chat.get_mute_duration() == 0
chat.mute()
assert chat.is_muted()
assert chat.get_mute_duration() == -1
chat.unmute()
assert not chat.is_muted()
chat.mute(50)
assert chat.is_muted()
assert chat.get_mute_duration() <= 50
with pytest.raises(ValueError):
chat.mute(-51)
# Regression test, this caused Rust panic previously
chat.mute(2**63 - 1)
assert chat.is_muted()
assert chat.get_mute_duration() == -1
def test_delete_and_send_fails(self, ac1, chat1):
chat1.delete()
ac1._evtracker.wait_next_messages_changed()
@@ -478,12 +452,12 @@ class TestOfflineChat:
class InPlugin:
@account_hookimpl
def ac_member_added(self, chat, contact, actor):
in_list.append(("added", chat, contact, actor))
def ac_member_added(self, chat, contact):
in_list.append(("added", chat, contact))
@account_hookimpl
def ac_member_removed(self, chat, contact, actor):
in_list.append(("removed", chat, contact, actor))
def ac_member_removed(self, chat, contact):
in_list.append(("removed", chat, contact))
ac1.add_account_plugin(InPlugin())
@@ -512,11 +486,10 @@ class TestOfflineChat:
assert len(in_list) == 10
chat_contacts = chat.get_contacts()
for in_cmd, in_chat, in_contact, in_actor in in_list:
for in_cmd, in_chat, in_contact in in_list:
assert in_cmd == "added"
assert in_chat == chat
assert in_contact in chat_contacts
assert in_actor is None
chat_contacts.remove(in_contact)
assert chat_contacts[0].id == 1 # self contact
@@ -544,7 +517,7 @@ def test_basic_imap_api(acfactory, tmpdir):
imap2 = ac2.direct_imap
imap2.idle_start()
ac2.direct_imap.idle_start()
chat12.send_text("hello")
ac2._evtracker.wait_next_incoming_message()
@@ -571,7 +544,10 @@ class TestOnlineAccount:
)
# rsa key gen can be slow especially on CI, adjust timeout
ac1._evtracker.set_timeout(120)
acfactory.wait_configure_and_start_io()
ac1.wait_configure_finish()
ac1.start_io()
ac2.wait_configure_finish()
ac2.start_io()
chat = acfactory.get_accepted_chat(ac1, ac2)
lp.sec("ac1: send unencrypted message to ac2")
@@ -603,7 +579,7 @@ class TestOnlineAccount:
ac1._configtracker.wait_progress()
ac1.stop_ongoing()
try:
ac1._configtracker.wait_finish()
ac1.wait_configure_finish()
except Exception:
pass
@@ -626,7 +602,12 @@ class TestOnlineAccount:
# are copied to it via BCC.
ac1_clone = acfactory.clone_online_account(ac1)
acfactory.wait_configure_and_start_io()
ac1.wait_configure_finish()
ac1.start_io()
ac2.wait_configure_finish()
ac2.start_io()
ac1_clone.wait_configure_finish()
ac1_clone.start_io()
chat = acfactory.get_accepted_chat(ac1, ac2)
@@ -731,8 +712,13 @@ class TestOnlineAccount:
lp.sec("ac2: start without mvbox/sentbox threads")
ac2 = acfactory.get_online_configuring_account()
lp.sec("ac2 and ac1: waiting for configuration")
acfactory.wait_configure_and_start_io()
lp.sec("ac2: waiting for configuration")
ac2.wait_configure_finish()
ac2.start_io()
lp.sec("ac1: waiting for configuration")
ac1.wait_configure_finish()
ac1.start_io()
lp.sec("ac1: send message and wait for ac2 to receive it")
acfactory.get_accepted_chat(ac1, ac2).send_text("message1")
@@ -741,7 +727,10 @@ class TestOnlineAccount:
def test_move_works(self, acfactory):
ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.get_online_configuring_account(mvbox=True, move=True)
acfactory.wait_configure_and_start_io()
ac2.wait_configure_finish()
ac2.start_io()
ac1.wait_configure_finish()
ac1.start_io()
chat = acfactory.get_accepted_chat(ac1, ac2)
chat.send_text("message1")
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
@@ -750,9 +739,12 @@ class TestOnlineAccount:
def test_move_works_on_self_sent(self, acfactory):
ac1 = acfactory.get_online_configuring_account(mvbox=True, move=True)
ac2 = acfactory.get_online_configuring_account()
acfactory.wait_configure_and_start_io()
ac1.set_config("bcc_self", "1")
ac2 = acfactory.get_online_configuring_account()
ac2.wait_configure_finish()
ac2.start_io()
ac1.wait_configure_finish()
ac1.start_io()
chat = acfactory.get_accepted_chat(ac1, ac2)
chat.send_text("message1")
@@ -877,7 +869,6 @@ class TestOnlineAccount:
lp.sec("mark messages as seen on ac2, wait for changes on ac1")
ac2.direct_imap.idle_start()
ac1.direct_imap.idle_start()
ac2.mark_seen_messages([msg2, msg4])
ac2.direct_imap.idle_check(terminate=True)
lp.step("1")
@@ -886,8 +877,6 @@ class TestOnlineAccount:
assert ev.data1 > const.DC_CHAT_ID_LAST_SPECIAL
assert ev.data2 > const.DC_MSG_ID_LAST_SPECIAL
lp.step("2")
ac1.direct_imap.idle_wait_for_seen() # Check that ac1 marks the read receipt as read
assert msg1.is_out_mdn_received()
assert msg3.is_out_mdn_received()
@@ -1195,7 +1184,10 @@ class TestOnlineAccount:
# as of Jul2019
ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.clone_online_account(ac1)
acfactory.wait_configure_and_start_io()
ac2.wait_configure_finish()
ac2.start_io()
ac1.wait_configure_finish()
ac1.start_io()
lp.sec("trigger ac setup message and return setupcode")
assert ac1.get_info()["fingerprint"] != ac2.get_info()["fingerprint"]
@@ -1218,7 +1210,10 @@ class TestOnlineAccount:
ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.clone_online_account(ac1)
ac2._evtracker.set_timeout(30)
acfactory.wait_configure_and_start_io()
ac2.wait_configure_finish()
ac2.start_io()
ac1.wait_configure_finish()
ac1.start_io()
lp.sec("trigger ac setup message but ignore")
assert ac1.get_info()["fingerprint"] != ac2.get_info()["fingerprint"]
@@ -1518,7 +1513,7 @@ class TestOnlineAccount:
lp.sec("ac3 reinstalls DC and generates a new key")
ac3.stop_io()
ac4 = acfactory.clone_online_account(ac3, pre_generated_key=False)
ac4._configtracker.wait_finish()
ac4.wait_configure_finish()
# Create contacts to make sure incoming messages are not treated as contact requests
chat41 = ac4.create_chat(ac1)
chat42 = ac4.create_chat(ac2)
@@ -1569,127 +1564,6 @@ class TestOnlineAccount:
assert msg.is_encrypted(), "Message is not encrypted"
assert msg.chat == ac2.create_chat(ac4)
def test_immediate_autodelete(self, acfactory, lp):
ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.get_online_configuring_account(mvbox=False, move=False, sentbox=False)
# "1" means delete immediately, while "0" means do not delete
ac2.set_config("delete_server_after", "1")
acfactory.wait_configure_and_start_io()
imap2 = ac2.direct_imap
imap2.idle_start()
lp.sec("ac1: create chat with ac2")
chat1 = ac1.create_chat(ac2)
ac2.create_chat(ac1)
sent_msg = chat1.send_text("hello")
imap2.idle_check(terminate=False)
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "hello"
imap2.idle_check(terminate=True)
ac2._evtracker.get_info_contains("close/expunge succeeded")
assert len(imap2.get_all_messages()) == 0
# Mark deleted message as seen and check that read receipt arrives
msg.mark_seen()
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_READ")
assert ev.data1 == chat1.id
assert ev.data2 == sent_msg.id
def test_ephemeral_timer(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("ac1: create chat with ac2")
chat1 = ac1.create_chat(ac2)
chat2 = ac2.create_chat(ac1)
lp.sec("ac1: set ephemeral timer to 60")
chat1.set_ephemeral_timer(60)
lp.sec("ac1: check that ephemeral timer is set for chat")
assert chat1.get_ephemeral_timer() == 60
chat1_summary = chat1.get_summary()
assert chat1_summary["ephemeral_timer"] == {'Enabled': {'duration': 60}}
lp.sec("ac2: receive system message about ephemeral timer modification")
ac2._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
system_message1 = ac2._evtracker.wait_next_incoming_message()
assert chat2.get_ephemeral_timer() == 60
assert system_message1.is_system_message()
# Disabled until markers are implemented
# assert "Ephemeral timer: 60\n" in system_message1.get_message_info()
lp.sec("ac2: send message to ac1")
sent_message = chat2.send_text("message")
assert sent_message.ephemeral_timer == 60
assert "Ephemeral timer: 60\n" in sent_message.get_message_info()
# Timer is started immediately for sent messages
assert sent_message.ephemeral_timestamp is not None
assert "Expires: " in sent_message.get_message_info()
lp.sec("ac1: waiting for message from ac2")
text_message = ac1._evtracker.wait_next_incoming_message()
assert text_message.text == "message"
assert text_message.ephemeral_timer == 60
assert "Ephemeral timer: 60\n" in text_message.get_message_info()
# Timer should not start until message is displayed
assert text_message.ephemeral_timestamp is None
assert "Expires: " not in text_message.get_message_info()
text_message.mark_seen()
text_message = ac1.get_message_by_id(text_message.id)
assert text_message.ephemeral_timestamp is not None
assert "Expires: " in text_message.get_message_info()
lp.sec("ac2: set ephemeral timer to 0")
chat2.set_ephemeral_timer(0)
ac2._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
lp.sec("ac1: receive system message about ephemeral timer modification")
ac1._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
system_message2 = ac1._evtracker.wait_next_incoming_message()
assert system_message2.ephemeral_timer is None
assert "Ephemeral timer: " not in system_message2.get_message_info()
assert chat1.get_ephemeral_timer() == 0
def test_delete_multiple_messages(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
chat12 = acfactory.get_accepted_chat(ac1, ac2)
lp.sec("ac1: sending seven messages")
texts = ["first", "second", "third", "fourth", "fifth", "sixth", "seventh"]
for text in texts:
chat12.send_text(text)
lp.sec("ac2: waiting for all messages on the other side")
to_delete = []
for text in texts:
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text in texts
if text != "third":
to_delete.append(msg)
lp.sec("ac2: deleting all messages except third")
assert len(to_delete) == len(texts) - 1
ac2.delete_messages(to_delete)
for msg in to_delete:
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
ac2._evtracker.get_info_contains("close/expunge succeeded")
lp.sec("imap2: test that only one message is left")
imap2 = ac2.direct_imap
assert len(imap2.get_all_messages()) == 1
class TestGroupStressTests:
def test_group_many_members_add_leave_remove(self, acfactory, lp):
@@ -1812,26 +1686,26 @@ class TestOnlineConfigureFails:
def test_invalid_password(self, acfactory):
ac1, configdict = acfactory.get_online_config()
ac1.update_config(dict(addr=configdict["addr"], mail_pw="123"))
configtracker = ac1.configure()
configtracker.wait_progress(500)
configtracker.wait_progress(0)
ac1.configure()
ac1._configtracker.wait_progress(500)
ac1._configtracker.wait_progress(0)
ev = ac1._evtracker.get_matching("DC_EVENT_ERROR_NETWORK")
assert "cannot login" in ev.data2.lower()
def test_invalid_user(self, acfactory):
ac1, configdict = acfactory.get_online_config()
ac1.update_config(dict(addr="x" + configdict["addr"], mail_pw=configdict["mail_pw"]))
configtracker = ac1.configure()
configtracker.wait_progress(500)
configtracker.wait_progress(0)
ac1.configure()
ac1._configtracker.wait_progress(500)
ac1._configtracker.wait_progress(0)
ev = ac1._evtracker.get_matching("DC_EVENT_ERROR_NETWORK")
assert "cannot login" in ev.data2.lower()
def test_invalid_domain(self, acfactory):
ac1, configdict = acfactory.get_online_config()
ac1.update_config((dict(addr=configdict["addr"] + "x", mail_pw=configdict["mail_pw"])))
configtracker = ac1.configure()
configtracker.wait_progress(500)
configtracker.wait_progress(0)
ac1.configure()
ac1._configtracker.wait_progress(500)
ac1._configtracker.wait_progress(0)
ev = ac1._evtracker.get_matching("DC_EVENT_ERROR_NETWORK")
assert "could not connect" in ev.data2.lower()

View File

@@ -7,7 +7,7 @@ envlist =
[testenv]
commands =
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored --strict-tls {posargs: tests examples}
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored {posargs: tests examples}
python tests/package_wheels.py {toxworkdir}/wheelhouse
passenv =
TRAVIS

View File

@@ -1 +1 @@
1.45.0
1.43.1

View File

@@ -5,27 +5,18 @@ import sys
import re
import pathlib
import subprocess
from argparse import ArgumentParser
rex = re.compile(r'version = "(\S+)"')
def regex_matches(relpath, regex=rex):
def read_toml_version(relpath):
p = pathlib.Path(relpath)
assert p.exists()
for line in open(str(p)):
m = regex.match(line)
m = rex.match(line)
if m is not None:
return m
def read_toml_version(relpath):
res = regex_matches(relpath, rex)
if res is not None:
return res.group(1)
return m.group(1)
raise ValueError("no version found in {}".format(relpath))
def replace_toml_version(relpath, newversion):
p = pathlib.Path(relpath)
assert p.exists()
@@ -34,28 +25,18 @@ def replace_toml_version(relpath, newversion):
for line in open(str(p)):
m = rex.match(line)
if m is not None:
print("{}: set version={}".format(relpath, newversion))
f.write('version = "{}"\n'.format(newversion))
else:
f.write(line)
os.rename(tmp_path, str(p))
if __name__ == "__main__":
def main():
parser = ArgumentParser(prog="set_core_version")
parser.add_argument("newversion")
toml_list = ["Cargo.toml", "deltachat-ffi/Cargo.toml"]
try:
opts = parser.parse_args()
except SystemExit:
print()
for x in toml_list:
if len(sys.argv) < 2:
for x in ("Cargo.toml", "deltachat-ffi/Cargo.toml"):
print("{}: {}".format(x, read_toml_version(x)))
print()
raise SystemExit("need argument: new version, example: 1.25.0")
newversion = opts.newversion
newversion = sys.argv[1]
if newversion.count(".") < 2:
raise SystemExit("need at least two dots in version")
@@ -74,10 +55,7 @@ def main():
replace_toml_version("Cargo.toml", newversion)
replace_toml_version("deltachat-ffi/Cargo.toml", newversion)
print("running cargo check")
subprocess.call(["cargo", "check"])
print("adding changes to git index")
subprocess.call(["git", "add", "-u"])
# subprocess.call(["cargo", "update", "-p", "deltachat"])
@@ -85,8 +63,3 @@ def main():
print("")
print(" git tag {}".format(newversion))
print("")
if __name__ == "__main__":
main()

View File

@@ -14,7 +14,6 @@ use thiserror::Error;
use crate::config::Config;
use crate::constants::*;
use crate::context::Context;
use crate::error::Error;
use crate::events::Event;
use crate::message;
@@ -61,7 +60,7 @@ impl<'a> BlobObject<'a> {
.map_err(|err| BlobError::WriteFailure {
blobdir: blobdir.to_path_buf(),
blobname: name.clone(),
cause: err.into(),
cause: err,
})?;
let blob = BlobObject {
blobdir,
@@ -168,9 +167,6 @@ impl<'a> BlobObject<'a> {
/// subdirectory is used and [BlobObject::sanitise_name] does not
/// modify the filename.
///
/// Paths into the blob directory may be either defined by an absolute path
/// or by the relative prefix `$BLOBDIR`.
///
/// # Errors
///
/// This merely delegates to the [BlobObject::create_and_copy] and
@@ -182,11 +178,6 @@ impl<'a> BlobObject<'a> {
) -> std::result::Result<BlobObject<'_>, BlobError> {
if src.as_ref().starts_with(context.get_blobdir()) {
BlobObject::from_path(context, src)
} else if src.as_ref().starts_with("$BLOBDIR/") {
BlobObject::from_name(
context,
src.as_ref().to_str().unwrap_or_default().to_string(),
)
} else {
BlobObject::create_and_copy(context, src).await
}
@@ -384,7 +375,7 @@ impl<'a> BlobObject<'a> {
img.save(&blob_abs).map_err(|err| BlobError::WriteFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
cause: err.into(),
cause: err,
})?;
Ok(())
@@ -417,40 +408,16 @@ impl<'a> BlobObject<'a> {
return Ok(());
}
let mut img = img.thumbnail(img_wh, img_wh);
match self.get_exif_orientation(context) {
Ok(90) => img = img.rotate90(),
Ok(180) => img = img.rotate180(),
Ok(270) => img = img.rotate270(),
_ => {}
}
let img = img.thumbnail(img_wh, img_wh);
img.save(&blob_abs).map_err(|err| BlobError::WriteFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
cause: err.into(),
cause: err,
})?;
Ok(())
}
pub fn get_exif_orientation(&self, context: &Context) -> Result<i32, Error> {
let file = std::fs::File::open(self.to_abs_path())?;
let mut bufreader = std::io::BufReader::new(&file);
let exifreader = exif::Reader::new();
let exif = exifreader.read_from_container(&mut bufreader)?;
if let Some(orientation) = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY) {
// possible orientation values are described at http://sylvana.net/jpegcrop/exif_orientation.html
// we only use rotation, in practise, flipping is not used.
match orientation.value.get_uint(0) {
Some(3) => return Ok(180),
Some(6) => return Ok(90),
Some(8) => return Ok(270),
other => warn!(context, "exif orientation value ignored: {:?}", other),
}
}
Ok(0)
}
}
impl<'a> fmt::Display for BlobObject<'a> {
@@ -474,7 +441,7 @@ pub enum BlobError {
blobdir: PathBuf,
blobname: String,
#[source]
cause: anyhow::Error,
cause: std::io::Error,
},
#[error("Failed to copy data from {} to blob {blobname} in {}", .src.display(), .blobdir.display())]
CopyFailure {
@@ -505,7 +472,7 @@ mod tests {
#[async_std::test]
async fn test_create() {
let t = TestContext::new().await;
let t = dummy_context().await;
let blob = BlobObject::create(&t.ctx, "foo", b"hello").await.unwrap();
let fname = t.ctx.get_blobdir().join("foo");
let data = fs::read(fname).await.unwrap();
@@ -516,7 +483,7 @@ mod tests {
#[async_std::test]
async fn test_lowercase_ext() {
let t = TestContext::new().await;
let t = dummy_context().await;
let blob = BlobObject::create(&t.ctx, "foo.TXT", b"hello")
.await
.unwrap();
@@ -525,7 +492,7 @@ mod tests {
#[async_std::test]
async fn test_as_file_name() {
let t = TestContext::new().await;
let t = dummy_context().await;
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
.await
.unwrap();
@@ -534,7 +501,7 @@ mod tests {
#[async_std::test]
async fn test_as_rel_path() {
let t = TestContext::new().await;
let t = dummy_context().await;
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
.await
.unwrap();
@@ -543,7 +510,7 @@ mod tests {
#[async_std::test]
async fn test_suffix() {
let t = TestContext::new().await;
let t = dummy_context().await;
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
.await
.unwrap();
@@ -554,7 +521,7 @@ mod tests {
#[async_std::test]
async fn test_create_dup() {
let t = TestContext::new().await;
let t = dummy_context().await;
BlobObject::create(&t.ctx, "foo.txt", b"hello")
.await
.unwrap();
@@ -578,7 +545,7 @@ mod tests {
#[async_std::test]
async fn test_double_ext_preserved() {
let t = TestContext::new().await;
let t = dummy_context().await;
BlobObject::create(&t.ctx, "foo.tar.gz", b"hello")
.await
.unwrap();
@@ -603,7 +570,7 @@ mod tests {
#[async_std::test]
async fn test_create_long_names() {
let t = TestContext::new().await;
let t = dummy_context().await;
let s = "1".repeat(150);
let blob = BlobObject::create(&t.ctx, &s, b"data").await.unwrap();
let blobname = blob.as_name().split('/').last().unwrap();
@@ -612,7 +579,7 @@ mod tests {
#[async_std::test]
async fn test_create_and_copy() {
let t = TestContext::new().await;
let t = dummy_context().await;
let src = t.dir.path().join("src");
fs::write(&src, b"boo").await.unwrap();
let blob = BlobObject::create_and_copy(&t.ctx, &src).await.unwrap();
@@ -628,7 +595,7 @@ mod tests {
#[async_std::test]
async fn test_create_from_path() {
let t = TestContext::new().await;
let t = dummy_context().await;
let src_ext = t.dir.path().join("external");
fs::write(&src_ext, b"boo").await.unwrap();
@@ -646,7 +613,7 @@ mod tests {
}
#[async_std::test]
async fn test_create_from_name_long() {
let t = TestContext::new().await;
let t = dummy_context().await;
let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
fs::write(&src_ext, b"boo").await.unwrap();
let blob = BlobObject::new_from_path(&t.ctx, &src_ext).await.unwrap();
@@ -668,40 +635,8 @@ mod tests {
#[test]
fn test_sanitise_name() {
let (stem, ext) =
let (_, ext) =
BlobObject::sanitise_name("Я ЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯ.txt");
assert_eq!(ext, ".txt");
assert!(!stem.is_empty());
// the extensions are kept together as between stem and extension a number may be added -
// and `foo.tar.gz` should become `foo-1234.tar.gz` and not `foo.tar-1234.gz`
let (stem, ext) = BlobObject::sanitise_name("wot.tar.gz");
assert_eq!(stem, "wot");
assert_eq!(ext, ".tar.gz");
let (stem, ext) = BlobObject::sanitise_name(".foo.bar");
assert_eq!(stem, "");
assert_eq!(ext, ".foo.bar");
let (stem, ext) = BlobObject::sanitise_name("foo?.bar");
assert!(stem.contains("foo"));
assert!(!stem.contains("?"));
assert_eq!(ext, ".bar");
let (stem, ext) = BlobObject::sanitise_name("no-extension");
assert_eq!(stem, "no-extension");
assert_eq!(ext, "");
let (stem, ext) = BlobObject::sanitise_name("path/ignored\\this: is* forbidden?.c");
assert_eq!(ext, ".c");
assert!(!stem.contains("path"));
assert!(!stem.contains("ignored"));
assert!(stem.contains("this"));
assert!(stem.contains("forbidden"));
assert!(!stem.contains("/"));
assert!(!stem.contains("\\"));
assert!(!stem.contains(":"));
assert!(!stem.contains("*"));
assert!(!stem.contains("?"));
}
}

View File

@@ -15,7 +15,6 @@ use crate::constants::*;
use crate::contact::*;
use crate::context::Context;
use crate::dc_tools::*;
use crate::ephemeral::{delete_expired_messages, schedule_ephemeral_task, Timer as EphemeralTimer};
use crate::error::{bail, ensure, format_err, Error};
use crate::events::Event;
use crate::job::{self, Action};
@@ -25,25 +24,6 @@ use crate::param::*;
use crate::sql;
use crate::stock::StockMessage;
/// An chat item, such as a message or a marker.
#[derive(Debug, Copy, Clone)]
pub enum ChatItem {
Message {
msg_id: MsgId,
},
/// A marker without inherent meaning. It is inserted before user
/// supplied MsgId.
Marker1,
/// Day marker, separating messages that correspond to different
/// days according to local time.
DayMarker {
/// Marker timestamp, for day markers
timestamp: i64,
},
}
/// Chat ID, including reserved IDs.
///
/// Some chat IDs are reserved to identify special chat types. This
@@ -444,7 +424,7 @@ impl ChatId {
async fn get_parent_mime_headers(self, context: &Context) -> Option<(String, String, String)> {
let collect =
|row: &rusqlite::Row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?));
let (rfc724_mid, mime_in_reply_to, mime_references, error): (
let (packed, rfc724_mid, mime_in_reply_to, mime_references): (
String,
String,
String,
@@ -452,14 +432,15 @@ impl ChatId {
) = self
.parent_query(
context,
"rfc724_mid, mime_in_reply_to, mime_references, error",
"param, rfc724_mid, mime_in_reply_to, mime_references",
collect,
)
.await
.ok()
.flatten()?;
if !error.is_empty() {
let param = packed.parse::<Params>().ok()?;
if param.exists(Param::Error) {
// Do not reply to error messages.
//
// An error message could be a group chat message that we failed to decrypt and
@@ -473,13 +454,12 @@ impl ChatId {
}
async fn parent_is_encrypted(self, context: &Context) -> Result<bool, Error> {
let collect = |row: &rusqlite::Row| Ok((row.get(0)?, row.get(1)?));
let res: Option<(String, String)> =
self.parent_query(context, "param, error", collect).await?;
let collect = |row: &rusqlite::Row| Ok(row.get(0)?);
let packed: Option<String> = self.parent_query(context, "param", collect).await?;
if let Some((ref packed, ref error)) = res {
if let Some(ref packed) = packed {
let param = packed.parse::<Params>()?;
Ok(error.is_empty() && param.exists(Param::GuaranteeE2ee))
Ok(!param.exists(Param::Error) && param.exists(Param::GuaranteeE2ee))
} else {
// No messages
Ok(false)
@@ -730,7 +710,6 @@ impl Chat {
.unwrap_or_else(std::path::PathBuf::new),
draft,
is_muted: self.is_muted(),
ephemeral_timer: self.id.get_ephemeral_timer(context).await?,
})
}
@@ -907,10 +886,11 @@ impl Chat {
// the whole list of messages referenced may be huge;
// only use the oldest and and the parent message
let parent_references = parent_references
.find(' ')
.and_then(|n| parent_references.get(..n))
.unwrap_or(&parent_references);
let parent_references = if let Some(n) = parent_references.find(' ') {
&parent_references[0..n]
} else {
&parent_references
};
if !parent_references.is_empty() && !parent_rfc724_mid.is_empty() {
// angle brackets are added by the mimefactory later
@@ -958,20 +938,10 @@ impl Chat {
.await?;
}
let ephemeral_timer = if msg.param.get_cmd() == SystemMessage::EphemeralTimerChanged {
EphemeralTimer::Disabled
} else {
self.id.get_ephemeral_timer(context).await?
};
let ephemeral_timestamp = match ephemeral_timer {
EphemeralTimer::Disabled => 0,
EphemeralTimer::Enabled { duration } => timestamp + i64::from(duration),
};
// add message to the database
if context.sql.execute(
"INSERT INTO msgs (rfc724_mid, chat_id, from_id, to_id, timestamp, type, state, txt, param, hidden, mime_in_reply_to, mime_references, location_id, ephemeral_timer, ephemeral_timestamp) VALUES (?,?,?,?,?, ?,?,?,?,?, ?,?,?,?,?);",
"INSERT INTO msgs (rfc724_mid, chat_id, from_id, to_id, timestamp, type, state, txt, param, hidden, mime_in_reply_to, mime_references, location_id) VALUES (?,?,?,?,?, ?,?,?,?,?, ?,?,?);",
paramsv![
new_rfc724_mid,
self.id,
@@ -986,8 +956,6 @@ impl Chat {
new_in_reply_to,
new_references,
location_id as i32,
ephemeral_timer,
ephemeral_timestamp
]
).await.is_ok() {
msg_id = context.sql.get_rowid(
@@ -1006,7 +974,6 @@ impl Chat {
} else {
error!(context, "Cannot send message, not configured.",);
}
schedule_ephemeral_task(context).await;
Ok(MsgId::new(msg_id))
}
@@ -1034,13 +1001,13 @@ impl rusqlite::types::ToSql for ChatVisibility {
impl rusqlite::types::FromSql for ChatVisibility {
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
i64::column_result(value).map(|val| {
i64::column_result(value).and_then(|val| {
match val {
2 => ChatVisibility::Pinned,
1 => ChatVisibility::Archived,
0 => ChatVisibility::Normal,
2 => Ok(ChatVisibility::Pinned),
1 => Ok(ChatVisibility::Archived),
0 => Ok(ChatVisibility::Normal),
// fallback to to Normal for unknown values, may happen eg. on imports created by a newer version.
_ => ChatVisibility::Normal,
_ => Ok(ChatVisibility::Normal),
}
})
}
@@ -1103,9 +1070,6 @@ pub struct ChatInfo {
///
/// The exact time its muted can be found out via the `chat.mute_duration` property
pub is_muted: bool,
/// Ephemeral message timer.
pub ephemeral_timer: EphemeralTimer,
// ToDo:
// - [ ] deaddrop,
// - [ ] summary,
@@ -1374,12 +1338,11 @@ pub(crate) fn msgtype_has_file(msgtype: Viewtype) -> bool {
Viewtype::Voice => true,
Viewtype::Video => true,
Viewtype::File => true,
Viewtype::VideochatInvitation => false,
}
}
async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<(), Error> {
if msg.viewtype == Viewtype::Text || msg.viewtype == Viewtype::VideochatInvitation {
if msg.viewtype == Viewtype::Text {
// the caller should check if the message text is empty
} else if msgtype_has_file(msg.viewtype) {
let blob = msg
@@ -1501,7 +1464,7 @@ pub async fn send_msg(
}
}
msg.param.remove(Param::PrepForwards);
msg.update_param(context).await;
msg.save_param_to_disk(context).await;
}
return send_msg_inner(context, chat_id, msg).await;
}
@@ -1612,59 +1575,16 @@ pub async fn send_text_msg(
send_msg(context, chat_id, &mut msg).await
}
pub async fn send_videochat_invitation(context: &Context, chat_id: ChatId) -> Result<MsgId, Error> {
ensure!(
!chat_id.is_special(),
"video chat invitation cannot be sent to special chat: {}",
chat_id
);
let instance = if let Some(instance) = context.get_config(Config::WebrtcInstance).await {
if !instance.is_empty() {
instance
} else {
bail!("webrtc_instance is empty");
}
} else {
bail!("webrtc_instance not set");
};
let room = dc_create_id();
let instance = if instance.contains("$ROOM") {
instance.replace("$ROOM", &room)
} else {
format!("{}{}", instance, room)
};
let mut msg = Message::new(Viewtype::VideochatInvitation);
msg.param.set(Param::WebrtcRoom, &instance);
msg.text = Some(
context
.stock_string_repl_str(
StockMessage::VideochatInviteMsgBody,
Message::parse_webrtc_instance(&instance).1,
)
.await,
);
send_msg(context, chat_id, &mut msg).await
}
pub async fn get_chat_msgs(
context: &Context,
chat_id: ChatId,
flags: u32,
marker1before: Option<MsgId>,
) -> Vec<ChatItem> {
match delete_expired_messages(context).await {
) -> Vec<MsgId> {
match delete_device_expired_messages(context).await {
Err(err) => warn!(context, "Failed to delete expired messages: {}", err),
Ok(messages_deleted) => {
if messages_deleted {
// Trigger reload of chatlist.
//
// On desktop chatlist is always shown on the side,
// and it is important to update the last message shown
// there.
context.emit_event(Event::MsgsChanged {
msg_id: MsgId::new(0),
chat_id: ChatId::new(0),
@@ -1683,20 +1603,18 @@ pub async fn get_chat_msgs(
let (curr_id, ts) = row?;
if let Some(marker_id) = marker1before {
if curr_id == marker_id {
ret.push(ChatItem::Marker1);
ret.push(MsgId::new(DC_MSG_ID_MARKER1));
}
}
if (flags & DC_GCM_ADDDAYMARKER) != 0 {
let curr_local_timestamp = ts + cnv_to_local;
let curr_day = curr_local_timestamp / 86400;
if curr_day != last_day {
ret.push(ChatItem::DayMarker {
timestamp: curr_day,
});
ret.push(MsgId::new(DC_MSG_ID_DAYMARKER));
last_day = curr_day;
}
}
ret.push(ChatItem::Message { msg_id: curr_id });
ret.push(curr_id);
}
Ok(ret)
};
@@ -1828,6 +1746,52 @@ pub async fn marknoticed_all_chats(context: &Context) -> Result<(), Error> {
Ok(())
}
/// Deletes messages which are expired according to "delete_device_after" setting.
///
/// Returns true if any message is deleted, so event can be emitted. If nothing
/// has been deleted, returns false.
pub async fn delete_device_expired_messages(context: &Context) -> Result<bool, Error> {
if let Some(delete_device_after) = context.get_config_delete_device_after().await {
let threshold_timestamp = time() - delete_device_after;
let self_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
.await
.unwrap_or_default()
.0;
let device_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
.await
.unwrap_or_default()
.0;
// Delete expired messages
//
// Only update the rows that have to be updated, to avoid emitting
// unnecessary "chat modified" events.
let rows_modified = context
.sql
.execute(
"UPDATE msgs \
SET txt = 'DELETED', chat_id = ? \
WHERE timestamp < ? \
AND chat_id > ? \
AND chat_id != ? \
AND chat_id != ?",
paramsv![
DC_CHAT_ID_TRASH,
threshold_timestamp,
DC_CHAT_ID_LAST_SPECIAL,
self_chat_id,
device_chat_id
],
)
.await?;
Ok(rows_modified > 0)
} else {
Ok(false)
}
}
pub async fn get_chat_media(
context: &Context,
chat_id: ChatId,
@@ -2632,7 +2596,7 @@ pub async fn forward_msgs(
.set(Param::PrepForwards, new_msg_id.to_u32().to_string());
}
msg.update_param(context).await;
msg.save_param_to_disk(context).await;
msg.param = save_param;
} else {
msg.state = MessageState::OutPending;
@@ -2708,12 +2672,10 @@ pub(crate) async fn get_chat_id_by_grpid(
/// Adds a message to device chat.
///
/// Optional `label` can be provided to ensure that message is added only once.
/// If `important` is true, a notification will be sent.
pub async fn add_device_msg_with_importance(
pub async fn add_device_msg(
context: &Context,
label: Option<&str>,
msg: Option<&mut Message>,
important: bool,
) -> Result<MsgId, Error> {
ensure!(
label.is_some() || msg.is_some(),
@@ -2773,24 +2735,12 @@ pub async fn add_device_msg_with_importance(
}
if !msg_id.is_unset() {
if important {
context.emit_event(Event::IncomingMsg { chat_id, msg_id });
} else {
context.emit_event(Event::MsgsChanged { chat_id, msg_id });
}
context.emit_event(Event::IncomingMsg { chat_id, msg_id });
}
Ok(msg_id)
}
pub async fn add_device_msg(
context: &Context,
label: Option<&str>,
msg: Option<&mut Message>,
) -> Result<MsgId, Error> {
add_device_msg_with_importance(context, label, msg, false).await
}
pub async fn was_device_msg_ever_added(context: &Context, label: &str) -> Result<bool, Error> {
ensure!(!label.is_empty(), "empty label");
if let Ok(()) = context
@@ -2833,16 +2783,9 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
/// For example, it can be a message showing that a member was added to a group.
pub(crate) async fn add_info_msg(context: &Context, chat_id: ChatId, text: impl AsRef<str>) {
let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device");
let ephemeral_timer = match chat_id.get_ephemeral_timer(context).await {
Err(e) => {
warn!(context, "Could not get timer for info msg: {}", e);
return;
}
Ok(ephemeral_timer) => ephemeral_timer,
};
if let Err(e) = context.sql.execute(
"INSERT INTO msgs (chat_id,from_id,to_id, timestamp,type,state, txt,rfc724_mid,ephemeral_timer) VALUES (?,?,?, ?,?,?, ?,?,?);",
if context.sql.execute(
"INSERT INTO msgs (chat_id,from_id,to_id, timestamp,type,state, txt,rfc724_mid) VALUES (?,?,?, ?,?,?, ?,?);",
paramsv![
chat_id,
DC_CONTACT_ID_INFO,
@@ -2852,10 +2795,8 @@ pub(crate) async fn add_info_msg(context: &Context, chat_id: ChatId, text: impl
MessageState::InNoticed,
text.as_ref().to_string(),
rfc724_mid,
ephemeral_timer
]
).await {
warn!(context, "Could not add info msg: {}", e);
).await.is_err() {
return;
}
@@ -2879,7 +2820,7 @@ mod tests {
#[async_std::test]
async fn test_chat_info() {
let t = TestContext::new().await;
let t = dummy_context().await;
let bob = Contact::create(&t.ctx, "bob", "bob@example.com")
.await
.unwrap();
@@ -2902,8 +2843,7 @@ mod tests {
"color": 15895624,
"profile_image": "",
"draft": "",
"is_muted": false,
"ephemeral_timer": "Disabled"
"is_muted": false
}
"#;
@@ -2914,7 +2854,7 @@ mod tests {
#[async_std::test]
async fn test_get_draft_no_draft() {
let t = TestContext::new().await;
let t = dummy_context().await;
let chat_id = create_by_contact_id(&t.ctx, DC_CONTACT_ID_SELF)
.await
.unwrap();
@@ -2924,7 +2864,7 @@ mod tests {
#[async_std::test]
async fn test_get_draft_special_chat_id() {
let t = TestContext::new().await;
let t = dummy_context().await;
let draft = ChatId::new(DC_CHAT_ID_LAST_SPECIAL)
.get_draft(&t.ctx)
.await
@@ -2936,14 +2876,14 @@ mod tests {
async fn test_get_draft_no_chat() {
// This is a weird case, maybe this should be an error but we
// do not get this info from the database currently.
let t = TestContext::new().await;
let t = dummy_context().await;
let draft = ChatId::new(42).get_draft(&t.ctx).await.unwrap();
assert!(draft.is_none());
}
#[async_std::test]
async fn test_get_draft() {
let t = TestContext::new().await;
let t = dummy_context().await;
let chat_id = create_by_contact_id(&t.ctx, DC_CONTACT_ID_SELF)
.await
.unwrap();
@@ -2959,7 +2899,7 @@ mod tests {
#[async_std::test]
async fn test_add_contact_to_chat_ex_add_self() {
// Adding self to a contact should succeed, even though it's pointless.
let t = TestContext::new().await;
let t = test_context().await;
let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo")
.await
.unwrap();
@@ -2971,7 +2911,7 @@ mod tests {
#[async_std::test]
async fn test_self_talk() {
let t = TestContext::new().await;
let t = dummy_context().await;
let chat_id = create_by_contact_id(&t.ctx, DC_CONTACT_ID_SELF)
.await
.unwrap();
@@ -2992,7 +2932,7 @@ mod tests {
#[async_std::test]
async fn test_deaddrop_chat() {
let t = TestContext::new().await;
let t = dummy_context().await;
let chat = Chat::load_from_db(&t.ctx, ChatId::new(DC_CHAT_ID_DEADDROP))
.await
.unwrap();
@@ -3007,7 +2947,7 @@ mod tests {
#[async_std::test]
async fn test_add_device_msg_unlabelled() {
let t = TestContext::new().await;
let t = test_context().await;
// add two device-messages
let mut msg1 = Message::new(Viewtype::Text);
@@ -3042,7 +2982,7 @@ mod tests {
#[async_std::test]
async fn test_add_device_msg_labelled() {
let t = TestContext::new().await;
let t = test_context().await;
// add two device-messages with the same label (second attempt is not added)
let mut msg1 = Message::new(Viewtype::Text);
@@ -3096,7 +3036,7 @@ mod tests {
#[async_std::test]
async fn test_add_device_msg_label_only() {
let t = TestContext::new().await;
let t = test_context().await;
let res = add_device_msg(&t.ctx, Some(""), None).await;
assert!(res.is_err());
let res = add_device_msg(&t.ctx, Some("some-label"), None).await;
@@ -3116,7 +3056,7 @@ mod tests {
#[async_std::test]
async fn test_was_device_msg_ever_added() {
let t = TestContext::new().await;
let t = test_context().await;
add_device_msg(&t.ctx, Some("some-label"), None).await.ok();
assert!(was_device_msg_ever_added(&t.ctx, "some-label")
.await
@@ -3140,7 +3080,7 @@ mod tests {
#[async_std::test]
async fn test_delete_device_chat() {
let t = TestContext::new().await;
let t = test_context().await;
let mut msg = Message::new(Viewtype::Text);
msg.text = Some("message text".to_string());
@@ -3160,7 +3100,7 @@ mod tests {
#[async_std::test]
async fn test_device_chat_cannot_sent() {
let t = TestContext::new().await;
let t = test_context().await;
t.ctx.update_device_chats().await.unwrap();
let (device_chat_id, _) =
create_or_lookup_by_contact_id(&t.ctx, DC_CONTACT_ID_DEVICE, Blocked::Not)
@@ -3180,7 +3120,7 @@ mod tests {
#[async_std::test]
async fn test_delete_and_reset_all_device_msgs() {
let t = TestContext::new().await;
let t = test_context().await;
let mut msg = Message::new(Viewtype::Text);
msg.text = Some("message text".to_string());
let msg_id1 = add_device_msg(&t.ctx, Some("some-label"), Some(&mut msg))
@@ -3217,7 +3157,7 @@ mod tests {
#[async_std::test]
async fn test_archive() {
// create two chats
let t = TestContext::new().await;
let t = dummy_context().await;
let mut msg = Message::new(Viewtype::Text);
msg.text = Some("foo".to_string());
let msg_id = add_device_msg(&t.ctx, None, Some(&mut msg)).await.unwrap();
@@ -3331,7 +3271,7 @@ mod tests {
#[async_std::test]
async fn test_pinned() {
let t = TestContext::new().await;
let t = dummy_context().await;
// create 3 chats, wait 1 second in between to get a reliable order (we order by time)
let mut msg = Message::new(Viewtype::Text);
@@ -3390,7 +3330,7 @@ mod tests {
#[async_std::test]
async fn test_set_chat_name() {
let t = TestContext::new().await;
let t = dummy_context().await;
let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo")
.await
.unwrap();
@@ -3414,7 +3354,7 @@ mod tests {
#[async_std::test]
async fn test_create_same_chat_twice() {
let context = TestContext::new().await;
let context = dummy_context().await;
let contact1 = Contact::create(&context.ctx, "bob", "bob@mail.de")
.await
.unwrap();
@@ -3433,7 +3373,7 @@ mod tests {
#[async_std::test]
async fn test_shall_attach_selfavatar() {
let t = TestContext::new().await;
let t = dummy_context().await;
let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo")
.await
.unwrap();
@@ -3457,7 +3397,7 @@ mod tests {
#[async_std::test]
async fn test_set_mute_duration() {
let t = TestContext::new().await;
let t = dummy_context().await;
let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo")
.await
.unwrap();
@@ -3525,7 +3465,7 @@ mod tests {
#[async_std::test]
async fn test_parent_is_encrypted() {
let t = TestContext::new().await;
let t = dummy_context().await;
let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo")
.await
.unwrap();

View File

@@ -5,7 +5,6 @@ use crate::chat::*;
use crate::constants::*;
use crate::contact::*;
use crate::context::*;
use crate::ephemeral::delete_expired_messages;
use crate::error::{bail, ensure, Result};
use crate::lot::Lot;
use crate::message::{Message, MessageState, MsgId};
@@ -77,7 +76,7 @@ impl Chatlist {
/// chats
/// - the flag DC_GCL_FOR_FORWARDING sorts "Saved messages" to the top of the chatlist
/// and hides the device-chat,
/// typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
// typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
/// - if the flag DC_GCL_NO_SPECIALS is set, deaddrop and archive link are not added
/// to the list (may be used eg. for selecting chats on forwarding, the flag is
/// not needed when DC_GCL_ARCHIVED_ONLY is already set)
@@ -100,7 +99,7 @@ impl Chatlist {
// Note that we do not emit DC_EVENT_MSGS_MODIFIED here even if some
// messages get deleted to avoid reloading the same chatlist.
if let Err(err) = delete_expired_messages(context).await {
if let Err(err) = delete_device_expired_messages(context).await {
warn!(context, "Failed to hide expired messages: {}", err);
}
@@ -148,12 +147,11 @@ impl Chatlist {
FROM chats c
LEFT JOIN msgs m
ON c.id=m.chat_id
AND m.id=(
SELECT id
AND m.timestamp=(
SELECT MAX(timestamp)
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?1)
ORDER BY timestamp DESC, id DESC LIMIT 1)
AND (hidden=0 OR state=?1))
WHERE c.id>9
AND c.blocked=0
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
@@ -175,12 +173,11 @@ impl Chatlist {
FROM chats c
LEFT JOIN msgs m
ON c.id=m.chat_id
AND m.id=(
SELECT id
AND m.timestamp=(
SELECT MAX(timestamp)
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?)
ORDER BY timestamp DESC, id DESC LIMIT 1)
AND (hidden=0 OR state=?))
WHERE c.id>9
AND c.blocked=0
AND c.archived=1
@@ -209,12 +206,11 @@ impl Chatlist {
FROM chats c
LEFT JOIN msgs m
ON c.id=m.chat_id
AND m.id=(
SELECT id
AND m.timestamp=(
SELECT MAX(timestamp)
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?1)
ORDER BY timestamp DESC, id DESC LIMIT 1)
AND (hidden=0 OR state=?1))
WHERE c.id>9 AND c.id!=?2
AND c.blocked=0
AND c.name LIKE ?3
@@ -240,12 +236,11 @@ impl Chatlist {
FROM chats c
LEFT JOIN msgs m
ON c.id=m.chat_id
AND m.id=(
SELECT id
AND m.timestamp=(
SELECT MAX(timestamp)
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?1)
ORDER BY timestamp DESC, id DESC LIMIT 1)
AND (hidden=0 OR state=?1))
WHERE c.id>9 AND c.id!=?2
AND c.blocked=0
AND NOT c.archived=?3
@@ -329,30 +324,20 @@ impl Chatlist {
// This is because we may want to display drafts here or stuff as
// "is typing".
// Also, sth. as "No messages" would not work if the summary comes from a message.
let mut ret = Lot::new();
let (chat_id, lastmsg_id) = match self.ids.get(index) {
Some(ids) => ids,
None => {
let mut ret = Lot::new();
ret.text2 = Some("ErrBadChatlistIndex".to_string());
return Lot::new();
return ret;
}
};
Chatlist::get_summary2(context, *chat_id, *lastmsg_id, chat).await
}
pub async fn get_summary2(
context: &Context,
chat_id: ChatId,
lastmsg_id: MsgId,
chat: Option<&Chat>,
) -> Lot {
let mut ret = Lot::new();
let chat_loaded: Chat;
let chat = if let Some(chat) = chat {
chat
} else if let Ok(chat) = Chat::load_from_db(context, chat_id).await {
} else if let Ok(chat) = Chat::load_from_db(context, *chat_id).await {
chat_loaded = chat;
&chat_loaded
} else {
@@ -361,7 +346,7 @@ impl Chatlist {
let mut lastcontact = None;
let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, lastmsg_id).await {
let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, *lastmsg_id).await {
if lastmsg.from_id != DC_CONTACT_ID_SELF
&& (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup)
{
@@ -439,7 +424,7 @@ mod tests {
#[async_std::test]
async fn test_try_load() {
let t = TestContext::new().await;
let t = dummy_context().await;
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
.await
.unwrap();
@@ -487,7 +472,7 @@ mod tests {
#[async_std::test]
async fn test_sort_self_talk_up_on_forward() {
let t = TestContext::new().await;
let t = dummy_context().await;
t.ctx.update_device_chats().await.unwrap();
create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
.await
@@ -512,7 +497,7 @@ mod tests {
#[async_std::test]
async fn test_search_special_chat_names() {
let t = TestContext::new().await;
let t = dummy_context().await;
t.ctx.update_device_chats().await.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-1234-s"), None)
@@ -545,7 +530,7 @@ mod tests {
#[async_std::test]
async fn test_get_summary_unwrap() {
let t = TestContext::new().await;
let t = dummy_context().await;
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
.await
.unwrap();

View File

@@ -11,7 +11,7 @@ use crate::dc_tools::*;
use crate::events::Event;
use crate::message::MsgId;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::stock::StockMessage;
use crate::{scheduler::InterruptInfo, stock::StockMessage};
/// The available configuration keys.
#[derive(
@@ -117,21 +117,9 @@ pub enum Config {
#[strum(serialize = "sys.config_keys")]
SysConfigKeys,
#[strum(props(default = "0"))]
/// Whether we send a warning if the password is wrong (set to false when we send a warning
/// because we do not want to send a second warning)
NotifyAboutWrongPw,
/// address to webrtc instance to use for videochats
WebrtcInstance,
}
impl Context {
pub async fn config_exists(&self, key: Config) -> bool {
self.sql.get_raw_config(self, key).await.is_some()
}
/// Get a configuration key. Returns `None` if no value is set, and no default value found.
pub async fn get_config(&self, key: Config) -> Option<String> {
let value = match key {
@@ -213,6 +201,22 @@ impl Context {
None => self.sql.set_raw_config(self, key, None).await,
}
}
Config::InboxWatch => {
let ret = self.sql.set_raw_config(self, key, value).await;
self.interrupt_inbox(InterruptInfo::new(false, None)).await;
ret
}
Config::SentboxWatch => {
let ret = self.sql.set_raw_config(self, key, value).await;
self.interrupt_sentbox(InterruptInfo::new(false, None))
.await;
ret
}
Config::MvboxWatch => {
let ret = self.sql.set_raw_config(self, key, value).await;
self.interrupt_mvbox(InterruptInfo::new(false, None)).await;
ret
}
Config::Selfstatus => {
let def = self.stock_str(StockMessage::StatusLine).await;
let val = if value.is_none() || value.unwrap() == def {
@@ -282,7 +286,7 @@ mod tests {
#[async_std::test]
async fn test_selfavatar_outside_blobdir() {
let t = TestContext::new().await;
let t = dummy_context().await;
let avatar_src = t.dir.path().join("avatar.jpg");
let avatar_bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
File::create(&avatar_src)
@@ -311,7 +315,7 @@ mod tests {
#[async_std::test]
async fn test_selfavatar_in_blobdir() {
let t = TestContext::new().await;
let t = dummy_context().await;
let avatar_src = t.ctx.get_blobdir().join("avatar.png");
let avatar_bytes = include_bytes!("../test-data/image/avatar900x900.png");
File::create(&avatar_src)
@@ -337,7 +341,7 @@ mod tests {
#[async_std::test]
async fn test_selfavatar_copy_without_recode() {
let t = TestContext::new().await;
let t = dummy_context().await;
let avatar_src = t.dir.path().join("avatar.png");
let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png");
File::create(&avatar_src)
@@ -361,7 +365,7 @@ mod tests {
#[async_std::test]
async fn test_media_quality_config_option() {
let t = TestContext::new().await;
let t = dummy_context().await;
let media_quality = t.ctx.get_config_int(Config::MediaQuality).await;
assert_eq!(media_quality, 0);
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();

View File

@@ -68,21 +68,16 @@ impl Context {
async fn inner_configure(&self) -> Result<()> {
info!(self, "Configure ...");
let was_configured_before = self.is_configured().await;
let mut param = LoginParam::from_database(self, "").await;
let success = configure(self, &mut param).await;
self.set_config(Config::NotifyAboutWrongPw, None).await?;
if let Some(provider) = provider::get_provider_info(&param.addr) {
if let Some(config_defaults) = &provider.config_defaults {
for def in config_defaults.iter() {
if !self.config_exists(def.key).await {
if !was_configured_before {
if let Some(config_defaults) = &provider.config_defaults {
for def in config_defaults.iter() {
info!(self, "apply config_defaults {}={}", def.key, def.value);
self.set_config(def.key, Some(def.value)).await?;
} else {
info!(
self,
"skip already set config_defaults {}={}", def.key, def.value
);
}
}
}
@@ -101,12 +96,11 @@ impl Context {
match success {
Ok(_) => {
self.set_config(Config::NotifyAboutWrongPw, Some("1"))
.await?;
progress!(self, 1000);
Ok(())
}
Err(err) => {
error!(self, "Configure Failed: {}", err);
progress!(self, 0);
Err(err)
}
@@ -232,7 +226,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
}
if !dc_exactly_one_bit_set(param.server_flags & DC_LP_IMAP_SOCKET_FLAGS as i32) {
param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS as i32);
param.server_flags |= if param.mail_port == 143 {
param.server_flags |= if param.send_port == 143 {
DC_LP_IMAP_SOCKET_STARTTLS as i32
} else {
DC_LP_IMAP_SOCKET_SSL as i32
@@ -459,10 +453,7 @@ async fn try_imap_connections(
.await
.is_ok()
{
return Ok(());
}
if was_autoconfig {
bail!("autoconfig did not succeed");
return Ok(()); // we directly return here if it was autoconfig or the connection succeeded
}
progress!(context, 670);
@@ -496,7 +487,7 @@ async fn try_imap_connection(
return Ok(());
}
if was_autoconfig {
bail!("autoconfig did not succeed");
return Ok(());
}
progress!(context, 650 + variation * 30);
@@ -556,7 +547,7 @@ async fn try_smtp_connections(
return Ok(());
}
if was_autoconfig {
bail!("No SMTP connection");
return Ok(());
}
progress!(context, 850);
@@ -630,7 +621,7 @@ mod tests {
#[async_std::test]
async fn test_no_panic_on_bad_credentials() {
let t = TestContext::new().await;
let t = dummy_context().await;
t.ctx
.set_config(Config::Addr, Some("probably@unexistant.addr"))
.await
@@ -644,7 +635,7 @@ mod tests {
#[async_std::test]
async fn test_get_offline_autoconfig() {
let context = TestContext::new().await.ctx;
let context = dummy_context().await.ctx;
let mut params = LoginParam::new();
params.addr = "someone123@example.org".to_string();

View File

@@ -84,20 +84,6 @@ impl Default for KeyGenType {
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[repr(i8)]
pub enum VideochatType {
Unknown = 0,
BasicWebrtc = 1,
Jitsi = 2,
}
impl Default for VideochatType {
fn default() -> Self {
VideochatType::Unknown
}
}
pub const DC_HANDSHAKE_CONTINUE_NORMAL_PROCESSING: i32 = 0x01;
pub const DC_HANDSHAKE_STOP_NORMAL_PROCESSING: i32 = 0x02;
pub const DC_HANDSHAKE_ADD_DELETE_JOB: i32 = 0x04;
@@ -310,9 +296,6 @@ pub enum Viewtype {
/// The file is set via dc_msg_set_file()
/// and retrieved via dc_msg_get_file().
File = 60,
/// Message is an invitation to a videochat.
VideochatInvitation = 70,
}
impl Default for Viewtype {
@@ -340,6 +323,54 @@ const DC_EVENT_FILE_COPIED: usize = 2055; // deprecated;
const DC_EVENT_IS_OFFLINE: usize = 2081; // deprecated;
const DC_ERROR_SEE_STRING: usize = 0; // deprecated;
const DC_ERROR_SELF_NOT_IN_GROUP: usize = 1; // deprecated;
const DC_STR_SELFNOTINGRP: usize = 21; // deprecated;
// TODO: Strings need some doumentation about used placeholders.
// These constants are used to set stock translation strings
const DC_STR_NOMESSAGES: usize = 1;
const DC_STR_SELF: usize = 2;
const DC_STR_DRAFT: usize = 3;
const DC_STR_VOICEMESSAGE: usize = 7;
const DC_STR_DEADDROP: usize = 8;
const DC_STR_IMAGE: usize = 9;
const DC_STR_VIDEO: usize = 10;
const DC_STR_AUDIO: usize = 11;
const DC_STR_FILE: usize = 12;
const DC_STR_STATUSLINE: usize = 13;
const DC_STR_NEWGROUPDRAFT: usize = 14;
const DC_STR_MSGGRPNAME: usize = 15;
const DC_STR_MSGGRPIMGCHANGED: usize = 16;
const DC_STR_MSGADDMEMBER: usize = 17;
const DC_STR_MSGDELMEMBER: usize = 18;
const DC_STR_MSGGROUPLEFT: usize = 19;
const DC_STR_GIF: usize = 23;
const DC_STR_ENCRYPTEDMSG: usize = 24;
const DC_STR_E2E_AVAILABLE: usize = 25;
const DC_STR_ENCR_TRANSP: usize = 27;
const DC_STR_ENCR_NONE: usize = 28;
const DC_STR_CANTDECRYPT_MSG_BODY: usize = 29;
const DC_STR_FINGERPRINTS: usize = 30;
const DC_STR_READRCPT: usize = 31;
const DC_STR_READRCPT_MAILBODY: usize = 32;
const DC_STR_MSGGRPIMGDELETED: usize = 33;
const DC_STR_E2E_PREFERRED: usize = 34;
const DC_STR_CONTACT_VERIFIED: usize = 35;
const DC_STR_CONTACT_NOT_VERIFIED: usize = 36;
const DC_STR_CONTACT_SETUP_CHANGED: usize = 37;
const DC_STR_ARCHIVEDCHATS: usize = 40;
const DC_STR_STARREDMSGS: usize = 41;
const DC_STR_AC_SETUP_MSG_SUBJECT: usize = 42;
const DC_STR_AC_SETUP_MSG_BODY: usize = 43;
const DC_STR_CANNOT_LOGIN: usize = 60;
const DC_STR_SERVER_RESPONSE: usize = 61;
const DC_STR_MSGACTIONBYUSER: usize = 62;
const DC_STR_MSGACTIONBYME: usize = 63;
const DC_STR_MSGLOCATIONENABLED: usize = 64;
const DC_STR_MSGLOCATIONDISABLED: usize = 65;
const DC_STR_LOCATION: usize = 66;
const DC_STR_STICKER: usize = 67;
const DC_STR_COUNT: usize = 67;
pub const DC_JOB_DELETE_MSG_ON_IMAP: i32 = 110;

View File

@@ -678,7 +678,7 @@ impl Contact {
let mut ret = String::new();
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
let peerstate = Peerstate::from_addr(context, &contact.addr).await?;
let peerstate = Peerstate::from_addr(context, &contact.addr).await;
let loginparam = LoginParam::from_database(context, "configured_").await;
if peerstate.is_some()
@@ -939,17 +939,7 @@ impl Contact {
}
}
let peerstate = match Peerstate::from_addr(context, &self.addr).await {
Ok(peerstate) => peerstate,
Err(err) => {
warn!(
context,
"Failed to load peerstate for address {}: {}", self.addr, err
);
return VerifiedStatus::Unverified;
}
};
let peerstate = Peerstate::from_addr(context, &self.addr).await;
if let Some(ps) = peerstate {
if ps.verified_key.is_some() {
return VerifiedStatus::BidirectVerified;
@@ -1039,10 +1029,10 @@ pub fn addr_normalize(addr: &str) -> &str {
let norm = addr.trim();
if norm.starts_with("mailto:") {
norm.get(7..).unwrap_or(norm)
} else {
norm
return &norm[7..];
}
norm
}
fn sanitize_name_and_addr(name: impl AsRef<str>, addr: impl AsRef<str>) -> (String, String) {
@@ -1052,15 +1042,11 @@ fn sanitize_name_and_addr(name: impl AsRef<str>, addr: impl AsRef<str>) -> (Stri
if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
(
if name.as_ref().is_empty() {
captures
.get(1)
.map_or("".to_string(), |m| normalize_name(m.as_str()))
normalize_name(&captures[1])
} else {
name.as_ref().to_string()
},
captures
.get(2)
.map_or("".to_string(), |m| m.as_str().to_string()),
captures[2].to_string(),
)
} else {
(name.as_ref().to_string(), addr.as_ref().to_string())
@@ -1099,45 +1085,21 @@ async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: boo
}
}
/// Set profile image for a contact.
///
/// The given profile image is expected to be already in the blob directory
/// as profile images can be set only by receiving messages, this should be always the case, however.
///
/// For contact SELF, the image is not saved in the contact-database but as Config::Selfavatar;
/// this typically happens if we see message with our own profile image, sent from another device.
pub(crate) async fn set_profile_image(
context: &Context,
contact_id: u32,
profile_image: &AvatarAction,
was_encrypted: bool,
) -> Result<()> {
// the given profile image is expected to be already in the blob directory
// as profile images can be set only by receiving messages, this should be always the case, however.
let mut contact = Contact::load_from_db(context, contact_id).await?;
let changed = match profile_image {
AvatarAction::Change(profile_image) => {
if contact_id == DC_CONTACT_ID_SELF {
if was_encrypted {
context
.set_config(Config::Selfavatar, Some(profile_image))
.await?;
} else {
info!(context, "Do not use unencrypted selfavatar.");
}
} else {
contact.param.set(Param::ProfileImage, profile_image);
}
contact.param.set(Param::ProfileImage, profile_image);
true
}
AvatarAction::Delete => {
if contact_id == DC_CONTACT_ID_SELF {
if was_encrypted {
context.set_config(Config::Selfavatar, None).await?;
} else {
info!(context, "Do not use unencrypted selfavatar deletion.");
}
} else {
contact.param.remove(Param::ProfileImage);
}
contact.param.remove(Param::ProfileImage);
true
}
};
@@ -1151,21 +1113,38 @@ pub(crate) async fn set_profile_image(
/// Normalize a name.
///
/// - Remove quotes (come from some bad MUA implementations)
/// - Convert names as "Petersen, Björn" to "Björn Petersen"
/// - Trims the resulting string
///
/// Typically, this function is not needed as it is called implicitly by `Contact::add_address_book`.
pub fn normalize_name(full_name: impl AsRef<str>) -> String {
let full_name = full_name.as_ref().trim();
let mut full_name = full_name.as_ref().trim();
if full_name.is_empty() {
return full_name.into();
}
match full_name.as_bytes() {
[b'\'', .., b'\''] | [b'\"', .., b'\"'] | [b'<', .., b'>'] => full_name
.get(1..full_name.len() - 1)
.map_or("".to_string(), |s| s.trim().into()),
_ => full_name.to_string(),
let len = full_name.len();
if len > 1 {
let firstchar = full_name.as_bytes()[0];
let lastchar = full_name.as_bytes()[len - 1];
if firstchar == b'\'' && lastchar == b'\''
|| firstchar == b'\"' && lastchar == b'\"'
|| firstchar == b'<' && lastchar == b'>'
{
full_name = &full_name[1..len - 1];
}
}
if let Some(p1) = full_name.find(',') {
let (last_name, first_name) = full_name.split_at(p1);
let last_name = last_name.trim();
let first_name = (&first_name[1..]).trim();
return format!("{} {}", first_name, last_name);
}
full_name.trim().into()
}
fn cat_fingerprint(
@@ -1256,6 +1235,7 @@ mod tests {
#[test]
fn test_normalize_name() {
assert_eq!(&normalize_name("Doe, John"), "John Doe");
assert_eq!(&normalize_name(" hello world "), "hello world");
assert_eq!(&normalize_name("<"), "<");
assert_eq!(&normalize_name(">"), ">");
@@ -1290,7 +1270,7 @@ mod tests {
#[async_std::test]
async fn test_get_contacts() {
let context = TestContext::new().await;
let context = dummy_context().await;
let contacts = Contact::get_all(&context.ctx, 0, Some("some2"))
.await
.unwrap();
@@ -1314,10 +1294,10 @@ mod tests {
#[async_std::test]
async fn test_is_self_addr() -> Result<()> {
let t = TestContext::new().await;
let t = test_context().await;
assert!(t.ctx.is_self_addr("me@me.org").await.is_err());
let addr = t.configure_alice().await;
let addr = configure_alice_keypair(&t.ctx).await;
assert_eq!(t.ctx.is_self_addr("me@me.org").await?, false);
assert_eq!(t.ctx.is_self_addr(&addr).await?, true);
@@ -1327,7 +1307,7 @@ mod tests {
#[async_std::test]
async fn test_add_or_lookup() {
// add some contacts, this also tests add_address_book()
let t = TestContext::new().await;
let t = dummy_context().await;
let book = concat!(
" Name one \n one@eins.org \n",
"Name two\ntwo@deux.net\n",
@@ -1420,10 +1400,10 @@ mod tests {
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
assert_eq!(sth_modified, Modifier::None);
let contact = Contact::load_from_db(&t.ctx, contact_id).await.unwrap();
assert_eq!(contact.get_name(), "Wonderland, Alice");
assert_eq!(contact.get_display_name(), "Wonderland, Alice");
assert_eq!(contact.get_name(), "Alice Wonderland");
assert_eq!(contact.get_display_name(), "Alice Wonderland");
assert_eq!(contact.get_addr(), "alice@w.de");
assert_eq!(contact.get_name_n_addr(), "Wonderland, Alice (alice@w.de)");
assert_eq!(contact.get_name_n_addr(), "Alice Wonderland (alice@w.de)");
// check SELF
let contact = Contact::load_from_db(&t.ctx, DC_CONTACT_ID_SELF)
@@ -1440,7 +1420,7 @@ mod tests {
#[async_std::test]
async fn test_remote_authnames() {
let t = TestContext::new().await;
let t = dummy_context().await;
// incoming mail `From: bob1 <bob@example.org>` - this should init authname and name
let (contact_id, sth_modified) = Contact::add_or_lookup(
@@ -1503,7 +1483,7 @@ mod tests {
#[async_std::test]
async fn test_remote_authnames_create_empty() {
let t = TestContext::new().await;
let t = dummy_context().await;
// manually create "claire@example.org" without a given name
let contact_id = Contact::create(&t.ctx, "", "claire@example.org")
@@ -1550,7 +1530,7 @@ mod tests {
#[async_std::test]
async fn test_remote_authnames_edit_empty() {
let t = TestContext::new().await;
let t = dummy_context().await;
// manually create "dave@example.org"
let contact_id = Contact::create(&t.ctx, "dave1", "dave@example.org")
@@ -1594,7 +1574,7 @@ mod tests {
#[async_std::test]
async fn test_name_in_address() {
let t = TestContext::new().await;
let t = dummy_context().await;
let contact_id = Contact::create(&t.ctx, "", "<dave@example.org>")
.await
@@ -1607,7 +1587,7 @@ mod tests {
.await
.unwrap();
let contact = Contact::load_from_db(&t.ctx, contact_id).await.unwrap();
assert_eq!(contact.get_name(), "Mueller, Dave");
assert_eq!(contact.get_name(), "Dave Mueller");
assert_eq!(contact.get_addr(), "dave@example.org");
let contact_id = Contact::create(&t.ctx, "name1", "name2 <dave@example.org>")

View File

@@ -6,7 +6,6 @@ use std::ops::Deref;
use async_std::path::{Path, PathBuf};
use async_std::sync::{channel, Arc, Mutex, Receiver, RwLock, Sender};
use async_std::task;
use crate::chat::*;
use crate::config::Config;
@@ -15,10 +14,12 @@ use crate::contact::*;
use crate::dc_tools::duration_to_str;
use crate::error::*;
use crate::events::{Event, EventEmitter, Events};
use crate::job::{self, Action};
use crate::key::{DcKey, SignedPublicKey};
use crate::login_param::LoginParam;
use crate::lot::Lot;
use crate::message::{self, MsgId};
use crate::message::{self, Message, MessengerMessage, MsgId};
use crate::param::Params;
use crate::scheduler::Scheduler;
use crate::sql::Sql;
use std::time::SystemTime;
@@ -51,13 +52,10 @@ pub struct InnerContext {
pub(crate) generating_key_mutex: Mutex<()>,
/// Mutex to enforce only a single running oauth2 is running.
pub(crate) oauth2_mutex: Mutex<()>,
/// Mutex to prevent a race condition when a "your pw is wrong" warning is sent, resulting in multiple messeges being sent.
pub(crate) wrong_pw_warning_mutex: Mutex<()>,
pub(crate) translated_stockstrings: RwLock<HashMap<usize, String>>,
pub(crate) events: Events,
pub(crate) scheduler: RwLock<Scheduler>,
pub(crate) ephemeral_task: RwLock<Option<task::JoinHandle<()>>>,
creation_time: SystemTime,
}
@@ -120,11 +118,9 @@ impl Context {
last_smeared_timestamp: RwLock::new(0),
generating_key_mutex: Mutex::new(()),
oauth2_mutex: Mutex::new(()),
wrong_pw_warning_mutex: Mutex::new(()),
translated_stockstrings: RwLock::new(HashMap::new()),
events: Events::default(),
scheduler: RwLock::new(Scheduler::Stopped),
ephemeral_task: RwLock::new(None),
creation_time: std::time::SystemTime::now(),
};
@@ -458,6 +454,34 @@ impl Context {
self.get_config(Config::ConfiguredMvboxFolder).await
== Some(folder_name.as_ref().to_string())
}
pub async fn do_heuristics_moves(&self, folder: &str, msg_id: MsgId) {
if !self.get_config_bool(Config::MvboxMove).await {
return;
}
if self.is_mvbox(folder).await {
return;
}
if let Ok(msg) = Message::load_from_db(self, msg_id).await {
if msg.is_setupmessage() {
// do not move setup messages;
// there may be a non-delta device that wants to handle it
return;
}
match msg.is_dc_message {
MessengerMessage::No => {}
MessengerMessage::Yes | MessengerMessage::Reply => {
job::add(
self,
job::Job::new(Action::MoveMsg, msg.id.to_u32(), Params::new(), 0),
)
.await;
}
}
}
}
}
impl InnerContext {
@@ -516,7 +540,7 @@ mod tests {
#[async_std::test]
async fn test_get_fresh_msgs() {
let t = TestContext::new().await;
let t = dummy_context().await;
let fresh = t.ctx.get_fresh_msgs().await;
assert!(fresh.is_empty())
}
@@ -571,13 +595,13 @@ mod tests {
#[async_std::test]
async fn no_crashes_on_context_deref() {
let t = TestContext::new().await;
let t = dummy_context().await;
std::mem::drop(t.ctx);
}
#[async_std::test]
async fn test_get_info() {
let t = TestContext::new().await;
let t = dummy_context().await;
let info = t.ctx.get_info().await;
assert!(info.get("database_dir").is_some());

View File

@@ -10,7 +10,6 @@ use crate::constants::*;
use crate::contact::*;
use crate::context::Context;
use crate::dc_tools::*;
use crate::ephemeral::{stock_ephemeral_timer_changed, Timer as EphemeralTimer};
use crate::error::{bail, ensure, format_err, Result};
use crate::events::Event;
use crate::headerdef::HeaderDef;
@@ -46,14 +45,13 @@ pub async fn dc_receive_imf(
) -> Result<()> {
info!(
context,
"Receiving message {}/{}, seen={}...",
"Receiving message {}/{}...",
if !server_folder.as_ref().is_empty() {
server_folder.as_ref()
} else {
"?"
},
server_uid,
seen
);
if std::env::var(crate::DCC_MIME_DEBUG).unwrap_or_default() == "2" {
@@ -197,14 +195,7 @@ pub async fn dc_receive_imf(
}
if let Some(avatar_action) = &mime_parser.user_avatar {
match contact::set_profile_image(
&context,
from_id,
avatar_action,
mime_parser.was_encrypted(),
)
.await
{
match contact::set_profile_image(&context, from_id, avatar_action).await {
Ok(()) => {
context.emit_event(Event::ChatModified(chat_id));
}
@@ -231,29 +222,11 @@ pub async fn dc_receive_imf(
)
.await;
}
} else if insert_msg_id
.needs_move(context, server_folder.as_ref())
.await
.unwrap_or_default()
{
} else {
// Move message if we don't delete it immediately.
job::add(
context,
job::Job::new(Action::MoveMsg, insert_msg_id.to_u32(), Params::new(), 0),
)
.await;
} else if !mime_parser.mdn_reports.is_empty() && mime_parser.has_chat_version() {
// This is a Delta Chat MDN. Mark as read.
job::add(
context,
job::Job::new(
Action::MarkseenMsgOnImap,
insert_msg_id.to_u32(),
Params::new(),
0,
),
)
.await;
context
.do_heuristics_moves(server_folder.as_ref(), insert_msg_id)
.await;
}
}
@@ -265,7 +238,7 @@ pub async fn dc_receive_imf(
cleanup(context, &create_event_to_send, created_db_entries);
mime_parser
.handle_reports(context, from_id, sent_timestamp, &mime_parser.parts)
.handle_reports(context, from_id, sent_timestamp)
.await;
Ok(())
@@ -340,6 +313,8 @@ async fn add_parts(
) -> Result<()> {
let mut state: MessageState;
let mut chat_id_blocked = Blocked::Not;
let mut sort_timestamp = 0;
let mut rcvd_timestamp = 0;
let mut mime_in_reply_to = String::new();
let mut mime_references = String::new();
let mut incoming_origin = incoming_origin;
@@ -359,7 +334,7 @@ async fn add_parts(
return Ok(());
}
let mut is_dc_message = if mime_parser.has_chat_version() {
let mut msgrmsg = if mime_parser.has_chat_version() {
MessengerMessage::Yes
} else if is_reply_to_messenger_message(context, mime_parser).await {
MessengerMessage::Reply
@@ -371,7 +346,7 @@ async fn add_parts(
let show_emails =
ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await).unwrap_or_default();
if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage
&& is_dc_message == MessengerMessage::No
&& msgrmsg == MessengerMessage::No
{
// this message is a classic email not a chat-message nor a reply to one
match show_emails {
@@ -400,7 +375,7 @@ async fn add_parts(
// handshake may mark contacts as verified and must be processed before chats are created
if mime_parser.get(HeaderDef::SecureJoin).is_some() {
is_dc_message = MessengerMessage::Yes; // avoid discarding by show_emails setting
msgrmsg = MessengerMessage::Yes; // avoid discarding by show_emails setting
*chat_id = ChatId::new(0);
allow_creation = true;
match handle_securejoin_handshake(context, mime_parser, from_id).await {
@@ -432,14 +407,6 @@ async fn add_parts(
.await
.unwrap_or_default();
if chat_id.is_unset() && mime_parser.failure_report.is_some() {
*chat_id = ChatId::new(DC_CHAT_ID_TRASH);
info!(
context,
"Message belongs to an NDN and is not shown in a chat.",
);
}
// get the chat_id - a chat_id here is no indicator that the chat is displayed in the normal list,
// it might also be blocked and displayed in the deaddrop as a result
if chat_id.is_unset() {
@@ -533,7 +500,7 @@ async fn add_parts(
if Blocked::Not != chat_id_blocked
&& state == MessageState::InFresh
&& !incoming_origin.is_known()
&& is_dc_message == MessengerMessage::No
&& msgrmsg == MessengerMessage::No
&& show_emails != ShowEmails::All
{
state = MessageState::InNoticed;
@@ -548,7 +515,7 @@ async fn add_parts(
// handshake may mark contacts as verified and must be processed before chats are created
if mime_parser.get(HeaderDef::SecureJoin).is_some() {
is_dc_message = MessengerMessage::Yes; // avoid discarding by show_emails setting
msgrmsg = MessengerMessage::Yes; // avoid discarding by show_emails setting
*chat_id = ChatId::new(0);
allow_creation = true;
match observe_securejoin_on_other_device(context, mime_parser, to_id).await {
@@ -587,7 +554,7 @@ async fn add_parts(
}
}
if chat_id.is_unset() && allow_creation {
let create_blocked = if MessengerMessage::No != is_dc_message
let create_blocked = if MessengerMessage::No != msgrmsg
&& !Contact::is_blocked_load(context, to_id).await
{
Blocked::Not
@@ -632,76 +599,19 @@ async fn add_parts(
*chat_id = ChatId::new(DC_CHAT_ID_TRASH);
}
}
// Extract ephemeral timer from the message.
let mut ephemeral_timer = if let Some(value) = mime_parser.get(HeaderDef::EphemeralTimer) {
match value.parse::<EphemeralTimer>() {
Ok(timer) => timer,
Err(err) => {
warn!(
context,
"can't parse ephemeral timer \"{}\": {}", value, err
);
EphemeralTimer::Disabled
}
}
} else {
EphemeralTimer::Disabled
};
let location_kml_is = mime_parser.location_kml.is_some();
let is_mdn = !mime_parser.mdn_reports.is_empty();
// Apply ephemeral timer changes to the chat.
//
// Only non-hidden timers are applied now. Timers from hidden
// messages such as read receipts can be useful to detect
// ephemeral timer support, but timer changes without visible
// received messages may be confusing to the user.
if !*hidden
&& !location_kml_is
&& !is_mdn
&& (*chat_id).get_ephemeral_timer(context).await? != ephemeral_timer
{
if let Err(err) = (*chat_id)
.inner_set_ephemeral_timer(context, ephemeral_timer)
.await
{
warn!(
context,
"failed to modify timer for chat {}: {}", chat_id, err
);
} else if mime_parser.is_system_message != SystemMessage::EphemeralTimerChanged {
chat::add_info_msg(
context,
*chat_id,
stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await,
)
.await;
}
}
if mime_parser.is_system_message == SystemMessage::EphemeralTimerChanged {
set_better_msg(
mime_parser,
stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await,
);
// Do not delete the system message itself.
//
// This prevents confusion when timer is changed
// to 1 week, and then changed to 1 hour: after 1
// hour, only the message about the change to 1
// week is left.
ephemeral_timer = EphemeralTimer::Disabled;
}
// correct message_timestamp, it should not be used before,
// however, we cannot do this earlier as we need from_id to be set
let in_fresh = state == MessageState::InFresh;
let rcvd_timestamp = time();
let sort_timestamp = calc_sort_timestamp(context, *sent_timestamp, *chat_id, in_fresh).await;
*sent_timestamp = std::cmp::min(*sent_timestamp, rcvd_timestamp);
calc_timestamps(
context,
*chat_id,
from_id,
*sent_timestamp,
!seen,
&mut sort_timestamp,
sent_timestamp,
&mut rcvd_timestamp,
)
.await;
// unarchive chat
chat_id.unarchive(context).await?;
@@ -727,6 +637,7 @@ async fn add_parts(
let mut parts = std::mem::replace(&mut mime_parser.parts, Vec::new());
let server_folder = server_folder.as_ref().to_string();
let location_kml_is = mime_parser.location_kml.is_some();
let is_system_message = mime_parser.is_system_message;
let mime_headers = if save_mime_headers {
Some(String::from_utf8_lossy(imf_raw).to_string())
@@ -736,6 +647,7 @@ async fn add_parts(
let sent_timestamp = *sent_timestamp;
let is_hidden = *hidden;
let chat_id = *chat_id;
let is_mdn = !mime_parser.reports.is_empty();
// TODO: can this clone be avoided?
let rfc724_mid = rfc724_mid.to_string();
@@ -752,8 +664,8 @@ async fn add_parts(
"INSERT INTO msgs \
(rfc724_mid, server_folder, server_uid, chat_id, from_id, to_id, timestamp, \
timestamp_sent, timestamp_rcvd, type, state, msgrmsg, txt, txt_raw, param, \
bytes, hidden, mime_headers, mime_in_reply_to, mime_references, error, ephemeral_timer, ephemeral_timestamp) \
VALUES (?,?,?,?,?,?, ?,?,?,?,?,?, ?,?,?,?,?,?, ?,?, ?,?,?);",
bytes, hidden, mime_headers, mime_in_reply_to, mime_references) \
VALUES (?,?,?,?,?,?, ?,?,?,?,?,?, ?,?,?,?,?,?, ?,?);",
)?;
let is_location_kml = location_kml_is
@@ -775,15 +687,6 @@ async fn add_parts(
part.param.set_int(Param::Cmd, is_system_message as i32);
}
let ephemeral_timestamp = if in_fresh {
0
} else {
match ephemeral_timer {
EphemeralTimer::Disabled => 0,
EphemeralTimer::Enabled { duration } => rcvd_timestamp + i64::from(duration)
}
};
stmt.execute(paramsv![
rfc724_mid,
server_folder,
@@ -796,7 +699,7 @@ async fn add_parts(
rcvd_timestamp,
part.typ,
state,
is_dc_message,
msgrmsg,
part.msg,
// txt_raw might contain invalid utf8
txt_raw,
@@ -806,9 +709,6 @@ async fn add_parts(
mime_headers,
mime_in_reply_to,
mime_references,
part.error,
ephemeral_timer,
ephemeral_timestamp
])?;
drop(stmt);
@@ -864,17 +764,15 @@ async fn add_parts(
chat.update_param(context).await?;
Ok(())
}
if !is_mdn {
update_last_subject(context, chat_id, mime_parser)
.await
.unwrap_or_else(|e| {
warn!(
context,
"Could not update LastSubject of chat: {}",
e.to_string()
)
});
}
update_last_subject(context, chat_id, mime_parser)
.await
.unwrap_or_else(|e| {
warn!(
context,
"Could not update LastSubject of chat: {}",
e.to_string()
)
});
Ok(())
}
@@ -939,38 +837,41 @@ async fn save_locations(
}
}
async fn calc_sort_timestamp(
#[allow(clippy::too_many_arguments)]
async fn calc_timestamps(
context: &Context,
message_timestamp: i64,
chat_id: ChatId,
from_id: u32,
message_timestamp: i64,
is_fresh_msg: bool,
) -> i64 {
let mut sort_timestamp = message_timestamp;
// get newest non fresh message for this chat
// update sort_timestamp if less than that
sort_timestamp: &mut i64,
sent_timestamp: &mut i64,
rcvd_timestamp: &mut i64,
) {
*rcvd_timestamp = time();
*sent_timestamp = message_timestamp;
if *sent_timestamp > *rcvd_timestamp {
*sent_timestamp = *rcvd_timestamp
}
*sort_timestamp = message_timestamp;
if is_fresh_msg {
let last_msg_time: Option<i64> = context
.sql
.query_get_value(
context,
"SELECT MAX(timestamp) FROM msgs WHERE chat_id=? AND state>?",
paramsv![chat_id, MessageState::InFresh],
"SELECT MAX(timestamp) FROM msgs WHERE chat_id=? and from_id!=? AND timestamp>=?",
paramsv![chat_id, from_id as i32, *sort_timestamp],
)
.await;
if let Some(last_msg_time) = last_msg_time {
if last_msg_time > sort_timestamp {
sort_timestamp = last_msg_time;
if last_msg_time > 0 && *sort_timestamp <= last_msg_time {
*sort_timestamp = last_msg_time + 1;
}
}
}
if sort_timestamp >= dc_smeared_time(context).await {
sort_timestamp = dc_create_smeared_timestamp(context).await;
if *sort_timestamp >= dc_smeared_time(context).await {
*sort_timestamp = dc_create_smeared_timestamp(context).await;
}
sort_timestamp
}
/// This function tries extracts the group-id from the message and returns the
@@ -1196,19 +1097,23 @@ async fn create_or_lookup_group(
// again, check chat_id
if chat_id.is_special() {
if mime_parser.decrypting_failed {
// It is possible that the message was sent to a valid,
// yet unknown group, which was rejected because
// Chat-Group-Name, which is in the encrypted part, was
// not found. We can't create a properly named group in
// this case, so assign error message to 1:1 chat with the
// sender instead.
return Ok((ChatId::new(0), Blocked::Not));
return if group_explicitly_left {
Ok((ChatId::new(DC_CHAT_ID_TRASH), chat_id_blocked))
} else {
// The message was decrypted successfully, but contains a late "quit" or otherwise
// unwanted message.
return Ok((ChatId::new(DC_CHAT_ID_TRASH), chat_id_blocked));
}
create_or_lookup_adhoc_group(
context,
mime_parser,
allow_creation,
create_blocked,
from_id,
to_ids,
)
.await
.map_err(|err| {
warn!(context, "failed to create ad-hoc group: {:?}", err);
err
})
};
}
// We have a valid chat_id > DC_CHAT_ID_LAST_SPECIAL.
@@ -1393,10 +1298,12 @@ async fn create_or_lookup_adhoc_group(
// decrypted.
//
// The subject may be encrypted and contain a placeholder such
// as "...". It can also be a COI group, with encrypted
// Chat-Group-ID and incompatible Message-ID format.
//
// Instead, assign the message to 1:1 chat with the sender.
// as "...". Besides that, it is possible that the message was
// sent to a valid, yet unknown group, which was rejected
// because Chat-Group-Name, which is in the encrypted part,
// was not found. Generating a new ID in this case would
// result in creation of a twin group with a different group
// ID.
warn!(
context,
"not creating ad-hoc group for message that cannot be decrypted"
@@ -1528,7 +1435,6 @@ async fn create_adhoc_grp_id(context: &Context, member_ids: &[u32]) -> String {
hex_hash(&members)
}
#[allow(clippy::indexing_slicing)]
fn hex_hash(s: impl AsRef<str>) -> String {
let bytes = s.as_ref().as_bytes();
let result = Sha256::digest(bytes);
@@ -1581,7 +1487,7 @@ async fn search_chat_ids_by_contact_ids(
matches = 0;
mismatches = 0;
}
if contact_ids.get(matches) == Some(&contact_id) {
if matches < contact_ids.len() && contact_id == contact_ids[matches] {
matches += 1;
} else {
mismatches += 1;
@@ -1615,7 +1521,7 @@ async fn check_verified_properties(
// this check is skipped for SELF as there is no proper SELF-peerstate
// and results in group-splits otherwise.
if from_id != DC_CONTACT_ID_SELF {
let peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
let peerstate = Peerstate::from_addr(context, contact.get_addr()).await;
if peerstate.is_none()
|| contact.is_verified_ex(context, peerstate.as_ref()).await
@@ -1669,7 +1575,7 @@ async fn check_verified_properties(
context.is_self_addr(&to_addr).await
);
let mut is_verified = _is_verified != 0;
let peerstate = Peerstate::from_addr(context, &to_addr).await?;
let peerstate = Peerstate::from_addr(context, &to_addr).await;
// mark gossiped keys (if any) as verified
if mimeparser.gossipped_addr.contains(&to_addr) {
@@ -1709,11 +1615,10 @@ async fn check_verified_properties(
fn set_better_msg(mime_parser: &mut MimeMessage, better_msg: impl AsRef<str>) {
let msg = better_msg.as_ref();
if !msg.is_empty() {
if let Some(part) = mime_parser.parts.get_mut(0) {
if part.typ == Viewtype::Text {
part.msg = msg.to_string();
}
if !msg.is_empty() && !mime_parser.parts.is_empty() {
let part = &mut mime_parser.parts[0];
if part.typ == Viewtype::Text {
part.msg = msg.to_string();
}
}
}
@@ -1872,10 +1777,10 @@ fn dc_create_incoming_rfc724_mid(
#[cfg(test)]
mod tests {
use super::*;
use crate::chat::{ChatItem, ChatVisibility};
use crate::chat::ChatVisibility;
use crate::chatlist::Chatlist;
use crate::message::Message;
use crate::test_utils::*;
use crate::test_utils::{dummy_context, TestContext};
#[test]
fn test_hex_hash() {
@@ -1887,7 +1792,7 @@ mod tests {
#[async_std::test]
async fn test_grpid_simple() {
let context = TestContext::new().await;
let context = dummy_context().await;
let raw = b"From: hello\n\
Subject: outer-subject\n\
In-Reply-To: <lqkjwelq123@123123>\n\
@@ -1904,7 +1809,7 @@ mod tests {
#[async_std::test]
async fn test_grpid_from_multiple() {
let context = TestContext::new().await;
let context = dummy_context().await;
let raw = b"From: hello\n\
Subject: outer-subject\n\
In-Reply-To: <Gr.HcxyMARjyJy.9-qweqwe@asd.net>\n\
@@ -1941,7 +1846,7 @@ mod tests {
#[async_std::test]
async fn test_is_known_rfc724_mid() {
let t = TestContext::new().await;
let t = dummy_context().await;
let mut msg = Message::new(Viewtype::Text);
msg.text = Some("first message".to_string());
let msg_id = chat::add_device_msg(&t.ctx, None, Some(&mut msg))
@@ -1957,7 +1862,7 @@ mod tests {
#[async_std::test]
async fn test_is_msgrmsg_rfc724_mid() {
let t = TestContext::new().await;
let t = dummy_context().await;
let mut msg = Message::new(Viewtype::Text);
msg.text = Some("first message".to_string());
let msg_id = chat::add_device_msg(&t.ctx, None, Some(&mut msg))
@@ -1971,34 +1876,51 @@ mod tests {
assert!(!is_msgrmsg_rfc724_mid(&t.ctx, "nonexistant@message.id").await);
}
static MSGRMSG: &[u8] = b"From: Bob <bob@example.com>\n\
To: alice@example.com\n\
async fn configured_offline_context() -> TestContext {
let t = dummy_context().await;
t.ctx
.set_config(Config::Addr, Some("alice@example.org"))
.await
.unwrap();
t.ctx
.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
.await
.unwrap();
t.ctx
.set_config(Config::Configured, Some("1"))
.await
.unwrap();
t
}
static MSGRMSG: &[u8] = b"From: Bob <bob@example.org>\n\
To: alice@example.org\n\
Chat-Version: 1.0\n\
Subject: Chat: hello\n\
Message-ID: <Mr.1111@example.com>\n\
Message-ID: <Mr.1111@example.org>\n\
Date: Sun, 22 Mar 2020 22:37:55 +0000\n\
\n\
hello\n";
static ONETOONE_NOREPLY_MAIL: &[u8] = b"From: Bob <bob@example.com>\n\
To: alice@example.com\n\
static ONETOONE_NOREPLY_MAIL: &[u8] = b"From: Bob <bob@example.org>\n\
To: alice@example.org\n\
Subject: Chat: hello\n\
Message-ID: <2222@example.com>\n\
Message-ID: <2222@example.org>\n\
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
\n\
hello\n";
static GRP_MAIL: &[u8] = b"From: bob@example.com\n\
To: alice@example.com, claire@example.com\n\
static GRP_MAIL: &[u8] = b"From: bob@example.org\n\
To: alice@example.org, claire@example.org\n\
Subject: group with Alice, Bob and Claire\n\
Message-ID: <3333@example.com>\n\
Message-ID: <3333@example.org>\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello\n";
#[async_std::test]
async fn test_adhoc_group_show_chats_only() {
let t = TestContext::new_alice().await;
let t = configured_offline_context().await;
assert_eq!(t.ctx.get_config_int(Config::ShowEmails).await, 0);
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
@@ -2025,7 +1947,7 @@ mod tests {
#[async_std::test]
async fn test_adhoc_group_show_accepted_contact_unknown() {
let t = TestContext::new_alice().await;
let t = configured_offline_context().await;
t.ctx
.set_config(Config::ShowEmails, Some("1"))
.await
@@ -2041,12 +1963,12 @@ mod tests {
#[async_std::test]
async fn test_adhoc_group_show_accepted_contact_known() {
let t = TestContext::new_alice().await;
let t = configured_offline_context().await;
t.ctx
.set_config(Config::ShowEmails, Some("1"))
.await
.unwrap();
Contact::create(&t.ctx, "Bob", "bob@example.com")
Contact::create(&t.ctx, "Bob", "bob@example.org")
.await
.unwrap();
dc_receive_imf(&t.ctx, GRP_MAIL, "INBOX", 1, false)
@@ -2061,7 +1983,7 @@ mod tests {
#[async_std::test]
async fn test_adhoc_group_show_accepted_contact_accepted() {
let t = TestContext::new_alice().await;
let t = configured_offline_context().await;
t.ctx
.set_config(Config::ShowEmails, Some("1"))
.await
@@ -2107,7 +2029,7 @@ mod tests {
#[async_std::test]
async fn test_adhoc_group_show_all() {
let t = TestContext::new_alice().await;
let t = configured_offline_context().await;
t.ctx
.set_config(Config::ShowEmails, Some("2"))
.await
@@ -2132,7 +2054,7 @@ mod tests {
#[async_std::test]
async fn test_read_receipt_and_unarchive() {
// create alice's account
let t = TestContext::new_alice().await;
let t = configured_offline_context().await;
// create one-to-one with bob, archive one-to-one
let bob_id = Contact::create(&t.ctx, "bob", "bob@exampel.org")
@@ -2175,14 +2097,14 @@ mod tests {
dc_receive_imf(
&t.ctx,
format!(
"From: alice@example.com\n\
To: bob@example.com\n\
"From: alice@example.org\n\
To: bob@example.org\n\
Subject: foo\n\
Message-ID: <Gr.{}.12345678901@example.com>\n\
Message-ID: <Gr.{}.12345678901@example.org>\n\
Chat-Version: 1.0\n\
Chat-Group-ID: {}\n\
Chat-Group-Name: foo\n\
Chat-Disposition-Notification-To: alice@example.com\n\
Chat-Disposition-Notification-To: alice@example.org\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello\n",
@@ -2197,11 +2119,7 @@ mod tests {
.unwrap();
let msgs = chat::get_chat_msgs(&t.ctx, group_id, 0, None).await;
assert_eq!(msgs.len(), 1);
let msg_id = if let ChatItem::Message { msg_id } = msgs.first().unwrap() {
msg_id
} else {
panic!("Wrong item type");
};
let msg_id = msgs.first().unwrap();
let msg = message::Message::load_from_db(&t.ctx, msg_id.clone())
.await
.unwrap();
@@ -2215,12 +2133,12 @@ mod tests {
dc_receive_imf(
&t.ctx,
format!(
"From: bob@example.com\n\
To: alice@example.com\n\
"From: bob@example.org\n\
To: alice@example.org\n\
Subject: message opened\n\
Date: Sun, 22 Mar 2020 23:37:57 +0000\n\
Chat-Version: 1.0\n\
Message-ID: <Mr.12345678902@example.com>\n\
Message-ID: <Mr.12345678902@example.org>\n\
Content-Type: multipart/report; report-type=disposition-notification; boundary=\"SNIPP\"\n\
\n\
\n\
@@ -2234,9 +2152,9 @@ mod tests {
Content-Type: message/disposition-notification\n\
\n\
Reporting-UA: Delta Chat 1.28.0\n\
Original-Recipient: rfc822;bob@example.com\n\
Final-Recipient: rfc822;bob@example.com\n\
Original-Message-ID: <Gr.{}.12345678901@example.com>\n\
Original-Recipient: rfc822;bob@example.org\n\
Final-Recipient: rfc822;bob@example.org\n\
Original-Message-ID: <Gr.{}.12345678901@example.org>\n\
Disposition: manual-action/MDN-sent-automatically; displayed\n\
\n\
\n\
@@ -2276,7 +2194,7 @@ mod tests {
// are very rare, however, we have to add them to the database (they go to the
// "deaddrop" chat) to avoid a re-download from the server. See also [**]
let t = TestContext::new_alice().await;
let t = configured_offline_context().await;
let context = &t.ctx;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
@@ -2284,9 +2202,9 @@ mod tests {
dc_receive_imf(
context,
b"To: bob@example.com\n\
b"To: bob@example.org\n\
Subject: foo\n\
Message-ID: <3924@example.com>\n\
Message-ID: <3924@example.org>\n\
Chat-Version: 1.0\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
@@ -2305,7 +2223,7 @@ mod tests {
#[async_std::test]
async fn test_escaped_from() {
let t = TestContext::new_alice().await;
let t = configured_offline_context().await;
let contact_id = Contact::create(&t.ctx, "foobar", "foobar@example.com")
.await
.unwrap();
@@ -2315,9 +2233,9 @@ mod tests {
dc_receive_imf(
&t.ctx,
b"From: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= <foobar@example.com>\n\
To: alice@example.com\n\
To: alice@example.org\n\
Subject: foo\n\
Message-ID: <asdklfjjaweofi@example.com>\n\
Message-ID: <asdklfjjaweofi@example.org>\n\
Chat-Version: 1.0\n\
Chat-Disposition-Notification-To: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= <foobar@example.com>\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
@@ -2332,15 +2250,11 @@ mod tests {
.await
.unwrap()
.get_authname(),
"Имя, Фамилия",
"Фамилия Имя", // The name was "Имя, Фамилия" and ("lastname, firstname") and should be swapped to "firstname, lastname"
);
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await;
assert_eq!(msgs.len(), 1);
let msg_id = if let ChatItem::Message { msg_id } = msgs.first().unwrap() {
msg_id
} else {
panic!("Wrong item type");
};
let msg_id = msgs.first().unwrap();
let msg = message::Message::load_from_db(&t.ctx, msg_id.clone())
.await
.unwrap();
@@ -2351,7 +2265,7 @@ mod tests {
#[async_std::test]
async fn test_escaped_recipients() {
let t = TestContext::new_alice().await;
let t = configured_offline_context().await;
Contact::create(&t.ctx, "foobar", "foobar@example.com")
.await
.unwrap();
@@ -2365,10 +2279,10 @@ mod tests {
dc_receive_imf(
&t.ctx,
b"From: Foobar <foobar@example.com>\n\
To: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= alice@example.com\n\
To: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= alice@example.org\n\
Cc: =?utf-8?q?=3Ch2=3E?= <carl@host.tld>\n\
Subject: foo\n\
Message-ID: <asdklfjjaweofi@example.com>\n\
Message-ID: <asdklfjjaweofi@example.org>\n\
Chat-Version: 1.0\n\
Chat-Disposition-Notification-To: <foobar@example.com>\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
@@ -2399,7 +2313,7 @@ mod tests {
#[async_std::test]
async fn test_cc_to_contact() {
let t = TestContext::new_alice().await;
let t = configured_offline_context().await;
Contact::create(&t.ctx, "foobar", "foobar@example.com")
.await
.unwrap();
@@ -2417,10 +2331,10 @@ mod tests {
dc_receive_imf(
&t.ctx,
b"From: Foobar <foobar@example.com>\n\
To: alice@example.com\n\
To: alice@example.org\n\
Cc: Carl <carl@host.tld>\n\
Subject: foo\n\
Message-ID: <asdklfjjaweofi@example.com>\n\
Message-ID: <asdklfjjaweofi@example.org>\n\
Chat-Version: 1.0\n\
Chat-Disposition-Notification-To: <foobar@example.com>\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
@@ -2440,187 +2354,4 @@ mod tests {
"Carl"
);
}
#[async_std::test]
async fn test_parse_ndn_tiscali() {
test_parse_ndn(
"alice@tiscali.it",
"shenauithz@testrun.org",
"Mr.un2NYERi1RM.lbQ5F9q-QyJ@tiscali.it",
include_bytes!("../test-data/message/tiscali_ndn.eml"),
"",
)
.await;
}
#[async_std::test]
async fn test_parse_ndn_testrun() {
test_parse_ndn(
"alice@testrun.org",
"hcksocnsofoejx@five.chat",
"Mr.A7pTA5IgrUA.q4bP41vAJOp@testrun.org",
include_bytes!("../test-data/message/testrun_ndn.eml"),
"Undelivered Mail Returned to Sender This is the mail system at host hq5.merlinux.eu.\n\nI\'m sorry to have to inform you that your message could not\nbe delivered to one or more recipients. It\'s attached below.\n\nFor further assistance, please send mail to postmaster.\n\nIf you do so, please include this problem report. You can\ndelete your own text from the attached returned message.\n\n The mail system\n\n<hcksocnsofoejx@five.chat>: host mail.five.chat[195.62.125.103] said: 550 5.1.1\n <hcksocnsofoejx@five.chat>: Recipient address rejected: User unknown in\n virtual mailbox table (in reply to RCPT TO command)"
)
.await;
}
#[async_std::test]
async fn test_parse_ndn_yahoo() {
test_parse_ndn(
"alice@yahoo.com",
"haeclirth.sinoenrat@yahoo.com",
"1680295672.3657931.1591783872936@mail.yahoo.com",
include_bytes!("../test-data/message/yahoo_ndn.eml"),
"Failure Notice Sorry, we were unable to deliver your message to the following address.\n\n<haeclirth.sinoenrat@yahoo.com>:\n554: delivery error: dd Not a valid recipient - atlas117.free.mail.ne1.yahoo.com [...]"
)
.await;
}
#[async_std::test]
async fn test_parse_ndn_gmail() {
test_parse_ndn(
"alice@gmail.com",
"assidhfaaspocwaeofi@gmail.com",
"CABXKi8zruXJc_6e4Dr087H5wE7sLp+u250o0N2q5DdjF_r-8wg@mail.gmail.com",
include_bytes!("../test-data/message/gmail_ndn.eml"),
"Delivery Status Notification (Failure) ** Die Adresse wurde nicht gefunden **\n\nIhre Nachricht wurde nicht an assidhfaaspocwaeofi@gmail.com zugestellt, weil die Adresse nicht gefunden wurde oder keine E-Mails empfangen kann.\n\nHier erfahren Sie mehr: https://support.google.com/mail/?p=NoSuchUser\n\nAntwort:\n\n550 5.1.1 The email account that you tried to reach does not exist. Please try double-checking the recipient\'s email address for typos or unnecessary spaces. Learn more at https://support.google.com/mail/?p=NoSuchUser i18sor6261697wrs.38 - gsmtp",
)
.await;
}
#[async_std::test]
async fn test_parse_ndn_gmx() {
test_parse_ndn(
"alice@gmx.com",
"snaerituhaeirns@gmail.com",
"9c9c2a32-056b-3592-c372-d7e8f0bd4bc2@gmx.de",
include_bytes!("../test-data/message/gmx_ndn.eml"),
"Mail delivery failed: returning message to sender This message was created automatically by mail delivery software.\n\nA message that you sent could not be delivered to one or more of\nits recipients. This is a permanent error. The following address(es)\nfailed:\n\nsnaerituhaeirns@gmail.com:\nSMTP error from remote server for RCPT TO command, host: gmail-smtp-in.l.google.com (66.102.1.27) reason: 550-5.1.1 The email account that you tried to reach does not exist. Please\n try\n550-5.1.1 double-checking the recipient\'s email address for typos or\n550-5.1.1 unnecessary spaces. Learn more at\n550 5.1.1 https://support.google.com/mail/?p=NoSuchUser f6si2517766wmc.21\n9 - gsmtp [...]"
)
.await;
}
#[async_std::test]
async fn test_parse_ndn_posteo() {
test_parse_ndn(
"alice@posteo.org",
"hanerthaertidiuea@gmx.de",
"04422840-f884-3e37-5778-8192fe22d8e1@posteo.de",
include_bytes!("../test-data/message/posteo_ndn.eml"),
"Undelivered Mail Returned to Sender This is the mail system at host mout01.posteo.de.\n\nI\'m sorry to have to inform you that your message could not\nbe delivered to one or more recipients. It\'s attached below.\n\nFor further assistance, please send mail to postmaster.\n\nIf you do so, please include this problem report. You can\ndelete your own text from the attached returned message.\n\n The mail system\n\n<hanerthaertidiuea@gmx.de>: host mx01.emig.gmx.net[212.227.17.5] said: 550\n Requested action not taken: mailbox unavailable (in reply to RCPT TO\n command)",
)
.await;
}
// ndn = Non Delivery Notification
async fn test_parse_ndn(
self_addr: &str,
foreign_addr: &str,
rfc724_mid_outgoing: &str,
raw_ndn: &[u8],
error_msg: &str,
) {
let t = TestContext::new().await;
t.configure_addr(self_addr).await;
dc_receive_imf(
&t.ctx,
format!(
"From: {}\n\
To: {}\n\
Subject: foo\n\
Message-ID: <{}>\n\
Chat-Version: 1.0\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello\n",
self_addr, foreign_addr, rfc724_mid_outgoing
)
.as_bytes(),
"INBOX",
1,
false,
)
.await
.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
let msg_id = chats.get_msg_id(0).unwrap();
// Check that the ndn would be downloaded:
let headers = mailparse::parse_mail(raw_ndn).unwrap().headers;
assert!(
crate::imap::prefetch_should_download(&t.ctx, &headers, ShowEmails::Off)
.await
.unwrap()
);
dc_receive_imf(&t.ctx, raw_ndn, "INBOX", 1, false)
.await
.unwrap();
let msg = Message::load_from_db(&t.ctx, msg_id).await.unwrap();
assert_eq!(msg.state, MessageState::OutFailed);
assert_eq!(msg.error, error_msg);
}
#[async_std::test]
async fn test_parse_ndn_group_msg() {
let t = TestContext::new().await;
t.configure_addr("alice@gmail.com").await;
dc_receive_imf(
&t.ctx,
b"From: alice@gmail.com\n\
To: bob@example.com, assidhfaaspocwaeofi@gmail.com\n\
Subject: foo\n\
Message-ID: <CADWx9Cs32Wa7Gy-gM0bvbq54P_FEHe7UcsAV=yW7sVVW=fiMYQ@mail.gmail.com>\n\
Chat-Version: 1.0\n\
Chat-Group-ID: abcde\n\
Chat-Group-Name: foo\n\
Chat-Disposition-Notification-To: alice@example.com\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello\n",
"INBOX",
1,
false,
)
.await
.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
let msg_id = chats.get_msg_id(0).unwrap();
let raw = include_bytes!("../test-data/message/gmail_ndn_group.eml");
dc_receive_imf(&t.ctx, raw, "INBOX", 1, false)
.await
.unwrap();
let msg = Message::load_from_db(&t.ctx, msg_id).await.unwrap();
assert_eq!(msg.state, MessageState::OutFailed);
let msgs = chat::get_chat_msgs(&t.ctx, msg.chat_id, 0, None).await;
let msg_id = if let ChatItem::Message { msg_id } = msgs.last().unwrap() {
msg_id
} else {
panic!("Wrong item type");
};
let last_msg = Message::load_from_db(&t.ctx, *msg_id).await.unwrap();
assert_eq!(
last_msg.text,
Some(
t.ctx
.stock_string_repl_str(
StockMessage::FailedSendingTo,
"assidhfaaspocwaeofi@gmail.com",
)
.await,
)
);
assert_eq!(last_msg.from_id, DC_CONTACT_ID_INFO);
}
}

View File

@@ -4,7 +4,6 @@
use core::cmp::{max, min};
use std::borrow::Cow;
use std::fmt;
use std::io::Cursor;
use std::str::FromStr;
use std::time::{Duration, SystemTime};
@@ -23,7 +22,6 @@ pub(crate) fn dc_exactly_one_bit_set(v: i32) -> bool {
/// Shortens a string to a specified length and adds "[...]" to the
/// end of the shortened string.
#[allow(clippy::indexing_slicing)]
pub(crate) fn dc_truncate(buf: &str, approx_chars: usize) -> Cow<str> {
let ellipse = "[...]";
@@ -56,7 +54,6 @@ const COLORS: [u32; 16] = [
0xf2_30_30, 0x39_b2_49, 0xbb_24_3b, 0x96_40_78, 0x66_87_4f, 0x30_8a_b9, 0x12_7e_d0, 0xbe_45_0c,
];
#[allow(clippy::indexing_slicing)]
pub(crate) fn dc_str_to_color(s: impl AsRef<str>) -> u32 {
let str_lower = s.as_ref().to_lowercase();
let mut checksum = 0;
@@ -201,7 +198,7 @@ fn encode_66bits_as_base64(v1: u32, v2: u32, fill: u32) -> String {
pub(crate) fn dc_create_outgoing_rfc724_mid(grpid: Option<&str>, from_addr: &str) -> String {
let hostname = from_addr
.find('@')
.and_then(|k| from_addr.get(k..))
.map(|k| &from_addr[k..])
.unwrap_or("@nohost");
match grpid {
Some(grpid) => format!("Gr.{}.{}{}", grpid, dc_create_id(), hostname),
@@ -243,9 +240,9 @@ pub fn dc_get_filesuffix_lc(path_filename: impl AsRef<str>) -> Option<String> {
/// Returns the `(width, height)` of the given image buffer.
pub fn dc_get_filemeta(buf: &[u8]) -> Result<(u32, u32), Error> {
let image = image::io::Reader::new(Cursor::new(buf)).with_guessed_format()?;
let dimensions = image.into_dimensions()?;
Ok(dimensions)
let meta = image_meta::load_from_buf(buf)?;
Ok((meta.dimensions.width, meta.dimensions.height))
}
/// Expand paths relative to $BLOBDIR into absolute paths.
@@ -777,7 +774,7 @@ mod tests {
#[async_std::test]
async fn test_file_handling() {
let t = TestContext::new().await;
let t = dummy_context().await;
let context = &t.ctx;
macro_rules! dc_file_exist {
($ctx:expr, $fname:expr) => {
@@ -856,7 +853,7 @@ mod tests {
#[async_std::test]
async fn test_create_smeared_timestamp() {
let t = TestContext::new().await;
let t = dummy_context().await;
assert_ne!(
dc_create_smeared_timestamp(&t.ctx).await,
dc_create_smeared_timestamp(&t.ctx).await
@@ -872,7 +869,7 @@ mod tests {
#[async_std::test]
async fn test_create_smeared_timestamps() {
let t = TestContext::new().await;
let t = dummy_context().await;
let count = MAX_SECONDS_TO_LEND_FROM_FUTURE - 1;
let start = dc_create_smeared_timestamps(&t.ctx, count as usize).await;
let next = dc_smeared_time(&t.ctx).await;
@@ -916,22 +913,4 @@ mod tests {
"3h 1m 0s"
);
}
#[test]
fn test_get_filemeta() {
let data = include_bytes!("../test-data/image/avatar900x900.png");
let (w, h) = dc_get_filemeta(data).unwrap();
assert_eq!(w, 900);
assert_eq!(h, 900);
let data = include_bytes!("../test-data/image/avatar1000x1000.jpg");
let (w, h) = dc_get_filemeta(data).unwrap();
assert_eq!(w, 1000);
assert_eq!(h, 1000);
let data = include_bytes!("../test-data/image/image100x50.gif");
let (w, h) = dc_get_filemeta(data).unwrap();
assert_eq!(w, 100);
assert_eq!(h, 50);
}
}

View File

@@ -46,12 +46,6 @@ pub fn dehtml(buf: &str) -> String {
Ok(quick_xml::events::Event::End(ref e)) => dehtml_endtag_cb(e, &mut dehtml),
Ok(quick_xml::events::Event::Text(ref e)) => dehtml_text_cb(e, &mut dehtml),
Ok(quick_xml::events::Event::CData(ref e)) => dehtml_cdata_cb(e, &mut dehtml),
Ok(quick_xml::events::Event::Empty(ref e)) => {
// Handle empty tags as a start tag immediately followed by end tag.
// For example, `<p/>` is treated as `<p></p>`.
dehtml_starttag_cb(e, &mut dehtml, &reader);
dehtml_endtag_cb(&BytesEnd::borrowed(e.name()), &mut dehtml);
}
Err(e) => {
eprintln!(
"Parse html error: Error at position {}: {:?}",
@@ -188,10 +182,6 @@ mod tests {
("&amp; bar", "& bar"),
// Note missing '
("<a href='/foo.png>Hi</a> ", ""),
(
"<a href='https://get.delta.chat/'/>",
"[](https://get.delta.chat/)",
),
("", ""),
];
for (input, output) in cases {
@@ -201,10 +191,10 @@ mod tests {
#[test]
fn test_dehtml_parse_br() {
let html = "\r\r\nline1<br>\r\n\r\n\r\rline2<br/>line3\n\r";
let html = "\r\r\nline1<br>\r\n\r\n\r\rline2\n\r";
let plain = dehtml(html);
assert_eq!(plain, "line1\n\r\r\rline2\nline3");
assert_eq!(plain, "line1\n\r\r\rline2");
}
#[test]

View File

@@ -115,14 +115,6 @@ impl EncryptHelper {
}
}
/// Tries to decrypt a message, but only if it is structured as an
/// Autocrypt message.
///
/// Returns decrypted body and a set of valid signature fingerprints
/// if successful.
///
/// If the message is wrongly signed, this will still return the decrypted
/// message but the HashSet will be empty.
pub async fn try_decrypt(
context: &Context,
mail: &ParsedMail<'_>,
@@ -140,7 +132,7 @@ pub async fn try_decrypt(
let autocryptheader = Aheader::from_headers(context, &from, &mail.headers);
if message_time > 0 {
peerstate = Peerstate::from_addr(context, &from).await?;
peerstate = Peerstate::from_addr(context, &from).await;
if let Some(ref mut peerstate) = peerstate {
if let Some(ref header) = autocryptheader {
@@ -163,7 +155,7 @@ pub async fn try_decrypt(
let mut signatures = HashSet::default();
if peerstate.as_ref().map(|p| p.last_seen).unwrap_or_else(|| 0) == 0 {
peerstate = Peerstate::from_addr(&context, &from).await?;
peerstate = Peerstate::from_addr(&context, &from).await;
}
if let Some(peerstate) = peerstate {
if peerstate.degrade_event.is_some() {
@@ -195,23 +187,24 @@ fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Result<&'a ParsedMail
"Not a multipart/encrypted message: {}",
mail.ctype.mimetype
);
if let [first_part, second_part] = &mail.subparts[..] {
ensure!(
first_part.ctype.mimetype == "application/pgp-encrypted",
"Invalid Autocrypt Level 1 version part: {:?}",
first_part.ctype,
);
ensure!(
mail.subparts.len() == 2,
"Invalid Autocrypt Level 1 Mime Parts"
);
ensure!(
second_part.ctype.mimetype == "application/octet-stream",
"Invalid Autocrypt Level 1 encrypted part: {:?}",
second_part.ctype
);
ensure!(
mail.subparts[0].ctype.mimetype == "application/pgp-encrypted",
"Invalid Autocrypt Level 1 version part: {:?}",
mail.subparts[0].ctype,
);
Ok(second_part)
} else {
bail!("Invalid Autocrypt Level 1 Mime Parts")
}
ensure!(
mail.subparts[1].ctype.mimetype == "application/octet-stream",
"Invalid Autocrypt Level 1 encrypted part: {:?}",
mail.subparts[1].ctype
);
Ok(&mail.subparts[1])
}
async fn decrypt_if_autocrypt_message<'a>(
@@ -221,6 +214,13 @@ async fn decrypt_if_autocrypt_message<'a>(
public_keyring_for_validate: Keyring<SignedPublicKey>,
ret_valid_signatures: &mut HashSet<Fingerprint>,
) -> Result<Option<Vec<u8>>> {
// The returned bool is true if we detected an Autocrypt-encrypted
// message and successfully decrypted it. Decryption then modifies the
// passed in mime structure in place. The returned bool is false
// if it was not an Autocrypt message.
//
// Errors are returned for failures related to decryption of AC-messages.
let encrypted_data_part = match get_autocrypt_mime(mail) {
Err(_) => {
// not an autocrypt mime message, abort and ignore
@@ -260,16 +260,13 @@ async fn decrypt_part(
)
.await?;
// If the message was wrongly or not signed, still return the plain text.
// The caller has to check the signatures then.
ensure!(!ret_valid_signatures.is_empty(), "no valid signatures");
return Ok(Some(plain));
}
Ok(None)
}
#[allow(clippy::indexing_slicing)]
fn has_decrypted_pgp_armor(input: &[u8]) -> bool {
if let Some(index) = input.iter().position(|b| *b > b' ') {
if input.len() - index > 26 {
@@ -330,14 +327,14 @@ mod tests {
#[async_std::test]
async fn test_prexisting() {
let t = TestContext::new().await;
let test_addr = t.configure_alice().await;
let t = dummy_context().await;
let test_addr = configure_alice_keypair(&t.ctx).await;
assert_eq!(ensure_secret_key_exists(&t.ctx).await.unwrap(), test_addr);
}
#[async_std::test]
async fn test_not_configured() {
let t = TestContext::new().await;
let t = dummy_context().await;
assert!(ensure_secret_key_exists(&t.ctx).await.is_err());
}
}

View File

@@ -1,528 +0,0 @@
//! # Ephemeral messages
//!
//! Ephemeral messages are messages that have an Ephemeral-Timer
//! header attached to them, which specifies time in seconds after
//! which the message should be deleted both from the device and from
//! the server. The timer is started when the message is marked as
//! seen, which usually happens when its contents is displayed on
//! device screen.
//!
//! Each chat, including 1:1, group chats and "saved messages" chat,
//! has its own ephemeral timer setting, which is applied to all
//! messages sent to the chat. The setting is synchronized to all the
//! devices participating in the chat by applying the timer value from
//! all received messages, including BCC-self ones, to the chat. This
//! way the setting is eventually synchronized among all participants.
//!
//! When user changes ephemeral timer setting for the chat, a system
//! message is automatically sent to update the setting for all
//! participants. This allows changing the setting for a chat like any
//! group chat setting, e.g. name and avatar, without the need to
//! write an actual message.
//!
//! ## Device settings
//!
//! In addition to per-chat ephemeral message setting, each device has
//! two global user-configured settings that complement per-chat
//! settings: `delete_device_after` and `delete_server_after`. These
//! settings are not synchronized among devices and apply to all
//! messages known to the device, including messages sent or received
//! before configuring the setting.
//!
//! `delete_device_after` configures the maximum time device is
//! storing the messages locally. `delete_server_after` configures the
//! time after which device will delete the messages it knows about
//! from the server.
//!
//! ## How messages are deleted
//!
//! When the message is deleted locally, its contents is removed and
//! it is moved to the trash chat. This database entry is then used to
//! track the Message-ID and corresponding IMAP folder and UID until
//! the message is deleted from the server. Vice versa, when device
//! deletes the message from the server, it removes IMAP folder and
//! UID information, but keeps the message contents. When database
//! entry is both moved to trash chat and does not contain UID
//! information, it is deleted from the database, leaving no trace of
//! the message.
//!
//! ## When messages are deleted
//!
//! Local deletion happens when the chatlist or chat is loaded. A
//! `MsgsChanged` event is emitted when a message deletion is due, to
//! make UI reload displayed messages and cause actual deletion.
//!
//! Server deletion happens by generating IMAP deletion jobs based on
//! the database entries which are expired either according to their
//! ephemeral message timers or global `delete_server_after` setting.
use crate::chat::{lookup_by_contact_id, send_msg, ChatId};
use crate::constants::{
Viewtype, DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_SELF,
};
use crate::context::Context;
use crate::dc_tools::time;
use crate::error::{ensure, Error};
use crate::events::Event;
use crate::message::{Message, MessageState, MsgId};
use crate::mimeparser::SystemMessage;
use crate::sql;
use crate::stock::StockMessage;
use async_std::task;
use serde::{Deserialize, Serialize};
use std::convert::{TryFrom, TryInto};
use std::num::ParseIntError;
use std::str::FromStr;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
pub enum Timer {
Disabled,
Enabled { duration: u32 },
}
impl Timer {
pub fn to_u32(self) -> u32 {
match self {
Self::Disabled => 0,
Self::Enabled { duration } => duration,
}
}
pub fn from_u32(duration: u32) -> Self {
if duration == 0 {
Self::Disabled
} else {
Self::Enabled { duration }
}
}
}
impl Default for Timer {
fn default() -> Self {
Self::Disabled
}
}
impl ToString for Timer {
fn to_string(&self) -> String {
self.to_u32().to_string()
}
}
impl FromStr for Timer {
type Err = ParseIntError;
fn from_str(input: &str) -> Result<Timer, ParseIntError> {
input.parse::<u32>().map(Self::from_u32)
}
}
impl rusqlite::types::ToSql for Timer {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let val = rusqlite::types::Value::Integer(match self {
Self::Disabled => 0,
Self::Enabled { duration } => i64::from(*duration),
});
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
}
impl rusqlite::types::FromSql for Timer {
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
i64::column_result(value).and_then(|value| {
if value == 0 {
Ok(Self::Disabled)
} else if let Ok(duration) = u32::try_from(value) {
Ok(Self::Enabled { duration })
} else {
Err(rusqlite::types::FromSqlError::OutOfRange(value))
}
})
}
}
impl ChatId {
/// Get ephemeral message timer value in seconds.
pub async fn get_ephemeral_timer(self, context: &Context) -> Result<Timer, Error> {
let timer = context
.sql
.query_get_value_result(
"SELECT ephemeral_timer FROM chats WHERE id=?;",
paramsv![self],
)
.await?;
Ok(timer.unwrap_or_default())
}
/// Set ephemeral timer value without sending a message.
///
/// Used when a message arrives indicating that someone else has
/// changed the timer value for a chat.
pub(crate) async fn inner_set_ephemeral_timer(
self,
context: &Context,
timer: Timer,
) -> Result<(), Error> {
ensure!(!self.is_special(), "Invalid chat ID");
context
.sql
.execute(
"UPDATE chats
SET ephemeral_timer=?
WHERE id=?;",
paramsv![timer, self],
)
.await?;
context.emit_event(Event::ChatEphemeralTimerModified {
chat_id: self,
timer,
});
Ok(())
}
/// Set ephemeral message timer value in seconds.
///
/// If timer value is 0, disable ephemeral message timer.
pub async fn set_ephemeral_timer(self, context: &Context, timer: Timer) -> Result<(), Error> {
if timer == self.get_ephemeral_timer(context).await? {
return Ok(());
}
self.inner_set_ephemeral_timer(context, timer).await?;
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(stock_ephemeral_timer_changed(context, timer, DC_CONTACT_ID_SELF).await);
msg.param.set_cmd(SystemMessage::EphemeralTimerChanged);
if let Err(err) = send_msg(context, self, &mut msg).await {
error!(
context,
"Failed to send a message about ephemeral message timer change: {:?}", err
);
}
Ok(())
}
}
/// Returns a stock message saying that ephemeral timer is changed to `timer` by `from_id`.
pub(crate) async fn stock_ephemeral_timer_changed(
context: &Context,
timer: Timer,
from_id: u32,
) -> String {
let stock_message = match timer {
Timer::Disabled => StockMessage::MsgEphemeralTimerDisabled,
Timer::Enabled { duration } => match duration {
60 => StockMessage::MsgEphemeralTimerMinute,
3600 => StockMessage::MsgEphemeralTimerHour,
86400 => StockMessage::MsgEphemeralTimerDay,
604_800 => StockMessage::MsgEphemeralTimerWeek,
2_419_200 => StockMessage::MsgEphemeralTimerFourWeeks,
_ => StockMessage::MsgEphemeralTimerEnabled,
},
};
context
.stock_system_msg(stock_message, timer.to_string(), "", from_id)
.await
}
impl MsgId {
/// Returns ephemeral message timer value for the message.
pub(crate) async fn ephemeral_timer(self, context: &Context) -> crate::sql::Result<Timer> {
let res = match context
.sql
.query_get_value_result(
"SELECT ephemeral_timer FROM msgs WHERE id=?",
paramsv![self],
)
.await?
{
None | Some(0) => Timer::Disabled,
Some(duration) => Timer::Enabled { duration },
};
Ok(res)
}
/// Starts ephemeral message timer for the message if it is not started yet.
pub(crate) async fn start_ephemeral_timer(self, context: &Context) -> crate::sql::Result<()> {
if let Timer::Enabled { duration } = self.ephemeral_timer(context).await? {
let ephemeral_timestamp = time() + i64::from(duration);
context
.sql
.execute(
"UPDATE msgs SET ephemeral_timestamp = ? \
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?) \
AND id = ?",
paramsv![ephemeral_timestamp, ephemeral_timestamp, self],
)
.await?;
schedule_ephemeral_task(context).await;
}
Ok(())
}
}
/// Deletes messages which are expired according to
/// `delete_device_after` setting or `ephemeral_timestamp` column.
///
/// Returns true if any message is deleted, so caller can emit
/// MsgsChanged event. If nothing has been deleted, returns
/// false. This function does not emit the MsgsChanged event itself,
/// because it is also called when chatlist is reloaded, and emitting
/// MsgsChanged there will cause infinite reload loop.
pub(crate) async fn delete_expired_messages(context: &Context) -> Result<bool, Error> {
let mut updated = context
.sql
.execute(
"UPDATE msgs \
SET txt = 'DELETED', chat_id = ? \
WHERE \
ephemeral_timestamp != 0 \
AND ephemeral_timestamp < ? \
AND chat_id != ?",
paramsv![DC_CHAT_ID_TRASH, time(), DC_CHAT_ID_TRASH],
)
.await?
> 0;
if let Some(delete_device_after) = context.get_config_delete_device_after().await {
let self_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
.await
.unwrap_or_default()
.0;
let device_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
.await
.unwrap_or_default()
.0;
let threshold_timestamp = time() - delete_device_after;
// Delete expired messages
//
// Only update the rows that have to be updated, to avoid emitting
// unnecessary "chat modified" events.
let rows_modified = context
.sql
.execute(
"UPDATE msgs \
SET txt = 'DELETED', chat_id = ? \
WHERE timestamp < ? \
AND chat_id > ? \
AND chat_id != ? \
AND chat_id != ?",
paramsv![
DC_CHAT_ID_TRASH,
threshold_timestamp,
DC_CHAT_ID_LAST_SPECIAL,
self_chat_id,
device_chat_id
],
)
.await?;
updated |= rows_modified > 0;
}
schedule_ephemeral_task(context).await;
Ok(updated)
}
/// Schedule a task to emit MsgsChanged event when the next local
/// deletion happens. Existing task is cancelled to make sure at most
/// one such task is scheduled at a time.
///
/// UI is expected to reload the chatlist or the chat in response to
/// MsgsChanged event, this will trigger actual deletion.
///
/// This takes into account only per-chat timeouts, because global device
/// timeouts are at least one hour long and deletion is triggered often enough
/// by user actions.
pub async fn schedule_ephemeral_task(context: &Context) {
let ephemeral_timestamp: Option<i64> = match context
.sql
.query_get_value_result(
"SELECT ephemeral_timestamp \
FROM msgs \
WHERE ephemeral_timestamp != 0 \
AND chat_id != ? \
ORDER BY ephemeral_timestamp ASC \
LIMIT 1",
paramsv![DC_CHAT_ID_TRASH], // Trash contains already deleted messages, skip them
)
.await
{
Err(err) => {
warn!(context, "Can't calculate next ephemeral timeout: {}", err);
return;
}
Ok(ephemeral_timestamp) => ephemeral_timestamp,
};
// Cancel existing task, if any
if let Some(ephemeral_task) = context.ephemeral_task.write().await.take() {
ephemeral_task.cancel().await;
}
if let Some(ephemeral_timestamp) = ephemeral_timestamp {
let now = SystemTime::now();
let until = UNIX_EPOCH
+ Duration::from_secs(ephemeral_timestamp.try_into().unwrap_or(u64::MAX))
+ Duration::from_secs(1);
if let Ok(duration) = until.duration_since(now) {
// Schedule a task, ephemeral_timestamp is in the future
let context1 = context.clone();
let ephemeral_task = task::spawn(async move {
async_std::task::sleep(duration).await;
emit_event!(
context1,
Event::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0)
}
);
});
*context.ephemeral_task.write().await = Some(ephemeral_task);
} else {
// Emit event immediately
emit_event!(
context,
Event::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0)
}
);
}
}
}
/// Returns ID of any expired message that should be deleted from the server.
///
/// It looks up the trash chat too, to find messages that are already
/// deleted locally, but not deleted on the server.
pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> sql::Result<Option<MsgId>> {
let now = time();
let threshold_timestamp = match context.get_config_delete_server_after().await {
None => 0,
Some(delete_server_after) => now - delete_server_after,
};
context
.sql
.query_row_optional(
"SELECT id FROM msgs \
WHERE ( \
timestamp < ? \
OR (ephemeral_timestamp != 0 AND ephemeral_timestamp < ?) \
) \
AND server_uid != 0 \
LIMIT 1",
paramsv![threshold_timestamp, now],
|row| row.get::<_, MsgId>(0),
)
.await
}
/// Start ephemeral timers for seen messages if they are not started
/// yet.
///
/// It is possible that timers are not started due to a missing or
/// failed `MsgId.start_ephemeral_timer()` call, either in the current
/// or previous version of Delta Chat.
///
/// This function is supposed to be called in the background,
/// e.g. from housekeeping task.
pub(crate) async fn start_ephemeral_timers(context: &Context) -> sql::Result<()> {
context
.sql
.execute(
"UPDATE msgs \
SET ephemeral_timestamp = ? + ephemeral_timer \
WHERE ephemeral_timer > 0 \
AND ephemeral_timestamp = 0 \
AND state NOT IN (?, ?, ?)",
paramsv![
time(),
MessageState::InFresh,
MessageState::InNoticed,
MessageState::OutDraft
],
)
.await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::*;
#[async_std::test]
async fn test_stock_ephemeral_messages() {
let context = TestContext::new().await.ctx;
assert_eq!(
stock_ephemeral_timer_changed(&context, Timer::Disabled, DC_CONTACT_ID_SELF).await,
"Message deletion timer is disabled by me."
);
assert_eq!(
stock_ephemeral_timer_changed(&context, Timer::Disabled, 0).await,
"Message deletion timer is disabled."
);
assert_eq!(
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 1 }, 0).await,
"Message deletion timer is set to 1 s."
);
assert_eq!(
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 30 }, 0).await,
"Message deletion timer is set to 30 s."
);
assert_eq!(
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 60 }, 0).await,
"Message deletion timer is set to 1 minute."
);
assert_eq!(
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 60 * 60 }, 0).await,
"Message deletion timer is set to 1 hour."
);
assert_eq!(
stock_ephemeral_timer_changed(
&context,
Timer::Enabled {
duration: 24 * 60 * 60
},
0
)
.await,
"Message deletion timer is set to 1 day."
);
assert_eq!(
stock_ephemeral_timer_changed(
&context,
Timer::Enabled {
duration: 7 * 24 * 60 * 60
},
0
)
.await,
"Message deletion timer is set to 1 week."
);
assert_eq!(
stock_ephemeral_timer_changed(
&context,
Timer::Enabled {
duration: 4 * 7 * 24 * 60 * 60
},
0
)
.await,
"Message deletion timer is set to 4 weeks."
);
}
}

View File

@@ -5,7 +5,6 @@ use async_std::sync::{channel, Receiver, Sender, TrySendError};
use strum::EnumProperty;
use crate::chat::ChatId;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::message::MsgId;
#[derive(Debug)]
@@ -140,6 +139,7 @@ pub enum Event {
/// Network errors should be reported to users in a non-disturbing way,
/// however, as network errors may come in a sequence,
/// it is not useful to raise each an every error to the user.
/// For this purpose, data1 is set to 1 if the error is probably worth reporting.
///
/// Moreover, if the UI detects that the device is offline,
/// it is probably more useful to report this to the user
@@ -189,19 +189,9 @@ pub enum Event {
/// Or the verify state of a chat has changed.
/// See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
/// and dc_remove_contact_from_chat().
///
/// This event does not include ephemeral timer modification, which
/// is a separate event.
#[strum(props(id = "2020"))]
ChatModified(ChatId),
/// Chat ephemeral timer changed.
#[strum(props(id = "2021"))]
ChatEphemeralTimerModified {
chat_id: ChatId,
timer: EphemeralTimer,
},
/// Contact(s) created, renamed, blocked or deleted.
///
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.

View File

@@ -21,7 +21,6 @@ pub enum HeaderDef {
References,
InReplyTo,
Precedence,
ContentType,
ChatVersion,
ChatGroupId,
ChatGroupName,
@@ -35,7 +34,6 @@ pub enum HeaderDef {
ChatContent,
ChatDuration,
ChatDispositionNotificationTo,
ChatWebrtcRoom,
Autocrypt,
AutocryptSetupMessage,
SecureJoin,
@@ -43,7 +41,6 @@ pub enum HeaderDef {
SecureJoinFingerprint,
SecureJoinInvitenumber,
SecureJoinAuth,
EphemeralTimer,
_TestHeader,
}

View File

@@ -7,7 +7,7 @@ use async_imap::{
use async_std::net::{self, TcpStream};
use super::session::Session;
use crate::login_param::dc_build_tls;
use crate::login_param::{dc_build_tls, CertificateChecks};
use super::session::SessionStream;
@@ -78,10 +78,10 @@ impl Client {
pub async fn connect_secure<A: net::ToSocketAddrs, S: AsRef<str>>(
addr: A,
domain: S,
strict_tls: bool,
certificate_checks: CertificateChecks,
) -> ImapResult<Self> {
let stream = TcpStream::connect(addr).await?;
let tls = dc_build_tls(strict_tls);
let tls = dc_build_tls(certificate_checks);
let tls_stream: Box<dyn SessionStream> =
Box::new(tls.connect(domain.as_ref(), stream).await?);
let mut client = ImapClient::new(tls_stream);
@@ -118,12 +118,16 @@ impl Client {
})
}
pub async fn secure<S: AsRef<str>>(self, domain: S, strict_tls: bool) -> ImapResult<Client> {
pub async fn secure<S: AsRef<str>>(
self,
domain: S,
certificate_checks: CertificateChecks,
) -> ImapResult<Client> {
if self.is_secure {
Ok(self)
} else {
let Client { mut inner, .. } = self;
let tls = dc_build_tls(strict_tls);
let tls = dc_build_tls(certificate_checks);
inner.run_command_and_check_ok("STARTTLS", None).await?;
let stream = inner.into_inner();

View File

@@ -1,7 +1,6 @@
use super::Imap;
use async_imap::extensions::idle::IdleResponse;
use async_imap::types::UnsolicitedResponse;
use async_std::prelude::*;
use std::time::{Duration, SystemTime};
@@ -14,19 +13,19 @@ type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("IMAP IDLE protocol failed to init/complete: {0}")]
#[error("IMAP IDLE protocol failed to init/complete")]
IdleProtocolFailed(#[from] async_imap::error::Error),
#[error("IMAP IDLE protocol timed out: {0}")]
#[error("IMAP IDLE protocol timed out")]
IdleTimeout(#[from] async_std::future::TimeoutError),
#[error("IMAP server does not have IDLE capability")]
IdleAbilityMissing,
#[error("IMAP select folder error: {0}")]
#[error("IMAP select folder error")]
SelectFolderError(#[from] select_folder::Error),
#[error("Setup handle error: {0}")]
#[error("Setup handle error")]
SetupHandleError(#[from] super::Error),
}
@@ -49,27 +48,11 @@ impl Imap {
self.select_folder(context, watch_folder.clone()).await?;
let session = self.session.take();
let timeout = Duration::from_secs(23 * 60);
let mut info = Default::default();
if let Some(session) = self.session.take() {
// if we have unsolicited responses we directly return
let mut unsolicited_exists = false;
while let Ok(response) = session.unsolicited_responses.try_recv() {
match response {
UnsolicitedResponse::Exists(_) => {
warn!(context, "skip idle, got unsolicited EXISTS {:?}", response);
unsolicited_exists = true;
}
_ => info!(context, "ignoring unsolicited response {:?}", response),
}
}
if unsolicited_exists {
self.session = Some(session);
return Ok(info);
}
if let Some(session) = session {
let mut handle = session.idle();
if let Err(err) = handle.init().await {
return Err(Error::IdleProtocolFailed(err));
@@ -82,43 +65,68 @@ impl Imap {
Interrupt(InterruptInfo),
}
info!(context, "Idle entering wait-on-remote state");
let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(async {
let probe_network = self.idle_interrupt.recv().await;
// cancel imap idle connection properly
if self.skip_next_idle_wait {
// interrupt_idle has happened before we
// provided self.interrupt
self.skip_next_idle_wait = false;
drop(idle_wait);
drop(interrupt);
Ok(Event::Interrupt(probe_network.unwrap_or_default()))
});
info!(context, "Idle wait was skipped");
} else {
info!(context, "Idle entering wait-on-remote state");
let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(
self.idle_interrupt.recv().map(|probe_network| {
Ok(Event::Interrupt(probe_network.unwrap_or_default()))
}),
);
match fut.await {
Ok(Event::IdleResponse(IdleResponse::NewData(x))) => {
info!(context, "Idle has NewData {:?}", x);
}
Ok(Event::IdleResponse(IdleResponse::Timeout)) => {
info!(context, "Idle-wait timeout or interruption");
}
Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => {
info!(context, "Idle wait was interrupted");
}
Ok(Event::Interrupt(i)) => {
info = i;
info!(context, "Idle wait was interrupted");
}
Err(err) => {
warn!(context, "Idle wait errored: {:?}", err);
match fut.await {
Ok(Event::IdleResponse(IdleResponse::NewData(_))) => {
info!(context, "Idle has NewData");
}
// TODO: idle_wait does not distinguish manual interrupts
// from Timeouts if we would know it's a Timeout we could bail
// directly and reconnect .
Ok(Event::IdleResponse(IdleResponse::Timeout)) => {
info!(context, "Idle-wait timeout or interruption");
}
Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => {
info!(context, "Idle wait was interrupted");
}
Ok(Event::Interrupt(i)) => {
info = i;
info!(context, "Idle wait was interrupted");
}
Err(err) => {
warn!(context, "Idle wait errored: {:?}", err);
}
}
}
let session = handle
// if we can't properly terminate the idle
// protocol let's break the connection.
let res = handle
.done()
.timeout(Duration::from_secs(15))
.await
.map_err(Error::IdleTimeout)??;
self.session = Some(Session { inner: session });
} else {
warn!(context, "Attempted to idle without a session");
.map_err(|err| {
self.trigger_reconnect();
Error::IdleTimeout(err)
})?;
match res {
Ok(session) => {
self.session = Some(Session { inner: session });
}
Err(err) => {
// if we cannot terminate IDLE it probably
// means that we waited long (with idle_wait)
// but the network went away/changed
self.trigger_reconnect();
return Err(Error::IdleProtocolFailed(err));
}
}
}
Ok(info)
@@ -140,66 +148,73 @@ impl Imap {
return self.idle_interrupt.recv().await.unwrap_or_default();
}
// check every minute if there are new messages
// TODO: grow sleep durations / make them more flexible
let mut interval = async_std::stream::interval(Duration::from_secs(60));
let mut info: InterruptInfo = Default::default();
if self.skip_next_idle_wait {
// interrupt_idle has happened before we
// provided self.interrupt
self.skip_next_idle_wait = false;
info!(context, "fake-idle wait was skipped");
} else {
// check every minute if there are new messages
// TODO: grow sleep durations / make them more flexible
let mut interval = async_std::stream::interval(Duration::from_secs(60));
enum Event {
Tick,
Interrupt(InterruptInfo),
}
// loop until we are interrupted or if we fetched something
let info = loop {
use futures::future::FutureExt;
match interval
.next()
.map(|_| Event::Tick)
.race(
self.idle_interrupt
.recv()
.map(|probe_network| Event::Interrupt(probe_network.unwrap_or_default())),
)
.await
{
Event::Tick => {
// try to connect with proper login params
// (setup_handle_if_needed might not know about them if we
// never successfully connected)
if let Err(err) = self.connect_configured(context).await {
warn!(context, "fake_idle: could not connect: {}", err);
continue;
}
if self.config.can_idle {
// we only fake-idled because network was gone during IDLE, probably
break InterruptInfo::new(false, None);
}
info!(context, "fake_idle is connected");
// we are connected, let's see if fetching messages results
// in anything. If so, we behave as if IDLE had data but
// will have already fetched the messages so perform_*_fetch
// will not find any new.
enum Event {
Tick,
Interrupt(InterruptInfo),
}
// loop until we are interrupted or if we fetched something
info =
loop {
use futures::future::FutureExt;
match interval
.next()
.map(|_| Event::Tick)
.race(self.idle_interrupt.recv().map(|probe_network| {
Event::Interrupt(probe_network.unwrap_or_default())
}))
.await
{
Event::Tick => {
// try to connect with proper login params
// (setup_handle_if_needed might not know about them if we
// never successfully connected)
if let Err(err) = self.connect_configured(context).await {
warn!(context, "fake_idle: could not connect: {}", err);
continue;
}
if self.config.can_idle {
// we only fake-idled because network was gone during IDLE, probably
break InterruptInfo::new(false, None);
}
info!(context, "fake_idle is connected");
// we are connected, let's see if fetching messages results
// in anything. If so, we behave as if IDLE had data but
// will have already fetched the messages so perform_*_fetch
// will not find any new.
if let Some(ref watch_folder) = watch_folder {
match self.fetch_new_messages(context, watch_folder).await {
Ok(res) => {
info!(context, "fetch_new_messages returned {:?}", res);
if res {
break InterruptInfo::new(false, None);
if let Some(ref watch_folder) = watch_folder {
match self.fetch_new_messages(context, watch_folder).await {
Ok(res) => {
info!(context, "fetch_new_messages returned {:?}", res);
if res {
break InterruptInfo::new(false, None);
}
}
Err(err) => {
error!(context, "could not fetch from folder: {}", err);
self.trigger_reconnect()
}
}
}
Err(err) => {
error!(context, "could not fetch from folder: {}", err);
self.trigger_reconnect()
}
}
Event::Interrupt(info) => {
// Interrupt
break info;
}
}
}
Event::Interrupt(info) => {
// Interrupt
break info;
}
}
};
};
}
info!(
context,

View File

@@ -23,12 +23,11 @@ use crate::events::Event;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::job::{self, Action};
use crate::login_param::{CertificateChecks, LoginParam};
use crate::message::{self, update_server_uid, MessageState};
use crate::message::{self, update_server_uid};
use crate::mimeparser;
use crate::oauth2::dc_get_oauth2_access_token;
use crate::param::Params;
use crate::provider::get_provider_info;
use crate::{chat, scheduler::InterruptInfo, stock::StockMessage};
use crate::{scheduler::InterruptInfo, stock::StockMessage};
mod client;
mod idle;
@@ -36,7 +35,6 @@ pub mod select_folder;
mod session;
use client::Client;
use message::Message;
use session::Session;
type Result<T> = std::result::Result<T, Error>;
@@ -116,8 +114,8 @@ pub struct Imap {
session: Option<Session>,
connected: bool,
interrupt: Option<stop_token::StopSource>,
skip_next_idle_wait: bool,
should_reconnect: bool,
login_failed_once: bool,
}
#[derive(Debug)]
@@ -151,7 +149,7 @@ struct ImapConfig {
pub imap_port: u16,
pub imap_user: String,
pub imap_pw: String,
pub strict_tls: bool,
pub certificate_checks: CertificateChecks,
pub server_flags: usize,
pub selected_folder: Option<String>,
pub selected_mailbox: Option<Mailbox>,
@@ -171,7 +169,7 @@ impl Default for ImapConfig {
imap_port: 0,
imap_user: "".into(),
imap_pw: "".into(),
strict_tls: false,
certificate_checks: Default::default(),
server_flags: 0,
selected_folder: None,
selected_mailbox: None,
@@ -190,8 +188,8 @@ impl Imap {
session: Default::default(),
connected: Default::default(),
interrupt: Default::default(),
skip_next_idle_wait: Default::default(),
should_reconnect: Default::default(),
login_failed_once: Default::default(),
}
}
@@ -230,7 +228,7 @@ impl Imap {
match Client::connect_insecure((imap_server, imap_port)).await {
Ok(client) => {
if (server_flags & DC_LP_IMAP_SOCKET_STARTTLS) != 0 {
client.secure(imap_server, config.strict_tls).await
client.secure(imap_server, config.certificate_checks).await
} else {
Ok(client)
}
@@ -242,8 +240,12 @@ impl Imap {
let imap_server: &str = config.imap_server.as_ref();
let imap_port = config.imap_port;
Client::connect_secure((imap_server, imap_port), imap_server, config.strict_tls)
.await
Client::connect_secure(
(imap_server, imap_port),
imap_server,
config.certificate_checks,
)
.await
};
let login_res = match connection_res {
@@ -296,40 +298,18 @@ impl Imap {
// needs to be set here to ensure it is set on reconnects.
self.connected = true;
self.session = Some(session);
self.login_failed_once = false;
Ok(())
}
Err((err, _)) => {
let imap_user = self.config.imap_user.to_owned();
let message = context
.stock_string_repl_str(StockMessage::CannotLogin, &imap_user)
.await;
warn!(context, "{} ({})", message, err);
emit_event!(context, Event::ErrorNetwork(message.clone()));
let lock = context.wrong_pw_warning_mutex.lock().await;
if self.login_failed_once
&& context.get_config_bool(Config::NotifyAboutWrongPw).await
{
if let Err(e) = context.set_config(Config::NotifyAboutWrongPw, None).await {
warn!(context, "{}", e);
}
drop(lock);
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(message);
if let Err(e) =
chat::add_device_msg_with_importance(context, None, Some(&mut msg), true)
.await
{
warn!(context, "{}", e);
}
} else {
self.login_failed_once = true;
}
emit_event!(
context,
Event::ErrorNetwork(format!("{} ({})", message, err))
);
self.trigger_reconnect();
Err(Error::LoginFailed(format!("cannot login as {}", imap_user)))
}
@@ -337,15 +317,9 @@ impl Imap {
}
async fn unsetup_handle(&mut self, context: &Context) {
// Close folder if messages should be expunged
if let Err(err) = self.close_folder(context).await {
warn!(context, "failed to close folder: {:?}", err);
}
// Logout from the server
if let Some(mut session) = self.session.take() {
if let Err(err) = session.logout().await {
warn!(context, "failed to logout: {:?}", err);
if let Err(err) = session.close().await {
warn!(context, "failed to close connection: {:?}", err);
}
}
self.connected = false;
@@ -405,15 +379,7 @@ impl Imap {
config.imap_port = imap_port;
config.imap_user = imap_user.to_string();
config.imap_pw = imap_pw.to_string();
let provider = get_provider_info(&lp.addr);
config.strict_tls = match lp.imap_certificate_checks {
CertificateChecks::Automatic => {
provider.map_or(false, |provider| provider.strict_tls)
}
CertificateChecks::Strict => true,
CertificateChecks::AcceptInvalidCertificates
| CertificateChecks::AcceptInvalidCertificates2 => false,
};
config.certificate_checks = lp.imap_certificate_checks;
config.server_flags = server_flags;
}
@@ -573,14 +539,8 @@ impl Imap {
let set = format!("{}", mailbox.exists);
match session.fetch(set, JUST_UID).await {
Ok(mut list) => {
let mut new_last_seen_uid = None;
while let Some(fetch) = list.next().await.transpose()? {
if fetch.message == mailbox.exists && fetch.uid.is_some() {
new_last_seen_uid = fetch.uid;
}
}
if let Some(new_last_seen_uid) = new_last_seen_uid {
new_last_seen_uid
if let Some(Ok(msg)) = list.next().await {
msg.uid.unwrap_or_default()
} else {
return Err(Error::Other("failed to fetch".into()));
}
@@ -761,17 +721,9 @@ impl Imap {
folder: S,
server_uids: &[u32],
) -> (Option<u32>, usize) {
let set = match server_uids {
[] => return (None, 0),
[server_uid] => server_uid.to_string(),
[first_uid, .., last_uid] => {
// XXX: it is assumed that UIDs are sorted and
// contiguous. If UIDs are not contiguous, more
// messages than needed will be downloaded.
debug_assert!(first_uid < last_uid, "uids must be sorted");
format!("{}:{}", first_uid, last_uid)
}
};
if server_uids.is_empty() {
return (None, 0);
}
if !self.is_connected() {
warn!(context, "Not connected");
@@ -787,6 +739,15 @@ impl Imap {
let session = self.session.as_mut().unwrap();
let set = if server_uids.len() == 1 {
server_uids[0].to_string()
} else {
let first_uid = server_uids[0];
let last_uid = server_uids[server_uids.len() - 1];
assert!(first_uid < last_uid, "uids must be sorted");
format!("{}:{}", first_uid, last_uid)
};
let mut msgs = match session.uid_fetch(&set, BODY_FLAGS).await {
Ok(msgs) => msgs,
Err(err) => {
@@ -810,6 +771,7 @@ impl Imap {
let mut last_uid = None;
let mut count = 0;
let mut tasks = Vec::with_capacity(server_uids.len());
while let Some(Ok(msg)) = msgs.next().await {
let server_uid = msg.uid.unwrap_or_default();
@@ -829,17 +791,32 @@ impl Imap {
let context = context.clone();
let folder = folder.clone();
// safe, as we checked above that there is a body.
let body = msg.body().unwrap();
let is_seen = msg.flags().any(|flag| flag == Flag::Seen);
let task = async_std::task::spawn(async move {
// safe, as we checked above that there is a body.
let body = msg.body().unwrap();
let is_seen = msg.flags().any(|flag| flag == Flag::Seen);
match dc_receive_imf(&context, &body, &folder, server_uid, is_seen).await {
Ok(_) => last_uid = Some(server_uid),
Err(err) => {
warn!(context, "dc_receive_imf error: {}", err);
match dc_receive_imf(&context, &body, &folder, server_uid, is_seen).await {
Ok(_) => Some(server_uid),
Err(err) => {
warn!(context, "dc_receive_imf error: {}", err);
read_errors += 1;
None
}
}
});
tasks.push(task);
}
for task in futures::future::join_all(tasks).await {
match task {
Some(uid) => {
last_uid = Some(uid);
}
None => {
read_errors += 1;
}
};
}
}
if count != server_uids.len() {
@@ -976,11 +953,7 @@ impl Imap {
if let Some(ref mut session) = &mut self.session {
let query = format!("+FLAGS ({})", flag);
match session.uid_store(uid_set, &query).await {
Ok(mut responses) => {
while let Some(_response) = responses.next().await {
// Read all the responses
}
}
Ok(_) => {}
Err(err) => {
warn!(
context,
@@ -1083,35 +1056,8 @@ impl Imap {
if let Some(ref mut session) = &mut self.session {
match session.uid_fetch(set, DELETE_CHECK_FLAGS).await {
Ok(mut msgs) => {
let mut remote_message_id = None;
while let Some(response) = msgs.next().await {
match response {
Ok(fetch) => {
if fetch.uid == Some(uid) {
remote_message_id = get_fetch_headers(&fetch)
.and_then(|headers| prefetch_get_message_id(&headers))
.ok();
}
}
Err(err) => {
warn!(context, "IMAP fetch error {}", err);
return ImapActionResult::RetryLater;
}
}
}
if let Some(remote_message_id) = remote_message_id {
if remote_message_id != message_id {
warn!(
context,
"Cannot delete on IMAP, {}: remote message-id '{}' != '{}'",
display_imap_id,
remote_message_id,
message_id,
);
return ImapActionResult::Failed;
}
let fetch = if let Some(Ok(fetch)) = msgs.next().await {
fetch
} else {
warn!(
context,
@@ -1120,6 +1066,21 @@ impl Imap {
message_id,
);
return ImapActionResult::AlreadyDone;
};
let remote_message_id = get_fetch_headers(&fetch)
.and_then(|headers| prefetch_get_message_id(&headers))
.unwrap_or_default();
if remote_message_id != message_id {
warn!(
context,
"Cannot delete on IMAP, {}: remote message-id '{}' != '{}'",
display_imap_id,
remote_message_id,
message_id,
);
return ImapActionResult::Failed;
}
}
Err(err) => {
@@ -1388,26 +1349,14 @@ async fn precheck_imf(
let delete_server_after = context.get_config_delete_server_after().await;
if delete_server_after != Some(0) {
if msg_id
.needs_move(context, server_folder)
.await
.unwrap_or_default()
{
// If the bcc-self message is not moved, directly
// add MarkSeen job, otherwise MarkSeen job is
// added after the Move Job completed.
job::add(
context,
job::Job::new(Action::MoveMsg, msg_id.to_u32(), Params::new(), 0),
)
context
.do_heuristics_moves(server_folder.as_ref(), msg_id)
.await;
} else {
job::add(
context,
job::Job::new(Action::MarkseenMsgOnImap, msg_id.to_u32(), Params::new(), 0),
)
.await;
}
job::add(
context,
job::Job::new(Action::MarkseenMsgOnImap, msg_id.to_u32(), Params::new(), 0),
)
.await;
}
} else if old_server_folder != server_folder {
info!(
@@ -1442,13 +1391,6 @@ async fn precheck_imf(
if old_server_folder != server_folder || old_server_uid != server_uid {
update_server_uid(context, rfc724_mid, server_folder, server_uid).await;
if let Ok(MessageState::InSeen) = msg_id.get_state(context).await {
job::add(
context,
job::Job::new(Action::MarkseenMsgOnImap, msg_id.to_u32(), Params::new(), 0),
)
.await;
};
context
.interrupt_inbox(InterruptInfo::new(false, Some(msg_id)))
.await;
@@ -1496,7 +1438,7 @@ async fn prefetch_is_reply_to_chat_message(
false
}
pub(crate) async fn prefetch_should_download(
async fn prefetch_should_download(
context: &Context,
headers: &[mailparse::MailHeader<'_>],
show_emails: ShowEmails,
@@ -1504,13 +1446,6 @@ pub(crate) async fn prefetch_should_download(
let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion).is_some();
let is_reply_to_chat_message = prefetch_is_reply_to_chat_message(context, &headers).await;
let maybe_ndn = if let Some(from) = headers.get_header_value(HeaderDef::From_) {
let from = from.to_ascii_lowercase();
from.contains("mailer-daemon") || from.contains("mail-daemon")
} else {
false
};
// Autocrypt Setup Message should be shown even if it is from non-chat client.
let is_autocrypt_setup_message = headers
.get_header_value(HeaderDef::AutocryptSetupMessage)
@@ -1528,8 +1463,8 @@ pub(crate) async fn prefetch_should_download(
}
ShowEmails::All => true,
};
let should_download = (show && !blocked_contact) || maybe_ndn;
Ok(should_download)
let show = show && !blocked_contact;
Ok(show)
}
async fn message_needs_processing(

View File

@@ -27,7 +27,7 @@ impl Imap {
///
/// CLOSE is considerably faster than an EXPUNGE, see
/// https://tools.ietf.org/html/rfc3501#section-6.4.2
pub(super) async fn close_folder(&mut self, context: &Context) -> Result<()> {
async fn close_folder(&mut self, context: &Context) -> Result<()> {
if let Some(ref folder) = self.config.selected_folder {
info!(context, "Expunge messages in \"{}\".", folder);
@@ -51,14 +51,6 @@ impl Imap {
Ok(())
}
/// Issues a CLOSE command if selected folder needs expunge.
pub(crate) async fn maybe_close_folder(&mut self, context: &Context) -> Result<()> {
if self.config.selected_folder_needs_expunge {
self.close_folder(context).await?;
}
Ok(())
}
/// select a folder, possibly update uid_validity and, if needed,
/// expunge the folder to remove delete-marked messages.
pub(super) async fn select_folder<S: AsRef<str>>(
@@ -84,7 +76,10 @@ impl Imap {
}
// deselect existing folder, if needed (it's also done implicitly by SELECT, however, without EXPUNGE then)
self.maybe_close_folder(context).await?;
let needs_expunge = { self.config.selected_folder_needs_expunge };
if needs_expunge {
self.close_folder(context).await?;
}
// select new folder
if let Some(ref folder) = folder {

View File

@@ -178,11 +178,10 @@ async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
///
/// The `passphrase` must be at least 2 characters long.
pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<String> {
let passphrase_begin = if let Some(passphrase_begin) = passphrase.get(..2) {
passphrase_begin
} else {
bail!("Passphrase must be at least 2 chars long.");
};
ensure!(
passphrase.len() >= 2,
"Passphrase must be at least 2 chars long."
);
let private_key = SignedSecretKey::load_self(context).await?;
let ac_headers = match context.get_config_bool(Config::E2eeEnabled).await {
false => None,
@@ -197,7 +196,7 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
"Passphrase-Format: numeric9x4\r\n",
"Passphrase-Begin: {}"
),
passphrase_begin
&passphrase[..2]
);
let pgp_msg = encr.replace("-----BEGIN PGP MESSAGE-----", &replacement);
@@ -449,33 +448,27 @@ async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) ->
"***IMPORT-in-progress: total_files_cnt={:?}", total_files_cnt,
);
// Load IDs only for now, without the file contents, to avoid
// consuming too much memory.
let file_ids = context
let files = context
.sql
.query_map(
"SELECT id FROM backup_blobs ORDER BY id",
"SELECT file_name, file_content FROM backup_blobs ORDER BY id;",
paramsv![],
|row| row.get(0),
|ids| {
ids.collect::<std::result::Result<Vec<i64>, _>>()
|row| {
let name: String = row.get(0)?;
let blob: Vec<u8> = row.get(1)?;
Ok((name, blob))
},
|files| {
files
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await?;
let mut all_files_extracted = true;
for (processed_files_cnt, file_id) in file_ids.into_iter().enumerate() {
// Load a single blob into memory
let (file_name, file_blob) = context
.sql
.query_row(
"SELECT file_name, file_content FROM backup_blobs WHERE id = ?",
paramsv![file_id],
|row| Ok((row.get::<_, String>(0)?, row.get::<_, Vec<u8>>(1)?)),
)
.await?;
for (processed_files_cnt, (file_name, file_blob)) in files.into_iter().enumerate() {
if context.shall_stop_ongoing().await {
all_files_extracted = false;
break;
@@ -783,9 +776,9 @@ mod tests {
#[async_std::test]
async fn test_render_setup_file() {
let t = TestContext::new().await;
let t = test_context().await;
t.configure_alice().await;
configure_alice_keypair(&t.ctx).await;
let msg = render_setup_file(&t.ctx, "hello").await.unwrap();
println!("{}", &msg);
// Check some substrings, indicating things got substituted.
@@ -802,12 +795,12 @@ mod tests {
#[async_std::test]
async fn test_render_setup_file_newline_replace() {
let t = TestContext::new().await;
let t = dummy_context().await;
t.ctx
.set_stock_translation(StockMessage::AcSetupMsgBody, "hello\r\nthere".to_string())
.await
.unwrap();
t.configure_alice().await;
configure_alice_keypair(&t.ctx).await;
let msg = render_setup_file(&t.ctx, "pw").await.unwrap();
println!("{}", &msg);
assert!(msg.contains("<p>hello<br>there</p>"));
@@ -815,7 +808,7 @@ mod tests {
#[async_std::test]
async fn test_create_setup_code() {
let t = TestContext::new().await;
let t = dummy_context().await;
let setupcode = create_setup_code(&t.ctx);
assert_eq!(setupcode.len(), 44);
assert_eq!(setupcode.chars().nth(4).unwrap(), '-');
@@ -830,7 +823,7 @@ mod tests {
#[async_std::test]
async fn test_export_key_to_asc_file() {
let context = TestContext::new().await;
let context = dummy_context().await;
let key = alice_keypair().public;
let blobdir = "$BLOBDIR";
assert!(export_key_to_asc_file(&context.ctx, blobdir, None, &key)

View File

@@ -21,7 +21,6 @@ use crate::constants::*;
use crate::contact::Contact;
use crate::context::Context;
use crate::dc_tools::*;
use crate::ephemeral::load_imap_deletion_msgid;
use crate::error::{bail, ensure, format_err, Error, Result};
use crate::events::Event;
use crate::imap::*;
@@ -95,6 +94,7 @@ pub enum Action {
// Jobs in the INBOX-thread, range from DC_IMAP_THREAD..DC_IMAP_THREAD+999
Housekeeping = 105, // low priority ...
EmptyServer = 107,
OldDeleteMsgOnImap = 110,
MarkseenMsgOnImap = 130,
// Moving message is prioritized lower than deletion so we don't
@@ -123,6 +123,7 @@ impl From<Action> for Thread {
Unknown => Thread::Unknown,
Housekeeping => Thread::Imap,
OldDeleteMsgOnImap => Thread::Imap,
DeleteMsgOnImap => Thread::Imap,
EmptyServer => Thread::Imap,
MarkseenMsgOnImap => Thread::Imap,
@@ -189,7 +190,7 @@ impl Job {
/// Saves the job to the database, creating a new entry if necessary.
///
/// The Job is consumed by this method.
pub(crate) async fn save(self, context: &Context) -> Result<()> {
pub async fn save(self, context: &Context) -> Result<()> {
let thread: Thread = self.action.into();
info!(context, "saving job for {}-thread: {:?}", thread, self);
@@ -253,48 +254,40 @@ impl Job {
let res = match err {
async_smtp::smtp::error::Error::Permanent(ref response) => {
// Workaround for incorrectly configured servers returning permanent errors
// instead of temporary ones.
let maybe_transient = match response.code {
match response.code {
// Sometimes servers send a permanent error when actually it is a temporary error
// For documentation see https://tools.ietf.org/html/rfc3463
// Code 5.5.0, see https://support.delta.chat/t/every-other-message-gets-stuck/877/2
Code {
category: Category::MailSystem,
detail: Detail::Zero,
..
} => {
// Ignore status code 5.5.0, see https://support.delta.chat/t/every-other-message-gets-stuck/877/2
// Maybe incorrectly configured Postfix milter with "reject" instead of "tempfail", which returns
// "550 5.5.0 Service unavailable" instead of "451 4.7.1 Service unavailable - try again later".
//
// Other enhanced status codes, such as Postfix
// "550 5.1.1 <foobar@example.org>: Recipient address rejected: User unknown in local recipient table"
// are not ignored.
response.message.get(0) == Some(&"5.5.0".to_string())
}
_ => false,
};
} => Status::RetryLater,
if maybe_transient {
Status::RetryLater
} else {
// If we do not retry, add an info message to the chat.
// Yandex error "554 5.7.1 [2] Message rejected under suspicion of SPAM; https://ya.cc/..."
// should definitely go here, because user has to open the link to
// resume message sending.
let msg_id = MsgId::new(self.foreign_id);
message::set_msg_failed(context, msg_id, Some(err.to_string())).await;
match Message::load_from_db(context, msg_id).await {
Ok(message) => {
chat::add_info_msg(context, message.chat_id, err.to_string())
_ => {
// If we do not retry, add an info message to the chat
// Error 5.7.1 should definitely go here: Yandex sends 5.7.1 with a link when it thinks that the email is SPAM.
match Message::load_from_db(context, MsgId::new(self.foreign_id))
.await
{
Ok(message) => {
chat::add_info_msg(
context,
message.chat_id,
err.to_string(),
)
.await
}
Err(e) => error!(
context,
"couldn't load chat_id to inform user about SMTP error: {}", e
),
};
Status::Finished(Err(format_err!("Permanent SMTP error: {}", err)))
}
Err(e) => warn!(
context,
"couldn't load chat_id to inform user about SMTP error: {}",
e
),
};
Status::Finished(Err(format_err!("Permanent SMTP error: {}", err)))
}
}
}
async_smtp::smtp::error::Error::Transient(_) => {
@@ -336,7 +329,7 @@ impl Job {
}
}
pub(crate) async fn send_msg_to_smtp(&mut self, context: &Context, smtp: &mut Smtp) -> Status {
pub async fn send_msg_to_smtp(&mut self, context: &Context, smtp: &mut Smtp) -> Status {
// SMTP server, if not yet done
if !smtp.is_connected().await {
let loginparam = LoginParam::from_database(context, "configured_").await;
@@ -631,9 +624,7 @@ impl Job {
}
}
if self.foreign_id & DC_EMPTY_INBOX > 0 {
if let Some(inbox_folder) = &context.get_config(Config::ConfiguredInboxFolder).await {
imap.empty_folder(context, &inbox_folder).await;
}
imap.empty_folder(context, "INBOX").await;
}
Status::Finished(Ok(()))
}
@@ -647,21 +638,7 @@ impl Job {
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await);
let folder = msg.server_folder.as_ref().unwrap();
let result = if msg.server_uid == 0 {
// The message is moved or deleted by us.
//
// Do not call set_seen with zero UID, as it will return
// ImapActionResult::RetryLater, but we do not want to
// retry. If the message was moved, we will create another
// job to mark the message as seen later. If it was
// deleted, there is nothing to do.
ImapActionResult::Failed
} else {
imap.set_seen(context, folder, msg.server_uid).await
};
match result {
match imap.set_seen(context, folder, msg.server_uid).await {
ImapActionResult::RetryLater => Status::RetryLater,
ImapActionResult::AlreadyDone => Status::Finished(Ok(())),
ImapActionResult::Success | ImapActionResult::Failed => {
@@ -669,18 +646,7 @@ impl Job {
// we want to send out an MDN anyway
// The job will not be retried so locally
// there is no risk of double-sending MDNs.
//
// Read receipts for system messages are never
// sent. These messages have no place to display
// received read receipt anyway. And since their text
// is locally generated, quoting them is dangerous as
// it may contain contact names. E.g., for original
// message "Group left by me", a read receipt will
// quote "Group left by <name>", and the name can be a
// display name stored in address book rather than
// the name sent in the From field by the user.
if msg.param.get_bool(Param::WantsMdn).unwrap_or_default()
&& !msg.is_system_message()
&& context.get_config_bool(Config::MdnsEnabled).await
{
if let Err(err) = send_mdn(context, &msg).await {
@@ -840,7 +806,7 @@ pub async fn send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<Job
if rendered_msg.is_encrypted && !needs_encryption {
msg.param.set_int(Param::GuaranteeE2ee, 1);
msg.update_param(context).await;
msg.save_param_to_disk(context).await;
}
ensure!(!recipients.is_empty(), "no recipients for smtp job set");
@@ -857,11 +823,31 @@ pub async fn send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<Job
Ok(Some(job))
}
pub(crate) enum Connection<'a> {
#[derive(Debug)]
pub enum Connection<'a> {
Inbox(&'a mut Imap),
Smtp(&'a mut Smtp),
}
async fn load_imap_deletion_msgid(context: &Context) -> sql::Result<Option<MsgId>> {
if let Some(delete_server_after) = context.get_config_delete_server_after().await {
let threshold_timestamp = time() - delete_server_after;
context
.sql
.query_row_optional(
"SELECT id FROM msgs \
WHERE timestamp < ? \
AND server_uid != 0",
paramsv![threshold_timestamp],
|row| row.get::<_, MsgId>(0),
)
.await
} else {
Ok(None)
}
}
async fn load_imap_deletion_job(context: &Context) -> sql::Result<Option<Job>> {
let res = if let Some(msg_id) = load_imap_deletion_msgid(context).await? {
Some(Job::new(
@@ -985,6 +971,7 @@ async fn perform_job_action(
location::job_maybe_send_locations_ended(context, job).await
}
Action::EmptyServer => job.empty_server(context, connection.inbox()).await,
Action::OldDeleteMsgOnImap => job.delete_msg_on_imap(context, connection.inbox()).await,
Action::DeleteMsgOnImap => job.delete_msg_on_imap(context, connection.inbox()).await,
Action::MarkseenMsgOnImap => job.markseen_msg_on_imap(context, connection.inbox()).await,
Action::MoveMsg => job.move_msg(context, connection.inbox()).await,
@@ -994,7 +981,10 @@ async fn perform_job_action(
}
};
info!(context, "Finished immediate try {} of job {}", tries, job);
info!(
context,
"Inbox finished immediate try {} of job {}", tries, job
);
try_res
}
@@ -1042,6 +1032,7 @@ pub async fn add(context: &Context, job: Job) {
Action::Unknown => unreachable!(),
Action::Housekeeping
| Action::EmptyServer
| Action::OldDeleteMsgOnImap
| Action::DeleteMsgOnImap
| Action::MarkseenMsgOnImap
| Action::MoveMsg => {
@@ -1219,7 +1210,7 @@ mod tests {
// We want to ensure that loading jobs skips over jobs which
// fails to load from the database instead of failing to load
// all jobs.
let t = TestContext::new().await;
let t = dummy_context().await;
insert_job(&t.ctx, -1).await; // This can not be loaded into Job struct.
let jobs = load_next(
&t.ctx,
@@ -1241,7 +1232,7 @@ mod tests {
#[async_std::test]
async fn test_load_next_job_one() {
let t = TestContext::new().await;
let t = dummy_context().await;
insert_job(&t.ctx, 1).await;

View File

@@ -247,7 +247,7 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
secret: SignedSecretKey::from_slice(&sec_bytes)?,
}),
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
let start = std::time::SystemTime::now();
let start = std::time::Instant::now();
let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await)
.unwrap_or_default();
info!(context, "Generating keypair with type {}", keytype);
@@ -258,7 +258,7 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
info!(
context,
"Keypair generated in {:.3}s.",
start.elapsed().unwrap_or_default().as_secs()
start.elapsed().as_secs()
);
Ok(keypair)
}
@@ -550,8 +550,8 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
#[async_std::test]
async fn test_load_self_existing() {
let alice = alice_keypair();
let t = TestContext::new().await;
t.configure_alice().await;
let t = dummy_context().await;
configure_alice_keypair(&t.ctx).await;
let pubkey = SignedPublicKey::load_self(&t.ctx).await.unwrap();
assert_eq!(alice.public, pubkey);
let seckey = SignedSecretKey::load_self(&t.ctx).await.unwrap();
@@ -560,7 +560,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
#[async_std::test]
async fn test_load_self_generate_public() {
let t = TestContext::new().await;
let t = dummy_context().await;
t.ctx
.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
.await
@@ -571,7 +571,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
#[async_std::test]
async fn test_load_self_generate_secret() {
let t = TestContext::new().await;
let t = dummy_context().await;
t.ctx
.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
.await
@@ -584,7 +584,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
async fn test_load_self_generate_concurrent() {
use std::thread;
let t = TestContext::new().await;
let t = dummy_context().await;
t.ctx
.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
.await
@@ -611,7 +611,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
async fn test_save_self_key_twice() {
// Saving the same key twice should result in only one row in
// the keypairs table.
let t = TestContext::new().await;
let t = dummy_context().await;
let ctx = Arc::new(t.ctx);
let ctx1 = ctx.clone();

View File

@@ -79,8 +79,8 @@ mod tests {
#[async_std::test]
async fn test_keyring_load_self() {
// new_self() implies load_self()
let t = TestContext::new().await;
t.configure_alice().await;
let t = dummy_context().await;
configure_alice_keypair(&t.ctx).await;
let alice = alice_keypair();
let pub_ring: Keyring<SignedPublicKey> = Keyring::new_self(&t.ctx).await.unwrap();

View File

@@ -1,11 +1,6 @@
#![forbid(unsafe_code)]
#![deny(
clippy::correctness,
missing_debug_implementations,
clippy::all,
clippy::indexing_slicing
)]
#![allow(clippy::match_bool, clippy::eval_order_dependence)]
#![deny(clippy::correctness, missing_debug_implementations, clippy::all)]
#![allow(clippy::match_bool)]
#[macro_use]
extern crate num_derive;
@@ -16,6 +11,8 @@ extern crate rusqlite;
extern crate strum;
#[macro_use]
extern crate strum_macros;
#[macro_use]
extern crate debug_stub_derive;
pub trait ToSql: rusqlite::ToSql + Send + Sync {}
@@ -48,7 +45,6 @@ pub mod constants;
pub mod contact;
pub mod context;
mod e2ee;
pub mod ephemeral;
mod imap;
pub mod imex;
mod scheduler;

View File

@@ -530,7 +530,7 @@ pub async fn save(
accuracy,
..
} = location;
let (loc_id, ts) = context
context
.sql
.with_conn(move |mut conn| {
let mut stmt_test = conn
@@ -569,11 +569,9 @@ pub async fn save(
)?;
}
}
Ok((newest_location_id, newest_timestamp))
Ok(())
})
.await?;
newest_timestamp = ts;
newest_location_id = loc_id;
}
Ok(newest_location_id)
@@ -724,11 +722,11 @@ pub(crate) async fn job_maybe_send_locations_ended(
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::TestContext;
use crate::test_utils::dummy_context;
#[async_std::test]
async fn test_kml_parse() {
let context = TestContext::new().await;
let context = dummy_context().await;
let xml =
b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n<Document addr=\"user@example.org\">\n<Placemark><Timestamp><when>2019-03-06T21:09:57Z</when></Timestamp><Point><coordinates accuracy=\"32.000000\">9.423110,53.790302</coordinates></Point></Placemark>\n<PlaceMARK>\n<Timestamp><WHEN > \n\t2018-12-13T22:11:12Z\t</WHEN></Timestamp><Point><coordinates aCCuracy=\"2.500000\"> 19.423110 \t , \n 63.790302\n </coordinates></Point></PlaceMARK>\n</Document>\n</kml>";

View File

@@ -41,17 +41,6 @@ macro_rules! error {
}};
}
#[macro_export]
macro_rules! error_network {
($ctx:expr, $msg:expr) => {
error_network!($ctx, $msg,)
};
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
let formatted = format!($msg, $($args),*);
emit_event!($ctx, $crate::Event::ErrorNetwork(formatted));
}};
}
#[macro_export]
macro_rules! emit_event {
($ctx:expr, $event:expr) => {

View File

@@ -130,6 +130,10 @@ impl LoginParam {
}
}
pub fn addr_str(&self) -> &str {
self.addr.as_str()
}
/// Save this loginparam to the database.
pub async fn save_to_database(
&self,
@@ -273,15 +277,21 @@ fn get_readable_flags(flags: i32) -> String {
res
}
pub fn dc_build_tls(strict_tls: bool) -> async_native_tls::TlsConnector {
pub fn dc_build_tls(certificate_checks: CertificateChecks) -> async_native_tls::TlsConnector {
let tls_builder = async_native_tls::TlsConnector::new();
if strict_tls {
tls_builder
} else {
tls_builder
match certificate_checks {
CertificateChecks::Automatic => {
// Same as AcceptInvalidCertificates for now.
// TODO: use provider database when it becomes available
tls_builder
.danger_accept_invalid_hostnames(true)
.danger_accept_invalid_certs(true)
}
CertificateChecks::Strict => tls_builder,
CertificateChecks::AcceptInvalidCertificates
| CertificateChecks::AcceptInvalidCertificates2 => tls_builder
.danger_accept_invalid_hostnames(true)
.danger_accept_invalid_certs(true)
.danger_accept_invalid_certs(true),
}
}

View File

@@ -91,9 +91,6 @@ pub enum LotState {
/// text1=domain
QrAccount = 250,
/// text1=domain, text2=instance pattern
QrWebrtcInstance = 260,
/// id=contact
QrAddr = 320,

View File

@@ -6,7 +6,6 @@ use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use crate::chat::{self, Chat, ChatId};
use crate::config::Config;
use crate::constants::*;
use crate::contact::*;
use crate::context::*;
@@ -15,7 +14,7 @@ use crate::error::{ensure, Error};
use crate::events::Event;
use crate::job::{self, Action};
use crate::lot::{Lot, LotState, Meaning};
use crate::mimeparser::{FailureReport, SystemMessage};
use crate::mimeparser::SystemMessage;
use crate::param::*;
use crate::pgp::*;
use crate::stock::StockMessage;
@@ -69,38 +68,18 @@ impl MsgId {
self.0 == 0
}
/// Returns message state.
pub async fn get_state(self, context: &Context) -> crate::sql::Result<MessageState> {
let result = context
.sql
.query_get_value_result("SELECT state FROM msgs WHERE id=?", paramsv![self])
.await?
.unwrap_or_default();
Ok(result)
/// Whether the message ID is the special marker1 marker.
///
/// See the docs of the `dc_get_chat_msgs` C API for details.
pub fn is_marker1(self) -> bool {
self.0 == DC_MSG_ID_MARKER1
}
/// Returns true if the message needs to be moved from `folder`.
pub async fn needs_move(self, context: &Context, folder: &str) -> Result<bool, Error> {
if !context.get_config_bool(Config::MvboxMove).await {
return Ok(false);
}
if context.is_mvbox(folder).await {
return Ok(false);
}
let msg = Message::load_from_db(context, self).await?;
if msg.is_setupmessage() {
// do not move setup messages;
// there may be a non-delta device that wants to handle it
return Ok(false);
}
match msg.is_dc_message {
MessengerMessage::No => Ok(false),
MessengerMessage::Yes | MessengerMessage::Reply => Ok(true),
}
/// Whether the message ID is the special day marker.
///
/// See the docs of the `dc_get_chat_msgs` C API for details.
pub fn is_daymarker(self) -> bool {
self.0 == DC_MSG_ID_DAYMARKER
}
/// Put message into trash chat and delete message text.
@@ -164,7 +143,16 @@ impl MsgId {
impl std::fmt::Display for MsgId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Msg#{}", self.0)
// Would be nice if we could use match here, but no computed values in ranges.
if self.0 == DC_MSG_ID_MARKER1 {
write!(f, "Msg#Marker1")
} else if self.0 == DC_MSG_ID_DAYMARKER {
write!(f, "Msg#DayMarker")
} else if self.0 <= DC_MSG_ID_LAST_SPECIAL {
write!(f, "Msg#UnknownSpecial")
} else {
write!(f, "Msg#{}", self.0)
}
}
}
@@ -258,8 +246,6 @@ pub struct Message {
pub(crate) timestamp_sort: i64,
pub(crate) timestamp_sent: i64,
pub(crate) timestamp_rcvd: i64,
pub(crate) ephemeral_timer: u32,
pub(crate) ephemeral_timestamp: i64,
pub(crate) text: Option<String>,
pub(crate) rfc724_mid: String,
pub(crate) in_reply_to: Option<String>,
@@ -269,7 +255,6 @@ pub struct Message {
pub(crate) starred: bool,
pub(crate) chat_blocked: Blocked,
pub(crate) location_id: u32,
pub(crate) error: String,
pub(crate) param: Params,
}
@@ -302,11 +287,8 @@ impl Message {
" m.timestamp AS timestamp,",
" m.timestamp_sent AS timestamp_sent,",
" m.timestamp_rcvd AS timestamp_rcvd,",
" m.ephemeral_timer AS ephemeral_timer,",
" m.ephemeral_timestamp AS ephemeral_timestamp,",
" m.type AS type,",
" m.state AS state,",
" m.error AS error,",
" m.msgrmsg AS msgrmsg,",
" m.txt AS txt,",
" m.param AS param,",
@@ -332,11 +314,8 @@ impl Message {
msg.timestamp_sort = row.get("timestamp")?;
msg.timestamp_sent = row.get("timestamp_sent")?;
msg.timestamp_rcvd = row.get("timestamp_rcvd")?;
msg.ephemeral_timer = row.get("ephemeral_timer")?;
msg.ephemeral_timestamp = row.get("ephemeral_timestamp")?;
msg.viewtype = row.get("type")?;
msg.state = row.get("state")?;
msg.error = row.get("error")?;
msg.is_dc_message = row.get("msgrmsg")?;
let text;
@@ -411,7 +390,7 @@ impl Message {
}
if !self.id.is_unset() {
self.update_param(context).await;
self.save_param_to_disk(context).await;
}
}
}
@@ -528,14 +507,6 @@ impl Message {
self.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() != 0
}
pub fn get_ephemeral_timer(&self) -> u32 {
self.ephemeral_timer
}
pub fn get_ephemeral_timestamp(&self) -> i64 {
self.ephemeral_timestamp
}
pub async fn get_summary(&mut self, context: &Context, chat: Option<&Chat>) -> Lot {
let mut ret = Lot::new();
@@ -600,11 +571,6 @@ impl Message {
|| cmd != SystemMessage::Unknown && cmd != SystemMessage::AutocryptSetupMessage
}
pub fn is_system_message(&self) -> bool {
let cmd = self.param.get_cmd();
cmd != SystemMessage::Unknown
}
/// Whether the message is still being created.
///
/// Messages with attachments might be created before the
@@ -643,39 +609,6 @@ impl Message {
None
}
/// split a webrtc_instance as defined by the corresponding config-value into a type and a url
pub fn parse_webrtc_instance(instance: &str) -> (VideochatType, String) {
let mut split = instance.splitn(2, ':');
let type_str = split.next().unwrap_or_default().to_lowercase();
let url = split.next();
match type_str.as_str() {
"basicwebrtc" => (
VideochatType::BasicWebrtc,
url.unwrap_or_default().to_string(),
),
"jitsi" => (VideochatType::Jitsi, url.unwrap_or_default().to_string()),
_ => (VideochatType::Unknown, instance.to_string()),
}
}
pub fn get_videochat_url(&self) -> Option<String> {
if self.viewtype == Viewtype::VideochatInvitation {
if let Some(instance) = self.param.get(Param::WebrtcRoom) {
return Some(Message::parse_webrtc_instance(instance).1);
}
}
None
}
pub fn get_videochat_type(&self) -> Option<VideochatType> {
if self.viewtype == Viewtype::VideochatInvitation {
if let Some(instance) = self.param.get(Param::WebrtcRoom) {
return Some(Message::parse_webrtc_instance(instance).0);
}
}
None
}
pub fn set_text(&mut self, text: Option<String>) {
self.text = text;
}
@@ -710,10 +643,10 @@ impl Message {
if duration > 0 {
self.param.set_int(Param::Duration, duration);
}
self.update_param(context).await;
self.save_param_to_disk(context).await;
}
pub async fn update_param(&mut self, context: &Context) -> bool {
pub async fn save_param_to_disk(&mut self, context: &Context) -> bool {
context
.sql
.execute(
@@ -830,10 +763,9 @@ impl From<MessageState> for LotState {
impl MessageState {
pub fn can_fail(self) -> bool {
match self {
MessageState::OutPreparing
| MessageState::OutPending
| MessageState::OutDelivered
| MessageState::OutMdnRcvd => true, // OutMdnRcvd can still fail because it could be a group message and only some recipients failed.
MessageState::OutPreparing | MessageState::OutPending | MessageState::OutDelivered => {
true
}
_ => false,
}
}
@@ -955,17 +887,6 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> String {
ret += "\n";
}
if msg.ephemeral_timer != 0 {
ret += &format!("Ephemeral timer: {}\n", msg.ephemeral_timer);
}
if msg.ephemeral_timestamp != 0 {
ret += &format!(
"Expires: {}\n",
dc_timestamp_to_str(msg.ephemeral_timestamp)
);
}
if msg.from_id == DC_CONTACT_ID_INFO || msg.to_id == DC_CONTACT_ID_INFO {
// device-internal message, no further details needed
return ret;
@@ -1016,9 +937,8 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> String {
}
ret += "\n";
if !msg.error.is_empty() {
ret += &format!("Error: {}", msg.error);
if let Some(err) = msg.param.get(Param::Error) {
ret += &format!("Error: {}", err)
}
if let Some(path) = msg.get_file(context) {
@@ -1171,14 +1091,6 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> bool {
let mut send_event = false;
for (id, curr_state, curr_blocked) in msgs.into_iter() {
if let Err(err) = id.start_ephemeral_timer(context).await {
error!(
context,
"Failed to start ephemeral timer for message {}: {}", id, err
);
continue;
}
if curr_blocked == Blocked::Not {
if curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed {
update_msg_state(context, id, MessageState::InSeen).await;
@@ -1235,7 +1147,7 @@ pub async fn star_msgs(context: &Context, msg_ids: Vec<MsgId>, star: bool) -> bo
.is_ok()
}
/// Returns a summary text.
/// Returns a summary test.
pub async fn get_summarytext_by_raw(
viewtype: Viewtype,
text: Option<impl AsRef<str>>,
@@ -1279,13 +1191,6 @@ pub async fn get_summarytext_by_raw(
format!("{} {}", label, file_name)
}
}
Viewtype::VideochatInvitation => {
append_text = false;
context
.stock_str(StockMessage::VideochatInvitation)
.await
.into_owned()
}
_ => {
if param.get_cmd() != SystemMessage::LocationOnly {
"".to_string()
@@ -1346,44 +1251,39 @@ pub async fn exists(context: &Context, msg_id: MsgId) -> bool {
pub async fn set_msg_failed(context: &Context, msg_id: MsgId, error: Option<impl AsRef<str>>) {
if let Ok(mut msg) = Message::load_from_db(context, msg_id).await {
let error = error.map(|e| e.as_ref().to_string()).unwrap_or_default();
if msg.state.can_fail() {
msg.state = MessageState::OutFailed;
warn!(context, "{} failed: {}", msg_id, error);
} else {
warn!(
context,
"{} seems to have failed ({}), but state is {}", msg_id, error, msg.state
)
}
if let Some(error) = error {
msg.param.set(Param::Error, error.as_ref());
warn!(context, "Message failed: {}", error.as_ref());
}
match context
if context
.sql
.execute(
"UPDATE msgs SET state=?, error=? WHERE id=?;",
paramsv![msg.state, error, msg_id],
"UPDATE msgs SET state=?, param=? WHERE id=?;",
paramsv![msg.state, msg.param.to_string(), msg_id],
)
.await
.is_ok()
{
Ok(_) => context.emit_event(Event::MsgFailed {
context.emit_event(Event::MsgFailed {
chat_id: msg.chat_id,
msg_id,
}),
Err(e) => {
warn!(context, "{:?}", e);
}
});
}
}
}
/// returns Some if an event should be send
pub async fn handle_mdn(
pub async fn mdn_from_ext(
context: &Context,
from_id: u32,
rfc724_mid: &str,
timestamp_sent: i64,
) -> Option<(ChatId, MsgId)> {
if from_id <= DC_CONTACT_ID_LAST_SPECIAL || rfc724_mid.is_empty() {
if from_id <= DC_MSG_ID_LAST_SPECIAL || rfc724_mid.is_empty() {
return None;
}
@@ -1418,10 +1318,10 @@ pub async fn handle_mdn(
if let Ok((msg_id, chat_id, chat_type, msg_state)) = res {
let mut read_by_all = false;
if msg_state == MessageState::OutPreparing
|| msg_state == MessageState::OutPending
|| msg_state == MessageState::OutDelivered
{
// if already marked as MDNS_RCVD msgstate_can_fail() returns false.
// however, it is important, that ret_msg_id is set above as this
// will allow the caller eg. to move the message away
if msg_state.can_fail() {
let mdn_already_in_table = context
.sql
.exists(
@@ -1484,69 +1384,6 @@ pub async fn handle_mdn(
None
}
/// Marks a message as failed after an ndn (non-delivery-notification) arrived.
/// Where appropriate, also adds an info message telling the user which of the recipients of a group message failed.
pub(crate) async fn handle_ndn(
context: &Context,
failed: &FailureReport,
error: Option<impl AsRef<str>>,
) {
if failed.rfc724_mid.is_empty() {
return;
}
let res = context
.sql
.query_row(
concat!(
"SELECT",
" m.id AS msg_id,",
" c.id AS chat_id,",
" c.type AS type",
" FROM msgs m LEFT JOIN chats c ON m.chat_id=c.id",
" WHERE rfc724_mid=? AND from_id=1",
),
paramsv![failed.rfc724_mid],
|row| {
Ok((
row.get::<_, MsgId>("msg_id")?,
row.get::<_, ChatId>("chat_id")?,
row.get::<_, Chattype>("type")?,
))
},
)
.await;
if let Err(ref err) = res {
info!(context, "Failed to select NDN {:?}", err);
}
if let Ok((msg_id, chat_id, chat_type)) = res {
set_msg_failed(context, msg_id, error).await;
if chat_type == Chattype::Group || chat_type == Chattype::VerifiedGroup {
if let Some(failed_recipient) = &failed.failed_recipient {
let contact_id =
Contact::lookup_id_by_addr(context, failed_recipient, Origin::Unknown).await;
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
// Tell the user which of the recipients failed if we know that (because in a group, this might otherwise be unclear)
chat::add_info_msg(
context,
chat_id,
context
.stock_string_repl_str(
StockMessage::FailedSendingTo,
contact.get_display_name(),
)
.await,
)
.await;
context.emit_event(Event::ChatModified(chat_id));
}
}
}
}
}
/// The number of messages assigned to real chat (!=deaddrop, !=trash)
pub async fn get_real_msg_cnt(context: &Context) -> i32 {
match context
@@ -1735,7 +1572,7 @@ mod tests {
async fn test_prepare_message_and_send() {
use crate::config::Config;
let d = test::TestContext::new().await;
let d = test::dummy_context().await;
let ctx = &d.ctx;
let contact = Contact::create(ctx, "", "dest@example.com")
@@ -1759,7 +1596,7 @@ mod tests {
#[async_std::test]
async fn test_get_summarytext_by_raw() {
let d = test::TestContext::new().await;
let d = test::dummy_context().await;
let ctx = &d.ctx;
let some_text = Some("bla bla".to_string());
@@ -1844,23 +1681,4 @@ mod tests {
"Autocrypt Setup Message" // file name is not added for autocrypt setup messages
);
}
#[async_std::test]
async fn test_parse_webrtc_instance() {
let (webrtc_type, url) = Message::parse_webrtc_instance("basicwebrtc:https://foo/bar");
assert_eq!(webrtc_type, VideochatType::BasicWebrtc);
assert_eq!(url, "https://foo/bar");
let (webrtc_type, url) = Message::parse_webrtc_instance("bAsIcwEbrTc:url");
assert_eq!(webrtc_type, VideochatType::BasicWebrtc);
assert_eq!(url, "url");
let (webrtc_type, url) = Message::parse_webrtc_instance("https://foo/bar?key=val#key=val");
assert_eq!(webrtc_type, VideochatType::Unknown);
assert_eq!(url, "https://foo/bar?key=val#key=val");
let (webrtc_type, url) = Message::parse_webrtc_instance("jitsi:https://j.si/foo");
assert_eq!(webrtc_type, VideochatType::Jitsi);
assert_eq!(url, "https://j.si/foo");
}
}

View File

@@ -9,7 +9,6 @@ use crate::contact::*;
use crate::context::{get_version_str, Context};
use crate::dc_tools::*;
use crate::e2ee::*;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::error::{bail, ensure, format_err, Error};
use crate::location;
use crate::message::{self, Message};
@@ -113,7 +112,12 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
)
.await?;
if !msg.is_system_message() && context.get_config_bool(Config::MdnsEnabled).await {
let command = msg.param.get_cmd();
if command != SystemMessage::AutocryptSetupMessage
&& command != SystemMessage::SecurejoinMessage
&& context.get_config_bool(Config::MdnsEnabled).await
{
req_mdn = true;
}
}
@@ -221,7 +225,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
.filter(|(_, addr)| addr != &self_addr)
{
res.push((
Peerstate::from_addr(self.context, addr).await?,
Peerstate::from_addr(self.context, addr).await,
addr.as_str(),
));
}
@@ -436,8 +440,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
to.push(from.clone());
}
unprotected_headers.push(Header::new("MIME-Version".into(), "1.0".into()));
if !self.references.is_empty() {
unprotected_headers.push(Header::new("References".into(), self.references.clone()));
}
@@ -494,16 +496,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
let e2ee_guaranteed = self.is_e2ee_guaranteed();
let encrypt_helper = EncryptHelper::new(self.context).await?;
let subject = if subject_str
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == ' ')
// We do not use needs_encoding() here because needs_encoding() returns true if the string contains a space
// but we do not want to encode all subjects just because they contain a space.
{
subject_str
} else {
encode_words(&subject_str)
};
let subject = encode_words(&subject_str);
let mut message = match self.loaded {
Loaded::Message { .. } => {
@@ -531,14 +524,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
Loaded::MDN { .. } => dc_create_outgoing_rfc724_mid(None, &self.from_addr),
};
let ephemeral_timer = self.msg.chat_id.get_ephemeral_timer(self.context).await?;
if let EphemeralTimer::Enabled { duration } = ephemeral_timer {
protected_headers.push(Header::new(
"Ephemeral-Timer".to_string(),
duration.to_string(),
));
}
// we could also store the message-id in the protected headers
// which would probably help to survive providers like
// Outlook.com or hotmail which mangle the Message-ID.
@@ -789,26 +774,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
"location-streaming-enabled".into(),
));
}
SystemMessage::EphemeralTimerChanged => {
protected_headers.push(Header::new(
"Chat-Content".to_string(),
"ephemeral-timer-changed".to_string(),
));
}
SystemMessage::LocationOnly => {
// This should prevent automatic replies,
// such as non-delivery reports.
//
// See https://tools.ietf.org/html/rfc3834
//
// Adding this header without encryption leaks some
// information about the message contents, but it can
// already be easily guessed from message timing and size.
unprotected_headers.push(Header::new(
"Auto-Submitted".to_string(),
"auto-generated".to_string(),
));
}
SystemMessage::AutocryptSetupMessage => {
unprotected_headers
.push(Header::new("Autocrypt-Setup-Message".into(), "v1".into()));
@@ -870,19 +835,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
if self.msg.viewtype == Viewtype::Sticker {
protected_headers.push(Header::new("Chat-Content".into(), "sticker".into()));
} else if self.msg.viewtype == Viewtype::VideochatInvitation {
protected_headers.push(Header::new(
"Chat-Content".into(),
"videochat-invitation".into(),
));
protected_headers.push(Header::new(
"Chat-Webrtc-Room".into(),
self.msg
.param
.get(Param::WebrtcRoom)
.unwrap_or_default()
.into(),
));
}
if self.msg.viewtype == Viewtype::Voice
@@ -1253,10 +1205,6 @@ pub fn needs_encoding(to_check: impl AsRef<str>) -> bool {
#[cfg(test)]
mod tests {
use super::*;
use crate::chatlist::Chatlist;
use crate::dc_receive_imf::dc_receive_imf;
use crate::mimeparser::*;
use crate::test_utils::TestContext;
#[test]
fn test_render_email_address() {
@@ -1264,9 +1212,6 @@ mod tests {
let addr = "x@y.org";
assert!(!display_name.is_ascii());
assert!(!display_name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == ' '));
let s = format!(
"{}",
@@ -1278,25 +1223,6 @@ mod tests {
assert_eq!(s, "=?utf-8?q?=C3=A4_space?= <x@y.org>");
}
#[test]
fn test_render_email_address_noescape() {
let display_name = "a space";
let addr = "x@y.org";
assert!(display_name.is_ascii());
assert!(display_name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == ' '));
let s = format!(
"{}",
Address::new_mailbox_with_name(display_name.to_string(), addr.to_string())
);
// Addresses should not be unnecessarily be encoded, see https://github.com/deltachat/deltachat-core-rust/issues/1575:
assert_eq!(s, "a space <x@y.org>");
}
#[test]
fn test_render_rfc724_mid() {
assert_eq!(
@@ -1340,15 +1266,34 @@ mod tests {
assert!(needs_encoding("foo bar"));
}
use crate::test_utils::{dummy_context, TestContext};
async fn configured_offline_context() -> TestContext {
let t = dummy_context().await;
t.ctx
.set_config(Config::Addr, Some("alice@example.org"))
.await
.unwrap();
t.ctx
.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
.await
.unwrap();
t.ctx
.set_config(Config::Configured, Some("1"))
.await
.unwrap();
t
}
#[async_std::test]
async fn test_subject() {
// 1.: Receive a mail from an MUA or Delta Chat
assert_eq!(
msg_to_subject_str(
b"From: Bob <bob@example.com>\n\
To: alice@example.com\n\
b"From: Bob <bob@example.org>\n\
To: alice@example.org\n\
Subject: Antw: Chat: hello\n\
Message-ID: <2222@example.com>\n\
Message-ID: <2222@example.org>\n\
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
\n\
hello\n"
@@ -1359,10 +1304,10 @@ mod tests {
assert_eq!(
msg_to_subject_str(
b"From: Bob <bob@example.com>\n\
To: alice@example.com\n\
b"From: Bob <bob@example.org>\n\
To: alice@example.org\n\
Subject: Infos: 42\n\
Message-ID: <2222@example.com>\n\
Message-ID: <2222@example.org>\n\
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
\n\
hello\n"
@@ -1374,11 +1319,11 @@ mod tests {
// 2. Receive a message from Delta Chat when we did not send any messages before
assert_eq!(
msg_to_subject_str(
b"From: Charlie <charlie@example.com>\n\
To: alice@example.com\n\
b"From: Charlie <charlie@example.org>\n\
To: alice@example.org\n\
Subject: Chat: hello\n\
Chat-Version: 1.0\n\
Message-ID: <2223@example.com>\n\
Message-ID: <2223@example.org>\n\
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
\n\
hello\n"
@@ -1388,11 +1333,10 @@ mod tests {
);
// 3. Send the first message to a new contact
let t = TestContext::new_alice().await;
let t = configured_offline_context().await;
assert_eq!(first_subject_str(t).await, "Message from alice@example.org");
assert_eq!(first_subject_str(t).await, "Message from alice@example.com");
let t = TestContext::new_alice().await;
let t = configured_offline_context().await;
t.ctx
.set_config(Config::Displayname, Some("Alice"))
.await
@@ -1401,11 +1345,11 @@ mod tests {
// 4. Receive messages with unicode characters and make sure that we do not panic (we do not care about the result)
msg_to_subject_str(
"From: Charlie <charlie@example.com>\n\
To: alice@example.com\n\
"From: Charlie <charlie@example.org>\n\
To: alice@example.org\n\
Subject: äääää\n\
Chat-Version: 1.0\n\
Message-ID: <2893@example.com>\n\
Message-ID: <2893@example.org>\n\
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
\n\
hello\n"
@@ -1414,70 +1358,22 @@ mod tests {
.await;
msg_to_subject_str(
"From: Charlie <charlie@example.com>\n\
To: alice@example.com\n\
"From: Charlie <charlie@example.org>\n\
To: alice@example.org\n\
Subject: aäääää\n\
Chat-Version: 1.0\n\
Message-ID: <2893@example.com>\n\
Message-ID: <2893@example.org>\n\
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
\n\
hello\n"
.as_bytes(),
)
.await;
// 5. Receive an mdn (read receipt) and make sure the mdn's subject is not used
let t = TestContext::new_alice().await;
dc_receive_imf(
&t.ctx,
b"From: alice@example.com\n\
To: Charlie <charlie@example.com>\n\
Subject: Hello, Charlie\n\
Chat-Version: 1.0\n\
Message-ID: <2893@example.com>\n\
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
\n\
hello\n",
"INBOX",
1,
false,
)
.await
.unwrap();
let new_msg = incoming_msg_to_reply_msg(b"From: charlie@example.com\n\
To: alice@example.com\n\
Subject: message opened\n\
Date: Sun, 22 Mar 2020 23:37:57 +0000\n\
Chat-Version: 1.0\n\
Message-ID: <Mr.12345678902@example.com>\n\
Content-Type: multipart/report; report-type=disposition-notification; boundary=\"SNIPP\"\n\
\n\
\n\
--SNIPP\n\
Content-Type: text/plain; charset=utf-8\n\
\n\
Read receipts do not guarantee sth. was read.\n\
\n\
\n\
--SNIPP\n\
Content-Type: message/disposition-notification\n\
\n\
Reporting-UA: Delta Chat 1.28.0\n\
Original-Recipient: rfc822;charlie@example.com\n\
Final-Recipient: rfc822;charlie@example.com\n\
Original-Message-ID: <2893@example.com>\n\
Disposition: manual-action/MDN-sent-automatically; displayed\n\
\n", &t.ctx).await;
let mf = MimeFactory::from_msg(&t.ctx, &new_msg, false)
.await
.unwrap();
// The subject string should not be "Re: message opened"
assert_eq!("Re: Hello, Charlie", mf.subject_str().await);
}
async fn first_subject_str(t: TestContext) -> String {
let contact_id =
Contact::add_or_lookup(&t.ctx, "Dave", "dave@example.com", Origin::ManuallyCreated)
Contact::add_or_lookup(&t.ctx, "Dave", "dave@example.org", Origin::ManuallyCreated)
.await
.unwrap()
.0;
@@ -1501,79 +1397,35 @@ mod tests {
}
async fn msg_to_subject_str(imf_raw: &[u8]) -> String {
let t = TestContext::new_alice().await;
let new_msg = incoming_msg_to_reply_msg(imf_raw, &t.ctx).await;
let mf = MimeFactory::from_msg(&t.ctx, &new_msg, false)
.await
.unwrap();
mf.subject_str().await
}
use crate::chatlist::Chatlist;
use crate::dc_receive_imf::dc_receive_imf;
// Creates a `Message` that replies "Hi" to the incoming email in `imf_raw`.
async fn incoming_msg_to_reply_msg(imf_raw: &[u8], context: &Context) -> Message {
context
let t = configured_offline_context().await;
t.ctx
.set_config(Config::ShowEmails, Some("2"))
.await
.unwrap();
dc_receive_imf(context, imf_raw, "INBOX", 1, false)
dc_receive_imf(&t.ctx, imf_raw, "INBOX", 1, false)
.await
.unwrap();
let chats = Chatlist::try_load(context, 0, None, None).await.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
let chat_id = chat::create_by_msg_id(context, chats.get_msg_id(0).unwrap())
let chat_id = chat::create_by_msg_id(&t.ctx, chats.get_msg_id(0).unwrap())
.await
.unwrap();
let mut new_msg = Message::new(Viewtype::Text);
new_msg.set_text(Some("Hi".to_string()));
new_msg.chat_id = chat_id;
chat::prepare_msg(context, chat_id, &mut new_msg)
chat::prepare_msg(&t.ctx, chat_id, &mut new_msg)
.await
.unwrap();
new_msg
}
#[async_std::test]
// This test could still be extended
async fn test_render_reply() {
let t = TestContext::new_alice().await;
let context = &t.ctx;
let msg = incoming_msg_to_reply_msg(
b"From: Charlie <charlie@example.com>\n\
To: alice@example.com\n\
Subject: Chat: hello\n\
Chat-Version: 1.0\n\
Message-ID: <2223@example.com>\n\
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
\n\
hello\n",
context,
)
.await;
let mimefactory = MimeFactory::from_msg(&t.ctx, &msg, false).await.unwrap();
let recipients = mimefactory.recipients();
assert_eq!(recipients, vec!["charlie@example.com"]);
let rendered_msg = mimefactory.render().await.unwrap();
let mail = mailparse::parse_mail(&rendered_msg.message).unwrap();
assert_eq!(
mail.headers
.iter()
.find(|h| h.get_key() == "MIME-Version")
.unwrap()
.get_value(),
"1.0"
);
let _mime_msg = MimeMessage::from_bytes(context, &rendered_msg.message)
let mf = MimeFactory::from_msg(&t.ctx, &new_msg, false)
.await
.unwrap();
mf.subject_str().await
}
}

View File

@@ -3,7 +3,6 @@ use std::future::Future;
use std::pin::Pin;
use deltachat_derive::{FromSql, ToSql};
use lazy_static::lazy_static;
use lettre_email::mime::{self, Mime};
use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo};
@@ -46,14 +45,7 @@ pub struct MimeMessage {
pub from: Vec<SingleInfo>,
pub chat_disposition_notification_to: Option<SingleInfo>,
pub decrypting_failed: bool,
/// Set of valid signature fingerprints if a message is an
/// Autocrypt encrypted and signed message.
///
/// If a message is not encrypted or the signature is not valid,
/// this set is empty.
pub signatures: HashSet<Fingerprint>,
pub gossipped_addr: HashSet<String>,
pub is_forwarded: bool,
pub is_system_message: SystemMessage,
@@ -61,8 +53,7 @@ pub struct MimeMessage {
pub message_kml: Option<location::Kml>,
pub(crate) user_avatar: Option<AvatarAction>,
pub(crate) group_avatar: Option<AvatarAction>,
pub(crate) mdn_reports: Vec<Report>,
pub(crate) failure_report: Option<FailureReport>,
pub(crate) reports: Vec<Report>,
}
#[derive(Debug, PartialEq)]
@@ -83,9 +74,6 @@ pub enum SystemMessage {
SecurejoinMessage = 7,
LocationStreamingEnabled = 8,
LocationOnly = 9,
/// Chat ephemeral message timer is changed.
EphemeralTimerChanged = 10,
}
impl Default for SystemMessage {
@@ -131,10 +119,7 @@ impl MimeMessage {
let (mail, signatures) = match e2ee::try_decrypt(context, &mail, message_time).await {
Ok((raw, signatures)) => {
if let Some(raw) = raw {
// Encrypted, but maybe unsigned message. Only if
// `signatures` set is non-empty, it is a valid
// autocrypt message.
// Valid autocrypt message, encrypted
mail_raw = raw;
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
@@ -144,27 +129,18 @@ impl MimeMessage {
// Handle any gossip headers if the mail was encrypted. See section
// "3.6 Key Gossip" of https://autocrypt.org/autocrypt-spec-1.1.0.pdf
// but only if the mail was correctly signed:
if !signatures.is_empty() {
let gossip_headers =
decrypted_mail.headers.get_all_values("Autocrypt-Gossip");
gossipped_addr =
update_gossip_peerstates(context, message_time, &mail, gossip_headers)
.await?;
}
let gossip_headers = decrypted_mail.headers.get_all_values("Autocrypt-Gossip");
gossipped_addr =
update_gossip_peerstates(context, message_time, &mail, gossip_headers)
.await?;
// let known protected headers from the decrypted
// part override the unencrypted top-level
// Signature was checked for original From, so we
// do not allow overriding it.
let mut throwaway_from = from.clone();
MimeMessage::merge_headers(
context,
&mut headers,
&mut recipients,
&mut throwaway_from,
&mut from,
&mut chat_disposition_notification_to,
&decrypted_mail.headers,
);
@@ -200,24 +176,16 @@ impl MimeMessage {
signatures,
gossipped_addr,
is_forwarded: false,
mdn_reports: Vec::new(),
reports: Vec::new(),
is_system_message: SystemMessage::Unknown,
location_kml: None,
message_kml: None,
user_avatar: None,
group_avatar: None,
failure_report: None,
};
parser.parse_mime_recursive(context, &mail).await?;
parser.heuristically_parse_ndn(context).await;
parser.parse_headers(context)?;
if parser.signatures.is_empty() {
for part in parser.parts.iter_mut() {
part.error = "No valid signature".to_string();
}
}
Ok(parser)
}
@@ -242,8 +210,6 @@ impl MimeMessage {
} else if let Some(value) = self.get(HeaderDef::ChatContent) {
if value == "location-streaming-enabled" {
self.is_system_message = SystemMessage::LocationStreamingEnabled;
} else if value == "ephemeral-timer-changed" {
self.is_system_message = SystemMessage::EphemeralTimerChanged;
}
}
Ok(())
@@ -260,24 +226,10 @@ impl MimeMessage {
}
}
fn parse_videochat_headers(&mut self) {
if let Some(value) = self.get(HeaderDef::ChatContent).cloned() {
if value == "videochat-invitation" {
let instance = self.get(HeaderDef::ChatWebrtcRoom).cloned();
if let Some(part) = self.parts.first_mut() {
part.typ = Viewtype::VideochatInvitation;
part.param
.set(Param::WebrtcRoom, instance.unwrap_or_default());
}
}
}
}
/// Squashes mutlipart chat messages with attachment into single-part messages.
///
/// Delta Chat sends attachments, such as images, in two-part messages, with the first message
/// containing an explanation. If such a message is detected, first part can be safely dropped.
#[allow(clippy::indexing_slicing)]
fn squash_attachment_parts(&mut self) {
if let [textpart, filepart] = &self.parts[..] {
let need_drop = {
@@ -301,8 +253,7 @@ impl MimeMessage {
self.parts[0].msg = "".to_string();
// swap new with old
self.parts.push(filepart); // push to the end
let _ = self.parts.swap_remove(0); // drops first element, replacing it with the last one in O(1)
std::mem::replace(&mut self.parts[0], filepart);
}
}
}
@@ -311,21 +262,22 @@ impl MimeMessage {
fn parse_attachments(&mut self) {
// Attachment messages should be squashed into a single part
// before calling this function.
if self.parts.len() != 1 {
return;
}
if let Some(mut part) = self.parts.pop() {
if part.typ == Viewtype::Audio && self.get(HeaderDef::ChatVoiceMessage).is_some() {
part.typ = Viewtype::Voice;
if self.parts.len() == 1 {
if self.parts[0].typ == Viewtype::Audio
&& self.get(HeaderDef::ChatVoiceMessage).is_some()
{
let part_mut = &mut self.parts[0];
part_mut.typ = Viewtype::Voice;
}
if part.typ == Viewtype::Image {
if self.parts[0].typ == Viewtype::Image {
if let Some(value) = self.get(HeaderDef::ChatContent) {
if value == "sticker" {
part.typ = Viewtype::Sticker;
let part_mut = &mut self.parts[0];
part_mut.typ = Viewtype::Sticker;
}
}
}
let part = &self.parts[0];
if part.typ == Viewtype::Audio
|| part.typ == Viewtype::Voice
|| part.typ == Viewtype::Video
@@ -333,19 +285,17 @@ impl MimeMessage {
if let Some(field_0) = self.get(HeaderDef::ChatDuration) {
let duration_ms = field_0.parse().unwrap_or_default();
if duration_ms > 0 && duration_ms < 24 * 60 * 60 * 1000 {
part.param.set_int(Param::Duration, duration_ms);
let part_mut = &mut self.parts[0];
part_mut.param.set_int(Param::Duration, duration_ms);
}
}
}
self.parts.push(part);
}
}
fn parse_headers(&mut self, context: &Context) -> Result<()> {
self.parse_system_message_headers(context)?;
self.parse_avatar_headers();
self.parse_videochat_headers();
self.squash_attachment_parts();
if let Some(ref subject) = self.get_subject() {
@@ -361,11 +311,12 @@ impl MimeMessage {
}
}
if prepend_subject {
let subj = subject
.find('[')
.and_then(|n| subject.get(..n))
.unwrap_or(subject)
.trim();
let subj = if let Some(n) = subject.find('[') {
&subject[0..n]
} else {
subject
}
.trim();
if !subj.is_empty() {
for part in self.parts.iter_mut() {
@@ -402,7 +353,7 @@ impl MimeMessage {
// just have send a message in the subject with an empty body.
// Besides, we want to show something in case our incoming-processing
// failed to properly handle an incoming message.
if self.parts.is_empty() && self.mdn_reports.is_empty() {
if self.parts.is_empty() && self.reports.is_empty() {
let mut part = Part::default();
part.typ = Viewtype::Text;
@@ -423,7 +374,8 @@ impl MimeMessage {
Some(AvatarAction::Delete)
} else {
let mut i = 0;
while let Some(part) = self.parts.get_mut(i) {
while i != self.parts.len() {
let part = &mut self.parts[i];
if let Some(part_filename) = &part.org_filename {
if part_filename == &header_value {
if let Some(blob) = part.param.get(Param::File) {
@@ -440,11 +392,6 @@ impl MimeMessage {
}
}
/// Returns true if the message was encrypted as defined in
/// Autocrypt standard.
///
/// This means the message was both encrypted and signed with a
/// valid signature.
pub fn was_encrypted(&self) -> bool {
!self.signatures.is_empty()
}
@@ -580,7 +527,7 @@ impl MimeMessage {
part.typ = Viewtype::Text;
part.msg_raw = Some(txt.clone());
part.msg = txt;
part.error = "Decryption failed".to_string();
part.param.set(Param::Error, "Decryption failed");
self.parts.push(part);
@@ -592,7 +539,7 @@ impl MimeMessage {
contains exactly two body parts. The first body
part is the body part over which the digital signature was created [...]
The second body part contains the control information necessary to
verify the digital signature." We simply take the first body part and
verify the digital signature." We simpliy take the first body part and
skip the rest. (see
https://k9mail.github.io/2016/11/24/OpenPGP-Considerations-Part-I.html
for background information why we use encrypted+signed) */
@@ -603,10 +550,10 @@ impl MimeMessage {
(mime::MULTIPART, "report") => {
/* RFC 6522: the first part is for humans, the second for machines */
if mail.subparts.len() >= 2 {
match mail.ctype.params.get("report-type").map(|s| s as &str) {
Some("disposition-notification") => {
if let Some(report_type) = mail.ctype.params.get("report-type") {
if report_type == "disposition-notification" {
if let Some(report) = self.process_report(context, mail)? {
self.mdn_reports.push(report);
self.reports.push(report);
}
// Add MDN part so we can track it, avoid
@@ -618,21 +565,9 @@ impl MimeMessage {
self.parts.push(part);
any_part_added = true;
}
// Some providers, e.g. Tiscali, forget to set the report-type. So, if it's None, assume that it might be delivery-status
Some("delivery-status") | None => {
if let Some(report) = self.process_delivery_status(context, mail)? {
self.failure_report = Some(report);
}
// Add all parts (we need another part, preferrably text/plain, to show as an error message)
for cur_data in mail.subparts.iter() {
if self.parse_mime_recursive(context, cur_data).await? {
any_part_added = true;
}
}
}
Some(_) => {
} else {
/* eg. `report-type=delivery-status`;
maybe we should show them as a little error icon */
if let Some(first) = mail.subparts.iter().next() {
any_part_added = self.parse_mime_recursive(context, first).await?;
}
@@ -813,11 +748,16 @@ impl MimeMessage {
}
pub fn repl_msg_by_error(&mut self, error_msg: impl AsRef<str>) {
if let Some(part) = self.parts.first_mut() {
part.typ = Viewtype::Text;
part.msg = format!("[{}]", error_msg.as_ref());
self.parts.truncate(1);
if self.parts.is_empty() {
return;
}
let part = &mut self.parts[0];
part.typ = Viewtype::Text;
part.msg = format!("[{}]", error_msg.as_ref());
self.parts.truncate(1);
assert_eq!(self.parts.len(), 1);
}
pub fn get_rfc724_mid(&self) -> Option<String> {
@@ -868,11 +808,7 @@ impl MimeMessage {
report: &mailparse::ParsedMail<'_>,
) -> Result<Option<Report>> {
// parse as mailheaders
let report_body = if let Some(subpart) = report.subparts.get(1) {
subpart.get_body_raw()?
} else {
bail!("Report does not have second MIME part");
};
let report_body = report.subparts[1].get_body_raw()?;
let (report_fields, _) = mailparse::parse_headers(&report_body)?;
// must be present
@@ -904,115 +840,24 @@ impl MimeMessage {
Ok(None)
}
fn process_delivery_status(
&self,
context: &Context,
report: &mailparse::ParsedMail<'_>,
) -> Result<Option<FailureReport>> {
// parse as mailheaders
if let Some(original_msg) = report
.subparts
.iter()
.find(|p| p.ctype.mimetype.contains("rfc822") || p.ctype.mimetype == "message/global")
{
let report_body = original_msg.get_body_raw()?;
let (report_fields, _) = mailparse::parse_headers(&report_body)?;
if let Some(original_message_id) = report_fields
.get_header_value(HeaderDef::MessageId)
.and_then(|v| parse_message_id(&v).ok())
{
let mut to_list = get_all_addresses_from_header(&report.headers, |header_key| {
header_key == "x-failed-recipients"
});
let to = if to_list.len() == 1 {
Some(to_list.pop().unwrap())
} else {
None // We do not know which recipient failed
};
return Ok(Some(FailureReport {
rfc724_mid: original_message_id,
failed_recipient: to.map(|s| s.addr),
}));
}
warn!(
context,
"ignoring unknown ndn-notification, Message-Id: {:?}",
report_fields.get_header_value(HeaderDef::MessageId)
);
/// Handle reports (only MDNs for now)
pub async fn handle_reports(&self, context: &Context, from_id: u32, sent_timestamp: i64) {
if self.reports.is_empty() {
return;
}
Ok(None)
}
/// Some providers like GMX and Yahoo do not send standard NDNs (Non Delivery notifications).
/// If you improve heuristics here you might also have to change prefetch_should_download() in imap/mod.rs.
/// Also you should add a test in dc_receive_imf.rs (there already are lots of test_parse_ndn_* tests).
#[allow(clippy::indexing_slicing)]
async fn heuristically_parse_ndn(&mut self, context: &Context) -> Option<()> {
let maybe_ndn = if let Some(from) = self.get(HeaderDef::From_) {
let from = from.to_ascii_lowercase();
from.contains("mailer-daemon") || from.contains("mail-daemon")
} else {
false
};
if maybe_ndn && self.failure_report.is_none() {
lazy_static! {
static ref RE: regex::Regex = regex::Regex::new(r"Message-ID:(.*)").unwrap();
}
for captures in self
.parts
.iter()
.filter_map(|part| part.msg_raw.as_ref())
.flat_map(|part| part.lines())
.filter_map(|line| RE.captures(line))
{
if let Ok(original_message_id) = parse_message_id(&captures[1]) {
if let Ok(Some(_)) =
message::rfc724_mid_exists(context, &original_message_id).await
{
self.failure_report = Some(FailureReport {
rfc724_mid: original_message_id,
failed_recipient: None,
})
}
}
}
}
None // Always return None, we just return anything so that we can use the '?' operator.
}
/// Handle reports
/// (MDNs = Message Disposition Notification, the message was read
/// and NDNs = Non delivery notification, the message could not be delivered)
pub async fn handle_reports(
&self,
context: &Context,
from_id: u32,
sent_timestamp: i64,
parts: &[Part],
) {
for report in &self.mdn_reports {
for report in &self.reports {
for original_message_id in
std::iter::once(&report.original_message_id).chain(&report.additional_message_ids)
{
if let Some((chat_id, msg_id)) =
message::handle_mdn(context, from_id, original_message_id, sent_timestamp).await
message::mdn_from_ext(context, from_id, original_message_id, sent_timestamp)
.await
{
context.emit_event(Event::MsgRead { chat_id, msg_id });
}
}
}
if let Some(failure_report) = &self.failure_report {
let error = parts
.iter()
.find(|p| p.typ == Viewtype::Text)
.map(|p| p.msg.clone());
message::handle_ndn(context, failure_report, error).await
}
}
}
@@ -1033,7 +878,7 @@ async fn update_gossip_peerstates(
.iter()
.any(|info| info.addr == header.addr.to_lowercase())
{
let mut peerstate = Peerstate::from_addr(context, &header.addr).await?;
let mut peerstate = Peerstate::from_addr(context, &header.addr).await;
if let Some(ref mut peerstate) = peerstate {
peerstate.apply_gossip(header, message_time);
peerstate.save_to_db(&context.sql, false).await?;
@@ -1069,13 +914,6 @@ pub(crate) struct Report {
additional_message_ids: Vec<String>,
}
#[derive(Debug)]
pub(crate) struct FailureReport {
pub rfc724_mid: String,
pub failed_recipient: Option<String>,
}
#[allow(clippy::indexing_slicing)]
pub(crate) fn parse_message_ids(ids: &str) -> Result<Vec<String>> {
// take care with mailparse::msgidparse() that is pretty untolerant eg. wrt missing `<` or `>`
let mut msgids = Vec::new();
@@ -1119,7 +957,6 @@ pub struct Part {
pub bytes: usize,
pub param: Params,
org_filename: Option<String>,
pub error: String,
}
/// return mimetype and viewtype for a parsed mail
@@ -1282,7 +1119,7 @@ mod tests {
#[async_std::test]
async fn test_dc_mimeparser_crash() {
let context = TestContext::new().await;
let context = dummy_context().await;
let raw = include_bytes!("../test-data/message/issue_523.txt");
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
@@ -1294,7 +1131,7 @@ mod tests {
#[async_std::test]
async fn test_get_rfc724_mid_exists() {
let context = TestContext::new().await;
let context = dummy_context().await;
let raw = include_bytes!("../test-data/message/mail_with_message_id.txt");
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
@@ -1308,7 +1145,7 @@ mod tests {
#[async_std::test]
async fn test_get_rfc724_mid_not_exists() {
let context = TestContext::new().await;
let context = dummy_context().await;
let raw = include_bytes!("../test-data/message/issue_523.txt");
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
@@ -1366,7 +1203,7 @@ mod tests {
#[async_std::test]
async fn test_parse_first_addr() {
let context = TestContext::new().await;
let context = dummy_context().await;
let raw = b"From: hello@one.org, world@two.org\n\
Chat-Disposition-Notification-To: wrong\n\
Content-Type: text/plain\n\
@@ -1387,7 +1224,7 @@ mod tests {
#[async_std::test]
async fn test_mimeparser_with_context() {
let context = TestContext::new().await;
let context = dummy_context().await;
let raw = b"From: hello\n\
Content-Type: multipart/mixed; boundary=\"==break==\";\n\
Subject: outer-subject\n\
@@ -1437,7 +1274,7 @@ mod tests {
#[async_std::test]
async fn test_mimeparser_with_avatars() {
let t = TestContext::new().await;
let t = dummy_context().await;
let raw = include_bytes!("../test-data/message/mail_attach_txt.eml");
let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).await.unwrap();
@@ -1478,31 +1315,9 @@ mod tests {
assert!(mimeparser.group_avatar.unwrap().is_change());
}
#[async_std::test]
async fn test_mimeparser_with_videochat() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/videochat_invitation.eml");
let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).await.unwrap();
assert_eq!(mimeparser.parts.len(), 1);
assert_eq!(mimeparser.parts[0].typ, Viewtype::VideochatInvitation);
assert_eq!(
mimeparser.parts[0]
.param
.get(Param::WebrtcRoom)
.unwrap_or_default(),
"https://example.org/p2p/?roomname=6HiduoAn4xN"
);
assert!(mimeparser.parts[0]
.msg
.contains("https://example.org/p2p/?roomname=6HiduoAn4xN"));
assert_eq!(mimeparser.user_avatar, None);
assert_eq!(mimeparser.group_avatar, None);
}
#[async_std::test]
async fn test_mimeparser_message_kml() {
let context = TestContext::new().await;
let context = dummy_context().await;
let raw = b"Chat-Version: 1.0\n\
From: foo <foo@example.org>\n\
To: bar <bar@example.org>\n\
@@ -1547,7 +1362,7 @@ Content-Disposition: attachment; filename=\"message.kml\"\n\
#[async_std::test]
async fn test_parse_mdn() {
let context = TestContext::new().await;
let context = dummy_context().await;
let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\
Date: Mon, 10 Jan 2020 00:00:00 +0000\n\
Chat-Version: 1.0\n\
@@ -1588,7 +1403,7 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\
);
assert_eq!(message.parts.len(), 1);
assert_eq!(message.mdn_reports.len(), 1);
assert_eq!(message.reports.len(), 1);
}
/// Test parsing multiple MDNs combined in a single message.
@@ -1597,7 +1412,7 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\
/// multipart MIME messages.
#[async_std::test]
async fn test_parse_multiple_mdns() {
let context = TestContext::new().await;
let context = dummy_context().await;
let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\
Date: Mon, 10 Jan 2020 00:00:00 +0000\n\
Chat-Version: 1.0\n\
@@ -1668,12 +1483,12 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\
);
assert_eq!(message.parts.len(), 2);
assert_eq!(message.mdn_reports.len(), 2);
assert_eq!(message.reports.len(), 2);
}
#[async_std::test]
async fn test_parse_mdn_with_additional_message_ids() {
let context = TestContext::new().await;
let context = dummy_context().await;
let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\
Date: Mon, 10 Jan 2020 00:00:00 +0000\n\
Chat-Version: 1.0\n\
@@ -1715,20 +1530,17 @@ Additional-Message-IDs: <foo@example.com> <foo@example.net>\n\
);
assert_eq!(message.parts.len(), 1);
assert_eq!(message.mdn_reports.len(), 1);
assert_eq!(message.reports.len(), 1);
assert_eq!(message.reports[0].original_message_id, "foo@example.org");
assert_eq!(
message.mdn_reports[0].original_message_id,
"foo@example.org"
);
assert_eq!(
&message.mdn_reports[0].additional_message_ids,
&message.reports[0].additional_message_ids,
&["foo@example.com", "foo@example.net"]
);
}
#[async_std::test]
async fn test_parse_inline_attachment() {
let context = TestContext::new().await;
let context = dummy_context().await;
let raw = br#"Date: Thu, 13 Feb 2020 22:41:20 +0000 (UTC)
From: sender@example.com
To: receiver@example.com
@@ -1768,7 +1580,7 @@ MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg==
#[async_std::test]
async fn parse_inline_image() {
let context = TestContext::new().await;
let context = dummy_context().await;
let raw = br#"Message-ID: <foobar@example.org>
From: foo <foo@example.org>
Subject: example
@@ -1814,7 +1626,7 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
#[async_std::test]
async fn parse_thunderbird_html_embedded_image() {
let context = TestContext::new().await;
let context = dummy_context().await;
let raw = br#"To: Alice <alice@example.org>
From: Bob <bob@example.org>
Subject: Test subject
@@ -1887,7 +1699,7 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
// Outlook specifies filename in the "name" attribute of Content-Type
#[async_std::test]
async fn parse_outlook_html_embedded_image() {
let context = TestContext::new().await;
let context = dummy_context().await;
let raw = br##"From: Anonymous <anonymous@example.org>
To: Anonymous <anonymous@example.org>
Subject: Delta Chat is great stuff!

View File

@@ -1,16 +1,12 @@
//! OAuth 2 module
use regex::Regex;
use std::collections::HashMap;
use async_std_resolver::{config, resolver};
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use serde::Deserialize;
use crate::context::Context;
use crate::dc_tools::*;
use crate::provider;
use crate::provider::Oauth2Authorizer;
const OAUTH2_GMAIL: Oauth2 = Oauth2 {
// see https://developers.google.com/identity/protocols/OAuth2InstalledApp
@@ -19,7 +15,6 @@ const OAUTH2_GMAIL: Oauth2 = Oauth2 {
init_token: "https://accounts.google.com/o/oauth2/token?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&code=$CODE&grant_type=authorization_code",
refresh_token: "https://accounts.google.com/o/oauth2/token?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&refresh_token=$REFRESH_TOKEN&grant_type=refresh_token",
get_userinfo: Some("https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token=$ACCESS_TOKEN"),
mx_pattern: Some(r"^aspmx\.l\.google\.com\.$"),
};
const OAUTH2_YANDEX: Oauth2 = Oauth2 {
@@ -29,11 +24,8 @@ const OAUTH2_YANDEX: Oauth2 = Oauth2 {
init_token: "https://oauth.yandex.com/token?grant_type=authorization_code&code=$CODE&client_id=$CLIENT_ID&client_secret=58b8c6e94cf44fbe952da8511955dacf",
refresh_token: "https://oauth.yandex.com/token?grant_type=refresh_token&refresh_token=$REFRESH_TOKEN&client_id=$CLIENT_ID&client_secret=58b8c6e94cf44fbe952da8511955dacf",
get_userinfo: None,
mx_pattern: None,
};
const OAUTH2_PROVIDERS: [Oauth2; 1] = [OAUTH2_GMAIL];
#[derive(Debug, Clone, PartialEq, Eq)]
struct Oauth2 {
client_id: &'static str,
@@ -41,7 +33,6 @@ struct Oauth2 {
init_token: &'static str,
refresh_token: &'static str,
get_userinfo: Option<&'static str>,
mx_pattern: Option<&'static str>,
}
/// OAuth 2 Access Token Response
@@ -62,7 +53,7 @@ pub async fn dc_get_oauth2_url(
addr: impl AsRef<str>,
redirect_uri: impl AsRef<str>,
) -> Option<String> {
if let Some(oauth2) = Oauth2::from_address(addr).await {
if let Some(oauth2) = Oauth2::from_address(addr) {
if context
.sql
.set_raw_config(
@@ -90,7 +81,7 @@ pub async fn dc_get_oauth2_access_token(
code: impl AsRef<str>,
regenerate: bool,
) -> Option<String> {
if let Some(oauth2) = Oauth2::from_address(addr).await {
if let Some(oauth2) = Oauth2::from_address(addr) {
let lock = context.oauth2_mutex.lock().await;
// read generated token
@@ -248,7 +239,7 @@ pub async fn dc_get_oauth2_addr(
addr: impl AsRef<str>,
code: impl AsRef<str>,
) -> Option<String> {
let oauth2 = Oauth2::from_address(addr.as_ref()).await?;
let oauth2 = Oauth2::from_address(addr.as_ref())?;
oauth2.get_userinfo?;
if let Some(access_token) =
@@ -272,56 +263,23 @@ pub async fn dc_get_oauth2_addr(
}
impl Oauth2 {
async fn from_address(addr: impl AsRef<str>) -> Option<Self> {
fn from_address(addr: impl AsRef<str>) -> Option<Self> {
let addr_normalized = normalize_addr(addr.as_ref());
if let Some(domain) = addr_normalized
.find('@')
.map(|index| addr_normalized.split_at(index + 1).1)
{
if let Some(provider) = provider::get_provider_info(&addr_normalized) {
match &provider.oauth2_authorizer {
Some(Oauth2Authorizer::Gmail) => Some(OAUTH2_GMAIL),
Some(Oauth2Authorizer::Yandex) => Some(OAUTH2_YANDEX),
None => None, // provider known to not support oauth2, no mx-lookup required
}
} else {
Oauth2::lookup_mx(domain).await
match domain {
"gmail.com" | "googlemail.com" => Some(OAUTH2_GMAIL),
"yandex.com" | "yandex.by" | "yandex.kz" | "yandex.ru" | "yandex.ua" | "ya.ru"
| "narod.ru" => Some(OAUTH2_YANDEX),
_ => None,
}
} else {
None
}
}
async fn lookup_mx(domain: impl AsRef<str>) -> Option<Self> {
if let Ok(resolver) = resolver(
config::ResolverConfig::default(),
config::ResolverOpts::default(),
)
.await
{
for provider in OAUTH2_PROVIDERS.iter() {
if let Some(pattern) = provider.mx_pattern {
let re = Regex::new(pattern).unwrap();
let mut fqdn: String = String::from(domain.as_ref());
if !fqdn.ends_with('.') {
fqdn.push_str(".");
}
if let Ok(res) = resolver.mx_lookup(fqdn).await {
for rr in res.iter() {
if re.is_match(&rr.exchange().to_lowercase().to_utf8()) {
return Some(provider.clone());
}
}
}
}
}
}
None
}
async fn get_addr(&self, context: &Context, access_token: impl AsRef<str>) -> Option<String> {
let userinfo_url = self.get_userinfo.unwrap_or_else(|| "");
let userinfo_url = replace_in_uri(&userinfo_url, "$ACCESS_TOKEN", access_token);
@@ -404,39 +362,25 @@ mod tests {
);
}
#[async_std::test]
async fn test_oauth_from_address() {
#[test]
fn test_oauth_from_address() {
assert_eq!(Oauth2::from_address("hello@gmail.com"), Some(OAUTH2_GMAIL));
assert_eq!(
Oauth2::from_address("hello@gmail.com").await,
Oauth2::from_address("hello@googlemail.com"),
Some(OAUTH2_GMAIL)
);
assert_eq!(
Oauth2::from_address("hello@googlemail.com").await,
Some(OAUTH2_GMAIL)
);
assert_eq!(
Oauth2::from_address("hello@yandex.com").await,
Some(OAUTH2_YANDEX)
);
assert_eq!(
Oauth2::from_address("hello@yandex.ru").await,
Oauth2::from_address("hello@yandex.com"),
Some(OAUTH2_YANDEX)
);
assert_eq!(Oauth2::from_address("hello@yandex.ru"), Some(OAUTH2_YANDEX));
assert_eq!(Oauth2::from_address("hello@web.de").await, None);
}
#[async_std::test]
async fn test_oauth_from_mx() {
assert_eq!(
Oauth2::from_address("hello@google.com").await,
Some(OAUTH2_GMAIL)
);
assert_eq!(Oauth2::from_address("hello@web.de"), None);
}
#[async_std::test]
async fn test_dc_get_oauth2_addr() {
let ctx = TestContext::new().await;
let ctx = dummy_context().await;
let addr = "dignifiedquire@gmail.com";
let code = "fail";
let res = dc_get_oauth2_addr(&ctx.ctx, addr, code).await;
@@ -446,7 +390,7 @@ mod tests {
#[async_std::test]
async fn test_dc_get_oauth2_url() {
let ctx = TestContext::new().await;
let ctx = dummy_context().await;
let addr = "dignifiedquire@gmail.com";
let redirect_uri = "chat.delta:/com.b44t.messenger";
let res = dc_get_oauth2_url(&ctx.ctx, addr, redirect_uri).await;
@@ -456,7 +400,7 @@ mod tests {
#[async_std::test]
async fn test_dc_get_oauth2_token() {
let ctx = TestContext::new().await;
let ctx = dummy_context().await;
let addr = "dignifiedquire@gmail.com";
let code = "fail";
let res = dc_get_oauth2_access_token(&ctx.ctx, addr, code, false).await;

View File

@@ -66,10 +66,10 @@ pub enum Param {
Arg4 = b'H',
/// For Messages
AttachGroupImage = b'A',
Error = b'L',
/// For Messages
WebrtcRoom = b'V',
AttachGroupImage = b'A',
/// For Messages: space-separated list of messaged IDs of forwarded copies.
///
@@ -174,7 +174,7 @@ impl str::FromStr for Params {
let key = key.unwrap_or_default().trim();
let value = value.unwrap_or_default().trim();
if let Some(key) = key.as_bytes().first().and_then(|key| Param::from_u8(*key)) {
if let Some(key) = Param::from_u8(key.as_bytes()[0]) {
inner.insert(key, value.to_string());
} else {
bail!("Unknown key: {}", key);
@@ -417,7 +417,7 @@ mod tests {
#[async_std::test]
async fn test_params_file_fs_path() {
let t = TestContext::new().await;
let t = dummy_context().await;
if let ParamsFile::FsPath(p) = ParamsFile::from_param(&t.ctx, "/foo/bar/baz").unwrap() {
assert_eq!(p, Path::new("/foo/bar/baz"));
} else {
@@ -427,7 +427,7 @@ mod tests {
#[async_std::test]
async fn test_params_file_blob() {
let t = TestContext::new().await;
let t = dummy_context().await;
if let ParamsFile::Blob(b) = ParamsFile::from_param(&t.ctx, "$BLOBDIR/foo").unwrap() {
assert_eq!(b.as_name(), "$BLOBDIR/foo");
} else {
@@ -438,7 +438,7 @@ mod tests {
// Tests for Params::get_file(), Params::get_path() and Params::get_blob().
#[async_std::test]
async fn test_params_get_fileparam() {
let t = TestContext::new().await;
let t = dummy_context().await;
let fname = t.dir.path().join("foo");
let mut p = Params::new();
p.set(Param::File, fname.to_str().unwrap());

View File

@@ -1,12 +1,12 @@
//! # [Autocrypt Peer State](https://autocrypt.org/level1.html#peer-state-management) module
use std::collections::HashSet;
use std::convert::TryFrom;
use std::fmt;
use num_traits::FromPrimitive;
use crate::aheader::*;
use crate::context::Context;
use crate::error::Result;
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
use crate::sql::Sql;
@@ -143,12 +143,8 @@ impl<'a> Peerstate<'a> {
res
}
pub async fn from_addr(context: &'a Context, addr: &str) -> Result<Option<Peerstate<'a>>> {
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
verified_key, verified_key_fingerprint \
FROM acpeerstates \
WHERE addr=? COLLATE NOCASE;";
pub async fn from_addr(context: &'a Context, addr: &str) -> Option<Peerstate<'a>> {
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, verified_key, verified_key_fingerprint FROM acpeerstates WHERE addr=? COLLATE NOCASE;";
Self::from_stmt(context, query, paramsv![addr]).await
}
@@ -156,7 +152,7 @@ impl<'a> Peerstate<'a> {
context: &'a Context,
_sql: &Sql,
fingerprint: &Fingerprint,
) -> Result<Option<Peerstate<'a>>> {
) -> Option<Peerstate<'a>> {
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
verified_key, verified_key_fingerprint \
@@ -172,10 +168,10 @@ impl<'a> Peerstate<'a> {
context: &'a Context,
query: &str,
params: Vec<&dyn crate::ToSql>,
) -> Result<Option<Peerstate<'a>>> {
let peerstate = context
) -> Option<Peerstate<'a>> {
context
.sql
.query_row_optional(query, params, |row| {
.query_row(query, params, |row| {
/* all the above queries start with this: SELECT
addr, last_seen, last_seen_autocrypt, prefer_encrypted,
public_key, gossip_timestamp, gossip_key, public_key_fingerprint,
@@ -191,18 +187,15 @@ impl<'a> Peerstate<'a> {
res.public_key_fingerprint = row
.get::<_, Option<String>>(7)?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default();
.transpose()?;
res.gossip_key_fingerprint = row
.get::<_, Option<String>>(8)?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default();
.transpose()?;
res.verified_key_fingerprint = row
.get::<_, Option<String>>(10)?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default();
.transpose()?;
res.public_key = row
.get(4)
.ok()
@@ -218,8 +211,8 @@ impl<'a> Peerstate<'a> {
Ok(res)
})
.await?;
Ok(peerstate)
.await
.ok()
}
pub fn recalc_fingerprint(&mut self) {
@@ -331,9 +324,11 @@ impl<'a> Peerstate<'a> {
pub fn render_gossip_header(&self, min_verified: PeerstateVerifiedStatus) -> Option<String> {
if let Some(key) = self.peek_key(min_verified) {
// TODO: avoid cloning
let public_key = SignedPublicKey::try_from(key.clone()).ok()?;
let header = Aheader::new(
self.addr.clone(),
key.clone(), // TODO: avoid cloning
public_key,
// Autocrypt 1.1.0 specification says that
// `prefer-encrypt` attribute SHOULD NOT be included,
// but we include it anyway to propagate encryption
@@ -408,20 +403,21 @@ impl<'a> Peerstate<'a> {
}
pub async fn save_to_db(&self, sql: &Sql, create: bool) -> crate::sql::Result<()> {
if create {
sql.execute(
"INSERT INTO acpeerstates (addr) VALUES(?);",
paramsv![self.addr],
)
.await?;
}
if self.to_save == Some(ToSave::All) || create {
sql.execute(
if create {
"INSERT INTO acpeerstates (last_seen, last_seen_autocrypt, prefer_encrypted, \
public_key, gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
verified_key, verified_key_fingerprint, addr \
) VALUES(?,?,?,?,?,?,?,?,?,?,?)"
} else {
"UPDATE acpeerstates \
SET last_seen=?, last_seen_autocrypt=?, prefer_encrypted=?, \
public_key=?, gossip_timestamp=?, gossip_key=?, public_key_fingerprint=?, gossip_key_fingerprint=?, \
verified_key=?, verified_key_fingerprint=? \
WHERE addr=?"
},
WHERE addr=?;",
paramsv![
self.last_seen,
self.last_seen_autocrypt,
@@ -454,11 +450,14 @@ impl<'a> Peerstate<'a> {
}
pub fn has_verified_key(&self, fingerprints: &HashSet<Fingerprint>) -> bool {
if let Some(vkc) = &self.verified_key_fingerprint {
fingerprints.contains(vkc) && self.verified_key.is_some()
} else {
false
if self.verified_key.is_some() && self.verified_key_fingerprint.is_some() {
let vkc = self.verified_key_fingerprint.as_ref().unwrap();
if fingerprints.contains(vkc) {
return true;
}
}
false
}
}
@@ -477,7 +476,7 @@ mod tests {
#[async_std::test]
async fn test_peerstate_save_to_db() {
let ctx = crate::test_utils::TestContext::new().await;
let ctx = crate::test_utils::dummy_context().await;
let addr = "hello@mail.com";
let pub_key = alice_keypair().public;
@@ -506,8 +505,7 @@ mod tests {
let peerstate_new = Peerstate::from_addr(&ctx.ctx, addr)
.await
.expect("failed to load peerstate from db")
.expect("no peerstate found in the database");
.expect("failed to load peerstate from db");
// clear to_save, as that is not persissted
peerstate.to_save = None;
@@ -515,14 +513,13 @@ mod tests {
let peerstate_new2 =
Peerstate::from_fingerprint(&ctx.ctx, &ctx.ctx.sql, &pub_key.fingerprint())
.await
.expect("failed to load peerstate from db")
.expect("no peerstate found in the database");
.expect("failed to load peerstate from db");
assert_eq!(peerstate, peerstate_new2);
}
#[async_std::test]
async fn test_peerstate_double_create() {
let ctx = crate::test_utils::TestContext::new().await;
let ctx = crate::test_utils::dummy_context().await;
let addr = "hello@mail.com";
let pub_key = alice_keypair().public;
@@ -555,7 +552,7 @@ mod tests {
#[async_std::test]
async fn test_peerstate_with_empty_gossip_key_save_to_db() {
let ctx = crate::test_utils::TestContext::new().await;
let ctx = crate::test_utils::dummy_context().await;
let addr = "hello@mail.com";
let pub_key = alice_keypair().public;
@@ -588,36 +585,7 @@ mod tests {
// clear to_save, as that is not persissted
peerstate.to_save = None;
assert_eq!(Some(peerstate), peerstate_new);
}
#[async_std::test]
async fn test_peerstate_load_db_defaults() {
let ctx = crate::test_utils::TestContext::new().await;
let addr = "hello@mail.com";
// Old code created peerstates with this code and updated
// other values later. If UPDATE failed, other columns had
// default values, in particular fingerprints were set to
// empty strings instead of NULL. This should not be the case
// anymore, but the regression test still checks that defaults
// can be loaded without errors.
ctx.ctx
.sql
.execute("INSERT INTO acpeerstates (addr) VALUES(?)", paramsv![addr])
.await
.expect("Failed to write to the database");
let peerstate = Peerstate::from_addr(&ctx.ctx, addr)
.await
.expect("Failed to load peerstate from db")
.expect("Loaded peerstate is empty");
// Check that default values for fingerprints are treated like
// NULL.
assert_eq!(peerstate.public_key_fingerprint, None);
assert_eq!(peerstate.gossip_key_fingerprint, None);
assert_eq!(peerstate.verified_key_fingerprint, None);
assert_eq!(peerstate, peerstate_new);
}
// TODO: don't copy this from stress.rs

View File

@@ -272,14 +272,6 @@ pub async fn pk_encrypt(
.await
}
/// Decrypts the message with keys from the private key keyring.
///
/// Receiver private keys are provided in
/// `private_keys_for_decryption`.
///
/// If `ret_signature_fingerprints` is not `None`, stores fingerprints
/// of all keys from the `public_keys_for_validation` keyring that
/// have valid signatures there.
#[allow(clippy::implicit_hasher)]
pub async fn pk_decrypt(
ctext: Vec<u8>,
@@ -298,41 +290,36 @@ pub async fn pk_decrypt(
})
.await?;
if let Some(msg) = msgs.into_iter().next() {
// get_content() will decompress the message if needed,
// but this avoids decompressing it again to check signatures
let msg = msg.decompress()?;
ensure!(!msgs.is_empty(), "No valid messages found");
let content = match msg.get_content()? {
Some(content) => content,
None => bail!("The decrypted message is empty"),
};
let content = match msgs[0].get_content()? {
Some(content) => content,
None => bail!("Decrypted message is empty"),
};
if let Some(ret_signature_fingerprints) = ret_signature_fingerprints {
if !public_keys_for_validation.is_empty() {
let fingerprints = async_std::task::spawn_blocking(move || {
let pkeys = public_keys_for_validation.keys();
if let Some(ret_signature_fingerprints) = ret_signature_fingerprints {
if !public_keys_for_validation.is_empty() {
let fingerprints = async_std::task::spawn_blocking(move || {
let dec_msg = &msgs[0];
let mut fingerprints: Vec<Fingerprint> = Vec::new();
if let signed_msg @ pgp::composed::Message::Signed { .. } = msg {
for pkey in pkeys {
if signed_msg.verify(&pkey.primary_key).is_ok() {
let fp = DcKey::fingerprint(pkey);
fingerprints.push(fp);
}
}
let pkeys = public_keys_for_validation.keys();
let mut fingerprints: Vec<Fingerprint> = Vec::new();
for pkey in pkeys {
if dec_msg.verify(&pkey.primary_key).is_ok() {
let fp = DcKey::fingerprint(pkey);
fingerprints.push(fp);
}
fingerprints
})
.await;
}
fingerprints
})
.await;
ret_signature_fingerprints.extend(fingerprints);
}
ret_signature_fingerprints.extend(fingerprints);
}
Ok(content)
} else {
bail!("No valid messages found");
}
Ok(content)
}
/// Symmetric encryption.
@@ -365,13 +352,11 @@ pub async fn symm_decrypt<T: std::io::Read + std::io::Seek>(
let decryptor = enc_msg.decrypt_with_password(|| passphrase)?;
let msgs = decryptor.collect::<pgp::errors::Result<Vec<_>>>()?;
if let Some(msg) = msgs.first() {
match msg.get_content()? {
Some(content) => Ok(content),
None => bail!("Decrypted message is empty"),
}
} else {
bail!("No valid messages found")
ensure!(!msgs.is_empty(), "No valid messages found");
match msgs[0].get_content()? {
Some(content) => Ok(content),
None => bail!("Decrypted message is empty"),
}
})
.await

View File

@@ -19,8 +19,6 @@ lazy_static::lazy_static! {
Server { protocol: SMTP, socket: STARTTLS, hostname: "newyear.aktivix.org", port: 25, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// aol.md: aol.com
@@ -32,23 +30,6 @@ lazy_static::lazy_static! {
server: vec![
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// arcor.de.md: arcor.de
static ref P_ARCOR_DE: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/arcor-de",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap.arcor.de", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: SSL, hostname: "mail.arcor.de", port: 465, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// autistici.org.md: autistici.org
@@ -62,8 +43,6 @@ lazy_static::lazy_static! {
Server { protocol: SMTP, socket: SSL, hostname: "smtp.autistici.org", port: 465, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// bluewin.ch.md: bluewin.ch
@@ -77,81 +56,16 @@ lazy_static::lazy_static! {
Server { protocol: SMTP, socket: SSL, hostname: "smtpauths.bluewin.ch", port: 465, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// chello.at.md: chello.at
static ref P_CHELLO_AT: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/chello-at",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "mail.mymagenta.at", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: SSL, hostname: "mail.mymagenta.at", port: 465, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// comcast.md: xfinity.com, comcast.net
static ref P_COMCAST: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/comcast",
server: vec![
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// - skipping provider with status OK and no special things to do
// dismail.de.md: dismail.de
static ref P_DISMAIL_DE: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/dismail-de",
server: vec![
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// - skipping provider with status OK and no special things to do
// disroot.md: disroot.org
static ref P_DISROOT: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/disroot",
server: vec![
],
config_defaults: None,
strict_tls: true,
oauth2_authorizer: None,
};
// dubby.org.md: dubby.org
static ref P_DUBBY_ORG: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/dubby-org",
server: vec![
],
config_defaults: Some(vec![
ConfigDefault { key: Config::BccSelf, value: "1" },
ConfigDefault { key: Config::SentboxWatch, value: "0" },
ConfigDefault { key: Config::MvboxWatch, value: "0" },
ConfigDefault { key: Config::MvboxMove, value: "0" },
]),
strict_tls: true,
oauth2_authorizer: None,
};
// - skipping provider with status OK and no special things to do
// example.com.md: example.com, example.org
static ref P_EXAMPLE_COM: Provider = Provider {
@@ -164,8 +78,6 @@ lazy_static::lazy_static! {
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.example.com", port: 1337, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// fastmail.md: fastmail.com
@@ -177,26 +89,6 @@ lazy_static::lazy_static! {
server: vec![
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// five.chat.md: five.chat
static ref P_FIVE_CHAT: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/five-chat",
server: vec![
],
config_defaults: Some(vec![
ConfigDefault { key: Config::BccSelf, value: "1" },
ConfigDefault { key: Config::SentboxWatch, value: "0" },
ConfigDefault { key: Config::MvboxWatch, value: "0" },
ConfigDefault { key: Config::MvboxMove, value: "0" },
]),
strict_tls: true,
oauth2_authorizer: None,
};
// freenet.de.md: freenet.de
@@ -210,8 +102,6 @@ lazy_static::lazy_static! {
Server { protocol: SMTP, socket: STARTTLS, hostname: "mx.freenet.de", port: 587, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// gmail.md: gmail.com, googlemail.com
@@ -225,8 +115,6 @@ lazy_static::lazy_static! {
Server { protocol: SMTP, socket: SSL, hostname: "smtp.gmail.com", port: 465, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: true,
oauth2_authorizer: Some(Oauth2Authorizer::Gmail),
};
// gmx.net.md: gmx.net, gmx.de, gmx.at, gmx.ch, gmx.org, gmx.eu, gmx.info, gmx.biz, gmx.com
@@ -241,35 +129,10 @@ lazy_static::lazy_static! {
Server { protocol: SMTP, socket: STARTTLS, hostname: "mail.gmx.net", port: 587, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// hey.com.md: hey.com
static ref P_HEY_COM: Provider = Provider {
status: Status::BROKEN,
before_login_hint: "hey.com does not offer the standard IMAP e-mail protocol, so you cannot log in with Delta Chat to hey.com.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/hey-com",
server: vec![
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// i.ua.md: i.ua
static ref P_I_UA: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/i-ua",
server: vec![
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// - skipping provider with status OK and no special things to do
// icloud.md: icloud.com, me.com, mac.com
static ref P_ICLOUD: Provider = Provider {
@@ -282,61 +145,19 @@ lazy_static::lazy_static! {
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.mail.me.com", port: 587, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// kolst.com.md: kolst.com
static ref P_KOLST_COM: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/kolst-com",
server: vec![
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// - skipping provider with status OK and no special things to do
// kontent.com.md: kontent.com
static ref P_KONTENT_COM: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/kontent-com",
server: vec![
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// - skipping provider with status OK and no special things to do
// mail.ru.md: mail.ru, inbox.ru, bk.ru, list.ru
static ref P_MAIL_RU: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/mail-ru",
server: vec![
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// - skipping provider with status OK and no special things to do
// mailbox.org.md: mailbox.org, secure.mailbox.org
static ref P_MAILBOX_ORG: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/mailbox-org",
server: vec![
],
config_defaults: None,
strict_tls: true,
oauth2_authorizer: None,
};
// - skipping provider with status OK and no special things to do
// nauta.cu.md: nauta.cu
static ref P_NAUTA_CU: Provider = Provider {
@@ -357,8 +178,6 @@ lazy_static::lazy_static! {
ConfigDefault { key: Config::E2eeEnabled, value: "0" },
ConfigDefault { key: Config::MediaQuality, value: "1" },
]),
strict_tls: false,
oauth2_authorizer: None,
};
// outlook.com.md: hotmail.com, outlook.com, office365.com, outlook.com.tr, live.com
@@ -372,11 +191,9 @@ lazy_static::lazy_static! {
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp-mail.outlook.com", port: 587, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// posteo.md: posteo.de, posteo.af, posteo.at, posteo.be, posteo.ch, posteo.cl, posteo.co, posteo.co.uk, posteo.com.br, posteo.cr, posteo.cz, posteo.dk, posteo.ee, posteo.es, posteo.eu, posteo.fi, posteo.gl, posteo.gr, posteo.hn, posteo.hr, posteo.hu, posteo.ie, posteo.in, posteo.is, posteo.jp, posteo.la, posteo.li, posteo.lt, posteo.lu, posteo.me, posteo.mx, posteo.my, posteo.net, posteo.nl, posteo.no, posteo.nz, posteo.org, posteo.pe, posteo.pl, posteo.pm, posteo.pt, posteo.ro, posteo.ru, posteo.se, posteo.sg, posteo.si, posteo.tn, posteo.uk, posteo.us
// posteo.md: posteo.de
static ref P_POSTEO: Provider = Provider {
status: Status::OK,
before_login_hint: "",
@@ -387,8 +204,6 @@ lazy_static::lazy_static! {
Server { protocol: SMTP, socket: STARTTLS, hostname: "posteo.de", port: 587, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: true,
oauth2_authorizer: None,
};
// protonmail.md: protonmail.com, protonmail.ch
@@ -400,48 +215,16 @@ lazy_static::lazy_static! {
server: vec![
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// riseup.net.md: riseup.net
static ref P_RISEUP_NET: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/riseup-net",
server: vec![
],
config_defaults: None,
strict_tls: true,
oauth2_authorizer: None,
};
// - skipping provider with status OK and no special things to do
// rogers.com.md: rogers.com
static ref P_ROGERS_COM: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/rogers-com",
server: vec![
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// - skipping provider with status OK and no special things to do
// systemli.org.md: systemli.org
static ref P_SYSTEMLI_ORG: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/systemli-org",
server: vec![
],
config_defaults: None,
strict_tls: true,
oauth2_authorizer: None,
};
// - skipping provider with status OK and no special things to do
// t-online.md: t-online.de, magenta.de
static ref P_T_ONLINE: Provider = Provider {
@@ -452,8 +235,6 @@ lazy_static::lazy_static! {
server: vec![
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// testrun.md: testrun.org
@@ -467,14 +248,7 @@ lazy_static::lazy_static! {
Server { protocol: IMAP, socket: STARTTLS, hostname: "testrun.org", port: 143, username_pattern: EMAIL },
Server { protocol: SMTP, socket: STARTTLS, hostname: "testrun.org", port: 587, username_pattern: EMAIL },
],
config_defaults: Some(vec![
ConfigDefault { key: Config::BccSelf, value: "1" },
ConfigDefault { key: Config::SentboxWatch, value: "0" },
ConfigDefault { key: Config::MvboxWatch, value: "0" },
ConfigDefault { key: Config::MvboxMove, value: "0" },
]),
strict_tls: true,
oauth2_authorizer: None,
config_defaults: None,
};
// tiscali.it.md: tiscali.it
@@ -488,50 +262,13 @@ lazy_static::lazy_static! {
Server { protocol: SMTP, socket: SSL, hostname: "smtp.tiscali.it", port: 465, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// ukr.net.md: ukr.net
static ref P_UKR_NET: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/ukr-net",
server: vec![
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// - skipping provider with status OK and no special things to do
// vfemail.md: vfemail.net
static ref P_VFEMAIL: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/vfemail",
server: vec![
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// vodafone.de.md: vodafone.de, vodafonemail.de
static ref P_VODAFONE_DE: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/vodafone-de",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap.vodafonemail.de", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.vodafonemail.de", port: 587, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// - skipping provider with status OK and no special things to do
// web.de.md: web.de, email.de, flirt.ms, hallo.ms, kuss.ms, love.ms, magic.ms, singles.ms, cool.ms, kanzler.ms, okay.ms, party.ms, pop.ms, stars.ms, techno.ms, clever.ms, deutschland.ms, genial.ms, ich.ms, online.ms, smart.ms, wichtig.ms, action.ms, fussball.ms, joker.ms, planet.ms, power.ms
static ref P_WEB_DE: Provider = Provider {
@@ -545,14 +282,12 @@ lazy_static::lazy_static! {
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.web.de", port: 587, username_pattern: EMAILLOCALPART },
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// yahoo.md: yahoo.com, yahoo.de, yahoo.it, yahoo.fr, yahoo.es, yahoo.se, yahoo.co.uk, yahoo.co.nz, yahoo.com.au, yahoo.com.ar, yahoo.com.br, yahoo.com.mx, ymail.com, rocketmail.com, yahoodns.net
static ref P_YAHOO: Provider = Provider {
status: Status::PREPARATION,
before_login_hint: "To use Delta Chat with your Yahoo email address you have to create an \"App-Password\" in the account security screen.",
before_login_hint: "To use Delta Chat with your Yahoo email address you have to allow \"less secure apps\" in the Yahoo webinterface.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/yahoo",
server: vec![
@@ -560,11 +295,9 @@ lazy_static::lazy_static! {
Server { protocol: SMTP, socket: SSL, hostname: "smtp.mail.yahoo.com", port: 465, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// yandex.ru.md: yandex.com, yandex.by, yandex.kz, yandex.ru, yandex.ua, ya.ru, narod.ru
// yandex.ru.md: yandex.ru, yandex.com
static ref P_YANDEX_RU: Provider = Provider {
status: Status::PREPARATION,
before_login_hint: "For Yandex accounts, you have to set IMAP protocol option turned on.",
@@ -573,8 +306,6 @@ lazy_static::lazy_static! {
server: vec![
],
config_defaults: None,
strict_tls: true,
oauth2_authorizer: Some(Oauth2Authorizer::Yandex),
};
// ziggo.nl.md: ziggo.nl
@@ -588,26 +319,16 @@ lazy_static::lazy_static! {
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.ziggo.nl", port: 587, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
pub static ref PROVIDER_DATA: HashMap<&'static str, &'static Provider> = [
("aktivix.org", &*P_AKTIVIX_ORG),
("aol.com", &*P_AOL),
("arcor.de", &*P_ARCOR_DE),
("autistici.org", &*P_AUTISTICI_ORG),
("bluewin.ch", &*P_BLUEWIN_CH),
("chello.at", &*P_CHELLO_AT),
("xfinity.com", &*P_COMCAST),
("comcast.net", &*P_COMCAST),
("dismail.de", &*P_DISMAIL_DE),
("disroot.org", &*P_DISROOT),
("dubby.org", &*P_DUBBY_ORG),
("example.com", &*P_EXAMPLE_COM),
("example.org", &*P_EXAMPLE_COM),
("fastmail.com", &*P_FASTMAIL),
("five.chat", &*P_FIVE_CHAT),
("freenet.de", &*P_FREENET_DE),
("gmail.com", &*P_GMAIL),
("googlemail.com", &*P_GMAIL),
@@ -620,19 +341,9 @@ lazy_static::lazy_static! {
("gmx.info", &*P_GMX_NET),
("gmx.biz", &*P_GMX_NET),
("gmx.com", &*P_GMX_NET),
("hey.com", &*P_HEY_COM),
("i.ua", &*P_I_UA),
("icloud.com", &*P_ICLOUD),
("me.com", &*P_ICLOUD),
("mac.com", &*P_ICLOUD),
("kolst.com", &*P_KOLST_COM),
("kontent.com", &*P_KONTENT_COM),
("mail.ru", &*P_MAIL_RU),
("inbox.ru", &*P_MAIL_RU),
("bk.ru", &*P_MAIL_RU),
("list.ru", &*P_MAIL_RU),
("mailbox.org", &*P_MAILBOX_ORG),
("secure.mailbox.org", &*P_MAILBOX_ORG),
("nauta.cu", &*P_NAUTA_CU),
("hotmail.com", &*P_OUTLOOK_COM),
("outlook.com", &*P_OUTLOOK_COM),
@@ -640,67 +351,12 @@ lazy_static::lazy_static! {
("outlook.com.tr", &*P_OUTLOOK_COM),
("live.com", &*P_OUTLOOK_COM),
("posteo.de", &*P_POSTEO),
("posteo.af", &*P_POSTEO),
("posteo.at", &*P_POSTEO),
("posteo.be", &*P_POSTEO),
("posteo.ch", &*P_POSTEO),
("posteo.cl", &*P_POSTEO),
("posteo.co", &*P_POSTEO),
("posteo.co.uk", &*P_POSTEO),
("posteo.com.br", &*P_POSTEO),
("posteo.cr", &*P_POSTEO),
("posteo.cz", &*P_POSTEO),
("posteo.dk", &*P_POSTEO),
("posteo.ee", &*P_POSTEO),
("posteo.es", &*P_POSTEO),
("posteo.eu", &*P_POSTEO),
("posteo.fi", &*P_POSTEO),
("posteo.gl", &*P_POSTEO),
("posteo.gr", &*P_POSTEO),
("posteo.hn", &*P_POSTEO),
("posteo.hr", &*P_POSTEO),
("posteo.hu", &*P_POSTEO),
("posteo.ie", &*P_POSTEO),
("posteo.in", &*P_POSTEO),
("posteo.is", &*P_POSTEO),
("posteo.jp", &*P_POSTEO),
("posteo.la", &*P_POSTEO),
("posteo.li", &*P_POSTEO),
("posteo.lt", &*P_POSTEO),
("posteo.lu", &*P_POSTEO),
("posteo.me", &*P_POSTEO),
("posteo.mx", &*P_POSTEO),
("posteo.my", &*P_POSTEO),
("posteo.net", &*P_POSTEO),
("posteo.nl", &*P_POSTEO),
("posteo.no", &*P_POSTEO),
("posteo.nz", &*P_POSTEO),
("posteo.org", &*P_POSTEO),
("posteo.pe", &*P_POSTEO),
("posteo.pl", &*P_POSTEO),
("posteo.pm", &*P_POSTEO),
("posteo.pt", &*P_POSTEO),
("posteo.ro", &*P_POSTEO),
("posteo.ru", &*P_POSTEO),
("posteo.se", &*P_POSTEO),
("posteo.sg", &*P_POSTEO),
("posteo.si", &*P_POSTEO),
("posteo.tn", &*P_POSTEO),
("posteo.uk", &*P_POSTEO),
("posteo.us", &*P_POSTEO),
("protonmail.com", &*P_PROTONMAIL),
("protonmail.ch", &*P_PROTONMAIL),
("riseup.net", &*P_RISEUP_NET),
("rogers.com", &*P_ROGERS_COM),
("systemli.org", &*P_SYSTEMLI_ORG),
("t-online.de", &*P_T_ONLINE),
("magenta.de", &*P_T_ONLINE),
("testrun.org", &*P_TESTRUN),
("tiscali.it", &*P_TISCALI_IT),
("ukr.net", &*P_UKR_NET),
("vfemail.net", &*P_VFEMAIL),
("vodafone.de", &*P_VODAFONE_DE),
("vodafonemail.de", &*P_VODAFONE_DE),
("web.de", &*P_WEB_DE),
("email.de", &*P_WEB_DE),
("flirt.ms", &*P_WEB_DE),
@@ -743,13 +399,8 @@ lazy_static::lazy_static! {
("ymail.com", &*P_YAHOO),
("rocketmail.com", &*P_YAHOO),
("yahoodns.net", &*P_YAHOO),
("yandex.com", &*P_YANDEX_RU),
("yandex.by", &*P_YANDEX_RU),
("yandex.kz", &*P_YANDEX_RU),
("yandex.ru", &*P_YANDEX_RU),
("yandex.ua", &*P_YANDEX_RU),
("ya.ru", &*P_YANDEX_RU),
("narod.ru", &*P_YANDEX_RU),
("yandex.com", &*P_YANDEX_RU),
("ziggo.nl", &*P_ZIGGO_NL),
].iter().copied().collect();
}

View File

@@ -35,13 +35,6 @@ pub enum UsernamePattern {
EMAILLOCALPART = 2,
}
#[derive(Debug, PartialEq)]
#[repr(u8)]
pub enum Oauth2Authorizer {
Yandex = 1,
Gmail = 2,
}
#[derive(Debug)]
pub struct Server {
pub protocol: Protocol,
@@ -79,8 +72,6 @@ pub struct Provider {
pub overview_page: &'static str,
pub server: Vec<Server>,
pub config_defaults: Option<Vec<ConfigDefault>>,
pub strict_tls: bool,
pub oauth2_authorizer: Option<Oauth2Authorizer>,
}
impl Provider {

View File

@@ -100,12 +100,6 @@ def process_data(data, file):
config_defaults = process_config_defaults(data)
strict_tls = data.get("strict_tls", False)
strict_tls = "true" if strict_tls else "false"
oauth2 = data.get("oauth2", "")
oauth2 = "Some(Oauth2Authorizer::" + camel(oauth2) + ")" if oauth2 != "" else "None"
provider = ""
before_login_hint = cleanstr(data.get("before_login_hint", ""))
after_login_hint = cleanstr(data.get("after_login_hint", ""))
@@ -117,8 +111,6 @@ def process_data(data, file):
provider += " overview_page: \"" + file2url(file) + "\",\n"
provider += " server: vec![\n" + server + " ],\n"
provider += " config_defaults: " + config_defaults + ",\n"
provider += " strict_tls: " + strict_tls + ",\n"
provider += " oauth2_authorizer: " + oauth2 + ",\n"
provider += " };\n\n"
else:
raise TypeError("SMTP and IMAP must be specified together or left out both")
@@ -129,11 +121,11 @@ def process_data(data, file):
# finally, add the provider
global out_all, out_domains
out_all += " // " + file[file.rindex("/")+1:] + ": " + comment.strip(", ") + "\n"
# also add provider with no special things to do -
# eg. _not_ supporting oauth2 is also an information and we can skip the mx-lookup in this case
out_all += provider
out_domains += domains
if status == "OK" and before_login_hint == "" and after_login_hint == "" and server == "" and config_defaults == "None":
out_all += " // - skipping provider with status OK and no special things to do\n\n"
else:
out_all += provider
out_domains += domains
def process_file(file):

146
src/qr.rs
View File

@@ -12,13 +12,11 @@ use crate::context::Context;
use crate::error::{bail, ensure, format_err, Error};
use crate::key::Fingerprint;
use crate::lot::{Lot, LotState};
use crate::message::Message;
use crate::param::*;
use crate::peerstate::*;
const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase
const DCACCOUNT_SCHEME: &str = "DCACCOUNT:";
const DCWEBRTC_SCHEME: &str = "DCWEBRTC:";
const MAILTO_SCHEME: &str = "mailto:";
const MATMSG_SCHEME: &str = "MATMSG:";
const VCARD_SCHEME: &str = "BEGIN:VCARD";
@@ -53,8 +51,6 @@ pub async fn check_qr(context: &Context, qr: impl AsRef<str>) -> Lot {
decode_openpgp(context, qr).await
} else if starts_with_ignore_case(qr, DCACCOUNT_SCHEME) {
decode_account(context, qr)
} else if starts_with_ignore_case(qr, DCWEBRTC_SCHEME) {
decode_webrtc_instance(context, qr)
} else if qr.starts_with(MAILTO_SCHEME) {
decode_mailto(context, qr).await
} else if qr.starts_with(SMTP_SCHEME) {
@@ -72,7 +68,6 @@ pub async fn check_qr(context: &Context, qr: impl AsRef<str>) -> Lot {
/// scheme: `OPENPGP4FPR:FINGERPRINT#a=ADDR&n=NAME&i=INVITENUMBER&s=AUTH`
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&g=GROUPNAME&x=GROUPID&i=INVITENUMBER&s=AUTH`
#[allow(clippy::indexing_slicing)]
async fn decode_openpgp(context: &Context, qr: &str) -> Lot {
let payload = &qr[OPENPGP4FPR_SCHEME.len()..];
@@ -143,10 +138,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Lot {
let mut lot = Lot::new();
// retrieve known state for this fingerprint
let peerstate = match Peerstate::from_fingerprint(context, &context.sql, &fingerprint).await {
Ok(peerstate) => peerstate,
Err(err) => return format_err!("Can't load peerstate: {}", err).into(),
};
let peerstate = Peerstate::from_fingerprint(context, &context.sql, &fingerprint).await;
if invitenumber.is_none() || auth.is_none() {
if let Some(peerstate) = peerstate {
@@ -195,14 +187,13 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Lot {
}
/// scheme: `DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3`
#[allow(clippy::indexing_slicing)]
fn decode_account(_context: &Context, qr: &str) -> Lot {
let payload = &qr[DCACCOUNT_SCHEME.len()..];
let mut lot = Lot::new();
if let Ok(url) = url::Url::parse(payload) {
if url.scheme() == "http" || url.scheme() == "https" {
if url.scheme() == "https" {
lot.state = LotState::QrAccount;
lot.text1 = url.host_str().map(|x| x.to_string());
} else {
@@ -217,31 +208,6 @@ fn decode_account(_context: &Context, qr: &str) -> Lot {
lot
}
/// scheme: `DCWEBRTC:https://meet.jit.si/$ROOM`
#[allow(clippy::indexing_slicing)]
fn decode_webrtc_instance(_context: &Context, qr: &str) -> Lot {
let payload = &qr[DCWEBRTC_SCHEME.len()..];
let mut lot = Lot::new();
let (_type, url) = Message::parse_webrtc_instance(payload);
if let Ok(url) = url::Url::parse(&url) {
if url.scheme() == "http" || url.scheme() == "https" {
lot.state = LotState::QrWebrtcInstance;
lot.text1 = url.host_str().map(|x| x.to_string());
lot.text2 = Some(payload.to_string())
} else {
lot.state = LotState::QrError;
lot.text1 = Some(format!("Bad scheme for webrtc instance: {}", payload));
}
} else {
lot.state = LotState::QrError;
lot.text1 = Some(format!("Invalid webrtc instance: {}", payload));
}
lot
}
#[derive(Debug, Deserialize)]
struct CreateAccountResponse {
email: String,
@@ -251,8 +217,7 @@ struct CreateAccountResponse {
/// take a qr of the type DC_QR_ACCOUNT, parse it's parameters,
/// download additional information from the contained url and set the parameters.
/// on success, a configure::configure() should be able to log in to the account
#[allow(clippy::indexing_slicing)]
async fn set_account_from_qr(context: &Context, qr: &str) -> Result<(), Error> {
pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<(), Error> {
let url_str = &qr[DCACCOUNT_SCHEME.len()..];
let response: Result<CreateAccountResponse, surf::Error> =
@@ -272,24 +237,9 @@ async fn set_account_from_qr(context: &Context, qr: &str) -> Result<(), Error> {
Ok(())
}
pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<(), Error> {
match check_qr(context, &qr).await.state {
LotState::QrAccount => set_account_from_qr(context, qr).await,
LotState::QrWebrtcInstance => {
let val = decode_webrtc_instance(context, qr).text2;
context
.set_config(Config::WebrtcInstance, val.as_deref())
.await?;
Ok(())
}
_ => bail!("qr code does not contain config: {}", qr),
}
}
/// Extract address for the mailto scheme.
///
/// Scheme: `mailto:addr...?subject=...&body=..`
#[allow(clippy::indexing_slicing)]
async fn decode_mailto(context: &Context, qr: &str) -> Lot {
let payload = &qr[MAILTO_SCHEME.len()..];
@@ -311,7 +261,6 @@ async fn decode_mailto(context: &Context, qr: &str) -> Lot {
/// Extract address for the smtp scheme.
///
/// Scheme: `SMTP:addr...:subject...:body...`
#[allow(clippy::indexing_slicing)]
async fn decode_smtp(context: &Context, qr: &str) -> Lot {
let payload = &qr[SMTP_SCHEME.len()..];
@@ -334,7 +283,6 @@ async fn decode_smtp(context: &Context, qr: &str) -> Lot {
/// Scheme: `MATMSG:TO:addr...;SUB:subject...;BODY:body...;`
///
/// There may or may not be linebreaks after the fields.
#[allow(clippy::indexing_slicing)]
async fn decode_matmsg(context: &Context, qr: &str) -> Lot {
// Does not work when the text `TO:` is used in subject/body _and_ TO: is not the first field.
// we ignore this case.
@@ -368,15 +316,14 @@ lazy_static! {
/// Extract address for the matmsg scheme.
///
/// Scheme: `VCARD:BEGIN\nN:last name;first name;...;\nEMAIL;<type>:addr...;
#[allow(clippy::indexing_slicing)]
async fn decode_vcard(context: &Context, qr: &str) -> Lot {
let name = VCARD_NAME_RE
.captures(qr)
.and_then(|caps| {
let last_name = caps.get(1)?.as_str().trim();
let first_name = caps.get(2)?.as_str().trim();
.map(|caps| {
let last_name = &caps[1];
let first_name = &caps[2];
Some(format!("{} {}", first_name, last_name))
format!("{} {}", first_name.trim(), last_name.trim())
})
.unwrap_or_default();
@@ -436,11 +383,11 @@ fn normalize_address(addr: &str) -> Result<String, Error> {
mod tests {
use super::*;
use crate::test_utils::TestContext;
use crate::test_utils::dummy_context;
#[async_std::test]
async fn test_decode_http() {
let ctx = TestContext::new().await;
let ctx = dummy_context().await;
let res = check_qr(&ctx.ctx, "http://www.hello.com").await;
@@ -452,7 +399,7 @@ mod tests {
#[async_std::test]
async fn test_decode_https() {
let ctx = TestContext::new().await;
let ctx = dummy_context().await;
let res = check_qr(&ctx.ctx, "https://www.hello.com").await;
@@ -464,7 +411,7 @@ mod tests {
#[async_std::test]
async fn test_decode_text() {
let ctx = TestContext::new().await;
let ctx = dummy_context().await;
let res = check_qr(&ctx.ctx, "I am so cool").await;
@@ -476,7 +423,7 @@ mod tests {
#[async_std::test]
async fn test_decode_vcard() {
let ctx = TestContext::new().await;
let ctx = dummy_context().await;
let res = check_qr(
&ctx.ctx,
@@ -494,7 +441,7 @@ mod tests {
#[async_std::test]
async fn test_decode_matmsg() {
let ctx = TestContext::new().await;
let ctx = dummy_context().await;
let res = check_qr(
&ctx.ctx,
@@ -512,7 +459,7 @@ mod tests {
#[async_std::test]
async fn test_decode_mailto() {
let ctx = TestContext::new().await;
let ctx = dummy_context().await;
let res = check_qr(
&ctx.ctx,
@@ -538,7 +485,7 @@ mod tests {
#[async_std::test]
async fn test_decode_smtp() {
let ctx = TestContext::new().await;
let ctx = dummy_context().await;
let res = check_qr(&ctx.ctx, "SMTP:stress@test.local:subjecthello:bodyworld").await;
@@ -552,7 +499,7 @@ mod tests {
#[async_std::test]
async fn test_decode_openpgp_group() {
let ctx = TestContext::new().await;
let ctx = dummy_context().await;
let res = check_qr(
&ctx.ctx,
@@ -581,7 +528,7 @@ mod tests {
#[async_std::test]
async fn test_decode_openpgp_secure_join() {
let ctx = TestContext::new().await;
let ctx = dummy_context().await;
let res = check_qr(
&ctx.ctx,
@@ -609,7 +556,7 @@ mod tests {
#[async_std::test]
async fn test_decode_openpgp_without_addr() {
let ctx = TestContext::new().await;
let ctx = dummy_context().await;
let res = check_qr(
&ctx.ctx,
@@ -644,7 +591,7 @@ mod tests {
#[async_std::test]
async fn test_decode_account() {
let ctx = TestContext::new().await;
let ctx = dummy_context().await;
let res = check_qr(
&ctx.ctx,
@@ -664,31 +611,12 @@ mod tests {
assert_eq!(res.get_text1().unwrap(), "example.org");
}
#[async_std::test]
async fn test_decode_webrtc_instance() {
let ctx = TestContext::new().await;
let res = check_qr(&ctx.ctx, "DCWEBRTC:basicwebrtc:https://basicurl.com/$ROOM").await;
assert_eq!(res.get_state(), LotState::QrWebrtcInstance);
assert_eq!(res.get_text1().unwrap(), "basicurl.com");
assert_eq!(
res.get_text2().unwrap(),
"basicwebrtc:https://basicurl.com/$ROOM"
);
// Test it again with mixcased "dcWebRTC:" uri scheme
let res = check_qr(&ctx.ctx, "dcWebRTC:https://example.org/").await;
assert_eq!(res.get_state(), LotState::QrWebrtcInstance);
assert_eq!(res.get_text1().unwrap(), "example.org");
assert_eq!(res.get_text2().unwrap(), "https://example.org/");
}
#[async_std::test]
async fn test_decode_account_bad_scheme() {
let ctx = TestContext::new().await;
let ctx = dummy_context().await;
let res = check_qr(
&ctx.ctx,
"DCACCOUNT:ftp://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
"DCACCOUNT:http://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
)
.await;
assert_eq!(res.get_state(), LotState::QrError);
@@ -697,40 +625,10 @@ mod tests {
// Test it again with lowercased "dcaccount:" uri scheme
let res = check_qr(
&ctx.ctx,
"dcaccount:ftp://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
"dcaccount:http://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
)
.await;
assert_eq!(res.get_state(), LotState::QrError);
assert!(res.get_text1().is_some());
}
#[async_std::test]
async fn test_set_config_from_qr() {
let ctx = TestContext::new().await;
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await.is_none());
let res = set_config_from_qr(&ctx.ctx, "badqr:https://example.org/").await;
assert!(!res.is_ok());
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await.is_none());
let res = set_config_from_qr(&ctx.ctx, "https://no.qr").await;
assert!(!res.is_ok());
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await.is_none());
let res = set_config_from_qr(&ctx.ctx, "dcwebrtc:https://example.org/").await;
assert!(res.is_ok());
assert_eq!(
ctx.ctx.get_config(Config::WebrtcInstance).await.unwrap(),
"https://example.org/"
);
let res =
set_config_from_qr(&ctx.ctx, "DCWEBRTC:basicwebrtc:https://foo.bar/?$ROOM&test").await;
assert!(res.is_ok());
assert_eq!(
ctx.ctx.get_config(Config::WebrtcInstance).await.unwrap(),
"basicwebrtc:https://foo.bar/?$ROOM&test"
);
}
}

View File

@@ -36,6 +36,14 @@ impl Context {
self.scheduler.read().await.interrupt_inbox(info).await;
}
pub(crate) async fn interrupt_sentbox(&self, info: InterruptInfo) {
self.scheduler.read().await.interrupt_sentbox(info).await;
}
pub(crate) async fn interrupt_mvbox(&self, info: InterruptInfo) {
self.scheduler.read().await.interrupt_mvbox(info).await;
}
pub(crate) async fn interrupt_smtp(&self, info: InterruptInfo) {
self.scheduler.read().await.interrupt_smtp(info).await;
}
@@ -74,18 +82,7 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
}
None => {
jobs_loaded = 0;
// Expunge folder if needed, e.g. if some jobs have
// deleted messages on the server.
if let Err(err) = connection.maybe_close_folder(&ctx).await {
warn!(ctx, "failed to close folder: {:?}", err);
}
info = if ctx.get_config_bool(Config::InboxWatch).await {
fetch_idle(&ctx, &mut connection, Config::ConfiguredInboxFolder).await
} else {
connection.fake_idle(&ctx, None).await
};
info = fetch_idle(&ctx, &mut connection, Config::ConfiguredInboxFolder).await;
}
}
}
@@ -105,14 +102,14 @@ async fn fetch(ctx: &Context, connection: &mut Imap) {
match ctx.get_config(Config::ConfiguredInboxFolder).await {
Some(watch_folder) => {
if let Err(err) = connection.connect_configured(&ctx).await {
error_network!(ctx, "{}", err);
error!(ctx, "{}", err);
return;
}
// fetch
if let Err(err) = connection.fetch(&ctx, &watch_folder).await {
connection.trigger_reconnect();
warn!(ctx, "{}", err);
error!(ctx, "{}", err);
}
}
None => {
@@ -127,14 +124,14 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
Some(watch_folder) => {
// connect and fake idle if unable to connect
if let Err(err) = connection.connect_configured(&ctx).await {
warn!(ctx, "imap connection failed: {}", err);
error!(ctx, "imap connection failed: {}", err);
return connection.fake_idle(&ctx, None).await;
}
// fetch
if let Err(err) = connection.fetch(&ctx, &watch_folder).await {
connection.trigger_reconnect();
warn!(ctx, "{}", err);
error!(ctx, "{}", err);
}
// idle
@@ -144,7 +141,7 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
.await
.unwrap_or_else(|err| {
connection.trigger_reconnect();
warn!(ctx, "{}", err);
error!(ctx, "{}", err);
InterruptInfo::new(false, None)
})
} else {
@@ -152,7 +149,7 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
}
}
None => {
warn!(ctx, "Can not watch {} folder, not set", folder);
warn!(ctx, "Can not watch inbox folder, not set");
connection.fake_idle(&ctx, None).await
}
}
@@ -246,21 +243,29 @@ impl Scheduler {
let (smtp, smtp_handlers) = SmtpConnectionState::new();
let (inbox, inbox_handlers) = ImapConnectionState::new();
*self = Scheduler::Running {
inbox,
mvbox,
sentbox,
smtp,
inbox_handle: None,
mvbox_handle: None,
sentbox_handle: None,
smtp_handle: None,
};
let (inbox_start_send, inbox_start_recv) = channel(1);
let (mvbox_start_send, mvbox_start_recv) = channel(1);
let mut mvbox_handle = None;
let (sentbox_start_send, sentbox_start_recv) = channel(1);
let mut sentbox_handle = None;
let (smtp_start_send, smtp_start_recv) = channel(1);
let ctx1 = ctx.clone();
let inbox_handle = Some(task::spawn(async move {
inbox_loop(ctx1, inbox_start_send, inbox_handlers).await
}));
if ctx.get_config_bool(Config::MvboxWatch).await {
if let Scheduler::Running { inbox_handle, .. } = self {
let ctx1 = ctx.clone();
mvbox_handle = Some(task::spawn(async move {
*inbox_handle = Some(task::spawn(async move {
inbox_loop(ctx1, inbox_start_send, inbox_handlers).await
}));
}
let (mvbox_start_send, mvbox_start_recv) = channel(1);
if let Scheduler::Running { mvbox_handle, .. } = self {
let ctx1 = ctx.clone();
*mvbox_handle = Some(task::spawn(async move {
simple_imap_loop(
ctx1,
mvbox_start_send,
@@ -269,13 +274,12 @@ impl Scheduler {
)
.await
}));
} else {
mvbox_start_send.send(()).await;
}
if ctx.get_config_bool(Config::SentboxWatch).await {
let (sentbox_start_send, sentbox_start_recv) = channel(1);
if let Scheduler::Running { sentbox_handle, .. } = self {
let ctx1 = ctx.clone();
sentbox_handle = Some(task::spawn(async move {
*sentbox_handle = Some(task::spawn(async move {
simple_imap_loop(
ctx1,
sentbox_start_send,
@@ -284,25 +288,15 @@ impl Scheduler {
)
.await
}));
} else {
sentbox_start_send.send(()).await;
}
let ctx1 = ctx.clone();
let smtp_handle = Some(task::spawn(async move {
smtp_loop(ctx1, smtp_start_send, smtp_handlers).await
}));
*self = Scheduler::Running {
inbox,
mvbox,
sentbox,
smtp,
inbox_handle,
mvbox_handle,
sentbox_handle,
smtp_handle,
};
let (smtp_start_send, smtp_start_recv) = channel(1);
if let Scheduler::Running { smtp_handle, .. } = self {
let ctx1 = ctx.clone();
*smtp_handle = Some(task::spawn(async move {
smtp_loop(ctx1, smtp_start_send, smtp_handlers).await
}));
}
// wait for all loops to be started
if let Err(err) = inbox_start_recv
@@ -392,18 +386,10 @@ impl Scheduler {
smtp_handle,
..
} => {
if let Some(handle) = inbox_handle.take() {
handle.await;
}
if let Some(handle) = mvbox_handle.take() {
handle.await;
}
if let Some(handle) = sentbox_handle.take() {
handle.await;
}
if let Some(handle) = smtp_handle.take() {
handle.await;
}
inbox_handle.take().expect("inbox not started").await;
mvbox_handle.take().expect("mvbox not started").await;
sentbox_handle.take().expect("sentbox not started").await;
smtp_handle.take().expect("smtp not started").await;
*self = Scheduler::Stopped;
}
@@ -485,6 +471,7 @@ impl SmtpConnectionState {
}
}
#[derive(Debug)]
struct SmtpConnectionHandlers {
connection: Smtp,
stop_receiver: Receiver<()>,

View File

@@ -114,7 +114,7 @@ pub async fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> O
Some(format!(
"OPENPGP4FPR:{}#a={}&g={}&x={}&i={}&s={}",
fingerprint.hex(),
fingerprint,
self_addr_urlencoded,
&group_name_urlencoded,
&chat.grpid,
@@ -129,11 +129,7 @@ pub async fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> O
// parameters used: a=n=i=s=
Some(format!(
"OPENPGP4FPR:{}#a={}&n={}&i={}&s={}",
fingerprint.hex(),
self_addr_urlencoded,
self_name_urlencoded,
&invitenumber,
&auth,
fingerprint, self_addr_urlencoded, self_name_urlencoded, &invitenumber, &auth,
))
};
@@ -352,8 +348,9 @@ async fn send_handshake_msg(
}
async fn chat_id_2_contact_id(context: &Context, contact_chat_id: ChatId) -> u32 {
if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await[..] {
contact_id
let contacts = chat::get_chat_contacts(context, contact_chat_id).await;
if contacts.len() == 1 {
contacts[0]
} else {
0
}
@@ -364,22 +361,11 @@ async fn fingerprint_equals_sender(
fingerprint: &Fingerprint,
contact_chat_id: ChatId,
) -> bool {
if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await[..] {
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
let peerstate = match Peerstate::from_addr(context, contact.get_addr()).await {
Ok(peerstate) => peerstate,
Err(err) => {
warn!(
context,
"Failed to sender peerstate for {}: {}",
contact.get_addr(),
err
);
return false;
}
};
let contacts = chat::get_chat_contacts(context, contact_chat_id).await;
if let Some(peerstate) = peerstate {
if contacts.len() == 1 {
if let Ok(contact) = Contact::load_from_db(context, contacts[0]).await {
if let Some(peerstate) = Peerstate::from_addr(context, contact.get_addr()).await {
if peerstate.public_key_fingerprint.is_some()
&& fingerprint == peerstate.public_key_fingerprint.as_ref().unwrap()
{
@@ -436,7 +422,6 @@ pub(crate) enum HandshakeMessage {
/// When handle_securejoin_handshake() is called,
/// the message is not yet filed in the database;
/// this is done by receive_imf() later on as needed.
#[allow(clippy::indexing_slicing)]
pub(crate) async fn handle_securejoin_handshake(
context: &Context,
mime_message: &MimeMessage,
@@ -987,7 +972,7 @@ async fn could_not_establish_secure_connection(
async fn mark_peer_as_verified(context: &Context, fingerprint: &Fingerprint) -> Result<(), Error> {
if let Some(ref mut peerstate) =
Peerstate::from_fingerprint(context, &context.sql, fingerprint).await?
Peerstate::from_fingerprint(context, &context.sql, fingerprint).await
{
if peerstate.set_verified(
PeerstateKeyType::PublicKey,
@@ -1021,19 +1006,24 @@ fn encrypted_and_signed(
if !mimeparser.was_encrypted() {
warn!(context, "Message not encrypted.",);
false
} else if let Some(expected_fingerprint) = expected_fingerprint {
if !mimeparser.signatures.contains(expected_fingerprint) {
warn!(
context,
"Message does not match expected fingerprint {}.", expected_fingerprint,
);
false
} else {
true
}
} else {
} else if mimeparser.signatures.is_empty() {
warn!(context, "Message not signed.",);
false
} else if expected_fingerprint.is_none() {
warn!(context, "Fingerprint for comparison missing.");
false
} else if !mimeparser
.signatures
.contains(expected_fingerprint.unwrap())
{
warn!(
context,
"Message does not match expected fingerprint {}.",
expected_fingerprint.unwrap(),
);
false
} else {
true
}
}

View File

@@ -7,15 +7,14 @@
// but for non-delta-compatibility, that seems to be better.
// (to be only compatible with delta, only "[\r\n|\n]-- {0,2}[\r\n|\n]" needs to be replaced)
pub fn escape_message_footer_marks(text: &str) -> String {
if let Some(text) = text.strip_prefix("--") {
"-\u{200B}-".to_string() + &text.replace("\n--", "\n-\u{200B}-")
if text.starts_with("--") {
"-\u{200B}-".to_string() + &text[2..].replace("\n--", "\n-\u{200B}-")
} else {
text.replace("\n--", "\n-\u{200B}-")
}
}
/// Remove standard (RFC 3676, §4.3) footer if it is found.
#[allow(clippy::indexing_slicing)]
fn remove_message_footer<'a>(lines: &'a [&str]) -> &'a [&'a str] {
let mut nearly_standard_footer = None;
for (ix, &line) in lines.iter().enumerate() {
@@ -42,11 +41,12 @@ fn remove_message_footer<'a>(lines: &'a [&str]) -> &'a [&'a str] {
/// Remove nonstandard footer and a boolean indicating whether such
/// footer was removed.
#[allow(clippy::indexing_slicing)]
fn remove_nonstandard_footer<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
for (ix, &line) in lines.iter().enumerate() {
if line == "--"
|| line.starts_with("---")
|| line == "---"
|| line == "----"
|| line.starts_with("-----")
|| line.starts_with("_____")
|| line.starts_with("=====")
|| line.starts_with("*****")
@@ -107,7 +107,6 @@ fn skip_forward_header<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
}
}
#[allow(clippy::indexing_slicing)]
fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
let mut last_quoted_line = None;
for (l, line) in lines.iter().enumerate().rev() {
@@ -133,7 +132,6 @@ fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
}
}
#[allow(clippy::indexing_slicing)]
fn remove_top_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
let mut last_quoted_line = None;
let mut has_quoted_headline = false;
@@ -334,17 +332,9 @@ mod tests {
let (plain, _) = simplify(escaped, true);
assert_eq!(plain, "text\n\n--\ntreated as footer when unescaped");
// Nonstandard footer sent by https://siju.es/
let input = "Message text here\n---Desde mi teléfono con SIJÚ\n\nQuote here".to_string();
let (plain, _) = simplify(input.clone(), false);
assert_eq!(plain, "Message text here [...]");
let (plain, _) = simplify(input.clone(), true);
assert_eq!(plain, input);
let input = "--\ntreated as footer when unescaped".to_string();
let (plain, _) = simplify(input.clone(), true);
assert_eq!(plain, ""); // see remove_message_footer() for some explanations
let escaped = escape_message_footer_marks(&input);
let (plain, _) = simplify(escaped, true);
assert_eq!(plain, "--\ntreated as footer when unescaped");

View File

@@ -2,7 +2,7 @@
pub mod send;
use std::time::{Duration, SystemTime};
use std::time::{Duration, Instant};
use async_smtp::smtp::client::net::*;
use async_smtp::*;
@@ -10,9 +10,8 @@ use async_smtp::*;
use crate::constants::*;
use crate::context::Context;
use crate::events::Event;
use crate::login_param::{dc_build_tls, CertificateChecks, LoginParam};
use crate::login_param::{dc_build_tls, LoginParam};
use crate::oauth2::*;
use crate::provider::get_provider_info;
use crate::stock::StockMessage;
/// SMTP write and read timeout in seconds.
@@ -45,8 +44,9 @@ pub enum Error {
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Default)]
pub(crate) struct Smtp {
#[derive(Default, DebugStub)]
pub struct Smtp {
#[debug_stub(some = "SmtpTransport")]
transport: Option<smtp::SmtpTransport>,
/// Email address we are sending from.
@@ -55,7 +55,7 @@ pub(crate) struct Smtp {
/// Timestamp of last successful send/receive network interaction
/// (eg connect or send succeeded). On initialization and disconnect
/// it is set to None.
last_success: Option<SystemTime>,
last_success: Option<Instant>,
}
impl Smtp {
@@ -76,11 +76,7 @@ impl Smtp {
/// have been successfully used the last 60 seconds
pub async fn has_maybe_stale_connection(&self) -> bool {
if let Some(last_success) = self.last_success {
SystemTime::now()
.duration_since(last_success)
.unwrap_or_default()
.as_secs()
> 60
Instant::now().duration_since(last_success).as_secs() > 60
} else {
false
}
@@ -117,14 +113,7 @@ impl Smtp {
let domain = &lp.send_server;
let port = lp.send_port as u16;
let provider = get_provider_info(&lp.addr);
let strict_tls = match lp.smtp_certificate_checks {
CertificateChecks::Automatic => provider.map_or(false, |provider| provider.strict_tls),
CertificateChecks::Strict => true,
CertificateChecks::AcceptInvalidCertificates
| CertificateChecks::AcceptInvalidCertificates2 => false,
};
let tls_config = dc_build_tls(strict_tls);
let tls_config = dc_build_tls(lp.smtp_certificate_checks);
let tls_parameters = ClientTlsParameters::new(domain.to_string(), tls_config);
let (creds, mechanism) = if 0 != lp.server_flags & (DC_LP_AUTH_OAUTH2 as i32) {
@@ -192,7 +181,7 @@ impl Smtp {
}
self.transport = Some(trans);
self.last_success = Some(SystemTime::now());
self.last_success = Some(Instant::now());
context.emit_event(Event::SmtpConnected(format!(
"SMTP-LOGIN as {} ok",

View File

@@ -5,7 +5,6 @@ use async_smtp::*;
use crate::context::Context;
use crate::events::Event;
use std::time::Duration;
pub type Result<T> = std::result::Result<T, Error>;
@@ -31,7 +30,7 @@ impl Smtp {
message: Vec<u8>,
job_id: u32,
) -> Result<()> {
let message_len_bytes = message.len();
let message_len = message.len();
let recipients_display = recipients
.iter()
@@ -48,18 +47,13 @@ impl Smtp {
);
if let Some(ref mut transport) = self.transport {
// The timeout is 1min + 3min per MB.
let timeout = 60 + (180 * message_len_bytes / 1_000_000) as u64;
transport
.send_with_timeout(mail, Some(&Duration::from_secs(timeout)))
.await
.map_err(Error::SendError)?;
transport.send(mail).await.map_err(Error::SendError)?;
context.emit_event(Event::SmtpMessageSent(format!(
"Message len={} was smtp-sent to {}",
message_len_bytes, recipients_display
message_len, recipients_display
)));
self.last_success = Some(std::time::SystemTime::now());
self.last_success = Some(std::time::Instant::now());
Ok(())
} else {

View File

@@ -13,7 +13,6 @@ use crate::chat::{update_device_icon, update_saved_messages_icon};
use crate::constants::{ShowEmails, DC_CHAT_ID_TRASH};
use crate::context::Context;
use crate::dc_tools::*;
use crate::ephemeral::start_ephemeral_timers;
use crate::param::*;
use crate::peerstate::*;
@@ -50,7 +49,7 @@ pub enum Error {
pub type Result<T> = std::result::Result<T, Error>;
/// A wrapper around the underlying Sqlite3 object.
#[derive(Debug)]
#[derive(DebugStub)]
pub struct Sql {
pool: RwLock<Option<r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>>>,
}
@@ -569,24 +568,16 @@ pub async fn housekeeping(context: &Context) {
}
}
if let Err(err) = start_ephemeral_timers(context).await {
warn!(
context,
"Housekeeping: cannot start ephemeral timers: {}", err
);
}
if let Err(err) = prune_tombstones(context).await {
warn!(
context,
"Housekeeping: Cannot prune message tombstones: {}", err
"Houskeeping: Cannot prune message tombstones: {}", err
);
}
info!(context, "Housekeeping done.",);
}
#[allow(clippy::indexing_slicing)]
fn is_file_in_use(files_in_use: &HashSet<String>, namespc_opt: Option<&str>, name: &str) -> bool {
let name_to_check = if let Some(namespc) = namespc_opt {
let name_len = name.len();
@@ -602,9 +593,11 @@ fn is_file_in_use(files_in_use: &HashSet<String>, namespc_opt: Option<&str>, nam
}
fn maybe_add_file(files_in_use: &mut HashSet<String>, file: impl AsRef<str>) {
if let Some(file) = file.as_ref().strip_prefix("$BLOBDIR/") {
files_in_use.insert(file.to_string());
if !file.as_ref().starts_with("$BLOBDIR") {
return;
}
files_in_use.insert(file.as_ref()[9..].into());
}
async fn maybe_add_from_param(
@@ -1248,46 +1241,6 @@ async fn open(
.await?;
sql.set_raw_config_int(context, "dbversion", 63).await?;
}
if dbversion < 64 {
info!(context, "[migration] v64");
sql.execute(
"ALTER TABLE msgs ADD COLUMN error TEXT DEFAULT '';",
paramsv![],
)
.await?;
sql.set_raw_config_int(context, "dbversion", 64).await?;
}
if dbversion < 65 {
info!(context, "[migration] v65");
sql.execute(
"ALTER TABLE chats ADD COLUMN ephemeral_timer INTEGER",
paramsv![],
)
.await?;
// Timer value in seconds. For incoming messages this
// timer starts when message is read, so we want to have
// the value stored here until the timer starts.
sql.execute(
"ALTER TABLE msgs ADD COLUMN ephemeral_timer INTEGER DEFAULT 0",
paramsv![],
)
.await?;
// Timestamp indicating when the message should be
// deleted. It is convenient to store it here because UI
// needs this value to display how much time is left until
// the message is deleted.
sql.execute(
"ALTER TABLE msgs ADD COLUMN ephemeral_timestamp INTEGER DEFAULT 0",
paramsv![],
)
.await?;
sql.set_raw_config_int(context, "dbversion", 65).await?;
}
if dbversion < 66 {
info!(context, "[migration] v66");
update_icons = true;
sql.set_raw_config_int(context, "dbversion", 66).await?;
}
// (2) updates that require high-level objects
// (the structure is complete now and all objects are usable)
@@ -1308,7 +1261,7 @@ async fn open(
)
.await?;
for addr in &addrs {
if let Some(ref mut peerstate) = Peerstate::from_addr(context, addr).await? {
if let Some(ref mut peerstate) = Peerstate::from_addr(context, addr).await {
peerstate.recalc_fingerprint();
peerstate.save_to_db(sql, false).await?;
}
@@ -1350,12 +1303,10 @@ mod test {
maybe_add_file(&mut files, "$BLOBDIR/hello");
maybe_add_file(&mut files, "$BLOBDIR/world.txt");
maybe_add_file(&mut files, "world2.txt");
maybe_add_file(&mut files, "$BLOBDIR");
assert!(files.contains("hello"));
assert!(files.contains("world.txt"));
assert!(!files.contains("world2.txt"));
assert!(!files.contains("$BLOBDIR"));
}
#[test]

View File

@@ -130,9 +130,7 @@ pub enum StockMessage {
))]
AcSetupMsgBody = 43,
#[strum(props(
fallback = "Cannot login as \"%1$s\". Please check if the email address and the password are correct."
))]
#[strum(props(fallback = "Cannot login as %1$s."))]
CannotLogin = 60,
#[strum(props(fallback = "Could not connect to %1$s: %2$s"))]
@@ -179,43 +177,11 @@ pub enum StockMessage {
however, of course, if they like, you may point them to 👉 https://get.delta.chat"))]
WelcomeMessage = 71,
#[strum(props(fallback = "Unknown sender for this chat. See 'info' for more details."))]
#[strum(props(fallback = "Unknown Sender for this chat. See 'info' for more details."))]
UnknownSenderForChat = 72,
#[strum(props(fallback = "Message from %1$s"))]
SubjectForNewContact = 73,
#[strum(props(fallback = "Failed to send message to %1$s."))]
FailedSendingTo = 74,
#[strum(props(fallback = "Message deletion timer is disabled."))]
MsgEphemeralTimerDisabled = 75,
// A fallback message for unknown timer values.
// "s" stands for "second" SI unit here.
#[strum(props(fallback = "Message deletion timer is set to %1$s s."))]
MsgEphemeralTimerEnabled = 76,
#[strum(props(fallback = "Message deletion timer is set to 1 minute."))]
MsgEphemeralTimerMinute = 77,
#[strum(props(fallback = "Message deletion timer is set to 1 hour."))]
MsgEphemeralTimerHour = 78,
#[strum(props(fallback = "Message deletion timer is set to 1 day."))]
MsgEphemeralTimerDay = 79,
#[strum(props(fallback = "Message deletion timer is set to 1 week."))]
MsgEphemeralTimerWeek = 80,
#[strum(props(fallback = "Message deletion timer is set to 4 weeks."))]
MsgEphemeralTimerFourWeeks = 81,
#[strum(props(fallback = "Video chat invitation"))]
VideochatInvitation = 82,
#[strum(props(fallback = "You are invited to a video chat, click %1$s to join."))]
VideochatInviteMsgBody = 83,
}
/*
@@ -365,10 +331,10 @@ impl Context {
let action1 = action.trim_end_matches('.');
match from_id {
0 => action,
DC_CONTACT_ID_SELF => {
1 => {
self.stock_string_repl_str(StockMessage::MsgActionByMe, action1)
.await
}
} // DC_CONTACT_ID_SELF
_ => {
let displayname = Contact::get_by_id(self, from_id)
.await
@@ -440,7 +406,7 @@ mod tests {
#[async_std::test]
async fn test_set_stock_translation() {
let t = TestContext::new().await;
let t = dummy_context().await;
t.ctx
.set_stock_translation(StockMessage::NoMessages, "xyz".to_string())
.await
@@ -450,7 +416,7 @@ mod tests {
#[async_std::test]
async fn test_set_stock_translation_wrong_replacements() {
let t = TestContext::new().await;
let t = dummy_context().await;
assert!(t
.ctx
.set_stock_translation(StockMessage::NoMessages, "xyz %1$s ".to_string())
@@ -465,7 +431,7 @@ mod tests {
#[async_std::test]
async fn test_stock_str() {
let t = TestContext::new().await;
let t = dummy_context().await;
assert_eq!(
t.ctx.stock_str(StockMessage::NoMessages).await,
"No messages."
@@ -474,7 +440,7 @@ mod tests {
#[async_std::test]
async fn test_stock_string_repl_str() {
let t = TestContext::new().await;
let t = dummy_context().await;
// uses %1$s substitution
assert_eq!(
t.ctx
@@ -487,7 +453,7 @@ mod tests {
#[async_std::test]
async fn test_stock_string_repl_int() {
let t = TestContext::new().await;
let t = dummy_context().await;
assert_eq!(
t.ctx
.stock_string_repl_int(StockMessage::MsgAddMember, 42)
@@ -498,7 +464,7 @@ mod tests {
#[async_std::test]
async fn test_stock_string_repl_str2() {
let t = TestContext::new().await;
let t = dummy_context().await;
assert_eq!(
t.ctx
.stock_string_repl_str2(StockMessage::ServerResponse, "foo", "bar")
@@ -509,7 +475,7 @@ mod tests {
#[async_std::test]
async fn test_stock_system_msg_simple() {
let t = TestContext::new().await;
let t = dummy_context().await;
assert_eq!(
t.ctx
.stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0)
@@ -520,7 +486,7 @@ mod tests {
#[async_std::test]
async fn test_stock_system_msg_add_member_by_me() {
let t = TestContext::new().await;
let t = dummy_context().await;
assert_eq!(
t.ctx
.stock_system_msg(
@@ -536,7 +502,7 @@ mod tests {
#[async_std::test]
async fn test_stock_system_msg_add_member_by_me_with_displayname() {
let t = TestContext::new().await;
let t = dummy_context().await;
Contact::create(&t.ctx, "Alice", "alice@example.com")
.await
.expect("failed to create contact");
@@ -555,7 +521,7 @@ mod tests {
#[async_std::test]
async fn test_stock_system_msg_add_member_by_other_with_displayname() {
let t = TestContext::new().await;
let t = dummy_context().await;
let contact_id = {
Contact::create(&t.ctx, "Alice", "alice@example.com")
.await
@@ -579,7 +545,7 @@ mod tests {
#[async_std::test]
async fn test_stock_system_msg_grp_name() {
let t = TestContext::new().await;
let t = dummy_context().await;
assert_eq!(
t.ctx
.stock_system_msg(
@@ -595,7 +561,7 @@ mod tests {
#[async_std::test]
async fn test_stock_system_msg_grp_name_other() {
let t = TestContext::new().await;
let t = dummy_context().await;
let id = Contact::create(&t.ctx, "Alice", "alice@example.com")
.await
.expect("failed to create contact");
@@ -610,7 +576,7 @@ mod tests {
#[async_std::test]
async fn test_update_device_chats() {
let t = TestContext::new().await;
let t = dummy_context().await;
t.ctx.update_device_chats().await.ok();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 2);

View File

@@ -18,59 +18,26 @@ pub(crate) struct TestContext {
pub dir: TempDir,
}
impl TestContext {
/// Create a new [TestContext].
///
/// The [Context] will be created and have an SQLite database named "db.sqlite" in the
/// [TestContext.dir] directory. This directory is cleaned up when the [TestContext] is
/// dropped.
///
/// [Context]: crate::context::Context
pub async fn new() -> Self {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let ctx = Context::new("FakeOS".into(), dbfile.into()).await.unwrap();
Self { ctx, dir }
}
/// Create a new, opened [TestContext] using given callback.
///
/// The [Context] will be opened with the SQLite database named
/// "db.sqlite" in the [TestContext.dir] directory.
///
/// [Context]: crate::context::Context
pub(crate) async fn test_context() -> TestContext {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let ctx = Context::new("FakeOs".into(), dbfile.into()).await.unwrap();
TestContext { ctx, dir }
}
/// Create a new configured [TestContext].
///
/// This is a shortcut which automatically calls [TestContext::configure_alice] after
/// creating the context.
pub async fn new_alice() -> Self {
let t = Self::new().await;
t.configure_alice().await;
t
}
/// Configure with alice@example.com.
///
/// The context will be fake-configured as the alice user, with a pre-generated secret
/// key. The email address of the user is returned as a string.
pub async fn configure_alice(&self) -> String {
let keypair = alice_keypair();
self.configure_addr(&keypair.addr.to_string()).await;
key::store_self_keypair(&self.ctx, &keypair, key::KeyPairUse::Default)
.await
.expect("Failed to save Alice's key");
keypair.addr.to_string()
}
/// Configure as a given email address.
///
/// The context will be configured but the key will not be pre-generated so if a key is
/// used the fingerprint will be different every time.
pub async fn configure_addr(&self, addr: &str) {
self.ctx.set_config(Config::Addr, Some(addr)).await.unwrap();
self.ctx
.set_config(Config::ConfiguredAddr, Some(addr))
.await
.unwrap();
self.ctx
.set_config(Config::Configured, Some("1"))
.await
.unwrap();
}
/// Return a dummy [TestContext].
///
/// The context will be opened and use the SQLite database as
/// specified in [test_context] but there is no callback hooked up,
/// i.e. [Context::call_cb] will always return `0`.
pub(crate) async fn dummy_context() -> TestContext {
test_context().await
}
/// Load a pre-generated keypair for alice@example.com from disk.
@@ -93,6 +60,20 @@ pub(crate) fn alice_keypair() -> key::KeyPair {
}
}
/// Creates Alice with a pre-generated keypair.
///
/// Returns the address of the keypair created (alice@example.com).
pub(crate) async fn configure_alice_keypair(ctx: &Context) -> String {
let keypair = alice_keypair();
ctx.set_config(Config::ConfiguredAddr, Some(&keypair.addr.to_string()))
.await
.unwrap();
key::store_self_keypair(&ctx, &keypair, key::KeyPairUse::Default)
.await
.expect("Failed to save Alice's key");
keypair.addr.to_string()
}
/// Load a pre-generated keypair for bob@example.net from disk.
///
/// Like [alice_keypair] but a different key and identity.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 B

View File

@@ -1,242 +0,0 @@
Delivered-To: alice@gmail.com
Received: by 2002:a1c:b4d7:0:0:0:0:0 with SMTP id d206csp3026053wmf;
Mon, 18 May 2020 09:23:25 -0700 (PDT)
X-Received: by 2002:a5d:4651:: with SMTP id j17mr19532177wrs.50.1589819005555;
Mon, 18 May 2020 09:23:25 -0700 (PDT)
ARC-Seal: i=1; a=rsa-sha256; t=1589819005; cv=none;
d=google.com; s=arc-20160816;
b=IZbNnzzuYzTFuqfuZwpd3ehqpYYGpn31c8DsfGbQ8rpbS0OTTROkVYvihQl8Ne/8X/
brEWsrcmaCh9WpFMzpI+cp/TY39uusnI6qdp5rcgrFmFgoANtwf3TBBj1+f7wBPn46BP
dQOUsg/J8KVfvzVgvL1x4uyJ0m9QirDgJeJ/BvrswbTleRQK7oY3fIireUCDxj6r2lCB
1Z0TKw1mgIb1LiFMZz8kvCNn3R4KSFnwS8rIju0hYwnsioNiExVQgumXL+RVkEZ9BMzf
UdoWIAw3VW+MOZFTpfLCEfgIPtLg/gtE0Q1P+a3KKpi8dkPiV2n6DGMecy9lTLtdhCXt
pnaA==
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816;
h=in-reply-to:references:subject:from:date:message-id:auto-submitted
:to:dkim-signature;
bh=5xjZvcHbEGbMY0K2QB+3U6tpm1L1LAVv5h1pd4YXDEE=;
b=nNP0DktrSjdBaFfhhoDi2O9KVKM0iXE5ZgubQ0q0ff68Z6Ke7c8dDBXEsZoToI0s4Y
w90KyJFpgMJLFmP3iVDRqCfohi2y1HGdWg5VXQPTvzM7+YozZRlbNNV9UsuyRY91CXrJ
a2XREBgB+LPMGQivwcHtUMZfyNv/4uiwWivk+92ySNDhxqOiDt4R5Jak/7RkZMFwQpsE
JGwk6asM6VqZlihkF24lKv3pPaob6feyX3wD5N0+Mqiy1kQTj2JkpQk6nkTmdf0gapZe
fOhU1NkbNfbuS3U7m2gEUiyktE+MhV/MgAzgBhm9bgNt2gQLVWju8rHkPndfv1PDmEkC
FsYQ==
ARC-Authentication-Results: i=1; mx.google.com;
dkim=pass header.i=@googlemail.com header.s=20161025 header.b=dPisws+O;
spf=pass (google.com: best guess record for domain of postmaster@mail-sor-f69.google.com designates 209.85.220.69 as permitted sender) smtp.helo=mail-sor-f69.google.com;
dmarc=pass (p=QUARANTINE sp=QUARANTINE dis=NONE) header.from=googlemail.com
Return-Path: <>
Received: from mail-sor-f69.google.com (mail-sor-f69.google.com. [209.85.220.69])
by mx.google.com with SMTPS id s18sor5584435wrb.25.2020.05.18.09.23.25
for <alice@gmail.com>
(Google Transport Security);
Mon, 18 May 2020 09:23:25 -0700 (PDT)
Received-SPF: pass (google.com: best guess record for domain of postmaster@mail-sor-f69.google.com designates 209.85.220.69 as permitted sender) client-ip=209.85.220.69;
Authentication-Results: mx.google.com;
dkim=pass header.i=@googlemail.com header.s=20161025 header.b=dPisws+O;
spf=pass (google.com: best guess record for domain of postmaster@mail-sor-f69.google.com designates 209.85.220.69 as permitted sender) smtp.helo=mail-sor-f69.google.com;
dmarc=pass (p=QUARANTINE sp=QUARANTINE dis=NONE) header.from=googlemail.com
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=googlemail.com; s=20161025;
h=to:auto-submitted:message-id:date:from:subject:references
:in-reply-to;
bh=5xjZvcHbEGbMY0K2QB+3U6tpm1L1LAVv5h1pd4YXDEE=;
b=dPisws+OwGFyOy0a612XYZgvz5T71GcJRJtU068/Tce8vN/+ggIQtUsZnZtsphe71v
2NvfP9ULxR4cXvomTvhrYAk19KdxN/S7SeyBbmXv3x/tg+DBVCmmPS/6RXrcl6Ms3Hkw
uPFQ9S3KcvHe/2bcb5LSTA/stIP4tuxxAXvsX2j+MjPYPWKAl50jkSbWK98U0Q0U+MTl
pKaaC9s9iEBafac8BFZCy4DfpumKlemNEyRa3cSV2hw+DYHKA5peModrK1A2tcsfstFF
rZi8yF/D90RIFbE04DI2QCxB3trsChNF1aYF06aSzI//wsfM1+lb+uGPi0YVkw3n4HrX
Xw4w==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=1e100.net; s=20161025;
h=x-gm-message-state:to:auto-submitted:message-id:date:from:subject
:references:in-reply-to;
bh=5xjZvcHbEGbMY0K2QB+3U6tpm1L1LAVv5h1pd4YXDEE=;
b=A/NCOtgbpA7VzB1G7ZFo8TA2FfrjuqjGdwMrJr3yXe21FrBFwzssprJwOkynqoVLkK
iJU7uMF/KTcQPDEmOLFThzFfe5GCx7eJtZPhwY+FbBlC5sq4I55/xaQLd0gOZ1BYXwMn
2bk169d2aoukbaLbGSQZF3d9atd+/e48YzkRxpmUoLcrWk2LcHAeQIG7SgT9pfX5DKPr
VpxM5/GMVEBbTRhBIWCeVSfpYCs80l0xEeTC3/B5lzpzMVDE8QCW6Dwh75b4Tb2K6yru
Zsy5ZpRmwv0wrkrb2vM+pl4IMkaF7s8XosIvlIT++fQV5xDFItT4atpykZvSDB92RKV0
8lEA==
X-Gm-Message-State: AOAM532RG/PT3ChZHBCDORGLtAjKvX8TGBuOy+AxrnEaJT6v1ieb+VV1
+ejly+/6UthxHYlkOJYAszCSgL4dKVFotoVaN7LhEA==
X-Google-Smtp-Source: ABdhPJz6veVKWhomCL4gK+whrybuMzHCDCq8AowgQvi7sobpMoM/k9CDw79jo1j3OUcTz6MEeUYLxEXuNIuu4zyoS7kVtsUYryGFHAI=
X-Received: by 2002:a5d:5183:: with SMTP id k3mr20545185wrv.159.1589819005394;
Mon, 18 May 2020 09:23:25 -0700 (PDT)
Content-Type: multipart/report; boundary="00000000000012d63005a5ee9520"; report-type=delivery-status
To: alice@gmail.com
Received: by 2002:a5d:5183:: with SMTP id k3mr13704211wrv.159; Mon, 18 May
2020 09:23:25 -0700 (PDT)
Return-Path: <>
Auto-Submitted: auto-replied
Message-ID: <5ec2b67d.1c69fb81.213af.67a5.GMR@mx.google.com>
Date: Mon, 18 May 2020 09:23:25 -0700 (PDT)
From: Mail Delivery Subsystem <mailer-daemon@googlemail.com>
Subject: Delivery Status Notification (Failure)
References: <CABXKi8zruXJc_6e4Dr087H5wE7sLp+u250o0N2q5DdjF_r-8wg@mail.gmail.com>
In-Reply-To: <CABXKi8zruXJc_6e4Dr087H5wE7sLp+u250o0N2q5DdjF_r-8wg@mail.gmail.com>
X-Failed-Recipients: assidhfaaspocwaeofi@gmail.com
--00000000000012d63005a5ee9520
Content-Type: multipart/related; boundary="00000000000012dc0005a5ee952f"
--00000000000012dc0005a5ee952f
Content-Type: multipart/alternative; boundary="00000000000012dc0705a5ee9530"
--00000000000012dc0705a5ee9530
Content-Type: text/plain; charset="UTF-8"
** Die Adresse wurde nicht gefunden **
Ihre Nachricht wurde nicht an assidhfaaspocwaeofi@gmail.com zugestellt, weil die Adresse nicht gefunden wurde oder keine E-Mails empfangen kann.
Hier erfahren Sie mehr: https://support.google.com/mail/?p=NoSuchUser
Antwort:
550 5.1.1 The email account that you tried to reach does not exist. Please try double-checking the recipient's email address for typos or unnecessary spaces. Learn more at https://support.google.com/mail/?p=NoSuchUser i18sor6261697wrs.38 - gsmtp
--00000000000012dc0705a5ee9530
Content-Type: text/html; charset="UTF-8"
<html>
<head>
<style>
* {
font-family:Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif;
}
</style>
</head>
<body>
<table cellpadding="0" cellspacing="0" class="email-wrapper" style="padding-top:32px;background-color:#ffffff;"><tbody>
<tr><td>
<table cellpadding=0 cellspacing=0><tbody>
<tr><td style="max-width:560px;padding:24px 24px 32px;background-color:#fafafa;border:1px solid #e0e0e0;border-radius:2px">
<img style="padding:0 24px 16px 0;float:left" width=72 height=72 alt="Fehlersymbol" src="cid:icon.png">
<table style="min-width:272px;padding-top:8px"><tbody>
<tr><td><h2 style="font-size:20px;color:#212121;font-weight:bold;margin:0">
Die Adresse wurde nicht gefunden
</h2></td></tr>
<tr><td style="padding-top:20px;color:#757575;font-size:16px;font-weight:normal;text-align:left">
Ihre Nachricht wurde nicht an <a style='color:#212121;text-decoration:none'><b>assidhfaaspocwaeofi@gmail.com</b></a> zugestellt, weil die Adresse nicht gefunden wurde oder keine E-Mails empfangen kann.
</td></tr>
<tr><td style="padding-top:24px;color:#4285F4;font-size:14px;font-weight:bold;text-align:left">
<a style="text-decoration:none" href="https://support.google.com/mail/?p=NoSuchUser">WEITERE INFORMATIONEN</a>
</td></tr>
</tbody></table>
</td></tr>
</tbody></table>
</td></tr>
<tr style="border:none;background-color:#fff;font-size:12.8px;width:90%">
<td align="left" style="padding:48px 10px">
Antwort:<br/>
<p style="font-family:monospace">
550 5.1.1 The email account that you tried to reach does not exist. Please try double-checking the recipient&#39;s email address for typos or unnecessary spaces. Learn more at https://support.google.com/mail/?p=NoSuchUser i18sor6261697wrs.38 - gsmtp
</p>
</td>
</tr>
</tbody></table>
</body>
</html>
--00000000000012dc0705a5ee9530--
--00000000000012dc0005a5ee952f
Content-Type: image/png; name="icon.png"
Content-Disposition: attachment; filename="icon.png"
Content-Transfer-Encoding: base64
Content-ID: <icon.png>
iVBORw0KGgoAAAANSUhEUgAAAJAAAACQCAYAAADnRuK4AAAACXBIWXMAABYlAAAWJQFJUiTwAAAA
GXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABTdJREFUeNrsnD9sFEcUh5+PRMqZ
yA0SPhAUQAQFUkyTgiBASARo6QApqVIkfdxGFJFSgGhJAUIiBaQB0ZIOKVCkwUgURjIg2fxL4kS+
YDvkbC/388bi8N16Z4/d7J/5PsniuD3fyePP772ZeTsDQRAYQL/UGAJAIEAgQCBAIAAEAgQCBAIE
AkAgyJT3Mv+Eq7vYK8mTE+MDRCAghQECAeRQA5V2ZOpmg5vDx3NPzRbmGRMEcmTrEbNNB8zWfRD+
f/Efs2e3zCZvMjaksBg27TfbcuSNPEKP9ZyuAQKtHX2O9ncNgWC57umMPKvRNb0GEKgnLoUyxTQC
rcns0/6uIRAs8/hGf9cQCJZpTpjdO2f25/03z+mxntM1eLtsZAgiUtX4JcaBCAQIBAgECARQ8CJa
G5jab4J4pm4WZmO3OALVh802fIwcLkyPkcKAGggAgQCBAIEAgQCBABAIEAjKA/1AnahhbO5FdOOY
VsrrDbPBYcYKgf5D2wLaV3p+22xh1u17tO3S+DTcvxvagUDeivPgx/a/95J/73w7Sj26Hn4pKo2M
ehuV/KyBJM6d0f7k6RKx/R63vvL2tmf/ItDdM2ZTP6f7nkp9Y2fDx1v9akmpIU+KSCLVUghUQfSL
zVKeTklbLxGoctw/nzC5rw8L5KRNbkpnKq6pgSqEClzNnFzY+XnYWrt6VpVk1vbwWvg+RKCKMOUw
Q1LEOXA+/MX3mpJvGDHb265xtnzmFoUK1HaKQGlMtePYM+q2KKjXuaS1NJYIEKgI8jhEgqHt4cqy
Ky53j3hyHz2bqSLp2o2LbJ7MxKovkGqXteoWpaOk96O9/yF/dF7NwlS36AuIQIBA5celQK4PIxBE
4LLzrtoLgaALdSy6CJRkWQCBPGLsTHznomZ9nszUECgJ2ml3WWHe+QVFNPSQx6UdZNtxr9pbEShN
eTTz8mQXHoHSlke7+Z+c9m6VGoHSkEfs/trLW3wQKApN1V3lGfnGu2Z6BFoLtYCs3GWBPAiUCLVh
/HoaeRCoT9R873KLM/IgUBfapnCpe5AHgXry4pf412ihEHkQqCdxd5VqrcezhUIESsJMTJ+Pdthp
Z0WgyNlXXPHc2Mc4IVAELl2Gnh8mhUDvCkfbIVAkcbf/aOoO3fMKhqAD3frTa4quwpn0hUDOkQhI
YYBAgECAQAAU0QlYObl+5Ug8NcprZkZxjUCxRPVA6zmtEXHCBykskrhjgHXN09PoEcgFl4M4H11j
nBAoApcj6ZoPGScEAgTKApcDoTw5sgWB+sGlz1n90IBAPdE6j1o21PfcC11jLagL1oFWRyGlKU3p
OxcSJQ7NZAjkhHp/uG2HFAYIBAgECASAQIBAgECAQAAIBOkxEARBtp9wdVfAMOfIifEBIhCQwgCB
ABAI0oV2jhxZ+nfBatuPZfgBCy0Eqqo8c01b+uu51XZvzOgDWoHNTGR+pCwpLEd5svuAZXlO2uEr
PyEQ8hRWHgRCHmqg0sjTnLalv6crJQ8C/U8stqNO0I4+VZOHFIY8COS1PGL2ybd5yUMKK7s8zYmL
dujyd3n+nESgcsvzZd4/KwIhDwIhT35QA6UyE1qyxZnfvJMHgdKS549JC1qvvJOHFIY8CFR5eV5O
XimqPAhUdHnmfx+zgxdOFXkoqIGKKs/cswnb/8Oeog8HEai48nxUhiFBIORBIOShBioskkbySCLk
IQIhDwIhj28p7FApR6b1qlEbHGpkO/rr6215vi/zH1r2x7tApSGFAQIBAgECAQIBIBAgECAQIBBA
LK8FGADCTxYrr+EVJgAAAABJRU5ErkJggg==
--00000000000012dc0005a5ee952f--
--00000000000012d63005a5ee9520
Content-Type: message/delivery-status
Reporting-MTA: dns; googlemail.com
Arrival-Date: Mon, 18 May 2020 09:23:25 -0700 (PDT)
X-Original-Message-ID: <CABXKi8zruXJc_6e4Dr087H5wE7sLp+u250o0N2q5DdjF_r-8wg@mail.gmail.com>
Final-Recipient: rfc822; assidhfaaspocwaeofi@gmail.com
Action: failed
Status: 5.1.1
Diagnostic-Code: smtp; 550-5.1.1 The email account that you tried to reach does not exist. Please try
550-5.1.1 double-checking the recipient's email address for typos or
550-5.1.1 unnecessary spaces. Learn more at
550 5.1.1 https://support.google.com/mail/?p=NoSuchUser i18sor6261697wrs.38 - gsmtp
Last-Attempt-Date: Mon, 18 May 2020 09:23:25 -0700 (PDT)
--00000000000012d63005a5ee9520
Content-Type: message/rfc822
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=gmail.com; s=20161025;
h=mime-version:from:date:message-id:subject:to;
bh=gtlm3j0shCgZYOVxUt74zkQ69Zq+GTQeHeXLfMlrhlk=;
b=a185ogBcMzF9whNVWvuyUoUunNZk3Vc1kEIFmPkX0IxLpAFcI+fOQajOSromGl7Oyi
yecLwQevpww2Xd0XjZ3UkZvrI9m9koRmh0QeoHvgTRORiVwj08+PVc3N4F9bCO4w9i0J
ir7SSsJqBCDovoIFSFDyNa64vs6Nxno0cH/DaPG7pVTdD+3jfB7nLXIsMQYeX+1eP6rB
UhKxH82r7Mh9CI2PWDQpVtGj63AMUEyHgE9Ou08PWbbKjrQOasoG3Tw8tB1GoN1XYssM
rxOTgWEoTiduZ35AUH6h+eChOn9OHuI3SPECcVob70Qndayia3dMKfHMO6sEx9J0Wpic
29vg==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=1e100.net; s=20161025;
h=x-gm-message-state:mime-version:from:date:message-id:subject:to;
bh=gtlm3j0shCgZYOVxUt74zkQ69Zq+GTQeHeXLfMlrhlk=;
b=miGIfL5BgnkD3wQvS34RtGwRRoh+8gJT5sFFfdX/hVyG/dvjXfdwP4yyNWr8ox8iY2
BLlahS4y4VGcbG1e2aYjurnWNytGu6utQcZax/uUngJ0bTOwXW1VaIiEZtqd6gTV+8d/
rrfQ459+4vXqIoQf0+Oi/U6dWwgJvPPjjRiToWdF3vIJE8R1iTRdZbW4lkgxSADbmskg
noT/gWGWblHtR6uuGuKGJ3bkhJKCBnjavKh0LlbWEeFBZfmVNPRvzEFWHjBDdu5wvSL5
0QJ+Qn0Orfn5CJuN3xPfzT1S2rI2iYZx37KX9zyMnZEx0ilkTYqCtBPWkrXRYDSXcxYS
Y1ag==
X-Gm-Message-State: AOAM531vhwpXiK8M12286dOJx0Q5fBl9ZaH6BJKts93GoxvPv0xdryP0
jg9wYmoP5MUHudsxAMCYDFsCUMVx2PEywyIsaQqklw==
X-Google-Smtp-Source: ABdhPJxlVJtTODM3pZZSTbbpAAAQRU8XbmuosDF9fgQZmVwxGZSzRWl22o+moppVRU/r8xMAyf0r3+qXwEBe1vZfjZo=
X-Received: by 2002:a5d:5183:: with SMTP id k3mr20545162wrv.159.1589819005034;
Mon, 18 May 2020 09:23:25 -0700 (PDT)
MIME-Version: 1.0
From: <alice@gmail.com>
Date: Mon, 18 May 2020 18:23:39 +0200
Message-ID: <CABXKi8zruXJc_6e4Dr087H5wE7sLp+u250o0N2q5DdjF_r-8wg@mail.gmail.com>
Subject: Kommt sowieso nicht an
To: assidhfaaspocwaeofi@gmail.com
Content-Type: multipart/alternative; boundary="0000000000000d652a05a5ee95df"
--0000000000000d652a05a5ee95df
Content-Type: text/plain; charset="UTF-8"
Wollte nur was testen
--0000000000000d652a05a5ee95df
Content-Type: text/html; charset="UTF-8"
<div dir="ltr">Wollte nur was testen<br></div>
--0000000000000d652a05a5ee95df--
--00000000000012d63005a5ee9520--

View File

@@ -1,242 +0,0 @@
Delivered-To: alice@gmail.com
Received: by 2002:a02:6629:0:0:0:0:0 with SMTP id k41csp368502jac;
Wed, 10 Jun 2020 05:17:57 -0700 (PDT)
X-Received: by 2002:a6b:1448:: with SMTP id 69mr2898530iou.83.1591791475733;
Wed, 10 Jun 2020 05:17:55 -0700 (PDT)
ARC-Seal: i=1; a=rsa-sha256; t=1591791475; cv=none;
d=google.com; s=arc-20160816;
b=a0vSKJPbMtGYFnuk1ye/gnnV00Zvva4OOJTMOyfm13xMJD0YAhzGVfa7Z+wn5sQ8dw
VAxpmDHCkjp4jol0C1iutiq2Nl0qma819oFPuuoMLLatKQXHpo8Jt+sL3MnwNR7J5bZC
1c6Fjk75EIsRWhJd1HCkm44A6UYHxqqsTnzQCaNiHbjsRsvbggxwlMGSrZ4silxqSDvo
Pzd/YDLCvsnZNSNIjIckKAwtGmY6sXctZ+JnOTykXAyL32Milfwy1vRL9xm10Q14biTR
+qaIQp4E6WE63g1WHvfAjs0Dru7DalUh4GGl/NAwqVhY1gVyRD5E9/nODyHAfxjvaxDD
4sMw==
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816;
h=in-reply-to:references:subject:from:date:message-id:auto-submitted
:to:dkim-signature;
bh=XaR1H4XeD+InO7mULPJn53omDGmxN+KG6DbSxyyErPM=;
b=OJbgbrktMKyczw25z/ib7lSdRX80PEK3Myh9fj4q6mDlXmPPv//Gv069znRQ4QbadM
HUXZH0WLMZcGyqI6SvGL/noxQ1O8yP0FYJJKTkoX0Gk2hHzfaE3x1scOP/o2FMMQXIFm
S4CgGBD6HHzBJYj/rSL3gzqLzx1Id/z5kTeDvH2cn8JJAcCE2q/nhjTyWUb87geoNlDJ
A1HRrLHK/0JOyRjHfg2zZCqIvSi1xmpiHStMyL9mfVyrQs98tsPxaOkJHjLplFARoPlr
mmmDvsFg7MPvFqkkANzz4JDHidnfKRULCgnrVj1yTU66UagUpQEGjZqz8/99YuU6nt1t
81sQ==
ARC-Authentication-Results: i=1; mx.google.com;
dkim=pass header.i=@googlemail.com header.s=20161025 header.b=aO4aNy7C;
spf=pass (google.com: best guess record for domain of postmaster@mail-sor-f69.google.com designates 209.85.220.69 as permitted sender) smtp.helo=mail-sor-f69.google.com;
dmarc=pass (p=QUARANTINE sp=QUARANTINE dis=NONE) header.from=googlemail.com
Return-Path: <>
Received: from mail-sor-f69.google.com (mail-sor-f69.google.com. [209.85.220.69])
by mx.google.com with SMTPS id w14sor16686480iow.23.2020.06.10.05.17.55
for <alice@gmail.com>
(Google Transport Security);
Wed, 10 Jun 2020 05:17:55 -0700 (PDT)
Received-SPF: pass (google.com: best guess record for domain of postmaster@mail-sor-f69.google.com designates 209.85.220.69 as permitted sender) client-ip=209.85.220.69;
Authentication-Results: mx.google.com;
dkim=pass header.i=@googlemail.com header.s=20161025 header.b=aO4aNy7C;
spf=pass (google.com: best guess record for domain of postmaster@mail-sor-f69.google.com designates 209.85.220.69 as permitted sender) smtp.helo=mail-sor-f69.google.com;
dmarc=pass (p=QUARANTINE sp=QUARANTINE dis=NONE) header.from=googlemail.com
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=googlemail.com; s=20161025;
h=to:auto-submitted:message-id:date:from:subject:references
:in-reply-to;
bh=XaR1H4XeD+InO7mULPJn53omDGmxN+KG6DbSxyyErPM=;
b=aO4aNy7CUOk9O4Jnsue/DvMFY6Ph0C34AbpoxJH+mLZpOmt/KYGCGYWgunZgamF15U
Vm8JY5yLKGwkTz2m3abDnKNP4fpl6zeZ5fyk5LvXH2Jema0iocHai6pJZBoFGPnonNmd
MscTf1sEltbOxwfOmM1BRHX34c1jW0+8Yd2+Nhg2DPvzuq1brOVin6bUV4VX5EeeuNqT
ZTewjJVPmO/B5NQhdpG81FO5w4hKSQ/VzZXnap2thMf3gOmnaoR+tbsnOIAiklcLdJ7b
57SKUwI041pwSmh9dffs0STl2GvMRSJyGCtBqMnzXgflqoGTcnPflWgR3LXHM/MIA0q8
WqRQ==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=1e100.net; s=20161025;
h=x-gm-message-state:to:auto-submitted:message-id:date:from:subject
:references:in-reply-to;
bh=XaR1H4XeD+InO7mULPJn53omDGmxN+KG6DbSxyyErPM=;
b=iORAzNvXegQ8oSp4RYb/S168muAiBox769seMk49kDBIvXwI+N8P4mUZq/zDi8DmQd
+wlLzVzowQq6EofiSpjOJWT9IC/k8otk15PMGtgHE4BGSSeKn7L30d3ocQS93HzYnLmA
VBlHBdFTKrsfKhe2+CQyCosTDGRpbkQLuRRyhxChEq8ltvaOHgbu1+eCeb9PsPuh6OxH
kvTHJZeA9A+eLOl26pBmqGIWkr7FlYW0wI6YPoEs9WXX5LSFOQs6fm/9l366eIR7IFFI
ihX5LrZl/Cf0lwwYX7fqIMgnHy1K+QnKuEb+dRQGqLbxdIEls9bXIF98iPQVkEWzgSZy
ip8Q==
X-Gm-Message-State: AOAM531ahfHE6oS9/nuni8pNf9bwC+DXAcaLV0owBwNCj9kcTPLCCNhX
W1JNciK0ivEIVB4dgiyLE/5K7iKbEznQhqyG9Bi1QA==
X-Google-Smtp-Source: ABdhPJygljUXswH0ycJyHmXVthi5IjlDvP8QdYlMdHUPKEtgIZeUk69Acti5LnswGhg63T9/L0PuGZGBM5XE5BsP0mMNNDRZyt+DgnE=
X-Received: by 2002:a05:6638:101c:: with SMTP id r28mr2990163jab.84.1591791475516;
Wed, 10 Jun 2020 05:17:55 -0700 (PDT)
Content-Type: multipart/report; boundary="00000000000074432a05a7b9d512"; report-type=delivery-status
To: alice@gmail.com
Received: by 2002:a05:6638:101c:: with SMTP id r28mr3059870jab.84; Wed, 10 Jun
2020 05:17:55 -0700 (PDT)
Return-Path: <>
Auto-Submitted: auto-replied
Message-ID: <5ee0cf73.1c69fb81.6888.c2f4.GMR@mx.google.com>
Date: Wed, 10 Jun 2020 05:17:55 -0700 (PDT)
From: Mail Delivery Subsystem <mailer-daemon@googlemail.com>
Subject: Delivery Status Notification (Failure)
References: <CADWx9Cs32Wa7Gy-gM0bvbq54P_FEHe7UcsAV=yW7sVVW=fiMYQ@mail.gmail.com>
In-Reply-To: <CADWx9Cs32Wa7Gy-gM0bvbq54P_FEHe7UcsAV=yW7sVVW=fiMYQ@mail.gmail.com>
X-Failed-Recipients: assidhfaaspocwaeofi@gmail.com
--00000000000074432a05a7b9d512
Content-Type: multipart/related; boundary="000000000000745e0805a7b9d51b"
--000000000000745e0805a7b9d51b
Content-Type: multipart/alternative; boundary="000000000000745e1705a7b9d51c"
--000000000000745e1705a7b9d51c
Content-Type: text/plain; charset="UTF-8"
** Die Adresse wurde nicht gefunden **
Ihre Nachricht wurde nicht an assidhfaaspocwaeofi@gmail.com zugestellt, weil die Adresse nicht gefunden wurde oder keine E-Mails empfangen kann.
Hier erfahren Sie mehr: https://support.google.com/mail/?p=NoSuchUser
Antwort:
550 5.1.1 The email account that you tried to reach does not exist. Please try double-checking the recipient's email address for typos or unnecessary spaces. Learn more at https://support.google.com/mail/?p=NoSuchUser h20sor9401601jar.6 - gsmtp
--000000000000745e1705a7b9d51c
Content-Type: text/html; charset="UTF-8"
<html>
<head>
<style>
* {
font-family:Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif;
}
</style>
</head>
<body>
<table cellpadding="0" cellspacing="0" class="email-wrapper" style="padding-top:32px;background-color:#ffffff;"><tbody>
<tr><td>
<table cellpadding=0 cellspacing=0><tbody>
<tr><td style="max-width:560px;padding:24px 24px 32px;background-color:#fafafa;border:1px solid #e0e0e0;border-radius:2px">
<img style="padding:0 24px 16px 0;float:left" width=72 height=72 alt="Fehlersymbol" src="cid:icon.png">
<table style="min-width:272px;padding-top:8px"><tbody>
<tr><td><h2 style="font-size:20px;color:#212121;font-weight:bold;margin:0">
Die Adresse wurde nicht gefunden
</h2></td></tr>
<tr><td style="padding-top:20px;color:#757575;font-size:16px;font-weight:normal;text-align:left">
Ihre Nachricht wurde nicht an <a style='color:#212121;text-decoration:none'><b>assidhfaaspocwaeofi@gmail.com</b></a> zugestellt, weil die Adresse nicht gefunden wurde oder keine E-Mails empfangen kann.
</td></tr>
<tr><td style="padding-top:24px;color:#4285F4;font-size:14px;font-weight:bold;text-align:left">
<a style="text-decoration:none" href="https://support.google.com/mail/?p=NoSuchUser">WEITERE INFORMATIONEN</a>
</td></tr>
</tbody></table>
</td></tr>
</tbody></table>
</td></tr>
<tr style="border:none;background-color:#fff;font-size:12.8px;width:90%">
<td align="left" style="padding:48px 10px">
Antwort:<br/>
<p style="font-family:monospace">
550 5.1.1 The email account that you tried to reach does not exist. Please try double-checking the recipient&#39;s email address for typos or unnecessary spaces. Learn more at https://support.google.com/mail/?p=NoSuchUser h20sor9401601jar.6 - gsmtp
</p>
</td>
</tr>
</tbody></table>
</body>
</html>
--000000000000745e1705a7b9d51c--
--000000000000745e0805a7b9d51b
Content-Type: image/png; name="icon.png"
Content-Disposition: attachment; filename="icon.png"
Content-Transfer-Encoding: base64
Content-ID: <icon.png>
iVBORw0KGgoAAAANSUhEUgAAAJAAAACQCAYAAADnRuK4AAAACXBIWXMAABYlAAAWJQFJUiTwAAAA
GXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABTdJREFUeNrsnD9sFEcUh5+PRMqZ
yA0SPhAUQAQFUkyTgiBASARo6QApqVIkfdxGFJFSgGhJAUIiBaQB0ZIOKVCkwUgURjIg2fxL4kS+
YDvkbC/388bi8N16Z4/d7J/5PsniuD3fyePP772ZeTsDQRAYQL/UGAJAIEAgQCBAIAAEAgQCBAIE
AkAgyJT3Mv+Eq7vYK8mTE+MDRCAghQECAeRQA5V2ZOpmg5vDx3NPzRbmGRMEcmTrEbNNB8zWfRD+
f/Efs2e3zCZvMjaksBg27TfbcuSNPEKP9ZyuAQKtHX2O9ncNgWC57umMPKvRNb0GEKgnLoUyxTQC
rcns0/6uIRAs8/hGf9cQCJZpTpjdO2f25/03z+mxntM1eLtsZAgiUtX4JcaBCAQIBAgECARQ8CJa
G5jab4J4pm4WZmO3OALVh802fIwcLkyPkcKAGggAgQCBAIEAgQCBABAIEAjKA/1AnahhbO5FdOOY
VsrrDbPBYcYKgf5D2wLaV3p+22xh1u17tO3S+DTcvxvagUDeivPgx/a/95J/73w7Sj26Hn4pKo2M
ehuV/KyBJM6d0f7k6RKx/R63vvL2tmf/ItDdM2ZTP6f7nkp9Y2fDx1v9akmpIU+KSCLVUghUQfSL
zVKeTklbLxGoctw/nzC5rw8L5KRNbkpnKq6pgSqEClzNnFzY+XnYWrt6VpVk1vbwWvg+RKCKMOUw
Q1LEOXA+/MX3mpJvGDHb265xtnzmFoUK1HaKQGlMtePYM+q2KKjXuaS1NJYIEKgI8jhEgqHt4cqy
Ky53j3hyHz2bqSLp2o2LbJ7MxKovkGqXteoWpaOk96O9/yF/dF7NwlS36AuIQIBA5celQK4PIxBE
4LLzrtoLgaALdSy6CJRkWQCBPGLsTHznomZ9nszUECgJ2ml3WWHe+QVFNPSQx6UdZNtxr9pbEShN
eTTz8mQXHoHSlke7+Z+c9m6VGoHSkEfs/trLW3wQKApN1V3lGfnGu2Z6BFoLtYCs3GWBPAiUCLVh
/HoaeRCoT9R873KLM/IgUBfapnCpe5AHgXry4pf412ihEHkQqCdxd5VqrcezhUIESsJMTJ+Pdthp
Z0WgyNlXXPHc2Mc4IVAELl2Gnh8mhUDvCkfbIVAkcbf/aOoO3fMKhqAD3frTa4quwpn0hUDOkQhI
YYBAgECAQAAU0QlYObl+5Ug8NcprZkZxjUCxRPVA6zmtEXHCBykskrhjgHXN09PoEcgFl4M4H11j
nBAoApcj6ZoPGScEAgTKApcDoTw5sgWB+sGlz1n90IBAPdE6j1o21PfcC11jLagL1oFWRyGlKU3p
OxcSJQ7NZAjkhHp/uG2HFAYIBAgECASAQIBAgECAQAAIBOkxEARBtp9wdVfAMOfIifEBIhCQwgCB
ABAI0oV2jhxZ+nfBatuPZfgBCy0Eqqo8c01b+uu51XZvzOgDWoHNTGR+pCwpLEd5svuAZXlO2uEr
PyEQ8hRWHgRCHmqg0sjTnLalv6crJQ8C/U8stqNO0I4+VZOHFIY8COS1PGL2ybd5yUMKK7s8zYmL
dujyd3n+nESgcsvzZd4/KwIhDwIhT35QA6UyE1qyxZnfvJMHgdKS549JC1qvvJOHFIY8CFR5eV5O
XimqPAhUdHnmfx+zgxdOFXkoqIGKKs/cswnb/8Oeog8HEai48nxUhiFBIORBIOShBioskkbySCLk
IQIhDwIhj28p7FApR6b1qlEbHGpkO/rr6215vi/zH1r2x7tApSGFAQIBAgECAQIBIBAgECAQIBBA
LK8FGADCTxYrr+EVJgAAAABJRU5ErkJggg==
--000000000000745e0805a7b9d51b--
--00000000000074432a05a7b9d512
Content-Type: message/delivery-status
Reporting-MTA: dns; googlemail.com
Arrival-Date: Wed, 10 Jun 2020 05:17:55 -0700 (PDT)
X-Original-Message-ID: <CADWx9Cs32Wa7Gy-gM0bvbq54P_FEHe7UcsAV=yW7sVVW=fiMYQ@mail.gmail.com>
Final-Recipient: rfc822; assidhfaaspocwaeofi@gmail.com
Action: failed
Status: 5.1.1
Diagnostic-Code: smtp; 550-5.1.1 The email account that you tried to reach does not exist. Please try
550-5.1.1 double-checking the recipient's email address for typos or
550-5.1.1 unnecessary spaces. Learn more at
550 5.1.1 https://support.google.com/mail/?p=NoSuchUser h20sor9401601jar.6 - gsmtp
Last-Attempt-Date: Wed, 10 Jun 2020 05:17:55 -0700 (PDT)
--00000000000074432a05a7b9d512
Content-Type: message/rfc822
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=gmail.com; s=20161025;
h=mime-version:from:date:message-id:subject:to;
bh=Y1ylbv3YC5frF/LtF2it4tQQ0OVZstDdWqivvggIOB0=;
b=eyr60XbgOrgHoZFpRYzw9WQIR7aEBaYKWhiEcqdnugB+hn0W2KVcTkKiL2C6zSF+jh
l+lM+dNZZTUcMqWx4kVgTVtqwUNea8OUqe+WLqx04ULwdKZn1okbKYovaiavCLKOKDnf
ZP5mNz3Ka/ywpCGoq8rdgnXc7NunnkWeaBpYY/BWOmLU4WNXX8zS7etXXhQE4YPQEJT4
Sh2o/YIIjDLncJFMyE+25n3tbd2mIoLt4sjaCHE5ibm9w7zojyHM+LDCQ37cM74FEAAa
88KTn0gSnCFBCfojhfxOH78CpySHG3FFfTlpCefwP2A5J9MQlb6QdSVa9STYSx3IntJ4
L7Tg==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=1e100.net; s=20161025;
h=x-gm-message-state:mime-version:from:date:message-id:subject:to;
bh=Y1ylbv3YC5frF/LtF2it4tQQ0OVZstDdWqivvggIOB0=;
b=pBL4/bKUDw5E2zo1uR2Tl69h2iTlMgIAcnzQgodPCbU4jZ9kH+F5H9rfbzXCjT06J7
L72SYpdfgc5fOwM4GhRcdYnyK3wiXQ8ugpL19nbYt2iWo/vRF3GidawXXDGb2GUYpkzX
1Mz531cy2/HOsmQbUQ7304KV+OUghtcg8eLNnFuhQch7n12Kk3yy3AOzjrLoktcdgIsy
/HxBjyut0Au+A2t6si+PVwTHvC647a0BioeV0tUYLigzu3/jgP9Hb8eRZaXTX5VC6iZi
9QMH/+rXp05IK7OpGWh22xDpeV8CDkQ2sLFaBhKxtJ+nYoerM64t8EJXBBsVQb18ojGz
pW/A==
X-Gm-Message-State: AOAM5330q6kn/TKataMNEVigNfNdr/xii/PQgHXzJyMbwLvsETlNfLoy
1rM9JBIGrcHeEDRx4qhZfl5S4bircceU7c3i6Fyn2fRO
X-Google-Smtp-Source: ABdhPJwysG+S90b/g+9mK7LgeHhmJTBowst6JMhL16+a0coTi7P1NVp9jjaNHJfhvhLodYG6eHIvWdbQGJnAP2brEzI=
X-Received: by 2002:a05:6638:101c:: with SMTP id r28mr2990137jab.84.1591791475066;
Wed, 10 Jun 2020 05:17:55 -0700 (PDT)
MIME-Version: 1.0
From: Deltachat Test <alice@gmail.com>
Date: Wed, 10 Jun 2020 14:18:26 +0200
Message-ID: <CADWx9Cs32Wa7Gy-gM0bvbq54P_FEHe7UcsAV=yW7sVVW=fiMYQ@mail.gmail.com>
Subject: test
To: bob@example.org, assidhfaaspocwaeofi@gmail.com
Content-Type: multipart/alternative; boundary="0000000000006d8d7d05a7b9d5b3"
--0000000000006d8d7d05a7b9d5b3
Content-Type: text/plain; charset="UTF-8"
test
--0000000000006d8d7d05a7b9d5b3
Content-Type: text/html; charset="UTF-8"
<div dir="ltr">test<br></div>
--0000000000006d8d7d05a7b9d5b3--
--00000000000074432a05a7b9d512--

View File

@@ -1,113 +0,0 @@
Return-Path: <>
Received: from mout-bounce.gmx.net ([212.227.15.44]) by mx-ha.gmx.net
(mxgmx101 [212.227.17.5]) with ESMTPS (Nemesis) id 1Mr97m-1jC6Y01o86-00oEqk
for <alice@gmx.de>; Tue, 09 Jun 2020 14:35:30 +0200
Received: from localhost by mout-bounce.gmx.net id 0LhiZF-1jDTj11ZoH-00msO3
Tue, 09 Jun 2020 14:35:30 +0200
Date: Tue, 09 Jun 2020 14:35:30 +0200
From: "GMX Mailer Daemon" <mailer-daemon@gmx.de>
To: alice@gmx.de
Subject: Mail delivery failed: returning message to sender
Auto-Submitted: auto-replied
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit
X-UI-Out-Filterresults: unknown:0;V03:K0:O8yx6kuPaGQ=:0wIDPNXEr0wX2oNsLnXaWA
==
Envelope-To: <alice@gmx.de>
X-GMX-Antispam: 0 (Mail was not recognized as spam); Detail=V3;
X-Spam-Flag: NO
X-UI-Filterresults: notjunk:1;V03:K0:QcE43EBhMmU=:IC5vvzi9jhPS/698Wuubzw1Q4N
h87X9j9B3CBN0ZKXB67KepwyNHmh9pxmFIUOMimylv7UK9np+j3X55roOd0nX9BmaaZ3Twvqf
UaSsxmyU+cNr6m3+oOb3udJBLe2pJEZDk1cOwACb5NXzYPSaIj4APfGCyvrzIx3FGkNuScNBb
tCbbKUJ0GB/VmJLB34XfF6dNN+Iwv9IQ9Yrvw/VXv9vWKsi3qRGGUt3yRw5jUKhQlBY21Pnoq
m0LqoMbAKfH1tKEQ/5TymH1ei50YKyWzZ89ISkQwkbYLaqN+6meGACpY18j43VCU9Fk4WQR7y
3XvBYh2CO0CnCn+M9VsnasYag2sNrySe9nzyKfRTaxEg8qlJtl7kS4GX/FsxhHPavkqnU62Gl
9V5TxIG7tmIR0Bf11sPzG/WGegoOHxrfz+qYR81llLMOHznpdDRKjsYDtO/rFBGZzYTiCZsrW
dZPVXV25SVcrDGZOaop3JoCbglmXLcSLLhmfE5MzyJEGte3I+6EiZJNeIe8qN3wMDTsRtJL9S
J6b2F/5/kTGVAWnXtNlf69BholCrxvjC4Snt3Xjc+7WIO8iw2c5YjmWy+4bAwd4uWll529hZd
6pUYGwjFRnKleivCaJIt7DqbvbE7GZSbQH8fXm3zYqYTrrxiWdlykdoOA1OGbeM06RHJt3mJB
osZPU8BZKt0OiBOW64vg6gyAsNC0f02EA7dvRWYgFYqlSogfWZQIOKDKibMVHpIaA0foXg4BG
TEQDlsTIL0n2WC9WVqkMdm6xUXHgpArCrAsUhw3mEqPywEfJeBHn60tP2vQ9+pDIQAj5dQCDV
y96qSiCX4p31HfrWwAXB9mHfl4OO/tPcKUGBclj2rZ/NMc4O+7yDedLWXQnRzQExfOJLBbBh3
xgiNlWFHvDLn0pKG9EI1+3wJ7m2GF2jzDtbQTBv9z26DuAq5WbHZHupzeyfP7VCVXcKuB6sG1
3+LWcdYtcXfqT58HwcvDLwowC4uJpiHfHwtVdiGMtHnmYLysp0V425g+vofQfNzBgR3d9JC15
G+HS44o6x6Legm6KnHYH3k0KhR7fgcgswJv/S+I/ryppUhGb2jezVZIUzgvAplzIUDAWnrHdF
KVqZ5wBJ0acShIfgMlsIxnBmcnIQ4R4jq3zAyj4XTFxVUFanU8ySiXubxV5PzJqj+GsVa2sjx
9n/xQRJLwgMC4BYqzP6lEPwg/g5AneDAnl7ZlcQPC4SCMblC8N1KZyyIDTXPOI/o4lfdMYb9P
7DmBp2S8aA2yuDe5XT20OmX3kVWeBOsBaAGvVFpIn7gwIDqnFh9WSMP6mkCwfChN3D1yLquYB
KAODgRZV5lVNmK+eOjW8m2oiRxmfxrjXLtw5PEhn3RkiRN4HnoePJeoYC7SG4EUwg+wYPu3M6
exP/YigoE6bjuBS5d0imUTRDMiwg469GyrFo1J1GkRVvj3lXSF4Nt11j6waqu1l0ReDYfU+QM
EMPGLEh1vRChCaqz4L7YI5FlSAVXxfmst0JRyE4k5r9CToEbZuYlPQ+jbcvptwBSaqzMb/YfT
cCrU2RWHUfmYIy8x8A/t8ScRbYPzs1lTK3yn1hYeXpw8Fgkip6DIIXAJwUUp+2SLcELICIo+p
uumMN0P/OZHH3V/hZ0dPr9xsYi7gdd/vyRIRPUwiL1rSp2WJGi+w4atun8kQBgnbAznRObDh1
4zzKhApX9jo5gtFN6640QDEI5KpsMuPoty4rj9OK163ntKWGR451n+5ZX2FilTlpZYlIPO9Hy
SrjHzBog4texceR1OLh4pb/WFB0XFSjQchPAltXCYQFs82aDDk/A0nOPk=
This message was created automatically by mail delivery software.
A message that you sent could not be delivered to one or more of
its recipients. This is a permanent error. The following address(es)
failed:
snaerituhaeirns@gmail.com:
SMTP error from remote server for RCPT TO command, host: gmail-smtp-in.l.google.com (66.102.1.27) reason: 550-5.1.1 The email account that you tried to reach does not exist. Please
try
550-5.1.1 double-checking the recipient's email address for typos or
550-5.1.1 unnecessary spaces. Learn more at
550 5.1.1 https://support.google.com/mail/?p=NoSuchUser f6si2517766wmc.21
9 - gsmtp
--- The header of the original message is following. ---
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=gmx.net;
s=badeba3b8450; t=1591706130;
bh=g3zLYH4xKxcPrHOD18z9YfpQcnk/GaJedfustWU5uGs=;
h=X-UI-Sender-Class:To:From:Subject:Date;
b=NwY6W33mI1bAq6lpr6kbY+LD2hO9cDJBItTgY3NRIT94A6rKTVlSmhFM3AxYgFnj0
Db0hncsNRDqcdtRoOo8Emcah5NJURvEQohG37lkug3GqneB4+FNTdYCeQbOKlZn6on
pYYD/T9CmeL2HG3+8voeBjZIUenyXrF2WXG37hFY=
X-UI-Sender-Class: 01bb95c1-4bf8-414a-932a-4f6e2808ef9c
Received: from [192.168.178.30] ([84.57.126.154]) by mail.gmx.com (mrgmx005
[212.227.17.190]) with ESMTPSA (Nemesis) id 1MKbkM-1jNoq60HKm-00KyL2 for
<snaerituhaeirns@gmail.com>; Tue, 09 Jun 2020 14:35:30 +0200
To: snaerituhaeirns@gmail.com
From: Alice <alice@gmx.de>
Subject: test
Message-ID: <9c9c2a32-056b-3592-c372-d7e8f0bd4bc2@gmx.de>
Date: Tue, 9 Jun 2020 14:36:10 +0200
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101
Thunderbird/68.8.1
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 7bit
Content-Language: de-DE
X-Provags-ID: V03:K1:7awoptmynaF5MqxCAincdsa7zFCwQFNkFb6xRxigK06uEiGN00b
Mv2wJU91CEd4mvCCWzrTtaWLZDLH8pjAWaT4+HvYIUbpwNx2jC6WHTppkYYRMVJnm4lG9pr
SUx1OlIcp0kbsnl3mB9xYNFwm9jzpR9Kx8QEHwIbZiiSFBcH56498UGQi//kKXVMos8C14o
I7cmwYmr8xB09DwLMKXfg==
X-Spam-Flag: NO
X-UI-Out-Filterresults: notjunk:1;V03:K0:NWQAUbhAkBc=:yAaolOnVCDWEZhgUwwvtEs
wXbSJ/GfMvDRpCkYpFBvHXOTpGm6hjdjQ0vLK2hvu/Hz22UdlWbIdc1J2oO9S5U20mIdc+1bS
TPSSpqPFc7ICPx4Wbvv2SEp9ZqH2q7ORC52UvUWfI6OjAJEPDNrXQFdUiZAa72hLj1NPeG6Qi
4AbL2HwLfJ8s6TeOCm6TXRRuD+w1o/ASFOqQmoao2dFyZ2BaoAgOKPKxXYfwVGceuUygpchyS
0d2bZYOXSLR+6rUYevjZAq1OCi9AIC6/wlkOe5yIRk4gJFMfPauaICsdnq3uZ9ikCAX83VWun
PJVMxTLTP54lgo2h0jMBX3uKk10+/wzXWplllxX9NnSa3x1V28n6raslNF0IoC6Pm72kC6Jzr
GkC22viCm3/Y4uHlPMOXbY5WFrQe/D9GKeJeXBLoGciNwIFkUG12a+iqWtoT+h5HVObTW8LxM
+UtEl97nAwxYSM+sGfIpasRpZc7r/SgN3JWGO9R9WaXpW4Cc1dH7RI+hzuZDsDUBEGTUTVPDo
0SvjKHiJ6sUqGDyfv4HUgVutus6EYP27LALND4ekfom2DPRFopZhbtV5fZT7CL1Q1NogU7tYf
/FdmR1T1J1zCAZSFvyR5LBkfglZlHzgdnTF2heuxyqKq4dm0hnLFSULB1+CVWsg8hzrruvO5q
XzA6qIhBQUZmWo7wBpqpkBPxzjgTGGtXc5y5e6+crxYbbuQdnUWEnyw2xI4d6pJPqtHDA2/vT
ZgvNDUGceavTR5Rtyb14hhX4Q6dWK3ATy16j4hs9Aq+q/IKyVAX3A5nFYbJRIz+2YnoLr2YOa
IrScEorXjvTxjw+aBy73SZBe2REPzJ+O6k7chVrYjV9Q28FiGVuRYJYxWw/59Pes7IAbmfQBV
4vqGCQQr4eG78gVwjw+SNdp4/6jdNkIHDqR4XW9id/r5wYxKKj4UUkSor3/+h9Rd9srh+GApy
uOxw/ejFvbRcxFIjvadpq1KLnO7nM27nJ4lp44ul3i7VUGefLM/45TCsuds2HM1iQWhPFQ54y
SA5sYjf73EUJdkHchaf5i+4uSOmbOWQ4Yvmd8+IoyoXAxvEzY2Xh53nWi8ZPY1Tu4Bw8GRrz7
L+VK0QiWCg3/hM7wRlFFyshmMrScMk5fOf9ynqd0JbHB7u+n4/GUwx3im/w8+NgSd3YOz7wNU
KD1snDWoMUO8f23Ik1Osym688OLWNwKYT+mZbMIMXcz1fB+olRZn4czMhN5DiSb8hyOxRI8NE
PNfaoN87CXiRkgazV6U1eiRkfcK2AvI7zOJF1tclUHZ9awyYoXtxfEzZ+J/2TCXiC7V2iSkUF
EjwgPxlJmccccjsxc46v1ajnTxLo0tJbZ0+DJXWkCgQ0d/iiScQ=

View File

@@ -1,113 +0,0 @@
Return-Path: <>
Delivered-To: alice@posteo.org
Received: from proxy02.posteo.name ([127.0.0.1])
by dovecot03.posteo.local (Dovecot) with LMTP id zvCFJRzX317LGQIA+3EWog
for <alice@posteo.org>; Tue, 09 Jun 2020 20:44:24 +0200
Received: from proxy02.posteo.de ([127.0.0.1])
by proxy02.posteo.name (Dovecot) with LMTP id mhNkNAnR316xBQMAGFAyLg
; Tue, 09 Jun 2020 20:44:23 +0200
Received: from mailin06.posteo.de (unknown [10.0.1.6])
by proxy02.posteo.de (Postfix) with ESMTPS id 49hJtv3RRcz11m7
for <alice@posteo.org>; Tue, 9 Jun 2020 20:44:23 +0200 (CEST)
Received: from mx04.posteo.de (mailin06.posteo.de [127.0.0.1])
by mailin06.posteo.de (Postfix) with ESMTPS id 6935920DD2
for <alice@posteo.org>; Tue, 9 Jun 2020 20:44:23 +0200 (CEST)
X-Virus-Scanned: amavisd-new at posteo.de
X-Spam-Flag: NO
X-Spam-Score: -1
X-Spam-Level:
X-Spam-Status: No, score=-1 tagged_above=-1000 required=8
tests=[ALL_TRUSTED=-1] autolearn=disabled
Received: from mout01.posteo.de (mout01.posteo.de [185.67.36.65])
by mx04.posteo.de (Postfix) with ESMTPS id 49hJtv001Vz10kT
for <alice@posteo.org>; Tue, 9 Jun 2020 20:44:22 +0200 (CEST)
Authentication-Results: mx04.posteo.de; dmarc=none (p=none dis=none) header.from=mout01.posteo.de
Received: by mout01.posteo.de (Postfix)
id DCB6B1200DD; Tue, 9 Jun 2020 20:44:22 +0200 (CEST)
Date: Tue, 9 Jun 2020 20:44:22 +0200 (CEST)
From: MAILER-DAEMON@mout01.posteo.de (Mail Delivery System)
Subject: Undelivered Mail Returned to Sender
To: alice@posteo.org
Auto-Submitted: auto-replied
MIME-Version: 1.0
Content-Type: multipart/report; report-type=delivery-status;
boundary="B39111200B9.1591728262/mout01.posteo.de"
Content-Transfer-Encoding: 7bit
Message-Id: <20200609184422.DCB6B1200DD@mout01.posteo.de>
This is a MIME-encapsulated message.
--B39111200B9.1591728262/mout01.posteo.de
Content-Description: Notification
Content-Type: text/plain; charset=us-ascii
This is the mail system at host mout01.posteo.de.
I'm sorry to have to inform you that your message could not
be delivered to one or more recipients. It's attached below.
For further assistance, please send mail to postmaster.
If you do so, please include this problem report. You can
delete your own text from the attached returned message.
The mail system
<hanerthaertidiuea@gmx.de>: host mx01.emig.gmx.net[212.227.17.5] said: 550
Requested action not taken: mailbox unavailable (in reply to RCPT TO
command)
--B39111200B9.1591728262/mout01.posteo.de
Content-Description: Delivery report
Content-Type: message/delivery-status
Reporting-MTA: dns; mout01.posteo.de
X-Postfix-Queue-ID: B39111200B9
X-Postfix-Sender: rfc822; alice@posteo.org
Arrival-Date: Tue, 9 Jun 2020 20:44:22 +0200 (CEST)
Final-Recipient: rfc822; hanerthaertidiuea@gmx.de
Original-Recipient: rfc822;hanerthaertidiuea@gmx.de
Action: failed
Status: 5.0.0
Remote-MTA: dns; mx01.emig.gmx.net
Diagnostic-Code: smtp; 550 Requested action not taken: mailbox unavailable
--B39111200B9.1591728262/mout01.posteo.de
Content-Description: Undelivered Message Headers
Content-Type: text/rfc822-headers
Return-Path: <alice@posteo.org>
Received: from mout01.posteo.de (unknown [10.0.0.65])
by mout01.posteo.de (Postfix) with ESMTPS id B39111200B9
for <hanerthaertidiuea@gmx.de>; Tue, 9 Jun 2020 20:44:22 +0200 (CEST)
Received: from submission-encrypt01.posteo.de (unknown [10.0.0.75])
by mout01.posteo.de (Postfix) with ESMTPS id 8A684160060
for <hanerthaertidiuea@gmx.de>; Tue, 9 Jun 2020 20:44:22 +0200 (CEST)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=posteo.de; s=2017;
t=1591728262; bh=g3zLYH4xKxcPrHOD18z9YfpQcnk/GaJedfustWU5uGs=;
h=To:From:Subject:Date:From;
b=brJnt4PLAX3Tda1RHCo91aB1kMAL/Ku9dmO7D2DD41Zu5ShNsyqqyDkyxb1DsDn3O
6KuBZe3/8gemBuCJ/mxzwd9v8sBnlrV+5afIk0Ye9VvthZsc4HoG79+FiVOi9F38o0
DtJJFYFw/X7mAc5Xyt0B0JvtiTPpBdRAkluUQm+QW6cW6GGlwicVW19qvebzq+sHyP
X2bZ8wpo78yVgvjPBK3DLaXa+pKFMBjLdDUcIE2bZnY6u6F1x8SXGKGBoxVwdJipJx
v14so5IejNsf4LYJjH3Qb8xgK1aAi6e6nQn4YXV0INL6ahzgALiT9N6vwunNKYVJNi
fPPKvBWDfUS4Q==
Received: from customer (localhost [127.0.0.1])
by submission (posteo.de) with ESMTPSA id 49hJtt1WPbz6tmV
for <hanerthaertidiuea@gmx.de>; Tue, 9 Jun 2020 20:44:22 +0200 (CEST)
To: hanerthaertidiuea@gmx.de
From: deltachat <alice@posteo.org>
Subject: test
Message-ID: <04422840-f884-3e37-5778-8192fe22d8e1@posteo.de>
Date: Tue, 9 Jun 2020 20:45:02 +0200
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101
Thunderbird/68.8.1
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 7bit
Content-Language: de-DE
Posteo-User: alice@posteo.org
Posteo-Dkim: ok
--B39111200B9.1591728262/mout01.posteo.de--

View File

@@ -1,107 +0,0 @@
Return-Path: <>
Delivered-To: alice@testrun.org
Received: from hq5.merlinux.eu
by hq5.merlinux.eu (Dovecot) with LMTP id Ye02K6PB5F43cQAAPzvFDg
for <alice@testrun.org>; Sat, 13 Jun 2020 14:08:03 +0200
Received: by hq5.merlinux.eu (Postfix)
id 9EBE627A0B2E; Sat, 13 Jun 2020 14:08:03 +0200 (CEST)
Date: Sat, 13 Jun 2020 14:08:03 +0200 (CEST)
From: MAILER-DAEMON@hq5.merlinux.eu (Mail Delivery System)
Subject: Undelivered Mail Returned to Sender
To: alice@testrun.org
Auto-Submitted: auto-replied
MIME-Version: 1.0
Content-Type: multipart/report; report-type=delivery-status;
boundary="CDB8D27A0B2C.1592050083/hq5.merlinux.eu"
Content-Transfer-Encoding: 8bit
Message-Id: <20200613120803.9EBE627A0B2E@hq5.merlinux.eu>
This is a MIME-encapsulated message.
--CDB8D27A0B2C.1592050083/hq5.merlinux.eu
Content-Description: Notification
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit
This is the mail system at host hq5.merlinux.eu.
I'm sorry to have to inform you that your message could not
be delivered to one or more recipients. It's attached below.
For further assistance, please send mail to postmaster.
If you do so, please include this problem report. You can
delete your own text from the attached returned message.
The mail system
<hcksocnsofoejx@five.chat>: host mail.five.chat[195.62.125.103] said: 550 5.1.1
<hcksocnsofoejx@five.chat>: Recipient address rejected: User unknown in
virtual mailbox table (in reply to RCPT TO command)
--CDB8D27A0B2C.1592050083/hq5.merlinux.eu
Content-Description: Delivery report
Content-Type: message/global-delivery-status
Content-Transfer-Encoding: 8bit
Reporting-MTA: dns; hq5.merlinux.eu
X-Postfix-Queue-ID: CDB8D27A0B2C
X-Postfix-Sender: rfc822; alice@testrun.org
Arrival-Date: Sat, 13 Jun 2020 14:08:01 +0200 (CEST)
Final-Recipient: rfc822; hcksocnsofoejx@five.chat
Original-Recipient: rfc822;hcksocnsofoejx@five.chat
Action: failed
Status: 5.1.1
Remote-MTA: dns; mail.five.chat
Diagnostic-Code: smtp; 550 5.1.1 <hcksocnsofoejx@five.chat>: Recipient address
rejected: User unknown in virtual mailbox table
--CDB8D27A0B2C.1592050083/hq5.merlinux.eu
Content-Description: Undelivered Message
Content-Type: message/global
Content-Transfer-Encoding: 8bit
Return-Path: <alice@testrun.org>
Received: from localhost (p200300edb723070079835ce22985a199.dip0.t-ipconnect.de [IPv6:2003:ed:b723:700:7983:5ce2:2985:a199])
by hq5.merlinux.eu (Postfix) with UTF8SMTPSA id CDB8D27A0B2C;
Sat, 13 Jun 2020 14:08:01 +0200 (CEST)
DKIM-Signature: v=1; a=rsa-sha256; c=simple/simple; d=testrun.org;
s=testrun; t=1592050082;
bh=Kvhta0OMsTRVC7OlaAqo68TBE0KuGBv4vUBp6Ez/7VY=;
h=Subject:References:In-Reply-To:Date:To:From:From;
b=Ql60JEGFXLNvjsyihATw2z34ct++8xZvTPNw0snXe6+oqdqsRZJ9tWNDTxOgx8Iqf
HQ4puBVGcWjIlszYQVLlq3APi04o2ep3GrD8EF0J0GpDdW8yw6wCos6Q8r+TWmXwET
kGXHTRPVaUIqZF2i/utypxMfd1ua0S3jBDnIXTe/p2+XvfC3Cf3hZGW+FQ/Zd7G8Vh
/U2rgX5BTIGf26ZCbmcMaXWkftgv6+yns0AmzorV9yB+EhTkWIUjk+C25bRtMbJ5mZ
93dwdr+sXrrSZLSi+LBqc57Dv9j4p/SUmB4zPlvfUv7/bqLi36pypvtCJ5Ul8UEXSb
XNFZPaEl+mwjA==
Content-Type: text/plain; charset=utf-8
Chat-Disposition-Notification-To: alice@testrun.org
Subject: =?utf-8?q?Message_from_hocuri1=40testrun=2Eorg?=
MIME-Version: 1.0
References: <Mr.VSg3KXFUOTG.9sn7JBxZn1W@testrun.org>
<Mr.F4nR4LnXb6v.gqVbCJRgsmn@testrun.org>
In-Reply-To: <Mr.F4nR4LnXb6v.gqVbCJRgsmn@testrun.org>
Date: Sat, 13 Jun 2020 12:08:01 +0000
X-Mailer: Delta Chat Core 1.35.0/Android 1.9.5
Chat-Version: 1.0
Autocrypt: addr=alice@testrun.org; prefer-encrypt=mutual;
keydata=xjMEXt3z1xYJKwYBBAHaRw8BAQdAf6MctU/8cmEqwEN9VFZ3gHBFIxKiEaARZl1DFUkI7e
rNFTxob2N1cmkxQHRlc3RydW4ub3JnPsKLBBAWCAAzAhkBBQJe3fPXAhsDBAsJCAcGFQgJCgsCAxYC
ARYhBIZ3ajRUkki89+04sgctAtaIXygAAAoJEActAtaIXygAG2IA/1nTmmmkHAc1Bjtx2FOstbaS+N
XHjxaK+hkoWllsyhz0AQDJJ1++u7jVZPRn/j1LlByrT3Jv/D1aY14J5rjj+ADVBM44BF7d89cSCisG
AQQBl1UBBQEBB0DpSTaZ30dAVwM9PkBe2h+gFyxn9HSorP4XCHJu/lIdPAMBCAfCeAQYFggAIAUCXt
3z1wIbDBYhBIZ3ajRUkki89+04sgctAtaIXygAAAoJEActAtaIXygA2QkA/16toWCtseYKw8G1X2j7
xYR3Cyabq37hgbesDOThIIzNAP0UCUS8mnunmkS5adEbftRaDi2JZoGxDw46jtJJ2+13Cw==
Message-ID: <Mr.A7pTA5IgrUA.q4bP41vAJOp@testrun.org>
To: <hcksocnsofoejx@five.chat>
From: <alice@testrun.org>
F
--
Sent with my Delta Chat Messenger: https://delta.chat
--CDB8D27A0B2C.1592050083/hq5.merlinux.eu--

View File

@@ -1,91 +0,0 @@
Return-Path: <>
Delivered-To: alice@tiscali.it
Received: from director-5.mail.tiscali.sys ([10.39.80.174])
by dovecot-08.mail.tiscali.sys with LMTP id SBRfEpGb517VAgAAd2fHbg
for <alice@tiscali.it>; Mon, 15 Jun 2020 16:02:25 +0000
Received: from cmgw-4.mail.tiscali.it ([10.39.80.174])
by director-5.mail.tiscali.sys with LMTP id MFUPL5Cb516tawAArQJVuQ
; Mon, 15 Jun 2020 16:02:25 +0000
Received: from michael.mail.tiscali.it ([213.205.33.246])
by cmgw-4.mail.tiscali.it with
id rTtS2200V5JdeUd01U2RlV; Mon, 15 Jun 2020 16:02:25 +0000
x-cnfs-analysis: v=2.3 cv=ZdPMyfdA c=1 sm=1 tr=0 cx=a_idp_d
a=AfTPebshMYb+aQOCLa9q3Q==:117 a=HpEJnUlJZJkA:10 a=jmdcTMp_Gj4A:10
a=r77TgQKjGQsHNAKrUKIA:9 a=b8iBRs35AAAA:8 a=NMdB-582e605uxHDr_AA:9
a=QEXdDO2ut3YA:10 a=MhhPCb74-dYA:10 a=HXlsH_Kov2KnitTn7A4A:9
a=BkuCPOF3BOzethIN9HQA:9 a=qG5HpJ6ZyD35YNEB:21 a=kvHihYffoorsyJbA:21
a=xD8EQi6zkreDqSNPYj5l:22
Date: Mon, 15 Jun 2020 16:02:25 +0000
From: Mail Delivery System <mail-daemon@smtp.tiscali.it>
To: alice@tiscali.it
Subject: Delivery status notification
MIME-Version: 1.0
Content-Type: multipart/report; boundary="------------I305M09060309060P_896715922369450"
This is a multi-part message in MIME format.
--------------I305M09060309060P_896715922369450
Content-Type: text/plain; charset=UTF-8;
Content-Transfer-Encoding: 8bit
This is an automatically generated Delivery Status Notification.
Delivery to the following recipients was aborted after 2 second(s):
* shenauithz@testrun.org
--------------I305M09060309060P_896715922369450
Content-Type: message/delivery-status; charset=UTF-8;
Content-Transfer-Encoding: 8bit
Reporting-MTA: dns; michael.mail.tiscali.it [213.205.33.13]
Received-From-MTA: dns; localhost [146.241.100.150]
Arrival-Date: Mon, 15 Jun 2020 16:02:23 +0000
Final-recipient: rfc822; shenauithz@testrun.org
Action: failed
Status: 5.1.1
Diagnostic-Code: smtp; 550 5.1.1 <shenauithz@testrun.org>: Recipient address rejected: User unknown in virtual mailbox table
Last-attempt-Date: Mon, 15 Jun 2020 16:02:25 +0000
--------------I305M09060309060P_896715922369450
Content-Type: text/rfc822-headers; Content-Transfer-Encoding: 8bit
Content-Disposition: attachment
x-auth-user: alice@tiscali.it
Chat-Disposition-Notification-To: alice@tiscali.it
Chat-User-Avatar: avatar.jpg
Subject: =?utf-8?q?Message_from_=F0=9F=8F=9E=EF=B8=8F_Mefiscali?=
MIME-Version: 1.0
Date: Mon, 15 Jun 2020 16:02:22 +0000
X-Mailer: Delta Chat Core 1.35.0/Android 1.9.5
Chat-Version: 1.0
Autocrypt: addr=alice@tiscali.it; prefer-encrypt=mutual;
keydata=xjMEXtFRUBYJKwYBBAHaRw8BAQdA5sqHJqkWlveCgsNd0rtwtZrT1mmo1gwaGC5+WheYk5
nNHTxhbmRyZWFzLmxhdHRtYW5uQHRpc2NhbGkuaXQ+wosEEBYIADMCGQEFAl7RUXoCGwMECwkIBwYV
CAkKCwIDFgIBFiEEJUsbRIjZEaRNAs1p1Z5M1vshkrAACgkQ1Z5M1vshkrAAaAEA4wssXeU2IXnowv
iu3zmcNzDgE4HdmW4RFyqJC6bgxXQA/3aTfE/PhQgZvi6RrKMvP4zygXpD9y+3ydIZP88Bp8kIzjgE
XtFRUBIKKwYBBAGXVQEFAQEHQKaAwlP0j9m0aYsCtO+qD9+foH0kiTN5BWDe5YcZrckVAwEIB8J4BB
gWCAAgBQJe0VF6AhsMFiEEJUsbRIjZEaRNAs1p1Z5M1vshkrAACgkQ1Z5M1vshkrA1JgEAkscCQlps
h3ZxlLqBlbf2+85f4S4aGQfFPtIYEkKKhYEBAJbQulNNp9UarvhfyBiIdvkBVDcCnJZwzbORqp8RM0 gC
Message-ID: <Mr.un2NYERi1RM.lbQ5F9q-QyJ@tiscali.it>
To: <shenauithz@testrun.org>
From: =?utf-8?b?8J+Pnu+4jyBNZWZpc2NhbGk=?= <alice@tiscali.it>
Content-Type: multipart/mixed; boundary="5uAmYQux1HZxxriijTjjKSp4DMoJwq"
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=tiscali.it; s=smtp;
t=1592236943; bh=C3taz+zuSre1ko5Q5CzGPmbyrgegKYBClx/3Dv7t/Xw=;
h=Subject:Date:To:From;
b=LrcfLfrQoemOkHTQsqR8MExqNlx5KPYNFWhwlBWylvVc5GlmlhzqM6SAVKd0NVsKE
gVRlBId5FvnlwoJ2WZnXaw/+3lWKilMTuzzQ1oFGvLnZ1XUaUEfuliIv+9NI79dJWX
+S3jsSgzJMJc9+fO6s9bJsX1EHQ2a8GXNbwDtLXs=
--------------I305M09060309060P_896715922369450--

View File

@@ -1,15 +0,0 @@
Content-Type: text/plain; charset=utf-8
Subject: Message from user
Message-ID: <Mr.f1O61111evx.ikocf333353@example.org>
Date: Mon, 20 Jul 2020 14:28:30 +0000
X-Mailer: Delta Chat Core 1.40.0/CLI
Chat-Version: 1.0
Chat-Content: videochat-invitation
Chat-Webrtc-Room: https://example.org/p2p/?roomname=6HiduoAn4xN
To: <tunis3@example.org>
From: "=?utf-8?q??=" <tunis4@example.org>
You are invited to an videochat, click https://example.org/p2p/?roomname=6HiduoAn4xN to join.
--
Sent with my Delta Chat Messenger: https://delta.chat

View File

@@ -1,100 +0,0 @@
X-Atlas-Received: from 10.218.250.153 by atlas222.free.mail.ne1.yahoo.com with http; Wed, 10 Jun 2020 10:11:23 +0000
Return-Path: <>
Received: from 77.238.177.145 (EHLO sonic314-19.consmr.mail.ir2.yahoo.com)
by atlas222.free.mail.ne1.yahoo.com with SMTPs; Wed, 10 Jun 2020 10:11:23 +0000
X-Originating-Ip: [77.238.177.145]
Received-SPF: none (domain of sonic314-19.consmr.mail.ir2.yahoo.com does not designate permitted sender hosts)
Authentication-Results: atlas222.free.mail.ne1.yahoo.com;
dkim=pass header.i=@yahoo.com header.s=@bounce;
spf=none smtp.mailfrom=sonic314-19.consmr.mail.ir2.yahoo.com;
dmarc=success(p=REJECT) header.from=yahoo.com;
X-Apparently-To: alice@yahoo.com; Wed, 10 Jun 2020 10:11:23 +0000
X-YMailISG: RT5ZnycWLDvIW52uqHS_EWNgl31NdJPyLLB2F4SYb1GCAoo9
pcninuVU5GDMBZykeMT4cSUt4ZqXxS5FdEeWJqtGIAtbEGbIL8Uhcoszqm4m
JuMJiQZwEE7W_fsS_9MUK5gZtMkhKkSnAuaeaOLKNYAwFZdBqA0uEYA5EmVf
EC9J4RGQ4hZvrMqMj_W.cj4pvbEC.pyirLxTfkICuUkZVguYoxG16y1EOJPw
B48fhXvF5ErU7WAHKxyRM3bMOg7b5pXHKn1dtRSVAXEuqBAQrWig1pePpYH1
wO54sYT7cgmdiFvfLY5rR7YcBzopmKJBycKzBVoRLCY4gvoNyTLPKx9o3AAz
WU4B7TGejDBElYSLpfnyvQg8wU27zzo2IVBZWUNztP0Ca8CQ07Y7TxUZAO.f
DNO5c7nd81PHMRDbSeaw1BTV2Yd9vlBc7syYmwGvtVBJQwRU7qPN.DpFO2jC
9j9DytVhm5231gdBBRSzW78yG.VvaIdJgq_YViKNM9VxFseTz3Sjt3TaYznP
gAVq.MxpopNsSZf_tedwAhXDWyrjKsRPK.v2ANivmuWGPednniEaMYhxJ05M
_5SnJ.hAU.l6h3HCEfiU.SH390_3tZgYNfxCo4GPPFMfnNPmKa3.rgpChBCz
9CRexJ8BSFyCEeAhuqQ8vSJfSuittJmXvS6Tk8Rxd9HUJAtKzZ.xCWZQ4tA6
Yp2aRG23_rK_C6hH8ArkkvbG.uVQTt6DltSX6avJLObBfIhBH0x64RoFjGee
vYXxM741Okm0jH7r79c8GhnAwas_bwfkaTW9e1nhYP0eyI36z_QwLYgOH3Mm
LrUcejpOMDR60QWDuDyRbWXOJdr3Q2K0ERhuAy6YnINq0sL3HX7t5wjsFLvp
_7Ri_eruTfIst4C7DZwERwui6aDSEAdF1Z8oZukBVmiyZsHmhJQCUik646iy
3ASMR3lX7R3q2PBHQo2oC3qte8Fzz1FhKoMtfCGtIpeCazlkhDEJ6eTBSQ3R
Pe7M_GPiv3QNp7qu5CWHlzy6hWEKIkNwx.WRGYzfxkyJMmJm4UrhQYUfa4lG
Wb8n.mfYnS_KGYtzyRFNqAL0IGo.1MB9aG6qQk456Fz9GJgbHLWrMXVtyfrr
Uo7mKih8FCrdUKv5X6KBnpY0vvyoH5jrWyrvo3DW0bq_JvZ9U51JwUhoGY5U
c1t.yCSJbs8tnrGZHuUTOvouWzpCAJsk34AqRyH0wDJZQsAwBW5UZ3jx8ARA
FicoSqZCa4wEP9WaaXvfzFbmLW0-
X-Originating-IP: [77.238.177.145]
Received: from 10.217.135.165 (EHLO sonic314-19.consmr.mail.ir2.yahoo.com) (77.238.177.145)
by mta4277.mail.ne1.yahoo.com with SMTPS; Wed, 10 Jun 2020 10:11:21 +0000
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=yahoo.com; s=bounce; t=1591783879; bh=4BmZzBC/nu0AJ9r0i0xNuCENks2KZcuXCbjSHdzbg9Y=; h=Date:From:To:Subject:From:Subject; b=lPxu8goOGOLVgnwbndfdptZ7zI5VEo0lSSr+ONGxwdtuhrySKDU6Sp41/g6jWbAiVPT1947j/B5wOlPfa5tv4XkWrGf0JCbT1I20ZJIkNfNwt4F0qPnbJAiHFIDPxcY68utjC9IgPWJd0cGqJNXbFwbJBu88rtrbMoInzLakh5I=
Received: from sonic.gate.mail.ne1.yahoo.com by sonic314.consmr.mail.ir2.yahoo.com with HTTP; Wed, 10 Jun 2020 10:11:19 +0000
Date: Wed, 10 Jun 2020 10:11:19 +0000
From: MAILER-DAEMON@yahoo.com
To: alice@yahoo.com
Message-ID: <1713051795.39992.1591783879940@sonic314.consmr.mail.ir2.yahoo.com>
Subject: Failure Notice
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 7bit
Content-Length: 3347
Sorry, we were unable to deliver your message to the following address.
<haeclirth.sinoenrat@yahoo.com>:
554: delivery error: dd Not a valid recipient - atlas117.free.mail.ne1.yahoo.com
--- Below this line is a copy of the message.
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=yahoo.com; s=s2048; t=1591783876; bh=kXD6TZuQDjqZf/AEAJ2HCX32Titkj3IywytG6GSm4yQ=; h=Date:From:To:Subject:References:From:Subject; b=PexnIBxnVSkyutv/jVn+Wlt5QPuVHnWleP3baWlvqkXaKR51pCaZIoGJggMEonitEeJkcYpgplBawEBz0hlGf63NqOHHkxUk6U1olwc0y9kj5kDH5lrORgXrf7U4z5t+i5n0II36MxbG9n/5tDRXoiabLFoWx//3O9x/ZJvVWPlq8RBFzVG8aoL2TkBQAVcqX/vW1f3WJuopaUYWB4AzR3TyuC2kVQPFqbPMk+G/VmyuZFmZesh1bSBva5hdKYLxES5v8hvTIDRqSYZrnZ4V67MqicZ8m229Xf4Za8qOE+a2Z+Vv5VrQ+CjjPZwRAcmKHkLY80VCSkpeL2R2YG4APg==
X-YMail-OSG: 4spnw98VM1k_g51CM6oepdNEMiPFRtZ0ZG_zOBGIlhcNvS1mkzr8l8VTB0CY_Q0
dR52ikl7QVYESerRQgcGqBfLwKpem7i1XSrCl2HuyHeWzF6Gu5MqFPMCak.v8GyDXNO075NEwNt1
i18CJ29cEjiHthoamgmj0oqerAglglKRhTuuAFy4wUmZZm7VyvaW4wHUD1g7DeWGijQsCglSYMUK
CmoFcKsWOZBSYPMkp7iRwUp52pXHFin3qf4uQ27K_Sh.6s7KLAfWVkV7L_5AR3MyCPAVzm71.1yG
G7Vy5HSBgGMQ90B7VbcjOkCg3F4JNl4Z_P2ejV1KZ.tNoPLgO.FmsfFy1OXBGf3m2mDmRcuEO4K2
mTRhsjZf.2iiWpx02b3tY.oUtYrIBXBVIFPbTB9sBMn_9Z_qdVmO3gjD6gCPEBzuVvEO0eZIrgaw
EDTZt8Z9tSRDm1.4gV8LWQBYShF7XuMV0togiLYIO8s_iTHcTbhKhPlwxP.mxr06xcx_9kzReVTL
9lB1FkB5Jm0WccWHGhLBqeMjGDoaNqPxLqJ.1tI58tLXsPoR6m1NFVEdzI1G.4AVBeXZ_9BjgUhm
KY33sEg.GwIjUlWWWuSyRZ1q1K2nqi1z29wH2R1Glmdmx0lyqfMg9Xe8HV7YZu2CuZ8SlDLLB.rX
NU4PwMsNfU6pK2HejQPsJuyOlI5Q824rXRF5xTLYKsYcQptoFXyLe6MXKW1ThBLQV2nWYDRs_V.e
SBmt22TfuOwu4Y5ju0sXmztZ8zpiIC8_rnAa5bVBEHxzkic64UZdukDX9V12Pk3G2sGYRyPTH472
wBX33JpDuq6BtrKr4FXjCLeVppARTHpiKM0jHMjmNf1bF0TvrcCsC9zAYtitAqgcGZNFETNuV3KM
57XifdDEwUPOuww0ApSWO.iwP2POvIRBVlrxdgA8MbLmuuX4UxNCw23z1f7MVY6F3L60LUrX5GZO
aKaMmD1XTzx32J6c_TUmyuViT5vphqpEooTzHG2X7ALb4xC8yHlE4wDKyaEDARZ.8P2lO9T18oCz
OQvJjwDaLOkeAmo23yRMn70bYJK3tP9Z5cS1C0TE8PEtz4sd1syQUIZZ2g8JG_AQcE4lUZSZlIKN
AHjB8h8Uin35zKe0Le1DBjdQUmpgAETlmYE7V0nJDEmagB3dtpbokgRBuuBfhXlFpxHcnAmBFFFm
XOSLWEPnmxu2o8CCjjz3QUBy2fr3EI_D2VFpy..MuZgRwtES.l24m_95xtQxI28R4SWZN6LsS_rr
1S33BJCCCAfXtCAFzCfz5.qSzHRYbLdY5do6yKj0pPLQTUTjlMwmCUGPcSJhsyxkkEVIK1W_Z16R
ZRls-
Received: from sonic.gate.mail.ne1.yahoo.com by sonic314.consmr.mail.ir2.yahoo.com with HTTP; Wed, 10 Jun 2020 10:11:16 +0000
Date: Wed, 10 Jun 2020 10:11:12 +0000 (UTC)
From: Delta Chat Test <alice@yahoo.com>
To: "haeclirth.sinoenrat@yahoo.com" <haeclirth.sinoenrat@yahoo.com>
Message-ID: <1680295672.3657931.1591783872936@mail.yahoo.com>
Subject: test
MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary="----=_Part_3657930_145367320.1591783872935"
References: <1680295672.3657931.1591783872936.ref@mail.yahoo.com>
X-Mailer: WebService/1.1.16072 YMailNorrin Mozilla/5.0 (X11; Linux x86_64; rv:77.0) Gecko/20100101 Firefox/77.0
Content-Length: 494
------=_Part_3657930_145367320.1591783872935
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 7bit
test
------=_Part_3657930_145367320.1591783872935
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: 7bit
<html><head></head><body><div class="yahoo-style-wrap" style="font-family:Helvetica Neue, Helvetica, Arial, sans-serif;font-size:16px;"><div dir="ltr" data-setdir="false">test<br></div></div></body></html>
------=_Part_3657930_145367320.1591783872935--