1{ config, lib, pkgs, ... }:
2
3let
4 inherit (lib) attrValues concatMapStringsSep concatStrings
5 concatStringsSep flatten imap1 literalExpression mapAttrsToList
6 mkEnableOption mkIf mkOption mkRemovedOptionModule optional optionalAttrs
7 optionalString singleton types mkRenamedOptionModule nameValuePair
8 mapAttrs' listToAttrs filter;
9 inherit (lib.strings) match;
10
11 cfg = config.services.dovecot2;
12 dovecotPkg = pkgs.dovecot;
13
14 baseDir = "/run/dovecot2";
15 stateDir = "/var/lib/dovecot";
16
17 sieveScriptSettings = mapAttrs' (to: _: nameValuePair "sieve_${to}" "${stateDir}/sieve/${to}") cfg.sieve.scripts;
18 imapSieveMailboxSettings = listToAttrs (flatten (imap1 (idx: el:
19 singleton {
20 name = "imapsieve_mailbox${toString idx}_name";
21 value = el.name;
22 } ++ optional (el.from != null) {
23 name = "imapsieve_mailbox${toString idx}_from";
24 value = el.from;
25 } ++ optional (el.causes != []) {
26 name = "imapsieve_mailbox${toString idx}_causes";
27 value = concatStringsSep "," el.causes;
28 } ++ optional (el.before != null) {
29 name = "imapsieve_mailbox${toString idx}_before";
30 value = "file:${stateDir}/imapsieve/before/${baseNameOf el.before}";
31 } ++ optional (el.after != null) {
32 name = "imapsieve_mailbox${toString idx}_after";
33 value = "file:${stateDir}/imapsieve/after/${baseNameOf el.after}";
34 }
35 ) cfg.imapsieve.mailbox));
36
37 mkExtraConfigCollisionWarning = term: ''
38 You referred to ${term} in `services.dovecot2.extraConfig`.
39
40 Due to gradual transition to structured configuration for plugin configuration, it is possible
41 this will cause your plugin configuration to be ignored.
42
43 Consider setting `services.dovecot2.pluginSettings.${term}` instead.
44 '';
45
46 # Those settings are automatically set based on other parts
47 # of this module.
48 automaticallySetPluginSettings = [
49 "sieve_plugins"
50 "sieve_extensions"
51 "sieve_global_extensions"
52 "sieve_pipe_bin_dir"
53 ]
54 ++ (builtins.attrNames sieveScriptSettings)
55 ++ (builtins.attrNames imapSieveMailboxSettings);
56
57 # The idea is to match everything that looks like `$term =`
58 # but not `# $term something something`
59 # or `# $term = some value` because those are comments.
60 configContainsSetting = lines: term: (match "^[^#]*\b${term}\b.*=" lines) != null;
61
62 warnAboutExtraConfigCollisions = map mkExtraConfigCollisionWarning (filter (configContainsSetting cfg.extraConfig) automaticallySetPluginSettings);
63
64 sievePipeBinScriptDirectory = pkgs.linkFarm "sieve-pipe-bins" (map (el: {
65 name = builtins.unsafeDiscardStringContext (baseNameOf el);
66 path = el;
67 }) cfg.sieve.pipeBins);
68
69 dovecotConf = concatStrings [
70 ''
71 base_dir = ${baseDir}
72 protocols = ${concatStringsSep " " cfg.protocols}
73 sendmail_path = /run/wrappers/bin/sendmail
74 # defining mail_plugins must be done before the first protocol {} filter because of https://doc.dovecot.org/configuration_manual/config_file/config_file_syntax/#variable-expansion
75 mail_plugins = $mail_plugins ${concatStringsSep " " cfg.mailPlugins.globally.enable}
76 ''
77
78 (
79 concatStringsSep "\n" (
80 mapAttrsToList (
81 protocol: plugins: ''
82 protocol ${protocol} {
83 mail_plugins = $mail_plugins ${concatStringsSep " " plugins.enable}
84 }
85 ''
86 ) cfg.mailPlugins.perProtocol
87 )
88 )
89
90 (
91 if cfg.sslServerCert == null then ''
92 ssl = no
93 disable_plaintext_auth = no
94 '' else ''
95 ssl_cert = <${cfg.sslServerCert}
96 ssl_key = <${cfg.sslServerKey}
97 ${optionalString (cfg.sslCACert != null) ("ssl_ca = <" + cfg.sslCACert)}
98 ${optionalString cfg.enableDHE ''ssl_dh = <${config.security.dhparams.params.dovecot2.path}''}
99 disable_plaintext_auth = yes
100 ''
101 )
102
103 ''
104 default_internal_user = ${cfg.user}
105 default_internal_group = ${cfg.group}
106 ${optionalString (cfg.mailUser != null) "mail_uid = ${cfg.mailUser}"}
107 ${optionalString (cfg.mailGroup != null) "mail_gid = ${cfg.mailGroup}"}
108
109 mail_location = ${cfg.mailLocation}
110
111 maildir_copy_with_hardlinks = yes
112 pop3_uidl_format = %08Xv%08Xu
113
114 auth_mechanisms = plain login
115
116 service auth {
117 user = root
118 }
119 ''
120
121 (
122 optionalString cfg.enablePAM ''
123 userdb {
124 driver = passwd
125 }
126
127 passdb {
128 driver = pam
129 args = ${optionalString cfg.showPAMFailure "failure_show_msg=yes"} dovecot2
130 }
131 ''
132 )
133
134 (
135 optionalString (cfg.mailboxes != {}) ''
136 namespace inbox {
137 inbox=yes
138 ${concatStringsSep "\n" (map mailboxConfig (attrValues cfg.mailboxes))}
139 }
140 ''
141 )
142
143 (
144 optionalString cfg.enableQuota ''
145 service quota-status {
146 executable = ${dovecotPkg}/libexec/dovecot/quota-status -p postfix
147 inet_listener {
148 port = ${cfg.quotaPort}
149 }
150 client_limit = 1
151 }
152
153 plugin {
154 quota_rule = *:storage=${cfg.quotaGlobalPerUser}
155 quota = count:User quota # per virtual mail user quota
156 quota_status_success = DUNNO
157 quota_status_nouser = DUNNO
158 quota_status_overquota = "552 5.2.2 Mailbox is full"
159 quota_grace = 10%%
160 quota_vsizes = yes
161 }
162 ''
163 )
164
165 # General plugin settings:
166 # - sieve is mostly generated here, refer to `pluginSettings` to follow
167 # the control flow.
168 ''
169 plugin {
170 ${concatStringsSep "\n" (mapAttrsToList (key: value: " ${key} = ${value}") cfg.pluginSettings)}
171 }
172 ''
173
174 cfg.extraConfig
175 ];
176
177 modulesDir = pkgs.symlinkJoin {
178 name = "dovecot-modules";
179 paths = map (pkg: "${pkg}/lib/dovecot") ([ dovecotPkg ] ++ map (module: module.override { dovecot = dovecotPkg; }) cfg.modules);
180 };
181
182 mailboxConfig = mailbox: ''
183 mailbox "${mailbox.name}" {
184 auto = ${toString mailbox.auto}
185 '' + optionalString (mailbox.autoexpunge != null) ''
186 autoexpunge = ${mailbox.autoexpunge}
187 '' + optionalString (mailbox.specialUse != null) ''
188 special_use = \${toString mailbox.specialUse}
189 '' + "}";
190
191 mailboxes = { name, ... }: {
192 options = {
193 name = mkOption {
194 type = types.strMatching ''[^"]+'';
195 example = "Spam";
196 default = name;
197 readOnly = true;
198 description = "The name of the mailbox.";
199 };
200 auto = mkOption {
201 type = types.enum [ "no" "create" "subscribe" ];
202 default = "no";
203 example = "subscribe";
204 description = "Whether to automatically create or create and subscribe to the mailbox or not.";
205 };
206 specialUse = mkOption {
207 type = types.nullOr (types.enum [ "All" "Archive" "Drafts" "Flagged" "Junk" "Sent" "Trash" ]);
208 default = null;
209 example = "Junk";
210 description = "Null if no special use flag is set. Other than that every use flag mentioned in the RFC is valid.";
211 };
212 autoexpunge = mkOption {
213 type = types.nullOr types.str;
214 default = null;
215 example = "60d";
216 description = ''
217 To automatically remove all email from the mailbox which is older than the
218 specified time.
219 '';
220 };
221 };
222 };
223in
224{
225 imports = [
226 (mkRemovedOptionModule [ "services" "dovecot2" "package" ] "")
227 (mkRenamedOptionModule [ "services" "dovecot2" "sieveScripts" ] [ "services" "dovecot2" "sieve" "scripts" ])
228 ];
229
230 options.services.dovecot2 = {
231 enable = mkEnableOption "the dovecot 2.x POP3/IMAP server";
232
233 enablePop3 = mkEnableOption "starting the POP3 listener (when Dovecot is enabled)";
234
235 enableImap = mkEnableOption "starting the IMAP listener (when Dovecot is enabled)" // { default = true; };
236
237 enableLmtp = mkEnableOption "starting the LMTP listener (when Dovecot is enabled)";
238
239 protocols = mkOption {
240 type = types.listOf types.str;
241 default = [];
242 description = "Additional listeners to start when Dovecot is enabled.";
243 };
244
245 user = mkOption {
246 type = types.str;
247 default = "dovecot2";
248 description = "Dovecot user name.";
249 };
250
251 group = mkOption {
252 type = types.str;
253 default = "dovecot2";
254 description = "Dovecot group name.";
255 };
256
257 extraConfig = mkOption {
258 type = types.lines;
259 default = "";
260 example = "mail_debug = yes";
261 description = "Additional entries to put verbatim into Dovecot's config file.";
262 };
263
264 mailPlugins =
265 let
266 plugins = hint: types.submodule {
267 options = {
268 enable = mkOption {
269 type = types.listOf types.str;
270 default = [];
271 description = "mail plugins to enable as a list of strings to append to the ${hint} `$mail_plugins` configuration variable";
272 };
273 };
274 };
275 in
276 mkOption {
277 type = with types; submodule {
278 options = {
279 globally = mkOption {
280 description = "Additional entries to add to the mail_plugins variable for all protocols";
281 type = plugins "top-level";
282 example = { enable = [ "virtual" ]; };
283 default = { enable = []; };
284 };
285 perProtocol = mkOption {
286 description = "Additional entries to add to the mail_plugins variable, per protocol";
287 type = attrsOf (plugins "corresponding per-protocol");
288 default = {};
289 example = { imap = [ "imap_acl" ]; };
290 };
291 };
292 };
293 description = "Additional entries to add to the mail_plugins variable, globally and per protocol";
294 example = {
295 globally.enable = [ "acl" ];
296 perProtocol.imap.enable = [ "imap_acl" ];
297 };
298 default = { globally.enable = []; perProtocol = {}; };
299 };
300
301 configFile = mkOption {
302 type = types.nullOr types.path;
303 default = null;
304 description = "Config file used for the whole dovecot configuration.";
305 apply = v: if v != null then v else pkgs.writeText "dovecot.conf" dovecotConf;
306 };
307
308 mailLocation = mkOption {
309 type = types.str;
310 default = "maildir:/var/spool/mail/%u"; /* Same as inbox, as postfix */
311 example = "maildir:~/mail:INBOX=/var/spool/mail/%u";
312 description = ''
313 Location that dovecot will use for mail folders. Dovecot mail_location option.
314 '';
315 };
316
317 mailUser = mkOption {
318 type = types.nullOr types.str;
319 default = null;
320 description = "Default user to store mail for virtual users.";
321 };
322
323 mailGroup = mkOption {
324 type = types.nullOr types.str;
325 default = null;
326 description = "Default group to store mail for virtual users.";
327 };
328
329 createMailUser = mkEnableOption ''automatically creating the user
330 given in {option}`services.dovecot.user` and the group
331 given in {option}`services.dovecot.group`.'' // { default = true; };
332
333 modules = mkOption {
334 type = types.listOf types.package;
335 default = [];
336 example = literalExpression "[ pkgs.dovecot_pigeonhole ]";
337 description = ''
338 Symlinks the contents of lib/dovecot of every given package into
339 /etc/dovecot/modules. This will make the given modules available
340 if a dovecot package with the module_dir patch applied is being used.
341 '';
342 };
343
344 sslCACert = mkOption {
345 type = types.nullOr types.str;
346 default = null;
347 description = "Path to the server's CA certificate key.";
348 };
349
350 sslServerCert = mkOption {
351 type = types.nullOr types.str;
352 default = null;
353 description = "Path to the server's public key.";
354 };
355
356 sslServerKey = mkOption {
357 type = types.nullOr types.str;
358 default = null;
359 description = "Path to the server's private key.";
360 };
361
362 enablePAM = mkEnableOption "creating a own Dovecot PAM service and configure PAM user logins" // { default = true; };
363
364 enableDHE = mkEnableOption "ssl_dh and generation of primes for the key exchange" // { default = true; };
365
366 showPAMFailure = mkEnableOption "showing the PAM failure message on authentication error (useful for OTPW)";
367
368 mailboxes = mkOption {
369 type = with types; coercedTo
370 (listOf unspecified)
371 (list: listToAttrs (map (entry: { name = entry.name; value = removeAttrs entry ["name"]; }) list))
372 (attrsOf (submodule mailboxes));
373 default = {};
374 example = literalExpression ''
375 {
376 Spam = { specialUse = "Junk"; auto = "create"; };
377 }
378 '';
379 description = "Configure mailboxes and auto create or subscribe them.";
380 };
381
382 enableQuota = mkEnableOption "the dovecot quota service";
383
384 quotaPort = mkOption {
385 type = types.str;
386 default = "12340";
387 description = ''
388 The Port the dovecot quota service binds to.
389 If using postfix, add check_policy_service inet:localhost:12340 to your smtpd_recipient_restrictions in your postfix config.
390 '';
391 };
392 quotaGlobalPerUser = mkOption {
393 type = types.str;
394 default = "100G";
395 example = "10G";
396 description = "Quota limit for the user in bytes. Supports suffixes b, k, M, G, T and %.";
397 };
398
399
400 pluginSettings = mkOption {
401 # types.str does not coerce from packages, like `sievePipeBinScriptDirectory`.
402 type = types.attrsOf (types.oneOf [ types.str types.package ]);
403 default = {};
404 example = literalExpression ''
405 {
406 sieve = "file:~/sieve;active=~/.dovecot.sieve";
407 }
408 '';
409 description = ''
410 Plugin settings for dovecot in general, e.g. `sieve`, `sieve_default`, etc.
411
412 Some of the other knobs of this module will influence by default the plugin settings, but you
413 can still override any plugin settings.
414
415 If you override a plugin setting, its value is cleared and you have to copy over the defaults.
416 '';
417 };
418
419 imapsieve.mailbox = mkOption {
420 default = [];
421 description = "Configure Sieve filtering rules on IMAP actions";
422 type = types.listOf (types.submodule ({ config, ... }: {
423 options = {
424 name = mkOption {
425 description = ''
426 This setting configures the name of a mailbox for which administrator scripts are configured.
427
428 The settings defined hereafter with matching sequence numbers apply to the mailbox named by this setting.
429
430 This setting supports wildcards with a syntax compatible with the IMAP LIST command, meaning that this setting can apply to multiple or even all ("*") mailboxes.
431 '';
432 example = "Junk";
433 type = types.str;
434 };
435
436 from = mkOption {
437 default = null;
438 description = ''
439 Only execute the administrator Sieve scripts for the mailbox configured with services.dovecot2.imapsieve.mailbox.<name>.name when the message originates from the indicated mailbox.
440
441 This setting supports wildcards with a syntax compatible with the IMAP LIST command, meaning that this setting can apply to multiple or even all ("*") mailboxes.
442 '';
443 example = "*";
444 type = types.nullOr types.str;
445 };
446
447 causes = mkOption {
448 default = [ ];
449 description = ''
450 Only execute the administrator Sieve scripts for the mailbox configured with services.dovecot2.imapsieve.mailbox.<name>.name when one of the listed IMAPSIEVE causes apply.
451
452 This has no effect on the user script, which is always executed no matter the cause.
453 '';
454 example = [ "COPY" "APPEND" ];
455 type = types.listOf (types.enum [ "APPEND" "COPY" "FLAG" ]);
456 };
457
458 before = mkOption {
459 default = null;
460 description = ''
461 When an IMAP event of interest occurs, this sieve script is executed before any user script respectively.
462
463 This setting each specify the location of a single sieve script. The semantics of this setting is similar to sieve_before: the specified scripts form a sequence together with the user script in which the next script is only executed when an (implicit) keep action is executed.
464 '';
465 example = literalExpression "./report-spam.sieve";
466 type = types.nullOr types.path;
467 };
468
469 after = mkOption {
470 default = null;
471 description = ''
472 When an IMAP event of interest occurs, this sieve script is executed after any user script respectively.
473
474 This setting each specify the location of a single sieve script. The semantics of this setting is similar to sieve_after: the specified scripts form a sequence together with the user script in which the next script is only executed when an (implicit) keep action is executed.
475 '';
476 example = literalExpression "./report-spam.sieve";
477 type = types.nullOr types.path;
478 };
479 };
480 }));
481 };
482
483 sieve = {
484 plugins = mkOption {
485 default = [];
486 example = [ "sieve_extprograms" ];
487 description = "Sieve plugins to load";
488 type = types.listOf types.str;
489 };
490
491 extensions = mkOption {
492 default = [];
493 description = "Sieve extensions for use in user scripts";
494 example = [ "notify" "imapflags" "vnd.dovecot.filter" ];
495 type = types.listOf types.str;
496 };
497
498 globalExtensions = mkOption {
499 default = [];
500 example = [ "vnd.dovecot.environment" ];
501 description = "Sieve extensions for use in global scripts";
502 type = types.listOf types.str;
503 };
504
505 scripts = mkOption {
506 type = types.attrsOf types.path;
507 default = {};
508 description = "Sieve scripts to be executed. Key is a sequence, e.g. 'before2', 'after' etc.";
509 };
510
511 pipeBins = mkOption {
512 default = [];
513 example = literalExpression ''
514 map lib.getExe [
515 (pkgs.writeShellScriptBin "learn-ham.sh" "exec ''${pkgs.rspamd}/bin/rspamc learn_ham")
516 (pkgs.writeShellScriptBin "learn-spam.sh" "exec ''${pkgs.rspamd}/bin/rspamc learn_spam")
517 ]
518 '';
519 description = "Programs available for use by the vnd.dovecot.pipe extension";
520 type = types.listOf types.path;
521 };
522 };
523 };
524
525 config = mkIf cfg.enable {
526 security.pam.services.dovecot2 = mkIf cfg.enablePAM {};
527
528 security.dhparams = mkIf (cfg.sslServerCert != null && cfg.enableDHE) {
529 enable = true;
530 params.dovecot2 = {};
531 };
532
533 services.dovecot2 = {
534 protocols =
535 optional cfg.enableImap "imap"
536 ++ optional cfg.enablePop3 "pop3"
537 ++ optional cfg.enableLmtp "lmtp";
538
539 mailPlugins = mkIf cfg.enableQuota {
540 globally.enable = [ "quota" ];
541 perProtocol.imap.enable = [ "imap_quota" ];
542 };
543
544 sieve.plugins =
545 optional (cfg.imapsieve.mailbox != []) "sieve_imapsieve"
546 ++ optional (cfg.sieve.pipeBins != []) "sieve_extprograms";
547
548 sieve.globalExtensions = optional (cfg.sieve.pipeBins != []) "vnd.dovecot.pipe";
549
550 pluginSettings = lib.mapAttrs (n: lib.mkDefault) ({
551 sieve_plugins = concatStringsSep " " cfg.sieve.plugins;
552 sieve_extensions = concatStringsSep " " (map (el: "+${el}") cfg.sieve.extensions);
553 sieve_global_extensions = concatStringsSep " " (map (el: "+${el}") cfg.sieve.globalExtensions);
554 sieve_pipe_bin_dir = sievePipeBinScriptDirectory;
555 } // sieveScriptSettings // imapSieveMailboxSettings);
556 };
557
558 users.users = {
559 dovenull =
560 {
561 uid = config.ids.uids.dovenull2;
562 description = "Dovecot user for untrusted logins";
563 group = "dovenull";
564 };
565 } // optionalAttrs (cfg.user == "dovecot2") {
566 dovecot2 =
567 {
568 uid = config.ids.uids.dovecot2;
569 description = "Dovecot user";
570 group = cfg.group;
571 };
572 } // optionalAttrs (cfg.createMailUser && cfg.mailUser != null) {
573 ${cfg.mailUser} =
574 { description = "Virtual Mail User"; isSystemUser = true; } // optionalAttrs (cfg.mailGroup != null)
575 { group = cfg.mailGroup; };
576 };
577
578 users.groups = {
579 dovenull.gid = config.ids.gids.dovenull2;
580 } // optionalAttrs (cfg.group == "dovecot2") {
581 dovecot2.gid = config.ids.gids.dovecot2;
582 } // optionalAttrs (cfg.createMailUser && cfg.mailGroup != null) {
583 ${cfg.mailGroup} = {};
584 };
585
586 environment.etc."dovecot/modules".source = modulesDir;
587 environment.etc."dovecot/dovecot.conf".source = cfg.configFile;
588
589 systemd.services.dovecot2 = {
590 description = "Dovecot IMAP/POP3 server";
591
592 after = [ "network.target" ];
593 wantedBy = [ "multi-user.target" ];
594 restartTriggers = [ cfg.configFile modulesDir ];
595
596 startLimitIntervalSec = 60; # 1 min
597 serviceConfig = {
598 Type = "notify";
599 ExecStart = "${dovecotPkg}/sbin/dovecot -F";
600 ExecReload = "${dovecotPkg}/sbin/doveadm reload";
601 Restart = "on-failure";
602 RestartSec = "1s";
603 RuntimeDirectory = [ "dovecot2" ];
604 };
605
606 # When copying sieve scripts preserve the original time stamp
607 # (should be 0) so that the compiled sieve script is newer than
608 # the source file and Dovecot won't try to compile it.
609 preStart = ''
610 rm -rf ${stateDir}/sieve ${stateDir}/imapsieve
611 '' + optionalString (cfg.sieve.scripts != {}) ''
612 mkdir -p ${stateDir}/sieve
613 ${concatStringsSep "\n" (
614 mapAttrsToList (
615 to: from: ''
616 if [ -d '${from}' ]; then
617 mkdir '${stateDir}/sieve/${to}'
618 cp -p "${from}/"*.sieve '${stateDir}/sieve/${to}'
619 else
620 cp -p '${from}' '${stateDir}/sieve/${to}'
621 fi
622 ${pkgs.dovecot_pigeonhole}/bin/sievec '${stateDir}/sieve/${to}'
623 ''
624 ) cfg.sieve.scripts
625 )}
626 chown -R '${cfg.mailUser}:${cfg.mailGroup}' '${stateDir}/sieve'
627 ''
628 + optionalString (cfg.imapsieve.mailbox != []) ''
629 mkdir -p ${stateDir}/imapsieve/{before,after}
630
631 ${
632 concatMapStringsSep "\n"
633 (el:
634 optionalString (el.before != null) ''
635 cp -p ${el.before} ${stateDir}/imapsieve/before/${baseNameOf el.before}
636 ${pkgs.dovecot_pigeonhole}/bin/sievec '${stateDir}/imapsieve/before/${baseNameOf el.before}'
637 ''
638 + optionalString (el.after != null) ''
639 cp -p ${el.after} ${stateDir}/imapsieve/after/${baseNameOf el.after}
640 ${pkgs.dovecot_pigeonhole}/bin/sievec '${stateDir}/imapsieve/after/${baseNameOf el.after}'
641 ''
642 )
643 cfg.imapsieve.mailbox
644 }
645
646 ${
647 optionalString (cfg.mailUser != null && cfg.mailGroup != null)
648 "chown -R '${cfg.mailUser}:${cfg.mailGroup}' '${stateDir}/imapsieve'"
649 }
650 '';
651 };
652
653 environment.systemPackages = [ dovecotPkg ];
654
655 warnings = warnAboutExtraConfigCollisions;
656
657 assertions = [
658 {
659 assertion = (cfg.sslServerCert == null) == (cfg.sslServerKey == null)
660 && (cfg.sslCACert != null -> !(cfg.sslServerCert == null || cfg.sslServerKey == null));
661 message = "dovecot needs both sslServerCert and sslServerKey defined for working crypto";
662 }
663 {
664 assertion = cfg.showPAMFailure -> cfg.enablePAM;
665 message = "dovecot is configured with showPAMFailure while enablePAM is disabled";
666 }
667 {
668 assertion = cfg.sieve.scripts != {} -> (cfg.mailUser != null && cfg.mailGroup != null);
669 message = "dovecot requires mailUser and mailGroup to be set when `sieve.scripts` is set";
670 }
671 ];
672
673 };
674
675 meta.maintainers = [ lib.maintainers.dblsaiko ];
676}