feat(email_client): email client implementation with tests and configuration
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
parent
e81e628487
commit
78a55a6c06
10
.drone.yml
10
.drone.yml
|
@ -18,6 +18,12 @@ steps:
|
|||
environment:
|
||||
POSTGRES_HOST: postgres.server
|
||||
SKIP_DOCKER: "true"
|
||||
EMAIL_SENDER: Cyberduck <cyberduck@coincoinmail.fr>
|
||||
EMAIL_HOST: coincoinmail.fr
|
||||
EMAIL_PASSWORD:
|
||||
from_secret: email_password
|
||||
EMAIL_USER:
|
||||
from_secret: email_user
|
||||
commands:
|
||||
- ./scripts/init_db.sh
|
||||
- cargo build
|
||||
|
@ -50,6 +56,10 @@ steps:
|
|||
from_secret: postgres_password
|
||||
POSTGRES_DB:
|
||||
from_secret: postgres_db
|
||||
EMAIL_PASSWORD:
|
||||
from_secret: email_password
|
||||
EMAIL_USER:
|
||||
from_secret: email_user
|
||||
commands:
|
||||
- docker stack deploy -c docker/deployment/stack.yml zero2prod
|
||||
volumes:
|
||||
|
|
|
@ -5,6 +5,10 @@ repos:
|
|||
- id: fmt
|
||||
- id: cargo-check
|
||||
- id: clippy
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: sqlx-offline
|
||||
|
|
|
@ -31,6 +31,10 @@ services:
|
|||
- POSTGRES_PASSWORD
|
||||
- POSTGRES_PORT=5432
|
||||
- POSTGRES_HOST=postgres
|
||||
- EMAIL_USER
|
||||
- EMAIL_PASSWORD
|
||||
- EMAIL_SENDER="Cyberduck <cyberduck@coincoinmail.fr>"
|
||||
- EMAIL_HOST=coincoinmail.fr
|
||||
- PROD=true
|
||||
networks:
|
||||
- net
|
||||
|
|
|
@ -55,5 +55,9 @@ printf "DB_PASSWORD=${DB_PASSWORD}\n" >> .env
|
|||
printf "DB_PORT=${DB_PORT}\n" >> .env
|
||||
printf "DB_HOST=${DB_HOST}\n" >> .env
|
||||
printf "DB_NAME=${DB_NAME}\n" >> .env
|
||||
printf "EMAIL_USER=${EMAIL_USER}\n" >> .env
|
||||
printf "EMAIL_SENDER=${EMAIL_SENDER}\n" >> .env
|
||||
printf "EMAIL_HOST=${EMAIL_HOST}\n" >> .env
|
||||
printf "EMAIL_PASSWORD=${EMAIL_PASSWORD}\n" >> .env
|
||||
|
||||
printf "${GREEN}Postgres has been migrated, ready to go!${END}"
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
//! src/configuration.rs
|
||||
use dotenv::dotenv;
|
||||
use lettre::transport::smtp::authentication::Credentials;
|
||||
use lettre::AsyncSmtpTransport;
|
||||
use lettre::Tokio1Executor;
|
||||
use secrecy::ExposeSecret;
|
||||
use secrecy::Secret;
|
||||
use serde::Deserialize;
|
||||
use sqlx::{Connection, Executor, PgConnection, PgPool};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::email_client::EmailClient;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct DatabaseConfig {
|
||||
pub user: String,
|
||||
|
@ -19,7 +24,7 @@ pub fn parse_db_config() -> DatabaseConfig {
|
|||
dotenv().ok();
|
||||
envy::prefixed("DB_")
|
||||
.from_env::<DatabaseConfig>()
|
||||
.expect("Database config environment variables")
|
||||
.expect("Missing database config environment variables")
|
||||
}
|
||||
|
||||
impl DatabaseConfig {
|
||||
|
@ -76,3 +81,36 @@ impl DatabaseConfig {
|
|||
connection_pool
|
||||
}
|
||||
}
|
||||
|
||||
// default starttls => set as config ?
|
||||
#[derive(Deserialize)]
|
||||
pub struct EmailConfig {
|
||||
pub sender: String,
|
||||
pub user: String,
|
||||
pub password: Secret<String>,
|
||||
pub host: String,
|
||||
}
|
||||
|
||||
pub fn parse_email_config() -> EmailConfig {
|
||||
dotenv().ok();
|
||||
envy::prefixed("EMAIL_")
|
||||
.from_env::<EmailConfig>()
|
||||
.expect("Missing email config environment variables")
|
||||
}
|
||||
|
||||
impl EmailConfig {
|
||||
pub fn email_client(&self) -> EmailClient {
|
||||
let creds = Credentials::new(
|
||||
self.user.to_owned(),
|
||||
self.password.expose_secret().to_owned(),
|
||||
);
|
||||
let mailer = AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&self.host)
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
EmailClient {
|
||||
sender: self.sender.parse().unwrap(),
|
||||
mailer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,41 +1,37 @@
|
|||
use lettre::transport::smtp::authentication::Credentials;
|
||||
use lettre::message::Mailbox;
|
||||
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
|
||||
|
||||
use crate::domain::SubscriberEmail;
|
||||
use crate::domain::{SubscriberEmail, SubscriberName};
|
||||
|
||||
// here I use my own email server instead of an API
|
||||
// I skip the "properly call and test/mock and API" part of the book
|
||||
// But I learn how to directly send mail from rust
|
||||
|
||||
pub struct EmailClient {
|
||||
sender: SubscriberEmail, // internals
|
||||
pub sender: Mailbox,
|
||||
pub mailer: AsyncSmtpTransport<Tokio1Executor>,
|
||||
}
|
||||
|
||||
// create initializer from config
|
||||
// test send from tests
|
||||
impl EmailClient {
|
||||
// init
|
||||
pub fn format_recipient(name: SubscriberName, email: SubscriberEmail) -> Mailbox {
|
||||
format!("{} <{}>", name.as_ref(), email.as_ref())
|
||||
.parse()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn send_email(
|
||||
&self,
|
||||
recipient: SubscriberEmail,
|
||||
recipient: Mailbox,
|
||||
subject: &str,
|
||||
html_content: &str,
|
||||
) -> Result<(), String> {
|
||||
let email = Message::builder()
|
||||
.from(self.sender.as_ref().parse().unwrap())
|
||||
// todo => create from name + email
|
||||
.to(recipient.as_ref().parse().unwrap())
|
||||
.from(self.sender.to_owned())
|
||||
.to(recipient)
|
||||
.subject(subject)
|
||||
.body(String::from(html_content))
|
||||
.unwrap();
|
||||
// set on init
|
||||
let creds = Credentials::new(
|
||||
self.sender.as_ref().to_string(),
|
||||
"5yfHvyKUYbvvNB".to_string(),
|
||||
);
|
||||
// set on init
|
||||
let mailer = AsyncSmtpTransport::<Tokio1Executor>::relay("smtp.coincoinmail.fr")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
match mailer.send(email).await {
|
||||
match self.mailer.send(email).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(format!("Could not send email: {:?}", e)),
|
||||
}
|
||||
|
@ -44,5 +40,22 @@ impl EmailClient {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// test sending => return ok
|
||||
use crate::{
|
||||
configuration::parse_email_config,
|
||||
domain::{SubscriberEmail, SubscriberName},
|
||||
};
|
||||
|
||||
use super::EmailClient;
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_email_without_error() {
|
||||
let email_client = parse_email_config().email_client();
|
||||
let name = SubscriberName::parse("flavien".to_owned()).unwrap();
|
||||
let email = SubscriberEmail::parse("flavien@coincoinmail.fr".to_owned()).unwrap();
|
||||
let recipient = EmailClient::format_recipient(name, email);
|
||||
email_client
|
||||
.send_email(recipient, "Zero2Prod test", "OK")
|
||||
.await
|
||||
.expect("Mail sending error");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use clap::{command, Parser};
|
||||
use std::net::TcpListener;
|
||||
use zero2prod::configuration::parse_db_config;
|
||||
use zero2prod::configuration::{parse_db_config, parse_email_config};
|
||||
use zero2prod::startup::run;
|
||||
use zero2prod::telemetry::{get_subscriber, init_subscriber};
|
||||
|
||||
|
@ -25,6 +25,7 @@ async fn main() -> std::io::Result<()> {
|
|||
};
|
||||
let listener = TcpListener::bind(address)?;
|
||||
let connection_pool = parse_db_config().connection_pool().await;
|
||||
let email_client = parse_email_config().email_client();
|
||||
|
||||
run(listener, connection_pool)?.await
|
||||
run(listener, connection_pool, email_client)?.await
|
||||
}
|
||||
|
|
|
@ -1,17 +1,26 @@
|
|||
use crate::routes::{health_check, subscriptions};
|
||||
use crate::{
|
||||
email_client::EmailClient,
|
||||
routes::{health_check, subscriptions},
|
||||
};
|
||||
use actix_web::{dev::Server, web, App, HttpServer};
|
||||
use sqlx::PgPool;
|
||||
use std::net::TcpListener;
|
||||
use tracing_actix_web::TracingLogger;
|
||||
|
||||
pub fn run(listener: TcpListener, db_pool: PgPool) -> Result<Server, std::io::Error> {
|
||||
pub fn run(
|
||||
listener: TcpListener,
|
||||
db_pool: PgPool,
|
||||
email_client: EmailClient,
|
||||
) -> Result<Server, std::io::Error> {
|
||||
let db_pool = web::Data::new(db_pool);
|
||||
let email_client = web::Data::new(email_client);
|
||||
let server = HttpServer::new(move || {
|
||||
App::new()
|
||||
.wrap(TracingLogger::default())
|
||||
.service(health_check)
|
||||
.service(subscriptions)
|
||||
.app_data(db_pool.clone())
|
||||
.app_data(email_client.clone())
|
||||
})
|
||||
.listen(listener)?
|
||||
.run();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use once_cell::sync::Lazy;
|
||||
use sqlx::PgPool;
|
||||
use std::net::TcpListener;
|
||||
use zero2prod::configuration::parse_db_config;
|
||||
use zero2prod::configuration::{parse_db_config, parse_email_config};
|
||||
use zero2prod::startup::run;
|
||||
use zero2prod::telemetry::{get_subscriber, init_subscriber};
|
||||
|
||||
|
@ -30,7 +30,9 @@ async fn spawn_app() -> TestApp {
|
|||
let address = format!("http://127.0.0.1:{}", port);
|
||||
|
||||
let connection_pool = parse_db_config().test_connection_pool().await;
|
||||
let server = run(listener, connection_pool.clone()).expect("Failed to bind address");
|
||||
let email_client = parse_email_config().email_client();
|
||||
let server =
|
||||
run(listener, connection_pool.clone(), email_client).expect("Failed to bind address");
|
||||
let _ = tokio::spawn(server);
|
||||
|
||||
TestApp {
|
||||
|
|
Loading…
Reference in New Issue