diff --git a/README.md b/README.md index a366e4ba..4e28774f 100644 --- a/README.md +++ b/README.md @@ -901,6 +901,79 @@ If success, 200 with the list of activities, for example: } ``` +#### PUT /api/aliases/:alias_id + +Update alias note. In the future, the endpoint will support other updates (e.g. mailbox update) as well. + +Input: +- `Authentication` header that contains the api key +- `alias_id` in url. +- `note` in request body + +Output: +If success, return 200 + +#### GET /api/aliases/:alias_id/contacts + +Get contacts for a given alias. + +Input: +- `Authentication` header that contains the api key +- `alias_id`: the alias id, passed in url. +- `page_id` used in request query (`?page_id=0`). The endpoint returns maximum 20 contacts for each page. `page_id` starts at 0. + +Output: +If success, 200 with the list of contacts, for example: + +```json +{ + "contacts": [ + { + "contact": "marketing@example.com", + "creation_date": "2020-02-21 11:35:00+00:00", + "creation_timestamp": 1582284900, + "last_email_sent_date": null, + "last_email_sent_timestamp": null, + "reverse_alias": "marketing at example.com " + }, + { + "contact": "newsletter@example.com", + "creation_date": "2020-02-21 11:35:00+00:00", + "creation_timestamp": 1582284900, + "last_email_sent_date": "2020-02-21 11:35:00+00:00",, + "last_email_sent_timestamp": 1582284900, + "reverse_alias": "newsletter at example.com " + } + ] +} +``` + +Please note that last_email_sent_timestamp and last_email_sent_date can be null. + + +#### POST /api/aliases/:alias_id/contacts + +Create a new contact for an alias. + +Input: +- `Authentication` header that contains the api key +- `alias_id` in url. +- `contact` in request body + +Output: +If success, return 201 +Return 409 if contact is already added. + +``` +{ + "contact": "First Last ", + "creation_date": "2020-03-14 11:52:41+00:00", + "creation_timestamp": 1584186761, + "last_email_sent_date": null, + "last_email_sent_timestamp": null, + "reverse_alias": "First Last first@example.com " +} +``` ### Database migration diff --git a/app/api/views/alias.py b/app/api/views/alias.py index 18f5a46a..25f0dfdc 100644 --- a/app/api/views/alias.py +++ b/app/api/views/alias.py @@ -3,10 +3,26 @@ from flask import jsonify, request from flask_cors import cross_origin from app.api.base import api_bp, verify_api_key +from app.config import PAGE_LIMIT from app.dashboard.views.alias_log import get_alias_log from app.dashboard.views.index import get_alias_info, AliasInfo from app.extensions import db -from app.models import GenEmail +from app.models import GenEmail, ForwardEmail, ForwardEmailLog +from app.utils import random_string +import re + +from flask import render_template, request, redirect, url_for, flash +from flask_login import login_required, current_user +from flask_wtf import FlaskForm +from wtforms import StringField, validators, ValidationError + +from app.config import EMAIL_DOMAIN +from app.dashboard.base import dashboard_bp +from app.email_utils import get_email_part +from app.extensions import db +from app.log import LOG +from app.models import GenEmail, ForwardEmail +from app.utils import random_string @api_bp.route("/aliases") @@ -157,4 +173,157 @@ def get_alias_activities(alias_id): activities.append(activity) - return (jsonify(activities=activities), 200) + return jsonify(activities=activities), 200 + + +@api_bp.route("/aliases/", methods=["PUT"]) +@cross_origin() +@verify_api_key +def update_alias(alias_id): + """ + Update alias note + Input: + alias_id: in url + note: in body + Output: + 200 + + + """ + data = request.get_json() + if not data: + return jsonify(error="request body cannot be empty"), 400 + + user = g.user + gen_email: GenEmail = GenEmail.get(alias_id) + + if gen_email.user_id != user.id: + return jsonify(error="Forbidden"), 403 + + new_note = data.get("note") + gen_email.note = new_note + db.session.commit() + + return jsonify(note=new_note), 200 + + +def serialize_forward_email(fe: ForwardEmail) -> dict: + + res = { + "creation_date": fe.created_at.format(), + "creation_timestamp": fe.created_at.timestamp, + "last_email_sent_date": None, + "last_email_sent_timestamp": None, + "contact": fe.website_from or fe.website_email, + "reverse_alias": fe.website_send_to(), + } + + fel: ForwardEmailLog = fe.last_reply() + if fel: + res["last_email_sent_date"] = fel.created_at.format() + res["last_email_sent_timestamp"] = fel.created_at.timestamp + + return res + + +def get_alias_contacts(gen_email, page_id: int) -> [dict]: + q = ( + ForwardEmail.query.filter_by(gen_email_id=gen_email.id) + .order_by(ForwardEmail.id.desc()) + .limit(PAGE_LIMIT) + .offset(page_id * PAGE_LIMIT) + ) + + res = [] + for fe in q.all(): + res.append(serialize_forward_email(fe)) + + return res + + +@api_bp.route("/aliases//contacts") +@cross_origin() +@verify_api_key +def get_alias_contacts_route(alias_id): + """ + Get alias contacts + Input: + page_id: in query + Output: + - contacts: list of contacts: + - creation_date + - creation_timestamp + - last_email_sent_date + - last_email_sent_timestamp + - contact + - reverse_alias + + """ + user = g.user + try: + page_id = int(request.args.get("page_id")) + except (ValueError, TypeError): + return jsonify(error="page_id must be provided in request query"), 400 + + gen_email: GenEmail = GenEmail.get(alias_id) + + if gen_email.user_id != user.id: + return jsonify(error="Forbidden"), 403 + + contacts = get_alias_contacts(gen_email, page_id) + + return jsonify(contacts=contacts), 200 + + +@api_bp.route("/aliases//contacts", methods=["POST"]) +@cross_origin() +@verify_api_key +def create_contact_route(alias_id): + """ + Create contact for an alias + Input: + alias_id: in url + contact: in body + Output: + 201 if success + 409 if contact already added + + + """ + data = request.get_json() + if not data: + return jsonify(error="request body cannot be empty"), 400 + + user = g.user + gen_email: GenEmail = GenEmail.get(alias_id) + + if gen_email.user_id != user.id: + return jsonify(error="Forbidden"), 403 + + contact_email = data.get("contact") + + # generate a reply_email, make sure it is unique + # not use while to avoid infinite loop + reply_email = f"ra+{random_string(25)}@{EMAIL_DOMAIN}" + for _ in range(1000): + reply_email = f"ra+{random_string(25)}@{EMAIL_DOMAIN}" + if not ForwardEmail.get_by(reply_email=reply_email): + break + + website_email = get_email_part(contact_email) + + # already been added + if ForwardEmail.get_by(gen_email_id=gen_email.id, website_email=website_email): + return jsonify(error="Contact already added"), 409 + + forward_email = ForwardEmail.create( + gen_email_id=gen_email.id, + website_email=website_email, + website_from=contact_email, + reply_email=reply_email, + ) + + LOG.d("create reverse-alias for %s %s", contact_email, gen_email) + db.session.commit() + + return jsonify(**serialize_forward_email(forward_email)), 201 diff --git a/app/models.py b/app/models.py index a7cd1771..aa18c0a1 100644 --- a/app/models.py +++ b/app/models.py @@ -696,7 +696,7 @@ class ClientUser(db.Model, ModelMixin): class ForwardEmail(db.Model, ModelMixin): """ - Emails that are forwarded through SL: email that is sent by website to user via SL alias + Store configuration of sender (website-email) and alias. """ __table_args__ = ( diff --git a/tests/api/test_alias.py b/tests/api/test_alias.py index f6bc62b7..4a6aa67e 100644 --- a/tests/api/test_alias.py +++ b/tests/api/test_alias.py @@ -160,3 +160,110 @@ def test_alias_activities(flask_client): headers={"Authentication": api_key.code}, ) assert len(r.json["activities"]) < 3 + + +def test_update_alias(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() + + gen_email = GenEmail.create_new_random(user) + db.session.commit() + + r = flask_client.put( + url_for("api.update_alias", alias_id=gen_email.id), + headers={"Authentication": api_key.code}, + json={"note": "test note"}, + ) + + assert r.status_code == 200 + assert r.json == {"note": "test note"} + + +def test_alias_contacts(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() + + gen_email = GenEmail.create_new_random(user) + db.session.commit() + + # create some alias log + for i in range(PAGE_LIMIT + 1): + forward_email = ForwardEmail.create( + website_email=f"marketing-{i}@example.com", + reply_email=f"reply-{i}@a.b", + gen_email_id=gen_email.id, + ) + db.session.commit() + + ForwardEmailLog.create(forward_id=forward_email.id, is_reply=True) + db.session.commit() + + r = flask_client.get( + url_for("api.get_alias_contacts_route", alias_id=gen_email.id, page_id=0), + headers={"Authentication": api_key.code}, + ) + + assert r.status_code == 200 + assert len(r.json["contacts"]) == PAGE_LIMIT + for ac in r.json["contacts"]: + assert ac["creation_date"] + assert ac["creation_timestamp"] + assert ac["last_email_sent_date"] + assert ac["last_email_sent_timestamp"] + assert ac["contact"] + assert ac["reverse_alias"] + + # second page, should return 1 result only + r = flask_client.get( + url_for("api.get_alias_contacts_route", alias_id=gen_email.id, page_id=1), + headers={"Authentication": api_key.code}, + ) + assert len(r.json["contacts"]) == 1 + + +def test_create_contact_route(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() + + gen_email = GenEmail.create_new_random(user) + db.session.commit() + + r = flask_client.post( + url_for("api.create_contact_route", alias_id=gen_email.id), + headers={"Authentication": api_key.code}, + json={"contact": "First Last "}, + ) + + assert r.status_code == 201 + assert r.json["contact"] == "First Last " + assert "creation_date" in r.json + assert "creation_timestamp" in r.json + assert r.json["last_email_sent_date"] is None + assert r.json["last_email_sent_timestamp"] is None + assert r.json["reverse_alias"] + + # re-add a contact, should return 409 + r = flask_client.post( + url_for("api.create_contact_route", alias_id=gen_email.id), + headers={"Authentication": api_key.code}, + json={"contact": "First2 Last2 "}, + ) + assert r.status_code == 409