Compare commits

...

29 Commits

Author SHA1 Message Date
dignifiedquire
94b50f6742 update to released iroh version 2022-12-02 16:56:52 +01:00
dignifiedquire
0bcba98928 Merge remote-tracking branch 'origin/master' into iroh-share 2022-12-02 16:51:41 +01:00
dignifiedquire
20564529a7 update github workflows 2022-11-23 16:54:02 +01:00
dignifiedquire
415279c450 update dependencies, including iroh to 0.1.1 2022-11-23 16:50:03 +01:00
dignifiedquire
1fd4e8e0ec fixup 2022-11-23 16:36:05 +01:00
dignifiedquire
b6e8c9f1a4 clippy 2022-11-23 16:32:20 +01:00
dignifiedquire
b2c68c77f2 fixup rebase 2022-11-23 16:32:20 +01:00
dignifiedquire
c9cd4c9746 clippy fixes 2022-11-23 16:32:20 +01:00
dignifiedquire
1f4384b302 fixup 2022-11-23 16:32:20 +01:00
dignifiedquire
26719ee358 apply CR 2022-11-23 16:32:19 +01:00
dignifiedquire
09d5cbe564 update deps 2022-11-23 16:31:22 +01:00
Asmir Avdicevic
ba869a2e7f ci: protoc fix (#3566) 2022-11-23 16:30:50 +01:00
dignifiedquire
6c6499f2ff apply updates 2022-11-23 16:30:48 +01:00
dignifiedquire
f696f3e485 update iroh-share & cleanup api 2022-11-23 16:27:47 +01:00
B. Petersen
1633b2fd26 clear config cache before receiving a backup (with new config values) 2022-11-23 16:27:12 +01:00
B. Petersen
411d0a7cd4 dc_reveive_backup() ffi just takes the qr-code again instead of several parameters 2022-11-23 16:27:12 +01:00
bjoern
0ed3348bdf add dc_receive_backup ffi (#3504)
* add dc_receive_backup ffi

* fix compile issues

* make clippy happy

Co-authored-by: dignifiedquire <me@dignifiedquire.com>
2022-11-23 16:27:12 +01:00
B. Petersen
e61ee76e68 document ffi wrt sending and receiving backups 2022-11-23 16:27:12 +01:00
B. Petersen
17f01e5e71 fix: make passphrase work for repl's send-backup 2022-11-23 16:27:12 +01:00
B. Petersen
94c16ef176 repl: documentation and tab-completion for the new send-backup commands 2022-11-23 16:27:12 +01:00
B. Petersen
4ff746ef81 fix "the trait Add<&&str> is not implemented for std::string::String" 2022-11-23 16:27:12 +01:00
dignifiedquire
0aa4e21f7a proper shutdown and progress reports 2022-11-23 16:27:12 +01:00
dignifiedquire
995bf6fa9e update deltachat.h 2022-11-23 16:27:12 +01:00
dignifiedquire
63872fc271 generate qr codes 2022-11-23 16:27:11 +01:00
dignifiedquire
679242d5c4 remove unused event 2022-11-23 16:26:51 +01:00
dignifiedquire
364e06d8ae update deps 2022-11-23 16:26:49 +01:00
dignifiedquire
14fc7b7d10 implement receive-backup 2022-11-23 16:26:16 +01:00
dignifiedquire
9f7e962832 integrate backup sending via iroh into the repl 2022-11-23 16:26:14 +01:00
dignifiedquire
0e06bcb182 pull in iroh-share 2022-11-23 16:25:39 +01:00
30 changed files with 4964 additions and 95 deletions

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +1 @@
1.61.0
1.63.0

View File

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

View File

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

View File

@@ -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(&param.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?;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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::*;

View File

@@ -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?;
}

View File

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

View File

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