use axum::{
Json,
extract::{Multipart, Path, Query, State},
http::{HeaderMap, StatusCode, header},
response::{IntoResponse, Redirect, Response},
};
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, Order, PaginatorTrait, QueryFilter, QueryOrder, Set,
};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::sync::Arc;
use tracing::warn;
use uuid::Uuid;
use crate::app::AppState;
use crate::addons::voicemail::mailbox_service::MailboxService;
use crate::addons::voicemail::models::{
mailbox::{
ActiveModel as MailboxActiveModel, Column as MailboxColumn, Entity as MailboxEntity,
},
message::{
ActiveModel as MessageActiveModel, Column as MessageColumn, Entity as MessageEntity,
Model as Message,
},
};
use crate::addons::voicemail::settings::{FileConfig, SettingsForm};
use crate::addons::voicemail::storage::VoicemailStorage;
pub const VALID_PROMPT_NAMES: &[&str] = &[
"voicemail_greeting_default",
"beep",
"vm_saved",
"vm_menu",
"vm_enter_extension",
"vm_enter_pin",
"vm_auth_failed",
"vm_wrong_pin",
"vm_error",
"vm_no_messages",
"vm_no_more_messages",
"vm_deleted",
];
fn prompt_write_path(cfg: &FileConfig, name: &str) -> std::path::PathBuf {
std::path::Path::new(&cfg.voicemail.sounds_dir)
.join(&cfg.voicemail.language)
.join(format!("{}.wav", name))
}
fn prompt_read_path(cfg: &FileConfig, name: &str) -> Option<std::path::PathBuf> {
let base = std::path::Path::new(&cfg.voicemail.sounds_dir);
let lang_path = base
.join(&cfg.voicemail.language)
.join(format!("{}.wav", name));
if lang_path.exists() {
return Some(lang_path);
}
let en_path = base.join("en").join(format!("{}.wav", name));
if en_path.exists() {
return Some(en_path);
}
let root_path = base.join(format!("{}.wav", name));
if root_path.exists() {
return Some(root_path);
}
None
}
async fn write_prompt_file(dest: &std::path::Path, data: &[u8]) -> Result<(), String> {
if let Some(parent) = dest.parent() {
tokio::fs::create_dir_all(parent)
.await
.map_err(|e| format!("create dir: {}", e))?;
}
tokio::fs::write(dest, data)
.await
.map_err(|e| format!("write file: {}", e))
}
#[derive(Clone)]
pub struct VoicemailWebState {
pub app: AppState,
pub mailbox_svc: MailboxService,
pub storage: Arc<VoicemailStorage>,
pub config_path: std::path::PathBuf,
}
#[derive(Deserialize, Default)]
pub struct MessageListQuery {
pub unread: Option<bool>,
}
#[derive(Deserialize)]
pub struct CreateMailboxForm {
pub extension: String,
pub email: Option<String>,
pub pin: String,
}
#[derive(Deserialize)]
pub struct ChangePinForm {
pub old_pin: Option<String>,
pub new_pin: String,
}
#[derive(Serialize)]
struct MessageResponse {
id: String,
caller_id: String,
duration: i32,
read: bool,
transcript: Option<String>,
summary: Option<String>,
created_at: String,
audio_url: String,
}
impl From<&Message> for MessageResponse {
fn from(m: &Message) -> Self {
Self {
id: m.id.to_string(),
caller_id: m.caller_id.clone(),
duration: m.duration,
read: m.read,
transcript: m.transcript.clone(),
summary: m.summary.clone(),
created_at: m.created_at.format("%Y-%m-%d %H:%M:%S").to_string(),
audio_url: format!("/api/voicemail/messages/{}/audio", m.id),
}
}
}
fn console(state: &VoicemailWebState) -> &crate::console::ConsoleState {
state.app.console.as_ref().expect("Console not initialized")
}
pub async fn ui_list_mailboxes(
State(state): State<VoicemailWebState>,
headers: HeaderMap,
) -> Response {
let db = state.mailbox_svc.db.clone();
let mailboxes = MailboxEntity::find()
.order_by(MailboxColumn::Extension, Order::Asc)
.all(&db)
.await
.unwrap_or_default();
let mut rows = Vec::with_capacity(mailboxes.len());
for mb in &mailboxes {
let unread: u64 = MessageEntity::find()
.filter(MessageColumn::BoxId.eq(mb.id))
.filter(MessageColumn::Read.eq(false))
.count(&db)
.await
.unwrap_or(0);
let total: u64 = MessageEntity::find()
.filter(MessageColumn::BoxId.eq(mb.id))
.count(&db)
.await
.unwrap_or(0);
rows.push(json!({
"extension": mb.extension,
"email": mb.email,
"unread": unread,
"total": total,
"created_at": mb.created_at.format("%Y-%m-%d").to_string(),
}));
}
let _lic = crate::license::get_license_status("voicemail");
let license_valid = _lic.as_ref().map(|l| l.valid && !l.expired).unwrap_or(false);
let license_expired = _lic.as_ref().map(|l| l.expired).unwrap_or(false);
console(&state).render_with_headers(
"voicemail/mailboxes.html",
json!({
"mailboxes": rows,
"nav_active": "Voicemail",
"license_valid": license_valid,
"license_expired": license_expired,
}),
&headers,
)
}
pub async fn ui_mailbox_messages(
State(state): State<VoicemailWebState>,
Path(extension): Path<String>,
headers: HeaderMap,
) -> Response {
let db = state.mailbox_svc.db.clone();
let mailbox = match state.mailbox_svc.find_by_extension(&extension).await {
Ok(Some(mb)) => mb,
Ok(None) => {
return Redirect::to("/console/voicemail").into_response();
}
Err(e) => {
warn!(extension = %extension, "mailbox lookup failed: {}", e);
return Redirect::to("/console/voicemail").into_response();
}
};
let mut messages = MessageEntity::find()
.filter(MessageColumn::BoxId.eq(mailbox.id))
.filter(MessageColumn::Read.eq(false))
.order_by(MessageColumn::CreatedAt, Order::Desc)
.all(&db)
.await
.unwrap_or_default();
let read = MessageEntity::find()
.filter(MessageColumn::BoxId.eq(mailbox.id))
.filter(MessageColumn::Read.eq(true))
.order_by(MessageColumn::CreatedAt, Order::Desc)
.all(&db)
.await
.unwrap_or_default();
messages.extend(read);
let msgs_json: Vec<_> = messages
.iter()
.map(|m| {
json!({
"id": m.id.to_string(),
"caller_id": m.caller_id,
"duration": m.duration,
"read": m.read,
"transcript": m.transcript,
"summary": m.summary,
"created_at": m.created_at.format("%Y-%m-%d %H:%M").to_string(),
"audio_url": format!("/api/voicemail/messages/{}/audio", m.id),
})
})
.collect();
let unread_count = messages.iter().filter(|m| !m.read).count();
console(&state).render_with_headers(
"voicemail/messages.html",
json!({
"extension": extension,
"email": mailbox.email,
"has_greeting": mailbox.greeting_path.is_some(),
"greeting_url": format!("/api/voicemail/mailboxes/{}/greeting", extension),
"messages": msgs_json,
"unread_count": unread_count,
"nav_active": "Voicemail",
}),
&headers,
)
}
pub async fn form_create_mailbox(
State(state): State<VoicemailWebState>,
axum::Form(form): axum::Form<CreateMailboxForm>,
) -> Response {
let email = form.email.as_deref().filter(|s| !s.is_empty());
match state
.mailbox_svc
.create_mailbox(&form.extension, email, &form.pin)
.await
{
Ok(_) => Redirect::to("/console/voicemail").into_response(),
Err(e) => {
warn!(extension = %form.extension, "create_mailbox failed: {}", e);
Redirect::to("/console/voicemail").into_response()
}
}
}
pub async fn form_delete_mailbox(
State(state): State<VoicemailWebState>,
Path(extension): Path<String>,
) -> Response {
let db = state.mailbox_svc.db.clone();
if let Ok(Some(mb)) = state.mailbox_svc.find_by_extension(&extension).await {
if let Ok(msgs) = MessageEntity::find()
.filter(MessageColumn::BoxId.eq(mb.id))
.all(&db)
.await
{
for msg in msgs {
if let Err(e) = state.storage.delete_audio(&msg.audio_path).await {
warn!(audio_path = %msg.audio_path, "audio delete failed: {}", e);
}
}
}
let active: MailboxActiveModel = mb.into();
if let Err(e) = active.delete(&db).await {
warn!(extension = %extension, "mailbox delete failed: {}", e);
}
}
Redirect::to("/console/voicemail").into_response()
}
pub async fn form_reset_pin(
State(state): State<VoicemailWebState>,
Path(extension): Path<String>,
) -> Response {
match state.mailbox_svc.reset_pin(&extension).await {
Ok(new_pin) => {
let location = format!(
"/console/voicemail/{}?new_pin={}",
urlencoding::encode(&extension),
urlencoding::encode(&new_pin)
);
Redirect::to(&location).into_response()
}
Err(e) => {
warn!(extension = %extension, "reset_pin failed: {}", e);
Redirect::to("/console/voicemail").into_response()
}
}
}
pub async fn form_delete_message(
State(state): State<VoicemailWebState>,
Path(id): Path<Uuid>,
) -> Response {
let db = state.mailbox_svc.db.clone();
match MessageEntity::find_by_id(id).one(&db).await {
Ok(Some(msg)) => {
let ext = {
let mb = MailboxEntity::find_by_id(msg.box_id)
.one(&db)
.await
.ok()
.flatten();
mb.map(|m| m.extension).unwrap_or_default()
};
if let Err(e) = state.storage.delete_audio(&msg.audio_path).await {
warn!(audio_path = %msg.audio_path, "audio delete failed: {}", e);
}
let active: MessageActiveModel = msg.into();
if let Err(e) = active.delete(&db).await {
warn!(id = %id, "message delete failed: {}", e);
}
let location = format!("/console/voicemail/{}", urlencoding::encode(&ext));
Redirect::to(&location).into_response()
}
_ => Redirect::to("/console/voicemail").into_response(),
}
}
pub async fn form_mark_read(
State(state): State<VoicemailWebState>,
Path(id): Path<Uuid>,
) -> Response {
let db = state.mailbox_svc.db.clone();
if let Ok(Some(msg)) = MessageEntity::find_by_id(id).one(&db).await {
let ext = {
let mb = MailboxEntity::find_by_id(msg.box_id)
.one(&db)
.await
.ok()
.flatten();
mb.map(|m| m.extension).unwrap_or_default()
};
if !msg.read {
let mut active: MessageActiveModel = msg.into();
active.read = Set(true);
if let Err(e) = active.update(&db).await {
warn!(id = %id, "mark_read failed: {}", e);
}
}
let location = format!("/console/voicemail/{}", urlencoding::encode(&ext));
Redirect::to(&location).into_response()
} else {
Redirect::to("/console/voicemail").into_response()
}
}
pub async fn api_list_messages(
State(state): State<VoicemailWebState>,
Path(extension): Path<String>,
Query(q): Query<MessageListQuery>,
) -> Response {
let db = state.mailbox_svc.db.clone();
let mailbox = match state.mailbox_svc.find_by_extension(&extension).await {
Ok(Some(mb)) => mb,
Ok(None) => {
return (
StatusCode::NOT_FOUND,
Json(json!({"error": "mailbox not found"})),
)
.into_response();
}
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": e.to_string()})),
)
.into_response();
}
};
let mut query = MessageEntity::find()
.filter(MessageColumn::BoxId.eq(mailbox.id))
.order_by(MessageColumn::CreatedAt, Order::Desc);
if let Some(true) = q.unread {
query = query.filter(MessageColumn::Read.eq(false));
}
let messages = match query.all(&db).await {
Ok(m) => m,
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": e.to_string()})),
)
.into_response();
}
};
let resp: Vec<MessageResponse> = messages.iter().map(MessageResponse::from).collect();
Json(json!({ "extension": extension, "messages": resp })).into_response()
}
pub async fn api_audio(State(state): State<VoicemailWebState>, Path(id): Path<Uuid>) -> Response {
let db = state.mailbox_svc.db.clone();
let msg = match MessageEntity::find_by_id(id).one(&db).await {
Ok(Some(m)) => m,
Ok(None) => {
return (StatusCode::NOT_FOUND, "message not found").into_response();
}
Err(e) => {
return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response();
}
};
if let Some(local_path) = state.storage.local_path(&msg.audio_path) {
if local_path.exists() {
match tokio::fs::read(&local_path).await {
Ok(bytes) => {
let filename = local_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("voicemail.wav");
return (
[
(header::CONTENT_TYPE, "audio/wav"),
(
header::CONTENT_DISPOSITION,
&format!("attachment; filename=\"{}\"", filename),
),
],
bytes,
)
.into_response();
}
Err(e) => {
warn!(path = ?local_path, "failed to read audio: {}", e);
return (StatusCode::INTERNAL_SERVER_ERROR, "audio read failed")
.into_response();
}
}
}
}
match state.storage.get_audio(&msg.audio_path).await {
Ok(bytes) => {
let filename = std::path::Path::new(&msg.audio_path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("voicemail.wav")
.to_string();
(
[
(header::CONTENT_TYPE, "audio/wav"),
(
header::CONTENT_DISPOSITION,
&format!("attachment; filename=\"{}\"", filename),
),
],
bytes.to_vec(),
)
.into_response()
}
Err(e) => {
warn!(audio_path = %msg.audio_path, "audio fetch failed: {}", e);
(StatusCode::NOT_FOUND, "audio not found").into_response()
}
}
}
pub async fn ui_settings(
State(state): State<VoicemailWebState>,
headers: HeaderMap,
) -> Response {
let cfg = FileConfig::load(&state.config_path);
let storage_type;
let storage_path;
let storage_bucket;
let storage_region;
let storage_access_key;
let storage_vendor;
let storage_endpoint;
let storage_prefix;
match &cfg.voicemail.storage {
crate::addons::voicemail::storage::VoicemailStorageConfig::Local { path } => {
storage_type = "local".to_string();
storage_path = path.clone();
storage_bucket = String::new();
storage_region = String::new();
storage_access_key = String::new();
storage_vendor = String::new();
storage_endpoint = String::new();
storage_prefix = String::new();
}
crate::addons::voicemail::storage::VoicemailStorageConfig::S3 {
vendor,
bucket,
region,
access_key,
endpoint,
prefix,
..
} => {
storage_type = "s3".to_string();
storage_path = String::new();
storage_bucket = bucket.clone();
storage_region = region.clone();
storage_access_key = access_key.clone();
storage_vendor = toml::to_string(vendor)
.unwrap_or_default()
.trim_matches('"')
.to_string();
storage_endpoint = endpoint.clone().unwrap_or_default();
storage_prefix = prefix.clone().unwrap_or_default();
}
}
let _lic = crate::license::get_license_status("voicemail");
let license_valid = _lic.as_ref().map(|l| l.valid && !l.expired).unwrap_or(false);
let license_expired = _lic.as_ref().map(|l| l.expired).unwrap_or(false);
console(&state).render_with_headers(
"voicemail/settings.html",
json!({
"nav_active": "Voicemail",
"license_valid": license_valid,
"license_expired": license_expired,
"spool_dir": cfg.voicemail.spool_dir,
"max_duration_secs": cfg.voicemail.max_duration_secs,
"silence_timeout_secs": cfg.voicemail.silence_timeout_secs,
"storage_type": storage_type,
"storage_path": storage_path,
"storage_bucket": storage_bucket,
"storage_region": storage_region,
"storage_access_key": storage_access_key,
"storage_vendor": storage_vendor,
"storage_endpoint": storage_endpoint,
"storage_prefix": storage_prefix,
"max_messages_per_mailbox": cfg.voicemail.max_messages_per_mailbox.unwrap_or(0),
"max_age_days": cfg.voicemail.max_age_days.unwrap_or(0),
"transcribe_enabled": cfg.voicemail.transcribe_enabled,
"smtp_host": cfg.smtp_host(),
"smtp_port": cfg.smtp_port(),
"smtp_username": cfg.smtp_username(),
"smtp_from": cfg.smtp_from(),
"smtp_subject_template": cfg.smtp_subject_template(),
"smtp_enabled": cfg.smtp_enabled(),
"webhook_url": cfg.webhook_url_str(),
"language": cfg.voicemail.language,
"sounds_dir": cfg.voicemail.sounds_dir,
}),
&headers,
)
}
pub async fn form_save_settings(
State(state): State<VoicemailWebState>,
axum::Form(form): axum::Form<SettingsForm>,
) -> Response {
let existing = FileConfig::load(&state.config_path);
let updated = form.apply_to(existing);
if let Err(e) = updated.save(&state.config_path) {
warn!(path = ?state.config_path, "save settings failed: {}", e);
}
Redirect::to("/console/voicemail/settings").into_response()
}
pub async fn api_prompt_audio(
State(state): State<VoicemailWebState>,
Path(name): Path<String>,
) -> Response {
if !VALID_PROMPT_NAMES.contains(&name.as_str()) {
return (StatusCode::BAD_REQUEST, "unknown prompt name").into_response();
}
let cfg = FileConfig::load(&state.config_path);
let path = match prompt_read_path(&cfg, &name) {
Some(p) => p,
None => return (StatusCode::NOT_FOUND, "prompt file not found").into_response(),
};
match tokio::fs::read(&path).await {
Ok(bytes) => (
[
(header::CONTENT_TYPE, "audio/wav"),
(header::CACHE_CONTROL, "no-cache"),
],
bytes,
)
.into_response(),
Err(e) => {
warn!(path = ?path, "read prompt failed: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "read failed").into_response()
}
}
}
pub async fn api_prompt_upload(
State(state): State<VoicemailWebState>,
Path(name): Path<String>,
mut multipart: Multipart,
) -> Response {
if !VALID_PROMPT_NAMES.contains(&name.as_str()) {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "unknown prompt name"})),
)
.into_response();
}
let mut audio_bytes: Option<Vec<u8>> = None;
while let Ok(Some(field)) = multipart.next_field().await {
if field.name() == Some("file") {
match field.bytes().await {
Ok(b) if !b.is_empty() => audio_bytes = Some(b.to_vec()),
_ => {}
}
}
}
let data = match audio_bytes {
Some(b) => b,
None => {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "no `file` field in multipart"})),
)
.into_response();
}
};
let cfg = FileConfig::load(&state.config_path);
let dest = prompt_write_path(&cfg, &name);
match write_prompt_file(&dest, &data).await {
Ok(()) => Json(json!({
"ok": true,
"path": dest.to_string_lossy()
}))
.into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e}))).into_response(),
}
}
#[derive(Deserialize)]
pub struct PromptUrlForm {
pub url: String,
}
pub async fn api_prompt_from_url(
State(state): State<VoicemailWebState>,
Path(name): Path<String>,
axum::Form(form): axum::Form<PromptUrlForm>,
) -> Response {
if !VALID_PROMPT_NAMES.contains(&name.as_str()) {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "unknown prompt name"})),
)
.into_response();
}
let bytes = match reqwest::get(&form.url).await {
Ok(resp) if resp.status().is_success() => match resp.bytes().await {
Ok(b) => b,
Err(e) => {
warn!(url = %form.url, "read prompt URL body failed: {}", e);
return (
StatusCode::BAD_GATEWAY,
Json(json!({"error": "failed to read response body"})),
)
.into_response();
}
},
Ok(resp) => {
return (
StatusCode::BAD_GATEWAY,
Json(json!({"error": format!("remote returned {}", resp.status())})),
)
.into_response();
}
Err(e) => {
warn!(url = %form.url, "fetch prompt URL failed: {}", e);
return (
StatusCode::BAD_GATEWAY,
Json(json!({"error": "fetch failed"})),
)
.into_response();
}
};
let cfg = FileConfig::load(&state.config_path);
let dest = prompt_write_path(&cfg, &name);
match write_prompt_file(&dest, &bytes).await {
Ok(()) => Json(json!({
"ok": true,
"path": dest.to_string_lossy()
}))
.into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e}))).into_response(),
}
}
pub async fn api_greeting_audio(
State(state): State<VoicemailWebState>,
Path(extension): Path<String>,
) -> Response {
let mailbox = match state.mailbox_svc.find_by_extension(&extension).await {
Ok(Some(mb)) => mb,
Ok(None) => {
return (StatusCode::NOT_FOUND, "mailbox not found").into_response();
}
Err(e) => {
return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response();
}
};
let greeting_path = match mailbox.greeting_path {
Some(p) => p,
None => {
return (StatusCode::NOT_FOUND, "no greeting set").into_response();
}
};
if let Some(local) = state.storage.local_path(&greeting_path) {
if local.exists() {
match tokio::fs::read(&local).await {
Ok(bytes) => {
let filename = local
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("greeting.wav");
return (
[
(header::CONTENT_TYPE, "audio/wav"),
(
header::CONTENT_DISPOSITION,
&format!("inline; filename=\"{}\"", filename),
),
],
bytes,
)
.into_response();
}
Err(e) => {
warn!(path = ?local, "read greeting failed: {}", e);
return (StatusCode::INTERNAL_SERVER_ERROR, "read failed").into_response();
}
}
}
}
match state.storage.get_greeting(&greeting_path).await {
Ok(bytes) => {
let filename = std::path::Path::new(&greeting_path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("greeting.wav")
.to_string();
(
[
(header::CONTENT_TYPE, "audio/wav"),
(
header::CONTENT_DISPOSITION,
&format!("inline; filename=\"{}\"", filename),
),
],
bytes.to_vec(),
)
.into_response()
}
Err(e) => {
warn!(greeting = %greeting_path, "greeting fetch failed: {}", e);
(StatusCode::NOT_FOUND, "greeting not found").into_response()
}
}
}
pub async fn form_upload_greeting(
State(state): State<VoicemailWebState>,
Path(extension): Path<String>,
mut multipart: Multipart,
) -> Response {
let mailbox = match state.mailbox_svc.find_by_extension(&extension).await {
Ok(Some(mb)) => mb,
Ok(None) => {
return Redirect::to(&format!(
"/console/voicemail/{}",
urlencoding::encode(&extension)
))
.into_response();
}
Err(_) => {
return Redirect::to("/console/voicemail").into_response();
}
};
if let Some(old_path) = &mailbox.greeting_path {
if let Err(e) = state.storage.delete_greeting(old_path).await {
warn!(path = %old_path, "delete old greeting failed: {}", e);
}
}
let mut audio_bytes: Option<Vec<u8>> = None;
let mut file_ext = "wav".to_string();
while let Ok(Some(field)) = multipart.next_field().await {
if field.name() == Some("greeting") {
if let Some(ct) = field.content_type() {
file_ext = match ct {
"audio/mpeg" | "audio/mp3" => "mp3".to_string(),
"audio/ogg" => "ogg".to_string(),
_ => "wav".to_string(),
};
}
match field.bytes().await {
Ok(b) if !b.is_empty() => audio_bytes = Some(b.to_vec()),
_ => {}
}
}
}
let data = match audio_bytes {
Some(b) => b,
None => {
return Redirect::to(&format!(
"/console/voicemail/{}",
urlencoding::encode(&extension)
))
.into_response();
}
};
match state
.storage
.upload_greeting(&data, &extension, &file_ext)
.await
{
Ok(key) => {
if let Err(e) = state
.mailbox_svc
.set_greeting_path(&extension, Some(&key))
.await
{
warn!(extension = %extension, "set_greeting_path failed: {}", e);
}
}
Err(e) => {
warn!(extension = %extension, "upload_greeting failed: {}", e);
}
}
let location = format!("/console/voicemail/{}", urlencoding::encode(&extension));
Redirect::to(&location).into_response()
}
pub async fn form_delete_greeting(
State(state): State<VoicemailWebState>,
Path(extension): Path<String>,
) -> Response {
if let Ok(Some(mailbox)) = state.mailbox_svc.find_by_extension(&extension).await {
if let Some(path) = &mailbox.greeting_path {
if let Err(e) = state.storage.delete_greeting(path).await {
warn!(path = %path, "delete greeting failed: {}", e);
}
}
if let Err(e) = state.mailbox_svc.set_greeting_path(&extension, None).await {
warn!(extension = %extension, "clear greeting_path failed: {}", e);
}
}
let location = format!("/console/voicemail/{}", urlencoding::encode(&extension));
Redirect::to(&location).into_response()
}