TL;DR fetchmail to move all emails from email provider to local mailbox that is then served via IMAP by dovecot
Hi, I like being able to switch between email providers easily without having to change my email address (related post). For example right now people have to go through the hassle of going from mygreatusername@some.host to anotherusername@another.host. It’s a big barrier because you now have to update that email address everywhere. Imagine having everything on gmail and then moving to startmail, fastmail, posteo, or whatever else.
A solution I was made aware of is to:
- pay for a domain e.g
mydomain.orgfor 10 years (can be cheap) - use their inbuilt email (sometimes free) or pick an email provider that allows custom domains
- pull all the email to server you host
- serve that email
That way, you will have your myname@mydomain.org and switch email providers underneath while keeping all your emails.
Example config
This config uses the module I wrote (maybe something else exists, but I couldn’t find it). It pulls emails of myaccount@my.domain from pop.remote.host to my.host and exposes them via IMAPS as myaccount@my.domain on my.host.
Notice that my.domain need not be the same as my.host. This allows me to hide my IMAP server. Somebody looking at the MX record of my.domain won’t immediately find the IMAP server.
{ config, ... }:
{
/**
configuration to for fetchmail to retrieve from the remote host
emails will be moved into a the **local** mailbox of a user with the same email address
*/
environment.etc."mail/fetchmailrc" = {
text = ''
poll pop.remote.host protocol pop3 port 995:
user "[email protected]" with password "passwordWithouQuotes" is vmail here
options fetchall
ssl
mda "dovecot-deliver -d [email protected]"
'';
user = config.services.email-fetch-serve.daemonUser;
group = config.services.email-fetch-serve.daemonGroup;
};
/**
usernames and passwords used to log into the **self-hosted** IMAP service
Uses same format as /etc/passwd
https://doc.dovecot.org/2.4.3/core/config/auth/databases/passwd_file.html
*/
environment.etc."mail/imap.passwd" = {
text = ''
[email protected]:{plain}password
'';
user = config.services.email-fetch-serve.daemonUser;
group = config.services.email-fetch-serve.daemonGroup;
};
services.email-fetch-serve = {
enable = true;
sslCertPath = "/var/acme/certs/mydomain.crt";
sslCertKey = "/var/acme/certs/mydomain.key";
fetchmailRcPath = "/etc/mail/fetchmailrc";
imap = {
port = 993;
openFirewall = true;
passdb = "/etc/mail/imap.passwd";
};
};
}
the module
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.email-fetch-serve;
daemonUserHome = "/var/spool/${cfg.daemonUser}";
sslEnabled = (cfg.sslCertPath != null) && (cfg.sslCertKey != null);
/**
Used by fetchmail to deliver mail to dovecot
*/
dovecot-deliver-wrapper = pkgs.writeShellScriptBin "dovecot-deliver" ''${pkgs.dovecot}/libexec/dovecot/deliver "''${@}"'';
in
{
/**
A self-hosted "email relay" that allows fetching emails from a server and then serving it
via IMAP.
Emails are retrieved with fetchmail and exposed via dovecot.
By default, dovecot used IMAP which unencrypted, but with an ssl certificate and key, it can
be encrypted and thus turned into IMAPS.
To generate SSL certs, the `security.acme` option is powerful, but you can also use a
self-signed certificate.
To store secrets, do consider using
- agenix: https://github.com/ryantm/agenix
- sopsnix: https://github.com/Mic92/sops-nix
*/
options = with lib; {
services.email-fetch-serve = {
enable = mkEnableOption "emails from an email server and serve them via IMAP";
sslCertPath = mkOption {
type = types.nullOr types.externalPath;
description = "Giving a path to an SSL cert **and** key will enable IMAPS and disable IMAP";
default = null;
};
sslCertKey = mkOption {
type = types.nullOr types.externalPath;
description = "Giving a path to an SSL key **and** cert will enable IMAPS and disable IMAP";
default = null;
};
fetchmailRcPath = mkOption {
type = types.externalPath;
description = "Configuration for fetchmail";
example = ''
poll pop.remote.host protocol pop3 port 995:
user "[email protected]" with password "passwordWithouQuotes" is vmail here
options fetchall
ssl
mda "dovecot-deliver -d [email protected]"
'';
};
imap = {
port = mkOption {
type = types.int;
description = ''
Which port to host the IMAP service on. If sslCertPath is set this will
be the port of othe IMAPS service'';
default = 143; # Default IMAP port
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
example = true;
description = "Allow external traffic to reach the IMAP(S) port";
};
passdb = mkOption {
type = types.externalPath;
description = ''
Where passwords for IMAP are stored. Should be secret and accessible by vmail user
https://doc.dovecot.org/2.4.3/core/config/auth/databases/passwd_file.html
https://doc.dovecot.org/2.4.3/core/config/auth/passdb.html
'';
};
};
daemonUser = mkOption {
type = types.str;
description = "Name of the user running the daemons";
default = "vmail";
};
daemonGroup = mkOption {
type = types.str;
description = "Name of the user's group running the daemons";
default = "vmail";
};
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
# Either both SSL vars are set or none are set
assertion =
(cfg.sslCertPath == null && cfg.sslCertKey == null)
|| (cfg.sslCertPath != null && cfg.sslCertKey != null);
message = "email-fetch-serve service must have sslCertPath AND sslCertKey to have functional SSL";
}
];
# How electronic email works
# https://tldp.org/HOWTO/Mail-Administrator-HOWTO-3.html
# ${daemonUserHome} needs to be created and owned by vmail
users.users."${cfg.daemonUser}" = {
createHome = true;
home = daemonUserHome;
group = cfg.daemonGroup;
isSystemUser = true;
};
users.groups."${cfg.daemonGroup}" = { };
services.dovecot2 = lib.mkMerge [
({
# Taken and adapted from https://wiki.nixos.org/wiki/Dovecot
enable = cfg.enable;
createMailUser = true;
enableImap = true;
mailUser = cfg.daemonUser;
mailGroup = cfg.daemonGroup;
# implement virtual users
# https://doc.dovecot.org/2.3/configuration_manual/howto/simple_virtual_install/
# store virtual mail under
# /var/spool/mail/vmail/<DOMAIN>/<USER>/Maildir/
mailLocation = "maildir:~/Maildir";
mailboxes = {
# use rfc standard https://apple.stackexchange.com/a/201346
All = {
auto = "create";
autoexpunge = null;
specialUse = "All";
};
Archive = {
auto = "create";
autoexpunge = null;
specialUse = "Archive";
};
Drafts = {
auto = "create";
autoexpunge = null;
specialUse = "Drafts";
};
Flagged = {
auto = "create";
autoexpunge = null;
specialUse = "Flagged";
};
Junk = {
auto = "create";
autoexpunge = "60d";
specialUse = "Junk";
};
Sent = {
auto = "create";
autoexpunge = null;
specialUse = "Sent";
};
Trash = {
auto = "create";
autoexpunge = "60d";
specialUse = "Trash";
};
};
extraConfig = lib.concatStrings [
''
# force to use full user name plus domain name
# for disambiguation
auth_username_format = %Lu
# Authentication configuration:
auth_mechanisms = plain
passdb {
driver = passwd-file
args = ${cfg.imap.passdb}
}
userdb {
driver = static
# the full e-mail address inside passwd-file is the username (%u)
# [email protected]
# %d for domain_name %n for user_name
args = uid=${cfg.daemonUser} gid=${cfg.daemonGroup} username_format=%u home=${daemonUserHome}/%d/%n
}
''
(lib.optionalString (!sslEnabled) ''
service imap-login {
inet_listener imap {
port = ${builtins.toString cfg.imap.port}
}
inet_listener imaps {
port = 0
}
'')
(lib.optionalString (sslEnabled) ''
service imap-login {
inet_listener imap {
port = 0
}
inet_listener imaps {
port = ${builtins.toString cfg.imap.port}
}
}'')
];
})
(lib.mkIf sslEnabled {
sslServerCert = cfg.sslCertPath;
sslServerKey = cfg.sslCertKey;
})
];
# Open the firewall port to be able to be contacted
networking.firewall.allowedTCPPorts = [ cfg.imap.port ];
networking.firewall.allowedUDPPorts = [ cfg.imap.port ];
#####################
# To fetch the emails
systemd.services.fetchmail = {
enable = cfg.enable;
after = [ "dovecot2.service" ];
wantedBy = [ "dovecot2.service" ];
path = [ dovecot-deliver-wrapper ];
serviceConfig = {
User = cfg.daemonUser;
ExecStart = "${pkgs.fetchmail}/bin/fetchmail --fetchmailrc ${cfg.fetchmailRcPath} --daemon 60";
};
};
};
}
Nice. Perhaps crosspost this to the selfhosted comm, I bet they’d be interested
Done :) I don’t know how many NixOS users are in the community, but maybe it’ll be useful to someone.

