Return 421 when there's too much activity on an alias or mailbox
This commit is contained in:
parent
661547ec3a
commit
8caebc0142
|
@ -208,6 +208,14 @@ PAGE_LIMIT = 20
|
||||||
LOCAL_FILE_UPLOAD = "LOCAL_FILE_UPLOAD" in os.environ
|
LOCAL_FILE_UPLOAD = "LOCAL_FILE_UPLOAD" in os.environ
|
||||||
UPLOAD_DIR = None
|
UPLOAD_DIR = None
|
||||||
|
|
||||||
|
# Greylisting features
|
||||||
|
# minimal time in seconds an alias can receive/send emails
|
||||||
|
MIN_TIME_BETWEEN_ACTIVITY_PER_ALIAS = 8
|
||||||
|
|
||||||
|
# minimal time in seconds a mailbox can receive/send emails
|
||||||
|
MIN_TIME_BETWEEN_ACTIVITY_PER_MAILBOX = 3
|
||||||
|
|
||||||
|
|
||||||
if LOCAL_FILE_UPLOAD:
|
if LOCAL_FILE_UPLOAD:
|
||||||
print("Upload files to local dir")
|
print("Upload files to local dir")
|
||||||
UPLOAD_DIR = os.path.join(ROOT_DIR, "static/upload")
|
UPLOAD_DIR = os.path.join(ROOT_DIR, "static/upload")
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
import arrow
|
||||||
|
|
||||||
|
from app.alias_utils import try_auto_create
|
||||||
|
from app.config import (
|
||||||
|
MIN_TIME_BETWEEN_ACTIVITY_PER_ALIAS,
|
||||||
|
MIN_TIME_BETWEEN_ACTIVITY_PER_MAILBOX,
|
||||||
|
)
|
||||||
|
from app.extensions import db
|
||||||
|
from app.log import LOG
|
||||||
|
from app.models import Alias, EmailLog, Contact
|
||||||
|
|
||||||
|
|
||||||
|
def greylisting_needed_for_alias(alias: Alias) -> bool:
|
||||||
|
# get the latest email activity on this alias
|
||||||
|
r = (
|
||||||
|
db.session.query(EmailLog, Contact)
|
||||||
|
.filter(EmailLog.contact_id == Contact.id, Contact.alias_id == alias.id)
|
||||||
|
.order_by(EmailLog.id.desc())
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
if r:
|
||||||
|
email_log, _ = r
|
||||||
|
now = arrow.now()
|
||||||
|
if (now - email_log.created_at).seconds < MIN_TIME_BETWEEN_ACTIVITY_PER_ALIAS:
|
||||||
|
LOG.d(
|
||||||
|
"Too much forward on alias %s. Latest email log %s", alias, email_log,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def greylisting_needed_for_mailbox(alias: Alias) -> bool:
|
||||||
|
# get the latest email activity on this mailbox
|
||||||
|
r = (
|
||||||
|
db.session.query(EmailLog, Contact, Alias)
|
||||||
|
.filter(
|
||||||
|
EmailLog.contact_id == Contact.id,
|
||||||
|
Contact.alias_id == Alias.id,
|
||||||
|
Alias.mailbox_id == alias.mailbox_id,
|
||||||
|
)
|
||||||
|
.order_by(EmailLog.id.desc())
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
if r:
|
||||||
|
email_log, _, _ = r
|
||||||
|
now = arrow.now()
|
||||||
|
if (now - email_log.created_at).seconds < MIN_TIME_BETWEEN_ACTIVITY_PER_MAILBOX:
|
||||||
|
LOG.d(
|
||||||
|
"Too much forward on mailbox %s. Latest email log %s. Alias %s",
|
||||||
|
alias.mailbox,
|
||||||
|
email_log,
|
||||||
|
alias,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def greylisting_needed_forward_phase(alias_address: str) -> bool:
|
||||||
|
alias = Alias.get_by(email=alias_address)
|
||||||
|
|
||||||
|
if alias:
|
||||||
|
return greylisting_needed_for_alias(alias) or greylisting_needed_for_mailbox(
|
||||||
|
alias
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
LOG.d(
|
||||||
|
"alias %s not exist. Try to see if it can be created on the fly",
|
||||||
|
alias_address,
|
||||||
|
)
|
||||||
|
alias = try_auto_create(alias_address)
|
||||||
|
if alias:
|
||||||
|
return greylisting_needed_for_mailbox(alias)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def greylisting_needed_reply_phase(reply_email: str) -> bool:
|
||||||
|
contact = Contact.get_by(reply_email=reply_email)
|
||||||
|
if not contact:
|
||||||
|
return False
|
||||||
|
|
||||||
|
alias = contact.alias
|
||||||
|
return greylisting_needed_for_alias(alias) or greylisting_needed_for_mailbox(alias)
|
||||||
|
|
||||||
|
|
||||||
|
def greylisting_needed(mail_from: str, rcpt_tos: [str]) -> bool:
|
||||||
|
for rcpt_to in rcpt_tos:
|
||||||
|
if rcpt_to.startswith("reply+") or rcpt_to.startswith("ra+"):
|
||||||
|
reply_email = rcpt_to.lower()
|
||||||
|
if greylisting_needed_reply_phase(reply_email):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
# Forward phase
|
||||||
|
address = rcpt_to.lower() # alias@SL
|
||||||
|
if greylisting_needed_forward_phase(address):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
|
@ -69,6 +69,7 @@ from app.email_utils import (
|
||||||
get_orig_message_from_spamassassin_report,
|
get_orig_message_from_spamassassin_report,
|
||||||
)
|
)
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
|
from app.greylisting import greylisting_needed
|
||||||
from app.log import LOG
|
from app.log import LOG
|
||||||
from app.models import (
|
from app.models import (
|
||||||
Alias,
|
Alias,
|
||||||
|
@ -792,6 +793,13 @@ def handle(envelope: Envelope, smtp: SMTP) -> str:
|
||||||
LOG.d("Handle unsubscribe request from %s", envelope.mail_from)
|
LOG.d("Handle unsubscribe request from %s", envelope.mail_from)
|
||||||
return handle_unsubscribe(envelope)
|
return handle_unsubscribe(envelope)
|
||||||
|
|
||||||
|
# Whether it's necessary to apply greylisting
|
||||||
|
if greylisting_needed(envelope.mail_from, envelope.rcpt_tos):
|
||||||
|
LOG.warning(
|
||||||
|
"Grey listing applied for %s %s", envelope.mail_from, envelope.rcpt_tos
|
||||||
|
)
|
||||||
|
return "421 SL Retry later"
|
||||||
|
|
||||||
# result of all deliveries
|
# result of all deliveries
|
||||||
# each element is a couple of whether the delivery is successful and the smtp status
|
# each element is a couple of whether the delivery is successful and the smtp status
|
||||||
res: [(bool, str)] = []
|
res: [(bool, str)] = []
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
from app.extensions import db
|
||||||
|
from app.greylisting import (
|
||||||
|
greylisting_needed_forward_phase,
|
||||||
|
greylisting_needed_for_alias,
|
||||||
|
greylisting_needed_for_mailbox,
|
||||||
|
greylisting_needed_reply_phase,
|
||||||
|
)
|
||||||
|
from app.models import User, Alias, EmailLog, Contact
|
||||||
|
|
||||||
|
|
||||||
|
def test_greylisting_needed_forward_phase_for_alias(flask_client):
|
||||||
|
user = User.create(
|
||||||
|
email="a@b.c", password="password", name="Test User", activated=True
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# no greylisting for a new alias
|
||||||
|
alias = Alias.create_new_random(user)
|
||||||
|
db.session.commit()
|
||||||
|
assert not greylisting_needed_for_alias(alias)
|
||||||
|
|
||||||
|
# greylisting when there's a previous activity on alias
|
||||||
|
contact = Contact.create(
|
||||||
|
user_id=user.id,
|
||||||
|
alias_id=alias.id,
|
||||||
|
website_email="contact@example.com",
|
||||||
|
reply_email="rep@sl.local",
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
EmailLog.create(user_id=user.id, contact_id=contact.id)
|
||||||
|
assert greylisting_needed_for_alias(alias)
|
||||||
|
|
||||||
|
|
||||||
|
def test_greylisting_needed_forward_phase_for_mailbox(flask_client):
|
||||||
|
user = User.create(
|
||||||
|
email="a@b.c", password="password", name="Test User", activated=True
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
alias = Alias.create_new_random(user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
contact = Contact.create(
|
||||||
|
user_id=user.id,
|
||||||
|
alias_id=alias.id,
|
||||||
|
website_email="contact@example.com",
|
||||||
|
reply_email="rep@sl.local",
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
EmailLog.create(user_id=user.id, contact_id=contact.id)
|
||||||
|
|
||||||
|
# Create another alias with the same mailbox
|
||||||
|
# will be greylisted as there's a previous activity on mailbox
|
||||||
|
alias = Alias.create_new_random(user)
|
||||||
|
db.session.commit()
|
||||||
|
assert greylisting_needed_for_mailbox(alias)
|
||||||
|
|
||||||
|
|
||||||
|
def test_greylisting_needed_forward_phase(flask_client):
|
||||||
|
# no greylisting when alias not exist
|
||||||
|
assert not greylisting_needed_forward_phase("not-exist@alias.com")
|
||||||
|
|
||||||
|
|
||||||
|
def test_greylisting_needed_reply_phase(flask_client):
|
||||||
|
# no greylisting when reply_email not exist
|
||||||
|
assert not greylisting_needed_reply_phase("not-exist-reply@alias.com")
|
||||||
|
|
||||||
|
user = User.create(
|
||||||
|
email="a@b.c", password="password", name="Test User", activated=True
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
alias = Alias.create_new_random(user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
contact = Contact.create(
|
||||||
|
user_id=user.id,
|
||||||
|
alias_id=alias.id,
|
||||||
|
website_email="contact@example.com",
|
||||||
|
reply_email="rep@sl.local",
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
EmailLog.create(user_id=user.id, contact_id=contact.id)
|
||||||
|
|
||||||
|
assert greylisting_needed_reply_phase("rep@sl.local")
|
Loading…
Reference in New Issue