217 lines
5.9 KiB
Rust
217 lines
5.9 KiB
Rust
use leptos::{html::Form, logging};
|
|
use leptos_meta::{Meta, Title};
|
|
use leptos::{prelude::*, server_fn::codec::JsonEncoding};
|
|
use markdown_view_leptos::markdown_view;
|
|
use serde::{Deserialize, Serialize};
|
|
use thiserror::Error;
|
|
|
|
#[server]
|
|
async fn submit_mail(mail: String, subject: String, message: String) -> Result<String, MailError> {
|
|
use crate::AppState;
|
|
const NTFY_TOPIC_CREDENTIAL_NAME: &str = "ntfy-topic";
|
|
|
|
let mail = format!("From: {}\nSubject: {}\n{}", mail, subject, message);
|
|
logging::log!("{}", mail);
|
|
|
|
let state = use_context::<AppState>().unwrap();
|
|
let ntfy_topic = std::fs::read_to_string(
|
|
state
|
|
.config
|
|
.credentials_directory
|
|
.join(NTFY_TOPIC_CREDENTIAL_NAME),
|
|
)?;
|
|
let ntfy_url = state.config.ntfy_instance.join(ntfy_topic.trim())?;
|
|
|
|
state
|
|
.http_client
|
|
.post(ntfy_url)
|
|
.body(mail)
|
|
.send()
|
|
.await
|
|
.map_err(|err| err.without_url())?
|
|
.error_for_status()
|
|
.map_err(|err| err.without_url())?;
|
|
|
|
leptos_axum::redirect("/mail");
|
|
Ok("Mail successfully sent!".to_owned())
|
|
}
|
|
|
|
#[component]
|
|
fn MailForm() -> impl IntoView {
|
|
let form: NodeRef<Form> = NodeRef::new();
|
|
let submit_mail = ServerAction::<SubmitMail>::new();
|
|
let pending = submit_mail.pending();
|
|
let flash_message = submit_mail.value();
|
|
|
|
Effect::new(move ||
|
|
if flash_message.get().is_some_and(|m| m.is_ok()) {
|
|
if let Some(form) = form.get() {
|
|
form.reset();
|
|
} else {
|
|
logging::warn!("Failed to reset form");
|
|
}
|
|
}
|
|
);
|
|
|
|
view! {
|
|
<Show when=move || { flash_message.get().is_some() }>
|
|
<div
|
|
class="notification is-light"
|
|
class=("is-success", move || flash_message.get().is_some_and(|m| m.is_ok()))
|
|
class=("is-danger", move || flash_message.get().is_some_and(|m| m.is_err()))
|
|
>
|
|
<button
|
|
class="delete"
|
|
aria-label="Close"
|
|
on:click=move |_| {
|
|
*flash_message.write() = None;
|
|
}
|
|
/>
|
|
<span role="alert">
|
|
{move || match flash_message.get() {
|
|
None => "".to_owned(),
|
|
Some(Ok(message)) => message,
|
|
Some(Err(error)) => format!("{}", error),
|
|
}}
|
|
</span>
|
|
</div>
|
|
</Show>
|
|
|
|
<ActionForm node_ref=form action=submit_mail>
|
|
<fieldset disabled=move || pending.get()>
|
|
<div class="field">
|
|
<label class="label" for="mail">
|
|
Email address
|
|
</label>
|
|
<div class="control">
|
|
<input
|
|
id="mail"
|
|
class="input"
|
|
type="email"
|
|
placeholder="Your address"
|
|
name="mail"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label class="label" for="subject">
|
|
Subject
|
|
</label>
|
|
<div class="control">
|
|
<input
|
|
id="subject"
|
|
class="input"
|
|
type="text"
|
|
placeholder="E.g. There's a typo on your home page!"
|
|
name="subject"
|
|
autocomplete="off"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label class="label" for="message">
|
|
Message
|
|
</label>
|
|
<textarea
|
|
id="message"
|
|
class="textarea"
|
|
type="text"
|
|
rows="6"
|
|
name="message"
|
|
autocomplete="off"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="control">
|
|
<div class="field">
|
|
<button
|
|
type="submit"
|
|
class="button is-link"
|
|
class=("is-loading", move || pending.get())
|
|
>
|
|
Send
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
</ActionForm>
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
pub fn Mail() -> impl IntoView {
|
|
view! {
|
|
<Meta name="description" content="tlater.net mail submission" />
|
|
<Title text="Mail submission" />
|
|
|
|
<section class="section">
|
|
<div class="container">
|
|
<h1 class="title has-text-weight-normal">Contact Me</h1>
|
|
|
|
<div class="columns">
|
|
<div class="column">
|
|
<MailForm />
|
|
</div>
|
|
|
|
<div class="column content">
|
|
{markdown_view!(
|
|
r#"
|
|
Any messages you enter here are directly forwarded to me. I aim to
|
|
respond within a day.
|
|
|
|
Don't be upset about the form, I want to avoid the spam publishing
|
|
your email address brings with it... And minimize the amount of mail
|
|
that doesn't reach me, this form is an exception in all my spam
|
|
filters, you see ;)
|
|
"#
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
}
|
|
}
|
|
|
|
#[derive(Error, Debug, Clone, Serialize, Deserialize)]
|
|
pub enum MailError {
|
|
#[error("This form appears to currently be broken :(")]
|
|
Permanent,
|
|
#[error("Mail service appears to be down; please try again later")]
|
|
Temporary,
|
|
#[error("Server error: {0}")]
|
|
ServerFnError(ServerFnErrorErr),
|
|
}
|
|
|
|
impl From<url::ParseError> for MailError {
|
|
fn from(error: url::ParseError) -> Self {
|
|
logging::error!("Invalid ntfy URL: {error}");
|
|
Self::Permanent
|
|
}
|
|
}
|
|
|
|
impl From<std::io::Error> for MailError {
|
|
fn from(error: std::io::Error) -> Self {
|
|
logging::error!("Couldn't read ntfy topic secret: {error}");
|
|
Self::Permanent
|
|
}
|
|
}
|
|
|
|
impl From<reqwest::Error> for MailError {
|
|
fn from(error: reqwest::Error) -> Self {
|
|
logging::error!("Failed to connect to ntfy: {error}");
|
|
Self::Temporary
|
|
}
|
|
}
|
|
|
|
impl FromServerFnError for MailError {
|
|
type Encoder = JsonEncoding;
|
|
|
|
fn from_server_fn_error(value: ServerFnErrorErr) -> Self {
|
|
Self::ServerFnError(value)
|
|
}
|
|
}
|