Introduction

This is a WIP document for vSMTP v1.2.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 developed in Rust. Compared to solutions developed in other programming languages (usually C or C++), Rust provide 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.

Its 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 free and under an Open Source 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 from which this book is generated can be found on GitHub.

Contributing

We’d love your help! Please see the CONTRIBUTING.md file to learn about the kinds of contributions we’re looking for.

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 deliver local mail to a client storage using mbox or maildir formats. To retrieve emails from your MUA it is necessary to install a MDA that can handle POP and/or IMAP protocols.

☞ | For Debian/Ubuntu server the most straightforward solution is to download and install courier-imap package and specify to the courier-imap MDA where are located the MailDir/ folders and use a MUA like Mozilla ThunderBird.

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 modular and highly customizable. Adding or modifying subsystems is facilitated by the internal design of the software. An API is available allowing easy integration into existing security elements. Several native plug-ins are already available.

  • Mail exports in raw and json format.
  • Third-party softwares called by user-defined services.
  • Mods and addons support.
  • Applications logs.

Filtering

vSMTP has a complete filtering system. In addition to the standard analysis of the SMTP envelope, vsmtp adds the possibility of interacting on the fly on the content of messages (MIME). It is possible to filter, modify, encrypt, etc. any part of an email. 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 delivering.
  • SMTP relaying and forwarding.

External services

vSMTP supports SMTP delegation, command calls and file databases. Next versions will bring SQL and NoSQL databases and in-memory caches supports. Compliancy with Postfix SMTP access policy delegation and Unix/IP socket calls are planned for Q3/2022.

Email authentication mechanisms

  • Message submission RFCs.
  • Null MX RFC.
  • SPF support.
  • DKIM signer and verifier.
  • DMARC is planned for the next minor release.
  • DANE protocol is planned for 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:

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

vSMTP is a stand-alone application with few kernel interactions, it may run on any system with slight modifications.

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.

Using packages

Linux/Debian distros

Packages .deb can be downloaded from the release section of the vSMTP github.

Linux/RedHat distros

help wanted

BSD ports

help wanted ( Issue 484 )

Using cargo

crates.io

vSMTP is published on https://crates.io, meaning it can be install through cargo

cargo install vsmtp

Docker

help wanted ( Issue #340 )

Step-by-step tutorial

In this tutorial we will follow Doe’s family. John, Jane and their child Jimmy and Jenny want 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 bought 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

Several examples can be found in the example/config folder.

The configuration file

vsmtp.toml is the main configuration file. It is located in /etc/vsmtp directory. Backup the vsmtp.toml file. Open it with your favorite editor. Remove everything, and copy the configuration bellow.

# Version requirement. Do not remove or modify it
version_requirement = ">=1.0.0"

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

# addresses that the server will listen to.
[server.interfaces]
addr = ["192.168.1.254:25"]
addr_submission = ["192.168.1.254:587"]
addr_submissions = ["192.168.1.254:465"]

# Tls settings.
[server.tls]
security_level = "May"
preempt_cipherlist = false
handshake_timeout = "200ms"
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"

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

# The log level that will be written in syslogs.
[server.logs.level]
server = "warn"

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

Now that the server is configured, we need to define rules used to filter messages. This is the role of vSL.

vSL : the vSMTP Scripting Language

You are able to define the behavior of vSMTP thanks to a simple but powerful programming language, the vSMTP Scripting Language (vSL). vSL is based on four main concepts : rules, actions, objects and services.

Rules execute code at different stages of a SMTP transaction and then return a status code to vSMTP, telling the server what to do next. Using rules, you can deny, accept and quarantine incoming messages.

// rule "<name>" || {
//     // <rule body>
//     return <status-code>;
// }

rule "my blacklist" || {
  if client_ip() == "222.11.16.196" {
    // Spam address detected ! We deny the transaction.
    deny()
  } else {
    // the client ip is valid, we can proceed.
    next()
  }
}

Actions simply execute code. Compared to a rule, an action does not return anything, and thus do not influence 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 contain re-usable fields like mailboxes, ip addresses, domain names, file content etc …

// object <name> <type> = "<value>";

object example fqdn = "example.com";
object my_address address = "john.doe@example.com";
object whitelist file:address = "/etc/vsmtp/whitelist.txt";

print(`the example domain: ${example}`);
print(`my personal address: ${my_address}`);
print(`content of whitelist: ${whitelist}`);

Services are interfaces with third party software.

// service <name> <type>[:<content-type>] = "<value>";

// vsmtp will send messages using the smtp protocol
// to the software listening on 127.0.0.1:10026.
service clamsmtpd smtp = #{
    delegator: #{
        address: "127.0.0.1:10026",
        timeout: "60s",
    },
    receiver: "127.0.0.1:10024",
};

// vsmtp will connect to a csv database with
// this service.
service greylist db:csv = #{
    connector: "/db/user_accounts.csv",
    access: "O_RDONLY",
    refresh: "always",
    delimiter: ",",
};

The main.vsl file is the entry point for vSL, located in the /etc/vsmtp/rules directory by default.

RHAI and vSL

vSMTP scripting is based on the RHAI language. Please consult The RHAI book for detailed information about variables, functions, etc.

Defining objects

Let’s define together all the required objects for John Doe’s MTA. Open your favorite editor and create a objects.vsl file in the rule directory.

// -- 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";
cat blacklist.txt
# domain-spam.com
# spam-domain.org
# domain-spammers.com
# foobar-spam-pro.org

Now we need to apply some rules on these objects.

Defining directives (rules and actions)

Let’s add some rules in the main.vsl file for Doe’s family MTA. Here is what we want to configure:

  • Jenny is 11 years old, Jane wants a blind copy of her daughter messages.
  • We want to deliver emails using the Maildir format if a recipient is from the family.
// -- main.vsl
// Import the object file. The 'doe' prefix is an alias.
import "objects" as doe;

#{
  // the "blacklist" will run once the client sends a "MAIL FROM" command.
  // this is the stage when the sender is known. the sender can be accessed using the `mail_from()` function.
  mail: [
    // Deny any sender with a domain listed in the `blacklist` group.
    rule "blacklist" || if mail_from().domain in doe::blacklist { deny() } else { next() }
  ],

  // This stage is executed every time a "RCPT TO" command is sent by the client. The current recipient can be inspected using the `rcpt()` function.
  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 deliver stage is executed just before vsmtp delivers the message. It can be used to setup how vsmtp will deliver the message.
  deliver: [
    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 ctx().rcpt_list {
        if rcpt in doe::family_addr { maildir(rcpt.to_string()) } else { deliver(rcpt.to_string()) }
      }
    }
  ]
}

Hardening vSMTP

Disabling open relay

Doe’s family server is connected to the Internet. It must not be configured to accept mail from any sender and deliver it to any recipient. This is an undesirable setup as it can be exploited by spammers and other malicious users.

Here are the strict minimum rules for a properly configured server. It will only accept messages from outside:

  • If the recipient is a Doe’s family account, whatever the sender.
  • If the sender is authenticated as a Doe’s family account, whatever the recipient.

All IPs from the internal network are allowed to send messages.

Edit your main.vsl code and add the rules below.

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

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

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

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.

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

This is a temporary vSL code required by v1.1. Future releases will bring an out-of-the box vSL function. Do not forget to start saslauthd daemon with MECHANISM=“shadow” in /etc/default/saslauthd.

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

Using the SPF protocol

You can find more information about SPF protocol in the advanced section.

To allow other MTAs to verify that outgoing email from Doe’s family domain comes from its server, we need to enable the SPF protocol. This is done by adding a new DNS text record that only allows only the MX record to send a mail for doe-family.com.

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

That’s all for outgoing messages. What about incoming messages ?

Edit your main.vsl code and just add the “check spf” rule.

// -- main.vsl
#{
  mail: [
    rule "check spf" || check_spf("both", "soft"),
  ]
}

SMTP security delegation

Adding an antivirus

John is aware of security issues. Malware remains a scourge on the internet. So he decides to add a second layer of antivirus.

Therefore, he installed ClamAV which comes with the clamsmtpd antivirus daemon.

vSMTP support security delegation via the SMTP protocol and vsl’s configuration. In the next example, we are going to configure vsmtp to delegate emails to clamsmtpd.

Clamav setup

The following example assumes that you started the clamsmtpd service with the following config:

## -- /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

To start clamav, use the following commands:

$ sudo systemctl start clamsmtp
$ sudo systemctl start clamav-daemon

The service

First off, create a smtp service in vsl like so:

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

In the toml configuration, you need to enable the receiver’s socket.

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

Rules

You service is configured. Now, to use it, create the following rule using the delegate keyword.

once the “check email for virus” rule is run, vsmtp will send the email to the clamsmtpd service and rule evaluation will be on hold. Once all results are received on port 10025, evaluation will resume, and the body of this rule will be evaluated.

// -- 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 the smtp protocol and then execute its body.
        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
            // once a virus is detected, we juste have to
            // check on that.
            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.

That’s it

Any service that supports the SMTP protocol can be used to delegate the email processing / security with the delegate directive.

Configuring the vSMTP service

vSMTP configuration file

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

When starting vSMTP, the configuration file is read and entirely parsed, producing an error if the format is invalid or a field value is incorrect. The server never crashes if the configuration is loaded successfully.

The folder examples/config contains example of vSMTP configuration. All fields are optional and set to default if missing.

☞ | vSMTP service must be restarted to apply changes.

Configuring SMTP filtering

SMTP filtering is performed by the rule engine. The end user can interact and modify the behavior of vSMTP by adding objects and rules in .vsl configuration files. The main.vsl file in the “rules” folder is injected into the rules engine. It is the entry point for your filtering.

If there is no .vsl file, the server will refuse all incoming and outgoing mails, as well as domain forwarding. The .vSL files are commonly stored in the /etc/vsmtp/rules directory.

Please refer to the examples in the vSMTP repository and read the reference guide on vSL for detailed information.

☞ | vSMTP service must be restarted to apply changes.

vSMTP configuration files

vSMTP and its sub-systems use TOML language for their configuration files. TOML files are frequently compared to INI for their similarities in syntax and use as configuration files.

TOML uses tables (hash tables) as collections of key/value pairs. Key/value pairs within tables are not guaranteed to be in any specific order. Tables appear in square brackets on a line by themselves. Dots are used to signify nested tables. Nested array of tables are also allowed.

The vsmtp.toml file

This is the main configuration file. It should be located in /etc/vsmtp. However it can be modified in the systemd’s service file /etc/systemd/system/vsmtp.service or using the --config option in interactive mode.

The vSMTP toml file is currently split into two main tables:

TableComment
[server]The vSMTP overall configuration and its system interactions.
[app]The application configuration and the rule engine behavior.

Future releases may bring new tables.

The [server] table contains all the required information to start the vSMTP server and the root domain parameters. 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:

  • Omitted: In this case, the default settings are applied. They can be retrieved with the vsmtp config-show command.
  • 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.

The vsmtp.service file

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

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

Domain Name System configuration

vSMTP can manage complex DNS situations. The default configuration can be updated for each virtual domain.

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 configuration can be applied on root or on virtual domains.

DNS resolver

The default behavior is to use the operating system /etc/resolv.conf as the upstream resolver. However other configurations are available and can be easily changed to the Google or the CloudFlare Public DNS using the type field.

[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 for identifying a server that accepts email for a domain. In essence, the SMTP client first looks up a DNS MX RR, and, if that is not found, it falls back to looking up a DNS A or AAAA RR. If a CNAME record is found, the resulting name is processed as if it were the initial name.

This mechanism has two major defects.

  • It overloads a DNS record with 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 and:
    • It delay notification to the sender in the case of misdirected mail.
    • It consumes 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.

Email Authentication Mechanisms

Preventing scammers from usurping your identity by using your 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) consists of defining authorized senders in a given domain. It thus allows email clients to verify that incoming email from a domain comes from a host authorized by the administrator of this domain.

SPF is an authentication standard for linking a domain name and an email address.

The DomainKeys Identified Mail (DKIM) protocol allows you to sign your email with your domain name. The objective of the DKIM protocol is not only to prove that the domain name has not been usurped, but also that the message has not been altered during its transmission.

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

These are the main protocols for verifying the identity of senders. This is one of the most effective ways to prevent phishers and other fraudsters from impersonating a legitimate sender whose identity they are impersonating using the same domain name.

The implementation of these protocols improves the deliverability of emails sent, since you will be better identified by the ISPs (Internet Service Providers) and email clients of your recipients. You then optimize your chances that your emails will arrive in the inbox of your recipients and not in the “spam” or “junk mail” folder.

These protocols have become standards for sending email. A message sent without an SPF and/or DKIM signature is viewed with suspicion by the various email analysis tools.

SPF and DKIM : limitations

SPF has its limits. If the email is forwarded, the verification may not take place, since the address sending the forwarded message will not necessarily be included in the list of addresses validated by SPF.

As a sender, the DKIM signature will not prevent you from being considered a spammer if you do not apply good emailing practices.

Moreover, SPF and DKIM do not specify the action to apply in case of verification failure. This is where the DMARC protocol comes in, telling 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 effectively 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 an SPF or DKIM standard.

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 pass commonly accepted authentication checks. This 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 arrived at the first ARC-aware MTA.

BIMI

Brand Indicators for Message Identification, or 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

Basically the “null MX” protocol is a simple mechanism by which a domain can indicate that it does not accept email. It is described in RFC 7505.

This protocol defines a null MX that will cause all mail delivery attempts to a domain to fail immediately.

DNS record

A “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 is to reject 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 v1.2 features

The standard API as 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 provide a NULL MX record, the email is immediately placed into the dead queue.

Sender Policy Framework (SPF)

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

SPF is an authentication standard for linking 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

A SPF record is a TXT record type. There should be only one SPF record per domain. If you have multiple DNS SPF records, email carriers won’t know which one to use, which could cause authentication issues.

Here is a basic SPF record example. Please refer to RFC 7208 for further details.

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

It means “only servers in the range 123.123.123.0/24 and MTA (MX) are authorized to send emails from my domain example.com. All senders not listed here are considered unauthorized. “

Besides the aforementioned “-all” there is also a tilde version: ~all. It indicates 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.

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.“

Even if the RFC 5321 tends to normalize the HELO/EHLO arguments as the fully qualified domain name of the SMTP client, 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 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.“

Please notes that RFC5321 allows the reverse-path to be null. In this case, the RFC 7208 defines the “MAIL FROM” identity to be the mailbox composed of 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 utilization 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. According to RFCs, two options are available :

  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 standard API has a dedicated function to check the SPF policy. check_spf return a status, containing custom codes to send to the client.

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

DomainKeys Identified Message

This document specifies the vSMTP implementation of the DomainKeys Identified Mail Signatures (DKIM) protocol described in RFC 6376.

DKIM is an open standard for email authentication that verifies the message of an email. DKIM gives emails a signature header which is added to the email. This signature is secured by a key pair (private/public) and a certificate.

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 directly to retrieve the appropriate public key.

DNS records

Like the SPF protocol, DKIM is a DNS TXT record inserted into the sender domain’s DNS. It contains the public key for the DKIM settings. The private key held by the sending email server can be verified against the public key by the receiving email server.

DKIM selectors are used to connect and decrypt encrypted signatures.

Unlike SPF authentication, your domain can have multiple DNS DKIM records without causing a problem.

A domain can have multiple public keys if it has multiple mail servers (each mail server has its own private key that matches only one public key). A selector is an attribute within a DKIM signature that helps the recipient’s server find the correct public key in the sender’s DNS.

A DKIM record must be placed in at address : [selector]._domainkey.[domain]. and the query on may only result in one TXT type record maximum.

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

Here is an example of a DKIM record:

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

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

vSMTP implementation

vSMTP can act as signer or verifier as described in the RFC.

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.

Unlike SPF there’s no specific DKIM header thus 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 for reporting the DKIM and SPF mechanisms.

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 API has a dedicated function to use DKIM.

Check the Security file to get the full documentation for verify_dkim.

Domain-based Message Authentication, Reporting and Conformance

This is a DRAFT for the 1.3 release. Support is planned for Q3/2022.

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 take.

Authenticated Received Chain

This is a DRAFT for future releases

The Authenticated Received Chain (ARC) protocol provides an authenticated “chain of custody” for a message, allowing each entity that handles the message to see what entities handled it before and what the message’s authentication assessment was at each step in the handling.

The current RFC is not an Internet Standards Track specification; it is published for examination, experimental implementation, and evaluation.

Until ARC RFC is not released as an “Internet Standard” the vSMTP implementation should be considered as a “work in progress”. However it represents the consensus of the IETF community. It has received public review and has been approved for publication by the Internet Engineering Steering Group (IESG).

The implementation of this feature in the vSMTP framework is planned for a future release.

Brand Indicators for Message Identification

This is a DRAFT for future releases

Brand Indicators for Message Identification (BIMI) permits Domain Owners to coordinate with Mail User Agents (MUAs) to display brand-specific Indicators next to properly authenticated messages.

While the previous authentication and identification methods work in the background, BIMI is the first whose objective is to make visible and strengthen a brand on the front-end side. The logo is displayed within the user interface of the recipient’s webmail.

There are two aspects of BIMI coordination:

  1. A scalable mechanism for Domain Owners to publish their desired Indicators
  2. A mechanism for Mail Transfer Agents (MTAs) to verify the authenticity of the Indicator.

The current BIMI RFC is not an Internet Standards Track specification. It is “Internet-Draft”. Please remember that it is inappropriate to use Internet-Drafts as reference material.

Until BIMI RFC is not released as an “Internet Standard” the vSMTP implementation should be considered as a “work in progress”.

The implementation of this feature in the vSMTP framework is planned for a future release.

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) is designed to prevent 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 consist of 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 latest field (Certificate Association Data) stores the full value or digest of the certificate or 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 client’s SNI extension, the server SHOULD respond with another certificate chain (a default or closest match). This is because clients might support more than one server name but can only put 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.”

This is implement 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.

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

Logging

In vSMTP, logs are separated in two categories: server logs & app logs.

  • server logs are relative to vsmtp, they regroup information about the client, internal state & errors.
  • app logs are written using the log(level, message) function in your vsl rules.

Server logs

By default, server logs are located at /var/log/vsmtp/vsmtp.log.

Logs are configured by “modules”, that represent a specific part of the server. The rule_engine module will enable logs for the rule engine, parser for the mime parser, etc …

To configure server logs, use the syntax down below in your vsmtp.toml:

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

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

    # set specific vSMTP module logs (override "server" log level for specific module)
    # possible modules are:
    #   - queue
    #   - receiver
    #   - rule_engine
    #   - delivery
    #   - parser
    #   - runtime
    #   - processes
    "receiver=info",
    "rule_engine=warn",
    "delivery=error",
    "parser=trace",
]

Application logs

To configure applicative logs (log output of your vsl files, using the log(level, message) function), write the following in your vsmtp.toml:

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

Syslogs

vSMTP automatically write to syslogs with the info level by default. It is not yet configurable. Files are written at /dev/log or /var/run/syslog.

Managing vSMTP from the command line

Starting vSMTP

vSMTP was designed to run as a Unix service and is not intended to be run interactively from the command line. 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 config 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 fashion. 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 with the vqueue command.

Features available in v0.10:

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

Feature planned for v0.11:

  • vqueue msg <msg-id> re-run : Reintroduce the message in the delivery system (and reevaluate the 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"
format = "{d(%Y-%m-%d %H:%M:%S%.f)} {h({l:<5})} {t:<30} $ {m}{n}"
size_limit = 10485760
archive_count = 10
level = ["trace"]

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, and simply adds helper syntax and functions on top of it.

Advanced users can use the Rhai scripting language on top of vSL to create and manage a wide variety of actions. You can check out the Rhai reference book to learn everything you can do with this language, but for now, if you just want to learn the gist of how the rule system works, follow this section.

Code highlighting is available for the vscode ide, using the Rhai extension.

To interact with the SMTP traffic, vSL combines:

Rules can be applied at any stage of the SMTP transaction.

The delivery system uses the same concepts by applying rules to targeted domains and users.

vSL stages and SMTP states

vSMTP can interact with the messaging transaction at multiple levels. These are related to the states defined in the SMTP protocol.

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

vSMTP stages

Here is a list of 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.
deliverBefore deliveringThe entire mail.
1

Preq stage triggers after the end of receiving data from the client, just before the server answer back with a 250 code. 2: Postq stage triggers after the preq stage, when the connection closes and the SMTP code was sent to the client.

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

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

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

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

    // other stages ...
}

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

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.

This early detection of inappropriate mails has several advantages :

  • 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 end within a deadline, a system facing a heavy workload may have difficulties to respond in time.

To protect against bursts and crashes, vSMTP implements several internal mechanisms like ‘delay-variation’ or ‘temporary service unavailable messages’, in accordance 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 through the Connection and Transaction modules) is unique for each incoming connexion.

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 except that rules must return a vSL rule engine status. They follow the same syntax :

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

Check out the Status file to see which status you can use and what their effects are.

action "action name" || {
    // ... action body.
}

You can also use the inline syntax below:

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

Here are some rule examples:

// 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 log:

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

✎ | You can use String Interpolation to inject variables in strings.

The delegate directive is different: it also use 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 delegation guide 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(`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("johndoe@compagny.com", "john.doe@company.net");
            remove_rcpt("customer@company.net");
            add_rcpt("no-reply@company.net");
        },
    ],

    // ... other rules & actions
}

Implicit rules

To avoid undefined behavior, the implicit status in a stage is next(). For security purpose end-users should always add a trailing rule at the end of a stage. if not, the implicit next() of the last rule will jump 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).

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 also written in Rust.

Documentation

Check out the Delivery module documentation.

Services

Services are declared using the service keyword.

Commands

A command service lets you run commands using vsl.

service clamscan cmd = #{
    timeout: "10s",
    command: "clamscan",
    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" ]);

Databases

The db service, enables you to communicate with a database. Here we open a csv database with the ‘connector’ field.

service greylist db:csv = #{
    connector: "/db/user_accounts.csv",
    access: "O_RDONLY",
    refresh: "always",
    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]);

Smtp

The smtp service enables you to use the delegate directive to delegate the email to another service via the smtp protocol. Here we send the email to the clamsmtpd antivirus.

// -- service.vsl
service clamsmtpd smtp = #{
    delegator: #{
        address: "127.0.0.1:10026",
        timeout: "60s",
    },
    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 file to get access to the full list of functions for services.

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 do not capture their external scope except for functions (they are “pure”). you must pass necessary variables via parameters.

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.

accept()
Tell the rule engine to accept the incomming 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(),
        rule "ignored checks" || print("this will be ignored.")
    ],

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

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 "" || {
           // The client is denied if a recipient's domain matches satan.org,
           // this is a blacklist, sort-of.
           if ctx().rcpt.domain == "satan.org" {
               deny()
           } else {
               next()
           }
       },
    ],
}

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

Effective smtp stage

all of them.

Example

#{
    rcpt: [
        rule "" || {
           // 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 ctx().rcpt.domain == "satan.org" {
               deny(error_code)
           } else {
               next()
           }
       },
    ],
}

faccept()
Tell the rule engine to force accept the incomming 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() },
    ],
}



info(code)
Ask the client to retry to send the current comment by sending an information code.

Effective smtp stage

all of them.

Example

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

next()
Tell the rule engine that a rule succeeded.

Effective smtp stage

all of them.

Example

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

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.

Effective smtp stage

all of them.

Example

#{
    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.

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"),
    ]
}

append_header(header, value)
Append a new header to 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 tought the email is not received at the current stage, vsmtp stores new headers and will prepend them to the ones received once the preq stage is reached.

Example

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

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"),
    ]
}

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

preq and onwards.

Example

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

has_header(header)
Checks if the message contains a specific header.

Args

  • header - the name of the header to search.

Effective smtp stage

preq and onwards.

Example

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

prepend_header(header, value)
Prepend a new header to 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 tought the email is not received at the current stage, vsmtp stores new headers and will prepend them to the ones received once the preq stage is reached.

Example

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

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"),
    ]
}

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"),
    ]
}

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"),
    ]
}

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 tought the email is not received at the current stage, vsmtp stores new headers and will prepend them 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} (analysed by vsmtp)`);
        }
    ],
}

Envelop

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

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"),
    ]
}

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"),
    ]
}

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"),
    ]
}

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"),
    ]
}

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.

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()}`),
    ]
}

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()}`),
    ]
}

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()}`),
    ]
}

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()}`),
    ]
}

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()}`),
    ]
}

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()}`),
    ]
}

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()}`),
    ]
}

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.

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()}`),
    ]
}

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()}`),
    ]
}

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()}`),
    ]
}

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()}`),
    ]
}

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()}`),
    ]
}

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.

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()}`),
    ]
}

is_authenticated()
Check if the client is authenticated.

Effective smtp stage

authenticate only.

Return

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

Example

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

is_secured()
Check if the client's connexion was secure.

Effective smtp stage

authenticate only.

Return

  • bool - true if the client securly connected with the auth protocol, false otherwise.

Example

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

Security

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

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.


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)
   }
]



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)
   }
]



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 exemple)
        //
        // 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 ${ctx().mail_from} identity ...`);
            check_spf("spf", "soft")
        },

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

        // you can also use the low level system api.
        rule "check spf 3" || {
            let query = sys::check_spf(ctx(), srv());

            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)
            //
            // the 'cause' parameter gives you the cause of the result if there
            // was an error, and the mechanism of the result if it succeeded.
            switch query.result {
                "pass" => next(),
                "fail" => {
                    log("error", `check spf error: ${query.cause}`);
                    deny()
                },
                _ => next(),
            };
        },
    ],
}

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 exemple)
        //
        // 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 ${ctx().mail_from} identity ...`);
            check_spf("spf", "soft")
        },

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

        // you can also use the low level system api.
        rule "check spf 3" || {
            let query = sys::check_spf(ctx(), srv());

            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)
            //
            // the 'cause' parameter gives you the cause of the result if there
            // was an error, and the mechanism of the result if it succeeded.
            switch query.result {
                "pass" => next(),
                "fail" => {
                    log("error", `check spf error: ${query.cause}`);
                    deny()
                },
                _ => next(),
            };
        },
    ],
}

verify_dkim()
Verify the `DKIM-Signature` header(s) in the mail and produce a `Authentication-Results`. see https://datatracker.ietf.org/doc/html/rfc6376

Return

  • accept() - a signature was successfully verified.
  • deny() - no signature could be verified.

Delivery

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

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"),
    ]
}

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(),
    ]
}

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"),
    ]
}

disable_delivery_all()
Disable delivery for all single recipients.

Effective smtp stage

All of them.

Example

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

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"),
    ]
}

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"),
    ]
}

maildir(rcpt)
Set the delivery method to maildir for a recipient. After all rules are evaluated, the email will be stored localy 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"),
    ]
}

maildir_all()
Set the delivery method to maildir for all recipients. After all rules are evaluated, the email will be stored localy 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(),
    ]
}

mbox(rcpt)
Set the delivery method to mbox for a recipient. After all rules are evaluated, the email will be stored localy 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"),
    ]
}

mbox_all()
Set the delivery method to mbox for all recipients. After all rules are evaluated, the email will be stored localy 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.

get(key)
Get the value of a key in a database.

Args

  • key - the key to query.

Return

  • Array of records - an array containing the results.

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.len() == 0 {
                log("debug", `${mail_from()} is not in my database`);
            } else {
                log("debug", `${mail_from()} found in my database`);
            }
       }
    ]
}

rm(key)
Remove a record from a 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());
       }
    ]
}

set(record)
Set a record into a 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 …

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()}.`);
       }
    ]
}

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("metadatas"),
    ]
}

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()}.`);
       }
    ]
}

in_domain(rcpt)
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(ctx().rcpt) { next() } else { deny() },
    ]
}



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."),
    ]
}

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}`);
            }
       }
    ]
}

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}`);
            }
       }
    ]
}

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()}.`);
       }
    ]
}

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.`);
           }
       }
    ]
}

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 above was performed on an Ubuntu Server 20.04. There may be slight changes to apply for other distributions.

Install Rust language

Follow the instruction from the official website https://www.rust-lang.org/tools/install

Check dependencies

vSMTP requires libc libraries and GCC compiler/linker. On a Debian system these can be installed through the build-essential package.

The Transport Layer Security protocol (TLS) is provided by OpenSSL development libraries. The Debian package is libssl-dev package.

The authentication is provided by the GNU gsasl and Cyrus sasl libraries.

libclang frontend is also required.

sudo apt update
sudo apt install
  pkg-config
  build-essential
  libssl-dev
  libgsasl7-dev
  libsasl2-2
  sasl2-bin
  libclang-dev

vSMTP compilation

cargo (Rust package manager) will download all required dependencies and compile the source code in accordance with your environment.

$> cargo build
[...]
$> 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 minimal 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/ for binaries
  • /etc/vsmtp/
    • /etc/vsmtp/vsmtp.toml : the default configuration file
    • /etc/vsmtp/rules/ for rules
    • /etc/vsmtp/certs/ for certificates
  • /var/spool/vsmtp/ for internal queues
  • /var/log/
    • /var/log/vsmtp/ for internal logs and trace
    • /var/log/mail.log and mail.err for syslog (not implemented in current release)
  • /home/~user/Maildir for local IMAP delivery

This default behavior can be changed in the vSMTP configuration file /etc/vsmtp/vsmtp.toml which is called at startup by the vsmtp service script.

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/

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

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, do not forget to add your private key and certificate to /etc/vsmtp/certs and allow vsmtp user to read them.

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 to /etc/systemd/system.

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

Please note that vSMTP comes with a mechanism that drop 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 freshports website. You can 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 comes with all required dependencies. Please check that sasl is included in your release (see Linux dependencies).

vSMTP compilation

$> cargo build
[...]
$> 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, do not forget to add your private key and certificate to /etc/vsmtp/certs and allow vsmtp user to read them.

Disabling sendmail

Sendmail may have been disabled during FreeBSD install. If not, add the following 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 comes with a mechanism that drop 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

If you want to start with an other mechanism please follow these instructions. You must grant the rights to the user to bind on ports <1024. The 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 enable 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.