Why?
Although not problematic for most commercial Email providers(GMail, Outlook, etc.), self-hosted Email are prone to inbound SPAM.
A couple solutions exist: the most common one is to use local Spamassassin - but training the model takes time and there's no good corpse available - and spammers are much smarter.
There are a handful of hosted Inbound SPAM filtering solutions but are quite expensive:
- Mailchannels - $20/month/5 domains, then hikes to $507/mth/1000 domains.
- Spamtitan - pricing is "quotation only".
- Spydermail - $1.99/mailbox/month, starting from 15 mailboxes.
- MXguarddog - $0.25/mailbox/month.
- Spamhero - per mailbox per mail pricing.
- McAfee: pricing not available.
Use Mailgun
Mailgun is included in Github student package. Flex plan provides 1000 Emails/month for free.
Code of connector:
import os
from flask import Flask, request, jsonify
from imap_tools import MailBox
app = Flask(__name__)
# HTTP param > environment variable > default
IMAP_USERNAME = os.getenv('IMAP_USERNAME', '')
IMAP_PASSWORD = os.getenv('IMAP_PASSWORD', '')
IMAP_SERVER = os.getenv('IMAP_SERVER', '')
MAILGUN_ANTISPAM_USE_BOOL = int(os.getenv('MAILGUN_ANTISPAM_USE_BOOL', '0') or 0)
MAILGUN_ANTISPAM_SSCORE_CUTOFF = int(os.getenv('MAILGUN_ANTISPAM_SSCORE_CUTOFF', '20') or 20)
def get_target_mailbox(data, use_bool=MAILGUN_ANTISPAM_USE_BOOL, sscore_cutoff=MAILGUN_ANTISPAM_SSCORE_CUTOFF):
"""
Return target mailbox from the data received from Mailgun.
See https://documentation.mailgun.com/en/latest/user_manual.html#spam-filter
for more details.
MAILGUN_ANTISPAM_USE_BOOL suppresses MAILGUN_ANTISPAM_CUTOFF.
:param junk_threshold: threshold for junk mail according to Mailgun
:type junk_threshold: float
:param data: mail data from Mailgun
:type data: dict
:return: INBOX/Junk
:rtype: str
"""
# No = Not spam, Yes = Spam
# "At the time of writing this, we are filtering spam at a score of around 5.0 but we are constantly calibrating
# this."
if use_bool:
if data['X-Mailgun-Sflag'][0] == 'No':
return 'INBOX'
return 'Junk'
# lower = less likely to be spam
# negative = very unlikely to be spam
# > 20 is very likely to be spam
if float(data['X-Mailgun-Sscore'][0]) < sscore_cutoff:
return 'INBOX'
return 'Junk'
@app.route('/post_mime', methods=['POST'])
def post_mime():
# TODO: check if the request is from Mailgun
data = request.form.to_dict(flat=False)
# HTTP param suppresses everything else
imap_username = request.args.get('username', IMAP_USERNAME)
imap_password = request.args.get('password', IMAP_PASSWORD)
imap_server = request.args.get('server', IMAP_SERVER)
use_bool = int(request.args.get('use_bool', MAILGUN_ANTISPAM_USE_BOOL))
sscore_cutoff = int(request.args.get('sscore_cutoff', MAILGUN_ANTISPAM_SSCORE_CUTOFF))
# decide whether incoming mail is spam or not
try:
target_mailbox = get_target_mailbox(data, use_bool, sscore_cutoff)
except Exception as e:
return jsonify({'error': 'Cannot decide target: ' + str(e)}), 500
# directly deliver to target mailbox
try:
with MailBox(imap_server).login(imap_username, imap_password) as mailbox:
msg = '\n'.join(data['body-mime'])
mailbox.append(msg.encode(), target_mailbox, dt=None)
except Exception as e:
return jsonify({'error': str(e), 'status': -1}), 500
return jsonify({'status': 0})
if __name__ == '__main__':
app.run(host='localhost', port=8000, debug=True)
Setup environment variables as follows:
Name Value
IMAP_PASSWORD your password
IMAP_SERVER the server
IMAP_USERNAME login
LANG en_US.UTF-8
LC_ALL en_US.UTF-8
LC_LANG en_US.UTF-8
MAILGUN_ANTISPAM_SSCORE_CUTOFF 20
MAILGUN_ANTISPAM_USE_BOOL 1
PYTHONIOENCODING UTF-8
Go to Mailgun, setup a Route with catch all forwarding to https://your-binded-domain/post_mime
.