Skip to content

Commit

Permalink
chore: make email an optional feature
Browse files Browse the repository at this point in the history
  • Loading branch information
lajp committed Oct 13, 2024
1 parent 1a3653c commit e6136a0
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 31 deletions.
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ authors = ["Luukas Pörtfors <[email protected]>"]
lto = true

[features]
default = ["email"]
system_fonts = ["dep:fontdb"]
email = ["dep:reqwest"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

Expand All @@ -29,7 +31,7 @@ garde = "0.17.0"
iban_validate = "4.0.1"
lopdf = { git = "https://github.com/J-F-Liu/lopdf.git", rev = "7f24a1c3ebc42470a37b4315b843331e4f81cdcd" }
regex = "1.10.6"
reqwest = { version = "0.12.5", default-features = false, features = ["multipart", "rustls-tls"] }
reqwest = { version = "0.12.5", default-features = false, features = ["multipart", "rustls-tls"], optional = true }
serde = "1.0.195"
serde_derive = "1.0.195"
serde_json = "1.0.111"
Expand Down
63 changes: 55 additions & 8 deletions src/api/invoices.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
use std::sync::LazyLock;

use crate::error::Error;
#[cfg(feature = "email")]
use crate::mailgun::MailgunClient;
use axum::{async_trait, body::Bytes, http::StatusCode, Json};

use axum::{async_trait, body::Bytes, http::StatusCode};
use axum_typed_multipart::{
FieldData, FieldMetadata, TryFromChunks, TryFromMultipart, TypedMultipart, TypedMultipartError,
};

use axum_valid::Garde;
use futures::stream::Stream;
use garde::Validate;
use iban::Iban;
use regex::Regex;
use serde_derive::{Deserialize, Serialize};

use typst::model::Document;

static ALLOWED_FILENAME: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?i)\.(jpg|jpeg|png|gif|svg|pdf)$").unwrap());

Expand Down Expand Up @@ -129,19 +134,61 @@ fn try_handle_file(field: FieldData<Bytes>) -> Result<InvoiceAttachment, Error>
})
}

pub async fn create(
#[cfg(feature = "email")]
pub async fn create_email(
client: MailgunClient,
Garde(TypedMultipart(mut multipart)): Garde<TypedMultipart<InvoiceForm>>,
) -> Result<(StatusCode, Json<Invoice>), Error> {
) -> Result<(StatusCode, axum::Json<Invoice>), Error> {
let orig = multipart.data.clone();
multipart.data.attachments = Result::from_iter(
multipart.data.attachments =
Result::from_iter(multipart.attachments.into_iter().map(try_handle_file))?;

let document: Document = multipart.data.to_owned().try_into()?;
let pdf = typst_pdf::pdf(&document, typst::foundations::Smart::Auto, None);

let mut pdfs = vec![pdf];
pdfs.extend_from_slice(
multipart
.data
.attachments
.into_iter()
.map(try_handle_file)
.collect::<Vec<_>>(),
)?;
.map(|a| a.bytes)
.collect::<Vec<_>>()
.as_slice(),
);

let pdf = crate::merge::merge_pdf(pdfs)?;

client.send_mail(multipart.data).await?;
client.send_mail(&orig, pdf).await?;
Ok((StatusCode::CREATED, axum::Json(orig)))
}

#[cfg(not(feature = "email"))]
pub async fn create(
Garde(TypedMultipart(mut multipart)): Garde<TypedMultipart<InvoiceForm>>,
) -> Result<axum::response::Response, Error> {
multipart.data.attachments =
Result::from_iter(multipart.attachments.into_iter().map(try_handle_file))?;

let document: Document = multipart.data.to_owned().try_into()?;
let pdf = typst_pdf::pdf(&document, typst::foundations::Smart::Auto, None);

let mut pdfs = vec![pdf];
pdfs.extend_from_slice(
multipart
.data
.attachments
.into_iter()
.map(|a| a.bytes)
.collect::<Vec<_>>()
.as_slice(),
);

let pdf = crate::merge::merge_pdf(pdfs)?;

Ok(axum::response::Response::builder()
.status(StatusCode::CREATED)
.header("Content-Type", "application/pdf")
.body(Bytes::from(pdf).into())
.unwrap())
}
8 changes: 7 additions & 1 deletion src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,13 @@ pub fn app() -> Router<crate::state::State> {

Router::new()
.route("/health", get(health))
.route("/invoices", post(invoices::create))
.route(
"/invoices",
#[cfg(feature = "email")]
post(invoices::create_email),
#[cfg(not(feature = "email"))]
post(invoices::create),
)
.layer(TraceLayer::new_for_http())
.layer(cors_layer)
.layer(DefaultBodyLimit::disable())
Expand Down
7 changes: 4 additions & 3 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use serde_derive::Serialize;
#[allow(clippy::enum_variant_names)]
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[cfg(feature = "email")]
#[error("Reqwest error {0}")]
ReqwestError(#[from] reqwest::Error),
#[error("Error while parsing multipart form")]
Expand Down Expand Up @@ -38,9 +39,9 @@ impl IntoResponse for Error {
error!(%self);

let status = match self {
Error::InternalServerError(_) | Error::ReqwestError(_) | Error::TypstError => {
StatusCode::INTERNAL_SERVER_ERROR
}
Error::InternalServerError(_) | Error::TypstError => StatusCode::INTERNAL_SERVER_ERROR,
#[cfg(feature = "email")]
Error::ReqwestError(_) => StatusCode::INTERNAL_SERVER_ERROR,
Error::JsonError(_)
| Error::MissingFilename
| Error::MultipartError(_)
Expand Down
19 changes: 1 addition & 18 deletions src/mailgun/invoices.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,9 @@
use super::MailgunClient;
use crate::api::invoices::Invoice;
use crate::error::Error;
use crate::merge::merge_pdf;
use typst::model::Document;

impl MailgunClient {
pub async fn send_mail(self, invoice: Invoice) -> Result<(), Error> {
let document: Document = invoice.to_owned().try_into()?;
let pdf = typst_pdf::pdf(&document, typst::foundations::Smart::Auto, None);

let mut pdfs = vec![pdf];
pdfs.extend_from_slice(
invoice
.attachments
.into_iter()
.map(|a| a.bytes)
.collect::<Vec<_>>()
.as_slice(),
);

let pdf = merge_pdf(pdfs)?;

pub async fn send_mail(self, invoice: &Invoice, pdf: Vec<u8>) -> Result<(), Error> {
let invoice_recipient = format!("{} <{}>", invoice.recipient_name, invoice.recipient_email);
let form = reqwest::multipart::Form::new()
.text("from", self.from)
Expand Down
3 changes: 3 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::sync::LazyLock;

mod api;
mod error;
#[cfg(feature = "email")]
mod mailgun;
mod merge;
mod state;
Expand All @@ -18,6 +19,7 @@ mod tests;
#[macro_use]
extern crate tracing;

#[cfg(feature = "email")]
#[derive(Parser, Clone, Debug)]
struct MailgunConfig {
/// Url used by mailgun
Expand All @@ -40,6 +42,7 @@ struct MailgunConfig {
#[derive(Parser, Clone, Debug)]
#[command(version, about, long_about = None)]
struct LaskugenConfig {
#[cfg(feature = "email")]
#[clap(flatten)]
mailgun: MailgunConfig,
/// The listen port for the HTTP server
Expand Down
4 changes: 4 additions & 0 deletions src/state.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
#[cfg(feature = "email")]
use crate::mailgun::MailgunClient;

use axum::extract::FromRef;

#[derive(FromRef, Clone)]
pub struct State {
#[cfg(feature = "email")]
pub mailgun_client: MailgunClient,
pub for_garde: (),
}
Expand All @@ -11,6 +14,7 @@ pub async fn new() -> State {
dotenv::dotenv().ok();

State {
#[cfg(feature = "email")]
mailgun_client: MailgunClient::from(crate::CONFIG.mailgun.clone()),
for_garde: (),
}
Expand Down

0 comments on commit e6136a0

Please sign in to comment.