Skip to content
/ idmail Public

An email alias and account management interface for self-hosted mailservers

License

Notifications You must be signed in to change notification settings

oddlama/idmail

Repository files navigation

Installation | Building | API Endpoints | Stalwart configuration | Provisioning

Important

Sent and recv counts require MTA-specific hook setups that are currently not documented!

📧 idmail

Idmail is an email alias and account management interface for self-hosted mailservers, which you can use to hide your true email address from online services. This is NOT an email forwarding service like addy.io! Idmail is a frontend to a sqlite database which contains a table of mailboxes and aliases to be consumed by a mailsever like Stalwart, maddy, Postfix or others. The following features are available:

  • 🧑,🌐 Manage user accounts and domains (as an admin)
  • 📫,🕵️ Manage mailboxes and aliases (per user)
  • 🔄 Generate random aliases
  • 🔑 API endpoint allows integration with password managers (Bitwarden, ...)
  • 📈 Track sent/received statistics per alias
  • 🌌 Per-domain catch-all
  • 🌟 Provisioning support

If you login with a mailbox account, you can change the mailbox password and manage its aliases. Mailbox accounts can use the API to create new aliases with the API token from their settings page. Logging in with a user account (these have no @domain.tld suffix), you can additionally create new mailboxes and manage any domains assigned to you by an admin.

You will have to integrate this with a mailserver that supports querying an sqlite database for mailbox accounts and aliases. We recommend using Stalwart and provide the necessary queries for it, but any other server will work fine if you adjust the queries accordingly.

🚀 Installation

❓ Other distributions

Refer to the second part of the Building section for details on how to build and deploy this application.

❄️ NixOS

Installation under NixOS is straightforward. This repository provides an overlay and NixOS module for simple deployment. First, add this repository flake input and import the module on your NixOS system(s):

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    idmail.url = "github:oddlama/idmail";
    idmail.inputs.nixpkgs.follows = "nixpkgs";
  };

  outputs = { self, nixpkgs, idmail }: {
    # Add the module to your system(s)
    nixosConfigurations.yourhostname = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        ./configuration.nix
        idmail.nixosModules.default
      ];
    };
  };
}

Afterwards, simply enable the service in your configuration and proxy the web interface:

{
  services.idmail.enable = true;

  services.nginx.virtualHosts."alias.example.com" = {
    forceSSL = true;
    locations."/" = {
      proxyPass = "http://localhost:3000";
      proxyWebsockets = true;
    };
  };
}

The database will be available under /var/lib/idmail/idmail.db for consumption by other services, the service listens on 127.0.0.1:3000 by default. The example above uses nginx to reverse proxy the application. If the admin user was not provisioned, it will be recovered on start and a generated password will be printed to the journal.

You can provision anything by using the services.idmail.provision configuration. See Provisioning or view the module source for more information.

{
  services.idmail.provision = {
    enable = true;
    users.admin = {
      admin = true;
      # Generate hash with `echo -n "password" | nix run nixpkgs#libargon2 -- somerandomsalt -id`
      password_hash = "$argon2id$v=19$m=4096,t=3,p=1$YXJnbGluYXJsZ2luMjRvaQ$DXdfVNRSFS1QSvJo7OmXIhAYYtT/D92Ku16DiJwxn8U";
      #password_hash = "%{file:/path/to/secret}%"; # Or read a file at runtime
    };
    domains."example.com" = {
      owner = "admin";
      public = true;
    };
    mailboxes."[email protected]" = {
      password_hash = "$argon2id$v=19$m=4096,t=3,p=1$YXJnbGluYXJsZ2luMjRvaQ$fiD9Bp3KidVI/E mGudu6 h9XmF9TU9Bx4VGX0PniDE";
      owner = "admin";
      #api_token = "VC0lZ6O49nfxU4oK0KbahlSMsqBFiHyYFGUQvzzki6ky5mSM"; # Please don't hardcode api tokens
      api_token = "%{file:/path/to/secret}%";
    };
    aliases."[email protected]" = {
      target = "[email protected]";
      owner = "[email protected]";
      comment = "Used for xyz";
    };
  };
}

For a working in-the-field configuration, feel free to have a look at my personal repository. Specifically stalwart-mail.nix and idmail.nix.

🧰 Building

This project is made to be build via nix. If you have nix installed, the project can be built simply by running:

nix build github:oddlama/idmail

If you want to build it yourself instead, you can do so by executing:

export RUSTFLAGS="--cfg=web_sys_unstable_apis"
export LEPTOS_ENV="PROD"
cargo leptos build --release -vvv

You can then run the server like this:

export LEPTOS_SITE_ADDR="0.0.0.0:3000" # only if you want to change listen address or port
./target/release/idmail

You can host binary in any way you prefer (Docker, systemd services, ...). Afterwards, configure your mailserver to utilize the database for lookups (see Stalwart configuration) and optionally configure your password manager to use one of the provided API Endpoints. If the admin user doesn't exist on start, it will be recovered and a generated password will be printed to stdout.

☁️ API Endpoints

API endpoints are provided which allow you to generate random aliases, compatible with those provided by addy.io (AnonAddy) or SimpleLogin. This means you can use it with a password manager to automatically create aliases for your logins. Aliases will be generated via the faker_rand Username generator, and may produce the following results:

Example of generated email addresses

There are two different API endpoints available:

  • addy.io compatible: Allows you to select a domain. A random avaliable domain is selected by the server if left empty or filled with the special value random.
  • SimpleLogin compatible: Does not allow selecting a domain, so a random available domain is always selected

Both endpoints always generate the same random usernames and ignore any format options in case the original API provides those. The required API token can be generated on the settings page when logging into the Web interface as a mailbox account.

addy.io compatible endpoint

  • Url: https://idmail.example.com/api/v1/aliases
  • Method: POST
  • Token: Via header Authorization: Bearer {token}
  • Success: 201
Example request and response (curl)

Request:

curl -X POST \
    -H "Content-Type: application/json" \
    -H "Accept: application/json" \
    -H "Authorization: Bearer {token}" \
    --data '{"domain":"example.com","description":"An optional comment added to the entry"}'
    localhost:3000/api/v1/aliases

Response:

{
    "data": {
        "active": true,
        "aliasable_id": null,
        "aliasable_type": null,
        "created_at": "2000-01-01 00:00:00",
        "deleted_at": null,
        "description": "An optional comment added to the entry",
        "domain": "example.com",
        "email": "[email protected]",
        "emails_blocked": 0,
        "emails_forwarded": 0,
        "emails_replied": 0,
        "emails_sent": 0,
        "extension": null,
        "from_name": null,
        "id": "00000000-0000-0000-0000-000000000000",
        "last_blocked": null,
        "last_forwarded": "2000-01-01 00:00:00",
        "last_replied": null,
        "last_sent": null,
        "local_part": "00000000-0000-0000-0000-000000000000",
        "recipients": [],
        "updated_at": "2000-01-01 00:00:00",
        "user_id": "00000000-0000-0000-0000-000000000000"
    }
}

SimpleLogin compatible endpoint

  • Url: https://idmail.example.com/api/alias/random/new
  • Method: POST
  • Token: Via header Authorization: {token}
  • Success: 201
Example request and response (curl)

Request:

curl -X POST \
    -H "Content-Type: application/json" \
    -H "Accept: application/json" \
    -H "Authorization: {token}" \
    --data '{"note":"A comment added to the entry"}' \
    localhost:3000/api/alias/random/new

Response:

{
    "alias": "[email protected]"
}

⛔ Reserved addresses

For security purposes, we always reserve a list of special mailbox/alias names which only the domain owner (or admin) may create. The list currently contains:

abuse
admin
hostmaster
info
no-reply
postmaster
root
security
support
webmaster

Warning

Never use an admin account to create mailboxes for other people, as it allows them to use these reserved addresses! (if the mailbox is owner is the domain owner)

⚙️ Stalwart configuration

To integrate the idmail sqlite database with your stalwart server, you will need to provide the necessary SQL queries to stalwart. This is done by configuring and external directory in stalwart. This requires some complex queries to honor the active flag correctly and output everything in the format stalwart expects.

You have to make sure that stalwart has read-write access to the idmail.db database file and the related files for sqlite WAL mode. Here's the configuration that you will need (don't forget to adjust the path to the idmail.db database):

Example stalwart configuration
[storage]
directory = "idmail"

[directory.idmail]
type = "sql"
store = "idmail"

[directory.idmail.columns]
name = "name"
description = "description"
secret = "secret"
email = "email"
# quotas are currently not implemented in idmail
#quota = "quota"
class = "type"

[store.idmail]
# TODO: adjust the path below!
path = "/path/to/idmail.db"
type = "sqlite"

[store.idmail.query]
domains = """\
SELECT domain FROM domains \
    WHERE domain = ?1 \
"""
emails = """\
SELECT address FROM ( \
    SELECT m.address AS address, 1 AS rowOrder \
        FROM mailboxes AS m \
        JOIN domains AS d ON m.domain = d.domain \
        JOIN users AS u ON m.owner = u.username \
        WHERE m.address = ?1 AND m.active = true AND d.active = true AND u.active = true \
    UNION SELECT a.address AS address, 2 AS rowOrder \
        FROM aliases AS a \
        JOIN domains AS d ON a.domain = d.domain \
        JOIN ( \
            SELECT username FROM users \
            WHERE active = true \
            UNION SELECT m.address AS username FROM mailboxes AS m \
            JOIN users AS u ON m.owner = u.username \
            WHERE m.active = true AND u.active = true \
        ) AS u ON a.owner = u.username \
        WHERE a.target = ?1 AND a.active = true AND d.active = true \
    UNION SELECT ('@' || d.domain) AS address, 2 AS rowOrder FROM domains AS d \
        JOIN mailboxes AS m ON d.catch_all = m.address \
        JOIN users AS u ON m.owner = u.username \
        WHERE d.catch_all = ?1 AND d.active = true AND m.active = true AND u.active = true \
    ORDER BY rowOrder, address ASC \
) \
"""
expand = """\
SELECT m.address AS address FROM mailboxes AS m \
    JOIN domains AS d ON m.domain = d.domain \
    JOIN users AS u ON m.owner = u.username \
    WHERE m.address = ?1 AND m.active = true AND d.active = true AND u.active = true \
UNION SELECT a.address AS address FROM aliases AS a \
    JOIN domains AS d ON a.domain = d.domain \
    JOIN ( \
        SELECT username FROM users \
            WHERE active = true \
        UNION SELECT m.address AS username FROM mailboxes AS m \
            JOIN users AS u ON m.owner = u.username \
            WHERE m.active = true AND u.active = true \
    ) AS u ON a.owner = u.username \
    WHERE a.address = ?1 AND a.active = true AND d.active = true \
ORDER BY address \
LIMIT 50 \
"""
members = ""
name = """\
SELECT m.address AS name, 'individual' AS type, m.password_hash AS secret, m.address AS description, 0 AS quota FROM mailboxes AS m \
    JOIN domains AS d ON m.domain = d.domain \
    JOIN users AS u ON m.owner = u.username \
    WHERE m.address = ?1 AND m.active = true AND d.active = true AND u.active = true \
"""
# the ordering allows aliases to override existing mailboxes.
# The web interface never allows you to create such an alias,
# but by provisioning you can create send-only mailboxes that
# have their incoming mail redirected somewhere else
recipients = """\
SELECT name FROM ( \
    SELECT a.target AS name, 1 AS rowOrder AS name FROM aliases AS a \
        JOIN domains AS d ON a.domain = d.domain \
        JOIN ( \
            SELECT username FROM users \
                WHERE active = true \
            UNION SELECT m.address AS username FROM mailboxes AS m \
                JOIN users AS u ON m.owner = u.username \
                WHERE m.active = true AND u.active = true \
        ) AS u ON a.owner = u.username \
        WHERE a.address = ?1 AND a.active = true AND d.active = true \
    UNION SELECT m.address AS name, 2 AS rowOrder AS name FROM mailboxes AS m \
        JOIN domains AS d ON m.domain = d.domain \
        JOIN users AS u ON m.owner = u.username \
        WHERE m.address = ?1 AND m.active = true AND d.active = true AND u.active = true \
    UNION SELECT d.catch_all AS name, 3 AS rowOrder AS name FROM domains AS d \
        JOIN mailboxes AS m ON d.catch_all = m.address \
        JOIN users AS u ON m.owner = u.username \
        WHERE ?1 = ('@' || d.domain) AND d.active = true AND m.active = true AND u.active = true \
    ORDER BY rowOrder, name ASC \
    LIMIT 1 \
) \
"""
verify = """\
SELECT m.address AS address FROM mailboxes AS m \
    JOIN domains AS d ON m.domain = d.domain \
    JOIN users AS u ON m.owner = u.username \
    WHERE m.address LIKE '%' || ?1 || '%' AND m.active = true AND d.active = true AND u.active = true \
UNION SELECT a.address AS address FROM aliases AS a \
    JOIN domains AS d ON a.domain = d.domain \
    JOIN ( \
        SELECT username FROM users \
            WHERE active = true \
        UNION SELECT m.address AS username FROM mailboxes AS m \
            JOIN users AS u ON m.owner = u.username \
            WHERE m.active = true AND u.active = true \
    ) AS u ON a.owner = u.username \
    WHERE a.address LIKE '%' || ?1 || '%' AND a.active = true AND d.active = true \
ORDER BY address \
LIMIT 5 \
"""

🌟 Provisioning

To support declarative deployment you can provision users, domains, mailboxes and aliases out of the box. This works by pointing the environment variable IDMAIL_PROVISION to a toml file containing the desired state. The application automatically tracks provisioned entities and ensures that they will automatically be removed again if you remove them from the state file, without touching entities that were created dynamically by you our your users. This will not cascade deletion, so removing a domain will not touch any dependent aliases or mailboxes. The mailserver queries should always validate combinations by joining the appropriate tables.

The state file has the format shown below:

[users."username"]
# Password hash, should be a argon2id hash.
# Can be generated with: `echo -n "whatever" | argon2 somerandomsalt -id`
# Also accepts "%{file:/path/to/secret}%" to refer to the contents of a file.
password_hash = "$argon2id$v=19$m=4096,t=3,p=1$YXJnbGluYXJsZ2luMjRvaQ$DXdfVNRSFS1QSvJo7OmXIhAYYtT/D92Ku16DiJwxn8U"
# Whether the user should be an admin.
# Optional, default: false
admin = false
# Whether the user should be active
# Optional, default: true
active = true

[domains."example.com"]
# The user which owns this domain. Allows that user to modify
# the catch all address and the domain's active state.
# Creation and deletion of any domain is always restricted to admins only.
owner = "username"
# A catch-all address for this domain.
# Optional. Default: None
catch_all = "[email protected]"
# Whether the domain should be available for use by any registered
# user instead of just the owner. Admins can always use any domain,
# regardless of this setting.
# Optional, default: false
public = false
# Whether the domain should be active
# Optional, default: true
active = true

[mailboxes."me@example.com"]
# Password hash, should be a argon2id hash.
# Can be generated with: `echo -n "whatever" | argon2 somerandomsalt -id`
# Also accepts "%{file:/path/to/secret}%" to refer to the contents of a file.
password_hash = "$argon2id$v=19$m=4096,t=3,p=1$YXJnbGluYXJsZ2luMjRvaQ$fiD9Bp3KidVI/E mGudu6 h9XmF9TU9Bx4VGX0PniDE"
# The user which owns this mailbox. That user has full control over the mailbox and its aliases.
owner = "username"
# An API token for this mailbox to allow alias creation via the API endpoints.
# Optional. Default: None (API access disabled)
# Minimum length 16. Must be unique!
# Also accepts "%{file:/path/to/secret}%" to refer to the contents of a file.
api_token = "VC0lZ6O49nfxU4oK0KbahlSMsqBFiHyYFGUQvzzki6ky5mSM"
#api_token = "%{file:/path/to/secret}%"
# Whether the mailbox should be active
# Optional, default: true
active = true

[aliases."somealias@example.com"]
# The target address for this alias. The WebUI restricts users to only
# target mailboxes they own. Admins and this provisioning file
# have no such restrictions.
target = "[email protected]"
# The user/mailbox which owns this alias. If owned by a mailbox,
# the user owning the mailbox transitively owns this.
owner = "[email protected]"
# A comment to store alongside this alias.
# Optional, default: None
comment = "Used for xyz"
# Whether the user should be active
# Optional, default: true
active = true

Small example which creates an admin user and one domain:

[users.admin]
password_hash = "$argon2id$v=19$m=4096,t=3,p=1$YXJnbGluYXJsZ2luMjRvaQ$DXdfVNRSFS1QSvJo7OmXIhAYYtT/D92Ku16DiJwxn8U"
admin = true

[domains."example.com"]
owner = "admin"
public = true

📜 License

Licensed under the MIT license (LICENSE or https://opensource.org/licenses/MIT). Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, shall be licensed as above, without any additional terms or conditions.

About

An email alias and account management interface for self-hosted mailservers

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published