diff options
42 files changed, 4949 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..476be4a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +log/* +MYMETA.* +msg*.json
\ No newline at end of file @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <https://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. + + JWebmail - A Webmail Solution + Copyright (C) 2020 Jannis M. Hoffmann <jannis@fehcom.de> + + 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 <https://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: + + JWebmail Copyright (C) 2020 Jannis M. Hoffmann <jannis@fehcom.de> + 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 +<https://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 +<https://www.gnu.org/licenses/why-not-lgpl.html>. diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..2a2f56f --- /dev/null +++ b/MANIFEST @@ -0,0 +1,36 @@ +jwebmail.conf +lib/JWebmail.pm +lib/JWebmail/Controller/All.pm +lib/JWebmail/Model/ReadMails.pm +lib/JWebmail/Model/Driver/Mock.pm +lib/JWebmail/Model/Driver/QMailAuthuser/Extract.pm +lib/JWebmail/Model/Driver/QMailAuthuser.pm +lib/JWebmail/Model/WriteMails.pm +lib/JWebmail/Plugin/Helper.pm +lib/JWebmail/Plugin/INIConfig.pm +lib/JWebmail/Plugin/I18N.pm +lib/JWebmail/Plugin/ServerSideSessionData.pm +README +LICENSE +lang/de.lang +lang/en.lang +Makefile.PL +t/All.t +script/jwebmail +CREDITS +MANIFEST +public/style.css +templates/headers/_display_top_nav.html.ep +templates/headers/_display_bot_nav.html.ep +templates/headers/_displayfolders.html.ep +templates/headers/_display_headers.html.ep +templates/_pagination1.html.ep +templates/not_found_.html.ep +templates/error.html.ep +templates/layouts/mainlayout.html.ep +templates/all/readmail.html.ep +templates/all/writemail.html.ep +templates/all/displayheaders.html.ep +templates/all/about.html.ep +templates/all/noaction.html.ep +templates/_pagination2.html.ep diff --git a/Makefile.PL b/Makefile.PL new file mode 100644 index 0000000..ea55cd8 --- /dev/null +++ b/Makefile.PL @@ -0,0 +1,20 @@ +use strict; +use warnings; + +use ExtUtils::MakeMaker; + +WriteMakefile( + AUTHOR => '"Jannis M. Hoffmann" <jannis@fehcom.de>', + MIN_PERL_VERSION => 'v5.18', + NAME => 'JWebmail', + VERSION_FROM => 'lib/JWebmail.pm', + LICENSE => 'GPL', + PREREQ_PM => { + 'Mojolicious' => '8.57', + 'File::Type' => 0, + 'Email::MIME' => 0, + 'Config::Tiny' => 0, + 'Mail::Box::Manager' => 0, + }, + test => {TESTS => 't/*.t'} +);
\ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b1cb02a --- /dev/null +++ b/README.md @@ -0,0 +1,253 @@ +JWebmail +======== + +This is a rewrite of oMail by Oliver Müller <om@omnis.ch>. + +oMail has not seen much progress in the last two decades +so my <jannis@fehcom.de> goal is to bring it up to date. + +## This includes: +- Using a perl web framework and leave the deprecated CGI behind. + You can still use it in a cgi setup if you like but you now + have the option of plack/psgi and fcgi as well as the + build in server hypnotoad. +- Set up a MVC architecture instead of spaghetti. +- Improve security by only running a small part of the + model with elevated privileges. +- Make sure it works well with sqmail and its authuser + authenticator and maildir but also permit other setups + (currently not supported but adding more should be easy). + Maybe I even add a POP or IMAP based backends instead + of reading them from disk. + +## License +JWebmail is available under the GNU General Public License +version 3 or later. + +## JWebmail-webmail - INSTALL +You still need to install sqmail and setup +an external web server. + +## Future feature list +- [ ] address book support + +### Read +- [ ] bounce +- [ ] add links on email addresses in header : click = add into addressbook + + +v1.0.0 release plan +------------------- +* consider renaming, relicensing + ✓ License + ✓ GPLv3+ and enter copyright info + * Maybe the translation/documentation can be made available under a different + * may relicense this under the AGPL. + * Rename + * jwebmail +* make github ready + * create base configuration + * remove sensitive files + * add git vcs + ✓ remove part of the english translation +* check legal requirements +* INV: wrong subject being shown +✓ BUG: home not displaying +✓ BUG: empty folder not displaying correctly +✓ better documentation + ✓ document i18n snippets + ✓ cleanup comments + ✓ list functionality for ReadMails#communicate + ✓ OMail + ✓ OMail::Helper + ✓ OMail::Controller::All + ✓ OMail::Plugin::I18N + ✓ OMail::Plugin::INIConfig + ✓ OMail::Plugin::ServerSideSessionData + ✓ OMail::Model::WriteMails + ✓ OMail::Model::ReadMails + ✓ OMail::Model::Driver::QMailAuthuser + ✓ OMail::Model::Driver::QMailAuthuser::Extract +✓ better pagination + ✓ BUG: pagination forward -> backward is shifting by 1 (page start needs to be decremented) + ✓ move out to helper + ✓ more generic names +✓ advance ini config plugin + ✓ set global section to global scope + ✓ introduce arrays + ✓ make nesting sections more explicit +✓ write more tests + ✓ test pagination + ✓ test mail_line + ✓ test for ini parser + ✓ basic test for application +✓ improve i18n + ✓ german translation + ✓ look into i18n configuration + ✓ remove TXT alias +✓ more configuration (for model) + ✓ disable cram + ✓ select mock read model + ✓ lazy init for mock model + ✓ add switch disabling message send + ✓ Extract: user to switch to + ✓ Extract: adjustable maildir directory + * separate development and production configuration +✓ read secret from config file +✓ Extract: configurable perl lib +✓ Extract: encoding issues +✓ improve session data security + ✓ use a server side cookie implementation + ✓ use a one time pad + ✓ resolve server/client session duration issues + ✓ use cryptographically secure random data + ✓ hide password length +✓ handle empty folders +✓ logging support for Extract.pm +✓ true perl 5.16 support +✓ cpan build and deploy script +✓ remove prefs +✓ file upload for attachment + ✓ file type detection + ✓ move WriteMails from Email::Simple to Email::MIME +✓ configuration as plugin (Mojo::Plugin::Config) +✓ model as helpers, initialized in startup +✓ send + ✓ multiple mails for cc etc. + * content-transfer encoding, research (currently 8bit) +✓ better design for send and read + ✓ send + ✓ read +✓ sandbox html mails +✓ i18n as ini files +✓ rework mail folders +✓ rewrite about +✓ search in subject + + +v1.1.0 release plan +------------------- +* improve session data security + * improve server side session cleanup process coordination + * add a delete session function for s3d, maybe +* improve i18n + * add localization of dates and time +* advance ini config plugin + * BUG: toplevel section cant be an array + * allow non-leaf nodes to be arrays + * allow quotes + * allow continuation over multiple lines + * warn about overrides + * add template support, maybe +* repurpose status field in displayheader +* better pagination + * merge with partial templates, maybe + + +v1.2.0 release plan +------------------- +* add config validation +* show new messages per folder +* moving mails to other folders + * creating new folders + * backend +* click on sender to answer +* mobile optimize +* download mail and attachments +* cleanup css +* allow multiple attachments +* improve performance, consider alternatives to Extract.pm + * based on Maildir::Light +* add more mime types + * jpeg + * png + * giv +* consider using more mojo functions + * base64 + * encoding + * json + * filepaths + * dump + * Mojolicious::Types + * mail? +* consider using Crypt::URandom instead of Crypt::Random +* improve session data security +* add mails to Sent folder + + +v1.3.0 release plan +------------------- +* smtp send model, maybe +* pop read model, maybe +* add icons for navigation + + +Posts +----- +* Complain about IPC::Open2 ignoring 'open' pragma +* Complain about undef references causing errors, and non-fine granular switch no strict 'refs' +* Thank for perldoc.org + + +I18N patch url_for +------------------ +I have taken the monkey patching approach that was taken by Mojolicious::Plugin::I18N. +I used `can` to get the old method for looser coupling and used the Mojo::Util::monkey_patch +instead of manually doing it. This is probably overkill. + +I'm desperately looking for a different approach. Yeah the monkey patching works great, +but it violates the open-closed principal. But I cannot find an appropriate alternative. +Extending the controller and overriding url_for does not work cleanly as the user directly +inherits from Mojolicious::Controller. +Also going the 'better' approach of solving at the root by using an extension of +Mojolicious::Routes::Match->path_for and supplying it to the controller by setting +match does not work as it is on the one hand extremely difficult to inject it in +the first place and the attribute is overwritten in the dispatching process anyways. +One issue when taking the Match approach is that it needs knowledge of the stash +values which can cause cyclic references. + +I thought of three approaches injecting the modified Match instance into the class: +1. Extending the Mojolicious::Controller and overriding the new method. + This has the issue that inheritance is static but one can use Roles that + are dynamically consumed. +2. Overriding build_controller in Mojolicious. To make this cleanly it needs to be monkey patched + by the plugin which is exactly what we want to avoid. :( +3. The matcher can be set in a hook relatively early in its lifetime + and hooks compose well. + +A completely different option is to use the router directly and register a global +route that has the language as parameter. But omitting the language leads to problems. + +One can use a redirect on root. Very easy but also not very effective. + + +Concepts +-------- +- Router +- Configuration +- Middleware (auth) +- Controller/Handler +- Templates +- Template helpers +- i18n (url rewriting) +- Sessions (server side) +- Flash, maybe +- Pagination +- Validation +- Logging +- Debug printing +- Development server +- MIME handling + + +Dependencies +------------ +- M & V + - Mojolicious + - Config::Tiny + - Digest::HMAC_MD5 (optional) + - File::Type + - Crypt::Random +- C + - Mail::Box::Manager + - Email::MIME
\ No newline at end of file diff --git a/lang/CVS/Entries b/lang/CVS/Entries new file mode 100644 index 0000000..f0de199 --- /dev/null +++ b/lang/CVS/Entries @@ -0,0 +1,6 @@ +/de.lang/1.4/Sat Mar 18 21:13:25 2000// +/en.lang/1.4/Sat Mar 18 21:13:25 2000// +/fr.lang/1.4/Sat Mar 18 21:13:25 2000// +/it.lang/1.4/Sat Mar 18 21:13:25 2000// +/template.lang/1.5/Sat Mar 18 21:13:25 2000// +D diff --git a/lang/CVS/Repository b/lang/CVS/Repository new file mode 100644 index 0000000..958a813 --- /dev/null +++ b/lang/CVS/Repository @@ -0,0 +1 @@ +webmail/lang diff --git a/lang/CVS/Root b/lang/CVS/Root new file mode 100644 index 0000000..d1faa17 --- /dev/null +++ b/lang/CVS/Root @@ -0,0 +1 @@ +swix@cvs.oMail.sourceforge.net:/cvsroot/oMail diff --git a/lang/de.lang b/lang/de.lang new file mode 100644 index 0000000..88ac8b4 --- /dev/null +++ b/lang/de.lang @@ -0,0 +1,51 @@ +login = anmelden +userid = nuzerkennung +passwd = passwort +failed = fehlgeschlagen +about = über +mbox_size = Mailboxgröße +and = und +subject = betreff +version = version +from = von +to = für +cc = CC +date = datum +size = größe +content-type = inhaltsart +send_to = senden an +answer_to = Antworten gehen an +content = inhalt +check_all = alle markieren +move = verschieben +nr = nummer +status = status +logout = abmelden +compose = schreiben +search = suche +of = von +messages = nachrichten +new = neu +mbox_size = mailboxgröße +home = Übersicht +no = nein +yes = ja +page = seite +next = nächste +last = letzte +first = erste +previous = vorherige +sender = gesendet von + +# Mailboxen +Queue = Warteschlange +Drafts = Vorlagen +Home = Wurzelverzeichnis + +# Fehler +no_session = Keine aktive Sitzung. +no_folder = Dieses Verzeichnis gibt es nicht. +error_send = Die Nachricht konnte nicht gesendet werden. +succ_send = Die Nachricht wurde verschikt. +succ_move = Nachrichten wurden verschoben. +empty_folder = Dies Verzeichnis ist leer.
\ No newline at end of file diff --git a/lang/en.lang b/lang/en.lang new file mode 100644 index 0000000..aef8649 --- /dev/null +++ b/lang/en.lang @@ -0,0 +1,51 @@ +messages = messages +from = from +to = to +cc = cc + +failed = failed +search = search +move = move +and = and +content-type = content-type +send_to = send to +answer_to = answer to +content = content +check_all = check all +nr = number +mbox_size = Mailbox size +about = about +version = version +first = first +previous = previous +next = next +last = last +yes = yes +no = no +userid = user-id +passwd = password +login = login +of = of +new = new +compose = compose +logout = logout +page = page +status = status +date = date +sender = sender +subject = subject +size = size +home = home + +no_session = You have no active session! +no_folder = "no such folder" +error_send = 'Error when sending message' +succ_send = 'Message has been send' +succ_move = 'Messages have been moved' +empty_folder = This folder is empty. + +INBOX = inbox +SENT = sent +TRASH = trash +SAVED = saved +Home = home
\ No newline at end of file diff --git a/lang/i18n.pod b/lang/i18n.pod new file mode 100644 index 0000000..8f7035b --- /dev/null +++ b/lang/i18n.pod @@ -0,0 +1,87 @@ +=encoding utf-8 + +=head1 SYNOPSIS + + @@ de.lang + yes = ja + no = nein + +=head1 DESCRIPTION + +Place your translation files here. +Use the two letter language naming convention. +Use lower case phrases as they will be capitalized by the templates. + +=head1 INTERNATIONALIZATION AND LOCALIZATION + +=head2 Single Words + + Phrase Description + -------------------------- + failed an operation failed + and + no + yes + messages mails + of page x *of* n + page a page consisting of multiple mails + login header name + userid form field label + passwd form field label + about about page name + mbox_size mail box size in byte + version version translation for JWebmail version + subject mail field subject + from mail field from + to mail field to + cc mail field cc + date mail field date + size mail size + content-type mail field content-type + sender mail field sender + answer_to mail field reply back to + send_to write mail to + content the mail body rendered as html + check_all tick/untick all mails for move + move move mails to a different folder + nr row number, column description + status whether a mail is multipart + logout close session + compose write an email + search search in mails + new amount of new mails + home back button to read the main folder + first first page + previous previous page + next next page + last last page + +=head2 Phrases + + succ_send tell the user the mail was send successfully + succ_move tell the user the mails where moved successfully + empty_folder tell the user the folder is empty + +=head2 Error Messages + + no_session the session has expired or did not exist at all + no_folder the selected mail folder does not exists + error_send error sending the message + +=head2 Formats + +Currently there are no formats. + +=head2 Other + + Common Mail Folders + --- + Queue + Drafts + Home + +=head1 SEE OTHER + +L<JWebmail::Plugin::I18N> + +=cut
\ No newline at end of file diff --git a/lib/JWebmail.pm b/lib/JWebmail.pm new file mode 100644 index 0000000..7275891 --- /dev/null +++ b/lib/JWebmail.pm @@ -0,0 +1,108 @@ +package JWebmail v1.0.0; + +use Mojo::Base 'Mojolicious'; + +use JWebmail::Controller::Webmail; +use JWebmail::Model::ReadMails; +use JWebmail::Model::Driver::QMailAuthuser; +use JWebmail::Model::Driver::Mock; +use JWebmail::Model::WriteMails; + + +sub startup { + my $self = shift; + + $self->moniker('jwebmail'); + + # load plugins + push @{$self->plugins->namespaces}, 'JWebmail::Plugin'; + + $self->plugin('INIConfig'); + $self->plugin('ServerSideSessionData'); + $self->plugin('Helper'); + $self->plugin('I18N', $self->config('i18n') // {}); + + $self->secrets( [$self->config('secret')] ) if $self->config('secret'); + delete $self->config->{secret}; + + # initialize models + $self->helper(users => sub { + state $x = JWebmail::Model::ReadMails->new( + driver => $self->config->{development}{use_read_mock} + ? JWebmail::Model::Driver::Mock->new() + : JWebmail::Model::Driver::QMailAuthuser->new( + logfile => $self->home->child('log', 'extract.log'), + %{ $self->config->{model}{read}{driver} // {} }, + ) + ); + }); + $self->helper(send_mail => sub { my ($c, $mail) = @_; JWebmail::Model::WriteMails::sendmail($mail) }); + $JWebmail::Model::WriteMails::Block_Writes = 1 if $self->config->{development}{block_writes}; + + # add helper and stash values + $self->defaults(version => __PACKAGE__->VERSION); + + $self->route(); +} + + +sub route { + my $self = shift; + + my $r = shift || $self->routes; + + $r->get('/' => 'noaction')->to('Webmail#noaction'); + $r->get('/about')->to('Webmail#about'); + $r->post('/login')->to('Webmail#login'); + $r->get('/logout')->to('Webmail#logout'); + + my $a = $r->under('/')->to('Webmail#auth'); + $a->get('/home/:folder')->to('Webmail#displayheaders', folder => '')->name('displayheaders'); + $a->get('/read/#id' => 'read')->to('Webmail#readmail'); + $a->get('/write')->to('Webmail#writemail'); + $a->post('/write' => 'send')-> to('Webmail#sendmail'); + $a->post('/move')->to('Webmail#move'); + $a->get('/raw/#id')->to('Webmail#raw'); +} + + +1 + +__END__ + +=encoding utf-8 + +=head1 NAME + +JWebmail - Provides a web based e-mail client meant to be used with s/qmail. + +=head1 SYNOPSIS + + hypnotoad script/jwebmail + +And use a server in reverse proxy configuration. + +=head1 DESCRIPTION + +=head1 CONFIGURATION + +Use the jwebmail.conf file. + +=head1 AUTHORS + +Copyright (C) 2020 Jannis M. Hoffmann L<jannis@fehcom.de> + +=head1 BASED ON + +Copyright (C) 2001 Olivier Müller L<om@omnis.ch> (GPLv2+ project: oMail Webmail) + +Copyright (C) 2000 Ernie Miller (GPL project: Neomail) + +See the CREDITS file for project contributors. + +=head1 LICENSE + +This module is licensed under the terms of the GPLv3 or any later version at your option. +Please take a look at the provided LICENSE file shipped with this module. + +=cut
\ No newline at end of file diff --git a/lib/JWebmail/Controller/Webmail.pm b/lib/JWebmail/Controller/Webmail.pm new file mode 100644 index 0000000..3ec93f1 --- /dev/null +++ b/lib/JWebmail/Controller/Webmail.pm @@ -0,0 +1,386 @@ +package JWebmail::Controller::Webmail; + +use Mojo::Base 'Mojolicious::Controller'; + +use File::Type; + +use constant { + S_USER => 'user', # Key for user name in active session +}; + + +# no action has been taken, display login page +sub noaction { + my $self = shift; + + my $user = $self->session(S_USER); + if ($user) { + $self->res->code(307); + $self->redirect_to('home'); + } +} + + +# middleware +sub auth { + my $self = shift; + + my $user = $self->session(S_USER); + my $pw = $self->session_passwd; + + unless ($user && $pw) { + $self->flash(message => $self->l('no_session')); + $self->res->code(401); + $self->redirect_to('logout'); + return 0; + } + + return 1; +} + + +sub _time :prototype(&$$) { + my $code = shift; + my $self = shift; + my $name = shift; + + $self->timing->begin($name); + + my @res = $code->(); + + my $elapsed = $self->timing->elapsed($name); + $self->app->log->debug("$name took $elapsed seconds"); + + return wantarray ? @res : $res[-1]; +} + + +sub login { + my $self = shift; + + my $v = $self->validation; + + my $user = $v->required('userid')->size(4, 50)->param; + my $passwd = $v->required('password')->size(4, 50)->like(qr/^.+$/)->param; # no new-lines + + if ($v->has_error) { + $self->res->code(400); + return $self->render(action => 'noaction'); + } + + my $valid = _time { $self->users->verify_user($user, $passwd) } $self, 'verify user'; + + if ($valid) { + $self->session(S_USER() => $user); + $self->session_passwd($passwd); + + $self->res->code(303); + $self->redirect_to('displayheaders'); + } + else { + $self->res->code(401); + $self->render(action => 'noaction', + warning => $self->l('login') . ' ' . $self->l('failed') . '!', + ); + } +} + + +sub logout { + my $self = shift; + + delete $self->session->{S_USER()}; + $self->session_passwd(''); + + # $self->session(expires => 1); + + $self->res->code(303); + $self->redirect_to('noaction'); +} + + +sub about { + my $self = shift; + + $self->stash( + scriptadmin => $self->config->{defaults}{scriptadmin}, + http_host => $self->tx->req->url->to_abs->host, + request_uri => $self->tx->req->url, + remote_addr => $self->tx->original_remote_address, + ); +} + + +sub displayheaders { + no warnings 'experimental::smartmatch'; + my $self = shift; + + my $auth = AuthReadMails->new( + user => $self->session(S_USER), + password => $self->session_passwd, + challenge => $self->app->secrets->[0], + ); + + my $folders = _time { $self->users->folders($auth) } $self, 'user folders'; + push @$folders, ''; + + unless ( $self->stash('folder') ~~ $folders ) { + $self->res->code(404); + $self->render(template => 'error', + error => $self->l('no_folder'), + links => [map { $self->url_for(folder => $_) } @$folders], + ); + return; + } + + my $v = $self->validation; + my $sort = $v->optional('sort')->like(qr'^!?(?:date|subject|sender|size)$')->param // '!date'; + my $search = $v->optional('search')->param; + + if ($v->has_error) { + $self->res->code(400); + $self->render(template => 'error', error => "errors in @{ $v->failed }"); + return; + } + + my ($total_byte_size, $cnt, $new) = _time { $self->users->count($auth, $self->stash('folder')) } $self, 'user count'; + + my ($start, $end) = $self->paginate($cnt); + + $self->timing->begin('user_headers'); + my $headers; + if ($search) { + $headers = $self->users->search( + $auth, $search, $self->stash('folder'), + ); + } + else { + $headers = $self->users->read_headers_for( + auth => $auth, + folder => $self->stash('folder'), + start => $start, + end => $end, + sort => $sort, + ); + } + my $elapsed = $self->timing->elapsed('user_headers'); + $self->app->log->debug("Reading user headers took $elapsed seconds"); + + $self->stash( + msgs => $headers, + mail_folders => $folders, + total_size => $total_byte_size, + total_new_mails => $new, + ); +} + + +sub readmail { + my $self = shift; + + my $mid = $self->stash('id'); + + my $auth = AuthReadMails->new( + user => $self->session(S_USER), + password => $self->session_passwd, + challenge => $self->app->secrets->[0], + ); + + my $mail; + eval { $mail = $self->users->show($auth, $mid) }; + if (my $err = $@) { + if ($err =~ m/unkown mail-id|no such message/) { + $self->reply->not_found; + return; + } + die $@; + } + + $self->render(action => 'readmail', + msg => $mail, + ); +} + + +sub writemail { } + + +sub sendmail { + my $self = shift; + + my %mail; + my $v = $self->validation; + $v->csrf_protect; + + $mail{to} = $v->required('to', 'not_empty')->check('mail_line')->every_param; + $mail{message} = $v->required('body', 'not_empty')->param; + $mail{subject} = $v->required('subject', 'not_empty')->param; + $mail{cc} = $v->optional('cc', 'not_empty')->check('mail_line')->every_param; + $mail{bcc} = $v->optional('bcc', 'not_empty')->check('mail_line')->every_param; + $mail{reply} = $v->optional('back_to', 'not_empty')->check('mail_line')->param; + $mail{attach} = $v->optional('attach', 'non_empty_ul')->upload->param; + $mail{attach_type} = File::Type->new()->mime_type($mail{attach}->asset->get_chunk(0, 512)) if $mail{attach}; + $mail{from} = $self->session(S_USER); + + if ($v->has_error) { + $self->log->debug("mail send failed. Error in @{ $v->failed }"); + + $self->render(action => 'writemail', + warning => $self->l('error_send'), + ); + return; + } + + my $error = $self->send_mail(\%mail); + + if ($error) { + $v->error('send'=> ['internal_error']); # make validation fail so that values are restored + + $self->render(action => 'writemail', + warning => $self->l('error_send'), + ); + return; + } + + $self->flash(message => $self->l('succ_send')); + $self->res->code(303); + $self->redirect_to('displayheaders'); +} + + +sub move { + my $self = shift; + + my $v = $self->validation; + $v->csrf_protect; + + if ($v->has_error) { + return; + } + + my $auth = AuthReadMails->new( + user => $self->session(S_USER), + password => $self->session_passwd, + challenge => $self->app->secrets->[0], + ); + my $folders = $self->users->folders($auth); + + my $mm = $self->every_param('mail'); + my $folder = $self->param('folder'); + + no warnings 'experimental::smartmatch'; + die "$folder not valid" unless $folder ~~ $folders; + + $self->users->move($auth, $_, $folder) for @$mm; + + $self->flash(message => $self->l('succ_move')); + $self->res->code(303); + $self->redirect_to('displayheaders'); +} + + +sub raw { + my $self = shift; + + my $mid = $self->stash('id'); + + my $auth = AuthReadMails->new( + user => $self->session(S_USER), + password => $self->session_passwd, + challenge => $self->app->secrets->[0], + ); + + my $mail = $self->users->show($auth, $mid); + + if ($self->param('body')//'' eq 'html') { + if ($mail->{content_type} eq 'text/html') { + $self->render(text => $mail->{body}) ; + } + elsif ($mail->{content_type} eq 'multipart/alternative') { + my ($content) = grep {$_->{type} eq 'text/html'} @{ $mail->{body} }; + $self->render(text => $content->{val}); + } + else { + $self->res->code(404); + } + } + else { + $self->res->headers->content_type('text/plain'); + $self->render(text => $self->dumper($mail)); + } +} + + +1 + +__END__ + +=encoding utf-8 + +=head1 NAME + +Webmail - All functions comprising the webmail application. + +=head1 SYNOPSIS + + my $r = $app->routes; + $r->get('/about')->to('Webmail#about'); + $r->post('/login')->to('Webmail#login'); + +=head1 DESCRIPTION + +The controller of JWebmail. + +=head1 METHODS + +=head2 noaction + +The login page. This should be the root. + +=head2 auth + + my $a = $r->under('/')->to('Webmail#auth'); + + An intermediate route that makes sure a user has a valid session. + +=head2 login + +Post route that checks login data. + +=head2 logout + +Route that clears session data. + +=head2 about + +Public route. + +=head2 displayheaders + +Provides an overview over messages. + +=head2 readmail + +Displays a single mail. + +=head2 writemail + +A mail editor. + +=head2 sendmail + +Sends a mail written in writemail. + +=head2 move + +Moves mails between mail forlders. + +=head2 raw + +Displays the mail raw, ready to be downloaded. + +=head1 DEPENCIES + +Mojolicious and File::Type + +=cut
\ No newline at end of file diff --git a/lib/JWebmail/Model/Driver/Mock.pm b/lib/JWebmail/Model/Driver/Mock.pm new file mode 100644 index 0000000..eb8c0d0 --- /dev/null +++ b/lib/JWebmail/Model/Driver/Mock.pm @@ -0,0 +1,102 @@ +package JWebmail::Model::Driver::Mock; + +use Mojo::Base -base; + +use List::Util 'sum'; + +use Mojo::JSON qw(decode_json); + + +use constant { + VALID_USER => 'me@example.de', + VALID_PW => 'vwxyz', +}; + +use constant { + LIST_START => 0, + LIST_END => 1, + LIST_SORT => 2, + LIST_FOLDER => 3, +}; + +sub _read_json_file { + my ($file_name) = @_; + + open(my $body_file, '<', $file_name); + local $/; + my $body = <$body_file>; + close $body_file; + + return decode_json($body); +} + + +sub list_reply { + state $init = _read_json_file('msgs.json'); +} +sub read_reply { + state $init = { + 'SC-ORD-MAIL54526c63b751646618a793be3f8329cca@sc-ord-mail5' => _read_json_file('msg2.json'), + 'example' => _read_json_file('msg.json'), + }; +} + + +sub communicate { + no warnings 'experimental::smartmatch'; + + my $self = shift; + + my %args = @_; + + given ($args{mode}) { + when ('auth') { + return (undef, 0) if $args{user} eq VALID_USER && $args{password} eq VALID_PW; + return (undef, 2); + } + when ('list') { + return ([@{ $self->list_reply }[$args{args}->[LIST_START]..$args{args}->[LIST_END]]], 0) if !$args{args}->[LIST_SORT]; + return ([], 0) if $args{args}->[LIST_FOLDER] eq 'test'; + my $s = sub { + my $sort_by = $args{args}->[LIST_SORT]; + my $rev = $sort_by !~ m/^![[:lower:]]+/ ? 1 : -1; + $sort_by =~ s/!//; + return ($a->{$sort_by} cmp $b->{$sort_by}) * $rev; + }; + return ([sort { &$s } @{ $self->list_reply }[$args{args}->[LIST_START]..$args{args}->[LIST_END]]], 0); + } + when ('count') { + return ({ + count => scalar(@{ $self->list_reply }), + size => sum(map {$_->{size}} @{ $self->list_reply }), + new => 0, + }, 0); + } + when ('read-mail') { + my $mid = $args{args}->[0]; + my $mail = $self->read_reply->{$mid}; + return ($mail, 0) if $mail; + return ({error => 'unkown mail-id'}, 3); + } + when ('folders') { + return ([qw(cur test devel debug)], 0); + } + when ('move') { + local $, = ' '; + say "@{ $args{args} }"; + return (undef, 0); + } + default { return ({error => 'unkown mode'}, 3); } + } +} + + +1 + +__END__ + +=head1 NAME + +Mock - Simple file based mock for the L<JWebmail::Model::ReadMails> module. + +=cut
\ No newline at end of file diff --git a/lib/JWebmail/Model/Driver/QMailAuthuser.pm b/lib/JWebmail/Model/Driver/QMailAuthuser.pm new file mode 100644 index 0000000..65e90f1 --- /dev/null +++ b/lib/JWebmail/Model/Driver/QMailAuthuser.pm @@ -0,0 +1,142 @@ +package JWebmail::Model::Driver::QMailAuthuser; + +use Mojo::Base -base; + +use IPC::Open2; +use File::Basename 'fileparse'; +use JSON::PP; + + +has 'user'; +has 'maildir'; +has 'include'; +has qmail_dir => '/var/qmail/'; +has prog => [fileparse(__FILE__)]->[1] . '/QMailAuthuser/Extract.pm'; +has logfile => '/dev/null'; + + +sub communicate { + use autodie; + + my $self = shift; + my %args = @_; + + $args{challenge} //= ''; + $args{args} //= []; + + my $exec = do { + if ($args{mode} eq 'auth') { + $self->qmail_dir . "/bin/qmail-authuser true 3<&0"; + } + else { + my ($user_name) = $args{user} =~ /(\w*)@/; + + $self->qmail_dir.'/bin/qmail-authuser' + . ' perl ' + . join('', map { ' -I ' . $_ } @{ $self->include }) + . ' -- ' + . join(' ', map { $_ =~ s/(['\\])/\\$1/g; "'$_'" } ($self->prog, $self->maildir, $self->user, $user_name, $args{mode}, @{$args{args}})) + . ' 3<&0' + . ' 2>>'.$self->logfile; + } + }; + + my $pid = open2(my $reader, my $writer, $exec) + or die 'failed to create subprocess'; + + $writer->print("$args{user}\0$args{password}\0$args{challenge}\0") + or die 'pipe wite failed'; + close $writer + or die 'closing write pipe failed'; + + binmode $reader, ':utf8'; + my $input = <$reader>; + close $reader + or die 'closing read pipe failed'; + + waitpid($pid, 0); + my $rc = $? >> 8; + + my $resp; + if ($rc == 3 || $rc == 0) { + eval { $resp = decode_json $input; }; + if ($@) { $resp = {error => 'decoding error'} }; + } + elsif ($rc) { + $resp = {error => "qmail-authuser returned code: $rc"}; + } + + return ($resp, $rc); +} + + +1 + +__END__ + +=encoding utf-8 + +=head1 NAME + +QMailAuthuser + +=head1 SYNOPSIS + + my $m = JWebmail::Model::ReadMails->new(driver => JWebmail::Model::Driver::QMailAuthuser->new(...)); + +=head1 DESCRIPTION + +This ReadMails driver starts and communicates with L<JWebmail::Model::Driver::QMailAuthuser::Extract> over qmail-authuser. +The Extract programm runs with elevated priviliges to be able to read and modify mailboxes. + +=head1 ATTRIBUTES + +=head2 qmail_dir + +The parent directory of the bin directory where all qmail executables live. +Default C</var/qmail/>. + +=head2 prog + +The path to the extractor programm. +Default is the location of L<JWebmail::Model::Driver::QMailAuthuser::Extract> package. + +=head2 logfile + +A path to a log file that the extractor logs to. +Default '/dev/null' but highly recommended to set a real one. +Keep in mind that a different user need to be able to write to it. + +=head1 METHODS + +=head2 communicate + +Arguments: + +=over 6 + +=item mode + +=item args + +Depends on the mode + +=item user + +User name + +=item password + +User password + +=item challenge + +Challenge when using cram + +=back + +=head1 SEE ALSO + +L<JWebmail::Model::ReadMails>, L<JWebmail::Model::Driver::QMailAuthuser::Extract> + +=cut
\ No newline at end of file diff --git a/lib/JWebmail/Model/Driver/QMailAuthuser/Extract.pm b/lib/JWebmail/Model/Driver/QMailAuthuser/Extract.pm new file mode 100755 index 0000000..30ac4e9 --- /dev/null +++ b/lib/JWebmail/Model/Driver/QMailAuthuser/Extract.pm @@ -0,0 +1,293 @@ +package JWebmail::Model::Driver::QMailAuthuser::Extract; + +use v5.18; +use strict; +use warnings; +use utf8; + +use POSIX (); +use JSON::PP; +use Carp; +use Encode v2.88 qw(decode); + +use open IO => ':encoding(UTF-8)', ':std'; +no warnings 'experimental::smartmatch'; + +use Mail::Box::Manager; + +use constant { + ROOT_MAILDIR => '.', +}; + + +sub main { + my ($maildir) = shift(@ARGV) =~ m/(.*)/; + my ($su) = shift(@ARGV) =~ m/(.*)/; + my ($user) = shift(@ARGV) =~ m/([[:alpha:]]+)/; + my $mode = shift @ARGV; _ok($mode =~ m/([[:alpha:]-]{1,20})/); + my @args = @ARGV; + + delete $ENV{PATH}; + + my $netfehcom_uid = getpwnam($su); + #$> = $netfehcom_uid; + die "won't stay as root" if $netfehcom_uid == 0; + POSIX::setuid($netfehcom_uid); + if ($!) { + warn 'error setting uid'; + exit(1); + } + + my $folder = Mail::Box::Manager->new->open( + folder => "$maildir/$user/", + type => 'maildir', + access => 'rw', + ); + + my $reply = do { + given ($mode) { + when('list') { list($folder, @args) } + when('read-mail') { read_mail($folder, @args) } + when('count') { count_messages($folder, @args) } + when('search') { search($folder, @args) } + when('folders') { folders($folder, @args) } + when('move') { move($folder, @args) } + default { {error => 'unkown mode', mode => $mode} } + } + }; + $folder->close; + + print encode_json $reply; + if (ref $reply eq 'HASH' && $reply->{error}) { + exit 3; + } +} + + +sub _sort_mails { + my $sort = shift // ''; + my $reverse = 1; + + if ($sort =~ m/^!/) { + $reverse = -1; + $sort = substr $sort, 1; + } + + given ($sort) { + when ('date') { return sub { ($a->timestamp <=> $b->timestamp) * $reverse } } + when ('sender') { return sub { ($a->from->[0] cmp $b->from->[0]) * $reverse } } + when ('subject') { return sub { ($a->subject cmp $b->subject) * $reverse } } + when ('size') { return sub { ($a->size <=> $b->size) * $reverse } } + when ('') { return sub { ($a->timestamp <=> $b->timestamp) * $reverse } } + default { warn "unkown sort-verb '$sort'"; return sub { ($a->timestamp <=> $b->timestamp) * $reverse } } + } +} + + +sub _ok { + if (!shift) { + carp 'verify failed'; + exit 4; + } +} + + +sub list { + my ($f, $start, $end, $sortby, $folder) = @_; + $folder = ".$folder"; + + _ok($start =~ m/^\d+$/); + _ok($end =~ m/^\d+$/); + _ok(0 <= $start && $start <= $end); + _ok($sortby =~ m/^(!?\w+|\w*)$/n); + _ok($folder ~~ [$f->listSubFolders, ROOT_MAILDIR]); + + $f = $f->openSubFolder($folder) if $folder ne ROOT_MAILDIR; + + return [] if $start == $end; + + my $sref = _sort_mails($sortby); + my @msgs = $f->messages; + @msgs = sort { &$sref } @msgs; + @msgs = @msgs[$start..$end]; + + my @msgs2; + + for my $msg (@msgs) { + my $msg2 = { + #subject => scalar decode_mimewords($msg->subject), + subject => decode('MIME-Header', $msg->subject), + from => _addresses($msg->from), + to => _addresses($msg->to), + cc => _addresses($msg->cc), + bcc => _addresses($msg->bcc), + date => _iso8601_utc($msg->timestamp), + size => $msg->size, + content_type => ''. $msg->contentType, + mid => $msg->messageId, + new => $msg->label('seen'), + }; + push @msgs2, $msg2; + } + + return \@msgs2; +} + + +sub count_messages { + my ($f, $folder) = @_; + $folder = ".$folder"; + + _ok($folder ~~ [$f->listSubFolders, ROOT_MAILDIR]); + + $f = $f->openSubFolder($folder) if $folder ne ROOT_MAILDIR; + + return { + count => scalar($f->messages('ALL')), + size => $f->size, + new => scalar $f->messages('!seen'), + } +} + + +sub _iso8601_utc { + my @date_time = gmtime(shift); + $date_time[5] += 1900; + $date_time[4]++; + return sprintf('%6$04d-%5$02d-%4$02dT%3$02d:%2$02d:%1$02dZ', @date_time); +} + +sub _unquote { my $x = shift; [$x =~ m/"(.*?)"(?<!\\)/]->[0] || $x } + +sub _addresses { + [map { {address => $_->address, name => _unquote(decode('MIME-Header', $_->phrase))} } @_] +} + + +sub read_mail { + my ($folder, $mid) = @_; + + my $msg = $folder->find($mid); + return {error => 'no such message', mid => $mid} unless $msg; + return { + subject => decode('MIME-Header', $msg->subject), + from => _addresses($msg->from), + to => _addresses($msg->to), + cc => _addresses($msg->cc), + bcc => _addresses($msg->bcc), + date => _iso8601_utc($msg->timestamp), + size => $msg->size, + content_type => ''. $msg->contentType, + body => do { + if ($msg->isMultipart) { + [map {{type => ''. $_->contentType, val => '' . $_->decoded}} $msg->body->parts] + } + else { + '' . $msg->body->decoded + } + }, + } +} + + +sub search { + my $f = shift; + my $search_pattern = shift; + my $folder = shift; + $folder = ".$folder"; + + $f = $f->openSubFolder($folder) if $folder ne ROOT_MAILDIR; + + my @msgs = $f->messages(sub { + my $m = shift; + + return scalar(grep { $_->decoded =~ /$search_pattern/ || (decode('MIME-Header', $_->subject)) =~ /$search_pattern/ } $m->body->parts) + if $m->isMultipart; + $m->body->decoded =~ /$search_pattern/ ||(decode('MIME-Header', $m->subject)) =~ /$search_pattern/; + }); + + my @msgs2; + for my $msg (@msgs) { + my $msg2 = { + subject => decode('MIME-Header', $msg->subject), + from => _addresses($msg->from), + to => _addresses($msg->to), + cc => _addresses($msg->cc), + bcc => _addresses($msg->bcc), + date => _iso8601_utc($msg->timestamp), + size => $msg->size, + content_type => ''. $msg->contentType, + mid => $msg->messageId, + }; + push @msgs2, $msg2; + } + + return \@msgs2; +} + + +sub folders { + my $f = shift; + + return [grep { $_ =~ m/^\./ && $_ =~ s/\.// && 1 } $f->listSubFolders]; +} + + +sub move { + my ($f, $mid, $dst) = @_; + $dst = ".$dst"; + + _ok($dst ~~ [$f->listSubFolders, ROOT_MAILDIR]); + + $f->moveMessage($dst, $dst->find($mid)); +} + + +main() if !caller; + +1 + +__END__ + +=encoding utf-8 + +=head1 NAME + +JWebmail::Model::Driver::QMailAuthuser::Extract - Maildir reader + +=head1 SYNOPSIS + +Extract delivers information about emails. +Runs with elevated priviliges. + +=head1 DESCRIPTION + +This programm is started by qmail-authuser with elevated priviliges after +a succsessful login. +Input directives are provided as command line arguments. +Output is delivered via STDOUT and log information via STDERR. + +=head1 ARGUMENTS + + prog <maildir> <system-user> <mail-user> <mode> <args...> + +=head2 Modes + + list <start> <end> <sort-by> <folder> + count <folder> + read-mail <mid> + search <pattern> <folder> + folders + move <mid> <dst-folder> + +All arguments must be supplied for a given mode even if empty (as ''). + +=head1 DEPENDENCIES + +Currently Mail::Box::Manager does all the hard work. + +=head1 SEE ALSO + +L<JWebmail::Model::Driver::QMailAuthuser> + +=cut
\ No newline at end of file diff --git a/lib/JWebmail/Model/ReadMails.pm b/lib/JWebmail/Model/ReadMails.pm new file mode 100644 index 0000000..0f2e1cc --- /dev/null +++ b/lib/JWebmail/Model/ReadMails.pm @@ -0,0 +1,227 @@ +package JWebmail::Model::ReadMails; + +use Mojo::Base -base; + +use Class::Struct AuthReadMails => { + user => '$', + password => '$', + challenge => '$', +}; + + +has 'driver'; + + +sub verify_user { + + my $self = shift; + + my ($user, $password) = @_; + + return !scalar $self->driver->communicate( + user => $user, + password => $password, + mode => 'auth', + ) +} + + +sub read_headers_for { + + my $self = shift; + + my %h = @_; + my ($auth, $folder, $start, $end, $sort) = @h{qw(auth folder start end sort)}; + + my ($resp, $rc) = $self->driver->communicate( + user => $auth->user, + password => $auth->password, + challenge => $auth->challenge, + mode => 'list', + args => [$start || '0', $end || '0', $sort || 'date', $folder || ''], + ); + die "connection error: $resp->{error}" if $rc; + return $resp; +} + + +sub count { + + my $self = shift; + + my ($auth, $folder) = @_; + + my ($resp, $rc) = $self->driver->communicate( + user => $auth->user, + password => $auth->password, + challenge => $auth->challenge, + mode => 'count', + args => [$folder], + ); + die "connection error: $resp->{error}" if $rc; + return ($resp->{size}, $resp->{count}, $resp->{new}); +} + + +sub show { + my $self = shift; + + my ($auth, $mid) = @_; + + my ($resp, $rc) = $self->driver->communicate( + user => $auth->user, + password => $auth->password, + challenge => $auth->challenge, + mode => 'read-mail', + args => [$mid], + ); + die "connection error: $resp->{error}, $resp->{mid}" if $rc; + return $resp; +} + + +sub search { + my $self = shift; + + my ($auth, $pattern, $folder) = @_; + + my ($resp, $rc) = $self->driver->communicate( + user => $auth->user, + password => $auth->password, + challenge => $auth->challenge, + mode => 'search', + args => [$pattern, $folder], + ); + die "connection error: $resp->{error}" if $rc; + return $resp; +} + + +sub folders { + my $self = shift; + + my ($auth) = @_; + + my ($resp, $rc) = $self->driver->communicate( + user => $auth->user, + password => $auth->password, + challenge => $auth->challenge, + mode => 'folders', + ); + die "connection error: $resp->{error}" if $rc; + return $resp; +} + + +sub move { + my $self = shift; + + my ($auth, $mid, $folder) = @_; + + my ($resp, $rc) = $self->driver->communicate( + user => $auth->user, + password => $auth->password, + challenge => $auth->challenge, + mode => 'move', + args => [$mid, $folder], + ); + die "connection error: $resp->{error}" if $rc; + return 1; +} + + +1 + +__END__ + +=encoding utf-8 + +=head1 NAME + +ReadMails - Read recieved mails + +=head1 SYNOPSIS + + my $m = JWebmail::Model::ReadMails->new(driver => ...); + $m->search($auth, qr/Hot singles in your area/, ''); + +=head1 DESCRIPTION + +This module is a facade for the actions of its driver. +All actions are delegated to it. + +The first parameter is authentication info as AuthReadMails +whith the rest varying. + +The communication is stateless. + +=head1 ATTRIBUTES + +=head2 driver + +The driver does the actual work of reading the mailbox. + +=head1 METHODS + +=head2 new + +Instantiate a new object. The 'driver' option is required. + +=head2 verify_user + +Checks user name and password. + +=head2 read_headers_for + +Provides bundeled information on a subset of mails of a mailbox. +Can be sorted and of varying size. + +=head2 count + +Returns size of the mail box folder in bytes the number of mails. + +=head2 show + +Returns a sepecific mail as a perl hash. + +=head2 search + +Searches for a message with the given pattern. + +=head2 folders + +List all mailbox sub folders. + +=head2 move + +Move mails between folders. + +=head1 CLASSES + +=head2 AuthReadMails + +A struct that bundles auth data. + +=head3 Attributes + +=head4 user + +The user name. + +=head4 password + +The users password in plaintext or as hmac if cram is used. + +=head4 challenge + +Optinal challange for when you use cram authentication. + +=head3 Methods + +=head4 new + +=head1 SEE ALSO + +L<JWebmail::Model::Driver::QMailAuthuser>, L<JWebmail::Model::Driver::Mock>, L<JWebmail> + +=cut
\ No newline at end of file diff --git a/lib/JWebmail/Model/WriteMails.pm b/lib/JWebmail/Model/WriteMails.pm new file mode 100644 index 0000000..5df5379 --- /dev/null +++ b/lib/JWebmail/Model/WriteMails.pm @@ -0,0 +1,143 @@ +package JWebmail::Model::WriteMails; + +use v5.18; +use warnings; +use utf8; + +use Exporter 'import'; +our @EXPORT_OK = qw(sendmail); +use Data::Dumper; + +use Email::MIME; + + +our $Block_Writes = 0; + + +sub _build_mail { + my $mail = shift; + + my $text_part = Email::MIME->create( + attributes => { + content_type => 'text/plain', + charset => 'utf-8', + encoding => '8bit', + }, + body_str => $mail->{message}, + ); + my $attach; + $attach = Email::MIME->create( + attributes => { + content_type => $mail->{attach_type}, + encoding => 'base64', + }, + body => $mail->{attach}->asset->slurp, + ) if $mail->{attach}; + + my $email = Email::MIME->create( + header_str => [ + From => $mail->{from}, + To => $mail->{to}, + Subject => $mail->{subject}, + 'X-Mailer' => 'JWebmail', + ], + parts => [$text_part, $attach || () ], + ); + $email->header_str_set(CC => @{$mail->{cc}}) if $mail->{cc}; + $email->header_str_set('Reply-To' => $mail->{reply}) if $mail->{reply}; + + return $email->as_string; +} + + +sub _send { + my ($mime, @recipients) = @_; + + open(my $m, '|-', 'sendmail', '-i', @recipients) + or die 'Connecting to sendmail failed. Is it in your PATH?'; + $m->print($mime->as_string); + close($m); + return $? >> 8; +} + + +sub sendmail { + my $mail = shift; + + my $mime = _build_mail($mail); + + my @recipients; + push @recipients, @{ $mail->{to} } if $mail->{to}; + push @recipients, @{ $mail->{cc} } if $mail->{cc}; + push @recipients, @{ $mail->{bcc} } if $mail->{bcc}; + + say $mime if $Block_Writes; + return 1 if $Block_Writes; + + return _send($mime, @recipients); +} + + +1 + +__END__ + +=encoding utf-8 + +=head1 NAME + +WriteMails - Build and send mails via a sendmail interface + +=head1 SYNOPSIS + + JWebmail::Model::WriteMails::sendmail { + from => ..., + to => ..., + subject => ..., + }; + +=head1 DESCRIPTION + +Build and send mails. + +=head1 FUNCTIONS + +=head2 sendmail + +Send the mail immediately. + +=head3 from + +The sender. + +=head3 to + +The recipient(s). + +=head3 reply + +The address the recipient is meant to reply to (optinal, if missing from is assumed). + +=head3 cc + +Secondary recipients, visible to other. + +=head3 bcc + +Secondary recipients, invisible to other. + +=head3 subject + +=head3 message + +The message body. Should be plain text encoded as utf-8. + +=head3 attach + +Optinal attachment. + +=head3 attach_type + +The mime type of the attachment. + +=cut
\ No newline at end of file diff --git a/lib/JWebmail/Plugin/Helper.pm b/lib/JWebmail/Plugin/Helper.pm new file mode 100644 index 0000000..5e557d1 --- /dev/null +++ b/lib/JWebmail/Plugin/Helper.pm @@ -0,0 +1,448 @@ +package JWebmail::Plugin::Helper; + +use Mojo::Base 'Mojolicious::Plugin'; + +use POSIX qw(floor round log ceil); +use MIME::Base64; +use Encode; +use Mojo::Util 'xml_escape'; +use List::Util qw(min max); + +use constant TRUE_RANDOM => eval { require Crypt::Random; Crypt::Random->import('makerandom_octet'); 1 }; +use constant HMAC => eval { require Digest::HMAC_MD5; Digest::HMAC_MD5->import('hmac_md5'); 1 }; + +### filter and checks for mojo validator + +sub mail_line { + my ($v, $name, $value, @args) = @_; + + my $mail_addr = qr/\w+\@\w+\.\w+/; + # my $unescaped_quote = qr/"(*nlb:\\)/; # greater perl version required + my $unescaped_quote = qr/"(?<!:\\)/; + + return $value !~ /^( + ( + ( + ( + $unescaped_quote.*?$unescaped_quote + ) | ( + [\w\s]* + ) + ) + \s*<$mail_addr> + ) | ( + $mail_addr + ))$ + /xno; +} + + +sub filter_empty_upload { + my ($v, $name, $value) = @_; + + return $value->filename ? $value : undef; +} + +### template formatting functions + +sub print_sizes10 { + my $var = shift; + if ($var == 0) { return '0 Byte'; } + + my $i = floor(((log($var)/log(10))+1e-5) / 3); + my $expo = $i * 3; + + my @PREFIX; + $PREFIX[0] = 'Byte'; + $PREFIX[1] = 'kByte'; + $PREFIX[2] = 'MByte'; + $PREFIX[3] = 'GByte'; + $PREFIX[4] = 'TByte'; + $PREFIX[5] = 'PByte'; + + return sprintf('%.0f %s', $var / (10**$expo), $PREFIX[$i]); +} + + +sub print_sizes2 { + my $var = shift; + if ($var == 0) { return '0 Byte'; } + + my $i = floor(((log($var)/log(2))+1e-5) / 10); + my $expo = $i * 10; + my %PREFIX = ( + 0 => 'Byte', + 1 => 'KiByte', + 2 => 'MiByte', + 3 => 'GiByte', + 4 => 'TiByte', + 5 => 'PiByte', + ); + my $pref = $PREFIX{$i}; + return round($var / (2**$expo)) . " $pref"; +} + +### mime type html render functions + +my $render_text_plain = sub { + my ($c, $content) = @_; + + $content = xml_escape($content); + $content =~ s/\n/<br>/g; + + return $content; +}; + + +my $render_text_html = sub { + my $c_ = shift; + + return '<iframe src="' . $c_->url_for('rawid', id => $c_->stash('id'))->query(body => 'html') . '" class=html-mail />'; +}; + + +our %MIME_Render_Subs = ( + 'text/plain' => $render_text_plain, + 'text/html' => $render_text_html, +); + + +sub mime_render { + my ($c, $enc, $cont) = @_; + + my $renderer = $MIME_Render_Subs{$enc}; + return '' unless defined $renderer; + return $renderer->($c, $cont); +}; + +### session password handling + +use constant { S_PASSWD => 'pw', S_OTP_S3D_PW => 'otp_s3d_pw' }; + +sub _rand_data { + my $len = shift; + + return makerandom_octet(Length => $len, Strength => 0); +} + +sub _pseudo_rand_data { + my $len = shift; + + my $res = ''; + for (0..$len-1) { + vec($res, $_, 8) = int rand 256; + } + + return $res; +} + +sub session_passwd { + my ($c, $passwd) = @_; + + warn_cram($c); + warn_crypt($c); + + if (defined $passwd) { # set + if ( HMAC && lc($c->config->{'session'}{secure} || 'none') eq 'cram' ) { + $c->session(S_PASSWD() => $passwd ? encode_base64(hmac_md5($passwd, $c->app->secrets->[0]), '') : ''); + } + elsif (lc($c->config->{'session'}->{secure} || 'none') eq 's3d') { + unless ($passwd) { + $c->s3d(S_PASSWD, ''); + delete $c->session->{S_OTP_S3D_PW()}; + return; + } + die "'$passwd' contains invalid character \\n" if $passwd =~ /\n/; + if (length $passwd < 20) { + $passwd .= "\n" . " " x (20 - length($passwd) - 1); + } + my $rand_bytes = TRUE_RANDOM ? _rand_data(length $passwd) : _pseudo_rand_data(length $passwd); + $c->s3d(S_PASSWD, encode_base64(encode('UTF-8', $passwd) ^ $rand_bytes, '')); + $c->session(S_OTP_S3D_PW, encode_base64($rand_bytes, '')); + } + else { + $c->session(S_PASSWD() => $passwd); + } + } + else { # get + if ( HMAC && lc($c->config->{'session'}->{secure} || 'none') eq 'cram' ) { + return ($c->app->secrets->[0], $c->session(S_PASSWD)); + } + elsif (lc($c->config->{'session'}->{secure} || 'none') eq 's3d') { + my $pw = decode_base64($c->s3d(S_PASSWD) || ''); + my $otp = decode_base64($c->session(S_OTP_S3D_PW) || ''); + my ($res) = split "\n", decode('UTF-8', $pw ^ $otp), 2; + return $res; + } + else { + return $c->session(S_PASSWD); + } + } +} + +sub warn_cram { + my $c = shift; + + state $once = 0; + + if ( !HMAC && !$once && lc($c->config->{'session'}->{secure} || 'none') eq 'cram' ) { + $c->log->warn("cram requires Digest::HMAC_MD5. Falling back to 'none'."); + } + + $once = 1; +} + +sub warn_crypt { + my $c = shift; + + state $once = 0; + + if ( !TRUE_RANDOM && !$once && lc($c->config->{'session'}->{secure} || 'none') eq 's3d' ) { + $c->log->warn("Falling back to pseudo random generation. Please install Crypt::Random"); + } + + $once = 1; +} + +### pagination + +sub _clamp { + my ($x, $y, $z) = @_; + + die '!($x <= $z)' unless $x <= $z; + + if ($x <= $y && $y <= $z) { + return $y; + } + + return $x if ($y < $x); + return $z if ($z < $y); +} + +sub _paginate { + my %args = @_; + + my $first_item = $args{first_item}; + my $page_size = $args{page_size} || 1; + my $total_items = $args{total_items}; + + my $first_item1 = $total_items ? $first_item+1 : 0; + + my $current_page = ceil($first_item/$page_size); + my $total_pages = ceil($total_items/$page_size); + + my $page = sub { + my $page_ = shift; + return [0, 0] unless $total_items; + $page_ = _clamp(0, $page_, $total_pages-1); + [_clamp(1, $page_*$page_size + 1, $total_items), _clamp(1, ($page_+1)*$page_size, $total_items)] + }; + + return ( + first_item => $first_item1, + last_item => _clamp($first_item1, $first_item + $page_size, $total_items), + total_items => $total_items, + page_size => $page_size, + + total_pages => $total_pages, + current_page => $current_page + 1, + + first_page => $page->(0), + prev_page => $page->($current_page-1), + next_page => $page->($current_page+1), + last_page => $page->($total_pages-1), + ); +} + +sub paginate { + my $c = shift; + my $count = shift; + + my $v = $c->validation; + my $start = $v->optional('start')->num(0, undef)->param // 0; + my $psize = $v->optional('page_size')->num(1, undef)->param // 50; + + $start = _clamp(0, $start, max($count-1, 0)); + my $end = _clamp($start, $start+$psize-1, max($count-1, 0)); + + $c->stash(_paginate(first_item => $start, page_size => $psize, total_items => $count)); + + return $start, $end; +} + +### registering + +sub register { + my ($self, $app, $conf) = @_; + + if (ref $conf->{import} eq 'ARRAY' and my @import = @{ $conf->{import} }) { + no warnings 'experimental::smartmatch'; + + # selective import + $app->helper(print_sizes10 => sub { shift; print_sizes10(@_) }) + if 'print_sizes10' ~~ @import; + $app->helper(print_sizes2 => sub { shift; print_sizes2(@_) }) + if 'print_sizes2' ~~ @import; + $app->helper(mime_render => \&mime_render) + if 'mime_render' ~~ @import; + $app->helper(session_passwd => \&session_passwd) + if 'session_passwd' ~~ @import; + $app->helper(paginate => \&paginate) + if 'paginate' ~~ @import; + $app->validator->add_check(mail_line => \&mail_line) + if 'mail_line' ~~ @import; + $app->validator->add_filter(non_empty_ul => \&filter_empty_upload) + if 'non_empty_ul' ~~ @import; + } + elsif (!$conf->{import}) { # default imports + $app->helper(print_sizes10 => sub { shift; print_sizes10(@_) }); + $app->helper(mime_render => \&mime_render); + $app->helper(session_passwd => \&session_passwd); + $app->helper(paginate => \&paginate); + + $app->validator->add_check(mail_line => \&mail_line); + + $app->validator->add_filter(non_empty_ul => \&filter_empty_upload); + } +} + + +1 + +__END__ + +=encoding utf-8 + +=head1 NAME + +Helper - Functions used as helpers in controller and templates and additional validator checks and filters + +=head1 SYNOPSIS + + use Mojo::Base 'Mojolicious'; + + use JWebmail::Plugin::Helper; + + sub startup($self) { + $self->helper(mime_render => \&JWebmail::Plugin::Helper::mime_render); + } + + # or + + $app->plugin('Helper'); + +=head1 DESCRIPTION + +L<JWebmail::Helper> provides useful helper functions and validator cheks and filter for +L<JWebmail::Controller::All> and various templates. + +=head1 FUNCTIONS + +=head2 mail_line + +A check for validator used in mail headers for fields containing email addresses. + + $app->validator->add_check(mail_line => \&JWebmail::Plugin::Helper::mail_line); + + my $v = $c->validation; + $v->required('to', 'not_empty')->check('mail_line'); + +=head2 filter_empty_upload + +A filter for validator used to filter out empty uploads. + + $app->validator->add_filter(non_empty_ul => \&JWebmail::Plugin::Helper::filter_empty_upload); + + my $v = $c->validation; + $v->required('file_upload', 'non_empty_ul'); + +=head2 print_sizes10 + +A helper for templates used to format byte sizes. + + $app->helper(print_sizes10 => sub { shift; JWebmail::Plugin::Helper::print_sizes10(@_) }); + + %= print_sizes10 12345 # => 12 kB + +=head2 print_sizes2 + +A helper for templates used to format byte sizes. + + %= print_sizes10 12345 # => 12 KiB + +This is not registered by default. + +=head2 paginate + +A helper for calculationg page bounds. + +Takes the total number of items as argument. + +Reads in 'start' and 'page_size' query arguments. +start is 0 based. + +Returns the calculated start and end points as 0 based inclusive range. + +Sets the stash values (all 1 based inclusive): + + first_item + last_item + total_items + page_size + total_pages + current_page + first_page + prev_page + next_page + last_page + +=head2 mime_render + +A helper for templates used to display the content of a mail for the browser. +The output is valid html and should not be escaped. + + $app->helper(mime_render => \&JWebmail::Plugin::Helper::mime_render); + + %== mime_render 'text/plain' $content + +=head2 session_passwd + +A helper used to set and get the session password. The behaivour can be altered by +setting the config variable C<< session => {secure => 's3d'} >>. + + $app->helper(session_passwd => \&JWebmail::Plugin::Helper::session_passwd); + + $c->session_passwd('s3cret'); + +Currently the following modes are supported: + +=over 6 + +=item none + +password is plainly stored in session cookie + +=item cram + +challenge response authentication mechanism uses the C<< $app->secret->[0] >> as nonce. +This is optional if Digest::HMAC_MD5 is installed. + +=item s3d + +data is stored on the server. Additionally the password is encrypted by an one-time-pad that is stored in the user cookie. + +=back + +=head1 DEPENDENCIES + +Mojolicious, Crypt::Random and optianally Digest::HMAC_MD5. + +=head1 SEE ALSO + +L<JWebmail>, L<JWebmail::Controller::All>, L<Mojolicious>, L<Mojolicious::Controller> + +=head1 NOTICE + +This package is part of JWebmail. + +=cut
\ No newline at end of file diff --git a/lib/JWebmail/Plugin/I18N.pm b/lib/JWebmail/Plugin/I18N.pm new file mode 100644 index 0000000..dc10fdd --- /dev/null +++ b/lib/JWebmail/Plugin/I18N.pm @@ -0,0 +1,212 @@ +package JWebmail::Plugin::I18N; + +use Mojo::Base 'Mojolicious::Plugin'; + +use Mojolicious::Controller; +use Mojo::File; +use Mojo::Util 'monkey_patch'; + + +has '_language_loaded' => sub { {} }; + + +sub register { + my ($self, $app, $conf) = @_; + + my $i18n_log = $app->log->context('[' . __PACKAGE__ . ']'); + + # config + # 1. what languages + # 2. where are the files + # 3. fallback language + # + # look for languages automatically + my $defaultLang = $conf->{default_language} || 'en'; + my $fileLocation = $conf->{directory} && Mojo::File->new($conf->{directory})->is_abs + ? $conf->{directory} + : $app->home->child($conf->{directory} || 'lang'); + my @languages = keys %{$conf->{languages} // {}}; + + unless (@languages) { + @languages = map { $_ =~ s|^.*/(..)\.lang$|$1|r } glob("$fileLocation/*.lang"); + } + + $app->defaults(lang => $defaultLang); + $app->defaults(languages => [@languages]); + + # load languages + my $TXT; + for my $l (@languages) { + $TXT->{$l} = _loadi18n($fileLocation, $l, $i18n_log); + } + + { + local $" = ','; + $i18n_log->debug("loaded languages (@languages)"); + } + + $self->_language_loaded( { map { $_ => 1 } @languages } ); + + # add translator as helper + my $i18n = sub { + my ($lang, $word) = @_; + $TXT->{$lang}{$word} || scalar( + local $" = ' ', + $lang && $word ? $app->log->debug('[' . __PACKAGE__ . "] missing translation for $lang:$word @{[ caller(2) ]}[0..2]") : (), + '', + ) + }; + $app->helper( l => sub { my $c = shift; $i18n->($c->stash->{lang}, shift) } ); + + # rewrite url + $app->hook(before_dispatch => sub { $self->read_language_hook(@_) }); + + # patch url_for + my $mojo_url_for = Mojolicious::Controller->can('url_for'); + my $i18n_url_for = sub { + my $c = shift; + my $url = $mojo_url_for->($c, @_); + + my $args = (ref $_[0] eq 'HASH' and $_[0]) || (ref $_[1] eq 'HASH' and $_[1]) || do { my %x = @_[(@_ % 2) .. $#_]; \%x }; + my $lang = $args->{lang} // $c->stash->{lang}; + + if ( $lang && (ref $_[0] eq 'HASH' || !ref $_[0] && ($_[0]//'') !~ m![:@/.]!) ) { + unshift @{ $url->path->parts }, $lang + if ($url->path->parts->[0] // '') ne $lang; + $url = $url->to_abs(Mojo::URL->new('/')); + } + + return $url; + }; + monkey_patch 'Mojolicious::Controller', url_for => $i18n_url_for; + + 0 +} + + +sub read_language_hook { + my $self = shift; + my $c = shift; + + # URL detection + if (my $path = $c->req->url->path) { + + my $part = $path->parts->[0]; + + if ( $part && $self->_language_loaded->{$part} ) { + # Ignore static files + return if $c->res->code; + + $c->app->log->debug('[' . __PACKAGE__ . "] Found language $part in URL $path"); + + # Save lang in stash + $c->stash(lang => $part); + + if ( @{ $path->parts } == 1 && !$path->trailing_slash ) { + return $c->redirect_to($c->req->url->path->trailing_slash(1)); # default controller adds language back + } + + # Clean path + shift @{$path->parts}; + $path->trailing_slash(0); + } + } +} + + +sub _loadi18n { + + my $langsubdir = shift; + my $lang = shift; + my $log = shift; + + my $langFile = "$langsubdir/$lang.lang"; + my $TXT; + + if ( -f $langFile ) { + $TXT = Config::Tiny->read($langFile, 'utf8')->{'_'}; + if ($@ || !defined $TXT) { + $log->error("error reading file $langFile: $@"); + } + } + else { + $log->warn("language file $langFile does not exist!"); + } + return $TXT; +} + + +1 + +__END__ + +=encoding utf8 + +=head1 NAME + +JWebmail::Plugin::I18N - Custom Made I18N Support Inspired by Mojolicious::Plugin::I18N + +=head1 SYNOPSIS + + $app->plugin('I18N', { + languages => [qw(en de es)], + default_language => 'en', + directory => '/path/to/language/files/', + }) + + # in your controller + $c->l('hello') + + # in your templates + <%= l 'hello' %> + + @@ de.lang + login = anmelden + userid = nuzerkennung + passwd = passwort + failed = fehlgeschlagen + about = über + + example.com/de/myroute # $c->stash('lang') eq 'de' + example.com/myroute # $c->stash('lang') eq $defaultLanguage + + # on example.com/de/myroute + url_for('my_other_route') #=> example.com/de/my_other_route + + url_for('my_other_route', lang => 'es') #=> example.com/es/my_other_route + +=head1 DESCRIPTION + +L<JWebmail::Plugin::I18N> provides I18N support. + +The language will be taken from the first path segment of the url. +Be carefult with colliding routes. + +Mojolicious::Controller::url_for is patched so that the current language will be kept for +router named urls. + +=head1 OPTIONS + +=head2 default_language + +The default language when no other information is provided. + +=head2 directory + +Directory to look for language files. + +=head2 languages + +List of allowed languages. +Files of the pattern "$lang.lang" will be looked for. + +=head1 HELPERS + +=head2 l + +This is used for your translations. + + $c->l('hello') + $app->helper('hello')->() + +=cut
\ No newline at end of file diff --git a/lib/JWebmail/Plugin/I18N2.pm b/lib/JWebmail/Plugin/I18N2.pm new file mode 100644 index 0000000..53813de --- /dev/null +++ b/lib/JWebmail/Plugin/I18N2.pm @@ -0,0 +1,185 @@ +package JWebmail::Plugin::I18N2; + +use Mojo::Base 'Mojolicious::Plugin'; + +use Mojolicious::Controller; +use Mojo::File; +use Mojo::Util 'monkey_patch'; + + +has '_language_loaded' => sub { {} }; + + +sub register { + my ($self, $app, $conf) = @_; + + my $i18n_log = $app->log->context('[' . __PACKAGE__ . ']'); + + # config + # 1. what languages + # 2. where are the files + # 3. fallback language + # + # look for languages automatically + my $defaultLang = $conf->{default_language} || 'en'; + my $fileLocation = $conf->{directory} && Mojo::File->new($conf->{directory})->is_abs + ? $conf->{directory} + : $app->home->child($conf->{directory} || 'lang'); + my @languages = keys %{$conf->{languages} // {}}; + + unless (@languages) { + @languages = map { $_ =~ s|^.*/(..)\.lang$|$1|r } glob("$fileLocation/*.lang"); + } + + $app->defaults(languages => [@languages]); + + # load languages + my $TXT; + for my $l (@languages) { + $TXT->{$l} = _loadi18n($fileLocation, $l, $i18n_log); + } + + { + local $" = ','; + $i18n_log->debug("loaded languages (@languages)"); + } + + $self->_language_loaded( { map { $_ => 1 } @languages } ); + + # add translator as helper + my $i18n = sub { + my ($lang, $word) = @_; + $TXT->{$lang}{$word} || scalar( + local $" = ' ', + $lang && $word ? $app->log->debug('[' . __PACKAGE__ . "] missing translation for $lang:$word @{[ caller(2) ]}[0..2]") : (), + '', + ) + }; + $app->helper( l => sub { my $c = shift; $i18n->($c->stash->{lang}, shift) } ); + + $app->hook(before_dispatch => sub { + my $c = shift; + unshift @{ $c->req->url->path->parts }, '' + unless $self->_language_loaded->{$c->req->url->path->parts->[0] || ''}; + }); + + # patch url_for + my $mojo_url_for = Mojolicious::Controller->can('url_for'); + my $i18n_url_for = sub { + my $c = shift; + if (ref $_[0] eq 'HASH') { + $_[0]->{lang} ||= $c->stash('lang'); + } + elsif (ref $_[1] eq 'HASH') { + $_[1]->{lang} ||= $c->stash('lang'); + } + elsif (@_) { + push @_, lang => $c->stash('lang'); + } + else { + @_ = {lang => $c->stash('lang')}; + } + return $mojo_url_for->($c, @_); + }; + monkey_patch 'Mojolicious::Controller', url_for => $i18n_url_for; + + return $app->routes->any('/:lang' => {lang => 'en'}); +} + + +sub _loadi18n { + + my $langsubdir = shift; + my $lang = shift; + my $log = shift; + + my $langFile = "$langsubdir/$lang.lang"; + my $TXT; + + if ( -f $langFile ) { + $TXT = Config::Tiny->read($langFile, 'utf8')->{'_'}; + if ($@ || !defined $TXT) { + $log->error("error reading file $langFile: $@"); + } + } + else { + $log->warn("language file $langFile does not exist!"); + } + return $TXT; +} + + +1 + +__END__ + +=encoding utf8 + +=head1 NAME + +JWebmail::Plugin::I18N2 - Custom Made I18N Support an alternative to JWebmail::Plugin::I18N + +=head1 SYNOPSIS + + $app->plugin('I18N2', { + languages => [qw(en de es)], + default_language => 'en', + directory => '/path/to/language/files/', + }) + + # in your controller + $c->l('hello') + + # in your templates + <%= l 'hello' %> + + @@ de.lang + login = anmelden + userid = nuzerkennung + passwd = passwort + failed = fehlgeschlagen + about = über + + example.com/de/myroute # $c->stash('lang') eq 'de' + example.com/myroute # $c->stash('lang') eq $defaultLanguage + + # on example.com/de/myroute + url_for('my_other_route') #=> example.com/de/my_other_route + + url_for('my_other_route', lang => 'es') #=> example.com/es/my_other_route + +=head1 DESCRIPTION + +L<JWebmail::Plugin::I18N2> provides I18N support. + +The language will be taken from the first path segment of the url. +Be carefult with colliding routes. + +Mojolicious::Controller::url_for is patched so that the current language will be kept for +router named urls. + +=head1 OPTIONS + +=head2 default_language + +The default language when no other information is provided. + +=head2 directory + +Directory to look for language files. + +=head2 languages + +List of allowed languages. +Files of the pattern "$lang.lang" will be looked for. + +=head1 HELPERS + +=head2 l + +This is used for your translations. + + $c->l('hello') + $app->helper('hello')->() + +=cut
\ No newline at end of file diff --git a/lib/JWebmail/Plugin/INIConfig.pm b/lib/JWebmail/Plugin/INIConfig.pm new file mode 100644 index 0000000..fe0fb1a --- /dev/null +++ b/lib/JWebmail/Plugin/INIConfig.pm @@ -0,0 +1,136 @@ +package JWebmail::Plugin::INIConfig; +use Mojo::Base 'Mojolicious::Plugin::Config'; + +use List::Util 'all'; + +use Config::Tiny; + + +sub parse { + my ($self, $content, $file, $conf, $app) = @_; + + my $ct = Config::Tiny->new; + my $config = $ct->read_string($content, 'utf8'); + die qq{Can't parse config "$file": } . $ct->errstr unless defined $config; + + $config = _process_config($config) unless $conf->{flat}; + + return $config; +} + + +sub _process_config { + my $val_prev = shift; + my %val = %$val_prev; + + # arrayify section with number keys + for my $key (keys %val) { + if (keys %{$val{$key}} && all { $_ =~ /\d+/} keys %{$val{$key}}) { + my $tmp = $val{$key}; + $val{$key} = []; + + for (keys %$tmp) { + $val{$key}[$_] = $tmp->{$_}; + } + } + } + + # merge top section + my $top_section = $val{'_'}; + delete $val{'_'}; + for (keys %$top_section) { + $val{$_} = $top_section->{$_} unless $val{$_}; + } + + # make implicit nesting explicit + for my $key (grep { $_ =~ /^\w+(::\w+)+$/} keys %val) { + + my @sections = split m/::/, $key; + my $x = \%val; + my $y; + for (@sections) { + $x->{$_} = {} unless ref $x->{$_};# eq 'HASH'; + $y = $x; + $x = $x->{$_}; + } + # merge + if (ref $val{$key} eq 'ARRAY') { + $y->{$sections[-1]} = []; + $x = $y->{$sections[-1]}; + for ( keys @{ $val{$key} } ) { + $x->[$_] = $val{$key}[$_]; + } + } + else { + for ( keys %{ $val{$key} } ) { + $x->{$_} = $val{$key}{$_}; + } + } + delete $val{$key}; + } + + return \%val +} + + +1 + +__END__ + +=encoding utf-8 + +=head1 NAME + +INIConfig - Reads in ini config files. + +=head1 SYNOPSIS + + $app->plugin('INIConfig'); + + @@ my_app.conf + + # global section + key = val ; line comment + [section] + other_key = other_val + [other::section] + 0 = key1 + 1 = key2 + 2 = key3 + +=head1 DESCRIPTION + +INI configuration is simple with limited nesting and propper comments. +For more precise specification on the syntax see the Config::Tiny documentation +on metacpan. + +=head1 OPTIONS + +=head2 default + +Sets default configuration values. + +=head2 ext + +Sets file extension defaults to '.conf'. + +=head2 file + +Sets file name default '$app->moniker'. + +=head2 flat + +Keep configuration to exactly two nesting levels for all +and disable auto array conversion. + +=head1 METHODS + +=head2 parse + +overrides the parse method of Mojolicious::Plugin::Config + +=head1 DEPENDENCIES + +Config::Tiny + +=cut
\ No newline at end of file diff --git a/lib/JWebmail/Plugin/ServerSideSessionData.pm b/lib/JWebmail/Plugin/ServerSideSessionData.pm new file mode 100644 index 0000000..9890358 --- /dev/null +++ b/lib/JWebmail/Plugin/ServerSideSessionData.pm @@ -0,0 +1,147 @@ +package JWebmail::Plugin::ServerSideSessionData; + +use Mojo::Base 'Mojolicious::Plugin'; + +use Mojo::JSON qw(decode_json encode_json); +use Mojo::File; + +use constant { + S_KEY => 's3d.key', +}; + + +has '_session_directory'; +sub session_directory { my $self = shift; @_ ? $self->_session_directory(Mojo::File->new(@_)) : $self->_session_directory } + +has 'expiration'; +has 'cleanup_interval'; + +has '_cleanup'; +sub cleanup { + my $self = shift; + if (@_) { + return $self->_cleanup(@_); + } + else { + if ($self->_cleanup < time) { + return 0; + } + else { + $self->_cleanup(time + $self->cleanup_interval); + return 1; + } + } +} + + +sub s3d { + my $self = shift; + my $c = shift; + + # cleanup old sessions + if ($self->cleanup) { + my $t = time; + for ($self->session_directory->list->each) { + if ( $_->stat->mtime + $self->expiration < $t ) { + $_->remove; + } + } + } + + my $file = $self->session_directory->child($c->session(S_KEY) || $c->req->request_id . $$); + + if (-e $file) { + if ($file->stat->mtime + $self->expiration < time) { + $file->remove; + } + else { + $file->touch; + } + } + my $data = decode_json($file->slurp) if (-s $file); + + my ($key, $val) = @_; + + if (defined $val) { # set + unless (-e $file) { + $c->session(S_KEY, $file->basename); + } + $data = ref $data ? $data : {}; + $data->{$key} = $val; + + #$file->spurt(encode_json $data); + open(my $f, '>', $file) or die "$!"; + chmod 0600, $f; + $f->say(encode_json $data); + close($f); + } + else { # get + return defined $key ? $data->{$key} : $data; + } +}; + + +sub register { + my ($self, $app, $conf) = @_; + + $self->session_directory($conf->{directory} || "/tmp/" . $app->moniker); + $self->expiration($conf->{expiration} || $app->sessions->default_expiration); + $self->cleanup_interval($conf->{cleanup_interval} || $self->expiration); + $self->cleanup(time + $self->cleanup_interval); + + unless (-d $self->session_directory) { + mkdir($self->session_directory) + or $! ? die "failed to create directory: $!" : 1; + } + + $app->helper( s3d => sub { $self->s3d(@_) } ); +} + + +1 + +__END__ + +=encoding utf-8 + +=head1 NAME + +ServeSideSessionData - Stores session data on the server (alias SSSD or S3D) + +=head1 SYNOPSIS + + $app->plugin('ServeSideSessionData'); + + $c->s3d(data => 'Hello, S3D'); + $c->s3d('data'); + +=head1 DESCRIPTION + +Store data temporarily on the server. +The only protetction on the server are struct user access rights. + +=head1 OPTIONS + +=head2 directory + +default C<< 'tmp/' . $app->moniker >> + +=head2 expiration + +default session expiration + +=head2 cleanup_interval + +default session expiration + +=head1 HELPERS + +=head2 s3d + +Stores and retrieves values. + + $c->s3d(data => 'Hello, S3D'); + $c->s3d('data'); + $c->s3d->{data}; + +=cut
\ No newline at end of file diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..287a653 --- /dev/null +++ b/public/style.css @@ -0,0 +1,347 @@ +:root { + --color-font: #000000; + --color-background: #DDDDDD; + --color-table-em: lightblue; + --color-table-em2: #D3DCE3; + --color-table-row: #EEEEEE; + --color-link: blue; + --color-link-visited: purple; + --color-link-hover: red; + --color-warning: orange; + --color-head: #002266; +} + + +/* GENERAL */ +body { + font-family: sans; +} +a:link { + text-decoration: none; + color: var(--color-link); +} +a:visited { + text-decoration: none; + color: var(--color-link-visited); +} +a:hover { + text-decoration: underline; + color: var(--color-link-hover); +} +footer { + margin-top: 50px; + text-align: center; + font-size: small; +} +caption { + vertical-align: middle; + font-size: large; + background-color: var(--color-background); + text-align: center; + font-weight: bold; +} + +.ow h1 { + color: var(--color-background); + background-color: var(--color-head); + width: 80%; + padding: 10px; + margin-left: auto; + margin-right: auto; +} +.ow form, +.ow dl { + width: 80%; + background-color: var(--color-background); + padding: 10px; + margin-left: auto; + margin-right: auto; +} +.ow p.warn { + background-color: var(--color-warning); + text-align: center; + width: 80%; + margin-left: auto; + margin-right: auto; + + padding: 10px; +} + +.ow nav { + margin-left: auto; + margin-right: auto; + width: 80%; + padding: 10px; + text-align: center; + background-color: var(--color-background); +} + +div.show-body { + width: 80%; + margin-left: auto; + margin-right: auto; +} +iframe.html-mail { + width: 100%; + height: 400px; +} + +a.btn { + padding: 5px; + /* margin: 4px; */ + /* border-width: 5px; */ + /* border-style: solid; */ + + color: var(--color-background); + background-color: var(--color-head); + border-color: var(--color-head); + + text-align: center; + display: inline-block; + cursor: pointer; + text-decoration: none; +} +a.btn:hover { + background-color: #0060a0; + /* border-color: black; */ +} + +/* a.btn::before { content: '['; } */ +/* a.btn::after { content: ']'; } */ + + +/* NESTED */ +td.login-form input[type=text] { + width: 100%; + margin: 8px 0; + box-sizing: border-box; +} + + +/* CLASSES */ +table.top { + margin-left: auto; + margin-right: auto; + padding: 10px; + width: 50%; + background-color: var(--color-background); +} +td.small-section { + text-align: center; + background-color: var(--color-background); +} +td.large-section { + vertical-align: middle; + background-color: var(--color-table-row); + width: 470; +} +p.center { + text-align: center; +} +td.login-form { + valign: middle; + align: center; + background-color: var(--color-table-row); +} +tr.submit-row { + text-align: right; +} +td.label-cell { + text-align: right; +} +td.warning { + background-color: var(--color-warning); + text-align: center; +} +p.languages { + color: var(--color-link); +} +td.sep { + bgcolor: #ffffff; +} +td.sort-param { + valign: middle; + width: 12%; + bgcolor: var(--color-table-em2); +} +input.field-with-error { + border: 2px solid red; + border-radius: 4px; +} +.alter-font { + font-family: serif; +} +strong.up { + text-transform: uppercase; + color: var(--color-background); +} +a.bright { + color: #ffffff; +} +td.folder-list { + align: left; + width: 33%; + color: #ffffff; + size: 3; +} +em.msg-count { + color: #ffffff; + size: 3; + font-family: serif; +} + +ul.line { + list-style-type: none; + margin-top: 0px; + margin-bottom: 0px; + /* padding: 0; */ +} +ul.line > li:not(:last-child)::after { + /* border-right: 2px solid var(--color-font); */ + color: var(--color-background); + content: "|"; + margin: 0 .25em; +} +ul.line > li { + /* float: left; */ + display: inline; +} + +div.show-body { + background-color: var(--color-table-row); +} + +td.new-mail > tr { + font-weight: bold; +} + + +/* IDS */ +th#top-section { + background-color: var(--color-background); +} +td#main-version { + font-size: large; + font-weight: bold; +} +table#noaction { + border: 0; + padding: 5px; + margin-left: auto; + margin-right: auto; + margin-top: 100px; + width: 300px; +} +tr#bottom-section { + background-color: var(--color-background); +} +table#displayheaders { + width: 95%; + cellpadding: 1; + cellspacing: 1; + border: 0; + padding: 5px; + margin-left: auto; + margin-right: auto; + /* margin-top: 100px; */ + /* width: 300px; */ +} +table#displayheaders tfoot { + background-color: var(--color-background); +} +tr#sort { + background-color: var(--color-table-em2); +} +td#loginmessage { + bgcolor: var(--color-table-em); + align: center; + size: 2; +} +td#navigation { + bgcolor: #dcdcdc; +} + +table#bot-nav { + text-align: center; + vertical-align: middle; +} + +table#bot-nav > tbody > tr > td:nth-child(2) { + /* text-align: right; */ + /* float: left; */ + /* vertical-align: middle; */ +} + +table#display-folders { + background-color: var(--color-head); + align: center; +} + +table#mail-headers { + border-collapse: collapse; +} +table#mail-headers td { + padding: 5px; +} +table#mail-headers > tbody > tr > td:nth-child(1) { + text-align: right; +} +table#mail-headers > tbody > tr > td:nth-child(3) { + text-align: center; +} +table#mail-headers > tbody > tr > td:nth-child(6) { + text-align: right; +} +table#mail-headers > tbody > tr > td:nth-child(7) { + text-align: center; +} +table#mail-headers > tbody > tr { + background-color: var(--color-table-row); +} +table#mail-headers > tbody > tr:nth-child(even) { + background-color: var(--color-background); +} + +td#pag2 { + white-space: nowrap; +} + +dl#show-head { + background-color: var(--color-background); +} + +form#move-mail { + display: inline; +} + +#pag2 > form { + display: inline; +} + +td#navigation { + background-color: var(--color-background); +} + +form#write-form > label { + display: block; +} + +p#empty { + background-color: var(--color-table-em2); + padding: 25px 0 25px 1cm; + margin: 0; +} + + +/* ANIMATIONS */ +.flash-fade { + animation-duration: 5s; + animation-name: fade; + animation-iteration-count: 1; + animation-fill-mode: forwards; +} +@keyframes fade { + 0% { opacity: 1; } + 90% { opacity: 1; } + 100% { opacity: 0; display: none; } +}
\ No newline at end of file diff --git a/script/jwebmail b/script/jwebmail new file mode 100755 index 0000000..7265d71 --- /dev/null +++ b/script/jwebmail @@ -0,0 +1,11 @@ +#!/usr/bin/env perl + +use strict; +use warnings; + +use Mojo::File qw(curfile); +use lib curfile->dirname->sibling('lib')->to_string; +use Mojolicious::Commands; + +# Start command line interface for application +Mojolicious::Commands->start_app('JWebmail'); diff --git a/t/Helper.t b/t/Helper.t new file mode 100644 index 0000000..99758a3 --- /dev/null +++ b/t/Helper.t @@ -0,0 +1,184 @@ +use v5.22; +use warnings; +use utf8; + +use Test::More; + +use Encode 'decode'; +use MIME::Words 'decode_mimewords'; + +use JWebmail::Plugin::Helper; + + +subtest 'print_size10' => sub { + my %TESTS = ( + 1 => '1 Byte', + 10 => '10 Byte', + 100 => '100 Byte', + 1000 => '1 kByte', + 10000 => '10 kByte', + 100000 => '100 kByte', + 1000000 => '1 MByte', + 10 * 10**6 => '10 MByte', + 10 * 2**20 => '10 MByte', + 800 => '800 Byte', + 9999 => '10 kByte', + 9500 => '10 kByte', + 1024 => '1 kByte', + 1023 => '1 kByte', + ); + + plan tests => scalar keys %TESTS; + + while (my ($input, $want) = each %TESTS) { + is(JWebmail::Plugin::Helper::print_sizes10($input), $want); + } +}; + + +subtest 'vaild_mail_line' => sub { + my %TESTS = ( + 'abc@example.com' => 1, + 'ABC Ex <abc@example.com>' => 1, + '"ABC Ex" <abc@example.com>' => 1, + '"A@B.V Ex" <abc@example.com>' => 1, + '"A@B.V Ex\"" <abc@example.com>' => 1, + 'ABC Ex abc@example.com' => 0, + ); + + plan tests => scalar keys %TESTS; + + while (my ($input, $want) = each %TESTS) { + cmp_ok(JWebmail::Plugin::Helper->mail_line('', $input), '!=', $want); + } +}; + + +subtest 'mime_word_decode' => sub { + my $input = "=?utf-8?Q?Jannis=20wir=20vermissen=20dich!=20Komm=20zur=C3=BCck=20und=20spare=20mit=20uns=20beim=20shoppen=20deiner=20Lieblingsmarken?="; + my $want = "Jannis wir vermissen dich! Komm zurück und spare mit uns beim shoppen deiner Lieblingsmarken"; + my $got = scalar decode_mimewords $input; + + isnt $want, $got; + is $want, _to_perl_enc(decode_mimewords $input); + is $want, decode('MIME-Header', $input); + + done_testing 3; +}; + +sub _to_perl_enc { + my $out = ''; + for (@_) { + if ($_->[1]) { + $out .= decode($_->[1], $_->[0]); + } + else { + $out .= $_->[0]; + } + } + return $out; +} + + +subtest 'pagination' => sub { + my %res; + + %res = JWebmail::Plugin::Helper::_paginate(first_item => 0, page_size => 10, total_items => 55); + + is $res{first_item}, 1; + is $res{last_item}, 10; + is $res{total_items}, 55; + + is $res{page_size}, 10; + is $res{total_pages}, 6; + is $res{current_page}, 1; + + is_deeply $res{first_page}, [1, 10], 'first'; + is_deeply $res{prev_page}, [1, 10], 'prev'; + is_deeply $res{next_page}, [11, 20], 'next'; + is_deeply $res{last_page}, [51, 55], 'last'; + + %res = JWebmail::Plugin::Helper::_paginate(first_item => 10, page_size => 10, total_items => 55); + + is $res{first_item}, 11; + is $res{last_item}, 20; + is $res{total_items}, 55; + + is $res{page_size}, 10; + is $res{total_pages}, 6; + is $res{current_page}, 2; + + is_deeply $res{first_page}, [1, 10], 'first'; + is_deeply $res{prev_page}, [1, 10], 'prev'; + is_deeply $res{next_page}, [21, 30], 'next'; + is_deeply $res{last_page}, [51, 55], 'last'; + + %res = JWebmail::Plugin::Helper::_paginate(first_item => 20, page_size => 10, total_items => 55); + + is $res{first_item}, 21; + is $res{last_item}, 30; + is $res{total_items}, 55; + + is $res{page_size}, 10; + is $res{total_pages}, 6; + is $res{current_page}, 3; + + is_deeply $res{first_page}, [1, 10], 'first'; + is_deeply $res{prev_page}, [11, 20], 'prev'; + is_deeply $res{next_page}, [31, 40], 'next'; + is_deeply $res{last_page}, [51, 55], 'last'; + + %res = JWebmail::Plugin::Helper::_paginate(first_item => 50, page_size => 10, total_items => 55); + + is $res{first_item}, 51; + is $res{last_item}, 55; + is $res{total_items}, 55; + + is $res{page_size}, 10; + is $res{total_pages}, 6; + is $res{current_page}, 6; + + is_deeply $res{first_page}, [1, 10], 'first'; + is_deeply $res{prev_page}, [41, 50], 'prev'; + is_deeply $res{next_page}, [51, 55], 'next'; + is_deeply $res{last_page}, [51, 55], 'last'; + + %res = JWebmail::Plugin::Helper::_paginate(first_item => 0, page_size => 10, total_items => 0); + + is $res{first_item}, 0; + is $res{last_item}, 0; + is $res{total_items}, 0; + + is $res{page_size}, 10; + is $res{total_pages}, 0; + is $res{current_page}, 1; + + is_deeply $res{first_page}, [0, 0], 'first'; + is_deeply $res{prev_page}, [0, 0], 'prev'; + is_deeply $res{next_page}, [0, 0], 'next'; + is_deeply $res{last_page}, [0, 0], 'last'; + + SKIP: { + skip 'The first_item does not align with page boundaries and behaiviour is not specified.'; + + %res = JWebmail::Plugin::Helper::_paginate(first_item => 19, page_size => 10, total_items => 55); + + is $res{first_item}, 20; + is $res{last_item}, 29; + is $res{total_items}, 55; + + is $res{page_size}, 10; + is $res{total_pages}, 6; + is $res{current_page}, 3; + + is_deeply $res{first_page}, [1, 10], 'first'; + is_deeply $res{prev_page}, [11, 20], 'prev'; + is_deeply $res{next_page}, [31, 40], 'next'; + is_deeply $res{last_page}, [51, 55], 'last'; + } + + done_testing; +}; + + +done_testing;
\ No newline at end of file @@ -0,0 +1,96 @@ +package JWebmail::Test::INI; + +use v5.22; +use warnings; +use utf8; + +use Config::Tiny; +use JWebmail::Plugin::INIConfig; +use Data::Dumper; + +use Test2::Bundle::More; +#use Test2::V0; +#use Test::More; + + +local $/; +my $data = <DATA>; +close DATA; + +my $ct = Config::Tiny->new; +my $conf = $ct->read_string($data); + +ok(not $ct->errstr) or diag $ct->errstr; + +SKIP: { + skip 'only for debuging'; + + diag explain $conf; +} + + +subtest 'flat' => sub { + is $conf->{'_'}{a}, 'b'; + is $conf->{section}{d}, 'e'; + is $conf->{section}{1}, 'a'; + is $conf->{'nested::section'}{a}, 'info'; + is $conf->{array_section}{0}, 'a'; + is $conf->{'nested::array_section'}{0}, 'a'; +}; + + +subtest 'processed' => sub { + my $conf2 = JWebmail::Plugin::INIConfig::_process_config($conf); + + is $conf2->{a}, 'b'; + is $conf2->{section}{d}, 'e'; + is $conf2->{section}{1}, 'a'; + is $conf2->{nested}{section}{a}, 'info'; + is $conf2->{nested}{section}{deeply}{x}, 'deeply'; + is $conf2->{array_section}[0], 'a'; + is $conf2->{nested}{array_section}[0], 'a'; +}; + + +done_testing; + + +__DATA__ + +# example file +# [global_section alias _] +a = b ; line comment + +[section] +d = e +f = e # not a comment + +"ha llo , = &f" = 'nic a = %& xa' + +1 = a +2 = b + +x = +y = + +[othersection] +long = my very long value + +[nested::section] +a = info + +[nested::section::deeply] +x = deeply + +[array_section] +0 = a +1 = b +2 = c +3 = d +4 = e + +[nested::array_section] +0 = a + +#[nested::array_section::1::deeply] +#key = val
\ No newline at end of file diff --git a/t/Webmail.t b/t/Webmail.t new file mode 100644 index 0000000..48406b9 --- /dev/null +++ b/t/Webmail.t @@ -0,0 +1,34 @@ +use v5.22; +use warnings; +use utf8; + +use Test::More; +use Test::Mojo; + +use JWebmail::Model::Driver::Mock; + +my $user = JWebmail::Model::Driver::Mock::VALID_USER; +my $pw = JWebmail::Model::Driver::Mock::VALID_PW; + +my $t = Test::Mojo->new('JWebmail', { + development => { use_read_mock => 1, block_writes => 1 }, +}); + +$t->get_ok('/')->status_is(200); + +$t->post_ok('/login', form => {userid => $user, password => 'x'}) + ->status_is(400); + +$t->post_ok('/login', form => {userid => $user, password => 'abcde'}) + ->status_is(401); + +$t->post_ok('/login', form => {userid => $user, password => $pw}) + ->status_is(303); + +done_testing(); + + +#$r->get('/123' => sub { my $c = shift; $c->render(inline => $c->stash->{lang}) }); +#my $x = $self->build_controller; +#$x->match->find($self, {method => 'GET', path => '//write'}); +#print $self->dumper($x->match->stack);
\ No newline at end of file diff --git a/templates/_pagination1.html.ep b/templates/_pagination1.html.ep new file mode 100644 index 0000000..14354c0 --- /dev/null +++ b/templates/_pagination1.html.ep @@ -0,0 +1,5 @@ +<a href="<%= url_with->query({start => $prev_page->[0]-1}) %>"><img src="/left.gif" alt="←"></a> +<a href="<%= url_with->query({start => $first_page->[0]-1}) %>"><img src="/first.gif" alt="↞"></a> +[<%= join(' ', ucfirst l('page'), $current_page, l('of'), $total_pages) %>] +<a href="<%= url_with->query({start => $last_page->[0]-1}) %>"><img src="/last.gif" alt="↠"></a> +<a href="<%= url_with->query({start => $next_page->[0]-1}) %>"><img src="/right.gif" alt="→"></a> diff --git a/templates/_pagination2.html.ep b/templates/_pagination2.html.ep new file mode 100644 index 0000000..f838841 --- /dev/null +++ b/templates/_pagination2.html.ep @@ -0,0 +1,19 @@ +<a href="<%= url_with->query({start => $first_page->[0]-1}) %>"> <img src="/first.gif" alt="<%= l('first') . ' ' . l 'page' %>"></a>\ +<a href="<%= url_with->query({start => $prev_page->[0]-1}) %>"> <img src="/left.gif" alt="<%= l('previous') . ' ' . l 'page' %>"> </a>\ + +<form> + [<label for=custompage><%= ucfirst l 'page' %></label> + <input type=number name=start id=custompage placeholder="<%= $current_page %>" size=3 /> + <%= l 'of' %> <%= $total_pages %>] + +% my $h = $c->req->query_params->to_hash; +% while (my ($k, $v) = each %$h) { +% if ($k ne 'start') { + <input type=hidden name="<%=$k%>" value="<%=$v%>" /> +% } +% } + +</form>\ + +<a href="<%= url_with->query({start => $next_page->[0]-1}) %>"> <img src="/right.gif" alt="<%= l('next') . ' ' . l 'page' %>"> </a>\ +<a href="<%= url_with->query({start => $last_page->[0]-1}) %>"> <img src="/last.gif" alt="<%= l('last') . ' ' . l('page') %>"> </a>
\ No newline at end of file diff --git a/templates/error.html.ep b/templates/error.html.ep new file mode 100644 index 0000000..92391d6 --- /dev/null +++ b/templates/error.html.ep @@ -0,0 +1,27 @@ +<html> + + <head> + <title>Error</title> + </head> + + <body> + <h1>Error</h1> + <p class=center> +% if (my $msg = stash 'error') { +%= $msg +% } +% else { + Uwu :( +% } + </p> +% if (my $see_other = stash 'links') { + See: + <nav> + % for (@$see_other) { + <a href="<%= $_ %>"> <%= $_ %> </a><br> + % } + </nav> +% } + </body> + +</html>
\ No newline at end of file diff --git a/templates/headers/_display_bot_nav.html.ep b/templates/headers/_display_bot_nav.html.ep new file mode 100644 index 0000000..107ac67 --- /dev/null +++ b/templates/headers/_display_bot_nav.html.ep @@ -0,0 +1,45 @@ +<table id=bot-nav width=100%> + + <colgroup> + <col width=70% /> + <col width=10% /> + <col width=20% /> + </colgroup> + + <tr> + <td> + %= include '_pagination1'; + </td> + + <td> + <label for=allbox><%= l 'check_all' %>: </label> + <input name=allbox type=checkbox onclick="check_all(this);"> + </td> + + <td> + <form id=move-mail action="<%= url_for('move') %>" method=post> + <label for=select-folder> <%= l('move') . ' ' . l('to') %> </label> + <select name=folder id=select-folder> + %== "<option value='$_'>$_</option>" for grep {$_ ne $folder} @$mail_folders; + </select> + + %= csrf_field + + <input type="submit" value="<%= l 'move' %>"> + </form> + + </td> + </tr> +</table> + + +<script> +function check_all(box) { + const setTo = box.checked; + const mails = document.getElementById('mail-headers').tBodies[0].rows; + + for (const m of mails) { + m.lastElementChild.children[0].checked = setTo; + } +} +</script>
\ No newline at end of file diff --git a/templates/headers/_display_headers.html.ep b/templates/headers/_display_headers.html.ep new file mode 100644 index 0000000..67e60b4 --- /dev/null +++ b/templates/headers/_display_headers.html.ep @@ -0,0 +1,94 @@ +% my $sort_param = begin + % my $param = shift; + + <th class=sort-param> + <a href="<%= url_with->query(sort => (param('sort') || '') eq $param ? '!' . $param : $param) %>"> + % no warnings qw(experimental::smartmatch); + %= do { given (param('sort')) { '↑' when ($param); '↓' when ('!' . $param) } } + %= ucfirst l $param; + </a> + </th> + +% end + +<table id=mail-headers> + + <colgroup> + <col width=5% /> + <col width=10% /> + <col width=15% /> + <col width=25% /> + <col width=25% /> + <col width=10% /> + <col width=10% /> + </colgroup> + + <thead> + <tr id=sort> + <th> <%= ucfirst l 'nr' %> </th> + + %= $sort_param->('status'); + + %= $sort_param->('date'); + + + % if ($folder ne "SENT") { + %= $sort_param->('sender'); + % } else { + <th class=sort-param> + <a href="<%= url_with->query(sort => param('sort') ne '!sender' ? 'sender' : '!sender' ) %>"> + <%= ucfirst l 'recipient' %> + % if (param('sort') eq "sender") { + <IMG SRC="/down.gif" width="12" height="12" border="0" alt="v"> + % } elsif (param('sort') eq "recipient_rev") { + <IMG SRC="/up.gif" width="12" height="12" border="0" alt="^"> + % } + </a> + </th> + % } + + %= $sort_param->('subject'); + + %= $sort_param->('size'); + + <th> + <!-- <img src="/chkb.gif"> --> + <input type=checkbox checked=1 disabled=1> + </th> + </tr> + </thead> + + + <tbody> + % foreach my $msgnum ($first_item .. $last_item) { + % my $msg = $msgs->[$msgnum - $first_item]; + + <tr class="<%= $msg->{new} ? 'new-mail' : '' %>" id="<%= $msg->{mid} %>" > + <td> + %= $msgnum + </td> + <td> + %= ucfirst($msg->{is_multipart} ? l('yes') : l('no')); + </td> + <td> + % sub d { qr/([[:digit:]]{$_[0]})/ } + % my ($year,$mon,$mday,$hour,$min,$sec) = $msg->{date} =~ m/@{[d(4).'-'.d(2).'-'.d(2).'T'.d(2).':'.d(2).':'.d(2).'Z']}/; + %= join('/', $mday, $mon, $year) . " $hour:$min"; + </td> + <td> + %= $msg->{from}->[0]->{name} || $msg->{from}->[0]->{email}; + </td> + <td> + <a href="<%= url_for('read', id => $msg->{mid}) %>"> <%= $msg->{subject} || '_' %> </a> + </td> + <td> + %= print_sizes10 $msg->{size}; + </td> + <td> + <input type=checkbox name=mail value="<%= $msg->{mid} %>" form=move-mail> + </td> + </tr> + + % } + </tbody> +</table>
\ No newline at end of file diff --git a/templates/headers/_display_top_nav.html.ep b/templates/headers/_display_top_nav.html.ep new file mode 100644 index 0000000..5888cb3 --- /dev/null +++ b/templates/headers/_display_top_nav.html.ep @@ -0,0 +1,33 @@ +<table width=100%> + <tr> + + <td> + <ul class=line> + %# <a href="<%= url_with($prefsurl) %>"><%= TXT 'userconfig' %></a> + %# <a href="<%=$prefsurl%>?action=editaddresses&folder=<%=$folder%>&sessionid=<%=$thissession%>&sort=<%=$sort%>&firstmessage=<%=$firstmessage+1%>&lang=<%=$lang%>" ><%= TXT 'addressbook' %></a> + <li> + <a href="<%= url_for('logout') %>"><%= ucfirst l 'logout' %></a> + </li> + <li> + <a href="<%= url_for('write') %>" ><%= ucfirst l 'compose' %></a> + </li> + </ul> + </td> + + <td> + <form action="<%= url_for %>"> + <label for=search><%= ucfirst l 'search' %></label>: <input type=search name=search size=8> + </form> + </td> + + <td id=pag2> + %= include '_pagination2'; + </td> + + <td> + <!-- delete button --> + %# <form action="<%= url_for('delete_msg') %>" name=Formdel onsubmit="return confirm('<%= TXT q(js_confirm_delete) %>')" > </form> + </td> + + </tr> +</table>
\ No newline at end of file diff --git a/templates/headers/_displayfolders.html.ep b/templates/headers/_displayfolders.html.ep new file mode 100644 index 0000000..8cff0ed --- /dev/null +++ b/templates/headers/_displayfolders.html.ep @@ -0,0 +1,26 @@ +<table id=display-folders width=100%> + <tr> + + <td id=folder-list class=alter-font> + <strong class=up> <%= l($folder) || $folder %> </strong> + + <ul class=line> +% for my $v (grep {$_ ne $folder} @$mail_folders) { + <li><a href="<%= url_for(folder => $v) %>" class=bright> <%= $v ne '' ? (l($v) || $v) : l 'Home' %> </a></li> +% } + </ul> + </td> + + <td> + <em class=msg-count> + <%= $first_item %>-<%= $last_item %> <%= l 'of' %> <%= $total_items %> <%= l 'messages' %>\ + <%= ", $total_new_mails " . l('new') if $total_new_mails > 0; =%> + +% if ($total_size) { + - <%= ucfirst l('mbox_size') . ": " . print_sizes10 $total_size %> +% } + </em> + </td> + + </tr> +</table>
\ No newline at end of file diff --git a/templates/layouts/mainlayout.html.ep b/templates/layouts/mainlayout.html.ep new file mode 100644 index 0000000..e084578 --- /dev/null +++ b/templates/layouts/mainlayout.html.ep @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<meta charset=UTF-8> + +<html lang="<%= $lang %>"> + <head> + <title> <%= stash('title') || 'JWebmail' %> </title> + <link href="/style.css" rel=stylesheet> + </head> + + <body> + %= content + + <footer> + <a href="<%= url_for('about') %>"> + <%= ucfirst l 'about' %> JWebmail + </a> + <br/> + <%= ucfirst l 'version' %> <%= $version %> + </footer> + </body> +</html> diff --git a/templates/not_found_.html.ep b/templates/not_found_.html.ep new file mode 100644 index 0000000..d1b353f --- /dev/null +++ b/templates/not_found_.html.ep @@ -0,0 +1,16 @@ +<html> + + <head> + <title>Not Found</title> + </head> + + <body> + <p class=center> + Not the page you are looking for. + </p> + <p class=center> + Go back or go to the <a href="<%= url_for 'noaction' %>">start page</a>. + </p> + </body> + +</html>
\ No newline at end of file diff --git a/templates/webmail/about.html.ep b/templates/webmail/about.html.ep new file mode 100644 index 0000000..c6d1247 --- /dev/null +++ b/templates/webmail/about.html.ep @@ -0,0 +1,71 @@ +%# about template + +% layout 'mainlayout'; + +<table class=top> + + <tr> + <th> + About JWebmail <%= $version %> + </th> + </tr> + + <tr> + <td class=large-section> + <ul> + <li> + JWebmail <%= $version %> is a Webmail solution meant to be used with + <a href="https://www.fehcom.de/sqmail/sqmail.html">s/qmail</a> + </li> + + <li>Features: + <ul> + <!-- + <li>qmail, vmailmgr and vpopmail authentication support (<em>not</em> sendmail)</li> + <li>multiple signatures und headers support</li> + <li>basic folders support (4 defined folders)</li> + <li>featured addressbook</li> + <li>100% Maildir based</li> + <li>reads the mail directely from the server disk, without need for POP3 or IMAP</li> + --> + <li>multiple language support</li> + <li>session management </li> + <li>search for mails</li> + <li>CGI support but also psgi/plack and fcgi</li> + </ul> + </li> + + <li> + This is a + <a href="http://www.gnu.org/copyleft/gpl.html" target="_new">GPL</a> + licensed project, created by <a href="mailto:">Oliver 'omnis' Müller</a> + and currently maintained by + <a href="mailto:jannis@fehcom.de">Jannis M. Hoffmann</a> + </li> + + + <li>Supported languages: + <p class=languages> +% foreach (@$languages) { + <%= $_ %> +% } + </p> + </li> + + <li> + JWebmail is programmed in <a href="http://www.perl.org">Perl</a>, and is + a complete rewrite of oMail-webmail. + </li> + + </ul> + </td> + </tr> + + <tr> + <td> + <nav> + <a href="<%= url_for('noaction') %>" class=btn>login</a> + </nav> + </td> + </tr> +</table> diff --git a/templates/webmail/displayheaders.html.ep b/templates/webmail/displayheaders.html.ep new file mode 100644 index 0000000..d823d9e --- /dev/null +++ b/templates/webmail/displayheaders.html.ep @@ -0,0 +1,46 @@ +% layout 'mainlayout'; + +<table id=displayheaders> + + <thead> + <tr> + <td id=folders> + %= include 'headers/_displayfolders'; + </td> + </tr> + +% if (my $loginmessage = stash 'loginmessage') { + <tr> + <td id=loginmessage> <%= $loginmessage %> </td> + </tr> +% } + + <tr> + <td id=navigation> + %= include 'headers/_display_top_nav'; + </td> + </tr> + </thead> + + <tbody> + <tr> + <td> +% if (@$msgs) { + %= include 'headers/_display_headers'; +% } +% else { + <p id=empty> <%= l 'empty_folder' %> </p> +% } + </td> + </tr> + </tbody> + + <tfoot> + <tr> + <td class=navigation> + %= include 'headers/_display_bot_nav'; + </td> + </tr> + </tfoot> + +</table>
\ No newline at end of file diff --git a/templates/webmail/noaction.html.ep b/templates/webmail/noaction.html.ep new file mode 100644 index 0000000..54a8106 --- /dev/null +++ b/templates/webmail/noaction.html.ep @@ -0,0 +1,60 @@ +% layout 'mainlayout'; + +<table id=noaction> + <thead> + <tr> + <th id=top-section> JWebmail – <%= ucfirst l 'login' %> </th> + </tr> + </thead> + + <tbody> +% if (my $msg = flash('message') || stash('warning')) { + <tr> + <td class="warning flash-fade"> + %= $msg + <td> + </tr> +% } + + <tr> + <td class=login-form> + <form method=post name=login1 action="<%= url_for('login') %>"> + + <table> + <tr> + <td class=label-cell> + <label for=userid><%= ucfirst l 'userid' %></label>: + </td> + <td> + %= text_field 'userid' + </td> + </tr> + <tr> + <td class=label-cell> + <label for=password><%= ucfirst l 'passwd' %></label>: + </td> + <td> + %= password_field 'password' + </td> + </tr> + <tr class=submit-row> + <td colspan=2> + <input type=submit value="<%= l 'login' %>"> + </td> + </tr> + </table> + + </form> + </td> + </tr> + + </tbody> +</table> + +%= javascript begin + if (!document.login1.userid.value) { + document.login1.userid.focus(); + } else { + document.login1.password.focus(); + } +% end diff --git a/templates/webmail/readmail.html.ep b/templates/webmail/readmail.html.ep new file mode 100644 index 0000000..f537d96 --- /dev/null +++ b/templates/webmail/readmail.html.ep @@ -0,0 +1,52 @@ +% layout 'mainlayout'; + +% my $mail_fmt = begin + % my ($category, $value) = @_; + <dt> <%= ucfirst l $category %> </dt> + <dd> <%= ref $value ? join(' ' . l('and') . ' ', map {"$_->{name} <$_->{address}>"} @$value) : $value %> </dd> +% end + +<div class=ow> + +<h1>Read Mail</h1> + +<dl id=show-head> + <dt> <%= uc l 'subject' %> </dt> + <dd> <%= $msg->{subject} %> </dd> +%= $mail_fmt->('from', $msg->{from}); +%= $mail_fmt->('to', $msg->{to}); +%= $mail_fmt->('cc', $msg->{cc}) if !ref $msg->{cc} || @{ $msg->{cc} }; +%= $mail_fmt->('bcc', $msg->{bcc}) if !ref $msg->{bcc} || @{ $msg->{cc} }; + <dt> <%= uc l 'date' %> </dt> + <dd> <%= $msg->{date} %> </dd> + <dt> <%= uc l 'size' %> </dt> + <dd> <%= print_sizes10 $msg->{size} %> </dd> + <dt> <%= uc l 'content-type' %> </dt> + <dd> <%= $msg->{content_type} %> </dd> +</dl> + +% my $body = $msg->{body}; + +% if ($msg->{content_type} eq 'multipart/alternative') { +% for (reverse @$body) { + <div class=show-body> +% my $x = mime_render($_->{type}, $_->{val}); +%== $x; +% last if $x; + </div> +% } +% } +% elsif (ref $body eq 'HASH') { +% for (%$body) { + <div class=show-body> +%== mime_render($_->{type}, $_->{val}); + </div> +% } +% } +% else { + <div class=show-body> +%== mime_render($msg->{content_type}, $body); + </div> +% } + +</div>
\ No newline at end of file diff --git a/templates/webmail/writemail.html.ep b/templates/webmail/writemail.html.ep new file mode 100644 index 0000000..171542e --- /dev/null +++ b/templates/webmail/writemail.html.ep @@ -0,0 +1,50 @@ +% layout 'mainlayout'; + +<div class=ow> + +<h1>Write Message</h1> + +% if (my $msg = stash('warning')) { + <p class=warn> <%= $msg %> </p> +% } + +<form method=post enctype=multipart/form-data id=write-form> + + <label for=mail> <%= ucfirst l 'send_to' %> </label> + %= email_field 'to', id => 'mail', multiple => '', required => '' + <br> + + <label for=subject> <%= ucfirst l 'subject' %> </label> + %= text_field 'subject', 'required' => '' + <br> + + <label for=cc>CC</label> + %= email_field 'cc', 'multiple' => '' + <br> + + <label for=bcc>BCC</label> + %= email_field 'bcc', 'multiple' => '' + <br> + + <label for=back_to> <%= ucfirst l 'answer_to' %> </label> + %= email_field 'back_to' + <br> + + <label for=txt> <%= ucfirst l 'content' %> </label> + %= text_area 'body', cols => 80, rows => 24, id => 'txt' + <br> + + %= file_field 'attach' + <br> + + <input type=submit value="send" /> + + %= csrf_field + +</form> + +<nav> +<a href="<%= url_for('displayheaders') %>" class=btn> <%= l 'home' %> </a> +</nav> + +</div>
\ No newline at end of file |