diff --git a/.cargo-husky/hooks/pre-push b/.cargo-husky/hooks/pre-push index dece58ee..e07e64bc 100755 --- a/.cargo-husky/hooks/pre-push +++ b/.cargo-husky/hooks/pre-push @@ -36,15 +36,15 @@ cargo clippy --all-targets -- -D warnings # With all features echo "### build --all-features ###" cargo build --all-features -echo "### nextest run --all-features --workspace ###" -cargo nextest run --all-features --no-fail-fast --workspace +echo "### nextest run --all-features ###" +cargo nextest run --all-features --no-fail-fast # Nextest doesn't support doc tests, run those separately -echo "### test --doc --all-features --workspace ###" -cargo test --doc --all-features --no-fail-fast --workspace -echo "### check --all-features --workspace ###" -cargo check --all-features --workspace -echo "### clippy --workspace --all-targets --all-features -- -D warnings ###" -cargo clippy --workspace --all-targets --all-features -- -D warnings +echo "### test --doc --all-features ###" +cargo test --doc --all-features --no-fail-fast +echo "### check --all-features ###" +cargo check --all-features +echo "### clippy --all-targets --all-features -- -D warnings ###" +cargo clippy --all-targets --all-features -- -D warnings echo "### cargo doc --all-features --no-deps ###" RUSTDOCFLAGS="-D rustdoc::all -A rustdoc::private_intra_doc_links" cargo doc --all-features --no-deps diff --git a/README.md b/README.md index a22a80ea..1b3d5407 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ and [Poem](https://github.com/poem-web/poem). the `db-sql` feature) - Built-in support for [Sidekiq.rs](https://crates.io/crates/rusty-sidekiq) for running async/background jobs (requires the `sidekiq` feature) +- Built-in support for sending emails via SMTP (requires the `email-smtp` feature) - Structured logs/traces using tokio's [tracing](https://docs.rs/tracing/latest/tracing/) crate. Export traces/metrics using OpenTelemetry (requires the `otel` feature). - Health checks to ensure the app's external dependencies are healthy @@ -50,20 +51,20 @@ and [Poem](https://github.com/poem-web/poem). ## Start local DB ```shell -# Replace `example_dev` with your app name, e.g., `myapp_dev` (unless you're using our `full` example, as demonstrated below) -# Dev +# Replace `example_dev` with your app name, e.g., `myapp_dev` docker run -d -p 5432:5432 -e POSTGRES_USER=roadster -e POSTGRES_DB=example_dev -e POSTGRES_PASSWORD=roadster postgres:15.3-alpine -# Test -docker run -d -p 5433:5432 -e POSTGRES_USER=roadster -e POSTGRES_DB=example_test -e POSTGRES_PASSWORD=roadster postgres:15.3-alpine ``` ## Start local Redis instance (for [Sidekiq.rs](https://crates.io/crates/rusty-sidekiq)) ```shell -# Dev docker run -d -p 6379:6379 redis:7.2-alpine -# Test -docker run -d -p 6380:6379 redis:7.2-alpine +``` + +## Start local SMTP server instance (example: [maildev](https://github.com/maildev/maildev)) + +```shell +docker run -d -p 1080:1080 -p 1025:1025 maildev/maildev ``` ## Create your app @@ -90,6 +91,10 @@ echo ROADSTER__ENVIRONMENT=development >> .env cargo run ``` +## Explore the API + +Navigate to http://localhost:3000/api/_docs to explore the app's OpenAPI playground + # Add a UI Currently, Roadster is focused on back-end API development with Rust. We leave it to the consumer to decide how they @@ -106,6 +111,15 @@ framework ([Leptos](https://github.com/leptos-rs/leptos) / [Yew](https://github. |-----------------------------------------------|-------------------------------------------------------------------------------------| | [Leptos](https://github.com/leptos-rs/leptos) | [leptos-ssr](https://github.com/roadster-rs/roadster/tree/main/examples/leptos-ssr) | +# Email + +## Local testing of sending emails via SMTP + +If you're using our SMTP integration to send emails, you can test locally using a mock SMTP server. Some options: + +- [maildev](https://github.com/maildev/maildev) +- [smtp4dev](https://github.com/rnwood/smtp4dev) + # Tracing + OpenTelemetry Roadster allows reporting traces and metrics using the `tracing` and `opentelemetry_rust` integrations. Provide the URL diff --git a/examples/full/README.md b/examples/full/README.md index 0ef1b0eb..b39e1a60 100644 --- a/examples/full/README.md +++ b/examples/full/README.md @@ -10,6 +10,8 @@ export ROADSTER__ENVIRONMENT=development # Start the database and redis (for sidekiq). Note: change the credentials when deploying to prod docker run -d -p 5432:5432 -e POSTGRES_USER=roadster -e POSTGRES_DB=example_dev -e POSTGRES_PASSWORD=roadster postgres:15.3-alpine docker run -d -p 6379:6379 redis:7.2-alpine +# Start a local smtp server, such as https://github.com/maildev/maildev +docker run -d -p 1080:1080 -p 1025:1025 maildev/maildev # Start the app cargo run ``` diff --git a/examples/full/config/development/email.toml b/examples/full/config/development/email.toml index 79816a54..45e9b36e 100644 --- a/examples/full/config/development/email.toml +++ b/examples/full/config/development/email.toml @@ -2,4 +2,10 @@ from = "no-reply@example.com" [email.smtp.connection] +# The `smtps` scheme should be used in production uri = "smtp://localhost:1025" +# Alternatively, provide connection details as individual fields +#host = "smtp.example.com" +#port = 465 +#username = "username" +#password = "password" diff --git a/src/config/email/smtp.rs b/src/config/email/smtp.rs index c92d01dc..f759bbc4 100644 --- a/src/config/email/smtp.rs +++ b/src/config/email/smtp.rs @@ -37,6 +37,7 @@ impl Validate for SmtpConnection { #[non_exhaustive] pub struct SmtpConnectionFields { pub host: String, + pub port: Option, pub username: String, pub password: String, } @@ -69,7 +70,15 @@ impl TryFrom<&SmtpConnection> for SmtpTransportBuilder { SmtpConnection::Fields(fields) => { let credentials = Credentials::new(fields.username.clone(), fields.password.clone()); - SmtpTransport::relay(&fields.host).map(|builder| builder.credentials(credentials)) + SmtpTransport::relay(&fields.host) + .map(|builder| { + if let Some(port) = fields.port { + builder.port(port) + } else { + builder + } + }) + .map(|builder| builder.credentials(credentials)) } SmtpConnection::Uri(fields) => SmtpTransport::from_url(fields.uri.as_ref()), } @@ -153,6 +162,17 @@ mod tests { uri = "smtps://username:password@smtp.example.com:425" "# )] + #[case( + r#" + from = "no-reply@example.com" + + [smtp.connection] + host = "smtp.example.com" + port = 465 + username = "username" + password = "password" + "# + )] #[cfg_attr(coverage_nightly, coverage(off))] fn serialization(_case: TestCase, #[case] config: &str) { let email: Email = toml::from_str(config).unwrap(); diff --git a/src/config/email/snapshots/roadster__config__email__smtp__tests__serialization@case_6.snap b/src/config/email/snapshots/roadster__config__email__smtp__tests__serialization@case_6.snap new file mode 100644 index 00000000..b5730854 --- /dev/null +++ b/src/config/email/snapshots/roadster__config__email__smtp__tests__serialization@case_6.snap @@ -0,0 +1,10 @@ +--- +source: src/config/email/smtp.rs +expression: email +--- +from = 'no-reply@example.com' +[smtp.connection] +host = 'smtp.example.com' +port = 465 +username = 'username' +password = 'password'