Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
e36e9d3077 | |||
b2430cbc5b | |||
1258115397 |
@ -169,6 +169,12 @@ For HTML templates, we use `djlint`. Before creating a pull request, please run
|
||||
poetry run djlint --check templates
|
||||
```
|
||||
|
||||
If some files aren't properly formatted, you can format all files with
|
||||
|
||||
```bash
|
||||
poetry run djlint --reformat .
|
||||
```
|
||||
|
||||
## Test sending email
|
||||
|
||||
[swaks](http://www.jetmore.org/john/code/swaks/) is used for sending test emails to the `email_handler`.
|
||||
|
@ -18,6 +18,8 @@ class NewApiKeyForm(FlaskForm):
|
||||
|
||||
def clean_up_unused_or_old_api_keys(user_id: int):
|
||||
total_keys = ApiKey.filter_by(user_id=user_id).count()
|
||||
if total_keys <= config.MAX_API_KEYS:
|
||||
return
|
||||
# Remove oldest unused
|
||||
for api_key in (
|
||||
ApiKey.filter_by(user_id=user_id, last_used=None)
|
||||
|
@ -8,6 +8,7 @@ from wtforms import PasswordField, validators
|
||||
|
||||
from app.config import CONNECT_WITH_PROTON
|
||||
from app.dashboard.base import dashboard_bp
|
||||
from app.extensions import limiter
|
||||
from app.log import LOG
|
||||
from app.models import PartnerUser
|
||||
from app.proton.utils import get_proton_partner
|
||||
@ -21,6 +22,7 @@ class LoginForm(FlaskForm):
|
||||
|
||||
|
||||
@dashboard_bp.route("/enter_sudo", methods=["GET", "POST"])
|
||||
@limiter.limit("3/minute")
|
||||
@login_required
|
||||
def enter_sudo():
|
||||
password_check_form = LoginForm()
|
||||
|
@ -951,6 +951,8 @@ def add_header(msg: Message, text_header, html_header=None) -> Message:
|
||||
for part in msg.get_payload():
|
||||
if isinstance(part, Message):
|
||||
new_parts.append(add_header(part, text_header, html_header))
|
||||
elif isinstance(part, str):
|
||||
new_parts.append(MIMEText(part))
|
||||
else:
|
||||
new_parts.append(part)
|
||||
clone_msg = copy(msg)
|
||||
@ -959,7 +961,14 @@ def add_header(msg: Message, text_header, html_header=None) -> Message:
|
||||
|
||||
elif content_type in ("multipart/mixed", "multipart/signed"):
|
||||
new_parts = []
|
||||
parts = list(msg.get_payload())
|
||||
payload = msg.get_payload()
|
||||
if isinstance(payload, str):
|
||||
# The message is badly formatted inject as new
|
||||
new_parts = [MIMEText(text_header, "plain"), MIMEText(payload, "plain")]
|
||||
clone_msg = copy(msg)
|
||||
clone_msg.set_payload(new_parts)
|
||||
return clone_msg
|
||||
parts = list(payload)
|
||||
LOG.d("only add header for the first part for %s", content_type)
|
||||
for ix, part in enumerate(parts):
|
||||
if ix == 0:
|
||||
|
@ -74,8 +74,8 @@ class UnsubscribeEncoder:
|
||||
)
|
||||
signed_data = cls._get_signer().sign(serialized_data).decode("utf-8")
|
||||
encoded_request = f"{UNSUB_PREFIX}.{signed_data}"
|
||||
if len(encoded_request) > 256:
|
||||
LOG.e("Encoded request is longer than 256 chars")
|
||||
if len(encoded_request) > 512:
|
||||
LOG.w("Encoded request is longer than 512 chars")
|
||||
return encoded_request
|
||||
|
||||
@staticmethod
|
||||
|
@ -46,6 +46,7 @@ class SendRequest:
|
||||
"mail_options": self.mail_options,
|
||||
"rcpt_options": self.rcpt_options,
|
||||
"is_forward": self.is_forward,
|
||||
"retries": self.retries,
|
||||
}
|
||||
return json.dumps(data).encode("utf-8")
|
||||
|
||||
@ -66,6 +67,7 @@ class SendRequest:
|
||||
mail_options=decoded_data["mail_options"],
|
||||
rcpt_options=decoded_data["rcpt_options"],
|
||||
is_forward=decoded_data["is_forward"],
|
||||
retries=decoded_data.get("retries", 1),
|
||||
)
|
||||
|
||||
def save_request_to_unsent_dir(self, prefix: str = "DeliveryFail"):
|
||||
|
@ -341,7 +341,7 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle):
|
||||
sa.Boolean, default=True, nullable=False, server_default="1"
|
||||
)
|
||||
|
||||
activated = sa.Column(sa.Boolean, default=False, nullable=False)
|
||||
activated = sa.Column(sa.Boolean, default=False, nullable=False, index=True)
|
||||
|
||||
# an account can be disabled if having harmful behavior
|
||||
disabled = sa.Column(sa.Boolean, default=False, nullable=False, server_default="0")
|
||||
@ -411,7 +411,10 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle):
|
||||
)
|
||||
|
||||
referral_id = sa.Column(
|
||||
sa.ForeignKey("referral.id", ondelete="SET NULL"), nullable=True, default=None
|
||||
sa.ForeignKey("referral.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
default=None,
|
||||
index=True,
|
||||
)
|
||||
|
||||
referral = orm.relationship("Referral", foreign_keys=[referral_id])
|
||||
@ -534,6 +537,12 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle):
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
sa.Index(
|
||||
"ix_users_activated_trial_end_lifetime", activated, trial_end, lifetime
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def directory_quota(self):
|
||||
return min(
|
||||
@ -1445,7 +1454,7 @@ class Alias(Base, ModelMixin):
|
||||
)
|
||||
|
||||
# have I been pwned
|
||||
hibp_last_check = sa.Column(ArrowType, default=None)
|
||||
hibp_last_check = sa.Column(ArrowType, default=None, index=True)
|
||||
hibp_breaches = orm.relationship("Hibp", secondary="alias_hibp")
|
||||
|
||||
# to use Postgres full text search. Only applied on "note" column for now
|
||||
@ -2928,6 +2937,8 @@ class Monitoring(Base, ModelMixin):
|
||||
active_queue = sa.Column(sa.Integer, nullable=False)
|
||||
deferred_queue = sa.Column(sa.Integer, nullable=False)
|
||||
|
||||
__table_args__ = (Index("ix_monitoring_created_at", "created_at"),)
|
||||
|
||||
|
||||
class BatchImport(Base, ModelMixin):
|
||||
__tablename__ = "batch_import"
|
||||
@ -3053,6 +3064,8 @@ class Bounce(Base, ModelMixin):
|
||||
email = sa.Column(sa.String(256), nullable=False, index=True)
|
||||
info = sa.Column(sa.Text, nullable=True)
|
||||
|
||||
__table_args__ = (sa.Index("ix_bounce_created_at", "created_at"),)
|
||||
|
||||
|
||||
class TransactionalEmail(Base, ModelMixin):
|
||||
"""Storing all email addresses that receive transactional emails, including account email and mailboxes.
|
||||
@ -3062,6 +3075,8 @@ class TransactionalEmail(Base, ModelMixin):
|
||||
__tablename__ = "transactional_email"
|
||||
email = sa.Column(sa.String(256), nullable=False, unique=False)
|
||||
|
||||
__table_args__ = (sa.Index("ix_transactional_email_created_at", "created_at"),)
|
||||
|
||||
|
||||
class Payout(Base, ModelMixin):
|
||||
"""Referral payouts"""
|
||||
|
23
app/cron.py
23
app/cron.py
@ -66,12 +66,14 @@ from server import create_light_app
|
||||
|
||||
def notify_trial_end():
|
||||
for user in User.filter(
|
||||
User.activated.is_(True), User.trial_end.isnot(None), User.lifetime.is_(False)
|
||||
User.activated.is_(True),
|
||||
User.trial_end.isnot(None),
|
||||
User.trial_end >= arrow.now().shift(days=2),
|
||||
User.trial_end < arrow.now().shift(days=3),
|
||||
User.lifetime.is_(False),
|
||||
).all():
|
||||
try:
|
||||
if user.in_trial() and arrow.now().shift(
|
||||
days=3
|
||||
) > user.trial_end >= arrow.now().shift(days=2):
|
||||
if user.in_trial():
|
||||
LOG.d("Send trial end email to user %s", user)
|
||||
send_trial_end_soon_email(user)
|
||||
# happens if user has been deleted in the meantime
|
||||
@ -104,7 +106,9 @@ def delete_logs():
|
||||
|
||||
|
||||
def delete_refused_emails():
|
||||
for refused_email in RefusedEmail.filter_by(deleted=False).all():
|
||||
for refused_email in (
|
||||
RefusedEmail.filter_by(deleted=False).order_by(RefusedEmail.id).all()
|
||||
):
|
||||
if arrow.now().shift(days=1) > refused_email.delete_at >= arrow.now():
|
||||
LOG.d("Delete refused email %s", refused_email)
|
||||
if refused_email.path:
|
||||
@ -272,7 +276,11 @@ def compute_metric2() -> Metric2:
|
||||
_24h_ago = now.shift(days=-1)
|
||||
|
||||
nb_referred_user_paid = 0
|
||||
for user in User.filter(User.referral_id.isnot(None)):
|
||||
for user in (
|
||||
User.filter(User.referral_id.isnot(None))
|
||||
.yield_per(500)
|
||||
.enable_eagerloads(False)
|
||||
):
|
||||
if user.is_paid():
|
||||
nb_referred_user_paid += 1
|
||||
|
||||
@ -1020,7 +1028,8 @@ async def check_hibp():
|
||||
)
|
||||
.filter(Alias.enabled)
|
||||
.order_by(Alias.hibp_last_check.asc())
|
||||
.all()
|
||||
.yield_per(500)
|
||||
.enable_eagerloads(False)
|
||||
):
|
||||
await queue.put(alias.id)
|
||||
|
||||
|
@ -35,12 +35,6 @@ jobs:
|
||||
schedule: "0 12 * * *"
|
||||
captureStderr: true
|
||||
|
||||
- name: SimpleLogin Sanity Check
|
||||
command: python /code/cron.py -j sanity_check
|
||||
shell: /bin/bash
|
||||
schedule: "0 2 * * *"
|
||||
captureStderr: true
|
||||
|
||||
- name: SimpleLogin Delete Old Monitoring records
|
||||
command: python /code/cron.py -j delete_old_monitoring
|
||||
shell: /bin/bash
|
||||
|
123
app/docs/ssl.md
123
app/docs/ssl.md
@ -1,4 +1,4 @@
|
||||
# SSL, HTTPS, and HSTS
|
||||
# SSL, HTTPS, HSTS and additional security measures
|
||||
|
||||
It's highly recommended to enable SSL/TLS on your server, both for the web app and email server.
|
||||
|
||||
@ -58,3 +58,124 @@ Now, reload Nginx:
|
||||
```bash
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
## Additional security measures
|
||||
|
||||
For additional security, we recommend you take some extra steps.
|
||||
|
||||
### Enable Certificate Authority Authorization (CAA)
|
||||
|
||||
[Certificate Authority Authorization](https://letsencrypt.org/docs/caa/) is a step you can take to restrict the list of certificate authorities that are allowed to issue certificates for your domains.
|
||||
|
||||
Use [SSLMate’s CAA Record Generator](https://sslmate.com/caa/) to create a **CAA record** with the following configuration:
|
||||
|
||||
- `flags`: `0`
|
||||
- `tag`: `issue`
|
||||
- `value`: `"letsencrypt.org"`
|
||||
|
||||
To verify if the DNS works, the following command
|
||||
|
||||
```bash
|
||||
dig @1.1.1.1 mydomain.com caa
|
||||
```
|
||||
|
||||
should return:
|
||||
|
||||
```
|
||||
mydomain.com. 3600 IN CAA 0 issue "letsencrypt.org"
|
||||
```
|
||||
|
||||
### SMTP MTA Strict Transport Security (MTA-STS)
|
||||
|
||||
[MTA-STS](https://datatracker.ietf.org/doc/html/rfc8461) is an extra step you can take to broadcast the ability of your instance to receive and, optionally enforce, TSL-secure SMTP connections to protect email traffic.
|
||||
|
||||
Enabling MTA-STS requires you serve a specific file from subdomain `mta-sts.domain.com` on a well-known route.
|
||||
|
||||
Create a text file `/var/www/.well-known/mta-sts.txt` with the content:
|
||||
|
||||
```txt
|
||||
version: STSv1
|
||||
mode: testing
|
||||
mx: app.mydomain.com
|
||||
max_age: 86400
|
||||
```
|
||||
|
||||
It is recommended to start with `mode: testing` for starters to get time to review failure reports. Add as many `mx:` domain entries as you have matching **MX records** in your DNS configuration.
|
||||
|
||||
Create a **TXT record** for `_mta-sts.mydomain.com.` with the following value:
|
||||
|
||||
```txt
|
||||
v=STSv1; id=UNIX_TIMESTAMP
|
||||
```
|
||||
|
||||
With `UNIX_TIMESTAMP` being the current date/time.
|
||||
|
||||
Use the following command to generate the record:
|
||||
|
||||
```bash
|
||||
echo "v=STSv1; id=$(date +%s)"
|
||||
```
|
||||
|
||||
To verify if the DNS works, the following command
|
||||
|
||||
```bash
|
||||
dig @1.1.1.1 _mta-sts.mydomain.com txt
|
||||
```
|
||||
|
||||
should return a result similar to this one:
|
||||
|
||||
```
|
||||
_mta-sts.mydomain.com. 3600 IN TXT "v=STSv1; id=1689416399"
|
||||
```
|
||||
|
||||
Create an additional Nginx configuration in `/etc/nginx/sites-enabled/mta-sts` with the following content:
|
||||
|
||||
```
|
||||
server {
|
||||
server_name mta-sts.mydomain.com;
|
||||
root /var/www;
|
||||
listen 80;
|
||||
|
||||
location ^~ /.well-known {}
|
||||
}
|
||||
```
|
||||
|
||||
Restart Nginx with the following command:
|
||||
|
||||
```sh
|
||||
sudo service nginx restart
|
||||
```
|
||||
|
||||
A correct configuration of MTA-STS, however, requires that the certificate used to host the `mta-sts` subdomain matches that of the subdomain referred to by the **MX record** from the DNS. In other words, both `mta-sts.mydomain.com` and `app.mydomain.com` must share the same certificate.
|
||||
|
||||
The easiest way to do this is to _expand_ the certificate associated with `app.mydomain.com` to also support the `mta-sts` subdomain using the following command:
|
||||
|
||||
```sh
|
||||
certbot --expand --nginx -d app.mydomain.com,mta-sts.mydomain.com
|
||||
```
|
||||
|
||||
## SMTP TLS Reporting
|
||||
|
||||
[TLSRPT](https://datatracker.ietf.org/doc/html/rfc8460) is used by SMTP systems to report failures in establishing TLS-secure sessions as broadcast by the MTA-STS configuration.
|
||||
|
||||
Configuring MTA-STS in `mode: testing` as shown in the previous section gives you time to review failures from some SMTP senders.
|
||||
|
||||
Create a **TXT record** for `_smtp._tls.mydomain.com.` with the following value:
|
||||
|
||||
```txt
|
||||
v=TSLRPTv1; rua=mailto:YOUR_EMAIL
|
||||
```
|
||||
|
||||
The TLSRPT configuration at the DNS level allows SMTP senders that fail to initiate TLS-secure sessions to send reports to a particular email address. We suggest creating a `tls-reports` alias in SimpleLogin for this purpose.
|
||||
|
||||
To verify if the DNS works, the following command
|
||||
|
||||
```bash
|
||||
dig @1.1.1.1 _smtp._tls.mydomain.com txt
|
||||
```
|
||||
|
||||
should return a result similar to this one:
|
||||
|
||||
```
|
||||
_smtp._tls.mydomain.com. 3600 IN TXT "v=TSLRPTv1; rua=mailto:tls-reports@mydomain.com"
|
||||
```
|
||||
|
42
app/migrations/versions/2023_072819_01827104004b_.py
Normal file
42
app/migrations/versions/2023_072819_01827104004b_.py
Normal file
@ -0,0 +1,42 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 01827104004b
|
||||
Revises: 2634b41f54db
|
||||
Create Date: 2023-07-28 19:39:28.675490
|
||||
|
||||
"""
|
||||
import sqlalchemy_utils
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '01827104004b'
|
||||
down_revision = '2634b41f54db'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
with op.get_context().autocommit_block():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_index(op.f('ix_alias_hibp_last_check'), 'alias', ['hibp_last_check'], unique=False, postgresql_concurrently=True)
|
||||
op.create_index('ix_bounce_created_at', 'bounce', ['created_at'], unique=False, postgresql_concurrently=True)
|
||||
op.create_index('ix_monitoring_created_at', 'monitoring', ['created_at'], unique=False, postgresql_concurrently=True)
|
||||
op.create_index('ix_transactional_email_created_at', 'transactional_email', ['created_at'], unique=False, postgresql_concurrently=True)
|
||||
op.create_index(op.f('ix_users_activated'), 'users', ['activated'], unique=False, postgresql_concurrently=True)
|
||||
op.create_index('ix_users_activated_trial_end_lifetime', 'users', ['activated', 'trial_end', 'lifetime'], unique=False, postgresql_concurrently=True)
|
||||
op.create_index(op.f('ix_users_referral_id'), 'users', ['referral_id'], unique=False, postgresql_concurrently=True)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_users_referral_id'), table_name='users')
|
||||
op.drop_index('ix_users_activated_trial_end_lifetime', table_name='users')
|
||||
op.drop_index(op.f('ix_users_activated'), table_name='users')
|
||||
op.drop_index('ix_transactional_email_created_at', table_name='transactional_email')
|
||||
op.drop_index('ix_monitoring_created_at', table_name='monitoring')
|
||||
op.drop_index('ix_bounce_created_at', table_name='bounce')
|
||||
op.drop_index(op.f('ix_alias_hibp_last_check'), table_name='alias')
|
||||
# ### end Alembic commands ###
|
@ -9,10 +9,13 @@
|
||||
<h1 class="card-title">Create new account</h1>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Email address</label>
|
||||
{{ form.email(class="form-control", type="email") }}
|
||||
{{ form.email(class="form-control", type="email", placeholder="YourName@protonmail.com") }}
|
||||
<div class="small-text alert alert-info" style="margin-top: 1px">
|
||||
Emails sent to your alias will be forwarded to this email address.
|
||||
<br>
|
||||
It can't be a disposable or forwarding email address.
|
||||
<br>
|
||||
We recommend using a <a href="https://proton.me/mail" target="_blank">Proton Mail</a> address
|
||||
</div>
|
||||
{{ render_field_errors(form.email) }}
|
||||
</div>
|
||||
|
@ -28,7 +28,7 @@
|
||||
<form id="supportZendeskForm" method="post" enctype="multipart/form-data">
|
||||
<div class="mt-4 mb-5">
|
||||
<label for="issueDescription" class="form-label font-weight-bold">What happened?</label>
|
||||
<textarea class="form-control" required name="ticket_content" id="issueDescription" rows="3" placeholder="Please provide as much information as possible. For example which alias(es), mailbox(es) ar affected, if this is a persistent issue...">{{- ticket_content or '' -}}</textarea>
|
||||
<textarea class="form-control" required name="ticket_content" id="issueDescription" rows="3" placeholder="Please provide as much information as possible. For example which alias(es), mailbox(es) are affected, if this is a persistent issue...">{{- ticket_content or '' -}}</textarea>
|
||||
</div>
|
||||
<div class="mt-5 font-weight-bold">Attach files to support request</div>
|
||||
<div class="text-muted">Only images, text and emails are accepted</div>
|
||||
|
@ -5,7 +5,7 @@
|
||||
<div class="page-single">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col mx-auto" style="max-width: 28rem">
|
||||
<div class="col mx-auto" style="max-width: 32rem">
|
||||
<div class="text-center mb-6">
|
||||
<a href="{{ LANDING_PAGE_URL }}">
|
||||
<img src="/static/logo.svg"
|
||||
|
@ -37,6 +37,17 @@ def test_create_delete_api_key(flask_client):
|
||||
assert ApiKey.filter(ApiKey.user_id == user.id).count() == 1
|
||||
assert api_key.name == "for test"
|
||||
|
||||
# create second api_key
|
||||
create_r = flask_client.post(
|
||||
url_for("dashboard.api_key"),
|
||||
data={"form-name": "create", "name": "for test 2"},
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert create_r.status_code == 200
|
||||
api_key_2 = ApiKey.filter_by(user_id=user.id).order_by(ApiKey.id.desc()).first()
|
||||
assert ApiKey.filter(ApiKey.user_id == user.id).count() == 2
|
||||
assert api_key_2.name == "for test 2"
|
||||
|
||||
# delete api_key
|
||||
delete_r = flask_client.post(
|
||||
url_for("dashboard.api_key"),
|
||||
@ -44,7 +55,7 @@ def test_create_delete_api_key(flask_client):
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert delete_r.status_code == 200
|
||||
assert ApiKey.count() == nb_api_key
|
||||
assert ApiKey.count() == nb_api_key + 1
|
||||
|
||||
|
||||
def test_delete_all_api_keys(flask_client):
|
||||
|
21
app/tests/example_emls/add_header_multipart.eml
Normal file
21
app/tests/example_emls/add_header_multipart.eml
Normal file
@ -0,0 +1,21 @@
|
||||
Sender: somebody@somewhere.net
|
||||
Content-Type: multipart/mixed; boundary="----=_Part_3946_1099248058.1688752298149"
|
||||
|
||||
--0c916c9b5fe3c925d7bafeb988bb6794
|
||||
Content-Type: text/plain; charset="UTF-8"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
notification test
|
||||
|
||||
--0c916c9b5fe3c925d7bafeb988bb6794
|
||||
Content-Type: text/html; charset="UTF-8"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<html><head><meta http-equiv=3D"Content-Type" content=3D"text/html; charset=
|
||||
=3DUTF-8"><meta http-equiv=3D"X-UA-Compatible" content=3D"IE=3Dedge"><meta =
|
||||
name=3D"format-detection" content=3D"telephone=3Dno"><meta name=3D"viewport=
|
||||
" content=3D"width=3Ddevice-width, initial-scale=3D1.0">
|
||||
|
||||
--0c916c9b5fe3c925d7bafeb988bb6794--
|
||||
|
||||
|
27
app/tests/example_emls/email_to_pgp_encrypt.eml
Normal file
27
app/tests/example_emls/email_to_pgp_encrypt.eml
Normal file
@ -0,0 +1,27 @@
|
||||
From: {{sender_address}}
|
||||
To: {{recipient_address}}
|
||||
Subject: Test subject
|
||||
Content-Type: multipart/alternative; boundary="MLF8fvg556fdhFDH7=_?:
|
||||
|
||||
--MLF8fvg556fdhFDH7=_?:
|
||||
Content-Type: text/plain;
|
||||
charset="utf-8"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
*************************************************************************
|
||||
|
||||
This five-part limited series, based on the brilliant graphic novel by Me
|
||||
|
||||
--MLF8fvg556fdhFDH7=_?:
|
||||
Content-Type: text/html;
|
||||
charset="utf-8"
|
||||
Content-Transfer-Encoding: 8bit
|
||||
--MLF8fvg556fdhFDH7=_?:
|
||||
Content-Type: text/plain;
|
||||
charset="utf-8"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
*************************************************************************
|
||||
*************************************************************************
|
||||
|
||||
|
33
app/tests/handler/test_encrypt_pgp.py
Normal file
33
app/tests/handler/test_encrypt_pgp.py
Normal file
@ -0,0 +1,33 @@
|
||||
from aiosmtpd.smtp import Envelope
|
||||
|
||||
import email_handler
|
||||
from app.config import get_abs_path
|
||||
from app.db import Session
|
||||
from app.pgp_utils import load_public_key
|
||||
from tests.utils import create_new_user, load_eml_file, random_email
|
||||
|
||||
from app.models import Alias
|
||||
|
||||
|
||||
def test_encrypt_with_pgp():
|
||||
user = create_new_user()
|
||||
pgp_public_key = open(get_abs_path("local_data/public-pgp.asc")).read()
|
||||
mailbox = user.default_mailbox
|
||||
mailbox.pgp_public_key = pgp_public_key
|
||||
mailbox.generic_subject = True
|
||||
mailbox.pgp_finger_print = load_public_key(pgp_public_key)
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.flush()
|
||||
sender_address = random_email()
|
||||
msg = load_eml_file(
|
||||
"email_to_pgp_encrypt.eml",
|
||||
{
|
||||
"sender_address": sender_address,
|
||||
"recipient_address": alias.email,
|
||||
},
|
||||
)
|
||||
envelope = Envelope()
|
||||
envelope.mail_from = sender_address
|
||||
envelope.rcpt_tos = [alias.email]
|
||||
result = email_handler.MailHandler()._handle(envelope, msg)
|
||||
assert result is not None
|
@ -810,7 +810,7 @@ def test_add_header_multipart_with_invalid_part():
|
||||
if i < 2:
|
||||
assert part.get_payload().index("INJECT") > -1
|
||||
else:
|
||||
assert part == "invalid"
|
||||
assert part.get_payload() == "invalid"
|
||||
|
||||
|
||||
def test_sl_formataddr():
|
||||
@ -822,3 +822,10 @@ def test_sl_formataddr():
|
||||
# test that the same name-address can't be handled by the built-in formataddr
|
||||
with pytest.raises(UnicodeEncodeError):
|
||||
formataddr(("é", "è@ç.à"))
|
||||
|
||||
|
||||
def test_add_header_to_invalid_multipart():
|
||||
msg = load_eml_file("add_header_multipart.eml")
|
||||
msg = add_header(msg, "test", "test")
|
||||
data = msg.as_string()
|
||||
assert data != ""
|
||||
|
Reference in New Issue
Block a user