Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Unreleased
- Add `re delete emails` to delete emails by id from a bucket

# v0.40.0
- Allow `re get emails` to filter by `--mailbox`, `--from-timestamp`, and `--to-timestamp`

Expand Down
23 changes: 23 additions & 0 deletions api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,22 @@ impl Client {
)
}

/// Delete emails by id in a bucket.
pub fn delete_emails(
&self,
bucket: impl Into<BucketIdentifier>,
emails: &[EmailId],
) -> Result<()> {
let bucket_full_name = match bucket.into() {
bucket @ BucketIdentifier::Id(_) => self.get_bucket(bucket)?.full_name(),
BucketIdentifier::FullName(bucket_full_name) => bucket_full_name,
};
self.delete_query(
self.endpoints.delete_emails(&bucket_full_name)?,
Some(&id_list_query(emails.iter().map(|id| &id.0))),
)
}

/// Get a page of comments from a source.
pub fn get_comments_iter_page(
&self,
Expand Down Expand Up @@ -2459,6 +2475,13 @@ impl Endpoints {
)
}

fn delete_emails(&self, bucket_name: &BucketFullName) -> Result<Url> {
construct_endpoint(
&self.base,
&["api", "_private", "buckets", &bucket_name.0, "emails"],
)
}

fn post_user(&self, user_id: &UserId) -> Result<Url> {
construct_endpoint(&self.base, &["api", "_private", "users", &user_id.0])
}
Expand Down
20 changes: 19 additions & 1 deletion cli/src/commands/delete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use structopt::StructOpt;
use reinfer_client::{
resources::{bucket::GetKeyedSyncStateIdsRequest, project::ForceDeleteProject},
BucketIdentifier, Client, CommentId, CommentsIter, CommentsIterTimerange, DatasetIdentifier,
ProjectName, Source, SourceIdentifier, UserIdentifier,
EmailId, ProjectName, Source, SourceIdentifier, UserIdentifier,
};

use crate::progress::{Options as ProgressOptions, Progress};
Expand All @@ -38,6 +38,18 @@ pub enum DeleteArgs {
comments: Vec<CommentId>,
},

#[structopt(name = "emails")]
/// Delete emails by id in a bucket.
Emails {
#[structopt(short = "b", long = "bucket")]
/// Name or id of the bucket to delete emails from
bucket: BucketIdentifier,

#[structopt(name = "email id")]
/// Ids of the emails to delete
emails: Vec<EmailId>,
},

#[structopt(name = "bulk")]
/// Delete all comments in a given time range.
BulkComments {
Expand Down Expand Up @@ -134,6 +146,12 @@ pub fn run(delete_args: &DeleteArgs, client: Client) -> Result<()> {
.context("Operation to delete comments has failed.")?;
log::info!("Deleted comments.");
}
DeleteArgs::Emails { bucket, emails } => {
client
.delete_emails(bucket.clone(), emails)
.context("Operation to delete emails has failed.")?;
log::info!("Deleted emails.");
}
DeleteArgs::BulkComments {
source: source_identifier,
include_annotated,
Expand Down
83 changes: 83 additions & 0 deletions cli/tests/test_buckets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,89 @@ fn test_get_emails_filter_by_mailbox_and_timerange() {
cli.run(["delete", "bucket", &bucket]);
}

#[test]
fn test_delete_emails() {
let cli = TestCli::get();
let owner = TestCli::project();

let bucket = format!("{}/test-delete-emails-{}", owner, Uuid::new_v4());
cli.run(["create", "bucket", &bucket]);

// Three emails in the bucket.
let emails = [
("keep", "alice@reinfer.io", "2020-01-01T00:00:00Z"),
("delete-1", "bob@reinfer.io", "2020-02-01T00:00:00Z"),
("delete-2", "carol@reinfer.io", "2020-03-01T00:00:00Z"),
];
let jsonl = emails
.iter()
.map(|(id, mailbox, timestamp)| {
serde_json::json!({
"id": id,
"mailbox": mailbox,
"timestamp": timestamp,
"mime_content": format!(
"Date: {timestamp}\r\nFrom: {mailbox}\r\nTo: support@reinfer.io\r\n\
Subject: {id}\r\nContent-Type: text/plain\r\n\r\nHello from {id}\r\n"
),
})
.to_string()
})
.collect::<Vec<_>>()
.join("\n");
cli.run_with_stdin(["create", "emails", "-y", "-b", &bucket], jsonl.as_bytes());

// Sorted list of email ids currently in the bucket.
let ids = |output: &str| -> Vec<String> {
let mut values: Vec<String> = output
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| {
serde_json::from_str::<serde_json::Value>(line).unwrap()["id"]
.as_str()
.unwrap()
.to_owned()
})
.collect();
values.sort();
values
};

assert_eq!(
ids(&cli.run(["get", "emails", &bucket])),
vec!["delete-1", "delete-2", "keep"],
"all three emails should be present before deletion"
);

// Delete two of the three by id.
let output = cli.run(["delete", "emails", "-b", &bucket, "delete-1", "delete-2"]);
assert!(output.is_empty(), "{}", output);

assert_eq!(
ids(&cli.run(["get", "emails", &bucket])),
vec!["keep"],
"only the un-deleted email should remain"
);

// Deletion is idempotent: deleting an already-deleted / missing id succeeds.
let output = cli.run([
"delete",
"emails",
"-b",
&bucket,
"delete-1",
"does-not-exist",
]);
assert!(output.is_empty(), "{}", output);
assert_eq!(
ids(&cli.run(["get", "emails", &bucket])),
vec!["keep"],
"idempotent delete of missing ids should not affect remaining emails"
);

cli.run(["delete", "bucket", &bucket]);
}

#[test]
fn test_create_without_org_fails() {
let cli = TestCli::get();
Expand Down
Loading