Merge pull request #135 from simple-login/alias-pagination

Alias pagination, support sorting
This commit is contained in:
Son Nguyen Kim 2020-04-25 14:02:50 +02:00 committed by GitHub
commit 51676f02b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 220 additions and 160 deletions

View File

@ -1009,7 +1009,8 @@ Update alias note. In the future, the endpoint will support other updates (e.g.
Input: Input:
- `Authentication` header that contains the api key - `Authentication` header that contains the api key
- `alias_id` in url. - `alias_id` in url.
- `note` in request body - (optional) `note` in request body
- (optional) `mailbox_id` in request body
Output: Output:
If success, return 200 If success, return 200

View File

@ -3,6 +3,7 @@ from functools import wraps
import arrow import arrow
from flask import Blueprint, request, jsonify, g from flask import Blueprint, request, jsonify, g
from flask_login import current_user from flask_login import current_user
from app.extensions import db from app.extensions import db
from app.models import ApiKey from app.models import ApiKey

View File

@ -2,6 +2,7 @@ from dataclasses import dataclass
from arrow import Arrow from arrow import Arrow
from sqlalchemy import or_, func, case from sqlalchemy import or_, func, case
from sqlalchemy.orm import joinedload
from app.config import PAGE_LIMIT from app.config import PAGE_LIMIT
from app.extensions import db from app.extensions import db
@ -11,6 +12,7 @@ from app.models import Alias, Contact, EmailLog, Mailbox
@dataclass @dataclass
class AliasInfo: class AliasInfo:
alias: Alias alias: Alias
mailbox: Mailbox
nb_forward: int nb_forward: int
nb_blocked: int nb_blocked: int
@ -89,6 +91,7 @@ def get_alias_infos_with_pagination(user, page_id=0, query=None) -> [AliasInfo]:
ret = [] ret = []
q = ( q = (
db.session.query(Alias) db.session.query(Alias)
.options(joinedload(Alias.mailbox))
.filter(Alias.user_id == user.id) .filter(Alias.user_id == user.id)
.order_by(Alias.created_at.desc()) .order_by(Alias.created_at.desc())
) )
@ -106,7 +109,9 @@ def get_alias_infos_with_pagination(user, page_id=0, query=None) -> [AliasInfo]:
return ret return ret
def get_alias_infos_with_pagination_v2(user, page_id=0, query=None) -> [AliasInfo]: def get_alias_infos_with_pagination_v2(
user, page_id=0, query=None, sort=None
) -> [AliasInfo]:
ret = [] ret = []
latest_activity = func.max( latest_activity = func.max(
case( case(
@ -119,12 +124,11 @@ def get_alias_infos_with_pagination_v2(user, page_id=0, query=None) -> [AliasInf
).label("latest") ).label("latest")
q = ( q = (
db.session.query(Alias, latest_activity) db.session.query(Alias, Mailbox, latest_activity)
.join(Contact, Alias.id == Contact.alias_id, isouter=True) .join(Contact, Alias.id == Contact.alias_id, isouter=True)
.join(EmailLog, Contact.id == EmailLog.contact_id, isouter=True) .join(EmailLog, Contact.id == EmailLog.contact_id, isouter=True)
.filter(Alias.user_id == user.id) .filter(Alias.user_id == user.id)
.group_by(Alias.id) .filter(Alias.mailbox_id == Mailbox.id)
.order_by(latest_activity.desc())
) )
if query: if query:
@ -132,10 +136,18 @@ def get_alias_infos_with_pagination_v2(user, page_id=0, query=None) -> [AliasInf
or_(Alias.email.ilike(f"%{query}%"), Alias.note.ilike(f"%{query}%")) or_(Alias.email.ilike(f"%{query}%"), Alias.note.ilike(f"%{query}%"))
) )
if sort == "old2new":
q = q.order_by(Alias.created_at)
else:
# default sorting
q = q.order_by(latest_activity.desc())
q = q.group_by(Alias.id, Mailbox.id)
q = q.limit(PAGE_LIMIT).offset(page_id * PAGE_LIMIT) q = q.limit(PAGE_LIMIT).offset(page_id * PAGE_LIMIT)
for alias, latest_activity in q: for alias, mailbox, latest_activity in q:
ret.append(get_alias_info_v2(alias)) ret.append(get_alias_info_v2(alias, mailbox))
return ret return ret
@ -147,7 +159,9 @@ def get_alias_info(alias: Alias) -> AliasInfo:
.filter(EmailLog.contact_id == Contact.id) .filter(EmailLog.contact_id == Contact.id)
) )
alias_info = AliasInfo(alias=alias, nb_blocked=0, nb_forward=0, nb_reply=0,) alias_info = AliasInfo(
alias=alias, nb_blocked=0, nb_forward=0, nb_reply=0, mailbox=alias.mailbox
)
for _, el in q: for _, el in q:
if el.is_reply: if el.is_reply:
@ -160,7 +174,7 @@ def get_alias_info(alias: Alias) -> AliasInfo:
return alias_info return alias_info
def get_alias_info_v2(alias: Alias) -> AliasInfo: def get_alias_info_v2(alias: Alias, mailbox) -> AliasInfo:
q = ( q = (
db.session.query(Contact, EmailLog) db.session.query(Contact, EmailLog)
.filter(Contact.alias_id == alias.id) .filter(Contact.alias_id == alias.id)
@ -171,7 +185,9 @@ def get_alias_info_v2(alias: Alias) -> AliasInfo:
latest_email_log = None latest_email_log = None
latest_contact = None latest_contact = None
alias_info = AliasInfo(alias=alias, nb_blocked=0, nb_forward=0, nb_reply=0,) alias_info = AliasInfo(
alias=alias, nb_blocked=0, nb_forward=0, nb_reply=0, mailbox=mailbox
)
for contact, email_log in q: for contact, email_log in q:
if email_log.is_reply: if email_log.is_reply:

View File

@ -19,7 +19,7 @@ from app.dashboard.views.alias_log import get_alias_log
from app.email_utils import parseaddr_unicode from app.email_utils import parseaddr_unicode
from app.extensions import db from app.extensions import db
from app.log import LOG from app.log import LOG
from app.models import Alias, Contact from app.models import Alias, Contact, Mailbox
from app.utils import random_string from app.utils import random_string
@ -234,8 +234,6 @@ def update_alias(alias_id):
note: in body note: in body
Output: Output:
200 200
""" """
data = request.get_json() data = request.get_json()
if not data: if not data:
@ -247,11 +245,25 @@ def update_alias(alias_id):
if alias.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") changed = False
alias.note = new_note if "note" in data:
db.session.commit() new_note = data.get("note")
alias.note = new_note
changed = True
return jsonify(note=new_note), 200 if "mailbox_id" in data:
mailbox_id = int(data.get("mailbox_id"))
mailbox = Mailbox.get(mailbox_id)
if not mailbox or mailbox.user_id != user.id or not mailbox.verified:
return jsonify(error="Forbidden"), 400
alias.mailbox_id = mailbox_id
changed = True
if changed:
db.session.commit()
return jsonify(ok=True), 200
@api_bp.route("/aliases/<int:alias_id>", methods=["GET"]) @api_bp.route("/aliases/<int:alias_id>", methods=["GET"])
@ -374,8 +386,6 @@ def delete_contact(contact_id):
contact_id: in url contact_id: in url
Output: Output:
200 200
""" """
user = g.user user = g.user
contact = Contact.get(contact_id) contact = Contact.get(contact_id)

View File

@ -1,7 +1,7 @@
import pyotp import pyotp
from flask import jsonify, request from flask import jsonify, request
from flask_cors import cross_origin from flask_cors import cross_origin
from itsdangerous import Signer, BadSignature from itsdangerous import Signer
from app.api.base import api_bp from app.api.base import api_bp
from app.config import FLASK_SECRET from app.config import FLASK_SECRET

View File

@ -6,11 +6,11 @@ from wtforms import StringField, validators
from app import email_utils, config from app import email_utils, config
from app.auth.base import auth_bp from app.auth.base import auth_bp
from app.auth.views.login_utils import get_referral from app.auth.views.login_utils import get_referral
from app.config import URL, DISABLE_REGISTRATION from app.config import URL
from app.email_utils import can_be_used_as_personal_email, email_already_used from app.email_utils import can_be_used_as_personal_email, email_already_used
from app.extensions import db from app.extensions import db
from app.log import LOG from app.log import LOG
from app.models import User, ActivationCode, Referral from app.models import User, ActivationCode
from app.utils import random_string, encode_url from app.utils import random_string, encode_url

View File

@ -1,4 +1,3 @@
import arrow
from flask import request, flash, render_template, redirect, url_for from flask import request, flash, render_template, redirect, url_for
from flask_login import login_user from flask_login import login_user
from flask_wtf import FlaskForm from flask_wtf import FlaskForm

View File

@ -2,8 +2,6 @@ import os
import random import random
import string import string
import subprocess import subprocess
import tempfile
from uuid import uuid4
from dotenv import load_dotenv from dotenv import load_dotenv

View File

@ -63,8 +63,19 @@
<div class="col-lg-6 pt-1"> <div class="col-lg-6 pt-1">
<div class="float-right"> <div class="float-right">
<form method="get" class="form-inline"> <form method="get" class="form-inline">
<select name="sort"
onchange="this.form.submit()"
class="form-control custom-select mr-3">
<option value="" {% if sort == "" %} selected {% endif %}>
Sort by most recent activity
</option>
<option value="old2new" {% if sort == "old2new" %} selected {% endif %}>
Oldest Alias to Newest
</option>
</select>
<input type="search" name="query" placeholder="Enter to search for alias" <input type="search" name="query" placeholder="Enter to search for alias"
class="form-control shadow text-right" class="form-control shadow"
style="max-width: 15em" style="max-width: 15em"
value="{{ query }}"> value="{{ query }}">
</form> </form>
@ -77,7 +88,7 @@
{% set alias = alias_info.alias %} {% 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.alias.id == highlight_alias_id %} highlight-row {% endif %} ">
<div class="row"> <div class="row">
<div class="col-8"> <div class="col-8">
@ -179,7 +190,8 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<a href="{{ url_for('dashboard.alias_contact_manager', alias_id=alias.id) }}" <a href="{{ url_for('dashboard.alias_contact_manager', alias_id=alias.id) }}"
{% if alias_info.show_intro_test_send_email %} id="send-email-{{ alias.id }}"
{% if loop.index ==1 %}
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>
To send an email to your contact, SimpleLogin will create a <em>special</em> email address. <br><br> To send an email to your contact, SimpleLogin will create a <em>special</em> email address. <br><br>
@ -210,10 +222,11 @@
<div class="small-text mt-2">Current mailbox</div> <div class="small-text mt-2">Current mailbox</div>
<div class="d-flex"> <div class="d-flex">
<div class="flex-grow-1 mr-2"> <div class="flex-grow-1 mr-2">
<select class="form-control form-control-sm custom-select" name="mailbox"> <select id="mailbox-{{ alias.id }}"
class="form-control form-control-sm custom-select" name="mailbox">
{% for mailbox in mailboxes %} {% for mailbox in mailboxes %}
<option value="{{ mailbox }}" {% if mailbox == alias_info.mailbox.email %} selected {% endif %}> <option value="{{ mailbox.id }}" {% if mailbox.id == alias_info.mailbox.id %} selected {% endif %}>
{{ mailbox }} {{ mailbox.email }}
</option> </option>
{% endfor %} {% endfor %}
</select> </select>
@ -223,9 +236,10 @@
<input type="hidden" name="form-name" value="set-mailbox"> <input type="hidden" name="form-name" value="set-mailbox">
<input type="hidden" name="alias-id" value="{{ alias.id }}"> <input type="hidden" name="alias-id" value="{{ alias.id }}">
<button class="btn btn-sm btn-outline-info w-100"> <a data-alias="{{ alias.id }}"
class="save-mailbox btn btn-sm btn-outline-info w-100">
Update Update
</button> </a>
</div> </div>
</div> </div>
@ -241,6 +255,7 @@
<div class="flex-grow-1 mr-2"> <div class="flex-grow-1 mr-2">
<textarea <textarea
id="note-{{ alias.id }}"
name="note" name="note"
class="form-control" class="form-control"
rows="2" rows="2"
@ -251,9 +266,10 @@
<input type="hidden" name="form-name" value="set-note"> <input type="hidden" name="form-name" value="set-note">
<input type="hidden" name="alias-id" value="{{ alias.id }}"> <input type="hidden" name="alias-id" value="{{ alias.id }}">
<button class="btn btn-sm btn-outline-success w-100"> <a data-alias="{{ alias.id }}"
class="save-note btn btn-sm btn-outline-success w-100">
Save Save
</button> </a>
</div> </div>
</div> </div>
</form> </form>
@ -279,6 +295,23 @@
{% endfor %} {% endfor %}
</div> </div>
<div class="row">
<div class="col">
<nav aria-label="Alias navigation">
<ul class="pagination">
<li class="page-item {% if page == 0 %}disabled{% endif %}">
<a class="page-link"
href="{{ url_for('dashboard.index', page=page-1, query=query, sort=sort) }}">Previous</a>
</li>
<li class="page-item {% if last_page %}disabled{% endif %}">
<a class="page-link"
href="{{ url_for('dashboard.index', page=page+1, query=query, sort=sort) }}">Next</a>
</li>
</ul>
</nav>
</div>
</div>
{% if client_users %} {% if client_users %}
<div class="page-header row"> <div class="page-header row">
@ -393,7 +426,6 @@
let aliasId = $(this).data("alias"); let aliasId = $(this).data("alias");
let alias = $(this).parent().find(".alias").val(); let alias = $(this).parent().find(".alias").val();
try { try {
let res = await fetch(`/api/aliases/${aliasId}/toggle`, { let res = await fetch(`/api/aliases/${aliasId}/toggle`, {
method: "POST", method: "POST",
@ -424,6 +456,69 @@
var oldValue = !$(this).prop("checked"); var oldValue = !$(this).prop("checked");
$(this).prop("checked", oldValue); $(this).prop("checked", oldValue);
} }
})
$(".save-note").on("click", async function () {
let aliasId = $(this).data("alias");
let note = $(`#note-${aliasId}`).val();
try {
let res = await fetch(`/api/aliases/${aliasId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
note: note,
}),
});
if (res.ok) {
toastr.success(`Saved`);
} else {
toastr.error("Sorry for the inconvenience! Could you refresh the page & retry please?", "Unknown Error");
// reset to the original value
var oldValue = !$(this).prop("checked");
$(this).prop("checked", oldValue);
}
} catch (e) {
toastr.error("Sorry for the inconvenience! Could you refresh the page & retry please?", "Unknown Error");
// reset to the original value
var oldValue = !$(this).prop("checked");
$(this).prop("checked", oldValue);
}
})
$(".save-mailbox").on("click", async function () {
let aliasId = $(this).data("alias");
let mailbox_id = $(`#mailbox-${aliasId}`).val();
try {
let res = await fetch(`/api/aliases/${aliasId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
mailbox_id: mailbox_id,
}),
});
if (res.ok) {
toastr.success(`Mailbox Updated`);
} else {
toastr.error("Sorry for the inconvenience! Could you refresh the page & retry please?", "Unknown Error");
// reset to the original value
var oldValue = !$(this).prop("checked");
$(this).prop("checked", oldValue);
}
} catch (e) {
toastr.error("Sorry for the inconvenience! Could you refresh the page & retry please?", "Unknown Error");
// reset to the original value
var oldValue = !$(this).prop("checked");
$(this).prop("checked", oldValue);
}
}) })
</script> </script>

View File

@ -1,5 +1,4 @@
import re import re
from email.utils import parseaddr
from flask import render_template, request, redirect, url_for, flash from flask import render_template, request, redirect, url_for, flash
from flask_login import login_required, current_user from flask_login import login_required, current_user

View File

@ -3,9 +3,9 @@ from flask_login import login_required, current_user
from app.config import PADDLE_MONTHLY_PRODUCT_ID, PADDLE_YEARLY_PRODUCT_ID from app.config import PADDLE_MONTHLY_PRODUCT_ID, PADDLE_YEARLY_PRODUCT_ID
from app.dashboard.base import dashboard_bp from app.dashboard.base import dashboard_bp
from app.extensions import db
from app.log import LOG from app.log import LOG
from app.models import Subscription, PlanEnum from app.models import Subscription, PlanEnum
from app.extensions import db
from app.paddle_utils import cancel_subscription, change_plan from app.paddle_utils import cancel_subscription, change_plan

View File

@ -40,7 +40,7 @@ def custom_alias():
) )
) )
mailboxes = current_user.mailboxes() mailboxes = [mb.email for mb in current_user.mailboxes()]
if request.method == "POST": if request.method == "POST":
alias_prefix = request.form.get("prefix") alias_prefix = request.form.get("prefix")

View File

@ -1,48 +1,32 @@
from dataclasses import dataclass
from arrow import Arrow
from flask import render_template, request, redirect, url_for, flash from flask import render_template, request, redirect, url_for, flash
from flask_login import login_required, current_user from flask_login import login_required, current_user
from sqlalchemy import or_
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from app import email_utils from app import email_utils
from app.api.serializer import get_alias_infos_with_pagination_v2
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 ( from app.models import (
Alias, Alias,
ClientUser, ClientUser,
Contact,
EmailLog,
DeletedAlias, DeletedAlias,
AliasGeneratorEnum, AliasGeneratorEnum,
Mailbox, Mailbox,
) )
@dataclass
class AliasInfo:
alias: Alias
mailbox: Mailbox
nb_forward: int
nb_blocked: int
nb_reply: int
latest_activity: Arrow
latest_email_log: EmailLog = None
latest_contact: Contact = None
show_intro_test_send_email: bool = False
highlight: bool = False
@dashboard_bp.route("/", methods=["GET", "POST"]) @dashboard_bp.route("/", methods=["GET", "POST"])
@login_required @login_required
def index(): def index():
query = request.args.get("query") or "" query = request.args.get("query") or ""
sort = request.args.get("sort") or ""
page = 0
if request.args.get("page"):
page = int(request.args.get("page"))
highlight_alias_id = None highlight_alias_id = None
if request.args.get("highlight_alias_id"): if request.args.get("highlight_alias_id"):
highlight_alias_id = int(request.args.get("highlight_alias_id")) highlight_alias_id = int(request.args.get("highlight_alias_id"))
@ -187,80 +171,12 @@ def index():
return render_template( return render_template(
"dashboard/index.html", "dashboard/index.html",
client_users=client_users, client_users=client_users,
alias_infos=get_alias_infos(current_user, query, highlight_alias_id), alias_infos=get_alias_infos_with_pagination_v2(current_user, page, query, sort),
highlight_alias_id=highlight_alias_id, highlight_alias_id=highlight_alias_id,
query=query, query=query,
AliasGeneratorEnum=AliasGeneratorEnum, AliasGeneratorEnum=AliasGeneratorEnum,
mailboxes=mailboxes, mailboxes=mailboxes,
show_intro=show_intro, show_intro=show_intro,
page=page,
sort=sort,
) )
def get_alias_infos(user, query=None, highlight_alias_id=None) -> [AliasInfo]:
if query:
query = query.strip().lower()
aliases = {} # dict of alias email and AliasInfo
q = (
db.session.query(Alias, Contact, EmailLog, Mailbox)
.join(Contact, Alias.id == Contact.alias_id, isouter=True)
.join(EmailLog, Contact.id == EmailLog.contact_id, isouter=True)
.join(Mailbox, Alias.mailbox_id == Mailbox.id, isouter=True)
.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}%"))
)
for alias, contact, email_log, mailbox in q:
if alias.email not in aliases:
aliases[alias.email] = AliasInfo(
alias=alias,
mailbox=mailbox,
nb_blocked=0,
nb_forward=0,
nb_reply=0,
highlight=alias.id == highlight_alias_id,
latest_activity=alias.created_at,
)
alias_info = aliases[alias.email]
if not email_log:
continue
if email_log.created_at > alias_info.latest_activity:
alias_info.latest_activity = email_log.created_at
alias_info.latest_email_log = email_log
alias_info.latest_contact = contact
if email_log.is_reply:
alias_info.nb_reply += 1
elif email_log.blocked:
alias_info.nb_blocked += 1
else:
alias_info.nb_forward += 1
ret = list(aliases.values())
ret = sorted(ret, key=lambda a: a.latest_activity, reverse=True)
# make sure the highlighted alias is the first element
highlight_index = None
for ix, alias in enumerate(ret):
if alias.highlight:
highlight_index = ix
break
if highlight_index:
ret.insert(0, ret.pop(highlight_index))
# only show intro on the first enabled alias
for alias in ret:
if alias.alias.enabled:
alias.show_intro_test_send_email = True
break
return ret

View File

@ -3,13 +3,7 @@ from flask_login import login_required, current_user
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, validators from wtforms import StringField, validators
from app.config import ( from app.config import ADMIN_EMAIL
PADDLE_VENDOR_ID,
PADDLE_MONTHLY_PRODUCT_ID,
PADDLE_YEARLY_PRODUCT_ID,
URL,
ADMIN_EMAIL,
)
from app.dashboard.base import dashboard_bp from app.dashboard.base import dashboard_bp
from app.email_utils import send_email from app.email_utils import send_email
from app.extensions import db from app.extensions import db

View File

@ -1,7 +1,7 @@
from flask import render_template, request, redirect, url_for, flash from flask import render_template, request, redirect, url_for, flash
from flask_login import login_required, current_user from flask_login import login_required, current_user
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from itsdangerous import Signer, BadSignature from itsdangerous import Signer
from wtforms import validators from wtforms import validators
from wtforms.fields.html5 import EmailField from wtforms.fields.html5 import EmailField
@ -9,7 +9,6 @@ from app.config import EMAIL_DOMAIN, ALIAS_DOMAINS, MAILBOX_SECRET, URL
from app.dashboard.base import dashboard_bp from app.dashboard.base import dashboard_bp
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,
mailbox_already_used, mailbox_already_used,
render, render,
send_email, send_email,

View File

@ -1,21 +1,22 @@
from smtplib import SMTPRecipientsRefused
from flask import render_template, request, redirect, url_for, flash from flask import render_template, request, redirect, url_for, flash
from flask_login import login_required, current_user from flask_login import login_required, current_user
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from itsdangerous import Signer, BadSignature from itsdangerous import Signer
from wtforms import validators from wtforms import validators
from wtforms.fields.html5 import EmailField from wtforms.fields.html5 import EmailField
from app.config import MAILBOX_SECRET from app.config import MAILBOX_SECRET
from app.config import URL from app.config import URL
from app.dashboard.base import dashboard_bp from app.dashboard.base import dashboard_bp
from app.email_utils import can_be_used_as_personal_email, email_already_used from app.email_utils import can_be_used_as_personal_email
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 Alias, 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
class ChangeEmailForm(FlaskForm): class ChangeEmailForm(FlaskForm):

View File

@ -4,7 +4,7 @@ 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.log import LOG from app.log import LOG
from app.models import EmailLog, Referral from app.models import Referral
from app.utils import random_string from app.utils import random_string

View File

@ -3,10 +3,8 @@ from flask_login import current_user, login_required
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, validators from wtforms import StringField, validators
from app import email_utils
from app.developer.base import developer_bp from app.developer.base import developer_bp
from app.extensions import db from app.extensions import db
from app.log import LOG
from app.models import Client from app.models import Client

View File

@ -1,7 +1,8 @@
import enum import enum
import random import random
import uuid import uuid
from email.utils import parseaddr, formataddr from email.utils import formataddr
from typing import List
import arrow import arrow
import bcrypt import bcrypt
@ -357,12 +358,12 @@ class User(db.Model, ModelMixin, UserMixin):
def verified_custom_domains(self): def verified_custom_domains(self):
return CustomDomain.query.filter_by(user_id=self.id, verified=True).all() return CustomDomain.query.filter_by(user_id=self.id, verified=True).all()
def mailboxes(self) -> [str]: def mailboxes(self) -> List["Mailbox"]:
"""list of mailbox emails that user own""" """list of mailbox that user own"""
mailboxes = [] mailboxes = []
for mailbox in Mailbox.query.filter_by(user_id=self.id, verified=True): for mailbox in Mailbox.query.filter_by(user_id=self.id, verified=True):
mailboxes.append(mailbox.email) mailboxes.append(mailbox)
return mailboxes return mailboxes

View File

@ -28,7 +28,7 @@ from app.oauth_models import (
SUPPORTED_OPENID_FLOWS_STR, SUPPORTED_OPENID_FLOWS_STR,
response_types_to_str, response_types_to_str,
) )
from app.utils import random_string, encode_url, convert_to_id, random_word from app.utils import random_string, encode_url, random_word
@oauth_bp.route("/authorize", methods=["GET", "POST"]) @oauth_bp.route("/authorize", methods=["GET", "POST"])

View File

@ -1,5 +1,5 @@
from io import BytesIO
import os import os
from io import BytesIO
import boto3 import boto3
import requests import requests

View File

@ -160,11 +160,12 @@ def fake_data():
m1 = Mailbox.create(user_id=user.id, email="m1@cd.ef", verified=True) m1 = Mailbox.create(user_id=user.id, email="m1@cd.ef", verified=True)
db.session.commit() db.session.commit()
user.default_mailbox_id = m1.id
Alias.create_new(user, "e1@", mailbox_id=m1.id) Alias.create_new(user, "e1@", mailbox_id=m1.id)
for i in range(10): for i in range(30):
Alias.create_new(user, f"e{i}@", mailbox_id=m1.id) if i % 2 == 0:
Alias.create_new(user, f"e{i}@", mailbox_id=m1.id)
else:
Alias.create_new(user, f"e{i}@")
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(

View File

@ -6,7 +6,7 @@ from flask import url_for
from app.config import PAGE_LIMIT from app.config import PAGE_LIMIT
from app.extensions import db from app.extensions import db
from app.models import User, ApiKey, Alias, Contact, EmailLog from app.models import User, ApiKey, Alias, Contact, EmailLog, Mailbox
def test_get_aliases_error_without_pagination(flask_client): def test_get_aliases_error_without_pagination(flask_client):
@ -292,7 +292,38 @@ def test_update_alias(flask_client):
) )
assert r.status_code == 200 assert r.status_code == 200
assert r.json == {"note": "test note"}
def test_update_alias_mailbox(flask_client):
user = User.create(
email="a@b.c", password="password", name="Test User", activated=True
)
db.session.commit()
mb = Mailbox.create(user_id=user.id, email="ab@cd.com", verified=True)
# create api_key
api_key = ApiKey.create(user.id, "for test")
db.session.commit()
alias = Alias.create_new_random(user)
db.session.commit()
r = flask_client.put(
url_for("api.update_alias", alias_id=alias.id),
headers={"Authentication": api_key.code},
json={"mailbox_id": mb.id},
)
assert r.status_code == 200
# fail when update with non-existing mailbox
r = flask_client.put(
url_for("api.update_alias", alias_id=alias.id),
headers={"Authentication": api_key.code},
json={"mailbox_id": -1},
)
assert r.status_code == 400
def test_alias_contacts(flask_client): def test_alias_contacts(flask_client):