Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/cs/use destination caller id #249

Merged
merged 8 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions imessage-database/src/tables/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ pub struct Message {
pub text: Option<String>,
pub service: Option<String>,
pub handle_id: Option<i32>,
pub destination_caller_id: Option<String>,
pub subject: Option<String>,
pub date: i64,
pub date_read: i64,
Expand Down Expand Up @@ -108,6 +109,7 @@ impl Table for Message {
text: row.get("text").unwrap_or(None),
service: row.get("service").unwrap_or(None),
handle_id: row.get("handle_id").unwrap_or(None),
destination_caller_id: row.get("destination_caller_id").unwrap_or(None),
subject: row.get("subject").unwrap_or(None),
date: row.get("date")?,
date_read: row.get("date_read").unwrap_or(0),
Expand Down Expand Up @@ -966,6 +968,7 @@ mod tests {
text: None,
service: Some("iMessage".to_string()),
handle_id: Some(i32::default()),
destination_caller_id: None,
subject: None,
date: i64::default(),
date_read: i64::default(),
Expand Down
18 changes: 9 additions & 9 deletions imessage-exporter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,21 @@ This binary is available on [crates.io](https://crates.io/crates/imessage-export

`cargo install imessage-exporter` is the best way to install the app for normal use.

<details><summary>Uninstall steps</summary><p><pre>% cargo uninstall imessage-exporter</pre></p></details>
<details><summary>Uninstall steps</summary><p><pre>$ cargo uninstall imessage-exporter</pre></p></details>

### Homebrew

This binary is available via [`brew`](https://formulae.brew.sh/formula/imessage-exporter).

`brew install imessage-exporter` will install the app, but it may not be up to date with the latest release.

<details><summary>Uninstall steps</summary><p><pre>% brew uninstall imessage-exporter</pre></p></details>
<details><summary>Uninstall steps</summary><p><pre>$ brew uninstall imessage-exporter</pre></p></details>

### Prebuilt Binaries

The [releases page](https://github.com/ReagentX/imessage-exporter/releases) provides prebuilt binaries for both Apple Silicon and Intel-based Macs.

<details><summary>Uninstall steps</summary><p><pre>% rm path/to/imessage-exporter-binary</pre></p></details>
<details><summary>Uninstall steps</summary><p><pre>$ rm path/to/imessage-exporter-binary</pre></p></details>

### Installing manually

Expand Down Expand Up @@ -98,37 +98,37 @@ The [releases page](https://github.com/ReagentX/imessage-exporter/releases) prov
Export as `html` and copy attachments in web-compatible formats from the default iMessage Database location to your home directory:

```zsh
% imessage-exporter -f html -c compatible
$ imessage-exporter -f html -c compatible
```

Export as `txt` and copy attachments in their original formats from the default iMessage Database location to a new folder in the current working directory called `output`:

```zsh
% imessage-exporter -f txt -o output -c efficient
$ imessage-exporter -f txt -o output -c efficient
```

Export as `txt` from the an unencrypted iPhone backup located at `~/iphone_backup_latest` to a new folder in the current working directory called `backup_export`:

```zsh
% imessage-exporter -f txt -p ~/iphone_backup_latest -a iOS -o backup_export
$ imessage-exporter -f txt -p ~/iphone_backup_latest -a iOS -o backup_export
```

Export as `html` from `/Volumes/external/chat.db` to `/Volumes/external/export` without copying attachments:

```zsh
% imessage-exporter -f html -c disabled -p /Volumes/external/chat.db -o /Volumes/external/export
$ imessage-exporter -f html -c disabled -p /Volumes/external/chat.db -o /Volumes/external/export
```

Export as `html` from `/Volumes/external/chat.db` to `/Volumes/external/export` with attachments in `/Volumes/external/Attachments`:

```zsh
% imessage-exporter -f html -c efficient -p /Volumes/external/chat.db -r /Volumes/external/Attachments -o /Volumes/external/export
$ imessage-exporter -f html -c efficient -p /Volumes/external/chat.db -r /Volumes/external/Attachments -o /Volumes/external/export
```

Export messages from `2020-01-01` to `2020-12-31` as `txt` from the default macOS iMessage Database location to `~/export-2020`:

```zsh
% imessage-exporter -f txt -o ~/export-2020 -s 2020-01-01 -e 2021-01-01 -a macOS
$ imessage-exporter -f txt -o ~/export-2020 -s 2020-01-01 -e 2021-01-01 -a macOS
```

## Features
Expand Down
134 changes: 132 additions & 2 deletions imessage-exporter/src/app/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub const OPTION_DISABLE_LAZY_LOADING: &str = "no-lazy";
pub const OPTION_CUSTOM_NAME: &str = "custom-name";
pub const OPTION_PLATFORM: &str = "platform";
pub const OPTION_BYPASS_FREE_SPACE_CHECK: &str = "ignore-disk-warning";
pub const OPTION_USE_CALLER_ID: &str = "use-caller-id";

// Other CLI Text
pub const SUPPORTED_FILE_TYPES: &str = "txt, html";
Expand Down Expand Up @@ -62,6 +63,8 @@ pub struct Options {
pub no_lazy: bool,
/// Custom name for database owner in output
pub custom_name: Option<String>,
/// If true, use the database owner's caller ID instead of "Me"
pub use_caller_id: bool,
/// The database source's platform
pub platform: Platform,
/// If true, disable the free disk space check
Expand All @@ -80,6 +83,7 @@ impl Options {
let end_date: Option<&String> = args.get_one(OPTION_END_DATE);
let no_lazy = args.get_flag(OPTION_DISABLE_LAZY_LOADING);
let custom_name: Option<&String> = args.get_one(OPTION_CUSTOM_NAME);
let use_caller_id = args.get_flag(OPTION_USE_CALLER_ID);
let platform_type: Option<&String> = args.get_one(OPTION_PLATFORM);
let ignore_disk_space = args.get_flag(OPTION_BYPASS_FREE_SPACE_CHECK);

Expand Down Expand Up @@ -114,6 +118,11 @@ impl Options {
"Option {OPTION_END_DATE} is enabled, which requires `--{OPTION_EXPORT_TYPE}`"
)));
}
if use_caller_id && export_file_type.is_none() {
return Err(RuntimeError::InvalidOptions(format!(
"Option {OPTION_USE_CALLER_ID} is enabled, which requires `--{OPTION_EXPORT_TYPE}`"
)));
}

// Warn the user if they are exporting to a file type for which lazy loading has no effect
if no_lazy && export_file_type != Some(&"html".to_string()) {
Expand Down Expand Up @@ -148,6 +157,18 @@ impl Options {
"Diagnostics are enabled; {OPTION_END_DATE} is disallowed"
)));
}
if diagnostic && use_caller_id {
return Err(RuntimeError::InvalidOptions(format!(
"Diagnostics are enabled; {OPTION_USE_CALLER_ID} is disallowed"
)));
}

// Ensure that there are no custom name conflicts
if custom_name.is_some() && use_caller_id {
return Err(RuntimeError::InvalidOptions(format!(
"`--{OPTION_CUSTOM_NAME}` is enabled; `--{OPTION_USE_CALLER_ID}` is disallowed"
)));
}

// Build query context
let mut query_context = QueryContext::default();
Expand Down Expand Up @@ -217,6 +238,7 @@ impl Options {
query_context,
no_lazy,
custom_name: custom_name.cloned(),
use_caller_id,
platform,
ignore_disk_space,
})
Expand Down Expand Up @@ -369,16 +391,24 @@ fn get_command() -> Command {
Arg::new(OPTION_CUSTOM_NAME)
.short('m')
.long(OPTION_CUSTOM_NAME)
.help("Specify an optional custom name for the database owner's messages in exports\n")
.help(format!("Specify an optional custom name for the database owner's messages in exports\nConflicts with --{OPTION_USE_CALLER_ID}\n"))
.display_order(10)
)
.arg(
Arg::new(OPTION_USE_CALLER_ID)
.short('i')
.long(OPTION_USE_CALLER_ID)
.help(format!("Use the database owner's caller ID in exports instead of \"Me\"\nConflicts with --{OPTION_CUSTOM_NAME}\n"))
.action(ArgAction::SetTrue)
.display_order(11)
)
.arg(
Arg::new(OPTION_BYPASS_FREE_SPACE_CHECK)
.short('b')
.long(OPTION_BYPASS_FREE_SPACE_CHECK)
.help("Bypass the disk space check when exporting data\nBy default, exports will not run if there is not enough free disk space\n")
.action(ArgAction::SetTrue)
.display_order(11)
.display_order(12)
)
}

Expand Down Expand Up @@ -420,6 +450,7 @@ mod arg_tests {
query_context: QueryContext::default(),
no_lazy: false,
custom_name: None,
use_caller_id: false,
platform: Platform::default(),
ignore_disk_space: false,
};
Expand Down Expand Up @@ -492,6 +523,19 @@ mod arg_tests {
assert!(actual.is_err());
}

#[test]
fn cant_build_option_diagnostic_flag_with_caller_id() {
// Get matches from sample args
let cli_args: Vec<&str> = vec!["imessage-exporter", "-d", "-i"];
let command = get_command();
let args = command.get_matches_from(cli_args);

// Build the Options
let actual = Options::from_args(&args);

assert!(actual.is_err());
}

#[test]
fn can_build_option_export_html() {
// Get matches from sample args
Expand All @@ -514,6 +558,7 @@ mod arg_tests {
query_context: QueryContext::default(),
no_lazy: false,
custom_name: None,
use_caller_id: false,
platform: Platform::default(),
ignore_disk_space: false,
};
Expand Down Expand Up @@ -542,6 +587,7 @@ mod arg_tests {
query_context: QueryContext::default(),
no_lazy: true,
custom_name: None,
use_caller_id: false,
platform: Platform::default(),
ignore_disk_space: false,
};
Expand Down Expand Up @@ -639,6 +685,90 @@ mod arg_tests {

assert!(actual.is_err());
}

#[test]
fn can_build_option_custom_name() {
// Get matches from sample args
let cli_args: Vec<&str> = vec!["imessage-exporter", "-f", "txt", "-m", "Name"];
let command = get_command();
let args = command.get_matches_from(cli_args);

// Build the Options
let actual = Options::from_args(&args).unwrap();

// Expected data
let expected = Options {
db_path: default_db_path(),
attachment_root: None,
attachment_manager: AttachmentManager::default(),
diagnostic: false,
export_type: Some(ExportType::Txt),
export_path: validate_path(None, &None).unwrap(),
query_context: QueryContext::default(),
no_lazy: false,
custom_name: Some("Name".to_string()),
use_caller_id: false,
platform: Platform::default(),
ignore_disk_space: false,
};

assert_eq!(actual, expected);
}

#[test]
fn can_build_option_caller_id() {
// Get matches from sample args
let cli_args: Vec<&str> = vec!["imessage-exporter", "-f", "txt", "-i"];
let command = get_command();
let args = command.get_matches_from(cli_args);

// Build the Options
let actual = Options::from_args(&args).unwrap();

// Expected data
let expected = Options {
db_path: default_db_path(),
attachment_root: None,
attachment_manager: AttachmentManager::default(),
diagnostic: false,
export_type: Some(ExportType::Txt),
export_path: validate_path(None, &None).unwrap(),
query_context: QueryContext::default(),
no_lazy: false,
custom_name: None,
use_caller_id: true,
platform: Platform::default(),
ignore_disk_space: false,
};

assert_eq!(actual, expected);
}

#[test]
fn cant_build_option_custom_name_and_caller_id() {
// Get matches from sample args
let cli_args: Vec<&str> = vec!["imessage-exporter", "-f", "txt", "-m", "Name", "-i"];
let command = get_command();
let args = command.get_matches_from(cli_args);

// Build the Options
let actual = Options::from_args(&args);

assert!(actual.is_err());
}

#[test]
fn cant_build_option_caller_id_no_export() {
// Get matches from sample args
let cli_args: Vec<&str> = vec!["imessage-exporter", "-f", "txt", "-m", "Name", "-i"];
let command = get_command();
let args = command.get_matches_from(cli_args);

// Build the Options
let actual = Options::from_args(&args);

assert!(actual.is_err());
}
}

#[cfg(test)]
Expand Down