Use mailing lists instead of aliases for stalwart
This commit is contained in:
parent
56b71230e9
commit
f09444d832
6 changed files with 78 additions and 52 deletions
4
migrations/20231008093906_alias_table.sql
Normal file
4
migrations/20231008093906_alias_table.sql
Normal 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;
|
||||||
7
migrations/20231008094925_lists.sql
Normal file
7
migrations/20231008094925_lists.sql
Normal 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;
|
||||||
12
migrations/20231008100114_lists_recipients.sql
Normal file
12
migrations/20231008100114_lists_recipients.sql
Normal 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)
|
||||||
|
);
|
||||||
3
migrations/20231008101016_type_not_null.sql
Normal file
3
migrations/20231008101016_type_not_null.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
-- Add migration script here
|
||||||
|
ALTER TABLE emails
|
||||||
|
ALTER COLUMN type SET NOT NULL;
|
||||||
68
src/main.rs
68
src/main.rs
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue