Compare commits

...

45 Commits

Author SHA1 Message Date
link2xt
2f7b3c0a9a feat: replace leaky bucket with exponential rate limiting 2024-09-03 21:43:03 +00:00
link2xt
3969383857 Merge tag 'v1.142.12' 2024-09-02 23:58:26 +00:00
link2xt
e4ebb91712 chore(release): prepare for 1.142.12 2024-09-02 23:57:32 +00:00
iequidoo
eb3c1b3c25 fix: Display Config::MdnsEnabled as true by default (#5948) 2024-09-02 23:51:51 +00:00
iequidoo
c257482838 fix: Display Config::MdnsEnabled as true by default (#5948) 2024-09-02 20:50:15 -03:00
link2xt
0a46e64971 fix: use default server list for providers that don't have one
There are providers in the provider database
that do not have servers specified.
For such providers default list should be tried
just like when configuring unknown providers.
2024-09-02 22:57:31 +00:00
iequidoo
845420cf17 test: Alice is (non-)bot on Bob's side after QR contact setup 2024-09-02 18:06:52 -03:00
dependabot[bot]
96ea0db88e Merge pull request #5945 from deltachat/dependabot/cargo/libc-0.2.158 2024-09-01 23:21:12 +00:00
dependabot[bot]
d99c735e12 Merge pull request #5944 from deltachat/dependabot/cargo/async-imap-0.10.0 2024-09-01 22:40:49 +00:00
dependabot[bot]
d48f4100e9 Merge pull request #5943 from deltachat/dependabot/cargo/bytes-1.7.1 2024-09-01 22:39:57 +00:00
dependabot[bot]
7e73d5fdac chore(cargo): bump serde_json from 1.0.122 to 1.0.127
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.122 to 1.0.127.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.122...1.0.127)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-01 19:27:16 -03:00
dependabot[bot]
152cdfe9bc chore(cargo): bump syn from 2.0.72 to 2.0.77
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.72 to 2.0.77.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.72...2.0.77)

---
updated-dependencies:
- dependency-name: syn
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-01 19:16:07 -03:00
dependabot[bot]
a9eedafbcb chore(cargo): bump serde from 1.0.205 to 1.0.209
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.205 to 1.0.209.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.205...v1.0.209)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-01 19:10:15 -03:00
dependabot[bot]
5baf191483 chore(cargo): bump quote from 1.0.36 to 1.0.37
Bumps [quote](https://github.com/dtolnay/quote) from 1.0.36 to 1.0.37.
- [Release notes](https://github.com/dtolnay/quote/releases)
- [Commits](https://github.com/dtolnay/quote/compare/1.0.36...1.0.37)

---
updated-dependencies:
- dependency-name: quote
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-01 19:02:34 -03:00
dependabot[bot]
2d2e703884 chore(cargo): bump libc from 0.2.155 to 0.2.158
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.155 to 0.2.158.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.158/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.155...0.2.158)

---
updated-dependencies:
- dependency-name: libc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-01 21:09:15 +00:00
dependabot[bot]
026450ddf3 chore(cargo): bump async-imap from 0.9.7 to 0.10.0
Bumps [async-imap](https://github.com/async-email/async-imap) from 0.9.7 to 0.10.0.
- [Changelog](https://github.com/async-email/async-imap/blob/main/CHANGELOG.md)
- [Commits](https://github.com/async-email/async-imap/compare/v0.9.7...v0.10.0)

---
updated-dependencies:
- dependency-name: async-imap
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-01 21:09:01 +00:00
dependabot[bot]
5646782d23 chore(cargo): bump bytes from 1.5.0 to 1.7.1
Bumps [bytes](https://github.com/tokio-rs/bytes) from 1.5.0 to 1.7.1.
- [Release notes](https://github.com/tokio-rs/bytes/releases)
- [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/bytes/compare/v1.5.0...v1.7.1)

---
updated-dependencies:
- dependency-name: bytes
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-01 21:08:44 +00:00
link2xt
dd1c2e836b feat(securejoin): ignore invalid *-request-with-auth messages silently 2024-09-01 16:10:41 +00:00
link2xt
be73076e9e chore(cargo): replace unmaintained ansi_term with nu-ansi-term 2024-09-01 16:10:30 +00:00
link2xt
9d47be0d8a Merge tag 'v1.142.11' 2024-08-30 22:38:15 +00:00
link2xt
93e181b2da docs: document that bcc_self is enabled by default
bcc_self has been enabled by default
since core version 1.95.0
by merging
PR <https://github.com/deltachat/deltachat-core-rust/pull/3612>.

However deltachat.h documentation
still incorrectly said that bcc_self is disabled by default.
2024-08-30 19:21:01 +00:00
link2xt
3867808927 chore(cargo): reduce number of duplicate dependencies 2024-08-30 04:24:31 +00:00
link2xt
c7c3b9ca90 feat: replace reqwest with hyper
This change replaces
usage of `reqwest` and `hyper-util`
with custom connection establishment code
so it is done in the same way
as for IMAP and SMTP connections.
This way we control HTTP, IMAP and SMTP
connection establishment
and schedule connection attempts
to resolved IP addresses
in the same way for all 3 protocols.
2024-08-29 23:10:17 +00:00
link2xt
54cfc21e28 Remove old iroh 0.4 2024-08-29 20:59:41 +00:00
link2xt
f01514dba4 fix: start new connections independently of connection failures
With current implementation
every time connection fails
we take the next delay from `delays` iterator.
In the worst case first 4 DNS results
immediately refuse connection
and we start fifth connection attempt
with 1 year timeout,
effectively continuing all remaining
connection attempts without concurrency.

With new implementation
new connection attempts are
added to `connection_attempt_set`
independently of connection failures
and after 10 seconds
we always end up with five
parallel connection attempts
as long as there are enough IP addresses.
2024-08-29 13:24:28 +00:00
link2xt
ee5723416e chore(cargo): update iroh to 0.23.0 2024-08-28 23:15:38 +00:00
link2xt
aab8ef2726 feat: parallelize IMAP and SMTP connection attempts (#5915)
Previously for each connection candidate (essentially host and port
pair) after resolving the host to a list of IPs Delta Chat iterated IP
addresses one by one. Now for IMAP and SMTP we try up to 5 IP addresses
in parallel. We start with one connection and add more connections
later. If some connection fails, e.g. we try to connect to IPv6 on IPv4
network and get "Network is unreachable" (ENETUNREACH) error, we replace
failed connection with another one immediately.

Co-authored-by: Hocuri <hocuri@gmx.de>
2024-08-28 22:00:07 +00:00
link2xt
84c1ffd7cc fix: do not allow quotes with "... wrote:" headers in chat messages 2024-08-28 16:05:03 +00:00
link2xt
273158a337 fix: add Auto-Submitted header in a single place
This ensures we don't add multiple Auto-Submitted headers
when bots send vg-request or vc-request messages.

The change fixes failing
receive_imf::tests::test_bot_accepts_another_group_after_qr_scan
test.
2024-08-27 18:31:07 +00:00
link2xt
099f0e2d18 Merge tag 'v1.142.10' 2024-08-26 18:54:27 +00:00
iequidoo
af77c0c987 feat: Add "Auto-Submitted: auto-replied" header to appropriate SecureJoin messages
I.e. to all messages except "v{c,g}-request" as they sent out on a QR code scanning which is a
manual action and "vg-member-added" as formally this message is auto-submitted, but the member
addition is a result of an explicit user action. Otherwise it would be strange to have the
Auto-Submitted header in "member-added" messages of verified groups only.
2024-08-25 16:19:41 -03:00
link2xt
f912bc78e6 fix(http): set I/O timeout to 1 minute rather than whole request timeout
Before the fix HTTP client
had no connection timeout,
so it only had a chance
to test one IPv6 and one IPv4
address if the first addresses timed out.
Now it can test at least 4 addresses
of each family and more if some addresses
refuse connection rather than time out.
2024-08-25 17:06:34 +00:00
link2xt
137ee9334c feat: always use preloaded DNS results
Otherwise if DNS server returns incorrect results,
we may never try preloaded DNS results.
For example, we may get our first results
from a captive portal.

To test, add `127.0.0.1 example.org`
and try to create an account.
Without this change we only try 127.0.0.1 and fail.
With this change preloaded DNS results are tried as well.
2024-08-25 15:33:18 +00:00
link2xt
36e5e964e5 Merge tag 'v1.142.9' 2024-08-24 21:43:43 +00:00
link2xt
ef12a76a9e chore: update provider database 2024-08-23 13:24:07 +00:00
bjoern
6b3de9d7da recognize t.me proxy qr codes (#5895)
this PR adds the type DC_QR_SOCKS5_PROXY to `dc_check_qr()` for
**supporting telegram proxy QR codes**. if returned, the UI should ask
the user if they want to us the proxy and call
`dc_set_config_from_qr();` afterwards (plus maybe `dc_configure()`).

idea is to improve our proxy story, follow ups may be:

- in UI, - move proxy out of "Account & Password", as a **separate
"Proxy Activity"** (it should stay in "Advanced" for now, however, below
"Server", which might be moved up)

- allow **opening the "Proxy Activity" from the welcome screens**
three-dot-menu (that would also solve a long standing issue that
entering the email address bypasses the proxy

- show proxy usage in the "Connectivity View" and/or add an **icon** to
the main chatlist screen (beside three-dot menu) in case some proxy is
in use; tapping this icon will open the "Proxy Activity"

- the the new "Proxy Activity", add a **share / show proxy QR code**
button. that would generate invite links in the form
`https://i.delta.chat/socks#...` - so that tapping then opens the app.
support for these links need to be added to core then.

- handle a list of proxies in core, offer selection in UI. the list
could be one for all profiles and could be filled eg. by normal invite
links or other channels

---------

Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com>
2024-08-23 09:49:49 +02:00
link2xt
3599e4be16 fix: save custom username if user entered it 2024-08-23 05:44:28 +00:00
link2xt
8dc844e194 refactor(login_param): use Config:: constants to avoid typos in key names 2024-08-22 00:44:06 +00:00
link2xt
104c60840a test: test that alternative port 443 works 2024-08-22 00:14:57 +00:00
link2xt
f2cb098148 fix: make SMTP server list readable in context info 2024-08-21 13:04:37 +00:00
link2xt
30b998eca3 Merge tag 'v1.142.8' 2024-08-21 12:48:17 +00:00
link2xt
b5133fe8c8 fix: fix loading imap_certificate_checks
Fix a typo in the config name
(by using `Config::` to avoid it)
and make sure we don't panic on unknown values.

Also test that we don't panic on unknown
`configured_imap_certificate_checks` values.
2024-08-21 12:46:12 +00:00
link2xt
08ec133aac refactor: use get_configured_provider() in ConfiguredLoginParam::load() 2024-08-21 07:24:15 +00:00
link2xt
7d7391887a fix: do not ignore legacy configured_{mail,send}_user for known providers 2024-08-21 07:24:15 +00:00
link2xt
e7d4ccffe2 feat: automatic reconfiguration 2024-08-19 16:36:56 +00:00
52 changed files with 2575 additions and 2651 deletions

View File

@@ -1,5 +1,11 @@
# Changelog
## [1.142.12] - 2024-09-02
### Fixes
- Display Config::MdnsEnabled as true by default ([#5948](https://github.com/deltachat/deltachat-core-rust/pull/5948)).
## [1.142.11] - 2024-08-30
### Fixes
@@ -4799,3 +4805,4 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.142.9]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.8...v1.142.9
[1.142.10]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.9..v1.142.10
[1.142.11]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.10..v1.142.11
[1.142.12]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.11..v1.142.12

1307
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.142.11"
version = "1.142.12"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.77"
@@ -41,12 +41,13 @@ ratelimit = { path = "./deltachat-ratelimit" }
anyhow = { workspace = true }
async-broadcast = "0.7.1"
async-channel = { workspace = true }
async-imap = { version = "0.9.7", default-features = false, features = ["runtime-tokio"] }
async-imap = { version = "0.10.0", default-features = false, features = ["runtime-tokio"] }
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
base64 = { workspace = true }
brotli = { version = "6", default-features=false, features = ["std"] }
bytes = "1"
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
@@ -57,11 +58,13 @@ futures = { workspace = true }
futures-lite = { workspace = true }
hex = "0.4.0"
hickory-resolver = "0.24"
http-body-util = "0.1.2"
humansize = "2"
hyper = "1"
hyper-util = "0.1.7"
image = { version = "0.25.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh_old = { version = "0.4.2", default-features = false, package = "iroh"}
iroh-net = { version = "0.22.0", default-features = false }
iroh-gossip = { version = "0.22.0", default-features = false, features = ["net"] }
iroh-net = { version = "0.23.0", default-features = false }
iroh-gossip = { version = "0.23.0", default-features = false, features = ["net"] }
kamadak-exif = "0.5.3"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = { workspace = true }
@@ -79,11 +82,11 @@ quick-xml = "0.36"
quoted_printable = "0.5"
rand = { workspace = true }
regex = { workspace = true }
reqwest = { version = "0.12.5", features = ["json"] }
rusqlite = { workspace = true, features = ["sqlcipher"] }
rust-hsluv = "0.1"
sanitize-filename = { workspace = true }
serde_json = { workspace = true }
serde_urlencoded = "0.7.1"
serde = { workspace = true, features = ["derive"] }
sha-1 = "0.10"
smallvec = "1.13.2"
@@ -102,11 +105,11 @@ url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
[dev-dependencies]
ansi_term = { workspace = true }
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
criterion = { version = "0.5.1", features = ["async_tokio"] }
futures-lite = { workspace = true }
log = { workspace = true }
nu-ansi-term = { workspace = true }
proptest = { version = "1", default-features = false, features = ["std"] }
tempfile = { workspace = true }
testdir = "0.9.0"
@@ -156,7 +159,6 @@ harness = false
[workspace.dependencies]
anyhow = "1"
ansi_term = "0.12.1"
async-channel = "2.3.1"
base64 = "0.22"
chrono = { version = "0.4.38", default-features = false }
@@ -167,6 +169,7 @@ futures = "0.3.30"
futures-lite = "2.3.0"
libc = "0.2"
log = "0.4"
nu-ansi-term = "0.46"
num-traits = "0.2"
once_cell = "1.18.0"
rand = "0.8"
@@ -194,8 +197,7 @@ default = ["vendored"]
internals = []
vendored = [
"async-native-tls/vendored",
"rusqlite/bundled-sqlcipher-vendored-openssl",
"reqwest/native-tls-vendored"
"rusqlite/bundled-sqlcipher-vendored-openssl"
]
[lints.rust]

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.142.11"
version = "1.142.12"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"

View File

@@ -422,8 +422,8 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `mdns_enabled` = 0=do not send or request read receipts,
* 1=send and request read receipts
* default=send and request read receipts, only send but not reuqest if `bot` is set
* - `bcc_self` = 0=do not send a copy of outgoing messages to self (default),
* 1=send a copy of outgoing messages to self.
* - `bcc_self` = 0=do not send a copy of outgoing messages to self,
* 1=send a copy of outgoing messages to self (default).
* 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.
* - `sentbox_watch`= 1=watch `Sent`-folder for changes,
@@ -2507,6 +2507,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
#define DC_QR_BACKUP 251
#define DC_QR_BACKUP2 252
#define DC_QR_WEBRTC_INSTANCE 260 // text1=domain, text2=instance pattern
#define DC_QR_SOCKS5_PROXY 270 // text1=host, text2=port
#define DC_QR_ADDR 320 // id=contact
#define DC_QR_TEXT 330 // text1=text
#define DC_QR_URL 332 // text1=URL
@@ -2560,6 +2561,10 @@ void dc_stop_ongoing_process (dc_context_t* context);
* ask the user if they want to use the given service for video chats;
* if so, call dc_set_config_from_qr().
*
* - DC_QR_SOCKS5_PROXY with dc_lot_t::text1=host, dc_lot_t::text2=port:
* ask the user if they want to use the given proxy and overwrite the previous one, if any.
* if so, call dc_set_config_from_qr() and restart I/O.
*
* - DC_QR_ADDR with dc_lot_t::id=Contact ID:
* e-mail address scanned, optionally, a draft message could be set in
* dc_lot_t::text1 in which case dc_lot_t::text1_meaning will be DC_TEXT1_DRAFT;

View File

@@ -49,9 +49,9 @@ impl Lot {
Qr::FprMismatch { .. } => None,
Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint),
Qr::Account { domain } => Some(domain),
Qr::Backup { .. } => None,
Qr::Backup2 { .. } => None,
Qr::WebrtcInstance { domain, .. } => Some(domain),
Qr::Socks5Proxy { host, .. } => Some(host),
Qr::Addr { draft, .. } => draft.as_deref(),
Qr::Url { url } => Some(url),
Qr::Text { text } => Some(text),
@@ -68,7 +68,10 @@ impl Lot {
pub fn get_text2(&self) -> Option<Cow<str>> {
match self {
Self::Summary(summary) => Some(summary.truncated_text(160)),
Self::Qr(_) => None,
Self::Qr(qr) => match qr {
Qr::Socks5Proxy { port, .. } => Some(Cow::Owned(format!("{port}"))),
_ => None,
},
Self::Error(_) => None,
}
}
@@ -102,9 +105,9 @@ impl Lot {
Qr::FprMismatch { .. } => LotState::QrFprMismatch,
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
Qr::Account { .. } => LotState::QrAccount,
Qr::Backup { .. } => LotState::QrBackup,
Qr::Backup2 { .. } => LotState::QrBackup2,
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
Qr::Socks5Proxy { .. } => LotState::QrSocks5Proxy,
Qr::Addr { .. } => LotState::QrAddr,
Qr::Url { .. } => LotState::QrUrl,
Qr::Text { .. } => LotState::QrText,
@@ -128,9 +131,9 @@ impl Lot {
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(),
Qr::FprWithoutAddr { .. } => Default::default(),
Qr::Account { .. } => Default::default(),
Qr::Backup { .. } => Default::default(),
Qr::Backup2 { .. } => Default::default(),
Qr::WebrtcInstance { .. } => Default::default(),
Qr::Socks5Proxy { .. } => Default::default(),
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
Qr::Url { .. } => Default::default(),
Qr::Text { .. } => Default::default(),
@@ -185,6 +188,9 @@ pub enum LotState {
/// text1=domain, text2=instance pattern
QrWebrtcInstance = 260,
/// text1=host, text2=port
QrSocks5Proxy = 270,
/// id=contact
QrAddr = 320,

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.142.11"
version = "1.142.12"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"

View File

@@ -32,9 +32,6 @@ pub enum QrObject {
Account {
domain: String,
},
Backup {
ticket: String,
},
Backup2 {
auth_token: String,
@@ -44,6 +41,12 @@ pub enum QrObject {
domain: String,
instance_pattern: String,
},
Socks5Proxy {
host: String,
port: u16,
user: Option<String>,
pass: Option<String>,
},
Addr {
contact_id: u32,
draft: Option<String>,
@@ -134,9 +137,6 @@ impl From<Qr> for QrObject {
}
Qr::FprWithoutAddr { fingerprint } => QrObject::FprWithoutAddr { fingerprint },
Qr::Account { domain } => QrObject::Account { domain },
Qr::Backup { ticket } => QrObject::Backup {
ticket: ticket.to_string(),
},
Qr::Backup2 {
ref node_addr,
auth_token,
@@ -152,6 +152,17 @@ impl From<Qr> for QrObject {
domain,
instance_pattern,
},
Qr::Socks5Proxy {
host,
port,
user,
pass,
} => QrObject::Socks5Proxy {
host,
port,
user,
pass,
},
Qr::Addr { contact_id, draft } => {
let contact_id = contact_id.to_u32();
QrObject::Addr { contact_id, draft }

View File

@@ -58,5 +58,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.142.11"
"version": "1.142.12"
}

View File

@@ -1,57 +1,43 @@
//! # Rate limiting module.
//!
//! This module contains implementation of token bucket policy.
//! This module contains implementation
//! of [exponential rate limiting](https://dotat.at/@/2024-09-02-ewma.html).
//! Implementation is simplified to only use one variable (`next_time`) to store the state.
//! Its primary use is preventing Delta Chat from sending too many messages, especially automatic,
//! such as read receipts.
use std::time::{Duration, SystemTime};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[derive(Debug)]
pub struct Ratelimit {
/// Time of the last update.
last_update: SystemTime,
/// Number of messages sent within the time window ending at `last_update`.
current_value: f64,
/// Next time we are allowed to send, i.e. when measured rate (number of messages sent within
/// the window) drops down to the limit `quota`.
///
/// Measured in seconds since unix epoch.
next_time: SystemTime,
/// Time window size.
window: Duration,
window: f64,
/// Number of messages allowed to send within the time window.
quota: f64,
limit: f64,
}
impl Ratelimit {
/// Returns a new rate limiter with the given constraints.
///
/// Rate limiter will allow to send no more than `quota` messages within duration `window`.
pub fn new(window: Duration, quota: f64) -> Self {
Self::new_at(window, quota, SystemTime::now())
}
/// Returns a new rate limiter with given current time for testing purposes.
fn new_at(window: Duration, quota: f64, now: SystemTime) -> Self {
/// Rate limiter will allow to send no more than `limit` messages within duration `window`.
pub fn new(window: Duration, limit: f64) -> Self {
Self {
last_update: now,
current_value: 0.0,
window,
quota,
next_time: UNIX_EPOCH,
window: window.as_secs_f64(),
limit,
}
}
/// Returns current number of sent messages.
fn current_value_at(&self, now: SystemTime) -> f64 {
let rate: f64 = self.quota / self.window.as_secs_f64();
let elapsed = now
.duration_since(self.last_update)
.unwrap_or(Duration::ZERO)
.as_secs_f64();
f64::max(0.0, self.current_value - rate * elapsed)
}
/// Returns true if it is allowed to send a message.
fn can_send_at(&self, now: SystemTime) -> bool {
self.current_value_at(now) + 1.0 <= self.quota
now >= self.next_time
}
/// Returns true if can send another message now.
@@ -62,8 +48,20 @@ impl Ratelimit {
}
fn send_at(&mut self, now: SystemTime) {
self.current_value = f64::min(self.quota, self.current_value_at(now) + 1.0);
self.last_update = now;
let now = now
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_secs_f64();
let next_time = self
.next_time
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_secs_f64();
self.next_time = UNIX_EPOCH
+ Duration::from_secs_f64(
now + self.window
* (((next_time - now) / self.window).exp() + 1.0 / self.limit).ln(),
);
}
/// Increases current usage value.
@@ -76,14 +74,7 @@ impl Ratelimit {
}
fn until_can_send_at(&self, now: SystemTime) -> Duration {
let current_value = self.current_value_at(now);
if current_value + 1.0 <= self.quota {
Duration::ZERO
} else {
let requirement = current_value + 1.0 - self.quota;
let rate = self.quota / self.window.as_secs_f64();
Duration::from_secs_f64(requirement / rate)
}
self.next_time.duration_since(now).unwrap_or(Duration::ZERO)
}
/// Calculates the time until `can_send` will return `true`.
@@ -100,7 +91,7 @@ mod tests {
fn test_ratelimit() {
let now = SystemTime::now();
let mut ratelimit = Ratelimit::new_at(Duration::new(60, 0), 3.0, now);
let mut ratelimit = Ratelimit::new(Duration::new(60, 0), 3.0);
assert!(ratelimit.can_send_at(now));
// Send burst of 3 messages.
@@ -109,6 +100,8 @@ mod tests {
ratelimit.send_at(now);
assert!(ratelimit.can_send_at(now));
ratelimit.send_at(now);
assert!(ratelimit.can_send_at(now));
ratelimit.send_at(now);
// Can't send more messages now.
assert!(!ratelimit.can_send_at(now));

View File

@@ -1,16 +1,16 @@
[package]
name = "deltachat-repl"
version = "1.142.11"
version = "1.142.12"
license = "MPL-2.0"
edition = "2021"
repository = "https://github.com/deltachat/deltachat-core-rust"
[dependencies]
ansi_term = { workspace = true }
anyhow = { workspace = true }
deltachat = { workspace = true, features = ["internals"]}
dirs = "5"
log = { workspace = true }
nu-ansi-term = { workspace = true }
rusqlite = { workspace = true }
rustyline = "14"
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }

View File

@@ -12,7 +12,6 @@ use std::borrow::Cow::{self, Borrowed, Owned};
use std::io::{self, Write};
use std::process::Command;
use ansi_term::Color;
use anyhow::{bail, Error};
use deltachat::chat::ChatId;
use deltachat::config;
@@ -22,6 +21,7 @@ use deltachat::qr_code_generator::get_securejoin_qr_svg;
use deltachat::securejoin::*;
use deltachat::EventType;
use log::{error, info, warn};
use nu_ansi_term::Color;
use rustyline::completion::{Completer, FilenameCompleter, Pair};
use rustyline::error::ReadlineError;
use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "1.142.11"
version = "1.142.12"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",

View File

@@ -9,18 +9,19 @@ import io
import pathlib
import ssl
from contextlib import contextmanager
from typing import TYPE_CHECKING
from imap_tools import (
AND,
Header,
MailBox,
MailBoxTls,
MailMessage,
MailMessageFlags,
errors,
)
from . import Account, const
if TYPE_CHECKING:
from . import Account
FLAGS = b"FLAGS"
FETCH = b"FETCH"
@@ -35,28 +36,15 @@ class DirectImap:
self.connect()
def connect(self):
# Assume the testing server supports TLS on port 993.
host = self.account.get_config("configured_mail_server")
port = int(self.account.get_config("configured_mail_port"))
security = int(self.account.get_config("configured_mail_security"))
port = 993
user = self.account.get_config("addr")
host = user.rsplit("@")[-1]
pw = self.account.get_config("mail_pw")
if security == const.SocketSecurity.PLAIN:
ssl_context = None
else:
ssl_context = ssl.create_default_context()
# don't check if certificate hostname doesn't match target hostname
ssl_context.check_hostname = False
# don't check if the certificate is trusted by a certificate authority
ssl_context.verify_mode = ssl.CERT_NONE
if security == const.SocketSecurity.STARTTLS:
self.conn = MailBoxTls(host, port, ssl_context=ssl_context)
elif security == const.SocketSecurity.PLAIN or security == const.SocketSecurity.SSL:
self.conn = MailBox(host, port, ssl_context=ssl_context)
self.conn = MailBox(host, port, ssl_context=ssl.create_default_context())
self.conn.login(user, pw)
self.select_folder("INBOX")

View File

@@ -650,7 +650,8 @@ def test_withdraw_securejoin_qr(acfactory):
logging.info("Bob scanned withdrawn QR code")
while True:
event = alice.wait_for_event()
if event.kind == EventType.MSGS_CHANGED and event.chat_id != 0:
if (
event.kind == EventType.WARNING
and "Ignoring vg-request-with-auth message because of invalid auth code." in event.msg
):
break
snapshot = alice.get_message_by_id(event.msg_id).get_snapshot()
assert snapshot.text == "Cannot establish guaranteed end-to-end encryption with {}".format(bob.get_config("addr"))

View File

@@ -83,6 +83,26 @@ def test_configure_ip(acfactory) -> None:
account.configure()
def test_configure_alternative_port(acfactory) -> None:
"""Test that configuration with alternative port 443 works."""
account = acfactory.new_preconfigured_account()
account.set_config("mail_port", "443")
account.set_config("send_port", "443")
account.configure()
def test_configure_username(acfactory) -> None:
account = acfactory.new_preconfigured_account()
addr = account.get_config("addr")
account.set_config("mail_user", addr)
account.configure()
assert account.get_config("configured_mail_user") == addr
def test_account(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.142.11"
version = "1.142.12"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"

View File

@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "1.142.11"
"version": "1.142.12"
}

View File

@@ -1,7 +1,6 @@
[advisories]
ignore = [
"RUSTSEC-2020-0071",
"RUSTSEC-2022-0093",
# Timing attack on RSA.
# Delta Chat does not use RSA for new keys
@@ -10,15 +9,8 @@ ignore = [
# <https://rustsec.org/advisories/RUSTSEC-2023-0071>
"RUSTSEC-2023-0071",
# Unmaintained ansi_term
"RUSTSEC-2021-0139",
# Unmaintained encoding
"RUSTSEC-2021-0153",
# Problem in curve25519-dalek 3.2.0 used by iroh 0.4.
# curve25519-dalek 4.1.3 has the problem fixed.
"RUSTSEC-2024-0344",
]
[bans]
@@ -31,24 +23,13 @@ skip = [
{ name = "asn1-rs-impl", version = "0.1.0" },
{ name = "asn1-rs", version = "0.5.2" },
{ name = "async-channel", version = "1.9.0" },
{ name = "base16ct", version = "0.1.1" },
{ name = "base64", version = "<0.21" },
{ name = "base64", version = "0.21.7" },
{ name = "bitflags", version = "1.3.2" },
{ name = "block-buffer", version = "<0.10" },
{ name = "convert_case", version = "0.4.0" },
{ name = "curve25519-dalek", version = "3.2.0" },
{ name = "darling_core", version = "<0.14" },
{ name = "darling_macro", version = "<0.14" },
{ name = "darling", version = "<0.14" },
{ name = "der_derive", version = "0.6.1" },
{ name = "derive_more", version = "0.99.17" },
{ name = "der-parser", version = "8.2.0" },
{ name = "der", version = "0.6.1" },
{ name = "digest", version = "<0.10" },
{ name = "dlopen2", version = "0.4.1" },
{ name = "ed25519-dalek", version = "1.0.1" },
{ name = "ed25519", version = "1.5.3" },
{ name = "event-listener", version = "2.5.3" },
{ name = "event-listener", version = "4.0.3" },
{ name = "fastrand", version = "1.9.0" },
@@ -59,42 +40,25 @@ skip = [
{ name = "http", version = "0.2.12" },
{ name = "hyper", version = "0.14.28" },
{ name = "idna", version = "0.4.0" },
{ name = "netlink-packet-core", version = "0.5.0" },
{ name = "netlink-packet-route", version = "0.15.0" },
{ name = "nix", version = "0.26.4" },
{ name = "oid-registry", version = "0.6.1" },
{ name = "pem-rfc7468", version = "0.6.0" },
{ name = "pem", version = "1.1.1" },
{ name = "pkcs8", version = "0.9.0" },
{ name = "quick-error", version = "<2.0" },
{ name = "rand_chacha", version = "<0.3" },
{ name = "rand_core", version = "<0.6" },
{ name = "rand", version = "<0.8" },
{ name = "rcgen", version = "<0.12.1" },
{ name = "redox_syscall", version = "0.3.5" },
{ name = "regex-automata", version = "0.1.10" },
{ name = "regex-syntax", version = "0.6.29" },
{ name = "ring", version = "0.16.20" },
{ name = "rustls-pemfile", version = "1.0.4" },
{ name = "rustls", version = "0.21.11" },
{ name = "rustls-webpki", version = "0.101.7" },
{ name = "sec1", version = "0.3.0" },
{ name = "sha2", version = "<0.10" },
{ name = "signature", version = "1.6.4" },
{ name = "spin", version = "<0.9.6" },
{ name = "spki", version = "0.6.0" },
{ name = "ssh-encoding", version = "0.1.0" },
{ name = "ssh-key", version = "0.5.1" },
{ name = "strsim", version = "0.10.0" },
{ name = "sync_wrapper", version = "0.1.2" },
{ name = "synstructure", version = "0.12.6" },
{ name = "syn", version = "1.0.109" },
{ name = "system-configuration-sys", version = "0.5.0" },
{ name = "system-configuration", version = "0.5.1" },
{ name = "time", version = "<0.3" },
{ name = "tokio-rustls", version = "0.24.1" },
{ name = "toml_edit", version = "0.21.1" },
{ name = "untrusted", version = "0.7.1" },
{ name = "wasi", version = "<0.11" },
{ name = "webpki-roots", version ="0.25.4" },
{ name = "windows_aarch64_gnullvm", version = "<0.52" },
@@ -104,12 +68,10 @@ skip = [
{ name = "windows_i686_msvc", version = "<0.52" },
{ name = "windows-sys", version = "<0.52" },
{ name = "windows-targets", version = "<0.52" },
{ name = "windows", version = "0.32.0" },
{ name = "windows", version = "<0.54.0" },
{ name = "windows_x86_64_gnullvm", version = "<0.52" },
{ name = "windows_x86_64_gnu", version = "<0.52" },
{ name = "windows_x86_64_msvc", version = "<0.52" },
{ name = "winnow", version = "0.5.40" },
{ name = "winreg", version = "0.50.0" },
{ name = "x509-parser", version = "<0.16.0" },
]

View File

@@ -136,6 +136,7 @@ module.exports = {
DC_QR_LOGIN: 520,
DC_QR_REVIVE_VERIFYCONTACT: 510,
DC_QR_REVIVE_VERIFYGROUP: 512,
DC_QR_SOCKS5_PROXY: 270,
DC_QR_TEXT: 330,
DC_QR_URL: 332,
DC_QR_WEBRTC_INSTANCE: 260,

View File

@@ -136,6 +136,7 @@ export enum C {
DC_QR_LOGIN = 520,
DC_QR_REVIVE_VERIFYCONTACT = 510,
DC_QR_REVIVE_VERIFYGROUP = 512,
DC_QR_SOCKS5_PROXY = 270,
DC_QR_TEXT = 330,
DC_QR_URL = 332,
DC_QR_WEBRTC_INSTANCE = 260,

View File

@@ -55,5 +55,5 @@
"test:mocha": "mocha node/test/test.mjs --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.142.11"
"version": "1.142.12"
}

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "1.142.11"
version = "1.142.12"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"
requires-python = ">=3.7"

View File

@@ -8,19 +8,19 @@ import io
import pathlib
import ssl
from contextlib import contextmanager
from typing import List
from typing import List, TYPE_CHECKING
from imap_tools import (
AND,
Header,
MailBox,
MailBoxTls,
MailMessage,
MailMessageFlags,
errors,
)
from deltachat import Account, const
if TYPE_CHECKING:
from deltachat import Account
FLAGS = b"FLAGS"
FETCH = b"FETCH"
@@ -28,7 +28,7 @@ ALL = "1:*"
class DirectImap:
def __init__(self, account: Account) -> None:
def __init__(self, account: "Account") -> None:
self.account = account
self.logid = account.get_config("displayname") or id(account)
self._idling = False
@@ -36,27 +36,13 @@ class DirectImap:
def connect(self):
host = self.account.get_config("configured_mail_server")
port = int(self.account.get_config("configured_mail_port"))
security = int(self.account.get_config("configured_mail_security"))
port = 993
user = self.account.get_config("addr")
host = user.rsplit("@")[-1]
pw = self.account.get_config("mail_pw")
if security == const.DC_SOCKET_PLAIN:
ssl_context = None
else:
ssl_context = ssl.create_default_context()
# don't check if certificate hostname doesn't match target hostname
ssl_context.check_hostname = False
# don't check if the certificate is trusted by a certificate authority
ssl_context.verify_mode = ssl.CERT_NONE
if security == const.DC_SOCKET_STARTTLS:
self.conn = MailBoxTls(host, port, ssl_context=ssl_context)
elif security == const.DC_SOCKET_PLAIN or security == const.DC_SOCKET_SSL:
self.conn = MailBox(host, port, ssl_context=ssl_context)
self.conn = MailBox(host, port, ssl_context=ssl.create_default_context())
self.conn.login(user, pw)
self.select_folder("INBOX")

View File

@@ -1 +1 @@
2024-08-30
2024-09-02

View File

@@ -6,7 +6,7 @@ set -euo pipefail
export TZ=UTC
# Provider database revision.
REV=05c1b2029da74718e4bdc3799a46e29c4f794dc7
REV=ab970e40d3979893c3bb6a93030e1a52223d7db6
CORE_ROOT="$PWD"
TMP="$(mktemp -d)"

View File

@@ -129,6 +129,7 @@ pub enum Config {
/// True if Message Delivery Notifications (read receipts) should
/// be sent and requested.
#[strum(props(default = "1"))]
MdnsEnabled,
/// True if "Sent" folder should be watched for changes.
@@ -199,21 +200,32 @@ pub enum Config {
/// The primary email address. Also see `SecondaryAddrs`.
ConfiguredAddr,
/// List of configured IMAP servers as a JSON array.
ConfiguredImapServers,
/// Configured IMAP server hostname.
///
/// This is replaced by `configured_imap_servers` for new configurations.
ConfiguredMailServer,
/// Configured IMAP server port.
///
/// This is replaced by `configured_imap_servers` for new configurations.
ConfiguredMailPort,
/// Configured IMAP server security (e.g. TLS, STARTTLS).
///
/// This is replaced by `configured_imap_servers` for new configurations.
ConfiguredMailSecurity,
/// Configured IMAP server username.
///
/// This is set if user has configured username manually.
ConfiguredMailUser,
/// Configured IMAP server password.
ConfiguredMailPw,
/// Configured IMAP server port.
ConfiguredMailPort,
/// Configured IMAP server security (e.g. TLS, STARTTLS).
ConfiguredMailSecurity,
/// Configured TLS certificate checks.
/// This option is saved on successful configuration
/// and should not be modified manually.
@@ -222,18 +234,32 @@ pub enum Config {
/// but has "IMAP" in the name for backwards compatibility.
ConfiguredImapCertificateChecks,
/// List of configured SMTP servers as a JSON array.
ConfiguredSmtpServers,
/// Configured SMTP server hostname.
///
/// This is replaced by `configured_smtp_servers` for new configurations.
ConfiguredSendServer,
/// Configured SMTP server port.
///
/// This is replaced by `configured_smtp_servers` for new configurations.
ConfiguredSendPort,
/// Configured SMTP server security (e.g. TLS, STARTTLS).
///
/// This is replaced by `configured_smtp_servers` for new configurations.
ConfiguredSendSecurity,
/// Configured SMTP server username.
///
/// This is set if user has configured username manually.
ConfiguredSendUser,
/// Configured SMTP server password.
ConfiguredSendPw,
/// Configured SMTP server port.
ConfiguredSendPort,
/// Deprecated, stored for backwards compatibility.
///
/// ConfiguredImapCertificateChecks is actually used.
@@ -242,9 +268,6 @@ pub enum Config {
/// Whether OAuth 2 is used with configured provider.
ConfiguredServerFlags,
/// Configured SMTP server security (e.g. TLS, STARTTLS).
ConfiguredSendSecurity,
/// Configured folder for incoming messages.
ConfiguredInboxFolder,
@@ -516,18 +539,15 @@ impl Context {
/// Returns whether MDNs should be requested.
pub(crate) async fn should_request_mdns(&self) -> Result<bool> {
match self.get_config_bool_opt(Config::MdnsEnabled).await? {
Some(val) => Ok(val),
None => Ok(!self.get_config_bool(Config::Bot).await?),
match self.config_exists(Config::MdnsEnabled).await? {
true => self.get_config_bool(Config::MdnsEnabled).await,
false => Ok(!self.get_config_bool(Config::Bot).await?),
}
}
/// Returns whether MDNs should be sent.
pub(crate) async fn should_send_mdns(&self) -> Result<bool> {
Ok(self
.get_config_bool_opt(Config::MdnsEnabled)
.await?
.unwrap_or(true))
self.get_config_bool(Config::MdnsEnabled).await
}
/// Gets configured "delete_server_after" value.
@@ -984,9 +1004,13 @@ mod tests {
let t = &TestContext::new_alice().await;
assert!(t.should_request_mdns().await?);
assert!(t.should_send_mdns().await?);
// The setting should be displayed correctly.
assert!(t.get_config_bool(Config::MdnsEnabled).await?);
t.set_config_bool(Config::Bot, true).await?;
assert!(!t.should_request_mdns().await?);
assert!(t.should_send_mdns().await?);
assert!(t.get_config_bool(Config::MdnsEnabled).await?);
Ok(())
}

View File

@@ -11,7 +11,7 @@
mod auto_mozilla;
mod auto_outlook;
mod server_params;
pub(crate) mod server_params;
use anyhow::{bail, ensure, Context as _, Result};
use auto_mozilla::moz_autoconfigure;
@@ -25,14 +25,16 @@ use tokio::task;
use crate::config::{self, Config};
use crate::context::Context;
use crate::imap::{session::Session as ImapSession, Imap};
use crate::imap::Imap;
use crate::log::LogExt;
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam};
use crate::login_param::{
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
ConnectionCandidate, EnteredCertificateChecks, EnteredLoginParam,
};
use crate::message::{Message, Viewtype};
use crate::oauth2::get_oauth2_addr;
use crate::provider::{Protocol, Socket, UsernamePattern};
use crate::smtp::Smtp;
use crate::socks::Socks5Config;
use crate::stock_str;
use crate::sync::Sync::*;
use crate::tools::time;
@@ -110,16 +112,15 @@ impl Context {
async fn inner_configure(&self) -> Result<()> {
info!(self, "Configure ...");
let mut param = LoginParam::load_candidate_params(self).await?;
let param = EnteredLoginParam::load(self).await?;
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
let success = configure(self, &mut param).await;
let configured_param_res = configure(self, &param).await;
self.set_config_internal(Config::NotifyAboutWrongPw, None)
.await?;
on_configure_completed(self, param, old_addr).await?;
on_configure_completed(self, configured_param_res?, old_addr).await?;
success?;
self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
.await?;
Ok(())
@@ -128,7 +129,7 @@ impl Context {
async fn on_configure_completed(
context: &Context,
param: LoginParam,
param: ConfiguredLoginParam,
old_addr: Option<String>,
) -> Result<()> {
if let Some(provider) = param.provider {
@@ -178,19 +179,28 @@ async fn on_configure_completed(
Ok(())
}
async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
progress!(ctx, 1);
/// Retrieves data from autoconfig and provider database
/// to transform user-entered login parameters into complete configuration.
async fn get_configured_param(
ctx: &Context,
param: &EnteredLoginParam,
) -> Result<ConfiguredLoginParam> {
ensure!(!param.addr.is_empty(), "Missing email address.");
ensure!(!param.imap.password.is_empty(), "Missing (IMAP) password.");
// SMTP password is an "advanced" setting. If unset, use the same password as for IMAP.
let smtp_password = if param.smtp.password.is_empty() {
param.imap.password.clone()
} else {
param.smtp.password.clone()
};
let socks5_config = param.socks5_config.clone();
let socks5_enabled = socks5_config.is_some();
let ctx2 = ctx.clone();
let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await });
// Step 1: Load the parameters and check email-address and password
// OAuth is always set either for both IMAP and SMTP or not at all.
if param.imap.oauth2 {
let mut addr = param.addr.clone();
if param.oauth2 {
// the used oauth2 addr may differ, check this.
// if get_oauth2_addr() is not available in the oauth2 implementation, just use the given one.
progress!(ctx, 10);
@@ -199,7 +209,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
.and_then(|e| e.parse().ok())
{
info!(ctx, "Authorized address is {}", oauth2_addr);
param.addr = oauth2_addr;
addr = oauth2_addr;
ctx.sql
.set_raw_config("addr", Some(param.addr.as_str()))
.await?;
@@ -211,9 +221,9 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
let parsed = EmailAddress::new(&param.addr).context("Bad email-address")?;
let param_domain = parsed.domain;
// Step 2: Autoconfig
progress!(ctx, 200);
let provider;
let param_autoconfig;
if param.imap.server.is_empty()
&& param.imap.port == 0
@@ -225,77 +235,51 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
&& param.smtp.user.is_empty()
{
// no advanced parameters entered by the user: query provider-database or do Autoconfig
info!(
ctx,
"checking internal provider-info for offline autoconfig"
);
if let Some(provider) =
provider::get_provider_info(ctx, &param_domain, socks5_enabled).await
{
param.provider = Some(provider);
match provider.status {
provider::Status::Ok | provider::Status::Preparation => {
if provider.server.is_empty() {
info!(ctx, "offline autoconfig found, but no servers defined");
param_autoconfig = None;
} else {
info!(ctx, "offline autoconfig found");
let servers = provider
.server
.iter()
.map(|s| ServerParams {
protocol: s.protocol,
socket: s.socket,
hostname: s.hostname.to_string(),
port: s.port,
username: match s.username_pattern {
UsernamePattern::Email => param.addr.to_string(),
UsernamePattern::Emaillocalpart => {
if let Some(at) = param.addr.find('@') {
param.addr.split_at(at).0.to_string()
} else {
param.addr.to_string()
}
}
},
})
.collect();
provider = provider::get_provider_info(ctx, &param_domain, socks5_enabled).await;
if let Some(provider) = provider {
if provider.server.is_empty() {
info!(ctx, "Offline autoconfig found, but no servers defined.");
param_autoconfig = None;
} else {
info!(ctx, "Offline autoconfig found.");
let servers = provider
.server
.iter()
.map(|s| ServerParams {
protocol: s.protocol,
socket: s.socket,
hostname: s.hostname.to_string(),
port: s.port,
username: match s.username_pattern {
UsernamePattern::Email => param.addr.to_string(),
UsernamePattern::Emaillocalpart => {
if let Some(at) = param.addr.find('@') {
param.addr.split_at(at).0.to_string()
} else {
param.addr.to_string()
}
}
},
})
.collect();
param_autoconfig = Some(servers)
}
}
provider::Status::Broken => {
info!(ctx, "offline autoconfig found, provider is broken");
param_autoconfig = None;
}
param_autoconfig = Some(servers)
}
} else {
// Try receiving autoconfig
info!(ctx, "no offline autoconfig found");
info!(ctx, "No offline autoconfig found.");
param_autoconfig = get_autoconfig(ctx, param, &param_domain).await;
}
} else {
provider = None;
param_autoconfig = None;
}
let user_strict_tls = match param.certificate_checks {
CertificateChecks::Automatic => None,
CertificateChecks::Strict => Some(true),
CertificateChecks::AcceptInvalidCertificates
| CertificateChecks::AcceptInvalidCertificates2 => Some(false),
};
let provider_strict_tls = param.provider.map(|provider| provider.opt.strict_tls);
let strict_tls = user_strict_tls.or(provider_strict_tls).unwrap_or(true);
// Do not save `CertificateChecks::Automatic` into `configured_imap_certificate_checks`.
param.certificate_checks = if strict_tls {
CertificateChecks::Strict
} else {
CertificateChecks::AcceptInvalidCertificates
};
progress!(ctx, 500);
let mut servers = param_autoconfig.unwrap_or_default();
@@ -326,107 +310,125 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
let servers = expand_param_vector(servers, &param.addr, &param_domain);
let configured_login_param = ConfiguredLoginParam {
addr,
imap: servers
.iter()
.filter_map(|params| {
let Ok(security) = params.socket.try_into() else {
return None;
};
if params.protocol == Protocol::Imap {
Some(ConfiguredServerLoginParam {
connection: ConnectionCandidate {
host: params.hostname.clone(),
port: params.port,
security,
},
user: params.username.clone(),
})
} else {
None
}
})
.collect(),
imap_user: param.imap.user.clone(),
imap_password: param.imap.password.clone(),
smtp: servers
.iter()
.filter_map(|params| {
let Ok(security) = params.socket.try_into() else {
return None;
};
if params.protocol == Protocol::Smtp {
Some(ConfiguredServerLoginParam {
connection: ConnectionCandidate {
host: params.hostname.clone(),
port: params.port,
security,
},
user: params.username.clone(),
})
} else {
None
}
})
.collect(),
smtp_user: param.smtp.user.clone(),
smtp_password,
socks5_config: param.socks5_config.clone(),
provider,
certificate_checks: match param.certificate_checks {
EnteredCertificateChecks::Automatic => ConfiguredCertificateChecks::Automatic,
EnteredCertificateChecks::Strict => ConfiguredCertificateChecks::Strict,
EnteredCertificateChecks::AcceptInvalidCertificates
| EnteredCertificateChecks::AcceptInvalidCertificates2 => {
ConfiguredCertificateChecks::AcceptInvalidCertificates
}
},
oauth2: param.oauth2,
};
Ok(configured_login_param)
}
async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<ConfiguredLoginParam> {
progress!(ctx, 1);
let ctx2 = ctx.clone();
let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await });
let configured_param = get_configured_param(ctx, param).await?;
let strict_tls = configured_param.strict_tls();
progress!(ctx, 550);
// Spawn SMTP configuration task
let mut smtp = Smtp::new();
// to try SMTP while connecting to IMAP.
let context_smtp = ctx.clone();
let mut smtp_param = param.smtp.clone();
let smtp_addr = param.addr.clone();
let smtp_servers: Vec<ServerParams> = servers
.iter()
.filter(|params| params.protocol == Protocol::Smtp)
.cloned()
.collect();
let smtp_param = configured_param.smtp.clone();
let smtp_password = configured_param.smtp_password.clone();
let smtp_addr = configured_param.addr.clone();
let smtp_socks5 = configured_param.socks5_config.clone();
let smtp_config_task = task::spawn(async move {
let mut smtp_configured = false;
let mut errors = Vec::new();
for smtp_server in smtp_servers {
smtp_param.user.clone_from(&smtp_server.username);
smtp_param.server.clone_from(&smtp_server.hostname);
smtp_param.port = smtp_server.port;
smtp_param.security = smtp_server.socket;
let mut smtp = Smtp::new();
smtp.connect(
&context_smtp,
&smtp_param,
&smtp_password,
&smtp_socks5,
&smtp_addr,
strict_tls,
configured_param.oauth2,
)
.await?;
match try_smtp_one_param(
&context_smtp,
&smtp_param,
&socks5_config,
&smtp_addr,
strict_tls,
&mut smtp,
)
.await
{
Ok(_) => {
smtp_configured = true;
break;
}
Err(e) => errors.push(e),
}
}
if smtp_configured {
Ok(smtp_param)
} else {
Err(errors)
}
Ok::<(), anyhow::Error>(())
});
progress!(ctx, 600);
// Configure IMAP
let mut imap: Option<(Imap, ImapSession)> = None;
let imap_servers: Vec<&ServerParams> = servers
.iter()
.filter(|params| params.protocol == Protocol::Imap)
.collect();
let imap_servers_count = imap_servers.len();
let mut errors = Vec::new();
for (imap_server_index, imap_server) in imap_servers.into_iter().enumerate() {
param.imap.user.clone_from(&imap_server.username);
param.imap.server.clone_from(&imap_server.hostname);
param.imap.port = imap_server.port;
param.imap.security = imap_server.socket;
match try_imap_one_param(
ctx,
&param.imap,
&param.socks5_config,
&param.addr,
strict_tls,
)
.await
{
Ok(configured_imap) => {
imap = Some(configured_imap);
break;
}
Err(e) => errors.push(e),
}
progress!(
ctx,
600 + (800 - 600) * (1 + imap_server_index) / imap_servers_count
);
}
let (mut imap, mut imap_session) = match imap {
Some(imap) => imap,
None => bail!(nicer_configuration_error(ctx, errors).await),
let (_s, r) = async_channel::bounded(1);
let mut imap = Imap::new(
configured_param.imap.clone(),
configured_param.imap_password.clone(),
configured_param.socks5_config.clone(),
&configured_param.addr,
strict_tls,
configured_param.oauth2,
r,
);
let mut imap_session = match imap.connect(ctx).await {
Ok(session) => session,
Err(err) => bail!("{}", nicer_configuration_error(ctx, err.to_string()).await),
};
progress!(ctx, 850);
// Wait for SMTP configuration
match smtp_config_task.await.unwrap() {
Ok(smtp_param) => {
param.smtp = smtp_param;
}
Err(errors) => {
bail!(nicer_configuration_error(ctx, errors).await);
}
}
smtp_config_task.await.unwrap()?;
progress!(ctx, 900);
@@ -474,8 +476,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
}
}
// the trailing underscore is correct
param.save_as_configured_params(ctx).await?;
configured_param.save_as_configured_params(ctx).await?;
ctx.set_config_internal(Config::ConfiguredTimestamp, Some(&time().to_string()))
.await?;
@@ -493,7 +494,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
ctx.sql.set_raw_config_bool("configured", true).await?;
Ok(())
Ok(configured_param)
}
/// Retrieve available autoconfigurations.
@@ -502,7 +503,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
/// B. If we have no configuration yet, search configuration in Thunderbird's central database
async fn get_autoconfig(
ctx: &Context,
param: &LoginParam,
param: &EnteredLoginParam,
param_domain: &str,
) -> Option<Vec<ServerParams>> {
let param_addr_urlencoded = utf8_percent_encode(&param.addr, NON_ALPHANUMERIC).to_string();
@@ -573,140 +574,19 @@ async fn get_autoconfig(
None
}
async fn try_imap_one_param(
context: &Context,
param: &ServerLoginParam,
socks5_config: &Option<Socks5Config>,
addr: &str,
strict_tls: bool,
) -> Result<(Imap, ImapSession), ConfigurationError> {
let inf = format!(
"imap: {}@{}:{} security={} strict_tls={} oauth2={} socks5_config={}",
param.user,
param.server,
param.port,
param.security,
strict_tls,
param.oauth2,
if let Some(socks5_config) = socks5_config {
socks5_config.to_string()
} else {
"None".to_string()
}
);
info!(context, "Trying: {}", inf);
let (_s, r) = async_channel::bounded(1);
let mut imap = match Imap::new(param, socks5_config.clone(), addr, strict_tls, r) {
Err(err) => {
info!(context, "failure: {:#}", err);
return Err(ConfigurationError {
config: inf,
msg: format!("{err:#}"),
});
}
Ok(imap) => imap,
};
match imap.connect(context).await {
Err(err) => {
info!(context, "IMAP failure: {err:#}.");
Err(ConfigurationError {
config: inf,
msg: format!("{err:#}"),
})
}
Ok(session) => {
info!(context, "IMAP success: {inf}.");
Ok((imap, session))
}
}
}
async fn try_smtp_one_param(
context: &Context,
param: &ServerLoginParam,
socks5_config: &Option<Socks5Config>,
addr: &str,
strict_tls: bool,
smtp: &mut Smtp,
) -> Result<(), ConfigurationError> {
let inf = format!(
"smtp: {}@{}:{} security={} strict_tls={} oauth2={} socks5_config={}",
param.user,
param.server,
param.port,
param.security,
strict_tls,
param.oauth2,
if let Some(socks5_config) = socks5_config {
socks5_config.to_string()
} else {
"None".to_string()
}
);
info!(context, "Trying: {}", inf);
if let Err(err) = smtp
.connect(context, param, socks5_config, addr, strict_tls)
.await
async fn nicer_configuration_error(context: &Context, e: String) -> String {
if e.to_lowercase().contains("could not resolve")
|| e.to_lowercase().contains("connection attempts")
|| e.to_lowercase()
.contains("temporary failure in name resolution")
|| e.to_lowercase().contains("name or service not known")
|| e.to_lowercase()
.contains("failed to lookup address information")
{
info!(context, "SMTP failure: {err:#}.");
Err(ConfigurationError {
config: inf,
msg: format!("{err:#}"),
})
} else {
info!(context, "SMTP success: {inf}.");
smtp.disconnect();
Ok(())
}
}
/// Failure to connect and login with email client configuration.
#[derive(Debug, thiserror::Error)]
#[error("Trying {config}…\nError: {msg}")]
pub struct ConfigurationError {
/// Tried configuration description.
config: String,
/// Error message.
msg: String,
}
async fn nicer_configuration_error(context: &Context, errors: Vec<ConfigurationError>) -> String {
let first_err = if let Some(f) = errors.first() {
f
} else {
// This means configuration failed but no errors have been captured. This should never
// happen, but if it does, the user will see classic "Error: no error".
return "no error".to_string();
};
if errors.iter().all(|e| {
e.msg.to_lowercase().contains("could not resolve")
|| e.msg.to_lowercase().contains("no dns resolution results")
|| e.msg
.to_lowercase()
.contains("temporary failure in name resolution")
|| e.msg.to_lowercase().contains("name or service not known")
|| e.msg
.to_lowercase()
.contains("failed to lookup address information")
}) {
return stock_str::error_no_network(context).await;
}
if errors.iter().all(|e| e.msg == first_err.msg) {
return first_err.msg.to_string();
}
errors
.iter()
.map(|e| e.to_string())
.collect::<Vec<String>>()
.join("\n\n")
e
}
#[derive(Debug, thiserror::Error)]
@@ -732,7 +612,9 @@ pub enum Error {
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::config::Config;
use crate::login_param::EnteredServerLoginParam;
use crate::test_utils::TestContext;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -744,4 +626,24 @@ mod tests {
t.set_config(Config::MailPw, Some("123456")).await.unwrap();
assert!(t.configure().await.is_err());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_configured_param() -> Result<()> {
let t = &TestContext::new().await;
let entered_param = EnteredLoginParam {
addr: "alice@example.org".to_string(),
imap: EnteredServerLoginParam {
user: "alice@example.net".to_string(),
password: "foobar".to_string(),
..Default::default()
},
..Default::default()
};
let configured_param = get_configured_param(t, &entered_param).await?;
assert_eq!(configured_param.imap_user, "alice@example.net");
assert_eq!(configured_param.smtp_user, "");
Ok(())
}
}

View File

@@ -30,7 +30,6 @@ use crate::context::Context;
use crate::events::EventType;
use crate::key::{load_self_public_key, DcKey, SignedPublicKey};
use crate::log::LogExt;
use crate::login_param::LoginParam;
use crate::message::MessageState;
use crate::mimeparser::AvatarAction;
use crate::param::{Param, Params};
@@ -1191,7 +1190,10 @@ impl Contact {
);
let contact = Contact::get_by_id(context, contact_id).await?;
let loginparam = LoginParam::load_configured_params(context).await?;
let addr = context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
let peerstate = Peerstate::from_addr(context, &contact.addr).await?;
let Some(peerstate) = peerstate.filter(|peerstate| peerstate.peek_key(false).is_some())
@@ -1220,8 +1222,8 @@ impl Contact {
.peek_key(false)
.map(|k| k.fingerprint().to_string())
.unwrap_or_default();
if loginparam.addr < peerstate.addr {
cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, "");
if addr < peerstate.addr {
cat_fingerprint(&mut ret, &addr, &fingerprint_self, "");
cat_fingerprint(
&mut ret,
&peerstate.addr,
@@ -1235,7 +1237,7 @@ impl Contact {
&fingerprint_other_verified,
&fingerprint_other_unverified,
);
cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, "");
cat_fingerprint(&mut ret, &addr, &fingerprint_self, "");
}
Ok(ret)
@@ -2888,7 +2890,7 @@ Hi."#;
bob.recv_msg(&sent_msg).await;
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
let green = ansi_term::Color::Green.normal();
let green = nu_ansi_term::Color::Green.normal();
assert!(
contact.was_seen_recently(),
"{}",

View File

@@ -27,7 +27,7 @@ use crate::download::DownloadState;
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::imap::{FolderMeaning, Imap, ServerMetadata};
use crate::key::{load_self_public_key, load_self_secret_key, DcKey as _};
use crate::login_param::LoginParam;
use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam};
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
use crate::param::{Param, Params};
use crate::peer_channels::Iroh;
@@ -715,8 +715,10 @@ impl Context {
/// Returns information about the context as key-value pairs.
pub async fn get_info(&self) -> Result<BTreeMap<&'static str, String>> {
let unset = "0";
let l = LoginParam::load_candidate_params_unchecked(self).await?;
let l2 = LoginParam::load_configured_params(self).await?;
let l = EnteredLoginParam::load(self).await?;
let l2 = ConfiguredLoginParam::load(self)
.await?
.map_or_else(|| "Not configured".to_string(), |param| param.to_string());
let secondary_addrs = self.get_secondary_self_addrs().await?.join(", ");
let displayname = self.get_config(Config::Displayname).await?;
let chats = get_chat_cnt(self).await?;
@@ -807,7 +809,7 @@ impl Context {
res.insert("is_configured", is_configured.to_string());
res.insert("socks5_enabled", socks5_enabled.to_string());
res.insert("entered_account_settings", l.to_string());
res.insert("used_account_settings", l2.to_string());
res.insert("used_account_settings", l2);
if let Some(server_id) = &*self.server_id.read().await {
res.insert("imap_server_id", format!("{server_id:?}"));

View File

@@ -32,7 +32,9 @@ use crate::contact::{Contact, ContactId, Modifier, Origin};
use crate::context::Context;
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::login_param::{LoginParam, ServerLoginParam};
use crate::login_param::{
prioritize_server_login_params, ConfiguredLoginParam, ConfiguredServerLoginParam,
};
use crate::message::{self, Message, MessageState, MessengerMessage, MsgId, Viewtype};
use crate::mimeparser;
use crate::oauth2::get_oauth2_access_token;
@@ -73,12 +75,17 @@ pub(crate) struct Imap {
addr: String,
/// Login parameters.
lp: ServerLoginParam,
lp: Vec<ConfiguredServerLoginParam>,
/// Password.
password: String,
/// SOCKS 5 configuration.
socks5_config: Option<Socks5Config>,
strict_tls: bool,
oauth2: bool,
login_failed_once: bool,
pub(crate) connectivity: ConnectivityStore,
@@ -228,31 +235,29 @@ impl Imap {
///
/// `addr` is used to renew token if OAuth2 authentication is used.
pub fn new(
lp: &ServerLoginParam,
lp: Vec<ConfiguredServerLoginParam>,
password: String,
socks5_config: Option<Socks5Config>,
addr: &str,
strict_tls: bool,
oauth2: bool,
idle_interrupt_receiver: Receiver<()>,
) -> Result<Self> {
if lp.server.is_empty() || lp.user.is_empty() || lp.password.is_empty() {
bail!("Incomplete IMAP connection parameters");
}
let imap = Imap {
) -> Self {
Imap {
idle_interrupt_receiver,
addr: addr.to_string(),
lp: lp.clone(),
lp,
password,
socks5_config,
strict_tls,
oauth2,
login_failed_once: false,
connectivity: Default::default(),
conn_last_try: UNIX_EPOCH,
conn_backoff_ms: 0,
// 1 connection per minute + a burst of 2.
ratelimit: Ratelimit::new(Duration::new(120, 0), 2.0),
};
Ok(imap)
}
}
/// Creates new disconnected IMAP client using configured parameters.
@@ -260,18 +265,18 @@ impl Imap {
context: &Context,
idle_interrupt_receiver: Receiver<()>,
) -> Result<Self> {
if !context.is_configured().await? {
bail!("IMAP Connect without configured params");
}
let param = LoginParam::load_configured_params(context).await?;
let param = ConfiguredLoginParam::load(context)
.await?
.context("Not configured")?;
let imap = Self::new(
&param.imap,
param.imap.clone(),
param.imap_password.clone(),
param.socks5_config.clone(),
&param.addr,
param.strict_tls(),
param.oauth2,
idle_interrupt_receiver,
)?;
);
Ok(imap)
}
@@ -283,10 +288,6 @@ impl Imap {
/// instead if you are going to actually use connection rather than trying connection
/// parameters.
pub(crate) async fn connect(&mut self, context: &Context) -> Result<Session> {
if self.lp.server.is_empty() {
bail!("IMAP operation attempted while it is torn down");
}
let now = tools::Time::now();
let until_can_send = max(
min(self.conn_last_try, now)
@@ -328,91 +329,107 @@ impl Imap {
);
self.conn_backoff_ms = max(BACKOFF_MIN_MS, self.conn_backoff_ms);
let connection_res = Client::connect(
context,
self.lp.server.as_ref(),
self.lp.port,
self.strict_tls,
self.socks5_config.clone(),
self.lp.security,
)
.await;
let client = connection_res?;
self.conn_backoff_ms = BACKOFF_MIN_MS;
self.ratelimit.send();
let imap_user: &str = self.lp.user.as_ref();
let imap_pw: &str = self.lp.password.as_ref();
let oauth2 = self.lp.oauth2;
let login_res = if oauth2 {
info!(context, "Logging into IMAP server with OAuth 2");
let addr: &str = self.addr.as_ref();
let token = get_oauth2_access_token(context, addr, imap_pw, true)
.await?
.context("IMAP could not get OAUTH token")?;
let auth = OAuth2 {
user: imap_user.into(),
access_token: token,
let login_params = prioritize_server_login_params(&context.sql, &self.lp, "imap").await?;
let mut first_error = None;
for lp in login_params {
info!(context, "IMAP trying to connect to {}.", &lp.connection);
let connection_candidate = lp.connection.clone();
let client = match Client::connect(
context,
self.socks5_config.clone(),
self.strict_tls,
connection_candidate,
)
.await
{
Ok(client) => client,
Err(err) => {
warn!(context, "IMAP failed to connect: {err:#}.");
first_error.get_or_insert(err);
continue;
}
};
client.authenticate("XOAUTH2", auth).await
} else {
info!(context, "Logging into IMAP server with LOGIN");
client.login(imap_user, imap_pw).await
};
match login_res {
Ok(session) => {
// Store server ID in the context to display in account info.
let mut lock = context.server_id.write().await;
lock.clone_from(&session.capabilities.server_id);
self.conn_backoff_ms = BACKOFF_MIN_MS;
self.ratelimit.send();
self.login_failed_once = false;
context.emit_event(EventType::ImapConnected(format!(
"IMAP-LOGIN as {}",
self.lp.user
)));
self.connectivity.set_connected(context).await;
info!(context, "Successfully logged into IMAP server");
Ok(session)
}
let imap_user: &str = lp.user.as_ref();
let imap_pw: &str = &self.password;
Err(err) => {
let imap_user = self.lp.user.to_owned();
let message = stock_str::cannot_login(context, &imap_user).await;
let login_res = if self.oauth2 {
info!(context, "Logging into IMAP server with OAuth 2.");
let addr: &str = self.addr.as_ref();
warn!(context, "{} ({:#})", message, err);
let token = get_oauth2_access_token(context, addr, imap_pw, true)
.await?
.context("IMAP could not get OAUTH token")?;
let auth = OAuth2 {
user: imap_user.into(),
access_token: token,
};
client.authenticate("XOAUTH2", auth).await
} else {
info!(context, "Logging into IMAP server with LOGIN.");
client.login(imap_user, imap_pw).await
};
let lock = context.wrong_pw_warning_mutex.lock().await;
if self.login_failed_once
&& err.to_string().to_lowercase().contains("authentication")
&& context.get_config_bool(Config::NotifyAboutWrongPw).await?
{
if let Err(e) = context
.set_config_internal(Config::NotifyAboutWrongPw, None)
.await
{
warn!(context, "{:#}", e);
}
drop(lock);
match login_res {
Ok(session) => {
// Store server ID in the context to display in account info.
let mut lock = context.server_id.write().await;
lock.clone_from(&session.capabilities.server_id);
let mut msg = Message::new(Viewtype::Text);
msg.text.clone_from(&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;
self.login_failed_once = false;
context.emit_event(EventType::ImapConnected(format!(
"IMAP-LOGIN as {}",
lp.user
)));
self.connectivity.set_connected(context).await;
info!(context, "Successfully logged into IMAP server");
return Ok(session);
}
Err(format_err!("{}\n\n{:#}", message, err))
Err(err) => {
let imap_user = lp.user.to_owned();
let message = stock_str::cannot_login(context, &imap_user).await;
let err_str = err.to_string();
warn!(context, "IMAP failed to login: {err:#}.");
first_error.get_or_insert(format_err!("{message} ({err:#})"));
let lock = context.wrong_pw_warning_mutex.lock().await;
if self.login_failed_once
&& err_str.to_lowercase().contains("authentication")
&& context.get_config_bool(Config::NotifyAboutWrongPw).await?
{
if let Err(e) = context
.set_config_internal(Config::NotifyAboutWrongPw, None)
.await
{
warn!(context, "{e:#}.");
}
drop(lock);
let mut msg = Message::new(Viewtype::Text);
msg.text.clone_from(&message);
if let Err(e) = chat::add_device_msg_with_importance(
context,
None,
Some(&mut msg),
true,
)
.await
{
warn!(context, "Failed to add device message: {e:#}.");
}
} else {
self.login_failed_once = true;
}
}
}
}
Err(first_error.unwrap_or_else(|| format_err!("No IMAP connection candidates provided")))
}
/// Prepare for IMAP operation.

View File

@@ -1,7 +1,7 @@
use std::net::SocketAddr;
use std::ops::{Deref, DerefMut};
use anyhow::{bail, format_err, Context as _, Result};
use anyhow::{Context as _, Result};
use async_imap::Client as ImapClient;
use async_imap::Session as ImapSession;
use fast_socks5::client::Socks5Stream;
@@ -10,12 +10,13 @@ use tokio::io::BufWriter;
use super::capabilities::Capabilities;
use super::session::Session;
use crate::context::Context;
use crate::login_param::{ConnectionCandidate, ConnectionSecurity};
use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp};
use crate::net::session::SessionStream;
use crate::net::tls::wrap_tls;
use crate::net::update_connection_history;
use crate::net::{connect_tcp_inner, connect_tls_inner};
use crate::provider::Socket;
use crate::net::{
connect_tcp_inner, connect_tls_inner, run_connection_attempts, update_connection_history,
};
use crate::socks::Socks5Config;
use crate::tools::time;
@@ -107,62 +108,93 @@ impl Client {
Ok(Session::new(session, capabilities))
}
async fn connection_attempt(
context: Context,
host: String,
security: ConnectionSecurity,
resolved_addr: SocketAddr,
strict_tls: bool,
) -> Result<Self> {
let context = &context;
let host = &host;
info!(
context,
"Attempting IMAP connection to {host} ({resolved_addr})."
);
let res = match security {
ConnectionSecurity::Tls => {
Client::connect_secure(resolved_addr, host, strict_tls).await
}
ConnectionSecurity::Starttls => {
Client::connect_starttls(resolved_addr, host, strict_tls).await
}
ConnectionSecurity::Plain => Client::connect_insecure(resolved_addr).await,
};
match res {
Ok(client) => {
let ip_addr = resolved_addr.ip().to_string();
let port = resolved_addr.port();
let save_cache = match security {
ConnectionSecurity::Tls | ConnectionSecurity::Starttls => strict_tls,
ConnectionSecurity::Plain => false,
};
if save_cache {
update_connect_timestamp(context, host, &ip_addr).await?;
}
update_connection_history(context, "imap", host, port, &ip_addr, time()).await?;
Ok(client)
}
Err(err) => {
warn!(
context,
"Failed to connect to {host} ({resolved_addr}): {err:#}."
);
Err(err)
}
}
}
pub async fn connect(
context: &Context,
host: &str,
port: u16,
strict_tls: bool,
socks5_config: Option<Socks5Config>,
security: Socket,
strict_tls: bool,
candidate: ConnectionCandidate,
) -> Result<Self> {
let host = &candidate.host;
let port = candidate.port;
let security = candidate.security;
if let Some(socks5_config) = socks5_config {
let client = match security {
Socket::Automatic => bail!("IMAP port security is not configured"),
Socket::Ssl => {
ConnectionSecurity::Tls => {
Client::connect_secure_socks5(context, host, port, strict_tls, socks5_config)
.await?
}
Socket::Starttls => {
ConnectionSecurity::Starttls => {
Client::connect_starttls_socks5(context, host, port, socks5_config, strict_tls)
.await?
}
Socket::Plain => {
ConnectionSecurity::Plain => {
Client::connect_insecure_socks5(context, host, port, socks5_config).await?
}
};
Ok(client)
} else {
let mut first_error = None;
let load_cache =
strict_tls && (security == Socket::Ssl || security == Socket::Starttls);
for resolved_addr in
lookup_host_with_cache(context, host, port, "imap", load_cache).await?
{
let res = match security {
Socket::Automatic => bail!("IMAP port security is not configured"),
Socket::Ssl => Client::connect_secure(resolved_addr, host, strict_tls).await,
Socket::Starttls => {
Client::connect_starttls(resolved_addr, host, strict_tls).await
}
Socket::Plain => Client::connect_insecure(resolved_addr).await,
};
match res {
Ok(client) => {
let ip_addr = resolved_addr.ip().to_string();
if load_cache {
update_connect_timestamp(context, host, &ip_addr).await?;
}
update_connection_history(context, "imap", host, port, &ip_addr, time())
.await?;
return Ok(client);
}
Err(err) => {
warn!(context, "Failed to connect to {resolved_addr}: {err:#}.");
first_error.get_or_insert(err);
}
}
}
Err(first_error.unwrap_or_else(|| format_err!("no DNS resolution results for {host}")))
let load_cache = match security {
ConnectionSecurity::Tls | ConnectionSecurity::Starttls => strict_tls,
ConnectionSecurity::Plain => false,
};
let connection_futures =
lookup_host_with_cache(context, host, port, "imap", load_cache)
.await?
.into_iter()
.map(|resolved_addr| {
let context = context.clone();
let host = host.to_string();
Self::connection_attempt(context, host, security, resolved_addr, strict_tls)
});
run_connection_attempts(connection_futures).await
}
}

View File

@@ -31,36 +31,24 @@ use std::pin::Pin;
use std::sync::Arc;
use std::task::Poll;
use anyhow::{anyhow, bail, ensure, format_err, Context as _, Result};
use futures_lite::StreamExt;
use anyhow::{bail, Context as _, Result};
use iroh_net::relay::RelayMode;
use iroh_net::Endpoint;
use iroh_old;
use iroh_old::blobs::Collection;
use iroh_old::get::DataStream;
use iroh_old::progress::ProgressEmitter;
use iroh_old::provider::Ticket;
use tokio::fs::{self, File};
use tokio::io::{self, AsyncWriteExt, BufWriter};
use tokio::sync::broadcast::error::RecvError;
use tokio::sync::{broadcast, Mutex};
use tokio::task::{JoinHandle, JoinSet};
use tokio_stream::wrappers::ReadDirStream;
use tokio::fs;
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use crate::chat::{add_device_msg, delete_and_reset_all_device_msgs};
use crate::chat::add_device_msg;
use crate::context::Context;
use crate::imex::BlobDirContents;
use crate::message::{Message, Viewtype};
use crate::qr::{self, Qr};
use crate::qr::Qr;
use crate::stock_str::backup_transfer_msg_body;
use crate::tools::{create_id, time, TempPathGuard};
use crate::EventType;
use super::{export_backup_stream, export_database, import_backup_stream, DBFILE_BACKUP_NAME};
const MAX_CONCURRENT_DIALS: u8 = 16;
/// ALPN protocol identifier for the backup transfer protocol.
const BACKUP_ALPN: &[u8] = b"/deltachat/backup";
@@ -279,33 +267,6 @@ impl Future for BackupProvider {
}
}
/// Retrieves backup from a legacy backup provider using iroh 0.4.
pub async fn get_legacy_backup(context: &Context, qr: Qr) -> Result<()> {
ensure!(
matches!(qr, Qr::Backup { .. }),
"QR code for backup must be of type DCBACKUP"
);
ensure!(
!context.is_configured().await?,
"Cannot import backups to accounts in use."
);
// Acquire global "ongoing" mutex.
let cancel_token = context.alloc_ongoing().await?;
let _guard = context.scheduler.pause(context.clone()).await;
info!(
context,
"Running get_backup for {}",
qr::format_backup(&qr)?
);
let res = tokio::select! {
biased;
res = get_backup_inner(context, qr) => res,
_ = cancel_token.recv() => Err(format_err!("cancelled")),
};
context.free_ongoing().await;
res
}
pub async fn get_backup2(
context: &Context,
node_addr: iroh_net::NodeAddr,
@@ -349,202 +310,20 @@ pub async fn get_backup2(
///
/// This is a long running operation which will return only when completed.
///
/// Using [`Qr`] as argument is a bit odd as it only accepts specific variants of it. It
/// does avoid having [`iroh_old::provider::Ticket`] in the primary API however, without
/// Using [`Qr`] as argument is a bit odd as it only accepts specific variant of it. It
/// does avoid having [`iroh_net::NodeAddr`] in the primary API however, without
/// having to revert to untyped bytes.
pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
match qr {
Qr::Backup { .. } => get_legacy_backup(context, qr).await?,
Qr::Backup2 {
node_addr,
auth_token,
} => get_backup2(context, node_addr, auth_token).await?,
_ => bail!("QR code for backup must be of type DCBACKUP or DCBACKUP2"),
_ => bail!("QR code for backup must be of type DCBACKUP2"),
}
Ok(())
}
async fn get_backup_inner(context: &Context, qr: Qr) -> Result<()> {
let ticket = match qr {
Qr::Backup { ticket } => ticket,
_ => bail!("QR code for backup must be of type DCBACKUP"),
};
match transfer_from_provider(context, &ticket).await {
Ok(()) => {
context.sql.run_migrations(context).await?;
delete_and_reset_all_device_msgs(context).await?;
context.emit_event(ReceiveProgress::Completed.into());
Ok(())
}
Err(err) => {
// Clean up any blobs we already wrote.
let readdir = fs::read_dir(context.get_blobdir()).await?;
let mut readdir = ReadDirStream::new(readdir);
while let Some(dirent) = readdir.next().await {
if let Ok(dirent) = dirent {
fs::remove_file(dirent.path()).await.ok();
}
}
context.emit_event(ReceiveProgress::Failed.into());
Err(err)
}
}
}
async fn transfer_from_provider(context: &Context, ticket: &Ticket) -> Result<()> {
let progress = ProgressEmitter::new(0, ReceiveProgress::max_blob_progress());
spawn_progress_proxy(context.clone(), progress.subscribe());
let on_connected = || {
context.emit_event(ReceiveProgress::Connected.into());
async { Ok(()) }
};
let on_collection = |collection: &Collection| {
context.emit_event(ReceiveProgress::CollectionReceived.into());
progress.set_total(collection.total_blobs_size());
async { Ok(()) }
};
let jobs = Mutex::new(JoinSet::default());
let on_blob =
|hash, reader, name| on_blob(context, &progress, &jobs, ticket, hash, reader, name);
// Perform the transfer.
let keylog = false; // Do not enable rustls SSLKEYLOGFILE env var functionality
let stats = iroh_old::get::run_ticket(
ticket,
keylog,
MAX_CONCURRENT_DIALS,
on_connected,
on_collection,
on_blob,
)
.await?;
let mut jobs = jobs.lock().await;
while let Some(job) = jobs.join_next().await {
job.context("job failed")?;
}
drop(progress);
info!(
context,
"Backup transfer finished, transfer rate was {} Mbps.",
stats.mbits()
);
Ok(())
}
/// Get callback when a blob is received from the provider.
///
/// This writes the blobs to the blobdir. If the blob is the database it will import it to
/// the database of the current [`Context`].
async fn on_blob(
context: &Context,
progress: &ProgressEmitter,
jobs: &Mutex<JoinSet<()>>,
ticket: &Ticket,
_hash: iroh_old::Hash,
mut reader: DataStream,
name: String,
) -> Result<DataStream> {
ensure!(!name.is_empty(), "Received a nameless blob");
let path = if name.starts_with("db/") {
let context_dir = context
.get_blobdir()
.parent()
.ok_or_else(|| anyhow!("Context dir not found"))?;
let dbfile = context_dir.join(DBFILE_BACKUP_NAME);
if fs::metadata(&dbfile).await.is_ok() {
fs::remove_file(&dbfile).await?;
warn!(context, "Previous database export deleted");
}
dbfile
} else {
ensure!(name.starts_with("blob/"), "malformatted blob name");
let blobname = name.rsplit('/').next().context("malformatted blob name")?;
context.get_blobdir().join(blobname)
};
let mut wrapped_reader = progress.wrap_async_read(&mut reader);
let file = File::create(&path).await?;
let mut file = BufWriter::with_capacity(128 * 1024, file);
io::copy(&mut wrapped_reader, &mut file).await?;
file.flush().await?;
if name.starts_with("db/") {
let context = context.clone();
let token = ticket.token().to_string();
jobs.lock().await.spawn(async move {
if let Err(err) = context.sql.import(&path, token).await {
error!(context, "cannot import database: {:#?}", err);
}
if let Err(err) = fs::remove_file(&path).await {
error!(
context,
"failed to delete database import file '{}': {:#?}",
path.display(),
err,
);
}
});
}
Ok(reader)
}
/// Spawns a task proxying progress events.
///
/// This spawns a tokio task which receives events from the [`ProgressEmitter`] and sends
/// them to the context. The task finishes when the emitter is dropped.
///
/// This could be done directly in the emitter by making it less generic.
fn spawn_progress_proxy(context: Context, mut rx: broadcast::Receiver<u16>) {
tokio::spawn(async move {
loop {
match rx.recv().await {
Ok(step) => context.emit_event(ReceiveProgress::BlobProgress(step).into()),
Err(RecvError::Closed) => break,
Err(RecvError::Lagged(_)) => continue,
}
}
});
}
/// Create [`EventType::ImexProgress`] events using readable names.
///
/// Plus you get warnings if you don't use all variants.
#[derive(Debug)]
enum ReceiveProgress {
Connected,
CollectionReceived,
/// A value between 0 and 85 interpreted as a percentage.
///
/// Other values are already used by the other variants of this enum.
BlobProgress(u16),
Completed,
Failed,
}
impl ReceiveProgress {
/// The maximum value for [`ReceiveProgress::BlobProgress`].
///
/// This only exists to keep this magic value local in this type.
fn max_blob_progress() -> u16 {
85
}
}
impl From<ReceiveProgress> for EventType {
fn from(source: ReceiveProgress) -> Self {
let val = match source {
ReceiveProgress::Connected => 50,
ReceiveProgress::CollectionReceived => 100,
ReceiveProgress::BlobProgress(val) => 100 + 10 * val,
ReceiveProgress::Completed => 1000,
ReceiveProgress::Failed => 0,
};
EventType::ImexProgress(val.into())
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;

File diff suppressed because it is too large Load Diff

View File

@@ -2357,6 +2357,25 @@ mod tests {
assert_eq!(quoted_msg.get_text(), msg2.quoted_text().unwrap());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_quote() {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
tcm.send_recv_accept(alice, bob, "Hi!").await;
let msg = tcm
.send_recv(
alice,
bob,
"On 2024-08-28, Alice wrote:\n> A quote.\nNot really.",
)
.await;
assert!(msg.quoted_text().is_none());
assert!(msg.quoted_message(bob).await.unwrap().is_none());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_unencrypted_quote_encrypted_message() -> Result<()> {
let mut tcm = TestContextManager::new();

View File

@@ -579,6 +579,16 @@ impl MimeFactory {
"Auto-Submitted".to_string(),
"auto-generated".to_string(),
));
} else if let Loaded::Message { msg, .. } = &self.loaded {
if msg.param.get_cmd() == SystemMessage::SecurejoinMessage {
let step = msg.param.get(Param::Arg).unwrap_or_default();
if step != "vg-request" && step != "vc-request" {
headers.push(Header::new(
"Auto-Submitted".to_string(),
"auto-replied".to_string(),
));
}
}
}
if let Loaded::Message { chat, .. } = &self.loaded {

View File

@@ -1,4 +1,5 @@
//! # Common network utilities.
use std::future::Future;
use std::net::SocketAddr;
use std::pin::Pin;
use std::time::Duration;
@@ -6,10 +7,12 @@ use std::time::Duration;
use anyhow::{format_err, Context as _, Result};
use async_native_tls::TlsStream;
use tokio::net::TcpStream;
use tokio::task::JoinSet;
use tokio::time::timeout;
use tokio_io_timeout::TimeoutStream;
use crate::context::Context;
use crate::sql::Sql;
use crate::tools::time;
pub(crate) mod dns;
@@ -64,21 +67,22 @@ pub(crate) async fn update_connection_history(
Ok(())
}
/// Returns timestamp of the most recent successful connection
/// to the host and port for given protocol.
pub(crate) async fn load_connection_timestamp(
context: &Context,
sql: &Sql,
alpn: &str,
host: &str,
port: u16,
addr: &str,
addr: Option<&str>,
) -> Result<Option<i64>> {
let timestamp = context
.sql
let timestamp = sql
.query_get_value(
"SELECT timestamp FROM connection_history
WHERE host = ?
AND port = ?
AND alpn = ?
AND addr = ?",
AND addr = IFNULL(?, addr)",
(host, port, alpn, addr),
)
.await?;
@@ -121,6 +125,96 @@ pub(crate) async fn connect_tls_inner(
Ok(tls_stream)
}
/// Runs connection attempt futures.
///
/// Accepts iterator of connection attempt futures
/// and runs them until one of them succeeds
/// or all of them fail.
///
/// If all connection attempts fail, returns the first error.
///
/// This functions starts with one connection attempt and maintains
/// up to five parallel connection attempts if connecting takes time.
pub(crate) async fn run_connection_attempts<O, I, F>(mut futures: I) -> Result<O>
where
I: Iterator<Item = F>,
F: Future<Output = Result<O>> + Send + 'static,
O: Send + 'static,
{
let mut connection_attempt_set = JoinSet::new();
// Start additional connection attempts after 300 ms, 1 s, 5 s and 10 s.
// This way we can have up to 5 parallel connection attempts at the same time.
let mut delay_set = JoinSet::new();
for delay in [
Duration::from_millis(300),
Duration::from_secs(1),
Duration::from_secs(5),
Duration::from_secs(10),
] {
delay_set.spawn(tokio::time::sleep(delay));
}
let mut first_error = None;
let res = loop {
if let Some(fut) = futures.next() {
connection_attempt_set.spawn(fut);
}
tokio::select! {
biased;
res = connection_attempt_set.join_next() => {
match res {
Some(res) => {
match res.context("Failed to join task") {
Ok(Ok(conn)) => {
// Successfully connected.
break Ok(conn);
}
Ok(Err(err)) => {
// Some connection attempt failed.
first_error.get_or_insert(err);
}
Err(err) => {
break Err(err);
}
}
}
None => {
// Out of connection attempts.
//
// Break out of the loop and return error.
break Err(
first_error.unwrap_or_else(|| format_err!("No connection attempts were made"))
);
}
}
},
_ = delay_set.join_next(), if !delay_set.is_empty() => {
// Delay expired.
//
// Don't do anything other than pushing
// another connection attempt into `connection_attempt_set`.
}
}
};
// Abort remaining connection attempts and free resources
// such as OS sockets and `Context` references
// held by connection attempt tasks.
//
// `delay_set` contains just `sleep` tasks
// so no need to await futures there,
// it is enough that futures are aborted
// when the set is dropped.
connection_attempt_set.shutdown().await;
res
}
/// If `load_cache` is true, may use cached DNS results.
/// Because the cache may be poisoned with incorrect results by networks hijacking DNS requests,
/// this option should only be used when connection is authenticated,
@@ -133,22 +227,9 @@ pub(crate) async fn connect_tcp(
port: u16,
load_cache: bool,
) -> Result<Pin<Box<TimeoutStream<TcpStream>>>> {
let mut first_error = None;
for resolved_addr in lookup_host_with_cache(context, host, port, "", load_cache).await? {
match connect_tcp_inner(resolved_addr).await {
Ok(stream) => {
return Ok(stream);
}
Err(err) => {
warn!(
context,
"Failed to connect to {}: {:#}.", resolved_addr, err
);
first_error.get_or_insert(err);
}
}
}
Err(first_error.unwrap_or_else(|| format_err!("no DNS resolution results for {host}")))
let connection_futures = lookup_host_with_cache(context, host, port, "", load_cache)
.await?
.into_iter()
.map(connect_tcp_inner);
run_connection_attempts(connection_futures).await
}

View File

@@ -108,6 +108,10 @@ pub(crate) async fn update_connect_timestamp(
Ok(())
}
/// Preloaded DNS results that can be used in case of DNS server failures.
///
/// See <https://support.delta.chat/t/no-dns-resolution-result/2778> and
/// <https://github.com/deltachat/deltachat-core-rust/issues/4920> for reasons.
static DNS_PRELOAD: Lazy<HashMap<&'static str, Vec<IpAddr>>> = Lazy::new(|| {
HashMap::from([
(
@@ -501,21 +505,6 @@ static DNS_PRELOAD: Lazy<HashMap<&'static str, Vec<IpAddr>>> = Lazy::new(|| {
])
});
/// Load hardcoded cache if everything else fails.
///
/// See <https://support.delta.chat/t/no-dns-resolution-result/2778> and
/// <https://github.com/deltachat/deltachat-core-rust/issues/4920> for reasons.
///
/// In the future we may pre-resolve all provider database addresses
/// and build them in.
fn load_hardcoded_cache(hostname: &str, port: u16) -> Vec<SocketAddr> {
if let Some(ips) = DNS_PRELOAD.get(hostname) {
ips.iter().map(|ip| SocketAddr::new(*ip, port)).collect()
} else {
Vec::new()
}
}
async fn lookup_cache(
context: &Context,
host: &str,
@@ -574,11 +563,16 @@ async fn sort_by_connection_timestamp(
alpn: &str,
host: &str,
) -> Result<Vec<SocketAddr>> {
let mut res: Vec<(Option<i64>, SocketAddr)> = Vec::new();
let mut res: Vec<(Option<i64>, SocketAddr)> = Vec::with_capacity(input.len());
for addr in input {
let timestamp =
load_connection_timestamp(context, alpn, host, addr.port(), &addr.ip().to_string())
.await?;
let timestamp = load_connection_timestamp(
&context.sql,
alpn,
host,
addr.port(),
Some(&addr.ip().to_string()),
)
.await?;
res.push((timestamp, addr));
}
res.sort_by_key(|(ts, _addr)| std::cmp::Reverse(*ts));
@@ -626,8 +620,13 @@ pub(crate) async fn lookup_host_with_cache(
}
}
if resolved_addrs.is_empty() {
return Ok(load_hardcoded_cache(hostname, port));
if let Some(ips) = DNS_PRELOAD.get(hostname) {
for ip in ips {
let addr = SocketAddr::new(*ip, port);
if !resolved_addrs.contains(&addr) {
resolved_addrs.push(addr);
}
}
}
}

View File

@@ -1,22 +1,17 @@
//! # HTTP module.
use std::sync::Arc;
use anyhow::{anyhow, Result};
use anyhow::{anyhow, bail, Context as _, Result};
use bytes::Bytes;
use http_body_util::BodyExt;
use hyper_util::rt::TokioIo;
use mime::Mime;
use once_cell::sync::Lazy;
use serde::Serialize;
use crate::context::Context;
use crate::net::lookup_host_with_cache;
use crate::net::session::SessionStream;
use crate::net::tls::wrap_tls;
use crate::socks::Socks5Config;
static LETSENCRYPT_ROOT: Lazy<reqwest::tls::Certificate> = Lazy::new(|| {
reqwest::tls::Certificate::from_der(include_bytes!(
"../../assets/root-certificates/letsencrypt/isrgrootx1.der"
))
.unwrap()
});
/// HTTP(S) GET response.
#[derive(Debug)]
pub struct Response {
@@ -32,48 +27,95 @@ pub struct Response {
/// Retrieves the text contents of URL using HTTP GET request.
pub async fn read_url(context: &Context, url: &str) -> Result<String> {
Ok(read_url_inner(context, url).await?.text().await?)
let response = read_url_blob(context, url).await?;
let text = String::from_utf8_lossy(&response.blob);
Ok(text.to_string())
}
async fn get_http_sender<B>(
context: &Context,
parsed_url: hyper::Uri,
) -> Result<hyper::client::conn::http1::SendRequest<B>>
where
B: hyper::body::Body + 'static + Send,
B::Data: Send,
B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
{
let scheme = parsed_url.scheme_str().context("URL has no scheme")?;
let host = parsed_url.host().context("URL has no host")?;
let socks5_config_opt = Socks5Config::from_database(&context.sql).await?;
let stream: Box<dyn SessionStream> = match scheme {
"http" => {
let port = parsed_url.port_u16().unwrap_or(80);
// It is safe to use cached IP addresses
// for HTTPS URLs, but for HTTP URLs
// better resolve from scratch each time to prevent
// cache poisoning attacks from having lasting effects.
let load_cache = false;
if let Some(socks5_config) = socks5_config_opt {
let socks5_stream = socks5_config
.connect(context, host, port, load_cache)
.await?;
Box::new(socks5_stream)
} else {
let tcp_stream = crate::net::connect_tcp(context, host, port, load_cache).await?;
Box::new(tcp_stream)
}
}
"https" => {
let port = parsed_url.port_u16().unwrap_or(443);
let load_cache = true;
let strict_tls = true;
if let Some(socks5_config) = socks5_config_opt {
let socks5_stream = socks5_config
.connect(context, host, port, load_cache)
.await?;
let tls_stream = wrap_tls(strict_tls, host, &[], socks5_stream).await?;
Box::new(tls_stream)
} else {
let tcp_stream = crate::net::connect_tcp(context, host, port, load_cache).await?;
let tls_stream = wrap_tls(strict_tls, host, &[], tcp_stream).await?;
Box::new(tls_stream)
}
}
_ => bail!("Unknown URL scheme"),
};
let io = TokioIo::new(stream);
let (sender, conn) = hyper::client::conn::http1::handshake(io).await?;
tokio::task::spawn(conn);
Ok(sender)
}
/// Retrieves the binary contents of URL using HTTP GET request.
pub async fn read_url_blob(context: &Context, url: &str) -> Result<Response> {
let response = read_url_inner(context, url).await?;
let content_type = response
.headers()
.get(reqwest::header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.parse::<Mime>().ok());
let mimetype = content_type
.as_ref()
.map(|mime| mime.essence_str().to_string());
let encoding = content_type.as_ref().and_then(|mime| {
mime.get_param(mime::CHARSET)
.map(|charset| charset.as_str().to_string())
});
let blob: Vec<u8> = response.bytes().await?.into();
Ok(Response {
blob,
mimetype,
encoding,
})
}
async fn read_url_inner(context: &Context, url: &str) -> Result<reqwest::Response> {
// It is safe to use cached IP addresses
// for HTTPS URLs, but for HTTP URLs
// better resolve from scratch each time to prevent
// cache poisoning attacks from having lasting effects.
let load_cache = url.starts_with("https://");
let client = get_client(context, load_cache).await?;
let mut url = url.to_string();
// Follow up to 10 http-redirects
for _i in 0..10 {
let response = client.get(&url).send().await?;
let parsed_url = url
.parse::<hyper::Uri>()
.with_context(|| format!("Failed to parse URL {url:?}"))?;
let mut sender = get_http_sender(context, parsed_url.clone()).await?;
let authority = parsed_url
.authority()
.context("URL has no authority")?
.clone();
let req = hyper::Request::builder()
.uri(parsed_url.path())
.header(hyper::header::HOST, authority.as_str())
.body(http_body_util::Empty::<Bytes>::new())?;
let response = sender.send_request(req).await?;
if response.status().is_redirection() {
let headers = response.headers();
let header = headers
let header = response
.headers()
.get_all("location")
.iter()
.last()
@@ -84,72 +126,119 @@ async fn read_url_inner(context: &Context, url: &str) -> Result<reqwest::Respons
continue;
}
return Ok(response);
let content_type = response
.headers()
.get("content-type")
.and_then(|value| value.to_str().ok())
.and_then(|value| value.parse::<Mime>().ok());
let mimetype = content_type
.as_ref()
.map(|mime| mime.essence_str().to_string());
let encoding = content_type.as_ref().and_then(|mime| {
mime.get_param(mime::CHARSET)
.map(|charset| charset.as_str().to_string())
});
let body = response.collect().await?.to_bytes();
let blob: Vec<u8> = body.to_vec();
return Ok(Response {
blob,
mimetype,
encoding,
});
}
Err(anyhow!("Followed 10 redirections"))
}
struct CustomResolver {
context: Context,
/// Whether to return cached results or not.
/// If resolver can be used for URLs
/// without TLS, e.g. HTTP URLs from HTML email,
/// this must be false. If TLS is used
/// and certificate hostnames are checked,
/// it is safe to load cache.
load_cache: bool,
}
impl CustomResolver {
fn new(context: Context, load_cache: bool) -> Self {
Self {
context,
load_cache,
}
/// Sends an empty POST request to the URL.
///
/// Returns response text and whether request was successful or not.
///
/// Does not follow redirects.
pub(crate) async fn post_empty(context: &Context, url: &str) -> Result<(String, bool)> {
let parsed_url = url
.parse::<hyper::Uri>()
.with_context(|| format!("Failed to parse URL {url:?}"))?;
let scheme = parsed_url.scheme_str().context("URL has no scheme")?;
if scheme != "https" {
bail!("POST requests to non-HTTPS URLs are not allowed");
}
let mut sender = get_http_sender(context, parsed_url.clone()).await?;
let authority = parsed_url
.authority()
.context("URL has no authority")?
.clone();
let req = hyper::Request::post(parsed_url.path())
.header(hyper::header::HOST, authority.as_str())
.body(http_body_util::Empty::<Bytes>::new())?;
let response = sender.send_request(req).await?;
let response_status = response.status();
let body = response.collect().await?.to_bytes();
let text = String::from_utf8_lossy(&body);
let response_text = text.to_string();
Ok((response_text, response_status.is_success()))
}
impl reqwest::dns::Resolve for CustomResolver {
fn resolve(&self, hostname: reqwest::dns::Name) -> reqwest::dns::Resolving {
let context = self.context.clone();
let load_cache = self.load_cache;
Box::pin(async move {
let port = 443; // Actual port does not matter.
let socket_addrs =
lookup_host_with_cache(&context, hostname.as_str(), port, "", load_cache).await;
match socket_addrs {
Ok(socket_addrs) => {
let addrs: reqwest::dns::Addrs = Box::new(socket_addrs.into_iter());
Ok(addrs)
}
Err(err) => Err(err.into()),
}
})
/// Posts string to the given URL.
///
/// Returns true if successful HTTP response code was returned.
///
/// Does not follow redirects.
#[allow(dead_code)]
pub(crate) async fn post_string(context: &Context, url: &str, body: String) -> Result<bool> {
let parsed_url = url
.parse::<hyper::Uri>()
.with_context(|| format!("Failed to parse URL {url:?}"))?;
let scheme = parsed_url.scheme_str().context("URL has no scheme")?;
if scheme != "https" {
bail!("POST requests to non-HTTPS URLs are not allowed");
}
let mut sender = get_http_sender(context, parsed_url.clone()).await?;
let authority = parsed_url
.authority()
.context("URL has no authority")?
.clone();
let request = hyper::Request::post(parsed_url.path())
.header(hyper::header::HOST, authority.as_str())
.body(body)?;
let response = sender.send_request(request).await?;
Ok(response.status().is_success())
}
pub(crate) async fn get_client(context: &Context, load_cache: bool) -> Result<reqwest::Client> {
let socks5_config = Socks5Config::from_database(&context.sql).await?;
let resolver = Arc::new(CustomResolver::new(context.clone(), load_cache));
/// Sends a POST request with x-www-form-urlencoded data.
///
/// Does not follow redirects.
pub(crate) async fn post_form<T: Serialize + ?Sized>(
context: &Context,
url: &str,
form: &T,
) -> Result<Bytes> {
let parsed_url = url
.parse::<hyper::Uri>()
.with_context(|| format!("Failed to parse URL {url:?}"))?;
let scheme = parsed_url.scheme_str().context("URL has no scheme")?;
if scheme != "https" {
bail!("POST requests to non-HTTPS URLs are not allowed");
}
let builder = reqwest::ClientBuilder::new()
.timeout(super::TIMEOUT)
.add_root_certificate(LETSENCRYPT_ROOT.clone())
.dns_resolver(resolver);
let builder = if let Some(socks5_config) = socks5_config {
let proxy = reqwest::Proxy::all(socks5_config.to_url())?;
builder.proxy(proxy)
} else {
// Disable usage of "system" proxy configured via environment variables.
// It is enabled by default in `reqwest`, see
// <https://docs.rs/reqwest/0.11.14/reqwest/struct.ClientBuilder.html#method.no_proxy>
// for documentation.
builder.no_proxy()
};
Ok(builder.build()?)
let encoded_body = serde_urlencoded::to_string(form).context("Failed to encode data")?;
let mut sender = get_http_sender(context, parsed_url.clone()).await?;
let authority = parsed_url
.authority()
.context("URL has no authority")?
.clone();
let request = hyper::Request::post(parsed_url.path())
.header(hyper::header::HOST, authority.as_str())
.header("content-type", "application/x-www-form-urlencoded")
.body(encoded_body)?;
let response = sender.send_request(request).await?;
let bytes = response.collect().await?.to_bytes();
Ok(bytes)
}

View File

@@ -2,12 +2,14 @@
use std::collections::HashMap;
use anyhow::Result;
use anyhow::{Context as _, Result};
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use serde::Deserialize;
use crate::config::Config;
use crate::context::Context;
use crate::net::http::post_form;
use crate::net::read_url_blob;
use crate::provider;
use crate::provider::Oauth2Authorizer;
use crate::tools::time;
@@ -159,25 +161,19 @@ pub(crate) async fn get_oauth2_access_token(
// ... and POST
// All OAuth URLs are hardcoded HTTPS URLs,
// so it is safe to load DNS cache.
let load_cache = true;
let client = crate::net::http::get_client(context, load_cache).await?;
let response: Response = match client.post(post_url).form(&post_param).send().await {
Ok(resp) => match resp.json().await {
let response: Response = match post_form(context, post_url, &post_param).await {
Ok(resp) => match serde_json::from_slice(&resp) {
Ok(response) => response,
Err(err) => {
warn!(
context,
"Failed to parse OAuth2 JSON response from {}: error: {}", token_url, err
"Failed to parse OAuth2 JSON response from {token_url}: {err:#}."
);
return Ok(None);
}
},
Err(err) => {
warn!(context, "Error calling OAuth2 at {}: {:?}", token_url, err);
warn!(context, "Error calling OAuth2 at {token_url}: {err:#}.");
return Ok(None);
}
};
@@ -246,11 +242,20 @@ pub(crate) async fn get_oauth2_addr(
}
if let Some(access_token) = get_oauth2_access_token(context, addr, code, false).await? {
let addr_out = oauth2.get_addr(context, &access_token).await;
let addr_out = match oauth2.get_addr(context, &access_token).await {
Ok(addr) => addr,
Err(err) => {
warn!(context, "Error getting addr: {err:#}.");
None
}
};
if addr_out.is_none() {
// regenerate
if let Some(access_token) = get_oauth2_access_token(context, addr, code, true).await? {
Ok(oauth2.get_addr(context, &access_token).await)
Ok(oauth2
.get_addr(context, &access_token)
.await
.unwrap_or_default())
} else {
Ok(None)
}
@@ -282,7 +287,7 @@ impl Oauth2 {
None
}
async fn get_addr(&self, context: &Context, access_token: &str) -> Option<String> {
async fn get_addr(&self, context: &Context, access_token: &str) -> Result<Option<String>> {
let userinfo_url = self.get_userinfo.unwrap_or("");
let userinfo_url = replace_in_uri(userinfo_url, "$ACCESS_TOKEN", access_token);
@@ -294,44 +299,21 @@ impl Oauth2 {
// "picture": "https://lh4.googleusercontent.com/-Gj5jh_9R0BY/AAAAAAAAAAI/AAAAAAAAAAA/IAjtjfjtjNA/photo.jpg"
// }
// All OAuth URLs are hardcoded HTTPS URLs,
// so it is safe to load DNS cache.
let load_cache = true;
let client = match crate::net::http::get_client(context, load_cache).await {
Ok(cl) => cl,
Err(err) => {
warn!(context, "failed to get HTTP client: {}", err);
return None;
}
};
let response = match client.get(userinfo_url).send().await {
Ok(response) => response,
Err(err) => {
warn!(context, "failed to get userinfo: {}", err);
return None;
}
};
let response: Result<HashMap<String, serde_json::Value>, _> = response.json().await;
let parsed = match response {
Ok(parsed) => parsed,
Err(err) => {
warn!(context, "Error getting userinfo: {}", err);
return None;
}
};
let response = read_url_blob(context, &userinfo_url).await?;
let parsed: HashMap<String, serde_json::Value> =
serde_json::from_slice(&response.blob).context("Error getting userinfo")?;
// CAVE: serde_json::Value.as_str() removes the quotes of json-strings
// but serde_json::Value.to_string() does not!
if let Some(addr) = parsed.get("email") {
if let Some(s) = addr.as_str() {
Some(s.to_string())
Ok(Some(s.to_string()))
} else {
warn!(context, "E-mail in userinfo is not a string: {}", addr);
None
Ok(None)
}
} else {
warn!(context, "E-mail missing in userinfo.");
None
Ok(None)
}
}
}

View File

@@ -1,6 +1,6 @@
//! [Provider database](https://providers.delta.chat/) module.
mod data;
pub(crate) mod data;
use anyhow::Result;
use deltachat_contact_tools::EmailAddress;

View File

@@ -874,6 +874,20 @@ static P_MEHL_CLOUD: Provider = Provider {
after_login_hint: "",
overview_page: "https://providers.delta.chat/mehl-cloud",
server: &[
Server {
protocol: Imap,
socket: Ssl,
hostname: "mehl.cloud",
port: 443,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "mehl.cloud",
port: 443,
username_pattern: Email,
},
Server {
protocol: Imap,
socket: Ssl,
@@ -1009,6 +1023,20 @@ static P_NINE_TESTRUN_ORG: Provider = Provider {
after_login_hint: "",
overview_page: "https://providers.delta.chat/nine-testrun-org",
server: &[
Server {
protocol: Imap,
socket: Ssl,
hostname: "nine.testrun.org",
port: 443,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "nine.testrun.org",
port: 443,
username_pattern: Email,
},
Server {
protocol: Imap,
socket: Ssl,
@@ -1037,20 +1065,6 @@ static P_NINE_TESTRUN_ORG: Provider = Provider {
port: 587,
username_pattern: Email,
},
Server {
protocol: Imap,
socket: Ssl,
hostname: "nine.testrun.org",
port: 443,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "nine.testrun.org",
port: 443,
username_pattern: Email,
},
],
opt: ProviderOptions::new(),
config_defaults: Some(&[ConfigDefault {
@@ -2417,4 +2431,4 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
});
pub static _PROVIDER_UPDATED: Lazy<chrono::NaiveDate> =
Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2024, 8, 14).unwrap());
Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2024, 8, 23).unwrap());

View File

@@ -61,16 +61,13 @@ impl PushSubscriber {
return Ok(());
};
let load_cache = true;
let response = http::get_client(context, load_cache)
.await?
.post("https://notifications.delta.chat/register")
.body(format!("{{\"token\":\"{token}\"}}"))
.send()
.await?;
let response_status = response.status();
if response_status.is_success() {
if http::post_string(
context,
"https://notifications.delta.chat/register",
format!("{{\"token\":\"{token}\"}}"),
)
.await?
{
state.heartbeat_subscribed = true;
}
Ok(())

246
src/qr.rs
View File

@@ -19,10 +19,10 @@ use crate::context::Context;
use crate::events::EventType;
use crate::key::Fingerprint;
use crate::message::Message;
use crate::net::http::post_empty;
use crate::peerstate::Peerstate;
use crate::token;
use crate::tools::validate_id;
use iroh_old as iroh;
const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase
const IDELTACHAT_SCHEME: &str = "https://i.delta.chat/#";
@@ -30,6 +30,7 @@ const IDELTACHAT_NOSLASH_SCHEME: &str = "https://i.delta.chat#";
const DCACCOUNT_SCHEME: &str = "DCACCOUNT:";
pub(super) const DCLOGIN_SCHEME: &str = "DCLOGIN:";
const DCWEBRTC_SCHEME: &str = "DCWEBRTC:";
const TG_SOCKS_SCHEME: &str = "https://t.me/socks";
const MAILTO_SCHEME: &str = "mailto:";
const MATMSG_SCHEME: &str = "MATMSG:";
const VCARD_SCHEME: &str = "BEGIN:VCARD";
@@ -37,9 +38,6 @@ const SMTP_SCHEME: &str = "SMTP:";
const HTTP_SCHEME: &str = "http://";
const HTTPS_SCHEME: &str = "https://";
/// Legacy backup transfer based on iroh 0.4.
pub(crate) const DCBACKUP_SCHEME: &str = "DCBACKUP:";
/// Backup transfer based on iroh-net.
pub(crate) const DCBACKUP2_SCHEME: &str = "DCBACKUP2:";
@@ -110,20 +108,6 @@ pub enum Qr {
domain: String,
},
/// Provides a backup that can be retrieved using legacy iroh 0.4.
///
/// This contains all the data needed to connect to a device and download a backup from
/// it to configure the receiving device with the same account.
Backup {
/// Printable version of the provider information.
///
/// This is the printable version of a `sendme` ticket, which contains all the
/// information to connect to and authenticate a backup provider.
///
/// The format is somewhat opaque, but `sendme` can deserialise this.
ticket: iroh::provider::Ticket,
},
/// Provides a backup that can be retrieved using iroh-net based backup transfer protocol.
Backup2 {
/// Iroh node address.
@@ -142,6 +126,21 @@ pub enum Qr {
instance_pattern: String,
},
/// Ask the user if they want to add or use the given SOCKS5 proxy
Socks5Proxy {
/// SOCKS5 server
host: String,
/// SOCKS5 port
port: u16,
/// SOCKS5 user
user: Option<String>,
/// SOCKS5 password
pass: Option<String>,
},
/// Contact address is scanned.
///
/// Optionally, a draft message could be provided.
@@ -277,8 +276,8 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
dclogin_scheme::decode_login(qr)?
} else if starts_with_ignore_case(qr, DCWEBRTC_SCHEME) {
decode_webrtc_instance(context, qr)?
} else if starts_with_ignore_case(qr, DCBACKUP_SCHEME) {
decode_backup(qr)?
} else if starts_with_ignore_case(qr, TG_SOCKS_SCHEME) {
decode_tg_socks_proxy(context, qr)?
} else if starts_with_ignore_case(qr, DCBACKUP2_SCHEME) {
decode_backup2(qr)?
} else if qr.starts_with(MAILTO_SCHEME) {
@@ -301,7 +300,7 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
Ok(qrcode)
}
/// Formats the text of the [`Qr::Backup`] variant.
/// Formats the text of the [`Qr::Backup2`] variant.
///
/// This is the inverse of [`check_qr`] for that variant only.
///
@@ -309,7 +308,6 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
/// into `FromStr`.
pub fn format_backup(qr: &Qr) -> Result<String> {
match qr {
Qr::Backup { ref ticket } => Ok(format!("{DCBACKUP_SCHEME}{ticket}")),
Qr::Backup2 {
ref node_addr,
ref auth_token,
@@ -539,16 +537,37 @@ fn decode_webrtc_instance(_context: &Context, qr: &str) -> Result<Qr> {
}
}
/// Decodes a [`DCBACKUP_SCHEME`] QR code.
///
/// The format of this scheme is `DCBACKUP:<encoded ticket>`. The encoding is the
/// [`iroh::provider::Ticket`]'s `Display` impl.
fn decode_backup(qr: &str) -> Result<Qr> {
let payload = qr
.strip_prefix(DCBACKUP_SCHEME)
.ok_or_else(|| anyhow!("invalid DCBACKUP scheme"))?;
let ticket: iroh::provider::Ticket = payload.parse().context("invalid DCBACKUP payload")?;
Ok(Qr::Backup { ticket })
/// scheme: `https://t.me/socks?server=foo&port=123` or `https://t.me/socks?server=1.2.3.4&port=123`
fn decode_tg_socks_proxy(_context: &Context, qr: &str) -> Result<Qr> {
let url = url::Url::parse(qr).context("Invalid t.me/socks url")?;
const SOCKS5_DEFAULT_PORT: u16 = 1080;
let mut host: Option<String> = None;
let mut port: u16 = SOCKS5_DEFAULT_PORT;
let mut user: Option<String> = None;
let mut pass: Option<String> = None;
for (key, value) in url.query_pairs() {
if key == "server" {
host = Some(value.to_string());
} else if key == "port" {
port = value.parse().unwrap_or(SOCKS5_DEFAULT_PORT);
} else if key == "user" {
user = Some(value.to_string());
} else if key == "pass" {
pass = Some(value.to_string());
}
}
if let Some(host) = host {
Ok(Qr::Socks5Proxy {
host,
port,
user,
pass,
})
} else {
bail!("Bad t.me/socks url: {:?}", url);
}
}
/// Decodes a [`DCBACKUP2_SCHEME`] QR code.
@@ -594,21 +613,8 @@ async fn set_account_from_qr(context: &Context, qr: &str) -> Result<()> {
bail!("DCACCOUNT QR codes must use HTTPS scheme");
}
// As only HTTPS is used, it is safe to load DNS cache.
let load_cache = true;
let response = crate::net::http::get_client(context, load_cache)
.await?
.post(url_str)
.send()
.await?;
let response_status = response.status();
let response_text = response
.text()
.await
.context("Cannot create account, request failed: empty response")?;
if response_status.is_success() {
let (response_text, response_success) = post_empty(context, url_str).await?;
if response_success {
let CreateAccountSuccessResponse { password, email } = serde_json::from_str(&response_text)
.with_context(|| {
format!("Cannot create account, response is malformed:\n{response_text:?}")
@@ -649,6 +655,29 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
.set_config_internal(Config::WebrtcInstance, Some(&instance_pattern))
.await?;
}
Qr::Socks5Proxy {
host,
port,
user,
pass,
} => {
// disable proxy before changing settings to not use a combination of old and new
context
.set_config_bool(Config::Socks5Enabled, false)
.await?;
context.set_config(Config::Socks5Host, Some(&host)).await?;
context
.set_config_u32(Config::Socks5Port, u32::from(port))
.await?;
context
.set_config(Config::Socks5User, user.as_deref())
.await?;
context
.set_config(Config::Socks5Password, pass.as_deref())
.await?;
context.set_config_bool(Config::Socks5Enabled, true).await?;
}
Qr::WithdrawVerifyContact {
invitenumber,
authcode,
@@ -870,6 +899,7 @@ mod tests {
use super::*;
use crate::aheader::EncryptPreference;
use crate::chat::{create_group_chat, ProtectionStatus};
use crate::config::Config;
use crate::key::DcKey;
use crate::securejoin::get_securejoin_qr;
use crate::test_utils::{alice_keypair, TestContext};
@@ -1478,6 +1508,73 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_tg_socks_proxy() -> Result<()> {
let t = TestContext::new().await;
let qr = check_qr(&t, "https://t.me/socks?server=84.53.239.95&port=4145").await?;
assert_eq!(
qr,
Qr::Socks5Proxy {
host: "84.53.239.95".to_string(),
port: 4145,
user: None,
pass: None,
}
);
let qr = check_qr(&t, "https://t.me/socks?server=foo.bar&port=123").await?;
assert_eq!(
qr,
Qr::Socks5Proxy {
host: "foo.bar".to_string(),
port: 123,
user: None,
pass: None,
}
);
let qr = check_qr(&t, "https://t.me/socks?server=foo.baz").await?;
assert_eq!(
qr,
Qr::Socks5Proxy {
host: "foo.baz".to_string(),
port: 1080,
user: None,
pass: None,
}
);
let qr = check_qr(
&t,
"https://t.me/socks?server=foo.baz&port=12345&user=ada&pass=ms%21%2F%24",
)
.await?;
assert_eq!(
qr,
Qr::Socks5Proxy {
host: "foo.baz".to_string(),
port: 12345,
user: Some("ada".to_string()),
pass: Some("ms!/$".to_string()),
}
);
// wrong domain results in Qr:Url instead of Qr::Socks5Proxy
let qr = check_qr(&t, "https://not.me/socks?noserver=84.53.239.95&port=4145").await?;
assert_eq!(
qr,
Qr::Url {
url: "https://not.me/socks?noserver=84.53.239.95&port=4145".to_string()
}
);
let qr = check_qr(&t, "https://t.me/socks?noserver=84.53.239.95&port=4145").await;
assert!(qr.is_err());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_account_bad_scheme() {
let ctx = TestContext::new().await;
@@ -1498,7 +1595,7 @@ mod tests {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_config_from_qr() -> Result<()> {
async fn test_set_webrtc_instance_config_from_qr() -> Result<()> {
let ctx = TestContext::new().await;
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await?.is_none());
@@ -1528,4 +1625,57 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_socks5_proxy_config_from_qr() -> Result<()> {
let t = TestContext::new().await;
assert_eq!(t.get_config_bool(Config::Socks5Enabled).await?, false);
let res = set_config_from_qr(&t, "https://t.me/socks?server=foo&port=666").await;
assert!(res.is_ok());
assert_eq!(t.get_config_bool(Config::Socks5Enabled).await?, true);
assert_eq!(
t.get_config(Config::Socks5Host).await?,
Some("foo".to_string())
);
assert_eq!(t.get_config_u32(Config::Socks5Port).await?, 666);
assert_eq!(t.get_config(Config::Socks5User).await?, None);
assert_eq!(t.get_config(Config::Socks5Password).await?, None);
// make sure, user&password are reset when not specified in the URL
t.set_config(Config::Socks5User, Some("alice")).await?;
t.set_config(Config::Socks5Password, Some("secret")).await?;
let res = set_config_from_qr(&t, "https://t.me/socks?server=1.2.3.4").await;
assert!(res.is_ok());
assert_eq!(t.get_config_bool(Config::Socks5Enabled).await?, true);
assert_eq!(
t.get_config(Config::Socks5Host).await?,
Some("1.2.3.4".to_string())
);
assert_eq!(t.get_config_u32(Config::Socks5Port).await?, 1080);
assert_eq!(t.get_config(Config::Socks5User).await?, None);
assert_eq!(t.get_config(Config::Socks5Password).await?, None);
// make sure, user&password are set when specified in the URL
let res =
set_config_from_qr(&t, "https://t.me/socks?server=jau&user=Da&pass=x%26%25%24X").await;
assert!(res.is_ok());
assert_eq!(t.get_config_bool(Config::Socks5Enabled).await?, true);
assert_eq!(
t.get_config(Config::Socks5Host).await?,
Some("jau".to_string())
);
assert_eq!(t.get_config_u32(Config::Socks5Port).await?, 1080);
assert_eq!(
t.get_config(Config::Socks5User).await?,
Some("Da".to_string())
);
assert_eq!(
t.get_config(Config::Socks5Password).await?,
Some("x&%$X".to_string())
);
Ok(())
}
}

View File

@@ -8,7 +8,7 @@ use num_traits::cast::ToPrimitive;
use super::{Qr, DCLOGIN_SCHEME};
use crate::config::Config;
use crate::context::Context;
use crate::login_param::CertificateChecks;
use crate::login_param::EnteredCertificateChecks;
use crate::provider::Socket;
/// Options for `dclogin:` scheme.
@@ -55,7 +55,7 @@ pub enum LoginOptions {
smtp_security: Option<Socket>,
/// Certificate checks.
certificate_checks: Option<CertificateChecks>,
certificate_checks: Option<EnteredCertificateChecks>,
},
}
@@ -146,11 +146,12 @@ fn parse_socket_security(security: Option<&String>) -> Result<Option<Socket>> {
fn parse_certificate_checks(
certificate_checks: Option<&String>,
) -> Result<Option<CertificateChecks>> {
) -> Result<Option<EnteredCertificateChecks>> {
Ok(match certificate_checks.map(|s| s.as_str()) {
Some("0") => Some(CertificateChecks::Automatic),
Some("1") => Some(CertificateChecks::Strict),
Some("3") => Some(CertificateChecks::AcceptInvalidCertificates),
Some("0") => Some(EnteredCertificateChecks::Automatic),
Some("1") => Some(EnteredCertificateChecks::Strict),
Some("2") => Some(EnteredCertificateChecks::AcceptInvalidCertificates),
Some("3") => Some(EnteredCertificateChecks::AcceptInvalidCertificates2),
Some(other) => bail!("Unknown certificatecheck level: {}", other),
None => None,
})
@@ -263,7 +264,7 @@ mod test {
use anyhow::bail;
use super::{decode_login, LoginOptions};
use crate::{login_param::CertificateChecks, provider::Socket, qr::Qr};
use crate::{login_param::EnteredCertificateChecks, provider::Socket, qr::Qr};
macro_rules! login_options_just_pw {
($pw: expr) => {
@@ -386,7 +387,7 @@ mod test {
smtp_username: Some("max@host.tld".to_owned()),
smtp_password: Some("3242HS".to_owned()),
smtp_security: Some(Socket::Plain),
certificate_checks: Some(CertificateChecks::Strict),
certificate_checks: Some(EnteredCertificateChecks::Strict),
}
);
} else {

View File

@@ -369,60 +369,42 @@ pub(crate) async fn handle_securejoin_handshake(
==========================================================*/
// verify that Secure-Join-Fingerprint:-header matches the fingerprint of Bob
let fingerprint: Fingerprint =
match mime_message.get_header(HeaderDef::SecureJoinFingerprint) {
Some(fp) => fp.parse()?,
None => {
could_not_establish_secure_connection(
context,
contact_id,
info_chat_id(context, contact_id).await?,
"Fingerprint not provided.",
)
.await?;
return Ok(HandshakeMessage::Ignore);
}
};
if !encrypted_and_signed(context, mime_message, Some(&fingerprint)) {
could_not_establish_secure_connection(
let Some(fp) = mime_message.get_header(HeaderDef::SecureJoinFingerprint) else {
warn!(
context,
contact_id,
info_chat_id(context, contact_id).await?,
"Auth not encrypted.",
)
.await?;
"Ignoring {step} message because fingerprint is not provided."
);
return Ok(HandshakeMessage::Ignore);
};
let fingerprint: Fingerprint = fp.parse()?;
if !encrypted_and_signed(context, mime_message, Some(&fingerprint)) {
warn!(
context,
"Ignoring {step} message because the message is not encrypted."
);
return Ok(HandshakeMessage::Ignore);
}
if !verify_sender_by_fingerprint(context, &fingerprint, contact_id).await? {
could_not_establish_secure_connection(
warn!(
context,
contact_id,
info_chat_id(context, contact_id).await?,
"Fingerprint mismatch on inviter-side.",
)
.await?;
"Ignoring {step} message because of fingerprint mismatch."
);
return Ok(HandshakeMessage::Ignore);
}
info!(context, "Fingerprint verified.",);
// verify that the `Secure-Join-Auth:`-header matches the secret written to the QR code
let Some(auth) = mime_message.get_header(HeaderDef::SecureJoinAuth) else {
could_not_establish_secure_connection(
warn!(
context,
contact_id,
info_chat_id(context, contact_id).await?,
"Auth not provided.",
)
.await?;
"Ignoring {step} message because of missing auth code."
);
return Ok(HandshakeMessage::Ignore);
};
let Some(group_chat_id) = token::auth_chat_id(context, auth).await? else {
could_not_establish_secure_connection(
warn!(
context,
contact_id,
info_chat_id(context, contact_id).await?,
"Auth invalid.",
)
.await?;
"Ignoring {step} message because of invalid auth code."
);
return Ok(HandshakeMessage::Ignore);
};
@@ -439,13 +421,10 @@ pub(crate) async fn handle_securejoin_handshake(
)
.await?;
if !fingerprint_found {
could_not_establish_secure_connection(
warn!(
context,
contact_id,
info_chat_id(context, contact_id).await?,
"Fingerprint mismatch on inviter-side.",
)
.await?;
"Ignoring {step} message because of the failure to find matching peerstate."
);
return Ok(HandshakeMessage::Ignore);
}
contact_id.regossip_keys(context).await?;
@@ -781,6 +760,7 @@ mod tests {
CheckProtectionTimestamp,
WrongAliceGossip,
SecurejoinWaitTimeout,
AliceIsBot,
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -803,6 +783,11 @@ mod tests {
test_setup_contact_ex(SetupContactCase::SecurejoinWaitTimeout).await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_setup_contact_alice_is_bot() {
test_setup_contact_ex(SetupContactCase::AliceIsBot).await
}
async fn test_setup_contact_ex(case: SetupContactCase) {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
@@ -811,13 +796,19 @@ mod tests {
bob.set_config(Config::Displayname, Some("Bob Examplenet"))
.await
.unwrap();
alice
.set_config(Config::VerifiedOneOnOneChats, Some("1"))
.await
.unwrap();
bob.set_config(Config::VerifiedOneOnOneChats, Some("1"))
.await
.unwrap();
let alice_auto_submitted_hdr;
match case {
SetupContactCase::AliceIsBot => {
alice.set_config_bool(Config::Bot, true).await.unwrap();
alice_auto_submitted_hdr = "Auto-Submitted: auto-generated";
}
_ => alice_auto_submitted_hdr = "Auto-Submitted: auto-replied",
};
for t in [&alice, &bob] {
t.set_config_bool(Config::VerifiedOneOnOneChats, true)
.await
.unwrap();
}
assert_eq!(
Chatlist::try_load(&alice, 0, None, None)
@@ -853,6 +844,7 @@ mod tests {
assert!(!msg.was_encrypted());
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vc-request");
assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some());
assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none());
// Step 3: Alice receives vc-request, sends vc-auth-required
alice.recv_msg_trash(&sent).await;
@@ -865,6 +857,7 @@ mod tests {
);
let sent = alice.pop_sent_msg().await;
assert!(sent.payload.contains(alice_auto_submitted_hdr));
assert!(!sent.payload.contains("Alice Exampleorg"));
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
@@ -911,6 +904,7 @@ mod tests {
// Check Bob sent the right message.
let sent = bob.pop_sent_msg().await;
assert!(sent.payload.contains("Auto-Submitted: auto-replied"));
assert!(!sent.payload.contains("Bob Examplenet"));
let mut msg = alice.parse_msg(&sent).await;
let vc_request_with_auth_ts_sent = msg
@@ -976,6 +970,7 @@ mod tests {
.await
.unwrap();
assert_eq!(contact_bob.get_authname(), "Bob Examplenet");
assert_eq!(contact_bob.is_bot(), false);
// exactly one one-to-one chat should be visible for both now
// (check this before calling alice.create_chat() explicitly below)
@@ -1015,6 +1010,7 @@ mod tests {
// Check Alice sent the right message to Bob.
let sent = alice.pop_sent_msg().await;
assert!(sent.payload.contains(alice_auto_submitted_hdr));
assert!(!sent.payload.contains("Alice Exampleorg"));
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
@@ -1033,6 +1029,7 @@ mod tests {
.await
.unwrap();
assert_eq!(contact_alice.get_authname(), "Alice Exampleorg");
assert_eq!(contact_alice.is_bot(), case == SetupContactCase::AliceIsBot);
if case != SetupContactCase::SecurejoinWaitTimeout {
// Later we check that the timeout message isn't added to the already protected chat.
@@ -1247,6 +1244,7 @@ mod tests {
assert!(!msg.was_encrypted());
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vg-request");
assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some());
assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none());
// Old Delta Chat core sent `Secure-Join-Group` header in `vg-request`,
// but it was only used by Alice in `vg-request-with-auth`.
@@ -1260,6 +1258,7 @@ mod tests {
alice.recv_msg_trash(&sent).await;
let sent = alice.pop_sent_msg().await;
assert!(sent.payload.contains("Auto-Submitted: auto-replied"));
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
@@ -1293,6 +1292,7 @@ mod tests {
}
// Check Bob sent the right handshake message.
assert!(sent.payload.contains("Auto-Submitted: auto-replied"));
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
@@ -1325,6 +1325,10 @@ mod tests {
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vg-member-added"
);
// Formally this message is auto-submitted, but as the member addition is a result of an
// explicit user action, the Auto-Submitted header shouldn't be present. Otherwise it would
// be strange to have it in "member-added" messages of verified groups only.
assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none());
{
// Now Alice's chat with Bob should still be hidden, the verified message should
@@ -1451,13 +1455,11 @@ First thread."#;
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
alice
.set_config(Config::VerifiedOneOnOneChats, Some("1"))
.await
.unwrap();
bob.set_config(Config::VerifiedOneOnOneChats, Some("1"))
.await
.unwrap();
for t in [&alice, &bob] {
t.set_config_bool(Config::VerifiedOneOnOneChats, true)
.await
.unwrap();
}
let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap();
join_securejoin(&bob.ctx, &qr).await.unwrap();

View File

@@ -117,7 +117,7 @@ pub(crate) fn simplify(mut input: String, is_chat_message: bool) -> SimplifiedTe
let lines = split_lines(&input);
let (lines, is_forwarded) = skip_forward_header(&lines);
let (lines, mut top_quote) = remove_top_quote(lines);
let (lines, mut top_quote) = remove_top_quote(lines, is_chat_message);
let original_lines = &lines;
let (lines, footer_lines) = remove_message_footer(lines);
let footer = footer_lines.map(|footer_lines| render_message(footer_lines, false));
@@ -210,7 +210,10 @@ fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>)
}
#[allow(clippy::indexing_slicing)]
fn remove_top_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>) {
fn remove_top_quote<'a>(
lines: &'a [&str],
is_chat_message: bool,
) -> (&'a [&'a str], Option<String>) {
let mut first_quoted_line = 0;
let mut last_quoted_line = None;
let mut has_quoted_headline = false;
@@ -220,7 +223,11 @@ fn remove_top_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>) {
first_quoted_line = l;
}
last_quoted_line = Some(l)
} else if is_quoted_headline(line) && !has_quoted_headline && last_quoted_line.is_none() {
} else if !is_chat_message
&& is_quoted_headline(line)
&& !has_quoted_headline
&& last_quoted_line.is_none()
{
has_quoted_headline = true
} else {
/* non-quoting line found */
@@ -396,17 +403,34 @@ mod tests {
#[test]
fn test_remove_top_quote() {
let (lines, top_quote) = remove_top_quote(&["> first", "> second"]);
let (lines, top_quote) = remove_top_quote(&["> first", "> second"], true);
assert!(lines.is_empty());
assert_eq!(top_quote.unwrap(), "first\nsecond");
let (lines, top_quote) = remove_top_quote(&["> first", "> second", "not a quote"]);
let (lines, top_quote) = remove_top_quote(&["> first", "> second", "not a quote"], true);
assert_eq!(lines, &["not a quote"]);
assert_eq!(top_quote.unwrap(), "first\nsecond");
let (lines, top_quote) = remove_top_quote(&["not a quote", "> first", "> second"]);
let (lines, top_quote) = remove_top_quote(&["not a quote", "> first", "> second"], true);
assert_eq!(lines, &["not a quote", "> first", "> second"]);
assert!(top_quote.is_none());
let (lines, top_quote) = remove_top_quote(
&["On 2024-08-28, Bob wrote:", "> quote", "not a quote"],
false,
);
assert_eq!(lines, &["not a quote"]);
assert_eq!(top_quote.unwrap(), "quote");
let (lines, top_quote) = remove_top_quote(
&["On 2024-08-28, Bob wrote:", "> quote", "not a quote"],
true,
);
assert_eq!(
lines,
&["On 2024-08-28, Bob wrote:", "> quote", "not a quote"]
);
assert!(top_quote.is_none());
}
#[test]

View File

@@ -5,7 +5,7 @@ pub mod send;
use anyhow::{bail, format_err, Context as _, Error, Result};
use async_smtp::response::{Category, Code, Detail};
use async_smtp::{self as smtp, EmailAddress, SmtpTransport};
use async_smtp::{EmailAddress, SmtpTransport};
use tokio::task;
use crate::chat::{add_info_msg_with_cmd, ChatId};
@@ -13,12 +13,12 @@ use crate::config::Config;
use crate::contact::{Contact, ContactId};
use crate::context::Context;
use crate::events::EventType;
use crate::login_param::{LoginParam, ServerLoginParam};
use crate::login_param::prioritize_server_login_params;
use crate::login_param::{ConfiguredLoginParam, ConfiguredServerLoginParam};
use crate::message::Message;
use crate::message::{self, MsgId};
use crate::mimefactory::MimeFactory;
use crate::net::session::SessionBufStream;
use crate::oauth2::get_oauth2_access_token;
use crate::scheduler::connectivity::ConnectivityStore;
use crate::socks::Socks5Config;
use crate::sql;
@@ -88,96 +88,76 @@ impl Smtp {
}
self.connectivity.set_connecting(context).await;
let lp = LoginParam::load_configured_params(context).await?;
let lp = ConfiguredLoginParam::load(context)
.await?
.context("Not configured")?;
self.connect(
context,
&lp.smtp,
&lp.smtp_password,
&lp.socks5_config,
&lp.addr,
lp.strict_tls(),
lp.oauth2,
)
.await
}
/// Connect using the provided login params.
#[allow(clippy::too_many_arguments)]
pub async fn connect(
&mut self,
context: &Context,
lp: &ServerLoginParam,
login_params: &[ConfiguredServerLoginParam],
password: &str,
socks5_config: &Option<Socks5Config>,
addr: &str,
strict_tls: bool,
oauth2: bool,
) -> Result<()> {
if self.is_connected() {
warn!(context, "SMTP already connected.");
return Ok(());
}
if lp.server.is_empty() || lp.port == 0 {
bail!("bad connection parameters");
}
let from = EmailAddress::new(addr.to_string())
.with_context(|| format!("invalid login address {addr}"))?;
.with_context(|| format!("Invalid address {addr:?}"))?;
self.from = Some(from);
let domain = &lp.server;
let port = lp.port;
let session_stream = connect::connect_stream(
context,
domain,
port,
strict_tls,
socks5_config.clone(),
lp.security,
)
.await?;
let client = smtp::SmtpClient::new().smtp_utf8(true).without_greeting();
let mut transport = SmtpTransport::new(client, session_stream).await?;
// Authenticate.
{
let (creds, mechanism) = if lp.oauth2 {
// oauth2
let send_pw = &lp.password;
let access_token = get_oauth2_access_token(context, addr, send_pw, false).await?;
if access_token.is_none() {
bail!("SMTP OAuth 2 error {}", addr);
let login_params =
prioritize_server_login_params(&context.sql, login_params, "smtp").await?;
for lp in login_params {
info!(context, "SMTP trying to connect to {}.", &lp.connection);
let transport = match connect::connect_and_auth(
context,
socks5_config,
strict_tls,
lp.connection.clone(),
oauth2,
addr,
&lp.user,
password,
)
.await
{
Ok(transport) => transport,
Err(err) => {
warn!(context, "SMTP failed to connect: {err:#}.");
continue;
}
let user = &lp.user;
(
smtp::authentication::Credentials::new(
user.to_string(),
access_token.unwrap_or_default(),
),
vec![smtp::authentication::Mechanism::Xoauth2],
)
} else {
// plain
let user = lp.user.clone();
let pw = lp.password.clone();
(
smtp::authentication::Credentials::new(user, pw),
vec![
smtp::authentication::Mechanism::Plain,
smtp::authentication::Mechanism::Login,
],
)
};
transport.try_login(&creds, &mechanism).await?;
self.transport = Some(transport);
self.last_success = Some(tools::Time::now());
context.emit_event(EventType::SmtpConnected(format!(
"SMTP-LOGIN as {} ok",
lp.user,
)));
return Ok(());
}
self.transport = Some(transport);
self.last_success = Some(tools::Time::now());
context.emit_event(EventType::SmtpConnected(format!(
"SMTP-LOGIN as {} ok",
lp.user,
)));
Ok(())
Err(format_err!("SMTP failed to connect"))
}
}

View File

@@ -2,17 +2,19 @@
use std::net::SocketAddr;
use anyhow::{bail, format_err, Context as _, Result};
use anyhow::{bail, Context as _, Result};
use async_smtp::{SmtpClient, SmtpTransport};
use tokio::io::BufStream;
use crate::context::Context;
use crate::login_param::{ConnectionCandidate, ConnectionSecurity};
use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp};
use crate::net::session::SessionBufStream;
use crate::net::tls::wrap_tls;
use crate::net::update_connection_history;
use crate::net::{connect_tcp_inner, connect_tls_inner};
use crate::provider::Socket;
use crate::net::{
connect_tcp_inner, connect_tls_inner, run_connection_attempts, update_connection_history,
};
use crate::oauth2::get_oauth2_access_token;
use crate::socks::Socks5Config;
use crate::tools::time;
@@ -26,6 +28,95 @@ fn alpn(port: u16) -> &'static [&'static str] {
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn connect_and_auth(
context: &Context,
socks5_config: &Option<Socks5Config>,
strict_tls: bool,
candidate: ConnectionCandidate,
oauth2: bool,
addr: &str,
user: &str,
password: &str,
) -> Result<SmtpTransport<Box<dyn SessionBufStream>>> {
let session_stream =
connect_stream(context, socks5_config.clone(), strict_tls, candidate).await?;
let client = async_smtp::SmtpClient::new()
.smtp_utf8(true)
.without_greeting();
let mut transport = SmtpTransport::new(client, session_stream).await?;
// Authenticate.
let (creds, mechanism) = if oauth2 {
// oauth2
let access_token = get_oauth2_access_token(context, addr, password, false).await?;
if access_token.is_none() {
bail!("SMTP OAuth 2 error {}", addr);
}
(
async_smtp::authentication::Credentials::new(
user.to_string(),
access_token.unwrap_or_default(),
),
vec![async_smtp::authentication::Mechanism::Xoauth2],
)
} else {
// plain
(
async_smtp::authentication::Credentials::new(user.to_string(), password.to_string()),
vec![
async_smtp::authentication::Mechanism::Plain,
async_smtp::authentication::Mechanism::Login,
],
)
};
transport.try_login(&creds, &mechanism).await?;
Ok(transport)
}
async fn connection_attempt(
context: Context,
host: String,
security: ConnectionSecurity,
resolved_addr: SocketAddr,
strict_tls: bool,
) -> Result<Box<dyn SessionBufStream>> {
let context = &context;
let host = &host;
info!(
context,
"Attempting SMTP connection to {host} ({resolved_addr})."
);
let res = match security {
ConnectionSecurity::Tls => connect_secure(resolved_addr, host, strict_tls).await,
ConnectionSecurity::Starttls => connect_starttls(resolved_addr, host, strict_tls).await,
ConnectionSecurity::Plain => connect_insecure(resolved_addr).await,
};
match res {
Ok(stream) => {
let ip_addr = resolved_addr.ip().to_string();
let port = resolved_addr.port();
let save_cache = match security {
ConnectionSecurity::Tls | ConnectionSecurity::Starttls => strict_tls,
ConnectionSecurity::Plain => false,
};
if save_cache {
update_connect_timestamp(context, host, &ip_addr).await?;
}
update_connection_history(context, "smtp", host, port, &ip_addr, time()).await?;
Ok(stream)
}
Err(err) => {
warn!(
context,
"Failed to connect to {host} ({resolved_addr}): {err:#}."
);
Err(err)
}
}
}
/// Returns TLS, STARTTLS or plaintext connection
/// using SOCKS5 or direct connection depending on the given configuration.
///
@@ -34,59 +125,46 @@ fn alpn(port: u16) -> &'static [&'static str] {
/// does not send welcome message over TLS connection
/// after establishing it, welcome message is always ignored
/// to unify the result regardless of whether TLS or STARTTLS is used.
pub(crate) async fn connect_stream(
async fn connect_stream(
context: &Context,
host: &str,
port: u16,
strict_tls: bool,
socks5_config: Option<Socks5Config>,
security: Socket,
strict_tls: bool,
candidate: ConnectionCandidate,
) -> Result<Box<dyn SessionBufStream>> {
let host = &candidate.host;
let port = candidate.port;
let security = candidate.security;
if let Some(socks5_config) = socks5_config {
let stream = match security {
Socket::Automatic => bail!("SMTP port security is not configured"),
Socket::Ssl => {
ConnectionSecurity::Tls => {
connect_secure_socks5(context, host, port, strict_tls, socks5_config.clone())
.await?
}
Socket::Starttls => {
ConnectionSecurity::Starttls => {
connect_starttls_socks5(context, host, port, strict_tls, socks5_config.clone())
.await?
}
Socket::Plain => {
ConnectionSecurity::Plain => {
connect_insecure_socks5(context, host, port, socks5_config.clone()).await?
}
};
Ok(stream)
} else {
let mut first_error = None;
let load_cache = strict_tls && (security == Socket::Ssl || security == Socket::Starttls);
let load_cache = match security {
ConnectionSecurity::Tls | ConnectionSecurity::Starttls => strict_tls,
ConnectionSecurity::Plain => false,
};
for resolved_addr in lookup_host_with_cache(context, host, port, "smtp", load_cache).await?
{
let res = match security {
Socket::Automatic => bail!("SMTP port security is not configured"),
Socket::Ssl => connect_secure(resolved_addr, host, strict_tls).await,
Socket::Starttls => connect_starttls(resolved_addr, host, strict_tls).await,
Socket::Plain => connect_insecure(resolved_addr).await,
};
match res {
Ok(stream) => {
let ip_addr = resolved_addr.ip().to_string();
if load_cache {
update_connect_timestamp(context, host, &ip_addr).await?;
}
update_connection_history(context, "smtp", host, port, &ip_addr, time())
.await?;
return Ok(stream);
}
Err(err) => {
warn!(context, "Failed to connect to {resolved_addr}: {err:#}.");
first_error.get_or_insert(err);
}
}
}
Err(first_error.unwrap_or_else(|| format_err!("no DNS resolution results for {host}")))
let connection_futures = lookup_host_with_cache(context, host, port, "smtp", load_cache)
.await?
.into_iter()
.map(|resolved_addr| {
let context = context.clone();
let host = host.to_string();
connection_attempt(context, host, security, resolved_addr, strict_tls)
});
run_connection_attempts(connection_futures).await
}
}

View File

@@ -8,7 +8,6 @@ use fast_socks5::client::{Config, Socks5Stream};
use fast_socks5::util::target_addr::ToTargetAddr;
use fast_socks5::AuthenticationMethod;
use fast_socks5::Socks5Command;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use tokio::net::TcpStream;
use tokio_io_timeout::TimeoutStream;
@@ -54,20 +53,6 @@ impl Socks5Config {
}
}
/// Converts SOCKS5 configuration into URL.
pub fn to_url(&self) -> String {
// `socks5h` means that hostname is resolved into address by the proxy
// and DNS requests should not leak.
let mut url = "socks5h://".to_string();
if let Some((username, password)) = &self.user_password {
let username_urlencoded = utf8_percent_encode(username, NON_ALPHANUMERIC).to_string();
let password_urlencoded = utf8_percent_encode(password, NON_ALPHANUMERIC).to_string();
url += &format!("{username_urlencoded}:{password_urlencoded}@");
}
url += &format!("{}:{}", self.host, self.port);
url
}
/// If `load_dns_cache` is true, loads cached DNS resolution results.
/// Use this only if the connection is going to be protected with TLS checks.
pub async fn connect(
@@ -114,35 +99,3 @@ impl fmt::Display for Socks5Config {
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_socks5h_url() {
let config = Socks5Config {
host: "127.0.0.1".to_string(),
port: 9050,
user_password: None,
};
assert_eq!(config.to_url(), "socks5h://127.0.0.1:9050");
let config = Socks5Config {
host: "example.org".to_string(),
port: 1080,
user_password: Some(("root".to_string(), "toor".to_string())),
};
assert_eq!(config.to_url(), "socks5h://root:toor@example.org:1080");
let config = Socks5Config {
host: "example.org".to_string(),
port: 1080,
user_password: Some(("root".to_string(), "foo/?\\@".to_string())),
};
assert_eq!(
config.to_url(),
"socks5h://root:foo%2F%3F%5C%40@example.org:1080"
);
}
}

View File

@@ -1345,12 +1345,12 @@ pub(crate) async fn new_group_send_first_message(context: &Context) -> String {
translated(context, StockMessage::NewGroupSendFirstMessage).await
}
/// Text to put in the [`Qr::Backup`] rendered SVG image.
/// Text to put in the [`Qr::Backup2`] rendered SVG image.
///
/// The default is "Scan to set up second device for <account name (account addr)>". The
/// account name and address are looked up from the context.
///
/// [`Qr::Backup`]: crate::qr::Qr::Backup
/// [`Qr::Backup2`]: crate::qr::Qr::Backup2
pub(crate) async fn backup_transfer_qr(context: &Context) -> Result<String> {
let contact = Contact::get_by_id(context, ContactId::SELF).await?;
let addr = contact.get_addr();

View File

@@ -10,10 +10,10 @@ use std::path::Path;
use std::sync::Arc;
use std::time::{Duration, Instant};
use ansi_term::Color;
use async_channel::{self as channel, Receiver, Sender};
use chat::ChatItem;
use deltachat_contact_tools::{ContactAddress, EmailAddress};
use nu_ansi_term::Color;
use once_cell::sync::Lazy;
use pretty_assertions::assert_eq;
use rand::Rng;