diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 77d8ae80..c622af42 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -62,6 +62,8 @@ To install it in your development environment.
## Run tests
+For most tests, you will need to have ``redis`` installed and started on your machine (listening on port 6379).
+
```bash
sh scripts/run-test.sh
```
@@ -80,6 +82,12 @@ To run the code locally, please create a local setting file based on `example.en
cp example.env .env
```
+You need to edit your .env to reflect the postgres exposed port, edit the `DB_URI` to:
+
+```
+DB_URI=postgresql://myuser:mypassword@localhost:35432/simplelogin
+```
+
Run the postgres database:
```bash
@@ -198,4 +206,11 @@ python email_handler.py
swaks --to e1@sl.local --from hey@google.com --server 127.0.0.1:20381
```
-Now open http://localhost:1080/ (or http://localhost:1080/ for MailHog), you should see the forwarded email.
\ No newline at end of file
+Now open http://localhost:1080/ (or http://localhost:1080/ for MailHog), you should see the forwarded email.
+
+## Job runner
+
+Some features require a job handler (such as GDPR data export). To test such feature you need to run the job_runner
+```bash
+python job_runner.py
+```
\ No newline at end of file
diff --git a/app/alias_utils.py b/app/alias_utils.py
index c431e377..7b1305bb 100644
--- a/app/alias_utils.py
+++ b/app/alias_utils.py
@@ -1,8 +1,11 @@
+import csv
+from io import StringIO
import re
from typing import Optional, Tuple
from email_validator import validate_email, EmailNotValidError
from sqlalchemy.exc import IntegrityError, DataError
+from flask import make_response
from app.config import (
BOUNCE_PREFIX_FOR_REPLY_PHASE,
@@ -364,3 +367,33 @@ def check_alias_prefix(alias_prefix) -> bool:
return False
return True
+
+
+def alias_export_csv(user, csv_direct_export=False):
+ """
+ Get user aliases as importable CSV file
+ Output:
+ Importable CSV file
+
+ """
+ data = [["alias", "note", "enabled", "mailboxes"]]
+ for alias in Alias.filter_by(user_id=user.id).all(): # type: Alias
+ # Always put the main mailbox first
+ # It is seen a primary while importing
+ alias_mailboxes = alias.mailboxes
+ alias_mailboxes.insert(
+ 0, alias_mailboxes.pop(alias_mailboxes.index(alias.mailbox))
+ )
+
+ mailboxes = " ".join([mailbox.email for mailbox in alias_mailboxes])
+ data.append([alias.email, alias.note, alias.enabled, mailboxes])
+
+ si = StringIO()
+ cw = csv.writer(si)
+ cw.writerows(data)
+ if csv_direct_export:
+ return si.getvalue()
+ output = make_response(si.getvalue())
+ output.headers["Content-Disposition"] = "attachment; filename=aliases.csv"
+ output.headers["Content-type"] = "text/csv"
+ return output
diff --git a/app/api/views/export.py b/app/api/views/export.py
index e8bace4f..8f5b3347 100644
--- a/app/api/views/export.py
+++ b/app/api/views/export.py
@@ -1,12 +1,9 @@
-import csv
-from io import StringIO
-
from flask import g
from flask import jsonify
-from flask import make_response
from app.api.base import api_bp, require_api_auth
from app.models import Alias, Client, CustomDomain
+from app.alias_utils import alias_export_csv
@api_bp.route("/export/data", methods=["GET"])
@@ -49,24 +46,4 @@ def export_aliases():
Importable CSV file
"""
- user = g.user
-
- data = [["alias", "note", "enabled", "mailboxes"]]
- for alias in Alias.filter_by(user_id=user.id).all(): # type: Alias
- # Always put the main mailbox first
- # It is seen a primary while importing
- alias_mailboxes = alias.mailboxes
- alias_mailboxes.insert(
- 0, alias_mailboxes.pop(alias_mailboxes.index(alias.mailbox))
- )
-
- mailboxes = " ".join([mailbox.email for mailbox in alias_mailboxes])
- data.append([alias.email, alias.note, alias.enabled, mailboxes])
-
- si = StringIO()
- cw = csv.writer(si)
- cw.writerows(data)
- output = make_response(si.getvalue())
- output.headers["Content-Disposition"] = "attachment; filename=aliases.csv"
- output.headers["Content-type"] = "text/csv"
- return output
+ return alias_export_csv(g.user)
diff --git a/app/dashboard/__init__.py b/app/dashboard/__init__.py
index cc19eaa2..ebfc38d6 100644
--- a/app/dashboard/__init__.py
+++ b/app/dashboard/__init__.py
@@ -6,6 +6,7 @@ from .views import (
subdomain,
billing,
alias_log,
+ alias_export,
unsubscribe,
api_key,
custom_domain,
diff --git a/app/dashboard/views/alias_export.py b/app/dashboard/views/alias_export.py
new file mode 100644
index 00000000..9d48b382
--- /dev/null
+++ b/app/dashboard/views/alias_export.py
@@ -0,0 +1,9 @@
+from app.dashboard.base import dashboard_bp
+from flask_login import login_required, current_user
+from app.alias_utils import alias_export_csv
+
+
+@dashboard_bp.route("/alias_export", methods=["GET"])
+@login_required
+def alias_export_route():
+ return alias_export_csv(current_user)
diff --git a/scripts/reset_local_db.sh b/scripts/reset_local_db.sh
index 422c2a8b..cf8e4f0f 100755
--- a/scripts/reset_local_db.sh
+++ b/scripts/reset_local_db.sh
@@ -1,6 +1,6 @@
#!/bin/sh
-export DB_URI=postgresql://myuser:mypassword@localhost:15432/simplelogin
+export DB_URI=postgresql://myuser:mypassword@localhost:35432/simplelogin
echo 'drop schema public cascade; create schema public;' | psql $DB_URI
poetry run alembic upgrade head
diff --git a/scripts/reset_test_db.sh b/scripts/reset_test_db.sh
index 25466010..a24edfe2 100755
--- a/scripts/reset_test_db.sh
+++ b/scripts/reset_test_db.sh
@@ -1,6 +1,6 @@
#!/bin/sh
-export DB_URI=postgresql://myuser:mypassword@localhost:15432/test
+export DB_URI=postgresql://myuser:mypassword@localhost:35432/test
echo 'drop schema public cascade; create schema public;' | psql $DB_URI
poetry run alembic upgrade head
diff --git a/templates/dashboard/setting.html b/templates/dashboard/setting.html
index df257911..43550576 100644
--- a/templates/dashboard/setting.html
+++ b/templates/dashboard/setting.html
@@ -702,15 +702,20 @@
- Alias import
+ Alias import/export
You can import your aliases created on other platforms into SimpleLogin.
+ You can also export your aliases to a readable csv format for a future batch import.
Batch Import
+
+ Export Aliases
+
diff --git a/tests/api/test_import_export.py b/tests/api/test_import_export.py
index 67b0da1c..be9d1f0a 100644
--- a/tests/api/test_import_export.py
+++ b/tests/api/test_import_export.py
@@ -1,105 +1,18 @@
-import csv
-from io import StringIO
-
-from flask import url_for
-
-from app import alias_utils
from app.db import Session
from app.import_utils import import_from_csv
from app.models import (
CustomDomain,
Mailbox,
Alias,
- AliasMailbox,
BatchImport,
File,
)
-from tests.utils import login, create_new_user, random_domain, random_token
+from tests.utils_test_alias import alias_export
+from tests.utils import login, random_domain, random_token
def test_export(flask_client):
- # Create users
- user1 = login(flask_client)
- user2 = create_new_user()
- Session.commit()
-
- # Remove onboarding aliases
- for alias in Alias.filter_by(user_id=user1.id).all():
- alias_utils.delete_alias(alias, user1)
- for alias in Alias.filter_by(user_id=user2.id).all():
- alias_utils.delete_alias(alias, user2)
- Session.commit()
-
- # Create domains
- ok_domain = CustomDomain.create(
- user_id=user1.id, domain=random_domain(), verified=True
- )
- bad_domain = CustomDomain.create(
- user_id=user2.id, domain=random_domain(), verified=True
- )
- Session.commit()
-
- # Create mailboxes
- mailbox1 = Mailbox.create(
- user_id=user1.id, email=f"{random_token()}@{ok_domain.domain}", verified=True
- )
- mailbox2 = Mailbox.create(
- user_id=user1.id, email=f"{random_token()}@{ok_domain.domain}", verified=True
- )
- badmailbox1 = Mailbox.create(
- user_id=user2.id,
- email=f"{random_token()}@{bad_domain.domain}",
- verified=True,
- )
- Session.commit()
-
- # Create aliases
- alias1 = Alias.create(
- user_id=user1.id,
- email=f"{random_token()}@my-domain.com",
- note="Used on eBay",
- mailbox_id=mailbox1.id,
- )
- alias2 = Alias.create(
- user_id=user1.id,
- email=f"{random_token()}@my-domain.com",
- note="Used on Facebook, Instagram.",
- mailbox_id=mailbox1.id,
- )
- Alias.create(
- user_id=user2.id,
- email=f"{random_token()}@my-domain.com",
- note="Should not appear",
- mailbox_id=badmailbox1.id,
- )
- Session.commit()
-
- # Add second mailbox to an alias
- AliasMailbox.create(
- alias_id=alias2.id,
- mailbox_id=mailbox2.id,
- )
- Session.commit()
-
- # Export
- r = flask_client.get(url_for("api.export_aliases"))
- assert r.status_code == 200
- assert r.mimetype == "text/csv"
- csv_data = csv.DictReader(StringIO(r.data.decode("utf-8")))
- found_aliases = set()
- for row in csv_data:
- found_aliases.add(row["alias"])
- if row["alias"] == alias1.email:
- assert alias1.note == row["note"]
- assert "True" == row["enabled"]
- assert mailbox1.email == row["mailboxes"]
- elif row["alias"] == alias2.email:
- assert alias2.note == row["note"]
- assert "True" == row["enabled"]
- assert f"{mailbox1.email} {mailbox2.email}" == row["mailboxes"]
- else:
- raise AssertionError("Unknown alias")
- assert set((alias1.email, alias2.email)) == found_aliases
+ alias_export(flask_client, "api.export_aliases")
def test_import_no_mailboxes_no_domains(flask_client):
diff --git a/tests/dashboard/test_alias_csv_export.py b/tests/dashboard/test_alias_csv_export.py
new file mode 100644
index 00000000..13e147d1
--- /dev/null
+++ b/tests/dashboard/test_alias_csv_export.py
@@ -0,0 +1,5 @@
+from tests.utils_test_alias import alias_export
+
+
+def test_alias_export(flask_client):
+ alias_export(flask_client, "dashboard.alias_export_route")
diff --git a/tests/utils_test_alias.py b/tests/utils_test_alias.py
new file mode 100644
index 00000000..1762f72b
--- /dev/null
+++ b/tests/utils_test_alias.py
@@ -0,0 +1,95 @@
+import csv
+from io import StringIO
+
+from flask import url_for
+
+from app.alias_utils import delete_alias
+from app.db import Session
+from app.models import Alias, CustomDomain, Mailbox, AliasMailbox
+
+from tests.utils import login, create_new_user, random_domain, random_token
+
+
+def alias_export(flask_client, target_url):
+ # Create users
+ user1 = login(flask_client)
+ user2 = create_new_user()
+ Session.commit()
+
+ # Remove onboarding aliases
+ for alias in Alias.filter_by(user_id=user1.id).all():
+ delete_alias(alias, user1)
+ for alias in Alias.filter_by(user_id=user2.id).all():
+ delete_alias(alias, user2)
+ Session.commit()
+
+ # Create domains
+ ok_domain = CustomDomain.create(
+ user_id=user1.id, domain=random_domain(), verified=True
+ )
+ bad_domain = CustomDomain.create(
+ user_id=user2.id, domain=random_domain(), verified=True
+ )
+ Session.commit()
+
+ # Create mailboxes
+ mailbox1 = Mailbox.create(
+ user_id=user1.id, email=f"{random_token()}@{ok_domain.domain}", verified=True
+ )
+ mailbox2 = Mailbox.create(
+ user_id=user1.id, email=f"{random_token()}@{ok_domain.domain}", verified=True
+ )
+ badmailbox1 = Mailbox.create(
+ user_id=user2.id,
+ email=f"{random_token()}@{bad_domain.domain}",
+ verified=True,
+ )
+ Session.commit()
+
+ # Create aliases
+ alias1 = Alias.create(
+ user_id=user1.id,
+ email=f"{random_token()}@my-domain.com",
+ note="Used on eBay",
+ mailbox_id=mailbox1.id,
+ )
+ alias2 = Alias.create(
+ user_id=user1.id,
+ email=f"{random_token()}@my-domain.com",
+ note="Used on Facebook, Instagram.",
+ mailbox_id=mailbox1.id,
+ )
+ Alias.create(
+ user_id=user2.id,
+ email=f"{random_token()}@my-domain.com",
+ note="Should not appear",
+ mailbox_id=badmailbox1.id,
+ )
+ Session.commit()
+
+ # Add second mailbox to an alias
+ AliasMailbox.create(
+ alias_id=alias2.id,
+ mailbox_id=mailbox2.id,
+ )
+ Session.commit()
+
+ # Export
+ r = flask_client.get(url_for(target_url))
+ assert r.status_code == 200
+ assert r.mimetype == "text/csv"
+ csv_data = csv.DictReader(StringIO(r.data.decode("utf-8")))
+ found_aliases = set()
+ for row in csv_data:
+ found_aliases.add(row["alias"])
+ if row["alias"] == alias1.email:
+ assert alias1.note == row["note"]
+ assert "True" == row["enabled"]
+ assert mailbox1.email == row["mailboxes"]
+ elif row["alias"] == alias2.email:
+ assert alias2.note == row["note"]
+ assert "True" == row["enabled"]
+ assert f"{mailbox1.email} {mailbox2.email}" == row["mailboxes"]
+ else:
+ raise AssertionError("Unknown alias")
+ assert set((alias1.email, alias2.email)) == found_aliases