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::{
|
||||
cmp::Ordering,
|
||||
collections::{HashMap, VecDeque},
|
||||
fmt::Display,
|
||||
net::SocketAddr,
|
||||
|
|
@ -520,11 +521,28 @@ struct ListRecipient {
|
|||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Alias {
|
||||
struct List {
|
||||
mail: 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(
|
||||
state: State<Arc<AppState>>,
|
||||
User(user): User,
|
||||
|
|
@ -543,13 +561,36 @@ async fn home(
|
|||
.fetch_all(&state.db)
|
||||
.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!(
|
||||
Mail,
|
||||
r#"
|
||||
SELECT mail
|
||||
FROM emails
|
||||
WHERE id = $1 AND type = 'list'
|
||||
ORDER BY lower(substring(mail from position('@' in mail)+1 )),lower(mail)
|
||||
"#,
|
||||
user
|
||||
)
|
||||
|
|
@ -572,16 +613,16 @@ async fn home(
|
|||
lists.get_mut(&list.list).unwrap().push(list.recipient);
|
||||
}
|
||||
|
||||
let aliases: Vec<_> = lists
|
||||
let lists: Vec<_> = lists
|
||||
.into_iter()
|
||||
.map(|(mail, recipients)| Alias { mail, recipients })
|
||||
.sorted_by(|a, b| a.mail.cmp(&b.mail))
|
||||
.map(|(mail, recipients)| List { mail, recipients })
|
||||
.sorted_by(|a, b| compare_mails(&a.mail, &b.mail))
|
||||
.collect();
|
||||
|
||||
let mut context = tera::Context::new();
|
||||
context.insert("mails", &mails);
|
||||
context.insert("mails", &aliased_mails);
|
||||
context.insert("mail_domain", &state.mail_domain);
|
||||
context.insert("lists", &aliases);
|
||||
context.insert("lists", &lists);
|
||||
if let Some(err) = query.user_error {
|
||||
tracing::info!("User error: {err:?}");
|
||||
context.insert("user_error", &err.to_string());
|
||||
|
|
@ -649,17 +690,17 @@ async fn delete_list(
|
|||
#[derive(Serialize, Deserialize, Debug)]
|
||||
enum UserError {
|
||||
MailAlreadyExists,
|
||||
NameAlreadyExists(String),
|
||||
UnauthorizedAliasDest(String),
|
||||
}
|
||||
|
||||
impl Display for UserError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
UserError::MailAlreadyExists => {
|
||||
write!(f, "email address is already used by another user")
|
||||
write!(f, "email address is already in use")
|
||||
}
|
||||
UserError::NameAlreadyExists(n) => {
|
||||
write!(f, "account name '{n}' is already used by another user")
|
||||
UserError::UnauthorizedAliasDest(dest) => {
|
||||
write!(f, "not authorized to use '{dest}' as an alias destination")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -809,6 +850,66 @@ async fn delete_recipient(
|
|||
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)]
|
||||
struct Password {
|
||||
password: SecretString,
|
||||
|
|
@ -875,6 +976,7 @@ async fn main() -> color_eyre::Result<()> {
|
|||
.route("/list/recipient/delete", post(delete_recipient))
|
||||
.route("/list/delete", post(delete_list))
|
||||
.route("/password", post(set_password))
|
||||
.route("/alias/add", post(alias_add))
|
||||
.fallback(page_not_found)
|
||||
.with_state(Arc::new(AppState {
|
||||
db,
|
||||
|
|
|
|||
|
|
@ -116,13 +116,37 @@
|
|||
<h2 class="title is-2 mt-2">Mails</h2>
|
||||
<ul class="list-group">
|
||||
{% for mail in mails %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
{{ mail.mail }}
|
||||
{{ self::delete_modal(modal_id="mailDelete" ~ loop.index,
|
||||
confirm_text="Delete mail '" ~ mail.mail ~ "'",
|
||||
action="/mail/delete",
|
||||
payload=["mail", mail.mail])
|
||||
}}
|
||||
<li class="list-group-item d-flex flex-column">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
{{ mail.primary }}
|
||||
{{ self::delete_modal(modal_id="mailDelete" ~ loop.index,
|
||||
confirm_text="Delete mail '" ~ mail.primary ~ "'",
|
||||
action="/mail/delete",
|
||||
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>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
|
@ -147,6 +171,7 @@
|
|||
payload=["name", list.mail])
|
||||
}}
|
||||
</div>
|
||||
<h3>Recipients</h3>
|
||||
<ul class="list-group mt-1">
|
||||
{% set list_idx = loop.index %}
|
||||
{% for recpt in list.recipients %}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue