Use mailing lists instead of aliases for stalwart

This commit is contained in:
traxys 2023-10-08 13:06:12 +02:00
parent 56b71230e9
commit f09444d832
6 changed files with 78 additions and 52 deletions

View file

@ -0,0 +1,4 @@
-- Add migration script here
DROP TABLE alias_recipient;
DELETE FROM emails WHERE alias = true;
ALTER TABLE emails DROP COLUMN alias;

View file

@ -0,0 +1,7 @@
CREATE TYPE mail_type AS ENUM ('primary', 'alias', 'list');
ALTER TABLE emails
ADD COLUMN type mail_type DEFAULT 'primary';
ALTER TABLE emails
ALTER COLUMN type DROP DEFAULT;

View file

@ -0,0 +1,12 @@
-- Add migration script here
ALTER TABLE emails
ADD CONSTRAINT email_type_unique UNIQUE (mail, type);
CREATE TABLE list_recipients (
list TEXT NOT NULL,
type mail_type NOT NULL CHECK (type = 'list'),
recipient TEXT NOT NULL,
PRIMARY KEY (list, recipient),
FOREIGN KEY (list, type) REFERENCES emails (mail, type)
);

View file

@ -0,0 +1,3 @@
-- Add migration script here
ALTER TABLE emails
ALTER COLUMN type SET NOT NULL;

View file

@ -514,8 +514,8 @@ struct HomeQuery {
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
struct AliasRecipient { struct ListRecipient {
alias: String, list: String,
recipient: String, recipient: String,
} }
@ -535,7 +535,7 @@ async fn home(
r#" r#"
SELECT mail SELECT mail
FROM emails FROM emails
WHERE id = $1 AND alias = false WHERE id = $1 AND type = 'primary'
ORDER BY lower(substring(mail from position('@' in mail)+1 )),lower(mail) ORDER BY lower(substring(mail from position('@' in mail)+1 )),lower(mail)
"#, "#,
user user
@ -543,12 +543,12 @@ async fn home(
.fetch_all(&state.db) .fetch_all(&state.db)
.await?; .await?;
let aliases = sqlx::query_as!( let lists = sqlx::query_as!(
Mail, Mail,
r#" r#"
SELECT mail SELECT mail
FROM emails FROM emails
WHERE id = $1 AND alias = true WHERE id = $1 AND type = 'list'
ORDER BY lower(substring(mail from position('@' in mail)+1 )),lower(mail) ORDER BY lower(substring(mail from position('@' in mail)+1 )),lower(mail)
"#, "#,
user user
@ -556,23 +556,23 @@ async fn home(
.fetch_all(&state.db) .fetch_all(&state.db)
.await?; .await?;
let mut alias_stream = sqlx::query_as!( let mut recipient_stream = sqlx::query_as!(
AliasRecipient, ListRecipient,
r#" r#"
SELECT alias_recipient.mail as alias,recipient SELECT list_recipients.list as list,recipient
FROM emails,alias_recipient FROM emails,list_recipients
WHERE id = $1 AND alias = true AND emails.mail = alias_recipient.mail WHERE id = $1 AND emails.type = 'list' AND emails.mail = list_recipients.list
"#, "#,
user user
) )
.fetch(&state.db); .fetch(&state.db);
let mut aliases: HashMap<_, _> = aliases.into_iter().map(|a| (a.mail, Vec::new())).collect(); let mut lists: HashMap<_, _> = lists.into_iter().map(|a| (a.mail, Vec::new())).collect();
while let Some(alias) = alias_stream.try_next().await? { while let Some(list) = recipient_stream.try_next().await? {
aliases.get_mut(&alias.alias).unwrap().push(alias.recipient); lists.get_mut(&list.list).unwrap().push(list.recipient);
} }
let aliases: Vec<_> = aliases let aliases: Vec<_> = lists
.into_iter() .into_iter()
.map(|(mail, recipients)| Alias { mail, recipients }) .map(|(mail, recipients)| Alias { mail, recipients })
.sorted_by(|a, b| a.mail.cmp(&b.mail)) .sorted_by(|a, b| a.mail.cmp(&b.mail))
@ -581,7 +581,7 @@ async fn home(
let mut context = tera::Context::new(); let mut context = tera::Context::new();
context.insert("mails", &mails); context.insert("mails", &mails);
context.insert("mail_domain", &state.mail_domain); context.insert("mail_domain", &state.mail_domain);
context.insert("aliases", &aliases); context.insert("lists", &aliases);
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());
@ -615,14 +615,14 @@ async fn delete_mail(
} }
#[tracing::instrument(skip(state))] #[tracing::instrument(skip(state))]
async fn delete_alias( async fn delete_list(
state: State<Arc<AppState>>, state: State<Arc<AppState>>,
User(user): User, User(user): User,
Form(delete): Form<Mail>, Form(delete): Form<Mail>,
) -> Result<Redirect, Error> { ) -> Result<Redirect, Error> {
let mut tx = state.db.begin().await?; let mut tx = state.db.begin().await?;
sqlx::query!("DELETE FROM alias_recipient WHERE mail = $1", delete.mail) sqlx::query!("DELETE FROM list_recipients WHERE list = $1", delete.mail)
.execute(&mut *tx) .execute(&mut *tx)
.await?; .await?;
@ -696,7 +696,7 @@ async fn add_mail(
Ok(UserError::MailAlreadyExists.into()) Ok(UserError::MailAlreadyExists.into())
} else { } else {
sqlx::query!( sqlx::query!(
"INSERT INTO emails (id, mail) VALUES ($1, $2) ON CONFLICT DO NOTHING", "INSERT INTO emails (id, mail, type) VALUES ($1, $2, 'primary') ON CONFLICT DO NOTHING",
user, user,
add.mail add.mail
) )
@ -707,7 +707,7 @@ async fn add_mail(
} }
} }
async fn add_alias( async fn add_list(
state: State<Arc<AppState>>, state: State<Arc<AppState>>,
User(user): User, User(user): User,
Form(add): Form<Mail>, Form(add): Form<Mail>,
@ -726,7 +726,7 @@ async fn add_alias(
Ok(UserError::MailAlreadyExists.into()) Ok(UserError::MailAlreadyExists.into())
} else { } else {
sqlx::query!( sqlx::query!(
"INSERT INTO emails (id, mail, alias) VALUES ($1, $2, true) ON CONFLICT DO NOTHING", "INSERT INTO emails (id, mail, type) VALUES ($1, $2, 'list') ON CONFLICT DO NOTHING",
user, user,
add.mail add.mail
) )
@ -741,12 +741,12 @@ async fn add_alias(
async fn add_recipient( async fn add_recipient(
state: State<Arc<AppState>>, state: State<Arc<AppState>>,
User(user): User, User(user): User,
Form(add): Form<AliasRecipient>, Form(add): Form<ListRecipient>,
) -> Result<Redirect, Error> { ) -> Result<Redirect, Error> {
let can_use_alias = sqlx::query!( let can_use_list = sqlx::query!(
"SELECT COUNT(*) FROM emails WHERE id = $1 AND mail = $2", "SELECT COUNT(*) FROM emails WHERE id = $1 AND mail = $2",
user, user,
add.alias add.list
) )
.fetch_one(&state.db) .fetch_one(&state.db)
.await? .await?
@ -754,14 +754,14 @@ async fn add_recipient(
.expect("count should not be null") .expect("count should not be null")
> 0; > 0;
if !can_use_alias { if !can_use_list {
tracing::error!("User is not authorized to use this alias"); tracing::error!("User is not authorized to use this alias");
return Err(Error::InternalError); return Err(Error::InternalError);
} }
sqlx::query!( sqlx::query!(
"INSERT INTO alias_recipient (mail, recipient) VALUES ($1, $2) ON CONFLICT DO NOTHING", "INSERT INTO list_recipients (list, recipient, type) VALUES ($1, $2, 'list') ON CONFLICT DO NOTHING",
add.alias, add.list,
add.recipient add.recipient
) )
.execute(&state.db) .execute(&state.db)
@ -774,12 +774,12 @@ async fn add_recipient(
async fn delete_recipient( async fn delete_recipient(
state: State<Arc<AppState>>, state: State<Arc<AppState>>,
User(user): User, User(user): User,
Form(delete): Form<AliasRecipient>, Form(delete): Form<ListRecipient>,
) -> Result<Redirect, Error> { ) -> Result<Redirect, Error> {
let can_use_alias = sqlx::query!( let can_use_alias = sqlx::query!(
"SELECT COUNT(*) FROM emails WHERE id = $1 AND mail = $2", "SELECT COUNT(*) FROM emails WHERE id = $1 AND mail = $2",
user, user,
delete.alias delete.list
) )
.fetch_one(&state.db) .fetch_one(&state.db)
.await? .await?
@ -793,8 +793,8 @@ async fn delete_recipient(
} }
let rows_affected = sqlx::query!( let rows_affected = sqlx::query!(
"DELETE FROM alias_recipient WHERE mail = $1 AND recipient = $2", "DELETE FROM list_recipients WHERE list = $1 AND recipient = $2",
delete.alias, delete.list,
delete.recipient, delete.recipient,
) )
.execute(&state.db) .execute(&state.db)
@ -870,10 +870,10 @@ async fn main() -> color_eyre::Result<()> {
.route("/", get(home)) .route("/", get(home))
.route("/mail/delete", post(delete_mail)) .route("/mail/delete", post(delete_mail))
.route("/mail/add", post(add_mail)) .route("/mail/add", post(add_mail))
.route("/alias/add", post(add_alias)) .route("/list/add", post(add_list))
.route("/alias/recipient/add", post(add_recipient)) .route("/list/recipient/add", post(add_recipient))
.route("/alias/recipient/delete", post(delete_recipient)) .route("/list/recipient/delete", post(delete_recipient))
.route("/alias/delete", post(delete_alias)) .route("/list/delete", post(delete_list))
.route("/password", post(set_password)) .route("/password", post(set_password))
.fallback(page_not_found) .fallback(page_not_found)
.with_state(Arc::new(AppState { .with_state(Arc::new(AppState {

View file

@ -135,27 +135,27 @@
payload=[], payload=[],
prefill=true) prefill=true)
}} }}
<h2 class="title is-2 mt-2">Aliases</h2> <h2 class="title is-2 mt-2">Lists</h2>
<ul class="list-group"> <ul class="list-group">
{% for alias in aliases %} {% for list in lists %}
<li class="list-group-item d-flex flex-column"> <li class="list-group-item d-flex flex-column">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
{{ alias.mail }} {{ list.mail }}
{{ self::delete_modal(modal_id="aliasDelete" ~ loop.index, {{ self::delete_modal(modal_id="listDelete" ~ loop.index,
confirm_text="Delete alias '" ~ alias.mail ~ "'", confirm_text="Delete list '" ~ list.mail ~ "'",
action="/alias/delete", action="/list/delete",
payload=["mail", alias.mail]) payload=["name", list.mail])
}} }}
</div> </div>
<ul class="list-group mt-1"> <ul class="list-group mt-1">
{% set alias_idx = loop.index %} {% set list_idx = loop.index %}
{% for recpt in alias.recipients %} {% for recpt in list.recipients %}
<li class="list-group-item d-flex justify-content-between align-items-center"> <li class="list-group-item d-flex justify-content-between align-items-center">
{{ recpt }} {{ recpt }}
{{ self::delete_modal(modal_id="aliasRctDelete" ~ alias_idx ~ loop.index, {{ self::delete_modal(modal_id="listRctDelete" ~ list_idx ~ loop.index,
confirm_text="Delete recipient '" ~ recpt ~ "' for '" ~ alias.mail ~ "'", confirm_text="Delete recipient '" ~ recpt ~ "' for '" ~ list.mail ~ "'",
action="/alias/recipient/delete", action="/list/recipient/delete",
payload=["alias", alias.mail, "recipient", recpt]) payload=["list", list.mail, "recipient", recpt])
}} }}
</li> </li>
{% endfor %} {% endfor %}
@ -164,18 +164,18 @@
add_button="Add Recipient", add_button="Add Recipient",
button_classes="mt-2 w-25", button_classes="mt-2 w-25",
add_text="Add a new recipient", add_text="Add a new recipient",
action="/alias/recipient/add", action="/list/recipient/add",
input_name="recipient", input_name="recipient",
payload=["alias", alias.mail]) payload=["list", list.mail])
}} }}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
{{ self::add_modal(modal_id="addAlias", {{ self::add_modal(modal_id="addAlias",
add_button="Add new alias", add_button="Add new list",
button_classes="mt-2", button_classes="mt-2",
add_text="Add new alias", add_text="Add new list",
action="/alias/add", action="/list/add",
input_name="mail", input_name="mail",
payload=[], payload=[],
prefill=true) prefill=true)