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