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 @@
{{ form.csrf_token }}
-
Forgot password
+

Forgot password

diff --git a/app/auth/templates/auth/login.html b/app/auth/templates/auth/login.html index 82169c9f..b1474186 100644 --- a/app/auth/templates/auth/login.html +++ b/app/auth/templates/auth/login.html @@ -16,7 +16,7 @@ {{ form.csrf_token }}
-
Welcome back!
+

Welcome back!

{{ form.email(class="form-control", type="email", autofocus="true") }} diff --git a/app/auth/templates/auth/register.html b/app/auth/templates/auth/register.html index b95b0d2d..8d6fb4ce 100644 --- a/app/auth/templates/auth/register.html +++ b/app/auth/templates/auth/register.html @@ -8,7 +8,7 @@ {{ form.csrf_token }}
-
Create new account
+

Create new account

diff --git a/app/auth/views/activate.py b/app/auth/views/activate.py index 9324905d..e9943f99 100644 --- a/app/auth/views/activate.py +++ b/app/auth/views/activate.py @@ -57,8 +57,8 @@ def activate(): # The activation link contains the original page, for ex authorize page if "next" in request.args: next_url = request.args.get("next") - LOG.debug("redirect user to %s", next_url) + LOG.d("redirect user to %s", next_url) return redirect(next_url) else: - LOG.debug("redirect user to dashboard") + LOG.d("redirect user to dashboard") return redirect(url_for("dashboard.index")) diff --git a/app/auth/views/facebook.py b/app/auth/views/facebook.py index f3c7b0e4..66d2d695 100644 --- a/app/auth/views/facebook.py +++ b/app/auth/views/facebook.py @@ -115,7 +115,7 @@ def facebook_callback(): # The activation link contains the original page, for ex authorize page if "facebook_next_url" in session: next_url = session["facebook_next_url"] - LOG.debug("redirect user to %s", next_url) + LOG.d("redirect user to %s", next_url) # reset the next_url to avoid user getting redirected at each login :) session.pop("facebook_next_url", None) diff --git a/app/auth/views/fido.py b/app/auth/views/fido.py index c629df49..aa1ed063 100644 --- a/app/auth/views/fido.py +++ b/app/auth/views/fido.py @@ -95,7 +95,7 @@ def fido(): ) new_sign_count = webauthn_assertion_response.verify() except Exception as e: - LOG.warning(f"An error occurred in WebAuthn verification process: {e}") + LOG.w(f"An error occurred in WebAuthn verification process: {e}") flash("Key verification failed.", "warning") # Trigger rate limiter g.deduct_limit = True diff --git a/app/auth/views/github.py b/app/auth/views/github.py index d130705e..99a93dde 100644 --- a/app/auth/views/github.py +++ b/app/auth/views/github.py @@ -75,7 +75,7 @@ def github_callback(): break if not email: - LOG.exception(f"cannot get email for github user {github_user_data} {emails}") + LOG.e(f"cannot get email for github user {github_user_data} {emails}") flash( "Cannot get a valid email from Github, please another way to login/sign up", "error", diff --git a/app/auth/views/google.py b/app/auth/views/google.py index 66336c93..08dddc3d 100644 --- a/app/auth/views/google.py +++ b/app/auth/views/google.py @@ -101,7 +101,7 @@ def google_callback(): # The activation link contains the original page, for ex authorize page if "google_next_url" in session: next_url = session["google_next_url"] - LOG.debug("redirect user to %s", next_url) + LOG.d("redirect user to %s", next_url) # reset the next_url to avoid user getting redirected at each login :) session.pop("google_next_url", None) diff --git a/app/auth/views/login.py b/app/auth/views/login.py index 4d7ae548..f9c87f7d 100644 --- a/app/auth/views/login.py +++ b/app/auth/views/login.py @@ -25,7 +25,7 @@ def login(): if current_user.is_authenticated: if next_url: - LOG.debug("user is already authenticated, redirect to %s", next_url) + LOG.d("user is already authenticated, redirect to %s", next_url) return redirect(next_url) else: LOG.d("user is already authenticated, redirect to dashboard") diff --git a/app/auth/views/login_utils.py b/app/auth/views/login_utils.py index 9bd2b7ad..c4426d06 100644 --- a/app/auth/views/login_utils.py +++ b/app/auth/views/login_utils.py @@ -29,15 +29,15 @@ def after_login(user, next_url): else: return redirect(url_for("auth.mfa")) else: - LOG.debug("log user %s in", user) + LOG.d("log user %s in", user) login_user(user) # User comes to login page from another page if next_url: - LOG.debug("redirect user to %s", next_url) + LOG.d("redirect user to %s", next_url) return redirect(next_url) else: - LOG.debug("redirect user to dashboard") + LOG.d("redirect user to dashboard") return redirect(url_for("dashboard.index")) diff --git a/app/auth/views/recovery.py b/app/auth/views/recovery.py index aa788d47..fac50932 100644 --- a/app/auth/views/recovery.py +++ b/app/auth/views/recovery.py @@ -58,10 +58,10 @@ def recovery_route(): # User comes to login page from another page if next_url: - LOG.debug("redirect user to %s", next_url) + LOG.d("redirect user to %s", next_url) return redirect(next_url) else: - LOG.debug("redirect user to dashboard") + LOG.d("redirect user to dashboard") return redirect(url_for("dashboard.index")) else: # Trigger rate limiter diff --git a/app/auth/views/register.py b/app/auth/views/register.py index a594d69d..30714cdf 100644 --- a/app/auth/views/register.py +++ b/app/auth/views/register.py @@ -53,7 +53,7 @@ def register(): # 'challenge_ts': '2020-07-23T10:03:25', # 'hostname': '127.0.0.1'} if not hcaptcha_res["success"]: - LOG.warning( + LOG.w( "User put wrong captcha %s %s", form.email.data, hcaptcha_res, @@ -74,7 +74,7 @@ def register(): if personal_email_already_used(email): flash(f"Email {email} already used", "error") else: - LOG.debug("create user %s", email) + LOG.d("create user %s", email) user = User.create( email=email, name="", diff --git a/app/config.py b/app/config.py index 432fa075..2553e3f0 100644 --- a/app/config.py +++ b/app/config.py @@ -45,7 +45,6 @@ if config_file: else: load_dotenv() -RESET_DB = "RESET_DB" in os.environ COLOR_LOG = "COLOR_LOG" in os.environ # Allow user to have 1 year of premium: set the expiration_date to 1 year more @@ -158,7 +157,6 @@ DISABLE_ALIAS_SUFFIX = "DISABLE_ALIAS_SUFFIX" in os.environ UNSUBSCRIBER = os.environ.get("UNSUBSCRIBER") DKIM_SELECTOR = b"dkim" -DKIM_HEADERS = [b"from", b"to"] DKIM_PRIVATE_KEY = None if "DKIM_PRIVATE_KEY_PATH" in os.environ: @@ -334,6 +332,9 @@ AlERT_WRONG_MX_RECORD_CUSTOM_DOMAIN = "custom_domain_mx_record_issue" # alert when a new alias is about to be created on a disabled directory ALERT_DIRECTORY_DISABLED_ALIAS_CREATION = "alert_directory_disabled_alias_creation" +ALERT_HOTMAIL_COMPLAINT = "alert_hotmail_complaint" +ALERT_YAHOO_COMPLAINT = "alert_yahoo_complaint" + # <<<<< END ALERT EMAIL >>>> # Disable onboarding emails @@ -394,3 +395,11 @@ except Exception: HIBP_API_KEYS = sl_getenv("HIBP_API_KEYS", list) or [] NEWRELIC_CONFIG_PATH = os.environ.get("NEWRELIC_CONFIG_PATH") + +POSTMASTER = os.environ.get("POSTMASTER") + +# store temporary files, especially for debugging +TEMP_DIR = os.environ.get("TEMP_DIR") + +# enable the alias automation disable: an alias can be automatically disabled if it has too many bounces +ALIAS_AUTOMATIC_DISABLE = "ALIAS_AUTOMATIC_DISABLE" in os.environ diff --git a/app/dashboard/templates/dashboard/api_key.html b/app/dashboard/templates/dashboard/api_key.html index b832d2cc..c002d769 100644 --- a/app/dashboard/templates/dashboard/api_key.html +++ b/app/dashboard/templates/dashboard/api_key.html @@ -27,7 +27,7 @@
-
{{ api_key.name }}
+
{{ api_key.name or "N/A" }}
{% if api_key.last_used %} Last used: {{ api_key.last_used | dt }}
diff --git a/app/dashboard/templates/dashboard/custom_alias.html b/app/dashboard/templates/dashboard/custom_alias.html index 5810bdb5..bca4fe5a 100644 --- a/app/dashboard/templates/dashboard/custom_alias.html +++ b/app/dashboard/templates/dashboard/custom_alias.html @@ -25,15 +25,16 @@
{% endif %} - +
@@ -69,7 +70,7 @@
- {% if custom_domain.verified %} - - {% else %} - - {% endif %} - + {% if not custom_domain.ownership_verified %} - {% if not mx_ok %} -
- Your DNS is not correctly set. The MX record we obtain is: -
- {% if not mx_errors %} - (Empty) - {% endif %} - {% for r in mx_errors %} - {{ r }}
- {% endfor %} +
+ To verify ownership of the domain, please add the following TXT record. + Some domain registrars (Namecheap, CloudFlare, etc) might use @ for the root domain.
- {% if custom_domain.verified %} -
- Without the MX record set up correctly, you can miss emails sent to your aliases. - Please update the MX record ASAP. + +
+ Record: TXT
+ Domain: {{ custom_domain.domain }} or @
+ Value: {{ custom_domain.get_ownership_dns_txt_value() }} +
+ +
+ + +
+ + {% if not ownership_ok %} +
+ Your DNS is not correctly set. The TXT record we obtain is: +
+ {% if not ownership_errors %} + (Empty) + {% endif %} + {% for r in ownership_errors %} + {{ r }}
+ {% endfor %} +
{% endif %} +
+ {% endif %} +
+ {% endif %} + +
+ + {% if not custom_domain.ownership_verified %} +
+ A domain ownership must be verified first.
{% endif %} -
-
+
+
1. MX record -
-
2. SPF (Optional) - {% if custom_domain.spf_verified %} - âś… - {% else %} - đźš« - {% endif %} -
- -
- SPF (Wikipedia↗) is an email - authentication method - designed to detect forging sender addresses during the delivery of the email.
- Setting up SPF is highly recommended to reduce the chance your emails ending up in the recipient's Spam folder. -
- -
Add the following TXT DNS record to your domain.
- -
- Record: TXT
- Domain: {{ custom_domain.domain }} or - @
- Value: - - {{ spf_record }} - -
- -
- - {% if custom_domain.spf_verified %} - - {% else %} - - {% endif %} -
- - {% if not spf_ok %} -
- Your DNS is not correctly set. The TXT record we obtain is: -
- {% if not spf_errors %} - (Empty) - {% endif %} - - {% for r in spf_errors %} - {{ r }}
- {% endfor %} -
- {% if custom_domain.spf_verified %} - Without SPF setup, emails you sent from your alias might end up in Spam/Junk folder. + {% if custom_domain.verified %} + âś… + {% else %} + đźš« {% endif %}
- {% endif %} -
-
+
Add the following MX DNS record to your domain.
+ Please note that there's a point (.) at the end target addresses. + This is to make sure the absolute address is used. +
+ Also some domain registrars (Namecheap, CloudFlare, etc) might use @ for the root domain. +
-
-
3. DKIM (Optional) - {% if custom_domain.dkim_verified %} - âś… - {% else %} - đźš« - {% endif %} -
+ {% for priority, email_server in EMAIL_SERVERS_WITH_PRIORITY %} +
+ Record: MX
+ Domain: {{ custom_domain.domain }} or + @
+ Priority: {{ priority }}
+ Target: {{ email_server }} +
+ {% endfor %} -
- DKIM (Wikipedia↗) - is an - email - authentication method - designed to avoid email spoofing.
- Setting up DKIM is highly recommended to reduce the chance your emails ending up in the recipient's Spam folder. -
- -
Add the following CNAME DNS record to your domain.
- -
- Record: CNAME
- Domain: dkim._domainkey
- Value: - - {{ dkim_cname }}. - -
- -
- Some DNS registrar might require a full record path, in this case please use - dkim._domainkey.{{ custom_domain.domain }} as domain value instead.
- If you are using a subdomain, e.g. subdomain.domain.com, - you need to use dkim._domainkey.subdomain as domain value instead. -
-
-
- If you are using CloudFlare, please make sure to not select the Proxy option.

- -
- -
- - {% if custom_domain.dkim_verified %} - - {% else %} - - {% endif %} -
- - {% if not dkim_ok %} -
- Your DNS is not correctly set. - {% if dkim_errors %} - The CNAME record we obtain for - dkim._domainkey.{{ custom_domain.domain }} is: +
+ + {% if custom_domain.verified %} + + {% else %} + + {% endif %} +
+ {% if not mx_ok %} +
+ Your DNS is not correctly set. The MX record we obtain is:
- {% for r in dkim_errors %} + {% if not mx_errors %} + (Empty) + {% endif %} + {% for r in mx_errors %} {{ r }}
{% endfor %}
- {% endif %} + {% if custom_domain.verified %} +
+ Without the MX record set up correctly, you can miss emails sent to your aliases. + Please update the MX record ASAP. +
+ {% endif %} +
+ {% endif %} +
- {% if custom_domain.dkim_verified %} - Without DKIM setup, emails you sent from your alias might end up in Spam/Junk folder. +
+ +
+
2. SPF (Optional) + {% if custom_domain.spf_verified %} + âś… + {% else %} + đźš« {% endif %}
- {% endif %} -
-
+
+ SPF (Wikipedia↗) is an email + authentication method + designed to detect forging sender addresses during the delivery of the email.
+ Setting up SPF is highly recommended to reduce the chance your emails ending up in the recipient's Spam + folder. +
-
-
4. DMARC (Optional) - {% if custom_domain.dmarc_verified %} - âś… - {% else %} - đźš« +
Add the following TXT DNS record to your domain.
+ +
+ Record: TXT
+ Domain: {{ custom_domain.domain }} or + @
+ Value: + + {{ spf_record }} + +
+ +
+ + {% if custom_domain.spf_verified %} + + {% else %} + + {% endif %} +
+ + {% if not spf_ok %} +
+ Your DNS is not correctly set. The TXT record we obtain is: +
+ {% if not spf_errors %} + (Empty) + {% endif %} + + {% for r in spf_errors %} + {{ r }}
+ {% endfor %} +
+ {% if custom_domain.spf_verified %} + Without SPF setup, emails you sent from your alias might end up in Spam/Junk folder. + {% endif %} +
{% endif %}
-
- DMARC (Wikipedia↗) - is designed to protect the domain from unauthorized use, commonly known as email spoofing.
- Built around SPF and DKIM, a DMARC policy tells the receiving mail server what to do if - neither of those authentication methods passes. -
+
-
Add the following TXT DNS record to your domain.
+
+
3. DKIM (Optional) + {% if custom_domain.dkim_verified %} + âś… + {% else %} + đźš« + {% endif %} +
-
- Record: TXT
- Domain: _dmarc
- Value: - - {{ dmarc_record }} - -
+
+ DKIM (Wikipedia↗) + is an + email + authentication method + designed to avoid email spoofing.
+ Setting up DKIM is highly recommended to reduce the chance your emails ending up in the recipient's Spam + folder. +
-
- Some DNS registrar might require a full record path, in this case please use - _dmarc.{{ custom_domain.domain }} as domain value instead.
- If you are using a subdomain, e.g. subdomain.domain.com, - you need to use _dmarc.subdomain as domain value instead. -
-
+
Add the following CNAME DNS record to your domain.
-
- - {% if custom_domain.dmarc_verified %} - - {% else %} - - {% endif %} -
+
+ Record: CNAME
+ Domain: dkim._domainkey
+ Value: + + {{ dkim_cname }}. + +
- {% if not dmarc_ok %} -
- Your DNS is not correctly set. - The TXT record we obtain is: -
- {% if not dmarc_errors %} - (Empty) +
+ Some DNS registrar might require a full record path, in this case please use + dkim._domainkey.{{ custom_domain.domain }} as domain value instead.
+ If you are using a subdomain, e.g. subdomain.domain.com, + you need to use dkim._domainkey.subdomain as domain value instead. +
+
+
+ If you are using CloudFlare, please make sure to not select the Proxy option.

+ +
+ +
+ + {% if custom_domain.dkim_verified %} + + {% else %} + + {% endif %} +
+ + {% if not dkim_ok %} +
+ Your DNS is not correctly set. + {% if dkim_errors %} + The CNAME record we obtain for + dkim._domainkey.{{ custom_domain.domain }} is: + +
+ {% for r in dkim_errors %} + {{ r }}
+ {% endfor %} +
{% endif %} - {% for r in dmarc_errors %} - {{ r }}
- {% endfor %} + {% if custom_domain.dkim_verified %} + Without DKIM setup, emails you sent from your alias might end up in Spam/Junk folder. + {% endif %}
+ {% endif %} +
+ +
+ +
+
4. DMARC (Optional) {% if custom_domain.dmarc_verified %} - Without DMARC setup, emails sent from your alias might end up in the Spam/Junk folder. + âś… + {% else %} + đźš« {% endif %}
- {% endif %} + +
+ DMARC (Wikipedia↗) + is designed to protect the domain from unauthorized use, commonly known as email spoofing.
+ Built around SPF and DKIM, a DMARC policy tells the receiving mail server what to do if + neither of those authentication methods passes. +
+ +
Add the following TXT DNS record to your domain.
+ +
+ Record: TXT
+ Domain: _dmarc
+ Value: + + {{ dmarc_record }} + +
+ +
+ Some DNS registrar might require a full record path, in this case please use + _dmarc.{{ custom_domain.domain }} as domain value instead.
+ If you are using a subdomain, e.g. subdomain.domain.com, + you need to use _dmarc.subdomain as domain value instead. +
+
+ +
+ + {% if custom_domain.dmarc_verified %} + + {% else %} + + {% endif %} +
+ + {% if not dmarc_ok %} +
+ Your DNS is not correctly set. + The TXT record we obtain is: +
+ {% if not dmarc_errors %} + (Empty) + {% endif %} + + {% for r in dmarc_errors %} + {{ r }}
+ {% endfor %} +
+ {% if custom_domain.dmarc_verified %} + Without DMARC setup, emails sent from your alias might end up in the Spam/Junk folder. + {% endif %} +
+ {% endif %} +
diff --git a/app/dashboard/templates/dashboard/domain_detail/info.html b/app/dashboard/templates/dashboard/domain_detail/info.html index fa03e344..5d860010 100644 --- a/app/dashboard/templates/dashboard/domain_detail/info.html +++ b/app/dashboard/templates/dashboard/domain_detail/info.html @@ -7,39 +7,18 @@ {% endblock %} {% block domain_detail_content %} -

{{ custom_domain.domain }} - {% if custom_domain.verified %} - âś… - {% else %} - - đźš« - - - {% endif %} -

+

{{ custom_domain.domain }}

-
Created {{ custom_domain.created_at | dt }}
- - {{ nb_alias }} aliases +
Created {{ custom_domain.created_at | dt }}. {{ nb_alias }} aliases

-
Catch All
-
- This feature allows you to create aliases on the fly. - Simply use anything@{{ custom_domain.domain }} - next time you need an email address.
- The alias will be created the first time it receives an email - and automatically belong to {{ custom_domain.domain }} mailboxes ( - {% for mailbox in custom_domain.mailboxes %} - {{ mailbox.email }} - {% if not loop.last %},{% endif %} - {% endfor %}) -
+

Auto create/on the fly alias

+
+
+
+ Simply use anything@{{ custom_domain.domain }} + next time you need an alias: it'll be automatically + created the first time it receives an email. + To have more fine-grained control, you can also define + auto create + rules + . +
+ + +
+ +
+
Auto-created aliases are automatically owned by the following mailboxes + . +
+ {% set domain_mailboxes=custom_domain.mailboxes %} +
+ + + +
+
+ +
+
+ +
+
+
+
-
Default Alias Name
-
- This name will be used as the default alias name when you send - or reply from an alias, unless overwritten by the alias specific name. +

Default Display Name

+
+ Default display name for aliases created with {{ custom_domain.domain }} + unless overwritten by the alias display name.
-
+
- + placeholder="Alias Display Name">
- + {% if custom_domain.name %} - + {% endif %}

-
Random Prefix Generation
-
- A random prefix can be generated for this domain for usage in the New Alias - feature. +

Random Prefix Generation

+
+ Add a random prefix for this domain when creating a new alias.
@@ -106,20 +128,22 @@

-

Delete Domain

-
Please note that this operation is irreversible. - All aliases associated with this domain will be also deleted. +

Delete Domain

+
This operation is irreversible. + All aliases associated with this domain will be deleted.
- Delete domain + Delete domain
{% endblock %} {% block script %} {% endblock %} + diff --git a/app/dashboard/templates/dashboard/index.html b/app/dashboard/templates/dashboard/index.html index 731c2a79..7f0ae62a 100644 --- a/app/dashboard/templates/dashboard/index.html +++ b/app/dashboard/templates/dashboard/index.html @@ -92,124 +92,154 @@
- -
- -
-
-
- - -
-
-
- - -
- -