;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2015 Andy Wingo <wingo@igalia.com>
-;;; Copyright © 2017 Clément Lassieur <clement@lassieur.org>
+;;; Copyright © 2017, 2018 Clément Lassieur <clement@lassieur.org>
+;;; Copyright © 2017 Carlo Zancanaro <carlo@zancanaro.id.au>
+;;; Copyright © 2017 Tobias Geerinckx-Rice <me@tobias.gr>
;;;
;;; This file is part of GNU Guix.
;;;
#:use-module (guix packages)
#:use-module (guix gexp)
#:use-module (ice-9 match)
+ #:use-module (ice-9 format)
+ #:use-module (srfi srfi-1)
#:export (dovecot-service
dovecot-service-type
dovecot-configuration
opensmtpd-configuration
opensmtpd-configuration?
opensmtpd-service-type
- %default-opensmtpd-config-file))
+ %default-opensmtpd-config-file
+
+ mail-aliases-service-type
+
+ exim-configuration
+ exim-configuration?
+ exim-service-type
+ %default-exim-config-file))
;;; Commentary:
;;;
;;;
;;; Code:
+(define (uglify-field-name field-name)
+ (let ((str (symbol->string field-name)))
+ (string-join (string-split (if (string-suffix? "?" str)
+ (substring str 0 (1- (string-length str)))
+ str)
+ #\-)
+ "_")))
+
+(define (serialize-field field-name val)
+ (format #t "~a=~a\n" (uglify-field-name field-name) val))
+
+(define (serialize-string field-name val)
+ (serialize-field field-name val))
+
+(define (space-separated-string-list? val)
+ (and (list? val)
+ (and-map (lambda (x)
+ (and (string? x) (not (string-index x #\space))))
+ val)))
+(define (serialize-space-separated-string-list field-name val)
+ (serialize-field field-name (string-join val " ")))
(define (comma-separated-string-list? val)
(and (list? val)
(define (serialize-comma-separated-string-list field-name val)
(serialize-field field-name (string-join val ",")))
+(define (file-name? val)
+ (and (string? val)
+ (string-prefix? "/" val)))
+(define (serialize-file-name field-name val)
+ (serialize-string field-name val))
+
(define (colon-separated-file-name-list? val)
(and (list? val)
;; Trailing slashes not needed and not
(define (serialize-colon-separated-file-name-list field-name val)
(serialize-field field-name (string-join val ":")))
+(define (serialize-boolean field-name val)
+ (serialize-string field-name (if val "yes" "no")))
+
(define (non-negative-integer? val)
(and (exact-integer? val) (not (negative? val))))
(define (serialize-non-negative-integer field-name val)
@samp{pam}, @samp{passwd}, @samp{shadow}, @samp{bsdauth}, and
@samp{static}.")
(args
- (free-form-args '())
- "A list of key-value args to the passdb driver."))
+ (space-separated-string-list '())
+ "Space separated list of arguments to the passdb driver."))
(define (serialize-passdb-configuration field-name val)
(format #t "passdb {\n")
"The driver that the userdb should use. Valid values include
@samp{passwd} and @samp{static}.")
(args
- (free-form-args '())
- "A list of key-value args to the userdb driver.")
+ (space-separated-string-list '())
+ "Space separated list of arguments to the userdb driver.")
(override-fields
(free-form-args '())
"Override fields from passwd."))
(define-configuration unix-listener-configuration
(path
- (file-name (configuration-missing-field 'unix-listener 'path))
- "The file name on which to listen.")
+ (string (configuration-missing-field 'unix-listener 'path))
+ "Path to the file, relative to @code{base-dir} field. This is also used as
+the section name.")
(mode
(string "0600")
"The access mode for the socket.")
(define-configuration fifo-listener-configuration
(path
- (file-name (configuration-missing-field 'fifo-listener 'path))
- "The file name on which to listen.")
+ (string (configuration-missing-field 'fifo-listener 'path))
+ "Path to the file, relative to @code{base-dir} field. This is also used as
+the section name.")
(mode
(string "0600")
"The access mode for the socket.")
"Listeners for the service. A listener is either an
@code{unix-listener-configuration}, a @code{fifo-listener-configuration}, or
an @code{inet-listener-configuration}.")
+ (client-limit
+ (non-negative-integer 0)
+ "Maximum number of simultaneous client connections per process. Once this
+number of connections is received, the next incoming connection will prompt
+Dovecot to spawn another process. If set to 0, @code{default-client-limit} is
+used instead.")
(service-count
(non-negative-integer 1)
"Number of connections to handle before starting a new process.
Typically the only useful values are 0 (unlimited) or 1. 1 is more
secure, but 0 is faster. <doc/wiki/LoginProcess.txt>.")
+ (process-limit
+ (non-negative-integer 0)
+ "Maximum number of processes that can exist for this service. If set to 0,
+@code{default-process-limit} is used instead.")
(process-min-avail
(non-negative-integer 0)
"Number of processes to always keep waiting for more connections.")
(list
(service-configuration
(kind "imap-login")
+ (client-limit 0)
+ (process-limit 0)
(listeners
(list
(inet-listener-configuration (protocol "imap") (port 143) (ssl? #f))
(inet-listener-configuration (protocol "pop3s") (port 995) (ssl? #t)))))
(service-configuration
(kind "lmtp")
+ (client-limit 1)
+ (process-limit 0)
(listeners
(list (unix-listener-configuration (path "lmtp") (mode "0666")))))
- (service-configuration (kind "imap"))
- (service-configuration (kind "pop3"))
- (service-configuration (kind "auth")
- ;; In what could be taken to be a bug, the default value of 1 for
- ;; service-count makes it so that a PAM auth worker can't fork off
- ;; subprocesses for making blocking queries. The result is that nobody
- ;; can log in -- very secure, but not very useful! If we simply omit
- ;; the service-count, it will default to the value of
- ;; auth-worker-max-count, which is 30, instead of defaulting to 1, which
- ;; is the default for all other services. As a hack, bump this value to
- ;; 30.
- (service-count 30)
+ (service-configuration
+ (kind "imap")
+ (client-limit 1)
+ (process-limit 1024))
+ (service-configuration
+ (kind "pop3")
+ (client-limit 1)
+ (process-limit 1024))
+ (service-configuration
+ (kind "auth")
+ (service-count 0)
+ (client-limit 0)
+ (process-limit 1)
(listeners
(list (unix-listener-configuration (path "auth-userdb")))))
- (service-configuration (kind "auth-worker"))
- (service-configuration (kind "dict")
+ (service-configuration
+ (kind "auth-worker")
+ (client-limit 1)
+ (process-limit 0))
+ (service-configuration
+ (kind "dict")
+ (client-limit 1)
+ (process-limit 0)
(listeners (list (unix-listener-configuration (path "dict")))))))
"List of services to enable. Available services include @samp{imap},
@samp{imap-login}, @samp{pop3}, @samp{pop3-login}, @samp{auth}, and
"How long to redirect users to a specific server after it no longer
has any connections.")
- (director-doveadm-port
- (non-negative-integer 0)
- "TCP/IP port that accepts doveadm connections (instead of director
-connections) If you enable this, you'll also need to add
-@samp{inet-listener} for the port.")
-
(director-username-hash
(string "%Lu")
"How the username is translated before being hashed. Useful values
string, %$ contains the data we want to log.")
(mail-log-prefix
- (string "\"%s(%u): \"")
+ (string "\"%s(%u)<%{pid}><%{session}>: \"")
"Log prefix for mail processes. See doc/wiki/Variables.txt for list
of possible variables you can use.")
updated.")
(mdbox-rotate-size
- (non-negative-integer #e2e6)
+ (non-negative-integer #e10e6)
"Maximum dbox file size until it's rotated.")
(mdbox-rotate-interval
x500UniqueIdentifier are the usual choices. You'll also need to set
@samp{auth-ssl-username-from-cert? #t}.")
- (ssl-parameters-regenerate
- (hours 168)
- "How often to regenerate the SSL parameters file. Generation is
-quite CPU intensive operation. The value is in hours, 0 disables
-regeneration entirely.")
-
- (ssl-protocols
- (string "!SSLv2")
- "SSL protocols to use.")
+ (ssl-min-protocol
+ (string "TLSv1")
+ "Minimum SSL protocol version to accept.")
(ssl-cipher-list
- (string "ALL:!LOW:!SSLv2:!EXP:!aNULL")
+ (string "ALL:!kRSA:!SRP:!kDHd:!DSS:!aNULL:!eNULL:!EXPORT:!DES:!3DES:!MD5:!PSK:!RC4:!ADH:!LOW@STRENGTH")
"SSL ciphers to use.")
(ssl-crypto-device
often.")
(imap-logout-format
- (string "in=%i out=%o")
+ (string "in=%i out=%o deleted=%{deleted} expunged=%{expunged} trashed=%{trashed} hdr_count=%{fetch_hdr_count} hdr_bytes=%{fetch_hdr_bytes} body_count=%{fetch_body_count} body_bytes=%{fetch_body_bytes}")
"IMAP logout format string:
@table @code
@item %i
total number of bytes read from client
@item %o
total number of bytes sent to client.
-@end table")
+@end table
+See @file{doc/wiki/Variables.txt} for a list of all the variables you can use.")
(imap-capability
(string "")
(home-directory "/var/empty")
(shell (file-append shadow "/sbin/nologin")))))
-(define %dovecot-activation
+(define (%dovecot-activation config)
;; Activation gexp.
- #~(begin
- (use-modules (guix build utils))
- (define (mkdir-p/perms directory owner perms)
- (mkdir-p directory)
- (chown "/var/run/dovecot" (passwd:uid owner) (passwd:gid owner))
- (chmod directory perms))
- (define (build-subject parameters)
- (string-concatenate
- (map (lambda (pair)
- (let ((k (car pair)) (v (cdr pair)))
- (define (escape-char str chr)
- (string-join (string-split str chr) (string #\\ chr)))
- (string-append "/" k "="
- (escape-char (escape-char v #\=) #\/))))
- (filter (lambda (pair) (cdr pair)) parameters))))
- (define* (create-self-signed-certificate-if-absent
- #:key private-key public-key (owner (getpwnam "root"))
- (common-name (gethostname))
- (organization-name "GuixSD")
- (organization-unit-name "Default Self-Signed Certificate")
- (subject-parameters `(("CN" . ,common-name)
- ("O" . ,organization-name)
- ("OU" . ,organization-unit-name)))
- (subject (build-subject subject-parameters)))
- ;; Note that by default, OpenSSL outputs keys in PEM format. This
- ;; is what we want.
- (unless (file-exists? private-key)
- (cond
- ((zero? (system* (string-append #$openssl "/bin/openssl")
- "genrsa" "-out" private-key "2048"))
- (chown private-key (passwd:uid owner) (passwd:gid owner))
- (chmod private-key #o400))
- (else
- (format (current-error-port)
- "Failed to create private key at ~a.\n" private-key))))
- (unless (file-exists? public-key)
- (cond
- ((zero? (system* (string-append #$openssl "/bin/openssl")
- "req" "-new" "-x509" "-key" private-key
- "-out" public-key "-days" "3650"
- "-batch" "-subj" subject))
- (chown public-key (passwd:uid owner) (passwd:gid owner))
- (chmod public-key #o444))
- (else
- (format (current-error-port)
- "Failed to create public key at ~a.\n" public-key)))))
- (let ((user (getpwnam "dovecot")))
- (mkdir-p/perms "/var/run/dovecot" user #o755)
- (mkdir-p/perms "/var/lib/dovecot" user #o755)
- (mkdir-p/perms "/etc/dovecot" user #o755)
- (mkdir-p/perms "/etc/dovecot/private" user #o700)
- (create-self-signed-certificate-if-absent
- #:private-key "/etc/dovecot/private/default.pem"
- #:public-key "/etc/dovecot/default.pem"
- #:owner (getpwnam "root")
- #:common-name (format #f "Dovecot service on ~a" (gethostname))))))
+ (let ((config-str
+ (cond
+ ((opaque-dovecot-configuration? config)
+ (opaque-dovecot-configuration-string config))
+ (else
+ (with-output-to-string
+ (lambda ()
+ (serialize-configuration config
+ dovecot-configuration-fields)))))))
+ #~(begin
+ (use-modules (guix build utils))
+ (define (mkdir-p/perms directory owner perms)
+ (mkdir-p directory)
+ (chown "/var/run/dovecot" (passwd:uid owner) (passwd:gid owner))
+ (chmod directory perms))
+ (define (build-subject parameters)
+ (string-concatenate
+ (map (lambda (pair)
+ (let ((k (car pair)) (v (cdr pair)))
+ (define (escape-char str chr)
+ (string-join (string-split str chr) (string #\\ chr)))
+ (string-append "/" k "="
+ (escape-char (escape-char v #\=) #\/))))
+ (filter (lambda (pair) (cdr pair)) parameters))))
+ (define* (create-self-signed-certificate-if-absent
+ #:key private-key public-key (owner (getpwnam "root"))
+ (common-name (gethostname))
+ (organization-name "GuixSD")
+ (organization-unit-name "Default Self-Signed Certificate")
+ (subject-parameters `(("CN" . ,common-name)
+ ("O" . ,organization-name)
+ ("OU" . ,organization-unit-name)))
+ (subject (build-subject subject-parameters)))
+ ;; Note that by default, OpenSSL outputs keys in PEM format. This
+ ;; is what we want.
+ (unless (file-exists? private-key)
+ (cond
+ ((zero? (system* (string-append #$openssl "/bin/openssl")
+ "genrsa" "-out" private-key "2048"))
+ (chown private-key (passwd:uid owner) (passwd:gid owner))
+ (chmod private-key #o400))
+ (else
+ (format (current-error-port)
+ "Failed to create private key at ~a.\n" private-key))))
+ (unless (file-exists? public-key)
+ (cond
+ ((zero? (system* (string-append #$openssl "/bin/openssl")
+ "req" "-new" "-x509" "-key" private-key
+ "-out" public-key "-days" "3650"
+ "-batch" "-subj" subject))
+ (chown public-key (passwd:uid owner) (passwd:gid owner))
+ (chmod public-key #o444))
+ (else
+ (format (current-error-port)
+ "Failed to create public key at ~a.\n" public-key)))))
+ (let ((user (getpwnam "dovecot")))
+ (mkdir-p/perms "/var/run/dovecot" user #o755)
+ (mkdir-p/perms "/var/lib/dovecot" user #o755)
+ (mkdir-p/perms "/etc/dovecot" user #o755)
+ (copy-file #$(plain-file "dovecot.conf" config-str)
+ "/etc/dovecot/dovecot.conf")
+ (mkdir-p/perms "/etc/dovecot/private" user #o700)
+ (create-self-signed-certificate-if-absent
+ #:private-key "/etc/dovecot/private/default.pem"
+ #:public-key "/etc/dovecot/default.pem"
+ #:owner (getpwnam "root")
+ #:common-name (format #f "Dovecot service on ~a" (gethostname)))))))
(define (dovecot-shepherd-service config)
"Return a list of <shepherd-service> for CONFIG."
- (let* ((config-str
- (cond
- ((opaque-dovecot-configuration? config)
- (opaque-dovecot-configuration-string config))
- (else
- (with-output-to-string
- (lambda ()
- (serialize-configuration config
- dovecot-configuration-fields))))))
- (config-file (plain-file "dovecot.conf" config-str))
- (dovecot (if (opaque-dovecot-configuration? config)
- (opaque-dovecot-configuration-dovecot config)
- (dovecot-configuration-dovecot config))))
+ (let ((dovecot (if (opaque-dovecot-configuration? config)
+ (opaque-dovecot-configuration-dovecot config)
+ (dovecot-configuration-dovecot config))))
(list (shepherd-service
(documentation "Run the Dovecot POP3/IMAP mail server.")
(provision '(dovecot))
(requirement '(networking))
(start #~(make-forkexec-constructor
(list (string-append #$dovecot "/sbin/dovecot")
- "-F" "-c" #$config-file)))
+ "-F")))
(stop #~(make-forkexec-constructor
(list (string-append #$dovecot "/sbin/dovecot")
- "-c" #$config-file "stop")))))))
+ "stop")))))))
(define %dovecot-pam-services
(list (unix-pam-service "dovecot")))
(service-extension pam-root-service-type
(const %dovecot-pam-services))
(service-extension activation-service-type
- (const %dovecot-activation))))))
+ %dovecot-activation)))))
(define* (dovecot-service #:key (config (dovecot-configuration)))
"Return a service that runs @command{dovecot}, a mail server that can run
(compose list opensmtpd-configuration-package))
(service-extension shepherd-root-service-type
opensmtpd-shepherd-service)))))
+
+\f
+;;;
+;;; mail aliases.
+;;;
+
+(define (mail-aliases-etc aliases)
+ `(("aliases" ,(plain-file "aliases"
+ ;; Ideally we'd use a format string like
+ ;; "~:{~a: ~{~a~^,~}\n~}", but it gives a
+ ;; warning that I can't figure out how to fix,
+ ;; so we'll just use string-join below instead.
+ (format #f "~:{~a: ~a\n~}"
+ (map (match-lambda
+ ((alias addresses ...)
+ (list alias (string-join addresses ","))))
+ aliases))))))
+
+(define mail-aliases-service-type
+ (service-type
+ (name 'mail-aliases)
+ (extensions
+ (list (service-extension etc-service-type mail-aliases-etc)))
+ (compose concatenate)
+ (extend append)))
+
+\f
+;;;
+;;; Exim.
+;;;
+
+(define-record-type* <exim-configuration> exim-configuration
+ make-exim-configuration
+ exim-configuration?
+ (package exim-configuration-package ;<package>
+ (default exim))
+ (config-file exim-configuration-config-file ;file-like
+ (default #f)))
+
+(define %exim-accounts
+ (list (user-group
+ (name "exim")
+ (system? #t))
+ (user-account
+ (name "exim")
+ (group "exim")
+ (system? #t)
+ (comment "Exim Daemon")
+ (home-directory "/var/empty")
+ (shell (file-append shadow "/sbin/nologin")))))
+
+(define (exim-computed-config-file package config-file)
+ (computed-file "exim.conf"
+ #~(call-with-output-file #$output
+ (lambda (port)
+ (format port "
+exim_user = exim
+exim_group = exim
+.include ~a"
+ #$(or config-file
+ (file-append package "/etc/exim.conf")))))))
+
+(define exim-shepherd-service
+ (match-lambda
+ (($ <exim-configuration> package config-file)
+ (list (shepherd-service
+ (provision '(exim mta))
+ (documentation "Run the exim daemon.")
+ (requirement '(networking))
+ (start #~(make-forkexec-constructor
+ '(#$(file-append package "/bin/exim")
+ "-bd" "-v" "-C"
+ #$(exim-computed-config-file package config-file))))
+ (stop #~(make-kill-destructor)))))))
+
+(define exim-activation
+ (match-lambda
+ (($ <exim-configuration> package config-file)
+ (with-imported-modules '((guix build utils))
+ #~(begin
+ (use-modules (guix build utils))
+
+ (let ((uid (passwd:uid (getpw "exim")))
+ (gid (group:gid (getgr "exim"))))
+ (mkdir-p "/var/spool/exim")
+ (chown "/var/spool/exim" uid gid))
+
+ (zero? (system* #$(file-append package "/bin/exim")
+ "-bV" "-C" #$(exim-computed-config-file package config-file))))))))
+
+(define exim-profile
+ (compose list exim-configuration-package))
+
+(define exim-service-type
+ (service-type
+ (name 'exim)
+ (extensions
+ (list (service-extension shepherd-root-service-type exim-shepherd-service)
+ (service-extension account-service-type (const %exim-accounts))
+ (service-extension activation-service-type exim-activation)
+ (service-extension profile-service-type exim-profile)
+ (service-extension mail-aliases-service-type (const '()))))))