Implement mail aliases
This commit is contained in:
parent
f09444d832
commit
1c2b4c408e
3 changed files with 152 additions and 18 deletions
7
migrations/20231008140942_alias.sql
Normal file
7
migrations/20231008140942_alias.sql
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Add migration script here
|
||||||
|
CREATE TABLE alias (
|
||||||
|
alias TEXT NOT NULL PRIMARY KEY,
|
||||||
|
type mail_type NOT NULL CHECK (type = 'alias'),
|
||||||
|
dest TEXT NOT NULL REFERENCES emails(mail),
|
||||||
|
FOREIGN KEY (alias, type) REFERENCES emails (mail, type)
|
||||||
|
);
|
||||||
124
src/main.rs
124
src/main.rs
|
|
@ -1,4 +1,5 @@
|
||||||
use std::{
|
use std::{
|
||||||
|
cmp::Ordering,
|
||||||
collections::{HashMap, VecDeque},
|
collections::{HashMap, VecDeque},
|
||||||
fmt::Display,
|
fmt::Display,
|
||||||
net::SocketAddr,
|
net::SocketAddr,
|
||||||
|
|
@ -520,11 +521,28 @@ struct ListRecipient {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct Alias {
|
struct List {
|
||||||
mail: String,
|
mail: String,
|
||||||
recipients: Vec<String>,
|
recipients: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct AliasedMail {
|
||||||
|
primary: String,
|
||||||
|
aliases: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compare_mails(a: &str, b: &str) -> Ordering {
|
||||||
|
let a = a.to_lowercase();
|
||||||
|
let b = b.to_lowercase();
|
||||||
|
match (a.split_once('@'), b.split_once('@')) {
|
||||||
|
(Some((user_a, domain_a)), Some((user_b, domain_b))) => {
|
||||||
|
domain_a.cmp(domain_b).then(user_a.cmp(user_b))
|
||||||
|
}
|
||||||
|
_ => a.cmp(&b),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn home(
|
async fn home(
|
||||||
state: State<Arc<AppState>>,
|
state: State<Arc<AppState>>,
|
||||||
User(user): User,
|
User(user): User,
|
||||||
|
|
@ -543,13 +561,36 @@ async fn home(
|
||||||
.fetch_all(&state.db)
|
.fetch_all(&state.db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let mut alias_stream = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
SELECT alias,dest FROM alias WHERE dest IN (SELECT mail FROM emails WHERE id = $1)
|
||||||
|
ORDER BY lower(substring(alias from position('@' in alias)+1 )),lower(alias)
|
||||||
|
"#,
|
||||||
|
user
|
||||||
|
)
|
||||||
|
.fetch(&state.db);
|
||||||
|
|
||||||
|
let mut aliased_mails: HashMap<_, Vec<String>> =
|
||||||
|
mails.into_iter().map(|a| (a.mail, Vec::new())).collect();
|
||||||
|
while let Some(alias) = alias_stream.try_next().await? {
|
||||||
|
aliased_mails
|
||||||
|
.get_mut(&alias.dest)
|
||||||
|
.unwrap()
|
||||||
|
.push(alias.alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
let aliased_mails: Vec<_> = aliased_mails
|
||||||
|
.into_iter()
|
||||||
|
.map(|(primary, aliases)| AliasedMail { primary, aliases })
|
||||||
|
.sorted_by(|a, b| compare_mails(&a.primary, &b.primary))
|
||||||
|
.collect();
|
||||||
|
|
||||||
let lists = sqlx::query_as!(
|
let lists = sqlx::query_as!(
|
||||||
Mail,
|
Mail,
|
||||||
r#"
|
r#"
|
||||||
SELECT mail
|
SELECT mail
|
||||||
FROM emails
|
FROM emails
|
||||||
WHERE id = $1 AND type = 'list'
|
WHERE id = $1 AND type = 'list'
|
||||||
ORDER BY lower(substring(mail from position('@' in mail)+1 )),lower(mail)
|
|
||||||
"#,
|
"#,
|
||||||
user
|
user
|
||||||
)
|
)
|
||||||
|
|
@ -572,16 +613,16 @@ async fn home(
|
||||||
lists.get_mut(&list.list).unwrap().push(list.recipient);
|
lists.get_mut(&list.list).unwrap().push(list.recipient);
|
||||||
}
|
}
|
||||||
|
|
||||||
let aliases: Vec<_> = lists
|
let lists: Vec<_> = lists
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(mail, recipients)| Alias { mail, recipients })
|
.map(|(mail, recipients)| List { mail, recipients })
|
||||||
.sorted_by(|a, b| a.mail.cmp(&b.mail))
|
.sorted_by(|a, b| compare_mails(&a.mail, &b.mail))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let mut context = tera::Context::new();
|
let mut context = tera::Context::new();
|
||||||
context.insert("mails", &mails);
|
context.insert("mails", &aliased_mails);
|
||||||
context.insert("mail_domain", &state.mail_domain);
|
context.insert("mail_domain", &state.mail_domain);
|
||||||
context.insert("lists", &aliases);
|
context.insert("lists", &lists);
|
||||||
if let Some(err) = query.user_error {
|
if let Some(err) = query.user_error {
|
||||||
tracing::info!("User error: {err:?}");
|
tracing::info!("User error: {err:?}");
|
||||||
context.insert("user_error", &err.to_string());
|
context.insert("user_error", &err.to_string());
|
||||||
|
|
@ -649,17 +690,17 @@ async fn delete_list(
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
enum UserError {
|
enum UserError {
|
||||||
MailAlreadyExists,
|
MailAlreadyExists,
|
||||||
NameAlreadyExists(String),
|
UnauthorizedAliasDest(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for UserError {
|
impl Display for UserError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
UserError::MailAlreadyExists => {
|
UserError::MailAlreadyExists => {
|
||||||
write!(f, "email address is already used by another user")
|
write!(f, "email address is already in use")
|
||||||
}
|
}
|
||||||
UserError::NameAlreadyExists(n) => {
|
UserError::UnauthorizedAliasDest(dest) => {
|
||||||
write!(f, "account name '{n}' is already used by another user")
|
write!(f, "not authorized to use '{dest}' as an alias destination")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -809,6 +850,66 @@ async fn delete_recipient(
|
||||||
Ok(Redirect::to("/"))
|
Ok(Redirect::to("/"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct NewAlias {
|
||||||
|
mail: String,
|
||||||
|
alias: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(state))]
|
||||||
|
async fn alias_add(
|
||||||
|
state: State<Arc<AppState>>,
|
||||||
|
User(user): User,
|
||||||
|
Form(alias): Form<NewAlias>,
|
||||||
|
) -> Result<Redirect, Error> {
|
||||||
|
let mut tx = state.db.begin().await?;
|
||||||
|
|
||||||
|
let can_use_mail = sqlx::query!(
|
||||||
|
"SELECT COUNT(*) FROM emails WHERE id = $1 AND mail = $2",
|
||||||
|
user,
|
||||||
|
alias.mail
|
||||||
|
)
|
||||||
|
.fetch_one(&mut *tx)
|
||||||
|
.await?
|
||||||
|
.count
|
||||||
|
.expect("count should not be null")
|
||||||
|
> 0;
|
||||||
|
|
||||||
|
if !can_use_mail {
|
||||||
|
return Ok(UserError::UnauthorizedAliasDest(alias.mail).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let has_mail = sqlx::query!("SELECT COUNT(*) FROM emails WHERE mail = $1", alias.alias)
|
||||||
|
.fetch_one(&mut *tx)
|
||||||
|
.await?
|
||||||
|
.count
|
||||||
|
.expect("count should not be null");
|
||||||
|
|
||||||
|
if has_mail != 0 {
|
||||||
|
return Ok(UserError::MailAlreadyExists.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO emails (id, mail, type) VALUES ($1, $2, 'alias') ON CONFLICT DO NOTHING",
|
||||||
|
user,
|
||||||
|
alias.alias
|
||||||
|
)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO alias (alias, type, dest) VALUES ($1, 'alias', $2) ON CONFLICT DO NOTHING",
|
||||||
|
alias.alias,
|
||||||
|
alias.mail
|
||||||
|
)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
|
Ok(Redirect::to("/"))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
struct Password {
|
struct Password {
|
||||||
password: SecretString,
|
password: SecretString,
|
||||||
|
|
@ -875,6 +976,7 @@ async fn main() -> color_eyre::Result<()> {
|
||||||
.route("/list/recipient/delete", post(delete_recipient))
|
.route("/list/recipient/delete", post(delete_recipient))
|
||||||
.route("/list/delete", post(delete_list))
|
.route("/list/delete", post(delete_list))
|
||||||
.route("/password", post(set_password))
|
.route("/password", post(set_password))
|
||||||
|
.route("/alias/add", post(alias_add))
|
||||||
.fallback(page_not_found)
|
.fallback(page_not_found)
|
||||||
.with_state(Arc::new(AppState {
|
.with_state(Arc::new(AppState {
|
||||||
db,
|
db,
|
||||||
|
|
|
||||||
|
|
@ -116,12 +116,36 @@
|
||||||
<h2 class="title is-2 mt-2">Mails</h2>
|
<h2 class="title is-2 mt-2">Mails</h2>
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
{% for mail in mails %}
|
{% for mail in mails %}
|
||||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
<li class="list-group-item d-flex flex-column">
|
||||||
{{ mail.mail }}
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
{{ mail.primary }}
|
||||||
{{ self::delete_modal(modal_id="mailDelete" ~ loop.index,
|
{{ self::delete_modal(modal_id="mailDelete" ~ loop.index,
|
||||||
confirm_text="Delete mail '" ~ mail.mail ~ "'",
|
confirm_text="Delete mail '" ~ mail.primary ~ "'",
|
||||||
action="/mail/delete",
|
action="/mail/delete",
|
||||||
payload=["mail", mail.mail])
|
payload=["mail", mail.primary])
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<h3>Aliases</h3>
|
||||||
|
<ul class="list-group mt-1">
|
||||||
|
{% set alias_idx = loop.index %}
|
||||||
|
{% for alias in mail.aliases %}
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
{{ alias }}
|
||||||
|
{{ self::delete_modal(modal_id="aliasDelete" ~ alias_idx ~ loop.index,
|
||||||
|
confirm_text="Delete alias '" ~ alias ~ "' (for '" ~ mail.primary ~ "')",
|
||||||
|
action="/alias/delete",
|
||||||
|
payload=["alias", alias])
|
||||||
|
}}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{{ self::add_modal(modal_id="addAlias" ~ loop.index,
|
||||||
|
add_button="Add Alias",
|
||||||
|
button_classes="mt-2 w-25",
|
||||||
|
add_text="Add a alias",
|
||||||
|
action="/alias/add",
|
||||||
|
input_name="alias",
|
||||||
|
payload=["mail", mail.primary])
|
||||||
}}
|
}}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
@ -147,6 +171,7 @@
|
||||||
payload=["name", list.mail])
|
payload=["name", list.mail])
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
|
<h3>Recipients</h3>
|
||||||
<ul class="list-group mt-1">
|
<ul class="list-group mt-1">
|
||||||
{% set list_idx = loop.index %}
|
{% set list_idx = loop.index %}
|
||||||
{% for recpt in list.recipients %}
|
{% for recpt in list.recipients %}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue