Introduction

This is a WIP document for vSMTP v1.3.x. Remember that these versions are not intended for production use.

Welcome to vBook, the vSMTP reference guide. It serves as vSMTP’s primary documentation and tutorial resource.

vSMTP

vSMTP is a secured, faster and greener Mail Transfer Agent (MTA).

It is a more efficient, more secure and more ergonomic product than the competition. It is up to 10 times faster than its competitors and significantly reduces the need for IT resources.

It is written in Rust. Compared to solutions developed in other programming languages (usually C or C++), Rust provides a memory safety model, guaranteeing no segmentation fault or race conditions. It is the latest generation language best suited to system programming, network services, and embedded systems.

vSMTP development goes through a full cycle of testing. Unit and integration tests allow more comprehensive coverage of safety tests, one covering faults of the other, and vice versa (static view and runtime view).

License

The standard version of vSMTP is released under the GPLv3 license. It is provided as usual without any warranty. Please refer to the vSMTP LICENSE for further information.

This book is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License. For further details please refer to the LICENSE file.

Source Code

The source files for this book can be found on GitHub.

Contributing

The vSMTP and vBook projects need you. Parts requiring your help are labeled as help wanted. Please see the CONTRIBUTING.md file for more information.

Features

vSMTP is a Mail Transfer Agent (MTA) and a Mail Submission Agent (MSA). It is not intended to be a Mail User Agent (MUA) nor a Mail Delivery Agent (MDA).

For outgoing mail, vSMTP can directly be addressed by your MUA using the SMTP protocol.

For incoming mails, vSMTP can store messages on a server file-system using Mbox or Maildir formats. To retrieve emails from a remote client (MUA) it is necessary to install a MDA that can handle POP and/or IMAP protocols.

Roadmap

Take a look at the ROADMAP in vSMTP repository.

Follow the development of vsmtp, plannings and announcements for incoming features on the official discord server.

Available features

Networking

  • Listen and serve on multiple addresses.
  • Support for IPv4 and IPv6 format.
  • Built on high performance asynchronous connections.
  • Handle one or multiple emails per connections.
  • Compliancy with Internet Message Format and Simple Mail Transfer Protocol RFCs.
  • TLS 1.3 support.
  • Complete DNS configurations (thanks to Benjamin Fry’s Trust-DNS crate).
  • Support for high workload through built-in mechanisms.

API

vSMTP is a modular and highly customizable product. Adding or modifying subsystems is made easier by the internal design of the software. An API is available and allows easy integration into existing security elements. Several native plug-ins are already available.

  • Mail exports in RAW and JSON format.
  • Third-party software called by user-defined services.
  • Mods and addons support.
  • System and applications logs.

Filtering

vSMTP has a complete filtering system. In addition to the standard analysis of the SMTP envelope, vSMTP provides on the fly interactions on the content of messages (MIME). Users can generate complex routing and filtering scenarios through a simple and intuitive advanced scripting language.

  • Before and after queueing filtering.
  • vSMTP scripting language allowing administrators to define complex rules.
  • Interaction at each SMTP state (HELO/EHLO, CONNECT, MAIL, RCPT, DATA).

Delivery

  • SMTP remote delivery - using a third-party software, Lettre.
  • Mbox and Maildir format for local delivery.
  • SMTP relaying and forwarding.

External services

vSMTP supports SMTP delegation, command calls, and file databases. CSV files and MySQL databases are supported.

Next versions will provide LDAP, NoSQL databases, and in-memory caches supports. Compliancy with Postfix SMTP access policy delegation and Unix/IP socket calls are planned for Q4/2022.

Email authentication mechanisms

  • Message submission RFCs.
  • Null MX RFC.
  • SPF support.
  • DKIM signer and verifier.
  • DMARC verifier, reporting is not natively supported.
  • DANE protocol is planned for a future release.
  • ARC and BIMI experimental and future Internet standards are currently not supported.

Installation

vSMTP is a stand-alone application with few kernel interactions, it may run on any system with slight modifications. Many installation methods are available:

  • by extracting a binary package suitable for your distribution,
  • by using Rust’s Cargo tool,
  • by deploying a Docker container.

If your system is not supported or if these installation method are not suited for your usage, you can contact us by opening an issue on github or by joining the official discord server.

Either way, you can download and build from source the project, see the dedicated chapter.

Requirements

Physical requirements

The current release has been tested and deployed on x86/64 environments.

Operating systems

vSMTP is tested and deployed on Ubuntu Server 20.04 with kernel 5.4, but vSMTP should be compatible with any recent Linux distributions.

FreeBSD 13.x is supported using the latest port branch which includes Rust 1.60. NetBSD and OpenBSD supports are planned for Q1-2023.

Microsoft Windows Server is not supported.

Installation methods

Linux distros

Debian binary packages .deb can be downloaded from the release section of the vSMTP github.

sudo apt install vsmtp

Fedora and RedHat packages are planned for future releases. help wanted

BSD ports

help wanted ( Issue 484 )

Rust Cargo

crates.io

vSMTP is published on https://crates.io. It can be install using cargo tool if the Rust language is installed on your server.

cargo install vsmtp

Docker

The official repository for vSMTP on Docker Hub is viridit/vsmtp.

Configuring vSMTP

System configuration

The behavior of the vSMTP service (network, default directories, tls, etc.) can be configured by editing the vsmtp.toml file. Its default location is /etc/vsmtp/vsmtp.toml.

When starting the vSMTP service, its configuration file is read and fully parsed. An error occurs when a key or a value is incorrect. The server never crashes if the configuration is successfully loaded.

Please refer to the vSMTP TOML key/value list chapter for further details.

Application configuration

SMTP filtering is performed by a rule engine. Scripts are written in simple, but powerful, programming language : vSMTP Scripting Language (vSL). The entry point is the main.vsl file.

Its syntax is similar to a configuration format, but with programmatic capabilities. It is based on four main concepts : rules, actions, objects and services.

Please refer to the dedicated chapter guide on vSL for further details.

Service configuration

The vsmtp.service file contains all the mandatory information to start the vSMTP service on Linux server using systemd as the system and service manager.

Doe’s family: Step-by-step tutorial

In this tutorial we suppose that Doe’s family (John, Jane and their children Jimmy and Jenny) wants to self-host their mailboxes.

Context

They use a router/firewall to route and secure their home network. They also setup a small server in the DMZ to manage the family website, emails, etc.

The network looks like the following :

{ Internet ISP } --- Firewall --- { DMZ }
                        |
              { Internal Network }

They also rented a domain name doe-family.com. They decided to prefix their mailboxes with the common standard “first_name.last_name” like jenny.doe@doe-family.com

Network configuration

vSMTP is installed on an Ubuntu server 20.04 virtual instance hosted by the DMZ server.

{ Internet ISP } --- Firewall --- { DMZ : MTA }
                        |
              { Internal Network }


- Public IP : 80.80.80.80
- MTA IP : 192.168.1.254/32 - fqdn : mta.doe-family.com
- Internal Network : 192.168.0.0/24

Firewall rules

# Pseudo code - depends on your FW
#
# Allow SMTP and IMAP from Internet to MTA
Public IP > MTA : TCP/SMTP 25, TCP/SMTP 465, TCP/SMTP 587
# Allow viewing family emails using IMAP/ssl from the Internet
Public IP > MTA : IMAP/ssl 993
# Outgoing SMTP traffic
Internal NET > MTA : TCP/SMTP 25, TCP/SMTP 465, TCP/SMTP 587
MTA > {Internet} : TCP/SMTP 25, TCP/SMTP 465
# DNS traffic from MTA
MTA > {Internet} : UDP/DNS 53, TCP/DNS 53

DNS

MX preference = 1, mail exchanger = vsmtp.doe-family.com

Certificate

John generated a certificate through the Let’s Encrypt Certificate Authority for vSMTP server.

sudo certbot certonly --manual --preferred-challenges=dns --agree-tos -d mta.doe-family.com

Basic configuration

Listen and serve

First of all, create the file /etc/vsmtp/vsmtp.toml with this content:

version_requirement = ">=1.3.0"

# root domain of the server.
[server]
domain = "doe-family.com"

# addresses that the server will listen to.
# (change `192.168.1.254` for the address you want to listen to)
[server.interfaces]
addr = ["192.168.1.254:25"]
addr_submission = ["192.168.1.254:587"]
addr_submissions = ["192.168.1.254:465"]

The server can now listen and serve SMTP connections.

$> sudo systemd restart vsmtp
$> telnet 192.168.1.254:25
220 doe-family.com Service ready
554 permanent problems with the remote server

By default, the server will deny any connection. We have to define rules to accept connections and filter messages.

Define filtering logics

We will configure these rules:

  • Messages from blacklisted domain will be rejected.
  • As Jenny is 11 years old, Jane wants a blind copy of her daughter messages.
  • Messages sent to the family must be delivered in MailBox format.

First, let’s define all the required objects for John Doe’s MTA.

Create the /etc/vsmtp/rules/objects.vsl file with the content:

// -- /etc/vsmtp/rules/objects.vsl
// IP addresses of the MTA and the internal IP range.
object local_mta ip4 = "192.168.1.254";
object internal_net rg4 = "192.168.0.0/24";

// Doe's family domain name.
object family_domain fqdn = "doe-family.com";

// Mailboxes.
object john address = "john.doe@doe-family.com";
object jane address = "jane.doe@doe-family.com";
object jimmy address = "jimmy.doe@doe-family.com";
object jenny address = "jenny.doe@doe-family.com";

// A group to manipulate mailboxes.
object family_addr group = [john, jane, jimmy, jenny];

// Quarantine folders.
object unknown_quarantine string = "doe/bad_user";
object virus_queue string = "doe/virus";

// A user blacklist file.
object blacklist file:fqdn = "blacklist.txt";

The content of the blacklist.txt file is:

# domain-spam.com
# spam-domain.org
# domain-spammers.com
# foobar-spam-pro.org

And create the /etc/vsmtp/rules/main.vsl file with the content:

// -- /etc/vsmtp/rules/main.vsl
// Import the object file. The 'doe' prefix is an alias.
import "objects" as doe;

#{
  // List of rules execute after receiving "MAIL FROM" from a client.
  // At this stage the sender is known and can be accessed using `mail_from()`.
  mail: [
    // Deny any sender with a domain listed in the `blacklist` group.
    rule "blacklist" || {
      if mail_from().domain in doe::blacklist { deny() } else { next() }
    }
  ],

  // List of rules execute after receiving "RCPT TO" from a client.
  // The current recipient can be inspected using `rcpt()`.
  rcpt: [
    // automatically set Jane as a BCC if Jenny is part of the recipients.
    action "bcc jenny" || if rcpt() is doe::jenny { bcc(doe::jane) },
  ],

  // The delivery stage is executed just before vsmtp delivers the message.
  delivery: [
    action "setup delivery" || {
      // if a recipient is part of the family, we deliver the email locally.
      // Otherwise, we just deliver the email to another server.
      for rcpt in rcpt_list() {
        if rcpt in doe::family_addr { maildir(rcpt) } else { deliver(rcpt) }
      }
    }
  ]
}

Add these lines to your /etc/vsmtp/vsmtp.toml:

# ...

# entry point for our rules.
[app.vsl]
filepath = "/etc/vsmtp/rules/main.vsl"

Restart the server to apply the rules.

SSL/TLS

Connections should be encrypted using the SSL/TLS protocol, even on a private network.

The vSMTP implementation is based on the state-of-the-art rustls library.

Add the following to the /etc/vsmtp/vsmtp.toml file:

# ...

# TLS settings
[server.tls]
security_level = "May"
preempt_cipherlist = false
handshake_timeout = "1000ms"
protocol_version = ["TLSv1.2", "TLSv1.3"]
certificate = "/etc/letsencrypt/live/mta.doe-family.com/cert.pem"
private_key = "/etc/letsencrypt/live/mta.doe-family.com/privkey.pem"

Authentication

Doe’s family users must be authenticated if they send messages from an external network (i.e. from a cellular net). As John decided to create Unix users, the shadow mechanism is required.

Add the following to your /etc/vsmtp/vsmtp.toml file:

# ...

[server.smtp.auth]
must_be_authenticated = false
enable_dangerous_mechanism_in_clair = false

And this rule to your /etc/vsmtp/rules/main.vsl file:

#{
  // ... previous code ...

  authenticate: [
    rule "auth /etc/shadow" || { authenticate() }
  ],
}

⚠️ authenticate() function call the program testsaslauthd itself calling the saslauthd daemon. Make sure to install the Cyrus sasl binary package for your distribution and configure the saslauthd daemon with MECHANISM="shadow" in /etc/default/saslauthd. Future releases will bring improved vSL API.

Hardening vSMTP

Disabling open mail relay

The server must only accept messages from the Internet that comply with :

  • The recipient is a Doe’s family account, whatever the sender is (incoming messages).
  • The sender is authenticated as a Doe’s family account, whatever the recipient is (outgoing messages).

From the internal network, all IPs are allowed to send messages.

Edit the /etc/vsmtp/rules/main.vsl file and add the rules:

// -- main.vsl
import "objects" as doe;

#{
  // ... previous code ...

  mail: [
    rule "relay mail from" || check_mail_relay(doe::internal_net),
  ],

  rcpt: [
    rule "relay rcpt" || check_rcpt_relay(doe::internal_net),
  ],
}

Now Doe’s family server is protected against open-relaying attacks.

Using the SPF protocol

The SPF protocol allows other MTAs to check that outgoing messages from Doe’s family domain are valid. A new DNS record is added into the doe-family.com DNS zone. It declares that only the server declared in the MX record is allowed to send messages on behalf of Doe’s family.

doe-family.com.          TXT "v=spf1 +mx -all"

For incoming messages, the SPF protocol is configured to check the sender credentials at the mail stage.

Edit the /etc/vsmtp/rules/main.vsl file and add the rule:

// -- main.vsl
#{
  // ... previous code ...

  mail: [
    rule "check spf" || check_spf("both", "soft"),
  ]
}

Using DKIM

The DKIM protocol is natively implemented in vSMTP.

This protocol ensure that the content of the message has not been modified during the transport. A new DNS record is added into the doe-family.com DNS zone. It declares the public key usable to verify the messages (see how to had the DKIM record).

We will configure these rule:

  • The sender is a Doe’s family account : a DKIM signature is added to the message.
  • The recipient is a Doe’s family account : the DKIM signatures are verified.

Add the following to the /etc/vsmtp/vsmtp.toml file:

[server.dkim]
private_key = "/path/to/private-key"

Edit the /etc/vsmtp/rules/main.vsl file and add the rules:

#{
  // ... previous code ...

  postq: [
    action "sign dkim" || {
      if in_domain(mail_from()) {
        dkim_sign("2022-09" /* selector of the DNS record */);
      } else {
        dkim_verify();
      }
    }
  ],
}

Adding an antivirus

Malware remains a scourge. As John is aware of security issues, he decides to add a layer of antivirus directly on the MTA. He installed ClamAV which comes with the clamsmtpd antivirus daemon.

vSMTP security delegation

vSMTP support security delegation via the SMTP protocol (all the logics is defined in .vsl):

{ Incoming msg }  ---> vSMTP server ---> { Delivered msg }
                        |       ^
                        |       |
      clamsmtpd socket  |       |   vSMTP socket
         (delegator)    |       |    (receiver)
       127.0.0.1:10026  |       |  127.0.0.1:10025
                        |       |
                        v       |
                  { clamsmtpd daemon } <-> { ClamAV }

ClamAV setup

The following example assumes that the clamsmtpd service is loaded and started with the following configuration:

## -- /etc/clamsmtpd.conf

# The address to send scanned mail to.
# This option is required unless TransparentProxy is enabled
OutAddress: 10025

# Address to listen on (defaults to all local addresses on port 10025)
Listen: 127.0.0.1:10026

# Tells clamav to forward the email to vsmtp
# event thought it found a virus. (it drops the email by default)
Action: pass

Use the following commands to start clamav:

sudo systemctl start clamsmtp
sudo systemctl start clamav-daemon

The service

Create a smtp service in the file /etc/vsmtp/rules/service.vsl:

// -- /etc/vsmtp/rules/service.vsl
service clamsmtpd smtp = #{
  delegator: #{
    address: "127.0.0.1:10026",
    timeout: "60s",
  },
  receiver: "127.0.0.1:10025",
};

The receiver’s socket must be enabled in the /etc/vsmtp/vsmtp.toml.

# -- /etc/vsmtp/vsmtp.toml
[server.interfaces]
#       clients              delegation results
addr = ["192.168.1.254:25", "127.0.0.1:10025"]

The delegate keyword

Create the antivirus passthrough using the delegate keyword.

// -- /etc/vsmtp/rules/main.vsl
// You cannot use `import "service" as service;` here because `service` is
// a reserved keyword.
import "service" as svc;

#{
  postq: [
    // a `delegate` directive is like a `rule`, it needs to return a status code.
    // The difference is that it first sends the content of the mail to the service
    // via SMTP and then execute the body of the function.
    delegate svc::clamsmtpd "check email for virus" || {
      // this is executed once the delegation result are received.
      log("debug", "email analyzed.");

      // ClamAV inserts the "X-Virus-Infected" header if it founds a virus
      if has_header("X-Virus-Infected") {
        quarantine("virus_q")
      } else {
        next()
      }
    }
  ],
}

Since there is no heavy network traffic, John decided to do a “post-queue” filtering.

Compromised emails are quarantined in the virus_q folder.

Once the check email for virus rule is run, vSMTP will send the email to the clamsmtpd service and the rule evaluation is on hold. Once all results are received on the delegation port (10025), evaluation resumes, and the body of this rule is evaluated.

Greylist

Greylisting is one method to defend a SMTP server against spam.

The goal is to temporarily reject emails from a sender that is not yet in registered in a database, then accepting it back if the sender retries.

To build a greylist, you need to create a database and a vSMTP service. For this tutorial, we will use the mysql database.

The database

Install MySQL

Please follow this great tutorial to install MySQL. This setup has been tested on Ubuntu 22.04, check out MySQL website for other systems.

# Install mysql.
$ sudo apt update
$ sudo apt install mysql-server
$ sudo systemctl start mysql.service

# Login as root.
$ sudo mysql

# Replace auth_socket authentication by a simple password.
mysql> ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your-password';
mysql> exit

# Update root password & remove unnecessary stuff.
$ sudo mysql_secure_installation

# Connect as root with the password.
mysql -u root -p

# Reset auth to auth_socket, this way you can connect with `sudo mysql`
mysql> ALTER USER 'root'@'localhost' IDENTIFIED WITH auth_socket;

Setup a user

To manage your database, you should create a new user with restricted privileges instead of relying on root.

# Here we use localhost as our host but you could also setup your database on another server.
mysql> CREATE USER 'greylist-manager'@'localhost' IDENTIFIED BY 'your-password';

Setup the database and a table

To create the database, simply put the content of this sql code into a greylist.sql file.

CREATE DATABASE greylist;

CREATE TABLE greylist.sender(
    address varchar(500) NOT null primary key,
    user varchar(500) NOT null,
    domain varchar(500) NOT null
);

And then run the mysql command as root to generate the database.

sudo mysql < greylist.sql

Grant necessary privileges to your user on the newly create table.

mysql> GRANT SELECT, INSERT ON greylist.sender TO 'greylist-manager'@'localhost';

The greylist database is now operational.

Setup vSMTP

To setup vSMTP, you first need to create a mysql service, that will enable you to query your database. You can, for example, write it in a services.vsl file where your main.vsl is located.

// -- services.vsl
service greylist db:mysql = #{
    // Change this url to the url of your database, or keep it like this if the 'greylist-manager' user is setup on localhost.
    url: "mysql://localhost/",

    // Select the user that you created when setting up your database.
    user: "greylist-manager",
    password: "your-password",
};

The query function from the db:mysql service is used to query a mysql database. Variables are passed to the query using string interpolation.

⚠️ String interpolation can lead to SQL injection if not used properly. Make sure to sanitize your inputs, set only required privileges to the mysql user, and check what kind of data you are injecting.

Create a greylist rule in your main.vsl file.

// -- main.vsl
import "services" as svc;

#{
    // The greylist is effective in the "mail" stage, because the sender
    // of the email is received at this stage.
    mail: [
        rule "greylist" || {
            let sender = mail_from();

            // if the sender is not recognized in our database,
            // we deny the transaction and write the sender into
            // the database.
            if svc::greylist.query(`SELECT * FROM greylist.sender WHERE address = '${sender}';`) == [] {
                // Writing the sender into the database.
                svc::greylist.query(`
                    INSERT INTO greylist.sender (user, domain, address)
                    values ("${sender.local_part}", "${sender.domain}", "${sender}");
                `);

                // vsl exposes a `code_greylist` code which is a "451 4.7.1" enhanced code.
                deny(code_greylist)
            } else {
                // the user is known by the server, the transaction
                // can proceed.
                accept()
            }
        }
    ]
}

Logging

The logging system is backed by tokio tracing and piped to multiple ‘subscriber’ :

Backend logs

Backend logs concerns the vSMTP internals output.

The default output directory is /var/log/vsmtp/vsmtp.log.

Log levels can be configured by “modules”, representing part of the server, using the env_logger syntax (see docs.rs). The vsmtp_rule_engine module enables logs for the rule engine, the vsmtp_mail_parser module for the mime parser, etc.

The vsmtp.toml file is used to configure server logs:

[server.logs]
# You can change the location of the server logs.
filepath = "./tmp/system/vsmtp.log"

level = [
    # set global logging level to "info" for all the modules.
    "info",

    # set the logging level per module.
    "vsmtp_server::receiver=info",
    "vsmtp_rule_engine=warn",
    "vsmtp_delivery=error",
]

Application logs

Application logs are defined using the log(level, message) function in the vSL rules.

The default output location (/var/log/vsmtp/app.log) can be modified in the vsmtp.toml file :

[app.logs]
# You can change the location of the application logs.
filepath = "./tmp/system/app.log"

Syslogd

vSMTP send logs to the syslog daemon using the mail facility :

# if the table is missing, system's log are skipped
[server.logs.system]
# write only the message of a specific level and more
level = "info"
backend = "syslogd"
# format used by the logger see https://www.rfc-editor.org/rfc/rfc3164 and https://www.rfc-editor.org/rfc/rfc5424
format = "3164"

socket = { type = "unix", path = "/dev/log" }
# or
socket = { type = "tcp", server = "127.0.0.1:601" }
# or
socket = { type = "udp", server = "127.0.0.1:514", local = "127.0.0.1:0" }
# note: address can be ipv4 / ipv6

Journald

vSMTP send logs to the journald daemon :

# if the table is missing, system's log are skipped
[server.logs.system]
# write only the message of a specific level and more
level = "info"
backend = "journald"

Domain Name System configuration

vSMTP can handle complex DNS situations. A default configuration is applied on the root domain and virtual domains inherit from it. Specific values can be updated on root domain or on a per virtual domain basis.

vSMTP relies on Benjamin Fry’s Trust-DNS crate to handle DNS queries.

DNS parameters are stored in the [server.dns] and [server.virtual.dns] tables and sub-tables. Please refer to vSMTP reference guide and Trust-DNS repository for detailed information.

DNS resolver

The default behavior of the upstream resolver is defined by the operating system /etc/resolv.conf file. Alternative configurations such as Google or CloudFlare Public DNS may be applied using the type field in the server.dns table.

[server.dns]
type = "system" | "google" | "cloudflare"

Please see Google and CloudFlare privacy statement for important information about what they track.

Locating the target host

Section 5 of RFC5321 covers the sequence to identify a server that accepts email for a domain. The SMTP client first looks up a DNS MX RR, and if is not found, falls back to a DNS A or AAAA request. If a CNAME record is found, the resulting name is processed as if it were the initial name.

This mechanism suffers from two major drawbacks.

  • A DNS overload because of an email service semantic.
  • If there are no SMTP listeners at the A/AAAA addresses, message delivery will be attempted repeatedly many times before the sending Mail Transfer Agent (MTA) gives up, implying:
    • a delay notification to the sender in the case of misdirected mail,
    • a waste of IT resources at the sender.

The “Null MX” protocol solves these issues.

Resolver options

DNS Options can be set in the TOML [server.dns.options] table.

ParametervalueDescriptionDefault value
timeoutintegerSpecify the timeout for a request.5 seconds.
attemptsintegerusize Number of retries after lookup failure before giving up.2 attempts.
rotatetrue/falseRotate through the resource records in the response.No rotation.
validatetrue/falseUse DNSSec to validate the request.False.
ip_strategyenum1The ip_strategy for the Resolver to use when lookup Ipv4 or Ipv6 addresses.IPv4 then IPv6.
cache_sizeintegerCache size is in number of records.32 records.
num_concurrent_reqsintegerNumber of concurrent requests per query.2 concurrent requests.
preserve_intermediatestrue/falsePreserve all intermediate records in the lookup response, such as CNAME records.True.
1

Ipv4Only, Ipv6Only, Ipv4AndIpv6, Ipv6thenIpv4, Ipv4thenIpv6

A Resolver configuration example :

[server.dns]
type = "cloudflare"

[server.dns.options]
timeout = "5s"
cache_size = 500
ip_strategy = "Ipv6thenIpv4"
validate = true

Advanced parameters are available. Please check vSMTP reference guide and Trust-DNS repository.

DNS and SMTP return codes

In case of a DNS failure, the RFC 3463, Enhanced Mail System Status Codes, registers two specific SMTP return codes.

CodeX.4.3
TextDirectory server failure.
Basic status code451, 550
DescriptionThe network system was unable to forward the message, because a directory server was unavailable. This is useful only as a persistent transient error. The inability to connect to an Internet DNS server is one example of the directory server failure error.
CodeX.4.4
TextUnable to route.
Basic status codeNot given.
DescriptionThe mail system was unable to determine the next hop for the message because the necessary routing information was unavailable from the directory server. This is useful for both permanent and persistent transient errors. A DNS lookup returning only an SOA (Start of Administration) record for a domain name is one example of the unable to route error.

Reverse DNS queries

Most email authentication mechanisms rely on reverse DNS queries. The configuration of these protocol-related queries is registered in their corresponding TOML tables.

However, for specific DNS reverse queries can also be directly using the vSL reverse lookup function.

RFC 7372 registers status codes to be returned if a message is being rejected or deferred specifically because of email authentication failures.

CodeX.7.25
TextReverse DNS validation failed.
Basic status code550
DescriptionAn SMTP client’s IP address failed a reverse DNS validation check, contrary to local policy requirements.

Protocol-specific return codes are described in the corresponding chapters.

Virtual Domain

As shown in the example below, virtual domains can be configured under the root domain.

[server]
# system configuration
#

#
# Root domain
#
domain = "root-example.net"

[server.dns]
type = "google"

[server.tls]
security_level = "None"

# ...
# ... End of main domain configuration

#
# Virtual domain : "example1.com"
#
[server.virtual."example1.com"]
# DNS type is not specified - thus it's inherited from the main domain

[server.virtual."example1.com".tls]
protocol_version = "TLSv1.3"
certificate = "./certs/certificate-example1.crt"
private_key = "./certs/private-example2.key"

#
# Virtual domain : "example2.com"
#
[server.virtual."example2.com"]

[server.virtual."example2.com".dns]
type = "system"

[server.virtual."example2.com".tls]
protocol_version = "TLSv1.3"
certificate = "./certs/certificate-example2.crt"
private_key = "./certs/private-example2.key"

Parameters can be:

  • Specified in primary domain: All virtual domains use these settings.
  • Specific to a virtual domain.

Please refer to the reference guide for a fully description of the key/value pairs.

Email Authentication Mechanisms

Prevent scammers from usurping an identity by using its domain name is a must nowadays. This is where the SPF, DKIM and DMARC authentication protocols come into play.

This chapter described how to define vSMTP as a legitimate sender and receiver.

SPF and DKIM : what’s that all about ?

Sender Policy Framework (SPF) specifies the authorized senders in a given domain. It thus allows email clients to check that incoming email from a domain comes from a host authorized by the administrator of this domain.

SPF is an authentication standard that links a domain name and an email address.

The DomainKeys Identified Mail (DKIM) protocol allows the sender to sign its message with its domain name. The DKIM protocol is used to attest that the domain name has not been usurped, and that the message has not been altered during its transmission.

DKIM is an authentication protocol that links a domain name to a message.

The simultaneous use of this two protocols is one of the most effective way to prevent phishers and other fraudsters from impersonating a legitimate sender using its domain name.

The implementation of these protocols improves email deliverability, since senders are better identified by the ISPs (Internet Service Providers) and email clients of your recipients. You then optimize your chances that your emails will

These two protocols are now widely used. A message sent without an SPF and/or DKIM signature is viewed with suspicion by the various email analysis tools and may be flagged as a “spam”.

SPF and DKIM : limitations

SPF has limits. If a message is forwarded, the verification may not be performed, since the address of the sender of the forwarded message is not necessarily included in the list of addresses agreed by the SPF evaluation.

DKIM also has limits. The DKIM signature does not prevent the sender from being considered a spammer if good emailing practices are not applied.

Moreover, SPF and DKIM do not specify the action to apply in case of verification failure. The DMARC protocol comes in, tells the recipient’s server how it should act if the sender’s authentication processes fail.

DMARC standard

Domain-based Message Authentication, Reporting and Conformance, or DMARC, is an authentication standard complementary to SPF and DKIM intended to fight more efficiently against phishing and other spamming practices.

DMARC allows domain holders to tell ISPs and email clients what to do when a signed message from their domain is not formally identified by a SPF or a DKIM mechanism.

ARC (extension to DMARC)

Parts of legitimate messages (subject tags, footers, etc.) can be altered due to forwarding (mailing list, virus scanner, etc.) and thus no longer succeed commonly accepted authentication checks. It can happen when an Internet domain publishes a strict DMARC policy.

ARC captures and conveys authentication results and allows the final recipient to check the authentication status of the message when it arrives at the first ARC-aware MTA.

BIMI

Brand Indicators for Message Identification (BIMI) is a future standard that makes it easier to identify the sender of an email. BIMI coordinates e-mail publishers and domain name owners to allow the latter to display their logos directly at the level of their customers’ e-mail boxes, i.e. next to the name of the issuer. BIMI requires DMARC.

For brands, BIMI is an opportunity to protect their identities by e-mail (fight against “phishing”) and to increase the reach and visibility of their logos. Because these logos are only included in DMARC-authenticated emails, consumers’ trust in their brands is also enhanced.

Null MX record

The “null MX” protocol is used to state that mail services are disabled in a given domain. The protocol is described in RFC 7505.

When a specific DNS record (a null MX) is effectively defined in the DNS zone of a given domain, all mail delivery attempts fail immediately.

DNS record

The null MX RR is an ordinary MX record with an RDATA section consisting of preference number 0 and a “.” as the exchange domain, to denote that there exists no mail exchanger.

nomail.example.com. 86400 IN MX 0 "."

A domain that advertises a null MX must not advertise any other MX RR.

Sender with “Null MX” records

As Null MX is primarily intended for domains that do not send or receive any mail, vSMTP default behavior rejects mail that has an invalid return address.

Mail systems should not publish a null MX record for domains that they use in MAIL FROM (RFC5321) or From (RFC5322) directives.

Null MX return codes

The RFC 7505 defines two specific return codes.

CodeX.1.10
TextRecipient address has null MX
Basic status code556
DescriptionThe associated address is marked as invalid using a null MX.
CodeX.7.27
TextSender address has null MX
Basic status code550
DescriptionThe associated sender address has a null MX, and the SMTP receiver is configured to reject mail from such sender (e.g., because it could not return a DSN).

vSL predefined functions

This is a DRAFT for future releases

The standard API includes a dedicated abstract to check the Null MX record.

// -- main.vsl

mail: [
  rule "check null MX" || check_null_mx();
]

Current Behavior

Currently, MX records are automatically checked on delivery. If the destination server provides a NULL MX record, the message is immediately moved into the dead queue.

Sender Policy Framework (SPF)

This document describes the vSMTP implementation of the Sender Policy Framework (SPF) protocol described in RFC 7208.

SPF is an authentication standard used to link a domain name and an email address. it allows email clients to verify that incoming email from a domain comes from a host authorized by the administrator of this domain.

The SPF framework allows the ADministrative Management Domains (ADMDs) to explicitly authorize hosts to send email. The authorization list is published in the DNS records of the sender’s domain.

DNS records

The type of a SPF record is TXT. There should be only one SPF record per domain. In case of multiple SPF records, an error may be raised.

Here is a basic SPF record example: “only servers in the range 123.123.123.0/24 and MTA (MX) are authorized to send emails from my domain example.com. All other senders are considered unauthorized.”

example.com.          TXT "v=spf1 +mx ip4:123.123.123.0/24 -all"

There is also a tilde version: ~all. It warns that other senders are not allowed, but must still be accepted. This “Soft Fail” statement was first introduced for testing purposes, but is now used by various hosting providers.

Use of wildcard records for publishing is discouraged.

Please refer to RFC 7208 for further details.

vSMTP implementation

HELO/EHLO Identity

The RFC 7208 does not enforce a HELO/EHLO verification.

“It is RECOMMENDED that SPF verifiers not only check the “MAIL FROM” identity but also separately check the “HELO” identity […] Additionally, since SPF records published for “HELO” identities refer to a single host, when available, they are a very reliable source of host authorization status. Checking “HELO” before “MAIL FROM” is the RECOMMENDED sequence if both are checked.“

The RFC 5321 tends to normalize the HELO/EHLO arguments to represent the fully qualified domain name of the SMTP client. However the vSMTP SPF verifier is prepared for the identity to be an IP address literal or simply be malformed.

“SPF check can only be performed when the “HELO” string is a valid, multi-label domain name.“

MAIL FROM identity

According to the RFC, “MAIL FROM” check occurs when :

“SPF verifiers MUST check the “MAIL FROM” identity if a “HELO” check either has not been performed or has not reached a definitive policy result.“

Note that RFC5321 allows the reverse-path to be null. In this case, the RFC 7208 defines the “MAIL FROM” identity as the local-part “postmaster” and the “HELO” identity.

Location of checks

As defined by the RFC :

“The authorization check SHOULD be performed during the processing of the SMTP transaction that receives the mail. This reduces the complexity of determining the correct IP address to use as an input to check_host() and allows errors to be returned directly to the sending MTA by way of SMTP replies.”

vSMTP allows the use of the SPF framework only at the “MAIL FROM” stage.

Results of Evaluation

The vSMTP SPF verifier implements results semantically equivalent to the RFC.

ResultDescription
none(a) no syntactically valid DNS domain name was extracted from the SMTP session that could be used as the one to be or (b) no SPF records were retrieved from the DNS.
neutralThe ADMD has explicitly stated that it is not asserting whether the IP address is authorized.
passThe client is authorized to inject mail with the given identity.
failThe client is not authorized to use the domain in the given identity.
softfailThe host is probably not authorized but the ADMD has not published a stronger policy.
temperrorA transient (generally DNS) error while performing the check.
permerrorThe domain’s published records (DNS) could not be correctly interpreted.
policyAdded by RFC 8601, Section 2.4 - indicate that some local policy mechanism was applied that augments or even replaces (i.e., overrides) the result returned by the authentication mechanism. The property and value in this case identify the local policy that was applied and the result it returned.

Results headers

Results should be recorded in the message header. Two options are available according to the RFC:

  1. The “Received-SPF” header
Received-SPF: pass (mybox.example.org: domain of myname@example.com
  designates 192.0.2.1 as permitted sender)
  receiver=mybox.example.org; client-ip=192.0.2.1;
  envelope-from="myname@example.com"; helo=foo.example.com;
  1. The “Authentication-Results” header described in RFC 8601 : Message Header Field for Indicating Message Authentication Status.
Authentication-Results: example.com; spf=pass smtp.mailfrom=example.net

SPF failure codes

The RFC 7372 “Email Auth Status Codes” introduces new status codes for reporting the DKIM and SPF mechanisms.

CodeX.7.23
TextSPF validation failed
Basic status code550
DescriptionA message completed an SPF check that produced a “fail” result
Used in place of5.7.1, as described in Section 8.4 of RFC 7208.
CodeX.7.24
TextSPF validation error
Basic status code451/550
DescriptionEvaluation of SPF relative to an arriving message resulted in an error.
Used in place of4.4.3 or 5.5.2, as described in Sections 8.6 and 8.7 of RFC 7208.

The following error codes can also be sent by the SPF framework.

CodeX.7.25
TextReverse DNS validation failed
Basic status code550
DescriptionAn SMTP client’s IP address failed a reverse DNS validation check, contrary to local policy requirements.
Used in place ofn/a
CodeX.7.26
TextMultiple authentication checks failed
Basic status code500
DescriptionA message failed more than one message authentication check, contrary to local policy requirements. The particular mechanisms that failed are not specified.
Used in place ofn/a

vSL predefined function

The vSL standard API has a dedicated function to check the SPF policy. check_spf returns a status, containing custom codes to be sent to the client.

Check the Security file to get the full documentation about check_spf.

DomainKeys Identified Message

The vSMTP implementation of the DomainKeys Identified Mail Signatures (DKIM) protocol is described in RFC 6376.

DKIM is an open standard for email authentication used to check the integrity of the content. A signature header is added to the messages. This signature is secured by a key pair (private/public).

DKIM signatures work like a watermark. Therefore, they survive forwarding, which is not the case for SPF.

Assertion of responsibility is validated through a cryptographic signature and by querying the Signer’s domain to retrieve the appropriate public key.

DNS records

DKIM relies on a DNS TXT record inserted into the sender domain’s DNS zone. The record contains the public key for the DKIM settings. The private key held by the sending mail server can be verified against the public key by the receiving mail server.

Unlike SPF record, several DKIM records can be linked to a domain. A domain can have several public keys if it has several mail servers (each mail server has its own private key that matches only one public key). The recipient’s mail server uses an attribute called selector of the DKIM signature to find the correct public key in the sender’s DNS zone.

A DKIM record must follow the syuntax : [selector]._domainkey.[domain]. and the query on may only result in one TXT type record maximum.

Here is an example of a DKIM record:

mail._domainkey.example.com. 86400 IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG...

DKIM keys are not meant to be changed frequently. A high time-to-live (TTL) value of 86400 seconds or more is not uncommon.

For detailed information about fields in a DKIM record please check the RFC 6376.

vSMTP implementation

vSMTP can act as DKIM signer or verifier.

Results of Evaluation

The vSMTP DKIM verifier implements results semantically equivalent to the RFC.

ResultDescription
noneThe message was not signed.
passThe message was signed, the signature or signatures were acceptable to the ADMD, and the signature(s) passed verification tests.
failThe message was signed and the signature or signatures were acceptable to the ADMD, but they failed the verification test(s).
policyThe message was signed, but some aspect of the signature or signatures was not acceptable to the ADMD.
neutralThe message was signed, but the signature or signatures contained syntax errors or were not otherwise able to be processed. This result is also used for other failures not covered elsewhere in this list.
temperrorThe message could not be verified due to some error that is likely transient in nature, such as a temporary inability to retrieve a public key. A later attempt may produce a final result.
permerrorThe message could not be verified due to some error that is unrecoverable, such as a required header field being absent. A later attempt is unlikely to produce a final result.

Results headers

Results should be recorded in the message header before any existing DKIM-Signature or preexisting authentication status header fields in the header field block.

There’s no specific DKIM header. The Authentication-Results header described in RFC 8601 should be used.

Authentication-Results: example.com;
            dkim=pass (good signature) header.d=example.com
Received: from mail-router.example.com
                (mail-router.example.com [192.0.2.1])
            by auth-checker.example.com (8.11.6/8.11.6)
                with ESMTP id i7PK0sH7021929;
            Fri, Feb 15 2002 17:19:22 -0800
DKIM-Signature:  v=1; a=rsa-sha256; s=gatsby; d=example.com;
            t=1188964191; c=simple/simple; h=From:Date:To:Subject:
            Message-Id:Authentication-Results;
            bh=sEuZGD/pSr7ANysbY3jtdaQ3Xv9xPQtS0m70;
            b=EToRSuvUfQVP3Bkz ... rTB0t0gYnBVCM=

DKIM failure codes

The RFC 7372 “Email Auth Status Codes” introduces new status codes to report the DKIM and SPF mechanisms results.

CodeX.7.20
TextNo passing DKIM signature found
Basic status code550
DescriptionA message did not contain any passing DKIM signatures.
CodeX.7.21
TextNo acceptable DKIM signature found
Basic status code550
DescriptionA message contains one or more passing DKIM signatures, but none are acceptable.
CodeX.7.22
TextNo valid author-matched DKIM signature found.
Basic status code550
DescriptionA message contains one or more passing DKIM signatures, but none are acceptable because none have an identifier(s) that matches the author address(es) found in the From header field.

The following error codes can also be sent by the DKIM framework.

CodeX.7.25
TextReverse DNS validation failed
Basic status code550
DescriptionAn SMTP client’s IP address failed a reverse DNS validation check, contrary to local policy requirements.
Used in place ofn/a
CodeX.7.26
TextMultiple authentication checks failed
Basic status code500
DescriptionA message failed more than one message authentication check, contrary to local policy requirements. The particular mechanisms that failed are not specified.
Used in place ofn/a

vSL predefined function

The standard vSL API has dedicated functions to handle the DKIM protocol.

Check the Security chapter to get the full documentation about verify_dkim.

Domain-based Message Authentication, Reporting and Conformance

This document specifies the vSMTP implementation of the Domain-based Message Authentication, Reporting and Conformance (DMARC) protocol described in RFC 7489.

DMARC allows email administrators to prevent hackers from impersonating their organization. This type of attack is also called “spoofing” because the message appears to come from the spoofed organization or domain.

In order to counter spoofing attacks, DMARC uses SPF and DKIM protocols.

When a message appears to come from an organization, but does not pass authentication checks or does not meet the authentication criteria, DMARC policy tells mail servers what action to perform.

The DMARC implementation of vSMTP support the Mail Receiver part, and apply the policy specified in the DNS record.

This rule will conducts SPF and DKIM authentication checks by passing the necessary data to their respective modules. The results of these are passed to the DMARC module along with the Author’s domain. The DMARC module attempts to retrieve a policy from the DNS for that domain. If a policy is found, it is combined with the Author’s domain and the SPF and DKIM results to produce a DMARC policy result (“pass” or “fail”).

#{
  preq: [
    rule "check dmarc" || check_dmarc(),
  ]
}

DMARC reporting and DMARC feedback system is not implemented.

SMTP Security via Opportunistic DNS-Based Authentication of Named Entities (DANE) Transport Layer Security (TLS)

This is a DRAFT for future releases. Support is planned for Q3/2022.

DNS-based Authentication of Named Entities (DANE) prevents snooping of email traffic by requiring the use of TLS encryption whenever possible during transport.

A client using DANE requests the public key of a server through the Domain Name System SECurity Extensions (DNSSEC) protocol.

DANE introduces the DNS “TLSA” resource record (RR) type. This record associate a certificate or a public key of an end-entity or a trusted issuing authority with the corresponding Transport Layer Security (TLS) endpoint.

Trusted public certificate are not needed anymore.

vSMTP implementation

DANE TLSA Record Overview

TLSA resource records are stored at a prefixed DNS domain name. The records includes four fields. The record type is determined by the values of the first three fields:

  • The Certificate Usage field: PKIX-TA(0), PKIX-EE(1), DANE-TA(2), and DANE-EE(3).
  • The Selector field: Cert(0) and SPKI(1).
  • The Matching Type field : Full(0), SHA2-256(1), and SHA2-512(2).

The fourth field (Certificate Association Data) stores either the full value, the digest of the certificate or the subject public key, as determined by the matching type and selector, respectively.

                                      |------- Certificate usage
              |-- Base domain name    | |----- Selector  
              |                       | | |--- Matching type  
              v                       v v v
   _25._tcp.mail.example.com. IN TLSA 2 0 1 (
    ^     ^                            E8B54E0B4BAA815B06D3462D65FBC7C0
    |     |-- Protocol                 CF556ECCF9F5303EBFBB77D022F834C0 )
    |                                      ^
    |-- Port                               |----- Certificate Association Data

vSMTP supports only DANE-TA(2) and DANE-EE(3) certificate usage.

This is implement by vSMTP using key = valueand key = value in the TOML configuration file.

DANE TLS Requirements

TLS clients using DANE MUST support the SNI extension of TLS. Servers may support SNI and respond with a matching certificate chain but may also ignore SNI and respond with a default certificate chain. When a server supports SNI but is not configured with a certificate chain that exactly matches the SNI extension of the client, the server should respond with another certificate chain (a default or closest match), because clients might support more than one server name but can only store a single name in the SNI extension.

These behaviors are set with the key = valueand key = value in the TOML configuration file.

Digest Algorithm Agility

As described in the RFC :

“Client implementations SHOULD implement a default order of digest algorithms by strength This order SHOULD be configurable by the administrator or user of the client software. If possible, a configurable mapping from numeric DANE TLSA matching types to underlying digest algorithms provided by the cryptographic library SHOULD be implemented to allow new matching types to be used with software that predates their introduction. Configurable ordering of digest algorithms SHOULD be extensible to any new digest algorithms.”

It is implemented by vSMTP using key = valueand key = value in the TOML configuration file.

Algorithm agility is to be applied after first discarding any unusable or malformed records (unsupported digest algorithm, or incorrect digest length). For each usage and selector, the client SHOULD process only any usable records with a matching type of Full(0) and the usable records whose digest algorithm is considered by the client to be the strongest among usable records with the given usage and selector.

It is implemented by vSMTP using key = valueand key = value in the TOML configuration file.

Managing vSMTP from the command line

Starting vSMTP

vSMTP is designed to run as a Unix service and is not intended to be run interactively using the command lines. However, in case of startup problems, it can be useful to run it with a minimal configuration file to check the settings. In any case, vSMTP must be started with root privileges.

$ sudo vsmtp -c /etc/vsmtp/vsmtp-minimal.toml
2022-05-11 17:16:40.609916181 WARN  [139916115511680] server::rule_engine            $ No 'main.vsl' provided in the config, the server will deny any incoming transaction by default.
2022-05-11 17:16:40.622996178 WARN  [139915467675200] vsmtp_server::server           $ No TLS configuration provided, listening on submissions protocol (port 465) will cause issue

Managing configuration

$ vsmtp --help
vsmtp 1.0.0
Team viridIT <https://viridit.com/>
Next-gen MTA. Secured, Faster and Greener

USAGE:
    vsmtp [OPTIONS] [SUBCOMMAND]

OPTIONS:
    -c, --config <CONFIG>      Path of the vSMTP configuration file (toml format)
    -h, --help                 Print help information
    -n, --no-daemon            Do not run the program as a daemon
    -t, --timeout <TIMEOUT>    Make the server stop after a delay (human readable format)
    -V, --version              Print version information

SUBCOMMANDS:
    config-diff    Show the difference between the loaded config and the default one
    config-show    Show the loaded config (as serialized json format)
    help           Print this message or the help of the given subcommand(s)

Loaded configurations can be checked using config-diff and config-show subcommands.

$ sudo vsmtp -c /etc/vsmtp/vsmtp.toml config-show
Loading configuration at path='/etc/vsmtp/vsmtp.toml'
Loaded configuration: {
  "server": {
    "domain": "testserver.com",
    "addr": "0.0.0.0:25",
    "addr_submission": "0.0.0.0:587",
    "addr_submissions": "0.0.0.0:465",
    "thread_count": 10
  },
  "log": {
    "file": "/var/log/vsmtp/vsmtp.log",
    "level": {
      "receiver": "INFO",
      "rules": "WARN",
      "default": "WARN",
      "resolver": "WARN"
    }
  },
 
  "reply_codes": {
    "Code214": "214 my custom help message\r\n",
    "Code220": "220 testserver.com ESMTP Service ready\r\n",
   
    ... // etc.
  }
}
$ sudo vsmtp -c /etc/vsmtp/vsmtp.toml config-diff
Loading configuration at path='/etc/vsmtp/vsmtp.toml'
 {
   "server": {
     "domain": "testserver.com",
     "addr": "0.0.0.0:25",
     "addr_submission": "0.0.0.0:587",
     "addr_submissions": "0.0.0.0:465",
-    "thread_count": 2                      // DEFAULT configuration
+    "thread_count": 10                     // CURRENT configuration
   },
   "log": {
-    "file": "./trash/log.log",             // DEFAULT configuration            
+    "file": "/var/log/vsmtp/vsmtp.log",    // CURRENT configuration
     "level": {
-      "default": "OFF"                     
+      "resolver": "WARN",                  
+      "default": "WARN",                   // etc.
+      "rules": "WARN",
+      "receiver": "INFO"
     }
   },

   "reply_codes": {
-    "Code214": "214 joining us https://viridit.com/support\r\n",
-    "Code220": "220 testserver.com Service ready\r\n",
+    "Code214": "214 my custom help message\r\n",
+    "Code220": "220 testserver.com ESMTP Service ready\r\n",

        ... // etc.
    }
 }

Managing queues

Internal and user defined queues can be managed using the vqueue command with root privileges.

The vqueue show subcommand displays a summary of vSMTP queues in a Postfix qshape way. The -c option allows vqueue to parse queues and quarantines defined in the TOML configuration file.

WORKING    is at '/var/spool/vsmtp/working' : <EMPTY>

DELIVER    is at '/var/spool/vsmtp/deliver' :
                T    5   10   20   40   80  160  320  640 1280 1280+
       TOTAL    4    0    0    0    0    0    0    0    0    4    0
example1.com    1    0    0    0    0    0    0    0    0    1    0
example2.com    1    0    0    0    0    0    0    0    0    1    0
example3.com    1    0    0    0    0    0    0    0    0    1    0
example4.com    1    0    0    0    0    0    0    0    0    1    0

DEFERRED   is at '/var/spool/vsmtp/deferred' : <EMPTY>

DEAD       is at '/var/spool/vsmtp/dead' : <EMPTY>

Managing messages

Like queues, messages are also managed using the vqueue command.

Features available in v0.10:

  • vqueue msg <msg-id> show [json | eml] : Print the content of a message.
  • vqueue msg <msg-id> move <queue> : Move a message to a queue.
  • vqueue msg <msg-id> remove : Remove a message from disk.

Feature planned:

  • vqueue msg <msg-id> re-run : Reintroduce a message in the delivery system (and reevaluate its status).
  • User defined quarantine queues inspection.

Complete vSMTP TOML key/value list

The behavior of your server can be configured using a configuration file, and using the -c, --config flag of the vsmtp.

All the parameters are optional and have default values. If -c, --config is not provided, the default values of the configuration will be used.

The configuration file will be read and parsed right after starting the program, producing an error if there is an invalid syntax, a filepath failed to be opened, or any kind of errors.

If you have a non-explicit error when you start your server, you can create an issue on the github repo, or ask for help in our discord server.

Format

The configuration file format is TOML.

Examples

You will find examples here and the technical documentation here.

Parameters

version_requirement

The only mandatory parameter to provide. This parameter is follow the semver format. It is used to specify the version require of vsmtp to parse successfully this file.

Example:

version_requirement = ">=1.0.0, <2.0.0"

server.domain

Example:

[server]
domain = "mydomain.com"

server.client_count_max

[server]
client_count_max = 16

server.system

[server.system]
user = "vsmtp"
group = "vsmtp"
group_local = "vsmtp"

server.system.thread_pool

[server.system.thread_pool]
receiver = 6
processing = 6
delivery = 6

server.interfaces

vSMTP can be used as a MTA and/or a MSA.

A MTA listen on port 25, and using server side authentication (SPF / DKIM / DMARC …) to be protected from spam and identity substitution.

A MSA listen on port 587 and 465 (with TLS tunnel), in the same fashion than HTTP over port 80 and HTTPS over 443. On these ports, stronger policies apply and client side authentication (SASL) is required.

Expose your public addresses here, (support both ipv4 and ipv6) :

[server.interfaces]
addr = ["192.168.1.254:25"]                  # <---- MTA's addresses
addr_submission = ["192.168.1.254:587"]      # <---- MSA's addresses
addr_submissions = ["192.168.1.254:465"]     # <---- MSA's addresses for SMTPS

These field are arrays, and you can leave any of them empty. Make sure to provide at least one address, otherwise an error will be produced on startup.

You might want to add local address (127.0.0.1:25 for example) when using delegation services.

server.logs

[server.logs]
filepath = "/var/log/vsmtp/vsmtp.log"
level = [
    "info",
    "vsmtp_server::receiver=info",
    "vsmtp_rule_engine=warn",
    "vsmtp_delivery=error",
]

server.queues.dirpath

[server.queues]
dirpath = "/var/spool/vsmtp"

server.queues.working

[server.queues.working]
channel_size = 32

server.queues.delivery

[server.queues.delivery]
channel_size = 32
deferred_retry_max = 100
deferred_retry_period = "5m"

server.tls

[server.tls]
security_level = "Encrypt"
preempt_cipherlist = false
handshake_timeout = "200ms"
protocol_version = "TLSv1.3"
certificate = "/etc/vsmtp/tls/domain.com.certificate.crt"
private_key = "/etc/vsmtp/tls/domain.com.private_key.key"

server.virtual

[server.virtual."mta1.domain.com"]

[server.virtual."mta2.domain.com".dns]
type = "system"

[server.virtual."mta3.domain.com".tls]
protocol_version = "TLSv1.3"
certificate = "/etc/vsmtp/tls/mta3.domain.com.certificate.crt"
private_key = "/etc/vsmtp/tls/mta3.domain.com.private_key.key"

[server.virtual."mta4.domain.com".tls]
protocol_version = "TLSv1.3"
certificate = "/etc/vsmtp/tls/mta4.domain.com.certificate.crt"
private_key = "/etc/vsmtp/tls/mta4.domain.com.private_key.key"

[server.virtual."mta4.domain.com".dns]
type = "google"

server.smtp

[server.smtp]
rcpt_count_max = 1000
disable_ehlo = false
required_extension = ["STARTTLS", "SMTPUTF8", "8BITMIME", "AUTH"]

server.smtp.error

[server.smtp.error]
soft_count = 10
hard_count = 20
delay = "5s"

server.smtp.timeout_client

[server.smtp.timeout_client]
connect = "5m"
helo = "5m"
mail_from = "5m"
rcpt_to = "5m"
data = "5m"

server.smtp.auth

[server.smtp.auth]
must_be_authenticated = true
enable_dangerous_mechanism_in_clair = true
mechanism = ["PLAIN", "LOGIN", "CRAM-MD5", "ANONYMOUS"]

server.dns

[server.dns]
type = "custom"

[server.dns.config]
domain = "example.dns.com"
search = ["example.dns.com"]
name_servers = []

[server.dns.options]
# Sets the number of dots that must appear (unless it's a final dot representing the root)
#  that must appear before a query is assumed to include the TLD. The default is one, which
#  means that `www` would never be assumed to be a TLD, and would always be appended to either
#  the search
ndots = 1
# Number of retries after lookup failure before giving up. Defaults to 2
attempts = 2
# Rotate through the resource records in the response (if there is more than one for a given name)
rotate = false
# Enable edns, for larger records
edns0 = false
# Use DNSSec to validate the request
validate = false
# The ip_strategy for the Resolver to use when lookup Ipv4 or Ipv6 addresses
ip_strategy = "Ipv4thenIpv6"
# Cache size is in number of records (some records can be large)
cache_size = 32
# Check /ect/hosts file before dns requery (only works for unix like OS)
use_hosts_file = true
# Number of concurrent requests per query
#
# Where more than one nameserver is configured, this configures the resolver to send queries
# to a number of servers in parallel. Defaults to 2; 0 or 1 will execute requests serially.
num_concurrent_reqs = 2
# Preserve all intermediate records in the lookup response, suchas CNAME records
preserve_intermediates = true
# Try queries over TCP if they fail over UDP.
try_tcp_on_error = false

app.dirpath

[app]
dirpath = "/var/spool/vsmtp/app"

app.vsl

[app.vsl]
filepath = "/etc/vsmtp/main.vsl"

app.logs

[app.logs]
filepath = "/var/log/vsmtp/app.log"
level = "WARN"
format = "{d} - {m}{n}"
size_limit = 10485760
archive_count = 10

vSL - the vSMTP Scripting Language

vSL is a lightweight scripting language dedicated to email filtering. It is based on the Rhai scripting language.

The entry point for vSMTP is a file defined in the configuration file (/etc/vsmtp/rules/main.vsl usually). This file must be evaluated as a Rhai map, composed of keys and values.

To avoid repetitive code in your logics, you can define objects, use the vsl api and use dedicated services to interact with third-party software.

A /etc/vsmtp/rules/main.vsl file can grow large is you define a lot of logics, but you can split all your .vsl in severals files using Rhai modules.

Store all you .vsl files in the /etc/vsmtp/rules folder and use a version control system such as git.

Syntax highlighting is available for Microsoft VSCode IDE, using the Rhai extension.

vSL stages and SMTP states

vSMTP interacts with the messaging transaction at all states defined in the SMTP protocol.

At each step, vSL updates a global context containing transaction and mail data.

vSMTP stages

Available stages in order of evaluation:

StageSMTP stateContext available
connectBefore HELO/EHLO commandConnection related information.
heloAfter HELO/EHLO commandHELO string.
mailAfter MAIL FROM commandSender address.
rcptAfter each RCPT TO commandThe entire SMTP envelop.
preqBefore queuing1The entire mail.
postqAfter queuing2The entire mail.
deliveryBefore deliveringThe entire mail.
1

Preq stage triggers after the end of receiving data from the client, just before the server answers back with a 250 code.

2

Postq stage triggers after the preq stage, when the connection closes and the SMTP code is sent to the client.

Stages are declared in a main.vsl file like below:

#{
    connect: [
        // rules, actions, delegations ...
    ],

    mail: [
        // rules, actions, delegations ...
    ],

    postq: [
        // rules, actions, delegations ...
    ],

    // other stages ...
}

Stages do not need to be declared in the previous given order, but it is a good practice.

Before queueing vs. after queueing

vSMTP can process mails before the incoming SMTP mail transfer completes and thus rejects inappropriate mails by sending an SMTP error code and closing the connection.

The advantages of an early detection of unwanted mails are:

  • The responsibility is on the remote SMTP client side.
  • It consumes less CPU and disk resources.
  • The system is more reactive.

However, as the SMTP transfer must to be completed within a deadline, heavy workload may cause a system to fail to respond in time.

To protect against bursts and crashes, vSMTP implements several internal mechanisms like ‘delay-variation’ or ‘temporary service unavailable messages’, in conformance with the SMTP RFCs.

Context variables

As described above, depending on the stage, vSL exposes data to the end user. Check out both Connection and Transaction modules.

Connection vs mail transaction

As defined in the SMTP RFCs, a single connection can handle several mail transactions.

[... connection from an IP]
HELO                                    # Start of SMTP transaction
    > MAIL FROM > RCPT TO > DATA        # First mail
    > MAIL FROM > RCPT TO > DATA        # Second mail
    > [...]
QUIT                                    # End of transaction

☢ | The “mail context” (data obtained from the Connection and Transaction modules) is unique for each incoming connection.

Rules, actions & delegate

Rules are the entry point to interact with the SMTP traffic at a user level.

Overall Syntax

Rules and actions are quite similar but rules must return a vSL rule engine status. They are defined in the same way:

action "action name" || {
    // ... action body.
}
rule "rule name" || {
    // ... rule body.
    accept() // a rule returns a rule engine status

}

Rule engine status and effects are listed in the API, in the status module.

An inline syntax is also available, like below:

action "name" || instruction,
rule "name" || instruction,

An example of a rule:

// Inline rule that only accepts a client at 192.168.1.254
rule "check connect" || if client_ip() == "192.168.1.254" { next() } else { deny() }

The same rule, including a string interpolation in a log:

rule "check connect" || {
    log("warn", `Connection from : ${client_ip()}`);
    if client_ip() == "192.168.1.254" { next() } else { deny() }
}

The delegate directive is different: it uses a smtp service to delegate the email to a third party software:

service third_party smtp = #{
    delegator: #{
        address: "127.0.0.1:10026",
        timeout: "60s",
    },
    receiver: "127.0.0.1:10024",
};

delegate third_party "delegate email processing" || { ... }

Check out the Policy delegation chapter for an in-depth description of the delegation mechanism.

Rules and vSMTP Stages

Rules are bound to a vSMTP stage. Stages that are not used can be omitted, but must appear only once if used. They are declared in the main.vsl file.

// -- objects.vsl

object my_company fqdn = "mycompany.net";

//-- main.vsl

import "objects" as obj;

#{
    connect: [
        action "log connect" || log("warn", `Connection from : ${client_ip()}`),
        rule "check connect" || if client_ip() == "192.168.1.254" { next() } else { deny() },
    ],

    rcpt: [
        rule "local_domain" || {
            if obj::my_company == rcpt().domain { next() } else { deny() }
        },
    ],

    preq: [
        action "rewrite recipients" || {
            rewrite_rcpt_envelop("johndoe@compagny.com", "john.doe@company.net");
            remove_rcpt("customer@company.net");
            add_rcpt("no-reply@company.net");
        },
    ],

    // ... other rules & actions
}

Implicit rules

For security purpose, end-users should always add a trailing rule at the end of a stage. However, to avoid undefined behavior, an implicit trailing rule is set to next(), moving the rule engine to the next stage.

//-- objects.vsl

object my_company fqdn = "mycompany.net";

//-- main.vsl
import "objects" as obj;

#{
    rcpt: [
        rule "local domain" || {
            if obj::my_company == rcpt().domain { accept() } else { next() }
        },

        // ... other rules / actions

        // Trailing rule (denying is the default behavior for rcpt stage)
        rule "default" || deny(),
    ]
}

As with firewall rules, the best practice is to deny “everything” and only accept authorized and known clients (like the example above).

Action

Actions are similar to rules but do not return any status code and thus cannot modify the state of a transaction.

// action "<name>" || {
//     // <action body>
// }

action "log incoming transaction" || {
  // We use actions to execute code that does not
  // need to change the state of the transaction.
  log("debug", `new transaction by ${client_ip()}`);
}

Objects

Objects are declared through the “object” keyword. Two syntax are available. The inline syntax:

object <name> <type> = "<value>";
object my_host ip4 = "192.168.1.34";
object local_domain fqdn = "foo.bar";

The extended syntax, allowing the use of user-defined fields:

object <name> <type> = #{
    value: "value",
    <field1>: "value",
    ...
    <fieldn>: "value"
};
object local_mda ip4 = #{
    value: "192.168.0.34",
    color: "bbf3ab",
    description: "Internal delivery agent"
};

✎ | Only the “value:” field is mandatory. The other fields are not.

Type of implemented objects

The following type of objects are supported natively:

TypeDescriptionSyntaxComments
stringUntyped valuestringGeneric variable.
ip4IPv4 addressx.y.z.tDecimal values.
ip6IPv6 addressa:b:c:d:e:f:g:hHex values.
rg4IPv4 networkx.y.z.t/rgDecimal values.
rg6IPv6 prefixa:b:c:d:e:f:g:h/rgHex values.
addressEmail addressidentifier@fqdnString.
identifierLocal part of an addressuserString.
fqdnFully qualified domain namemy.domain.comString.
regexRegular expressionPERL regular expression.
groupA group of objectsSee group section.
fileA file of objectsUnix fileSee file section.
codea custom smtp codeSee code section.

About files

File objects are standard Unix text files containing values delimited by CRLF. Only one type of object is authorized and must be declared after the keyword “file:”.

object local_MTA file:ip4 = "/etc/vsmtp/config/local_mta.txt";
cat /etc/vsmtp/config/local_mta.txt
# 192.168.1.10
# 192.168.1.12
# 10.3.4.240

About groups

Groups are collections of objects. They can store references to other objects, store fresh objects or mix any type of object inside.

Unlike objects where fields are declared between parentheses, groups use squared brackets.

object whitelist file:address = "/etc/vsmtp/config/whitelist.txt";

object authorized_users group = [
  whitelist,
  object admin address = "admin@mydomain.com",
];

Groups can be nested into other groups.

object deep_group group = [
  object foo_emails regex = "^[a-z0-9.]+@foo.com$",
  authorizedUsers,
];

✎ | When used with check operators (==, !=, in etc …), the whole group will be tested. The test stops when one of the groups content matches.

About codes

custom codes can be declared with the following syntax.

object code554_7_1 code = #{
  base: 554,
  enhanced: "5.7.1",
  text: "Relay access denied"
};

// use the code in your rules. deny or send a informational message.
deny(code554_7_1);
info(code554_7_1);

Pre-defined objects

vSL already exposes some objects for you to use. You can check out the Variable file to get documentation on those objects and their use.

Delivery sub-system

The delivery subsystem uses specific actions. They can be called at any vSMTP stages.

Delivering local mails

The incoming mail traffic can locally be delivered using :

☢ | The Local Mail Transfer Protocol (LMTP) is currently not implemented.

Delivering distant mails

vSMTP uses a well known and secured third-party software Lettre written in Rust.

Documentation

Check out the Delivery module documentation.

Services

Services are mainly used to interact with third party software. Services are declared using the service keyword, followed by its name, and its type.

service <name> <type> = #{
    <field1>: "value",
    <field2>: "value",
    ...
    <fieldn>: "value"
};

The command type

This type lets you execute Unix shell commands.

service clamscan cmd = #{
    // Time allowed to execute the command.
    // Command is aborted if the timeout value is reached
    timeout: "60s",
    // The user to execute the command with. (optional)
    user: "user",
    // The group to execute the command with. (optional)
    group: "group",
    // Name of the command to execute.
    command: "command-to-execute",
    // Array of arguments to execute the command with.
    args: ["--arg1", "--arg2", "--arg3"],
};

See the Time chapter for more information on available time scale formats for the timeout field.

Example

service clamscan cmd = #{
    // Time allowed to execute the command. Command is aborted if reached.
    timeout: "10s",
    // Name of the command to execute.
    command: "clamscan",
    // Array of arguments to execute the command with.
    args: ["--infected", "--remove", "--recursive", "/home/jdoe"],
};

// run the service.
// the command executed will be:
// clamscan --infected --remove --recursive /home/jdoe
clamscan.run_cmd();
// run the service with custom arguments (based one are replaced).
// clamscan --infected /home/another
clamscan.run_cmd([ "--infected", "/home/another" ]);

The database type

The db service allows connection and operations on databases. Several subtypes are available.

service <name> <db:subtype> = #{
    ...
};

CSV database

CSV databases are declared this way:

service greylist db:csv = #{
    // The path to the csv database.
    connector: "/db/user_accounts.csv",
    // The access mode of the database. Can be:
    // `O_RDONLY`, `O_WRONLY` or `O_RDWR`.
    access: "O_RDONLY",
    // The refresh mode of the database.
    // Can be "always" (database is always refreshed once queried)
    // or "no" (database is readonly and never refreshed).
    //
    // WARNING: using the "always" option can make vsmtp really slow,
    //          because it has to pull the whole database in memory every
    //          time it is queried. Use it only if you have a small database
    //          or if the database is read only.
    refresh: "always",
    // The delimiter character used in the csv file.
    delimiter: ",",
};

// query & update the database.
let john = greylist.get("john");
greylist.set(["new", "user", "new.user@example.com"]);
greylist.rm("green");

// manipulating a record.

// ["john", "doe", "john.doe@example.com"]
print(john);

// records are stored in vsl arrays.
// to get a field in a record, simply use it's index.

// "john.doe@example.com"
print(john[2]);

MySQL Database

Using Rhai arrays and maps, vSL can easily fetch and update data from a mysql database.

Again taking previous “greylisting” as an example, a database named “greylist”, with a table “sender” described as follows is created:

+---------+--------------+------+-----+---------+
| Field   | Type         | Null | Key | Default |
+---------+--------------+------+-----+---------+
| address | varchar(500) | NO   | PRI | NULL    |
| user    | varchar(500) | NO   |     | NULL    |
| domain  | varchar(500) | NO   |     | NULL    |
+---------+--------------+------+-----+---------+

To connect to the database, create a “mysql_greylist” service of type db:mysql.

service mysql_greylist db:mysql = #{
    // the url to connect to your database.
    url: "mysql://localhost/",
    // the user to use when connecting. (optional)
    user: "guest",
    // the password for the user. (optional)
    password: "1234",
    // the number of connections to open on your database. (optional, 4 by default)
    connections: 4,
    // the time allowed to the database to send a
    // response to your query. (optional, 30s by default)
    timeout: "3s",
};

This service is used to query and update the database using SQL commands.

// Query the database.
let senders = mysql_greylist.query("SELECT * FROM greylist.sender;");

// Like the csv database, the `query` function of the mysql database return
// an array of records, except that each record is a Rhai Map, meaning that
// you can access the record fields using their names.
//
// vSL will then return fetched records using this form:
// Array [
//     Map #{
//         "user": "john.doe",
//         "domain": "example.com",
//         "address": "john.doe@example.com",
//     },
//     Map #{
//         "user": "green",
//         "domain": "test.com",
//         "address": "green@test.com",
//     },
// ]
//
// (We assume that the "sender" table is populated with two records in the above example)
//
// To extract records and fields, use the syntax below.
print(`first sender address : ${senders[0].address}`); // will print "john.doe@example.com";
print(`second sender domain : ${senders[1].domain}`); // will print "test.com";

// We can also update the database this way:
let sender = mail_from();
mysql_greylist.query(`INSERT INTO greylist.sender (user, domain, address) values (${sender.local_part}, ${sender.domain}, ${sender.address});`);

The smtp type

The smtp type allows the vSL delegate directive to delegate the email to another service via the smtp protocol. The example hereunder explains how to delegate to ClamAV antivirus through its SMTP proxy (clamsmtpd).

// -- service.vsl
service clamsmtpd smtp = #{
    delegator: #{
        // The service address to delegate to.
        address: "127.0.0.1:10026",
        // The time allowed between each message.
        timeout: "60s",
    },
    // The address where vsmtp will gather the results of the delegation.
    receiver: "127.0.0.1:10024",
};

// -- main.vsl

// you cannot use `import "service" as service;` here because `service` is
// a reserved keyword.
import "service" as svc;

#{
    postq: [
        // this will delegate the email using the `clamsmtpd` service.
        delegate svc::clamsmtpd "delegate antivirus processing" || {
            // this is executed after the delegation results have been
            // received on port 10024.
            ...
        }
    ]
}

Check out the Services chapter for the full list of functions for services.

Time

Some objects and functions require argument of time. Here is a list of available time scales format that you can use.

Time scaleExpression
nanoseconds“nanos” / “nsec” / “ns”
microseconds“usec” / “us”
milliseconds“millis” / “msec” / “ms”
seconds“seconds” / “second” / “secs” / “sec” / “s”
minutes“minutes” / “minute” / “min” / “mins” / “m”
hours“hours” / “hour” / “hr” / “hrs” / “h”
days“days” / “day” / “d”
weeks“weeks” / “week” / “w”
months“months” / “month” / “M”
years“years” / “year” / “y”

For reference, humantime crate is used to parse time.

Here is an example:

service clamscan cmd = #{
    // for the `cmd` service, you can specify a timeout for the command.
    // You can use the different time scales above to specify the time.
    timeout: "10s",
    // timeout: "200usec",
    // timeout: "1minute",
    // timeout: "10000nsec",
    // ...
};

Advanced scripting

Using Rhai language for programming complex actions

On top of vSL predefined actions, users can define complex rules using the Rhai scripting language.

action "let example" || {
    let my_string = "The question is 7x6 = 42 ?";
    // ... do stuff

    log("error", `I'm writing this string : ${my_string} as an error`);
};

Rhai functions can be declared and used in vSL.

fn my_condition() {
    let my_int = if client_ip() == "192.168.1.34" { 42 } else { 0 };

    if (my_int == 42) {
        true
    } else {
        false
    }
}

fn my_action1() {
    log("warn", "Ok - coming from localhost");
    next()
}

fn my_action2(rcpts) {
    let admin = "admin@foobar.com";
    log("error", `Not from localhost. Logging the recipients's list: ${rcpts}`);

    for rc in rcpts {
      log("debug", `  - ${rc}`);
    }

    next()
}


#{
    // ... other rules.
    rcpt: [
        rule "rcpt log" || { if my_condition() { my_action1() } else { my_action2(rcpt_list()) } },
    ]
}

✎ | Rhai’s function are “pure”, they do not capture their external scope. Variables must be passed as arguments.

Importing user defined modules

External modules can be imported in the main.vsl file.

Rhai functions are automatically exported. Therefore do not forget to add the private keyword for internal functions. Unlike functions, variables are not exported. You must do it manually using the export keyword. Check out the Rhai Book for more information.

Example :

// -- mod/my_module.vsl
fn my_function() {
    let z = add_function(24);
    // ... do stuff.
}

private fn add_function(v) {
    return v + 42;
}

export const x = 42;
// -- main.vsl
import "mod/my_module" as my_mod;

my_mod::my_function();
print(my_mod::x);

// add_function(33) -> won't work because `add_function` is private.

vSL’s API

vSL, on top of the Rhai language, exposes it’s own api in the form of functions and objects. The following files are auto generated documentation for each of vSL’s modules.

Status

The state of an SMTP transaction can be changed through specific functions from this module.

fn accept()

Tell the rule engine to accept the incoming transaction for the current stage. This means that all rules following the one `accept` is called in the current stage will be ignored.

Effective smtp stage

all of them.

Example

#{
    connect: [
        // "ignored checks" will be ignored because the previous rule returned accept.
        rule "accept" || accept(),
        action "ignore checks" || print("this will be ignored because the previous rule used accept()."),
    ],

    mail: [
        // rule evaluation is resumed in the next stage.
        rule "resume rules" || print("evaluation resumed!");
    ]
}


fn deny()

Stop rules evaluation and/or send an error code to the client. The code sent is `554 - permanent problems with the remote server`.

Effective smtp stage

all of them.

Example

#{
    rcpt: [
        rule "check for satan" || {
           // The client is denied if a recipient's domain matches satan.org,
           // this is a blacklist, sort-of.
           if rcpt().domain == "satan.org" {
               deny()
           } else {
               next()
           }
       },
    ],
}


fn deny(code)

Stop rules evaluation and/or send a custom code to the client.

Effective smtp stage

all of them.

Example

#{
    rcpt: [
        rule "check for satan" || {
           // a custom error code can be used with `deny`.
           object error_code code = #{ code: 550, enhanced: "", text: "satan.org is not welcome here." };

           // The client is denied if a recipient's domain matches satan.org,
           // this is a blacklist, sort-of.
           if rcpt().domain == "satan.org" {
               deny(error_code)
           } else {
               next()
           }
       },
    ],
}


fn faccept()

Tell the rule engine to force accept the incoming transaction. This means that all rules following the one `faccept` is called will be ignored.

Use this return status when you are sure that the incoming client can be trusted.

Effective smtp stage

all of them.

Example

#{
    connect: [
        // Here we imagine that "192.168.1.10" is a trusted source, so we can force accept
        // any other rules that don't need to be run.
        rule "check for trusted source" || if client_ip() == "192.168.1.10" { faccept() } else { next() },
    ],

    // The following rules will not be evaluated if `client_ip() == "192.168.1.10"` is true.
    mail: [
        rule "another rule" || {
            // ... doing stuff
        }
    ],
}




fn info(code)

Ask the client to retry to send the current command by sending an information code.

Effective smtp stage

all of them.

Example

#{
    connect: [
        rule "please retry" || {
           object info_code code = #{ code: 451, enhanced: "", text: "failed to understand you request, please retry." };
           info(info_code)
       },
    ],
}


fn next()

Tell the rule engine that a rule succeeded.

Effective smtp stage

all of them.

Example

#{
    connect: [
        // once "go to the next rule" is evaluated, the rule engine execute "another rule".
        rule "go to the next rule" || next(),
        action "another rule" || print("checking stuff ..."),
    ],
}


fn quarantine(queue)

Skip all rules until the email is received and place the email in a quarantine queue.

Args

  • queue - the relative path to the queue where the email will be quarantined. This path will be concatenated to the [app.dirpath] field in your vsmtp.toml.

Effective smtp stage

all of them.

Example

import "services" as svc;

#{
    postq: [
          delegate svc::clamsmtpd "check email for virus" || {
              // the email is placed in quarantined if a virus is detected by
              // a service.
              if has_header("X-Virus-Infected") {
                quarantine("virus_queue")
              } else {
                next()
              }
          }
    ],
}


Message

Those methods are used to query data from the email and/or mutate it.

fn add_rcpt_message(addr)

Add a recipient to the `To` header of the message.

Args

  • addr - the recipient address to add to the To header.

Effective smtp stage

preq and onwards.

Example

#{
    preq: [
       action "update recipients" || add_rcpt_message("john.doe@example.com"),
    ]
}


fn append_header(header, value)

Add a new header at the end of the header list in the message.

Args

  • header - the name of the header to append.
  • value - the value of the header to append.

Effective smtp stage

All of them. Even though the email is not received at the current stage, vsmtp stores new headers and will add them on top of the ones received once the preq stage is reached.

Example

#{
    postq: [
        action "append a header" || {
            append_header("X-JOHN", "received by john's server.");
        }
    ],
}


fn get_domain()

Get the domain of an email address.

Args

  • address - the address to extract the domain from.

Effective smtp stage

All of them.

Example

#{
    mail: [
        // You can also use the `get_domain(mail_from())` syntax.
        action "display sender's domain" || {
            log("info", `received a message from domain ${mail_from().domain}.`);
        }
    ],
}


fn get_domains()

Get all domains of the recipient list.

Args

  • rcpt_list - the recipient list.

Effective smtp stage

mail and onwards.

Example

#{
    mail: [
        action "display recipients domains" || {
            print("list of recipients domains:");

            // You can also use the `get_domains(rcpt_list())` syntax.
            for domain in rcpt_list().domains {
                print(`- ${domain}`);
            }
        }
    ],
}


fn get_header(header)

Get a specific header from the incoming message.

Args

  • header - the name of the header to get.

Return

  • string - the header value, or an empty string if the header was not found.

Effective smtp stage

All of them, although it is most useful in the preq stage because this is when the email body is received.

Example

#{
    postq: [
        action "display VSMTP header" || {
            print(get_header("X-VSMTP"));
        }
    ],
}


fn get_local_part()

Get the local part of an email address.

Args

  • address - the address to extract the local part from.

Effective smtp stage

All of them.

Example

#{
    mail: [
        // You can also use the `get_local_part(mail_from())` syntax.
        action "display mail from identity" || {
            log("info", `received a message from ${mail_from().local_part}.`);
        }
    ],
}


fn get_local_parts()

Get all local parts of the recipient list.

Args

  • rcpt_list - the recipient list.

Effective smtp stage

mail and onwards.

Example

#{
    mail: [
        action "display recipients usernames" || {
            print("list of recipients user names:");

            // You can also use the `get_local_parts(rcpt_list())` syntax.
            for user in rcpt_list().local_parts {
                print(`- ${user}`);
            }
        }
    ],
}


fn has_header(header)

Checks if the message contains a specific header.

Args

  • header - the name of the header to search.

Effective smtp stage

All of them, although it is most useful in the preq stage because this is when the email body is received.

Example

#{
    postq: [
        action "check for VSMTP header" || {
            if has_header("X-VSMTP") {
                log("info", "incoming message could be from another vsmtp server");
            }
        }
    ],
}


fn mail()

Get a copy of the whole email as a string.

Effective smtp stage

preq and onwards.

Example

#{
    postq: [
       action "display email content" || log("trace", `email content: ${mail()}`),
    ]
}


fn prepend_header(header, value)

Add a new header on top all other headers in the message.

Args

  • header - the name of the header to prepend.
  • value - the value of the header to prepend.

Effective smtp stage

All of them. Even though the email is not received at the current stage, vsmtp stores new headers and will add them on top of the ones received once the preq stage is reached.

Example

#{
    postq: [
        action "prepend a header" || {
            prepend_header("X-JOHN", "received by john's server.");
        }
    ],
}


fn remove_rcpt_message(addr)

Remove a recipient from the `To` header of the message.

Args

  • addr - the recipient to remove to the To header.

Effective smtp stage

preq and onwards.

Example

#{
    preq: [
       action "update recipients" || remove_rcpt_message("john.doe@example.com"),
    ]
}


fn rewrite_mail_from_message(new_addr)

Change the sender's address in the `From` header of the message.

Args

  • new_addr - the new sender address to set.

Effective smtp stage

preq and onwards.

Example

#{
    preq: [
       action "replace sender" || rewrite_mail_from_message("john.server@example.com"),
    ]
}


fn rewrite_rcpt_message(old_addr, new_addr)

Replace a recipient by an other in the `To` header of the message.

Args

  • old_addr - the recipient to replace.
  • new_addr - the new address to use when replacing old_addr.

Effective smtp stage

preq and onwards.

Example

#{
    preq: [
       action "rewrite recipient" || rewrite_rcpt_message("john.doe@example.com", "john-mta@example.com"),
    ]
}


fn set_header(header, value)

Replace an existing header value by a new value, or append a new header to the message.

Args

  • header - the name of the header to set or add.
  • value - the value of the header to set or add.

Effective smtp stage

All of them. Even though the email is not received at the current stage, vsmtp stores new headers and will add them on top to the ones received once the preq stage is reached.

Be aware that if you want to set a header value from the original message, you must use set_header in the preq stage and onwards.

Example

#{
    postq: [
        action "update subject" || {
            let subject = get_header("Subject");
            set_header("Subject", `${subject} (analyzed by vsmtp)`);
        }
    ],
}


Envelop

The SMTP envelop can be mutated by several function from this module.

fn add_rcpt_envelop(rcpt)

Add a new recipient to the envelop. Note that this does not add the recipient to the `To` header. Use `add_rcpt_message` for that.

Args

  • rcpt - the new recipient to add.

Effective smtp stage

All of them.

Example

#{
    connect: [
       // always deliver a copy of the message to "john.doe@example.com".
       action "rewrite envelop" || add_rcpt_envelop("john.doe@example.com"),
    ]
}


fn bcc(rcpt)

Add a recipient as a blind carbon copy. The equivalent of `add_rcpt_envelop`.

Args

  • rcpt - the recipient to add as a blind carbon copy.

Effective smtp stage

All of them.

Example

#{
    connect: [
       // set "john.doe@example.com" as a blind carbon copy.
       action "bcc" || bcc("john.doe@example.com"),
    ]
}


fn remove_rcpt_envelop(rcpt)

Remove a recipient from the envelop. Note that this does not remove the recipient from the `To` header. Use `remove_rcpt_message` for that.

Args

  • rcpt - the recipient to remove.

Effective smtp stage

All of them.

Example

#{
    preq: [
       // never deliver to "john.doe@example.com".
       action "rewrite envelop" || remove_rcpt_envelop("john.doe@example.com"),
    ]
}


fn rewrite_mail_from(new_addr)

Rewrite the value of the `MAIL FROM` command has well has the `From` header.

Args

  • new_addr - the new sender address to set.

Effective smtp stage

preq and onwards.

Example

#{
    preq: [
       action "rewrite sender" || rewrite_mail_from("john.doe@example.com"),
    ]
}


fn rewrite_mail_from_envelop(new_addr)

Rewrite the sender received from the `MAIL FROM` command.

Args

  • new_addr - the new sender address to set.

Effective smtp stage

mail and onwards.

Example

#{
    preq: [
       action "rewrite envelop" || rewrite_mail_from_envelop("unknown@example.com"),
    ]
}


fn rewrite_rcpt_envelop(old_addr, new_addr)

Replace a recipient received by a `RCPT TO` command.

Args

  • old_addr - the recipient to replace.
  • new_addr - the new address to use when replacing old_addr.

Effective smtp stage

rcpt and onwards.

Example

#{
    preq: [
       action "rewrite envelop" || rewrite_rcpt_envelop("john.doe@example.com", "john.main@example.com"),
    ]
}


Connection

Metadata is available for each client, this module lets you query those metadatas.

fn client_address()

Get the address of the client.

Effective smtp stage

All of them.

Return

  • string - the client’s address with the ip:port format.

Example

#{
    connect: [
       action "log info" || log("info", `${client_address()}`),
    ]
}


fn client_ip()

Get the ip address of the client.

Effective smtp stage

All of them.

Return

  • string - the client’s ip address.

Example

#{
    connect: [
       action "log info" || log("info", `${client_ip()}`),
    ]
}


fn client_port()

Get the ip port of the client.

Effective smtp stage

All of them.

Return

  • int - the client’s port.

Example

#{
    connect: [
       action "log info" || log("info", `${client_port()}`),
    ]
}


fn connection_timestamp()

Get a the timestamp of the client's connection time.

Effective smtp stage

All of them.

Return

  • timestamp - the connexion timestamp of the client.

Example

#{
    connect: [
       action "log info" || log("info", `${connection_timestamp()}`),
    ]
}


fn server_address()

Get the full server address.

Effective smtp stage

All of them.

Return

  • string - the server’s address with the ip:port format.

Example

#{
    connect: [
       action "log info" || log("info", `${server_address()}`),
    ]
}


fn server_ip()

Get the server's ip.

Effective smtp stage

All of them.

Return

  • string - the server’s ip.

Example

#{
    connect: [
       action "log info" || log("info", `${server_ip()}`),
    ]
}


fn server_name()

Get the name of the server.

Effective smtp stage

All of them.

Return

  • string - the name of the server.

Example

#{
    connect: [
       action "log info" || log("info", `${server_name()}`),
    ]
}


fn server_port()

Get the server's port.

Effective smtp stage

All of them.

Return

  • string - the server’s port.

Example

#{
    connect: [
       action "log info" || log("info", `${server_port()}`),
    ]
}


Transaction

At each SMTP stage, data from the client is received via ‘SMTP commands’. This module lets you query the content of the commands.

fn helo()

Get the value of the `HELO/EHLO` command sent by the client.

Effective smtp stage

helo and onwards.

Return

  • string - the value of the HELO/EHLO command.

Example

#{
    helo: [
       action "log info" || log("info", `${helo()}`),
    ]
}


fn mail_from()

Get the value of the `MAIL FROM` command sent by the client.

Effective smtp stage

mail and onwards.

Return

  • address - the sender address.

Example

#{
    helo: [
       action "log info" || log("info", `${mail_from()}`),
    ]
}


fn mail_timestamp()

Get the time of reception of the email.

Effective smtp stage

preq and onwards.

Return

  • string - the timestamp.

Example

#{
    preq: [
       action "receiving the email" || log("info", `time of reception: ${mail_timestamp()}`),
    ]
}


fn message_id()

Get the unique id of the received message.

Effective smtp stage

preq and onwards.

Return

  • string - the message id.

Example

#{
    preq: [
       action "message received" || log("info", `message id: ${message_id()}`),
    ]
}


fn rcpt()

Get the value of the current `RCPT TO` command sent by the client.

Effective smtp stage

rcpt and onwards. Please note that rcpt() will always return the last recipient received in stages after the rcpt stage. Therefore, this functions is best used in the rcpt stage.

Return

  • address - the address of the received recipient.

Example

#{
    rcpt: [
       action "log recipients" || log("info", `new recipient: ${rcpt()}`),
    ]
}


fn rcpt_list()

Get the list of recipients received by the client.

Effective smtp stage

rcpt and onwards. Note that you will not have all recipients received all at once in the rcpt stage. It is better to use this function in the later stages.

Return

  • Array of addresses - the list containing all recipients.

Example

#{
    preq: [
       action "log recipients" || log("info", `all recipients: ${rcpt_list()}`),
    ]
}


Auth

This module contains authentication mechanisms to secure your server.

fn auth()

Get authentication credentials from the client.

Effective smtp stage

authenticate only.

Return

  • Credentials - the credentials of the client.

Example

#{
    authenticate: [
       action "log info" || log("info", `${auth()}`),
    ]
}


fn authenticate()

Process the SASL authentication mechanism.

The current implementation support “PLAIN” mechanism, and will call the testsaslauthd program to check the credentials.

The credentials will be verified depending on the mode of saslauthd.

A native implementation will be provided in the future.



fn is_authenticated()

Check if the client is authenticated.

Effective smtp stage

authenticate only.

Return

  • bool - true if the client succeeded to authenticate itself, false otherwise.

Example

#{
    authenticate: [
       action "log info" || log("info", `${is_authenticated()}`),
    ]
}


fn is_secured()

Has the connection been secured under the encryption protocol SSL/TLS

Effective smtp stage

all

Return

  • boolean value (true if the connection is secured, false otherwise)

Example

#{
  mail: [
    action "log ssl/tls" || {
      log("info", `My client is ${if is_secured() { "secured" } else { "unsecured!!!" }}`)
    }
  ]
}


Security

This module contains multiple security functions that you can use to protect your server.

fn check_dmarc()

Apply the DMARC policy to the mail.

Effective smtp stage

preq and onwards.

Example

#{
  preq: [
    rule "check dmarc" || { check_dmarc() },
  ]
}


fn check_mail_relay(allowed_hosts)

Do not accept a message from a known internal domain if the client is unknown.

Args

  • allowed_hosts - group of IPv4 | IPv6 | IPv4 range | IPv6 range | fqdn

Return

  • deny()
  • next()

Effective smtp stage

mail and onwards.

Example

mail: [
   rule "check mail relay" || {
       object allowed_hosts group = [
           object mta_ip ip4 = "192.168.1.254",
           object mta_fqdn fqdn = "mta-internal.foobar.com"
       ];
       check_mail_relay(allowed_hosts)
   }
]




fn check_rcpt_relay(allowed_hosts)

Do not accept open relaying.

Args

  • allowed_hosts - group of IPv4 | IPv6 | IPv4 range | IPv6 range | fqdn

Return

  • deny()
  • next()

Effective smtp stage

rcpt only.

Example

rcpt: [
   rule "check rcpt relay" || {
       object allowed_hosts group = [
           object mta_ip ip4 = "192.168.1.254",
           object mta_fqdn fqdn = "mta-internal.foobar.com"
       ];
       check_rcpt_relay(allowed_hosts)
   }
]




fn check_spf(header)

Check spf record following the Sender Policy Framework (RFC 7208). A wrapper with the policy set to "strict" by default. see https://datatracker.ietf.org/doc/html/rfc7208

Args

  • header - “spf” | “auth” | “both” | “none”

Return

  • deny(code550_7_23 | code451_7_24 | code550_7_24) - an error occurred during lookup. (returned even when a softfail is received using the “strict” policy)
  • next() - the operation succeeded.

Effective smtp stage

rcpt and onwards.

Errors

  • The header argument is not valid.
  • The policy argument is not valid.

Note

check_spf only checks for the sender’s identity, not the helo value.

Example

#{
    mail: [
       rule "check spf relay" || check_spf(allowed_hosts),
    ]
}

#{
    mail: [
        // if this check succeed, it wil return `next`.
        // if it fails, it might return `deny` with a custom code
        // (X.7.24 or X.7.25 for example)
        //
        // if you want to use the return status, just put the check_spf
        // function on the last line of your rule.
        rule "check spf 1" || {
            log("debug", `running sender policy framework on ${mail_from()} identity ...`);
            check_spf("spf", "soft")
        },

        // policy is set to "strict" by default.
        rule "check spf 2" || check_spf("both"),
    ],
}


fn check_spf(header, policy)

Check spf record following the Sender Policy Framework (RFC 7208). see https://datatracker.ietf.org/doc/html/rfc7208

Args

  • header - “spf” | “auth” | “both” | “none”
  • policy - “strict” | “soft”

Return

  • deny(code550_7_23 | code451_7_24 | code550_7_24) - an error occurred during lookup. (returned even when a softfail is received using the “strict” policy)
  • next() - the operation succeeded.

Effective smtp stage

rcpt and onwards.

Errors

  • The header argument is not valid.
  • The policy argument is not valid.

Note

check_spf only checks for the sender’s identity, not the helo value.

Example

#{
    mail: [
       rule "check spf" || check_spf("spf", "soft")
    ]
}

#{
    mail: [
        // if this check succeed, it wil return `next`.
        // if it fails, it might return `deny` with a custom code
        // (X.7.24 or X.7.25 for example)
        //
        // if you want to use the return status, just put the check_spf
        // function on the last line of your rule.
        rule "check spf 1" || {
            log("debug", `running sender policy framework on ${mail_from()} identity ...`);
            check_spf("spf", "soft")
        },

        // policy is set to "strict" by default.
        rule "check spf 2" || check_spf("both"),
    ],
}


fn check_spf_inner()

WARNING: This is a low level api.

Get spf record following the Sender Policy Framework (RFC 7208). see https://datatracker.ietf.org/doc/html/rfc7208

Return

  • a rhai Map
    • result (String) : the result of an SPF evaluation.
    • cause (String) : the “mechanism” that matched or the “problem” error (RFC 7208-9.1).

Effective smtp stage

rcpt and onwards.

Note

check_spf only checks for the sender’s identity, not the helo value.

Example

#{
    mail: [
        rule "raw check spf" || {
            let query = check_spf_inner();

            log("debug", `result: ${query.result}`);

            // the 'result' parameter gives you the result of evaluation.
            // (see https://datatracker.ietf.org/doc/html/rfc7208#section-2.6)
            switch query.result {
                "pass" => next(),
                "fail" => {
                    // the 'cause' parameter gives you the cause of the result if there
                    // was an error, and the mechanism of the result if it succeeded.
                    log("error", `check spf error: ${query.cause}`);
                    deny()
                },
                _ => next(),
            };
        },
    ],
}


fn sign_dkim(selector)

Alias for `sign_dkim(selector, ["From", "To", "Date", "Subject", "From"], "simple/relaxed")`


fn sign_dkim(selector, headers_field, canonicalization)

Produce a `DKIM-Signature` header.

Args

  • selector - the DNS selector to expose the public key & for the verifier
  • headers_field - list of headers to sign
  • canonicalization - the canonicalization algorithm to use (ex: “simple/relaxed”)

Effective smtp stage

preq and onwards.

Example

#{
  preq: [
    action "sign dkim" || {
      sign_dkim("2022-09", ["From", "To", "Date", "Subject", "From"], "simple/relaxed");
    },
  ]
}


fn verify_dkim()

Verify the `DKIM-Signature` header(s) in the mail and produce a `Authentication-Results`. If this function has already been called once, it will return the result of the previous call. see https://datatracker.ietf.org/doc/html/rfc6376

Return

  • a DkimResult object

Effective smtp stage

preq and onwards.

Example

#{
  preq: [
    action "check dkim" || { verify_dkim(); },
  ]
}


Delivery

Those methods are used to setup the method of delivery for one / every recipient.

fn deliver(rcpt)

Set the delivery method to deliver for a single recipient. After all rules are evaluated, the email will be sent to the recipient using the domain of its address.

Args

  • rcpt - the recipient to apply the method to.

Effective smtp stage

All of them.

Example

#{
    delivery: [
       action "setup delivery" || deliver("john.doe@example.com"),
    ]
}


fn deliver_all()

Set the delivery method to deliver for all recipients. After all rules are evaluated, the email will be sent to all recipients using the domain of their respective address.

Effective smtp stage

All of them.

Example

#{
    delivery: [
       action "setup delivery" || deliver_all(),
    ]
}


fn disable_delivery(rcpt)

Disable the delivery for a single recipient.

Args

  • rcpt - the recipient to apply the method to.

Effective smtp stage

All of them.

Example

#{
    delivery: [
       action "disable delivery" || disable_delivery("john.doe@example.com"),
    ]
}


fn disable_delivery_all()

Disable delivery for all single recipients.

Effective smtp stage

All of them.

Example

#{
    delivery: [
       action "disable delivery" || disable_delivery_all(),
    ]
}


fn forward(rcpt, target)

Set the delivery method to forwarding for a single recipient. After all rules are evaluated, forwarding will be used to deliver the email to the recipient.

Args

  • rcpt - the recipient to apply the method to.
  • target - the target to forward the email to.

Effective smtp stage

All of them.

Example

#{
    delivery: [
       action "setup forwarding" || forward("john.doe@example.com", "mta-john.example.com"),
    ]
}


fn forward_all(target)

Set the delivery method to forwarding for all recipients. After all rules are evaluated, forwarding will be used to deliver the email.

Args

  • target - the target to forward the email to.

Effective smtp stage

All of them.

Example

#{
    delivery: [
       action "setup forwarding" || forward_all("mta-john.example.com"),
    ]
}


fn maildir(rcpt)

Set the delivery method to maildir for a recipient. After all rules are evaluated, the email will be stored locally in the `~/Maildir/new/` folder of the recipient's user if it exists on the server.

Args

  • rcpt - the recipient to apply the method to.

Effective smtp stage

All of them.

Example

#{
    delivery: [
       action "setup maildir" || maildir("john.doe@example.com"),
    ]
}


fn maildir_all()

Set the delivery method to maildir for all recipients. After all rules are evaluated, the email will be stored locally in each `~/Maildir/new` folder of they respective recipient if they exists on the server.

Effective smtp stage

All of them.

Example

#{
    delivery: [
       action "setup mbox" || mbox_all(),
    ]
}


fn mbox(rcpt)

Set the delivery method to mbox for a recipient. After all rules are evaluated, the email will be stored locally in the mail box of the recipient if it exists on the server.

Args

  • rcpt - the recipient to apply the method to.

Effective smtp stage

All of them.

Example

#{
    delivery: [
       action "setup mbox" || mbox("john.doe@example.com"),
    ]
}


fn mbox_all()

Set the delivery method to mbox for all recipients. After all rules are evaluated, the email will be stored locally in the mail box of all recipients if they exists on the server.

Effective smtp stage

All of them.

Example

#{
    delivery: [
       action "setup mbox" || mbox_all(),
    ]
}


Services

Services are external programs that can be used via the functions available in this module.

fn get(key)

Get the value of a key in a csv database.

Args

  • key - the key to query.

Return

  • Array of records - an array containing the results. If no record is found, an empty array is returned.

Effective smtp stage

All of them.

Example

import "services" as svc;

#{
    mail: [
       action "fetch database" || {
            let records = svc::my_database.get(mail_from());

            if records == [] {
                log("debug", `${mail_from()} is not in my database`);
            } else {
                log("debug", `${mail_from()} found in my database: ${records}`);
            }
       }
    ]
}


fn query(q)

Query a database.

Args

  • q - the query to execute.

Effective smtp stage

All of them.

Example

service my_db db:mysql = #{
    url: "mysql://localhost/",
    user: "guest",
};
import "services" as svc;

#{
    connect: [
       action "log database" || {
            let version_record = svc::my_db.query("SELECT version();");
            // Result is a array of maps: [ #{"version()": "'version-of-mysql'"} ]
            log("trace", `Database version is ${version_record[0]["version()"]}`);

            // Fetching a 'greylist' database with a 'sender' table containing the (user, domain, address) fields.
            let senders = svc::my_db.query("SELECT * FROM greylist.sender");

            // Iterate over rows.
            for sender in senders {
                // You can access the columns values using Rhai's Map syntax:
                print(`name: ${sender.user}, domain: ${sender.domain}, address: ${sender.address}`);
            }

            // Populate the database with a new record.
            svc::my_db.query(`INSERT INTO greylist.sender (user, domain, address) values ("john.doe", "example.com", "john.doe@example.com");`);
       }
    ]
}


fn rm(key)

Remove a record from a csv database.

Args

  • key - the key to remove.

Effective smtp stage

All of them.

Example

import "services" as svc;

#{
    mail: [
       action "remove sender from database" || {
            svc::my_database.rm(mail_from());
       }
    ]
}


fn run(args)

Run a command from a `cmd` service with arguments. This allows you to run a command with dynamic arguments.

Args

  • args - an array of strings that will replace current command arguments defined in the args field of a cmd service.

Effective smtp stage

All of them.

Example

// services.vsl
service echo cmd = #{
    timeout: "2s",
    command: "echo",
    args: ["-e", "using cmd to print to stdout\r\n"],
};
// main.vsl
import "services" as svc;

#{
    rcpt: [
       action "print recipient using command" || {
            // prints all recipients using the `echo` command.
            svc::echo.cmd_run(["-E", "-n", `new recipient: ${rcpt()}`]);
       }
    ]
}


fn set(record)

Set a record into a csv database.

Args

  • record - the record to set.

Effective smtp stage

All of them.

Example

import "services" as svc;

#{
    mail: [
       action "set sender in database" || {
            svc::my_database.set([ mail_from() ]);
       }
    ]
}


Utils

Those miscellaneous functions lets you query data from your system, log stuff, perform dns lookups etc …

fn date()

Get the current date.

Return

  • string - the current date.

Effective smtp stage

All of them.

Example

#{
    preq: [
       action "append info header" || {
            append_header("X-VSMTP", `email received by ${hostname()} the ${date()}.`);
       }
    ]
}


fn dump(dir)

Export the current message and the envelop to a file as a `json` file. The message id of the email is used to name the file.

Args

  • dir - the directory where to store the data. Relative to the application path.

Effective smtp stage

preq and onwards.

Example

#{
    preq: [
       action "dump email" || dump("metadata"),
    ]
}


fn hostname()

Get the hostname of this machine.

Return

  • string - the host name of the machine.

Effective smtp stage

All of them.

Example

#{
    preq: [
       action "append info header" || {
            append_header("X-VSMTP", `email received by ${hostname()}.`);
       }
    ]
}


fn in_domain(rcpt)

get the domain used to identify a recipient. check if the recipient passed as argument is part of the domains (root & sni) of the server.

Args

  • rcpt - the recipient to check, of type string | object address | rcpt.

Return

  • bool - true of the recipient’s domain is part of the server’s root or sni domains, false otherwise.

Effective smtp stage

all of them, but should be use in the rcpt stage.

Example

#{
    rcpt: [
       rule "check rcpt domain" || if in_domain(rcpt()) { next() } else { deny() },
    ]
}




fn log(level, message)

Log information to stdout in `nodaemon` mode or to a file.

Args

  • level - the level of the message, can be “trace”, “debug”, “info”, “warn” or “error”.
  • message - the message to log.

Effective smtp stage

All of them.

Example

#{
    preq: [
       action "log info" || log("info", "this is an informational log."),
    ]
}


fn lookup(host)

Performs a dual-stack DNS lookup for the given hostname.

Args

  • host - A valid hostname to search.

Return

  • array - an array of IPs. The array is empty if no IPs were found for the host.

Effective smtp stage

All of them.

Example

#{
    rcpt: [
       action "perform lookup" || {
            let domain = rcpt().domain;
            let ips = lookup(domain);

            print(`ips found for ${domain}`);
            for ip in ips {
                print(`- ${ip}`);
            }
       }
    ]
}


fn rlookup(ip)

Performs a reverse lookup for the given IP.

Args

  • ip - The IP to query.

Return

  • array - an array of FQDNs. The array is empty if nothing was found.

Effective smtp stage

All of them.

Example

#{
    connect: [
       action "perform reverse lookup" || {
            let domains = rlookup(client_ip());

            print(`domains found for ip ${client_ip()}`);
            for domain in domains {
                print(`- ${domain}`);
            }
       }
    ]
}


fn time()

Get the current time.

Return

  • string - the current time.

Effective smtp stage

All of them.

Example

#{
    preq: [
       action "append info header" || {
            append_header("X-VSMTP", `email received by ${hostname()} the ${date()} at ${time()}.`);
       }
    ]
}


fn user_exist(name)

Check if a user exists on this server.

Args

  • name - the name of the user.

Return

  • bool - true if the user exists, false otherwise.

Effective smtp stage

All of them.

Example

#{
    rcpt: [
       action "check for local user" || {
           if user_exist(rcpt().local_part) {
               log("debug", `${rcpt().local_part} exists on disk.`);
           }
       }
    ]
}


fn write(dir)

Export the current raw message to a file as an `eml` file. The message id of the email is used to name the file.

Args

  • dir - the directory where to store the email. Relative to the application path.

Effective smtp stage

preq and onwards.

Example

#{
    preq: [
       action "write to file" || write("archives"),
    ]
}


Variables

namevalue
code451_7_24Code(Reply { code: Enhanced { code: 451, enhanced: “5.7.24” }, text: “SPF validation error” })
code550_7_20Code(Reply { code: Enhanced { code: 550, enhanced: “5.7.20” }, text: “No passing DKIM signature found” })
code550_7_21Code(Reply { code: Enhanced { code: 550, enhanced: “5.7.21” }, text: “No acceptable DKIM signature found” })
code550_7_22Code(Reply { code: Enhanced { code: 550, enhanced: “5.7.22” }, text: “No valid author-matched DKIM signature found” })
code550_7_23Code(Reply { code: Enhanced { code: 550, enhanced: “5.7.23” }, text: “SPF validation failed” })
code550_7_24Code(Reply { code: Enhanced { code: 550, enhanced: “5.7.24” }, text: “SPF validation error” })
code550_7_25Code(Reply { code: Enhanced { code: 550, enhanced: “5.7.25” }, text: “Reverse DNS validation failed” })
code550_7_26Code(Reply { code: Enhanced { code: 500, enhanced: “5.7.26” }, text: “Multiple authentication checks failed” })
code550_7_27Code(Reply { code: Enhanced { code: 550, enhanced: “5.7.27” }, text: “Sender address has null MX” })
code554_7_1Code(Reply { code: Enhanced { code: 554, enhanced: “5.7.1” }, text: “Relay access denied” })
code556_1_10Code(Reply { code: Enhanced { code: 556, enhanced: “5.1.10” }, text: “Recipient address has null MX” })
net_10Rg4(IpRange [10.0.0.0/8])
net_172Rg4(IpRange [172.16.0.0/12])
net_192Rg4(IpRange [192.168.0.0/16])
non_routable_netGroup([Rg4(IpRange [192.168.0.0/16]), Rg4(IpRange [172.16.0.0/12]), Rg4(IpRange [10.0.0.0/8])])

Installing vSMTP from source

Download the source code

You can also download a specific version on the release folder.

Or

Get the latest release (main branch)

git clone -b main --single-branch https://github.com/viridIT/vSMTP.git

Or

Get the latest unstable changes (develop branch)

git clone -b develop --single-branch https://github.com/viridIT/vSMTP.git

Linux installation

The installation described below was performed on an Ubuntu Server 20.04. Slight changes may be needed for other distributions.

Install Rust language

See the official website https://www.rust-lang.org/tools/install

Check dependencies

vSMTP requires the libc libraries and the GCC compiler/linker. On a Debian system, they are contained in the build-essential package.

The authentication is provided by the Cyrus sasl binary package.

sudo apt update
sudo apt install
  pkg-config 
  build-essential
  sasl2-bin

vSMTP compilation

cargo (Rust package manager) downloads all required dependencies and compile the source code in conformance to the current environment.

$> cargo build --workspace --release
[...]
$> cargo run -- --help
vsmtp 1.1.3
Team viridIT <https://viridit.com/>
Next-gen MTA. Secured, Faster and Greener

USAGE:
    vsmtp [OPTIONS] [SUBCOMMAND]

OPTIONS:
    -c, --config <CONFIG>      Path of the vSMTP configuration file (toml format)
    -h, --help                 Print help information
    -n, --no-daemon            Do not run the program as a daemon
    -t, --timeout <TIMEOUT>    Make the server stop after a delay (human readable format)
    -V, --version              Print version information

SUBCOMMANDS:
    config-diff    Show the difference between the loaded config and the default one
    config-show    Show the loaded config (as serialized json format)
    help           Print this message or the help of the given subcommand(s)

By default Rust/Cargo use static linking to compile. All libraries required are compiled into the executable, allowing vSMTP to be a standalone application.

Configure the Operating System for vSMTP

For security purpose, vSMTP should run using a dedicated account with minimum privileges.

$ sudo adduser --system --shell /usr/sbin/nologin --no-create-home \
    --uid 9999 --group --disabled-password --disabled-login vsmtp
Adding system user 'vsmtp' (UID 9999) ...
Adding new group 'vsmtp' (GID 9999) ...
Adding new user 'vsmtp' (UID 9999) with group 'vsmtp' ...
Not creating home directory '/home/vsmtp'.

vSMTP binaries and config files should be located in:

  • /usr/sbin/ : binaries
  • /etc/vsmtp/
    • /etc/vsmtp/vsmtp.toml : default configuration file
    • /etc/vsmtp/rules/ : rules
    • /etc/vsmtp/certs/ : certificates
  • /var/spool/vsmtp/ : internal queues
  • /var/log/
    • /var/log/vsmtp.log/ : internal logs and trace
    • /var/log/mail.log and mail.err : syslog
  • /home/~user/Maildir : local IMAP delivery

These default locations may be changed in the /etc/vsmtp/vsmtp.toml configuration file which is called by the service startup /etc/systemd/system/vsmtp.service file.

sudo mkdir /etc/vsmtp /etc/vsmtp/rules /etc/vsmtp/certs /var/log/vsmtp /var/spool/vsmtp
sudo cp ./target/release/vsmtp /usr/sbin/
sudo cp ./target/release/vqueue /usr/sbin/

A minimal vsmtp.toml configuration file that matches vsmtp version (i.e. 1.0.0) must be created.

sudo bash -c 'echo "version_requirement = \">=1.0.0\"" > /etc/vsmtp/vsmtp.toml'

Grant rights to files and folders.

sudo chown -R vsmtp:vsmtp /var/log/vsmtp /etc/vsmtp/* /var/spool/vsmtp

If required, add private key and certificate to /etc/vsmtp/certs and grant reading rights to the vsmtp user.

Configuring the MTA service

Check and disable current MTA

Check if you have a mail transfer agent service running and disable it. The example below is related to a Postfix service running on port 25.

$ sudo ss -ltpn | grep ":25"
tcp        0      0 127.0.0.1:25              127.0.0.1:*               LISTEN      39423/master
$ sudo systemctl status postfix
● postfix.service - Postfix Mail Transport Agent
     Loaded: loaded (/lib/systemd/system/postfix.service; enabled; vendor preset: enabled)
     Active: active (exited) since Fri 2021-12-10 11:10:58 CET; 5min ago
    Process: 39426 ExecStart=/bin/true (code=exited, status=0/SUCCESS)
   Main PID: 39426 (code=exited, status=0/SUCCESS)
$ sudo systemctl stop postfix
$ sudo systemctl disable postfix
Synchronizing state of postfix.service with SysV service script with /lib/systemd/systemd-sysv-install.
Executing: /lib/systemd/systemd-sysv-install disable postfix
Removed /etc/systemd/system/multi-user.target.wants/postfix.service.

☞ | Depending on Linux distributions, instead of ss command you may have to use netstat -ltpn

Add vSMTP as a systemd service

Copy the daemon configuration file in /etc/systemd/system.

sudo cp ./tools/install/deb/vsmtp.service /etc/systemd/system/vsmtp.service

Please note that vSMTP drops privileges at startup. The service type must be set to forking.

Do not modify this file unless you know what you are doing.

Enable and activate vSMTP service

$ sudo systemctl enable vsmtp.service
Created symlink /etc/systemd/system/multi-user.target.wants/vsmtp.service → /etc/systemd/system/vsmtp.service.

$ sudo systemctl start vsmtp

$ sudo systemctl status vsmtp
● vsmtp.service - vSMTP Mail Transfer Agent
     Loaded: loaded (/etc/systemd/system/vsmtp.service; enabled; vendor preset: enabled)
     Active: active (running) since Fri 2021-12-10 11:32:15 CET; 21s ago
   Main PID: 2164 (vsmtp)
      Tasks: 25 (limit: 38287)
     Memory: 39.9M
     CGroup: /system.slice/vsmtp.service
             └─2164 /usr/sbin/vsmtp -c /etc/vsmtp/vsmtp.toml

Check that vSMTP is working properly

$ ss -ltpn | grep vsmtp
State    Recv-Q   Send-Q     Local Address:Port     Peer Address:Port   Process
LISTEN   0        128        127.0.0.1:587           127.0.0.1:*       users:(("vsmtp",pid=2127,fd=5))
LISTEN   0        128        127.0.0.1:465           127.0.0.1:*       users:(("vsmtp",pid=2127,fd=6))
LISTEN   0        128        127.0.0.1:25            127.0.0.1:*       users:(("vsmtp",pid=2127,fd=4))

$ nc -C localhost 25
220 mydomain.com Service ready
451 Timeout - closing connection.

FreeBSD installation

The installation described above was performed on an FreeBSD 13.0 server.

Installing Rust language

Rust port, packages and information can be found on the freshports website. Find more information about packages and port in the FreeBSD handbook.

pkg install lang/rust

Rust 1.60+ package is required. You may have to switch to the latest ports branch. Please refer to the freeBSD wiki.

Dependencies

FreeBSD 13.x includes all required dependencies. Check that sasl is included in your release (see Linux dependencies).

vSMTP compilation

$> cargo build --workspace --release
[...]
$> cargo run -- --help
vsmtp 1.1.3
Team viridIT <https://viridit.com/>
Next-gen MTA. Secured, Faster and Greener

USAGE:
    vsmtp [OPTIONS] [SUBCOMMAND]

OPTIONS:
    -c, --config <CONFIG>      Path of the vSMTP configuration file (toml format)
    -h, --help                 Print help information
    -n, --no-daemon            Do not run the program as a daemon
    -t, --timeout <TIMEOUT>    Make the server stop after a delay (human readable format)
    -V, --version              Print version information

SUBCOMMANDS:
    config-diff    Show the difference between the loaded config and the default one
    config-show    Show the loaded config (as serialized json format)
    help           Print this message or the help of the given subcommand(s)

Configuring the Operating System for vSMTP

Create the directories and change the owner and group.

mkdir /etc/vsmtp /etc/vsmtp/rules /etc/vsmtp/certs /var/log/vsmtp /var/spool/vsmtp
cp ./target/release/vsmtp /usr/sbin/
cp ./target/release/vqueue /usr/sbin/
cp ./examples/config/minimal.toml /etc/vsmtp/vsmtp.toml
chown -R vsmtp:vsmtp /var/log/vsmtp /etc/vsmtp/* /var/spool/vsmtp

Create a minimal vsmtp.toml configuration file that matches vsmtp version (i.e. 1.0.0)

echo "version_requirement = \">=1.0.0\"" > /etc/vsmtp/vsmtp.toml

Grant rights to files and folders.

chmod 555 /usr/sbin/vsmtp
sudo chown -R vsmtp:vsmtp /var/log/vsmtp /etc/vsmtp/* /var/spool/vsmtp

If required, add private key and certificate to /etc/vsmtp/certs and grant reading rights to the vsmtp user.

Disabling sendmail

Sendmail may have been disabled during FreeBSD installation. If not, add the following lines in the /etc/rc.conf file and reboot the system.

sendmail_enable="NO"
sendmail_submit_enable="NO"
sendmail_outbound_enable="NO"
sendmail_msp_queue_enable="NO"

Use sockstat command to check that sendmail is disabled.

Add vSMTP user:group

pw groupadd vsmtp -g 999
pw useradd vsmtp -u 999 -d /noexistent -g vsmtp -s /sbin/nologin
chown -R vsmtp:vsmtp /var/log/vsmtp /etc/vsmtp/* /var/spool/vsmtp

Adding a vSMTP as a system service

vSMTP drops privileges at startup. User ACLs are no longer needed.

Please add:

  • the flag `vsmtp_enable=“YES” in /etc/rc.conf.
  • the vsmtp script in /usr/local/etc/rc.d
cp ./tools/install/freebsd/freebsd-vsmtp.service /usr/local/etc/rc.d/vsmtp
#! /bin/sh

# PROVIDE: vsmtp
# REQUIRE: DAEMON
# KEYWORD: shutdown

#
# Add the following lines to /etc/rc.conf to enable vsmtp:
#
# vsmtp_enable="YES"

. /etc/rc.subr

name="vsmtp"
rcvar="${name}_enable"

load_rc_config $name

: ${vsmtp_enable:=NO}
: ${vsmtp_config:=/etc/vsmtp/vsmtp.toml}
: ${vsmtp_flags:=--config}

command="/usr/sbin/vsmtp"
command_args="${vsmtp_config}"

run_rc_command "$1"

Starting with a non privileged user

To start with an other mechanism please follow these instructions:

  • Grant the rights to the user to bind on ports <1024.
  • kernel must be updated to support network ACL.
  • Add to these options to the KERNEL file and rebuild it.
options MAC
options MAC_PORTACL
cd /usr/src
make buildkernel KERNCONF=MYKERNEL
make installkernel KERNCONF=MYKERNEL
$ sysctl security.mac
security.mac.portacl.rules:
security.mac.portacl.port_high: 1023
security.mac.portacl.autoport_exempt: 1
security.mac.portacl.suser_exempt: 1
security.mac.portacl.enabled: 1
security.mac.mmap_revocation_via_cow: 0
security.mac.mmap_revocation: 1
security.mac.labeled: 0
security.mac.max_slots: 4
security.mac.version: 4
$ sysctl security.mac.portacl.rules=uid:999:tcp:25,uid:999:tcp:587,uid:999:tcp:465
security.mac.portacl.rules: uid:999:tcp:25, -> uid:999:tcp:25,uid:999:tcp:587,uid:999:tcp:465
$ sysctl security.mac.portacl.rules
security.mac.portacl.rules: uid:999:tcp:25,uid:999:tcp:587,uid:999:tcp:465
$ sysctl net.inet.ip.portrange.reservedlow=0
net.inet.ip.portrange.reservedlow: 0 -> 0
$ sysctl net.inet.ip.portrange.reservedhigh=0
net.inet.ip.portrange.reservedhigh: 1023 -> 0

The user with uid 999 should now be authorized to bind on standard SMTP ports (25, 587, 465).

Queue System

Queues are active in postq & delivery stages. Here is a schema of how messages are transferred between queues, following the rule engine results once a stage has been evaluated.

Queues System

Trouble shooting

This section is Under construction.

No logs available (daemon mode)

Sometimes, logs do not get initialized fast enough in daemon mode, leading to vSMTP not starting on error. To fix this, use the server with the --no-daemon option, this will log error messages on stderr, enabling you to get directions to fix your problem.

$ sudo systemctl start vsmtp
$ sudo systemctl status vsmtp
# this prints a failed start status.

$ vsmtp -c /path/to/config.toml --no-daemon
# this will print errors on stderr.

Mail Agent

Email Architecture

The following diagram illustrates the flow of mail among these defined components.

   +-----+   +-----+   +------------+
   | MUA |-->| MSA |-->| Border MTA |
   +-----+   +-----+   +------------+
                             |
                             |
                             V
                        +----------+
                        | Internet |
                        +----------+
                             |
                             |
                             V
+------------------+   +------------+
| Intermediate MTA |<--| Border MTA |
+------------------+   +------------+
          |
          |
          V
       +-----+   +-----+
       | MDA |-->| MUA |
       +-----+   +-----+

MUA (Mail User Agent)

Client application that allows receiving and sending emails. It can be a desktop application such as Microsoft Outlook/Thunderbird/… or web-based such as Gmail/Hotmail/… (the latter is also called Webmail).

MSA (Mail Submission Agent)

The MSA is responsible for the incoming traffic in the mail system.

A server program that receives mail from an MUA, checks for any errors, and transfers it (via SMTP) to a MTA.

MTA (Mail Transfer Agent)

The MTA is responsible to deliver the mails to the recipients’s mail exchange, using the DNS logics.

A server application that receives mail from the MSA, or from another MTA. It will find (through name servers and the DNS) the MX record from the recipient domain’s DNS zone in order to know how to transfer the mail. It then transfers the mail (with SMTP) to another MTA (which is known as SMTP relaying) or, if the recipient’s server has been reached, to the MDA.

  • A “border MTA” is an MTA that acts as a gateway between the general Internet and the users within an organizational boundary.
  • A “delivery MTA” (or Mail Delivery Agent or MDA) is an MTA that actually enacts delivery of a message to a user’s inbox or other final delivery.
  • An “intermediate MTA” is any MTA that is not a delivery MTA and is also not the first MTA to handle the message.

MDA (Mail Delivery Agent)

A server program that receives mail from the server’s MTA, and stores it into the mailbox. MDA is also known as LDA (Local Delivery Agent).

An example is Dovecot, which is mainly a POP3 and IMAP server allowing an MUA to retrieve mail, but also includes an MDA which takes mail from an MTA and delivers it to the server’s mailbox.