Skip to content

Commit 554e7a8

Browse files
committed
refactor: use hybrid check for gmail draft-only mode (ID + path fallback)
1 parent 4db0c7b commit 554e7a8

File tree

3 files changed

+42
-11
lines changed

3 files changed

+42
-11
lines changed

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ path = "src/main.rs"
3434
[dependencies]
3535
aes-gcm = "0.10"
3636
anyhow = "1"
37-
clap = { version = "4", features = ["derive", "string"] }
37+
clap = { version = "4", features = ["derive", "string", "env"] }
3838
dirs = "5"
3939
dotenvy = "0.15"
4040
hostname = "0.4"
@@ -80,5 +80,6 @@ inherits = "release"
8080
lto = "thin"
8181

8282
[dev-dependencies]
83+
assert_cmd = "2"
8384
serial_test = "3.4.0"
8485
tempfile = "3"

src/commands.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ pub fn build_cli(doc: &RestDescription) -> Command {
3333
.value_name("TEMPLATE")
3434
.global(true),
3535
)
36+
.arg(
37+
clap::Arg::new("draft-only")
38+
.long("draft-only")
39+
.help("Gmail draft-only mode: block sending and strictly allow only draft creation/updates. Also reads GOOGLE_WORKSPACE_GMAIL_DRAFT_ONLY env var.")
40+
.action(clap::ArgAction::SetTrue)
41+
.env("GOOGLE_WORKSPACE_GMAIL_DRAFT_ONLY")
42+
.global(true),
43+
)
44+
3645
.arg(
3746
clap::Arg::new("dry-run")
3847
.long("dry-run")

src/main.rs

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ async fn run() -> Result<(), GwsError> {
207207
}
208208

209209
// Walk the subcommand tree to find the target method
210-
let (method, matched_args) = resolve_method_from_matches(&doc, &matches)?;
210+
let (method, matched_args, method_path) = resolve_method_from_matches(&doc, &matches)?;
211211

212212
let params_json = matched_args.get_one::<String>("params").map(|s| s.as_str());
213213
let body_json = matched_args
@@ -228,6 +228,22 @@ async fn run() -> Result<(), GwsError> {
228228
.map(|s| s.as_str());
229229

230230
let dry_run = matched_args.get_flag("dry-run");
231+
let draft_only = matches.get_flag("draft-only");
232+
if draft_only && !dry_run && api_name == "gmail" {
233+
let is_send_method = if let Some(ref id) = method.id {
234+
id == "gmail.users.messages.send" || id == "gmail.users.drafts.send"
235+
} else {
236+
// Fallback to path if ID is missing (suggested by previous review for robustness)
237+
method_path.len() == 3
238+
&& method_path[0] == "users"
239+
&& (method_path[1] == "messages" || method_path[1] == "drafts")
240+
&& method_path[2] == "send"
241+
};
242+
243+
if is_send_method {
244+
return Err(GwsError::Validation("Gmail draft-only mode is active. Sending mail is blocked (preparing a draft is still allowed).".to_string()));
245+
}
246+
}
231247

232248
// Build pagination config from flags
233249
let pagination = parse_pagination_config(matched_args);
@@ -361,13 +377,13 @@ fn parse_sanitize_config(
361377
fn resolve_method_from_matches<'a>(
362378
doc: &'a discovery::RestDescription,
363379
matches: &'a clap::ArgMatches,
364-
) -> Result<(&'a discovery::RestMethod, &'a clap::ArgMatches), GwsError> {
380+
) -> Result<(&'a discovery::RestMethod, &'a clap::ArgMatches, Vec<String>), GwsError> {
365381
// Walk the subcommand chain
366-
let mut path: Vec<&str> = Vec::new();
382+
let mut path: Vec<String> = Vec::new();
367383
let mut current_matches = matches;
368384

369385
while let Some((sub_name, sub_matches)) = current_matches.subcommand() {
370-
path.push(sub_name);
386+
path.push(sub_name.to_string());
371387
current_matches = sub_matches;
372388
}
373389

@@ -379,7 +395,7 @@ fn resolve_method_from_matches<'a>(
379395

380396
// path looks like ["files", "list"] or ["files", "permissions", "list"]
381397
// Walk the Discovery Document resources to find the method
382-
let resource_name = path[0];
398+
let resource_name = &path[0];
383399
let resource = doc
384400
.resources
385401
.get(resource_name)
@@ -388,7 +404,7 @@ fn resolve_method_from_matches<'a>(
388404
let mut current_resource = resource;
389405

390406
// Navigate sub-resources (everything except the last element, which is the method)
391-
for &name in &path[1..path.len() - 1] {
407+
for name in &path[1..path.len() - 1] {
392408
// Check if this is a sub-resource
393409
if let Some(sub) = current_resource.resources.get(name) {
394410
current_resource = sub;
@@ -400,11 +416,11 @@ fn resolve_method_from_matches<'a>(
400416
}
401417

402418
// The last element is the method name
403-
let method_name = path[path.len() - 1];
419+
let method_name = &path[path.len() - 1];
404420

405421
// Check if this is a method on the current resource
406422
if let Some(method) = current_resource.methods.get(method_name) {
407-
return Ok((method, current_matches));
423+
return Ok((method, current_matches, path));
408424
}
409425

410426
// Maybe it's a resource that has methods — need one more subcommand
@@ -488,6 +504,9 @@ fn print_usage() {
488504
println!();
489505
println!("DISCLAIMER:");
490506
println!(" This is not an officially supported Google product.");
507+
println!();
508+
println!("DISCLAIMER:");
509+
println!(" This is not an officially supported Google product.");
491510
}
492511

493512
fn is_help_flag(arg: &str) -> bool {
@@ -622,8 +641,9 @@ mod tests {
622641
.subcommand(clap::Command::new("files").subcommand(clap::Command::new("list")));
623642

624643
let matches = cmd.get_matches_from(vec!["gws", "files", "list"]);
625-
let (method, _) = resolve_method_from_matches(&doc, &matches).unwrap();
644+
let (method, _, method_path) = resolve_method_from_matches(&doc, &matches).unwrap();
626645
assert_eq!(method.id.as_deref(), Some("drive.files.list"));
646+
assert_eq!(method_path, vec!["files", "list"]);
627647
}
628648

629649
#[test]
@@ -655,8 +675,9 @@ mod tests {
655675
));
656676

657677
let matches = cmd.get_matches_from(vec!["gws", "files", "permissions", "get"]);
658-
let (method, _) = resolve_method_from_matches(&doc, &matches).unwrap();
678+
let (method, _, method_path) = resolve_method_from_matches(&doc, &matches).unwrap();
659679
assert_eq!(method.id.as_deref(), Some("drive.files.permissions.get"));
680+
assert_eq!(method_path, vec!["files", "permissions", "get"]);
660681
}
661682

662683
#[test]

0 commit comments

Comments
 (0)