diff --git a/autotests/folding/test.sieve.fold b/autotests/folding/test.sieve.fold new file mode 100644 --- /dev/null +++ b/autotests/folding/test.sieve.fold @@ -0,0 +1,387 @@ +# Sieve syntax highlighting test +# see RFC 5228 + +# comments §2.3. + +# Single line with alerts TODO ### +/* + Multiline comment with alerts FIXME + this also should be folding region + */ + +# literal data §2.4. +# numbers §2.4.1. +42 +100K +10M +3G + +# strings §2.4.2. +"string" +"string\nwith\"special chars" +# not a string +"multi \ + line \ + #with\a \ + continuation" +# not a string +text: +Multiline +string\" "with quotes" +or # comments +this should also be a folding region +.dot +..dot +dotstuffed dot: +.. +ending with a single . +. + +# encoded characters §2.4.2.4. +# valid +"$${hex:24 24}" +"${unicode:40}" +"$${hex:40}" +"${hex: da }" +"${hex:FE }" +"${HEX: 40}" +"${unicode:40}" +"${UNICODE:40}" +"${UnICoDE:0000040}" +"${Unicode:ff}" +#invalid +"${hex:40" +"${hex:4${hex:30}}" +"${ unicode:40}" +"${Unicode:Cool}" +# semantic errors, syntactically correct +"${hex:400}" +"${unicode:200000}" +"${Unicode:DF01}" + +# test lists §2.5.1 +if anyof (not exists ["From", "Date"], + header :contains "from" "fool@example.com") { + discard; +} +if allof (not exists ["From", "Date"], + header :contains "from" "fool@example.com") { + discard; +} + +# match types §2.7.1. +:contains +:matches +:is + +# comparators §2.7.3. +if header :contains :comparator "i;octet" "Subject" "MAKE MONEY FAST" { + discard; +} + +# comparison against addresses §2.7.4. +:localpart +:domain +:all + +# commands §2.9. +keep; +fileinto "inbox.bla"; +redirect "test@kde.org"; +discard; + +# control if §3.1. +# this also should create folding regions +if header :contains "from" "foo" { + discard; +} elsif header :contains ["subject"] ["$$$"] { + discard; +} else { + fileinto "INBOX"; +} + +# control require §3.2. +require "fileinto"; +require ["vacation", "imapflags"]; + +# control stop §3.3. +stop; + +# test commands §5. +address +allof +anyof +exists +false +header +not +size +true + +# test address §5.1. +if address :is :all "from" "kde@example.com" { + discard; +} elsif address :domain :is ["From", "To"] "example.com" { # comment + keep; # comment +} + +# test envelope §5.4. +if envelope :all :is "from" "kde@example.com" { + discard; +} + +# test exists §5.5. +if not exists ["From","Date"] { + discard; +} + +# test header §5.7. +not header :matches "Cc" "?*" + +# test size §5.9. +if size :over 500K { discard; } +if size :under 1M { keep; } else { discard; } + + +/* + * Extensions + * see https://www.iana.org/assignments/sieve-extensions/sieve-extensions.xhtml + */ + +# Body RFC5173 +:raw +:content +:text +if body :raw :contains "MAKE MONEY FAST" { + discard; +} +if body :content "text" :contains ["bla", "blub"] { + fileinto "inbox.foo"; +} + +# Convert RFC6558 +require ["convert"]; +convert "image/tiff" "image/jpeg" ["pix-x=320","pix-y=240"]; + +# Copy RFC3894 +require ["copy", "fileinto"]; +fileinto :copy "incoming"; + +# Date RFC5260 +require ["date", "relational", "fileinto"]; +if allof(header :is "from" "boss@example.com", + date :value "ge" :originalzone "date" "hour" "09", + date :value "lt" :originalzone "date" "hour" "17") + { fileinto "urgent"; } +if anyof(date :is "received" "weekday" "0", + date :is "received" "weekday" "6") +{ fileinto "weekend"; } +if anyof(currentdate :is "weekday" "0", + currentdate :is "weekday" "6", + currentdate :value "lt" "hour" "09", + currentdate :value "ge" "hour" "17") +{ redirect "pager@example.com"; } +if allof(currentdate :value "ge" "date" "2007-06-30", + currentdate :value "le" "date" "2007-07-07") +{ vacation :days 7 "I'm away during the first week in July."; } + +require ["date", "variables", "fileinto"]; +if currentdate :matches "month" "*" { set "month" "${1}"; } +if currentdate :matches "year" "*" { set "year" "${1}"; } +fileinto "${month}-${year}"; + +require ["variables", "date", "editheader"]; +if currentdate :matches "std11" "*" {addheader "Processing-date" "${0}";} + +require ["date", "relational", "index"]; +if date :value "gt" :index 2 :zone "-0500" "received" "iso8601" "2007-02-26T09:00:00-05:00" +{ redirect "aftercutoff@example.org"; } + +if header :index 1 :matches "received" "*(* [*.*.*.*])*" {} + +# Duplicate RFC7352 +require ["duplicate", "variables"]; +if duplicate { discard; } +if duplicate :header "message-id" { discard; } +if header :matches "message-id" "*" { + if duplicate :uniqueid "${0}" { discard; } +} + +# Editheader RFC5293 +addheader "X-Hello" "World"; +deleteheader :index 1 "X-Hello"; + +# Enclose RFC5703 +:mime +:anychild +:type +:subtype +:contenttype +:param +replace +enclose +extracttext +if header :mime :type "Content-Type" "image" {} +replace "Executable attachment removed by user filter"; +foreverypart { + if header :mime :param "filename" :matches ["Content-Type", "Content-Disposition"] + ["*.com", "*.exe", "*.vbs", "*.scr", "*.pif", "*.hta", "*.bat", "*.zip" ] { + # these attachment types are executable + enclose :subject "Warning" text: +WARNING! The enclosed message contains executable attachments. +These attachment types may contain a computer virus program +that can infect your computer and potentially damage your data. +. +; + break; + } +} +extracttext :first 100 "msgcontent"; + +# Notify RFC5435 +notify :importance "1" :message "This is probably very important" "mailto:kde@example.com"; +notify :message "${from_addr}${env_from}: ${subject}" "mailto:kde@example.com"; +if not valid_notify_method ["mailto:", "http://gw.example.net/notify?test"] { stop; } +if notify_method_capability "xmpp:tim@example.com?message;subject=SIEVE" "Online" "yes" {} +set :encodeurl "body_param" "stuff"; + +# Envelope DSN RFC6009 +if envelope "notify" "SUCCESS" {} +if allof (envelope "notify" "FAILURE", envelope :comparator "i;ascii-numeric" :count "eq" "notify" "1") {} +if envelope :matches "orcpt" "rfc822;*@example.com" {} +if anyof (envelope :contains "bytimerelative" "-", envelope :value "eq" :comparator "i;ascii-numeric" "bytimerelative" "0") {} +redirect :copy :notify "NEVER" "elsewhere@example.com"; +redirect :copy :bytimerelative 600 "cellphone@example.com"; +redirect :copy :bytimeabsolute "${date}T20:00:00${zone}" :bymode "return" "cellphone@example.com"; + +# Environment RFC5183 +if environment :contains "item" "" {} + +# Reject RFC5429 +ereject "I no longer accept mail from this address"; +reject text: +Your message is too big. If you want to send me a big attachment, +put it on a public web site and send me a URL. +. +; + +# External Lists RFC6134 +if envelope :list "from" ":addrbook:default" {} +if currentdate :list "date" "tag:example.com,2011-01-01:localHolidays" {} +if allof (envelope :detail "to" "mylist", header :list "from" "tag:example.com,2010-05-28:mylist") { + redirect :list "tag:example.com,2010-05-28:mylist"; +} +if string :list "${ip}" "tag:example.com,2011-04-10:DisallowedIPs" {} +if header :mime :param "filename" :list ["Content-Type", "Content-Disposition"] "tag:example.com,2011-04-10:BadFileNameExts" {} + +# ihave RFC5463 +if ihave "fileinto" {} +error "failed!"; + +# IMAP Sieve RFC6785 +if anyof (environment :is "imap.cause" "APPEND", environment :is "imap.cause" "COPY") { + if environment :is "imap.mailbox" "ActionItems" { + redirect :copy "actionitems@example.com"; + } +} + +# IMAP4 Flags RFC5232 +setflag "\\Deleted"; +setflag "flagvar" "\\Flagged"; +addflag "flagvar" "\\Deleted"; +addflag "flagvar" ["\\Deleted", "\\Answered"]; +removeflag "flagvar" "$MDNRequired"; +hasflag :contains "MyVar" "Junk" +hasflag :contains "${MyVar}" ["label", "forward"] +hasflag :count "ge" :comparator "i;ascii-numeric" "MyFlags" 2 +fileinto :flags "\\Deleted" "INBOX.bla"; + +# Include RFC6609 +include :personal "always_allow"; +include :global "spam_tests"; +return; +global "i_am_on_vacation"; +set "global.i_am_on_vacation" "1"; + +# Mailbox RFC5490 +if mailboxexists "bla" {} +fileinto :create "inbox.bla"; +if metadata :is "INBOX" "/private/vendor/vendor.isode/auto-replies" "on" {} +if metadataexists "INBOX" "/private/vendor/foo" {} +if servermetadata :matches "/private/vendor/vendor.isode/notification-uri" "*" {} +if servermetadataexists "/private/vendor/foo" {} + +# Regexp (draft) +if not address :regex ["to", "cc", "bcc"] "me(\\+.*)?@company\\.com" {} + +# Relational RFC5231 +if address :count "ge" :comparator "i;ascii-numeric" ["to", "cc"] ["3"] {} +if header :value "lt" :comparator "i;ascii-numeric" ["x-priority"] ["3"] {} + +# Spamtest RFC5235 +if spamtest :value "eq" :comparator "i;ascii-numeric" "0" {} +elsif spamtest :value "ge" :comparator "i;ascii-numeric" "3" {} +elsif spamtest :percent :value "lt" :comparator "i;ascii-numeric" "37" {} +if virustest :value "eq" :comparator "i;ascii-numeric" "4" {} + +# Subaddress RFC5233 +if envelope :user "to" "postmaster" {} +if envelope :detail "to" "mta-filters" {} + +# Vacation RFC5230 +vacation "I'm out"; +vacation :subject "Automatic response to: ${1}" + "I'm away -- send mail to foo in my absence"; +vacation :handle "ran-away" "I'm out"; +vacation :mime text: + Content-Type: multipart/alternative; boundary=foo + + --foo + + Hello ${sender}, I'm at the beach relaxing. + + --foo + Content-Type: text/html; charset=us-ascii + + + How to relax + +

I'm at the beach relaxing. + + + --foo-- +. +; +vacation :days 23 :addresses ["kde@example.edu"] "I'm away until October 19."; + +# Vacation Seconds RFC6131 +vacation :addresses ["kde@example.edu"] :seconds 1800 + "I am in a meeting, and do not have access to email."; + +# Variables RFC5229 +"&%${}!" # invalid +"${doh!}" #invalid +"${company}" +"bla ${var42} blub" +"${1}" +"${BAD${Company}" # second one is the variable +"${President, ${Company} Inc.}" # inner one is the variable +set "var" "value"; +set "var2" "${var}"; + +set "a" "juMBlEd lETteRS"; +set :length "b" "${a}"; +set :lower "b" "${a}"; +set :upperfirst "b" "${a}"; +set :upperfirst :lower "b" "${a}"; +set :quotewildcard "b" "Rock*"; +set :lowerfirst "b" "${a}"; + +if string :matches " ${state} " "* pending *" {} + +# Dovecot debug extension +require "vnd.dovecot.debug"; +if header :contains "subject" "hello" { + debug_log "Subject header contains hello!"; +} diff --git a/autotests/html/test.sieve.html b/autotests/html/test.sieve.html new file mode 100644 --- /dev/null +++ b/autotests/html/test.sieve.html @@ -0,0 +1,394 @@ + + + +test.sieve + +

+# Sieve syntax highlighting test
+# see RFC 5228
+
+# comments §2.3.
+
+# Single line with alerts TODO ###
+/*
+ Multiline comment with alerts FIXME
+ this also should be folding region
+ */
+
+# literal data §2.4.
+# numbers §2.4.1.
+42
+100K
+10M
+3G
+
+# strings §2.4.2.
+"string"
+"string\nwith\"special chars"
+# not a string
+"multi \
+ line \
+ #with\a \
+ continuation"
+# not a string
+text:
+Multiline
+string\" "with quotes"
+or # comments
+this should also be a folding region
+.dot
+..dot
+dotstuffed dot:
+..
+ending with a single .
+.
+
+# encoded characters §2.4.2.4.
+# valid
+"$${hex:24 24}"
+"${unicode:40}"
+"$${hex:40}"
+"${hex: da }"
+"${hex:FE }"
+"${HEX: 40}"
+"${unicode:40}"
+"${UNICODE:40}"
+"${UnICoDE:0000040}"
+"${Unicode:ff}"
+#invalid
+"${hex:40"
+"${hex:4${hex:30}}"
+"${ unicode:40}"
+"${Unicode:Cool}"
+# semantic errors, syntactically correct
+"${hex:400}"
+"${unicode:200000}"
+"${Unicode:DF01}"
+
+# test lists §2.5.1
+if anyof (not exists ["From", "Date"],
+          header :contains "from" "fool@example.com") {
+    discard;
+}
+if allof (not exists ["From", "Date"],
+          header :contains "from" "fool@example.com") {
+    discard;
+}
+
+# match types §2.7.1.
+:contains
+:matches
+:is
+
+# comparators §2.7.3.
+if header :contains :comparator "i;octet" "Subject" "MAKE MONEY FAST" {
+    discard;
+}
+
+# comparison against addresses §2.7.4.
+:localpart
+:domain
+:all
+
+# commands §2.9.
+keep;
+fileinto "inbox.bla";
+redirect "test@kde.org";
+discard;
+
+# control if §3.1.
+# this also should create folding regions
+if header :contains "from" "foo" {
+    discard;
+} elsif header :contains ["subject"] ["$$$"] {
+    discard;
+} else {
+    fileinto "INBOX";
+}
+
+# control require §3.2.
+require "fileinto";
+require ["vacation", "imapflags"];
+
+# control stop §3.3.
+stop;
+
+# test commands §5.
+address
+allof
+anyof
+exists
+false
+header
+not
+size
+true
+
+# test address §5.1.
+if address :is :all "from" "kde@example.com" {
+    discard;
+} elsif address :domain :is ["From", "To"] "example.com" { # comment
+    keep; # comment
+}
+
+# test envelope §5.4.
+if envelope :all :is "from" "kde@example.com" {
+    discard;
+}
+
+# test exists §5.5.
+if not exists ["From","Date"] {
+    discard;
+}
+
+# test header §5.7.
+not header :matches "Cc" "?*"
+
+# test size §5.9.
+if size :over 500K { discard; }
+if size :under 1M { keep; } else { discard; }
+
+
+/*
+ * Extensions
+ * see https://www.iana.org/assignments/sieve-extensions/sieve-extensions.xhtml
+ */
+
+# Body RFC5173
+:raw
+:content
+:text
+if body :raw :contains "MAKE MONEY FAST" {
+    discard;
+}
+if body :content "text" :contains ["bla", "blub"] {
+    fileinto "inbox.foo";
+}
+
+# Convert RFC6558
+require ["convert"];
+convert "image/tiff" "image/jpeg" ["pix-x=320","pix-y=240"];
+
+# Copy RFC3894
+require ["copy", "fileinto"];
+fileinto :copy "incoming";
+
+# Date RFC5260
+require ["date", "relational", "fileinto"];
+if allof(header :is "from" "boss@example.com",
+         date :value "ge" :originalzone "date" "hour" "09",
+         date :value "lt" :originalzone "date" "hour" "17")
+    { fileinto "urgent"; }
+if anyof(date :is "received" "weekday" "0",
+         date :is "received" "weekday" "6")
+{ fileinto "weekend"; }
+if anyof(currentdate :is "weekday" "0",
+         currentdate :is "weekday" "6",
+         currentdate :value "lt" "hour" "09",
+         currentdate :value "ge" "hour" "17")
+{ redirect "pager@example.com"; }
+if allof(currentdate :value "ge" "date" "2007-06-30",
+         currentdate :value "le" "date" "2007-07-07")
+{ vacation :days 7  "I'm away during the first week in July."; }
+
+require ["date", "variables", "fileinto"];
+if currentdate :matches "month" "*" { set "month" "${1}"; }
+if currentdate :matches "year"  "*" { set "year"  "${1}"; }
+fileinto "${month}-${year}";
+
+require ["variables", "date", "editheader"];
+if currentdate :matches "std11" "*" {addheader "Processing-date" "${0}";}
+
+require ["date", "relational", "index"];
+if date :value "gt" :index 2 :zone "-0500" "received" "iso8601" "2007-02-26T09:00:00-05:00"
+{ redirect "aftercutoff@example.org"; }
+
+if header :index 1 :matches "received" "*(* [*.*.*.*])*" {}
+
+# Duplicate RFC7352
+require ["duplicate", "variables"];
+if duplicate { discard; }
+if duplicate :header "message-id" { discard; }
+if header :matches "message-id" "*" {
+    if duplicate :uniqueid "${0}" { discard; }
+}
+
+# Editheader RFC5293
+addheader "X-Hello" "World";
+deleteheader :index 1 "X-Hello";
+
+# Enclose RFC5703
+:mime
+:anychild
+:type
+:subtype
+:contenttype
+:param
+replace
+enclose
+extracttext
+if header :mime :type "Content-Type" "image" {}
+replace "Executable attachment removed by user filter";
+foreverypart {
+    if header :mime :param "filename" :matches ["Content-Type", "Content-Disposition"]
+        ["*.com", "*.exe", "*.vbs", "*.scr", "*.pif", "*.hta", "*.bat", "*.zip" ] {
+        # these attachment types are executable
+        enclose :subject "Warning" text:
+WARNING! The enclosed message contains executable attachments.
+These attachment types may contain a computer virus program
+that can infect your computer and potentially damage your data.
+.
+;
+        break;
+    }
+}
+extracttext :first 100 "msgcontent";
+
+# Notify RFC5435
+notify :importance "1" :message "This is probably very important" "mailto:kde@example.com";
+notify :message "${from_addr}${env_from}: ${subject}" "mailto:kde@example.com";
+if not valid_notify_method ["mailto:", "http://gw.example.net/notify?test"] { stop; }
+if notify_method_capability "xmpp:tim@example.com?message;subject=SIEVE" "Online" "yes" {}
+set :encodeurl "body_param" "stuff";
+
+# Envelope DSN RFC6009
+if envelope "notify" "SUCCESS" {}
+if allof (envelope "notify" "FAILURE", envelope :comparator "i;ascii-numeric" :count "eq" "notify" "1") {}
+if envelope :matches "orcpt" "rfc822;*@example.com" {}
+if anyof (envelope :contains "bytimerelative" "-", envelope :value "eq" :comparator "i;ascii-numeric" "bytimerelative" "0") {}
+redirect :copy :notify "NEVER" "elsewhere@example.com";
+redirect :copy :bytimerelative 600 "cellphone@example.com";
+redirect :copy :bytimeabsolute "${date}T20:00:00${zone}" :bymode "return" "cellphone@example.com";
+
+# Environment RFC5183
+if environment :contains "item" "" {}
+
+# Reject RFC5429
+ereject "I no longer accept mail from this address";
+reject text:
+Your message is too big.  If you want to send me a big attachment,
+put it on a public web site and send me a URL.
+.
+;
+
+# External Lists RFC6134
+if envelope :list "from" ":addrbook:default" {}
+if currentdate :list "date" "tag:example.com,2011-01-01:localHolidays" {}
+if allof (envelope :detail "to" "mylist", header :list "from" "tag:example.com,2010-05-28:mylist") {
+    redirect :list "tag:example.com,2010-05-28:mylist";
+}
+if string :list "${ip}" "tag:example.com,2011-04-10:DisallowedIPs" {}
+if header :mime :param "filename" :list ["Content-Type", "Content-Disposition"] "tag:example.com,2011-04-10:BadFileNameExts" {}
+
+# ihave RFC5463
+if ihave "fileinto" {}
+error "failed!";
+
+# IMAP Sieve RFC6785
+if anyof (environment :is "imap.cause" "APPEND", environment :is "imap.cause" "COPY")  {
+    if environment :is "imap.mailbox" "ActionItems" {
+        redirect :copy "actionitems@example.com";
+    }
+}
+
+# IMAP4 Flags RFC5232
+setflag "\\Deleted";
+setflag "flagvar" "\\Flagged";
+addflag "flagvar" "\\Deleted";
+addflag "flagvar" ["\\Deleted", "\\Answered"];
+removeflag "flagvar" "$MDNRequired";
+hasflag :contains "MyVar" "Junk"
+hasflag :contains "${MyVar}" ["label", "forward"]
+hasflag :count "ge" :comparator "i;ascii-numeric" "MyFlags" 2
+fileinto :flags "\\Deleted" "INBOX.bla";
+
+# Include RFC6609
+include :personal "always_allow";
+include :global "spam_tests";
+return;
+global "i_am_on_vacation";
+set "global.i_am_on_vacation" "1";
+
+# Mailbox RFC5490
+if mailboxexists "bla" {}
+fileinto :create "inbox.bla";
+if metadata :is "INBOX" "/private/vendor/vendor.isode/auto-replies" "on" {}
+if metadataexists "INBOX" "/private/vendor/foo" {}
+if servermetadata :matches "/private/vendor/vendor.isode/notification-uri" "*" {}
+if servermetadataexists "/private/vendor/foo" {}
+
+# Regexp (draft)
+if not address :regex ["to", "cc", "bcc"] "me(\\+.*)?@company\\.com" {}
+
+# Relational RFC5231
+if address :count "ge" :comparator "i;ascii-numeric" ["to", "cc"] ["3"] {}
+if header :value "lt" :comparator "i;ascii-numeric" ["x-priority"] ["3"] {}
+
+# Spamtest RFC5235
+if spamtest :value "eq" :comparator "i;ascii-numeric" "0" {}
+elsif spamtest :value "ge" :comparator "i;ascii-numeric" "3" {}
+elsif spamtest :percent :value "lt" :comparator "i;ascii-numeric" "37" {}
+if virustest :value "eq" :comparator "i;ascii-numeric" "4" {}
+
+# Subaddress RFC5233
+if envelope :user "to" "postmaster" {}
+if envelope :detail "to" "mta-filters" {}
+
+# Vacation RFC5230
+vacation "I'm out";
+vacation :subject "Automatic response to: ${1}"
+                  "I'm away -- send mail to foo in my absence";
+vacation :handle "ran-away" "I'm out";
+vacation :mime text:
+   Content-Type: multipart/alternative; boundary=foo
+
+   --foo
+
+   Hello ${sender}, I'm at the beach relaxing.
+
+   --foo
+   Content-Type: text/html; charset=us-ascii
+
+   <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN"
+    "http://www.w3.org/TR/REC-html40/strict.dtd">
+   <HTML><HEAD><TITLE>How to relax</TITLE>
+   <BASE HREF="http://home.example.com/pictures/"></HEAD>
+   <BODY><P>I'm at the <A HREF="beach.gif">beach</A> relaxing.
+   </BODY></HTML>
+
+   --foo--
+.
+;
+vacation :days 23 :addresses ["kde@example.edu"] "I'm away until October 19.";
+
+# Vacation Seconds RFC6131
+vacation :addresses ["kde@example.edu"] :seconds 1800
+    "I am in a meeting, and do not have access to email.";
+
+# Variables RFC5229
+"&%${}!" # invalid
+"${doh!}" #invalid
+"${company}"
+"bla ${var42} blub"
+"${1}"
+"${BAD${Company}" # second one is the variable
+"${President, ${Company} Inc.}" # inner one is the variable
+set "var" "value";
+set "var2" "${var}";
+
+set "a" "juMBlEd lETteRS";
+set :length "b" "${a}";
+set :lower "b" "${a}";
+set :upperfirst "b" "${a}";
+set :upperfirst :lower "b" "${a}";
+set :quotewildcard "b" "Rock*";
+set :lowerfirst "b" "${a}";
+
+if string :matches " ${state} " "* pending *" {}
+
+# Dovecot debug extension
+require "vnd.dovecot.debug";
+if header :contains "subject" "hello" {
+    debug_log "Subject header contains hello!";
+}
+
diff --git a/autotests/input/test.sieve b/autotests/input/test.sieve new file mode 100644 --- /dev/null +++ b/autotests/input/test.sieve @@ -0,0 +1,387 @@ +# Sieve syntax highlighting test +# see RFC 5228 + +# comments §2.3. + +# Single line with alerts TODO ### +/* + Multiline comment with alerts FIXME + this also should be folding region + */ + +# literal data §2.4. +# numbers §2.4.1. +42 +100K +10M +3G + +# strings §2.4.2. +"string" +"string\nwith\"special chars" +# not a string +"multi \ + line \ + #with\a \ + continuation" +# not a string +text: +Multiline +string\" "with quotes" +or # comments +this should also be a folding region +.dot +..dot +dotstuffed dot: +.. +ending with a single . +. + +# encoded characters §2.4.2.4. +# valid +"$${hex:24 24}" +"${unicode:40}" +"$${hex:40}" +"${hex: da }" +"${hex:FE }" +"${HEX: 40}" +"${unicode:40}" +"${UNICODE:40}" +"${UnICoDE:0000040}" +"${Unicode:ff}" +#invalid +"${hex:40" +"${hex:4${hex:30}}" +"${ unicode:40}" +"${Unicode:Cool}" +# semantic errors, syntactically correct +"${hex:400}" +"${unicode:200000}" +"${Unicode:DF01}" + +# test lists §2.5.1 +if anyof (not exists ["From", "Date"], + header :contains "from" "fool@example.com") { + discard; +} +if allof (not exists ["From", "Date"], + header :contains "from" "fool@example.com") { + discard; +} + +# match types §2.7.1. +:contains +:matches +:is + +# comparators §2.7.3. +if header :contains :comparator "i;octet" "Subject" "MAKE MONEY FAST" { + discard; +} + +# comparison against addresses §2.7.4. +:localpart +:domain +:all + +# commands §2.9. +keep; +fileinto "inbox.bla"; +redirect "test@kde.org"; +discard; + +# control if §3.1. +# this also should create folding regions +if header :contains "from" "foo" { + discard; +} elsif header :contains ["subject"] ["$$$"] { + discard; +} else { + fileinto "INBOX"; +} + +# control require §3.2. +require "fileinto"; +require ["vacation", "imapflags"]; + +# control stop §3.3. +stop; + +# test commands §5. +address +allof +anyof +exists +false +header +not +size +true + +# test address §5.1. +if address :is :all "from" "kde@example.com" { + discard; +} elsif address :domain :is ["From", "To"] "example.com" { # comment + keep; # comment +} + +# test envelope §5.4. +if envelope :all :is "from" "kde@example.com" { + discard; +} + +# test exists §5.5. +if not exists ["From","Date"] { + discard; +} + +# test header §5.7. +not header :matches "Cc" "?*" + +# test size §5.9. +if size :over 500K { discard; } +if size :under 1M { keep; } else { discard; } + + +/* + * Extensions + * see https://www.iana.org/assignments/sieve-extensions/sieve-extensions.xhtml + */ + +# Body RFC5173 +:raw +:content +:text +if body :raw :contains "MAKE MONEY FAST" { + discard; +} +if body :content "text" :contains ["bla", "blub"] { + fileinto "inbox.foo"; +} + +# Convert RFC6558 +require ["convert"]; +convert "image/tiff" "image/jpeg" ["pix-x=320","pix-y=240"]; + +# Copy RFC3894 +require ["copy", "fileinto"]; +fileinto :copy "incoming"; + +# Date RFC5260 +require ["date", "relational", "fileinto"]; +if allof(header :is "from" "boss@example.com", + date :value "ge" :originalzone "date" "hour" "09", + date :value "lt" :originalzone "date" "hour" "17") + { fileinto "urgent"; } +if anyof(date :is "received" "weekday" "0", + date :is "received" "weekday" "6") +{ fileinto "weekend"; } +if anyof(currentdate :is "weekday" "0", + currentdate :is "weekday" "6", + currentdate :value "lt" "hour" "09", + currentdate :value "ge" "hour" "17") +{ redirect "pager@example.com"; } +if allof(currentdate :value "ge" "date" "2007-06-30", + currentdate :value "le" "date" "2007-07-07") +{ vacation :days 7 "I'm away during the first week in July."; } + +require ["date", "variables", "fileinto"]; +if currentdate :matches "month" "*" { set "month" "${1}"; } +if currentdate :matches "year" "*" { set "year" "${1}"; } +fileinto "${month}-${year}"; + +require ["variables", "date", "editheader"]; +if currentdate :matches "std11" "*" {addheader "Processing-date" "${0}";} + +require ["date", "relational", "index"]; +if date :value "gt" :index 2 :zone "-0500" "received" "iso8601" "2007-02-26T09:00:00-05:00" +{ redirect "aftercutoff@example.org"; } + +if header :index 1 :matches "received" "*(* [*.*.*.*])*" {} + +# Duplicate RFC7352 +require ["duplicate", "variables"]; +if duplicate { discard; } +if duplicate :header "message-id" { discard; } +if header :matches "message-id" "*" { + if duplicate :uniqueid "${0}" { discard; } +} + +# Editheader RFC5293 +addheader "X-Hello" "World"; +deleteheader :index 1 "X-Hello"; + +# Enclose RFC5703 +:mime +:anychild +:type +:subtype +:contenttype +:param +replace +enclose +extracttext +if header :mime :type "Content-Type" "image" {} +replace "Executable attachment removed by user filter"; +foreverypart { + if header :mime :param "filename" :matches ["Content-Type", "Content-Disposition"] + ["*.com", "*.exe", "*.vbs", "*.scr", "*.pif", "*.hta", "*.bat", "*.zip" ] { + # these attachment types are executable + enclose :subject "Warning" text: +WARNING! The enclosed message contains executable attachments. +These attachment types may contain a computer virus program +that can infect your computer and potentially damage your data. +. +; + break; + } +} +extracttext :first 100 "msgcontent"; + +# Notify RFC5435 +notify :importance "1" :message "This is probably very important" "mailto:kde@example.com"; +notify :message "${from_addr}${env_from}: ${subject}" "mailto:kde@example.com"; +if not valid_notify_method ["mailto:", "http://gw.example.net/notify?test"] { stop; } +if notify_method_capability "xmpp:tim@example.com?message;subject=SIEVE" "Online" "yes" {} +set :encodeurl "body_param" "stuff"; + +# Envelope DSN RFC6009 +if envelope "notify" "SUCCESS" {} +if allof (envelope "notify" "FAILURE", envelope :comparator "i;ascii-numeric" :count "eq" "notify" "1") {} +if envelope :matches "orcpt" "rfc822;*@example.com" {} +if anyof (envelope :contains "bytimerelative" "-", envelope :value "eq" :comparator "i;ascii-numeric" "bytimerelative" "0") {} +redirect :copy :notify "NEVER" "elsewhere@example.com"; +redirect :copy :bytimerelative 600 "cellphone@example.com"; +redirect :copy :bytimeabsolute "${date}T20:00:00${zone}" :bymode "return" "cellphone@example.com"; + +# Environment RFC5183 +if environment :contains "item" "" {} + +# Reject RFC5429 +ereject "I no longer accept mail from this address"; +reject text: +Your message is too big. If you want to send me a big attachment, +put it on a public web site and send me a URL. +. +; + +# External Lists RFC6134 +if envelope :list "from" ":addrbook:default" {} +if currentdate :list "date" "tag:example.com,2011-01-01:localHolidays" {} +if allof (envelope :detail "to" "mylist", header :list "from" "tag:example.com,2010-05-28:mylist") { + redirect :list "tag:example.com,2010-05-28:mylist"; +} +if string :list "${ip}" "tag:example.com,2011-04-10:DisallowedIPs" {} +if header :mime :param "filename" :list ["Content-Type", "Content-Disposition"] "tag:example.com,2011-04-10:BadFileNameExts" {} + +# ihave RFC5463 +if ihave "fileinto" {} +error "failed!"; + +# IMAP Sieve RFC6785 +if anyof (environment :is "imap.cause" "APPEND", environment :is "imap.cause" "COPY") { + if environment :is "imap.mailbox" "ActionItems" { + redirect :copy "actionitems@example.com"; + } +} + +# IMAP4 Flags RFC5232 +setflag "\\Deleted"; +setflag "flagvar" "\\Flagged"; +addflag "flagvar" "\\Deleted"; +addflag "flagvar" ["\\Deleted", "\\Answered"]; +removeflag "flagvar" "$MDNRequired"; +hasflag :contains "MyVar" "Junk" +hasflag :contains "${MyVar}" ["label", "forward"] +hasflag :count "ge" :comparator "i;ascii-numeric" "MyFlags" 2 +fileinto :flags "\\Deleted" "INBOX.bla"; + +# Include RFC6609 +include :personal "always_allow"; +include :global "spam_tests"; +return; +global "i_am_on_vacation"; +set "global.i_am_on_vacation" "1"; + +# Mailbox RFC5490 +if mailboxexists "bla" {} +fileinto :create "inbox.bla"; +if metadata :is "INBOX" "/private/vendor/vendor.isode/auto-replies" "on" {} +if metadataexists "INBOX" "/private/vendor/foo" {} +if servermetadata :matches "/private/vendor/vendor.isode/notification-uri" "*" {} +if servermetadataexists "/private/vendor/foo" {} + +# Regexp (draft) +if not address :regex ["to", "cc", "bcc"] "me(\\+.*)?@company\\.com" {} + +# Relational RFC5231 +if address :count "ge" :comparator "i;ascii-numeric" ["to", "cc"] ["3"] {} +if header :value "lt" :comparator "i;ascii-numeric" ["x-priority"] ["3"] {} + +# Spamtest RFC5235 +if spamtest :value "eq" :comparator "i;ascii-numeric" "0" {} +elsif spamtest :value "ge" :comparator "i;ascii-numeric" "3" {} +elsif spamtest :percent :value "lt" :comparator "i;ascii-numeric" "37" {} +if virustest :value "eq" :comparator "i;ascii-numeric" "4" {} + +# Subaddress RFC5233 +if envelope :user "to" "postmaster" {} +if envelope :detail "to" "mta-filters" {} + +# Vacation RFC5230 +vacation "I'm out"; +vacation :subject "Automatic response to: ${1}" + "I'm away -- send mail to foo in my absence"; +vacation :handle "ran-away" "I'm out"; +vacation :mime text: + Content-Type: multipart/alternative; boundary=foo + + --foo + + Hello ${sender}, I'm at the beach relaxing. + + --foo + Content-Type: text/html; charset=us-ascii + + + How to relax + +

I'm at the beach relaxing. + + + --foo-- +. +; +vacation :days 23 :addresses ["kde@example.edu"] "I'm away until October 19."; + +# Vacation Seconds RFC6131 +vacation :addresses ["kde@example.edu"] :seconds 1800 + "I am in a meeting, and do not have access to email."; + +# Variables RFC5229 +"&%${}!" # invalid +"${doh!}" #invalid +"${company}" +"bla ${var42} blub" +"${1}" +"${BAD${Company}" # second one is the variable +"${President, ${Company} Inc.}" # inner one is the variable +set "var" "value"; +set "var2" "${var}"; + +set "a" "juMBlEd lETteRS"; +set :length "b" "${a}"; +set :lower "b" "${a}"; +set :upperfirst "b" "${a}"; +set :upperfirst :lower "b" "${a}"; +set :quotewildcard "b" "Rock*"; +set :lowerfirst "b" "${a}"; + +if string :matches " ${state} " "* pending *" {} + +# Dovecot debug extension +require "vnd.dovecot.debug"; +if header :contains "subject" "hello" { + debug_log "Subject header contains hello!"; +} diff --git a/autotests/reference/test.sieve.ref b/autotests/reference/test.sieve.ref new file mode 100644 --- /dev/null +++ b/autotests/reference/test.sieve.ref @@ -0,0 +1,387 @@ +# Sieve syntax highlighting test
+# see RFC 5228
+
+# comments §2.3.
+
+# Single line with alerts TODO ###
+/*
+ Multiline comment with alerts FIXME
+ this also should be folding region
+ */
+
+# literal data §2.4.
+# numbers §2.4.1.
+42
+100K
+10M
+3G
+
+# strings §2.4.2.
+"string"
+"string\nwith\"special chars"
+# not a string
+"multi \
+ line \
+ #with\a \
+ continuation"
+# not a string
+text:
+Multiline
+string\" "with quotes"
+or # comments
+this should also be a folding region
+.dot
+..dot
+dotstuffed dot:
+..
+ending with a single .
+.
+
+# encoded characters §2.4.2.4.
+# valid
+"$${hex:24 24}"
+"${unicode:40}"
+"$${hex:40}"
+"${hex: da }"
+"${hex:FE }"
+"${HEX: 40}"
+"${unicode:40}"
+"${UNICODE:40}"
+"${UnICoDE:0000040}"
+"${Unicode:ff}"
+#invalid
+"${hex:40"
+"${hex:4${hex:30}}"
+"${ unicode:40}"
+"${Unicode:Cool}"
+# semantic errors, syntactically correct
+"${hex:400}"
+"${unicode:200000}"
+"${Unicode:DF01}"
+
+# test lists §2.5.1
+if anyof (not exists ["From", "Date"],
+ header :contains "from" "fool@example.com") {
+ discard;
+}
+if allof (not exists ["From", "Date"],
+ header :contains "from" "fool@example.com") {
+ discard;
+}
+
+# match types §2.7.1.
+:contains
+:matches
+:is
+
+# comparators §2.7.3.
+if header :contains :comparator "i;octet" "Subject" "MAKE MONEY FAST" {
+ discard;
+}
+
+# comparison against addresses §2.7.4.
+:localpart
+:domain
+:all
+
+# commands §2.9.
+keep;
+fileinto "inbox.bla";
+redirect "test@kde.org";
+discard;
+
+# control if §3.1.
+# this also should create folding regions
+if header :contains "from" "foo" {
+ discard;
+} elsif header :contains ["subject"] ["$$$"] {
+ discard;
+} else {
+ fileinto "INBOX";
+}
+
+# control require §3.2.
+require "fileinto";
+require ["vacation", "imapflags"];
+
+# control stop §3.3.
+stop;
+
+# test commands §5.
+address
+allof
+anyof
+exists
+false
+header
+not
+size
+true
+
+# test address §5.1.
+if address :is :all "from" "kde@example.com" {
+ discard;
+} elsif address :domain :is ["From", "To"] "example.com" { # comment
+ keep; # comment
+}
+
+# test envelope §5.4.
+if envelope :all :is "from" "kde@example.com" {
+ discard;
+}
+
+# test exists §5.5.
+if not exists ["From","Date"] {
+ discard;
+}
+
+# test header §5.7.
+not header :matches "Cc" "?*"
+
+# test size §5.9.
+if size :over 500K { discard; }
+if size :under 1M { keep; } else { discard; }
+
+
+/*
+ * Extensions
+ * see https://www.iana.org/assignments/sieve-extensions/sieve-extensions.xhtml
+ */
+
+# Body RFC5173
+:raw
+:content
+:text
+if body :raw :contains "MAKE MONEY FAST" {
+ discard;
+}
+if body :content "text" :contains ["bla", "blub"] {
+ fileinto "inbox.foo";
+}
+
+# Convert RFC6558
+require ["convert"];
+convert "image/tiff" "image/jpeg" ["pix-x=320","pix-y=240"];
+
+# Copy RFC3894
+require ["copy", "fileinto"];
+fileinto :copy "incoming";
+
+# Date RFC5260
+require ["date", "relational", "fileinto"];
+if allof(header :is "from" "boss@example.com",
+ date :value "ge" :originalzone "date" "hour" "09",
+ date :value "lt" :originalzone "date" "hour" "17")
+ { fileinto "urgent"; }
+if anyof(date :is "received" "weekday" "0",
+ date :is "received" "weekday" "6")
+{ fileinto "weekend"; }
+if anyof(currentdate :is "weekday" "0",
+ currentdate :is "weekday" "6",
+ currentdate :value "lt" "hour" "09",
+ currentdate :value "ge" "hour" "17")
+{ redirect "pager@example.com"; }
+if allof(currentdate :value "ge" "date" "2007-06-30",
+ currentdate :value "le" "date" "2007-07-07")
+{ vacation :days 7 "I'm away during the first week in July."; }
+
+require ["date", "variables", "fileinto"];
+if currentdate :matches "month" "*" { set "month" "${1}"; }
+if currentdate :matches "year" "*" { set "year" "${1}"; }
+fileinto "${month}-${year}";
+
+require ["variables", "date", "editheader"];
+if currentdate :matches "std11" "*" {addheader "Processing-date" "${0}";}
+
+require ["date", "relational", "index"];
+if date :value "gt" :index 2 :zone "-0500" "received" "iso8601" "2007-02-26T09:00:00-05:00"
+{ redirect "aftercutoff@example.org"; }
+
+if header :index 1 :matches "received" "*(* [*.*.*.*])*" {}
+
+# Duplicate RFC7352
+require ["duplicate", "variables"];
+if duplicate { discard; }
+if duplicate :header "message-id" { discard; }
+if header :matches "message-id" "*" {
+ if duplicate :uniqueid "${0}" { discard; }
+}
+
+# Editheader RFC5293
+addheader "X-Hello" "World";
+deleteheader :index 1 "X-Hello";
+
+# Enclose RFC5703
+:mime
+:anychild
+:type
+:subtype
+:contenttype
+:param
+replace
+enclose
+extracttext
+if header :mime :type "Content-Type" "image" {}
+replace "Executable attachment removed by user filter";
+foreverypart {
+ if header :mime :param "filename" :matches ["Content-Type", "Content-Disposition"]
+ ["*.com", "*.exe", "*.vbs", "*.scr", "*.pif", "*.hta", "*.bat", "*.zip" ] {
+ # these attachment types are executable
+ enclose :subject "Warning" text:
+WARNING! The enclosed message contains executable attachments.
+These attachment types may contain a computer virus program
+that can infect your computer and potentially damage your data.
+.
+;
+ break;
+ }
+}
+extracttext :first 100 "msgcontent";
+
+# Notify RFC5435
+notify :importance "1" :message "This is probably very important" "mailto:kde@example.com";
+notify :message "${from_addr}${env_from}: ${subject}" "mailto:kde@example.com";
+if not valid_notify_method ["mailto:", "http://gw.example.net/notify?test"] { stop; }
+if notify_method_capability "xmpp:tim@example.com?message;subject=SIEVE" "Online" "yes" {}
+set :encodeurl "body_param" "stuff";
+
+# Envelope DSN RFC6009
+if envelope "notify" "SUCCESS" {}
+if allof (envelope "notify" "FAILURE", envelope :comparator "i;ascii-numeric" :count "eq" "notify" "1") {}
+if envelope :matches "orcpt" "rfc822;*@example.com" {}
+if anyof (envelope :contains "bytimerelative" "-", envelope :value "eq" :comparator "i;ascii-numeric" "bytimerelative" "0") {}
+redirect :copy :notify "NEVER" "elsewhere@example.com";
+redirect :copy :bytimerelative 600 "cellphone@example.com";
+redirect :copy :bytimeabsolute "${date}T20:00:00${zone}" :bymode "return" "cellphone@example.com";
+
+# Environment RFC5183
+if environment :contains "item" "" {}
+
+# Reject RFC5429
+ereject "I no longer accept mail from this address";
+reject text:
+Your message is too big. If you want to send me a big attachment,
+put it on a public web site and send me a URL.
+.
+;
+
+# External Lists RFC6134
+if envelope :list "from" ":addrbook:default" {}
+if currentdate :list "date" "tag:example.com,2011-01-01:localHolidays" {}
+if allof (envelope :detail "to" "mylist", header :list "from" "tag:example.com,2010-05-28:mylist") {
+ redirect :list "tag:example.com,2010-05-28:mylist";
+}
+if string :list "${ip}" "tag:example.com,2011-04-10:DisallowedIPs" {}
+if header :mime :param "filename" :list ["Content-Type", "Content-Disposition"] "tag:example.com,2011-04-10:BadFileNameExts" {}
+
+# ihave RFC5463
+if ihave "fileinto" {}
+error "failed!";
+
+# IMAP Sieve RFC6785
+if anyof (environment :is "imap.cause" "APPEND", environment :is "imap.cause" "COPY") {
+ if environment :is "imap.mailbox" "ActionItems" {
+ redirect :copy "actionitems@example.com";
+ }
+}
+
+# IMAP4 Flags RFC5232
+setflag "\\Deleted";
+setflag "flagvar" "\\Flagged";
+addflag "flagvar" "\\Deleted";
+addflag "flagvar" ["\\Deleted", "\\Answered"];
+removeflag "flagvar" "$MDNRequired";
+hasflag :contains "MyVar" "Junk"
+hasflag :contains "${MyVar}" ["label", "forward"]
+hasflag :count "ge" :comparator "i;ascii-numeric" "MyFlags" 2
+fileinto :flags "\\Deleted" "INBOX.bla";
+
+# Include RFC6609
+include :personal "always_allow";
+include :global "spam_tests";
+return;
+global "i_am_on_vacation";
+set "global.i_am_on_vacation" "1";
+
+# Mailbox RFC5490
+if mailboxexists "bla" {}
+fileinto :create "inbox.bla";
+if metadata :is "INBOX" "/private/vendor/vendor.isode/auto-replies" "on" {}
+if metadataexists "INBOX" "/private/vendor/foo" {}
+if servermetadata :matches "/private/vendor/vendor.isode/notification-uri" "*" {}
+if servermetadataexists "/private/vendor/foo" {}
+
+# Regexp (draft)
+if not address :regex ["to", "cc", "bcc"] "me(\\+.*)?@company\\.com" {}
+
+# Relational RFC5231
+if address :count "ge" :comparator "i;ascii-numeric" ["to", "cc"] ["3"] {}
+if header :value "lt" :comparator "i;ascii-numeric" ["x-priority"] ["3"] {}
+
+# Spamtest RFC5235
+if spamtest :value "eq" :comparator "i;ascii-numeric" "0" {}
+elsif spamtest :value "ge" :comparator "i;ascii-numeric" "3" {}
+elsif spamtest :percent :value "lt" :comparator "i;ascii-numeric" "37" {}
+if virustest :value "eq" :comparator "i;ascii-numeric" "4" {}
+
+# Subaddress RFC5233
+if envelope :user "to" "postmaster" {}
+if envelope :detail "to" "mta-filters" {}
+
+# Vacation RFC5230
+vacation "I'm out";
+vacation :subject "Automatic response to: ${1}"
+ "I'm away -- send mail to foo in my absence";
+vacation :handle "ran-away" "I'm out";
+vacation :mime text:
+ Content-Type: multipart/alternative; boundary=foo
+
+ --foo
+
+ Hello ${sender}, I'm at the beach relaxing.
+
+ --foo
+ Content-Type: text/html; charset=us-ascii
+
+
+ "http://www.w3.org/TR/REC-html40/strict.dtd">
+ How to relax
+
+

I'm at the beach relaxing.
+
+
+ --foo--
+.
+;
+vacation :days 23 :addresses ["kde@example.edu"] "I'm away until October 19.";
+
+# Vacation Seconds RFC6131
+vacation :addresses ["kde@example.edu"] :seconds 1800
+ "I am in a meeting, and do not have access to email.";
+
+# Variables RFC5229
+"&%${}!" # invalid
+"${doh!}" #invalid
+"${company}"
+"bla ${var42} blub"
+"${1}"
+"${BAD${Company}" # second one is the variable
+"${President, ${Company} Inc.}" # inner one is the variable
+set "var" "value";
+set "var2" "${var}";
+
+set "a" "juMBlEd lETteRS";
+set :length "b" "${a}";
+set :lower "b" "${a}";
+set :upperfirst "b" "${a}";
+set :upperfirst :lower "b" "${a}";
+set :quotewildcard "b" "Rock*";
+set :lowerfirst "b" "${a}";
+
+if string :matches " ${state} " "* pending *" {}
+
+# Dovecot debug extension
+require "vnd.dovecot.debug";
+if header :contains "subject" "hello" {
+ debug_log "Subject header contains hello!";
+}
diff --git a/data/syntax/sieve.xml b/data/syntax/sieve.xml --- a/data/syntax/sieve.xml +++ b/data/syntax/sieve.xml @@ -1,82 +1,152 @@ - - - - - require - if - elsif - else - discard - stop - fileinto - keep - reject - redirect - setflag - addflag - removeflag - hasflag - deleteheader - addheader - notify - set - return - vacation - enclose - replace - include - global - foreverypart - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + require + if + elsif + else + foreverypart + global + + + addflag + addheader + break + convert + debug_log + deleteheader + discard + enclose + ereject + error + extracttext + fileinto + include + keep + notify + redirect + reject + removeflag + replace + return + setflag + set + stop + vacation + + + address + allof + anyof + body + currentdate + date + duplicate + envelope + environment + exists + false + hasflag + header + ihave + mailboxexists + metadata + metadataexists + not + notify_method_capability + servermetadata + servermetadataexists + spamtest + size + string + true + valid_notify_method + virustest + + + :contains + :count + :is + :matches + :regex + :value + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +