diff --git a/README.md b/README.md index 2f25970..cf455a7 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ This draws heavily on Sovereign by Alex Payne (https://github.com/al3x/sovereign Deploying to EC2 from the command line -------------------------------------- +Amazon's EC2 isn't a great place to host a mail server. Do you still need to request permission to send email first? And you don't know if you'll get an IP address with a bad reputation from its previous owner. But it makes deployment easy, so it may at least be useful for testing. + Sign up for Amazon Web Services. Create an Access Key at https://console.aws.amazon.com/iam/home?#security_credential. Download the key and save the information somewhere secure. @@ -53,6 +55,8 @@ Somehow download these files. sh scripts/index.sh ... logout + +You'll also want to set reverse DNS (PTR), which is something your hosting provider will probably have a control panel for. Terminate your instance with: diff --git a/scripts/mail.sh b/scripts/mail.sh index 45443fb..3cdf6ce 100755 --- a/scripts/mail.sh +++ b/scripts/mail.sh @@ -109,17 +109,22 @@ EOF sed -i "s/#port = 143/port = 0/" /etc/dovecot/conf.d/10-master.conf sed -i "s/#port = 110/port = 0/" /etc/dovecot/conf.d/10-master.conf -# Modify the unix socket for LMTP. -sed -i "s/unix_listener lmtp \(.*\)/unix_listener \/var\/spool\/postfix\/private\/dovecot-lmtp \1\n user = postfix\n group = postfix\n/" /etc/dovecot/conf.d/10-master.conf - -# Add an additional auth socket for postfix. Check if it already is -# set to make sure this is idempotent. -if grep -q "mailinabox-postfix-private-auth" /etc/dovecot/conf.d/10-master.conf; then - # already done - true; -else - sed -i "s/\(\s*unix_listener auth-userdb\)/ unix_listener \/var\/spool\/postfix\/private\/auth \{ # mailinabox-postfix-private-auth\n mode = 0666\n user = postfix\n group = postfix\n \}\n\1/" /etc/dovecot/conf.d/10-master.conf -fi +# Create a Unix domain socket specific for postgres to connect via LMTP because +# postgres is already configured to use this location, and create a TCP socket +# for spampd to inject mail on (if it's configured later). dovecot's standard +# lmtp unix socket is also listening. +cat > /etc/dovecot/conf.d/99-local.conf << EOF; +service lmtp { + unix_listener /var/spool/postfix/private/dovecot-lmtp { + user = postfix + group = postfix + } + inet_listener lmtp { + address = 127.0.0.1 + port = 10026 + } +} +EOF # Drew Crawford sets the auth-worker process to run as the mail user, but we don't care if it runs as root. diff --git a/scripts/spamassassin.sh b/scripts/spamassassin.sh new file mode 100644 index 0000000..6a8ebc4 --- /dev/null +++ b/scripts/spamassassin.sh @@ -0,0 +1,48 @@ +# Spam filtering with spamassassin via spampd. + +apt-get -q -y install spampd dovecot-antispam + +# Hook into postfix. Replace dovecot with spampd as the mail delivery agent. +tools/editconf.py /etc/postfix/main.cf virtual_transport=lmtp:[127.0.0.1]:10025 + +# Hook into dovecot. This is actually the default but we don't want to lose track of it. +tools/editconf.py /etc/default/spampd DESTPORT=10026 + +# Automatically move spam into a folder called Spam. Enable the sieve plugin. +# (Note: Be careful if we want to use multiple plugins later.) +# The sieve scripts are installed by users_update.sh. +sudo sed -i "s/#mail_plugins = .*/mail_plugins = \$mail_plugins sieve/" /etc/dovecot/conf.d/20-lmtp.conf + +# Enable the antispam plugin to detect when a message moves between folders so we can +# pass it to sa-learn for training. (Be careful if we use multiple plugins later.) +sudo sed -i "s/#mail_plugins = .*/mail_plugins = \$mail_plugins antispam/" /etc/dovecot/conf.d/20-imap.conf + +# When mail is moved in or out of the dovecot Spam folder, re-train. +# from http://wiki2.dovecot.org/Plugins/Antispam +cat > /usr/bin/sa-learn-pipe.sh << EOF; +cat<&0 >> /tmp/sendmail-msg-\$\$.txt +/usr/bin/sa-learn \$* /tmp/sendmail-msg-\$\$.txt > /dev/null +rm -f /tmp/sendmail-msg-\$\$.txt +exit 0 +EOF + +chmod a+x /usr/bin/sa-learn-pipe.sh + +cat > /etc/dovecot/conf.d/99-local-spampd.conf << EOF; +plugin { + antispam_backend = pipe + antispam_spam_pattern_ignorecase = SPAM + antispam_allow_append_to_spam = yes + antispam_pipe_program_spam_arg = /usr/bin/sa-learn-pipe.sh --spam + antispam_pipe_program_notspam_arg = /usr/bin/sa-learn-pipe.sh --ham + antispam_pipe_program = /bin/bash +} +EOF + +# Initial training? +# sa-learn --ham storage/mail/mailboxes/*/*/cur/ +# sa-learn --spam storage/mail/mailboxes/*/*/.Spam/cur/ + +sudo service spampd restart +sudo service dovecot restart + diff --git a/scripts/users_update.sh b/scripts/users_update.sh new file mode 100644 index 0000000..4e5f768 --- /dev/null +++ b/scripts/users_update.sh @@ -0,0 +1,22 @@ +# Install dovecot sieve scripts to automatically move spam into the Spam folder. + +db_path=$STORAGE_ROOT/mail/users.sqlite + +for user in `echo "SELECT email FROM users;" | sqlite3 $db_path`; do + maildir=`echo $user | sed "s/\(.*\)@\(.*\)/\2\/\1/"` + + # Write the sieve file to move mail classified as spam into the spam folder. + mkdir -p $STORAGE_ROOT/mail/mailboxes/$maildir; # in case user has not received any mail + cat > $STORAGE_ROOT/mail/mailboxes/$maildir/.dovecot.sieve << EOF; +require ["regex", "fileinto", "imap4flags"]; + +if allof (header :regex "X-Spam-Status" "^Yes") { + setflag "\\\\Seen"; + fileinto "Spam"; + stop; +} +EOF + + +done + diff --git a/tests/imap.py b/tests/imap.py index e37c68d..af3442a 100644 --- a/tests/imap.py +++ b/tests/imap.py @@ -1,7 +1,9 @@ import imaplib, os +username = "testuser@" + os.environ.get("DOMAIN", "testdomain.com") + M = imaplib.IMAP4_SSL(os.environ["INSTANCE_IP"]) -M.login("testuser@testdomain.com", "testpw") +M.login(username, "testpw") M.select() print("Login successful.") typ, data = M.search(None, 'ALL') diff --git a/tests/smtp.py b/tests/smtp_submission.py similarity index 100% rename from tests/smtp.py rename to tests/smtp_submission.py diff --git a/tools/editconf.py b/tools/editconf.py index c7e3af5..e5e8d87 100755 --- a/tools/editconf.py +++ b/tools/editconf.py @@ -4,20 +4,27 @@ import sys, re # sanity check if len(sys.argv) < 3: - print("usage: python3 editconf.py /etc/file.conf NAME=VAL [NAME=VAL ...]") + print("usage: python3 editconf.py /etc/file.conf [-s] NAME=VAL [NAME=VAL ...]") sys.exit(1) # parse command line arguments filename = sys.argv[1] settings = sys.argv[2:] +delimiter = "=" +delimiter_re = r"\s*=\s*" +if settings[0] == "-s": + settings.pop(0) + delimiter = " " + delimiter_re = r"\s+" + # create the new config file in memory found = set() buf = "" for line in open(filename): for i in range(len(settings)): name, val = settings[i].split("=", 1) - m = re.match("\s*" + re.escape(name) + "\s*=\s*(.*?)\s*$", line) + m = re.match("\s*" + re.escape(name) + delimiter_re + "(.*?)\s*$", line) if m: # If this is already the setting, do nothing. if m.group(1) == val: @@ -33,7 +40,7 @@ for line in open(filename): break # add the new setting - buf += name + "=" + val + "\n" + buf += name + delimiter + val + "\n" # note that we've applied this option found.add(i) @@ -46,7 +53,8 @@ for line in open(filename): # Put any settings we didn't see at the end of the file. for i in range(len(settings)): if i not in found: - buf += settings[i] + "\n" + name, val = settings[i].split("=", 1) + buf += name + delimiter + val + "\n" # Write out the new file. with open(filename, "w") as f: