@smbcloud / cli / commits / 0e1d103

Add mail command and API client for mail app management

- Add `mail` command to CLI with subcommands for mail apps, inboxes, and messages - Implement `smbcloud-mail` crate for mail API calls - Add mail data models to `smbcloud-model` - Wire mail command into CLI entrypoint - Update workspace and crate dependencies - Remove unused `smbcloud-mail-sdk` crate

keypair34 committed May 2, 2026 at 19:17 UTC 0e1d1034c07310ba091c2f50f57437151e93d412
22 files changed +1391 -6
Cargo.lock
+13
@@ -2543,6 +2543,7 @@ dependencies = [
2543 "serde_json",
2544 "serde_repr",
2545 "smbcloud-auth",
2546 + "smbcloud-mail",
2547 "smbcloud-model",
2548 "smbcloud-network",
2549 "smbcloud-networking",
@@ -2588,6 +2589,18 @@ dependencies = [
2589 "tokio",
2590 ]
2591
2592 +[[package]]
2593 +name = "smbcloud-mail"
2594 +version = "0.3.38"
2595 +dependencies = [
2596 + "reqwest",
2597 + "serde",
2598 + "serde_json",
2599 + "smbcloud-model",
2600 + "smbcloud-network",
2601 + "smbcloud-networking",
2602 +]
2603 +
2604 [[package]]
2605 name = "smbcloud-model"
2606 version = "0.3.35"
Cargo.toml
+1
@@ -29,6 +29,7 @@ serde_repr = "0.1"
29 smbcloud-auth = { version = "0.3", path = "crates/smbcloud-auth" }
30 smbcloud-auth-sdk = { version = "0.3", path = "crates/smbcloud-auth-sdk" }
31 smbcloud-gresiq-sdk = { version = "0.3", path = "crates/smbcloud-gresiq-sdk" }
32 +smbcloud-mail = { version = "0.3", path = "crates/smbcloud-mail" }
33 smbcloud-model = { version = "0.3", path = "crates/smbcloud-model" }
34 smbcloud-network = { version = "0.3", path = "crates/smbcloud-network" }
35 smbcloud-networking = { version = "0.3", path = "crates/smbcloud-networking" }
crates/cli/Cargo.toml
+1
@@ -44,6 +44,7 @@ serde = { workspace = true, features = ["derive"] }
44 serde_json = { workspace = true }
45 serde_repr = { workspace = true }
46 smbcloud-auth = { workspace = true }
47 +smbcloud-mail = { workspace = true }
48 smbcloud-model = { workspace = true }
49 smbcloud-network = { workspace = true }
50 smbcloud-networking = { workspace = true }
crates/cli/src/cli/mod.rs
+6 -1
@@ -1,5 +1,5 @@
1 use {
2 - crate::{account, project},
2 + crate::{account, mail, project},
3 clap::{Parser, Subcommand},
4 smbcloud_network::environment::Environment,
5 spinners::Spinner,
@@ -65,4 +65,9 @@ pub enum Commands {
65 #[clap(subcommand)]
66 command: project::cli::Commands,
67 },
68 + #[clap(about = "Manage smbCloud Mail.")]
69 + Mail {
70 + #[clap(subcommand)]
71 + command: mail::cli::Commands,
72 + },
73 }
crates/cli/src/lib.rs
+1
@@ -3,6 +3,7 @@ use smbcloud_networking::smb_client::SmbClient;
3 pub mod account;
4 pub mod cli;
5 pub mod deploy;
6 +pub mod mail;
7 pub mod project;
8 mod token;
9 mod ui;
crates/cli/src/mail.rs new
+4
@@ -0,0 +1,4 @@
1 +pub mod cli;
2 +mod current_project;
3 +pub mod process;
4 +mod render;
crates/cli/src/mail/cli.rs new
+122
@@ -0,0 +1,122 @@
1 +use clap::Subcommand;
2 +
3 +#[derive(Subcommand)]
4 +pub enum Commands {
5 + #[clap(about = "List your mail apps.")]
6 + List {
7 + #[clap(long)]
8 + project_id: Option<String>,
9 + },
10 + #[clap(about = "Show a mail app.")]
11 + Show {
12 + #[clap(long, required = true)]
13 + id: String,
14 + },
15 + #[clap(about = "Create a mail app.")]
16 + New {
17 + #[clap(long, required = true)]
18 + name: String,
19 + #[clap(long, required = true)]
20 + domain: String,
21 + #[clap(long)]
22 + project_id: Option<String>,
23 + #[clap(long)]
24 + aws_region: Option<String>,
25 + },
26 + #[clap(about = "Update a mail app.")]
27 + Update {
28 + #[clap(long, required = true)]
29 + id: String,
30 + #[clap(long)]
31 + name: Option<String>,
32 + #[clap(long)]
33 + domain: Option<String>,
34 + #[clap(long)]
35 + aws_region: Option<String>,
36 + },
37 + #[clap(about = "Delete a mail app.")]
38 + Delete {
39 + #[clap(long, required = true)]
40 + id: String,
41 + },
42 + #[clap(about = "Manage mail inbox routes.")]
43 + Inbox {
44 + #[clap(subcommand)]
45 + command: InboxCommands,
46 + },
47 + #[clap(about = "Inspect inbound mail messages.")]
48 + Message {
49 + #[clap(subcommand)]
50 + command: MessageCommands,
51 + },
52 +}
53 +
54 +#[derive(Subcommand)]
55 +pub enum InboxCommands {
56 + #[clap(about = "Create a mail inbox route.")]
57 + New {
58 + #[clap(long, required = true)]
59 + app_id: String,
60 + #[clap(long, required = true)]
61 + local_part: String,
62 + #[clap(long, required = true)]
63 + forward_to: String,
64 + #[clap(long)]
65 + sender_email: Option<String>,
66 + },
67 + #[clap(about = "Update a mail inbox route.")]
68 + Update {
69 + #[clap(long, required = true)]
70 + app_id: String,
71 + #[clap(long, required = true)]
72 + id: String,
73 + #[clap(long)]
74 + local_part: Option<String>,
75 + #[clap(long)]
76 + forward_to: Option<String>,
77 + #[clap(long)]
78 + sender_email: Option<String>,
79 + },
80 + #[clap(about = "Delete a mail inbox route.")]
81 + Delete {
82 + #[clap(long, required = true)]
83 + app_id: String,
84 + #[clap(long, required = true)]
85 + id: String,
86 + },
87 + #[clap(about = "Send a test email for a mail inbox route.")]
88 + Test {
89 + #[clap(long, required = true)]
90 + app_id: String,
91 + #[clap(long, required = true)]
92 + id: String,
93 + #[clap(long)]
94 + recipient_email: Option<String>,
95 + #[clap(long)]
96 + subject: Option<String>,
97 + #[clap(long)]
98 + body: Option<String>,
99 + },
100 +}
101 +
102 +#[derive(Subcommand)]
103 +pub enum MessageCommands {
104 + #[clap(about = "List inbound mail messages for an inbox.")]
105 + List {
106 + #[clap(long, required = true)]
107 + app_id: String,
108 + #[clap(long, required = true)]
109 + inbox_id: String,
110 + #[clap(long, default_value_t = 10)]
111 + limit: u32,
112 + },
113 + #[clap(about = "Show one inbound mail message.")]
114 + Show {
115 + #[clap(long, required = true)]
116 + app_id: String,
117 + #[clap(long, required = true)]
118 + inbox_id: String,
119 + #[clap(long, required = true)]
120 + id: String,
121 + },
122 +}
crates/cli/src/mail/current_project.rs new
+43
@@ -0,0 +1,43 @@
1 +use anyhow::{anyhow, Result};
2 +use smbcloud_model::project::Config;
3 +use smbcloud_network::environment::Environment;
4 +
5 +pub(crate) fn resolve_optional_project_id(
6 + env: Environment,
7 + project_id: Option<String>,
8 +) -> Result<Option<String>> {
9 + match project_id {
10 + Some(project_id) => Ok(Some(project_id)),
11 + None => current_project_id(env),
12 + }
13 +}
14 +
15 +pub(crate) fn resolve_required_project_id(
16 + env: Environment,
17 + project_id: Option<String>,
18 +) -> Result<String> {
19 + match resolve_optional_project_id(env, project_id)? {
20 + Some(project_id) => Ok(project_id),
21 + None => Err(anyhow!(
22 + "No project selected. Pass `--project-id` or run `smb project use --id <project-id>`."
23 + )),
24 + }
25 +}
26 +
27 +fn current_project_id(env: Environment) -> Result<Option<String>> {
28 + let home_directory =
29 + home::home_dir().ok_or_else(|| anyhow!("Failed to resolve your home directory."))?;
30 + let config_path = home_directory.join(env.smb_dir()).join("config.json");
31 +
32 + if !config_path.exists() {
33 + return Ok(None);
34 + }
35 +
36 + let config_content = std::fs::read_to_string(&config_path)
37 + .map_err(|error| anyhow!("Failed to read `{}`: {}", config_path.display(), error))?;
38 +
39 + let config: Config = serde_json::from_str(&config_content)
40 + .map_err(|error| anyhow!("Failed to parse `{}`: {}", config_path.display(), error))?;
41 +
42 + Ok(config.current_project.map(|project| project.id.to_string()))
43 +}
crates/cli/src/mail/process.rs new
+442
@@ -0,0 +1,442 @@
1 +use crate::{
2 + cli::CommandResult,
3 + client,
4 + mail::{
5 + cli::{Commands, InboxCommands, MessageCommands},
6 + current_project::{resolve_optional_project_id, resolve_required_project_id},
7 + render::{
8 + print_mail_app_detail, print_mail_apps, print_mail_inbox_detail,
9 + print_mail_message_detail, print_mail_messages, print_mail_test_delivery,
10 + },
11 + },
12 + token::get_smb_token::get_smb_token,
13 + ui::{
14 + confirm_dialog::confirm_delete_tui, fail_message, fail_symbol, succeed_message,
15 + succeed_symbol,
16 + },
17 +};
18 +use anyhow::{anyhow, Result};
19 +use smbcloud_mail::{
20 + mail_app::{create_mail_app, delete_mail_app, get_mail_app, get_mail_apps, update_mail_app},
21 + mail_inbox::{create_mail_inbox, delete_mail_inbox, send_test_email, update_mail_inbox},
22 + mail_message::{get_mail_message, get_mail_messages},
23 +};
24 +use smbcloud_model::mail::{
25 + MailAppCreate, MailAppUpdate, MailInboxCreate, MailInboxUpdate, MailTestEmailRequest,
26 +};
27 +use smbcloud_network::environment::Environment;
28 +use spinners::Spinner;
29 +
30 +pub async fn process_mail(env: Environment, commands: Commands) -> Result<CommandResult> {
31 + match commands {
32 + Commands::List { project_id } => process_mail_list(env, project_id).await,
33 + Commands::Show { id } => process_mail_show(env, id).await,
34 + Commands::New {
35 + name,
36 + domain,
37 + project_id,
38 + aws_region,
39 + } => process_mail_new(env, name, domain, project_id, aws_region).await,
40 + Commands::Update {
41 + id,
42 + name,
43 + domain,
44 + aws_region,
45 + } => process_mail_update(env, id, name, domain, aws_region).await,
46 + Commands::Delete { id } => process_mail_delete(env, id).await,
47 + Commands::Inbox { command } => process_mail_inbox(env, command).await,
48 + Commands::Message { command } => process_mail_message(env, command).await,
49 + }
50 +}
51 +
52 +async fn process_mail_inbox(env: Environment, commands: InboxCommands) -> Result<CommandResult> {
53 + match commands {
54 + InboxCommands::New {
55 + app_id,
56 + local_part,
57 + forward_to,
58 + sender_email,
59 + } => process_mail_inbox_new(env, app_id, local_part, forward_to, sender_email).await,
60 + InboxCommands::Update {
61 + app_id,
62 + id,
63 + local_part,
64 + forward_to,
65 + sender_email,
66 + } => process_mail_inbox_update(env, app_id, id, local_part, forward_to, sender_email).await,
67 + InboxCommands::Delete { app_id, id } => process_mail_inbox_delete(env, app_id, id).await,
68 + InboxCommands::Test {
69 + app_id,
70 + id,
71 + recipient_email,
72 + subject,
73 + body,
74 + } => process_mail_inbox_test(env, app_id, id, recipient_email, subject, body).await,
75 + }
76 +}
77 +
78 +async fn process_mail_message(
79 + env: Environment,
80 + commands: MessageCommands,
81 +) -> Result<CommandResult> {
82 + match commands {
83 + MessageCommands::List {
84 + app_id,
85 + inbox_id,
86 + limit,
87 + } => process_mail_message_list(env, app_id, inbox_id, limit).await,
88 + MessageCommands::Show {
89 + app_id,
90 + inbox_id,
91 + id,
92 + } => process_mail_message_show(env, app_id, inbox_id, id).await,
93 + }
94 +}
95 +
96 +async fn process_mail_list(env: Environment, project_id: Option<String>) -> Result<CommandResult> {
97 + let access_token = get_smb_token(env)?;
98 + let resolved_project_id = resolve_optional_project_id(env, normalize_optional(project_id))?;
99 + let mut spinner = loading_spinner("Loading mail apps");
100 +
101 + let mail_apps = get_mail_apps(env, client(), access_token, resolved_project_id)
102 + .await
103 + .map_err(api_error)?;
104 +
105 + spinner.stop_and_persist(&succeed_symbol(), succeed_message("Loaded."));
106 + print_mail_apps(&mail_apps);
107 +
108 + Ok(done_result(if mail_apps.is_empty() {
109 + "No mail apps found."
110 + } else {
111 + "Done."
112 + }))
113 +}
114 +
115 +async fn process_mail_show(env: Environment, id: String) -> Result<CommandResult> {
116 + let access_token = get_smb_token(env)?;
117 + let mail_app_id = normalize_required("mail app id", id)?;
118 + let mut spinner = loading_spinner("Loading mail app");
119 +
120 + let mail_app = get_mail_app(env, client(), access_token, mail_app_id)
121 + .await
122 + .map_err(api_error)?;
123 +
124 + spinner.stop_and_persist(&succeed_symbol(), succeed_message("Loaded."));
125 + print_mail_app_detail(&mail_app);
126 +
127 + Ok(done_result("Done."))
128 +}
129 +
130 +async fn process_mail_new(
131 + env: Environment,
132 + name: String,
133 + domain: String,
134 + project_id: Option<String>,
135 + aws_region: Option<String>,
136 +) -> Result<CommandResult> {
137 + let access_token = get_smb_token(env)?;
138 + let mail_app = MailAppCreate {
139 + name: normalize_required("name", name)?,
140 + project_id: resolve_required_project_id(env, normalize_optional(project_id))?,
141 + domain: normalize_required("domain", domain)?,
142 + aws_region: normalize_optional(aws_region),
143 + };
144 + let mut spinner = loading_spinner("Creating mail app");
145 +
146 + let mail_app = create_mail_app(env, client(), access_token, mail_app)
147 + .await
148 + .map_err(api_error)?;
149 +
150 + spinner.stop_and_persist(&succeed_symbol(), succeed_message("Created."));
151 + print_mail_app_detail(&mail_app);
152 +
153 + Ok(done_result("Mail app created."))
154 +}
155 +
156 +async fn process_mail_update(
157 + env: Environment,
158 + id: String,
159 + name: Option<String>,
160 + domain: Option<String>,
161 + aws_region: Option<String>,
162 +) -> Result<CommandResult> {
163 + let access_token = get_smb_token(env)?;
164 + let mail_app_id = normalize_required("mail app id", id)?;
165 + let update = MailAppUpdate {
166 + name: normalize_optional(name),
167 + domain: normalize_optional(domain),
168 + aws_region: normalize_optional(aws_region),
169 + };
170 +
171 + if update.is_empty() {
172 + return Err(anyhow!(
173 + "Specify at least one of `--name`, `--domain`, or `--aws-region`."
174 + ));
175 + }
176 +
177 + let mut spinner = loading_spinner("Updating mail app");
178 + let mail_app = update_mail_app(env, client(), access_token, mail_app_id, update)
179 + .await
180 + .map_err(api_error)?;
181 +
182 + spinner.stop_and_persist(&succeed_symbol(), succeed_message("Updated."));
183 + print_mail_app_detail(&mail_app);
184 +
185 + Ok(done_result("Mail app updated."))
186 +}
187 +
188 +async fn process_mail_delete(env: Environment, id: String) -> Result<CommandResult> {
189 + let mail_app_id = normalize_required("mail app id", id)?;
190 + let confirmed = confirm_delete_tui(&format!("Delete mail app #{mail_app_id}"))
191 + .map_err(|error| anyhow!(error))?;
192 +
193 + if !confirmed {
194 + return Ok(done_result("Cancelled."));
195 + }
196 +
197 + let access_token = get_smb_token(env)?;
198 + let spinner = loading_spinner("Deleting mail app");
199 +
200 + match delete_mail_app(env, client(), access_token, mail_app_id).await {
201 + Ok(()) => Ok(CommandResult {
202 + spinner,
203 + symbol: succeed_symbol(),
204 + msg: succeed_message("Done. Mail app has been deleted."),
205 + }),
206 + Err(error) => Ok(CommandResult {
207 + spinner,
208 + symbol: fail_symbol(),
209 + msg: fail_message(&error.to_string()),
210 + }),
211 + }
212 +}
213 +
214 +async fn process_mail_inbox_new(
215 + env: Environment,
216 + app_id: String,
217 + local_part: String,
218 + forward_to: String,
219 + sender_email: Option<String>,
220 +) -> Result<CommandResult> {
221 + let access_token = get_smb_token(env)?;
222 + let mail_inbox = MailInboxCreate {
223 + local_part: normalize_required("local part", local_part)?,
224 + forward_to_email: normalize_required("forward target", forward_to)?,
225 + sender_email: normalize_optional(sender_email),
226 + };
227 + let mut spinner = loading_spinner("Creating mail inbox");
228 +
229 + let mail_inbox = create_mail_inbox(
230 + env,
231 + client(),
232 + access_token,
233 + normalize_required("mail app id", app_id)?,
234 + mail_inbox,
235 + )
236 + .await
237 + .map_err(api_error)?;
238 +
239 + spinner.stop_and_persist(&succeed_symbol(), succeed_message("Created."));
240 + print_mail_inbox_detail(&mail_inbox);
241 +
242 + Ok(done_result("Mail inbox created."))
243 +}
244 +
245 +async fn process_mail_inbox_update(
246 + env: Environment,
247 + app_id: String,
248 + id: String,
249 + local_part: Option<String>,
250 + forward_to: Option<String>,
251 + sender_email: Option<String>,
252 +) -> Result<CommandResult> {
253 + let access_token = get_smb_token(env)?;
254 + let update = MailInboxUpdate {
255 + local_part: normalize_optional(local_part),
256 + forward_to_email: normalize_optional(forward_to),
257 + sender_email: normalize_optional(sender_email),
258 + };
259 +
260 + if update.is_empty() {
261 + return Err(anyhow!(
262 + "Specify at least one of `--local-part`, `--forward-to`, or `--sender-email`."
263 + ));
264 + }
265 +
266 + let mut spinner = loading_spinner("Updating mail inbox");
267 + let mail_inbox = update_mail_inbox(
268 + env,
269 + client(),
270 + access_token,
271 + normalize_required("mail app id", app_id)?,
272 + normalize_required("mail inbox id", id)?,
273 + update,
274 + )
275 + .await
276 + .map_err(api_error)?;
277 +
278 + spinner.stop_and_persist(&succeed_symbol(), succeed_message("Updated."));
279 + print_mail_inbox_detail(&mail_inbox);
280 +
281 + Ok(done_result("Mail inbox updated."))
282 +}
283 +
284 +async fn process_mail_inbox_delete(
285 + env: Environment,
286 + app_id: String,
287 + id: String,
288 +) -> Result<CommandResult> {
289 + let mail_app_id = normalize_required("mail app id", app_id)?;
290 + let mail_inbox_id = normalize_required("mail inbox id", id)?;
291 + let confirmed = confirm_delete_tui(&format!(
292 + "Delete mail inbox #{mail_inbox_id} from mail app #{mail_app_id}"
293 + ))
294 + .map_err(|error| anyhow!(error))?;
295 +
296 + if !confirmed {
297 + return Ok(done_result("Cancelled."));
298 + }
299 +
300 + let access_token = get_smb_token(env)?;
301 + let spinner = loading_spinner("Deleting mail inbox");
302 +
303 + match delete_mail_inbox(env, client(), access_token, mail_app_id, mail_inbox_id).await {
304 + Ok(()) => Ok(CommandResult {
305 + spinner,
306 + symbol: succeed_symbol(),
307 + msg: succeed_message("Done. Mail inbox has been deleted."),
308 + }),
309 + Err(error) => Ok(CommandResult {
310 + spinner,
311 + symbol: fail_symbol(),
312 + msg: fail_message(&error.to_string()),
313 + }),
314 + }
315 +}
316 +
317 +async fn process_mail_inbox_test(
318 + env: Environment,
319 + app_id: String,
320 + id: String,
321 + recipient_email: Option<String>,
322 + subject: Option<String>,
323 + body: Option<String>,
324 +) -> Result<CommandResult> {
325 + let access_token = get_smb_token(env)?;
326 + let request = MailTestEmailRequest {
327 + recipient_email: normalize_optional(recipient_email),
328 + subject: normalize_optional(subject),
329 + body: normalize_optional(body),
330 + };
331 + let mut spinner = loading_spinner("Sending mail test email");
332 +
333 + let delivery = send_test_email(
334 + env,
335 + client(),
336 + access_token,
337 + normalize_required("mail app id", app_id)?,
338 + normalize_required("mail inbox id", id)?,
339 + request,
340 + )
341 + .await
342 + .map_err(api_error)?;
343 +
344 + spinner.stop_and_persist(&succeed_symbol(), succeed_message("Sent."));
345 + print_mail_test_delivery(&delivery);
346 +
347 + Ok(done_result("Mail test email sent."))
348 +}
349 +
350 +async fn process_mail_message_list(
351 + env: Environment,
352 + app_id: String,
353 + inbox_id: String,
354 + limit: u32,
355 +) -> Result<CommandResult> {
356 + let access_token = get_smb_token(env)?;
357 + let mut spinner = loading_spinner("Loading mail messages");
358 +
359 + let messages = get_mail_messages(
360 + env,
361 + client(),
362 + access_token,
363 + normalize_required("mail app id", app_id)?,
364 + normalize_required("mail inbox id", inbox_id)?,
365 + Some(limit.clamp(1, 100)),
366 + )
367 + .await
368 + .map_err(api_error)?;
369 +
370 + spinner.stop_and_persist(&succeed_symbol(), succeed_message("Loaded."));
371 + print_mail_messages(&messages);
372 +
373 + Ok(done_result(if messages.is_empty() {
374 + "No mail messages found."
375 + } else {
376 + "Done."
377 + }))
378 +}
379 +
380 +async fn process_mail_message_show(
381 + env: Environment,
382 + app_id: String,
383 + inbox_id: String,
384 + id: String,
385 +) -> Result<CommandResult> {
386 + let access_token = get_smb_token(env)?;
387 + let mut spinner = loading_spinner("Loading mail message");
388 +
389 + let message = get_mail_message(
390 + env,
391 + client(),
392 + access_token,
393 + normalize_required("mail app id", app_id)?,
394 + normalize_required("mail inbox id", inbox_id)?,
395 + normalize_required("mail message id", id)?,
396 + )
397 + .await
398 + .map_err(api_error)?;
399 +
400 + spinner.stop_and_persist(&succeed_symbol(), succeed_message("Loaded."));
401 + print_mail_message_detail(&message);
402 +
403 + Ok(done_result("Done."))
404 +}
405 +
406 +fn loading_spinner(message: &str) -> Spinner {
407 + Spinner::new(
408 + spinners::Spinners::SimpleDotsScrolling,
409 + succeed_message(message),
410 + )
411 +}
412 +
413 +fn done_result(message: &str) -> CommandResult {
414 + CommandResult {
415 + spinner: loading_spinner("Done"),
416 + symbol: succeed_symbol(),
417 + msg: succeed_message(message),
418 + }
419 +}
420 +
421 +fn normalize_required(label: &str, value: String) -> Result<String> {
422 + let normalized_value = value.trim().to_string();
423 + if normalized_value.is_empty() {
424 + return Err(anyhow!("{} cannot be empty.", label));
425 + }
426 + Ok(normalized_value)
427 +}
428 +
429 +fn normalize_optional(value: Option<String>) -> Option<String> {
430 + value.and_then(|value| {
431 + let normalized_value = value.trim().to_string();
432 + if normalized_value.is_empty() {
433 + None
434 + } else {
435 + Some(normalized_value)
436 + }
437 + })
438 +}
439 +
440 +fn api_error(error: impl std::fmt::Display) -> anyhow::Error {
441 + anyhow!(error.to_string())
442 +}
crates/cli/src/mail/render.rs new
+184
@@ -0,0 +1,184 @@
1 +use console::style;
2 +use smbcloud_model::mail::{MailApp, MailInbox, MailMessage, MailTestEmailDelivery};
3 +use std::collections::HashMap;
4 +
5 +fn print_heading(title: &str) {
6 + println!("\n{}", style(title).bold().underlined());
7 +}
8 +
9 +fn print_field(label: &str, value: impl AsRef<str>) {
10 + println!("{} {}", style(format!("{label}:")).cyan(), value.as_ref());
11 +}
12 +
13 +fn print_optional_field(label: &str, value: Option<&str>) {
14 + if let Some(value) = value {
15 + print_field(label, value);
16 + }
17 +}
18 +
19 +fn print_string_map(title: &str, values: &HashMap<String, String>) {
20 + if values.is_empty() {
21 + return;
22 + }
23 +
24 + print_heading(title);
25 + let mut pairs: Vec<_> = values.iter().collect();
26 + pairs.sort_by(|left, right| left.0.cmp(right.0));
27 +
28 + for (key, value) in pairs {
29 + print_field(key, value);
30 + }
31 +}
32 +
33 +pub(crate) fn print_mail_apps(mail_apps: &[MailApp]) {
34 + print_heading("Mail apps");
35 +
36 + if mail_apps.is_empty() {
37 + println!("No mail apps found.");
38 + return;
39 + }
40 +
41 + for mail_app in mail_apps {
42 + println!(
43 + "#{} {} ({}) [{}] project={} inboxes={}",
44 + mail_app.id,
45 + style(&mail_app.name).bold(),
46 + mail_app.domain,
47 + mail_app.status,
48 + mail_app.project_id,
49 + mail_app.inboxes.len()
50 + );
51 + }
52 +}
53 +
54 +pub(crate) fn print_mail_app_detail(mail_app: &MailApp) {
55 + print_heading("Mail app");
56 + print_field("ID", mail_app.id.to_string());
57 + print_field("Name", &mail_app.name);
58 + print_field("Domain", &mail_app.domain);
59 + print_field("Status", mail_app.status.to_string());
60 + print_field("AWS region", &mail_app.aws_region);
61 + print_field("Project ID", mail_app.project_id.to_string());
62 + print_field("Tenant ID", mail_app.tenant_id.to_string());
63 + print_field("SES inbound MX", &mail_app.ses_inbound_mx_value);
64 + print_field("Inbound bucket", &mail_app.inbound_bucket_name);
65 + print_field("Inbound prefix", &mail_app.inbound_email_key_prefix);
66 + print_field("Created at", mail_app.created_at.to_rfc3339());
67 + print_field("Updated at", mail_app.updated_at.to_rfc3339());
68 + print_string_map("Lambda config", &mail_app.lambda_config);
69 +
70 + print_heading("Inboxes");
71 + if mail_app.inboxes.is_empty() {
72 + println!("No inboxes configured.");
73 + return;
74 + }
75 +
76 + for inbox in &mail_app.inboxes {
77 + println!(
78 + "#{} {} -> {} [{}]",
79 + inbox.id,
80 + style(&inbox.full_address).bold(),
81 + inbox.forward_to_email,
82 + inbox.status
83 + );
84 + }
85 +}
86 +
87 +pub(crate) fn print_mail_inbox_detail(mail_inbox: &MailInbox) {
88 + print_heading("Mail inbox");
89 + print_field("ID", mail_inbox.id.to_string());
90 + print_field("Mail app ID", mail_inbox.mail_app_id.to_string());
91 + print_field("Project ID", mail_inbox.project_id.to_string());
92 + print_field("Tenant ID", mail_inbox.tenant_id.to_string());
93 + print_field("Local part", &mail_inbox.local_part);
94 + print_field("Inbox email", &mail_inbox.inbox_email);
95 + print_field("Forward target", &mail_inbox.forward_to_email);
96 + print_field("Sender email", &mail_inbox.sender_email);
97 + print_field("Status", mail_inbox.status.to_string());
98 + print_field("Email key prefix", &mail_inbox.email_key_prefix);
99 + print_field("Created at", mail_inbox.created_at.to_rfc3339());
100 + print_field("Updated at", mail_inbox.updated_at.to_rfc3339());
101 + print_optional_field(
102 + "Last test email sent at",
103 + mail_inbox
104 + .last_test_email_sent_at
105 + .as_ref()
106 + .map(|value| value.to_rfc3339())
107 + .as_deref(),
108 + );
109 + print_string_map("Lambda config", &mail_inbox.lambda_config);
110 +}
111 +
112 +pub(crate) fn print_mail_test_delivery(delivery: &MailTestEmailDelivery) {
113 + print_heading("Mail test delivery");
114 + print_field("Message ID", &delivery.message_id);
115 + print_field("Recipient", &delivery.recipient_email);
116 + print_field("Delivery method", &delivery.delivery_method);
117 + print_field("Sent at", &delivery.sent_at);
118 +}
119 +
120 +pub(crate) fn print_mail_messages(messages: &[MailMessage]) {
121 + print_heading("Mail messages");
122 +
123 + if messages.is_empty() {
124 + println!("No mail messages found.");
125 + return;
126 + }
127 +
128 + for message in messages {
129 + println!(
130 + "#{} {} from {} [{}]",
131 + message.id,
132 + message
133 + .subject
134 + .clone()
135 + .unwrap_or_else(|| "(no subject)".to_string()),
136 + message.from_email,
137 + message.status
138 + );
139 + if let Some(text_preview) = &message.text_preview {
140 + println!(" {}", text_preview);
141 + }
142 + }
143 +}
144 +
145 +pub(crate) fn print_mail_message_detail(message: &MailMessage) {
146 + print_heading("Mail message");
147 + print_field("ID", message.id.to_string());
148 + print_field("Inbox ID", message.mail_inbox_id.to_string());
149 + print_field("Status", message.status.to_string());
150 + print_field("Provider message ID", &message.provider_message_id);
151 + print_field("Original recipient", &message.original_recipient_email);
152 + print_field("From email", &message.from_email);
153 + print_optional_field("From name", message.from_name.as_deref());
154 + print_optional_field("Subject", message.subject.as_deref());
155 + print_optional_field("Text preview", message.text_preview.as_deref());
156 + print_optional_field("Forward error", message.forward_error.as_deref());
157 + print_field("Received at", message.received_at.to_rfc3339());
158 + print_optional_field(
159 + "Forwarded at",
160 + message
161 + .forwarded_at
162 + .as_ref()
163 + .map(|value| value.to_rfc3339())
164 + .as_deref(),
165 + );
166 + print_optional_field(
167 + "Size bytes",
168 + message.size_bytes.map(|value| value.to_string()).as_deref(),
169 + );
170 + if !message.to_emails.is_empty() {
171 + print_field("To", message.to_emails.join(", "));
172 + }
173 + if !message.cc_emails.is_empty() {
174 + print_field("CC", message.cc_emails.join(", "));
175 + }
176 + if !message.forward_recipients.is_empty() {
177 + print_field("Forward recipients", message.forward_recipients.join(", "));
178 + }
179 + print_optional_field("S3 bucket", message.s3_bucket.as_deref());
180 + print_optional_field("S3 key", message.s3_key.as_deref());
181 + print_optional_field("Text body", message.text_body.as_deref());
182 + print_optional_field("HTML body", message.html_body.as_deref());
183 + print_string_map("Headers", &message.headers_json);
184 +}
crates/cli/src/main.rs
+3
@@ -7,6 +7,7 @@ use {
7 clear_smb_token,
8 cli::{Cli, CommandResult, Commands},
9 deploy::process_deploy::process_deploy,
10 + mail::process::process_mail,
11 project::{crud_create::process_project_init, process::process_project},
12 },
13 smbcloud_network::{environment::Environment, network::check_internet_connection},
@@ -125,6 +126,7 @@ async fn run(cli: Cli) -> Result<CommandResult> {
126 | Some(Commands::Login {})
127 | Some(Commands::Logout {})
128 | Some(Commands::Account { .. })
129 + | Some(Commands::Mail { .. })
130 | Some(Commands::Project { .. })
131 | None => true,
132 Some(Commands::Init {}) => true,
@@ -144,6 +146,7 @@ async fn run(cli: Cli) -> Result<CommandResult> {
146 Some(Commands::Account { command }) => process_account(cli.environment, command).await,
147 Some(Commands::Login {}) => process_login(cli.environment, None).await,
148 Some(Commands::Logout {}) => process_logout(cli.environment).await,
149 + Some(Commands::Mail { command }) => process_mail(cli.environment, command).await,
150 Some(Commands::Project { command }) => process_project(cli.environment, command).await,
151 None => process_deploy(cli.environment, None).await,
152 }
crates/cli/src/project/crud_read.rs
+4 -2
@@ -105,12 +105,14 @@ pub(crate) async fn process_project_use(env: Environment, id: String) -> Result<
105 );
106 match home::home_dir() {
107 Some(path) => {
108 - debug!("{}", path.to_str().unwrap());
108 + let config_directory = path.join(env.smb_dir());
109 + std::fs::create_dir_all(&config_directory)?;
110 + debug!("{}", config_directory.display());
111 let mut file = OpenOptions::new()
112 .create(true)
113 .truncate(true)
114 .write(true)
113 - .open([path.to_str().unwrap(), "/.smb/config.json"].join(""))?;
115 + .open(config_directory.join("config.json"))?;
116 let json = serde_json::to_string(&config)?;
117 file.write_all(json.as_bytes())?;
118
crates/smbcloud-mail-sdk/README.md deleted
-3
@@ -1,3 +0,0 @@
1 -# smbcloud-mail-sdk
2 -
3 -smbCloud [Mail](https://upinipin.fandom.com/ms/wiki/Mail) native SDK.
crates/smbcloud-mail/Cargo.toml new
+26
@@ -0,0 +1,26 @@
1 +[package]
2 +name = "smbcloud-mail"
3 +version = "0.3.38"
4 +edition = "2021"
5 +authors = ["Seto Elkahfi <hej@setoelkahfi.se>"]
6 +description = "smbCloud mail API client for the CLI."
7 +license = "Apache-2.0"
8 +repository = "https://github.com/smbcloudXYZ/smbcloud-cli"
9 +documentation = "https://smbcloud.xyz/posts"
10 +keywords = ["cli", "cloud", "mail", "smbcloud", "email"]
11 +categories = [
12 + "command-line-utilities",
13 + "api-bindings",
14 +]
15 +readme = "README.md"
16 +
17 +[lib]
18 +path = "src/smbcloud_mail.rs"
19 +
20 +[dependencies]
21 +reqwest = { workspace = true, features = ["json", "rustls-tls-native-roots"] }
22 +serde = { workspace = true, features = ["derive"] }
23 +serde_json = { workspace = true }
24 +smbcloud-model = { workspace = true }
25 +smbcloud-network = { workspace = true }
26 +smbcloud-networking = { workspace = true }
crates/smbcloud-mail/src/mail_app.rs new
+85
@@ -0,0 +1,85 @@
1 +use crate::{
2 + request::request_empty,
3 + url_builder::{build_mail_app_url, build_mail_apps_url},
4 +};
5 +use reqwest::Client;
6 +use serde::Serialize;
7 +use smbcloud_model::{
8 + error_codes::ErrorResponse,
9 + mail::{MailApp, MailAppCreate, MailAppUpdate},
10 +};
11 +use smbcloud_network::{environment::Environment, network::request};
12 +use smbcloud_networking::{constants::SMB_USER_AGENT, smb_client::SmbClient};
13 +
14 +#[derive(Serialize)]
15 +struct MailAppEnvelope<T> {
16 + mail_app: T,
17 +}
18 +
19 +pub async fn get_mail_apps(
20 + env: Environment,
21 + client: (&SmbClient, &str),
22 + access_token: String,
23 + project_id: Option<String>,
24 +) -> Result<Vec<MailApp>, ErrorResponse> {
25 + let builder = Client::new()
26 + .get(build_mail_apps_url(env, client, project_id.as_deref()))
27 + .header("Authorization", access_token)
28 + .header("User-agent", SMB_USER_AGENT);
29 + request(builder).await
30 +}
31 +
32 +pub async fn get_mail_app(
33 + env: Environment,
34 + client: (&SmbClient, &str),
35 + access_token: String,
36 + mail_app_id: String,
37 +) -> Result<MailApp, ErrorResponse> {
38 + let builder = Client::new()
39 + .get(build_mail_app_url(env, client, &mail_app_id))
40 + .header("Authorization", access_token)
41 + .header("User-agent", SMB_USER_AGENT);
42 + request(builder).await
43 +}
44 +
45 +pub async fn create_mail_app(
46 + env: Environment,
47 + client: (&SmbClient, &str),
48 + access_token: String,
49 + mail_app: MailAppCreate,
50 +) -> Result<MailApp, ErrorResponse> {
51 + let builder = Client::new()
52 + .post(build_mail_apps_url(env, client, None))
53 + .json(&MailAppEnvelope { mail_app })
54 + .header("Authorization", access_token)
55 + .header("User-agent", SMB_USER_AGENT);
56 + request(builder).await
57 +}
58 +
59 +pub async fn update_mail_app(
60 + env: Environment,
61 + client: (&SmbClient, &str),
62 + access_token: String,
63 + mail_app_id: String,
64 + mail_app: MailAppUpdate,
65 +) -> Result<MailApp, ErrorResponse> {
66 + let builder = Client::new()
67 + .put(build_mail_app_url(env, client, &mail_app_id))
68 + .json(&MailAppEnvelope { mail_app })
69 + .header("Authorization", access_token)
70 + .header("User-agent", SMB_USER_AGENT);
71 + request(builder).await
72 +}
73 +
74 +pub async fn delete_mail_app(
75 + env: Environment,
76 + client: (&SmbClient, &str),
77 + access_token: String,
78 + mail_app_id: String,
79 +) -> Result<(), ErrorResponse> {
80 + let builder = Client::new()
81 + .delete(build_mail_app_url(env, client, &mail_app_id))
82 + .header("Authorization", access_token)
83 + .header("User-agent", SMB_USER_AGENT);
84 + request_empty(builder).await
85 +}
crates/smbcloud-mail/src/mail_inbox.rs new
+85
@@ -0,0 +1,85 @@
1 +use crate::{
2 + request::request_empty,
3 + url_builder::{build_mail_inbox_test_url, build_mail_inbox_url, build_mail_inboxes_url},
4 +};
5 +use reqwest::Client;
6 +use serde::Serialize;
7 +use smbcloud_model::{
8 + error_codes::ErrorResponse,
9 + mail::{
10 + MailInbox, MailInboxCreate, MailInboxUpdate, MailTestEmailDelivery, MailTestEmailRequest,
11 + },
12 +};
13 +use smbcloud_network::{environment::Environment, network::request};
14 +use smbcloud_networking::{constants::SMB_USER_AGENT, smb_client::SmbClient};
15 +
16 +#[derive(Serialize)]
17 +struct MailInboxEnvelope<T> {
18 + mail_inbox: T,
19 +}
20 +
21 +pub async fn create_mail_inbox(
22 + env: Environment,
23 + client: (&SmbClient, &str),
24 + access_token: String,
25 + mail_app_id: String,
26 + mail_inbox: MailInboxCreate,
27 +) -> Result<MailInbox, ErrorResponse> {
28 + let builder = Client::new()
29 + .post(build_mail_inboxes_url(env, client, &mail_app_id))
30 + .json(&MailInboxEnvelope { mail_inbox })
31 + .header("Authorization", access_token)
32 + .header("User-agent", SMB_USER_AGENT);
33 + request(builder).await
34 +}
35 +
36 +pub async fn update_mail_inbox(
37 + env: Environment,
38 + client: (&SmbClient, &str),
39 + access_token: String,
40 + mail_app_id: String,
41 + inbox_id: String,
42 + mail_inbox: MailInboxUpdate,
43 +) -> Result<MailInbox, ErrorResponse> {
44 + let builder = Client::new()
45 + .put(build_mail_inbox_url(env, client, &mail_app_id, &inbox_id))
46 + .json(&MailInboxEnvelope { mail_inbox })
47 + .header("Authorization", access_token)
48 + .header("User-agent", SMB_USER_AGENT);
49 + request(builder).await
50 +}
51 +
52 +pub async fn delete_mail_inbox(
53 + env: Environment,
54 + client: (&SmbClient, &str),
55 + access_token: String,
56 + mail_app_id: String,
57 + inbox_id: String,
58 +) -> Result<(), ErrorResponse> {
59 + let builder = Client::new()
60 + .delete(build_mail_inbox_url(env, client, &mail_app_id, &inbox_id))
61 + .header("Authorization", access_token)
62 + .header("User-agent", SMB_USER_AGENT);
63 + request_empty(builder).await
64 +}
65 +
66 +pub async fn send_test_email(
67 + env: Environment,
68 + client: (&SmbClient, &str),
69 + access_token: String,
70 + mail_app_id: String,
71 + inbox_id: String,
72 + mail_inbox: MailTestEmailRequest,
73 +) -> Result<MailTestEmailDelivery, ErrorResponse> {
74 + let builder = Client::new()
75 + .post(build_mail_inbox_test_url(
76 + env,
77 + client,
78 + &mail_app_id,
79 + &inbox_id,
80 + ))
81 + .json(&MailInboxEnvelope { mail_inbox })
82 + .header("Authorization", access_token)
83 + .header("User-agent", SMB_USER_AGENT);
84 + request(builder).await
85 +}
crates/smbcloud-mail/src/mail_message.rs new
+47
@@ -0,0 +1,47 @@
1 +use crate::url_builder::{build_mail_message_url, build_mail_messages_url};
2 +use reqwest::Client;
3 +use smbcloud_model::{error_codes::ErrorResponse, mail::MailMessage};
4 +use smbcloud_network::{environment::Environment, network::request};
5 +use smbcloud_networking::{constants::SMB_USER_AGENT, smb_client::SmbClient};
6 +
7 +pub async fn get_mail_messages(
8 + env: Environment,
9 + client: (&SmbClient, &str),
10 + access_token: String,
11 + mail_app_id: String,
12 + inbox_id: String,
13 + limit: Option<u32>,
14 +) -> Result<Vec<MailMessage>, ErrorResponse> {
15 + let builder = Client::new()
16 + .get(build_mail_messages_url(
17 + env,
18 + client,
19 + &mail_app_id,
20 + &inbox_id,
21 + limit,
22 + ))
23 + .header("Authorization", access_token)
24 + .header("User-agent", SMB_USER_AGENT);
25 + request(builder).await
26 +}
27 +
28 +pub async fn get_mail_message(
29 + env: Environment,
30 + client: (&SmbClient, &str),
31 + access_token: String,
32 + mail_app_id: String,
33 + inbox_id: String,
34 + message_id: String,
35 +) -> Result<MailMessage, ErrorResponse> {
36 + let builder = Client::new()
37 + .get(build_mail_message_url(
38 + env,
39 + client,
40 + &mail_app_id,
41 + &inbox_id,
42 + &message_id,
43 + ))
44 + .header("Authorization", access_token)
45 + .header("User-agent", SMB_USER_AGENT);
46 + request(builder).await
47 +}
crates/smbcloud-mail/src/request.rs new
+30
@@ -0,0 +1,30 @@
1 +use reqwest::{RequestBuilder, StatusCode};
2 +use serde_json::Value;
3 +use smbcloud_model::error_codes::{ErrorCode, ErrorResponse};
4 +use smbcloud_network::network::{check_internet_connection, parse_error_response};
5 +
6 +pub(crate) async fn request_empty(builder: RequestBuilder) -> Result<(), ErrorResponse> {
7 + if !check_internet_connection().await {
8 + return Err(ErrorResponse::Error {
9 + error_code: ErrorCode::NetworkError,
10 + message: "No internet connection. Please check your network settings and try again."
11 + .to_string(),
12 + });
13 + }
14 +
15 + let response = builder.send().await.map_err(|_| ErrorResponse::Error {
16 + error_code: ErrorCode::NetworkError,
17 + message: ErrorCode::NetworkError.message(None).to_string(),
18 + })?;
19 +
20 + match response.status() {
21 + StatusCode::OK | StatusCode::CREATED | StatusCode::NO_CONTENT => Ok(()),
22 + _ => match parse_error_response::<Value>(response).await {
23 + Ok(_) => Err(ErrorResponse::Error {
24 + error_code: ErrorCode::ParseError,
25 + message: ErrorCode::ParseError.message(None).to_string(),
26 + }),
27 + Err(error_response) => Err(error_response),
28 + },
29 + }
30 +}
crates/smbcloud-mail/src/smbcloud_mail.rs new
+5
@@ -0,0 +1,5 @@
1 +pub mod mail_app;
2 +pub mod mail_inbox;
3 +pub mod mail_message;
4 +mod request;
5 +mod url_builder;
crates/smbcloud-mail/src/url_builder.rs new
+106
@@ -0,0 +1,106 @@
1 +use {
2 + smbcloud_network::environment::Environment,
3 + smbcloud_networking::{smb_base_url_builder, smb_client::SmbClient},
4 +};
5 +
6 +pub(crate) fn build_mail_apps_url(
7 + env: Environment,
8 + client: (&SmbClient, &str),
9 + project_id: Option<&str>,
10 +) -> String {
11 + let mut url_builder = smb_base_url_builder(env, client);
12 + url_builder.add_route("v1/mail_apps");
13 + if let Some(project_id) = project_id {
14 + url_builder.add_param("project_id", project_id);
15 + }
16 + url_builder.build()
17 +}
18 +
19 +pub(crate) fn build_mail_app_url(
20 + env: Environment,
21 + client: (&SmbClient, &str),
22 + mail_app_id: &str,
23 +) -> String {
24 + let mut url_builder = smb_base_url_builder(env, client);
25 + url_builder.add_route("v1/mail_apps");
26 + url_builder.add_route(mail_app_id);
27 + url_builder.build()
28 +}
29 +
30 +pub(crate) fn build_mail_inboxes_url(
31 + env: Environment,
32 + client: (&SmbClient, &str),
33 + mail_app_id: &str,
34 +) -> String {
35 + let mut url_builder = smb_base_url_builder(env, client);
36 + url_builder.add_route("v1/mail_apps");
37 + url_builder.add_route(mail_app_id);
38 + url_builder.add_route("inboxes");
39 + url_builder.build()
40 +}
41 +
42 +pub(crate) fn build_mail_inbox_url(
43 + env: Environment,
44 + client: (&SmbClient, &str),
45 + mail_app_id: &str,
46 + inbox_id: &str,
47 +) -> String {
48 + let mut url_builder = smb_base_url_builder(env, client);
49 + url_builder.add_route("v1/mail_apps");
50 + url_builder.add_route(mail_app_id);
51 + url_builder.add_route("inboxes");
52 + url_builder.add_route(inbox_id);
53 + url_builder.build()
54 +}
55 +
56 +pub(crate) fn build_mail_inbox_test_url(
57 + env: Environment,
58 + client: (&SmbClient, &str),
59 + mail_app_id: &str,
60 + inbox_id: &str,
61 +) -> String {
62 + let mut url_builder = smb_base_url_builder(env, client);
63 + url_builder.add_route("v1/mail_apps");
64 + url_builder.add_route(mail_app_id);
65 + url_builder.add_route("inboxes");
66 + url_builder.add_route(inbox_id);
67 + url_builder.add_route("send_test_email");
68 + url_builder.build()
69 +}
70 +
71 +pub(crate) fn build_mail_messages_url(
72 + env: Environment,
73 + client: (&SmbClient, &str),
74 + mail_app_id: &str,
75 + inbox_id: &str,
76 + limit: Option<u32>,
77 +) -> String {
78 + let mut url_builder = smb_base_url_builder(env, client);
79 + url_builder.add_route("v1/mail_apps");
80 + url_builder.add_route(mail_app_id);
81 + url_builder.add_route("inboxes");
82 + url_builder.add_route(inbox_id);
83 + url_builder.add_route("messages");
84 + if let Some(limit) = limit {
85 + let limit_string = limit.to_string();
86 + url_builder.add_param("limit", &limit_string);
87 + }
88 + url_builder.build()
89 +}
90 +
91 +pub(crate) fn build_mail_message_url(
92 + env: Environment,
93 + client: (&SmbClient, &str),
94 + mail_app_id: &str,
95 + inbox_id: &str,
96 + message_id: &str,
97 +) -> String {
98 + let mut url_builder = smb_base_url_builder(env, client);
99 + url_builder.add_route("v1/mail_apps");
100 + url_builder.add_route(mail_app_id);
101 + url_builder.add_route("inboxes");
102 + url_builder.add_route(inbox_id);
103 + url_builder.add_route("messages");
104 + url_builder.add_route(message_id);
105 + url_builder.build()
106 +}
crates/smbcloud-model/src/lib.rs
+1
@@ -4,6 +4,7 @@ pub mod error_codes;
4 pub mod forgot;
5 pub mod frontend_app;
6 pub mod login;
7 +pub mod mail;
8 pub mod oauth;
9 pub mod project;
10 pub mod repository;
crates/smbcloud-model/src/mail.rs new
+182
@@ -0,0 +1,182 @@
1 +use {
2 + chrono::{DateTime, Utc},
3 + serde::{Deserialize, Serialize},
4 + serde_repr::{Deserialize_repr, Serialize_repr},
5 + std::{collections::HashMap, fmt::Display},
6 +};
7 +
8 +#[derive(Deserialize_repr, Serialize_repr, Debug, Clone, Copy, PartialEq, Eq, Default)]
9 +#[repr(u8)]
10 +pub enum MailStatus {
11 + #[default]
12 + PendingDns = 0,
13 + Active = 1,
14 + Suspended = 2,
15 +}
16 +
17 +impl Display for MailStatus {
18 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19 + match self {
20 + MailStatus::PendingDns => write!(f, "pending_dns"),
21 + MailStatus::Active => write!(f, "active"),
22 + MailStatus::Suspended => write!(f, "suspended"),
23 + }
24 + }
25 +}
26 +
27 +#[derive(Deserialize_repr, Serialize_repr, Debug, Clone, Copy, PartialEq, Eq, Default)]
28 +#[repr(u8)]
29 +pub enum MailMessageStatus {
30 + #[default]
31 + Received = 0,
32 + Forwarded = 1,
33 + ForwardFailed = 2,
34 +}
35 +
36 +impl Display for MailMessageStatus {
37 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 + match self {
39 + MailMessageStatus::Received => write!(f, "received"),
40 + MailMessageStatus::Forwarded => write!(f, "forwarded"),
41 + MailMessageStatus::ForwardFailed => write!(f, "forward_failed"),
42 + }
43 + }
44 +}
45 +
46 +#[derive(Deserialize, Serialize, Debug, Clone)]
47 +pub struct MailApp {
48 + pub id: i32,
49 + pub name: String,
50 + pub domain: String,
51 + pub aws_region: String,
52 + pub project_id: i32,
53 + pub tenant_id: i32,
54 + pub created_at: DateTime<Utc>,
55 + pub updated_at: DateTime<Utc>,
56 + pub status: MailStatus,
57 + pub ses_inbound_mx_value: String,
58 + pub inbound_bucket_name: String,
59 + pub inbound_email_key_prefix: String,
60 + #[serde(default)]
61 + pub lambda_config: HashMap<String, String>,
62 + #[serde(default)]
63 + pub inboxes: Vec<MailInbox>,
64 +}
65 +
66 +#[derive(Deserialize, Serialize, Debug, Clone)]
67 +pub struct MailInbox {
68 + pub id: i32,
69 + pub local_part: String,
70 + pub full_address: String,
71 + pub inbox_email: String,
72 + pub sender_email: String,
73 + pub forward_to_email: String,
74 + pub mail_app_id: i32,
75 + pub project_id: i32,
76 + pub tenant_id: i32,
77 + pub last_test_email_sent_at: Option<DateTime<Utc>>,
78 + pub created_at: DateTime<Utc>,
79 + pub updated_at: DateTime<Utc>,
80 + pub status: MailStatus,
81 + pub email_key_prefix: String,
82 + #[serde(default)]
83 + pub lambda_config: HashMap<String, String>,
84 +}
85 +
86 +#[derive(Deserialize, Serialize, Debug, Clone)]
87 +pub struct MailMessage {
88 + pub id: i32,
89 + pub mail_inbox_id: i32,
90 + pub provider_message_id: String,
91 + pub original_recipient_email: String,
92 + pub from_email: String,
93 + pub from_name: Option<String>,
94 + pub subject: Option<String>,
95 + pub text_preview: Option<String>,
96 + pub size_bytes: Option<i64>,
97 + pub forwarded_at: Option<DateTime<Utc>>,
98 + pub forward_error: Option<String>,
99 + pub received_at: DateTime<Utc>,
100 + pub created_at: DateTime<Utc>,
101 + pub updated_at: DateTime<Utc>,
102 + pub status: MailMessageStatus,
103 + pub text_body: Option<String>,
104 + pub html_body: Option<String>,
105 + #[serde(default)]
106 + pub to_emails: Vec<String>,
107 + #[serde(default)]
108 + pub cc_emails: Vec<String>,
109 + #[serde(default)]
110 + pub forward_recipients: Vec<String>,
111 + #[serde(default)]
112 + pub headers_json: HashMap<String, String>,
113 + pub s3_bucket: Option<String>,
114 + pub s3_key: Option<String>,
115 +}
116 +
117 +#[derive(Serialize, Debug, Clone)]
118 +pub struct MailAppCreate {
119 + pub name: String,
120 + pub project_id: String,
121 + pub domain: String,
122 + #[serde(skip_serializing_if = "Option::is_none")]
123 + pub aws_region: Option<String>,
124 +}
125 +
126 +#[derive(Serialize, Debug, Clone, Default)]
127 +pub struct MailAppUpdate {
128 + #[serde(skip_serializing_if = "Option::is_none")]
129 + pub name: Option<String>,
130 + #[serde(skip_serializing_if = "Option::is_none")]
131 + pub domain: Option<String>,
132 + #[serde(skip_serializing_if = "Option::is_none")]
133 + pub aws_region: Option<String>,
134 +}
135 +
136 +impl MailAppUpdate {
137 + pub fn is_empty(&self) -> bool {
138 + self.name.is_none() && self.domain.is_none() && self.aws_region.is_none()
139 + }
140 +}
141 +
142 +#[derive(Serialize, Debug, Clone)]
143 +pub struct MailInboxCreate {
144 + pub local_part: String,
145 + pub forward_to_email: String,
146 + #[serde(skip_serializing_if = "Option::is_none")]
147 + pub sender_email: Option<String>,
148 +}
149 +
150 +#[derive(Serialize, Debug, Clone, Default)]
151 +pub struct MailInboxUpdate {
152 + #[serde(skip_serializing_if = "Option::is_none")]
153 + pub local_part: Option<String>,
154 + #[serde(skip_serializing_if = "Option::is_none")]
155 + pub forward_to_email: Option<String>,
156 + #[serde(skip_serializing_if = "Option::is_none")]
157 + pub sender_email: Option<String>,
158 +}
159 +
160 +impl MailInboxUpdate {
161 + pub fn is_empty(&self) -> bool {
162 + self.local_part.is_none() && self.forward_to_email.is_none() && self.sender_email.is_none()
163 + }
164 +}
165 +
166 +#[derive(Serialize, Debug, Clone, Default)]
167 +pub struct MailTestEmailRequest {
168 + #[serde(skip_serializing_if = "Option::is_none")]
169 + pub recipient_email: Option<String>,
170 + #[serde(skip_serializing_if = "Option::is_none")]
171 + pub subject: Option<String>,
172 + #[serde(skip_serializing_if = "Option::is_none")]
173 + pub body: Option<String>,
174 +}
175 +
176 +#[derive(Deserialize, Serialize, Debug, Clone)]
177 +pub struct MailTestEmailDelivery {
178 + pub message_id: String,
179 + pub recipient_email: String,
180 + pub delivery_method: String,
181 + pub sent_at: String,
182 +}