From c5ba5978cd0558185d595fc0729408ffcaed8f81 Mon Sep 17 00:00:00 2001 From: quambene Date: Thu, 21 Mar 2024 22:04:09 +0100 Subject: [PATCH 01/54] Add integration test job --- .github/workflows/rust-ci.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 555328c..5ae70dd 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -48,8 +48,15 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - - name: cargo test - run: cargo test --locked + - name: cargo test --lib + run: cargo test --lib --locked + integration-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: cargo test --test '*' + run: cargo test --test '*' --locked doc-test: runs-on: ubuntu-latest steps: From e36b696f6c08540461b43cd87263e388ed0bd395 Mon Sep 17 00:00:00 2001 From: quambene Date: Thu, 21 Mar 2024 22:08:31 +0100 Subject: [PATCH 02/54] Add testing to readme --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 627c266..03dec82 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ elie@cartan.com ... ok - [Third-party APIs](#third-party-apis) - [Data sources](#data-sources) - [Comparison with Mailchimp, Sendgrid, and ConvertKit](#comparison-with-mailchimp-sendgrid-and-convertkit) +- [Testing](#testing) ## Install Pigeon @@ -436,3 +437,18 @@ provider | daily limit Pigeon+AWS | 50,000 Mailchimp | equals monthly limit Sendgrid | equals monthly limit + +## Testing + +Integration tests require a locally running database. + +``` bash +# Run unit tests and integration tests +cargo test + +# Run unit tests +cargo test --lib + +# Run integration tests +cargo test --test '*' +``` From dbba3397496f6dd906387686d6b19675226f21e1 Mon Sep 17 00:00:00 2001 From: quambene Date: Thu, 21 Mar 2024 22:57:31 +0100 Subject: [PATCH 03/54] Update readme --- README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 03dec82..b4e25a3 100644 --- a/README.md +++ b/README.md @@ -440,7 +440,19 @@ Sendgrid | equals monthly limit ## Testing -Integration tests require a locally running database. +Integration tests require a locally running database, and an AWS SES account. +Specify the following environment variables: + +- AWS SES + - `AWS_ACCESS_KEY_ID` + - `AWS_SECRET_ACCESS_KEY` + - `AWS_REGION` +- Postgres + - `DB_HOST` + - `DB_PORT` + - `DB_USER` + - `DB_PASSWORD` + - `DB_NAME` ``` bash # Run unit tests and integration tests From 9a1e94b6bfaeb540c19d970e8e190f7bf409db88 Mon Sep 17 00:00:00 2001 From: quambene Date: Thu, 21 Mar 2024 22:59:12 +0100 Subject: [PATCH 04/54] Move tests --- src/cmd/query.rs | 72 --------- src/cmd/read.rs | 21 --- src/cmd/send_bulk.rs | 329 ---------------------------------------- tests/test_connect.rs | 0 tests/test_init.rs | 0 tests/test_query.rs | 67 ++++++++ tests/test_read.rs | 16 ++ tests/test_send.rs | 0 tests/test_send_bulk.rs | 324 +++++++++++++++++++++++++++++++++++++++ 9 files changed, 407 insertions(+), 422 deletions(-) create mode 100644 tests/test_connect.rs create mode 100644 tests/test_init.rs create mode 100644 tests/test_query.rs create mode 100644 tests/test_read.rs create mode 100644 tests/test_send.rs create mode 100644 tests/test_send_bulk.rs diff --git a/src/cmd/query.rs b/src/cmd/query.rs index 12d90dc..2254333 100644 --- a/src/cmd/query.rs +++ b/src/cmd/query.rs @@ -97,75 +97,3 @@ pub fn query(matches: &ArgMatches) -> Result<(), anyhow::Error> { Err(anyhow!("Missing argument '{}'", cmd::QUERY)) } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::{app, cmd}; - use std::env; - - #[test] - #[ignore] - fn test_display_query() { - let test_query = env::var("TEST_QUERY").expect("Missing environment variable 'TEST_QUERY'"); - let args = vec![cmd::BIN, cmd::QUERY, test_query.as_str(), "--display"]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::QUERY).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = query(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_save_query() { - let test_query = env::var("TEST_QUERY").expect("Missing environment variable 'TEST_QUERY'"); - let args = vec![ - cmd::BIN, - cmd::QUERY, - test_query.as_str(), - "--display", - "--save", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::QUERY).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = query(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_save_dir() { - let test_query = env::var("TEST_QUERY").expect("Missing environment variable 'TEST_QUERY'"); - let args = vec![ - cmd::BIN, - cmd::QUERY, - test_query.as_str(), - "--display", - "--save", - "--save-dir", - "./my-saved-queries", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::QUERY).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = query(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } -} diff --git a/src/cmd/read.rs b/src/cmd/read.rs index 7da9f11..e03c0e2 100644 --- a/src/cmd/read.rs +++ b/src/cmd/read.rs @@ -40,24 +40,3 @@ pub fn read(matches: &ArgMatches) -> Result<(), anyhow::Error> { Err(anyhow!("Missing argument '{}'", cmd::READ)) } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::{app, cmd}; - - #[test] - fn test_read() { - let args = vec![cmd::BIN, cmd::READ, "./test_data/receiver.csv"]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::READ).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = read(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } -} diff --git a/src/cmd/send_bulk.rs b/src/cmd/send_bulk.rs index b893f07..1a82525 100644 --- a/src/cmd/send_bulk.rs +++ b/src/cmd/send_bulk.rs @@ -139,332 +139,3 @@ pub fn send_bulk(matches: &ArgMatches) -> Result<(), anyhow::Error> { Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - use crate::{app, cmd}; - - #[test] - #[ignore] - fn test_send_bulk_subject_content_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND_BULK, - "albert@einstein.com", - "--receiver-file", - "./test_data/receiver.csv", - "--subject", - "Test Subject", - "--content", - "This is a test message (plaintext).", - "--dry-run", - "--display", - "--assume-yes", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send_bulk(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_send_bulk_text_file_html_file_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND_BULK, - "albert@einstein.com", - "--receiver-file", - "./test_data/receiver.csv", - "--subject", - "Test Subject", - "--text-file", - "./test_data/message.txt", - "--html-file", - "./test_data/message.html", - "--dry-run", - "--display", - "--assume-yes", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send_bulk(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_send_bulk_message_file_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND_BULK, - "albert@einstein.com", - "--receiver-file", - "./test_data/receiver.csv", - "--message-file", - "./test_data/message.yaml", - "--dry-run", - "--display", - "--assume-yes", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send_bulk(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_send_bulk_receiver_column_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND_BULK, - "albert@einstein.com", - "--receiver-file", - "./test_data/contacts.csv", - "--message-file", - "./test_data/message.yaml", - "--receiver-column", - "contact", - "--dry-run", - "--display", - "--assume-yes", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send_bulk(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_send_bulk_personalize_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND_BULK, - "albert@einstein.com", - "--receiver-file", - "./test_data/receiver.csv", - "--message-file", - "./test_data/message_personalized.yaml", - "--personalize", - "first_name", - "last_name", - "--dry-run", - "--display", - "--assume-yes", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send_bulk(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_archive_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND_BULK, - "albert@einstein.com", - "--receiver-file", - "./test_data/receiver.csv", - "--message-file", - "./test_data/message.yaml", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send_bulk(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_archive_dir_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND_BULK, - "albert@einstein.com", - "--receiver-file", - "./test_data/receiver.csv", - "--message-file", - "./test_data/message.yaml", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - "--archive-dir", - "./my-sent-emails", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send_bulk(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_attachment_pdf_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND_BULK, - "albert@einstein.com", - "--receiver-file", - "./test_data/receiver.csv", - "--message-file", - "./test_data/message.yaml", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - "--attachment", - "./test_data/test.pdf", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send_bulk(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_attachment_png_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND_BULK, - "albert@einstein.com", - "--receiver-file", - "./test_data/receiver.csv", - "--message-file", - "./test_data/message.yaml", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - "--attachment", - "./test_data/test.png", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send_bulk(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_attachment_odt_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND_BULK, - "albert@einstein.com", - "--receiver-file", - "./test_data/receiver.csv", - "--message-file", - "./test_data/message.yaml", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - "--attachment", - "./test_data/test.odt", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send_bulk(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_send_bulk_aws_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND_BULK, - "albert@einstein.com", - "--receiver-file", - "./test_data/receiver.csv", - "--message-file", - "./test_data/message.yaml", - "--dry-run", - "--display", - "--assume-yes", - "--connection", - val::AWS, - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send_bulk(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } -} diff --git a/tests/test_connect.rs b/tests/test_connect.rs new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_init.rs b/tests/test_init.rs new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_query.rs b/tests/test_query.rs new file mode 100644 index 0000000..edcd269 --- /dev/null +++ b/tests/test_query.rs @@ -0,0 +1,67 @@ +use pigeon_rs::{app, cmd}; +use std::env; + +#[test] +#[ignore] +fn test_display_query() { + let test_query = env::var("TEST_QUERY").expect("Missing environment variable 'TEST_QUERY'"); + let args = vec![cmd::BIN, cmd::QUERY, test_query.as_str(), "--display"]; + + let app = app(); + let matches = app.get_matches_from(args); + let subcommand_matches = matches.subcommand_matches(cmd::QUERY).unwrap(); + println!("subcommand matches: {:#?}", subcommand_matches); + + let res = cmd::query(subcommand_matches); + println!("res: {:#?}", res); + + assert!(res.is_ok()) +} + +#[test] +#[ignore] +fn test_save_query() { + let test_query = env::var("TEST_QUERY").expect("Missing environment variable 'TEST_QUERY'"); + let args = vec![ + cmd::BIN, + cmd::QUERY, + test_query.as_str(), + "--display", + "--save", + ]; + + let app = app(); + let matches = app.get_matches_from(args); + let subcommand_matches = matches.subcommand_matches(cmd::QUERY).unwrap(); + println!("subcommand matches: {:#?}", subcommand_matches); + + let res = cmd::query(subcommand_matches); + println!("res: {:#?}", res); + + assert!(res.is_ok()) +} + +#[test] +#[ignore] +fn test_save_dir() { + let test_query = env::var("TEST_QUERY").expect("Missing environment variable 'TEST_QUERY'"); + let args = vec![ + cmd::BIN, + cmd::QUERY, + test_query.as_str(), + "--display", + "--save", + "--save-dir", + "./my-saved-queries", + ]; + + let app = app(); + let matches = app.get_matches_from(args); + let subcommand_matches = matches.subcommand_matches(cmd::QUERY).unwrap(); + println!("subcommand matches: {:#?}", subcommand_matches); + + let res = cmd::query(subcommand_matches); + println!("res: {:#?}", res); + + assert!(res.is_ok()) +} diff --git a/tests/test_read.rs b/tests/test_read.rs new file mode 100644 index 0000000..af4b4f8 --- /dev/null +++ b/tests/test_read.rs @@ -0,0 +1,16 @@ +use pigeon_rs::{app, cmd}; + +#[test] +fn test_read() { + let args = vec![cmd::BIN, cmd::READ, "./test_data/receiver.csv"]; + + let app = app(); + let matches = app.get_matches_from(args); + let subcommand_matches = matches.subcommand_matches(cmd::READ).unwrap(); + println!("subcommand matches: {:#?}", subcommand_matches); + + let res = cmd::read(subcommand_matches); + println!("res: {:#?}", res); + + assert!(res.is_ok()) +} diff --git a/tests/test_send.rs b/tests/test_send.rs new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_send_bulk.rs b/tests/test_send_bulk.rs new file mode 100644 index 0000000..3b79cc8 --- /dev/null +++ b/tests/test_send_bulk.rs @@ -0,0 +1,324 @@ +use pigeon_rs::{app, cmd}; + +#[test] +#[ignore] +fn test_send_bulk_subject_content_dry() { + let args = vec![ + cmd::BIN, + cmd::SEND_BULK, + "albert@einstein.com", + "--receiver-file", + "./test_data/receiver.csv", + "--subject", + "Test Subject", + "--content", + "This is a test message (plaintext).", + "--dry-run", + "--display", + "--assume-yes", + ]; + + let app = app(); + let matches = app.get_matches_from(args); + let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); + println!("subcommand matches: {:#?}", subcommand_matches); + + let res = cmd::send_bulk(subcommand_matches); + println!("res: {:#?}", res); + + assert!(res.is_ok()) +} + +#[test] +#[ignore] +fn test_send_bulk_text_file_html_file_dry() { + let args = vec![ + cmd::BIN, + cmd::SEND_BULK, + "albert@einstein.com", + "--receiver-file", + "./test_data/receiver.csv", + "--subject", + "Test Subject", + "--text-file", + "./test_data/message.txt", + "--html-file", + "./test_data/message.html", + "--dry-run", + "--display", + "--assume-yes", + ]; + + let app = app(); + let matches = app.get_matches_from(args); + let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); + println!("subcommand matches: {:#?}", subcommand_matches); + + let res = cmd::send_bulk(subcommand_matches); + println!("res: {:#?}", res); + + assert!(res.is_ok()) +} + +#[test] +#[ignore] +fn test_send_bulk_message_file_dry() { + let args = vec![ + cmd::BIN, + cmd::SEND_BULK, + "albert@einstein.com", + "--receiver-file", + "./test_data/receiver.csv", + "--message-file", + "./test_data/message.yaml", + "--dry-run", + "--display", + "--assume-yes", + ]; + + let app = app(); + let matches = app.get_matches_from(args); + let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); + println!("subcommand matches: {:#?}", subcommand_matches); + + let res = cmd::send_bulk(subcommand_matches); + println!("res: {:#?}", res); + + assert!(res.is_ok()) +} + +#[test] +#[ignore] +fn test_send_bulk_receiver_column_dry() { + let args = vec![ + cmd::BIN, + cmd::SEND_BULK, + "albert@einstein.com", + "--receiver-file", + "./test_data/contacts.csv", + "--message-file", + "./test_data/message.yaml", + "--receiver-column", + "contact", + "--dry-run", + "--display", + "--assume-yes", + ]; + + let app = app(); + let matches = app.get_matches_from(args); + let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); + println!("subcommand matches: {:#?}", subcommand_matches); + + let res = cmd::send_bulk(subcommand_matches); + println!("res: {:#?}", res); + + assert!(res.is_ok()) +} + +#[test] +#[ignore] +fn test_send_bulk_personalize_dry() { + let args = vec![ + cmd::BIN, + cmd::SEND_BULK, + "albert@einstein.com", + "--receiver-file", + "./test_data/receiver.csv", + "--message-file", + "./test_data/message_personalized.yaml", + "--personalize", + "first_name", + "last_name", + "--dry-run", + "--display", + "--assume-yes", + ]; + + let app = app(); + let matches = app.get_matches_from(args); + let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); + println!("subcommand matches: {:#?}", subcommand_matches); + + let res = cmd::send_bulk(subcommand_matches); + println!("res: {:#?}", res); + + assert!(res.is_ok()) +} + +#[test] +#[ignore] +fn test_archive_dry() { + let args = vec![ + cmd::BIN, + cmd::SEND_BULK, + "albert@einstein.com", + "--receiver-file", + "./test_data/receiver.csv", + "--message-file", + "./test_data/message.yaml", + "--dry-run", + "--display", + "--assume-yes", + "--archive", + ]; + + let app = app(); + let matches = app.get_matches_from(args); + let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); + println!("subcommand matches: {:#?}", subcommand_matches); + + let res = cmd::send_bulk(subcommand_matches); + println!("res: {:#?}", res); + + assert!(res.is_ok()) +} + +#[test] +#[ignore] +fn test_archive_dir_dry() { + let args = vec![ + cmd::BIN, + cmd::SEND_BULK, + "albert@einstein.com", + "--receiver-file", + "./test_data/receiver.csv", + "--message-file", + "./test_data/message.yaml", + "--dry-run", + "--display", + "--assume-yes", + "--archive", + "--archive-dir", + "./my-sent-emails", + ]; + + let app = app(); + let matches = app.get_matches_from(args); + let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); + println!("subcommand matches: {:#?}", subcommand_matches); + + let res = cmd::send_bulk(subcommand_matches); + println!("res: {:#?}", res); + + assert!(res.is_ok()) +} + +#[test] +#[ignore] +fn test_attachment_pdf_dry() { + let args = vec![ + cmd::BIN, + cmd::SEND_BULK, + "albert@einstein.com", + "--receiver-file", + "./test_data/receiver.csv", + "--message-file", + "./test_data/message.yaml", + "--dry-run", + "--display", + "--assume-yes", + "--archive", + "--attachment", + "./test_data/test.pdf", + ]; + + let app = app(); + let matches = app.get_matches_from(args); + let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); + println!("subcommand matches: {:#?}", subcommand_matches); + + let res = cmd::send_bulk(subcommand_matches); + println!("res: {:#?}", res); + + assert!(res.is_ok()) +} + +#[test] +#[ignore] +fn test_attachment_png_dry() { + let args = vec![ + cmd::BIN, + cmd::SEND_BULK, + "albert@einstein.com", + "--receiver-file", + "./test_data/receiver.csv", + "--message-file", + "./test_data/message.yaml", + "--dry-run", + "--display", + "--assume-yes", + "--archive", + "--attachment", + "./test_data/test.png", + ]; + + let app = app(); + let matches = app.get_matches_from(args); + let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); + println!("subcommand matches: {:#?}", subcommand_matches); + + let res = cmd::send_bulk(subcommand_matches); + println!("res: {:#?}", res); + + assert!(res.is_ok()) +} + +#[test] +#[ignore] +fn test_attachment_odt_dry() { + let args = vec![ + cmd::BIN, + cmd::SEND_BULK, + "albert@einstein.com", + "--receiver-file", + "./test_data/receiver.csv", + "--message-file", + "./test_data/message.yaml", + "--dry-run", + "--display", + "--assume-yes", + "--archive", + "--attachment", + "./test_data/test.odt", + ]; + + let app = app(); + let matches = app.get_matches_from(args); + let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); + println!("subcommand matches: {:#?}", subcommand_matches); + + let res = cmd::send_bulk(subcommand_matches); + println!("res: {:#?}", res); + + assert!(res.is_ok()) +} + +#[test] +#[ignore] +fn test_send_bulk_aws_dry() { + let args = vec![ + cmd::BIN, + cmd::SEND_BULK, + "albert@einstein.com", + "--receiver-file", + "./test_data/receiver.csv", + "--message-file", + "./test_data/message.yaml", + "--dry-run", + "--display", + "--assume-yes", + "--connection", + "aws", + ]; + + let app = app(); + let matches = app.get_matches_from(args); + let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); + println!("subcommand matches: {:#?}", subcommand_matches); + + let res = cmd::send_bulk(subcommand_matches); + println!("res: {:#?}", res); + + assert!(res.is_ok()) +} From 9c2fce39330a8f7d61a05f72453af4ee5fc74504 Mon Sep 17 00:00:00 2001 From: quambene Date: Fri, 22 Mar 2024 00:30:06 +0100 Subject: [PATCH 05/54] Refactor test --- Cargo.lock | 110 +++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 10 ++-- tests/test_query.rs | 20 ++++---- 3 files changed, 123 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 78c4bc8..90bf289 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -80,6 +80,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + [[package]] name = "anyhow" version = "1.0.75" @@ -146,6 +152,21 @@ dependencies = [ "zstd", ] +[[package]] +name = "assert_cmd" +version = "2.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed72493ac66d5804837f480ab3766c72bdfab91a65e565fc54fa9e42db0073a8" +dependencies = [ + "anstyle", + "bstr", + "doc-comment", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + [[package]] name = "async-trait" version = "0.1.74" @@ -276,6 +297,17 @@ dependencies = [ "syn_derive", ] +[[package]] +name = "bstr" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "bumpalo" version = "3.14.0" @@ -582,6 +614,12 @@ dependencies = [ "sct 0.6.1", ] +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "digest" version = "0.9.0" @@ -623,6 +661,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "dyn-clone" version = "1.0.16" @@ -738,6 +782,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1391,9 +1444,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "memmap2" @@ -1495,6 +1548,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + [[package]] name = "now" version = "0.1.3" @@ -1659,6 +1718,7 @@ name = "pigeon-rs" version = "0.4.0" dependencies = [ "anyhow", + "assert_cmd", "base64 0.13.1", "bytes", "chrono", @@ -1669,6 +1729,7 @@ dependencies = [ "lettre", "polars", "postgres", + "predicates", "rusoto_core", "rusoto_credential", "rusoto_ses", @@ -2035,6 +2096,36 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "predicates" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" + +[[package]] +name = "predicates-tree" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "proc-macro-crate" version = "2.0.1" @@ -2931,6 +3022,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "textwrap" version = "0.11.0" @@ -3217,6 +3314,15 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "want" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index 2761490..002152b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,10 @@ categories = ["command-line-utilities", "email"] readme = "README.md" license = "Apache-2.0" +[[bin]] +name = "pigeon" +path = "src/main.rs" + [dependencies] anyhow = "1.0.40" rusoto_ses = { version = "0.47.0", default-features = false, features = ["rustls"] } @@ -34,6 +38,6 @@ infer = "0.5.0" bytes = "1.1.0" base64 = "0.13.0" -[[bin]] -name = "pigeon" -path = "src/main.rs" +[dev-dependencies] +assert_cmd = "2.0.14" +predicates = "3.1.0" diff --git a/tests/test_query.rs b/tests/test_query.rs index edcd269..ee19e34 100644 --- a/tests/test_query.rs +++ b/tests/test_query.rs @@ -1,21 +1,17 @@ +use assert_cmd::Command; use pigeon_rs::{app, cmd}; +use predicates::str; use std::env; #[test] -#[ignore] fn test_display_query() { let test_query = env::var("TEST_QUERY").expect("Missing environment variable 'TEST_QUERY'"); - let args = vec![cmd::BIN, cmd::QUERY, test_query.as_str(), "--display"]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::QUERY).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = cmd::query(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) + println!("Execute 'pigeon query {test_query} --display'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args(["query", test_query.as_str(), "--display"]); + cmd.assert() + .success() + .stdout(str::contains("Display query result")); } #[test] From 2f01ae2139ab7173b22829de481f060a0cb0f4cc Mon Sep 17 00:00:00 2001 From: quambene Date: Fri, 22 Mar 2024 00:31:01 +0100 Subject: [PATCH 06/54] Clean up ci --- .github/workflows/rust-ci.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 5ae70dd..988b8f8 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -50,13 +50,6 @@ jobs: - uses: dtolnay/rust-toolchain@stable - name: cargo test --lib run: cargo test --lib --locked - integration-test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - name: cargo test --test '*' - run: cargo test --test '*' --locked doc-test: runs-on: ubuntu-latest steps: From 00abf58871571febf08a7fd34ddffbb3bb35cb2f Mon Sep 17 00:00:00 2001 From: quambene Date: Sat, 23 Mar 2024 00:11:49 +0100 Subject: [PATCH 07/54] Update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1493e8c..c08b7cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ +### Unreleased + +- changed + - Refactor integration tests + - Update to rust 1.77 + ### v0.4.0 (2022-12-24) - added From 9464588a7986a1d4a858073e7825d37796163c72 Mon Sep 17 00:00:00 2001 From: quambene Date: Sat, 23 Mar 2024 00:22:09 +0100 Subject: [PATCH 08/54] Update rust --- rust-toolchain.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 4d2d28e..5887fd4 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.74" +channel = "1.77" profile = "default" \ No newline at end of file From 2d628e37faef658ec0bb612303fac8c2f7b9cf6c Mon Sep 17 00:00:00 2001 From: quambene Date: Sat, 23 Mar 2024 00:54:37 +0100 Subject: [PATCH 09/54] Restructure tests --- Cargo.toml | 4 ++++ README.md | 4 ++++ tests/cmd/lib.rs | 1 + tests/cmd/test_connect.rs | 22 ++++++++++++++++++++++ tests/{ => cmd}/test_init.rs | 0 tests/{ => cmd}/test_query.rs | 0 tests/{ => cmd}/test_read.rs | 0 tests/{ => cmd}/test_send.rs | 0 tests/{ => cmd}/test_send_bulk.rs | 0 tests/test_connect.rs | 0 10 files changed, 31 insertions(+) create mode 100644 tests/cmd/lib.rs create mode 100644 tests/cmd/test_connect.rs rename tests/{ => cmd}/test_init.rs (100%) rename tests/{ => cmd}/test_query.rs (100%) rename tests/{ => cmd}/test_read.rs (100%) rename tests/{ => cmd}/test_send.rs (100%) rename tests/{ => cmd}/test_send_bulk.rs (100%) delete mode 100644 tests/test_connect.rs diff --git a/Cargo.toml b/Cargo.toml index 002152b..1e90362 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,10 @@ license = "Apache-2.0" name = "pigeon" path = "src/main.rs" +[[test]] +name = "integration_tests" +path = "tests/cmd/lib.rs" + [dependencies] anyhow = "1.0.40" rusoto_ses = { version = "0.47.0", default-features = false, features = ["rustls"] } diff --git a/README.md b/README.md index b4e25a3..f2fc7d3 100644 --- a/README.md +++ b/README.md @@ -443,6 +443,10 @@ Sendgrid | equals monthly limit Integration tests require a locally running database, and an AWS SES account. Specify the following environment variables: +- SMTP + - `SMTP_SERVER` + - `SMTP_USERNAME` + - `SMTP_PASSWORD` - AWS SES - `AWS_ACCESS_KEY_ID` - `AWS_SECRET_ACCESS_KEY` diff --git a/tests/cmd/lib.rs b/tests/cmd/lib.rs new file mode 100644 index 0000000..7278c35 --- /dev/null +++ b/tests/cmd/lib.rs @@ -0,0 +1 @@ +mod test_connect; diff --git a/tests/cmd/test_connect.rs b/tests/cmd/test_connect.rs new file mode 100644 index 0000000..3df24c6 --- /dev/null +++ b/tests/cmd/test_connect.rs @@ -0,0 +1,22 @@ +use assert_cmd::Command; +use predicates::{boolean::PredicateBooleanExt, str}; + +#[test] +fn test_connect_smtp() { + println!("Execute 'pigeon connect smtp'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args(["connect", "smtp"]); + cmd.assert() + .success() + .stdout(str::contains("Connecting to SMTP server").and(str::contains("ok"))); +} + +#[test] +fn test_connect_aws() { + println!("Execute 'pigeon connect aws'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args(["connect", "aws"]); + cmd.assert() + .success() + .stdout(str::contains("Connecting to aws server in region").and(str::contains("ok"))); +} diff --git a/tests/test_init.rs b/tests/cmd/test_init.rs similarity index 100% rename from tests/test_init.rs rename to tests/cmd/test_init.rs diff --git a/tests/test_query.rs b/tests/cmd/test_query.rs similarity index 100% rename from tests/test_query.rs rename to tests/cmd/test_query.rs diff --git a/tests/test_read.rs b/tests/cmd/test_read.rs similarity index 100% rename from tests/test_read.rs rename to tests/cmd/test_read.rs diff --git a/tests/test_send.rs b/tests/cmd/test_send.rs similarity index 100% rename from tests/test_send.rs rename to tests/cmd/test_send.rs diff --git a/tests/test_send_bulk.rs b/tests/cmd/test_send_bulk.rs similarity index 100% rename from tests/test_send_bulk.rs rename to tests/cmd/test_send_bulk.rs diff --git a/tests/test_connect.rs b/tests/test_connect.rs deleted file mode 100644 index e69de29..0000000 From a89ec55431d0fa560daccbab475941633d334306 Mon Sep 17 00:00:00 2001 From: quambene Date: Sat, 23 Mar 2024 01:46:37 +0100 Subject: [PATCH 10/54] Refactor test --- src/data_sources/csv.rs | 8 +--- tests/cmd/lib.rs | 1 + tests/cmd/test_query.rs | 91 +++++++++++++++++++++++++---------------- 3 files changed, 59 insertions(+), 41 deletions(-) diff --git a/src/data_sources/csv.rs b/src/data_sources/csv.rs index 5e36497..1e531ed 100644 --- a/src/data_sources/csv.rs +++ b/src/data_sources/csv.rs @@ -2,11 +2,7 @@ use crate::arg; use anyhow::{anyhow, Context}; use clap::ArgMatches; use polars::prelude::{CsvReader, CsvWriter, DataFrame, SerReader, SerWriter}; -use std::{ - fs, - path::{Path, PathBuf}, - time::SystemTime, -}; +use std::{fs, path::Path, time::SystemTime}; pub fn read_csv(csv_file: &Path) -> Result { println!("Reading csv file '{}' ...", csv_file.display()); @@ -21,7 +17,7 @@ pub fn write_csv(matches: &ArgMatches, mut df: DataFrame) -> Result<(), anyhow:: let current_time = now_utc.to_rfc3339_opts(chrono::SecondsFormat::Secs, true); let target_dir = match matches.value_of(arg::SAVE_DIR) { - Some(save_dir) => PathBuf::from(save_dir), + Some(save_dir) => Path::new(save_dir), None => return Err(anyhow!("Missing value for argument '{}'", arg::SAVE_DIR)), }; let target_file = format!("query_{}.csv", ¤t_time); diff --git a/tests/cmd/lib.rs b/tests/cmd/lib.rs index 7278c35..ab93e7a 100644 --- a/tests/cmd/lib.rs +++ b/tests/cmd/lib.rs @@ -1 +1,2 @@ mod test_connect; +mod test_query; diff --git a/tests/cmd/test_query.rs b/tests/cmd/test_query.rs index ee19e34..6502fb2 100644 --- a/tests/cmd/test_query.rs +++ b/tests/cmd/test_query.rs @@ -1,10 +1,10 @@ use assert_cmd::Command; -use pigeon_rs::{app, cmd}; use predicates::str; -use std::env; +use std::{env, fs}; +use tempfile::tempdir; #[test] -fn test_display_query() { +fn test_query_display() { let test_query = env::var("TEST_QUERY").expect("Missing environment variable 'TEST_QUERY'"); println!("Execute 'pigeon query {test_query} --display'"); let mut cmd = Command::cargo_bin("pigeon").unwrap(); @@ -15,49 +15,70 @@ fn test_display_query() { } #[test] -#[ignore] -fn test_save_query() { +fn test_query_save() { let test_query = env::var("TEST_QUERY").expect("Missing environment variable 'TEST_QUERY'"); - let args = vec![ - cmd::BIN, - cmd::QUERY, - test_query.as_str(), - "--display", - "--save", - ]; + let temp_dir = tempdir().unwrap(); + let temp_path = temp_dir.path(); + assert!(temp_path.exists(), "Missing path: {}", temp_path.display()); + let save_dir = temp_path.to_str().unwrap(); - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::QUERY).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); + println!("Execute 'pigeon query {test_query} --save'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.current_dir(save_dir); + cmd.args(["query", test_query.as_str(), "--save"]); + cmd.assert() + .success() + .stdout(str::contains("Save query result to file")); - let res = cmd::query(subcommand_matches); - println!("res: {:#?}", res); + if let Ok(mut entries) = fs::read_dir(temp_path.join("saved_queries")) { + let dir_entry = entries.find_map(|entry| { + if let Ok(entry) = entry { + if entry.file_name().to_str().is_some_and(|file_name| { + file_name.contains("query") && file_name.ends_with(".csv") + }) { + return Some(entry); + } + } - assert!(res.is_ok()) + None + }); + assert!(dir_entry.is_some()); + } } #[test] -#[ignore] -fn test_save_dir() { +fn test_query_save_dir() { let test_query = env::var("TEST_QUERY").expect("Missing environment variable 'TEST_QUERY'"); - let args = vec![ - cmd::BIN, - cmd::QUERY, + let temp_dir = tempdir().unwrap(); + let temp_path = temp_dir.path(); + assert!(temp_path.exists(), "Missing path: {}", temp_path.display()); + let save_dir = temp_path.to_str().unwrap(); + + println!("Execute 'pigeon query {test_query} --save --save-dir {save_dir}'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "query", test_query.as_str(), - "--display", "--save", "--save-dir", - "./my-saved-queries", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::QUERY).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); + save_dir, + ]); + cmd.assert() + .success() + .stdout(str::contains("Save query result to file")); - let res = cmd::query(subcommand_matches); - println!("res: {:#?}", res); + if let Ok(mut entries) = fs::read_dir(temp_path) { + let dir_entry = entries.find_map(|entry| { + if let Ok(entry) = entry { + if entry.file_name().to_str().is_some_and(|file_name| { + file_name.contains("query") && file_name.ends_with(".csv") + }) { + return Some(entry); + } + } - assert!(res.is_ok()) + None + }); + assert!(dir_entry.is_some()); + } } From cb5cbf38849a0aa9b148c1574db68346a5a91b2f Mon Sep 17 00:00:00 2001 From: quambene Date: Sat, 23 Mar 2024 01:47:03 +0100 Subject: [PATCH 11/54] Add tempfile and disable lib test --- Cargo.lock | 16 ++++++++-------- Cargo.toml | 4 ++++ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 90bf289..c6241f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1352,9 +1352,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.151" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libm" @@ -1735,6 +1735,7 @@ dependencies = [ "rusoto_ses", "serde", "serde_yaml", + "tempfile", "tokio", "url", "uuid 0.8.2", @@ -2539,9 +2540,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.28" +version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ "bitflags 2.4.1", "errno", @@ -3011,15 +3012,14 @@ checksum = "cfb5fa503293557c5158bd215fdc225695e567a77e453f5d4452a50a193969bd" [[package]] name = "tempfile" -version = "3.8.1" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand 2.0.1", - "redox_syscall", "rustix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1e90362..cd4c6db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,9 @@ license = "Apache-2.0" name = "pigeon" path = "src/main.rs" +[lib] +test = false + [[test]] name = "integration_tests" path = "tests/cmd/lib.rs" @@ -45,3 +48,4 @@ base64 = "0.13.0" [dev-dependencies] assert_cmd = "2.0.14" predicates = "3.1.0" +tempfile = "3.10.1" From 7d32c1bd85947fbac74f5f81d88bb1242673d5cc Mon Sep 17 00:00:00 2001 From: quambene Date: Sun, 24 Mar 2024 00:47:21 +0100 Subject: [PATCH 12/54] Add status and version to readme --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index f2fc7d3..bd1ed62 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ # Pigeon +[![Latest Version](https://img.shields.io/crates/v/bogrep.svg)](https://crates.io/crates/bogrep) +[![Build Status](https://github.com/quambene/bogrep/actions/workflows/rust-ci.yml/badge.svg)](https://github.com/quambene/bogrep/actions/workflows/rust-ci.yml) + Pigeon is a command line tool for automating your email workflow in a cheap and efficient way. Utilize your most efficient dev tools you are already familiar with. For example, query the subscribers of your newsletter, create a plaintext and html email from a template file, and send it to all of them: From 8009f06dd1eed07831dc9e3cb4bc2b6215e37940 Mon Sep 17 00:00:00 2001 From: quambene Date: Sun, 24 Mar 2024 00:58:52 +0100 Subject: [PATCH 13/54] Rewrite test read --- tests/cmd/lib.rs | 1 + tests/cmd/test_read.rs | 40 +++++++++++++++++++++++++++++----------- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/tests/cmd/lib.rs b/tests/cmd/lib.rs index ab93e7a..7eba88f 100644 --- a/tests/cmd/lib.rs +++ b/tests/cmd/lib.rs @@ -1,2 +1,3 @@ mod test_connect; mod test_query; +mod test_read; diff --git a/tests/cmd/test_read.rs b/tests/cmd/test_read.rs index af4b4f8..f2656ce 100644 --- a/tests/cmd/test_read.rs +++ b/tests/cmd/test_read.rs @@ -1,16 +1,34 @@ -use pigeon_rs::{app, cmd}; +use assert_cmd::Command; +use predicates::{boolean::PredicateBooleanExt, str}; #[test] fn test_read() { - let args = vec![cmd::BIN, cmd::READ, "./test_data/receiver.csv"]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::READ).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = cmd::read(subcommand_matches); - println!("res: {:#?}", res); + let test_data = "./test_data/receiver.csv"; + println!("Execute 'pigeon read {test_data}'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args(["read", test_data]); + cmd.assert().success().stdout(str::contains( + "Reading csv file './test_data/receiver.csv' ...", + )); +} - assert!(res.is_ok()) +#[test] +fn test_read_display() { + let test_data = "./test_data/receiver.csv"; + println!("Execute 'pigeon read {test_data} --display'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args(["read", test_data, "--display"]); + cmd.assert().success().stdout( + str::contains("Reading csv file './test_data/receiver.csv' ...").and(str::contains( + "Display csv file: shape: (2, 3) +┌────────────┬──────────────┬────────────────────────────┐ +│ first_name ┆ last_name ┆ email │ +│ --- ┆ --- ┆ --- │ +│ str ┆ str ┆ str │ +╞════════════╪══════════════╪════════════════════════════╡ +│ Marie ┆ Curie ┆ marie@curie.com │ +│ Alexandre ┆ Grothendieck ┆ alexandre@grothendieck.com │ +└────────────┴──────────────┴────────────────────────────┘", + )), + ); } From 961ff3b090ce9d347bc4f18f16898cab3dfed66f Mon Sep 17 00:00:00 2001 From: quambene Date: Sun, 24 Mar 2024 03:08:47 +0100 Subject: [PATCH 14/54] Refactor test --- src/email_transmission/client.rs | 15 +- src/email_transmission/mock_client.rs | 16 ++ src/email_transmission/mod.rs | 15 +- src/email_transmission/smtp.rs | 2 +- tests/cmd/lib.rs | 1 + tests/cmd/test_connect.rs | 6 + tests/cmd/test_send_bulk.rs | 230 +++++++++----------------- 7 files changed, 117 insertions(+), 168 deletions(-) create mode 100644 src/email_transmission/mock_client.rs diff --git a/src/email_transmission/client.rs b/src/email_transmission/client.rs index 72a22a8..6f3ce08 100644 --- a/src/email_transmission/client.rs +++ b/src/email_transmission/client.rs @@ -1,24 +1,19 @@ -use super::{SentEmail, SmtpClient}; +use super::{MockClient, SendEmail, SmtpClient}; use crate::{ arg::{self, val}, - email_builder::Email, email_provider::AwsSesClient, }; use anyhow::anyhow; use clap::ArgMatches; -pub trait SendEmail<'a> { - fn send( - &self, - matches: &ArgMatches, - email: &'a Email<'a>, - ) -> Result, anyhow::Error>; -} - pub struct Client; impl Client { pub fn init<'a>(matches: &ArgMatches) -> Result>, anyhow::Error> { + if matches.is_present(arg::DRY_RUN) { + return Ok(Box::new(MockClient)); + } + if matches.is_present(arg::CONNECTION) { match matches.value_of(arg::CONNECTION) { Some(connection) => match connection.to_lowercase().as_str() { diff --git a/src/email_transmission/mock_client.rs b/src/email_transmission/mock_client.rs new file mode 100644 index 0000000..c851428 --- /dev/null +++ b/src/email_transmission/mock_client.rs @@ -0,0 +1,16 @@ +use super::{SendEmail, SentEmail, Status}; +use crate::email_builder::Email; +use clap::ArgMatches; + +pub struct MockClient; + +impl<'a> SendEmail<'a> for MockClient { + fn send( + &self, + _matches: &ArgMatches, + email: &'a Email<'a>, + ) -> Result, anyhow::Error> { + let email = SentEmail::new(email, Status::DryRun); + Ok(email) + } +} diff --git a/src/email_transmission/mod.rs b/src/email_transmission/mod.rs index f65de05..80fa060 100644 --- a/src/email_transmission/mod.rs +++ b/src/email_transmission/mod.rs @@ -1,9 +1,22 @@ mod client; +mod mock_client; mod sent_email; mod smtp; mod status; -pub use client::{Client, SendEmail}; +use clap::ArgMatches; +pub use client::Client; +pub use mock_client::MockClient; pub use sent_email::SentEmail; pub use smtp::SmtpClient; pub use status::Status; + +use crate::email_builder::Email; + +pub trait SendEmail<'a> { + fn send( + &self, + matches: &ArgMatches, + email: &'a Email<'a>, + ) -> Result, anyhow::Error>; +} diff --git a/src/email_transmission/smtp.rs b/src/email_transmission/smtp.rs index d475924..e458b83 100644 --- a/src/email_transmission/smtp.rs +++ b/src/email_transmission/smtp.rs @@ -1,4 +1,4 @@ -use super::{client::SendEmail, SentEmail, Status}; +use super::{SendEmail, SentEmail, Status}; use crate::{ arg, email_builder::Email, diff --git a/tests/cmd/lib.rs b/tests/cmd/lib.rs index 7eba88f..e9e7df2 100644 --- a/tests/cmd/lib.rs +++ b/tests/cmd/lib.rs @@ -1,3 +1,4 @@ mod test_connect; mod test_query; mod test_read; +mod test_send_bulk; diff --git a/tests/cmd/test_connect.rs b/tests/cmd/test_connect.rs index 3df24c6..1d3928f 100644 --- a/tests/cmd/test_connect.rs +++ b/tests/cmd/test_connect.rs @@ -1,7 +1,10 @@ use assert_cmd::Command; use predicates::{boolean::PredicateBooleanExt, str}; +/// This test requires environment variables `SMTP_SERVER`, `SMTP_USERNAME`, and +/// `SMTP_PASSWORD`. #[test] +#[ignore] fn test_connect_smtp() { println!("Execute 'pigeon connect smtp'"); let mut cmd = Command::cargo_bin("pigeon").unwrap(); @@ -11,7 +14,10 @@ fn test_connect_smtp() { .stdout(str::contains("Connecting to SMTP server").and(str::contains("ok"))); } +/// This test requires environment variables `AWS_ACCESS_KEY_ID`, +/// `AWS_SECRET_ACCESS_KEY`, and `AWS_REGION`. #[test] +#[ignore] fn test_connect_aws() { println!("Execute 'pigeon connect aws'"); let mut cmd = Command::cargo_bin("pigeon").unwrap(); diff --git a/tests/cmd/test_send_bulk.rs b/tests/cmd/test_send_bulk.rs index 3b79cc8..bb0e41d 100644 --- a/tests/cmd/test_send_bulk.rs +++ b/tests/cmd/test_send_bulk.rs @@ -1,11 +1,13 @@ -use pigeon_rs::{app, cmd}; +use assert_cmd::Command; +use predicates::{boolean::PredicateBooleanExt, str}; #[test] #[ignore] fn test_send_bulk_subject_content_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND_BULK, + println!("Execute 'pigeon send-bulk'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send-bulk", "albert@einstein.com", "--receiver-file", "./test_data/receiver.csv", @@ -16,25 +18,23 @@ fn test_send_bulk_subject_content_dry() { "--dry-run", "--display", "--assume-yes", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = cmd::send_bulk(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) + ]); + cmd.assert().success().stdout( + str::contains("Reading csv file './test_data/receiver.csv' ...").and( + str::contains("Display csv file:") + .and(str::contains("Display emails:")) + .and(str::contains("Dry run: \u{1b}[32mactivated\u{1b}[0m")), + ), + ); } #[test] #[ignore] fn test_send_bulk_text_file_html_file_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND_BULK, + println!("Execute 'pigeon send-bulk'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send-bulk", "albert@einstein.com", "--receiver-file", "./test_data/receiver.csv", @@ -47,25 +47,17 @@ fn test_send_bulk_text_file_html_file_dry() { "--dry-run", "--display", "--assume-yes", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = cmd::send_bulk(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) + ]); + cmd.assert().success(); } #[test] #[ignore] fn test_send_bulk_message_file_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND_BULK, + println!("Execute 'pigeon send-bulk'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send-bulk", "albert@einstein.com", "--receiver-file", "./test_data/receiver.csv", @@ -74,25 +66,17 @@ fn test_send_bulk_message_file_dry() { "--dry-run", "--display", "--assume-yes", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = cmd::send_bulk(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) + ]); + cmd.assert().success(); } #[test] #[ignore] fn test_send_bulk_receiver_column_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND_BULK, + println!("Execute 'pigeon send-bulk'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send-bulk", "albert@einstein.com", "--receiver-file", "./test_data/contacts.csv", @@ -103,25 +87,17 @@ fn test_send_bulk_receiver_column_dry() { "--dry-run", "--display", "--assume-yes", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = cmd::send_bulk(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) + ]); + cmd.assert().success(); } #[test] #[ignore] fn test_send_bulk_personalize_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND_BULK, + println!("Execute 'pigeon send-bulk'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send-bulk", "albert@einstein.com", "--receiver-file", "./test_data/receiver.csv", @@ -133,25 +109,17 @@ fn test_send_bulk_personalize_dry() { "--dry-run", "--display", "--assume-yes", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = cmd::send_bulk(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) + ]); + cmd.assert().success(); } #[test] #[ignore] fn test_archive_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND_BULK, + println!("Execute 'pigeon send-bulk'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send-bulk", "albert@einstein.com", "--receiver-file", "./test_data/receiver.csv", @@ -161,25 +129,17 @@ fn test_archive_dry() { "--display", "--assume-yes", "--archive", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = cmd::send_bulk(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) + ]); + cmd.assert().success(); } #[test] #[ignore] fn test_archive_dir_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND_BULK, + println!("Execute 'pigeon send-bulk'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send-bulk", "albert@einstein.com", "--receiver-file", "./test_data/receiver.csv", @@ -191,25 +151,17 @@ fn test_archive_dir_dry() { "--archive", "--archive-dir", "./my-sent-emails", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = cmd::send_bulk(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) + ]); + cmd.assert().success(); } #[test] #[ignore] fn test_attachment_pdf_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND_BULK, + println!("Execute 'pigeon send-bulk'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send-bulk", "albert@einstein.com", "--receiver-file", "./test_data/receiver.csv", @@ -221,25 +173,17 @@ fn test_attachment_pdf_dry() { "--archive", "--attachment", "./test_data/test.pdf", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = cmd::send_bulk(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) + ]); + cmd.assert().success(); } #[test] #[ignore] fn test_attachment_png_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND_BULK, + println!("Execute 'pigeon send-bulk'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send-bulk", "albert@einstein.com", "--receiver-file", "./test_data/receiver.csv", @@ -251,25 +195,17 @@ fn test_attachment_png_dry() { "--archive", "--attachment", "./test_data/test.png", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = cmd::send_bulk(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) + ]); + cmd.assert().success(); } #[test] #[ignore] fn test_attachment_odt_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND_BULK, + println!("Execute 'pigeon send-bulk'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send-bulk", "albert@einstein.com", "--receiver-file", "./test_data/receiver.csv", @@ -281,25 +217,17 @@ fn test_attachment_odt_dry() { "--archive", "--attachment", "./test_data/test.odt", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = cmd::send_bulk(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) + ]); + cmd.assert().success(); } #[test] #[ignore] fn test_send_bulk_aws_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND_BULK, + println!("Execute 'pigeon send-bulk'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send-bulk", "albert@einstein.com", "--receiver-file", "./test_data/receiver.csv", @@ -309,16 +237,6 @@ fn test_send_bulk_aws_dry() { "--display", "--assume-yes", "--connection", - "aws", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND_BULK).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = cmd::send_bulk(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) + ]); + cmd.assert().success(); } From b6a7aee4d0a43f04103905c93df95c37b43434a6 Mon Sep 17 00:00:00 2001 From: quambene Date: Sun, 24 Mar 2024 23:44:49 +0100 Subject: [PATCH 15/54] Refactor tests --- src/cmd/send.rs | 633 ------------------------------------ tests/cmd/lib.rs | 1 + tests/cmd/test_send.rs | 461 ++++++++++++++++++++++++++ tests/cmd/test_send_bulk.rs | 1 - 4 files changed, 462 insertions(+), 634 deletions(-) diff --git a/src/cmd/send.rs b/src/cmd/send.rs index bc22c36..08eb1bc 100644 --- a/src/cmd/send.rs +++ b/src/cmd/send.rs @@ -134,636 +134,3 @@ pub fn send(matches: &ArgMatches) -> Result<(), anyhow::Error> { Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - use crate::{app, cmd}; - use std::env; - - #[test] - #[ignore] - fn test_send_subject_content_smtp_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND, - "albert@einstein.com", - "marie@curie.com", - "--subject", - "Test Subject", - "--content", - "This is a test message (plaintext).", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_send_subject_content_smtp() { - let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); - let receiver = - env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); - - let args = vec![ - cmd::BIN, - cmd::SEND, - &sender, - &receiver, - "--subject", - "Test Subject", - "--content", - "This is a test message (plaintext).", - "--display", - "--assume-yes", - "--archive", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_send_message_file_smtp_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND, - "albert@einstein.com", - "marie@curie.com", - "--message-file", - "./test_data/message.yaml", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_send_message_file_smtp() { - let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); - let receiver = - env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); - - let args = vec![ - cmd::BIN, - cmd::SEND, - &sender, - &receiver, - "--message-file", - "./test_data/message.yaml", - "--display", - "--assume-yes", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_send_message_file_aws_api() { - let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); - let receiver = - env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); - - let args = vec![ - cmd::BIN, - cmd::SEND, - &sender, - &receiver, - "--connection", - val::AWS, - "--message-file", - "./test_data/message.yaml", - "--display", - "--assume-yes", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_send_message_file_empty_smtp_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND, - "albert@einstein.com", - "marie@curie.com", - "--message-file", - "./test_data/empty_message.yaml", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_send_message_file_none_html_smtp_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND, - "albert@einstein.com", - "marie@curie.com", - "--message-file", - "./test_data/none_html_message.yaml", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_send_message_file_content_none_smtp_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND, - "albert@einstein.com", - "marie@curie.com", - "--message-file", - "./test_data/content_none_message.yaml", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_err()) - } - - #[test] - #[ignore] - fn test_archive_smtp_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND, - "albert@einstein.com", - "marie@curie.com", - "--message-file", - "./test_data/message.yaml", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_archive_dir_smtp_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND, - "albert@einstein.com", - "marie@curie.com", - "--message-file", - "./test_data/message.yaml", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - "--archive-dir", - "./my-sent-emails", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_attachment_pdf_smtp_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND, - "albert@einstein.com", - "marie@curie.com", - "--message-file", - "./test_data/message.yaml", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - "--attachment", - "./test_data/test.pdf", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_attachment_png_smtp_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND, - "albert@einstein.com", - "marie@curie.com", - "--message-file", - "./test_data/message.yaml", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - "--attachment", - "./test_data/test.png", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_attachment_odt_smtp_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND, - "albert@einstein.com", - "marie@curie.com", - "--message-file", - "./test_data/message.yaml", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - "--attachment", - "./test_data/test.odt", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_attachment_pdf_smtp() { - let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); - let receiver = - env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); - - let args = vec![ - cmd::BIN, - cmd::SEND, - &sender, - &receiver, - "--message-file", - "./test_data/message.yaml", - "--display", - "--assume-yes", - "--archive", - "--attachment", - "./test_data/test.pdf", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_attachment_pdf_aws_api() { - let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); - let receiver = - env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); - - let args = vec![ - cmd::BIN, - cmd::SEND, - &sender, - &receiver, - "--connection", - val::AWS, - "--message-file", - "./test_data/message.yaml", - "--display", - "--assume-yes", - "--archive", - "--attachment", - "./test_data/test.pdf", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_send_aws_api_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND, - "albert@einstein.com", - "marie@curie.com", - "--subject", - "Test Subject", - "--content", - "This is a test message (plaintext).", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - "--connection", - val::AWS, - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_send_aws_api() { - let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); - let receiver = - env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); - - let args = vec![ - cmd::BIN, - cmd::SEND, - &sender, - &receiver, - "--subject", - "Test Subject", - "--content", - "This is a test message (plaintext).", - "--display", - "--assume-yes", - "--archive", - "--connection", - val::AWS, - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_send_text_file_smtp_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND, - "albert@einstein.com", - "marie@curie.com", - "--subject", - "Test Subject", - "--text-file", - "./test_data/message.txt", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_send_html_file_smtp_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND, - "albert@einstein.com", - "marie@curie.com", - "--subject", - "Test Subject", - "--html-file", - "./test_data/message.html", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_send_text_file_html_file_smtp_dry() { - let args = vec![ - cmd::BIN, - cmd::SEND, - "albert@einstein.com", - "marie@curie.com", - "--subject", - "Test Subject", - "--text-file", - "./test_data/message.txt", - "--html-file", - "./test_data/message.html", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } - - #[test] - #[ignore] - fn test_send_text_file_html_file_smtp() { - let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); - let receiver = - env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); - - let args = vec![ - cmd::BIN, - cmd::SEND, - &sender, - &receiver, - "--subject", - "Test Subject", - "--text-file", - "./test_data/message.txt", - "--html-file", - "./test_data/message.html", - "--display", - "--assume-yes", - "--archive", - ]; - - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches(cmd::SEND).unwrap(); - println!("subcommand matches: {:#?}", subcommand_matches); - - let res = send(subcommand_matches); - println!("res: {:#?}", res); - - assert!(res.is_ok()) - } -} diff --git a/tests/cmd/lib.rs b/tests/cmd/lib.rs index e9e7df2..d19c368 100644 --- a/tests/cmd/lib.rs +++ b/tests/cmd/lib.rs @@ -1,4 +1,5 @@ mod test_connect; mod test_query; mod test_read; +mod test_send; mod test_send_bulk; diff --git a/tests/cmd/test_send.rs b/tests/cmd/test_send.rs index e69de29..b884b61 100644 --- a/tests/cmd/test_send.rs +++ b/tests/cmd/test_send.rs @@ -0,0 +1,461 @@ +use std::env; + +use assert_cmd::Command; +use predicates::str; + +#[test] +fn test_send_dry() { + println!("Execute 'pigeon send'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send", + "albert@einstein.com", + "receiver@gmail.com", + "--subject", + "Test subject", + "--content", + "This is a test email.", + "--dry-run", + ]); + cmd.assert().success().stdout(str::contains("abc")); +} + +#[test] +fn test_send_subject_content_smtp_dry() { + println!("Execute 'pigeon send'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send", + "albert@einstein.com", + "marie@curie.com", + "--subject", + "Test Subject", + "--content", + "This is a test message (plaintext).", + "--dry-run", + "--display", + "--assume-yes", + "--archive", + ]); + cmd.assert().success().stdout(str::contains("abc")); +} + +#[test] +#[ignore] +fn test_send_subject_content_smtp() { + println!("Execute 'pigeon send'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send", + "--subject", + "Test Subject", + "--content", + "This is a test message (plaintext).", + "--display", + "--assume-yes", + "--archive", + ]); + cmd.assert().success().stdout(str::contains("abc")); +} + +#[test] +fn test_send_message_file_smtp_dry() { + println!("Execute 'pigeon send'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send", + "albert@einstein.com", + "marie@curie.com", + "--message-file", + "./test_data/message.yaml", + "--dry-run", + "--display", + "--assume-yes", + "--archive", + ]); + cmd.assert().success().stdout(str::contains("abc")); +} + +#[test] +#[ignore] +fn test_send_message_file_smtp() { + let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); + let receiver = env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); + + println!("Execute 'pigeon send'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send", + &sender, + &receiver, + "--message-file", + "./test_data/message.yaml", + "--display", + "--assume-yes", + ]); + cmd.assert().success().stdout(str::contains("abc")); +} + +#[test] +#[ignore] +fn test_send_message_file_aws_api() { + let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); + let receiver = env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); + + println!("Execute 'pigeon send'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send", + &sender, + &receiver, + "--connection", + "aws", + "--message-file", + "./test_data/message.yaml", + "--display", + "--assume-yes", + ]); + cmd.assert().success().stdout(str::contains("abc")); +} + +#[test] +fn test_send_message_file_empty_smtp_dry() { + println!("Execute 'pigeon send'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send", + "albert@einstein.com", + "marie@curie.com", + "--message-file", + "./test_data/empty_message.yaml", + "--dry-run", + "--display", + "--assume-yes", + "--archive", + ]); + cmd.assert().success().stdout(str::contains("abc")); +} + +#[test] +#[ignore] +fn test_send_message_file_none_html_smtp_dry() { + println!("Execute 'pigeon send'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send", + "albert@einstein.com", + "marie@curie.com", + "--message-file", + "./test_data/none_html_message.yaml", + "--dry-run", + "--display", + "--assume-yes", + "--archive", + ]); + cmd.assert().success().stdout(str::contains("abc")); +} + +#[test] +fn test_send_message_file_content_none_smtp_dry() { + println!("Execute 'pigeon send'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send", + "albert@einstein.com", + "marie@curie.com", + "--message-file", + "./test_data/content_none_message.yaml", + "--dry-run", + "--display", + "--assume-yes", + "--archive", + ]); + cmd.assert().success().stdout(str::contains("abc")); +} + +#[test] +fn test_archive_smtp_dry() { + println!("Execute 'pigeon send'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send", + "albert@einstein.com", + "marie@curie.com", + "--message-file", + "./test_data/message.yaml", + "--dry-run", + "--display", + "--assume-yes", + "--archive", + ]); + cmd.assert().success().stdout(str::contains("abc")); +} + +#[test] +fn test_archive_dir_smtp_dry() { + println!("Execute 'pigeon send'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send", + "albert@einstein.com", + "marie@curie.com", + "--message-file", + "./test_data/message.yaml", + "--dry-run", + "--display", + "--assume-yes", + "--archive", + "--archive-dir", + "./my-sent-emails", + ]); + cmd.assert().success().stdout(str::contains("abc")); +} + +#[test] +#[ignore] +fn test_attachment_pdf_smtp_dry() { + println!("Execute 'pigeon send'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send", + "albert@einstein.com", + "marie@curie.com", + "--message-file", + "./test_data/message.yaml", + "--dry-run", + "--display", + "--assume-yes", + "--archive", + "--attachment", + "./test_data/test.pdf", + ]); + cmd.assert().success().stdout(str::contains("abc")); +} + +#[test] +#[ignore] +fn test_attachment_png_smtp_dry() { + println!("Execute 'pigeon send'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send", + "albert@einstein.com", + "marie@curie.com", + "--message-file", + "./test_data/message.yaml", + "--dry-run", + "--display", + "--assume-yes", + "--archive", + "--attachment", + "./test_data/test.png", + ]); + cmd.assert().success().stdout(str::contains("abc")); +} + +#[test] +#[ignore] +fn test_attachment_odt_smtp_dry() { + println!("Execute 'pigeon send'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send", + "albert@einstein.com", + "marie@curie.com", + "--message-file", + "./test_data/message.yaml", + "--dry-run", + "--display", + "--assume-yes", + "--archive", + "--attachment", + "./test_data/test.odt", + ]); + cmd.assert().success().stdout(str::contains("abc")); +} + +#[test] +#[ignore] +fn test_attachment_pdf_smtp() { + let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); + let receiver = env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); + + println!("Execute 'pigeon send'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send", + &sender, + &receiver, + "--message-file", + "./test_data/message.yaml", + "--display", + "--assume-yes", + "--archive", + "--attachment", + "./test_data/test.pdf", + ]); + cmd.assert().success().stdout(str::contains("abc")); +} + +#[test] +#[ignore] +fn test_attachment_pdf_aws_api() { + let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); + let receiver = env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); + + println!("Execute 'pigeon send'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send", + &sender, + &receiver, + "--connection", + "aws", + "--message-file", + "./test_data/message.yaml", + "--display", + "--assume-yes", + "--archive", + "--attachment", + "./test_data/test.pdf", + ]); + cmd.assert().success().stdout(str::contains("abc")); +} + +#[test] +#[ignore] +fn test_send_aws_api_dry() { + println!("Execute 'pigeon send'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send", + "albert@einstein.com", + "marie@curie.com", + "--subject", + "Test Subject", + "--content", + "This is a test message (plaintext).", + "--dry-run", + "--display", + "--assume-yes", + "--archive", + "--connection", + "aws", + ]); + cmd.assert().success().stdout(str::contains("abc")); +} + +#[test] +#[ignore] +fn test_send_aws_api() { + let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); + let receiver = env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); + + println!("Execute 'pigeon send'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send", + &sender, + &receiver, + "--subject", + "Test Subject", + "--content", + "This is a test message (plaintext).", + "--display", + "--assume-yes", + "--archive", + "--connection", + "aws", + ]); + cmd.assert().success().stdout(str::contains("abc")); +} + +#[test] +#[ignore] +fn test_send_text_file_smtp_dry() { + println!("Execute 'pigeon send'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send", + "albert@einstein.com", + "marie@curie.com", + "--subject", + "Test Subject", + "--text-file", + "./test_data/message.txt", + "--dry-run", + "--display", + "--assume-yes", + "--archive", + ]); + cmd.assert().success().stdout(str::contains("abc")); +} + +#[test] +#[ignore] +fn test_send_html_file_smtp_dry() { + println!("Execute 'pigeon send'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send", + "albert@einstein.com", + "marie@curie.com", + "--subject", + "Test Subject", + "--html-file", + "./test_data/message.html", + "--dry-run", + "--display", + "--assume-yes", + "--archive", + ]); + cmd.assert().success().stdout(str::contains("abc")); +} + +#[test] +#[ignore] +fn test_send_text_file_html_file_smtp_dry() { + println!("Execute 'pigeon send'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send", + "albert@einstein.com", + "marie@curie.com", + "--subject", + "Test Subject", + "--text-file", + "./test_data/message.txt", + "--html-file", + "./test_data/message.html", + "--dry-run", + "--display", + "--assume-yes", + "--archive", + ]); + cmd.assert().success().stdout(str::contains("abc")); +} + +#[test] +#[ignore] +fn test_send_text_file_html_file_smtp() { + let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); + let receiver = env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); + + println!("Execute 'pigeon send'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.args([ + "send", + &sender, + &receiver, + "--subject", + "Test Subject", + "--text-file", + "./test_data/message.txt", + "--html-file", + "./test_data/message.html", + "--display", + "--assume-yes", + "--archive", + ]); + cmd.assert().success().stdout(str::contains("abc")); +} diff --git a/tests/cmd/test_send_bulk.rs b/tests/cmd/test_send_bulk.rs index bb0e41d..2726f0a 100644 --- a/tests/cmd/test_send_bulk.rs +++ b/tests/cmd/test_send_bulk.rs @@ -2,7 +2,6 @@ use assert_cmd::Command; use predicates::{boolean::PredicateBooleanExt, str}; #[test] -#[ignore] fn test_send_bulk_subject_content_dry() { println!("Execute 'pigeon send-bulk'"); let mut cmd = Command::cargo_bin("pigeon").unwrap(); From cf80af315556d654a3baf56cdc3596e2fcdb7e02 Mon Sep 17 00:00:00 2001 From: quambene Date: Sun, 24 Mar 2024 23:45:10 +0100 Subject: [PATCH 16/54] Fix clippy warnings --- src/data_sources/csv.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data_sources/csv.rs b/src/data_sources/csv.rs index 1e531ed..f635b5a 100644 --- a/src/data_sources/csv.rs +++ b/src/data_sources/csv.rs @@ -24,7 +24,7 @@ pub fn write_csv(matches: &ArgMatches, mut df: DataFrame) -> Result<(), anyhow:: match target_dir.exists() { true => (), - false => fs::create_dir(&target_dir).context(format!( + false => fs::create_dir(target_dir).context(format!( "Can't create directory: '{}'", target_dir.display() ))?, From 57f636c1141288f7e0c56ad738d06b9699de1ad5 Mon Sep 17 00:00:00 2001 From: quambene Date: Mon, 25 Mar 2024 00:12:35 +0100 Subject: [PATCH 17/54] Refactor args --- src/arg.rs | 2 +- src/cmd/connect.rs | 16 +-- src/cmd/init.rs | 9 +- src/cmd/mod.rs | 14 +- src/cmd/query.rs | 50 +------ src/cmd/read.rs | 16 +-- src/cmd/send.rs | 86 +----------- src/cmd/send_bulk.rs | 105 +-------------- src/cmd/simple_query.rs | 16 +-- src/lib.rs | 281 ++++++++++++++++++++++++++++++++++++++-- 10 files changed, 290 insertions(+), 305 deletions(-) diff --git a/src/arg.rs b/src/arg.rs index 1585a17..71b26b6 100644 --- a/src/arg.rs +++ b/src/arg.rs @@ -32,7 +32,7 @@ pub mod val { // default value for RECEIVER_COLUMN pub const EMAIL: &str = "email"; - // possible values for arg CONNECTION and subcommand CONNECT + // possible values for argument CONNECTION and subcommand CONNECT pub const SMTP: &str = "smtp"; pub const AWS: &str = "aws"; } diff --git a/src/cmd/connect.rs b/src/cmd/connect.rs index 3f1a0e9..9938bf5 100644 --- a/src/cmd/connect.rs +++ b/src/cmd/connect.rs @@ -5,21 +5,7 @@ use crate::{ email_transmission::SmtpClient, }; use anyhow::{anyhow, Result}; -use clap::{Arg, ArgMatches}; - -pub fn connect_args() -> [Arg<'static, 'static>; 2] { - [ - Arg::with_name(cmd::CONNECT) - .takes_value(true) - .possible_values(&[val::SMTP, val::AWS]) - .default_value(val::SMTP) - .help("Check connection to SMTP server."), - Arg::with_name(arg::VERBOSE) - .long(arg::VERBOSE) - .takes_value(false) - .help("Shows what is going on for subcommand"), - ] -} +use clap::ArgMatches; pub fn connect(matches: &ArgMatches) -> Result<(), anyhow::Error> { if matches.is_present(arg::VERBOSE) { diff --git a/src/cmd/init.rs b/src/cmd/init.rs index 5d29713..d68a0c8 100644 --- a/src/cmd/init.rs +++ b/src/cmd/init.rs @@ -1,12 +1,5 @@ use crate::{arg, email_builder::MessageTemplate}; -use clap::{Arg, ArgMatches}; - -pub fn init_args() -> [Arg<'static, 'static>; 1] { - [Arg::with_name(arg::VERBOSE) - .long(arg::VERBOSE) - .takes_value(false) - .help("Shows what is going on for subcommand")] -} +use clap::ArgMatches; pub fn init(matches: &ArgMatches) -> Result<(), anyhow::Error> { if matches.is_present(arg::VERBOSE) { diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 4c390bf..031f839 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -6,13 +6,13 @@ mod send; mod send_bulk; mod simple_query; -pub use connect::{connect, connect_args}; -pub use init::{init, init_args}; -pub use query::{query, query_args}; -pub use read::{read, read_args}; -pub use send::{send, send_args}; -pub use send_bulk::{send_bulk, send_bulk_args}; -pub use simple_query::{simple_query, simple_query_args}; +pub use connect::connect; +pub use init::init; +pub use query::query; +pub use read::read; +pub use send::send; +pub use send_bulk::send_bulk; +pub use simple_query::simple_query; // Binary name pub const BIN: &str = "pigeon"; diff --git a/src/cmd/query.rs b/src/cmd/query.rs index 2254333..719cffe 100644 --- a/src/cmd/query.rs +++ b/src/cmd/query.rs @@ -4,55 +4,7 @@ use crate::{ data_sources::{query_postgres, write_csv, write_image}, }; use anyhow::{anyhow, Result}; -use clap::{Arg, ArgMatches}; - -pub fn query_args() -> [Arg<'static, 'static>; 9] { - [ - Arg::with_name(cmd::QUERY) - .index(1) - .required(true) - .takes_value(true) - .help("Takes a sql query"), - Arg::with_name(arg::SSH_TUNNEL) - .long(arg::SSH_TUNNEL) - .value_name("port") - .takes_value(true) - .help("Connect to db through ssh tunnel"), - Arg::with_name(arg::SAVE) - .long(arg::SAVE) - .takes_value(false) - .help("Save query result"), - Arg::with_name(arg::SAVE_DIR) - .long(arg::SAVE_DIR) - .takes_value(true) - .default_value("./saved_queries") - .help("Specifies the output directory for saved query"), - Arg::with_name(arg::FILE_TYPE) - .long(arg::FILE_TYPE) - .takes_value(true) - .default_value("csv") - .possible_values(&["csv", "jpg", "png"]) - .help("Specifies the file type for saved query"), - Arg::with_name(arg::IMAGE_COLUMN) - .long(arg::IMAGE_COLUMN) - .required_ifs(&[(arg::FILE_TYPE, "jpg"), (arg::FILE_TYPE, "png")]) - .takes_value(true) - .help("Specifies the column in which to look for images"), - Arg::with_name(arg::IMAGE_NAME) - .long(arg::IMAGE_NAME) - .required_ifs(&[(arg::FILE_TYPE, "jpg"), (arg::FILE_TYPE, "png")]) - .takes_value(true) - .help("Specifies the column used for the image name"), - Arg::with_name(arg::DISPLAY) - .long(arg::DISPLAY) - .takes_value(false) - .help("Print query result to terminal"), - Arg::with_name(arg::VERBOSE) - .long(arg::VERBOSE) - .takes_value(false) - .help("Shows what is going on for subcommand"), - ] -} +use clap::ArgMatches; pub fn query(matches: &ArgMatches) -> Result<(), anyhow::Error> { if matches.is_present(arg::VERBOSE) { diff --git a/src/cmd/read.rs b/src/cmd/read.rs index e03c0e2..8622b38 100644 --- a/src/cmd/read.rs +++ b/src/cmd/read.rs @@ -1,22 +1,8 @@ use crate::{arg, cmd, data_sources::read_csv}; use anyhow::{anyhow, Result}; -use clap::{Arg, ArgMatches}; +use clap::ArgMatches; use std::path::PathBuf; -pub fn read_args() -> [Arg<'static, 'static>; 3] { - [ - Arg::with_name(cmd::READ).required(true).takes_value(true), - Arg::with_name(arg::VERBOSE) - .long(arg::VERBOSE) - .takes_value(false) - .help("Shows what is going on for subcommand"), - Arg::with_name(arg::DISPLAY) - .long(arg::DISPLAY) - .takes_value(false) - .help("Display csv file in terminal"), - ] -} - pub fn read(matches: &ArgMatches) -> Result<(), anyhow::Error> { if matches.is_present(arg::VERBOSE) { println!("matches: {:#?}", matches); diff --git a/src/cmd/send.rs b/src/cmd/send.rs index 08eb1bc..6448c99 100644 --- a/src/cmd/send.rs +++ b/src/cmd/send.rs @@ -1,94 +1,12 @@ use crate::{ - arg::{self, val}, + arg::{self}, email_builder::{Confirmed, Email}, email_formatter::EmlFormatter, email_transmission::Client, helper::format_green, }; use anyhow::Result; -use clap::{Arg, ArgMatches}; - -pub fn send_args() -> [Arg<'static, 'static>; 15] { - [ - Arg::with_name(arg::SENDER) - .index(1) - .required(true) - .takes_value(true) - .requires_all(&[arg::RECEIVER]) - .help("Email address of the sender"), - Arg::with_name(arg::RECEIVER) - .index(2) - .required(true) - .takes_value(true) - .requires_all(&[arg::SENDER]) - .help("Email address of the receiver"), - Arg::with_name(arg::SUBJECT) - .long(arg::SUBJECT) - .takes_value(true) - .required_unless_one(&[arg::MESSAGE_FILE]) - .help("Subject of the email"), - Arg::with_name(arg::CONTENT) - .long(arg::CONTENT) - .takes_value(true) - .requires(arg::SUBJECT) - .required_unless_one(&[arg::MESSAGE_FILE, arg::TEXT_FILE, arg::HTML_FILE]) - .conflicts_with_all(&[arg::MESSAGE_FILE, arg::TEXT_FILE, arg::HTML_FILE]) - .help("Content of the email"), - Arg::with_name(arg::MESSAGE_FILE) - .long(arg::MESSAGE_FILE) - .takes_value(true) - .required_unless_one(&[arg::SUBJECT, arg::CONTENT, arg::TEXT_FILE, arg::HTML_FILE]) - .conflicts_with_all(&[arg::CONTENT, arg::TEXT_FILE, arg::HTML_FILE]) - .help("Path of the message file"), - Arg::with_name(arg::TEXT_FILE) - .long(arg::TEXT_FILE) - .takes_value(true) - .requires(arg::SUBJECT) - .conflicts_with_all(&[arg::CONTENT, arg::MESSAGE_FILE]) - .help("Path of text file"), - Arg::with_name(arg::HTML_FILE) - .long(arg::HTML_FILE) - .takes_value(true) - .requires(arg::SUBJECT) - .conflicts_with_all(&[arg::CONTENT, arg::MESSAGE_FILE]) - .help("Path of html file"), - Arg::with_name(arg::ATTACHMENT) - .long(arg::ATTACHMENT) - .takes_value(true) - .help("Path of attachment"), - Arg::with_name(arg::ARCHIVE) - .long(arg::ARCHIVE) - .takes_value(false) - .help("Archive sent emails"), - Arg::with_name(arg::ARCHIVE_DIR) - .long(arg::ARCHIVE_DIR) - .takes_value(true) - .default_value("./sent_emails") - .help("Path of sent emails"), - Arg::with_name(arg::DISPLAY) - .long(arg::DISPLAY) - .takes_value(false) - .help("Display email in terminal"), - Arg::with_name(arg::DRY_RUN) - .long(arg::DRY_RUN) - .takes_value(false) - .help("Prepare email but do not send email"), - Arg::with_name(arg::ASSUME_YES) - .long(arg::ASSUME_YES) - .takes_value(false) - .help("Send email without confirmation"), - Arg::with_name(arg::CONNECTION) - .long(arg::CONNECTION) - .takes_value(true) - .possible_values(&[val::SMTP, val::AWS]) - .default_value(val::SMTP) - .help("Send emails via SMTP or AWS API"), - Arg::with_name(arg::VERBOSE) - .long(arg::VERBOSE) - .takes_value(false) - .help("Shows what is going on for subcommand"), - ] -} +use clap::ArgMatches; pub fn send(matches: &ArgMatches) -> Result<(), anyhow::Error> { if matches.is_present(arg::VERBOSE) { diff --git a/src/cmd/send_bulk.rs b/src/cmd/send_bulk.rs index 1a82525..6ba71e4 100644 --- a/src/cmd/send_bulk.rs +++ b/src/cmd/send_bulk.rs @@ -1,111 +1,10 @@ use crate::{ - arg::{self, val}, + arg::{self}, email_builder::{BulkEmail, Confirmed, Message, Receiver, Sender}, helper::format_green, }; use anyhow::Result; -use clap::{Arg, ArgMatches}; - -pub fn send_bulk_args() -> [Arg<'static, 'static>; 19] { - [ - Arg::with_name(arg::SENDER) - .index(1) - .required(true) - .takes_value(true) - .help("Email address of the sender"), - Arg::with_name(arg::RECEIVER_FILE) - .long(arg::RECEIVER_FILE) - .required_unless(arg::RECEIVER_QUERY) - .takes_value(true) - .help("Email addresses of multiple receivers fetched from provided csv file"), - Arg::with_name(arg::RECEIVER_QUERY) - .long(arg::RECEIVER_QUERY) - .required_unless(arg::RECEIVER_FILE) - .takes_value(true) - .help("Email addresses of multiple receivers fetched from provided query"), - Arg::with_name(arg::SUBJECT) - .long(arg::SUBJECT) - .takes_value(true) - .required_unless_one(&[arg::MESSAGE_FILE]) - .help("Subject of the email"), - Arg::with_name(arg::CONTENT) - .long(arg::CONTENT) - .takes_value(true) - .requires(arg::SUBJECT) - .required_unless_one(&[arg::MESSAGE_FILE, arg::TEXT_FILE, arg::HTML_FILE]) - .conflicts_with_all(&[arg::MESSAGE_FILE, arg::TEXT_FILE, arg::HTML_FILE]) - .help("Content of the email"), - Arg::with_name(arg::MESSAGE_FILE) - .long(arg::MESSAGE_FILE) - .takes_value(true) - .required_unless_one(&[arg::SUBJECT, arg::CONTENT, arg::TEXT_FILE, arg::HTML_FILE]) - .conflicts_with_all(&[arg::CONTENT, arg::TEXT_FILE, arg::HTML_FILE]) - .help("Path of the message file"), - Arg::with_name(arg::TEXT_FILE) - .long(arg::TEXT_FILE) - .takes_value(true) - .requires(arg::SUBJECT) - .conflicts_with_all(&[arg::CONTENT, arg::MESSAGE_FILE]) - .help("Path of text file"), - Arg::with_name(arg::HTML_FILE) - .long(arg::HTML_FILE) - .takes_value(true) - .requires(arg::SUBJECT) - .conflicts_with_all(&[arg::CONTENT, arg::MESSAGE_FILE]) - .help("Path of html file"), - Arg::with_name(arg::ATTACHMENT) - .long(arg::ATTACHMENT) - .takes_value(true) - .help("Path of attachment"), - Arg::with_name(arg::ARCHIVE) - .long(arg::ARCHIVE) - .takes_value(false) - .help("Archive sent emails"), - Arg::with_name(arg::ARCHIVE_DIR) - .long(arg::ARCHIVE_DIR) - .takes_value(true) - .default_value("./sent_emails") - .help("Path of sent emails"), - Arg::with_name(arg::RECEIVER_COLUMN) - .long(arg::RECEIVER_COLUMN) - .takes_value(true) - .default_value(val::EMAIL) - .help("Specifies the column in which to look for email addresses"), - Arg::with_name(arg::PERSONALIZE) - .long(arg::PERSONALIZE) - .takes_value(true) - .multiple(true) - .max_values(100) - .help("Personalizes email for variables defined in the message template"), - Arg::with_name(arg::DISPLAY) - .long(arg::DISPLAY) - .takes_value(false) - .help("Print emails to terminal"), - Arg::with_name(arg::DRY_RUN) - .long(arg::DRY_RUN) - .takes_value(false) - .help("Prepare emails but do not send emails"), - Arg::with_name(arg::ASSUME_YES) - .long(arg::ASSUME_YES) - .takes_value(false) - .help("Send emails without confirmation"), - Arg::with_name(arg::SSH_TUNNEL) - .long(arg::SSH_TUNNEL) - .value_name("port") - .takes_value(true) - .help("Query db through ssh tunnel"), - Arg::with_name(arg::CONNECTION) - .long(arg::CONNECTION) - .takes_value(true) - .possible_values(&[val::SMTP, val::AWS]) - .default_value(val::SMTP) - .help("Send emails via SMTP or AWS API"), - Arg::with_name(arg::VERBOSE) - .long(arg::VERBOSE) - .takes_value(false) - .help("Shows what is going on for subcommand"), - ] -} +use clap::ArgMatches; pub fn send_bulk(matches: &ArgMatches) -> Result<(), anyhow::Error> { if matches.is_present(arg::VERBOSE) { diff --git a/src/cmd/simple_query.rs b/src/cmd/simple_query.rs index 1648c69..c959f0f 100644 --- a/src/cmd/simple_query.rs +++ b/src/cmd/simple_query.rs @@ -1,22 +1,8 @@ use crate::{arg, cmd, data_sources::ConnVars}; use anyhow::{anyhow, Result}; -use clap::{Arg, ArgMatches}; +use clap::ArgMatches; use postgres::{Client, NoTls, SimpleQueryMessage}; -pub fn simple_query_args() -> [Arg<'static, 'static>; 2] { - [ - Arg::with_name(cmd::SIMPLE_QUERY) - .index(1) - .required(true) - .takes_value(true) - .help("Takes a sql query"), - Arg::with_name(arg::VERBOSE) - .long(arg::VERBOSE) - .takes_value(false) - .help("Shows what is going on for subcommand"), - ] -} - pub fn simple_query(matches: &ArgMatches) -> Result<(), anyhow::Error> { if matches.is_present(arg::VERBOSE) { println!("matches: {:#?}", matches); diff --git a/src/lib.rs b/src/lib.rs index 2c46d3a..2926290 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,8 @@ mod email_provider; mod email_transmission; mod helper; -use clap::{crate_name, crate_version, App, SubCommand}; +use arg::val; +use clap::{crate_name, crate_version, App, Arg, SubCommand}; pub fn app() -> App<'static, 'static> { App::new(crate_name!()) @@ -22,36 +23,300 @@ pub fn app() -> App<'static, 'static> { .subcommand( SubCommand::with_name(cmd::INIT) .about("Create template files in current directory") - .args(&cmd::init_args()), + .args(&[Arg::with_name(arg::VERBOSE) + .long(arg::VERBOSE) + .takes_value(false) + .help("Shows what is going on for subcommand")]), ) .subcommand( SubCommand::with_name(cmd::CONNECT) .about("Check connection to SMTP server or email provider") - .args(&cmd::connect_args()), + .args(&[ + Arg::with_name(cmd::CONNECT) + .takes_value(true) + .possible_values(&[val::SMTP, val::AWS]) + .default_value(val::SMTP) + .help("Check connection to SMTP server."), + Arg::with_name(arg::VERBOSE) + .long(arg::VERBOSE) + .takes_value(false) + .help("Shows what is going on for subcommand"), + ]), ) .subcommand( SubCommand::with_name(cmd::QUERY) .about("Query database and display results in terminal (select statements only)") - .args(&cmd::query_args()), + .args(&[ + Arg::with_name(cmd::QUERY) + .index(1) + .required(true) + .takes_value(true) + .help("Takes a sql query"), + Arg::with_name(arg::SSH_TUNNEL) + .long(arg::SSH_TUNNEL) + .value_name("port") + .takes_value(true) + .help("Connect to db through ssh tunnel"), + Arg::with_name(arg::SAVE) + .long(arg::SAVE) + .takes_value(false) + .help("Save query result"), + Arg::with_name(arg::SAVE_DIR) + .long(arg::SAVE_DIR) + .takes_value(true) + .default_value("./saved_queries") + .help("Specifies the output directory for saved query"), + Arg::with_name(arg::FILE_TYPE) + .long(arg::FILE_TYPE) + .takes_value(true) + .default_value("csv") + .possible_values(&["csv", "jpg", "png"]) + .help("Specifies the file type for saved query"), + Arg::with_name(arg::IMAGE_COLUMN) + .long(arg::IMAGE_COLUMN) + .required_ifs(&[(arg::FILE_TYPE, "jpg"), (arg::FILE_TYPE, "png")]) + .takes_value(true) + .help("Specifies the column in which to look for images"), + Arg::with_name(arg::IMAGE_NAME) + .long(arg::IMAGE_NAME) + .required_ifs(&[(arg::FILE_TYPE, "jpg"), (arg::FILE_TYPE, "png")]) + .takes_value(true) + .help("Specifies the column used for the image name"), + Arg::with_name(arg::DISPLAY) + .long(arg::DISPLAY) + .takes_value(false) + .help("Print query result to terminal"), + Arg::with_name(arg::VERBOSE) + .long(arg::VERBOSE) + .takes_value(false) + .help("Shows what is going on for subcommand"), + ]), ) .subcommand( SubCommand::with_name(cmd::SIMPLE_QUERY) .about("Simple query using the simple query protocol") - .args(&cmd::simple_query_args()), + .args(&[ + Arg::with_name(cmd::SIMPLE_QUERY) + .index(1) + .required(true) + .takes_value(true) + .help("Takes a sql query"), + Arg::with_name(arg::VERBOSE) + .long(arg::VERBOSE) + .takes_value(false) + .help("Shows what is going on for subcommand"), + ]), ) .subcommand( SubCommand::with_name(cmd::READ) .about("Read csv file and display results in terminal") - .args(&cmd::read_args()), + .args(&[ + Arg::with_name(cmd::READ).required(true).takes_value(true), + Arg::with_name(arg::VERBOSE) + .long(arg::VERBOSE) + .takes_value(false) + .help("Shows what is going on for subcommand"), + Arg::with_name(arg::DISPLAY) + .long(arg::DISPLAY) + .takes_value(false) + .help("Display csv file in terminal"), + ]), ) .subcommand( SubCommand::with_name(cmd::SEND) .about("Send email to single recipient") - .args(&cmd::send_args()), + .args(&[ + Arg::with_name(arg::SENDER) + .index(1) + .required(true) + .takes_value(true) + .requires_all(&[arg::RECEIVER]) + .help("Email address of the sender"), + Arg::with_name(arg::RECEIVER) + .index(2) + .required(true) + .takes_value(true) + .requires_all(&[arg::SENDER]) + .help("Email address of the receiver"), + Arg::with_name(arg::SUBJECT) + .long(arg::SUBJECT) + .takes_value(true) + .required_unless_one(&[arg::MESSAGE_FILE]) + .help("Subject of the email"), + Arg::with_name(arg::CONTENT) + .long(arg::CONTENT) + .takes_value(true) + .requires(arg::SUBJECT) + .required_unless_one(&[arg::MESSAGE_FILE, arg::TEXT_FILE, arg::HTML_FILE]) + .conflicts_with_all(&[arg::MESSAGE_FILE, arg::TEXT_FILE, arg::HTML_FILE]) + .help("Content of the email"), + Arg::with_name(arg::MESSAGE_FILE) + .long(arg::MESSAGE_FILE) + .takes_value(true) + .required_unless_one(&[ + arg::SUBJECT, + arg::CONTENT, + arg::TEXT_FILE, + arg::HTML_FILE, + ]) + .conflicts_with_all(&[arg::CONTENT, arg::TEXT_FILE, arg::HTML_FILE]) + .help("Path of the message file"), + Arg::with_name(arg::TEXT_FILE) + .long(arg::TEXT_FILE) + .takes_value(true) + .requires(arg::SUBJECT) + .conflicts_with_all(&[arg::CONTENT, arg::MESSAGE_FILE]) + .help("Path of text file"), + Arg::with_name(arg::HTML_FILE) + .long(arg::HTML_FILE) + .takes_value(true) + .requires(arg::SUBJECT) + .conflicts_with_all(&[arg::CONTENT, arg::MESSAGE_FILE]) + .help("Path of html file"), + Arg::with_name(arg::ATTACHMENT) + .long(arg::ATTACHMENT) + .takes_value(true) + .help("Path of attachment"), + Arg::with_name(arg::ARCHIVE) + .long(arg::ARCHIVE) + .takes_value(false) + .help("Archive sent emails"), + Arg::with_name(arg::ARCHIVE_DIR) + .long(arg::ARCHIVE_DIR) + .takes_value(true) + .default_value("./sent_emails") + .help("Path of sent emails"), + Arg::with_name(arg::DISPLAY) + .long(arg::DISPLAY) + .takes_value(false) + .help("Display email in terminal"), + Arg::with_name(arg::DRY_RUN) + .long(arg::DRY_RUN) + .takes_value(false) + .help("Prepare email but do not send email"), + Arg::with_name(arg::ASSUME_YES) + .long(arg::ASSUME_YES) + .takes_value(false) + .help("Send email without confirmation"), + Arg::with_name(arg::CONNECTION) + .long(arg::CONNECTION) + .takes_value(true) + .possible_values(&[val::SMTP, val::AWS]) + .default_value(val::SMTP) + .help("Send emails via SMTP or AWS API"), + Arg::with_name(arg::VERBOSE) + .long(arg::VERBOSE) + .takes_value(false) + .help("Shows what is going on for subcommand"), + ]), ) .subcommand( SubCommand::with_name(cmd::SEND_BULK) .about("Send email to multiple recipients") - .args(&cmd::send_bulk_args()), + .args(&[ + Arg::with_name(arg::SENDER) + .index(1) + .required(true) + .takes_value(true) + .help("Email address of the sender"), + Arg::with_name(arg::RECEIVER_FILE) + .long(arg::RECEIVER_FILE) + .required_unless(arg::RECEIVER_QUERY) + .takes_value(true) + .help( + "Email addresses of multiple receivers fetched from provided csv file", + ), + Arg::with_name(arg::RECEIVER_QUERY) + .long(arg::RECEIVER_QUERY) + .required_unless(arg::RECEIVER_FILE) + .takes_value(true) + .help("Email addresses of multiple receivers fetched from provided query"), + Arg::with_name(arg::SUBJECT) + .long(arg::SUBJECT) + .takes_value(true) + .required_unless_one(&[arg::MESSAGE_FILE]) + .help("Subject of the email"), + Arg::with_name(arg::CONTENT) + .long(arg::CONTENT) + .takes_value(true) + .requires(arg::SUBJECT) + .required_unless_one(&[arg::MESSAGE_FILE, arg::TEXT_FILE, arg::HTML_FILE]) + .conflicts_with_all(&[arg::MESSAGE_FILE, arg::TEXT_FILE, arg::HTML_FILE]) + .help("Content of the email"), + Arg::with_name(arg::MESSAGE_FILE) + .long(arg::MESSAGE_FILE) + .takes_value(true) + .required_unless_one(&[ + arg::SUBJECT, + arg::CONTENT, + arg::TEXT_FILE, + arg::HTML_FILE, + ]) + .conflicts_with_all(&[arg::CONTENT, arg::TEXT_FILE, arg::HTML_FILE]) + .help("Path of the message file"), + Arg::with_name(arg::TEXT_FILE) + .long(arg::TEXT_FILE) + .takes_value(true) + .requires(arg::SUBJECT) + .conflicts_with_all(&[arg::CONTENT, arg::MESSAGE_FILE]) + .help("Path of text file"), + Arg::with_name(arg::HTML_FILE) + .long(arg::HTML_FILE) + .takes_value(true) + .requires(arg::SUBJECT) + .conflicts_with_all(&[arg::CONTENT, arg::MESSAGE_FILE]) + .help("Path of html file"), + Arg::with_name(arg::ATTACHMENT) + .long(arg::ATTACHMENT) + .takes_value(true) + .help("Path of attachment"), + Arg::with_name(arg::ARCHIVE) + .long(arg::ARCHIVE) + .takes_value(false) + .help("Archive sent emails"), + Arg::with_name(arg::ARCHIVE_DIR) + .long(arg::ARCHIVE_DIR) + .takes_value(true) + .default_value("./sent_emails") + .help("Path of sent emails"), + Arg::with_name(arg::RECEIVER_COLUMN) + .long(arg::RECEIVER_COLUMN) + .takes_value(true) + .default_value(val::EMAIL) + .help("Specifies the column in which to look for email addresses"), + Arg::with_name(arg::PERSONALIZE) + .long(arg::PERSONALIZE) + .takes_value(true) + .multiple(true) + .max_values(100) + .help("Personalizes email for variables defined in the message template"), + Arg::with_name(arg::DISPLAY) + .long(arg::DISPLAY) + .takes_value(false) + .help("Print emails to terminal"), + Arg::with_name(arg::DRY_RUN) + .long(arg::DRY_RUN) + .takes_value(false) + .help("Prepare emails but do not send emails"), + Arg::with_name(arg::ASSUME_YES) + .long(arg::ASSUME_YES) + .takes_value(false) + .help("Send emails without confirmation"), + Arg::with_name(arg::SSH_TUNNEL) + .long(arg::SSH_TUNNEL) + .value_name("port") + .takes_value(true) + .help("Query db through ssh tunnel"), + Arg::with_name(arg::CONNECTION) + .long(arg::CONNECTION) + .takes_value(true) + .possible_values(&[val::SMTP, val::AWS]) + .default_value(val::SMTP) + .help("Send emails via SMTP or AWS API"), + Arg::with_name(arg::VERBOSE) + .long(arg::VERBOSE) + .takes_value(false) + .help("Shows what is going on for subcommand"), + ]), ) } From 546f36c77707d70ceb961290370a4a090ea94dd2 Mon Sep 17 00:00:00 2001 From: quambene Date: Mon, 25 Mar 2024 00:23:22 +0100 Subject: [PATCH 18/54] Refactor email --- src/cmd/send.rs | 10 +++++++--- src/cmd/send_bulk.rs | 16 +++++++++++++++- src/email_builder/bulk_email.rs | 27 ++------------------------- src/email_builder/email.rs | 11 +---------- 4 files changed, 25 insertions(+), 39 deletions(-) diff --git a/src/cmd/send.rs b/src/cmd/send.rs index 6448c99..7310b41 100644 --- a/src/cmd/send.rs +++ b/src/cmd/send.rs @@ -1,6 +1,6 @@ use crate::{ - arg::{self}, - email_builder::{Confirmed, Email}, + arg, + email_builder::{Confirmed, Email, Message, MimeFormat, Receiver, Sender}, email_formatter::EmlFormatter, email_transmission::Client, helper::format_green, @@ -13,7 +13,11 @@ pub fn send(matches: &ArgMatches) -> Result<(), anyhow::Error> { println!("matches: {:#?}", matches); } - let email = Email::build(matches)?; + let sender = Sender::init(matches)?; + let receiver = Receiver::init(matches)?; + let message = Message::build(matches)?; + let mime_format = MimeFormat::new(matches, sender, receiver, &message)?; + let email = Email::new(sender, receiver, &message, &mime_format)?; if matches.is_present(arg::DISPLAY) { println!("Display email: {:#?}", email); diff --git a/src/cmd/send_bulk.rs b/src/cmd/send_bulk.rs index 6ba71e4..4b4a51c 100644 --- a/src/cmd/send_bulk.rs +++ b/src/cmd/send_bulk.rs @@ -3,6 +3,7 @@ use crate::{ email_builder::{BulkEmail, Confirmed, Message, Receiver, Sender}, helper::format_green, }; +use anyhow::anyhow; use anyhow::Result; use clap::ArgMatches; @@ -14,7 +15,20 @@ pub fn send_bulk(matches: &ArgMatches) -> Result<(), anyhow::Error> { let sender = Sender::init(matches)?; let df_receiver = Receiver::dataframe(matches)?; let default_message = Message::build(matches)?; - let bulk_email = BulkEmail::build(matches, sender, &df_receiver, &default_message)?; + let bulk_email = if matches.is_present(arg::PERSONALIZE) { + match matches.values_of(arg::PERSONALIZE) { + Some(personalized_columns) => BulkEmail::personalize( + matches, + sender, + &df_receiver, + &default_message, + personalized_columns, + )?, + None => return Err(anyhow!("Missing value for argument '{}'", arg::PERSONALIZE)), + } + } else { + BulkEmail::new(matches, sender, &df_receiver, &default_message)? + }; if matches.is_present(arg::DISPLAY) { println!("Display emails: {:#?}", bulk_email); diff --git a/src/email_builder/bulk_email.rs b/src/email_builder/bulk_email.rs index cba37a9..3d0d74a 100644 --- a/src/email_builder/bulk_email.rs +++ b/src/email_builder/bulk_email.rs @@ -5,7 +5,7 @@ use crate::{ email_formatter::EmlFormatter, email_transmission::Client, }; -use anyhow::{anyhow, Context, Result}; +use anyhow::{Context, Result}; use clap::{ArgMatches, Values}; use polars::prelude::DataFrame; use std::io; @@ -16,33 +16,10 @@ pub struct BulkEmail<'a> { } impl<'a> BulkEmail<'a> { - pub fn build( - matches: &'a ArgMatches, - sender: &'a str, - df_receiver: &'a DataFrame, - default_message: &'a Message, - ) -> Result { - let bulk_email = if matches.is_present(arg::PERSONALIZE) { - match matches.values_of(arg::PERSONALIZE) { - Some(personalized_columns) => BulkEmail::personalize( - matches, - sender, - df_receiver, - default_message, - personalized_columns, - )?, - None => return Err(anyhow!("Missing value for argument '{}'", arg::PERSONALIZE)), - } - } else { - BulkEmail::new(matches, sender, df_receiver, default_message)? - }; - - Ok(bulk_email) - } - pub fn new( matches: &'a ArgMatches, sender: &'a str, + df_receiver: &'a DataFrame, message: &'a Message, ) -> Result { diff --git a/src/email_builder/email.rs b/src/email_builder/email.rs index 9da661f..610adfb 100644 --- a/src/email_builder/email.rs +++ b/src/email_builder/email.rs @@ -1,6 +1,6 @@ use crate::{ arg, - email_builder::{Confirmed, Message, MimeFormat, Receiver, Sender}, + email_builder::{Confirmed, Message, MimeFormat}, }; use anyhow::{anyhow, Context, Result}; use clap::ArgMatches; @@ -15,15 +15,6 @@ pub struct Email<'a> { } impl<'a> Email<'a> { - pub fn build(matches: &'a ArgMatches) -> Result { - let sender = Sender::init(matches)?; - let receiver = Receiver::init(matches)?; - let message = Message::build(matches)?; - let mime_format = MimeFormat::new(matches, sender, receiver, &message)?; - let email = Email::new(sender, receiver, &message, &mime_format)?; - Ok(email) - } - pub fn new( sender: &'a str, receiver: &'a str, From 487b3bb24ccb36e38d57902eee12057c333834d2 Mon Sep 17 00:00:00 2001 From: quambene Date: Mon, 25 Mar 2024 01:56:49 +0100 Subject: [PATCH 19/54] Refactor mime format --- src/cmd/send.rs | 5 ++- src/email_builder/bulk_email.rs | 10 +++-- src/email_builder/message/mod.rs | 22 +++++----- src/email_builder/mime.rs | 74 ++++++++++++++++++++++++++------ test_data/email_html.txt | 9 ++++ test_data/email_plaintext.txt | 9 ++++ tests/cmd/test_send.rs | 61 +++++--------------------- 7 files changed, 112 insertions(+), 78 deletions(-) create mode 100644 test_data/email_html.txt create mode 100644 test_data/email_plaintext.txt diff --git a/src/cmd/send.rs b/src/cmd/send.rs index 7310b41..259ecf6 100644 --- a/src/cmd/send.rs +++ b/src/cmd/send.rs @@ -7,16 +7,19 @@ use crate::{ }; use anyhow::Result; use clap::ArgMatches; +use std::time::SystemTime; pub fn send(matches: &ArgMatches) -> Result<(), anyhow::Error> { if matches.is_present(arg::VERBOSE) { println!("matches: {:#?}", matches); } + let now = SystemTime::now(); let sender = Sender::init(matches)?; let receiver = Receiver::init(matches)?; let message = Message::build(matches)?; - let mime_format = MimeFormat::new(matches, sender, receiver, &message)?; + let attachment = matches.value_of(arg::ATTACHMENT); + let mime_format = MimeFormat::new(sender, receiver, &message, attachment, now)?; let email = Email::new(sender, receiver, &message, &mime_format)?; if matches.is_present(arg::DISPLAY) { diff --git a/src/email_builder/bulk_email.rs b/src/email_builder/bulk_email.rs index 3d0d74a..3d8954b 100644 --- a/src/email_builder/bulk_email.rs +++ b/src/email_builder/bulk_email.rs @@ -8,7 +8,7 @@ use crate::{ use anyhow::{Context, Result}; use clap::{ArgMatches, Values}; use polars::prelude::DataFrame; -use std::io; +use std::{io, time::SystemTime}; #[derive(Debug)] pub struct BulkEmail<'a> { @@ -23,6 +23,7 @@ impl<'a> BulkEmail<'a> { df_receiver: &'a DataFrame, message: &'a Message, ) -> Result { + let now = SystemTime::now(); let mut emails: Vec = vec![]; let receiver_column_name = Receiver::column_name(matches)?; let receivers = TabularData::column(receiver_column_name, df_receiver)?; @@ -30,7 +31,8 @@ impl<'a> BulkEmail<'a> { for receiver in receivers { match receiver { Some(receiver) => { - let mime_format = MimeFormat::new(matches, sender, receiver, message)?; + let attachment = matches.value_of(arg::ATTACHMENT); + let mime_format = MimeFormat::new(sender, receiver, message, attachment, now)?; let email = Email::new(sender, receiver, message, &mime_format)?; emails.push(email); } @@ -48,6 +50,7 @@ impl<'a> BulkEmail<'a> { default_message: &Message, personalized_columns: Values, ) -> Result { + let now = SystemTime::now(); let mut emails: Vec = vec![]; let columns: Vec<&str> = personalized_columns.collect(); let receiver_column_name = Receiver::column_name(matches)?; @@ -57,7 +60,8 @@ impl<'a> BulkEmail<'a> { message.personalize(i, df_receiver, &columns)?; let receiver = TabularData::row(i, receiver_column_name, df_receiver)?; - let mime_format = MimeFormat::new(matches, sender, receiver, &message)?; + let attachment = matches.value_of(arg::ATTACHMENT); + let mime_format = MimeFormat::new(sender, receiver, &message, attachment, now)?; let email = Email::new(sender, receiver, &message, &mime_format)?; emails.push(email); diff --git a/src/email_builder/message/mod.rs b/src/email_builder/message/mod.rs index e243adb..78cf288 100644 --- a/src/email_builder/message/mod.rs +++ b/src/email_builder/message/mod.rs @@ -15,6 +15,17 @@ pub struct Message { } impl Message { + pub fn new(subject: S, text: Option, html: Option) -> Self + where + S: Into, + { + Self { + subject: subject.into(), + text: text.map(|text| text.into()), + html: html.map(|text| text.into()), + } + } + pub fn build(matches: &ArgMatches) -> Result { let message = if matches.is_present(arg::SUBJECT) && matches.is_present(arg::CONTENT) { Message::from_cmd(matches)? @@ -111,15 +122,4 @@ impl Message { Err(anyhow!("Missing argument '{}'", arg::SUBJECT)) } } - - fn new(subject: S, text: Option, html: Option) -> Self - where - S: Into, - { - Self { - subject: subject.into(), - text: text.map(|text| text.into()), - html: html.map(|text| text.into()), - } - } } diff --git a/src/email_builder/mime.rs b/src/email_builder/mime.rs index 51c5ce9..f96092b 100644 --- a/src/email_builder/mime.rs +++ b/src/email_builder/mime.rs @@ -1,34 +1,31 @@ -use crate::{arg, email_builder}; +use crate::email_builder; use anyhow::{anyhow, Context}; -use clap::ArgMatches; use lettre::{ message::{header, MultiPart, SinglePart}, - Message, + Message as LettreMessage, }; -use std::{fmt, fs, path::Path, str}; +use std::{fmt, fs, path::Path, str, time::SystemTime}; #[derive(Clone)] pub struct MimeFormat { - pub message: Message, + pub message: LettreMessage, } impl MimeFormat { pub fn new( - matches: &ArgMatches, sender: &str, receiver: &str, message: &email_builder::Message, + attachment: Option<&str>, + now: SystemTime, ) -> Result { - let message_builder = Message::builder() + let message_builder = LettreMessage::builder() .from(sender.parse().context("Can't parse sender")?) .to(receiver.parse().context("Can't parse receiver")?) - .subject(&message.subject); + .subject(&message.subject) + .date(now); - let message = match ( - &message.text, - &message.html, - matches.value_of(arg::ATTACHMENT), - ) { + let message = match (&message.text, &message.html, attachment) { (Some(text), Some(html), Some(attachment)) => message_builder.multipart( MultiPart::mixed() .multipart(Self::alternative(text, html)) @@ -116,3 +113,54 @@ impl fmt::Debug for MimeFormat { ) } } + +#[cfg(test)] +mod tests { + use self::email_builder::Message; + use super::*; + use std::{fs::File, io::Read, time::UNIX_EPOCH}; + + #[test] + fn test_mime_format_plaintext() { + let date_time = chrono::DateTime::parse_from_rfc3339("2024-01-01T14:00:00Z") + .unwrap() + .timestamp() as u64; + let system_time = UNIX_EPOCH + std::time::Duration::from_secs(date_time); + let sender = "albert@einstein.com"; + let receiver = "marie@curie.com"; + let subject = "Test Subject"; + let text = "This is a test message (plaintext)."; + let message = Message::new(subject, Some(text), None); + + let res = MimeFormat::new(sender, receiver, &message, None, system_time); + assert!(res.is_ok()); + + let mime_format = format!("{:?}", res.unwrap()); + let mut expected_file = File::open("./test_data/email_plaintext.txt").unwrap(); + let mut expected_format = String::new(); + expected_file.read_to_string(&mut expected_format).unwrap(); + assert_eq!(mime_format.replace("\r", ""), expected_format); + } + + #[test] + fn test_mime_format_html() { + let date_time = chrono::DateTime::parse_from_rfc3339("2024-01-01T14:00:00Z") + .unwrap() + .timestamp() as u64; + let system_time = UNIX_EPOCH + std::time::Duration::from_secs(date_time); + let sender = "albert@einstein.com"; + let receiver = "marie@curie.com"; + let subject = "Test Subject"; + let html = "

This is a test message (html).

"; + let message = Message::new(subject, None, Some(html)); + + let res = MimeFormat::new(sender, receiver, &message, None, system_time); + assert!(res.is_ok()); + + let mime_format = format!("{:?}", res.unwrap()); + let mut expected_file = File::open("./test_data/email_html.txt").unwrap(); + let mut expected_format = String::new(); + expected_file.read_to_string(&mut expected_format).unwrap(); + assert_eq!(mime_format.replace("\r", ""), expected_format); + } +} diff --git a/test_data/email_html.txt b/test_data/email_html.txt new file mode 100644 index 0000000..673af82 --- /dev/null +++ b/test_data/email_html.txt @@ -0,0 +1,9 @@ +From: albert@einstein.com +To: marie@curie.com +Subject: Test Subject +Date: Mon, 01 Jan 2024 14:00:00 +0000 +MIME-Version: 1.0 +Content-Type: text/html; charset=utf-8 +Content-Transfer-Encoding: 7bit + +

This is a test message (html).

diff --git a/test_data/email_plaintext.txt b/test_data/email_plaintext.txt new file mode 100644 index 0000000..f5fd67e --- /dev/null +++ b/test_data/email_plaintext.txt @@ -0,0 +1,9 @@ +From: albert@einstein.com +To: marie@curie.com +Subject: Test Subject +Date: Mon, 01 Jan 2024 14:00:00 +0000 +MIME-Version: 1.0 +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: 7bit + +This is a test message (plaintext). diff --git a/tests/cmd/test_send.rs b/tests/cmd/test_send.rs index b884b61..8c807d4 100644 --- a/tests/cmd/test_send.rs +++ b/tests/cmd/test_send.rs @@ -1,27 +1,27 @@ -use std::env; - use assert_cmd::Command; use predicates::str; +use std::env; #[test] -fn test_send_dry() { +fn test_send_message_file_smtp_dry() { println!("Execute 'pigeon send'"); let mut cmd = Command::cargo_bin("pigeon").unwrap(); cmd.args([ "send", "albert@einstein.com", - "receiver@gmail.com", - "--subject", - "Test subject", - "--content", - "This is a test email.", + "marie@curie.com", + "--message-file", + "./test_data/message.yaml", "--dry-run", + "--display", + "--assume-yes", + "--archive", ]); cmd.assert().success().stdout(str::contains("abc")); } #[test] -fn test_send_subject_content_smtp_dry() { +fn test_send_aws_api_dry() { println!("Execute 'pigeon send'"); let mut cmd = Command::cargo_bin("pigeon").unwrap(); cmd.args([ @@ -36,6 +36,8 @@ fn test_send_subject_content_smtp_dry() { "--display", "--assume-yes", "--archive", + "--connection", + "aws", ]); cmd.assert().success().stdout(str::contains("abc")); } @@ -58,24 +60,6 @@ fn test_send_subject_content_smtp() { cmd.assert().success().stdout(str::contains("abc")); } -#[test] -fn test_send_message_file_smtp_dry() { - println!("Execute 'pigeon send'"); - let mut cmd = Command::cargo_bin("pigeon").unwrap(); - cmd.args([ - "send", - "albert@einstein.com", - "marie@curie.com", - "--message-file", - "./test_data/message.yaml", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - ]); - cmd.assert().success().stdout(str::contains("abc")); -} - #[test] #[ignore] fn test_send_message_file_smtp() { @@ -322,29 +306,6 @@ fn test_attachment_pdf_aws_api() { cmd.assert().success().stdout(str::contains("abc")); } -#[test] -#[ignore] -fn test_send_aws_api_dry() { - println!("Execute 'pigeon send'"); - let mut cmd = Command::cargo_bin("pigeon").unwrap(); - cmd.args([ - "send", - "albert@einstein.com", - "marie@curie.com", - "--subject", - "Test Subject", - "--content", - "This is a test message (plaintext).", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - "--connection", - "aws", - ]); - cmd.assert().success().stdout(str::contains("abc")); -} - #[test] #[ignore] fn test_send_aws_api() { From 93692045407bfaaba69c0ee94209b09c44c72361 Mon Sep 17 00:00:00 2001 From: quambene Date: Mon, 25 Mar 2024 02:48:58 +0100 Subject: [PATCH 20/54] Add tests --- src/cmd/send.rs | 2 +- src/email_builder/bulk_email.rs | 5 +- src/email_builder/mime.rs | 82 +++++++++++++++++++++++++++------ test_data/email_multipart.txt | 19 ++++++++ 4 files changed, 91 insertions(+), 17 deletions(-) create mode 100644 test_data/email_multipart.txt diff --git a/src/cmd/send.rs b/src/cmd/send.rs index 259ecf6..9f09fa5 100644 --- a/src/cmd/send.rs +++ b/src/cmd/send.rs @@ -19,7 +19,7 @@ pub fn send(matches: &ArgMatches) -> Result<(), anyhow::Error> { let receiver = Receiver::init(matches)?; let message = Message::build(matches)?; let attachment = matches.value_of(arg::ATTACHMENT); - let mime_format = MimeFormat::new(sender, receiver, &message, attachment, now)?; + let mime_format = MimeFormat::new(sender, receiver, &message, attachment, now, None)?; let email = Email::new(sender, receiver, &message, &mime_format)?; if matches.is_present(arg::DISPLAY) { diff --git a/src/email_builder/bulk_email.rs b/src/email_builder/bulk_email.rs index 3d8954b..41c4f9e 100644 --- a/src/email_builder/bulk_email.rs +++ b/src/email_builder/bulk_email.rs @@ -32,7 +32,8 @@ impl<'a> BulkEmail<'a> { match receiver { Some(receiver) => { let attachment = matches.value_of(arg::ATTACHMENT); - let mime_format = MimeFormat::new(sender, receiver, message, attachment, now)?; + let mime_format = + MimeFormat::new(sender, receiver, message, attachment, now, None)?; let email = Email::new(sender, receiver, message, &mime_format)?; emails.push(email); } @@ -61,7 +62,7 @@ impl<'a> BulkEmail<'a> { let receiver = TabularData::row(i, receiver_column_name, df_receiver)?; let attachment = matches.value_of(arg::ATTACHMENT); - let mime_format = MimeFormat::new(sender, receiver, &message, attachment, now)?; + let mime_format = MimeFormat::new(sender, receiver, &message, attachment, now, None)?; let email = Email::new(sender, receiver, &message, &mime_format)?; emails.push(email); diff --git a/src/email_builder/mime.rs b/src/email_builder/mime.rs index f96092b..1d2d314 100644 --- a/src/email_builder/mime.rs +++ b/src/email_builder/mime.rs @@ -18,6 +18,7 @@ impl MimeFormat { message: &email_builder::Message, attachment: Option<&str>, now: SystemTime, + multipart_boundary: Option<&str>, ) -> Result { let message_builder = LettreMessage::builder() .from(sender.parse().context("Can't parse sender")?) @@ -28,20 +29,18 @@ impl MimeFormat { let message = match (&message.text, &message.html, attachment) { (Some(text), Some(html), Some(attachment)) => message_builder.multipart( MultiPart::mixed() - .multipart(Self::alternative(text, html)) + .multipart(Self::alternative(text, html, multipart_boundary)) .singlepart(Self::attachment(attachment)?), ), (Some(text), Some(html), None) => { - message_builder.multipart(Self::alternative(text, html)) + message_builder.multipart(Self::alternative(text, html, multipart_boundary)) } (Some(text), None, Some(attachment)) => message_builder.multipart( - MultiPart::mixed() - .singlepart(Self::text_plain(text)) + Self::multipart_text(text, multipart_boundary) .singlepart(Self::attachment(attachment)?), ), (None, Some(html), Some(attachment)) => message_builder.multipart( - MultiPart::mixed() - .singlepart(Self::text_html(html)) + Self::multipart_html(html, multipart_boundary) .singlepart(Self::attachment(attachment)?), ), (Some(text), None, None) => message_builder.singlepart(Self::text_plain(text)), @@ -68,6 +67,36 @@ impl MimeFormat { .body(text.to_string()) } + fn multipart_text(text: &str, boundary: Option<&str>) -> MultiPart { + match boundary { + Some(boundary) => MultiPart::mixed() + .boundary(boundary) + .singlepart(Self::text_plain(text)), + None => MultiPart::mixed().singlepart(Self::text_plain(text)), + } + } + + fn multipart_html(html: &str, boundary: Option<&str>) -> MultiPart { + match boundary { + Some(boundary) => MultiPart::mixed() + .boundary(boundary) + .singlepart(Self::text_html(html)), + None => MultiPart::mixed().singlepart(Self::text_html(html)), + } + } + + fn alternative(text: &str, html: &str, boundary: Option<&str>) -> MultiPart { + match boundary { + Some(boundary) => MultiPart::alternative() + .boundary(boundary) + .singlepart(Self::text_plain(text)) + .singlepart(Self::text_html(html)), + None => MultiPart::alternative() + .singlepart(Self::text_plain(text)) + .singlepart(Self::text_html(html)), + } + } + fn attachment(file: &str) -> Result { let path = Path::new(file); let file_name = match path.file_name() { @@ -96,12 +125,6 @@ impl MimeFormat { .header(header::ContentDisposition::attachment(file_name)) .body(bytes)) } - - fn alternative(text: &str, html: &str) -> MultiPart { - MultiPart::alternative() - .singlepart(Self::text_plain(text)) - .singlepart(Self::text_html(html)) - } } impl fmt::Debug for MimeFormat { @@ -132,7 +155,7 @@ mod tests { let text = "This is a test message (plaintext)."; let message = Message::new(subject, Some(text), None); - let res = MimeFormat::new(sender, receiver, &message, None, system_time); + let res = MimeFormat::new(sender, receiver, &message, None, system_time, None); assert!(res.is_ok()); let mime_format = format!("{:?}", res.unwrap()); @@ -154,7 +177,7 @@ mod tests { let html = "

This is a test message (html).

"; let message = Message::new(subject, None, Some(html)); - let res = MimeFormat::new(sender, receiver, &message, None, system_time); + let res = MimeFormat::new(sender, receiver, &message, None, system_time, None); assert!(res.is_ok()); let mime_format = format!("{:?}", res.unwrap()); @@ -163,4 +186,35 @@ mod tests { expected_file.read_to_string(&mut expected_format).unwrap(); assert_eq!(mime_format.replace("\r", ""), expected_format); } + + #[test] + fn test_mime_format_multipart() { + let date_time = chrono::DateTime::parse_from_rfc3339("2024-01-01T14:00:00Z") + .unwrap() + .timestamp() as u64; + let system_time = UNIX_EPOCH + std::time::Duration::from_secs(date_time); + let sender = "albert@einstein.com"; + let receiver = "marie@curie.com"; + let subject = "Test Subject"; + let text = "This is a test message (plaintext)."; + let html = "

This is a test message (html).

"; + let message = Message::new(subject, Some(text), Some(html)); + let multipart_boundary = "RZcCpBhV4GEzm8ETTVblOuzZ8bwGzGVyjkQfGTMt"; + + let res = MimeFormat::new( + sender, + receiver, + &message, + None, + system_time, + Some(multipart_boundary), + ); + assert!(res.is_ok()); + + let mime_format = format!("{:?}", res.unwrap()); + let mut expected_file = File::open("./test_data/email_multipart.txt").unwrap(); + let mut expected_format = String::new(); + expected_file.read_to_string(&mut expected_format).unwrap(); + assert_eq!(mime_format.replace("\r", ""), expected_format); + } } diff --git a/test_data/email_multipart.txt b/test_data/email_multipart.txt new file mode 100644 index 0000000..dd8ece9 --- /dev/null +++ b/test_data/email_multipart.txt @@ -0,0 +1,19 @@ +From: albert@einstein.com +To: marie@curie.com +Subject: Test Subject +Date: Mon, 01 Jan 2024 14:00:00 +0000 +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="RZcCpBhV4GEzm8ETTVblOuzZ8bwGzGVyjkQfGTMt" + +--RZcCpBhV4GEzm8ETTVblOuzZ8bwGzGVyjkQfGTMt +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: 7bit + +This is a test message (plaintext). +--RZcCpBhV4GEzm8ETTVblOuzZ8bwGzGVyjkQfGTMt +Content-Type: text/html; charset=utf-8 +Content-Transfer-Encoding: 7bit + +

This is a test message (html).

+--RZcCpBhV4GEzm8ETTVblOuzZ8bwGzGVyjkQfGTMt-- From 500467c74c860115ec856dff281bd7e8aa39c038 Mon Sep 17 00:00:00 2001 From: quambene Date: Mon, 25 Mar 2024 02:54:41 +0100 Subject: [PATCH 21/54] Clean up tests --- tests/cmd/test_send.rs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/tests/cmd/test_send.rs b/tests/cmd/test_send.rs index 8c807d4..05cbd76 100644 --- a/tests/cmd/test_send.rs +++ b/tests/cmd/test_send.rs @@ -2,24 +2,6 @@ use assert_cmd::Command; use predicates::str; use std::env; -#[test] -fn test_send_message_file_smtp_dry() { - println!("Execute 'pigeon send'"); - let mut cmd = Command::cargo_bin("pigeon").unwrap(); - cmd.args([ - "send", - "albert@einstein.com", - "marie@curie.com", - "--message-file", - "./test_data/message.yaml", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - ]); - cmd.assert().success().stdout(str::contains("abc")); -} - #[test] fn test_send_aws_api_dry() { println!("Execute 'pigeon send'"); From dbaa9294553d4f93b9f387b61f080577bfef348d Mon Sep 17 00:00:00 2001 From: quambene Date: Wed, 27 Mar 2024 02:19:51 +0100 Subject: [PATCH 22/54] Test mime format --- src/cmd/send.rs | 8 +- src/email_builder/bulk_email.rs | 15 +- src/email_builder/mime.rs | 257 +++++++++++++----- ...rt.txt => email_multipart_alternative.txt} | 0 test_data/email_multipart_mixed.txt | 20 ++ .../email_multipart_mixed_alternative.txt | 30 ++ test_data/email_singlepart_attachment.txt | 10 + ...ail_html.txt => email_singlepart_html.txt} | 0 ...ext.txt => email_singlepart_plaintext.txt} | 0 test_data/test.txt | 1 + tests/cmd/test_send.rs | 200 +++----------- 11 files changed, 306 insertions(+), 235 deletions(-) rename test_data/{email_multipart.txt => email_multipart_alternative.txt} (100%) create mode 100644 test_data/email_multipart_mixed.txt create mode 100644 test_data/email_multipart_mixed_alternative.txt create mode 100644 test_data/email_singlepart_attachment.txt rename test_data/{email_html.txt => email_singlepart_html.txt} (100%) rename test_data/{email_plaintext.txt => email_singlepart_plaintext.txt} (100%) create mode 100644 test_data/test.txt diff --git a/src/cmd/send.rs b/src/cmd/send.rs index 9f09fa5..8ba7975 100644 --- a/src/cmd/send.rs +++ b/src/cmd/send.rs @@ -7,7 +7,7 @@ use crate::{ }; use anyhow::Result; use clap::ArgMatches; -use std::time::SystemTime; +use std::{path::Path, time::SystemTime}; pub fn send(matches: &ArgMatches) -> Result<(), anyhow::Error> { if matches.is_present(arg::VERBOSE) { @@ -18,8 +18,10 @@ pub fn send(matches: &ArgMatches) -> Result<(), anyhow::Error> { let sender = Sender::init(matches)?; let receiver = Receiver::init(matches)?; let message = Message::build(matches)?; - let attachment = matches.value_of(arg::ATTACHMENT); - let mime_format = MimeFormat::new(sender, receiver, &message, attachment, now, None)?; + let attachment = matches + .value_of(arg::ATTACHMENT) + .map(|attachment| Path::new(attachment)); + let mime_format = MimeFormat::new(sender, receiver, &message, attachment, now)?; let email = Email::new(sender, receiver, &message, &mime_format)?; if matches.is_present(arg::DISPLAY) { diff --git a/src/email_builder/bulk_email.rs b/src/email_builder/bulk_email.rs index 41c4f9e..31794cc 100644 --- a/src/email_builder/bulk_email.rs +++ b/src/email_builder/bulk_email.rs @@ -8,7 +8,7 @@ use crate::{ use anyhow::{Context, Result}; use clap::{ArgMatches, Values}; use polars::prelude::DataFrame; -use std::{io, time::SystemTime}; +use std::{io, path::Path, time::SystemTime}; #[derive(Debug)] pub struct BulkEmail<'a> { @@ -31,9 +31,10 @@ impl<'a> BulkEmail<'a> { for receiver in receivers { match receiver { Some(receiver) => { - let attachment = matches.value_of(arg::ATTACHMENT); - let mime_format = - MimeFormat::new(sender, receiver, message, attachment, now, None)?; + let attachment = matches + .value_of(arg::ATTACHMENT) + .map(|attachment| Path::new(attachment)); + let mime_format = MimeFormat::new(sender, receiver, message, attachment, now)?; let email = Email::new(sender, receiver, message, &mime_format)?; emails.push(email); } @@ -61,8 +62,10 @@ impl<'a> BulkEmail<'a> { message.personalize(i, df_receiver, &columns)?; let receiver = TabularData::row(i, receiver_column_name, df_receiver)?; - let attachment = matches.value_of(arg::ATTACHMENT); - let mime_format = MimeFormat::new(sender, receiver, &message, attachment, now, None)?; + let attachment = matches + .value_of(arg::ATTACHMENT) + .map(|attachment| Path::new(attachment)); + let mime_format = MimeFormat::new(sender, receiver, &message, attachment, now)?; let email = Email::new(sender, receiver, &message, &mime_format)?; emails.push(email); diff --git a/src/email_builder/mime.rs b/src/email_builder/mime.rs index 1d2d314..56f8aee 100644 --- a/src/email_builder/mime.rs +++ b/src/email_builder/mime.rs @@ -16,37 +16,49 @@ impl MimeFormat { sender: &str, receiver: &str, message: &email_builder::Message, - attachment: Option<&str>, + attachment: Option<&Path>, now: SystemTime, - multipart_boundary: Option<&str>, ) -> Result { + let sender = sender.parse().context("Can't parse sender")?; + let receiver = receiver.parse().context("Can't parse receiver")?; let message_builder = LettreMessage::builder() - .from(sender.parse().context("Can't parse sender")?) - .to(receiver.parse().context("Can't parse receiver")?) + .from(sender) + .to(receiver) .subject(&message.subject) .date(now); - let message = match (&message.text, &message.html, attachment) { (Some(text), Some(html), Some(attachment)) => message_builder.multipart( MultiPart::mixed() - .multipart(Self::alternative(text, html, multipart_boundary)) - .singlepart(Self::attachment(attachment)?), + .multipart( + MultiPart::alternative() + .singlepart(Self::singlepart_text_plain(text)) + .singlepart(Self::singlepart_text_html(html)), + ) + .singlepart(Self::singlepart_attachment(attachment)?), + ), + (Some(text), Some(html), None) => message_builder.multipart( + MultiPart::alternative() + .singlepart(Self::singlepart_text_plain(text)) + .singlepart(Self::singlepart_text_html(html)), ), - (Some(text), Some(html), None) => { - message_builder.multipart(Self::alternative(text, html, multipart_boundary)) - } (Some(text), None, Some(attachment)) => message_builder.multipart( - Self::multipart_text(text, multipart_boundary) - .singlepart(Self::attachment(attachment)?), + MultiPart::mixed() + .singlepart(Self::singlepart_text_plain(text)) + .singlepart(Self::singlepart_attachment(attachment)?), ), (None, Some(html), Some(attachment)) => message_builder.multipart( - Self::multipart_html(html, multipart_boundary) - .singlepart(Self::attachment(attachment)?), + MultiPart::mixed() + .singlepart(Self::singlepart_text_html(html)) + .singlepart(Self::singlepart_attachment(attachment)?), ), - (Some(text), None, None) => message_builder.singlepart(Self::text_plain(text)), - (None, Some(html), None) => message_builder.singlepart(Self::text_html(html)), + (Some(text), None, None) => { + message_builder.singlepart(Self::singlepart_text_plain(text)) + } + (None, Some(html), None) => { + message_builder.singlepart(Self::singlepart_text_html(html)) + } (None, None, Some(attachment)) => { - message_builder.singlepart(Self::attachment(attachment)?) + message_builder.singlepart(Self::singlepart_attachment(attachment)?) } (None, None, None) => return Err(anyhow!("Missing email body")), } @@ -55,50 +67,19 @@ impl MimeFormat { Ok(Self { message }) } - fn text_plain(text: &str) -> SinglePart { + fn singlepart_text_plain(text: &str) -> SinglePart { SinglePart::builder() .header(header::ContentType::TEXT_PLAIN) .body(text.to_string()) } - fn text_html(text: &str) -> SinglePart { + fn singlepart_text_html(text: &str) -> SinglePart { SinglePart::builder() .header(header::ContentType::TEXT_HTML) .body(text.to_string()) } - fn multipart_text(text: &str, boundary: Option<&str>) -> MultiPart { - match boundary { - Some(boundary) => MultiPart::mixed() - .boundary(boundary) - .singlepart(Self::text_plain(text)), - None => MultiPart::mixed().singlepart(Self::text_plain(text)), - } - } - - fn multipart_html(html: &str, boundary: Option<&str>) -> MultiPart { - match boundary { - Some(boundary) => MultiPart::mixed() - .boundary(boundary) - .singlepart(Self::text_html(html)), - None => MultiPart::mixed().singlepart(Self::text_html(html)), - } - } - - fn alternative(text: &str, html: &str, boundary: Option<&str>) -> MultiPart { - match boundary { - Some(boundary) => MultiPart::alternative() - .boundary(boundary) - .singlepart(Self::text_plain(text)) - .singlepart(Self::text_html(html)), - None => MultiPart::alternative() - .singlepart(Self::text_plain(text)) - .singlepart(Self::text_html(html)), - } - } - - fn attachment(file: &str) -> Result { - let path = Path::new(file); + fn singlepart_attachment(path: &Path) -> Result { let file_name = match path.file_name() { Some(file_name) => match file_name.to_str() { Some(file_name) => file_name, @@ -143,8 +124,71 @@ mod tests { use super::*; use std::{fs::File, io::Read, time::UNIX_EPOCH}; + impl MimeFormat { + pub fn new_with_boundaries( + sender: &str, + receiver: &str, + message: &email_builder::Message, + attachment: Option<&Path>, + now: SystemTime, + boundaries: Vec<&str>, + ) -> Result { + let sender = sender.parse().context("Can't parse sender")?; + let receiver = receiver.parse().context("Can't parse receiver")?; + let message_builder = LettreMessage::builder() + .from(sender) + .to(receiver) + .subject(&message.subject) + .date(now); + let message = match (&message.text, &message.html, attachment) { + (Some(text), Some(html), Some(attachment)) => message_builder.multipart( + MultiPart::mixed() + .boundary(boundaries[0]) + .multipart( + MultiPart::alternative() + .boundary(boundaries[1]) + .singlepart(Self::singlepart_text_plain(text)) + .singlepart(Self::singlepart_text_html(html)), + ) + .singlepart(Self::singlepart_attachment(attachment)?), + ), + (Some(text), Some(html), None) => message_builder.multipart( + MultiPart::alternative() + .boundary(boundaries[0]) + .singlepart(Self::singlepart_text_plain(text)) + .singlepart(Self::singlepart_text_html(html)), + ), + (Some(text), None, Some(attachment)) => message_builder.multipart( + MultiPart::mixed() + .boundary(boundaries[0]) + .singlepart(Self::singlepart_text_plain(text)) + .singlepart(Self::singlepart_attachment(attachment)?), + ), + (None, Some(html), Some(attachment)) => message_builder.multipart( + MultiPart::mixed() + .boundary(boundaries[0]) + .singlepart(Self::singlepart_text_html(html)) + .singlepart(Self::singlepart_attachment(attachment)?), + ), + (Some(text), None, None) => { + message_builder.singlepart(Self::singlepart_text_plain(text)) + } + (None, Some(html), None) => { + message_builder.singlepart(Self::singlepart_text_html(html)) + } + (None, None, Some(attachment)) => { + message_builder.singlepart(Self::singlepart_attachment(attachment)?) + } + (None, None, None) => return Err(anyhow!("Missing email body")), + } + .context("Can't create MIME formatted email")?; + + Ok(Self { message }) + } + } + #[test] - fn test_mime_format_plaintext() { + fn test_mime_format_singlepart_plaintext() { let date_time = chrono::DateTime::parse_from_rfc3339("2024-01-01T14:00:00Z") .unwrap() .timestamp() as u64; @@ -155,18 +199,18 @@ mod tests { let text = "This is a test message (plaintext)."; let message = Message::new(subject, Some(text), None); - let res = MimeFormat::new(sender, receiver, &message, None, system_time, None); + let res = MimeFormat::new(sender, receiver, &message, None, system_time); assert!(res.is_ok()); let mime_format = format!("{:?}", res.unwrap()); - let mut expected_file = File::open("./test_data/email_plaintext.txt").unwrap(); + let mut expected_file = File::open("./test_data/email_singlepart_plaintext.txt").unwrap(); let mut expected_format = String::new(); expected_file.read_to_string(&mut expected_format).unwrap(); assert_eq!(mime_format.replace("\r", ""), expected_format); } #[test] - fn test_mime_format_html() { + fn test_mime_format_singlepart_html() { let date_time = chrono::DateTime::parse_from_rfc3339("2024-01-01T14:00:00Z") .unwrap() .timestamp() as u64; @@ -177,18 +221,40 @@ mod tests { let html = "

This is a test message (html).

"; let message = Message::new(subject, None, Some(html)); - let res = MimeFormat::new(sender, receiver, &message, None, system_time, None); + let res = MimeFormat::new(sender, receiver, &message, None, system_time); + assert!(res.is_ok()); + + let mime_format = format!("{:?}", res.unwrap()); + let mut expected_file = File::open("./test_data/email_singlepart_html.txt").unwrap(); + let mut expected_format = String::new(); + expected_file.read_to_string(&mut expected_format).unwrap(); + assert_eq!(mime_format.replace("\r", ""), expected_format); + } + + #[test] + fn test_mime_format_singlepart_attachment() { + let date_time = chrono::DateTime::parse_from_rfc3339("2024-01-01T14:00:00Z") + .unwrap() + .timestamp() as u64; + let system_time = UNIX_EPOCH + std::time::Duration::from_secs(date_time); + let sender = "albert@einstein.com"; + let receiver = "marie@curie.com"; + let subject = "Test Subject"; + let message = Message::new(subject, None, None); + let attachment = Path::new("./test_data/test.txt"); + + let res = MimeFormat::new(sender, receiver, &message, Some(attachment), system_time); assert!(res.is_ok()); let mime_format = format!("{:?}", res.unwrap()); - let mut expected_file = File::open("./test_data/email_html.txt").unwrap(); + let mut expected_file = File::open("./test_data/email_singlepart_attachment.txt").unwrap(); let mut expected_format = String::new(); expected_file.read_to_string(&mut expected_format).unwrap(); assert_eq!(mime_format.replace("\r", ""), expected_format); } #[test] - fn test_mime_format_multipart() { + fn test_mime_format_multipart_alternative() { let date_time = chrono::DateTime::parse_from_rfc3339("2024-01-01T14:00:00Z") .unwrap() .timestamp() as u64; @@ -199,20 +265,87 @@ mod tests { let text = "This is a test message (plaintext)."; let html = "

This is a test message (html).

"; let message = Message::new(subject, Some(text), Some(html)); - let multipart_boundary = "RZcCpBhV4GEzm8ETTVblOuzZ8bwGzGVyjkQfGTMt"; + let boundaries = vec!["RZcCpBhV4GEzm8ETTVblOuzZ8bwGzGVyjkQfGTMt"]; - let res = MimeFormat::new( + let res = MimeFormat::new_with_boundaries( sender, receiver, &message, None, system_time, - Some(multipart_boundary), + boundaries, + ); + assert!(res.is_ok()); + + let mime_format = format!("{:?}", res.unwrap()); + let mut expected_file = File::open("./test_data/email_multipart_alternative.txt").unwrap(); + let mut expected_format = String::new(); + expected_file.read_to_string(&mut expected_format).unwrap(); + assert_eq!(mime_format.replace("\r", ""), expected_format); + } + + #[test] + fn test_mime_format_multipart_mixed() { + let date_time = chrono::DateTime::parse_from_rfc3339("2024-01-01T14:00:00Z") + .unwrap() + .timestamp() as u64; + let system_time = UNIX_EPOCH + std::time::Duration::from_secs(date_time); + let sender = "albert@einstein.com"; + let receiver = "marie@curie.com"; + let subject = "Test Subject"; + let text = "This is a test message (plaintext)."; + let message = Message::new(subject, Some(text), None); + let boundaries = vec!["RZcCpBhV4GEzm8ETTVblOuzZ8bwGzGVyjkQfGTMt"]; + let attachment = Path::new("./test_data/test.txt"); + + let res = MimeFormat::new_with_boundaries( + sender, + receiver, + &message, + Some(attachment), + system_time, + boundaries, + ); + assert!(res.is_ok()); + + let mime_format = format!("{:?}", res.unwrap()); + let mut expected_file = File::open("./test_data/email_multipart_mixed.txt").unwrap(); + let mut expected_format = String::new(); + expected_file.read_to_string(&mut expected_format).unwrap(); + assert_eq!(mime_format.replace("\r", ""), expected_format); + } + + #[test] + fn test_mime_format_multipart_mixed_alternative() { + let date_time = chrono::DateTime::parse_from_rfc3339("2024-01-01T14:00:00Z") + .unwrap() + .timestamp() as u64; + let system_time = UNIX_EPOCH + std::time::Duration::from_secs(date_time); + let sender = "albert@einstein.com"; + let receiver = "marie@curie.com"; + let subject = "Test Subject"; + let text = "This is a test message (plaintext)."; + let html = "

This is a test message (html).

"; + let message = Message::new(subject, Some(text), Some(html)); + let boundaries = vec![ + "OTi56O3hPypBNfzLsCk053S1timfKY03AexmLpxU", + "HDKQ1fKhhPf7wLdMpdLSlteF05Rxv6VCIqIQf82I", + ]; + let attachment = Path::new("./test_data/test.txt"); + + let res = MimeFormat::new_with_boundaries( + sender, + receiver, + &message, + Some(attachment), + system_time, + boundaries, ); assert!(res.is_ok()); let mime_format = format!("{:?}", res.unwrap()); - let mut expected_file = File::open("./test_data/email_multipart.txt").unwrap(); + let mut expected_file = + File::open("./test_data/email_multipart_mixed_alternative.txt").unwrap(); let mut expected_format = String::new(); expected_file.read_to_string(&mut expected_format).unwrap(); assert_eq!(mime_format.replace("\r", ""), expected_format); diff --git a/test_data/email_multipart.txt b/test_data/email_multipart_alternative.txt similarity index 100% rename from test_data/email_multipart.txt rename to test_data/email_multipart_alternative.txt diff --git a/test_data/email_multipart_mixed.txt b/test_data/email_multipart_mixed.txt new file mode 100644 index 0000000..c5faf53 --- /dev/null +++ b/test_data/email_multipart_mixed.txt @@ -0,0 +1,20 @@ +From: albert@einstein.com +To: marie@curie.com +Subject: Test Subject +Date: Mon, 01 Jan 2024 14:00:00 +0000 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="RZcCpBhV4GEzm8ETTVblOuzZ8bwGzGVyjkQfGTMt" + +--RZcCpBhV4GEzm8ETTVblOuzZ8bwGzGVyjkQfGTMt +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: 7bit + +This is a test message (plaintext). +--RZcCpBhV4GEzm8ETTVblOuzZ8bwGzGVyjkQfGTMt +Content-Type: application/octet-stream +Content-Disposition: attachment; filename="test.txt" +Content-Transfer-Encoding: 7bit + +This is a test attachment. +--RZcCpBhV4GEzm8ETTVblOuzZ8bwGzGVyjkQfGTMt-- diff --git a/test_data/email_multipart_mixed_alternative.txt b/test_data/email_multipart_mixed_alternative.txt new file mode 100644 index 0000000..fa0d1cd --- /dev/null +++ b/test_data/email_multipart_mixed_alternative.txt @@ -0,0 +1,30 @@ +From: albert@einstein.com +To: marie@curie.com +Subject: Test Subject +Date: Mon, 01 Jan 2024 14:00:00 +0000 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="OTi56O3hPypBNfzLsCk053S1timfKY03AexmLpxU" + +--OTi56O3hPypBNfzLsCk053S1timfKY03AexmLpxU +Content-Type: multipart/alternative; + boundary="HDKQ1fKhhPf7wLdMpdLSlteF05Rxv6VCIqIQf82I" + +--HDKQ1fKhhPf7wLdMpdLSlteF05Rxv6VCIqIQf82I +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: 7bit + +This is a test message (plaintext). +--HDKQ1fKhhPf7wLdMpdLSlteF05Rxv6VCIqIQf82I +Content-Type: text/html; charset=utf-8 +Content-Transfer-Encoding: 7bit + +

This is a test message (html).

+--HDKQ1fKhhPf7wLdMpdLSlteF05Rxv6VCIqIQf82I-- +--OTi56O3hPypBNfzLsCk053S1timfKY03AexmLpxU +Content-Type: application/octet-stream +Content-Disposition: attachment; filename="test.txt" +Content-Transfer-Encoding: 7bit + +This is a test attachment. +--OTi56O3hPypBNfzLsCk053S1timfKY03AexmLpxU-- diff --git a/test_data/email_singlepart_attachment.txt b/test_data/email_singlepart_attachment.txt new file mode 100644 index 0000000..d287934 --- /dev/null +++ b/test_data/email_singlepart_attachment.txt @@ -0,0 +1,10 @@ +From: albert@einstein.com +To: marie@curie.com +Subject: Test Subject +Date: Mon, 01 Jan 2024 14:00:00 +0000 +MIME-Version: 1.0 +Content-Type: application/octet-stream +Content-Disposition: attachment; filename="test.txt" +Content-Transfer-Encoding: 7bit + +This is a test attachment. diff --git a/test_data/email_html.txt b/test_data/email_singlepart_html.txt similarity index 100% rename from test_data/email_html.txt rename to test_data/email_singlepart_html.txt diff --git a/test_data/email_plaintext.txt b/test_data/email_singlepart_plaintext.txt similarity index 100% rename from test_data/email_plaintext.txt rename to test_data/email_singlepart_plaintext.txt diff --git a/test_data/test.txt b/test_data/test.txt new file mode 100644 index 0000000..3eae1d0 --- /dev/null +++ b/test_data/test.txt @@ -0,0 +1 @@ +This is a test attachment. \ No newline at end of file diff --git a/tests/cmd/test_send.rs b/tests/cmd/test_send.rs index 05cbd76..bc99963 100644 --- a/tests/cmd/test_send.rs +++ b/tests/cmd/test_send.rs @@ -2,88 +2,6 @@ use assert_cmd::Command; use predicates::str; use std::env; -#[test] -fn test_send_aws_api_dry() { - println!("Execute 'pigeon send'"); - let mut cmd = Command::cargo_bin("pigeon").unwrap(); - cmd.args([ - "send", - "albert@einstein.com", - "marie@curie.com", - "--subject", - "Test Subject", - "--content", - "This is a test message (plaintext).", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - "--connection", - "aws", - ]); - cmd.assert().success().stdout(str::contains("abc")); -} - -#[test] -#[ignore] -fn test_send_subject_content_smtp() { - println!("Execute 'pigeon send'"); - let mut cmd = Command::cargo_bin("pigeon").unwrap(); - cmd.args([ - "send", - "--subject", - "Test Subject", - "--content", - "This is a test message (plaintext).", - "--display", - "--assume-yes", - "--archive", - ]); - cmd.assert().success().stdout(str::contains("abc")); -} - -#[test] -#[ignore] -fn test_send_message_file_smtp() { - let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); - let receiver = env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); - - println!("Execute 'pigeon send'"); - let mut cmd = Command::cargo_bin("pigeon").unwrap(); - cmd.args([ - "send", - &sender, - &receiver, - "--message-file", - "./test_data/message.yaml", - "--display", - "--assume-yes", - ]); - cmd.assert().success().stdout(str::contains("abc")); -} - -#[test] -#[ignore] -fn test_send_message_file_aws_api() { - let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); - let receiver = env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); - - println!("Execute 'pigeon send'"); - let mut cmd = Command::cargo_bin("pigeon").unwrap(); - cmd.args([ - "send", - &sender, - &receiver, - "--connection", - "aws", - "--message-file", - "./test_data/message.yaml", - "--display", - "--assume-yes", - ]); - cmd.assert().success().stdout(str::contains("abc")); -} - #[test] fn test_send_message_file_empty_smtp_dry() { println!("Execute 'pigeon send'"); @@ -178,71 +96,68 @@ fn test_archive_dir_smtp_dry() { } #[test] -#[ignore] -fn test_attachment_pdf_smtp_dry() { +fn test_send_aws_api_dry() { println!("Execute 'pigeon send'"); let mut cmd = Command::cargo_bin("pigeon").unwrap(); cmd.args([ "send", "albert@einstein.com", "marie@curie.com", - "--message-file", - "./test_data/message.yaml", + "--subject", + "Test Subject", + "--content", + "This is a test message (plaintext).", "--dry-run", "--display", "--assume-yes", "--archive", - "--attachment", - "./test_data/test.pdf", + "--connection", + "aws", ]); cmd.assert().success().stdout(str::contains("abc")); } #[test] #[ignore] -fn test_attachment_png_smtp_dry() { +fn test_send_subject_content_smtp() { println!("Execute 'pigeon send'"); let mut cmd = Command::cargo_bin("pigeon").unwrap(); cmd.args([ "send", - "albert@einstein.com", - "marie@curie.com", - "--message-file", - "./test_data/message.yaml", - "--dry-run", + "--subject", + "Test Subject", + "--content", + "This is a test message (plaintext).", "--display", "--assume-yes", "--archive", - "--attachment", - "./test_data/test.png", ]); cmd.assert().success().stdout(str::contains("abc")); } #[test] #[ignore] -fn test_attachment_odt_smtp_dry() { +fn test_send_message_file_smtp() { + let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); + let receiver = env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); + println!("Execute 'pigeon send'"); let mut cmd = Command::cargo_bin("pigeon").unwrap(); cmd.args([ "send", - "albert@einstein.com", - "marie@curie.com", + &sender, + &receiver, "--message-file", "./test_data/message.yaml", - "--dry-run", "--display", "--assume-yes", - "--archive", - "--attachment", - "./test_data/test.odt", ]); cmd.assert().success().stdout(str::contains("abc")); } #[test] #[ignore] -fn test_attachment_pdf_smtp() { +fn test_send_message_file_aws_api() { let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); let receiver = env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); @@ -252,20 +167,19 @@ fn test_attachment_pdf_smtp() { "send", &sender, &receiver, + "--connection", + "aws", "--message-file", "./test_data/message.yaml", "--display", "--assume-yes", - "--archive", - "--attachment", - "./test_data/test.pdf", ]); cmd.assert().success().stdout(str::contains("abc")); } #[test] #[ignore] -fn test_attachment_pdf_aws_api() { +fn test_attachment_pdf_smtp() { let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); let receiver = env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); @@ -275,8 +189,6 @@ fn test_attachment_pdf_aws_api() { "send", &sender, &receiver, - "--connection", - "aws", "--message-file", "./test_data/message.yaml", "--display", @@ -290,7 +202,7 @@ fn test_attachment_pdf_aws_api() { #[test] #[ignore] -fn test_send_aws_api() { +fn test_attachment_pdf_aws_api() { let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); let receiver = env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); @@ -300,80 +212,40 @@ fn test_send_aws_api() { "send", &sender, &receiver, - "--subject", - "Test Subject", - "--content", - "This is a test message (plaintext).", - "--display", - "--assume-yes", - "--archive", "--connection", "aws", - ]); - cmd.assert().success().stdout(str::contains("abc")); -} - -#[test] -#[ignore] -fn test_send_text_file_smtp_dry() { - println!("Execute 'pigeon send'"); - let mut cmd = Command::cargo_bin("pigeon").unwrap(); - cmd.args([ - "send", - "albert@einstein.com", - "marie@curie.com", - "--subject", - "Test Subject", - "--text-file", - "./test_data/message.txt", - "--dry-run", + "--message-file", + "./test_data/message.yaml", "--display", "--assume-yes", "--archive", + "--attachment", + "./test_data/test.pdf", ]); cmd.assert().success().stdout(str::contains("abc")); } #[test] #[ignore] -fn test_send_html_file_smtp_dry() { - println!("Execute 'pigeon send'"); - let mut cmd = Command::cargo_bin("pigeon").unwrap(); - cmd.args([ - "send", - "albert@einstein.com", - "marie@curie.com", - "--subject", - "Test Subject", - "--html-file", - "./test_data/message.html", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - ]); - cmd.assert().success().stdout(str::contains("abc")); -} +fn test_send_aws_api() { + let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); + let receiver = env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); -#[test] -#[ignore] -fn test_send_text_file_html_file_smtp_dry() { println!("Execute 'pigeon send'"); let mut cmd = Command::cargo_bin("pigeon").unwrap(); cmd.args([ "send", - "albert@einstein.com", - "marie@curie.com", + &sender, + &receiver, "--subject", "Test Subject", - "--text-file", - "./test_data/message.txt", - "--html-file", - "./test_data/message.html", - "--dry-run", + "--content", + "This is a test message (plaintext).", "--display", "--assume-yes", "--archive", + "--connection", + "aws", ]); cmd.assert().success().stdout(str::contains("abc")); } From 647245ad8196bdd883b17df0e7a2d39164cf3bdf Mon Sep 17 00:00:00 2001 From: quambene Date: Wed, 27 Mar 2024 03:01:53 +0100 Subject: [PATCH 23/54] Test message from args --- src/cmd/send.rs | 2 +- src/cmd/send_bulk.rs | 2 +- src/email_builder/message.rs | 302 ++++++++++++++++++ src/email_builder/message/mod.rs | 125 -------- src/email_builder/message/reader.rs | 38 --- .../{message => }/message_template.rs | 0 src/email_builder/mod.rs | 4 +- ...{empty_message.yaml => message_empty.yaml} | 0 ...nt_none_message.yaml => message_none.yaml} | 0 test_data/none_html_message.yaml | 7 - tests/cmd/test_send.rs | 200 ++---------- 11 files changed, 327 insertions(+), 353 deletions(-) create mode 100644 src/email_builder/message.rs delete mode 100644 src/email_builder/message/mod.rs delete mode 100644 src/email_builder/message/reader.rs rename src/email_builder/{message => }/message_template.rs (100%) rename test_data/{empty_message.yaml => message_empty.yaml} (100%) rename test_data/{content_none_message.yaml => message_none.yaml} (100%) delete mode 100644 test_data/none_html_message.yaml diff --git a/src/cmd/send.rs b/src/cmd/send.rs index 8ba7975..190c63d 100644 --- a/src/cmd/send.rs +++ b/src/cmd/send.rs @@ -17,7 +17,7 @@ pub fn send(matches: &ArgMatches) -> Result<(), anyhow::Error> { let now = SystemTime::now(); let sender = Sender::init(matches)?; let receiver = Receiver::init(matches)?; - let message = Message::build(matches)?; + let message = Message::from_args(matches)?; let attachment = matches .value_of(arg::ATTACHMENT) .map(|attachment| Path::new(attachment)); diff --git a/src/cmd/send_bulk.rs b/src/cmd/send_bulk.rs index 4b4a51c..1e64e69 100644 --- a/src/cmd/send_bulk.rs +++ b/src/cmd/send_bulk.rs @@ -14,7 +14,7 @@ pub fn send_bulk(matches: &ArgMatches) -> Result<(), anyhow::Error> { let sender = Sender::init(matches)?; let df_receiver = Receiver::dataframe(matches)?; - let default_message = Message::build(matches)?; + let default_message = Message::from_args(matches)?; let bulk_email = if matches.is_present(arg::PERSONALIZE) { match matches.values_of(arg::PERSONALIZE) { Some(personalized_columns) => BulkEmail::personalize( diff --git a/src/email_builder/message.rs b/src/email_builder/message.rs new file mode 100644 index 0000000..c1506c3 --- /dev/null +++ b/src/email_builder/message.rs @@ -0,0 +1,302 @@ +use crate::{arg, data_loader::TabularData}; +use anyhow::{anyhow, Result}; +use clap::ArgMatches; +use polars::prelude::DataFrame; +use std::{fs, path::Path}; + +use super::MessageTemplate; + +#[derive(Debug, Clone, PartialEq)] +pub struct Message { + pub subject: String, + pub text: Option, + pub html: Option, +} + +impl Message { + pub fn new(subject: S, text: Option, html: Option) -> Self + where + S: Into, + { + Self { + subject: subject.into(), + text: text.map(|text| text.into()), + html: html.map(|text| text.into()), + } + } + + pub fn from_args(matches: &ArgMatches) -> Result { + let message = if matches.is_present(arg::SUBJECT) && matches.is_present(arg::CONTENT) { + Message::from_cmd(matches)? + } else if matches.is_present(arg::MESSAGE_FILE) { + Message::from_template(matches)? + } else if matches.is_present(arg::SUBJECT) + && (matches.is_present(arg::TEXT_FILE) || matches.is_present(arg::HTML_FILE)) + { + Message::from_file(matches)? + } else { + return Err(anyhow!( + "Missing arguments. Please provide {} and {} or {}", + arg::SUBJECT, + arg::CONTENT, + arg::MESSAGE_FILE, + )); + }; + + Ok(message) + } + + pub fn from_file(matches: &ArgMatches) -> Result { + let subject = Message::subject(matches)?.to_string(); + let text = Self::read(matches, arg::TEXT_FILE)?; + let html = Self::read(matches, arg::HTML_FILE)?; + let message = Message::new(subject, text, html); + Ok(message) + } + + pub fn from_template(matches: &ArgMatches) -> Result { + let message_template = MessageTemplate::read(matches)?; + let message = Message::new( + message_template.subject, + message_template.text, + message_template.html, + ); + Ok(message) + } + + pub fn from_cmd(matches: &ArgMatches) -> Result { + match ( + matches.value_of(arg::SUBJECT), + matches.value_of(arg::CONTENT), + ) { + (Some(subject), Some(content)) => { + let message = Message::new(subject, Some(content), None); + Ok(message) + } + (Some(_), None) => Err(anyhow!("Missing value for argument '{}'", arg::CONTENT)), + (None, Some(_)) => Err(anyhow!("Missing value for argument '{}'", arg::SUBJECT)), + (None, None) => Err(anyhow!( + "Missing values for '{}' and '{}'", + arg::SUBJECT, + arg::CONTENT + )), + } + } + + pub fn personalize( + &mut self, + index: usize, + df_receiver: &DataFrame, + columns: &[&str], + ) -> Result<(), anyhow::Error> { + for &col_name in columns.iter() { + let col_value = TabularData::row(index, col_name, df_receiver)?; + self.replace(col_name, col_value); + } + + Ok(()) + } + + fn replace(&mut self, col_name: &str, col_value: &str) { + self.subject = self + .subject + .replace(&format!("{{{}}}", col_name), col_value); + self.text = self + .text + .as_ref() + .map(|text| text.replace(&format!("{{{}}}", col_name), col_value)); + self.html = self + .html + .as_ref() + .map(|html| html.replace(&format!("{{{}}}", col_name), col_value)); + } + + fn subject<'a>(matches: &'a ArgMatches) -> Result<&'a str, anyhow::Error> { + if matches.is_present(arg::SUBJECT) { + match matches.value_of(arg::SUBJECT) { + Some(subject) => Ok(subject), + None => Err(anyhow!("Missing value for argument '{}'", arg::SUBJECT)), + } + } else { + Err(anyhow!("Missing argument '{}'", arg::SUBJECT)) + } + } + + fn read(matches: &ArgMatches, arg: &str) -> Result, anyhow::Error> { + if matches.is_present(arg) { + match matches.value_of(arg) { + Some(text_file) => { + let path = Path::new(text_file); + println!("Reading text file '{}' ...", path.display()); + let message = fs::read_to_string(path)?; + + if matches.is_present(arg::DISPLAY) { + println!("Display message file: {:#?}", message); + } + + Ok(Some(message)) + } + None => Err(anyhow!("Missing value for argument '{}'", arg)), + } + } else { + Ok(None) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::app; + + #[test] + fn test_message_from_args_subject_content() { + let args = vec![ + "pigeon", + "send", + "albert@einstein.com", + "marie@curie.com", + "--subject", + "Test subject", + "--content", + "This is a test message (plaintext).", + ]; + let app = app(); + let matches = app.get_matches_from(args); + let subcommand_matches = matches.subcommand_matches("send").unwrap(); + + let res = Message::from_args(&subcommand_matches); + assert!(res.is_ok(), "{}", res.unwrap_err()); + + let message = res.unwrap(); + assert_eq!( + message, + Message { + subject: "Test subject".to_owned(), + text: Some("This is a test message (plaintext).".to_owned()), + html: None, + } + ); + } + + #[test] + fn test_message_from_args_text_and_html_file() { + let args = vec![ + "pigeon", + "send", + "albert@einstein.com", + "marie@curie.com", + "--subject", + "Test subject", + "--text-file", + "./test_data/message.txt", + "--html-file", + "./test_data/message.html", + ]; + let app = app(); + let matches = app.get_matches_from(args); + let subcommand_matches = matches.subcommand_matches("send").unwrap(); + + let res = Message::from_args(&subcommand_matches); + assert!(res.is_ok(), "{}", res.unwrap_err()); + + let message = res.unwrap(); + assert_eq!( + message, + Message { + subject: "Test subject".to_owned(), + text: Some( + "This is a test message (plaintext).\n\nThis is the last line.".to_owned() + ), + html: Some( + "

This is a test message (html).

\n\n

This is the last line.

" + .to_owned() + ), + } + ); + } + + #[test] + fn test_message_from_args_message_file() { + let args = vec![ + "pigeon", + "send", + "albert@einstein.com", + "marie@curie.com", + "--message-file", + "./test_data/message.yaml", + ]; + let app = app(); + let matches = app.get_matches_from(args); + let subcommand_matches = matches.subcommand_matches("send").unwrap(); + + let res = Message::from_args(&subcommand_matches); + assert!(res.is_ok(), "{}", res.unwrap_err()); + + let message = res.unwrap(); + assert_eq!( + message, + Message { + subject: "Test subject".to_owned(), + text: Some("This is a test message (plaintext).".to_owned()), + html: Some("

This is a test message (html).

".to_owned()), + } + ); + } + + #[test] + fn test_message_from_args_message_file_empty() { + let args = vec![ + "pigeon", + "send", + "albert@einstein.com", + "marie@curie.com", + "--message-file", + "./test_data/message_empty.yaml", + ]; + let app = app(); + let matches = app.get_matches_from(args); + let subcommand_matches = matches.subcommand_matches("send").unwrap(); + + let res = Message::from_args(&subcommand_matches); + assert!(res.is_ok(), "{}", res.unwrap_err()); + + let message = res.unwrap(); + assert_eq!( + message, + Message { + subject: "Test subject".to_owned(), + text: Some("".to_owned()), + html: Some("".to_owned()), + } + ); + } + + #[test] + fn test_message_from_args_message_none() { + let args = vec![ + "pigeon", + "send", + "albert@einstein.com", + "marie@curie.com", + "--message-file", + "./test_data/message_none.yaml", + ]; + let app = app(); + let matches = app.get_matches_from(args); + let subcommand_matches = matches.subcommand_matches("send").unwrap(); + + let res = Message::from_args(&subcommand_matches); + assert!(res.is_ok(), "{}", res.unwrap_err()); + + let message = res.unwrap(); + assert_eq!( + message, + Message { + subject: "Test subject".to_owned(), + text: None, + html: None, + } + ); + } +} diff --git a/src/email_builder/message/mod.rs b/src/email_builder/message/mod.rs deleted file mode 100644 index 78cf288..0000000 --- a/src/email_builder/message/mod.rs +++ /dev/null @@ -1,125 +0,0 @@ -mod message_template; -mod reader; - -pub use self::{message_template::MessageTemplate, reader::Reader}; -use crate::{arg, data_loader::TabularData}; -use anyhow::{anyhow, Result}; -use clap::ArgMatches; -use polars::prelude::DataFrame; - -#[derive(Debug, Clone)] -pub struct Message { - pub subject: String, - pub text: Option, - pub html: Option, -} - -impl Message { - pub fn new(subject: S, text: Option, html: Option) -> Self - where - S: Into, - { - Self { - subject: subject.into(), - text: text.map(|text| text.into()), - html: html.map(|text| text.into()), - } - } - - pub fn build(matches: &ArgMatches) -> Result { - let message = if matches.is_present(arg::SUBJECT) && matches.is_present(arg::CONTENT) { - Message::from_cmd(matches)? - } else if matches.is_present(arg::MESSAGE_FILE) { - Message::from_template(matches)? - } else if matches.is_present(arg::SUBJECT) - && (matches.is_present(arg::TEXT_FILE) || matches.is_present(arg::HTML_FILE)) - { - Message::from_file(matches)? - } else { - return Err(anyhow!( - "Missing arguments. Please provide {} and {} or {}", - arg::SUBJECT, - arg::CONTENT, - arg::MESSAGE_FILE, - )); - }; - - Ok(message) - } - - pub fn from_file(matches: &ArgMatches) -> Result { - let subject = Message::subject(matches)?.to_string(); - let text = Reader::read_txt(matches)?; - let html = Reader::read_html(matches)?; - let message = Message::new(subject, text, html); - Ok(message) - } - - pub fn from_template(matches: &ArgMatches) -> Result { - let message_template = MessageTemplate::read(matches)?; - let message = Message::new( - message_template.subject, - message_template.text, - message_template.html, - ); - Ok(message) - } - - pub fn from_cmd(matches: &ArgMatches) -> Result { - match ( - matches.value_of(arg::SUBJECT), - matches.value_of(arg::CONTENT), - ) { - (Some(subject), Some(content)) => { - let message = Message::new(subject, Some(content), None); - Ok(message) - } - (Some(_), None) => Err(anyhow!("Missing value for argument '{}'", arg::CONTENT)), - (None, Some(_)) => Err(anyhow!("Missing value for argument '{}'", arg::SUBJECT)), - (None, None) => Err(anyhow!( - "Missing values for '{}' and '{}'", - arg::SUBJECT, - arg::CONTENT - )), - } - } - - pub fn personalize( - &mut self, - index: usize, - df_receiver: &DataFrame, - columns: &[&str], - ) -> Result<(), anyhow::Error> { - for &col_name in columns.iter() { - let col_value = TabularData::row(index, col_name, df_receiver)?; - self.replace(col_name, col_value); - } - - Ok(()) - } - - fn replace(&mut self, col_name: &str, col_value: &str) { - self.subject = self - .subject - .replace(&format!("{{{}}}", col_name), col_value); - self.text = self - .text - .as_ref() - .map(|text| text.replace(&format!("{{{}}}", col_name), col_value)); - self.html = self - .html - .as_ref() - .map(|html| html.replace(&format!("{{{}}}", col_name), col_value)); - } - - fn subject<'a>(matches: &'a ArgMatches) -> Result<&'a str, anyhow::Error> { - if matches.is_present(arg::SUBJECT) { - match matches.value_of(arg::SUBJECT) { - Some(subject) => Ok(subject), - None => Err(anyhow!("Missing value for argument '{}'", arg::SUBJECT)), - } - } else { - Err(anyhow!("Missing argument '{}'", arg::SUBJECT)) - } - } -} diff --git a/src/email_builder/message/reader.rs b/src/email_builder/message/reader.rs deleted file mode 100644 index 96915c0..0000000 --- a/src/email_builder/message/reader.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::arg; -use anyhow::anyhow; -use clap::ArgMatches; -use std::{fs, path::Path}; - -#[derive(Debug)] -pub struct Reader; - -impl Reader { - pub fn read_txt(matches: &ArgMatches) -> Result, anyhow::Error> { - Self::read(matches, arg::TEXT_FILE) - } - - pub fn read_html(matches: &ArgMatches) -> Result, anyhow::Error> { - Self::read(matches, arg::HTML_FILE) - } - - fn read(matches: &ArgMatches, arg: &str) -> Result, anyhow::Error> { - if matches.is_present(arg) { - match matches.value_of(arg) { - Some(text_file) => { - let path = Path::new(text_file); - println!("Reading text file '{}' ...", path.display()); - let message = fs::read_to_string(path)?; - - if matches.is_present(arg::DISPLAY) { - println!("Display message file: {:#?}", message); - } - - Ok(Some(message)) - } - None => Err(anyhow!("Missing value for argument '{}'", arg)), - } - } else { - Ok(None) - } - } -} diff --git a/src/email_builder/message/message_template.rs b/src/email_builder/message_template.rs similarity index 100% rename from src/email_builder/message/message_template.rs rename to src/email_builder/message_template.rs diff --git a/src/email_builder/mod.rs b/src/email_builder/mod.rs index 5d6945c..087d7bc 100644 --- a/src/email_builder/mod.rs +++ b/src/email_builder/mod.rs @@ -1,13 +1,15 @@ mod bulk_email; mod email; mod message; +mod message_template; mod mime; mod receiver; mod sender; pub use bulk_email::BulkEmail; pub use email::Email; -pub use message::{Message, MessageTemplate}; +pub use message::Message; +pub use message_template::MessageTemplate; pub use mime::MimeFormat; pub use receiver::Receiver; pub use sender::Sender; diff --git a/test_data/empty_message.yaml b/test_data/message_empty.yaml similarity index 100% rename from test_data/empty_message.yaml rename to test_data/message_empty.yaml diff --git a/test_data/content_none_message.yaml b/test_data/message_none.yaml similarity index 100% rename from test_data/content_none_message.yaml rename to test_data/message_none.yaml diff --git a/test_data/none_html_message.yaml b/test_data/none_html_message.yaml deleted file mode 100644 index df81603..0000000 --- a/test_data/none_html_message.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# Specify the subject, plaintext and html version of your email. -# Personalize message by wrapping variables in curly brackets, eg. {first_name}. - -# The subject of your email -subject: "Test subject" -# The plaintext version -text: "" diff --git a/tests/cmd/test_send.rs b/tests/cmd/test_send.rs index bc99963..a5a92c7 100644 --- a/tests/cmd/test_send.rs +++ b/tests/cmd/test_send.rs @@ -2,177 +2,67 @@ use assert_cmd::Command; use predicates::str; use std::env; -#[test] -fn test_send_message_file_empty_smtp_dry() { - println!("Execute 'pigeon send'"); - let mut cmd = Command::cargo_bin("pigeon").unwrap(); - cmd.args([ - "send", - "albert@einstein.com", - "marie@curie.com", - "--message-file", - "./test_data/empty_message.yaml", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - ]); - cmd.assert().success().stdout(str::contains("abc")); -} - #[test] #[ignore] -fn test_send_message_file_none_html_smtp_dry() { - println!("Execute 'pigeon send'"); - let mut cmd = Command::cargo_bin("pigeon").unwrap(); - cmd.args([ - "send", - "albert@einstein.com", - "marie@curie.com", - "--message-file", - "./test_data/none_html_message.yaml", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - ]); - cmd.assert().success().stdout(str::contains("abc")); -} - -#[test] -fn test_send_message_file_content_none_smtp_dry() { - println!("Execute 'pigeon send'"); - let mut cmd = Command::cargo_bin("pigeon").unwrap(); - cmd.args([ - "send", - "albert@einstein.com", - "marie@curie.com", - "--message-file", - "./test_data/content_none_message.yaml", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - ]); - cmd.assert().success().stdout(str::contains("abc")); -} - -#[test] -fn test_archive_smtp_dry() { - println!("Execute 'pigeon send'"); - let mut cmd = Command::cargo_bin("pigeon").unwrap(); - cmd.args([ - "send", - "albert@einstein.com", - "marie@curie.com", - "--message-file", - "./test_data/message.yaml", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - ]); - cmd.assert().success().stdout(str::contains("abc")); -} - -#[test] -fn test_archive_dir_smtp_dry() { - println!("Execute 'pigeon send'"); - let mut cmd = Command::cargo_bin("pigeon").unwrap(); - cmd.args([ - "send", - "albert@einstein.com", - "marie@curie.com", - "--message-file", - "./test_data/message.yaml", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - "--archive-dir", - "./my-sent-emails", - ]); - cmd.assert().success().stdout(str::contains("abc")); -} - -#[test] -fn test_send_aws_api_dry() { - println!("Execute 'pigeon send'"); +fn test_send_smtp() { + println!("Execute 'pigeon send --connection smtp'"); let mut cmd = Command::cargo_bin("pigeon").unwrap(); cmd.args([ "send", - "albert@einstein.com", - "marie@curie.com", "--subject", "Test Subject", "--content", "This is a test message (plaintext).", - "--dry-run", "--display", "--assume-yes", "--archive", "--connection", - "aws", - ]); - cmd.assert().success().stdout(str::contains("abc")); -} - -#[test] -#[ignore] -fn test_send_subject_content_smtp() { - println!("Execute 'pigeon send'"); - let mut cmd = Command::cargo_bin("pigeon").unwrap(); - cmd.args([ - "send", - "--subject", - "Test Subject", - "--content", - "This is a test message (plaintext).", - "--display", - "--assume-yes", - "--archive", + "smtp", ]); cmd.assert().success().stdout(str::contains("abc")); } #[test] #[ignore] -fn test_send_message_file_smtp() { +fn test_send_aws() { let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); let receiver = env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); - println!("Execute 'pigeon send'"); + println!("Execute 'pigeon send --connection aws ...'"); let mut cmd = Command::cargo_bin("pigeon").unwrap(); cmd.args([ "send", &sender, &receiver, - "--message-file", - "./test_data/message.yaml", + "--subject", + "Test Subject", + "--content", + "This is a test message (plaintext).", "--display", "--assume-yes", + "--archive", + "--connection", + "aws", ]); cmd.assert().success().stdout(str::contains("abc")); } #[test] -#[ignore] -fn test_send_message_file_aws_api() { - let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); - let receiver = env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); - +fn test_archive_dir_smtp_dry() { println!("Execute 'pigeon send'"); let mut cmd = Command::cargo_bin("pigeon").unwrap(); cmd.args([ "send", - &sender, - &receiver, - "--connection", - "aws", + "albert@einstein.com", + "marie@curie.com", "--message-file", "./test_data/message.yaml", + "--dry-run", "--display", "--assume-yes", + "--archive", + "--archive-dir", + "./my-sent-emails", ]); cmd.assert().success().stdout(str::contains("abc")); } @@ -202,7 +92,7 @@ fn test_attachment_pdf_smtp() { #[test] #[ignore] -fn test_attachment_pdf_aws_api() { +fn test_attachment_pdf_aws() { let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); let receiver = env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); @@ -224,53 +114,3 @@ fn test_attachment_pdf_aws_api() { ]); cmd.assert().success().stdout(str::contains("abc")); } - -#[test] -#[ignore] -fn test_send_aws_api() { - let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); - let receiver = env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); - - println!("Execute 'pigeon send'"); - let mut cmd = Command::cargo_bin("pigeon").unwrap(); - cmd.args([ - "send", - &sender, - &receiver, - "--subject", - "Test Subject", - "--content", - "This is a test message (plaintext).", - "--display", - "--assume-yes", - "--archive", - "--connection", - "aws", - ]); - cmd.assert().success().stdout(str::contains("abc")); -} - -#[test] -#[ignore] -fn test_send_text_file_html_file_smtp() { - let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); - let receiver = env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); - - println!("Execute 'pigeon send'"); - let mut cmd = Command::cargo_bin("pigeon").unwrap(); - cmd.args([ - "send", - &sender, - &receiver, - "--subject", - "Test Subject", - "--text-file", - "./test_data/message.txt", - "--html-file", - "./test_data/message.html", - "--display", - "--assume-yes", - "--archive", - ]); - cmd.assert().success().stdout(str::contains("abc")); -} From 810669c0dab4b83965ef532fcf3ad2c0b789c6a0 Mon Sep 17 00:00:00 2001 From: quambene Date: Thu, 28 Mar 2024 01:01:24 +0100 Subject: [PATCH 24/54] Refactor bulk email --- src/cmd/send.rs | 4 +--- src/cmd/send_bulk.rs | 23 +++++++++++++++++------ src/email_builder/bulk_email.rs | 17 +++++------------ src/email_builder/message.rs | 10 +++++----- src/email_builder/mime.rs | 12 ++++++------ 5 files changed, 34 insertions(+), 32 deletions(-) diff --git a/src/cmd/send.rs b/src/cmd/send.rs index 190c63d..8187d0e 100644 --- a/src/cmd/send.rs +++ b/src/cmd/send.rs @@ -18,9 +18,7 @@ pub fn send(matches: &ArgMatches) -> Result<(), anyhow::Error> { let sender = Sender::init(matches)?; let receiver = Receiver::init(matches)?; let message = Message::from_args(matches)?; - let attachment = matches - .value_of(arg::ATTACHMENT) - .map(|attachment| Path::new(attachment)); + let attachment = matches.value_of(arg::ATTACHMENT).map(Path::new); let mime_format = MimeFormat::new(sender, receiver, &message, attachment, now)?; let email = Email::new(sender, receiver, &message, &mime_format)?; diff --git a/src/cmd/send_bulk.rs b/src/cmd/send_bulk.rs index 1e64e69..4225eea 100644 --- a/src/cmd/send_bulk.rs +++ b/src/cmd/send_bulk.rs @@ -6,6 +6,7 @@ use crate::{ use anyhow::anyhow; use anyhow::Result; use clap::ArgMatches; +use std::path::Path; pub fn send_bulk(matches: &ArgMatches) -> Result<(), anyhow::Error> { if matches.is_present(arg::VERBOSE) { @@ -13,21 +14,31 @@ pub fn send_bulk(matches: &ArgMatches) -> Result<(), anyhow::Error> { } let sender = Sender::init(matches)?; + let receiver_column_name = Receiver::column_name(matches)?; let df_receiver = Receiver::dataframe(matches)?; let default_message = Message::from_args(matches)?; + let attachment = matches.value_of(arg::ATTACHMENT).map(Path::new); let bulk_email = if matches.is_present(arg::PERSONALIZE) { - match matches.values_of(arg::PERSONALIZE) { - Some(personalized_columns) => BulkEmail::personalize( - matches, + if let Some(personalized_columns) = matches.values_of(arg::PERSONALIZE) { + BulkEmail::personalize( sender, + receiver_column_name, &df_receiver, &default_message, personalized_columns, - )?, - None => return Err(anyhow!("Missing value for argument '{}'", arg::PERSONALIZE)), + attachment, + )? + } else { + return Err(anyhow!("Missing value for argument '{}'", arg::PERSONALIZE)); } } else { - BulkEmail::new(matches, sender, &df_receiver, &default_message)? + BulkEmail::new( + sender, + receiver_column_name, + &df_receiver, + &default_message, + attachment, + )? }; if matches.is_present(arg::DISPLAY) { diff --git a/src/email_builder/bulk_email.rs b/src/email_builder/bulk_email.rs index 31794cc..e831513 100644 --- a/src/email_builder/bulk_email.rs +++ b/src/email_builder/bulk_email.rs @@ -1,7 +1,7 @@ use crate::{ arg, data_loader::TabularData, - email_builder::{Confirmed, Email, Message, MimeFormat, Receiver}, + email_builder::{Confirmed, Email, Message, MimeFormat}, email_formatter::EmlFormatter, email_transmission::Client, }; @@ -17,23 +17,19 @@ pub struct BulkEmail<'a> { impl<'a> BulkEmail<'a> { pub fn new( - matches: &'a ArgMatches, sender: &'a str, - + receiver_column_name: &str, df_receiver: &'a DataFrame, message: &'a Message, + attachment: Option<&Path>, ) -> Result { let now = SystemTime::now(); let mut emails: Vec = vec![]; - let receiver_column_name = Receiver::column_name(matches)?; let receivers = TabularData::column(receiver_column_name, df_receiver)?; for receiver in receivers { match receiver { Some(receiver) => { - let attachment = matches - .value_of(arg::ATTACHMENT) - .map(|attachment| Path::new(attachment)); let mime_format = MimeFormat::new(sender, receiver, message, attachment, now)?; let email = Email::new(sender, receiver, message, &mime_format)?; emails.push(email); @@ -46,25 +42,22 @@ impl<'a> BulkEmail<'a> { } pub fn personalize( - matches: &ArgMatches, sender: &'a str, + receiver_column_name: &str, df_receiver: &'a DataFrame, default_message: &Message, personalized_columns: Values, + attachment: Option<&Path>, ) -> Result { let now = SystemTime::now(); let mut emails: Vec = vec![]; let columns: Vec<&str> = personalized_columns.collect(); - let receiver_column_name = Receiver::column_name(matches)?; for i in 0..df_receiver.height() { let mut message = default_message.clone(); message.personalize(i, df_receiver, &columns)?; let receiver = TabularData::row(i, receiver_column_name, df_receiver)?; - let attachment = matches - .value_of(arg::ATTACHMENT) - .map(|attachment| Path::new(attachment)); let mime_format = MimeFormat::new(sender, receiver, &message, attachment, now)?; let email = Email::new(sender, receiver, &message, &mime_format)?; diff --git a/src/email_builder/message.rs b/src/email_builder/message.rs index c1506c3..832e8e5 100644 --- a/src/email_builder/message.rs +++ b/src/email_builder/message.rs @@ -165,7 +165,7 @@ mod tests { let matches = app.get_matches_from(args); let subcommand_matches = matches.subcommand_matches("send").unwrap(); - let res = Message::from_args(&subcommand_matches); + let res = Message::from_args(subcommand_matches); assert!(res.is_ok(), "{}", res.unwrap_err()); let message = res.unwrap(); @@ -197,7 +197,7 @@ mod tests { let matches = app.get_matches_from(args); let subcommand_matches = matches.subcommand_matches("send").unwrap(); - let res = Message::from_args(&subcommand_matches); + let res = Message::from_args(subcommand_matches); assert!(res.is_ok(), "{}", res.unwrap_err()); let message = res.unwrap(); @@ -230,7 +230,7 @@ mod tests { let matches = app.get_matches_from(args); let subcommand_matches = matches.subcommand_matches("send").unwrap(); - let res = Message::from_args(&subcommand_matches); + let res = Message::from_args(subcommand_matches); assert!(res.is_ok(), "{}", res.unwrap_err()); let message = res.unwrap(); @@ -258,7 +258,7 @@ mod tests { let matches = app.get_matches_from(args); let subcommand_matches = matches.subcommand_matches("send").unwrap(); - let res = Message::from_args(&subcommand_matches); + let res = Message::from_args(subcommand_matches); assert!(res.is_ok(), "{}", res.unwrap_err()); let message = res.unwrap(); @@ -286,7 +286,7 @@ mod tests { let matches = app.get_matches_from(args); let subcommand_matches = matches.subcommand_matches("send").unwrap(); - let res = Message::from_args(&subcommand_matches); + let res = Message::from_args(subcommand_matches); assert!(res.is_ok(), "{}", res.unwrap_err()); let message = res.unwrap(); diff --git a/src/email_builder/mime.rs b/src/email_builder/mime.rs index 56f8aee..6cfec72 100644 --- a/src/email_builder/mime.rs +++ b/src/email_builder/mime.rs @@ -206,7 +206,7 @@ mod tests { let mut expected_file = File::open("./test_data/email_singlepart_plaintext.txt").unwrap(); let mut expected_format = String::new(); expected_file.read_to_string(&mut expected_format).unwrap(); - assert_eq!(mime_format.replace("\r", ""), expected_format); + assert_eq!(mime_format.replace('\r', ""), expected_format); } #[test] @@ -228,7 +228,7 @@ mod tests { let mut expected_file = File::open("./test_data/email_singlepart_html.txt").unwrap(); let mut expected_format = String::new(); expected_file.read_to_string(&mut expected_format).unwrap(); - assert_eq!(mime_format.replace("\r", ""), expected_format); + assert_eq!(mime_format.replace('\r', ""), expected_format); } #[test] @@ -250,7 +250,7 @@ mod tests { let mut expected_file = File::open("./test_data/email_singlepart_attachment.txt").unwrap(); let mut expected_format = String::new(); expected_file.read_to_string(&mut expected_format).unwrap(); - assert_eq!(mime_format.replace("\r", ""), expected_format); + assert_eq!(mime_format.replace('\r', ""), expected_format); } #[test] @@ -281,7 +281,7 @@ mod tests { let mut expected_file = File::open("./test_data/email_multipart_alternative.txt").unwrap(); let mut expected_format = String::new(); expected_file.read_to_string(&mut expected_format).unwrap(); - assert_eq!(mime_format.replace("\r", ""), expected_format); + assert_eq!(mime_format.replace('\r', ""), expected_format); } #[test] @@ -312,7 +312,7 @@ mod tests { let mut expected_file = File::open("./test_data/email_multipart_mixed.txt").unwrap(); let mut expected_format = String::new(); expected_file.read_to_string(&mut expected_format).unwrap(); - assert_eq!(mime_format.replace("\r", ""), expected_format); + assert_eq!(mime_format.replace('\r', ""), expected_format); } #[test] @@ -348,6 +348,6 @@ mod tests { File::open("./test_data/email_multipart_mixed_alternative.txt").unwrap(); let mut expected_format = String::new(); expected_file.read_to_string(&mut expected_format).unwrap(); - assert_eq!(mime_format.replace("\r", ""), expected_format); + assert_eq!(mime_format.replace('\r', ""), expected_format); } } From fabb2a187e858f447afd32a26453b7d773ec123a Mon Sep 17 00:00:00 2001 From: quambene Date: Thu, 28 Mar 2024 01:20:17 +0100 Subject: [PATCH 25/54] Refactor sender --- src/cmd/send.rs | 4 ++-- src/cmd/send_bulk.rs | 4 ++-- src/email_builder/bulk_email.rs | 9 +++++---- src/email_builder/email.rs | 5 +++-- src/email_builder/mime.rs | 17 +++++++++-------- src/email_builder/receiver.rs | 1 + src/email_builder/sender.rs | 9 +++++---- src/email_transmission/sent_email.rs | 4 ++-- 8 files changed, 29 insertions(+), 24 deletions(-) diff --git a/src/cmd/send.rs b/src/cmd/send.rs index 8187d0e..aa7715e 100644 --- a/src/cmd/send.rs +++ b/src/cmd/send.rs @@ -19,8 +19,8 @@ pub fn send(matches: &ArgMatches) -> Result<(), anyhow::Error> { let receiver = Receiver::init(matches)?; let message = Message::from_args(matches)?; let attachment = matches.value_of(arg::ATTACHMENT).map(Path::new); - let mime_format = MimeFormat::new(sender, receiver, &message, attachment, now)?; - let email = Email::new(sender, receiver, &message, &mime_format)?; + let mime_format = MimeFormat::new(&sender, receiver, &message, attachment, now)?; + let email = Email::new(&sender, receiver, &message, &mime_format)?; if matches.is_present(arg::DISPLAY) { println!("Display email: {:#?}", email); diff --git a/src/cmd/send_bulk.rs b/src/cmd/send_bulk.rs index 4225eea..8fbf91e 100644 --- a/src/cmd/send_bulk.rs +++ b/src/cmd/send_bulk.rs @@ -21,7 +21,7 @@ pub fn send_bulk(matches: &ArgMatches) -> Result<(), anyhow::Error> { let bulk_email = if matches.is_present(arg::PERSONALIZE) { if let Some(personalized_columns) = matches.values_of(arg::PERSONALIZE) { BulkEmail::personalize( - sender, + &sender, receiver_column_name, &df_receiver, &default_message, @@ -33,7 +33,7 @@ pub fn send_bulk(matches: &ArgMatches) -> Result<(), anyhow::Error> { } } else { BulkEmail::new( - sender, + &sender, receiver_column_name, &df_receiver, &default_message, diff --git a/src/email_builder/bulk_email.rs b/src/email_builder/bulk_email.rs index e831513..20ea003 100644 --- a/src/email_builder/bulk_email.rs +++ b/src/email_builder/bulk_email.rs @@ -1,3 +1,4 @@ +use super::Sender; use crate::{ arg, data_loader::TabularData, @@ -17,7 +18,7 @@ pub struct BulkEmail<'a> { impl<'a> BulkEmail<'a> { pub fn new( - sender: &'a str, + sender: &'a Sender, receiver_column_name: &str, df_receiver: &'a DataFrame, message: &'a Message, @@ -30,8 +31,8 @@ impl<'a> BulkEmail<'a> { for receiver in receivers { match receiver { Some(receiver) => { - let mime_format = MimeFormat::new(sender, receiver, message, attachment, now)?; - let email = Email::new(sender, receiver, message, &mime_format)?; + let mime_format = MimeFormat::new(&sender, receiver, message, attachment, now)?; + let email = Email::new(&sender, receiver, message, &mime_format)?; emails.push(email); } None => continue, @@ -42,7 +43,7 @@ impl<'a> BulkEmail<'a> { } pub fn personalize( - sender: &'a str, + sender: &'a Sender, receiver_column_name: &str, df_receiver: &'a DataFrame, default_message: &Message, diff --git a/src/email_builder/email.rs b/src/email_builder/email.rs index 610adfb..0aa5164 100644 --- a/src/email_builder/email.rs +++ b/src/email_builder/email.rs @@ -1,3 +1,4 @@ +use super::Sender; use crate::{ arg, email_builder::{Confirmed, Message, MimeFormat}, @@ -8,7 +9,7 @@ use std::io; #[derive(Debug)] pub struct Email<'a> { - pub sender: &'a str, + pub sender: &'a Sender<'a>, pub receiver: &'a str, pub message: Message, pub mime_format: MimeFormat, @@ -16,7 +17,7 @@ pub struct Email<'a> { impl<'a> Email<'a> { pub fn new( - sender: &'a str, + sender: &'a Sender, receiver: &'a str, message: &Message, mime_format: &MimeFormat, diff --git a/src/email_builder/mime.rs b/src/email_builder/mime.rs index 6cfec72..522042b 100644 --- a/src/email_builder/mime.rs +++ b/src/email_builder/mime.rs @@ -1,3 +1,4 @@ +use super::Sender; use crate::email_builder; use anyhow::{anyhow, Context}; use lettre::{ @@ -13,13 +14,13 @@ pub struct MimeFormat { impl MimeFormat { pub fn new( - sender: &str, + sender: &Sender, receiver: &str, message: &email_builder::Message, attachment: Option<&Path>, now: SystemTime, ) -> Result { - let sender = sender.parse().context("Can't parse sender")?; + let sender = sender.0.parse().context("Can't parse sender")?; let receiver = receiver.parse().context("Can't parse receiver")?; let message_builder = LettreMessage::builder() .from(sender) @@ -193,13 +194,13 @@ mod tests { .unwrap() .timestamp() as u64; let system_time = UNIX_EPOCH + std::time::Duration::from_secs(date_time); - let sender = "albert@einstein.com"; + let sender = Sender("albert@einstein.com"); let receiver = "marie@curie.com"; let subject = "Test Subject"; let text = "This is a test message (plaintext)."; let message = Message::new(subject, Some(text), None); - let res = MimeFormat::new(sender, receiver, &message, None, system_time); + let res = MimeFormat::new(&sender, receiver, &message, None, system_time); assert!(res.is_ok()); let mime_format = format!("{:?}", res.unwrap()); @@ -215,13 +216,13 @@ mod tests { .unwrap() .timestamp() as u64; let system_time = UNIX_EPOCH + std::time::Duration::from_secs(date_time); - let sender = "albert@einstein.com"; + let sender = Sender("albert@einstein.com"); let receiver = "marie@curie.com"; let subject = "Test Subject"; let html = "

This is a test message (html).

"; let message = Message::new(subject, None, Some(html)); - let res = MimeFormat::new(sender, receiver, &message, None, system_time); + let res = MimeFormat::new(&sender, receiver, &message, None, system_time); assert!(res.is_ok()); let mime_format = format!("{:?}", res.unwrap()); @@ -237,13 +238,13 @@ mod tests { .unwrap() .timestamp() as u64; let system_time = UNIX_EPOCH + std::time::Duration::from_secs(date_time); - let sender = "albert@einstein.com"; + let sender = Sender("albert@einstein.com"); let receiver = "marie@curie.com"; let subject = "Test Subject"; let message = Message::new(subject, None, None); let attachment = Path::new("./test_data/test.txt"); - let res = MimeFormat::new(sender, receiver, &message, Some(attachment), system_time); + let res = MimeFormat::new(&sender, receiver, &message, Some(attachment), system_time); assert!(res.is_ok()); let mime_format = format!("{:?}", res.unwrap()); diff --git a/src/email_builder/receiver.rs b/src/email_builder/receiver.rs index 46085ce..af31061 100644 --- a/src/email_builder/receiver.rs +++ b/src/email_builder/receiver.rs @@ -3,6 +3,7 @@ use anyhow::anyhow; use clap::ArgMatches; use polars::prelude::DataFrame; +#[derive(Debug)] pub struct Receiver; impl Receiver { diff --git a/src/email_builder/sender.rs b/src/email_builder/sender.rs index b92a7dc..2d400d6 100644 --- a/src/email_builder/sender.rs +++ b/src/email_builder/sender.rs @@ -2,13 +2,14 @@ use crate::arg; use anyhow::anyhow; use clap::ArgMatches; -pub struct Sender; +#[derive(Debug)] +pub struct Sender<'a>(pub &'a str); -impl Sender { - pub fn init<'a>(matches: &'a ArgMatches) -> Result<&'a str, anyhow::Error> { +impl<'a> Sender<'a> { + pub fn init(matches: &'a ArgMatches) -> Result, anyhow::Error> { if matches.is_present(arg::SENDER) { match matches.value_of(arg::SENDER) { - Some(sender) => Ok(sender), + Some(sender) => Ok(Sender(sender)), None => Err(anyhow!("Missing value for argument '{}'", arg::SENDER)), } } else { diff --git a/src/email_transmission/sent_email.rs b/src/email_transmission/sent_email.rs index 24dcb98..b228441 100644 --- a/src/email_transmission/sent_email.rs +++ b/src/email_transmission/sent_email.rs @@ -1,9 +1,9 @@ use super::Status; -use crate::email_builder::{Email, Message}; +use crate::email_builder::{Email, Message, Sender}; #[derive(Debug)] pub struct SentEmail<'a> { - pub sender: &'a str, + pub sender: &'a Sender<'a>, pub receiver: &'a str, pub message: &'a Message, pub status: Status, From 1e9bf476a934a18eda044d9501444228263fc6ed Mon Sep 17 00:00:00 2001 From: quambene Date: Thu, 28 Mar 2024 01:39:29 +0100 Subject: [PATCH 26/54] Refactor receiver --- src/cmd/send.rs | 8 ++++---- src/cmd/send_bulk.rs | 6 +++--- src/email_builder/bulk_email.rs | 22 ++++++++++++---------- src/email_builder/email.rs | 10 +++++----- src/email_builder/mime.rs | 20 ++++++++++---------- src/email_builder/receiver.rs | 20 ++++++++++++-------- src/email_builder/sender.rs | 4 ++-- src/email_transmission/sent_email.rs | 8 ++++---- 8 files changed, 52 insertions(+), 46 deletions(-) diff --git a/src/cmd/send.rs b/src/cmd/send.rs index aa7715e..a3ddb18 100644 --- a/src/cmd/send.rs +++ b/src/cmd/send.rs @@ -15,12 +15,12 @@ pub fn send(matches: &ArgMatches) -> Result<(), anyhow::Error> { } let now = SystemTime::now(); - let sender = Sender::init(matches)?; - let receiver = Receiver::init(matches)?; + let sender = Sender::from_args(matches)?; + let receiver = Receiver::from_args(matches)?; let message = Message::from_args(matches)?; let attachment = matches.value_of(arg::ATTACHMENT).map(Path::new); - let mime_format = MimeFormat::new(&sender, receiver, &message, attachment, now)?; - let email = Email::new(&sender, receiver, &message, &mime_format)?; + let mime_format = MimeFormat::new(sender, receiver, &message, attachment, now)?; + let email = Email::new(sender, receiver, &message, &mime_format)?; if matches.is_present(arg::DISPLAY) { println!("Display email: {:#?}", email); diff --git a/src/cmd/send_bulk.rs b/src/cmd/send_bulk.rs index 8fbf91e..d288c7b 100644 --- a/src/cmd/send_bulk.rs +++ b/src/cmd/send_bulk.rs @@ -13,7 +13,7 @@ pub fn send_bulk(matches: &ArgMatches) -> Result<(), anyhow::Error> { println!("matches: {:#?}", matches); } - let sender = Sender::init(matches)?; + let sender = Sender::from_args(matches)?; let receiver_column_name = Receiver::column_name(matches)?; let df_receiver = Receiver::dataframe(matches)?; let default_message = Message::from_args(matches)?; @@ -21,7 +21,7 @@ pub fn send_bulk(matches: &ArgMatches) -> Result<(), anyhow::Error> { let bulk_email = if matches.is_present(arg::PERSONALIZE) { if let Some(personalized_columns) = matches.values_of(arg::PERSONALIZE) { BulkEmail::personalize( - &sender, + sender, receiver_column_name, &df_receiver, &default_message, @@ -33,7 +33,7 @@ pub fn send_bulk(matches: &ArgMatches) -> Result<(), anyhow::Error> { } } else { BulkEmail::new( - &sender, + sender, receiver_column_name, &df_receiver, &default_message, diff --git a/src/email_builder/bulk_email.rs b/src/email_builder/bulk_email.rs index 20ea003..de07051 100644 --- a/src/email_builder/bulk_email.rs +++ b/src/email_builder/bulk_email.rs @@ -1,4 +1,4 @@ -use super::Sender; +use super::{Receiver, Sender}; use crate::{ arg, data_loader::TabularData, @@ -18,7 +18,7 @@ pub struct BulkEmail<'a> { impl<'a> BulkEmail<'a> { pub fn new( - sender: &'a Sender, + sender: Sender<'a>, receiver_column_name: &str, df_receiver: &'a DataFrame, message: &'a Message, @@ -31,8 +31,9 @@ impl<'a> BulkEmail<'a> { for receiver in receivers { match receiver { Some(receiver) => { - let mime_format = MimeFormat::new(&sender, receiver, message, attachment, now)?; - let email = Email::new(&sender, receiver, message, &mime_format)?; + let mime_format = + MimeFormat::new(sender, Receiver(receiver), message, attachment, now)?; + let email = Email::new(sender, Receiver(receiver), message, &mime_format)?; emails.push(email); } None => continue, @@ -43,7 +44,7 @@ impl<'a> BulkEmail<'a> { } pub fn personalize( - sender: &'a Sender, + sender: Sender<'a>, receiver_column_name: &str, df_receiver: &'a DataFrame, default_message: &Message, @@ -59,8 +60,9 @@ impl<'a> BulkEmail<'a> { message.personalize(i, df_receiver, &columns)?; let receiver = TabularData::row(i, receiver_column_name, df_receiver)?; - let mime_format = MimeFormat::new(sender, receiver, &message, attachment, now)?; - let email = Email::new(sender, receiver, &message, &mime_format)?; + let mime_format = + MimeFormat::new(sender, Receiver(receiver), &message, attachment, now)?; + let email = Email::new(sender, Receiver(receiver), &message, &mime_format)?; emails.push(email); } @@ -92,11 +94,11 @@ impl<'a> BulkEmail<'a> { pub fn confirm(&self) -> Result { let mut input = String::new(); let email_count = self.emails.len(); - let receivers: Vec = self + let receivers = self .emails .iter() - .map(|email| email.receiver.to_string()) - .collect(); + .map(|email| email.receiver.as_str()) + .collect::>(); println!( "Preparing to send an email to {} recipients: {:#?}", email_count, receivers diff --git a/src/email_builder/email.rs b/src/email_builder/email.rs index 0aa5164..91ff719 100644 --- a/src/email_builder/email.rs +++ b/src/email_builder/email.rs @@ -1,4 +1,4 @@ -use super::Sender; +use super::{Receiver, Sender}; use crate::{ arg, email_builder::{Confirmed, Message, MimeFormat}, @@ -9,16 +9,16 @@ use std::io; #[derive(Debug)] pub struct Email<'a> { - pub sender: &'a Sender<'a>, - pub receiver: &'a str, + pub sender: Sender<'a>, + pub receiver: Receiver<'a>, pub message: Message, pub mime_format: MimeFormat, } impl<'a> Email<'a> { pub fn new( - sender: &'a Sender, - receiver: &'a str, + sender: Sender<'a>, + receiver: Receiver<'a>, message: &Message, mime_format: &MimeFormat, ) -> Result { diff --git a/src/email_builder/mime.rs b/src/email_builder/mime.rs index 522042b..b6b4c36 100644 --- a/src/email_builder/mime.rs +++ b/src/email_builder/mime.rs @@ -1,4 +1,4 @@ -use super::Sender; +use super::{Receiver, Sender}; use crate::email_builder; use anyhow::{anyhow, Context}; use lettre::{ @@ -14,14 +14,14 @@ pub struct MimeFormat { impl MimeFormat { pub fn new( - sender: &Sender, - receiver: &str, + sender: Sender, + receiver: Receiver, message: &email_builder::Message, attachment: Option<&Path>, now: SystemTime, ) -> Result { let sender = sender.0.parse().context("Can't parse sender")?; - let receiver = receiver.parse().context("Can't parse receiver")?; + let receiver = receiver.0.parse().context("Can't parse receiver")?; let message_builder = LettreMessage::builder() .from(sender) .to(receiver) @@ -195,12 +195,12 @@ mod tests { .timestamp() as u64; let system_time = UNIX_EPOCH + std::time::Duration::from_secs(date_time); let sender = Sender("albert@einstein.com"); - let receiver = "marie@curie.com"; + let receiver = Receiver("marie@curie.com"); let subject = "Test Subject"; let text = "This is a test message (plaintext)."; let message = Message::new(subject, Some(text), None); - let res = MimeFormat::new(&sender, receiver, &message, None, system_time); + let res = MimeFormat::new(sender, receiver, &message, None, system_time); assert!(res.is_ok()); let mime_format = format!("{:?}", res.unwrap()); @@ -217,12 +217,12 @@ mod tests { .timestamp() as u64; let system_time = UNIX_EPOCH + std::time::Duration::from_secs(date_time); let sender = Sender("albert@einstein.com"); - let receiver = "marie@curie.com"; + let receiver = Receiver("marie@curie.com"); let subject = "Test Subject"; let html = "

This is a test message (html).

"; let message = Message::new(subject, None, Some(html)); - let res = MimeFormat::new(&sender, receiver, &message, None, system_time); + let res = MimeFormat::new(sender, receiver, &message, None, system_time); assert!(res.is_ok()); let mime_format = format!("{:?}", res.unwrap()); @@ -239,12 +239,12 @@ mod tests { .timestamp() as u64; let system_time = UNIX_EPOCH + std::time::Duration::from_secs(date_time); let sender = Sender("albert@einstein.com"); - let receiver = "marie@curie.com"; + let receiver = Receiver("marie@curie.com"); let subject = "Test Subject"; let message = Message::new(subject, None, None); let attachment = Path::new("./test_data/test.txt"); - let res = MimeFormat::new(&sender, receiver, &message, Some(attachment), system_time); + let res = MimeFormat::new(sender, receiver, &message, Some(attachment), system_time); assert!(res.is_ok()); let mime_format = format!("{:?}", res.unwrap()); diff --git a/src/email_builder/receiver.rs b/src/email_builder/receiver.rs index af31061..8bb7b71 100644 --- a/src/email_builder/receiver.rs +++ b/src/email_builder/receiver.rs @@ -3,14 +3,18 @@ use anyhow::anyhow; use clap::ArgMatches; use polars::prelude::DataFrame; -#[derive(Debug)] -pub struct Receiver; +#[derive(Debug, Clone, Copy)] +pub struct Receiver<'a>(pub &'a str); -impl Receiver { - pub fn init<'a>(matches: &'a ArgMatches) -> Result<&'a str, anyhow::Error> { +impl<'a> Receiver<'a> { + pub fn as_str(&self) -> &str { + self.0 + } + + pub fn from_args(matches: &'a ArgMatches) -> Result { if matches.is_present(arg::RECEIVER) { match matches.value_of(arg::RECEIVER) { - Some(receiver) => Ok(receiver), + Some(receiver) => Ok(Self(receiver)), None => Err(anyhow!("Missing value for argument '{}'", arg::RECEIVER)), } } else { @@ -42,7 +46,7 @@ impl Receiver { } } - pub fn column_name<'a>(matches: &'a ArgMatches<'a>) -> Result<&str, anyhow::Error> { + pub fn column_name(matches: &'a ArgMatches<'a>) -> Result<&str, anyhow::Error> { // If argument 'RECEIVER_COLUMN' is not present the default value 'email' will be used as column name match matches.value_of(arg::RECEIVER_COLUMN) { Some(column_name) => Ok(column_name), @@ -53,7 +57,7 @@ impl Receiver { } } - pub fn file_name<'a>(matches: &'a ArgMatches<'a>) -> Result<&str, anyhow::Error> { + pub fn file_name(matches: &'a ArgMatches<'a>) -> Result<&str, anyhow::Error> { match matches.value_of(arg::RECEIVER_FILE) { Some(file_name) => Ok(file_name), None => Err(anyhow!( @@ -63,7 +67,7 @@ impl Receiver { } } - pub fn query<'a>(matches: &'a ArgMatches<'a>) -> Result<&str, anyhow::Error> { + pub fn query(matches: &'a ArgMatches<'a>) -> Result<&str, anyhow::Error> { match matches.value_of(arg::RECEIVER_QUERY) { Some(query) => Ok(query), None => Err(anyhow!( diff --git a/src/email_builder/sender.rs b/src/email_builder/sender.rs index 2d400d6..c091adf 100644 --- a/src/email_builder/sender.rs +++ b/src/email_builder/sender.rs @@ -2,11 +2,11 @@ use crate::arg; use anyhow::anyhow; use clap::ArgMatches; -#[derive(Debug)] +#[derive(Debug, Copy, Clone)] pub struct Sender<'a>(pub &'a str); impl<'a> Sender<'a> { - pub fn init(matches: &'a ArgMatches) -> Result, anyhow::Error> { + pub fn from_args(matches: &'a ArgMatches) -> Result { if matches.is_present(arg::SENDER) { match matches.value_of(arg::SENDER) { Some(sender) => Ok(Sender(sender)), diff --git a/src/email_transmission/sent_email.rs b/src/email_transmission/sent_email.rs index b228441..0d8bb9f 100644 --- a/src/email_transmission/sent_email.rs +++ b/src/email_transmission/sent_email.rs @@ -1,10 +1,10 @@ use super::Status; -use crate::email_builder::{Email, Message, Sender}; +use crate::email_builder::{Email, Message, Receiver, Sender}; #[derive(Debug)] pub struct SentEmail<'a> { - pub sender: &'a Sender<'a>, - pub receiver: &'a str, + pub sender: Sender<'a>, + pub receiver: Receiver<'a>, pub message: &'a Message, pub status: Status, } @@ -20,6 +20,6 @@ impl<'a> SentEmail<'a> { } pub fn display_status(&self) { - println!("{} ... {}", self.receiver, self.status); + println!("{} ... {}", self.receiver.0, self.status); } } From 471a4003fda2f5ae82ec71086a4814f27c2edd19 Mon Sep 17 00:00:00 2001 From: quambene Date: Thu, 28 Mar 2024 01:56:33 +0100 Subject: [PATCH 27/54] Refactor send --- src/cmd/send.rs | 36 ++++++++++++++-- src/cmd/send_bulk.rs | 74 +++++++++++++++++++++++++++++---- src/email_builder/bulk_email.rs | 70 ++----------------------------- src/email_builder/email.rs | 38 +---------------- 4 files changed, 105 insertions(+), 113 deletions(-) diff --git a/src/cmd/send.rs b/src/cmd/send.rs index a3ddb18..5d60f1d 100644 --- a/src/cmd/send.rs +++ b/src/cmd/send.rs @@ -5,9 +5,9 @@ use crate::{ email_transmission::Client, helper::format_green, }; -use anyhow::Result; +use anyhow::Context; use clap::ArgMatches; -use std::{path::Path, time::SystemTime}; +use std::{io, path::Path, time::SystemTime}; pub fn send(matches: &ArgMatches) -> Result<(), anyhow::Error> { if matches.is_present(arg::VERBOSE) { @@ -40,7 +40,7 @@ pub fn send(matches: &ArgMatches) -> Result<(), anyhow::Error> { sent_email.display_status(); eml_formatter.archive(matches, &email)?; } else { - let confirmation = email.confirm(matches)?; + let confirmation = confirm_email(&email)?; match confirmation { Confirmed::Yes => { let sent_email = client.send(matches, &email)?; @@ -59,3 +59,33 @@ pub fn send(matches: &ArgMatches) -> Result<(), anyhow::Error> { Ok(()) } + +pub fn confirm_email(email: &Email) -> Result { + let mut input = String::new(); + + println!( + "Preparing to send an email to 1 recipient: {}", + email.receiver.0 + ); + + println!("Should an email be sent to 1 recipient? Yes (y) or no (n)"); + let confirmation = loop { + io::stdin() + .read_line(&mut input) + .context("Can't read input")?; + match input.trim() { + "y" | "yes" | "Yes" => { + break Confirmed::Yes; + } + "n" | "no" | "No" => { + println!("Aborted ..."); + break Confirmed::No; + } + _ => { + println!("Choose yes (y) or no (n). Try again."); + continue; + } + } + }; + Ok(confirmation) +} diff --git a/src/cmd/send_bulk.rs b/src/cmd/send_bulk.rs index d288c7b..6776c24 100644 --- a/src/cmd/send_bulk.rs +++ b/src/cmd/send_bulk.rs @@ -1,12 +1,14 @@ use crate::{ - arg::{self}, - email_builder::{BulkEmail, Confirmed, Message, Receiver, Sender}, + arg, + email_builder::{BulkEmail, Confirmed, Email, Message, Receiver, Sender}, + email_formatter::EmlFormatter, + email_transmission::Client, helper::format_green, }; -use anyhow::anyhow; use anyhow::Result; +use anyhow::{anyhow, Context}; use clap::ArgMatches; -use std::path::Path; +use std::{io, path::Path}; pub fn send_bulk(matches: &ArgMatches) -> Result<(), anyhow::Error> { if matches.is_present(arg::VERBOSE) { @@ -50,12 +52,12 @@ pub fn send_bulk(matches: &ArgMatches) -> Result<(), anyhow::Error> { } if matches.is_present(arg::ASSUME_YES) { - bulk_email.process(matches)?; + process_emails(&bulk_email.emails, matches)?; } else { - let confirmation = bulk_email.confirm()?; + let confirmation = confirm_emails(&bulk_email.emails)?; match confirmation { Confirmed::Yes => { - bulk_email.process(matches)?; + process_emails(&bulk_email.emails, matches)?; } Confirmed::No => (), } @@ -63,3 +65,61 @@ pub fn send_bulk(matches: &ArgMatches) -> Result<(), anyhow::Error> { Ok(()) } + +pub fn process_emails(emails: &[Email], matches: &ArgMatches) -> Result<(), anyhow::Error> { + let client = Client::init(matches)?; + let eml_formatter = EmlFormatter::new(matches)?; + + println!("Sending email to {} receivers ...", emails.len()); + + for email in emails { + let sent_email = client.send(matches, email)?; + sent_email.display_status(); + eml_formatter.archive(matches, email)?; + } + + if matches.is_present(arg::DRY_RUN) { + println!("All emails sent (dry run)."); + } else { + println!("All emails sent."); + } + + Ok(()) +} + +pub fn confirm_emails(emails: &[Email]) -> Result { + let mut input = String::new(); + let email_count = emails.len(); + let receivers = emails + .iter() + .map(|email| email.receiver.as_str()) + .collect::>(); + println!( + "Preparing to send an email to {} recipients: {:#?}", + email_count, receivers + ); + + println!( + "Should an email be sent to {} recipients? Yes (y) or no (n)", + email_count + ); + let confirmation = loop { + io::stdin() + .read_line(&mut input) + .context("Can't read input")?; + match input.trim() { + "y" | "yes" | "Yes" => { + break Confirmed::Yes; + } + "n" | "no" | "No" => { + println!("Aborted ..."); + break Confirmed::No; + } + _ => { + println!("Choose yes (y) or no (n). Try again."); + continue; + } + } + }; + Ok(confirmation) +} diff --git a/src/email_builder/bulk_email.rs b/src/email_builder/bulk_email.rs index de07051..9939579 100644 --- a/src/email_builder/bulk_email.rs +++ b/src/email_builder/bulk_email.rs @@ -1,15 +1,12 @@ use super::{Receiver, Sender}; use crate::{ - arg, data_loader::TabularData, - email_builder::{Confirmed, Email, Message, MimeFormat}, - email_formatter::EmlFormatter, - email_transmission::Client, + email_builder::{Email, Message, MimeFormat}, }; -use anyhow::{Context, Result}; -use clap::{ArgMatches, Values}; +use anyhow::Result; +use clap::Values; use polars::prelude::DataFrame; -use std::{io, path::Path, time::SystemTime}; +use std::{path::Path, time::SystemTime}; #[derive(Debug)] pub struct BulkEmail<'a> { @@ -69,63 +66,4 @@ impl<'a> BulkEmail<'a> { Ok(BulkEmail { emails }) } - - pub fn process(&self, matches: &ArgMatches) -> Result<(), anyhow::Error> { - let client = Client::init(matches)?; - let eml_formatter = EmlFormatter::new(matches)?; - - println!("Sending email to {} receivers ...", self.emails.len()); - - for email in &self.emails { - let sent_email = client.send(matches, email)?; - sent_email.display_status(); - eml_formatter.archive(matches, email)?; - } - - if matches.is_present(arg::DRY_RUN) { - println!("All emails sent (dry run)."); - } else { - println!("All emails sent."); - } - - Ok(()) - } - - pub fn confirm(&self) -> Result { - let mut input = String::new(); - let email_count = self.emails.len(); - let receivers = self - .emails - .iter() - .map(|email| email.receiver.as_str()) - .collect::>(); - println!( - "Preparing to send an email to {} recipients: {:#?}", - email_count, receivers - ); - - println!( - "Should an email be sent to {} recipients? Yes (y) or no (n)", - email_count - ); - let confirmation = loop { - io::stdin() - .read_line(&mut input) - .context("Can't read input")?; - match input.trim() { - "y" | "yes" | "Yes" => { - break Confirmed::Yes; - } - "n" | "no" | "No" => { - println!("Aborted ..."); - break Confirmed::No; - } - _ => { - println!("Choose yes (y) or no (n). Try again."); - continue; - } - } - }; - Ok(confirmation) - } } diff --git a/src/email_builder/email.rs b/src/email_builder/email.rs index 91ff719..83b6166 100644 --- a/src/email_builder/email.rs +++ b/src/email_builder/email.rs @@ -1,11 +1,5 @@ use super::{Receiver, Sender}; -use crate::{ - arg, - email_builder::{Confirmed, Message, MimeFormat}, -}; -use anyhow::{anyhow, Context, Result}; -use clap::ArgMatches; -use std::io; +use crate::email_builder::{Message, MimeFormat}; #[derive(Debug)] pub struct Email<'a> { @@ -30,34 +24,4 @@ impl<'a> Email<'a> { }; Ok(email) } - - pub fn confirm(&self, matches: &ArgMatches) -> Result { - let mut input = String::new(); - - match matches.value_of(arg::RECEIVER) { - Some(receiver) => println!("Preparing to send an email to 1 recipient: {}", receiver), - None => return Err(anyhow!("Missing value for argument '{}'", arg::RECEIVER)), - } - - println!("Should an email be sent to 1 recipient? Yes (y) or no (n)"); - let confirmation = loop { - io::stdin() - .read_line(&mut input) - .context("Can't read input")?; - match input.trim() { - "y" | "yes" | "Yes" => { - break Confirmed::Yes; - } - "n" | "no" | "No" => { - println!("Aborted ..."); - break Confirmed::No; - } - _ => { - println!("Choose yes (y) or no (n). Try again."); - continue; - } - } - }; - Ok(confirmation) - } } From 167acc208d691898c11a47e80181b97eb06e85cf Mon Sep 17 00:00:00 2001 From: quambene Date: Thu, 28 Mar 2024 02:27:40 +0100 Subject: [PATCH 28/54] Refactor client --- src/cmd/send.rs | 22 +++++++++----- src/cmd/send_bulk.rs | 42 ++++++++++++++++++++------- src/email_formatter/eml.rs | 28 +++++++++--------- src/email_provider/aws.rs | 35 +++++++++------------- src/email_transmission/client.rs | 19 +++++++----- src/email_transmission/mock_client.rs | 7 +---- src/email_transmission/mod.rs | 7 +---- src/email_transmission/smtp.rs | 39 +++++++++---------------- 8 files changed, 101 insertions(+), 98 deletions(-) diff --git a/src/cmd/send.rs b/src/cmd/send.rs index 5d60f1d..4d9a2e1 100644 --- a/src/cmd/send.rs +++ b/src/cmd/send.rs @@ -14,6 +14,8 @@ pub fn send(matches: &ArgMatches) -> Result<(), anyhow::Error> { println!("matches: {:#?}", matches); } + let dry_run = matches.is_present(arg::DRY_RUN); + let is_archived = matches.is_present(arg::ARCHIVE); let now = SystemTime::now(); let sender = Sender::from_args(matches)?; let receiver = Receiver::from_args(matches)?; @@ -26,26 +28,32 @@ pub fn send(matches: &ArgMatches) -> Result<(), anyhow::Error> { println!("Display email: {:#?}", email); } - if matches.is_present(arg::DRY_RUN) { + if dry_run { println!("Dry run: {}", format_green("activated")); } - let client = Client::init(matches)?; - let eml_formatter = EmlFormatter::new(matches)?; + let client = Client::from_args(matches)?; + let eml_formatter = EmlFormatter::from_args(matches)?; println!("Sending email to 1 recipient ..."); if matches.is_present(arg::ASSUME_YES) { - let sent_email = client.send(matches, &email)?; + let sent_email = client.send(&email)?; sent_email.display_status(); - eml_formatter.archive(matches, &email)?; + + if is_archived { + eml_formatter.archive(&email, dry_run)?; + } } else { let confirmation = confirm_email(&email)?; match confirmation { Confirmed::Yes => { - let sent_email = client.send(matches, &email)?; + let sent_email = client.send(&email)?; sent_email.display_status(); - eml_formatter.archive(matches, &email)?; + + if is_archived { + eml_formatter.archive(&email, dry_run)?; + } } Confirmed::No => (), } diff --git a/src/cmd/send_bulk.rs b/src/cmd/send_bulk.rs index 6776c24..f78cc5e 100644 --- a/src/cmd/send_bulk.rs +++ b/src/cmd/send_bulk.rs @@ -15,6 +15,8 @@ pub fn send_bulk(matches: &ArgMatches) -> Result<(), anyhow::Error> { println!("matches: {:#?}", matches); } + let dry_run = matches.is_present(arg::DRY_RUN); + let is_archived = matches.is_present(arg::ARCHIVE); let sender = Sender::from_args(matches)?; let receiver_column_name = Receiver::column_name(matches)?; let df_receiver = Receiver::dataframe(matches)?; @@ -42,22 +44,36 @@ pub fn send_bulk(matches: &ArgMatches) -> Result<(), anyhow::Error> { attachment, )? }; + let client = Client::from_args(matches)?; + let eml_formatter = EmlFormatter::from_args(matches)?; if matches.is_present(arg::DISPLAY) { println!("Display emails: {:#?}", bulk_email); } - if matches.is_present(arg::DRY_RUN) { + if dry_run { println!("Dry run: {}", format_green("activated")); } if matches.is_present(arg::ASSUME_YES) { - process_emails(&bulk_email.emails, matches)?; + process_emails( + &client, + &eml_formatter, + &bulk_email.emails, + dry_run, + is_archived, + )?; } else { let confirmation = confirm_emails(&bulk_email.emails)?; match confirmation { Confirmed::Yes => { - process_emails(&bulk_email.emails, matches)?; + process_emails( + &client, + &eml_formatter, + &bulk_email.emails, + dry_run, + is_archived, + )?; } Confirmed::No => (), } @@ -66,19 +82,25 @@ pub fn send_bulk(matches: &ArgMatches) -> Result<(), anyhow::Error> { Ok(()) } -pub fn process_emails(emails: &[Email], matches: &ArgMatches) -> Result<(), anyhow::Error> { - let client = Client::init(matches)?; - let eml_formatter = EmlFormatter::new(matches)?; - +pub fn process_emails<'a>( + client: &Client<'a>, + eml_formatter: &EmlFormatter, + emails: &'a [Email], + dry_run: bool, + is_archived: bool, +) -> Result<(), anyhow::Error> { println!("Sending email to {} receivers ...", emails.len()); for email in emails { - let sent_email = client.send(matches, email)?; + let sent_email = client.send(email)?; sent_email.display_status(); - eml_formatter.archive(matches, email)?; + + if is_archived { + eml_formatter.archive(email, dry_run)?; + } } - if matches.is_present(arg::DRY_RUN) { + if dry_run { println!("All emails sent (dry run)."); } else { println!("All emails sent."); diff --git a/src/email_formatter/eml.rs b/src/email_formatter/eml.rs index 3089950..618a525 100644 --- a/src/email_formatter/eml.rs +++ b/src/email_formatter/eml.rs @@ -13,7 +13,7 @@ pub struct EmlFormatter<'a> { } impl<'a> EmlFormatter<'a> { - pub fn new(matches: &'a ArgMatches<'a>) -> Result { + pub fn from_args(matches: &'a ArgMatches<'a>) -> Result { let target_dir = match matches.value_of(arg::ARCHIVE_DIR) { Some(archive_dir) => Path::new(archive_dir), None => return Err(anyhow!("Missing value for argument '{}'", arg::ARCHIVE_DIR)), @@ -32,21 +32,19 @@ impl<'a> EmlFormatter<'a> { Ok(formatter) } - pub fn archive(&self, matches: &ArgMatches, email: &Email) -> Result<(), anyhow::Error> { - if matches.is_present(arg::ARCHIVE) { - let message_id = self - .transport - .send(&email.mime_format.message) - .context("Can't save email in .eml format")?; + pub fn archive(&self, email: &Email, dry_run: bool) -> Result<(), anyhow::Error> { + let message_id = self + .transport + .send(&email.mime_format.message) + .context("Can't save email in .eml format")?; - let old_path = old_path(message_id.as_str(), self.target_dir); - let new_path = new_path(matches, message_id.as_str(), self.target_dir); + let old_path = old_path(message_id.as_str(), self.target_dir); + let new_path = new_path(message_id.as_str(), self.target_dir, dry_run); - println!("Archiving '{}' ...", new_path.display()); + println!("Archiving '{}' ...", new_path.display()); - // TODO: renaming file is required because of issue/discussion https://github.com/lettre/lettre/discussions/711 - fs::rename(old_path, new_path).context("Can't rename archived email")?; - } + // TODO: renaming file is required because of issue/discussion https://github.com/lettre/lettre/discussions/711 + fs::rename(old_path, new_path).context("Can't rename archived email")?; Ok(()) } @@ -57,12 +55,12 @@ fn old_path(message_id: &str, target_dir: &Path) -> PathBuf { target_dir.join(old_file_name) } -fn new_path(matches: &ArgMatches, message_id: &str, target_dir: &Path) -> PathBuf { +fn new_path(message_id: &str, target_dir: &Path, dry_run: bool) -> PathBuf { let now = std::time::SystemTime::now(); let now_utc: chrono::DateTime = now.into(); let timestamp = now_utc.to_rfc3339_opts(chrono::SecondsFormat::Secs, true); - let new_file_name = if matches.is_present(arg::DRY_RUN) { + let new_file_name = if dry_run { format!("{}_{}_dry-run.eml", timestamp, message_id) } else { format!("{}_{}.eml", timestamp, message_id) diff --git a/src/email_provider/aws.rs b/src/email_provider/aws.rs index 83db8d3..efae26d 100644 --- a/src/email_provider/aws.rs +++ b/src/email_provider/aws.rs @@ -57,29 +57,20 @@ impl AwsSesClient { impl<'a> SendEmail<'a> for AwsSesClient { #[tokio::main] - async fn send( - &self, - matches: &ArgMatches, - email: &'a Email<'a>, - ) -> Result, anyhow::Error> { - let sent_email = if matches.is_present(arg::DRY_RUN) { - let status = Status::DryRun; - SentEmail::new(email, status) - } else { - let raw_message = RawMessage { - data: Bytes::from(base64::encode(email.mime_format.message.formatted())), - }; - let request = SendRawEmailRequest { - raw_message, - ..Default::default() - }; - let response = self.client.send_raw_email(request).await; - let status = match response { - Ok(response) => Status::SentOk(response.message_id), - Err(err) => Status::SentError(err.to_string()), - }; - SentEmail::new(email, status) + async fn send(&self, email: &'a Email<'a>) -> Result, anyhow::Error> { + let raw_message = RawMessage { + data: Bytes::from(base64::encode(email.mime_format.message.formatted())), }; + let request = SendRawEmailRequest { + raw_message, + ..Default::default() + }; + let response = self.client.send_raw_email(request).await; + let status = match response { + Ok(response) => Status::SentOk(response.message_id), + Err(err) => Status::SentError(err.to_string()), + }; + let sent_email = SentEmail::new(email, status); Ok(sent_email) } diff --git a/src/email_transmission/client.rs b/src/email_transmission/client.rs index 6f3ce08..d5d7215 100644 --- a/src/email_transmission/client.rs +++ b/src/email_transmission/client.rs @@ -1,17 +1,18 @@ -use super::{MockClient, SendEmail, SmtpClient}; +use super::{MockClient, SendEmail, SentEmail, SmtpClient}; use crate::{ arg::{self, val}, + email_builder::Email, email_provider::AwsSesClient, }; use anyhow::anyhow; use clap::ArgMatches; -pub struct Client; +pub struct Client<'a>(pub Box>); -impl Client { - pub fn init<'a>(matches: &ArgMatches) -> Result>, anyhow::Error> { +impl<'a> Client<'a> { + pub fn from_args(matches: &ArgMatches) -> Result { if matches.is_present(arg::DRY_RUN) { - return Ok(Box::new(MockClient)); + return Ok(Client(Box::new(MockClient))); } if matches.is_present(arg::CONNECTION) { @@ -19,11 +20,11 @@ impl Client { Some(connection) => match connection.to_lowercase().as_str() { val::SMTP => { let client = SmtpClient::new()?; - Ok(Box::new(client)) + Ok(Client(Box::new(client))) } val::AWS => { let client = AwsSesClient::new(matches)?; - Ok(Box::new(client)) + Ok(Client(Box::new(client))) } other => Err(anyhow!(format!( "Value '{}' for argument '{}' not supported", @@ -40,4 +41,8 @@ impl Client { Err(anyhow!(format!("Missing argument '{}'", arg::CONNECTION))) } } + + pub fn send(&self, email: &'a Email<'a>) -> Result, anyhow::Error> { + self.0.send(email) + } } diff --git a/src/email_transmission/mock_client.rs b/src/email_transmission/mock_client.rs index c851428..a9ff1b0 100644 --- a/src/email_transmission/mock_client.rs +++ b/src/email_transmission/mock_client.rs @@ -1,15 +1,10 @@ use super::{SendEmail, SentEmail, Status}; use crate::email_builder::Email; -use clap::ArgMatches; pub struct MockClient; impl<'a> SendEmail<'a> for MockClient { - fn send( - &self, - _matches: &ArgMatches, - email: &'a Email<'a>, - ) -> Result, anyhow::Error> { + fn send(&self, email: &'a Email<'a>) -> Result, anyhow::Error> { let email = SentEmail::new(email, Status::DryRun); Ok(email) } diff --git a/src/email_transmission/mod.rs b/src/email_transmission/mod.rs index 80fa060..9a22c8f 100644 --- a/src/email_transmission/mod.rs +++ b/src/email_transmission/mod.rs @@ -4,7 +4,6 @@ mod sent_email; mod smtp; mod status; -use clap::ArgMatches; pub use client::Client; pub use mock_client::MockClient; pub use sent_email::SentEmail; @@ -14,9 +13,5 @@ pub use status::Status; use crate::email_builder::Email; pub trait SendEmail<'a> { - fn send( - &self, - matches: &ArgMatches, - email: &'a Email<'a>, - ) -> Result, anyhow::Error>; + fn send(&self, email: &'a Email<'a>) -> Result, anyhow::Error>; } diff --git a/src/email_transmission/smtp.rs b/src/email_transmission/smtp.rs index e458b83..d637a2b 100644 --- a/src/email_transmission/smtp.rs +++ b/src/email_transmission/smtp.rs @@ -1,11 +1,9 @@ use super::{SendEmail, SentEmail, Status}; use crate::{ - arg, email_builder::Email, helper::{format_green, format_red}, }; use anyhow::Context; -use clap::ArgMatches; use lettre::{transport::smtp::authentication::Credentials, SmtpTransport, Transport}; use std::env; @@ -48,30 +46,21 @@ impl SmtpClient { } impl<'a> SendEmail<'a> for SmtpClient { - fn send( - &self, - matches: &ArgMatches, - email: &'a Email<'a>, - ) -> Result, anyhow::Error> { - let sent_email = if matches.is_present(arg::DRY_RUN) { - let status = Status::DryRun; - SentEmail::new(email, status) - } else { - let response = self - .transport - .send(&email.mime_format.message) - .context("Can't sent email via SMTP"); - let status = match response { - Ok(response) => { - let response_string = response.message().collect::(); - let messages: Vec<&str> = response_string.split(' ').collect(); - let message_id = messages[1]; - Status::SentOk(message_id.to_string()) - } - Err(err) => Status::SentError(err.to_string()), - }; - SentEmail::new(email, status) + fn send(&self, email: &'a Email<'a>) -> Result, anyhow::Error> { + let response = self + .transport + .send(&email.mime_format.message) + .context("Can't sent email via SMTP"); + let status = match response { + Ok(response) => { + let response_string = response.message().collect::(); + let messages: Vec<&str> = response_string.split(' ').collect(); + let message_id = messages[1]; + Status::SentOk(message_id.to_string()) + } + Err(err) => Status::SentError(err.to_string()), }; + let sent_email = SentEmail::new(email, status); Ok(sent_email) } From 08a518ac538a49ec781cb97fb732e2f1f221eaec Mon Sep 17 00:00:00 2001 From: quambene Date: Thu, 28 Mar 2024 03:11:00 +0100 Subject: [PATCH 29/54] Fix test run --- Cargo.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cd4c6db..ad051f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,9 +16,6 @@ license = "Apache-2.0" name = "pigeon" path = "src/main.rs" -[lib] -test = false - [[test]] name = "integration_tests" path = "tests/cmd/lib.rs" From 62c4a7fb452d7d155b9a0418dd367ab6fa9313dc Mon Sep 17 00:00:00 2001 From: quambene Date: Sat, 30 Mar 2024 01:57:38 +0100 Subject: [PATCH 30/54] Refactor bulk email --- src/arg.rs | 10 +++ src/cmd/query.rs | 20 +++-- src/cmd/send_bulk.rs | 26 ++---- src/data_loader/mod.rs | 3 - src/data_loader/tabular_data.rs | 74 ----------------- src/data_sources/mod.rs | 2 +- src/data_sources/postgres.rs | 55 ++++++++----- src/data_sources/ssh_tunnel.rs | 31 ++++--- src/email_builder/bulk_email.rs | 81 ++++++++++++------- src/email_builder/bulk_receiver.rs | 126 +++++++++++++++++++++++++++++ src/email_builder/message.rs | 19 +---- src/email_builder/mod.rs | 2 + src/email_builder/receiver.rs | 60 +------------- src/email_builder/sender.rs | 2 +- src/lib.rs | 1 - 15 files changed, 267 insertions(+), 245 deletions(-) delete mode 100644 src/data_loader/mod.rs delete mode 100644 src/data_loader/tabular_data.rs create mode 100644 src/email_builder/bulk_receiver.rs diff --git a/src/arg.rs b/src/arg.rs index 71b26b6..94445a4 100644 --- a/src/arg.rs +++ b/src/arg.rs @@ -1,3 +1,6 @@ +use anyhow::anyhow; +use clap::ArgMatches; + // args for command pub const VERBOSE: &str = "verbose"; @@ -36,3 +39,10 @@ pub mod val { pub const SMTP: &str = "smtp"; pub const AWS: &str = "aws"; } + +pub fn value<'a>(name: &str, matches: &'a ArgMatches<'a>) -> Result<&'a str, anyhow::Error> { + match matches.value_of(name) { + Some(query) => Ok(query), + None => Err(anyhow!("Missing value for argument '{}'", name)), + } +} diff --git a/src/cmd/query.rs b/src/cmd/query.rs index 719cffe..82b0b68 100644 --- a/src/cmd/query.rs +++ b/src/cmd/query.rs @@ -1,7 +1,6 @@ use crate::{ - arg::{self}, - cmd, - data_sources::{query_postgres, write_csv, write_image}, + arg, cmd, + data_sources::{self, ConnVars, DbConnection}, }; use anyhow::{anyhow, Result}; use clap::ArgMatches; @@ -14,7 +13,10 @@ pub fn query(matches: &ArgMatches) -> Result<(), anyhow::Error> { if matches.is_present(cmd::QUERY) { match matches.value_of(cmd::QUERY) { Some(query) => { - let df_query_result = query_postgres(matches, query)?; + let conn_vars = ConnVars::from_env()?; + let ssh_tunnel = matches.value_of(arg::SSH_TUNNEL); + let connection = DbConnection::new(&conn_vars, ssh_tunnel)?; + let df_query_result = data_sources::query_postgres(&connection, query)?; if matches.is_present(arg::DISPLAY) { println!("Display query result: {}", df_query_result); @@ -24,9 +26,13 @@ pub fn query(matches: &ArgMatches) -> Result<(), anyhow::Error> { // If argument 'FILE_TYPE' is not present the default value 'csv' will be used match matches.value_of(arg::FILE_TYPE) { Some(file_type) => match file_type { - "csv" => write_csv(matches, df_query_result)?, - x if x == "jpg" => write_image(matches, df_query_result, x)?, - x if x == "png" => write_image(matches, df_query_result, x)?, + "csv" => data_sources::write_csv(matches, df_query_result)?, + x if x == "jpg" => { + data_sources::write_image(matches, df_query_result, x)? + } + x if x == "png" => { + data_sources::write_image(matches, df_query_result, x)? + } _ => { return Err(anyhow!( "Value '{}' not supported for argument '{}'", diff --git a/src/cmd/send_bulk.rs b/src/cmd/send_bulk.rs index f78cc5e..1e0b5c3 100644 --- a/src/cmd/send_bulk.rs +++ b/src/cmd/send_bulk.rs @@ -1,6 +1,6 @@ use crate::{ arg, - email_builder::{BulkEmail, Confirmed, Email, Message, Receiver, Sender}, + email_builder::{BulkEmail, BulkReceiver, Confirmed, Email, Message, Sender}, email_formatter::EmlFormatter, email_transmission::Client, helper::format_green, @@ -18,31 +18,19 @@ pub fn send_bulk(matches: &ArgMatches) -> Result<(), anyhow::Error> { let dry_run = matches.is_present(arg::DRY_RUN); let is_archived = matches.is_present(arg::ARCHIVE); let sender = Sender::from_args(matches)?; - let receiver_column_name = Receiver::column_name(matches)?; - let df_receiver = Receiver::dataframe(matches)?; - let default_message = Message::from_args(matches)?; + let receivers = BulkReceiver::from_args(matches)?; + let message = Message::from_args(matches)?; let attachment = matches.value_of(arg::ATTACHMENT).map(Path::new); + let bulk_email = if matches.is_present(arg::PERSONALIZE) { if let Some(personalized_columns) = matches.values_of(arg::PERSONALIZE) { - BulkEmail::personalize( - sender, - receiver_column_name, - &df_receiver, - &default_message, - personalized_columns, - attachment, - )? + let columns = personalized_columns.collect::>(); + BulkEmail::personalize(sender, &receivers, &message, &columns, attachment)? } else { return Err(anyhow!("Missing value for argument '{}'", arg::PERSONALIZE)); } } else { - BulkEmail::new( - sender, - receiver_column_name, - &df_receiver, - &default_message, - attachment, - )? + BulkEmail::new(sender, &receivers, &message, attachment)? }; let client = Client::from_args(matches)?; let eml_formatter = EmlFormatter::from_args(matches)?; diff --git a/src/data_loader/mod.rs b/src/data_loader/mod.rs deleted file mode 100644 index 6725ce8..0000000 --- a/src/data_loader/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod tabular_data; - -pub use tabular_data::TabularData; diff --git a/src/data_loader/tabular_data.rs b/src/data_loader/tabular_data.rs deleted file mode 100644 index 557fbc7..0000000 --- a/src/data_loader/tabular_data.rs +++ /dev/null @@ -1,74 +0,0 @@ -use crate::{ - arg, - data_sources::{query_postgres, read_csv}, - email_builder::Receiver, -}; -use anyhow::{anyhow, Context}; -use clap::ArgMatches; -use polars::{ - chunked_array::ChunkedArray, - prelude::{DataFrame, TakeRandom, Utf8Type}, -}; -use std::path::PathBuf; - -pub struct TabularData; - -impl TabularData { - pub fn from_query(matches: &ArgMatches) -> Result { - let receiver_query = Receiver::query(matches)?; - let df_receiver = query_postgres(matches, receiver_query)?; - - if matches.is_present(arg::DISPLAY) { - println!("Display query result: {}", df_receiver); - } - - Ok(df_receiver) - } - - pub fn from_file(matches: &ArgMatches) -> Result { - let receiver_file = Receiver::file_name(matches)?; - let path = PathBuf::from(receiver_file); - let df_receiver = read_csv(&path)?; - - if matches.is_present(arg::DISPLAY) { - println!("Display csv file: {}", df_receiver); - } - - Ok(df_receiver) - } - - pub fn row<'a>( - index: usize, - column_name: &str, - df: &'a DataFrame, - ) -> Result<&'a str, anyhow::Error> { - let column_value = df - .column(column_name) - .context(format!("Missing column '{}'", column_name))? - .utf8() - .context("Can't convert series to chunked array")? - .get(index); - - match column_value { - Some(column_value) => Ok(column_value), - None => Err(anyhow!( - "Missing value for column '{}' in row {}", - column_name, - index - )), - } - } - - pub fn column<'a>( - column_name: &str, - df: &'a DataFrame, - ) -> Result<&'a ChunkedArray, anyhow::Error> { - let column = df - .column(column_name) - .context(format!("Missing column '{}'", column_name))? - .utf8() - .context("Can't convert series to chunked array")?; - - Ok(column) - } -} diff --git a/src/data_sources/mod.rs b/src/data_sources/mod.rs index 40d86ff..23d6146 100644 --- a/src/data_sources/mod.rs +++ b/src/data_sources/mod.rs @@ -6,6 +6,6 @@ mod ssh_tunnel; pub use self::{ csv::{read_csv, write_csv}, image::write_image, - postgres::{query_postgres, ConnVars}, + postgres::{query_postgres, ConnVars, DbConnection}, }; pub use ssh_tunnel::SshTunnel; diff --git a/src/data_sources/postgres.rs b/src/data_sources/postgres.rs index 4527eae..ad81999 100644 --- a/src/data_sources/postgres.rs +++ b/src/data_sources/postgres.rs @@ -1,6 +1,5 @@ -use crate::{arg, data_sources::SshTunnel}; +use super::SshTunnel; use anyhow::{Context, Result}; -use clap::ArgMatches; use connectorx::{ destinations::arrow2::Arrow2Destination, prelude::{Dispatcher, PostgresArrow2Transport}, @@ -61,29 +60,44 @@ impl ConnVars { Ok(conn_vars) } - pub fn connection_url(&self) -> String { - format!( + pub fn connection_url(&self) -> Result { + let connection_url = format!( "postgresql://{}:{}@{}:{}/{}", &self.db_user, &self.db_password.0, &self.db_host, &self.db_port, &self.db_name - ) + ); + let url = Url::parse(&connection_url)?; + Ok(url) } } -pub fn query_postgres(matches: &ArgMatches, query: &str) -> Result { - let conn_vars = ConnVars::from_env()?; - - let ssh_tunnel = if matches.is_present(arg::SSH_TUNNEL) { - Some(SshTunnel::new(matches, &conn_vars)?) - } else { - None - }; +pub struct DbConnection { + /// The connection url of the DB. + url: Url, + /// Connection via ssh tunnel. + ssh_tunnel: Option, +} - let connection_url = match &ssh_tunnel { - Some(tunnel) => Url::parse(&tunnel.connection_url)?, - None => Url::parse(&conn_vars.connection_url())?, - }; +impl DbConnection { + pub fn new(conn_vars: &ConnVars, ssh_tunnel: Option<&str>) -> Result { + let connection = if let Some(tunnel) = ssh_tunnel { + let ssh_tunnel = SshTunnel::new(tunnel, conn_vars)?; + Self { + url: ssh_tunnel.connection_url().to_owned(), + ssh_tunnel: Some(ssh_tunnel), + } + } else { + let connection_url = conn_vars.connection_url()?; + Self { + url: connection_url, + ssh_tunnel: None, + } + }; + Ok(connection) + } +} - let (config, _tls) = rewrite_tls_args(&connection_url)?; +pub fn query_postgres(connection: &DbConnection, query: &str) -> Result { + let (config, _tls) = rewrite_tls_args(&connection.url)?; let source = PostgresSource::::new(config, NoTls, 3)?; let mut destination = Arrow2Destination::new(); let queries = &[CXQuery::naked(query)]; @@ -96,9 +110,8 @@ pub fn query_postgres(matches: &ArgMatches, query: &str) -> Result tunnel.kill()?, - None => (), + if let Some(tunnel) = &connection.ssh_tunnel { + tunnel.kill()?; } Ok(df) diff --git a/src/data_sources/ssh_tunnel.rs b/src/data_sources/ssh_tunnel.rs index 4af20f2..72be644 100644 --- a/src/data_sources/ssh_tunnel.rs +++ b/src/data_sources/ssh_tunnel.rs @@ -1,20 +1,20 @@ -use crate::{arg, data_sources::postgres::ConnVars}; +use crate::data_sources::postgres::ConnVars; use anyhow::{anyhow, Context, Result}; -use clap::ArgMatches; use std::{ env, process::{Child, Command}, }; +use url::Url; pub struct SshTunnel { process: Child, - pub connection_url: String, + connection_url: Url, } const LOCALHOST: &str = "127.0.0.1"; impl SshTunnel { - pub fn new(matches: &ArgMatches, conn_vars: &ConnVars) -> Result { + pub fn new(ssh_tunnel: &str, conn_vars: &ConnVars) -> Result { println!("Opening ssh tunnel ..."); let server_host = env::var("SERVER_HOST") @@ -22,10 +22,7 @@ impl SshTunnel { let server_user = env::var("SERVER_USER") .context("Missing environment variable 'SERVER_USER'. Needed for ssh tunnel.")?; - let local_port = match matches.value_of(arg::SSH_TUNNEL) { - Some(port) => port, - None => return Err(anyhow!("Missing value for argument '{}'", arg::SSH_TUNNEL)), - }; + let local_port = ssh_tunnel; let local_url = &(LOCALHOST.to_string() + ":" + local_port) as &str; let db_url = format!("{}:{}", conn_vars.db_host, &conn_vars.db_port); @@ -36,7 +33,12 @@ impl SshTunnel { .args(["-N", "-T", "-L", &port_fwd, &ssh_connection]) .spawn()?; - let connection_url = SshTunnel::connection_url(conn_vars, local_url); + let connection_url = format!( + "postgresql://{}:{}@{}/{}", + &conn_vars.db_user, &conn_vars.db_password.0, local_url, &conn_vars.db_name + ); + let connection_url = Url::parse(&connection_url)?; + let ssh_tunnel = SshTunnel { process, connection_url, @@ -49,6 +51,10 @@ impl SshTunnel { Ok(ssh_tunnel) } + pub fn connection_url(&self) -> &Url { + &self.connection_url + } + pub fn kill(&self) -> Result<(), anyhow::Error> { let pid = self.process.id(); let mut cmd = Command::new("kill"); @@ -76,11 +82,4 @@ impl SshTunnel { ))), } } - - fn connection_url(conn_vars: &ConnVars, tunnel_url: &str) -> String { - format!( - "postgresql://{}:{}@{}/{}", - &conn_vars.db_user, &conn_vars.db_password.0, tunnel_url, &conn_vars.db_name - ) - } } diff --git a/src/email_builder/bulk_email.rs b/src/email_builder/bulk_email.rs index 9939579..94e0008 100644 --- a/src/email_builder/bulk_email.rs +++ b/src/email_builder/bulk_email.rs @@ -1,11 +1,6 @@ -use super::{Receiver, Sender}; -use crate::{ - data_loader::TabularData, - email_builder::{Email, Message, MimeFormat}, -}; +use super::{BulkReceiver, Receiver, Sender}; +use crate::email_builder::{Email, Message, MimeFormat}; use anyhow::Result; -use clap::Values; -use polars::prelude::DataFrame; use std::{path::Path, time::SystemTime}; #[derive(Debug)] @@ -16,24 +11,20 @@ pub struct BulkEmail<'a> { impl<'a> BulkEmail<'a> { pub fn new( sender: Sender<'a>, - receiver_column_name: &str, - df_receiver: &'a DataFrame, + bulk_receiver: &'a BulkReceiver, message: &'a Message, attachment: Option<&Path>, ) -> Result { let now = SystemTime::now(); let mut emails: Vec = vec![]; - let receivers = TabularData::column(receiver_column_name, df_receiver)?; + let receivers = bulk_receiver.receiver_column()?; for receiver in receivers { - match receiver { - Some(receiver) => { - let mime_format = - MimeFormat::new(sender, Receiver(receiver), message, attachment, now)?; - let email = Email::new(sender, Receiver(receiver), message, &mime_format)?; - emails.push(email); - } - None => continue, + if let Some(receiver) = receiver { + let mime_format = + MimeFormat::new(sender, Receiver(receiver), message, attachment, now)?; + let email = Email::new(sender, Receiver(receiver), message, &mime_format)?; + emails.push(email); } } @@ -42,21 +33,23 @@ impl<'a> BulkEmail<'a> { pub fn personalize( sender: Sender<'a>, - receiver_column_name: &str, - df_receiver: &'a DataFrame, - default_message: &Message, - personalized_columns: Values, + receivers: &'a BulkReceiver, + message: &Message, + columns: &[&str], attachment: Option<&Path>, ) -> Result { let now = SystemTime::now(); let mut emails: Vec = vec![]; - let columns: Vec<&str> = personalized_columns.collect(); - for i in 0..df_receiver.height() { - let mut message = default_message.clone(); - message.personalize(i, df_receiver, &columns)?; + for i in 0..receivers.height() { + let mut message = message.clone(); - let receiver = TabularData::row(i, receiver_column_name, df_receiver)?; + for &col_name in columns.iter() { + let col_value = receivers.row(i, col_name)?; + message.personalize(col_name, col_value); + } + + let receiver = receivers.receiver_row(i)?; let mime_format = MimeFormat::new(sender, Receiver(receiver), &message, attachment, now)?; let email = Email::new(sender, Receiver(receiver), &message, &mime_format)?; @@ -67,3 +60,37 @@ impl<'a> BulkEmail<'a> { Ok(BulkEmail { emails }) } } + +#[cfg(test)] +mod tests { + use polars::{frame::DataFrame, prelude::NamedFrom, series::Series}; + + use super::*; + + #[test] + fn test_bulk_email() { + let sender = Sender("albert@einstein.com"); + let subject = "Test Subject"; + let text = "This is a test message (plaintext)."; + let html = "

This is a test message (html).

"; + let message = Message::new(subject, Some(text), Some(html)); + let column_name = "email"; + let receiver_column = Series::new(column_name, &["marie@curie.com", "emmy@noether.com"]); + let df_receiver = DataFrame::new(vec![receiver_column]).unwrap(); + let receivers = BulkReceiver::new(column_name.to_owned(), df_receiver); + + let res = BulkEmail::new(sender, &receivers, &message, None); + assert!(res.is_ok()); + + let emails = res.unwrap().emails; + assert_eq!(emails.len(), 2); + assert!(emails.iter().any(|email| email.sender == sender)); + + let receivers = emails + .iter() + .map(|email| email.receiver) + .collect::>(); + assert!(receivers.contains(&Receiver("marie@curie.com"))); + assert!(receivers.contains(&Receiver("emmy@noether.com"))); + } +} diff --git a/src/email_builder/bulk_receiver.rs b/src/email_builder/bulk_receiver.rs new file mode 100644 index 0000000..a731d25 --- /dev/null +++ b/src/email_builder/bulk_receiver.rs @@ -0,0 +1,126 @@ +use std::path::Path; + +use crate::{ + arg, cmd, + data_sources::{self, ConnVars, DbConnection}, +}; +use anyhow::{anyhow, Context}; +use clap::ArgMatches; +use polars::{ + chunked_array::{ops::TakeRandom, ChunkedArray}, + datatypes::Utf8Type, + frame::DataFrame, +}; + +pub struct BulkReceiver { + pub column_name: String, + pub df_receiver: DataFrame, +} + +impl BulkReceiver { + pub fn new(column_name: String, df_receiver: DataFrame) -> Self { + Self { + column_name, + df_receiver, + } + } + + pub fn from_args(matches: &ArgMatches) -> Result { + let column_name = arg::value(arg::RECEIVER_COLUMN, matches)?; + let receiver_query = matches.value_of(arg::RECEIVER_QUERY); + let receiver_path = matches.value_of(arg::RECEIVER_FILE).map(Path::new); + + match (receiver_query, receiver_path) { + (Some(query), None) => { + let conn_vars = ConnVars::from_env()?; + let ssh_tunnel = matches.value_of(arg::SSH_TUNNEL); + let connection = DbConnection::new(&conn_vars, ssh_tunnel)?; + let df_receiver = data_sources::query_postgres(&connection, query)?; + + if matches.is_present(arg::DISPLAY) { + println!("Display query result: {}", df_receiver); + } + + Ok(Self::new( + column_name.to_owned() , + df_receiver, + )) + }, + (None, Some(path)) => { + let df_receiver = data_sources::read_csv(&path)?; + + if matches.is_present(arg::DISPLAY) { + println!("Display csv file: {}", df_receiver); + } + + Ok(Self::new( + column_name.to_owned() , + df_receiver, + )) + }, + (Some(_), Some(_)) => { + Err(anyhow!( + "Argument conflict: arguments {} and {} are not allowed at the same time. Check usage via '{} help {}'", + arg::RECEIVER_QUERY, + arg::RECEIVER_FILE, + cmd::BIN, + cmd::SEND_BULK, + )) + }, + (None, None) => { + Err(anyhow!( + "Missing arguments: please specify argument {} or {}. Check usage via '{} help {}'", + arg::RECEIVER_QUERY, + arg::RECEIVER_FILE, + cmd::BIN, + cmd::SEND_BULK, + )) + }, + } + } + + pub fn height(&self) -> usize { + self.df_receiver.height() + } + + pub fn receiver_column<'a>(&'a self) -> Result<&'a ChunkedArray, anyhow::Error> { + self.column(&self.column_name) + } + + pub fn receiver_row<'a>(&'a self, index: usize) -> Result<&'a str, anyhow::Error> { + self.row(index, &self.column_name) + } + + pub fn row<'a>(&'a self, index: usize, column_name: &str) -> Result<&'a str, anyhow::Error> { + let column_value = self + .df_receiver + .column(column_name) + .context(format!("Missing column '{}'", column_name))? + .utf8() + .context("Can't convert series to chunked array")? + .get(index); + + match column_value { + Some(column_value) => Ok(column_value), + None => Err(anyhow!( + "Missing value for column '{}' in row {}", + column_name, + index + )), + } + } + + pub fn column<'a>( + &'a self, + column_name: &str, + ) -> Result<&'a ChunkedArray, anyhow::Error> { + let column = self + .df_receiver + .column(column_name) + .context(format!("Missing column '{column_name}'"))? + .utf8() + .context("Can't convert series to chunked array")?; + + Ok(column) + } +} diff --git a/src/email_builder/message.rs b/src/email_builder/message.rs index 832e8e5..09ed7d0 100644 --- a/src/email_builder/message.rs +++ b/src/email_builder/message.rs @@ -1,7 +1,6 @@ -use crate::{arg, data_loader::TabularData}; +use crate::arg; use anyhow::{anyhow, Result}; use clap::ArgMatches; -use polars::prelude::DataFrame; use std::{fs, path::Path}; use super::MessageTemplate; @@ -83,21 +82,7 @@ impl Message { } } - pub fn personalize( - &mut self, - index: usize, - df_receiver: &DataFrame, - columns: &[&str], - ) -> Result<(), anyhow::Error> { - for &col_name in columns.iter() { - let col_value = TabularData::row(index, col_name, df_receiver)?; - self.replace(col_name, col_value); - } - - Ok(()) - } - - fn replace(&mut self, col_name: &str, col_value: &str) { + pub fn personalize(&mut self, col_name: &str, col_value: &str) { self.subject = self .subject .replace(&format!("{{{}}}", col_name), col_value); diff --git a/src/email_builder/mod.rs b/src/email_builder/mod.rs index 087d7bc..0e98bbc 100644 --- a/src/email_builder/mod.rs +++ b/src/email_builder/mod.rs @@ -1,4 +1,5 @@ mod bulk_email; +mod bulk_receiver; mod email; mod message; mod message_template; @@ -7,6 +8,7 @@ mod receiver; mod sender; pub use bulk_email::BulkEmail; +pub use bulk_receiver::BulkReceiver; pub use email::Email; pub use message::Message; pub use message_template::MessageTemplate; diff --git a/src/email_builder/receiver.rs b/src/email_builder/receiver.rs index 8bb7b71..60baa09 100644 --- a/src/email_builder/receiver.rs +++ b/src/email_builder/receiver.rs @@ -1,9 +1,8 @@ -use crate::{arg, cmd, data_loader::TabularData}; +use crate::arg; use anyhow::anyhow; use clap::ArgMatches; -use polars::prelude::DataFrame; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Receiver<'a>(pub &'a str); impl<'a> Receiver<'a> { @@ -21,59 +20,4 @@ impl<'a> Receiver<'a> { Err(anyhow!("Missing argument '{}'", arg::RECEIVER)) } } - - pub fn dataframe(matches: &ArgMatches) -> Result { - match ( - matches.is_present(arg::RECEIVER_QUERY), - matches.is_present(arg::RECEIVER_FILE), - ) { - (true, false) => TabularData::from_query(matches), - (false, true) => TabularData::from_file(matches), - (true, true) => Err(anyhow!( - "Argument conflict: arguments {} and {} are not allowed at the same time. Check usage via '{} help {}'", - arg::RECEIVER_QUERY, - arg::RECEIVER_FILE, - cmd::BIN, - cmd::SEND_BULK, - )), - (false, false) => Err(anyhow!( - "Missing arguments: please specify argument {} or {}. Check usage via '{} help {}'", - arg::RECEIVER_QUERY, - arg::RECEIVER_FILE, - cmd::BIN, - cmd::SEND_BULK, - )), - } - } - - pub fn column_name(matches: &'a ArgMatches<'a>) -> Result<&str, anyhow::Error> { - // If argument 'RECEIVER_COLUMN' is not present the default value 'email' will be used as column name - match matches.value_of(arg::RECEIVER_COLUMN) { - Some(column_name) => Ok(column_name), - None => Err(anyhow!( - "Missing value for argument '{}'", - arg::RECEIVER_COLUMN - )), - } - } - - pub fn file_name(matches: &'a ArgMatches<'a>) -> Result<&str, anyhow::Error> { - match matches.value_of(arg::RECEIVER_FILE) { - Some(file_name) => Ok(file_name), - None => Err(anyhow!( - "Missing value for argument '{}'", - arg::RECEIVER_FILE - )), - } - } - - pub fn query(matches: &'a ArgMatches<'a>) -> Result<&str, anyhow::Error> { - match matches.value_of(arg::RECEIVER_QUERY) { - Some(query) => Ok(query), - None => Err(anyhow!( - "Missing value for argument '{}'", - arg::RECEIVER_QUERY - )), - } - } } diff --git a/src/email_builder/sender.rs b/src/email_builder/sender.rs index c091adf..2363394 100644 --- a/src/email_builder/sender.rs +++ b/src/email_builder/sender.rs @@ -2,7 +2,7 @@ use crate::arg; use anyhow::anyhow; use clap::ArgMatches; -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq)] pub struct Sender<'a>(pub &'a str); impl<'a> Sender<'a> { diff --git a/src/lib.rs b/src/lib.rs index 2926290..09b0a60 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,5 @@ pub mod arg; pub mod cmd; -mod data_loader; mod data_sources; mod email_builder; mod email_formatter; From 0341149f017c002156ed4cce1609a12c232140ef Mon Sep 17 00:00:00 2001 From: quambene Date: Sat, 30 Mar 2024 01:58:35 +0100 Subject: [PATCH 31/54] Format code --- src/cmd/send_bulk.rs | 3 +-- src/email_builder/bulk_email.rs | 3 +-- src/email_builder/bulk_receiver.rs | 3 +-- src/email_builder/message.rs | 3 +-- src/email_transmission/mod.rs | 3 +-- 5 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/cmd/send_bulk.rs b/src/cmd/send_bulk.rs index 1e0b5c3..c580bba 100644 --- a/src/cmd/send_bulk.rs +++ b/src/cmd/send_bulk.rs @@ -5,8 +5,7 @@ use crate::{ email_transmission::Client, helper::format_green, }; -use anyhow::Result; -use anyhow::{anyhow, Context}; +use anyhow::{anyhow, Context, Result}; use clap::ArgMatches; use std::{io, path::Path}; diff --git a/src/email_builder/bulk_email.rs b/src/email_builder/bulk_email.rs index 94e0008..e685b63 100644 --- a/src/email_builder/bulk_email.rs +++ b/src/email_builder/bulk_email.rs @@ -63,9 +63,8 @@ impl<'a> BulkEmail<'a> { #[cfg(test)] mod tests { - use polars::{frame::DataFrame, prelude::NamedFrom, series::Series}; - use super::*; + use polars::{frame::DataFrame, prelude::NamedFrom, series::Series}; #[test] fn test_bulk_email() { diff --git a/src/email_builder/bulk_receiver.rs b/src/email_builder/bulk_receiver.rs index a731d25..d762941 100644 --- a/src/email_builder/bulk_receiver.rs +++ b/src/email_builder/bulk_receiver.rs @@ -1,5 +1,3 @@ -use std::path::Path; - use crate::{ arg, cmd, data_sources::{self, ConnVars, DbConnection}, @@ -11,6 +9,7 @@ use polars::{ datatypes::Utf8Type, frame::DataFrame, }; +use std::path::Path; pub struct BulkReceiver { pub column_name: String, diff --git a/src/email_builder/message.rs b/src/email_builder/message.rs index 09ed7d0..ef180fd 100644 --- a/src/email_builder/message.rs +++ b/src/email_builder/message.rs @@ -1,10 +1,9 @@ +use super::MessageTemplate; use crate::arg; use anyhow::{anyhow, Result}; use clap::ArgMatches; use std::{fs, path::Path}; -use super::MessageTemplate; - #[derive(Debug, Clone, PartialEq)] pub struct Message { pub subject: String, diff --git a/src/email_transmission/mod.rs b/src/email_transmission/mod.rs index 9a22c8f..939f015 100644 --- a/src/email_transmission/mod.rs +++ b/src/email_transmission/mod.rs @@ -4,14 +4,13 @@ mod sent_email; mod smtp; mod status; +use crate::email_builder::Email; pub use client::Client; pub use mock_client::MockClient; pub use sent_email::SentEmail; pub use smtp::SmtpClient; pub use status::Status; -use crate::email_builder::Email; - pub trait SendEmail<'a> { fn send(&self, email: &'a Email<'a>) -> Result, anyhow::Error>; } From a668fc974fdc030e33e9194b6fbdcd4b14fcd27c Mon Sep 17 00:00:00 2001 From: quambene Date: Sat, 30 Mar 2024 02:01:01 +0100 Subject: [PATCH 32/54] Fix clippy warnings --- src/email_builder/bulk_email.rs | 12 +++++------- src/email_builder/bulk_receiver.rs | 6 +++--- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/email_builder/bulk_email.rs b/src/email_builder/bulk_email.rs index e685b63..8e22695 100644 --- a/src/email_builder/bulk_email.rs +++ b/src/email_builder/bulk_email.rs @@ -19,13 +19,11 @@ impl<'a> BulkEmail<'a> { let mut emails: Vec = vec![]; let receivers = bulk_receiver.receiver_column()?; - for receiver in receivers { - if let Some(receiver) = receiver { - let mime_format = - MimeFormat::new(sender, Receiver(receiver), message, attachment, now)?; - let email = Email::new(sender, Receiver(receiver), message, &mime_format)?; - emails.push(email); - } + for receiver in receivers.into_iter().flatten() { + let mime_format = + MimeFormat::new(sender, Receiver(receiver), message, attachment, now)?; + let email = Email::new(sender, Receiver(receiver), message, &mime_format)?; + emails.push(email); } Ok(BulkEmail { emails }) diff --git a/src/email_builder/bulk_receiver.rs b/src/email_builder/bulk_receiver.rs index d762941..f9b65f7 100644 --- a/src/email_builder/bulk_receiver.rs +++ b/src/email_builder/bulk_receiver.rs @@ -46,7 +46,7 @@ impl BulkReceiver { )) }, (None, Some(path)) => { - let df_receiver = data_sources::read_csv(&path)?; + let df_receiver = data_sources::read_csv(path)?; if matches.is_present(arg::DISPLAY) { println!("Display csv file: {}", df_receiver); @@ -82,11 +82,11 @@ impl BulkReceiver { self.df_receiver.height() } - pub fn receiver_column<'a>(&'a self) -> Result<&'a ChunkedArray, anyhow::Error> { + pub fn receiver_column(&self) -> Result<&ChunkedArray, anyhow::Error> { self.column(&self.column_name) } - pub fn receiver_row<'a>(&'a self, index: usize) -> Result<&'a str, anyhow::Error> { + pub fn receiver_row(&self, index: usize) -> Result<&str, anyhow::Error> { self.row(index, &self.column_name) } From b662a7a4b363c15da3134098b5c1d4962ec4df21 Mon Sep 17 00:00:00 2001 From: quambene Date: Sat, 30 Mar 2024 02:14:53 +0100 Subject: [PATCH 33/54] Clean up tests --- tests/cmd/test_query.rs | 3 +++ tests/cmd/test_send.rs | 1 + 2 files changed, 4 insertions(+) diff --git a/tests/cmd/test_query.rs b/tests/cmd/test_query.rs index 6502fb2..5b44eab 100644 --- a/tests/cmd/test_query.rs +++ b/tests/cmd/test_query.rs @@ -4,6 +4,7 @@ use std::{env, fs}; use tempfile::tempdir; #[test] +#[ignore] fn test_query_display() { let test_query = env::var("TEST_QUERY").expect("Missing environment variable 'TEST_QUERY'"); println!("Execute 'pigeon query {test_query} --display'"); @@ -15,6 +16,7 @@ fn test_query_display() { } #[test] +#[ignore] fn test_query_save() { let test_query = env::var("TEST_QUERY").expect("Missing environment variable 'TEST_QUERY'"); let temp_dir = tempdir().unwrap(); @@ -47,6 +49,7 @@ fn test_query_save() { } #[test] +#[ignore] fn test_query_save_dir() { let test_query = env::var("TEST_QUERY").expect("Missing environment variable 'TEST_QUERY'"); let temp_dir = tempdir().unwrap(); diff --git a/tests/cmd/test_send.rs b/tests/cmd/test_send.rs index a5a92c7..5cafdb5 100644 --- a/tests/cmd/test_send.rs +++ b/tests/cmd/test_send.rs @@ -48,6 +48,7 @@ fn test_send_aws() { } #[test] +#[ignore] fn test_archive_dir_smtp_dry() { println!("Execute 'pigeon send'"); let mut cmd = Command::cargo_bin("pigeon").unwrap(); From c927602a7a6ca0e2236d13dab4c7916e9f6415bd Mon Sep 17 00:00:00 2001 From: quambene Date: Sat, 30 Mar 2024 02:29:56 +0100 Subject: [PATCH 34/54] Refactor read --- src/cmd/read.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cmd/read.rs b/src/cmd/read.rs index 8622b38..302256a 100644 --- a/src/cmd/read.rs +++ b/src/cmd/read.rs @@ -1,4 +1,4 @@ -use crate::{arg, cmd, data_sources::read_csv}; +use crate::{arg, cmd, data_sources}; use anyhow::{anyhow, Result}; use clap::ArgMatches; use std::path::PathBuf; @@ -12,7 +12,7 @@ pub fn read(matches: &ArgMatches) -> Result<(), anyhow::Error> { match matches.value_of(cmd::READ) { Some(csv_file) => { let path = PathBuf::from(csv_file); - let csv = read_csv(&path)?; + let csv = data_sources::read_csv(&path)?; if matches.is_present(arg::DISPLAY) { println!("Display csv file: {}", csv); From 8bd125238885c328ba9727cc60449b88a59c84a4 Mon Sep 17 00:00:00 2001 From: quambene Date: Sat, 30 Mar 2024 02:58:52 +0100 Subject: [PATCH 35/54] Refactor csv --- src/cmd/query.rs | 8 +++++- src/data_sources/csv.rs | 57 +++++++++++++++++++++++++++++++---------- 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/src/cmd/query.rs b/src/cmd/query.rs index 82b0b68..a214b3a 100644 --- a/src/cmd/query.rs +++ b/src/cmd/query.rs @@ -1,3 +1,5 @@ +use std::path::Path; + use crate::{ arg, cmd, data_sources::{self, ConnVars, DbConnection}, @@ -15,6 +17,7 @@ pub fn query(matches: &ArgMatches) -> Result<(), anyhow::Error> { Some(query) => { let conn_vars = ConnVars::from_env()?; let ssh_tunnel = matches.value_of(arg::SSH_TUNNEL); + let connection = DbConnection::new(&conn_vars, ssh_tunnel)?; let df_query_result = data_sources::query_postgres(&connection, query)?; @@ -26,7 +29,10 @@ pub fn query(matches: &ArgMatches) -> Result<(), anyhow::Error> { // If argument 'FILE_TYPE' is not present the default value 'csv' will be used match matches.value_of(arg::FILE_TYPE) { Some(file_type) => match file_type { - "csv" => data_sources::write_csv(matches, df_query_result)?, + "csv" => { + let save_dir = Path::new(arg::value(arg::SAVE_DIR, matches)?); + data_sources::write_csv(save_dir, df_query_result)?; + } x if x == "jpg" => { data_sources::write_image(matches, df_query_result, x)? } diff --git a/src/data_sources/csv.rs b/src/data_sources/csv.rs index f635b5a..b0a3ac2 100644 --- a/src/data_sources/csv.rs +++ b/src/data_sources/csv.rs @@ -1,6 +1,4 @@ -use crate::arg; -use anyhow::{anyhow, Context}; -use clap::ArgMatches; +use anyhow::Context; use polars::prelude::{CsvReader, CsvWriter, DataFrame, SerReader, SerWriter}; use std::{fs, path::Path, time::SystemTime}; @@ -11,26 +9,20 @@ pub fn read_csv(csv_file: &Path) -> Result { Ok(df) } -pub fn write_csv(matches: &ArgMatches, mut df: DataFrame) -> Result<(), anyhow::Error> { +pub fn write_csv(save_dir: &Path, mut df: DataFrame) -> Result<(), anyhow::Error> { let now = SystemTime::now(); let now_utc: chrono::DateTime = now.into(); let current_time = now_utc.to_rfc3339_opts(chrono::SecondsFormat::Secs, true); - let target_dir = match matches.value_of(arg::SAVE_DIR) { - Some(save_dir) => Path::new(save_dir), - None => return Err(anyhow!("Missing value for argument '{}'", arg::SAVE_DIR)), - }; let target_file = format!("query_{}.csv", ¤t_time); - match target_dir.exists() { + match save_dir.exists() { true => (), - false => fs::create_dir(target_dir).context(format!( - "Can't create directory: '{}'", - target_dir.display() - ))?, + false => fs::create_dir(save_dir) + .context(format!("Can't create directory: '{}'", save_dir.display()))?, } - let target_path = target_dir.join(target_file); + let target_path = save_dir.join(target_file); println!("Save query result to file: {}", target_path.display()); let csv_file = &mut fs::File::create(target_path)?; @@ -42,3 +34,40 @@ pub fn write_csv(matches: &ArgMatches, mut df: DataFrame) -> Result<(), anyhow:: Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + use polars::{datatypes::AnyValue, frame::row::Row}; + + #[test] + fn test_read_csv() { + let csv_file = Path::new("./test_data/receiver.csv"); + let res = read_csv(csv_file); + assert!(res.is_ok()); + + let df_receiver = res.unwrap(); + assert_eq!( + df_receiver.get_column_names(), + &["first_name", "last_name", "email"] + ); + assert_eq!( + df_receiver.get_row(0).unwrap(), + Row::new( + ["Marie", "Curie", "marie@curie.com"] + .into_iter() + .map(|s| AnyValue::Utf8(s)) + .collect() + ) + ); + assert_eq!( + df_receiver.get_row(1).unwrap(), + Row::new( + ["Alexandre", "Grothendieck", "alexandre@grothendieck.com"] + .into_iter() + .map(|s| AnyValue::Utf8(s)) + .collect() + ) + ); + } +} From ee10ffc36493073ec46b5581bcb650be96fb5a47 Mon Sep 17 00:00:00 2001 From: quambene Date: Sat, 30 Mar 2024 16:43:55 +0100 Subject: [PATCH 36/54] Test write csv --- src/cmd/query.rs | 16 ++++------ src/cmd/send.rs | 6 ++-- src/cmd/send_bulk.rs | 4 ++- src/data_sources/csv.rs | 65 ++++++++++++++++++++++++++++++++++---- src/email_formatter/eml.rs | 16 ++++++---- 5 files changed, 82 insertions(+), 25 deletions(-) diff --git a/src/cmd/query.rs b/src/cmd/query.rs index a214b3a..4643d31 100644 --- a/src/cmd/query.rs +++ b/src/cmd/query.rs @@ -5,6 +5,7 @@ use crate::{ data_sources::{self, ConnVars, DbConnection}, }; use anyhow::{anyhow, Result}; +use chrono::Utc; use clap::ArgMatches; pub fn query(matches: &ArgMatches) -> Result<(), anyhow::Error> { @@ -15,14 +16,15 @@ pub fn query(matches: &ArgMatches) -> Result<(), anyhow::Error> { if matches.is_present(cmd::QUERY) { match matches.value_of(cmd::QUERY) { Some(query) => { + let now = Utc::now(); let conn_vars = ConnVars::from_env()?; let ssh_tunnel = matches.value_of(arg::SSH_TUNNEL); let connection = DbConnection::new(&conn_vars, ssh_tunnel)?; - let df_query_result = data_sources::query_postgres(&connection, query)?; + let mut df_query = data_sources::query_postgres(&connection, query)?; if matches.is_present(arg::DISPLAY) { - println!("Display query result: {}", df_query_result); + println!("Display query result: {}", df_query); } if matches.is_present(arg::SAVE) { @@ -31,14 +33,10 @@ pub fn query(matches: &ArgMatches) -> Result<(), anyhow::Error> { Some(file_type) => match file_type { "csv" => { let save_dir = Path::new(arg::value(arg::SAVE_DIR, matches)?); - data_sources::write_csv(save_dir, df_query_result)?; - } - x if x == "jpg" => { - data_sources::write_image(matches, df_query_result, x)? - } - x if x == "png" => { - data_sources::write_image(matches, df_query_result, x)? + data_sources::write_csv(&mut df_query, save_dir, now)?; } + x if x == "jpg" => data_sources::write_image(matches, df_query, x)?, + x if x == "png" => data_sources::write_image(matches, df_query, x)?, _ => { return Err(anyhow!( "Value '{}' not supported for argument '{}'", diff --git a/src/cmd/send.rs b/src/cmd/send.rs index 4d9a2e1..aa63b98 100644 --- a/src/cmd/send.rs +++ b/src/cmd/send.rs @@ -6,6 +6,7 @@ use crate::{ helper::format_green, }; use anyhow::Context; +use chrono::Utc; use clap::ArgMatches; use std::{io, path::Path, time::SystemTime}; @@ -32,6 +33,7 @@ pub fn send(matches: &ArgMatches) -> Result<(), anyhow::Error> { println!("Dry run: {}", format_green("activated")); } + let now = Utc::now(); let client = Client::from_args(matches)?; let eml_formatter = EmlFormatter::from_args(matches)?; @@ -42,7 +44,7 @@ pub fn send(matches: &ArgMatches) -> Result<(), anyhow::Error> { sent_email.display_status(); if is_archived { - eml_formatter.archive(&email, dry_run)?; + eml_formatter.archive(&email, now, dry_run)?; } } else { let confirmation = confirm_email(&email)?; @@ -52,7 +54,7 @@ pub fn send(matches: &ArgMatches) -> Result<(), anyhow::Error> { sent_email.display_status(); if is_archived { - eml_formatter.archive(&email, dry_run)?; + eml_formatter.archive(&email, now, dry_run)?; } } Confirmed::No => (), diff --git a/src/cmd/send_bulk.rs b/src/cmd/send_bulk.rs index c580bba..df046e7 100644 --- a/src/cmd/send_bulk.rs +++ b/src/cmd/send_bulk.rs @@ -6,6 +6,7 @@ use crate::{ helper::format_green, }; use anyhow::{anyhow, Context, Result}; +use chrono::Utc; use clap::ArgMatches; use std::{io, path::Path}; @@ -83,7 +84,8 @@ pub fn process_emails<'a>( sent_email.display_status(); if is_archived { - eml_formatter.archive(email, dry_run)?; + let now = Utc::now(); + eml_formatter.archive(email, now, dry_run)?; } } diff --git a/src/data_sources/csv.rs b/src/data_sources/csv.rs index b0a3ac2..48de0fa 100644 --- a/src/data_sources/csv.rs +++ b/src/data_sources/csv.rs @@ -1,6 +1,7 @@ use anyhow::Context; +use chrono::{DateTime, Utc}; use polars::prelude::{CsvReader, CsvWriter, DataFrame, SerReader, SerWriter}; -use std::{fs, path::Path, time::SystemTime}; +use std::{fs, path::Path}; pub fn read_csv(csv_file: &Path) -> Result { println!("Reading csv file '{}' ...", csv_file.display()); @@ -9,10 +10,12 @@ pub fn read_csv(csv_file: &Path) -> Result { Ok(df) } -pub fn write_csv(save_dir: &Path, mut df: DataFrame) -> Result<(), anyhow::Error> { - let now = SystemTime::now(); - let now_utc: chrono::DateTime = now.into(); - let current_time = now_utc.to_rfc3339_opts(chrono::SecondsFormat::Secs, true); +pub fn write_csv( + df: &mut DataFrame, + save_dir: &Path, + now: DateTime, +) -> Result<(), anyhow::Error> { + let current_time = now.to_rfc3339_opts(chrono::SecondsFormat::Secs, true); let target_file = format!("query_{}.csv", ¤t_time); @@ -30,7 +33,7 @@ pub fn write_csv(save_dir: &Path, mut df: DataFrame) -> Result<(), anyhow::Error CsvWriter::new(csv_file) .with_datetime_format(Some(timestamp_format.to_string())) - .finish(&mut df)?; + .finish(df)?; Ok(()) } @@ -38,7 +41,8 @@ pub fn write_csv(save_dir: &Path, mut df: DataFrame) -> Result<(), anyhow::Error #[cfg(test)] mod tests { use super::*; - use polars::{datatypes::AnyValue, frame::row::Row}; + use polars::{datatypes::AnyValue, frame::row::Row, prelude::NamedFrom, series::Series}; + use tempfile::tempdir; #[test] fn test_read_csv() { @@ -70,4 +74,51 @@ mod tests { ) ); } + + #[test] + fn test_write_csv() { + let timestamp = chrono::DateTime::parse_from_rfc3339("2024-01-01T14:00:00Z").unwrap(); + let temp_dir = tempdir().unwrap(); + let temp_path = temp_dir.path(); + assert!(temp_path.exists(), "Missing path: {}", temp_path.display()); + + let first_name_column = Series::new("first_name", &["Marie", "Alexandre"]); + let last_name_column = Series::new("last_name", &["Curie", "Grothendieck"]); + let email_column = Series::new("email", &["marie@curie.com", "alexandre@grothendieck.com"]); + let mut df_receiver = + DataFrame::new(vec![first_name_column, last_name_column, email_column]).unwrap(); + + let res = write_csv(&mut df_receiver, temp_path, timestamp.naive_utc().and_utc()); + assert!(res.is_ok(), "{}", res.unwrap_err()); + + let csv_file = temp_path.join(format!( + "query_{}.csv", + timestamp.to_rfc3339_opts(chrono::SecondsFormat::Secs, true), + )); + assert!(csv_file.exists()); + + let df_receiver = read_csv(&csv_file).unwrap(); + assert_eq!( + df_receiver.get_column_names(), + &["first_name", "last_name", "email"] + ); + assert_eq!( + df_receiver.get_row(0).unwrap(), + Row::new( + ["Marie", "Curie", "marie@curie.com"] + .into_iter() + .map(|s| AnyValue::Utf8(s)) + .collect() + ) + ); + assert_eq!( + df_receiver.get_row(1).unwrap(), + Row::new( + ["Alexandre", "Grothendieck", "alexandre@grothendieck.com"] + .into_iter() + .map(|s| AnyValue::Utf8(s)) + .collect() + ) + ); + } } diff --git a/src/email_formatter/eml.rs b/src/email_formatter/eml.rs index 618a525..aec8c8b 100644 --- a/src/email_formatter/eml.rs +++ b/src/email_formatter/eml.rs @@ -1,5 +1,6 @@ use crate::{arg, email_builder::Email}; use anyhow::{anyhow, Context}; +use chrono::{DateTime, Utc}; use clap::ArgMatches; use lettre::{FileTransport, Transport}; use std::{ @@ -32,14 +33,19 @@ impl<'a> EmlFormatter<'a> { Ok(formatter) } - pub fn archive(&self, email: &Email, dry_run: bool) -> Result<(), anyhow::Error> { + pub fn archive( + &self, + email: &Email, + now: DateTime, + dry_run: bool, + ) -> Result<(), anyhow::Error> { let message_id = self .transport .send(&email.mime_format.message) .context("Can't save email in .eml format")?; let old_path = old_path(message_id.as_str(), self.target_dir); - let new_path = new_path(message_id.as_str(), self.target_dir, dry_run); + let new_path = new_path(message_id.as_str(), self.target_dir, dry_run, now); println!("Archiving '{}' ...", new_path.display()); @@ -55,10 +61,8 @@ fn old_path(message_id: &str, target_dir: &Path) -> PathBuf { target_dir.join(old_file_name) } -fn new_path(message_id: &str, target_dir: &Path, dry_run: bool) -> PathBuf { - let now = std::time::SystemTime::now(); - let now_utc: chrono::DateTime = now.into(); - let timestamp = now_utc.to_rfc3339_opts(chrono::SecondsFormat::Secs, true); +fn new_path(message_id: &str, target_dir: &Path, dry_run: bool, now: DateTime) -> PathBuf { + let timestamp = now.to_rfc3339_opts(chrono::SecondsFormat::Secs, true); let new_file_name = if dry_run { format!("{}_{}_dry-run.eml", timestamp, message_id) From 73a80cc64903e785cff48ac576946d8617b7d0b5 Mon Sep 17 00:00:00 2001 From: quambene Date: Sat, 30 Mar 2024 16:45:33 +0100 Subject: [PATCH 37/54] Rename sources --- src/cmd/query.rs | 10 +++++----- src/cmd/read.rs | 4 ++-- src/cmd/simple_query.rs | 2 +- src/email_builder/bulk_receiver.rs | 6 +++--- src/email_builder/message.rs | 5 +++++ src/lib.rs | 2 +- src/{data_sources => sources}/csv.rs | 0 src/{data_sources => sources}/image.rs | 0 src/{data_sources => sources}/mod.rs | 0 src/{data_sources => sources}/postgres.rs | 0 src/{data_sources => sources}/ssh_tunnel.rs | 2 +- tests/cmd/lib.rs | 1 + tests/cmd/test_init.rs | 4 ++++ 13 files changed, 23 insertions(+), 13 deletions(-) rename src/{data_sources => sources}/csv.rs (100%) rename src/{data_sources => sources}/image.rs (100%) rename src/{data_sources => sources}/mod.rs (100%) rename src/{data_sources => sources}/postgres.rs (100%) rename src/{data_sources => sources}/ssh_tunnel.rs (98%) diff --git a/src/cmd/query.rs b/src/cmd/query.rs index 4643d31..2920388 100644 --- a/src/cmd/query.rs +++ b/src/cmd/query.rs @@ -2,7 +2,7 @@ use std::path::Path; use crate::{ arg, cmd, - data_sources::{self, ConnVars, DbConnection}, + sources::{self, ConnVars, DbConnection}, }; use anyhow::{anyhow, Result}; use chrono::Utc; @@ -21,7 +21,7 @@ pub fn query(matches: &ArgMatches) -> Result<(), anyhow::Error> { let ssh_tunnel = matches.value_of(arg::SSH_TUNNEL); let connection = DbConnection::new(&conn_vars, ssh_tunnel)?; - let mut df_query = data_sources::query_postgres(&connection, query)?; + let mut df_query = sources::query_postgres(&connection, query)?; if matches.is_present(arg::DISPLAY) { println!("Display query result: {}", df_query); @@ -33,10 +33,10 @@ pub fn query(matches: &ArgMatches) -> Result<(), anyhow::Error> { Some(file_type) => match file_type { "csv" => { let save_dir = Path::new(arg::value(arg::SAVE_DIR, matches)?); - data_sources::write_csv(&mut df_query, save_dir, now)?; + sources::write_csv(&mut df_query, save_dir, now)?; } - x if x == "jpg" => data_sources::write_image(matches, df_query, x)?, - x if x == "png" => data_sources::write_image(matches, df_query, x)?, + x if x == "jpg" => sources::write_image(matches, df_query, x)?, + x if x == "png" => sources::write_image(matches, df_query, x)?, _ => { return Err(anyhow!( "Value '{}' not supported for argument '{}'", diff --git a/src/cmd/read.rs b/src/cmd/read.rs index 302256a..47725d7 100644 --- a/src/cmd/read.rs +++ b/src/cmd/read.rs @@ -1,4 +1,4 @@ -use crate::{arg, cmd, data_sources}; +use crate::{arg, cmd, sources}; use anyhow::{anyhow, Result}; use clap::ArgMatches; use std::path::PathBuf; @@ -12,7 +12,7 @@ pub fn read(matches: &ArgMatches) -> Result<(), anyhow::Error> { match matches.value_of(cmd::READ) { Some(csv_file) => { let path = PathBuf::from(csv_file); - let csv = data_sources::read_csv(&path)?; + let csv = sources::read_csv(&path)?; if matches.is_present(arg::DISPLAY) { println!("Display csv file: {}", csv); diff --git a/src/cmd/simple_query.rs b/src/cmd/simple_query.rs index c959f0f..c9426cc 100644 --- a/src/cmd/simple_query.rs +++ b/src/cmd/simple_query.rs @@ -1,4 +1,4 @@ -use crate::{arg, cmd, data_sources::ConnVars}; +use crate::{arg, cmd, sources::ConnVars}; use anyhow::{anyhow, Result}; use clap::ArgMatches; use postgres::{Client, NoTls, SimpleQueryMessage}; diff --git a/src/email_builder/bulk_receiver.rs b/src/email_builder/bulk_receiver.rs index f9b65f7..a222c63 100644 --- a/src/email_builder/bulk_receiver.rs +++ b/src/email_builder/bulk_receiver.rs @@ -1,6 +1,6 @@ use crate::{ arg, cmd, - data_sources::{self, ConnVars, DbConnection}, + sources::{self, ConnVars, DbConnection}, }; use anyhow::{anyhow, Context}; use clap::ArgMatches; @@ -34,7 +34,7 @@ impl BulkReceiver { let conn_vars = ConnVars::from_env()?; let ssh_tunnel = matches.value_of(arg::SSH_TUNNEL); let connection = DbConnection::new(&conn_vars, ssh_tunnel)?; - let df_receiver = data_sources::query_postgres(&connection, query)?; + let df_receiver = sources::query_postgres(&connection, query)?; if matches.is_present(arg::DISPLAY) { println!("Display query result: {}", df_receiver); @@ -46,7 +46,7 @@ impl BulkReceiver { )) }, (None, Some(path)) => { - let df_receiver = data_sources::read_csv(path)?; + let df_receiver = sources::read_csv(path)?; if matches.is_present(arg::DISPLAY) { println!("Display csv file: {}", df_receiver); diff --git a/src/email_builder/message.rs b/src/email_builder/message.rs index ef180fd..8579c0c 100644 --- a/src/email_builder/message.rs +++ b/src/email_builder/message.rs @@ -283,4 +283,9 @@ mod tests { } ); } + + #[test] + fn test_message_personalize() { + todo!() + } } diff --git a/src/lib.rs b/src/lib.rs index 09b0a60..5544767 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,11 @@ pub mod arg; pub mod cmd; -mod data_sources; mod email_builder; mod email_formatter; mod email_provider; mod email_transmission; mod helper; +mod sources; use arg::val; use clap::{crate_name, crate_version, App, Arg, SubCommand}; diff --git a/src/data_sources/csv.rs b/src/sources/csv.rs similarity index 100% rename from src/data_sources/csv.rs rename to src/sources/csv.rs diff --git a/src/data_sources/image.rs b/src/sources/image.rs similarity index 100% rename from src/data_sources/image.rs rename to src/sources/image.rs diff --git a/src/data_sources/mod.rs b/src/sources/mod.rs similarity index 100% rename from src/data_sources/mod.rs rename to src/sources/mod.rs diff --git a/src/data_sources/postgres.rs b/src/sources/postgres.rs similarity index 100% rename from src/data_sources/postgres.rs rename to src/sources/postgres.rs diff --git a/src/data_sources/ssh_tunnel.rs b/src/sources/ssh_tunnel.rs similarity index 98% rename from src/data_sources/ssh_tunnel.rs rename to src/sources/ssh_tunnel.rs index 72be644..ccd4ef0 100644 --- a/src/data_sources/ssh_tunnel.rs +++ b/src/sources/ssh_tunnel.rs @@ -1,4 +1,4 @@ -use crate::data_sources::postgres::ConnVars; +use crate::sources::postgres::ConnVars; use anyhow::{anyhow, Context, Result}; use std::{ env, diff --git a/tests/cmd/lib.rs b/tests/cmd/lib.rs index d19c368..7bbe22f 100644 --- a/tests/cmd/lib.rs +++ b/tests/cmd/lib.rs @@ -1,4 +1,5 @@ mod test_connect; +mod test_init; mod test_query; mod test_read; mod test_send; diff --git a/tests/cmd/test_init.rs b/tests/cmd/test_init.rs index e69de29..4229e1c 100644 --- a/tests/cmd/test_init.rs +++ b/tests/cmd/test_init.rs @@ -0,0 +1,4 @@ +#[test] +fn test_init() { + todo!() +} From 63a9a7299508ee48e59bf2a9a81cd90915d84dff Mon Sep 17 00:00:00 2001 From: quambene Date: Sat, 30 Mar 2024 17:12:52 +0100 Subject: [PATCH 38/54] Test init --- src/cmd/init.rs | 45 +++++++++++++++++- src/email_builder/message_template.rs | 66 +++++---------------------- tests/cmd/test_init.rs | 18 +++++++- 3 files changed, 73 insertions(+), 56 deletions(-) diff --git a/src/cmd/init.rs b/src/cmd/init.rs index d68a0c8..bca5bc7 100644 --- a/src/cmd/init.rs +++ b/src/cmd/init.rs @@ -1,10 +1,53 @@ use crate::{arg, email_builder::MessageTemplate}; +use anyhow::Context; use clap::ArgMatches; +use std::{env, io}; pub fn init(matches: &ArgMatches) -> Result<(), anyhow::Error> { if matches.is_present(arg::VERBOSE) { println!("matches: {:#?}", matches); } - MessageTemplate::create(matches) + let current_dir = env::current_dir().context("Can't get current directory")?; + let file_name = MessageTemplate::file_name(); + let template_path = current_dir.join(file_name); + + match template_path.exists() { + true => { + let mut input = String::new(); + println!("Message template already exists. Should this template be overwritten? Yes (y) or no (n)"); + loop { + io::stdin() + .read_line(&mut input) + .context("Can't read input")?; + match input.trim() { + "y" | "yes" | "Yes" => { + println!( + "Overwriting message template in current directory '{}' ...", + template_path.display() + ); + MessageTemplate::write(&template_path)?; + break; + } + "n" | "no" | "No" => { + println!("Aborted ..."); + break; + } + _ => { + println!("Choose yes (y) or no (n). Try again."); + continue; + } + } + } + } + false => { + println!( + "Creating message template in current directory: {} ...", + template_path.display() + ); + MessageTemplate::write(&template_path)?; + } + } + + Ok(()) } diff --git a/src/email_builder/message_template.rs b/src/email_builder/message_template.rs index 05f1da7..0b69f84 100644 --- a/src/email_builder/message_template.rs +++ b/src/email_builder/message_template.rs @@ -3,12 +3,13 @@ use anyhow::{anyhow, Context, Result}; use clap::ArgMatches; use serde::Deserialize; use std::{ - env, fs, - io::{self, Write}, - path::PathBuf, + fs, + io::Write, + path::{Path, PathBuf}, }; const TEMPLATE_FILE_NAME: &str = "message.yaml"; + static MESSAGE_TEMPLATE: &str = r##"# Specify the subject, plaintext and html version of your email. # Personalize message by wrapping variables in curly brackets, eg. {first_name}. @@ -28,51 +29,8 @@ pub struct MessageTemplate { } impl MessageTemplate { - pub fn create(_matches: &ArgMatches) -> Result<(), anyhow::Error> { - let current_dir = env::current_dir().context("Can't get current directory")?; - let path_dir = current_dir; - - let file_name = TEMPLATE_FILE_NAME; - let template_path = path_dir.join(file_name); - - match template_path.exists() { - true => { - let mut input = String::new(); - println!("Message template already exists. Should this template be overwritten? Yes (y) or no (n)"); - loop { - io::stdin() - .read_line(&mut input) - .context("Can't read input")?; - match input.trim() { - "y" | "yes" | "Yes" => { - println!( - "Overwriting message template in current directory '{:#?}' ...", - template_path - ); - create_template(template_path)?; - break; - } - "n" | "no" | "No" => { - println!("Aborted ..."); - break; - } - _ => { - println!("Choose yes (y) or no (n). Try again."); - continue; - } - } - } - } - false => { - println!( - "Creating message template in current directory: {:#?} ...", - template_path - ); - create_template(template_path)?; - } - } - - Ok(()) + pub fn file_name() -> &'static str { + TEMPLATE_FILE_NAME } pub fn read(matches: &ArgMatches) -> Result { @@ -99,11 +57,11 @@ impl MessageTemplate { Err(anyhow!("Missing argument '{}'", arg::MESSAGE_FILE)) } } -} -fn create_template(path: PathBuf) -> Result<(), anyhow::Error> { - let mut message_template = - fs::File::create(path).context("Unable to create message template.")?; - message_template.write_all(MESSAGE_TEMPLATE.as_bytes())?; - Ok(()) + pub fn write(path: &Path) -> Result<(), anyhow::Error> { + let mut message_template = + fs::File::create(path).context("Unable to create message template.")?; + message_template.write_all(MESSAGE_TEMPLATE.as_bytes())?; + Ok(()) + } } diff --git a/tests/cmd/test_init.rs b/tests/cmd/test_init.rs index 4229e1c..e9142b4 100644 --- a/tests/cmd/test_init.rs +++ b/tests/cmd/test_init.rs @@ -1,4 +1,20 @@ +use assert_cmd::Command; +use predicates::{boolean::PredicateBooleanExt, str}; +use tempfile::tempdir; + #[test] fn test_init() { - todo!() + let temp_dir = tempdir().unwrap(); + let temp_path = temp_dir.path(); + assert!(temp_path.exists(), "Missing path: {}", temp_path.display()); + + println!("Execute 'pigeon init'"); + let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.current_dir(temp_path); + cmd.args(["init"]); + cmd.assert().success().stdout( + str::contains("Creating message template in current directory:").and(str::contains( + format!("{}/message.yaml", temp_path.display()), + )), + ); } From 99d1dbe5a5cfa1cae6ddd6b1ea385c760899179d Mon Sep 17 00:00:00 2001 From: quambene Date: Sat, 30 Mar 2024 17:56:00 +0100 Subject: [PATCH 39/54] Refactor message template --- src/cmd/init.rs | 8 +- src/email_builder/message.rs | 154 +++++++++++++------------- src/email_builder/message_template.rs | 67 ----------- src/email_builder/mod.rs | 2 - src/helper.rs | 8 ++ 5 files changed, 89 insertions(+), 150 deletions(-) delete mode 100644 src/email_builder/message_template.rs diff --git a/src/cmd/init.rs b/src/cmd/init.rs index bca5bc7..8c9bd91 100644 --- a/src/cmd/init.rs +++ b/src/cmd/init.rs @@ -1,4 +1,4 @@ -use crate::{arg, email_builder::MessageTemplate}; +use crate::{arg, email_builder::Message}; use anyhow::Context; use clap::ArgMatches; use std::{env, io}; @@ -9,7 +9,7 @@ pub fn init(matches: &ArgMatches) -> Result<(), anyhow::Error> { } let current_dir = env::current_dir().context("Can't get current directory")?; - let file_name = MessageTemplate::file_name(); + let file_name = Message::template_name(); let template_path = current_dir.join(file_name); match template_path.exists() { @@ -26,7 +26,7 @@ pub fn init(matches: &ArgMatches) -> Result<(), anyhow::Error> { "Overwriting message template in current directory '{}' ...", template_path.display() ); - MessageTemplate::write(&template_path)?; + Message::write_template(&template_path)?; break; } "n" | "no" | "No" => { @@ -45,7 +45,7 @@ pub fn init(matches: &ArgMatches) -> Result<(), anyhow::Error> { "Creating message template in current directory: {} ...", template_path.display() ); - MessageTemplate::write(&template_path)?; + Message::write_template(&template_path)?; } } diff --git a/src/email_builder/message.rs b/src/email_builder/message.rs index 8579c0c..98778d6 100644 --- a/src/email_builder/message.rs +++ b/src/email_builder/message.rs @@ -1,10 +1,27 @@ -use super::MessageTemplate; -use crate::arg; -use anyhow::{anyhow, Result}; +use crate::{arg, helper}; +use anyhow::{anyhow, Context, Result}; use clap::ArgMatches; -use std::{fs, path::Path}; +use serde::Deserialize; +use std::{ + fs::{self, File}, + io::Write, + path::Path, +}; -#[derive(Debug, Clone, PartialEq)] +const TEMPLATE_FILE_NAME: &str = "message.yaml"; + +static MESSAGE_TEMPLATE: &str = r##"# Specify the subject, plaintext and html version of your email. +# Personalize message by wrapping variables in curly brackets, eg. {first_name}. + +# The subject of your email +subject: "" +# The plaintext version +text: "" +# The html version +html: "" +"##; + +#[derive(Debug, Clone, PartialEq, Deserialize)] pub struct Message { pub subject: String, pub text: Option, @@ -24,60 +41,58 @@ impl Message { } pub fn from_args(matches: &ArgMatches) -> Result { - let message = if matches.is_present(arg::SUBJECT) && matches.is_present(arg::CONTENT) { - Message::from_cmd(matches)? + if matches.is_present(arg::SUBJECT) && matches.is_present(arg::CONTENT) { + match ( + matches.value_of(arg::SUBJECT), + matches.value_of(arg::CONTENT), + ) { + (Some(subject), Some(content)) => { + let message = Message::new(subject, Some(content), None); + Ok(message) + } + (Some(_), None) => Err(anyhow!("Missing value for argument '{}'", arg::CONTENT)), + (None, Some(_)) => Err(anyhow!("Missing value for argument '{}'", arg::SUBJECT)), + (None, None) => Err(anyhow!( + "Missing values for '{}' and '{}'", + arg::SUBJECT, + arg::CONTENT + )), + } } else if matches.is_present(arg::MESSAGE_FILE) { - Message::from_template(matches)? + let message_file = arg::value(arg::MESSAGE_FILE, matches)?; + let message_path = Path::new(message_file); + let message = Message::read(message_path)?; + + if matches.is_present(arg::DISPLAY) { + println!("Display message file: {:#?}", message); + } + + Ok(message) } else if matches.is_present(arg::SUBJECT) && (matches.is_present(arg::TEXT_FILE) || matches.is_present(arg::HTML_FILE)) { - Message::from_file(matches)? + let subject = arg::value(arg::SUBJECT, matches)?; + let text_path = matches.value_of(arg::TEXT_FILE).map(Path::new); + let html_path = matches.value_of(arg::HTML_FILE).map(Path::new); + let text = if let Some(path) = text_path { + Some(helper::read_file(path)?) + } else { + None + }; + let html = if let Some(path) = html_path { + Some(helper::read_file(path)?) + } else { + None + }; + let message = Message::new(subject, text.as_deref(), html.as_deref()); + Ok(message) } else { - return Err(anyhow!( + Err(anyhow!( "Missing arguments. Please provide {} and {} or {}", arg::SUBJECT, arg::CONTENT, arg::MESSAGE_FILE, - )); - }; - - Ok(message) - } - - pub fn from_file(matches: &ArgMatches) -> Result { - let subject = Message::subject(matches)?.to_string(); - let text = Self::read(matches, arg::TEXT_FILE)?; - let html = Self::read(matches, arg::HTML_FILE)?; - let message = Message::new(subject, text, html); - Ok(message) - } - - pub fn from_template(matches: &ArgMatches) -> Result { - let message_template = MessageTemplate::read(matches)?; - let message = Message::new( - message_template.subject, - message_template.text, - message_template.html, - ); - Ok(message) - } - - pub fn from_cmd(matches: &ArgMatches) -> Result { - match ( - matches.value_of(arg::SUBJECT), - matches.value_of(arg::CONTENT), - ) { - (Some(subject), Some(content)) => { - let message = Message::new(subject, Some(content), None); - Ok(message) - } - (Some(_), None) => Err(anyhow!("Missing value for argument '{}'", arg::CONTENT)), - (None, Some(_)) => Err(anyhow!("Missing value for argument '{}'", arg::SUBJECT)), - (None, None) => Err(anyhow!( - "Missing values for '{}' and '{}'", - arg::SUBJECT, - arg::CONTENT - )), + )) } } @@ -95,36 +110,21 @@ impl Message { .map(|html| html.replace(&format!("{{{}}}", col_name), col_value)); } - fn subject<'a>(matches: &'a ArgMatches) -> Result<&'a str, anyhow::Error> { - if matches.is_present(arg::SUBJECT) { - match matches.value_of(arg::SUBJECT) { - Some(subject) => Ok(subject), - None => Err(anyhow!("Missing value for argument '{}'", arg::SUBJECT)), - } - } else { - Err(anyhow!("Missing argument '{}'", arg::SUBJECT)) - } + fn read(path: &Path) -> Result { + println!("Reading message file '{}' ...", path.display()); + let yaml = fs::read_to_string(&path)?; + let message = serde_yaml::from_str(&yaml)?; + Ok(message) } - fn read(matches: &ArgMatches, arg: &str) -> Result, anyhow::Error> { - if matches.is_present(arg) { - match matches.value_of(arg) { - Some(text_file) => { - let path = Path::new(text_file); - println!("Reading text file '{}' ...", path.display()); - let message = fs::read_to_string(path)?; - - if matches.is_present(arg::DISPLAY) { - println!("Display message file: {:#?}", message); - } + pub fn template_name() -> &'static str { + TEMPLATE_FILE_NAME + } - Ok(Some(message)) - } - None => Err(anyhow!("Missing value for argument '{}'", arg)), - } - } else { - Ok(None) - } + pub fn write_template(path: &Path) -> Result<(), anyhow::Error> { + let mut message_file = File::create(path).context("Unable to create message template.")?; + message_file.write_all(MESSAGE_TEMPLATE.as_bytes())?; + Ok(()) } } diff --git a/src/email_builder/message_template.rs b/src/email_builder/message_template.rs deleted file mode 100644 index 0b69f84..0000000 --- a/src/email_builder/message_template.rs +++ /dev/null @@ -1,67 +0,0 @@ -use crate::arg; -use anyhow::{anyhow, Context, Result}; -use clap::ArgMatches; -use serde::Deserialize; -use std::{ - fs, - io::Write, - path::{Path, PathBuf}, -}; - -const TEMPLATE_FILE_NAME: &str = "message.yaml"; - -static MESSAGE_TEMPLATE: &str = r##"# Specify the subject, plaintext and html version of your email. -# Personalize message by wrapping variables in curly brackets, eg. {first_name}. - -# The subject of your email -subject: "" -# The plaintext version -text: "" -# The html version -html: "" -"##; - -#[derive(Debug, Deserialize)] -pub struct MessageTemplate { - pub subject: String, - pub text: Option, - pub html: Option, -} - -impl MessageTemplate { - pub fn file_name() -> &'static str { - TEMPLATE_FILE_NAME - } - - pub fn read(matches: &ArgMatches) -> Result { - if matches.is_present(arg::MESSAGE_FILE) { - match matches.value_of(arg::MESSAGE_FILE) { - Some(message_file) => { - let path = PathBuf::from(message_file); - println!("Reading message file '{}' ...", path.display()); - let yaml = fs::read_to_string(&path)?; - let message = serde_yaml::from_str(&yaml)?; - - if matches.is_present(arg::DISPLAY) { - println!("Display message file: {:#?}", message); - } - - Ok(message) - } - None => Err(anyhow!( - "Missing value for argument '{}'", - arg::MESSAGE_FILE - )), - } - } else { - Err(anyhow!("Missing argument '{}'", arg::MESSAGE_FILE)) - } - } - - pub fn write(path: &Path) -> Result<(), anyhow::Error> { - let mut message_template = - fs::File::create(path).context("Unable to create message template.")?; - message_template.write_all(MESSAGE_TEMPLATE.as_bytes())?; - Ok(()) - } -} diff --git a/src/email_builder/mod.rs b/src/email_builder/mod.rs index 0e98bbc..0840a3a 100644 --- a/src/email_builder/mod.rs +++ b/src/email_builder/mod.rs @@ -2,7 +2,6 @@ mod bulk_email; mod bulk_receiver; mod email; mod message; -mod message_template; mod mime; mod receiver; mod sender; @@ -11,7 +10,6 @@ pub use bulk_email::BulkEmail; pub use bulk_receiver::BulkReceiver; pub use email::Email; pub use message::Message; -pub use message_template::MessageTemplate; pub use mime::MimeFormat; pub use receiver::Receiver; pub use sender::Sender; diff --git a/src/helper.rs b/src/helper.rs index 9d96761..9eeae1a 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -1,3 +1,5 @@ +use std::{fs, path::Path}; + pub fn format_green(text: &str) -> String { const GREEN: &str = "\x1b[32m"; const END: &str = "\x1b[0m"; @@ -11,3 +13,9 @@ pub fn format_red(text: &str) -> String { let red_text = format!("{}{}{}", RED, text, END); red_text } + +pub fn read_file(path: &Path) -> Result { + println!("Reading file '{}' ...", path.display()); + let content = fs::read_to_string(path)?; + Ok(content) +} From 9fe5f4501f66d08c0fb6df15da8ca3e224a2f4bb Mon Sep 17 00:00:00 2001 From: quambene Date: Sat, 30 Mar 2024 17:56:54 +0100 Subject: [PATCH 40/54] Rename helper --- src/cmd/send.rs | 2 +- src/cmd/send_bulk.rs | 2 +- src/email_builder/message.rs | 6 +++--- src/email_provider/aws.rs | 2 +- src/email_transmission/smtp.rs | 2 +- src/email_transmission/status.rs | 2 +- src/lib.rs | 2 +- src/{helper.rs => utils.rs} | 0 8 files changed, 9 insertions(+), 9 deletions(-) rename src/{helper.rs => utils.rs} (100%) diff --git a/src/cmd/send.rs b/src/cmd/send.rs index aa63b98..7f0f631 100644 --- a/src/cmd/send.rs +++ b/src/cmd/send.rs @@ -3,7 +3,7 @@ use crate::{ email_builder::{Confirmed, Email, Message, MimeFormat, Receiver, Sender}, email_formatter::EmlFormatter, email_transmission::Client, - helper::format_green, + utils::format_green, }; use anyhow::Context; use chrono::Utc; diff --git a/src/cmd/send_bulk.rs b/src/cmd/send_bulk.rs index df046e7..b00b8f1 100644 --- a/src/cmd/send_bulk.rs +++ b/src/cmd/send_bulk.rs @@ -3,7 +3,7 @@ use crate::{ email_builder::{BulkEmail, BulkReceiver, Confirmed, Email, Message, Sender}, email_formatter::EmlFormatter, email_transmission::Client, - helper::format_green, + utils::format_green, }; use anyhow::{anyhow, Context, Result}; use chrono::Utc; diff --git a/src/email_builder/message.rs b/src/email_builder/message.rs index 98778d6..56dcb25 100644 --- a/src/email_builder/message.rs +++ b/src/email_builder/message.rs @@ -1,4 +1,4 @@ -use crate::{arg, helper}; +use crate::{arg, utils}; use anyhow::{anyhow, Context, Result}; use clap::ArgMatches; use serde::Deserialize; @@ -75,12 +75,12 @@ impl Message { let text_path = matches.value_of(arg::TEXT_FILE).map(Path::new); let html_path = matches.value_of(arg::HTML_FILE).map(Path::new); let text = if let Some(path) = text_path { - Some(helper::read_file(path)?) + Some(utils::read_file(path)?) } else { None }; let html = if let Some(path) = html_path { - Some(helper::read_file(path)?) + Some(utils::read_file(path)?) } else { None }; diff --git a/src/email_provider/aws.rs b/src/email_provider/aws.rs index efae26d..1643c42 100644 --- a/src/email_provider/aws.rs +++ b/src/email_provider/aws.rs @@ -2,7 +2,7 @@ use crate::{ arg, email_builder::Email, email_transmission::{SendEmail, SentEmail, Status}, - helper::format_green, + utils::format_green, }; use anyhow::{Context, Result}; use bytes::Bytes; diff --git a/src/email_transmission/smtp.rs b/src/email_transmission/smtp.rs index d637a2b..92189c8 100644 --- a/src/email_transmission/smtp.rs +++ b/src/email_transmission/smtp.rs @@ -1,7 +1,7 @@ use super::{SendEmail, SentEmail, Status}; use crate::{ email_builder::Email, - helper::{format_green, format_red}, + utils::{format_green, format_red}, }; use anyhow::Context; use lettre::{transport::smtp::authentication::Credentials, SmtpTransport, Transport}; diff --git a/src/email_transmission/status.rs b/src/email_transmission/status.rs index 2c0cdd8..c601305 100644 --- a/src/email_transmission/status.rs +++ b/src/email_transmission/status.rs @@ -1,4 +1,4 @@ -use crate::helper::{format_green, format_red}; +use crate::utils::{format_green, format_red}; use std::fmt; #[derive(Debug, Clone)] diff --git a/src/lib.rs b/src/lib.rs index 5544767..79fc7b8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,8 +4,8 @@ mod email_builder; mod email_formatter; mod email_provider; mod email_transmission; -mod helper; mod sources; +mod utils; use arg::val; use clap::{crate_name, crate_version, App, Arg, SubCommand}; diff --git a/src/helper.rs b/src/utils.rs similarity index 100% rename from src/helper.rs rename to src/utils.rs From 58d0f5c8df8a16cca077cada2e992020c7d7e27d Mon Sep 17 00:00:00 2001 From: quambene Date: Sat, 30 Mar 2024 17:58:26 +0100 Subject: [PATCH 41/54] Add integration and os tests to CI --- .github/workflows/rust-ci.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 988b8f8..5cf6e7d 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -50,6 +50,27 @@ jobs: - uses: dtolnay/rust-toolchain@stable - name: cargo test --lib run: cargo test --lib --locked + integration-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: cargo test --test '*' + run: cargo test --test '*' --locked + os-test: + runs-on: ${{ matrix.os }} + name: os-test / ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [macos-latest, windows-latest] + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: cargo test --lib + run: cargo test --lib --locked + - name: cargo test --test '*' + run: cargo test --test '*' --locked doc-test: runs-on: ubuntu-latest steps: From f79efca2b3653614b62d00e2b2fc7b3e4242769f Mon Sep 17 00:00:00 2001 From: quambene Date: Sat, 30 Mar 2024 19:07:32 +0100 Subject: [PATCH 42/54] Test personalize --- src/email_builder/message.rs | 143 ++++++++++++++++------------ test_data/message_personalized.yaml | 2 +- 2 files changed, 81 insertions(+), 64 deletions(-) diff --git a/src/email_builder/message.rs b/src/email_builder/message.rs index 56dcb25..6f6e063 100644 --- a/src/email_builder/message.rs +++ b/src/email_builder/message.rs @@ -61,7 +61,7 @@ impl Message { } else if matches.is_present(arg::MESSAGE_FILE) { let message_file = arg::value(arg::MESSAGE_FILE, matches)?; let message_path = Path::new(message_file); - let message = Message::read(message_path)?; + let message = Message::read_yaml(message_path)?; if matches.is_present(arg::DISPLAY) { println!("Display message file: {:#?}", message); @@ -110,7 +110,7 @@ impl Message { .map(|html| html.replace(&format!("{{{}}}", col_name), col_value)); } - fn read(path: &Path) -> Result { + fn read_yaml(path: &Path) -> Result { println!("Reading message file '{}' ...", path.display()); let yaml = fs::read_to_string(&path)?; let message = serde_yaml::from_str(&yaml)?; @@ -134,23 +134,10 @@ mod tests { use crate::app; #[test] - fn test_message_from_args_subject_content() { - let args = vec![ - "pigeon", - "send", - "albert@einstein.com", - "marie@curie.com", - "--subject", - "Test subject", - "--content", - "This is a test message (plaintext).", - ]; - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches("send").unwrap(); - - let res = Message::from_args(subcommand_matches); - assert!(res.is_ok(), "{}", res.unwrap_err()); + fn test_read_yaml() { + let yaml_path = Path::new("./test_data/message.yaml"); + let res = Message::read_yaml(yaml_path); + assert!(res.is_ok()); let message = res.unwrap(); assert_eq!( @@ -158,57 +145,83 @@ mod tests { Message { subject: "Test subject".to_owned(), text: Some("This is a test message (plaintext).".to_owned()), - html: None, + html: Some("

This is a test message (html).

".to_owned()) } - ); + ) } #[test] - fn test_message_from_args_text_and_html_file() { - let args = vec![ - "pigeon", - "send", - "albert@einstein.com", - "marie@curie.com", - "--subject", - "Test subject", - "--text-file", - "./test_data/message.txt", - "--html-file", - "./test_data/message.html", - ]; - let app = app(); - let matches = app.get_matches_from(args); - let subcommand_matches = matches.subcommand_matches("send").unwrap(); + fn test_read_yaml_empty() { + let yaml_path = Path::new("./test_data/message_empty.yaml"); + let res = Message::read_yaml(yaml_path); + assert!(res.is_ok()); - let res = Message::from_args(subcommand_matches); - assert!(res.is_ok(), "{}", res.unwrap_err()); + let message = res.unwrap(); + assert_eq!( + message, + Message { + subject: "Test subject".to_owned(), + text: Some("".to_owned()), + html: Some("".to_owned()) + } + ) + } + + #[test] + fn test_read_yaml_none() { + let yaml_path = Path::new("./test_data/message_none.yaml"); + let res = Message::read_yaml(yaml_path); + assert!(res.is_ok()); let message = res.unwrap(); assert_eq!( message, Message { subject: "Test subject".to_owned(), - text: Some( - "This is a test message (plaintext).\n\nThis is the last line.".to_owned() - ), + text: None, + html: None, + } + ) + } + + #[test] + fn test_personalize() { + let text = r#"Dear {first_name} {last_name}, +This is a test message (plaintext)."#; + let html = r#"Dear {first_name} {last_name}, +
+
+This is a test message (html)."#; + let mut message = Message::new( + "Test subject".to_owned(), + Some(text.to_owned()), + Some(html.to_owned()), + ); + message.personalize("first_name", "Marie"); + message.personalize("last_name", "Curie"); + assert_eq!( + message, + Message { + subject: "Test subject".to_owned(), + text: Some("Dear Marie Curie,\nThis is a test message (plaintext).".to_owned()), html: Some( - "

This is a test message (html).

\n\n

This is the last line.

" - .to_owned() - ), + "Dear Marie Curie,\n
\n
\nThis is a test message (html).".to_owned() + ) } ); } #[test] - fn test_message_from_args_message_file() { + fn test_message_from_args_subject_content() { let args = vec![ "pigeon", "send", "albert@einstein.com", "marie@curie.com", - "--message-file", - "./test_data/message.yaml", + "--subject", + "Test subject", + "--content", + "This is a test message (plaintext).", ]; let app = app(); let matches = app.get_matches_from(args); @@ -223,20 +236,24 @@ mod tests { Message { subject: "Test subject".to_owned(), text: Some("This is a test message (plaintext).".to_owned()), - html: Some("

This is a test message (html).

".to_owned()), + html: None, } ); } #[test] - fn test_message_from_args_message_file_empty() { + fn test_message_from_args_text_and_html_file() { let args = vec![ "pigeon", "send", "albert@einstein.com", "marie@curie.com", - "--message-file", - "./test_data/message_empty.yaml", + "--subject", + "Test subject", + "--text-file", + "./test_data/message.txt", + "--html-file", + "./test_data/message.html", ]; let app = app(); let matches = app.get_matches_from(args); @@ -250,21 +267,26 @@ mod tests { message, Message { subject: "Test subject".to_owned(), - text: Some("".to_owned()), - html: Some("".to_owned()), + text: Some( + "This is a test message (plaintext).\n\nThis is the last line.".to_owned() + ), + html: Some( + "

This is a test message (html).

\n\n

This is the last line.

" + .to_owned() + ), } ); } #[test] - fn test_message_from_args_message_none() { + fn test_message_from_args_message_file() { let args = vec![ "pigeon", "send", "albert@einstein.com", "marie@curie.com", "--message-file", - "./test_data/message_none.yaml", + "./test_data/message.yaml", ]; let app = app(); let matches = app.get_matches_from(args); @@ -278,14 +300,9 @@ mod tests { message, Message { subject: "Test subject".to_owned(), - text: None, - html: None, + text: Some("This is a test message (plaintext).".to_owned()), + html: Some("

This is a test message (html).

".to_owned()), } ); } - - #[test] - fn test_message_personalize() { - todo!() - } } diff --git a/test_data/message_personalized.yaml b/test_data/message_personalized.yaml index f0a92a1..b3f49ac 100644 --- a/test_data/message_personalized.yaml +++ b/test_data/message_personalized.yaml @@ -5,7 +5,7 @@ subject: "Test subject" # The plaintext version text: "Dear {first_name} {last_name}, - \nThis is a test message (plaintext)." + This is a test message (plaintext)." # The html version html: "Dear {first_name} {last_name},
From fa228001d3f905ae73d0194ae84a2d5a5f81cdf5 Mon Sep 17 00:00:00 2001 From: quambene Date: Sat, 30 Mar 2024 19:44:46 +0100 Subject: [PATCH 43/54] Test bulk receiver --- src/cmd/send.rs | 4 +- src/cmd/send_bulk.rs | 4 +- src/email_builder/bulk_receiver.rs | 125 ----------------- src/email_builder/mod.rs | 4 +- src/email_builder/receiver.rs | 214 +++++++++++++++++++++++++++-- src/email_builder/sender.rs | 17 +-- tests/cmd/test_send_bulk.rs | 147 ++------------------ 7 files changed, 220 insertions(+), 295 deletions(-) delete mode 100644 src/email_builder/bulk_receiver.rs diff --git a/src/cmd/send.rs b/src/cmd/send.rs index 7f0f631..f7ee443 100644 --- a/src/cmd/send.rs +++ b/src/cmd/send.rs @@ -18,8 +18,8 @@ pub fn send(matches: &ArgMatches) -> Result<(), anyhow::Error> { let dry_run = matches.is_present(arg::DRY_RUN); let is_archived = matches.is_present(arg::ARCHIVE); let now = SystemTime::now(); - let sender = Sender::from_args(matches)?; - let receiver = Receiver::from_args(matches)?; + let sender = Sender(arg::value(arg::SENDER, matches)?); + let receiver = Receiver(arg::value(arg::RECEIVER, matches)?); let message = Message::from_args(matches)?; let attachment = matches.value_of(arg::ATTACHMENT).map(Path::new); let mime_format = MimeFormat::new(sender, receiver, &message, attachment, now)?; diff --git a/src/cmd/send_bulk.rs b/src/cmd/send_bulk.rs index b00b8f1..f844337 100644 --- a/src/cmd/send_bulk.rs +++ b/src/cmd/send_bulk.rs @@ -17,7 +17,7 @@ pub fn send_bulk(matches: &ArgMatches) -> Result<(), anyhow::Error> { let dry_run = matches.is_present(arg::DRY_RUN); let is_archived = matches.is_present(arg::ARCHIVE); - let sender = Sender::from_args(matches)?; + let sender = Sender(arg::value(arg::SENDER, matches)?); let receivers = BulkReceiver::from_args(matches)?; let message = Message::from_args(matches)?; let attachment = matches.value_of(arg::ATTACHMENT).map(Path::new); @@ -103,7 +103,7 @@ pub fn confirm_emails(emails: &[Email]) -> Result { let email_count = emails.len(); let receivers = emails .iter() - .map(|email| email.receiver.as_str()) + .map(|email| email.receiver.as_ref()) .collect::>(); println!( "Preparing to send an email to {} recipients: {:#?}", diff --git a/src/email_builder/bulk_receiver.rs b/src/email_builder/bulk_receiver.rs deleted file mode 100644 index a222c63..0000000 --- a/src/email_builder/bulk_receiver.rs +++ /dev/null @@ -1,125 +0,0 @@ -use crate::{ - arg, cmd, - sources::{self, ConnVars, DbConnection}, -}; -use anyhow::{anyhow, Context}; -use clap::ArgMatches; -use polars::{ - chunked_array::{ops::TakeRandom, ChunkedArray}, - datatypes::Utf8Type, - frame::DataFrame, -}; -use std::path::Path; - -pub struct BulkReceiver { - pub column_name: String, - pub df_receiver: DataFrame, -} - -impl BulkReceiver { - pub fn new(column_name: String, df_receiver: DataFrame) -> Self { - Self { - column_name, - df_receiver, - } - } - - pub fn from_args(matches: &ArgMatches) -> Result { - let column_name = arg::value(arg::RECEIVER_COLUMN, matches)?; - let receiver_query = matches.value_of(arg::RECEIVER_QUERY); - let receiver_path = matches.value_of(arg::RECEIVER_FILE).map(Path::new); - - match (receiver_query, receiver_path) { - (Some(query), None) => { - let conn_vars = ConnVars::from_env()?; - let ssh_tunnel = matches.value_of(arg::SSH_TUNNEL); - let connection = DbConnection::new(&conn_vars, ssh_tunnel)?; - let df_receiver = sources::query_postgres(&connection, query)?; - - if matches.is_present(arg::DISPLAY) { - println!("Display query result: {}", df_receiver); - } - - Ok(Self::new( - column_name.to_owned() , - df_receiver, - )) - }, - (None, Some(path)) => { - let df_receiver = sources::read_csv(path)?; - - if matches.is_present(arg::DISPLAY) { - println!("Display csv file: {}", df_receiver); - } - - Ok(Self::new( - column_name.to_owned() , - df_receiver, - )) - }, - (Some(_), Some(_)) => { - Err(anyhow!( - "Argument conflict: arguments {} and {} are not allowed at the same time. Check usage via '{} help {}'", - arg::RECEIVER_QUERY, - arg::RECEIVER_FILE, - cmd::BIN, - cmd::SEND_BULK, - )) - }, - (None, None) => { - Err(anyhow!( - "Missing arguments: please specify argument {} or {}. Check usage via '{} help {}'", - arg::RECEIVER_QUERY, - arg::RECEIVER_FILE, - cmd::BIN, - cmd::SEND_BULK, - )) - }, - } - } - - pub fn height(&self) -> usize { - self.df_receiver.height() - } - - pub fn receiver_column(&self) -> Result<&ChunkedArray, anyhow::Error> { - self.column(&self.column_name) - } - - pub fn receiver_row(&self, index: usize) -> Result<&str, anyhow::Error> { - self.row(index, &self.column_name) - } - - pub fn row<'a>(&'a self, index: usize, column_name: &str) -> Result<&'a str, anyhow::Error> { - let column_value = self - .df_receiver - .column(column_name) - .context(format!("Missing column '{}'", column_name))? - .utf8() - .context("Can't convert series to chunked array")? - .get(index); - - match column_value { - Some(column_value) => Ok(column_value), - None => Err(anyhow!( - "Missing value for column '{}' in row {}", - column_name, - index - )), - } - } - - pub fn column<'a>( - &'a self, - column_name: &str, - ) -> Result<&'a ChunkedArray, anyhow::Error> { - let column = self - .df_receiver - .column(column_name) - .context(format!("Missing column '{column_name}'"))? - .utf8() - .context("Can't convert series to chunked array")?; - - Ok(column) - } -} diff --git a/src/email_builder/mod.rs b/src/email_builder/mod.rs index 0840a3a..e10841f 100644 --- a/src/email_builder/mod.rs +++ b/src/email_builder/mod.rs @@ -1,5 +1,4 @@ mod bulk_email; -mod bulk_receiver; mod email; mod message; mod mime; @@ -7,11 +6,10 @@ mod receiver; mod sender; pub use bulk_email::BulkEmail; -pub use bulk_receiver::BulkReceiver; pub use email::Email; pub use message::Message; pub use mime::MimeFormat; -pub use receiver::Receiver; +pub use receiver::{BulkReceiver, Receiver}; pub use sender::Sender; pub enum Confirmed { diff --git a/src/email_builder/receiver.rs b/src/email_builder/receiver.rs index 60baa09..9b79173 100644 --- a/src/email_builder/receiver.rs +++ b/src/email_builder/receiver.rs @@ -1,23 +1,213 @@ -use crate::arg; -use anyhow::anyhow; +use crate::{ + arg, cmd, + sources::{self, ConnVars, DbConnection}, +}; +use anyhow::{anyhow, Context}; use clap::ArgMatches; +use polars::{ + chunked_array::{ops::TakeRandom, ChunkedArray}, + datatypes::Utf8Type, + frame::DataFrame, +}; +use std::path::Path; #[derive(Debug, Clone, Copy, PartialEq)] pub struct Receiver<'a>(pub &'a str); -impl<'a> Receiver<'a> { - pub fn as_str(&self) -> &str { +impl<'a> AsRef for Receiver<'a> { + fn as_ref(&self) -> &str { self.0 } +} - pub fn from_args(matches: &'a ArgMatches) -> Result { - if matches.is_present(arg::RECEIVER) { - match matches.value_of(arg::RECEIVER) { - Some(receiver) => Ok(Self(receiver)), - None => Err(anyhow!("Missing value for argument '{}'", arg::RECEIVER)), - } - } else { - Err(anyhow!("Missing argument '{}'", arg::RECEIVER)) +#[derive(Debug, PartialEq)] +pub struct BulkReceiver { + pub column_name: String, + pub df_receiver: DataFrame, +} + +impl BulkReceiver { + pub fn new(column_name: String, df_receiver: DataFrame) -> Self { + Self { + column_name, + df_receiver, } } + + pub fn from_args(matches: &ArgMatches) -> Result { + let column_name = arg::value(arg::RECEIVER_COLUMN, matches)?; + let receiver_query = matches.value_of(arg::RECEIVER_QUERY); + let receiver_path = matches.value_of(arg::RECEIVER_FILE).map(Path::new); + + match (receiver_query, receiver_path) { + (Some(query), None) => { + let conn_vars = ConnVars::from_env()?; + let ssh_tunnel = matches.value_of(arg::SSH_TUNNEL); + let connection = DbConnection::new(&conn_vars, ssh_tunnel)?; + let df_receiver = sources::query_postgres(&connection, query)?; + + if matches.is_present(arg::DISPLAY) { + println!("Display query result: {}", df_receiver); + } + + Ok(Self::new( + column_name.to_owned() , + df_receiver, + )) + }, + (None, Some(path)) => { + let df_receiver = sources::read_csv(path)?; + + if matches.is_present(arg::DISPLAY) { + println!("Display csv file: {}", df_receiver); + } + + Ok(Self::new( + column_name.to_owned() , + df_receiver, + )) + }, + (Some(_), Some(_)) => { + Err(anyhow!( + "Argument conflict: arguments {} and {} are not allowed at the same time. Check usage via '{} help {}'", + arg::RECEIVER_QUERY, + arg::RECEIVER_FILE, + cmd::BIN, + cmd::SEND_BULK, + )) + }, + (None, None) => { + Err(anyhow!( + "Missing arguments: please specify argument {} or {}. Check usage via '{} help {}'", + arg::RECEIVER_QUERY, + arg::RECEIVER_FILE, + cmd::BIN, + cmd::SEND_BULK, + )) + }, + } + } + + pub fn height(&self) -> usize { + self.df_receiver.height() + } + + pub fn receiver_column(&self) -> Result<&ChunkedArray, anyhow::Error> { + self.column(&self.column_name) + } + + pub fn receiver_row(&self, index: usize) -> Result<&str, anyhow::Error> { + self.row(index, &self.column_name) + } + + pub fn row<'a>(&'a self, index: usize, column_name: &str) -> Result<&'a str, anyhow::Error> { + let column_value = self + .df_receiver + .column(column_name) + .context(format!("Missing column '{}'", column_name))? + .utf8() + .context("Can't convert series to chunked array")? + .get(index); + + match column_value { + Some(column_value) => Ok(column_value), + None => Err(anyhow!( + "Missing value for column '{}' in row {}", + column_name, + index + )), + } + } + + pub fn column<'a>( + &'a self, + column_name: &str, + ) -> Result<&'a ChunkedArray, anyhow::Error> { + let column = self + .df_receiver + .column(column_name) + .context(format!("Missing column '{column_name}'"))? + .utf8() + .context("Can't convert series to chunked array")?; + + Ok(column) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::app; + use polars::{prelude::NamedFrom, series::Series}; + + #[test] + fn test_bulk_receiver_from_args_receiver_file() { + let args = vec![ + "pigeon", + "send-bulk", + "albert@einstein.com", + "--receiver-file", + "./test_data/receiver.csv", + "--message-file", + "./test_data/message.yaml", + ]; + let app = app(); + let matches = app.get_matches_from(args); + let subcommand_matches = matches.subcommand_matches("send-bulk").unwrap(); + + let res = BulkReceiver::from_args(subcommand_matches); + assert!(res.is_ok(), "{}", res.unwrap_err()); + + let actual = res.unwrap(); + let first_name_column = Series::new("first_name", &["Marie", "Alexandre"]); + let last_name_column = Series::new("last_name", &["Curie", "Grothendieck"]); + let email_column = Series::new("email", &["marie@curie.com", "alexandre@grothendieck.com"]); + let expected = + DataFrame::new(vec![first_name_column, last_name_column, email_column]).unwrap(); + assert_eq!( + actual, + BulkReceiver { + column_name: "email".to_owned(), + df_receiver: expected + } + ); + } + + #[test] + fn test_bulk_receiver_from_args_receiver_column() { + let args = vec![ + "pigeon", + "send-bulk", + "albert@einstein.com", + "--receiver-file", + "./test_data/contacts.csv", + "--message-file", + "./test_data/message.yaml", + "--receiver-column", + "contact", + ]; + let app = app(); + let matches = app.get_matches_from(args); + let subcommand_matches = matches.subcommand_matches("send-bulk").unwrap(); + + let res = BulkReceiver::from_args(subcommand_matches); + assert!(res.is_ok(), "{}", res.unwrap_err()); + + let actual = res.unwrap(); + let first_name_column = Series::new("first_name", &["Marie", "Alexandre"]); + let last_name_column = Series::new("last_name", &["Curie", "Grothendieck"]); + let email_column = Series::new( + "contact", + &["marie@curie.com", "alexandre@grothendieck.com"], + ); + let expected = + DataFrame::new(vec![first_name_column, last_name_column, email_column]).unwrap(); + assert_eq!( + actual, + BulkReceiver { + column_name: "contact".to_owned(), + df_receiver: expected + } + ); + } } diff --git a/src/email_builder/sender.rs b/src/email_builder/sender.rs index 2363394..14ba161 100644 --- a/src/email_builder/sender.rs +++ b/src/email_builder/sender.rs @@ -1,19 +1,8 @@ -use crate::arg; -use anyhow::anyhow; -use clap::ArgMatches; - #[derive(Debug, Copy, Clone, PartialEq)] pub struct Sender<'a>(pub &'a str); -impl<'a> Sender<'a> { - pub fn from_args(matches: &'a ArgMatches) -> Result { - if matches.is_present(arg::SENDER) { - match matches.value_of(arg::SENDER) { - Some(sender) => Ok(Sender(sender)), - None => Err(anyhow!("Missing value for argument '{}'", arg::SENDER)), - } - } else { - Err(anyhow!("Missing argument '{}'", arg::SENDER)) - } +impl<'a> AsRef for Sender<'a> { + fn as_ref(&self) -> &str { + self.0 } } diff --git a/tests/cmd/test_send_bulk.rs b/tests/cmd/test_send_bulk.rs index 2726f0a..e1d5a17 100644 --- a/tests/cmd/test_send_bulk.rs +++ b/tests/cmd/test_send_bulk.rs @@ -29,7 +29,7 @@ fn test_send_bulk_subject_content_dry() { #[test] #[ignore] -fn test_send_bulk_text_file_html_file_dry() { +fn test_send_bulk_subject_content() { println!("Execute 'pigeon send-bulk'"); let mut cmd = Command::cargo_bin("pigeon").unwrap(); cmd.args([ @@ -39,20 +39,20 @@ fn test_send_bulk_text_file_html_file_dry() { "./test_data/receiver.csv", "--subject", "Test Subject", - "--text-file", - "./test_data/message.txt", - "--html-file", - "./test_data/message.html", - "--dry-run", + "--content", + "This is a test message (plaintext).", "--display", "--assume-yes", ]); - cmd.assert().success(); + cmd.assert().success().stdout( + str::contains("Reading csv file './test_data/receiver.csv' ...") + .and(str::contains("Display csv file:").and(str::contains("Display emails:"))), + ); } #[test] #[ignore] -fn test_send_bulk_message_file_dry() { +fn test_send_bulk_aws_dry() { println!("Execute 'pigeon send-bulk'"); let mut cmd = Command::cargo_bin("pigeon").unwrap(); cmd.args([ @@ -65,49 +65,8 @@ fn test_send_bulk_message_file_dry() { "--dry-run", "--display", "--assume-yes", - ]); - cmd.assert().success(); -} - -#[test] -#[ignore] -fn test_send_bulk_receiver_column_dry() { - println!("Execute 'pigeon send-bulk'"); - let mut cmd = Command::cargo_bin("pigeon").unwrap(); - cmd.args([ - "send-bulk", - "albert@einstein.com", - "--receiver-file", - "./test_data/contacts.csv", - "--message-file", - "./test_data/message.yaml", - "--receiver-column", - "contact", - "--dry-run", - "--display", - "--assume-yes", - ]); - cmd.assert().success(); -} - -#[test] -#[ignore] -fn test_send_bulk_personalize_dry() { - println!("Execute 'pigeon send-bulk'"); - let mut cmd = Command::cargo_bin("pigeon").unwrap(); - cmd.args([ - "send-bulk", - "albert@einstein.com", - "--receiver-file", - "./test_data/receiver.csv", - "--message-file", - "./test_data/message_personalized.yaml", - "--personalize", - "first_name", - "last_name", - "--dry-run", - "--display", - "--assume-yes", + "--connection", + "aws", ]); cmd.assert().success(); } @@ -153,89 +112,3 @@ fn test_archive_dir_dry() { ]); cmd.assert().success(); } - -#[test] -#[ignore] -fn test_attachment_pdf_dry() { - println!("Execute 'pigeon send-bulk'"); - let mut cmd = Command::cargo_bin("pigeon").unwrap(); - cmd.args([ - "send-bulk", - "albert@einstein.com", - "--receiver-file", - "./test_data/receiver.csv", - "--message-file", - "./test_data/message.yaml", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - "--attachment", - "./test_data/test.pdf", - ]); - cmd.assert().success(); -} - -#[test] -#[ignore] -fn test_attachment_png_dry() { - println!("Execute 'pigeon send-bulk'"); - let mut cmd = Command::cargo_bin("pigeon").unwrap(); - cmd.args([ - "send-bulk", - "albert@einstein.com", - "--receiver-file", - "./test_data/receiver.csv", - "--message-file", - "./test_data/message.yaml", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - "--attachment", - "./test_data/test.png", - ]); - cmd.assert().success(); -} - -#[test] -#[ignore] -fn test_attachment_odt_dry() { - println!("Execute 'pigeon send-bulk'"); - let mut cmd = Command::cargo_bin("pigeon").unwrap(); - cmd.args([ - "send-bulk", - "albert@einstein.com", - "--receiver-file", - "./test_data/receiver.csv", - "--message-file", - "./test_data/message.yaml", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - "--attachment", - "./test_data/test.odt", - ]); - cmd.assert().success(); -} - -#[test] -#[ignore] -fn test_send_bulk_aws_dry() { - println!("Execute 'pigeon send-bulk'"); - let mut cmd = Command::cargo_bin("pigeon").unwrap(); - cmd.args([ - "send-bulk", - "albert@einstein.com", - "--receiver-file", - "./test_data/receiver.csv", - "--message-file", - "./test_data/message.yaml", - "--dry-run", - "--display", - "--assume-yes", - "--connection", - ]); - cmd.assert().success(); -} From cd6ac0407dca128cf42b459ef57c8072d0a12eab Mon Sep 17 00:00:00 2001 From: quambene Date: Sat, 30 Mar 2024 20:20:02 +0100 Subject: [PATCH 44/54] Test bulk email --- src/cmd/send_bulk.rs | 12 +++- src/email_builder/bulk_email.rs | 107 ++++++++++++++++++++++---------- 2 files changed, 84 insertions(+), 35 deletions(-) diff --git a/src/cmd/send_bulk.rs b/src/cmd/send_bulk.rs index f844337..57150b2 100644 --- a/src/cmd/send_bulk.rs +++ b/src/cmd/send_bulk.rs @@ -24,13 +24,19 @@ pub fn send_bulk(matches: &ArgMatches) -> Result<(), anyhow::Error> { let bulk_email = if matches.is_present(arg::PERSONALIZE) { if let Some(personalized_columns) = matches.values_of(arg::PERSONALIZE) { - let columns = personalized_columns.collect::>(); - BulkEmail::personalize(sender, &receivers, &message, &columns, attachment)? + let personalized_columns = personalized_columns.collect::>(); + BulkEmail::new( + sender, + &receivers, + &message, + attachment, + &personalized_columns, + )? } else { return Err(anyhow!("Missing value for argument '{}'", arg::PERSONALIZE)); } } else { - BulkEmail::new(sender, &receivers, &message, attachment)? + BulkEmail::new(sender, &receivers, &message, attachment, &[])? }; let client = Client::from_args(matches)?; let eml_formatter = EmlFormatter::from_args(matches)?; diff --git a/src/email_builder/bulk_email.rs b/src/email_builder/bulk_email.rs index 8e22695..2780a94 100644 --- a/src/email_builder/bulk_email.rs +++ b/src/email_builder/bulk_email.rs @@ -14,45 +14,35 @@ impl<'a> BulkEmail<'a> { bulk_receiver: &'a BulkReceiver, message: &'a Message, attachment: Option<&Path>, + personalized_columns: &[&str], ) -> Result { let now = SystemTime::now(); let mut emails: Vec = vec![]; - let receivers = bulk_receiver.receiver_column()?; - for receiver in receivers.into_iter().flatten() { - let mime_format = - MimeFormat::new(sender, Receiver(receiver), message, attachment, now)?; - let email = Email::new(sender, Receiver(receiver), message, &mime_format)?; - emails.push(email); - } + if personalized_columns.is_empty() { + let receivers = bulk_receiver.receiver_column()?; + for receiver in receivers.into_iter().flatten() { + let mime_format = + MimeFormat::new(sender, Receiver(receiver), message, attachment, now)?; + let email = Email::new(sender, Receiver(receiver), message, &mime_format)?; + emails.push(email); + } + } else { + for i in 0..bulk_receiver.height() { + let mut message = message.clone(); - Ok(BulkEmail { emails }) - } + for &col_name in personalized_columns.iter() { + let col_value = bulk_receiver.row(i, col_name)?; + message.personalize(col_name, col_value); + } - pub fn personalize( - sender: Sender<'a>, - receivers: &'a BulkReceiver, - message: &Message, - columns: &[&str], - attachment: Option<&Path>, - ) -> Result { - let now = SystemTime::now(); - let mut emails: Vec = vec![]; + let receiver = bulk_receiver.receiver_row(i)?; + let mime_format = + MimeFormat::new(sender, Receiver(receiver), &message, attachment, now)?; + let email = Email::new(sender, Receiver(receiver), &message, &mime_format)?; - for i in 0..receivers.height() { - let mut message = message.clone(); - - for &col_name in columns.iter() { - let col_value = receivers.row(i, col_name)?; - message.personalize(col_name, col_value); + emails.push(email); } - - let receiver = receivers.receiver_row(i)?; - let mime_format = - MimeFormat::new(sender, Receiver(receiver), &message, attachment, now)?; - let email = Email::new(sender, Receiver(receiver), &message, &mime_format)?; - - emails.push(email); } Ok(BulkEmail { emails }) @@ -76,7 +66,7 @@ mod tests { let df_receiver = DataFrame::new(vec![receiver_column]).unwrap(); let receivers = BulkReceiver::new(column_name.to_owned(), df_receiver); - let res = BulkEmail::new(sender, &receivers, &message, None); + let res = BulkEmail::new(sender, &receivers, &message, None, &[]); assert!(res.is_ok()); let emails = res.unwrap().emails; @@ -90,4 +80,57 @@ mod tests { assert!(receivers.contains(&Receiver("marie@curie.com"))); assert!(receivers.contains(&Receiver("emmy@noether.com"))); } + + #[test] + fn test_bulk_email_personalized() { + let sender = Sender("albert@einstein.com"); + let subject = "Test Subject"; + let text = r#"Dear {first_name} {last_name}, +This is a test message (plaintext)."#; + let html = r#"Dear {first_name} {last_name}, +
+
+This is a test message (html)."#; + let message = Message::new(subject, Some(text), Some(html)); + let first_name_column = Series::new("first_name", &["Marie", "Emmy"]); + let last_name_column = Series::new("last_name", &["Curie", "Noether"]); + let email_column = Series::new("email", &["marie@curie.com", "emmy@noether.com"]); + let df_receiver = + DataFrame::new(vec![first_name_column, last_name_column, email_column]).unwrap(); + let receivers = BulkReceiver::new("email".to_owned(), df_receiver); + + let res = BulkEmail::new( + sender, + &receivers, + &message, + None, + &["first_name", "last_name"], + ); + assert!(res.is_ok()); + + let emails = res.unwrap().emails; + let receivers = emails + .iter() + .map(|email| email.receiver) + .collect::>(); + assert!(receivers.contains(&Receiver("marie@curie.com"))); + assert!(receivers.contains(&Receiver("emmy@noether.com"))); + + let text_messages = emails + .iter() + .map(|email| email.message.text.as_ref().unwrap().as_str()) + .collect::>(); + assert!(text_messages.contains(&"Dear Marie Curie,\nThis is a test message (plaintext).")); + assert!(text_messages.contains(&"Dear Emmy Noether,\nThis is a test message (plaintext).")); + + let html_messages = emails + .iter() + .map(|email| email.message.html.as_ref().unwrap().as_str()) + .collect::>(); + dbg!(&html_messages); + assert!(html_messages + .contains(&"Dear Marie Curie,\n
\n
\nThis is a test message (html).")); + assert!(html_messages + .contains(&"Dear Emmy Noether,\n
\n
\nThis is a test message (html).")); + } } From 7e527550c9cc9c30d8c19fa22ba8fd6633be71cf Mon Sep 17 00:00:00 2001 From: quambene Date: Sat, 30 Mar 2024 20:21:20 +0100 Subject: [PATCH 45/54] Fix clippy warnings --- src/email_builder/message.rs | 2 +- src/sources/csv.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/email_builder/message.rs b/src/email_builder/message.rs index 6f6e063..35a1e2d 100644 --- a/src/email_builder/message.rs +++ b/src/email_builder/message.rs @@ -112,7 +112,7 @@ impl Message { fn read_yaml(path: &Path) -> Result { println!("Reading message file '{}' ...", path.display()); - let yaml = fs::read_to_string(&path)?; + let yaml = fs::read_to_string(path)?; let message = serde_yaml::from_str(&yaml)?; Ok(message) } diff --git a/src/sources/csv.rs b/src/sources/csv.rs index 48de0fa..99c1576 100644 --- a/src/sources/csv.rs +++ b/src/sources/csv.rs @@ -60,7 +60,7 @@ mod tests { Row::new( ["Marie", "Curie", "marie@curie.com"] .into_iter() - .map(|s| AnyValue::Utf8(s)) + .map(AnyValue::Utf8) .collect() ) ); @@ -69,7 +69,7 @@ mod tests { Row::new( ["Alexandre", "Grothendieck", "alexandre@grothendieck.com"] .into_iter() - .map(|s| AnyValue::Utf8(s)) + .map(AnyValue::Utf8) .collect() ) ); @@ -107,7 +107,7 @@ mod tests { Row::new( ["Marie", "Curie", "marie@curie.com"] .into_iter() - .map(|s| AnyValue::Utf8(s)) + .map(AnyValue::Utf8) .collect() ) ); @@ -116,7 +116,7 @@ mod tests { Row::new( ["Alexandre", "Grothendieck", "alexandre@grothendieck.com"] .into_iter() - .map(|s| AnyValue::Utf8(s)) + .map(AnyValue::Utf8) .collect() ) ); From f0dd7bc7e1fc333f82df3200566b59a8d0997dee Mon Sep 17 00:00:00 2001 From: quambene Date: Sat, 30 Mar 2024 21:32:44 +0100 Subject: [PATCH 46/54] Test archive --- src/cmd/send.rs | 5 +- src/cmd/send_bulk.rs | 15 ++-- src/email_formatter/eml.rs | 99 +++++++++++++++++++++---- tests/cmd/test_query.rs | 9 +++ tests/cmd/test_send.rs | 143 ++++++++++++++++++++++++------------ tests/cmd/test_send_bulk.rs | 90 ++++++----------------- 6 files changed, 221 insertions(+), 140 deletions(-) diff --git a/src/cmd/send.rs b/src/cmd/send.rs index f7ee443..c0009db 100644 --- a/src/cmd/send.rs +++ b/src/cmd/send.rs @@ -15,9 +15,10 @@ pub fn send(matches: &ArgMatches) -> Result<(), anyhow::Error> { println!("matches: {:#?}", matches); } + let now = SystemTime::now(); let dry_run = matches.is_present(arg::DRY_RUN); let is_archived = matches.is_present(arg::ARCHIVE); - let now = SystemTime::now(); + let archive_dir = Path::new(arg::value(arg::ARCHIVE_DIR, matches)?); let sender = Sender(arg::value(arg::SENDER, matches)?); let receiver = Receiver(arg::value(arg::RECEIVER, matches)?); let message = Message::from_args(matches)?; @@ -35,7 +36,7 @@ pub fn send(matches: &ArgMatches) -> Result<(), anyhow::Error> { let now = Utc::now(); let client = Client::from_args(matches)?; - let eml_formatter = EmlFormatter::from_args(matches)?; + let eml_formatter = EmlFormatter::new(archive_dir)?; println!("Sending email to 1 recipient ..."); diff --git a/src/cmd/send_bulk.rs b/src/cmd/send_bulk.rs index 57150b2..8ec71d1 100644 --- a/src/cmd/send_bulk.rs +++ b/src/cmd/send_bulk.rs @@ -17,6 +17,7 @@ pub fn send_bulk(matches: &ArgMatches) -> Result<(), anyhow::Error> { let dry_run = matches.is_present(arg::DRY_RUN); let is_archived = matches.is_present(arg::ARCHIVE); + let archive_dir = Path::new(arg::value(arg::ARCHIVE_DIR, matches)?); let sender = Sender(arg::value(arg::SENDER, matches)?); let receivers = BulkReceiver::from_args(matches)?; let message = Message::from_args(matches)?; @@ -39,7 +40,7 @@ pub fn send_bulk(matches: &ArgMatches) -> Result<(), anyhow::Error> { BulkEmail::new(sender, &receivers, &message, attachment, &[])? }; let client = Client::from_args(matches)?; - let eml_formatter = EmlFormatter::from_args(matches)?; + let eml_formatter = EmlFormatter::new(archive_dir)?; if matches.is_present(arg::DISPLAY) { println!("Display emails: {:#?}", bulk_email); @@ -73,6 +74,12 @@ pub fn send_bulk(matches: &ArgMatches) -> Result<(), anyhow::Error> { } } + if dry_run { + println!("All emails sent (dry run)."); + } else { + println!("All emails sent."); + } + Ok(()) } @@ -95,12 +102,6 @@ pub fn process_emails<'a>( } } - if dry_run { - println!("All emails sent (dry run)."); - } else { - println!("All emails sent."); - } - Ok(()) } diff --git a/src/email_formatter/eml.rs b/src/email_formatter/eml.rs index aec8c8b..5e2498c 100644 --- a/src/email_formatter/eml.rs +++ b/src/email_formatter/eml.rs @@ -1,32 +1,27 @@ -use crate::{arg, email_builder::Email}; -use anyhow::{anyhow, Context}; +use crate::email_builder::Email; +use anyhow::Context; use chrono::{DateTime, Utc}; -use clap::ArgMatches; use lettre::{FileTransport, Transport}; use std::{ fs, path::{Path, PathBuf}, }; -pub struct EmlFormatter<'a> { - target_dir: &'a Path, +/// Structure to store emails in EML format. +pub struct EmlFormatter { + target_dir: PathBuf, transport: FileTransport, } -impl<'a> EmlFormatter<'a> { - pub fn from_args(matches: &'a ArgMatches<'a>) -> Result { - let target_dir = match matches.value_of(arg::ARCHIVE_DIR) { - Some(archive_dir) => Path::new(archive_dir), - None => return Err(anyhow!("Missing value for argument '{}'", arg::ARCHIVE_DIR)), - }; - +impl EmlFormatter { + pub fn new(target_dir: &Path) -> Result { if !target_dir.exists() { fs::create_dir(target_dir).context("Unable to create directory for archived emails")?; } let transport = FileTransport::new(target_dir); let formatter = Self { - target_dir, + target_dir: target_dir.to_owned(), transport, }; @@ -44,8 +39,8 @@ impl<'a> EmlFormatter<'a> { .send(&email.mime_format.message) .context("Can't save email in .eml format")?; - let old_path = old_path(message_id.as_str(), self.target_dir); - let new_path = new_path(message_id.as_str(), self.target_dir, dry_run, now); + let old_path = old_path(message_id.as_str(), &self.target_dir); + let new_path = new_path(message_id.as_str(), &self.target_dir, dry_run, now); println!("Archiving '{}' ...", new_path.display()); @@ -72,3 +67,77 @@ fn new_path(message_id: &str, target_dir: &Path, dry_run: bool, now: DateTime(now: SystemTime) -> Email<'a> { + let sender = Sender("albert@einstein.com"); + let receiver = Receiver("marie@curie.com"); + let subject = "Test subject"; + let text = "This is a test message (plaintext)."; + let html = "

This is a test message (html).

"; + let message = Message::new(subject, Some(text), Some(html)); + let mime_format = MimeFormat::new(sender, receiver, &message, None, now).unwrap(); + Email::new(sender, receiver, &message, &mime_format).unwrap() + } + + #[test] + fn test_archive() { + let temp_dir = tempdir().unwrap(); + let temp_path = temp_dir.path(); + assert!(temp_path.exists(), "Missing path: {}", temp_path.display()); + + let now = Utc::now(); + let now_system_time = SystemTime::UNIX_EPOCH + Duration::from_secs(now.timestamp() as u64); + let dry_run = false; + let email = create_email(now_system_time); + + let email_formatter = EmlFormatter::new(temp_path).unwrap(); + let res = email_formatter.archive(&email, now, dry_run); + assert!(res.is_ok(), "{}", res.unwrap_err()); + + if let Ok(entries) = fs::read_dir(temp_path) { + let files = entries.flatten().collect::>(); + assert_eq!(files.len(), 1); + + let path = &files[0].path(); + assert_eq!(path.extension().unwrap().to_str().unwrap(), "eml"); + + let eml = fs::read_to_string(path).unwrap(); + assert_eq!(eml, format!("{:?}", email.mime_format)); + } + } + + #[test] + fn test_archive_dry() { + let temp_dir = tempdir().unwrap(); + let temp_path = temp_dir.path(); + assert!(temp_path.exists(), "Missing path: {}", temp_path.display()); + + let now = Utc::now(); + let now_system_time = SystemTime::UNIX_EPOCH + Duration::from_secs(now.timestamp() as u64); + let dry_run = true; + let email = create_email(now_system_time); + + let email_formatter = EmlFormatter::new(temp_path).unwrap(); + let res = email_formatter.archive(&email, now, dry_run); + assert!(res.is_ok(), "{}", res.unwrap_err()); + + if let Ok(entries) = fs::read_dir(temp_path) { + let files = entries.flatten().collect::>(); + assert_eq!(files.len(), 1); + + let path = &files[0].path(); + assert_eq!(path.extension().unwrap().to_str().unwrap(), "eml"); + assert!(path.to_str().unwrap().contains("dry-run")); + + let eml = fs::read_to_string(path).unwrap(); + assert_eq!(eml, format!("{:?}", email.mime_format)); + } + } +} diff --git a/tests/cmd/test_query.rs b/tests/cmd/test_query.rs index 5b44eab..1f3a345 100644 --- a/tests/cmd/test_query.rs +++ b/tests/cmd/test_query.rs @@ -1,3 +1,12 @@ +/* These tests requires the following environment variables: + - DB_HOST + - DB_PORT + - DB_USER + - DB_PASSWORD + - DB_NAME + - TEST_QUERY +*/ + use assert_cmd::Command; use predicates::str; use std::{env, fs}; diff --git a/tests/cmd/test_send.rs b/tests/cmd/test_send.rs index 5cafdb5..93ea0e8 100644 --- a/tests/cmd/test_send.rs +++ b/tests/cmd/test_send.rs @@ -1,117 +1,166 @@ use assert_cmd::Command; -use predicates::str; +use predicates::{boolean::PredicateBooleanExt, str}; use std::env; +use tempfile::tempdir; +/// This test requires the following environment variables: +/// - SMTP_SERVER +/// - SMTP_USERNAME +/// - SMTP_PASSWORD +/// - TEST_SENDER +/// - TEST_RECEIVER #[test] #[ignore] fn test_send_smtp() { + let temp_dir = tempdir().unwrap(); + let temp_path = temp_dir.path(); + assert!(temp_path.exists(), "Missing path: {}", temp_path.display()); + + let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); + let receiver = env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); + println!("Execute 'pigeon send --connection smtp'"); let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.current_dir(temp_path); cmd.args([ "send", - "--subject", - "Test Subject", - "--content", - "This is a test message (plaintext).", + &sender, + &receiver, + "--message-file", + "./test_data/message.yaml", + "--attachment", + "./test_data/test.pdf", + "--archive", + "--archive-dir", + "./my-sent-emails", "--display", "--assume-yes", - "--archive", "--connection", "smtp", ]); - cmd.assert().success().stdout(str::contains("abc")); + cmd.assert().success().stdout( + str::contains("Reading csv file './test_data/receiver.csv' ...") + .and(str::contains("Display csv file:").and(str::contains("Display emails:"))), + ); + + assert!(temp_path.join("my-sent-emails").exists()); } +/// This test requires the following environment variables: +/// - AWS_ACCESS_KEY_ID +/// - AWS_SECRET_ACCESS_KEY +/// - AWS_REGION +/// - TEST_SENDER +/// - TEST_RECEIVER #[test] #[ignore] fn test_send_aws() { + let temp_dir = tempdir().unwrap(); + let temp_path = temp_dir.path(); + assert!(temp_path.exists(), "Missing path: {}", temp_path.display()); + let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); let receiver = env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); println!("Execute 'pigeon send --connection aws ...'"); let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.current_dir(temp_path); cmd.args([ "send", &sender, &receiver, - "--subject", - "Test Subject", - "--content", - "This is a test message (plaintext).", + "--message-file", + "./test_data/message.yaml", + "--attachment", + "./test_data/test.pdf", + "--archive", + "--archive-dir", + "./my-sent-emails", "--display", "--assume-yes", "--archive", "--connection", "aws", ]); - cmd.assert().success().stdout(str::contains("abc")); -} + cmd.assert().success().stdout( + str::contains("Reading csv file './test_data/receiver.csv' ...") + .and(str::contains("Display csv file:").and(str::contains("Display emails:"))), + ); -#[test] -#[ignore] -fn test_archive_dir_smtp_dry() { - println!("Execute 'pigeon send'"); - let mut cmd = Command::cargo_bin("pigeon").unwrap(); - cmd.args([ - "send", - "albert@einstein.com", - "marie@curie.com", - "--message-file", - "./test_data/message.yaml", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - "--archive-dir", - "./my-sent-emails", - ]); - cmd.assert().success().stdout(str::contains("abc")); + assert!(temp_path.join("my-sent-emails").exists()); } #[test] -#[ignore] -fn test_attachment_pdf_smtp() { +fn test_send_smtp_dry() { + let temp_dir = tempdir().unwrap(); + let temp_path = temp_dir.path(); + assert!(temp_path.exists(), "Missing path: {}", temp_path.display()); + let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); let receiver = env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); - println!("Execute 'pigeon send'"); + println!("Execute 'pigeon send --connection smtp'"); let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.current_dir(temp_path); cmd.args([ "send", &sender, &receiver, "--message-file", "./test_data/message.yaml", - "--display", - "--assume-yes", - "--archive", "--attachment", "./test_data/test.pdf", + "--archive", + "--archive-dir", + "./my-sent-emails", + "--display", + "--assume-yes", + "--connection", + "smtp", + "--dry-run", ]); - cmd.assert().success().stdout(str::contains("abc")); + cmd.assert().success().stdout( + str::contains("Reading csv file './test_data/receiver.csv' ...") + .and(str::contains("Display csv file:").and(str::contains("Display emails:"))), + ); + + assert!(temp_path.join("my-sent-emails").exists()); } #[test] -#[ignore] -fn test_attachment_pdf_aws() { +fn test_send_aws_dry() { + let temp_dir = tempdir().unwrap(); + let temp_path = temp_dir.path(); + assert!(temp_path.exists(), "Missing path: {}", temp_path.display()); + let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); let receiver = env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); - println!("Execute 'pigeon send'"); + println!("Execute 'pigeon send --connection aws ...'"); let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.current_dir(temp_path); cmd.args([ "send", &sender, &receiver, - "--connection", - "aws", "--message-file", "./test_data/message.yaml", + "--attachment", + "./test_data/test.pdf", + "--archive", + "--archive-dir", + "./my-sent-emails", "--display", "--assume-yes", "--archive", - "--attachment", - "./test_data/test.pdf", + "--connection", + "aws", + "dry-run", ]); - cmd.assert().success().stdout(str::contains("abc")); + cmd.assert().success().stdout( + str::contains("Reading csv file './test_data/receiver.csv' ...") + .and(str::contains("Display csv file:").and(str::contains("Display emails:"))), + ); + + assert!(temp_path.join("my-sent-emails").exists()); } diff --git a/tests/cmd/test_send_bulk.rs b/tests/cmd/test_send_bulk.rs index e1d5a17..4b4d008 100644 --- a/tests/cmd/test_send_bulk.rs +++ b/tests/cmd/test_send_bulk.rs @@ -1,37 +1,16 @@ use assert_cmd::Command; use predicates::{boolean::PredicateBooleanExt, str}; +use tempfile::tempdir; #[test] -fn test_send_bulk_subject_content_dry() { - println!("Execute 'pigeon send-bulk'"); - let mut cmd = Command::cargo_bin("pigeon").unwrap(); - cmd.args([ - "send-bulk", - "albert@einstein.com", - "--receiver-file", - "./test_data/receiver.csv", - "--subject", - "Test Subject", - "--content", - "This is a test message (plaintext).", - "--dry-run", - "--display", - "--assume-yes", - ]); - cmd.assert().success().stdout( - str::contains("Reading csv file './test_data/receiver.csv' ...").and( - str::contains("Display csv file:") - .and(str::contains("Display emails:")) - .and(str::contains("Dry run: \u{1b}[32mactivated\u{1b}[0m")), - ), - ); -} +fn test_send_bulk_smtp_dry() { + let temp_dir = tempdir().unwrap(); + let temp_path = temp_dir.path(); + assert!(temp_path.exists(), "Missing path: {}", temp_path.display()); -#[test] -#[ignore] -fn test_send_bulk_subject_content() { println!("Execute 'pigeon send-bulk'"); let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.current_dir(temp_path); cmd.args([ "send-bulk", "albert@einstein.com", @@ -41,20 +20,30 @@ fn test_send_bulk_subject_content() { "Test Subject", "--content", "This is a test message (plaintext).", + "--archive", + "--archive-dir", + "./my-sent-emails", "--display", "--assume-yes", + "--dry-run", ]); cmd.assert().success().stdout( str::contains("Reading csv file './test_data/receiver.csv' ...") .and(str::contains("Display csv file:").and(str::contains("Display emails:"))), ); + + assert!(temp_path.join("my-sent-emails").exists()); } #[test] -#[ignore] fn test_send_bulk_aws_dry() { + let temp_dir = tempdir().unwrap(); + let temp_path = temp_dir.path(); + assert!(temp_path.exists(), "Missing path: {}", temp_path.display()); + println!("Execute 'pigeon send-bulk'"); let mut cmd = Command::cargo_bin("pigeon").unwrap(); + cmd.current_dir(temp_path); cmd.args([ "send-bulk", "albert@einstein.com", @@ -62,53 +51,16 @@ fn test_send_bulk_aws_dry() { "./test_data/receiver.csv", "--message-file", "./test_data/message.yaml", - "--dry-run", + "--archive", + "--archive-dir", + "./my-sent-emails", "--display", "--assume-yes", "--connection", "aws", - ]); - cmd.assert().success(); -} - -#[test] -#[ignore] -fn test_archive_dry() { - println!("Execute 'pigeon send-bulk'"); - let mut cmd = Command::cargo_bin("pigeon").unwrap(); - cmd.args([ - "send-bulk", - "albert@einstein.com", - "--receiver-file", - "./test_data/receiver.csv", - "--message-file", - "./test_data/message.yaml", "--dry-run", - "--display", - "--assume-yes", - "--archive", ]); cmd.assert().success(); -} -#[test] -#[ignore] -fn test_archive_dir_dry() { - println!("Execute 'pigeon send-bulk'"); - let mut cmd = Command::cargo_bin("pigeon").unwrap(); - cmd.args([ - "send-bulk", - "albert@einstein.com", - "--receiver-file", - "./test_data/receiver.csv", - "--message-file", - "./test_data/message.yaml", - "--dry-run", - "--display", - "--assume-yes", - "--archive", - "--archive-dir", - "./my-sent-emails", - ]); - cmd.assert().success(); + assert!(temp_path.join("my-sent-emails").exists()); } From 5bb819a029fe9802648552e393b3215716080e65 Mon Sep 17 00:00:00 2001 From: quambene Date: Sat, 30 Mar 2024 22:00:31 +0100 Subject: [PATCH 47/54] Test client --- src/email_transmission/client.rs | 105 +++++++++++++++++++------- src/email_transmission/mock_client.rs | 1 + 2 files changed, 80 insertions(+), 26 deletions(-) diff --git a/src/email_transmission/client.rs b/src/email_transmission/client.rs index d5d7215..7ae9cfe 100644 --- a/src/email_transmission/client.rs +++ b/src/email_transmission/client.rs @@ -1,3 +1,5 @@ +use std::fmt; + use super::{MockClient, SendEmail, SentEmail, SmtpClient}; use crate::{ arg::{self, val}, @@ -7,42 +9,93 @@ use crate::{ use anyhow::anyhow; use clap::ArgMatches; -pub struct Client<'a>(pub Box>); +#[derive(Debug, PartialEq)] +pub enum TransmissionType { + Smtp, + Aws, + Dry, +} + +impl fmt::Display for TransmissionType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let request_method = match self { + Self::Smtp => "smtp", + Self::Aws => "aws", + Self::Dry => "dry", + }; + + write!(f, "{}", request_method) + } +} + +pub struct Client<'a> { + #[allow(dead_code)] + transmission_type: TransmissionType, + client: Box>, +} impl<'a> Client<'a> { + pub fn new(transmission_type: TransmissionType, client: Box>) -> Self { + Self { + transmission_type, + client, + } + } + pub fn from_args(matches: &ArgMatches) -> Result { if matches.is_present(arg::DRY_RUN) { - return Ok(Client(Box::new(MockClient))); + let client = MockClient; + return Ok(Client::new(TransmissionType::Dry, Box::new(client))); } - if matches.is_present(arg::CONNECTION) { - match matches.value_of(arg::CONNECTION) { - Some(connection) => match connection.to_lowercase().as_str() { - val::SMTP => { - let client = SmtpClient::new()?; - Ok(Client(Box::new(client))) - } - val::AWS => { - let client = AwsSesClient::new(matches)?; - Ok(Client(Box::new(client))) - } - other => Err(anyhow!(format!( - "Value '{}' for argument '{}' not supported", - other, - arg::CONNECTION - ))), - }, - None => Err(anyhow!(format!( - "Missing value for argument '{}'", - arg::CONNECTION - ))), + let connection = arg::value(arg::CONNECTION, matches)?; + + match connection.to_lowercase().as_str() { + val::SMTP => { + let client = SmtpClient::new()?; + Ok(Client::new(TransmissionType::Smtp, Box::new(client))) + } + val::AWS => { + let client = AwsSesClient::new(matches)?; + Ok(Client::new(TransmissionType::Aws, Box::new(client))) } - } else { - Err(anyhow!(format!("Missing argument '{}'", arg::CONNECTION))) + other => Err(anyhow!(format!( + "Value '{}' for argument '{}' not supported", + other, + arg::CONNECTION + ))), } } pub fn send(&self, email: &'a Email<'a>) -> Result, anyhow::Error> { - self.0.send(email) + self.client.send(email) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::app; + + #[test] + fn test_client_from_args_dry() { + let args = vec![ + "pigeon", + "send", + "albert@einstein.com", + "marie@curie.com", + "--message-file", + "./test_data/message.yaml", + "--dry-run", + ]; + let app = app(); + let matches = app.get_matches_from(args); + let subcommand_matches = matches.subcommand_matches("send").unwrap(); + + let res = Client::from_args(subcommand_matches); + assert!(res.is_ok()); + + let client = res.unwrap(); + assert_eq!(client.transmission_type, TransmissionType::Dry); } } diff --git a/src/email_transmission/mock_client.rs b/src/email_transmission/mock_client.rs index a9ff1b0..b02a6ab 100644 --- a/src/email_transmission/mock_client.rs +++ b/src/email_transmission/mock_client.rs @@ -1,6 +1,7 @@ use super::{SendEmail, SentEmail, Status}; use crate::email_builder::Email; +#[derive(PartialEq)] pub struct MockClient; impl<'a> SendEmail<'a> for MockClient { From 005581fbe0f689f3cd877dac983c82a2de77be5c Mon Sep 17 00:00:00 2001 From: quambene Date: Sat, 30 Mar 2024 22:34:06 +0100 Subject: [PATCH 48/54] Finish tests --- src/cmd/query.rs | 3 +- src/cmd/send.rs | 4 +- src/email_builder/bulk_email.rs | 1 - src/email_transmission/client.rs | 3 +- tests/cmd/test_send.rs | 82 ++++++++++++++++++++------------ tests/cmd/test_send_bulk.rs | 32 +++++++++---- 6 files changed, 78 insertions(+), 47 deletions(-) diff --git a/src/cmd/query.rs b/src/cmd/query.rs index 2920388..5d2ebaf 100644 --- a/src/cmd/query.rs +++ b/src/cmd/query.rs @@ -1,5 +1,3 @@ -use std::path::Path; - use crate::{ arg, cmd, sources::{self, ConnVars, DbConnection}, @@ -7,6 +5,7 @@ use crate::{ use anyhow::{anyhow, Result}; use chrono::Utc; use clap::ArgMatches; +use std::path::Path; pub fn query(matches: &ArgMatches) -> Result<(), anyhow::Error> { if matches.is_present(arg::VERBOSE) { diff --git a/src/cmd/send.rs b/src/cmd/send.rs index c0009db..f96ae27 100644 --- a/src/cmd/send.rs +++ b/src/cmd/send.rs @@ -63,9 +63,9 @@ pub fn send(matches: &ArgMatches) -> Result<(), anyhow::Error> { } if matches.is_present(arg::DRY_RUN) { - println!("Email sent (dry run)."); + println!("Email sent (dry run)"); } else { - println!("Email sent."); + println!("Email sent"); } Ok(()) diff --git a/src/email_builder/bulk_email.rs b/src/email_builder/bulk_email.rs index 2780a94..93c1c83 100644 --- a/src/email_builder/bulk_email.rs +++ b/src/email_builder/bulk_email.rs @@ -127,7 +127,6 @@ This is a test message (html)."#; .iter() .map(|email| email.message.html.as_ref().unwrap().as_str()) .collect::>(); - dbg!(&html_messages); assert!(html_messages .contains(&"Dear Marie Curie,\n
\n
\nThis is a test message (html).")); assert!(html_messages diff --git a/src/email_transmission/client.rs b/src/email_transmission/client.rs index 7ae9cfe..547fe3d 100644 --- a/src/email_transmission/client.rs +++ b/src/email_transmission/client.rs @@ -1,5 +1,3 @@ -use std::fmt; - use super::{MockClient, SendEmail, SentEmail, SmtpClient}; use crate::{ arg::{self, val}, @@ -8,6 +6,7 @@ use crate::{ }; use anyhow::anyhow; use clap::ArgMatches; +use std::fmt; #[derive(Debug, PartialEq)] pub enum TransmissionType { diff --git a/tests/cmd/test_send.rs b/tests/cmd/test_send.rs index 93ea0e8..3ec4f25 100644 --- a/tests/cmd/test_send.rs +++ b/tests/cmd/test_send.rs @@ -1,6 +1,6 @@ use assert_cmd::Command; use predicates::{boolean::PredicateBooleanExt, str}; -use std::env; +use std::{env, fs}; use tempfile::tempdir; /// This test requires the following environment variables: @@ -12,12 +12,15 @@ use tempfile::tempdir; #[test] #[ignore] fn test_send_smtp() { + let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); + let receiver = env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); + let temp_dir = tempdir().unwrap(); let temp_path = temp_dir.path(); assert!(temp_path.exists(), "Missing path: {}", temp_path.display()); - let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); - let receiver = env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); + fs::copy("./test_data/message.yaml", temp_path.join("message.yaml")).unwrap(); + fs::copy("./test_data/test.pdf", temp_path.join("test.pdf")).unwrap(); println!("Execute 'pigeon send --connection smtp'"); let mut cmd = Command::cargo_bin("pigeon").unwrap(); @@ -27,9 +30,9 @@ fn test_send_smtp() { &sender, &receiver, "--message-file", - "./test_data/message.yaml", + "./message.yaml", "--attachment", - "./test_data/test.pdf", + "./test.pdf", "--archive", "--archive-dir", "./my-sent-emails", @@ -39,7 +42,7 @@ fn test_send_smtp() { "smtp", ]); cmd.assert().success().stdout( - str::contains("Reading csv file './test_data/receiver.csv' ...") + str::contains("Reading csv file './receiver.csv' ...") .and(str::contains("Display csv file:").and(str::contains("Display emails:"))), ); @@ -55,12 +58,15 @@ fn test_send_smtp() { #[test] #[ignore] fn test_send_aws() { + let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); + let receiver = env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); + let temp_dir = tempdir().unwrap(); let temp_path = temp_dir.path(); assert!(temp_path.exists(), "Missing path: {}", temp_path.display()); - let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); - let receiver = env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); + fs::copy("./test_data/message.yaml", temp_path.join("message.yaml")).unwrap(); + fs::copy("./test_data/test.pdf", temp_path.join("test.pdf")).unwrap(); println!("Execute 'pigeon send --connection aws ...'"); let mut cmd = Command::cargo_bin("pigeon").unwrap(); @@ -70,21 +76,20 @@ fn test_send_aws() { &sender, &receiver, "--message-file", - "./test_data/message.yaml", + "./message.yaml", "--attachment", - "./test_data/test.pdf", + "./test.pdf", "--archive", "--archive-dir", "./my-sent-emails", "--display", "--assume-yes", - "--archive", "--connection", "aws", ]); cmd.assert().success().stdout( - str::contains("Reading csv file './test_data/receiver.csv' ...") - .and(str::contains("Display csv file:").and(str::contains("Display emails:"))), + str::contains("Reading csv file './receiver.csv' ...") + .and(str::contains("Display csv file:").and(str::contains("Display email:"))), ); assert!(temp_path.join("my-sent-emails").exists()); @@ -96,20 +101,20 @@ fn test_send_smtp_dry() { let temp_path = temp_dir.path(); assert!(temp_path.exists(), "Missing path: {}", temp_path.display()); - let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); - let receiver = env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); + fs::copy("./test_data/message.yaml", temp_path.join("message.yaml")).unwrap(); + fs::copy("./test_data/test.pdf", temp_path.join("test.pdf")).unwrap(); println!("Execute 'pigeon send --connection smtp'"); let mut cmd = Command::cargo_bin("pigeon").unwrap(); cmd.current_dir(temp_path); cmd.args([ "send", - &sender, - &receiver, + "albert@einstein.com", + "marie@curie.com", "--message-file", - "./test_data/message.yaml", + "./message.yaml", "--attachment", - "./test_data/test.pdf", + "./test.pdf", "--archive", "--archive-dir", "./my-sent-emails", @@ -120,8 +125,16 @@ fn test_send_smtp_dry() { "--dry-run", ]); cmd.assert().success().stdout( - str::contains("Reading csv file './test_data/receiver.csv' ...") - .and(str::contains("Display csv file:").and(str::contains("Display emails:"))), + str::contains("Reading message file './message.yaml' ...") + .and(str::contains("Display message file:")) + .and(str::contains("Display email:")) + .and(str::contains("Dry run: \u{1b}[32mactivated\u{1b}[0m")) + .and(str::contains("Sending email to 1 recipient ...")) + .and(str::contains( + "marie@curie.com ... \u{1b}[32mdry run\u{1b}[0m", + )) + .and(str::contains("Archiving './my-sent-emails")) + .and(str::contains("Email sent (dry run)")), ); assert!(temp_path.join("my-sent-emails").exists()); @@ -133,33 +146,40 @@ fn test_send_aws_dry() { let temp_path = temp_dir.path(); assert!(temp_path.exists(), "Missing path: {}", temp_path.display()); - let sender = env::var("TEST_SENDER").expect("Missing environment variable 'TEST_SENDER'"); - let receiver = env::var("TEST_RECEIVER").expect("Missing environment variable 'TEST_RECEIVER'"); + fs::copy("./test_data/message.yaml", temp_path.join("message.yaml")).unwrap(); + fs::copy("./test_data/test.pdf", temp_path.join("test.pdf")).unwrap(); println!("Execute 'pigeon send --connection aws ...'"); let mut cmd = Command::cargo_bin("pigeon").unwrap(); cmd.current_dir(temp_path); cmd.args([ "send", - &sender, - &receiver, + "albert@einstein.com", + "marie@curie.com", "--message-file", - "./test_data/message.yaml", + "./message.yaml", "--attachment", - "./test_data/test.pdf", + "./test.pdf", "--archive", "--archive-dir", "./my-sent-emails", "--display", "--assume-yes", - "--archive", "--connection", "aws", - "dry-run", + "--dry-run", ]); cmd.assert().success().stdout( - str::contains("Reading csv file './test_data/receiver.csv' ...") - .and(str::contains("Display csv file:").and(str::contains("Display emails:"))), + str::contains("Reading message file './message.yaml' ...") + .and(str::contains("Display message file:")) + .and(str::contains("Display email:")) + .and(str::contains("Dry run: \u{1b}[32mactivated\u{1b}[0m")) + .and(str::contains("Sending email to 1 recipient ...")) + .and(str::contains( + "marie@curie.com ... \u{1b}[32mdry run\u{1b}[0m", + )) + .and(str::contains("Archiving './my-sent-emails")) + .and(str::contains("Email sent (dry run)")), ); assert!(temp_path.join("my-sent-emails").exists()); diff --git a/tests/cmd/test_send_bulk.rs b/tests/cmd/test_send_bulk.rs index 4b4d008..ffe07c3 100644 --- a/tests/cmd/test_send_bulk.rs +++ b/tests/cmd/test_send_bulk.rs @@ -1,5 +1,6 @@ use assert_cmd::Command; use predicates::{boolean::PredicateBooleanExt, str}; +use std::fs; use tempfile::tempdir; #[test] @@ -8,6 +9,10 @@ fn test_send_bulk_smtp_dry() { let temp_path = temp_dir.path(); assert!(temp_path.exists(), "Missing path: {}", temp_path.display()); + fs::copy("./test_data/receiver.csv", temp_path.join("receiver.csv")).unwrap(); + fs::copy("./test_data/message.yaml", temp_path.join("message.yaml")).unwrap(); + fs::copy("./test_data/test.pdf", temp_path.join("test.pdf")).unwrap(); + println!("Execute 'pigeon send-bulk'"); let mut cmd = Command::cargo_bin("pigeon").unwrap(); cmd.current_dir(temp_path); @@ -15,11 +20,11 @@ fn test_send_bulk_smtp_dry() { "send-bulk", "albert@einstein.com", "--receiver-file", - "./test_data/receiver.csv", - "--subject", - "Test Subject", - "--content", - "This is a test message (plaintext).", + "./receiver.csv", + "--message-file", + "./message.yaml", + "--attachment", + "./test.pdf", "--archive", "--archive-dir", "./my-sent-emails", @@ -28,7 +33,7 @@ fn test_send_bulk_smtp_dry() { "--dry-run", ]); cmd.assert().success().stdout( - str::contains("Reading csv file './test_data/receiver.csv' ...") + str::contains("Reading csv file './receiver.csv' ...") .and(str::contains("Display csv file:").and(str::contains("Display emails:"))), ); @@ -41,6 +46,10 @@ fn test_send_bulk_aws_dry() { let temp_path = temp_dir.path(); assert!(temp_path.exists(), "Missing path: {}", temp_path.display()); + fs::copy("./test_data/receiver.csv", temp_path.join("receiver.csv")).unwrap(); + fs::copy("./test_data/message.yaml", temp_path.join("message.yaml")).unwrap(); + fs::copy("./test_data/test.pdf", temp_path.join("test.pdf")).unwrap(); + println!("Execute 'pigeon send-bulk'"); let mut cmd = Command::cargo_bin("pigeon").unwrap(); cmd.current_dir(temp_path); @@ -48,9 +57,11 @@ fn test_send_bulk_aws_dry() { "send-bulk", "albert@einstein.com", "--receiver-file", - "./test_data/receiver.csv", + "./receiver.csv", "--message-file", - "./test_data/message.yaml", + "./message.yaml", + "--attachment", + "./test.pdf", "--archive", "--archive-dir", "./my-sent-emails", @@ -60,7 +71,10 @@ fn test_send_bulk_aws_dry() { "aws", "--dry-run", ]); - cmd.assert().success(); + cmd.assert().success().stdout( + str::contains("Reading csv file './receiver.csv' ...") + .and(str::contains("Display csv file:").and(str::contains("Display emails:"))), + ); assert!(temp_path.join("my-sent-emails").exists()); } From d89ac68ce9f5b2e40c5b227359c51e7ba3d1e923 Mon Sep 17 00:00:00 2001 From: quambene Date: Sat, 30 Mar 2024 22:38:34 +0100 Subject: [PATCH 49/54] Finish tests II --- src/cmd/send.rs | 2 +- src/cmd/send_bulk.rs | 4 ++-- tests/cmd/test_send.rs | 4 ++-- tests/cmd/test_send_bulk.rs | 24 ++++++++++++++++++++++-- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/cmd/send.rs b/src/cmd/send.rs index f96ae27..ac4cdef 100644 --- a/src/cmd/send.rs +++ b/src/cmd/send.rs @@ -38,7 +38,7 @@ pub fn send(matches: &ArgMatches) -> Result<(), anyhow::Error> { let client = Client::from_args(matches)?; let eml_formatter = EmlFormatter::new(archive_dir)?; - println!("Sending email to 1 recipient ..."); + println!("Sending email to 1 receiver ..."); if matches.is_present(arg::ASSUME_YES) { let sent_email = client.send(&email)?; diff --git a/src/cmd/send_bulk.rs b/src/cmd/send_bulk.rs index 8ec71d1..8146951 100644 --- a/src/cmd/send_bulk.rs +++ b/src/cmd/send_bulk.rs @@ -75,9 +75,9 @@ pub fn send_bulk(matches: &ArgMatches) -> Result<(), anyhow::Error> { } if dry_run { - println!("All emails sent (dry run)."); + println!("All emails sent (dry run)"); } else { - println!("All emails sent."); + println!("All emails sent"); } Ok(()) diff --git a/tests/cmd/test_send.rs b/tests/cmd/test_send.rs index 3ec4f25..c55e219 100644 --- a/tests/cmd/test_send.rs +++ b/tests/cmd/test_send.rs @@ -129,7 +129,7 @@ fn test_send_smtp_dry() { .and(str::contains("Display message file:")) .and(str::contains("Display email:")) .and(str::contains("Dry run: \u{1b}[32mactivated\u{1b}[0m")) - .and(str::contains("Sending email to 1 recipient ...")) + .and(str::contains("Sending email to 1 receiver ...")) .and(str::contains( "marie@curie.com ... \u{1b}[32mdry run\u{1b}[0m", )) @@ -174,7 +174,7 @@ fn test_send_aws_dry() { .and(str::contains("Display message file:")) .and(str::contains("Display email:")) .and(str::contains("Dry run: \u{1b}[32mactivated\u{1b}[0m")) - .and(str::contains("Sending email to 1 recipient ...")) + .and(str::contains("Sending email to 1 receiver ...")) .and(str::contains( "marie@curie.com ... \u{1b}[32mdry run\u{1b}[0m", )) diff --git a/tests/cmd/test_send_bulk.rs b/tests/cmd/test_send_bulk.rs index ffe07c3..0c5a7c8 100644 --- a/tests/cmd/test_send_bulk.rs +++ b/tests/cmd/test_send_bulk.rs @@ -34,7 +34,17 @@ fn test_send_bulk_smtp_dry() { ]); cmd.assert().success().stdout( str::contains("Reading csv file './receiver.csv' ...") - .and(str::contains("Display csv file:").and(str::contains("Display emails:"))), + .and(str::contains("Display csv file:")) + .and(str::contains("Reading message file './message.yaml' ...")) + .and(str::contains("Display message file:")) + .and(str::contains("Display emails:")) + .and(str::contains("Dry run: \u{1b}[32mactivated\u{1b}[0m")) + .and(str::contains("Sending email to 2 receivers ...")) + .and(str::contains( + "marie@curie.com ... \u{1b}[32mdry run\u{1b}[0m", + )) + .and(str::contains("Archiving './my-sent-emails")) + .and(str::contains("All emails sent (dry run)")), ); assert!(temp_path.join("my-sent-emails").exists()); @@ -73,7 +83,17 @@ fn test_send_bulk_aws_dry() { ]); cmd.assert().success().stdout( str::contains("Reading csv file './receiver.csv' ...") - .and(str::contains("Display csv file:").and(str::contains("Display emails:"))), + .and(str::contains("Display csv file:")) + .and(str::contains("Reading message file './message.yaml' ...")) + .and(str::contains("Display message file:")) + .and(str::contains("Display emails:")) + .and(str::contains("Dry run: \u{1b}[32mactivated\u{1b}[0m")) + .and(str::contains("Sending email to 2 receivers ...")) + .and(str::contains( + "marie@curie.com ... \u{1b}[32mdry run\u{1b}[0m", + )) + .and(str::contains("Archiving './my-sent-emails")) + .and(str::contains("All emails sent (dry run)")), ); assert!(temp_path.join("my-sent-emails").exists()); From b9d722a220456f64dbad84a9e1191306cafa822b Mon Sep 17 00:00:00 2001 From: quambene Date: Sat, 30 Mar 2024 23:43:45 +0100 Subject: [PATCH 50/54] Add coverage to readme --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bd1ed62..ac26f11 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,9 @@ # Pigeon [![Latest Version](https://img.shields.io/crates/v/bogrep.svg)](https://crates.io/crates/bogrep) -[![Build Status](https://github.com/quambene/bogrep/actions/workflows/rust-ci.yml/badge.svg)](https://github.com/quambene/bogrep/actions/workflows/rust-ci.yml) +[![Build +Status](https://github.com/quambene/bogrep/actions/workflows/rust-ci.yml/badge.svg)](https://github.com/quambene/bogrep/actions/workflows/rust-ci.yml) +[![codecov](https://codecov.io/gh/quambene/pigeon-rs/graph/badge.svg)](https://app.codecov.io/gh/quambene/pigeon-rs) Pigeon is a command line tool for automating your email workflow in a cheap and efficient way. Utilize your most efficient dev tools you are already familiar with. From 3e0e6f44a3d4626ddf44a5414f20e18ba787492e Mon Sep 17 00:00:00 2001 From: quambene Date: Sun, 31 Mar 2024 00:37:16 +0100 Subject: [PATCH 51/54] Refactor module --- src/email_builder/bulk_email.rs | 135 ------------------------------- src/email_builder/email.rs | 136 +++++++++++++++++++++++++++++++- src/email_builder/mod.rs | 4 +- 3 files changed, 136 insertions(+), 139 deletions(-) delete mode 100644 src/email_builder/bulk_email.rs diff --git a/src/email_builder/bulk_email.rs b/src/email_builder/bulk_email.rs deleted file mode 100644 index 93c1c83..0000000 --- a/src/email_builder/bulk_email.rs +++ /dev/null @@ -1,135 +0,0 @@ -use super::{BulkReceiver, Receiver, Sender}; -use crate::email_builder::{Email, Message, MimeFormat}; -use anyhow::Result; -use std::{path::Path, time::SystemTime}; - -#[derive(Debug)] -pub struct BulkEmail<'a> { - pub emails: Vec>, -} - -impl<'a> BulkEmail<'a> { - pub fn new( - sender: Sender<'a>, - bulk_receiver: &'a BulkReceiver, - message: &'a Message, - attachment: Option<&Path>, - personalized_columns: &[&str], - ) -> Result { - let now = SystemTime::now(); - let mut emails: Vec = vec![]; - - if personalized_columns.is_empty() { - let receivers = bulk_receiver.receiver_column()?; - for receiver in receivers.into_iter().flatten() { - let mime_format = - MimeFormat::new(sender, Receiver(receiver), message, attachment, now)?; - let email = Email::new(sender, Receiver(receiver), message, &mime_format)?; - emails.push(email); - } - } else { - for i in 0..bulk_receiver.height() { - let mut message = message.clone(); - - for &col_name in personalized_columns.iter() { - let col_value = bulk_receiver.row(i, col_name)?; - message.personalize(col_name, col_value); - } - - let receiver = bulk_receiver.receiver_row(i)?; - let mime_format = - MimeFormat::new(sender, Receiver(receiver), &message, attachment, now)?; - let email = Email::new(sender, Receiver(receiver), &message, &mime_format)?; - - emails.push(email); - } - } - - Ok(BulkEmail { emails }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use polars::{frame::DataFrame, prelude::NamedFrom, series::Series}; - - #[test] - fn test_bulk_email() { - let sender = Sender("albert@einstein.com"); - let subject = "Test Subject"; - let text = "This is a test message (plaintext)."; - let html = "

This is a test message (html).

"; - let message = Message::new(subject, Some(text), Some(html)); - let column_name = "email"; - let receiver_column = Series::new(column_name, &["marie@curie.com", "emmy@noether.com"]); - let df_receiver = DataFrame::new(vec![receiver_column]).unwrap(); - let receivers = BulkReceiver::new(column_name.to_owned(), df_receiver); - - let res = BulkEmail::new(sender, &receivers, &message, None, &[]); - assert!(res.is_ok()); - - let emails = res.unwrap().emails; - assert_eq!(emails.len(), 2); - assert!(emails.iter().any(|email| email.sender == sender)); - - let receivers = emails - .iter() - .map(|email| email.receiver) - .collect::>(); - assert!(receivers.contains(&Receiver("marie@curie.com"))); - assert!(receivers.contains(&Receiver("emmy@noether.com"))); - } - - #[test] - fn test_bulk_email_personalized() { - let sender = Sender("albert@einstein.com"); - let subject = "Test Subject"; - let text = r#"Dear {first_name} {last_name}, -This is a test message (plaintext)."#; - let html = r#"Dear {first_name} {last_name}, -
-
-This is a test message (html)."#; - let message = Message::new(subject, Some(text), Some(html)); - let first_name_column = Series::new("first_name", &["Marie", "Emmy"]); - let last_name_column = Series::new("last_name", &["Curie", "Noether"]); - let email_column = Series::new("email", &["marie@curie.com", "emmy@noether.com"]); - let df_receiver = - DataFrame::new(vec![first_name_column, last_name_column, email_column]).unwrap(); - let receivers = BulkReceiver::new("email".to_owned(), df_receiver); - - let res = BulkEmail::new( - sender, - &receivers, - &message, - None, - &["first_name", "last_name"], - ); - assert!(res.is_ok()); - - let emails = res.unwrap().emails; - let receivers = emails - .iter() - .map(|email| email.receiver) - .collect::>(); - assert!(receivers.contains(&Receiver("marie@curie.com"))); - assert!(receivers.contains(&Receiver("emmy@noether.com"))); - - let text_messages = emails - .iter() - .map(|email| email.message.text.as_ref().unwrap().as_str()) - .collect::>(); - assert!(text_messages.contains(&"Dear Marie Curie,\nThis is a test message (plaintext).")); - assert!(text_messages.contains(&"Dear Emmy Noether,\nThis is a test message (plaintext).")); - - let html_messages = emails - .iter() - .map(|email| email.message.html.as_ref().unwrap().as_str()) - .collect::>(); - assert!(html_messages - .contains(&"Dear Marie Curie,\n
\n
\nThis is a test message (html).")); - assert!(html_messages - .contains(&"Dear Emmy Noether,\n
\n
\nThis is a test message (html).")); - } -} diff --git a/src/email_builder/email.rs b/src/email_builder/email.rs index 83b6166..2172a4e 100644 --- a/src/email_builder/email.rs +++ b/src/email_builder/email.rs @@ -1,5 +1,8 @@ +use super::{BulkReceiver, Receiver, Sender}; use super::{Receiver, Sender}; -use crate::email_builder::{Message, MimeFormat}; +use crate::email_builder::{Email, Message, Message, MimeFormat, MimeFormat}; +use anyhow::Result; +use std::{path::Path, time::SystemTime}; #[derive(Debug)] pub struct Email<'a> { @@ -25,3 +28,134 @@ impl<'a> Email<'a> { Ok(email) } } + +#[derive(Debug)] +pub struct BulkEmail<'a> { + pub emails: Vec>, +} + +impl<'a> BulkEmail<'a> { + pub fn new( + sender: Sender<'a>, + bulk_receiver: &'a BulkReceiver, + message: &'a Message, + attachment: Option<&Path>, + personalized_columns: &[&str], + ) -> Result { + let now = SystemTime::now(); + let mut emails: Vec = vec![]; + + if personalized_columns.is_empty() { + let receivers = bulk_receiver.receiver_column()?; + for receiver in receivers.into_iter().flatten() { + let mime_format = + MimeFormat::new(sender, Receiver(receiver), message, attachment, now)?; + let email = Email::new(sender, Receiver(receiver), message, &mime_format)?; + emails.push(email); + } + } else { + for i in 0..bulk_receiver.height() { + let mut message = message.clone(); + + for &col_name in personalized_columns.iter() { + let col_value = bulk_receiver.row(i, col_name)?; + message.personalize(col_name, col_value); + } + + let receiver = bulk_receiver.receiver_row(i)?; + let mime_format = + MimeFormat::new(sender, Receiver(receiver), &message, attachment, now)?; + let email = Email::new(sender, Receiver(receiver), &message, &mime_format)?; + + emails.push(email); + } + } + + Ok(BulkEmail { emails }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use polars::{frame::DataFrame, prelude::NamedFrom, series::Series}; + + #[test] + fn test_bulk_email() { + let sender = Sender("albert@einstein.com"); + let subject = "Test Subject"; + let text = "This is a test message (plaintext)."; + let html = "

This is a test message (html).

"; + let message = Message::new(subject, Some(text), Some(html)); + let column_name = "email"; + let receiver_column = Series::new(column_name, &["marie@curie.com", "emmy@noether.com"]); + let df_receiver = DataFrame::new(vec![receiver_column]).unwrap(); + let receivers = BulkReceiver::new(column_name.to_owned(), df_receiver); + + let res = BulkEmail::new(sender, &receivers, &message, None, &[]); + assert!(res.is_ok()); + + let emails = res.unwrap().emails; + assert_eq!(emails.len(), 2); + assert!(emails.iter().any(|email| email.sender == sender)); + + let receivers = emails + .iter() + .map(|email| email.receiver) + .collect::>(); + assert!(receivers.contains(&Receiver("marie@curie.com"))); + assert!(receivers.contains(&Receiver("emmy@noether.com"))); + } + + #[test] + fn test_bulk_email_personalized() { + let sender = Sender("albert@einstein.com"); + let subject = "Test Subject"; + let text = r#"Dear {first_name} {last_name}, +This is a test message (plaintext)."#; + let html = r#"Dear {first_name} {last_name}, +
+
+This is a test message (html)."#; + let message = Message::new(subject, Some(text), Some(html)); + let first_name_column = Series::new("first_name", &["Marie", "Emmy"]); + let last_name_column = Series::new("last_name", &["Curie", "Noether"]); + let email_column = Series::new("email", &["marie@curie.com", "emmy@noether.com"]); + let df_receiver = + DataFrame::new(vec![first_name_column, last_name_column, email_column]).unwrap(); + let receivers = BulkReceiver::new("email".to_owned(), df_receiver); + + let res = BulkEmail::new( + sender, + &receivers, + &message, + None, + &["first_name", "last_name"], + ); + assert!(res.is_ok()); + + let emails = res.unwrap().emails; + let receivers = emails + .iter() + .map(|email| email.receiver) + .collect::>(); + assert!(receivers.contains(&Receiver("marie@curie.com"))); + assert!(receivers.contains(&Receiver("emmy@noether.com"))); + + let text_messages = emails + .iter() + .map(|email| email.message.text.as_ref().unwrap().as_str()) + .collect::>(); + assert!(text_messages.contains(&"Dear Marie Curie,\nThis is a test message (plaintext).")); + assert!(text_messages.contains(&"Dear Emmy Noether,\nThis is a test message (plaintext).")); + + let html_messages = emails + .iter() + .map(|email| email.message.html.as_ref().unwrap().as_str()) + .collect::>(); + assert!(html_messages + .contains(&"Dear Marie Curie,\n
\n
\nThis is a test message (html).")); + assert!(html_messages + .contains(&"Dear Emmy Noether,\n
\n
\nThis is a test message (html).")); + } +} diff --git a/src/email_builder/mod.rs b/src/email_builder/mod.rs index e10841f..4be9f97 100644 --- a/src/email_builder/mod.rs +++ b/src/email_builder/mod.rs @@ -1,12 +1,10 @@ -mod bulk_email; mod email; mod message; mod mime; mod receiver; mod sender; -pub use bulk_email::BulkEmail; -pub use email::Email; +pub use email::{BulkEmail, Email}; pub use message::Message; pub use mime::MimeFormat; pub use receiver::{BulkReceiver, Receiver}; From fbe30ad55b7ce2d96fae198f29803576ef7ea3da Mon Sep 17 00:00:00 2001 From: quambene Date: Sun, 31 Mar 2024 00:41:11 +0100 Subject: [PATCH 52/54] Refactor module II --- src/email_builder/email.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/email_builder/email.rs b/src/email_builder/email.rs index 2172a4e..a761c76 100644 --- a/src/email_builder/email.rs +++ b/src/email_builder/email.rs @@ -1,6 +1,5 @@ use super::{BulkReceiver, Receiver, Sender}; -use super::{Receiver, Sender}; -use crate::email_builder::{Email, Message, Message, MimeFormat, MimeFormat}; +use crate::email_builder::{Message, MimeFormat}; use anyhow::Result; use std::{path::Path, time::SystemTime}; From b0519acc4b6720d26d342d8b95a06b00dfd60f16 Mon Sep 17 00:00:00 2001 From: quambene Date: Sun, 31 Mar 2024 01:53:02 +0100 Subject: [PATCH 53/54] Fix ci --- .github/workflows/rust-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 5cf6e7d..2d98362 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -63,7 +63,7 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-latest, windows-latest] + os: [macos-latest] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable From f68f67d29c87e2ede106ee79dd83ab0a58445afb Mon Sep 17 00:00:00 2001 From: quambene Date: Sun, 31 Mar 2024 01:57:36 +0100 Subject: [PATCH 54/54] Update dependencies --- Cargo.lock | 629 ++++++++++++++++++++++++++--------------------------- 1 file changed, 313 insertions(+), 316 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c6241f6..6c38f5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ "getrandom", "once_cell", @@ -30,9 +30,9 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.6" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", "getrandom", @@ -43,9 +43,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -88,15 +88,15 @@ checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" [[package]] name = "argminmax" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202108b46429b765ef483f8a24d5c46f48c14acfdacc086dd4ab6dddf6bcdbd2" +checksum = "52424b59d69d69d5056d508b260553afd91c57e21849579cd1f50ee8b8b88eaa" dependencies = [ "num-traits", ] @@ -129,7 +129,7 @@ version = "0.17.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59c468daea140b747d781a1da9f7db5f0a8e6636d4af20cc539e43d05b0604fa" dependencies = [ - "ahash 0.8.6", + "ahash 0.8.11", "arrow-format", "bytemuck", "chrono", @@ -169,13 +169,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.74" +version = "0.1.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.57", ] [[package]] @@ -200,15 +200,15 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", @@ -227,9 +227,15 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.5" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" [[package]] name = "bitflags" @@ -239,9 +245,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "bitvec" @@ -275,9 +281,9 @@ dependencies = [ [[package]] name = "borsh" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d4d6dafc1a3bb54687538972158f07b2c948bc57d5890df22c0739098b3028" +checksum = "0901fc8eb0aca4c83be0106d6f2db17d86a08dfc2c25f0e84464bf381158add6" dependencies = [ "borsh-derive", "cfg_aliases", @@ -285,15 +291,15 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4918709cc4dd777ad2b6303ed03cb37f3ca0ccede8c1b0d28ac6db8f4710e0" +checksum = "51670c3aa053938b0ee3bd67c3817e471e626151131b934038e83c5bf8de48f5" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.57", "syn_derive", ] @@ -310,15 +316,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.14.0" +version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" [[package]] name = "bytecheck" -version = "0.6.11" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6372023ac861f6e6dc89c8344a8f398fb42aaba2b5dbc649ca0c0e9dbcb627" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" dependencies = [ "bytecheck_derive", "ptr_meta", @@ -327,9 +333,9 @@ dependencies = [ [[package]] name = "bytecheck_derive" -version = "0.6.11" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7ec4c6f261935ad534c0c22dbef2201b45918860eb1c574b972bd213a76af61" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" dependencies = [ "proc-macro2", "quote", @@ -338,22 +344,22 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" +checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1" +checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.57", ] [[package]] @@ -364,15 +370,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cc" -version = "1.0.83" +version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" dependencies = [ "jobserver", "libc", @@ -402,9 +408,9 @@ checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" dependencies = [ "android-tzdata", "iana-time-zone", @@ -412,7 +418,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.48.5", + "windows-targets 0.52.4", ] [[package]] @@ -494,53 +500,46 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-deque" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fca89a0e215bab21874660c67903c5f143333cab1da83d041c7ded6053774751" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ - "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.16" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2fe95351b870527a5d09bf563ed3c97c0cffb87cf1c78a591bf48bb218d9aa" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "autocfg", - "cfg-if", "crossbeam-utils", - "memoffset", ] [[package]] name = "crossbeam-utils" -version = "0.8.17" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d96137f14f244c37f989d9fff8f95e6c18b918e71f36638f8c49112e4c78f" -dependencies = [ - "cfg-if", -] +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crossterm" @@ -548,7 +547,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "crossterm_winapi", "libc", "parking_lot", @@ -669,23 +668,23 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "dyn-clone" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "either" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" [[package]] name = "email-encoding" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbfb21b9878cf7a348dcb8559109aabc0ec40d69924bd706fa5149846c4fef75" +checksum = "a87260449b06739ee78d6281c68d2a0ff3e3af64a78df63d3a1aeb3c06997c8a" dependencies = [ - "base64 0.21.5", + "base64 0.22.0", "memchr", ] @@ -697,14 +696,14 @@ checksum = "e2153bd83ebc09db15bcbdc3e2194d901804952e3dc96967e1cd3b0c5c32d112" [[package]] name = "enum_dispatch" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f33313078bb8d4d05a2733a94ac4c2d8a0df9a2b84424ebf4f33bfc224a890e" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.57", ] [[package]] @@ -752,9 +751,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" [[package]] name = "fehler" @@ -835,9 +834,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -850,9 +849,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -860,15 +859,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -877,38 +876,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.57", ] [[package]] name = "futures-sink" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -934,9 +933,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "js-sys", @@ -959,9 +958,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.3.22" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" +checksum = "4fbd2820c5e49886948654ab546d0688ff24530286bdcf8fca3cefb16d4618eb" dependencies = [ "bytes", "fnv", @@ -969,7 +968,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 2.1.0", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", @@ -988,7 +987,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash 0.7.7", + "ahash 0.7.8", ] [[package]] @@ -997,7 +996,7 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" dependencies = [ - "ahash 0.8.6", + "ahash 0.8.11", "allocator-api2", "rayon", ] @@ -1019,9 +1018,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.3" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -1059,9 +1058,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", @@ -1108,7 +1107,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.5", + "socket2 0.5.6", "tokio", "tower-service", "tracing", @@ -1134,9 +1133,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.58" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1187,9 +1186,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.1.0" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown 0.14.3", @@ -1224,24 +1223,24 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.66" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -1258,7 +1257,7 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76bd09637ae3ec7bd605b8e135e757980b3968430ff2b1a4a94fb7769e50166d" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", "email-encoding", "email_address", "fastrand 1.9.0", @@ -1273,7 +1272,7 @@ dependencies = [ "rustls-pemfile", "socket2 0.4.10", "tokio", - "uuid 1.6.1", + "uuid 1.8.0", "webpki-roots", ] @@ -1368,7 +1367,7 @@ version = "0.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "libc", "redox_syscall", ] @@ -1381,9 +1380,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" @@ -1397,9 +1396,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "lz4" @@ -1444,9 +1443,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "memmap2" @@ -1457,15 +1456,6 @@ dependencies = [ "libc", ] -[[package]] -name = "memoffset" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" -dependencies = [ - "autocfg", -] - [[package]] name = "mime" version = "0.3.17" @@ -1480,18 +1470,18 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi", @@ -1500,9 +1490,9 @@ dependencies = [ [[package]] name = "multiversion" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2c7b9d7fe61760ce5ea19532ead98541f6b4c495d87247aff9826445cf6872a" +checksum = "c4851161a11d3ad0bf9402d90ffc3967bf231768bfd7aeb61755ad06dbf1a142" dependencies = [ "multiversion-macros", "target-features", @@ -1510,9 +1500,9 @@ dependencies = [ [[package]] name = "multiversion-macros" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26a83d8500ed06d68877e9de1dde76c1dbb83885dcdbda4ef44ccbc3fbda2ac8" +checksum = "79a74ddee9e0c27d2578323c13905793e91622148f138ba29738f9dddb835e90" dependencies = [ "proc-macro2", "quote", @@ -1574,9 +1564,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", "libm", @@ -1588,15 +1578,15 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.3.3", + "hermit-abi 0.3.9", "libc", ] [[package]] name = "object" -version = "0.32.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] @@ -1609,17 +1599,17 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.61" +version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b8419dc8cc6d866deb801274bba2e6f8f6108c1bb7fcc10ee5ab864931dbb45" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "cfg-if", "foreign-types", "libc", @@ -1636,7 +1626,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.57", ] [[package]] @@ -1647,9 +1637,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.97" +version = "0.9.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3eaad34cdd97d81de97964fc7f29e2d104f483840d906ef56daa1912338460b" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" dependencies = [ "cc", "libc", @@ -1744,9 +1734,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -1756,9 +1746,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "planus" @@ -1806,14 +1796,14 @@ version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b24f92fc5b167f668ff85ab9607dfa72e2c09664cacef59297ee8601dee60126" dependencies = [ - "ahash 0.8.6", + "ahash 0.8.11", "arrow2", - "bitflags 2.4.1", + "bitflags 2.5.0", "chrono", "comfy-table", "either", "hashbrown 0.14.3", - "indexmap 2.1.0", + "indexmap 2.2.6", "num-traits", "once_cell", "polars-arrow", @@ -1847,7 +1837,7 @@ version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92cab0df9f2a35702fa5aec99edfaabf9ae8e9cdd0acf69e143ad2d132f34f9c" dependencies = [ - "ahash 0.8.6", + "ahash 0.8.11", "arrow2", "async-trait", "bytes", @@ -1878,8 +1868,8 @@ version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c33762ec2a55e01c9f8776b34db86257c70a0a3b3929bd4eb91a52aacf61456" dependencies = [ - "ahash 0.8.6", - "bitflags 2.4.1", + "ahash 0.8.11", + "bitflags 2.5.0", "glob", "once_cell", "polars-arrow", @@ -1904,7 +1894,7 @@ dependencies = [ "argminmax", "arrow2", "either", - "indexmap 2.1.0", + "indexmap 2.2.6", "memchr", "polars-arrow", "polars-core", @@ -1940,7 +1930,7 @@ version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb67b014f0295e8e9dbb84404a91d666d477b3bc248a2ed51bc442833b16da35" dependencies = [ - "ahash 0.8.6", + "ahash 0.8.11", "arrow2", "once_cell", "polars-arrow", @@ -2007,7 +1997,7 @@ version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c326708a370d71dc6e11a8f4bbc10a8479e1c314dc048ba73543b815cd0bf339" dependencies = [ - "ahash 0.8.6", + "ahash 0.8.11", "hashbrown 0.14.3", "num-traits", "once_cell", @@ -2064,7 +2054,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49b6c5ef183cd3ab4ba005f1ca64c21e8bd97ce4699cfea9e8d9a2c4958ca520" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", "byteorder", "bytes", "fallible-iterator", @@ -2129,11 +2119,10 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "2.0.1" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97dc5fea232fc28d2f597b37c4876b348a40e33f3b02cc975c8d006d78d94b1a" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" dependencies = [ - "toml_datetime", "toml_edit", ] @@ -2162,9 +2151,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.70" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] @@ -2191,9 +2180,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -2273,9 +2262,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -2283,9 +2272,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -2313,25 +2302,25 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.2" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", "regex-automata", - "regex-syntax 0.8.2", + "regex-syntax 0.8.3", ] [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.2", + "regex-syntax 0.8.3", ] [[package]] @@ -2342,15 +2331,15 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "rend" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2571463863a6bd50c32f94402933f03457a3fbaf697a707c5be741e459f08fd" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" dependencies = [ "bytecheck", ] @@ -2372,23 +2361,24 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.7" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", + "cfg-if", "getrandom", "libc", "spin 0.9.8", "untrusted 0.9.0", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "rkyv" -version = "0.7.43" +version = "0.7.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527a97cdfef66f65998b5f3b637c26f5a5ec09cc52a3f9932313ac645f4190f5" +checksum = "5cba464629b3394fc4dbc6f940ff8f5b4ff5c7aef40f29166fd4ad12acbc99c0" dependencies = [ "bitvec", "bytecheck", @@ -2399,14 +2389,14 @@ dependencies = [ "rkyv_derive", "seahash", "tinyvec", - "uuid 1.6.1", + "uuid 1.8.0", ] [[package]] name = "rkyv_derive" -version = "0.7.43" +version = "0.7.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5c462a1328c8e67e4d6dbad1eb0355dd43e8ab432c6e227a43657f16ade5033" +checksum = "a7dddfff8de25e6f62b9d64e6e432bf1c6736c57d20323e15ee10435fbda7c65" dependencies = [ "proc-macro2", "quote", @@ -2498,15 +2488,15 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.33.1" +version = "1.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06676aec5ccb8fc1da723cc8c0f9a46549f21ebb8753d3915c6c41db1e7f1dc4" +checksum = "1790d1c4c0ca81211399e0e0af16333276f375209e71a37b67698a373db5b47a" dependencies = [ "arrayvec", "borsh", "bytes", "num-traits", - "postgres", + "postgres-types", "rand", "rkyv", "serde", @@ -2515,9 +2505,9 @@ dependencies = [ [[package]] name = "rust_decimal_macros" -version = "1.33.1" +version = "1.34.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e43721f4ef7060ebc2c3ede757733209564ca8207f47674181bcd425dd76945" +checksum = "e418701588729bef95e7a655f2b483ad64bb97c46e8e79fde83efd92aaab6d82" dependencies = [ "quote", "rust_decimal", @@ -2544,7 +2534,7 @@ version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "errno", "libc", "linux-raw-sys", @@ -2571,7 +2561,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ "log", - "ring 0.17.7", + "ring 0.17.8", "rustls-webpki 0.101.7", "sct 0.7.1", ] @@ -2594,7 +2584,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", ] [[package]] @@ -2613,7 +2603,7 @@ version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring 0.17.7", + "ring 0.17.8", "untrusted 0.9.0", ] @@ -2625,17 +2615,17 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "ryu" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] name = "schannel" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -2669,7 +2659,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring 0.17.7", + "ring 0.17.8", "untrusted 0.9.0", ] @@ -2681,9 +2671,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "security-framework" -version = "2.9.2" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -2694,9 +2684,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" dependencies = [ "core-foundation-sys", "libc", @@ -2704,35 +2694,35 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" [[package]] name = "serde" -version = "1.0.193" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.57", ] [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" dependencies = [ "itoa", "ryu", @@ -2789,9 +2779,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" @@ -2825,9 +2815,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.2" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "smartstring" @@ -2852,12 +2842,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -2941,7 +2931,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.41", + "syn 2.0.57", ] [[package]] @@ -2963,9 +2953,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.41" +version = "2.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" +checksum = "11a6ae1e52eb25aab8f3fb9fca13be982a373b8f1157ca14b897a825ba4a2d35" dependencies = [ "proc-macro2", "quote", @@ -2981,7 +2971,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.57", ] [[package]] @@ -3006,9 +2996,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "target-features" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfb5fa503293557c5158bd215fdc225695e567a77e453f5d4452a50a193969bd" +checksum = "c1bbb9f3c5c463a01705937a24fdabc5047929ac764b2d5b9cf681c1f5041ed5" [[package]] name = "tempfile" @@ -3017,7 +3007,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", - "fastrand 2.0.1", + "fastrand 2.0.2", "rustix", "windows-sys 0.52.0", ] @@ -3039,22 +3029,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.51" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.51" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.57", ] [[package]] @@ -3074,9 +3064,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.35.0" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d45b238a16291a4e1584e61820b8ae57d696cc5015c459c229ccc6990cc1c" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", "bytes", @@ -3085,7 +3075,7 @@ dependencies = [ "num_cpus", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.5", + "socket2 0.5.6", "tokio-macros", "windows-sys 0.48.0", ] @@ -3098,7 +3088,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.57", ] [[package]] @@ -3143,7 +3133,7 @@ dependencies = [ "postgres-protocol", "postgres-types", "rand", - "socket2 0.5.5", + "socket2 0.5.6", "tokio", "tokio-util", "whoami", @@ -3176,17 +3166,17 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" [[package]] name = "toml_edit" -version = "0.20.2" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap 2.1.0", + "indexmap 2.2.6", "toml_datetime", "winnow", ] @@ -3230,9 +3220,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-bidi" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" @@ -3242,9 +3232,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] @@ -3289,9 +3279,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.6.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ "getrandom", ] @@ -3338,11 +3328,17 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" -version = "0.2.89" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3350,24 +3346,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.89" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.57", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.89" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3375,28 +3371,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.89" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.57", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.89" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "web-sys" -version = "0.3.66" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", @@ -3423,11 +3419,12 @@ dependencies = [ [[package]] name = "whoami" -version = "1.4.1" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" +checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" dependencies = [ - "wasm-bindgen", + "redox_syscall", + "wasite", "web-sys", ] @@ -3455,11 +3452,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.51.1" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.48.5", + "windows-targets 0.52.4", ] [[package]] @@ -3477,7 +3474,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.0", + "windows-targets 0.52.4", ] [[package]] @@ -3497,17 +3494,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", ] [[package]] @@ -3518,9 +3515,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" [[package]] name = "windows_aarch64_msvc" @@ -3530,9 +3527,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" [[package]] name = "windows_i686_gnu" @@ -3542,9 +3539,9 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" [[package]] name = "windows_i686_msvc" @@ -3554,9 +3551,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" [[package]] name = "windows_x86_64_gnu" @@ -3566,9 +3563,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" [[package]] name = "windows_x86_64_gnullvm" @@ -3578,9 +3575,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" [[package]] name = "windows_x86_64_msvc" @@ -3590,15 +3587,15 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" [[package]] name = "winnow" -version = "0.5.30" +version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b5c3db89721d50d0e2a673f5043fc4722f76dcc352d7b1ab8b8288bed4ed2c5" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" dependencies = [ "memchr", ] @@ -3620,9 +3617,9 @@ checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" [[package]] name = "xxhash-rust" -version = "0.8.7" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9828b178da53440fa9c766a3d2f73f7cf5d0ac1fe3980c1e5018d899fd19e07b" +checksum = "927da81e25be1e1a2901d59b81b37dd2efd1fc9c9345a55007f09bf5a2d3ee03" [[package]] name = "yaml-rust" @@ -3635,22 +3632,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.31" +version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c4061bedbb353041c12f413700357bec76df2c7e2ca8e4df8bac24c6bf68e3d" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.31" +version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.57", ] [[package]] @@ -3680,9 +3677,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.9+zstd.1.5.5" +version = "2.0.10+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" dependencies = [ "cc", "pkg-config",