1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.clamsmtp;
9 clamdSocket = "/run/clamav/clamd.ctl"; # See services/security/clamav.nix
10in
11{
12 ##### interface
13 options = {
14 services.clamsmtp = {
15 enable = lib.mkOption {
16 type = lib.types.bool;
17 default = false;
18 description = "Whether to enable clamsmtp.";
19 };
20
21 instances = lib.mkOption {
22 description = "Instances of clamsmtp to run.";
23 type = lib.types.listOf (
24 lib.types.submodule {
25 options = {
26 action = lib.mkOption {
27 type = lib.types.enum [
28 "bounce"
29 "drop"
30 "pass"
31 ];
32 default = "drop";
33 description = ''
34 Action to take when a virus is detected.
35
36 Note that viruses often spoof sender addresses, so bouncing is
37 in most cases not a good idea.
38 '';
39 };
40
41 header = lib.mkOption {
42 type = lib.types.str;
43 default = "";
44 example = "X-Virus-Scanned: ClamAV using ClamSMTP";
45 description = ''
46 A header to add to scanned messages. See {manpage}`clamsmtpd.conf(5)` for
47 more details. Empty means no header.
48 '';
49 };
50
51 keepAlives = lib.mkOption {
52 type = lib.types.int;
53 default = 0;
54 description = ''
55 Number of seconds to wait between each NOOP sent to the sending
56 server. 0 to disable.
57
58 This is meant for slow servers where the sending MTA times out
59 waiting for clamd to scan the file.
60 '';
61 };
62
63 listen = lib.mkOption {
64 type = lib.types.str;
65 example = "127.0.0.1:10025";
66 description = ''
67 Address to wait for incoming SMTP connections on. See
68 {manpage}`clamsmtpd.conf(5)` for more details.
69 '';
70 };
71
72 quarantine = lib.mkOption {
73 type = lib.types.bool;
74 default = false;
75 description = ''
76 Whether to quarantine files that contain viruses by leaving them
77 in the temporary directory.
78 '';
79 };
80
81 maxConnections = lib.mkOption {
82 type = lib.types.int;
83 default = 64;
84 description = "Maximum number of connections to accept at once.";
85 };
86
87 outAddress = lib.mkOption {
88 type = lib.types.str;
89 description = ''
90 Address of the SMTP server to send email to once it has been
91 scanned.
92 '';
93 };
94
95 tempDirectory = lib.mkOption {
96 type = lib.types.str;
97 default = "/tmp";
98 description = ''
99 Temporary directory that needs to be accessible to both clamd
100 and clamsmtpd.
101 '';
102 };
103
104 timeout = lib.mkOption {
105 type = lib.types.int;
106 default = 180;
107 description = "Time-out for network connections.";
108 };
109
110 transparentProxy = lib.mkOption {
111 type = lib.types.bool;
112 default = false;
113 description = "Enable clamsmtp's transparent proxy support.";
114 };
115
116 virusAction = lib.mkOption {
117 type = with lib.types; nullOr path;
118 default = null;
119 description = ''
120 Command to run when a virus is found. Please see VIRUS ACTION in
121 {manpage}`clamsmtpd(8)` for a discussion of this option and its safe use.
122 '';
123 };
124
125 xClient = lib.mkOption {
126 type = lib.types.bool;
127 default = false;
128 description = ''
129 Send the XCLIENT command to the receiving server, for forwarding
130 client addresses and connection information if the receiving
131 server supports this feature.
132 '';
133 };
134 };
135 }
136 );
137 };
138 };
139 };
140
141 ##### implementation
142 config =
143 let
144 configfile =
145 conf:
146 pkgs.writeText "clamsmtpd.conf" ''
147 Action: ${conf.action}
148 ClamAddress: ${clamdSocket}
149 Header: ${conf.header}
150 KeepAlives: ${toString conf.keepAlives}
151 Listen: ${conf.listen}
152 Quarantine: ${if conf.quarantine then "on" else "off"}
153 MaxConnections: ${toString conf.maxConnections}
154 OutAddress: ${conf.outAddress}
155 TempDirectory: ${conf.tempDirectory}
156 TimeOut: ${toString conf.timeout}
157 TransparentProxy: ${if conf.transparentProxy then "on" else "off"}
158 User: clamav
159 ${lib.optionalString (conf.virusAction != null) "VirusAction: ${conf.virusAction}"}
160 XClient: ${if conf.xClient then "on" else "off"}
161 '';
162 in
163 lib.mkIf cfg.enable {
164 assertions = [
165 {
166 assertion = config.services.clamav.daemon.enable;
167 message = "clamsmtp requires clamav to be enabled";
168 }
169 ];
170
171 systemd.services = lib.listToAttrs (
172 lib.imap1 (
173 i: conf:
174 lib.nameValuePair "clamsmtp-${toString i}" {
175 description = "ClamSMTP instance ${toString i}";
176 wantedBy = [ "multi-user.target" ];
177 script = "exec ${pkgs.clamsmtp}/bin/clamsmtpd -f ${configfile conf}";
178 after = [ "clamav-daemon.service" ];
179 requires = [ "clamav-daemon.service" ];
180 serviceConfig.Type = "forking";
181 serviceConfig.PrivateTmp = "yes";
182 unitConfig.JoinsNamespaceOf = "clamav-daemon.service";
183 }
184 ) cfg.instances
185 );
186 };
187
188 meta.maintainers = with lib.maintainers; [ ekleog ];
189}