commit
44e2e175ef
35
README.md
35
README.md
|
@ -632,8 +632,10 @@ Output: if api key is correct, return a json with user name and whether user is
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "John Wick",
|
"name": "John Wick",
|
||||||
"is_premium": false
|
"is_premium": false,
|
||||||
|
"email": "john@wick.com",
|
||||||
|
"in_trial": true
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -795,6 +797,13 @@ Input:
|
||||||
Output:
|
Output:
|
||||||
- 200: user is going to receive an email that contains the activation code.
|
- 200: user is going to receive an email that contains the activation code.
|
||||||
|
|
||||||
|
#### POST /api/auth/forgot_password
|
||||||
|
|
||||||
|
Input:
|
||||||
|
- email
|
||||||
|
|
||||||
|
Output: always return 200, even if email doesn't exist. User need to enter correctly their email.
|
||||||
|
|
||||||
#### GET /api/aliases
|
#### GET /api/aliases
|
||||||
|
|
||||||
Get user aliases.
|
Get user aliases.
|
||||||
|
@ -802,6 +811,7 @@ Get user aliases.
|
||||||
Input:
|
Input:
|
||||||
- `Authentication` header that contains the api key
|
- `Authentication` header that contains the api key
|
||||||
- `page_id` used for the pagination. The endpoint returns maximum 20 aliases for each page. `page_id` starts at 0.
|
- `page_id` used for the pagination. The endpoint returns maximum 20 aliases for each page. `page_id` starts at 0.
|
||||||
|
- (Optional) query: included in request body. Some frameworks might prevent GET request having a non-empty body, in this case this endpoint also supports POST.
|
||||||
|
|
||||||
Output:
|
Output:
|
||||||
If success, 200 with the list of aliases, for example:
|
If success, 200 with the list of aliases, for example:
|
||||||
|
@ -929,6 +939,7 @@ If success, 200 with the list of contacts, for example:
|
||||||
{
|
{
|
||||||
"contacts": [
|
"contacts": [
|
||||||
{
|
{
|
||||||
|
"id": 1,
|
||||||
"contact": "marketing@example.com",
|
"contact": "marketing@example.com",
|
||||||
"creation_date": "2020-02-21 11:35:00+00:00",
|
"creation_date": "2020-02-21 11:35:00+00:00",
|
||||||
"creation_timestamp": 1582284900,
|
"creation_timestamp": 1582284900,
|
||||||
|
@ -937,6 +948,7 @@ If success, 200 with the list of contacts, for example:
|
||||||
"reverse_alias": "marketing at example.com <reply+bzvpazcdedcgcpztehxzgjgzmxskqa@sl.co>"
|
"reverse_alias": "marketing at example.com <reply+bzvpazcdedcgcpztehxzgjgzmxskqa@sl.co>"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": 2,
|
||||||
"contact": "newsletter@example.com",
|
"contact": "newsletter@example.com",
|
||||||
"creation_date": "2020-02-21 11:35:00+00:00",
|
"creation_date": "2020-02-21 11:35:00+00:00",
|
||||||
"creation_timestamp": 1582284900,
|
"creation_timestamp": 1582284900,
|
||||||
|
@ -966,6 +978,7 @@ Return 409 if contact is already added.
|
||||||
|
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
|
"id": 1,
|
||||||
"contact": "First Last <first@example.com>",
|
"contact": "First Last <first@example.com>",
|
||||||
"creation_date": "2020-03-14 11:52:41+00:00",
|
"creation_date": "2020-03-14 11:52:41+00:00",
|
||||||
"creation_timestamp": 1584186761,
|
"creation_timestamp": 1584186761,
|
||||||
|
@ -975,6 +988,24 @@ Return 409 if contact is already added.
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### DELETE /api/contacts/:contact_id
|
||||||
|
|
||||||
|
Delete a contact
|
||||||
|
|
||||||
|
Input:
|
||||||
|
- `Authentication` header that contains the api key
|
||||||
|
- `contact_id` in url.
|
||||||
|
|
||||||
|
Output:
|
||||||
|
If success, 200.
|
||||||
|
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"deleted": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Database migration
|
### Database migration
|
||||||
|
|
||||||
The database migration is handled by `alembic`
|
The database migration is handled by `alembic`
|
||||||
|
|
|
@ -9,15 +9,18 @@ from app.api.base import api_bp, verify_api_key
|
||||||
from app.config import EMAIL_DOMAIN
|
from app.config import EMAIL_DOMAIN
|
||||||
from app.config import PAGE_LIMIT
|
from app.config import PAGE_LIMIT
|
||||||
from app.dashboard.views.alias_log import get_alias_log
|
from app.dashboard.views.alias_log import get_alias_log
|
||||||
from app.dashboard.views.index import get_alias_info, AliasInfo
|
from app.dashboard.views.index import (
|
||||||
|
AliasInfo,
|
||||||
|
get_alias_infos_with_pagination,
|
||||||
|
)
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.log import LOG
|
from app.log import LOG
|
||||||
from app.models import ForwardEmailLog
|
from app.models import Alias, Contact
|
||||||
from app.models import GenEmail, ForwardEmail
|
from app.models import EmailLog
|
||||||
from app.utils import random_string
|
from app.utils import random_string
|
||||||
|
|
||||||
|
|
||||||
@api_bp.route("/aliases")
|
@api_bp.route("/aliases", methods=["GET", "POST"])
|
||||||
@cross_origin()
|
@cross_origin()
|
||||||
@verify_api_key
|
@verify_api_key
|
||||||
def get_aliases():
|
def get_aliases():
|
||||||
|
@ -43,23 +46,30 @@ def get_aliases():
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
return jsonify(error="page_id must be provided in request query"), 400
|
return jsonify(error="page_id must be provided in request query"), 400
|
||||||
|
|
||||||
aliases: [AliasInfo] = get_alias_info(user, page_id=page_id)
|
query = None
|
||||||
|
data = request.get_json(silent=True)
|
||||||
|
if data:
|
||||||
|
query = data.get("query")
|
||||||
|
|
||||||
|
alias_infos: [AliasInfo] = get_alias_infos_with_pagination(
|
||||||
|
user, page_id=page_id, query=query
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
jsonify(
|
jsonify(
|
||||||
aliases=[
|
aliases=[
|
||||||
{
|
{
|
||||||
"id": alias.id,
|
"id": alias_info.id,
|
||||||
"email": alias.gen_email.email,
|
"email": alias_info.alias.email,
|
||||||
"creation_date": alias.gen_email.created_at.format(),
|
"creation_date": alias_info.alias.created_at.format(),
|
||||||
"creation_timestamp": alias.gen_email.created_at.timestamp,
|
"creation_timestamp": alias_info.alias.created_at.timestamp,
|
||||||
"nb_forward": alias.nb_forward,
|
"nb_forward": alias_info.nb_forward,
|
||||||
"nb_block": alias.nb_blocked,
|
"nb_block": alias_info.nb_blocked,
|
||||||
"nb_reply": alias.nb_reply,
|
"nb_reply": alias_info.nb_reply,
|
||||||
"enabled": alias.gen_email.enabled,
|
"enabled": alias_info.alias.enabled,
|
||||||
"note": alias.note,
|
"note": alias_info.note,
|
||||||
}
|
}
|
||||||
for alias in aliases
|
for alias_info in alias_infos
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
200,
|
200,
|
||||||
|
@ -79,12 +89,12 @@ def delete_alias(alias_id):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
user = g.user
|
user = g.user
|
||||||
gen_email = GenEmail.get(alias_id)
|
alias = Alias.get(alias_id)
|
||||||
|
|
||||||
if gen_email.user_id != user.id:
|
if alias.user_id != user.id:
|
||||||
return jsonify(error="Forbidden"), 403
|
return jsonify(error="Forbidden"), 403
|
||||||
|
|
||||||
GenEmail.delete(alias_id)
|
Alias.delete(alias_id)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify(deleted=True), 200
|
return jsonify(deleted=True), 200
|
||||||
|
@ -105,15 +115,15 @@ def toggle_alias(alias_id):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
user = g.user
|
user = g.user
|
||||||
gen_email: GenEmail = GenEmail.get(alias_id)
|
alias: Alias = Alias.get(alias_id)
|
||||||
|
|
||||||
if gen_email.user_id != user.id:
|
if alias.user_id != user.id:
|
||||||
return jsonify(error="Forbidden"), 403
|
return jsonify(error="Forbidden"), 403
|
||||||
|
|
||||||
gen_email.enabled = not gen_email.enabled
|
alias.enabled = not alias.enabled
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify(enabled=gen_email.enabled), 200
|
return jsonify(enabled=alias.enabled), 200
|
||||||
|
|
||||||
|
|
||||||
@api_bp.route("/aliases/<int:alias_id>/activities")
|
@api_bp.route("/aliases/<int:alias_id>/activities")
|
||||||
|
@ -138,12 +148,12 @@ def get_alias_activities(alias_id):
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
return jsonify(error="page_id must be provided in request query"), 400
|
return jsonify(error="page_id must be provided in request query"), 400
|
||||||
|
|
||||||
gen_email: GenEmail = GenEmail.get(alias_id)
|
alias: Alias = Alias.get(alias_id)
|
||||||
|
|
||||||
if gen_email.user_id != user.id:
|
if alias.user_id != user.id:
|
||||||
return jsonify(error="Forbidden"), 403
|
return jsonify(error="Forbidden"), 403
|
||||||
|
|
||||||
alias_logs = get_alias_log(gen_email, page_id)
|
alias_logs = get_alias_log(alias, page_id)
|
||||||
|
|
||||||
activities = []
|
activities = []
|
||||||
for alias_log in alias_logs:
|
for alias_log in alias_logs:
|
||||||
|
@ -187,21 +197,22 @@ def update_alias(alias_id):
|
||||||
return jsonify(error="request body cannot be empty"), 400
|
return jsonify(error="request body cannot be empty"), 400
|
||||||
|
|
||||||
user = g.user
|
user = g.user
|
||||||
gen_email: GenEmail = GenEmail.get(alias_id)
|
alias: Alias = Alias.get(alias_id)
|
||||||
|
|
||||||
if gen_email.user_id != user.id:
|
if alias.user_id != user.id:
|
||||||
return jsonify(error="Forbidden"), 403
|
return jsonify(error="Forbidden"), 403
|
||||||
|
|
||||||
new_note = data.get("note")
|
new_note = data.get("note")
|
||||||
gen_email.note = new_note
|
alias.note = new_note
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify(note=new_note), 200
|
return jsonify(note=new_note), 200
|
||||||
|
|
||||||
|
|
||||||
def serialize_forward_email(fe: ForwardEmail) -> dict:
|
def serialize_contact(fe: Contact) -> dict:
|
||||||
|
|
||||||
res = {
|
res = {
|
||||||
|
"id": fe.id,
|
||||||
"creation_date": fe.created_at.format(),
|
"creation_date": fe.created_at.format(),
|
||||||
"creation_timestamp": fe.created_at.timestamp,
|
"creation_timestamp": fe.created_at.timestamp,
|
||||||
"last_email_sent_date": None,
|
"last_email_sent_date": None,
|
||||||
|
@ -210,7 +221,7 @@ def serialize_forward_email(fe: ForwardEmail) -> dict:
|
||||||
"reverse_alias": fe.website_send_to(),
|
"reverse_alias": fe.website_send_to(),
|
||||||
}
|
}
|
||||||
|
|
||||||
fel: ForwardEmailLog = fe.last_reply()
|
fel: EmailLog = fe.last_reply()
|
||||||
if fel:
|
if fel:
|
||||||
res["last_email_sent_date"] = fel.created_at.format()
|
res["last_email_sent_date"] = fel.created_at.format()
|
||||||
res["last_email_sent_timestamp"] = fel.created_at.timestamp
|
res["last_email_sent_timestamp"] = fel.created_at.timestamp
|
||||||
|
@ -218,17 +229,17 @@ def serialize_forward_email(fe: ForwardEmail) -> dict:
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
def get_alias_contacts(gen_email, page_id: int) -> [dict]:
|
def get_alias_contacts(alias, page_id: int) -> [dict]:
|
||||||
q = (
|
q = (
|
||||||
ForwardEmail.query.filter_by(gen_email_id=gen_email.id)
|
Contact.query.filter_by(alias_id=alias.id)
|
||||||
.order_by(ForwardEmail.id.desc())
|
.order_by(Contact.id.desc())
|
||||||
.limit(PAGE_LIMIT)
|
.limit(PAGE_LIMIT)
|
||||||
.offset(page_id * PAGE_LIMIT)
|
.offset(page_id * PAGE_LIMIT)
|
||||||
)
|
)
|
||||||
|
|
||||||
res = []
|
res = []
|
||||||
for fe in q.all():
|
for fe in q.all():
|
||||||
res.append(serialize_forward_email(fe))
|
res.append(serialize_contact(fe))
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
@ -257,12 +268,12 @@ def get_alias_contacts_route(alias_id):
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
return jsonify(error="page_id must be provided in request query"), 400
|
return jsonify(error="page_id must be provided in request query"), 400
|
||||||
|
|
||||||
gen_email: GenEmail = GenEmail.get(alias_id)
|
alias: Alias = Alias.get(alias_id)
|
||||||
|
|
||||||
if gen_email.user_id != user.id:
|
if alias.user_id != user.id:
|
||||||
return jsonify(error="Forbidden"), 403
|
return jsonify(error="Forbidden"), 403
|
||||||
|
|
||||||
contacts = get_alias_contacts(gen_email, page_id)
|
contacts = get_alias_contacts(alias, page_id)
|
||||||
|
|
||||||
return jsonify(contacts=contacts), 200
|
return jsonify(contacts=contacts), 200
|
||||||
|
|
||||||
|
@ -287,9 +298,9 @@ def create_contact_route(alias_id):
|
||||||
return jsonify(error="request body cannot be empty"), 400
|
return jsonify(error="request body cannot be empty"), 400
|
||||||
|
|
||||||
user = g.user
|
user = g.user
|
||||||
gen_email: GenEmail = GenEmail.get(alias_id)
|
alias: Alias = Alias.get(alias_id)
|
||||||
|
|
||||||
if gen_email.user_id != user.id:
|
if alias.user_id != user.id:
|
||||||
return jsonify(error="Forbidden"), 403
|
return jsonify(error="Forbidden"), 403
|
||||||
|
|
||||||
contact_email = data.get("contact")
|
contact_email = data.get("contact")
|
||||||
|
@ -299,23 +310,48 @@ def create_contact_route(alias_id):
|
||||||
reply_email = f"ra+{random_string(25)}@{EMAIL_DOMAIN}"
|
reply_email = f"ra+{random_string(25)}@{EMAIL_DOMAIN}"
|
||||||
for _ in range(1000):
|
for _ in range(1000):
|
||||||
reply_email = f"ra+{random_string(25)}@{EMAIL_DOMAIN}"
|
reply_email = f"ra+{random_string(25)}@{EMAIL_DOMAIN}"
|
||||||
if not ForwardEmail.get_by(reply_email=reply_email):
|
if not Contact.get_by(reply_email=reply_email):
|
||||||
break
|
break
|
||||||
|
|
||||||
_, website_email = parseaddr(contact_email)
|
_, website_email = parseaddr(contact_email)
|
||||||
|
|
||||||
# already been added
|
# already been added
|
||||||
if ForwardEmail.get_by(gen_email_id=gen_email.id, website_email=website_email):
|
if Contact.get_by(alias_id=alias.id, website_email=website_email):
|
||||||
return jsonify(error="Contact already added"), 409
|
return jsonify(error="Contact already added"), 409
|
||||||
|
|
||||||
forward_email = ForwardEmail.create(
|
contact = Contact.create(
|
||||||
gen_email_id=gen_email.id,
|
alias_id=alias.id,
|
||||||
website_email=website_email,
|
website_email=website_email,
|
||||||
website_from=contact_email,
|
website_from=contact_email,
|
||||||
reply_email=reply_email,
|
reply_email=reply_email,
|
||||||
)
|
)
|
||||||
|
|
||||||
LOG.d("create reverse-alias for %s %s", contact_email, gen_email)
|
LOG.d("create reverse-alias for %s %s", contact_email, alias)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify(**serialize_forward_email(forward_email)), 201
|
return jsonify(**serialize_contact(contact)), 201
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/contacts/<int:contact_id>", methods=["DELETE"])
|
||||||
|
@cross_origin()
|
||||||
|
@verify_api_key
|
||||||
|
def delete_contact(contact_id):
|
||||||
|
"""
|
||||||
|
Delete contact
|
||||||
|
Input:
|
||||||
|
contact_id: in url
|
||||||
|
Output:
|
||||||
|
200
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
user = g.user
|
||||||
|
contact = Contact.get(contact_id)
|
||||||
|
|
||||||
|
if not contact or contact.alias.user_id != user.id:
|
||||||
|
return jsonify(error="Forbidden"), 403
|
||||||
|
|
||||||
|
Contact.delete(contact_id)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify(deleted=True), 200
|
||||||
|
|
|
@ -6,7 +6,7 @@ from app.api.base import api_bp, verify_api_key
|
||||||
from app.config import ALIAS_DOMAINS, DISABLE_ALIAS_SUFFIX
|
from app.config import ALIAS_DOMAINS, DISABLE_ALIAS_SUFFIX
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.log import LOG
|
from app.log import LOG
|
||||||
from app.models import AliasUsedOn, GenEmail, User
|
from app.models import AliasUsedOn, Alias, User
|
||||||
from app.utils import convert_to_id, random_word
|
from app.utils import convert_to_id, random_word
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ def options():
|
||||||
hostname = request.args.get("hostname")
|
hostname = request.args.get("hostname")
|
||||||
|
|
||||||
ret = {
|
ret = {
|
||||||
"existing": [ge.email for ge in GenEmail.query.filter_by(user_id=user.id)],
|
"existing": [ge.email for ge in Alias.query.filter_by(user_id=user.id)],
|
||||||
"can_create_custom": user.can_create_new_alias(),
|
"can_create_custom": user.can_create_new_alias(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,10 +39,10 @@ def options():
|
||||||
if hostname:
|
if hostname:
|
||||||
# put the latest used alias first
|
# put the latest used alias first
|
||||||
q = (
|
q = (
|
||||||
db.session.query(AliasUsedOn, GenEmail, User)
|
db.session.query(AliasUsedOn, Alias, User)
|
||||||
.filter(
|
.filter(
|
||||||
AliasUsedOn.gen_email_id == GenEmail.id,
|
AliasUsedOn.alias_id == Alias.id,
|
||||||
GenEmail.user_id == user.id,
|
Alias.user_id == user.id,
|
||||||
AliasUsedOn.hostname == hostname,
|
AliasUsedOn.hostname == hostname,
|
||||||
)
|
)
|
||||||
.order_by(desc(AliasUsedOn.created_at))
|
.order_by(desc(AliasUsedOn.created_at))
|
||||||
|
@ -111,7 +111,7 @@ def options_v2():
|
||||||
|
|
||||||
ret = {
|
ret = {
|
||||||
"existing": [
|
"existing": [
|
||||||
ge.email for ge in GenEmail.query.filter_by(user_id=user.id, enabled=True)
|
ge.email for ge in Alias.query.filter_by(user_id=user.id, enabled=True)
|
||||||
],
|
],
|
||||||
"can_create": user.can_create_new_alias(),
|
"can_create": user.can_create_new_alias(),
|
||||||
"suffixes": [],
|
"suffixes": [],
|
||||||
|
@ -122,10 +122,10 @@ def options_v2():
|
||||||
if hostname:
|
if hostname:
|
||||||
# put the latest used alias first
|
# put the latest used alias first
|
||||||
q = (
|
q = (
|
||||||
db.session.query(AliasUsedOn, GenEmail, User)
|
db.session.query(AliasUsedOn, Alias, User)
|
||||||
.filter(
|
.filter(
|
||||||
AliasUsedOn.gen_email_id == GenEmail.id,
|
AliasUsedOn.alias_id == Alias.id,
|
||||||
GenEmail.user_id == user.id,
|
Alias.user_id == user.id,
|
||||||
AliasUsedOn.hostname == hostname,
|
AliasUsedOn.hostname == hostname,
|
||||||
)
|
)
|
||||||
.order_by(desc(AliasUsedOn.created_at))
|
.order_by(desc(AliasUsedOn.created_at))
|
||||||
|
|
|
@ -10,6 +10,7 @@ from itsdangerous import Signer
|
||||||
from app import email_utils
|
from app import email_utils
|
||||||
from app.api.base import api_bp
|
from app.api.base import api_bp
|
||||||
from app.config import FLASK_SECRET, DISABLE_REGISTRATION
|
from app.config import FLASK_SECRET, DISABLE_REGISTRATION
|
||||||
|
from app.dashboard.views.setting import send_reset_password_email
|
||||||
from app.email_utils import (
|
from app.email_utils import (
|
||||||
can_be_used_as_personal_email,
|
can_be_used_as_personal_email,
|
||||||
email_already_used,
|
email_already_used,
|
||||||
|
@ -316,3 +317,29 @@ def auth_payload(user, device) -> dict:
|
||||||
ret["api_key"] = api_key.code
|
ret["api_key"] = api_key.code
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/auth/forgot_password", methods=["POST"])
|
||||||
|
@cross_origin()
|
||||||
|
def forgot_password():
|
||||||
|
"""
|
||||||
|
User forgot password
|
||||||
|
Input:
|
||||||
|
email
|
||||||
|
Output:
|
||||||
|
200 and a reset password email is sent to user
|
||||||
|
400 if email not exist
|
||||||
|
|
||||||
|
"""
|
||||||
|
data = request.get_json()
|
||||||
|
if not data or not data.get("email"):
|
||||||
|
return jsonify(error="request body must contain email"), 400
|
||||||
|
|
||||||
|
email = data.get("email").lower()
|
||||||
|
|
||||||
|
user = User.get_by(email=email)
|
||||||
|
|
||||||
|
if user:
|
||||||
|
send_reset_password_email(user)
|
||||||
|
|
||||||
|
return jsonify(ok=True)
|
||||||
|
|
|
@ -7,7 +7,7 @@ from app.config import MAX_NB_EMAIL_FREE_PLAN, ALIAS_DOMAINS
|
||||||
from app.dashboard.views.custom_alias import verify_prefix_suffix
|
from app.dashboard.views.custom_alias import verify_prefix_suffix
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.log import LOG
|
from app.log import LOG
|
||||||
from app.models import GenEmail, AliasUsedOn, User, CustomDomain
|
from app.models import Alias, AliasUsedOn, User, CustomDomain
|
||||||
from app.utils import convert_to_id
|
from app.utils import convert_to_id
|
||||||
|
|
||||||
|
|
||||||
|
@ -54,11 +54,11 @@ def new_custom_alias():
|
||||||
return jsonify(error="wrong alias prefix or suffix"), 400
|
return jsonify(error="wrong alias prefix or suffix"), 400
|
||||||
|
|
||||||
full_alias = alias_prefix + alias_suffix
|
full_alias = alias_prefix + alias_suffix
|
||||||
if GenEmail.get_by(email=full_alias):
|
if Alias.get_by(email=full_alias):
|
||||||
LOG.d("full alias already used %s", full_alias)
|
LOG.d("full alias already used %s", full_alias)
|
||||||
return jsonify(error=f"alias {full_alias} already exists"), 409
|
return jsonify(error=f"alias {full_alias} already exists"), 409
|
||||||
|
|
||||||
gen_email = GenEmail.create(
|
alias = Alias.create(
|
||||||
user_id=user.id, email=full_alias, mailbox_id=user.default_mailbox_id, note=note
|
user_id=user.id, email=full_alias, mailbox_id=user.default_mailbox_id, note=note
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -67,12 +67,12 @@ def new_custom_alias():
|
||||||
if alias_domain not in ALIAS_DOMAINS:
|
if alias_domain not in ALIAS_DOMAINS:
|
||||||
domain = CustomDomain.get_by(domain=alias_domain)
|
domain = CustomDomain.get_by(domain=alias_domain)
|
||||||
LOG.d("set alias %s to domain %s", full_alias, domain)
|
LOG.d("set alias %s to domain %s", full_alias, domain)
|
||||||
gen_email.custom_domain_id = domain.id
|
alias.custom_domain_id = domain.id
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
if hostname:
|
if hostname:
|
||||||
AliasUsedOn.create(gen_email_id=gen_email.id, hostname=hostname)
|
AliasUsedOn.create(alias_id=alias.id, hostname=hostname)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify(alias=full_alias), 201
|
return jsonify(alias=full_alias), 201
|
||||||
|
|
|
@ -6,7 +6,7 @@ from app.api.base import api_bp, verify_api_key
|
||||||
from app.config import MAX_NB_EMAIL_FREE_PLAN
|
from app.config import MAX_NB_EMAIL_FREE_PLAN
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.log import LOG
|
from app.log import LOG
|
||||||
from app.models import GenEmail, AliasUsedOn, AliasGeneratorEnum
|
from app.models import Alias, AliasUsedOn, AliasGeneratorEnum
|
||||||
|
|
||||||
|
|
||||||
@api_bp.route("/alias/random/new", methods=["POST"])
|
@api_bp.route("/alias/random/new", methods=["POST"])
|
||||||
|
@ -47,12 +47,12 @@ def new_random_alias():
|
||||||
else:
|
else:
|
||||||
return jsonify(error=f"{mode} must be either word or alias"), 400
|
return jsonify(error=f"{mode} must be either word or alias"), 400
|
||||||
|
|
||||||
gen_email = GenEmail.create_new_random(user=user, scheme=scheme, note=note)
|
alias = Alias.create_new_random(user=user, scheme=scheme, note=note)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
hostname = request.args.get("hostname")
|
hostname = request.args.get("hostname")
|
||||||
if hostname:
|
if hostname:
|
||||||
AliasUsedOn.create(gen_email_id=gen_email.id, hostname=hostname)
|
AliasUsedOn.create(alias_id=alias.id, hostname=hostname)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify(alias=gen_email.email), 201
|
return jsonify(alias=alias.email), 201
|
||||||
|
|
|
@ -1,13 +1,7 @@
|
||||||
from flask import jsonify, request, g
|
from flask import jsonify, g
|
||||||
from flask_cors import cross_origin
|
from flask_cors import cross_origin
|
||||||
from sqlalchemy import desc
|
|
||||||
|
|
||||||
from app.api.base import api_bp, verify_api_key
|
from app.api.base import api_bp, verify_api_key
|
||||||
from app.config import EMAIL_DOMAIN
|
|
||||||
from app.extensions import db
|
|
||||||
from app.log import LOG
|
|
||||||
from app.models import AliasUsedOn, GenEmail, User
|
|
||||||
from app.utils import convert_to_id, random_word
|
|
||||||
|
|
||||||
|
|
||||||
@api_bp.route("/user_info")
|
@api_bp.route("/user_info")
|
||||||
|
@ -19,4 +13,11 @@ def user_info():
|
||||||
"""
|
"""
|
||||||
user = g.user
|
user = g.user
|
||||||
|
|
||||||
return jsonify({"name": user.name, "is_premium": user.is_premium()})
|
return jsonify(
|
||||||
|
{
|
||||||
|
"name": user.name,
|
||||||
|
"is_premium": user.is_premium(),
|
||||||
|
"email": user.email,
|
||||||
|
"in_trial": user.in_trial(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
{% block default_content %}
|
{% block default_content %}
|
||||||
<div class="page-header row">
|
<div class="page-header row">
|
||||||
<h3 class="page-title col">
|
<h3 class="page-title col">
|
||||||
{{ alias }}
|
{{ alias.email }}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -26,8 +26,8 @@
|
||||||
This might sound complicated but trust us, only the first time is a bit awkward.
|
This might sound complicated but trust us, only the first time is a bit awkward.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{% if gen_email.mailbox_id %}
|
{% if alias.mailbox_id %}
|
||||||
Make sure you send the email from the mailbox <b>{{ gen_email.mailbox.email }}</b>.
|
Make sure you send the email from the mailbox <b>{{ alias.mailbox.email }}</b>.
|
||||||
This is because only the mailbox that owns the alias can send emails from it.
|
This is because only the mailbox that owns the alias can send emails from it.
|
||||||
{% else %}
|
{% else %}
|
||||||
Make sure you send the email from your personal email address ({{ current_user.email }}).
|
Make sure you send the email from your personal email address ({{ current_user.email }}).
|
||||||
|
@ -47,33 +47,33 @@
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{% for forward_email in forward_emails %}
|
{% for contact in contacts %}
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="my-2 p-2 card {% if forward_email.id == forward_email_id %} highlight-row {% endif %}">
|
<div class="my-2 p-2 card {% if contact.id == contact_id %} highlight-row {% endif %}">
|
||||||
<div>
|
<div>
|
||||||
<span>
|
<span>
|
||||||
<a href="{{ 'mailto:' + forward_email.website_send_to() }}"
|
<a href="{{ 'mailto:' + contact.website_send_to() }}"
|
||||||
data-toggle="tooltip"
|
data-toggle="tooltip"
|
||||||
title="You can click on this to open your email client. Or use the copy button 👉"
|
title="You can click on this to open your email client. Or use the copy button 👉"
|
||||||
class="font-weight-bold">*************************</a>
|
class="font-weight-bold">*************************</a>
|
||||||
|
|
||||||
<span class="clipboard btn btn-sm btn-success copy-btn" data-toggle="tooltip"
|
<span class="clipboard btn btn-sm btn-success copy-btn" data-toggle="tooltip"
|
||||||
title="Copy to clipboard"
|
title="Copy to clipboard"
|
||||||
data-clipboard-text="{{ forward_email.website_send_to() }}">
|
data-clipboard-text="{{ contact.website_send_to() }}">
|
||||||
Copy reverse-alias
|
Copy reverse-alias
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<i class="fe fe-mail"></i> ➡ {{ forward_email.website_from or forward_email.website_email }}
|
<i class="fe fe-mail"></i> ➡ {{ contact.website_from or contact.website_email }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-2 text-muted small-text">
|
<div class="mb-2 text-muted small-text">
|
||||||
Created {{ forward_email.created_at | dt }} <br>
|
Created {{ contact.created_at | dt }} <br>
|
||||||
|
|
||||||
{% if forward_email.last_reply() %}
|
{% if contact.last_reply() %}
|
||||||
{% set email_log = forward_email.last_reply() %}
|
{% set email_log = contact.last_reply() %}
|
||||||
Last email sent {{ email_log.created_at | dt }}
|
Last email sent {{ email_log.created_at | dt }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -81,7 +81,7 @@
|
||||||
<div>
|
<div>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<input type="hidden" name="form-name" value="delete">
|
<input type="hidden" name="form-name" value="delete">
|
||||||
<input type="hidden" name="forward-email-id" value="{{ forward_email.id }}">
|
<input type="hidden" name="contact-id" value="{{ contact.id }}">
|
||||||
<span class="card-link btn btn-link float-right delete-forward-email">
|
<span class="card-link btn btn-link float-right delete-forward-email">
|
||||||
Delete
|
Delete
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -70,7 +70,7 @@
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<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 delete-api-key">
|
<span class="card-link btn btn-link float-right text-danger delete-api-key">
|
||||||
Delete
|
Delete
|
||||||
</span>
|
</span>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -72,7 +72,7 @@
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{% for alias_info in aliases %}
|
{% for alias_info in aliases %}
|
||||||
{% set gen_email = alias_info.gen_email %}
|
{% set alias = alias_info.alias %}
|
||||||
|
|
||||||
<div class="col-12 col-lg-6">
|
<div class="col-12 col-lg-6">
|
||||||
<div class="card p-4 shadow-sm {% if alias_info.highlight %} highlight-row {% endif %} ">
|
<div class="card p-4 shadow-sm {% if alias_info.highlight %} highlight-row {% endif %} ">
|
||||||
|
@ -86,14 +86,14 @@
|
||||||
use it whenever possible, for example when signing up for a newsletter or creating a new account on a suspicious website 😎"
|
use it whenever possible, for example when signing up for a newsletter or creating a new account on a suspicious website 😎"
|
||||||
data-step="2"
|
data-step="2"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if gen_email.enabled %}
|
{% if alias.enabled %}
|
||||||
data-toggle="tooltip"
|
data-toggle="tooltip"
|
||||||
title="Copy to clipboard"
|
title="Copy to clipboard"
|
||||||
data-clipboard-text="{{ gen_email.email }}"
|
data-clipboard-text="{{ alias.email }}"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
>
|
>
|
||||||
<span class="font-weight-bold">{{ gen_email.email }}</span>
|
<span class="font-weight-bold">{{ alias.email }}</span>
|
||||||
{% if gen_email.enabled %}
|
{% if alias.enabled %}
|
||||||
<span class="btn btn-sm btn-success copy-btn">
|
<span class="btn btn-sm btn-success copy-btn">
|
||||||
Copy
|
Copy
|
||||||
</span>
|
</span>
|
||||||
|
@ -103,10 +103,10 @@
|
||||||
<div class="col text-right">
|
<div class="col text-right">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<input type="hidden" name="form-name" value="switch-email-forwarding">
|
<input type="hidden" name="form-name" value="switch-email-forwarding">
|
||||||
<input type="hidden" name="gen-email-id" value="{{ gen_email.id }}">
|
<input type="hidden" name="alias-id" value="{{ alias.id }}">
|
||||||
<label class="custom-switch cursor"
|
<label class="custom-switch cursor"
|
||||||
data-toggle="tooltip"
|
data-toggle="tooltip"
|
||||||
{% if gen_email.enabled %}
|
{% if alias.enabled %}
|
||||||
title="Block Alias"
|
title="Block Alias"
|
||||||
{% else %}
|
{% else %}
|
||||||
title="Unblock Alias"
|
title="Unblock Alias"
|
||||||
|
@ -122,9 +122,9 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
style="padding-left: 0px"
|
style="padding-left: 0px"
|
||||||
>
|
>
|
||||||
<input type="hidden" name="alias" class="alias" value="{{ gen_email.email }}">
|
<input type="hidden" name="alias" class="alias" value="{{ alias.email }}">
|
||||||
<input type="checkbox" class="custom-switch-input"
|
<input type="checkbox" class="custom-switch-input"
|
||||||
{{ "checked" if gen_email.enabled else "" }}>
|
{{ "checked" if alias.enabled else "" }}>
|
||||||
|
|
||||||
<span class="custom-switch-indicator"></span>
|
<span class="custom-switch-indicator"></span>
|
||||||
</label>
|
</label>
|
||||||
|
@ -135,7 +135,7 @@
|
||||||
<hr class="my-2">
|
<hr class="my-2">
|
||||||
|
|
||||||
<p class="small-text">
|
<p class="small-text">
|
||||||
Created {{ gen_email.created_at | dt }}
|
Created {{ alias.created_at | dt }}
|
||||||
{% if alias_info.highlight %}
|
{% if alias_info.highlight %}
|
||||||
- <span class="font-weight-bold text-success small-text">New</span>
|
- <span class="font-weight-bold text-success small-text">New</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -145,7 +145,7 @@
|
||||||
<span class="alias-activity">{{ alias_info.nb_forward }}</span> forwards,
|
<span class="alias-activity">{{ alias_info.nb_forward }}</span> forwards,
|
||||||
<span class="alias-activity">{{ alias_info.nb_blocked }}</span> blocks,
|
<span class="alias-activity">{{ alias_info.nb_blocked }}</span> blocks,
|
||||||
<span class="alias-activity">{{ alias_info.nb_reply }}</span> replies
|
<span class="alias-activity">{{ alias_info.nb_reply }}</span> replies
|
||||||
<a href="{{ url_for('dashboard.alias_log', alias_id=gen_email.id) }}"
|
<a href="{{ url_for('dashboard.alias_log', alias_id=alias.id) }}"
|
||||||
class="btn btn-sm btn-link">
|
class="btn btn-sm btn-link">
|
||||||
See All Activity →
|
See All Activity →
|
||||||
</a>
|
</a>
|
||||||
|
@ -167,7 +167,7 @@
|
||||||
|
|
||||||
<div class="">
|
<div class="">
|
||||||
<input type="hidden" name="form-name" value="set-mailbox">
|
<input type="hidden" name="form-name" value="set-mailbox">
|
||||||
<input type="hidden" name="gen-email-id" value="{{ gen_email.id }}">
|
<input type="hidden" name="alias-id" value="{{ alias.id }}">
|
||||||
|
|
||||||
<button class="btn btn-sm btn-outline-info w-100">
|
<button class="btn btn-sm btn-outline-info w-100">
|
||||||
Update
|
Update
|
||||||
|
@ -191,12 +191,12 @@
|
||||||
name="note"
|
name="note"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
rows="2"
|
rows="2"
|
||||||
placeholder="Alias Note.">{{ gen_email.note or "" }}</textarea>
|
placeholder="Alias Note.">{{ alias.note or "" }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="">
|
<div class="">
|
||||||
<input type="hidden" name="form-name" value="set-note">
|
<input type="hidden" name="form-name" value="set-note">
|
||||||
<input type="hidden" name="gen-email-id" value="{{ gen_email.id }}">
|
<input type="hidden" name="alias-id" value="{{ alias.id }}">
|
||||||
|
|
||||||
<button class="btn btn-sm btn-outline-success w-100">
|
<button class="btn btn-sm btn-outline-success w-100">
|
||||||
Save
|
Save
|
||||||
|
@ -207,8 +207,8 @@
|
||||||
|
|
||||||
<div class="row mt-3">
|
<div class="row mt-3">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
{% if gen_email.enabled %}
|
{% if alias.enabled %}
|
||||||
<a href="{{ url_for('dashboard.alias_contact_manager', alias_id=gen_email.id) }}"
|
<a href="{{ url_for('dashboard.alias_contact_manager', alias_id=alias.id) }}"
|
||||||
{% if alias_info.show_intro_test_send_email %}
|
{% if alias_info.show_intro_test_send_email %}
|
||||||
data-intro="Not only alias can receive emails, it can <em>send</em> emails too! <br><br>
|
data-intro="Not only alias can receive emails, it can <em>send</em> emails too! <br><br>
|
||||||
You can add a new <em>contact</em> to for your alias here. <br><br>
|
You can add a new <em>contact</em> to for your alias here. <br><br>
|
||||||
|
@ -228,8 +228,8 @@
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<input type="hidden" name="form-name" value="delete-email">
|
<input type="hidden" name="form-name" value="delete-email">
|
||||||
<input type="hidden" name="gen-email-id" value="{{ gen_email.id }}">
|
<input type="hidden" name="alias-id" value="{{ alias.id }}">
|
||||||
<input type="hidden" name="alias" class="alias" value="{{ gen_email.email }}">
|
<input type="hidden" name="alias" class="alias" value="{{ alias.email }}">
|
||||||
|
|
||||||
<span class="delete-email btn btn-link btn-sm float-right text-danger">
|
<span class="delete-email btn btn-link btn-sm float-right text-danger">
|
||||||
Delete <i class="dropdown-icon fe fe-trash-2 text-danger"></i>
|
Delete <i class="dropdown-icon fe fe-trash-2 text-danger"></i>
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
{% for fel in fels %}
|
{% for fel in fels %}
|
||||||
{% set refused_email = fel.refused_email %}
|
{% set refused_email = fel.refused_email %}
|
||||||
{% set forward = fel.forward %}
|
{% set forward = fel.forward %}
|
||||||
{% set gen_email = forward.gen_email %}
|
{% set alias = forward.alias %}
|
||||||
|
|
||||||
<div class="card p-4 shadow-sm {% if fel.id == highlight_fel_id %} highlight-row {% endif %}">
|
<div class="card p-4 shadow-sm {% if fel.id == highlight_fel_id %} highlight-row {% endif %}">
|
||||||
<div class="small-text">
|
<div class="small-text">
|
||||||
|
@ -41,8 +41,8 @@
|
||||||
From: {{ forward.website_from or forward.website_email }} <br>
|
From: {{ forward.website_from or forward.website_email }} <br>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
To: {{ gen_email.email }}
|
To: {{ alias.email }}
|
||||||
<a href='{{ url_for("dashboard.index", highlight_gen_email_id=gen_email.id) }}'
|
<a href='{{ url_for("dashboard.index", highlight_alias_id=alias.id) }}'
|
||||||
class="btn btn-sm btn-outline-danger">Disable Alias</a>
|
class="btn btn-sm btn-outline-danger">Disable Alias</a>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ from app.config import EMAIL_DOMAIN
|
||||||
from app.dashboard.base import dashboard_bp
|
from app.dashboard.base import dashboard_bp
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.log import LOG
|
from app.log import LOG
|
||||||
from app.models import GenEmail, ForwardEmail
|
from app.models import Alias, Contact
|
||||||
from app.utils import random_string
|
from app.utils import random_string
|
||||||
|
|
||||||
|
|
||||||
|
@ -47,18 +47,18 @@ class NewContactForm(FlaskForm):
|
||||||
|
|
||||||
@dashboard_bp.route("/alias_contact_manager/<alias_id>/", methods=["GET", "POST"])
|
@dashboard_bp.route("/alias_contact_manager/<alias_id>/", methods=["GET", "POST"])
|
||||||
@dashboard_bp.route(
|
@dashboard_bp.route(
|
||||||
"/alias_contact_manager/<alias_id>/<int:forward_email_id>", methods=["GET", "POST"]
|
"/alias_contact_manager/<alias_id>/<contact_id>", methods=["GET", "POST"]
|
||||||
)
|
)
|
||||||
@login_required
|
@login_required
|
||||||
def alias_contact_manager(alias_id, forward_email_id=None):
|
def alias_contact_manager(alias_id, contact_id=None):
|
||||||
gen_email = GenEmail.get(alias_id)
|
alias = Alias.get(alias_id)
|
||||||
|
|
||||||
# sanity check
|
# sanity check
|
||||||
if not gen_email:
|
if not alias:
|
||||||
flash("You do not have access to this page", "warning")
|
flash("You do not have access to this page", "warning")
|
||||||
return redirect(url_for("dashboard.index"))
|
return redirect(url_for("dashboard.index"))
|
||||||
|
|
||||||
if gen_email.user_id != current_user.id:
|
if alias.user_id != current_user.id:
|
||||||
flash("You do not have access to this page", "warning")
|
flash("You do not have access to this page", "warning")
|
||||||
return redirect(url_for("dashboard.index"))
|
return redirect(url_for("dashboard.index"))
|
||||||
|
|
||||||
|
@ -71,24 +71,23 @@ def alias_contact_manager(alias_id, forward_email_id=None):
|
||||||
|
|
||||||
# generate a reply_email, make sure it is unique
|
# generate a reply_email, make sure it is unique
|
||||||
# not use while to avoid infinite loop
|
# not use while to avoid infinite loop
|
||||||
|
reply_email = f"ra+{random_string(25)}@{EMAIL_DOMAIN}"
|
||||||
for _ in range(1000):
|
for _ in range(1000):
|
||||||
reply_email = f"ra+{random_string(25)}@{EMAIL_DOMAIN}"
|
reply_email = f"ra+{random_string(25)}@{EMAIL_DOMAIN}"
|
||||||
if not ForwardEmail.get_by(reply_email=reply_email):
|
if not Contact.get_by(reply_email=reply_email):
|
||||||
break
|
break
|
||||||
|
|
||||||
_, website_email = parseaddr(contact_email)
|
_, website_email = parseaddr(contact_email)
|
||||||
|
|
||||||
# already been added
|
# already been added
|
||||||
if ForwardEmail.get_by(
|
if Contact.get_by(alias_id=alias.id, website_email=website_email):
|
||||||
gen_email_id=gen_email.id, website_email=website_email
|
|
||||||
):
|
|
||||||
flash(f"{website_email} is already added", "error")
|
flash(f"{website_email} is already added", "error")
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for("dashboard.alias_contact_manager", alias_id=alias_id)
|
url_for("dashboard.alias_contact_manager", alias_id=alias_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
forward_email = ForwardEmail.create(
|
contact = Contact.create(
|
||||||
gen_email_id=gen_email.id,
|
alias_id=alias.id,
|
||||||
website_email=website_email,
|
website_email=website_email,
|
||||||
website_from=contact_email,
|
website_from=contact_email,
|
||||||
reply_email=reply_email,
|
reply_email=reply_email,
|
||||||
|
@ -102,26 +101,26 @@ def alias_contact_manager(alias_id, forward_email_id=None):
|
||||||
url_for(
|
url_for(
|
||||||
"dashboard.alias_contact_manager",
|
"dashboard.alias_contact_manager",
|
||||||
alias_id=alias_id,
|
alias_id=alias_id,
|
||||||
forward_email_id=forward_email.id,
|
contact_id=contact.id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
elif request.form.get("form-name") == "delete":
|
elif request.form.get("form-name") == "delete":
|
||||||
forward_email_id = request.form.get("forward-email-id")
|
contact_id = request.form.get("contact-id")
|
||||||
forward_email = ForwardEmail.get(forward_email_id)
|
contact = Contact.get(contact_id)
|
||||||
|
|
||||||
if not forward_email:
|
if not contact:
|
||||||
flash("Unknown error. Refresh the page", "warning")
|
flash("Unknown error. Refresh the page", "warning")
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for("dashboard.alias_contact_manager", alias_id=alias_id)
|
url_for("dashboard.alias_contact_manager", alias_id=alias_id)
|
||||||
)
|
)
|
||||||
elif forward_email.gen_email_id != gen_email.id:
|
elif contact.alias_id != alias.id:
|
||||||
flash("You cannot delete reverse-alias", "warning")
|
flash("You cannot delete reverse-alias", "warning")
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for("dashboard.alias_contact_manager", alias_id=alias_id)
|
url_for("dashboard.alias_contact_manager", alias_id=alias_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
contact_name = forward_email.website_from
|
contact_name = contact.website_from
|
||||||
ForwardEmail.delete(forward_email_id)
|
Contact.delete(contact_id)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
flash(f"Reverse-alias for {contact_name} has been deleted", "success")
|
flash(f"Reverse-alias for {contact_name} has been deleted", "success")
|
||||||
|
@ -130,19 +129,16 @@ def alias_contact_manager(alias_id, forward_email_id=None):
|
||||||
url_for("dashboard.alias_contact_manager", alias_id=alias_id)
|
url_for("dashboard.alias_contact_manager", alias_id=alias_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
# make sure highlighted forward_email is at array start
|
# make sure highlighted contact is at array start
|
||||||
forward_emails = gen_email.forward_emails
|
contacts = alias.contacts
|
||||||
|
|
||||||
if forward_email_id:
|
if contact_id:
|
||||||
forward_emails = sorted(
|
contacts = sorted(contacts, key=lambda fe: fe.id == contact_id, reverse=True)
|
||||||
forward_emails, key=lambda fe: fe.id == forward_email_id, reverse=True
|
|
||||||
)
|
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"dashboard/alias_contact_manager.html",
|
"dashboard/alias_contact_manager.html",
|
||||||
forward_emails=forward_emails,
|
contacts=contacts,
|
||||||
alias=gen_email.email,
|
alias=alias,
|
||||||
gen_email=gen_email,
|
|
||||||
new_contact_form=new_contact_form,
|
new_contact_form=new_contact_form,
|
||||||
forward_email_id=forward_email_id,
|
contact_id=contact_id,
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,7 +5,7 @@ from flask_login import login_required, current_user
|
||||||
from app.config import PAGE_LIMIT
|
from app.config import PAGE_LIMIT
|
||||||
from app.dashboard.base import dashboard_bp
|
from app.dashboard.base import dashboard_bp
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models import GenEmail, ForwardEmailLog, ForwardEmail
|
from app.models import Alias, EmailLog, Contact
|
||||||
|
|
||||||
|
|
||||||
class AliasLog:
|
class AliasLog:
|
||||||
|
@ -29,31 +29,31 @@ class AliasLog:
|
||||||
@dashboard_bp.route("/alias_log/<int:alias_id>/<int:page_id>")
|
@dashboard_bp.route("/alias_log/<int:alias_id>/<int:page_id>")
|
||||||
@login_required
|
@login_required
|
||||||
def alias_log(alias_id, page_id):
|
def alias_log(alias_id, page_id):
|
||||||
gen_email = GenEmail.get(alias_id)
|
alias = Alias.get(alias_id)
|
||||||
|
|
||||||
# sanity check
|
# sanity check
|
||||||
if not gen_email:
|
if not alias:
|
||||||
flash("You do not have access to this page", "warning")
|
flash("You do not have access to this page", "warning")
|
||||||
return redirect(url_for("dashboard.index"))
|
return redirect(url_for("dashboard.index"))
|
||||||
|
|
||||||
if gen_email.user_id != current_user.id:
|
if alias.user_id != current_user.id:
|
||||||
flash("You do not have access to this page", "warning")
|
flash("You do not have access to this page", "warning")
|
||||||
return redirect(url_for("dashboard.index"))
|
return redirect(url_for("dashboard.index"))
|
||||||
|
|
||||||
logs = get_alias_log(gen_email, page_id)
|
logs = get_alias_log(alias, page_id)
|
||||||
base = (
|
base = (
|
||||||
db.session.query(ForwardEmail, ForwardEmailLog)
|
db.session.query(Contact, EmailLog)
|
||||||
.filter(ForwardEmail.id == ForwardEmailLog.forward_id)
|
.filter(Contact.id == EmailLog.contact_id)
|
||||||
.filter(ForwardEmail.gen_email_id == gen_email.id)
|
.filter(Contact.alias_id == alias.id)
|
||||||
)
|
)
|
||||||
total = base.count()
|
total = base.count()
|
||||||
email_forwarded = (
|
email_forwarded = (
|
||||||
base.filter(ForwardEmailLog.is_reply == False)
|
base.filter(EmailLog.is_reply == False)
|
||||||
.filter(ForwardEmailLog.blocked == False)
|
.filter(EmailLog.blocked == False)
|
||||||
.count()
|
.count()
|
||||||
)
|
)
|
||||||
email_replied = base.filter(ForwardEmailLog.is_reply == True).count()
|
email_replied = base.filter(EmailLog.is_reply == True).count()
|
||||||
email_blocked = base.filter(ForwardEmailLog.blocked == True).count()
|
email_blocked = base.filter(EmailLog.blocked == True).count()
|
||||||
last_page = (
|
last_page = (
|
||||||
len(logs) < PAGE_LIMIT
|
len(logs) < PAGE_LIMIT
|
||||||
) # lightweight pagination without counting all objects
|
) # lightweight pagination without counting all objects
|
||||||
|
@ -61,15 +61,15 @@ def alias_log(alias_id, page_id):
|
||||||
return render_template("dashboard/alias_log.html", **locals())
|
return render_template("dashboard/alias_log.html", **locals())
|
||||||
|
|
||||||
|
|
||||||
def get_alias_log(gen_email: GenEmail, page_id=0):
|
def get_alias_log(alias: Alias, page_id=0):
|
||||||
logs: [AliasLog] = []
|
logs: [AliasLog] = []
|
||||||
mailbox = gen_email.mailbox_email()
|
mailbox = alias.mailbox_email()
|
||||||
|
|
||||||
q = (
|
q = (
|
||||||
db.session.query(ForwardEmail, ForwardEmailLog)
|
db.session.query(Contact, EmailLog)
|
||||||
.filter(ForwardEmail.id == ForwardEmailLog.forward_id)
|
.filter(Contact.id == EmailLog.contact_id)
|
||||||
.filter(ForwardEmail.gen_email_id == gen_email.id)
|
.filter(Contact.alias_id == alias.id)
|
||||||
.order_by(ForwardEmailLog.id.desc())
|
.order_by(EmailLog.id.desc())
|
||||||
.limit(PAGE_LIMIT)
|
.limit(PAGE_LIMIT)
|
||||||
.offset(page_id * PAGE_LIMIT)
|
.offset(page_id * PAGE_LIMIT)
|
||||||
)
|
)
|
||||||
|
@ -78,7 +78,7 @@ def get_alias_log(gen_email: GenEmail, page_id=0):
|
||||||
al = AliasLog(
|
al = AliasLog(
|
||||||
website_email=fe.website_email,
|
website_email=fe.website_email,
|
||||||
website_from=fe.website_from,
|
website_from=fe.website_from,
|
||||||
alias=gen_email.email,
|
alias=alias.email,
|
||||||
when=fel.created_at,
|
when=fel.created_at,
|
||||||
is_reply=fel.is_reply,
|
is_reply=fel.is_reply,
|
||||||
blocked=fel.blocked,
|
blocked=fel.blocked,
|
||||||
|
|
|
@ -6,7 +6,7 @@ from app.dashboard.base import dashboard_bp
|
||||||
from app.email_utils import email_belongs_to_alias_domains, get_email_domain_part
|
from app.email_utils import email_belongs_to_alias_domains, get_email_domain_part
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.log import LOG
|
from app.log import LOG
|
||||||
from app.models import GenEmail, CustomDomain, DeletedAlias, Mailbox
|
from app.models import Alias, CustomDomain, DeletedAlias, Mailbox
|
||||||
from app.utils import convert_to_id, random_word, word_exist
|
from app.utils import convert_to_id, random_word, word_exist
|
||||||
|
|
||||||
|
|
||||||
|
@ -60,9 +60,7 @@ def custom_alias():
|
||||||
):
|
):
|
||||||
full_alias = alias_prefix + alias_suffix
|
full_alias = alias_prefix + alias_suffix
|
||||||
|
|
||||||
if GenEmail.get_by(email=full_alias) or DeletedAlias.get_by(
|
if Alias.get_by(email=full_alias) or DeletedAlias.get_by(email=full_alias):
|
||||||
email=full_alias
|
|
||||||
):
|
|
||||||
LOG.d("full alias already used %s", full_alias)
|
LOG.d("full alias already used %s", full_alias)
|
||||||
flash(
|
flash(
|
||||||
f"Alias {full_alias} already exists, please choose another one",
|
f"Alias {full_alias} already exists, please choose another one",
|
||||||
|
@ -71,7 +69,7 @@ def custom_alias():
|
||||||
else:
|
else:
|
||||||
mailbox = Mailbox.get_by(email=mailbox_email)
|
mailbox = Mailbox.get_by(email=mailbox_email)
|
||||||
|
|
||||||
gen_email = GenEmail.create(
|
alias = Alias.create(
|
||||||
user_id=current_user.id,
|
user_id=current_user.id,
|
||||||
email=full_alias,
|
email=full_alias,
|
||||||
note=alias_note,
|
note=alias_note,
|
||||||
|
@ -83,14 +81,12 @@ def custom_alias():
|
||||||
custom_domain = CustomDomain.get_by(domain=alias_domain)
|
custom_domain = CustomDomain.get_by(domain=alias_domain)
|
||||||
if custom_domain:
|
if custom_domain:
|
||||||
LOG.d("Set alias %s domain to %s", full_alias, custom_domain)
|
LOG.d("Set alias %s domain to %s", full_alias, custom_domain)
|
||||||
gen_email.custom_domain_id = custom_domain.id
|
alias.custom_domain_id = custom_domain.id
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(f"Alias {full_alias} has been created", "success")
|
flash(f"Alias {full_alias} has been created", "success")
|
||||||
|
|
||||||
return redirect(
|
return redirect(url_for("dashboard.index", highlight_alias_id=alias.id))
|
||||||
url_for("dashboard.index", highlight_gen_email_id=gen_email.id)
|
|
||||||
)
|
|
||||||
# only happen if the request has been "hacked"
|
# only happen if the request has been "hacked"
|
||||||
else:
|
else:
|
||||||
flash("something went wrong", "warning")
|
flash("something went wrong", "warning")
|
||||||
|
|
|
@ -10,7 +10,7 @@ from app.dns_utils import (
|
||||||
get_txt_record,
|
get_txt_record,
|
||||||
)
|
)
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models import CustomDomain, GenEmail
|
from app.models import CustomDomain, Alias
|
||||||
|
|
||||||
|
|
||||||
@dashboard_bp.route("/domains/<int:custom_domain_id>/dns", methods=["GET", "POST"])
|
@dashboard_bp.route("/domains/<int:custom_domain_id>/dns", methods=["GET", "POST"])
|
||||||
|
@ -129,6 +129,6 @@ def domain_detail(custom_domain_id):
|
||||||
|
|
||||||
return redirect(url_for("dashboard.custom_domain"))
|
return redirect(url_for("dashboard.custom_domain"))
|
||||||
|
|
||||||
nb_alias = GenEmail.filter_by(custom_domain_id=custom_domain.id).count()
|
nb_alias = Alias.filter_by(custom_domain_id=custom_domain.id).count()
|
||||||
|
|
||||||
return render_template("dashboard/domain_detail/info.html", **locals())
|
return render_template("dashboard/domain_detail/info.html", **locals())
|
||||||
|
|
|
@ -10,10 +10,10 @@ from app.dashboard.base import dashboard_bp
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.log import LOG
|
from app.log import LOG
|
||||||
from app.models import (
|
from app.models import (
|
||||||
GenEmail,
|
Alias,
|
||||||
ClientUser,
|
ClientUser,
|
||||||
ForwardEmail,
|
Contact,
|
||||||
ForwardEmailLog,
|
EmailLog,
|
||||||
DeletedAlias,
|
DeletedAlias,
|
||||||
AliasGeneratorEnum,
|
AliasGeneratorEnum,
|
||||||
Mailbox,
|
Mailbox,
|
||||||
|
@ -22,7 +22,7 @@ from app.models import (
|
||||||
|
|
||||||
class AliasInfo:
|
class AliasInfo:
|
||||||
id: int
|
id: int
|
||||||
gen_email: GenEmail
|
alias: Alias
|
||||||
mailbox: Mailbox
|
mailbox: Mailbox
|
||||||
nb_forward: int
|
nb_forward: int
|
||||||
nb_blocked: int
|
nb_blocked: int
|
||||||
|
@ -41,21 +41,21 @@ class AliasInfo:
|
||||||
@login_required
|
@login_required
|
||||||
def index():
|
def index():
|
||||||
query = request.args.get("query") or ""
|
query = request.args.get("query") or ""
|
||||||
highlight_gen_email_id = None
|
highlight_alias_id = None
|
||||||
if request.args.get("highlight_gen_email_id"):
|
if request.args.get("highlight_alias_id"):
|
||||||
highlight_gen_email_id = int(request.args.get("highlight_gen_email_id"))
|
highlight_alias_id = int(request.args.get("highlight_alias_id"))
|
||||||
|
|
||||||
# User generates a new email
|
# User generates a new email
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
if request.form.get("form-name") == "trigger-email":
|
if request.form.get("form-name") == "trigger-email":
|
||||||
gen_email_id = request.form.get("gen-email-id")
|
alias_id = request.form.get("alias-id")
|
||||||
gen_email = GenEmail.get(gen_email_id)
|
alias = Alias.get(alias_id)
|
||||||
|
|
||||||
LOG.d("trigger an email to %s", gen_email)
|
LOG.d("trigger an email to %s", alias)
|
||||||
email_utils.send_test_email_alias(gen_email.email, gen_email.user.name)
|
email_utils.send_test_email_alias(alias.email, alias.user.name)
|
||||||
|
|
||||||
flash(
|
flash(
|
||||||
f"An email sent to {gen_email.email} is on its way, please check your inbox/spam folder",
|
f"An email sent to {alias.email} is on its way, please check your inbox/spam folder",
|
||||||
"success",
|
"success",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -72,51 +72,47 @@ def index():
|
||||||
)
|
)
|
||||||
if not scheme or not AliasGeneratorEnum.has_value(scheme):
|
if not scheme or not AliasGeneratorEnum.has_value(scheme):
|
||||||
scheme = current_user.alias_generator
|
scheme = current_user.alias_generator
|
||||||
gen_email = GenEmail.create_new_random(user=current_user, scheme=scheme)
|
alias = Alias.create_new_random(user=current_user, scheme=scheme)
|
||||||
|
|
||||||
gen_email.mailbox_id = current_user.default_mailbox_id
|
alias.mailbox_id = current_user.default_mailbox_id
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
LOG.d("generate new email %s for user %s", gen_email, current_user)
|
LOG.d("generate new email %s for user %s", alias, current_user)
|
||||||
flash(f"Alias {gen_email.email} has been created", "success")
|
flash(f"Alias {alias.email} has been created", "success")
|
||||||
|
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for(
|
url_for(
|
||||||
"dashboard.index",
|
"dashboard.index", highlight_alias_id=alias.id, query=query,
|
||||||
highlight_gen_email_id=gen_email.id,
|
|
||||||
query=query,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
flash(f"You need to upgrade your plan to create new alias.", "warning")
|
flash(f"You need to upgrade your plan to create new alias.", "warning")
|
||||||
|
|
||||||
elif request.form.get("form-name") == "switch-email-forwarding":
|
elif request.form.get("form-name") == "switch-email-forwarding":
|
||||||
gen_email_id = request.form.get("gen-email-id")
|
alias_id = request.form.get("alias-id")
|
||||||
gen_email: GenEmail = GenEmail.get(gen_email_id)
|
alias: Alias = Alias.get(alias_id)
|
||||||
|
|
||||||
LOG.d("switch email forwarding for %s", gen_email)
|
LOG.d("switch email forwarding for %s", alias)
|
||||||
|
|
||||||
gen_email.enabled = not gen_email.enabled
|
alias.enabled = not alias.enabled
|
||||||
if gen_email.enabled:
|
if alias.enabled:
|
||||||
flash(f"Alias {gen_email.email} is enabled", "success")
|
flash(f"Alias {alias.email} is enabled", "success")
|
||||||
else:
|
else:
|
||||||
flash(f"Alias {gen_email.email} is disabled", "warning")
|
flash(f"Alias {alias.email} is disabled", "warning")
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for(
|
url_for("dashboard.index", highlight_alias_id=alias.id, query=query)
|
||||||
"dashboard.index", highlight_gen_email_id=gen_email.id, query=query
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
elif request.form.get("form-name") == "delete-email":
|
elif request.form.get("form-name") == "delete-email":
|
||||||
gen_email_id = request.form.get("gen-email-id")
|
alias_id = request.form.get("alias-id")
|
||||||
gen_email: GenEmail = GenEmail.get(gen_email_id)
|
alias: Alias = Alias.get(alias_id)
|
||||||
|
|
||||||
LOG.d("delete gen email %s", gen_email)
|
LOG.d("delete gen email %s", alias)
|
||||||
email = gen_email.email
|
email = alias.email
|
||||||
GenEmail.delete(gen_email.id)
|
Alias.delete(alias.id)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(f"Alias {email} has been deleted", "success")
|
flash(f"Alias {email} has been deleted", "success")
|
||||||
|
|
||||||
|
@ -130,42 +126,37 @@ def index():
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
|
|
||||||
elif request.form.get("form-name") == "set-note":
|
elif request.form.get("form-name") == "set-note":
|
||||||
gen_email_id = request.form.get("gen-email-id")
|
alias_id = request.form.get("alias-id")
|
||||||
gen_email: GenEmail = GenEmail.get(gen_email_id)
|
alias: Alias = Alias.get(alias_id)
|
||||||
note = request.form.get("note")
|
note = request.form.get("note")
|
||||||
|
|
||||||
gen_email.note = note
|
alias.note = note
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
flash(f"Update note for alias {gen_email.email}", "success")
|
flash(f"Update note for alias {alias.email}", "success")
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for(
|
url_for("dashboard.index", highlight_alias_id=alias.id, query=query)
|
||||||
"dashboard.index", highlight_gen_email_id=gen_email.id, query=query
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
elif request.form.get("form-name") == "set-mailbox":
|
elif request.form.get("form-name") == "set-mailbox":
|
||||||
gen_email_id = request.form.get("gen-email-id")
|
alias_id = request.form.get("alias-id")
|
||||||
gen_email: GenEmail = GenEmail.get(gen_email_id)
|
alias: Alias = Alias.get(alias_id)
|
||||||
mailbox_email = request.form.get("mailbox")
|
mailbox_email = request.form.get("mailbox")
|
||||||
|
|
||||||
mailbox = Mailbox.get_by(email=mailbox_email)
|
mailbox = Mailbox.get_by(email=mailbox_email)
|
||||||
if not mailbox or mailbox.user_id != current_user.id:
|
if not mailbox or mailbox.user_id != current_user.id:
|
||||||
flash("Something went wrong, please retry", "warning")
|
flash("Something went wrong, please retry", "warning")
|
||||||
else:
|
else:
|
||||||
gen_email.mailbox_id = mailbox.id
|
alias.mailbox_id = mailbox.id
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
LOG.d("Set alias %s mailbox to %s", gen_email, mailbox)
|
LOG.d("Set alias %s mailbox to %s", alias, mailbox)
|
||||||
|
|
||||||
flash(
|
flash(
|
||||||
f"Update mailbox for {gen_email.email} to {mailbox_email}",
|
f"Update mailbox for {alias.email} to {mailbox_email}", "success",
|
||||||
"success",
|
|
||||||
)
|
)
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for(
|
url_for(
|
||||||
"dashboard.index",
|
"dashboard.index", highlight_alias_id=alias.id, query=query,
|
||||||
highlight_gen_email_id=gen_email.id,
|
|
||||||
query=query,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -174,7 +165,7 @@ def index():
|
||||||
client_users = (
|
client_users = (
|
||||||
ClientUser.filter_by(user_id=current_user.id)
|
ClientUser.filter_by(user_id=current_user.id)
|
||||||
.options(joinedload(ClientUser.client))
|
.options(joinedload(ClientUser.client))
|
||||||
.options(joinedload(ClientUser.gen_email))
|
.options(joinedload(ClientUser.alias))
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -185,51 +176,93 @@ def index():
|
||||||
return render_template(
|
return render_template(
|
||||||
"dashboard/index.html",
|
"dashboard/index.html",
|
||||||
client_users=client_users,
|
client_users=client_users,
|
||||||
aliases=get_alias_info(current_user, query, highlight_gen_email_id),
|
aliases=get_alias_infos(current_user, query, highlight_alias_id),
|
||||||
highlight_gen_email_id=highlight_gen_email_id,
|
highlight_alias_id=highlight_alias_id,
|
||||||
query=query,
|
query=query,
|
||||||
AliasGeneratorEnum=AliasGeneratorEnum,
|
AliasGeneratorEnum=AliasGeneratorEnum,
|
||||||
mailboxes=mailboxes,
|
mailboxes=mailboxes,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_alias_info(
|
def get_alias_info(alias: Alias) -> AliasInfo:
|
||||||
user, query=None, highlight_gen_email_id=None, page_id=None
|
q = (
|
||||||
) -> [AliasInfo]:
|
db.session.query(Contact, EmailLog)
|
||||||
|
.filter(Contact.alias_id == alias.id)
|
||||||
|
.filter(EmailLog.contact_id == Contact.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
alias_info = AliasInfo(
|
||||||
|
id=alias.id,
|
||||||
|
alias=alias,
|
||||||
|
mailbox=alias.mailbox,
|
||||||
|
note=alias.note,
|
||||||
|
nb_blocked=0,
|
||||||
|
nb_forward=0,
|
||||||
|
nb_reply=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, el in q:
|
||||||
|
if el.is_reply:
|
||||||
|
alias_info.nb_reply += 1
|
||||||
|
elif el.blocked:
|
||||||
|
alias_info.nb_blocked += 1
|
||||||
|
else:
|
||||||
|
alias_info.nb_forward += 1
|
||||||
|
|
||||||
|
return alias_info
|
||||||
|
|
||||||
|
|
||||||
|
def get_alias_infos_with_pagination(user, page_id=0, query=None) -> [AliasInfo]:
|
||||||
|
ret = []
|
||||||
|
q = (
|
||||||
|
db.session.query(Alias)
|
||||||
|
.options(joinedload(Alias.mailbox))
|
||||||
|
.filter(Alias.user_id == user.id)
|
||||||
|
.order_by(Alias.created_at.desc())
|
||||||
|
)
|
||||||
|
|
||||||
|
if query:
|
||||||
|
q = q.filter(
|
||||||
|
or_(Alias.email.ilike(f"%{query}%"), Alias.note.ilike(f"%{query}%"))
|
||||||
|
)
|
||||||
|
|
||||||
|
q = q.limit(PAGE_LIMIT).offset(page_id * PAGE_LIMIT)
|
||||||
|
|
||||||
|
for alias in q:
|
||||||
|
ret.append(get_alias_info(alias))
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def get_alias_infos(user, query=None, highlight_alias_id=None) -> [AliasInfo]:
|
||||||
if query:
|
if query:
|
||||||
query = query.strip().lower()
|
query = query.strip().lower()
|
||||||
|
|
||||||
aliases = {} # dict of alias and AliasInfo
|
aliases = {} # dict of alias and AliasInfo
|
||||||
|
|
||||||
q = (
|
q = (
|
||||||
db.session.query(GenEmail, ForwardEmail, ForwardEmailLog, Mailbox)
|
db.session.query(Alias, Contact, EmailLog, Mailbox)
|
||||||
.join(ForwardEmail, GenEmail.id == ForwardEmail.gen_email_id, isouter=True)
|
.join(Contact, Alias.id == Contact.alias_id, isouter=True)
|
||||||
.join(
|
.join(EmailLog, Contact.id == EmailLog.contact_id, isouter=True)
|
||||||
ForwardEmailLog, ForwardEmail.id == ForwardEmailLog.forward_id, isouter=True
|
.join(Mailbox, Alias.mailbox_id == Mailbox.id, isouter=True)
|
||||||
)
|
.filter(Alias.user_id == user.id)
|
||||||
.join(Mailbox, GenEmail.mailbox_id == Mailbox.id, isouter=True)
|
.order_by(Alias.created_at.desc())
|
||||||
.filter(GenEmail.user_id == user.id)
|
|
||||||
.order_by(GenEmail.created_at.desc())
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if query:
|
if query:
|
||||||
q = q.filter(
|
q = q.filter(
|
||||||
or_(GenEmail.email.ilike(f"%{query}%"), GenEmail.note.ilike(f"%{query}%"))
|
or_(Alias.email.ilike(f"%{query}%"), Alias.note.ilike(f"%{query}%"))
|
||||||
)
|
)
|
||||||
|
|
||||||
# pagination activated
|
|
||||||
if page_id is not None:
|
|
||||||
q = q.limit(PAGE_LIMIT).offset(page_id * PAGE_LIMIT)
|
|
||||||
|
|
||||||
for ge, fe, fel, mb in q:
|
for ge, fe, fel, mb in q:
|
||||||
if ge.email not in aliases:
|
if ge.email not in aliases:
|
||||||
aliases[ge.email] = AliasInfo(
|
aliases[ge.email] = AliasInfo(
|
||||||
id=ge.id,
|
id=ge.id,
|
||||||
gen_email=ge,
|
alias=ge,
|
||||||
nb_blocked=0,
|
nb_blocked=0,
|
||||||
nb_forward=0,
|
nb_forward=0,
|
||||||
nb_reply=0,
|
nb_reply=0,
|
||||||
highlight=ge.id == highlight_gen_email_id,
|
highlight=ge.id == highlight_alias_id,
|
||||||
mailbox=mb,
|
mailbox=mb,
|
||||||
note=ge.note,
|
note=ge.note,
|
||||||
)
|
)
|
||||||
|
@ -259,7 +292,7 @@ def get_alias_info(
|
||||||
|
|
||||||
# only show intro on the first enabled alias
|
# only show intro on the first enabled alias
|
||||||
for alias in ret:
|
for alias in ret:
|
||||||
if alias.gen_email.enabled:
|
if alias.alias.enabled:
|
||||||
alias.show_intro_test_send_email = True
|
alias.show_intro_test_send_email = True
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ from app.email_utils import can_be_used_as_personal_email, email_already_used
|
||||||
from app.email_utils import mailbox_already_used, render, send_email
|
from app.email_utils import mailbox_already_used, render, send_email
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.log import LOG
|
from app.log import LOG
|
||||||
from app.models import GenEmail, DeletedAlias
|
from app.models import Alias, DeletedAlias
|
||||||
from app.models import Mailbox
|
from app.models import Mailbox
|
||||||
from app.pgp_utils import PGPException, load_public_key
|
from app.pgp_utils import PGPException, load_public_key
|
||||||
from smtplib import SMTPRecipientsRefused
|
from smtplib import SMTPRecipientsRefused
|
||||||
|
@ -49,7 +49,7 @@ def mailbox_detail_route(mailbox_id):
|
||||||
# check if this email is not already used
|
# check if this email is not already used
|
||||||
if (
|
if (
|
||||||
mailbox_already_used(new_email, current_user)
|
mailbox_already_used(new_email, current_user)
|
||||||
or GenEmail.get_by(email=new_email)
|
or Alias.get_by(email=new_email)
|
||||||
or DeletedAlias.get_by(email=new_email)
|
or DeletedAlias.get_by(email=new_email)
|
||||||
):
|
):
|
||||||
flash(f"Email {new_email} already used", "error")
|
flash(f"Email {new_email} already used", "error")
|
||||||
|
|
|
@ -2,7 +2,7 @@ from flask import render_template, request
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
|
|
||||||
from app.dashboard.base import dashboard_bp
|
from app.dashboard.base import dashboard_bp
|
||||||
from app.models import ForwardEmailLog
|
from app.models import EmailLog
|
||||||
|
|
||||||
|
|
||||||
@dashboard_bp.route("/refused_email", methods=["GET", "POST"])
|
@dashboard_bp.route("/refused_email", methods=["GET", "POST"])
|
||||||
|
@ -13,9 +13,7 @@ def refused_email_route():
|
||||||
if highlight_fel_id:
|
if highlight_fel_id:
|
||||||
highlight_fel_id = int(highlight_fel_id)
|
highlight_fel_id = int(highlight_fel_id)
|
||||||
|
|
||||||
fels: [ForwardEmailLog] = ForwardEmailLog.query.filter(
|
fels: [EmailLog] = EmailLog.query.filter(EmailLog.refused_email_id != None).all()
|
||||||
ForwardEmailLog.refused_email_id != None
|
|
||||||
).all()
|
|
||||||
|
|
||||||
# make sure the highlighted fel is the first fel
|
# make sure the highlighted fel is the first fel
|
||||||
highlight_index = None
|
highlight_index = None
|
||||||
|
|
|
@ -21,7 +21,7 @@ from app.models import (
|
||||||
ResetPasswordCode,
|
ResetPasswordCode,
|
||||||
EmailChange,
|
EmailChange,
|
||||||
User,
|
User,
|
||||||
GenEmail,
|
Alias,
|
||||||
DeletedAlias,
|
DeletedAlias,
|
||||||
CustomDomain,
|
CustomDomain,
|
||||||
Client,
|
Client,
|
||||||
|
@ -71,7 +71,7 @@ def setting():
|
||||||
# check if this email is not already used
|
# check if this email is not already used
|
||||||
if (
|
if (
|
||||||
email_already_used(new_email)
|
email_already_used(new_email)
|
||||||
or GenEmail.get_by(email=new_email)
|
or Alias.get_by(email=new_email)
|
||||||
or DeletedAlias.get_by(email=new_email)
|
or DeletedAlias.get_by(email=new_email)
|
||||||
):
|
):
|
||||||
flash(f"Email {new_email} already used", "error")
|
flash(f"Email {new_email} already used", "error")
|
||||||
|
@ -165,9 +165,7 @@ def setting():
|
||||||
"custom_domains": [],
|
"custom_domains": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
for alias in GenEmail.filter_by(
|
for alias in Alias.filter_by(user_id=current_user.id).all(): # type: Alias
|
||||||
user_id=current_user.id
|
|
||||||
).all(): # type: GenEmail
|
|
||||||
data["aliases"].append(dict(email=alias.email, enabled=alias.enabled))
|
data["aliases"].append(dict(email=alias.email, enabled=alias.enabled))
|
||||||
|
|
||||||
for custom_domain in CustomDomain.filter_by(user_id=current_user.id).all():
|
for custom_domain in CustomDomain.filter_by(user_id=current_user.id).all():
|
||||||
|
|
|
@ -7,18 +7,18 @@ from flask_login import login_required, current_user
|
||||||
|
|
||||||
from app.dashboard.base import dashboard_bp
|
from app.dashboard.base import dashboard_bp
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models import GenEmail
|
from app.models import Alias
|
||||||
|
|
||||||
|
|
||||||
@dashboard_bp.route("/unsubscribe/<gen_email_id>", methods=["GET", "POST"])
|
@dashboard_bp.route("/unsubscribe/<alias_id>", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def unsubscribe(gen_email_id):
|
def unsubscribe(alias_id):
|
||||||
gen_email = GenEmail.get(gen_email_id)
|
alias = Alias.get(alias_id)
|
||||||
if not gen_email:
|
if not alias:
|
||||||
flash("Incorrect link. Redirect you to the home page", "warning")
|
flash("Incorrect link. Redirect you to the home page", "warning")
|
||||||
return redirect(url_for("dashboard.index"))
|
return redirect(url_for("dashboard.index"))
|
||||||
|
|
||||||
if gen_email.user_id != current_user.id:
|
if alias.user_id != current_user.id:
|
||||||
flash(
|
flash(
|
||||||
"You don't have access to this page. Redirect you to the home page",
|
"You don't have access to this page. Redirect you to the home page",
|
||||||
"warning",
|
"warning",
|
||||||
|
@ -27,10 +27,10 @@ def unsubscribe(gen_email_id):
|
||||||
|
|
||||||
# automatic unsubscribe, according to https://tools.ietf.org/html/rfc8058
|
# automatic unsubscribe, according to https://tools.ietf.org/html/rfc8058
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
gen_email.enabled = False
|
alias.enabled = False
|
||||||
flash(f"Alias {gen_email.email} has been blocked", "success")
|
flash(f"Alias {alias.email} has been blocked", "success")
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return redirect(url_for("dashboard.index", highlight_gen_email_id=gen_email.id))
|
return redirect(url_for("dashboard.index", highlight_alias_id=alias.id))
|
||||||
else: # ask user confirmation
|
else: # ask user confirmation
|
||||||
return render_template("dashboard/unsubscribe.html", alias=gen_email.email)
|
return render_template("dashboard/unsubscribe.html", alias=alias.email)
|
||||||
|
|
|
@ -231,20 +231,20 @@ def send_email(
|
||||||
smtp.sendmail(SUPPORT_EMAIL, to_email, msg_raw)
|
smtp.sendmail(SUPPORT_EMAIL, to_email, msg_raw)
|
||||||
|
|
||||||
|
|
||||||
def get_email_local_part(email):
|
def get_email_local_part(address):
|
||||||
"""
|
"""
|
||||||
Get the local part from email
|
Get the local part from email
|
||||||
ab@cd.com -> ab
|
ab@cd.com -> ab
|
||||||
"""
|
"""
|
||||||
return email[: email.find("@")]
|
return address[: address.find("@")]
|
||||||
|
|
||||||
|
|
||||||
def get_email_domain_part(email):
|
def get_email_domain_part(address):
|
||||||
"""
|
"""
|
||||||
Get the domain part from email
|
Get the domain part from email
|
||||||
ab@cd.com -> cd.com
|
ab@cd.com -> cd.com
|
||||||
"""
|
"""
|
||||||
return email[email.find("@") + 1 :]
|
return address[address.find("@") + 1 :]
|
||||||
|
|
||||||
|
|
||||||
def add_dkim_signature(msg: Message, email_domain: str):
|
def add_dkim_signature(msg: Message, email_domain: str):
|
||||||
|
@ -292,10 +292,10 @@ def delete_all_headers_except(msg: Message, headers: [str]):
|
||||||
del msg._headers[i]
|
del msg._headers[i]
|
||||||
|
|
||||||
|
|
||||||
def email_belongs_to_alias_domains(email: str) -> bool:
|
def email_belongs_to_alias_domains(address: str) -> bool:
|
||||||
"""return True if an email ends with one of the alias domains provided by SimpleLogin"""
|
"""return True if an email ends with one of the alias domains provided by SimpleLogin"""
|
||||||
for domain in ALIAS_DOMAINS:
|
for domain in ALIAS_DOMAINS:
|
||||||
if email.endswith("@" + domain):
|
if address.endswith("@" + domain):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -159,7 +159,7 @@ class User(db.Model, ModelMixin, UserMixin):
|
||||||
user.default_mailbox_id = mb.id
|
user.default_mailbox_id = mb.id
|
||||||
|
|
||||||
# create a first alias mail to show user how to use when they login
|
# create a first alias mail to show user how to use when they login
|
||||||
GenEmail.create_new(user, prefix="my-first-alias", mailbox_id=mb.id)
|
Alias.create_new(user, prefix="my-first-alias", mailbox_id=mb.id)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
||||||
# Schedule onboarding emails
|
# Schedule onboarding emails
|
||||||
|
@ -246,7 +246,7 @@ class User(db.Model, ModelMixin, UserMixin):
|
||||||
if self.is_premium():
|
if self.is_premium():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return GenEmail.filter_by(user_id=self.id).count() < MAX_NB_EMAIL_FREE_PLAN
|
return Alias.filter_by(user_id=self.id).count() < MAX_NB_EMAIL_FREE_PLAN
|
||||||
|
|
||||||
def set_password(self, password):
|
def set_password(self, password):
|
||||||
salt = bcrypt.gensalt()
|
salt = bcrypt.gensalt()
|
||||||
|
@ -270,16 +270,16 @@ class User(db.Model, ModelMixin, UserMixin):
|
||||||
"""return suggested email and other email choices """
|
"""return suggested email and other email choices """
|
||||||
website_name = convert_to_id(website_name)
|
website_name = convert_to_id(website_name)
|
||||||
|
|
||||||
all_gen_emails = [ge.email for ge in GenEmail.filter_by(user_id=self.id)]
|
all_aliases = [ge.email for ge in Alias.filter_by(user_id=self.id)]
|
||||||
if self.can_create_new_alias():
|
if self.can_create_new_alias():
|
||||||
suggested_gen_email = GenEmail.create_new(self, prefix=website_name).email
|
suggested_alias = Alias.create_new(self, prefix=website_name).email
|
||||||
else:
|
else:
|
||||||
# pick an email from the list of gen emails
|
# pick an email from the list of gen emails
|
||||||
suggested_gen_email = random.choice(all_gen_emails)
|
suggested_alias = random.choice(all_aliases)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
suggested_gen_email,
|
suggested_alias,
|
||||||
list(set(all_gen_emails).difference({suggested_gen_email})),
|
list(set(all_aliases).difference({suggested_alias})),
|
||||||
)
|
)
|
||||||
|
|
||||||
def suggested_names(self) -> (str, [str]):
|
def suggested_names(self) -> (str, [str]):
|
||||||
|
@ -515,7 +515,7 @@ def generate_email(
|
||||||
random_email = random_words() + "@" + EMAIL_DOMAIN
|
random_email = random_words() + "@" + EMAIL_DOMAIN
|
||||||
|
|
||||||
# check that the client does not exist yet
|
# check that the client does not exist yet
|
||||||
if not GenEmail.get_by(email=random_email) and not DeletedAlias.get_by(
|
if not Alias.get_by(email=random_email) and not DeletedAlias.get_by(
|
||||||
email=random_email
|
email=random_email
|
||||||
):
|
):
|
||||||
LOG.debug("generate email %s", random_email)
|
LOG.debug("generate email %s", random_email)
|
||||||
|
@ -526,8 +526,8 @@ def generate_email(
|
||||||
return generate_email(scheme=scheme, in_hex=in_hex)
|
return generate_email(scheme=scheme, in_hex=in_hex)
|
||||||
|
|
||||||
|
|
||||||
class GenEmail(db.Model, ModelMixin):
|
class Alias(db.Model, ModelMixin):
|
||||||
"""Generated email"""
|
"""Alias"""
|
||||||
|
|
||||||
user_id = db.Column(db.ForeignKey(User.id, ondelete="cascade"), nullable=False)
|
user_id = db.Column(db.ForeignKey(User.id, ondelete="cascade"), nullable=False)
|
||||||
email = db.Column(db.String(128), unique=True, nullable=False)
|
email = db.Column(db.String(128), unique=True, nullable=False)
|
||||||
|
@ -571,7 +571,7 @@ class GenEmail(db.Model, ModelMixin):
|
||||||
if not cls.get_by(email=email):
|
if not cls.get_by(email=email):
|
||||||
break
|
break
|
||||||
|
|
||||||
return GenEmail.create(
|
return Alias.create(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
email=email,
|
email=email,
|
||||||
note=note,
|
note=note,
|
||||||
|
@ -588,7 +588,7 @@ class GenEmail(db.Model, ModelMixin):
|
||||||
):
|
):
|
||||||
"""create a new random alias"""
|
"""create a new random alias"""
|
||||||
random_email = generate_email(scheme=scheme, in_hex=in_hex)
|
random_email = generate_email(scheme=scheme, in_hex=in_hex)
|
||||||
return GenEmail.create(
|
return Alias.create(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
email=random_email,
|
email=random_email,
|
||||||
mailbox_id=user.default_mailbox_id,
|
mailbox_id=user.default_mailbox_id,
|
||||||
|
@ -602,7 +602,7 @@ class GenEmail(db.Model, ModelMixin):
|
||||||
return self.user.email
|
return self.user.email
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<GenEmail {self.id} {self.email}>"
|
return f"<Alias {self.id} {self.email}>"
|
||||||
|
|
||||||
|
|
||||||
class ClientUser(db.Model, ModelMixin):
|
class ClientUser(db.Model, ModelMixin):
|
||||||
|
@ -614,9 +614,7 @@ class ClientUser(db.Model, ModelMixin):
|
||||||
client_id = db.Column(db.ForeignKey(Client.id, ondelete="cascade"), nullable=False)
|
client_id = db.Column(db.ForeignKey(Client.id, ondelete="cascade"), nullable=False)
|
||||||
|
|
||||||
# Null means client has access to user original email
|
# Null means client has access to user original email
|
||||||
gen_email_id = db.Column(
|
alias_id = db.Column(db.ForeignKey(Alias.id, ondelete="cascade"), nullable=True)
|
||||||
db.ForeignKey(GenEmail.id, ondelete="cascade"), nullable=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# user can decide to send to client another name
|
# user can decide to send to client another name
|
||||||
name = db.Column(
|
name = db.Column(
|
||||||
|
@ -628,13 +626,13 @@ class ClientUser(db.Model, ModelMixin):
|
||||||
db.Boolean, nullable=False, default=False, server_default="0"
|
db.Boolean, nullable=False, default=False, server_default="0"
|
||||||
)
|
)
|
||||||
|
|
||||||
gen_email = db.relationship(GenEmail, backref="client_users")
|
alias = db.relationship(Alias, backref="client_users")
|
||||||
|
|
||||||
user = db.relationship(User)
|
user = db.relationship(User)
|
||||||
client = db.relationship(Client)
|
client = db.relationship(Client)
|
||||||
|
|
||||||
def get_email(self):
|
def get_email(self):
|
||||||
return self.gen_email.email if self.gen_email_id else self.user.email
|
return self.alias.email if self.alias_id else self.user.email
|
||||||
|
|
||||||
def get_user_name(self):
|
def get_user_name(self):
|
||||||
if self.name:
|
if self.name:
|
||||||
|
@ -681,11 +679,11 @@ class ClientUser(db.Model, ModelMixin):
|
||||||
res[Scope.AVATAR_URL.value] = None
|
res[Scope.AVATAR_URL.value] = None
|
||||||
elif scope == Scope.EMAIL:
|
elif scope == Scope.EMAIL:
|
||||||
# Use generated email
|
# Use generated email
|
||||||
if self.gen_email_id:
|
if self.alias_id:
|
||||||
LOG.debug(
|
LOG.debug(
|
||||||
"Use gen email for user %s, client %s", self.user, self.client
|
"Use gen email for user %s, client %s", self.user, self.client
|
||||||
)
|
)
|
||||||
res[Scope.EMAIL.value] = self.gen_email.email
|
res[Scope.EMAIL.value] = self.alias.email
|
||||||
# Use user original email
|
# Use user original email
|
||||||
else:
|
else:
|
||||||
res[Scope.EMAIL.value] = self.user.email
|
res[Scope.EMAIL.value] = self.user.email
|
||||||
|
@ -693,18 +691,16 @@ class ClientUser(db.Model, ModelMixin):
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
class ForwardEmail(db.Model, ModelMixin):
|
class Contact(db.Model, ModelMixin):
|
||||||
"""
|
"""
|
||||||
Store configuration of sender (website-email) and alias.
|
Store configuration of sender (website-email) and alias.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
db.UniqueConstraint("gen_email_id", "website_email", name="uq_forward_email"),
|
db.UniqueConstraint("alias_id", "website_email", name="uq_contact"),
|
||||||
)
|
)
|
||||||
|
|
||||||
gen_email_id = db.Column(
|
alias_id = db.Column(db.ForeignKey(Alias.id, ondelete="cascade"), nullable=False)
|
||||||
db.ForeignKey(GenEmail.id, ondelete="cascade"), nullable=False
|
|
||||||
)
|
|
||||||
|
|
||||||
# used to be envelope header, should be mail header from instead
|
# used to be envelope header, should be mail header from instead
|
||||||
website_email = db.Column(db.String(512), nullable=False)
|
website_email = db.Column(db.String(512), nullable=False)
|
||||||
|
@ -719,7 +715,7 @@ class ForwardEmail(db.Model, ModelMixin):
|
||||||
# it has the prefix "reply+" to distinguish with other email
|
# it has the prefix "reply+" to distinguish with other email
|
||||||
reply_email = db.Column(db.String(512), nullable=False)
|
reply_email = db.Column(db.String(512), nullable=False)
|
||||||
|
|
||||||
gen_email = db.relationship(GenEmail, backref="forward_emails")
|
alias = db.relationship(Alias, backref="contacts")
|
||||||
|
|
||||||
def website_send_to(self):
|
def website_send_to(self):
|
||||||
"""return the email address with name.
|
"""return the email address with name.
|
||||||
|
@ -739,18 +735,18 @@ class ForwardEmail(db.Model, ModelMixin):
|
||||||
# cannot use formataddr here as this field is for email client, not for MTA
|
# cannot use formataddr here as this field is for email client, not for MTA
|
||||||
# return formataddr((self.website_email.replace("@", " at "), self.reply_email))
|
# return formataddr((self.website_email.replace("@", " at "), self.reply_email))
|
||||||
|
|
||||||
def last_reply(self) -> "ForwardEmailLog":
|
def last_reply(self) -> "EmailLog":
|
||||||
"""return the most recent reply"""
|
"""return the most recent reply"""
|
||||||
return (
|
return (
|
||||||
ForwardEmailLog.query.filter_by(forward_id=self.id, is_reply=True)
|
EmailLog.query.filter_by(contact_id=self.id, is_reply=True)
|
||||||
.order_by(desc(ForwardEmailLog.created_at))
|
.order_by(desc(EmailLog.created_at))
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ForwardEmailLog(db.Model, ModelMixin):
|
class EmailLog(db.Model, ModelMixin):
|
||||||
forward_id = db.Column(
|
contact_id = db.Column(
|
||||||
db.ForeignKey(ForwardEmail.id, ondelete="cascade"), nullable=False
|
db.ForeignKey(Contact.id, ondelete="cascade"), nullable=False
|
||||||
)
|
)
|
||||||
|
|
||||||
# whether this is a reply
|
# whether this is a reply
|
||||||
|
@ -769,7 +765,7 @@ class ForwardEmailLog(db.Model, ModelMixin):
|
||||||
)
|
)
|
||||||
|
|
||||||
refused_email = db.relationship("RefusedEmail")
|
refused_email = db.relationship("RefusedEmail")
|
||||||
forward = db.relationship(ForwardEmail)
|
forward = db.relationship(Contact)
|
||||||
|
|
||||||
|
|
||||||
class Subscription(db.Model, ModelMixin):
|
class Subscription(db.Model, ModelMixin):
|
||||||
|
@ -845,12 +841,10 @@ class AliasUsedOn(db.Model, ModelMixin):
|
||||||
"""Used to know where an alias is created"""
|
"""Used to know where an alias is created"""
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
db.UniqueConstraint("gen_email_id", "hostname", name="uq_alias_used"),
|
db.UniqueConstraint("alias_id", "hostname", name="uq_alias_used"),
|
||||||
)
|
)
|
||||||
|
|
||||||
gen_email_id = db.Column(
|
alias_id = db.Column(db.ForeignKey(Alias.id, ondelete="cascade"), nullable=False)
|
||||||
db.ForeignKey(GenEmail.id, ondelete="cascade"), nullable=False
|
|
||||||
)
|
|
||||||
|
|
||||||
hostname = db.Column(db.String(1024), nullable=False)
|
hostname = db.Column(db.String(1024), nullable=False)
|
||||||
|
|
||||||
|
@ -899,7 +893,7 @@ class CustomDomain(db.Model, ModelMixin):
|
||||||
user = db.relationship(User)
|
user = db.relationship(User)
|
||||||
|
|
||||||
def nb_alias(self):
|
def nb_alias(self):
|
||||||
return GenEmail.filter_by(custom_domain_id=self.id).count()
|
return Alias.filter_by(custom_domain_id=self.id).count()
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<Custom Domain {self.domain}>"
|
return f"<Custom Domain {self.domain}>"
|
||||||
|
@ -917,7 +911,7 @@ class Directory(db.Model, ModelMixin):
|
||||||
user = db.relationship(User)
|
user = db.relationship(User)
|
||||||
|
|
||||||
def nb_alias(self):
|
def nb_alias(self):
|
||||||
return GenEmail.filter_by(directory_id=self.id).count()
|
return Alias.filter_by(directory_id=self.id).count()
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<Directory {self.name}>"
|
return f"<Directory {self.name}>"
|
||||||
|
@ -949,7 +943,7 @@ class Mailbox(db.Model, ModelMixin):
|
||||||
pgp_finger_print = db.Column(db.String(512), nullable=True)
|
pgp_finger_print = db.Column(db.String(512), nullable=True)
|
||||||
|
|
||||||
def nb_alias(self):
|
def nb_alias(self):
|
||||||
return GenEmail.filter_by(mailbox_id=self.id).count()
|
return Alias.filter_by(mailbox_id=self.id).count()
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<Mailbox {self.email}>"
|
return f"<Mailbox {self.email}>"
|
||||||
|
|
|
@ -13,7 +13,7 @@ from app.models import (
|
||||||
Client,
|
Client,
|
||||||
AuthorizationCode,
|
AuthorizationCode,
|
||||||
ClientUser,
|
ClientUser,
|
||||||
GenEmail,
|
Alias,
|
||||||
RedirectUri,
|
RedirectUri,
|
||||||
OauthToken,
|
OauthToken,
|
||||||
DeletedAlias,
|
DeletedAlias,
|
||||||
|
@ -157,7 +157,7 @@ def authorize():
|
||||||
alias_prefix = request.form.get("prefix")
|
alias_prefix = request.form.get("prefix")
|
||||||
alias_suffix = request.form.get("suffix")
|
alias_suffix = request.form.get("suffix")
|
||||||
|
|
||||||
gen_email = None
|
alias = None
|
||||||
|
|
||||||
# user creates a new alias, not using suggested alias
|
# user creates a new alias, not using suggested alias
|
||||||
if alias_prefix:
|
if alias_prefix:
|
||||||
|
@ -176,14 +176,14 @@ def authorize():
|
||||||
):
|
):
|
||||||
full_alias = alias_prefix + alias_suffix
|
full_alias = alias_prefix + alias_suffix
|
||||||
|
|
||||||
if GenEmail.get_by(email=full_alias) or DeletedAlias.get_by(
|
if Alias.get_by(email=full_alias) or DeletedAlias.get_by(
|
||||||
email=full_alias
|
email=full_alias
|
||||||
):
|
):
|
||||||
LOG.error("alias %s already used, very rare!", full_alias)
|
LOG.error("alias %s already used, very rare!", full_alias)
|
||||||
flash(f"Alias {full_alias} already used", "error")
|
flash(f"Alias {full_alias} already used", "error")
|
||||||
return redirect(request.url)
|
return redirect(request.url)
|
||||||
else:
|
else:
|
||||||
gen_email = GenEmail.create(
|
alias = Alias.create(
|
||||||
user_id=current_user.id,
|
user_id=current_user.id,
|
||||||
email=full_alias,
|
email=full_alias,
|
||||||
mailbox_id=current_user.default_mailbox_id,
|
mailbox_id=current_user.default_mailbox_id,
|
||||||
|
@ -193,7 +193,7 @@ def authorize():
|
||||||
alias_domain = get_email_domain_part(full_alias)
|
alias_domain = get_email_domain_part(full_alias)
|
||||||
custom_domain = CustomDomain.get_by(domain=alias_domain)
|
custom_domain = CustomDomain.get_by(domain=alias_domain)
|
||||||
if custom_domain:
|
if custom_domain:
|
||||||
gen_email.custom_domain_id = custom_domain.id
|
alias.custom_domain_id = custom_domain.id
|
||||||
|
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
flash(f"Alias {full_alias} has been created", "success")
|
flash(f"Alias {full_alias} has been created", "success")
|
||||||
|
@ -206,9 +206,9 @@ def authorize():
|
||||||
chosen_email = request.form.get("suggested-email")
|
chosen_email = request.form.get("suggested-email")
|
||||||
# todo: add some checks on chosen_email
|
# todo: add some checks on chosen_email
|
||||||
if chosen_email != current_user.email:
|
if chosen_email != current_user.email:
|
||||||
gen_email = GenEmail.get_by(email=chosen_email)
|
alias = Alias.get_by(email=chosen_email)
|
||||||
if not gen_email:
|
if not alias:
|
||||||
gen_email = GenEmail.create(
|
alias = Alias.create(
|
||||||
email=chosen_email,
|
email=chosen_email,
|
||||||
user_id=current_user.id,
|
user_id=current_user.id,
|
||||||
mailbox_id=current_user.default_mailbox_id,
|
mailbox_id=current_user.default_mailbox_id,
|
||||||
|
@ -223,8 +223,8 @@ def authorize():
|
||||||
client_user = ClientUser.create(
|
client_user = ClientUser.create(
|
||||||
client_id=client.id, user_id=current_user.id
|
client_id=client.id, user_id=current_user.id
|
||||||
)
|
)
|
||||||
if gen_email:
|
if alias:
|
||||||
client_user.gen_email_id = gen_email.id
|
client_user.alias_id = alias.id
|
||||||
|
|
||||||
if custom_name:
|
if custom_name:
|
||||||
client_user.name = custom_name
|
client_user.name = custom_name
|
||||||
|
|
30
cron.py
30
cron.py
|
@ -10,9 +10,9 @@ from app.log import LOG
|
||||||
from app.models import (
|
from app.models import (
|
||||||
Subscription,
|
Subscription,
|
||||||
User,
|
User,
|
||||||
GenEmail,
|
Alias,
|
||||||
ForwardEmailLog,
|
EmailLog,
|
||||||
ForwardEmail,
|
Contact,
|
||||||
CustomDomain,
|
CustomDomain,
|
||||||
Client,
|
Client,
|
||||||
ManualSubscription,
|
ManualSubscription,
|
||||||
|
@ -111,18 +111,18 @@ def stats():
|
||||||
LOG.d("total number user %s", nb_user)
|
LOG.d("total number user %s", nb_user)
|
||||||
|
|
||||||
# nb gen emails
|
# nb gen emails
|
||||||
q = db.session.query(GenEmail, User).filter(GenEmail.user_id == User.id)
|
q = db.session.query(Alias, User).filter(Alias.user_id == User.id)
|
||||||
for ie in IGNORED_EMAILS:
|
for ie in IGNORED_EMAILS:
|
||||||
q = q.filter(~User.email.contains(ie))
|
q = q.filter(~User.email.contains(ie))
|
||||||
|
|
||||||
nb_gen_email = q.count()
|
nb_alias = q.count()
|
||||||
LOG.d("total number alias %s", nb_gen_email)
|
LOG.d("total number alias %s", nb_alias)
|
||||||
|
|
||||||
# nb mails forwarded
|
# nb mails forwarded
|
||||||
q = db.session.query(ForwardEmailLog, ForwardEmail, GenEmail, User).filter(
|
q = db.session.query(EmailLog, Contact, Alias, User).filter(
|
||||||
ForwardEmailLog.forward_id == ForwardEmail.id,
|
EmailLog.contact_id == Contact.id,
|
||||||
ForwardEmail.gen_email_id == GenEmail.id,
|
Contact.alias_id == Alias.id,
|
||||||
GenEmail.user_id == User.id,
|
Alias.user_id == User.id,
|
||||||
)
|
)
|
||||||
for ie in IGNORED_EMAILS:
|
for ie in IGNORED_EMAILS:
|
||||||
q = q.filter(~User.email.contains(ie))
|
q = q.filter(~User.email.contains(ie))
|
||||||
|
@ -141,11 +141,11 @@ def stats():
|
||||||
nb_premium = Subscription.query.count()
|
nb_premium = Subscription.query.count()
|
||||||
nb_custom_domain = CustomDomain.query.count()
|
nb_custom_domain = CustomDomain.query.count()
|
||||||
|
|
||||||
nb_custom_domain_alias = GenEmail.query.filter(
|
nb_custom_domain_alias = Alias.query.filter(
|
||||||
GenEmail.custom_domain_id.isnot(None)
|
Alias.custom_domain_id.isnot(None)
|
||||||
).count()
|
).count()
|
||||||
|
|
||||||
nb_disabled_alias = GenEmail.query.filter(GenEmail.enabled == False).count()
|
nb_disabled_alias = Alias.query.filter(Alias.enabled == False).count()
|
||||||
|
|
||||||
nb_app = Client.query.count()
|
nb_app = Client.query.count()
|
||||||
|
|
||||||
|
@ -153,7 +153,7 @@ def stats():
|
||||||
|
|
||||||
send_email(
|
send_email(
|
||||||
ADMIN_EMAIL,
|
ADMIN_EMAIL,
|
||||||
subject=f"SimpleLogin Stats for {today}, {nb_user} users, {nb_gen_email} aliases, {nb_forward} forwards",
|
subject=f"SimpleLogin Stats for {today}, {nb_user} users, {nb_alias} aliases, {nb_forward} forwards",
|
||||||
plaintext="",
|
plaintext="",
|
||||||
html=f"""
|
html=f"""
|
||||||
Stats for {today} <br>
|
Stats for {today} <br>
|
||||||
|
@ -161,7 +161,7 @@ Stats for {today} <br>
|
||||||
nb_user: {nb_user} <br>
|
nb_user: {nb_user} <br>
|
||||||
nb_premium: {nb_premium} <br>
|
nb_premium: {nb_premium} <br>
|
||||||
|
|
||||||
nb_alias: {nb_gen_email} <br>
|
nb_alias: {nb_alias} <br>
|
||||||
nb_disabled_alias: {nb_disabled_alias} <br>
|
nb_disabled_alias: {nb_disabled_alias} <br>
|
||||||
|
|
||||||
nb_custom_domain: {nb_custom_domain} <br>
|
nb_custom_domain: {nb_custom_domain} <br>
|
||||||
|
|
|
@ -25,7 +25,7 @@ docker exec -it sl-app python shell.py
|
||||||
"""
|
"""
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.log import LOG
|
from app.log import LOG
|
||||||
from app.models import Mailbox, GenEmail, User
|
from app.models import Mailbox, Alias, User
|
||||||
|
|
||||||
for user in User.query.all():
|
for user in User.query.all():
|
||||||
if user.default_mailbox_id:
|
if user.default_mailbox_id:
|
||||||
|
@ -40,7 +40,7 @@ for user in User.query.all():
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# assign existing alias to this mailbox
|
# assign existing alias to this mailbox
|
||||||
for gen_email in GenEmail.query.filter_by(user_id=user.id):
|
for gen_email in Alias.query.filter_by(user_id=user.id):
|
||||||
if not gen_email.mailbox_id:
|
if not gen_email.mailbox_id:
|
||||||
LOG.d("Set alias %s mailbox to default mailbox", gen_email)
|
LOG.d("Set alias %s mailbox to default mailbox", gen_email)
|
||||||
gen_email.mailbox_id = default_mb.id
|
gen_email.mailbox_id = default_mb.id
|
||||||
|
|
214
email_handler.py
214
email_handler.py
|
@ -69,9 +69,9 @@ from app.email_utils import (
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.log import LOG
|
from app.log import LOG
|
||||||
from app.models import (
|
from app.models import (
|
||||||
GenEmail,
|
Alias,
|
||||||
ForwardEmail,
|
Contact,
|
||||||
ForwardEmailLog,
|
EmailLog,
|
||||||
CustomDomain,
|
CustomDomain,
|
||||||
Directory,
|
Directory,
|
||||||
User,
|
User,
|
||||||
|
@ -98,35 +98,35 @@ def new_app():
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
def try_auto_create(alias: str) -> Optional[GenEmail]:
|
def try_auto_create(address: str) -> Optional[Alias]:
|
||||||
"""Try to auto-create the alias using directory or catch-all domain
|
"""Try to auto-create the alias using directory or catch-all domain
|
||||||
"""
|
"""
|
||||||
gen_email = try_auto_create_catch_all_domain(alias)
|
alias = try_auto_create_catch_all_domain(address)
|
||||||
if not gen_email:
|
if not alias:
|
||||||
gen_email = try_auto_create_directory(alias)
|
alias = try_auto_create_directory(address)
|
||||||
|
|
||||||
return gen_email
|
return alias
|
||||||
|
|
||||||
|
|
||||||
def try_auto_create_directory(alias: str) -> Optional[GenEmail]:
|
def try_auto_create_directory(address: str) -> Optional[Alias]:
|
||||||
"""
|
"""
|
||||||
Try to create an alias with directory
|
Try to create an alias with directory
|
||||||
"""
|
"""
|
||||||
# check if alias belongs to a directory, ie having directory/anything@EMAIL_DOMAIN format
|
# check if alias belongs to a directory, ie having directory/anything@EMAIL_DOMAIN format
|
||||||
if email_belongs_to_alias_domains(alias):
|
if email_belongs_to_alias_domains(address):
|
||||||
# if there's no directory separator in the alias, no way to auto-create it
|
# if there's no directory separator in the alias, no way to auto-create it
|
||||||
if "/" not in alias and "+" not in alias and "#" not in alias:
|
if "/" not in address and "+" not in address and "#" not in address:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# alias contains one of the 3 special directory separator: "/", "+" or "#"
|
# alias contains one of the 3 special directory separator: "/", "+" or "#"
|
||||||
if "/" in alias:
|
if "/" in address:
|
||||||
sep = "/"
|
sep = "/"
|
||||||
elif "+" in alias:
|
elif "+" in address:
|
||||||
sep = "+"
|
sep = "+"
|
||||||
else:
|
else:
|
||||||
sep = "#"
|
sep = "#"
|
||||||
|
|
||||||
directory_name = alias[: alias.find(sep)]
|
directory_name = address[: address.find(sep)]
|
||||||
LOG.d("directory_name %s", directory_name)
|
LOG.d("directory_name %s", directory_name)
|
||||||
|
|
||||||
directory = Directory.get_by(name=directory_name)
|
directory = Directory.get_by(name=directory_name)
|
||||||
|
@ -136,37 +136,37 @@ def try_auto_create_directory(alias: str) -> Optional[GenEmail]:
|
||||||
dir_user: User = directory.user
|
dir_user: User = directory.user
|
||||||
|
|
||||||
if not dir_user.can_create_new_alias():
|
if not dir_user.can_create_new_alias():
|
||||||
send_cannot_create_directory_alias(dir_user, alias, directory_name)
|
send_cannot_create_directory_alias(dir_user, address, directory_name)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# if alias has been deleted before, do not auto-create it
|
# if alias has been deleted before, do not auto-create it
|
||||||
if DeletedAlias.get_by(email=alias, user_id=directory.user_id):
|
if DeletedAlias.get_by(email=address, user_id=directory.user_id):
|
||||||
LOG.warning(
|
LOG.warning(
|
||||||
"Alias %s was deleted before, cannot auto-create using directory %s, user %s",
|
"Alias %s was deleted before, cannot auto-create using directory %s, user %s",
|
||||||
alias,
|
address,
|
||||||
directory_name,
|
directory_name,
|
||||||
dir_user,
|
dir_user,
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
LOG.d("create alias %s for directory %s", alias, directory)
|
LOG.d("create alias %s for directory %s", address, directory)
|
||||||
|
|
||||||
gen_email = GenEmail.create(
|
alias = Alias.create(
|
||||||
email=alias,
|
email=address,
|
||||||
user_id=directory.user_id,
|
user_id=directory.user_id,
|
||||||
directory_id=directory.id,
|
directory_id=directory.id,
|
||||||
mailbox_id=dir_user.default_mailbox_id,
|
mailbox_id=dir_user.default_mailbox_id,
|
||||||
)
|
)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return gen_email
|
return alias
|
||||||
|
|
||||||
|
|
||||||
def try_auto_create_catch_all_domain(alias: str) -> Optional[GenEmail]:
|
def try_auto_create_catch_all_domain(address: str) -> Optional[Alias]:
|
||||||
"""Try to create an alias with catch-all domain"""
|
"""Try to create an alias with catch-all domain"""
|
||||||
|
|
||||||
# try to create alias on-the-fly with custom-domain catch-all feature
|
# try to create alias on-the-fly with custom-domain catch-all feature
|
||||||
# check if alias is custom-domain alias and if the custom-domain has catch-all enabled
|
# check if alias is custom-domain alias and if the custom-domain has catch-all enabled
|
||||||
alias_domain = get_email_domain_part(alias)
|
alias_domain = get_email_domain_part(address)
|
||||||
custom_domain = CustomDomain.get_by(domain=alias_domain)
|
custom_domain = CustomDomain.get_by(domain=alias_domain)
|
||||||
|
|
||||||
if not custom_domain:
|
if not custom_domain:
|
||||||
|
@ -180,23 +180,23 @@ def try_auto_create_catch_all_domain(alias: str) -> Optional[GenEmail]:
|
||||||
domain_user: User = custom_domain.user
|
domain_user: User = custom_domain.user
|
||||||
|
|
||||||
if not domain_user.can_create_new_alias():
|
if not domain_user.can_create_new_alias():
|
||||||
send_cannot_create_domain_alias(domain_user, alias, alias_domain)
|
send_cannot_create_domain_alias(domain_user, address, alias_domain)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# if alias has been deleted before, do not auto-create it
|
# if alias has been deleted before, do not auto-create it
|
||||||
if DeletedAlias.get_by(email=alias, user_id=custom_domain.user_id):
|
if DeletedAlias.get_by(email=address, user_id=custom_domain.user_id):
|
||||||
LOG.warning(
|
LOG.warning(
|
||||||
"Alias %s was deleted before, cannot auto-create using domain catch-all %s, user %s",
|
"Alias %s was deleted before, cannot auto-create using domain catch-all %s, user %s",
|
||||||
alias,
|
address,
|
||||||
custom_domain,
|
custom_domain,
|
||||||
domain_user,
|
domain_user,
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
LOG.d("create alias %s for domain %s", alias, custom_domain)
|
LOG.d("create alias %s for domain %s", address, custom_domain)
|
||||||
|
|
||||||
gen_email = GenEmail.create(
|
alias = Alias.create(
|
||||||
email=alias,
|
email=address,
|
||||||
user_id=custom_domain.user_id,
|
user_id=custom_domain.user_id,
|
||||||
custom_domain_id=custom_domain.id,
|
custom_domain_id=custom_domain.id,
|
||||||
automatic_creation=True,
|
automatic_creation=True,
|
||||||
|
@ -204,29 +204,25 @@ def try_auto_create_catch_all_domain(alias: str) -> Optional[GenEmail]:
|
||||||
)
|
)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return gen_email
|
return alias
|
||||||
|
|
||||||
|
|
||||||
def get_or_create_forward_email(
|
def get_or_create_contact(website_from_header: str, alias: Alias) -> Contact:
|
||||||
website_from_header: str, gen_email: GenEmail
|
|
||||||
) -> ForwardEmail:
|
|
||||||
"""
|
"""
|
||||||
website_from_header can be the full-form email, i.e. "First Last <email@example.com>"
|
website_from_header can be the full-form email, i.e. "First Last <email@example.com>"
|
||||||
"""
|
"""
|
||||||
_, website_email = parseaddr(website_from_header)
|
_, website_email = parseaddr(website_from_header)
|
||||||
forward_email = ForwardEmail.get_by(
|
contact = Contact.get_by(alias_id=alias.id, website_email=website_email)
|
||||||
gen_email_id=gen_email.id, website_email=website_email
|
if contact:
|
||||||
)
|
|
||||||
if forward_email:
|
|
||||||
# update the website_from if needed
|
# update the website_from if needed
|
||||||
if forward_email.website_from != website_from_header:
|
if contact.website_from != website_from_header:
|
||||||
LOG.d("Update From header for %s", forward_email)
|
LOG.d("Update From header for %s", contact)
|
||||||
forward_email.website_from = website_from_header
|
contact.website_from = website_from_header
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
else:
|
else:
|
||||||
LOG.debug(
|
LOG.debug(
|
||||||
"create forward email for alias %s and website email %s",
|
"create forward email for alias %s and website email %s",
|
||||||
gen_email,
|
alias,
|
||||||
website_from_header,
|
website_from_header,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -234,28 +230,28 @@ def get_or_create_forward_email(
|
||||||
# not use while loop to avoid infinite loop
|
# not use while loop to avoid infinite loop
|
||||||
reply_email = f"reply+{random_string(30)}@{EMAIL_DOMAIN}"
|
reply_email = f"reply+{random_string(30)}@{EMAIL_DOMAIN}"
|
||||||
for _ in range(1000):
|
for _ in range(1000):
|
||||||
if not ForwardEmail.get_by(reply_email=reply_email):
|
if not Contact.get_by(reply_email=reply_email):
|
||||||
# found!
|
# found!
|
||||||
break
|
break
|
||||||
reply_email = f"reply+{random_string(30)}@{EMAIL_DOMAIN}"
|
reply_email = f"reply+{random_string(30)}@{EMAIL_DOMAIN}"
|
||||||
|
|
||||||
forward_email = ForwardEmail.create(
|
contact = Contact.create(
|
||||||
gen_email_id=gen_email.id,
|
alias_id=alias.id,
|
||||||
website_email=website_email,
|
website_email=website_email,
|
||||||
website_from=website_from_header,
|
website_from=website_from_header,
|
||||||
reply_email=reply_email,
|
reply_email=reply_email,
|
||||||
)
|
)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return forward_email
|
return contact
|
||||||
|
|
||||||
|
|
||||||
def should_append_alias(msg, alias):
|
def should_append_alias(msg: Message, address: str):
|
||||||
"""whether an alias should be appened to TO header in message"""
|
"""whether an alias should be appened to TO header in message"""
|
||||||
|
|
||||||
if msg["To"] and alias in msg["To"]:
|
if msg["To"] and address in msg["To"]:
|
||||||
return False
|
return False
|
||||||
if msg["Cc"] and alias in msg["Cc"]:
|
if msg["Cc"] and address in msg["Cc"]:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@ -301,15 +297,15 @@ def handle_forward(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str:
|
||||||
"""return *status_code message*"""
|
"""return *status_code message*"""
|
||||||
alias = rcpt_to.lower() # alias@SL
|
alias = rcpt_to.lower() # alias@SL
|
||||||
|
|
||||||
gen_email = GenEmail.get_by(email=alias)
|
alias = Alias.get_by(email=alias)
|
||||||
if not gen_email:
|
if not alias:
|
||||||
LOG.d("alias %s not exist. Try to see if it can be created on the fly", alias)
|
LOG.d("alias %s not exist. Try to see if it can be created on the fly", alias)
|
||||||
gen_email = try_auto_create(alias)
|
alias = try_auto_create(alias)
|
||||||
if not gen_email:
|
if not alias:
|
||||||
LOG.d("alias %s cannot be created on-the-fly, return 510", alias)
|
LOG.d("alias %s cannot be created on-the-fly, return 510", alias)
|
||||||
return "510 Email not exist"
|
return "510 Email not exist"
|
||||||
|
|
||||||
mailbox = gen_email.mailbox
|
mailbox = alias.mailbox
|
||||||
mailbox_email = mailbox.email
|
mailbox_email = mailbox.email
|
||||||
|
|
||||||
# create PGP email if needed
|
# create PGP email if needed
|
||||||
|
@ -317,10 +313,10 @@ def handle_forward(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str:
|
||||||
LOG.d("Encrypt message using mailbox %s", mailbox)
|
LOG.d("Encrypt message using mailbox %s", mailbox)
|
||||||
msg = prepare_pgp_message(msg, mailbox.pgp_finger_print)
|
msg = prepare_pgp_message(msg, mailbox.pgp_finger_print)
|
||||||
|
|
||||||
forward_email = get_or_create_forward_email(msg["From"], gen_email)
|
contact = get_or_create_contact(msg["From"], alias)
|
||||||
forward_log = ForwardEmailLog.create(forward_id=forward_email.id)
|
forward_log = EmailLog.create(contact_id=contact.id)
|
||||||
|
|
||||||
if gen_email.enabled:
|
if alias.enabled:
|
||||||
# add custom header
|
# add custom header
|
||||||
add_or_replace_header(msg, "X-SimpleLogin-Type", "Forward")
|
add_or_replace_header(msg, "X-SimpleLogin-Type", "Forward")
|
||||||
|
|
||||||
|
@ -338,12 +334,12 @@ def handle_forward(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str:
|
||||||
+ (" - " if website_name else "")
|
+ (" - " if website_name else "")
|
||||||
+ website_email.replace("@", " at ")
|
+ website_email.replace("@", " at ")
|
||||||
)
|
)
|
||||||
from_header = formataddr((new_website_name, forward_email.reply_email))
|
from_header = formataddr((new_website_name, contact.reply_email))
|
||||||
add_or_replace_header(msg, "From", from_header)
|
add_or_replace_header(msg, "From", from_header)
|
||||||
LOG.d("new from header:%s", from_header)
|
LOG.d("new from header:%s", from_header)
|
||||||
|
|
||||||
# append alias into the TO header if it's not present in To or CC
|
# append alias into the TO header if it's not present in To or CC
|
||||||
if should_append_alias(msg, alias):
|
if should_append_alias(msg, alias.email):
|
||||||
LOG.d("append alias %s to TO header %s", alias, msg["To"])
|
LOG.d("append alias %s to TO header %s", alias, msg["To"])
|
||||||
if msg["To"]:
|
if msg["To"]:
|
||||||
to_header = msg["To"] + "," + alias
|
to_header = msg["To"] + "," + alias
|
||||||
|
@ -353,7 +349,7 @@ def handle_forward(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str:
|
||||||
add_or_replace_header(msg, "To", to_header)
|
add_or_replace_header(msg, "To", to_header)
|
||||||
|
|
||||||
# add List-Unsubscribe header
|
# add List-Unsubscribe header
|
||||||
unsubscribe_link = f"{URL}/dashboard/unsubscribe/{gen_email.id}"
|
unsubscribe_link = f"{URL}/dashboard/unsubscribe/{alias.id}"
|
||||||
add_or_replace_header(msg, "List-Unsubscribe", f"<{unsubscribe_link}>")
|
add_or_replace_header(msg, "List-Unsubscribe", f"<{unsubscribe_link}>")
|
||||||
add_or_replace_header(
|
add_or_replace_header(
|
||||||
msg, "List-Unsubscribe-Post", "List-Unsubscribe=One-Click"
|
msg, "List-Unsubscribe-Post", "List-Unsubscribe=One-Click"
|
||||||
|
@ -373,14 +369,14 @@ def handle_forward(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str:
|
||||||
# encode message raw directly instead
|
# encode message raw directly instead
|
||||||
msg_raw = msg.as_string().encode()
|
msg_raw = msg.as_string().encode()
|
||||||
smtp.sendmail(
|
smtp.sendmail(
|
||||||
forward_email.reply_email,
|
contact.reply_email,
|
||||||
mailbox_email,
|
mailbox_email,
|
||||||
msg_raw,
|
msg_raw,
|
||||||
envelope.mail_options,
|
envelope.mail_options,
|
||||||
envelope.rcpt_options,
|
envelope.rcpt_options,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
LOG.d("%s is disabled, do not forward", gen_email)
|
LOG.d("%s is disabled, do not forward", alias)
|
||||||
forward_log.blocked = True
|
forward_log.blocked = True
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -395,22 +391,22 @@ def handle_reply(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str:
|
||||||
LOG.warning(f"Reply email {reply_email} has wrong domain")
|
LOG.warning(f"Reply email {reply_email} has wrong domain")
|
||||||
return "550 wrong reply email"
|
return "550 wrong reply email"
|
||||||
|
|
||||||
forward_email = ForwardEmail.get_by(reply_email=reply_email)
|
contact = Contact.get_by(reply_email=reply_email)
|
||||||
if not forward_email:
|
if not contact:
|
||||||
LOG.warning(f"No such forward-email with {reply_email} as reply-email")
|
LOG.warning(f"No such forward-email with {reply_email} as reply-email")
|
||||||
return "550 wrong reply email"
|
return "550 wrong reply email"
|
||||||
|
|
||||||
alias: str = forward_email.gen_email.email
|
address: str = contact.alias.email
|
||||||
alias_domain = alias[alias.find("@") + 1 :]
|
alias_domain = address[address.find("@") + 1 :]
|
||||||
|
|
||||||
# alias must end with one of the ALIAS_DOMAINS or custom-domain
|
# alias must end with one of the ALIAS_DOMAINS or custom-domain
|
||||||
if not email_belongs_to_alias_domains(alias):
|
if not email_belongs_to_alias_domains(address):
|
||||||
if not CustomDomain.get_by(domain=alias_domain):
|
if not CustomDomain.get_by(domain=alias_domain):
|
||||||
return "550 alias unknown by SimpleLogin"
|
return "550 alias unknown by SimpleLogin"
|
||||||
|
|
||||||
gen_email = forward_email.gen_email
|
alias = contact.alias
|
||||||
user = gen_email.user
|
user = alias.user
|
||||||
mailbox_email = gen_email.mailbox_email()
|
mailbox_email = alias.mailbox_email()
|
||||||
|
|
||||||
# bounce email initiated by Postfix
|
# bounce email initiated by Postfix
|
||||||
# can happen in case emails cannot be delivered to user-email
|
# can happen in case emails cannot be delivered to user-email
|
||||||
|
@ -419,41 +415,40 @@ def handle_reply(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str:
|
||||||
if envelope.mail_from == "<>":
|
if envelope.mail_from == "<>":
|
||||||
LOG.error(
|
LOG.error(
|
||||||
"Bounce when sending to alias %s from %s, user %s",
|
"Bounce when sending to alias %s from %s, user %s",
|
||||||
alias,
|
address,
|
||||||
forward_email.website_from,
|
contact.website_from,
|
||||||
gen_email.user,
|
alias.user,
|
||||||
)
|
)
|
||||||
|
|
||||||
handle_bounce(
|
handle_bounce(contact, alias, msg, user, mailbox_email)
|
||||||
alias, envelope, forward_email, gen_email, msg, smtp, user, mailbox_email
|
|
||||||
)
|
|
||||||
return "550 ignored"
|
return "550 ignored"
|
||||||
|
|
||||||
# only mailbox can send email to the reply-email
|
# only mailbox can send email to the reply-email
|
||||||
if envelope.mail_from.lower() != mailbox_email.lower():
|
if envelope.mail_from.lower() != mailbox_email.lower():
|
||||||
LOG.warning(
|
LOG.warning(
|
||||||
f"Reply email can only be used by user email. Actual mail_from: %s. msg from header: %s, User email %s. reply_email %s",
|
f"Reply email can only be used by mailbox. "
|
||||||
|
f"Actual mail_from: %s. msg from header: %s, Mailbox %s. reply_email %s",
|
||||||
envelope.mail_from,
|
envelope.mail_from,
|
||||||
msg["From"],
|
msg["From"],
|
||||||
mailbox_email,
|
mailbox_email,
|
||||||
reply_email,
|
reply_email,
|
||||||
)
|
)
|
||||||
|
|
||||||
user = gen_email.user
|
user = alias.user
|
||||||
send_email(
|
send_email(
|
||||||
mailbox_email,
|
mailbox_email,
|
||||||
f"Reply from your alias {alias} only works from your mailbox",
|
f"Reply from your alias {address} only works from your mailbox",
|
||||||
render(
|
render(
|
||||||
"transactional/reply-must-use-personal-email.txt",
|
"transactional/reply-must-use-personal-email.txt",
|
||||||
name=user.name,
|
name=user.name,
|
||||||
alias=alias,
|
alias=address,
|
||||||
sender=envelope.mail_from,
|
sender=envelope.mail_from,
|
||||||
mailbox_email=mailbox_email,
|
mailbox_email=mailbox_email,
|
||||||
),
|
),
|
||||||
render(
|
render(
|
||||||
"transactional/reply-must-use-personal-email.html",
|
"transactional/reply-must-use-personal-email.html",
|
||||||
name=user.name,
|
name=user.name,
|
||||||
alias=alias,
|
alias=address,
|
||||||
sender=envelope.mail_from,
|
sender=envelope.mail_from,
|
||||||
mailbox_email=mailbox_email,
|
mailbox_email=mailbox_email,
|
||||||
),
|
),
|
||||||
|
@ -476,7 +471,7 @@ def handle_reply(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str:
|
||||||
delete_header(msg, "DKIM-Signature")
|
delete_header(msg, "DKIM-Signature")
|
||||||
|
|
||||||
# the email comes from alias
|
# the email comes from alias
|
||||||
add_or_replace_header(msg, "From", alias)
|
add_or_replace_header(msg, "From", address)
|
||||||
|
|
||||||
# some email providers like ProtonMail adds automatically the Reply-To field
|
# some email providers like ProtonMail adds automatically the Reply-To field
|
||||||
# make sure to delete it
|
# make sure to delete it
|
||||||
|
@ -485,10 +480,10 @@ def handle_reply(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str:
|
||||||
# remove sender header if present as this could reveal user real email
|
# remove sender header if present as this could reveal user real email
|
||||||
delete_header(msg, "Sender")
|
delete_header(msg, "Sender")
|
||||||
|
|
||||||
add_or_replace_header(msg, "To", forward_email.website_email)
|
add_or_replace_header(msg, "To", contact.website_email)
|
||||||
|
|
||||||
# add List-Unsubscribe header
|
# add List-Unsubscribe header
|
||||||
unsubscribe_link = f"{URL}/dashboard/unsubscribe/{forward_email.gen_email_id}"
|
unsubscribe_link = f"{URL}/dashboard/unsubscribe/{contact.alias_id}"
|
||||||
add_or_replace_header(msg, "List-Unsubscribe", f"<{unsubscribe_link}>")
|
add_or_replace_header(msg, "List-Unsubscribe", f"<{unsubscribe_link}>")
|
||||||
add_or_replace_header(msg, "List-Unsubscribe-Post", "List-Unsubscribe=One-Click")
|
add_or_replace_header(msg, "List-Unsubscribe-Post", "List-Unsubscribe=One-Click")
|
||||||
|
|
||||||
|
@ -497,8 +492,8 @@ def handle_reply(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str:
|
||||||
|
|
||||||
LOG.d(
|
LOG.d(
|
||||||
"send email from %s to %s, mail_options:%s,rcpt_options:%s",
|
"send email from %s to %s, mail_options:%s,rcpt_options:%s",
|
||||||
alias,
|
address,
|
||||||
forward_email.website_email,
|
contact.website_email,
|
||||||
envelope.mail_options,
|
envelope.mail_options,
|
||||||
envelope.rcpt_options,
|
envelope.rcpt_options,
|
||||||
)
|
)
|
||||||
|
@ -513,31 +508,28 @@ def handle_reply(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str:
|
||||||
|
|
||||||
msg_raw = msg.as_string().encode()
|
msg_raw = msg.as_string().encode()
|
||||||
smtp.sendmail(
|
smtp.sendmail(
|
||||||
alias,
|
address,
|
||||||
forward_email.website_email,
|
contact.website_email,
|
||||||
msg_raw,
|
msg_raw,
|
||||||
envelope.mail_options,
|
envelope.mail_options,
|
||||||
envelope.rcpt_options,
|
envelope.rcpt_options,
|
||||||
)
|
)
|
||||||
|
|
||||||
ForwardEmailLog.create(forward_id=forward_email.id, is_reply=True)
|
EmailLog.create(contact_id=contact.id, is_reply=True)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return "250 Message accepted for delivery"
|
return "250 Message accepted for delivery"
|
||||||
|
|
||||||
|
|
||||||
def handle_bounce(
|
def handle_bounce(
|
||||||
alias, envelope, forward_email, gen_email, msg, smtp, user, mailbox_email
|
contact: Contact, alias: Alias, msg: Message, user: User, mailbox_email: str
|
||||||
):
|
):
|
||||||
fel: ForwardEmailLog = ForwardEmailLog.create(
|
address = alias.email
|
||||||
forward_id=forward_email.id, bounced=True
|
fel: EmailLog = EmailLog.create(contact_id=contact.id, bounced=True)
|
||||||
)
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
nb_bounced = ForwardEmailLog.filter_by(
|
nb_bounced = EmailLog.filter_by(contact_id=contact.id, bounced=True).count()
|
||||||
forward_id=forward_email.id, bounced=True
|
disable_alias_link = f"{URL}/dashboard/unsubscribe/{alias.id}"
|
||||||
).count()
|
|
||||||
disable_alias_link = f"{URL}/dashboard/unsubscribe/{gen_email.id}"
|
|
||||||
|
|
||||||
# Store the bounced email
|
# Store the bounced email
|
||||||
orig_msg = get_orig_message_from_bounce(msg)
|
orig_msg = get_orig_message_from_bounce(msg)
|
||||||
|
@ -569,19 +561,19 @@ def handle_bounce(
|
||||||
LOG.d(
|
LOG.d(
|
||||||
"Inform user %s about bounced email sent by %s to alias %s",
|
"Inform user %s about bounced email sent by %s to alias %s",
|
||||||
user,
|
user,
|
||||||
forward_email.website_from,
|
contact.website_from,
|
||||||
alias,
|
address,
|
||||||
)
|
)
|
||||||
send_email(
|
send_email(
|
||||||
# use user mail here as only user is authenticated to see the refused email
|
# use user mail here as only user is authenticated to see the refused email
|
||||||
user.email,
|
user.email,
|
||||||
f"Email from {forward_email.website_from} to {alias} cannot be delivered to your inbox",
|
f"Email from {contact.website_from} to {address} cannot be delivered to your inbox",
|
||||||
render(
|
render(
|
||||||
"transactional/bounced-email.txt",
|
"transactional/bounced-email.txt",
|
||||||
name=user.name,
|
name=user.name,
|
||||||
alias=alias,
|
alias=alias,
|
||||||
website_from=forward_email.website_from,
|
website_from=contact.website_from,
|
||||||
website_email=forward_email.website_email,
|
website_email=contact.website_email,
|
||||||
disable_alias_link=disable_alias_link,
|
disable_alias_link=disable_alias_link,
|
||||||
refused_email_url=refused_email_url,
|
refused_email_url=refused_email_url,
|
||||||
mailbox_email=mailbox_email,
|
mailbox_email=mailbox_email,
|
||||||
|
@ -590,8 +582,8 @@ def handle_bounce(
|
||||||
"transactional/bounced-email.html",
|
"transactional/bounced-email.html",
|
||||||
name=user.name,
|
name=user.name,
|
||||||
alias=alias,
|
alias=alias,
|
||||||
website_from=forward_email.website_from,
|
website_from=contact.website_from,
|
||||||
website_email=forward_email.website_email,
|
website_email=contact.website_email,
|
||||||
disable_alias_link=disable_alias_link,
|
disable_alias_link=disable_alias_link,
|
||||||
refused_email_url=refused_email_url,
|
refused_email_url=refused_email_url,
|
||||||
mailbox_email=mailbox_email,
|
mailbox_email=mailbox_email,
|
||||||
|
@ -603,22 +595,22 @@ def handle_bounce(
|
||||||
elif nb_bounced >= 2:
|
elif nb_bounced >= 2:
|
||||||
LOG.d(
|
LOG.d(
|
||||||
"Bounce happens again with alias %s from %s. Disable alias now ",
|
"Bounce happens again with alias %s from %s. Disable alias now ",
|
||||||
alias,
|
address,
|
||||||
forward_email.website_from,
|
contact.website_from,
|
||||||
)
|
)
|
||||||
gen_email.enabled = False
|
alias.enabled = False
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
send_email(
|
send_email(
|
||||||
# use user mail here as only user is authenticated to see the refused email
|
# use user mail here as only user is authenticated to see the refused email
|
||||||
user.email,
|
user.email,
|
||||||
f"Alias {alias} has been disabled due to second undelivered email from {forward_email.website_from}",
|
f"Alias {address} has been disabled due to second undelivered email from {contact.website_from}",
|
||||||
render(
|
render(
|
||||||
"transactional/automatic-disable-alias.txt",
|
"transactional/automatic-disable-alias.txt",
|
||||||
name=user.name,
|
name=user.name,
|
||||||
alias=alias,
|
alias=alias,
|
||||||
website_from=forward_email.website_from,
|
website_from=contact.website_from,
|
||||||
website_email=forward_email.website_email,
|
website_email=contact.website_email,
|
||||||
refused_email_url=refused_email_url,
|
refused_email_url=refused_email_url,
|
||||||
mailbox_email=mailbox_email,
|
mailbox_email=mailbox_email,
|
||||||
),
|
),
|
||||||
|
@ -626,8 +618,8 @@ def handle_bounce(
|
||||||
"transactional/automatic-disable-alias.html",
|
"transactional/automatic-disable-alias.html",
|
||||||
name=user.name,
|
name=user.name,
|
||||||
alias=alias,
|
alias=alias,
|
||||||
website_from=forward_email.website_from,
|
website_from=contact.website_from,
|
||||||
website_email=forward_email.website_email,
|
website_email=contact.website_email,
|
||||||
refused_email_url=refused_email_url,
|
refused_email_url=refused_email_url,
|
||||||
mailbox_email=mailbox_email,
|
mailbox_email=mailbox_email,
|
||||||
),
|
),
|
||||||
|
|
25
migrations/versions/2020_031709_7744c5c16159_.py
Normal file
25
migrations/versions/2020_031709_7744c5c16159_.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 7744c5c16159
|
||||||
|
Revises: 9081f1a90939
|
||||||
|
Create Date: 2020-03-17 09:52:10.662573
|
||||||
|
|
||||||
|
"""
|
||||||
|
import sqlalchemy_utils
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "7744c5c16159"
|
||||||
|
down_revision = "9081f1a90939"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.rename_table("forward_email", "contact")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.rename_table("contact", "forward_email")
|
56
migrations/versions/2020_031711_0809266d08ca_.py
Normal file
56
migrations/versions/2020_031711_0809266d08ca_.py
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 0809266d08ca
|
||||||
|
Revises: e9395fe234a4
|
||||||
|
Create Date: 2020-03-17 11:56:05.392474
|
||||||
|
|
||||||
|
"""
|
||||||
|
import sqlalchemy_utils
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "0809266d08ca"
|
||||||
|
down_revision = "e9395fe234a4"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# alias_used_on table
|
||||||
|
op.alter_column("alias_used_on", "gen_email_id", new_column_name="alias_id")
|
||||||
|
op.drop_constraint("uq_alias_used", "alias_used_on", type_="unique")
|
||||||
|
op.create_unique_constraint(
|
||||||
|
"uq_alias_used", "alias_used_on", ["alias_id", "hostname"]
|
||||||
|
)
|
||||||
|
op.drop_constraint(
|
||||||
|
"alias_used_on_gen_email_id_fkey", "alias_used_on", type_="foreignkey"
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
None, "alias_used_on", "alias", ["alias_id"], ["id"], ondelete="cascade"
|
||||||
|
)
|
||||||
|
|
||||||
|
# client_user table
|
||||||
|
op.alter_column("client_user", "gen_email_id", new_column_name="alias_id")
|
||||||
|
op.drop_constraint(
|
||||||
|
"client_user_gen_email_id_fkey", "client_user", type_="foreignkey"
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
None, "client_user", "alias", ["alias_id"], ["id"], ondelete="cascade"
|
||||||
|
)
|
||||||
|
|
||||||
|
# contact table
|
||||||
|
op.alter_column("contact", "gen_email_id", new_column_name="alias_id")
|
||||||
|
op.create_unique_constraint("uq_contact", "contact", ["alias_id", "website_email"])
|
||||||
|
op.drop_constraint("uq_forward_email", "contact", type_="unique")
|
||||||
|
op.drop_constraint("forward_email_gen_email_id_fkey", "contact", type_="foreignkey")
|
||||||
|
op.create_foreign_key(
|
||||||
|
None, "contact", "alias", ["alias_id"], ["id"], ondelete="cascade"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# One-way only
|
||||||
|
# Too complex to downgrade
|
||||||
|
raise Exception("Cannot downgrade")
|
31
migrations/versions/2020_031711_14167121af69_.py
Normal file
31
migrations/versions/2020_031711_14167121af69_.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 14167121af69
|
||||||
|
Revises: 7744c5c16159
|
||||||
|
Create Date: 2020-03-17 11:00:00.400334
|
||||||
|
|
||||||
|
"""
|
||||||
|
import sqlalchemy_utils
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "14167121af69"
|
||||||
|
down_revision = "7744c5c16159"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.alter_column("forward_email_log", "forward_id", new_column_name="contact_id")
|
||||||
|
op.drop_constraint(
|
||||||
|
"forward_email_log_forward_id_fkey", "forward_email_log", type_="foreignkey"
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
None, "forward_email_log", "contact", ["contact_id"], ["id"], ondelete="cascade"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.alter_column("forward_email_log", "contact_id", new_column_name="forward_id")
|
25
migrations/versions/2020_031711_6e061eb84167_.py
Normal file
25
migrations/versions/2020_031711_6e061eb84167_.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 6e061eb84167
|
||||||
|
Revises: 14167121af69
|
||||||
|
Create Date: 2020-03-17 11:08:02.004125
|
||||||
|
|
||||||
|
"""
|
||||||
|
import sqlalchemy_utils
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "6e061eb84167"
|
||||||
|
down_revision = "14167121af69"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.rename_table("forward_email_log", "email_log")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.rename_table("email_log", "forward_email_log")
|
25
migrations/versions/2020_031711_e9395fe234a4_.py
Normal file
25
migrations/versions/2020_031711_e9395fe234a4_.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: e9395fe234a4
|
||||||
|
Revises: 6e061eb84167
|
||||||
|
Create Date: 2020-03-17 11:37:33.157695
|
||||||
|
|
||||||
|
"""
|
||||||
|
import sqlalchemy_utils
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "e9395fe234a4"
|
||||||
|
down_revision = "6e061eb84167"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.rename_table("gen_email", "alias")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.rename_table("alias", "gen_email")
|
|
@ -40,7 +40,7 @@ from app.models import (
|
||||||
Client,
|
Client,
|
||||||
User,
|
User,
|
||||||
ClientUser,
|
ClientUser,
|
||||||
GenEmail,
|
Alias,
|
||||||
RedirectUri,
|
RedirectUri,
|
||||||
Subscription,
|
Subscription,
|
||||||
PlanEnum,
|
PlanEnum,
|
||||||
|
@ -160,7 +160,7 @@ def fake_data():
|
||||||
|
|
||||||
user.default_mailbox_id = m1.id
|
user.default_mailbox_id = m1.id
|
||||||
|
|
||||||
GenEmail.create_new(user, "e1@", mailbox_id=m1.id)
|
Alias.create_new(user, "e1@", mailbox_id=m1.id)
|
||||||
|
|
||||||
CustomDomain.create(user_id=user.id, domain="ab.cd", verified=True)
|
CustomDomain.create(user_id=user.id, domain="ab.cd", verified=True)
|
||||||
CustomDomain.create(
|
CustomDomain.create(
|
||||||
|
@ -427,7 +427,7 @@ def init_admin(app):
|
||||||
admin.init_app(app, index_view=SLAdminIndexView())
|
admin.init_app(app, index_view=SLAdminIndexView())
|
||||||
admin.add_view(SLModelView(User, db.session))
|
admin.add_view(SLModelView(User, db.session))
|
||||||
admin.add_view(SLModelView(Client, db.session))
|
admin.add_view(SLModelView(Client, db.session))
|
||||||
admin.add_view(SLModelView(GenEmail, db.session))
|
admin.add_view(SLModelView(Alias, db.session))
|
||||||
admin.add_view(SLModelView(ClientUser, db.session))
|
admin.add_view(SLModelView(ClientUser, db.session))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,14 +2,14 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ render_text("Hi " + name) }}
|
{{ render_text("Hi " + name) }}
|
||||||
{{ render_text("There are at least 2 emails sent to your alias <b>" + alias + "</b> from <b>" + website_email + "</b> that have been <b>refused</b> (or bounced) by your mailbox " + mailbox_email + ".") }}
|
{{ render_text("There are at least 2 emails sent to your alias <b>" + alias.email + "</b> from <b>" + website_email + "</b> that have been <b>refused</b> (or bounced) by your mailbox " + mailbox_email + ".") }}
|
||||||
|
|
||||||
|
|
||||||
{{ render_text("This is usually due to the email being considered as <b>spam</b> by your email provider.") }}
|
{{ render_text("This is usually due to the email being considered as <b>spam</b> by your email provider.") }}
|
||||||
|
|
||||||
{{ render_button("View the refused email", refused_email_url) }}
|
{{ render_button("View the refused email", refused_email_url) }}
|
||||||
|
|
||||||
{{ render_text('As security measure, we have <b>disabled</b> the alias ' + alias + ".") }}
|
{{ render_text('As security measure, we have <b>disabled</b> the alias ' + alias.email + ".") }}
|
||||||
|
|
||||||
{{ render_text('Please let us know if you have any question.') }}
|
{{ render_text('Please let us know if you have any question.') }}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
Hi {{name}}
|
Hi {{name}}
|
||||||
|
|
||||||
There are at least 2 emails sent to your alias {{alias}} from {{website_from}} that have been refused (or bounced) by your mailbox {{mailbox_email}}.
|
There are at least 2 emails sent to your alias {{alias.email}} from {{website_from}} that have been refused (or bounced) by your mailbox {{mailbox_email}}.
|
||||||
|
|
||||||
This is usually due to the email being considered as spam by your email provider.
|
This is usually due to the email being considered as spam by your email provider.
|
||||||
You can view this email here:
|
You can view this email here:
|
||||||
{{ refused_email_url }}
|
{{ refused_email_url }}
|
||||||
|
|
||||||
|
|
||||||
As security measure, we have disabled the alias {{alias}}.
|
As security measure, we have disabled the alias {{alias.email}}.
|
||||||
|
|
||||||
Please let us know if you have any question.
|
Please let us know if you have any question.
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ render_text("Hi " + name) }}
|
{{ render_text("Hi " + name) }}
|
||||||
{{ render_text("An email sent to your alias <b>" + alias + "</b> from <b>" + website_email + "</b> was <b>refused</b> (or bounced) by your mailbox " + mailbox_email + ".") }}
|
{{ render_text("An email sent to your alias <b>" + alias.email + "</b> from <b>" + website_email + "</b> was <b>refused</b> (or bounced) by your mailbox " + mailbox_email + ".") }}
|
||||||
|
|
||||||
{{ render_text('This is usually due to the email being considered as <b>spam</b> by your email provider.') }}
|
{{ render_text('This is usually due to the email being considered as <b>spam</b> by your email provider.') }}
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
{{ render_text('1. If the email is not spam at all, it means your email provider has wrongly classified it as spam. In this case you can <b>create a filter to whitelist</b> it. The filter could be based on the sender, email subject, etc. As how to create the filter differs for each email provider, please check with your email provider on how to whitelist an email. Let us know if you need help to setup the filter by replying to this email.') }}
|
{{ render_text('1. If the email is not spam at all, it means your email provider has wrongly classified it as spam. In this case you can <b>create a filter to whitelist</b> it. The filter could be based on the sender, email subject, etc. As how to create the filter differs for each email provider, please check with your email provider on how to whitelist an email. Let us know if you need help to setup the filter by replying to this email.') }}
|
||||||
|
|
||||||
{{ render_text('2. If this email is indeed spam, it means your alias <b>' + alias + '</b> is now in the hands of a spammer. In this case, you should <b>disable</b> or delete the alias immediately. Or, do nothing and we\'ll <b>automatically</b> disable this alias the second time the email is refused. Don\'t worry, we\'ll send you another email when that happens.') }}
|
{{ render_text('2. If this email is indeed spam, it means your alias <b>' + alias.email + '</b> is now in the hands of a spammer. In this case, you should <b>disable</b> or delete the alias immediately. Or, do nothing and we\'ll <b>automatically</b> disable this alias the second time the email is refused. Don\'t worry, we\'ll send you another email when that happens.') }}
|
||||||
|
|
||||||
{{ render_button("Disable alias", disable_alias_link) }}
|
{{ render_button("Disable alias", disable_alias_link) }}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
Hi {{name}}
|
Hi {{name}}
|
||||||
|
|
||||||
An email sent to your alias {{alias}} from {{website_from}} was refused (or bounced) by your mailbox {{mailbox_email}}.
|
An email sent to your alias {{alias.email}} from {{website_from}} was refused (or bounced) by your mailbox {{mailbox_email}}.
|
||||||
|
|
||||||
This is usually due to the email being considered as spam by your email provider.
|
This is usually due to the email being considered as spam by your email provider.
|
||||||
You can view this email here:
|
You can view this email here:
|
||||||
|
|
|
@ -4,11 +4,11 @@ from flask import url_for
|
||||||
|
|
||||||
from app.config import EMAIL_DOMAIN, MAX_NB_EMAIL_FREE_PLAN, PAGE_LIMIT
|
from app.config import EMAIL_DOMAIN, MAX_NB_EMAIL_FREE_PLAN, PAGE_LIMIT
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models import User, ApiKey, GenEmail, ForwardEmail, ForwardEmailLog
|
from app.models import User, ApiKey, Alias, Contact, EmailLog
|
||||||
from app.utils import random_word
|
from app.utils import random_word
|
||||||
|
|
||||||
|
|
||||||
def test_error_without_pagination(flask_client):
|
def test_get_aliases_error_without_pagination(flask_client):
|
||||||
user = User.create(
|
user = User.create(
|
||||||
email="a@b.c", password="password", name="Test User", activated=True
|
email="a@b.c", password="password", name="Test User", activated=True
|
||||||
)
|
)
|
||||||
|
@ -26,7 +26,7 @@ def test_error_without_pagination(flask_client):
|
||||||
assert r.json["error"]
|
assert r.json["error"]
|
||||||
|
|
||||||
|
|
||||||
def test_success_with_pagination(flask_client):
|
def test_get_aliases_with_pagination(flask_client):
|
||||||
user = User.create(
|
user = User.create(
|
||||||
email="a@b.c", password="password", name="Test User", activated=True
|
email="a@b.c", password="password", name="Test User", activated=True
|
||||||
)
|
)
|
||||||
|
@ -38,7 +38,7 @@ def test_success_with_pagination(flask_client):
|
||||||
|
|
||||||
# create more aliases than PAGE_LIMIT
|
# create more aliases than PAGE_LIMIT
|
||||||
for _ in range(PAGE_LIMIT + 1):
|
for _ in range(PAGE_LIMIT + 1):
|
||||||
GenEmail.create_new_random(user)
|
Alias.create_new_random(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# get aliases on the 1st page, should return PAGE_LIMIT aliases
|
# get aliases on the 1st page, should return PAGE_LIMIT aliases
|
||||||
|
@ -70,6 +70,38 @@ def test_success_with_pagination(flask_client):
|
||||||
assert len(r.json["aliases"]) == 2
|
assert len(r.json["aliases"]) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_aliases_with_pagination(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 more aliases than PAGE_LIMIT
|
||||||
|
Alias.create_new(user, "prefix1")
|
||||||
|
Alias.create_new(user, "prefix2")
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# get aliases without query, should return 3 aliases as one alias is created when user is created
|
||||||
|
r = flask_client.get(
|
||||||
|
url_for("api.get_aliases", page_id=0), headers={"Authentication": api_key.code}
|
||||||
|
)
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert len(r.json["aliases"]) == 3
|
||||||
|
|
||||||
|
# get aliases with "prefix1" query, should return 1 alias
|
||||||
|
r = flask_client.get(
|
||||||
|
url_for("api.get_aliases", page_id=0),
|
||||||
|
headers={"Authentication": api_key.code},
|
||||||
|
json={"query": "prefix1"},
|
||||||
|
)
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert len(r.json["aliases"]) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_delete_alias(flask_client):
|
def test_delete_alias(flask_client):
|
||||||
user = User.create(
|
user = User.create(
|
||||||
email="a@b.c", password="password", name="Test User", activated=True
|
email="a@b.c", password="password", name="Test User", activated=True
|
||||||
|
@ -80,11 +112,11 @@ def test_delete_alias(flask_client):
|
||||||
api_key = ApiKey.create(user.id, "for test")
|
api_key = ApiKey.create(user.id, "for test")
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
gen_email = GenEmail.create_new_random(user)
|
alias = Alias.create_new_random(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
r = flask_client.delete(
|
r = flask_client.delete(
|
||||||
url_for("api.delete_alias", alias_id=gen_email.id),
|
url_for("api.delete_alias", alias_id=alias.id),
|
||||||
headers={"Authentication": api_key.code},
|
headers={"Authentication": api_key.code},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -102,11 +134,11 @@ def test_toggle_alias(flask_client):
|
||||||
api_key = ApiKey.create(user.id, "for test")
|
api_key = ApiKey.create(user.id, "for test")
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
gen_email = GenEmail.create_new_random(user)
|
alias = Alias.create_new_random(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
r = flask_client.post(
|
r = flask_client.post(
|
||||||
url_for("api.toggle_alias", alias_id=gen_email.id),
|
url_for("api.toggle_alias", alias_id=alias.id),
|
||||||
headers={"Authentication": api_key.code},
|
headers={"Authentication": api_key.code},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -124,25 +156,25 @@ def test_alias_activities(flask_client):
|
||||||
api_key = ApiKey.create(user.id, "for test")
|
api_key = ApiKey.create(user.id, "for test")
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
gen_email = GenEmail.create_new_random(user)
|
alias = Alias.create_new_random(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# create some alias log
|
# create some alias log
|
||||||
forward_email = ForwardEmail.create(
|
contact = Contact.create(
|
||||||
website_email="marketing@example.com",
|
website_email="marketing@example.com",
|
||||||
reply_email="reply@a.b",
|
reply_email="reply@a.b",
|
||||||
gen_email_id=gen_email.id,
|
alias_id=alias.id,
|
||||||
)
|
)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
for _ in range(int(PAGE_LIMIT / 2)):
|
for _ in range(int(PAGE_LIMIT / 2)):
|
||||||
ForwardEmailLog.create(forward_id=forward_email.id, is_reply=True)
|
EmailLog.create(contact_id=contact.id, is_reply=True)
|
||||||
|
|
||||||
for _ in range(int(PAGE_LIMIT / 2) + 2):
|
for _ in range(int(PAGE_LIMIT / 2) + 2):
|
||||||
ForwardEmailLog.create(forward_id=forward_email.id, blocked=True)
|
EmailLog.create(contact_id=contact.id, blocked=True)
|
||||||
|
|
||||||
r = flask_client.get(
|
r = flask_client.get(
|
||||||
url_for("api.get_alias_activities", alias_id=gen_email.id, page_id=0),
|
url_for("api.get_alias_activities", alias_id=alias.id, page_id=0),
|
||||||
headers={"Authentication": api_key.code},
|
headers={"Authentication": api_key.code},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -156,7 +188,7 @@ def test_alias_activities(flask_client):
|
||||||
|
|
||||||
# second page, should return 1 or 2 results only
|
# second page, should return 1 or 2 results only
|
||||||
r = flask_client.get(
|
r = flask_client.get(
|
||||||
url_for("api.get_alias_activities", alias_id=gen_email.id, page_id=1),
|
url_for("api.get_alias_activities", alias_id=alias.id, page_id=1),
|
||||||
headers={"Authentication": api_key.code},
|
headers={"Authentication": api_key.code},
|
||||||
)
|
)
|
||||||
assert len(r.json["activities"]) < 3
|
assert len(r.json["activities"]) < 3
|
||||||
|
@ -172,11 +204,11 @@ def test_update_alias(flask_client):
|
||||||
api_key = ApiKey.create(user.id, "for test")
|
api_key = ApiKey.create(user.id, "for test")
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
gen_email = GenEmail.create_new_random(user)
|
alias = Alias.create_new_random(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
r = flask_client.put(
|
r = flask_client.put(
|
||||||
url_for("api.update_alias", alias_id=gen_email.id),
|
url_for("api.update_alias", alias_id=alias.id),
|
||||||
headers={"Authentication": api_key.code},
|
headers={"Authentication": api_key.code},
|
||||||
json={"note": "test note"},
|
json={"note": "test note"},
|
||||||
)
|
)
|
||||||
|
@ -195,23 +227,23 @@ def test_alias_contacts(flask_client):
|
||||||
api_key = ApiKey.create(user.id, "for test")
|
api_key = ApiKey.create(user.id, "for test")
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
gen_email = GenEmail.create_new_random(user)
|
alias = Alias.create_new_random(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# create some alias log
|
# create some alias log
|
||||||
for i in range(PAGE_LIMIT + 1):
|
for i in range(PAGE_LIMIT + 1):
|
||||||
forward_email = ForwardEmail.create(
|
contact = Contact.create(
|
||||||
website_email=f"marketing-{i}@example.com",
|
website_email=f"marketing-{i}@example.com",
|
||||||
reply_email=f"reply-{i}@a.b",
|
reply_email=f"reply-{i}@a.b",
|
||||||
gen_email_id=gen_email.id,
|
alias_id=alias.id,
|
||||||
)
|
)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
ForwardEmailLog.create(forward_id=forward_email.id, is_reply=True)
|
EmailLog.create(contact_id=contact.id, is_reply=True)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
r = flask_client.get(
|
r = flask_client.get(
|
||||||
url_for("api.get_alias_contacts_route", alias_id=gen_email.id, page_id=0),
|
url_for("api.get_alias_contacts_route", alias_id=alias.id, page_id=0),
|
||||||
headers={"Authentication": api_key.code},
|
headers={"Authentication": api_key.code},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -227,7 +259,7 @@ def test_alias_contacts(flask_client):
|
||||||
|
|
||||||
# second page, should return 1 result only
|
# second page, should return 1 result only
|
||||||
r = flask_client.get(
|
r = flask_client.get(
|
||||||
url_for("api.get_alias_contacts_route", alias_id=gen_email.id, page_id=1),
|
url_for("api.get_alias_contacts_route", alias_id=alias.id, page_id=1),
|
||||||
headers={"Authentication": api_key.code},
|
headers={"Authentication": api_key.code},
|
||||||
)
|
)
|
||||||
assert len(r.json["contacts"]) == 1
|
assert len(r.json["contacts"]) == 1
|
||||||
|
@ -243,11 +275,11 @@ def test_create_contact_route(flask_client):
|
||||||
api_key = ApiKey.create(user.id, "for test")
|
api_key = ApiKey.create(user.id, "for test")
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
gen_email = GenEmail.create_new_random(user)
|
alias = Alias.create_new_random(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
r = flask_client.post(
|
r = flask_client.post(
|
||||||
url_for("api.create_contact_route", alias_id=gen_email.id),
|
url_for("api.create_contact_route", alias_id=alias.id),
|
||||||
headers={"Authentication": api_key.code},
|
headers={"Authentication": api_key.code},
|
||||||
json={"contact": "First Last <first@example.com>"},
|
json={"contact": "First Last <first@example.com>"},
|
||||||
)
|
)
|
||||||
|
@ -262,8 +294,37 @@ def test_create_contact_route(flask_client):
|
||||||
|
|
||||||
# re-add a contact, should return 409
|
# re-add a contact, should return 409
|
||||||
r = flask_client.post(
|
r = flask_client.post(
|
||||||
url_for("api.create_contact_route", alias_id=gen_email.id),
|
url_for("api.create_contact_route", alias_id=alias.id),
|
||||||
headers={"Authentication": api_key.code},
|
headers={"Authentication": api_key.code},
|
||||||
json={"contact": "First2 Last2 <first@example.com>"},
|
json={"contact": "First2 Last2 <first@example.com>"},
|
||||||
)
|
)
|
||||||
assert r.status_code == 409
|
assert r.status_code == 409
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_contact(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()
|
||||||
|
|
||||||
|
alias = Alias.create_new_random(user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
contact = Contact.create(
|
||||||
|
alias_id=alias.id,
|
||||||
|
website_email="contact@example.com",
|
||||||
|
reply_email="reply+random@sl.io",
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
r = flask_client.delete(
|
||||||
|
url_for("api.delete_contact", contact_id=contact.id),
|
||||||
|
headers={"Authentication": api_key.code},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json == {"deleted": True}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
|
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models import User, ApiKey, AliasUsedOn, GenEmail
|
from app.models import User, ApiKey, AliasUsedOn, Alias
|
||||||
|
|
||||||
|
|
||||||
def test_different_scenarios(flask_client):
|
def test_different_scenarios(flask_client):
|
||||||
|
@ -40,9 +40,9 @@ def test_different_scenarios(flask_client):
|
||||||
assert r.json["custom"]["suggestion"] == "test"
|
assert r.json["custom"]["suggestion"] == "test"
|
||||||
|
|
||||||
# <<< with recommendation >>>
|
# <<< with recommendation >>>
|
||||||
alias = GenEmail.create_new(user, prefix="test")
|
alias = Alias.create_new(user, prefix="test")
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
AliasUsedOn.create(gen_email_id=alias.id, hostname="www.test.com")
|
AliasUsedOn.create(alias_id=alias.id, hostname="www.test.com")
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
r = flask_client.get(
|
r = flask_client.get(
|
||||||
|
@ -85,9 +85,9 @@ def test_different_scenarios_v2(flask_client):
|
||||||
assert r.json["prefix_suggestion"] == "test"
|
assert r.json["prefix_suggestion"] == "test"
|
||||||
|
|
||||||
# <<< with recommendation >>>
|
# <<< with recommendation >>>
|
||||||
alias = GenEmail.create_new(user, prefix="test")
|
alias = Alias.create_new(user, prefix="test")
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
AliasUsedOn.create(gen_email_id=alias.id, hostname="www.test.com")
|
AliasUsedOn.create(alias_id=alias.id, hostname="www.test.com")
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
r = flask_client.get(
|
r = flask_client.get(
|
||||||
|
|
|
@ -200,3 +200,19 @@ def test_auth_reactivate_success(flask_client):
|
||||||
assert act_code
|
assert act_code
|
||||||
assert len(act_code.code) == 6
|
assert len(act_code.code) == 6
|
||||||
assert act_code.tries == 3
|
assert act_code.tries == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_auth_login_forgot_password(flask_client):
|
||||||
|
User.create(email="a@b.c", password="password", name="Test User", activated=True)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
r = flask_client.post(url_for("api.forgot_password"), json={"email": "a@b.c"},)
|
||||||
|
|
||||||
|
assert r.status_code == 200
|
||||||
|
|
||||||
|
# No such email, still return 200
|
||||||
|
r = flask_client.post(
|
||||||
|
url_for("api.forgot_password"), json={"email": "not-exist@b.c"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert r.status_code == 200
|
||||||
|
|
|
@ -2,7 +2,7 @@ from flask import url_for
|
||||||
|
|
||||||
from app.config import EMAIL_DOMAIN, MAX_NB_EMAIL_FREE_PLAN
|
from app.config import EMAIL_DOMAIN, MAX_NB_EMAIL_FREE_PLAN
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models import User, ApiKey, GenEmail
|
from app.models import User, ApiKey, Alias
|
||||||
from app.utils import random_word
|
from app.utils import random_word
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ def test_success(flask_client):
|
||||||
assert r.status_code == 201
|
assert r.status_code == 201
|
||||||
assert r.json["alias"] == f"prefix.{word}@{EMAIL_DOMAIN}"
|
assert r.json["alias"] == f"prefix.{word}@{EMAIL_DOMAIN}"
|
||||||
|
|
||||||
new_ge = GenEmail.get_by(email=r.json["alias"])
|
new_ge = Alias.get_by(email=r.json["alias"])
|
||||||
assert new_ge.note == "test note"
|
assert new_ge.note == "test note"
|
||||||
|
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ def test_create_custom_alias_without_note(flask_client):
|
||||||
assert r.status_code == 201
|
assert r.status_code == 201
|
||||||
assert r.json["alias"] == f"prefix.{word}@{EMAIL_DOMAIN}"
|
assert r.json["alias"] == f"prefix.{word}@{EMAIL_DOMAIN}"
|
||||||
|
|
||||||
new_ge = GenEmail.get_by(email=r.json["alias"])
|
new_ge = Alias.get_by(email=r.json["alias"])
|
||||||
assert new_ge.note is None
|
assert new_ge.note is None
|
||||||
|
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ def test_out_of_quota(flask_client):
|
||||||
|
|
||||||
# create MAX_NB_EMAIL_FREE_PLAN custom alias to run out of quota
|
# create MAX_NB_EMAIL_FREE_PLAN custom alias to run out of quota
|
||||||
for _ in range(MAX_NB_EMAIL_FREE_PLAN):
|
for _ in range(MAX_NB_EMAIL_FREE_PLAN):
|
||||||
GenEmail.create_new(user, prefix="test")
|
Alias.create_new(user, prefix="test")
|
||||||
|
|
||||||
word = random_word()
|
word = random_word()
|
||||||
r = flask_client.post(
|
r = flask_client.post(
|
||||||
|
|
|
@ -4,7 +4,7 @@ from flask import url_for
|
||||||
|
|
||||||
from app.config import EMAIL_DOMAIN, MAX_NB_EMAIL_FREE_PLAN
|
from app.config import EMAIL_DOMAIN, MAX_NB_EMAIL_FREE_PLAN
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models import User, ApiKey, GenEmail
|
from app.models import User, ApiKey, Alias
|
||||||
|
|
||||||
|
|
||||||
def test_success(flask_client):
|
def test_success(flask_client):
|
||||||
|
@ -57,7 +57,7 @@ def test_custom_mode(flask_client):
|
||||||
|
|
||||||
assert r.status_code == 201
|
assert r.status_code == 201
|
||||||
alias = r.json["alias"]
|
alias = r.json["alias"]
|
||||||
ge = GenEmail.get_by(email=alias)
|
ge = Alias.get_by(email=alias)
|
||||||
assert ge.note == "test note"
|
assert ge.note == "test note"
|
||||||
|
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ def test_out_of_quota(flask_client):
|
||||||
|
|
||||||
# create MAX_NB_EMAIL_FREE_PLAN random alias to run out of quota
|
# create MAX_NB_EMAIL_FREE_PLAN random alias to run out of quota
|
||||||
for _ in range(MAX_NB_EMAIL_FREE_PLAN):
|
for _ in range(MAX_NB_EMAIL_FREE_PLAN):
|
||||||
GenEmail.create_new(user, prefix="test1")
|
Alias.create_new(user, prefix="test1")
|
||||||
|
|
||||||
r = flask_client.post(
|
r = flask_client.post(
|
||||||
url_for("api.new_random_alias", hostname="www.test.com"),
|
url_for("api.new_random_alias", hostname="www.test.com"),
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
|
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models import User, ApiKey, AliasUsedOn, GenEmail
|
from app.models import User, ApiKey, AliasUsedOn, Alias
|
||||||
|
|
||||||
|
|
||||||
def test_user_in_trial(flask_client):
|
def test_user_in_trial(flask_client):
|
||||||
|
@ -19,7 +19,12 @@ def test_user_in_trial(flask_client):
|
||||||
)
|
)
|
||||||
|
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
assert r.json == {"is_premium": True, "name": "Test User"}
|
assert r.json == {
|
||||||
|
"is_premium": True,
|
||||||
|
"name": "Test User",
|
||||||
|
"email": "a@b.c",
|
||||||
|
"in_trial": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_wrong_api_key(flask_client):
|
def test_wrong_api_key(flask_client):
|
||||||
|
|
|
@ -5,7 +5,7 @@ import pytest
|
||||||
|
|
||||||
from app.config import EMAIL_DOMAIN, MAX_NB_EMAIL_FREE_PLAN
|
from app.config import EMAIL_DOMAIN, MAX_NB_EMAIL_FREE_PLAN
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models import generate_email, User, GenEmail
|
from app.models import generate_email, User, Alias
|
||||||
|
|
||||||
|
|
||||||
def test_generate_email(flask_client):
|
def test_generate_email(flask_client):
|
||||||
|
@ -42,24 +42,24 @@ def test_suggested_emails_for_user_who_cannot_create_new_alias(flask_client):
|
||||||
|
|
||||||
# make sure user runs out of quota to create new email
|
# make sure user runs out of quota to create new email
|
||||||
for i in range(MAX_NB_EMAIL_FREE_PLAN):
|
for i in range(MAX_NB_EMAIL_FREE_PLAN):
|
||||||
GenEmail.create_new(user=user, prefix="test")
|
Alias.create_new(user=user, prefix="test")
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
suggested_email, other_emails = user.suggested_emails(website_name="test")
|
suggested_email, other_emails = user.suggested_emails(website_name="test")
|
||||||
|
|
||||||
# the suggested email is chosen from existing GenEmail
|
# the suggested email is chosen from existing Alias
|
||||||
assert GenEmail.get_by(email=suggested_email)
|
assert Alias.get_by(email=suggested_email)
|
||||||
|
|
||||||
# all other emails are generated emails
|
# all other emails are generated emails
|
||||||
for email in other_emails:
|
for email in other_emails:
|
||||||
assert GenEmail.get_by(email=email)
|
assert Alias.get_by(email=email)
|
||||||
|
|
||||||
|
|
||||||
def test_gen_email_create_random(flask_client):
|
def test_alias_create_random(flask_client):
|
||||||
user = User.create(
|
user = User.create(
|
||||||
email="a@b.c", password="password", name="Test User", activated=True
|
email="a@b.c", password="password", name="Test User", activated=True
|
||||||
)
|
)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
alias = GenEmail.create_new_random(user)
|
alias = Alias.create_new_random(user)
|
||||||
assert alias.email.endswith(EMAIL_DOMAIN)
|
assert alias.email.endswith(EMAIL_DOMAIN)
|
||||||
|
|
Loading…
Reference in a new issue