mirror of
https://github.com/simple-login/app.git
synced 2024-09-28 20:51:29 +02:00
Merge pull request #205 from simple-login/mailbox-api
Add endpoints for mailbox
This commit is contained in:
commit
8777db0729
44
README.md
44
README.md
@ -1161,6 +1161,50 @@ List of mailboxes. Each mailbox has id, email field.
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /api/mailboxes
|
||||
|
||||
Create a new mailbox
|
||||
|
||||
Input:
|
||||
- `Authentication` header that contains the api key
|
||||
- email: the new mailbox address
|
||||
|
||||
Output:
|
||||
- 201 along with the following response if new mailbox is created successfully. User is going to receive a verification email.
|
||||
- id: integer
|
||||
- email: the mailbox email address
|
||||
- verified: boolean.
|
||||
- default: whether is the default mailbox. User cannot delete the default mailbox
|
||||
- 400 with error message otherwise. The error message can be displayed to user.
|
||||
|
||||
#### DELETE /api/mailboxes/:mailbox_id
|
||||
|
||||
Delete a mailbox. User cannot delete the default mailbox
|
||||
|
||||
Input:
|
||||
- `Authentication` header that contains the api key
|
||||
- `mailbox_id`: in url
|
||||
|
||||
Output:
|
||||
- 200 if deleted successfully
|
||||
- 400 if error
|
||||
|
||||
#### PUT /api/mailboxes/:mailbox_id
|
||||
|
||||
Update a mailbox.
|
||||
|
||||
Input:
|
||||
- `Authentication` header that contains the api key
|
||||
- `mailbox_id`: in url
|
||||
- (optional) `default`: boolean. Set a mailbox as default mailbox.
|
||||
- (optional) `email`: email address. Change a mailbox email address.
|
||||
- (optional) `cancel_email_change`: boolean. Cancel mailbox email change.
|
||||
|
||||
Output:
|
||||
- 200 if updated successfully
|
||||
- 400 if error
|
||||
|
||||
|
||||
### Contact endpoints
|
||||
|
||||
#### DELETE /api/contacts/:contact_id
|
||||
|
@ -7,4 +7,5 @@ from .views import (
|
||||
auth_mfa,
|
||||
alias,
|
||||
apple,
|
||||
mailbox,
|
||||
)
|
||||
|
@ -120,27 +120,6 @@ def get_aliases_v2():
|
||||
)
|
||||
|
||||
|
||||
@api_bp.route("/mailboxes", methods=["GET"])
|
||||
@cross_origin()
|
||||
@require_api_auth
|
||||
def get_mailboxes():
|
||||
"""
|
||||
Get mailboxes
|
||||
Output:
|
||||
- mailboxes: list of alias:
|
||||
- id
|
||||
- email
|
||||
"""
|
||||
user = g.user
|
||||
|
||||
return (
|
||||
jsonify(
|
||||
mailboxes=[{"id": mb.id, "email": mb.email} for mb in user.mailboxes()]
|
||||
),
|
||||
200,
|
||||
)
|
||||
|
||||
|
||||
@api_bp.route("/aliases/<int:alias_id>", methods=["DELETE"])
|
||||
@cross_origin()
|
||||
@require_api_auth
|
||||
|
172
app/api/views/mailbox.py
Normal file
172
app/api/views/mailbox.py
Normal file
@ -0,0 +1,172 @@
|
||||
from smtplib import SMTPRecipientsRefused
|
||||
|
||||
from flask import g, jsonify
|
||||
from flask import jsonify
|
||||
from flask import request
|
||||
from flask_cors import cross_origin
|
||||
|
||||
from app.api.base import api_bp, require_api_auth
|
||||
from app.dashboard.views.mailbox import send_verification_email
|
||||
from app.dashboard.views.mailbox_detail import verify_mailbox_change
|
||||
from app.email_utils import (
|
||||
mailbox_already_used,
|
||||
email_domain_can_be_used_as_mailbox,
|
||||
)
|
||||
from app.extensions import db
|
||||
from app.models import Mailbox
|
||||
|
||||
|
||||
@api_bp.route("/mailboxes", methods=["POST"])
|
||||
@cross_origin()
|
||||
@require_api_auth
|
||||
def create_mailbox():
|
||||
"""
|
||||
Create a new mailbox. User needs to verify the mailbox via an activation email.
|
||||
Input:
|
||||
email: in body
|
||||
Output:
|
||||
the new mailbox
|
||||
- id
|
||||
- email
|
||||
- verified
|
||||
|
||||
"""
|
||||
user = g.user
|
||||
mailbox_email = request.get_json().get("email").lower().strip()
|
||||
|
||||
if mailbox_already_used(mailbox_email, user):
|
||||
return jsonify(error=f"{mailbox_email} already used"), 400
|
||||
elif not email_domain_can_be_used_as_mailbox(mailbox_email):
|
||||
return (
|
||||
jsonify(
|
||||
error=f"{mailbox_email} cannot be used. Please note a mailbox cannot "
|
||||
f"be a disposable email address"
|
||||
),
|
||||
400,
|
||||
)
|
||||
else:
|
||||
new_mailbox = Mailbox.create(email=mailbox_email, user_id=user.id)
|
||||
db.session.commit()
|
||||
|
||||
send_verification_email(user, new_mailbox)
|
||||
|
||||
return (
|
||||
jsonify(
|
||||
id=new_mailbox.id,
|
||||
email=new_mailbox.email,
|
||||
verified=new_mailbox.verified,
|
||||
default=user.default_mailbox_id == new_mailbox.id,
|
||||
),
|
||||
201,
|
||||
)
|
||||
|
||||
|
||||
@api_bp.route("/mailboxes/<mailbox_id>", methods=["DELETE"])
|
||||
@cross_origin()
|
||||
@require_api_auth
|
||||
def delete_mailbox(mailbox_id):
|
||||
"""
|
||||
Delete mailbox
|
||||
Input:
|
||||
mailbox_id: in url
|
||||
Output:
|
||||
200 if deleted successfully
|
||||
|
||||
"""
|
||||
user = g.user
|
||||
mailbox = Mailbox.get(mailbox_id)
|
||||
|
||||
if not mailbox or mailbox.user_id != user.id:
|
||||
return jsonify(error="Forbidden"), 403
|
||||
|
||||
if mailbox.id == user.default_mailbox_id:
|
||||
return jsonify(error="You cannot delete the default mailbox"), 400
|
||||
|
||||
Mailbox.delete(mailbox_id)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(deleted=True), 200
|
||||
|
||||
|
||||
@api_bp.route("/mailboxes/<mailbox_id>", methods=["PUT"])
|
||||
@cross_origin()
|
||||
@require_api_auth
|
||||
def update_mailbox(mailbox_id):
|
||||
"""
|
||||
Update mailbox
|
||||
Input:
|
||||
mailbox_id: in url
|
||||
(optional) default: in body. Set a mailbox as the default mailbox.
|
||||
(optional) email: in body. Change a mailbox email.
|
||||
(optional) cancel_email_change: in body. Cancel mailbox email change.
|
||||
Output:
|
||||
200 if updated successfully
|
||||
|
||||
"""
|
||||
user = g.user
|
||||
mailbox = Mailbox.get(mailbox_id)
|
||||
|
||||
if not mailbox or mailbox.user_id != user.id:
|
||||
return jsonify(error="Forbidden"), 403
|
||||
|
||||
data = request.get_json() or {}
|
||||
changed = False
|
||||
if "default" in data:
|
||||
is_default = data.get("default")
|
||||
if is_default:
|
||||
user.default_mailbox_id = mailbox.id
|
||||
changed = True
|
||||
|
||||
if "email" in data:
|
||||
new_email = data.get("email").lower().strip()
|
||||
|
||||
if mailbox_already_used(new_email, user):
|
||||
return jsonify(error=f"{new_email} already used"), 400
|
||||
elif not email_domain_can_be_used_as_mailbox(new_email):
|
||||
return (
|
||||
jsonify(
|
||||
error=f"{new_email} cannot be used. Please note a mailbox cannot "
|
||||
f"be a disposable email address"
|
||||
),
|
||||
400,
|
||||
)
|
||||
|
||||
try:
|
||||
verify_mailbox_change(user, mailbox, new_email)
|
||||
except SMTPRecipientsRefused:
|
||||
return jsonify(error=f"Incorrect mailbox, please recheck {new_email}"), 400
|
||||
else:
|
||||
mailbox.new_email = new_email
|
||||
changed = True
|
||||
|
||||
if "cancel_email_change" in data:
|
||||
cancel_email_change = data.get("cancel_email_change")
|
||||
if cancel_email_change:
|
||||
mailbox.new_email = None
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(updated=True), 200
|
||||
|
||||
|
||||
@api_bp.route("/mailboxes", methods=["GET"])
|
||||
@cross_origin()
|
||||
@require_api_auth
|
||||
def get_mailboxes():
|
||||
"""
|
||||
Get mailboxes
|
||||
Output:
|
||||
- mailboxes: list of alias:
|
||||
- id
|
||||
- email
|
||||
"""
|
||||
user = g.user
|
||||
|
||||
return (
|
||||
jsonify(
|
||||
mailboxes=[{"id": mb.id, "email": mb.email} for mb in user.mailboxes()]
|
||||
),
|
||||
200,
|
||||
)
|
@ -94,29 +94,7 @@ def mailbox_route():
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
s = Signer(MAILBOX_SECRET)
|
||||
mailbox_id_signed = s.sign(str(new_mailbox.id)).decode()
|
||||
verification_url = (
|
||||
URL
|
||||
+ "/dashboard/mailbox_verify"
|
||||
+ f"?mailbox_id={mailbox_id_signed}"
|
||||
)
|
||||
send_email(
|
||||
mailbox_email,
|
||||
f"Please confirm your email {mailbox_email}",
|
||||
render(
|
||||
"transactional/verify-mailbox.txt",
|
||||
user=current_user,
|
||||
link=verification_url,
|
||||
mailbox_email=mailbox_email,
|
||||
),
|
||||
render(
|
||||
"transactional/verify-mailbox.html",
|
||||
user=current_user,
|
||||
link=verification_url,
|
||||
mailbox_email=mailbox_email,
|
||||
),
|
||||
)
|
||||
send_verification_email(current_user, new_mailbox)
|
||||
|
||||
flash(
|
||||
f"You are going to receive an email to confirm {mailbox_email}.",
|
||||
@ -138,6 +116,30 @@ def mailbox_route():
|
||||
)
|
||||
|
||||
|
||||
def send_verification_email(user, mailbox):
|
||||
s = Signer(MAILBOX_SECRET)
|
||||
mailbox_id_signed = s.sign(str(mailbox.id)).decode()
|
||||
verification_url = (
|
||||
URL + "/dashboard/mailbox_verify" + f"?mailbox_id={mailbox_id_signed}"
|
||||
)
|
||||
send_email(
|
||||
mailbox.email,
|
||||
f"Please confirm your email {mailbox.email}",
|
||||
render(
|
||||
"transactional/verify-mailbox.txt",
|
||||
user=user,
|
||||
link=verification_url,
|
||||
mailbox_email=mailbox.email,
|
||||
),
|
||||
render(
|
||||
"transactional/verify-mailbox.html",
|
||||
user=user,
|
||||
link=verification_url,
|
||||
mailbox_email=mailbox.email,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dashboard_bp.route("/mailbox_verify")
|
||||
def mailbox_verify():
|
||||
s = Signer(MAILBOX_SECRET)
|
||||
|
@ -60,33 +60,8 @@ def mailbox_detail_route(mailbox_id):
|
||||
mailbox.new_email = new_email
|
||||
db.session.commit()
|
||||
|
||||
s = Signer(MAILBOX_SECRET)
|
||||
mailbox_id_signed = s.sign(str(mailbox.id)).decode()
|
||||
verification_url = (
|
||||
URL
|
||||
+ "/dashboard/mailbox/confirm_change"
|
||||
+ f"?mailbox_id={mailbox_id_signed}"
|
||||
)
|
||||
|
||||
try:
|
||||
send_email(
|
||||
new_email,
|
||||
f"Confirm mailbox change on SimpleLogin",
|
||||
render(
|
||||
"transactional/verify-mailbox-change.txt",
|
||||
user=current_user,
|
||||
link=verification_url,
|
||||
mailbox_email=mailbox.email,
|
||||
mailbox_new_email=new_email,
|
||||
),
|
||||
render(
|
||||
"transactional/verify-mailbox-change.html",
|
||||
user=current_user,
|
||||
link=verification_url,
|
||||
mailbox_email=mailbox.email,
|
||||
mailbox_new_email=new_email,
|
||||
),
|
||||
)
|
||||
verify_mailbox_change(current_user, mailbox, new_email)
|
||||
except SMTPRecipientsRefused:
|
||||
flash(
|
||||
f"Incorrect mailbox, please recheck {mailbox.email}",
|
||||
@ -151,6 +126,33 @@ def mailbox_detail_route(mailbox_id):
|
||||
return render_template("dashboard/mailbox_detail.html", **locals())
|
||||
|
||||
|
||||
def verify_mailbox_change(user, mailbox, new_email):
|
||||
s = Signer(MAILBOX_SECRET)
|
||||
mailbox_id_signed = s.sign(str(mailbox.id)).decode()
|
||||
verification_url = (
|
||||
URL + "/dashboard/mailbox/confirm_change" + f"?mailbox_id={mailbox_id_signed}"
|
||||
)
|
||||
|
||||
send_email(
|
||||
new_email,
|
||||
f"Confirm mailbox change on SimpleLogin",
|
||||
render(
|
||||
"transactional/verify-mailbox-change.txt",
|
||||
user=user,
|
||||
link=verification_url,
|
||||
mailbox_email=mailbox.email,
|
||||
mailbox_new_email=new_email,
|
||||
),
|
||||
render(
|
||||
"transactional/verify-mailbox-change.html",
|
||||
user=user,
|
||||
link=verification_url,
|
||||
mailbox_email=mailbox.email,
|
||||
mailbox_new_email=new_email,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dashboard_bp.route(
|
||||
"/mailbox/<int:mailbox_id>/cancel_email_change", methods=["GET", "POST"]
|
||||
)
|
||||
|
@ -1,5 +1,3 @@
|
||||
import json
|
||||
|
||||
from flask import url_for
|
||||
|
||||
from app.config import PAGE_LIMIT
|
||||
@ -571,28 +569,3 @@ def test_get_alias(flask_client):
|
||||
assert "nb_reply" in res
|
||||
assert "enabled" in res
|
||||
assert "note" in res
|
||||
|
||||
|
||||
def test_get_mailboxes(flask_client):
|
||||
user = User.create(
|
||||
email="a@b.c", password="password", name="Test User", activated=True
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
# create api_key
|
||||
api_key = ApiKey.create(user.id, "for test")
|
||||
db.session.commit()
|
||||
|
||||
Mailbox.create(user_id=user.id, email="m1@example.com", verified=True)
|
||||
Mailbox.create(user_id=user.id, email="m2@example.com", verified=False)
|
||||
db.session.commit()
|
||||
|
||||
r = flask_client.get(
|
||||
url_for("api.get_mailboxes"), headers={"Authentication": api_key.code},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
# m2@example.com is not returned as it's not verified
|
||||
assert r.json == {
|
||||
"mailboxes": [{"email": "a@b.c", "id": 1}, {"email": "m1@example.com", "id": 2}]
|
||||
}
|
||||
print(json.dumps(r.json, indent=2))
|
||||
|
187
tests/api/test_mailbox.py
Normal file
187
tests/api/test_mailbox.py
Normal file
@ -0,0 +1,187 @@
|
||||
import json
|
||||
|
||||
from flask import url_for
|
||||
|
||||
from flask import url_for
|
||||
|
||||
from app.extensions import db
|
||||
from app.models import User, ApiKey, Mailbox
|
||||
|
||||
|
||||
def test_create_mailbox(flask_client):
|
||||
user = User.create(
|
||||
email="a@b.c", password="password", name="Test User", activated=True
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
# create api_key
|
||||
api_key = ApiKey.create(user.id, "for test")
|
||||
db.session.commit()
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("api.create_mailbox"),
|
||||
headers={"Authentication": api_key.code},
|
||||
json={"email": "mailbox@gmail.com"},
|
||||
)
|
||||
|
||||
assert r.status_code == 201
|
||||
assert r.json["email"] == "mailbox@gmail.com"
|
||||
assert r.json["verified"] is False
|
||||
assert r.json["id"] > 0
|
||||
assert r.json["default"] is False
|
||||
|
||||
|
||||
def test_delete_mailbox(flask_client):
|
||||
user = User.create(
|
||||
email="a@b.c", password="password", name="Test User", activated=True
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
# create api_key
|
||||
api_key = ApiKey.create(user.id, "for test")
|
||||
db.session.commit()
|
||||
|
||||
# create a mailbox
|
||||
mb = Mailbox.create(user_id=user.id, email="mb@gmail.com")
|
||||
db.session.commit()
|
||||
|
||||
r = flask_client.delete(
|
||||
url_for("api.delete_mailbox", mailbox_id=mb.id),
|
||||
headers={"Authentication": api_key.code},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
|
||||
|
||||
def test_delete_default_mailbox(flask_client):
|
||||
user = User.create(
|
||||
email="a@b.c", password="password", name="Test User", activated=True
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
# create api_key
|
||||
api_key = ApiKey.create(user.id, "for test")
|
||||
db.session.commit()
|
||||
|
||||
# assert user cannot delete the default mailbox
|
||||
r = flask_client.delete(
|
||||
url_for("api.delete_mailbox", mailbox_id=user.default_mailbox_id),
|
||||
headers={"Authentication": api_key.code},
|
||||
)
|
||||
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
def test_set_mailbox_as_default(flask_client):
|
||||
user = User.create(
|
||||
email="a@b.c", password="password", name="Test User", activated=True
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
# create api_key
|
||||
api_key = ApiKey.create(user.id, "for test")
|
||||
db.session.commit()
|
||||
|
||||
# create a mailbox
|
||||
mb = Mailbox.create(user_id=user.id, email="mb@gmail.com")
|
||||
db.session.commit()
|
||||
assert user.default_mailbox_id != mb.id
|
||||
|
||||
r = flask_client.put(
|
||||
url_for("api.delete_mailbox", mailbox_id=mb.id),
|
||||
headers={"Authentication": api_key.code},
|
||||
json={"default": True},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
|
||||
mb = Mailbox.get(mb.id)
|
||||
assert user.default_mailbox_id == mb.id
|
||||
|
||||
|
||||
def test_update_mailbox_email(flask_client):
|
||||
user = User.create(
|
||||
email="a@b.c", password="password", name="Test User", activated=True
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
# create api_key
|
||||
api_key = ApiKey.create(user.id, "for test")
|
||||
db.session.commit()
|
||||
|
||||
# create a mailbox
|
||||
mb = Mailbox.create(user_id=user.id, email="mb@gmail.com")
|
||||
db.session.commit()
|
||||
|
||||
r = flask_client.put(
|
||||
url_for("api.delete_mailbox", mailbox_id=mb.id),
|
||||
headers={"Authentication": api_key.code},
|
||||
json={"email": "new-email@gmail.com"},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
|
||||
mb = Mailbox.get(mb.id)
|
||||
assert mb.new_email == "new-email@gmail.com"
|
||||
|
||||
|
||||
def test_cancel_mailbox_email_change(flask_client):
|
||||
user = User.create(
|
||||
email="a@b.c", password="password", name="Test User", activated=True
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
# create api_key
|
||||
api_key = ApiKey.create(user.id, "for test")
|
||||
db.session.commit()
|
||||
|
||||
# create a mailbox
|
||||
mb = Mailbox.create(user_id=user.id, email="mb@gmail.com")
|
||||
db.session.commit()
|
||||
|
||||
# update mailbox email
|
||||
r = flask_client.put(
|
||||
url_for("api.delete_mailbox", mailbox_id=mb.id),
|
||||
headers={"Authentication": api_key.code},
|
||||
json={"email": "new-email@gmail.com"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
mb = Mailbox.get(mb.id)
|
||||
assert mb.new_email == "new-email@gmail.com"
|
||||
|
||||
# cancel mailbox email change
|
||||
r = flask_client.put(
|
||||
url_for("api.delete_mailbox", mailbox_id=mb.id),
|
||||
headers={"Authentication": api_key.code},
|
||||
json={"cancel_email_change": True},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
mb = Mailbox.get(mb.id)
|
||||
assert mb.new_email is None
|
||||
|
||||
|
||||
def test_get_mailboxes(flask_client):
|
||||
user = User.create(
|
||||
email="a@b.c", password="password", name="Test User", activated=True
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
# create api_key
|
||||
api_key = ApiKey.create(user.id, "for test")
|
||||
db.session.commit()
|
||||
|
||||
Mailbox.create(user_id=user.id, email="m1@example.com", verified=True)
|
||||
Mailbox.create(user_id=user.id, email="m2@example.com", verified=False)
|
||||
db.session.commit()
|
||||
|
||||
r = flask_client.get(
|
||||
url_for("api.get_mailboxes"), headers={"Authentication": api_key.code},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
# m2@example.com is not returned as it's not verified
|
||||
assert r.json == {
|
||||
"mailboxes": [{"email": "a@b.c", "id": 1}, {"email": "m1@example.com", "id": 2}]
|
||||
}
|
||||
print(json.dumps(r.json, indent=2))
|
Loading…
Reference in New Issue
Block a user