commit 81a6b562b65a2489d89ee8c7d23d357958639f37
Author: Daniel Winzen <daniel@danwin1210.de>
Date:   Sun May 15 21:39:39 2022 +0200

    Initial commit

diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..94a9ed0
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is 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.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  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.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  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 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. Use with the GNU Affero General Public License.
+
+  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 Affero 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 special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU 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 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 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 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.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU 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 General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  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 GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..80df1fe
--- /dev/null
+++ b/README.md
@@ -0,0 +1,9 @@
+General Information:
+--------------------
+
+This is a setup for a Tor based email hosting server. It is provided as is and before putting it into production you should make changes according to your needs. This is a work in progress and you should carefully check the commit history for changes before updating.
+
+Installation Instructions:
+--------------------------
+
+TODO
diff --git a/common_config.php b/common_config.php
new file mode 100644
index 0000000..0293f22
--- /dev/null
+++ b/common_config.php
@@ -0,0 +1,236 @@
+<?php
+
+use Egulias\EmailValidator\EmailLexer;
+use Egulias\EmailValidator\EmailParser;
+use Egulias\EmailValidator\EmailValidator;
+use Egulias\EmailValidator\Validation\NoRFCWarningsValidation;
+
+const DBHOST = 'localhost'; // Database host
+const DBUSER = 'postfix'; // Database user
+const DBPASS = 'YOUR_PASSWORD'; // Database password
+const DBNAME = 'postfix'; // Database
+const PERSISTENT = true; // persistent database connection
+const CAPTCHA_DIFFICULTY = 1; // captcha difficulty from 0 to 3
+
+require_once( 'vendor/autoload.php' );
+
+function get_db_instance(): PDO
+{
+	static $db = null;
+	if ( $db !== null ) {
+		return $db;
+	}
+	try {
+		$db = new PDO( 'mysql:host=' . DBHOST . ';dbname=' . DBNAME, DBUSER, DBPASS, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_PERSISTENT => PERSISTENT ] );
+	} catch ( PDOException ) {
+		http_response_code( 500 );
+		die( 'No Connection to MySQL database!' );
+	}
+	return $db;
+}
+
+function z_base32_encode( string $input ): string
+{
+	$map = [
+		'y', 'b', 'n', 'd', 'r', 'f', 'g', '8', //  7
+		'e', 'j', 'k', 'm', 'c', 'p', 'q', 'x', // 15
+		'o', 't', '1', 'u', 'w', 'i', 's', 'z', // 23
+		'a', '3', '4', '5', 'h', '7', '6', '9', // 31
+	];
+	if ( empty( $input ) ) {
+		return '';
+	}
+	$input = str_split( $input );
+	$binaryString = '';
+	$c = count( $input );
+	for ( $i = 0; $i < $c; ++$i ) {
+		$binaryString .= str_pad( decbin( ord( $input[ $i ] ) ), 8, '0', STR_PAD_LEFT );
+	}
+	$fiveBitBinaryArray = str_split( $binaryString, 5 );
+	$base32 = '';
+	$i = 0;
+	$c = count( $fiveBitBinaryArray );
+	while ( $i < $c ) {
+		$base32 .= $map[ bindec( $fiveBitBinaryArray[ $i ] ) ];
+		++$i;
+	}
+	return $base32;
+}
+
+function send_captcha(): void
+{
+	if ( CAPTCHA_DIFFICULTY === 0 || ! extension_loaded( 'gd' ) ) {
+		return;
+	}
+	$db = get_db_instance();
+	$captchachars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+	$length = strlen( $captchachars ) - 1;
+	$code = '';
+	for ( $i = 0; $i < 5; ++$i ) {
+		$code .= $captchachars[ mt_rand( 0, $length ) ];
+	}
+	$randid = mt_rand();
+	$time = time();
+	$stmt = $db->prepare( 'INSERT INTO captcha (id, time, code) VALUES (?, ?, ?);' );
+	$stmt->execute( [ $randid, $time, $code ] );
+	echo '<div class="row"><div class="col"><td>Copy:<br>';
+	if ( CAPTCHA_DIFFICULTY === 1 ) {
+		$im = imagecreatetruecolor( 55, 24 );
+		$bg = imagecolorallocate( $im, 0, 0, 0 );
+		$fg = imagecolorallocate( $im, 255, 255, 255 );
+		imagefill( $im, 0, 0, $bg );
+		imagestring( $im, 5, 5, 5, $code, $fg );
+		echo '<img alt="" width="55" height="24" src="data:image/gif;base64,';
+	} elseif ( CAPTCHA_DIFFICULTY === 2 ) {
+		$im = imagecreatetruecolor( 55, 24 );
+		$bg = imagecolorallocate( $im, 0, 0, 0 );
+		$fg = imagecolorallocate( $im, 255, 255, 255 );
+		imagefill( $im, 0, 0, $bg );
+		imagestring( $im, 5, 5, 5, $code, $fg );
+		$line = imagecolorallocate( $im, 255, 255, 255 );
+		for ( $i = 0; $i < 2; ++$i ) {
+			imageline( $im, 0, mt_rand( 0, 24 ), 55, mt_rand( 0, 24 ), $line );
+		}
+		$dots = imagecolorallocate( $im, 255, 255, 255 );
+		for ( $i = 0; $i < 100; ++$i ) {
+			imagesetpixel( $im, mt_rand( 0, 55 ), mt_rand( 0, 24 ), $dots );
+		}
+		echo '<img alt="" width="55" height="24" src="data:image/gif;base64,';
+	} else {
+		$im = imagecreatetruecolor( 150, 200 );
+		$bg = imagecolorallocate( $im, 0, 0, 0 );
+		$fg = imagecolorallocate( $im, 255, 255, 255 );
+		imagefill( $im, 0, 0, $bg );
+		$chars = [];
+		$x = $y = 0;
+		for ( $i = 0; $i < 10; ++$i ) {
+			$found = false;
+			while ( ! $found ) {
+				$x = mt_rand( 10, 140 );
+				$y = mt_rand( 10, 180 );
+				$found = true;
+				foreach ( $chars as $char ) {
+					if ( $char[ 'x' ] >= $x && ( $char[ 'x' ] - $x ) < 25 ) {
+						$found = false;
+					} elseif ( $char[ 'x' ] < $x && ( $x - $char[ 'x' ] ) < 25 ) {
+						$found = false;
+					}
+					if ( ! $found ) {
+						if ( $char[ 'y' ] >= $y && ( $char[ 'y' ] - $y ) < 25 ) {
+							break;
+						} elseif ( $char[ 'y' ] < $y && ( $y - $char[ 'y' ] ) < 25 ) {
+							break;
+						} else {
+							$found = true;
+						}
+					}
+				}
+			}
+			$chars[] = [ 'x', 'y' ];
+			$chars[ $i ][ 'x' ] = $x;
+			$chars[ $i ][ 'y' ] = $y;
+			if ( $i < 5 ) {
+				imagechar( $im, 5, $chars[ $i ][ 'x' ], $chars[ $i ][ 'y' ], $captchachars[ mt_rand( 0, $length ) ], $fg );
+			} else {
+				imagechar( $im, 5, $chars[ $i ][ 'x' ], $chars[ $i ][ 'y' ], $code[ $i - 5 ], $fg );
+			}
+		}
+		$follow = imagecolorallocate( $im, 200, 0, 0 );
+		imagearc( $im, $chars[ 5 ][ 'x' ] + 4, $chars[ 5 ][ 'y' ] + 8, 16, 16, 0, 360, $follow );
+		for ( $i = 5; $i < 9; ++$i ) {
+			imageline( $im, $chars[ $i ][ 'x' ] + 4, $chars[ $i ][ 'y' ] + 8, $chars[ $i + 1 ][ 'x' ] + 4, $chars[ $i + 1 ][ 'y' ] + 8, $follow );
+		}
+		$line = imagecolorallocate( $im, 255, 255, 255 );
+		for ( $i = 0; $i < 5; ++$i ) {
+			imageline( $im, 0, mt_rand( 0, 200 ), 150, mt_rand( 0, 200 ), $line );
+		}
+		$dots = imagecolorallocate( $im, 255, 255, 255 );
+		for ( $i = 0; $i < 1000; ++$i ) {
+			imagesetpixel( $im, mt_rand( 0, 150 ), mt_rand( 0, 200 ), $dots );
+		}
+		echo '<img alt="" width="150" height="200" src="data:image/gif;base64,';
+	}
+	ob_start();
+	imagegif( $im );
+	imagedestroy( $im );
+	echo base64_encode( ob_get_clean() ) . '">';
+	echo '</div><div class="col"><input type="hidden" name="challenge" value="' . $randid . '"><input type="text" name="captcha" size="15" autocomplete="off" required></div></div>';
+}
+
+function check_captcha( string $challenge, string $captcha_code ): bool
+{
+	$db = get_db_instance();
+	if ( CAPTCHA_DIFFICULTY > 0 ) {
+		if ( empty( $challenge ) ) {
+			return false;
+		}
+		$code = '';
+		$stmt = $db->prepare( 'SELECT code FROM captcha WHERE id=?;' );
+		$stmt->execute( [ $challenge ] );
+		$stmt->bindColumn( 1, $code );
+		if ( ! $stmt->fetch( PDO::FETCH_BOUND ) ) {
+			return false;
+		}
+		$time = time();
+		$stmt = $db->prepare( 'DELETE FROM captcha WHERE id=? OR time < ?;' );
+		$stmt->execute( [ $challenge, $time - 600 ] );
+		if ( $captcha_code !== $code ) {
+			if ( CAPTCHA_DIFFICULTY !== 3 || strrev( $captcha_code ) !== $code ) {
+				return false;
+			}
+		}
+	}
+	return true;
+}
+
+function validate_email_list( array $targets, string &$msg = '' ): string
+{
+	$alias_goto = '';
+	$targets = array_unique( $targets );
+	foreach ( $targets as $email ) {
+		$validator = new EmailValidator();
+		if ( $validator->isValid( $email, new NoRFCWarningsValidation() ) ) {
+			$alias_goto .= ",$email";
+		} else {
+			$msg .= '<div class="red" role="alert">Oops, the email "' . htmlspecialchars( $email ) . '" doesn\' look like a valid email address and thus wasn\'t added to the forwarding list.</div>';
+		}
+	}
+	return ltrim( $alias_goto, ',' );
+}
+
+function check_domain_access( string &$email, string &$msg = '' ): bool
+{
+	if ( ! $_SESSION[ 'email_admin_superadmin' ] ) {
+		$db = get_db_instance();
+		$parser = new EmailParser( new EmailLexer() );
+		$parser->parse( $email );
+		$domain = $parser->getDomainPart();
+		$stmt = $db->prepare( 'SELECT target_domain FROM alias_domain WHERE alias_domain = ? AND active=1;' );
+		$stmt->execute( [ $domain ] );
+		if ( $tmp = $stmt->fetch( PDO::FETCH_ASSOC ) ) {
+			$domain = $tmp[ 'target_domain' ];
+			$email = preg_replace( '~@[^@+]$~iu', "@$domain", $email );
+		}
+		$managed_domains = [];
+		$stmt = $db->prepare( 'SELECT domain FROM domain_admins WHERE username = ?;' );
+		$stmt->execute( [ $_SESSION[ 'email_admin_user' ] ] );
+		while ( $tmp = $stmt->fetch( PDO::FETCH_ASSOC ) ) {
+			$managed_domains [] = $tmp[ 'domain' ];
+		}
+		if ( ! in_array( $domain, $managed_domains, true ) ) {
+			$msg .= '<div class="red" role="alert">You are not allowed to manage this domain.</div>';
+			return false;
+		}
+	}
+	return true;
+}
+
+function check_email_valid( string $email, string &$msg = '' ): bool
+{
+	$validator = new EmailValidator();
+	if ( ! $validator->isValid( $email, new NoRFCWarningsValidation() ) ) {
+		$msg .= '<div class="red" role="alert">Invalid email address.</div>';
+		return false;
+	}
+	return true;
+}
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..7287613
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,8 @@
+{
+    "require": {
+        "egulias/email-validator": "^3.1",
+        "ext-mbstring": "*",
+        "ext-pdo": "*",
+        "ext-gnupg": "*"
+    }
+}
diff --git a/cron.php b/cron.php
new file mode 100644
index 0000000..d9ba29c
--- /dev/null
+++ b/cron.php
@@ -0,0 +1,81 @@
+<?php
+if ( php_sapi_name() !== 'cli' ) {
+	exit;
+}
+const DBHOST_PROSODY = 'localhost'; // Database host
+const DBUSER_PROSODY = 'prosody'; // Database user
+const DBPASS_PROSODY = 'YOUR_PASSWORD'; // Database password
+const DBNAME_PROSODY = 'prosody'; // Database
+
+require_once 'common_config.php';
+$db = get_db_instance();
+try {
+	$db_prosody = new PDO( 'mysql:host=' . DBHOST_PROSODY . ';dbname=' . DBNAME_PROSODY, DBUSER_PROSODY, DBPASS_PROSODY, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ] );
+} catch ( PDOException $e ) {
+	die( 'No Connection to MySQL database!' );
+}
+setlocale( LC_CTYPE, 'C.UTF-8' ); // make sure to use UTF-8 locale. Non UTF-8 locales can cause serious issues when handling UTF-8 file names
+
+// mark accounts deletable that haven't been used in an entire year
+$expire_time = strtotime( '-1 year' );
+$stmt = $db->prepare( 'UPDATE mailbox SET active = -2 WHERE active IN (0, 1) AND modified < ? AND (last_login < ? OR last_login IS NULL);' );
+$stmt->execute( [ date( 'Y-m-d H:i:s', $expire_time ), $expire_time ] );
+
+// delete all associated data when deleting/disabling accounts
+$stmt = $db->query( 'SELECT username, local_part, domain, active FROM mailbox WHERE active IN (-1, -2);' );
+$disable = $db->prepare( 'UPDATE mailbox SET active = 0 WHERE username = ?;' );
+$delete = $db->prepare( 'DELETE FROM mailbox WHERE username = ?;' );
+$delete_alias = $db->prepare( 'DELETE FROM alias WHERE address = ?;' );
+$delete_prosody = $db_prosody->prepare( 'DELETE FROM prosody WHERE user = ? AND host = ?;' );
+$delete_prosody_archive = $db_prosody->prepare( 'DELETE FROM prosodyarchive WHERE user = ? AND host = ?;' );
+while ( $tmp = $stmt->fetch( PDO::FETCH_ASSOC ) ) {
+	$domain_basename = basename( $tmp[ 'domain' ] );
+	$local_basename = basename( $tmp[ 'local_part' ] );
+	if ( ! in_array( $domain_basename, [ '..', '.', '' ], true ) && ! in_array( $local_basename, [ '..', '.', '' ], true ) ) {
+		$mail_files = '/var/mail/vmail/' . $domain_basename . '/' . $local_basename;
+		if ( file_exists( $mail_files ) ) {
+			exec( 'rm -r ' . escapeshellarg( $mail_files ) );
+		}
+		@unlink( '/var/local/squirrelmail/data/' . $local_basename . '@' . $domain_basename . '.pref' );
+		@unlink( '/var/local/squirrelmail/data/' . $local_basename . '@' . $domain_basename . '.abook' );
+		if ( $tmp[ 'domain' ] === 'danwin1210.de' ) {
+			@unlink( '/var/local/squirrelmail/data/' . $local_basename . '.pref' );
+			@unlink( '/var/local/squirrelmail/data/' . $local_basename . '.abook' );
+			@unlink( '/var/local/squirrelmail/data/' . $local_basename . '@danielas3rtn54uwmofdo3x2bsdifr47huasnmbgqzfrec5ubupvtpid.onion.pref' );
+			@unlink( '/var/local/squirrelmail/data/' . $local_basename . '@danielas3rtn54uwmofdo3x2bsdifr47huasnmbgqzfrec5ubupvtpid.onion.abook' );
+			$delete_prosody->execute( [ $tmp[ 'local_part' ], $tmp[ 'domain' ] ] );
+			$delete_prosody_archive->execute( [ $tmp[ 'local_part' ], $tmp[ 'domain' ] ] );
+		}
+	}
+	if ( $tmp[ 'active' ] === '-2' ) {
+		$delete->execute( [ $tmp[ 'username' ] ] );
+	}
+	if ( $tmp[ 'active' ] === '-1' ) {
+		$disable->execute( [ $tmp[ 'username' ] ] );
+	}
+	$delete_alias->execute( [ $tmp[ 'username' ] ] );
+}
+$stmt = $db->query( 'SELECT domain FROM domain WHERE active = -1;' );
+$del_domain = $db->prepare( 'DELETE FROM domain WHERE domain = ?;' );
+$del_mailbox = $db->prepare( 'DELETE FROM mailbox WHERE domain = ?;' );
+$del_alias = $db->prepare( 'DELETE FROM alias WHERE domain = ?;' );
+while ( $tmp = $stmt->fetch( PDO::FETCH_ASSOC ) ) {
+	$domain_basename = basename( $tmp[ 'domain' ] );
+	if ( ! in_array( $domain_basename, [ '..', '.', '' ], true ) ) {
+		$del_alias->execute( [ $tmp[ 'domain' ] ] );
+		$del_mailbox->execute( [ $tmp[ 'domain' ] ] );
+		$files = glob( '/var/local/squirrelmail/data/*@' . $domain_basename . '.{pref,abook}', GLOB_BRACE );
+		$mail_files = '/var/mail/vmail/' . $domain_basename . '/';
+		if ( file_exists( $mail_files ) ) {
+			exec( 'rm -r ' . escapeshellarg( $mail_files ) );
+		}
+		if ( is_array( $files ) ) {
+			foreach ( $files as $file ) {
+				@unlink( $file );
+			}
+		}
+		$del_domain->execute( [ $tmp[ 'domain' ] ] );
+	}
+}
+// delete squirrelmail attachments older than an hour
+exec( 'find /var/local/squirrelmail/attach/ -type f -cmin +60 -delete' );
diff --git a/etc/dovecot/dovecot-dict-sql.conf.ext b/etc/dovecot/dovecot-dict-sql.conf.ext
new file mode 100644
index 0000000..fc02ddd
--- /dev/null
+++ b/etc/dovecot/dovecot-dict-sql.conf.ext
@@ -0,0 +1,12 @@
+connect = host=localhost dbname=postfix user=postfix password=YOUR_PASSWORD
+
+map {
+  pattern = shared/last-login/$user
+  table = mailbox
+  value_field = last_login
+  value_type = uint
+
+  fields {
+    username = $user
+  }
+}
diff --git a/etc/dovecot/dovecot-sql.conf.ext b/etc/dovecot/dovecot-sql.conf.ext
new file mode 100644
index 0000000..3d86993
--- /dev/null
+++ b/etc/dovecot/dovecot-sql.conf.ext
@@ -0,0 +1,10 @@
+connect = host=localhost dbname=postfix user=postfix_readonly password=YOUR_PASSWORD
+driver = mysql
+
+# Query to retrieve password. user can be used to retrieve username in other formats also.
+password_query = SELECT username AS user, CONCAT(password_hash_type, password) AS password, CONCAT(domain, '/', local_part, '/') AS maildir, 5000 AS userdb_uid, 5000 AS userdb_gid, CONCAT('*:bytes=', quota) AS userdb_quota_rule FROM mailbox WHERE username = CONCAT('%n', '@', COALESCE((SELECT target_domain FROM alias_domain WHERE alias_domain = '%d' AND active='1'), '%d')) AND active='1'
+
+# Query to retrieve user information, note uid matches dovecot.conf AND Postfix virtual_uid_maps parameter.
+user_query = SELECT CONCAT(domain, '/', local_part, '/') AS maildir, 5000 AS uid, 5000 AS gid, CONCAT('*:bytes=', quota) AS quota_rule FROM mailbox WHERE username = CONCAT('%n', '@', COALESCE((SELECT target_domain FROM alias_domain WHERE alias_domain = '%d' AND active='1'), '%d')) AND active='1'
+
+iterate_query = SELECT username AS user FROM mailbox
diff --git a/etc/dovecot/dovecot.conf b/etc/dovecot/dovecot.conf
new file mode 100644
index 0000000..829493b
--- /dev/null
+++ b/etc/dovecot/dovecot.conf
@@ -0,0 +1,180 @@
+#general settings
+listen = *, ::
+login_greeting = Server ready.
+mmap_disable = yes
+mail_fsync = always
+mail_nfs_index = yes
+mail_nfs_storage = yes
+info_log_path = /dev/null
+auth_verbose = no
+auth_verbose_passwords = no
+auth_debug = no
+auth_debug_passwords = no
+mail_debug = no
+verbose_ssl = no
+mail_location = maildir:/var/mail/vmail/%d/%n
+mail_plugins = $mail_plugins mail_crypt zlib
+mailbox_list_index = yes
+
+#plugin setup
+plugin {
+  mail_crypt_save_version = 2
+  mail_crypt_global_private_key = </etc/dovecot/ecprivkey.pem
+  mail_crypt_global_public_key = </etc/dovecot/ecpubkey.pem
+  zlib_save = gz
+  zlib_save_level = 6
+  quota_grace = 10%%
+  quota_status_success = DUNNO
+  quota_status_nouser = DUNNO
+  quota_status_overquota = "552 5.2.2 Mailbox is full"
+  quota = count:User quota
+  quota_rule = *:bytes=50M
+  quota_vsizes = yes
+  last_login_dict = proxy::lastlogin
+  last_login_key = last-login/%u
+}
+
+#auth settings
+disable_plaintext_auth = yes
+auth_cache_size = 1M
+auth_cache_ttl = 5mins
+auth_cache_negative_ttl = 5mins
+auth_default_realm = danwin1210.de
+auth_username_chars = 
+auth_mechanisms = plain login
+
+#TLS parameters
+ssl = required
+ssl_cert = </etc/acme.sh/danwin1210.de_ecc/fullchain.cer
+ssl_key = </etc/acme.sh/danwin1210.de_ecc/danwin1210.de.key
+ssl_client_ca_dir = /etc/ssl/certs
+ssl_dh = </etc/dovecot/dh.pem
+ssl_min_protocol = TLSv1.2
+ssl_cipher_list = HIGH:!PSK:!aNULL:!MD5:!SHA:!CAMELLIA:!AES+SHA256:!AES+SHA384;
+ssl_curve_list = X448:X25519:secp521r1:secp384r1
+ssl_prefer_server_ciphers = yes
+
+#protocol setup
+protocols = "imap pop3 lmtp"
+protocol lmtp {
+	postmaster_address = postmaster@danwin1210.de
+}
+protocol imap {
+  mail_plugins = $mail_plugins quota imap_quota imap_zlib last_login
+}
+protocol pop3 {
+  mail_plugins = $mail_plugins last_login
+}
+
+#service setup
+service anvil {
+  unix_listener anvil-auth-penalty {
+    #disable since we don't have IP info
+    mode = 0
+  }
+}
+service auth {
+  unix_listener auth-userdb {
+    mode = 0666
+    user = postfix
+    group = postfix
+  }
+  unix_listener /var/spool/postfix/private/auth {
+    mode = 0666
+    user = postfix
+    group = postfix
+  }
+
+  inet_listener {
+    port = 12345
+  }
+
+  user = dovecot
+  group = dovecot
+  client_limit=2448
+}
+service auth-worker {
+  unix_listener auth-worker {
+    mode = 0666
+    user = dovecot
+    group = dovecot
+  }
+}
+service imap {
+  service_count = 1000
+  client_limit = 1
+}
+service imap-login {
+  inet_listener imap {
+    port = 143
+  }
+  service_count = 1000
+  vsz_limit = 1G
+  process_min_avail = 4
+}
+service lmtp {
+  unix_listener /var/spool/postfix/private/dovecot-lmtp {
+    mode = 0660
+    user = postfix
+    group = postfix
+  }
+  user = vmail
+  group = vmail
+}
+service pop3 {
+  service_count = 1000
+  client_limit = 1
+}
+service pop3-login {
+  inet_listener pop3 {
+    port = 110
+  }
+  service_count = 1000
+  vsz_limit = 1G
+}
+service quota-status {
+  executable = quota-status -p postfix
+  inet_listener quota-status {
+    port = 12340
+  }
+  client_limit = 1
+}
+
+#SQL queries
+passdb {
+  driver = sql
+  args = /etc/dovecot/dovecot-sql.conf.ext
+}
+userdb {
+  driver = prefetch
+}
+userdb {
+  driver = sql
+  args = /etc/dovecot/dovecot-sql.conf.ext
+}
+dict {
+  lastlogin = mysql:/etc/dovecot/dovecot-dict-sql.conf.ext
+  user = dovecot
+  group = dovecot
+}
+
+#namespace configuration
+namespace inbox {
+  inbox = yes
+  mailbox Drafts {
+    special_use = \Drafts
+  }
+  mailbox Junk {
+    special_use = \Junk
+  }
+  mailbox Trash {
+    special_use = \Trash
+  }
+
+  mailbox Sent {
+    special_use = \Sent
+  }
+  mailbox "Sent Messages" {
+    special_use = \Sent
+  }
+}
diff --git a/etc/mysql/mariadb.conf.d/manual_settings.cnf b/etc/mysql/mariadb.conf.d/manual_settings.cnf
new file mode 100644
index 0000000..5979e8a
--- /dev/null
+++ b/etc/mysql/mariadb.conf.d/manual_settings.cnf
@@ -0,0 +1,44 @@
+[client]
+default-character-set = utf8mb4
+
+[mysqld]
+character_set_server = utf8mb4
+collation_server = utf8mb4_general_ci
+innodb_buffer_pool_size = 64M
+#myisam_sort_buffer_size = 200K
+#bulk_insert_buffer_size = 1M
+#sort_buffer_size = 1M
+innodb_log_buffer_size = 16M
+innodb_log_file_size = 16M
+innodb_flush_log_at_trx_commit = 0
+innodb_defragment = 1
+skip_name_resolve = 1
+query_cache_size = 16M
+query_cache_limit = 4M
+max_connections = 200
+table_open_cache = 15000
+tmp_table_size = 16M
+max_heap_table_size = 16M
+join_buffer_size = 4M
+aria_pagecache_buffer_size = 8M
+aria_sort_buffer_size = 8M
+open_files_limit = 100000
+bind_address = 0.0.0.0
+sql_mode=ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER
+plugin_load_add = file_key_management
+loose_file_key_management_filename = /etc/mysql/encryption/keyfile.enc
+loose_file_key_management_filekey = FILE:/etc/mysql/encryption/keyfile.key
+loose_file_key_management_encryption_algorithm = AES_CTR
+innodb_encrypt_tables = FORCE
+innodb_encrypt_temporary_tables = ON
+innodb_encrypt_log = ON
+encrypt_tmp_files = ON
+encrypt_tmp_disk_tables = ON
+enforce_storage_engine = InnoDB
+encrypt_binlog=ON
+innodb_compression_default=ON
+innodb_compression_algorithm=zlib
+innodb_rollback_on_timeout=1
+innodb_lock_wait_timeout=5
+binlog_row_image = minimal
+binlog_format = ROW
diff --git a/etc/postfix/body_checks b/etc/postfix/body_checks
new file mode 100644
index 0000000..e69de29
diff --git a/etc/postfix/header_checks b/etc/postfix/header_checks
new file mode 100644
index 0000000..a5667f8
--- /dev/null
+++ b/etc/postfix/header_checks
@@ -0,0 +1,2 @@
+/^To:.*@[^@]*\.de-mail\.de/ REJECT "Sorry, you tried contacting a De-Mail Address. This is a special System by the German government, not compatible with E-Mail. More info here: https://www.cio.bund.de/Web/DE/Innovative-Vorhaben/De-Mail/de_mail_node.html"
+/^(To|From).*@example\.com/    REJECT
diff --git a/etc/postfix/helo_checks b/etc/postfix/helo_checks
new file mode 100644
index 0000000..167865c
--- /dev/null
+++ b/etc/postfix/helo_checks
@@ -0,0 +1 @@
+DESKTOPRaihan REJECT
diff --git a/etc/postfix/main.cf b/etc/postfix/main.cf
new file mode 100644
index 0000000..14d0756
--- /dev/null
+++ b/etc/postfix/main.cf
@@ -0,0 +1,87 @@
+# general configuration options
+smtpd_banner = danwin1210.de ESMTP $mail_name (Debian/GNU)
+biff = no
+append_dot_mydomain = no
+delay_warning_time = 10h
+myhostname = danwin1210.de
+alias_maps = 
+alias_database = 
+myorigin = danwin1210.de
+mydestination = 
+mynetworks = 127.0.0.0/8 192.168.178.0/24 10.9.0.0/24
+recipient_delimiter = +
+inet_interfaces = all
+inet_protocols = all
+ignore_mx_lookup_error = yes
+always_add_missing_headers = yes
+message_drop_headers = bcc content-length resent-bcc return-path x-mailer x-originating-ip user-agent x-received x-sender-ip x-client-ip x-client-hostname
+notify_classes = 2bounce data delay resource software
+message_size_limit = 52428800
+backwards_bounce_logfile_compatibility = no
+show_user_unknown_table_name = no
+virtual_transport = lmtp:unix:/private/dovecot-lmtp
+compatibility_level = 3.6
+smtputf8_autodetect_classes = all
+
+# TLS parameters
+smtpd_tls_cert_file = /etc/acme.sh/danwin1210.de_ecc/fullchain.cer
+smtpd_tls_key_file = /etc/acme.sh/danwin1210.de_ecc/danwin1210.de.key
+smtpd_tls_ciphers = HIGH
+smtpd_tls_mandatory_ciphers = HIGH
+smtp_tls_ciphers = HIGH
+smtp_tls_mandatory_ciphers = HIGH
+tls_eecdh_auto_curves = X448 X25519 secp521r1 secp384r1 prime256v1
+smtpd_tls_protocols = TLSv1.2 TLSv1.3
+smtp_tls_protocols = TLSv1.2 TLSv1.3
+smtpd_tls_exclude_ciphers = aNULL MD5 SHA CAMELLIA
+smtpd_tls_mandatory_exclude_ciphers = aNULL MD5 SHA CAMELLIA
+smtp_tls_exclude_ciphers = aNULL MD5 SHA CAMELLIA AES+SHA256 AES+SHA384
+smtp_tls_mandatory_exclude_ciphers = aNULL MD5 SHA CAMELLIA AES+SHA256 AES+SHA384
+tls_preempt_cipherlist = yes
+smtpd_tls_dh1024_param_file = /etc/postfix/dh4096.pem
+smtpd_tls_security_level = may
+smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
+smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
+smtp_tls_security_level = may
+smtp_tls_CApath = /etc/ssl/certs
+smtp_dns_support_level = dnssec
+tls_ssl_options = NO_RENEGOTIATION
+smtp_tls_chain_files = /etc/postfix/danwin1210-mail.chain
+smtpd_tls_received_header = yes
+
+#lookup maps for domains and email addresses
+relay_domains = torbox.danwin1210.me torbox.danwin1210.de
+canonical_maps = inline:{{@mail2tor.onion=@mail2tor.com}, {@torbox3uiot6wchz.onion=@torbox36ijlcevujx7mjb4oiusvwgvmue7jfn2cvutwa6kl6to3uyqad.onion}, {@torbox.onion=@torbox36ijlcevujx7mjb4oiusvwgvmue7jfn2cvutwa6kl6to3uyqad.onion}, {@torbox.danwin1210.me=@torbox36ijlcevujx7mjb4oiusvwgvmue7jfn2cvutwa6kl6to3uyqad.onion}, {@torbox.danwin1210.de=@torbox36ijlcevujx7mjb4oiusvwgvmue7jfn2cvutwa6kl6to3uyqad.onion}}
+sender_canonical_maps = inline:{{@localhost=@danwin1210.de}, {@danielas3rtn54uwmofdo3x2bsdifr47huasnmbgqzfrec5ubupvtpid.onion=@danwin1210.de}, {@danwin1210.me=@danwin1210.de}}
+transport_maps = inline:{{torbox3uiot6wchz.onion=relay:[torbox36ijlcevujx7mjb4oiusvwgvmue7jfn2cvutwa6kl6to3uyqad.onion]:25}, {.onion=smtp}, {mail2tor.com=relay:[xc7tgk2c5onxni2wsy76jslfsitxjbbptejnqhw6gy2ft7khpevhc7ad.onion]:25}, {blackhost.xyz=relay:[blackhost7pws76u6vohksdahnm6adf7riukgcmahrwt43wv2drvyxid.onion]:25}} proxy:mysql:/etc/postfix/sql/mysql_transport_maps.cf inline:{*=relay:[10.9.0.1]:1025}
+virtual_alias_maps = proxy:mysql:/etc/postfix/sql/mysql_virtual_alias_maps.cf
+virtual_mailbox_domains = proxy:mysql:/etc/postfix/sql/mysql_virtual_domains_maps.cf
+virtual_mailbox_maps = proxy:mysql:/etc/postfix/sql/mysql_virtual_mailbox_maps.cf
+
+#sasl authentication and restrictions
+smtpd_sasl_auth_enable = yes
+smtpd_sasl_security_options = noanonymous
+smtpd_sasl_local_domain = danwin1210.de
+smtpd_sasl_type = dovecot
+smtpd_sasl_path = private/auth
+smtpd_client_restrictions = reject_unauth_pipelining
+smtpd_recipient_restrictions = check_policy_service inet:127.0.0.1:12340, permit_sasl_authenticated, check_recipient_access proxy:mysql:/etc/postfix/sql/mysql_tls_policy_in.cf
+smtpd_sender_restrictions = reject_sender_login_mismatch, check_sender_access inline:{{<> = REJECT}}, permit_sasl_authenticated
+smtpd_relay_restrictions = permit_sasl_authenticated, permit_auth_destination, reject_unauth_destination
+smtpd_sender_login_maps = proxy:mysql:/etc/postfix/sql/mysql_virtual_auth_maps.cf
+smtpd_helo_restrictions = check_helo_access hash:/etc/postfix/helo_checks
+
+# anti-spam settings
+smtpd_milters = inet:127.0.0.1:11332
+non_smtpd_milters = inet:127.0.0.1:11332
+milter_default_action = accept
+milter_protocol = 6
+header_checks = regexp:/etc/postfix/header_checks
+body_checks = regexp:/etc/postfix/body_checks
+disable_vrfy_command = yes
+smtpd_discard_ehlo_keywords = silent-discard, dsn
+smtpd_delay_reject = yes
+smtpd_helo_required = yes
+strict_rfc821_envelopes = yes
+default_destination_concurrency_limit = 2
+smtpd_recipient_limit = 10
diff --git a/etc/postfix/master.cf b/etc/postfix/master.cf
new file mode 100644
index 0000000..c334016
--- /dev/null
+++ b/etc/postfix/master.cf
@@ -0,0 +1,132 @@
+#
+# Postfix master process configuration file.  For details on the format
+# of the file, see the master(5) manual page (command: "man 5 master" or
+# on-line: http://www.postfix.org/master.5.html).
+#
+# Do not forget to execute "postfix reload" after editing this file.
+#
+# ==========================================================================
+# service type  private unpriv  chroot  wakeup  maxproc command + args
+#               (yes)   (yes)   (no)    (never) (100)
+# ==========================================================================
+smtp      inet  n       -       n       -       2       smtpd
+ -o smtpd_sasl_auth_enable=no
+ -o { smtpd_sender_restrictions = check_sender_access inline:{double-bounce@mail.danwin1210.de=OK} check_sender_access mysql:/etc/postfix/sql/mysql_virtual_domain_unauth_block.cf }
+26      inet  n       -       n       -       2       smtpd
+ -o smtpd_sasl_auth_enable=no
+ -o { smtpd_sender_restrictions = check_sender_access mysql:/etc/postfix/sql/mysql_virtual_domain_unauth_block.cf }
+ -o smtpd_tls_security_level=may
+#smtp      inet  n       -       y       -       1       postscreen
+#smtpd     pass  -       -       y       -       -       smtpd
+#dnsblog   unix  -       -       y       -       0       dnsblog
+#tlsproxy  unix  -       -       y       -       0       tlsproxy
+#submission inet n       -       y       -       -       smtpd
+#  -o syslog_name=postfix/submission
+#  -o smtpd_tls_security_level=encrypt
+#  -o smtpd_sasl_auth_enable=yes
+#  -o smtpd_reject_unlisted_recipient=no
+#  -o smtpd_client_restrictions=$mua_client_restrictions
+#  -o smtpd_helo_restrictions=$mua_helo_restrictions
+#  -o smtpd_sender_restrictions=$mua_sender_restrictions
+#  -o smtpd_recipient_restrictions=
+#  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
+#  -o milter_macro_daemon_name=ORIGINATING
+smtps     inet  n       -       n       -       2       smtpd
+  -o smtpd_tls_wrappermode=yes
+#  -o syslog_name=postfix/smtps
+#  -o smtpd_sasl_auth_enable=yes
+#  -o smtpd_reject_unlisted_recipient=no
+#  -o smtpd_client_restrictions=$mua_client_restrictions
+#  -o smtpd_helo_restrictions=$mua_helo_restrictions
+#  -o smtpd_sender_restrictions=$mua_sender_restrictions
+#  -o smtpd_recipient_restrictions=
+#  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
+#  -o milter_macro_daemon_name=ORIGINATING
+#628       inet  n       -       y       -       -       qmqpd
+587       inet  n       -       y       -       2       smtpd
+ -o smtpd_tls_security_level=encrypt
+pickup    unix  n       -       y       60      1       pickup
+cleanup   unix  n       -       y       -       0       cleanup
+qmgr      unix  n       -       n       300     1       qmgr
+#qmgr     unix  n       -       n       300     1       oqmgr
+tlsmgr    unix  -       -       y       1000?   1       tlsmgr
+rewrite   unix  -       -       y       -       -       trivial-rewrite
+bounce    unix  -       -       y       -       0       bounce
+defer     unix  -       -       y       -       0       bounce
+trace     unix  -       -       y       -       0       bounce
+verify    unix  -       -       y       -       1       verify
+flush     unix  n       -       y       1000?   0       flush
+proxymap  unix  -       -       n       -       -       proxymap
+#proxywrite unix -       -       n       -       1       proxymap
+smtp      unix  -       -       y       -       -       smtp
+#       -o smtp_helo_timeout=5 -o smtp_connect_timeout=5
+relay     unix  -       -       y       -       5       smtp
+showq     unix  n       -       y       -       -       showq
+error     unix  -       -       y       -       -       error
+retry     unix  -       -       y       -       -       error
+discard   unix  -       -       y       -       -       discard
+#local    unix  -       n       n       -       -       local
+#virtual   unix  -       n       n       -       -       virtual
+lmtp      unix  -       -       y       -       -       lmtp
+anvil     unix  -       -       y       -       1       anvil
+scache    unix  -       -       y       -       1       scache
+#
+# ====================================================================
+# Interfaces to non-Postfix software. Be sure to examine the manual
+# pages of the non-Postfix software to find out what options it wants.
+#
+# Many of the following services use the Postfix pipe(8) delivery
+# agent.  See the pipe(8) man page for information about ${recipient}
+# and other message envelope options.
+# ====================================================================
+#
+# maildrop. See the Postfix MAILDROP_README file for details.
+# Also specify in main.cf: maildrop_destination_recipient_limit=1
+#
+#maildrop  unix  -       n       n       -       -       pipe
+#  flags=DRhu user=vmail argv=/usr/bin/maildrop -d ${recipient}
+#
+# ====================================================================
+#
+# Recent Cyrus versions can use the existing "lmtp" master.cf entry.
+#
+# Specify in cyrus.conf:
+#   lmtp    cmd="lmtpd -a" listen="localhost:lmtp" proto=tcp4
+#
+# Specify in main.cf one or more of the following:
+#  mailbox_transport = lmtp:inet:localhost
+#  virtual_transport = lmtp:inet:localhost
+#
+# ====================================================================
+#
+# Cyrus 2.1.5 (Amos Gouaux)
+# Also specify in main.cf: cyrus_destination_recipient_limit=1
+#
+#cyrus     unix  -       n       n       -       -       pipe
+#  user=cyrus argv=/cyrus/bin/deliver -e -r ${sender} -m ${extension} ${user}
+#
+# ====================================================================
+# Old example of delivery via Cyrus.
+#
+#old-cyrus unix  -       n       n       -       -       pipe
+#  flags=R user=cyrus argv=/cyrus/bin/deliver -e -m ${extension} ${user}
+#
+# ====================================================================
+#
+# See the Postfix UUCP_README file for configuration details.
+#
+#uucp      unix  -       n       n       -       -       pipe
+#  flags=Fqhu user=uucp argv=uux -r -n -z -a$sender - $nexthop!rmail ($recipient)
+#
+# Other external delivery methods.
+#
+#ifmail    unix  -       n       n       -       -       pipe
+#  flags=F user=ftn argv=/usr/lib/ifmail/ifmail -r $nexthop ($recipient)
+#bsmtp     unix  -       n       n       -       -       pipe
+#  flags=Fq. user=bsmtp argv=/usr/lib/bsmtp/bsmtp -t$nexthop -f$sender $recipient
+#scalemail-backend unix	-	n	n	-	2	pipe
+#  flags=R user=scalemail argv=/usr/lib/scalemail/bin/scalemail-store ${nexthop} ${user} ${extension}
+#mailman   unix  -       n       n       -       -       pipe
+#  flags=FR user=list argv=/usr/lib/mailman/bin/postfix-to-mailman.py
+#  ${nexthop} ${user}
+
diff --git a/etc/postfix/sql/mysql_tls_policy_in.cf b/etc/postfix/sql/mysql_tls_policy_in.cf
new file mode 100644
index 0000000..854823c
--- /dev/null
+++ b/etc/postfix/sql/mysql_tls_policy_in.cf
@@ -0,0 +1,5 @@
+user = postfix_readonly
+password = YOUR_PASSWORD
+hosts = localhost
+dbname = postfix
+query = SELECT 'reject_plaintext_session' FROM mailbox WHERE username=CONCAT('%u', '@', COALESCE((SELECT target_domain FROM alias_domain WHERE alias_domain = '%d' AND active='1'), '%d')) AND active = 1 AND enforce_tls_in = 1 UNION SELECT 'reject_plaintext_session' FROM alias WHERE address=CONCAT('%u', '@', COALESCE((SELECT target_domain FROM alias_domain WHERE alias_domain = '%d' AND active='1'), '%d')) AND active = 1 AND enforce_tls_in = 1
diff --git a/etc/postfix/sql/mysql_transport_maps.cf b/etc/postfix/sql/mysql_transport_maps.cf
new file mode 100644
index 0000000..9bd4cdd
--- /dev/null
+++ b/etc/postfix/sql/mysql_transport_maps.cf
@@ -0,0 +1,5 @@
+user = postfix_readonly
+password = YOUR_PASSWORD
+hosts = localhost
+dbname = postfix
+query = SELECT ':' FROM domain WHERE domain='%d' AND active = '1'
diff --git a/etc/postfix/sql/mysql_virtual_alias_maps.cf b/etc/postfix/sql/mysql_virtual_alias_maps.cf
new file mode 100644
index 0000000..8b28884
--- /dev/null
+++ b/etc/postfix/sql/mysql_virtual_alias_maps.cf
@@ -0,0 +1,5 @@
+user = postfix_readonly
+password = YOUR_PASSWORD
+hosts = localhost
+dbname = postfix
+query = SELECT goto FROM alias WHERE address = CONCAT('%u', '@', COALESCE((SELECT target_domain FROM alias_domain WHERE alias_domain = '%d' AND active='1'), '%d')) AND active = 1;
diff --git a/etc/postfix/sql/mysql_virtual_auth_maps.cf b/etc/postfix/sql/mysql_virtual_auth_maps.cf
new file mode 100644
index 0000000..6da648b
--- /dev/null
+++ b/etc/postfix/sql/mysql_virtual_auth_maps.cf
@@ -0,0 +1,5 @@
+user = postfix_readonly
+password = YOUR_PASSWORD
+hosts = localhost
+dbname = postfix
+query = SELECT username FROM mailbox WHERE username=CONCAT('%u', '@', COALESCE((SELECT target_domain FROM alias_domain WHERE alias_domain = '%d' AND active='1'), '%d')) AND active = '1'
diff --git a/etc/postfix/sql/mysql_virtual_domain_unauth_block.cf b/etc/postfix/sql/mysql_virtual_domain_unauth_block.cf
new file mode 100644
index 0000000..a6f6b7c
--- /dev/null
+++ b/etc/postfix/sql/mysql_virtual_domain_unauth_block.cf
@@ -0,0 +1,6 @@
+user = postfix_readonly
+password = YOUR_PASSWORD
+hosts = localhost
+dbname = postfix
+query = SELECT 'REJECT' FROM domain WHERE domain='%d' AND active = '1'
+
diff --git a/etc/postfix/sql/mysql_virtual_domains_maps.cf b/etc/postfix/sql/mysql_virtual_domains_maps.cf
new file mode 100644
index 0000000..1fb5453
--- /dev/null
+++ b/etc/postfix/sql/mysql_virtual_domains_maps.cf
@@ -0,0 +1,5 @@
+user = postfix_readonly
+password = YOUR_PASSWORD
+hosts = localhost
+dbname = postfix
+query = SELECT domain FROM domain WHERE domain='%s' AND active = '1'
diff --git a/etc/postfix/sql/mysql_virtual_mailbox_maps.cf b/etc/postfix/sql/mysql_virtual_mailbox_maps.cf
new file mode 100644
index 0000000..d2c4ca4
--- /dev/null
+++ b/etc/postfix/sql/mysql_virtual_mailbox_maps.cf
@@ -0,0 +1,5 @@
+user = postfix_readonly
+password = YOUR_PASSWORD
+hosts = localhost
+dbname = postfix
+query = SELECT 1 FROM mailbox WHERE username='%s' AND active = '1'
diff --git a/etc/prosody/prosody.cfg.lua b/etc/prosody/prosody.cfg.lua
new file mode 100644
index 0000000..09089e1
--- /dev/null
+++ b/etc/prosody/prosody.cfg.lua
@@ -0,0 +1,326 @@
+-- Prosody Example Configuration File
+--
+-- Information on configuring Prosody can be found on our
+-- website at https://prosody.im/doc/configure
+--
+-- Tip: You can check that the syntax of this file is correct
+-- when you have finished by running this command:
+--     prosodyctl check config
+-- If there are any errors, it will let you know what and where
+-- they are, otherwise it will keep quiet.
+--
+-- The only thing left to do is rename this file to remove the .dist ending, and fill in the
+-- blanks. Good luck, and happy Jabbering!
+
+
+---------- Server-wide settings ----------
+-- Settings in this section apply to the whole server and are the default settings
+-- for any virtual hosts
+
+-- This is a (by default, empty) list of accounts that are admins
+-- for the server. Note that you must create the accounts separately
+-- (see https://prosody.im/doc/creating_accounts for info)
+-- Example: admins = { "user1@example.com", "user2@example.net" }
+admins = { "daniel@danwin1210.de" }
+
+-- Enable use of libevent for better performance under high load
+-- For more information see: https://prosody.im/doc/libevent
+use_libevent = true
+
+-- Prosody will always look in its source directory for modules, but
+-- this option allows you to specify additional locations where Prosody
+-- will look for modules first. For community modules, see https://modules.prosody.im/
+-- For a local administrator it's common to place local modifications
+-- under /usr/local/ hierarchy:
+--plugin_paths = { "/usr/local/lib/prosody/modules" }
+
+-- This is the list of modules Prosody will load on startup.
+-- It looks for mod_modulename.lua in the plugins folder, so make sure that exists too.
+-- Documentation for bundled modules can be found at: https://prosody.im/doc/modules
+modules_enabled = {
+
+	-- Generally required
+		"roster"; -- Allow users to have a roster. Recommended ;)
+		"saslauth"; -- Authentication for clients and servers. Recommended if you want to log in.
+		"tls"; -- Add support for secure TLS on c2s/s2s connections
+		"dialback"; -- s2s dialback support
+		"disco"; -- Service discovery
+
+	-- Not essential, but recommended
+		"carbons"; -- Keep multiple clients in sync
+		"pep"; -- Enables users to publish their avatar, mood, activity, playing music and more
+		"private"; -- Private XML storage (for room bookmarks, etc.)
+		"blocklist"; -- Allow users to block communications with other users
+		"vcard4"; -- User profiles (stored in PEP)
+		"vcard_legacy"; -- Conversion between legacy vCard and PEP Avatar, vcard
+
+	-- Nice to have
+		-- "version"; -- Replies to server version requests
+		-- "uptime"; -- Report how long server has been running
+		"time"; -- Let others know the time here on this server
+		"ping"; -- Replies to XMPP pings with pongs
+		-- "register"; -- Allow users to register on this server using a client and change passwords
+		"mam"; -- Store messages in an archive and allow users to access it
+		"csi_simple"; -- Simple Mobile optimizations
+
+	-- Admin interfaces
+		"admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands
+		--"admin_telnet"; -- Opens telnet console interface on localhost port 5582
+
+	-- HTTP modules
+		"bosh"; -- Enable BOSH clients, aka "Jabber over HTTP"
+		"websocket"; -- XMPP over WebSockets
+		"http_files"; -- Serve static files from a directory over HTTP
+
+	-- Other specific functionality
+		"posix"; -- POSIX functionality, sends server to background, enables syslog, etc.
+		--"limits"; -- Enable bandwidth limiting for XMPP connections
+		"groups"; -- Shared roster support
+		"server_contact_info"; -- Publish contact information for this service
+		--"announce"; -- Send announcement to all online users
+		"s2s_bidi"; -- Bi-directional server-to-server (XEP-0288)
+		--"welcome"; -- Welcome users who register accounts
+		--"watchregistrations"; -- Alert admins of registrations
+		--"motd"; -- Send a message to users when they log in
+		--"legacyauth"; -- Legacy authentication. Only used by some old clients and bots.
+		-- "proxy65"; -- Enables a file transfer proxy service which clients behind NAT can use
+
+	-- Custom added
+		"blocking"; -- Allow blocking users
+		"alias"; -- Alias onion to clearnet
+		"smacks"; -- Stream management
+		-- "csi"; -- Client state indication
+		"filter_chatstates"; -- Hide typing indication on mobile phones
+		"throttle_presence"; -- Update precense on moblile phones less often
+		"cloud_notify"; -- Push notifications for mobile
+		"conversejs";
+		"http_altconnect";
+		"external_services";
+}
+
+-- These modules are auto-loaded, but should you want
+-- to disable them then uncomment them here:
+modules_disabled = {
+	-- "offline"; -- Store offline messages
+	-- "c2s"; -- Handle client connections
+	-- "s2s"; -- Handle server-to-server connections
+}
+
+-- Disable account creation by default, for security
+-- For more information see https://prosody.im/doc/creating_accounts
+allow_registration = false
+
+-- Debian:
+--   Do not send the server to background, either systemd or start-stop-daemon take care of that.
+--
+daemonize = false;
+
+-- Debian:
+--   Please, don't change this option since /run/prosody/
+--   is one of the few directories Prosody is allowed to write to
+--
+pidfile = "/run/prosody/prosody.pid";
+
+-- Force clients to use encrypted connections? This option will
+-- prevent clients from authenticating unless they are using encryption.
+ssl = {
+	key = "/etc/acme.sh/danwin1210.de_ecc/danwin1210.de.key";
+	certificate = "/etc/acme.sh/danwin1210.de_ecc/fullchain.cer";
+	dhparam = "/etc/prosody/dh4096.pem";
+	curve = "X448:X25519:secp521r1:secp384r1:secp256k1";
+	ciphers = "HIGH+kEDH:HIGH+kEECDH:HIGH:!RSA:!PSK:!SRP:!3DES:!aNULL:!SHA:!MD5:!CAMELLIA:!ECDHE-RSA-AES256-SHA384:!ECDHE-RSA-AES128-SHA256:!DHE-RSA-AES128-SHA256:!DHE-RSA-AES256-SHA256";
+}
+
+c2s_require_encryption = true
+
+-- Force servers to use encrypted connections? This option will
+-- prevent servers from authenticating unless they are using encryption.
+
+s2s_require_encryption = true
+
+-- Force certificate authentication for server-to-server connections?
+
+s2s_secure_auth = false
+
+-- Some servers have invalid or self-signed certificates. You can list
+-- remote domains here that will not be required to authenticate using
+-- certificates. They will be authenticated using DNS instead, even
+-- when s2s_secure_auth is enabled.
+
+--s2s_insecure_domains = { "insecure.example" }
+
+-- Even if you disable s2s_secure_auth, you can still require valid
+-- certificates for some domains by specifying a list here.
+
+--s2s_secure_domains = { "jabber.org" }
+
+-- Select the authentication backend to use. The 'internal' providers
+-- use Prosody's configured data storage to store the authentication data.
+
+authentication = "imap"
+
+-- Select the storage backend to use. By default Prosody uses flat files
+-- in its configured data directory, but it also supports more backends
+-- through modules. An "sql" backend is included by default, but requires
+-- additional dependencies. See https://prosody.im/doc/storage for more info.
+
+storage = "sql" -- Default is "internal" (Debian: "sql" requires one of the
+-- lua-dbi-sqlite3, lua-dbi-mysql or lua-dbi-postgresql packages to work)
+
+-- For the "sql" backend, you can uncomment *one* of the below to configure:
+--sql = { driver = "SQLite3", database = "prosody.sqlite" } -- Default. 'database' is the filename.
+--sql = { driver = "MySQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" }
+--sql = { driver = "PostgreSQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" }
+sql = { driver = "MySQL", database = "prosody", username = "prosody", password = "YOUR_PASSWORD", host = "localhost" }
+
+
+-- Archiving configuration
+-- If mod_mam is enabled, Prosody will store a copy of every message. This
+-- is used to synchronize conversations between multiple clients, even if
+-- they are offline. This setting controls how long Prosody will keep
+-- messages in the archive before removing them.
+
+archive_expires_after = "1w" -- Remove archived messages after 1 week
+
+-- You can also configure messages to be stored in-memory only. For more
+-- archiving options, see https://prosody.im/doc/modules/mod_mam
+
+-- Logging configuration
+-- For advanced logging see https://prosody.im/doc/logging
+--
+-- Debian:
+--  Logs info and higher to /var/log
+--  Logs errors to syslog also
+log = {
+	-- Log files (change 'info' to 'debug' for debug logs):
+	info = "/var/log/prosody/prosody.log";
+	error = "/var/log/prosody/prosody.err";
+	-- Syslog:
+	{ levels = { "error" }; to = "syslog";  };
+}
+
+-- Uncomment to enable statistics
+-- For more info see https://prosody.im/doc/statistics
+-- statistics = "internal"
+
+-- Certificates
+-- Every virtual host and component needs a certificate so that clients and
+-- servers can securely verify its identity. Prosody will automatically load
+-- certificates/keys from the directory specified here.
+-- For more information, including how to use 'prosodyctl' to auto-import certificates
+-- (from e.g. Let's Encrypt) see https://prosody.im/doc/certificates
+
+-- Location of directory to find certificates in (relative to main config file):
+certificates = "certs"
+
+-- HTTPS currently only supports a single certificate, specify it here:
+--https_certificate = "/etc/prosody/certs/localhost.crt"
+
+----------- Virtual hosts -----------
+-- You need to add a VirtualHost entry for each domain you wish Prosody to serve.
+-- Settings under each VirtualHost entry apply *only* to that host.
+-- It's customary to maintain VirtualHost entries in separate config files
+-- under /etc/prosody/conf.d/ directory. Examples of such config files can
+-- be found in /etc/prosody/conf.avail/ directory.
+
+------ Additional config files ------
+-- For organizational purposes you may prefer to add VirtualHost and
+-- Component definitions in their own config files. This line includes
+-- all config files in /etc/prosody/conf.d/
+
+-- custom
+plugin_paths = {"/srv/prosody-modules/"}
+compression_level = 9
+auth_imap_verify_certificate = false
+imap_auth_host = "127.0.0.1"
+aliases = {
+	["danwin1210.me"] = "danwin1210.de";
+	["danielas3rtn54uwmofdo3x2bsdifr47huasnmbgqzfrec5ubupvtpid.onion"] = "danwin1210.de";
+}
+alias_response = "User $alias can be contacted at $target";
+defautl_storage = "sql"
+interfaces = { "0.0.0.0", "::" } -- Listen address
+contact_info = {
+  abuse = { "https://danwin1210.de/contact.php", "mailto:daniel@danwin1210.de" };
+  admin = { "https://danwin1210.de/contact.php", "mailto:daniel@danwin1210.de" };
+  feedback = { "https://danwin1210.de/contact.php", "mailto:daniel@danwin1210.de" };
+  security = { "https://danwin1210.de/contact.php", "mailto:daniel@danwin1210.de" };
+  support = { "https://danwin1210.de/contact.php", "mailto:daniel@danwin1210.de" };
+}
+data_path = "/srv/var/lib/prosody"
+legacy_ssl_ports = {5223}
+external_services = {
+    {
+        type = "stun",
+        transport = "udp",
+        host = "danwin1210.de",
+        port = 3478
+    }, {
+        type = "turn",
+        transport = "udp",
+        host = "danwin1210.de",
+        port = 3478,
+        secret = "YOUR_SECRET"
+    },
+    {
+        type = "stun",
+        transport = "tcp",
+        host = "danwin1210.de",
+        port = 3478
+    }, {
+        type = "turn",
+        transport = "tcp",
+        host = "danwin1210.de",
+        port = 3478,
+        secret = "YOUR_SECRET"
+    },
+    {
+        type = "stuns",
+        transport = "tcp",
+        host = "danwin1210.de",
+        port = 5349
+    }, {
+        type = "turns",
+        transport = "tcp",
+        host = "danwin1210.de",
+        port = 5349,
+        secret = "YOUR_SECRET"
+    }
+}
+cross_domain_bosh = true
+cross_domain_websocket = true
+
+VirtualHost "danwin1210.de"
+
+--VirtualHost "example.com"
+--	certificate = "/path/to/example.crt"
+
+------ Components ------
+-- You can specify components to add hosts that provide special services,
+-- like multi-user conferences, and transports.
+-- For more information on components, see https://prosody.im/doc/components
+
+---Set up a MUC (multi-user chat) room server on conference.example.com:
+Component "conference.danwin1210.de" "muc"
+modules_enabled = {
+  "vcard_muc",
+  "muc_mam" ,
+}
+
+-- Set up a SOCKS5 bytestream proxy for server-proxied file transfers:
+Component "proxy.danwin1210.de" "proxy65"
+
+Component "upload.danwin1210.de" "http_file_share"
+http_file_share_size_limit = 100*1024*1024 -- 100 MiB
+http_file_share_daily_quota = 100*1024*1024 -- 100 MiB per day per user
+http_file_share_global_quota = 10*1024*1024*1024 -- 10 GiB total
+
+---Set up an external component (default component port is 5347)
+--
+-- External components allow adding various services, such as gateways/
+-- transports to other networks like ICQ, MSN and Yahoo. For more info
+-- see: https://prosody.im/doc/components#adding_an_external_component
+--
+--Component "gateway.example.com"
+--	component_secret = "password"
+Include "conf.d/*.cfg.lua"
diff --git a/tools/crypt_maildir.sh b/tools/crypt_maildir.sh
new file mode 100755
index 0000000..edb4e3c
--- /dev/null
+++ b/tools/crypt_maildir.sh
@@ -0,0 +1,114 @@
+#!/bin/bash
+#
+# Encrypt/Decrypt/Check emails with Dovecot's mail-crpyt-plugin
+# This script will encrypt/decrypt emails in-place
+# Please read: https://wiki.dovecot.org/Design/Dcrypt and https://wiki2.dovecot.org/Plugins/MailCrypt
+#
+# Update variables with your keys and patch otherwise you will loose data!
+#
+# I take no responsibility for data loos this script may cause
+#
+# IMPORTANT:
+# BEFORE USE ADD THIS MAGIC(5) TO YOUR LOCAL MAGIC DATABASE:
+#/etc/magic and /etc/magic.mime:
+#0	string	CRYPTED	MailCrypt
+#!:mime application/mail-crypt
+
+count=0
+processed=0
+tempfile=$(mktemp)
+
+uid=5000
+gid=5000
+maildir_path=$(pwd)
+private_key_path=/etc/dovecot/ecprivkey.pem
+public_key_path=/etc/dovecot/ecpubkey.pem
+
+if [ "$1" == "" ]; then
+    echo "Missing user folder"
+    exit 1
+fi
+
+case $2 in
+  encrypt) mode=encrypt; text_d="Encrypting"
+  ;;
+  decrypt) mode=decrypt; text_d="Decrypting"
+  ;;
+  check) mode=check; text_d="Checking"
+  ;;
+  *)  echo "Unknown mode. Modes: [encrypt|decrypt|check]"; exit 1
+esac
+
+_encrypt(){
+  touch -r "$mailmessage" $tempfile
+  doveadm fs put compress gz:9:crypt:private_key_path=$private_key_path:public_key_path=$public_key_path:posix:prefix=$maildir_path/$userdir/ "$mailmessage" "$mailmessage"
+  touch -r $tempfile "$mailmessage"
+  chown $uid:$gid "$mailmessage"
+}
+
+_decrypt(){
+  touch -r "$mailmessage" $tempfile
+  doveadm fs get compress maybe-gz:9:crypt:private_key_path=$private_key_path:public_key_path=$public_key_path:posix:prefix=$maildir_path/$userdir/ "$mailmessage" > .tempdecrypted
+  mv .tempdecrypted "$mailmessage"
+  touch -r $tempfile "$mailmessage"
+  chmod 0600 "$mailmessage"
+  chown $uid:$gid "$mailmessage"
+}
+
+userdir="$1"
+
+if [ ! -d $maildir_path/$userdir/ ];then
+  echo "Folder do not exist: $maildir_path/$userdir/"
+  exit 1
+fi
+
+totalfiles=$(find $maildir_path/$userdir/ -type f  ! -iname 'dovecot*' ! -iname 'maildirfolder' ! -iname 'subscriptions' | wc -l | xargs)
+echo
+echo "$text_d mails in $maildir_path/$userdir/"
+echo "Found $totalfiles, processing..."
+echo ". plain text" 
+echo "+ gzipped "
+echo "* encrypted "
+echo "< encrypting"
+echo "> decrypting"
+echo
+
+# operate in context
+cd $maildir_path/$userdir/
+for mailmessage in `find . -type f  ! -iname 'dovecot*' ! -iname 'maildirfolder' ! -iname 'subscriptions'`; do
+  message=$(basename "$mailmessage")
+  if [ ! -f "$mailmessage" ];then
+    continue;
+  fi;
+  testfiletype=$(file -b --mime-type "$mailmessage")
+  if [ "$testfiletype" != "application/mail-crypt" ]  ;then
+      if [ "$testfiletype" != "application/gzip" ]  ;then
+          echo -n "."
+      else
+          echo -n "+"
+      fi
+      if [ "$mode" == "encrypt" ];then
+        _encrypt
+        echo -n "<"
+      fi
+    else
+      echo -n "*"
+      if [ "$mode" == "decrypt" ];then
+        _decrypt
+        echo -n ">"
+      fi
+  fi
+  count=$(($count + 1))
+  processed=$(($processed + 1))
+  if [ $count == 10 ];then
+    echo -n "$processed/$totalfiles"
+    echo -e
+    count=0
+  fi
+
+
+done
+
+rm -f $tempfile
+
+echo -e "\n\nDone"
diff --git a/tools/mass_mail/composer.json b/tools/mass_mail/composer.json
new file mode 100644
index 0000000..1501444
--- /dev/null
+++ b/tools/mass_mail/composer.json
@@ -0,0 +1,5 @@
+{
+    "require": {
+        "phpmailer/phpmailer": "^6.6"
+    }
+}
diff --git a/tools/mass_mail/index.php b/tools/mass_mail/index.php
new file mode 100644
index 0000000..4f47275
--- /dev/null
+++ b/tools/mass_mail/index.php
@@ -0,0 +1,51 @@
+<?php
+require_once 'vendor/autoload.php';
+use PHPMailer\PHPMailer\PHPMailer;
+use PHPMailer\PHPMailer\SMTP;
+use PHPMailer\PHPMailer\Exception;
+const DBHOST = 'localhost'; // Database host
+const DBUSER = 'postfix_readonly'; // Database user
+const DBPASS = 'YOUR_PASSWORD'; // Database password
+const DBNAME = 'postfix'; // Database
+
+try{
+	$db=new PDO('mysql:host=' . DBHOST . ';dbname=' . DBNAME, DBUSER, DBPASS, [PDO::ATTR_ERRMODE=>PDO::ERRMODE_WARNING, PDO::ATTR_PERSISTENT=>false]);
+}catch(PDOException $e){
+	die('No Connection to MySQL database!');
+}
+$stmt = $db->query('SELECT username FROM mailbox WHERE active = 1;');
+$all_accounts = $stmt->fetchAll(PDO::FETCH_ASSOC);
+$count = count($all_accounts);
+$i = 0;
+foreach($all_accounts as $account){
+	// skip to account x if script was aborted
+	if(++$i < 1){
+		continue;
+	}
+	echo "Sending mail to $account[username] ($i of $count)...\n";
+	$mail = new PHPMailer(true);
+	$mail->isSMTP();
+	$mail->Host = '127.0.0.1';
+	$mail->SMTPAuth = true;
+	$mail->Username = 'YOUR_SMTP_USER';
+	$mail->Password = 'YOUR_SMTP_PASSWORD';
+	$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
+	$mail->Port = 465;
+	$mail->SMTPOptions = [
+		'ssl' => [
+			'verify_peer' => false,
+			'verify_peer_name' => false,
+			'allow_self_signed' => true,
+		]
+	];
+	$mail->setFrom('YOUR_SMTP_USER', 'YOUR_NAME');
+	$mail->Subject = 'YOUR_SUBJECT';
+	$mail->Body    = 'YOUR_MESSAGE';
+	try {
+		$mail->addAddress($account['username']);
+		$mail->send();
+		$mail->clearAddresses();
+	} catch (Exception $e) {
+		file_put_contents(__DIR__.'/failed.txt', "Sending mail to $account[username] ($i of $count)...\nMessage could not be sent. Mailer Error: {$mail->ErrorInfo}", FILE_APPEND);
+	}
+}
diff --git a/usr/local/etc/rspamd/local.d/actions.conf b/usr/local/etc/rspamd/local.d/actions.conf
new file mode 100644
index 0000000..3de63a5
--- /dev/null
+++ b/usr/local/etc/rspamd/local.d/actions.conf
@@ -0,0 +1,3 @@
+reject = 15;
+add_header = 8;
+greylist = 7;
diff --git a/usr/local/etc/rspamd/local.d/antivirus.conf b/usr/local/etc/rspamd/local.d/antivirus.conf
new file mode 100644
index 0000000..4c57f1a
--- /dev/null
+++ b/usr/local/etc/rspamd/local.d/antivirus.conf
@@ -0,0 +1,11 @@
+clamav {
+  # Scan whole message
+  scan_mime_parts = false;
+  #scan_text_mime = true;
+  #scan_image_mime = true;
+  symbol = "CLAM_VIRUS";
+  type = "clamav";
+  log_clean = true;
+  servers = "/var/run/clamav/clamd.ctl";
+  max_size = 20971520;
+}
diff --git a/usr/local/etc/rspamd/local.d/arc.conf b/usr/local/etc/rspamd/local.d/arc.conf
new file mode 100644
index 0000000..0887ee7
--- /dev/null
+++ b/usr/local/etc/rspamd/local.d/arc.conf
@@ -0,0 +1,16 @@
+sign_authenticated = true;
+sign_local = true;
+domain {
+  danwin1210.de {
+    selectors [
+     {
+       path: "/usr/local/etc/rspamd/dkim_keys/danwin1210.de-rsa";
+       selector: "20211204-rsa";
+     },
+     {
+       path: "/usr/local/etc/rspamd/dkim_keys/danwin1210.de-ed25519";
+       selector: "20211204-ed25519";
+     }
+   ]
+ }
+}
diff --git a/usr/local/etc/rspamd/local.d/classifier-bayes.conf b/usr/local/etc/rspamd/local.d/classifier-bayes.conf
new file mode 100644
index 0000000..d83f163
--- /dev/null
+++ b/usr/local/etc/rspamd/local.d/classifier-bayes.conf
@@ -0,0 +1 @@
+autolearn = true;
diff --git a/usr/local/etc/rspamd/local.d/dkim_signing.conf b/usr/local/etc/rspamd/local.d/dkim_signing.conf
new file mode 100644
index 0000000..ce4020b
--- /dev/null
+++ b/usr/local/etc/rspamd/local.d/dkim_signing.conf
@@ -0,0 +1,20 @@
+use_domain = "header";
+use_domain_sign_networks = "header";
+use_domain_sign_local = "header";
+allow_username_mismatch = true;
+allow_hdrfrom_mismatch = true;
+try_fallback = false;
+domain {
+  danwin1210.de {
+    selectors [
+     {
+       path: "/usr/local/etc/rspamd/dkim_keys/danwin1210.de-rsa";
+       selector: "20211204-rsa";
+     },
+     {
+       path: "/usr/local/etc/rspamd/dkim_keys/danwin1210.de-ed25519";
+       selector: "20211204-ed25519";
+     }
+   ]
+ }
+}
diff --git a/usr/local/etc/rspamd/local.d/greylist.conf b/usr/local/etc/rspamd/local.d/greylist.conf
new file mode 100644
index 0000000..a6ee831
--- /dev/null
+++ b/usr/local/etc/rspamd/local.d/greylist.conf
@@ -0,0 +1 @@
+enabled = false;
diff --git a/usr/local/etc/rspamd/local.d/groups.conf b/usr/local/etc/rspamd/local.d/groups.conf
new file mode 100644
index 0000000..974a53e
--- /dev/null
+++ b/usr/local/etc/rspamd/local.d/groups.conf
@@ -0,0 +1,15 @@
+symbols {
+  "CLAM_VIRUS" {
+    "weight": 10.0
+  }
+  "CLAM_VIRUS_ENCRYPTED" {
+    "weight": 1.0
+  }
+  "CLAM_VIRUS_MACRO" {
+    "weight": 1.0
+  }
+  "AUTHENTICATED_USER" {
+    description = "authenticated user should receive higher scoring to prevent outgoing spam";
+    score = 5.0;
+  }
+}
diff --git a/usr/local/etc/rspamd/local.d/logging.inc b/usr/local/etc/rspamd/local.d/logging.inc
new file mode 100644
index 0000000..89a7c9f
--- /dev/null
+++ b/usr/local/etc/rspamd/local.d/logging.inc
@@ -0,0 +1,4 @@
+type = console
+systemd = true
+color = true
+level = notice
diff --git a/usr/local/etc/rspamd/local.d/neural.conf b/usr/local/etc/rspamd/local.d/neural.conf
new file mode 100644
index 0000000..f4658db
--- /dev/null
+++ b/usr/local/etc/rspamd/local.d/neural.conf
@@ -0,0 +1,24 @@
+rules {
+  "LONG" {
+    train {
+      max_trains = 200;
+      max_usages = 20;
+      max_iterations = 25;
+      learning_rate = 0.01,
+    }
+    symbol_spam = "NEURAL_SPAM_LONG";
+    symbol_ham = "NEURAL_HAM_LONG";
+    ann_expire = 45d;
+  }
+  "SHORT" {
+    train {
+      max_trains = 100;
+      max_usages = 10;
+      max_iterations = 15;
+      learning_rate = 0.01,
+    }
+    symbol_spam = "NEURAL_SPAM_SHORT";
+    symbol_ham = "NEURAL_HAM_SHORT";
+    ann_expire = 7d;
+  }
+}
diff --git a/usr/local/etc/rspamd/local.d/neural_group.conf b/usr/local/etc/rspamd/local.d/neural_group.conf
new file mode 100644
index 0000000..fca5cec
--- /dev/null
+++ b/usr/local/etc/rspamd/local.d/neural_group.conf
@@ -0,0 +1,18 @@
+symbols = {
+  "NEURAL_SPAM_LONG" {
+    weight = 3.7; # sample weight
+    description = "Neural network spam (long)";
+  }
+  "NEURAL_HAM_LONG" {
+    weight = -4.0; # sample weight
+    description = "Neural network ham (long)";
+  }
+  "NEURAL_SPAM_SHORT" {
+    weight = 2.5; # sample weight
+    description = "Neural network spam (short)";
+  }
+  "NEURAL_HAM_SHORT" {
+    weight = -2.0; # sample weight
+    description = "Neural network ham (short)";
+  }
+}
diff --git a/usr/local/etc/rspamd/local.d/options.inc b/usr/local/etc/rspamd/local.d/options.inc
new file mode 100644
index 0000000..9de8ee9
--- /dev/null
+++ b/usr/local/etc/rspamd/local.d/options.inc
@@ -0,0 +1,3 @@
+dns {
+  enable_dnssec = true;
+}
diff --git a/usr/local/etc/rspamd/local.d/phishing.conf b/usr/local/etc/rspamd/local.d/phishing.conf
new file mode 100644
index 0000000..69be164
--- /dev/null
+++ b/usr/local/etc/rspamd/local.d/phishing.conf
@@ -0,0 +1 @@
+phishtank_enabled = false;
diff --git a/usr/local/etc/rspamd/local.d/ratelimit.conf b/usr/local/etc/rspamd/local.d/ratelimit.conf
new file mode 100644
index 0000000..6fd7556
--- /dev/null
+++ b/usr/local/etc/rspamd/local.d/ratelimit.conf
@@ -0,0 +1,24 @@
+whitelisted_rcpts = ["postmaster", "mailer-daemon", "daniel@danwin1210.de"]
+whitelisted_user = ["daniel@danwin1210.de"]
+rates {
+  to = {
+    bucket = {
+      burst = 20;
+      rate =  1 / 1m;
+    }
+  }
+  sending_limit_2_per_min {
+    selector = 'user.lower.append("sending_limit_2_per_min")';
+    bucket = {
+      burst = 20;
+      rate = 2 / 1m;
+    }
+  }
+  sending_limit_500_per_day {
+    selector = 'user.lower.append("sending_limit_500_per_day")';
+    bucket = {
+      burst = 400;
+      rate = 50 / 3h;
+    }
+  }
+}
diff --git a/usr/local/etc/rspamd/local.d/redis.conf b/usr/local/etc/rspamd/local.d/redis.conf
new file mode 100644
index 0000000..5a9c582
--- /dev/null
+++ b/usr/local/etc/rspamd/local.d/redis.conf
@@ -0,0 +1 @@
+servers = "127.0.0.1";
diff --git a/usr/local/etc/rspamd/local.d/statistics_group.conf b/usr/local/etc/rspamd/local.d/statistics_group.conf
new file mode 100644
index 0000000..c3c9fd5
--- /dev/null
+++ b/usr/local/etc/rspamd/local.d/statistics_group.conf
@@ -0,0 +1,5 @@
+symbols {
+  "BAYES_SPAM" {
+    "score": 7.0
+  }
+}
diff --git a/usr/local/etc/rspamd/local.d/worker-fuzzy.inc b/usr/local/etc/rspamd/local.d/worker-fuzzy.inc
new file mode 100644
index 0000000..2275744
--- /dev/null
+++ b/usr/local/etc/rspamd/local.d/worker-fuzzy.inc
@@ -0,0 +1,10 @@
+count = 1;
+keypair {
+    privkey = "YOUR_PRIVATE_KEY";
+    type = "kex";
+    algorithm = "curve25519";
+    id = "YOUR_ID";
+    pubkey = "YPUR_PUBLIC_KEY";
+    encoding = "base32";
+}
+encrypted_only = true;
diff --git a/usr/local/etc/rspamd/local.d/worker-proxy.inc b/usr/local/etc/rspamd/local.d/worker-proxy.inc
new file mode 100644
index 0000000..86b2c4e
--- /dev/null
+++ b/usr/local/etc/rspamd/local.d/worker-proxy.inc
@@ -0,0 +1 @@
+bind_socket = "*:11332";
diff --git a/usr/local/etc/rspamd/lua/rspamd.local.lua b/usr/local/etc/rspamd/lua/rspamd.local.lua
new file mode 100644
index 0000000..5448355
--- /dev/null
+++ b/usr/local/etc/rspamd/lua/rspamd.local.lua
@@ -0,0 +1,8 @@
+rspamd_config.AUTHENTICATED_USER = {
+	callback = function(task)
+		local uname = task:get_user()
+		if uname then
+			return 1
+		end
+	end
+}
diff --git a/usr/local/etc/rspamd/override.d/fuzzy_check.conf b/usr/local/etc/rspamd/override.d/fuzzy_check.conf
new file mode 100644
index 0000000..1b979f5
--- /dev/null
+++ b/usr/local/etc/rspamd/override.d/fuzzy_check.conf
@@ -0,0 +1,26 @@
+rule "localhost" {
+  algorithm = "mumhash";
+  servers = "localhost:11335";
+  encryption_key = "YOUR_ENCRYPTION_KEY";
+  symbol = "FUZZY_UNKNOWN";
+  mime_types = ["*"];
+  max_score = 20.0;
+  read_only = no;
+  skip_unknown = yes;
+  short_text_direct_hash = true; # If less than min_length then use direct hash
+  min_length = 64; # Minimum words count to consider shingles
+  fuzzy_map = {
+    FUZZY_DENIED {
+      max_score = 20.0;
+      flag = 1;
+    }
+    FUZZY_PROB {
+      max_score = 10.0;
+      flag = 2;
+    }
+    FUZZY_WHITE {
+      max_score = 2.0;
+      flag = 3;
+    }
+  }
+}
diff --git a/usr/local/etc/rspamd/override.d/worker-controller.inc b/usr/local/etc/rspamd/override.d/worker-controller.inc
new file mode 100644
index 0000000..7e30bc3
--- /dev/null
+++ b/usr/local/etc/rspamd/override.d/worker-controller.inc
@@ -0,0 +1,4 @@
+bind_socket = "*:11334";
+password = "YOUR_PASSWORD_HASH";
+enable_password = "YOUR_PASSWORD_HASH";
+secure_ip = "";
diff --git a/www/admin.php b/www/admin.php
new file mode 100644
index 0000000..577d457
--- /dev/null
+++ b/www/admin.php
@@ -0,0 +1,1052 @@
+<?php
+
+use Egulias\EmailValidator\EmailLexer;
+use Egulias\EmailValidator\EmailParser;
+
+require_once( '../common_config.php' );
+
+session_start();
+if ( empty( $_SESSION[ 'csrf_token' ] ) ) {
+	$_SESSION[ 'csrf_token' ] = sha1( uniqid() );
+}
+$msg = '';
+$db = get_db_instance();
+if ( ! empty( $_SESSION[ 'email_admin_user' ] ) ) {
+	$stmt = $db->prepare( 'SELECT null FROM admin WHERE username=? AND active = 1;' );
+	$stmt->execute( [ $_SESSION[ 'email_admin_user' ] ] );
+	if ( ! $stmt->fetch( PDO::FETCH_ASSOC ) ) {
+		$_SESSION = [];
+		session_regenerate_id( true );
+		$_SESSION[ 'csrf_token' ] = sha1( uniqid() );
+		$msg .= '<div class="red" role="alert">It looks like your user no longer exists!</div>';
+	}
+}
+if ( $_SERVER[ 'REQUEST_METHOD' ] === 'POST' ) {
+	if ( isset( $_POST[ 'action' ] ) ) {
+		if ( $_SESSION[ 'csrf_token' ] !== $_POST[ 'csrf_token' ] ?? '' ) {
+			die( 'Invalid csfr token' );
+		}
+		if ( $_POST[ 'action' ] === 'logout' ) {
+			$_SESSION = [];
+			session_regenerate_id( true );
+			$_SESSION[ 'csrf_token' ] = sha1( uniqid() );
+			$msg .= '<div class="green" role="alert">Successfully logged out</div>';
+		} elseif ( $_POST[ 'action' ] === 'login' ) {
+			if ( empty( $_POST[ 'user' ] ) ) {
+				$ok = false;
+				$msg .= '<div class="red" role="alert">Invalid username.</div>';
+			}
+			$stmt = $db->prepare( 'SELECT username, password, password_hash_type, superadmin FROM admin WHERE username = ? AND active = 1;' );
+			$stmt->execute( [ $_POST[ 'user' ] ] );
+			if ( $tmp = $stmt->fetch( PDO::FETCH_ASSOC ) ) {
+				if ( empty( $_POST[ 'pwd' ] ) || ! password_verify( $_POST[ 'pwd' ], $tmp[ 'password' ] ) ) {
+					$msg .= '<div class="red" role="alert">Incorrect username or password</div>';
+				} else {
+					$_SESSION[ 'email_admin_user' ] = $tmp[ 'username' ];
+					$_SESSION[ 'email_admin_superadmin' ] = (bool) $tmp[ 'superadmin' ];
+					// update password hash if it's using an old hashing algorithm
+					if ( $tmp[ 'password_hash_type' ] !== '{ARGON2ID}' ) {
+						$hash = password_hash( $_POST[ 'pwd' ], PASSWORD_ARGON2ID );
+						$stmt = $db->prepare( 'UPDATE admin SET password_hash_type = "{ARGON2ID}", password = ? WHERE username = ? AND active = 1;' );
+						$stmt->execute( [ $hash, $_SESSION[ 'email_admin_user' ] ] );
+					}
+				}
+			} else {
+				$msg .= '<div class="red" role="alert">Incorrect username or password.</div>';
+			}
+		} elseif ( ! empty( $_SESSION[ 'email_admin_user' ] ) ) {
+			if ( $_POST[ 'action' ] === 'update_alias' ) {
+				$alias_goto = '';
+				if ( isset( $_POST[ 'alias_keep_copy' ] ) ) {
+					$alias_goto .= $_SESSION[ 'email_admin_user' ] . ',';
+				}
+				if ( ! empty( $_POST[ 'alias_to' ] ) ) {
+					$additional = preg_split( "/[\s,]+/", $_POST[ 'alias_to' ] );
+					$alias_goto .= validate_email_list( $additional, $msg );
+				}
+				$alias_goto = rtrim( $alias_goto, ',' );
+				$stmt = $db->prepare( 'UPDATE alias SET goto = ? WHERE address = ? AND active = 1;' );
+				$stmt->execute( [ $alias_goto, $_SESSION[ 'email_admin_user' ] ] );
+
+			} elseif ( $_POST[ 'action' ] === 'delete_admin' && ! empty( $_POST[ 'admin' ] ) && $_SESSION[ 'email_admin_superadmin' ] ) {
+				$msg .= '<div class="red" role="alert">Warning: This will permanently delete the admin account "' . htmlspecialchars( $_POST[ 'admin' ] ) . '". It cannot be reversed. Are you absolutely sure?</div>';
+				$msg .= '<form method="post"><input type="hidden" name="csrf_token" value="' . $_SESSION[ 'csrf_token' ] . '">';
+				$msg .= '<input type="hidden" name="admin" value="' . htmlspecialchars( $_POST[ 'admin' ] ) . '">';
+				$msg .= '<button type="submit" name="action" value="delete_admin2">Yes, I want to permanently delete this admin account</button></form>';
+			} elseif ( $_POST[ 'action' ] === 'delete_domain' && ! empty( $_POST[ 'domain' ] ) && $_SESSION[ 'email_admin_superadmin' ] ) {
+				$msg .= '<div class="red" role="alert">Warning: This will permanently delete the domain "' . htmlspecialchars( $_POST[ 'domain' ] ) . '". It cannot be reversed. Are you absolutely sure?</div>';
+				$msg .= '<form method="post"><input type="hidden" name="csrf_token" value="' . $_SESSION[ 'csrf_token' ] . '">';
+				$msg .= '<input type="hidden" name="domain" value="' . htmlspecialchars( $_POST[ 'domain' ] ) . '">';
+				$msg .= '<button type="submit" name="action" value="delete_domain2">Yes, I want to permanently delete this domain</button></form>';
+			} elseif ( $_POST[ 'action' ] === 'delete_alias_domain' && ! empty( $_POST[ 'alias_domain' ] ) && $_SESSION[ 'email_admin_superadmin' ] ) {
+				$msg .= '<div class="red" role="alert">Warning: This will permanently delete the alias domain "' . htmlspecialchars( $_POST[ 'alias_domain' ] ) . '". It cannot be reversed. Are you absolutely sure?</div>';
+				$msg .= '<form method="post"><input type="hidden" name="csrf_token" value="' . $_SESSION[ 'csrf_token' ] . '">';
+				$msg .= '<input type="hidden" name="alias_domain" value="' . htmlspecialchars( $_POST[ 'alias_domain' ] ) . '">';
+				$msg .= '<button type="submit" name="action" value="delete_alias_domain2">Yes, I want to permanently delete this alias domain</button></form>';
+			} elseif ( $_POST[ 'action' ] === 'delete_alias' && ! empty( $_POST[ 'alias' ] ) ) {
+				$msg .= '<div class="red" role="alert">Warning: This will permanently delete the alias "' . htmlspecialchars( $_POST[ 'alias' ] ) . '". It cannot be reversed. Are you absolutely sure?</div>';
+				$msg .= '<form method="post"><input type="hidden" name="csrf_token" value="' . $_SESSION[ 'csrf_token' ] . '">';
+				$msg .= '<input type="hidden" name="alias" value="' . htmlspecialchars( $_POST[ 'alias' ] ) . '">';
+				$msg .= '<button type="submit" name="action" value="delete_alias2">Yes, I want to permanently delete this alias</button></form>';
+			} elseif ( $_POST[ 'action' ] === 'delete_mailbox' && ! empty( $_POST[ 'user' ] ) ) {
+				$msg .= '<div class="red" role="alert">Warning: This will permanently delete the alias "' . htmlspecialchars( $_POST[ 'user' ] ) . '". It cannot be reversed. Are you absolutely sure?</div>';
+				$msg .= '<form method="post"><input type="hidden" name="csrf_token" value="' . $_SESSION[ 'csrf_token' ] . '">';
+				$msg .= '<input type="hidden" name="user" value="' . htmlspecialchars( $_POST[ 'user' ] ) . '">';
+				$msg .= '<button type="submit" name="action" value="delete_mailbox2">Yes, I want to permanently delete this mailbox</button></form>';
+			} elseif ( $_POST[ 'action' ] === 'delete_admin2' && ! empty( $_POST[ 'admin' ] ) && $_SESSION[ 'email_admin_superadmin' ] ) {
+				if ( $_SESSION[ 'email_admin_user' ] === $_POST[ 'admin' ] ) {
+					$msg .= '<div class="red" role="alert">You can\'t delete your own admin account!</div>';
+				} else {
+					$stmt = $db->prepare( 'DELETE FROM admin WHERE username = ?;' );
+					$stmt->execute( [ $_POST[ 'admin' ] ] );
+					$msg .= '<div class="green" role="alert">Successfully deleted admin account.</div>';
+				}
+			} elseif ( $_POST[ 'action' ] === 'delete_domain2' && ! empty( $_POST[ 'domain' ] ) && $_SESSION[ 'email_admin_superadmin' ] ) {
+				$stmt = $db->prepare( 'UPDATE domain SET active = -1 WHERE domain = ?;' );
+				$stmt->execute( [ $_POST[ 'domain' ] ] );
+				$msg .= '<div class="green" role="alert">Successfully deleted domain.</div>';
+			} elseif ( $_POST[ 'action' ] === 'delete_alias_domain2' && ! empty( $_POST[ 'alias_domain' ] ) && $_SESSION[ 'email_admin_superadmin' ] ) {
+				$stmt = $db->prepare( 'DELETE FROM alias_domain WHERE alias_domain = ?;' );
+				$stmt->execute( [ $_POST[ 'alias_domain' ] ] );
+				$msg .= '<div class="green" role="alert">Successfully deleted alias domain.</div>';
+			} elseif ( $_POST[ 'action' ] === 'delete_alias2' && ! empty( $_POST[ 'alias' ] ) ) {
+				if ( check_domain_access( $_POST[ 'alias' ], $msg ) ) {
+					$stmt = $db->prepare( 'DELETE FROM alias WHERE address = ?;' );
+					$stmt->execute( [ $_POST[ 'alias' ] ] );
+					$msg .= '<div class="green" role="alert">Successfully deleted alias.</div>';
+				}
+			} elseif ( $_POST[ 'action' ] === 'delete_mailbox2' && ! empty( $_POST[ 'user' ] ) ) {
+				if ( check_domain_access( $_POST[ 'user' ], $msg ) ) {
+					$stmt = $db->prepare( 'UPDATE mailbox SET active = -2 WHERE username = ?;' );
+					$stmt->execute( [ $_POST[ 'user' ] ] );
+					$msg .= '<div class="green" role="alert">Successfully deleted mailbox.</div>';
+				}
+			} elseif ( $_POST[ 'action' ] === 'save_edit_admin' && ! empty( $_POST[ 'admin' ] ) && ( $_SESSION[ 'email_admin_superadmin' ] || $_POST[ 'admin' ] === $_SESSION[ 'email_admin_user' ] ) ) {
+				$stmt = $db->prepare( 'SELECT null FROM admin WHERE username = ?;' );
+				$stmt->execute( [ $_POST[ 'admin' ] ] );
+				if ( ! $stmt->fetch() ) {
+					$msg .= '<div class="red" role="alert">Oops, it looks like the admin account "' . htmlspecialchars( $_POST[ 'admin' ] ) . '" doesn\'t exist.</div>';
+				} else {
+					if ( ! empty( $_POST[ 'pass_update' ] ) ) {
+						if ( empty( $_POST[ 'pass_update2' ] ) || $_POST[ 'pass_update' ] !== $_POST[ 'pass_update2' ] ) {
+							$msg .= '<div class="red" role="alert">Passwords don\'t match!</div>';
+						} else {
+							$hash = password_hash( $_POST[ 'pass_update' ], PASSWORD_ARGON2ID );
+							$stmt = $db->prepare( 'UPDATE admin SET password_hash_type = "{ARGON2ID}", password = ?, modified = NOW() WHERE username = ?;' );
+							$stmt->execute( [ $hash, $_POST[ 'admin' ] ] );
+							$msg .= '<div class="green" role="alert">Successfully updated password.</div>';
+						}
+					}
+					if ( $_SESSION[ 'email_admin_superadmin' ] ) {
+						if ( $_POST[ 'admin' ] !== $_SESSION[ 'email_admin_user' ] ) {
+							$active = isset( $_POST[ 'active' ] ) ? 1 : 0;
+							$superadmin = isset( $_POST[ 'superadmin' ] ) ? 1 : 0;
+							$stmt = $db->prepare( 'UPDATE admin SET superadmin = ?, active = ?, modified = NOW() WHERE username = ?;' );
+							$stmt->execute( [ $superadmin, $active, $_POST[ 'admin' ] ] );
+						}
+						$managed_domains = [];
+						$stmt = $db->prepare( 'SELECT domain FROM domain_admins WHERE username = ?;' );
+						$stmt->execute( [ $_POST[ 'admin' ] ] );
+						while ( $tmp = $stmt->fetch( PDO::FETCH_ASSOC ) ) {
+							$managed_domains [] = $tmp[ 'domain' ];
+						}
+						foreach ( $managed_domains as $domain ) {
+							if ( ! in_array( $domain, $_POST[ 'domains' ], true ) ) {
+								$stmt = $db->prepare( 'DELETE FROM domain_admins WHERE username = ? AND domain = ?;' );
+								$stmt->execute( [ $_POST[ 'admin' ], $domain ] );
+							}
+						}
+						foreach ( $_POST[ 'domains' ] as $domain ) {
+							if ( ! in_array( $domain, $managed_domains, true ) ) {
+								$stmt = $db->prepare( 'INSERT INTO domain_admins (username, domain) VALUES (?, ?);' );
+								$stmt->execute( [ $_POST[ 'admin' ], $domain ] );
+							}
+						}
+					}
+					$msg .= '<div class="green" role="alert">Successfully edited admin account.</div>';
+				}
+			} elseif ( $_POST[ 'action' ] === 'save_new_admin' && ! empty( $_POST[ 'admin' ] ) && $_SESSION[ 'email_admin_superadmin' ] ) {
+				$stmt = $db->prepare( 'SELECT null FROM admin WHERE username = ?;' );
+				$stmt->execute( [ $_POST[ 'admin' ] ] );
+				if ( $stmt->fetch() ) {
+					$msg .= '<div class="red" role="alert">Oops, it looks like the admin account "' . htmlspecialchars( $_POST[ 'admin' ] ) . '" already exists.</div>';
+				} else {
+					if ( empty( $_POST[ 'pass_update2' ] ) || $_POST[ 'pass_update' ] !== $_POST[ 'pass_update2' ] ) {
+						$msg .= '<div class="red" role="alert">Passwords empty or don\'t match!</div>';
+					} else {
+						$hash = password_hash( $_POST[ 'pass_update' ], PASSWORD_ARGON2ID );
+						$active = isset( $_POST[ 'active' ] ) ? 1 : 0;
+						$superadmin = isset( $_POST[ 'superadmin' ] ) ? 1 : 0;
+						$stmt = $db->prepare( 'INSERT INTO admin (password_hash_type, password, superadmin, active, username, created, modified) VALUES ("{ARGON2ID}", ?, ?, ?, ?, NOW(), NOW());' );
+						$stmt->execute( [ $hash, $superadmin, $active, $_POST[ 'admin' ] ] );
+						$msg .= '<div class="green" role="alert">Successfully created admin account.</div>';
+					}
+				}
+			} elseif ( $_POST[ 'action' ] === 'save_edit_domain' && ! empty( $_POST[ 'domain' ] ) && $_SESSION[ 'email_admin_superadmin' ] ) {
+				$stmt = $db->prepare( 'SELECT null FROM domain WHERE domain = ?;' );
+				$stmt->execute( [ $_POST[ 'domain' ] ] );
+				if ( ! $stmt->fetch() ) {
+					$msg .= '<div class="red" role="alert">Oops, it looks like the domain "' . htmlspecialchars( $_POST[ 'domain' ] ) . '" doesn\'t exists.</div>';
+				} else {
+					$active = isset( $_POST[ 'active' ] ) ? 1 : 0;
+					$stmt = $db->prepare( 'UPDATE domain set active = ?, modified = NOW() WHERE domain = ?;' );
+					$stmt->execute( [ $active, $_POST[ 'domain' ] ] );
+					$msg .= '<div class="green" role="alert">Successfully updated domain.</div>';
+				}
+			} elseif ( $_POST[ 'action' ] === 'save_edit_alis_domain' && ! empty( $_POST[ 'alias_domain' ] ) && $_SESSION[ 'email_admin_superadmin' ] ) {
+				$stmt = $db->prepare( 'SELECT null FROM alias_domain WHERE alias_domain = ?;' );
+				$stmt->execute( [ $_POST[ 'alias_domain' ] ] );
+				if ( ! $stmt->fetch() ) {
+					$msg .= '<div class="red" role="alert">Oops, it looks like the alias domain "' . htmlspecialchars( $_POST[ 'alias_domain' ] ) . '" doesn\'t exists.</div>';
+				} else {
+					$active = isset( $_POST[ 'active' ] ) ? 1 : 0;
+					$stmt = $db->prepare( 'UPDATE alias_domain set active = ?, modified = NOW() WHERE alias_domain = ?;' );
+					$stmt->execute( [ $active, $_POST[ 'alias_domain' ] ] );
+					$msg .= '<div class="green" role="alert">Successfully updated alias domain.</div>';
+				}
+			} elseif ( $_POST[ 'action' ] === 'save_new_domain' && ! empty( $_POST[ 'domain' ] ) && $_SESSION[ 'email_admin_superadmin' ] ) {
+				$stmt = $db->prepare( 'SELECT null FROM domain WHERE domain = ?;' );
+				$stmt->execute( [ $_POST[ 'domain' ] ] );
+				if ( $stmt->fetch() ) {
+					$msg .= '<div class="red" role="alert">Oops, it looks like the domain "' . htmlspecialchars( $_POST[ 'domain' ] ) . '" already exists.</div>';
+				} else {
+					$active = isset( $_POST[ 'active' ] ) ? 1 : 0;
+					$stmt = $db->prepare( 'INSERT INTO domain (active, domain, created, modified) VALUES (?, ?, NOW(), NOW());' );
+					$stmt->execute( [ $active, $_POST[ 'domain' ] ] );
+					$msg .= '<div class="green" role="alert">Successfully created domain.</div>';
+				}
+			} elseif ( $_POST[ 'action' ] === 'save_new_alias_domain' && ! empty( $_POST[ 'alias_domain' ] ) && $_SESSION[ 'email_admin_superadmin' ] ) {
+				$stmt = $db->prepare( 'SELECT null FROM alias_domain WHERE alias_domain = ?;' );
+				$stmt->execute( [ $_POST[ 'alias_domain' ] ] );
+				if ( $stmt->fetch() ) {
+					$msg .= '<div class="red" role="alert">Oops, it looks like the alias domain "' . htmlspecialchars( $_POST[ 'domain' ] ) . '" already exists.</div>';
+				} else {
+					$active = isset( $_POST[ 'active' ] ) ? 1 : 0;
+					$stmt = $db->prepare( 'INSERT INTO alias_domain (active, alias_domain, target_domain, created, modified) VALUES (?, ?, ?, NOW(), NOW());' );
+					$stmt->execute( [ $active, $_POST[ 'alias_domain' ], $_POST[ 'target_domain' ] ] );
+					$msg .= '<div class="green" role="alert">Successfully created alias domain.</div>';
+				}
+			} elseif ( $_POST[ 'action' ] === 'save_new_alias' && ! empty( $_POST[ 'alias' ] ) && ! empty( $_POST[ 'target' ] ) ) {
+				$ok = check_email_valid( $_POST[ 'alias' ], $msg );
+				if ( $ok ) {
+					$ok = check_domain_access( $_POST[ 'alias' ], $msg );
+				}
+				if ( $ok ) {
+					$targets = preg_split( "/[\s,]+/", $_POST[ 'target' ] );
+					$alias_goto = validate_email_list( $targets, $msg );
+					$stmt = $db->prepare( 'SELECT null FROM alias WHERE address = ?;' );
+					$stmt->execute( [ $_POST[ 'alias' ] ] );
+					if ( $stmt->fetch() ) {
+						$msg .= '<div class="red" role="alert">Oops, it looks like the alias "' . htmlspecialchars( $_POST[ 'alias' ] ) . '" already exists.</div>';
+					} else {
+						$parser = new EmailParser( new EmailLexer() );
+						$parser->parse( $_POST[ 'alias' ] );
+						$domain = $parser->getDomainPart();
+						$active = isset( $_POST[ 'active' ] ) ? 1 : 0;
+						$enforce_tls_in = isset( $_POST[ 'enforce_tls_in' ] ) ? 1 : 0;
+						$stmt = $db->prepare( 'INSERT INTO alias (goto, address, domain, active, created, modified, enforce_tls_in) VALUES (?, ?, ?, ?, NOW(), NOW(), ?);' );
+						$stmt->execute( [ $alias_goto, $_POST[ 'alias' ], $domain, $active, $enforce_tls_in ] );
+						$msg .= '<div class="green" role="alert">Successfully added alias.</div>';
+					}
+				}
+			} elseif ( $_POST[ 'action' ] === 'save_edit_alias' && ! empty( $_POST[ 'alias' ] ) && ! empty( $_POST[ 'target' ] ) ) {
+				$ok = check_email_valid( $_POST[ 'alias' ], $msg );
+				if ( $ok ) {
+					$ok = check_domain_access( $_POST[ 'alias' ], $msg );
+				}
+				if ( $ok ) {
+					$targets = preg_split( "/[\s,]+/", $_POST[ 'target' ] );
+					$alias_goto = validate_email_list( $targets, $msg );
+					$active = isset( $_POST[ 'active' ] ) ? 1 : 0;
+					$enforce_tls_in = isset( $_POST[ 'enforce_tls_in' ] ) ? 1 : 0;
+					$stmt = $db->prepare( 'UPDATE alias SET goto = ?, active = ?, enforce_tls_in = ?, modified = NOW() WHERE address = ?;' );
+					$stmt->execute( [ $alias_goto, $active, $enforce_tls_in, $_POST[ 'alias' ] ] );
+					$msg .= '<div class="green" role="alert">Successfully updated alias.</div>';
+				}
+			} elseif ( $_POST[ 'action' ] === 'save_edit_mailbox' && ! empty( $_POST[ 'user' ] ) ) {
+				$ok = check_email_valid( $_POST[ 'user' ], $msg );
+				if ( $ok ) {
+					$ok = check_domain_access( $_POST[ 'user' ], $msg );
+				}
+				if ( $ok ) {
+					$alias_goto = '';
+					if ( isset( $_POST[ 'alias_keep_copy' ] ) ) {
+						$alias_goto .= $_POST[ 'user' ] . ',';
+					}
+					if ( ! empty( $_POST[ 'alias_to' ] ) ) {
+						$additional = preg_split( "/[\s,]+/", $_POST[ 'alias_to' ] );
+						$alias_goto .= validate_email_list( $additional, $msg );
+					}
+					$quota = 1024 * 1024 * 1024;
+					$alias_goto = rtrim( $alias_goto, ',' );
+					$stmt = $db->prepare( 'UPDATE alias SET goto = ?, enforce_tls_in = ?, active = ? WHERE address = ?;' );
+					$stmt->execute( [ $alias_goto, ( isset( $_POST[ 'enforce_tls_in' ] ) ? 1 : 0 ), ( isset( $_POST[ 'active' ] ) ? 1 : 0 ), $_POST[ 'user' ] ] );
+					$stmt = $db->prepare( 'UPDATE mailbox SET enforce_tls_in = ?, enforce_tls_out = ?, active = ?, quota = ?, modified = NOW() WHERE username = ?;' );
+					$stmt->execute( [ ( isset( $_POST[ 'enforce_tls_in' ] ) ? 1 : 0 ), ( isset( $_POST[ 'enforce_tls_out' ] ) ? 1 : 0 ), ( isset( $_POST[ 'active' ] ) ? 1 : 0 ), $quota, $_POST[ 'user' ] ] );
+					$msg .= '<div class="green" role="alert">Successfully updated mailbox.</div>';
+				}
+			} elseif ( $_POST[ 'action' ] === 'save_new_mailbox' && ! empty( $_POST[ 'user' ] ) ) {
+				$email = $_POST[ 'user' ];
+				$ok = check_email_valid( $email, $msg );
+				if ( $ok ) {
+					$ok = check_domain_access( $email, $msg );
+				}
+				if ( $ok ) {
+					$stmt = $db->prepare( 'SELECT null FROM mailbox WHERE username = ? UNION SELECT null FROM alias WHERE address = ?;' );
+					$stmt->execute( [ $email, $email ] );
+					if ( $stmt->fetch() ) {
+						$ok = false;
+						$msg .= '<div class="red" role="alert">Sorry, this user already exists</div>';
+					}
+					if ( $ok ) {
+						$parser = new EmailParser( new EmailLexer() );
+						$parser->parse( $email );
+						$user = $parser->getLocalPart();
+						$domain = $parser->getDomainPart();
+						$hash = password_hash( $_POST[ 'pwd' ], PASSWORD_ARGON2ID );
+						$quota = 50 * 1024 * 1024;
+						$alias_goto = '';
+						if ( isset( $_POST[ 'alias_keep_copy' ] ) ) {
+							$alias_goto .= $email . ',';
+						}
+						if ( ! empty( $_POST[ 'alias_to' ] ) ) {
+							$additional = preg_split( "/[\s,]+/", $_POST[ 'alias_to' ] );
+							$alias_goto .= validate_email_list( $additional, $msg );
+						}
+						$alias_goto = rtrim( $alias_goto, ',' );
+						$stmt = $db->prepare( 'INSERT INTO alias (address, goto, domain, created, modified, enforce_tls_in, active) VALUES (?, ?, ?, NOW(), NOW(), ?, ?);' );
+						$stmt->execute( [ $email, $alias_goto, $domain, ( isset( $_POST[ 'enforce_tls_in' ] ) ? 1 : 0 ), ( isset( $_POST[ 'active' ] ) ? 1 : 0 ) ] );
+						$stmt = $db->prepare( 'INSERT INTO mailbox (username, password, quota, local_part, domain, created, modified, password_hash_type, openpgpkey_wkd, enforce_tls_in, enforce_tls_out, active) VALUES(?, ?, ?, ?, ?, NOW(), NOW(), ?, ?, ?, ?, ?);' );
+						$stmt->execute( [ $email, $hash, $quota, $user, $domain, '{ARGON2ID}', z_base32_encode( hash( 'sha1', mb_strtolower( $user ), true ) ), ( isset( $_POST[ 'enforce_tls_in' ] ) ? 1 : 0 ), ( isset( $_POST[ 'enforce_tls_out' ] ) ? 1 : 0 ), ( isset( $_POST[ 'active' ] ) ? 1 : 0 ) ] );
+						$msg .= '<div class="green" role="alert">Successfully created new mailbox!</div>';
+					}
+				}
+			} elseif ( $_POST[ 'action' ] === 'save_password_mailbox' && ! empty( $_POST[ 'user' ] ) ) {
+				$ok = check_email_valid( $_POST[ 'user' ], $msg );
+				if ( $ok ) {
+					$ok = check_domain_access( $_POST[ 'user' ], $msg );
+				}
+				if ( $ok ) {
+					if ( empty( $_POST[ 'pass_update' ] ) || empty( $_POST[ 'pass_update2' ] ) || $_POST[ 'pass_update' ] !== $_POST[ 'pass_update2' ] ) {
+						$msg .= '<div class="red" role="alert">Passwords empty or don\'t match</div>';
+					} else {
+						$hash = password_hash( $_POST[ 'pass_update' ], PASSWORD_ARGON2ID );
+						$stmt = $db->prepare( 'UPDATE mailbox SET password_hash_type = "{ARGON2ID}", password = ? WHERE username = ?;' );
+						$stmt->execute( [ $hash, $_POST[ 'user' ] ] );
+						$msg .= '<div class="green" role="alert">Successfully updated password</div>';
+					}
+				}
+			} elseif ( $_POST[ 'action' ] === 'disable_tfa_mailbox' && ! empty( $_POST[ 'user' ] ) ) {
+				$ok = check_email_valid( $_POST[ 'user' ], $msg );
+				if ( $ok ) {
+					$ok = check_domain_access( $_POST[ 'user' ], $msg );
+				}
+				if ( $ok ) {
+					$stmt = $db->prepare( 'UPDATE mailbox SET tfa = 0 WHERE username = ?;' );
+					$stmt->execute( [ $_POST[ 'user' ] ] );
+					$msg .= '<div class="green" role="alert">Successfully disabled two factor authentication</div>';
+				}
+			}
+		}
+	}
+}
+?>
+<!DOCTYPE html>
+    <html lang="en-gb">
+    <head>
+        <title>Daniel - E-Mail and XMPP - Admin management</title>
+        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+        <meta name="author" content="Daniel Winzen">
+        <meta name="viewport" content="width=device-width, initial-scale=1">
+        <meta name="description" content="Lets admins manage their email domain and user accounts.">
+        <link rel="canonical" href="https://danwin1210.de/mail/admin.php">
+    </head>
+    <body>
+        <main>
+	<?php if ( ! empty( $_SESSION[ 'email_admin_user' ] ) ) { ?>
+        <form method="post"><input type="hidden" name="csrf_token" value="<?php echo $_SESSION[ 'csrf_token' ]; ?>">
+        <p>Logged in as <?php echo htmlspecialchars( $_SESSION[ 'email_admin_user' ] ); ?> |
+            <button name="action" value="logout" type="submit">Logout</button><?php
+			if ( $_SESSION[ 'email_admin_superadmin' ] ) {
+				?> | <a href="?action=admins">Manage admins</a><?php
+				?> | <a href="?action=alias_domains">Manage alias domains</a><?php
+			} else {
+				?> | <a href="?action=edit_admin">Manage your admin account</a><?php
+			}
+			?> | <a href="?action=domains">Manage domains</a><?php
+			?> | <a href="?action=alias">Manage aliases</a><?php
+			?> | <a href="?action=mailbox">Manage mailboxes</a><?php
+			?></p></form><?php
+	}
+	echo "<p>$msg</p>";
+	if ( empty( $_SESSION[ 'email_admin_user' ] ) ) { ?>
+        <form class="form_limit" action="admin.php" method="post">
+            <input type="hidden" name="csrf_token" value="<?php echo $_SESSION[ 'csrf_token' ]; ?>">
+            <div class="row">
+                <div class="col"><label for="user">Username</label></div>
+                <div class="col"><input type="text" name="user" id="user" autocomplete="username" required></div>
+            </div>
+            <div class="row">
+                <div class="col"><label for="pwd">Password</label></div>
+                <div class="col"><input type="password" name="pwd" id="pwd" autocomplete="new-password" required></div>
+            </div>
+            <div class="row">
+                <div class="col">
+                    <button name="action" value="login" type="submit">Login</button>
+                </div>
+            </div>
+        </form>
+	<?php } else {
+		if ( empty( $_REQUEST[ 'action' ] ) || $_REQUEST[ 'action' ] === 'login' ) {
+			?><p>Welcome to the admin management interface. You can configure your domain(s) and accounts here. Please
+                select an option from the menu.</p><?php
+		} elseif ( in_array( $_REQUEST[ 'action' ], [ 'admins', 'delete_admin2' ], true ) && $_SESSION[ 'email_admin_superadmin' ] ) {
+			send_manage_admins();
+		} elseif ( in_array( $_REQUEST[ 'action' ], [ 'domains', 'delete_domain2' ], true ) ) {
+			send_manage_domains();
+		} elseif ( in_array( $_REQUEST[ 'action' ], [ 'alias_domains', 'delete_alias_domain2' ], true ) && $_SESSION[ 'email_admin_superadmin' ] ) {
+			send_manage_alias_domains();
+		} elseif ( in_array( $_REQUEST[ 'action' ], [ 'alias', 'delete_alias2' ], true ) ) {
+			send_manage_aliases();
+		} elseif ( in_array( $_REQUEST[ 'action' ], [ 'mailbox', 'delete_mailbox2' ], true ) ) {
+			send_manage_mailboxes();
+		} elseif ( in_array( $_REQUEST[ 'action' ], [ 'new_admin', 'save_new_admin' ], true ) && $_SESSION[ 'email_admin_superadmin' ] ) {
+			send_new_admin();
+		} elseif ( in_array( $_REQUEST[ 'action' ], [ 'new_domain', 'save_new_domain' ], true ) && $_SESSION[ 'email_admin_superadmin' ] ) {
+			send_new_domain();
+		} elseif ( in_array( $_REQUEST[ 'action' ], [ 'new_alias_domain', 'save_new_alias_domain' ], true ) && $_SESSION[ 'email_admin_superadmin' ] ) {
+			send_new_alias_domain();
+		} elseif ( in_array( $_REQUEST[ 'action' ], [ 'new_alias', 'save_new_alias' ], true ) ) {
+			send_new_alias();
+		} elseif ( in_array( $_REQUEST[ 'action' ], [ 'new_mailbox', 'save_new_mailbox' ], true ) ) {
+			send_new_mailbox();
+		} elseif ( in_array( $_REQUEST[ 'action' ], [ 'edit_admin', 'save_edit_admin' ], true ) ) {
+			send_edit_admin();
+		} elseif ( in_array( $_REQUEST[ 'action' ], [ 'edit_domain', 'save_edit_domain' ], true ) ) {
+			send_edit_domain();
+		} elseif ( in_array( $_REQUEST[ 'action' ], [ 'edit_alias_domain', 'save_edit_alias_domain' ], true ) && $_SESSION[ 'email_admin_superadmin' ] ) {
+			send_edit_alias_domain();
+		} elseif ( in_array( $_REQUEST[ 'action' ], [ 'edit_alias', 'save_edit_alias' ], true ) ) {
+			send_edit_alias();
+		} elseif ( in_array( $_REQUEST[ 'action' ], [ 'edit_mailbox', 'save_edit_mailbox', 'save_password_mailbox', 'disable_tfa_mailbox' ], true ) ) {
+			send_edit_mailbox();
+		} elseif ( empty( $msg ) ) {
+			?><p>Oops, it looks like the page you tried to access does not exist or you do not have permission to access
+                it.</p><?php
+		}
+	} ?>
+    </main>
+    </body>
+</html>
+
+<?php
+function send_manage_admins(): void
+{
+	$db = get_db_instance();
+	$stmt = $db->query( 'SELECT username, modified, active FROM admin;' );
+	?>
+    <p><a href="?action=new_admin">Create new admin</a></p>
+    <form class="form_limit" action="admin.php" method="post">
+        <input type="hidden" name="csrf_token" value="<?php echo $_SESSION[ 'csrf_token' ]; ?>">
+        <input type="hidden" name="action" value="edit_admin">
+        <div class="row">
+            <div class="col">Admin</div>
+            <div class="col">Active</div>
+            <div class="col">Last modified</div>
+            <div class="col">Edit account</div>
+        </div>
+		<?php
+		while ( $tmp = $stmt->fetch( PDO::FETCH_ASSOC ) ) {
+			$active = 'Disabled';
+			if ( $tmp[ 'active' ] === 1 ) {
+				$active = 'Active';
+			}
+			echo '<div class="row"><div class="col">' . htmlspecialchars( $tmp[ 'username' ] ) . '</div><div class="col">' . $active . '</div><div class="col">' . $tmp[ 'modified' ] . '</div><div class="col"><button type="submit" name="admin" value="' . htmlspecialchars( $tmp[ 'username' ] ) . '">Edit</button></div></div>';
+		}
+		?></form>
+    <p><a href="?action=new_admin">Create new admin</a></p>
+	<?php
+}
+
+function send_edit_admin(): void
+{
+	$db = get_db_instance();
+	$admin = $_POST[ 'admin' ] ?? $_SESSION[ 'email_admin_user' ];
+	$stmt = $db->prepare( 'SELECT username, superadmin, active FROM admin WHERE username = ?;' );
+	$stmt->execute( [ $admin ] );
+	if ( $admin = $stmt->fetch( PDO::FETCH_ASSOC ) ) {
+		?>
+        <h2>Edit admin account <?php echo htmlspecialchars( $admin[ 'username' ] ); ?></h2>
+        <form class="form_limit" action="admin.php" method="post">
+            <input type="hidden" name="csrf_token" value="<?php echo $_SESSION[ 'csrf_token' ]; ?>">
+            <input type="hidden" name="admin" value="<?php echo htmlspecialchars( $admin[ 'username' ] ); ?>"
+                   autocomplete="username">
+            <div class="row">
+                <div class="col"><label for="pass_update">Password</label></div>
+                <div class="col"><input type="password" name="pass_update" id="pass_update" autocomplete="new-password">
+                </div>
+            </div>
+            <div class="row">
+                <div class="col"><label for="pass_update2">Password again</label></div>
+                <div class="col"><input type="password" name="pass_update2" id="pass_update2"
+                                        autocomplete="new-password"></div>
+            </div>
+			<?php if ( $admin[ 'username' ] !== $_SESSION[ 'email_admin_user' ] ) { ?>
+                <div class="row">
+                    <div class="col"><label><input type="checkbox" name="superadmin"
+                                                   value="1"<?php echo $admin[ 'superadmin' ] ? ' checked' : ''; ?>>Superadmin</label>
+                    </div>
+                    <div class="col">Superadmins can manage other admins</div>
+                </div>
+                <div class="row">
+                    <div class="col"><label><input type="checkbox" name="active"
+                                                   value="1"<?php echo $admin[ 'active' ] ? ' checked' : ''; ?>>Active</label>
+                    </div>
+                </div>
+			<?php } else { ?>
+                <div class="row">
+                    <div class="col"><label><input type="checkbox" name="superadmin"
+                                                   value="1"<?php echo $admin[ 'superadmin' ] ? ' checked' : ''; ?>
+                                                   disabled>Superadmin</label></div>
+                    <div class="col">Superadmins can manage other admins</div>
+                </div>
+                <div class="row">
+                    <div class="col"><label><input type="checkbox" name="active"
+                                                   value="1"<?php echo $admin[ 'active' ] ? ' checked' : ''; ?>
+                                                   disabled>Active</label></div>
+                </div>
+			<?php } ?>
+            <div class="row">
+                <div class="col"><label for="domains">Managed domains</label></div>
+                <div class="col"><select name="domains[]" id="domains" multiple><?php
+						$domains = [];
+						$managed_domains = [];
+						$stmt = $db->query( 'SELECT domain FROM domain;' );
+						while ( $tmp = $stmt->fetch( PDO::FETCH_ASSOC ) ) {
+							$domains [] = $tmp[ 'domain' ];
+						}
+						$stmt = $db->prepare( 'SELECT domain FROM domain_admins WHERE username = ?;' );
+						$stmt->execute( [ $admin[ 'username' ] ] );
+						while ( $tmp = $stmt->fetch( PDO::FETCH_ASSOC ) ) {
+							$managed_domains [] = $tmp[ 'domain' ];
+						}
+						foreach ( $domains as $domain ) {
+							echo '<option value="' . htmlspecialchars( $domain ) . '"' . ( in_array( $domain, $managed_domains, true ) ? ' selected' : '' ) . '>' . htmlspecialchars( $domain ) . '</value>';
+						}
+						?></select></div>
+            </div>
+            <div class="row">
+                <div class="col">
+                    <button name="action" value="save_edit_admin" type="submit">Save changes</button>
+                </div>
+            </div>
+			<?php if ( $admin[ 'username' ] !== $_SESSION[ 'email_admin_user' ] ) { ?>
+                <div class="row">
+                    <div class="col">
+                        <button type="submit" name="action" value="delete_admin">Delete admin</button>
+                    </div>
+                </div>
+			<?php } ?>
+        </form>
+		<?php
+	} else {
+		echo '<p>Oops, this admin doesn\'t seem to exist.</p>';
+	}
+}
+
+function send_new_admin(): void
+{
+	?>
+    <h2>Create new admin account</h2>
+    <form class="form_limit" action="admin.php" method="post">
+        <input type="hidden" name="csrf_token" value="<?php echo $_SESSION[ 'csrf_token' ]; ?>">
+        <div class="row">
+            <div class="col"><label for="admin">Username</label></div>
+            <div class="col"><input type="text" name="admin" id="admin" autocomplete="username"></div>
+        </div>
+        <div class="row">
+            <div class="col"><label for="pass_update">Password</label></div>
+            <div class="col"><input type="password" name="pass_update" id="pass_update" autocomplete="new-password">
+            </div>
+        </div>
+        <div class="row">
+            <div class="col"><label for="pass_update2">Password again</label></div>
+            <div class="col"><input type="password" name="pass_update2" id="pass_update2" autocomplete="new-password">
+            </div>
+        </div>
+        <div class="row">
+            <div class="col"><label><input type="checkbox" name="superadmin" value="1">Superadmin</label></div>
+            <div class="col">Superadmins can manage other admins</div>
+        </div>
+        <div class="row">
+            <div class="col"><label><input type="checkbox" name="active" value="1">Active</label></div>
+        </div>
+        <div class="row">
+            <div class="col">
+                <button name="action" value="save_new_admin" type="submit">Add admin</button>
+            </div>
+        </div>
+    </form>
+	<?php
+}
+
+function send_manage_domains(): void
+{
+	$db = get_db_instance();
+	$stmt = $db->query( 'SELECT domain, modified, active FROM domain;' );
+	if ( $_SESSION[ 'email_admin_superadmin' ] ) {
+		?>
+        <p><a href="?action=new_domain">Create new domain</a></p>
+	<?php } ?>
+    <form class="form_limit" action="admin.php" method="post">
+        <input type="hidden" name="csrf_token" value="<?php echo $_SESSION[ 'csrf_token' ]; ?>">
+        <input type="hidden" name="action" value="edit_domain">
+        <div class="row">
+            <div class="col">Domain</div>
+            <div class="col">Active</div>
+            <div class="col">Last modified</div>
+            <div class="col">Edit domain</div>
+        </div>
+		<?php
+		while ( $tmp = $stmt->fetch( PDO::FETCH_ASSOC ) ) {
+			$active = 'Disabled';
+			if ( $tmp[ 'active' ] === 1 ) {
+				$active = 'Active';
+			} elseif ( $tmp[ 'active' ] === -1 ) {
+				$active = 'Deleting';
+			}
+			echo '<div class="row"><div class="col">' . htmlspecialchars( $tmp[ 'domain' ] ) . '</div><div class="col">' . $active . '</div><div class="col">' . $tmp[ 'modified' ] . '</div><div class="col"><button type="submit" name="domain" value="' . htmlspecialchars( $tmp[ 'domain' ] ) . '">Edit</button></div></div>';
+		}
+		?></form>
+	<?php if ( $_SESSION[ 'email_admin_superadmin' ] ) { ?>
+    <p><a href="?action=new_domain">Create new domain</a></p>
+	<?php
+}
+}
+
+function send_new_domain(): void
+{
+	?>
+    <h2>Create new domain</h2>
+    <form class="form_limit" action="admin.php" method="post">
+        <input type="hidden" name="csrf_token" value="<?php echo $_SESSION[ 'csrf_token' ]; ?>">
+        <div class="row">
+            <div class="col"><label for="domain">Domain</label></div>
+            <div class="col"><input type="text" name="domain" id="domain"></div>
+        </div>
+        <div class="row">
+            <div class="col"><label><input type="checkbox" name="active" value="1">Active</label></div>
+        </div>
+        <div class="row">
+            <div class="col">
+                <button name="action" value="save_new_domain" type="submit">Add domain</button>
+            </div>
+        </div>
+    </form>
+	<?php
+}
+
+function send_edit_domain(): void
+{
+	$db = get_db_instance();
+	$stmt = $db->prepare( 'SELECT domain, active FROM domain WHERE domain = ?;' );
+	$stmt->execute( [ $_POST[ 'domain' ] ] );
+	if ( $admin = $stmt->fetch( PDO::FETCH_ASSOC ) ) {
+		?>
+        <h2>Edit domain <?php echo htmlspecialchars( $_POST[ 'domain' ] ); ?></h2>
+        <form class="form_limit" action="admin.php" method="post">
+            <input type="hidden" name="csrf_token" value="<?php echo $_SESSION[ 'csrf_token' ]; ?>">
+            <input type="hidden" name="domain" value="<?php echo htmlspecialchars( $_POST[ 'domain' ] ); ?>">
+            <div class="row">
+                <div class="col"><label><input type="checkbox" name="active"
+                                               value="1"<?php echo $admin[ 'active' ] ? ' checked' : ''; ?>>Active</label>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col">
+                    <button name="action" value="save_edit_domain" type="submit">Save changes</button>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col">
+                    <button type="submit" name="action" value="delete_domain">Delete domain</button>
+                </div>
+            </div>
+        </form>
+		<?php
+	} else {
+		echo '<p>Oops, this admin doesn\'t seem to exist.</p>';
+	}
+}
+
+function send_manage_alias_domains(): void
+{
+	$db = get_db_instance();
+	$stmt = $db->query( 'SELECT alias_domain, target_domain, modified, active FROM alias_domain;' );
+	if ( $_SESSION[ 'email_admin_superadmin' ] ) {
+		?>
+        <p><a href="?action=new_alias_domain">Create new alias domain</a></p>
+	<?php } ?>
+    <form class="form_limit" action="admin.php" method="post">
+        <input type="hidden" name="csrf_token" value="<?php echo $_SESSION[ 'csrf_token' ]; ?>">
+        <input type="hidden" name="action" value="edit_alias_domain">
+        <div class="row">
+            <div class="col">Alias Domain</div>
+            <div class="col">Target Domain</div>
+            <div class="col">Active</div>
+            <div class="col">Last modified</div>
+            <div class="col">Edit alias domain</div>
+        </div>
+		<?php
+		while ( $tmp = $stmt->fetch( PDO::FETCH_ASSOC ) ) {
+			$active = 'Disabled';
+			if ( $tmp[ 'active' ] === 1 ) {
+				$active = 'Active';
+			}
+			echo '<div class="row"><div class="col">' . htmlspecialchars( $tmp[ 'alias_domain' ] ) . '</div><div class="col">' . htmlspecialchars( $tmp[ 'target_domain' ] ) . '</div><div class="col">' . $active . '</div><div class="col">' . $tmp[ 'modified' ] . '</div><div class="col"><button type="submit" name="alias_domain" value="' . htmlspecialchars( $tmp[ 'alias_domain' ] ) . '">Edit</button></div></div>';
+		}
+		?></form>
+	<?php if ( $_SESSION[ 'email_admin_superadmin' ] ) { ?>
+    <p><a href="?action=new_alias_domain">Create new alias domain</a></p>
+	<?php
+}
+}
+
+function send_new_alias_domain(): void
+{
+	?>
+    <h2>Create new alias domain</h2>
+    <form class="form_limit" action="admin.php" method="post">
+        <input type="hidden" name="csrf_token" value="<?php echo $_SESSION[ 'csrf_token' ]; ?>">
+        <div class="row">
+            <div class="col"><label for="alias_domain">Alias Domain</label></div>
+            <div class="col"><input type="text" name="alias_domain" id="alias_domain"></div>
+        </div>
+        <div class="row">
+            <div class="col"><label for="target_domain">Target Domain</label></div>
+            <div class="col"><input type="text" name="target_domain" id="target_domain"></div>
+        </div>
+        <div class="row">
+            <div class="col"><label><input type="checkbox" name="active" value="1">Active</label></div>
+        </div>
+        <div class="row">
+            <div class="col">
+                <button name="action" value="save_new_alias_domain" type="submit">Add alias domain</button>
+            </div>
+        </div>
+    </form>
+	<?php
+}
+
+function send_edit_alias_domain(): void
+{
+	$db = get_db_instance();
+	$stmt = $db->prepare( 'SELECT alias_domain, target_domain, active FROM alias_domain WHERE alias_domain = ?;' );
+	$stmt->execute( [ $_POST[ 'alias_domain' ] ] );
+	if ( $alias = $stmt->fetch( PDO::FETCH_ASSOC ) ) {
+		?>
+        <h2>Edit alias domain <?php echo htmlspecialchars( $_POST[ 'alias_domain' ] ); ?></h2>
+        <form class="form_limit" action="admin.php" method="post">
+            <input type="hidden" name="csrf_token" value="<?php echo $_SESSION[ 'csrf_token' ]; ?>">
+            <input type="hidden" name="alias_domain"
+                   value="<?php echo htmlspecialchars( $_POST[ 'alias_domain' ] ); ?>">
+            <div class="row">
+                <div class="col"><label for="target_domain">Target Domain</label></div>
+                <div class="col"><input type="text" name="target_domain" id="target_domain"
+                                        value="<?php echo htmlspecialchars( $alias[ 'target_domain' ] ); ?>"></div>
+            </div>
+            <div class="row">
+                <div class="col"><label><input type="checkbox" name="active"
+                                               value="1"<?php echo $alias[ 'active' ] ? ' checked' : ''; ?>>Active</label>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col">
+                    <button name="action" value="save_edit_alias_domain" type="submit">Save changes</button>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col">
+                    <button type="submit" name="action" value="delete_alias_domain">Delete alias domain</button>
+                </div>
+            </div>
+        </form>
+		<?php
+	} else {
+		echo '<p>Oops, this alias domain doesn\'t seem to exist.</p>';
+	}
+}
+
+function send_manage_aliases(): void
+{
+	$db = get_db_instance();
+	$stmt = $db->prepare( 'SELECT a.address, a.goto, a.modified, a.active FROM alias AS a LEFT JOIN mailbox AS m ON (m.username=a.address AND m.active=1) WHERE a.domain IN (SELECT domain FROM domain_admins WHERE username = ?) AND isnull(m.username) limit 200;' );
+	$stmt->execute( [ $_SESSION[ 'email_admin_user' ] ] );
+	?>
+    <p><a href="?action=new_alias">Create new alias</a></p>
+    <form class="form_limit" action="admin.php" method="post">
+        <input type="hidden" name="csrf_token" value="<?php echo $_SESSION[ 'csrf_token' ]; ?>">
+        <input type="hidden" name="action" value="edit_alias">
+        <div class="row">
+            <div class="col">Alias</div>
+            <div class="col">Target</div>
+            <div class="col">Active</div>
+            <div class="col">Last modified</div>
+            <div class="col">Edit alias</div>
+        </div>
+		<?php
+		while ( $tmp = $stmt->fetch( PDO::FETCH_ASSOC ) ) {
+			$active = 'Disabled';
+			if ( $tmp[ 'active' ] === 1 ) {
+				$active = 'Active';
+			}
+			echo '<div class="row"><div class="col">' . htmlspecialchars( $tmp[ 'address' ] ) . '</div><div class="col">' . htmlspecialchars( $tmp[ 'goto' ] ) . '</div><div class="col">' . $active . '</div><div class="col">' . $tmp[ 'modified' ] . '</div><div class="col"><button type="submit" name="alias" value="' . htmlspecialchars( $tmp[ 'address' ] ) . '">Edit</button></div></div>';
+		}
+		?></form>
+    <p><a href="?action=new_alias">Create new alias</a></p>
+	<?php
+}
+
+function send_new_alias(): void
+{
+	?>
+    <h2>Create new alias</h2>
+    <form class="form_limit" action="admin.php" method="post">
+        <input type="hidden" name="csrf_token" value="<?php echo $_SESSION[ 'csrf_token' ]; ?>">
+        <div class="row">
+            <div class="col"><label for="alias">Alias</label></div>
+            <div class="col"><input type="text" name="alias" id="alias"></div>
+        </div>
+        <div class="row">
+            <div class="col"><label for="target">Target</label></div>
+            <div class="col"><input type="text" name="target" id="target"></div>
+        </div>
+        <div class="row">
+            <div class="col"><label><input type="checkbox" name="active" value="1">Active</label></div>
+        </div>
+        <div class="row">
+            <div class="col"><label><input type="checkbox" name="enforce_tls_in" value="1">Enforce encryption</label>
+            </div>
+        </div>
+        <div class="row">
+            <div class="col">
+                <button name="action" value="save_new_alias" type="submit">Add alias</button>
+            </div>
+        </div>
+    </form>
+	<?php
+}
+
+function send_edit_alias(): void
+{
+	$db = get_db_instance();
+	$stmt = $db->prepare( 'SELECT a.address, a.goto, a.active, a.enforce_tls_in FROM alias AS a LEFT JOIN mailbox AS m ON (m.username=a.address AND m.active=1) WHERE a.address = ? AND isnull(m.username);' );
+	$stmt->execute( [ $_POST[ 'alias' ] ] );
+	if ( $alias = $stmt->fetch( PDO::FETCH_ASSOC ) ) {
+		?>
+        <h2>Edit alias <?php echo htmlspecialchars( $_POST[ 'alias' ] ); ?></h2>
+        <form class="form_limit" action="admin.php" method="post">
+            <input type="hidden" name="csrf_token" value="<?php echo $_SESSION[ 'csrf_token' ]; ?>">
+            <input type="hidden" name="alias" value="<?php echo htmlspecialchars( $_POST[ 'alias' ] ); ?>">
+            <div class="row">
+                <div class="col"><label for="target">Target</label></div>
+                <div class="col"><textarea name="target"
+                                           id="target"><?php echo str_replace( ',', "\n", htmlspecialchars( $alias[ 'goto' ] ) ); ?></textarea>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col"><label><input type="checkbox" name="active"
+                                               value="1"<?php echo $alias[ 'active' ] ? ' checked' : ''; ?>>Active</label>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col"><label><input type="checkbox" name="enforce_tls_in"
+                                               value="1"<?php echo $alias[ 'enforce_tls_in' ] ? ' checked' : ''; ?>>Enforce
+                        encryption</label></div>
+            </div>
+            <div class="row">
+                <div class="col">
+                    <button name="action" value="save_edit_alias" type="submit">Save changes</button>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col">
+                    <button type="submit" name="action" value="delete_alias">Delete alias</button>
+                </div>
+            </div>
+        </form>
+		<?php
+	} else {
+		echo '<p>Oops, this alias doesn\'t seem to exist.</p>';
+	}
+}
+
+function send_manage_mailboxes(): void
+{
+	$db = get_db_instance();
+	$stmt = $db->prepare( 'SELECT username, modified, active FROM mailbox WHERE domain IN (SELECT domain FROM domain_admins WHERE username = ?) limit 200;' );
+	$stmt->execute( [ $_SESSION[ 'email_admin_user' ] ] );
+	?>
+    <p><a href="?action=new_mailbox">Create new mailbox</a></p>
+    <form class="form_limit" action="admin.php" method="post">
+        <input type="hidden" name="csrf_token" value="<?php echo $_SESSION[ 'csrf_token' ]; ?>">
+        <input type="hidden" name="action" value="edit_mailbox">
+        <div class="row">
+            <div class="col">Username</div>
+            <div class="col">Active</div>
+            <div class="col">Last modified</div>
+            <div class="col">Edit mailbox</div>
+        </div>
+		<?php
+		while ( $tmp = $stmt->fetch( PDO::FETCH_ASSOC ) ) {
+			$active = 'Disabled';
+			if ( $tmp[ 'active' ] === 1 ) {
+				$active = 'Active';
+			} elseif ( $tmp[ 'active' ] === -1 ) {
+				$active = 'Disabling';
+			} elseif ( $tmp[ 'active' ] === -2 ) {
+				$active = 'Deleting';
+			}
+			echo '<div class="row"><div class="col">' . htmlspecialchars( $tmp[ 'username' ] ) . '</div><div class="col">' . $active . '</div><div class="col">' . $tmp[ 'modified' ] . '</div><div class="col"><button type="submit" name="user" value="' . htmlspecialchars( $tmp[ 'username' ] ) . '">Edit</button></div></div>';
+		}
+		?></form>
+    <p><a href="?action=new_mailbox">Create new mailbox</a></p>
+	<?php
+}
+
+function send_new_mailbox(): void
+{
+	?>
+    <h2>Create new mailbox</h2>
+    <form class="form_limit" action="admin.php" method="post">
+        <input type="hidden" name="csrf_token" value="<?php echo $_SESSION[ 'csrf_token' ]; ?>">
+        <div class="row">
+            <div class="col"><label for="user">Username</label></div>
+            <div class="col"><input type="text" name="user" id="user"></div>
+        </div>
+        <div class="row">
+            <div class="col"><label for="pwd">Password</label></div>
+            <div class="col"><input type="password" name="pwd" id="pwd" autocomplete="new-password" required></div>
+        </div>
+        <div class="row">
+            <div class="col"><label for="pwd2">Password again</label></div>
+            <div class="col"><input type="password" name="pwd2" id="pwd2" autocomplete="new-password" required></div>
+        </div>
+        <div class="row">
+            <div class="col"><label for="alias_to">Forward to</label></div>
+            <div class="col"><textarea name="alias_to" id="alias_to"></textarea></div>
+        </div>
+        <div class="row">
+            <div class="col"><label for="alias_keep_copy">Keep a local copy</label></div>
+            <div class="col"><input type="checkbox" name="alias_keep_copy" id="alias_keep_copy" checked></div>
+        </div>
+        <div class="row">
+            <div class="col"><label><input type="checkbox" name="active" value="1" checked>Active</label></div>
+        </div>
+        <div class="row">
+            <div class="col"><label><input type="checkbox" name="enforce_tls_in" value="1" checked>Enforce encryption
+                    for incoming mail</label>
+            </div>
+        </div>
+        <div class="row">
+            <div class="col"><label><input type="checkbox" name="enforce_tls_out" value="1" checked>Enforce encryption
+                    for outgoing mail</label>
+            </div>
+        </div>
+        <div class="row">
+            <div class="col">
+                <button name="action" value="save_new_mailbox" type="submit">Add mailbox</button>
+            </div>
+        </div>
+    </form>
+	<?php
+}
+
+function send_edit_mailbox(): void
+{
+	$db = get_db_instance();
+	$stmt = $db->prepare( 'SELECT a.goto, m.active, m.enforce_tls_in, m.enforce_tls_out FROM alias AS a INNER JOIN mailbox AS m ON (m.username=a.address) WHERE m.username = ?;' );
+	$stmt->execute( [ $_REQUEST[ 'user' ] ] );
+	if ( $email = $stmt->fetch( PDO::FETCH_ASSOC ) ) {
+		$aliases = explode( ',', $email[ 'goto' ] );
+		$aliases_to = implode( "\n", array_diff( $aliases, [ $_POST[ 'user' ] ] ) );
+		?>
+        <h2>Edit mailbox <?php echo htmlspecialchars( $_POST[ 'user' ] ); ?></h2>
+        <form class="form_limit" action="admin.php" method="post">
+            <input type="hidden" name="csrf_token" value="<?php echo $_SESSION[ 'csrf_token' ]; ?>">
+            <input type="hidden" name="user" value="<?php echo htmlspecialchars( $_POST[ 'user' ] ); ?>">
+            <div class="row">
+                <div class="col"><label for="alias_to">Forward to</label></div>
+                <div class="col"><textarea name="alias_to"
+                                           id="alias_to"><?php echo htmlspecialchars( $aliases_to ); ?></textarea></div>
+            </div>
+            <div class="row">
+                <div class="col"><label for="alias_keep_copy">Keep a local copy</label></div>
+                <div class="col"><input type="checkbox" name="alias_keep_copy"
+                                        id="alias_keep_copy"<?php echo in_array( $_POST[ 'user' ], $aliases, true ) ? ' checked' : ''; ?>>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col"><label><input type="checkbox" name="active"
+                                               value="1"<?php echo $email[ 'active' ] === 1 ? ' checked' : ''; ?>>Active</label>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col"><label><input type="checkbox" name="enforce_tls_in"
+                                               value="1"<?php echo $email[ 'enforce_tls_in' ] === 1 ? ' checked' : ''; ?>>Enforce
+                        encryption for incoming mail</label>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col"><label><input type="checkbox" name="enforce_tls_out"
+                                               value="1"<?php echo $email[ 'enforce_tls_out' ] === 1 ? ' checked' : ''; ?>>Enforce
+                        encryption for outgoing mail</label>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col">
+                    <button name="action" value="save_edit_mailbox" type="submit">Save mailbox</button>
+                </div>
+            </div>
+        </form>
+        <h2>Change password</h2>
+        <form class="form_limit" action="admin.php" method="post">
+            <input type="hidden" name="csrf_token" value="<?php echo $_SESSION[ 'csrf_token' ]; ?>">
+            <input type="hidden" name="user" value="<?php echo htmlspecialchars( $_POST[ 'user' ] ); ?>">
+            <div class="row">
+                <div class="col"><label for="pass_update">Password</label></div>
+                <div class="col"><input type="password" name="pass_update" id="pass_update" autocomplete="new-password"
+                                        required></div>
+            </div>
+            <div class="row">
+                <div class="col"><label for="pass_update2">Password again</label></div>
+                <div class="col"><input type="password" name="pass_update2" id="pass_update2"
+                                        autocomplete="new-password" required></div>
+            </div>
+            <div class="row">
+                <div class="col">
+                    <button name="action" value="save_password_mailbox" type="submit">Change password</button>
+                </div>
+            </div>
+        </form>
+        <h2>Delete mailbox / Disable two factor authentication</h2>
+        <form class="form_limit" action="admin.php" method="post">
+            <input type="hidden" name="csrf_token" value="<?php echo $_SESSION[ 'csrf_token' ]; ?>">
+            <input type="hidden" name="user" value="<?php echo htmlspecialchars( $_POST[ 'user' ] ); ?>">
+            <div class="row">
+                <div class="col">
+                    <button type="submit" name="action" value="disable_tfa_mailbox">Disable two factor authentication
+                    </button>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col">
+                    <button type="submit" name="action" value="delete_mailbox">Delete mailbox</button>
+                </div>
+            </div>
+        </form>
+		<?php
+	} else {
+		echo '<p>Oops, this mailbox doesn\'t seem to exist.</p>';
+	}
+}
diff --git a/www/index.php b/www/index.php
new file mode 100644
index 0000000..09972b9
--- /dev/null
+++ b/www/index.php
@@ -0,0 +1,27 @@
+<!DOCTYPE html><html lang="en-gb"><head>
+<title>Daniel - E-Mail and XMPP</title>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+<meta name="author" content="Daniel Winzen">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<meta name="description" content="Get a free and anonymous E-Mail address and an XMPP/Jabber account">
+<link rel="canonical" href="https://danwin1210.de/mail/">
+</head><body>
+<main>
+<p>Info | <a href="/mail/register.php">Register</a> | <a href="/mail/squirrelmail/src/login.php" target="_blank">Webmail-Login</a> | <a href="/mail/manage_account.php">Manage account</a></p>
+<h2>What you will get</h2>
+<p>You get a free anonymous E-Mail address and an XMPP/Jabber account using the same details. Your Jabber ID is user@danwin1210.de and can be connected to directly from clearnet or via TOR hidden service (danielas3rtn54uwmofdo3x2bsdifr47huasnmbgqzfrec5ubupvtpid.onion).</p>
+<p>You will have 50MB of disk space available for your mails. If you desperately need more space, just <a href="/contact.php">contact me</a>. Your E-Mail address will be user@danwin1210.de</p>
+<p>For privacy, please use PGP mail encryption, if you can. This prevents others from reading your mails (including me and/or LEA). GnuPGs official home: <a href="https://gnupg.org">https://gnupg.org</a> Windows GUI: <a href="https://gpg4usb.org">https://gpg4usb.org</a></p>
+<h2>E-Mail Setup</h2>
+<p>SMTP: danwin1210.de Port 465 (SSL/TLS) or 587 (StartTLS)<br>
+IMAP: danwin1210.de Port 993 (SSL/TLS) or 143 (StartTLS)<br>
+POP3: danwin1210.de Port 995 (SSL/TLS) or 110 (StartTLS)<br>
+Authentication: PLAIN, LOGIN</p>
+<p>You can also connect on the same ports via the tor onion address danielas3rtn54uwmofdo3x2bsdifr47huasnmbgqzfrec5ubupvtpid.onion, but you will have to accept an SSL certificate only valid for the clearnet domain.</p>
+<h2>XMPP setup</h2>
+<p>Domain: danwin1210.de<br>
+Connect server: danielas3rtn54uwmofdo3x2bsdifr47huasnmbgqzfrec5ubupvtpid.onion (optional for torification)<br>
+File transfer proxy: proxy.danwin1210.de<br>
+BOSH URL: https://danwin1210.de:5281/http-bind (only enable if you have to, as it is slower than directly using xmpp)</p>
+</main>
+</body></html>
diff --git a/www/manage_account.php b/www/manage_account.php
new file mode 100644
index 0000000..60e8170
--- /dev/null
+++ b/www/manage_account.php
@@ -0,0 +1,415 @@
+<?php
+require_once( '../common_config.php' );
+
+session_start();
+if ( empty( $_SESSION[ 'csrf_token' ] ) ) {
+	$_SESSION[ 'csrf_token' ] = sha1( uniqid() );
+}
+$msg = '';
+if ( ! empty( $_SESSION[ 'email_user' ] ) ) {
+	$db = get_db_instance();
+	$stmt = $db->prepare( 'SELECT null FROM mailbox WHERE username=? AND active = 1;' );
+	$stmt->execute( [ $_SESSION[ 'email_user' ] ] );
+	if ( ! $user = $stmt->fetch( PDO::FETCH_ASSOC ) ) {
+		$_SESSION = [];
+		session_regenerate_id( true );
+		$_SESSION[ 'csrf_token' ] = sha1( uniqid() );
+		$msg .= '<div class="red" role="alert">It looks like your user no longer exists!</div>';
+	}
+}
+
+if ( $_SERVER[ 'REQUEST_METHOD' ] === 'POST' ) {
+	if ( $_SESSION[ 'csrf_token' ] !== $_POST[ 'csrf_token' ] ?? '' ) {
+		die( 'Invalid csfr token' );
+	}
+	if ( isset( $_SESSION[ '2fa_code' ] ) ) {
+		if ( ! empty( $_POST[ '2fa_code' ] ) && $_POST[ '2fa_code' ] === $_SESSION[ '2fa_code' ] ) {
+			unset( $_SESSION[ '2fa_code' ] );
+			unset( $_SESSION[ 'pgp_key' ] );
+		} else {
+			$msg .= '<p style="color:red">Wrong 2FA code</p>';
+		}
+	}
+	if ( ! isset( $_SESSION[ '2fa_code' ] ) && isset( $_POST[ 'action' ] ) ) {
+		if ( $_POST[ 'action' ] === 'logout' ) {
+			$_SESSION = [];
+			session_regenerate_id( true );
+			$_SESSION[ 'csrf_token' ] = sha1( uniqid() );
+			$msg .= '<div class="green" role="alert">Successfully logged out</div>';
+		} elseif ( $_POST[ 'action' ] === 'login' ) {
+			$ok = true;
+			if ( ! check_captcha( $_POST[ 'challenge' ] ?? '', $_POST[ 'captcha' ] ?? '' ) ) {
+				$ok = false;
+				$msg .= '<div class="red" role="alert">Invalid captcha</div>';
+			}
+			if ( empty( $_POST[ 'user' ] ) || ! preg_match( '/^([^+]+?)(@([^@]+))?$/i', $_POST[ 'user' ], $match ) ) {
+				$ok = false;
+				$msg .= '<div class="red" role="alert">Invalid username.</div>';
+			}
+			if ( $ok ) {
+				$db = get_db_instance();
+				$user = $match[ 1 ];
+				$domain = $match[ 3 ] ?? 'danwin1210.de';
+				$stmt = $db->prepare( 'SELECT target_domain FROM alias_domain WHERE alias_domain = ? AND active=1;' );
+				$stmt->execute( [ $domain ] );
+				if ( $tmp = $stmt->fetch( PDO::FETCH_ASSOC ) ) {
+					$domain = $tmp[ 'target_domain' ];
+				}
+				$stmt = $db->prepare( 'SELECT username, password, password_hash_type, tfa, pgp_key FROM mailbox WHERE username = ? AND active = 1;' );
+				$stmt->execute( [ "$user@$domain" ] );
+				if ( $tmp = $stmt->fetch( PDO::FETCH_ASSOC ) ) {
+					if ( empty( $_POST[ 'pwd' ] ) || ! password_verify( $_POST[ 'pwd' ], $tmp[ 'password' ] ) ) {
+						$ok = false;
+						$msg .= '<div class="red" role="alert">Incorrect username or password</div>';
+					} else {
+						$_SESSION[ 'email_user' ] = $tmp[ 'username' ];
+						$stmt = $db->prepare( 'UPDATE mailbox SET last_login = ? WHERE username = ? AND active = 1;' );
+						$stmt->execute( [ time(), $_SESSION[ 'email_user' ] ] );
+						// update password hash if it's using an old hashing algorithm
+						if ( $tmp[ 'password_hash_type' ] !== '{ARGON2ID}' ) {
+							$hash = password_hash( $_POST[ 'pwd' ], PASSWORD_ARGON2ID );
+							$stmt = $db->prepare( 'UPDATE mailbox SET password_hash_type = "{ARGON2ID}", password = ? WHERE username = ? AND active = 1;' );
+							$stmt->execute( [ $hash, $_SESSION[ 'email_user' ] ] );
+						}
+						if ( $tmp[ 'tfa' ] ) {
+							$code = bin2hex( random_bytes( 3 ) );
+							$_SESSION[ '2fa_code' ] = $code;
+							$_SESSION[ 'pgp_key' ] = $tmp[ 'pgp_key' ];
+						}
+					}
+				} else {
+					$msg .= '<div class="red" role="alert">Incorrect username or password</div>';
+				}
+			}
+		} elseif ( ! empty( $_SESSION[ 'email_user' ] ) && $_POST[ 'action' ] === 'update_settings' ) {
+			$alias_goto = '';
+			if ( isset( $_POST[ 'alias_keep_copy' ] ) ) {
+				$alias_goto .= $_SESSION[ 'email_user' ] . ',';
+			}
+			if ( ! empty( $_POST[ 'alias_to' ] ) ) {
+				$additional = preg_split( "/[\s,]+/", $_POST[ 'alias_to' ] );
+				$alias_goto .= validate_email_list( $additional, $msg );
+			}
+			$alias_goto = rtrim( $alias_goto, ',' );
+			$stmt = $db->prepare( 'UPDATE alias SET goto = ?, enforce_tls_in = ? WHERE address = ? AND active = 1;' );
+			$stmt->execute( [ $alias_goto, ( isset( $_POST[ 'enforce_tls_in' ] ) ? 1 : 0 ), $_SESSION[ 'email_user' ] ] );
+			$stmt = $db->prepare( 'UPDATE mailbox SET enforce_tls_in = ?, enforce_tls_out = ? WHERE username = ? AND active = 1;' );
+			$stmt->execute( [ ( isset( $_POST[ 'enforce_tls_in' ] ) ? 1 : 0 ), ( isset( $_POST[ 'enforce_tls_out' ] ) ? 1 : 0 ), $_SESSION[ 'email_user' ] ] );
+		} elseif ( ! empty( $_SESSION[ 'email_user' ] ) && $_POST[ 'action' ] === 'update_password' ) {
+			if ( empty( $_POST[ 'pass_update' ] ) || empty( $_POST[ 'pass_update2' ] ) || $_POST[ 'pass_update' ] !== $_POST[ 'pass_update2' ] ) {
+				$msg .= '<div class="red" role="alert">Passwords empty or don\'t match</div>';
+			} else {
+				$hash = password_hash( $_POST[ 'pass_update' ], PASSWORD_ARGON2ID );
+				$stmt = $db->prepare( 'UPDATE mailbox SET password_hash_type = "{ARGON2ID}", password = ? WHERE username = ? AND active = 1;' );
+				$stmt->execute( [ $hash, $_SESSION[ 'email_user' ] ] );
+				$msg .= '<div class="green" role="alert">Successfully updated password</div>';
+			}
+		} elseif ( ! empty( $_SESSION[ 'email_user' ] ) && $_POST[ 'action' ] === 'delete_account' ) {
+			$msg .= '<div class="red" role="alert">Warning: This will permenently delete your account and all your data. Anyone can immediately register with this user again. It cannot be reversed. Are you absolutely sure?</div>';
+			$msg .= '<form method="post"><input type="hidden" name="csrf_token" value="' . $_SESSION[ 'csrf_token' ] . '">';
+			$msg .= '<button type="submit" name="action" value="delete_account2">Yes, I want to permanently delete my account</button></form>';
+		} elseif ( ! empty( $_SESSION[ 'email_user' ] ) && $_POST[ 'action' ] === 'disable_account' ) {
+			$msg .= '<div class="red" role="alert">Warning: This will disable your account for a year and delete all your data. After a year it is available for registrations again. It cannot be reversed. Are you absolutely sure?</div>';
+			$msg .= '<form method="post"><input type="hidden" name="csrf_token" value="' . $_SESSION[ 'csrf_token' ] . '">';
+			$msg .= '<button type="submit" name="action" value="disable_account2">Yes, I want to disable my account</button></form>';
+		} elseif ( ! empty( $_SESSION[ 'email_user' ] ) && $_POST[ 'action' ] === 'delete_account2' ) {
+			$stmt = $db->prepare( 'DELETE FROM alias WHERE address = ?;' );
+			$stmt->execute( [ $_SESSION[ 'email_user' ] ] );
+			$stmt = $db->prepare( 'UPDATE mailbox SET active = -2 WHERE username = ? AND active = 1;' );
+			$stmt->execute( [ $_SESSION[ 'email_user' ] ] );
+			$_SESSION = [];
+			session_regenerate_id( true );
+			$_SESSION[ 'csrf_token' ] = sha1( uniqid() );
+			$msg .= '<div class="green" role="alert">Successfully deleted account</div>';
+		} elseif ( ! empty( $_SESSION[ 'email_user' ] ) && $_POST[ 'action' ] === 'disable_account2' ) {
+			$stmt = $db->prepare( 'UPDATE alias SET active = 0 WHERE address = ?;' );
+			$stmt->execute( [ $_SESSION[ 'email_user' ] ] );
+			$stmt = $db->prepare( 'UPDATE mailbox SET active = -1 WHERE username = ? AND active = 1;' );
+			$stmt->execute( [ $_SESSION[ 'email_user' ] ] );
+			$_SESSION = [];
+			session_regenerate_id( true );
+			$_SESSION[ 'csrf_token' ] = sha1( uniqid() );
+			$msg .= '<div class="green" role="alert">Successfully disabled account</div>';
+		} elseif ( isset( $_POST[ 'pgp_key' ] ) && ! empty( $_SESSION[ 'email_user' ] ) && $_POST[ 'action' ] === 'update_pgp_key' ) {
+			$pgp_key = trim( $_POST[ 'pgp_key' ] );
+			if ( empty( $pgp_key ) ) {
+				$msg .= "<p class=\"green\">Successfully removed the key</p>";
+				$stmt = $db->prepare( 'UPDATE mailbox SET pgp_key = "", tfa = 0, pgp_verified = 0 WHERE username = ?;' );
+				$stmt->execute( [ $_SESSION[ 'email_user' ] ] );
+			} else {
+				$gpg = gnupg_init();
+				gnupg_seterrormode( $gpg, GNUPG_ERROR_WARNING );
+				gnupg_setarmor( $gpg, 1 );
+				$imported_key = gnupg_import( $gpg, $pgp_key );
+				if ( ! $imported_key ) {
+					$msg .= "<p class=\"red\">There was an error importing the key</p>";
+				} else {
+					$has_this_email = false;
+					$key_info = gnupg_keyinfo( $gpg, $imported_key[ 'fingerprint' ] );
+					foreach ( $key_info as $key ) {
+						foreach ( $key[ 'uids' ] as $uid ) {
+							if ( $uid[ 'email' ] === $_SESSION[ 'email_user' ] ) {
+								$has_this_email = true;
+								break;
+							}
+						}
+					}
+					if ( $has_this_email ) {
+						$msg .= "<p class=\"green\">Successfully imported the key</p>";
+						$stmt = $db->prepare( 'UPDATE mailbox SET pgp_key = ?, tfa = 0, pgp_verified = 0 WHERE username = ?;' );
+						$stmt->execute( [ $pgp_key, $_SESSION[ 'email_user' ] ] );
+					} else {
+						$msg .= sprintf( '<p class="red">Oops, looks like the key is missing this email address as user id. Please add your address "%s" as user ID to your pgp key or create a new key pair.</p>', htmlspecialchars( $_SESSION[ 'email_user' ] ) );
+					}
+				}
+			}
+		} elseif ( isset( $_POST[ 'enable_2fa_code' ] ) && ! empty( $_SESSION[ 'email_user' ] ) && $_POST[ 'action' ] === 'enable_2fa' ) {
+			if ( $_POST[ 'enable_2fa_code' ] !== $_SESSION[ 'enable_2fa_code' ] ) {
+				$msg .= "<p class=\"red\">Sorry, the code was incorrect</p>";
+			} else {
+				$stmt = $db->prepare( 'UPDATE mailbox SET tfa = 1, pgp_verified = 1 WHERE username = ?;' );
+				$stmt->execute( [ $_SESSION[ 'email_user' ] ] );
+				$msg .= "<p class=\"green\">Successfully enabled 2FA</p>";
+			}
+		}
+	}
+}
+?>
+<!DOCTYPE html>
+<html lang="en-gb">
+<head>
+    <title>Daniel - E-Mail and XMPP - Manage account</title>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <meta name="author" content="Daniel Winzen">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <meta name="description"
+          content="Manage your free and anonymous E-Mail address and an XMPP/Jabber account. Add forwarding addresses, change your password or disable/delete your account.">
+    <link rel="canonical" href="https://danwin1210.de/mail/manage_account.php">
+</head>
+<body>
+<main>
+<?php
+if ( isset( $_SESSION[ '2fa_code' ] ) ){
+$gpg = gnupg_init();
+gnupg_seterrormode( $gpg, GNUPG_ERROR_WARNING );
+gnupg_setarmor( $gpg, 1 );
+$imported_key = gnupg_import( $gpg, $_SESSION[ 'pgp_key' ] );
+if ( $imported_key ){
+$key_info = gnupg_keyinfo( $gpg, $imported_key[ 'fingerprint' ] );
+foreach ( $key_info as $key ) {
+	if ( $key[ 'can_encrypt' ] ) {
+		foreach ( $key[ 'subkeys' ] as $subkey ) {
+			gnupg_addencryptkey( $gpg, $subkey[ 'fingerprint' ] );
+		}
+	}
+}
+$encrypted = gnupg_encrypt( $gpg, "To login, please enter the following code to confirm ownership of your key:\n\n" . $_SESSION[ '2fa_code' ] . "\n" );
+echo $msg;
+echo "<p>To login, please decrypt the following PGP encrypted message and confirm the code:</p>";
+echo "<pre>$encrypted</pre>";
+?>
+<form class="form_limit" action="manage_account.php" method="post">
+    <input type="hidden" name="csrf_token" value="<?php echo $_SESSION[ 'csrf_token' ]; ?>">
+    <div class="row">
+        <div class="col"><input type="text" name="2fa_code" aria-label="2FA code"></div>
+        <div class="col">
+            <button type="submit">Confirm</button>
+        </div>
+    </div>
+</form>
+</maim></body>
+</html>
+<?php
+exit;
+}
+}
+if ( ! empty( $_SESSION[ 'email_user' ] ) ){ ?>
+<form method="post"><input type="hidden" name="csrf_token" value="<?php echo $_SESSION[ 'csrf_token' ]; ?>">
+	<?php } ?>
+    <p><a href="/mail/">Info</a> |<?php
+		if ( ! empty( $_SESSION[ 'email_user' ] ) ) { ?>
+            Logged in as <?php echo htmlspecialchars( $_SESSION[ 'email_user' ] );
+		} else { ?>
+            <a href="/mail/register.php">Register</a>
+		<?php } ?> | <a href="/mail/squirrelmail/src/login.php" target="_blank">Webmail-Login</a> <?php
+		if ( ! empty( $_SESSION[ 'email_user' ] ) ) { ?>
+            |
+            <button name="action" value="logout" type="submit">Logout</button>
+		<?php } else { ?>
+            | Manage account<?php
+		} ?></p>
+	<?php if ( ! empty( $_SESSION[ 'email_user' ] ) ){ ?></form><?php }
+echo "<p>$msg</p>";
+if ( empty( $_SESSION[ 'email_user' ] ) ) { ?>
+    <form class="form_limit" action="manage_account.php" method="post">
+        <input type="hidden" name="csrf_token" value="<?php echo $_SESSION[ 'csrf_token' ]; ?>">
+        <div class="row">
+            <div class="col"><label for="user">Username</label></div>
+            <div class="col"><input type="text" name="user" id="user" autocomplete="username" required
+                                    value="<?php echo htmlspecialchars( $_POST[ 'user' ] ?? '' ); ?>"></div>
+        </div>
+        <div class="row">
+            <div class="col"><label for="pwd">Password</label></div>
+            <div class="col"><input type="password" name="pwd" id="pwd" autocomplete="new-password" required></div>
+        </div>
+		<?php send_captcha(); ?>
+        <div class="row">
+            <div class="col">
+                <button name="action" value="login" type="submit">Login</button>
+            </div>
+        </div>
+    </form>
+<?php } else {
+	$aliases = [];
+	$stmt = $db->prepare( 'SELECT goto FROM alias WHERE address = ?;' );
+	$stmt->execute( [ $_SESSION[ 'email_user' ] ] );
+	if ( $tmp = $stmt->fetch( PDO::FETCH_ASSOC ) ) {
+		$aliases = explode( ',', $tmp[ 'goto' ] );
+	}
+	$aliases_to = implode( "\n", array_diff( $aliases, [ $_SESSION[ 'email_user' ] ] ) );
+	$stmt = $db->prepare( 'SELECT enforce_tls_in, enforce_tls_out FROM mailbox WHERE username = ?;' );
+	$stmt->execute( [ $_SESSION[ 'email_user' ] ] );
+	$tls_status = $stmt->fetch( PDO::FETCH_ASSOC );
+	?>
+    <form class="form_limit" action="manage_account.php" method="post">
+        <h2>Settings</h2>
+        <h3>Delivery</h3>
+        <p>Edit how your mail is delivered. You can add forwarding addresses one per line, or comma seperated. When you
+            disable the "keep a local copy" checkbox, your mail will only be sent to your forwarding addresses.</p>
+        <input type="hidden" name="csrf_token" value="<?php echo $_SESSION[ 'csrf_token' ]; ?>">
+        <div class="row">
+            <div class="col"><label for="alias_to">Forward to</label></div>
+            <div class="col"><textarea name="alias_to"
+                                       id="alias_to"><?php echo htmlspecialchars( $aliases_to ); ?></textarea></div>
+        </div>
+        <div class="row">
+            <div class="col"><label for="alias_keep_copy">Keep a local copy</label></div>
+            <div class="col"><input type="checkbox" name="alias_keep_copy"
+                                    id="alias_keep_copy"<?php echo in_array( $_SESSION[ 'email_user' ], $aliases, true ) ? ' checked' : ''; ?>>
+            </div>
+        </div>
+        <h3>Encryption</h3>
+        <p>If you are having issues sending or receiving mails with some other provider, you can try disabling forced
+            encryption here. But be aware, that this makes it possible for 3rd parties on the network to read your
+            emails. Make sure to ask your correspondent to demand encryption support from their provider for a safer
+            internet.</p>
+        <div class="row">
+            <div class="col"><label for="enforce_tls_in">Enforce encryption for incoming mail</label></div>
+            <div class="col"><input type="checkbox" name="enforce_tls_in"
+                                    id="enforce_tls_in"<?php echo ! empty( $tls_status[ 'enforce_tls_in' ] ) ? ' checked' : ''; ?>>
+            </div>
+        </div>
+        <div class="row">
+            <div class="col"><label for="enforce_tls_out">Enforce encryption for outgoing mail</label></div>
+            <div class="col"><input type="checkbox" name="enforce_tls_out"
+                                    id="enforce_tls_out"<?php echo ! empty( $tls_status[ 'enforce_tls_out' ] ) ? ' checked' : ''; ?>>
+            </div>
+        </div>
+        <div class="row">
+            <div class="col">
+                <button name="action" value="update_settings" type="submit">Update settings</button>
+            </div>
+        </div>
+    </form>
+
+    <h2>Change password</h2>
+    <form class="form_limit" action="manage_account.php" method="post">
+        <input type="hidden" name="csrf_token" value="<?php echo $_SESSION[ 'csrf_token' ]; ?>">
+        <div class="row">
+            <div class="col"><label for="pass_update">Password</label></div>
+            <div class="col"><input type="password" name="pass_update" id="pass_update" autocomplete="new-password"
+                                    required></div>
+        </div>
+        <div class="row">
+            <div class="col"><label for="pass_update2">Password again</label></div>
+            <div class="col"><input type="password" name="pass_update2" id="pass_update2" autocomplete="new-password"
+                                    required></div>
+        </div>
+        <div class="row">
+            <div class="col">
+                <button name="action" value="update_password" type="submit">Change password</button>
+            </div>
+        </div>
+    </form>
+
+	<?php
+	$stmt = $db->prepare( 'SELECT pgp_key, tfa FROM mailbox WHERE username = ?;' );
+	$stmt->execute( [ $_SESSION[ 'email_user' ] ] );
+	$pgp_status = $stmt->fetch( PDO::FETCH_ASSOC );
+	if ( ! empty( $pgp_status[ 'pgp_key' ] ) ) {
+		if ( $pgp_status[ 'tfa' ] === 1 ) {
+			echo "<p class=\"green\">Yay, PGP based 2FA is enabled!</p>";
+		} else {
+			$gpg = gnupg_init();
+			gnupg_seterrormode( $gpg, GNUPG_ERROR_WARNING );
+			gnupg_setarmor( $gpg, 1 );
+			$imported_key = gnupg_import( $gpg, $pgp_status[ 'pgp_key' ] );
+			if ( $imported_key ) {
+				$key_info = gnupg_keyinfo( $gpg, $imported_key[ 'fingerprint' ] );
+				foreach ( $key_info as $key ) {
+					if ( ! $key[ 'can_encrypt' ] ) {
+						echo "<p>Sorry, this key can't be used to encrypt a message to you. Your key may have expired or has been revoked.</p>";
+					} else {
+						foreach ( $key[ 'subkeys' ] as $subkey ) {
+							gnupg_addencryptkey( $gpg, $subkey[ 'fingerprint' ] );
+						}
+					}
+				}
+				$_SESSION[ 'enable_2fa_code' ] = bin2hex( random_bytes( 3 ) );
+				if ( $encrypted = gnupg_encrypt( $gpg, "To enable 2FA, please enter the following code to confirm ownership of your key:\n\n$_SESSION[enable_2fa_code]\n" ) ) {
+					echo '<h2>Enable 2FA</h2>';
+					echo "<p>To enable 2FA using your PGP key, please decrypt the following PGP encrypted message and confirm the code:</p>";
+					echo "<pre>$encrypted</pre>";
+					?>
+                    <form class="form_limit" action="manage_account.php" method="post">
+                        <input type="hidden" name="csrf_token" value="<?php echo $_SESSION[ 'csrf_token' ]; ?>">
+                        <div class="row">
+                            <div class="col"><input type="text" name="enable_2fa_code" aria-label="2FA Code"></div>
+                            <div>
+                                <button type="submit" name="action" value="enable_2fa">Confirm</button>
+                            </div>
+                        </div>
+                    </form>
+					<?php
+				}
+			}
+		}
+	}
+	?>
+
+    <h2>Add PGP key for 2FA and end-to-end encryption</h2>
+    <form class="form_limit" action="manage_account.php" method="post">
+        <input type="hidden" name="csrf_token" value="<?php echo $_SESSION[ 'csrf_token' ]; ?>">
+        <div class="row">
+            <div class="col"><textarea name="pgp_key" rows="10" cols="50"
+                                       aria-label="PGP key"><?php echo htmlspecialchars( $pgp_status[ 'pgp_key' ] ?? '' ); ?></textarea>
+            </div>
+        </div>
+        <div>
+            <div>
+                <button type="submit" name="action" value="update_pgp_key">Update PGP key</button>
+            </div>
+        </div>
+    </form>
+
+    <form class="form_limit" action="manage_account.php" method="post">
+        <h2>Disable/Delete account</h2>
+        <p>Warning, this is permanent and cannot be undone. Disabling an account will delete your email data from the
+            server, but leave the account blocked in the database for a year, so no one else can use it. Deleting your
+            account will completely wipe all records of it and it will be available for new registrations again.</p>
+        <input type="hidden" name="csrf_token" value="<?php echo $_SESSION[ 'csrf_token' ]; ?>">
+        <div class="row">
+            <div class="col">
+                <button type="submit" name="action" value="disable_account">Disable account</button>
+            </div>
+        </div>
+        <div class="row">
+            <div class="col">
+                <button type="submit" name="action" value="delete_account">Delete account</button>
+            </div>
+        </div>
+    </form>
+<?php } ?>
+</main>
+</body></html>
+
diff --git a/www/openpgpkey_wkd.php b/www/openpgpkey_wkd.php
new file mode 100644
index 0000000..5a934fb
--- /dev/null
+++ b/www/openpgpkey_wkd.php
@@ -0,0 +1,22 @@
+<?php
+require_once '../common_config.php';
+header( 'Access-Control-Allow-Origin: *' );
+$db = get_db_instance();
+$stmt = $db->prepare( 'SELECT pgp_key FROM mailbox WHERE openpgpkey_wkd = ? AND domain = ?;' );
+$stmt->execute( [ explode( '?', basename( $_SERVER[ 'REQUEST_URI' ] ) )[ 0 ], $_GET[ 'domain' ] ?? $_SERVER[ 'HTTP_HOST' ] ] );
+$res = $stmt->fetch( PDO::FETCH_ASSOC );
+if ( ! empty( $res[ 'pgp_key' ] ) ) {
+	$gpg = gnupg_init();
+	gnupg_seterrormode( $gpg, GNUPG_ERROR_WARNING );
+	gnupg_setarmor( $gpg, 0 );
+	$imported_key = gnupg_import( $gpg, $res[ 'pgp_key' ] );
+	if ( ! $imported_key ) {
+		http_response_code( 500 );
+	} else {
+		http_response_code( 200 );
+		header( 'Content-Type: application/octet-stream' );
+		echo gnupg_export( $gpg, $imported_key[ 'fingerprint' ] );
+	}
+} else {
+	http_response_code( 404 );
+}
diff --git a/www/register.php b/www/register.php
new file mode 100644
index 0000000..1d33419
--- /dev/null
+++ b/www/register.php
@@ -0,0 +1,117 @@
+<?php
+
+use Egulias\EmailValidator\EmailValidator;
+use Egulias\EmailValidator\Validation\NoRFCWarningsValidation;
+
+require_once( '../vendor/autoload.php' );
+require_once( '../common_config.php' );
+session_start();
+if ( empty( $_SESSION[ 'csrf_token' ] ) || $_SESSION[ 'UA' ] !== $_SERVER[ 'HTTP_USER_AGENT' ] ) {
+	$_SESSION[ 'csrf_token' ] = sha1( uniqid() );
+	$_SESSION[ 'UA' ] = $_SERVER[ 'HTTP_USER_AGENT' ];
+}
+$msg = '';
+if ( isset( $_POST[ 'user' ] ) ) {
+	$ok = true;
+	if ( $_SESSION[ 'csrf_token' ] !== $_POST[ 'csrf_token' ] ?? '' ) {
+		$ok = false;
+		$msg .= '<div class="red" role="alert">Invalid csfr token</div>';
+	}
+	if ( ! check_captcha( $_POST[ 'challenge' ] ?? '', $_POST[ 'captcha' ] ?? '' ) ) {
+		$ok = false;
+		$msg .= '<div class="red" role="alert">Invalid captcha</div>';
+	}
+	$db = get_db_instance();
+	if ( ! preg_match( '/^([^+\/\'"]+?)(@([^@]+))?$/iu', $_POST[ 'user' ], $match ) ) {
+		$ok = false;
+		$msg .= '<div class="red" role="alert">Invalid username. It may not contain a +, \', " or /.</div>';
+	}
+	$user = mb_strtolower( $match[ 1 ] ?? '' );
+	$domain = $match[ 3 ] ?? 'danwin1210.de';
+	if ( $ok && ( empty( $_POST[ 'pwd' ] ) || empty( $_POST[ 'pwd2' ] ) || $_POST[ 'pwd' ] !== $_POST[ 'pwd2' ] ) ) {
+		$ok = false;
+		$msg .= '<div class="red" role="alert">Passwords empty or don\'t match</div>';
+	} elseif ( $ok ) {
+		$stmt = $db->prepare( 'SELECT target_domain FROM alias_domain WHERE alias_domain = ? AND active=1;' );
+		$stmt->execute( [ $domain ] );
+		if ( $tmp = $stmt->fetch( PDO::FETCH_ASSOC ) ) {
+			$domain = $tmp[ 'target_domain' ];
+		}
+		$stmt = $db->prepare( 'SELECT null FROM domain WHERE domain = ? AND active = 1;' );
+		$stmt->execute( [ $domain ] );
+		if ( ! $stmt->fetch() ) {
+			$ok = false;
+			$msg .= '<div class="red" role="alert">The domain you specified is not allowed</div>';
+		} else {
+			$validator = new EmailValidator();
+			if ( ! $validator->isValid( "$user@$domain", new NoRFCWarningsValidation() ) ) {
+				$ok = false;
+				$msg .= '<div class="red" role="alert">The email address you specified is not valid</div>';
+			}
+		}
+	}
+	if ( $ok ) {
+		$stmt = $db->prepare( 'SELECT null FROM mailbox WHERE username = ? UNION SELECT null FROM alias WHERE address = ?;' );
+		$stmt->execute( [ "$user@$domain", "$user@$domain" ] );
+		if ( $stmt->fetch() ) {
+			$ok = false;
+			$msg .= '<div class="red" role="alert">Sorry, this user already exists</div>';
+		}
+		if ( $ok ) {
+			$hash = password_hash( $_POST[ 'pwd' ], PASSWORD_ARGON2ID );
+			$stmt = $db->prepare( 'INSERT INTO alias (address, goto, domain, created, modified) VALUES (?, ?, ?, NOW(), NOW());' );
+			$stmt->execute( [ "$user@$domain", "$user@$domain", $domain ] );
+			$stmt = $db->prepare( 'INSERT INTO mailbox (username, password, quota, local_part, domain, created, modified, password_hash_type, openpgpkey_wkd) VALUES(?, ?, 51200000, ?, ?, NOW(), NOW(), ?, ?);' );
+			$stmt->execute( [ "$user@$domain", $hash, $user, $domain, '{ARGON2ID}', z_base32_encode( hash( 'sha1', mb_strtolower( $user ), true ) ) ] );
+			$msg .= '<div class="green" role="alert">Successfully created new mailbox!</div>';
+		}
+	}
+}
+?>
+<!DOCTYPE html>
+<html lang="en-gb">
+<head>
+    <title>Daniel - E-Mail and XMPP - Register</title>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <meta name="author" content="Daniel Winzen">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <meta name="description" content="Register for a free and anonymous E-Mail address and an XMPP/Jabber account">
+    <link rel="canonical" href="https://danwin1210.de/mail/register.php">
+</head>
+<body>
+<main>
+<p><a href="/mail/">Info</a> | Register | <a href="/mail/squirrelmail/src/login.php" target="_blank">Webmail-Login</a> |
+    <a href="/mail/manage_account.php">Manage account</a></p>
+<?php echo "<p>$msg</p>"; ?>
+<form class="form_limit" action="register.php" method="post"><input type="hidden" name="csrf_token"
+                                                                    value="<?php echo $_SESSION[ 'csrf_token' ] ?>">
+    <div class="row">
+        <div class="col"><label for="user">Username</label></div>
+        <div class="col"><input type="text" name="user" id="user" autocomplete="username" required
+                                value="<?php echo htmlspecialchars( $_POST[ 'user' ] ?? '' ); ?>"></div>
+    </div>
+    <div class="row">
+        <div class="col"><label for="pwd">Password</label></div>
+        <div class="col"><input type="password" name="pwd" id="pwd" autocomplete="new-password" required></div>
+    </div>
+    <div class="row">
+        <div class="col"><label for="pwd2">Password again</label></div>
+        <div class="col"><input type="password" name="pwd2" id="pwd2" autocomplete="new-password" required></div>
+    </div>
+    <div class="row">
+        <div class="col"><label for="accept_privacy">I have read and agreed to the <a href="/privacy.php"
+                                                                                      target="_blank">Privacy Policy</a></label>
+        </div>
+        <div class="col"><input type="checkbox" id="accept_privacy" name="accept_privacy" required></div>
+    </div>
+	<?php send_captcha(); ?>
+    <div class="row">
+        <div class="col">
+            <button type="submit">Register</button>
+        </div>
+    </div>
+</form>
+</main>
+</body>
+</html>
+