Compare commits

..

10 commits

Author SHA1 Message Date
traxys
e4013ec974 EnsureDBOwnership
Some checks failed
/ tests (push) Has been cancelled
2023-12-05 23:17:14 +01:00
traxys
2225d626b9 Requests as postgresql 2023-12-05 23:16:43 +01:00
traxys
6277631144 Correctly package templates 2023-10-13 20:43:53 +02:00
traxys
6a34258a30 Use single threaded runtime 2023-10-13 20:24:42 +02:00
traxys
46a646b67b Expose port settings in nixos module 2023-10-11 12:38:35 +02:00
traxys
b28e6363c4 Rename to stalwart-accounts 2023-10-10 21:19:02 +02:00
traxys
54feff4cc6 Add sqlx queries for offline build 2023-10-10 20:57:57 +02:00
traxys
b1df749b24 Add a nixos module 2023-10-10 20:57:54 +02:00
traxys
e1d539e893 Rework stalwart queries 2023-10-09 16:36:35 +02:00
traxys
be8a56fb21 Fix issue with deleting list recipients 2023-10-09 13:41:26 +02:00
28 changed files with 615 additions and 60 deletions

View file

@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM list_recipients WHERE list = $1 AND recipient = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "23e11c1e470c49de109c7c88f4e161096ac6b83d1239ce910906a0d2d3d99d02"
}

View file

@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT alias,dest FROM alias WHERE dest IN (SELECT mail FROM emails WHERE id = $1)\n ORDER BY lower(substring(alias from position('@' in alias)+1 )),lower(alias)\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "alias",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "dest",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false
]
},
"hash": "31a4c053d4c984e9d0d5820487525e07f00fada97ec87581bc80a68f3dd11ee2"
}

View file

@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM emails WHERE id != $1 AND mail = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
null
]
},
"hash": "31a67e09c41b8b9cc3b3065590ab3850c915b7da3468ee658d842161098939ae"
}

View file

@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO emails (id, mail, type) VALUES ($1, $2, 'list') ON CONFLICT DO NOTHING",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": []
},
"hash": "3403255f07a53813f0a8163828bc933e6cc0d5436ad9ce5a8bfbba71a2c2903b"
}

View file

@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM list_recipients WHERE list = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "449dea2a3a52b67cf88878708ed00e7ff430ad3e46452940a61bd7d7dc06b204"
}

View file

@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT list_recipients.list as list,recipient\n FROM emails,list_recipients\n WHERE id = $1 AND emails.type = 'list' AND emails.mail = list_recipients.list\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "list",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "recipient",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false
]
},
"hash": "46015003c53421ce280ca79e445796bd51dc72b1437a341ebe50549ac527c0be"
}

View file

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id FROM accounts WHERE sub = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "542eacaa20dcac0437aafce6545630ffe65b7a8196d09ec1491374def326a258"
}

View file

@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM emails WHERE id = $1 AND mail = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": []
},
"hash": "567f765aebef4fbebd557387ab45e43200402bc30f6d982ba7c583a0c976d7f6"
}

View file

@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO list_recipients (list, recipient, type) VALUES ($1, $2, 'list') ON CONFLICT DO NOTHING",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "681be1e58f24e62eb2643204044e6f783d54ce228d9830fb4d46b13c6efffcb8"
}

View file

@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM alias WHERE alias = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "7869c07f3affdabc150961ed9b9c0e1f8202208e32fca3b7c5da979b94aba02a"
}

View file

@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO emails (id, mail, type) VALUES ($1, $2, 'alias') ON CONFLICT DO NOTHING",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": []
},
"hash": "8518449399c2ade2867d714efe1cfc8660b3a1bc735e69d5e92d85d00b81bcd2"
}

View file

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT mail \n FROM emails \n WHERE id = $1 AND type = 'list'\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "mail",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "9e23f135b4ec69fa4d99f797fd54c1fb7db93b3452c77065572a1c303e36cfdc"
}

View file

@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO accounts (id, sub) VALUES ($1, $2)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": []
},
"hash": "a17528ac7ef8aec3fe2de5925256d78201238e168fdea110af801682f03c8159"
}

View file

@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO emails (id, mail, type) VALUES ($1, $2, 'primary') ON CONFLICT DO NOTHING",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": []
},
"hash": "b1a3280e13824a918d2264dd7b5fdb1ce846aad8cb343c734169d9ecf6484ba2"
}

View file

@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE accounts SET password = $1 WHERE id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Uuid"
]
},
"nullable": []
},
"hash": "bbc7f016c7bd8e4ddfc3b393ce55e8d5a72158d502ac9b75da84cb9451d934e2"
}

View file

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT mail \n FROM emails \n WHERE id = $1 AND type = 'primary'\n ORDER BY lower(substring(mail from position('@' in mail)+1 )),lower(mail)\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "mail",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "bd78c0a43f02328f6381ed68f79f812bd7cda553263260860812f646da20b7a1"
}

View file

@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM alias WHERE dest = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "c4d597b5651efad2cc83bf83088a88be3bafeb004a9ffc4d21362c3897bd22d0"
}

View file

@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM emails WHERE id = $1 AND mail = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
null
]
},
"hash": "ce862ed5cde153f4003bb2b28e3bbb9545e0d4f3847afcfb787a38dee1b38ec0"
}

View file

@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO alias (alias, type, dest) VALUES ($1, 'alias', $2) ON CONFLICT DO NOTHING",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "ebe5f8137e9f535eaf7d494b89d4bb442783e5e2135b53cada6d256e976c4857"
}

View file

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM emails WHERE mail = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "f66e03782ba7379cdacb638e0dc2e867408e04290d94df064e874f8d565f9fa7"
}

View file

@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM emails WHERE mail = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "fbbb39ffb5ff4a6b7e26ec9133e297f55bdc832f35b04fb8da0c75cb52e02298"
}

58
Cargo.lock generated
View file

@ -1292,35 +1292,6 @@ version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "mail_accounts"
version = "0.1.0"
dependencies = [
"argon2",
"axum",
"axum-extra",
"base64 0.21.2",
"color-eyre",
"cookie",
"envious",
"futures-util",
"itertools 0.11.0",
"jwt-simple",
"once_cell",
"openidconnect",
"parking_lot",
"secrecy",
"serde",
"serde_urlencoded",
"sqlx",
"tera",
"thiserror",
"tokio",
"tracing",
"tracing-subscriber",
"uuid",
]
[[package]]
name = "matchers"
version = "0.1.0"
@ -2599,6 +2570,35 @@ dependencies = [
"uuid",
]
[[package]]
name = "stalwart-accounts"
version = "0.1.0"
dependencies = [
"argon2",
"axum",
"axum-extra",
"base64 0.21.2",
"color-eyre",
"cookie",
"envious",
"futures-util",
"itertools 0.11.0",
"jwt-simple",
"once_cell",
"openidconnect",
"parking_lot",
"secrecy",
"serde",
"serde_urlencoded",
"sqlx",
"tera",
"thiserror",
"tokio",
"tracing",
"tracing-subscriber",
"uuid",
]
[[package]]
name = "stringprep"
version = "0.1.3"

View file

@ -1,5 +1,5 @@
[package]
name = "mail_accounts"
name = "stalwart-accounts"
version = "0.1.0"
authors = ["traxys <quentin@familleboyer.net>"]
edition = "2021"

View file

@ -29,6 +29,14 @@
RUST_DOC_PATH = "${rust}/share/doc/rust/html/std/index.html";
};
defaultPackage = naersk'.buildPackage ./.;
});
packages.default = naersk'.buildPackage {
src = ./.;
postInstall = ''
mkdir -p $out/share
cp -r templates $out/share
'';
};
})
// {nixosModules.stalwart-accounts = import ./nixos self;};
}

153
nixos/default.nix Normal file
View file

@ -0,0 +1,153 @@
self: {
lib,
pkgs,
config,
options,
...
}:
with lib; {
options.services.stalwart-accounts = {
enable = mkEnableOption "stalwart-accounts, an account manager for stalwart mail server";
package = mkOption {
type = types.package;
inherit (self.packages.${config.nixpkgs.system}) default;
};
logLevel = mkOption {
type = types.str;
default = "info";
};
settings = {
jwtSecret = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
The JWT secret to be used by the application. Should be passed through environmentFile,
with MAIL_ADMIN_JWT_SECRET.
'';
};
oidcEndpoint = mkOption {
type = types.str;
};
oidcClientId = mkOption {
type = types.str;
};
oidcClientSecret = mkOption {
type = types.nullOr types.str;
default = null;
description = "The environment variable MAIL_ADMIN_CLIENT_SECRET should be preferred";
};
domain = mkOption {
type = types.str;
};
scopes = mkOption {
type = types.str;
default = "openid,profile";
};
mailDomain = mkOption {
type = types.str;
};
databaseUrl = mkOption {
type = types.str;
default = "postgres://${config.services.stalwart-accounts.user}/stalwart-accounts?host=/var/run/postgresql";
};
port = mkOption {
type = types.port;
default = 8080;
};
};
user = mkOption {
type = types.str;
default = "stalwart-accounts";
};
environmentFile = mkOption {
type = types.nullOr types.path;
default = null;
};
};
config = let
cfg = config.services.stalwart-accounts;
in
mkIf cfg.enable {
systemd.services.stalwart-accounts = {
description = "stalwart-accounts";
after = ["network.target" "postgresql.service"];
wantedBy = ["multi-user.target"];
serviceConfig = {
Type = "simple";
User = cfg.user;
ExecStart = "${cfg.package}/bin/stalwart-accounts";
EnvironmentFile = optional (cfg.environmentFile != null) cfg.environmentFile;
WorkingDirectory = "${cfg.package}/share";
# Security
NoNewPrivileges = true;
# Sandboxing
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = true;
PrivateUsers = true;
ProtectHostname = true;
ProtectClock = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
RestrictAddressFamilies = ["AF_UNIX AF_INET AF_INET6"];
LockPersonality = true;
MemoryDenyWriteExecute = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
PrivateMounts = true;
};
environment = {
RUST_LOG = cfg.logLevel;
MAIL_ADMIN_DATABASE_URL = cfg.settings.databaseUrl;
MAIL_ADMIN_DOMAIN = cfg.settings.domain;
MAIL_ADMIN_OIDC_ENDPOINT = cfg.settings.oidcEndpoint;
MAIL_ADMIN_CLIENT_ID = cfg.settings.oidcClientId;
MAIL_ADMIN_SCOPES = cfg.settings.scopes;
MAIL_ADMIN_MAIL_DOMAIN = cfg.settings.mailDomain;
MAIL_ADMIN_JWT_SECRET = cfg.settings.jwtSecret;
MAIL_ADMIN_CLIENT_SECRET = cfg.settings.oidcClientSecret;
MAIL_ADMIN_PORT = toString cfg.settings.port;
};
};
services.postgresql =
mkIf (cfg.settings.databaseUrl == options.services.stalwart-accounts.settings.databaseUrl.default)
{
ensureUsers = [
{
name = cfg.user;
ensureDBOwnership = true;
}
];
ensureDatabases = ["stalwart-accounts"];
};
users = mkIf (cfg.user == "stalwart-accounts") {
users.stalwart-accounts = {
description = "stalwart-accounts user";
group = "stalwart-accounts";
isSystemUser = true;
};
groups.stalwart-accounts = {};
};
};
}

View file

@ -5,9 +5,18 @@
100 GB quota
```sql
SELECT name, 'individual' as type, password as secret, '' as description, 107374182400 as quota
FROM accounts
WHERE secret != NULL AND name = ?
SELECT
mail as name,
'individual' as type,
password as secret,
'' as description,
107374182400 as quota
FROM emails
JOIN accounts ON emails.id = accounts.id
WHERE
type = 'primary'
AND mail = $1
AND password IS NOT NULL
```
## members
@ -19,47 +28,51 @@ SELECT NULL as member_of WHERE 1=0
## recipients
```sql
SELECT name
FROM emails JOIN accounts on emails.id = accounts.id
WHERE mail = ?
SELECT dest as name FROM alias WHERE alias = $1
UNION
SELECT mail as name
FROM emails
WHERE id = (SELECT id FROM emails WHERE type = 'list' AND mail = $1)
AND type = 'primary'
UNION
SELECT mail as name FROM emails WHERE mail = $1
```
## emails
```sql
SELECT mail as address
FROM (
SELECT name || '@familleboyer.net' as mail, 0 as type FROM accounts WHERE name = ?
UNION
SELECT mail, (alias)::int + 1 as type
FROM emails JOIN accounts ON accounts.id = emails.id
WHERE name = ?
) as mails
ORDER BY type
SELECT mais as address
FROM emails
WHERE
(id = (SELECT id FROM emails WHERE mail = $1) AND type = 'alias')
OR mail = $1
ORDER BY type, mail
```
## verify
```sql
SELECT mail as address
FROM (
SELECT name || '@familleboyer.net' as mail FROM accounts
UNION
SELECT mail
FROM emails JOIN accounts ON accounts.id = emails.id
WHERE NOT alias
) as mails
WHERE mail LIKE '%' || ? || '%' ORDER BY mail LIMIT 5
SELECT mail as address
FROM emails
WHERE
mail LIKE '%' || $1 || '%'
AND type = 'primary'
ORDER BY address
LIMIT 5
```
## expand
```sql
SELECT NULL as address WHERE 1=0
SELECT recipient as address
FROM list_recipients
WHERE list = $1
ORDER BY address
LIMIT 50
```
## domains
```sql
SELECT 1 WHERE ? = 'familleboyer.net'
SELECT 1 FROM emails WHERE mail LIKE '%@' || $1 LIMIT 1
```

View file

@ -991,7 +991,7 @@ async fn set_password(
Ok(Redirect::to("/"))
}
#[tokio::main]
#[tokio::main(flavor = "current_thread")]
async fn main() -> color_eyre::Result<()> {
color_eyre::install()?;

View file

@ -180,7 +180,7 @@
{{ self::delete_modal(modal_id="listDelete" ~ loop.index,
confirm_text="Delete list '" ~ list.mail ~ "'",
action="/list/delete",
payload=["name", list.mail])
payload=["mail", list.mail])
}}
</div>
<h3>Recipients</h3>