Browse Source

Initial mailstack implementation

Simon Detheridge 4 months ago
commit
c4731279e8
100 changed files with 6452 additions and 0 deletions
  1. 3 0
      bin/add_user
  2. 3 0
      bin/del_user
  3. 3 0
      bin/list_users
  4. 3 0
      bin/set_user_password
  5. 13 0
      clamav/Dockerfile
  6. 715 0
      clamav/clamd.conf
  7. 9 0
      clamav/run.sh
  8. 16 0
      db/Dockerfile
  9. 7 0
      db/bin/Gemfile
  10. 15 0
      db/bin/Gemfile.lock
  11. 66 0
      db/bin/add_user
  12. 28 0
      db/bin/del_user
  13. 66 0
      db/bin/set_user_password
  14. 107 0
      db/schema.sql
  15. 149 0
      docker-compose.yml
  16. 19 0
      dovecot/Dockerfile
  17. 100 0
      dovecot/conf/dovecot.conf
  18. 6 0
      dovecot/conf/sql-authdb.conf.template
  19. 7 0
      dovecot/run.sh
  20. 28 0
      haraka/Dockerfile
  21. 1 0
      haraka/dkim/.gitignore
  22. 78 0
      haraka/dkim/dkim_key_gen.sh
  23. 10 0
      haraka/haraka/config/aliases_mysql.ini
  24. 9 0
      haraka/haraka/config/auth_mysql_query.ini
  25. 1 0
      haraka/haraka/config/databytes
  26. 1 0
      haraka/haraka/config/dkim_sign.ini
  27. 2 0
      haraka/haraka/config/lmtp.ini
  28. 11 0
      haraka/haraka/config/log.ini
  29. 1 0
      haraka/haraka/config/me
  30. 10 0
      haraka/haraka/config/mysql_provider.ini
  31. 81 0
      haraka/haraka/config/plugins
  32. 7 0
      haraka/haraka/config/rcpt_to.mysql.ini
  33. 2 0
      haraka/haraka/config/relay.ini
  34. 1 0
      haraka/haraka/config/relay_acl_allow
  35. 6 0
      haraka/haraka/config/rspamd.ini
  36. 42 0
      haraka/haraka/config/smtp.ini
  37. 5 0
      haraka/haraka/config/tls.ini
  38. 71 0
      haraka/haraka/plugins/aliases_mysql.js
  39. 47 0
      haraka/haraka/plugins/auth_mysql_query.js
  40. 75 0
      haraka/haraka/plugins/mysql_provider.js
  41. 70 0
      haraka/haraka/plugins/rcpt_to.mysql.js
  42. 5 0
      haraka/run.sh
  43. 7 0
      proxy/Dockerfile
  44. 53 0
      proxy/nginx.conf
  45. 1 0
      rainloop/Dockerfile
  46. 3 0
      redis/Dockerfile
  47. 2 0
      redis/redis.conf
  48. 14 0
      rspamd/Dockerfile
  49. 7 0
      rspamd/initial_train.sh
  50. 821 0
      rspamd/rspamd/2tld.inc
  51. 30 0
      rspamd/rspamd/actions.conf
  52. 11 0
      rspamd/rspamd/cgp.inc
  53. 38 0
      rspamd/rspamd/common.conf
  54. 128 0
      rspamd/rspamd/composites.conf
  55. 70 0
      rspamd/rspamd/dmarc_whitelist.inc
  56. 111 0
      rspamd/rspamd/groups.conf
  57. 42 0
      rspamd/rspamd/local.d/antivirus.conf
  58. 16 0
      rspamd/rspamd/local.d/classifier-bayes.conf
  59. 2 0
      rspamd/rspamd/local.d/redis.conf
  60. 5 0
      rspamd/rspamd/local.d/worker-normal.inc
  61. 22 0
      rspamd/rspamd/logging.inc
  62. 208 0
      rspamd/rspamd/maillist.inc
  63. 24 0
      rspamd/rspamd/metrics.conf
  64. 22 0
      rspamd/rspamd/mid.inc
  65. 1532 0
      rspamd/rspamd/mime_types.inc
  66. 3 0
      rspamd/rspamd/modules.conf
  67. 56 0
      rspamd/rspamd/modules.d/antivirus.conf
  68. 68 0
      rspamd/rspamd/modules.d/arc.conf
  69. 30 0
      rspamd/rspamd/modules.d/asn.conf
  70. 22 0
      rspamd/rspamd/modules.d/chartable.conf
  71. 69 0
      rspamd/rspamd/modules.d/clickhouse.conf
  72. 29 0
      rspamd/rspamd/modules.d/dcc.conf
  73. 26 0
      rspamd/rspamd/modules.d/dkim.conf
  74. 73 0
      rspamd/rspamd/modules.d/dkim_signing.conf
  75. 20 0
      rspamd/rspamd/modules.d/dmarc.conf
  76. 21 0
      rspamd/rspamd/modules.d/elastic.conf
  77. 49 0
      rspamd/rspamd/modules.d/emails.conf
  78. 23 0
      rspamd/rspamd/modules.d/force_actions.conf
  79. 23 0
      rspamd/rspamd/modules.d/forged_recipients.conf
  80. 35 0
      rspamd/rspamd/modules.d/greylist.conf
  81. 27 0
      rspamd/rspamd/modules.d/hfilter.conf
  82. 26 0
      rspamd/rspamd/modules.d/history_redis.conf
  83. 26 0
      rspamd/rspamd/modules.d/ip_score.conf
  84. 21 0
      rspamd/rspamd/modules.d/maillist.conf
  85. 25 0
      rspamd/rspamd/modules.d/metadata_exporter.conf
  86. 23 0
      rspamd/rspamd/modules.d/metric_exporter.conf
  87. 28 0
      rspamd/rspamd/modules.d/mid.conf
  88. 30 0
      rspamd/rspamd/modules.d/milter_headers.conf
  89. 40 0
      rspamd/rspamd/modules.d/mime_types.conf
  90. 168 0
      rspamd/rspamd/modules.d/multimap.conf
  91. 44 0
      rspamd/rspamd/modules.d/mx_check.conf
  92. 40 0
      rspamd/rspamd/modules.d/neural.conf
  93. 27 0
      rspamd/rspamd/modules.d/once_received.conf
  94. 35 0
      rspamd/rspamd/modules.d/phishing.conf
  95. 43 0
      rspamd/rspamd/modules.d/ratelimit.conf
  96. 154 0
      rspamd/rspamd/modules.d/rbl.conf
  97. 27 0
      rspamd/rspamd/modules.d/redis.conf
  98. 22 0
      rspamd/rspamd/modules.d/regexp.conf
  99. 31 0
      rspamd/rspamd/modules.d/replies.conf
  100. 0 0
      rspamd/rspamd/modules.d/reputation.conf

+ 3 - 0
bin/add_user

@@ -0,0 +1,3 @@
+#!/bin/bash
+
+docker-compose exec db /scripts/add_user "$@"

+ 3 - 0
bin/del_user

@@ -0,0 +1,3 @@
+#!/bin/bash
+
+docker-compose exec db /scripts/del_user "$@"

+ 3 - 0
bin/list_users

@@ -0,0 +1,3 @@
+#!/bin/bash
+
+docker-compose exec db mysql -udovecot -p$MYSQL_PASSWORD -e "SELECT CONCAT(username,'@',domain) FROM users" -BN dovecot

+ 3 - 0
bin/set_user_password

@@ -0,0 +1,3 @@
+#!/bin/bash
+
+docker-compose exec db /scripts/set_user_password "$@"

+ 13 - 0
clamav/Dockerfile

@@ -0,0 +1,13 @@
+FROM alpine:3.8
+
+RUN apk add --no-cache --update clamav clamav-daemon && \
+    mkdir /run/clamav && \
+    chown clamav:clamav /run/clamav /var/lib/clamav /var/log/clamav -R
+
+ADD clamav/run.sh /run.sh
+ADD clamav/clamd.conf /etc/clamav/clamd.conf
+
+EXPOSE 3310
+USER clamav
+CMD /run.sh
+

+ 715 - 0
clamav/clamd.conf

@@ -0,0 +1,715 @@
+##
+## Example config file for the Clam AV daemon
+## Please read the clamd.conf(5) manual before editing this file.
+##
+
+
+# Comment or remove the line below.
+# Example
+
+# Uncomment this option to enable logging.
+# LogFile must be writable for the user running daemon.
+# A full path is required.
+# Default: disabled
+LogFile /var/log/clamav/clamd.log
+
+# By default the log file is locked for writing - the lock protects against
+# running clamd multiple times (if want to run another clamd, please
+# copy the configuration file, change the LogFile variable, and run
+# the daemon with --config-file option).
+# This option disables log file locking.
+# Default: no
+#LogFileUnlock yes
+
+# Maximum size of the log file.
+# Value of 0 disables the limit.
+# You may use 'M' or 'm' for megabytes (1M = 1m = 1048576 bytes)
+# and 'K' or 'k' for kilobytes (1K = 1k = 1024 bytes). To specify the size
+# in bytes just don't use modifiers. If LogFileMaxSize is enabled, log
+# rotation (the LogRotate option) will always be enabled.
+# Default: 1M
+#LogFileMaxSize 2M
+
+# Log time with each message.
+# Default: no
+LogTime yes
+
+# Also log clean files. Useful in debugging but drastically increases the
+# log size.
+# Default: no
+#LogClean yes
+
+# Use system logger (can work together with LogFile).
+# Default: no
+#LogSyslog yes
+
+# Specify the type of syslog messages - please refer to 'man syslog'
+# for facility names.
+# Default: LOG_LOCAL6
+#LogFacility LOG_MAIL
+
+# Enable verbose logging.
+# Default: no
+#LogVerbose yes
+
+# Enable log rotation. Always enabled when LogFileMaxSize is enabled.
+# Default: no
+#LogRotate yes
+
+# Enable Prelude output.
+# Default: no
+#PreludeEnable yes
+#
+# Set the name of the analyzer used by prelude-admin.
+# Default: ClamAV
+#PreludeAnalyzerName ClamAV
+
+# Log additional information about the infected file, such as its
+# size and hash, together with the virus name.
+#ExtendedDetectionInfo yes
+
+# This option allows you to save a process identifier of the listening
+# daemon (main thread).
+# Default: disabled
+PidFile /run/clamav/clamd.pid
+
+# Optional path to the global temporary directory.
+# Default: system specific (usually /tmp or /var/tmp).
+#TemporaryDirectory /var/tmp
+
+# Path to the database directory.
+# Default: hardcoded (depends on installation options)
+#DatabaseDirectory /var/lib/clamav
+
+# Only load the official signatures published by the ClamAV project.
+# Default: no
+#OfficialDatabaseOnly no
+
+# The daemon can work in local mode, network mode or both. 
+# Due to security reasons we recommend the local mode.
+
+# Path to a local socket file the daemon will listen on.
+# Default: disabled (must be specified by a user)
+LocalSocket /run/clamav/clamd.sock
+
+# Sets the group ownership on the unix socket.
+# Default: disabled (the primary group of the user running clamd)
+#LocalSocketGroup virusgroup
+
+# Sets the permissions on the unix socket to the specified mode.
+# Default: disabled (socket is world accessible)
+#LocalSocketMode 660
+
+# Remove stale socket after unclean shutdown.
+# Default: yes
+#FixStaleSocket yes
+
+# TCP port address.
+# Default: no
+TCPSocket 3310
+
+# TCP address.
+# By default we bind to INADDR_ANY, probably not wise.
+# Enable the following to provide some degree of protection
+# from the outside world. This option can be specified multiple
+# times if you want to listen on multiple IPs. IPv6 is now supported.
+# Default: no
+#TCPAddr 127.0.0.1
+
+# Maximum length the queue of pending connections may grow to.
+# Default: 200
+#MaxConnectionQueueLength 30
+
+# Clamd uses FTP-like protocol to receive data from remote clients.
+# If you are using clamav-milter to balance load between remote clamd daemons
+# on firewall servers you may need to tune the options below.
+
+# Close the connection when the data size limit is exceeded.
+# The value should match your MTA's limit for a maximum attachment size.
+# Default: 25M
+#StreamMaxLength 10M
+
+# Limit port range.
+# Default: 1024
+#StreamMinPort 30000
+# Default: 2048
+#StreamMaxPort 32000
+
+# Maximum number of threads running at the same time.
+# Default: 10
+#MaxThreads 20
+
+# Waiting for data from a client socket will timeout after this time (seconds).
+# Default: 120
+#ReadTimeout 300
+
+# This option specifies the time (in seconds) after which clamd should
+# timeout if a client doesn't provide any initial command after connecting.
+# Default: 5
+#CommandReadTimeout 5
+
+# This option specifies how long to wait (in milliseconds) if the send buffer
+# is full.
+# Keep this value low to prevent clamd hanging
+#
+# Default: 500
+#SendBufTimeout 200
+
+# Maximum number of queued items (including those being processed by
+# MaxThreads threads)
+# It is recommended to have this value at least twice MaxThreads if possible.
+# WARNING: you shouldn't increase this too much to avoid running out  of file
+# descriptors,
+# the following condition should hold:
+# MaxThreads*MaxRecursion + (MaxQueue - MaxThreads) + 6< RLIMIT_NOFILE (usual
+# max is 1024)
+#
+# Default: 100
+#MaxQueue 200
+
+# Waiting for a new job will timeout after this time (seconds).
+# Default: 30
+#IdleTimeout 60
+
+# Don't scan files and directories matching regex
+# This directive can be used multiple times
+# Default: scan all
+#ExcludePath ^/proc/
+#ExcludePath ^/sys/
+
+# Maximum depth directories are scanned at.
+# Default: 15
+#MaxDirectoryRecursion 20
+
+# Follow directory symlinks.
+# Default: no
+#FollowDirectorySymlinks yes
+
+# Follow regular file symlinks.
+# Default: no
+#FollowFileSymlinks yes
+
+# Scan files and directories on other filesystems.
+# Default: yes
+#CrossFilesystems yes
+
+# Perform a database check.
+# Default: 600 (10 min)
+#SelfCheck 600
+
+# Execute a command when virus is found. In the command string %v will
+# be replaced with the virus name.
+# Default: no
+#VirusEvent /usr/local/bin/send_sms 123456789 "VIRUS ALERT: %v"
+
+# Run as another user (clamd must be started by root for this option to work)
+# Default: don't drop privileges
+User clamav
+
+# Stop daemon when libclamav reports out of memory condition.
+#ExitOnOOM yes
+
+# Don't fork into background.
+# Default: no
+#Foreground yes
+
+# Enable debug messages in libclamav.
+# Default: no
+#Debug yes
+
+# Do not remove temporary files (for debug purposes).
+# Default: no
+#LeaveTemporaryFiles yes
+
+# Permit use of the ALLMATCHSCAN command. If set to no, clamd will reject
+# any ALLMATCHSCAN command as invalid.
+# Default: yes
+#AllowAllMatchScan no
+
+# Detect Possibly Unwanted Applications.
+# Default: no
+#DetectPUA yes
+
+# Exclude a specific PUA category. This directive can be used multiple times.
+# See https://github.com/vrtadmin/clamav-faq/blob/master/faq/faq-pua.md for 
+# the complete list of PUA categories.
+# Default: Load all categories (if DetectPUA is activated)
+#ExcludePUA NetTool
+#ExcludePUA PWTool
+
+# Only include a specific PUA category. This directive can be used multiple
+# times.
+# Default: Load all categories (if DetectPUA is activated)
+#IncludePUA Spy
+#IncludePUA Scanner
+#IncludePUA RAT
+
+# In some cases (eg. complex malware, exploits in graphic files, and others),
+# ClamAV uses special algorithms to provide accurate detection. This option
+# controls the algorithmic detection.
+# Default: yes
+#AlgorithmicDetection yes
+
+# This option causes memory or nested map scans to dump the content to disk.
+# If you turn on this option, more data is written to disk and is available
+# when the LeaveTemporaryFiles option is enabled.
+#ForceToDisk yes
+
+# This option allows you to disable the caching feature of the engine. By
+# default, the engine will store an MD5 in a cache of any files that are
+# not flagged as virus or that hit limits checks. Disabling the cache will
+# have a negative performance impact on large scans.
+# Default: no
+#DisableCache yes
+
+##
+## Executable files
+##
+
+# PE stands for Portable Executable - it's an executable file format used
+# in all 32 and 64-bit versions of Windows operating systems. This option
+# allows ClamAV to perform a deeper analysis of executable files and it's also
+# required for decompression of popular executable packers such as UPX, FSG,
+# and Petite. If you turn off this option, the original files will still be
+# scanned, but without additional processing.
+# Default: yes
+#ScanPE yes
+
+# Certain PE files contain an authenticode signature. By default, we check
+# the signature chain in the PE file against a database of trusted and
+# revoked certificates if the file being scanned is marked as a virus.
+# If any certificate in the chain validates against any trusted root, but
+# does not match any revoked certificate, the file is marked as whitelisted.
+# If the file does match a revoked certificate, the file is marked as virus.
+# The following setting completely turns off authenticode verification.
+# Default: no
+#DisableCertCheck yes
+
+# Executable and Linking Format is a standard format for UN*X executables.
+# This option allows you to control the scanning of ELF files.
+# If you turn off this option, the original files will still be scanned, but
+# without additional processing.
+# Default: yes
+#ScanELF yes
+
+# With this option clamav will try to detect broken executables (both PE and
+# ELF) and mark them as Broken.Executable.
+# Default: no
+#DetectBrokenExecutables yes
+
+
+##
+## Documents
+##
+
+# This option enables scanning of OLE2 files, such as Microsoft Office
+# documents and .msi files.
+# If you turn off this option, the original files will still be scanned, but
+# without additional processing.
+# Default: yes
+#ScanOLE2 yes
+
+# With this option enabled OLE2 files with VBA macros, which were not
+# detected by signatures will be marked as "Heuristics.OLE2.ContainsMacros".
+# Default: no
+#OLE2BlockMacros no
+
+# This option enables scanning within PDF files.
+# If you turn off this option, the original files will still be scanned, but
+# without decoding and additional processing.
+# Default: yes
+#ScanPDF yes
+
+# This option enables scanning within SWF files.
+# If you turn off this option, the original files will still be scanned, but
+# without decoding and additional processing.
+# Default: yes
+#ScanSWF yes
+
+# This option enables scanning xml-based document files supported by libclamav.
+# If you turn off this option, the original files will still be scanned, but
+# without additional processing.
+# Default: yes
+#ScanXMLDOCS yes
+
+# This option enables scanning of HWP3 files.
+# If you turn off this option, the original files will still be scanned, but
+# without additional processing.
+# Default: yes
+#ScanHWP3 yes
+
+
+##
+## Mail files
+##
+
+# Enable internal e-mail scanner.
+# If you turn off this option, the original files will still be scanned, but
+# without parsing individual messages/attachments.
+# Default: yes
+#ScanMail yes
+
+# Scan RFC1341 messages split over many emails.
+# You will need to periodically clean up $TemporaryDirectory/clamav-partial
+# directory.
+# WARNING: This option may open your system to a DoS attack.
+#	   Never use it on loaded servers.
+# Default: no
+#ScanPartialMessages yes
+
+# With this option enabled ClamAV will try to detect phishing attempts by using
+# signatures.
+# Default: yes
+#PhishingSignatures yes
+
+# Scan URLs found in mails for phishing attempts using heuristics.
+# Default: yes
+#PhishingScanURLs yes
+
+# Always block SSL mismatches in URLs, even if the URL isn't in the database.
+# This can lead to false positives.
+#
+# Default: no
+#PhishingAlwaysBlockSSLMismatch no
+
+# Always block cloaked URLs, even if URL isn't in database.
+# This can lead to false positives.
+#
+# Default: no
+#PhishingAlwaysBlockCloak no
+
+# Detect partition intersections in raw disk images using heuristics.
+# Default: no
+#PartitionIntersection no
+
+# Allow heuristic match to take precedence.
+# When enabled, if a heuristic scan (such as phishingScan) detects
+# a possible virus/phish it will stop scan immediately. Recommended, saves CPU
+# scan-time.
+# When disabled, virus/phish detected by heuristic scans will be reported
+# only at the end of a scan. If an archive contains both a heuristically
+# detected virus/phish, and a real malware, the real malware will be reported.
+#
+# Keep this disabled if you intend to handle "*.Heuristics.*" viruses 
+# differently from "real" malware.
+# If a non-heuristically-detected virus (signature-based) is found first, 
+# the scan is interrupted immediately, regardless of this config option.
+#
+# Default: no
+#HeuristicScanPrecedence yes
+
+
+##
+## Data Loss Prevention (DLP)
+##
+
+# Enable the DLP module
+# Default: No
+#StructuredDataDetection yes
+
+# This option sets the lowest number of Credit Card numbers found in a file
+# to generate a detect.
+# Default: 3
+#StructuredMinCreditCardCount 5
+
+# This option sets the lowest number of Social Security Numbers found
+# in a file to generate a detect.
+# Default: 3
+#StructuredMinSSNCount 5
+
+# With this option enabled the DLP module will search for valid
+# SSNs formatted as xxx-yy-zzzz
+# Default: yes
+#StructuredSSNFormatNormal yes
+
+# With this option enabled the DLP module will search for valid
+# SSNs formatted as xxxyyzzzz
+# Default: no
+#StructuredSSNFormatStripped yes
+
+
+##
+## HTML
+##
+
+# Perform HTML normalisation and decryption of MS Script Encoder code.
+# Default: yes
+# If you turn off this option, the original files will still be scanned, but
+# without additional processing.
+#ScanHTML yes
+
+
+##
+## Archives
+##
+
+# ClamAV can scan within archives and compressed files.
+# If you turn off this option, the original files will still be scanned, but
+# without unpacking and additional processing.
+# Default: yes
+#ScanArchive yes
+
+# Mark encrypted archives as viruses (Encrypted.Zip, Encrypted.RAR).
+# Default: no
+#ArchiveBlockEncrypted no
+
+
+##
+## Limits
+##
+
+# The options below protect your system against Denial of Service attacks
+# using archive bombs.
+
+# This option sets the maximum amount of data to be scanned for each input
+# file.
+# Archives and other containers are recursively extracted and scanned up to
+# this value.
+# Value of 0 disables the limit
+# Note: disabling this limit or setting it too high may result in severe damage
+# to the system.
+# Default: 100M
+#MaxScanSize 150M
+
+# Files larger than this limit won't be scanned. Affects the input file itself
+# as well as files contained inside it (when the input file is an archive, a
+# document or some other kind of container).
+# Value of 0 disables the limit.
+# Note: disabling this limit or setting it too high may result in severe damage
+# to the system.
+# Default: 25M
+#MaxFileSize 30M
+
+# Nested archives are scanned recursively, e.g. if a Zip archive contains a RAR
+# file, all files within it will also be scanned. This options specifies how
+# deeply the process should be continued.
+# Note: setting this limit too high may result in severe damage to the system.
+# Default: 16
+#MaxRecursion 10
+
+# Number of files to be scanned within an archive, a document, or any other
+# container file.
+# Value of 0 disables the limit.
+# Note: disabling this limit or setting it too high may result in severe damage
+# to the system.
+# Default: 10000
+#MaxFiles 15000
+
+# Maximum size of a file to check for embedded PE. Files larger than this value
+# will skip the additional analysis step.
+# Note: disabling this limit or setting it too high may result in severe damage
+# to the system.
+# Default: 10M
+#MaxEmbeddedPE 10M
+
+# Maximum size of a HTML file to normalize. HTML files larger than this value
+# will not be normalized or scanned.
+# Note: disabling this limit or setting it too high may result in severe damage
+# to the system.
+# Default: 10M
+#MaxHTMLNormalize 10M
+
+# Maximum size of a normalized HTML file to scan. HTML files larger than this
+# value after normalization will not be scanned.
+# Note: disabling this limit or setting it too high may result in severe damage
+# to the system.
+# Default: 2M
+#MaxHTMLNoTags 2M
+
+# Maximum size of a script file to normalize. Script content larger than this
+# value will not be normalized or scanned.
+# Note: disabling this limit or setting it too high may result in severe damage
+# to the system.
+# Default: 5M
+#MaxScriptNormalize 5M
+
+# Maximum size of a ZIP file to reanalyze type recognition. ZIP files larger
+# than this value will skip the step to potentially reanalyze as PE.
+# Note: disabling this limit or setting it too high may result in severe damage
+# to the system.
+# Default: 1M
+#MaxZipTypeRcg 1M
+
+# This option sets the maximum number of partitions of a raw disk image to be
+# scanned.
+# Raw disk images with more partitions than this value will have up to
+# the value number partitions scanned. Negative values are not allowed.
+# Note: setting this limit too high may result in severe damage or impact
+# performance.
+# Default: 50
+#MaxPartitions 128
+
+# This option sets the maximum number of icons within a PE to be scanned.
+# PE files with more icons than this value will have up to the value number
+# icons scanned.
+# Negative values are not allowed.
+# WARNING: setting this limit too high may result in severe damage or impact
+# performance.
+# Default: 100
+#MaxIconsPE 200
+
+# This option sets the maximum recursive calls for HWP3 parsing during
+# scanning. HWP3 files using more than this limit will be terminated and
+# alert the user.
+# Scans will be unable to scan any HWP3 attachments if the recursive limit
+# is reached.
+# Negative values are not allowed.
+# WARNING: setting this limit too high may result in severe damage or impact
+# performance.
+# Default: 16
+#MaxRecHWP3 16
+
+# This option sets the maximum calls to the PCRE match function during
+# an instance of regex matching.
+# Instances using more than this limit will be terminated and alert the user
+# but the scan will continue.
+# For more information on match_limit, see the PCRE documentation.
+# Negative values are not allowed.
+# WARNING: setting this limit too high may severely impact performance.
+# Default: 100000
+#PCREMatchLimit 20000
+
+# This option sets the maximum recursive calls to the PCRE match function
+# during an instance of regex matching.
+# Instances using more than this limit will be terminated and alert the user
+# but the scan will continue.
+# For more information on match_limit_recursion, see the PCRE documentation.
+# Negative values are not allowed and values > PCREMatchLimit are superfluous.
+# WARNING: setting this limit too high may severely impact performance.
+# Default: 5000
+#PCRERecMatchLimit 10000
+
+# This option sets the maximum filesize for which PCRE subsigs will be
+# executed. Files exceeding this limit will not have PCRE subsigs executed
+# unless a subsig is encompassed to a smaller buffer.
+# Negative values are not allowed.
+# Setting this value to zero disables the limit.
+# WARNING: setting this limit too high or disabling it may severely impact
+# performance.
+# Default: 25M
+#PCREMaxFileSize 100M
+
+# When BlockMax is set, files exceeding the MaxFileSize, MaxScanSize, or
+# MaxRecursion limit will be flagged with the virus
+# "Heuristic.Limits.Exceeded".
+# Default: no
+#BlockMax yes
+
+##
+## On-access Scan Settings
+##
+
+# Enable on-access scanning. Currently, this is supported via fanotify.
+# Clamuko/Dazuko support has been deprecated.
+# Default: no
+#ScanOnAccess yes
+
+# Set the  mount point to be scanned. The mount point specified, or the mount
+# point containing the specified directory will be watched. If any directories
+# are specified, this option will preempt the DDD system. This will notify
+# only. It can be used multiple times.
+# (On-access scan only)
+# Default: disabled
+#OnAccessMountPath /
+#OnAccessMountPath /home/user
+
+# Don't scan files larger than OnAccessMaxFileSize
+# Value of 0 disables the limit.
+# Default: 5M
+#OnAccessMaxFileSize 10M
+
+# Set the include paths (all files inside them will be scanned). You can have
+# multiple OnAccessIncludePath directives but each directory must be added
+# in a separate line. (On-access scan only)
+# Default: disabled
+#OnAccessIncludePath /home
+#OnAccessIncludePath /students
+
+# Set the exclude paths. All subdirectories are also excluded.
+# (On-access scan only)
+# Default: disabled
+#OnAccessExcludePath /home/bofh
+
+# With this option you can whitelist the root UID (0). Processes run under
+# root with be able to access all files without triggering scans or
+# permission denied events.
+# Note that if clamd cannot check the uid of the process that generated an
+# on-access scan event (e.g., because OnAccessPrevention was not enabled, and
+# the process already exited), clamd will perform a scan.  Thus, setting
+# OnAccessExcludeRootUID is not *guaranteed* to prevent every access by the
+# root user from triggering a scan (unless OnAccessPrevention is enabled).
+# Default: no
+#OnAccessExcludeRootUID no
+
+# With this option you can whitelist specific UIDs. Processes with these UIDs
+# will be able to access all files without triggering scans or permission
+# denied events.
+# This option can be used multiple times (one per line).
+# Using a value of 0 on any line will disable this option entirely.
+# To whitelist the root UID (0) please enable the OnAccessExcludeRootUID
+# option.
+# Also note that if clamd cannot check the uid of the process that generated an
+# on-access scan event (e.g., because OnAccessPrevention was not enabled, and
+# the process already exited), clamd will perform a scan.  Thus, setting
+# OnAccessExcludeUID is not *guaranteed* to prevent every access by the
+# specified uid from triggering a scan (unless OnAccessPrevention is enabled).
+# Default: disabled
+#OnAccessExcludeUID -1
+
+# Toggles dynamic directory determination. Allows for recursively watching
+# include paths.
+# (On-access scan only)
+# Default: no
+#OnAccessDisableDDD yes
+
+# Modifies fanotify blocking behaviour when handling permission events.
+# If off, fanotify will only notify if the file scanned is a virus,
+# and not perform any blocking.
+# (On-access scan only)
+# Default: no
+#OnAccessPrevention yes
+
+# Toggles extra scanning and notifications when a file or directory is
+# created or moved.
+# Requires the  DDD system to kick-off extra scans.
+# NOTE:  This feature is disabled until a thread resource leak bug
+#        in the OnAccessExtraScanning code can be resolved.
+# (On-access scan only)
+# Default: no
+#OnAccessExtraScanning yes
+
+##
+## Bytecode
+##
+
+# With this option enabled ClamAV will load bytecode from the database. 
+# It is highly recommended you keep this option on, otherwise you'll miss
+# detections for many new viruses.
+# Default: yes
+#Bytecode yes
+
+# Set bytecode security level.
+# Possible values:
+#   None -      No security at all, meant for debugging.
+#               DO NOT USE THIS ON PRODUCTION SYSTEMS.
+#               This value is only available if clamav was built
+#               with --enable-debug!
+#   TrustSigned - Trust bytecode loaded from signed .c[lv]d files, insert
+#               runtime safety checks for bytecode loaded from other sources.
+#   Paranoid -  Don't trust any bytecode, insert runtime checks for all.
+# Recommended: TrustSigned, because bytecode in .cvd files already has these
+# checks.
+# Note that by default only signed bytecode is loaded, currently you can only
+# load unsigned bytecode in --enable-debug mode.
+#
+# Default: TrustSigned
+#BytecodeSecurity TrustSigned
+
+# Set bytecode timeout in milliseconds.
+# 
+# Default: 5000
+# BytecodeTimeout 1000
+
+##
+## Statistics gathering and submitting
+##

+ 9 - 0
clamav/run.sh

@@ -0,0 +1,9 @@
+#!/bin/sh
+
+cat /dev/null > /var/log/clamav/{clamd,freshclam}.log
+
+freshclam
+freshclam -d
+clamd
+
+tail -f /var/log/clamav/*.log

+ 16 - 0
db/Dockerfile

@@ -0,0 +1,16 @@
+FROM mariadb:10.4
+
+RUN apt-get -y update && \
+    apt-get -y install ruby ruby-mysql2 && \
+    gem update --system && \
+    gem install bundler
+
+ADD db/bin /scripts
+RUN cd /scripts && bundle install
+
+ADD db/schema.sql /docker-entrypoint-initdb.d/schema.sql
+
+ENV MYSQL_RANDOM_ROOT_PASSWORD=1
+ENV MYSQL_USER=dovecot
+ENV MYSQL_DATABASE=dovecot
+

+ 7 - 0
db/bin/Gemfile

@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+source "https://rubygems.org"
+
+gem "highline"
+gem "rfc822"
+

+ 15 - 0
db/bin/Gemfile.lock

@@ -0,0 +1,15 @@
+GEM
+  remote: https://rubygems.org/
+  specs:
+    highline (2.0.2)
+    rfc822 (0.1.5)
+
+PLATFORMS
+  ruby
+
+DEPENDENCIES
+  highline
+  rfc822
+
+BUNDLED WITH
+   1.16.2

+ 66 - 0
db/bin/add_user

@@ -0,0 +1,66 @@
+#!/usr/bin/env ruby
+
+require 'highline/import'
+require 'mysql2'
+require 'optparse'
+require 'rfc822'
+
+options = {}
+optparse = OptionParser.new do |opts|
+  opts.banner = "Usage: add_user user@domain [options]"
+
+  opts.on("-pPASSWORD",
+          "--password=PASSWORD",
+          "Specify password from command-line") do |p|
+    options[:password] = p
+  end
+
+  opts.on("-s",
+          "--stdin",
+          "Read password from stdin (for scripts)") do |s|
+    options[:stdin] = s
+  end
+end
+optparse.parse!
+
+# Check required conditions
+if ARGV.count != 1
+  puts optparse
+  exit(-1)
+end
+
+user = ARGV[0]
+unless user.is_email?
+  puts "#{user} is not an email address"
+  exit -1
+end
+
+password = if options[:password]
+  options[:password]
+elsif options[:stdin]
+  STDIN.gets.chomp
+else
+  p1 = ask("Password: ") { |q| q.echo = "*" }
+  p2 = ask("Confirm password:  ") { |q| q.echo = "*" }
+  if p1 != p2
+    puts "Passwords do not match."
+    exit -1
+  elsif p1.empty?
+    puts "No password supplied"
+    exit -1
+  else
+    p1
+  end
+end
+
+client = Mysql2::Client.new(host: 'db',
+                            username: 'dovecot',
+                            password: ENV['MYSQL_PASSWORD'],
+                            database: 'dovecot')
+
+user, domain = user.split('@')
+user = client.escape(user)
+domain = client.escape(domain)
+password = client.escape(password)
+client.query("CALL add_user('#{user}','#{domain}','#{password}');")
+

+ 28 - 0
db/bin/del_user

@@ -0,0 +1,28 @@
+#!/usr/bin/env ruby
+
+require 'highline/import'
+require 'mysql2'
+require 'rfc822'
+
+# Check required conditions
+if ARGV.count != 1
+  puts "Usage: del_user user@domain"
+  exit(-1)
+end
+
+user = ARGV[0]
+unless user.is_email?
+  puts "#{user} is not an email address"
+  exit -1
+end
+
+client = Mysql2::Client.new(host: 'db',
+                            username: 'dovecot',
+                            password: ENV['MYSQL_PASSWORD'],
+                            database: 'dovecot')
+
+user, domain = user.split('@')
+user = client.escape(user)
+domain = client.escape(domain)
+client.query("DELETE FROM users WHERE username='#{user}' AND domain='#{domain}';")
+

+ 66 - 0
db/bin/set_user_password

@@ -0,0 +1,66 @@
+#!/usr/bin/env ruby
+
+require 'highline/import'
+require 'mysql2'
+require 'optparse'
+require 'rfc822'
+
+options = {}
+optparse = OptionParser.new do |opts|
+  opts.banner = "Usage: set_user_password user@domain [options]"
+
+  opts.on("-pPASSWORD",
+          "--password=PASSWORD",
+          "Specify password from command-line") do |p|
+    options[:password] = p
+  end
+
+  opts.on("-s",
+          "--stdin",
+          "Read password from stdin (for scripts)") do |s|
+    options[:stdin] = s
+  end
+end
+optparse.parse!
+
+# Check required conditions
+if ARGV.count != 1
+  puts optparse
+  exit(-1)
+end
+
+user = ARGV[0]
+unless user.is_email?
+  puts "#{user} is not an email address"
+  exit -1
+end
+
+password = if options[:password]
+  options[:password]
+elsif options[:stdin]
+  STDIN.gets.chomp
+else
+  p1 = ask("Password: ") { |q| q.echo = "*" }
+  p2 = ask("Confirm password:  ") { |q| q.echo = "*" }
+  if p1 != p2
+    puts "Passwords do not match."
+    exit -1
+  elsif p1.empty?
+    puts "No password supplied"
+    exit -1
+  else
+    p1
+  end
+end
+
+client = Mysql2::Client.new(host: 'db',
+                            username: 'dovecot',
+                            password: ENV['MYSQL_PASSWORD'],
+                            database: 'dovecot')
+
+user, domain = user.split('@')
+user = client.escape(user)
+domain = client.escape(domain)
+password = client.escape(password)
+client.query("CALL set_user_password('#{user}','#{domain}','#{password}');")
+

+ 107 - 0
db/schema.sql

@@ -0,0 +1,107 @@
+-- MySQL dump 10.16  Distrib 10.1.37-MariaDB, for Linux (x86_64)
+--
+-- Host: localhost    Database: dovecot
+-- ------------------------------------------------------
+-- Server version	10.1.37-MariaDB
+
+/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
+/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
+/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
+/*!40101 SET NAMES utf8 */;
+/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
+/*!40103 SET TIME_ZONE='+00:00' */;
+/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
+
+--
+-- Table structure for table `aliases`
+--
+
+USE dovecot;
+
+DROP TABLE IF EXISTS `aliases`;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `aliases` (
+  `action` varchar(16) NOT NULL,
+  `config` varchar(255) NOT NULL,
+  `email` varchar(129) NOT NULL,
+  `relay` tinyint(1) DEFAULT '0',
+  PRIMARY KEY (`email`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Table structure for table `users`
+--
+
+DROP TABLE IF EXISTS `users`;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `users` (
+  `username` varchar(64) NOT NULL,
+  `domain` varchar(64) NOT NULL,
+  `salt` varchar(8) NOT NULL,
+  `hash` char(64) NOT NULL,
+  PRIMARY KEY (`username`,`domain`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping routines for database 'dovecot'
+--
+/*!50003 DROP PROCEDURE IF EXISTS `add_user` */;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8 */ ;
+/*!50003 SET character_set_results = utf8 */ ;
+/*!50003 SET collation_connection  = utf8_general_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+CREATE DEFINER=`root`@`localhost` PROCEDURE `add_user`(IN uname VARCHAR(64), IN dname VARCHAR(64), IN password TEXT)
+BEGIN
+  DECLARE newsalt VARCHAR(8);
+  SET newsalt = LEFT(UUID(), 8);
+  INSERT INTO users (username, domain, salt, hash) VALUES (uname, dname, newsalt, SHA2(CONCAT(newsalt,password), 256));
+END ;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!50003 DROP PROCEDURE IF EXISTS `set_user_password` */;
+/*!50003 SET @saved_cs_client      = @@character_set_client */ ;
+/*!50003 SET @saved_cs_results     = @@character_set_results */ ;
+/*!50003 SET @saved_col_connection = @@collation_connection */ ;
+/*!50003 SET character_set_client  = utf8 */ ;
+/*!50003 SET character_set_results = utf8 */ ;
+/*!50003 SET collation_connection  = utf8_general_ci */ ;
+/*!50003 SET @saved_sql_mode       = @@sql_mode */ ;
+/*!50003 SET sql_mode              = 'NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' */ ;
+DELIMITER ;;
+CREATE DEFINER=`root`@`localhost` PROCEDURE `set_user_password`(IN uname VARCHAR(64), IN dname VARCHAR(64), IN password TEXT)
+BEGIN
+  DECLARE newsalt VARCHAR(8);
+  SET newsalt = LEFT(UUID(), 8);
+  UPDATE users SET salt=newsalt, hash=SHA2(CONCAT(newsalt,password), 256) WHERE username=uname AND domain=dname;
+END ;;
+DELIMITER ;
+/*!50003 SET sql_mode              = @saved_sql_mode */ ;
+/*!50003 SET character_set_client  = @saved_cs_client */ ;
+/*!50003 SET character_set_results = @saved_cs_results */ ;
+/*!50003 SET collation_connection  = @saved_col_connection */ ;
+/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
+
+/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
+/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
+/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
+/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
+/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
+/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
+/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
+
+-- Dump completed on 2019-04-02  8:26:40

+ 149 - 0
docker-compose.yml

@@ -0,0 +1,149 @@
+version: "3.6"
+
+# Set env vars MYSQL_PASSWORD, MAIL_HOSTNAME and SSL_DOMAIN.
+# SSL certificates should be on the host machine in
+# /etc/letsencrypt/live/$SSL_DOMAIN/
+# as created by certbot
+
+# MYSQL_PASSWORD must be set the first time the db container is brought up, as
+# this is when the database is initialised. It must also be set whenever
+# bringing up the stack, as the containers use it at runtime.
+#
+# Once the db is created in the `db` volume you will need to use the mysql
+# command-line tools if you want to edit it.
+
+# MAIL_HOSTNAME should be the external host-name of the mail host itself
+# and must match reverse-dns
+
+volumes:
+  mail:
+  queue:
+  redis:
+  rspamd:
+  rainloop:
+  db:
+
+services:
+  db:
+    build:
+      context: .
+      dockerfile: db/Dockerfile
+    restart: always
+    volumes:
+      - type: volume
+        source: db
+        target: /var/lib/mysql
+    environment:
+      - MYSQL_PASSWORD=${MYSQL_PASSWORD}
+
+  haraka:
+    build:
+      context: .
+      dockerfile: haraka/Dockerfile
+    ports: 
+      - "25:2525"
+      - "587:2525"
+    restart: always
+    volumes:
+      - type: volume
+        source: queue
+        target: /haraka/queue
+      - type: bind
+        source: /etc/letsencrypt/live/${SSL_DOMAIN}/fullchain.pem
+        target: /haraka/certs/fullchain.pem
+      - type: bind
+        source: /etc/letsencrypt/live/${SSL_DOMAIN}/privkey.pem
+        target: /haraka/certs/privkey.pem
+    environment:
+      - MYSQL_PASSWORD=${MYSQL_PASSWORD}
+      - MAIL_HOSTNAME=${MAIL_HOSTNAME}
+    depends_on:
+      - db
+
+  dovecot:
+    build:
+      context: .
+      dockerfile: dovecot/Dockerfile
+    ports:
+      - "143:10143"
+      - "993:10993"
+    expose:
+      - "2524"
+    restart: always
+    volumes:
+      - type: volume
+        source: mail
+        target: /mail
+      - type: bind
+        source: /etc/letsencrypt/live/${SSL_DOMAIN}/fullchain.pem
+        target: /conf/ssl/fullchain.pem
+      - type: bind
+        source: /etc/letsencrypt/live/${SSL_DOMAIN}/privkey.pem
+        target: /conf/ssl/privkey.pem
+    environment:
+      - MYSQL_PASSWORD=${MYSQL_PASSWORD}
+    depends_on:
+      - db
+
+  redis:
+    build:
+      context: .
+      dockerfile: redis/Dockerfile
+    expose: 
+      - "6379"
+    restart: always
+    volumes:
+      - type: volume
+        source: redis
+        target: /data
+
+  rspamd:
+    build:
+      context: .
+      dockerfile: rspamd/Dockerfile
+    expose:
+      - "11333"
+    restart: always
+    volumes:
+      - type: volume
+        source: rspamd
+        target: /var/lib/rspamd
+    depends_on:
+      - redis
+
+  clamav:
+    build:
+      context: .
+      dockerfile: clamav/Dockerfile
+    expose:
+      - "3310"
+    restart: always
+
+  rainloop:
+    build: 
+      context: .
+      dockerfile: rainloop/Dockerfile
+    volumes:
+      - rainloop:/rainloop/data
+    expose:
+      - "8888"
+    restart: always
+    environment:
+      - LOG_TO_STDOUT=true
+
+  proxy:
+    build:
+      context: .
+      dockerfile: proxy/Dockerfile
+    ports:
+      - "80:8080"
+      - "443:8443"
+    restart: always
+    volumes:
+      - type: bind
+        source: /etc/letsencrypt/live/${SSL_DOMAIN}/fullchain.pem
+        target: /etc/nginx/certs/fullchain.pem
+      - type: bind
+        source: /etc/letsencrypt/live/${SSL_DOMAIN}/privkey.pem
+        target: /etc/nginx/certs/privkey.pem
+

+ 19 - 0
dovecot/Dockerfile

@@ -0,0 +1,19 @@
+FROM alpine:3.8
+
+RUN apk add --update --no-cache dovecot dovecot-mysql libressl \
+            dovecot-pigeonhole-plugin bash && \
+    mkdir -p /run/dovecot /var/lib/dovecot /mail /conf/ssl && \
+    openssl dhparam -dsaparam 4096 -out /conf/ssl/dh.pem && \
+    chown dovecot:dovecot /run/dovecot /var/lib/dovecot /mail /conf -R
+
+ADD dovecot/run.sh /run.sh
+ADD --chown=dovecot dovecot/conf /conf
+
+VOLUME /mail
+
+USER dovecot
+
+EXPOSE 10143 4190
+
+CMD /run.sh
+

+ 100 - 0
dovecot/conf/dovecot.conf

@@ -0,0 +1,100 @@
+auth_debug=no
+
+namespace inbox {
+  inbox = yes
+  location = 
+  mailbox Drafts {
+    special_use = \Drafts
+  }
+  mailbox Junk {
+    special_use = \Junk
+  }
+  mailbox Sent {
+    special_use = \Sent
+  }
+  mailbox "Sent Messages" {
+    special_use = \Sent
+  }
+  mailbox Trash {
+    special_use = \Trash
+  }
+  prefix = 
+}
+
+first_valid_uid = 50
+mail_uid = dovecot
+mail_gid = dovecot
+mail_location = maildir:/mail/%d/%u
+default_internal_user = dovecot
+default_login_user = dovecot
+
+submission_host = haraka:2525
+
+passdb {
+  args = /conf/sql-authdb.conf
+  driver = sql
+}
+userdb {
+  args = /conf/sql-authdb.conf
+  driver = sql
+}
+
+log_path = /dev/stderr
+info_log_path = /dev/stdout
+log_timestamp = "%Y/%m/%d %H:%M:%S "
+
+service anvil {
+  chroot =
+}
+service stats {
+  chroot =
+}
+service imap {
+  vsz_limit = 1G
+  chroot =
+}
+service imap-login {
+  vsz_limit = 1G
+  chroot =
+}
+service lmtp {
+  chroot =
+}
+service managesieve-login {
+  chroot =
+}
+service managesieve {
+  chroot =
+}
+
+auth_verbose=yes
+ssl = required
+ssl_cert = </conf/ssl/fullchain.pem
+ssl_key = </conf/ssl/privkey.pem
+ssl_dh = </conf/ssl/dh.pem
+ssl_cipher_list = EECDH+AESGCM:EDH+aRSA+AESGCM:EECDH+AES256:EDH+aRSA+AES256:EECDH+AES128:EDH+aRSA+AES128:RSA+AES:RSA+3DES
+
+protocols = imap lmtp sieve
+service imap-login {
+  inet_listener imap {
+    port = 10143
+  }
+  inet_listener imaps {
+    port = 10993
+  }
+}
+service lmtp {
+  inet_listener lmtp {
+    port = 2524
+  }
+  user = dovecot
+}
+
+protocol lmtp {
+  mail_plugins = $mail_plugins sieve
+}
+
+plugin sieve {
+  sieve = file:/mail/sieve/%u.sieve;active=/mail/sieve/%u.active.sieve
+}
+

+ 6 - 0
dovecot/conf/sql-authdb.conf.template

@@ -0,0 +1,6 @@
+driver = mysql
+connect = host=db dbname=dovecot user=dovecot password=__MYSQL_PASSWORD__
+password_query = SELECT username, domain, NULL as password, 'y' as nopassword FROM users WHERE username = '%n' AND domain = '%d' AND hash = SHA2(CONCAT(salt, '%w'), 256)
+user_query = SELECT username, domain, 'dovecot' as uid, 'dovecot' as gid, 'Maildir:/mail/%d/%n' as mail FROM users WHERE username = '%n' AND domain = '%d'
+iterate_query = SELECT username, domain FROM users
+

+ 7 - 0
dovecot/run.sh

@@ -0,0 +1,7 @@
+#!/bin/bash
+
+escaped=$(sed 's/[&/\]/\\&/g' <<<"${MYSQL_PASSWORD}")
+
+sed "s/__MYSQL_PASSWORD__/${escaped}/" < /conf/sql-authdb.conf.template > /conf/sql-authdb.conf
+
+dovecot -c /conf/dovecot.conf -F

+ 28 - 0
haraka/Dockerfile

@@ -0,0 +1,28 @@
+FROM alpine:3.8
+
+RUN apk add --update --no-cache nodejs nodejs-npm python git alpine-sdk \
+    mariadb-connector-c mariadb-connector-c-dev libressl && \
+    adduser -D -H haraka && \
+    mkdir -p /haraka/queue && \
+    chown haraka:haraka /haraka -R && \
+    npm install --unsafe-perm -g Haraka@2.8.23 haraka-net-utils mysql address-rfc2822 && \
+    npm cache clean --force && \
+    apk del git alpine-sdk mariadb-connector-c-dev
+
+VOLUME /haraka/queue
+
+# generate the haraka folder with `haraka -i`
+RUN haraka -i /haraka
+# overwrite any configs with local ones
+ADD --chown=haraka haraka/haraka /haraka
+ADD --chown=haraka haraka/dkim /haraka/config/dkim
+
+# add the run-script
+ADD haraka/run.sh /run.sh
+
+EXPOSE 2525
+
+USER haraka
+
+CMD /run.sh
+

+ 1 - 0
haraka/dkim/.gitignore

@@ -0,0 +1 @@
+*/

+ 78 - 0
haraka/dkim/dkim_key_gen.sh

@@ -0,0 +1,78 @@
+#!/bin/sh
+
+DOMAIN="$1"
+SMTPD="$2"
+
+usage()
+{
+    echo "   usage: ${0} <example.com> [haraka username]" 2>&1
+    echo 2>&1
+    exit 1
+}
+
+if [ -z "$DOMAIN" ]; then
+    usage
+fi
+
+if [ -z "$SMTPD" ]; then
+    SMTPD="www"
+fi
+
+# Create a directory for each DKIM signing domain
+mkdir -p "$DOMAIN"
+cd "$DOMAIN" || exit
+
+# The selector can be any value that is a valid DNS label
+# Create in the common format: mmmYYYY (apr2014)
+date '+%h%Y' | tr '[:upper:]' '[:lower:]' > selector
+
+# Generate private and public keys
+#           - Key length considerations -
+# The minimum recommended key length for short duration keys (ones that
+# will be replaced within a few months) is 1024. If you are unlikely to
+# rotate your keys frequently, choose 2048, at the expense of more CPU.
+openssl genrsa -out private 2048
+chmod 0400 private
+openssl rsa -in private -out public -pubout
+
+DNS_NAME="$(tr -d '\n' < selector)._domainkey"
+DNS_ADDRESS="v=DKIM1;p=$(grep -v '^-' public | tr -d '\n')"
+
+# Fold width is arbitrary, any value between 80 and 255 is reasonable
+BIND_SPLIT_ADDRESS="$(echo "$DNS_ADDRESS" | fold -w 110 | sed -e 's/^/	"/g; s/$/"/g')"
+
+# Make it really easy to publish the public key in DNS
+# by creating a file named 'dns', with instructions
+cat > dns <<EO_DKIM_DNS
+
+Add this TXT record to the ${DOMAIN} DNS zone.
+
+${DNS_NAME}    IN   TXT   ${DNS_ADDRESS}
+
+
+BIND zone file formatted:
+
+${DNS_NAME}    IN   TXT (
+${BIND_SPLIT_ADDRESS}
+        )
+
+Tell the world that the ONLY mail servers that send mail from this domain are DKIM signed and/or bear our MX and A records.
+
+With SPF:
+
+        SPF "v=spf1 mx a -all"
+        TXT "v=spf1 mx a -all"
+
+With DMARC:
+
+_dmarc  TXT "v=DMARC1; p=reject; adkim=s; aspf=r; rua=mailto:dmarc-feedback@${DOMAIN}; ruf=mailto:dmarc-feedback@${DOMAIN}; pct=100"
+
+For more information about DKIM and SPF policy,
+the documentation within each plugin contains a longer discussion and links to more detailed information:
+
+   haraka -h dkim_sign
+   haraka -h spf
+
+EO_DKIM_DNS
+
+cd ..

+ 10 - 0
haraka/haraka/config/aliases_mysql.ini

@@ -0,0 +1,10 @@
+# required selects are:
+#  action: alias action
+#  config: action configuration
+#      alias -> forwarder targets devided by |
+#
+# replacements
+#   %u = entire user@domain
+#   %n = user part of user@domain
+#   %d = domain part of user@domain
+query=SELECT action, config, relay FROM aliases WHERE email = '%u' OR email = CONCAT('*@', '%d') AND '%u' NOT IN (SELECT CONCAT(username, '@', domain) FROM users) LIMIT 1

+ 9 - 0
haraka/haraka/config/auth_mysql_query.ini

@@ -0,0 +1,9 @@
+# required selects are:
+#  password: cram md5 password
+#
+# replacements
+#   %u = entire user@domain
+#   %n = user part of user@domain
+#   %d = domain part of user@domain
+#   %w = plaintext password
+query=SELECT 'y' AS valid FROM users WHERE username=%n AND domain=%d AND hash = SHA2(CONCAT(salt, %w), 256)

+ 1 - 0
haraka/haraka/config/databytes

@@ -0,0 +1 @@
+104857600

+ 1 - 0
haraka/haraka/config/dkim_sign.ini

@@ -0,0 +1 @@
+headers_to_sign = From

+ 2 - 0
haraka/haraka/config/lmtp.ini

@@ -0,0 +1,2 @@
+host=dovecot
+port=2524

+ 11 - 0
haraka/haraka/config/log.ini

@@ -0,0 +1,11 @@
+[main]
+
+; level=data, protocol, debug, info, notice, warn, error, crit, alert, emerg
+level=notice
+
+; prepend timestamps to log entries? This setting does NOT affect logs emitted
+; by logging plugins (like syslog).
+timestamps=true
+
+;  format=default, logfmt
+format=default

+ 1 - 0
haraka/haraka/config/me

@@ -0,0 +1 @@
+_set_by_MAIL_HOSTNAME_env_var_

+ 10 - 0
haraka/haraka/config/mysql_provider.ini

@@ -0,0 +1,10 @@
+# host name
+host=db
+# port number
+port=3306
+# mysql user name
+user=dovecot
+# required - mysql database name
+database=dovecot
+# mysql used char_set
+char_set=utf8

+ 81 - 0
haraka/haraka/config/plugins

@@ -0,0 +1,81 @@
+# This file lists plugins that Haraka will run
+#
+# Plugin ordering often matters, run 'haraka -o -c /path/to/haraka/config'
+# to see the order plugins (and their hooks) will run in.
+#
+# To see a list of all plugins, run 'haraka -l'
+#
+# To see the help docs for a particular plugin, run 'haraka -h plugin.name'
+
+process_title
+mysql_provider
+# Log to syslog (see 'haraka -h syslog')
+# syslog
+
+# CONNECT
+#toobusy
+relay
+# control which IPs, rDNS hostnames, HELO hostnames, MAIL FROM addresses, and
+# RCPT TO address you accept mail from. See 'haraka -h access'.
+# access
+# p0f
+# geoip
+# asn
+# fcrdns
+# block mails from known bad hosts (see config/dnsbl.zones for the DNS zones queried)
+dnsbl
+
+# HELO
+#early_talker
+# see config/helo.checks.ini for configuration
+helo.checks
+# see 'haraka -h tls' for config instructions before enabling!
+tls
+#
+# AUTH plugins require TLS before AUTH is advertised, see
+#     https://github.com/haraka/Haraka/wiki/Require-SSL-TLS
+# auth/flat_file
+# auth/auth_proxy
+# auth/auth_ldap
+
+aliases_mysql
+
+auth_mysql_query
+
+# MAIL FROM
+# Only accept mail where the MAIL FROM domain is resolvable to an MX record
+mail_from.is_resolvable
+spf
+
+# RCPT TO
+# At least one rcpt_to plugin is REQUIRED for inbound email. The simplest
+# plugin is in_host_list, see 'haraka -h rcpt_to.in_host_list' to configure.
+rcpt_to.mysql
+#qmail-deliverable
+#rcpt_to.ldap
+#rcpt_to.routes
+
+# DATA
+#bounce
+# Check mail headers are valid
+data.headers
+#data.uribl
+#attachment
+#clamd
+#spamassassin
+dkim_sign
+#karma
+#limit
+rspamd
+
+# QUEUE
+# queues: discard  qmail-queue  quarantine  smtp_forward  smtp_proxy
+# Queue mail via smtp - see config/smtp_forward.ini for where your mail goes
+#queue/smtp_forward
+
+queue/lmtp
+
+# Disconnect client if they spew bad SMTP commands at us
+max_unrecognized_commands
+
+#watch

+ 7 - 0
haraka/haraka/config/rcpt_to.mysql.ini

@@ -0,0 +1,7 @@
+# query for the actual email check. Should be a select with any return value.
+#
+# replacements
+#   %u = entire user@domain
+#   %n = user part of user@domain
+#   %d = domain part of user@domain
+query=SELECT 1 FROM users WHERE username=%n AND domain=%d UNION SELECT 1 FROM aliases WHERE email=%u OR email=CONCAT('*@', %d) LIMIT 1;

+ 2 - 0
haraka/haraka/config/relay.ini

@@ -0,0 +1,2 @@
+[relay]
+acl=true

+ 1 - 0
haraka/haraka/config/relay_acl_allow

@@ -0,0 +1 @@
+10.0.0.0/8

+ 6 - 0
haraka/haraka/config/rspamd.ini

@@ -0,0 +1,6 @@
+host = rspamd
+port = 11333
+add_headers = always
+reject.spam = false
+dkim.enabled = false
+

+ 42 - 0
haraka/haraka/config/smtp.ini

@@ -0,0 +1,42 @@
+; address to listen on (default: all IPv6 and IPv4 addresses, port 25)
+; use "[::0]:25" to listen on IPv6 and IPv4 (not all OSes)
+listen=[::0]:2525
+
+; public IP address (default: none)
+; If your machine is behind a NAT, some plugins (SPF, GeoIP) gain features
+; if they know the servers public IP. If 'stun' is installed, Haraka will
+; try to figure it out. If that doesn't work, set it here.
+;public_ip=N.N.N.N
+
+; Time in seconds to let sockets be idle with no activity
+;inactivity_timeout=300
+
+; Drop privileges to this user/group
+;user=smtp
+;group=smtp
+
+; Don't stop Haraka if plugins fail to compile
+;ignore_bad_plugins=0
+
+; Run using cluster to fork multiple backend processes
+;nodes=cpus
+
+; Daemonize
+daemonize=false
+;daemon_log_file=/var/log/haraka.log
+;daemon_pid_file=/var/run/haraka.pid
+
+; Spooling
+; Save memory by spooling large messages to disk
+;spool_dir=/var/spool/haraka
+; Specify -1 to never spool to disk
+; Specify 0 to always spool to disk
+; Otherwise specify a size in bytes, once reached the
+; message will be spooled to disk to save memory.
+;spool_after=
+
+; Force Shutdown Timeout
+; - Haraka tries to close down gracefully, but if everything is shut down
+;   after this time it will hard close. 30s is usually long enough to
+;   wait for outbound connections to finish.
+;force_shutdown_timeout=30

+ 5 - 0
haraka/haraka/config/tls.ini

@@ -0,0 +1,5 @@
+key=../certs/privkey.pem
+cert=../certs/fullchain.pem
+
+ciphers=EECDH+AESGCM:EDH+aRSA+AESGCM:EECDH+AES256:EDH+aRSA+AES256:EECDH+AES128:EDH+aRSA+AES128:RSA+AES:RSA+3DES
+

+ 71 - 0
haraka/haraka/plugins/aliases_mysql.js

@@ -0,0 +1,71 @@
+var net_utils = require('haraka-net-utils');
+const Address = require("address-rfc2821").Address;
+
+exports.register = function () {
+  this.inherits("queue/discard");
+  this.register_hook("rcpt", "aliases_mysql");
+};
+
+exports.aliases_mysql = function (next, connection, params) {
+  if (!params || !params[0]) return next();
+
+  var address = params[0];
+  this.getAliasByEmail(connection, address, function (error, result) {
+    if (error || !result || !result.action) {
+      if (error) connection.logdebug(exports, "Error: " + error.message);
+      return next();
+    }
+
+    switch (result.action.toLowerCase()) {
+      case "drop":
+        exports.drop(connection, address);
+        next(DENY);
+        break;
+      case "alias":
+        exports.alias(connection, address, result);
+        next(OK);
+        break;
+      default:
+        connection.logwarn(exports, "unknown action: " + result.action);
+        next();
+    }
+  });
+};
+
+exports.getAliasByEmail = function (connection, address, callback) {
+  if (!connection.server.notes.mysql_provider) return callback(new Error('mysql provider seems mot initialized'));
+
+  var cfg = this.config.get('aliases_mysql.ini', 'ini').main || {};
+  if (!cfg.query) return callback(new Error('no query configured'));
+
+  var query = cfg.query.replace(/%d/g, address.host).replace(/%n/g, address.user).replace(/%u/g, address.address());
+
+  connection.logdebug(exports, "exec query: " + query);
+
+  connection.server.notes.mysql_provider.query(query, function (err, result) {
+    if (err) return callback(err);
+    if (!result || !result[0]) return callback(null, null);
+    return callback(null, result[0]);
+  });
+};
+
+exports.drop = function (connection, rcpt) {
+  connection.logdebug(exports, "marking " + rcpt + " for drop");
+  connection.transaction.notes.discard = true;
+};
+
+exports.alias = function (connection, rcpt, alias) {
+  if (alias === null || !alias.config || alias.config.length === 0) {
+    connection.loginfo(exports, 'alias failed for ' + rcpt + ', no "to" field in alias config');
+    return false;
+  }
+
+  connection.transaction.rcpt_to.pop();
+  connection.relaying = alias.relay == 1;
+
+  var aliases = alias.config.split("|");
+  for (var index = 0; index < aliases.length; index++) {
+    connection.logdebug(exports, "aliasing " + rcpt + " to " + aliases[index]);
+    connection.transaction.rcpt_to.push(new Address('<' + aliases[index] + '>'));
+  }
+};

+ 47 - 0
haraka/haraka/plugins/auth_mysql_query.js

@@ -0,0 +1,47 @@
+var net_utils = require('haraka-net-utils');
+var addrparser = require("address-rfc2822");
+
+exports.register = function () {
+  this.inherits('auth/auth_base');
+};
+
+exports.hook_capabilities = function (next, connection) {
+  // Do not allow AUTH unless private IP or encrypted
+  if (!net_utils.is_rfc1918(connection.remote_ip) && !connection.using_tls) {
+    return next();
+  }
+
+  var methods = ["PLAIN", "LOGIN"];
+  connection.capabilities.push('AUTH ' + methods.join(' '));
+  connection.notes.allowed_auth_methods = methods;
+
+  return next();
+};
+
+exports.check_plain_passwd = function (connection, user, passwd, callback) {
+  if (!connection.server.notes.mysql_provider) return callback(new Error('mysql provider seems not initialized'));
+
+  try { // supports only full email addresses as user for full query flexibility
+    var address = addrparser.parse(user)[0];
+  } catch (e) {
+    connection.logdebug(exports, 'auth_mysql_query only accepts complete email as login user: ' + user);
+    return callback(null, null);
+  }
+
+  var cfg = this.config.get('auth_mysql_query.ini', 'ini').main || {};
+  if (!cfg.query) throw new Error('no query configured');
+
+  let query_values = [address.host(), address.user(), address.address, passwd];
+  connection.server.notes.mysql_provider.escape(query_values, function(err, values) {
+    if (err) throw err;
+    var query = cfg.query.replace(/%d/g, values[0]).replace(/%n/g, values[1]).replace(/%u/g, values[2]);
+    var query_with_password = query.replace(/%w/g, values[3]);
+
+    connection.logdebug(exports, "exec query: " + query);
+    connection.server.notes.mysql_provider.query(query_with_password, function (err, result) {
+      if (err) throw err;
+      if (result && result[0] && result[0].valid === 'y') return callback(true);
+      callback(false);
+    });
+  });
+};

+ 75 - 0
haraka/haraka/plugins/mysql_provider.js

@@ -0,0 +1,75 @@
+var mysql = require('mysql');
+var logger = require('./logger.js');
+
+var connection = null;
+
+exports.hook_init_master = function (callback, server) {
+  server.loginfo(exports, 'init mysql provider');
+  server.notes["mysql_provider"] = exports;
+  callback();
+};
+
+exports.disconnect = function () {
+  if (connection) {
+    logger.logdebug(exports, 'reseting mysql connection');
+    connection.end();
+  }
+  connection = null;
+};
+
+exports.connect = function (callback) {
+  if (connection) return callback(null, connection);
+
+  var cfg = this.config.get('mysql_provider.ini', 'ini').main || {};
+  if (!cfg.host) cfg.host = "localhost";
+  if (!cfg.port) cfg.port = "3006";
+  if (!cfg.database) cfg.database = "";
+  if (!cfg.password) cfg.password = process.env['MYSQL_PASSWORD'];
+
+  logger.logdebug(exports,
+    'MySQL host="' + cfg.host + '"' +
+    ' port="' + cfg.port + '"' +
+    ' user="' + cfg.user + '"' +
+    ' database="' + cfg.database + '"');
+
+  var conn = mysql.createConnection({
+    host: cfg.host || '',
+    port: cfg.port || '',
+    charset: cfg.charset || '',
+    user: cfg.user || '',
+    password: cfg.password || '',
+    database: cfg.database || '',
+    socketPath: cfg.socketPath || ''
+  });
+  conn.connect(function (err) {
+    if (err) return callback(err);
+
+    // reset connection on error
+    conn.on('error', function (err) {
+      logger.logalert(exports, new Date() + ' mysql connection error: ' + err.message);
+      exports.disconnect();
+    });
+
+    connection = conn;
+    return callback(null, conn);
+  });
+};
+
+// see mysql query documentation -> params are resolved there
+exports.query = function (query, callback) {
+  exports.connect(function (err, connection) {
+    if (err) return callback(err);
+    connection.query(query, callback);
+  });
+};
+
+exports.escape = function(value, callback) {
+  exports.connect(function(err, connection) {
+    if (err) return callback(err);
+    if (typeof value === 'string') {
+      callback(null, connection.escape(value));
+    } else {
+      callback(null, value.map(v => connection.escape(v)));
+    }
+  });
+};

+ 70 - 0
haraka/haraka/plugins/rcpt_to.mysql.js

@@ -0,0 +1,70 @@
+exports.register = function () {
+  var plugin = this;
+  plugin.register_hook("rcpt", "rcpt_mysql");
+  plugin.load_cfg_ini();
+};
+
+exports.load_cfg_ini = function () {
+  var plugin = this;
+  plugin.cfg = plugin.config.get(
+    'rcpt_to.mysql.ini',
+    function () {
+      plugin.load_cfg_ini();
+    }
+  );
+};
+
+exports.rcpt_mysql = function (next, connection, params) {
+  var plugin = this;
+  var txn = connection.transaction;
+  if (!txn || !params || !params[0]) {
+    return;
+  }
+
+  connection.logdebug(plugin, "Checking if " + params[0] + " is in mysql");
+
+  // a client with relaying privileges is sending from a local domain.
+  // Any RCPT is acceptable.
+  if (connection.relaying && txn.notes.local_sender) {
+    txn.results.add(plugin, {pass: "relaying local_sender"});
+    return next(OK);
+  }
+
+  exports.in_mysql(connection, params[0], function (err, result) {
+    if (err) {
+      txn.results.add(plugin, {err: err});
+      return next();
+    }
+
+    if (result) {
+      txn.results.add(plugin, {pass: "rcpt_to"});
+      return next(OK);
+    }
+
+    // no need to DENY[SOFT] for invalid addresses. If no rcpt_to.* plugin
+    // returns OK, then the address is not accepted.
+    txn.results.add(plugin, {msg: "rcpt!local"});
+    return next();
+  });
+};
+
+exports.in_mysql = function (connection, address, callback) {
+  var txn = connection.transaction;
+  var plugin = this;
+
+  if (!connection.server.notes.mysql_provider) return callback(new Error('mysql provider seems mot initialized'));
+  if (!plugin.cfg.main.query) return callback(new Error('no query configured'));
+
+  var values = [address.host, address.user, address.address()];
+  connection.server.notes.mysql_provider.escape(values, function(error, query_values) {
+    if (error) return callback(error);
+    var query = plugin.cfg.main.query.replace(/%d/g, query_values[0]).replace(/%n/g, query_values[1]).replace(/%u/g, query_values[2]);
+  
+    txn.results.add(plugin, {msg: "exec query: " + query});
+    connection.server.notes.mysql_provider.query(query, function (err, result) {
+      if (err) return callback(err);
+      if (!result || !result[0]) return callback(null, null);
+      callback(null, true);
+    });
+  });
+};

+ 5 - 0
haraka/run.sh

@@ -0,0 +1,5 @@
+#!/bin/sh
+
+echo $MAIL_HOSTNAME > /haraka/config/me
+
+haraka -c haraka

+ 7 - 0
proxy/Dockerfile

@@ -0,0 +1,7 @@
+FROM nginx:alpine
+
+ADD proxy/nginx.conf /etc/nginx/nginx.conf
+
+EXPOSE 8080
+EXPOSE 8443
+

+ 53 - 0
proxy/nginx.conf

@@ -0,0 +1,53 @@
+user  nginx;
+worker_processes  1;
+
+error_log  /dev/stderr notice;
+
+events {
+    worker_connections  1024;
+}
+
+http {
+    include       mime.types;
+    default_type  application/octet-stream;
+
+    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
+                      '$status $body_bytes_sent "$http_referer" '
+                      '"$http_user_agent" "$http_x_forwarded_for"';
+
+    access_log  /dev/stdout  main;
+
+    sendfile        on;
+    #tcp_nopush     on;
+
+    keepalive_timeout  65;
+
+    gzip  on;
+
+    server {
+        listen       8080;
+        server_name  _;
+	return 301 https://$host$request_uri;
+    }
+
+    server {
+        listen       8443 ssl;
+        server_name  _;
+
+        ssl_certificate      /etc/nginx/certs/fullchain.pem;
+        ssl_certificate_key  /etc/nginx/certs/privkey.pem;
+
+        ssl_session_cache    shared:SSL:1m;
+        ssl_session_timeout  5m;
+
+        ssl_ciphers  HIGH:!aNULL:!MD5;
+        ssl_prefer_server_ciphers  on;
+
+        client_max_body_size 50M;
+
+        location / {
+	    proxy_pass http://rainloop:8888;
+        }
+    }
+}
+

+ 1 - 0
rainloop/Dockerfile

@@ -0,0 +1 @@
+FROM hardware/rainloop:latest

+ 3 - 0
redis/Dockerfile

@@ -0,0 +1,3 @@
+FROM redis:alpine3.8
+COPY redis/redis.conf /usr/local/etc/redis/redis.conf
+CMD [ "redis-server", "/usr/local/etc/redis/redis.conf" ]

+ 2 - 0
redis/redis.conf

@@ -0,0 +1,2 @@
+maxmemory 500mb
+maxmemory-policy volatile-ttl

+ 14 - 0
rspamd/Dockerfile

@@ -0,0 +1,14 @@
+FROM alpine:edge
+
+RUN apk add --update --no-cache rspamd rspamd-client && \
+    mkdir -p /run/rspamd && \
+    chown rspamd:rspamd /run/rspamd
+ADD rspamd/initial_train.sh /initial_train.sh
+ADD rspamd/run.sh /run.sh
+ADD rspamd/rspamd /etc/rspamd
+
+EXPOSE 11333
+
+VOLUME [ "/rspamd" ]
+
+CMD /run.sh

+ 7 - 0
rspamd/initial_train.sh

@@ -0,0 +1,7 @@
+#!/bin/sh
+
+wget -O /tmp/bayes.ham.sqlite https://rspamd.com/rspamd_statistics/bayes.ham.sqlite
+wget -O /tmp/bayes.spam.sqlite https://rspamd.com/rspamd_statistics/bayes.spam.sqlite
+
+rspamadm statconvert --spam-db /tmp/bayes.spam.sqlite --ham-db /tmp/bayes.ham.sqlite --redis-host redis --symbol-spam BAYES_SPAM --symbol-ham BAYES_HAM
+

+ 821 - 0
rspamd/rspamd/2tld.inc

@@ -0,0 +1,821 @@
+0000host.com
+000webhost.com
+007sites.com
+007webpro.com
+00author.com
+00bp.com
+00it.com
+00server.com
+00trek.com
+012webpages.com
+0505mb.com
+0805.ru
+0buckhost.com
+0catch.com
+0fees.net
+0golf.com
+0moola.com
+0pi.com
+10001mb.com
+100free.com
+100freemb.com
+100mb.com
+100webspace.net
+101freehost.com
+10fast.net
+110mb.com
+1111mb.com
+125mb.com
+12gbfree.com
+150m.com
+18.lc
+1asphost.com
+1freewebspace.com
+1gb.ru
+1hwy.com
+1majorhost.com
+1sta.com
+1sweethost.com
+1.vg
+1x.net
+20fr.com
+20ii.com
+20is.com
+20m.com
+20to.com
+21r.ru
+22web.net
+247ihost.com
+24.lc
+250free.com
+275mb.com
+2.ag
+2.je
+2.ly
+2u-2.com
+3000mb.com
+30mb.com
+3d.lc
+3dn.ru
+3eu.ru
+3-hosting.net
+4all.ru
+4.je
+4mg.com
+4sql.net
+4t.com
+501megs.com
+50megs.com
+50webs.com
+55fast.com
+5gbfree.com
+5nxs.com
+5u.com
+6bone.pl
+6.je
+6te.net
+700up.com
+70mb.ru
+741.com
+789mb.com
+7.je
+890m.com
+8.je
+8k.com
+8m.com
+8m.net
+911mb.com
+99inch.com
+9cy.com
+9gb.de
+9.je
+a1free.net
+abkhazia.su
+about.lc
+access.to
+ac.ng
+act.gov.au
+aecru.org
+aeroport.ci
+agava.ru
+aget.ru
+aiq.ru
+alfaspace.net
+all.lc
+al.ru
+altervista.org
+angelcities.com
+angelfire.com
+anschauen.in
+arcadepages.com
+armenia.su
+ar.nf
+ashgabad.su
+asia.gp
+as.no
+at.cr
+at.lv
+atspace.biz
+atspace.com
+atspace.name
+atspace.org
+atspace.us
+at.st
+at.ua
+auto.lc
+aw3.de
+awardspace.biz
+awardspace.com
+azerbaijan.su
+b2b.lc
+b4site.com
+badland.com
+bappy.com
+batcave.net
+bebto.com
+belgorod.su
+beplaced.ru
+berlin.tl
+bilder.lc
+bilder.net
+bip.ru
+bizmail.ru
+biz.ps
+biz.ua
+biz.uz
+blogindo.net
+blog.lc
+blog-nn.ru
+bl.uk
+boom.ru
+bos.ru
+bravehost.com
+brinkster.net
+builtfree.org
+bukhara.su
+bund.in
+byethost10.com
+byethost11.com
+byethost12.com
+byethost13.com
+byethost14.com
+byethost15.com
+byethost16.com
+byethost17.com
+byethost18.com
+byethost24.com
+byethost2.com
+byethost3.com
+byethost4.com
+byethost5.com
+byethost6.com
+byethost7.com
+byethost8.com
+byethost9.com
+byethost.com
+by.ru
+byteact.com
+ca.nf
+cantv.net
+center.tl
+chat.dj
+chat.ru
+cheeb.com
+chimkent.su
+ch.kg
+ch.st
+cileni.com
+city.tl
+ciy.de
+cl4n.org
+clandomain.de
+clandomain.org
+clan.lc
+clan.mn
+clan.su
+clubfx.ru
+club.lc
+cn.nf
+co.cc
+codingclub.ru
+co.gp
+com1.ru
+community.lc
+com.nu
+cool.hn
+cool.lc
+coolpage.biz
+cs-clan.org
+csiro.au
+cwx.ru
+da.cx
+datadiri.com
+dax.ru
+de.cg
+deep-ice.com
+de.gd
+de.gp
+de.ht
+de.im
+de.ki
+dem.ru
+de.pl
+design.tl
+deutschland.nu
+dex1.com
+dezigner.ru
+digitalzones.com
+dnevnik.ru
+dnip.net
+do.am
+domain.lc
+download.ac
+download.sh
+dr.ag
+dreamstation.com
+dtn.ru
+dvd.lc
+e2e.ru
+east-kazakhstan.su
+ecard.ru
+eclub.lv
+email.su
+enacre.net
+english.lc
+envy.nu
+epage.ru
+eqo.de
+esel.in
+es.gp
+eu.cr
+eu.gg
+eu.gp
+eu.ki
+eu.nu
+eur.lc
+euro.lc
+europa.lc
+europtrade.ru
+euro.ru
+ex6.ru
+exnet.su
+faithweb.com
+far.ru
+fatal.ru
+fatfreehost.com
+fbhosting.com
+fbi.ru
+fcpages.com
+ffx.ru
+filme.lc
+firm.nu
+fk.gs
+flf.li
+flirten.info
+flirt.lc
+fora.pl
+forever.kz
+forum24.ru
+forum.lc
+fotos.lc
+fr33webhost.com
+frageon.net
+free.bg
+freebitty.com
+freecities.com
+free.cr
+freehomepage.com
+freehost10.com
+freehostia.com
+freehosting.net
+freehostingz.com
+freehostonline.com
+freehostpage.com
+freehostplace.com
+freehostpro.com
+freehostspace.com
+freehostyou.com
+free.lc
+freepage.ru
+freeservers.com
+free-site-host.com
+freesite.org
+freetzi.com
+freewaywebhost.com
+freeweb7.com
+free-web-hosting.biz
+freewebhostingpro.com
+free-webhosts.com
+freewebpage.org
+freewebpages.org
+freewebspace.com
+freewhost.com
+freezoka.com
+freezona.ru
+fr.gp
+fromru.com
+fromru.su
+front.ru
+fun.gg
+fws1.com
+fxcity.ru
+fx-club.org
+fxf.ru
+game.lc
+gaming.lc
+ganja.ru
+georgia.su
+gigcities.com
+gigsweb.com
+gilde.in
+glooby.net
+gmbh.tw
+gmxhome.de
+gn8.net
+go9.ru
+goa.ru
+gobot.com
+googlegroups.com
+gorodok.net
+go.ru
+gov.gg
+gov.im
+gov.je
+gov.mt
+gq.nu
+gratis.lc
+greatnow.com
+greenline.ru
+gr.kg
+guest.de
+guests.de
+h10.ru
+h11.ru
+h12.ru
+h14.ru
+h15.ru
+h16.ru
+h17.ru
+h18.ru
+h1.ru
+h2m.ru
+h5.ru
+hase.in
+hash.ru
+hexe.in
+hit.bg
+ho11.com
+hocom.by
+hocomua.ru
+hoha.ru
+home.lc
+homepage.cx
+homepage.lc
+homestead.com
+hophost.net
+hop.ru
+hopto.org
+hostaim.com
+hostia.ru
+hostingisus.com
+hostonfly.ru
+host-page.com
+hostq.ru
+hostwq.net
+hotbox.ru
+hoter.ru
+hot.lc
+hotmail.ru
+hotusa.org
+ho.ua
+hp.lc
+htmlplanet.com
+hu2.ru
+hu.kg
+hund.in
+hut1.ru
+hut2.ru
+hut.ru
+hy.cz
+i8.com
+ibnsites.com
+ic.cz
+icr38.net
+id.ru
+iespana.es
+ifastnet.com
+ifolder.ru
+ifrance.com
+iifree.net
+ilovethis.ru
+imess.net
+indiegroup.com
+i-nets.ru
+infobox.ru
+info.nu
+infos.cx
+infos.lc
+infostore.org
+inlocal.ru
+in.nf
+interia.pl
+internet.ly
+int.ps
+intwayblog.net
+intway.info
+io.ua
+iplot.ru
+irc.pl
+isgreat.org
+itgo.com
+it.gp
+itrello.com
+iwarp.com
+iwebsource.com
+izypage.net
+jambyl.su
+jet.uk
+jimdo.com
+jino-net.ru
+jino.ru
+jixx.de
+jixx.net
+job.ec
+joolo.com
+judiciary.uk
+justfree.com
+jvl.com
+k12.nd.us
+k12.sd.us
+k12.wv.us
+k1.cx
+karacol.su
+karaganda.su
+kogaryu.com
+komi.su
+kostenlos.lc
+kr3w.de
+krovatka.net
+krovatka.su
+kustanai.su
+kwikphp.com
+kzet.ru
+l4n.org
+land.ru
+lbgo.com
+lgg.ru
+lib.wv.us
+liebe.lc
+links.lc
+logmail.ru
+lookingat.us
+lpchat.com
+lu.kg
+lycos.es
+lydo.org
+maclenet.com
+mail15.com
+mail2k.ru
+mail333.com
+mail.ht
+mangyshlak.su
+mbone.pl
+md6.ru
+md8.ru
+me.ly
+memebot.com
+metastock.ru
+mindnmagick.com
+mirohost.net
+mitglieder.in
+mobi.ps
+mod.uk
+mokoginta.net
+mosreg.ru
+movillink.com
+moy.su
+mp3.cr
+mp3.gp
+mpeg3.ru
+musik.cx
+musik.lc
+m.vu
+my1.ru
+my-age.net
+mybyte.ru
+myfreewebhost.org
+mylivepage.ru
+myweb.io
+na.by
+nagakute.aichi.jp
+name.ac
+name.vg
+name.vu
+narod2.ru
+narod.ru
+navoi.su
+nazwa.pl
+net23.net
+net84.net
+netfast.org
+netfirms.com
+netfreehost.com
+netsolhost.com
+netz.tl
+newmail.ru
+nextmail.ru
+ngo.pl
+nichost.ru
+nic.im
+nic.uk
+nightmail.ru
+nl.gp
+nl.kg
+nls.uk
+nm.ru
+nofeehost.com
+noneto.com
+north-kazakhstan.su
+notlong.com
+now.lc
+npx.de
+nrg.ru
+nt.gov.au
+nutzworld.net
+nxt.ru
+oceansfree.com
+o-f.com
+okis.ru
+ok.ru
+omp9.com
+on-4.com
+onecoolhost.com
+onepage.ru
+onep.ru
+on.lc
+online.cr
+online.gp
+online.tc
+onlymail.ru
+orc.ru
+orgfree.com
+org.pg.
+ourprofile.us
+ownspace.org
+p0.ru
+page.by
+parliament.uk
+party.lc
+pc.lc
+photos.lc
+php0h.com
+php0h.net
+php1h.com
+phreak.ru
+pips.ru
+pisem.net
+pisem.su
+pixi.ru
+planet.tl
+plazma.ru
+pochtamt.ru
+pochta.org
+pochta.ru
+politik.in
+polubomu.ru
+pop3.ru
+portal.lc
+privat.lc
+pro.ac
+programist.ru
+prohosting.com
+prohosts.org
+pro.ly
+promural.ru
+promzone.ru
+pro.vg
+proxycheker.ru
+psend.com
+pskov.ru
+pt.kg
+pud.ru
+quake.ru
+qu.am
+quotaless.com
+qwerty.in
+qwerty.su
+r2.ru
+rbcmail.ru
+reghosting.com
+republika.pl
+rubas.ru
+ru.gg
+ru.nf
+ruprom.net
+ru.ru
+russian.ru
+rx.ru
+s5.com
+samomu.ru
+sapo.pt
+sbn.bz
+sch.gg
+sch.je
+scriptmania.com
+sdsmt.edu
+search.gp
+seite24.eu
+seite.asia
+seite.com
+seite.cz
+seite.es
+seite.im
+seite.in
+seite.info
+seite.li
+seite.lt
+seite.lv
+seite.name
+seite.pl
+seite.ru
+seite.sc
+seite.st
+seite.vc
+se.nf
+server.gg
+server.tl
+servetown.com
+service.lc
+servik.com
+se-ua.net
+share.lc
+shop.fm
+shopping.cr
+shopping.hn
+siedlce.pl
+sinfree.net
+site40.net
+site50.net
+site88.net
+siteburg.com
+sitecity.ru
+siteedit.ru
+siteedit.su
+sitehome.ru
+sitesfree.com
+sitesled.com
+sk6.ru
+sk6.ru
+sms.cr
+smtp.ru
+space.lc
+sphosting.com
+sport.dj
+sppages.com
+sprinterweb.net
+sp.ru
+sshn.se
+start.lc
+stsland.ru
+students.ru
+student.su
+subs.ru
+supercharts.ru
+swiftphp.com
+szm.com
+t35.com
+takizawa.iwate.jp
+tashkent.su
+te4m.de
+team.cx
+team.tl
+tecbox.com
+tekcities.com
+terapad.com
+termez.su
+test.lc
+teufel.in
+thelan.info
+times.lv
+tipp.cz
+top8.com
+topcities.com
+topfreewebhosting.com
+topf.ru
+top.lc
+top.tc
+tora.ru
+totalh.com
+tripod.com
+tselinograd.su
+tu1.ru
+tu2.ru
+turkish.lc
+tushino.com
+tut.su
+tv.gg
+tvheaven.com
+tw1.ru
+uadom.net
+ucoz.com
+ucoz.de
+ucoz.es
+ucoz.hu
+ucoz.kz
+ucoz.lv
+ucoz.net
+ucoz.org
+ucoz.ru
+ucoz.ua
+ueuo.com
+ufanet.ru
+uk.cr
+uk.nf
+ukrbiz.net
+uk.st
+undonet.com
+urlaub.lc
+urllogs.com
+url-site.com
+ur.ru
+usafreespace.com
+usa.gg
+usenet.pl
+user.lc
+us.gg
+us.ly
+us.nf
+us.st
+valuehost.ru
+vdelo.ru
+vdsite.ru
+vettepics.com
+vhost4free.com
+vio.ru
+vipcentr.ru
+vipshop.ru
+vip.su
+viptop.ru
+virtuale.org
+vi-rus.ru
+voten.in
+vo.uz
+vov.ru
+vze.com
+vz.lc
+w6.ru
+wallst.ru
+wapn.ru
+wbs.cz