mirror of
https://github.com/chatmail/core.git
synced 2026-04-07 08:02:11 +03:00
Compare commits
85 Commits
v1.142.4
...
link2xt/na
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
365afd9ac5 | ||
|
|
418dfbf994 | ||
|
|
533a872118 | ||
|
|
2ae854e8ea | ||
|
|
3969383857 | ||
|
|
e4ebb91712 | ||
|
|
eb3c1b3c25 | ||
|
|
c257482838 | ||
|
|
0a46e64971 | ||
|
|
845420cf17 | ||
|
|
96ea0db88e | ||
|
|
d99c735e12 | ||
|
|
d48f4100e9 | ||
|
|
7e73d5fdac | ||
|
|
152cdfe9bc | ||
|
|
a9eedafbcb | ||
|
|
5baf191483 | ||
|
|
2d2e703884 | ||
|
|
026450ddf3 | ||
|
|
5646782d23 | ||
|
|
dd1c2e836b | ||
|
|
be73076e9e | ||
|
|
9d47be0d8a | ||
|
|
fcf3dbbad4 | ||
|
|
d344cc3bdd | ||
|
|
93e181b2da | ||
|
|
3867808927 | ||
|
|
c7c3b9ca90 | ||
|
|
54cfc21e28 | ||
|
|
f01514dba4 | ||
|
|
ee5723416e | ||
|
|
aab8ef2726 | ||
|
|
84c1ffd7cc | ||
|
|
273158a337 | ||
|
|
099f0e2d18 | ||
|
|
2dd85afdc2 | ||
|
|
cdeca9ed9d | ||
|
|
af77c0c987 | ||
|
|
f912bc78e6 | ||
|
|
137ee9334c | ||
|
|
36e5e964e5 | ||
|
|
495337743a | ||
|
|
775edab7b1 | ||
|
|
fe9fa17005 | ||
|
|
ef12a76a9e | ||
|
|
6b3de9d7da | ||
|
|
3599e4be16 | ||
|
|
8dc844e194 | ||
|
|
104c60840a | ||
|
|
f2cb098148 | ||
|
|
30b998eca3 | ||
|
|
b5133fe8c8 | ||
|
|
0d0f556f21 | ||
|
|
0e365395bf | ||
|
|
08ec133aac | ||
|
|
7d7391887a | ||
|
|
e7d4ccffe2 | ||
|
|
8538a3c148 | ||
|
|
cb4b992204 | ||
|
|
af4d54ab50 | ||
|
|
1faff84905 | ||
|
|
62fde21d9a | ||
|
|
6f3729a00f | ||
|
|
fbf66ba02b | ||
|
|
ed74f4d1d9 | ||
|
|
a268946f8d | ||
|
|
7432c6de84 | ||
|
|
7fe9342d0d | ||
|
|
a0e89e4d4e | ||
|
|
0c3a476449 | ||
|
|
de517c15ff | ||
|
|
b83d5b0dbf | ||
|
|
27924a259f | ||
|
|
530256b1bf | ||
|
|
23d15d7485 | ||
|
|
3c38d2e105 | ||
|
|
a53ffcf5e3 | ||
|
|
22366cf246 | ||
|
|
ddc2b86875 | ||
|
|
9e966615f2 | ||
|
|
3335fc727d | ||
|
|
00d7b38e02 | ||
|
|
2a8a98c432 | ||
|
|
13841491d4 | ||
|
|
2137c05cd6 |
7
.github/dependabot.yml
vendored
7
.github/dependabot.yml
vendored
@@ -7,3 +7,10 @@ updates:
|
||||
commit-message:
|
||||
prefix: "chore(cargo)"
|
||||
open-pull-requests-limit: 50
|
||||
|
||||
# Keep GitHub Actions up to date.
|
||||
# <https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot>
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
2
.github/workflows/dependabot.yml
vendored
2
.github/workflows/dependabot.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
steps:
|
||||
- name: Dependabot metadata
|
||||
id: metadata
|
||||
uses: dependabot/fetch-metadata@v1.1.1
|
||||
uses: dependabot/fetch-metadata@v2.2.0
|
||||
with:
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
- name: Approve a PR
|
||||
|
||||
2
.github/workflows/node-docs.yml
vendored
2
.github/workflows/node-docs.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
mv docs js
|
||||
|
||||
- name: Upload
|
||||
uses: horochx/deploy-via-scp@v1.0.1
|
||||
uses: horochx/deploy-via-scp@1.1.0
|
||||
with:
|
||||
user: ${{ secrets.USERNAME }}
|
||||
key: ${{ secrets.KEY }}
|
||||
|
||||
2
.github/workflows/upload-docs.yml
vendored
2
.github/workflows/upload-docs.yml
vendored
@@ -74,7 +74,7 @@ jobs:
|
||||
show-progress: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
- name: npm install
|
||||
|
||||
94
CHANGELOG.md
94
CHANGELOG.md
@@ -1,5 +1,91 @@
|
||||
# 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
|
||||
|
||||
- Set backward verification when observing vc-contact-confirm or `vg-member-added` ([#5930](https://github.com/deltachat/deltachat-core-rust/pull/5930)).
|
||||
|
||||
## [1.142.10] - 2024-08-26
|
||||
|
||||
### Fixes
|
||||
|
||||
- Only include one From: header in securejoin messages ([#5917](https://github.com/deltachat/deltachat-core-rust/pull/5917)).
|
||||
|
||||
## [1.142.9] - 2024-08-24
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix reading of multiline SMTP greetings ([#5911](https://github.com/deltachat/deltachat-core-rust/pull/5911)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Update preloaded DNS cache.
|
||||
|
||||
## [1.142.8] - 2024-08-21
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not panic on unknown CertificateChecks values.
|
||||
|
||||
## [1.142.7] - 2024-08-17
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not save "Automatic" into configured_imap_certificate_checks. **This fixes regression introduced in core 1.142.4. Versions 1.142.4..1.142.6 should not be used in releases.**
|
||||
- Create a group unblocked for bot even if 1:1 chat is blocked ([#5514](https://github.com/deltachat/deltachat-core-rust/pull/5514)).
|
||||
- Update rpgp from 0.13.1 to 0.13.2 to fix "unable to decrypt" errors when sending messages to old Delta Chat clients and using Ed25519 keys to encrypt.
|
||||
- Do not request ALPN on standard ports and when using STARTTLS.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- jsonrpc: Add ContactObject::e2ee_avail.
|
||||
|
||||
### Tests
|
||||
|
||||
- Protected group for bot is auto-accepted.
|
||||
|
||||
## [1.142.6] - 2024-08-15
|
||||
|
||||
### Fixes
|
||||
|
||||
- Default to strict TLS checks if not configured.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- deltachat-rpc-client: Fix ruff 0.6.0 warnings.
|
||||
|
||||
## [1.142.5] - 2024-08-14
|
||||
|
||||
### Fixes
|
||||
|
||||
- Still try to create "INBOX.DeltaChat" if couldn't create "DeltaChat" ([#5870](https://github.com/deltachat/deltachat-core-rust/pull/5870)).
|
||||
- `store_seen_flags_on_imap`: Skip to next messages if couldn't select folder ([#5870](https://github.com/deltachat/deltachat-core-rust/pull/5870)).
|
||||
- Increase timeout for QR generation to 60s ([#5882](https://github.com/deltachat/deltachat-core-rust/pull/5882)).
|
||||
|
||||
### Documentation
|
||||
|
||||
- Document new `mdns_enabled` behavior (bots do not send MDNs by default).
|
||||
|
||||
### CI
|
||||
|
||||
- Configure Dependabot to update GitHub Actions.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Bump regex from 1.10.5 to 1.10.6.
|
||||
- cargo: Bump serde from 1.0.204 to 1.0.205.
|
||||
- deps: Bump horochx/deploy-via-scp from 1.0.1 to 1.1.0.
|
||||
- deps: Bump dependabot/fetch-metadata from 1.1.1 to 2.2.0.
|
||||
- deps: Bump actions/setup-node from 2 to 4.
|
||||
- Update provider database.
|
||||
|
||||
## [1.142.4] - 2024-08-09
|
||||
|
||||
### Build system
|
||||
@@ -4712,3 +4798,11 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.142.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.1...v1.142.2
|
||||
[1.142.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.2...v1.142.3
|
||||
[1.142.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.3...v1.142.4
|
||||
[1.142.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.4...v1.142.5
|
||||
[1.142.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.5...v1.142.6
|
||||
[1.142.7]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.6...v1.142.7
|
||||
[1.142.8]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.7...v1.142.8
|
||||
[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
|
||||
|
||||
1331
Cargo.lock
generated
1331
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
24
Cargo.toml
24
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.142.4"
|
||||
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 }
|
||||
@@ -73,17 +76,17 @@ num-traits = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
percent-encoding = "2.3"
|
||||
parking_lot = "0.12"
|
||||
pgp = { version = "0.13", default-features = false }
|
||||
pgp = { version = "0.13.2", default-features = false }
|
||||
qrcodegen = "1.7.0"
|
||||
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]
|
||||
|
||||
BIN
assets/certificates/imap.nauta.cu.der
Normal file
BIN
assets/certificates/imap.nauta.cu.der
Normal file
Binary file not shown.
BIN
assets/certificates/smtp.nauta.cu.der
Normal file
BIN
assets/certificates/smtp.nauta.cu.der
Normal file
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.142.4"
|
||||
version = "1.142.12"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -420,9 +420,10 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* and also recoded to a reasonable size.
|
||||
* - `e2ee_enabled` = 0=no end-to-end-encryption, 1=prefer end-to-end-encryption (default)
|
||||
* - `mdns_enabled` = 0=do not send or request read receipts,
|
||||
* 1=send and request read receipts (default)
|
||||
* - `bcc_self` = 0=do not send a copy of outgoing messages to self (default),
|
||||
* 1=send a copy of outgoing messages to self.
|
||||
* 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,
|
||||
* 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,
|
||||
@@ -2506,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
|
||||
@@ -2559,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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.142.4"
|
||||
version = "1.142.12"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
|
||||
@@ -1672,10 +1672,10 @@ impl CommandApi {
|
||||
///
|
||||
/// This call will block until the QR code is ready,
|
||||
/// even if there is no concurrent call to [`CommandApi::provide_backup`],
|
||||
/// but will fail after 10 seconds to avoid deadlocks.
|
||||
/// but will fail after 60 seconds to avoid deadlocks.
|
||||
async fn get_backup_qr(&self, account_id: u32) -> Result<String> {
|
||||
let qr = tokio::time::timeout(
|
||||
Duration::from_secs(10),
|
||||
Duration::from_secs(60),
|
||||
self.inner_get_backup_qr(account_id),
|
||||
)
|
||||
.await
|
||||
@@ -1691,13 +1691,13 @@ impl CommandApi {
|
||||
///
|
||||
/// This call will block until the QR code is ready,
|
||||
/// even if there is no concurrent call to [`CommandApi::provide_backup`],
|
||||
/// but will fail after 10 seconds to avoid deadlocks.
|
||||
/// but will fail after 60 seconds to avoid deadlocks.
|
||||
///
|
||||
/// Returns the QR code rendered as an SVG image.
|
||||
async fn get_backup_qr_svg(&self, account_id: u32) -> Result<String> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let qr = tokio::time::timeout(
|
||||
Duration::from_secs(10),
|
||||
Duration::from_secs(60),
|
||||
self.inner_get_backup_qr(account_id),
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -19,6 +19,7 @@ pub struct ContactObject {
|
||||
profile_image: Option<String>, // BLOBS
|
||||
name_and_addr: String,
|
||||
is_blocked: bool,
|
||||
e2ee_avail: bool,
|
||||
|
||||
/// True if the contact can be added to verified groups.
|
||||
///
|
||||
@@ -79,6 +80,7 @@ impl ContactObject {
|
||||
profile_image, //BLOBS
|
||||
name_and_addr: contact.get_name_n_addr(),
|
||||
is_blocked: contact.is_blocked(),
|
||||
e2ee_avail: contact.e2ee_avail(context).await?,
|
||||
is_verified,
|
||||
is_profile_verified,
|
||||
verifier_id,
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -58,5 +58,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.142.4"
|
||||
"version": "1.142.12"
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.142.4"
|
||||
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"] }
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "1.142.4"
|
||||
version = "1.142.12"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -114,13 +114,13 @@ class ACFactory:
|
||||
return to_client.run_until(lambda e: e.kind == EventType.INCOMING_MSG)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@pytest.fixture
|
||||
def rpc(tmp_path) -> AsyncGenerator:
|
||||
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
|
||||
with rpc_server:
|
||||
yield rpc_server
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@pytest.fixture
|
||||
def acfactory(rpc) -> AsyncGenerator:
|
||||
return ACFactory(DeltaChat(rpc))
|
||||
|
||||
@@ -12,10 +12,11 @@ import threading
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import EventType
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@pytest.fixture
|
||||
def path_to_webxdc(request):
|
||||
p = request.path.parent.parent.parent.joinpath("test-data/webxdc/chess.xdc")
|
||||
assert p.exists()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import Chat, EventType, SpecialContactId
|
||||
|
||||
|
||||
@@ -60,8 +61,15 @@ def test_qr_setup_contact_svg(acfactory) -> None:
|
||||
|
||||
|
||||
@pytest.mark.parametrize("protect", [True, False])
|
||||
def test_qr_securejoin(acfactory, protect):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
def test_qr_securejoin(acfactory, protect, tmp_path):
|
||||
alice, bob, fiona = acfactory.get_online_accounts(3)
|
||||
|
||||
# Setup second device for Alice
|
||||
# to test observing securejoin protocol.
|
||||
alice.export_backup(tmp_path)
|
||||
files = list(tmp_path.glob("*.tar"))
|
||||
alice2 = acfactory.get_unconfigured_account()
|
||||
alice2.import_backup(files[0])
|
||||
|
||||
logging.info("Alice creates a verified group")
|
||||
alice_chat = alice.create_group("Verified group", protect=protect)
|
||||
@@ -96,6 +104,21 @@ def test_qr_securejoin(acfactory, protect):
|
||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||
assert bob_contact_alice_snapshot.is_verified
|
||||
|
||||
# Start second Alice device.
|
||||
# Alice observes securejoin protocol and verifies Bob on second device.
|
||||
alice2.start_io()
|
||||
alice2.wait_for_securejoin_inviter_success()
|
||||
alice2_contact_bob = alice2.get_contact_by_addr(bob.get_config("addr"))
|
||||
alice2_contact_bob_snapshot = alice2_contact_bob.get_snapshot()
|
||||
assert alice2_contact_bob_snapshot.is_verified
|
||||
|
||||
# The QR code token is synced, so alice2 must be able to handle join requests.
|
||||
logging.info("Fiona joins verified group via alice2")
|
||||
alice.stop_io()
|
||||
fiona.secure_join(qr_code)
|
||||
alice2.wait_for_securejoin_inviter_success()
|
||||
fiona.wait_for_securejoin_joiner_success()
|
||||
|
||||
|
||||
def test_qr_securejoin_contact_request(acfactory) -> None:
|
||||
"""Alice invites Bob to a group when Bob's chat with Alice is in a contact request mode."""
|
||||
@@ -634,7 +657,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"))
|
||||
|
||||
@@ -3,11 +3,13 @@ import concurrent.futures
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import Contact, EventType, Message, events
|
||||
from deltachat_rpc_client.const import DownloadState, MessageState
|
||||
from deltachat_rpc_client.direct_imap import DirectImap
|
||||
@@ -69,6 +71,38 @@ def test_configure_starttls(acfactory) -> None:
|
||||
assert account.is_configured()
|
||||
|
||||
|
||||
def test_configure_ip(acfactory) -> None:
|
||||
account = acfactory.new_preconfigured_account()
|
||||
|
||||
domain = account.get_config("addr").rsplit("@")[-1]
|
||||
ip_address = socket.gethostbyname(domain)
|
||||
|
||||
# This should fail TLS check.
|
||||
account.set_config("mail_server", ip_address)
|
||||
with pytest.raises(JsonRpcError):
|
||||
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)
|
||||
|
||||
@@ -621,3 +655,24 @@ def test_get_http_response(acfactory):
|
||||
http_response = alice._rpc.get_http_response(alice.id, "https://example.org")
|
||||
assert http_response["mimetype"] == "text/html"
|
||||
assert b"<title>Example Domain</title>" in base64.b64decode((http_response["blob"] + "==").encode())
|
||||
|
||||
|
||||
def test_configured_imap_certificate_checks(acfactory):
|
||||
alice = acfactory.new_configured_account()
|
||||
configured_certificate_checks = alice.get_config("configured_imap_certificate_checks")
|
||||
|
||||
# Certificate checks should be configured (not None)
|
||||
assert configured_certificate_checks
|
||||
|
||||
# 0 is the value old Delta Chat core versions used
|
||||
# to mean user entered "imap_certificate_checks=0" (Automatic)
|
||||
# and configuration failed to use strict TLS checks
|
||||
# so it switched strict TLS checks off.
|
||||
#
|
||||
# New versions of Delta Chat are not disabling TLS checks
|
||||
# unless users explicitly disables them
|
||||
# or provider database says provider has invalid certificates.
|
||||
#
|
||||
# Core 1.142.4, 1.142.5 and 1.142.6 saved this value due to bug.
|
||||
# This test is a regression test to prevent this happening again.
|
||||
assert configured_certificate_checks != "0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.142.4"
|
||||
version = "1.142.12"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "1.142.4"
|
||||
"version": "1.142.12"
|
||||
}
|
||||
|
||||
38
deny.toml
38
deny.toml
@@ -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" },
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.4"
|
||||
"version": "1.142.12"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "1.142.4"
|
||||
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"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -1 +1 @@
|
||||
2024-08-09
|
||||
2024-09-02
|
||||
@@ -6,7 +6,7 @@ set -euo pipefail
|
||||
export TZ=UTC
|
||||
|
||||
# Provider database revision.
|
||||
REV=956475aff51caf577067de1f747a788899fd63fc
|
||||
REV=ab970e40d3979893c3bb6a93030e1a52223d7db6
|
||||
|
||||
CORE_ROOT="$PWD"
|
||||
TMP="$(mktemp -d)"
|
||||
|
||||
25
src/chat.rs
25
src/chat.rs
@@ -1934,8 +1934,8 @@ impl Chat {
|
||||
msg.param.set_int(Param::AttachGroupImage, 1);
|
||||
self.param.remove(Param::Unpromoted);
|
||||
self.update_param(context).await?;
|
||||
// send_sync_msg() is called (usually) a moment later at send_msg_to_smtp()
|
||||
// when the group creation message is actually sent through SMTP --
|
||||
// send_sync_msg() is called a moment later at `smtp::send_smtp_messages()`
|
||||
// when the group creation message is already in the `smtp` table --
|
||||
// this makes sure, the other devices are aware of grpid that is used in the sync-message.
|
||||
context
|
||||
.sync_qr_code_tokens(Some(self.id))
|
||||
@@ -3726,17 +3726,13 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
bail!("can not add contact because the account is not part of the group/broadcast");
|
||||
}
|
||||
|
||||
let sync_qr_code_tokens;
|
||||
if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 {
|
||||
chat.param.remove(Param::Unpromoted);
|
||||
chat.update_param(context).await?;
|
||||
if context
|
||||
.sync_qr_code_tokens(Some(chat_id))
|
||||
.await
|
||||
.log_err(context)
|
||||
.is_ok()
|
||||
{
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
}
|
||||
sync_qr_code_tokens = true;
|
||||
} else {
|
||||
sync_qr_code_tokens = false;
|
||||
}
|
||||
|
||||
if context.is_self_addr(contact.get_addr()).await? {
|
||||
@@ -3780,6 +3776,15 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
return Err(e);
|
||||
}
|
||||
sync = Nosync;
|
||||
if sync_qr_code_tokens
|
||||
&& context
|
||||
.sync_qr_code_tokens(Some(chat_id))
|
||||
.await
|
||||
.log_err(context)
|
||||
.is_ok()
|
||||
{
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
}
|
||||
}
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
if sync.into() {
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
474
src/configure.rs
474
src/configure.rs
@@ -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::{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, ¶m).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(¶m.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,63 +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, ¶m_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, ¶m_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, ¶m_domain).await;
|
||||
}
|
||||
} else {
|
||||
provider = None;
|
||||
param_autoconfig = None;
|
||||
}
|
||||
|
||||
let strict_tls = param.strict_tls();
|
||||
|
||||
progress!(ctx, 500);
|
||||
|
||||
let mut servers = param_autoconfig.unwrap_or_default();
|
||||
@@ -312,107 +310,125 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
|
||||
let servers = expand_param_vector(servers, ¶m.addr, ¶m_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,
|
||||
¶m.imap,
|
||||
¶m.socks5_config,
|
||||
¶m.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);
|
||||
|
||||
@@ -460,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?;
|
||||
|
||||
@@ -479,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.
|
||||
@@ -488,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(¶m.addr, NON_ALPHANUMERIC).to_string();
|
||||
@@ -559,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)]
|
||||
@@ -718,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)]
|
||||
@@ -730,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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -1402,6 +1404,17 @@ impl Contact {
|
||||
self.status.as_str()
|
||||
}
|
||||
|
||||
/// Returns whether end-to-end encryption to the contact is available.
|
||||
pub async fn e2ee_avail(&self, context: &Context) -> Result<bool> {
|
||||
if self.id == ContactId::SELF {
|
||||
return Ok(true);
|
||||
}
|
||||
let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? else {
|
||||
return Ok(false);
|
||||
};
|
||||
Ok(peerstate.peek_key(false).is_some())
|
||||
}
|
||||
|
||||
/// Returns true if the contact
|
||||
/// can be added to verified chats,
|
||||
/// i.e. has a verified key
|
||||
@@ -2673,6 +2686,8 @@ mod tests {
|
||||
|
||||
let encrinfo = Contact::get_encrinfo(&alice, contact_bob_id).await?;
|
||||
assert_eq!(encrinfo, "No encryption");
|
||||
let contact = Contact::get_by_id(&alice, contact_bob_id).await?;
|
||||
assert!(!contact.e2ee_avail(&alice).await?);
|
||||
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat_alice = bob
|
||||
@@ -2696,6 +2711,8 @@ bob@example.net:
|
||||
CCCB 5AA9 F6E1 141C 9431
|
||||
65F1 DB18 B18C BCF7 0487"
|
||||
);
|
||||
let contact = Contact::get_by_id(&alice, contact_bob_id).await?;
|
||||
assert!(contact.e2ee_avail(&alice).await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2873,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(),
|
||||
"{}",
|
||||
|
||||
@@ -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:?}"));
|
||||
|
||||
252
src/imap.rs
252
src/imap.rs
@@ -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(
|
||||
¶m.imap,
|
||||
param.imap.clone(),
|
||||
param.imap_password.clone(),
|
||||
param.socks5_config.clone(),
|
||||
¶m.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.
|
||||
@@ -1045,18 +1062,12 @@ impl Session {
|
||||
.await?;
|
||||
|
||||
for (folder, rowid_set, uid_set) in UidGrouper::from(rows) {
|
||||
self.select_with_uidvalidity(context, &folder)
|
||||
.await
|
||||
.context("failed to select folder")?;
|
||||
|
||||
if let Err(err) = self.add_flag_finalized_with_set(&uid_set, "\\Seen").await {
|
||||
if let Err(err) = self.select_with_uidvalidity(context, &folder).await {
|
||||
warn!(context, "store_seen_flags_on_imap: Failed to select {folder}, will retry later: {err:#}.");
|
||||
} else if let Err(err) = self.add_flag_finalized_with_set(&uid_set, "\\Seen").await {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot mark messages {} in folder {} as seen, will retry later: {}.",
|
||||
uid_set,
|
||||
folder,
|
||||
err
|
||||
);
|
||||
"Cannot mark messages {uid_set} in {folder} as seen, will retry later: {err:#}.");
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
@@ -1510,8 +1521,8 @@ impl Session {
|
||||
|
||||
/// Attempts to configure mvbox.
|
||||
///
|
||||
/// Tries to find any folder in the given list of `folders`. If none is found, tries to create
|
||||
/// `folders[0]`. This method does not use LIST command to ensure that
|
||||
/// Tries to find any folder examining `folders` in the order they go. If none is found, tries
|
||||
/// to create any folder in the same order. This method does not use LIST command to ensure that
|
||||
/// configuration works even if mailbox lookup is forbidden via Access Control List (see
|
||||
/// <https://datatracker.ietf.org/doc/html/rfc4314>).
|
||||
///
|
||||
@@ -1545,16 +1556,17 @@ impl Session {
|
||||
if !create_mvbox {
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(folder) = folders.first() else {
|
||||
return Ok(None);
|
||||
};
|
||||
match self.select_with_uidvalidity(context, folder).await {
|
||||
Ok(_) => {
|
||||
info!(context, "MVBOX-folder {} created.", folder);
|
||||
return Ok(Some(folder));
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Cannot create MVBOX-folder {:?}: {}", folder, err);
|
||||
// Some servers require namespace-style folder names like "INBOX.DeltaChat", so we try all
|
||||
// the variants here.
|
||||
for folder in folders {
|
||||
match self.select_with_uidvalidity(context, folder).await {
|
||||
Ok(_) => {
|
||||
info!(context, "MVBOX-folder {} created.", folder);
|
||||
return Ok(Some(folder));
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Cannot create MVBOX-folder {:?}: {}", folder, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -38,6 +39,16 @@ impl DerefMut for Client {
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts port number to ALPN list.
|
||||
fn alpn(port: u16) -> &'static [&'static str] {
|
||||
if port == 993 {
|
||||
// Do not request ALPN on standard port.
|
||||
&[]
|
||||
} else {
|
||||
&["imap"]
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine server capabilities.
|
||||
///
|
||||
/// If server supports ID capability, send our client ID.
|
||||
@@ -97,67 +108,98 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect_secure(addr: SocketAddr, hostname: &str, strict_tls: bool) -> Result<Self> {
|
||||
let tls_stream = connect_tls_inner(addr, hostname, strict_tls, "imap").await?;
|
||||
let tls_stream = connect_tls_inner(addr, hostname, strict_tls, alpn(addr.port())).await?;
|
||||
let buffered_stream = BufWriter::new(tls_stream);
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
||||
let mut client = Client::new(session_stream);
|
||||
@@ -197,7 +239,7 @@ impl Client {
|
||||
let buffered_tcp_stream = client.into_inner();
|
||||
let tcp_stream = buffered_tcp_stream.into_inner();
|
||||
|
||||
let tls_stream = wrap_tls(strict_tls, host, "imap", tcp_stream)
|
||||
let tls_stream = wrap_tls(strict_tls, host, &[], tcp_stream)
|
||||
.await
|
||||
.context("STARTTLS upgrade failed")?;
|
||||
|
||||
@@ -217,7 +259,7 @@ impl Client {
|
||||
let socks5_stream = socks5_config
|
||||
.connect(context, domain, port, strict_tls)
|
||||
.await?;
|
||||
let tls_stream = wrap_tls(strict_tls, domain, "imap", socks5_stream).await?;
|
||||
let tls_stream = wrap_tls(strict_tls, domain, alpn(port), socks5_stream).await?;
|
||||
let buffered_stream = BufWriter::new(tls_stream);
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
||||
let mut client = Client::new(session_stream);
|
||||
@@ -270,7 +312,7 @@ impl Client {
|
||||
let buffered_socks5_stream = client.into_inner();
|
||||
let socks5_stream: Socks5Stream<_> = buffered_socks5_stream.into_inner();
|
||||
|
||||
let tls_stream = wrap_tls(strict_tls, hostname, "imap", socks5_stream)
|
||||
let tls_stream = wrap_tls(strict_tls, hostname, &[], socks5_stream)
|
||||
.await
|
||||
.context("STARTTLS upgrade failed")?;
|
||||
let buffered_stream = BufWriter::new(tls_stream);
|
||||
|
||||
@@ -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;
|
||||
|
||||
1122
src/login_param.rs
1122
src/login_param.rs
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
@@ -726,18 +736,18 @@ impl MimeFactory {
|
||||
} else if header_name == "autocrypt" {
|
||||
unprotected_headers.push(header.clone());
|
||||
} else if header_name == "from" {
|
||||
protected_headers.push(header.clone());
|
||||
if is_encrypted && verified || is_securejoin_message {
|
||||
unprotected_headers.push(
|
||||
Header::new_with_value(
|
||||
header.name,
|
||||
vec![Address::new_mailbox(self.from_addr.clone())],
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
} else {
|
||||
unprotected_headers.push(header);
|
||||
// Unencrypted securejoin messages should _not_ include the display name:
|
||||
if is_encrypted || !is_securejoin_message {
|
||||
protected_headers.push(header.clone());
|
||||
}
|
||||
|
||||
unprotected_headers.push(
|
||||
Header::new_with_value(
|
||||
header.name,
|
||||
vec![Address::new_mailbox(self.from_addr.clone())],
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
} else if header_name == "to" {
|
||||
protected_headers.push(header.clone());
|
||||
if is_encrypted {
|
||||
@@ -902,12 +912,11 @@ impl MimeFactory {
|
||||
.fold(message, |message, header| message.header(header.clone()));
|
||||
|
||||
if skip_autocrypt || !context.get_config_bool(Config::SignUnencrypted).await? {
|
||||
let protected: HashSet<Header> = HashSet::from_iter(protected_headers.into_iter());
|
||||
for h in unprotected_headers.split_off(0) {
|
||||
if !protected.contains(&h) {
|
||||
unprotected_headers.push(h);
|
||||
}
|
||||
}
|
||||
// Deduplicate unprotected headers that also are in the protected headers:
|
||||
let protected: HashSet<&str> =
|
||||
HashSet::from_iter(protected_headers.iter().map(|h| h.name.as_str()));
|
||||
unprotected_headers.retain(|h| !protected.contains(&h.name.as_str()));
|
||||
|
||||
message
|
||||
} else {
|
||||
let message = message.header(get_content_type_directives_header());
|
||||
|
||||
129
src/net.rs
129
src/net.rs
@@ -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?;
|
||||
@@ -114,13 +118,103 @@ pub(crate) async fn connect_tls_inner(
|
||||
addr: SocketAddr,
|
||||
host: &str,
|
||||
strict_tls: bool,
|
||||
alpn: &str,
|
||||
alpn: &[&str],
|
||||
) -> Result<TlsStream<Pin<Box<TimeoutStream<TcpStream>>>>> {
|
||||
let tcp_stream = connect_tcp_inner(addr).await?;
|
||||
let tls_stream = wrap_tls(strict_tls, host, alpn, tcp_stream).await?;
|
||||
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
|
||||
}
|
||||
|
||||
469
src/net/dns.rs
469
src/net/dns.rs
@@ -1,6 +1,7 @@
|
||||
//! DNS resolution and cache.
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use std::collections::HashMap;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::str::FromStr;
|
||||
use tokio::net::lookup_host;
|
||||
@@ -9,6 +10,7 @@ use tokio::time::timeout;
|
||||
use super::load_connection_timestamp;
|
||||
use crate::context::Context;
|
||||
use crate::tools::time;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
/// Inserts entry into DNS cache
|
||||
/// or updates existing one with a new timestamp.
|
||||
@@ -106,71 +108,402 @@ pub(crate) async fn update_connect_timestamp(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load hardcoded cache if everything else fails.
|
||||
/// 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.
|
||||
///
|
||||
/// 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> {
|
||||
match hostname {
|
||||
"mail.sangham.net" => {
|
||||
static DNS_PRELOAD: Lazy<HashMap<&'static str, Vec<IpAddr>>> = Lazy::new(|| {
|
||||
HashMap::from([
|
||||
(
|
||||
"mail.sangham.net",
|
||||
vec![
|
||||
SocketAddr::new(
|
||||
IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0xc17, 0x798c, 0, 0, 0, 1)),
|
||||
port,
|
||||
),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(159, 69, 186, 85)), port),
|
||||
]
|
||||
}
|
||||
"nine.testrun.org" => {
|
||||
IpAddr::V4(Ipv4Addr::new(159, 69, 186, 85)),
|
||||
IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0xc17, 0x798c, 0, 0, 0, 1)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"nine.testrun.org",
|
||||
vec![
|
||||
SocketAddr::new(
|
||||
IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0x241, 0x4ce8, 0, 0, 0, 2)),
|
||||
port,
|
||||
),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(116, 202, 233, 236)), port),
|
||||
]
|
||||
}
|
||||
"disroot.org" => {
|
||||
vec![SocketAddr::new(
|
||||
IpAddr::V4(Ipv4Addr::new(178, 21, 23, 139)),
|
||||
port,
|
||||
)]
|
||||
}
|
||||
"mail.riseup.net" => {
|
||||
IpAddr::V4(Ipv4Addr::new(116, 202, 233, 236)),
|
||||
IpAddr::V4(Ipv4Addr::new(128, 140, 126, 197)),
|
||||
IpAddr::V4(Ipv4Addr::new(49, 12, 116, 128)),
|
||||
IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0x241, 0x4ce8, 0, 0, 0, 2)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"disroot.org",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(178, 21, 23, 139))],
|
||||
),
|
||||
(
|
||||
"imap.gmail.com",
|
||||
vec![
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(198, 252, 153, 70)), port),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(198, 252, 153, 71)), port),
|
||||
]
|
||||
}
|
||||
"imap.gmail.com" => {
|
||||
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 108)),
|
||||
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 109)),
|
||||
IpAddr::V4(Ipv4Addr::new(66, 102, 1, 108)),
|
||||
IpAddr::V4(Ipv4Addr::new(66, 102, 1, 109)),
|
||||
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x400c, 0xc1f, 0, 0, 0, 0x6c)),
|
||||
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x400c, 0xc1f, 0, 0, 0, 0x6d)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"smtp.gmail.com",
|
||||
vec![
|
||||
SocketAddr::new(
|
||||
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x400c, 0xc1f, 0, 0, 0, 0x6c)),
|
||||
port,
|
||||
),
|
||||
SocketAddr::new(
|
||||
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x400c, 0xc1f, 0, 0, 0, 0x6d)),
|
||||
port,
|
||||
),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(142, 250, 110, 109)), port),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(142, 250, 110, 108)), port),
|
||||
]
|
||||
}
|
||||
"smtp.gmail.com" => {
|
||||
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 109)),
|
||||
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x4013, 0xc04, 0, 0, 0, 0x6c)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"mail.autistici.org",
|
||||
vec![
|
||||
SocketAddr::new(
|
||||
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x4013, 0xc04, 0, 0, 0, 0x6c)),
|
||||
port,
|
||||
),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(142, 250, 110, 109)), port),
|
||||
]
|
||||
}
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
IpAddr::V4(Ipv4Addr::new(198, 167, 222, 108)),
|
||||
IpAddr::V4(Ipv4Addr::new(82, 94, 249, 234)),
|
||||
IpAddr::V4(Ipv4Addr::new(93, 190, 126, 19)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"smtp.autistici.org",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(198, 167, 222, 108)),
|
||||
IpAddr::V4(Ipv4Addr::new(82, 94, 249, 234)),
|
||||
IpAddr::V4(Ipv4Addr::new(93, 190, 126, 19)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"daleth.cafe",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(37, 27, 6, 204))],
|
||||
),
|
||||
(
|
||||
"imap.163.com",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(111, 124, 203, 45))],
|
||||
),
|
||||
(
|
||||
"smtp.163.com",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(103, 129, 252, 45))],
|
||||
),
|
||||
(
|
||||
"imap.aol.com",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(212, 82, 101, 33)),
|
||||
IpAddr::V4(Ipv4Addr::new(87, 248, 98, 69)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"smtp.aol.com",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(87, 248, 97, 31))],
|
||||
),
|
||||
(
|
||||
"mail.arcor.de",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(2, 207, 150, 234))],
|
||||
),
|
||||
(
|
||||
"imap.arcor.de",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(2, 207, 150, 230))],
|
||||
),
|
||||
(
|
||||
"imap.fastmail.com",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(103, 168, 172, 43)),
|
||||
IpAddr::V4(Ipv4Addr::new(103, 168, 172, 58)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"smtp.fastmail.com",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(103, 168, 172, 45)),
|
||||
IpAddr::V4(Ipv4Addr::new(103, 168, 172, 60)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"imap.gmx.net",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(212, 227, 17, 170)),
|
||||
IpAddr::V4(Ipv4Addr::new(212, 227, 17, 186)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"imap.mail.de",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(62, 201, 172, 16))],
|
||||
),
|
||||
(
|
||||
"smtp.mailbox.org",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(185, 97, 174, 196))],
|
||||
),
|
||||
(
|
||||
"imap.mailbox.org",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(185, 97, 174, 199))],
|
||||
),
|
||||
(
|
||||
"imap.naver.com",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(125, 209, 238, 153))],
|
||||
),
|
||||
(
|
||||
"imap.ouvaton.coop",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(194, 36, 166, 20))],
|
||||
),
|
||||
(
|
||||
"imap.purelymail.com",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(18, 204, 123, 63))],
|
||||
),
|
||||
(
|
||||
"imap.tiscali.it",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(213, 205, 33, 10))],
|
||||
),
|
||||
(
|
||||
"smtp.tiscali.it",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(213, 205, 33, 13))],
|
||||
),
|
||||
(
|
||||
"imap.web.de",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(212, 227, 17, 162)),
|
||||
IpAddr::V4(Ipv4Addr::new(212, 227, 17, 178)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"imap.ziggo.nl",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(84, 116, 6, 3))],
|
||||
),
|
||||
(
|
||||
"imap.zoho.eu",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(185, 230, 214, 25))],
|
||||
),
|
||||
(
|
||||
"imaps.bluewin.ch",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(16, 62, 253, 42)),
|
||||
IpAddr::V4(Ipv4Addr::new(16, 63, 141, 244)),
|
||||
IpAddr::V4(Ipv4Addr::new(16, 63, 146, 183)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"mail.buzon.uy",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(185, 101, 93, 79))],
|
||||
),
|
||||
(
|
||||
"mail.ecloud.global",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(95, 217, 246, 96))],
|
||||
),
|
||||
(
|
||||
"mail.ende.in.net",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(95, 217, 5, 72))],
|
||||
),
|
||||
(
|
||||
"mail.gmx.net",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(212, 227, 17, 168)),
|
||||
IpAddr::V4(Ipv4Addr::new(212, 227, 17, 190)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"mail.infomaniak.com",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(83, 166, 143, 44)),
|
||||
IpAddr::V4(Ipv4Addr::new(83, 166, 143, 45)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"mail.mymagenta.at",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(80, 109, 253, 241))],
|
||||
),
|
||||
(
|
||||
"mail.nubo.coop",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(79, 99, 201, 10))],
|
||||
),
|
||||
(
|
||||
"mail.riseup.net",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(198, 252, 153, 70)),
|
||||
IpAddr::V4(Ipv4Addr::new(198, 252, 153, 71)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"mail.systemausfall.org",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(51, 75, 71, 249)),
|
||||
IpAddr::V4(Ipv4Addr::new(80, 153, 252, 42)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"mail.systemli.org",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(93, 190, 126, 36))],
|
||||
),
|
||||
(
|
||||
"mehl.cloud",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(95, 217, 223, 172))],
|
||||
),
|
||||
(
|
||||
"mx.freenet.de",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(195, 4, 92, 210)),
|
||||
IpAddr::V4(Ipv4Addr::new(195, 4, 92, 211)),
|
||||
IpAddr::V4(Ipv4Addr::new(195, 4, 92, 212)),
|
||||
IpAddr::V4(Ipv4Addr::new(195, 4, 92, 213)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"newyear.aktivix.org",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(162, 247, 75, 192))],
|
||||
),
|
||||
(
|
||||
"pimap.schulon.org",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(194, 77, 246, 20))],
|
||||
),
|
||||
(
|
||||
"posteo.de",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(185, 67, 36, 168)),
|
||||
IpAddr::V4(Ipv4Addr::new(185, 67, 36, 169)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"psmtp.schulon.org",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(194, 77, 246, 20))],
|
||||
),
|
||||
(
|
||||
"secureimap.t-online.de",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 114)),
|
||||
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 115)),
|
||||
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 50)),
|
||||
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 51)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"securesmtp.t-online.de",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 110)),
|
||||
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 46)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"smtp.aliyun.com",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(47, 246, 136, 232))],
|
||||
),
|
||||
(
|
||||
"smtp.mail.de",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(62, 201, 172, 21))],
|
||||
),
|
||||
(
|
||||
"smtp.mail.ru",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(217, 69, 139, 160)),
|
||||
IpAddr::V4(Ipv4Addr::new(94, 100, 180, 160)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"imap.mail.yahoo.com",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(87, 248, 103, 8)),
|
||||
IpAddr::V4(Ipv4Addr::new(212, 82, 101, 24)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"smtp.mail.yahoo.com",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(87, 248, 97, 36))],
|
||||
),
|
||||
(
|
||||
"imap.mailo.com",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(213, 182, 54, 20))],
|
||||
),
|
||||
(
|
||||
"smtp.mailo.com",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(213, 182, 54, 20))],
|
||||
),
|
||||
(
|
||||
"smtp.naver.com",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(125, 209, 238, 155))],
|
||||
),
|
||||
(
|
||||
"smtp.ouvaton.coop",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(194, 36, 166, 20))],
|
||||
),
|
||||
(
|
||||
"smtp.purelymail.com",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(18, 204, 123, 63))],
|
||||
),
|
||||
(
|
||||
"imap.qq.com",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(43, 129, 255, 54))],
|
||||
),
|
||||
(
|
||||
"smtp.qq.com",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(43, 129, 255, 54))],
|
||||
),
|
||||
(
|
||||
"imap.rambler.ru",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 169)),
|
||||
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 171)),
|
||||
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 168)),
|
||||
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 170)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"smtp.rambler.ru",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 165)),
|
||||
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 167)),
|
||||
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 166)),
|
||||
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 164)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"imap.vivaldi.net",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(31, 209, 137, 15))],
|
||||
),
|
||||
(
|
||||
"smtp.vivaldi.net",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(31, 209, 137, 12))],
|
||||
),
|
||||
(
|
||||
"imap.vodafonemail.de",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(2, 207, 150, 230))],
|
||||
),
|
||||
(
|
||||
"smtp.vodafonemail.de",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(2, 207, 150, 234))],
|
||||
),
|
||||
(
|
||||
"smtp.web.de",
|
||||
vec![
|
||||
IpAddr::V4(Ipv4Addr::new(213, 165, 67, 108)),
|
||||
IpAddr::V4(Ipv4Addr::new(213, 165, 67, 124)),
|
||||
],
|
||||
),
|
||||
(
|
||||
"imap.yandex.com",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(77, 88, 21, 125))],
|
||||
),
|
||||
(
|
||||
"smtp.yandex.com",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(77, 88, 21, 158))],
|
||||
),
|
||||
(
|
||||
"smtp.ziggo.nl",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(84, 116, 6, 3))],
|
||||
),
|
||||
(
|
||||
"smtp.zoho.eu",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(185, 230, 212, 164))],
|
||||
),
|
||||
(
|
||||
"smtpauths.bluewin.ch",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(195, 186, 120, 54))],
|
||||
),
|
||||
(
|
||||
"stinpriza.net",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(5, 9, 122, 184))],
|
||||
),
|
||||
(
|
||||
"undernet.uy",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(167, 62, 254, 153))],
|
||||
),
|
||||
(
|
||||
"webbox222.server-home.org",
|
||||
vec![IpAddr::V4(Ipv4Addr::new(91, 203, 111, 88))],
|
||||
),
|
||||
])
|
||||
});
|
||||
|
||||
async fn lookup_cache(
|
||||
context: &Context,
|
||||
@@ -230,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));
|
||||
@@ -282,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
291
src/net/http.rs
291
src/net/http.rs
@@ -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)
|
||||
}
|
||||
|
||||
@@ -14,12 +14,33 @@ static LETSENCRYPT_ROOT: Lazy<Certificate> = Lazy::new(|| {
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
pub fn build_tls(strict_tls: bool, alpns: &[&str]) -> TlsConnector {
|
||||
static IMAP_NAUTA_CU: Lazy<Certificate> = Lazy::new(|| {
|
||||
Certificate::from_der(include_bytes!(
|
||||
"../../assets/certificates/imap.nauta.cu.der"
|
||||
))
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
static SMTP_NAUTA_CU: Lazy<Certificate> = Lazy::new(|| {
|
||||
Certificate::from_der(include_bytes!(
|
||||
"../../assets/certificates/smtp.nauta.cu.der"
|
||||
))
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
fn build_tls(strict_tls: bool, hostname: &str, alpns: &[&str]) -> TlsConnector {
|
||||
let tls_builder = TlsConnector::new()
|
||||
.min_protocol_version(Some(Protocol::Tlsv12))
|
||||
.request_alpns(alpns)
|
||||
.add_root_certificate(LETSENCRYPT_ROOT.clone());
|
||||
|
||||
// Add self-signed certificates for known hostnames.
|
||||
let tls_builder = match hostname {
|
||||
"imap.nauta.cu" => tls_builder.add_root_certificate(IMAP_NAUTA_CU.clone()),
|
||||
"smtp.nauta.cu" => tls_builder.add_root_certificate(SMTP_NAUTA_CU.clone()),
|
||||
_ => tls_builder,
|
||||
};
|
||||
|
||||
if strict_tls {
|
||||
tls_builder
|
||||
} else {
|
||||
@@ -32,10 +53,10 @@ pub fn build_tls(strict_tls: bool, alpns: &[&str]) -> TlsConnector {
|
||||
pub async fn wrap_tls<T: AsyncRead + AsyncWrite + Unpin>(
|
||||
strict_tls: bool,
|
||||
hostname: &str,
|
||||
alpn: &str,
|
||||
alpn: &[&str],
|
||||
stream: T,
|
||||
) -> Result<TlsStream<T>> {
|
||||
let tls = build_tls(strict_tls, &[alpn]);
|
||||
let tls = build_tls(strict_tls, hostname, alpn);
|
||||
let tls_stream = tls.connect(hostname, stream).await?;
|
||||
Ok(tls_stream)
|
||||
}
|
||||
@@ -48,7 +69,7 @@ mod tests {
|
||||
fn test_build_tls() {
|
||||
// we are using some additional root certificates.
|
||||
// make sure, they do not break construction of TlsConnector
|
||||
let _ = build_tls(true, &[]);
|
||||
let _ = build_tls(false, &[]);
|
||||
let _ = build_tls(true, "example.org", &[]);
|
||||
let _ = build_tls(false, "example.org", &[]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -520,7 +520,7 @@ static P_FREENET_DE: Provider = Provider {
|
||||
static P_GMAIL: Provider = Provider {
|
||||
id: "gmail",
|
||||
status: Status::Preparation,
|
||||
before_login_hint: "For Gmail accounts, you need to create an app-password if you have \"2-Step Verification\" enabled. If this setting is not available, you need to enable \"less secure apps\".",
|
||||
before_login_hint: "For Gmail accounts, you need to have \"2-Step Verification\" enabled and create an app-password.",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/gmail",
|
||||
server: &[
|
||||
@@ -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 {
|
||||
@@ -1600,11 +1614,13 @@ static P_VIVALDI: Provider = Provider {
|
||||
// vk.com.md: vk.com
|
||||
static P_VK_COM: Provider = Provider {
|
||||
id: "vk.com",
|
||||
status: Status::Broken,
|
||||
before_login_hint: "К сожалению, VK Почта не поддерживает работу с Delta Chat. См. https://help.vk.mail.ru/vkmail/questions/client",
|
||||
status: Status::Preparation,
|
||||
before_login_hint: "Вам необходимо сгенерировать \"пароль для внешнего приложения\" в веб-интерфейсе mail.ru https://account.mail.ru/user/2-step-auth/passwords/ чтобы vk.com работал с Delta Chat.",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/vk-com",
|
||||
server: &[
|
||||
Server { protocol: Imap, socket: Ssl, hostname: "imap.mail.ru", port: 993, username_pattern: Email },
|
||||
Server { protocol: Smtp, socket: Ssl, hostname: "smtp.mail.ru", port: 465, username_pattern: Email },
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: None,
|
||||
@@ -2415,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, 1).unwrap());
|
||||
Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2024, 8, 23).unwrap());
|
||||
|
||||
17
src/push.rs
17
src/push.rs
@@ -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
246
src/qr.rs
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -807,7 +807,7 @@ async fn add_parts(
|
||||
// 1:1 chat is blocked, but the contact is not.
|
||||
// This happens when 1:1 chat is hidden
|
||||
// during scanning of a group invitation code.
|
||||
Blocked::Request
|
||||
create_blocked_default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::contact;
|
||||
use crate::download::MIN_DOWNLOAD_LIMIT;
|
||||
use crate::imap::prefetch_should_download;
|
||||
use crate::imex::{imex, ImexMode};
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::test_utils::{get_chat_msg, mark_as_verified, TestContext, TestContextManager};
|
||||
use crate::tools::{time, SystemTime};
|
||||
|
||||
@@ -3275,6 +3276,46 @@ async fn test_auto_accept_group_for_bots() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_auto_accept_protected_group_for_bots() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
bob.set_config(Config::Bot, Some("1")).await.unwrap();
|
||||
mark_as_verified(alice, bob).await;
|
||||
mark_as_verified(bob, alice).await;
|
||||
let group_id = alice
|
||||
.create_group_with_members(ProtectionStatus::Protected, "Group", &[bob])
|
||||
.await;
|
||||
let sent = alice.send_text(group_id, "Hello!").await;
|
||||
let msg = bob.recv_msg(&sent).await;
|
||||
let chat = chat::Chat::load_from_db(bob, msg.chat_id).await?;
|
||||
assert!(!chat.is_contact_request());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_bot_accepts_another_group_after_qr_scan() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
bob.set_config(Config::Bot, Some("1")).await?;
|
||||
|
||||
let group_id = chat::create_group_chat(alice, ProtectionStatus::Protected, "Group").await?;
|
||||
let qr = get_securejoin_qr(alice, Some(group_id)).await?;
|
||||
tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||
|
||||
let group_id = alice
|
||||
.create_group_with_members(ProtectionStatus::Protected, "Group", &[bob])
|
||||
.await;
|
||||
let sent = alice.send_text(group_id, "Hello!").await;
|
||||
let msg = bob.recv_msg(&sent).await;
|
||||
let chat = chat::Chat::load_from_db(bob, msg.chat_id).await?;
|
||||
assert!(!chat.is_contact_request());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_as_bot() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
@@ -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?;
|
||||
@@ -637,6 +616,10 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
};
|
||||
peerstate.set_verified(key.clone(), fingerprint, addr)?;
|
||||
if matches!(step, "vg-member-added" | "vc-contact-confirm") {
|
||||
peerstate.backward_verified_key_id =
|
||||
Some(context.get_config_i64(Config::KeyId).await?).filter(|&id| id > 0);
|
||||
}
|
||||
peerstate.prefer_encrypt = EncryptPreference::Mutual;
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
|
||||
@@ -777,6 +760,7 @@ mod tests {
|
||||
CheckProtectionTimestamp,
|
||||
WrongAliceGossip,
|
||||
SecurejoinWaitTimeout,
|
||||
AliceIsBot,
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -799,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;
|
||||
@@ -807,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)
|
||||
@@ -843,11 +838,13 @@ mod tests {
|
||||
);
|
||||
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
assert!(!sent.payload.contains("Bob Examplenet"));
|
||||
assert_eq!(sent.recipient(), EmailAddress::new(alice_addr).unwrap());
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
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;
|
||||
@@ -860,6 +857,8 @@ 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());
|
||||
assert_eq!(
|
||||
@@ -905,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
|
||||
@@ -970,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)
|
||||
@@ -1009,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());
|
||||
@@ -1027,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.
|
||||
@@ -1241,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`.
|
||||
@@ -1254,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!(
|
||||
@@ -1287,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!(
|
||||
@@ -1319,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
|
||||
@@ -1445,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();
|
||||
|
||||
@@ -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]
|
||||
|
||||
106
src/smtp.rs
106
src/smtp.rs
@@ -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"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,20 +2,121 @@
|
||||
|
||||
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;
|
||||
|
||||
/// Converts port number to ALPN list.
|
||||
fn alpn(port: u16) -> &'static [&'static str] {
|
||||
if port == 465 {
|
||||
// Do not request ALPN on standard port.
|
||||
&[]
|
||||
} else {
|
||||
&["smtp"]
|
||||
}
|
||||
}
|
||||
|
||||
#[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.
|
||||
///
|
||||
@@ -24,59 +125,46 @@ use crate::tools::time;
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,11 +177,12 @@ pub(crate) async fn connect_stream(
|
||||
async fn skip_smtp_greeting<R: tokio::io::AsyncBufReadExt + Unpin>(stream: &mut R) -> Result<()> {
|
||||
let mut line = String::with_capacity(512);
|
||||
loop {
|
||||
line.clear();
|
||||
let read = stream.read_line(&mut line).await?;
|
||||
if read == 0 {
|
||||
bail!("Unexpected EOF while reading SMTP greeting.");
|
||||
}
|
||||
if line.starts_with("220- ") {
|
||||
if line.starts_with("220-") {
|
||||
continue;
|
||||
} else if line.starts_with("220 ") {
|
||||
return Ok(());
|
||||
@@ -113,7 +202,7 @@ async fn connect_secure_socks5(
|
||||
let socks5_stream = socks5_config
|
||||
.connect(context, hostname, port, strict_tls)
|
||||
.await?;
|
||||
let tls_stream = wrap_tls(strict_tls, hostname, "smtp", socks5_stream).await?;
|
||||
let tls_stream = wrap_tls(strict_tls, hostname, alpn(port), socks5_stream).await?;
|
||||
let mut buffered_stream = BufStream::new(tls_stream);
|
||||
skip_smtp_greeting(&mut buffered_stream).await?;
|
||||
let session_stream: Box<dyn SessionBufStream> = Box::new(buffered_stream);
|
||||
@@ -135,7 +224,7 @@ async fn connect_starttls_socks5(
|
||||
let client = SmtpClient::new().smtp_utf8(true);
|
||||
let transport = SmtpTransport::new(client, BufStream::new(socks5_stream)).await?;
|
||||
let tcp_stream = transport.starttls().await?.into_inner();
|
||||
let tls_stream = wrap_tls(strict_tls, hostname, "smtp", tcp_stream)
|
||||
let tls_stream = wrap_tls(strict_tls, hostname, &[], tcp_stream)
|
||||
.await
|
||||
.context("STARTTLS upgrade failed")?;
|
||||
let buffered_stream = BufStream::new(tls_stream);
|
||||
@@ -163,7 +252,7 @@ async fn connect_secure(
|
||||
hostname: &str,
|
||||
strict_tls: bool,
|
||||
) -> Result<Box<dyn SessionBufStream>> {
|
||||
let tls_stream = connect_tls_inner(addr, hostname, strict_tls, "smtp").await?;
|
||||
let tls_stream = connect_tls_inner(addr, hostname, strict_tls, alpn(addr.port())).await?;
|
||||
let mut buffered_stream = BufStream::new(tls_stream);
|
||||
skip_smtp_greeting(&mut buffered_stream).await?;
|
||||
let session_stream: Box<dyn SessionBufStream> = Box::new(buffered_stream);
|
||||
@@ -181,7 +270,7 @@ async fn connect_starttls(
|
||||
let client = async_smtp::SmtpClient::new().smtp_utf8(true);
|
||||
let transport = async_smtp::SmtpTransport::new(client, BufStream::new(tcp_stream)).await?;
|
||||
let tcp_stream = transport.starttls().await?.into_inner();
|
||||
let tls_stream = wrap_tls(strict_tls, host, "smtp", tcp_stream)
|
||||
let tls_stream = wrap_tls(strict_tls, host, &[], tcp_stream)
|
||||
.await
|
||||
.context("STARTTLS upgrade failed")?;
|
||||
|
||||
@@ -197,3 +286,19 @@ async fn connect_insecure(addr: SocketAddr) -> Result<Box<dyn SessionBufStream>>
|
||||
let session_stream: Box<dyn SessionBufStream> = Box::new(buffered_stream);
|
||||
Ok(session_stream)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tokio::io::BufReader;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_skip_smtp_greeting() -> Result<()> {
|
||||
let greeting = b"220-server261.web-hosting.com ESMTP Exim 4.96.2 #2 Sat, 24 Aug 2024 12:25:53 -0400 \r\n\
|
||||
220-We do not authorize the use of this system to transport unsolicited,\r\n\
|
||||
220 and/or bulk e-mail.\r\n";
|
||||
let mut buffered_stream = BufReader::new(&greeting[..]);
|
||||
skip_smtp_greeting(&mut buffered_stream).await
|
||||
}
|
||||
}
|
||||
|
||||
47
src/socks.rs
47
src/socks.rs
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
35
src/sync.rs
35
src/sync.rs
@@ -328,8 +328,10 @@ mod tests {
|
||||
use anyhow::bail;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::ProtectionStatus;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::contact::{Contact, Origin};
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
@@ -630,4 +632,37 @@ mod tests {
|
||||
assert_eq!(msg.text, "hi");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_unpromoted_group_no_qr_sync() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
alice.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
let alice_chatid =
|
||||
chat::create_group_chat(alice, ProtectionStatus::Protected, "the chat").await?;
|
||||
let qr = get_securejoin_qr(alice, Some(alice_chatid)).await?;
|
||||
let msg_id = alice.send_sync_msg().await?;
|
||||
assert!(msg_id.is_none());
|
||||
|
||||
let bob = &tcm.bob().await;
|
||||
tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||
let msg_id = alice.send_sync_msg().await?;
|
||||
// The group becomes promoted when Bob joins, so the QR code token is synced.
|
||||
assert!(msg_id.is_some());
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
let mut sync_items = msg.sync_items.unwrap().items;
|
||||
assert_eq!(sync_items.len(), 1);
|
||||
let data = sync_items.pop().unwrap().data;
|
||||
let SyncDataOrUnknown::SyncData(AddQrToken(_)) = data else {
|
||||
unreachable!();
|
||||
};
|
||||
|
||||
let fiona = &tcm.fiona().await;
|
||||
tcm.exec_securejoin_qr(fiona, alice, &qr).await;
|
||||
let msg_id = alice.send_sync_msg().await?;
|
||||
// The QR code token was already synced before.
|
||||
assert!(msg_id.is_none());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! This private module is only compiled for test runs.
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::fmt::Write;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::panic;
|
||||
@@ -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;
|
||||
@@ -175,7 +175,12 @@ impl TestContextManager {
|
||||
));
|
||||
|
||||
let qr = get_securejoin_qr(&scanned.ctx, None).await.unwrap();
|
||||
join_securejoin(&scanner.ctx, &qr).await.unwrap();
|
||||
self.exec_securejoin_qr(scanner, scanned, &qr).await;
|
||||
}
|
||||
|
||||
/// Executes SecureJoin initiated by `scanner` scanning `qr` generated by `scanned`.
|
||||
pub async fn exec_securejoin_qr(&self, scanner: &TestContext, scanned: &TestContext, qr: &str) {
|
||||
join_securejoin(&scanner.ctx, qr).await.unwrap();
|
||||
|
||||
loop {
|
||||
if let Some(sent) = scanner.pop_sent_msg_opt(Duration::ZERO).await {
|
||||
@@ -472,6 +477,36 @@ impl TestContext {
|
||||
update_msg_state(&self.ctx, msg_id, MessageState::OutDelivered)
|
||||
.await
|
||||
.expect("failed to update message state");
|
||||
|
||||
let payload_headers = payload.split("\r\n\r\n").next().unwrap().lines();
|
||||
let payload_header_names: Vec<_> = payload_headers
|
||||
.map(|h| h.split(':').next().unwrap())
|
||||
.collect();
|
||||
|
||||
// Check that we are sending exactly one From, Subject, Date, To, Message-ID, and MIME-Version header:
|
||||
for header in &[
|
||||
"From",
|
||||
"Subject",
|
||||
"Date",
|
||||
"To",
|
||||
"Message-ID",
|
||||
"MIME-Version",
|
||||
] {
|
||||
assert_eq!(
|
||||
payload_header_names.iter().filter(|h| *h == header).count(),
|
||||
1,
|
||||
"This sent email should contain the header {header} exactly 1 time:\n{payload}"
|
||||
);
|
||||
}
|
||||
// Check that we aren't sending any header twice:
|
||||
let mut hash_set = HashSet::new();
|
||||
for header_name in payload_header_names {
|
||||
assert!(
|
||||
hash_set.insert(header_name),
|
||||
"This sent email shouldn't contain the header {header_name} multiple times:\n{payload}"
|
||||
);
|
||||
}
|
||||
|
||||
Some(SentMessage {
|
||||
payload,
|
||||
sender_msg_id: msg_id,
|
||||
|
||||
@@ -881,7 +881,7 @@ async fn test_verified_member_added_reordering() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_unencrypted_name_if_verified() -> Result<()> {
|
||||
async fn test_no_unencrypted_name_if_encrypted() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
for verified in [false, true] {
|
||||
let alice = tcm.alice().await;
|
||||
@@ -898,7 +898,7 @@ async fn test_no_unencrypted_name_if_verified() -> Result<()> {
|
||||
let chat_id = bob.create_chat(&alice).await.id;
|
||||
let msg = &bob.send_text(chat_id, "hi").await;
|
||||
|
||||
assert_eq!(msg.payload.contains("Bob Smith"), !verified);
|
||||
assert_eq!(msg.payload.contains("Bob Smith"), false);
|
||||
assert!(msg.payload.contains("BEGIN PGP MESSAGE"));
|
||||
|
||||
let msg = alice.recv_msg(msg).await;
|
||||
|
||||
Reference in New Issue
Block a user