melib: bundle stalwartlabs/sieve into melib
We can't use it as a dependency because it uses external code for mail parsing etc, which would be duplication of functionality since melib does that already. Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>feature/sieve
parent
7eed82783a
commit
6ead906b93
|
@ -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"
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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
|
|
@ -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.
|
|
@ -0,0 +1,28 @@
|
|||
[package]
|
||||
name = "sieve-rs"
|
||||
description = "Sieve filter interpreter for Rust"
|
||||
authors = [ "Stalwart Labs <hello@stalw.art>"]
|
||||
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"
|
|
@ -0,0 +1,661 @@
|
|||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU 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.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
<https://www.gnu.org/licenses/>.
|
|
@ -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 <list-sales@example.org>
|
||||
To: John Doe <jdoe@example.org>
|
||||
List-ID: <sales@example.org>
|
||||
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<String> = 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.
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Value>,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
impl<'x> CompilerState<'x> {
|
||||
pub(crate) fn parse_test_convert(&mut self) -> Result<Test, CompileError> {
|
||||
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(())
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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" <fieldno: number> [":last"]]
|
||||
[COMPARATOR] [MATCH-TYPE]
|
||||
<field-name: string>
|
||||
[<value-patterns: string-list>]
|
||||
|
||||
*/
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub(crate) struct DeleteHeader {
|
||||
pub index: Option<i32>,
|
||||
pub comparator: Comparator,
|
||||
pub match_type: MatchType,
|
||||
pub field_name: Value,
|
||||
pub value_patterns: Vec<Value>,
|
||||
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(())
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Value>,
|
||||
pub mailbox_id: Option<Value>,
|
||||
pub special_use: Option<Value>,
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<VariableType>,
|
||||
pub flags: Vec<Value>,
|
||||
}
|
||||
|
||||
#[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(())
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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"] <value: string>
|
||||
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(())
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Value>,
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Value>,
|
||||
pub from: Option<Value>,
|
||||
pub replacement: Value,
|
||||
pub mime: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct Enclose {
|
||||
pub subject: Option<Value>,
|
||||
pub headers: Vec<Value>,
|
||||
pub value: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct ExtractText {
|
||||
pub modifiers: Vec<Modifier>,
|
||||
pub first: Option<usize>,
|
||||
pub name: VariableType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub(crate) enum MimeOpts<T> {
|
||||
Type,
|
||||
Subtype,
|
||||
ContentType,
|
||||
Param(Vec<T>),
|
||||
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<MimeOpts<Value>, 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,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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]
|
||||
<method: string>
|
||||
|
||||
*/
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub(crate) struct Notify {
|
||||
pub from: Option<Value>,
|
||||
pub importance: Option<Value>,
|
||||
pub options: Vec<Value>,
|
||||
pub message: Option<Value>,
|
||||
pub fcc: Option<FileCarbonCopy<Value>>,
|
||||
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<Value> {
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Value>,
|
||||
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<NotifyItem>),
|
||||
Default,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
||||
pub enum Ret {
|
||||
Full,
|
||||
Hdrs,
|
||||
Default,
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Usage: redirect [:bytimerelative <rlimit: number> /
|
||||
:bytimeabsolute <alimit:string>
|
||||
[:bymode "notify"|"return"] [:bytrace]]
|
||||
<address: string>
|
||||
|
||||
*/
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
||||
pub enum ByTime<T> {
|
||||
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<Value> {
|
||||
fn map_local_vars(&mut self, last_id: usize) {
|
||||
if let ByTime::Absolute { alimit, .. } = self {
|
||||
alimit.map_local_vars(last_id)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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(())
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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: 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(())
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Modifier>,
|
||||
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<VariableType, CompileError> {
|
||||
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<VariableType, ErrorType> {
|
||||
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<Word> 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!(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Value>,
|
||||
pub from: Option<Value>,
|
||||
pub mime: bool,
|
||||
pub fcc: Option<FileCarbonCopy<Value>>,
|
||||
pub reason: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub(crate) struct TestVacation {
|
||||
pub addresses: Vec<Value>,
|
||||
pub period: Period,
|
||||
pub handle: Option<Value>,
|
||||
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] <reason: string>
|
||||
|
||||
vacation [FCC]
|
||||
[":days" number | ":seconds" number]
|
||||
[":subject" string]
|
||||
[":from" string]
|
||||
[":addresses" string-list]
|
||||
[":mime"]
|
||||
[":handle" string]
|
||||
<reason: string>
|
||||
|
||||
":flags" <list-of-flags: string-list>
|
||||
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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;
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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,
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Token, String>,
|
||||
{
|
||||
pub(crate) tokenizer: Tokenizer<'x, F>,
|
||||
pub(crate) output: Vec<Expression>,
|
||||
operator_stack: Vec<Token>,
|
||||
}
|
||||
|
||||
impl<'x, F> ExpressionParser<'x, F>
|
||||
where
|
||||
F: Fn(&str, bool) -> Result<Token, String>,
|
||||
{
|
||||
pub fn from_tokenizer(tokenizer: Tokenizer<'x, F>) -> Self {
|
||||
Self {
|
||||
tokenizer,
|
||||
output: Vec::new(),
|
||||
operator_stack: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(mut self) -> Result<Self, String> {
|
||||
let mut arg_count: Vec<i32> = 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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Token, String>,
|
||||
{
|
||||
pub(crate) iter: Peekable<Enumerate<Iter<'x, u8>>>,
|
||||
token_map: F,
|
||||
buf: Vec<u8>,
|
||||
depth: u32,
|
||||
next_token: Vec<Token>,
|
||||
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<Token, String>,
|
||||
{
|
||||
#[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<Enumerate<Iter<'x, u8>>>, 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<Option<Token>, 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<Token, String> {
|
||||
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::<f64>()
|
||||
.map(|f| Token::Number(Number::Float(f)))
|
||||
.map_err(|_| format!("Invalid float value {}", buf,))
|
||||
} else {
|
||||
buf.parse::<i64>()
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<MatchType, CompileError> {
|
||||
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<Comparator, CompileError> {
|
||||
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<Vec<String>, 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<Value, CompileError> {
|
||||
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<Vec<Value>, 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<Value, CompileError> {
|
||||
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<Vec<Value>, 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<Vec<Value>, 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<Capability>,
|
||||
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<String> 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,
|
||||
};
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Expression>,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Block {
|
||||
is_all: bool,
|
||||
is_not: bool,
|
||||
p_count: u32,
|
||||
jmps: Vec<usize>,
|
||||
}
|
||||
|
||||
impl<'x> CompilerState<'x> {
|
||||
pub(crate) fn parse_test(&mut self) -> Result<(), CompileError> {
|
||||
let mut block_stack: Vec<Block> = 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
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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;
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Value>,
|
||||
pub key_list: Vec<Value>,
|
||||
pub address_part: AddressPart,
|
||||
pub match_type: MatchType,
|
||||
pub comparator: Comparator,
|
||||
pub index: Option<i32>,
|
||||
|
||||
pub mime_anychild: bool,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
impl<'x> CompilerState<'x> {
|
||||
pub(crate) fn parse_test_address(&mut self) -> Result<Test, CompileError> {
|
||||
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<Word> 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!(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Value>,
|
||||
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<Value>),
|
||||
Text,
|
||||
}
|
||||
|
||||
impl<'x> CompilerState<'x> {
|
||||
pub(crate) fn parse_test_body(&mut self) -> Result<Test, CompileError> {
|
||||
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,
|
||||
}))
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Value>,
|
||||
pub match_type: MatchType,
|
||||
pub comparator: Comparator,
|
||||
pub index: Option<i32>,
|
||||
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<i64>,
|
||||
pub match_type: MatchType,
|
||||
pub comparator: Comparator,
|
||||
pub date_part: DatePart,
|
||||
pub key_list: Vec<Value>,
|
||||
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<Test, CompileError> {
|
||||
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<Test, CompileError> {
|
||||
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<i64, CompileError> {
|
||||
let token_info = self.tokens.unwrap_next()?;
|
||||
if let Token::StringConstant(value) = &token_info.token {
|
||||
let timezone = match value {
|
||||
StringConstant::String(value) => value.parse::<i64>().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,
|
||||
};
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Value>,
|
||||
pub dup_match: DupMatch,
|
||||
pub seconds: Option<u64>,
|
||||
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<Test, CompileError> {
|
||||
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 => {}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Envelope>,
|
||||
pub key_list: Vec<Value>,
|
||||
pub address_part: AddressPart,
|
||||
pub match_type: MatchType,
|
||||
pub comparator: Comparator,
|
||||
pub zone: Option<i64>,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
impl<'x> CompilerState<'x> {
|
||||
pub(crate) fn parse_test_envelope(&mut self) -> Result<Test, CompileError> {
|
||||
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<String> for Envelope {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
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<Self, Self::Error> {
|
||||
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,
|
||||
};
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Test, CompileError> {
|
||||
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,
|
||||
}))
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Value>,
|
||||
pub mime_anychild: bool,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
impl<'x> CompilerState<'x> {
|
||||
pub(crate) fn parse_test_exists(&mut self) -> Result<Test, CompileError> {
|
||||
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,
|
||||
}))
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Value>,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
impl<'x> CompilerState<'x> {
|
||||
pub(crate) fn parse_test_valid_ext_list(&mut self) -> Result<Test, CompileError> {
|
||||
Ok(Test::ValidExtList(TestValidExtList {
|
||||
list_names: self.parse_strings(false)?,
|
||||
is_not: false,
|
||||
}))
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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]
|
||||
[<variable-list: string-list>]
|
||||
<list-of-flags: string-list>
|
||||
*/
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub(crate) struct TestHasFlag {
|
||||
pub comparator: Comparator,
|
||||
pub match_type: MatchType,
|
||||
pub variable_list: Vec<VariableType>,
|
||||
pub flags: Vec<Value>,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
impl<'x> CompilerState<'x> {
|
||||
pub(crate) fn parse_test_hasflag(&mut self) -> Result<Test, CompileError> {
|
||||
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,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Value>,
|
||||
pub key_list: Vec<Value>,
|
||||
pub match_type: MatchType,
|
||||
pub comparator: Comparator,
|
||||
pub index: Option<i32>,
|
||||
|
||||
pub mime_opts: MimeOpts<Value>,
|
||||
pub mime_anychild: bool,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
impl<'x> CompilerState<'x> {
|
||||
pub(crate) fn parse_test_header(&mut self) -> Result<Test, CompileError> {
|
||||
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<Value> {
|
||||
fn map_local_vars(&mut self, last_id: usize) {
|
||||
if let MimeOpts::Param(value) = self {
|
||||
value.map_local_vars(last_id)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Capability>,
|
||||
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<Test, CompileError> {
|
||||
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(())
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Value>,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub(crate) struct TestMetadataExists {
|
||||
pub mailbox: Option<Value>,
|
||||
pub annotation_names: Vec<Value>,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
metadata [MATCH-TYPE] [COMPARATOR]
|
||||
<mailbox: string>
|
||||
<annotation-name: string> <key-list: string-list>
|
||||
|
||||
*/
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub(crate) struct TestMetadata {
|
||||
pub match_type: MatchType,
|
||||
pub comparator: Comparator,
|
||||
pub medatata: Metadata<Value>,
|
||||
pub key_list: Vec<Value>,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
servermetadata [MATCH-TYPE] [COMPARATOR]
|
||||
<annotation-name: string> <key-list: string-list>
|
||||
|
||||
*/
|
||||
|
||||
impl<'x> CompilerState<'x> {
|
||||
pub(crate) fn parse_test_mailboxexists(&mut self) -> Result<Test, CompileError> {
|
||||
Ok(Test::MailboxExists(TestMailboxExists {
|
||||
mailbox_names: self.parse_strings(false)?,
|
||||
is_not: false,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_test_metadataexists(&mut self) -> Result<Test, CompileError> {
|
||||
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<Test, CompileError> {
|
||||
Ok(Test::MetadataExists(TestMetadataExists {
|
||||
mailbox: None,
|
||||
annotation_names: self.parse_strings(false)?,
|
||||
is_not: false,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_test_metadata(&mut self) -> Result<Test, CompileError> {
|
||||
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<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()?;
|
||||
}
|
||||
_ => {
|
||||
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<Test, CompileError> {
|
||||
let mut match_type = MatchType::Is;
|
||||
let mut comparator = Comparator::AsciiCaseMap;
|
||||
let mut annotation_name = None;
|
||||
let mut key_list: Vec<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()?;
|
||||
}
|
||||
_ => {
|
||||
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<Value> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Value>,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
impl<'x> CompilerState<'x> {
|
||||
pub(crate) fn parse_test_mailboxidexists(&mut self) -> Result<Test, CompileError> {
|
||||
Ok(Test::MailboxIdExists(TestMailboxIdExists {
|
||||
mailbox_ids: self.parse_strings(false)?,
|
||||
is_not: false,
|
||||
}))
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Value>,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub(crate) struct TestValidNotifyMethod {
|
||||
pub notification_uris: Vec<Value>,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
impl<'x> CompilerState<'x> {
|
||||
pub(crate) fn parse_test_valid_notify_method(&mut self) -> Result<Test, CompileError> {
|
||||
Ok(Test::ValidNotifyMethod(TestValidNotifyMethod {
|
||||
notification_uris: self.parse_strings(false)?,
|
||||
is_not: false,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_test_notify_method_capability(&mut self) -> Result<Test, CompileError> {
|
||||
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,
|
||||
}))
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<PluginArgument<Value, Value>>,
|
||||
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<Test, CompileError> {
|
||||
Ok(Test::Plugin(self.parse_plugin_(schema)?))
|
||||
}
|
||||
|
||||
fn parse_plugin_(&mut self, schema: &PluginSchema) -> Result<Plugin, CompileError> {
|
||||
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<PluginArgument<Value, Value>, 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<Value, Value> {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Test, CompileError> {
|
||||
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"))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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]
|
||||
<value: string>
|
||||
*/
|
||||
|
||||
#[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<Test, CompileError> {
|
||||
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<Test, CompileError> {
|
||||
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,
|
||||
}))
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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 [<mailbox: string>]
|
||||
<special-use-attrs: string-list>
|
||||
*/
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub(crate) struct TestSpecialUseExists {
|
||||
pub mailbox: Option<Value>,
|
||||
pub attributes: Vec<Value>,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
impl<'x> CompilerState<'x> {
|
||||
pub(crate) fn parse_test_specialuseexists(&mut self) -> Result<Test, CompileError> {
|
||||
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,
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Value>,
|
||||
pub key_list: Vec<Value>,
|
||||
pub is_not: bool,
|
||||
}
|
||||
|
||||
impl<'x> CompilerState<'x> {
|
||||
pub(crate) fn parse_test_string(&mut self) -> Result<Test, CompileError> {
|
||||
let mut match_type = MatchType::Is;
|
||||
let mut comparator = Comparator::AsciiCaseMap;
|
||||
let mut source = None;
|
||||
let mut key_list: Vec<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
|
||||
| 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,
|
||||
}))
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<u8>),
|
||||
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<str> {
|
||||
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<StringConstant> 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}"),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Value, ErrorType> {
|
||||
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<Option<VariableType>, 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<Option<VariableType>, 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<VariableType, ErrorType> {
|
||||
#[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<expr::Token, String> {
|
||||
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<Value>,
|
||||
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::<f64>().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::<i64>().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<Self, Self::Error> {
|
||||
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<Self, Self::Error> {
|
||||
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<Self, Self::Error> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Iter<'x, u8>>,
|
||||
pub buf: Vec<u8>,
|
||||
pub next_token: Vec<TokenInfo>,
|
||||
|
||||
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<TokenInfo> {
|
||||
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::<usize>()
|
||||
{
|
||||
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<TokenInfo, CompileError> {
|
||||
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::<i64>().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::<f64>().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<u8> {
|
||||
self.iter.peek().map(|ch| **ch)
|
||||
}
|
||||
|
||||
pub fn unwrap_next(&mut self) -> Result<TokenInfo, CompileError> {
|
||||
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<String, CompileError> {
|
||||
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<usize, CompileError> {
|
||||
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<Result<&TokenInfo, CompileError>> {
|
||||
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<TokenInfo, CompileError>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
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!(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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"),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Expression>),
|
||||
Regex(Regex),
|
||||
List(Vec<Value>),
|
||||
}
|
||||
|
||||
#[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<VariableType>,
|
||||
pub functions: Vec<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct HeaderVariable {
|
||||
pub name: Vec<HeaderName<'static>>,
|
||||
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<Number> 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<String>) -> &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<String>,
|
||||
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<String>) -> &mut Self {
|
||||
self.with_tagged_argument(tag, PluginSchemaArgument::Text)
|
||||
}
|
||||
|
||||
pub fn with_tagged_number_argument(&mut self, tag: impl Into<String>) -> &mut Self {
|
||||
self.with_tagged_argument(tag, PluginSchemaArgument::Number)
|
||||
}
|
||||
|
||||
pub fn with_tagged_regex_argument(&mut self, tag: impl Into<String>) -> &mut Self {
|
||||
self.with_tagged_argument(tag, PluginSchemaArgument::Regex)
|
||||
}
|
||||
|
||||
pub fn with_tagged_variable_argument(&mut self, tag: impl Into<String>) -> &mut Self {
|
||||
self.with_tagged_argument(tag, PluginSchemaArgument::Variable)
|
||||
}
|
||||
|
||||
pub fn with_tagged_string_array_argument(&mut self, tag: impl Into<String>) -> &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<String>) -> &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<String>) -> &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<String>) -> &mut Self {
|
||||
self.with_tagged_argument(
|
||||
tag,
|
||||
PluginSchemaArgument::Array(Box::new(PluginSchemaArgument::Variable)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn with_tag(&mut self, tag: impl Into<String>) -> &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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
self.expr.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Regex {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
<String>::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<Cow<'static, str>>) -> 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<Cow<'static, str>>) -> 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::<Vec<_>>(),
|
||||
)
|
||||
.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()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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 <list-sales@example.org>
|
||||
To: John Doe <jdoe@example.org>
|
||||
List-ID: <sales@example.org>
|
||||
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<String> = 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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);
|
||||
}
|
|
@ -24,6 +24,10 @@ use crate::utils::parsec::*;
|
|||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RuleBlock(pub Vec<Rule>);
|
||||
|
||||
pub mod compiler;
|
||||
pub mod lib;
|
||||
pub mod runtime;
|
||||
|
||||
/*
|
||||
MATCH-TYPE =/ COUNT / VALUE
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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)
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Cow<'static, str>>,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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();
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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::<Vec<_>>();
|
||||
|
||||
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<String> {
|
||||
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<String> {
|
||||
match self.vars_global.get("__flags") {
|
||||
Some(flags) if !flags.is_empty() => flags
|
||||
.to_cow()
|
||||
.split(' ')
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_local_or_global_flags(&self, strings: &[Value]) -> Vec<String> {
|
||||
if strings.is_empty() {
|
||||
self.get_global_flags()
|
||||
} else {
|
||||
self.get_local_flags(strings)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Sieve>),
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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"<auto-generated@message-id>");
|
||||
|
||||
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"<auto-generated@message-id>");
|
||||
|
||||
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<Event> {
|
||||
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<u8> {
|
||||
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<u64> = 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));
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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::<usize>()
|
||||
+ params
|
||||
.headers
|
||||
.iter()
|
||||
.map(|(h, v)| h.len() + v.len() + 4)
|
||||
.sum::<usize>()
|
||||
+ 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<String>,
|
||||
cc: Vec<String>,
|
||||
bcc: Vec<String>,
|
||||
body: Option<String>,
|
||||
headers: Vec<(HeaderName<'static>, String)>,
|
||||
}
|
||||
|
||||
fn parse_mailto(uri: &str) -> Option<MailtoMessage> {
|
||||
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);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<String> {
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<u8>, 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");
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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;
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Sieve>,
|
||||
pub(crate) prev_pos: usize,
|
||||
pub(crate) prev_vars_local: Vec<Variable<'static>>,
|
||||
pub(crate) prev_vars_match: Vec<Variable<'static>>,
|
||||
}
|
||||
|
||||
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<Result<Event, RuntimeError>> {
|
||||
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::<Vec<_>>()
|
||||
.into_iter()
|
||||
.enumerate(),
|
||||
Variable::ArrayRef(arr) if !arr.is_empty() => arr
|
||||
.iter()
|
||||
.map(|v| v.to_owned())
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.enumerate(),
|
||||
Variable::String(s) => s
|
||||
.lines()
|
||||
.map(|line| Variable::String(line.to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.enumerate(),
|
||||
Variable::StringRef(s) => s
|
||||
.lines()
|
||||
.map(|line| Variable::String(line.to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
.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<Envelope>,
|
||||
value: impl Into<Cow<'x, str>>,
|
||||
) {
|
||||
if let Ok(envelope) = envelope.try_into() {
|
||||
if matches!(&envelope, Envelope::From | Envelope::To) {
|
||||
let value: Cow<str> = 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<Cow<'static, str>, 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<Envelope>,
|
||||
value: impl Into<Cow<'x, str>>,
|
||||
) -> 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<Cow<'x, str>>) {
|
||||
self.user_address = from.into();
|
||||
}
|
||||
|
||||
pub fn with_user_address(mut self, from: impl Into<Cow<'x, str>>) -> 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<Cow<'static, str>>,
|
||||
value: impl Into<Variable<'x>>,
|
||||
) {
|
||||
self.vars_env.insert(name.into(), value.into());
|
||||
}
|
||||
|
||||
pub fn with_env_variable(
|
||||
mut self,
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
value: impl Into<Variable<'x>>,
|
||||
) -> Self {
|
||||
self.set_env_variable(name, value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_global_variable(
|
||||
&mut self,
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
value: impl Into<Variable<'x>>,
|
||||
) {
|
||||
self.vars_env.insert(name.into(), value.into());
|
||||
}
|
||||
|
||||
pub fn with_global_variable(
|
||||
mut self,
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
value: impl Into<Variable<'x>>,
|
||||
) -> Self {
|
||||
self.set_global_variable(name, value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_medatata(
|
||||
&mut self,
|
||||
name: impl Into<Metadata<String>>,
|
||||
value: impl Into<Cow<'x, str>>,
|
||||
) {
|
||||
self.metadata.push((name.into(), value.into()));
|
||||
}
|
||||
|
||||
pub fn with_metadata(
|
||||
mut self,
|
||||
name: impl Into<Metadata<String>>,
|
||||
value: impl Into<Cow<'x, str>>,
|
||||
) -> Self {
|
||||
self.set_medatata(name, value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_spam_status(&mut self, status: impl Into<SpamStatus>) {
|
||||
self.spam_status = status.into();
|
||||
}
|
||||
|
||||
pub fn with_spam_status(mut self, status: impl Into<SpamStatus>) -> Self {
|
||||
self.set_spam_status(status);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_virus_status(&mut self, status: impl Into<VirusStatus>) {
|
||||
self.virus_status = status.into();
|
||||
}
|
||||
|
||||
pub fn with_virus_status(mut self, status: impl Into<VirusStatus>) -> 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<Item = &str> {
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Variable<'x>> {
|
||||
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<Variable<'x>> {
|
||||
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<Variable<'y>> {
|
||||
strings.iter().map(|s| self.eval_value(s)).collect()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn eval_values_owned(&self, strings: &[Value]) -> Vec<String> {
|
||||
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<Variable<'x>>) {
|
||||
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<Variable<'x>> {
|
||||
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<Variable<'x>> {
|
||||
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<u8> {
|
||||
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()
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Variable<'x>> {
|
||||
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::<i64>() {
|
||||
Variable::Integer(n)
|
||||
} else if let Ok(n) = s.parse::<f64>() {
|
||||
Variable::Float(n)
|
||||
} else {
|
||||
Variable::Integer(0)
|
||||
}
|
||||
}
|
||||
Variable::StringRef(s) if !s.is_empty() => {
|
||||
if let Ok(n) = s.parse::<i64>() {
|
||||
Variable::Integer(n)
|
||||
} else if let Ok(n) = s.parse::<f64>() {
|
||||
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<Ordering> {
|
||||
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<bool> for Number {
|
||||
#[inline(always)]
|
||||
fn from(b: bool) -> Self {
|
||||
Number::Integer(i64::from(b))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for Number {
|
||||
#[inline(always)]
|
||||
fn from(n: i64) -> Self {
|
||||
Number::Integer(n)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for Number {
|
||||
#[inline(always)]
|
||||
fn from(n: f64) -> Self {
|
||||
Number::Float(n)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> 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<String, Variable>) -> Option<Variable>;
|
||||
}
|
||||
|
||||
impl EvalExpression for Vec<Expression> {
|
||||
fn eval(&self, variables: &HashMap<String, Variable>) -> Option<Variable> {
|
||||
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<String, Variable>) {
|
||||
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<Expression> {
|
||||
ExpressionParser::from_tokenizer(Tokenizer::new(expr, |var_name: &str, _: bool| {
|
||||
Ok::<_, String>(Token::Variable(VariableType::Global(var_name.to_string())))
|
||||
}))
|
||||
.parse()
|
||||
.unwrap()
|
||||
.output
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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<Variable<'x>>),
|
||||
ArrayRef(&'x Vec<Variable<'x>>),
|
||||
}
|
||||
|
||||
#[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<Number> {
|
||||
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::<i64>().map(Number::Integer).ok()
|
||||
} else {
|
||||
s.parse::<f64>().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::<i64>().unwrap_or(0),
|
||||
Variable::StringRef(s) if !s.is_empty() => s.parse::<i64>().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<String> 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<Cow<'x, str>> 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<Vec<Variable<'x>>> for Variable<'x> {
|
||||
fn from(l: Vec<Variable<'x>>) -> Self {
|
||||
Variable::Array(l)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Number> for Variable<'_> {
|
||||
fn from(n: Number) -> Self {
|
||||
match n {
|
||||
Number::Integer(n) => Variable::Integer(n),
|
||||
Number::Float(n) => Variable::Float(n),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<usize> for Variable<'_> {
|
||||
fn from(n: usize) -> Self {
|
||||
Variable::Integer(n as i64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for Variable<'_> {
|
||||
fn from(n: i64) -> Self {
|
||||
Variable::Integer(n)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for Variable<'_> {
|
||||
fn from(n: u64) -> Self {
|
||||
Variable::Integer(n as i64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for Variable<'_> {
|
||||
fn from(n: f64) -> Self {
|
||||
Variable::Float(n)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for Variable<'_> {
|
||||
fn from(n: i32) -> Self {
|
||||
Variable::Integer(n as i64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> 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<std::cmp::Ordering> {
|
||||
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<Variable<'x>> {
|
||||
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<String, Number> {
|
||||
pub fn unwrap_string(self) -> Option<String> {
|
||||
match self {
|
||||
PluginArgument::Text(s) => s.into(),
|
||||
PluginArgument::Number(n) => n.to_string().into(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unwrap_number(self) -> Option<Number> {
|
||||
match self {
|
||||
PluginArgument::Number(n) => n.into(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unwrap_regex(self) -> Option<Regex> {
|
||||
match self {
|
||||
PluginArgument::Regex(r) => r.into(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unwrap_array(self) -> Option<Vec<Self>> {
|
||||
match self {
|
||||
PluginArgument::Array(a) => a.into(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unwrap_variable(self) -> Option<VariableType> {
|
||||
match self {
|
||||
PluginArgument::Variable(v) => v.into(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unwrap_string_array(self) -> Option<Vec<String>> {
|
||||
match self {
|
||||
PluginArgument::Array(a) => a
|
||||
.into_iter()
|
||||
.filter_map(Self::unwrap_string)
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unwrap_number_array(self) -> Option<Vec<Number>> {
|
||||
match self {
|
||||
PluginArgument::Array(a) => a
|
||||
.into_iter()
|
||||
.filter_map(Self::unwrap_number)
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unwrap_regex_array(self) -> Option<Vec<Regex>> {
|
||||
match self {
|
||||
PluginArgument::Array(a) => a
|
||||
.into_iter()
|
||||
.filter_map(Self::unwrap_regex)
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unwrap_variable_array(self) -> Option<Vec<VariableType>> {
|
||||
match self {
|
||||
PluginArgument::Array(a) => a
|
||||
.into_iter()
|
||||
.filter_map(Self::unwrap_variable)
|
||||
.collect::<Vec<_>>()
|
||||
.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<Capability>) {
|
||||
self.allowed_capabilities.insert(capability.into());
|
||||
}
|
||||
|
||||
pub fn with_capability(mut self, capability: impl Into<Capability>) -> Self {
|
||||
self.set_capability(capability);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn unset_capability(&mut self, capability: impl Into<Capability>) {
|
||||
self.allowed_capabilities.remove(&capability.into());
|
||||
}
|
||||
|
||||
pub fn without_capability(mut self, capability: impl Into<Capability>) -> Self {
|
||||
self.unset_capability(capability);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn without_capabilities(
|
||||
mut self,
|
||||
capabilities: impl IntoIterator<Item = impl Into<Capability>>,
|
||||
) -> Self {
|
||||
for capability in capabilities {
|
||||
self.allowed_capabilities.remove(&capability.into());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_protected_header(&mut self, header_name: impl Into<Cow<'static, str>>) {
|
||||
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<Cow<'static, str>>) -> Self {
|
||||
self.set_protected_header(header_name);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_protected_headers(
|
||||
mut self,
|
||||
header_names: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
|
||||
) -> Self {
|
||||
self.protected_headers = header_names
|
||||
.into_iter()
|
||||
.filter_map(HeaderName::parse)
|
||||
.collect();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_env_variable(
|
||||
&mut self,
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
value: impl Into<Variable<'static>>,
|
||||
) {
|
||||
self.environment.insert(name.into(), value.into());
|
||||
}
|
||||
|
||||
pub fn with_env_variable(
|
||||
mut self,
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
value: impl Into<Cow<'static, str>>,
|
||||
) -> Self {
|
||||
self.set_env_variable(name.into(), value.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_medatata(
|
||||
&mut self,
|
||||
name: impl Into<Metadata<String>>,
|
||||
value: impl Into<Cow<'static, str>>,
|
||||
) {
|
||||
self.metadata.push((name.into(), value.into()));
|
||||
}
|
||||
|
||||
pub fn with_metadata(
|
||||
mut self,
|
||||
name: impl Into<Metadata<String>>,
|
||||
value: impl Into<Cow<'static, str>>,
|
||||
) -> Self {
|
||||
self.set_medatata(name, value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_valid_notification_uri(&mut self, uri: impl Into<Cow<'static, str>>) {
|
||||
self.valid_notification_uris.insert(uri.into());
|
||||
}
|
||||
|
||||
pub fn with_valid_notification_uri(mut self, uri: impl Into<Cow<'static, str>>) -> Self {
|
||||
self.valid_notification_uris.insert(uri.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_valid_notification_uris(
|
||||
mut self,
|
||||
uris: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
|
||||
) -> Self {
|
||||
self.valid_notification_uris = uris.into_iter().map(Into::into).collect();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_valid_ext_list(&mut self, name: impl Into<Cow<'static, str>>) {
|
||||
self.valid_ext_lists.insert(name.into());
|
||||
}
|
||||
|
||||
pub fn with_valid_ext_list(mut self, name: impl Into<Cow<'static, str>>) -> 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<Item = impl Into<Cow<'static, str>>>,
|
||||
) -> 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<Cow<'static, str>>) {
|
||||
self.vacation_default_subject = value.into();
|
||||
}
|
||||
|
||||
pub fn with_vacation_default_subject(mut self, value: impl Into<Cow<'static, str>>) -> Self {
|
||||
self.set_vacation_default_subject(value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_vacation_subject_prefix(&mut self, value: impl Into<Cow<'static, str>>) {
|
||||
self.vacation_subject_prefix = value.into();
|
||||
}
|
||||
|
||||
pub fn with_vacation_subject_prefix(mut self, value: impl Into<Cow<'static, str>>) -> Self {
|
||||
self.set_vacation_subject_prefix(value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_local_hostname(&mut self, value: impl Into<Cow<'static, str>>) {
|
||||
self.local_hostname = value.into();
|
||||
}
|
||||
|
||||
pub fn with_local_hostname(mut self, value: impl Into<Cow<'static, str>>) -> 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<String>, fnc: Function) -> Self {
|
||||
self.with_function_args(name, fnc, 1)
|
||||
}
|
||||
|
||||
pub fn with_function_no_args(self, name: impl Into<String>, fnc: Function) -> Self {
|
||||
self.with_function_args(name, fnc, 0)
|
||||
}
|
||||
|
||||
pub fn with_function_args(
|
||||
mut self,
|
||||
name: impl Into<String>,
|
||||
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<Script>, script: impl Into<Arc<Sieve>>) -> Self {
|
||||
Input::Script {
|
||||
name: name.into(),
|
||||
script: script.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn success() -> Self {
|
||||
Input::True
|
||||
}
|
||||
|
||||
pub fn fail() -> Self {
|
||||
Input::False
|
||||
}
|
||||
|
||||
pub fn variables(list: Vec<SetVariable>) -> Self {
|
||||
Input::Variables { list }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for Input {
|
||||
fn from(value: bool) -> Self {
|
||||
if value {
|
||||
Input::True
|
||||
} else {
|
||||
Input::False
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Script {
|
||||
type Target = String;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match self {
|
||||
Script::Personal(name) | Script::Global(name) => name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for Script {
|
||||
fn as_ref(&self) -> &str {
|
||||
match self {
|
||||
Script::Personal(name) | Script::Global(name) => name.as_str(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<String> for Script {
|
||||
fn as_ref(&self) -> &String {
|
||||
match self {
|
||||
Script::Personal(name) | Script::Global(name) => name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Script {
|
||||
pub fn into_string(self) -> String {
|
||||
match self {
|
||||
Script::Personal(name) | Script::Global(name) => name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &String {
|
||||
match self {
|
||||
Script::Personal(name) | Script::Global(name) => name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Script {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Script {
|
||||
fn from(name: String) -> Self {
|
||||
Script::Personal(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Script {
|
||||
fn from(name: &str) -> Self {
|
||||
Script::Personal(name.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Metadata<T> {
|
||||
pub fn server(annotation: impl Into<T>) -> Self {
|
||||
Metadata::Server {
|
||||
annotation: annotation.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mailbox(name: impl Into<T>, annotation: impl Into<T>) -> Self {
|
||||
Metadata::Mailbox {
|
||||
name: name.into(),
|
||||
annotation: annotation.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Metadata<String> {
|
||||
fn from(annotation: String) -> Self {
|
||||
Metadata::Server { annotation }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'_ str> for Metadata<String> {
|
||||
fn from(annotation: &'_ str) -> Self {
|
||||
Metadata::Server {
|
||||
annotation: annotation.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(String, String)> for Metadata<String> {
|
||||
fn from((name, annotation): (String, String)) -> Self {
|
||||
Metadata::Mailbox { name, annotation }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&'_ str, &'_ str)> for Metadata<String> {
|
||||
fn from((name, annotation): (&'_ str, &'_ str)) -> Self {
|
||||
Metadata::Mailbox {
|
||||
name: name.to_string(),
|
||||
annotation: annotation.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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, Sieve};
|
||||
|
||||
const SIEVE_MARKER: u8 = 0xff;
|
||||
|
||||
pub enum SerializeError {
|
||||
Other,
|
||||
}
|
||||
|
||||
impl Sieve {
|
||||
pub fn deserialize(bytes: &[u8]) -> Result<Self, Box<bincode::ErrorKind>> {
|
||||
if bytes.len() > 2 && bytes[0] == SIEVE_MARKER && bytes[1] == Compiler::VERSION as u8 {
|
||||
bincode::deserialize(&bytes[2..])
|
||||
} else {
|
||||
Err(Box::new(bincode::ErrorKind::Custom(
|
||||
"Incompatible version".to_string(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> Result<Vec<u8>, Box<bincode::ErrorKind>> {
|
||||
let mut buf = Vec::with_capacity(bincode::serialized_size(self)? as usize + 2);
|
||||
buf.push(SIEVE_MARKER);
|
||||
buf.push(Compiler::VERSION as u8);
|
||||
bincode::serialize_into(&mut buf, self)?;
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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 crate::sieve::{
|
||||
compiler::{
|
||||
grammar::{Comparator, RelationalMatch},
|
||||
Value,
|
||||
},
|
||||
runtime::Variable,
|
||||
MatchAs,
|
||||
};
|
||||
|
||||
use super::glob::{glob_match, glob_match_capture};
|
||||
|
||||
impl Comparator {
|
||||
pub(crate) fn is(&self, a: &Variable<'_>, b: &Variable<'_>) -> bool {
|
||||
match self {
|
||||
Comparator::Octet => a.to_cow() == b.to_cow(),
|
||||
Comparator::AsciiNumeric => RelationalMatch::Eq.cmp(&a.to_number(), &b.to_number()),
|
||||
_ => a.to_cow().to_lowercase() == b.to_cow().to_lowercase(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn contains(&self, haystack: &str, needle: &str) -> bool {
|
||||
needle.is_empty()
|
||||
|| match self {
|
||||
Comparator::Octet => haystack.contains(needle),
|
||||
_ => haystack.to_lowercase().contains(&needle.to_lowercase()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn relational(
|
||||
&self,
|
||||
relation: &RelationalMatch,
|
||||
a: &Variable<'_>,
|
||||
b: &Variable<'_>,
|
||||
) -> bool {
|
||||
match self {
|
||||
Comparator::Octet => relation.cmp(a.to_cow().as_ref(), b.to_cow().as_ref()),
|
||||
Comparator::AsciiNumeric => relation.cmp(&a.to_number(), &b.to_number()),
|
||||
_ => relation.cmp(&a.to_cow().to_lowercase(), &b.to_cow().to_lowercase()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn matches(
|
||||
&self,
|
||||
value: &str,
|
||||
pattern: &str,
|
||||
capture_positions: u64,
|
||||
captured_values: &mut Vec<(usize, String)>,
|
||||
) -> bool {
|
||||
match self {
|
||||
Comparator::AsciiCaseMap if capture_positions == 0 => {
|
||||
glob_match(&value.to_lowercase(), pattern, true)
|
||||
}
|
||||
Comparator::AsciiCaseMap => {
|
||||
glob_match_capture(value, pattern, true, capture_positions, captured_values)
|
||||
}
|
||||
_ if capture_positions == 0 => glob_match(value, pattern, false),
|
||||
_ => glob_match_capture(value, pattern, false, capture_positions, captured_values),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn regex(
|
||||
&self,
|
||||
pattern: &Value,
|
||||
pattern_expr: &Variable<'_>,
|
||||
value: &str,
|
||||
mut capture_positions: u64,
|
||||
captured_values: &mut Vec<(usize, String)>,
|
||||
) -> bool {
|
||||
let regex = if let Value::Regex(regex) = pattern {
|
||||
Cow::Borrowed(®ex.regex)
|
||||
} else {
|
||||
match fancy_regex::Regex::new(pattern_expr.to_cow().as_ref()) {
|
||||
Ok(regex) => Cow::Owned(regex),
|
||||
Err(err) => {
|
||||
debug_assert!(false, "Failed to compile regex: {err:?}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if capture_positions == 0 {
|
||||
regex.is_match(value).unwrap_or_default()
|
||||
} else if let Ok(Some(captures)) = regex.captures(value) {
|
||||
captured_values.clear();
|
||||
while capture_positions != 0 {
|
||||
let index = 63 - capture_positions.leading_zeros();
|
||||
capture_positions ^= 1 << index;
|
||||
if let Some(match_var) = captures.get(index as usize) {
|
||||
captured_values.push((index as usize, match_var.as_str().to_string()));
|
||||
}
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn as_match(&self) -> MatchAs {
|
||||
match self {
|
||||
Comparator::AsciiCaseMap => MatchAs::Lowercase,
|
||||
Comparator::AsciiNumeric => MatchAs::Number,
|
||||
_ => MatchAs::Octet,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RelationalMatch {
|
||||
pub fn cmp<T>(&self, a: &T, b: &T) -> bool
|
||||
where
|
||||
T: PartialOrd + ?Sized,
|
||||
{
|
||||
match self {
|
||||
RelationalMatch::Gt => a.gt(b),
|
||||
RelationalMatch::Ge => a.ge(b),
|
||||
RelationalMatch::Lt => a.lt(b),
|
||||
RelationalMatch::Le => a.le(b),
|
||||
RelationalMatch::Eq => a.eq(b),
|
||||
RelationalMatch::Ne => a.ne(b),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,313 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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::char::REPLACEMENT_CHARACTER;
|
||||
|
||||
use crate::sieve::MAX_MATCH_VARIABLES;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum PatternChar {
|
||||
WildcardMany { num: usize, match_pos: usize },
|
||||
WildcardSingle { match_pos: usize },
|
||||
Char { char: char, match_pos: usize },
|
||||
}
|
||||
|
||||
fn compile(str: &str, to_lower: bool) -> Vec<PatternChar> {
|
||||
let mut chars = Vec::new();
|
||||
let mut is_escaped = false;
|
||||
let mut str = str.chars().peekable();
|
||||
|
||||
while let Some(char) = str.next() {
|
||||
match char {
|
||||
'*' if !is_escaped => {
|
||||
let mut num = 1;
|
||||
while let Some('*') = str.peek() {
|
||||
num += 1;
|
||||
str.next();
|
||||
}
|
||||
chars.push(PatternChar::WildcardMany { num, match_pos: 0 });
|
||||
}
|
||||
'?' if !is_escaped => {
|
||||
chars.push(PatternChar::WildcardSingle { match_pos: 0 });
|
||||
}
|
||||
'\\' if !is_escaped => {
|
||||
is_escaped = true;
|
||||
continue;
|
||||
}
|
||||
_ => {
|
||||
if is_escaped {
|
||||
is_escaped = false;
|
||||
}
|
||||
if to_lower && char.is_uppercase() {
|
||||
for char in char.to_lowercase() {
|
||||
chars.push(PatternChar::Char { char, match_pos: 0 });
|
||||
}
|
||||
} else {
|
||||
chars.push(PatternChar::Char { char, match_pos: 0 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
chars
|
||||
}
|
||||
|
||||
// Credits: Algorithm ported from https://research.swtch.com/glob
|
||||
|
||||
pub(crate) fn glob_match(value: &str, pattern: &str, to_lower: bool) -> bool {
|
||||
let pattern = compile(pattern, to_lower);
|
||||
let value = if to_lower {
|
||||
value.to_lowercase().chars().collect::<Vec<_>>()
|
||||
} else {
|
||||
value.chars().collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
let mut px = 0;
|
||||
let mut nx = 0;
|
||||
let mut next_px = 0;
|
||||
let mut next_nx = 0;
|
||||
|
||||
while px < pattern.len() || nx < value.len() {
|
||||
match pattern.get(px) {
|
||||
Some(PatternChar::Char { char, .. }) => {
|
||||
if matches!(value.get(nx), Some(nc) if nc == char ) {
|
||||
px += 1;
|
||||
nx += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Some(PatternChar::WildcardSingle { .. }) => {
|
||||
if nx < value.len() {
|
||||
px += 1;
|
||||
nx += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Some(PatternChar::WildcardMany { .. }) => {
|
||||
next_px = px;
|
||||
next_nx = nx + 1;
|
||||
px += 1;
|
||||
continue;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
if 0 < next_nx && next_nx <= value.len() {
|
||||
px = next_px;
|
||||
nx = next_nx;
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn glob_match_capture(
|
||||
value_: &str,
|
||||
pattern: &str,
|
||||
to_lower: bool,
|
||||
capture_positions: u64,
|
||||
captured_values: &mut Vec<(usize, String)>,
|
||||
) -> bool {
|
||||
let mut pattern = compile(pattern, to_lower);
|
||||
let value = if to_lower {
|
||||
let mut value = Vec::with_capacity(value_.len());
|
||||
for char in value_.chars() {
|
||||
if char.is_uppercase() {
|
||||
for (pos, lowerchar) in char.to_lowercase().enumerate() {
|
||||
value.push((
|
||||
lowerchar,
|
||||
if pos == 0 {
|
||||
char
|
||||
} else {
|
||||
REPLACEMENT_CHARACTER
|
||||
},
|
||||
));
|
||||
}
|
||||
} else {
|
||||
value.push((char, char));
|
||||
}
|
||||
}
|
||||
value
|
||||
} else {
|
||||
value_.chars().map(|char| (char, char)).collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
let mut px = 0;
|
||||
let mut nx = 0;
|
||||
let mut next_px = 0;
|
||||
let mut next_nx = 0;
|
||||
|
||||
while px < pattern.len() || nx < value.len() {
|
||||
match pattern.get_mut(px) {
|
||||
Some(PatternChar::Char { char, match_pos }) => {
|
||||
if matches!(value.get(nx), Some(nc) if &nc.0 == char ) {
|
||||
*match_pos = nx;
|
||||
px += 1;
|
||||
nx += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Some(PatternChar::WildcardSingle { match_pos }) => {
|
||||
if nx < value.len() {
|
||||
*match_pos = nx;
|
||||
px += 1;
|
||||
nx += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Some(PatternChar::WildcardMany { match_pos, .. }) => {
|
||||
*match_pos = nx;
|
||||
next_px = px;
|
||||
next_nx = nx + 1;
|
||||
px += 1;
|
||||
continue;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
if 0 < next_nx && next_nx <= value.len() {
|
||||
px = next_px;
|
||||
nx = next_nx;
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut last_pos = 0;
|
||||
|
||||
captured_values.clear();
|
||||
if capture_positions & 1 != 0 {
|
||||
captured_values.push((0usize, value_.to_string()));
|
||||
}
|
||||
|
||||
let mut wildcard_pos = 1;
|
||||
for item in pattern {
|
||||
if wildcard_pos <= MAX_MATCH_VARIABLES {
|
||||
last_pos = match item {
|
||||
PatternChar::WildcardMany { mut num, match_pos } => {
|
||||
while num > 1 {
|
||||
if capture_positions & (1 << wildcard_pos) != 0 {
|
||||
captured_values.push((wildcard_pos, String::with_capacity(0)));
|
||||
}
|
||||
wildcard_pos += 1;
|
||||
num -= 1;
|
||||
}
|
||||
|
||||
if capture_positions & (1 << wildcard_pos) != 0 {
|
||||
if let Some(range) = value.get(last_pos..match_pos) {
|
||||
captured_values.push((
|
||||
wildcard_pos,
|
||||
range
|
||||
.iter()
|
||||
.filter_map(|(_, char)| {
|
||||
if char != &REPLACEMENT_CHARACTER {
|
||||
Some(char)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<String>(),
|
||||
));
|
||||
} else {
|
||||
debug_assert!(false, "Glob pattern failure.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
wildcard_pos += 1;
|
||||
match_pos
|
||||
}
|
||||
PatternChar::WildcardSingle { match_pos } => {
|
||||
if capture_positions & (1 << wildcard_pos) != 0 {
|
||||
if let Some((char, orig_char)) = value.get(match_pos) {
|
||||
captured_values.push((
|
||||
wildcard_pos,
|
||||
(if orig_char != &REPLACEMENT_CHARACTER {
|
||||
orig_char
|
||||
} else {
|
||||
char
|
||||
})
|
||||
.to_string(),
|
||||
));
|
||||
} else {
|
||||
debug_assert!(false, "Glob pattern failure.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
wildcard_pos += 1;
|
||||
match_pos
|
||||
}
|
||||
PatternChar::Char { match_pos, .. } => match_pos,
|
||||
} + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn glob_match() {
|
||||
for (value, pattern, expected_result) in [
|
||||
(
|
||||
"frop.......frop.........frop....",
|
||||
"?*frop*",
|
||||
vec!["f", "rop.......", ".........frop...."],
|
||||
),
|
||||
("frop:frup:frop", "*:*:*", vec!["frop", "frup", "frop"]),
|
||||
(
|
||||
"a b c d e f g",
|
||||
"? ? ? ? ? ? ?",
|
||||
vec!["a", "b", "c", "d", "e", "f", "g"],
|
||||
),
|
||||
("puk pok puk pok", "pu*ok", vec!["k pok puk p"]),
|
||||
("snot kip snot", "snot*snot", vec![" kip "]),
|
||||
(
|
||||
"klopfropstroptop",
|
||||
"*fr??*top",
|
||||
vec!["klop", "o", "p", "strop"],
|
||||
),
|
||||
("toptoptop", "*top", vec!["toptop"]),
|
||||
(
|
||||
"Fehlende Straße zur Karte hinzufügen",
|
||||
"FEHLENDE * ZUR Karte HINZUFÜGEN",
|
||||
vec!["Straße"],
|
||||
),
|
||||
] {
|
||||
let mut match_values = Vec::new();
|
||||
assert!(
|
||||
super::glob_match_capture(value, pattern, true, u64::MAX ^ 1, &mut match_values),
|
||||
"{value:?} {pattern:?}",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
match_values.into_iter().map(|(_, v)| v).collect::<Vec<_>>(),
|
||||
expected_result,
|
||||
"{value:?} {pattern:?}",
|
||||
);
|
||||
assert!(
|
||||
super::glob_match(value, pattern, true),
|
||||
"{value:?} {pattern:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,221 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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::slice::Iter;
|
||||
|
||||
use mail_parser::{Message, MessagePart, MimeHeaders, PartType};
|
||||
|
||||
use crate::sieve::Context;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum ContentTypeFilter {
|
||||
Type(String),
|
||||
TypeSubtype((String, String)),
|
||||
}
|
||||
|
||||
pub(crate) struct SubpartIterator<'x> {
|
||||
ctx: &'x Context<'x>,
|
||||
iter: Iter<'x, usize>,
|
||||
iter_stack: Vec<Iter<'x, usize>>,
|
||||
anychild: bool,
|
||||
}
|
||||
|
||||
impl<'x> SubpartIterator<'x> {
|
||||
pub(crate) fn new(ctx: &'x Context<'x>, parts: &'x [usize], anychild: bool) -> Self {
|
||||
SubpartIterator {
|
||||
ctx,
|
||||
iter: parts.iter(),
|
||||
iter_stack: Vec::new(),
|
||||
anychild,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn next(&mut self) -> Option<(usize, &MessagePart<'x>)> {
|
||||
loop {
|
||||
if let Some(&part_id) = self.iter.next() {
|
||||
let subpart = self.ctx.message.parts.get(part_id)?;
|
||||
match &subpart.body {
|
||||
PartType::Multipart(subparts) if self.anychild => {
|
||||
self.iter_stack
|
||||
.push(std::mem::replace(&mut self.iter, subparts.iter()));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
return Some((part_id, subpart));
|
||||
}
|
||||
if let Some(prev_iter) = self.iter_stack.pop() {
|
||||
self.iter = prev_iter;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> Context<'x> {
|
||||
pub(crate) fn find_nested_parts<'z: 'x>(
|
||||
&'z self,
|
||||
mut message: &'x Message<'x>,
|
||||
ct_filter: &[ContentTypeFilter],
|
||||
visitor_fnc: &mut impl FnMut(&MessagePart, &[u8]) -> bool,
|
||||
) -> bool {
|
||||
let mut iter_stack = Vec::new();
|
||||
let mut iter = vec![self.part].into_iter();
|
||||
|
||||
loop {
|
||||
while let Some(part_id) = iter.next() {
|
||||
if let Some(subpart) = message.parts.get(part_id) {
|
||||
let process_part = if !ct_filter.is_empty() {
|
||||
let mut process_part = false;
|
||||
let (ct, cst) = if let Some(ct) = subpart.content_type() {
|
||||
(ct.c_type.as_ref(), ct.c_subtype.as_deref().unwrap_or(""))
|
||||
} else {
|
||||
match &subpart.body {
|
||||
PartType::Text(_) => ("text", "plain"),
|
||||
PartType::Html(_) => ("text", "html"),
|
||||
PartType::Message(_) => ("message", "rfc822"),
|
||||
PartType::Multipart(_) => ("multipart", "mixed"),
|
||||
_ => ("application", "octet-stream"),
|
||||
}
|
||||
};
|
||||
|
||||
for ctf in ct_filter {
|
||||
match ctf {
|
||||
ContentTypeFilter::Type(name) => {
|
||||
if name.eq_ignore_ascii_case(ct) {
|
||||
process_part = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ContentTypeFilter::TypeSubtype((name, subname)) => {
|
||||
if name.eq_ignore_ascii_case(ct)
|
||||
&& subname.eq_ignore_ascii_case(cst)
|
||||
{
|
||||
process_part = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
process_part
|
||||
} else {
|
||||
true
|
||||
};
|
||||
if process_part && visitor_fnc(subpart, message.raw_message.as_ref()) {
|
||||
return true;
|
||||
}
|
||||
match &subpart.body {
|
||||
PartType::Multipart(subparts) => {
|
||||
iter_stack.push((
|
||||
std::mem::replace(&mut iter, subparts.clone().into_iter()),
|
||||
None,
|
||||
));
|
||||
}
|
||||
PartType::Message(next_message) => {
|
||||
iter_stack.push((
|
||||
std::mem::replace(&mut iter, vec![0].into_iter()),
|
||||
Some(message),
|
||||
));
|
||||
message = next_message;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((prev_iter, prev_message)) = iter_stack.pop() {
|
||||
iter = prev_iter;
|
||||
if let Some(prev_message) = prev_message {
|
||||
message = prev_message;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub(crate) fn find_nested_parts_ids(&self, include_current: bool) -> Vec<usize> {
|
||||
if self.part == 0 {
|
||||
if include_current {
|
||||
(0..self.message.parts.len()).collect()
|
||||
} else if self.message.parts.len() > 1 {
|
||||
(1..self.message.parts.len()).collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
} else {
|
||||
let mut part_ids = Vec::new();
|
||||
let mut iter_stack = Vec::new();
|
||||
|
||||
if include_current {
|
||||
part_ids.push(self.part);
|
||||
}
|
||||
|
||||
if let Some(PartType::Multipart(subparts)) =
|
||||
self.message.parts.get(self.part).map(|p| &p.body)
|
||||
{
|
||||
let mut iter = subparts.iter();
|
||||
loop {
|
||||
while let Some(&part_id) = iter.next() {
|
||||
part_ids.push(part_id);
|
||||
if let Some(PartType::Multipart(subparts)) =
|
||||
self.message.parts.get(part_id).map(|p| &p.body)
|
||||
{
|
||||
iter_stack.push(std::mem::replace(&mut iter, subparts.iter()));
|
||||
}
|
||||
}
|
||||
if let Some(prev_iter) = iter_stack.pop() {
|
||||
iter = prev_iter;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
part_ids
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ContentTypeFilter {
|
||||
pub(crate) fn parse(ct: &str) -> Option<ContentTypeFilter> {
|
||||
let mut iter = ct.split('/');
|
||||
let name = iter.next()?;
|
||||
if let Some(sub_name) = iter.next() {
|
||||
if !name.is_empty() && !sub_name.is_empty() && iter.next().is_none() {
|
||||
Some(ContentTypeFilter::TypeSubtype((
|
||||
name.to_string(),
|
||||
sub_name.to_string(),
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else if !name.is_empty() {
|
||||
Some(ContentTypeFilter::Type(name.to_string()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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::{test::Test, Capability},
|
||||
Context, Event, Mailbox,
|
||||
};
|
||||
|
||||
use super::RuntimeError;
|
||||
|
||||
pub mod comparator;
|
||||
pub mod glob;
|
||||
pub mod mime;
|
||||
pub mod test_address;
|
||||
pub mod test_body;
|
||||
pub mod test_date;
|
||||
pub mod test_duplicate;
|
||||
pub mod test_envelope;
|
||||
pub mod test_exists;
|
||||
pub mod test_extlists;
|
||||
pub mod test_hasflag;
|
||||
pub mod test_header;
|
||||
pub mod test_metadata;
|
||||
pub mod test_notify;
|
||||
pub mod test_size;
|
||||
pub mod test_spamtest;
|
||||
pub mod test_string;
|
||||
|
||||
pub(crate) enum TestResult {
|
||||
Bool(bool),
|
||||
Event { event: Event, is_not: bool },
|
||||
Error(RuntimeError),
|
||||
}
|
||||
|
||||
impl Test {
|
||||
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
|
||||
match &self {
|
||||
Test::Header(test) => test.exec(ctx),
|
||||
Test::Address(test) => test.exec(ctx),
|
||||
Test::Envelope(test) => test.exec(ctx),
|
||||
Test::Exists(test) => test.exec(ctx),
|
||||
Test::Size(test) => test.exec(ctx),
|
||||
Test::Body(test) => test.exec(ctx),
|
||||
Test::String(test) => test.exec(ctx, false),
|
||||
Test::EvalExpression(expr) => TestResult::Bool(
|
||||
ctx.eval_expression(&expr.expr)
|
||||
.and_then(|v| v.to_number_checked())
|
||||
.unwrap_or_default()
|
||||
.is_non_zero()
|
||||
^ expr.is_not,
|
||||
),
|
||||
Test::HasFlag(test) => test.exec(ctx),
|
||||
Test::Date(test) => test.exec(ctx),
|
||||
Test::CurrentDate(test) => test.exec(ctx),
|
||||
Test::Duplicate(test) => test.exec(ctx),
|
||||
Test::NotifyMethodCapability(test) => test.exec(ctx),
|
||||
Test::ValidNotifyMethod(test) => test.exec(ctx),
|
||||
Test::Environment(test) => test.exec(ctx, true),
|
||||
Test::ValidExtList(test) => test.exec(ctx),
|
||||
Test::Ihave(test) => TestResult::Bool(
|
||||
test.capabilities.iter().all(|c| {
|
||||
![Capability::Variables, Capability::EncodedCharacter].contains(c)
|
||||
&& ctx.runtime.allowed_capabilities.contains(c)
|
||||
}) ^ test.is_not,
|
||||
),
|
||||
Test::MailboxExists(test) => TestResult::Event {
|
||||
event: Event::MailboxExists {
|
||||
mailboxes: test
|
||||
.mailbox_names
|
||||
.iter()
|
||||
.map(|m| Mailbox::Name(ctx.eval_value(m).into_string()))
|
||||
.collect(),
|
||||
special_use: Vec::new(),
|
||||
},
|
||||
is_not: test.is_not,
|
||||
},
|
||||
Test::Vacation(test) => test.exec(ctx),
|
||||
Test::Metadata(test) => test.exec(ctx),
|
||||
Test::MetadataExists(test) => test.exec(ctx),
|
||||
Test::MailboxIdExists(test) => TestResult::Event {
|
||||
event: Event::MailboxExists {
|
||||
mailboxes: test
|
||||
.mailbox_ids
|
||||
.iter()
|
||||
.map(|m| Mailbox::Id(ctx.eval_value(m).into_string()))
|
||||
.collect(),
|
||||
special_use: Vec::new(),
|
||||
},
|
||||
is_not: test.is_not,
|
||||
},
|
||||
Test::SpamTest(test) => test.exec(ctx),
|
||||
Test::VirusTest(test) => test.exec(ctx),
|
||||
Test::SpecialUseExists(test) => TestResult::Event {
|
||||
event: Event::MailboxExists {
|
||||
mailboxes: if let Some(mailbox) = &test.mailbox {
|
||||
vec![Mailbox::Name(ctx.eval_value(mailbox).into_string())]
|
||||
} else {
|
||||
Vec::new()
|
||||
},
|
||||
special_use: ctx.eval_values_owned(&test.attributes),
|
||||
},
|
||||
is_not: test.is_not,
|
||||
},
|
||||
Test::Convert(test) => test.exec(ctx),
|
||||
Test::Plugin(test) => TestResult::Event {
|
||||
event: ctx.eval_plugin_arguments(test),
|
||||
is_not: test.is_not,
|
||||
},
|
||||
Test::True => TestResult::Bool(true),
|
||||
Test::False => TestResult::Bool(false),
|
||||
Test::Invalid(invalid) => {
|
||||
TestResult::Error(RuntimeError::InvalidInstruction(invalid.clone()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,300 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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::{
|
||||
parsers::{
|
||||
fields::address::{
|
||||
parse_address_detail_part, parse_address_domain, parse_address_local_part,
|
||||
parse_address_user_part,
|
||||
},
|
||||
MessageStream,
|
||||
},
|
||||
Addr, Address, Header, HeaderValue,
|
||||
};
|
||||
|
||||
use crate::sieve::{
|
||||
compiler::{
|
||||
grammar::{tests::test_address::TestAddress, AddressPart, MatchType},
|
||||
Number,
|
||||
},
|
||||
runtime::Variable,
|
||||
Context, Event,
|
||||
};
|
||||
|
||||
use super::TestResult;
|
||||
|
||||
impl TestAddress {
|
||||
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
|
||||
let key_list = ctx.eval_values(&self.key_list);
|
||||
let header_list = ctx.parse_header_names(&self.header_list);
|
||||
|
||||
let result = match &self.match_type {
|
||||
MatchType::Is | MatchType::Contains => {
|
||||
let is_is = matches!(&self.match_type, MatchType::Is);
|
||||
ctx.find_headers(
|
||||
&header_list,
|
||||
self.index,
|
||||
self.mime_anychild,
|
||||
|header, _, _| {
|
||||
ctx.find_addresses(header, &self.address_part, |value| {
|
||||
for key in &key_list {
|
||||
if is_is {
|
||||
if self.comparator.is(&Variable::from(value), key) {
|
||||
return true;
|
||||
}
|
||||
} else if self.comparator.contains(value, key.to_cow().as_ref()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
MatchType::Value(rel_match) => ctx.find_headers(
|
||||
&header_list,
|
||||
self.index,
|
||||
self.mime_anychild,
|
||||
|header, _, _| {
|
||||
ctx.find_addresses(header, &self.address_part, |value| {
|
||||
for key in &key_list {
|
||||
if self
|
||||
.comparator
|
||||
.relational(rel_match, &Variable::from(value), key)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
})
|
||||
},
|
||||
),
|
||||
MatchType::Matches(capture_positions) | MatchType::Regex(capture_positions) => {
|
||||
let mut captured_positions = Vec::new();
|
||||
let is_matches = matches!(&self.match_type, MatchType::Matches(_));
|
||||
let result = ctx.find_headers(
|
||||
&header_list,
|
||||
self.index,
|
||||
self.mime_anychild,
|
||||
|header, _, _| {
|
||||
ctx.find_addresses(header, &self.address_part, |value| {
|
||||
for (pattern_expr, pattern) in key_list.iter().zip(self.key_list.iter())
|
||||
{
|
||||
if is_matches {
|
||||
if self.comparator.matches(
|
||||
value,
|
||||
pattern_expr.to_cow().as_ref(),
|
||||
*capture_positions,
|
||||
&mut captured_positions,
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
} else if self.comparator.regex(
|
||||
pattern,
|
||||
pattern_expr,
|
||||
value,
|
||||
*capture_positions,
|
||||
&mut captured_positions,
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
})
|
||||
},
|
||||
);
|
||||
if !captured_positions.is_empty() {
|
||||
ctx.set_match_variables(captured_positions);
|
||||
}
|
||||
result
|
||||
}
|
||||
MatchType::Count(rel_match) => {
|
||||
let mut count: i64 = 0;
|
||||
ctx.find_headers(
|
||||
&header_list,
|
||||
self.index,
|
||||
self.mime_anychild,
|
||||
|header, _, _| {
|
||||
ctx.find_addresses(header, &self.address_part, |value| {
|
||||
if !value.is_empty() {
|
||||
count += 1;
|
||||
}
|
||||
false
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
let mut result = false;
|
||||
for key in &key_list {
|
||||
if rel_match.cmp(&Number::from(count), &key.to_number()) {
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
MatchType::List => {
|
||||
let mut values: Vec<String> = Vec::new();
|
||||
|
||||
ctx.find_headers(
|
||||
&header_list,
|
||||
self.index,
|
||||
self.mime_anychild,
|
||||
|header, _, _| {
|
||||
ctx.find_addresses(header, &self.address_part, |value| {
|
||||
if !value.is_empty() && !values.iter().any(|v| v.eq(value)) {
|
||||
values.push(value.to_string());
|
||||
}
|
||||
false
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
if !values.is_empty() {
|
||||
return TestResult::Event {
|
||||
event: Event::ListContains {
|
||||
lists: ctx.eval_values_owned(&self.key_list),
|
||||
values,
|
||||
match_as: self.comparator.as_match(),
|
||||
},
|
||||
is_not: self.is_not,
|
||||
};
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
TestResult::Bool(result ^ self.is_not)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> Context<'x> {
|
||||
#[allow(unused_assignments)]
|
||||
pub(crate) fn find_addresses(
|
||||
&self,
|
||||
header: &Header,
|
||||
part: &AddressPart,
|
||||
mut visitor_fnc: impl FnMut(&str) -> bool,
|
||||
) -> bool {
|
||||
match &header.value {
|
||||
HeaderValue::Address(Address::List(addr_list)) => {
|
||||
for addr in addr_list {
|
||||
if let Some(addr) = part.eval(addr) {
|
||||
if visitor_fnc(addr) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
HeaderValue::Address(Address::Group(group_list)) => {
|
||||
for group in group_list {
|
||||
for addr in &group.addresses {
|
||||
if let Some(addr) = part.eval(addr) {
|
||||
if visitor_fnc(addr) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
_ => {
|
||||
let mut raw_header = None;
|
||||
let bytes = if header.offset_end > 0 {
|
||||
self.message
|
||||
.raw_message
|
||||
.get(header.offset_start..header.offset_end)
|
||||
.unwrap_or(b"")
|
||||
} else if let HeaderValue::Text(text) = &header.value {
|
||||
// Inserted header
|
||||
raw_header = format!("{text}\n").into_bytes().into();
|
||||
raw_header.as_deref().unwrap()
|
||||
} else {
|
||||
b""
|
||||
};
|
||||
|
||||
match MessageStream::new(bytes).parse_address() {
|
||||
HeaderValue::Address(Address::List(addr_list)) => {
|
||||
for addr in &addr_list {
|
||||
if let Some(addr) = part.eval(addr) {
|
||||
if visitor_fnc(addr) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
HeaderValue::Address(Address::Group(group_list)) => {
|
||||
for group in group_list {
|
||||
for addr in &group.addresses {
|
||||
if let Some(addr) = part.eval(addr) {
|
||||
if visitor_fnc(addr) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
_ => visitor_fnc(""),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AddressPart {
|
||||
pub(crate) fn eval<'x>(&self, addr: &'x Addr<'x>) -> Option<&'x str> {
|
||||
let email = addr.address.as_deref().or(addr.name.as_deref());
|
||||
match (self, email) {
|
||||
(AddressPart::All, _) => email,
|
||||
(AddressPart::LocalPart, Some(email)) if !email.is_empty() => {
|
||||
parse_address_local_part(email)
|
||||
}
|
||||
(AddressPart::Domain, Some(email)) if !email.is_empty() => parse_address_domain(email),
|
||||
(AddressPart::User, Some(email)) if !email.is_empty() => parse_address_user_part(email),
|
||||
(AddressPart::Detail, Some(email)) if !email.is_empty() => {
|
||||
parse_address_detail_part(email)
|
||||
}
|
||||
(AddressPart::Name, _) => addr.name.as_deref(),
|
||||
_ => email,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn eval_string<'x>(&self, addr: &'x str) -> Option<&'x str> {
|
||||
if !addr.is_empty() {
|
||||
match self {
|
||||
AddressPart::All => addr.into(),
|
||||
AddressPart::LocalPart => parse_address_local_part(addr),
|
||||
AddressPart::Domain => parse_address_domain(addr),
|
||||
AddressPart::User => parse_address_user_part(addr),
|
||||
AddressPart::Detail => parse_address_detail_part(addr),
|
||||
_ => addr.into(),
|
||||
}
|
||||
} else {
|
||||
addr.into()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,229 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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, MimeHeaders, PartType};
|
||||
|
||||
use crate::sieve::{
|
||||
compiler::{
|
||||
grammar::{
|
||||
tests::test_body::{BodyTransform, TestBody},
|
||||
MatchType,
|
||||
},
|
||||
Number,
|
||||
},
|
||||
runtime::Variable,
|
||||
Context,
|
||||
};
|
||||
|
||||
use super::{mime::ContentTypeFilter, TestResult};
|
||||
|
||||
impl TestBody {
|
||||
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
|
||||
// Check Subject (not a Sieve standard)
|
||||
let key_list = ctx.eval_values(&self.key_list);
|
||||
if self.include_subject {
|
||||
let subject = if !matches!(&self.body_transform, BodyTransform::Raw) {
|
||||
ctx.message.subject().unwrap_or_default()
|
||||
} else {
|
||||
ctx.message.header_raw("Subject").unwrap_or_default()
|
||||
};
|
||||
|
||||
for (key, pattern) in key_list.iter().zip(self.key_list.iter()) {
|
||||
let result = match &self.match_type {
|
||||
MatchType::Is => self.comparator.is(&Variable::from(subject), key),
|
||||
MatchType::Contains => self.comparator.contains(subject, key.to_cow().as_ref()),
|
||||
MatchType::Value(rel_match) => {
|
||||
self.comparator
|
||||
.relational(rel_match, &Variable::from(subject), key)
|
||||
}
|
||||
MatchType::Matches(_) => {
|
||||
self.comparator
|
||||
.matches(subject, key.to_cow().as_ref(), 0, &mut Vec::new())
|
||||
}
|
||||
MatchType::Regex(_) => {
|
||||
self.comparator
|
||||
.regex(pattern, key, subject, 0, &mut Vec::new())
|
||||
}
|
||||
_ => break,
|
||||
};
|
||||
|
||||
if result {
|
||||
return TestResult::Bool(result ^ self.is_not);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ct_filter = match &self.body_transform {
|
||||
BodyTransform::Text | BodyTransform::Raw => Vec::new(),
|
||||
BodyTransform::Content(values) => {
|
||||
let mut ct_filter = Vec::with_capacity(values.len());
|
||||
for ct in values {
|
||||
let ct = ctx.eval_value(ct);
|
||||
if ct.is_empty() {
|
||||
break;
|
||||
} else if let Some(ctf) = ContentTypeFilter::parse(ct.into_cow().as_ref()) {
|
||||
ct_filter.push(ctf);
|
||||
} else {
|
||||
return TestResult::Bool(false ^ self.is_not);
|
||||
}
|
||||
}
|
||||
ct_filter
|
||||
}
|
||||
};
|
||||
|
||||
let result = if let MatchType::Count(rel_match) = &self.match_type {
|
||||
let mut count = 0;
|
||||
let mut result = false;
|
||||
|
||||
ctx.find_nested_parts(&ctx.message, &ct_filter, &mut |_part, _raw_message| {
|
||||
count += 1;
|
||||
false
|
||||
});
|
||||
|
||||
for key in &self.key_list {
|
||||
if rel_match.cmp(&Number::from(count), &ctx.eval_value(key).to_number()) {
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
} else {
|
||||
ctx.find_nested_parts(&ctx.message, &ct_filter, &mut |part, raw_message| {
|
||||
let text = match (&self.body_transform, &part.body) {
|
||||
(BodyTransform::Content(_), PartType::Message(message)) => {
|
||||
if let Some(part) = message.parts.get(0) {
|
||||
String::from_utf8_lossy(
|
||||
raw_message
|
||||
.get(part.raw_header_offset()..part.raw_body_offset())
|
||||
.unwrap_or(b""),
|
||||
)
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
(BodyTransform::Content(_), PartType::Multipart(_)) => {
|
||||
if let Some(boundary) =
|
||||
part.content_type().and_then(|ct| ct.attribute("boundary"))
|
||||
{
|
||||
let mime_body = std::str::from_utf8(
|
||||
raw_message
|
||||
.get(part.raw_body_offset()..part.raw_end_offset())
|
||||
.unwrap_or(b""),
|
||||
)
|
||||
.unwrap_or("");
|
||||
let mut mime_part = String::with_capacity(64);
|
||||
if let Some((prologue, epilogue)) =
|
||||
mime_body.split_once(&format!("\n--{boundary}"))
|
||||
{
|
||||
mime_part.push_str(prologue);
|
||||
if let Some((_, epilogue)) =
|
||||
epilogue.rsplit_once(&format!("\n--{boundary}--"))
|
||||
{
|
||||
mime_part.push_str(epilogue);
|
||||
}
|
||||
}
|
||||
mime_part.into()
|
||||
} else {
|
||||
String::from_utf8_lossy(
|
||||
raw_message
|
||||
.get(part.raw_body_offset()..part.raw_end_offset())
|
||||
.unwrap_or(b""),
|
||||
)
|
||||
}
|
||||
}
|
||||
(BodyTransform::Raw, _) => {
|
||||
match &part.body {
|
||||
PartType::Text(text) if part.raw_body_offset() == 0 => {
|
||||
// Inserted part
|
||||
text.as_ref().into()
|
||||
}
|
||||
_ if part.raw_end_offset() > part.raw_body_offset() => {
|
||||
String::from_utf8_lossy(
|
||||
raw_message
|
||||
.get(part.raw_body_offset()..part.raw_end_offset())
|
||||
.unwrap_or(b""),
|
||||
)
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
}
|
||||
(_, PartType::Text(text))
|
||||
| (BodyTransform::Content(_), PartType::Html(text)) => text.as_ref().into(),
|
||||
(_, PartType::Html(html)) => html_to_text(html.as_ref()).into(),
|
||||
(
|
||||
BodyTransform::Text,
|
||||
PartType::Binary(bytes) | PartType::InlineBinary(bytes),
|
||||
) if part.content_type().map_or(false, |ct| {
|
||||
ct.c_type.eq_ignore_ascii_case("application")
|
||||
&& ct.c_subtype.as_ref().map_or(false, |st| st.contains("xml"))
|
||||
}) =>
|
||||
{
|
||||
html_to_text(std::str::from_utf8(bytes.as_ref()).unwrap_or("")).into()
|
||||
}
|
||||
(
|
||||
BodyTransform::Content(_),
|
||||
PartType::Binary(bytes) | PartType::InlineBinary(bytes),
|
||||
) => String::from_utf8_lossy(bytes.as_ref()),
|
||||
_ => {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let mut result = false;
|
||||
|
||||
for (key, pattern) in key_list.iter().zip(self.key_list.iter()) {
|
||||
result = match &self.match_type {
|
||||
MatchType::Is => self.comparator.is(&Variable::from(text.as_ref()), key),
|
||||
MatchType::Contains => self
|
||||
.comparator
|
||||
.contains(text.as_ref(), key.to_cow().as_ref()),
|
||||
MatchType::Value(rel_match) => self.comparator.relational(
|
||||
rel_match,
|
||||
&Variable::from(text.as_ref()),
|
||||
key,
|
||||
),
|
||||
MatchType::Matches(_) => self.comparator.matches(
|
||||
text.as_ref(),
|
||||
key.to_cow().as_ref(),
|
||||
0,
|
||||
&mut Vec::new(),
|
||||
),
|
||||
MatchType::Regex(_) => {
|
||||
self.comparator
|
||||
.regex(pattern, key, text.as_ref(), 0, &mut Vec::new())
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if result {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
})
|
||||
};
|
||||
|
||||
TestResult::Bool(result ^ self.is_not)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,313 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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::{parsers::MessageStream, DateTime, Header, HeaderValue};
|
||||
|
||||
use crate::sieve::{
|
||||
compiler::{
|
||||
grammar::{
|
||||
tests::test_date::{DatePart, TestCurrentDate, TestDate, Zone},
|
||||
MatchType,
|
||||
},
|
||||
Number,
|
||||
},
|
||||
runtime::Variable,
|
||||
Context, Event,
|
||||
};
|
||||
|
||||
use super::TestResult;
|
||||
|
||||
impl TestDate {
|
||||
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
|
||||
let header_name = if let Some(header_name) = ctx.parse_header_name(&self.header_name) {
|
||||
header_name
|
||||
} else {
|
||||
return TestResult::Bool(false ^ self.is_not);
|
||||
};
|
||||
|
||||
let result = match &self.match_type {
|
||||
MatchType::Count(rel_match) => {
|
||||
let mut date_count = 0;
|
||||
ctx.find_headers(
|
||||
&[header_name],
|
||||
self.index,
|
||||
self.mime_anychild,
|
||||
|header, _, _| {
|
||||
if ctx.find_dates(header).is_some() {
|
||||
date_count += 1;
|
||||
}
|
||||
false
|
||||
},
|
||||
);
|
||||
|
||||
let mut result = false;
|
||||
for key in &self.key_list {
|
||||
if rel_match.cmp(&Number::from(date_count), &ctx.eval_value(key).to_number()) {
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
MatchType::List => {
|
||||
let mut values = Vec::new();
|
||||
ctx.find_headers(
|
||||
&[header_name],
|
||||
self.index,
|
||||
self.mime_anychild,
|
||||
|header, _, _| {
|
||||
if let Some(dt) = ctx.find_dates(header) {
|
||||
let value = self.date_part.eval(self.zone.eval(dt.as_ref()).as_ref());
|
||||
if !value.is_empty() && !values.iter().any(|v: &String| v.eq(&value)) {
|
||||
values.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
},
|
||||
);
|
||||
if !values.is_empty() {
|
||||
return TestResult::Event {
|
||||
event: Event::ListContains {
|
||||
lists: ctx.eval_values_owned(&self.key_list),
|
||||
values,
|
||||
match_as: self.comparator.as_match(),
|
||||
},
|
||||
is_not: self.is_not,
|
||||
};
|
||||
}
|
||||
false
|
||||
}
|
||||
_ => {
|
||||
let key_list = ctx.eval_values(&self.key_list);
|
||||
let mut captured_values = Vec::new();
|
||||
|
||||
let result = ctx.find_headers(
|
||||
&[header_name],
|
||||
self.index,
|
||||
self.mime_anychild,
|
||||
|header, _, _| {
|
||||
if let Some(dt) = ctx.find_dates(header) {
|
||||
let date_part =
|
||||
self.date_part.eval(self.zone.eval(dt.as_ref()).as_ref());
|
||||
for key in &key_list {
|
||||
if match &self.match_type {
|
||||
MatchType::Is => {
|
||||
self.comparator.is(&Variable::from(&date_part), key)
|
||||
}
|
||||
MatchType::Contains => {
|
||||
self.comparator.contains(&date_part, key.to_cow().as_ref())
|
||||
}
|
||||
MatchType::Value(rel_match) => self.comparator.relational(
|
||||
rel_match,
|
||||
&Variable::from(&date_part),
|
||||
key,
|
||||
),
|
||||
MatchType::Matches(capture_positions) => {
|
||||
self.comparator.matches(
|
||||
&date_part,
|
||||
key.to_cow().as_ref(),
|
||||
*capture_positions,
|
||||
&mut captured_values,
|
||||
)
|
||||
}
|
||||
MatchType::Regex(capture_positions) => self.comparator.matches(
|
||||
&date_part,
|
||||
key.to_cow().as_ref(),
|
||||
*capture_positions,
|
||||
&mut captured_values,
|
||||
),
|
||||
MatchType::Count(_) | MatchType::List => false,
|
||||
} {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
},
|
||||
);
|
||||
if !captured_values.is_empty() {
|
||||
ctx.set_match_variables(captured_values);
|
||||
}
|
||||
result
|
||||
}
|
||||
};
|
||||
|
||||
TestResult::Bool(result ^ self.is_not)
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCurrentDate {
|
||||
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
|
||||
let mut result = false;
|
||||
|
||||
match &self.match_type {
|
||||
MatchType::Count(rel_match) => {
|
||||
for key in &self.key_list {
|
||||
if rel_match.cmp(&Number::from(1.0), &ctx.eval_value(key).to_number()) {
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
MatchType::List => {
|
||||
let value = self.date_part.eval(
|
||||
&(if let Some(zone) = self.zone {
|
||||
DateTime::from_timestamp(ctx.current_time).to_timezone(zone)
|
||||
} else {
|
||||
DateTime::from_timestamp(ctx.current_time)
|
||||
}),
|
||||
);
|
||||
if !value.is_empty() {
|
||||
return TestResult::Event {
|
||||
event: Event::ListContains {
|
||||
lists: ctx.eval_values_owned(&self.key_list),
|
||||
values: vec![value],
|
||||
match_as: self.comparator.as_match(),
|
||||
},
|
||||
is_not: self.is_not,
|
||||
};
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let mut captured_values = Vec::new();
|
||||
let date_part = self.date_part.eval(
|
||||
&(if let Some(zone) = self.zone {
|
||||
DateTime::from_timestamp(ctx.current_time).to_timezone(zone)
|
||||
} else {
|
||||
DateTime::from_timestamp(ctx.current_time)
|
||||
}),
|
||||
);
|
||||
|
||||
for key in &self.key_list {
|
||||
let key = ctx.eval_value(key);
|
||||
|
||||
if match &self.match_type {
|
||||
MatchType::Is => self.comparator.is(&Variable::from(&date_part), &key),
|
||||
MatchType::Contains => self
|
||||
.comparator
|
||||
.contains(&date_part, key.into_cow().as_ref()),
|
||||
MatchType::Value(rel_match) => {
|
||||
self.comparator
|
||||
.relational(rel_match, &Variable::from(&date_part), &key)
|
||||
}
|
||||
MatchType::Matches(capture_positions) => self.comparator.matches(
|
||||
&date_part,
|
||||
key.into_cow().as_ref(),
|
||||
*capture_positions,
|
||||
&mut captured_values,
|
||||
),
|
||||
MatchType::Regex(capture_positions) => self.comparator.matches(
|
||||
&date_part,
|
||||
key.into_cow().as_ref(),
|
||||
*capture_positions,
|
||||
&mut captured_values,
|
||||
),
|
||||
MatchType::Count(_) | MatchType::List => false,
|
||||
} {
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !captured_values.is_empty() {
|
||||
ctx.set_match_variables(captured_values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TestResult::Bool(result ^ self.is_not)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> Context<'x> {
|
||||
#[allow(unused_assignments)]
|
||||
pub(crate) fn find_dates(&self, header: &'x Header) -> Option<Cow<'x, DateTime>> {
|
||||
if let HeaderValue::DateTime(dt) = &header.value {
|
||||
if dt.is_valid() {
|
||||
return Some(Cow::Borrowed(dt));
|
||||
}
|
||||
} else if header.offset_end > 0 {
|
||||
let bytes = self
|
||||
.message
|
||||
.raw_message
|
||||
.get(header.offset_start..header.offset_end)?;
|
||||
if let HeaderValue::DateTime(dt) = MessageStream::new(bytes).parse_date() {
|
||||
if dt.is_valid() {
|
||||
return Some(Cow::Owned(dt));
|
||||
}
|
||||
}
|
||||
} else if let HeaderValue::Text(text) = &header.value {
|
||||
// Inserted header
|
||||
let bytes = format!("{text}\n").into_bytes();
|
||||
if let HeaderValue::DateTime(dt) = MessageStream::new(&bytes).parse_date() {
|
||||
if dt.is_valid() {
|
||||
return Some(Cow::Owned(dt));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl DatePart {
|
||||
fn eval(&self, dt: &DateTime) -> String {
|
||||
match self {
|
||||
DatePart::Year => format!("{:04}", dt.year),
|
||||
DatePart::Month => format!("{:02}", dt.month),
|
||||
DatePart::Day => format!("{:02}", dt.day),
|
||||
DatePart::Date => format!("{:04}-{:02}-{:02}", dt.year, dt.month, dt.day,),
|
||||
DatePart::Julian => ((dt.julian_day() as f64 - 2400000.5) as i64).to_string(),
|
||||
DatePart::Hour => format!("{:02}", dt.hour),
|
||||
DatePart::Minute => format!("{:02}", dt.minute),
|
||||
DatePart::Second => format!("{:02}", dt.second),
|
||||
DatePart::Time => format!("{:02}:{:02}:{:02}", dt.hour, dt.minute, dt.second,),
|
||||
DatePart::Iso8601 => dt.to_rfc3339(),
|
||||
DatePart::Std11 => dt.to_rfc822(),
|
||||
DatePart::Zone => format!(
|
||||
"{}{:02}{:02}",
|
||||
if dt.tz_before_gmt && (dt.tz_hour > 0 || dt.tz_minute > 0) {
|
||||
"-"
|
||||
} else {
|
||||
"+"
|
||||
},
|
||||
dt.tz_hour,
|
||||
dt.tz_minute
|
||||
),
|
||||
DatePart::Weekday => dt.day_of_week().to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Zone {
|
||||
pub(crate) fn eval<'x>(&self, dt: &'x DateTime) -> Cow<'x, DateTime> {
|
||||
match self {
|
||||
Zone::Time(tz) => Cow::Owned(dt.to_timezone(*tz)),
|
||||
Zone::Original => Cow::Borrowed(dt),
|
||||
Zone::Local => Cow::Owned(DateTime::from_timestamp(dt.to_timestamp())),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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::{parsers::MessageStream, HeaderValue};
|
||||
|
||||
use crate::sieve::{
|
||||
compiler::grammar::tests::test_duplicate::{DupMatch, TestDuplicate},
|
||||
Context, Event,
|
||||
};
|
||||
|
||||
use super::TestResult;
|
||||
|
||||
impl TestDuplicate {
|
||||
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
|
||||
let id = match &self.dup_match {
|
||||
DupMatch::Header(header_name) => {
|
||||
let mut value = String::new();
|
||||
if let Some(header_name) = ctx.parse_header_name(header_name) {
|
||||
ctx.find_headers(&[header_name], None, true, |header, _, _| {
|
||||
if header.offset_end > 0 {
|
||||
if let Some(bytes) = ctx
|
||||
.message
|
||||
.raw_message
|
||||
.get(header.offset_start..header.offset_end)
|
||||
{
|
||||
if let HeaderValue::Text(id) = MessageStream::new(bytes).parse_id()
|
||||
{
|
||||
if !id.is_empty() {
|
||||
value = id.to_string();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let HeaderValue::Text(text) = &header.value {
|
||||
// Inserted header
|
||||
let bytes = format!("{text}\n").into_bytes();
|
||||
if let HeaderValue::Text(id) = MessageStream::new(&bytes).parse_id() {
|
||||
if !id.is_empty() {
|
||||
value = id.to_string();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
});
|
||||
}
|
||||
value.into()
|
||||
}
|
||||
DupMatch::UniqueId(s) => ctx.eval_value(s).into_cow(),
|
||||
DupMatch::Default => ctx.message.message_id().unwrap_or("").into(),
|
||||
};
|
||||
|
||||
TestResult::Event {
|
||||
event: Event::DuplicateId {
|
||||
id: if id.is_empty() {
|
||||
return TestResult::Bool(false ^ self.is_not);
|
||||
} else if let Some(handle) = &self.handle {
|
||||
format!("{}{}", ctx.eval_value(handle).into_cow(), id)
|
||||
} else {
|
||||
id.into_owned()
|
||||
},
|
||||
expiry: self.seconds.unwrap_or(ctx.runtime.default_duplicate_expiry),
|
||||
last: self.last,
|
||||
},
|
||||
is_not: self.is_not,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,279 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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;
|
||||
|
||||
use crate::sieve::{
|
||||
compiler::{
|
||||
grammar::{tests::test_envelope::TestEnvelope, MatchType},
|
||||
Number,
|
||||
},
|
||||
runtime::Variable,
|
||||
Context, Envelope, Event,
|
||||
};
|
||||
|
||||
use super::TestResult;
|
||||
|
||||
impl TestEnvelope {
|
||||
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
|
||||
let key_list = ctx.eval_values(&self.key_list);
|
||||
|
||||
let result = match &self.match_type {
|
||||
MatchType::Is | MatchType::Contains => {
|
||||
let is_is = matches!(&self.match_type, MatchType::Is);
|
||||
|
||||
ctx.find_envelopes(self, |value| {
|
||||
for key in &key_list {
|
||||
if is_is {
|
||||
if self.comparator.is(&Variable::from(value), key) {
|
||||
return true;
|
||||
}
|
||||
} else if self.comparator.contains(value, key.to_cow().as_ref()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
})
|
||||
}
|
||||
MatchType::Value(rel_match) => ctx.find_envelopes(self, |value| {
|
||||
for key in &key_list {
|
||||
if self
|
||||
.comparator
|
||||
.relational(rel_match, &Variable::from(value), key)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}),
|
||||
MatchType::Matches(capture_positions) | MatchType::Regex(capture_positions) => {
|
||||
let mut captured_positions = Vec::new();
|
||||
let is_matches = matches!(&self.match_type, MatchType::Matches(_));
|
||||
|
||||
let result = ctx.find_envelopes(self, |value| {
|
||||
for (pattern_expr, pattern) in key_list.iter().zip(self.key_list.iter()) {
|
||||
if is_matches {
|
||||
if self.comparator.matches(
|
||||
value,
|
||||
pattern_expr.to_cow().as_ref(),
|
||||
*capture_positions,
|
||||
&mut captured_positions,
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
} else if self.comparator.regex(
|
||||
pattern,
|
||||
pattern_expr,
|
||||
value,
|
||||
*capture_positions,
|
||||
&mut captured_positions,
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
});
|
||||
|
||||
if !captured_positions.is_empty() {
|
||||
ctx.set_match_variables(captured_positions);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
MatchType::Count(rel_match) => {
|
||||
let mut count = 0;
|
||||
|
||||
ctx.find_envelopes(self, |value| {
|
||||
if !value.is_empty() {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
false
|
||||
});
|
||||
|
||||
let mut result = false;
|
||||
for key in &key_list {
|
||||
if rel_match.cmp(&Number::from(count), &key.to_number()) {
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
MatchType::List => {
|
||||
let mut values: Vec<String> = Vec::new();
|
||||
|
||||
ctx.find_envelopes(self, |value| {
|
||||
if !value.is_empty() && !values.iter().any(|v| v.eq(value)) {
|
||||
values.push(value.to_string());
|
||||
}
|
||||
|
||||
false
|
||||
});
|
||||
|
||||
if !values.is_empty() {
|
||||
return TestResult::Event {
|
||||
event: Event::ListContains {
|
||||
lists: ctx.eval_values_owned(&self.key_list),
|
||||
values,
|
||||
match_as: self.comparator.as_match(),
|
||||
},
|
||||
is_not: self.is_not,
|
||||
};
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
};
|
||||
TestResult::Bool(result ^ self.is_not)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> Context<'x> {
|
||||
fn find_envelopes(
|
||||
&self,
|
||||
test_envelope: &TestEnvelope,
|
||||
mut cb: impl FnMut(&str) -> bool,
|
||||
) -> bool {
|
||||
for (name, value) in &self.envelope {
|
||||
if test_envelope.envelope_list.contains(name)
|
||||
&& match name {
|
||||
Envelope::From | Envelope::To | Envelope::Orcpt => {
|
||||
if let Some(value) = test_envelope
|
||||
.address_part
|
||||
.eval_string(value.to_cow().as_ref())
|
||||
{
|
||||
cb(value)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Envelope::ByTimeAbsolute if test_envelope.zone.is_some() => {
|
||||
if let Some(dt) = DateTime::parse_rfc3339(value.to_cow().as_ref()) {
|
||||
cb(&dt.to_timezone(test_envelope.zone.unwrap()).to_rfc3339())
|
||||
} else {
|
||||
cb("")
|
||||
}
|
||||
}
|
||||
_ => cb(value.to_cow().as_ref()),
|
||||
}
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_envelope_address(addr: &str) -> Option<&str> {
|
||||
let addr = addr.as_bytes();
|
||||
let mut addr_start_pos = 0;
|
||||
let mut addr_end_pos = addr.len();
|
||||
let mut last_ch = 0;
|
||||
let mut at_pos = 0;
|
||||
let mut has_bracket = false;
|
||||
let mut in_path = false;
|
||||
|
||||
if addr.is_empty() {
|
||||
return "".into();
|
||||
}
|
||||
|
||||
for (pos, &ch) in addr.iter().enumerate() {
|
||||
match ch {
|
||||
b'<' => {
|
||||
if pos == 0 {
|
||||
addr_start_pos = pos + 1;
|
||||
has_bracket = true;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
b'>' => {
|
||||
if has_bracket && pos == addr.len() - 1 {
|
||||
if addr.len() > 2 {
|
||||
has_bracket = false;
|
||||
addr_end_pos = pos;
|
||||
} else {
|
||||
// <>
|
||||
return "".into();
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
b':' => {
|
||||
if at_pos != 0 {
|
||||
at_pos = 0;
|
||||
addr_start_pos = pos + 1;
|
||||
in_path = false;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
b',' => {
|
||||
if at_pos != 0 {
|
||||
at_pos = 0;
|
||||
in_path = true;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
b'@' => {
|
||||
if at_pos == 0 && pos != addr.len() - 1 {
|
||||
at_pos = pos;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
b'.' => {
|
||||
if (at_pos != 0 && last_ch == b'.') || last_ch == b'@' {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if ch.is_ascii_whitespace() || !ch.is_ascii() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
last_ch = ch;
|
||||
}
|
||||
|
||||
if !has_bracket && !in_path && at_pos > addr_start_pos && addr_end_pos - 1 > at_pos {
|
||||
std::str::from_utf8(&addr[addr_start_pos..addr_end_pos])
|
||||
.unwrap()
|
||||
.into()
|
||||
} else {
|
||||
match addr.get(addr_start_pos..addr_end_pos) {
|
||||
Some(addr) if at_pos == 0 && addr.eq_ignore_ascii_case(b"mailer-daemon") => {
|
||||
std::str::from_utf8(addr).unwrap().into()
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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::tests::test_exists::TestExists, Context};
|
||||
|
||||
use super::{mime::SubpartIterator, TestResult};
|
||||
|
||||
impl TestExists {
|
||||
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
|
||||
let header_names = ctx.parse_header_names(&self.header_names);
|
||||
let mut header_exists = vec![false; header_names.len()];
|
||||
let parts = [ctx.part];
|
||||
let mut part_iter = SubpartIterator::new(ctx, &parts, self.mime_anychild);
|
||||
let mut result = false;
|
||||
|
||||
while let Some((_, message_part)) = part_iter.next() {
|
||||
for (pos, header_name) in header_names.iter().enumerate() {
|
||||
if !header_exists[pos]
|
||||
&& message_part.headers.iter().any(|h| &h.name == header_name)
|
||||
{
|
||||
header_exists[pos] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if header_exists.iter().all(|v| *v) {
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
TestResult::Bool(result ^ self.is_not)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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::tests::test_extlists::TestValidExtList, Context};
|
||||
|
||||
use super::TestResult;
|
||||
|
||||
impl TestValidExtList {
|
||||
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
|
||||
let mut num_valid = 0;
|
||||
|
||||
for list in &self.list_names {
|
||||
if ctx
|
||||
.runtime
|
||||
.valid_ext_lists
|
||||
.contains(&ctx.eval_value(list).into_cow())
|
||||
{
|
||||
num_valid += 1;
|
||||
}
|
||||
}
|
||||
|
||||
TestResult::Bool((num_valid == self.list_names.len()) ^ self.is_not)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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::{tests::test_hasflag::TestHasFlag, MatchType},
|
||||
Number, VariableType,
|
||||
},
|
||||
runtime::Variable,
|
||||
Context,
|
||||
};
|
||||
|
||||
use super::TestResult;
|
||||
|
||||
impl TestHasFlag {
|
||||
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
|
||||
let mut variable_list_ = None;
|
||||
let variable_list = if !self.variable_list.is_empty() {
|
||||
&self.variable_list
|
||||
} else {
|
||||
variable_list_.get_or_insert_with(|| vec![VariableType::Global("__flags".to_string())])
|
||||
};
|
||||
|
||||
let result = if let MatchType::Count(rel_match) = &self.match_type {
|
||||
let mut flag_count = 0;
|
||||
for variable in variable_list {
|
||||
match ctx.get_variable(variable) {
|
||||
Some(flags) if !flags.is_empty() => {
|
||||
flag_count += flags.to_cow().split(' ').count();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
let mut result = false;
|
||||
for key in &self.flags {
|
||||
if rel_match.cmp(
|
||||
&Number::from(flag_count as i64),
|
||||
&ctx.eval_value(key).to_number(),
|
||||
) {
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
result
|
||||
} else {
|
||||
let mut captured_values = Vec::new();
|
||||
let result = ctx.tokenize_flags(&self.flags, |check_flag| {
|
||||
for variable in variable_list {
|
||||
match ctx.get_variable(variable) {
|
||||
Some(flags) if !flags.is_empty() => {
|
||||
for flag in flags.to_cow().split(' ') {
|
||||
if match &self.match_type {
|
||||
MatchType::Is => self
|
||||
.comparator
|
||||
.is(&Variable::from(flag), &Variable::from(check_flag)),
|
||||
MatchType::Contains => {
|
||||
self.comparator.contains(flag, check_flag)
|
||||
}
|
||||
MatchType::Value(rel_match) => self.comparator.relational(
|
||||
rel_match,
|
||||
&Variable::from(flag),
|
||||
&Variable::from(check_flag),
|
||||
),
|
||||
MatchType::Matches(capture_positions) => {
|
||||
self.comparator.matches(
|
||||
flag,
|
||||
check_flag,
|
||||
*capture_positions,
|
||||
&mut captured_values,
|
||||
)
|
||||
}
|
||||
MatchType::Regex(capture_positions) => self.comparator.matches(
|
||||
flag,
|
||||
check_flag,
|
||||
*capture_positions,
|
||||
&mut captured_values,
|
||||
),
|
||||
MatchType::Count(_) | MatchType::List => false,
|
||||
} {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
false
|
||||
});
|
||||
if !captured_values.is_empty() {
|
||||
ctx.set_match_variables(captured_values);
|
||||
}
|
||||
result
|
||||
};
|
||||
|
||||
TestResult::Bool(result ^ self.is_not)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,379 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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::{parsers::MessageStream, Header, HeaderName, HeaderValue};
|
||||
|
||||
use crate::sieve::{
|
||||
compiler::{
|
||||
grammar::{actions::action_mime::MimeOpts, tests::test_header::TestHeader, MatchType},
|
||||
Number, Value,
|
||||
},
|
||||
runtime::Variable,
|
||||
Context, Event,
|
||||
};
|
||||
|
||||
use super::{mime::SubpartIterator, TestResult};
|
||||
|
||||
impl TestHeader {
|
||||
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
|
||||
let key_list = ctx.eval_values(&self.key_list);
|
||||
let header_list = ctx.parse_header_names(&self.header_list);
|
||||
let mime_opts = match &self.mime_opts {
|
||||
MimeOpts::Type => MimeOpts::Type,
|
||||
MimeOpts::Subtype => MimeOpts::Subtype,
|
||||
MimeOpts::ContentType => MimeOpts::ContentType,
|
||||
MimeOpts::Param(params) => MimeOpts::Param(ctx.eval_values(params)),
|
||||
MimeOpts::None => MimeOpts::None,
|
||||
};
|
||||
|
||||
let result = match &self.match_type {
|
||||
MatchType::Is | MatchType::Contains => {
|
||||
let is_is = matches!(&self.match_type, MatchType::Is);
|
||||
ctx.find_headers(
|
||||
&header_list,
|
||||
self.index,
|
||||
self.mime_anychild,
|
||||
|header, _, _| {
|
||||
ctx.find_header_values(header, &mime_opts, |value| {
|
||||
for key in &key_list {
|
||||
if is_is {
|
||||
if self.comparator.is(&Variable::from(value), key) {
|
||||
return true;
|
||||
}
|
||||
} else if self.comparator.contains(value, key.to_cow().as_ref()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
MatchType::Value(rel_match) => ctx.find_headers(
|
||||
&header_list,
|
||||
self.index,
|
||||
self.mime_anychild,
|
||||
|header, _, _| {
|
||||
ctx.find_header_values(header, &mime_opts, |value| {
|
||||
for key in &key_list {
|
||||
if self
|
||||
.comparator
|
||||
.relational(rel_match, &Variable::from(value), key)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
})
|
||||
},
|
||||
),
|
||||
MatchType::Matches(capture_positions) | MatchType::Regex(capture_positions) => {
|
||||
let mut captured_values = Vec::new();
|
||||
let is_matches = matches!(&self.match_type, MatchType::Matches(_));
|
||||
let result = ctx.find_headers(
|
||||
&header_list,
|
||||
self.index,
|
||||
self.mime_anychild,
|
||||
|header, _, _| {
|
||||
ctx.find_header_values(header, &mime_opts, |value| {
|
||||
for (pattern_expr, pattern) in key_list.iter().zip(self.key_list.iter())
|
||||
{
|
||||
if is_matches {
|
||||
if self.comparator.matches(
|
||||
value,
|
||||
pattern_expr.to_cow().as_ref(),
|
||||
*capture_positions,
|
||||
&mut captured_values,
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
} else if self.comparator.regex(
|
||||
pattern,
|
||||
pattern_expr,
|
||||
value,
|
||||
*capture_positions,
|
||||
&mut captured_values,
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
})
|
||||
},
|
||||
);
|
||||
if !captured_values.is_empty() {
|
||||
ctx.set_match_variables(captured_values);
|
||||
}
|
||||
result
|
||||
}
|
||||
MatchType::Count(rel_match) => {
|
||||
let mut count = 0;
|
||||
ctx.find_headers(
|
||||
&header_list,
|
||||
self.index,
|
||||
self.mime_anychild,
|
||||
|header, _, _| {
|
||||
match &mime_opts {
|
||||
MimeOpts::None => {
|
||||
count += 1;
|
||||
}
|
||||
MimeOpts::Type | MimeOpts::Subtype | MimeOpts::ContentType => {
|
||||
if let HeaderValue::ContentType(_) = &header.value {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
MimeOpts::Param(params) => {
|
||||
if let HeaderValue::ContentType(ct) = &header.value {
|
||||
if let Some(attributes) = &ct.attributes {
|
||||
for (attr_name, _) in attributes {
|
||||
if params
|
||||
.iter()
|
||||
.any(|p| p.to_cow().eq_ignore_ascii_case(attr_name))
|
||||
{
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
},
|
||||
);
|
||||
|
||||
let mut result = false;
|
||||
for key in &key_list {
|
||||
if rel_match.cmp(&Number::from(count), &key.to_number()) {
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
MatchType::List => {
|
||||
let mut values: Vec<String> = Vec::new();
|
||||
ctx.find_headers(
|
||||
&header_list,
|
||||
self.index,
|
||||
self.mime_anychild,
|
||||
|header, _, _| {
|
||||
ctx.find_header_values(header, &mime_opts, |value| {
|
||||
if !value.is_empty() && !values.iter().any(|v| v.eq(value)) {
|
||||
values.push(value.to_string());
|
||||
}
|
||||
false
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
if !values.is_empty() {
|
||||
return TestResult::Event {
|
||||
event: Event::ListContains {
|
||||
lists: ctx.eval_values_owned(&self.key_list),
|
||||
values,
|
||||
match_as: self.comparator.as_match(),
|
||||
},
|
||||
is_not: self.is_not,
|
||||
};
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
TestResult::Bool(result ^ self.is_not)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> Context<'x> {
|
||||
pub(crate) fn parse_header_names<'z: 'y, 'y>(
|
||||
&'z self,
|
||||
header_names: &'y [Value],
|
||||
) -> Vec<HeaderName<'y>> {
|
||||
let mut result = Vec::with_capacity(header_names.len());
|
||||
for header_name in header_names {
|
||||
if let Some(header_name) = self.parse_header_name(header_name) {
|
||||
result.push(header_name);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn parse_header_name<'z: 'y, 'y>(
|
||||
&'z self,
|
||||
header_name: &'y Value,
|
||||
) -> Option<HeaderName<'y>> {
|
||||
HeaderName::parse(self.eval_value(header_name).into_cow())
|
||||
}
|
||||
|
||||
pub(crate) fn find_headers(
|
||||
&self,
|
||||
header_names: &[HeaderName],
|
||||
index: Option<i32>,
|
||||
any_child: bool,
|
||||
mut visitor_fnc: impl FnMut(&Header, usize, usize) -> bool,
|
||||
) -> bool {
|
||||
let parts = [self.part];
|
||||
let mut part_iter = SubpartIterator::new(self, &parts, any_child);
|
||||
|
||||
while let Some((part_id, message_part)) = part_iter.next() {
|
||||
'outer: for header_name in header_names {
|
||||
match index {
|
||||
None => {
|
||||
for (pos, header) in message_part
|
||||
.headers
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, h)| &h.name == header_name)
|
||||
{
|
||||
if visitor_fnc(header, part_id, pos) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(index) if index >= 0 => {
|
||||
let mut header_count = 0;
|
||||
|
||||
for (pos, header) in message_part.headers.iter().enumerate() {
|
||||
if &header.name == header_name {
|
||||
header_count += 1;
|
||||
if header_count == index {
|
||||
if visitor_fnc(header, part_id, pos) {
|
||||
return true;
|
||||
}
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(index) => {
|
||||
let index = -index;
|
||||
let mut header_count = 0;
|
||||
|
||||
for (pos, header) in message_part.headers.iter().enumerate().rev() {
|
||||
if &header.name == header_name {
|
||||
header_count += 1;
|
||||
if header_count == index {
|
||||
if visitor_fnc(header, part_id, pos) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[allow(unused_assignments)]
|
||||
pub(crate) fn find_header_values(
|
||||
&self,
|
||||
header: &Header,
|
||||
mime_opts: &MimeOpts<Variable>,
|
||||
mut visitor_fnc: impl FnMut(&str) -> bool,
|
||||
) -> bool {
|
||||
let mut raw_header = None;
|
||||
let mut header_value_ = None;
|
||||
let header_value = if header.offset_end != 0 {
|
||||
&header.value
|
||||
} else {
|
||||
let value = if let HeaderValue::Text(text) = &header.value {
|
||||
text.as_ref()
|
||||
} else {
|
||||
#[cfg(test)]
|
||||
panic!("Unexpected value.");
|
||||
#[cfg(not(test))]
|
||||
return false;
|
||||
};
|
||||
if mime_opts == &MimeOpts::None {
|
||||
return visitor_fnc(value);
|
||||
} else {
|
||||
raw_header = format!("{value}\n").into_bytes().into();
|
||||
header_value_ = MessageStream::new(raw_header.as_ref().unwrap())
|
||||
.parse_content_type()
|
||||
.into();
|
||||
header_value_.as_ref().unwrap()
|
||||
}
|
||||
};
|
||||
|
||||
match (mime_opts, header_value) {
|
||||
(MimeOpts::None, HeaderValue::Text(text))
|
||||
if matches!(
|
||||
&header.name,
|
||||
HeaderName::Subject
|
||||
| HeaderName::Comments
|
||||
| HeaderName::ContentDescription
|
||||
| HeaderName::ContentLocation
|
||||
| HeaderName::ContentTransferEncoding,
|
||||
) =>
|
||||
{
|
||||
visitor_fnc(text.as_ref())
|
||||
}
|
||||
(MimeOpts::None, _) => {
|
||||
if let HeaderValue::Text(text) = MessageStream::new(
|
||||
self.message
|
||||
.raw_message
|
||||
.get(header.offset_start..header.offset_end)
|
||||
.unwrap_or(b""),
|
||||
)
|
||||
.parse_unstructured()
|
||||
{
|
||||
visitor_fnc(text.as_ref())
|
||||
} else {
|
||||
visitor_fnc("")
|
||||
}
|
||||
}
|
||||
(MimeOpts::Type, HeaderValue::ContentType(ct)) => visitor_fnc(ct.c_type.as_ref()),
|
||||
(MimeOpts::Subtype, HeaderValue::ContentType(ct)) => {
|
||||
visitor_fnc(ct.c_subtype.as_deref().unwrap_or(""))
|
||||
}
|
||||
(MimeOpts::ContentType, HeaderValue::ContentType(ct)) => {
|
||||
if let Some(sub_type) = &ct.c_subtype {
|
||||
visitor_fnc(&format!("{}/{}", ct.c_type, sub_type))
|
||||
} else {
|
||||
visitor_fnc(ct.c_type.as_ref())
|
||||
}
|
||||
}
|
||||
(MimeOpts::Param(params), HeaderValue::ContentType(ct)) => {
|
||||
if let Some(attributes) = &ct.attributes {
|
||||
for param in params {
|
||||
for (attr_name, attr_value) in attributes {
|
||||
if param.to_cow().eq_ignore_ascii_case(attr_name)
|
||||
&& visitor_fnc(attr_value.as_ref())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
visitor_fnc("")
|
||||
}
|
||||
_ => visitor_fnc(""),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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::{
|
||||
tests::test_mailbox::{TestMetadata, TestMetadataExists},
|
||||
MatchType,
|
||||
},
|
||||
Number,
|
||||
},
|
||||
runtime::Variable,
|
||||
Context, Metadata,
|
||||
};
|
||||
|
||||
use super::TestResult;
|
||||
|
||||
impl TestMetadata {
|
||||
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
|
||||
let metadata = match &self.medatata {
|
||||
Metadata::Server { annotation } => Metadata::Server {
|
||||
annotation: ctx.eval_value(annotation).into_cow(),
|
||||
},
|
||||
Metadata::Mailbox { name, annotation } => Metadata::Mailbox {
|
||||
name: ctx.eval_value(name).into_cow(),
|
||||
annotation: ctx.eval_value(annotation).into_cow(),
|
||||
},
|
||||
};
|
||||
|
||||
let value = if let Some((_, value)) = [&ctx.metadata, &ctx.runtime.metadata]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.find(|(m, _)| match (m, &metadata) {
|
||||
(Metadata::Server { annotation: a }, Metadata::Server { annotation: b }) => {
|
||||
a.eq_ignore_ascii_case(b)
|
||||
}
|
||||
(
|
||||
Metadata::Mailbox {
|
||||
name: a,
|
||||
annotation: c,
|
||||
},
|
||||
Metadata::Mailbox {
|
||||
name: b,
|
||||
annotation: d,
|
||||
},
|
||||
) => a.eq(b) && c.eq_ignore_ascii_case(d),
|
||||
_ => false,
|
||||
}) {
|
||||
value.as_ref()
|
||||
} else {
|
||||
return TestResult::Bool(false ^ self.is_not);
|
||||
};
|
||||
|
||||
let mut result = false;
|
||||
if let MatchType::Count(match_type) = &self.match_type {
|
||||
for key in &self.key_list {
|
||||
if match_type.cmp(&Number::Float(1.0), &ctx.eval_value(key).to_number()) {
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut captured_values = Vec::new();
|
||||
|
||||
for pattern in &self.key_list {
|
||||
let key = ctx.eval_value(pattern);
|
||||
result = match &self.match_type {
|
||||
MatchType::Is => self.comparator.is(&Variable::from(value), &key),
|
||||
MatchType::Contains => self.comparator.contains(value, key.into_cow().as_ref()),
|
||||
MatchType::Value(relation) => {
|
||||
self.comparator
|
||||
.relational(relation, &Variable::from(value), &key)
|
||||
}
|
||||
MatchType::Matches(capture_positions) => self.comparator.matches(
|
||||
value,
|
||||
key.into_cow().as_ref(),
|
||||
*capture_positions,
|
||||
&mut captured_values,
|
||||
),
|
||||
MatchType::Regex(capture_positions) => self.comparator.regex(
|
||||
pattern,
|
||||
&key,
|
||||
value,
|
||||
*capture_positions,
|
||||
&mut captured_values,
|
||||
),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if result {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !captured_values.is_empty() {
|
||||
ctx.set_match_variables(captured_values);
|
||||
}
|
||||
}
|
||||
|
||||
TestResult::Bool(result ^ self.is_not)
|
||||
}
|
||||
}
|
||||
|
||||
impl TestMetadataExists {
|
||||
pub(crate) fn exec(&self, ctx: &Context) -> TestResult {
|
||||
let mailbox = self
|
||||
.mailbox
|
||||
.as_ref()
|
||||
.map(|s| ctx.eval_value(s).into_string());
|
||||
let mut annotations = ctx.eval_values(&self.annotation_names);
|
||||
|
||||
for (metadata, _) in [&ctx.metadata, &ctx.runtime.metadata].into_iter().flatten() {
|
||||
match (metadata, mailbox.as_ref()) {
|
||||
(Metadata::Server { annotation }, None) => {
|
||||
annotations.retain(|a| !a.to_cow().eq_ignore_ascii_case(annotation))
|
||||
}
|
||||
(Metadata::Mailbox { name, annotation }, Some(mailbox)) if name.eq(mailbox) => {
|
||||
annotations.retain(|a| !a.to_cow().eq_ignore_ascii_case(annotation));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
if annotations.is_empty() {
|
||||
return TestResult::Bool(true ^ self.is_not);
|
||||
}
|
||||
}
|
||||
|
||||
TestResult::Bool(false ^ self.is_not)
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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 crate::sieve::{
|
||||
compiler::{
|
||||
grammar::{
|
||||
tests::test_notify::{TestNotifyMethodCapability, TestValidNotifyMethod},
|
||||
MatchType,
|
||||
},
|
||||
Number,
|
||||
},
|
||||
runtime::{actions::action_notify::validate_uri, Variable},
|
||||
Context,
|
||||
};
|
||||
|
||||
use super::TestResult;
|
||||
|
||||
impl TestValidNotifyMethod {
|
||||
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
|
||||
let mut num_valid = 0;
|
||||
|
||||
for uri in &self.notification_uris {
|
||||
let uri = ctx.eval_value(uri).into_cow();
|
||||
if let Some(scheme) = validate_uri(uri.as_ref()) {
|
||||
if ctx
|
||||
.runtime
|
||||
.valid_notification_uris
|
||||
.contains(&Cow::from(scheme))
|
||||
|| ctx.runtime.valid_notification_uris.contains(&uri)
|
||||
{
|
||||
num_valid += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TestResult::Bool((num_valid == self.notification_uris.len()) ^ self.is_not)
|
||||
}
|
||||
}
|
||||
|
||||
impl TestNotifyMethodCapability {
|
||||
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
|
||||
let uri = ctx.eval_value(&self.notification_uri).into_cow();
|
||||
if !ctx
|
||||
.eval_value(&self.notification_capability)
|
||||
.into_cow()
|
||||
.eq_ignore_ascii_case("online")
|
||||
|| !validate_uri(uri.as_ref()).map_or(false, |scheme| {
|
||||
ctx.runtime
|
||||
.valid_notification_uris
|
||||
.contains(&Cow::from(scheme))
|
||||
|| ctx.runtime.valid_notification_uris.contains(&uri)
|
||||
})
|
||||
{
|
||||
return TestResult::Bool(false ^ self.is_not);
|
||||
}
|
||||
|
||||
if let MatchType::Count(rel_match) = &self.match_type {
|
||||
for key in &self.key_list {
|
||||
if rel_match.cmp(&Number::from(1.0), &ctx.eval_value(key).to_number()) {
|
||||
return TestResult::Bool(true ^ self.is_not);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for pattern in &self.key_list {
|
||||
let key = ctx.eval_value(pattern);
|
||||
if match &self.match_type {
|
||||
MatchType::Is => self.comparator.is(&Variable::from("maybe"), &key),
|
||||
MatchType::Contains => {
|
||||
self.comparator.contains("maybe", key.into_cow().as_ref())
|
||||
}
|
||||
MatchType::Value(relation) => {
|
||||
self.comparator
|
||||
.relational(relation, &Variable::from("maybe"), &key)
|
||||
}
|
||||
MatchType::Matches(_) => self.comparator.matches(
|
||||
"maybe",
|
||||
key.into_cow().as_ref(),
|
||||
0,
|
||||
&mut Vec::new(),
|
||||
),
|
||||
MatchType::Regex(_) => {
|
||||
self.comparator
|
||||
.regex(pattern, &key, "maybe", 0, &mut Vec::new())
|
||||
}
|
||||
_ => false,
|
||||
} {
|
||||
return TestResult::Bool(true ^ self.is_not);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TestResult::Bool(false ^ self.is_not)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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::tests::test_size::TestSize, Context};
|
||||
|
||||
use super::TestResult;
|
||||
|
||||
impl TestSize {
|
||||
pub(crate) fn exec(&self, ctx: &Context) -> TestResult {
|
||||
TestResult::Bool(
|
||||
(if self.over {
|
||||
ctx.message_size > self.limit
|
||||
} else {
|
||||
ctx.message_size < self.limit
|
||||
}) ^ self.is_not,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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::{
|
||||
tests::test_spamtest::{TestSpamTest, TestVirusTest},
|
||||
MatchType,
|
||||
},
|
||||
Number,
|
||||
},
|
||||
runtime::Variable,
|
||||
Context, SpamStatus, VirusStatus,
|
||||
};
|
||||
|
||||
use super::TestResult;
|
||||
|
||||
impl TestSpamTest {
|
||||
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
|
||||
let status = if self.percent {
|
||||
ctx.spam_status.as_percentage()
|
||||
} else {
|
||||
ctx.spam_status.as_number()
|
||||
};
|
||||
let value = ctx.eval_value(&self.value);
|
||||
let mut captured_values = Vec::new();
|
||||
|
||||
let result = match &self.match_type {
|
||||
MatchType::Is => self.comparator.is(&status, &value),
|
||||
MatchType::Contains => self
|
||||
.comparator
|
||||
.contains(status.into_cow().as_ref(), value.into_cow().as_ref()),
|
||||
MatchType::Value(rel_match) => self.comparator.relational(rel_match, &status, &value),
|
||||
MatchType::Matches(capture_positions) => self.comparator.matches(
|
||||
status.into_cow().as_ref(),
|
||||
value.into_cow().as_ref(),
|
||||
*capture_positions,
|
||||
&mut captured_values,
|
||||
),
|
||||
MatchType::Regex(capture_positions) => self.comparator.regex(
|
||||
&self.value,
|
||||
&value,
|
||||
status.into_cow().as_ref(),
|
||||
*capture_positions,
|
||||
&mut captured_values,
|
||||
),
|
||||
MatchType::Count(rel_match) => rel_match.cmp(
|
||||
&Number::from(if matches!(&ctx.spam_status, SpamStatus::Unknown) {
|
||||
0.0
|
||||
} else {
|
||||
1.1
|
||||
}),
|
||||
&value.to_number(),
|
||||
),
|
||||
MatchType::List => false,
|
||||
};
|
||||
|
||||
if !captured_values.is_empty() {
|
||||
ctx.set_match_variables(captured_values);
|
||||
}
|
||||
|
||||
TestResult::Bool(result ^ self.is_not)
|
||||
}
|
||||
}
|
||||
|
||||
impl TestVirusTest {
|
||||
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
|
||||
let status = ctx.virus_status.as_number();
|
||||
let value = ctx.eval_value(&self.value);
|
||||
let mut captured_values = Vec::new();
|
||||
|
||||
let result = match &self.match_type {
|
||||
MatchType::Is => self.comparator.is(&status, &value),
|
||||
MatchType::Contains => self
|
||||
.comparator
|
||||
.contains(status.into_cow().as_ref(), value.into_cow().as_ref()),
|
||||
MatchType::Value(rel_match) => self.comparator.relational(rel_match, &status, &value),
|
||||
MatchType::Matches(capture_positions) => self.comparator.matches(
|
||||
status.into_cow().as_ref(),
|
||||
value.into_cow().as_ref(),
|
||||
*capture_positions,
|
||||
&mut captured_values,
|
||||
),
|
||||
MatchType::Regex(capture_positions) => self.comparator.regex(
|
||||
&self.value,
|
||||
&value,
|
||||
status.into_cow().as_ref(),
|
||||
*capture_positions,
|
||||
&mut captured_values,
|
||||
),
|
||||
MatchType::Count(rel_match) => rel_match.cmp(
|
||||
&Number::from(if matches!(&ctx.virus_status, VirusStatus::Unknown) {
|
||||
0.0
|
||||
} else {
|
||||
1.1
|
||||
}),
|
||||
&value.to_number(),
|
||||
),
|
||||
MatchType::List => false,
|
||||
};
|
||||
|
||||
if !captured_values.is_empty() {
|
||||
ctx.set_match_variables(captured_values);
|
||||
}
|
||||
|
||||
TestResult::Bool(result ^ self.is_not)
|
||||
}
|
||||
}
|
||||
|
||||
impl SpamStatus {
|
||||
pub fn from_number(number: u32) -> Self {
|
||||
match number {
|
||||
1 => SpamStatus::Ham,
|
||||
2..=9 => SpamStatus::MaybeSpam(number as f64 / 10.0),
|
||||
10 => SpamStatus::Spam,
|
||||
_ => SpamStatus::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn as_number(&self) -> Variable<'static> {
|
||||
Variable::Integer(match self {
|
||||
SpamStatus::Unknown => 0,
|
||||
SpamStatus::Ham => 1,
|
||||
SpamStatus::MaybeSpam(pct) => {
|
||||
let n = (pct * 10.0) as i64;
|
||||
if n < 2 {
|
||||
2
|
||||
} else if n > 9 {
|
||||
9
|
||||
} else {
|
||||
n
|
||||
}
|
||||
}
|
||||
SpamStatus::Spam => 10,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn as_percentage(&self) -> Variable<'static> {
|
||||
Variable::Integer(match self {
|
||||
SpamStatus::Unknown | SpamStatus::Ham => 0,
|
||||
SpamStatus::MaybeSpam(pct) => {
|
||||
let n = (pct * 100.0).ceil() as i64;
|
||||
if n > 100 {
|
||||
100
|
||||
} else if n < 1 {
|
||||
1
|
||||
} else {
|
||||
n
|
||||
}
|
||||
}
|
||||
SpamStatus::Spam => 100,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl VirusStatus {
|
||||
pub fn from_number(number: u32) -> Self {
|
||||
match number {
|
||||
1 => VirusStatus::Clean,
|
||||
2 => VirusStatus::Replaced,
|
||||
3 => VirusStatus::Cured,
|
||||
4 => VirusStatus::MaybeVirus,
|
||||
5 => VirusStatus::Virus,
|
||||
_ => VirusStatus::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn as_number(&self) -> Variable<'static> {
|
||||
Variable::Integer(match self {
|
||||
VirusStatus::Unknown => 0,
|
||||
VirusStatus::Clean => 1,
|
||||
VirusStatus::Replaced => 2,
|
||||
VirusStatus::Cured => 3,
|
||||
VirusStatus::MaybeVirus => 4,
|
||||
VirusStatus::Virus => 5,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for SpamStatus {
|
||||
fn from(number: u32) -> Self {
|
||||
SpamStatus::from_number(number)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for SpamStatus {
|
||||
fn from(number: i32) -> Self {
|
||||
SpamStatus::from_number(number as u32)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<usize> for SpamStatus {
|
||||
fn from(number: usize) -> Self {
|
||||
SpamStatus::from_number(number as u32)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for VirusStatus {
|
||||
fn from(number: u32) -> Self {
|
||||
VirusStatus::from_number(number)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for VirusStatus {
|
||||
fn from(number: i32) -> Self {
|
||||
VirusStatus::from_number(number as u32)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<usize> for VirusStatus {
|
||||
fn from(number: usize) -> Self {
|
||||
VirusStatus::from_number(number as u32)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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::{tests::test_string::TestString, MatchType},
|
||||
Number,
|
||||
},
|
||||
Context, Event,
|
||||
};
|
||||
|
||||
use super::TestResult;
|
||||
|
||||
impl TestString {
|
||||
pub(crate) fn exec(&self, ctx: &mut Context, empty_is_null: bool) -> TestResult {
|
||||
let mut result = false;
|
||||
|
||||
match &self.match_type {
|
||||
MatchType::Count(match_type) => {
|
||||
let num_items = self
|
||||
.source
|
||||
.iter()
|
||||
.filter(|x| !ctx.eval_value(x).is_empty())
|
||||
.count() as i64;
|
||||
if !empty_is_null || num_items > 0 {
|
||||
for key in &self.key_list {
|
||||
if match_type
|
||||
.cmp(&Number::from(num_items), &ctx.eval_value(key).to_number())
|
||||
{
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MatchType::List => {
|
||||
let mut values = Vec::with_capacity(self.source.len());
|
||||
for source in &self.source {
|
||||
let value = ctx.eval_value(source).into_cow();
|
||||
if !value.is_empty() && !values.iter().any(|v: &String| v.eq(value.as_ref())) {
|
||||
values.push(value.into_owned());
|
||||
}
|
||||
}
|
||||
if !values.is_empty() {
|
||||
return TestResult::Event {
|
||||
event: Event::ListContains {
|
||||
lists: ctx.eval_values_owned(&self.key_list),
|
||||
values,
|
||||
match_as: self.comparator.as_match(),
|
||||
},
|
||||
is_not: self.is_not,
|
||||
};
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let mut captured_values = Vec::new();
|
||||
let sources = ctx.eval_values(&self.source);
|
||||
|
||||
for pattern in &self.key_list {
|
||||
let key = ctx.eval_value(pattern);
|
||||
for source in &sources {
|
||||
if !empty_is_null || !source.is_empty() {
|
||||
result = match &self.match_type {
|
||||
MatchType::Is => self.comparator.is(source, &key),
|
||||
MatchType::Contains => self
|
||||
.comparator
|
||||
.contains(source.to_cow().as_ref(), key.to_cow().as_ref()),
|
||||
MatchType::Value(relation) => {
|
||||
self.comparator.relational(relation, source, &key)
|
||||
}
|
||||
MatchType::Matches(capture_positions) => self.comparator.matches(
|
||||
source.to_cow().as_ref(),
|
||||
key.to_cow().as_ref(),
|
||||
*capture_positions,
|
||||
&mut captured_values,
|
||||
),
|
||||
MatchType::Regex(capture_positions) => self.comparator.regex(
|
||||
pattern,
|
||||
&key,
|
||||
source.to_cow().as_ref(),
|
||||
*capture_positions,
|
||||
&mut captured_values,
|
||||
),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if result {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !captured_values.is_empty() {
|
||||
ctx.set_match_variables(captured_values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TestResult::Bool(result ^ self.is_not)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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::Context;
|
||||
|
||||
use super::Variable;
|
||||
|
||||
impl<'x> Context<'x> {
|
||||
pub(crate) fn set_match_variables(&mut self, set_vars: Vec<(usize, String)>) {
|
||||
for (var_num, value) in set_vars {
|
||||
if let Some(var) = self.vars_match.get_mut(var_num) {
|
||||
*var = value.into();
|
||||
} else {
|
||||
debug_assert!(false, "Invalid match variable {var_num}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clear_match_variables(&mut self, mut positions: u64) {
|
||||
while positions != 0 {
|
||||
let index = 63 - positions.leading_zeros();
|
||||
positions ^= 1 << index;
|
||||
if let Some(match_var) = self.vars_match.get_mut(index as usize) {
|
||||
if !match_var.is_empty() {
|
||||
*match_var = Variable::default();
|
||||
}
|
||||
} else {
|
||||
debug_assert!(false, "Failed to clear match variable at index {index}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
Test suite from Dovecot Pigeonhole.
|
||||
|
||||
AUTHORS
|
||||
===
|
||||
|
||||
Stephan Bosch <stephan@rename-it.nl>
|
||||
|
||||
This package is built for and partly based on the Dovecot Secure IMAP server
|
||||
written by:
|
||||
|
||||
Timo Sirainen <tss@iki.fi>.
|
|
@ -0,0 +1,39 @@
|
|||
require "vnd.stalwart.testsuite";
|
||||
|
||||
test_set "message" text:
|
||||
From: stephan@example.org
|
||||
Cc: frop@example.com
|
||||
To: test@dovecot.example.net
|
||||
X-A: This is a TEST header
|
||||
Subject: Test Message
|
||||
|
||||
Test!
|
||||
.
|
||||
;
|
||||
|
||||
test "i;ascii-casemap :contains (1)" {
|
||||
if not header :contains :comparator "i;ascii-casemap" "X-A" "TEST" {
|
||||
test_fail "should have matched";
|
||||
}
|
||||
}
|
||||
|
||||
test "i;ascii-casemap :contains (2)" {
|
||||
if not header :contains :comparator "i;ascii-casemap" "X-A" "test" {
|
||||
test_fail "should have matched";
|
||||
}
|
||||
}
|
||||
|
||||
test "i;ascii-casemap :matches (1)" {
|
||||
if not header :matches :comparator "i;ascii-casemap" "X-A" "This*TEST*r" {
|
||||
test_fail "should have matched";
|
||||
}
|
||||
}
|
||||
|
||||
test "i;ascii-casemap :matches (2)" {
|
||||
if not header :matches :comparator "i;ascii-casemap" "X-A" "ThIs*tEsT*R" {
|
||||
test_fail "should have matched";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
require "vnd.stalwart.testsuite";
|
||||
|
||||
test_set "message" text:
|
||||
From: stephan@example.org
|
||||
Cc: frop@example.com
|
||||
To: test@dovecot.example.net
|
||||
X-A: This is a TEST header
|
||||
Subject: Test Message
|
||||
|
||||
Test!
|
||||
.
|
||||
;
|
||||
|
||||
test "i;octet :contains" {
|
||||
if not header :contains :comparator "i;octet" "X-A" "TEST" {
|
||||
test_fail "should have matched";
|
||||
}
|
||||
}
|
||||
|
||||
test "i;octet not :contains" {
|
||||
if header :contains :comparator "i;octet" "X-A" "test" {
|
||||
test_fail "should not have matched";
|
||||
}
|
||||
}
|
||||
|
||||
test "i;octet :matches" {
|
||||
if not header :matches :comparator "i;octet" "X-A" "This*TEST*r" {
|
||||
test_fail "should have matched";
|
||||
}
|
||||
}
|
||||
|
||||
test "i;octet not :matches" {
|
||||
if header :matches :comparator "i;octet" "X-A" "ThIs*tEsT*R" {
|
||||
test_fail "should not have matched";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
require "vnd.stalwart.testsuite";
|
||||
|
||||
# Just test whether valid scripts will compile without problems
|
||||
|
||||
test "Trivial" {
|
||||
# Commands must be case-insensitive
|
||||
keep;
|
||||
Keep;
|
||||
KEEP;
|
||||
discard;
|
||||
DisCaRD;
|
||||
|
||||
# Tags must be case-insensitive
|
||||
if size :UNDER 34 {
|
||||
}
|
||||
|
||||
if header :Is "from" "tukker@example.com" {
|
||||
}
|
||||
|
||||
# Numbers must be case-insensitive
|
||||
if anyof( size :UNDER 34m, size :oVeR 50M ) {
|
||||
}
|
||||
}
|
||||
|
||||
test "Redirect" {
|
||||
redirect "stephan@example.org";
|
||||
redirect " stephan@example.org";
|
||||
redirect "stephan @example.org";
|
||||
redirect "stephan@ example.org";
|
||||
redirect "stephan@example.org ";
|
||||
redirect " stephan @ example.org ";
|
||||
redirect "Stephan Bosch<stephan@example.org>";
|
||||
redirect " Stephan Bosch<stephan@example.org>";
|
||||
redirect "Stephan Bosch <stephan@example.org>";
|
||||
redirect "Stephan Bosch< stephan@example.org>";
|
||||
redirect "Stephan Bosch<stephan @example.org>";
|
||||
redirect "Stephan Bosch<stephan@ example.org>";
|
||||
redirect "Stephan Bosch<stephan@example.org >";
|
||||
redirect "Stephan Bosch<stephan@example.org> ";
|
||||
redirect " Stephan Bosch < stephan @ example.org > ";
|
||||
|
||||
# Test address syntax
|
||||
redirect "\"Stephan Bosch\"@example.org";
|
||||
redirect "Stephan.Bosch@eXamPle.oRg";
|
||||
redirect "Stephan.Bosch@example.org";
|
||||
redirect "Stephan Bosch <stephan@example.org>";
|
||||
}
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
require "vnd.stalwart.testsuite";
|
||||
|
||||
require "relational";
|
||||
require "comparator-i;ascii-numeric";
|
||||
|
||||
/*
|
||||
* Errors triggered in the compiled scripts are pretty reduntant over the
|
||||
* tested commands, but we want to be thorough.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Lexer errors
|
||||
*/
|
||||
|
||||
test "Lexer errors (FIXME: count only)" {
|
||||
if test_script_compile "errors/lexer.sieve" {
|
||||
test_fail "compile should have failed.";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Parser errors
|
||||
*/
|
||||
|
||||
test "Parser errors (FIXME: count only)" {
|
||||
if test_script_compile "errors/parser.sieve" {
|
||||
test_fail "compile should have failed.";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Header test
|
||||
*/
|
||||
|
||||
test "Header errors" {
|
||||
if test_script_compile "errors/header.sieve" {
|
||||
test_fail "compile should have failed.";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Address test
|
||||
*/
|
||||
|
||||
|
||||
test "Address errors" {
|
||||
if test_script_compile "errors/address.sieve" {
|
||||
test_fail "compile should have failed.";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* If command
|
||||
*/
|
||||
|
||||
test "If errors (FIXME: count only)" {
|
||||
if test_script_compile "errors/if.sieve" {
|
||||
test_fail "compile should have failed.";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Require command
|
||||
*/
|
||||
|
||||
test "Require errors (FIXME: count only)" {
|
||||
if test_script_compile "errors/require.sieve" {
|
||||
test_fail "compile should have failed.";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Size test
|
||||
*/
|
||||
|
||||
test "Size errors (FIXME: count only)" {
|
||||
if test_script_compile "errors/size.sieve" {
|
||||
test_fail "compile should have failed.";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Envelope test
|
||||
*/
|
||||
|
||||
test "Envelope errors (FIXME: count only)" {
|
||||
if test_script_compile "errors/envelope.sieve" {
|
||||
test_fail "compile should have failed.";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Stop command
|
||||
*/
|
||||
|
||||
test "Stop errors (FIXME: count only)" {
|
||||
if test_script_compile "errors/stop.sieve" {
|
||||
test_fail "compile should have failed.";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Keep command
|
||||
*/
|
||||
|
||||
test "Keep errors (FIXME: count only)" {
|
||||
if test_script_compile "errors/keep.sieve" {
|
||||
test_fail "compile should have failed.";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Fileinto command
|
||||
*/
|
||||
|
||||
test "Fileinto errors (FIXME: count only)" {
|
||||
if test_script_compile "errors/fileinto.sieve" {
|
||||
test_fail "compile should have failed.";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* COMPARATOR errors
|
||||
*/
|
||||
|
||||
test "COMPARATOR errors (FIXME: count only)" {
|
||||
if test_script_compile "errors/comparator.sieve" {
|
||||
test_fail "compile should have failed.";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* ADDRESS-PART errors
|
||||
*/
|
||||
|
||||
test "ADDRESS-PART errors (FIXME: count only)" {
|
||||
if test_script_compile "errors/address-part.sieve" {
|
||||
test_fail "compile should have failed.";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* MATCH-TYPE errors
|
||||
*/
|
||||
|
||||
test "MATCH-TYPE errors (FIXME: count only)" {
|
||||
if test_script_compile "errors/match-type.sieve" {
|
||||
test_fail "compile should have failed.";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Encoded-character errors
|
||||
*/
|
||||
|
||||
test "Encoded-character errors (FIXME: count only)" {
|
||||
if test_script_compile "errors/encoded-character.sieve" {
|
||||
test_fail "compile should have failed.";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Outgoing address errors
|
||||
*/
|
||||
|
||||
/*test "Outgoing address errors (FIXME: count only)" {
|
||||
if test_script_compile "errors/out-address.sieve" {
|
||||
test_fail "compile should have failed.";
|
||||
}
|
||||
|
||||
}*/
|
||||
|
||||
/*
|
||||
* Tagged argument errors
|
||||
*/
|
||||
|
||||
test "Tagged argument errors (FIXME: count only)" {
|
||||
if test_script_compile "errors/tag.sieve" {
|
||||
test_fail "compile should have failed.";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Typos
|
||||
*/
|
||||
|
||||
test "Typos" {
|
||||
if test_script_compile "errors/typos.sieve" {
|
||||
test_fail "compile should have failed.";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Unsupported language features
|
||||
*/
|
||||
|
||||
test "Unsupported language features (FIXME: count only)" {
|
||||
if test_script_compile "errors/unsupported.sieve" {
|
||||
test_fail "compile should have failed.";
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Address part errors
|
||||
*
|
||||
* Total errors: 2 (+1 = 3)
|
||||
*/
|
||||
|
||||
# Duplicate address part (1)
|
||||
if address :all :comparator "i;octet" :domain "from" "STEPHAN" {
|
||||
|
||||
# Duplicate address part (2)
|
||||
if address :domain :localpart :comparator "i;octet" "from" "friep.example.com" {
|
||||
keep;
|
||||
}
|
||||
|
||||
stop;
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
require "comparator-i;ascii-numeric";
|
||||
|
||||
/*
|
||||
* Address test errors
|
||||
*
|
||||
* Total count: 8 (+1 = 9)
|
||||
*/
|
||||
|
||||
/*
|
||||
* Command structure
|
||||
*/
|
||||
|
||||
# Invalid tag
|
||||
if address :nonsense :comparator "i;ascii-casemap" :localpart "From" "nico" {
|
||||
discard;
|
||||
}
|
||||
|
||||
# Invalid first argument
|
||||
if address :is :comparator "i;ascii-numeric" :localpart 45 "nico" {
|
||||
discard;
|
||||
}
|
||||
|
||||
# Invalid second argument
|
||||
if address :is :comparator "i;ascii-numeric" :localpart "From" 45 {
|
||||
discard;
|
||||
}
|
||||
|
||||
# Invalid second argument
|
||||
if address :comparator "i;ascii-numeric" :localpart "From" :is {
|
||||
discard;
|
||||
}
|
||||
|
||||
# Missing second argument
|
||||
if address :is :comparator "i;ascii-numeric" :localpart "From" {
|
||||
discard;
|
||||
}
|
||||
|
||||
# Missing arguments
|
||||
if address :is :comparator "i;ascii-numeric" :localpart {
|
||||
discard;
|
||||
}
|
||||
|
||||
# Not an error
|
||||
if address :localpart :is :comparator "i;ascii-casemap" "from" ["frop", "frop"] {
|
||||
discard;
|
||||
}
|
||||
|
||||
/*
|
||||
* Specified headers must contain addresses
|
||||
*/
|
||||
|
||||
# Invalid header
|
||||
if address :is "frop" "frml" {
|
||||
keep;
|
||||
}
|
||||
|
||||
# Not an error
|
||||
if address :is "reply-to" "frml" {
|
||||
keep;
|
||||
}
|
||||
|
||||
# Invalid header (#2)
|
||||
if address :is ["to", "frop"] "frml" {
|
||||
keep;
|
||||
}
|
||||
|
||||
# Not an error
|
||||
if address :is ["to", "reply-to"] "frml" {
|
||||
keep;
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Address part errors
|
||||
*
|
||||
* Total errors: 5 (+1 = 6)
|
||||
*/
|
||||
|
||||
# 1: No argument
|
||||
if address :comparator { }
|
||||
|
||||
# 2: Number argument
|
||||
if address :comparator 1 "from" "frop" { }
|
||||
|
||||
# 3: String list argument
|
||||
if address :comparator ["a", "b"] "from" "frop" { }
|
||||
|
||||
# 4: Unknown tag
|
||||
if address :comparator :frop "from" "frop" { }
|
||||
|
||||
# 5: Known tag
|
||||
if address :comparator :all "from" "frop" { }
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Encoded-character errors
|
||||
*
|
||||
* Total errors: 2 (+1 = 3)
|
||||
*/
|
||||
|
||||
require "encoded-character";
|
||||
require "fileinto";
|
||||
|
||||
# Invalid unicode character (1)
|
||||
fileinto "INBOX.${unicode:200000}";
|
||||
|
||||
# Not an error
|
||||
fileinto "INBOX.${unicode:200000";
|
||||
|
||||
# Invalid unicode character (2)
|
||||
fileinto "INBOX.${Unicode:DF01}";
|
||||
|
||||
# Not an error
|
||||
fileinto "INBOX.${Unicode:DF01";
|
||||
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue