commit
5fe48e4821
|
@ -1335,6 +1335,12 @@ class EmailLog(db.Model, ModelMixin):
|
||||||
db.ForeignKey("refused_email.id", ondelete="SET NULL"), nullable=True
|
db.ForeignKey("refused_email.id", ondelete="SET NULL"), nullable=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# in forward phase, this is the mailbox that will receive the email
|
||||||
|
# in reply phase, this is the mailbox (or a mailbox's authorized address) that sends the email
|
||||||
|
mailbox_id = db.Column(
|
||||||
|
db.ForeignKey("mailbox.id", ondelete="cascade"), nullable=True
|
||||||
|
)
|
||||||
|
|
||||||
# in case of bounce, record on what mailbox the email has been bounced
|
# in case of bounce, record on what mailbox the email has been bounced
|
||||||
# useful when an alias has several mailboxes
|
# useful when an alias has several mailboxes
|
||||||
bounced_mailbox_id = db.Column(
|
bounced_mailbox_id = db.Column(
|
||||||
|
@ -1345,6 +1351,7 @@ class EmailLog(db.Model, ModelMixin):
|
||||||
forward = db.relationship(Contact)
|
forward = db.relationship(Contact)
|
||||||
|
|
||||||
contact = db.relationship(Contact, backref="email_logs")
|
contact = db.relationship(Contact, backref="email_logs")
|
||||||
|
mailbox = db.relationship("Mailbox", lazy="joined", foreign_keys=[mailbox_id])
|
||||||
|
|
||||||
def bounced_mailbox(self) -> str:
|
def bounced_mailbox(self) -> str:
|
||||||
if self.bounced_mailbox_id:
|
if self.bounced_mailbox_id:
|
||||||
|
|
131
email_handler.py
131
email_handler.py
|
@ -126,7 +126,6 @@ from server import create_app, create_light_app
|
||||||
_DIRECTION = "X-SimpleLogin-Type"
|
_DIRECTION = "X-SimpleLogin-Type"
|
||||||
|
|
||||||
_IP_HEADER = "X-SimpleLogin-Client-IP"
|
_IP_HEADER = "X-SimpleLogin-Client-IP"
|
||||||
_MAILBOX_ID_HEADER = "X-SimpleLogin-Mailbox-ID"
|
|
||||||
_EMAIL_LOG_ID_HEADER = "X-SimpleLogin-EmailLog-ID"
|
_EMAIL_LOG_ID_HEADER = "X-SimpleLogin-EmailLog-ID"
|
||||||
_MESSAGE_ID = "Message-ID"
|
_MESSAGE_ID = "Message-ID"
|
||||||
_ENVELOPE_FROM = "X-SimpleLogin-Envelope-From"
|
_ENVELOPE_FROM = "X-SimpleLogin-Envelope-From"
|
||||||
|
@ -531,13 +530,11 @@ def handle_forward(envelope, msg: Message, rcpt_to: str) -> List[Tuple[bool, str
|
||||||
|
|
||||||
contact = get_or_create_contact(msg["From"], envelope.mail_from, alias)
|
contact = get_or_create_contact(msg["From"], envelope.mail_from, alias)
|
||||||
|
|
||||||
email_log = EmailLog.create(contact_id=contact.id, user_id=contact.user_id)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
if not alias.enabled:
|
if not alias.enabled:
|
||||||
LOG.d("%s is disabled, do not forward", alias)
|
LOG.d("%s is disabled, do not forward", alias)
|
||||||
email_log.blocked = True
|
EmailLog.create(
|
||||||
|
contact_id=contact.id, user_id=contact.user_id, blocked=True, commit=True
|
||||||
|
)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
# do not return 5** to allow user to receive emails later when alias is enabled
|
# do not return 5** to allow user to receive emails later when alias is enabled
|
||||||
return [(True, "250 Message accepted for delivery")]
|
return [(True, "250 Message accepted for delivery")]
|
||||||
|
@ -560,7 +557,6 @@ def handle_forward(envelope, msg: Message, rcpt_to: str) -> List[Tuple[bool, str
|
||||||
forward_email_to_mailbox(
|
forward_email_to_mailbox(
|
||||||
alias,
|
alias,
|
||||||
copy(msg),
|
copy(msg),
|
||||||
email_log,
|
|
||||||
contact,
|
contact,
|
||||||
envelope,
|
envelope,
|
||||||
mailbox,
|
mailbox,
|
||||||
|
@ -574,7 +570,6 @@ def handle_forward(envelope, msg: Message, rcpt_to: str) -> List[Tuple[bool, str
|
||||||
def forward_email_to_mailbox(
|
def forward_email_to_mailbox(
|
||||||
alias,
|
alias,
|
||||||
msg: Message,
|
msg: Message,
|
||||||
email_log: EmailLog,
|
|
||||||
contact: Contact,
|
contact: Contact,
|
||||||
envelope,
|
envelope,
|
||||||
mailbox,
|
mailbox,
|
||||||
|
@ -588,7 +583,7 @@ def forward_email_to_mailbox(
|
||||||
|
|
||||||
# sanity check: make sure mailbox is not actually an alias
|
# sanity check: make sure mailbox is not actually an alias
|
||||||
if get_email_domain_part(alias.email) == get_email_domain_part(mailbox.email):
|
if get_email_domain_part(alias.email) == get_email_domain_part(mailbox.email):
|
||||||
LOG.warning(
|
LOG.exception(
|
||||||
"Mailbox has the same domain as alias. %s -> %s -> %s",
|
"Mailbox has the same domain as alias. %s -> %s -> %s",
|
||||||
contact,
|
contact,
|
||||||
alias,
|
alias,
|
||||||
|
@ -619,6 +614,10 @@ def forward_email_to_mailbox(
|
||||||
# so when user fixes the mailbox, the email can be delivered
|
# so when user fixes the mailbox, the email can be delivered
|
||||||
return False, "421 SL E14"
|
return False, "421 SL E14"
|
||||||
|
|
||||||
|
email_log = EmailLog.create(
|
||||||
|
contact_id=contact.id, user_id=user.id, mailbox_id=mailbox.id, commit=True
|
||||||
|
)
|
||||||
|
|
||||||
# Spam check
|
# Spam check
|
||||||
spam_status = ""
|
spam_status = ""
|
||||||
is_spam = False
|
is_spam = False
|
||||||
|
@ -694,7 +693,6 @@ def forward_email_to_mailbox(
|
||||||
delete_header(msg, "Sender")
|
delete_header(msg, "Sender")
|
||||||
|
|
||||||
delete_header(msg, _IP_HEADER)
|
delete_header(msg, _IP_HEADER)
|
||||||
add_or_replace_header(msg, _MAILBOX_ID_HEADER, str(mailbox.id))
|
|
||||||
add_or_replace_header(msg, _EMAIL_LOG_ID_HEADER, str(email_log.id))
|
add_or_replace_header(msg, _EMAIL_LOG_ID_HEADER, str(email_log.id))
|
||||||
add_or_replace_header(msg, _MESSAGE_ID, make_msgid(str(email_log.id), EMAIL_DOMAIN))
|
add_or_replace_header(msg, _MESSAGE_ID, make_msgid(str(email_log.id), EMAIL_DOMAIN))
|
||||||
add_or_replace_header(msg, _ENVELOPE_FROM, envelope.mail_from)
|
add_or_replace_header(msg, _ENVELOPE_FROM, envelope.mail_from)
|
||||||
|
@ -847,7 +845,10 @@ def handle_reply(envelope, msg: Message, rcpt_to: str) -> (bool, str):
|
||||||
return True, "250 SL E11"
|
return True, "250 SL E11"
|
||||||
|
|
||||||
email_log = EmailLog.create(
|
email_log = EmailLog.create(
|
||||||
contact_id=contact.id, is_reply=True, user_id=contact.user_id
|
contact_id=contact.id,
|
||||||
|
is_reply=True,
|
||||||
|
user_id=contact.user_id,
|
||||||
|
mailbox_id=mailbox.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Spam check
|
# Spam check
|
||||||
|
@ -956,7 +957,6 @@ def handle_reply(envelope, msg: Message, rcpt_to: str) -> (bool, str):
|
||||||
msg["Date"] = date_header
|
msg["Date"] = date_header
|
||||||
|
|
||||||
msg[_DIRECTION] = "Reply"
|
msg[_DIRECTION] = "Reply"
|
||||||
msg[_MAILBOX_ID_HEADER] = str(mailbox.id)
|
|
||||||
msg[_EMAIL_LOG_ID_HEADER] = str(email_log.id)
|
msg[_EMAIL_LOG_ID_HEADER] = str(email_log.id)
|
||||||
|
|
||||||
LOG.d(
|
LOG.d(
|
||||||
|
@ -1150,6 +1150,10 @@ def handle_unknown_mailbox(
|
||||||
|
|
||||||
|
|
||||||
def handle_bounce(contact: Contact, alias: Alias, msg: Message, user: User):
|
def handle_bounce(contact: Contact, alias: Alias, msg: Message, user: User):
|
||||||
|
"""
|
||||||
|
Handle bounce that is sent to the reverse-alias
|
||||||
|
Happens when an email cannot be to a mailbox
|
||||||
|
"""
|
||||||
disable_alias_link = f"{URL}/dashboard/unsubscribe/{alias.id}"
|
disable_alias_link = f"{URL}/dashboard/unsubscribe/{alias.id}"
|
||||||
|
|
||||||
# Store the bounced email
|
# Store the bounced email
|
||||||
|
@ -1160,8 +1164,7 @@ def handle_bounce(contact: Contact, alias: Alias, msg: Message, user: User):
|
||||||
s3.upload_email_from_bytesio(full_report_path, BytesIO(to_bytes(msg)), random_name)
|
s3.upload_email_from_bytesio(full_report_path, BytesIO(to_bytes(msg)), random_name)
|
||||||
|
|
||||||
file_path = None
|
file_path = None
|
||||||
mailbox = None
|
|
||||||
email_log: EmailLog = None
|
|
||||||
orig_msg = get_orig_message_from_bounce(msg)
|
orig_msg = get_orig_message_from_bounce(msg)
|
||||||
if not orig_msg:
|
if not orig_msg:
|
||||||
# Some MTA does not return the original message in bounce message
|
# Some MTA does not return the original message in bounce message
|
||||||
|
@ -1176,37 +1179,6 @@ def handle_bounce(contact: Contact, alias: Alias, msg: Message, user: User):
|
||||||
else:
|
else:
|
||||||
file_path = f"refused-emails/{random_name}.eml"
|
file_path = f"refused-emails/{random_name}.eml"
|
||||||
s3.upload_email_from_bytesio(file_path, BytesIO(to_bytes(msg)), random_name)
|
s3.upload_email_from_bytesio(file_path, BytesIO(to_bytes(msg)), random_name)
|
||||||
try:
|
|
||||||
mailbox_id = int(orig_msg[_MAILBOX_ID_HEADER])
|
|
||||||
except TypeError:
|
|
||||||
LOG.warning(
|
|
||||||
"cannot parse mailbox from original message header %s",
|
|
||||||
orig_msg[_MAILBOX_ID_HEADER],
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
mailbox = Mailbox.get(mailbox_id)
|
|
||||||
if not mailbox or mailbox.user_id != user.id:
|
|
||||||
LOG.exception(
|
|
||||||
"Tampered message mailbox_id %s, %s, %s, %s %s",
|
|
||||||
mailbox_id,
|
|
||||||
user,
|
|
||||||
alias,
|
|
||||||
contact,
|
|
||||||
full_report_path,
|
|
||||||
)
|
|
||||||
# cannot use this tampered mailbox, reset it
|
|
||||||
mailbox = None
|
|
||||||
|
|
||||||
# try to get the original email_log
|
|
||||||
try:
|
|
||||||
email_log_id = int(orig_msg[_EMAIL_LOG_ID_HEADER])
|
|
||||||
except TypeError:
|
|
||||||
LOG.warning(
|
|
||||||
"cannot parse email log from original message header %s",
|
|
||||||
orig_msg[_EMAIL_LOG_ID_HEADER],
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
email_log = EmailLog.get(email_log_id)
|
|
||||||
|
|
||||||
refused_email = RefusedEmail.create(
|
refused_email = RefusedEmail.create(
|
||||||
path=file_path, full_report_path=full_report_path, user_id=user.id
|
path=file_path, full_report_path=full_report_path, user_id=user.id
|
||||||
|
@ -1214,46 +1186,32 @@ def handle_bounce(contact: Contact, alias: Alias, msg: Message, user: User):
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
LOG.d("Create refused email %s", refused_email)
|
LOG.d("Create refused email %s", refused_email)
|
||||||
|
|
||||||
if not mailbox:
|
# try to parse email_log
|
||||||
LOG.debug("Try to get mailbox from bounce report")
|
email_log = None
|
||||||
try:
|
try:
|
||||||
mailbox_id = int(get_header_from_bounce(msg, _MAILBOX_ID_HEADER))
|
email_log_id = int(get_header_from_bounce(msg, _EMAIL_LOG_ID_HEADER))
|
||||||
except Exception:
|
except Exception:
|
||||||
LOG.warning("cannot get mailbox-id from bounce report, %s", refused_email)
|
LOG.warning("cannot get email log id from bounce report, %s", refused_email)
|
||||||
else:
|
else:
|
||||||
mailbox = Mailbox.get(mailbox_id)
|
email_log = EmailLog.get(email_log_id)
|
||||||
if not mailbox or mailbox.user_id != user.id:
|
|
||||||
LOG.exception(
|
|
||||||
"Tampered message mailbox_id %s, %s, %s, %s %s",
|
|
||||||
mailbox_id,
|
|
||||||
user,
|
|
||||||
alias,
|
|
||||||
contact,
|
|
||||||
full_report_path,
|
|
||||||
)
|
|
||||||
mailbox = None
|
|
||||||
|
|
||||||
if not email_log:
|
# create new email_log if unable to parse from bounce report
|
||||||
LOG.d("Try to get email log from bounce report")
|
|
||||||
try:
|
|
||||||
email_log_id = int(get_header_from_bounce(msg, _EMAIL_LOG_ID_HEADER))
|
|
||||||
except Exception:
|
|
||||||
LOG.warning("cannot get email log id from bounce report, %s", refused_email)
|
|
||||||
else:
|
|
||||||
email_log = EmailLog.get(email_log_id)
|
|
||||||
|
|
||||||
# use the default mailbox as the last option
|
|
||||||
if not mailbox:
|
|
||||||
LOG.warning("Use %s default mailbox %s", alias, refused_email)
|
|
||||||
mailbox = alias.mailbox
|
|
||||||
|
|
||||||
# create a new email log as the last option
|
|
||||||
if not email_log:
|
if not email_log:
|
||||||
LOG.warning("cannot get the original email_log, create a new one")
|
LOG.warning("cannot get the original email_log, create a new one")
|
||||||
email_log: EmailLog = EmailLog.create(
|
email_log: EmailLog = EmailLog.create(
|
||||||
contact_id=contact.id, user_id=contact.user_id
|
contact_id=contact.id, user_id=contact.user_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# try to get mailbox
|
||||||
|
mailbox = None
|
||||||
|
if email_log:
|
||||||
|
mailbox = email_log.mailbox
|
||||||
|
|
||||||
|
# use the default mailbox if unable to parse from bounce report
|
||||||
|
if not mailbox:
|
||||||
|
LOG.warning("Use %s default mailbox %s", alias, refused_email)
|
||||||
|
mailbox = alias.mailbox
|
||||||
|
|
||||||
email_log.bounced = True
|
email_log.bounced = True
|
||||||
email_log.refused_email_id = refused_email.id
|
email_log.refused_email_id = refused_email.id
|
||||||
email_log.bounced_mailbox_id = mailbox.id
|
email_log.bounced_mailbox_id = mailbox.id
|
||||||
|
@ -1379,20 +1337,11 @@ def handle_bounce_reply_phase(alias: Alias, msg: Message, user: User):
|
||||||
|
|
||||||
email_log.bounced = True
|
email_log.bounced = True
|
||||||
email_log.refused_email_id = refused_email.id
|
email_log.refused_email_id = refused_email.id
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
try:
|
mailbox = email_log.mailbox or alias.mailbox
|
||||||
mailbox_id = int(get_header_from_bounce(msg, _MAILBOX_ID_HEADER))
|
email_log.bounced_mailbox_id = mailbox.id
|
||||||
except Exception:
|
|
||||||
LOG.warning(
|
db.session.commit()
|
||||||
"cannot parse mailbox from bounce message report %s %s", alias, user
|
|
||||||
)
|
|
||||||
# fall back to the default mailbox
|
|
||||||
mailbox = alias.mailbox
|
|
||||||
else:
|
|
||||||
mailbox = Mailbox.get(mailbox_id)
|
|
||||||
email_log.bounced_mailbox_id = mailbox.id
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
refused_email_url = (
|
refused_email_url = (
|
||||||
URL + f"/dashboard/refused_email?highlight_id=" + str(email_log.id)
|
URL + f"/dashboard/refused_email?highlight_id=" + str(email_log.id)
|
||||||
|
|
31
migrations/versions/2020_112416_623662ea0e7e_.py
Normal file
31
migrations/versions/2020_112416_623662ea0e7e_.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 623662ea0e7e
|
||||||
|
Revises: d1edb3cadec8
|
||||||
|
Create Date: 2020-11-24 16:34:02.327556
|
||||||
|
|
||||||
|
"""
|
||||||
|
import sqlalchemy_utils
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '623662ea0e7e'
|
||||||
|
down_revision = 'd1edb3cadec8'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('email_log', sa.Column('mailbox_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key(None, 'email_log', 'mailbox', ['mailbox_id'], ['id'], ondelete='cascade')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(None, 'email_log', type_='foreignkey')
|
||||||
|
op.drop_column('email_log', 'mailbox_id')
|
||||||
|
# ### end Alembic commands ###
|
Loading…
Reference in a new issue