2019-11-07 17:49:26 +01:00
"""
2019-11-08 11:05:34 +01:00
Handle the email * forward * and * reply * . phase . There are 3 actors :
2020-03-26 11:19:20 +01:00
- contact : who sends emails to alias @sl.co address
2019-11-07 17:49:26 +01:00
- SL email handler ( this script )
2020-03-26 11:19:20 +01:00
- user personal email : to be protected . Should never leak to contact .
2019-11-07 17:49:26 +01:00
This script makes sure that in the forward phase , the email that is forwarded to user personal email has the following
envelope and header fields :
Envelope :
2020-03-26 11:19:20 +01:00
mail from : @contact
2019-11-08 11:05:34 +01:00
rcpt to : @personal_email
2019-11-07 17:49:26 +01:00
Header :
2020-03-26 11:19:20 +01:00
From : @contact
2019-11-08 11:05:34 +01:00
To : alias @sl.co # so user knows this email is sent to alias
Reply - to : special @sl.co # magic HERE
2019-11-07 17:49:26 +01:00
And in the reply phase :
Envelope :
2020-03-26 11:19:20 +01:00
mail from : @contact
rcpt to : @contact
2019-11-07 17:49:26 +01:00
Header :
2020-03-26 11:19:20 +01:00
From : alias @sl.co # so for contact the email comes from alias. magic HERE
To : @contact
2019-11-07 17:49:26 +01:00
The special @sl.co allows to hide user personal email when user clicks " Reply " to the forwarded email .
It should contain the following info :
- alias
2020-03-26 11:19:20 +01:00
- @contact
2019-11-07 17:49:26 +01:00
"""
2020-09-02 17:36:11 +02:00
import argparse
2020-04-02 18:10:08 +02:00
import email
2019-11-07 17:49:26 +01:00
import time
2020-03-15 22:29:53 +01:00
import uuid
2020-03-08 23:07:23 +01:00
from email import encoders
2020-11-02 19:09:57 +01:00
from email . encoders import encode_noop
2020-01-04 10:25:19 +01:00
from email . message import Message
2020-03-08 23:07:23 +01:00
from email . mime . application import MIMEApplication
from email . mime . multipart import MIMEMultipart
2022-09-05 08:40:24 +02:00
from email . utils import make_msgid , formatdate , getaddresses
2020-03-14 16:34:23 +01:00
from io import BytesIO
2021-12-23 19:34:17 +01:00
from smtplib import SMTPRecipientsRefused , SMTPServerDisconnected
2020-09-28 17:41:16 +02:00
from typing import List , Tuple , Optional
2019-11-07 17:49:26 +01:00
2021-07-23 15:48:43 +02:00
import newrelic . agent
2020-09-30 11:05:21 +02:00
from aiosmtpd . controller import Controller
2020-06-07 11:41:35 +02:00
from aiosmtpd . smtp import Envelope
2021-09-10 16:51:36 +02:00
from email_validator import validate_email , EmailNotValidError
from flanker . addresslib import address
from flanker . addresslib . address import EmailAddress
2020-09-03 19:42:52 +02:00
from sqlalchemy . exc import IntegrityError
2020-06-07 11:41:35 +02:00
2022-04-21 08:59:46 +02:00
from app import pgp_utils , s3 , config
2020-04-04 15:24:27 +02:00
from app . alias_utils import try_auto_create
2020-02-15 11:04:22 +01:00
from app . config import (
EMAIL_DOMAIN ,
URL ,
2020-03-28 23:19:25 +01:00
UNSUBSCRIBER ,
2020-04-14 12:45:47 +02:00
LOAD_PGP_EMAIL_HANDLER ,
2020-05-07 13:28:04 +02:00
ENFORCE_SPF ,
2020-05-09 20:45:04 +02:00
ALERT_REVERSE_ALIAS_UNKNOWN_MAILBOX ,
ALERT_BOUNCE_EMAIL ,
ALERT_SPAM_EMAIL ,
2020-08-15 16:38:16 +02:00
SPAMASSASSIN_HOST ,
MAX_SPAM_SCORE ,
2020-08-15 16:53:57 +02:00
MAX_REPLY_PHASE_SPAM_SCORE ,
2020-08-25 12:51:05 +02:00
ALERT_SEND_EMAIL_CYCLE ,
2020-08-30 19:06:50 +02:00
ALERT_MAILBOX_IS_ALIAS ,
2020-11-02 19:09:57 +01:00
PGP_SENDER_PRIVATE_KEY ,
2020-11-04 12:32:15 +01:00
ALERT_BOUNCE_EMAIL_REPLY_PHASE ,
2020-11-14 15:55:53 +01:00
NOREPLY ,
2021-01-11 14:55:55 +01:00
BOUNCE_PREFIX ,
BOUNCE_SUFFIX ,
2021-01-26 09:59:08 +01:00
TRANSACTIONAL_BOUNCE_PREFIX ,
TRANSACTIONAL_BOUNCE_SUFFIX ,
2021-03-26 10:00:48 +01:00
ENABLE_SPAM_ASSASSIN ,
2021-05-25 17:58:45 +02:00
BOUNCE_PREFIX_FOR_REPLY_PHASE ,
2021-09-06 19:44:18 +02:00
POSTMASTER ,
2021-11-22 18:17:07 +01:00
OLD_UNSUBSCRIBER ,
2022-01-08 00:42:03 +01:00
ALERT_FROM_ADDRESS_IS_REVERSE_ALIAS ,
2022-02-16 18:38:31 +01:00
ALERT_TO_NOREPLY ,
2020-02-15 11:04:22 +01:00
)
2021-10-12 14:36:47 +02:00
from app . db import Session
2021-10-11 12:00:37 +02:00
from app . email import status , headers
2021-07-23 15:48:43 +02:00
from app . email . rate_limit import rate_limited
2021-03-17 10:07:13 +01:00
from app . email . spam import get_spam_score
2019-12-17 17:48:06 +01:00
from app . email_utils import (
send_email ,
2021-09-25 18:47:15 +02:00
add_dkim_signature ,
2020-01-07 19:50:36 +01:00
add_or_replace_header ,
delete_header ,
2020-02-11 16:46:53 +01:00
render ,
2020-03-14 16:34:23 +01:00
get_orig_message_from_bounce ,
2020-03-14 22:24:02 +01:00
delete_all_headers_except ,
2020-03-30 22:05:31 +02:00
get_spam_info ,
get_orig_message_from_spamassassin_report ,
2020-05-09 20:45:04 +02:00
send_email_with_rate_control ,
2020-06-09 17:16:32 +02:00
get_email_domain_part ,
2020-08-21 12:03:23 +02:00
copy ,
2020-08-27 10:43:48 +02:00
send_email_at_most_times ,
2020-10-15 16:21:31 +02:00
is_valid_alias_address_domain ,
2021-09-25 18:47:15 +02:00
should_add_dkim_signature ,
2020-11-07 13:00:45 +01:00
add_header ,
2020-11-09 21:16:50 +01:00
get_header_unicode ,
2020-11-16 19:15:09 +01:00
generate_reply_email ,
2021-10-19 12:14:16 +02:00
is_reverse_alias ,
2020-11-22 13:07:09 +01:00
normalize_reply_email ,
2020-11-25 15:20:42 +01:00
is_valid_email ,
2020-11-30 15:15:44 +01:00
replace ,
2020-12-16 18:48:10 +01:00
should_disable ,
2021-01-26 09:46:47 +01:00
parse_id_from_bounce ,
2021-03-17 10:09:02 +01:00
spf_pass ,
2021-03-17 10:59:13 +01:00
sanitize_header ,
2021-06-04 17:15:59 +02:00
get_queue_id ,
2021-08-02 11:33:58 +02:00
should_ignore_bounce ,
2021-09-10 17:31:29 +02:00
parse_full_address ,
2021-10-14 15:46:52 +02:00
get_mailbox_bounce_info ,
2021-11-02 14:32:16 +01:00
save_email_for_debugging ,
2022-04-06 17:31:46 +02:00
save_envelope_for_debugging ,
2022-03-25 18:14:31 +01:00
get_verp_info_from_email ,
2022-04-14 18:25:03 +02:00
generate_verp_email ,
2022-09-05 08:40:24 +02:00
sl_formataddr ,
2020-01-07 19:50:36 +01:00
)
2022-01-07 14:57:47 +01:00
from app . errors import (
NonReverseAliasInReplyPhase ,
VERPTransactional ,
VERPForward ,
VERPReply ,
2022-01-08 00:16:16 +01:00
CannotCreateContactForReverseAlias ,
2022-01-07 14:57:47 +01:00
)
2022-04-21 09:26:44 +02:00
from app . handler . dmarc import (
apply_dmarc_policy_for_reply_phase ,
apply_dmarc_policy_for_forward_phase ,
)
2022-04-25 14:40:42 +02:00
from app . handler . provider_complaint import (
2022-04-14 18:46:11 +02:00
handle_hotmail_complaint ,
handle_yahoo_complaint ,
)
2022-09-05 08:40:24 +02:00
from app . handler . spamd_result import (
SpamdResult ,
SPFCheckResult ,
)
2022-06-30 11:40:01 +02:00
from app . handler . unsubscribe_generator import UnsubscribeGenerator
from app . handler . unsubscribe_handler import UnsubscribeHandler
2021-03-15 19:41:42 +01:00
from app . log import LOG , set_message_id
2022-04-28 14:43:24 +02:00
from app . mail_sender import sl_sendmail
from app . message_utils import message_to_bytes
2020-01-30 08:43:31 +01:00
from app . models import (
2020-03-17 11:51:40 +01:00
Alias ,
2020-03-17 10:56:59 +01:00
Contact ,
2022-02-21 12:52:21 +01:00
BlockBehaviourEnum ,
2020-03-17 11:10:50 +01:00
EmailLog ,
2020-01-30 08:43:31 +01:00
User ,
2020-03-14 16:34:23 +01:00
RefusedEmail ,
2020-05-07 13:28:04 +02:00
Mailbox ,
2021-01-26 09:56:13 +01:00
Bounce ,
TransactionalEmail ,
2021-06-22 17:52:24 +02:00
IgnoredEmail ,
2021-10-18 17:34:11 +02:00
MessageIDMatching ,
2022-01-24 16:10:36 +01:00
Notification ,
2022-03-30 16:09:17 +02:00
VerpType ,
2020-01-30 08:43:31 +01:00
)
2022-09-01 15:10:11 +02:00
from app . pgp_utils import (
PGPException ,
sign_data_with_pgpy ,
sign_data ,
load_public_key_and_check ,
)
2023-01-25 13:17:20 +01:00
from app . utils import sanitize_email , canonicalize_email
2020-04-14 12:45:47 +02:00
from init_app import load_pgp_public_keys
2021-10-12 15:03:16 +02:00
from server import create_light_app
2019-11-07 17:49:26 +01:00
2020-11-02 19:09:57 +01:00
2022-07-12 18:17:39 +02:00
def get_or_create_contact ( from_header : str , mail_from : str , alias : Alias ) - > Contact :
2020-02-19 16:49:40 +01:00
"""
2020-04-05 15:24:09 +02:00
contact_from_header is the RFC 2047 format FROM header
2020-02-19 16:49:40 +01:00
"""
2021-09-10 17:31:29 +02:00
try :
contact_name , contact_email = parse_full_address ( from_header )
except ValueError :
contact_name , contact_email = " " , " "
2023-02-10 10:07:43 +01:00
# Ensure contact_name is within limits
if len ( contact_name ) > = Contact . MAX_NAME_LENGTH :
contact_name = contact_name [ 0 : Contact . MAX_NAME_LENGTH ]
2020-11-25 17:50:25 +01:00
if not is_valid_email ( contact_email ) :
2020-05-15 15:46:37 +02:00
# From header is wrongly formatted, try with mail_from
2020-11-14 15:55:53 +01:00
if mail_from and mail_from != " <> " :
2021-03-16 09:17:23 +01:00
LOG . w (
2021-09-10 17:06:38 +02:00
" Cannot parse email from from_header %s , use mail_from %s " ,
2020-12-05 18:15:53 +01:00
from_header ,
2020-11-25 15:20:00 +01:00
mail_from ,
)
2021-09-10 17:06:38 +02:00
contact_email = mail_from
2020-05-13 22:35:27 +02:00
2020-11-25 17:50:25 +01:00
if not is_valid_email ( contact_email ) :
2021-03-16 09:17:23 +01:00
LOG . w (
2020-11-25 15:20:42 +01:00
" invalid contact email %s . Parse from %s %s " ,
contact_email ,
2020-12-05 18:15:53 +01:00
from_header ,
2020-11-25 15:20:42 +01:00
mail_from ,
)
# either reuse a contact with empty email or create a new contact with empty email
contact_email = " "
2021-11-18 16:44:04 +01:00
contact_email = sanitize_email ( contact_email , not_lower = True )
2021-01-11 12:27:02 +01:00
2021-09-27 10:21:49 +02:00
if contact_name and " \x00 " in contact_name :
LOG . w ( " issue with contact name %s " , contact_name )
contact_name = " "
2020-04-04 20:06:35 +02:00
contact = Contact . get_by ( alias_id = alias . id , website_email = contact_email )
2020-03-17 10:56:59 +01:00
if contact :
2020-04-05 15:24:09 +02:00
if contact . name != contact_name :
LOG . d (
2020-08-27 10:20:48 +02:00
" Update contact %s name %s to %s " ,
contact ,
contact . name ,
contact_name ,
2020-04-05 15:24:09 +02:00
)
contact . name = contact_name
2021-10-12 14:36:47 +02:00
Session . commit ( )
2020-10-27 10:40:44 +01:00
2020-11-25 15:20:00 +01:00
# contact created in the past does not have mail_from and from_header field
if not contact . mail_from and mail_from :
2020-10-27 10:40:44 +01:00
LOG . d (
2020-11-25 15:20:00 +01:00
" Set contact mail_from %s : %s to %s " ,
2020-10-27 10:40:44 +01:00
contact ,
contact . mail_from ,
mail_from ,
)
contact . mail_from = mail_from
2021-10-12 14:36:47 +02:00
Session . commit ( )
2020-02-19 16:17:13 +01:00
else :
2020-02-19 15:50:38 +01:00
2020-09-03 19:42:52 +02:00
try :
contact = Contact . create (
user_id = alias . user_id ,
alias_id = alias . id ,
website_email = contact_email ,
name = contact_name ,
2020-09-14 18:22:26 +02:00
mail_from = mail_from ,
2020-12-06 19:37:20 +01:00
reply_email = generate_reply_email ( contact_email , alias . user )
2020-11-25 17:50:25 +01:00
if is_valid_email ( contact_email )
2020-11-18 11:48:09 +01:00
else NOREPLY ,
2022-01-07 10:22:02 +01:00
automatic_created = True ,
2020-09-03 19:42:52 +02:00
)
2020-11-18 11:48:09 +01:00
if not contact_email :
2020-11-14 15:55:53 +01:00
LOG . d ( " Create a contact with invalid email for %s " , alias )
contact . invalid_email = True
2022-01-07 10:04:12 +01:00
LOG . d (
" create contact %s for %s , reverse alias: %s " ,
contact_email ,
alias ,
contact . reply_email ,
)
2021-10-12 14:36:47 +02:00
Session . commit ( )
2020-09-03 19:42:52 +02:00
except IntegrityError :
2021-03-16 09:17:23 +01:00
LOG . w ( " Contact %s %s already exist " , alias , contact_email )
2021-10-12 14:36:47 +02:00
Session . rollback ( )
2020-09-03 19:42:52 +02:00
contact = Contact . get_by ( alias_id = alias . id , website_email = contact_email )
2020-02-19 15:50:38 +01:00
2020-03-17 10:56:59 +01:00
return contact
2020-02-19 16:49:40 +01:00
2021-07-28 09:12:52 +02:00
def get_or_create_reply_to_contact (
2022-01-07 13:02:16 +01:00
reply_to_header : str , alias : Alias , msg : Message
2021-07-28 09:12:52 +02:00
) - > Optional [ Contact ] :
"""
Get or create the contact for the Reply - To header
"""
2021-09-10 17:31:29 +02:00
try :
contact_name , contact_address = parse_full_address ( reply_to_header )
except ValueError :
return
2021-07-28 09:12:52 +02:00
2021-09-10 17:31:29 +02:00
if not is_valid_email ( contact_address ) :
2021-07-28 09:12:52 +02:00
LOG . w (
" invalid reply-to address %s . Parse from %s " ,
2021-09-10 17:31:29 +02:00
contact_address ,
2021-07-28 09:12:52 +02:00
reply_to_header ,
)
return None
2021-09-10 17:31:29 +02:00
contact = Contact . get_by ( alias_id = alias . id , website_email = contact_address )
2021-07-28 09:12:52 +02:00
if contact :
return contact
else :
LOG . d (
2022-04-11 14:51:33 +02:00
" create contact %s for alias %s via reply-to header %s " ,
2021-09-10 17:31:29 +02:00
contact_address ,
2021-07-28 09:12:52 +02:00
alias ,
reply_to_header ,
)
try :
contact = Contact . create (
user_id = alias . user_id ,
alias_id = alias . id ,
2021-09-10 17:31:29 +02:00
website_email = contact_address ,
name = contact_name ,
reply_email = generate_reply_email ( contact_address , alias . user ) ,
2022-01-07 10:22:02 +01:00
automatic_created = True ,
2021-07-28 09:12:52 +02:00
)
2021-10-12 14:36:47 +02:00
Session . commit ( )
2021-07-28 09:12:52 +02:00
except IntegrityError :
2021-09-10 17:31:29 +02:00
LOG . w ( " Contact %s %s already exist " , alias , contact_address )
2021-10-12 14:36:47 +02:00
Session . rollback ( )
2021-09-10 17:31:29 +02:00
contact = Contact . get_by ( alias_id = alias . id , website_email = contact_address )
2021-07-28 09:12:52 +02:00
return contact
2020-03-28 19:16:55 +01:00
def replace_header_when_forward ( msg : Message , alias : Alias , header : str ) :
"""
Replace CC or To header by Reply emails in forward phase
"""
new_addrs : [ str ] = [ ]
2020-11-26 09:49:03 +01:00
headers = msg . get_all ( header , [ ] )
2021-01-15 11:30:43 +01:00
# headers can be an array of Header, convert it to string here
2021-09-10 16:51:36 +02:00
headers = [ get_header_unicode ( h ) for h in headers ]
2021-01-11 12:27:02 +01:00
2021-09-10 16:51:36 +02:00
full_addresses : [ EmailAddress ] = [ ]
for h in headers :
full_addresses + = address . parse_list ( h )
for full_address in full_addresses :
2021-11-18 16:44:04 +01:00
contact_email = sanitize_email ( full_address . address , not_lower = True )
2020-03-28 19:16:55 +01:00
# no transformation when alias is already in the header
2021-11-18 16:44:04 +01:00
if contact_email . lower ( ) == alias . email :
2021-09-10 16:51:36 +02:00
new_addrs . append ( full_address . full_spec ( ) )
2020-03-28 19:16:55 +01:00
continue
2021-09-10 16:51:36 +02:00
try :
# NOT allow unicode for contact address
2021-09-10 17:06:38 +02:00
validate_email (
contact_email , check_deliverability = False , allow_smtputf8 = False
)
2021-09-10 16:51:36 +02:00
except EmailNotValidError :
2021-03-16 09:17:23 +01:00
LOG . w ( " invalid contact email %s . %s . Skip " , contact_email , headers )
2020-11-25 15:21:01 +01:00
continue
2020-04-05 12:59:36 +02:00
contact = Contact . get_by ( alias_id = alias . id , website_email = contact_email )
2020-03-28 19:16:55 +01:00
if contact :
2020-04-05 15:39:48 +02:00
# update the contact name if needed
2021-09-10 16:51:36 +02:00
if contact . name != full_address . display_name :
2020-04-05 14:50:12 +02:00
LOG . d (
" Update contact %s name %s to %s " ,
contact ,
contact . name ,
2021-09-10 16:51:36 +02:00
full_address . display_name ,
2020-04-05 14:50:12 +02:00
)
2021-09-10 16:51:36 +02:00
contact . name = full_address . display_name
2021-10-12 14:36:47 +02:00
Session . commit ( )
2020-03-28 19:16:55 +01:00
else :
2021-09-08 11:29:55 +02:00
LOG . d (
2020-03-28 19:16:55 +01:00
" create contact for alias %s and email %s , header %s " ,
alias ,
2020-04-05 12:59:36 +02:00
contact_email ,
2020-03-28 19:16:55 +01:00
header ,
)
2020-09-09 17:00:07 +02:00
try :
contact = Contact . create (
user_id = alias . user_id ,
alias_id = alias . id ,
website_email = contact_email ,
2021-09-10 16:51:36 +02:00
name = full_address . display_name ,
2020-12-06 19:37:20 +01:00
reply_email = generate_reply_email ( contact_email , alias . user ) ,
2020-09-09 17:00:07 +02:00
is_cc = header . lower ( ) == " cc " ,
2022-01-07 10:22:02 +01:00
automatic_created = True ,
2020-09-09 17:00:07 +02:00
)
2021-10-12 14:36:47 +02:00
Session . commit ( )
2020-09-09 17:00:07 +02:00
except IntegrityError :
2021-03-16 09:17:23 +01:00
LOG . w ( " Contact %s %s already exist " , alias , contact_email )
2021-10-12 14:36:47 +02:00
Session . rollback ( )
2020-09-09 17:00:07 +02:00
contact = Contact . get_by ( alias_id = alias . id , website_email = contact_email )
2020-03-28 19:16:55 +01:00
2020-04-05 15:24:09 +02:00
new_addrs . append ( contact . new_addr ( ) )
2020-03-28 19:16:55 +01:00
2020-08-25 12:48:28 +02:00
if new_addrs :
2020-03-28 19:16:55 +01:00
new_header = " , " . join ( new_addrs )
LOG . d ( " Replace %s header, old: %s , new: %s " , header , msg [ header ] , new_header )
add_or_replace_header ( msg , header , new_header )
else :
2020-08-25 12:48:28 +02:00
LOG . d ( " Delete %s header, old value %s " , header , msg [ header ] )
delete_header ( msg , header )
2020-03-28 19:16:55 +01:00
2022-04-15 10:16:03 +02:00
def add_alias_to_header_if_needed ( msg , alias ) :
"""
During the forward phase , add alias to To : header if it isn ' t included in To and Cc header
It can happen that the alias isn ' t included in To: and CC: header, for example if this is a BCC email
: return :
"""
to_header = str ( msg [ headers . TO ] ) if msg [ headers . TO ] else None
cc_header = str ( msg [ headers . CC ] ) if msg [ headers . CC ] else None
# nothing to do
if to_header and alias . email in to_header :
return
# nothing to do
if cc_header and alias . email in cc_header :
return
LOG . d ( f " add { alias } to To: header { to_header } " )
if to_header :
2022-04-19 18:45:59 +02:00
add_or_replace_header ( msg , headers . TO , f " { to_header } , { alias . email } " )
2022-04-15 10:16:03 +02:00
else :
2022-04-19 18:45:59 +02:00
add_or_replace_header ( msg , headers . TO , alias . email )
2022-04-15 10:16:03 +02:00
2020-03-28 19:16:55 +01:00
def replace_header_when_reply ( msg : Message , alias : Alias , header : str ) :
"""
Replace CC or To Reply emails by original emails
"""
new_addrs : [ str ] = [ ]
2021-01-15 11:30:43 +01:00
headers = msg . get_all ( header , [ ] )
# headers can be an array of Header, convert it to string here
headers = [ str ( h ) for h in headers ]
2020-03-28 19:16:55 +01:00
2022-01-11 13:11:28 +01:00
# headers can contain \r or \n
headers = [ h . replace ( " \r " , " " ) for h in headers ]
headers = [ h . replace ( " \n " , " " ) for h in headers ]
2021-01-15 11:30:43 +01:00
for _ , reply_email in getaddresses ( headers ) :
2020-03-28 19:16:55 +01:00
# no transformation when alias is already in the header
2022-01-07 10:34:08 +01:00
# can happen when user clicks "Reply All"
2020-04-05 15:27:35 +02:00
if reply_email == alias . email :
2020-03-28 19:16:55 +01:00
continue
2020-04-05 15:27:35 +02:00
contact = Contact . get_by ( reply_email = reply_email )
2020-03-28 19:16:55 +01:00
if not contact :
2021-03-16 09:17:23 +01:00
LOG . w (
2022-01-11 12:29:42 +01:00
" email %s contained in %s header in reply phase must be reply emails. headers: %s " ,
2022-01-07 10:34:08 +01:00
reply_email ,
header ,
headers ,
2020-03-30 21:45:18 +02:00
)
2022-01-07 17:53:06 +01:00
raise NonReverseAliasInReplyPhase ( reply_email )
2020-03-28 19:16:55 +01:00
# still keep this email in header
2022-01-07 10:34:08 +01:00
# new_addrs.append(reply_email)
2020-04-05 15:27:35 +02:00
else :
2022-09-05 08:40:24 +02:00
new_addrs . append ( sl_formataddr ( ( contact . name , contact . website_email ) ) )
2020-03-28 19:16:55 +01:00
2020-08-25 12:48:28 +02:00
if new_addrs :
new_header = " , " . join ( new_addrs )
LOG . d ( " Replace %s header, old: %s , new: %s " , header , msg [ header ] , new_header )
add_or_replace_header ( msg , header , new_header )
else :
LOG . d ( " delete the %s header. Old value %s " , header , msg [ header ] )
delete_header ( msg , header )
2020-03-28 19:16:55 +01:00
2020-11-02 19:09:57 +01:00
def prepare_pgp_message (
orig_msg : Message , pgp_fingerprint : str , public_key : str , can_sign : bool = False
2020-11-07 13:00:45 +01:00
) - > Message :
2020-03-08 23:07:23 +01:00
msg = MIMEMultipart ( " encrypted " , protocol = " application/pgp-encrypted " )
2020-11-01 18:06:05 +01:00
# clone orig message to avoid modifying it
clone_msg = copy ( orig_msg )
2020-04-14 20:49:48 +02:00
# copy all headers from original message except all standard MIME headers
2020-11-01 18:06:05 +01:00
for i in reversed ( range ( len ( clone_msg . _headers ) ) ) :
header_name = clone_msg . _headers [ i ] [ 0 ] . lower ( )
2021-10-11 12:19:21 +02:00
if header_name . lower ( ) not in headers . MIME_HEADERS :
2020-11-01 18:06:05 +01:00
msg [ header_name ] = clone_msg . _headers [ i ] [ 1 ]
2020-03-08 23:07:23 +01:00
2020-11-02 19:09:57 +01:00
# Delete unnecessary headers in clone_msg except _MIME_HEADERS to save space
2020-03-14 22:24:02 +01:00
delete_all_headers_except (
2020-11-01 18:06:05 +01:00
clone_msg ,
2021-10-11 12:19:21 +02:00
headers . MIME_HEADERS ,
2020-03-14 22:24:02 +01:00
)
2021-10-11 12:10:18 +02:00
if clone_msg [ headers . CONTENT_TYPE ] is None :
2020-11-01 18:06:28 +01:00
LOG . d ( " Content-Type missing " )
2021-10-11 12:10:18 +02:00
clone_msg [ headers . CONTENT_TYPE ] = " text/plain "
2020-11-01 18:06:28 +01:00
2021-10-11 12:10:18 +02:00
if clone_msg [ headers . MIME_VERSION ] is None :
2020-11-01 18:06:28 +01:00
LOG . d ( " Mime-Version missing " )
2021-10-11 12:10:18 +02:00
clone_msg [ headers . MIME_VERSION ] = " 1.0 "
2020-11-01 18:06:28 +01:00
2020-03-08 23:07:23 +01:00
first = MIMEApplication (
_subtype = " pgp-encrypted " , _encoder = encoders . encode_7or8bit , _data = " "
)
first . set_payload ( " Version: 1 " )
msg . attach ( first )
2020-11-02 19:09:57 +01:00
if can_sign and PGP_SENDER_PRIVATE_KEY :
LOG . d ( " Sign msg " )
clone_msg = sign_msg ( clone_msg )
# use pgpy as fallback
2020-09-08 11:10:22 +02:00
second = MIMEApplication (
" octet-stream " , _encoder = encoders . encode_7or8bit , name = " encrypted.asc "
)
second . add_header ( " Content-Disposition " , ' inline; filename= " encrypted.asc " ' )
2020-10-28 11:50:14 +01:00
2020-11-02 19:09:57 +01:00
# encrypt
2020-10-28 17:07:53 +01:00
# use pgpy as fallback
2022-04-28 14:43:24 +02:00
msg_bytes = message_to_bytes ( clone_msg )
2020-10-28 17:07:53 +01:00
try :
2020-11-01 18:06:05 +01:00
encrypted_data = pgp_utils . encrypt_file ( BytesIO ( msg_bytes ) , pgp_fingerprint )
2020-10-28 17:07:53 +01:00
second . set_payload ( encrypted_data )
except PGPException :
2022-09-01 15:10:11 +02:00
LOG . w (
" Cannot encrypt using python-gnupg, check if public key is valid and try with pgpy "
)
# check if the public key is valid
load_public_key_and_check ( public_key )
2020-11-01 18:06:05 +01:00
encrypted = pgp_utils . encrypt_file_with_pgpy ( msg_bytes , public_key )
2020-10-28 17:07:53 +01:00
second . set_payload ( str ( encrypted ) )
2022-09-02 11:47:04 +02:00
LOG . i (
f " encryption works with pgpy and not with python-gnupg, public key { public_key } "
)
2020-10-28 11:50:14 +01:00
2020-03-08 23:07:23 +01:00
msg . attach ( second )
return msg
2020-11-02 19:09:57 +01:00
def sign_msg ( msg : Message ) - > Message :
container = MIMEMultipart (
" signed " , protocol = " application/pgp-signature " , micalg = " pgp-sha256 "
)
container . attach ( msg )
signature = MIMEApplication (
_subtype = " pgp-signature " , name = " signature.asc " , _data = " " , _encoder = encode_noop
)
signature . add_header ( " Content-Disposition " , ' attachment; filename= " signature.asc " ' )
try :
2022-04-28 14:43:24 +02:00
signature . set_payload ( sign_data ( message_to_bytes ( msg ) . replace ( b " \n " , b " \r \n " ) ) )
2020-11-02 19:09:57 +01:00
except Exception :
2021-09-08 11:29:55 +02:00
LOG . e ( " Cannot sign, try using pgpy " )
2020-11-02 19:09:57 +01:00
signature . set_payload (
2022-04-28 14:43:24 +02:00
sign_data_with_pgpy ( message_to_bytes ( msg ) . replace ( b " \n " , b " \r \n " ) )
2020-11-02 19:09:57 +01:00
)
container . attach ( signature )
return container
2022-01-08 00:09:45 +01:00
def handle_email_sent_to_ourself ( alias , from_addr : str , msg : Message , user ) :
2020-08-25 12:51:05 +02:00
# store the refused email
random_name = str ( uuid . uuid4 ( ) )
full_report_path = f " refused-emails/cycle- { random_name } .eml "
2022-04-28 14:43:24 +02:00
s3 . upload_email_from_bytesio (
full_report_path , BytesIO ( message_to_bytes ( msg ) ) , random_name
)
2020-08-25 12:51:05 +02:00
refused_email = RefusedEmail . create (
path = None , full_report_path = full_report_path , user_id = alias . user_id
)
2021-10-12 14:36:47 +02:00
Session . commit ( )
2020-08-25 12:51:05 +02:00
LOG . d ( " Create refused email %s " , refused_email )
# link available for 6 days as it gets deleted in 7 days
refused_email_url = refused_email . get_url ( expires_in = 518400 )
2022-06-07 16:44:57 +02:00
Notification . create (
user_id = user . id ,
title = f " Email sent to { alias . email } from its own mailbox { from_addr } " ,
message = Notification . render (
" notification/cycle-email.html " ,
alias = alias ,
from_addr = from_addr ,
refused_email_url = refused_email_url ,
) ,
commit = True ,
)
2020-08-27 10:43:48 +02:00
send_email_at_most_times (
2020-08-25 12:51:05 +02:00
user ,
ALERT_SEND_EMAIL_CYCLE ,
2022-01-08 00:09:45 +01:00
from_addr ,
f " Email sent to { alias . email } from its own mailbox { from_addr } " ,
2020-08-25 12:51:05 +02:00
render (
2021-11-02 11:46:41 +01:00
" transactional/cycle-email.txt.jinja2 " ,
2020-08-25 12:51:05 +02:00
alias = alias ,
2022-01-08 00:09:45 +01:00
from_addr = from_addr ,
2020-08-25 12:51:05 +02:00
refused_email_url = refused_email_url ,
) ,
render (
" transactional/cycle-email.html " ,
alias = alias ,
2022-01-08 00:09:45 +01:00
from_addr = from_addr ,
2020-08-25 12:51:05 +02:00
refused_email_url = refused_email_url ,
) ,
)
2020-09-30 11:05:21 +02:00
def handle_forward ( envelope , msg : Message , rcpt_to : str ) - > List [ Tuple [ bool , str ] ] :
2020-08-30 19:22:21 +02:00
""" return an array of SMTP status (is_success, smtp_status)
is_success indicates whether an email has been delivered and
2022-03-11 08:56:27 +01:00
smtp_status is the SMTP Status ( " 250 Message accepted " , " 550 Non-existent email address " , etc . )
2020-03-28 21:24:43 +01:00
"""
2021-09-10 17:48:36 +02:00
alias_address = rcpt_to # alias@SL
2020-02-19 16:49:40 +01:00
2021-09-10 17:48:36 +02:00
alias = Alias . get_by ( email = alias_address )
2020-03-17 11:51:40 +01:00
if not alias :
2021-09-10 17:48:36 +02:00
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 )
2020-03-17 11:51:40 +01:00
if not alias :
2021-09-10 17:48:36 +02:00
LOG . d ( " alias %s cannot be created on-the-fly, return 550 " , alias_address )
2021-08-02 11:33:58 +02:00
if should_ignore_bounce ( envelope . mail_from ) :
return [ ( True , status . E207 ) ]
else :
return [ ( False , status . E515 ) ]
2020-02-19 16:49:40 +01:00
2020-11-04 12:32:15 +01:00
user = alias . user
if user . disabled :
2021-03-16 09:17:23 +01:00
LOG . w ( " User %s disabled, disable forwarding emails for %s " , user , alias )
2021-08-02 11:33:58 +02:00
if should_ignore_bounce ( envelope . mail_from ) :
return [ ( True , status . E207 ) ]
else :
return [ ( False , status . E504 ) ]
2020-10-04 12:49:27 +02:00
2022-01-07 16:22:35 +01:00
# check if email is sent from alias's owning mailbox(es)
mail_from = envelope . mail_from
2022-01-08 00:09:45 +01:00
for addr in alias . authorized_addresses ( ) :
2022-01-07 16:22:35 +01:00
# email sent from a mailbox to its alias
2022-01-08 00:09:45 +01:00
if addr == mail_from :
LOG . i ( " cycle email sent from %s to %s " , addr , alias )
handle_email_sent_to_ourself ( alias , addr , msg , user )
2022-01-07 16:22:35 +01:00
return [ ( True , status . E209 ) ]
2020-08-25 12:51:05 +02:00
2021-10-11 12:10:18 +02:00
from_header = get_header_unicode ( msg [ headers . FROM ] )
2021-07-28 09:12:52 +02:00
LOG . d ( " Create or get contact for from_header: %s " , from_header )
2022-11-15 10:07:06 +01:00
contact = get_or_create_contact ( from_header , envelope . mail_from , alias )
2020-11-04 12:32:15 +01:00
2021-07-28 09:12:52 +02:00
reply_to_contact = None
2021-10-11 12:10:18 +02:00
if msg [ headers . REPLY_TO ] :
reply_to = get_header_unicode ( msg [ headers . REPLY_TO ] )
2022-01-07 17:53:06 +01:00
LOG . d ( " Create or get contact for reply_to_header: %s " , reply_to )
2021-07-28 09:12:52 +02:00
# ignore when reply-to = alias
if reply_to == alias . email :
LOG . i ( " Reply-to same as alias %s " , alias )
else :
2022-01-07 13:02:16 +01:00
reply_to_contact = get_or_create_reply_to_contact ( reply_to , alias , msg )
2021-07-28 09:12:52 +02:00
2021-10-28 10:19:58 +02:00
if not alias . enabled or contact . block_forward :
2020-04-27 18:18:40 +02:00
LOG . d ( " %s is disabled, do not forward " , alias )
2020-11-24 16:38:34 +01:00
EmailLog . create (
2021-07-11 12:28:23 +02:00
contact_id = contact . id ,
user_id = contact . user_id ,
blocked = True ,
alias_id = contact . alias_id ,
commit = True ,
2020-11-24 16:38:34 +01:00
)
2022-02-21 12:52:21 +01:00
2022-11-15 10:07:06 +01:00
# by default return 2** instead of 5** to allow user to receive emails again
# when alias is enabled or contact is unblocked
2022-02-21 12:52:21 +01:00
res_status = status . E200
if user . block_behaviour == BlockBehaviourEnum . return_5xx :
res_status = status . E502
return [ ( True , res_status ) ]
2020-03-30 22:05:31 +02:00
2022-03-18 15:44:07 +01:00
# Check if we need to reject or quarantine based on dmarc
2022-04-11 09:28:57 +02:00
msg , dmarc_delivery_status = apply_dmarc_policy_for_forward_phase (
2022-04-08 11:28:14 +02:00
alias , contact , envelope , msg
)
if dmarc_delivery_status is not None :
return [ ( False , dmarc_delivery_status ) ]
2022-03-18 15:44:07 +01:00
2020-05-10 16:57:47 +02:00
ret = [ ]
2020-08-21 10:41:50 +02:00
mailboxes = alias . mailboxes
2020-08-30 19:22:21 +02:00
# no valid mailbox
if not mailboxes :
2022-01-08 16:59:32 +01:00
LOG . w ( " no valid mailboxes for %s " , alias )
2021-08-02 11:33:58 +02:00
if should_ignore_bounce ( envelope . mail_from ) :
return [ ( True , status . E207 ) ]
else :
return [ ( False , status . E516 ) ]
2020-08-30 19:22:21 +02:00
2020-11-04 14:55:54 +01:00
for mailbox in mailboxes :
2020-09-10 09:38:30 +02:00
if not mailbox . verified :
2021-03-16 09:17:23 +01:00
LOG . d ( " %s unverified, do not forward " , mailbox )
2021-06-23 19:47:06 +02:00
ret . append ( ( False , status . E517 ) )
2020-09-10 09:38:30 +02:00
else :
2020-11-04 14:55:54 +01:00
# create a copy of message for each forward
2020-09-10 09:38:30 +02:00
ret . append (
2020-09-30 11:05:21 +02:00
forward_email_to_mailbox (
2021-07-28 09:12:52 +02:00
alias , copy ( msg ) , contact , envelope , mailbox , user , reply_to_contact
2020-09-10 09:38:30 +02:00
)
2020-05-10 16:57:47 +02:00
)
return ret
2020-09-30 11:05:21 +02:00
def forward_email_to_mailbox (
2020-05-10 16:57:47 +02:00
alias ,
msg : Message ,
contact : Contact ,
envelope ,
mailbox ,
user ,
2021-07-28 09:12:52 +02:00
reply_to_contact : Optional [ Contact ] ,
2020-05-10 16:57:47 +02:00
) - > ( bool , str ) :
LOG . d ( " Forward %s -> %s -> %s " , contact , alias , mailbox )
2020-06-09 17:16:32 +02:00
2020-10-12 13:28:21 +02:00
if mailbox . disabled :
2021-08-02 11:33:58 +02:00
LOG . d ( " %s disabled, do not forward " )
if should_ignore_bounce ( envelope . mail_from ) :
return True , status . E207
else :
return False , status . E518
2020-10-12 13:28:21 +02:00
2020-06-09 17:16:32 +02:00
# sanity check: make sure mailbox is not actually an alias
if get_email_domain_part ( alias . email ) == get_email_domain_part ( mailbox . email ) :
2021-03-16 09:17:23 +01:00
LOG . w (
2020-06-09 17:16:32 +02:00
" Mailbox has the same domain as alias. %s -> %s -> %s " ,
contact ,
alias ,
mailbox ,
)
2020-08-30 19:06:50 +02:00
mailbox_url = f " { URL } /dashboard/mailbox/ { mailbox . id } / "
send_email_with_rate_control (
user ,
ALERT_MAILBOX_IS_ALIAS ,
user . email ,
2020-12-21 09:39:26 +01:00
f " Your mailbox { mailbox . email } and alias { alias . email } use the same domain " ,
2020-08-30 19:06:50 +02:00
render (
2021-11-02 11:37:46 +01:00
" transactional/mailbox-invalid.txt.jinja2 " ,
2020-08-30 19:06:50 +02:00
mailbox = mailbox ,
mailbox_url = mailbox_url ,
2020-12-21 11:57:12 +01:00
alias = alias ,
2020-08-30 19:06:50 +02:00
) ,
render (
" transactional/mailbox-invalid.html " ,
mailbox = mailbox ,
mailbox_url = mailbox_url ,
2020-12-21 11:57:12 +01:00
alias = alias ,
2020-08-30 19:06:50 +02:00
) ,
2020-08-31 17:32:46 +02:00
max_nb_alert = 1 ,
2020-08-30 19:06:50 +02:00
)
2020-08-30 19:08:53 +02:00
# retry later
# so when user fixes the mailbox, the email can be delivered
2021-06-23 19:47:06 +02:00
return False , status . E405
2020-06-09 17:16:32 +02:00
2020-11-24 16:38:34 +01:00
email_log = EmailLog . create (
2021-07-11 12:28:23 +02:00
contact_id = contact . id ,
user_id = user . id ,
mailbox_id = mailbox . id ,
alias_id = contact . alias_id ,
2021-11-10 09:38:20 +01:00
message_id = str ( msg [ headers . MESSAGE_ID ] ) ,
2021-07-11 12:28:23 +02:00
commit = True ,
2020-11-24 16:38:34 +01:00
)
2021-05-24 12:08:30 +02:00
LOG . d ( " Create %s for %s , %s , %s " , email_log , contact , user , mailbox )
2020-11-24 16:38:34 +01:00
2021-03-26 10:00:48 +01:00
if ENABLE_SPAM_ASSASSIN :
# Spam check
spam_status = " "
is_spam = False
2020-08-15 16:38:16 +02:00
2021-03-26 10:00:48 +01:00
if SPAMASSASSIN_HOST :
start = time . time ( )
spam_score , spam_report = get_spam_score ( msg , email_log )
LOG . d (
" %s -> %s - spam score: %s in %s seconds. Spam report %s " ,
contact ,
alias ,
spam_score ,
time . time ( ) - start ,
spam_report ,
)
email_log . spam_score = spam_score
2021-10-12 14:36:47 +02:00
Session . commit ( )
2020-08-20 11:58:46 +02:00
2021-03-26 10:00:48 +01:00
if ( user . max_spam_score and spam_score > user . max_spam_score ) or (
not user . max_spam_score and spam_score > MAX_SPAM_SCORE
) :
is_spam = True
# only set the spam report for spam
email_log . spam_report = spam_report
else :
is_spam , spam_status = get_spam_info ( msg , max_score = user . max_spam_score )
2020-08-15 16:38:16 +02:00
2021-03-26 10:00:48 +01:00
if is_spam :
LOG . w (
" Email detected as spam. %s -> %s . Spam Score: %s , Spam Report: %s " ,
contact ,
alias ,
email_log . spam_score ,
email_log . spam_report ,
)
email_log . is_spam = True
email_log . spam_status = spam_status
2021-10-12 14:36:47 +02:00
Session . commit ( )
2020-05-15 16:34:07 +02:00
2021-03-26 10:00:48 +01:00
handle_spam ( contact , alias , msg , user , mailbox , email_log )
2021-06-23 19:47:06 +02:00
return False , status . E519
2020-03-30 22:05:31 +02:00
2020-11-14 15:55:53 +01:00
if contact . invalid_email :
LOG . d ( " add noreply information %s %s " , alias , mailbox )
msg = add_header (
msg ,
f """ Email sent to { alias . email } from an invalid address and cannot be replied """ ,
f """ Email sent to { alias . email } from an invalid address and cannot be replied """ ,
)
2021-01-28 16:34:24 +01:00
delete_all_headers_except (
msg ,
[
2021-10-11 12:19:21 +02:00
headers . FROM ,
headers . TO ,
headers . CC ,
headers . SUBJECT ,
2022-01-07 10:22:46 +01:00
headers . DATE ,
2021-10-11 12:00:37 +02:00
# do not delete original message id
headers . MESSAGE_ID ,
2021-01-28 16:34:24 +01:00
# References and In-Reply-To are used for keeping the email thread
2021-10-11 12:00:37 +02:00
headers . REFERENCES ,
headers . IN_REPLY_TO ,
2021-01-28 16:34:24 +01:00
]
2021-10-11 12:19:21 +02:00
+ headers . MIME_HEADERS ,
2021-01-28 16:34:24 +01:00
)
2020-03-08 23:07:23 +01:00
# create PGP email if needed
2020-11-24 11:28:14 +01:00
if mailbox . pgp_enabled ( ) and user . is_premium ( ) and not alias . disable_pgp :
2020-03-08 23:07:23 +01:00
LOG . d ( " Encrypt message using mailbox %s " , mailbox )
2020-11-07 13:00:45 +01:00
if mailbox . generic_subject :
LOG . d ( " Use a generic subject for %s " , mailbox )
2021-10-11 12:10:18 +02:00
orig_subject = msg [ headers . SUBJECT ]
2020-11-09 21:16:50 +01:00
orig_subject = get_header_unicode ( orig_subject )
2020-11-07 13:00:45 +01:00
add_or_replace_header ( msg , " Subject " , mailbox . generic_subject )
msg = add_header (
msg ,
2020-11-07 17:23:28 +01:00
f """ Forwarded by SimpleLogin to { alias . email } with " { orig_subject } " as subject """ ,
f """ Forwarded by SimpleLogin to { alias . email } with <b> { orig_subject } </b> as subject """ ,
2020-11-07 13:00:45 +01:00
)
2020-06-08 13:54:42 +02:00
try :
2020-10-28 11:50:14 +01:00
msg = prepare_pgp_message (
2020-11-02 19:09:57 +01:00
msg , mailbox . pgp_finger_print , mailbox . pgp_public_key , can_sign = True
2020-10-28 11:50:14 +01:00
)
2020-06-08 13:54:42 +02:00
except PGPException :
2022-09-02 11:47:04 +02:00
LOG . w (
2020-06-08 13:54:42 +02:00
" Cannot encrypt message %s -> %s . %s %s " , contact , alias , mailbox , user
)
2022-09-02 11:47:04 +02:00
msg = add_header (
msg ,
f """ PGP encryption fails with { mailbox . email } ' s PGP key """ ,
)
2020-03-08 23:07:23 +01:00
2020-04-27 18:18:40 +02:00
# add custom header
2021-10-11 12:19:21 +02:00
add_or_replace_header ( msg , headers . SL_DIRECTION , " Forward " )
2020-04-27 18:18:40 +02:00
2021-10-11 12:19:21 +02:00
msg [ headers . SL_EMAIL_LOG_ID ] = str ( email_log . id )
2022-02-25 12:22:09 +01:00
if user . include_header_email_header :
msg [ headers . SL_ENVELOPE_FROM ] = envelope . mail_from
2021-05-12 10:46:07 +02:00
# when an alias isn't in the To: header, there's no way for users to know what alias has received the email
2021-10-11 12:19:21 +02:00
msg [ headers . SL_ENVELOPE_TO ] = alias . email
2021-10-11 12:00:37 +02:00
if not msg [ headers . DATE ] :
LOG . w ( " missing date header, create one " )
msg [ headers . DATE ] = formatdate ( )
2020-11-02 14:53:22 +01:00
2021-11-01 18:45:10 +01:00
replace_sl_message_id_by_original_message_id ( msg )
2021-10-18 17:35:16 +02:00
2022-01-08 16:59:32 +01:00
# change the from_header so the email comes from a reverse-alias
2020-04-27 18:18:40 +02:00
# replace the email part in from: header
2022-01-08 16:59:32 +01:00
old_from_header = msg [ headers . FROM ]
2020-04-27 18:18:40 +02:00
new_from_header = contact . new_addr ( )
add_or_replace_header ( msg , " From " , new_from_header )
2022-01-08 16:59:32 +01:00
LOG . d ( " From header, new: %s , old: %s " , new_from_header , old_from_header )
2020-04-27 18:18:40 +02:00
2021-07-28 09:12:52 +02:00
if reply_to_contact :
2021-10-11 12:10:18 +02:00
reply_to_header = msg [ headers . REPLY_TO ]
2021-07-28 09:12:52 +02:00
new_reply_to_header = reply_to_contact . new_addr ( )
add_or_replace_header ( msg , " Reply-To " , new_reply_to_header )
LOG . d ( " Reply-To header, new: %s , old: %s " , new_reply_to_header , reply_to_header )
2020-11-01 18:13:50 +01:00
# replace CC & To emails by reverse-alias for all emails that are not alias
2022-01-08 00:16:32 +01:00
try :
2022-04-15 10:16:03 +02:00
replace_header_when_forward ( msg , alias , headers . CC )
replace_header_when_forward ( msg , alias , headers . TO )
2022-01-08 00:16:32 +01:00
except CannotCreateContactForReverseAlias :
LOG . d ( " CannotCreateContactForReverseAlias error, delete %s " , email_log )
EmailLog . delete ( email_log . id )
Session . commit ( )
raise
2020-04-27 18:18:40 +02:00
2022-04-15 10:16:03 +02:00
# add alias to To: header if it isn't included in To and Cc header
add_alias_to_header_if_needed ( msg , alias )
2020-04-27 18:18:40 +02:00
# add List-Unsubscribe header
2022-06-30 11:40:01 +02:00
msg = UnsubscribeGenerator ( ) . add_header_to_message ( alias , contact , msg )
2019-12-15 10:18:33 +01:00
2021-09-25 18:47:15 +02:00
add_dkim_signature ( msg , EMAIL_DOMAIN )
2020-04-27 18:18:40 +02:00
LOG . d (
2021-03-16 09:17:23 +01:00
" Forward mail from %s to %s , mail_options: %s , rcpt_options: %s " ,
2020-04-27 18:18:40 +02:00
contact . website_email ,
2020-05-10 16:32:54 +02:00
mailbox . email ,
2020-04-27 18:18:40 +02:00
envelope . mail_options ,
envelope . rcpt_options ,
)
2020-09-02 10:16:13 +02:00
try :
2020-09-29 12:57:14 +02:00
sl_sendmail (
2021-01-11 14:55:55 +01:00
# use a different envelope sender for each forward (aka VERP)
2022-03-25 18:14:31 +01:00
generate_verp_email ( VerpType . bounce_forward , email_log . id ) ,
2020-09-02 10:16:13 +02:00
mailbox . email ,
2020-09-29 12:57:14 +02:00
msg ,
2020-09-02 10:16:13 +02:00
envelope . mail_options ,
envelope . rcpt_options ,
2021-01-28 13:50:24 +01:00
is_forward = True ,
2020-09-02 10:16:13 +02:00
)
2021-12-29 15:17:57 +01:00
except ( SMTPServerDisconnected , SMTPRecipientsRefused , TimeoutError ) :
2021-03-16 09:17:23 +01:00
LOG . w (
2021-12-29 15:17:57 +01:00
" Postfix error during forward phase %s -> %s -> %s " ,
2020-09-02 10:16:13 +02:00
contact ,
alias ,
mailbox ,
2021-12-23 19:34:17 +01:00
exc_info = True ,
2020-09-02 10:16:13 +02:00
)
2021-08-02 11:33:58 +02:00
if should_ignore_bounce ( envelope . mail_from ) :
return True , status . E207
else :
2022-01-08 16:59:32 +01:00
EmailLog . delete ( email_log . id , commit = True )
2021-12-29 15:17:57 +01:00
# so Postfix can retry
2021-12-23 18:17:29 +01:00
return False , status . E407
2020-09-02 10:16:13 +02:00
else :
2021-10-12 14:36:47 +02:00
Session . commit ( )
2021-06-23 19:47:06 +02:00
return True , status . E200
2019-11-19 10:23:06 +01:00
2021-11-01 18:45:10 +01:00
def replace_sl_message_id_by_original_message_id ( msg ) :
# Replace SL Message-ID by original one in In-Reply-To header
if msg [ headers . IN_REPLY_TO ] :
matching : MessageIDMatching = MessageIDMatching . get_by (
2021-11-08 11:21:01 +01:00
sl_message_id = str ( msg [ headers . IN_REPLY_TO ] )
2021-11-01 18:45:10 +01:00
)
if matching :
LOG . d (
" replace SL message id by original one in in-reply-to header, %s -> %s " ,
msg [ headers . IN_REPLY_TO ] ,
matching . original_message_id ,
)
del msg [ headers . IN_REPLY_TO ]
msg [ headers . IN_REPLY_TO ] = matching . original_message_id
# Replace SL Message-ID by original Message-ID in References header
if msg [ headers . REFERENCES ] :
2022-06-29 19:48:22 +02:00
message_ids = str ( msg [ headers . REFERENCES ] ) . split ( )
2021-11-01 18:45:10 +01:00
new_message_ids = [ ]
for message_id in message_ids :
matching = MessageIDMatching . get_by ( sl_message_id = message_id )
if matching :
LOG . d (
" replace SL message id by original one in references header, %s -> %s " ,
message_id ,
matching . original_message_id ,
)
new_message_ids . append ( matching . original_message_id )
else :
new_message_ids . append ( message_id )
del msg [ headers . REFERENCES ]
msg [ headers . REFERENCES ] = " " . join ( new_message_ids )
2020-09-30 11:05:21 +02:00
def handle_reply ( envelope , msg : Message , rcpt_to : str ) - > ( bool , str ) :
2020-03-28 21:24:43 +01:00
"""
2021-01-28 16:34:24 +01:00
Return whether an email has been delivered and
2020-03-28 21:24:43 +01:00
the smtp status ( " 250 Message accepted " , " 550 Non-existent email address " , etc )
"""
2022-04-06 12:51:04 +02:00
2020-09-14 17:38:48 +02:00
reply_email = rcpt_to
2019-11-19 10:23:06 +01:00
2020-02-19 16:17:13 +01:00
# reply_email must end with EMAIL_DOMAIN
if not reply_email . endswith ( EMAIL_DOMAIN ) :
2021-03-16 09:17:23 +01:00
LOG . w ( f " Reply email { reply_email } has wrong domain " )
2021-06-23 19:47:06 +02:00
return False , status . E501
2019-11-19 10:23:06 +01:00
2020-11-22 13:07:09 +01:00
# handle case where reply email is generated with non-allowed char
reply_email = normalize_reply_email ( reply_email )
2020-11-18 16:11:00 +01:00
2020-03-17 10:56:59 +01:00
contact = Contact . get_by ( reply_email = reply_email )
if not contact :
2022-03-11 08:56:27 +01:00
LOG . w ( f " No contact with { reply_email } as reverse alias " )
2021-06-23 19:47:06 +02:00
return False , status . E502
2019-12-18 17:07:20 +01:00
2020-03-28 19:16:55 +01:00
alias = contact . alias
2021-12-30 10:24:57 +01:00
alias_address : str = contact . alias . email
alias_domain = alias_address [ alias_address . find ( " @ " ) + 1 : ]
2019-11-19 10:23:06 +01:00
2020-10-15 16:21:31 +02:00
# Sanity check: verify alias domain is managed by SimpleLogin
# scenario: a user have removed a domain but due to a bug, the aliases are still there
if not is_valid_alias_address_domain ( alias . email ) :
2021-09-08 11:29:55 +02:00
LOG . e ( " %s domain isn ' t known " , alias )
2021-06-23 19:47:06 +02:00
return False , status . E503
2019-11-19 10:23:06 +01:00
2020-03-17 11:51:40 +01:00
user = alias . user
2020-09-14 17:38:48 +02:00
mail_from = envelope . mail_from
2020-02-19 16:17:13 +01:00
2020-10-04 12:49:27 +02:00
if user . disabled :
2021-09-08 11:29:55 +02:00
LOG . e (
2020-10-04 12:49:27 +02:00
" User %s disabled, disable sending emails from %s to %s " ,
user ,
alias ,
contact ,
)
2022-04-06 17:44:05 +02:00
return False , status . E504
2022-04-06 12:51:04 +02:00
# Check if we need to reject or quarantine based on dmarc
dmarc_delivery_status = apply_dmarc_policy_for_reply_phase (
alias , contact , envelope , msg
)
if dmarc_delivery_status is not None :
2022-04-06 17:44:05 +02:00
return False , dmarc_delivery_status
2020-10-04 12:49:27 +02:00
2020-09-28 17:43:09 +02:00
# Anti-spoofing
mailbox = get_mailbox_from_mail_from ( mail_from , alias )
if not mailbox :
2020-08-26 14:39:51 +02:00
if alias . disable_email_spoofing_check :
# ignore this error, use default alias mailbox
2021-03-16 09:17:23 +01:00
LOG . w (
2020-08-26 14:39:51 +02:00
" ignore unknown sender to reverse-alias %s : %s -> %s " ,
mail_from ,
alias ,
contact ,
)
mailbox = alias . mailbox
else :
# only mailbox can send email to the reply-email
2020-09-16 17:24:42 +02:00
handle_unknown_mailbox ( envelope , msg , reply_email , user , alias , contact )
2022-03-16 09:06:49 +01:00
# return 2** to avoid Postfix sending out bounces and avoid backscatter issue
return False , status . E214
2020-05-10 18:19:29 +02:00
2020-08-26 14:39:51 +02:00
if ENFORCE_SPF and mailbox . force_spf and not alias . disable_email_spoofing_check :
2021-03-17 10:16:09 +01:00
if not spf_pass ( envelope , mailbox , user , alias , contact . website_email , msg ) :
2021-06-23 19:47:06 +02:00
# cannot use 4** here as sender will retry.
# cannot use 5** because that generates bounce report
return True , status . E201
2020-05-07 13:28:04 +02:00
2020-08-16 14:28:47 +02:00
email_log = EmailLog . create (
2020-11-24 16:38:34 +01:00
contact_id = contact . id ,
2021-07-11 12:28:23 +02:00
alias_id = contact . alias_id ,
2020-11-24 16:38:34 +01:00
is_reply = True ,
user_id = contact . user_id ,
mailbox_id = mailbox . id ,
2021-10-18 17:35:16 +02:00
message_id = msg [ headers . MESSAGE_ID ] ,
2020-12-11 11:13:19 +01:00
commit = True ,
2020-08-16 14:28:47 +02:00
)
2021-05-24 12:08:30 +02:00
LOG . d ( " Create %s for %s , %s , %s " , email_log , contact , user , mailbox )
2020-08-16 14:28:47 +02:00
2020-08-15 16:53:57 +02:00
# Spam check
2021-03-26 10:00:48 +01:00
if ENABLE_SPAM_ASSASSIN :
spam_status = " "
is_spam = False
# do not use user.max_spam_score here
if SPAMASSASSIN_HOST :
start = time . time ( )
spam_score , spam_report = get_spam_score ( msg , email_log )
LOG . d (
" %s -> %s - spam score %s in %s seconds. Spam report %s " ,
alias ,
contact ,
spam_score ,
time . time ( ) - start ,
spam_report ,
)
email_log . spam_score = spam_score
if spam_score > MAX_REPLY_PHASE_SPAM_SCORE :
is_spam = True
# only set the spam report for spam
email_log . spam_report = spam_report
else :
is_spam , spam_status = get_spam_info (
msg , max_score = MAX_REPLY_PHASE_SPAM_SCORE
)
2020-08-15 16:53:57 +02:00
2021-03-26 10:00:48 +01:00
if is_spam :
LOG . w (
" Email detected as spam. Reply phase. %s -> %s . Spam Score: %s , Spam Report: %s " ,
alias ,
contact ,
email_log . spam_score ,
email_log . spam_report ,
)
2020-08-16 14:28:47 +02:00
2021-03-26 10:00:48 +01:00
email_log . is_spam = True
email_log . spam_status = spam_status
2021-10-12 14:36:47 +02:00
Session . commit ( )
2020-08-15 16:53:57 +02:00
2021-03-26 10:00:48 +01:00
handle_spam ( contact , alias , msg , user , mailbox , email_log , is_reply = True )
2021-06-23 19:47:06 +02:00
return False , status . E506
2020-08-15 16:53:57 +02:00
2020-11-02 14:51:37 +01:00
delete_all_headers_except (
msg ,
[
2021-10-11 12:19:21 +02:00
headers . FROM ,
headers . TO ,
headers . CC ,
headers . SUBJECT ,
2022-01-07 10:22:46 +01:00
headers . DATE ,
2021-10-11 12:00:37 +02:00
# do not delete original message id
headers . MESSAGE_ID ,
2020-12-11 11:13:19 +01:00
# References and In-Reply-To are used for keeping the email thread
2021-10-11 12:00:37 +02:00
headers . REFERENCES ,
headers . IN_REPLY_TO ,
2020-11-02 14:51:37 +01:00
]
2021-10-11 12:19:21 +02:00
+ headers . MIME_HEADERS ,
2020-11-02 14:51:37 +01:00
)
2020-11-01 18:12:09 +01:00
2022-10-30 19:59:42 +01:00
orig_to = msg [ headers . TO ]
orig_cc = msg [ headers . CC ]
2021-10-25 14:33:42 +02:00
# replace the reverse-alias by the contact email in the email body
2020-11-01 18:02:43 +01:00
# as this is usually included when replying
if user . replace_reverse_alias :
2020-11-30 15:15:44 +01:00
LOG . d ( " Replace reverse-alias %s by contact email %s " , reply_email , contact )
msg = replace ( msg , reply_email , contact . website_email )
2022-11-18 14:30:19 +01:00
LOG . d ( " Replace mailbox %s by alias email %s " , mailbox . email , alias . email )
msg = replace ( msg , mailbox . email , alias . email )
2020-11-01 18:02:43 +01:00
2022-10-10 10:00:19 +02:00
if config . ENABLE_ALL_REVERSE_ALIAS_REPLACEMENT :
start = time . time ( )
# MAX_NB_REVERSE_ALIAS_REPLACEMENT is there to limit potential attack
contact_query = (
Contact . query ( )
. filter ( Contact . alias_id == alias . id )
. limit ( config . MAX_NB_REVERSE_ALIAS_REPLACEMENT )
)
# replace reverse alias by real address for all contacts
for ( reply_email , website_email ) in contact_query . values (
Contact . reply_email , Contact . website_email
) :
msg = replace ( msg , reply_email , website_email )
elapsed = time . time ( ) - start
LOG . d (
" Replace reverse alias by real address for %s contacts takes %s seconds " ,
contact_query . count ( ) ,
elapsed ,
)
newrelic . agent . record_custom_metric (
" Custom/reverse_alias_replacement_time " , elapsed
)
2020-11-01 18:02:43 +01:00
# create PGP email if needed
if contact . pgp_finger_print and user . is_premium ( ) :
LOG . d ( " Encrypt message for contact %s " , contact )
try :
msg = prepare_pgp_message (
msg , contact . pgp_finger_print , contact . pgp_public_key
)
except PGPException :
2021-06-23 19:55:41 +02:00
LOG . e (
2020-11-01 18:02:43 +01:00
" Cannot encrypt message %s -> %s . %s %s " , alias , contact , mailbox , user
)
2021-11-02 10:59:17 +01:00
# programming error, user shouldn't see a new email log
2022-01-08 00:28:26 +01:00
EmailLog . delete ( email_log . id , commit = True )
2020-11-01 18:02:43 +01:00
# return 421 so the client can retry later
2021-06-23 19:47:06 +02:00
return False , status . E402
2020-11-01 18:02:43 +01:00
2021-10-12 14:36:47 +02:00
Session . commit ( )
2020-11-04 12:32:15 +01:00
2020-03-28 19:16:55 +01:00
# make the email comes from alias
2020-05-03 16:05:34 +02:00
from_header = alias . email
# add alias name from alias
2020-04-26 10:41:24 +02:00
if alias . name :
2021-11-22 11:23:21 +01:00
LOG . d ( " Put alias name %s in from header " , alias . name )
2022-09-05 08:40:24 +02:00
from_header = sl_formataddr ( ( alias . name , alias . email ) )
2020-05-03 16:05:34 +02:00
elif alias . custom_domain :
# add alias name from domain
if alias . custom_domain . name :
2021-11-22 11:23:21 +01:00
LOG . d (
" Put domain default alias name %s in from header " ,
alias . custom_domain . name ,
)
2022-09-05 08:40:24 +02:00
from_header = sl_formataddr ( ( alias . custom_domain . name , alias . email ) )
2020-05-03 16:05:34 +02:00
2021-11-22 11:23:21 +01:00
LOG . d ( " From header is %s " , from_header )
2021-10-11 12:13:24 +02:00
add_or_replace_header ( msg , headers . FROM , from_header )
2020-02-10 17:24:14 +01:00
2022-01-07 17:53:06 +01:00
try :
2022-09-27 09:43:58 +02:00
if str ( msg [ headers . TO ] ) . lower ( ) == " undisclosed-recipients:; " :
# no need to replace TO header
LOG . d ( " email is sent in BCC mode " )
del msg [ headers . TO ]
else :
replace_header_when_reply ( msg , alias , headers . TO )
2022-01-07 17:53:06 +01:00
replace_header_when_reply ( msg , alias , headers . CC )
except NonReverseAliasInReplyPhase as e :
LOG . w ( " non reverse-alias in reply %s %s %s " , e , contact , alias )
2022-01-08 00:28:26 +01:00
# the email is ignored, delete the email log
EmailLog . delete ( email_log . id , commit = True )
2022-01-11 13:14:47 +01:00
send_email (
2022-01-07 17:53:06 +01:00
mailbox . email ,
f " Email sent to { contact . email } contains non reverse-alias addresses " ,
render (
" transactional/non-reverse-alias-reply-phase.txt.jinja2 " ,
destination = contact . email ,
alias = alias . email ,
subject = msg [ headers . SUBJECT ] ,
) ,
)
# user is informed and will retry
return True , status . E200
2019-12-15 17:04:46 +01:00
2021-11-01 18:43:19 +01:00
replace_original_message_id ( alias , email_log , msg )
2021-10-11 12:13:24 +02:00
if not msg [ headers . DATE ] :
date_header = formatdate ( )
LOG . w ( " missing date header, add one " )
msg [ headers . DATE ] = date_header
2020-08-25 12:47:28 +02:00
2021-10-11 12:19:21 +02:00
msg [ headers . SL_DIRECTION ] = " Reply "
msg [ headers . SL_EMAIL_LOG_ID ] = str ( email_log . id )
2020-08-25 12:47:28 +02:00
2020-02-19 16:17:13 +01:00
LOG . d (
" send email from %s to %s , mail_options: %s ,rcpt_options: %s " ,
2020-03-28 19:16:55 +01:00
alias . email ,
2020-03-17 10:56:59 +01:00
contact . website_email ,
2020-02-19 16:17:13 +01:00
envelope . mail_options ,
envelope . rcpt_options ,
)
2019-12-17 17:48:06 +01:00
2021-09-25 18:47:15 +02:00
if should_add_dkim_signature ( alias_domain ) :
add_dkim_signature ( msg , alias_domain )
2020-06-20 16:19:01 +02:00
try :
2020-09-29 12:57:14 +02:00
sl_sendmail (
2022-03-25 18:14:31 +01:00
generate_verp_email ( VerpType . bounce_reply , email_log . id , alias_domain ) ,
2020-06-20 16:19:01 +02:00
contact . website_email ,
2020-09-29 12:57:14 +02:00
msg ,
2020-06-20 16:19:01 +02:00
envelope . mail_options ,
envelope . rcpt_options ,
2021-01-28 13:50:24 +01:00
is_forward = False ,
2020-06-20 16:19:01 +02:00
)
2022-10-30 19:59:42 +01:00
# if alias belongs to several mailboxes, notify other mailboxes about this email
other_mailboxes = [ mb for mb in alias . mailboxes if mb . email != mailbox . email ]
for mb in other_mailboxes :
notify_mailbox ( alias , mailbox , mb , msg , orig_to , orig_cc , alias_domain )
2020-06-20 16:19:01 +02:00
except Exception :
2021-03-16 09:17:23 +01:00
LOG . w ( " Cannot send email from %s to %s " , alias , contact )
2022-01-08 16:59:32 +01:00
EmailLog . delete ( email_log . id , commit = True )
2020-06-20 16:19:01 +02:00
send_email (
mailbox . email ,
f " Email cannot be sent to { contact . email } from { alias . email } " ,
render (
2021-11-02 10:58:51 +01:00
" transactional/reply-error.txt.jinja2 " ,
2020-06-20 16:19:01 +02:00
user = user ,
alias = alias ,
contact = contact ,
contact_domain = get_email_domain_part ( contact . email ) ,
) ,
render (
" transactional/reply-error.html " ,
user = user ,
alias = alias ,
contact = contact ,
contact_domain = get_email_domain_part ( contact . email ) ,
) ,
)
2020-01-07 19:14:36 +01:00
2020-09-02 10:16:13 +02:00
# return 250 even if error as user is already informed of the incident and can retry sending the email
2021-06-23 19:47:06 +02:00
return True , status . E200
2019-11-20 18:52:49 +01:00
2020-01-08 12:44:29 +01:00
2022-10-30 19:59:42 +01:00
def notify_mailbox (
alias , mailbox , other_mb : Mailbox , msg , orig_to , orig_cc , alias_domain
) :
""" Notify another mailbox about an email sent by a mailbox to a reverse alias """
LOG . d (
f " notify { other_mb . email } about email sent "
f " from { mailbox . email } on behalf of { alias . email } to { msg [ headers . TO ] } "
)
notif = add_header (
msg ,
f """ **** Don ' t forget to remove this section if you reply to this email ****
Email sent on behalf of alias { alias . email } using mailbox { mailbox . email } """ ,
)
# use alias as From to hint that the email is sent from the alias
add_or_replace_header ( notif , headers . FROM , alias . email )
# keep the reverse alias in CC and To header so user can reply more easily
add_or_replace_header ( notif , headers . TO , orig_to )
add_or_replace_header ( notif , headers . CC , orig_cc )
# add DKIM as the email is sent from alias
if should_add_dkim_signature ( alias_domain ) :
add_dkim_signature ( msg , alias_domain )
# this notif is considered transactional email
transaction = TransactionalEmail . create ( email = other_mb . email , commit = True )
sl_sendmail (
2022-11-04 14:22:28 +01:00
generate_verp_email ( VerpType . transactional , transaction . id , alias_domain ) ,
2022-10-30 19:59:42 +01:00
other_mb . email ,
notif ,
)
2021-11-01 18:43:19 +01:00
def replace_original_message_id ( alias : Alias , email_log : EmailLog , msg : Message ) :
"""
Replace original Message - ID by SL - Message - ID during the reply phase
for " message-id " and " References " headers
"""
original_message_id = msg [ headers . MESSAGE_ID ]
2021-11-05 09:43:58 +01:00
if original_message_id :
matching = MessageIDMatching . get_by ( original_message_id = original_message_id )
# can happen when a user replies to multiple recipient from their alias
# a SL Message_id will be created for the first recipient
# it should be reused for other recipients
if matching :
sl_message_id = matching . sl_message_id
LOG . d ( " reuse the sl_message_id %s " , sl_message_id )
else :
sl_message_id = make_msgid (
str ( email_log . id ) , get_email_domain_part ( alias . email )
)
LOG . d ( " create a new sl_message_id %s " , sl_message_id )
try :
MessageIDMatching . create (
sl_message_id = sl_message_id ,
original_message_id = original_message_id ,
email_log_id = email_log . id ,
commit = True ,
)
except IntegrityError :
LOG . w (
" another matching with original_message_id %s was created in the mean time " ,
original_message_id ,
)
Session . rollback ( )
matching = MessageIDMatching . get_by (
original_message_id = original_message_id
)
sl_message_id = matching . sl_message_id
2021-11-01 18:43:19 +01:00
else :
sl_message_id = make_msgid (
str ( email_log . id ) , get_email_domain_part ( alias . email )
)
2021-11-05 09:43:58 +01:00
LOG . d ( " no original_message_id, create a new sl_message_id %s " , sl_message_id )
2021-11-01 20:36:15 +01:00
2021-11-01 18:43:19 +01:00
del msg [ headers . MESSAGE_ID ]
msg [ headers . MESSAGE_ID ] = sl_message_id
2021-11-01 20:36:15 +01:00
2021-11-01 18:43:19 +01:00
email_log . sl_message_id = sl_message_id
Session . commit ( )
# Replace all original headers in References header by SL Message ID header if needed
if msg [ headers . REFERENCES ] :
2022-06-29 19:48:22 +02:00
message_ids = str ( msg [ headers . REFERENCES ] ) . split ( )
2021-11-01 18:43:19 +01:00
new_message_ids = [ ]
for message_id in message_ids :
matching = MessageIDMatching . get_by ( original_message_id = message_id )
if matching :
LOG . d (
" replace original message id by SL one, %s -> %s " ,
message_id ,
matching . sl_message_id ,
)
new_message_ids . append ( matching . sl_message_id )
else :
new_message_ids . append ( message_id )
del msg [ headers . REFERENCES ]
msg [ headers . REFERENCES ] = " " . join ( new_message_ids )
2020-09-28 17:41:16 +02:00
def get_mailbox_from_mail_from ( mail_from : str , alias ) - > Optional [ Mailbox ] :
""" return the corresponding mailbox given the mail_from and alias
Usually the mail_from = mailbox . email but it can also be one of the authorized address
"""
2023-01-25 13:17:20 +01:00
def __check ( email_address : str , alias : Alias ) - > Optional [ Mailbox ] :
for mailbox in alias . mailboxes :
if mailbox . email == email_address :
2020-09-28 17:41:16 +02:00
return mailbox
2023-01-25 13:17:20 +01:00
for authorized_address in mailbox . authorized_addresses :
if authorized_address . email == email_address :
LOG . d (
" Found an authorized address for %s %s %s " ,
alias ,
mailbox ,
authorized_address ,
)
return mailbox
return None
# We need to first check for the uncanonicalized version because we still have users in the db with the
# email non canonicalized. So if it matches the already existing one use that, otherwise check the canonical one
return __check ( mail_from , alias ) or __check ( canonicalize_email ( mail_from ) , alias )
2020-09-28 17:41:16 +02:00
2020-09-16 17:24:42 +02:00
def handle_unknown_mailbox (
envelope , msg , reply_email : str , user : User , alias : Alias , contact : Contact
) :
2021-03-16 09:17:23 +01:00
LOG . w (
2020-12-06 13:54:59 +01:00
" Reply email can only be used by mailbox. "
" Actual mail_from: %s . msg from header: %s , reverse-alias %s , %s %s %s " ,
2020-05-09 23:12:30 +02:00
envelope . mail_from ,
2021-10-11 12:10:18 +02:00
msg [ headers . FROM ] ,
2020-05-09 23:12:30 +02:00
reply_email ,
2020-05-10 18:19:29 +02:00
alias ,
user ,
2020-09-16 17:24:42 +02:00
contact ,
2020-05-09 23:12:30 +02:00
)
2020-09-29 11:00:50 +02:00
authorize_address_link = (
f " { URL } /dashboard/mailbox/ { alias . mailbox_id } /#authorized-address "
)
2020-09-29 13:03:15 +02:00
mailbox_emails = [ mailbox . email for mailbox in alias . mailboxes ]
2020-05-09 23:12:30 +02:00
send_email_with_rate_control (
user ,
ALERT_REVERSE_ALIAS_UNKNOWN_MAILBOX ,
2020-05-10 18:19:29 +02:00
user . email ,
2020-08-27 11:12:48 +02:00
f " Attempt to use your alias { alias . email } from { envelope . mail_from } " ,
2020-05-09 23:12:30 +02:00
render (
" transactional/reply-must-use-personal-email.txt " ,
2020-05-10 18:19:29 +02:00
alias = alias ,
2020-05-09 23:12:30 +02:00
sender = envelope . mail_from ,
2020-09-29 11:00:50 +02:00
authorize_address_link = authorize_address_link ,
2020-09-29 13:11:04 +02:00
mailbox_emails = mailbox_emails ,
2020-05-09 23:12:30 +02:00
) ,
render (
" transactional/reply-must-use-personal-email.html " ,
2020-05-10 18:19:29 +02:00
alias = alias ,
2020-05-09 23:12:30 +02:00
sender = envelope . mail_from ,
2020-09-29 11:00:50 +02:00
authorize_address_link = authorize_address_link ,
2020-09-29 13:11:04 +02:00
mailbox_emails = mailbox_emails ,
2020-05-09 23:12:30 +02:00
) ,
)
2021-01-11 14:55:55 +01:00
def handle_bounce_forward_phase ( msg : Message , email_log : EmailLog ) :
2020-11-04 12:32:15 +01:00
"""
2021-01-11 14:55:55 +01:00
Handle forward phase bounce
Happens when an email cannot be sent to a mailbox
2020-11-04 12:32:15 +01:00
"""
2021-01-11 14:55:55 +01:00
contact = email_log . contact
alias = contact . alias
2021-01-04 14:22:07 +01:00
user = alias . user
2021-01-11 15:53:08 +01:00
mailbox = email_log . mailbox
# email_log.mailbox should be set during the forward phase
if not mailbox :
2021-09-08 11:29:55 +02:00
LOG . e ( " Use %s default mailbox %s " , alias , alias . mailbox )
2021-01-11 15:53:08 +01:00
mailbox = alias . mailbox
2021-10-14 15:46:52 +02:00
bounce_info = get_mailbox_bounce_info ( msg )
if bounce_info :
2021-10-19 12:05:35 +02:00
Bounce . create (
email = mailbox . email , info = bounce_info . as_bytes ( ) . decode ( ) , commit = True
)
2021-10-14 15:46:52 +02:00
else :
2021-11-02 14:32:16 +01:00
LOG . w ( " cannot get bounce info, debug at %s " , save_email_for_debugging ( msg ) )
2021-10-14 15:46:52 +02:00
Bounce . create ( email = mailbox . email , commit = True )
2021-01-26 09:56:13 +01:00
2021-09-08 11:29:55 +02:00
LOG . d (
2021-01-11 15:53:08 +01:00
" Handle forward bounce %s -> %s -> %s . %s " , contact , alias , mailbox , email_log
)
2021-01-04 14:22:07 +01:00
2021-01-11 14:55:55 +01:00
# Store the bounced email, generate a name for the email
random_name = str ( uuid . uuid4 ( ) )
2020-11-04 15:38:26 +01:00
2021-01-11 14:55:55 +01:00
full_report_path = f " refused-emails/full- { random_name } .eml "
2021-01-11 15:25:54 +01:00
s3 . upload_email_from_bytesio (
2022-04-28 14:43:24 +02:00
full_report_path , BytesIO ( message_to_bytes ( msg ) ) , f " full- { random_name } "
2021-01-11 15:25:54 +01:00
)
2021-01-11 14:55:55 +01:00
file_path = None
orig_msg = get_orig_message_from_bounce ( msg )
if not orig_msg :
# Some MTA does not return the original message in bounce message
# nothing we can do here
2021-03-16 09:17:23 +01:00
LOG . w (
2021-01-11 14:55:55 +01:00
" Cannot parse original message from bounce message %s %s %s %s " ,
2020-11-04 15:38:26 +01:00
alias ,
user ,
2021-01-11 14:55:55 +01:00
contact ,
full_report_path ,
)
else :
file_path = f " refused-emails/ { random_name } .eml "
2021-01-11 15:25:54 +01:00
s3 . upload_email_from_bytesio (
2022-04-28 14:43:24 +02:00
file_path , BytesIO ( message_to_bytes ( orig_msg ) ) , random_name
2021-01-11 15:25:54 +01:00
)
2021-01-11 14:55:55 +01:00
refused_email = RefusedEmail . create (
path = file_path , full_report_path = full_report_path , user_id = user . id
)
2021-10-12 14:36:47 +02:00
Session . flush ( )
2021-01-11 14:55:55 +01:00
LOG . d ( " Create refused email %s " , refused_email )
email_log . bounced = True
email_log . refused_email_id = refused_email . id
email_log . bounced_mailbox_id = mailbox . id
2021-10-12 14:36:47 +02:00
Session . commit ( )
2021-01-11 14:55:55 +01:00
refused_email_url = f " { URL } /dashboard/refused_email?highlight_id= { email_log . id } "
2022-03-07 15:44:27 +01:00
alias_will_be_disabled , reason = should_disable ( alias )
2022-03-07 15:45:36 +01:00
if alias_will_be_disabled :
LOG . w (
f " Disable alias { alias } because { reason } . { alias . mailboxes } { alias . user } . Last contact { contact } "
)
alias . enabled = False
2022-03-09 17:59:02 +01:00
Notification . create (
user_id = user . id ,
title = f " { alias . email } has been disabled due to multiple bounces " ,
message = Notification . render (
" notification/alias-disable.html " , alias = alias , mailbox = mailbox
) ,
)
2022-03-10 08:33:26 +01:00
Session . commit ( )
2022-03-07 15:45:36 +01:00
send_email_with_rate_control (
user ,
ALERT_BOUNCE_EMAIL ,
user . email ,
f " Alias { alias . email } has been disabled due to multiple bounces " ,
render (
" transactional/bounce/automatic-disable-alias.txt " ,
alias = alias ,
refused_email_url = refused_email_url ,
mailbox_email = mailbox . email ,
) ,
render (
" transactional/bounce/automatic-disable-alias.html " ,
alias = alias ,
refused_email_url = refused_email_url ,
mailbox_email = mailbox . email ,
) ,
max_nb_alert = 10 ,
ignore_smtp_error = True ,
)
else :
2021-01-11 14:55:55 +01:00
LOG . d (
" Inform user %s about a bounce from contact %s to alias %s " ,
user ,
contact ,
alias ,
)
disable_alias_link = f " { URL } /dashboard/unsubscribe/ { alias . id } "
2021-10-28 18:41:36 +02:00
block_sender_link = f " { URL } /dashboard/alias_contact_manager/ { alias . id } ?highlight_contact_id= { contact . id } "
2022-01-24 16:10:36 +01:00
Notification . create (
user_id = user . id ,
title = f " Email from { contact . website_email } to { alias . email } cannot be delivered to { mailbox . email } " ,
message = Notification . render (
" notification/bounce-forward-phase.html " ,
alias = alias ,
website_email = contact . website_email ,
disable_alias_link = disable_alias_link ,
refused_email_url = refused_email . get_url ( ) ,
mailbox_email = mailbox . email ,
block_sender_link = block_sender_link ,
) ,
commit = True ,
)
2021-01-11 14:55:55 +01:00
send_email_with_rate_control (
user ,
ALERT_BOUNCE_EMAIL ,
user . email ,
2022-04-18 09:30:29 +02:00
f " An email sent to { alias . email } cannot be delivered to your mailbox " ,
2021-01-11 14:55:55 +01:00
render (
2021-10-28 11:43:44 +02:00
" transactional/bounce/bounced-email.txt.jinja2 " ,
2021-01-11 14:55:55 +01:00
alias = alias ,
website_email = contact . website_email ,
disable_alias_link = disable_alias_link ,
2021-10-28 11:43:44 +02:00
block_sender_link = block_sender_link ,
2021-01-11 14:55:55 +01:00
refused_email_url = refused_email_url ,
mailbox_email = mailbox . email ,
) ,
render (
" transactional/bounce/bounced-email.html " ,
alias = alias ,
website_email = contact . website_email ,
disable_alias_link = disable_alias_link ,
refused_email_url = refused_email_url ,
mailbox_email = mailbox . email ,
) ,
max_nb_alert = 10 ,
2021-09-20 13:51:16 +02:00
# smtp error can happen if user mailbox is unreachable, that might explain the bounce
ignore_smtp_error = True ,
2021-01-11 14:55:55 +01:00
)
2020-11-04 15:38:26 +01:00
2021-03-06 17:44:31 +01:00
def handle_bounce_reply_phase ( envelope , msg : Message , email_log : EmailLog ) :
2021-01-11 14:55:55 +01:00
"""
Handle reply phase bounce
Happens when an email cannot be sent from an alias to a contact
"""
2021-01-26 09:56:13 +01:00
contact : Contact = email_log . contact
2021-01-11 14:55:55 +01:00
alias = contact . alias
user = alias . user
2021-01-11 15:53:08 +01:00
mailbox = email_log . mailbox or alias . mailbox
2021-09-08 11:29:55 +02:00
LOG . d ( " Handle reply bounce %s -> %s -> %s . %s " , mailbox , alias , contact , email_log )
2021-01-11 14:55:55 +01:00
2021-10-14 15:46:52 +02:00
bounce_info = get_mailbox_bounce_info ( msg )
if bounce_info :
Bounce . create (
2021-11-18 16:44:04 +01:00
email = sanitize_email ( contact . website_email , not_lower = True ) ,
2021-10-19 12:05:35 +02:00
info = bounce_info . as_bytes ( ) . decode ( ) ,
2021-10-14 15:46:52 +02:00
commit = True ,
)
else :
2021-11-02 14:32:16 +01:00
LOG . w ( " cannot get bounce info, debug at %s " , save_email_for_debugging ( msg ) )
2021-11-18 16:44:04 +01:00
Bounce . create (
email = sanitize_email ( contact . website_email , not_lower = True ) , commit = True
)
2021-01-26 09:56:13 +01:00
2020-11-04 12:32:15 +01:00
# Store the bounced email
# generate a name for the email
random_name = str ( uuid . uuid4 ( ) )
full_report_path = f " refused-emails/full- { random_name } .eml "
2022-04-28 14:43:24 +02:00
s3 . upload_email_from_bytesio (
full_report_path , BytesIO ( message_to_bytes ( msg ) ) , random_name
)
2020-11-04 12:32:15 +01:00
orig_msg = get_orig_message_from_bounce ( msg )
2020-11-04 15:38:26 +01:00
file_path = None
if orig_msg :
file_path = f " refused-emails/ { random_name } .eml "
s3 . upload_email_from_bytesio (
2022-04-28 14:43:24 +02:00
file_path , BytesIO ( message_to_bytes ( orig_msg ) ) , random_name
2020-11-04 15:38:26 +01:00
)
2020-11-04 12:32:15 +01:00
2020-11-04 15:38:26 +01:00
refused_email = RefusedEmail . create (
path = file_path , full_report_path = full_report_path , user_id = user . id , commit = True
)
LOG . d ( " Create refused email %s " , refused_email )
2020-11-04 12:32:15 +01:00
2020-11-04 15:38:26 +01:00
email_log . bounced = True
email_log . refused_email_id = refused_email . id
2020-11-04 12:32:15 +01:00
2020-11-24 16:50:55 +01:00
email_log . bounced_mailbox_id = mailbox . id
2021-10-12 14:36:47 +02:00
Session . commit ( )
2020-11-04 12:32:15 +01:00
2020-12-06 13:54:59 +01:00
refused_email_url = f " { URL } /dashboard/refused_email?highlight_id= { email_log . id } "
2020-11-04 12:32:15 +01:00
LOG . d (
" Inform user %s about bounced email sent by %s to %s " ,
user ,
alias ,
contact ,
)
2022-01-24 16:13:45 +01:00
Notification . create (
user_id = user . id ,
title = f " Email cannot be sent to { contact . email } from your alias { alias . email } " ,
message = Notification . render (
" notification/bounce-reply-phase.html " ,
alias = alias ,
contact = contact ,
refused_email_url = refused_email . get_url ( ) ,
) ,
commit = True ,
)
2020-11-04 12:32:15 +01:00
send_email_with_rate_control (
user ,
ALERT_BOUNCE_EMAIL_REPLY_PHASE ,
mailbox . email ,
f " Email cannot be sent to { contact . email } from your alias { alias . email } " ,
render (
2021-01-04 14:22:07 +01:00
" transactional/bounce/bounce-email-reply-phase.txt " ,
2020-11-04 12:32:15 +01:00
alias = alias ,
contact = contact ,
refused_email_url = refused_email_url ,
) ,
render (
2021-01-04 14:22:07 +01:00
" transactional/bounce/bounce-email-reply-phase.html " ,
2020-11-04 12:32:15 +01:00
alias = alias ,
contact = contact ,
refused_email_url = refused_email_url ,
) ,
)
2020-03-30 22:05:31 +02:00
def handle_spam (
contact : Contact ,
alias : Alias ,
msg : Message ,
user : User ,
2020-08-21 10:18:58 +02:00
mailbox : Mailbox ,
2020-04-27 18:18:40 +02:00
email_log : EmailLog ,
2020-08-15 16:53:57 +02:00
is_reply = False , # whether the email is in forward or reply phase
2020-03-30 22:05:31 +02:00
) :
# Store the report & original email
orig_msg = get_orig_message_from_spamassassin_report ( msg )
# generate a name for the email
random_name = str ( uuid . uuid4 ( ) )
2020-04-02 18:09:05 +02:00
full_report_path = f " spams/full- { random_name } .eml "
2022-04-28 14:43:24 +02:00
s3 . upload_email_from_bytesio (
full_report_path , BytesIO ( message_to_bytes ( msg ) ) , random_name
)
2020-03-30 22:05:31 +02:00
file_path = None
if orig_msg :
2020-04-02 18:09:05 +02:00
file_path = f " spams/ { random_name } .eml "
2020-03-30 22:05:31 +02:00
s3 . upload_email_from_bytesio (
2022-04-28 14:43:24 +02:00
file_path , BytesIO ( message_to_bytes ( orig_msg ) ) , random_name
2020-03-30 22:05:31 +02:00
)
refused_email = RefusedEmail . create (
path = file_path , full_report_path = full_report_path , user_id = user . id
)
2021-10-12 14:36:47 +02:00
Session . flush ( )
2020-03-30 22:05:31 +02:00
email_log . refused_email_id = refused_email . id
2021-10-12 14:36:47 +02:00
Session . commit ( )
2020-03-30 22:05:31 +02:00
LOG . d ( " Create spam email %s " , refused_email )
2020-12-06 13:54:59 +01:00
refused_email_url = f " { URL } /dashboard/refused_email?highlight_id= { email_log . id } "
2020-03-30 22:05:31 +02:00
disable_alias_link = f " { URL } /dashboard/unsubscribe/ { alias . id } "
2020-08-15 16:53:57 +02:00
if is_reply :
LOG . d (
2020-12-02 12:40:29 +01:00
" Inform %s ( %s ) about spam email sent from alias %s to %s . %s " ,
2020-08-21 10:20:08 +02:00
mailbox ,
2020-08-15 16:53:57 +02:00
user ,
alias ,
contact ,
2020-12-02 12:40:29 +01:00
refused_email ,
2020-08-15 16:53:57 +02:00
)
send_email_with_rate_control (
user ,
ALERT_SPAM_EMAIL ,
2020-08-21 10:18:58 +02:00
mailbox . email ,
2020-11-18 16:16:37 +01:00
f " Email from { alias . email } to { contact . website_email } is detected as spam " ,
2020-08-15 16:53:57 +02:00
render (
" transactional/spam-email-reply-phase.txt " ,
alias = alias ,
website_email = contact . website_email ,
disable_alias_link = disable_alias_link ,
refused_email_url = refused_email_url ,
) ,
render (
" transactional/spam-email-reply-phase.html " ,
alias = alias ,
website_email = contact . website_email ,
disable_alias_link = disable_alias_link ,
refused_email_url = refused_email_url ,
) ,
)
else :
# inform user
LOG . d (
2020-08-21 10:20:08 +02:00
" Inform %s ( %s ) about spam email sent by %s to alias %s " ,
mailbox ,
2020-08-15 16:53:57 +02:00
user ,
contact ,
alias ,
)
send_email_with_rate_control (
user ,
ALERT_SPAM_EMAIL ,
2020-08-21 10:18:58 +02:00
mailbox . email ,
2020-08-15 16:53:57 +02:00
f " Email from { contact . website_email } to { alias . email } is detected as spam " ,
render (
" transactional/spam-email.txt " ,
alias = alias ,
website_email = contact . website_email ,
disable_alias_link = disable_alias_link ,
refused_email_url = refused_email_url ,
) ,
render (
" transactional/spam-email.html " ,
alias = alias ,
website_email = contact . website_email ,
disable_alias_link = disable_alias_link ,
refused_email_url = refused_email_url ,
) ,
)
2020-03-30 22:05:31 +02:00
2022-01-04 18:06:08 +01:00
def is_automatic_out_of_office ( msg : Message ) - > bool :
2022-01-05 15:21:54 +01:00
"""
Return whether an email is out - of - office
For info , out - of - office is sent to the envelope mail_from and not the From : header
More info on https : / / datatracker . ietf . org / doc / html / rfc3834 #section-4 and https://support.google.com/mail/thread/21246740/my-auto-reply-filter-isn-t-replying-to-original-sender-address?hl=en&msgid=21261237
"""
if msg [ headers . AUTO_SUBMITTED ] is None :
return False
2022-01-04 18:06:08 +01:00
2022-01-05 15:21:54 +01:00
if msg [ headers . AUTO_SUBMITTED ] . lower ( ) in ( " auto-replied " , " auto-generated " ) :
2022-01-04 18:06:08 +01:00
LOG . d (
2022-01-05 15:21:54 +01:00
" out-of-office email %s : %s " ,
headers . AUTO_SUBMITTED ,
msg [ headers . AUTO_SUBMITTED ] ,
2022-01-04 18:06:08 +01:00
)
return True
return False
2022-01-05 15:22:22 +01:00
def is_bounce ( envelope : Envelope , msg : Message ) :
""" Detect whether an email is a Delivery Status Notification """
return (
envelope . mail_from == " <> "
and msg . get_content_type ( ) . lower ( ) == " multipart/report "
)
2022-03-25 18:14:31 +01:00
def handle_transactional_bounce (
envelope : Envelope , msg , rcpt_to , transactional_id = None
) :
2021-01-26 09:59:08 +01:00
LOG . d ( " handle transactional bounce sent to %s " , rcpt_to )
2020-06-10 13:57:23 +02:00
2021-01-26 09:59:08 +01:00
# parse the TransactionalEmail
2022-03-25 18:14:31 +01:00
transactional_id = transactional_id or parse_id_from_bounce ( rcpt_to )
2021-01-26 09:59:08 +01:00
transactional = TransactionalEmail . get ( transactional_id )
2020-06-10 13:57:23 +02:00
2021-02-06 16:00:32 +01:00
# a transaction might have been deleted in delete_logs()
2021-01-26 09:59:08 +01:00
if transactional :
2021-03-16 09:17:23 +01:00
LOG . i ( " Create bounce for %s " , transactional . email )
2021-10-14 15:46:52 +02:00
bounce_info = get_mailbox_bounce_info ( msg )
if bounce_info :
Bounce . create (
2021-10-19 12:05:35 +02:00
email = transactional . email ,
info = bounce_info . as_bytes ( ) . decode ( ) ,
commit = True ,
2021-10-14 15:46:52 +02:00
)
else :
2021-11-02 14:32:16 +01:00
LOG . w ( " cannot get bounce info, debug at %s " , save_email_for_debugging ( msg ) )
2021-10-14 15:46:52 +02:00
Bounce . create ( email = transactional . email , commit = True )
2020-06-10 13:57:23 +02:00
2021-06-02 11:38:25 +02:00
def handle_bounce ( envelope , email_log : EmailLog , msg : Message ) - > str :
2021-03-17 10:09:54 +01:00
"""
Return SMTP status , e . g . " 500 Error "
"""
if not email_log :
2021-06-02 11:38:25 +02:00
LOG . w ( " No such email log " )
2021-06-23 19:47:06 +02:00
return status . E512
2021-03-17 10:09:54 +01:00
2021-06-02 11:38:25 +02:00
contact : Contact = email_log . contact
alias = contact . alias
LOG . d (
" handle bounce for %s , phase= %s , contact= %s , alias= %s " ,
email_log ,
email_log . get_phase ( ) ,
contact ,
alias ,
)
2021-03-17 10:09:54 +01:00
if email_log . is_reply :
content_type = msg . get_content_type ( ) . lower ( )
if content_type != " multipart/report " or envelope . mail_from != " <> " :
# forward the email again to the alias
2021-05-30 20:02:41 +02:00
LOG . i (
" Handle auto reply %s %s " ,
2021-03-17 10:09:54 +01:00
content_type ,
envelope . mail_from ,
)
contact : Contact = email_log . contact
alias = contact . alias
email_log . auto_replied = True
2021-10-12 14:36:47 +02:00
Session . commit ( )
2021-03-17 10:09:54 +01:00
# replace the BOUNCE_EMAIL by alias in To field
add_or_replace_header ( msg , " To " , alias . email )
envelope . rcpt_tos = [ alias . email ]
# same as handle()
# result of all deliveries
# each element is a couple of whether the delivery is successful and the smtp status
res : [ ( bool , str ) ] = [ ]
for is_delivered , smtp_status in handle_forward ( envelope , msg , alias . email ) :
res . append ( ( is_delivered , smtp_status ) )
for ( is_success , smtp_status ) in res :
# Consider all deliveries successful if 1 delivery is successful
if is_success :
return smtp_status
# Failed delivery for all, return the first failure
return res [ 0 ] [ 1 ]
2021-11-13 11:21:19 +01:00
handle_bounce_reply_phase ( envelope , msg , email_log )
return status . E212
2021-03-17 10:09:54 +01:00
else : # forward phase
handle_bounce_forward_phase ( msg , email_log )
2021-11-13 11:21:19 +01:00
return status . E211
2021-03-17 10:09:54 +01:00
2021-06-22 17:52:24 +02:00
def should_ignore ( mail_from : str , rcpt_tos : List [ str ] ) - > bool :
if len ( rcpt_tos ) != 1 :
return False
rcpt_to = rcpt_tos [ 0 ]
if IgnoredEmail . get_by ( mail_from = mail_from , rcpt_to = rcpt_to ) :
return True
return False
2022-02-16 18:38:31 +01:00
def send_no_reply_response ( mail_from : str , msg : Message ) :
2022-02-17 14:33:04 +01:00
mailbox = Mailbox . get_by ( email = mail_from )
2022-02-21 12:30:26 +01:00
if not mailbox :
2022-02-16 18:39:18 +01:00
LOG . d ( " Unknown sender. Skipping reply from {} " . format ( NOREPLY ) )
2022-02-17 14:33:04 +01:00
return
send_email_at_most_times (
2022-02-21 12:30:26 +01:00
mailbox . user ,
2022-02-17 14:33:04 +01:00
ALERT_TO_NOREPLY ,
2022-02-21 12:30:26 +01:00
mailbox . user . email ,
2022-02-17 14:33:04 +01:00
" Auto: {} " . format ( msg [ headers . SUBJECT ] or " No subject " ) ,
render ( " transactional/noreply.text.jinja2 " ) ,
)
2022-02-16 18:38:31 +01:00
2022-01-07 14:26:58 +01:00
def handle ( envelope : Envelope , msg : Message ) - > str :
2020-04-04 16:09:24 +02:00
""" Return SMTP status """
2020-09-14 17:19:29 +02:00
# sanitize mail_from, rcpt_tos
2021-01-11 12:27:02 +01:00
mail_from = sanitize_email ( envelope . mail_from )
rcpt_tos = [ sanitize_email ( rcpt_to ) for rcpt_to in envelope . rcpt_tos ]
2020-09-14 17:19:29 +02:00
envelope . mail_from = mail_from
envelope . rcpt_tos = rcpt_tos
2022-04-05 11:56:45 +02:00
# some emails don't have this header, set the default value (7bit) in this case
if headers . CONTENT_TRANSFER_ENCODING not in msg :
LOG . i ( " Set CONTENT_TRANSFER_ENCODING " )
msg [ headers . CONTENT_TRANSFER_ENCODING ] = " 7bit "
2021-06-04 17:15:59 +02:00
postfix_queue_id = get_queue_id ( msg )
if postfix_queue_id :
set_message_id ( postfix_queue_id )
2021-06-23 18:19:13 +02:00
else :
2021-10-15 10:37:22 +02:00
LOG . d (
" Cannot parse Postfix queue ID from %s %s " ,
msg . get_all ( headers . RECEIVED ) ,
msg [ headers . RECEIVED ] ,
)
2021-06-23 18:19:13 +02:00
if should_ignore ( mail_from , rcpt_tos ) :
2021-06-23 19:47:51 +02:00
LOG . w ( " Ignore email mail_from= %s rcpt_to= %s " , mail_from , rcpt_tos )
2021-06-23 19:47:06 +02:00
return status . E204
2021-03-17 10:19:27 +01:00
# sanitize email headers
2021-03-17 10:59:13 +01:00
sanitize_header ( msg , " from " )
sanitize_header ( msg , " to " )
sanitize_header ( msg , " cc " )
sanitize_header ( msg , " reply-to " )
2021-03-17 10:19:27 +01:00
2021-03-16 09:17:23 +01:00
LOG . d (
" ==>> Handle mail_from: %s , rcpt_tos: %s , header_from: %s , header_to: %s , "
2022-03-16 10:25:28 +01:00
" cc: %s , reply-to: %s , message_id: %s , client_ip: %s , headers: %s , mail_options: %s , rcpt_options: %s " ,
2021-03-16 09:17:23 +01:00
mail_from ,
rcpt_tos ,
2021-10-11 12:10:18 +02:00
msg [ headers . FROM ] ,
msg [ headers . TO ] ,
msg [ headers . CC ] ,
msg [ headers . REPLY_TO ] ,
2021-10-19 12:05:41 +02:00
msg [ headers . MESSAGE_ID ] ,
2022-03-16 10:25:28 +01:00
msg [ headers . SL_CLIENT_IP ] ,
msg . _headers ,
2021-03-16 09:17:23 +01:00
envelope . mail_options ,
envelope . rcpt_options ,
)
2022-01-07 16:14:21 +01:00
# region mail_from or from_header is a reverse alias which should never happen
2022-01-08 00:42:03 +01:00
email_sent_from_reverse_alias = False
2021-03-15 19:55:22 +01:00
contact = Contact . get_by ( reply_email = mail_from )
if contact :
2022-01-08 00:42:03 +01:00
email_sent_from_reverse_alias = True
2022-01-07 16:14:21 +01:00
from_header = get_header_unicode ( msg [ headers . FROM ] )
if from_header :
try :
_ , from_header_address = parse_full_address ( from_header )
except ValueError :
2022-01-08 00:43:49 +01:00
LOG . w ( " cannot parse the From header %s " , from_header )
2022-01-07 16:14:21 +01:00
else :
contact = Contact . get_by ( reply_email = from_header_address )
if contact :
2022-01-08 00:42:03 +01:00
email_sent_from_reverse_alias = True
if email_sent_from_reverse_alias :
LOG . w ( f " email sent from reverse alias { contact } { contact . alias } { contact . user } " )
user = contact . user
send_email_at_most_times (
user ,
ALERT_FROM_ADDRESS_IS_REVERSE_ALIAS ,
user . email ,
" SimpleLogin shouldn ' t be used with another email forwarding system " ,
render (
" transactional/email-sent-from-reverse-alias.txt.jinja2 " ,
) ,
)
2022-01-07 16:14:21 +01:00
# endregion
2021-03-15 19:55:22 +01:00
2020-04-04 16:09:24 +02:00
# unsubscribe request
2021-11-22 18:17:07 +01:00
if UNSUBSCRIBER and ( rcpt_tos == [ UNSUBSCRIBER ] or rcpt_tos == [ OLD_UNSUBSCRIBER ] ) :
2020-09-14 17:19:29 +02:00
LOG . d ( " Handle unsubscribe request from %s " , mail_from )
2022-06-30 11:40:01 +02:00
return UnsubscribeHandler ( ) . handle_unsubscribe_from_message ( envelope , msg )
2020-04-04 16:09:24 +02:00
2022-01-07 16:14:21 +01:00
# region mail sent to VERP
2022-03-25 18:14:31 +01:00
verp_info = get_verp_info_from_email ( rcpt_tos [ 0 ] )
2022-01-07 12:18:46 +01:00
# sent to transactional VERP. Either bounce emails or out-of-office
2021-01-26 09:59:08 +01:00
if (
len ( rcpt_tos ) == 1
and rcpt_tos [ 0 ] . startswith ( TRANSACTIONAL_BOUNCE_PREFIX )
and rcpt_tos [ 0 ] . endswith ( TRANSACTIONAL_BOUNCE_SUFFIX )
2022-03-25 18:14:31 +01:00
) or ( verp_info and verp_info [ 0 ] == VerpType . transactional ) :
2022-01-05 15:30:44 +01:00
if is_bounce ( envelope , msg ) :
2022-03-25 18:14:31 +01:00
handle_transactional_bounce (
envelope , msg , rcpt_tos [ 0 ] , verp_info and verp_info [ 1 ]
)
2022-01-05 15:30:44 +01:00
return status . E205
elif is_automatic_out_of_office ( msg ) :
LOG . d (
" Ignore out-of-office for transactional emails. Headers: %s " , msg . items
)
return status . E206
else :
2022-01-07 14:57:47 +01:00
raise VERPTransactional
2020-06-10 13:57:23 +02:00
2022-01-07 12:18:46 +01:00
# sent to forward VERP, can be either bounce or out-of-office
2021-01-11 14:55:55 +01:00
if (
len ( rcpt_tos ) == 1
and rcpt_tos [ 0 ] . startswith ( BOUNCE_PREFIX )
and rcpt_tos [ 0 ] . endswith ( BOUNCE_SUFFIX )
2022-03-25 18:14:31 +01:00
) or ( verp_info and verp_info [ 0 ] == VerpType . bounce_forward ) :
email_log_id = ( verp_info and verp_info [ 1 ] ) or parse_id_from_bounce ( rcpt_tos [ 0 ] )
2021-06-02 11:38:25 +02:00
email_log = EmailLog . get ( email_log_id )
2021-05-25 17:59:09 +02:00
2022-01-05 15:30:44 +01:00
if not email_log :
LOG . w ( " No such email log " )
return status . E512
if is_bounce ( envelope , msg ) :
2022-01-05 09:50:58 +01:00
return handle_bounce ( envelope , email_log , msg )
2022-01-05 15:30:44 +01:00
elif is_automatic_out_of_office ( msg ) :
2022-01-07 12:24:14 +01:00
handle_out_of_office_forward_phase ( email_log , envelope , msg , rcpt_tos )
2022-01-05 15:30:44 +01:00
else :
2022-01-07 14:57:47 +01:00
raise VERPForward
2022-01-05 09:50:58 +01:00
2022-01-07 12:18:46 +01:00
# sent to reply VERP, can be either bounce or out-of-office
2022-03-25 18:14:31 +01:00
if (
len ( rcpt_tos ) == 1
and rcpt_tos [ 0 ] . startswith ( f " { BOUNCE_PREFIX_FOR_REPLY_PHASE } + " )
or ( verp_info and verp_info [ 0 ] == VerpType . bounce_reply )
2021-05-25 17:59:09 +02:00
) :
2022-03-25 18:14:31 +01:00
email_log_id = ( verp_info and verp_info [ 1 ] ) or parse_id_from_bounce ( rcpt_tos [ 0 ] )
2021-06-02 11:38:25 +02:00
email_log = EmailLog . get ( email_log_id )
2022-01-05 09:50:58 +01:00
2022-01-05 15:30:44 +01:00
if not email_log :
LOG . w ( " No such email log " )
return status . E512
2022-01-06 14:35:16 +01:00
# bounce by contact
2022-01-05 15:30:44 +01:00
if is_bounce ( envelope , msg ) :
return handle_bounce ( envelope , email_log , msg )
elif is_automatic_out_of_office ( msg ) :
2022-01-07 12:24:14 +01:00
handle_out_of_office_reply_phase ( email_log , envelope , msg , rcpt_tos )
2022-01-05 15:30:44 +01:00
else :
2022-01-07 14:57:47 +01:00
raise VERPReply (
f " cannot handle email sent to reply VERP, "
f " { email_log . alias } -> { email_log . contact } ( { email_log } , { email_log . user } "
2022-01-05 15:30:44 +01:00
)
2021-01-11 14:55:55 +01:00
2021-06-02 11:46:00 +02:00
# iCloud returns the bounce with mail_from=bounce+{email_log_id}+@simplelogin.co, rcpt_to=alias
2022-03-25 18:14:31 +01:00
verp_info = get_verp_info_from_email ( mail_from [ 0 ] )
2021-06-02 16:27:48 +02:00
if (
len ( rcpt_tos ) == 1
and mail_from . startswith ( BOUNCE_PREFIX )
and mail_from . endswith ( BOUNCE_SUFFIX )
2022-03-25 18:14:31 +01:00
) or ( verp_info and verp_info [ 0 ] == VerpType . bounce_forward ) :
email_log_id = ( verp_info and verp_info [ 1 ] ) or parse_id_from_bounce ( mail_from )
2021-06-02 11:46:00 +02:00
email_log = EmailLog . get ( email_log_id )
alias = Alias . get_by ( email = rcpt_tos [ 0 ] )
2021-06-04 15:23:48 +02:00
LOG . w (
2022-04-05 11:52:43 +02:00
" iCloud bounces %s %s , saved to %s " ,
2021-06-02 11:46:00 +02:00
email_log ,
alias ,
2022-04-05 11:52:43 +02:00
save_email_for_debugging ( msg , file_name_prefix = " icloud_bounce_ " ) ,
2021-06-02 11:46:00 +02:00
)
return handle_bounce ( envelope , email_log , msg )
2022-01-07 12:18:46 +01:00
# endregion
2022-01-07 16:14:21 +01:00
# region hotmail, yahoo complaints
2022-01-07 12:18:46 +01:00
if (
len ( rcpt_tos ) == 1
and mail_from == " staff@hotmail.com "
and rcpt_tos [ 0 ] == POSTMASTER
) :
LOG . w ( " Handle hotmail complaint " )
# if the complaint cannot be handled, forward it normally
if handle_hotmail_complaint ( msg ) :
return status . E208
if (
len ( rcpt_tos ) == 1
and mail_from == " feedback@arf.mail.yahoo.com "
and rcpt_tos [ 0 ] == POSTMASTER
) :
LOG . w ( " Handle yahoo complaint " )
# if the complaint cannot be handled, forward it normally
if handle_yahoo_complaint ( msg ) :
return status . E210
2022-01-07 16:14:21 +01:00
# endregion
2021-09-22 09:58:40 +02:00
2021-06-24 09:47:01 +02:00
if rate_limited ( mail_from , rcpt_tos ) :
2021-10-13 10:27:54 +02:00
LOG . w ( " Rate Limiting applied for mail_from: %s rcpt_tos: %s " , mail_from , rcpt_tos )
# add more logging info. TODO: remove
if len ( rcpt_tos ) == 1 :
alias = Alias . get_by ( email = rcpt_tos [ 0 ] )
if alias :
LOG . w (
" total number email log on %s , %s is %s , %s " ,
alias ,
alias . user ,
EmailLog . filter ( EmailLog . alias_id == alias . id ) . count ( ) ,
EmailLog . filter ( EmailLog . user_id == alias . user_id ) . count ( ) ,
)
2021-10-12 14:53:30 +02:00
2021-10-13 10:27:54 +02:00
if should_ignore_bounce ( envelope . mail_from ) :
return status . E207
else :
return status . E522
2020-04-04 16:27:22 +02:00
2022-01-05 15:20:17 +01:00
# Handle "out-of-office" auto notice, i.e. an automatic response is sent for every forwarded email
2021-10-19 12:14:16 +02:00
if len ( rcpt_tos ) == 1 and is_reverse_alias ( rcpt_tos [ 0 ] ) and mail_from == " <> " :
2022-01-05 15:20:17 +01:00
contact = Contact . get_by ( reply_email = rcpt_tos [ 0 ] )
2021-05-30 20:02:41 +02:00
LOG . w (
2022-01-05 17:43:11 +01:00
" out-of-office email to reverse alias %s . Saved to %s " ,
2022-01-05 15:20:17 +01:00
contact ,
save_email_for_debugging ( msg ) , # todo: remove
2021-05-30 20:02:41 +02:00
)
2021-06-23 19:47:06 +02:00
return status . E206
2021-05-30 19:58:08 +02:00
2020-04-04 16:09:24 +02:00
# result of all deliveries
# each element is a couple of whether the delivery is successful and the smtp status
res : [ ( bool , str ) ] = [ ]
2021-05-06 17:08:30 +02:00
nb_rcpt_tos = len ( rcpt_tos )
for rcpt_index , rcpt_to in enumerate ( rcpt_tos ) :
2022-04-21 08:59:46 +02:00
if rcpt_to in config . NOREPLIES :
2022-02-16 18:38:31 +01:00
LOG . i ( " email sent to {} address from {} " . format ( NOREPLY , mail_from ) )
send_no_reply_response ( mail_from , msg )
return status . E200
2020-11-14 15:55:53 +01:00
2021-05-06 17:08:30 +02:00
# create a copy of msg for each recipient except the last one
# as copy() is a slow function
if rcpt_index < nb_rcpt_tos - 1 :
LOG . d ( " copy message for rcpt %s " , rcpt_to )
copy_msg = copy ( msg )
else :
copy_msg = msg
2021-12-30 10:24:57 +01:00
# Reply case: the recipient is a reverse alias. Used to start with "reply+" or "ra+"
2021-10-19 12:14:16 +02:00
if is_reverse_alias ( rcpt_to ) :
2021-10-11 12:10:18 +02:00
LOG . d (
" Reply phase %s ( %s ) -> %s " , mail_from , copy_msg [ headers . FROM ] , rcpt_to
)
2021-05-06 17:08:30 +02:00
is_delivered , smtp_status = handle_reply ( envelope , copy_msg , rcpt_to )
2020-04-04 16:09:24 +02:00
res . append ( ( is_delivered , smtp_status ) )
else : # Forward case
2021-03-16 09:17:23 +01:00
LOG . d (
2020-11-04 19:42:20 +01:00
" Forward phase %s ( %s ) -> %s " ,
2020-09-14 17:19:29 +02:00
mail_from ,
2021-10-11 12:10:18 +02:00
copy_msg [ headers . FROM ] ,
2020-07-23 10:32:10 +02:00
rcpt_to ,
)
2021-05-06 17:08:30 +02:00
for is_delivered , smtp_status in handle_forward (
envelope , copy_msg , rcpt_to
) :
2020-05-10 16:57:47 +02:00
res . append ( ( is_delivered , smtp_status ) )
2020-04-04 16:09:24 +02:00
2022-03-11 08:56:27 +01:00
# to know whether both successful and unsuccessful deliveries can happen at the same time
nb_success = len ( [ is_success for ( is_success , smtp_status ) in res if is_success ] )
2022-05-17 18:15:39 +02:00
# ignore E518 which is a normal condition
2022-03-11 08:56:27 +01:00
nb_non_success = len (
2022-05-17 18:15:39 +02:00
[
is_success
for ( is_success , smtp_status ) in res
if not is_success and smtp_status != status . E518
]
2022-03-11 08:56:27 +01:00
)
2022-03-16 09:05:57 +01:00
if nb_success > 0 and nb_non_success > 0 :
LOG . e ( f " some deliveries fail and some success, { mail_from } , { rcpt_tos } , { res } " )
2022-03-11 08:56:27 +01:00
2020-04-04 16:09:24 +02:00
for ( is_success , smtp_status ) in res :
# Consider all deliveries successful if 1 delivery is successful
if is_success :
return smtp_status
# Failed delivery for all, return the first failure
return res [ 0 ] [ 1 ]
2022-01-07 12:24:14 +01:00
def handle_out_of_office_reply_phase ( email_log , envelope , msg , rcpt_tos ) :
""" convert the email into a normal email sent to the alias, so it can be forwarded to mailbox """
LOG . d (
" send the out-of-office email to the alias %s , old to_header: %s rcpt_tos: %s , %s " ,
email_log . alias ,
msg [ headers . TO ] ,
rcpt_tos ,
email_log ,
)
alias_address = email_log . alias . email
rcpt_tos [ 0 ] = alias_address
envelope . rcpt_tos = [ alias_address ]
add_or_replace_header ( msg , headers . TO , alias_address )
# delete reply-to header that can affect email delivery
delete_header ( msg , headers . REPLY_TO )
LOG . d (
" after out-of-office transformation to_header: %s reply_to: %s rcpt_tos: %s " ,
msg . get_all ( headers . TO ) ,
msg . get_all ( headers . REPLY_TO ) ,
rcpt_tos ,
)
def handle_out_of_office_forward_phase ( email_log , envelope , msg , rcpt_tos ) :
""" convert the email into a normal email sent to the reverse alias, so it can be forwarded to contact """
LOG . d (
" send the out-of-office email to the contact %s , old to_header: %s rcpt_tos: %s %s " ,
email_log . contact ,
msg [ headers . TO ] ,
rcpt_tos ,
email_log ,
)
reverse_alias = email_log . contact . reply_email
rcpt_tos [ 0 ] = reverse_alias
envelope . rcpt_tos = [ reverse_alias ]
add_or_replace_header ( msg , headers . TO , reverse_alias )
# delete reply-to header that can affect email delivery
delete_header ( msg , headers . REPLY_TO )
LOG . d (
" after out-of-office transformation to_header: %s reply_to: %s rcpt_tos: %s " ,
msg . get_all ( headers . TO ) ,
msg . get_all ( headers . REPLY_TO ) ,
rcpt_tos ,
)
2020-02-19 16:17:13 +01:00
class MailHandler :
2020-04-02 18:10:08 +02:00
async def handle_DATA ( self , server , session , envelope : Envelope ) :
2022-01-07 14:57:47 +01:00
msg = email . message_from_bytes ( envelope . original_content )
2020-08-17 11:40:58 +02:00
try :
2022-01-07 14:26:58 +01:00
ret = self . _handle ( envelope , msg )
2020-08-17 11:40:58 +02:00
return ret
2022-01-08 00:16:16 +01:00
# happen if reverse-alias is used during the forward phase
# as in this case, a new reverse-alias needs to be created for this reverse-alias -> chaos
except CannotCreateContactForReverseAlias as e :
LOG . w (
" Probably due to reverse-alias used in the forward phase, "
" error: %s mail_from: %s , rcpt_tos: %s , header_from: %s , header_to: %s " ,
e ,
envelope . mail_from ,
envelope . rcpt_tos ,
msg [ headers . FROM ] ,
msg [ headers . TO ] ,
)
return status . E524
2022-04-08 11:06:01 +02:00
except ( VERPReply , VERPForward , VERPTransactional ) as e :
2022-01-09 20:35:57 +01:00
LOG . w (
" email handling fail with error: %s "
" mail_from: %s , rcpt_tos: %s , header_from: %s , header_to: %s " ,
e ,
envelope . mail_from ,
envelope . rcpt_tos ,
msg [ headers . FROM ] ,
msg [ headers . TO ] ,
)
return status . E213
2022-01-07 14:57:47 +01:00
except Exception as e :
2021-06-23 19:57:21 +02:00
LOG . e (
2022-01-08 16:58:23 +01:00
" email handling fail with error: %s "
" mail_from: %s , rcpt_tos: %s , header_from: %s , header_to: %s , saved to %s " ,
2022-01-07 14:57:47 +01:00
e ,
2020-08-27 10:20:48 +02:00
envelope . mail_from ,
envelope . rcpt_tos ,
2022-01-07 14:26:58 +01:00
msg [ headers . FROM ] ,
msg [ headers . TO ] ,
2022-04-06 17:31:46 +02:00
save_envelope_for_debugging (
envelope , file_name_prefix = e . __class__ . __name__
2022-01-08 16:58:23 +01:00
) , # todo: remove
2020-08-17 11:40:58 +02:00
)
2021-06-23 19:47:06 +02:00
return status . E404
2020-08-16 11:10:01 +02:00
2021-12-30 14:00:28 +01:00
@newrelic.agent.background_task ( )
2022-01-07 14:26:58 +01:00
def _handle ( self , envelope : Envelope , msg : Message ) :
2020-09-30 11:05:21 +02:00
start = time . time ( )
2021-05-06 17:20:33 +02:00
# generate a different message_id to keep track of an email lifecycle
message_id = str ( uuid . uuid4 ( ) )
set_message_id ( message_id )
2022-01-05 16:26:31 +01:00
LOG . d ( " ====>=====>====>====>====>====>====>====> " )
2021-03-16 09:17:23 +01:00
LOG . i (
2022-01-05 16:26:31 +01:00
" New message, mail from %s , rctp tos %s " ,
2020-09-30 11:05:21 +02:00
envelope . mail_from ,
envelope . rcpt_tos ,
)
2022-01-08 00:11:16 +01:00
newrelic . agent . record_custom_metric (
" Custom/nb_rcpt_tos " , len ( envelope . rcpt_tos )
)
2019-12-17 20:43:31 +01:00
2021-10-26 10:52:28 +02:00
with create_light_app ( ) . app_context ( ) :
2022-03-29 15:09:10 +02:00
return_status = handle ( envelope , msg )
2021-10-13 10:27:59 +02:00
elapsed = time . time ( ) - start
2022-03-30 09:53:35 +02:00
# Only bounce messages if the return-path passes the spf check. Otherwise black-hole it.
2022-04-07 19:17:37 +02:00
spamd_result = SpamdResult . extract_from_headers ( msg )
2022-03-29 15:09:10 +02:00
if return_status [ 0 ] == " 5 " :
2022-04-07 19:17:37 +02:00
if spamd_result and spamd_result . spf in (
2022-03-29 15:09:10 +02:00
SPFCheckResult . fail ,
SPFCheckResult . soft_fail ,
) :
LOG . i (
" Replacing 5XX to 216 status because the return-path failed the spf check "
)
return_status = status . E216
2021-12-29 16:30:12 +01:00
2022-01-17 14:42:27 +01:00
LOG . i (
2022-03-29 10:52:11 +02:00
" Finish mail_from %s , rcpt_tos %s , takes %s seconds with return code ' %s ' <<=== " ,
2021-10-13 10:27:59 +02:00
envelope . mail_from ,
envelope . rcpt_tos ,
elapsed ,
2022-03-29 15:09:10 +02:00
return_status ,
2021-10-13 10:27:59 +02:00
)
2022-04-07 19:17:37 +02:00
SpamdResult . send_to_new_relic ( msg )
2021-12-30 14:00:28 +01:00
newrelic . agent . record_custom_metric ( " Custom/email_handler_time " , elapsed )
newrelic . agent . record_custom_metric ( " Custom/number_incoming_email " , 1 )
2022-03-29 15:09:10 +02:00
return return_status
2019-11-07 17:49:26 +01:00
2020-09-30 11:05:21 +02:00
def main ( port : int ) :
""" Use aiosmtpd Controller """
controller = Controller ( MailHandler ( ) , hostname = " 0.0.0.0 " , port = port )
2020-09-02 17:36:11 +02:00
2020-09-30 11:05:21 +02:00
controller . start ( )
LOG . d ( " Start mail controller %s %s " , controller . hostname , controller . port )
2020-09-02 17:36:11 +02:00
2020-09-30 11:05:21 +02:00
if LOAD_PGP_EMAIL_HANDLER :
2021-03-16 09:17:23 +01:00
LOG . w ( " LOAD PGP keys " )
2021-10-12 14:47:01 +02:00
load_pgp_public_keys ( )
2020-09-30 11:05:21 +02:00
while True :
time . sleep ( 2 )
if __name__ == " __main__ " :
parser = argparse . ArgumentParser ( )
parser . add_argument (
" -p " , " --port " , help = " SMTP port to listen for " , type = int , default = 20381
)
args = parser . parse_args ( )
2021-03-16 09:17:23 +01:00
LOG . i ( " Listen for port %s " , args . port )
2020-09-30 11:05:21 +02:00
main ( port = args . port )