Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
5210cb6515 | |||
b643f0644b | |||
5d093db4f6 | |||
0b16fcac67 | |||
a0d294da53 | |||
c3f755aede | |||
0aea62c222 |
@ -34,6 +34,8 @@ steps:
|
|||||||
settings:
|
settings:
|
||||||
webhook:
|
webhook:
|
||||||
from_secret: slack_webhook
|
from_secret: slack_webhook
|
||||||
|
icon_url:
|
||||||
|
from_secret: slack_avatar
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
event:
|
event:
|
||||||
|
14
README.md
14
README.md
@ -1,7 +1,9 @@
|
|||||||
# Simple Login
|
# Simple Login
|
||||||
|
|
||||||
This repo exists to automatically capture any releases of the SaaS edition of SimpleLogin. It checks once a day, and builds the latest one automatically if it is newer than the currentlty built version.
|
[](https://drone.mrmeeb.stream/MrMeeb/simple-login)
|
||||||
|
|
||||||
This exists to simplify deployment of SimpleLogin in a self-hosted capacity, while also allowing the use of the latest version; SimpleLogin do not provide an up-to-date version for this use.
|
This repo exists to automatically capture any releases of the SaaS edition of SimpleLogin. It checks once a day, and builds the latest one automatically if it is newer than the currentlty built version.
|
||||||
|
|
||||||
|
This exists to simplify deployment of SimpleLogin in a self-hosted capacity, while also allowing the use of the latest version; SimpleLogin do not provide an up-to-date version for this use.
|
||||||
|
|
||||||
The image is built for amd64 and arm64 devices.
|
The image is built for amd64 and arm64 devices.
|
1
app/.gitignore
vendored
1
app/.gitignore
vendored
@ -15,3 +15,4 @@ venv/
|
|||||||
.coverage
|
.coverage
|
||||||
htmlcov
|
htmlcov
|
||||||
adhoc
|
adhoc
|
||||||
|
.env.*
|
@ -21,3 +21,4 @@ repos:
|
|||||||
- id: djlint-jinja
|
- id: djlint-jinja
|
||||||
files: '.*\.html'
|
files: '.*\.html'
|
||||||
entry: djlint --reformat
|
entry: djlint --reformat
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
FROM node:10.17.0-alpine AS npm
|
FROM node:10.17.0-alpine AS npm
|
||||||
WORKDIR /code
|
WORKDIR /code
|
||||||
COPY ./static/package*.json /code/static/
|
COPY ./static/package*.json /code/static/
|
||||||
RUN cd /code/static && npm install
|
RUN cd /code/static && npm ci
|
||||||
|
|
||||||
# Main image
|
# Main image
|
||||||
FROM python:3.10
|
FROM python:3.10
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -620,3 +620,8 @@ class MetricAdmin(SLModelView):
|
|||||||
column_exclude_list = ["created_at", "updated_at", "id"]
|
column_exclude_list = ["created_at", "updated_at", "id"]
|
||||||
|
|
||||||
can_export = True
|
can_export = True
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidMailboxDomainAdmin(SLModelView):
|
||||||
|
can_create = True
|
||||||
|
can_delete = True
|
||||||
|
@ -78,6 +78,9 @@ def delete_mailbox(mailbox_id):
|
|||||||
Delete mailbox
|
Delete mailbox
|
||||||
Input:
|
Input:
|
||||||
mailbox_id: in url
|
mailbox_id: in url
|
||||||
|
(optional) transfer_aliases_to: in body. Id of the new mailbox for the aliases.
|
||||||
|
If omitted or the value is set to -1,
|
||||||
|
the aliases of the mailbox will be deleted too.
|
||||||
Output:
|
Output:
|
||||||
200 if deleted successfully
|
200 if deleted successfully
|
||||||
|
|
||||||
@ -91,11 +94,36 @@ def delete_mailbox(mailbox_id):
|
|||||||
if mailbox.id == user.default_mailbox_id:
|
if mailbox.id == user.default_mailbox_id:
|
||||||
return jsonify(error="You cannot delete the default mailbox"), 400
|
return jsonify(error="You cannot delete the default mailbox"), 400
|
||||||
|
|
||||||
|
data = request.get_json() or {}
|
||||||
|
transfer_mailbox_id = data.get("transfer_aliases_to")
|
||||||
|
if transfer_mailbox_id and int(transfer_mailbox_id) >= 0:
|
||||||
|
transfer_mailbox = Mailbox.get(transfer_mailbox_id)
|
||||||
|
|
||||||
|
if not transfer_mailbox or transfer_mailbox.user_id != user.id:
|
||||||
|
return (
|
||||||
|
jsonify(error="You must transfer the aliases to a mailbox you own."),
|
||||||
|
403,
|
||||||
|
)
|
||||||
|
|
||||||
|
if transfer_mailbox_id == mailbox_id:
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
error="You can not transfer the aliases to the mailbox you want to delete."
|
||||||
|
),
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not transfer_mailbox.verified:
|
||||||
|
return jsonify(error="Your new mailbox is not verified"), 400
|
||||||
|
|
||||||
# Schedule delete account job
|
# Schedule delete account job
|
||||||
LOG.w("schedule delete mailbox job for %s", mailbox)
|
LOG.w("schedule delete mailbox job for %s", mailbox)
|
||||||
Job.create(
|
Job.create(
|
||||||
name=JOB_DELETE_MAILBOX,
|
name=JOB_DELETE_MAILBOX,
|
||||||
payload={"mailbox_id": mailbox.id},
|
payload={
|
||||||
|
"mailbox_id": mailbox.id,
|
||||||
|
"transfer_mailbox_id": transfer_mailbox_id,
|
||||||
|
},
|
||||||
run_at=arrow.now(),
|
run_at=arrow.now(),
|
||||||
commit=True,
|
commit=True,
|
||||||
)
|
)
|
||||||
|
@ -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"]
|
||||||
|
@ -7,6 +7,7 @@ from app.dashboard.base import dashboard_bp
|
|||||||
from app.dashboard.views.enter_sudo import sudo_required
|
from app.dashboard.views.enter_sudo import sudo_required
|
||||||
from app.db import Session
|
from app.db import Session
|
||||||
from app.models import ApiKey
|
from app.models import ApiKey
|
||||||
|
from app.utils import CSRFValidationForm
|
||||||
|
|
||||||
|
|
||||||
class NewApiKeyForm(FlaskForm):
|
class NewApiKeyForm(FlaskForm):
|
||||||
@ -23,9 +24,13 @@ def api_key():
|
|||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
csrf_form = CSRFValidationForm()
|
||||||
new_api_key_form = NewApiKeyForm()
|
new_api_key_form = NewApiKeyForm()
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
if not csrf_form.validate():
|
||||||
|
flash("Invalid request", "warning")
|
||||||
|
return redirect(request.url)
|
||||||
if request.form.get("form-name") == "delete":
|
if request.form.get("form-name") == "delete":
|
||||||
api_key_id = request.form.get("api-key-id")
|
api_key_id = request.form.get("api-key-id")
|
||||||
|
|
||||||
@ -62,5 +67,8 @@ def api_key():
|
|||||||
return redirect(url_for("dashboard.api_key"))
|
return redirect(url_for("dashboard.api_key"))
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"dashboard/api_key.html", api_keys=api_keys, new_api_key_form=new_api_key_form
|
"dashboard/api_key.html",
|
||||||
|
api_keys=api_keys,
|
||||||
|
new_api_key_form=new_api_key_form,
|
||||||
|
csrf_form=csrf_form,
|
||||||
)
|
)
|
||||||
|
@ -34,7 +34,7 @@ def batch_import_route():
|
|||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
if not csrf_form.validate():
|
if not csrf_form.validate():
|
||||||
flash("Invalid request", "warning")
|
flash("Invalid request", "warning")
|
||||||
redirect(request.url)
|
return redirect(request.url)
|
||||||
if len(batch_imports) > 10:
|
if len(batch_imports) > 10:
|
||||||
flash(
|
flash(
|
||||||
"You have too many imports already. Wait until some get cleaned up",
|
"You have too many imports already. Wait until some get cleaned up",
|
||||||
|
@ -3,6 +3,7 @@ from flask_login import login_required, current_user
|
|||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import StringField, validators
|
from wtforms import StringField, validators
|
||||||
|
|
||||||
|
from app import parallel_limiter
|
||||||
from app.config import EMAIL_SERVERS_WITH_PRIORITY
|
from app.config import EMAIL_SERVERS_WITH_PRIORITY
|
||||||
from app.dashboard.base import dashboard_bp
|
from app.dashboard.base import dashboard_bp
|
||||||
from app.db import Session
|
from app.db import Session
|
||||||
@ -19,6 +20,7 @@ class NewCustomDomainForm(FlaskForm):
|
|||||||
|
|
||||||
@dashboard_bp.route("/custom_domain", methods=["GET", "POST"])
|
@dashboard_bp.route("/custom_domain", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
|
@parallel_limiter.lock(only_when=lambda: request.method == "POST")
|
||||||
def custom_domain():
|
def custom_domain():
|
||||||
custom_domains = CustomDomain.filter_by(
|
custom_domains = CustomDomain.filter_by(
|
||||||
user_id=current_user.id, is_sl_subdomain=False
|
user_id=current_user.id, is_sl_subdomain=False
|
||||||
|
@ -9,6 +9,7 @@ from wtforms import (
|
|||||||
IntegerField,
|
IntegerField,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from app import parallel_limiter
|
||||||
from app.config import (
|
from app.config import (
|
||||||
EMAIL_DOMAIN,
|
EMAIL_DOMAIN,
|
||||||
ALIAS_DOMAINS,
|
ALIAS_DOMAINS,
|
||||||
@ -45,6 +46,7 @@ class DeleteDirForm(FlaskForm):
|
|||||||
|
|
||||||
@dashboard_bp.route("/directory", methods=["GET", "POST"])
|
@dashboard_bp.route("/directory", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
|
@parallel_limiter.lock(only_when=lambda: request.method == "POST")
|
||||||
def directory():
|
def directory():
|
||||||
dirs = (
|
dirs = (
|
||||||
Directory.filter_by(user_id=current_user.id)
|
Directory.filter_by(user_id=current_user.id)
|
||||||
|
@ -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()
|
||||||
|
@ -2,10 +2,11 @@ import arrow
|
|||||||
from flask import render_template, request, redirect, url_for, flash
|
from flask import render_template, request, redirect, url_for, flash
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from itsdangerous import Signer
|
from itsdangerous import TimestampSigner
|
||||||
from wtforms import validators
|
from wtforms import validators, IntegerField
|
||||||
from wtforms.fields.html5 import EmailField
|
from wtforms.fields.html5 import EmailField
|
||||||
|
|
||||||
|
from app import parallel_limiter
|
||||||
from app.config import MAILBOX_SECRET, URL, JOB_DELETE_MAILBOX
|
from app.config import MAILBOX_SECRET, URL, JOB_DELETE_MAILBOX
|
||||||
from app.dashboard.base import dashboard_bp
|
from app.dashboard.base import dashboard_bp
|
||||||
from app.db import Session
|
from app.db import Session
|
||||||
@ -27,8 +28,16 @@ class NewMailboxForm(FlaskForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteMailboxForm(FlaskForm):
|
||||||
|
mailbox_id = IntegerField(
|
||||||
|
validators=[validators.DataRequired()],
|
||||||
|
)
|
||||||
|
transfer_mailbox_id = IntegerField()
|
||||||
|
|
||||||
|
|
||||||
@dashboard_bp.route("/mailbox", methods=["GET", "POST"])
|
@dashboard_bp.route("/mailbox", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
|
@parallel_limiter.lock(only_when=lambda: request.method == "POST")
|
||||||
def mailbox_route():
|
def mailbox_route():
|
||||||
mailboxes = (
|
mailboxes = (
|
||||||
Mailbox.filter_by(user_id=current_user.id)
|
Mailbox.filter_by(user_id=current_user.id)
|
||||||
@ -38,28 +47,56 @@ def mailbox_route():
|
|||||||
|
|
||||||
new_mailbox_form = NewMailboxForm()
|
new_mailbox_form = NewMailboxForm()
|
||||||
csrf_form = CSRFValidationForm()
|
csrf_form = CSRFValidationForm()
|
||||||
|
delete_mailbox_form = DeleteMailboxForm()
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
if not csrf_form.validate():
|
|
||||||
flash("Invalid request", "warning")
|
|
||||||
return redirect(request.url)
|
|
||||||
if request.form.get("form-name") == "delete":
|
if request.form.get("form-name") == "delete":
|
||||||
mailbox_id = request.form.get("mailbox-id")
|
if not delete_mailbox_form.validate():
|
||||||
mailbox = Mailbox.get(mailbox_id)
|
flash("Invalid request", "warning")
|
||||||
|
return redirect(request.url)
|
||||||
|
mailbox = Mailbox.get(delete_mailbox_form.mailbox_id.data)
|
||||||
|
|
||||||
if not mailbox or mailbox.user_id != current_user.id:
|
if not mailbox or mailbox.user_id != current_user.id:
|
||||||
flash("Unknown error. Refresh the page", "warning")
|
flash("Invalid mailbox. Refresh the page", "warning")
|
||||||
return redirect(url_for("dashboard.mailbox_route"))
|
return redirect(url_for("dashboard.mailbox_route"))
|
||||||
|
|
||||||
if mailbox.id == current_user.default_mailbox_id:
|
if mailbox.id == current_user.default_mailbox_id:
|
||||||
flash("You cannot delete default mailbox", "error")
|
flash("You cannot delete default mailbox", "error")
|
||||||
return redirect(url_for("dashboard.mailbox_route"))
|
return redirect(url_for("dashboard.mailbox_route"))
|
||||||
|
|
||||||
|
transfer_mailbox_id = delete_mailbox_form.transfer_mailbox_id.data
|
||||||
|
if transfer_mailbox_id and transfer_mailbox_id > 0:
|
||||||
|
transfer_mailbox = Mailbox.get(transfer_mailbox_id)
|
||||||
|
|
||||||
|
if not transfer_mailbox or transfer_mailbox.user_id != current_user.id:
|
||||||
|
flash(
|
||||||
|
"You must transfer the aliases to a mailbox you own.", "error"
|
||||||
|
)
|
||||||
|
return redirect(url_for("dashboard.mailbox_route"))
|
||||||
|
|
||||||
|
if transfer_mailbox.id == mailbox.id:
|
||||||
|
flash(
|
||||||
|
"You can not transfer the aliases to the mailbox you want to delete.",
|
||||||
|
"error",
|
||||||
|
)
|
||||||
|
return redirect(url_for("dashboard.mailbox_route"))
|
||||||
|
|
||||||
|
if not transfer_mailbox.verified:
|
||||||
|
flash("Your new mailbox is not verified", "error")
|
||||||
|
return redirect(url_for("dashboard.mailbox_route"))
|
||||||
|
|
||||||
# Schedule delete account job
|
# Schedule delete account job
|
||||||
LOG.w("schedule delete mailbox job for %s", mailbox)
|
LOG.w(
|
||||||
|
f"schedule delete mailbox job for {mailbox.id} with transfer to mailbox {transfer_mailbox_id}"
|
||||||
|
)
|
||||||
Job.create(
|
Job.create(
|
||||||
name=JOB_DELETE_MAILBOX,
|
name=JOB_DELETE_MAILBOX,
|
||||||
payload={"mailbox_id": mailbox.id},
|
payload={
|
||||||
|
"mailbox_id": mailbox.id,
|
||||||
|
"transfer_mailbox_id": transfer_mailbox_id
|
||||||
|
if transfer_mailbox_id > 0
|
||||||
|
else None,
|
||||||
|
},
|
||||||
run_at=arrow.now(),
|
run_at=arrow.now(),
|
||||||
commit=True,
|
commit=True,
|
||||||
)
|
)
|
||||||
@ -72,7 +109,10 @@ def mailbox_route():
|
|||||||
|
|
||||||
return redirect(url_for("dashboard.mailbox_route"))
|
return redirect(url_for("dashboard.mailbox_route"))
|
||||||
if request.form.get("form-name") == "set-default":
|
if request.form.get("form-name") == "set-default":
|
||||||
mailbox_id = request.form.get("mailbox-id")
|
if not csrf_form.validate():
|
||||||
|
flash("Invalid request", "warning")
|
||||||
|
return redirect(request.url)
|
||||||
|
mailbox_id = request.form.get("mailbox_id")
|
||||||
mailbox = Mailbox.get(mailbox_id)
|
mailbox = Mailbox.get(mailbox_id)
|
||||||
|
|
||||||
if not mailbox or mailbox.user_id != current_user.id:
|
if not mailbox or mailbox.user_id != current_user.id:
|
||||||
@ -124,7 +164,8 @@ def mailbox_route():
|
|||||||
|
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for(
|
url_for(
|
||||||
"dashboard.mailbox_detail_route", mailbox_id=new_mailbox.id
|
"dashboard.mailbox_detail_route",
|
||||||
|
mailbox_id=new_mailbox.id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -132,38 +173,13 @@ def mailbox_route():
|
|||||||
"dashboard/mailbox.html",
|
"dashboard/mailbox.html",
|
||||||
mailboxes=mailboxes,
|
mailboxes=mailboxes,
|
||||||
new_mailbox_form=new_mailbox_form,
|
new_mailbox_form=new_mailbox_form,
|
||||||
|
delete_mailbox_form=delete_mailbox_form,
|
||||||
csrf_form=csrf_form,
|
csrf_form=csrf_form,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def delete_mailbox(mailbox_id: int):
|
|
||||||
from server import create_light_app
|
|
||||||
|
|
||||||
with create_light_app().app_context():
|
|
||||||
mailbox = Mailbox.get(mailbox_id)
|
|
||||||
if not mailbox:
|
|
||||||
return
|
|
||||||
|
|
||||||
mailbox_email = mailbox.email
|
|
||||||
user = mailbox.user
|
|
||||||
|
|
||||||
Mailbox.delete(mailbox_id)
|
|
||||||
Session.commit()
|
|
||||||
LOG.d("Mailbox %s %s deleted", mailbox_id, mailbox_email)
|
|
||||||
|
|
||||||
send_email(
|
|
||||||
user.email,
|
|
||||||
f"Your mailbox {mailbox_email} has been deleted",
|
|
||||||
f"""Mailbox {mailbox_email} along with its aliases are deleted successfully.
|
|
||||||
|
|
||||||
Regards,
|
|
||||||
SimpleLogin team.
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def send_verification_email(user, mailbox):
|
def send_verification_email(user, mailbox):
|
||||||
s = Signer(MAILBOX_SECRET)
|
s = TimestampSigner(MAILBOX_SECRET)
|
||||||
mailbox_id_signed = s.sign(str(mailbox.id)).decode()
|
mailbox_id_signed = s.sign(str(mailbox.id)).decode()
|
||||||
verification_url = (
|
verification_url = (
|
||||||
URL + "/dashboard/mailbox_verify" + f"?mailbox_id={mailbox_id_signed}"
|
URL + "/dashboard/mailbox_verify" + f"?mailbox_id={mailbox_id_signed}"
|
||||||
@ -188,11 +204,11 @@ def send_verification_email(user, mailbox):
|
|||||||
|
|
||||||
@dashboard_bp.route("/mailbox_verify")
|
@dashboard_bp.route("/mailbox_verify")
|
||||||
def mailbox_verify():
|
def mailbox_verify():
|
||||||
s = Signer(MAILBOX_SECRET)
|
s = TimestampSigner(MAILBOX_SECRET)
|
||||||
mailbox_id = request.args.get("mailbox_id")
|
mailbox_id = request.args.get("mailbox_id")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r_id = int(s.unsign(mailbox_id))
|
r_id = int(s.unsign(mailbox_id, max_age=900))
|
||||||
except Exception:
|
except Exception:
|
||||||
flash("Invalid link. Please delete and re-add your mailbox", "error")
|
flash("Invalid link. Please delete and re-add your mailbox", "error")
|
||||||
return redirect(url_for("dashboard.mailbox_route"))
|
return redirect(url_for("dashboard.mailbox_route"))
|
||||||
|
@ -4,7 +4,7 @@ from email_validator import validate_email, EmailNotValidError
|
|||||||
from flask import render_template, request, redirect, url_for, flash
|
from flask import render_template, request, redirect, url_for, flash
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from itsdangerous import Signer
|
from itsdangerous import TimestampSigner
|
||||||
from wtforms import validators
|
from wtforms import validators
|
||||||
from wtforms.fields.html5 import EmailField
|
from wtforms.fields.html5 import EmailField
|
||||||
|
|
||||||
@ -210,7 +210,7 @@ def mailbox_detail_route(mailbox_id):
|
|||||||
|
|
||||||
|
|
||||||
def verify_mailbox_change(user, mailbox, new_email):
|
def verify_mailbox_change(user, mailbox, new_email):
|
||||||
s = Signer(MAILBOX_SECRET)
|
s = TimestampSigner(MAILBOX_SECRET)
|
||||||
mailbox_id_signed = s.sign(str(mailbox.id)).decode()
|
mailbox_id_signed = s.sign(str(mailbox.id)).decode()
|
||||||
verification_url = (
|
verification_url = (
|
||||||
f"{URL}/dashboard/mailbox/confirm_change?mailbox_id={mailbox_id_signed}"
|
f"{URL}/dashboard/mailbox/confirm_change?mailbox_id={mailbox_id_signed}"
|
||||||
@ -262,11 +262,11 @@ def cancel_mailbox_change_route(mailbox_id):
|
|||||||
|
|
||||||
@dashboard_bp.route("/mailbox/confirm_change")
|
@dashboard_bp.route("/mailbox/confirm_change")
|
||||||
def mailbox_confirm_change_route():
|
def mailbox_confirm_change_route():
|
||||||
s = Signer(MAILBOX_SECRET)
|
s = TimestampSigner(MAILBOX_SECRET)
|
||||||
signed_mailbox_id = request.args.get("mailbox_id")
|
signed_mailbox_id = request.args.get("mailbox_id")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
mailbox_id = int(s.unsign(signed_mailbox_id))
|
mailbox_id = int(s.unsign(signed_mailbox_id, max_age=900))
|
||||||
except Exception:
|
except Exception:
|
||||||
flash("Invalid link", "error")
|
flash("Invalid link", "error")
|
||||||
return redirect(url_for("dashboard.index"))
|
return redirect(url_for("dashboard.index"))
|
||||||
|
@ -5,6 +5,7 @@ from app.dashboard.base import dashboard_bp
|
|||||||
from app.dashboard.views.enter_sudo import sudo_required
|
from app.dashboard.views.enter_sudo import sudo_required
|
||||||
from app.db import Session
|
from app.db import Session
|
||||||
from app.models import RecoveryCode
|
from app.models import RecoveryCode
|
||||||
|
from app.utils import CSRFValidationForm
|
||||||
|
|
||||||
|
|
||||||
@dashboard_bp.route("/mfa_cancel", methods=["GET", "POST"])
|
@dashboard_bp.route("/mfa_cancel", methods=["GET", "POST"])
|
||||||
@ -15,8 +16,13 @@ def mfa_cancel():
|
|||||||
flash("you don't have MFA enabled", "warning")
|
flash("you don't have MFA enabled", "warning")
|
||||||
return redirect(url_for("dashboard.index"))
|
return redirect(url_for("dashboard.index"))
|
||||||
|
|
||||||
|
csrf_form = CSRFValidationForm()
|
||||||
|
|
||||||
# user cancels TOTP
|
# user cancels TOTP
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
if not csrf_form.validate():
|
||||||
|
flash("Invalid request", "warning")
|
||||||
|
return redirect(request.url)
|
||||||
current_user.enable_otp = False
|
current_user.enable_otp = False
|
||||||
current_user.otp_secret = None
|
current_user.otp_secret = None
|
||||||
Session.commit()
|
Session.commit()
|
||||||
@ -28,4 +34,4 @@ def mfa_cancel():
|
|||||||
flash("TOTP is now disabled", "warning")
|
flash("TOTP is now disabled", "warning")
|
||||||
return redirect(url_for("dashboard.index"))
|
return redirect(url_for("dashboard.index"))
|
||||||
|
|
||||||
return render_template("dashboard/mfa_cancel.html")
|
return render_template("dashboard/mfa_cancel.html", csrf_form=csrf_form)
|
||||||
|
@ -2,7 +2,10 @@ import re
|
|||||||
|
|
||||||
from flask import render_template, request, redirect, url_for, flash
|
from flask import render_template, request, redirect, url_for, flash
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import StringField, validators
|
||||||
|
|
||||||
|
from app import parallel_limiter
|
||||||
from app.config import MAX_NB_SUBDOMAIN
|
from app.config import MAX_NB_SUBDOMAIN
|
||||||
from app.dashboard.base import dashboard_bp
|
from app.dashboard.base import dashboard_bp
|
||||||
from app.errors import SubdomainInTrashError
|
from app.errors import SubdomainInTrashError
|
||||||
@ -13,8 +16,18 @@ from app.models import CustomDomain, Mailbox, SLDomain
|
|||||||
_SUBDOMAIN_PATTERN = r"[0-9a-z-]{1,}"
|
_SUBDOMAIN_PATTERN = r"[0-9a-z-]{1,}"
|
||||||
|
|
||||||
|
|
||||||
|
class NewSubdomainForm(FlaskForm):
|
||||||
|
domain = StringField(
|
||||||
|
"domain", validators=[validators.DataRequired(), validators.Length(max=64)]
|
||||||
|
)
|
||||||
|
subdomain = StringField(
|
||||||
|
"subdomain", validators=[validators.DataRequired(), validators.Length(max=64)]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dashboard_bp.route("/subdomain", methods=["GET", "POST"])
|
@dashboard_bp.route("/subdomain", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
|
@parallel_limiter.lock(only_when=lambda: request.method == "POST")
|
||||||
def subdomain_route():
|
def subdomain_route():
|
||||||
if not current_user.subdomain_is_available():
|
if not current_user.subdomain_is_available():
|
||||||
flash("Unknown error, redirect to the home page", "error")
|
flash("Unknown error, redirect to the home page", "error")
|
||||||
@ -26,9 +39,13 @@ def subdomain_route():
|
|||||||
).all()
|
).all()
|
||||||
|
|
||||||
errors = {}
|
errors = {}
|
||||||
|
new_subdomain_form = NewSubdomainForm()
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
if request.form.get("form-name") == "create":
|
if request.form.get("form-name") == "create":
|
||||||
|
if not new_subdomain_form.validate():
|
||||||
|
flash("Invalid new subdomain", "warning")
|
||||||
|
return redirect(url_for("dashboard.subdomain_route"))
|
||||||
if not current_user.is_premium():
|
if not current_user.is_premium():
|
||||||
flash("Only premium plan can add subdomain", "warning")
|
flash("Only premium plan can add subdomain", "warning")
|
||||||
return redirect(request.url)
|
return redirect(request.url)
|
||||||
@ -39,8 +56,8 @@ def subdomain_route():
|
|||||||
)
|
)
|
||||||
return redirect(request.url)
|
return redirect(request.url)
|
||||||
|
|
||||||
subdomain = request.form.get("subdomain").lower().strip()
|
subdomain = new_subdomain_form.subdomain.data.lower().strip()
|
||||||
domain = request.form.get("domain").lower().strip()
|
domain = new_subdomain_form.domain.data.lower().strip()
|
||||||
|
|
||||||
if len(subdomain) < 3:
|
if len(subdomain) < 3:
|
||||||
flash("Subdomain must have at least 3 characters", "error")
|
flash("Subdomain must have at least 3 characters", "error")
|
||||||
@ -108,4 +125,5 @@ def subdomain_route():
|
|||||||
sl_domains=sl_domains,
|
sl_domains=sl_domains,
|
||||||
errors=errors,
|
errors=errors,
|
||||||
subdomains=subdomains,
|
subdomains=subdomains,
|
||||||
|
new_subdomain_form=new_subdomain_form,
|
||||||
)
|
)
|
||||||
|
@ -42,9 +42,11 @@ class UnsubscribeLink:
|
|||||||
class UnsubscribeEncoder:
|
class UnsubscribeEncoder:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def encode(
|
def encode(
|
||||||
action: UnsubscribeAction, data: Union[int, UnsubscribeOriginalData]
|
action: UnsubscribeAction,
|
||||||
|
data: Union[int, UnsubscribeOriginalData],
|
||||||
|
force_web: bool = False,
|
||||||
) -> UnsubscribeLink:
|
) -> UnsubscribeLink:
|
||||||
if config.UNSUBSCRIBER:
|
if config.UNSUBSCRIBER and not force_web:
|
||||||
return UnsubscribeLink(UnsubscribeEncoder.encode_mailto(action, data), True)
|
return UnsubscribeLink(UnsubscribeEncoder.encode_mailto(action, data), True)
|
||||||
return UnsubscribeLink(UnsubscribeEncoder.encode_url(action, data), False)
|
return UnsubscribeLink(UnsubscribeEncoder.encode_url(action, data), False)
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ class UnsubscribeHandler:
|
|||||||
return status.E507
|
return status.E507
|
||||||
mailbox = Mailbox.get_by(email=envelope.mail_from)
|
mailbox = Mailbox.get_by(email=envelope.mail_from)
|
||||||
if not mailbox:
|
if not mailbox:
|
||||||
LOG.w("Unknown mailbox %s", msg[headers.SUBJECT])
|
LOG.w("Unknown mailbox %s", envelope.mail_from)
|
||||||
return status.E507
|
return status.E507
|
||||||
|
|
||||||
if unsub_data.action == UnsubscribeAction.DisableAlias:
|
if unsub_data.action == UnsubscribeAction.DisableAlias:
|
||||||
|
@ -17,7 +17,7 @@ from attr import dataclass
|
|||||||
from app import config
|
from app import config
|
||||||
from app.email import headers
|
from app.email import headers
|
||||||
from app.log import LOG
|
from app.log import LOG
|
||||||
from app.message_utils import message_to_bytes
|
from app.message_utils import message_to_bytes, message_format_base64_parts
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -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,13 +168,17 @@ 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
|
||||||
|
|
||||||
def _save_request_to_unsent_dir(self, send_request: SendRequest):
|
def _save_request_to_unsent_dir(
|
||||||
file_name = f"DeliveryFail-{int(time.time())}-{uuid.uuid4()}.{SendRequest.SAVE_EXTENSION}"
|
self, send_request: SendRequest, prefix: str = "DeliveryFail"
|
||||||
|
):
|
||||||
|
file_name = (
|
||||||
|
f"{prefix}-{int(time.time())}-{uuid.uuid4()}.{SendRequest.SAVE_EXTENSION}"
|
||||||
|
)
|
||||||
file_path = os.path.join(config.SAVE_UNSENT_DIR, file_name)
|
file_path = os.path.join(config.SAVE_UNSENT_DIR, file_name)
|
||||||
file_contents = send_request.to_bytes()
|
file_contents = send_request.to_bytes()
|
||||||
with open(file_path, "wb") as fd:
|
with open(file_path, "wb") as fd:
|
||||||
@ -258,7 +260,7 @@ def sl_sendmail(
|
|||||||
send_request = SendRequest(
|
send_request = SendRequest(
|
||||||
envelope_from,
|
envelope_from,
|
||||||
envelope_to,
|
envelope_to,
|
||||||
msg,
|
message_format_base64_parts(msg),
|
||||||
mail_options,
|
mail_options,
|
||||||
rcpt_options,
|
rcpt_options,
|
||||||
is_forward,
|
is_forward,
|
||||||
|
@ -1,21 +1,42 @@
|
|||||||
|
import re
|
||||||
from email import policy
|
from email import policy
|
||||||
from email.message import Message
|
from email.message import Message
|
||||||
|
|
||||||
|
from app.email import headers
|
||||||
from app.log import LOG
|
from app.log import LOG
|
||||||
|
|
||||||
|
# Spam assassin might flag as spam with a different line length
|
||||||
|
BASE64_LINELENGTH = 76
|
||||||
|
|
||||||
|
|
||||||
def message_to_bytes(msg: Message) -> bytes:
|
def message_to_bytes(msg: Message) -> bytes:
|
||||||
"""replace Message.as_bytes() method by trying different policies"""
|
"""replace Message.as_bytes() method by trying different policies"""
|
||||||
for generator_policy in [None, policy.SMTP, policy.SMTPUTF8]:
|
for generator_policy in [None, policy.SMTP, policy.SMTPUTF8]:
|
||||||
try:
|
try:
|
||||||
return msg.as_bytes(policy=generator_policy)
|
return msg.as_bytes(policy=generator_policy)
|
||||||
except:
|
except Exception:
|
||||||
LOG.w("as_bytes() fails with %s policy", policy, exc_info=True)
|
LOG.w("as_bytes() fails with %s policy", policy, exc_info=True)
|
||||||
|
|
||||||
msg_string = msg.as_string()
|
msg_string = msg.as_string()
|
||||||
try:
|
try:
|
||||||
return msg_string.encode()
|
return msg_string.encode()
|
||||||
except:
|
except Exception:
|
||||||
LOG.w("as_string().encode() fails", exc_info=True)
|
LOG.w("as_string().encode() fails", exc_info=True)
|
||||||
|
|
||||||
return msg_string.encode(errors="replace")
|
return msg_string.encode(errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def message_format_base64_parts(msg: Message) -> Message:
|
||||||
|
for part in msg.walk():
|
||||||
|
if part.get(
|
||||||
|
headers.CONTENT_TRANSFER_ENCODING
|
||||||
|
) == "base64" and part.get_content_type() in ("text/plain", "text/html"):
|
||||||
|
# Remove line breaks
|
||||||
|
body = re.sub("[\r\n]", "", part.get_payload())
|
||||||
|
# Split in 80 column lines
|
||||||
|
chunks = [
|
||||||
|
body[i : i + BASE64_LINELENGTH]
|
||||||
|
for i in range(0, len(body), BASE64_LINELENGTH)
|
||||||
|
]
|
||||||
|
part.set_payload("\r\n".join(chunks))
|
||||||
|
return msg
|
||||||
|
@ -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__ = (
|
||||||
|
@ -27,13 +27,15 @@ def send_newsletter_to_user(newsletter, user) -> (bool, str):
|
|||||||
comm_alias_id = comm_alias.id
|
comm_alias_id = comm_alias.id
|
||||||
|
|
||||||
unsubscribe_oneclick = unsubscribe_link
|
unsubscribe_oneclick = unsubscribe_link
|
||||||
if via_email:
|
if via_email and comm_alias_id > -1:
|
||||||
unsubscribe_oneclick = UnsubscribeEncoder.encode(
|
unsubscribe_oneclick = UnsubscribeEncoder.encode(
|
||||||
UnsubscribeAction.DisableAlias, comm_alias_id
|
UnsubscribeAction.DisableAlias,
|
||||||
)
|
comm_alias_id,
|
||||||
|
force_web=True,
|
||||||
|
).link
|
||||||
|
|
||||||
send_email(
|
send_email(
|
||||||
comm_alias.email,
|
comm_email,
|
||||||
newsletter.subject,
|
newsletter.subject,
|
||||||
text_template.render(
|
text_template.render(
|
||||||
user=user,
|
user=user,
|
||||||
|
@ -7,7 +7,7 @@ from app.session import RedisSessionStore
|
|||||||
|
|
||||||
def initialize_redis_services(app: flask.Flask, redis_url: str):
|
def initialize_redis_services(app: flask.Flask, redis_url: str):
|
||||||
|
|
||||||
if redis_url.startswith("redis://"):
|
if redis_url.startswith("redis://") or redis_url.startswith("rediss://"):
|
||||||
storage = limits.storage.RedisStorage(redis_url)
|
storage = limits.storage.RedisStorage(redis_url)
|
||||||
app.session_interface = RedisSessionStore(storage.storage, storage.storage, app)
|
app.session_interface = RedisSessionStore(storage.storage, storage.storage, app)
|
||||||
set_redis_concurrent_lock(storage)
|
set_redis_concurrent_lock(storage)
|
||||||
|
@ -387,7 +387,7 @@ Input:
|
|||||||
|
|
||||||
- `Authentication` header that contains the api key
|
- `Authentication` header that contains the api key
|
||||||
- (Optional but recommended) `hostname` passed in query string
|
- (Optional but recommended) `hostname` passed in query string
|
||||||
- (Optional) mode: either `uuid` or `word`. By default, use the user setting when creating new random alias.
|
- (Optional) mode: either `uuid` or `word` passed in query string. By default, use the user setting when creating new random alias.
|
||||||
- Request Message Body in json (`Content-Type` is `application/json`)
|
- Request Message Body in json (`Content-Type` is `application/json`)
|
||||||
- (Optional) note: alias note
|
- (Optional) note: alias note
|
||||||
|
|
||||||
@ -764,6 +764,7 @@ Input:
|
|||||||
|
|
||||||
- `Authentication` header that contains the api key
|
- `Authentication` header that contains the api key
|
||||||
- `mailbox_id`: in url
|
- `mailbox_id`: in url
|
||||||
|
- (optional) `transfer_aliases_to`: in body as json. id of the new mailbox for the aliases. If omitted or set to -1, the aliases will be delete with the mailbox.
|
||||||
|
|
||||||
Output:
|
Output:
|
||||||
|
|
||||||
|
@ -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 != "<>":
|
||||||
@ -836,10 +840,12 @@ def forward_email_to_mailbox(
|
|||||||
orig_subject = msg[headers.SUBJECT]
|
orig_subject = msg[headers.SUBJECT]
|
||||||
orig_subject = get_header_unicode(orig_subject)
|
orig_subject = get_header_unicode(orig_subject)
|
||||||
add_or_replace_header(msg, "Subject", mailbox.generic_subject)
|
add_or_replace_header(msg, "Subject", mailbox.generic_subject)
|
||||||
|
sender = msg[headers.FROM]
|
||||||
|
sender = get_header_unicode(sender)
|
||||||
msg = add_header(
|
msg = add_header(
|
||||||
msg,
|
msg,
|
||||||
f"""Forwarded by SimpleLogin to {alias.email} with "{orig_subject}" as subject""",
|
f"""Forwarded by SimpleLogin to {alias.email} from "{sender}" with "{orig_subject}" as subject""",
|
||||||
f"""Forwarded by SimpleLogin to {alias.email} with <b>{orig_subject}</b> as subject""",
|
f"""Forwarded by SimpleLogin to {alias.email} from "{sender}" with <b>{orig_subject}</b> as subject""",
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -1384,21 +1390,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(
|
||||||
|
@ -124,6 +124,58 @@ def welcome_proton(user):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_mailbox_job(job: Job):
|
||||||
|
mailbox_id = job.payload.get("mailbox_id")
|
||||||
|
mailbox = Mailbox.get(mailbox_id)
|
||||||
|
if not mailbox:
|
||||||
|
return
|
||||||
|
|
||||||
|
transfer_mailbox_id = job.payload.get("transfer_mailbox_id")
|
||||||
|
alias_transferred_to = None
|
||||||
|
if transfer_mailbox_id:
|
||||||
|
transfer_mailbox = Mailbox.get(transfer_mailbox_id)
|
||||||
|
if transfer_mailbox:
|
||||||
|
alias_transferred_to = transfer_mailbox.email
|
||||||
|
|
||||||
|
for alias in mailbox.aliases:
|
||||||
|
if alias.mailbox_id == mailbox.id:
|
||||||
|
alias.mailbox_id = transfer_mailbox.id
|
||||||
|
if transfer_mailbox in alias._mailboxes:
|
||||||
|
alias._mailboxes.remove(transfer_mailbox)
|
||||||
|
else:
|
||||||
|
alias._mailboxes.remove(mailbox)
|
||||||
|
if transfer_mailbox not in alias._mailboxes:
|
||||||
|
alias._mailboxes.append(transfer_mailbox)
|
||||||
|
Session.commit()
|
||||||
|
|
||||||
|
mailbox_email = mailbox.email
|
||||||
|
user = mailbox.user
|
||||||
|
Mailbox.delete(mailbox_id)
|
||||||
|
Session.commit()
|
||||||
|
LOG.d("Mailbox %s %s deleted", mailbox_id, mailbox_email)
|
||||||
|
|
||||||
|
if alias_transferred_to:
|
||||||
|
send_email(
|
||||||
|
user.email,
|
||||||
|
f"Your mailbox {mailbox_email} has been deleted",
|
||||||
|
f"""Mailbox {mailbox_email} and its alias have been transferred to {alias_transferred_to}.
|
||||||
|
Regards,
|
||||||
|
SimpleLogin team.
|
||||||
|
""",
|
||||||
|
retries=3,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
send_email(
|
||||||
|
user.email,
|
||||||
|
f"Your mailbox {mailbox_email} has been deleted",
|
||||||
|
f"""Mailbox {mailbox_email} along with its aliases have been deleted successfully.
|
||||||
|
Regards,
|
||||||
|
SimpleLogin team.
|
||||||
|
""",
|
||||||
|
retries=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def process_job(job: Job):
|
def process_job(job: Job):
|
||||||
if job.name == config.JOB_ONBOARDING_1:
|
if job.name == config.JOB_ONBOARDING_1:
|
||||||
user_id = job.payload.get("user_id")
|
user_id = job.payload.get("user_id")
|
||||||
@ -178,27 +230,7 @@ def process_job(job: Job):
|
|||||||
retries=3,
|
retries=3,
|
||||||
)
|
)
|
||||||
elif job.name == config.JOB_DELETE_MAILBOX:
|
elif job.name == config.JOB_DELETE_MAILBOX:
|
||||||
mailbox_id = job.payload.get("mailbox_id")
|
delete_mailbox_job(job)
|
||||||
mailbox = Mailbox.get(mailbox_id)
|
|
||||||
if not mailbox:
|
|
||||||
return
|
|
||||||
|
|
||||||
mailbox_email = mailbox.email
|
|
||||||
user = mailbox.user
|
|
||||||
|
|
||||||
Mailbox.delete(mailbox_id)
|
|
||||||
Session.commit()
|
|
||||||
LOG.d("Mailbox %s %s deleted", mailbox_id, mailbox_email)
|
|
||||||
|
|
||||||
send_email(
|
|
||||||
user.email,
|
|
||||||
f"Your mailbox {mailbox_email} has been deleted",
|
|
||||||
f"""Mailbox {mailbox_email} along with its aliases are deleted successfully.
|
|
||||||
Regards,
|
|
||||||
SimpleLogin team.
|
|
||||||
""",
|
|
||||||
retries=3,
|
|
||||||
)
|
|
||||||
|
|
||||||
elif job.name == config.JOB_DELETE_DOMAIN:
|
elif job.name == config.JOB_DELETE_DOMAIN:
|
||||||
custom_domain_id = job.payload.get("custom_domain_id")
|
custom_domain_id = job.payload.get("custom_domain_id")
|
||||||
|
@ -3552,7 +3552,6 @@ impute
|
|||||||
inaner
|
inaner
|
||||||
inborn
|
inborn
|
||||||
inbred
|
inbred
|
||||||
incest
|
|
||||||
inched
|
inched
|
||||||
inches
|
inches
|
||||||
incing
|
incing
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -149803,11 +149803,6 @@ incessant
|
|||||||
incessantly
|
incessantly
|
||||||
incessantness
|
incessantness
|
||||||
incession
|
incession
|
||||||
incest
|
|
||||||
incests
|
|
||||||
incestuous
|
|
||||||
incestuously
|
|
||||||
incestuousness
|
|
||||||
incgrporate
|
incgrporate
|
||||||
inch
|
inch
|
||||||
inchain
|
inchain
|
||||||
@ -204633,9 +204628,6 @@ nonincandescent
|
|||||||
nonincandescently
|
nonincandescently
|
||||||
nonincarnate
|
nonincarnate
|
||||||
nonincarnated
|
nonincarnated
|
||||||
nonincestuous
|
|
||||||
nonincestuously
|
|
||||||
nonincestuousness
|
|
||||||
nonincident
|
nonincident
|
||||||
nonincidental
|
nonincidental
|
||||||
nonincidentally
|
nonincidentally
|
||||||
@ -344408,8 +344400,6 @@ unincarnated
|
|||||||
unincensed
|
unincensed
|
||||||
uninceptive
|
uninceptive
|
||||||
uninceptively
|
uninceptively
|
||||||
unincestuous
|
|
||||||
unincestuously
|
|
||||||
uninchoative
|
uninchoative
|
||||||
unincidental
|
unincidental
|
||||||
unincidentally
|
unincidentally
|
||||||
@ -370100,4 +370090,4 @@ zwinglianism
|
|||||||
zwinglianist
|
zwinglianist
|
||||||
zwitter
|
zwitter
|
||||||
zwitterion
|
zwitterion
|
||||||
zwitterionic
|
zwitterionic
|
||||||
|
@ -44,6 +44,7 @@ from app.admin_model import (
|
|||||||
NewsletterUserAdmin,
|
NewsletterUserAdmin,
|
||||||
DailyMetricAdmin,
|
DailyMetricAdmin,
|
||||||
MetricAdmin,
|
MetricAdmin,
|
||||||
|
InvalidMailboxDomainAdmin,
|
||||||
)
|
)
|
||||||
from app.api.base import api_bp
|
from app.api.base import api_bp
|
||||||
from app.auth.base import auth_bp
|
from app.auth.base import auth_bp
|
||||||
@ -105,6 +106,7 @@ from app.models import (
|
|||||||
NewsletterUser,
|
NewsletterUser,
|
||||||
DailyMetric,
|
DailyMetric,
|
||||||
Metric2,
|
Metric2,
|
||||||
|
InvalidMailboxDomain,
|
||||||
)
|
)
|
||||||
from app.monitor.base import monitor_bp
|
from app.monitor.base import monitor_bp
|
||||||
from app.newsletter_utils import send_newsletter_to_user
|
from app.newsletter_utils import send_newsletter_to_user
|
||||||
@ -764,6 +766,7 @@ def init_admin(app):
|
|||||||
admin.add_view(NewsletterUserAdmin(NewsletterUser, Session))
|
admin.add_view(NewsletterUserAdmin(NewsletterUser, Session))
|
||||||
admin.add_view(DailyMetricAdmin(DailyMetric, Session))
|
admin.add_view(DailyMetricAdmin(DailyMetric, Session))
|
||||||
admin.add_view(MetricAdmin(Metric2, Session))
|
admin.add_view(MetricAdmin(Metric2, Session))
|
||||||
|
admin.add_view(InvalidMailboxDomainAdmin(InvalidMailboxDomain, Session))
|
||||||
|
|
||||||
|
|
||||||
def register_custom_commands(app):
|
def register_custom_commands(app):
|
||||||
|
@ -50,7 +50,9 @@
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
This Youtube video can also quickly walk you through the steps:
|
This Youtube video can also quickly walk you through the steps:
|
||||||
<a href="https://www.youtube.com/watch?v=VsypF-DBaow" target="_blank">
|
<a href="https://www.youtube.com/watch?v=VsypF-DBaow"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer">
|
||||||
How to send emails from an alias <i class="fe fe-external-link"></i>
|
How to send emails from an alias <i class="fe fe-external-link"></i>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
@ -43,6 +43,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
|
{{ csrf_form.csrf_token }}
|
||||||
<input type="hidden" name="form-name" value="delete">
|
<input type="hidden" name="form-name" value="delete">
|
||||||
<input type="hidden" name="api-key-id" value="{{ api_key.id }}">
|
<input type="hidden" name="api-key-id" value="{{ api_key.id }}">
|
||||||
<span class="card-link btn btn-link float-right text-danger delete-api-key">Delete</span>
|
<span class="card-link btn btn-link float-right text-danger delete-api-key">Delete</span>
|
||||||
@ -57,6 +58,7 @@
|
|||||||
{% if api_keys|length > 0 %}
|
{% if api_keys|length > 0 %}
|
||||||
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
|
{{ csrf_form.csrf_token }}
|
||||||
<input type="hidden" name="form-name" value="delete-all">
|
<input type="hidden" name="form-name" value="delete-all">
|
||||||
<span class="delete btn btn-outline-danger delete-all-api-keys float-right">
|
<span class="delete btn btn-outline-danger delete-all-api-keys float-right">
|
||||||
Delete All <i class="fe fe-trash"></i>
|
Delete All <i class="fe fe-trash"></i>
|
||||||
@ -66,7 +68,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<hr />
|
<hr />
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{{ new_api_key_form.csrf_token }}
|
{{ csrf_form.csrf_token }}
|
||||||
<input type="hidden" name="form-name" value="create">
|
<input type="hidden" name="form-name" value="create">
|
||||||
<h2 class="h4">New API Key</h2>
|
<h2 class="h4">New API Key</h2>
|
||||||
{{ new_api_key_form.name(class="form-control", placeholder="Chrome") }}
|
{{ new_api_key_form.name(class="form-control", placeholder="Chrome") }}
|
||||||
|
@ -23,7 +23,9 @@
|
|||||||
|
|
||||||
<div class="alert alert-danger" role="alert">
|
<div class="alert alert-danger" role="alert">
|
||||||
This feature is only available on Premium plan.
|
This feature is only available on Premium plan.
|
||||||
<a href="{{ URL }}/dashboard/pricing" target="_blank" rel="noopener">
|
<a href="{{ URL }}/dashboard/pricing"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer">
|
||||||
Upgrade<i class="fe fe-external-link"></i>
|
Upgrade<i class="fe fe-external-link"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -78,7 +78,7 @@
|
|||||||
data-clipboard-text=".*suffix">.*suffix</em>
|
data-clipboard-text=".*suffix">.*suffix</em>
|
||||||
<br />
|
<br />
|
||||||
To test out regex, we recommend using regex tester tool like
|
To test out regex, we recommend using regex tester tool like
|
||||||
<a href="https://regex101.com" target="_blank">https://regex101.com↗</a>
|
<a href="https://regex101.com" target="_blank" rel="noopener noreferrer">https://regex101.com↗</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -158,7 +158,7 @@
|
|||||||
SPF
|
SPF
|
||||||
<a href="https://en.wikipedia.org/wiki/Sender_Policy_Framework"
|
<a href="https://en.wikipedia.org/wiki/Sender_Policy_Framework"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener">(Wikipedia↗)</a>
|
rel="noopener noreferrer">(Wikipedia↗)</a>
|
||||||
is an email
|
is an email
|
||||||
authentication method
|
authentication method
|
||||||
designed to detect forging sender addresses during the delivery of the email.
|
designed to detect forging sender addresses during the delivery of the email.
|
||||||
@ -229,7 +229,7 @@
|
|||||||
DKIM
|
DKIM
|
||||||
<a href="https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail"
|
<a href="https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener">(Wikipedia↗)</a>
|
rel="noopener noreferrer">(Wikipedia↗)</a>
|
||||||
is an
|
is an
|
||||||
email
|
email
|
||||||
authentication method
|
authentication method
|
||||||
@ -335,7 +335,7 @@
|
|||||||
DMARC
|
DMARC
|
||||||
<a href="https://en.wikipedia.org/wiki/DMARC"
|
<a href="https://en.wikipedia.org/wiki/DMARC"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener">
|
rel="noopener noreferrer">
|
||||||
(Wikipedia↗)
|
(Wikipedia↗)
|
||||||
</a>
|
</a>
|
||||||
is designed to protect the domain from unauthorized use, commonly known as email spoofing.
|
is designed to protect the domain from unauthorized use, commonly known as email spoofing.
|
||||||
|
@ -25,17 +25,18 @@
|
|||||||
<div class="alert alert-primary collapse {% if mailboxes|length == 1 %} show{% endif %}"
|
<div class="alert alert-primary collapse {% if mailboxes|length == 1 %} show{% endif %}"
|
||||||
id="howtouse"
|
id="howtouse"
|
||||||
role="alert">
|
role="alert">
|
||||||
A <em>mailbox</em> is just another personal email address. When creating a new alias, you could choose the
|
A <em>mailbox</em> is just another personal email address. When creating a new alias, you could choose
|
||||||
|
the
|
||||||
mailbox that <em>owns</em> this alias, i.e:
|
mailbox that <em>owns</em> this alias, i.e:
|
||||||
<br />
|
<br/>
|
||||||
- all emails sent to this alias will be forwarded to this mailbox
|
- all emails sent to this alias will be forwarded to this mailbox
|
||||||
<br />
|
<br/>
|
||||||
- from this mailbox, you can reply/send emails from the alias.
|
- from this mailbox, you can reply/send emails from the alias.
|
||||||
<br />
|
<br/>
|
||||||
<br />
|
<br/>
|
||||||
When you signed up, a mailbox is automatically created with your email <b>{{ current_user.email }}</b>
|
When you signed up, a mailbox is automatically created with your email <b>{{ current_user.email }}</b>
|
||||||
<br />
|
<br/>
|
||||||
<br />
|
<br/>
|
||||||
The mailbox doesn't have to be your email: it can be your friend's email
|
The mailbox doesn't have to be your email: it can be your friend's email
|
||||||
if you want to create aliases for your buddy.
|
if you want to create aliases for your buddy.
|
||||||
</div>
|
</div>
|
||||||
@ -74,11 +75,12 @@
|
|||||||
</h5>
|
</h5>
|
||||||
<h6 class="card-subtitle mb-2 text-muted">
|
<h6 class="card-subtitle mb-2 text-muted">
|
||||||
Created {{ mailbox.created_at | dt }}
|
Created {{ mailbox.created_at | dt }}
|
||||||
<br />
|
<br/>
|
||||||
<span class="font-weight-bold">{{ mailbox.nb_alias() }}</span> aliases.
|
<span class="font-weight-bold">{{ mailbox.nb_alias() }}</span> aliases.
|
||||||
<br />
|
<br/>
|
||||||
</h6>
|
</h6>
|
||||||
<a href="{{ url_for('dashboard.mailbox_detail_route', mailbox_id=mailbox.id) }}">Edit ➡</a>
|
<a href="{{ url_for('dashboard.mailbox_detail_route', mailbox_id=mailbox.id) }}">Edit
|
||||||
|
➡</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer p-0">
|
<div class="card-footer p-0">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -89,7 +91,7 @@
|
|||||||
{{ csrf_form.csrf_token }}
|
{{ csrf_form.csrf_token }}
|
||||||
<input type="hidden" name="form-name" value="set-default">
|
<input type="hidden" name="form-name" value="set-default">
|
||||||
<input type="hidden" class="mailbox" value="{{ mailbox.email }}">
|
<input type="hidden" class="mailbox" value="{{ mailbox.email }}">
|
||||||
<input type="hidden" name="mailbox-id" value="{{ mailbox.id }}">
|
<input type="hidden" name="mailbox_id" value="{{ mailbox.id }}">
|
||||||
<button class="card-link btn btn-link {% if mailbox.id == current_user.default_mailbox_id %} disabled{% endif %}">
|
<button class="card-link btn btn-link {% if mailbox.id == current_user.default_mailbox_id %} disabled{% endif %}">
|
||||||
Set As Default Mailbox
|
Set As Default Mailbox
|
||||||
</button>
|
</button>
|
||||||
@ -98,10 +100,24 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{{ csrf_form.csrf_token }}
|
{{ delete_mailbox_form.csrf_token }}
|
||||||
<input type="hidden" name="form-name" value="delete">
|
<input type="hidden" name="form-name" value="delete">
|
||||||
<input type="hidden" class="mailbox" value="{{ mailbox.email }}">
|
<input type="hidden" class="mailbox" value="{{ mailbox.email }}">
|
||||||
<input type="hidden" name="mailbox-id" value="{{ mailbox.id }}">
|
<input type="hidden" name="mailbox_id" value="{{ mailbox.id }}">
|
||||||
|
<select hidden name="transfer_mailbox_id" value="">
|
||||||
|
<option value="-1">
|
||||||
|
Delete my aliases
|
||||||
|
</option>
|
||||||
|
{% for mailbox_opt in mailboxes %}
|
||||||
|
|
||||||
|
{% if mailbox_opt.verified and mailbox_opt.id != mailbox.id %}
|
||||||
|
|
||||||
|
<option value="{{ mailbox_opt.id }}">
|
||||||
|
{{ mailbox_opt.email }}
|
||||||
|
</option>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
<span class="card-link btn btn-link text-danger float-right delete-mailbox {% if mailbox.id == current_user.default_mailbox_id %} disabled{% endif %}">
|
<span class="card-link btn btn-link text-danger float-right delete-mailbox {% if mailbox.id == current_user.default_mailbox_id %} disabled{% endif %}">
|
||||||
Delete
|
Delete
|
||||||
</span>
|
</span>
|
||||||
@ -128,31 +144,39 @@
|
|||||||
{% block script %}
|
{% block script %}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(".delete-mailbox").on("click", function (e) {
|
$(".delete-mailbox").on("click", function (e) {
|
||||||
let mailbox = $(this).parent().find(".mailbox").val();
|
let mailbox = $(this).parent().find(".mailbox").val();
|
||||||
|
|
||||||
let that = $(this);
|
let new_mailboxes = $(this).parent().find("select[name='transfer_mailbox_id']").find("option")
|
||||||
let message = `All aliases owned by this mailbox <b>${mailbox}</b> will be also deleted, ` +
|
let inputOptions = new_mailboxes.map((index, option) => { return {["value"]: option.value, ["text"]: option.text}}).toArray()
|
||||||
" please confirm.";
|
|
||||||
|
|
||||||
bootbox.confirm({
|
let that = $(this);
|
||||||
message: message,
|
let message = `All aliases owned by the mailbox <b>${mailbox}</b> will be also deleted.<br>` +
|
||||||
buttons: {
|
"You can choose to transfer them to a different mailbox:<br><br>";
|
||||||
confirm: {
|
|
||||||
label: 'Yes, delete it',
|
bootbox.prompt({
|
||||||
className: 'btn-danger'
|
title: '<b>Delete Mailbox</b>',
|
||||||
},
|
message: message,
|
||||||
cancel: {
|
value: ["-1"],
|
||||||
label: 'Cancel',
|
inputType: 'select',
|
||||||
className: 'btn-outline-primary'
|
inputOptions: inputOptions,
|
||||||
}
|
buttons: {
|
||||||
},
|
confirm: {
|
||||||
callback: function (result) {
|
label: 'Yes, delete it',
|
||||||
if (result) {
|
className: 'btn-danger'
|
||||||
that.closest("form").submit();
|
},
|
||||||
}
|
cancel: {
|
||||||
}
|
label: 'Cancel',
|
||||||
})
|
className: 'btn-outline-primary mr-auto'
|
||||||
});
|
}
|
||||||
|
},
|
||||||
|
callback: function (result) {
|
||||||
|
if (result) {
|
||||||
|
that.closest("form").find("select[name='transfer_mailbox_id']").val(result)
|
||||||
|
that.closest("form").submit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
or use WebAuthn (FIDO).
|
or use WebAuthn (FIDO).
|
||||||
</div>
|
</div>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
|
{{ csrf_form.csrf_token }}
|
||||||
<button class="btn btn-danger mt-2">Disable TOTP</button>
|
<button class="btn btn-danger mt-2">Disable TOTP</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -72,7 +72,7 @@ PGP Encryption
|
|||||||
</ul>
|
</ul>
|
||||||
<div class="small-text">
|
<div class="small-text">
|
||||||
More information on our
|
More information on our
|
||||||
<a href="https://simplelogin.io/pricing" target="_blank" rel="noopener">
|
<a href="https://simplelogin.io/pricing" target="_blank" rel="noopener noreferrer">
|
||||||
Pricing
|
Pricing
|
||||||
Page <i class="fe fe-external-link"></i>
|
Page <i class="fe fe-external-link"></i>
|
||||||
</a>
|
</a>
|
||||||
@ -120,7 +120,7 @@ Upgrade your SimpleLogin account
|
|||||||
<div id="normal-upgrade" class="{% if proton_upgrade %} collapse{% endif %}">
|
<div id="normal-upgrade" class="{% if proton_upgrade %} collapse{% endif %}">
|
||||||
<div class="display-6 my-3">
|
<div class="display-6 my-3">
|
||||||
🔐 Secure payments by
|
🔐 Secure payments by
|
||||||
<a href="https://paddle.com" target="_blank" rel="noopener">
|
<a href="https://paddle.com" target="_blank" rel="noopener noreferrer">
|
||||||
Paddle <i class="fe fe-external-link"></i>
|
Paddle <i class="fe fe-external-link"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -164,23 +164,19 @@ $4/month
|
|||||||
<hr />
|
<hr />
|
||||||
<i class="fa fa-bitcoin"></i>
|
<i class="fa fa-bitcoin"></i>
|
||||||
Payment via
|
Payment via
|
||||||
<a href="https://commerce.coinbase.com/?lang=en" target="_blank">
|
<a href="https://commerce.coinbase.com/?lang=en" target="_blank" rel="noopener noreferrer">
|
||||||
Coinbase Commerce<i class="fe fe-external-link"></i>
|
Coinbase Commerce<i class="fe fe-external-link"></i>
|
||||||
</a>
|
</a>
|
||||||
<br />
|
<br />
|
||||||
Currently Bitcoin, Bitcoin Cash, Dai, Ethereum, Litecoin and USD Coin are supported.
|
Currently Bitcoin, Bitcoin Cash, Dai, Ethereum, Litecoin and USD Coin are supported.
|
||||||
<br />
|
<br />
|
||||||
<a class="btn btn-outline-primary" href="{{ url_for('dashboard.coinbase_checkout_route') }}" target="_blank">
|
<a class="btn btn-outline-primary" href="{{ url_for('dashboard.coinbase_checkout_route') }}" target="_blank" rel="noopener noreferrer">
|
||||||
Yearly billing - Crypto
|
Yearly billing - Crypto
|
||||||
<br />
|
<br />
|
||||||
$30/year
|
$30/year
|
||||||
<i class="fe fe-external-link"></i>
|
<i class="fe fe-external-link"></i>
|
||||||
</a>
|
</a>
|
||||||
<hr />
|
<hr />
|
||||||
For other payment options, please send us an email at
|
|
||||||
<a href="mailto:hi@simplelogin.io">hi@simplelogin.io</a>
|
|
||||||
.
|
|
||||||
<br />
|
|
||||||
If you have bought a coupon, please go to the
|
If you have bought a coupon, please go to the
|
||||||
<a href="{{ url_for('dashboard.coupon_route') }}">coupon page</a>
|
<a href="{{ url_for('dashboard.coupon_route') }}">coupon page</a>
|
||||||
to apply the coupon code.
|
to apply the coupon code.
|
||||||
|
@ -17,7 +17,8 @@
|
|||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
This page shows all emails that are either refused by your mailbox (bounced) or detected as spams/phishing (quarantine) via our
|
This page shows all emails that are either refused by your mailbox (bounced) or detected as spams/phishing (quarantine) via our
|
||||||
<a href="https://simplelogin.io/docs/getting-started/anti-phishing/"
|
<a href="https://simplelogin.io/docs/getting-started/anti-phishing/"
|
||||||
target="_blank">anti-phishing program ↗</a>
|
target="_blank"
|
||||||
|
rel="noopener noreferrer">anti-phishing program ↗</a>
|
||||||
<ul class="p-4 mb-0">
|
<ul class="p-4 mb-0">
|
||||||
<li>
|
<li>
|
||||||
If the email is indeed spam, this means the alias is now in the hands of a spammer,
|
If the email is indeed spam, this means the alias is now in the hands of a spammer,
|
||||||
@ -26,7 +27,8 @@
|
|||||||
<li>
|
<li>
|
||||||
If the email isn't spam and your mailbox refuses the email, we recommend to create a <b>filter</b> to avoid your mailbox provider from blocking legitimate emails. Please refer to
|
If the email isn't spam and your mailbox refuses the email, we recommend to create a <b>filter</b> to avoid your mailbox provider from blocking legitimate emails. Please refer to
|
||||||
<a href="https://simplelogin.io/docs/getting-started/troubleshooting/#emails-end-up-in-spam"
|
<a href="https://simplelogin.io/docs/getting-started/troubleshooting/#emails-end-up-in-spam"
|
||||||
target="_blank">Setting up filter for SimpleLogin emails ↗</a>
|
target="_blank"
|
||||||
|
rel="noopener noreferrer">Setting up filter for SimpleLogin emails ↗</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
If the email is flagged as spams/phishing, this means that the sender explicitly states their emails should respect
|
If the email is flagged as spams/phishing, this means that the sender explicitly states their emails should respect
|
||||||
|
@ -73,7 +73,8 @@
|
|||||||
Yearly plan subscribed with cryptocurrency which expires on
|
Yearly plan subscribed with cryptocurrency which expires on
|
||||||
{{ coinbase_sub.end_at.format("YYYY-MM-DD") }}.
|
{{ coinbase_sub.end_at.format("YYYY-MM-DD") }}.
|
||||||
<a href="{{ url_for('dashboard.coinbase_checkout_route') }}"
|
<a href="{{ url_for('dashboard.coinbase_checkout_route') }}"
|
||||||
target="_blank">
|
target="_blank"
|
||||||
|
rel="noopener noreferrer">
|
||||||
Extend Subscription <i class="fe fe-external-link"></i>
|
Extend Subscription <i class="fe fe-external-link"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
This feature is only available on Premium plan.
|
This feature is only available on Premium plan.
|
||||||
<a href="{{ url_for('dashboard.pricing') }}"
|
<a href="{{ url_for('dashboard.pricing') }}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener">
|
rel="noopener noreferrer">
|
||||||
Upgrade<i class="fe fe-external-link"></i>
|
Upgrade<i class="fe fe-external-link"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -38,7 +38,7 @@
|
|||||||
Handy when you need to quickly give out an email address, for example on a phone call, in a meeting or just
|
Handy when you need to quickly give out an email address, for example on a phone call, in a meeting or just
|
||||||
anywhere you want.
|
anywhere you want.
|
||||||
<br />
|
<br />
|
||||||
After choosing a subdomain, simply use <b>anything@my-subdomain.simplelogin.co</b>
|
After choosing a subdomain, simply use <b>anything@my-subdomain.simplelogin.com</b>
|
||||||
next time you need an alias:
|
next time you need an alias:
|
||||||
it'll be <b>automatically created</b> the first time it receives an email.
|
it'll be <b>automatically created</b> the first time it receives an email.
|
||||||
<br />
|
<br />
|
||||||
@ -72,6 +72,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="h4 mb-1">New Subdomain</h2>
|
<h2 class="h4 mb-1">New Subdomain</h2>
|
||||||
<form method="post" class="mt-2" data-parsley-validate>
|
<form method="post" class="mt-2" data-parsley-validate>
|
||||||
|
{{ new_subdomain_form.csrf_token }}
|
||||||
<input type="hidden" name="form-name" value="create">
|
<input type="hidden" name="form-name" value="create">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Subdomain</label>
|
<label>Subdomain</label>
|
||||||
|
@ -33,6 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<a href="https://docs.simplelogin.io"
|
<a href="https://docs.simplelogin.io"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
class="btn btn-block btn-secondary mt-4">
|
class="btn btn-block btn-secondary mt-4">
|
||||||
Documentation <i class="fe fe-external-link"></i>
|
Documentation <i class="fe fe-external-link"></i>
|
||||||
</a>
|
</a>
|
||||||
|
@ -10,7 +10,9 @@
|
|||||||
<h4 class="alert-heading">Well done!</h4>
|
<h4 class="alert-heading">Well done!</h4>
|
||||||
<p>
|
<p>
|
||||||
Please head to our
|
Please head to our
|
||||||
<a href="https://docs.simplelogin.io" target="_blank" rel="noopener">
|
<a href="https://docs.simplelogin.io"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer">
|
||||||
documentation <i class="fe fe-external-link"></i>
|
documentation <i class="fe fe-external-link"></i>
|
||||||
</a>
|
</a>
|
||||||
to see how to add SIWSL into your app.
|
to see how to add SIWSL into your app.
|
||||||
|
@ -49,6 +49,7 @@
|
|||||||
<a href="{{ url_for('developer.new_client') }}" class="btn btn-primary">New website</a>
|
<a href="{{ url_for('developer.new_client') }}" class="btn btn-primary">New website</a>
|
||||||
<a href="https://docs.simplelogin.io"
|
<a href="https://docs.simplelogin.io"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
class="ml-2 btn btn-secondary">
|
class="ml-2 btn btn-secondary">
|
||||||
Docs <i class="fe fe-external-link"></i>
|
Docs <i class="fe fe-external-link"></i>
|
||||||
</a>
|
</a>
|
||||||
|
@ -13,7 +13,9 @@
|
|||||||
|
|
||||||
<div class="col-sm-4 col-xl-2">
|
<div class="col-sm-4 col-xl-2">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<a href="{{ client.home_url }}" target="_blank" rel="noopener">
|
<a href="{{ client.home_url }}"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer">
|
||||||
<img class="card-img-top" src="{{ client.get_icon_url() }}">
|
<img class="card-img-top" src="{{ client.get_icon_url() }}">
|
||||||
</a>
|
</a>
|
||||||
<div class="card-body d-flex flex-column">
|
<div class="card-body d-flex flex-column">
|
||||||
|
@ -46,7 +46,7 @@ https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
|
|||||||
<a href="{{ link }}"
|
<a href="{{ link }}"
|
||||||
class="f-fallback button"
|
class="f-fallback button"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener noreferrer"
|
||||||
style="color: #FFF;
|
style="color: #FFF;
|
||||||
border-color: #3869d4;
|
border-color: #3869d4;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
|
@ -9,8 +9,7 @@
|
|||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
Please contact us at
|
Please <a href="https://app.simplelogin.io/dashboard/support">contact us</a>
|
||||||
<a href="mailto:hi@simplelogin.io">hi@simplelogin.io</a>
|
|
||||||
to renew your subscription.
|
to renew your subscription.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
|
@ -2,4 +2,6 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
Your subscription will end on {{ manual_sub.end_at.format("YYYY-MM-DD") }}
|
Your subscription will end on {{ manual_sub.end_at.format("YYYY-MM-DD") }}
|
||||||
|
|
||||||
|
Please contact us on https://app.simplelogin.io/dashboard/support to renew your subscription.
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
{{ render_text("You recently requested to change mailbox <b>"+ mailbox_email +"</b> to <b>" + mailbox_new_email + "</b>.") }}
|
{{ render_text("You recently requested to change mailbox <b>"+ mailbox_email +"</b> to <b>" + mailbox_new_email + "</b>.") }}
|
||||||
{{ render_text("To confirm, please click on the button below.") }}
|
{{ render_text("To confirm, please click on the button below.") }}
|
||||||
{{ render_button("Confirm mailbox change", link) }}
|
{{ render_button("Confirm mailbox change", link) }}
|
||||||
|
{{ render_text("This email will only be valid for the next 15 minutes.") }}
|
||||||
{{ render_text('Thanks,
|
{{ render_text('Thanks,
|
||||||
<br />
|
<br />
|
||||||
SimpleLogin Team.') }}
|
SimpleLogin Team.') }}
|
||||||
|
@ -8,4 +8,6 @@ You recently requested to change mailbox {{mailbox_email}} to {{mailbox_new_emai
|
|||||||
To confirm, please click on this link:
|
To confirm, please click on this link:
|
||||||
|
|
||||||
{{link}}
|
{{link}}
|
||||||
|
|
||||||
|
This link will only be valid during the next 15 minutes.
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
{{ render_text("You have added <b>"+ mailbox_email +"</b> as an additional mailbox.") }}
|
{{ render_text("You have added <b>"+ mailbox_email +"</b> as an additional mailbox.") }}
|
||||||
{{ render_text("To confirm, please click on the button below.") }}
|
{{ render_text("To confirm, please click on the button below.") }}
|
||||||
{{ render_button("Confirm mailbox", link) }}
|
{{ render_button("Confirm mailbox", link) }}
|
||||||
|
{{ render_text("This email will only be valid for the next 15 minutes.") }}
|
||||||
{{ render_text('Thanks,
|
{{ render_text('Thanks,
|
||||||
<br />
|
<br />
|
||||||
SimpleLogin Team.') }}
|
SimpleLogin Team.') }}
|
||||||
|
@ -8,4 +8,6 @@ You have added {{mailbox_email}} as an additional mailbox.
|
|||||||
To confirm, please click on this link:
|
To confirm, please click on this link:
|
||||||
|
|
||||||
{{link}}
|
{{link}}
|
||||||
|
|
||||||
|
This link will only be valid during the next 15 minutes.
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -145,28 +145,28 @@
|
|||||||
<ul class="list-group list-group-transparent list-group-white list-group-flush list-group-borderless mb-0 footer-list-group">
|
<ul class="list-group list-group-transparent list-group-white list-group-flush list-group-borderless mb-0 footer-list-group">
|
||||||
<li>
|
<li>
|
||||||
<a class="list-group-item text-white footer-item "
|
<a class="list-group-item text-white footer-item "
|
||||||
rel="noopener"
|
rel="noopener noreferrer"
|
||||||
href="https://chrome.google.com/webstore/detail/dphilobhebphkdjbpfohgikllaljmgbn">
|
href="https://chrome.google.com/webstore/detail/dphilobhebphkdjbpfohgikllaljmgbn">
|
||||||
Chrome Extension
|
Chrome Extension
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="list-group-item text-white footer-item "
|
<a class="list-group-item text-white footer-item "
|
||||||
rel="noopener"
|
rel="noopener noreferrer"
|
||||||
href="https://addons.mozilla.org/firefox/addon/simplelogin/">
|
href="https://addons.mozilla.org/firefox/addon/simplelogin/">
|
||||||
Firefox Add-on
|
Firefox Add-on
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="list-group-item text-white footer-item "
|
<a class="list-group-item text-white footer-item "
|
||||||
rel="noopener"
|
rel="noopener noreferrer"
|
||||||
href="https://microsoftedge.microsoft.com/addons/detail/simpleloginreceive-sen/diacfpipniklenphgljfkmhinphjlfff">
|
href="https://microsoftedge.microsoft.com/addons/detail/simpleloginreceive-sen/diacfpipniklenphgljfkmhinphjlfff">
|
||||||
Edge Add-on
|
Edge Add-on
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="list-group-item text-white footer-item "
|
<a class="list-group-item text-white footer-item "
|
||||||
rel="noopener"
|
rel="noopener noreferrer"
|
||||||
href="https://apps.apple.com/app/id1494051017">
|
href="https://apps.apple.com/app/id1494051017">
|
||||||
Safari
|
Safari
|
||||||
Extension
|
Extension
|
||||||
@ -174,7 +174,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="list-group-item text-white footer-item "
|
<a class="list-group-item text-white footer-item "
|
||||||
rel="noopener"
|
rel="noopener noreferrer"
|
||||||
href="https://apps.apple.com/app/id1494359858">
|
href="https://apps.apple.com/app/id1494359858">
|
||||||
iOS
|
iOS
|
||||||
(App Store)
|
(App Store)
|
||||||
@ -182,14 +182,14 @@
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="list-group-item text-white footer-item "
|
<a class="list-group-item text-white footer-item "
|
||||||
rel="noopener"
|
rel="noopener noreferrer"
|
||||||
href="https://play.google.com/store/apps/details?id=io.simplelogin.android">
|
href="https://play.google.com/store/apps/details?id=io.simplelogin.android">
|
||||||
Android (Play Store)
|
Android (Play Store)
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="list-group-item text-white footer-item "
|
<a class="list-group-item text-white footer-item "
|
||||||
rel="noopener"
|
rel="noopener noreferrer"
|
||||||
href="https://f-droid.org/en/packages/io.simplelogin.android.fdroid/">
|
href="https://f-droid.org/en/packages/io.simplelogin.android.fdroid/">
|
||||||
Android (F-Droid)
|
Android (F-Droid)
|
||||||
</a>
|
</a>
|
||||||
|
@ -75,14 +75,17 @@
|
|||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Help</a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Help</a>
|
||||||
<div class="dropdown-menu dropdown-menu-left dropdown-menu-arrow">
|
<div class="dropdown-menu dropdown-menu-left dropdown-menu-arrow">
|
||||||
<div class="dropdown-item">
|
<div class="dropdown-item">
|
||||||
<a href="https://simplelogin.io/docs/" target="_blank">
|
<a href="https://simplelogin.io/docs/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer">
|
||||||
Docs
|
Docs
|
||||||
<i class="fa fa-external-link" aria-hidden="true"></i>
|
<i class="fa fa-external-link" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-item">
|
<div class="dropdown-item">
|
||||||
<a href="https://github.com/simple-login/app/discussions"
|
<a href="https://github.com/simple-login/app/discussions"
|
||||||
target="_blank">
|
target="_blank"
|
||||||
|
rel="noopener noreferrer">
|
||||||
Forum
|
Forum
|
||||||
<i class="fa fa-external-link" aria-hidden="true"></i>
|
<i class="fa fa-external-link" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
@ -94,7 +97,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="nav-item">
|
<div class="nav-item">
|
||||||
<a href="https://simplelogin.io/docs/" target="_blank">
|
<a href="https://simplelogin.io/docs/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer">
|
||||||
Docs
|
Docs
|
||||||
<i class="fa fa-external-link" aria-hidden="true"></i>
|
<i class="fa fa-external-link" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
|
@ -98,14 +98,17 @@
|
|||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Help</a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Help</a>
|
||||||
<div class="dropdown-menu dropdown-menu-left dropdown-menu-arrow">
|
<div class="dropdown-menu dropdown-menu-left dropdown-menu-arrow">
|
||||||
<div class="dropdown-item">
|
<div class="dropdown-item">
|
||||||
<a href="https://simplelogin.io/docs/" target="_blank">
|
<a href="https://simplelogin.io/docs/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer">
|
||||||
Docs
|
Docs
|
||||||
<i class="fa fa-external-link" aria-hidden="true"></i>
|
<i class="fa fa-external-link" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-item">
|
<div class="dropdown-item">
|
||||||
<a href="https://github.com/simple-login/app/discussions"
|
<a href="https://github.com/simple-login/app/discussions"
|
||||||
target="_blank">
|
target="_blank"
|
||||||
|
rel="noopener noreferrer">
|
||||||
Forum
|
Forum
|
||||||
<i class="fa fa-external-link" aria-hidden="true"></i>
|
<i class="fa fa-external-link" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
|
39
app/tests/example_emls/bad_base64format.eml
Normal file
39
app/tests/example_emls/bad_base64format.eml
Normal file
File diff suppressed because one or more lines are too long
90
app/tests/jobs/test_delete_mailbox_job.py
Normal file
90
app/tests/jobs/test_delete_mailbox_job.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
from sqlalchemy_utils.types.arrow import arrow
|
||||||
|
|
||||||
|
from app.config import JOB_DELETE_MAILBOX
|
||||||
|
from app.db import Session
|
||||||
|
from app.mail_sender import mail_sender
|
||||||
|
from app.models import Alias, Mailbox, Job, AliasMailbox
|
||||||
|
from job_runner import delete_mailbox_job
|
||||||
|
from tests.utils import create_new_user, random_email
|
||||||
|
|
||||||
|
|
||||||
|
@mail_sender.store_emails_test_decorator
|
||||||
|
def test_delete_mailbox_transfer_mailbox_primary(flask_client):
|
||||||
|
user = create_new_user()
|
||||||
|
m1 = Mailbox.create(
|
||||||
|
user_id=user.id, email=random_email(), verified=True, flush=True
|
||||||
|
)
|
||||||
|
m2 = Mailbox.create(
|
||||||
|
user_id=user.id, email=random_email(), verified=True, flush=True
|
||||||
|
)
|
||||||
|
|
||||||
|
alias_id = Alias.create_new(user, "prefix", mailbox_id=m1.id).id
|
||||||
|
AliasMailbox.create(alias_id=alias_id, mailbox_id=m2.id)
|
||||||
|
job = Job.create(
|
||||||
|
name=JOB_DELETE_MAILBOX,
|
||||||
|
payload={"mailbox_id": m1.id, "transfer_mailbox_id": m2.id},
|
||||||
|
run_at=arrow.now(),
|
||||||
|
commit=True,
|
||||||
|
)
|
||||||
|
Session.commit()
|
||||||
|
delete_mailbox_job(job)
|
||||||
|
alias = Alias.get(alias_id)
|
||||||
|
assert alias.mailbox_id == m2.id
|
||||||
|
assert len(alias._mailboxes) == 0
|
||||||
|
mails_sent = mail_sender.get_stored_emails()
|
||||||
|
assert len(mails_sent) == 1
|
||||||
|
assert str(mails_sent[0].msg).find("alias have been transferred") > -1
|
||||||
|
|
||||||
|
|
||||||
|
@mail_sender.store_emails_test_decorator
|
||||||
|
def test_delete_mailbox_transfer_mailbox_in_list(flask_client):
|
||||||
|
user = create_new_user()
|
||||||
|
m1 = Mailbox.create(
|
||||||
|
user_id=user.id, email=random_email(), verified=True, flush=True
|
||||||
|
)
|
||||||
|
m2 = Mailbox.create(
|
||||||
|
user_id=user.id, email=random_email(), verified=True, flush=True
|
||||||
|
)
|
||||||
|
m3 = Mailbox.create(
|
||||||
|
user_id=user.id, email=random_email(), verified=True, flush=True
|
||||||
|
)
|
||||||
|
|
||||||
|
alias_id = Alias.create_new(user, "prefix", mailbox_id=m1.id).id
|
||||||
|
AliasMailbox.create(alias_id=alias_id, mailbox_id=m2.id)
|
||||||
|
job = Job.create(
|
||||||
|
name=JOB_DELETE_MAILBOX,
|
||||||
|
payload={"mailbox_id": m2.id, "transfer_mailbox_id": m3.id},
|
||||||
|
run_at=arrow.now(),
|
||||||
|
commit=True,
|
||||||
|
)
|
||||||
|
Session.commit()
|
||||||
|
delete_mailbox_job(job)
|
||||||
|
alias = Alias.get(alias_id)
|
||||||
|
assert alias.mailbox_id == m1.id
|
||||||
|
assert len(alias._mailboxes) == 1
|
||||||
|
assert alias._mailboxes[0].id == m3.id
|
||||||
|
mails_sent = mail_sender.get_stored_emails()
|
||||||
|
assert len(mails_sent) == 1
|
||||||
|
assert str(mails_sent[0].msg).find("alias have been transferred") > -1
|
||||||
|
|
||||||
|
|
||||||
|
@mail_sender.store_emails_test_decorator
|
||||||
|
def test_delete_mailbox_no_transfer(flask_client):
|
||||||
|
user = create_new_user()
|
||||||
|
m1 = Mailbox.create(
|
||||||
|
user_id=user.id, email=random_email(), verified=True, flush=True
|
||||||
|
)
|
||||||
|
|
||||||
|
alias_id = Alias.create_new(user, "prefix", mailbox_id=m1.id).id
|
||||||
|
job = Job.create(
|
||||||
|
name=JOB_DELETE_MAILBOX,
|
||||||
|
payload={"mailbox_id": m1.id},
|
||||||
|
run_at=arrow.now(),
|
||||||
|
commit=True,
|
||||||
|
)
|
||||||
|
Session.commit()
|
||||||
|
delete_mailbox_job(job)
|
||||||
|
assert Alias.get(alias_id) is None
|
||||||
|
mails_sent = mail_sender.get_stored_emails()
|
||||||
|
assert len(mails_sent) == 1
|
||||||
|
assert str(mails_sent[0].msg).find("along with its aliases have been deleted") > -1
|
@ -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
|
||||||
|
@ -2,7 +2,8 @@ import email
|
|||||||
from app.email_utils import (
|
from app.email_utils import (
|
||||||
copy,
|
copy,
|
||||||
)
|
)
|
||||||
from app.message_utils import message_to_bytes
|
from app.message_utils import message_to_bytes, message_format_base64_parts
|
||||||
|
from tests.utils import load_eml_file
|
||||||
|
|
||||||
|
|
||||||
def test_copy():
|
def test_copy():
|
||||||
@ -33,3 +34,13 @@ def test_to_bytes():
|
|||||||
|
|
||||||
msg = email.message_from_string("éèà€")
|
msg = email.message_from_string("éèà€")
|
||||||
assert message_to_bytes(msg).decode() == "\néèà€"
|
assert message_to_bytes(msg).decode() == "\néèà€"
|
||||||
|
|
||||||
|
|
||||||
|
def test_base64_line_breaks():
|
||||||
|
msg = load_eml_file("bad_base64format.eml")
|
||||||
|
msg = message_format_base64_parts(msg)
|
||||||
|
for part in msg.walk():
|
||||||
|
if part.get("content-transfer-encoding") == "base64":
|
||||||
|
body = part.get_payload()
|
||||||
|
for line in body.splitlines():
|
||||||
|
assert len(line) <= 76
|
||||||
|
Reference in New Issue
Block a user