Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
f025458998 |
@ -334,6 +334,12 @@ smtpd_recipient_restrictions =
|
|||||||
permit
|
permit
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Check that the ssl certificates `/etc/ssl/certs/ssl-cert-snakeoil.pem` and `/etc/ssl/private/ssl-cert-snakeoil.key` exist. Depending on the linux distribution you are using they may or may not be present. If they are not, you will need to generate them with this command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout /etc/ssl/private/ssl-cert-snakeoil.key -out /etc/ssl/certs/ssl-cert-snakeoil.pem
|
||||||
|
```
|
||||||
|
|
||||||
Create the `/etc/postfix/pgsql-relay-domains.cf` file with the following content.
|
Create the `/etc/postfix/pgsql-relay-domains.cf` file with the following content.
|
||||||
Make sure that the database config is correctly set, replace `mydomain.com` with your domain, update 'myuser' and 'mypassword' with your postgres credentials.
|
Make sure that the database config is correctly set, replace `mydomain.com` with your domain, update 'myuser' and 'mypassword' with your postgres credentials.
|
||||||
|
|
||||||
|
@ -86,7 +86,7 @@ def delete_mailbox(mailbox_id):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
user = g.user
|
user = g.user
|
||||||
mailbox = Mailbox.get(id=mailbox_id)
|
mailbox = Mailbox.get(mailbox_id)
|
||||||
|
|
||||||
if not mailbox or mailbox.user_id != user.id:
|
if not mailbox or mailbox.user_id != user.id:
|
||||||
return jsonify(error="Forbidden"), 403
|
return jsonify(error="Forbidden"), 403
|
||||||
|
@ -111,11 +111,15 @@ POSTFIX_SERVER = os.environ.get("POSTFIX_SERVER", "240.0.0.1")
|
|||||||
DISABLE_REGISTRATION = "DISABLE_REGISTRATION" in os.environ
|
DISABLE_REGISTRATION = "DISABLE_REGISTRATION" in os.environ
|
||||||
|
|
||||||
# allow using a different postfix port, useful when developing locally
|
# allow using a different postfix port, useful when developing locally
|
||||||
POSTFIX_PORT = int(os.environ.get("POSTFIX_PORT", 25))
|
|
||||||
|
|
||||||
# Use port 587 instead of 25 when sending emails through Postfix
|
# Use port 587 instead of 25 when sending emails through Postfix
|
||||||
# Useful when calling Postfix from an external network
|
# Useful when calling Postfix from an external network
|
||||||
POSTFIX_SUBMISSION_TLS = "POSTFIX_SUBMISSION_TLS" in os.environ
|
POSTFIX_SUBMISSION_TLS = "POSTFIX_SUBMISSION_TLS" in os.environ
|
||||||
|
if POSTFIX_SUBMISSION_TLS:
|
||||||
|
default_postfix_port = 587
|
||||||
|
else:
|
||||||
|
default_postfix_port = 25
|
||||||
|
POSTFIX_PORT = int(os.environ.get("POSTFIX_PORT", default_postfix_port))
|
||||||
POSTFIX_TIMEOUT = os.environ.get("POSTFIX_TIMEOUT", 3)
|
POSTFIX_TIMEOUT = os.environ.get("POSTFIX_TIMEOUT", 3)
|
||||||
|
|
||||||
# ["domain1.com", "domain2.com"]
|
# ["domain1.com", "domain2.com"]
|
||||||
|
@ -150,7 +150,13 @@ def index():
|
|||||||
flash(f"Alias {alias.email} has been disabled", "success")
|
flash(f"Alias {alias.email} has been disabled", "success")
|
||||||
|
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for("dashboard.index", query=query, sort=sort, filter=alias_filter)
|
url_for(
|
||||||
|
"dashboard.index",
|
||||||
|
query=query,
|
||||||
|
sort=sort,
|
||||||
|
filter=alias_filter,
|
||||||
|
page=page,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
mailboxes = current_user.mailboxes()
|
mailboxes = current_user.mailboxes()
|
||||||
|
@ -69,17 +69,20 @@ def mailbox_route():
|
|||||||
transfer_mailbox = Mailbox.get(transfer_mailbox_id)
|
transfer_mailbox = Mailbox.get(transfer_mailbox_id)
|
||||||
|
|
||||||
if not transfer_mailbox or transfer_mailbox.user_id != current_user.id:
|
if not transfer_mailbox or transfer_mailbox.user_id != current_user.id:
|
||||||
flash("You must transfer the aliases to a mailbox you own.")
|
flash(
|
||||||
|
"You must transfer the aliases to a mailbox you own.", "error"
|
||||||
|
)
|
||||||
return redirect(url_for("dashboard.mailbox_route"))
|
return redirect(url_for("dashboard.mailbox_route"))
|
||||||
|
|
||||||
if transfer_mailbox.id == mailbox.id:
|
if transfer_mailbox.id == mailbox.id:
|
||||||
flash(
|
flash(
|
||||||
"You can not transfer the aliases to the mailbox you want to delete."
|
"You can not transfer the aliases to the mailbox you want to delete.",
|
||||||
|
"error",
|
||||||
)
|
)
|
||||||
return redirect(url_for("dashboard.mailbox_route"))
|
return redirect(url_for("dashboard.mailbox_route"))
|
||||||
|
|
||||||
if not transfer_mailbox.verified:
|
if not transfer_mailbox.verified:
|
||||||
flash("Your new mailbox is not verified")
|
flash("Your new mailbox is not verified", "error")
|
||||||
return redirect(url_for("dashboard.mailbox_route"))
|
return redirect(url_for("dashboard.mailbox_route"))
|
||||||
|
|
||||||
# Schedule delete account job
|
# Schedule delete account job
|
||||||
@ -147,12 +150,12 @@ def mailbox_route():
|
|||||||
elif not email_can_be_used_as_mailbox(mailbox_email):
|
elif not email_can_be_used_as_mailbox(mailbox_email):
|
||||||
flash(f"You cannot use {mailbox_email}.", "error")
|
flash(f"You cannot use {mailbox_email}.", "error")
|
||||||
else:
|
else:
|
||||||
transfer_mailbox = Mailbox.create(
|
new_mailbox = Mailbox.create(
|
||||||
email=mailbox_email, user_id=current_user.id
|
email=mailbox_email, user_id=current_user.id
|
||||||
)
|
)
|
||||||
Session.commit()
|
Session.commit()
|
||||||
|
|
||||||
send_verification_email(current_user, transfer_mailbox)
|
send_verification_email(current_user, new_mailbox)
|
||||||
|
|
||||||
flash(
|
flash(
|
||||||
f"You are going to receive an email to confirm {mailbox_email}.",
|
f"You are going to receive an email to confirm {mailbox_email}.",
|
||||||
@ -162,7 +165,7 @@ def mailbox_route():
|
|||||||
return redirect(
|
return redirect(
|
||||||
url_for(
|
url_for(
|
||||||
"dashboard.mailbox_detail_route",
|
"dashboard.mailbox_detail_route",
|
||||||
mailbox_id=transfer_mailbox.id,
|
mailbox_id=new_mailbox.id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -117,14 +117,12 @@ class MailSender:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def _send_to_smtp(self, send_request: SendRequest, retries: int) -> bool:
|
def _send_to_smtp(self, send_request: SendRequest, retries: int) -> bool:
|
||||||
if config.POSTFIX_SUBMISSION_TLS and config.POSTFIX_PORT == 25:
|
|
||||||
smtp_port = 587
|
|
||||||
else:
|
|
||||||
smtp_port = config.POSTFIX_PORT
|
|
||||||
try:
|
try:
|
||||||
start = time.time()
|
start = time.time()
|
||||||
with SMTP(
|
with SMTP(
|
||||||
config.POSTFIX_SERVER, smtp_port, timeout=config.POSTFIX_TIMEOUT
|
config.POSTFIX_SERVER,
|
||||||
|
config.POSTFIX_PORT,
|
||||||
|
timeout=config.POSTFIX_TIMEOUT,
|
||||||
) as smtp:
|
) as smtp:
|
||||||
if config.POSTFIX_SUBMISSION_TLS:
|
if config.POSTFIX_SUBMISSION_TLS:
|
||||||
smtp.starttls()
|
smtp.starttls()
|
||||||
@ -170,7 +168,7 @@ class MailSender:
|
|||||||
LOG.e(f"Ignore smtp error {e}")
|
LOG.e(f"Ignore smtp error {e}")
|
||||||
return False
|
return False
|
||||||
LOG.e(
|
LOG.e(
|
||||||
f"Could not send message to smtp server {config.POSTFIX_SERVER}:{smtp_port}"
|
f"Could not send message to smtp server {config.POSTFIX_SERVER}:{config.POSTFIX_PORT}"
|
||||||
)
|
)
|
||||||
self._save_request_to_unsent_dir(send_request)
|
self._save_request_to_unsent_dir(send_request)
|
||||||
return False
|
return False
|
||||||
|
@ -1641,6 +1641,8 @@ class Contact(Base, ModelMixin):
|
|||||||
Store configuration of sender (website-email) and alias.
|
Store configuration of sender (website-email) and alias.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
MAX_NAME_LENGTH = 512
|
||||||
|
|
||||||
__tablename__ = "contact"
|
__tablename__ = "contact"
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
|
@ -168,7 +168,7 @@ from app.pgp_utils import (
|
|||||||
sign_data,
|
sign_data,
|
||||||
load_public_key_and_check,
|
load_public_key_and_check,
|
||||||
)
|
)
|
||||||
from app.utils import sanitize_email
|
from app.utils import sanitize_email, canonicalize_email
|
||||||
from init_app import load_pgp_public_keys
|
from init_app import load_pgp_public_keys
|
||||||
from server import create_light_app
|
from server import create_light_app
|
||||||
|
|
||||||
@ -182,6 +182,10 @@ def get_or_create_contact(from_header: str, mail_from: str, alias: Alias) -> Con
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
contact_name, contact_email = "", ""
|
contact_name, contact_email = "", ""
|
||||||
|
|
||||||
|
# Ensure contact_name is within limits
|
||||||
|
if len(contact_name) >= Contact.MAX_NAME_LENGTH:
|
||||||
|
contact_name = contact_name[0 : Contact.MAX_NAME_LENGTH]
|
||||||
|
|
||||||
if not is_valid_email(contact_email):
|
if not is_valid_email(contact_email):
|
||||||
# From header is wrongly formatted, try with mail_from
|
# From header is wrongly formatted, try with mail_from
|
||||||
if mail_from and mail_from != "<>":
|
if mail_from and mail_from != "<>":
|
||||||
@ -1384,21 +1388,26 @@ def get_mailbox_from_mail_from(mail_from: str, alias) -> Optional[Mailbox]:
|
|||||||
"""return the corresponding mailbox given the mail_from and alias
|
"""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
|
Usually the mail_from=mailbox.email but it can also be one of the authorized address
|
||||||
"""
|
"""
|
||||||
for mailbox in alias.mailboxes:
|
|
||||||
if mailbox.email == mail_from:
|
|
||||||
return mailbox
|
|
||||||
|
|
||||||
for authorized_address in mailbox.authorized_addresses:
|
def __check(email_address: str, alias: Alias) -> Optional[Mailbox]:
|
||||||
if authorized_address.email == mail_from:
|
for mailbox in alias.mailboxes:
|
||||||
LOG.d(
|
if mailbox.email == email_address:
|
||||||
"Found an authorized address for %s %s %s",
|
|
||||||
alias,
|
|
||||||
mailbox,
|
|
||||||
authorized_address,
|
|
||||||
)
|
|
||||||
return mailbox
|
return mailbox
|
||||||
|
|
||||||
return None
|
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)
|
||||||
|
|
||||||
|
|
||||||
def handle_unknown_mailbox(
|
def handle_unknown_mailbox(
|
||||||
|
@ -159,9 +159,9 @@ def delete_mailbox_job(job: Job):
|
|||||||
user.email,
|
user.email,
|
||||||
f"Your mailbox {mailbox_email} has been deleted",
|
f"Your mailbox {mailbox_email} has been deleted",
|
||||||
f"""Mailbox {mailbox_email} and its alias have been transferred to {alias_transferred_to}.
|
f"""Mailbox {mailbox_email} and its alias have been transferred to {alias_transferred_to}.
|
||||||
Regards,
|
Regards,
|
||||||
SimpleLogin team.
|
SimpleLogin team.
|
||||||
""",
|
""",
|
||||||
retries=3,
|
retries=3,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -169,9 +169,9 @@ def delete_mailbox_job(job: Job):
|
|||||||
user.email,
|
user.email,
|
||||||
f"Your mailbox {mailbox_email} has been deleted",
|
f"Your mailbox {mailbox_email} has been deleted",
|
||||||
f"""Mailbox {mailbox_email} along with its aliases have been deleted successfully.
|
f"""Mailbox {mailbox_email} along with its aliases have been deleted successfully.
|
||||||
Regards,
|
Regards,
|
||||||
SimpleLogin team.
|
SimpleLogin team.
|
||||||
""",
|
""",
|
||||||
retries=3,
|
retries=3,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ from app.models import (
|
|||||||
Contact,
|
Contact,
|
||||||
SentAlert,
|
SentAlert,
|
||||||
)
|
)
|
||||||
|
from app.utils import random_string, canonicalize_email
|
||||||
from email_handler import (
|
from email_handler import (
|
||||||
get_mailbox_from_mail_from,
|
get_mailbox_from_mail_from,
|
||||||
should_ignore,
|
should_ignore,
|
||||||
@ -308,3 +309,62 @@ def test_replace_contacts_and_user_in_reply_phase(flask_client):
|
|||||||
payload = sent_mails[0].msg.get_payload()[0].get_payload()
|
payload = sent_mails[0].msg.get_payload()[0].get_payload()
|
||||||
assert payload.find("Contact is {}".format(contact_real_mail)) > -1
|
assert payload.find("Contact is {}".format(contact_real_mail)) > -1
|
||||||
assert payload.find("Other contact is {}".format(contact2_real_mail)) > -1
|
assert payload.find("Other contact is {}".format(contact2_real_mail)) > -1
|
||||||
|
|
||||||
|
|
||||||
|
@mail_sender.store_emails_test_decorator
|
||||||
|
def test_send_email_from_non_canonical_address_on_reply(flask_client):
|
||||||
|
email_address = f"{random_string(10)}.suf@gmail.com"
|
||||||
|
user = create_new_user(email=canonicalize_email(email_address))
|
||||||
|
alias = Alias.create_new_random(user)
|
||||||
|
Session.commit()
|
||||||
|
contact = Contact.create(
|
||||||
|
user_id=user.id,
|
||||||
|
alias_id=alias.id,
|
||||||
|
website_email=random_email(),
|
||||||
|
reply_email=f"{random_string(10)}@{EMAIL_DOMAIN}",
|
||||||
|
commit=True,
|
||||||
|
)
|
||||||
|
envelope = Envelope()
|
||||||
|
envelope.mail_from = email_address
|
||||||
|
envelope.rcpt_tos = [contact.reply_email]
|
||||||
|
msg = EmailMessage()
|
||||||
|
msg[headers.TO] = contact.reply_email
|
||||||
|
msg[headers.SUBJECT] = random_string()
|
||||||
|
result = email_handler.handle(envelope, msg)
|
||||||
|
assert result == status.E200
|
||||||
|
sent_mails = mail_sender.get_stored_emails()
|
||||||
|
assert len(sent_mails) == 1
|
||||||
|
email_logs = EmailLog.filter_by(user_id=user.id).all()
|
||||||
|
assert len(email_logs) == 1
|
||||||
|
assert email_logs[0].alias_id == alias.id
|
||||||
|
assert email_logs[0].mailbox_id == user.default_mailbox_id
|
||||||
|
|
||||||
|
|
||||||
|
@mail_sender.store_emails_test_decorator
|
||||||
|
def test_send_email_from_non_canonical_matches_already_existing_user(flask_client):
|
||||||
|
email_address = f"{random_string(10)}.suf@gmail.com"
|
||||||
|
create_new_user(email=canonicalize_email(email_address))
|
||||||
|
user = create_new_user(email=email_address)
|
||||||
|
alias = Alias.create_new_random(user)
|
||||||
|
Session.commit()
|
||||||
|
contact = Contact.create(
|
||||||
|
user_id=user.id,
|
||||||
|
alias_id=alias.id,
|
||||||
|
website_email=random_email(),
|
||||||
|
reply_email=f"{random_string(10)}@{EMAIL_DOMAIN}",
|
||||||
|
commit=True,
|
||||||
|
)
|
||||||
|
envelope = Envelope()
|
||||||
|
envelope.mail_from = email_address
|
||||||
|
envelope.rcpt_tos = [contact.reply_email]
|
||||||
|
msg = EmailMessage()
|
||||||
|
msg[headers.TO] = contact.reply_email
|
||||||
|
msg[headers.SUBJECT] = random_string()
|
||||||
|
result = email_handler.handle(envelope, msg)
|
||||||
|
assert result == status.E200
|
||||||
|
sent_mails = mail_sender.get_stored_emails()
|
||||||
|
assert len(sent_mails) == 1
|
||||||
|
email_logs = EmailLog.filter_by(user_id=user.id).all()
|
||||||
|
assert len(email_logs) == 1
|
||||||
|
assert email_logs[0].alias_id == alias.id
|
||||||
|
assert email_logs[0].mailbox_id == user.default_mailbox_id
|
||||||
|
Loading…
x
Reference in New Issue
Block a user