diff --git a/Cargo.lock b/Cargo.lock index da202e15..8de44f59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,6 +24,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ "cfg-if 1.0.0", + "getrandom", "once_cell", "version_check", ] @@ -226,6 +227,30 @@ dependencies = [ "byteorder", ] +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -654,6 +679,16 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -860,6 +895,16 @@ dependencies = [ "slab", ] +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets", +] + [[package]] name = "getrandom" version = "0.2.10" @@ -1223,6 +1268,23 @@ dependencies = [ "time", ] +[[package]] +name = "mail-builder" +version = "0.3.1" +source = "git+https://github.com/stalwartlabs/mail-builder#1eb0b5a72211c491cbe338920e8dfd3a675d6653" +dependencies = [ + "gethostname", +] + +[[package]] +name = "mail-parser" +version = "0.9.0" +source = "git+https://github.com/stalwartlabs/mail-parser#e5a4e65112fd8aa4c527d37b87413d939f1259a1" +dependencies = [ + "encoding_rs", + "serde", +] + [[package]] name = "mailin" version = "0.6.3" @@ -1300,12 +1362,15 @@ dependencies = [ name = "melib" version = "0.8.1" dependencies = [ + "ahash", "async-stream", "base64 0.13.1", + "bincode", "bitflags 2.4.0", "data-encoding", "encoding", "encoding_rs", + "fancy-regex", "flate2", "futures", "imap-codec", @@ -1314,11 +1379,14 @@ dependencies = [ "libc", "libloading", "log", + "mail-builder", + "mail-parser", "mailin-embedded", "native-tls", "nix", "nom", "notify", + "phf", "polling", "regex", "rusqlite", @@ -1653,6 +1721,48 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.29", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.3" @@ -1758,6 +1868,21 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + [[package]] name = "redox_syscall" version = "0.2.16" @@ -2043,6 +2168,12 @@ dependencies = [ "libc", ] +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "slab" version = "0.4.9" diff --git a/melib/Cargo.toml b/melib/Cargo.toml index b2b3dcae..226f2760 100644 --- a/melib/Cargo.toml +++ b/melib/Cargo.toml @@ -50,10 +50,16 @@ serde_path_to_error = { version = "0.1" } smallvec = { version = "^1.5.0", features = ["serde"] } smol = "1.0.0" socket2 = { version = "0.4", features = [] } - unicode-segmentation = { version = "1.2.1", default-features = false, optional = true } uuid = { version = "^1", features = ["serde", "v4", "v5"] } xdg = "2.1.0" +# sieve +mail-parser = { version = "0.9", git = "https://github.com/stalwartlabs/mail-parser", features = ["ludicrous_mode", "full_encoding", "serde_support"] } +mail-builder = { version = "0.3", git = "https://github.com/stalwartlabs/mail-builder", features = ["ludicrous_mode"] } +phf = { version = "0.11", features = ["macros"] } +bincode = "1.3.3" +ahash = { version = "0.8.0" } +fancy-regex = "0.11.0" [dev-dependencies] mailin-embedded = { version = "0.7", features = ["rtls"] } diff --git a/melib/src/sieve/.gitignore b/melib/src/sieve/.gitignore new file mode 100644 index 00000000..96b939a8 --- /dev/null +++ b/melib/src/sieve/.gitignore @@ -0,0 +1,12 @@ +.git1 +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk +*.failed diff --git a/melib/src/sieve/CHANGELOG b/melib/src/sieve/CHANGELOG new file mode 100644 index 00000000..a0953388 --- /dev/null +++ b/melib/src/sieve/CHANGELOG @@ -0,0 +1,17 @@ +sieve-rs 0.3.1 +================================ +- Bump `mail-builder` dependency to 0.3.0. + +sieve-rs 0.3.0 +================================ +- Updated ``execute`` grammar. +- Upgraded to latest mail-parser. +- Envelope accessible from environment variables. + +sieve-rs 0.2.0 +================================ +- Improved event loop. + +sieve-rs 0.1.0 +================================ +- Initial release. diff --git a/melib/src/sieve/Cargo.toml b/melib/src/sieve/Cargo.toml new file mode 100644 index 00000000..9bb35b1c --- /dev/null +++ b/melib/src/sieve/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "sieve-rs" +description = "Sieve filter interpreter for Rust" +authors = [ "Stalwart Labs "] +repository = "https://github.com/stalwartlabs/sieve" +homepage = "https://github.com/stalwartlabs/sieve" +license = "AGPL-3.0-only" +keywords = ["sieve", "interpreter", "compiler", "email", "mail"] +categories = ["email", "compilers"] +readme = "README.md" +version = "0.3.1" +edition = "2021" + +[lib] +name = "sieve" + +[dependencies] +mail-parser = { version = "0.9", git = "https://github.com/stalwartlabs/mail-parser", features = ["ludicrous_mode", "full_encoding", "serde_support"] } +mail-builder = { version = "0.3", git = "https://github.com/stalwartlabs/mail-builder", features = ["ludicrous_mode"] } +phf = { version = "0.11", features = ["macros"] } +serde = { version = "1.0", features = ["derive"] } +bincode = "1.3.3" +ahash = { version = "0.8.0" } +fancy-regex = "0.11.0" + +[dev-dependencies] +serde_json = "1.0" +evalexpr = "11.1.0" diff --git a/melib/src/sieve/LICENSE b/melib/src/sieve/LICENSE new file mode 100644 index 00000000..0ad25db4 --- /dev/null +++ b/melib/src/sieve/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/melib/src/sieve/README.md b/melib/src/sieve/README.md new file mode 100644 index 00000000..3d11eb2f --- /dev/null +++ b/melib/src/sieve/README.md @@ -0,0 +1,244 @@ +# sieve + +[![crates.io](https://img.shields.io/crates/v/sieve-rs)](https://crates.io/crates/sieve-rs) +[![build](https://github.com/stalwartlabs/sieve/actions/workflows/rust.yml/badge.svg)](https://github.com/stalwartlabs/sieve/actions/workflows/rust.yml) +[![docs.rs](https://img.shields.io/docsrs/sieve-rs)](https://docs.rs/sieve-rs) +[![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) + +_sieve_ is a fast and secure Sieve filter interpreter for Rust that supports all [registered Sieve extensions](https://www.iana.org/assignments/sieve-extensions/sieve-extensions.xhtml). + +## Usage Example + +```rust +use sieve::{runtime::RuntimeError, Action, Compiler, Event, Input, Runtime}; + +// Sieve script to execute +let text_script = br#" +require ["fileinto", "body", "imap4flags"]; + +if body :contains "tps" { + setflag "$tps_reports"; +} + +if header :matches "List-ID" "*<*@*" { + fileinto "INBOX.lists.${2}"; stop; +} +"#; + +// Message to filter +let raw_message = r#"From: Sales Mailing List +To: John Doe +List-ID: +Subject: TPS Reports + +We're putting new coversheets on all the TPS reports before they go out now. +So if you could go ahead and try to remember to do that from now on, that'd be great. All right! +"#; + +// Compile +let compiler = Compiler::new(); +let script = compiler.compile(text_script).unwrap(); + +// Build runtime +let runtime = Runtime::new(); + +// Create filter instance +let mut instance = runtime.filter(raw_message.as_bytes()); +let mut input = Input::script("my-script", script); +let mut messages: Vec = Vec::new(); + +// Start event loop +while let Some(result) = instance.run(input) { + match result { + Ok(event) => match event { + Event::IncludeScript { name, optional } => { + // NOTE: Just for demonstration purposes, script name needs to be validated first. + if let Ok(bytes) = std::fs::read(name.as_str()) { + let script = compiler.compile(&bytes).unwrap(); + input = Input::script(name, script); + } else if optional { + input = Input::False; + } else { + panic!("Script {} not found.", name); + } + } + Event::MailboxExists { .. } => { + // Set to true if the mailbox exists + input = false.into(); + } + Event::ListContains { .. } => { + // Set to true if the list(s) contains an entry + input = false.into(); + } + Event::DuplicateId { .. } => { + // Set to true if the ID is duplicate + input = false.into(); + } + Event::Execute { command, arguments } => { + println!( + "Script executed command {:?} with parameters {:?}", + command, arguments + ); + // Set to true if the script succeeded + input = false.into(); + } + + Event::Keep { flags, message_id } => { + println!( + "Keep message '{}' with flags {:?}.", + if message_id > 0 { + messages[message_id - 1].as_str() + } else { + raw_message + }, + flags + ); + input = true.into(); + } + Event::Discard => { + println!("Discard message."); + input = true.into(); + } + Event::Reject { reason, .. } => { + println!("Reject message with reason {:?}.", reason); + input = true.into(); + } + Event::FileInto { + folder, + flags, + message_id, + .. + } => { + println!( + "File message '{}' in folder {:?} with flags {:?}.", + if message_id > 0 { + messages[message_id - 1].as_str() + } else { + raw_message + }, + folder, + flags + ); + input = true.into(); + } + Event::SendMessage { + recipient, + message_id, + .. + } => { + println!( + "Send message '{}' to {:?}.", + if message_id > 0 { + messages[message_id - 1].as_str() + } else { + raw_message + }, + recipient + ); + input = true.into(); + } + Event::Notify { + message, method, .. + } => { + println!("Notify URI {:?} with message {:?}", method, message); + input = true.into(); + } + Event::CreatedMessage { message, .. } => { + messages.push(String::from_utf8(message).unwrap()); + input = true.into(); + } + + #[cfg(test)] + _ => unreachable!(), + }, + Err(error) => { + match error { + RuntimeError::TooManyIncludes => { + eprintln!("Too many included scripts."); + } + RuntimeError::InvalidInstruction(instruction) => { + eprintln!( + "Invalid instruction {:?} found at {}:{}.", + instruction.name(), + instruction.line_num(), + instruction.line_pos() + ); + } + RuntimeError::ScriptErrorMessage(message) => { + eprintln!("Script called the 'error' function with {:?}", message); + } + RuntimeError::CapabilityNotAllowed(capability) => { + eprintln!( + "Capability {:?} has been disabled by the administrator.", + capability + ); + } + RuntimeError::CapabilityNotSupported(capability) => { + eprintln!("Capability {:?} not supported.", capability); + } + RuntimeError::CPULimitReached => { + eprintln!("Script exceeded the configured CPU limit."); + } + } + input = true.into(); + } + } +} +``` + +## Testing & Fuzzing + +To run the testsuite: + +```bash + $ cargo test --all-features +``` + +To fuzz the library with `cargo-fuzz`: + +```bash + $ cargo +nightly fuzz run sieve +``` + +## Conformed RFCs + +- [RFC 5228 - Sieve: An Email Filtering Language](https://datatracker.ietf.org/doc/html/rfc5228) +- [RFC 3894 - Copying Without Side Effects](https://datatracker.ietf.org/doc/html/rfc3894) +- [RFC 5173 - Body Extension](https://datatracker.ietf.org/doc/html/rfc5173) +- [RFC 5183 - Environment Extension](https://datatracker.ietf.org/doc/html/rfc5183) +- [RFC 5229 - Variables Extension](https://datatracker.ietf.org/doc/html/rfc5229) +- [RFC 5230 - Vacation Extension](https://datatracker.ietf.org/doc/html/rfc5230) +- [RFC 5231 - Relational Extension](https://datatracker.ietf.org/doc/html/rfc5231) +- [RFC 5232 - Imap4flags Extension](https://datatracker.ietf.org/doc/html/rfc5232) +- [RFC 5233 - Subaddress Extension](https://datatracker.ietf.org/doc/html/rfc5233) +- [RFC 5235 - Spamtest and Virustest Extensions](https://datatracker.ietf.org/doc/html/rfc5235) +- [RFC 5260 - Date and Index Extensions](https://datatracker.ietf.org/doc/html/rfc5260) +- [RFC 5293 - Editheader Extension](https://datatracker.ietf.org/doc/html/rfc5293) +- [RFC 5429 - Reject and Extended Reject Extensions](https://datatracker.ietf.org/doc/html/rfc5429) +- [RFC 5435 - Extension for Notifications](https://datatracker.ietf.org/doc/html/rfc5435) +- [RFC 5463 - Ihave Extension](https://datatracker.ietf.org/doc/html/rfc5463) +- [RFC 5490 - Extensions for Checking Mailbox Status and Accessing Mailbox Metadata](https://datatracker.ietf.org/doc/html/rfc5490) +- [RFC 5703 - MIME Part Tests, Iteration, Extraction, Replacement, and Enclosure](https://datatracker.ietf.org/doc/html/rfc5703) +- [RFC 6009 - Delivery Status Notifications and Deliver-By Extensions](https://datatracker.ietf.org/doc/html/rfc6009) +- [RFC 6131 - Sieve Vacation Extension: "Seconds" Parameter](https://datatracker.ietf.org/doc/html/rfc6131) +- [RFC 6134 - Externally Stored Lists](https://datatracker.ietf.org/doc/html/rfc6134) +- [RFC 6558 - Converting Messages before Delivery](https://datatracker.ietf.org/doc/html/rfc6558) +- [RFC 6609 - Include Extension](https://datatracker.ietf.org/doc/html/rfc6609) +- [RFC 7352 - Detecting Duplicate Deliveries](https://datatracker.ietf.org/doc/html/rfc7352) +- [RFC 8579 - Delivering to Special-Use Mailboxes](https://datatracker.ietf.org/doc/html/rfc8579) +- [RFC 8580 - File Carbon Copy (FCC)](https://datatracker.ietf.org/doc/html/rfc8580) +- [RFC 9042 - Delivery by MAILBOXID](https://datatracker.ietf.org/doc/html/rfc9042) +- [REGEX-01 - Regular Expression Extension (draft-ietf-sieve-regex-01)](https://www.ietf.org/archive/id/draft-ietf-sieve-regex-01.html) + +## License + +Licensed under the terms of the [GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.en.html) as published by +the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +See [LICENSE](LICENSE) for more details. + +You can be released from the requirements of the AGPLv3 license by purchasing +a commercial license. Please contact licensing@stalw.art for more details. + +## Copyright + +Copyright (C) 2020-2023, Stalwart Labs Ltd. diff --git a/melib/src/sieve/compiler/grammar/actions/action_convert.rs b/melib/src/sieve/compiler/grammar/actions/action_convert.rs new file mode 100644 index 00000000..3033f619 --- /dev/null +++ b/melib/src/sieve/compiler/grammar/actions/action_convert.rs @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use serde::{Deserialize, Serialize}; + +use crate::sieve::compiler::{ + grammar::{ + instruction::{CompilerState, Instruction}, + test::Test, + }, + CompileError, Value, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct Convert { + pub from_media_type: Value, + pub to_media_type: Value, + pub transcoding_params: Vec, + pub is_not: bool, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_test_convert(&mut self) -> Result { + Ok(Test::Convert(Convert { + from_media_type: self.parse_string()?, + to_media_type: self.parse_string()?, + transcoding_params: self.parse_strings(false)?, + is_not: false, + })) + } + + pub(crate) fn parse_convert(&mut self) -> Result<(), CompileError> { + let cmd = Instruction::Convert(Convert { + from_media_type: self.parse_string()?, + to_media_type: self.parse_string()?, + transcoding_params: self.parse_strings(false)?, + is_not: false, + }); + self.instructions.push(cmd); + Ok(()) + } +} diff --git a/melib/src/sieve/compiler/grammar/actions/action_editheader.rs b/melib/src/sieve/compiler/grammar/actions/action_editheader.rs new file mode 100644 index 00000000..31839946 --- /dev/null +++ b/melib/src/sieve/compiler/grammar/actions/action_editheader.rs @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use mail_parser::HeaderName; +use serde::{Deserialize, Serialize}; + +use crate::sieve::compiler::{ + grammar::{ + instruction::{CompilerState, Instruction}, + Capability, Comparator, + }, + lexer::{word::Word, Token}, + CompileError, ErrorType, Value, +}; + +use crate::sieve::compiler::grammar::MatchType; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct AddHeader { + pub last: bool, + pub field_name: Value, + pub value: Value, +} + +/* + Usage: "deleteheader" [":index" [":last"]] + [COMPARATOR] [MATCH-TYPE] + + [] + +*/ +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct DeleteHeader { + pub index: Option, + pub comparator: Comparator, + pub match_type: MatchType, + pub field_name: Value, + pub value_patterns: Vec, + pub mime_anychild: bool, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_addheader(&mut self) -> Result<(), CompileError> { + let mut field_name = None; + let value; + let mut last = false; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag(Word::Last) => { + self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?; + last = true; + } + _ => { + let string = self.parse_string_token(token_info)?; + if field_name.is_none() { + if let Value::Text(header_name) = &string { + if HeaderName::parse(header_name).is_none() { + return Err(self + .tokens + .unwrap_next()? + .custom(ErrorType::InvalidHeaderName)); + } + } + + field_name = string.into(); + } else { + if matches!( + &string, + Value::Text(value) if value.len() > self.compiler.max_header_size + ) { + return Err(self + .tokens + .unwrap_next()? + .custom(ErrorType::HeaderTooLong)); + } + value = string; + break; + } + } + } + } + self.instructions.push(Instruction::AddHeader(AddHeader { + last, + field_name: field_name.unwrap(), + value, + })); + Ok(()) + } + + pub(crate) fn parse_deleteheader(&mut self) -> Result<(), CompileError> { + let field_name: Value; + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let mut index = None; + let mut index_last = false; + let mut mime = false; + let mut mime_anychild = false; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex), + ) => { + self.validate_argument( + 1, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + comparator = self.parse_comparator()?; + } + Token::Tag(Word::Index) => { + self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?; + index = (self.tokens.expect_number(u16::MAX as usize)? as i32).into(); + } + Token::Tag(Word::Last) => { + self.validate_argument(4, None, token_info.line_num, token_info.line_pos)?; + index_last = true; + } + Token::Tag(Word::Mime) => { + self.validate_argument( + 5, + Capability::Mime.into(), + token_info.line_num, + token_info.line_pos, + )?; + mime = true; + } + Token::Tag(Word::AnyChild) => { + self.validate_argument( + 6, + Capability::Mime.into(), + token_info.line_num, + token_info.line_pos, + )?; + mime_anychild = true; + } + _ => { + field_name = self.parse_string_token(token_info)?; + if let Value::Text(header_name) = &field_name { + if HeaderName::parse(header_name).is_none() { + return Err(self + .tokens + .unwrap_next()? + .custom(ErrorType::InvalidHeaderName)); + } + } + break; + } + } + } + + if !mime && mime_anychild { + return Err(self.tokens.unwrap_next()?.missing_tag(":mime")); + } + + let cmd = Instruction::DeleteHeader(DeleteHeader { + index: if index_last { index.map(|i| -i) } else { index }, + comparator, + match_type, + field_name, + value_patterns: if let Some(Ok( + Token::StringConstant(_) | Token::StringVariable(_) | Token::BracketOpen, + )) = self.tokens.peek().map(|r| r.map(|t| &t.token)) + { + let mut key_list = self.parse_strings(false)?; + self.validate_match(&match_type, &mut key_list)?; + key_list + } else { + Vec::new() + }, + mime_anychild, + }); + self.instructions.push(cmd); + Ok(()) + } +} diff --git a/melib/src/sieve/compiler/grammar/actions/action_fileinto.rs b/melib/src/sieve/compiler/grammar/actions/action_fileinto.rs new file mode 100644 index 00000000..03204bfa --- /dev/null +++ b/melib/src/sieve/compiler/grammar/actions/action_fileinto.rs @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use serde::{Deserialize, Serialize}; + +use crate::sieve::compiler::{ + grammar::{ + instruction::{CompilerState, Instruction}, + Capability, + }, + lexer::{word::Word, Token}, + CompileError, Value, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct FileInto { + pub copy: bool, + pub create: bool, + pub folder: Value, + pub flags: Vec, + pub mailbox_id: Option, + pub special_use: Option, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_fileinto(&mut self) -> Result<(), CompileError> { + let folder; + let mut copy = false; + let mut create = false; + let mut flags = Vec::new(); + let mut mailbox_id = None; + let mut special_use = None; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag(Word::Copy) => { + self.validate_argument( + 1, + Capability::Copy.into(), + token_info.line_num, + token_info.line_pos, + )?; + copy = true; + } + Token::Tag(Word::Create) => { + self.validate_argument( + 2, + Capability::Mailbox.into(), + token_info.line_num, + token_info.line_pos, + )?; + create = true; + } + Token::Tag(Word::Flags) => { + self.validate_argument( + 3, + Capability::Imap4Flags.into(), + token_info.line_num, + token_info.line_pos, + )?; + flags = self.parse_strings(false)?; + } + Token::Tag(Word::MailboxId) => { + self.validate_argument( + 4, + Capability::Mailbox.into(), + token_info.line_num, + token_info.line_pos, + )?; + mailbox_id = self.parse_string()?.into(); + } + Token::Tag(Word::SpecialUse) => { + self.validate_argument( + 5, + Capability::SpecialUse.into(), + token_info.line_num, + token_info.line_pos, + )?; + special_use = self.parse_string()?.into(); + } + _ => { + folder = self.parse_string_token(token_info)?; + break; + } + } + } + + self.instructions.push(Instruction::FileInto(FileInto { + folder, + copy, + create, + flags, + mailbox_id, + special_use, + })); + Ok(()) + } +} diff --git a/melib/src/sieve/compiler/grammar/actions/action_flags.rs b/melib/src/sieve/compiler/grammar/actions/action_flags.rs new file mode 100644 index 00000000..f3fcf60f --- /dev/null +++ b/melib/src/sieve/compiler/grammar/actions/action_flags.rs @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use serde::{Deserialize, Serialize}; + +use crate::sieve::compiler::{ + grammar::instruction::{CompilerState, Instruction}, + lexer::{word::Word, Token}, + CompileError, ErrorType, Value, VariableType, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct EditFlags { + pub action: Action, + pub name: Option, + pub flags: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) enum Action { + Set, + Add, + Remove, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_flag_action(&mut self, word: Word) -> Result<(), CompileError> { + let token_info = self.tokens.unwrap_next()?; + let action = match word { + Word::SetFlag => Action::Set, + Word::AddFlag => Action::Add, + Word::RemoveFlag => Action::Remove, + _ => unreachable!(), + }; + + let instruction = Instruction::EditFlags( + match ( + &token_info.token, + self.tokens.peek().map(|r| r.map(|t| &t.token)), + ) { + ( + Token::StringConstant(_), + Some(Ok( + Token::StringConstant(_) | Token::StringVariable(_) | Token::BracketOpen, + )), + ) => EditFlags { + name: self.parse_variable_name(token_info, false)?.into(), + flags: self.parse_strings(false)?, + action, + }, + (Token::BracketOpen, _) + | ( + Token::StringConstant(_) | Token::StringVariable(_), + Some(Ok(Token::Semicolon)), + ) => EditFlags { + name: None, + flags: self.parse_strings_token(token_info)?, + action, + }, + _ => { + return Err(token_info.custom(ErrorType::InvalidArguments)); + } + }, + ); + + self.instructions.push(instruction); + + Ok(()) + } +} diff --git a/melib/src/sieve/compiler/grammar/actions/action_include.rs b/melib/src/sieve/compiler/grammar/actions/action_include.rs new file mode 100644 index 00000000..f4f51ea5 --- /dev/null +++ b/melib/src/sieve/compiler/grammar/actions/action_include.rs @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use serde::{Deserialize, Serialize}; + +use crate::sieve::compiler::{ + grammar::instruction::{CompilerState, Instruction}, + lexer::{word::Word, Token}, + CompileError, Value, +}; + +/* + +include [LOCATION] [":once"] [":optional"] + LOCATION = ":personal" / ":global" + +*/ + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct Include { + pub location: Location, + pub once: bool, + pub optional: bool, + pub value: Value, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) enum Location { + Personal, + Global, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_include(&mut self) -> Result<(), CompileError> { + let value; + let mut once = false; + let mut optional = false; + let mut location = Location::Personal; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag(Word::Once) => { + self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?; + once = true; + } + Token::Tag(Word::Optional) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + optional = true; + } + Token::Tag(Word::Personal) => { + self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?; + location = Location::Personal; + } + Token::Tag(Word::Global) => { + self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?; + location = Location::Global; + } + _ => { + value = self.parse_string_token(token_info)?; + break; + } + } + } + + self.instructions.push(Instruction::Include(Include { + location, + once, + optional, + value, + })); + Ok(()) + } +} diff --git a/melib/src/sieve/compiler/grammar/actions/action_keep.rs b/melib/src/sieve/compiler/grammar/actions/action_keep.rs new file mode 100644 index 00000000..64deeb7a --- /dev/null +++ b/melib/src/sieve/compiler/grammar/actions/action_keep.rs @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use serde::{Deserialize, Serialize}; + +use crate::sieve::compiler::{ + grammar::{ + instruction::{CompilerState, Instruction}, + Capability, + }, + lexer::{word::Word, Token}, + CompileError, Value, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct Keep { + pub flags: Vec, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_keep(&mut self) -> Result<(), CompileError> { + let cmd = Instruction::Keep(Keep { + flags: match self.tokens.peek().map(|r| r.map(|t| &t.token)) { + Some(Ok(Token::Tag(Word::Flags))) => { + let token_info = self.tokens.next().unwrap().unwrap(); + self.validate_argument( + 0, + Capability::Imap4Flags.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_strings(false)? + } + _ => Vec::new(), + }, + }); + self.instructions.push(cmd); + Ok(()) + } +} diff --git a/melib/src/sieve/compiler/grammar/actions/action_mime.rs b/melib/src/sieve/compiler/grammar/actions/action_mime.rs new file mode 100644 index 00000000..2eea5b3f --- /dev/null +++ b/melib/src/sieve/compiler/grammar/actions/action_mime.rs @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use serde::{Deserialize, Serialize}; + +use crate::sieve::compiler::{ + grammar::instruction::{CompilerState, Instruction}, + lexer::{word::Word, Token}, + CompileError, Value, VariableType, +}; + +use super::action_set::Modifier; + +#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] +pub(crate) struct ForEveryPart { + pub jz_pos: usize, +} + +#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] +pub(crate) struct Replace { + pub subject: Option, + pub from: Option, + pub replacement: Value, + pub mime: bool, +} + +#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] +pub(crate) struct Enclose { + pub subject: Option, + pub headers: Vec, + pub value: Value, +} + +#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] +pub(crate) struct ExtractText { + pub modifiers: Vec, + pub first: Option, + pub name: VariableType, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) enum MimeOpts { + Type, + Subtype, + ContentType, + Param(Vec), + None, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_replace(&mut self) -> Result<(), CompileError> { + let mut subject = None; + let mut from = None; + let replacement; + let mut mime = false; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag(Word::Mime) => { + self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?; + mime = true; + } + Token::Tag(Word::Subject) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + subject = self.parse_string()?.into(); + } + Token::Tag(Word::From) => { + self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?; + from = self.parse_string()?.into(); + } + _ => { + replacement = self.parse_string_token(token_info)?; + break; + } + } + } + + self.instructions.push(Instruction::Replace(Replace { + subject, + from, + replacement, + mime, + })); + Ok(()) + } + + pub(crate) fn parse_enclose(&mut self) -> Result<(), CompileError> { + let mut subject = None; + let mut headers = Vec::new(); + let value; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag(Word::Subject) => { + self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?; + subject = self.parse_string()?.into(); + } + Token::Tag(Word::Headers) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + headers = self.parse_strings(false)?; + } + _ => { + value = self.parse_string_token(token_info)?; + break; + } + } + } + + self.instructions.push(Instruction::Enclose(Enclose { + subject, + headers, + value, + })); + Ok(()) + } + + pub(crate) fn parse_extracttext(&mut self) -> Result<(), CompileError> { + let mut modifiers = Vec::new(); + let mut first = None; + let name; + let mut is_local = false; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag(Word::First) => { + self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?; + first = self.tokens.expect_number(usize::MAX)?.into(); + } + Token::Tag( + word @ (Word::Lower + | Word::Upper + | Word::LowerFirst + | Word::UpperFirst + | Word::QuoteWildcard + | Word::QuoteRegex + | Word::Length), + ) => { + let modifier = word.into(); + if !modifiers.contains(&modifier) { + modifiers.push(modifier); + } + } + Token::Tag(Word::Replace) => { + let find = self.tokens.unwrap_next()?; + let replace = self.tokens.unwrap_next()?; + modifiers.push(Modifier::Replace { + find: self.parse_string_token(find)?, + replace: self.parse_string_token(replace)?, + }); + } + Token::Tag(Word::Local) => { + is_local = true; + } + _ => { + name = self.parse_variable_name(token_info, is_local)?; + break; + } + } + } + + modifiers.sort_unstable_by_key(|m| std::cmp::Reverse(m.order())); + + self.instructions + .push(Instruction::ExtractText(ExtractText { + modifiers, + first, + name, + })); + Ok(()) + } + + pub(crate) fn parse_mimeopts(&mut self, opts: Word) -> Result, CompileError> { + Ok(match opts { + Word::Type => MimeOpts::Type, + Word::Subtype => MimeOpts::Subtype, + Word::ContentType => MimeOpts::ContentType, + Word::Param => MimeOpts::Param(self.parse_strings(false)?), + _ => MimeOpts::None, + }) + } +} diff --git a/melib/src/sieve/compiler/grammar/actions/action_notify.rs b/melib/src/sieve/compiler/grammar/actions/action_notify.rs new file mode 100644 index 00000000..b3585021 --- /dev/null +++ b/melib/src/sieve/compiler/grammar/actions/action_notify.rs @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use serde::{Deserialize, Serialize}; + +use crate::sieve::{ + compiler::{ + grammar::{ + instruction::{CompilerState, Instruction, MapLocalVars}, + Capability, + }, + lexer::{word::Word, Token}, + CompileError, ErrorType, Value, + }, + runtime::actions::action_notify::{validate_from, validate_uri}, + FileCarbonCopy, +}; + +/* +notify [":from" string] + [":importance" <"1" / "2" / "3">] + [":options" string-list] + [":message" string] + + +*/ + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct Notify { + pub from: Option, + pub importance: Option, + pub options: Vec, + pub message: Option, + pub fcc: Option>, + pub method: Value, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_notify(&mut self) -> Result<(), CompileError> { + let method; + let mut from = None; + let mut importance = None; + let mut message = None; + let mut options = Vec::new(); + + let mut fcc = None; + let mut create = false; + let mut flags = Vec::new(); + let mut special_use = None; + let mut mailbox_id = None; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag(Word::From) => { + self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?; + let address = self.parse_string()?; + if let Value::Text(address) = &address { + if address.is_empty() || !validate_from(address) { + return Err(token_info.custom(ErrorType::InvalidAddress)); + } + } + from = address.into(); + } + Token::Tag(Word::Message) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + message = self.parse_string()?.into(); + } + Token::Tag(Word::Importance) => { + self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?; + importance = self.parse_string()?.into(); + } + Token::Tag(Word::Options) => { + self.validate_argument(4, None, token_info.line_num, token_info.line_pos)?; + options = self.parse_strings(false)?; + } + Token::Tag(Word::Create) => { + self.validate_argument( + 5, + Capability::Mailbox.into(), + token_info.line_num, + token_info.line_pos, + )?; + create = true; + } + Token::Tag(Word::SpecialUse) => { + self.validate_argument( + 6, + Capability::SpecialUse.into(), + token_info.line_num, + token_info.line_pos, + )?; + special_use = self.parse_string()?.into(); + } + Token::Tag(Word::MailboxId) => { + self.validate_argument( + 7, + Capability::MailboxId.into(), + token_info.line_num, + token_info.line_pos, + )?; + mailbox_id = self.parse_string()?.into(); + } + Token::Tag(Word::Fcc) => { + self.validate_argument( + 8, + Capability::Fcc.into(), + token_info.line_num, + token_info.line_pos, + )?; + fcc = self.parse_string()?.into(); + } + Token::Tag(Word::Flags) => { + self.validate_argument( + 9, + Capability::Imap4Flags.into(), + token_info.line_num, + token_info.line_pos, + )?; + flags = self.parse_strings(false)?; + } + _ => { + if let Token::StringConstant(uri) = &token_info.token { + if validate_uri(uri.to_string().as_ref()).is_none() { + return Err(token_info.custom(ErrorType::InvalidURI)); + } + } + + method = self.parse_string_token(token_info)?; + break; + } + } + } + + if fcc.is_none() + && (create || !flags.is_empty() || special_use.is_some() || mailbox_id.is_some()) + { + return Err(self.tokens.unwrap_next()?.missing_tag(":fcc")); + } + + self.instructions.push(Instruction::Notify(Notify { + method, + from, + importance, + options, + message, + fcc: if let Some(fcc) = fcc { + FileCarbonCopy { + mailbox: fcc, + create, + flags, + special_use, + mailbox_id, + } + .into() + } else { + None + }, + })); + Ok(()) + } +} + +impl MapLocalVars for FileCarbonCopy { + fn map_local_vars(&mut self, last_id: usize) { + self.mailbox.map_local_vars(last_id); + self.mailbox_id.map_local_vars(last_id); + self.flags.map_local_vars(last_id); + self.special_use.map_local_vars(last_id); + } +} diff --git a/melib/src/sieve/compiler/grammar/actions/action_redirect.rs b/melib/src/sieve/compiler/grammar/actions/action_redirect.rs new file mode 100644 index 00000000..e04c870a --- /dev/null +++ b/melib/src/sieve/compiler/grammar/actions/action_redirect.rs @@ -0,0 +1,259 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use serde::{Deserialize, Serialize}; + +use crate::sieve::compiler::{ + grammar::{ + instruction::{CompilerState, Instruction, MapLocalVars}, + Capability, + }, + lexer::{word::Word, Token}, + CompileError, Value, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct Redirect { + pub copy: bool, + pub address: Value, + pub notify: Notify, + pub return_of_content: Ret, + pub by_time: ByTime, + pub list: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +pub enum NotifyItem { + Success, + Failure, + Delay, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +pub enum Notify { + Never, + Items(Vec), + Default, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +pub enum Ret { + Full, + Hdrs, + Default, +} + +/* + + Usage: redirect [:bytimerelative / + :bytimeabsolute + [:bymode "notify"|"return"] [:bytrace]] + + +*/ + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +pub enum ByTime { + Relative { + rlimit: u64, + mode: ByMode, + trace: bool, + }, + Absolute { + alimit: T, + mode: ByMode, + trace: bool, + }, + None, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +pub enum ByMode { + Notify, + Return, + Default, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_redirect(&mut self) -> Result<(), CompileError> { + let address; + let mut copy = false; + let mut ret = Ret::Default; + let mut notify = Notify::Default; + let mut list = false; + let mut by_mode = ByMode::Default; + let mut by_trace = false; + let mut by_rlimit = None; + let mut by_alimit = None; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag(Word::Copy) => { + self.validate_argument( + 1, + Capability::Copy.into(), + token_info.line_num, + token_info.line_pos, + )?; + copy = true; + } + Token::Tag(Word::List) => { + self.validate_argument( + 2, + Capability::ExtLists.into(), + token_info.line_num, + token_info.line_pos, + )?; + list = true; + } + Token::Tag(Word::ByTrace) => { + self.validate_argument( + 3, + Capability::RedirectDeliverBy.into(), + token_info.line_num, + token_info.line_pos, + )?; + by_trace = true; + } + Token::Tag(Word::ByMode) => { + self.validate_argument( + 4, + Capability::RedirectDeliverBy.into(), + token_info.line_num, + token_info.line_pos, + )?; + let by_mode_ = self.tokens.expect_static_string()?; + if by_mode_.eq_ignore_ascii_case("notify") { + by_mode = ByMode::Notify; + } else if by_mode_.eq_ignore_ascii_case("return") { + by_mode = ByMode::Return; + } else { + return Err(token_info.expected("\"notify\" or \"return\"")); + } + } + Token::Tag(Word::ByTimeRelative) => { + self.validate_argument( + 5, + Capability::RedirectDeliverBy.into(), + token_info.line_num, + token_info.line_pos, + )?; + by_rlimit = (self.tokens.expect_number(u64::MAX as usize)? as u64).into(); + } + Token::Tag(Word::ByTimeAbsolute) => { + self.validate_argument( + 5, + Capability::RedirectDeliverBy.into(), + token_info.line_num, + token_info.line_pos, + )?; + by_alimit = self.parse_string()?.into(); + } + Token::Tag(Word::Ret) => { + self.validate_argument( + 6, + Capability::RedirectDsn.into(), + token_info.line_num, + token_info.line_pos, + )?; + let ret_ = self.tokens.expect_static_string()?; + if ret_.eq_ignore_ascii_case("full") { + ret = Ret::Full; + } else if ret_.eq_ignore_ascii_case("hdrs") { + ret = Ret::Hdrs; + } else { + return Err(token_info.expected("\"FULL\" or \"HDRS\"")); + } + } + Token::Tag(Word::Notify) => { + self.validate_argument( + 7, + Capability::RedirectDsn.into(), + token_info.line_num, + token_info.line_pos, + )?; + let notify_ = self.tokens.expect_static_string()?; + if notify_.eq_ignore_ascii_case("never") { + notify = Notify::Never; + } else { + let mut items = Vec::new(); + for item in notify_.split(',') { + let item = item.trim(); + if item.eq_ignore_ascii_case("success") { + items.push(NotifyItem::Success); + } else if item.eq_ignore_ascii_case("failure") { + items.push(NotifyItem::Failure); + } else if item.eq_ignore_ascii_case("delay") { + items.push(NotifyItem::Delay); + } + } + if !items.is_empty() { + notify = Notify::Items(items); + } else { + return Err( + token_info.expected("\"NEVER\" or \"SUCCESS, FAILURE, DELAY, ..\"") + ); + } + } + } + _ => { + address = self.parse_string_token(token_info)?; + break; + } + } + } + + self.instructions.push(Instruction::Redirect(Redirect { + address, + copy, + notify, + return_of_content: ret, + by_time: if let Some(alimit) = by_alimit { + ByTime::Absolute { + alimit, + mode: by_mode, + trace: by_trace, + } + } else if let Some(rlimit) = by_rlimit { + ByTime::Relative { + rlimit, + mode: by_mode, + trace: by_trace, + } + } else { + ByTime::None + }, + list, + })); + Ok(()) + } +} + +impl MapLocalVars for ByTime { + fn map_local_vars(&mut self, last_id: usize) { + if let ByTime::Absolute { alimit, .. } = self { + alimit.map_local_vars(last_id) + } + } +} diff --git a/melib/src/sieve/compiler/grammar/actions/action_reject.rs b/melib/src/sieve/compiler/grammar/actions/action_reject.rs new file mode 100644 index 00000000..e0a0d0f9 --- /dev/null +++ b/melib/src/sieve/compiler/grammar/actions/action_reject.rs @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use serde::{Deserialize, Serialize}; + +use crate::sieve::compiler::{ + grammar::instruction::{CompilerState, Instruction}, + CompileError, Value, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct Reject { + pub ereject: bool, + pub reason: Value, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_reject(&mut self, ereject: bool) -> Result<(), CompileError> { + let cmd = Instruction::Reject(Reject { + ereject, + reason: self.parse_string()?, + }); + self.instructions.push(cmd); + Ok(()) + } +} diff --git a/melib/src/sieve/compiler/grammar/actions/action_require.rs b/melib/src/sieve/compiler/grammar/actions/action_require.rs new file mode 100644 index 00000000..ca691d5f --- /dev/null +++ b/melib/src/sieve/compiler/grammar/actions/action_require.rs @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use crate::sieve::compiler::{ + grammar::{ + instruction::{CompilerState, Instruction}, + Capability, + }, + lexer::Token, + CompileError, +}; + +impl<'x> CompilerState<'x> { + fn add_capability(&mut self, capabilities: &mut Vec, capability: Capability) { + if !self.has_capability(&capability) { + let parent_capability = if matches!(&capability, Capability::SpamTestPlus) { + Some(Capability::SpamTest) + } else { + None + }; + capabilities.push(capability.clone()); + self.block.capabilities.insert(capability); + + if let Some(capability) = parent_capability { + if !self.has_capability(&capability) { + capabilities.push(capability.clone()); + self.block.capabilities.insert(capability); + } + } + } + } + + pub(crate) fn parse_require(&mut self) -> Result<(), CompileError> { + let mut capabilities = Vec::new(); + + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::BracketOpen => loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::StringConstant(value) => { + self.add_capability( + &mut capabilities, + Capability::parse(value.to_string().as_ref()), + ); + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Comma => (), + Token::BracketClose => break, + _ => { + return Err(token_info.expected("']' or ','")); + } + } + } + _ => { + return Err(token_info.expected("string")); + } + } + }, + Token::StringConstant(value) => { + self.add_capability( + &mut capabilities, + Capability::parse(value.to_string().as_ref()), + ); + } + _ => { + return Err(token_info.expected("'[' or string")); + } + } + + if !capabilities.is_empty() { + if self.block.require_pos == usize::MAX { + self.block.require_pos = self.instructions.len(); + self.instructions.push(Instruction::Require(capabilities)); + } else if let Some(Instruction::Require(capabilties)) = + self.instructions.get_mut(self.block.require_pos) + { + capabilties.extend(capabilities) + } else { + #[cfg(test)] + panic!( + "Invalid require instruction position {}.", + self.block.require_pos + ) + } + } + + Ok(()) + } +} diff --git a/melib/src/sieve/compiler/grammar/actions/action_set.rs b/melib/src/sieve/compiler/grammar/actions/action_set.rs new file mode 100644 index 00000000..4d415c5d --- /dev/null +++ b/melib/src/sieve/compiler/grammar/actions/action_set.rs @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use serde::{Deserialize, Serialize}; +use std::convert::TryFrom; + +use crate::sieve::{ + compiler::{ + grammar::instruction::{CompilerState, Instruction}, + lexer::{tokenizer::TokenInfo, word::Word, Token}, + CompileError, ErrorType, Value, VariableType, + }, + Envelope, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) enum Modifier { + Lower, + Upper, + LowerFirst, + UpperFirst, + QuoteWildcard, + QuoteRegex, + EncodeUrl, + Length, + Replace { find: Value, replace: Value }, +} + +impl Modifier { + pub fn order(&self) -> usize { + match self { + Modifier::Lower => 41, + Modifier::Upper => 40, + Modifier::LowerFirst => 31, + Modifier::UpperFirst => 30, + Modifier::QuoteWildcard => 20, + Modifier::QuoteRegex => 21, + Modifier::EncodeUrl => 15, + Modifier::Length => 10, + Modifier::Replace { .. } => 40, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct Set { + pub modifiers: Vec, + pub name: VariableType, + pub value: Value, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_set(&mut self) -> Result<(), CompileError> { + let mut modifiers = Vec::new(); + let mut name = None; + let mut is_local = false; + let value; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::Lower + | Word::Upper + | Word::LowerFirst + | Word::UpperFirst + | Word::QuoteWildcard + | Word::QuoteRegex + | Word::Length + | Word::EncodeUrl), + ) => { + let modifier = word.into(); + if !modifiers.contains(&modifier) { + modifiers.push(modifier); + } + } + Token::Tag(Word::Replace) => { + let find = self.tokens.unwrap_next()?; + let replace = self.tokens.unwrap_next()?; + modifiers.push(Modifier::Replace { + find: self.parse_string_token(find)?, + replace: self.parse_string_token(replace)?, + }); + } + Token::Tag(Word::Local) => { + is_local = true; + } + _ => { + if name.is_none() { + name = self.parse_variable_name(token_info, is_local)?.into(); + } else { + value = self.parse_string_token(token_info)?; + break; + } + } + } + } + + modifiers.sort_unstable_by_key(|m| std::cmp::Reverse(m.order())); + + self.instructions.push(Instruction::Set(Set { + modifiers, + name: name.unwrap(), + value, + })); + Ok(()) + } + + pub(crate) fn parse_variable_name( + &mut self, + token_info: TokenInfo, + register_as_local: bool, + ) -> Result { + match token_info.token { + Token::StringConstant(value) => self + .register_variable(value.into_string(), register_as_local) + .map_err(|error_type| CompileError { + line_num: token_info.line_num, + line_pos: token_info.line_pos, + error_type, + }), + _ => Err(token_info.custom(ErrorType::ExpectedConstantString)), + } + } + + pub(crate) fn register_variable( + &mut self, + name: String, + register_as_local: bool, + ) -> Result { + let name = name.to_lowercase(); + if let Some((namespace, part)) = name.split_once('.') { + match namespace { + "global" | "t" => Ok(VariableType::Global(part.to_string())), + "envelope" => Envelope::try_from(part) + .map(VariableType::Envelope) + .map_err(|_| ErrorType::InvalidNamespace(namespace.to_string())), + _ => Err(ErrorType::InvalidNamespace(namespace.to_string())), + } + } else { + Ok(if !self.is_var_global(&name) { + VariableType::Local(self.register_local_var(name, register_as_local)) + } else { + VariableType::Global(name) + }) + } + } +} + +impl From for Modifier { + fn from(word: Word) -> Self { + match word { + Word::Lower => Modifier::Lower, + Word::Upper => Modifier::Upper, + Word::LowerFirst => Modifier::LowerFirst, + Word::UpperFirst => Modifier::UpperFirst, + Word::QuoteWildcard => Modifier::QuoteWildcard, + Word::QuoteRegex => Modifier::QuoteRegex, + Word::Length => Modifier::Length, + Word::EncodeUrl => Modifier::EncodeUrl, + _ => unreachable!(), + } + } +} diff --git a/melib/src/sieve/compiler/grammar/actions/action_vacation.rs b/melib/src/sieve/compiler/grammar/actions/action_vacation.rs new file mode 100644 index 00000000..826b5ef7 --- /dev/null +++ b/melib/src/sieve/compiler/grammar/actions/action_vacation.rs @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use serde::{Deserialize, Serialize}; + +use crate::sieve::{ + compiler::{ + grammar::{ + instruction::{CompilerState, Instruction}, + test::Test, + Capability, + }, + lexer::{word::Word, Token}, + CompileError, Value, + }, + FileCarbonCopy, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct Vacation { + pub subject: Option, + pub from: Option, + pub mime: bool, + pub fcc: Option>, + pub reason: Value, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct TestVacation { + pub addresses: Vec, + pub period: Period, + pub handle: Option, + pub reason: Value, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) enum Period { + Days(u64), + Seconds(u64), + Default, +} + +/* + +vacation [":days" number] [":subject" string] + [":from" string] [":addresses" string-list] + [":mime"] [":handle" string] + +vacation [FCC] + [":days" number | ":seconds" number] + [":subject" string] + [":from" string] + [":addresses" string-list] + [":mime"] + [":handle" string] + + +":flags" + + + FCC = ":fcc" string *FCC-OPTS + ; per Section 2.6.2 of RFC 5228, + ; the tagged arguments in FCC may appear in any order + + FCC-OPTS = CREATE / IMAP-FLAGS / SPECIAL-USE + ; each option MUST NOT appear more than once + + CREATE = ":create" + IMAP-FLAGS = ":flags" string-list + SPECIAL-USE = ":specialuse" string +*/ + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_vacation(&mut self) -> Result<(), CompileError> { + let mut period = Period::Default; + let mut subject = None; + let mut from = None; + let mut handle = None; + let mut addresses = Vec::new(); + let mut mime = false; + let reason; + + let mut fcc = None; + let mut create = false; + let mut flags = Vec::new(); + let mut special_use = None; + let mut mailbox_id = None; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag(Word::Mime) => { + self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?; + mime = true; + } + Token::Tag(Word::Create) => { + self.validate_argument( + 2, + Capability::Mailbox.into(), + token_info.line_num, + token_info.line_pos, + )?; + create = true; + } + Token::Tag(Word::Days) => { + self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?; + period = Period::Days(self.tokens.expect_number(u64::MAX as usize)? as u64); + } + Token::Tag(Word::Seconds) => { + self.validate_argument( + 3, + Capability::VacationSeconds.into(), + token_info.line_num, + token_info.line_pos, + )?; + period = Period::Seconds(self.tokens.expect_number(u64::MAX as usize)? as u64); + } + Token::Tag(Word::Subject) => { + self.validate_argument(4, None, token_info.line_num, token_info.line_pos)?; + subject = self.parse_string()?.into(); + } + Token::Tag(Word::From) => { + self.validate_argument(5, None, token_info.line_num, token_info.line_pos)?; + from = self.parse_string()?.into(); + } + Token::Tag(Word::Handle) => { + self.validate_argument(6, None, token_info.line_num, token_info.line_pos)?; + handle = self.parse_string()?.into(); + } + Token::Tag(Word::SpecialUse) => { + self.validate_argument( + 7, + Capability::SpecialUse.into(), + token_info.line_num, + token_info.line_pos, + )?; + special_use = self.parse_string()?.into(); + } + Token::Tag(Word::MailboxId) => { + self.validate_argument( + 8, + Capability::MailboxId.into(), + token_info.line_num, + token_info.line_pos, + )?; + mailbox_id = self.parse_string()?.into(); + } + Token::Tag(Word::Fcc) => { + self.validate_argument( + 9, + Capability::Fcc.into(), + token_info.line_num, + token_info.line_pos, + )?; + fcc = self.parse_string()?.into(); + } + Token::Tag(Word::Flags) => { + self.validate_argument( + 10, + Capability::Imap4Flags.into(), + token_info.line_num, + token_info.line_pos, + )?; + flags = self.parse_strings(false)?; + } + Token::Tag(Word::Addresses) => { + self.validate_argument(11, None, token_info.line_num, token_info.line_pos)?; + addresses = self.parse_strings(false)?; + } + _ => { + reason = self.parse_string_token(token_info)?; + break; + } + } + } + + if fcc.is_none() + && (create || !flags.is_empty() || special_use.is_some() || mailbox_id.is_some()) + { + return Err(self.tokens.unwrap_next()?.missing_tag(":fcc")); + } + + self.instructions + .push(Instruction::Test(Test::Vacation(TestVacation { + period, + handle, + reason: reason.clone(), + addresses, + }))); + + self.instructions + .push(Instruction::Jz(self.instructions.len() + 2)); + + self.instructions.push(Instruction::Vacation(Vacation { + reason, + subject, + from, + mime, + fcc: if let Some(fcc) = fcc { + FileCarbonCopy { + mailbox: fcc, + create, + flags, + special_use, + mailbox_id, + } + .into() + } else { + None + }, + })); + + Ok(()) + } +} diff --git a/melib/src/sieve/compiler/grammar/actions/mod.rs b/melib/src/sieve/compiler/grammar/actions/mod.rs new file mode 100644 index 00000000..e45c993a --- /dev/null +++ b/melib/src/sieve/compiler/grammar/actions/mod.rs @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +pub mod action_convert; +pub mod action_editheader; +pub mod action_fileinto; +pub mod action_flags; +pub mod action_include; +pub mod action_keep; +pub mod action_mime; +pub mod action_notify; +pub mod action_redirect; +pub mod action_reject; +pub mod action_require; +pub mod action_set; +pub mod action_vacation; diff --git a/melib/src/sieve/compiler/grammar/expr/mod.rs b/melib/src/sieve/compiler/grammar/expr/mod.rs new file mode 100644 index 00000000..30481f7f --- /dev/null +++ b/melib/src/sieve/compiler/grammar/expr/mod.rs @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use serde::{Deserialize, Serialize}; + +use crate::sieve::compiler::{Number, VariableType}; + +pub mod parser; +pub mod tokenizer; + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub(crate) enum Expression { + Variable(VariableType), + Number(Number), + String(String), + BinaryOperator(BinaryOperator), + UnaryOperator(UnaryOperator), + Function { id: u32, num_args: u32 }, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] +pub(crate) enum BinaryOperator { + Add, + Subtract, + Multiply, + Divide, + + And, + Or, + Xor, + + Eq, + Ne, + Lt, + Le, + Gt, + Ge, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] +pub(crate) enum UnaryOperator { + Not, + Minus, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub(crate) enum Token { + Variable(VariableType), + Function { + name: String, + id: u32, + num_args: u32, + }, + Number(Number), + String(String), + BinaryOperator(BinaryOperator), + UnaryOperator(UnaryOperator), + OpenParen, + CloseParen, + Comma, +} diff --git a/melib/src/sieve/compiler/grammar/expr/parser.rs b/melib/src/sieve/compiler/grammar/expr/parser.rs new file mode 100644 index 00000000..941dc046 --- /dev/null +++ b/melib/src/sieve/compiler/grammar/expr/parser.rs @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use super::{tokenizer::Tokenizer, BinaryOperator, Expression, Token}; + +pub(crate) struct ExpressionParser<'x, F> +where + F: Fn(&str, bool) -> Result, +{ + pub(crate) tokenizer: Tokenizer<'x, F>, + pub(crate) output: Vec, + operator_stack: Vec, +} + +impl<'x, F> ExpressionParser<'x, F> +where + F: Fn(&str, bool) -> Result, +{ + pub fn from_tokenizer(tokenizer: Tokenizer<'x, F>) -> Self { + Self { + tokenizer, + output: Vec::new(), + operator_stack: Vec::new(), + } + } + + pub fn parse(mut self) -> Result { + let mut arg_count: Vec = vec![]; + + while let Some(token) = self.tokenizer.next()? { + match token { + Token::Variable(v) => { + if let Some(x) = arg_count.last_mut() { + *x = x.saturating_add(1); + } + self.output.push(Expression::Variable(v)) + } + Token::Number(n) => { + if let Some(x) = arg_count.last_mut() { + *x = x.saturating_add(1); + } + self.output.push(Expression::Number(n)) + } + Token::String(s) => { + if let Some(x) = arg_count.last_mut() { + *x = x.saturating_add(1); + } + self.output.push(Expression::String(s)) + } + Token::UnaryOperator(uop) => self.operator_stack.push(Token::UnaryOperator(uop)), + Token::OpenParen => self.operator_stack.push(token), + Token::CloseParen => { + loop { + match self.operator_stack.pop() { + Some(Token::OpenParen) => { + break; + } + Some(Token::BinaryOperator(bop)) => { + self.output.push(Expression::BinaryOperator(bop)) + } + Some(Token::UnaryOperator(uop)) => { + self.output.push(Expression::UnaryOperator(uop)) + } + _ => return Err("Mismatched parentheses".to_string()), + } + } + + if let Some(Token::Function { id, num_args, name }) = self.operator_stack.last() + { + let got_args = arg_count.pop().unwrap(); + if got_args != *num_args as i32 { + return Err(format!( + "Expression function {:?} expected {} arguments, got {}", + name, num_args, got_args + )); + } + let expr = Expression::Function { + id: *id, + num_args: *num_args, + }; + self.operator_stack.pop(); + self.output.push(expr); + } + } + Token::BinaryOperator(bop) => { + if let Some(x) = arg_count.last_mut() { + *x = x.saturating_sub(1); + } + while let Some(top_token) = self.operator_stack.last() { + match top_token { + Token::BinaryOperator(top_bop) => { + if bop.precedence() <= top_bop.precedence() { + let top_bop = *top_bop; + self.operator_stack.pop(); + self.output.push(Expression::BinaryOperator(top_bop)); + } else { + break; + } + } + Token::UnaryOperator(top_uop) => { + let top_uop = *top_uop; + self.operator_stack.pop(); + self.output.push(Expression::UnaryOperator(top_uop)); + } + _ => break, + } + } + self.operator_stack.push(Token::BinaryOperator(bop)); + } + Token::Function { id, name, num_args } => { + if let Some(x) = arg_count.last_mut() { + *x = x.saturating_add(1); + } + arg_count.push(0); + self.operator_stack + .push(Token::Function { id, name, num_args }) + } + Token::Comma => { + while let Some(token) = self.operator_stack.last() { + match token { + Token::OpenParen => break, + Token::BinaryOperator(bop) => { + self.output.push(Expression::BinaryOperator(*bop)); + self.operator_stack.pop(); + } + Token::UnaryOperator(uop) => { + self.output.push(Expression::UnaryOperator(*uop)); + self.operator_stack.pop(); + } + _ => break, + } + } + } + } + } + + while let Some(token) = self.operator_stack.pop() { + match token { + Token::BinaryOperator(bop) => self.output.push(Expression::BinaryOperator(bop)), + Token::UnaryOperator(uop) => self.output.push(Expression::UnaryOperator(uop)), + _ => return Err("Invalid token on the operator stack".to_string()), + } + } + + Ok(self) + } +} + +impl BinaryOperator { + fn precedence(&self) -> i32 { + match self { + BinaryOperator::Multiply | BinaryOperator::Divide => 7, + BinaryOperator::Add | BinaryOperator::Subtract => 6, + BinaryOperator::Gt | BinaryOperator::Ge | BinaryOperator::Lt | BinaryOperator::Le => 5, + BinaryOperator::Eq | BinaryOperator::Ne => 4, + BinaryOperator::Xor => 3, + BinaryOperator::And => 2, + BinaryOperator::Or => 1, + } + } +} diff --git a/melib/src/sieve/compiler/grammar/expr/tokenizer.rs b/melib/src/sieve/compiler/grammar/expr/tokenizer.rs new file mode 100644 index 00000000..2c4da33a --- /dev/null +++ b/melib/src/sieve/compiler/grammar/expr/tokenizer.rs @@ -0,0 +1,281 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::{ + iter::{Enumerate, Peekable}, + slice::Iter, +}; + +use crate::sieve::{compiler::Number, runtime::eval::IntoString}; + +use super::{BinaryOperator, Token, UnaryOperator}; + +pub(crate) struct Tokenizer<'x, F> +where + F: Fn(&str, bool) -> Result, +{ + pub(crate) iter: Peekable>>, + token_map: F, + buf: Vec, + depth: u32, + next_token: Vec, + has_number: bool, + has_dot: bool, + has_alpha: bool, + is_start: bool, + is_eof: bool, +} + +impl<'x, F> Tokenizer<'x, F> +where + F: Fn(&str, bool) -> Result, +{ + #[cfg(test)] + pub fn new(expr: &'x str, token_map: F) -> Self { + Self::from_iter(expr.as_bytes().iter().enumerate().peekable(), token_map) + } + + #[allow(clippy::should_implement_trait)] + pub(crate) fn from_iter(iter: Peekable>>, token_map: F) -> Self { + Self { + iter, + buf: Vec::new(), + depth: 0, + next_token: Vec::with_capacity(2), + has_number: false, + has_dot: false, + has_alpha: false, + is_start: true, + is_eof: false, + token_map, + } + } + + #[allow(clippy::should_implement_trait)] + pub(crate) fn next(&mut self) -> Result, String> { + if let Some(token) = self.next_token.pop() { + return Ok(Some(token)); + } else if self.is_eof { + return Ok(None); + } + + while let Some((_, &ch)) = self.iter.next() { + match ch { + b'A'..=b'Z' | b'a'..=b'z' | b'_' => { + self.buf.push(ch); + self.has_alpha = true; + } + b'0'..=b'9' => { + self.buf.push(ch); + self.has_number = true; + } + b'.' => { + self.buf.push(ch); + self.has_dot = true; + } + b'}' => { + self.is_eof = true; + break; + } + b'[' if self.buf.last().map_or(false, |c| c.is_ascii_alphanumeric()) => { + self.buf.push(ch); + } + b'-' if self.buf.last().map_or(false, |c| *c == b'[') + || matches!(self.buf.get(0..7), Some(b"header.")) => + { + self.buf.push(ch); + } + b']' if self.buf.contains(&b'[') => { + self.buf.push(b']'); + } + b'*' if self.buf.last().map_or(false, |&c| c == b'[' || c == b'.') => { + self.buf.push(ch); + } + _ => { + let prev_token = if !self.buf.is_empty() { + self.is_start = false; + self.parse_buf()?.into() + } else { + None + }; + let token = match ch { + b'&' => { + if matches!(self.iter.peek(), Some((_, b'&'))) { + self.iter.next(); + } + Token::BinaryOperator(BinaryOperator::And) + } + b'|' => { + if matches!(self.iter.peek(), Some((_, b'|'))) { + self.iter.next(); + } + Token::BinaryOperator(BinaryOperator::Or) + } + b'!' => { + if matches!(self.iter.peek(), Some((_, b'='))) { + self.iter.next(); + Token::BinaryOperator(BinaryOperator::Ne) + } else { + Token::UnaryOperator(UnaryOperator::Not) + } + } + b'^' => Token::BinaryOperator(BinaryOperator::Xor), + b'(' => { + self.depth += 1; + Token::OpenParen + } + b')' => { + if self.depth == 0 { + return Err("Unmatched close parenthesis".to_string()); + } + self.depth -= 1; + Token::CloseParen + } + b'+' => Token::BinaryOperator(BinaryOperator::Add), + b'*' => Token::BinaryOperator(BinaryOperator::Multiply), + b'/' => Token::BinaryOperator(BinaryOperator::Divide), + b'-' => { + if self.is_start { + Token::UnaryOperator(UnaryOperator::Minus) + } else { + Token::BinaryOperator(BinaryOperator::Subtract) + } + } + b'=' => match self.iter.next() { + Some((_, b'=')) => Token::BinaryOperator(BinaryOperator::Eq), + Some((_, b'>')) => Token::BinaryOperator(BinaryOperator::Ge), + Some((_, b'<')) => Token::BinaryOperator(BinaryOperator::Le), + _ => Token::BinaryOperator(BinaryOperator::Eq), + }, + b'>' => match self.iter.peek() { + Some((_, b'=')) => { + self.iter.next(); + Token::BinaryOperator(BinaryOperator::Ge) + } + _ => Token::BinaryOperator(BinaryOperator::Gt), + }, + b'<' => match self.iter.peek() { + Some((_, b'=')) => { + self.iter.next(); + Token::BinaryOperator(BinaryOperator::Le) + } + _ => Token::BinaryOperator(BinaryOperator::Lt), + }, + b',' => { + if self.depth == 0 { + return Err("Comma outside of function call".to_string()); + } + Token::Comma + } + b' ' | b'\r' | b'\n' => { + if prev_token.is_some() { + return Ok(prev_token); + } else { + continue; + } + } + b'\"' | b'\'' => { + let mut buf = Vec::with_capacity(16); + let stop_ch = ch; + let mut last_ch = 0; + let mut found_end = false; + + for (_, &ch) in self.iter.by_ref() { + if ch == stop_ch && last_ch != b'\\' { + found_end = true; + break; + } else if ch != b'\\' || last_ch == b'\\' { + buf.push(ch); + } + last_ch = ch; + } + + if found_end { + Token::String( + String::from_utf8(buf) + .map_err(|_| "Invalid UTF-8".to_string())?, + ) + } else { + return Err("Unterminated string".to_string()); + } + } + _ => { + return Err(format!("Invalid character {:?}", char::from(ch),)); + } + }; + self.is_start = ch == b'('; + + return if prev_token.is_some() { + self.next_token.push(token); + Ok(prev_token) + } else { + Ok(Some(token)) + }; + } + } + } + + if self.depth > 0 { + Err("Unmatched open parenthesis".to_string()) + } else if !self.buf.is_empty() { + self.parse_buf().map(Some) + } else { + Ok(None) + } + } + + fn parse_buf(&mut self) -> Result { + let buf = std::mem::take(&mut self.buf).into_string(); + if self.has_number && !self.has_alpha { + self.has_number = false; + if self.has_dot { + self.has_dot = false; + + buf.parse::() + .map(|f| Token::Number(Number::Float(f))) + .map_err(|_| format!("Invalid float value {}", buf,)) + } else { + buf.parse::() + .map(|i| Token::Number(Number::Integer(i))) + .map_err(|_| format!("Invalid integer value {}", buf,)) + } + } else { + let has_dot = self.has_dot; + let has_number = self.has_number; + + self.has_alpha = false; + self.has_number = false; + self.has_dot = false; + + if !has_number && !has_dot && [4, 5].contains(&buf.len()) { + if buf == "true" { + return Ok(Token::Number(Number::Integer(1))); + } else if buf == "false" { + return Ok(Token::Number(Number::Integer(0))); + } + } + + (self.token_map)(&buf, has_dot) + } + } +} diff --git a/melib/src/sieve/compiler/grammar/instruction.rs b/melib/src/sieve/compiler/grammar/instruction.rs new file mode 100644 index 00000000..cfac5593 --- /dev/null +++ b/melib/src/sieve/compiler/grammar/instruction.rs @@ -0,0 +1,1203 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use ahash::{AHashMap, AHashSet}; +use serde::{Deserialize, Serialize}; + +use crate::sieve::{ + compiler::{ + grammar::{test::Test, MatchType}, + lexer::{tokenizer::Tokenizer, word::Word, Token}, + CompileError, ErrorType, Value, VariableType, + }, + Compiler, Sieve, +}; + +use super::{ + actions::{ + action_convert::Convert, + action_editheader::{AddHeader, DeleteHeader}, + action_fileinto::FileInto, + action_flags::EditFlags, + action_include::Include, + action_keep::Keep, + action_mime::{Enclose, ExtractText, ForEveryPart, Replace}, + action_notify::Notify, + action_redirect::Redirect, + action_reject::Reject, + action_set::Set, + action_vacation::Vacation, + }, + expr::Expression, + tests::test_plugin::Plugin, + Capability, Clear, ForEveryLine, Invalid, +}; + +use super::tests::test_ihave::Error; + +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub(crate) enum Instruction { + Require(Vec), + Keep(Keep), + FileInto(FileInto), + Redirect(Redirect), + Discard, + Stop, + Invalid(Invalid), + Test(Test), + Jmp(usize), + Jz(usize), + Jnz(usize), + + // RFC 5703 + ForEveryPartPush, + ForEveryPart(ForEveryPart), + ForEveryPartPop(usize), + Replace(Replace), + Enclose(Enclose), + ExtractText(ExtractText), + + // RFC 6558 + Convert(Convert), + + // RFC 5293 + AddHeader(AddHeader), + DeleteHeader(DeleteHeader), + + // RFC 5229 + Set(Set), + Clear(Clear), + + // RFC 5435 + Notify(Notify), + + // RFC 5429 + Reject(Reject), + + // RFC 5230 + Vacation(Vacation), + + // RFC 5463 + Error(Error), + + // RFC 5232 + EditFlags(EditFlags), + + // RFC 6609 + Include(Include), + Return, + + // Plugin extension + Plugin(Plugin), + + // For every line extension + ForEveryLineInit(Value), + ForEveryLine(ForEveryLine), +} + +pub(crate) const MAX_PARAMS: usize = 11; + +#[derive(Debug)] +pub(crate) struct Block { + pub(crate) btype: Word, + pub(crate) label: Option, + pub(crate) line_num: usize, + pub(crate) line_pos: usize, + pub(crate) last_block_start: usize, + pub(crate) if_jmps: Vec, + pub(crate) break_jmps: Vec, + pub(crate) match_test_pos: Vec, + pub(crate) match_test_vars: u64, + pub(crate) vars_local: AHashMap, + pub(crate) capabilities: AHashSet, + pub(crate) require_pos: usize, +} + +pub(crate) struct CompilerState<'x> { + pub(crate) compiler: &'x Compiler, + pub(crate) tokens: Tokenizer<'x>, + pub(crate) instructions: Vec, + pub(crate) block_stack: Vec, + pub(crate) block: Block, + pub(crate) last_block_type: Word, + pub(crate) vars_global: AHashSet, + pub(crate) vars_num: usize, + pub(crate) vars_num_max: usize, + pub(crate) vars_match_max: usize, + pub(crate) vars_local: usize, + pub(crate) param_check: [bool; MAX_PARAMS], + pub(crate) includes_num: usize, +} + +impl Compiler { + pub fn compile(&self, script: &[u8]) -> Result { + if script.len() > self.max_script_size { + return Err(CompileError { + line_num: 0, + line_pos: 0, + error_type: ErrorType::ScriptTooLong, + }); + } + + let mut state = CompilerState { + compiler: self, + tokens: Tokenizer::new(self, script), + instructions: Vec::new(), + block_stack: Vec::new(), + block: Block::new(Word::Not), + last_block_type: Word::Not, + vars_global: AHashSet::new(), + vars_num: 0, + vars_num_max: 0, + vars_match_max: 0, + vars_local: 0, + param_check: [false; MAX_PARAMS], + includes_num: 0, + }; + + while let Some(token_info) = state.tokens.next() { + let token_info = token_info?; + state.reset_param_check(); + + match token_info.token { + Token::Identifier(instruction) => { + let mut is_new_block = None; + + match instruction { + Word::Require => { + state.parse_require()?; + } + Word::If => { + state.parse_test()?; + state.block.if_jmps.clear(); + is_new_block = Block::new(Word::If).into(); + } + Word::ElsIf => { + if let Word::If | Word::ElsIf = &state.last_block_type { + state.parse_test()?; + is_new_block = Block::new(Word::ElsIf).into(); + } else { + return Err(token_info.expected("'if' before 'elsif'")); + } + } + Word::Else => { + if let Word::If | Word::ElsIf = &state.last_block_type { + is_new_block = Block::new(Word::Else).into(); + } else { + return Err(token_info.expected("'if' or 'elsif' before 'else'")); + } + } + Word::Keep => { + state.parse_keep()?; + } + Word::FileInto => { + state.validate_argument( + 0, + Capability::FileInto.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_fileinto()?; + } + Word::Redirect => { + state.parse_redirect()?; + } + Word::Discard => { + state.instructions.push(Instruction::Discard); + } + Word::Stop => { + state.instructions.push(Instruction::Stop); + } + + // RFC 5703 + Word::ForEveryPart => { + state.validate_argument( + 0, + Capability::ForEveryPart.into(), + token_info.line_num, + token_info.line_pos, + )?; + + if state + .block_stack + .iter() + .filter(|b| matches!(&b.btype, Word::ForEveryPart)) + .count() + == self.max_nested_foreverypart + { + return Err( + token_info.custom(ErrorType::TooManyNestedForEveryParts) + ); + } + + is_new_block = if let Some(Ok(Token::Tag(Word::Name))) = + state.tokens.peek().map(|r| r.map(|t| &t.token)) + { + let tag = state.tokens.next().unwrap().unwrap(); + let label = state.tokens.expect_static_string()?; + for block in &state.block_stack { + if block.label.as_ref().map_or(false, |n| n.eq(&label)) { + return Err( + tag.custom(ErrorType::LabelAlreadyDefined(label)) + ); + } + } + Block::new(Word::ForEveryPart).with_label(label) + } else { + Block::new(Word::ForEveryPart) + } + .into(); + + state.instructions.push(Instruction::ForEveryPartPush); + state + .instructions + .push(Instruction::ForEveryPart(ForEveryPart { + jz_pos: usize::MAX, + })); + } + Word::Break => { + if let Some(Ok(Token::Tag(Word::Name))) = + state.tokens.peek().map(|r| r.map(|t| &t.token)) + { + state.validate_argument( + 0, + Capability::ForEveryPart.into(), + token_info.line_num, + token_info.line_pos, + )?; + + let tag = state.tokens.next().unwrap().unwrap(); + let label = state.tokens.expect_static_string()?; + let mut label_found = false; + let mut num_pops = 0; + + for block in [&mut state.block] + .into_iter() + .chain(state.block_stack.iter_mut().rev()) + { + if let Word::ForEveryPart = &block.btype { + num_pops += 1; + if block.label.as_ref().map_or(false, |n| n.eq(&label)) { + state + .instructions + .push(Instruction::ForEveryPartPop(num_pops)); + block.break_jmps.push(state.instructions.len()); + label_found = true; + break; + } + } + } + + if !label_found { + return Err(tag.custom(ErrorType::LabelUndefined(label))); + } + } else { + let mut block_found = None; + if matches!( + &state.block.btype, + Word::ForEveryPart | Word::ForEveryLine + ) { + block_found = Some(&mut state.block); + } else { + for block in state.block_stack.iter_mut().rev() { + if matches!( + &block.btype, + Word::ForEveryPart | Word::ForEveryLine + ) { + block_found = Some(block); + break; + } + } + } + + let block = block_found.ok_or_else(|| { + token_info.custom(ErrorType::BreakOutsideLoop) + })?; + if matches!(block.btype, Word::ForEveryPart) { + state.instructions.push(Instruction::ForEveryPartPop(1)); + } + + block.break_jmps.push(state.instructions.len()); + } + + state.instructions.push(Instruction::Jmp(usize::MAX)); + } + Word::Replace => { + state.validate_argument( + 0, + Capability::Replace.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_replace()?; + } + Word::Enclose => { + state.validate_argument( + 0, + Capability::Enclose.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_enclose()?; + } + Word::ExtractText => { + state.validate_argument( + 0, + Capability::ExtractText.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_extracttext()?; + } + + // RFC 6558 + Word::Convert => { + state.validate_argument( + 0, + Capability::Convert.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_convert()?; + } + + // RFC 5293 + Word::AddHeader => { + state.validate_argument( + 0, + Capability::EditHeader.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_addheader()?; + } + Word::DeleteHeader => { + state.validate_argument( + 0, + Capability::EditHeader.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_deleteheader()?; + } + + // RFC 5229 + Word::Set => { + state.validate_argument( + 0, + Capability::Variables.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_set()?; + } + + // RFC 5435 + Word::Notify => { + state.validate_argument( + 0, + Capability::Enotify.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_notify()?; + } + + // RFC 5429 + Word::Reject => { + state.validate_argument( + 0, + Capability::Reject.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_reject(false)?; + } + Word::Ereject => { + state.validate_argument( + 0, + Capability::Ereject.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_reject(true)?; + } + + // RFC 5230 + Word::Vacation => { + state.validate_argument( + 0, + Capability::Vacation.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_vacation()?; + } + + // RFC 5463 + Word::Error => { + state.validate_argument( + 0, + Capability::Ihave.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_error()?; + } + + // RFC 5232 + Word::SetFlag | Word::AddFlag | Word::RemoveFlag => { + state.validate_argument( + 0, + Capability::Imap4Flags.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_flag_action(instruction)?; + } + + // RFC 6609 + Word::Include => { + if state.includes_num < self.max_includes { + state.validate_argument( + 0, + Capability::Include.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_include()?; + state.includes_num += 1; + } else { + return Err(token_info.custom(ErrorType::TooManyIncludes)); + } + } + Word::Return => { + state.validate_argument( + 0, + Capability::Include.into(), + token_info.line_num, + token_info.line_pos, + )?; + let mut num_pops = 0; + + for block in [&state.block] + .into_iter() + .chain(state.block_stack.iter().rev()) + { + if let Word::ForEveryPart = &block.btype { + num_pops += 1; + } + } + + if num_pops > 0 { + state + .instructions + .push(Instruction::ForEveryPartPop(num_pops)); + } + + state.instructions.push(Instruction::Return); + } + Word::Global => { + state.validate_argument( + 0, + Capability::Include.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.validate_argument( + 0, + Capability::Variables.into(), + token_info.line_num, + token_info.line_pos, + )?; + for global in state.parse_static_strings()? { + if !state.is_var_local(&global) { + if global.len() < self.max_variable_name_size { + state.register_global_var(&global); + } else { + return Err(state + .tokens + .unwrap_next()? + .custom(ErrorType::VariableTooLong)); + } + } else { + return Err(state + .tokens + .unwrap_next()? + .custom(ErrorType::VariableIsLocal(global))); + } + } + } + + // ForEveryLine extension + Word::ForEveryLine => { + state.validate_argument( + 0, + Capability::ForEveryLine.into(), + token_info.line_num, + token_info.line_pos, + )?; + + if state + .block_stack + .iter() + .any(|b| matches!(&b.btype, Word::ForEveryLine)) + { + return Err(token_info.custom(ErrorType::TooManyNestedBlocks)); + } + + let var_idx = state.vars_num; + is_new_block = Block::new(Word::ForEveryLine) + .with_local_var("line", state.vars_num) + .with_local_var("line_num", state.vars_num + 1) + .into(); + state.vars_num += 2; + + let source = state.parse_string()?; + state + .instructions + .push(Instruction::ForEveryLineInit(source)); + state + .instructions + .push(Instruction::ForEveryLine(ForEveryLine { + var_idx, + jz_pos: usize::MAX, + })); + } + + _ => { + state.ignore_instruction()?; + state.instructions.push(Instruction::Invalid(Invalid { + name: instruction.to_string(), + line_num: token_info.line_num, + line_pos: token_info.line_pos, + })); + continue; + } + } + + if let Some(mut new_block) = is_new_block { + new_block.line_num = state.tokens.line_num; + new_block.line_pos = state.tokens.pos - state.tokens.line_start; + + state.tokens.expect_token(Token::CurlyOpen)?; + if state.block_stack.len() < self.max_nested_blocks { + state.block.last_block_start = state.instructions.len() - 1; + state.block_stack.push(state.block); + state.block = new_block; + } else { + return Err(CompileError { + line_num: state.block.line_num, + line_pos: state.block.line_pos, + error_type: ErrorType::TooManyNestedBlocks, + }); + } + } else { + state.expect_instruction_end()?; + } + } + Token::CurlyClose if !state.block_stack.is_empty() => { + state.block_end(); + let mut prev_block = state.block_stack.pop().unwrap(); + match &state.block.btype { + Word::ForEveryPart => { + state + .instructions + .push(Instruction::Jmp(prev_block.last_block_start)); + let cur_pos = state.instructions.len(); + if let Instruction::ForEveryPart(fep) = + &mut state.instructions[prev_block.last_block_start] + { + fep.jz_pos = cur_pos; + } else { + debug_assert!(false, "This should not have happened."); + } + for pos in state.block.break_jmps { + if let Instruction::Jmp(jmp_pos) = &mut state.instructions[pos] { + *jmp_pos = cur_pos; + } else { + debug_assert!(false, "This should not have happened."); + } + } + state.last_block_type = Word::Not; + } + Word::If | Word::ElsIf => { + let next_is_block = matches!( + state.tokens.peek().map(|r| r.map(|t| &t.token)), + Some(Ok(Token::Identifier(Word::ElsIf | Word::Else))) + ); + if next_is_block { + prev_block.if_jmps.push(state.instructions.len()); + state.instructions.push(Instruction::Jmp(usize::MAX)); + } + let cur_pos = state.instructions.len(); + if let Instruction::Jz(jmp_pos) = + &mut state.instructions[prev_block.last_block_start] + { + *jmp_pos = cur_pos; + } else { + debug_assert!(false, "This should not have happened."); + } + if !next_is_block { + for pos in prev_block.if_jmps.drain(..) { + if let Instruction::Jmp(jmp_pos) = &mut state.instructions[pos] + { + *jmp_pos = cur_pos; + } else { + debug_assert!(false, "This should not have happened."); + } + } + state.last_block_type = Word::Not; + } else { + state.last_block_type = state.block.btype; + } + } + Word::Else => { + let cur_pos = state.instructions.len(); + for pos in prev_block.if_jmps.drain(..) { + if let Instruction::Jmp(jmp_pos) = &mut state.instructions[pos] { + *jmp_pos = cur_pos; + } else { + debug_assert!(false, "This should not have happened."); + } + } + state.last_block_type = Word::Else; + } + Word::ForEveryLine => { + state + .instructions + .push(Instruction::Jmp(prev_block.last_block_start)); + let cur_pos = state.instructions.len(); + if let Instruction::ForEveryLine(fep) = + &mut state.instructions[prev_block.last_block_start] + { + fep.jz_pos = cur_pos; + } else { + debug_assert!(false, "This should not have happened."); + } + for pos in state.block.break_jmps { + if let Instruction::Jmp(jmp_pos) = &mut state.instructions[pos] { + *jmp_pos = cur_pos; + } else { + debug_assert!(false, "This should not have happened."); + } + } + state.last_block_type = Word::Not; + } + _ => { + debug_assert!(false, "This should not have happened."); + } + } + + state.block = prev_block; + } + + #[cfg(test)] + Token::Unknown(instruction) if instruction.contains("test") => { + use crate::sieve::PluginArgument; + + let has_arguments = instruction != "test"; + let mut plugin = Plugin { + id: u32::MAX, + arguments: vec![PluginArgument::Text(Value::Text(instruction))], + is_not: false, + }; + + if !has_arguments { + plugin + .arguments + .push(PluginArgument::Text(state.parse_string()?)); + state.instructions.push(Instruction::Plugin(plugin)); + let mut new_block = Block::new(Word::Else); + new_block.line_num = state.tokens.line_num; + new_block.line_pos = state.tokens.pos - state.tokens.line_start; + state.tokens.expect_token(Token::CurlyOpen)?; + state.block.last_block_start = state.instructions.len() - 1; + state.block_stack.push(state.block); + state.block = new_block; + } else { + loop { + plugin.arguments.push(PluginArgument::Text( + match state.tokens.unwrap_next()?.token { + Token::StringConstant(s) => Value::from(s), + Token::StringVariable(s) => state + .tokenize_string(&s, true) + .map_err(|error_type| CompileError { + line_num: 0, + line_pos: 0, + error_type, + })?, + Token::Number(n) => Value::Number( + crate::sieve::compiler::Number::Integer(n as i64), + ), + Token::Identifier(s) => Value::Text(s.to_string()), + Token::Tag(s) => Value::Text(format!(":{s}")), + Token::Unknown(s) => Value::Text(s), + Token::Semicolon => break, + other => panic!("Invalid test param {other:?}"), + }, + )); + } + state.instructions.push(Instruction::Plugin(plugin)); + } + } + + Token::Unknown(instruction) => { + if let Some(schema) = self.plugins.get(&instruction) { + state.validate_argument( + 0, + Capability::Plugins.into(), + token_info.line_num, + token_info.line_pos, + )?; + state.parse_plugin(schema)?; + } else { + state.ignore_instruction()?; + state.instructions.push(Instruction::Invalid(Invalid { + name: instruction, + line_num: token_info.line_num, + line_pos: token_info.line_pos, + })); + } + } + _ => { + return Err(token_info.expected("instruction")); + } + } + } + + if !state.block_stack.is_empty() { + return Err(CompileError { + line_num: state.block.line_num, + line_pos: state.block.line_pos, + error_type: ErrorType::UnterminatedBlock, + }); + } + + // Map local variables + let mut num_vars = std::cmp::max(state.vars_num_max, state.vars_num); + if state.vars_local > 0 { + state.map_local_vars(num_vars); + num_vars += state.vars_local; + } + + Ok(Sieve { + instructions: state.instructions, + num_vars, + num_match_vars: state.vars_match_max, + }) + } +} + +impl<'x> CompilerState<'x> { + pub(crate) fn is_var_local(&self, name: &str) -> bool { + let name = name.to_ascii_lowercase(); + if self.block.vars_local.contains_key(&name) { + true + } else { + for block in self.block_stack.iter().rev() { + if block.vars_local.contains_key(&name) { + return true; + } + } + false + } + } + + pub(crate) fn is_var_global(&self, name: &str) -> bool { + let name = name.to_ascii_lowercase(); + self.vars_global.contains(&name) + } + + pub(crate) fn register_local_var(&mut self, name: String, register_as_local: bool) -> usize { + if let Some(var_id) = self.get_local_var(&name) { + var_id + } else if !register_as_local || self.block_stack.is_empty() { + let var_id = self.vars_num; + self.block.vars_local.insert(name, var_id); + self.vars_num += 1; + var_id + } else { + let var_id = usize::MAX - self.vars_local; + self.block_stack + .first_mut() + .unwrap() + .vars_local + .insert(name, usize::MAX - self.vars_local); + self.vars_local += 1; + var_id + } + } + + pub(crate) fn register_global_var(&mut self, name: &str) { + self.vars_global.insert(name.to_ascii_lowercase()); + } + + pub(crate) fn get_local_var(&self, name: &str) -> Option { + let name = name.to_ascii_lowercase(); + if let Some(var_id) = self.block.vars_local.get(&name) { + Some(*var_id) + } else { + for block in self.block_stack.iter().rev() { + if let Some(var_id) = block.vars_local.get(&name) { + return Some(*var_id); + } + } + None + } + } + + pub(crate) fn register_match_var(&mut self, num: usize) -> bool { + let mut block = &mut self.block; + + if block.match_test_pos.is_empty() { + for block_ in self.block_stack.iter_mut().rev() { + if !block_.match_test_pos.is_empty() { + block = block_; + break; + } + } + } + + if !block.match_test_pos.is_empty() { + debug_assert!(num < 63); + + for pos in &block.match_test_pos { + if let Instruction::Test(test) = &mut self.instructions[*pos] { + let match_type = match test { + Test::Address(t) => &mut t.match_type, + Test::Body(t) => &mut t.match_type, + Test::Date(t) => &mut t.match_type, + Test::CurrentDate(t) => &mut t.match_type, + Test::Envelope(t) => &mut t.match_type, + Test::HasFlag(t) => &mut t.match_type, + Test::Header(t) => &mut t.match_type, + Test::Metadata(t) => &mut t.match_type, + Test::NotifyMethodCapability(t) => &mut t.match_type, + Test::SpamTest(t) => &mut t.match_type, + Test::String(t) | Test::Environment(t) => &mut t.match_type, + Test::VirusTest(t) => &mut t.match_type, + _ => { + debug_assert!(false, "This should not have happened: {test:?}"); + return false; + } + }; + if let MatchType::Matches(positions) | MatchType::Regex(positions) = match_type + { + *positions |= 1 << num; + block.match_test_vars = *positions; + } else { + debug_assert!(false, "This should not have happened"); + return false; + } + } else { + debug_assert!(false, "This should not have happened"); + return false; + } + } + true + } else { + false + } + } + + pub(crate) fn block_end(&mut self) { + let vars_num_block = self.block.vars_local.len(); + if vars_num_block > 0 { + if self.vars_num > self.vars_num_max { + self.vars_num_max = self.vars_num; + } + self.vars_num -= vars_num_block; + self.instructions.push(Instruction::Clear(Clear { + match_vars: self.block.match_test_vars, + local_vars_idx: self.vars_num as u32, + local_vars_num: vars_num_block as u32, + })); + } else if self.block.match_test_vars != 0 { + self.instructions.push(Instruction::Clear(Clear { + match_vars: self.block.match_test_vars, + local_vars_idx: 0, + local_vars_num: 0, + })); + } + } + + fn map_local_vars(&mut self, last_id: usize) { + for instruction in &mut self.instructions { + match instruction { + Instruction::Test(v) => v.map_local_vars(last_id), + Instruction::Keep(k) => k.flags.map_local_vars(last_id), + Instruction::FileInto(v) => { + v.folder.map_local_vars(last_id); + v.flags.map_local_vars(last_id); + v.mailbox_id.map_local_vars(last_id); + v.special_use.map_local_vars(last_id); + } + Instruction::Redirect(v) => { + v.address.map_local_vars(last_id); + v.by_time.map_local_vars(last_id); + } + Instruction::Replace(v) => { + v.subject.map_local_vars(last_id); + v.from.map_local_vars(last_id); + v.replacement.map_local_vars(last_id); + } + Instruction::Enclose(v) => { + v.subject.map_local_vars(last_id); + v.headers.map_local_vars(last_id); + v.value.map_local_vars(last_id); + } + Instruction::ExtractText(v) => { + v.name.map_local_vars(last_id); + } + Instruction::Convert(v) => { + v.from_media_type.map_local_vars(last_id); + v.to_media_type.map_local_vars(last_id); + v.transcoding_params.map_local_vars(last_id); + } + Instruction::AddHeader(v) => { + v.field_name.map_local_vars(last_id); + v.value.map_local_vars(last_id); + } + Instruction::DeleteHeader(v) => { + v.field_name.map_local_vars(last_id); + v.value_patterns.map_local_vars(last_id); + } + Instruction::Set(v) => { + v.name.map_local_vars(last_id); + v.value.map_local_vars(last_id); + } + Instruction::Notify(v) => { + v.from.map_local_vars(last_id); + v.importance.map_local_vars(last_id); + v.options.map_local_vars(last_id); + v.message.map_local_vars(last_id); + v.fcc.map_local_vars(last_id); + v.method.map_local_vars(last_id); + } + Instruction::Reject(v) => { + v.reason.map_local_vars(last_id); + } + Instruction::Vacation(v) => { + v.subject.map_local_vars(last_id); + v.from.map_local_vars(last_id); + v.fcc.map_local_vars(last_id); + v.reason.map_local_vars(last_id); + } + Instruction::Error(v) => { + v.message.map_local_vars(last_id); + } + Instruction::EditFlags(v) => { + v.name.map_local_vars(last_id); + v.flags.map_local_vars(last_id); + } + Instruction::Include(v) => { + v.value.map_local_vars(last_id); + } + Instruction::Plugin(v) => { + v.arguments.map_local_vars(last_id); + } + _ => {} + } + } + } +} + +pub trait MapLocalVars { + fn map_local_vars(&mut self, last_id: usize); +} + +impl MapLocalVars for Test { + fn map_local_vars(&mut self, last_id: usize) { + match self { + Test::Address(v) => { + v.header_list.map_local_vars(last_id); + v.key_list.map_local_vars(last_id); + } + Test::Envelope(v) => { + v.key_list.map_local_vars(last_id); + } + Test::Exists(v) => { + v.header_names.map_local_vars(last_id); + } + Test::Header(v) => { + v.key_list.map_local_vars(last_id); + v.header_list.map_local_vars(last_id); + v.mime_opts.map_local_vars(last_id); + } + Test::Body(v) => { + v.key_list.map_local_vars(last_id); + } + Test::Convert(v) => { + v.from_media_type.map_local_vars(last_id); + v.to_media_type.map_local_vars(last_id); + v.transcoding_params.map_local_vars(last_id); + } + Test::Date(v) => { + v.key_list.map_local_vars(last_id); + v.header_name.map_local_vars(last_id); + } + Test::CurrentDate(v) => { + v.key_list.map_local_vars(last_id); + } + Test::Duplicate(v) => { + v.handle.map_local_vars(last_id); + v.dup_match.map_local_vars(last_id); + } + Test::String(v) => { + v.source.map_local_vars(last_id); + v.key_list.map_local_vars(last_id); + } + Test::Environment(v) => { + v.source.map_local_vars(last_id); + v.key_list.map_local_vars(last_id); + } + Test::NotifyMethodCapability(v) => { + v.key_list.map_local_vars(last_id); + v.notification_capability.map_local_vars(last_id); + v.notification_uri.map_local_vars(last_id); + } + Test::ValidNotifyMethod(v) => { + v.notification_uris.map_local_vars(last_id); + } + Test::ValidExtList(v) => { + v.list_names.map_local_vars(last_id); + } + Test::HasFlag(v) => { + v.variable_list.map_local_vars(last_id); + v.flags.map_local_vars(last_id); + } + Test::MailboxExists(v) => { + v.mailbox_names.map_local_vars(last_id); + } + Test::Metadata(v) => { + v.key_list.map_local_vars(last_id); + v.medatata.map_local_vars(last_id); + } + Test::MetadataExists(v) => { + v.annotation_names.map_local_vars(last_id); + v.mailbox.map_local_vars(last_id); + } + Test::MailboxIdExists(v) => { + v.mailbox_ids.map_local_vars(last_id); + } + Test::SpamTest(v) => { + v.value.map_local_vars(last_id); + } + Test::VirusTest(v) => { + v.value.map_local_vars(last_id); + } + Test::SpecialUseExists(v) => { + v.mailbox.map_local_vars(last_id); + v.attributes.map_local_vars(last_id); + } + Test::Vacation(v) => { + v.addresses.map_local_vars(last_id); + v.handle.map_local_vars(last_id); + v.reason.map_local_vars(last_id); + } + Test::EvalExpression(v) => { + v.expr.map_local_vars(last_id); + } + Test::Plugin(v) => { + v.arguments.map_local_vars(last_id); + } + _ => (), + } + } +} + +impl MapLocalVars for VariableType { + fn map_local_vars(&mut self, last_id: usize) { + match self { + VariableType::Local(id) if *id > last_id => { + *id = (usize::MAX - *id) + last_id; + } + _ => (), + } + } +} + +impl MapLocalVars for Value { + fn map_local_vars(&mut self, last_id: usize) { + match self { + Value::Variable(var) => var.map_local_vars(last_id), + Value::Expression(expr) => expr.map_local_vars(last_id), + Value::List(items) => items.map_local_vars(last_id), + _ => (), + } + } +} + +impl MapLocalVars for Option { + fn map_local_vars(&mut self, last_id: usize) { + if let Some(value) = self { + value.map_local_vars(last_id); + } + } +} + +impl MapLocalVars for Expression { + fn map_local_vars(&mut self, last_id: usize) { + if let Expression::Variable(var) = self { + var.map_local_vars(last_id) + } + } +} + +impl MapLocalVars for Vec { + fn map_local_vars(&mut self, last_id: usize) { + for item in self { + item.map_local_vars(last_id); + } + } +} + +impl Block { + pub fn new(btype: Word) -> Self { + Block { + btype, + label: None, + line_num: 0, + line_pos: 0, + last_block_start: 0, + match_test_pos: vec![], + match_test_vars: 0, + if_jmps: vec![], + break_jmps: vec![], + vars_local: AHashMap::new(), + capabilities: AHashSet::new(), + require_pos: usize::MAX, + } + } + + pub fn with_label(mut self, label: String) -> Self { + self.label = label.into(); + self + } + + pub fn with_local_var(mut self, name: impl Into, id: usize) -> Self { + self.vars_local.insert(name.into(), id); + self + } +} diff --git a/melib/src/sieve/compiler/grammar/mod.rs b/melib/src/sieve/compiler/grammar/mod.rs new file mode 100644 index 00000000..832a60ba --- /dev/null +++ b/melib/src/sieve/compiler/grammar/mod.rs @@ -0,0 +1,688 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::fmt::Display; + +use phf::phf_map; +use serde::{Deserialize, Serialize}; + +use self::instruction::CompilerState; + +use super::{ + lexer::{tokenizer::TokenInfo, word::Word, Token}, + CompileError, ErrorType, Regex, Value, +}; + +pub mod actions; +pub mod expr; +pub mod instruction; +pub mod test; +pub mod tests; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +pub enum Capability { + Envelope, + EnvelopeDsn, + EnvelopeDeliverBy, + FileInto, + EncodedCharacter, + Comparator(Comparator), + Other(String), + Body, + Convert, + Copy, + Relational, + Date, + Index, + Duplicate, + Variables, + EditHeader, + ForEveryPart, + Mime, + Replace, + Enclose, + ExtractText, + Enotify, + RedirectDsn, + RedirectDeliverBy, + Environment, + Reject, + Ereject, + ExtLists, + SubAddress, + Vacation, + VacationSeconds, + Fcc, + Mailbox, + MailboxId, + MboxMetadata, + ServerMetadata, + SpecialUse, + Imap4Flags, + Ihave, + ImapSieve, + Include, + Regex, + SpamTest, + SpamTestPlus, + VirusTest, + + // Extensions + Eval, + Plugins, + ForEveryLine, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum AddressPart { + LocalPart, + Domain, + All, + User, + Detail, + Name, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) enum MatchType { + Is, + Contains, + Matches(u64), + Regex(u64), + Value(RelationalMatch), + Count(RelationalMatch), + List, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) enum RelationalMatch { + Gt, + Ge, + Lt, + Le, + Eq, + Ne, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +pub enum Comparator { + Elbonia, + Octet, + AsciiCaseMap, + AsciiNumeric, + Other(String), +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Clear { + pub(crate) local_vars_idx: u32, + pub(crate) local_vars_num: u32, + pub(crate) match_vars: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Invalid { + pub(crate) name: String, + pub(crate) line_num: usize, + pub(crate) line_pos: usize, +} + +#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] +pub(crate) struct ForEveryLine { + pub var_idx: usize, + pub jz_pos: usize, +} + +impl<'x> CompilerState<'x> { + #[inline(always)] + pub fn expect_instruction_end(&mut self) -> Result<(), CompileError> { + self.tokens.expect_token(Token::Semicolon) + } + + pub fn ignore_instruction(&mut self) -> Result<(), CompileError> { + // Skip entire instruction + let mut curly_count = 0; + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Semicolon if curly_count == 0 => { + break; + } + Token::CurlyOpen => { + curly_count += 1; + } + Token::CurlyClose => match curly_count { + 0 => { + return Err(token_info.expected("instruction")); + } + 1 => { + break; + } + _ => curly_count -= 1, + }, + _ => (), + } + } + + Ok(()) + } + + pub fn ignore_test(&mut self) -> Result<(), CompileError> { + let mut d_count = 0; + while let Some(token_info) = self.tokens.peek() { + match token_info?.token { + Token::ParenthesisOpen => { + d_count += 1; + } + Token::ParenthesisClose => { + if d_count == 0 { + break; + } else { + d_count -= 1; + } + } + Token::Comma => { + if d_count == 0 { + break; + } + } + Token::CurlyOpen => { + break; + } + _ => (), + } + self.tokens.next(); + } + + Ok(()) + } + + pub fn parse_match_type(&mut self, word: Word) -> Result { + match word { + Word::Is => Ok(MatchType::Is), + Word::Contains => Ok(MatchType::Contains), + Word::Matches => { + self.block.match_test_pos.push(self.instructions.len()); + Ok(MatchType::Matches(0)) + } + Word::Regex => { + self.block.match_test_pos.push(self.instructions.len()); + Ok(MatchType::Regex(0)) + } + Word::List => Ok(MatchType::List), + _ => { + let token_info = self.tokens.unwrap_next()?; + if let Token::StringConstant(text) = &token_info.token { + if let Some(relational) = RELATIONAL.get(text.to_string().as_ref()) { + return Ok(if word == Word::Value { + MatchType::Value(*relational) + } else { + MatchType::Count(*relational) + }); + } + } + Err(token_info.expected("relational match")) + } + } + } + + pub(crate) fn parse_comparator(&mut self) -> Result { + let comparator = self.tokens.expect_static_string()?; + Ok(if let Some(comparator) = COMPARATOR.get(&comparator) { + comparator.clone() + } else { + Comparator::Other(comparator) + }) + } + + pub(crate) fn parse_static_strings(&mut self) -> Result, CompileError> { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::BracketOpen => { + let mut strings = Vec::new(); + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::StringConstant(string) => { + strings.push(string.into_string()); + } + Token::Comma => (), + Token::BracketClose if !strings.is_empty() => break, + _ => return Err(token_info.expected("constant string")), + } + } + Ok(strings) + } + Token::StringConstant(string) => Ok(vec![string.into_string()]), + _ => Err(token_info.expected("'[' or constant string")), + } + } + + pub fn parse_string(&mut self) -> Result { + let next_token = self.tokens.unwrap_next()?; + match next_token.token { + Token::StringConstant(s) => Ok(Value::from(s)), + Token::StringVariable(s) => { + self.tokenize_string(&s, true) + .map_err(|error_type| CompileError { + line_num: next_token.line_num, + line_pos: next_token.line_pos, + error_type, + }) + } + Token::BracketOpen => { + let mut items = self.parse_string_list(false)?; + match items.pop() { + Some(s) if items.is_empty() => Ok(s), + _ => Err(next_token.expected("string")), + } + } + _ => Err(next_token.expected("string")), + } + } + + pub(crate) fn parse_strings(&mut self, allow_empty: bool) -> Result, CompileError> { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::BracketOpen => self.parse_string_list(allow_empty), + Token::StringConstant(s) => Ok(vec![Value::from(s)]), + Token::StringVariable(s) => { + self.tokenize_string(&s, true) + .map(|s| vec![s]) + .map_err(|error_type| CompileError { + line_num: token_info.line_num, + line_pos: token_info.line_pos, + error_type, + }) + } + _ => Err(token_info.expected("'[' or string")), + } + } + + pub(crate) fn parse_string_token( + &mut self, + token_info: TokenInfo, + ) -> Result { + match token_info.token { + Token::StringConstant(s) => Ok(Value::from(s)), + Token::StringVariable(s) => { + self.tokenize_string(&s, true) + .map_err(|error_type| CompileError { + line_num: token_info.line_num, + line_pos: token_info.line_pos, + error_type, + }) + } + _ => Err(token_info.expected("string")), + } + } + + pub(crate) fn parse_strings_token( + &mut self, + token_info: TokenInfo, + ) -> Result, CompileError> { + match token_info.token { + Token::StringConstant(s) => Ok(vec![Value::from(s)]), + Token::StringVariable(s) => { + self.tokenize_string(&s, true) + .map(|s| vec![s]) + .map_err(|error_type| CompileError { + line_num: token_info.line_num, + line_pos: token_info.line_pos, + error_type, + }) + } + Token::BracketOpen => self.parse_string_list(false), + _ => Err(token_info.expected("string")), + } + } + + pub(crate) fn parse_string_list( + &mut self, + allow_empty: bool, + ) -> Result, CompileError> { + let mut strings = Vec::new(); + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::StringConstant(s) => { + strings.push(Value::from(s)); + } + Token::StringVariable(s) => { + strings.push(self.tokenize_string(&s, true).map_err(|error_type| { + CompileError { + line_num: token_info.line_num, + line_pos: token_info.line_pos, + error_type, + } + })?); + } + Token::Comma => (), + Token::BracketClose if !strings.is_empty() || allow_empty => break, + _ => return Err(token_info.expected("string or string list")), + } + } + Ok(strings) + } + + #[inline(always)] + pub(crate) fn has_capability(&self, capability: &Capability) -> bool { + [&self.block] + .into_iter() + .chain(self.block_stack.iter()) + .any(|b| b.capabilities.contains(capability)) + } + + #[inline(always)] + pub(crate) fn reset_param_check(&mut self) { + self.param_check.fill(false); + } + + #[inline(always)] + pub(crate) fn validate_argument( + &mut self, + arg_num: usize, + capability: Option, + line_num: usize, + line_pos: usize, + ) -> Result<(), CompileError> { + if arg_num > 0 { + if let Some(param) = self.param_check.get_mut(arg_num - 1) { + if !*param { + *param = true; + } else { + return Err(CompileError { + line_num, + line_pos, + error_type: ErrorType::DuplicatedParameter, + }); + } + } else { + #[cfg(test)] + panic!("Argument out of range {arg_num}"); + } + } + if let Some(capability) = capability { + if !self.has_capability(&capability) { + return Err(CompileError { + line_num, + line_pos, + error_type: ErrorType::UndeclaredCapability(capability), + }); + } + } + + Ok(()) + } + + pub(crate) fn validate_match( + &mut self, + match_type: &MatchType, + key_list: &mut [Value], + ) -> Result<(), CompileError> { + if matches!(match_type, MatchType::Regex(_)) { + for key in key_list { + if let Value::Text(expr) = key { + match fancy_regex::Regex::new(expr) { + Ok(regex) => { + *key = Value::Regex(Regex { + regex, + expr: std::mem::take(expr), + }); + } + Err(err) => { + return Err(self + .tokens + .unwrap_next()? + .custom(ErrorType::InvalidRegex(format!("{expr}: {err}")))); + } + } + } + } + } + Ok(()) + } +} + +impl Capability { + pub fn parse(capability: &str) -> Capability { + if let Some(capability) = CAPABILITIES.get(capability) { + capability.clone() + } else if let Some(comparator) = capability.strip_prefix("comparator-") { + Capability::Comparator(Comparator::Other(comparator.to_string())) + } else { + Capability::Other(capability.to_string()) + } + } + + pub fn all() -> &'static [Capability] { + &[ + Capability::Envelope, + Capability::EnvelopeDsn, + Capability::EnvelopeDeliverBy, + Capability::FileInto, + Capability::EncodedCharacter, + Capability::Comparator(Comparator::Elbonia), + Capability::Comparator(Comparator::AsciiCaseMap), + Capability::Comparator(Comparator::AsciiNumeric), + Capability::Comparator(Comparator::Octet), + Capability::Body, + Capability::Convert, + Capability::Copy, + Capability::Relational, + Capability::Date, + Capability::Index, + Capability::Duplicate, + Capability::Variables, + Capability::EditHeader, + Capability::ForEveryPart, + Capability::Mime, + Capability::Replace, + Capability::Enclose, + Capability::ExtractText, + Capability::Enotify, + Capability::RedirectDsn, + Capability::RedirectDeliverBy, + Capability::Environment, + Capability::Reject, + Capability::Ereject, + Capability::ExtLists, + Capability::SubAddress, + Capability::Vacation, + Capability::VacationSeconds, + Capability::Fcc, + Capability::Mailbox, + Capability::MailboxId, + Capability::MboxMetadata, + Capability::ServerMetadata, + Capability::SpecialUse, + Capability::Imap4Flags, + Capability::Ihave, + Capability::ImapSieve, + Capability::Include, + Capability::Regex, + Capability::SpamTest, + Capability::SpamTestPlus, + Capability::VirusTest, + ] + } +} + +static RELATIONAL: phf::Map<&'static str, RelationalMatch> = phf_map! { + "gt" => RelationalMatch::Gt, + "ge" => RelationalMatch::Ge, + "lt" => RelationalMatch::Lt, + "le" => RelationalMatch::Le, + "eq" => RelationalMatch::Eq, + "ne" => RelationalMatch::Ne, +}; + +static COMPARATOR: phf::Map<&'static str, Comparator> = phf_map! { + "i;octet" => Comparator::Octet, + "i;ascii-casemap" => Comparator::AsciiCaseMap, + "i;ascii-numeric" => Comparator::AsciiNumeric, +}; + +impl Invalid { + pub fn name(&self) -> &str { + &self.name + } + + pub fn line_num(&self) -> usize { + self.line_num + } + + pub fn line_pos(&self) -> usize { + self.line_pos + } +} + +impl From<&str> for Capability { + fn from(value: &str) -> Self { + Capability::parse(value) + } +} + +impl From for Capability { + fn from(value: String) -> Self { + Capability::parse(&value) + } +} + +impl Display for Capability { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Capability::Envelope => f.write_str("envelope"), + Capability::EnvelopeDsn => f.write_str("envelope-dsn"), + Capability::EnvelopeDeliverBy => f.write_str("envelope-deliverby"), + Capability::FileInto => f.write_str("fileinto"), + Capability::EncodedCharacter => f.write_str("encoded-character"), + Capability::Comparator(Comparator::Elbonia) => f.write_str("comparator-elbonia"), + Capability::Comparator(Comparator::Octet) => f.write_str("comparator-i;octet"), + Capability::Comparator(Comparator::AsciiCaseMap) => { + f.write_str("comparator-i;ascii-casemap") + } + Capability::Comparator(Comparator::AsciiNumeric) => { + f.write_str("comparator-i;ascii-numeric") + } + Capability::Comparator(Comparator::Other(comparator)) => f.write_str(comparator), + Capability::Body => f.write_str("body"), + Capability::Convert => f.write_str("convert"), + Capability::Copy => f.write_str("copy"), + Capability::Relational => f.write_str("relational"), + Capability::Date => f.write_str("date"), + Capability::Index => f.write_str("index"), + Capability::Duplicate => f.write_str("duplicate"), + Capability::Variables => f.write_str("variables"), + Capability::EditHeader => f.write_str("editheader"), + Capability::ForEveryPart => f.write_str("foreverypart"), + Capability::Mime => f.write_str("mime"), + Capability::Replace => f.write_str("replace"), + Capability::Enclose => f.write_str("enclose"), + Capability::ExtractText => f.write_str("extracttext"), + Capability::Enotify => f.write_str("enotify"), + Capability::RedirectDsn => f.write_str("redirect-dsn"), + Capability::RedirectDeliverBy => f.write_str("redirect-deliverby"), + Capability::Environment => f.write_str("environment"), + Capability::Reject => f.write_str("reject"), + Capability::Ereject => f.write_str("ereject"), + Capability::ExtLists => f.write_str("extlists"), + Capability::SubAddress => f.write_str("subaddress"), + Capability::Vacation => f.write_str("vacation"), + Capability::VacationSeconds => f.write_str("vacation-seconds"), + Capability::Fcc => f.write_str("fcc"), + Capability::Mailbox => f.write_str("mailbox"), + Capability::MailboxId => f.write_str("mailboxid"), + Capability::MboxMetadata => f.write_str("mboxmetadata"), + Capability::ServerMetadata => f.write_str("servermetadata"), + Capability::SpecialUse => f.write_str("special-use"), + Capability::Imap4Flags => f.write_str("imap4flags"), + Capability::Ihave => f.write_str("ihave"), + Capability::ImapSieve => f.write_str("imapsieve"), + Capability::Include => f.write_str("include"), + Capability::Regex => f.write_str("regex"), + Capability::SpamTest => f.write_str("spamtest"), + Capability::SpamTestPlus => f.write_str("spamtestplus"), + Capability::VirusTest => f.write_str("virustest"), + Capability::Plugins => f.write_str("vnd.stalwart.plugins"), + Capability::ForEveryLine => f.write_str("vnd.stalwart.foreveryline"), + Capability::Eval => f.write_str("vnd.stalwart.eval"), + Capability::Other(capability) => f.write_str(capability), + } + } +} + +static CAPABILITIES: phf::Map<&'static str, Capability> = phf_map! { + "envelope" => Capability::Envelope, + "envelope-dsn" => Capability::EnvelopeDsn, + "envelope-deliverby" => Capability::EnvelopeDeliverBy, + "fileinto" => Capability::FileInto, + "encoded-character" => Capability::EncodedCharacter, + "comparator-elbonia" => Capability::Comparator(Comparator::Elbonia), + "comparator-i;octet" => Capability::Comparator(Comparator::Octet), + "comparator-i;ascii-casemap" => Capability::Comparator(Comparator::AsciiCaseMap), + "comparator-i;ascii-numeric" => Capability::Comparator(Comparator::AsciiNumeric), + "body" => Capability::Body, + "convert" => Capability::Convert, + "copy" => Capability::Copy, + "relational" => Capability::Relational, + "date" => Capability::Date, + "index" => Capability::Index, + "duplicate" => Capability::Duplicate, + "variables" => Capability::Variables, + "editheader" => Capability::EditHeader, + "foreverypart" => Capability::ForEveryPart, + "mime" => Capability::Mime, + "replace" => Capability::Replace, + "enclose" => Capability::Enclose, + "extracttext" => Capability::ExtractText, + "enotify" => Capability::Enotify, + "redirect-dsn" => Capability::RedirectDsn, + "redirect-deliverby" => Capability::RedirectDeliverBy, + "environment" => Capability::Environment, + "reject" => Capability::Reject, + "ereject" => Capability::Ereject, + "extlists" => Capability::ExtLists, + "subaddress" => Capability::SubAddress, + "vacation" => Capability::Vacation, + "vacation-seconds" => Capability::VacationSeconds, + "fcc" => Capability::Fcc, + "mailbox" => Capability::Mailbox, + "mailboxid" => Capability::MailboxId, + "mboxmetadata" => Capability::MboxMetadata, + "servermetadata" => Capability::ServerMetadata, + "special-use" => Capability::SpecialUse, + "imap4flags" => Capability::Imap4Flags, + "ihave" => Capability::Ihave, + "imapsieve" => Capability::ImapSieve, + "include" => Capability::Include, + "regex" => Capability::Regex, + "spamtest" => Capability::SpamTest, + "spamtestplus" => Capability::SpamTestPlus, + "virustest" => Capability::VirusTest, + + // Extensions + "vnd.stalwart.plugins" => Capability::Plugins, + "vnd.stalwart.foreveryline" => Capability::ForEveryLine, + "vnd.stalwart.eval" => Capability::Eval, +}; diff --git a/melib/src/sieve/compiler/grammar/test.rs b/melib/src/sieve/compiler/grammar/test.rs new file mode 100644 index 00000000..c8f27fb7 --- /dev/null +++ b/melib/src/sieve/compiler/grammar/test.rs @@ -0,0 +1,703 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use serde::{Deserialize, Serialize}; + +use crate::sieve::compiler::{ + lexer::{tokenizer::TokenInfo, word::Word, Token}, + CompileError, ErrorType, +}; + +use super::{ + actions::{action_convert::Convert, action_vacation::TestVacation}, + expr::{parser::ExpressionParser, tokenizer::Tokenizer, Expression}, + instruction::{CompilerState, Instruction}, + tests::{ + test_address::TestAddress, + test_body::TestBody, + test_date::{TestCurrentDate, TestDate}, + test_duplicate::TestDuplicate, + test_envelope::TestEnvelope, + test_exists::TestExists, + test_extlists::TestValidExtList, + test_hasflag::TestHasFlag, + test_header::TestHeader, + test_ihave::TestIhave, + test_mailbox::{TestMailboxExists, TestMetadata, TestMetadataExists}, + test_mailboxid::TestMailboxIdExists, + test_notify::{TestNotifyMethodCapability, TestValidNotifyMethod}, + test_plugin::Plugin, + test_size::TestSize, + test_spamtest::{TestSpamTest, TestVirusTest}, + test_specialuse::TestSpecialUseExists, + test_string::TestString, + }, + Capability, Invalid, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) enum Test { + True, + False, + Address(TestAddress), + Envelope(TestEnvelope), + Exists(TestExists), + Header(TestHeader), + Size(TestSize), + Invalid(Invalid), + + // RFC 5173 + Body(TestBody), + + // RFC 6558 + Convert(Convert), + + // RFC 5260 + Date(TestDate), + CurrentDate(TestCurrentDate), + + // RFC 7352 + Duplicate(TestDuplicate), + + // RFC 5229 & RFC 5183 + String(TestString), + Environment(TestString), + + // RFC 5435 + NotifyMethodCapability(TestNotifyMethodCapability), + ValidNotifyMethod(TestValidNotifyMethod), + + // RFC 6134 + ValidExtList(TestValidExtList), + + // RFC 5463 + Ihave(TestIhave), + + // RFC 5232 + HasFlag(TestHasFlag), + + // RFC 5490 + MailboxExists(TestMailboxExists), + Metadata(TestMetadata), + MetadataExists(TestMetadataExists), + + // RFC 9042 + MailboxIdExists(TestMailboxIdExists), + + // RFC 5235 + SpamTest(TestSpamTest), + VirusTest(TestVirusTest), + + // RFC 8579 + SpecialUseExists(TestSpecialUseExists), + + // RFC 5230 + Vacation(TestVacation), + + // Stalwart proprietary + EvalExpression(EvalExpression), + Plugin(Plugin), +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct EvalExpression { + pub expr: Vec, + pub is_not: bool, +} + +#[derive(Debug)] +struct Block { + is_all: bool, + is_not: bool, + p_count: u32, + jmps: Vec, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_test(&mut self) -> Result<(), CompileError> { + let mut block_stack: Vec = Vec::new(); + let mut block = Block { + is_all: false, + is_not: false, + p_count: 0, + jmps: Vec::new(), + }; + let mut is_not = false; + + loop { + let token_info = self.tokens.unwrap_next()?; + self.reset_param_check(); + let test = match token_info.token { + Token::Comma + if !block_stack.is_empty() + && matches!(self.instructions.last(), Some(Instruction::Test(_))) + && matches!( + self.tokens.peek(), + Some(Ok(TokenInfo { + token: Token::Identifier(_) | Token::Unknown(_), + .. + })) + ) => + { + is_not = block.is_not; + block.jmps.push(self.instructions.len()); + self.instructions.push(if block.is_all { + Instruction::Jz(usize::MAX) + } else { + Instruction::Jnz(usize::MAX) + }); + continue; + } + Token::ParenthesisOpen => { + block.p_count += 1; + continue; + } + Token::ParenthesisClose => { + if block.p_count > 0 { + block.p_count -= 1; + continue; + } else if let Some(prev_block) = block_stack.pop() { + let cur_pos = self.instructions.len(); + for jmp_pos in block.jmps { + if let Instruction::Jnz(jmp_pos) | Instruction::Jz(jmp_pos) = + &mut self.instructions[jmp_pos] + { + *jmp_pos = cur_pos; + } else { + debug_assert!(false, "This should not have happened") + } + } + + block = prev_block; + is_not = block.is_not; + if block_stack.is_empty() { + break; + } else { + continue; + } + } else { + return Err(token_info.expected("test name")); + } + } + Token::Identifier(Word::Not) => { + if !matches!( + self.tokens.peek(), + Some(Ok(TokenInfo { + token: Token::Identifier(_) | Token::Unknown(_), + .. + })) + ) { + return Err(token_info.expected("test name")); + } + is_not = !is_not; + continue; + } + Token::Identifier(word @ (Word::AnyOf | Word::AllOf)) => { + if block_stack.len() < self.tokens.compiler.max_nested_tests { + self.tokens.expect_token(Token::ParenthesisOpen)?; + block_stack.push(block); + let (is_all, block_is_not) = if word == Word::AllOf { + if !is_not { + (true, false) + } else { + (false, true) + } + } else if !is_not { + (false, false) + } else { + (true, true) + }; + block = Block { + is_all, + is_not: block_is_not, + p_count: 0, + jmps: Vec::new(), + }; + is_not = block_is_not; + continue; + } else { + return Err(CompileError { + line_num: token_info.line_num, + line_pos: token_info.line_pos, + error_type: ErrorType::TooManyNestedTests, + }); + } + } + Token::Identifier(Word::True) => { + if !is_not { + Test::True + } else { + is_not = false; + Test::False + } + } + Token::Identifier(Word::False) => { + if !is_not { + Test::False + } else { + is_not = false; + Test::True + } + } + Token::Identifier(Word::Address) => self.parse_test_address()?, + Token::Identifier(Word::Envelope) => { + self.validate_argument( + 0, + Capability::Envelope.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_envelope()? + } + Token::Identifier(Word::Header) => self.parse_test_header()?, + Token::Identifier(Word::Size) => self.parse_test_size()?, + Token::Identifier(Word::Exists) => self.parse_test_exists()?, + + // RFC 5173 + Token::Identifier(Word::Body) => { + self.validate_argument( + 0, + Capability::Body.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_body()? + } + + // RFC 6558 + Token::Identifier(Word::Convert) => { + self.validate_argument( + 0, + Capability::Convert.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_convert()? + } + + // RFC 5260 + Token::Identifier(Word::Date) => { + self.validate_argument( + 0, + Capability::Date.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_date()? + } + Token::Identifier(Word::CurrentDate) => { + self.validate_argument( + 0, + Capability::Date.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_currentdate()? + } + + // RFC 7352 + Token::Identifier(Word::Duplicate) => { + self.validate_argument( + 0, + Capability::Duplicate.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_duplicate()? + } + + // RFC 5229 + Token::Identifier(Word::String) => { + self.validate_argument( + 0, + Capability::Variables.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_string()? + } + + // RFC 5435 + Token::Identifier(Word::NotifyMethodCapability) => { + self.validate_argument( + 0, + Capability::Enotify.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_notify_method_capability()? + } + Token::Identifier(Word::ValidNotifyMethod) => { + self.validate_argument( + 0, + Capability::Enotify.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_valid_notify_method()? + } + + // RFC 5183 + Token::Identifier(Word::Environment) => { + self.validate_argument( + 0, + Capability::Environment.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_environment()? + } + + // RFC 6134 + Token::Identifier(Word::ValidExtList) => { + self.validate_argument( + 0, + Capability::ExtLists.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_valid_ext_list()? + } + + // RFC 5463 + Token::Identifier(Word::Ihave) => { + self.validate_argument( + 0, + Capability::Ihave.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_ihave()? + } + + // RFC 5232 + Token::Identifier(Word::HasFlag) => { + self.validate_argument( + 0, + Capability::Imap4Flags.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_hasflag()? + } + + // RFC 5490 + Token::Identifier(Word::MailboxExists) => { + self.validate_argument( + 0, + Capability::Mailbox.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_mailboxexists()? + } + Token::Identifier(Word::Metadata) => { + self.validate_argument( + 0, + Capability::MboxMetadata.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_metadata()? + } + Token::Identifier(Word::MetadataExists) => { + self.validate_argument( + 0, + Capability::MboxMetadata.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_metadataexists()? + } + Token::Identifier(Word::ServerMetadata) => { + self.validate_argument( + 0, + Capability::ServerMetadata.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_servermetadata()? + } + Token::Identifier(Word::ServerMetadataExists) => { + self.validate_argument( + 0, + Capability::ServerMetadata.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_servermetadataexists()? + } + + // RFC 9042 + Token::Identifier(Word::MailboxIdExists) => { + self.validate_argument( + 0, + Capability::MailboxId.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_mailboxidexists()? + } + + // RFC 5235 + Token::Identifier(Word::SpamTest) => { + self.validate_argument( + 0, + Capability::SpamTest.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_spamtest()? + } + Token::Identifier(Word::VirusTest) => { + self.validate_argument( + 0, + Capability::VirusTest.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_virustest()? + } + + // RFC 8579 + Token::Identifier(Word::SpecialUseExists) => { + self.validate_argument( + 0, + Capability::SpecialUse.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_specialuseexists()? + } + Token::Identifier(Word::Eval) => { + self.validate_argument( + 0, + Capability::Eval.into(), + token_info.line_num, + token_info.line_pos, + )?; + + let mut next_token = self.tokens.unwrap_next()?; + let expr = match next_token.token { + Token::StringConstant(s) => s.into_string().into_bytes(), + Token::StringVariable(s) => s, + _ => return Err(next_token.expected("string")), + }; + + match ExpressionParser::from_tokenizer(Tokenizer::from_iter( + expr.iter().enumerate().peekable(), + |var_name, maybe_namespace| { + self.parse_expr_fnc_or_var(var_name, maybe_namespace) + }, + )) + .parse() + { + Ok(parser) => Test::EvalExpression(EvalExpression { + expr: parser.output, + is_not: false, + }), + Err(err) => { + let err = ErrorType::InvalidExpression(format!( + "{}: {}", + std::str::from_utf8(&expr).unwrap_or_default(), + err + )); + next_token.token = Token::StringVariable(expr); + return Err(next_token.custom(err)); + } + } + } + + Token::Identifier(word) => { + self.ignore_test()?; + Test::Invalid(Invalid { + name: word.to_string(), + line_num: token_info.line_num, + line_pos: token_info.line_pos, + }) + } + #[cfg(test)] + Token::Unknown(name) if name.contains("test") => { + use crate::sieve::compiler::Value; + + let mut arguments = Vec::new(); + arguments.push(crate::sieve::PluginArgument::Text(Value::Text(name))); + while !matches!( + self.tokens.peek().map(|r| r.map(|t| &t.token)), + Some(Ok(Token::Comma + | Token::ParenthesisClose + | Token::CurlyOpen)) + ) { + arguments.push(crate::sieve::PluginArgument::Text( + match self.tokens.unwrap_next()?.token { + Token::StringConstant(s) => Value::from(s), + Token::StringVariable(s) => self + .tokenize_string(&s, true) + .map_err(|error_type| CompileError { + line_num: 0, + line_pos: 0, + error_type, + })?, + Token::Number(n) => { + Value::Number(crate::sieve::compiler::Number::Integer(n as i64)) + } + Token::Identifier(s) => Value::Text(s.to_string()), + Token::Tag(s) => Value::Text(format!(":{s}")), + Token::Unknown(s) => Value::Text(s), + other => panic!("Invalid test param {other:?}"), + }, + )); + } + Test::Plugin(Plugin { + id: u32::MAX, + arguments, + is_not: false, + }) + } + Token::Unknown(name) => { + if let Some(schema) = self.compiler.plugins.get(&name) { + self.validate_argument( + 0, + Capability::Plugins.into(), + token_info.line_num, + token_info.line_pos, + )?; + self.parse_test_plugin(schema)? + } else { + self.ignore_test()?; + Test::Invalid(Invalid { + name, + line_num: token_info.line_num, + line_pos: token_info.line_pos, + }) + } + } + _ => return Err(token_info.expected("test name")), + }; + + while block.p_count > 0 { + self.tokens.expect_token(Token::ParenthesisClose)?; + block.p_count -= 1; + } + + self.instructions.push(Instruction::Test(if !is_not { + test + } else { + test.set_not() + })); + + if block_stack.is_empty() { + break; + } + } + + self.instructions.push(Instruction::Jz(usize::MAX)); + Ok(()) + } +} + +impl Test { + pub fn set_not(mut self) -> Self { + match &mut self { + Test::True => return Test::False, + Test::False => return Test::True, + Test::Address(op) => { + op.is_not = true; + } + Test::Envelope(op) => { + op.is_not = true; + } + Test::Exists(op) => { + op.is_not = true; + } + Test::Header(op) => { + op.is_not = true; + } + Test::Size(op) => { + op.is_not = true; + } + Test::Body(op) => { + op.is_not = true; + } + Test::Convert(op) => { + op.is_not = true; + } + Test::Date(op) => { + op.is_not = true; + } + Test::CurrentDate(op) => { + op.is_not = true; + } + Test::Duplicate(op) => { + op.is_not = true; + } + Test::String(op) | Test::Environment(op) => { + op.is_not = true; + } + Test::NotifyMethodCapability(op) => { + op.is_not = true; + } + Test::ValidNotifyMethod(op) => { + op.is_not = true; + } + Test::ValidExtList(op) => { + op.is_not = true; + } + Test::Ihave(op) => { + op.is_not = true; + } + Test::HasFlag(op) => { + op.is_not = true; + } + Test::MailboxExists(op) => { + op.is_not = true; + } + Test::Metadata(op) => { + op.is_not = true; + } + Test::MetadataExists(op) => { + op.is_not = true; + } + Test::MailboxIdExists(op) => { + op.is_not = true; + } + Test::SpamTest(op) => { + op.is_not = true; + } + Test::VirusTest(op) => { + op.is_not = true; + } + Test::SpecialUseExists(op) => { + op.is_not = true; + } + Test::Plugin(op) => { + op.is_not = true; + } + Test::EvalExpression(op) => { + op.is_not = true; + } + Test::Vacation(_) | Test::Invalid(_) => {} + } + self + } +} diff --git a/melib/src/sieve/compiler/grammar/tests/mod.rs b/melib/src/sieve/compiler/grammar/tests/mod.rs new file mode 100644 index 00000000..3797415f --- /dev/null +++ b/melib/src/sieve/compiler/grammar/tests/mod.rs @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +pub mod test_address; +pub mod test_body; +pub mod test_date; +pub mod test_duplicate; +pub mod test_envelope; +pub mod test_environment; +pub mod test_exists; +pub mod test_extlists; +pub mod test_hasflag; +pub mod test_header; +pub mod test_ihave; +pub mod test_mailbox; +pub mod test_mailboxid; +pub mod test_notify; +pub mod test_plugin; +pub mod test_size; +pub mod test_spamtest; +pub mod test_specialuse; +pub mod test_string; diff --git a/melib/src/sieve/compiler/grammar/tests/test_address.rs b/melib/src/sieve/compiler/grammar/tests/test_address.rs new file mode 100644 index 00000000..6ee11bcc --- /dev/null +++ b/melib/src/sieve/compiler/grammar/tests/test_address.rs @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use serde::{Deserialize, Serialize}; + +use crate::sieve::compiler::{ + grammar::{instruction::CompilerState, test::Test, Capability, Comparator}, + lexer::{word::Word, Token}, + CompileError, Value, +}; + +use crate::sieve::compiler::grammar::{AddressPart, MatchType}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct TestAddress { + pub header_list: Vec, + pub key_list: Vec, + pub address_part: AddressPart, + pub match_type: MatchType, + pub comparator: Comparator, + pub index: Option, + + pub mime_anychild: bool, + pub is_not: bool, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_test_address(&mut self) -> Result { + let mut address_part = AddressPart::All; + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let mut header_list = None; + let mut key_list; + let mut index = None; + let mut index_last = false; + + let mut mime = false; + let mut mime_anychild = false; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::LocalPart + | Word::Domain + | Word::All + | Word::User + | Word::Detail + | Word::Name), + ) => { + self.validate_argument( + 1, + if matches!(word, Word::User | Word::Detail) { + Capability::SubAddress.into() + } else { + None + }, + token_info.line_num, + token_info.line_pos, + )?; + address_part = word.into(); + } + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex + | Word::List), + ) => { + self.validate_argument( + 2, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?; + comparator = self.parse_comparator()?; + } + Token::Tag(Word::Index) => { + self.validate_argument( + 4, + Capability::Index.into(), + token_info.line_num, + token_info.line_pos, + )?; + index = (self.tokens.expect_number(u16::MAX as usize)? as i32).into(); + } + Token::Tag(Word::Last) => { + self.validate_argument( + 5, + Capability::Index.into(), + token_info.line_num, + token_info.line_pos, + )?; + index_last = true; + } + Token::Tag(Word::Mime) => { + self.validate_argument( + 6, + Capability::Mime.into(), + token_info.line_num, + token_info.line_pos, + )?; + mime = true; + } + Token::Tag(Word::AnyChild) => { + self.validate_argument( + 7, + Capability::Mime.into(), + token_info.line_num, + token_info.line_pos, + )?; + mime_anychild = true; + } + _ => { + if header_list.is_none() { + header_list = self.parse_strings_token(token_info)?.into(); + } else { + key_list = self.parse_strings_token(token_info)?; + break; + } + } + } + } + + if !mime && mime_anychild { + return Err(self.tokens.unwrap_next()?.missing_tag(":mime")); + } + self.validate_match(&match_type, &mut key_list)?; + + Ok(Test::Address(TestAddress { + header_list: header_list.unwrap(), + key_list, + address_part, + match_type, + comparator, + index: if index_last { index.map(|i| -i) } else { index }, + mime_anychild, + is_not: false, + })) + } +} + +impl From for AddressPart { + fn from(word: Word) -> Self { + match word { + Word::LocalPart => AddressPart::LocalPart, + Word::Domain => AddressPart::Domain, + Word::All => AddressPart::All, + Word::User => AddressPart::User, + Word::Detail => AddressPart::Detail, + Word::Name => AddressPart::Name, + _ => unreachable!(), + } + } +} diff --git a/melib/src/sieve/compiler/grammar/tests/test_body.rs b/melib/src/sieve/compiler/grammar/tests/test_body.rs new file mode 100644 index 00000000..d319c7d4 --- /dev/null +++ b/melib/src/sieve/compiler/grammar/tests/test_body.rs @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use serde::{Deserialize, Serialize}; + +use crate::sieve::compiler::{ + grammar::{instruction::CompilerState, Capability, Comparator}, + lexer::{word::Word, Token}, + CompileError, Value, +}; + +use crate::sieve::compiler::grammar::{test::Test, MatchType}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct TestBody { + pub key_list: Vec, + pub body_transform: BodyTransform, + pub match_type: MatchType, + pub comparator: Comparator, + pub include_subject: bool, + pub is_not: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) enum BodyTransform { + Raw, + Content(Vec), + Text, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_test_body(&mut self) -> Result { + let mut body_transform = BodyTransform::Text; + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let mut key_list; + let mut include_subject = false; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag(Word::Raw) => { + self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?; + body_transform = BodyTransform::Raw; + } + Token::Tag(Word::Text) => { + self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?; + body_transform = BodyTransform::Text; + } + Token::Tag(Word::Content) => { + self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?; + body_transform = BodyTransform::Content(self.parse_strings(false)?); + } + Token::Tag(Word::Subject) => { + self.validate_argument(4, None, token_info.line_num, token_info.line_pos)?; + include_subject = true; + } + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex), + ) => { + self.validate_argument( + 2, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?; + comparator = self.parse_comparator()?; + } + _ => { + key_list = self.parse_strings_token(token_info)?; + break; + } + } + } + self.validate_match(&match_type, &mut key_list)?; + + Ok(Test::Body(TestBody { + key_list, + body_transform, + match_type, + comparator, + include_subject, + is_not: false, + })) + } +} diff --git a/melib/src/sieve/compiler/grammar/tests/test_date.rs b/melib/src/sieve/compiler/grammar/tests/test_date.rs new file mode 100644 index 00000000..4d04f9f3 --- /dev/null +++ b/melib/src/sieve/compiler/grammar/tests/test_date.rs @@ -0,0 +1,354 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use mail_parser::HeaderName; +use phf::phf_map; +use serde::{Deserialize, Serialize}; + +use crate::sieve::compiler::{ + grammar::{instruction::CompilerState, Capability, Comparator}, + lexer::{word::Word, StringConstant, Token}, + CompileError, ErrorType, Number, Value, +}; + +use crate::sieve::compiler::grammar::{test::Test, MatchType}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct TestDate { + pub header_name: Value, + pub key_list: Vec, + pub match_type: MatchType, + pub comparator: Comparator, + pub index: Option, + pub zone: Zone, + pub date_part: DatePart, + pub mime_anychild: bool, + pub is_not: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct TestCurrentDate { + pub zone: Option, + pub match_type: MatchType, + pub comparator: Comparator, + pub date_part: DatePart, + pub key_list: Vec, + pub is_not: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) enum Zone { + Time(i64), + Original, + Local, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) enum DatePart { + Year, + Month, + Day, + Date, + Julian, + Hour, + Minute, + Second, + Time, + Iso8601, + Std11, + Zone, + Weekday, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_test_date(&mut self) -> Result { + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let mut header_name = None; + let mut key_list; + let mut index = None; + let mut index_last = false; + let mut zone = Zone::Local; + let mut date_part = None; + + let mut mime = false; + let mut mime_anychild = false; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex + | Word::List), + ) => { + self.validate_argument( + 1, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + comparator = self.parse_comparator()?; + } + Token::Tag(Word::Index) => { + self.validate_argument( + 3, + Capability::Index.into(), + token_info.line_num, + token_info.line_pos, + )?; + index = (self.tokens.expect_number(u16::MAX as usize)? as i32).into(); + } + Token::Tag(Word::Last) => { + self.validate_argument( + 4, + Capability::Index.into(), + token_info.line_num, + token_info.line_pos, + )?; + index_last = true; + } + Token::Tag(Word::Mime) => { + self.validate_argument( + 5, + Capability::Mime.into(), + token_info.line_num, + token_info.line_pos, + )?; + mime = true; + } + Token::Tag(Word::AnyChild) => { + self.validate_argument( + 6, + Capability::Mime.into(), + token_info.line_num, + token_info.line_pos, + )?; + mime_anychild = true; + } + Token::Tag(Word::OriginalZone) => { + self.validate_argument(7, None, token_info.line_num, token_info.line_pos)?; + zone = Zone::Original; + } + Token::Tag(Word::Zone) => { + self.validate_argument(7, None, token_info.line_num, token_info.line_pos)?; + zone = Zone::Time(self.parse_timezone()?); + } + _ => { + if header_name.is_none() { + let header = self.parse_string_token(token_info)?; + if let Value::Text(header_name) = &header { + if HeaderName::parse(header_name).is_none() { + return Err(self + .tokens + .unwrap_next()? + .custom(ErrorType::InvalidHeaderName)); + } + } + header_name = header.into(); + } else if date_part.is_none() { + if let Token::StringConstant(string) = &token_info.token { + if let Some(date_part_) = + DATE_PART.get(&string.to_string().to_ascii_lowercase()) + { + date_part = (*date_part_).into(); + continue; + } + } + return Err(token_info.expected("valid date part")); + } else { + key_list = self.parse_strings_token(token_info)?; + break; + } + } + } + } + + if !mime && mime_anychild { + return Err(self.tokens.unwrap_next()?.missing_tag(":mime")); + } + self.validate_match(&match_type, &mut key_list)?; + + Ok(Test::Date(TestDate { + header_name: header_name.unwrap(), + key_list, + date_part: date_part.unwrap(), + match_type, + comparator, + index: if index_last { index.map(|i| -i) } else { index }, + zone, + mime_anychild, + is_not: false, + })) + } + + pub(crate) fn parse_test_currentdate(&mut self) -> Result { + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let mut key_list; + let mut zone = None; + let mut date_part = None; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex + | Word::List), + ) => { + self.validate_argument( + 1, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + comparator = self.parse_comparator()?; + } + Token::Tag(Word::Zone) => { + self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?; + zone = self.parse_timezone()?.into(); + } + _ => { + if date_part.is_none() { + if let Token::StringConstant(string) = &token_info.token { + if let Some(date_part_) = + DATE_PART.get(&string.to_string().to_ascii_lowercase()) + { + date_part = (*date_part_).into(); + continue; + } + } + return Err(token_info.expected("valid date part")); + } else { + key_list = self.parse_strings_token(token_info)?; + break; + } + } + } + } + self.validate_match(&match_type, &mut key_list)?; + + Ok(Test::CurrentDate(TestCurrentDate { + key_list, + date_part: date_part.unwrap(), + match_type, + comparator, + zone, + is_not: false, + })) + } + + pub(crate) fn parse_timezone(&mut self) -> Result { + let token_info = self.tokens.unwrap_next()?; + if let Token::StringConstant(value) = &token_info.token { + let timezone = match value { + StringConstant::String(value) => value.parse::().unwrap_or(i64::MAX), + StringConstant::Number(Number::Integer(n)) => *n, + StringConstant::Number(Number::Float(n)) => *n as i64, + }; + + return match timezone { + 0..=1400 => Ok((timezone / 100 * 3600) + (timezone % 100 * 60)), + -1200..=-1 => Ok((timezone / 100 * 3600) - (-timezone % 100 * 60)), + _ => Err(token_info.expected("invalid timezone")), + }; + } + Err(token_info.expected("string containing time zone")) + } +} + +/* + "year" => the year, "0000" .. "9999". + "month" => the month, "01" .. "12". + "day" => the day, "01" .. "31". + "date" => the date in "yyyy-mm-dd" format. + "julian" => the Modified Julian Day, that is, the date + expressed as an integer number of days since + 00:00 UTC on November 17, 1858 (using the Gregorian + calendar). This corresponds to the regular + Julian Day minus 2400000.5. Sample routines to + convert to and from modified Julian dates are + given in Appendix A. + "hour" => the hour, "00" .. "23". + "minute" => the minute, "00" .. "59". + "second" => the second, "00" .. "60". + "time" => the time in "hh:mm:ss" format. + "iso8601" => the date and time in restricted ISO 8601 format. + "std11" => the date and time in a format appropriate + for use in a Date: header field [RFC2822]. + "zone" => the time zone in use. If the user specified a + time zone with ":zone", "zone" will + contain that value. If :originalzone is specified + this value will be the original zone specified + in the date-time value. If neither argument is + specified the value will be the server's default + time zone in offset format "+hhmm" or "-hhmm". An + offset of 0 (Zulu) always has a positive sign. + "weekday" => the day of the week expressed as an integer between + "0" and "6". "0" is Sunday, "1" is Monday, etc. +*/ + +static DATE_PART: phf::Map<&'static str, DatePart> = phf_map! { + "year" => DatePart::Year, + "month" => DatePart::Month, + "day" => DatePart::Day, + "date" => DatePart::Date, + "julian" => DatePart::Julian, + "hour" => DatePart::Hour, + "minute" => DatePart::Minute, + "second" => DatePart::Second, + "time" => DatePart::Time, + "iso8601" => DatePart::Iso8601, + "std11" => DatePart::Std11, + "zone" => DatePart::Zone, + "weekday" => DatePart::Weekday, +}; diff --git a/melib/src/sieve/compiler/grammar/tests/test_duplicate.rs b/melib/src/sieve/compiler/grammar/tests/test_duplicate.rs new file mode 100644 index 00000000..25657bbc --- /dev/null +++ b/melib/src/sieve/compiler/grammar/tests/test_duplicate.rs @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use mail_parser::HeaderName; +use serde::{Deserialize, Serialize}; + +use crate::sieve::compiler::{ + grammar::instruction::{CompilerState, MapLocalVars}, + lexer::{word::Word, Token}, + CompileError, ErrorType, Value, +}; + +use crate::sieve::compiler::grammar::test::Test; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct TestDuplicate { + pub handle: Option, + pub dup_match: DupMatch, + pub seconds: Option, + pub last: bool, + pub is_not: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) enum DupMatch { + Header(Value), + UniqueId(Value), + Default, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_test_duplicate(&mut self) -> Result { + let mut handle = None; + let mut dup_match = DupMatch::Default; + let mut seconds = None; + let mut last = false; + + while let Some(token_info) = self.tokens.peek() { + let token_info = token_info?; + let line_num = token_info.line_num; + let line_pos = token_info.line_pos; + + match token_info.token { + Token::Tag(Word::Handle) => { + self.validate_argument(1, None, line_num, line_pos)?; + self.tokens.next(); + handle = self.parse_string()?.into(); + } + Token::Tag(Word::Header) => { + self.validate_argument(2, None, line_num, line_pos)?; + self.tokens.next(); + let header = self.parse_string()?; + if let Value::Text(header_name) = &header { + if HeaderName::parse(header_name).is_none() { + return Err(self + .tokens + .unwrap_next()? + .custom(ErrorType::InvalidHeaderName)); + } + } + dup_match = DupMatch::Header(header); + } + Token::Tag(Word::UniqueId) => { + self.validate_argument(2, None, line_num, line_pos)?; + self.tokens.next(); + dup_match = DupMatch::UniqueId(self.parse_string()?); + } + Token::Tag(Word::Seconds) => { + self.validate_argument(3, None, line_num, line_pos)?; + self.tokens.next(); + seconds = (self.tokens.expect_number(u64::MAX as usize)? as u64).into(); + } + Token::Tag(Word::Last) => { + self.validate_argument(4, None, line_num, line_pos)?; + self.tokens.next(); + last = true; + } + _ => break, + } + } + + Ok(Test::Duplicate(TestDuplicate { + handle, + dup_match, + seconds, + last, + is_not: false, + })) + } +} + +impl MapLocalVars for DupMatch { + fn map_local_vars(&mut self, last_id: usize) { + match self { + DupMatch::Header(header) => header.map_local_vars(last_id), + DupMatch::UniqueId(unique_id) => unique_id.map_local_vars(last_id), + DupMatch::Default => {} + } + } +} diff --git a/melib/src/sieve/compiler/grammar/tests/test_envelope.rs b/melib/src/sieve/compiler/grammar/tests/test_envelope.rs new file mode 100644 index 00000000..0af973d3 --- /dev/null +++ b/melib/src/sieve/compiler/grammar/tests/test_envelope.rs @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use phf::phf_map; +use serde::{Deserialize, Serialize}; + +use crate::sieve::{ + compiler::{ + grammar::{instruction::CompilerState, Capability, Comparator}, + lexer::{word::Word, Token}, + CompileError, ErrorType, Value, + }, + Envelope, +}; + +use crate::sieve::compiler::grammar::{test::Test, AddressPart, MatchType}; + +use std::convert::TryFrom; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct TestEnvelope { + pub envelope_list: Vec, + pub key_list: Vec, + pub address_part: AddressPart, + pub match_type: MatchType, + pub comparator: Comparator, + pub zone: Option, + pub is_not: bool, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_test_envelope(&mut self) -> Result { + let mut address_part = AddressPart::All; + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let mut envelope_list = None; + let mut key_list; + let mut zone = None; + + loop { + let mut token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::LocalPart | Word::Domain | Word::All | Word::User | Word::Detail), + ) => { + self.validate_argument( + 1, + if matches!(word, Word::User | Word::Detail) { + Capability::SubAddress.into() + } else { + None + }, + token_info.line_num, + token_info.line_pos, + )?; + address_part = word.into(); + } + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex + | Word::List), + ) => { + self.validate_argument( + 2, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?; + comparator = self.parse_comparator()?; + } + Token::Tag(Word::Zone) => { + self.validate_argument( + 4, + Capability::EnvelopeDeliverBy.into(), + token_info.line_num, + token_info.line_pos, + )?; + zone = self.parse_timezone()?.into(); + } + _ => { + if envelope_list.is_none() { + let mut envelopes = Vec::new(); + let line_num = token_info.line_num; + let line_pos = token_info.line_pos; + + match token_info.token { + Token::StringConstant(s) => match Envelope::try_from(s.into_string()) { + Ok(envelope) => { + envelopes.push(envelope); + } + Err(invalid) => { + token_info.token = Token::Comma; + return Err( + token_info.custom(ErrorType::InvalidEnvelope(invalid)) + ); + } + }, + Token::BracketOpen => loop { + let mut token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::StringConstant(s) => { + match Envelope::try_from(s.into_string()) { + Ok(envelope) => { + if !envelopes.contains(&envelope) { + envelopes.push(envelope); + } + } + Err(invalid) => { + token_info.token = Token::Comma; + return Err(token_info + .custom(ErrorType::InvalidEnvelope(invalid))); + } + } + } + Token::Comma => (), + Token::BracketClose if !envelopes.is_empty() => break, + _ => return Err(token_info.expected("constant string")), + } + }, + _ => return Err(token_info.expected("constant string")), + } + + for envelope in &envelopes { + match envelope { + Envelope::ByTimeAbsolute + | Envelope::ByTimeRelative + | Envelope::ByMode + | Envelope::ByTrace => { + self.validate_argument( + 0, + Capability::EnvelopeDeliverBy.into(), + line_num, + line_pos, + )?; + } + + Envelope::Notify + | Envelope::Orcpt + | Envelope::Ret + | Envelope::Envid => { + self.validate_argument( + 0, + Capability::EnvelopeDsn.into(), + line_num, + line_pos, + )?; + } + _ => (), + } + } + + envelope_list = envelopes.into(); + } else { + key_list = self.parse_strings_token(token_info)?; + break; + } + } + } + } + self.validate_match(&match_type, &mut key_list)?; + + Ok(Test::Envelope(TestEnvelope { + envelope_list: envelope_list.unwrap(), + key_list, + address_part, + match_type, + comparator, + zone, + is_not: false, + })) + } +} + +impl TryFrom for Envelope { + type Error = String; + + fn try_from(value: String) -> Result { + if let Some(envelope) = ENVELOPE.get(&value) { + Ok(*envelope) + } else { + Err(value) + } + } +} + +impl<'x> TryFrom<&'x str> for Envelope { + type Error = &'x str; + + fn try_from(value: &'x str) -> Result { + if let Some(envelope) = ENVELOPE.get(value) { + Ok(*envelope) + } else { + Err(value) + } + } +} + +pub(crate) static ENVELOPE: phf::Map<&'static str, Envelope> = phf_map! { + "from" => Envelope::From, + "to" => Envelope::To, + "bytimeabsolute" => Envelope::ByTimeAbsolute, + "bytimerelative" => Envelope::ByTimeRelative, + "bymode" => Envelope::ByMode, + "bytrace" => Envelope::ByTrace, + "notify" => Envelope::Notify, + "orcpt" => Envelope::Orcpt, + "ret" => Envelope::Ret, + "envid" => Envelope::Envid, +}; diff --git a/melib/src/sieve/compiler/grammar/tests/test_environment.rs b/melib/src/sieve/compiler/grammar/tests/test_environment.rs new file mode 100644 index 00000000..0fd95824 --- /dev/null +++ b/melib/src/sieve/compiler/grammar/tests/test_environment.rs @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use crate::sieve::compiler::{ + grammar::{instruction::CompilerState, Capability, Comparator}, + lexer::{word::Word, Token}, + CompileError, Value, VariableType, +}; + +use crate::sieve::compiler::grammar::{test::Test, MatchType}; + +use super::test_string::TestString; + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_test_environment(&mut self) -> Result { + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let mut name = None; + let mut key_list; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex), + ) => { + self.validate_argument( + 1, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + comparator = self.parse_comparator()?; + } + _ => { + if name.is_none() { + if let Token::StringConstant(s) = token_info.token { + name = Value::Variable(VariableType::Environment( + s.into_string().to_lowercase(), + )) + .into(); + } else { + return Err(token_info.expected("environment variable")); + } + } else { + key_list = self.parse_strings_token(token_info)?; + break; + } + } + } + } + self.validate_match(&match_type, &mut key_list)?; + + Ok(Test::Environment(TestString { + source: vec![name.unwrap()], + key_list, + match_type, + comparator, + is_not: false, + })) + } +} diff --git a/melib/src/sieve/compiler/grammar/tests/test_exists.rs b/melib/src/sieve/compiler/grammar/tests/test_exists.rs new file mode 100644 index 00000000..6d40a4a3 --- /dev/null +++ b/melib/src/sieve/compiler/grammar/tests/test_exists.rs @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use mail_parser::HeaderName; +use serde::{Deserialize, Serialize}; + +use crate::sieve::compiler::{ + grammar::{instruction::CompilerState, Capability}, + lexer::{word::Word, Token}, + CompileError, ErrorType, Value, +}; + +use crate::sieve::compiler::grammar::test::Test; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct TestExists { + pub header_names: Vec, + pub mime_anychild: bool, + pub is_not: bool, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_test_exists(&mut self) -> Result { + let mut header_names = None; + + let mut mime = false; + let mut mime_anychild = false; + + while header_names.is_none() { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag(Word::Mime) => { + self.validate_argument( + 1, + Capability::Mime.into(), + token_info.line_num, + token_info.line_pos, + )?; + mime = true; + } + Token::Tag(Word::AnyChild) => { + self.validate_argument( + 2, + Capability::Mime.into(), + token_info.line_num, + token_info.line_pos, + )?; + mime_anychild = true; + } + _ => { + let headers = self.parse_strings_token(token_info)?; + for header in &headers { + if let Value::Text(header_name) = &header { + if HeaderName::parse(header_name).is_none() { + return Err(self + .tokens + .unwrap_next()? + .custom(ErrorType::InvalidHeaderName)); + } + } + } + header_names = headers.into(); + } + } + } + + if !mime && mime_anychild { + return Err(self.tokens.unwrap_next()?.missing_tag(":mime")); + } + + Ok(Test::Exists(TestExists { + header_names: header_names.unwrap(), + mime_anychild, + is_not: false, + })) + } +} diff --git a/melib/src/sieve/compiler/grammar/tests/test_extlists.rs b/melib/src/sieve/compiler/grammar/tests/test_extlists.rs new file mode 100644 index 00000000..45e2f831 --- /dev/null +++ b/melib/src/sieve/compiler/grammar/tests/test_extlists.rs @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use serde::{Deserialize, Serialize}; + +use crate::sieve::compiler::grammar::instruction::CompilerState; +use crate::sieve::compiler::CompileError; +use crate::sieve::compiler::Value; + +use crate::sieve::compiler::grammar::test::Test; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct TestValidExtList { + pub list_names: Vec, + pub is_not: bool, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_test_valid_ext_list(&mut self) -> Result { + Ok(Test::ValidExtList(TestValidExtList { + list_names: self.parse_strings(false)?, + is_not: false, + })) + } +} diff --git a/melib/src/sieve/compiler/grammar/tests/test_hasflag.rs b/melib/src/sieve/compiler/grammar/tests/test_hasflag.rs new file mode 100644 index 00000000..a9282f8f --- /dev/null +++ b/melib/src/sieve/compiler/grammar/tests/test_hasflag.rs @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use serde::{Deserialize, Serialize}; + +use crate::sieve::compiler::{ + grammar::{instruction::CompilerState, Capability, Comparator}, + lexer::{tokenizer::TokenInfo, word::Word, Token}, + CompileError, ErrorType, Value, VariableType, +}; + +use crate::sieve::compiler::grammar::{test::Test, MatchType}; + +/* + Usage: hasflag [MATCH-TYPE] [COMPARATOR] + [] + +*/ + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct TestHasFlag { + pub comparator: Comparator, + pub match_type: MatchType, + pub variable_list: Vec, + pub flags: Vec, + pub is_not: bool, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_test_hasflag(&mut self) -> Result { + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let mut is_local = false; + + let mut maybe_variables; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex), + ) => { + self.validate_argument( + 1, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + comparator = self.parse_comparator()?; + } + Token::Tag(Word::Local) => { + is_local = true; + } + _ => { + maybe_variables = self.parse_strings_token(token_info)?; + break; + } + } + } + + match self.tokens.peek() { + Some(Ok(TokenInfo { + token: Token::StringConstant(_) | Token::StringVariable(_) | Token::BracketOpen, + line_num, + line_pos, + })) => { + if !maybe_variables.is_empty() { + let line_num = *line_num; + let line_pos = *line_pos; + + let mut variable_list = Vec::with_capacity(maybe_variables.len()); + for variable in maybe_variables { + match variable { + Value::Text(var_name) => { + variable_list.push( + self.register_variable(var_name, is_local).map_err( + |error_type| CompileError { + line_num, + line_pos, + error_type, + }, + )?, + ); + } + _ => { + return Err(self + .tokens + .unwrap_next()? + .custom(ErrorType::ExpectedConstantString)) + } + } + } + let mut flags = self.parse_strings(false)?; + self.validate_match(&match_type, &mut flags)?; + + Ok(Test::HasFlag(TestHasFlag { + comparator, + match_type, + variable_list, + flags, + is_not: false, + })) + } else { + Err(self + .tokens + .unwrap_next()? + .custom(ErrorType::ExpectedConstantString)) + } + } + _ => { + self.validate_match(&match_type, &mut maybe_variables)?; + + Ok(Test::HasFlag(TestHasFlag { + comparator, + match_type, + variable_list: Vec::new(), + flags: maybe_variables, + is_not: false, + })) + } + } + } +} diff --git a/melib/src/sieve/compiler/grammar/tests/test_header.rs b/melib/src/sieve/compiler/grammar/tests/test_header.rs new file mode 100644 index 00000000..bd575ea8 --- /dev/null +++ b/melib/src/sieve/compiler/grammar/tests/test_header.rs @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use mail_parser::HeaderName; +use serde::{Deserialize, Serialize}; + +use crate::sieve::compiler::{ + grammar::{ + actions::action_mime::MimeOpts, + instruction::{CompilerState, MapLocalVars}, + Capability, Comparator, + }, + lexer::{word::Word, Token}, + CompileError, ErrorType, Value, +}; + +use crate::sieve::compiler::grammar::{test::Test, MatchType}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct TestHeader { + pub header_list: Vec, + pub key_list: Vec, + pub match_type: MatchType, + pub comparator: Comparator, + pub index: Option, + + pub mime_opts: MimeOpts, + pub mime_anychild: bool, + pub is_not: bool, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_test_header(&mut self) -> Result { + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let mut header_list = None; + let mut key_list; + let mut index = None; + let mut index_last = false; + + let mut mime = false; + let mut mime_opts = MimeOpts::None; + let mut mime_anychild = false; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex + | Word::List), + ) => { + self.validate_argument( + 1, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + + comparator = self.parse_comparator()?; + } + Token::Tag(Word::Index) => { + self.validate_argument( + 3, + Capability::Index.into(), + token_info.line_num, + token_info.line_pos, + )?; + + index = (self.tokens.expect_number(u16::MAX as usize)? as i32).into(); + } + Token::Tag(Word::Last) => { + self.validate_argument( + 4, + Capability::Index.into(), + token_info.line_num, + token_info.line_pos, + )?; + + index_last = true; + } + Token::Tag(Word::Mime) => { + self.validate_argument( + 5, + Capability::Mime.into(), + token_info.line_num, + token_info.line_pos, + )?; + mime = true; + } + Token::Tag(Word::AnyChild) => { + self.validate_argument( + 6, + Capability::Mime.into(), + token_info.line_num, + token_info.line_pos, + )?; + mime_anychild = true; + } + Token::Tag( + word @ (Word::Type | Word::Subtype | Word::ContentType | Word::Param), + ) => { + self.validate_argument( + 7, + Capability::Mime.into(), + token_info.line_num, + token_info.line_pos, + )?; + mime_opts = self.parse_mimeopts(word)?; + } + _ => { + if header_list.is_none() { + let headers = self.parse_strings_token(token_info)?; + for header in &headers { + if let Value::Text(header_name) = &header { + if HeaderName::parse(header_name).is_none() { + return Err(self + .tokens + .unwrap_next()? + .custom(ErrorType::InvalidHeaderName)); + } + } + } + header_list = headers.into(); + } else { + key_list = self.parse_strings_token(token_info)?; + break; + } + } + } + } + + if !mime && (mime_anychild || mime_opts != MimeOpts::None) { + return Err(self.tokens.unwrap_next()?.missing_tag(":mime")); + } + self.validate_match(&match_type, &mut key_list)?; + + Ok(Test::Header(TestHeader { + header_list: header_list.unwrap(), + key_list, + match_type, + comparator, + index: if index_last { index.map(|i| -i) } else { index }, + mime_opts, + mime_anychild, + is_not: false, + })) + } +} + +impl MapLocalVars for MimeOpts { + fn map_local_vars(&mut self, last_id: usize) { + if let MimeOpts::Param(value) = self { + value.map_local_vars(last_id) + } + } +} diff --git a/melib/src/sieve/compiler/grammar/tests/test_ihave.rs b/melib/src/sieve/compiler/grammar/tests/test_ihave.rs new file mode 100644 index 00000000..2508eca4 --- /dev/null +++ b/melib/src/sieve/compiler/grammar/tests/test_ihave.rs @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use serde::{Deserialize, Serialize}; + +use crate::sieve::compiler::grammar::instruction::{CompilerState, Instruction}; +use crate::sieve::compiler::grammar::Capability; +use crate::sieve::compiler::CompileError; +use crate::sieve::compiler::Value; + +use crate::sieve::compiler::grammar::test::Test; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct TestIhave { + pub capabilities: Vec, + pub is_not: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct Error { + pub message: Value, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_test_ihave(&mut self) -> Result { + Ok(Test::Ihave(TestIhave { + capabilities: self + .parse_static_strings()? + .into_iter() + .map(|n| n.into()) + .collect(), + is_not: false, + })) + } + + pub(crate) fn parse_error(&mut self) -> Result<(), CompileError> { + let cmd = Instruction::Error(Error { + message: self.parse_string()?, + }); + self.instructions.push(cmd); + Ok(()) + } +} diff --git a/melib/src/sieve/compiler/grammar/tests/test_mailbox.rs b/melib/src/sieve/compiler/grammar/tests/test_mailbox.rs new file mode 100644 index 00000000..4eee1e29 --- /dev/null +++ b/melib/src/sieve/compiler/grammar/tests/test_mailbox.rs @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use serde::{Deserialize, Serialize}; + +use crate::sieve::{ + compiler::{ + grammar::{ + instruction::{CompilerState, MapLocalVars}, + Capability, Comparator, + }, + lexer::{word::Word, Token}, + CompileError, Value, + }, + Metadata, +}; + +use crate::sieve::compiler::grammar::{test::Test, MatchType}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct TestMailboxExists { + pub mailbox_names: Vec, + pub is_not: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct TestMetadataExists { + pub mailbox: Option, + pub annotation_names: Vec, + pub is_not: bool, +} + +/* + +metadata [MATCH-TYPE] [COMPARATOR] + + + +*/ + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct TestMetadata { + pub match_type: MatchType, + pub comparator: Comparator, + pub medatata: Metadata, + pub key_list: Vec, + pub is_not: bool, +} + +/* + +servermetadata [MATCH-TYPE] [COMPARATOR] + + +*/ + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_test_mailboxexists(&mut self) -> Result { + Ok(Test::MailboxExists(TestMailboxExists { + mailbox_names: self.parse_strings(false)?, + is_not: false, + })) + } + + pub(crate) fn parse_test_metadataexists(&mut self) -> Result { + Ok(Test::MetadataExists(TestMetadataExists { + mailbox: self.parse_string()?.into(), + annotation_names: self.parse_strings(false)?, + is_not: false, + })) + } + + pub(crate) fn parse_test_servermetadataexists(&mut self) -> Result { + Ok(Test::MetadataExists(TestMetadataExists { + mailbox: None, + annotation_names: self.parse_strings(false)?, + is_not: false, + })) + } + + pub(crate) fn parse_test_metadata(&mut self) -> Result { + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let mut mailbox = None; + let mut annotation_name = None; + let mut key_list: Vec; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex), + ) => { + self.validate_argument( + 1, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + comparator = self.parse_comparator()?; + } + _ => { + if mailbox.is_none() { + mailbox = self.parse_string_token(token_info)?.into(); + } else if annotation_name.is_none() { + annotation_name = self.parse_string_token(token_info)?.into(); + } else { + key_list = self.parse_strings_token(token_info)?; + break; + } + } + } + } + self.validate_match(&match_type, &mut key_list)?; + + Ok(Test::Metadata(TestMetadata { + match_type, + comparator, + medatata: Metadata::Mailbox { + name: mailbox.unwrap(), + annotation: annotation_name.unwrap(), + }, + key_list, + is_not: false, + })) + } + + pub(crate) fn parse_test_servermetadata(&mut self) -> Result { + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let mut annotation_name = None; + let mut key_list: Vec; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex), + ) => { + self.validate_argument( + 1, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + comparator = self.parse_comparator()?; + } + _ => { + if annotation_name.is_none() { + annotation_name = self.parse_string_token(token_info)?.into(); + } else { + key_list = self.parse_strings_token(token_info)?; + break; + } + } + } + } + self.validate_match(&match_type, &mut key_list)?; + + Ok(Test::Metadata(TestMetadata { + match_type, + comparator, + medatata: Metadata::Server { + annotation: annotation_name.unwrap(), + }, + key_list, + is_not: false, + })) + } +} + +impl MapLocalVars for Metadata { + fn map_local_vars(&mut self, last_id: usize) { + match self { + Metadata::Mailbox { name, annotation } => { + name.map_local_vars(last_id); + annotation.map_local_vars(last_id); + } + Metadata::Server { annotation } => { + annotation.map_local_vars(last_id); + } + } + } +} diff --git a/melib/src/sieve/compiler/grammar/tests/test_mailboxid.rs b/melib/src/sieve/compiler/grammar/tests/test_mailboxid.rs new file mode 100644 index 00000000..66d292e7 --- /dev/null +++ b/melib/src/sieve/compiler/grammar/tests/test_mailboxid.rs @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use serde::{Deserialize, Serialize}; + +use crate::sieve::compiler::grammar::instruction::CompilerState; +use crate::sieve::compiler::CompileError; +use crate::sieve::compiler::Value; + +use crate::sieve::compiler::grammar::test::Test; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct TestMailboxIdExists { + pub mailbox_ids: Vec, + pub is_not: bool, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_test_mailboxidexists(&mut self) -> Result { + Ok(Test::MailboxIdExists(TestMailboxIdExists { + mailbox_ids: self.parse_strings(false)?, + is_not: false, + })) + } +} diff --git a/melib/src/sieve/compiler/grammar/tests/test_notify.rs b/melib/src/sieve/compiler/grammar/tests/test_notify.rs new file mode 100644 index 00000000..aa1c1ab7 --- /dev/null +++ b/melib/src/sieve/compiler/grammar/tests/test_notify.rs @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use serde::{Deserialize, Serialize}; + +use crate::sieve::compiler::{ + grammar::{instruction::CompilerState, Capability, Comparator}, + lexer::{word::Word, Token}, + CompileError, Value, +}; + +use crate::sieve::compiler::grammar::{test::Test, MatchType}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct TestNotifyMethodCapability { + pub comparator: Comparator, + pub match_type: MatchType, + pub notification_uri: Value, + pub notification_capability: Value, + pub key_list: Vec, + pub is_not: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct TestValidNotifyMethod { + pub notification_uris: Vec, + pub is_not: bool, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_test_valid_notify_method(&mut self) -> Result { + Ok(Test::ValidNotifyMethod(TestValidNotifyMethod { + notification_uris: self.parse_strings(false)?, + is_not: false, + })) + } + + pub(crate) fn parse_test_notify_method_capability(&mut self) -> Result { + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let mut notification_uri = None; + let mut notification_capability = None; + let mut key_list; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex), + ) => { + self.validate_argument( + 1, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + comparator = self.parse_comparator()?; + } + _ => { + if notification_uri.is_none() { + notification_uri = self.parse_string_token(token_info)?.into(); + } else if notification_capability.is_none() { + notification_capability = self.parse_string_token(token_info)?.into(); + } else { + key_list = self.parse_strings_token(token_info)?; + break; + } + } + } + } + self.validate_match(&match_type, &mut key_list)?; + + Ok(Test::NotifyMethodCapability(TestNotifyMethodCapability { + key_list, + match_type, + comparator, + notification_uri: notification_uri.unwrap(), + notification_capability: notification_capability.unwrap(), + is_not: false, + })) + } +} diff --git a/melib/src/sieve/compiler/grammar/tests/test_plugin.rs b/melib/src/sieve/compiler/grammar/tests/test_plugin.rs new file mode 100644 index 00000000..891b11b9 --- /dev/null +++ b/melib/src/sieve/compiler/grammar/tests/test_plugin.rs @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::fmt::Display; + +use serde::{Deserialize, Serialize}; + +use crate::sieve::compiler::grammar::instruction::{CompilerState, Instruction, MapLocalVars}; +use crate::sieve::compiler::lexer::Token; +use crate::sieve::compiler::{CompileError, Regex}; +use crate::sieve::compiler::{ErrorType, Value}; +use crate::sieve::{ExternalId, PluginArgument, PluginSchema, PluginSchemaArgument}; + +use crate::sieve::compiler::grammar::test::Test; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct Plugin { + pub id: ExternalId, + pub arguments: Vec>, + pub is_not: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct Error { + pub message: Value, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_plugin(&mut self, schema: &PluginSchema) -> Result<(), CompileError> { + let instruction = Instruction::Plugin(self.parse_plugin_(schema)?); + self.tokens.expect_token(Token::Semicolon)?; + self.instructions.push(instruction); + Ok(()) + } + + pub(crate) fn parse_test_plugin( + &mut self, + schema: &PluginSchema, + ) -> Result { + Ok(Test::Plugin(self.parse_plugin_(schema)?)) + } + + fn parse_plugin_(&mut self, schema: &PluginSchema) -> Result { + let mut plugin = Plugin { + id: schema.id, + arguments: vec![], + is_not: false, + }; + let mut tags = vec![]; + let mut schema_args = schema.arguments.iter(); + + while let Some(token_info) = self.tokens.peek() { + let token_info = token_info?; + let (target, schema_arg) = match &token_info.token { + Token::Tag(tag) => { + let tag = tag.to_string(); + let token_info = self.tokens.unwrap_next()?; + if let Some(tagged_arg) = schema.tags.get(&tag) { + tags.push(PluginArgument::Tag(tagged_arg.id)); + if let Some(schema_arg) = &tagged_arg.argument { + (&mut tags, schema_arg) + } else { + continue; + } + } else { + return Err(token_info.expected("a valid argument")); + } + } + Token::Unknown(tag) => { + if let Some(tagged_arg) = + tag.strip_prefix(':').and_then(|tag| schema.tags.get(tag)) + { + self.tokens.unwrap_next()?; + tags.push(PluginArgument::Tag(tagged_arg.id)); + if let Some(schema_arg) = &tagged_arg.argument { + (&mut tags, schema_arg) + } else { + continue; + } + } else { + return Err(self.tokens.unwrap_next()?.expected("a valid argument")); + } + } + _ => { + if let Some(schema_arg) = schema_args.next() { + (&mut plugin.arguments, schema_arg) + } else { + break; + } + } + }; + + match schema_arg { + PluginSchemaArgument::Array(item_schema) => { + let mut items = vec![]; + if matches!(item_schema.as_ref(), PluginSchemaArgument::Variable) { + for item in self.parse_static_strings()? { + match self.register_variable(item, false) { + Ok(var) => { + items.push(PluginArgument::Variable(var)); + } + Err(err) => { + return Err(self.tokens.unwrap_next()?.custom(err)); + } + } + } + } else { + for item in self.parse_strings(true)? { + match item_schema.convert_argument(item) { + Ok(arg) => { + items.push(arg); + } + Err(err) => { + return Err(self.tokens.unwrap_next()?.custom(err)); + } + } + } + } + target.push(PluginArgument::Array(items)); + } + PluginSchemaArgument::Variable => { + let token = self.tokens.unwrap_next()?; + target.push(PluginArgument::Variable( + self.parse_variable_name(token, false)?, + )); + } + _ => match schema_arg.convert_argument(self.parse_string()?) { + Ok(arg) => { + target.push(arg); + } + Err(err) => { + return Err(self.tokens.unwrap_next()?.custom(err)); + } + }, + } + } + + if let Some(schema_arg) = schema_args.next() { + self.tokens + .unwrap_next()? + .expected(format!("expected a {schema_arg}")); + } + + plugin.arguments.extend(tags); + + Ok(plugin) + } +} + +impl PluginSchemaArgument { + fn convert_argument(&self, value: Value) -> Result, ErrorType> { + match self { + PluginSchemaArgument::Text => Ok(PluginArgument::Text(value)), + PluginSchemaArgument::Number => Ok(PluginArgument::Number(value)), + PluginSchemaArgument::Regex => { + if let Value::Text(expr) = value { + fancy_regex::Regex::new(&expr) + .map(|regex| PluginArgument::Regex(Regex { regex, expr })) + .map_err(|err| ErrorType::InvalidRegex(err.to_string())) + } else { + Err(ErrorType::InvalidRegex( + "Expected a regular expression".to_string(), + )) + } + } + _ => Err(ErrorType::InvalidArguments), + } + } +} + +impl MapLocalVars for PluginArgument { + fn map_local_vars(&mut self, last_id: usize) { + match self { + PluginArgument::Text(v) => v.map_local_vars(last_id), + PluginArgument::Number(v) => v.map_local_vars(last_id), + PluginArgument::Array(v) => v.map_local_vars(last_id), + PluginArgument::Variable(v) => v.map_local_vars(last_id), + _ => (), + } + } +} + +impl Display for PluginSchemaArgument { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PluginSchemaArgument::Text => write!(f, "string"), + PluginSchemaArgument::Number => write!(f, "number"), + PluginSchemaArgument::Regex => write!(f, "regular expression"), + PluginSchemaArgument::Variable => write!(f, "variable"), + PluginSchemaArgument::Array(item) => write!(f, "array of {}s", item), + } + } +} diff --git a/melib/src/sieve/compiler/grammar/tests/test_size.rs b/melib/src/sieve/compiler/grammar/tests/test_size.rs new file mode 100644 index 00000000..84c587f1 --- /dev/null +++ b/melib/src/sieve/compiler/grammar/tests/test_size.rs @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use serde::{Deserialize, Serialize}; + +use crate::sieve::compiler::{ + grammar::instruction::CompilerState, + lexer::{word::Word, Token}, + CompileError, +}; + +use crate::sieve::compiler::grammar::test::Test; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct TestSize { + pub over: bool, + pub limit: usize, + pub is_not: bool, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_test_size(&mut self) -> Result { + let token_info = self.tokens.unwrap_next()?; + let over = match token_info.token { + Token::Tag(Word::Over) => true, + Token::Tag(Word::Under) => false, + _ => { + return Err(token_info.expected("':over' or ':under'")); + } + }; + let token_info = self.tokens.unwrap_next()?; + if let Token::Number(limit) = token_info.token { + Ok(Test::Size(TestSize { + over, + limit, + is_not: false, + })) + } else { + Err(token_info.expected("number")) + } + } +} diff --git a/melib/src/sieve/compiler/grammar/tests/test_spamtest.rs b/melib/src/sieve/compiler/grammar/tests/test_spamtest.rs new file mode 100644 index 00000000..9eddb949 --- /dev/null +++ b/melib/src/sieve/compiler/grammar/tests/test_spamtest.rs @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use serde::{Deserialize, Serialize}; + +use crate::sieve::compiler::{ + grammar::{instruction::CompilerState, Capability, Comparator}, + lexer::{word::Word, Token}, + CompileError, Value, +}; + +use crate::sieve::compiler::grammar::{test::Test, MatchType}; + +/* + Usage: spamtest [":percent"] [COMPARATOR] [MATCH-TYPE] + +*/ + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct TestSpamTest { + pub value: Value, + pub match_type: MatchType, + pub comparator: Comparator, + pub percent: bool, + pub is_not: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct TestVirusTest { + pub value: Value, + pub match_type: MatchType, + pub comparator: Comparator, + pub is_not: bool, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_test_spamtest(&mut self) -> Result { + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let mut percent = false; + let value; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex), + ) => { + self.validate_argument( + 1, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + comparator = self.parse_comparator()?; + } + Token::Tag(Word::Percent) => { + self.validate_argument( + 3, + Capability::SpamTestPlus.into(), + token_info.line_num, + token_info.line_pos, + )?; + percent = true; + } + _ => { + value = self.parse_string_token(token_info)?; + break; + } + } + } + + Ok(Test::SpamTest(TestSpamTest { + value, + percent, + match_type, + comparator, + is_not: false, + })) + } + + pub(crate) fn parse_test_virustest(&mut self) -> Result { + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let value; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex), + ) => { + self.validate_argument( + 1, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + comparator = self.parse_comparator()?; + } + _ => { + value = self.parse_string_token(token_info)?; + break; + } + } + } + + Ok(Test::VirusTest(TestVirusTest { + value, + match_type, + comparator, + is_not: false, + })) + } +} diff --git a/melib/src/sieve/compiler/grammar/tests/test_specialuse.rs b/melib/src/sieve/compiler/grammar/tests/test_specialuse.rs new file mode 100644 index 00000000..10a23729 --- /dev/null +++ b/melib/src/sieve/compiler/grammar/tests/test_specialuse.rs @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use serde::{Deserialize, Serialize}; + +use crate::sieve::compiler::{ + grammar::instruction::CompilerState, lexer::Token, CompileError, Value, +}; + +use crate::sieve::compiler::grammar::test::Test; + +/* + Usage: specialuse_exists [] + +*/ + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct TestSpecialUseExists { + pub mailbox: Option, + pub attributes: Vec, + pub is_not: bool, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_test_specialuseexists(&mut self) -> Result { + let mut maybe_attributes = self.parse_strings(false)?; + + match self.tokens.peek().map(|r| r.map(|t| &t.token)) { + Some(Ok(Token::StringConstant(_) | Token::StringVariable(_) | Token::BracketOpen)) => { + if maybe_attributes.len() == 1 { + Ok(Test::SpecialUseExists(TestSpecialUseExists { + mailbox: maybe_attributes.pop(), + attributes: self.parse_strings(false)?, + is_not: false, + })) + } else { + Err(self.tokens.unwrap_next()?.expected("string")) + } + } + _ => Ok(Test::SpecialUseExists(TestSpecialUseExists { + mailbox: None, + attributes: maybe_attributes, + is_not: false, + })), + } + } +} diff --git a/melib/src/sieve/compiler/grammar/tests/test_string.rs b/melib/src/sieve/compiler/grammar/tests/test_string.rs new file mode 100644 index 00000000..4e826fff --- /dev/null +++ b/melib/src/sieve/compiler/grammar/tests/test_string.rs @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use serde::{Deserialize, Serialize}; + +use crate::sieve::compiler::{ + grammar::{instruction::CompilerState, Capability, Comparator}, + lexer::{word::Word, Token}, + CompileError, Value, +}; + +use crate::sieve::compiler::grammar::{test::Test, MatchType}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct TestString { + pub match_type: MatchType, + pub comparator: Comparator, + pub source: Vec, + pub key_list: Vec, + pub is_not: bool, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn parse_test_string(&mut self) -> Result { + let mut match_type = MatchType::Is; + let mut comparator = Comparator::AsciiCaseMap; + let mut source = None; + let mut key_list: Vec; + + loop { + let token_info = self.tokens.unwrap_next()?; + match token_info.token { + Token::Tag( + word @ (Word::Is + | Word::Contains + | Word::Matches + | Word::Value + | Word::Count + | Word::Regex + | Word::List), + ) => { + self.validate_argument( + 1, + match word { + Word::Value | Word::Count => Capability::Relational.into(), + Word::Regex => Capability::Regex.into(), + Word::List => Capability::ExtLists.into(), + _ => None, + }, + token_info.line_num, + token_info.line_pos, + )?; + + match_type = self.parse_match_type(word)?; + } + Token::Tag(Word::Comparator) => { + self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?; + comparator = self.parse_comparator()?; + } + _ => { + if source.is_none() { + source = self.parse_strings_token(token_info)?.into(); + } else { + key_list = self.parse_strings_token(token_info)?; + break; + } + } + } + } + self.validate_match(&match_type, &mut key_list)?; + + Ok(Test::String(TestString { + source: source.unwrap(), + key_list, + match_type, + comparator, + is_not: false, + })) + } +} diff --git a/melib/src/sieve/compiler/lexer/mod.rs b/melib/src/sieve/compiler/lexer/mod.rs new file mode 100644 index 00000000..18d5e31b --- /dev/null +++ b/melib/src/sieve/compiler/lexer/mod.rs @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +pub mod string; +pub mod tokenizer; +pub mod word; + +use std::{borrow::Cow, fmt::Display}; + +use self::word::Word; + +use super::{Number, Value}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum Token { + CurlyOpen, + CurlyClose, + BracketOpen, + BracketClose, + ParenthesisOpen, + ParenthesisClose, + Comma, + Semicolon, + StringConstant(StringConstant), + StringVariable(Vec), + Number(usize), + Identifier(Word), + Tag(Word), + Unknown(String), + Colon, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum StringConstant { + String(String), + Number(Number), +} + +impl StringConstant { + pub fn to_string(&self) -> Cow { + match self { + StringConstant::String(s) => s.as_str().into(), + StringConstant::Number(n) => n.to_string().into(), + } + } + + pub fn into_string(self) -> String { + match self { + StringConstant::String(s) => s, + StringConstant::Number(n) => n.to_string(), + } + } +} + +impl From for Value { + fn from(value: StringConstant) -> Self { + match value { + StringConstant::String(s) => Value::Text(s), + StringConstant::Number(n) => Value::Number(n), + } + } +} + +impl Display for Token { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Token::CurlyOpen => f.write_str("{"), + Token::CurlyClose => f.write_str("}"), + Token::BracketOpen => f.write_str("["), + Token::BracketClose => f.write_str("]"), + Token::ParenthesisOpen => f.write_str("("), + Token::ParenthesisClose => f.write_str(")"), + Token::Comma => f.write_str(","), + Token::Semicolon => f.write_str(";"), + Token::Colon => f.write_str(":"), + Token::Number(n) => write!(f, "{n}"), + Token::Identifier(w) => w.fmt(f), + Token::Tag(t) => write!(f, ":{t}"), + Token::Unknown(s) => f.write_str(s), + Token::StringVariable(s) => f.write_str(&String::from_utf8_lossy(s)), + Token::StringConstant(c) => match c { + StringConstant::String(s) => f.write_str(s), + StringConstant::Number(n) => write!(f, "{n}"), + }, + } + } +} diff --git a/melib/src/sieve/compiler/lexer/string.rs b/melib/src/sieve/compiler/lexer/string.rs new file mode 100644 index 00000000..4a82efd6 --- /dev/null +++ b/melib/src/sieve/compiler/lexer/string.rs @@ -0,0 +1,990 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::convert::TryFrom; +use std::fmt::Display; + +use mail_parser::HeaderName; + +use crate::sieve::{ + compiler::{ + grammar::{ + expr::{self, parser::ExpressionParser, tokenizer::Tokenizer}, + instruction::CompilerState, + AddressPart, + }, + ContentTypePart, ErrorType, HeaderPart, HeaderVariable, MessagePart, Number, + ReceivedHostname, ReceivedPart, Value, VariableType, + }, + runtime::eval::IntoString, + Envelope, MAX_MATCH_VARIABLES, +}; + +enum State { + None, + Variable, + Encoded { + is_unicode: bool, + initial_buf_size: usize, + }, +} + +impl<'x> CompilerState<'x> { + pub(crate) fn tokenize_string( + &mut self, + bytes: &[u8], + parse_decoded: bool, + ) -> Result { + let mut state = State::None; + let mut items = Vec::with_capacity(3); + let mut last_ch = 0; + + let mut var_start_pos = usize::MAX; + let mut var_is_number = true; + let mut var_has_namespace = false; + + let mut text_has_digits = true; + let mut text_has_dots = false; + + let mut hex_start = usize::MAX; + let mut decode_buf = Vec::with_capacity(bytes.len()); + let mut iter = bytes.iter().enumerate().peekable(); + + while let Some((mut pos, &ch)) = iter.next() { + let mut is_var_error = false; + + match state { + State::None => match ch { + b'{' if last_ch == b'$' => { + decode_buf.pop(); + var_start_pos = pos + 1; + var_is_number = true; + var_has_namespace = false; + state = State::Variable; + } + b'{' if last_ch == b'%' => { + decode_buf.pop(); + var_start_pos = pos + 1; + + // Add any text before the variable + if !decode_buf.is_empty() { + self.add_value( + &mut items, + &decode_buf, + parse_decoded, + text_has_digits, + text_has_dots, + )?; + decode_buf.clear(); + text_has_digits = true; + text_has_dots = false; + } + + match ExpressionParser::from_tokenizer(Tokenizer::from_iter( + iter, + |var_name, maybe_namespace| { + self.parse_expr_fnc_or_var(var_name, maybe_namespace) + }, + )) + .parse() + { + Ok(parser) => { + iter = parser.tokenizer.iter; + state = State::None; + + if !parser.output.is_empty() { + items.push(Value::Expression(parser.output)); + } else { + is_var_error = true; + pos = iter.peek().map(|(p, _)| *p).unwrap_or(bytes.len()) - 1; + } + } + Err(err) => { + return Err(ErrorType::InvalidExpression(format!( + "{}: {}", + std::str::from_utf8(bytes).unwrap_or_default(), + err + ))) + } + } + } + b'.' => { + if text_has_dots { + text_has_digits = false; + } else { + text_has_dots = true; + } + decode_buf.push(ch); + } + b'0'..=b'9' => { + decode_buf.push(ch); + } + _ => { + text_has_digits = false; + decode_buf.push(ch); + } + }, + State::Variable => match ch { + b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'[' | b']' | b'*' | b'-' => { + var_is_number = false; + } + b'.' => { + var_is_number = false; + var_has_namespace = true; + } + b'0'..=b'9' => {} + b'}' => { + if pos > var_start_pos { + // Add any text before the variable + if !decode_buf.is_empty() { + self.add_value( + &mut items, + &decode_buf, + parse_decoded, + text_has_digits, + text_has_dots, + )?; + decode_buf.clear(); + text_has_digits = true; + text_has_dots = false; + } + + // Parse variable type + let var_name = std::str::from_utf8(&bytes[var_start_pos..pos]).unwrap(); + let var_type = if !var_is_number { + self.parse_variable(var_name, var_has_namespace) + } else { + self.parse_match_variable(var_name) + }; + + match var_type { + Ok(Some(var)) => items.push(Value::Variable(var)), + Ok(None) => {} + Err( + ErrorType::InvalidNamespace(_) | ErrorType::InvalidEnvelope(_), + ) => { + is_var_error = true; + } + Err(e) => return Err(e), + } + + state = State::None; + } else { + is_var_error = true; + } + } + b':' => { + if parse_decoded && !var_has_namespace { + match bytes.get(var_start_pos..pos) { + Some(enc) if enc.eq_ignore_ascii_case(b"hex") => { + state = State::Encoded { + is_unicode: false, + initial_buf_size: decode_buf.len(), + }; + } + Some(enc) if enc.eq_ignore_ascii_case(b"unicode") => { + state = State::Encoded { + is_unicode: true, + initial_buf_size: decode_buf.len(), + }; + } + _ => { + is_var_error = true; + } + } + } else if var_has_namespace { + var_is_number = false; + } else { + is_var_error = true; + } + } + _ => { + is_var_error = true; + } + }, + + State::Encoded { + is_unicode, + initial_buf_size, + } => match ch { + b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F' => { + if hex_start == usize::MAX { + hex_start = pos; + } + } + b' ' | b'\t' | b'\r' | b'\n' | b'}' => { + if hex_start != usize::MAX { + let code = std::str::from_utf8(&bytes[hex_start..pos]).unwrap(); + hex_start = usize::MAX; + + if !is_unicode { + if let Ok(ch) = u8::from_str_radix(code, 16) { + decode_buf.push(ch); + } else { + is_var_error = true; + } + } else if let Ok(ch) = u32::from_str_radix(code, 16) { + let mut buf = [0; 4]; + decode_buf.extend_from_slice( + char::from_u32(ch) + .ok_or(ErrorType::InvalidUnicodeSequence(ch))? + .encode_utf8(&mut buf) + .as_bytes(), + ); + } else { + is_var_error = true; + } + } + if ch == b'}' { + if decode_buf.len() != initial_buf_size { + state = State::None; + } else { + is_var_error = true; + } + } + } + _ => { + is_var_error = true; + } + }, + } + + if is_var_error { + if let State::Encoded { + initial_buf_size, .. + } = state + { + if initial_buf_size != decode_buf.len() { + decode_buf.truncate(initial_buf_size); + } + } + decode_buf.extend_from_slice(&bytes[var_start_pos - 2..pos + 1]); + hex_start = usize::MAX; + state = State::None; + } + + last_ch = ch; + } + + match state { + State::Variable => { + decode_buf.extend_from_slice(&bytes[var_start_pos - 2..bytes.len()]); + } + State::Encoded { + initial_buf_size, .. + } => { + if initial_buf_size != decode_buf.len() { + decode_buf.truncate(initial_buf_size); + } + decode_buf.extend_from_slice(&bytes[var_start_pos - 2..bytes.len()]); + } + State::None => (), + } + + if !decode_buf.is_empty() { + self.add_value( + &mut items, + &decode_buf, + parse_decoded, + text_has_digits, + text_has_dots, + )?; + } + + Ok(match items.len() { + 1 => items.pop().unwrap(), + 0 => Value::Text(String::new()), + _ => Value::List(items), + }) + } + + fn parse_match_variable(&mut self, var_name: &str) -> Result, ErrorType> { + let num = var_name + .parse() + .map_err(|_| ErrorType::InvalidNumber(var_name.to_string()))?; + if num < MAX_MATCH_VARIABLES { + if self.register_match_var(num) { + let total_vars = num + 1; + if total_vars > self.vars_match_max { + self.vars_match_max = total_vars; + } + Ok(Some(VariableType::Match(num))) + } else { + Ok(None) + } + } else { + Err(ErrorType::InvalidMatchVariable(num)) + } + } + + pub fn parse_variable( + &self, + var_name: &str, + maybe_namespace: bool, + ) -> Result, ErrorType> { + if !maybe_namespace { + if self.is_var_global(var_name) { + Ok(Some(VariableType::Global(var_name.to_string()))) + } else if let Some(var_id) = self.get_local_var(var_name) { + Ok(Some(VariableType::Local(var_id))) + } else { + Ok(None) + } + } else { + let var = match var_name.to_lowercase().split_once('.') { + Some(("global" | "t", var_name)) if !var_name.is_empty() => { + VariableType::Global(var_name.to_string()) + } + Some(("env", var_name)) if !var_name.is_empty() => { + VariableType::Environment(var_name.to_string()) + } + Some(("envelope", var_name)) if !var_name.is_empty() => { + let envelope = match var_name { + "from" => Envelope::From, + "to" => Envelope::To, + "by_time_absolute" => Envelope::ByTimeAbsolute, + "by_time_relative" => Envelope::ByTimeRelative, + "by_mode" => Envelope::ByMode, + "by_trace" => Envelope::ByTrace, + "notify" => Envelope::Notify, + "orcpt" => Envelope::Orcpt, + "ret" => Envelope::Ret, + "envid" => Envelope::Envid, + _ => { + return Err(ErrorType::InvalidEnvelope(var_name.to_string())); + } + }; + VariableType::Envelope(envelope) + } + Some(("header", var_name)) if !var_name.is_empty() => { + self.parse_header_variable(var_name)? + } + Some(("body", var_name)) if !var_name.is_empty() => match var_name { + "text" => VariableType::Part(MessagePart::TextBody(false)), + "html" => VariableType::Part(MessagePart::HtmlBody(false)), + "to_text" => VariableType::Part(MessagePart::TextBody(true)), + "to_html" => VariableType::Part(MessagePart::HtmlBody(true)), + _ => return Err(ErrorType::InvalidNamespace(var_name.to_string())), + }, + Some(("part", var_name)) if !var_name.is_empty() => match var_name { + "text" => VariableType::Part(MessagePart::Contents), + "raw" => VariableType::Part(MessagePart::Raw), + _ => return Err(ErrorType::InvalidNamespace(var_name.to_string())), + }, + None => { + if self.is_var_global(var_name) { + VariableType::Global(var_name.to_string()) + } else if let Some(var_id) = self.get_local_var(var_name) { + VariableType::Local(var_id) + } else { + return Ok(None); + } + } + _ => return Err(ErrorType::InvalidNamespace(var_name.to_string())), + }; + + Ok(Some(var)) + } + } + + fn parse_header_variable(&self, var_name: &str) -> Result { + #[derive(Debug)] + enum State { + Name, + Index, + Part, + PartIndex, + } + let mut name = vec![]; + let mut has_name = false; + let mut has_wildcard = false; + let mut hdr_name = String::new(); + let mut hdr_index = String::new(); + let mut part = String::new(); + let mut part_index = String::new(); + let mut state = State::Name; + + for ch in var_name.chars() { + match state { + State::Name => match ch { + '[' => { + state = if hdr_index.is_empty() { + State::Index + } else if part.is_empty() { + State::PartIndex + } else { + return Err(ErrorType::InvalidExpression(var_name.to_string())); + }; + has_name = true; + } + '.' => { + state = State::Part; + has_name = true; + } + ' ' | '\t' | '\r' | '\n' => {} + '*' if !has_wildcard && hdr_name.is_empty() && name.is_empty() => { + has_wildcard = true; + } + ':' if !hdr_name.is_empty() && !has_wildcard => { + name.push( + HeaderName::parse(std::mem::take(&mut hdr_name)).ok_or_else(|| { + ErrorType::InvalidExpression(var_name.to_string()) + })?, + ); + } + _ if !has_name && !has_wildcard => { + hdr_name.push(ch); + } + _ => { + return Err(ErrorType::InvalidExpression(var_name.to_string())); + } + }, + State::Index => match ch { + ']' => { + state = State::Name; + } + ' ' | '\t' | '\r' | '\n' => {} + _ => { + hdr_index.push(ch); + } + }, + State::Part => match ch { + '[' => { + state = State::PartIndex; + } + ' ' | '\t' | '\r' | '\n' => {} + _ => { + part.push(ch); + } + }, + State::PartIndex => match ch { + ']' => { + state = State::Name; + } + ' ' | '\t' | '\r' | '\n' => {} + _ => { + part_index.push(ch); + } + }, + } + } + + if !hdr_name.is_empty() { + name.push( + HeaderName::parse(hdr_name) + .ok_or_else(|| ErrorType::InvalidExpression(var_name.to_string()))?, + ); + } + + if !name.is_empty() || has_wildcard { + Ok(VariableType::Header(HeaderVariable { + name, + part: HeaderPart::try_from(part.as_str()) + .map_err(|_| ErrorType::InvalidExpression(var_name.to_string()))?, + index_hdr: match hdr_index.as_str() { + "" => { + if !has_wildcard { + -1 + } else { + 0 + } + } + "*" => 0, + _ => hdr_index + .parse() + .map(|v| if v == 0 { 1 } else { v }) + .map_err(|_| ErrorType::InvalidExpression(var_name.to_string()))?, + }, + index_part: match part_index.as_str() { + "" => { + if !has_wildcard { + -1 + } else { + 0 + } + } + "*" => 0, + _ => part_index + .parse() + .map(|v| if v == 0 { 1 } else { v }) + .map_err(|_| ErrorType::InvalidExpression(var_name.to_string()))?, + }, + })) + } else { + Err(ErrorType::InvalidExpression(var_name.to_string())) + } + } + + pub fn parse_expr_fnc_or_var( + &self, + var_name: &str, + maybe_namespace: bool, + ) -> Result { + match self.parse_variable(var_name, maybe_namespace) { + Ok(Some(var)) => Ok(expr::Token::Variable(var)), + _ => { + if let Some((id, num_args)) = self.compiler.functions.get(var_name) { + Ok(expr::Token::Function { + name: var_name.to_string(), + id: *id, + num_args: *num_args, + }) + } else { + Err(format!("Invalid variable or function name {var_name:?}")) + } + } + } + } + + #[inline(always)] + fn add_value( + &mut self, + items: &mut Vec, + buf: &[u8], + parse_decoded: bool, + has_digits: bool, + has_dots: bool, + ) -> Result<(), ErrorType> { + if !parse_decoded { + items.push(if has_digits { + if has_dots { + match std::str::from_utf8(buf) + .ok() + .and_then(|v| (v, v.parse::().ok()?).into()) + { + Some((v, n)) if n.to_string() == v => Value::Number(Number::Float(n)), + _ => Value::Text(buf.to_vec().into_string()), + } + } else { + match std::str::from_utf8(buf) + .ok() + .and_then(|v| (v, v.parse::().ok()?).into()) + { + Some((v, n)) if n.to_string() == v => Value::Number(Number::Integer(n)), + _ => Value::Text(buf.to_vec().into_string()), + } + } + } else { + Value::Text(buf.to_vec().into_string()) + }); + } else { + match self.tokenize_string(buf, false)? { + Value::List(new_items) => items.extend(new_items), + item => items.push(item), + } + } + + Ok(()) + } +} + +impl TryFrom<&str> for HeaderPart { + type Error = (); + + fn try_from(value: &str) -> Result { + let (value, subvalue) = value.split_once('.').unwrap_or((value, "")); + Ok(match value { + "" | "text" => HeaderPart::Text, + // Addresses + "name" => HeaderPart::Address(AddressPart::Name), + "addr" => { + if !subvalue.is_empty() { + HeaderPart::Address(AddressPart::try_from(subvalue)?) + } else { + HeaderPart::Address(AddressPart::All) + } + } + + // Content-type + "type" => HeaderPart::ContentType(ContentTypePart::Type), + "subtype" => HeaderPart::ContentType(ContentTypePart::Subtype), + "attr" if !subvalue.is_empty() => { + HeaderPart::ContentType(ContentTypePart::Attribute(subvalue.to_string())) + } + + // Received + "rcvd" => { + if !subvalue.is_empty() { + HeaderPart::Received(ReceivedPart::try_from(subvalue)?) + } else { + HeaderPart::Text + } + } + + // Id + "id" => HeaderPart::Id, + + // Raw + "raw" => HeaderPart::Raw, + + // Date + "date" => HeaderPart::Date, + + // Content-type attributes + _ => { + return Err(()); + } + }) + } +} + +impl TryFrom<&str> for ReceivedPart { + type Error = (); + + fn try_from(value: &str) -> Result { + Ok(match value { + // Received + "from" => ReceivedPart::From(ReceivedHostname::Any), + "from.name" => ReceivedPart::From(ReceivedHostname::Name), + "from.ip" => ReceivedPart::From(ReceivedHostname::Ip), + "ip" => ReceivedPart::FromIp, + "iprev" => ReceivedPart::FromIpRev, + "by" => ReceivedPart::By(ReceivedHostname::Any), + "by.name" => ReceivedPart::By(ReceivedHostname::Name), + "by.ip" => ReceivedPart::By(ReceivedHostname::Ip), + "for" => ReceivedPart::For, + "with" => ReceivedPart::With, + "tls" => ReceivedPart::TlsVersion, + "cipher" => ReceivedPart::TlsCipher, + "id" => ReceivedPart::Id, + "ident" => ReceivedPart::Ident, + "date" => ReceivedPart::Date, + "date.raw" => ReceivedPart::DateRaw, + _ => return Err(()), + }) + } +} + +impl TryFrom<&str> for AddressPart { + type Error = (); + + fn try_from(value: &str) -> Result { + Ok(match value { + "name" => AddressPart::Name, + "addr" | "all" => AddressPart::All, + "addr.domain" => AddressPart::Domain, + "addr.local" => AddressPart::LocalPart, + "addr.user" => AddressPart::User, + "addr.detail" => AddressPart::Detail, + _ => return Err(()), + }) + } +} + +impl Display for Value { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Value::Text(t) => f.write_str(t), + Value::List(l) => { + for i in l { + i.fmt(f)?; + } + Ok(()) + } + Value::Number(n) => n.fmt(f), + Value::Variable(v) => v.fmt(f), + Value::Expression(_) => f.write_str("%{}"), + Value::Regex(r) => f.write_str(&r.expr), + } + } +} + +impl Display for VariableType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + VariableType::Local(v) => write!(f, "${{{v}}}"), + VariableType::Match(v) => write!(f, "${{{v}}}"), + VariableType::Global(v) => write!(f, "${{global.{v}}}"), + VariableType::Environment(v) => write!(f, "${{env.{v}}}"), + + VariableType::Envelope(env) => f.write_str(match env { + Envelope::From => "${{envelope.from}}", + Envelope::To => "${{envelope.to}}", + Envelope::ByTimeAbsolute => "${{envelope.by_time_absolute}}", + Envelope::ByTimeRelative => "${{envelope.by_time_relative}}", + Envelope::ByMode => "${{envelope.by_mode}}", + Envelope::ByTrace => "${{envelope.by_trace}}", + Envelope::Notify => "${{envelope.notify}}", + Envelope::Orcpt => "${{envelope.orcpt}}", + Envelope::Ret => "${{envelope.ret}}", + Envelope::Envid => "${{envelope.envit}}", + }), + + VariableType::Header(hdr) => { + write!( + f, + "${{header.{}", + hdr.name.first().map(|h| h.as_str()).unwrap_or_default() + )?; + if hdr.index_hdr != 0 { + write!(f, "[{}]", hdr.index_hdr)?; + } else { + f.write_str("[*]")?; + } + /*if hdr.part != HeaderPart::Text { + f.write_str(".")?; + f.write_str(match &hdr.part { + HeaderPart::Name => "name", + HeaderPart::Address => "address", + HeaderPart::Type => "type", + HeaderPart::Subtype => "subtype", + HeaderPart::Raw => "raw", + HeaderPart::Date => "date", + HeaderPart::Attribute(attr) => attr.as_str(), + HeaderPart::Text => unreachable!(), + })?; + }*/ + if hdr.index_part != 0 { + write!(f, "[{}]", hdr.index_part)?; + } else { + f.write_str("[*]")?; + } + f.write_str("}") + } + VariableType::Part(part) => { + write!( + f, + "${{{}", + match part { + MessagePart::TextBody(true) => "body.to_text", + MessagePart::TextBody(false) => "body.text", + MessagePart::HtmlBody(true) => "body.to_html", + MessagePart::HtmlBody(false) => "body.html", + MessagePart::Contents => "part.text", + MessagePart::Raw => "part.raw", + } + )?; + f.write_str("}") + } + } + } +} + +#[cfg(test)] +mod tests { + + use mail_parser::HeaderName; + + use super::Value; + use crate::sieve::compiler::grammar::instruction::{ + Block, CompilerState, Instruction, MAX_PARAMS, + }; + use crate::sieve::compiler::grammar::test::Test; + use crate::sieve::compiler::grammar::tests::test_string::TestString; + use crate::sieve::compiler::grammar::{Comparator, MatchType}; + use crate::sieve::compiler::lexer::tokenizer::Tokenizer; + use crate::sieve::compiler::lexer::word::Word; + use crate::sieve::compiler::{AddressPart, HeaderPart, HeaderVariable, VariableType}; + use crate::sieve::{AHashSet, Compiler}; + + use std::convert::{TryFrom, TryInto}; + + #[test] + fn tokenize_string() { + let c = Compiler::new(); + let mut block = Block::new(Word::Not); + block.match_test_pos.push(0); + let mut compiler = CompilerState { + compiler: &c, + instructions: vec![Instruction::Test(Test::String(TestString { + match_type: MatchType::Regex(u64::MAX), + comparator: Comparator::AsciiCaseMap, + source: vec![Value::Variable(VariableType::Local(0))], + key_list: vec![Value::Variable(VariableType::Local(0))], + is_not: false, + }))], + block_stack: Vec::new(), + block, + last_block_type: Word::Not, + vars_global: AHashSet::new(), + vars_num: 0, + vars_num_max: 0, + vars_local: 0, + tokens: Tokenizer::new(&c, b""), + vars_match_max: usize::MAX, + param_check: [false; MAX_PARAMS], + includes_num: 0, + }; + + for (input, expected_result) in [ + ("$${hex:24 24}", Value::Text("$$$".to_string())), + ("$${hex:40}", Value::Text("$@".to_string())), + ("${hex: 40 }", Value::Text("@".to_string())), + ("${HEX: 40}", Value::Text("@".to_string())), + ("${hex:40", Value::Text("${hex:40".to_string())), + ("${hex:400}", Value::Text("${hex:400}".to_string())), + ("${hex:4${hex:30}}", Value::Text("${hex:40}".to_string())), + ("${unicode:40}", Value::Text("@".to_string())), + ("${ unicode:40}", Value::Text("${ unicode:40}".to_string())), + ("${UNICODE:40}", Value::Text("@".to_string())), + ("${UnICoDE:0000040}", Value::Text("@".to_string())), + ("${Unicode:40}", Value::Text("@".to_string())), + ( + "${Unicode:40 40 ", + Value::Text("${Unicode:40 40 ".to_string()), + ), + ( + "${Unicode:Cool}", + Value::Text("${Unicode:Cool}".to_string()), + ), + ("", Value::Text("".to_string())), + ( + "${global.full}", + Value::Variable(VariableType::Global("full".to_string())), + ), + ( + "${BAD${global.Company}", + Value::List(vec![ + Value::Text("${BAD".to_string()), + Value::Variable(VariableType::Global("company".to_string())), + ]), + ), + ( + "${President, ${global.Company} Inc.}", + Value::List(vec![ + Value::Text("${President, ".to_string()), + Value::Variable(VariableType::Global("company".to_string())), + Value::Text(" Inc.}".to_string()), + ]), + ), + ( + "dear${hex:20 24 7b}global.Name}", + Value::List(vec![ + Value::Text("dear ".to_string()), + Value::Variable(VariableType::Global("name".to_string())), + ]), + ), + ( + "INBOX.lists.${2}", + Value::List(vec![ + Value::Text("INBOX.lists.".to_string()), + Value::Variable(VariableType::Match(2)), + ]), + ), + ( + "Ein unerh${unicode:00F6}rt gro${unicode:00DF}er Test", + Value::Text("Ein unerhört großer Test".to_string()), + ), + ("&%${}!", Value::Text("&%${}!".to_string())), + ("${doh!}", Value::Text("${doh!}".to_string())), + ( + "${hex: 20 }${global.hi}${hex: 20 }", + Value::List(vec![ + Value::Text(" ".to_string()), + Value::Variable(VariableType::Global("hi".to_string())), + Value::Text(" ".to_string()), + ]), + ), + ( + "${hex:20 24 7b z}${global.hi}${unicode:}${unicode: }${hex:20}", + Value::List(vec![ + Value::Text("${hex:20 24 7b z}".to_string()), + Value::Variable(VariableType::Global("hi".to_string())), + Value::Text("${unicode:}${unicode: } ".to_string()), + ]), + ), + ( + "${header.from}", + Value::Variable(VariableType::Header(HeaderVariable { + name: vec![HeaderName::From], + part: HeaderPart::Text, + index_hdr: -1, + index_part: -1, + })), + ), + ( + "${header.from.addr}", + Value::Variable(VariableType::Header(HeaderVariable { + name: vec![HeaderName::From], + part: HeaderPart::Address(AddressPart::All), + index_hdr: -1, + index_part: -1, + })), + ), + ( + "${header.from[1]}", + Value::Variable(VariableType::Header(HeaderVariable { + name: vec![HeaderName::From], + part: HeaderPart::Text, + index_hdr: 1, + index_part: -1, + })), + ), + ( + "${header.from[*]}", + Value::Variable(VariableType::Header(HeaderVariable { + name: vec![HeaderName::From], + part: HeaderPart::Text, + index_hdr: 0, + index_part: -1, + })), + ), + ( + "${header.from[20].name}", + Value::Variable(VariableType::Header(HeaderVariable { + name: vec![HeaderName::From], + part: HeaderPart::Address(AddressPart::Name), + index_hdr: 20, + index_part: -1, + })), + ), + ( + "${header.from[*].addr}", + Value::Variable(VariableType::Header(HeaderVariable { + name: vec![HeaderName::From], + part: HeaderPart::Address(AddressPart::All), + index_hdr: 0, + index_part: -1, + })), + ), + ( + "${header.from[-5].name[2]}", + Value::Variable(VariableType::Header(HeaderVariable { + name: vec![HeaderName::From], + part: HeaderPart::Address(AddressPart::Name), + index_hdr: -5, + index_part: 2, + })), + ), + ( + "${header.from[*].raw[*]}", + Value::Variable(VariableType::Header(HeaderVariable { + name: vec![HeaderName::From], + part: HeaderPart::Raw, + index_hdr: 0, + index_part: 0, + })), + ), + ] { + assert_eq!( + compiler.tokenize_string(input.as_bytes(), true).unwrap(), + expected_result, + "Failed for {input}" + ); + } + + for input in ["${unicode:200000}", "${Unicode:DF01}"] { + assert!(compiler.tokenize_string(input.as_bytes(), true).is_err()); + } + } +} diff --git a/melib/src/sieve/compiler/lexer/tokenizer.rs b/melib/src/sieve/compiler/lexer/tokenizer.rs new file mode 100644 index 00000000..e42b0d85 --- /dev/null +++ b/melib/src/sieve/compiler/lexer/tokenizer.rs @@ -0,0 +1,590 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::{iter::Peekable, slice::Iter}; + +use crate::sieve::{ + compiler::{CompileError, ErrorType, Number}, + runtime::eval::IntoString, + Compiler, +}; + +use super::{word::WORDS, StringConstant, Token}; + +pub(crate) struct Tokenizer<'x> { + pub compiler: &'x Compiler, + pub iter: Peekable>, + pub buf: Vec, + pub next_token: Vec, + + pub pos: usize, + pub line_num: usize, + pub line_start: usize, + + pub text_line_num: usize, + pub text_line_pos: usize, + + pub token_line_num: usize, + pub token_line_pos: usize, + + pub token_is_tag: bool, + + pub last_ch: u8, + pub state: State, +} + +#[derive(Debug)] +pub(crate) struct TokenInfo { + pub(crate) token: Token, + pub(crate) line_num: usize, + pub(crate) line_pos: usize, +} + +pub(crate) enum State { + None, + BracketComment, + HashComment, + QuotedString(StringType), + MultiLine(StringType), +} + +#[derive(Clone, Copy, Default)] +pub(crate) struct StringType { + maybe_variable: bool, + has_other: bool, + has_digits: bool, + has_dots: bool, +} + +impl<'x> Tokenizer<'x> { + pub fn new(compiler: &'x Compiler, bytes: &'x [u8]) -> Self { + Tokenizer { + compiler, + iter: bytes.iter().peekable(), + buf: Vec::with_capacity(bytes.len() / 2), + pos: usize::MAX, + line_num: 1, + line_start: 0, + text_line_num: 0, + text_line_pos: 0, + token_line_num: 0, + token_line_pos: 0, + token_is_tag: false, + next_token: Vec::with_capacity(2), + last_ch: 0, + state: State::None, + } + } + + pub fn get_current_token(&mut self) -> Option { + if !self.buf.is_empty() { + let word = std::str::from_utf8(&self.buf).unwrap(); + let token = if let Some(word) = WORDS.get(word) { + if self.token_is_tag { + self.token_line_pos -= 1; + Token::Tag(*word) + } else { + Token::Identifier(*word) + } + } else if self.buf.first().unwrap().is_ascii_digit() { + let multiplier = match self.buf.last().unwrap() { + b'k' => 1024, + b'm' => 1048576, + b'g' => 1073741824, + _ => 1, + }; + + if let Ok(number) = (if multiplier > 1 && self.buf.len() > 1 { + std::str::from_utf8(&self.buf[..self.buf.len() - 1]).unwrap() + } else { + word + }) + .parse::() + { + Token::Number(number.saturating_mul(multiplier)) + } else if self.token_is_tag { + Token::Unknown(format!(":{word}")) + } else { + Token::Unknown(word.to_string()) + } + } else if self.token_is_tag { + Token::Unknown(format!(":{word}")) + } else { + Token::Unknown(word.to_string()) + }; + + self.reset_current_token(); + + Some(TokenInfo { + token, + line_num: self.token_line_num, + line_pos: self.token_line_pos, + }) + } else { + None + } + } + + #[inline(always)] + pub fn reset_current_token(&mut self) { + self.buf.clear(); + self.token_is_tag = false; + } + + #[inline(always)] + pub fn token_is_tag(&mut self) { + self.token_is_tag = true; + } + + pub fn get_token(&mut self, token: Token) -> TokenInfo { + let next_token = TokenInfo { + token, + line_num: self.line_num, + line_pos: self.pos - self.line_start, + }; + if let Some(token) = self.get_current_token() { + self.next_token.push(next_token); + token + } else { + next_token + } + } + + pub fn get_string(&mut self, str_type: StringType) -> Result { + if self.buf.len() < self.compiler.max_string_size { + let token = if str_type.maybe_variable { + Token::StringVariable(self.buf.to_vec()) + } else { + let constant = self.buf.to_vec().into_string(); + if !str_type.has_other && str_type.has_digits { + if !str_type.has_dots { + if let Some(number) = constant.parse::().ok().and_then(|n| { + if n.to_string() == constant { + Some(n) + } else { + None + } + }) { + Token::StringConstant(StringConstant::Number(Number::Integer(number))) + } else { + Token::StringConstant(StringConstant::String(constant)) + } + } else if let Some(number) = constant.parse::().ok().and_then(|n| { + if n.to_string() == constant { + Some(n) + } else { + None + } + }) { + Token::StringConstant(StringConstant::Number(Number::Float(number))) + } else { + Token::StringConstant(StringConstant::String(constant)) + } + } else { + Token::StringConstant(StringConstant::String(constant)) + } + }; + + self.buf.clear(); + + Ok(TokenInfo { + token, + line_num: self.text_line_num, + line_pos: self.text_line_pos, + }) + } else { + Err(CompileError { + line_num: self.text_line_num, + line_pos: self.text_line_pos, + error_type: ErrorType::StringTooLong, + }) + } + } + + #[inline(always)] + pub fn push_byte(&mut self, ch: u8) { + if self.buf.is_empty() { + self.token_line_num = self.line_num; + self.token_line_pos = self.pos - self.line_start; + } + self.buf.push(ch); + } + + #[inline(always)] + pub fn new_line(&mut self) { + self.line_num += 1; + self.line_start = self.pos; + } + + #[inline(always)] + pub fn text_start(&mut self) { + self.text_line_num = self.line_num; + self.text_line_pos = self.pos - self.line_start; + } + + #[inline(always)] + pub fn is_token_start(&self) -> bool { + self.buf.is_empty() + } + + #[inline(always)] + pub fn token_bytes(&self) -> &[u8] { + &self.buf + } + + #[inline(always)] + pub fn next_byte(&mut self) -> Option<(u8, u8)> { + self.iter.next().map(|&ch| { + let last_ch = self.last_ch; + self.pos = self.pos.wrapping_add(1); + self.last_ch = ch; + (ch, last_ch) + }) + } + + #[inline(always)] + pub fn peek_byte(&mut self) -> Option { + self.iter.peek().map(|ch| **ch) + } + + pub fn unwrap_next(&mut self) -> Result { + if let Some(token) = self.next() { + token + } else { + Err(CompileError { + line_num: self.line_num, + line_pos: self.pos - self.line_start, + error_type: ErrorType::UnexpectedEOF, + }) + } + } + + pub fn expect_token(&mut self, token: Token) -> Result<(), CompileError> { + let next_token = self.unwrap_next()?; + if next_token.token == token { + Ok(()) + } else { + Err(next_token.expected(format!("'{token}'"))) + } + } + + pub fn expect_static_string(&mut self) -> Result { + let next_token = self.unwrap_next()?; + match next_token.token { + Token::StringConstant(s) => Ok(s.into_string()), + Token::BracketOpen => { + let mut string = None; + loop { + let token_info = self.unwrap_next()?; + match token_info.token { + Token::StringConstant(string_) => { + string = string_.into(); + } + Token::BracketClose if string.is_some() => break, + _ => return Err(token_info.expected("constant string")), + } + } + Ok(string.unwrap().into_string()) + } + _ => Err(next_token.expected("constant string")), + } + } + + pub fn expect_number(&mut self, max_value: usize) -> Result { + let next_token = self.unwrap_next()?; + if let Token::Number(n) = next_token.token { + if n < max_value { + Ok(n) + } else { + Err(next_token.expected(format!("number lower than {max_value}"))) + } + } else { + Err(next_token.expected("number")) + } + } + + pub fn invalid_character(&self) -> CompileError { + CompileError { + line_num: self.line_num, + line_pos: self.pos - self.line_start, + error_type: ErrorType::InvalidCharacter(self.last_ch), + } + } + + pub fn peek(&mut self) -> Option> { + if self.next_token.is_empty() { + match self.next()? { + Ok(next_token) => self.next_token.push(next_token), + Err(err) => return Some(Err(err)), + } + } + self.next_token.last().map(Ok) + } +} + +impl<'x> Iterator for Tokenizer<'x> { + type Item = Result; + + fn next(&mut self) -> Option { + if let Some(prev_token) = self.next_token.pop() { + return Some(Ok(prev_token)); + } + + 'outer: while let Some((ch, last_ch)) = self.next_byte() { + match self.state { + State::None => match ch { + b'a'..=b'z' | b'0'..=b'9' | b'_' | b'.' | b'$' => { + self.push_byte(ch); + } + b'A'..=b'Z' => { + self.push_byte(ch.to_ascii_lowercase()); + } + b':' => { + if self.is_token_start() + && matches!(self.peek_byte(), Some(b) if b.is_ascii_alphabetic()) + { + self.token_is_tag(); + } else if self.token_bytes().eq_ignore_ascii_case(b"text") { + self.state = State::MultiLine(StringType::default()); + self.text_start(); + while let Some((ch, _)) = self.next_byte() { + if ch == b'\n' { + self.new_line(); + self.reset_current_token(); + continue 'outer; + } + } + } else { + return Some(Ok(self.get_token(Token::Colon))); + //return Some(Err(self.invalid_character())); + } + } + b'"' => { + self.state = State::QuotedString(StringType::default()); + self.text_start(); + if let Some(token) = self.get_current_token() { + return Some(Ok(token)); + } + } + b'{' => { + return Some(Ok(self.get_token(Token::CurlyOpen))); + } + b'}' => { + return Some(Ok(self.get_token(Token::CurlyClose))); + } + b';' => { + return Some(Ok(self.get_token(Token::Semicolon))); + } + b',' => { + return Some(Ok(self.get_token(Token::Comma))); + } + b'[' => { + return Some(Ok(self.get_token(Token::BracketOpen))); + } + b']' => { + return Some(Ok(self.get_token(Token::BracketClose))); + } + b'(' => { + return Some(Ok(self.get_token(Token::ParenthesisOpen))); + } + b')' => { + return Some(Ok(self.get_token(Token::ParenthesisClose))); + } + b'/' => { + if let Some((b'*', _)) = self.next_byte() { + self.last_ch = 0; + self.state = State::BracketComment; + self.text_start(); + if let Some(token) = self.get_current_token() { + return Some(Ok(token)); + } + } else { + return Some(Err(self.invalid_character())); + } + } + b'#' => { + self.state = State::HashComment; + if let Some(token) = self.get_current_token() { + return Some(Ok(token)); + } + } + b'\n' => { + self.new_line(); + if let Some(token) = self.get_current_token() { + return Some(Ok(token)); + } + } + b' ' | b'\t' | b'\r' => { + if let Some(token) = self.get_current_token() { + return Some(Ok(token)); + } + } + _ => { + return Some(Err(self.invalid_character())); + } + }, + State::BracketComment { .. } => match ch { + b'/' if last_ch == b'*' => { + self.state = State::None; + } + b'\n' => { + self.new_line(); + } + _ => (), + }, + State::HashComment => { + if ch == b'\n' { + self.state = State::None; + self.new_line(); + } + } + State::QuotedString(mut str_type) => match ch { + b'"' if last_ch != b'\\' => { + self.state = State::None; + return Some(self.get_string(str_type)); + } + b'\n' => { + self.new_line(); + self.push_byte(b'\n'); + str_type.has_other = true; + self.state = State::QuotedString(str_type); + } + b'{' if (last_ch == b'$' || last_ch == b'%') => { + str_type.maybe_variable = true; + self.state = State::QuotedString(str_type); + self.push_byte(ch); + } + b'\\' => { + if last_ch == b'\\' { + self.push_byte(ch); + } + } + b'0'..=b'9' => { + if !str_type.has_digits { + str_type.has_digits = true; + self.state = State::QuotedString(str_type); + } + self.push_byte(ch); + } + b'.' => { + if !str_type.has_dots { + str_type.has_dots = true; + } else { + str_type.has_other = true; + } + self.state = State::QuotedString(str_type); + self.push_byte(ch); + } + _ => { + if !str_type.has_other && ch != b'-' { + str_type.has_other = true; + self.state = State::QuotedString(str_type); + } + self.push_byte(ch); + } + }, + State::MultiLine(mut str_type) => match ch { + b'.' if last_ch == b'\n' => { + let is_eof = match (self.next_byte(), self.peek_byte()) { + (Some((b'\r', _)), Some(b'\n')) => { + self.next_byte(); + true + } + (Some((b'\n', _)), _) => true, + (Some((b'.', _)), _) => { + self.push_byte(b'.'); + false + } + (Some((ch, _)), _) => { + self.push_byte(b'.'); + self.push_byte(ch); + false + } + _ => false, + }; + + if is_eof { + self.new_line(); + self.state = State::None; + return Some(self.get_string(str_type)); + } + } + b'\n' => { + self.new_line(); + self.push_byte(b'\n'); + } + b'{' if (last_ch == b'$' || last_ch == b'%') => { + str_type.maybe_variable = true; + self.state = State::MultiLine(str_type); + self.push_byte(ch); + } + b'0'..=b'9' => { + if !str_type.has_digits { + str_type.has_digits = true; + self.state = State::MultiLine(str_type); + } + self.push_byte(ch); + } + b'.' => { + if !str_type.has_dots { + str_type.has_dots = true; + } else { + str_type.has_other = true; + } + self.state = State::MultiLine(str_type); + self.push_byte(ch); + } + _ => { + if !str_type.has_other && ch != b'-' { + str_type.has_other = true; + self.state = State::MultiLine(str_type); + } + self.push_byte(ch); + } + }, + } + } + + match self.state { + State::BracketComment | State::QuotedString(_) | State::MultiLine(_) => { + Some(Err(CompileError { + line_num: self.text_line_num, + line_pos: self.text_line_pos, + error_type: (&self.state).into(), + })) + } + _ => None, + } + } +} + +impl From<&State> for ErrorType { + fn from(state: &State) -> Self { + match state { + State::BracketComment => ErrorType::UnterminatedComment, + State::QuotedString(_) => ErrorType::UnterminatedString, + State::MultiLine(_) => ErrorType::UnterminatedMultiline, + _ => unreachable!(), + } + } +} diff --git a/melib/src/sieve/compiler/lexer/word.rs b/melib/src/sieve/compiler/lexer/word.rs new file mode 100644 index 00000000..4f08104f --- /dev/null +++ b/melib/src/sieve/compiler/lexer/word.rs @@ -0,0 +1,420 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::fmt::Display; + +use phf::phf_map; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub(crate) enum Word { + AddFlag, + AddHeader, + Address, + Addresses, + All, + AllOf, + AnyChild, + AnyOf, + Body, + Break, + ByMode, + ByTimeAbsolute, + ByTimeRelative, + ByTrace, + Comparator, + Contains, + Content, + ContentType, + Convert, + Copy, + Count, + Create, + CurrentDate, + Date, + Days, + DeleteHeader, + Detail, + Discard, + Domain, + Duplicate, + Else, + ElsIf, + Enclose, + EncodeUrl, + Envelope, + Environment, + Ereject, + Error, + Exists, + ExtractText, + False, + Fcc, + FileInto, + First, + Flags, + ForEveryPart, + From, + Global, + Handle, + HasFlag, + Header, + Headers, + If, + Ihave, + Importance, + Include, + Index, + Is, + Keep, + Last, + Length, + List, + LocalPart, + Lower, + LowerFirst, + MailboxExists, + MailboxId, + MailboxIdExists, + Matches, + Message, + Metadata, + MetadataExists, + Mime, + Name, + Not, + Notify, + NotifyMethodCapability, + Once, + Optional, + Options, + OriginalZone, + Over, + Param, + Percent, + Personal, + QuoteRegex, + QuoteWildcard, + Raw, + Redirect, + Regex, + Reject, + RemoveFlag, + Replace, + Require, + Ret, + Return, + Seconds, + ServerMetadata, + ServerMetadataExists, + Set, + SetFlag, + Size, + SpamTest, + SpecialUse, + SpecialUseExists, + Stop, + String, + Subject, + Subtype, + Text, + True, + Type, + Under, + UniqueId, + Upper, + UpperFirst, + User, + Vacation, + ValidExtList, + ValidNotifyMethod, + Value, + VirusTest, + Zone, + + // Extensions + Eval, + Local, + ForEveryLine, +} + +pub(crate) static WORDS: phf::Map<&'static str, Word> = phf_map! { + "addflag" => Word::AddFlag, + "addheader" => Word::AddHeader, + "address" => Word::Address, + "addresses" => Word::Addresses, + "all" => Word::All, + "allof" => Word::AllOf, + "anychild" => Word::AnyChild, + "anyof" => Word::AnyOf, + "body" => Word::Body, + "break" => Word::Break, + "bymode" => Word::ByMode, + "bytimeabsolute" => Word::ByTimeAbsolute, + "bytimerelative" => Word::ByTimeRelative, + "bytrace" => Word::ByTrace, + "comparator" => Word::Comparator, + "contains" => Word::Contains, + "content" => Word::Content, + "contenttype" => Word::ContentType, + "convert" => Word::Convert, + "copy" => Word::Copy, + "count" => Word::Count, + "create" => Word::Create, + "currentdate" => Word::CurrentDate, + "date" => Word::Date, + "days" => Word::Days, + "deleteheader" => Word::DeleteHeader, + "detail" => Word::Detail, + "discard" => Word::Discard, + "domain" => Word::Domain, + "duplicate" => Word::Duplicate, + "else" => Word::Else, + "elsif" => Word::ElsIf, + "enclose" => Word::Enclose, + "encodeurl" => Word::EncodeUrl, + "envelope" => Word::Envelope, + "environment" => Word::Environment, + "ereject" => Word::Ereject, + "error" => Word::Error, + "exists" => Word::Exists, + "extracttext" => Word::ExtractText, + "false" => Word::False, + "fcc" => Word::Fcc, + "fileinto" => Word::FileInto, + "first" => Word::First, + "flags" => Word::Flags, + "foreverypart" => Word::ForEveryPart, + "from" => Word::From, + "global" => Word::Global, + "handle" => Word::Handle, + "hasflag" => Word::HasFlag, + "header" => Word::Header, + "headers" => Word::Headers, + "if" => Word::If, + "ihave" => Word::Ihave, + "importance" => Word::Importance, + "include" => Word::Include, + "index" => Word::Index, + "is" => Word::Is, + "keep" => Word::Keep, + "last" => Word::Last, + "length" => Word::Length, + "list" => Word::List, + "localpart" => Word::LocalPart, + "lower" => Word::Lower, + "lowerfirst" => Word::LowerFirst, + "mailboxexists" => Word::MailboxExists, + "mailboxid" => Word::MailboxId, + "mailboxidexists" => Word::MailboxIdExists, + "matches" => Word::Matches, + "message" => Word::Message, + "metadata" => Word::Metadata, + "metadataexists" => Word::MetadataExists, + "mime" => Word::Mime, + "name" => Word::Name, + "not" => Word::Not, + "notify" => Word::Notify, + "notify_method_capability" => Word::NotifyMethodCapability, + "once" => Word::Once, + "optional" => Word::Optional, + "options" => Word::Options, + "originalzone" => Word::OriginalZone, + "over" => Word::Over, + "param" => Word::Param, + "percent" => Word::Percent, + "personal" => Word::Personal, + "quoteregex" => Word::QuoteRegex, + "quotewildcard" => Word::QuoteWildcard, + "raw" => Word::Raw, + "redirect" => Word::Redirect, + "regex" => Word::Regex, + "reject" => Word::Reject, + "removeflag" => Word::RemoveFlag, + "replace" => Word::Replace, + "require" => Word::Require, + "ret" => Word::Ret, + "return" => Word::Return, + "seconds" => Word::Seconds, + "servermetadata" => Word::ServerMetadata, + "servermetadataexists" => Word::ServerMetadataExists, + "set" => Word::Set, + "setflag" => Word::SetFlag, + "size" => Word::Size, + "spamtest" => Word::SpamTest, + "specialuse" => Word::SpecialUse, + "specialuse_exists" => Word::SpecialUseExists, + "stop" => Word::Stop, + "string" => Word::String, + "subject" => Word::Subject, + "subtype" => Word::Subtype, + "text" => Word::Text, + "true" => Word::True, + "type" => Word::Type, + "under" => Word::Under, + "uniqueid" => Word::UniqueId, + "upper" => Word::Upper, + "upperfirst" => Word::UpperFirst, + "user" => Word::User, + "vacation" => Word::Vacation, + "valid_ext_list" => Word::ValidExtList, + "valid_notify_method" => Word::ValidNotifyMethod, + "value" => Word::Value, + "virustest" => Word::VirusTest, + "zone" => Word::Zone, + "eval" => Word::Eval, + "local" => Word::Local, + "foreveryline" => Word::ForEveryLine, +}; + +impl Display for Word { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Word::AddFlag => f.write_str("addflag"), + Word::AddHeader => f.write_str("addheader"), + Word::Address => f.write_str("address"), + Word::Addresses => f.write_str("addresses"), + Word::All => f.write_str("all"), + Word::AllOf => f.write_str("allof"), + Word::AnyChild => f.write_str("anychild"), + Word::AnyOf => f.write_str("anyof"), + Word::Body => f.write_str("body"), + Word::Break => f.write_str("break"), + Word::ByMode => f.write_str("bymode"), + Word::ByTimeAbsolute => f.write_str("bytimeabsolute"), + Word::ByTimeRelative => f.write_str("bytimerelative"), + Word::ByTrace => f.write_str("bytrace"), + Word::Comparator => f.write_str("comparator"), + Word::Contains => f.write_str("contains"), + Word::Content => f.write_str("content"), + Word::ContentType => f.write_str("contenttype"), + Word::Convert => f.write_str("convert"), + Word::Copy => f.write_str("copy"), + Word::Count => f.write_str("count"), + Word::Create => f.write_str("create"), + Word::CurrentDate => f.write_str("currentdate"), + Word::Date => f.write_str("date"), + Word::Days => f.write_str("days"), + Word::DeleteHeader => f.write_str("deleteheader"), + Word::Detail => f.write_str("detail"), + Word::Discard => f.write_str("discard"), + Word::Domain => f.write_str("domain"), + Word::Duplicate => f.write_str("duplicate"), + Word::Else => f.write_str("else"), + Word::ElsIf => f.write_str("elsif"), + Word::Enclose => f.write_str("enclose"), + Word::EncodeUrl => f.write_str("encodeurl"), + Word::Envelope => f.write_str("envelope"), + Word::Environment => f.write_str("environment"), + Word::Ereject => f.write_str("ereject"), + Word::Error => f.write_str("error"), + Word::Exists => f.write_str("exists"), + Word::ExtractText => f.write_str("extracttext"), + Word::False => f.write_str("false"), + Word::Fcc => f.write_str("fcc"), + Word::FileInto => f.write_str("fileinto"), + Word::First => f.write_str("first"), + Word::Flags => f.write_str("flags"), + Word::ForEveryPart => f.write_str("foreverypart"), + Word::From => f.write_str("from"), + Word::Global => f.write_str("global"), + Word::Handle => f.write_str("handle"), + Word::HasFlag => f.write_str("hasflag"), + Word::Header => f.write_str("header"), + Word::Headers => f.write_str("headers"), + Word::If => f.write_str("if"), + Word::Ihave => f.write_str("ihave"), + Word::Importance => f.write_str("importance"), + Word::Include => f.write_str("include"), + Word::Index => f.write_str("index"), + Word::Is => f.write_str("is"), + Word::Keep => f.write_str("keep"), + Word::Last => f.write_str("last"), + Word::Length => f.write_str("length"), + Word::List => f.write_str("list"), + Word::LocalPart => f.write_str("localpart"), + Word::Lower => f.write_str("lower"), + Word::LowerFirst => f.write_str("lowerfirst"), + Word::MailboxExists => f.write_str("mailboxexists"), + Word::MailboxId => f.write_str("mailboxid"), + Word::MailboxIdExists => f.write_str("mailboxidexists"), + Word::Matches => f.write_str("matches"), + Word::Message => f.write_str("message"), + Word::Metadata => f.write_str("metadata"), + Word::MetadataExists => f.write_str("metadataexists"), + Word::Mime => f.write_str("mime"), + Word::Name => f.write_str("name"), + Word::Not => f.write_str("not"), + Word::Notify => f.write_str("notify"), + Word::NotifyMethodCapability => f.write_str("notify_method_capability"), + Word::Once => f.write_str("once"), + Word::Optional => f.write_str("optional"), + Word::Options => f.write_str("options"), + Word::OriginalZone => f.write_str("originalzone"), + Word::Over => f.write_str("over"), + Word::Param => f.write_str("param"), + Word::Percent => f.write_str("percent"), + Word::Personal => f.write_str("personal"), + Word::QuoteRegex => f.write_str("quoteregex"), + Word::QuoteWildcard => f.write_str("quotewildcard"), + Word::Raw => f.write_str("raw"), + Word::Redirect => f.write_str("redirect"), + Word::Regex => f.write_str("regex"), + Word::Reject => f.write_str("reject"), + Word::RemoveFlag => f.write_str("removeflag"), + Word::Replace => f.write_str("replace"), + Word::Require => f.write_str("require"), + Word::Ret => f.write_str("ret"), + Word::Return => f.write_str("return"), + Word::Seconds => f.write_str("seconds"), + Word::ServerMetadata => f.write_str("servermetadata"), + Word::ServerMetadataExists => f.write_str("servermetadataexists"), + Word::Set => f.write_str("set"), + Word::SetFlag => f.write_str("setflag"), + Word::Size => f.write_str("size"), + Word::SpamTest => f.write_str("spamtest"), + Word::SpecialUse => f.write_str("specialuse"), + Word::SpecialUseExists => f.write_str("specialuse_exists"), + Word::Stop => f.write_str("stop"), + Word::String => f.write_str("string"), + Word::Subject => f.write_str("subject"), + Word::Subtype => f.write_str("subtype"), + Word::Text => f.write_str("text"), + Word::True => f.write_str("true"), + Word::Type => f.write_str("type"), + Word::Under => f.write_str("under"), + Word::UniqueId => f.write_str("uniqueid"), + Word::Upper => f.write_str("upper"), + Word::UpperFirst => f.write_str("upperfirst"), + Word::User => f.write_str("user"), + Word::Vacation => f.write_str("vacation"), + Word::ValidExtList => f.write_str("valid_ext_list"), + Word::ValidNotifyMethod => f.write_str("valid_notify_method"), + Word::Value => f.write_str("value"), + Word::VirusTest => f.write_str("virustest"), + Word::Zone => f.write_str("zone"), + Word::Eval => f.write_str("eval"), + Word::Local => f.write_str("local"), + Word::ForEveryLine => f.write_str("foreveryline"), + } + } +} diff --git a/melib/src/sieve/compiler/mod.rs b/melib/src/sieve/compiler/mod.rs new file mode 100644 index 00000000..2244ba8b --- /dev/null +++ b/melib/src/sieve/compiler/mod.rs @@ -0,0 +1,719 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::{borrow::Cow, fmt::Display}; + +use ahash::AHashMap; +use mail_parser::HeaderName; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use crate::sieve::{ + runtime::RuntimeError, Compiler, Envelope, ExternalId, FunctionMap, PluginSchema, + PluginSchemaArgument, PluginSchemaTag, +}; + +use self::{ + grammar::{expr::Expression, AddressPart, Capability}, + lexer::tokenizer::TokenInfo, +}; + +pub mod grammar; +pub mod lexer; + +#[derive(Debug)] +pub struct CompileError { + line_num: usize, + line_pos: usize, + error_type: ErrorType, +} + +#[derive(Debug)] +pub enum ErrorType { + InvalidCharacter(u8), + InvalidNumber(String), + InvalidMatchVariable(usize), + InvalidUnicodeSequence(u32), + InvalidNamespace(String), + InvalidRegex(String), + InvalidExpression(String), + InvalidUtf8String, + InvalidHeaderName, + InvalidArguments, + InvalidAddress, + InvalidURI, + InvalidEnvelope(String), + UnterminatedString, + UnterminatedComment, + UnterminatedMultiline, + UnterminatedBlock, + ScriptTooLong, + StringTooLong, + VariableTooLong, + VariableIsLocal(String), + HeaderTooLong, + ExpectedConstantString, + UnexpectedToken { + expected: Cow<'static, str>, + found: String, + }, + UnexpectedEOF, + TooManyNestedBlocks, + TooManyNestedTests, + TooManyNestedForEveryParts, + TooManyIncludes, + LabelAlreadyDefined(String), + LabelUndefined(String), + BreakOutsideLoop, + UnsupportedComparator(String), + DuplicatedParameter, + UndeclaredCapability(Capability), + MissingTag(Cow<'static, str>), +} + +impl Default for Compiler { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) enum Value { + Text(String), + Number(Number), + Variable(VariableType), + Expression(Vec), + Regex(Regex), + List(Vec), +} + +#[derive(Debug, Clone)] +pub struct Regex { + pub regex: fancy_regex::Regex, + pub expr: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum VariableType { + Local(usize), + Match(usize), + Global(String), + Environment(String), + Envelope(Envelope), + Header(HeaderVariable), + Part(MessagePart), +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Transform { + pub variable: Box, + pub functions: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct HeaderVariable { + pub name: Vec>, + pub part: HeaderPart, + pub index_hdr: i32, + pub index_part: i32, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum MessagePart { + TextBody(bool), + HtmlBody(bool), + Contents, + Raw, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum HeaderPart { + Text, + Date, + Id, + Address(AddressPart), + ContentType(ContentTypePart), + Received(ReceivedPart), + Raw, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum ContentTypePart { + Type, + Subtype, + Attribute(String), +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum ReceivedPart { + From(ReceivedHostname), + FromIp, + FromIpRev, + By(ReceivedHostname), + For, + With, + TlsVersion, + TlsCipher, + Id, + Ident, + Via, + Date, + DateRaw, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum ReceivedHostname { + Name, + Ip, + Any, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub enum Number { + Integer(i64), + Float(f64), +} + +impl Number { + #[cfg(test)] + pub fn to_float(&self) -> f64 { + match self { + Number::Integer(i) => *i as f64, + Number::Float(fl) => *fl, + } + } +} + +impl From for usize { + fn from(value: Number) -> Self { + match value { + Number::Integer(i) => i as usize, + Number::Float(fl) => fl as usize, + } + } +} + +impl Display for Number { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Number::Integer(i) => i.fmt(f), + Number::Float(fl) => fl.fmt(f), + } + } +} + +impl Compiler { + pub const VERSION: u32 = 2; + + pub fn new() -> Self { + Compiler { + max_script_size: 1024 * 1024, + max_string_size: 4096, + max_variable_name_size: 32, + max_nested_blocks: 15, + max_nested_tests: 15, + max_nested_foreverypart: 3, + max_match_variables: 30, + max_local_variables: 128, + max_header_size: 1024, + max_includes: 6, + plugins: AHashMap::new(), + functions: AHashMap::new(), + } + } + + pub fn set_max_header_size(&mut self, size: usize) { + self.max_header_size = size; + } + + pub fn with_max_header_size(mut self, size: usize) -> Self { + self.max_header_size = size; + self + } + + pub fn set_max_includes(&mut self, size: usize) { + self.max_includes = size; + } + + pub fn with_max_includes(mut self, size: usize) -> Self { + self.max_includes = size; + self + } + + pub fn set_max_nested_blocks(&mut self, size: usize) { + self.max_nested_blocks = size; + } + + pub fn with_max_nested_blocks(mut self, size: usize) -> Self { + self.max_nested_blocks = size; + self + } + + pub fn set_max_nested_tests(&mut self, size: usize) { + self.max_nested_tests = size; + } + + pub fn with_max_nested_tests(mut self, size: usize) -> Self { + self.max_nested_tests = size; + self + } + + pub fn set_max_nested_foreverypart(&mut self, size: usize) { + self.max_nested_foreverypart = size; + } + + pub fn with_max_nested_foreverypart(mut self, size: usize) -> Self { + self.max_nested_foreverypart = size; + self + } + + pub fn set_max_script_size(&mut self, size: usize) { + self.max_script_size = size; + } + + pub fn with_max_script_size(mut self, size: usize) -> Self { + self.max_script_size = size; + self + } + + pub fn set_max_string_size(&mut self, size: usize) { + self.max_string_size = size; + } + + pub fn with_max_string_size(mut self, size: usize) -> Self { + self.max_string_size = size; + self + } + + pub fn set_max_variable_name_size(&mut self, size: usize) { + self.max_variable_name_size = size; + } + + pub fn with_max_variable_name_size(mut self, size: usize) -> Self { + self.max_variable_name_size = size; + self + } + + pub fn set_max_match_variables(&mut self, size: usize) { + self.max_match_variables = size; + } + + pub fn with_max_match_variables(mut self, size: usize) -> Self { + self.max_match_variables = size; + self + } + + pub fn set_max_local_variables(&mut self, size: usize) { + self.max_local_variables = size; + } + + pub fn with_max_local_variables(mut self, size: usize) -> Self { + self.max_local_variables = size; + self + } + + pub fn register_plugin(&mut self, name: impl Into) -> &mut PluginSchema { + let id = self.plugins.len() as ExternalId; + self.plugins + .entry(name.into()) + .or_insert_with(|| PluginSchema { + id, + tags: AHashMap::new(), + arguments: Vec::new(), + }) + } + + pub fn register_functions(mut self, fnc_map: &mut FunctionMap) -> Self { + self.functions = std::mem::take(&mut fnc_map.map); + self + } +} + +impl PluginSchema { + pub fn with_id(&mut self, id: ExternalId) -> &mut Self { + self.id = id; + self + } + + pub fn with_string_argument(&mut self) -> &mut Self { + self.arguments.push(PluginSchemaArgument::Text); + self + } + + pub fn with_number_argument(&mut self) -> &mut Self { + self.arguments.push(PluginSchemaArgument::Number); + self + } + + pub fn with_regex_argument(&mut self) -> &mut Self { + self.arguments.push(PluginSchemaArgument::Regex); + self + } + + pub fn with_variable_argument(&mut self) -> &mut Self { + self.arguments.push(PluginSchemaArgument::Variable); + self + } + + pub fn with_string_array_argument(&mut self) -> &mut Self { + self.arguments.push(PluginSchemaArgument::Array(Box::new( + PluginSchemaArgument::Text, + ))); + self + } + + pub fn with_number_array_argument(&mut self) -> &mut Self { + self.arguments.push(PluginSchemaArgument::Array(Box::new( + PluginSchemaArgument::Number, + ))); + self + } + + pub fn with_regex_array_argument(&mut self) -> &mut Self { + self.arguments.push(PluginSchemaArgument::Array(Box::new( + PluginSchemaArgument::Regex, + ))); + self + } + + pub fn with_variable_array_argument(&mut self) -> &mut Self { + self.arguments.push(PluginSchemaArgument::Array(Box::new( + PluginSchemaArgument::Variable, + ))); + self + } + + pub fn with_argument(&mut self, argument: PluginSchemaArgument) -> &mut Self { + self.arguments.push(argument); + self + } + + pub fn with_tagged_argument( + &mut self, + tag: impl Into, + argument: PluginSchemaArgument, + ) -> &mut Self { + let id = self.tags.len() as ExternalId; + self.tags.insert( + tag.into(), + PluginSchemaTag { + id, + argument: argument.into(), + }, + ); + self + } + + pub fn with_tagged_string_argument(&mut self, tag: impl Into) -> &mut Self { + self.with_tagged_argument(tag, PluginSchemaArgument::Text) + } + + pub fn with_tagged_number_argument(&mut self, tag: impl Into) -> &mut Self { + self.with_tagged_argument(tag, PluginSchemaArgument::Number) + } + + pub fn with_tagged_regex_argument(&mut self, tag: impl Into) -> &mut Self { + self.with_tagged_argument(tag, PluginSchemaArgument::Regex) + } + + pub fn with_tagged_variable_argument(&mut self, tag: impl Into) -> &mut Self { + self.with_tagged_argument(tag, PluginSchemaArgument::Variable) + } + + pub fn with_tagged_string_array_argument(&mut self, tag: impl Into) -> &mut Self { + self.with_tagged_argument( + tag, + PluginSchemaArgument::Array(Box::new(PluginSchemaArgument::Text)), + ) + } + + pub fn with_tagged_number_array_argument(&mut self, tag: impl Into) -> &mut Self { + self.with_tagged_argument( + tag, + PluginSchemaArgument::Array(Box::new(PluginSchemaArgument::Number)), + ) + } + + pub fn with_tagged_regex_array_argument(&mut self, tag: impl Into) -> &mut Self { + self.with_tagged_argument( + tag, + PluginSchemaArgument::Array(Box::new(PluginSchemaArgument::Regex)), + ) + } + + pub fn with_tagged_variable_array_argument(&mut self, tag: impl Into) -> &mut Self { + self.with_tagged_argument( + tag, + PluginSchemaArgument::Array(Box::new(PluginSchemaArgument::Variable)), + ) + } + + pub fn with_tag(&mut self, tag: impl Into) -> &mut Self { + let id = self.tags.len() as ExternalId; + self.tags + .insert(tag.into(), PluginSchemaTag { id, argument: None }); + self + } +} + +impl CompileError { + pub fn line_num(&self) -> usize { + self.line_num + } + + pub fn line_pos(&self) -> usize { + self.line_pos + } + + pub fn error_type(&self) -> &ErrorType { + &self.error_type + } +} + +impl PartialEq for Regex { + fn eq(&self, other: &Self) -> bool { + self.expr == other.expr + } +} + +impl Eq for Regex {} + +impl Serialize for Regex { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.expr.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Regex { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + ::deserialize(deserializer).and_then(|expr| { + fancy_regex::Regex::new(&expr) + .map(|regex| Regex { regex, expr }) + .map_err(|err| serde::de::Error::custom(err.to_string())) + }) + } +} + +impl TokenInfo { + pub fn expected(self, expected: impl Into>) -> CompileError { + CompileError { + line_num: self.line_num, + line_pos: self.line_pos, + error_type: ErrorType::UnexpectedToken { + expected: expected.into(), + found: self.token.to_string(), + }, + } + } + + pub fn missing_tag(self, tag: impl Into>) -> CompileError { + CompileError { + line_num: self.line_num, + line_pos: self.line_pos, + error_type: ErrorType::MissingTag(tag.into()), + } + } + + pub fn custom(self, error_type: ErrorType) -> CompileError { + CompileError { + line_num: self.line_num, + line_pos: self.line_pos, + error_type, + } + } +} + +impl Display for CompileError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.error_type() { + ErrorType::InvalidCharacter(value) => { + write!(f, "Invalid character {:?}", char::from(*value)) + } + ErrorType::InvalidNumber(value) => write!(f, "Invalid number {value:?}"), + ErrorType::InvalidMatchVariable(value) => { + write!(f, "Match variable {value} out of range") + } + ErrorType::InvalidUnicodeSequence(value) => { + write!(f, "Invalid Unicode sequence {value:04x}") + } + ErrorType::InvalidNamespace(value) => write!(f, "Invalid namespace {value:?}"), + ErrorType::InvalidRegex(value) => write!(f, "Invalid regular expression {value:?}"), + ErrorType::InvalidExpression(value) => write!(f, "Invalid expression {value}"), + ErrorType::InvalidUtf8String => write!(f, "Invalid UTF-8 string"), + ErrorType::InvalidHeaderName => write!(f, "Invalid header name"), + ErrorType::InvalidArguments => write!(f, "Invalid Arguments"), + ErrorType::InvalidAddress => write!(f, "Invalid Address"), + ErrorType::InvalidURI => write!(f, "Invalid URI"), + ErrorType::InvalidEnvelope(value) => write!(f, "Invalid envelope {value:?}"), + ErrorType::UnterminatedString => write!(f, "Unterminated string"), + ErrorType::UnterminatedComment => write!(f, "Unterminated comment"), + ErrorType::UnterminatedMultiline => write!(f, "Unterminated multi-line string"), + ErrorType::UnterminatedBlock => write!(f, "Unterminated block"), + ErrorType::ScriptTooLong => write!(f, "Sieve script is too large"), + ErrorType::StringTooLong => write!(f, "String is too long"), + ErrorType::VariableTooLong => write!(f, "Variable name is too long"), + ErrorType::VariableIsLocal(value) => { + write!(f, "Variable {value:?} was already defined as local") + } + ErrorType::HeaderTooLong => write!(f, "Header value is too long"), + ErrorType::ExpectedConstantString => write!(f, "Expected a constant string"), + ErrorType::UnexpectedToken { expected, found } => { + write!(f, "Expected token {expected:?} but found {found:?}") + } + ErrorType::UnexpectedEOF => write!(f, "Unexpected end of file"), + ErrorType::TooManyNestedBlocks => write!(f, "Too many nested blocks"), + ErrorType::TooManyNestedTests => write!(f, "Too many nested tests"), + ErrorType::TooManyNestedForEveryParts => { + write!(f, "Too many nested foreverypart blocks") + } + ErrorType::TooManyIncludes => write!(f, "Too many includes"), + ErrorType::LabelAlreadyDefined(value) => write!(f, "Label {value:?} already defined"), + ErrorType::LabelUndefined(value) => write!(f, "Label {value:?} does not exist"), + ErrorType::BreakOutsideLoop => write!(f, "Break used outside of foreverypart loop"), + ErrorType::UnsupportedComparator(value) => { + write!(f, "Comparator {value:?} is not supported") + } + ErrorType::DuplicatedParameter => write!(f, "Duplicated argument"), + ErrorType::UndeclaredCapability(value) => { + write!(f, "Undeclared capability '{value}'") + } + ErrorType::MissingTag(value) => write!(f, "Missing tag {value:?}"), + }?; + + write!( + f, + " at line {}, column {}.", + self.line_num(), + self.line_pos() + ) + } +} + +impl Display for RuntimeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RuntimeError::TooManyIncludes => write!(f, ""), + RuntimeError::InvalidInstruction(value) => write!( + f, + "Script executed invalid instruction {:?} at line {}, column {}.", + value.name(), + value.line_pos(), + value.line_num() + ), + RuntimeError::ScriptErrorMessage(value) => { + write!(f, "Script reported error {value:?}.") + } + RuntimeError::CapabilityNotAllowed(value) => { + write!(f, "Capability '{value}' has been disabled.") + } + RuntimeError::CapabilityNotSupported(value) => { + write!(f, "Capability '{value}' not supported.") + } + RuntimeError::CPULimitReached => write!( + f, + "Script exceeded the maximum number of instructions allowed to execute." + ), + } + } +} + +#[cfg(test)] +mod tests { + use std::{fs, path::PathBuf}; + + use crate::sieve::Compiler; + + #[test] + fn parse_rfc() { + let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + test_dir.push("tests"); + test_dir.push("rfcs"); + let mut tests_run = 0; + + let mut compiler = Compiler::new().with_max_nested_foreverypart(10); + + compiler + .register_plugin("plugin1") + .with_tag("tag1") + .with_tag("tag2") + .with_tagged_string_argument("string_arg") + .with_number_argument() + .with_string_array_argument(); + + compiler + .register_plugin("plugin2") + .with_tagged_number_array_argument("array_arg") + .with_regex_array_argument(); + + for file_name in fs::read_dir(&test_dir).unwrap() { + let mut file_name = file_name.unwrap().path(); + if file_name.extension().map_or(false, |e| e == "sieve") { + println!("Parsing {}", file_name.display()); + + /*if !file_name + .file_name() + .unwrap() + .to_str() + .unwrap() + .contains("plugins") + { + let test = "true"; + continue; + }*/ + + let script = fs::read(&file_name).unwrap(); + file_name.set_extension("json"); + let expected_result = fs::read(&file_name).unwrap(); + + tests_run += 1; + + let sieve = compiler.compile(&script).unwrap(); + let json_sieve = serde_json::to_string_pretty( + &sieve + .instructions + .into_iter() + .enumerate() + .collect::>(), + ) + .unwrap(); + + if json_sieve.as_bytes() != expected_result { + file_name.set_extension("failed"); + fs::write(&file_name, json_sieve.as_bytes()).unwrap(); + panic!("Test failed, parsed sieve saved to {}", file_name.display()); + } + } + } + + assert!( + tests_run > 0, + "Did not find any tests to run in folder {}.", + test_dir.display() + ); + } +} diff --git a/melib/src/sieve/examples/filter.rs b/melib/src/sieve/examples/filter.rs new file mode 100644 index 00000000..a556565d --- /dev/null +++ b/melib/src/sieve/examples/filter.rs @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use sieve::{runtime::RuntimeError, Compiler, Event, Input, Runtime}; + +fn main() { + let text_script = br#" + require ["fileinto", "body", "imap4flags"]; + + if body :contains "tps" { + setflag "$tps_reports"; + } + + if header :matches "List-ID" "*<*@*" { + fileinto "INBOX.lists.${2}"; stop; + } + "#; + let raw_message = r#"From: Sales Mailing List +To: John Doe +List-ID: +Subject: TPS Reports + +We're putting new coversheets on all the TPS reports before they go out now. +So if you could go ahead and try to remember to do that from now on, that'd be great. All right! +"#; + + // Compile + let compiler = Compiler::new(); + let script = compiler.compile(text_script).unwrap(); + + // Build runtime + let runtime = Runtime::new(); + + // Create filter instance + let mut instance = runtime.filter(raw_message.as_bytes()); + let mut input = Input::script("my-script", script); + let mut messages: Vec = Vec::new(); + + // Start event loop + while let Some(result) = instance.run(input) { + match result { + Ok(event) => match event { + Event::IncludeScript { name, optional } => { + // NOTE: Just for demonstration purposes, script name needs to be validated first. + if let Ok(bytes) = std::fs::read(name.as_str()) { + let script = compiler.compile(&bytes).unwrap(); + input = Input::script(name, script); + } else if optional { + input = Input::False; + } else { + panic!("Script {name} not found."); + } + } + Event::MailboxExists { .. } => { + // Set to true if the mailbox exists + input = false.into(); + } + Event::ListContains { .. } => { + // Set to true if the list(s) contains an entry + input = false.into(); + } + Event::DuplicateId { .. } => { + // Set to true if the ID is duplicate + input = false.into(); + } + Event::Plugin { id, arguments } => { + println!("Script executed plugin {id} with parameters {arguments:?}"); + // Set to true if the script succeeded + input = false.into(); + } + Event::SetEnvelope { envelope, value } => { + println!("Set envelope {envelope:?} to {value:?}"); + input = true.into(); + } + + Event::Keep { flags, message_id } => { + println!( + "Keep message '{}' with flags {:?}.", + if message_id > 0 { + messages[message_id - 1].as_str() + } else { + raw_message + }, + flags + ); + input = true.into(); + } + Event::Discard => { + println!("Discard message."); + input = true.into(); + } + Event::Reject { reason, .. } => { + println!("Reject message with reason {reason:?}."); + input = true.into(); + } + Event::FileInto { + folder, + flags, + message_id, + .. + } => { + println!( + "File message '{}' in folder {:?} with flags {:?}.", + if message_id > 0 { + messages[message_id - 1].as_str() + } else { + raw_message + }, + folder, + flags + ); + input = true.into(); + } + Event::SendMessage { + recipient, + message_id, + .. + } => { + println!( + "Send message '{}' to {:?}.", + if message_id > 0 { + messages[message_id - 1].as_str() + } else { + raw_message + }, + recipient + ); + input = true.into(); + } + Event::Notify { + message, method, .. + } => { + println!("Notify URI {method:?} with message {message:?}"); + input = true.into(); + } + Event::CreatedMessage { message, .. } => { + messages.push(String::from_utf8(message).unwrap()); + input = true.into(); + } + + #[cfg(test)] + _ => unreachable!(), + }, + Err(error) => { + match error { + RuntimeError::TooManyIncludes => { + eprintln!("Too many included scripts."); + } + RuntimeError::InvalidInstruction(instruction) => { + eprintln!( + "Invalid instruction {:?} found at {}:{}.", + instruction.name(), + instruction.line_num(), + instruction.line_pos() + ); + } + RuntimeError::ScriptErrorMessage(message) => { + eprintln!("Script called the 'error' function with {message:?}"); + } + RuntimeError::CapabilityNotAllowed(capability) => { + eprintln!( + "Capability {capability:?} has been disabled by the administrator.", + ); + } + RuntimeError::CapabilityNotSupported(capability) => { + eprintln!("Capability {capability:?} not supported."); + } + RuntimeError::CPULimitReached => { + eprintln!("Script exceeded the configured CPU limit."); + } + } + input = true.into(); + } + } + } +} diff --git a/melib/src/sieve/examples/serialize.rs b/melib/src/sieve/examples/serialize.rs new file mode 100644 index 00000000..fdd34c67 --- /dev/null +++ b/melib/src/sieve/examples/serialize.rs @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use sieve::{Compiler, Sieve}; + +fn main() { + let script = br#"if header :matches \"List-ID\" \"*<*@*\" { + fileinto \"INBOX.lists.${2}\"; stop; + }"#; + + // Compile + let compiled_script = Compiler::new().compile(script).unwrap(); + + // Serialize + let serialized_script = compiled_script.serialize().unwrap(); + + // Deserialize + let deserialized_script = Sieve::deserialize(&serialized_script).unwrap(); + + assert_eq!(compiled_script, deserialized_script); +} diff --git a/melib/src/sieve.rs b/melib/src/sieve/managesieve.rs similarity index 99% rename from melib/src/sieve.rs rename to melib/src/sieve/managesieve.rs index 356bb907..b8dff50f 100644 --- a/melib/src/sieve.rs +++ b/melib/src/sieve/managesieve.rs @@ -24,6 +24,10 @@ use crate::utils::parsec::*; #[derive(Debug, Clone, PartialEq, Eq)] pub struct RuleBlock(pub Vec); +pub mod compiler; +pub mod lib; +pub mod runtime; + /* MATCH-TYPE =/ COUNT / VALUE diff --git a/melib/src/sieve/mod.rs b/melib/src/sieve/mod.rs new file mode 100644 index 00000000..db71a102 --- /dev/null +++ b/melib/src/sieve/mod.rs @@ -0,0 +1,1202 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +//! # sieve +//! +//! [![crates.io](https://img.shields.io/crates/v/sieve-rs)](https://crates.io/crates/sieve-rs) +//! [![build](https://github.com/stalwartlabs/sieve/actions/workflows/rust.yml/badge.svg)](https://github.com/stalwartlabs/sieve/actions/workflows/rust.yml) +//! [![docs.rs](https://img.shields.io/docsrs/sieve-rs)](https://docs.rs/sieve-rs) +//! [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) +//! +//! _sieve_ is a fast and secure Sieve filter interpreter for Rust that supports all [registered Sieve extensions](https://www.iana.org/assignments/sieve-extensions/sieve-extensions.xhtml). +//! +//! ## Usage Example +//! +//! ```rust +//! use sieve::{runtime::RuntimeError, Compiler, Event, Input, Runtime}; +//! +//! let text_script = br#" +//! require ["fileinto", "body", "imap4flags"]; +//! +//! if body :contains "tps" { +//! setflag "$tps_reports"; +//! } +//! +//! if header :matches "List-ID" "*<*@*" { +//! fileinto "INBOX.lists.${2}"; stop; +//! } +//! "#; +//! let raw_message = r#"From: Sales Mailing List +//! To: John Doe +//! List-ID: +//! Subject: TPS Reports +//! +//! We're putting new coversheets on all the TPS reports before they go out now. +//! So if you could go ahead and try to remember to do that from now on, that'd be great. All right! +//! "#; +//! +//! // Compile +//! let compiler = Compiler::new(); +//! let script = compiler.compile(text_script).unwrap(); +//! +//! // Build runtime +//! let runtime = Runtime::new(); +//! +//! // Create filter instance +//! let mut instance = runtime.filter(raw_message.as_bytes()); +//! let mut input = Input::script("my-script", script); +//! let mut messages: Vec = Vec::new(); +//! +//! // Start event loop +//! while let Some(result) = instance.run(input) { +//! match result { +//! Ok(event) => match event { +//! Event::IncludeScript { name, optional } => { +//! // NOTE: Just for demonstration purposes, script name needs to be validated first. +//! if let Ok(bytes) = std::fs::read(name.as_str()) { +//! let script = compiler.compile(&bytes).unwrap(); +//! input = Input::script(name, script); +//! } else if optional { +//! input = Input::False; +//! } else { +//! panic!("Script {} not found.", name); +//! } +//! } +//! Event::MailboxExists { .. } => { +//! // Set to true if the mailbox exists +//! input = false.into(); +//! } +//! Event::ListContains { .. } => { +//! // Set to true if the list(s) contains an entry +//! input = false.into(); +//! } +//! Event::DuplicateId { .. } => { +//! // Set to true if the ID is duplicate +//! input = false.into(); +//! } +//! Event::Plugin { id, arguments } => { +//! println!("Script executed plugin {id} with parameters {arguments:?}"); +//! // Set to true if the script succeeded +//! input = false.into(); +//! } +//! Event::SetEnvelope { envelope, value } => { +//! println!("Set envelope {envelope:?} to {value:?}"); +//! input = true.into(); +//! } +//! +//! Event::Keep { flags, message_id } => { +//! println!( +//! "Keep message '{}' with flags {:?}.", +//! if message_id > 0 { +//! messages[message_id - 1].as_str() +//! } else { +//! raw_message +//! }, +//! flags +//! ); +//! input = true.into(); +//! } +//! Event::Discard => { +//! println!("Discard message."); +//! input = true.into(); +//! } +//! Event::Reject { reason, .. } => { +//! println!("Reject message with reason {:?}.", reason); +//! input = true.into(); +//! } +//! Event::FileInto { +//! folder, +//! flags, +//! message_id, +//! .. +//! } => { +//! println!( +//! "File message '{}' in folder {:?} with flags {:?}.", +//! if message_id > 0 { +//! messages[message_id - 1].as_str() +//! } else { +//! raw_message +//! }, +//! folder, +//! flags +//! ); +//! input = true.into(); +//! } +//! Event::SendMessage { +//! recipient, +//! message_id, +//! .. +//! } => { +//! println!( +//! "Send message '{}' to {:?}.", +//! if message_id > 0 { +//! messages[message_id - 1].as_str() +//! } else { +//! raw_message +//! }, +//! recipient +//! ); +//! input = true.into(); +//! } +//! Event::Notify { +//! message, method, .. +//! } => { +//! println!("Notify URI {:?} with message {:?}", method, message); +//! input = true.into(); +//! } +//! Event::CreatedMessage { message, .. } => { +//! messages.push(String::from_utf8(message).unwrap()); +//! input = true.into(); +//! } +//! +//! #[cfg(test)] +//! _ => unreachable!(), +//! }, +//! Err(error) => { +//! match error { +//! RuntimeError::TooManyIncludes => { +//! eprintln!("Too many included scripts."); +//! } +//! RuntimeError::InvalidInstruction(instruction) => { +//! eprintln!( +//! "Invalid instruction {:?} found at {}:{}.", +//! instruction.name(), +//! instruction.line_num(), +//! instruction.line_pos() +//! ); +//! } +//! RuntimeError::ScriptErrorMessage(message) => { +//! eprintln!("Script called the 'error' function with {:?}", message); +//! } +//! RuntimeError::CapabilityNotAllowed(capability) => { +//! eprintln!( +//! "Capability {:?} has been disabled by the administrator.", +//! capability +//! ); +//! } +//! RuntimeError::CapabilityNotSupported(capability) => { +//! eprintln!("Capability {:?} not supported.", capability); +//! } +//! RuntimeError::CPULimitReached => { +//! eprintln!("Script exceeded the configured CPU limit."); +//! } +//! } +//! input = true.into(); +//! } +//! } +//! } +//! ``` +//! +//! ## Testing and Fuzzing +//! +//! To run the testsuite: +//! +//! ```bash +//! $ cargo test --all-features +//! ``` +//! +//! To fuzz the library with `cargo-fuzz`: +//! +//! ```bash +//! $ cargo +nightly fuzz run mail_parser +//! ``` +//! +//! ## Conformed RFCs +//! +//! - [RFC 5228 - Sieve: An Email Filtering Language](https://datatracker.ietf.org/doc/html/rfc5228) +//! - [RFC 3894 - Copying Without Side Effects](https://datatracker.ietf.org/doc/html/rfc3894) +//! - [RFC 5173 - Body Extension](https://datatracker.ietf.org/doc/html/rfc5173) +//! - [RFC 5183 - Environment Extension](https://datatracker.ietf.org/doc/html/rfc5183) +//! - [RFC 5229 - Variables Extension](https://datatracker.ietf.org/doc/html/rfc5229) +//! - [RFC 5230 - Vacation Extension](https://datatracker.ietf.org/doc/html/rfc5230) +//! - [RFC 5231 - Relational Extension](https://datatracker.ietf.org/doc/html/rfc5231) +//! - [RFC 5232 - Imap4flags Extension](https://datatracker.ietf.org/doc/html/rfc5232) +//! - [RFC 5233 - Subaddress Extension](https://datatracker.ietf.org/doc/html/rfc5233) +//! - [RFC 5235 - Spamtest and Virustest Extensions](https://datatracker.ietf.org/doc/html/rfc5235) +//! - [RFC 5260 - Date and Index Extensions](https://datatracker.ietf.org/doc/html/rfc5260) +//! - [RFC 5293 - Editheader Extension](https://datatracker.ietf.org/doc/html/rfc5293) +//! - [RFC 5429 - Reject and Extended Reject Extensions](https://datatracker.ietf.org/doc/html/rfc5429) +//! - [RFC 5435 - Extension for Notifications](https://datatracker.ietf.org/doc/html/rfc5435) +//! - [RFC 5463 - Ihave Extension](https://datatracker.ietf.org/doc/html/rfc5463) +//! - [RFC 5490 - Extensions for Checking Mailbox Status and Accessing Mailbox Metadata](https://datatracker.ietf.org/doc/html/rfc5490) +//! - [RFC 5703 - MIME Part Tests, Iteration, Extraction, Replacement, and Enclosure](https://datatracker.ietf.org/doc/html/rfc5703) +//! - [RFC 6009 - Delivery Status Notifications and Deliver-By Extensions](https://datatracker.ietf.org/doc/html/rfc6009) +//! - [RFC 6131 - Sieve Vacation Extension: "Seconds" Parameter](https://datatracker.ietf.org/doc/html/rfc6131) +//! - [RFC 6134 - Externally Stored Lists](https://datatracker.ietf.org/doc/html/rfc6134) +//! - [RFC 6558 - Converting Messages before Delivery](https://datatracker.ietf.org/doc/html/rfc6558) +//! - [RFC 6609 - Include Extension](https://datatracker.ietf.org/doc/html/rfc6609) +//! - [RFC 7352 - Detecting Duplicate Deliveries](https://datatracker.ietf.org/doc/html/rfc7352) +//! - [RFC 8579 - Delivering to Special-Use Mailboxes](https://datatracker.ietf.org/doc/html/rfc8579) +//! - [RFC 8580 - File Carbon Copy (FCC)](https://datatracker.ietf.org/doc/html/rfc8580) +//! - [RFC 9042 - Delivery by MAILBOXID](https://datatracker.ietf.org/doc/html/rfc9042) +//! - [REGEX-01 - Regular Expression Extension (draft-ietf-sieve-regex-01)](https://www.ietf.org/archive/id/draft-ietf-sieve-regex-01.html) +//! +//! ## License +//! +//! Licensed under the terms of the [GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.en.html) as published by +//! the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +//! See [LICENSE](LICENSE) for more details. +//! +//! You can be released from the requirements of the AGPLv3 license by purchasing +//! a commercial license. Please contact licensing@stalw.art for more details. +//! +//! ## Copyright +//! +//! Copyright (C) 2020-2023, Stalwart Labs Ltd. +//! + +use std::{borrow::Cow, iter::Enumerate, sync::Arc, vec::IntoIter}; + +pub mod compiler; +pub mod runtime; +use self::compiler::{ + grammar::{ + actions::action_redirect::{ByTime, Notify, Ret}, + instruction::Instruction, + Capability, + }, + Number, Regex, VariableType, +}; +use self::runtime::{context::ScriptStack, Variable}; +use mail_parser::{HeaderName, Message}; + +use ahash::{AHashMap, AHashSet}; +use serde::{Deserialize, Serialize}; + +pub(crate) const MAX_MATCH_VARIABLES: usize = 63; +pub(crate) const MAX_LOCAL_VARIABLES: usize = 256; + +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub struct Sieve { + instructions: Vec, + num_vars: usize, + num_match_vars: usize, +} + +pub struct Compiler { + // Settings + pub(crate) max_script_size: usize, + pub(crate) max_string_size: usize, + pub(crate) max_variable_name_size: usize, + pub(crate) max_nested_blocks: usize, + pub(crate) max_nested_tests: usize, + pub(crate) max_nested_foreverypart: usize, + pub(crate) max_match_variables: usize, + pub(crate) max_local_variables: usize, + pub(crate) max_header_size: usize, + pub(crate) max_includes: usize, + + // Plugins + pub(crate) plugins: AHashMap, + pub(crate) functions: AHashMap, +} + +pub type Function = for<'x> fn(&'x Context<'x>, Vec>) -> Variable<'x>; + +#[derive(Default, Clone)] +pub struct FunctionMap { + pub(crate) map: AHashMap, + pub(crate) functions: Vec, +} + +#[derive(Debug, Clone)] +pub struct Runtime { + pub(crate) allowed_capabilities: AHashSet, + pub(crate) valid_notification_uris: AHashSet>, + pub(crate) valid_ext_lists: AHashSet>, + pub(crate) protected_headers: Vec>, + pub(crate) environment: AHashMap, Variable<'static>>, + pub(crate) metadata: Vec<(Metadata, Cow<'static, str>)>, + pub(crate) include_scripts: AHashMap>, + pub(crate) local_hostname: Cow<'static, str>, + pub(crate) functions: Vec, + + pub(crate) max_nested_includes: usize, + pub(crate) cpu_limit: usize, + pub(crate) max_variable_size: usize, + pub(crate) max_redirects: usize, + pub(crate) max_received_headers: usize, + pub(crate) max_header_size: usize, + pub(crate) max_out_messages: usize, + + pub(crate) default_vacation_expiry: u64, + pub(crate) default_duplicate_expiry: u64, + + pub(crate) vacation_use_orig_rcpt: bool, + pub(crate) vacation_default_subject: Cow<'static, str>, + pub(crate) vacation_subject_prefix: Cow<'static, str>, +} + +#[derive(Clone, Debug)] +pub struct Context<'x> { + #[cfg(test)] + pub(crate) runtime: Runtime, + #[cfg(not(test))] + pub(crate) runtime: &'x Runtime, + pub(crate) user_address: Cow<'x, str>, + pub(crate) user_full_name: Cow<'x, str>, + pub(crate) current_time: i64, + + pub(crate) message: Message<'x>, + pub(crate) message_size: usize, + pub(crate) envelope: Vec<(Envelope, Variable<'x>)>, + pub(crate) metadata: Vec<(Metadata, Cow<'x, str>)>, + + pub(crate) part: usize, + pub(crate) part_iter: IntoIter, + pub(crate) part_iter_stack: Vec<(usize, IntoIter)>, + + pub(crate) line_iter: Enumerate>>, + + pub(crate) spam_status: SpamStatus, + pub(crate) virus_status: VirusStatus, + + pub(crate) pos: usize, + pub(crate) test_result: bool, + pub(crate) script_cache: AHashMap>, + pub(crate) script_stack: Vec, + pub(crate) vars_global: AHashMap, Variable<'static>>, + pub(crate) vars_env: AHashMap, Variable<'x>>, + pub(crate) vars_local: Vec>, + pub(crate) vars_match: Vec>, + + pub(crate) queued_events: IntoIter, + pub(crate) final_event: Option, + pub(crate) last_message_id: usize, + pub(crate) main_message_id: usize, + + pub(crate) has_changes: bool, + pub(crate) num_redirects: usize, + pub(crate) num_instructions: usize, + pub(crate) num_out_messages: usize, +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum Script { + Personal(String), + Global(String), +} + +pub struct PluginSchema { + pub id: ExternalId, + pub tags: AHashMap, + pub arguments: Vec, +} + +pub enum PluginSchemaArgument { + Text, + Number, + Regex, + Variable, + Array(Box), +} + +pub struct PluginSchemaTag { + pub id: ExternalId, + pub argument: Option, +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub enum Envelope { + From, + To, + ByTimeAbsolute, + ByTimeRelative, + ByMode, + ByTrace, + Notify, + Orcpt, + Ret, + Envid, +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub enum Metadata { + Server { annotation: T }, + Mailbox { name: T, annotation: T }, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum Event { + IncludeScript { + name: Script, + optional: bool, + }, + MailboxExists { + mailboxes: Vec, + special_use: Vec, + }, + ListContains { + lists: Vec, + values: Vec, + match_as: MatchAs, + }, + DuplicateId { + id: String, + expiry: u64, + last: bool, + }, + Plugin { + id: ExternalId, + arguments: Vec>, + }, + SetEnvelope { + envelope: Envelope, + value: String, + }, + + // Actions + Keep { + flags: Vec, + message_id: usize, + }, + Discard, + Reject { + extended: bool, + reason: String, + }, + FileInto { + folder: String, + flags: Vec, + mailbox_id: Option, + special_use: Option, + create: bool, + message_id: usize, + }, + SendMessage { + recipient: Recipient, + notify: Notify, + return_of_content: Ret, + by_time: ByTime, + message_id: usize, + }, + Notify { + from: Option, + importance: Importance, + options: Vec, + message: String, + method: String, + }, + CreatedMessage { + message_id: usize, + message: Vec, + }, +} + +pub type ExternalId = u32; + +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +pub enum PluginArgument { + Tag(ExternalId), + Text(T), + Number(N), + Regex(Regex), + Variable(VariableType), + Array(Vec>), +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +pub(crate) struct FileCarbonCopy { + pub mailbox: T, + pub mailbox_id: Option, + pub create: bool, + pub flags: Vec, + pub special_use: Option, +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum Importance { + High, + Normal, + Low, +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum MatchAs { + Octet, + Lowercase, + Number, +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum Recipient { + Address(String), + List(String), + Group(Vec), +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum Input { + True, + False, + Script { name: Script, script: Arc }, + Variables { list: Vec }, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct SetVariable { + pub name: VariableType, + pub value: Variable<'static>, +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum Mailbox { + Name(String), + Id(String), +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum SpamStatus { + Unknown, + Ham, + MaybeSpam(f64), + Spam, +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum VirusStatus { + Unknown, + Clean, + Replaced, + Cured, + MaybeVirus, + Virus, +} + +#[cfg(test)] +mod tests { + use std::{ + fs, + path::{Path, PathBuf}, + }; + + use ahash::{AHashMap, AHashSet}; + use mail_parser::{ + parsers::MessageStream, Encoding, HeaderValue, Message, MessageParser, MessagePart, + PartType, + }; + + use crate::sieve::{ + compiler::grammar::Capability, runtime::actions::action_mime::reset_test_boundary, + Compiler, Envelope, Event, FunctionMap, Input, Mailbox, PluginArgument, Recipient, Runtime, + SpamStatus, VirusStatus, + }; + + #[test] + fn test_suite() { + let mut tests = Vec::new(); + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("tests"); + + read_dir(path, &mut tests); + + for test in tests { + /*if !test + .file_name() + .unwrap() + .to_str() + .unwrap() + .contains("expressions") + { + continue; + }*/ + println!("===== {} =====", test.display()); + run_test(&test); + } + } + + fn read_dir(path: PathBuf, files: &mut Vec) { + for entry in fs::read_dir(path).unwrap() { + let entry = entry.unwrap().path(); + if entry.is_dir() { + read_dir(entry, files); + } else if entry + .extension() + .and_then(|e| e.to_str()) + .unwrap_or("") + .eq("svtest") + { + files.push(entry); + } + } + } + + fn run_test(script_path: &Path) { + let mut fnc_map = FunctionMap::new() + .with_function("trim", |_, v| match v.into_iter().next().unwrap() { + super::super::runtime::Variable::String(s) => s.trim().to_string().into(), + super::super::runtime::Variable::StringRef(s) => s.trim().into(), + v => v.to_string().into(), + }) + .with_function("len", |_, v| v[0].to_cow().len().into()) + .with_function("to_lowercase", |_, v| { + v[0].to_cow().to_lowercase().to_string().into() + }) + .with_function("to_uppercase", |_, v| { + v[0].to_cow().to_uppercase().to_string().into() + }) + .with_function("is_uppercase", |_, v| { + v[0].to_cow() + .as_ref() + .chars() + .filter(|c| c.is_alphabetic()) + .all(|c| c.is_uppercase()) + .into() + }) + .with_function("is_ascii", |_, v| { + v[0].to_cow().as_ref().chars().any(|c| !c.is_ascii()).into() + }) + .with_function("char_count", |_, v| { + v[0].to_cow().as_ref().chars().count().into() + }) + .with_function_args( + "contains", + |_, v| v[0].to_string().contains(&v[1].to_string()).into(), + 2, + ) + .with_function_args( + "eq_lowercase", + |_, v| { + v[0].to_cow() + .as_ref() + .eq_ignore_ascii_case(v[1].to_cow().as_ref()) + .into() + }, + 2, + ) + .with_function_args( + "concat_three", + |_, v| format!("{}-{}-{}", v[0], v[1], v[2]).into(), + 3, + ); + let mut compiler = Compiler::new() + .with_max_string_size(10240) + .register_functions(&mut fnc_map); + + // Register extensions + compiler + .register_plugin("execute") + .with_tag("query") + .with_tag("binary") + .with_string_argument() + .with_string_array_argument(); + + let mut ancestors = script_path.ancestors(); + ancestors.next(); + let base_path = ancestors.next().unwrap(); + let script = compiler + .compile(&add_crlf(&fs::read(script_path).unwrap())) + .unwrap(); + + let mut input = Input::script("", script); + let mut current_test = String::new(); + let mut raw_message_: Option> = None; + let mut prev_state = None; + let mut mailboxes = Vec::new(); + let mut lists: AHashMap> = AHashMap::new(); + let mut duplicated_ids = AHashSet::new(); + let mut actions = Vec::new(); + + 'outer: loop { + let runtime = Runtime::new() + .with_protected_header("Auto-Submitted") + .with_protected_header("Received") + .with_valid_notification_uri("mailto") + .with_max_out_messages(100) + .with_capability(Capability::Plugins) + .with_capability(Capability::ForEveryLine) + .with_capability(Capability::Eval) + .with_functions(&mut fnc_map.clone()); + let mut instance = runtime.filter(b""); + let raw_message = raw_message_.take().unwrap_or_default(); + instance.message = + MessageParser::new() + .parse(&raw_message) + .unwrap_or_else(|| Message { + html_body: vec![], + text_body: vec![], + attachments: vec![], + parts: vec![MessagePart { + headers: vec![], + is_encoding_problem: false, + body: PartType::Text("".into()), + encoding: Encoding::None, + offset_header: 0, + offset_body: 0, + offset_end: 0, + }], + raw_message: b""[..].into(), + }); + instance.message_size = raw_message.len(); + if let Some((pos, script_cache, script_stack, vars_global, vars_local, vars_match)) = + prev_state.take() + { + instance.pos = pos; + instance.script_cache = script_cache; + instance.script_stack = script_stack; + instance.vars_global = vars_global; + instance.vars_local = vars_local; + instance.vars_match = vars_match; + } + instance.set_env_variable("vnd.stalwart.default_mailbox", "INBOX"); + instance.set_env_variable("vnd.stalwart.username", "john.doe"); + instance.set_user_address("MAILER-DAEMON"); + if let Some(addr) = instance + .message + .from() + .and_then(|a| a.first()) + .and_then(|a| a.address.as_ref()) + { + instance.set_envelope(Envelope::From, addr.to_string()); + } + if let Some(addr) = instance + .message + .to() + .and_then(|a| a.first()) + .and_then(|a| a.address.as_ref()) + { + instance.set_envelope(Envelope::To, addr.to_string()); + } + + while let Some(event) = instance.run(input) { + match event.unwrap() { + Event::IncludeScript { name, optional } => { + let mut include_path = PathBuf::from(base_path); + include_path.push(if matches!(name, super::super::Script::Personal(_)) { + "included" + } else { + "included-global" + }); + include_path.push(format!("{name}.sieve")); + + if let Ok(bytes) = fs::read(include_path.as_path()) { + let script = compiler.compile(&add_crlf(&bytes)).unwrap(); + input = Input::script(name, script); + } else if optional { + input = Input::False; + } else { + panic!("Script {} not found.", include_path.display()); + } + } + Event::MailboxExists { + mailboxes: mailboxes_, + special_use, + } => { + for action in &actions { + if let Event::FileInto { folder, create, .. } = action { + if *create && !mailboxes.contains(folder) { + mailboxes.push(folder.to_string()); + } + } + } + input = (special_use.is_empty() + && mailboxes_.iter().all(|n| { + if let Mailbox::Name(n) = n { + mailboxes.contains(n) + } else { + false + } + })) + .into(); + } + Event::ListContains { + lists: lists_, + values, + .. + } => { + let mut result = false; + 'list: for list in &lists_ { + if let Some(list) = lists.get(list) { + for value in &values { + if list.contains(value) { + result = true; + break 'list; + } + } + } + } + + input = result.into(); + } + Event::DuplicateId { id, .. } => { + input = duplicated_ids.contains(&id).into(); + } + Event::Plugin { id, arguments } => { + if id == u32::MAX { + // Test functions + input = Input::True; + let mut arguments = arguments.into_iter(); + let command = arguments.next().unwrap().unwrap_string().unwrap(); + let mut params = arguments + .map(|arg| arg.unwrap_string().unwrap()) + .collect::>(); + + match command.as_str() { + "test" => { + current_test = params.pop().unwrap(); + println!("Running test '{current_test}'..."); + } + "test_set" => { + let mut params = params.into_iter(); + let target = params.next().expect("test_set parameter"); + if target == "message" { + let value = params.next().unwrap(); + raw_message_ = if value.eq_ignore_ascii_case(":smtp") { + let mut message = None; + for action in actions.iter().rev() { + if let Event::SendMessage { message_id, .. } = + action + { + let message_ = actions + .iter() + .find_map(|item| { + if let Event::CreatedMessage { + message_id: message_id_, + message, + } = item + { + if message_id == message_id_ { + return Some(message); + } + } + None + }) + .unwrap(); + /*println!( + "<[{}]>", + std::str::from_utf8(message_).unwrap() + );*/ + message = message_.into(); + break; + } + } + message.expect("No SMTP message found").to_vec().into() + } else { + value.into_bytes().into() + }; + prev_state = ( + instance.pos, + instance.script_cache, + instance.script_stack, + instance.vars_global, + instance.vars_local, + instance.vars_match, + ) + .into(); + + continue 'outer; + } else if let Some(envelope) = target.strip_prefix("envelope.") + { + let envelope = + Envelope::try_from(envelope.to_string()).unwrap(); + instance.envelope.retain(|(e, _)| e != &envelope); + instance.set_envelope(envelope, params.next().unwrap()); + } else if target == "currentdate" { + let bytes = params.next().unwrap().into_bytes(); + if let HeaderValue::DateTime(dt) = + MessageStream::new(&bytes).parse_date() + { + instance.current_time = dt.to_timestamp(); + } else { + panic!("Invalid currentdate"); + } + } else { + panic!("test_set {target} not implemented."); + } + } + "test_message" => { + let mut params = params.into_iter(); + input = match params.next().unwrap().as_str() { + ":folder" => { + let folder_name = params.next().expect("test_message folder name"); + matches!(&instance.final_event, Some(Event::Keep { .. })) || + actions.iter().any(|a| if !folder_name.eq_ignore_ascii_case("INBOX") { + matches!(a, Event::FileInto { folder, .. } if folder == &folder_name ) + } else { + matches!(a, Event::Keep { .. }) + }) + } + ":smtp" => { + actions.iter().any(|a| matches!(a, Event::SendMessage { .. } )) + } + param => panic!("Invalid test_message param '{param}'" ), + }.into(); + } + "test_assert_message" => { + let expected_message = + params.first().expect("test_set parameter"); + let built_message = instance.build_message(); + if expected_message.as_bytes() != built_message { + //fs::write("_deleteme.json", serde_json::to_string_pretty(&Message::parse(&built_message).unwrap()).unwrap()).unwrap(); + print!("<["); + print!("{}", String::from_utf8(built_message).unwrap()); + println!("]>"); + panic!("Message built incorrectly at '{current_test}'"); + } + } + "test_config_set" => { + let mut params = params.into_iter(); + let name = params.next().unwrap(); + let value = params.next().expect("test_config_set value"); + + match name.as_str() { + "sieve_editheader_protected" + | "sieve_editheader_forbid_add" + | "sieve_editheader_forbid_delete" => { + if !value.is_empty() { + for header_name in value.split(' ') { + instance.runtime.set_protected_header( + header_name.to_string(), + ); + } + } else { + instance.runtime.protected_headers.clear(); + } + } + "sieve_variables_max_variable_size" => { + instance + .runtime + .set_max_variable_size(value.parse().unwrap()); + } + "sieve_valid_ext_list" => { + instance.runtime.set_valid_ext_list(value); + } + "sieve_ext_list_item" => { + lists + .entry(value) + .or_insert_with(AHashSet::new) + .insert(params.next().expect("list item value")); + } + "sieve_duplicated_id" => { + duplicated_ids.insert(value); + } + "sieve_user_email" => { + instance.set_user_address(value); + } + "sieve_vacation_use_original_recipient" => { + instance.runtime.set_vacation_use_orig_rcpt( + value.eq_ignore_ascii_case("yes"), + ); + } + "sieve_vacation_default_subject" => { + instance.runtime.set_vacation_default_subject(value); + } + "sieve_vacation_default_subject_template" => { + instance.runtime.set_vacation_subject_prefix(value); + } + "sieve_spam_status" => { + instance.set_spam_status(SpamStatus::from_number( + value.parse().unwrap(), + )); + } + "sieve_spam_status_plus" => { + instance.set_spam_status( + match value.parse::().unwrap() { + 0 => SpamStatus::Unknown, + 100.. => SpamStatus::Spam, + n => SpamStatus::MaybeSpam((n as f64) / 100.0), + }, + ); + } + "sieve_virus_status" => { + instance.set_virus_status(VirusStatus::from_number( + value.parse().unwrap(), + )); + } + "sieve_editheader_max_header_size" => { + let mhs = if !value.is_empty() { + value.parse::().unwrap() + } else { + 1024 + }; + instance.runtime.set_max_header_size(mhs); + compiler.set_max_header_size(mhs); + } + "sieve_include_max_includes" => { + compiler.set_max_includes(if !value.is_empty() { + value.parse::().unwrap() + } else { + 3 + }); + } + "sieve_include_max_nesting_depth" => { + compiler.set_max_nested_blocks(if !value.is_empty() { + value.parse::().unwrap() + } else { + 3 + }); + } + param => panic!("Invalid test_config_set param '{param}'"), + } + } + "test_result_execute" => { + input = + (matches!(&instance.final_event, Some(Event::Keep { .. })) + || actions.iter().any(|a| { + matches!( + a, + Event::Keep { .. } + | Event::FileInto { .. } + | Event::SendMessage { .. } + ) + })) + .into(); + } + "test_result_action" => { + let param = + params.first().expect("test_result_action parameter"); + input = if param == "reject" { + (actions.iter().any(|a| matches!(a, Event::Reject { .. }))) + .into() + } else if param == "redirect" { + let param = params + .last() + .expect("test_result_action redirect address"); + (actions + .iter() + .any(|a| matches!(a, Event::SendMessage { recipient: Recipient::Address(address), .. } if address == param))) + .into() + } else if param == "keep" { + (matches!(&instance.final_event, Some(Event::Keep { .. })) + || actions + .iter() + .any(|a| matches!(a, Event::Keep { .. }))) + .into() + } else if param == "send_message" { + (actions + .iter() + .any(|a| matches!(a, Event::SendMessage { .. }))) + .into() + } else { + panic!("test_result_action {param} not implemented"); + }; + } + "test_result_action_count" => { + input = (actions.len() + == params.first().unwrap().parse::().unwrap()) + .into(); + } + "test_imap_metadata_set" => { + let mut params = params.into_iter(); + let first = params.next().expect("metadata parameter"); + let (mailbox, annotation) = if first == ":mailbox" { + ( + params.next().expect("metadata mailbox name").into(), + params.next().expect("metadata annotation name"), + ) + } else { + (None, first) + }; + let value = params.next().expect("metadata value"); + if let Some(mailbox) = mailbox { + instance.set_medatata((mailbox, annotation), value); + } else { + instance.set_medatata(annotation, value); + } + } + "test_mailbox_create" => { + mailboxes.push(params.pop().expect("mailbox to create")); + } + "test_result_reset" => { + actions.clear(); + instance.final_event = Event::Keep { + flags: vec![], + message_id: 0, + } + .into(); + instance.metadata.clear(); + instance.has_changes = false; + instance.num_redirects = 0; + instance.runtime.vacation_use_orig_rcpt = false; + mailboxes.clear(); + lists.clear(); + reset_test_boundary(); + } + "test_script_compile" => { + let mut include_path = PathBuf::from(base_path); + include_path.push(params.first().unwrap()); + + if let Ok(bytes) = fs::read(include_path.as_path()) { + let result = compiler.compile(&add_crlf(&bytes)); + /*if let Err(err) = &result { + println!("Error: {:?}", err); + }*/ + input = result.is_ok().into(); + } else { + panic!("Script {} not found.", include_path.display()); + } + } + "test_config_reload" => (), + "test_fail" => { + panic!( + "Test '{}' failed: {}", + current_test, + params.pop().unwrap() + ); + } + _ => panic!("Test command {command} not implemented."), + } + } else { + let mut arguments = arguments + .into_iter() + .filter(|a| !matches!(a, PluginArgument::Tag(_))); + let command = arguments.next().unwrap().unwrap_string().unwrap(); + let arguments = + arguments.next().unwrap().unwrap_string_array().unwrap(); + + assert_eq!(arguments, ["param1", "param2"]); + input = (if command.eq_ignore_ascii_case("always_succeed") { + true + } else if command.eq_ignore_ascii_case("always_fail") { + false + } else { + panic!("Unknown command {command}"); + }) + .into(); + } + } + + action => { + actions.push(action); + input = true.into(); + } + } + } + + return; + } + } + + fn add_crlf(bytes: &[u8]) -> Vec { + let mut result = Vec::with_capacity(bytes.len()); + let mut last_ch = 0; + for &ch in bytes { + if ch == b'\n' && last_ch != b'\r' { + result.push(b'\r'); + } + result.push(ch); + last_ch = ch; + } + result + } +} diff --git a/melib/src/sieve/runtime/actions/action_convert.rs b/melib/src/sieve/runtime/actions/action_convert.rs new file mode 100644 index 00000000..13accce2 --- /dev/null +++ b/melib/src/sieve/runtime/actions/action_convert.rs @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use mail_parser::{ + decoders::html::{html_to_text, text_to_html}, + Encoding, Header, HeaderName, HeaderValue, MimeHeaders, PartType, +}; + +use crate::sieve::{ + compiler::grammar::actions::action_convert::Convert, runtime::tests::TestResult, Context, +}; + +#[derive(Clone, Copy)] +enum Conversion { + TextToHtml, + TextPlainToHtml, + HtmlToText, +} + +impl Convert { + pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult { + let from_media_type = ctx.eval_value(&self.from_media_type).into_cow(); + let to_media_type = ctx.eval_value(&self.to_media_type).into_cow(); + + if from_media_type.eq_ignore_ascii_case(to_media_type.as_ref()) { + return TestResult::Bool(false ^ self.is_not); + } + + let conversion = if (from_media_type.eq_ignore_ascii_case("text") + || from_media_type.starts_with("text/")) + && to_media_type.eq_ignore_ascii_case("text/html") + { + if from_media_type.eq_ignore_ascii_case("text") { + Conversion::TextPlainToHtml + } else { + Conversion::TextToHtml + } + } else if from_media_type.eq_ignore_ascii_case("text/html") + && to_media_type.eq_ignore_ascii_case("text/plain") + { + Conversion::HtmlToText + } else { + return TestResult::Bool(false ^ self.is_not); + }; + let mut did_convert = false; + for part in ctx.message.parts.iter_mut() { + let (new_body, ct) = match (&part.body, conversion) { + (PartType::Html(html), Conversion::HtmlToText) => ( + PartType::Text(html_to_text(html.as_ref()).into()), + "text/plain; charset=utf8", + ), + (PartType::Text(text), Conversion::TextToHtml) => ( + PartType::Html(text_to_html(text.as_ref()).into()), + "text/html; charset=utf8", + ), + (PartType::Text(text), Conversion::TextPlainToHtml) + if part + .content_type() + .and_then(|ct| ct.c_subtype.as_ref()) + .map_or(false, |st| st.eq_ignore_ascii_case("plain")) => + { + ( + PartType::Html(text_to_html(text.as_ref()).into()), + "text/html; charset=utf8", + ) + } + _ => { + continue; + } + }; + part.headers = vec![Header { + name: HeaderName::Other("Content-Type".into()), + value: HeaderValue::Text(ct.to_string().into()), + offset_start: 0, + offset_end: 0, + offset_field: 0, + }]; + ctx.message_size = ctx.message_size + ct.len() + new_body.len() + 16 + - (if part.offset_body != 0 { + part.offset_end - part.offset_header + } else { + part.body.len() + }); + part.offset_body = 0; + part.body = new_body; + part.encoding = Encoding::QuotedPrintable; //Used as non-mime flag + did_convert = true; + } + + if did_convert { + ctx.has_changes = true; + } + + TestResult::Bool(did_convert ^ self.is_not) + } +} diff --git a/melib/src/sieve/runtime/actions/action_editheader.rs b/melib/src/sieve/runtime/actions/action_editheader.rs new file mode 100644 index 00000000..592c5709 --- /dev/null +++ b/melib/src/sieve/runtime/actions/action_editheader.rs @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::borrow::Cow; + +use mail_parser::{Header, HeaderName, HeaderValue}; + +use crate::sieve::{ + compiler::grammar::{ + actions::{ + action_editheader::{AddHeader, DeleteHeader}, + action_mime::MimeOpts, + }, + MatchType, + }, + runtime::Variable, + Context, +}; + +impl AddHeader { + pub(crate) fn exec(&self, ctx: &mut Context) { + let header_name_ = ctx.eval_value(&self.field_name).into_cow(); + let mut header_name = String::with_capacity(header_name_.len()); + + for ch in header_name_.chars() { + if ch.is_alphanumeric() || ch == '-' { + header_name.push(ch); + } + } + + if !header_name.is_empty() { + if let Some(header_name) = HeaderName::parse(header_name) { + if !ctx.runtime.protected_headers.contains(&header_name) { + ctx.has_changes = true; + ctx.insert_header( + ctx.part, + header_name, + ctx.eval_value(&self.value) + .into_cow() + .as_ref() + .remove_crlf(ctx.runtime.max_header_size), + self.last, + ) + } + } + } + } +} + +impl DeleteHeader { + pub(crate) fn exec(&self, ctx: &mut Context) { + let header_name = if let Some(header_name) = + HeaderName::parse(ctx.eval_value(&self.field_name).into_cow()) + { + header_name + } else { + return; + }; + let value_patterns = ctx.eval_values(&self.value_patterns); + let mut deleted_headers = Vec::new(); + let mut deleted_bytes = 0; + + if ctx.runtime.protected_headers.contains(&header_name) { + return; + } + + ctx.find_headers( + &[header_name], + self.index, + self.mime_anychild, + |header, part_id, header_pos| { + if !value_patterns.is_empty() { + let did_match = ctx.find_header_values(header, &MimeOpts::None, |value| { + for (pattern_expr, pattern) in + value_patterns.iter().zip(self.value_patterns.iter()) + { + if match &self.match_type { + MatchType::Is => { + self.comparator.is(&Variable::from(value), pattern_expr) + } + MatchType::Contains => self + .comparator + .contains(value, pattern_expr.to_cow().as_ref()), + MatchType::Value(rel_match) => self.comparator.relational( + rel_match, + &Variable::from(value), + pattern_expr, + ), + MatchType::Matches(_) => self.comparator.matches( + value, + pattern_expr.to_cow().as_ref(), + 0, + &mut Vec::new(), + ), + MatchType::Regex(_) => self.comparator.regex( + pattern, + pattern_expr, + value, + 0, + &mut Vec::new(), + ), + MatchType::Count(_) => false, + MatchType::List => false, + } { + return true; + } + } + false + }); + + if !did_match { + return false; + } + } + + if header.offset_end != 0 { + deleted_bytes += header.offset_end - header.offset_field; + } else { + deleted_bytes += header.name.as_str().len() + header.value.len() + 4; + } + deleted_headers.push((part_id, header_pos)); + + false + }, + ); + + if !deleted_headers.is_empty() { + ctx.has_changes = true; + for (part_id, header_pos) in deleted_headers.iter().rev() { + ctx.message.parts[*part_id].headers.remove(*header_pos); + } + } + + ctx.message_size -= deleted_bytes; + } +} + +pub(crate) trait RemoveCrLf { + fn remove_crlf(&self, max_len: usize) -> String; +} + +impl RemoveCrLf for &str { + fn remove_crlf(&self, max_len: usize) -> String { + let mut header_value = String::with_capacity(self.len()); + for ch in self.chars() { + if !['\n', '\r'].contains(&ch) { + if header_value.len() + ch.len_utf8() <= max_len { + header_value.push(ch); + } else { + return header_value; + } + } + } + header_value + } +} + +impl<'x> Context<'x> { + pub(crate) fn insert_header( + &mut self, + part_id: usize, + header_name: HeaderName<'x>, + header_value: impl Into>, + last: bool, + ) { + let header_value = header_value.into(); + self.message_size += header_name.len() + header_value.len() + 4; + let header = Header { + name: header_name, + value: HeaderValue::Text(header_value), + offset_start: 0, + offset_end: 0, + offset_field: 0, + }; + + if !last { + self.message.parts[part_id].headers.insert(0, header); + } else { + self.message.parts[part_id].headers.push(header); + } + } +} diff --git a/melib/src/sieve/runtime/actions/action_fileinto.rs b/melib/src/sieve/runtime/actions/action_fileinto.rs new file mode 100644 index 00000000..63bd8f9d --- /dev/null +++ b/melib/src/sieve/runtime/actions/action_fileinto.rs @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use crate::sieve::{compiler::grammar::actions::action_fileinto::FileInto, Context, Event}; + +impl FileInto { + pub(crate) fn exec(&self, ctx: &mut Context) { + let folder = ctx.eval_value(&self.folder).into_string(); + let mut events = Vec::with_capacity(2); + if let Some(event) = ctx.build_message_id() { + events.push(event); + } + + if !self.copy + && !matches!(&ctx.final_event, Some(Event::Keep { flags, .. }) if !flags.is_empty()) + { + ctx.final_event = None; + } + + events.push(Event::FileInto { + folder, + flags: ctx.get_local_or_global_flags(&self.flags), + mailbox_id: self + .mailbox_id + .as_ref() + .map(|mi| ctx.eval_value(mi).into_string()), + special_use: self + .special_use + .as_ref() + .map(|su| ctx.eval_value(su).into_string()), + create: self.create, + message_id: ctx.main_message_id, + }); + + ctx.queued_events = events.into_iter(); + } +} diff --git a/melib/src/sieve/runtime/actions/action_flags.rs b/melib/src/sieve/runtime/actions/action_flags.rs new file mode 100644 index 00000000..354b9b96 --- /dev/null +++ b/melib/src/sieve/runtime/actions/action_flags.rs @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use crate::sieve::{ + compiler::{ + grammar::actions::action_flags::{Action, EditFlags}, + Value, VariableType, + }, + Context, +}; + +impl EditFlags { + pub(crate) fn exec(&self, ctx: &mut Context) { + let mut var_name_ = None; + let var_name = self.name.as_ref().unwrap_or_else(|| { + var_name_.get_or_insert_with(|| VariableType::Global("__flags".to_string())) + }); + + match &self.action { + Action::Set => { + let mut flags_lc = Vec::new(); + let mut flags = String::new(); + ctx.tokenize_flags(&self.flags, |flag| { + let flag_lc = flag.to_lowercase(); + if !flags_lc.contains(&flag_lc) { + if !flags.is_empty() { + flags.push(' '); + } + flags.push_str(flag); + flags_lc.push(flag_lc); + } + false + }); + ctx.set_variable(var_name, flags.into()); + } + Action::Add => { + let mut new_flags = ctx + .get_variable(var_name) + .map(|v| v.to_cow()) + .unwrap_or_default() + .into_owned(); + let mut current_flags = new_flags + .split(' ') + .map(|f| f.to_lowercase()) + .collect::>(); + + ctx.tokenize_flags(&self.flags, |flag| { + let flag_lc = flag.to_lowercase(); + if !current_flags.contains(&flag_lc) { + if !new_flags.is_empty() { + new_flags.push(' '); + } + new_flags.push_str(flag); + current_flags.push(flag_lc); + } + false + }); + ctx.set_variable(var_name, new_flags.into()); + } + Action::Remove => { + let mut current_flags = Vec::new(); + let mut current_flags_lc = Vec::new(); + let flags = ctx + .get_variable(var_name) + .map(|v| v.to_cow().into_owned()) + .unwrap_or_default(); + + for flag in flags.split(' ') { + current_flags.push(flag); + current_flags_lc.push(flag.to_lowercase()); + } + ctx.tokenize_flags(&self.flags, |flag| { + let flag = flag.to_lowercase(); + if let Some(pos) = current_flags_lc.iter().position(|lflag| lflag == &flag) { + current_flags.swap_remove(pos); + current_flags_lc.swap_remove(pos); + } + false + }); + ctx.set_variable(var_name, current_flags.join(" ").into()); + } + } + } +} + +impl<'x> Context<'x> { + pub(crate) fn tokenize_flags( + &self, + strings: &[Value], + mut cb: impl FnMut(&str) -> bool, + ) -> bool { + for (pos, string) in strings.iter().enumerate() { + let flag = self.eval_value(string).into_cow(); + if !flag.is_empty() { + if pos == 0 && strings.len() == 1 { + for flag in flag.split_ascii_whitespace() { + if !flag.is_empty() && cb(flag) { + return true; + } + } + } else if cb(flag.trim()) { + return true; + } + } + } + false + } + + pub(crate) fn get_local_flags(&self, strings: &[Value]) -> Vec { + let mut flags = Vec::new(); + self.tokenize_flags(strings, |flag| { + flags.push(flag.to_string()); + false + }); + flags + } + + pub(crate) fn get_global_flags(&self) -> Vec { + match self.vars_global.get("__flags") { + Some(flags) if !flags.is_empty() => flags + .to_cow() + .split(' ') + .map(|s| s.to_string()) + .collect::>(), + _ => Vec::new(), + } + } + + pub(crate) fn get_local_or_global_flags(&self, strings: &[Value]) -> Vec { + if strings.is_empty() { + self.get_global_flags() + } else { + self.get_local_flags(strings) + } + } +} diff --git a/melib/src/sieve/runtime/actions/action_include.rs b/melib/src/sieve/runtime/actions/action_include.rs new file mode 100644 index 00000000..5d0e7585 --- /dev/null +++ b/melib/src/sieve/runtime/actions/action_include.rs @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::sync::Arc; + +use crate::sieve::{ + compiler::grammar::actions::action_include::{Include, Location}, + runtime::RuntimeError, + Context, Event, Script, Sieve, +}; + +pub(crate) enum IncludeResult { + Cached(Arc), + Event(Event), + Error(RuntimeError), + None, +} + +impl Include { + pub(crate) fn exec(&self, ctx: &Context) -> IncludeResult { + let script_name = ctx.eval_value(&self.value); + if !script_name.is_empty() { + let script_name = if self.location == Location::Global { + Script::Global(script_name.into_string()) + } else { + Script::Personal(script_name.into_string()) + }; + + let cached_script = ctx.script_cache.get(&script_name); + if !self.once || cached_script.is_none() { + if ctx.script_stack.len() < ctx.runtime.max_nested_includes { + if let Some(script) = cached_script + .or_else(|| ctx.runtime.include_scripts.get(script_name.as_str())) + { + return IncludeResult::Cached(script.clone()); + } else { + return IncludeResult::Event(Event::IncludeScript { + name: script_name, + optional: self.optional, + }); + } + } else { + return IncludeResult::Error(RuntimeError::TooManyIncludes); + } + } + } + + IncludeResult::None + } +} diff --git a/melib/src/sieve/runtime/actions/action_mime.rs b/melib/src/sieve/runtime/actions/action_mime.rs new file mode 100644 index 00000000..e2630b26 --- /dev/null +++ b/melib/src/sieve/runtime/actions/action_mime.rs @@ -0,0 +1,593 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::cmp::Reverse; + +use mail_parser::{ + decoders::html::html_to_text, Encoding, HeaderName, Message, MessagePart, PartType, +}; + +use crate::sieve::{ + compiler::{ + grammar::actions::action_mime::{Enclose, ExtractText, Replace}, + VariableType, + }, + Context, Event, +}; + +use super::action_editheader::RemoveCrLf; + +#[cfg(not(test))] +use mail_builder::headers::message_id::generate_message_id_header; + +impl Replace { + pub(crate) fn exec(&self, ctx: &mut Context) { + // Delete children parts + let mut part_ids = ctx.find_nested_parts_ids(false); + part_ids.sort_unstable_by_key(|a| Reverse(*a)); + for part_id in part_ids { + ctx.message.parts.remove(part_id); + } + ctx.has_changes = true; + + // Update part + let body = ctx.eval_value(&self.replacement).into_string(); + let body_len = body.len(); + + let part = &mut ctx.message.parts[ctx.part]; + + ctx.message_size = ctx.message_size + body_len + - (if part.offset_body != 0 { + part.offset_end - part.offset_header + } else { + part.body.len() + }); + part.body = PartType::Text(body.into()); + part.encoding = if !self.mime { + Encoding::QuotedPrintable + } else { + Encoding::None + }; + part.offset_body = 0; + let prev_headers = std::mem::take(&mut part.headers); + let mut add_date = true; + + if ctx.part == 0 { + for mut header in prev_headers { + let mut size = header.offset_end - header.offset_field; + match &header.name { + HeaderName::Subject => { + if self.subject.is_some() { + header.name = HeaderName::Other("Original-Subject".into()); + header.offset_field = header.offset_start; + size += "Original-".len(); + } + } + HeaderName::From => { + if self.from.is_some() { + header.name = HeaderName::Other("Original-From".into()); + header.offset_field = header.offset_start; + size += "Original-".len(); + } + } + + HeaderName::To | HeaderName::Cc | HeaderName::Bcc | HeaderName::Received => (), + HeaderName::Date => { + add_date = false; + } + _ => continue, + } + ctx.message_size += size; + part.headers.push(header); + } + + // Add From + let mut add_from = true; + if let Some(from) = self.from.as_ref().map(|f| ctx.eval_value(f)) { + if !from.is_empty() { + ctx.insert_header( + 0, + HeaderName::Other("From".into()), + from.into_cow() + .as_ref() + .remove_crlf(ctx.runtime.max_header_size), + true, + ); + add_from = false; + } + } + if add_from { + ctx.insert_header( + 0, + HeaderName::Other("From".to_string().into()), + ctx.user_from_field(), + true, + ); + } + + // Add Subject + if let Some(subject) = self.subject.as_ref().map(|f| ctx.eval_value(f)) { + if !subject.is_empty() { + ctx.insert_header( + 0, + HeaderName::Other("Subject".into()), + subject + .into_cow() + .as_ref() + .remove_crlf(ctx.runtime.max_header_size), + true, + ); + } + } + + // Add Date + if add_date { + #[cfg(not(test))] + let header_value = mail_builder::headers::date::Date::now().to_rfc822(); + #[cfg(test)] + let header_value = "Tue, 20 Nov 2022 05:14:20 -0300".to_string(); + + ctx.insert_header( + 0, + HeaderName::Other("Date".to_string().into()), + header_value, + true, + ); + } + + // Add Message-ID + let mut header_value = Vec::with_capacity(20); + #[cfg(not(test))] + generate_message_id_header(&mut header_value, &ctx.runtime.local_hostname).unwrap(); + #[cfg(test)] + header_value.extend_from_slice(b""); + + ctx.insert_header( + 0, + HeaderName::Other("Message-ID".to_string().into()), + String::from_utf8(header_value).unwrap(), + true, + ); + } + + if !self.mime { + ctx.insert_header( + ctx.part, + HeaderName::Other("Content-Type".into()), + "text/plain; charset=utf-8".to_string(), + true, + ); + } + } +} + +impl Enclose { + pub(crate) fn exec(&self, ctx: &mut Context) { + let body = ctx.eval_value(&self.value).into_string(); + let subject = self + .subject + .as_ref() + .map(|s| { + ctx.eval_value(s) + .into_cow() + .as_ref() + .remove_crlf(ctx.runtime.max_header_size) + }) + .or_else(|| ctx.message.subject().map(|s| s.to_string())) + .unwrap_or_default(); + + let message = std::mem::take(&mut ctx.message); + #[cfg(test)] + let boundary = make_test_boundary(); + #[cfg(not(test))] + let boundary = mail_builder::mime::make_boundary("."); + + ctx.message_size += ((boundary.len() + 6) * 3) + body.len() + 2; + ctx.part = 0; + ctx.has_changes = true; + ctx.message = Message { + html_body: Vec::with_capacity(0), + text_body: Vec::with_capacity(0), + attachments: Vec::with_capacity(0), + parts: vec![ + MessagePart { + headers: vec![], + is_encoding_problem: false, + body: PartType::Multipart(vec![1, 2]), + encoding: Encoding::None, + offset_header: 0, + offset_body: 0, + offset_end: 0, + }, + MessagePart { + headers: vec![], + is_encoding_problem: false, + body: PartType::Text(body.into()), + encoding: Encoding::QuotedPrintable, // Flag non-mime part + offset_header: 0, + offset_body: 0, + offset_end: 0, + }, + MessagePart { + headers: vec![], + is_encoding_problem: false, + body: PartType::Message(message), + encoding: Encoding::QuotedPrintable, // Flag non-mime part + offset_header: 0, + offset_body: 0, + offset_end: 0, + }, + ], + raw_message: b""[..].into(), + }; + + ctx.insert_header( + 0, + HeaderName::Other("Content-Type".into()), + format!("multipart/mixed; boundary=\"{boundary}\""), + true, + ); + ctx.insert_header(0, HeaderName::Other("Subject".into()), subject, true); + ctx.insert_header( + 1, + HeaderName::Other("Content-Type".into()), + "text/plain; charset=utf-8", + true, + ); + ctx.insert_header( + 2, + HeaderName::Other("Content-Type".into()), + "message/rfc822", + true, + ); + + let mut add_date = true; + let mut add_message_id = true; + let mut add_from = true; + + for header in &self.headers { + let header = ctx.eval_value(header); + if let Some((mut header_name, mut header_value)) = + header.into_cow().as_ref().split_once(':') + { + header_name = header_name.trim(); + header_value = header_value.trim(); + if !header_value.is_empty() { + if let Some(name) = HeaderName::parse(header_name) { + if !ctx.runtime.protected_headers.contains(&name) { + match &name { + HeaderName::Date => { + add_date = false; + } + HeaderName::From => { + add_from = false; + } + HeaderName::MessageId => { + add_message_id = false; + } + _ => (), + } + + ctx.insert_header( + 0, + HeaderName::Other(header_name.to_string().into()), + header_value.remove_crlf(ctx.runtime.max_header_size), + true, + ); + } + } + } + } + } + + if add_from { + ctx.insert_header( + 0, + HeaderName::Other("From".to_string().into()), + ctx.user_from_field(), + true, + ); + } + + if add_date { + #[cfg(not(test))] + let header_value = mail_builder::headers::date::Date::now().to_rfc822(); + #[cfg(test)] + let header_value = "Tue, 20 Nov 2022 05:14:20 -0300".to_string(); + + ctx.insert_header( + 0, + HeaderName::Other("Date".to_string().into()), + header_value, + true, + ); + } + + if add_message_id { + let mut header_value = Vec::with_capacity(20); + #[cfg(not(test))] + generate_message_id_header(&mut header_value, &ctx.runtime.local_hostname).unwrap(); + #[cfg(test)] + header_value.extend_from_slice(b""); + + ctx.insert_header( + 0, + HeaderName::Other("Message-ID".to_string().into()), + String::from_utf8(header_value).unwrap(), + true, + ); + } + } +} + +impl ExtractText { + pub(crate) fn exec(&self, ctx: &mut Context) { + let mut value = String::new(); + + if !ctx.part_iter_stack.is_empty() { + match ctx.message.parts.get(ctx.part).map(|p| &p.body) { + Some(PartType::Text(text)) => { + value = if let Some(first) = &self.first { + text.chars().take(*first).collect() + } else { + text.as_ref().to_string() + }; + } + Some(PartType::Html(html)) => { + value = if let Some(first) = &self.first { + html_to_text(html.as_ref()).chars().take(*first).collect() + } else { + html_to_text(html.as_ref()) + }; + } + _ => (), + } + + if !self.modifiers.is_empty() && !value.is_empty() { + for modifier in &self.modifiers { + value = modifier.apply(&value, ctx); + } + } + } + + match &self.name { + VariableType::Local(var_id) => { + if let Some(var) = ctx.vars_local.get_mut(*var_id) { + *var = value.into(); + } else { + debug_assert!(false, "Non-existent local variable {var_id}"); + } + } + VariableType::Global(var_name) => { + ctx.vars_global + .insert(var_name.to_string().into(), value.into()); + } + VariableType::Envelope(env) => { + ctx.queued_events = vec![Event::SetEnvelope { + envelope: *env, + value, + }] + .into_iter(); + } + _ => (), + } + } +} + +enum StackItem<'x> { + Message(&'x Message<'x>), + Boundary(&'x str), + None, +} + +impl<'x> Context<'x> { + pub(crate) fn build_message_id(&mut self) -> Option { + if self.has_changes { + self.last_message_id += 1; + self.main_message_id = self.last_message_id; + self.has_changes = false; + let message = self.build_message(); + Some(Event::CreatedMessage { + message_id: self.main_message_id, + message, + }) + } else { + None + } + } + + pub(crate) fn build_message(&mut self) -> Vec { + let mut current_message = &self.message; + let mut current_boundary = ""; + let mut message = Vec::with_capacity(self.message_size); + let mut iter = [0].iter(); + let mut iter_stack = Vec::new(); + let mut last_offset = 0; + + 'outer: loop { + while let Some(part) = iter.next().and_then(|p| current_message.parts.get(*p)) { + if last_offset > 0 { + message.extend_from_slice( + ¤t_message.raw_message[last_offset..part.offset_header], + ); + } else if !current_boundary.is_empty() + && part.offset_end == 0 + && !matches!(iter_stack.last(), Some((StackItem::Message(_), _, _))) + { + message.extend_from_slice(b"\r\n--"); + message.extend_from_slice(current_boundary.as_bytes()); + message.extend_from_slice(b"\r\n"); + } + + let mut ct_pos = usize::MAX; + + for (header_pos, header) in part.headers.iter().enumerate() { + if header.offset_end != 0 { + if header.offset_field != header.offset_start { + message.extend_from_slice( + ¤t_message.raw_message + [header.offset_field..header.offset_end], + ); + } else { + // Renamed header + message.extend_from_slice(header.name.as_str().as_bytes()); + message.extend_from_slice(b":"); + message.extend_from_slice( + ¤t_message.raw_message + [header.offset_start..header.offset_end], + ); + } + } else { + if header.name == HeaderName::Other("Content-Type".into()) { + ct_pos = header_pos; + } + + message.extend_from_slice(header.name.as_str().as_bytes()); + message.extend_from_slice(b": "); + message.extend_from_slice(header.value.as_text().unwrap_or("").as_bytes()); + message.extend_from_slice(b"\r\n"); + } + } + + if part.offset_body != 0 || part.encoding != Encoding::None { + // Add CRLF unless this is a :mime replaced part + message.extend_from_slice(b"\r\n"); + } + + if part.offset_body != 0 { + // Original message part + + if let PartType::Multipart(subparts) = &part.body { + // Multiparts contain offsets of the entire part, do not add. + iter_stack.push(( + StackItem::None, + part, + std::mem::replace(&mut iter, subparts.iter()), + )); + last_offset = part.offset_body; + continue 'outer; + } else { + message.extend_from_slice( + ¤t_message.raw_message[part.offset_body..part.offset_end], + ) + } + } else { + match &part.body { + PartType::Message(nested_message) => { + // Enclosed message + iter_stack.push(( + StackItem::Message(current_message), + part, + std::mem::replace(&mut iter, [0].iter()), + )); + current_message = nested_message; + continue 'outer; + } + PartType::Multipart(subparts) => { + // Multipart enclosing nested message, obtain MIME boundary + let prev_boundary = std::mem::replace( + &mut current_boundary, + if ct_pos != usize::MAX { + part.headers[ct_pos] + .value + .as_text() + .and_then(|h| h.split_once("boundary=\"")) + .and_then(|(_, h)| h.split_once('\"')) + .map(|(h, _)| h) + } else { + None + } + .unwrap_or("invalid-boundary"), + ); + + // Enclose multipart + iter_stack.push(( + StackItem::Boundary(prev_boundary), + part, + std::mem::replace(&mut iter, subparts.iter()), + )); + continue 'outer; + } + _ => { + // Replaced part + message.extend_from_slice(part.contents()); + } + } + } + last_offset = part.offset_end; + } + + if let Some((prev_item, prev_part, prev_iter)) = iter_stack.pop() { + match prev_item { + StackItem::Message(prev_message) => { + if last_offset > 0 { + if let Some(bytes) = current_message.raw_message.get(last_offset..) { + message.extend_from_slice(bytes); + } + last_offset = 0; + } + current_message = prev_message; + } + StackItem::Boundary(prev_boundary) => { + if !current_boundary.is_empty() { + message.extend_from_slice(b"\r\n--"); + message.extend_from_slice(current_boundary.as_bytes()); + message.extend_from_slice(b"--\r\n"); + } + current_boundary = prev_boundary; + } + StackItem::None => { + message.extend_from_slice( + ¤t_message.raw_message[last_offset..prev_part.offset_end], + ); + last_offset = prev_part.offset_end; + } + } + iter = prev_iter; + } else { + break; + } + } + + if last_offset > 0 { + if let Some(bytes) = current_message.raw_message.get(last_offset..) { + message.extend_from_slice(bytes); + } + } + + message + } +} + +#[cfg(test)] +thread_local!(static COUNTER: std::cell::Cell = 0.into()); + +#[cfg(test)] +pub(crate) fn make_test_boundary() -> String { + format!("boundary_{}", COUNTER.with(|c| { c.replace(c.get() + 1) })) +} + +#[cfg(test)] +pub(crate) fn reset_test_boundary() { + COUNTER.with(|c| c.replace(0)); +} diff --git a/melib/src/sieve/runtime/actions/action_notify.rs b/melib/src/sieve/runtime/actions/action_notify.rs new file mode 100644 index 00000000..9aaea524 --- /dev/null +++ b/melib/src/sieve/runtime/actions/action_notify.rs @@ -0,0 +1,595 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use mail_builder::headers::{date::Date, message_id::generate_message_id_header}; +use mail_parser::{decoders::quoted_printable::HEX_MAP, HeaderName}; + +use crate::sieve::{ + compiler::grammar::actions::{ + action_notify::Notify, + action_redirect::{ByTime, Ret}, + }, + Context, Event, Importance, Recipient, +}; + +use super::action_vacation::MAX_SUBJECT_LEN; + +impl Notify { + pub(crate) fn exec(&self, ctx: &mut Context) { + // Do not notify on Auto-Submitted messages + for header in &ctx.message.parts[0].headers { + if matches!(&header.name, HeaderName::Other(name) if name.eq_ignore_ascii_case("Auto-Submitted")) + && header + .value + .as_text() + .map_or(true, |v| !v.eq_ignore_ascii_case("no")) + { + return; + } + } + + let uri = ctx.eval_value(&self.method).into_string(); + let (scheme, params) = if let Some(parts) = parse_uri(&uri) { + parts + } else { + return; + }; + + let has_fcc = self.fcc.is_some(); + let is_mailto = scheme.eq_ignore_ascii_case("mailto") + && ctx.num_out_messages < ctx.runtime.max_out_messages; + let mut events = Vec::with_capacity(3); + + if is_mailto || has_fcc { + let params = if is_mailto { + if let Some(params) = parse_mailto(params) { + params + } else { + return; + } + } else { + MailtoMessage { + to: Vec::new(), + cc: Vec::new(), + bcc: Vec::new(), + body: None, + headers: Vec::new(), + } + }; + let from = if let Some(from) = &self.from { + let from = ctx.eval_value(from).into_cow(); + if from + .to_ascii_lowercase() + .contains(&ctx.user_address.to_ascii_lowercase()) + { + from + } else { + ctx.user_from_field().into() + } + } else { + ctx.user_from_field().into() + }; + let notify_message = self.message.as_ref().map(|m| ctx.eval_value(m).into_cow()); + let message_len = params + .to + .iter() + .chain(params.cc.iter()) + .map(|a| a.len() + 4) + .sum::() + + params + .headers + .iter() + .map(|(h, v)| h.len() + v.len() + 4) + .sum::() + + params.body.as_ref().map_or(0, |b| b.len()) + + notify_message.as_ref().map_or(0, |b| b.len()) + + from.len() + + 200; + + let mut message = Vec::with_capacity(message_len); + message.extend_from_slice(b"From: "); + message.extend_from_slice(from.as_bytes()); + message.extend_from_slice(b"\r\n"); + + for (header, addresses) in [("To: ", ¶ms.to), ("Cc: ", ¶ms.cc)] { + if !addresses.is_empty() { + message.extend_from_slice(header.as_bytes()); + for (pos, address) in addresses.iter().enumerate() { + if pos > 0 { + message.extend_from_slice(b", "); + } + if !address.contains('<') { + message.push(b'<'); + } + message.extend_from_slice(address.as_bytes()); + if !address.contains('<') { + message.push(b'>'); + } + } + message.extend_from_slice(b"\r\n"); + } + } + + let mut has_subject = None; + let mut has_date = false; + let mut has_message_id = false; + for (header, value) in ¶ms.headers { + match header { + HeaderName::Subject => { + has_subject = value.into(); + continue; + } + HeaderName::Date => { + has_date = true; + } + HeaderName::MessageId => { + has_message_id = true; + } + HeaderName::From => { + continue; + } + _ => (), + } + message.extend_from_slice(header.as_str().as_bytes()); + message.extend_from_slice(b": "); + message.extend_from_slice(value.as_bytes()); + message.extend_from_slice(b"\r\n"); + } + + if !has_date { + message.extend_from_slice(b"Date: "); + message.extend_from_slice(Date::now().to_rfc822().as_bytes()); + message.extend_from_slice(b"\r\n"); + } + + if !has_message_id { + message.extend_from_slice(b"Message-ID: "); + generate_message_id_header(&mut message, &ctx.runtime.local_hostname).unwrap(); + message.extend_from_slice(b"\r\n"); + } + + let (importance, priority) = + self.importance + .as_ref() + .map_or(("Normal", "3 (Normal)"), |i| { + match ctx.eval_value(i).into_cow().as_ref() { + "1" => ("High", "1 (High)"), + "3" => ("Low", "5 (Low)"), + _ => ("Normal", "3 (Normal)"), + } + }); + message.extend_from_slice(b"Importance: "); + message.extend_from_slice(importance.as_bytes()); + message.extend_from_slice(b"\r\n"); + + message.extend_from_slice(b"X-Priority: "); + message.extend_from_slice(priority.as_bytes()); + message.extend_from_slice(b"\r\n"); + + message.extend_from_slice(b"Subject: "); + let subject = if let Some(subject) = has_subject { + subject.as_str() + } else if let Some(subject) = ¬ify_message { + subject.as_ref() + } else if let Some(subject) = ctx.message.subject() { + subject + } else { + "" + }; + let mut iter = subject.chars().enumerate(); + let mut buf = [0; 4]; + #[allow(clippy::while_let_on_iterator)] + while let Some((pos, char)) = iter.next() { + if pos < MAX_SUBJECT_LEN { + message.extend_from_slice(char.encode_utf8(&mut buf).as_bytes()); + } else { + break; + } + } + if iter.next().is_some() { + message.extend_from_slice('…'.encode_utf8(&mut buf).as_bytes()); + } + message.extend_from_slice(b"\r\n"); + + message.extend_from_slice(b"Auto-Submitted: auto-notified\r\n"); + message.extend_from_slice(b"X-Sieve: yes\r\n"); + message.extend_from_slice(b"Content-type: text/plain; charset=utf-8\r\n\r\n"); + if let Some(body) = params.body { + message.extend_from_slice(body.as_bytes()); + } else if let Some(subject) = ¬ify_message { + message.extend_from_slice(subject.as_bytes()); + } else if let Some(subject) = ctx.message.subject() { + message.extend_from_slice(subject.as_bytes()); + } + + ctx.last_message_id += 1; + events.push(Event::CreatedMessage { + message_id: ctx.last_message_id, + message, + }); + + if is_mailto { + events.push(Event::SendMessage { + recipient: Recipient::Group( + params + .to + .into_iter() + .chain(params.cc) + .chain(params.bcc) + .map(|addr| { + if let Some((addr, _)) = addr + .rsplit_once('<') + .and_then(|(_, addr)| addr.rsplit_once('>')) + { + addr.to_string() + } else { + addr + } + }) + .collect(), + ), + notify: + crate::sieve::compiler::grammar::actions::action_redirect::Notify::Never, + return_of_content: Ret::Default, + by_time: ByTime::None, + message_id: ctx.last_message_id, + }); + } + } + + if !is_mailto { + events.push(Event::Notify { + method: uri, + from: self.from.as_ref().map(|f| ctx.eval_value(f).into_string()), + importance: self.importance.as_ref().map_or(Importance::Normal, |i| { + match ctx.eval_value(i).into_cow().as_ref() { + "1" => Importance::High, + "3" => Importance::Low, + _ => Importance::Normal, + } + }), + options: ctx.eval_values_owned(&self.options), + message: self + .message + .as_ref() + .map(|m| ctx.eval_value(m).into_string()) + .or_else(|| ctx.message.subject().map(|s| s.to_string())) + .unwrap_or_default(), + }); + ctx.num_out_messages += 1; + } + + if let Some(fcc) = &self.fcc { + // File carbon copy + events.push(Event::FileInto { + folder: ctx.eval_value(&fcc.mailbox).into_string(), + flags: ctx.get_local_flags(&fcc.flags), + mailbox_id: fcc + .mailbox_id + .as_ref() + .map(|m| ctx.eval_value(m).into_string()), + special_use: fcc + .special_use + .as_ref() + .map(|s| ctx.eval_value(s).into_string()), + create: fcc.create, + message_id: ctx.last_message_id, + }); + } + ctx.queued_events = events.into_iter(); + } +} + +pub fn validate_from(addr: &str) -> bool { + let mut has_at = false; + let mut has_dot = false; + let mut in_quote = false; + let mut in_angle = false; + let mut last_ch = 0; + + for &ch in addr.as_bytes().iter() { + match ch { + b'\"' => { + if last_ch != b'\\' { + in_quote = !in_quote; + } + } + b'<' if !in_quote => { + if !in_angle { + in_angle = true; + has_at = false; + has_dot = false; + } else { + return false; + } + } + b'>' if !in_quote => { + if in_angle { + in_angle = false; + } else { + return false; + } + } + b'@' if !in_quote => { + if !has_at && last_ch.is_ascii_alphanumeric() { + has_at = true; + } else { + return false; + } + } + b'.' if !in_quote && has_at => { + has_dot = true; + } + _ => (), + } + last_ch = ch; + } + + has_dot && has_at && !in_angle +} + +pub fn validate_uri(uri: &str) -> Option<&str> { + let (scheme, uri) = parse_uri(uri)?; + if scheme.eq_ignore_ascii_case("mailto") { + parse_mailto(uri)?; + scheme.into() + } else if ["xmpp", "tel", "http", "https"].contains(&scheme) { + scheme.into() + } else { + None + } +} + +pub(crate) fn parse_uri(uri: &str) -> Option<(&str, &str)> { + let (scheme, uri) = uri.split_once(':')?; + + if !uri.is_empty() { + Some((scheme, uri)) + } else { + None + } +} + +pub enum Mailto { + Header(HeaderName<'static>), + Body, + Other(String), +} + +enum State { + Address((HeaderName<'static>, bool)), + ParamName, + ParamValue(Mailto), +} + +#[derive(Default)] +struct MailtoMessage { + to: Vec, + cc: Vec, + bcc: Vec, + body: Option, + headers: Vec<(HeaderName<'static>, String)>, +} + +fn parse_mailto(uri: &str) -> Option { + let mut params = MailtoMessage::default(); + + let mut state = State::Address((HeaderName::To, false)); + let mut buf = Vec::new(); + let uri_ = uri.as_bytes(); + let mut iter = uri_.iter(); + let mut has_addresses = false; + + while let Some(&ch) = iter.next() { + match ch { + b'%' => { + let hex1 = HEX_MAP[*iter.next()? as usize]; + let hex2 = HEX_MAP[*iter.next()? as usize]; + if hex1 != -1 && hex2 != -1 { + let ch = ((hex1 as u8) << 4) | hex2 as u8; + + match &state { + State::Address((header, has_at)) => match ch { + b',' => { + if *has_at { + insert_address( + &mut params, + header.clone(), + String::from_utf8(std::mem::take(&mut buf)).ok()?, + ); + has_addresses = true; + state = State::Address((header.clone(), false)); + } else { + return None; + } + } + b'@' => { + if !*has_at { + state = State::Address((header.clone(), true)); + buf.push(ch); + } else { + return None; + } + } + _ => { + buf.push(ch); + } + }, + _ => buf.push(ch), + } + } else { + return None; + } + } + b',' => match &state { + State::Address((header, true)) => { + insert_address( + &mut params, + header.clone(), + String::from_utf8(std::mem::take(&mut buf)).ok()?, + ); + state = State::Address((header.clone(), false)); + has_addresses = true; + } + State::ParamValue(_) => buf.push(ch), + _ => return None, + }, + b'?' => match &state { + State::Address((header, has_at)) if *has_at || buf.is_empty() => { + if !buf.is_empty() { + insert_address( + &mut params, + header.clone(), + String::from_utf8(std::mem::take(&mut buf)).ok()?, + ); + has_addresses = true; + } + state = State::ParamName; + } + State::ParamValue(_) => buf.push(ch), + _ => return None, + }, + b'@' => match &state { + State::Address((header, false)) if !buf.is_empty() => { + buf.push(ch); + state = State::Address((header.clone(), true)); + } + State::ParamName | State::ParamValue(_) => buf.push(ch), + _ => return None, + }, + b'=' => match &state { + State::ParamName if !buf.is_empty() => { + let param = String::from_utf8(std::mem::take(&mut buf)).ok()?; + state = HeaderName::parse(param) + .map(|hdr| match hdr { + HeaderName::To | HeaderName::Cc | HeaderName::Bcc => { + State::Address((hdr, false)) + } + HeaderName::Other(param) => { + if param.eq_ignore_ascii_case("body") { + State::ParamValue(Mailto::Body) + } else { + State::ParamValue(Mailto::Other(param.into_owned())) + } + } + _ => State::ParamValue(Mailto::Header(hdr)), + }) + .unwrap_or_else(|| State::ParamValue(Mailto::Other(String::new()))); + } + State::ParamValue(_) => buf.push(ch), + _ => return None, + }, + b'&' => match state { + State::Address((header, true)) => { + if !buf.is_empty() { + insert_address( + &mut params, + header, + String::from_utf8(std::mem::take(&mut buf)).ok()?, + ); + } + state = State::ParamName; + } + State::ParamValue(param) => { + if !buf.is_empty() { + let value = String::from_utf8(std::mem::take(&mut buf)).ok()?; + match param { + Mailto::Header(header) => params.headers.push((header, value)), + Mailto::Body => params.body = value.into(), + Mailto::Other(header) => params.headers.push((header.into(), value)), + } + } + state = State::ParamName; + } + _ => return None, + }, + _ => match &state { + State::ParamName => { + if ch.is_ascii_alphanumeric() || [b'-', b'_'].contains(&ch) { + buf.push(ch); + } else { + return None; + } + } + _ => { + if !ch.is_ascii_whitespace() { + buf.push(ch); + } + } + }, + } + } + + if !buf.is_empty() { + let value = String::from_utf8(std::mem::take(&mut buf)).ok()?; + match state { + State::Address((header, true)) => { + insert_address(&mut params, header, value); + has_addresses = true; + } + State::ParamName => { + params + .headers + .push((HeaderName::Other(value.into()), String::new())); + } + State::ParamValue(param) => match param { + Mailto::Header(header) => params.headers.push((header, value)), + Mailto::Body => params.body = value.into(), + Mailto::Other(header) => params + .headers + .push((HeaderName::Other(header.into()), value)), + }, + _ => return None, + } + } + + if has_addresses { + Some(params) + } else { + None + } +} + +#[inline(always)] +fn insert_address(params: &mut MailtoMessage, name: HeaderName, value: String) { + if !params + .to + .iter() + .chain(params.cc.iter()) + .chain(params.bcc.iter()) + .any(|v| v.eq_ignore_ascii_case(&value)) + { + match name { + HeaderName::To => { + params.to.push(value); + } + HeaderName::Cc => { + params.cc.push(value); + } + HeaderName::Bcc => { + params.bcc.push(value); + } + _ => (), + } + } +} diff --git a/melib/src/sieve/runtime/actions/action_redirect.rs b/melib/src/sieve/runtime/actions/action_redirect.rs new file mode 100644 index 00000000..c89c4780 --- /dev/null +++ b/melib/src/sieve/runtime/actions/action_redirect.rs @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use mail_parser::{DateTime, HeaderName}; + +use crate::sieve::{ + compiler::grammar::actions::action_redirect::{ByTime, Redirect}, + Context, Envelope, Event, Recipient, +}; + +impl Redirect { + pub(crate) fn exec(&self, ctx: &mut Context) { + if let Some(address) = sanitize_address(ctx.eval_value(&self.address).into_cow().as_ref()) { + if ctx.num_redirects < ctx.runtime.max_redirects + && ctx.num_out_messages < ctx.runtime.max_out_messages + && ctx.message.parts[0] + .headers + .iter() + .filter(|h| matches!(&h.name, HeaderName::Received)) + .count() + < ctx.runtime.max_received_headers + { + // Try to avoid forwarding loops + if !self.list + && (address.eq_ignore_ascii_case(ctx.user_address.as_ref()) + || ctx.envelope.iter().any(|(e, v)| { + matches!(e, Envelope::From) + && v.to_cow().eq_ignore_ascii_case(address.as_str()) + })) + { + return; + } + + if !self.copy && matches!(&ctx.final_event, Some(Event::Keep { .. })) { + ctx.final_event = None; + } + + let mut events = Vec::with_capacity(2); + if let Some(event) = ctx.build_message_id() { + events.push(event); + } + ctx.num_redirects += 1; + ctx.num_out_messages += 1; + events.push(Event::SendMessage { + recipient: if !self.list { + Recipient::Address(address) + } else { + Recipient::List(address) + }, + notify: self.notify.clone(), + return_of_content: self.return_of_content.clone(), + by_time: match &self.by_time { + ByTime::Relative { + rlimit, + mode, + trace, + } => ByTime::Relative { + rlimit: *rlimit, + mode: mode.clone(), + trace: *trace, + }, + ByTime::Absolute { + alimit, + mode, + trace, + } => ByTime::Absolute { + alimit: DateTime::parse_rfc3339( + ctx.eval_value(alimit).into_cow().as_ref(), + ) + .and_then(|d| { + if d.is_valid() { + d.to_timestamp().into() + } else { + None + } + }) + .unwrap_or(0), + mode: mode.clone(), + trace: *trace, + }, + ByTime::None => ByTime::None, + }, + message_id: ctx.main_message_id, + }); + ctx.queued_events = events.into_iter(); + } + } + } +} + +pub(crate) fn sanitize_address(addr: &str) -> Option { + let mut result = String::with_capacity(addr.len()); + let mut in_quote = false; + let mut last_ch = '\n'; + let mut has_at = false; + let mut has_dot = false; + + for ch in addr.chars() { + match ch { + '\"' => { + if !in_quote { + in_quote = true; + } else if last_ch != '\\' { + in_quote = false; + } + } + '@' if !in_quote => { + if !has_at && !result.is_empty() { + has_at = true; + result.push(ch); + } else { + return None; + } + } + '.' if !in_quote && has_at && !has_dot => { + has_dot = true; + result.push(ch); + } + '<' => { + result.clear(); + has_at = false; + has_dot = false; + } + '>' => (), + _ => { + if !ch.is_ascii_whitespace() || in_quote { + result.push(ch); + } + } + } + last_ch = ch; + } + + if !result.is_empty() && has_at && has_dot { + Some(result) + } else { + None + } +} diff --git a/melib/src/sieve/runtime/actions/action_set.rs b/melib/src/sieve/runtime/actions/action_set.rs new file mode 100644 index 00000000..1920792b --- /dev/null +++ b/melib/src/sieve/runtime/actions/action_set.rs @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use crate::sieve::{ + compiler::{ + grammar::actions::action_set::{Modifier, Set}, + VariableType, + }, + runtime::Variable, + Context, Event, +}; +use std::fmt::Write; + +impl Set { + pub(crate) fn exec(&self, ctx: &mut Context) { + let mut value = ctx.eval_value(&self.value).into_owned(); + for modifier in &self.modifiers { + value = modifier.apply(value.into_cow().as_ref(), ctx).into(); + } + + ctx.set_variable(&self.name, value); + } +} + +impl<'x> Context<'x> { + pub(crate) fn set_variable(&mut self, var_name: &VariableType, mut variable: Variable<'x>) { + if variable.len() > self.runtime.max_variable_size { + let mut new_variable = String::with_capacity(self.runtime.max_variable_size); + for ch in variable.into_cow().chars() { + if ch.len_utf8() + new_variable.len() <= self.runtime.max_variable_size { + new_variable.push(ch); + } else { + break; + } + } + variable = new_variable.into(); + } + + match var_name { + VariableType::Local(var_id) => { + if let Some(var) = self.vars_local.get_mut(*var_id) { + *var = variable.into_owned(); + } else { + debug_assert!(false, "Non-existent local variable {var_id}"); + } + } + VariableType::Global(var_name) => { + self.vars_global + .insert(var_name.to_string().into(), variable.into_owned()); + } + VariableType::Envelope(env) => { + self.queued_events = vec![Event::SetEnvelope { + envelope: *env, + value: variable.into_string(), + }] + .into_iter(); + } + _ => (), + } + } + + pub(crate) fn get_variable(&self, var_name: &VariableType) -> Option<&Variable<'x>> { + match var_name { + VariableType::Local(var_id) => self.vars_local.get(*var_id), + VariableType::Global(var_name) => self.vars_global.get(var_name.as_str()), + VariableType::Envelope(env) => { + self.envelope.iter().find_map( + |(name, val)| { + if name == env { + Some(val) + } else { + None + } + }, + ) + } + _ => unreachable!(), + } + } +} + +impl Modifier { + pub(crate) fn apply(&self, input: &str, ctx: &Context) -> String { + let max_len = ctx.runtime.max_variable_size; + match self { + Modifier::Lower => input.to_lowercase(), + Modifier::Upper => input.to_uppercase(), + Modifier::LowerFirst => { + let mut result = String::with_capacity(input.len()); + for (pos, char) in input.chars().enumerate() { + if result.len() + char.len_utf8() <= max_len { + if pos != 0 { + result.push(char); + } else { + for char in char.to_lowercase() { + result.push(char); + } + } + } else { + return result; + } + } + result + } + Modifier::UpperFirst => { + let mut result = String::with_capacity(input.len()); + for (pos, char) in input.chars().enumerate() { + if result.len() + char.len_utf8() <= max_len { + if pos != 0 { + result.push(char); + } else { + for char in char.to_uppercase() { + result.push(char); + } + } + } else { + return result; + } + } + result + } + Modifier::QuoteWildcard => { + let mut result = String::with_capacity(input.len()); + for char in input.chars() { + if ['*', '\\', '?'].contains(&char) { + if result.len() + char.len_utf8() < max_len { + result.push('\\'); + result.push(char); + } else { + return result; + } + } else if result.len() + char.len_utf8() <= max_len { + result.push(char); + } else { + return result; + } + } + result + } + Modifier::QuoteRegex => { + let mut result = String::with_capacity(input.len()); + for char in input.chars() { + if [ + '*', '\\', '?', '.', '[', ']', '(', ')', '+', '{', '}', '|', '^', '=', ':', + '$', + ] + .contains(&char) + { + if result.len() + char.len_utf8() < max_len { + result.push('\\'); + result.push(char); + } else { + return result; + } + } else if result.len() + char.len_utf8() <= max_len { + result.push(char); + } else { + return result; + } + } + result + } + Modifier::Length => input.chars().count().to_string(), + Modifier::EncodeUrl => { + let mut buf = [0; 4]; + let mut result = String::with_capacity(input.len()); + + for char in input.chars() { + if char.is_ascii_alphanumeric() || ['-', '.', '_', '~'].contains(&char) { + if result.len() < max_len { + result.push(char); + } else { + return result; + } + } else if result.len() + (char.len_utf8() * 3) <= max_len { + for byte in char.encode_utf8(&mut buf).as_bytes().iter() { + write!(result, "%{byte:02x}").ok(); + } + } else { + return result; + } + } + result + } + Modifier::Replace { find, replace } => input.replace( + ctx.eval_value(find).into_cow().as_ref(), + ctx.eval_value(replace).into_cow().as_ref(), + ), + } + } +} diff --git a/melib/src/sieve/runtime/actions/action_vacation.rs b/melib/src/sieve/runtime/actions/action_vacation.rs new file mode 100644 index 00000000..001daabc --- /dev/null +++ b/melib/src/sieve/runtime/actions/action_vacation.rs @@ -0,0 +1,360 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::borrow::Cow; + +use mail_builder::headers::{date::Date, message_id::generate_message_id_header}; +use mail_parser::{HeaderName, HeaderValue}; + +use crate::sieve::{ + compiler::grammar::{ + actions::{ + action_redirect::{ByTime, Notify, Ret}, + action_vacation::{Period, TestVacation, Vacation}, + }, + AddressPart, + }, + runtime::tests::TestResult, + Context, Envelope, Event, Recipient, +}; + +pub(crate) const MAX_SUBJECT_LEN: usize = 256; + +impl TestVacation { + pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult { + let mut from = String::new(); + let mut user_addresses = Vec::new(); + + if ctx.num_out_messages >= ctx.runtime.max_out_messages { + return TestResult::Bool(false); + } + + for (name, value) in &ctx.envelope { + if !value.is_empty() { + match name { + Envelope::From => { + from = value.to_cow().to_ascii_lowercase(); + } + Envelope::To => { + if !ctx.runtime.vacation_use_orig_rcpt { + user_addresses.push(value.to_cow()); + } + } + Envelope::Orcpt => { + if ctx.runtime.vacation_use_orig_rcpt { + user_addresses.push(value.to_cow()); + } + } + _ => (), + } + } + } + + // Add user specified addresses + for address in &self.addresses { + let address = ctx.eval_value(address).into_cow(); + if !address.is_empty() { + user_addresses.push(address); + } + } + if !ctx.user_address.is_empty() { + user_addresses.push(ctx.user_address.as_ref().into()); + } + + // Do not reply to own address + if from.is_empty() + || user_addresses.is_empty() + || from.starts_with("mailer-daemon") + || from.starts_with("owner-") + || from.contains("-request@") + || user_addresses.iter().any(|a| a.eq_ignore_ascii_case(&from)) + { + return TestResult::Bool(false); + } + + // Check headers + let mut found_rcpt = false; + let mut received_count = 0; + for header in &ctx.message.parts[0].headers { + match &header.name { + HeaderName::To + | HeaderName::Cc + | HeaderName::Bcc + | HeaderName::ResentTo + | HeaderName::ResentBcc + | HeaderName::ResentCc + if !found_rcpt => + { + found_rcpt = ctx.find_addresses(header, &AddressPart::All, |addr| { + user_addresses.iter().any(|a| a.eq_ignore_ascii_case(addr)) + }); + } + HeaderName::ListArchive + | HeaderName::ListHelp + | HeaderName::ListId + | HeaderName::ListOwner + | HeaderName::ListPost + | HeaderName::ListSubscribe + | HeaderName::ListUnsubscribe => { + // Do not send vacation responses to lists + return TestResult::Bool(false); + } + HeaderName::Received => { + received_count += 1; + } + HeaderName::Other(header_name) => { + if header_name.eq_ignore_ascii_case("Auto-Submitted") { + if header + .value + .as_text() + .map_or(true, |v| !v.eq_ignore_ascii_case("no")) + { + return TestResult::Bool(false); + } + } else if header_name.eq_ignore_ascii_case("X-Auto-Response-Suppress") { + if header.value.as_text().map_or(false, |v| { + v.to_ascii_lowercase() + .split(',') + .any(|v| ["all", "oof"].contains(&v.trim())) + }) { + return TestResult::Bool(false); + } + } else if header_name.eq_ignore_ascii_case("Precedence") + && header + .value + .as_text() + .map_or(false, |v| v.eq_ignore_ascii_case("bulk")) + { + return TestResult::Bool(false); + } + } + _ => (), + } + } + + // No user address found in header or possible loop + if found_rcpt && received_count <= ctx.runtime.max_received_headers { + TestResult::Event { + event: Event::DuplicateId { + id: if let Some(handle) = &self.handle { + format!("_v{}{}", from, ctx.eval_value(handle).into_cow()) + } else { + format!("_v{}{}", from, ctx.eval_value(&self.reason).into_cow()) + }, + expiry: match &self.period { + Period::Days(days) => days * 86400, + Period::Seconds(seconds) => *seconds, + Period::Default => ctx.runtime.default_vacation_expiry, + }, + last: false, + }, + is_not: true, + } + } else { + TestResult::Bool(false) + } + } +} + +impl Vacation { + pub(crate) fn exec(&self, ctx: &mut Context) { + let mut vacation_to = Cow::from(""); + + for (name, value) in &ctx.envelope { + if !value.is_empty() && name == &Envelope::From { + vacation_to = value.to_cow(); + break; + } + } + + // Check headers + let mut vacation_subject = if let Some(subject) = &self.subject { + ctx.eval_value(subject) + } else { + "".into() + }; + + // Check headers + let mut message_id = None; + let mut vacation_to_full = None; + let mut references = None; + for header in &ctx.message.parts[0].headers { + match &header.name { + HeaderName::Subject if vacation_subject.is_empty() => { + if let Some(subject) = header.value.as_text() { + let mut vacation_subject_ = String::with_capacity(MAX_SUBJECT_LEN); + let mut iter = ctx + .runtime + .vacation_subject_prefix + .chars() + .chain(subject.chars()) + .enumerate(); + + #[allow(clippy::while_let_on_iterator)] + while let Some((pos, char)) = iter.next() { + if pos < MAX_SUBJECT_LEN { + vacation_subject_.push(char); + } else { + break; + } + } + if iter.next().is_some() { + vacation_subject_.push('…'); + } + vacation_subject = vacation_subject_.into(); + } + } + HeaderName::MessageId => { + message_id = header.value.as_text(); + } + HeaderName::References => { + if header.offset_start > 0 { + references = (&ctx.message.raw_message + [header.offset_start..header.offset_end]) + .into(); + } + } + HeaderName::From | HeaderName::Sender => { + if matches!(&header.value, HeaderValue::Address(address) if address.contains(vacation_to.as_ref())) + && header.offset_start > 0 + { + vacation_to_full = (&ctx.message.raw_message + [header.offset_start..header.offset_end]) + .into(); + } + } + _ => (), + } + } + + // Build message + let vacation_from = if let Some(from) = &self.from { + ctx.eval_value(from) + } else if !ctx.user_address.is_empty() { + ctx.user_from_field().into() + } else if let Some(addr) = + ctx.envelope + .iter() + .find_map(|(n, v)| if n == &Envelope::To { Some(v) } else { None }) + { + addr.to_cow().into() + } else { + "".into() + }; + if vacation_subject.is_empty() { + vacation_subject = ctx.runtime.vacation_default_subject.as_ref().into(); + } + let vacation_body = ctx.eval_value(&self.reason); + let message_len = vacation_body.len() + + vacation_from.len() + + vacation_to_full + .as_ref() + .map_or(vacation_to.len(), |t| t.len()) + + vacation_subject.len() + + message_id.as_ref().map_or(0, |m| m.len() * 2) + + references.as_ref().map_or(0, |m| m.len()) + + 160; + + let mut message = Vec::with_capacity(message_len); + write_header(&mut message, "From: ", vacation_from.into_cow().as_ref()); + if let Some(vacation_to_full) = vacation_to_full { + message.extend_from_slice(b"To:"); + message.extend_from_slice(vacation_to_full); + } else { + write_header(&mut message, "To: ", vacation_to.to_string().as_ref()); + } + write_header( + &mut message, + "Subject: ", + vacation_subject.into_cow().as_ref(), + ); + if let Some(message_id) = message_id { + message.extend_from_slice(b"In-Reply-To: <"); + message.extend_from_slice(message_id.as_bytes()); + message.extend_from_slice(b">\r\n"); + + message.extend_from_slice(b"References: <"); + message.extend_from_slice(message_id.as_bytes()); + if let Some(references) = references { + message.extend_from_slice(b"> "); + message.extend_from_slice(references); + } else { + message.extend_from_slice(b">\r\n"); + } + } + message.extend_from_slice(b"Date: "); + message.extend_from_slice(Date::now().to_rfc822().as_bytes()); + message.extend_from_slice(b"\r\n"); + + message.extend_from_slice(b"Message-ID: "); + generate_message_id_header(&mut message, &ctx.runtime.local_hostname).unwrap(); + message.extend_from_slice(b"\r\n"); + + write_header(&mut message, "Auto-Submitted: ", "auto-replied"); + if !self.mime { + message.extend_from_slice(b"Content-type: text/plain; charset=utf-8\r\n\r\n"); + } + message.extend_from_slice(vacation_body.into_cow().as_bytes()); + + // Add action + let mut events = Vec::with_capacity(3); + ctx.last_message_id += 1; + ctx.num_out_messages += 1; + events.push(Event::CreatedMessage { + message_id: ctx.last_message_id, + message, + }); + events.push(Event::SendMessage { + recipient: Recipient::Address(vacation_to.to_string()), + notify: Notify::Never, + return_of_content: Ret::Default, + by_time: ByTime::None, + message_id: ctx.last_message_id, + }); + + // File carbon copy + if let Some(fcc) = &self.fcc { + events.push(Event::FileInto { + folder: ctx.eval_value(&fcc.mailbox).into_string(), + flags: ctx.get_local_flags(&fcc.flags), + mailbox_id: fcc + .mailbox_id + .as_ref() + .map(|m| ctx.eval_value(m).into_string()), + special_use: fcc + .special_use + .as_ref() + .map(|s| ctx.eval_value(s).into_string()), + create: fcc.create, + message_id: ctx.last_message_id, + }); + } + ctx.queued_events = events.into_iter(); + } +} + +fn write_header(buf: &mut Vec, name: &str, value: &str) { + buf.extend_from_slice(name.as_bytes()); + buf.extend_from_slice(value.as_bytes()); + buf.extend_from_slice(b"\r\n"); +} diff --git a/melib/src/sieve/runtime/actions/mod.rs b/melib/src/sieve/runtime/actions/mod.rs new file mode 100644 index 00000000..05cd0ba4 --- /dev/null +++ b/melib/src/sieve/runtime/actions/mod.rs @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +pub mod action_convert; +pub mod action_editheader; +pub mod action_fileinto; +pub mod action_flags; +pub mod action_include; +pub mod action_mime; +pub mod action_notify; +pub mod action_redirect; +pub mod action_set; +pub mod action_vacation; diff --git a/melib/src/sieve/runtime/context.rs b/melib/src/sieve/runtime/context.rs new file mode 100644 index 00000000..be35b620 --- /dev/null +++ b/melib/src/sieve/runtime/context.rs @@ -0,0 +1,682 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::convert::TryInto; +use std::{borrow::Cow, sync::Arc, time::SystemTime}; + +use ahash::AHashMap; +use mail_parser::Message; + +use crate::sieve::{ + compiler::grammar::{instruction::Instruction, Capability}, + Context, Envelope, Event, Input, Metadata, Runtime, Sieve, SpamStatus, VirusStatus, + MAX_LOCAL_VARIABLES, MAX_MATCH_VARIABLES, +}; + +use super::{ + actions::action_include::IncludeResult, + tests::{test_envelope::parse_envelope_address, TestResult}, + RuntimeError, Variable, +}; + +#[derive(Clone, Debug)] +pub(crate) struct ScriptStack { + pub(crate) script: Arc, + pub(crate) prev_pos: usize, + pub(crate) prev_vars_local: Vec>, + pub(crate) prev_vars_match: Vec>, +} + +impl<'x> Context<'x> { + pub(crate) fn new(runtime: &'x Runtime, message: Message<'x>) -> Self { + Context { + #[cfg(test)] + runtime: runtime.clone(), + #[cfg(not(test))] + runtime, + message, + part: 0, + part_iter: Vec::new().into_iter(), + part_iter_stack: Vec::new(), + line_iter: Vec::new().into_iter().enumerate(), + pos: usize::MAX, + test_result: false, + script_cache: AHashMap::new(), + script_stack: Vec::with_capacity(0), + vars_global: AHashMap::new(), + vars_env: AHashMap::new(), + vars_local: Vec::with_capacity(0), + vars_match: Vec::with_capacity(0), + envelope: Vec::new(), + metadata: Vec::new(), + message_size: usize::MAX, + final_event: Event::Keep { + flags: Vec::with_capacity(0), + message_id: 0, + } + .into(), + queued_events: vec![].into_iter(), + has_changes: false, + user_address: "".into(), + user_full_name: "".into(), + current_time: SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0) as i64, + num_redirects: 0, + num_instructions: 0, + num_out_messages: 0, + last_message_id: 0, + main_message_id: 0, + virus_status: VirusStatus::Unknown, + spam_status: SpamStatus::Unknown, + } + } + + #[allow(clippy::while_let_on_iterator)] + pub fn run(&mut self, input: Input) -> Option> { + match input { + Input::True => self.test_result ^= true, + Input::False => self.test_result ^= false, + Input::Script { name, script } => { + let num_vars = script.num_vars; + let num_match_vars = script.num_match_vars; + + if num_match_vars <= MAX_MATCH_VARIABLES && num_vars <= MAX_LOCAL_VARIABLES { + if self.message_size == usize::MAX { + self.message_size = self.message.raw_message.len(); + } + + self.script_cache.insert(name, script.clone()); + self.script_stack.push(ScriptStack { + script, + prev_pos: self.pos, + prev_vars_local: std::mem::replace( + &mut self.vars_local, + vec![Variable::default(); num_vars], + ), + prev_vars_match: std::mem::replace( + &mut self.vars_match, + vec![Variable::default(); num_match_vars], + ), + }); + self.pos = 0; + self.test_result = false; + } + } + Input::Variables { list } => { + for item in list { + self.set_variable(&item.name, item.value); + } + self.test_result ^= true; + } + } + + // Return any queued events + if let Some(event) = self.queued_events.next() { + return Some(Ok(event)); + } + + let mut current_script = self.script_stack.last()?.script.clone(); + let mut iter = current_script.instructions.get(self.pos..)?.iter(); + + 'outer: loop { + while let Some(instruction) = iter.next() { + self.num_instructions += 1; + if self.num_instructions > self.runtime.cpu_limit { + self.finish_loop(); + return Some(Err(RuntimeError::CPULimitReached)); + } + self.pos += 1; + + match instruction { + Instruction::Jz(jmp_pos) => { + if !self.test_result { + debug_assert!(*jmp_pos > self.pos - 1); + self.pos = *jmp_pos; + iter = current_script.instructions.get(self.pos..)?.iter(); + continue; + } + } + Instruction::Jnz(jmp_pos) => { + if self.test_result { + debug_assert!(*jmp_pos > self.pos - 1); + self.pos = *jmp_pos; + iter = current_script.instructions.get(self.pos..)?.iter(); + continue; + } + } + Instruction::Jmp(jmp_pos) => { + debug_assert_ne!(*jmp_pos, self.pos - 1); + self.pos = *jmp_pos; + iter = current_script.instructions.get(self.pos..)?.iter(); + continue; + } + Instruction::Test(test) => match test.exec(self) { + TestResult::Bool(result) => { + self.test_result = result; + } + TestResult::Event { event, is_not } => { + self.test_result = is_not; + return Some(Ok(event)); + } + TestResult::Error(err) => { + self.finish_loop(); + return Some(Err(err)); + } + }, + Instruction::Clear(clear) => { + if clear.local_vars_num > 0 { + if let Some(local_vars) = self.vars_local.get_mut( + clear.local_vars_idx as usize + ..(clear.local_vars_idx + clear.local_vars_num) as usize, + ) { + for local_var in local_vars.iter_mut() { + if !local_var.is_empty() { + *local_var = Variable::default(); + } + } + } else { + debug_assert!(false, "Failed to clear local variables: {clear:?}"); + } + } + if clear.match_vars != 0 { + self.clear_match_variables(clear.match_vars); + } + } + Instruction::Keep(keep) => { + let next_event = self.build_message_id(); + self.final_event = Event::Keep { + flags: self.get_local_or_global_flags(&keep.flags), + message_id: self.main_message_id, + } + .into(); + if let Some(next_event) = next_event { + return Some(Ok(next_event)); + } + } + Instruction::FileInto(fi) => { + fi.exec(self); + if let Some(event) = self.queued_events.next() { + return Some(Ok(event)); + } + } + Instruction::Redirect(redirect) => { + redirect.exec(self); + if let Some(event) = self.queued_events.next() { + return Some(Ok(event)); + } + } + Instruction::Discard => { + self.final_event = Event::Discard.into(); + } + Instruction::Stop => { + self.script_stack.clear(); + break 'outer; + } + Instruction::Reject(reject) => { + self.final_event = None; + return Some(Ok(Event::Reject { + extended: reject.ereject, + reason: self.eval_value(&reject.reason).into_string(), + })); + } + Instruction::ForEveryPart(fep) => { + if let Some(next_part) = self.part_iter.next() { + self.part = next_part; + } else if let Some((prev_part, prev_part_iter)) = self.part_iter_stack.pop() + { + debug_assert!(fep.jz_pos > self.pos - 1); + self.part_iter = prev_part_iter; + self.part = prev_part; + self.pos = fep.jz_pos; + iter = current_script.instructions.get(self.pos..)?.iter(); + continue; + } else { + self.part = 0; + #[cfg(test)] + panic!("ForEveryPart executed without items on stack."); + } + } + Instruction::ForEveryPartPush => { + let part_iter = self + .find_nested_parts_ids(self.part_iter_stack.is_empty()) + .into_iter(); + self.part_iter_stack + .push((self.part, std::mem::replace(&mut self.part_iter, part_iter))); + } + Instruction::ForEveryPartPop(num_pops) => { + debug_assert!( + *num_pops > 0 && *num_pops <= self.part_iter_stack.len(), + "Pop out of range: {} with {} items.", + num_pops, + self.part_iter_stack.len() + ); + for _ in 0..*num_pops { + if let Some((prev_part, prev_part_iter)) = self.part_iter_stack.pop() { + self.part_iter = prev_part_iter; + self.part = prev_part; + } else { + break; + } + } + } + Instruction::ForEveryLineInit(source) => { + self.line_iter = match self.eval_value(source) { + Variable::Array(arr) if !arr.is_empty() => arr + .into_iter() + .map(|v| v.into_owned()) + .collect::>() + .into_iter() + .enumerate(), + Variable::ArrayRef(arr) if !arr.is_empty() => arr + .iter() + .map(|v| v.to_owned()) + .collect::>() + .into_iter() + .enumerate(), + Variable::String(s) => s + .lines() + .map(|line| Variable::String(line.to_string())) + .collect::>() + .into_iter() + .enumerate(), + Variable::StringRef(s) => s + .lines() + .map(|line| Variable::String(line.to_string())) + .collect::>() + .into_iter() + .enumerate(), + Variable::Integer(n) => { + vec![Variable::Integer(n)].into_iter().enumerate() + } + Variable::Float(n) => vec![Variable::Float(n)].into_iter().enumerate(), + _ => Vec::new().into_iter().enumerate(), + }; + } + Instruction::ForEveryLine(fep) => { + if let Some((line_num, line)) = self.line_iter.next() { + if let Some(var) = self.vars_local.get_mut(fep.var_idx) { + *var = line; + } else { + debug_assert!(false, "Non-existent local variable {}", fep.var_idx); + } + if let Some(var) = self.vars_local.get_mut(fep.var_idx + 1) { + *var = Variable::Integer((line_num + 1) as i64); + } else { + debug_assert!( + false, + "Non-existent local variable {}", + fep.var_idx + 1 + ); + } + } else { + debug_assert!(fep.jz_pos > self.pos - 1); + self.pos = fep.jz_pos; + iter = current_script.instructions.get(self.pos..)?.iter(); + continue; + } + } + + Instruction::Replace(replace) => replace.exec(self), + Instruction::Enclose(enclose) => enclose.exec(self), + Instruction::ExtractText(extract) => { + extract.exec(self); + if let Some(event) = self.queued_events.next() { + return Some(Ok(event)); + } + } + Instruction::AddHeader(add_header) => add_header.exec(self), + Instruction::DeleteHeader(delete_header) => delete_header.exec(self), + Instruction::Set(set) => { + set.exec(self); + if let Some(event) = self.queued_events.next() { + return Some(Ok(event)); + } + } + Instruction::Notify(notify) => { + notify.exec(self); + if let Some(event) = self.queued_events.next() { + return Some(Ok(event)); + } + } + Instruction::Vacation(vacation) => { + vacation.exec(self); + if let Some(event) = self.queued_events.next() { + return Some(Ok(event)); + } + } + Instruction::EditFlags(flags) => flags.exec(self), + Instruction::Include(include) => match include.exec(self) { + IncludeResult::Cached(script) => { + self.script_stack.push(ScriptStack { + script: script.clone(), + prev_pos: self.pos, + prev_vars_local: std::mem::replace( + &mut self.vars_local, + vec![Variable::default(); script.num_vars], + ), + prev_vars_match: std::mem::replace( + &mut self.vars_match, + vec![Variable::default(); script.num_match_vars], + ), + }); + self.pos = 0; + current_script = script; + iter = current_script.instructions.iter(); + continue; + } + IncludeResult::Event(event) => { + return Some(Ok(event)); + } + IncludeResult::Error(err) => { + self.finish_loop(); + return Some(Err(err)); + } + IncludeResult::None => (), + }, + Instruction::Convert(convert) => { + convert.exec(self); + } + Instruction::Return => { + break; + } + Instruction::Require(capabilities) => { + for capability in capabilities { + if !self.runtime.allowed_capabilities.contains(capability) { + self.finish_loop(); + return Some(Err( + if let Capability::Other(not_supported) = capability { + RuntimeError::CapabilityNotSupported(not_supported.clone()) + } else { + RuntimeError::CapabilityNotAllowed(capability.clone()) + }, + )); + } + } + } + Instruction::Error(err) => { + self.finish_loop(); + return Some(Err(RuntimeError::ScriptErrorMessage( + self.eval_value(&err.message).into_string(), + ))); + } + Instruction::Plugin(plugin) => { + return Some(Ok(self.eval_plugin_arguments(plugin))); + } + Instruction::Invalid(invalid) => { + self.finish_loop(); + return Some(Err(RuntimeError::InvalidInstruction(invalid.clone()))); + } + } + } + + if let Some(prev_script) = self.script_stack.pop() { + self.pos = prev_script.prev_pos; + self.vars_local = prev_script.prev_vars_local; + self.vars_match = prev_script.prev_vars_match; + } + + if let Some(script_stack) = self.script_stack.last() { + current_script = script_stack.script.clone(); + iter = current_script.instructions.get(self.pos..)?.iter(); + } else { + break; + } + } + + match self.final_event.take() { + Some(Event::Keep { + mut flags, + message_id, + }) => { + let create_event = if self.has_changes { + self.build_message_id() + } else { + None + }; + + let global_flags = self.get_global_flags(); + if flags.is_empty() && !global_flags.is_empty() { + flags = global_flags; + } + if let Some(create_event) = create_event { + self.queued_events = vec![ + create_event, + Event::Keep { + flags, + message_id: self.main_message_id, + }, + ] + .into_iter(); + self.queued_events.next().map(Ok) + } else { + Some(Ok(Event::Keep { flags, message_id })) + } + } + Some(event) => Some(Ok(event)), + _ => None, + } + } + + pub(crate) fn finish_loop(&mut self) { + self.script_stack.clear(); + if let Some(event) = self.final_event.take() { + self.queued_events = if let Event::Keep { + mut flags, + message_id, + } = event + { + let global_flags = self.get_global_flags(); + if flags.is_empty() && !global_flags.is_empty() { + flags = global_flags; + } + + if self.has_changes { + if let Some(event) = self.build_message_id() { + vec![ + event, + Event::Keep { + flags, + message_id: self.main_message_id, + }, + ] + } else { + vec![Event::Keep { flags, message_id }] + } + } else { + vec![Event::Keep { flags, message_id }] + } + } else { + vec![event] + } + .into_iter(); + } + } + + pub fn set_envelope( + &mut self, + envelope: impl TryInto, + value: impl Into>, + ) { + if let Ok(envelope) = envelope.try_into() { + if matches!(&envelope, Envelope::From | Envelope::To) { + let value: Cow = value.into(); + if let Some(value) = parse_envelope_address(value.as_ref()) { + self.envelope.push((envelope, value.to_string().into())); + } + } else { + self.envelope.push((envelope, Variable::from(value.into()))); + } + } + } + + pub fn with_vars_env(mut self, vars_env: AHashMap, Variable<'x>>) -> Self { + self.vars_env = vars_env; + self + } + + pub fn with_envelope_list(mut self, envelope: Vec<(Envelope, Variable<'x>)>) -> Self { + self.envelope = envelope; + self + } + + pub fn with_envelope( + mut self, + envelope: impl TryInto, + value: impl Into>, + ) -> Self { + self.set_envelope(envelope, value); + self + } + + pub fn clear_envelope(&mut self) { + self.envelope.clear() + } + + pub fn set_user_address(&mut self, from: impl Into>) { + self.user_address = from.into(); + } + + pub fn with_user_address(mut self, from: impl Into>) -> Self { + self.set_user_address(from); + self + } + + pub fn set_user_full_name(&mut self, name: &str) { + let mut name_ = String::with_capacity(name.len()); + for ch in name.chars() { + if ['\"', '\\'].contains(&ch) { + name_.push('\\'); + } + name_.push(ch); + } + self.user_full_name = name_.into(); + } + + pub fn with_user_full_name(mut self, name: &str) -> Self { + self.set_user_full_name(name); + self + } + + pub fn set_env_variable( + &mut self, + name: impl Into>, + value: impl Into>, + ) { + self.vars_env.insert(name.into(), value.into()); + } + + pub fn with_env_variable( + mut self, + name: impl Into>, + value: impl Into>, + ) -> Self { + self.set_env_variable(name, value); + self + } + + pub fn set_global_variable( + &mut self, + name: impl Into>, + value: impl Into>, + ) { + self.vars_env.insert(name.into(), value.into()); + } + + pub fn with_global_variable( + mut self, + name: impl Into>, + value: impl Into>, + ) -> Self { + self.set_global_variable(name, value); + self + } + + pub fn set_medatata( + &mut self, + name: impl Into>, + value: impl Into>, + ) { + self.metadata.push((name.into(), value.into())); + } + + pub fn with_metadata( + mut self, + name: impl Into>, + value: impl Into>, + ) -> Self { + self.set_medatata(name, value); + self + } + + pub fn set_spam_status(&mut self, status: impl Into) { + self.spam_status = status.into(); + } + + pub fn with_spam_status(mut self, status: impl Into) -> Self { + self.set_spam_status(status); + self + } + + pub fn set_virus_status(&mut self, status: impl Into) { + self.virus_status = status.into(); + } + + pub fn with_virus_status(mut self, status: impl Into) -> Self { + self.set_virus_status(status); + self + } + + pub fn take_message(&mut self) -> Message<'x> { + std::mem::take(&mut self.message) + } + + pub fn has_message_changed(&self) -> bool { + self.main_message_id > 0 + } + + pub(crate) fn user_from_field(&self) -> String { + if !self.user_full_name.is_empty() { + format!("\"{}\" <{}>", self.user_full_name, self.user_address) + } else { + self.user_address.to_string() + } + } + + pub fn global_variable_names(&self) -> impl Iterator { + self.vars_global.keys().map(|k| k.as_ref()) + } + + pub fn global_variable(&self, name: &str) -> Option<&Variable<'x>> { + self.vars_global.get(name) + } + + pub fn message(&self) -> &Message<'x> { + &self.message + } + + pub fn part(&self) -> usize { + self.part + } +} diff --git a/melib/src/sieve/runtime/eval.rs b/melib/src/sieve/runtime/eval.rs new file mode 100644 index 00000000..b1fceea0 --- /dev/null +++ b/melib/src/sieve/runtime/eval.rs @@ -0,0 +1,499 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::cmp::Ordering; + +use mail_parser::{ + decoders::html::{html_to_text, text_to_html}, + parsers::MessageStream, + Addr, Header, HeaderName, HeaderValue, Host, PartType, Received, +}; + +use crate::sieve::{ + compiler::{ + grammar::tests::test_plugin::Plugin, ContentTypePart, HeaderPart, HeaderVariable, + MessagePart, ReceivedHostname, ReceivedPart, Value, VariableType, + }, + Context, Event, PluginArgument, +}; + +use super::Variable; + +impl<'x> Context<'x> { + pub(crate) fn variable<'y: 'x>(&'y self, var: &VariableType) -> Option> { + match var { + VariableType::Local(var_num) => self.vars_local.get(*var_num).map(|v| v.as_ref()), + VariableType::Match(var_num) => self.vars_match.get(*var_num).map(|v| v.as_ref()), + VariableType::Global(var_name) => { + self.vars_global.get(var_name.as_str()).map(|v| v.as_ref()) + } + VariableType::Environment(var_name) => self + .vars_env + .get(var_name.as_str()) + .or_else(|| self.runtime.environment.get(var_name.as_str())) + .map(|v| v.as_ref()), + VariableType::Envelope(envelope) => self.envelope.iter().find_map(|(e, v)| { + if e == envelope { + Some(v.as_ref()) + } else { + None + } + }), + VariableType::Header(header) => self.eval_header(header), + VariableType::Part(part) => match part { + MessagePart::TextBody(convert) => { + let part = self.message.parts.get(*self.message.text_body.first()?)?; + match &part.body { + PartType::Text(text) => Some(text.as_ref().into()), + PartType::Html(html) if *convert => { + Some(html_to_text(html.as_ref()).into()) + } + _ => None, + } + } + MessagePart::HtmlBody(convert) => { + let part = self.message.parts.get(*self.message.html_body.first()?)?; + match &part.body { + PartType::Html(html) => Some(html.as_ref().into()), + PartType::Text(text) if *convert => { + Some(text_to_html(text.as_ref()).into()) + } + _ => None, + } + } + MessagePart::Contents => match &self.message.parts.get(self.part)?.body { + PartType::Text(text) | PartType::Html(text) => { + Variable::from(text.as_ref()).into() + } + PartType::Binary(bin) | PartType::InlineBinary(bin) => { + Variable::from(String::from_utf8_lossy(bin.as_ref())).into() + } + _ => None, + }, + MessagePart::Raw => { + let part = self.message.parts.get(self.part)?; + self.message + .raw_message() + .get(part.raw_body_offset()..part.raw_end_offset()) + .map(|v| Variable::from(String::from_utf8_lossy(v))) + } + }, + } + } + + pub(crate) fn eval_value<'z: 'y, 'y>(&'z self, string: &'y Value) -> Variable<'y> { + match string { + Value::Text(text) => Variable::String(text.into()), + Value::Variable(var) => self.variable(var).unwrap_or_default(), + Value::List(list) => { + let mut data = String::new(); + for item in list { + match item { + Value::Text(string) => { + data.push_str(string); + } + Value::Variable(var) => { + if let Some(value) = self.variable(var) { + data.push_str(&value.to_cow()); + } + } + Value::List(_) => { + debug_assert!(false, "This should not have happened: {string:?}"); + } + Value::Number(n) => { + data.push_str(&n.to_string()); + } + Value::Expression(expr) => { + if let Some(value) = self.eval_expression(expr) { + data.push_str(&value.to_string()); + } + } + Value::Regex(_) => (), + } + } + data.into() + } + Value::Number(n) => Variable::from(*n), + Value::Expression(expr) => self.eval_expression(expr).unwrap_or(Variable::default()), + Value::Regex(r) => Variable::StringRef(&r.expr), + } + } + + fn eval_header<'z: 'x>(&'z self, header: &HeaderVariable) -> Option> { + let mut result = Vec::new(); + let part = self.message.part(self.part)?; + let raw = self.message.raw_message(); + if !header.name.is_empty() { + let mut headers = part + .headers + .iter() + .filter(|h| header.name.contains(&h.name)); + match header.index_hdr.cmp(&0) { + Ordering::Greater => { + if let Some(h) = headers.nth((header.index_hdr - 1) as usize) { + header.eval_part(h, raw, &mut result); + } + } + Ordering::Less => { + if let Some(h) = headers + .rev() + .nth((header.index_hdr.unsigned_abs() - 1) as usize) + { + header.eval_part(h, raw, &mut result); + } + } + Ordering::Equal => { + for h in headers { + header.eval_part(h, raw, &mut result); + } + } + } + } else { + for h in &part.headers { + match &header.part { + HeaderPart::Raw => { + if let Some(var) = raw + .get(h.offset_field..h.offset_end) + .map(sanitize_raw_header) + { + result.push(Variable::from(var)); + } + } + HeaderPart::Text => { + if let HeaderValue::Text(text) = &h.value { + result.push(Variable::from(format!("{}: {}", h.name.as_str(), text))); + } else if let HeaderValue::Text(text) = + MessageStream::new(raw.get(h.offset_start..h.offset_end).unwrap_or(b"")) + .parse_unstructured() + { + result.push(Variable::from(format!("{}: {}", h.name.as_str(), text))); + } + } + _ => { + header.eval_part(h, raw, &mut result); + } + } + } + } + + match result.len() { + 1 => result.pop(), + 0 => None, + _ => Some(Variable::Array(result)), + } + } + + #[inline(always)] + pub(crate) fn eval_values<'z: 'y, 'y>(&'z self, strings: &'y [Value]) -> Vec> { + strings.iter().map(|s| self.eval_value(s)).collect() + } + + #[inline(always)] + pub(crate) fn eval_values_owned(&self, strings: &[Value]) -> Vec { + strings + .iter() + .map(|s| self.eval_value(s).into_cow().into_owned()) + .collect() + } + + pub(crate) fn eval_plugin_arguments(&self, plugin: &Plugin) -> Event { + let mut arguments = Vec::with_capacity(plugin.arguments.len()); + for argument in &plugin.arguments { + arguments.push(match argument { + PluginArgument::Tag(tag) => PluginArgument::Tag(*tag), + PluginArgument::Text(t) => PluginArgument::Text(self.eval_value(t).into_string()), + PluginArgument::Number(n) => PluginArgument::Number(self.eval_value(n).to_number()), + PluginArgument::Regex(r) => PluginArgument::Regex(r.clone()), + PluginArgument::Array(a) => { + let mut arr = Vec::with_capacity(a.len()); + for item in a { + arr.push(match item { + PluginArgument::Tag(tag) => PluginArgument::Tag(*tag), + PluginArgument::Text(t) => { + PluginArgument::Text(self.eval_value(t).into_string()) + } + PluginArgument::Number(n) => { + PluginArgument::Number(self.eval_value(n).to_number()) + } + PluginArgument::Regex(r) => PluginArgument::Regex(r.clone()), + PluginArgument::Variable(var) => PluginArgument::Variable(var.clone()), + PluginArgument::Array(_) => continue, + }); + } + PluginArgument::Array(arr) + } + PluginArgument::Variable(var) => PluginArgument::Variable(var.clone()), + }); + } + + Event::Plugin { + id: plugin.id, + arguments, + } + } +} + +impl HeaderVariable { + fn eval_part<'x>(&self, header: &'x Header<'x>, raw: &'x [u8], result: &mut Vec>) { + let var = match &self.part { + HeaderPart::Text => match &header.value { + HeaderValue::Text(v) if self.include_single_part() => { + Some(Variable::from(v.as_ref())) + } + HeaderValue::TextList(list) => match self.index_part.cmp(&0) { + Ordering::Greater => list + .get((self.index_part - 1) as usize) + .map(|v| Variable::from(v.as_ref())), + Ordering::Less => list + .iter() + .rev() + .nth((self.index_part.unsigned_abs() - 1) as usize) + .map(|v| Variable::from(v.as_ref())), + Ordering::Equal => { + for item in list { + result.push(Variable::from(item.as_ref())); + } + return; + } + }, + HeaderValue::ContentType(ct) => if let Some(st) = &ct.c_subtype { + Variable::from(format!("{}/{}", ct.c_type, st)) + } else { + Variable::from(ct.c_type.as_ref()) + } + .into(), + HeaderValue::Address(list) => { + let mut list = list.iter(); + match self.index_part.cmp(&0) { + Ordering::Greater => list + .nth((self.index_part - 1) as usize) + .map(|a| a.to_text()), + Ordering::Less => list + .rev() + .nth((self.index_part.unsigned_abs() - 1) as usize) + .map(|a| a.to_text()), + Ordering::Equal => { + for item in list { + result.push(item.to_text()); + } + return; + } + } + } + HeaderValue::DateTime(_) => raw + .get(header.offset_start..header.offset_end) + .and_then(|bytes| std::str::from_utf8(bytes).ok()) + .map(|s| s.trim()) + .map(Variable::from), + _ => None, + }, + HeaderPart::Address(addr) => match &header.value { + HeaderValue::Address(list) => { + let mut list = list.iter(); + match self.index_part.cmp(&0) { + Ordering::Greater => list + .nth((self.index_part - 1) as usize) + .and_then(|a| addr.eval(a)) + .map(Variable::from), + Ordering::Less => list + .rev() + .nth((self.index_part.unsigned_abs() - 1) as usize) + .and_then(|a| addr.eval(a)) + .map(Variable::from), + Ordering::Equal => { + for item in list { + if let Some(part) = addr.eval(item) { + result.push(Variable::from(part)); + } + } + return; + } + } + } + _ => None, + }, + HeaderPart::Date => { + if let HeaderValue::DateTime(dt) = &header.value { + Variable::from(dt.to_timestamp()).into() + } else { + raw.get(header.offset_start..header.offset_end) + .and_then(|bytes| match MessageStream::new(bytes).parse_date() { + HeaderValue::DateTime(dt) => Variable::from(dt.to_timestamp()).into(), + _ => None, + }) + } + } + HeaderPart::Id => match &header.name { + HeaderName::MessageId | HeaderName::ResentMessageId => match &header.value { + HeaderValue::Text(id) => Variable::from(id.as_ref()).into(), + HeaderValue::TextList(ids) => { + for id in ids { + result.push(Variable::from(id.as_ref())); + } + return; + } + _ => None, + }, + HeaderName::Other(_) => { + match MessageStream::new( + raw.get(header.offset_start..header.offset_end) + .unwrap_or(b""), + ) + .parse_id() + { + HeaderValue::Text(id) => Variable::from(id).into(), + HeaderValue::TextList(ids) => { + for id in ids { + result.push(Variable::from(id)); + } + return; + } + _ => None, + } + } + _ => None, + }, + + HeaderPart::Raw => raw + .get(header.offset_start..header.offset_end) + .map(sanitize_raw_header) + .map(Variable::from), + _ => match (&header.value, &self.part) { + (HeaderValue::ContentType(ct), HeaderPart::ContentType(part)) => match part { + ContentTypePart::Type => Variable::from(ct.c_type.as_ref()).into(), + ContentTypePart::Subtype => { + ct.c_subtype.as_ref().map(|s| Variable::from(s.as_ref())) + } + ContentTypePart::Attribute(attr) => ct.attributes.as_ref().and_then(|attrs| { + attrs.iter().find_map(|(k, v)| { + if k.eq_ignore_ascii_case(attr) { + Some(Variable::from(v.as_ref())) + } else { + None + } + }) + }), + }, + (HeaderValue::Received(rcvd), HeaderPart::Received(part)) => part.eval(rcvd), + _ => None, + }, + }; + + result.push(var.unwrap_or_default()); + } + + #[inline(always)] + fn include_single_part(&self) -> bool { + [-1, 0, 1].contains(&self.index_part) + } +} + +impl ReceivedPart { + pub fn eval<'x>(&self, rcvd: &'x Received<'x>) -> Option> { + match self { + ReceivedPart::From(from) => rcvd + .from() + .or_else(|| rcvd.helo()) + .and_then(|v| from.to_variable(v)), + ReceivedPart::FromIp => rcvd.from_ip().map(|ip| Variable::from(ip.to_string())), + ReceivedPart::FromIpRev => rcvd.from_iprev().map(Variable::from), + ReceivedPart::By(by) => rcvd.by().and_then(|v: &Host<'_>| by.to_variable(v)), + ReceivedPart::For => rcvd.for_().map(Variable::from), + ReceivedPart::With => rcvd.with().map(|v| Variable::from(v.as_str())), + ReceivedPart::TlsVersion => rcvd.tls_version().map(|v| Variable::from(v.as_str())), + ReceivedPart::TlsCipher => rcvd.tls_cipher().map(Variable::from), + ReceivedPart::Id => rcvd.id().map(Variable::from), + ReceivedPart::Ident => rcvd.ident().map(Variable::from), + ReceivedPart::Via => rcvd.via().map(Variable::from), + ReceivedPart::Date => rcvd.date().map(|d| Variable::from(d.to_timestamp())), + ReceivedPart::DateRaw => rcvd.date().map(|d| Variable::from(d.to_rfc822())), + } + } +} + +trait AddrToText<'x> { + fn to_text<'z: 'x>(&'z self) -> Variable<'x>; +} + +impl<'x> AddrToText<'x> for Addr<'x> { + fn to_text<'z: 'x>(&'z self) -> Variable<'x> { + if let Some(name) = &self.name { + if let Some(address) = &self.address { + Variable::String(format!("{name} <{address}>")) + } else { + Variable::StringRef(name.as_ref()) + } + } else if let Some(address) = &self.address { + Variable::String(format!("<{address}>")) + } else { + Variable::StringRef("") + } + } +} + +impl ReceivedHostname { + fn to_variable<'x>(&self, host: &'x Host<'x>) -> Option> { + match (self, host) { + (ReceivedHostname::Name, Host::Name(name)) => Variable::from(name.as_ref()).into(), + (ReceivedHostname::Ip, Host::IpAddr(ip)) => Variable::from(ip.to_string()).into(), + (ReceivedHostname::Any, _) => Variable::from(host.to_string()).into(), + _ => None, + } + } +} + +pub(crate) trait IntoString: Sized { + fn into_string(self) -> String; +} + +pub(crate) trait ToString: Sized { + fn to_string(&self) -> String; +} + +impl IntoString for Vec { + fn into_string(self) -> String { + String::from_utf8(self) + .unwrap_or_else(|err| String::from_utf8_lossy(err.as_bytes()).into_owned()) + } +} + +fn sanitize_raw_header(bytes: &[u8]) -> String { + let mut result = Vec::with_capacity(bytes.len()); + let mut last_is_space = false; + + for &ch in bytes { + if ch.is_ascii_whitespace() { + last_is_space = true; + } else { + if last_is_space { + if !result.is_empty() { + result.push(b' '); + } + last_is_space = false; + } + result.push(ch); + } + } + + result.into_string() +} diff --git a/melib/src/sieve/runtime/expression.rs b/melib/src/sieve/runtime/expression.rs new file mode 100644 index 00000000..ff640cab --- /dev/null +++ b/melib/src/sieve/runtime/expression.rs @@ -0,0 +1,689 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::{cmp::Ordering, fmt::Display}; + +use crate::sieve::{compiler::Number, runtime::Variable, Context}; + +use crate::sieve::compiler::grammar::expr::{BinaryOperator, Expression, UnaryOperator}; + +impl<'x> Context<'x> { + pub(crate) fn eval_expression<'y: 'x, 'z>( + &'y self, + expr: &'z [Expression], + ) -> Option> { + let mut stack = Vec::with_capacity(expr.len()); + for expr in expr { + match expr { + Expression::Variable(v) => { + stack.push(self.variable(v).unwrap_or_default()); + } + Expression::Number(val) => { + stack.push(Variable::from(*val)); + } + Expression::String(val) => { + stack.push(Variable::from(val.to_string())); + } + Expression::UnaryOperator(op) => { + let value = stack.pop()?; + stack.push(match op { + UnaryOperator::Not => value.op_not(), + UnaryOperator::Minus => value.op_minus(), + }); + } + Expression::BinaryOperator(op) => { + let right = stack.pop()?; + let left = stack.pop()?; + stack.push(match op { + BinaryOperator::Add => left.op_add(right), + BinaryOperator::Subtract => left.op_subtract(right), + BinaryOperator::Multiply => left.op_multiply(right), + BinaryOperator::Divide => left.op_divide(right), + BinaryOperator::And => left.op_and(right), + BinaryOperator::Or => left.op_or(right), + BinaryOperator::Xor => left.op_xor(right), + BinaryOperator::Eq => left.op_eq(right), + BinaryOperator::Ne => left.op_ne(right), + BinaryOperator::Lt => left.op_lt(right), + BinaryOperator::Le => left.op_le(right), + BinaryOperator::Gt => left.op_gt(right), + BinaryOperator::Ge => left.op_ge(right), + }); + } + Expression::Function { id, num_args } => { + let num_args = *num_args as usize; + let mut args = vec![Variable::Integer(0); num_args]; + for arg_num in 0..num_args { + args[num_args - arg_num - 1] = stack.pop()?; + } + stack.push((self.runtime.functions.get(*id as usize)?)(self, args)); + } + } + } + stack.pop() + } +} + +impl<'x> Variable<'x> { + fn op_add(self, other: Variable<'x>) -> Variable<'x> { + match (self, other) { + (Variable::Integer(a), Variable::Integer(b)) => Variable::Integer(a.saturating_add(b)), + (Variable::Float(a), Variable::Float(b)) => Variable::Float(a + b), + (Variable::Integer(i), Variable::Float(f)) + | (Variable::Float(f), Variable::Integer(i)) => Variable::Float(i as f64 + f), + (Variable::Array(mut a), Variable::Array(b)) => { + a.extend(b); + Variable::Array(a) + } + (Variable::ArrayRef(a), Variable::ArrayRef(b)) => { + Variable::Array(a.iter().chain(b).map(|v| v.as_ref()).collect()) + } + (Variable::Array(mut a), Variable::ArrayRef(b)) => { + a.extend(b.iter().map(|v| v.as_ref())); + Variable::Array(a) + } + (Variable::ArrayRef(a), Variable::Array(b)) => { + Variable::Array(a.iter().map(|v| v.as_ref()).chain(b).collect()) + } + (Variable::Array(mut a), b) => { + a.push(b); + Variable::Array(a) + } + (Variable::ArrayRef(a), b) => { + Variable::Array(a.iter().map(|v| v.as_ref()).chain([b]).collect()) + } + (a, Variable::Array(mut b)) => { + b.insert(0, a); + Variable::Array(b) + } + (a, Variable::ArrayRef(b)) => Variable::Array( + [a].into_iter() + .chain(b.iter().map(|v| v.as_ref())) + .collect(), + ), + (Variable::String(a), b) => { + if !a.is_empty() { + Variable::String(format!("{}{}", a, b)) + } else { + b + } + } + (a, Variable::String(b)) => { + if !b.is_empty() { + Variable::String(format!("{}{}", a, b)) + } else { + a + } + } + (Variable::StringRef(a), b) => { + if !a.is_empty() { + Variable::String(format!("{}{}", a, b)) + } else { + b + } + } + (a, Variable::StringRef(b)) => { + if !b.is_empty() { + Variable::String(format!("{}{}", a, b)) + } else { + a + } + } + } + } + + fn op_subtract(self, other: Variable<'x>) -> Variable<'x> { + match (self, other) { + (Variable::Integer(a), Variable::Integer(b)) => Variable::Integer(a.saturating_sub(b)), + (Variable::Float(a), Variable::Float(b)) => Variable::Float(a - b), + (Variable::Integer(a), Variable::Float(b)) => Variable::Float(a as f64 - b), + (Variable::Float(a), Variable::Integer(b)) => Variable::Float(a - b as f64), + (Variable::Array(mut a), b) | (b, Variable::Array(mut a)) => { + a.retain(|v| *v != b); + Variable::Array(a) + } + (a, b) => a.parse_number().op_subtract(b.parse_number()), + } + } + + fn op_multiply(self, other: Variable<'x>) -> Variable<'x> { + match (self, other) { + (Variable::Integer(a), Variable::Integer(b)) => Variable::Integer(a.saturating_mul(b)), + (Variable::Float(a), Variable::Float(b)) => Variable::Float(a * b), + (Variable::Integer(i), Variable::Float(f)) + | (Variable::Float(f), Variable::Integer(i)) => Variable::Float(i as f64 * f), + (a, b) => a.parse_number().op_multiply(b.parse_number()), + } + } + + fn op_divide(self, other: Variable<'x>) -> Variable<'x> { + match (self, other) { + (Variable::Integer(a), Variable::Integer(b)) => { + Variable::Float(if b != 0 { a as f64 / b as f64 } else { 0.0 }) + } + (Variable::Float(a), Variable::Float(b)) => { + Variable::Float(if b != 0.0 { a / b } else { 0.0 }) + } + (Variable::Integer(a), Variable::Float(b)) => { + Variable::Float(if b != 0.0 { a as f64 / b } else { 0.0 }) + } + (Variable::Float(a), Variable::Integer(b)) => { + Variable::Float(if b != 0 { a / b as f64 } else { 0.0 }) + } + (a, b) => a.parse_number().op_divide(b.parse_number()), + } + } + + fn op_and(self, other: Variable<'x>) -> Variable<'x> { + Variable::Integer(i64::from(self.to_bool() & other.to_bool())) + } + + fn op_or(self, other: Variable<'x>) -> Variable<'x> { + Variable::Integer(i64::from(self.to_bool() | other.to_bool())) + } + + fn op_xor(self, other: Variable<'x>) -> Variable<'x> { + Variable::Integer(i64::from(self.to_bool() ^ other.to_bool())) + } + + fn op_eq(self, other: Variable<'x>) -> Variable<'x> { + Variable::Integer(i64::from(self == other)) + } + + fn op_ne(self, other: Variable<'x>) -> Variable<'x> { + Variable::Integer(i64::from(self != other)) + } + + fn op_lt(self, other: Variable<'x>) -> Variable<'x> { + Variable::Integer(i64::from(self < other)) + } + + fn op_le(self, other: Variable<'x>) -> Variable<'x> { + Variable::Integer(i64::from(self <= other)) + } + + fn op_gt(self, other: Variable<'x>) -> Variable<'x> { + Variable::Integer(i64::from(self > other)) + } + + fn op_ge(self, other: Variable<'x>) -> Variable<'x> { + Variable::Integer(i64::from(self >= other)) + } + + fn op_not(self) -> Variable<'x> { + Variable::Integer(i64::from(!self.to_bool())) + } + + fn op_minus(self) -> Variable<'x> { + match self { + Variable::Integer(n) => Variable::Integer(-n), + Variable::Float(n) => Variable::Float(-n), + _ => self.parse_number().op_minus(), + } + } + + pub fn parse_number(&self) -> Variable<'x> { + match self { + Variable::String(s) if !s.is_empty() => { + if let Ok(n) = s.parse::() { + Variable::Integer(n) + } else if let Ok(n) = s.parse::() { + Variable::Float(n) + } else { + Variable::Integer(0) + } + } + Variable::StringRef(s) if !s.is_empty() => { + if let Ok(n) = s.parse::() { + Variable::Integer(n) + } else if let Ok(n) = s.parse::() { + Variable::Float(n) + } else { + Variable::Integer(0) + } + } + Variable::Integer(n) => Variable::Integer(*n), + Variable::Float(n) => Variable::Float(*n), + Variable::Array(l) => Variable::Integer(l.is_empty() as i64), + _ => Variable::Integer(0), + } + } + + pub fn to_bool(&self) -> bool { + match self { + Variable::Float(f) => *f != 0.0, + Variable::Integer(n) => *n != 0, + Variable::String(s) => !s.is_empty(), + Variable::StringRef(s) => !s.is_empty(), + Variable::Array(a) => !a.is_empty(), + Variable::ArrayRef(a) => !a.is_empty(), + } + } +} + +impl<'x> PartialEq for Variable<'x> { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Integer(a), Self::Integer(b)) => a == b, + (Self::Float(a), Self::Float(b)) => a == b, + (Self::Integer(a), Self::Float(b)) | (Self::Float(b), Self::Integer(a)) => { + *a as f64 == *b + } + (Self::String(a), Self::String(b)) => a == b, + (Self::StringRef(a), Self::StringRef(b)) => a == b, + (Self::String(a), Self::StringRef(b)) | (Self::StringRef(b), Self::String(a)) => a == b, + (Self::String(_) | Self::StringRef(_), Self::Integer(_) | Self::Float(_)) => { + &self.parse_number() == other + } + (Self::Integer(_) | Self::Float(_), Self::String(_) | Self::StringRef(_)) => { + self == &other.parse_number() + } + (Self::Array(a), Self::Array(b)) => a == b, + (Self::ArrayRef(a), Self::ArrayRef(b)) => a == b, + (Self::Array(a), Self::ArrayRef(b)) | (Self::ArrayRef(b), Self::Array(a)) => a == *b, + _ => false, + } + } +} + +impl Eq for Variable<'_> {} + +impl<'x> PartialOrd for Variable<'x> { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (Self::Integer(a), Self::Integer(b)) => a.partial_cmp(b), + (Self::Float(a), Self::Float(b)) => a.partial_cmp(b), + (Self::Integer(a), Self::Float(b)) => (*a as f64).partial_cmp(b), + (Self::Float(a), Self::Integer(b)) => a.partial_cmp(&(*b as f64)), + (Self::String(a), Self::String(b)) => a.partial_cmp(b), + (Self::StringRef(a), Self::StringRef(b)) => a.partial_cmp(b), + (Self::String(a), Self::StringRef(b)) => a.as_str().partial_cmp(*b), + (Self::StringRef(a), Self::String(b)) => a.partial_cmp(&b.as_str()), + (Self::String(_) | Self::StringRef(_), Self::Integer(_) | Self::Float(_)) => { + self.parse_number().partial_cmp(other) + } + (Self::Integer(_) | Self::Float(_), Self::String(_) | Self::StringRef(_)) => { + self.partial_cmp(&other.parse_number()) + } + (Self::Array(a), Self::Array(b)) => a.partial_cmp(b), + (Self::ArrayRef(a), Self::ArrayRef(b)) => a.partial_cmp(b), + (Self::Array(a), Self::ArrayRef(b)) => a.partial_cmp(b), + (Self::ArrayRef(a), Self::Array(b)) => a.partial_cmp(&b), + (Self::Array(_) | Self::ArrayRef(_) | Self::String(_) | Self::StringRef(_), _) => { + Ordering::Greater.into() + } + (_, Self::Array(_) | Self::ArrayRef(_)) => Ordering::Less.into(), + } + } +} + +impl<'x> Ord for Variable<'x> { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.partial_cmp(other).unwrap_or(Ordering::Greater) + } +} + +impl Display for Variable<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Variable::String(v) => v.fmt(f), + Variable::StringRef(v) => v.fmt(f), + Variable::Integer(v) => v.fmt(f), + Variable::Float(v) => v.fmt(f), + Variable::Array(v) => { + for (i, v) in v.iter().enumerate() { + if i > 0 { + f.write_str("\n")?; + } + v.fmt(f)?; + } + Ok(()) + } + Variable::ArrayRef(v) => { + for (i, v) in v.iter().enumerate() { + if i > 0 { + f.write_str("\n")?; + } + v.fmt(f)?; + } + Ok(()) + } + } + } +} + +impl Number { + pub fn is_non_zero(&self) -> bool { + match self { + Number::Integer(n) => *n != 0, + Number::Float(n) => *n != 0.0, + } + } +} + +impl Default for Number { + fn default() -> Self { + Number::Integer(0) + } +} + +trait IntoBool { + fn into_bool(self) -> bool; +} + +impl IntoBool for f64 { + #[inline(always)] + fn into_bool(self) -> bool { + self != 0.0 + } +} + +impl IntoBool for i64 { + #[inline(always)] + fn into_bool(self) -> bool { + self != 0 + } +} + +impl From for Number { + #[inline(always)] + fn from(b: bool) -> Self { + Number::Integer(i64::from(b)) + } +} + +impl From for Number { + #[inline(always)] + fn from(n: i64) -> Self { + Number::Integer(n) + } +} + +impl From for Number { + #[inline(always)] + fn from(n: f64) -> Self { + Number::Float(n) + } +} + +impl From for Number { + #[inline(always)] + fn from(n: i32) -> Self { + Number::Integer(n as i64) + } +} + +#[cfg(test)] +mod test { + use ahash::{HashMap, HashMapExt}; + + use crate::sieve::{ + compiler::{ + grammar::expr::{ + parser::ExpressionParser, tokenizer::Tokenizer, BinaryOperator, Expression, Token, + UnaryOperator, + }, + VariableType, + }, + runtime::Variable, + }; + + use evalexpr::*; + + pub trait EvalExpression { + fn eval(&self, variables: &HashMap) -> Option; + } + + impl EvalExpression for Vec { + fn eval(&self, variables: &HashMap) -> Option { + let mut stack = Vec::with_capacity(self.len()); + for expr in self.iter() { + match expr { + Expression::Variable(VariableType::Global(v)) => { + stack.push(variables.get(v)?.as_ref().into_owned()); + } + Expression::Number(val) => { + stack.push(Variable::from(*val)); + } + Expression::String(val) => { + stack.push(Variable::from(val.to_string())); + } + Expression::UnaryOperator(op) => { + let value = stack.pop()?; + stack.push(match op { + UnaryOperator::Not => value.op_not(), + UnaryOperator::Minus => value.op_minus(), + }); + } + Expression::BinaryOperator(op) => { + let right = stack.pop()?; + let left = stack.pop()?; + stack.push(match op { + BinaryOperator::Add => left.op_add(right), + BinaryOperator::Subtract => left.op_subtract(right), + BinaryOperator::Multiply => left.op_multiply(right), + BinaryOperator::Divide => left.op_divide(right), + BinaryOperator::And => left.op_and(right), + BinaryOperator::Or => left.op_or(right), + BinaryOperator::Xor => left.op_xor(right), + BinaryOperator::Eq => left.op_eq(right), + BinaryOperator::Ne => left.op_ne(right), + BinaryOperator::Lt => left.op_lt(right), + BinaryOperator::Le => left.op_le(right), + BinaryOperator::Gt => left.op_gt(right), + BinaryOperator::Ge => left.op_ge(right), + }); + } + _ => unreachable!("Invalid expression"), + } + } + stack.pop() + } + } + + #[test] + fn eval_expression() { + let mut variables = HashMap::from_iter([ + ("A".to_string(), Variable::Integer(0)), + ("B".to_string(), Variable::Integer(0)), + ("C".to_string(), Variable::Integer(0)), + ("D".to_string(), Variable::Integer(0)), + ("E".to_string(), Variable::Integer(0)), + ("F".to_string(), Variable::Integer(0)), + ("G".to_string(), Variable::Integer(0)), + ("H".to_string(), Variable::Integer(0)), + ("I".to_string(), Variable::Integer(0)), + ("J".to_string(), Variable::Integer(0)), + ]); + let num_vars = variables.len(); + + for expr in [ + "A + B", + "A * B", + "A / B", + "A - B", + "-A", + "A == B", + "A != B", + "A > B", + "A < B", + "A >= B", + "A <= B", + "A + B * C - D / E", + "A + B + C - D - E", + "(A + B) * (C - D) / E", + "A - B + C * D / E * F - G", + "A + B * C - D / E", + "(A + B) * (C - D) / E", + "A - B + C / D * E", + "(A + B) / (C - D) + E", + "A * (B + C) - D / E", + "A / (B - C + D) * E", + "(A + B) * C - D / (E + F)", + "A * B - C + D / E", + "A + B - C * D / E", + "(A * B + C) / D - E", + "A - B / C + D * E", + "A + B * (C - D) / E", + "A * B / C + (D - E)", + "(A - B) * C / D + E", + "A * (B / C) - D + E", + "(A + B) / (C + D) * E", + "A - B * C / D + E", + "A + (B - C) * D / E", + "(A + B) * (C / D) - E", + "A - B / (C * D) + E", + "(A + B) > (C - D) && E <= F", + "A * B == C / D || E - F != G + H", + "A / B >= C * D && E + F < G - H", + "(A * B - C) != (D / E + F) && G > H", + "A - B < C && D + E >= F * G", + "(A * B) > C && (D / E) < F || G == H", + "(A + B) <= (C - D) || E > F && G != H", + "A * B != C + D || E - F == G / H", + "A >= B * C && D < E - F || G != H + I", + "(A / B + C) > D && E * F <= G - H", + "A * (B - C) == D && E / F > G + H", + "(A - B + C) != D || E * F >= G && H < I", + "A < B / C && D + E * F == G - H", + "(A + B * C) <= D && E > F / G", + "(A * B - C) > D || E <= F + G && H != I", + "A != B / C && D == E * F - G", + "A <= B + C - D && E / F > G * H", + "(A - B * C) < D || E >= F + G && H != I", + "(A + B) / C == D && E - F < G * H", + "A * B != C && D >= E + F / G || H < I", + "!(A * B != C) && !(D >= E + F / G) || !(H < I)", + "-A - B - (- C - D) - E - (-F)", + ] { + for (pos, v) in variables.values_mut().enumerate() { + *v = Variable::Integer(pos as i64 + 1); + } + + assert_expr(expr, &variables); + + for (pos, v) in variables.values_mut().enumerate() { + *v = Variable::Integer((num_vars - pos) as i64); + } + + assert_expr(expr, &variables); + } + + for expr in [ + "true && false", + "!true || false", + "true && !false", + "!(true && false)", + "true || true && false", + "!false && (true || false)", + "!(true || !false) && true", + "!(!true && !false)", + "true || false && !true", + "!(true && true) || !false", + "!(!true || !false) && (!false) && !(!true)", + ] { + let pexp = parse_expression(expr.replace("true", "1").replace("false", "0").as_str()); + let result = pexp.eval(&HashMap::new()).unwrap(); + + //println!("{} => {:?}", expr, result); + + match (eval(expr).expect(expr), result) { + (Value::Float(a), Variable::Float(b)) if a == b => (), + (Value::Float(a), Variable::Integer(b)) if a == b as f64 => (), + (Value::Boolean(a), Variable::Integer(b)) if a == (b != 0) => (), + (a, b) => { + panic!("{} => {:?} != {:?}", expr, a, b) + } + } + } + } + + fn assert_expr(expr: &str, variables: &HashMap) { + let e = parse_expression(expr); + + let result = e.eval(variables).unwrap(); + + let mut str_expr = expr.to_string(); + let mut str_expr_float = expr.to_string(); + for (k, v) in variables { + let v = v.to_string(); + + if v.contains('.') { + str_expr_float = str_expr_float.replace(k, &v); + } else { + str_expr_float = str_expr_float.replace(k, &format!("{}.0", v)); + } + str_expr = str_expr.replace(k, &v); + } + + assert_eq!( + parse_expression(&str_expr) + .eval(&HashMap::new()) + .unwrap() + .to_number() + .to_float(), + result.to_number().to_float() + ); + + assert_eq!( + parse_expression(&str_expr_float) + .eval(&HashMap::new()) + .unwrap() + .to_number() + .to_float(), + result.to_number().to_float() + ); + + //println!("{str_expr} ({e:?}) => {result:?}"); + + match ( + eval(&str_expr_float) + .map(|v| { + // Divisions by zero are converted to 0.0 + if matches!(&v, Value::Float(f) if f.is_infinite()) { + Value::Float(0.0) + } else { + v + } + }) + .expect(&str_expr), + result, + ) { + (Value::Float(a), Variable::Float(b)) if a == b => (), + (Value::Float(a), Variable::Integer(b)) if a == b as f64 => (), + (Value::Boolean(a), Variable::Integer(b)) if a == (b != 0) => (), + (a, b) => { + panic!("{} => {:?} != {:?}", str_expr, a, b) + } + } + } + + fn parse_expression(expr: &str) -> Vec { + ExpressionParser::from_tokenizer(Tokenizer::new(expr, |var_name: &str, _: bool| { + Ok::<_, String>(Token::Variable(VariableType::Global(var_name.to_string()))) + })) + .parse() + .unwrap() + .output + } +} diff --git a/melib/src/sieve/runtime/mod.rs b/melib/src/sieve/runtime/mod.rs new file mode 100644 index 00000000..ecf637c3 --- /dev/null +++ b/melib/src/sieve/runtime/mod.rs @@ -0,0 +1,871 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * This file is part of the Stalwart Sieve Interpreter. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +pub mod actions; +pub mod context; +pub mod eval; +pub mod expression; +pub mod serialize; +pub mod tests; +pub mod variables; + +pub(self) use std::iter::FromIterator; +pub(self) use std::iter::IntoIterator; +use std::{borrow::Cow, fmt::Display, ops::Deref, sync::Arc}; + +use ahash::{AHashMap, AHashSet}; +use mail_parser::{Encoding, HeaderName, Message, MessageParser, MessagePart, PartType}; + +use crate::sieve::{ + compiler::{ + grammar::{Capability, Invalid}, + Number, Regex, VariableType, + }, + Context, Function, FunctionMap, Input, Metadata, PluginArgument, Runtime, Script, SetVariable, + Sieve, +}; + +use self::eval::ToString; + +#[derive(Debug, Clone)] +pub enum Variable<'x> { + String(String), + StringRef(&'x str), + Integer(i64), + Float(f64), + Array(Vec>), + ArrayRef(&'x Vec>), +} + +#[derive(Debug)] +pub enum RuntimeError { + TooManyIncludes, + InvalidInstruction(Invalid), + ScriptErrorMessage(String), + CapabilityNotAllowed(Capability), + CapabilityNotSupported(String), + CPULimitReached, +} + +impl<'x> Default for Variable<'x> { + fn default() -> Self { + Variable::StringRef("") + } +} + +impl<'x> Variable<'x> { + pub fn into_cow(self) -> Cow<'x, str> { + match self { + Variable::String(s) => Cow::Owned(s), + Variable::StringRef(s) => Cow::Borrowed(s), + Variable::Integer(n) => Cow::Owned(n.to_string()), + Variable::Float(n) => Cow::Owned(n.to_string()), + Variable::Array(l) => Cow::Owned(l.to_string()), + Variable::ArrayRef(l) => Cow::Owned(l.to_string()), + } + } + + pub fn to_cow<'y: 'x>(&'y self) -> Cow<'x, str> { + match self { + Variable::String(s) => Cow::Borrowed(s.as_str()), + Variable::StringRef(s) => Cow::Borrowed(*s), + Variable::Integer(n) => Cow::Owned(n.to_string()), + Variable::Float(n) => Cow::Owned(n.to_string()), + Variable::Array(l) => Cow::Owned(l.to_string()), + Variable::ArrayRef(l) => Cow::Owned(l.to_string()), + } + } + + pub fn into_string(self) -> String { + match self { + Variable::String(s) => s, + Variable::StringRef(s) => s.to_string(), + Variable::Integer(n) => n.to_string(), + Variable::Float(n) => n.to_string(), + Variable::Array(l) => l.to_string(), + Variable::ArrayRef(l) => l.to_string(), + } + } + + pub fn to_number(&self) -> Number { + self.to_number_checked() + .unwrap_or(Number::Float(f64::INFINITY)) + } + + pub fn to_number_checked(&self) -> Option { + let s = match self { + Variable::Integer(n) => return Number::Integer(*n).into(), + Variable::Float(n) => return Number::Float(*n).into(), + Variable::String(s) if !s.is_empty() => s.as_str(), + Variable::StringRef(s) if !s.is_empty() => *s, + _ => return None, + }; + + if !s.contains('.') { + s.parse::().map(Number::Integer).ok() + } else { + s.parse::().map(Number::Float).ok() + } + } + + pub fn to_integer(&self) -> i64 { + match self { + Variable::Integer(n) => *n, + Variable::Float(n) => *n as i64, + Variable::String(s) if !s.is_empty() => s.parse::().unwrap_or(0), + Variable::StringRef(s) if !s.is_empty() => s.parse::().unwrap_or(0), + _ => 0, + } + } + + pub fn len(&self) -> usize { + match self { + Variable::String(s) => s.len(), + Variable::StringRef(s) => s.len(), + Variable::Integer(_) | Variable::Float(_) => 2, + Variable::Array(l) => l.iter().map(|v| v.len() + 2).sum(), + Variable::ArrayRef(l) => l.iter().map(|v| v.len() + 2).sum(), + } + } + + pub fn is_empty(&self) -> bool { + match self { + Variable::String(s) => s.is_empty(), + Variable::StringRef(s) => s.is_empty(), + _ => false, + } + } + + pub fn to_owned(&self) -> Variable<'static> { + match self { + Variable::String(s) => Variable::String(s.to_string()), + Variable::StringRef(s) => Variable::String(s.to_string()), + Variable::Integer(n) => Variable::Integer(*n), + Variable::Float(n) => Variable::Float(*n), + Variable::Array(l) => Variable::Array(l.iter().map(Variable::to_owned).collect()), + Variable::ArrayRef(l) => Variable::Array(l.iter().map(Variable::to_owned).collect()), + } + } + + pub fn into_owned(self) -> Variable<'static> { + match self { + Variable::String(s) => Variable::String(s), + Variable::StringRef(s) => Variable::String(s.to_string()), + Variable::Integer(n) => Variable::Integer(n), + Variable::Float(n) => Variable::Float(n), + Variable::Array(l) => { + Variable::Array(l.into_iter().map(Variable::into_owned).collect()) + } + Variable::ArrayRef(l) => Variable::Array(l.iter().map(Variable::to_owned).collect()), + } + } + + pub fn as_ref<'y: 'x>(&'y self) -> Variable<'x> { + match self { + Variable::String(s) => Variable::StringRef(s.as_str()), + Variable::StringRef(s) => Variable::StringRef(s), + Variable::Integer(n) => Variable::Integer(*n), + Variable::Float(n) => Variable::Float(*n), + Variable::Array(l) => Variable::ArrayRef(l), + Variable::ArrayRef(l) => Variable::ArrayRef(l), + } + } +} + +impl<'x> From<&'x str> for Variable<'x> { + fn from(s: &'x str) -> Self { + Variable::StringRef(s) + } +} + +impl From for Variable<'_> { + fn from(s: String) -> Self { + Variable::String(s) + } +} + +impl<'x> From<&'x String> for Variable<'x> { + fn from(s: &'x String) -> Self { + Variable::StringRef(s.as_str()) + } +} + +impl<'x> From> for Variable<'x> { + fn from(s: Cow<'x, str>) -> Self { + match s { + Cow::Borrowed(s) => Variable::StringRef(s), + Cow::Owned(s) => Variable::String(s), + } + } +} + +impl<'x> From>> for Variable<'x> { + fn from(l: Vec>) -> Self { + Variable::Array(l) + } +} + +impl From for Variable<'_> { + fn from(n: Number) -> Self { + match n { + Number::Integer(n) => Variable::Integer(n), + Number::Float(n) => Variable::Float(n), + } + } +} + +impl From for Variable<'_> { + fn from(n: usize) -> Self { + Variable::Integer(n as i64) + } +} + +impl From for Variable<'_> { + fn from(n: i64) -> Self { + Variable::Integer(n) + } +} + +impl From for Variable<'_> { + fn from(n: u64) -> Self { + Variable::Integer(n as i64) + } +} + +impl From for Variable<'_> { + fn from(n: f64) -> Self { + Variable::Float(n) + } +} + +impl From for Variable<'_> { + fn from(n: i32) -> Self { + Variable::Integer(n as i64) + } +} + +impl From for Variable<'_> { + fn from(b: bool) -> Self { + Variable::Integer(i64::from(b)) + } +} + +impl PartialEq for Number { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Integer(a), Self::Integer(b)) => a == b, + (Self::Float(a), Self::Float(b)) => a == b, + (Self::Integer(a), Self::Float(b)) => (*a as f64) == *b, + (Self::Float(a), Self::Integer(b)) => *a == (*b as f64), + } + } +} + +impl Eq for Number {} + +impl PartialOrd for Number { + fn partial_cmp(&self, other: &Self) -> Option { + let (a, b) = match (self, other) { + (Number::Integer(a), Number::Integer(b)) => return a.partial_cmp(b), + (Number::Float(a), Number::Float(b)) => (*a, *b), + (Number::Integer(a), Number::Float(b)) => (*a as f64, *b), + (Number::Float(a), Number::Integer(b)) => (*a, *b as f64), + }; + a.partial_cmp(&b) + } +} + +impl<'x> self::eval::ToString for Vec> { + fn to_string(&self) -> String { + let mut result = String::with_capacity(self.len() * 10); + for item in self { + if !result.is_empty() { + result.push_str("\r\n"); + } + match item { + Variable::String(v) => result.push_str(v), + Variable::StringRef(v) => result.push_str(v), + Variable::Integer(v) => result.push_str(&v.to_string()), + Variable::Float(v) => result.push_str(&v.to_string()), + Variable::Array(_) | Variable::ArrayRef(_) => {} + } + } + result + } +} + +impl PluginArgument { + pub fn unwrap_string(self) -> Option { + match self { + PluginArgument::Text(s) => s.into(), + PluginArgument::Number(n) => n.to_string().into(), + _ => None, + } + } + + pub fn unwrap_number(self) -> Option { + match self { + PluginArgument::Number(n) => n.into(), + _ => None, + } + } + + pub fn unwrap_regex(self) -> Option { + match self { + PluginArgument::Regex(r) => r.into(), + _ => None, + } + } + + pub fn unwrap_array(self) -> Option> { + match self { + PluginArgument::Array(a) => a.into(), + _ => None, + } + } + + pub fn unwrap_variable(self) -> Option { + match self { + PluginArgument::Variable(v) => v.into(), + _ => None, + } + } + + pub fn unwrap_string_array(self) -> Option> { + match self { + PluginArgument::Array(a) => a + .into_iter() + .filter_map(Self::unwrap_string) + .collect::>() + .into(), + _ => None, + } + } + + pub fn unwrap_number_array(self) -> Option> { + match self { + PluginArgument::Array(a) => a + .into_iter() + .filter_map(Self::unwrap_number) + .collect::>() + .into(), + _ => None, + } + } + + pub fn unwrap_regex_array(self) -> Option> { + match self { + PluginArgument::Array(a) => a + .into_iter() + .filter_map(Self::unwrap_regex) + .collect::>() + .into(), + _ => None, + } + } + + pub fn unwrap_variable_array(self) -> Option> { + match self { + PluginArgument::Array(a) => a + .into_iter() + .filter_map(Self::unwrap_variable) + .collect::>() + .into(), + _ => None, + } + } +} + +impl Runtime { + pub fn new() -> Self { + #[allow(unused_mut)] + let mut allowed_capabilities = AHashSet::from_iter(Capability::all().iter().cloned()); + + #[cfg(test)] + allowed_capabilities.insert(Capability::Other("vnd.stalwart.testsuite".to_string())); + + Runtime { + allowed_capabilities, + environment: AHashMap::from_iter([ + ("name".into(), "Stalwart Sieve".into()), + ("version".into(), env!("CARGO_PKG_VERSION").into()), + ]), + metadata: Vec::new(), + include_scripts: AHashMap::new(), + max_nested_includes: 3, + cpu_limit: 5000, + max_variable_size: 4096, + max_redirects: 1, + max_received_headers: 10, + protected_headers: vec![ + HeaderName::Other("Original-Subject".into()), + HeaderName::Other("Original-From".into()), + ], + valid_notification_uris: AHashSet::new(), + valid_ext_lists: AHashSet::new(), + vacation_use_orig_rcpt: false, + vacation_default_subject: "Automated reply".into(), + vacation_subject_prefix: "Auto: ".into(), + max_header_size: 1024, + max_out_messages: 3, + default_vacation_expiry: 30 * 86400, + default_duplicate_expiry: 7 * 86400, + local_hostname: "localhost".into(), + functions: Vec::new(), + } + } + + pub fn set_cpu_limit(&mut self, size: usize) { + self.cpu_limit = size; + } + + pub fn with_cpu_limit(mut self, size: usize) -> Self { + self.cpu_limit = size; + self + } + + pub fn set_max_nested_includes(&mut self, size: usize) { + self.max_nested_includes = size; + } + + pub fn with_max_nested_includes(mut self, size: usize) -> Self { + self.max_nested_includes = size; + self + } + + pub fn set_max_redirects(&mut self, size: usize) { + self.max_redirects = size; + } + + pub fn with_max_redirects(mut self, size: usize) -> Self { + self.max_redirects = size; + self + } + + pub fn set_max_out_messages(&mut self, size: usize) { + self.max_out_messages = size; + } + + pub fn with_max_out_messages(mut self, size: usize) -> Self { + self.max_out_messages = size; + self + } + + pub fn set_max_received_headers(&mut self, size: usize) { + self.max_received_headers = size; + } + + pub fn with_max_received_headers(mut self, size: usize) -> Self { + self.max_received_headers = size; + self + } + + pub fn set_max_variable_size(&mut self, size: usize) { + self.max_variable_size = size; + } + + pub fn with_max_variable_size(mut self, size: usize) -> Self { + self.max_variable_size = size; + self + } + + pub fn set_max_header_size(&mut self, size: usize) { + self.max_header_size = size; + } + + pub fn with_max_header_size(mut self, size: usize) -> Self { + self.max_header_size = size; + self + } + + pub fn set_default_vacation_expiry(&mut self, expiry: u64) { + self.default_vacation_expiry = expiry; + } + + pub fn with_default_vacation_expiry(mut self, expiry: u64) -> Self { + self.default_vacation_expiry = expiry; + self + } + + pub fn set_default_duplicate_expiry(&mut self, expiry: u64) { + self.default_duplicate_expiry = expiry; + } + + pub fn with_default_duplicate_expiry(mut self, expiry: u64) -> Self { + self.default_duplicate_expiry = expiry; + self + } + + pub fn set_capability(&mut self, capability: impl Into) { + self.allowed_capabilities.insert(capability.into()); + } + + pub fn with_capability(mut self, capability: impl Into) -> Self { + self.set_capability(capability); + self + } + + pub fn unset_capability(&mut self, capability: impl Into) { + self.allowed_capabilities.remove(&capability.into()); + } + + pub fn without_capability(mut self, capability: impl Into) -> Self { + self.unset_capability(capability); + self + } + + pub fn without_capabilities( + mut self, + capabilities: impl IntoIterator>, + ) -> Self { + for capability in capabilities { + self.allowed_capabilities.remove(&capability.into()); + } + self + } + + pub fn set_protected_header(&mut self, header_name: impl Into>) { + if let Some(header_name) = HeaderName::parse(header_name) { + self.protected_headers.push(header_name); + } + } + + pub fn with_protected_header(mut self, header_name: impl Into>) -> Self { + self.set_protected_header(header_name); + self + } + + pub fn with_protected_headers( + mut self, + header_names: impl IntoIterator>>, + ) -> Self { + self.protected_headers = header_names + .into_iter() + .filter_map(HeaderName::parse) + .collect(); + self + } + + pub fn set_env_variable( + &mut self, + name: impl Into>, + value: impl Into>, + ) { + self.environment.insert(name.into(), value.into()); + } + + pub fn with_env_variable( + mut self, + name: impl Into>, + value: impl Into>, + ) -> Self { + self.set_env_variable(name.into(), value.into()); + self + } + + pub fn set_medatata( + &mut self, + name: impl Into>, + value: impl Into>, + ) { + self.metadata.push((name.into(), value.into())); + } + + pub fn with_metadata( + mut self, + name: impl Into>, + value: impl Into>, + ) -> Self { + self.set_medatata(name, value); + self + } + + pub fn set_valid_notification_uri(&mut self, uri: impl Into>) { + self.valid_notification_uris.insert(uri.into()); + } + + pub fn with_valid_notification_uri(mut self, uri: impl Into>) -> Self { + self.valid_notification_uris.insert(uri.into()); + self + } + + pub fn with_valid_notification_uris( + mut self, + uris: impl IntoIterator>>, + ) -> Self { + self.valid_notification_uris = uris.into_iter().map(Into::into).collect(); + self + } + + pub fn set_valid_ext_list(&mut self, name: impl Into>) { + self.valid_ext_lists.insert(name.into()); + } + + pub fn with_valid_ext_list(mut self, name: impl Into>) -> Self { + self.set_valid_ext_list(name); + self + } + + pub fn set_vacation_use_orig_rcpt(&mut self, value: bool) { + self.vacation_use_orig_rcpt = value; + } + + pub fn with_valid_ext_lists( + mut self, + lists: impl IntoIterator>>, + ) -> Self { + self.valid_ext_lists = lists.into_iter().map(Into::into).collect(); + self + } + + pub fn with_vacation_use_orig_rcpt(mut self, value: bool) -> Self { + self.set_vacation_use_orig_rcpt(value); + self + } + + pub fn set_vacation_default_subject(&mut self, value: impl Into>) { + self.vacation_default_subject = value.into(); + } + + pub fn with_vacation_default_subject(mut self, value: impl Into>) -> Self { + self.set_vacation_default_subject(value); + self + } + + pub fn set_vacation_subject_prefix(&mut self, value: impl Into>) { + self.vacation_subject_prefix = value.into(); + } + + pub fn with_vacation_subject_prefix(mut self, value: impl Into>) -> Self { + self.set_vacation_subject_prefix(value); + self + } + + pub fn set_local_hostname(&mut self, value: impl Into>) { + self.local_hostname = value.into(); + } + + pub fn with_local_hostname(mut self, value: impl Into>) -> Self { + self.set_local_hostname(value); + self + } + + pub fn with_functions(mut self, fnc_map: &mut FunctionMap) -> Self { + self.functions = std::mem::take(&mut fnc_map.functions); + self + } + + pub fn set_functions(&mut self, fnc_map: &mut FunctionMap) { + self.functions = std::mem::take(&mut fnc_map.functions); + } + + pub fn filter<'z: 'x, 'x>(&'z self, raw_message: &'x [u8]) -> Context<'x> { + Context::new( + self, + MessageParser::new() + .parse(raw_message) + .unwrap_or_else(|| Message { + parts: vec![MessagePart { + headers: vec![], + is_encoding_problem: false, + body: PartType::Text("".into()), + encoding: Encoding::None, + offset_header: 0, + offset_body: 0, + offset_end: 0, + }], + raw_message: b""[..].into(), + ..Default::default() + }), + ) + } + + pub fn filter_parsed<'z: 'x, 'x>(&'z self, message: Message<'x>) -> Context<'x> { + Context::new(self, message) + } +} + +impl FunctionMap { + pub fn new() -> Self { + Self::default() + } + + pub fn with_function(self, name: impl Into, fnc: Function) -> Self { + self.with_function_args(name, fnc, 1) + } + + pub fn with_function_no_args(self, name: impl Into, fnc: Function) -> Self { + self.with_function_args(name, fnc, 0) + } + + pub fn with_function_args( + mut self, + name: impl Into, + fnc: Function, + num_args: u32, + ) -> Self { + self.map + .insert(name.into(), (self.functions.len() as u32, num_args)); + self.functions.push(fnc); + self + } +} + +impl Default for Runtime { + fn default() -> Self { + Self::new() + } +} + +impl Input { + pub fn script(name: impl Into