mirror of
https://github.com/chatmail/core.git
synced 2026-04-02 05:22:14 +03:00
Compare commits
29 Commits
d6dacdcd27
...
iroh-share
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94b50f6742 | ||
|
|
0bcba98928 | ||
|
|
20564529a7 | ||
|
|
415279c450 | ||
|
|
1fd4e8e0ec | ||
|
|
b6e8c9f1a4 | ||
|
|
b2c68c77f2 | ||
|
|
c9cd4c9746 | ||
|
|
1f4384b302 | ||
|
|
26719ee358 | ||
|
|
09d5cbe564 | ||
|
|
ba869a2e7f | ||
|
|
6c6499f2ff | ||
|
|
f696f3e485 | ||
|
|
1633b2fd26 | ||
|
|
411d0a7cd4 | ||
|
|
0ed3348bdf | ||
|
|
e61ee76e68 | ||
|
|
17f01e5e71 | ||
|
|
94c16ef176 | ||
|
|
4ff746ef81 | ||
|
|
0aa4e21f7a | ||
|
|
995bf6fa9e | ||
|
|
63872fc271 | ||
|
|
679242d5c4 | ||
|
|
364e06d8ae | ||
|
|
14fc7b7d10 | ||
|
|
9f7e962832 | ||
|
|
0e06bcb182 |
24
.github/workflows/ci.yml
vendored
24
.github/workflows/ci.yml
vendored
@@ -40,6 +40,11 @@ jobs:
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
override: true
|
||||
- name: Install Protoc
|
||||
uses: arduino/setup-protoc@v1
|
||||
with:
|
||||
version: '3.20.1'
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v1
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
@@ -55,6 +60,11 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Protoc
|
||||
uses: arduino/setup-protoc@v1
|
||||
with:
|
||||
version: '3.20.1'
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Install rust stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
@@ -77,24 +87,28 @@ jobs:
|
||||
include:
|
||||
# Currently used Rust version, same as in `rust-toolchain` file.
|
||||
- os: ubuntu-latest
|
||||
rust: 1.61.0
|
||||
rust: 1.63.0
|
||||
python: 3.9
|
||||
- os: windows-latest
|
||||
rust: 1.61.0
|
||||
rust: 1.63.0
|
||||
python: false # Python bindings compilation on Windows is not supported.
|
||||
|
||||
# Minimum Supported Rust Version = 1.57.0
|
||||
# Minimum Supported Rust Version = 1.63.0
|
||||
#
|
||||
# Minimum Supported Python Version = 3.7
|
||||
# This is the minimum version for which manylinux Python wheels are
|
||||
# built.
|
||||
- os: ubuntu-latest
|
||||
rust: 1.57.0
|
||||
rust: 1.63.0
|
||||
python: 3.7
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- name: Install Protoc
|
||||
uses: arduino/setup-protoc@v1
|
||||
with:
|
||||
version: '3.20.1'
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Install ${{ matrix.rust }}
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
|
||||
5
.github/workflows/jsonrpc.yml
vendored
5
.github/workflows/jsonrpc.yml
vendored
@@ -26,6 +26,11 @@ jobs:
|
||||
override: true
|
||||
- name: Add Rust cache
|
||||
uses: Swatinem/rust-cache@v1.3.0
|
||||
- name: Install Protoc
|
||||
uses: arduino/setup-protoc@v1
|
||||
with:
|
||||
version: '3.20.1'
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: npm install
|
||||
run: |
|
||||
cd deltachat-jsonrpc/typescript
|
||||
|
||||
6
.github/workflows/node-package.yml
vendored
6
.github/workflows/node-package.yml
vendored
@@ -27,7 +27,11 @@ jobs:
|
||||
cargo -vV
|
||||
npm --version
|
||||
node --version
|
||||
|
||||
- name: Install Protoc
|
||||
uses: arduino/setup-protoc@v1
|
||||
with:
|
||||
version: '3.20.1'
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
|
||||
6
.github/workflows/node-tests.yml
vendored
6
.github/workflows/node-tests.yml
vendored
@@ -27,7 +27,11 @@ jobs:
|
||||
cargo -vV
|
||||
npm --version
|
||||
node --version
|
||||
|
||||
- name: Install Protoc
|
||||
uses: arduino/setup-protoc@v1
|
||||
with:
|
||||
version: '3.20.1'
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
|
||||
4077
Cargo.lock
generated
4077
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ version = "1.102.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.57"
|
||||
rust-version = "1.63"
|
||||
|
||||
[profile.dev]
|
||||
debug = 0
|
||||
@@ -83,6 +83,11 @@ futures-lite = "1.12.0"
|
||||
tokio-stream = { version = "0.1.11", features = ["fs"] }
|
||||
reqwest = { version = "0.11.13", features = ["json"] }
|
||||
async_zip = { version = "0.0.9", default-features = false, features = ["deflate"] }
|
||||
iroh-share = { version = "0.1.3" }
|
||||
iroh-resolver = { version = "0.1.3", default-features = false }
|
||||
tempfile = "3"
|
||||
multibase = "0.9"
|
||||
port_check = "0.1.5"
|
||||
|
||||
[dev-dependencies]
|
||||
ansi_term = "0.12.0"
|
||||
|
||||
@@ -26,6 +26,8 @@ anyhow = "1"
|
||||
thiserror = "1"
|
||||
rand = "0.7"
|
||||
once_cell = "1.16.0"
|
||||
iroh-share = { version = "0.1.3" }
|
||||
multibase = "0.9"
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
|
||||
@@ -24,6 +24,7 @@ typedef struct _dc_provider dc_provider_t;
|
||||
typedef struct _dc_event dc_event_t;
|
||||
typedef struct _dc_event_emitter dc_event_emitter_t;
|
||||
typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t;
|
||||
typedef struct _dc_backup_sender dc_backup_sender_t;
|
||||
|
||||
// Alias for backwards compatibility, use dc_event_emitter_t instead.
|
||||
typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
|
||||
@@ -2291,6 +2292,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
#define DC_QR_FPR_MISMATCH 220 // id=contact
|
||||
#define DC_QR_FPR_WITHOUT_ADDR 230 // test1=formatted fingerprint
|
||||
#define DC_QR_ACCOUNT 250 // text1=domain
|
||||
#define DC_QR_BACKUP 251
|
||||
#define DC_QR_WEBRTC_INSTANCE 260 // text1=domain, text2=instance pattern
|
||||
#define DC_QR_ADDR 320 // id=contact
|
||||
#define DC_QR_TEXT 330 // text1=text
|
||||
@@ -2336,6 +2338,10 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
* ask the user if they want to create an account on the given domain,
|
||||
* if so, call dc_set_config_from_qr() and then dc_configure().
|
||||
*
|
||||
* - DC_QR_BACKUP:
|
||||
* ask the user if they want to set up a new device.
|
||||
* If so, pass the qr-code to dc_receive_backup().
|
||||
*
|
||||
* - DC_QR_WEBRTC_INSTANCE with dc_lot_t::text1=domain:
|
||||
* ask the user if they want to use the given service for video chats;
|
||||
* if so, call dc_set_config_from_qr().
|
||||
@@ -2626,6 +2632,85 @@ char* dc_get_last_error (dc_context_t* context);
|
||||
void dc_str_unref (char* str);
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_backup_sender_t
|
||||
*
|
||||
* Set up another device.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create an object for sending a backup to another device.
|
||||
* The backup is sent through an peer-to-peer channel which is bootstrapped by a QR-code.
|
||||
* As a backup contains the whole state of the account, including credentials,
|
||||
* this can be used to setup a new device.
|
||||
*
|
||||
* @memberof dc_backup_sender_t
|
||||
* @param context The context.
|
||||
* @param folder Path to create the backup in before it is sent out.
|
||||
* The path must exist and being accessible at least until the other device has received all data.
|
||||
* The folder is not cleaned up by the backup send;
|
||||
* a temporary directory seems to be good place therefore.
|
||||
* @param passphrase Used to at-rest-encrypt the backuped database in `folder` before sending,
|
||||
* similar to dc_imex().
|
||||
* The same passphrase must be given to dc_receive_backup() on the other device.
|
||||
* If NULL or empty string is given, the sent backup is not encrypted at rest.
|
||||
* The transport of the backup is always encrypted additionally.
|
||||
* @return Opaque object for sending the backup.
|
||||
* On errors, NULL is returned and dc_get_last_error() returns an error that should be shown to the user.
|
||||
*/
|
||||
dc_backup_sender_t* dc_send_backup (dc_context_t* context, const char* folder, const char* passphrase);
|
||||
|
||||
|
||||
/**
|
||||
* Get QR code text that will offer the backup to other devices.
|
||||
* The QR code contains a ticket that will unambiguously identify the backup.
|
||||
*
|
||||
* The scanning device will pass the scanned content to dc_check_qr() then;
|
||||
* if dc_check_qr() returns DC_QR_BACKUP,
|
||||
* the backup transfer can be started using dc_receive_backup().
|
||||
*
|
||||
* @memberof dc_backup_sender_t
|
||||
* @param context The context.
|
||||
* @param backup_sender The backup sender object as created by dc_send_backup().
|
||||
* @return The text that should go to the QR code,
|
||||
* On errors, an empty QR code is returned, NULL is never returned.
|
||||
* The returned string must be released using dc_str_unref() after usage.
|
||||
*/
|
||||
char* dc_backup_sender_qr (dc_context_t* context, const dc_backup_sender_t* backup_sender);
|
||||
|
||||
|
||||
/**
|
||||
* Waits for the sending to finish and frees the backup sender object.
|
||||
*
|
||||
* @memberof dc_backup_sender_t
|
||||
* @param context The context.
|
||||
* @param backup_sender The backup sender object as created by dc_send_backup(),
|
||||
* If NULL is given, nothing is done.
|
||||
*/
|
||||
void dc_backup_sender_done (dc_context_t* context, dc_backup_sender_t* backup_sender);
|
||||
|
||||
|
||||
/**
|
||||
* Receive a backup offered by a dc_backup_sender_t object on another device.
|
||||
*
|
||||
* This function is typically not called on the device where the dc_backup_sender_t object exist,
|
||||
* but on a device that has scanned the QR code generated by dc_backup_sender_qr().
|
||||
*
|
||||
* While dc_receive_backup() returns immediately, the started job may take a while;
|
||||
* you can stop it using dc_stop_ongoing_process().
|
||||
* During execution of the job #DC_EVENT_IMEX_PROGRESS is sent out several times to indicate state and progress.
|
||||
*
|
||||
* @memberof dc_backup_sender_t
|
||||
* @param context The context.
|
||||
* @param qr The qr code containing all data needed to transfer the backup;
|
||||
* usually, this is the case if dc_check_qr() returns DC_QR_BACKUP.
|
||||
* @param passphrase Passphrase for the additional at-rest-encryption
|
||||
* as defined at dc_send_backup() on the other device.
|
||||
* If no passphrase is used, pass NULL or empty string.
|
||||
*/
|
||||
void dc_receive_backup (dc_context_t* context, const char* qr, const char* passphrase);
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_accounts_t
|
||||
*
|
||||
|
||||
@@ -51,6 +51,7 @@ mod lot;
|
||||
mod string;
|
||||
use self::string::*;
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::qr::Qr;
|
||||
|
||||
// as C lacks a good and portable error handling,
|
||||
// in general, the C Interface is forgiving wrt to bad parameters.
|
||||
@@ -2204,6 +2205,97 @@ pub unsafe extern "C" fn dc_imex(
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_send_backup(
|
||||
context: *mut dc_context_t,
|
||||
folder: *const libc::c_char,
|
||||
passphrase: *const libc::c_char,
|
||||
) -> *mut dc_backup_sender {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_send_backup()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let passphrase = to_opt_string_lossy(passphrase);
|
||||
let ctx = &*context;
|
||||
|
||||
if let Some(folder) = to_opt_string_lossy(folder) {
|
||||
block_on(async move {
|
||||
imex::send_backup(ctx, folder.as_ref(), passphrase)
|
||||
.await
|
||||
.map(|transfer| Box::into_raw(Box::new(dc_backup_sender { transfer })))
|
||||
.log_err(ctx, "send_backup failed")
|
||||
.unwrap_or(ptr::null_mut())
|
||||
})
|
||||
} else {
|
||||
eprintln!("dc_imex called without a valid directory");
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct dc_backup_sender {
|
||||
transfer: iroh_share::SenderTransfer,
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_backup_sender_qr(
|
||||
ctx: *mut dc_context_t,
|
||||
bs: *const dc_backup_sender,
|
||||
) -> *mut libc::c_char {
|
||||
if ctx.is_null() || bs.is_null() {
|
||||
eprintln!("ignoring careless call to dc_backup_sender_qr");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let ctx = &*ctx;
|
||||
let bs = &*bs;
|
||||
let ticket = bs.transfer.ticket();
|
||||
|
||||
qr_code_generator::generate_backup_qr_code(ticket)
|
||||
.map(|s| s.strdup())
|
||||
.log_err(ctx, "generate_backup_qr_code failed")
|
||||
.unwrap_or(ptr::null_mut())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_backup_sender_done(ctx: *mut dc_context_t, bs: *mut dc_backup_sender) {
|
||||
if ctx.is_null() || bs.is_null() {
|
||||
eprintln!("ignoring careless call to dc_backup_sender_wait");
|
||||
return;
|
||||
}
|
||||
let ctx = &*ctx;
|
||||
let bs = Box::from_raw(bs);
|
||||
|
||||
block_on(async move {
|
||||
if let Err(e) = bs.transfer.done().await {
|
||||
error!(ctx, "sending backup failed: {:?}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_receive_backup(
|
||||
ctx: *mut dc_context_t,
|
||||
qr: *const libc::c_char,
|
||||
passphrase: *const libc::c_char,
|
||||
) {
|
||||
if ctx.is_null() {
|
||||
eprintln!("ignoring careless call to dc_receive_backup");
|
||||
return;
|
||||
}
|
||||
let ctx = &*ctx;
|
||||
let ticket = match block_on(qr::check_qr(ctx, &to_string_lossy(qr))) {
|
||||
Ok(Qr::Backup { ticket }) => ticket.as_bytes(),
|
||||
_ => vec![],
|
||||
};
|
||||
let passphrase = to_opt_string_lossy(passphrase);
|
||||
|
||||
spawn(async move {
|
||||
imex::receive_backup(ctx, ticket, passphrase)
|
||||
.await
|
||||
.log_err(ctx, "IMEX failed")
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_imex_has_backup(
|
||||
context: *mut dc_context_t,
|
||||
|
||||
@@ -50,6 +50,7 @@ impl Lot {
|
||||
Qr::FprMismatch { .. } => None,
|
||||
Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint),
|
||||
Qr::Account { domain } => Some(domain),
|
||||
Qr::Backup { .. } => None,
|
||||
Qr::WebrtcInstance { domain, .. } => Some(domain),
|
||||
Qr::Addr { draft, .. } => draft.as_deref(),
|
||||
Qr::Url { url } => Some(url),
|
||||
@@ -101,6 +102,7 @@ impl Lot {
|
||||
Qr::FprMismatch { .. } => LotState::QrFprMismatch,
|
||||
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
|
||||
Qr::Account { .. } => LotState::QrAccount,
|
||||
Qr::Backup { .. } => LotState::QrBackup,
|
||||
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
|
||||
Qr::Addr { .. } => LotState::QrAddr,
|
||||
Qr::Url { .. } => LotState::QrUrl,
|
||||
@@ -125,6 +127,7 @@ 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::WebrtcInstance { .. } => Default::default(),
|
||||
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
|
||||
Qr::Url { .. } => Default::default(),
|
||||
@@ -173,6 +176,9 @@ pub enum LotState {
|
||||
/// text1=domain
|
||||
QrAccount = 250,
|
||||
|
||||
/// TODO
|
||||
QrBackup = 251,
|
||||
|
||||
/// text1=domain, text2=instance pattern
|
||||
QrWebrtcInstance = 260,
|
||||
|
||||
|
||||
@@ -20,3 +20,215 @@ fn maybe_empty_string_to_option(string: String) -> Option<String> {
|
||||
Some(string)
|
||||
}
|
||||
}
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[serde(rename = "Qr", rename_all = "camelCase")]
|
||||
#[serde(tag = "type")]
|
||||
pub enum QrObject {
|
||||
AskVerifyContact {
|
||||
contact_id: u32,
|
||||
fingerprint: String,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
},
|
||||
AskVerifyGroup {
|
||||
grpname: String,
|
||||
grpid: String,
|
||||
contact_id: u32,
|
||||
fingerprint: String,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
},
|
||||
FprOk {
|
||||
contact_id: u32,
|
||||
},
|
||||
FprMismatch {
|
||||
contact_id: Option<u32>,
|
||||
},
|
||||
FprWithoutAddr {
|
||||
fingerprint: String,
|
||||
},
|
||||
Account {
|
||||
domain: String,
|
||||
},
|
||||
WebrtcInstance {
|
||||
domain: String,
|
||||
instance_pattern: String,
|
||||
},
|
||||
Addr {
|
||||
contact_id: u32,
|
||||
draft: Option<String>,
|
||||
},
|
||||
Url {
|
||||
url: String,
|
||||
},
|
||||
Text {
|
||||
text: String,
|
||||
},
|
||||
WithdrawVerifyContact {
|
||||
contact_id: u32,
|
||||
fingerprint: String,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
},
|
||||
WithdrawVerifyGroup {
|
||||
grpname: String,
|
||||
grpid: String,
|
||||
contact_id: u32,
|
||||
fingerprint: String,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
},
|
||||
ReviveVerifyContact {
|
||||
contact_id: u32,
|
||||
fingerprint: String,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
},
|
||||
ReviveVerifyGroup {
|
||||
grpname: String,
|
||||
grpid: String,
|
||||
contact_id: u32,
|
||||
fingerprint: String,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
},
|
||||
Login {
|
||||
address: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<Qr> for QrObject {
|
||||
fn from(qr: Qr) -> Self {
|
||||
match qr {
|
||||
Qr::AskVerifyContact {
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::AskVerifyContact {
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
}
|
||||
}
|
||||
Qr::AskVerifyGroup {
|
||||
grpname,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::AskVerifyGroup {
|
||||
grpname,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
}
|
||||
}
|
||||
Qr::FprOk { contact_id } => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
QrObject::FprOk { contact_id }
|
||||
}
|
||||
Qr::FprMismatch { contact_id } => {
|
||||
let contact_id = contact_id.map(|contact_id| contact_id.to_u32());
|
||||
QrObject::FprMismatch { contact_id }
|
||||
}
|
||||
Qr::FprWithoutAddr { fingerprint } => QrObject::FprWithoutAddr { fingerprint },
|
||||
Qr::Account { domain } => QrObject::Account { domain },
|
||||
Qr::WebrtcInstance {
|
||||
domain,
|
||||
instance_pattern,
|
||||
} => QrObject::WebrtcInstance {
|
||||
domain,
|
||||
instance_pattern,
|
||||
},
|
||||
Qr::Addr { contact_id, draft } => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
QrObject::Addr { contact_id, draft }
|
||||
}
|
||||
Qr::Url { url } => QrObject::Url { url },
|
||||
Qr::Text { text } => QrObject::Text { text },
|
||||
Qr::WithdrawVerifyContact {
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::WithdrawVerifyContact {
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
}
|
||||
}
|
||||
Qr::WithdrawVerifyGroup {
|
||||
grpname,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::WithdrawVerifyGroup {
|
||||
grpname,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
}
|
||||
}
|
||||
Qr::ReviveVerifyContact {
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::ReviveVerifyContact {
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
}
|
||||
}
|
||||
Qr::ReviveVerifyGroup {
|
||||
grpname,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::ReviveVerifyGroup {
|
||||
grpname,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
}
|
||||
}
|
||||
Qr::Login { address, .. } => QrObject::Login { address },
|
||||
Qr::Backup { ticket } => QrObject::Backup {
|
||||
ticket: ticket.as_bytes(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ export type U32=number;
|
||||
export type Account=(({"type":"Configured";}&{"id":U32;"displayName":(string|null);"addr":(string|null);"profileImage":(string|null);"color":string;})|({"type":"Unconfigured";}&{"id":U32;}));
|
||||
export type U64=number;
|
||||
export type ProviderInfo={"beforeLoginHint":string;"overviewPage":string;"status":U32;};
|
||||
export type Qr=(({"type":"askVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"askVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"fprOk";}&{"contact_id":U32;})|({"type":"fprMismatch";}&{"contact_id":(U32|null);})|({"type":"fprWithoutAddr";}&{"fingerprint":string;})|({"type":"account";}&{"domain":string;})|({"type":"webrtcInstance";}&{"domain":string;"instance_pattern":string;})|({"type":"addr";}&{"contact_id":U32;"draft":(string|null);})|({"type":"url";}&{"url":string;})|({"type":"text";}&{"text":string;})|({"type":"withdrawVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"withdrawVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"reviveVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"reviveVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"login";}&{"address":string;}));
|
||||
export type U8=number;
|
||||
export type Qr=(({"type":"askVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"askVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"fprOk";}&{"contact_id":U32;})|({"type":"fprMismatch";}&{"contact_id":(U32|null);})|({"type":"fprWithoutAddr";}&{"fingerprint":string;})|({"type":"account";}&{"domain":string;})|({"type":"webrtcInstance";}&{"domain":string;"instance_pattern":string;})|({"type":"addr";}&{"contact_id":U32;"draft":(string|null);})|({"type":"url";}&{"url":string;})|({"type":"text";}&{"text":string;})|({"type":"withdrawVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"withdrawVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"reviveVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"reviveVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"login";}&{"address":string;})|({"type":"backup";}&{"ticket":(U8)[];}));
|
||||
export type Usize=number;
|
||||
export type I64=number;
|
||||
export type ChatListEntry=[U32,U32];
|
||||
|
||||
@@ -335,6 +335,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
continue-key-transfer <msg-id> <setup-code>\n\
|
||||
has-backup\n\
|
||||
export-backup\n\
|
||||
send-backup [<passphrase>]\n\
|
||||
receive-backup <ticket> [<passphrase>]\n\
|
||||
import-backup <backup-file>\n\
|
||||
export-keys\n\
|
||||
import-keys\n\
|
||||
@@ -475,7 +477,30 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
Some(arg2.to_string()),
|
||||
)
|
||||
.await?;
|
||||
println!("Exported to {}.", dir.to_string_lossy());
|
||||
println!("Exported to {}.", dir.display());
|
||||
}
|
||||
"send-backup" => {
|
||||
let tdir = tempfile::TempDir::new()?;
|
||||
let dir = tdir.path();
|
||||
println!("Storing backup in: {} ", dir.display());
|
||||
let transfer = send_backup(&context, dir, Some(arg1.to_string())).await?;
|
||||
let ticket = transfer.ticket();
|
||||
let ticket_bytes = ticket.as_bytes();
|
||||
|
||||
println!(
|
||||
"Ticket: {}",
|
||||
multibase::encode(multibase::Base::Base64, &ticket_bytes)
|
||||
);
|
||||
let qr_code = deltachat::qr_code_generator::generate_backup_qr_code(ticket)?;
|
||||
let file = dir.join("qr.svg");
|
||||
tokio::fs::write(file, qr_code.as_bytes()).await?;
|
||||
|
||||
transfer.done().await?;
|
||||
}
|
||||
"receive-backup" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <ticket> is missing.");
|
||||
let (_, ticket) = multibase::decode(&arg1)?;
|
||||
receive_backup(&context, ticket, Some(arg2.to_string())).await?;
|
||||
}
|
||||
"import-backup" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <backup-file> missing.");
|
||||
|
||||
@@ -155,13 +155,15 @@ impl Completer for DcHelper {
|
||||
}
|
||||
}
|
||||
|
||||
const IMEX_COMMANDS: [&str; 12] = [
|
||||
const IMEX_COMMANDS: [&str; 14] = [
|
||||
"initiate-key-transfer",
|
||||
"get-setupcodebegin",
|
||||
"continue-key-transfer",
|
||||
"has-backup",
|
||||
"export-backup",
|
||||
"import-backup",
|
||||
"send-backup",
|
||||
"receive-backup",
|
||||
"export-keys",
|
||||
"import-keys",
|
||||
"export-setup",
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.61.0
|
||||
1.63.0
|
||||
|
||||
@@ -7,7 +7,7 @@ set -euo pipefail
|
||||
#
|
||||
# Avoid using rustup here as it depends on reading /proc/self/exe and
|
||||
# has problems running under QEMU.
|
||||
RUST_VERSION=1.61.0
|
||||
RUST_VERSION=1.63.0
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
@@ -222,7 +222,7 @@ impl<'a> BlobObject<'a> {
|
||||
/// to be lowercase.
|
||||
pub fn suffix(&self) -> Option<&str> {
|
||||
let ext = self.name.rsplit('.').next();
|
||||
if ext == Some(&self.name) {
|
||||
if ext == Some(self.name.as_str()) {
|
||||
None
|
||||
} else {
|
||||
ext
|
||||
|
||||
@@ -455,7 +455,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
|
||||
progress!(ctx, 910);
|
||||
|
||||
if ctx.get_config(Config::ConfiguredAddr).await?.as_deref() != Some(¶m.addr) {
|
||||
if ctx.get_config(Config::ConfiguredAddr).await?.as_deref() != Some(param.addr.as_str()) {
|
||||
// Switched account, all server UIDs we know are invalid
|
||||
job::schedule_resync(ctx).await?;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ fn format_line_flowed(line: &str, prefix: &str) -> String {
|
||||
after_space = false;
|
||||
}
|
||||
}
|
||||
result + &buffer
|
||||
result + buffer.as_str()
|
||||
}
|
||||
|
||||
/// Returns text formatted according to RFC 3767 (format=flowed).
|
||||
|
||||
319
src/imex.rs
319
src/imex.rs
@@ -5,11 +5,12 @@ use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use ::pgp::types::KeyTrait;
|
||||
use anyhow::{bail, ensure, format_err, Context as _, Result};
|
||||
use futures::StreamExt;
|
||||
use anyhow::{anyhow, bail, ensure, format_err, Context as _, Result};
|
||||
use futures::{StreamExt, TryStreamExt};
|
||||
use futures_lite::FutureExt;
|
||||
use rand::{thread_rng, Rng};
|
||||
use tokio::fs::{self, File};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio_tar::Archive;
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
@@ -106,6 +107,188 @@ pub async fn imex(
|
||||
res
|
||||
}
|
||||
|
||||
/// Receives a backup, as sent from `send_backup`.
|
||||
///
|
||||
/// Only one receive-progress can run at the same time.
|
||||
/// To cancel a receive-progress, drop the future returned by this function.
|
||||
pub async fn receive_backup(
|
||||
context: &Context,
|
||||
ticket_bytes: Vec<u8>,
|
||||
passphrase: Option<String>,
|
||||
) -> Result<()> {
|
||||
let cancel = context.alloc_ongoing().await?;
|
||||
|
||||
let res = receive_backup_inner(context, ticket_bytes, passphrase.unwrap_or_default())
|
||||
.race(async {
|
||||
cancel.recv().await.ok();
|
||||
Err(format_err!("canceled"))
|
||||
})
|
||||
.await;
|
||||
|
||||
context.free_ongoing().await;
|
||||
|
||||
if let Err(err) = res.as_ref() {
|
||||
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
|
||||
error!(context, "IMEX failed to complete: {:#}", err);
|
||||
context.emit_event(EventType::ImexProgress(0));
|
||||
} else {
|
||||
info!(context, "IMEX successfully completed");
|
||||
context.emit_event(EventType::ImexProgress(1000));
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
async fn receive_backup_inner(
|
||||
context: &Context,
|
||||
ticket_bytes: Vec<u8>,
|
||||
passphrase: String,
|
||||
) -> Result<()> {
|
||||
use iroh_share::{Receiver, Ticket};
|
||||
ensure!(
|
||||
!context.is_configured().await?,
|
||||
"Cannot import backups to accounts in use."
|
||||
);
|
||||
ensure!(
|
||||
context.scheduler.read().await.is_none(),
|
||||
"cannot import backup, IO is running"
|
||||
);
|
||||
let ticket = Ticket::from_bytes(&ticket_bytes)?;
|
||||
|
||||
let recv_dir = tempfile::tempdir().unwrap();
|
||||
let recv_db = recv_dir.path().join("db");
|
||||
let port = port_check::free_local_port()
|
||||
.ok_or_else(|| anyhow!("failed to find free local port to bind to"))?;
|
||||
|
||||
let receiver = Receiver::new(port, &recv_db)
|
||||
.await
|
||||
.context("failed to create receiver")?;
|
||||
let mut receiver_transfer = receiver
|
||||
.transfer_from_ticket(&ticket)
|
||||
.await
|
||||
.context("failed to read transfer")?;
|
||||
let data = receiver_transfer.recv().await?;
|
||||
let mut progress = receiver_transfer.progress()?;
|
||||
|
||||
context.sql.config_cache.write().await.clear();
|
||||
|
||||
// progress report
|
||||
let ctx = context.clone();
|
||||
let progress_task = tokio::spawn(async move {
|
||||
let mut last_progress = 0;
|
||||
while let Some(ev) = progress.next().await {
|
||||
match ev {
|
||||
Ok(iroh_share::ProgressEvent::Piece { index, total }) => {
|
||||
let progress = 1000 * index / total;
|
||||
if progress != last_progress && progress < 1000 {
|
||||
ctx.emit_event(EventType::ImexProgress(progress));
|
||||
last_progress = progress;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(ctx, "IMEX receive backup failed to complete: {}", err);
|
||||
ctx.emit_event(EventType::ImexProgress(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let out = context.get_blobdir();
|
||||
|
||||
let mut read_dir_stream = data
|
||||
.read_dir()?
|
||||
.ok_or_else(|| anyhow::anyhow!("unexpected data structure"))?;
|
||||
|
||||
while let Some(link) = read_dir_stream.next().await {
|
||||
let link = link?;
|
||||
let file_content = data.read_file(&link).await?;
|
||||
let name = link.name.unwrap_or_default();
|
||||
let path = out.join(&name);
|
||||
|
||||
let file = tokio::fs::File::create(&path)
|
||||
.await
|
||||
.with_context(|| format!("create file: {}", path.display()))?;
|
||||
let mut file = tokio::io::BufWriter::new(file);
|
||||
let mut content = file_content.pretty()?;
|
||||
tokio::io::copy(&mut content, &mut file)
|
||||
.await
|
||||
.context("copy")?;
|
||||
file.flush().await?;
|
||||
|
||||
if name == DBFILE_BACKUP_NAME {
|
||||
let unpacked_database = context.get_blobdir().join(DBFILE_BACKUP_NAME);
|
||||
context
|
||||
.sql
|
||||
.import(&unpacked_database, passphrase.clone())
|
||||
.await
|
||||
.context("cannot import unpacked database")?;
|
||||
fs::remove_file(unpacked_database)
|
||||
.await
|
||||
.context("cannot remove unpacked database")?;
|
||||
} else {
|
||||
// nothing to do, unpacked directly into the blobs dir
|
||||
}
|
||||
}
|
||||
|
||||
progress_task.await?;
|
||||
receiver_transfer.finish().await?;
|
||||
|
||||
info!(context, "Received all data, written to: {}", out.display());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a backup.
|
||||
///
|
||||
/// Only one send-progress can run at the same time.
|
||||
/// To cancel a send-progress, drop the future returned by this function.
|
||||
pub async fn send_backup(
|
||||
context: &Context,
|
||||
path: &Path,
|
||||
passphrase: Option<String>,
|
||||
) -> Result<iroh_share::SenderTransfer> {
|
||||
let cancel = context.alloc_ongoing().await?;
|
||||
|
||||
let res = send_backup_inner(context, path, passphrase)
|
||||
.race(async {
|
||||
cancel.recv().await.ok();
|
||||
Err(format_err!("canceled"))
|
||||
})
|
||||
.await;
|
||||
|
||||
context.free_ongoing().await;
|
||||
|
||||
if let Err(err) = res.as_ref() {
|
||||
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
|
||||
error!(context, "Send backup failed to complete: {:#}", err);
|
||||
context.emit_event(EventType::ImexProgress(0));
|
||||
} else {
|
||||
info!(context, "Send backup successfully completed");
|
||||
context.emit_event(EventType::ImexProgress(1000));
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
async fn send_backup_inner(
|
||||
context: &Context,
|
||||
path: &Path,
|
||||
passphrase: Option<String>,
|
||||
) -> Result<iroh_share::SenderTransfer> {
|
||||
info!(context, "Send backup dir: {}", path.display());
|
||||
ensure!(context.sql.is_open().await, "Database not opened.");
|
||||
context.emit_event(EventType::ImexProgress(10));
|
||||
|
||||
// before we export anything, make sure the private key exists
|
||||
if e2ee::ensure_secret_key_exists(context).await.is_err() {
|
||||
bail!("Cannot create private key or private key not available.");
|
||||
} else {
|
||||
create_folder(context, &path).await?;
|
||||
}
|
||||
|
||||
export_backup_iroh(context, path, passphrase.unwrap_or_default()).await
|
||||
}
|
||||
|
||||
/// Returns the filename of the backup found (otherwise an error)
|
||||
pub async fn has_backup(_context: &Context, dir_name: &Path) -> Result<String> {
|
||||
let mut dir_iter = tokio::fs::read_dir(dir_name).await?;
|
||||
@@ -478,7 +661,8 @@ async fn import_backup(
|
||||
/// it can be renamed to dest_path. This guarantees that the backup is complete.
|
||||
fn get_next_backup_path(folder: &Path, backup_time: i64) -> Result<(PathBuf, PathBuf, PathBuf)> {
|
||||
let folder = PathBuf::from(folder);
|
||||
let stem = chrono::NaiveDateTime::from_timestamp(backup_time, 0)
|
||||
let stem = chrono::NaiveDateTime::from_timestamp_opt(backup_time, 0)
|
||||
.unwrap()
|
||||
// Don't change this file name format, in `dc_imex_has_backup` we use string comparison to determine which backup is newer:
|
||||
.format("delta-chat-backup-%Y-%m-%d")
|
||||
.to_string();
|
||||
@@ -608,6 +792,135 @@ async fn export_backup_inner(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn export_backup_iroh(
|
||||
context: &Context,
|
||||
dir: &Path,
|
||||
passphrase: String,
|
||||
) -> Result<iroh_share::SenderTransfer> {
|
||||
// get a fine backup file name (the name includes the date so that multiple backup instances are possible)
|
||||
let now = time();
|
||||
let (temp_db_path, temp_path, dest_path) = get_next_backup_path(dir, now)?;
|
||||
let sender_db_path = dir.join("iroh_db");
|
||||
let _d1 = DeleteOnDrop(temp_db_path.clone());
|
||||
let _d2 = DeleteOnDrop(temp_path.clone());
|
||||
let _d3 = DeleteOnDrop(sender_db_path.clone());
|
||||
|
||||
context
|
||||
.sql
|
||||
.set_raw_config_int("backup_time", now as i32)
|
||||
.await?;
|
||||
sql::housekeeping(context).await.ok_or_log(context);
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute("VACUUM;", paramsv![])
|
||||
.await
|
||||
.map_err(|e| warn!(context, "Vacuum failed, exporting anyway {}", e))
|
||||
.ok();
|
||||
|
||||
ensure!(
|
||||
context.scheduler.read().await.is_none(),
|
||||
"cannot export backup, IO is running"
|
||||
);
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Backup '{}' to '{}'.",
|
||||
context.get_dbfile().display(),
|
||||
dest_path.display(),
|
||||
);
|
||||
|
||||
context
|
||||
.sql
|
||||
.export(&temp_db_path, passphrase)
|
||||
.await
|
||||
.with_context(|| format!("failed to backup plaintext database to {:?}", temp_db_path))?;
|
||||
|
||||
let res = export_backup_iroh_inner(context, &temp_db_path, &temp_path).await;
|
||||
|
||||
match res {
|
||||
Ok(dir_builder) => {
|
||||
let port = port_check::free_local_port()
|
||||
.ok_or_else(|| anyhow!("failed to find free local port to bind to"))?;
|
||||
let sender = iroh_share::Sender::new(port, &sender_db_path).await?;
|
||||
let transfer = sender.transfer_from_dir_builder(dir_builder).await?;
|
||||
|
||||
Ok(transfer)
|
||||
}
|
||||
Err(e) => {
|
||||
error!(context, "backup failed: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn export_backup_iroh_inner(
|
||||
context: &Context,
|
||||
temp_db_path: &Path,
|
||||
temp_path: &Path,
|
||||
) -> Result<iroh_resolver::unixfs_builder::DirectoryBuilder> {
|
||||
use iroh_resolver::unixfs_builder::{DirectoryBuilder, FileBuilder};
|
||||
|
||||
let mut dir_builder = DirectoryBuilder::new();
|
||||
dir_builder.name(
|
||||
temp_path
|
||||
.file_name()
|
||||
.map(|s| s.to_string_lossy())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
|
||||
{
|
||||
let db_content = tokio::fs::File::open(temp_db_path).await?;
|
||||
let file = FileBuilder::new()
|
||||
.name(DBFILE_BACKUP_NAME)
|
||||
.content_reader(db_content)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
dir_builder.add_file(file);
|
||||
}
|
||||
|
||||
let read_dir: Vec<_> =
|
||||
tokio_stream::wrappers::ReadDirStream::new(fs::read_dir(context.get_blobdir()).await?)
|
||||
.try_collect()
|
||||
.await?;
|
||||
let count = read_dir.len();
|
||||
let mut written_files = 0;
|
||||
|
||||
let mut last_progress = 0;
|
||||
for entry in read_dir.into_iter() {
|
||||
let name = entry.file_name();
|
||||
if !entry.file_type().await?.is_file() {
|
||||
warn!(
|
||||
context,
|
||||
"Export: Found dir entry {} that is not a file, ignoring",
|
||||
name.to_string_lossy()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let file_content = File::open(entry.path()).await?;
|
||||
|
||||
{
|
||||
let file = FileBuilder::new()
|
||||
.name(name.to_string_lossy().to_owned())
|
||||
.content_reader(file_content)
|
||||
.build()
|
||||
.await?;
|
||||
dir_builder.add_file(file);
|
||||
}
|
||||
|
||||
written_files += 1;
|
||||
let progress = 1000 * written_files / count;
|
||||
if progress != last_progress && progress > 10 && progress < 1000 {
|
||||
// We already emitted ImexProgress(10) above
|
||||
context.emit_event(EventType::ImexProgress(progress));
|
||||
last_progress = progress;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(dir_builder)
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* Classic key import
|
||||
******************************************************************************/
|
||||
|
||||
@@ -476,7 +476,8 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)
|
||||
|
||||
fn get_kml_timestamp(utc: i64) -> String {
|
||||
// Returns a string formatted as YYYY-MM-DDTHH:MM:SSZ. The trailing `Z` indicates UTC.
|
||||
chrono::NaiveDateTime::from_timestamp(utc, 0)
|
||||
chrono::NaiveDateTime::from_timestamp_opt(utc, 0)
|
||||
.unwrap()
|
||||
.format("%Y-%m-%dT%H:%M:%SZ")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
@@ -531,7 +531,9 @@ impl<'a> MimeFactory<'a> {
|
||||
.push(Header::new("Subject".into(), encoded_subject));
|
||||
|
||||
let date = chrono::Utc
|
||||
.from_local_datetime(&chrono::NaiveDateTime::from_timestamp(self.timestamp, 0))
|
||||
.from_local_datetime(
|
||||
&chrono::NaiveDateTime::from_timestamp_opt(self.timestamp, 0).unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
.to_rfc2822();
|
||||
headers.unprotected.push(Header::new("Date".into(), date));
|
||||
@@ -1285,19 +1287,20 @@ impl<'a> MimeFactory<'a> {
|
||||
let extension_fields = if additional_msg_ids.is_empty() {
|
||||
"".to_string()
|
||||
} else {
|
||||
"Additional-Message-IDs: ".to_string()
|
||||
+ &additional_msg_ids
|
||||
format!(
|
||||
"Additional-Message-IDs: {}\r\n",
|
||||
additional_msg_ids
|
||||
.iter()
|
||||
.map(|mid| render_rfc724_mid(mid))
|
||||
.collect::<Vec<String>>()
|
||||
.join(" ")
|
||||
+ "\r\n"
|
||||
)
|
||||
};
|
||||
|
||||
message = message.child(
|
||||
PartBuilder::new()
|
||||
.content_type(&"message/disposition-notification".parse().unwrap())
|
||||
.body(message_text2 + &extension_fields)
|
||||
.body(message_text2 + extension_fields.as_str())
|
||||
.build(),
|
||||
);
|
||||
|
||||
@@ -1337,14 +1340,16 @@ async fn build_body_file(
|
||||
// etc.
|
||||
let filename_to_send: String = match msg.viewtype {
|
||||
Viewtype::Voice => chrono::Utc
|
||||
.timestamp(msg.timestamp_sort, 0)
|
||||
.timestamp_opt(msg.timestamp_sort, 0)
|
||||
.unwrap()
|
||||
.format(&format!("voice-message_%Y-%m-%d_%H-%M-%S.{}", &suffix))
|
||||
.to_string(),
|
||||
Viewtype::Image | Viewtype::Gif => format!(
|
||||
"{}.{}",
|
||||
if base_name.is_empty() {
|
||||
chrono::Utc
|
||||
.timestamp(msg.timestamp_sort, 0)
|
||||
.timestamp_opt(msg.timestamp_sort, 0)
|
||||
.unwrap()
|
||||
.format("image_%Y-%m-%d_%H-%M-%S")
|
||||
.to_string()
|
||||
} else {
|
||||
@@ -1355,7 +1360,8 @@ async fn build_body_file(
|
||||
Viewtype::Video => format!(
|
||||
"video_{}.{}",
|
||||
chrono::Utc
|
||||
.timestamp(msg.timestamp_sort, 0)
|
||||
.timestamp_opt(msg.timestamp_sort, 0)
|
||||
.unwrap()
|
||||
.format("%Y-%m-%d_%H-%M-%S"),
|
||||
&suffix
|
||||
),
|
||||
|
||||
@@ -67,7 +67,7 @@ impl PlainText {
|
||||
// and is only there to allow > at the beginning of a line that is no quote.
|
||||
line = line.strip_prefix(' ').unwrap_or(&line).to_string();
|
||||
if is_quote {
|
||||
line = "<em>".to_owned() + &line + "</em>";
|
||||
line = format!("<em>{}</em>", line);
|
||||
}
|
||||
|
||||
// a trailing space indicates that the line can be merged with the next one;
|
||||
@@ -83,7 +83,7 @@ impl PlainText {
|
||||
} else {
|
||||
// normal, fixed text
|
||||
if is_quote {
|
||||
line = "<em>".to_owned() + &line + "</em>";
|
||||
line = format!("<em>{}</em>", &line);
|
||||
}
|
||||
line += "<br/>\n";
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ pub async fn get_provider_by_mx(context: &Context, domain: &str) -> Option<&'sta
|
||||
}
|
||||
|
||||
let provider_fqdn = provider_domain.to_string() + ".";
|
||||
let provider_fqdn_dot = ".".to_string() + &provider_fqdn;
|
||||
let provider_fqdn_dot = format!(".{}", provider_fqdn);
|
||||
|
||||
for mx_domain in mx_domains.iter() {
|
||||
let mx_domain = mx_domain.exchange().to_lowercase().to_utf8();
|
||||
@@ -184,7 +184,9 @@ pub fn get_provider_by_id(id: &str) -> Option<&'static Provider> {
|
||||
|
||||
// returns update timestamp in seconds, UTC, compatible for comparison with time() and database times
|
||||
pub fn get_provider_update_timestamp() -> i64 {
|
||||
NaiveDateTime::new(*PROVIDER_UPDATED, NaiveTime::from_hms(0, 0, 0)).timestamp_millis() / 1_000
|
||||
NaiveDateTime::new(*PROVIDER_UPDATED, NaiveTime::from_hms_opt(0, 0, 0).unwrap())
|
||||
.timestamp_millis()
|
||||
/ 1_000
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -260,8 +262,8 @@ mod tests {
|
||||
#[test]
|
||||
fn test_get_provider_update_timestamp() {
|
||||
let timestamp_past = NaiveDateTime::new(
|
||||
NaiveDate::from_ymd(2020, 9, 9),
|
||||
NaiveTime::from_hms(0, 0, 0),
|
||||
NaiveDate::from_ymd_opt(2020, 9, 9).unwrap(),
|
||||
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
|
||||
)
|
||||
.timestamp_millis()
|
||||
/ 1_000;
|
||||
|
||||
@@ -1891,4 +1891,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(2022, 7, 5));
|
||||
Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2022, 7, 5).unwrap());
|
||||
|
||||
20
src/qr.rs
20
src/qr.rs
@@ -32,6 +32,7 @@ const VCARD_SCHEME: &str = "BEGIN:VCARD";
|
||||
const SMTP_SCHEME: &str = "SMTP:";
|
||||
const HTTP_SCHEME: &str = "http://";
|
||||
const HTTPS_SCHEME: &str = "https://";
|
||||
pub const DCBACKUP_SCHEME: &str = "DCBACKUP:";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Qr {
|
||||
@@ -61,6 +62,9 @@ pub enum Qr {
|
||||
Account {
|
||||
domain: String,
|
||||
},
|
||||
Backup {
|
||||
ticket: iroh_share::Ticket,
|
||||
},
|
||||
WebrtcInstance {
|
||||
domain: String,
|
||||
instance_pattern: String,
|
||||
@@ -127,6 +131,8 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
|
||||
decode_account(qr)?
|
||||
} else if starts_with_ignore_case(qr, DCLOGIN_SCHEME) {
|
||||
dclogin_scheme::decode_login(qr)?
|
||||
} else if starts_with_ignore_case(qr, DCBACKUP_SCHEME) {
|
||||
decode_backup(qr)?
|
||||
} else if starts_with_ignore_case(qr, DCWEBRTC_SCHEME) {
|
||||
decode_webrtc_instance(context, qr)?
|
||||
} else if qr.starts_with(MAILTO_SCHEME) {
|
||||
@@ -332,6 +338,18 @@ fn decode_account(qr: &str) -> Result<Qr> {
|
||||
}
|
||||
}
|
||||
|
||||
/// scheme: `DCBACKUP:<multibase + bincode encode ticket>`
|
||||
fn decode_backup(qr: &str) -> Result<Qr> {
|
||||
let payload = qr
|
||||
.get(DCBACKUP_SCHEME.len()..)
|
||||
.context("invalid DCBACKUP payload")?;
|
||||
|
||||
let (_, ticket_bytes) = multibase::decode(payload)?;
|
||||
let ticket = iroh_share::Ticket::from_bytes(&ticket_bytes)?;
|
||||
|
||||
Ok(Qr::Backup { ticket })
|
||||
}
|
||||
|
||||
/// scheme: `DCWEBRTC:https://meet.jit.si/$ROOM`
|
||||
fn decode_webrtc_instance(_context: &Context, qr: &str) -> Result<Qr> {
|
||||
let payload = qr
|
||||
@@ -516,7 +534,7 @@ async fn decode_mailto(context: &Context, qr: &str) -> Result<Qr> {
|
||||
if subject.is_empty() {
|
||||
body.to_string()
|
||||
} else {
|
||||
subject + "\n" + body
|
||||
subject + "\n" + *body
|
||||
}
|
||||
} else {
|
||||
subject
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::{
|
||||
config::Config,
|
||||
contact::{Contact, ContactId},
|
||||
context::Context,
|
||||
qr::DCBACKUP_SCHEME,
|
||||
securejoin, stock_str,
|
||||
};
|
||||
|
||||
@@ -262,6 +263,93 @@ fn inner_generate_secure_join_qr_code(
|
||||
Ok(svg)
|
||||
}
|
||||
|
||||
pub fn generate_backup_qr_code(ticket: &iroh_share::Ticket) -> Result<String> {
|
||||
let ticket_bytes = ticket.as_bytes();
|
||||
let ticket_str = multibase::encode(multibase::Base::Base64, &ticket_bytes);
|
||||
let ticket_str = format!("{}{}", DCBACKUP_SCHEME, ticket_str);
|
||||
// config
|
||||
let width = 515.0;
|
||||
let height = 630.0;
|
||||
let qr_code_size = 400.0;
|
||||
let qr_translate_up = 40.0;
|
||||
let card_roundness = 40.0;
|
||||
let card_border_size = 2.0;
|
||||
|
||||
let qr = QrCode::encode_text(&ticket_str, QrCodeEcc::Medium)?;
|
||||
let mut svg = String::with_capacity(28000);
|
||||
let mut w = tagger::new(&mut svg);
|
||||
|
||||
w.elem("svg", |d| {
|
||||
d.attr("xmlns", "http://www.w3.org/2000/svg")?;
|
||||
d.attr("viewBox", format_args!("0 0 {} {}", width, height))?;
|
||||
Ok(())
|
||||
})?
|
||||
.build(|w| {
|
||||
// White Background apears like a card
|
||||
w.single("rect", |d| {
|
||||
d.attr("x", card_border_size)?;
|
||||
d.attr("y", card_border_size)?;
|
||||
d.attr("rx", card_roundness)?;
|
||||
d.attr("stroke", "#c6c6c6")?;
|
||||
d.attr("stroke-width", card_border_size)?;
|
||||
d.attr("width", width - (card_border_size * 2.0))?;
|
||||
d.attr("height", height - (card_border_size * 2.0))?;
|
||||
d.attr("style", "fill:#f2f2f2")?;
|
||||
Ok(())
|
||||
})?;
|
||||
// Qrcode
|
||||
w.elem("g", |d| {
|
||||
d.attr(
|
||||
"transform",
|
||||
format!(
|
||||
"translate({},{})",
|
||||
(width - qr_code_size) / 2.0,
|
||||
((height - qr_code_size) / 2.0) - qr_translate_up
|
||||
),
|
||||
)
|
||||
// If the qr code should be in the wrong place,
|
||||
// we could also translate and scale the points in the path already,
|
||||
// but that would make the resulting svg way bigger in size and might bring up rounding issues,
|
||||
// so better avoid doing it manually if possible
|
||||
})?
|
||||
.build(|w| {
|
||||
w.single("path", |d| {
|
||||
let mut path_data = String::with_capacity(0);
|
||||
let scale = qr_code_size / qr.size() as f32;
|
||||
|
||||
for y in 0..qr.size() {
|
||||
for x in 0..qr.size() {
|
||||
if qr.get_module(x, y) {
|
||||
path_data += &format!("M{},{}h1v1h-1z", x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
d.attr("style", "fill:#000000")?;
|
||||
d.attr("d", path_data)?;
|
||||
d.attr("transform", format!("scale({})", scale))
|
||||
})
|
||||
})?;
|
||||
|
||||
// Footer logo
|
||||
const FOOTER_HEIGHT: f32 = 35.0;
|
||||
const FOOTER_WIDTH: f32 = 198.0;
|
||||
w.elem("g", |d| {
|
||||
d.attr(
|
||||
"transform",
|
||||
format!(
|
||||
"translate({},{})",
|
||||
(width - FOOTER_WIDTH) / 2.0,
|
||||
height - FOOTER_HEIGHT
|
||||
),
|
||||
)
|
||||
})?
|
||||
.build(|w| w.put_raw(include_str!("../assets/qrcode_logo_footer.svg")))
|
||||
})?;
|
||||
|
||||
Ok(svg)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1901,7 +1901,7 @@ async fn apply_mailinglist_changes(
|
||||
let (contact_id, _) =
|
||||
Contact::add_or_lookup(context, "", list_post, Origin::Hidden).await?;
|
||||
let mut contact = Contact::load_from_db(context, contact_id).await?;
|
||||
if contact.param.get(Param::ListId) != Some(listid) {
|
||||
if contact.param.get(Param::ListId) != Some(listid.as_str()) {
|
||||
contact.param.set(Param::ListId, listid);
|
||||
contact.update_param(context).await?;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
// (to be only compatible with delta, only "[\r\n|\n]-- {0,2}[\r\n|\n]" needs to be replaced)
|
||||
pub fn escape_message_footer_marks(text: &str) -> String {
|
||||
if let Some(text) = text.strip_prefix("--") {
|
||||
"-\u{200B}-".to_string() + &text.replace("\n--", "\n-\u{200B}-")
|
||||
format!("-\u{200B}-{}", text.replace("\n--", "\n-\u{200B}-"))
|
||||
} else {
|
||||
text.replace("\n--", "\n-\u{200B}-")
|
||||
}
|
||||
|
||||
21
src/tools.rs
21
src/tools.rs
@@ -113,7 +113,7 @@ pub(crate) fn truncate_by_lines(
|
||||
******************************************************************************/
|
||||
|
||||
pub fn timestamp_to_str(wanted: i64) -> String {
|
||||
let ts = Local.timestamp(wanted, 0);
|
||||
let ts = Local.timestamp_opt(wanted, 0).unwrap();
|
||||
ts.format("%Y.%m.%d %H:%M:%S").to_string()
|
||||
}
|
||||
|
||||
@@ -207,7 +207,8 @@ async fn maybe_warn_on_bad_time(context: &Context, now: i64, known_past_timestam
|
||||
stock_str::bad_time_msg_body(
|
||||
context,
|
||||
&Local
|
||||
.timestamp(now, 0)
|
||||
.timestamp_opt(now, 0)
|
||||
.unwrap()
|
||||
.format("%Y-%m-%d %H:%M:%S")
|
||||
.to_string(),
|
||||
)
|
||||
@@ -218,7 +219,9 @@ async fn maybe_warn_on_bad_time(context: &Context, now: i64, known_past_timestam
|
||||
Some(
|
||||
format!(
|
||||
"bad-time-warning-{}",
|
||||
chrono::NaiveDateTime::from_timestamp(now, 0).format("%Y-%m-%d") // repeat every day
|
||||
chrono::NaiveDateTime::from_timestamp_opt(now, 0)
|
||||
.unwrap()
|
||||
.format("%Y-%m-%d") // repeat every day
|
||||
)
|
||||
.as_str(),
|
||||
),
|
||||
@@ -241,7 +244,9 @@ async fn maybe_warn_on_outdated(context: &Context, now: i64, approx_compile_time
|
||||
Some(
|
||||
format!(
|
||||
"outdated-warning-{}",
|
||||
chrono::NaiveDateTime::from_timestamp(now, 0).format("%Y-%m") // repeat every month
|
||||
chrono::NaiveDateTime::from_timestamp_opt(now, 0)
|
||||
.unwrap()
|
||||
.format("%Y-%m") // repeat every month
|
||||
)
|
||||
.as_str(),
|
||||
),
|
||||
@@ -648,9 +653,9 @@ pub(crate) fn parse_receive_header(header: &str) -> String {
|
||||
if let Ok(date) = dateparse(&header) {
|
||||
// In tests, use the UTC timezone so that the test is reproducible
|
||||
#[cfg(test)]
|
||||
let date_obj = chrono::Utc.timestamp(date, 0);
|
||||
let date_obj = chrono::Utc.timestamp_opt(date, 0).unwrap();
|
||||
#[cfg(not(test))]
|
||||
let date_obj = Local.timestamp(date, 0);
|
||||
let date_obj = Local.timestamp_opt(date, 0).unwrap();
|
||||
|
||||
hop_info += &format!("Date: {}", date_obj.to_rfc2822());
|
||||
};
|
||||
@@ -1153,8 +1158,8 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true";
|
||||
let timestamp_now = time();
|
||||
let timestamp_future = timestamp_now + 60 * 60 * 24 * 7;
|
||||
let timestamp_past = NaiveDateTime::new(
|
||||
NaiveDate::from_ymd(2020, 9, 1),
|
||||
NaiveTime::from_hms(0, 0, 0),
|
||||
NaiveDate::from_ymd_opt(2020, 9, 1).unwrap(),
|
||||
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
|
||||
)
|
||||
.timestamp_millis()
|
||||
/ 1_000;
|
||||
|
||||
Reference in New Issue
Block a user