diff --git a/.flake8 b/.flake8
index 4b631a19..57cd495c 100644
--- a/.flake8
+++ b/.flake8
@@ -6,7 +6,9 @@ ignore =
W503,
E203,
# Ignore "f-string is missing placeholders"
- F541
+ F541,
+ # allow bare "except"
+ E722
exclude =
.git,
__pycache__,
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 58ccb1bb..2a3bfbdf 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -29,8 +29,7 @@ jobs:
# optional (defaults to `postgres`)
POSTGRES_USER: test
ports:
- # maps tcp port 5432 on service container to the host
- - 5432:5432
+ - 15432:5432
# set health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
@@ -64,6 +63,7 @@ jobs:
poetry run black --check .
flake8
+
- name: Test with pytest
run: |
pytest --cov=. --cov-report=term:skip-covered --cov-report=html:htmlcov --cov-fail-under=60
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8e8243ff..ae1da4c3 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,21 +1,6 @@
Thanks for taking the time to contribute! 🎉👍
-The project uses Flask and requires Python3.7+.
-
-## Quick start
-
-If you have Docker installed, run the following command to start SimpleLogin local server:
-
-
-```bash
-docker run --name sl -it --rm \
- -e RESET_DB=true \
- -e CONFIG=/code/example.env \
- -p 7777:7777 \
- simplelogin/app:3.4.0 python server.py
-```
-
-Then open http://localhost:7777, you should be able to login with `john@wick.com/password` account.
+The project uses Flask, Python3.7+ and requires Postgres 12+ as dependency.
## General Architecture
@@ -25,15 +10,16 @@ Then open http://localhost:7777, you should be able to login with `john@wick.com
SimpleLogin backend consists of 2 main components:
-- the `webapp` used by several clients: web UI (the dashboard), browser extension (Chrome & Firefox for now), OAuth clients (apps that integrate "Login with SimpleLogin" button) and mobile app (work in progress).
+- the `webapp` used by several clients: the web app, the browser extensions (Chrome & Firefox for now), OAuth clients (apps that integrate "Sign in with SimpleLogin" button) and mobile apps.
- the `email handler`: implements the email forwarding (i.e. alias receiving email) and email sending (i.e. alias sending email).
-## Run code locally
+## Install dependencies
-The project uses
+The project requires:
- Python 3.7+ and [poetry](https://python-poetry.org/) to manage dependencies
- Node v10 for front-end.
+- Postgres 12+
First, install all dependencies by running the following command.
Feel free to use `virtualenv` or similar tools to isolate development environment.
@@ -42,29 +28,25 @@ Feel free to use `virtualenv` or similar tools to isolate development environmen
poetry install
```
-On Mac, sometimes you might need to install some other packages like
+On Mac, sometimes you might need to install some other packages via `brew`:
```bash
brew install pkg-config libffi openssl postgresql
```
-You also need to install `gpg`, on Mac it can be done with:
+You also need to install `gpg` tool, on Mac it can be done with:
```bash
brew install gnupg
```
-Then make sure all tests pass. You need to run a local postgres database to run tests, it can be run with docker with:
+## Run tests
```bash
-docker run -e POSTGRES_PASSWORD=test -e POSTGRES_USER=test -e POSTGRES_DB=test -p 5432:5432 postgres:13
+sh scripts/run-test.sh
```
-then run all tests
-
-```bash
-pytest
-```
+## Run the code locally
Install npm packages
@@ -78,17 +60,19 @@ To run the code locally, please create a local setting file based on `example.en
cp example.env .env
```
+Run the postgres database:
+
+```bash
+docker run -e POSTGRES_PASSWORD=mypassword -e POSTGRES_USER=myuser -e POSTGRES_DB=simplelogin -p 35432:5432 postgres:13
+```
+
To run the server:
```
-python3 server.py
+flask db upgrade && flask dummy-data && python3 server.py
```
-then open http://localhost:7777, you should be able to login with the following account
-
-```
-john@wick.com / password
-```
+then open http://localhost:7777, you should be able to login with `john@wick.com / password` account.
You might need to change the `.env` file for developing certain features. This file is ignored by git.
@@ -101,7 +85,7 @@ Whenever the model changes, a new migration has to be created.
If you have Docker installed, you can create the migration by the following script:
```bash
-sh new_migration.sh
+sh scripts/new-migration.sh
```
Make sure to review the migration script before committing it.
diff --git a/LICENSE b/LICENSE
index 7e1ae8fa..0ad25db4 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,21 +1,661 @@
-MIT License
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
-Copyright (c) 2020 SimpleLogin
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+ Preamble
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
diff --git a/README.md b/README.md
index 94298775..dcb145f6 100644
--- a/README.md
+++ b/README.md
@@ -161,7 +161,7 @@ Similar to DKIM, setting up SPF is highly recommended.
Add a TXT record for `mydomain.com.` with the value:
```
-v=spf1 mx -all
+v=spf1 mx ~all
```
What it means is only your server can send email with `@mydomain.com` domain.
diff --git a/app/admin_model.py b/app/admin_model.py
index 31437c84..17c0e01e 100644
--- a/app/admin_model.py
+++ b/app/admin_model.py
@@ -3,7 +3,7 @@ from flask import redirect, url_for, request, flash
from flask_admin import expose, AdminIndexView
from flask_admin.actions import action
from flask_admin.contrib import sqla
-from flask_login import current_user
+from flask_login import current_user, login_user
from app.extensions import db
from app.models import User, ManualSubscription
@@ -122,6 +122,21 @@ class UserAdmin(SLModelView):
db.session.commit()
+ @action(
+ "login_as",
+ "Login as this user",
+ "Login as this user?",
+ )
+ def login_as(self, ids):
+ if len(ids) != 1:
+ flash("only 1 user can be selected", "error")
+ return
+
+ for user in User.query.filter(User.id.in_(ids)):
+ login_user(user)
+ flash(f"Login as user {user}", "success")
+ return redirect("/")
+
def manual_upgrade(way: str, ids: [int], is_giveaway: bool):
query = User.query.filter(User.id.in_(ids))
@@ -209,6 +224,12 @@ class ClientAdmin(SLModelView):
can_edit = True
+class CustomDomainAdmin(SLModelView):
+ column_searchable_list = ["domain", "user.email", "user.id"]
+ column_exclude_list = ["ownership_txt_token"]
+ can_edit = False
+
+
class ReferralAdmin(SLModelView):
column_searchable_list = ["id", "user.email", "code", "name"]
column_filters = ["id", "user.email", "code", "name"]
diff --git a/app/alias_utils.py b/app/alias_utils.py
index 7e0f4629..a5f1a7d5 100644
--- a/app/alias_utils.py
+++ b/app/alias_utils.py
@@ -1,6 +1,7 @@
-import re
+import re2 as re
from typing import Optional
+from email_validator import validate_email, EmailNotValidError
from sqlalchemy.exc import IntegrityError, DataError
from app.config import BOUNCE_PREFIX_FOR_REPLY_PHASE
@@ -10,6 +11,7 @@ from app.email_utils import (
send_cannot_create_domain_alias,
can_create_directory_for_address,
send_cannot_create_directory_alias_disabled,
+ get_email_local_part,
)
from app.errors import AliasInTrashError
from app.extensions import db
@@ -25,18 +27,23 @@ from app.models import (
Mailbox,
EmailLog,
Contact,
+ AutoCreateRule,
)
def try_auto_create(address: str) -> Optional[Alias]:
"""Try to auto-create the alias using directory or catch-all domain"""
if address.startswith(f"{BOUNCE_PREFIX_FOR_REPLY_PHASE}+"):
- LOG.exception(
- "alias %s can't start with %s", address, BOUNCE_PREFIX_FOR_REPLY_PHASE
- )
+ LOG.e("alias %s can't start with %s", address, BOUNCE_PREFIX_FOR_REPLY_PHASE)
return None
- alias = try_auto_create_catch_all_domain(address)
+ try:
+ # NOT allow unicode for now
+ validate_email(address, check_deliverability=False, allow_smtputf8=False)
+ except EmailNotValidError:
+ return None
+
+ alias = try_auto_create_via_domain(address)
if not alias:
alias = try_auto_create_directory(address)
@@ -68,16 +75,14 @@ def try_auto_create_directory(address: str) -> Optional[Alias]:
if not directory:
return None
- dir_user: User = directory.user
+ user: User = directory.user
- if not dir_user.can_create_new_alias():
- send_cannot_create_directory_alias(dir_user, address, directory_name)
+ if not user.can_create_new_alias():
+ send_cannot_create_directory_alias(user, address, directory_name)
return None
if directory.disabled:
- send_cannot_create_directory_alias_disabled(
- dir_user, address, directory_name
- )
+ send_cannot_create_directory_alias_disabled(user, address, directory_name)
return None
try:
@@ -90,6 +95,7 @@ def try_auto_create_directory(address: str) -> Optional[Alias]:
user_id=directory.user_id,
directory_id=directory.id,
mailbox_id=mailboxes[0].id,
+ note=f"Created by directory {directory.name}",
)
db.session.flush()
for i in range(1, len(mailboxes)):
@@ -101,22 +107,22 @@ def try_auto_create_directory(address: str) -> Optional[Alias]:
db.session.commit()
return alias
except AliasInTrashError:
- LOG.warning(
+ LOG.w(
"Alias %s was deleted before, cannot auto-create using directory %s, user %s",
address,
directory_name,
- dir_user,
+ user,
)
return None
except IntegrityError:
- LOG.warning("Alias %s already exists", address)
+ LOG.w("Alias %s already exists", address)
db.session.rollback()
alias = Alias.get_by(email=address)
return alias
-def try_auto_create_catch_all_domain(address: str) -> Optional[Alias]:
- """Try to create an alias with catch-all domain"""
+def try_auto_create_via_domain(address: str) -> Optional[Alias]:
+ """Try to create an alias with catch-all or auto-create rules on custom domain"""
# 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
@@ -126,11 +132,31 @@ def try_auto_create_catch_all_domain(address: str) -> Optional[Alias]:
if not custom_domain:
return None
- # custom_domain exists
- if not custom_domain.catch_all:
+ if not custom_domain.catch_all and len(custom_domain.auto_create_rules) == 0:
return None
+ elif not custom_domain.catch_all and len(custom_domain.auto_create_rules) > 0:
+ local = get_email_local_part(address)
+
+ for rule in custom_domain.auto_create_rules:
+ rule: AutoCreateRule
+ regex = re.compile(rule.regex)
+ if re.fullmatch(regex, local):
+ LOG.d(
+ "%s passes %s on %s",
+ address,
+ rule.regex,
+ custom_domain,
+ )
+ alias_note = f"Created by rule {rule.order} with regex {rule.regex}"
+ mailboxes = rule.mailboxes
+ break
+ else: # no rule passes
+ LOG.d("no rule passed to create %s", local)
+ return
+ else: # catch-all is enabled
+ mailboxes = custom_domain.mailboxes
+ alias_note = "Created by catch-all option"
- # custom_domain has catch-all enabled
domain_user: User = custom_domain.user
if not domain_user.can_create_new_alias():
@@ -139,13 +165,13 @@ def try_auto_create_catch_all_domain(address: str) -> Optional[Alias]:
try:
LOG.d("create alias %s for domain %s", address, custom_domain)
- mailboxes = custom_domain.mailboxes
alias = Alias.create(
email=address,
user_id=custom_domain.user_id,
custom_domain_id=custom_domain.id,
automatic_creation=True,
mailbox_id=mailboxes[0].id,
+ note=alias_note,
)
db.session.flush()
for i in range(1, len(mailboxes)):
@@ -156,7 +182,7 @@ def try_auto_create_catch_all_domain(address: str) -> Optional[Alias]:
db.session.commit()
return alias
except AliasInTrashError:
- LOG.warning(
+ LOG.w(
"Alias %s was deleted before, cannot auto-create using domain catch-all %s, user %s",
address,
custom_domain,
@@ -164,12 +190,12 @@ def try_auto_create_catch_all_domain(address: str) -> Optional[Alias]:
)
return None
except IntegrityError:
- LOG.warning("Alias %s already exists", address)
+ LOG.w("Alias %s already exists", address)
db.session.rollback()
alias = Alias.get_by(email=address)
return alias
except DataError:
- LOG.warning("Cannot create alias %s", address)
+ LOG.w("Cannot create alias %s", address)
db.session.rollback()
return None
@@ -184,7 +210,7 @@ def delete_alias(alias: Alias, user: User):
if not DomainDeletedAlias.get_by(
email=alias.email, domain_id=alias.custom_domain_id
):
- LOG.debug("add %s to domain %s trash", alias, alias.custom_domain_id)
+ LOG.d("add %s to domain %s trash", alias, alias.custom_domain_id)
db.session.add(
DomainDeletedAlias(
user_id=user.id, email=alias.email, domain_id=alias.custom_domain_id
diff --git a/app/api/serializer.py b/app/api/serializer.py
index 05669f9d..2128a05e 100644
--- a/app/api/serializer.py
+++ b/app/api/serializer.py
@@ -7,7 +7,15 @@ from sqlalchemy.orm import joinedload
from app.config import PAGE_LIMIT
from app.extensions import db
-from app.models import Alias, Contact, EmailLog, Mailbox, AliasMailbox, CustomDomain
+from app.models import (
+ Alias,
+ Contact,
+ EmailLog,
+ Mailbox,
+ AliasMailbox,
+ CustomDomain,
+ User,
+)
@dataclass
@@ -129,119 +137,46 @@ def get_alias_infos_with_pagination(user, page_id=0, query=None) -> [AliasInfo]:
def get_alias_infos_with_pagination_v3(
- user, page_id=0, query=None, sort=None, alias_filter=None
+ user,
+ page_id=0,
+ query=None,
+ sort=None,
+ alias_filter=None,
+ mailbox_id=None,
+ directory_id=None,
) -> [AliasInfo]:
- # subquery on alias annotated with nb_reply, nb_blocked, nb_forward, max_created_at, latest_email_log_created_at
- alias_activity_subquery = (
- db.session.query(
- Alias.id,
- func.sum(case([(EmailLog.is_reply, 1)], else_=0)).label("nb_reply"),
- func.sum(
- case(
- [(and_(EmailLog.is_reply.is_(False), EmailLog.blocked), 1)],
- else_=0,
- )
- ).label("nb_blocked"),
- func.sum(
- case(
- [
- (
- and_(
- EmailLog.is_reply.is_(False),
- EmailLog.blocked.is_(False),
- ),
- 1,
- )
- ],
- else_=0,
- )
- ).label("nb_forward"),
- func.max(EmailLog.created_at).label("latest_email_log_created_at"),
- )
- .join(EmailLog, Alias.id == EmailLog.alias_id, isouter=True)
- .filter(Alias.user_id == user.id)
- .group_by(Alias.id)
- .subquery()
- )
-
- alias_contact_subquery = (
- db.session.query(Alias.id, func.max(Contact.id).label("max_contact_id"))
- .join(Contact, Alias.id == Contact.alias_id, isouter=True)
- .filter(Alias.user_id == user.id)
- .group_by(Alias.id)
- .subquery()
- )
-
- latest_activity = case(
- [
- (Alias.created_at > EmailLog.created_at, Alias.created_at),
- (Alias.created_at < EmailLog.created_at, EmailLog.created_at),
- ],
- else_=Alias.created_at,
- )
-
- q = (
- db.session.query(
- Alias,
- Contact,
- EmailLog,
- CustomDomain,
- alias_activity_subquery.c.nb_reply,
- alias_activity_subquery.c.nb_blocked,
- alias_activity_subquery.c.nb_forward,
- )
- .options(joinedload(Alias.hibp_breaches))
- .join(Contact, Alias.id == Contact.alias_id, isouter=True)
- .join(CustomDomain, Alias.custom_domain_id == CustomDomain.id, isouter=True)
- .join(EmailLog, Contact.id == EmailLog.contact_id, isouter=True)
- .filter(Alias.id == alias_activity_subquery.c.id)
- .filter(Alias.id == alias_contact_subquery.c.id)
- .filter(
- or_(
- EmailLog.created_at
- == alias_activity_subquery.c.latest_email_log_created_at,
- and_(
- # no email log yet for this alias
- alias_activity_subquery.c.latest_email_log_created_at.is_(None),
- # to make sure only 1 contact is returned in this case
- or_(
- Contact.id == alias_contact_subquery.c.max_contact_id,
- alias_contact_subquery.c.max_contact_id.is_(None),
- ),
- ),
- )
- )
- )
+ q = construct_alias_query(user)
if query:
- q = (
- # to find mailbox whose email match the query
- q.join(AliasMailbox, Alias.id == AliasMailbox.alias_id, isouter=True)
- .join(
- Mailbox,
- or_(
- Mailbox.id == Alias.mailbox_id,
- Mailbox.id == AliasMailbox.mailbox_id,
- ),
- )
- .filter(
- or_(
- Alias.email.ilike(f"%{query}%"),
- # can't use match() here as it uses to_tsquery that expected a tsquery input
- # Alias.ts_vector.match(query),
- Alias.ts_vector.op("@@")(func.plainto_tsquery(query)),
- Alias.name.ilike(f"%{query}%"),
- Mailbox.email.ilike(f"%{query}%"),
- )
+ q = q.filter(
+ or_(
+ Alias.email.ilike(f"%{query}%"),
+ Alias.note.ilike(f"%{query}%"),
+ # can't use match() here as it uses to_tsquery that expected a tsquery input
+ # Alias.ts_vector.match(query),
+ Alias.ts_vector.op("@@")(func.plainto_tsquery("english", query)),
+ Alias.name.ilike(f"%{query}%"),
)
)
+ if mailbox_id:
+ q = q.join(
+ AliasMailbox, Alias.id == AliasMailbox.alias_id, isouter=True
+ ).filter(
+ or_(Alias.mailbox_id == mailbox_id, AliasMailbox.mailbox_id == mailbox_id)
+ )
+
+ if directory_id:
+ q = q.filter(Alias.directory_id == directory_id)
+
if alias_filter == "enabled":
q = q.filter(Alias.enabled)
elif alias_filter == "disabled":
q = q.filter(Alias.enabled.is_(False))
-
- q = q.order_by(Alias.pinned.desc())
+ elif alias_filter == "pinned":
+ q = q.filter(Alias.pinned)
+ elif alias_filter == "hibp":
+ q = q.filter(Alias.hibp_breaches.any())
if sort == "old2new":
q = q.order_by(Alias.created_at)
@@ -251,10 +186,16 @@ def get_alias_infos_with_pagination_v3(
q = q.order_by(Alias.email)
elif sort == "z2a":
q = q.order_by(Alias.email.desc())
- elif alias_filter == "hibp":
- q = q.filter(Alias.hibp_breaches.any())
else:
# default sorting
+ latest_activity = case(
+ [
+ (Alias.created_at > EmailLog.created_at, Alias.created_at),
+ (Alias.created_at < EmailLog.created_at, EmailLog.created_at),
+ ],
+ else_=Alias.created_at,
+ )
+ q = q.order_by(Alias.pinned.desc())
q = q.order_by(latest_activity.desc())
q = list(q.limit(PAGE_LIMIT).offset(page_id * PAGE_LIMIT))
@@ -367,3 +308,98 @@ def get_alias_contacts(alias, page_id: int) -> [dict]:
res.append(serialize_contact(fe))
return res
+
+
+def get_alias_info_v3(user: User, alias_id: int) -> AliasInfo:
+ # use the same query construction in get_alias_infos_with_pagination_v3
+ q = construct_alias_query(user)
+ q = q.filter(Alias.id == alias_id)
+
+ for alias, contact, email_log, custom_domain, nb_reply, nb_blocked, nb_forward in q:
+ return AliasInfo(
+ alias=alias,
+ mailbox=alias.mailbox,
+ mailboxes=alias.mailboxes,
+ nb_forward=nb_forward,
+ nb_blocked=nb_blocked,
+ nb_reply=nb_reply,
+ latest_email_log=email_log,
+ latest_contact=contact,
+ custom_domain=custom_domain,
+ )
+
+
+def construct_alias_query(user: User):
+ # subquery on alias annotated with nb_reply, nb_blocked, nb_forward, max_created_at, latest_email_log_created_at
+ alias_activity_subquery = (
+ db.session.query(
+ Alias.id,
+ func.sum(case([(EmailLog.is_reply, 1)], else_=0)).label("nb_reply"),
+ func.sum(
+ case(
+ [(and_(EmailLog.is_reply.is_(False), EmailLog.blocked), 1)],
+ else_=0,
+ )
+ ).label("nb_blocked"),
+ func.sum(
+ case(
+ [
+ (
+ and_(
+ EmailLog.is_reply.is_(False),
+ EmailLog.blocked.is_(False),
+ ),
+ 1,
+ )
+ ],
+ else_=0,
+ )
+ ).label("nb_forward"),
+ func.max(EmailLog.created_at).label("latest_email_log_created_at"),
+ )
+ .join(EmailLog, Alias.id == EmailLog.alias_id, isouter=True)
+ .filter(Alias.user_id == user.id)
+ .group_by(Alias.id)
+ .subquery()
+ )
+
+ alias_contact_subquery = (
+ db.session.query(Alias.id, func.max(Contact.id).label("max_contact_id"))
+ .join(Contact, Alias.id == Contact.alias_id, isouter=True)
+ .filter(Alias.user_id == user.id)
+ .group_by(Alias.id)
+ .subquery()
+ )
+
+ return (
+ db.session.query(
+ Alias,
+ Contact,
+ EmailLog,
+ CustomDomain,
+ alias_activity_subquery.c.nb_reply,
+ alias_activity_subquery.c.nb_blocked,
+ alias_activity_subquery.c.nb_forward,
+ )
+ .options(joinedload(Alias.hibp_breaches))
+ .join(Contact, Alias.id == Contact.alias_id, isouter=True)
+ .join(CustomDomain, Alias.custom_domain_id == CustomDomain.id, isouter=True)
+ .join(EmailLog, Contact.id == EmailLog.contact_id, isouter=True)
+ .filter(Alias.id == alias_activity_subquery.c.id)
+ .filter(Alias.id == alias_contact_subquery.c.id)
+ .filter(
+ or_(
+ EmailLog.created_at
+ == alias_activity_subquery.c.latest_email_log_created_at,
+ and_(
+ # no email log yet for this alias
+ alias_activity_subquery.c.latest_email_log_created_at.is_(None),
+ # to make sure only 1 contact is returned in this case
+ or_(
+ Contact.id == alias_contact_subquery.c.max_contact_id,
+ alias_contact_subquery.c.max_contact_id.is_(None),
+ ),
+ ),
+ )
+ )
+ )
diff --git a/app/api/views/alias.py b/app/api/views/alias.py
index efe6f9dc..64151bb7 100644
--- a/app/api/views/alias.py
+++ b/app/api/views/alias.py
@@ -1,3 +1,5 @@
+from flanker.addresslib import address
+from flanker.addresslib.address import EmailAddress
from flask import g
from flask import jsonify
from flask import request
@@ -16,8 +18,6 @@ from app.api.serializer import (
)
from app.dashboard.views.alias_log import get_alias_log
from app.email_utils import (
- parseaddr_unicode,
- is_valid_email,
generate_reply_email,
)
from app.extensions import db
@@ -400,10 +400,11 @@ def create_contact_route(alias_id):
if not contact_addr:
return jsonify(error="Contact cannot be empty"), 400
- contact_name, contact_email = parseaddr_unicode(contact_addr)
+ full_address: EmailAddress = address.parse(contact_addr)
+ if not full_address:
+ return jsonify(error=f"invalid contact email {contact_addr}"), 400
- if not is_valid_email(contact_email):
- return jsonify(error=f"invalid contact email {contact_email}"), 400
+ contact_name, contact_email = full_address.display_name, full_address.address
contact_email = sanitize_email(contact_email)
diff --git a/app/api/views/apple.py b/app/api/views/apple.py
index bcc8acfc..563c9d0b 100644
--- a/app/api/views/apple.py
+++ b/app/api/views/apple.py
@@ -36,7 +36,7 @@ def apple_process_payment():
200 of the payment is successful, i.e. user is upgraded to premium
"""
- LOG.debug("request for /apple/process_payment")
+ LOG.d("request for /apple/process_payment")
user = g.user
data = request.get_json()
receipt_data = data.get("receipt_data")
@@ -229,7 +229,7 @@ def apple_update_notification():
# "auto_renew_product_id": "io.simplelogin.ios_app.subscription.premium.yearly",
# "notification_type": "DID_CHANGE_RENEWAL_STATUS",
# }
- LOG.debug("request for /api/apple/update_notification")
+ LOG.d("request for /api/apple/update_notification")
data = request.get_json()
if not (
data
@@ -282,7 +282,7 @@ def apple_update_notification():
db.session.commit()
return jsonify(ok=True), 200
else:
- LOG.warning(
+ LOG.w(
"No existing AppleSub for original_transaction_id %s",
original_transaction_id,
)
@@ -305,16 +305,16 @@ def verify_receipt(receipt_data, user, password) -> Optional[AppleSubscription]:
_PROD_URL, json={"receipt-data": receipt_data, "password": password}
)
except RequestException:
- LOG.warning("cannot call Apple server %s", _PROD_URL)
+ LOG.w("cannot call Apple server %s", _PROD_URL)
return None
if r.status_code >= 500:
- LOG.warning("Apple server error, response:%s %s", r, r.content)
+ LOG.w("Apple server error, response:%s %s", r, r.content)
return None
if r.json() == {"status": 21007}:
# try sandbox_url
- LOG.warning("Use the sandbox url instead")
+ LOG.w("Use the sandbox url instead")
r = requests.post(
_SANDBOX_URL,
json={"receipt-data": receipt_data, "password": password},
@@ -472,7 +472,7 @@ def verify_receipt(receipt_data, user, password) -> Optional[AppleSubscription]:
# }
if data["status"] != 0:
- LOG.warning(
+ LOG.w(
"verifyReceipt status !=0, probably invalid receipt. User %s",
user,
)
@@ -499,7 +499,7 @@ def verify_receipt(receipt_data, user, password) -> Optional[AppleSubscription]:
# }
transactions = data["receipt"]["in_app"]
if not transactions:
- LOG.warning("Empty transactions in data %s", data)
+ LOG.w("Empty transactions in data %s", data)
return None
latest_transaction = max(transactions, key=lambda t: int(t["expires_date_ms"]))
@@ -527,7 +527,7 @@ def verify_receipt(receipt_data, user, password) -> Optional[AppleSubscription]:
else:
# the same original_transaction_id has been used on another account
if AppleSubscription.get_by(original_transaction_id=original_transaction_id):
- LOG.exception("Same Apple Sub has been used before, current user %s", user)
+ LOG.e("Same Apple Sub has been used before, current user %s", user)
return None
LOG.d(
diff --git a/app/api/views/auth.py b/app/api/views/auth.py
index fd6bc588..4b7a072f 100644
--- a/app/api/views/auth.py
+++ b/app/api/views/auth.py
@@ -96,7 +96,7 @@ def auth_register():
if not password or len(password) < 8:
return jsonify(error="password too short"), 400
- LOG.debug("create user %s", email)
+ LOG.d("create user %s", email)
user = User.create(email=email, name="", password=password)
db.session.flush()
@@ -166,7 +166,7 @@ def auth_activate():
return jsonify(error="Wrong email or code"), 400
- LOG.debug("activate user %s", user)
+ LOG.d("activate user %s", user)
user.activated = True
AccountActivation.delete(account_activation.id)
db.session.commit()
diff --git a/app/api/views/new_custom_alias.py b/app/api/views/new_custom_alias.py
index 762456a1..5148da72 100644
--- a/app/api/views/new_custom_alias.py
+++ b/app/api/views/new_custom_alias.py
@@ -69,10 +69,10 @@ def new_custom_alias_v2():
try:
alias_suffix = signer.unsign(signed_suffix, max_age=600).decode()
except SignatureExpired:
- LOG.warning("Alias creation time expired for %s", user)
+ LOG.w("Alias creation time expired for %s", user)
return jsonify(error="Alias creation time is expired, please retry"), 412
except Exception:
- LOG.warning("Alias suffix is tampered, user %s", user)
+ LOG.w("Alias suffix is tampered, user %s", user)
return jsonify(error="Tampered suffix"), 400
if not verify_prefix_suffix(user, alias_prefix, alias_suffix):
@@ -184,10 +184,10 @@ def new_custom_alias_v3():
try:
alias_suffix = signer.unsign(signed_suffix, max_age=600).decode()
except SignatureExpired:
- LOG.warning("Alias creation time expired for %s", user)
+ LOG.w("Alias creation time expired for %s", user)
return jsonify(error="Alias creation time is expired, please retry"), 412
except Exception:
- LOG.warning("Alias suffix is tampered, user %s", user)
+ LOG.w("Alias suffix is tampered, user %s", user)
return jsonify(error="Tampered suffix"), 400
if not verify_prefix_suffix(user, alias_prefix, alias_suffix):
diff --git a/app/api/views/new_random_alias.py b/app/api/views/new_random_alias.py
index c67b3aae..19d2e3bd 100644
--- a/app/api/views/new_random_alias.py
+++ b/app/api/views/new_random_alias.py
@@ -48,7 +48,7 @@ def new_random_alias():
elif mode == "uuid":
scheme = AliasGeneratorEnum.uuid.value
else:
- return jsonify(error=f"{mode} must be either word or alias"), 400
+ return jsonify(error=f"{mode} must be either word or uuid"), 400
alias = Alias.create_new_random(user=user, scheme=scheme, note=note)
db.session.commit()
diff --git a/app/api/views/setting.py b/app/api/views/setting.py
index 73a89598..a209265e 100644
--- a/app/api/views/setting.py
+++ b/app/api/views/setting.py
@@ -87,7 +87,7 @@ def update_setting():
# sanity check
if custom_domain.user_id != user.id or not custom_domain.verified:
- LOG.exception("%s cannot use domain %s", user, default_domain)
+ LOG.e("%s cannot use domain %s", user, default_domain)
return jsonify(error="invalid domain"), 400
else:
user.default_alias_custom_domain_id = custom_domain.id
diff --git a/app/auth/templates/auth/forgot_password.html b/app/auth/templates/auth/forgot_password.html
index 82daacfc..2c5c72b9 100644
--- a/app/auth/templates/auth/forgot_password.html
+++ b/app/auth/templates/auth/forgot_password.html
@@ -12,7 +12,7 @@