1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
//! Send email CLI commands
use crate::output::{OutputFormat, print_output};
use anyhow::Result;
use clap::{Args, Subcommand};
use grr_gmail::AttachmentData;
use grr_gmail::GmailClient;
use grr_gmail::client::{StreamAttachment, mime_message_stream};
use std::path::Path;
#[derive(Subcommand, Debug)]
pub enum SendCommands {
/// Send an email
Send(SendArgs),
/// Send an email with attachments
SendAttach(SendAttachArgs),
}
#[derive(Args, Debug)]
pub struct SendArgs {
/// Recipient email
pub to: String,
/// Subject
pub subject: String,
/// Body text
pub body: String,
/// CC recipients (comma-separated)
#[arg(long)]
pub cc: Option<String>,
/// BCC recipients (comma-separated)
#[arg(long)]
pub bcc: Option<String>,
/// Output format
#[arg(short, long, value_enum, default_value = "json")]
pub format: OutputFormat,
}
#[derive(Args, Debug)]
pub struct SendAttachArgs {
/// Recipient email
pub to: String,
/// Subject
pub subject: String,
/// Body text
pub body: String,
/// Attachment file paths (comma-separated)
#[arg(long, value_delimiter = ',')]
pub attachments: Vec<String>,
/// Thread ID to reply to (optional)
#[arg(long)]
pub thread_id: Option<String>,
/// CC recipients (comma-separated)
#[arg(long)]
pub cc: Option<String>,
/// BCC recipients (comma-separated)
#[arg(long)]
pub bcc: Option<String>,
/// Output format
#[arg(short, long, value_enum, default_value = "json")]
pub format: OutputFormat,
}
/// Routing decision for send-with-attachments.
///
/// The streaming media-upload endpoint (`uploadType=media`) posts raw
/// RFC822 bytes and cannot carry a `threadId`; only the legacy JSON
/// `SendMessageRequest` path threads the message. Legacy is therefore
/// chosen **iff** a thread id was requested; streaming stays the default
/// otherwise.
fn uses_legacy_attachment_path(thread_id: Option<&str>) -> bool {
thread_id.is_some()
}
fn attachment_filename(path: &Path) -> String {
path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("attachment")
.to_string()
}
fn attachment_mime_type(path: &Path) -> String {
mime_guess::from_path(path)
.first()
.map(|m| m.to_string())
.unwrap_or_else(|| "application/octet-stream".to_string())
}
pub async fn handle_send_cmd(client: &GmailClient, cmd: SendCommands) -> Result<()> {
match cmd {
SendCommands::Send(args) => {
let msg = if args.cc.is_some() || args.bcc.is_some() {
client
.send_with_options(
&args.to,
&args.subject,
&args.body,
args.cc.as_deref(),
args.bcc.as_deref(),
)
.await?
} else {
client.send(&args.to, &args.subject, &args.body).await?
};
print_output(&msg, args.format)?;
}
SendCommands::SendAttach(args) => {
let msg = if uses_legacy_attachment_path(args.thread_id.as_deref()) {
let mut attachments: Vec<AttachmentData> =
Vec::with_capacity(args.attachments.len());
for path_str in &args.attachments {
let path = Path::new(path_str);
attachments.push(AttachmentData {
content: tokio::fs::read(path).await?,
filename: attachment_filename(path),
mime_type: attachment_mime_type(path),
});
}
client
.send_with_attachments(
&args.to,
&args.subject,
&args.body,
attachments,
args.thread_id.as_deref(),
)
.await?
} else {
let attachments: Vec<StreamAttachment> = args
.attachments
.iter()
.map(|path_str| {
let path = Path::new(path_str);
StreamAttachment {
path: path.to_path_buf(),
filename: attachment_filename(path),
mime_type: attachment_mime_type(path),
}
})
.collect();
let stream = mime_message_stream(
&args.to,
&args.subject,
&args.body,
attachments,
args.thread_id.as_deref(),
);
client
.send_mime_stream(stream, args.thread_id.as_deref())
.await?
};
print_output(&msg, args.format)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn legacy_attachment_path_chosen_iff_thread_id_present() {
// Streaming media-upload drops threadId on the floor, so any
// --thread_id request MUST take the legacy JSON send path.
assert!(!uses_legacy_attachment_path(None));
assert!(uses_legacy_attachment_path(Some("thread-abc123")));
}
}