RFC 5260

Sieve Email Filtering: Date and Index Extensions

๐Ÿ“… published July 2008, from ietf

Sieve Email Filtering: Date and Index Extensions ๐ŸŽ€

Okay bestie, so this is RFC 5260 ๐Ÿ’–, written by N. Freed at Sun Microsystems ๐ŸŒธ. It's a Standards Track doc, so it's an actual Internet standard and not just a suggestion โœจ. It came out in July 2008 ๐Ÿ“…. The header doesn't list anything it obsoletes or updates, so it's standing alone ๐Ÿ’….


Status of This Memo ๐ŸŒˆ

This one is an Internet standards track protocol for the Internet community, and it's literally asking for discussion and suggestions for improvements ๐Ÿซถ. If you want to know where this protocol stands in the official standardization state, check the current edition of the "Internet Official Protocol Standards" (STD 1) ๐Ÿ“š. Distribution of this memo is unlimited, so share the tea ๐Ÿ’œ.


Abstract โœจ

This doc is all about the "date" and "index" extensions to the Sieve email filtering language ๐Ÿ’Œ. The "date" extension lets Sieve test date and time values in a bunch of different ways ๐Ÿ•ฐ๏ธ. The "index" extension lets you limit header and address tests to specific instances of header fields when those fields get repeated, which is so useful ๐Ÿฆ‹.


Table of Contents ๐Ÿงš

1.  Introduction . . . . . . . . . . . . . . . . . . . . . . . . .  2
2.  Conventions Used in This Document  . . . . . . . . . . . . . .  2
3.  Capability Identifiers . . . . . . . . . . . . . . . . . . . .  3
4.  Date Test  . . . . . . . . . . . . . . . . . . . . . . . . . .  3
  4.1.  Zone and Originalzone Arguments  . . . . . . . . . . . . .  4
  4.2.  Date-part Argument . . . . . . . . . . . . . . . . . . . .  4
  4.3.  Comparator Interactions with Date-part Arguments . . . . .  5
  4.4.  Examples . . . . . . . . . . . . . . . . . . . . . . . . .  6
5.  Currentdate Test . . . . . . . . . . . . . . . . . . . . . . .  6
  5.1.  Examples . . . . . . . . . . . . . . . . . . . . . . . . .  6
6.  Index Extension  . . . . . . . . . . . . . . . . . . . . . . .  7
  6.1.  Example  . . . . . . . . . . . . . . . . . . . . . . . . .  8
7.  Security Considerations  . . . . . . . . . . . . . . . . . . .  8
8.  IANA Considerations  . . . . . . . . . . . . . . . . . . . . .  9
9.  References . . . . . . . . . . . . . . . . . . . . . . . . . .  9
  9.1.  Normative References . . . . . . . . . . . . . . . . . . .  9
  9.2.  Informative References . . . . . . . . . . . . . . . . . . 10
Appendix A.  Julian Date Conversions . . . . . . . . . . . . . . . 11
Appendix B.  Acknowledgements  . . . . . . . . . . . . . . . . . . 12

1. Introduction โœจ

Okay so, Sieve [RFC5228] is a language for filtering email messages at or around the time of final delivery ๐Ÿ“ฌ. It's designed so it can be implemented on either a mail client or a mail server ๐Ÿ’ป. It's meant to be extensible, simple, and independent of access protocol, mail architecture, and operating system ๐Ÿง . It's also perfect for a mail server where users might not be allowed to run arbitrary programs, like on black box Internet Message Access Protocol [RFC3501] servers ๐Ÿ–ฅ๏ธ. Why? Because it doesn't have user-controlled loops or the ability to run external programs ๐Ÿ™…โ€โ™€๏ธ.

The "date" extension adds a new date test that pulls out and matches date/time info from structured header fields ๐Ÿ—“๏ธ. It's kind of similar in concept to the address test from [RFC5228], which does similar stuff on addresses in header fields ๐Ÿ’….

The "date" extension also adds a currentdate test, which looks at the date and time when the Sieve script actually runs โฐ.

Here's the thing though ๐Ÿค”. Some header fields with date/time info, like Received:, naturally show up more than once in a single header. So it's really useful to be able to restrict the date test to some subset of the fields that are there. For example, you might want to apply a date test to the last (earliest) Received: field ๐Ÿ”. Also, it might be useful to apply similar restrictions to the header or address tests from [RFC5228] too ๐Ÿ’œ.

That's why this spec also defines an "index" extension ๐ŸŽ€. It adds two extra tagged arguments, :index and :last, to the header, address, and date tests. If they're present, they say which occurrence of the named header field gets tested ๐Ÿฆ‹.


2. Conventions Used in This Document ๐Ÿ“–

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 [RFC2119] ๐Ÿซถ.

The terms used to describe the different parts of the Sieve language are taken from Section 1.1 of [RFC5228] ๐Ÿ“š. Section 2 of that same document covers the basic Sieve language syntax and semantics ๐Ÿงฉ. The date-time syntactic element, which is defined using ABNF notation [RFC5234] in [RFC3339], is also used here ๐ŸŒธ.


3. Capability Identifiers ๐Ÿท๏ธ

The capability strings for the two extensions in this document are date and index ๐Ÿ“.


4. Date Test ๐Ÿ—“๏ธ

Here's the usage:

Usage:   date [<":zone" <time-zone: string>> / ":originalzone"]
              [COMPARATOR] [MATCH-TYPE] <header-name: string>
              <date-part: string> <key-list: string-list>

The date test matches date/time info that comes from headers containing [RFC2822] date-time values ๐Ÿ’Œ. It pulls the date/time info out of the header, shifts it to the specified time zone, and figures out the value of the given date-part ๐Ÿ•ฐ๏ธ. The test returns true if the resulting string matches any of the strings in the key-list, depending on what the comparator and match keywords say ๐ŸŽฏ.

The test returns false no matter what in any of these cases ๐Ÿ™…โ€โ™€๏ธ:

  • the specified header field doesn't exist
  • the field exists but doesn't have a syntactically valid date-time spec
  • the date-time isn't valid according to the calendar rules (like January 32nd, or February 29 in a non-leap year) ๐Ÿ’€
  • the resulting string doesn't match any key-list value

The match type defaults to :is, and the default comparator is i;ascii-casemap โœจ.

Unlike the header and address tests, the date test can only be applied to a single header field at a time ๐Ÿ™ˆ. If there are multiple header fields with the same name, only the first one found gets used. (But heads up, this behavior can be changed with the "index" extension below ๐Ÿ”ฅ.) These restrictions keep the test simple and the meaning clear ๐Ÿ’œ.

The "relational" extension [RFC5231] adds a match type called :count ๐Ÿ”ข. The count for a date test is 1 if the specified field exists and contains a valid date, and 0 otherwise ๐Ÿ’–.

Implementations MUST support extraction of RFC 2822 date-time info that either makes up the entire header field (like in a standard Date: header field), or appears at the end of a header field after a semicolon (like in a standard Received: header field). ๐Ÿšจ

Implementations MAY support extraction of date and time info in RFC 2822 or other formats that show up in other positions in header field content ๐ŸŒˆ. If a field has more than one date or time value, the last one that appears SHOULD be used ๐Ÿซถ.


4.1. Zone and Originalzone Arguments ๐ŸŒ

The :originalzone argument says the time zone offset that was originally in the extracted date-time value should be kept ๐Ÿงณ. The :zone argument says a specific time zone offset that the date-time value should be shifted to before testing ๐Ÿ”„. It's an error to specify both :zone and :originalzone ๐Ÿ™…โ€โ™€๏ธ.

The value of time-zone MUST be an offset relative to UTC with this syntax:

time-zone  =  ( "+" / "-" ) 4DIGIT

The "+" or "-" tells you whether the time of day is ahead of (east of) or behind (west of) UTC ๐Ÿงญ. The first two digits are the number of hours difference from Universal Time, and the last two digits are the number of minutes difference from Universal Time โฑ๏ธ. This matches the RFC 2822 format for time zone offsets, not the ISO 8601 format, so keep that in mind ๐Ÿ‘€.

If both the :zone and :originalzone arguments are left out, the local time zone MUST be used. ๐Ÿ 

4.2. Date-part Argument ๐Ÿงฉ

The date-part argument says which part of the resulting date/time value to match against the key-list ๐ŸŽฏ. These are the possible case-insensitive values:

"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.

The restricted ISO 8601 format comes from the date-time ABNF production in [RFC3339], Section 5.6 ๐ŸŒธ. It has these extra rules: the letters "T" and "Z" MUST be in upper case, and a time zone offset of zero MUST be represented by "Z" and not "+00:00" โœจ.


4.3. Comparator Interactions with Date-part Arguments ๐ŸŽ€

Not every comparator plays nicely with every date-part argument ๐Ÿ™ƒ. In general, the date-parts can be compared and tested for equality with either i;ascii-casemap (the default) or i;octet, but there are two exceptions ๐Ÿ’…:

  • julian is an integer, and it might or might not have leading zeros ๐Ÿ”ข. Because of that, i;ascii-numeric is almost certainly the best comparator to use with it ๐Ÿ’–.
  • std11 is there so you can get date/time values in a format that's good for email header fields ๐Ÿ’Œ. The wide range of possible std11 syntaxes (which implementations of this extension are free to use when composing a std11 string) makes this format a poor pick for comparisons ๐Ÿ˜ฌ. Still, if you have to compare it, it's case-insensitive, so you'll need to use i;ascii-casemap ๐Ÿซ .

The "year", "month", "day", "hour", "minute", "second", and "weekday" date-parts all use fixed-width string representations of integers ๐Ÿง. So they can be compared with i;octet, i;ascii-casemap, and i;ascii-numeric, and you get equivalent results with each one ๐Ÿซถ.

"date" and "time" also use fixed-width string representations of integers ๐Ÿ“…. They can be compared with i;octet and i;ascii-casemap โœจ. But i;ascii-numeric can't be used with them, because it doesn't allow for non-digit characters ๐Ÿ™…โ€โ™€๏ธ.


Part 1 of 2 wraps here ๐Ÿฆ‹, and part 2 is coming up with the rest of the good stuff ๐Ÿ’œ๐Ÿซถ

4.4. Examples โœจ

okay so the Date: field is the sender's own claim about when they made the message, and you can check it ๐Ÿ•ฐ๏ธ like, "wait, did they send this during work hours or nah?" ๐Ÿคจ

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"; }

and testing the first Received: field tells you when the message actually landed in your local system, which is way more trustworthy bc the sender didn't write it ๐Ÿ’…

require ["date", "relational", "fileinto"];
if anyof(date :is "received" "weekday" "0",
         date :is "received" "weekday" "6")
{ fileinto "weekend"; }

5. Currentdate Test ๐ŸŒธ

Usage:

Usage:   currentdate [":zone" <time-zone: string>]
                     [COMPARATOR] [MATCH-TYPE]
                     <date-part: string>
                     <key-list: string-list>

the currentdate test is basically the date test's cooler sibling ๐Ÿ˜Ž except instead of digging up a date from the message header, it checks the current date and time. The ":zone" argument and the date-part arguments work exactly the same as they do in the date test, so no new vocab to learn ๐Ÿงš

Heads up, though: every currentdate test in a single Sieve script MUST refer to the same point in time during execution of the script. No one test gets to be in 2009 while another is in 2026 ๐Ÿ™ˆ

Also, the :count value of a currentdate test is always 1. Always. No exceptions ๐Ÿ’œ

5.1. Examples ๐Ÿ“

The simplest use of currentdate is an action that only fires at certain times โฐ Like, a user might want messages sent to their pager after business hours and on weekends:

require ["date", "relational"];
if anyof(currentdate :is "weekday" "0",
         currentdate :is "weekday" "6",
         currentdate :value "lt" "hour" "09",
         currentdate :value "ge" "hour" "17")
{ redirect "pager@example.com"; }

currentdate can also set up vacation [RFC5230] responses ahead of time and then turn them off automatically ๐Ÿ–๏ธ so you don't have to remember to do it yourself:

require ["date", "relational", "vacation"];
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."; }

currentdate is also friends with the variables extension ๐Ÿซถ so you can pass time-dependent values into other tests and actions. This Sieve script drops messages into a folder named after the current month and year ๐Ÿ“:

require ["date", "variables", "fileinto"];
if currentdate :matches "month" "*" { set "month" "${1}"; }
if currentdate :matches "year"  "*" { set "year"  "${1}"; }
fileinto "${month}-${year}";

And finally, currentdate pairs up with the editheader extension to stamp a header field with the date and time info ๐Ÿ–Š๏ธ

require ["variables", "date", "editheader"];
if currentdate :matches "std11" "*"
  {addheader "Processing-date" "${0}";}

6. Index Extension ๐Ÿฆ‹

The "index" extension, if it's specified, adds optional :index and :last arguments to the header, address, and date tests. Here's the lineup ๐Ÿ‘‘

Syntax:

Syntax:   date [":index" <fieldno: number> [":last"]]
               [<":zone" <time-zone: string>> / ":originalzone"]
               [COMPARATOR] [MATCH-TYPE] <header-name: string>
               <date-part: string> <key-list: string-list>

Syntax:   header [":index" <fieldno: number> [":last"]]
                 [COMPARATOR] [MATCH-TYPE]
                 <header-names: string-list> <key-list: string-list>

Syntax:   address [":index" <fieldno: number> [":last"]]
                  [ADDRESS-PART] [COMPARATOR] [MATCH-TYPE]
                  <header-list: string-list> <key-list: string-list>

So if you specify :index <fieldno>, the matching attempts are limited to that one header field, with fieldno starting at 1 for the first named header field ๐ŸŽฏ Add :last on top of that and the count runs backwards, so 1 means the last named header field, 2 means the second to last, and so on ๐Ÿ”™ Specifying :last without :index is an error, so don't do that ๐Ÿ™…โ€โ™€๏ธ

Quick reminder: :index only counts separate header fields, NOT multiple occurrences inside one single field. So you can't use :index to grab one specific address out of an address list that lives inside a single header field ๐Ÿ’€

Both header and address let you name more than one header field. When you do, all the named header fields get counted in the order you list them in the header-list ๐Ÿ“‹

6.1. Example ๐Ÿงšโ€โ™€๏ธ

Mail delivery can involve multiple hops, which means the Received: field holding info about when a message first entered the local administrative domain might be the second or later field in the message โœˆ๏ธ As long as that field offset stays consistent, you can test it:

# Implement the Internet-Draft cutoff date check assuming the
# second Received: field specifies when the message first
# entered the local email infrastructure.
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"; }

7. Security Considerations ๐Ÿ”

Okay this part is seriously important, bestie ๐Ÿšจ The stuff defined here, just like the base Sieve specification, works on message header information that can be forged super easily ๐Ÿ˜ญ Some fields are way more reliable than others though. For example, the Date: field is usually put in by the message sender and can be changed at any point. Meanwhile, the uppermost Received: field is usually added by the local mail system, so it's really hard for the sender or some middleman to fake ๐Ÿ•ต๏ธโ€โ™€๏ธ

Using the currentdate test makes script behavior inherently less predictable and harder to analyze. That could cause problems for systems that rely on script analysis to spot sketchy scripts ๐Ÿ™ƒ

Every security consideration in the base Sieve specification also applies to these extensions ๐Ÿ›ก๏ธ

8. IANA Considerations ๐Ÿ“

The following templates specify the IANA registrations for the two Sieve extensions in this document ๐ŸŽ€

To: iana@iana.org
Subject: Registration of new Sieve extensions

Capability name: date
Description:     The "date" extension gives Sieve the ability
                 to test date and time values.
RFC number:      RFC 5260
Contact address: Sieve discussion list <ietf-mta-filters@imc.org>

Capability name: index
Description:     The "index" extension provides a means to
                 limit header and address tests to specific
                 instances when more than one field of a
                 given type is present.
RFC number:      RFC 5260
Contact address: Sieve discussion list <ietf-mta-filters@imc.org>

9. References ๐Ÿ“š

9.1. Normative References ๐Ÿ’œ

  • [CALGO199] Tantzen, R., "Algorithm 199: Conversions Between Calendar Date and Julian Day Number", Collected Algorithms from CACM 199.
  • [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, March 1997.
  • [RFC2822] Resnick, P., "Internet Message Format", RFC 2822, April 2001.
  • [RFC3339] Klyne, G., Ed. and C. Newman, "Date and Time on the Internet: Timestamps", RFC 3339, July 2002.
  • [RFC5228] Guenther, P. and T. Showalter, "Sieve: An Email Filtering Language", RFC 5228, January 2008.
  • [RFC5231] Segmuller, W. and B. Leiba, "Sieve Email Filtering: Relational Extension", RFC 5231, January 2008.
  • [RFC5234] Crocker, D. and P. Overell, "Augmented BNF for Syntax Specifications: ABNF", STD 68, RFC 5234, January 2008.

9.2. Informative References ๐ŸŒˆ

  • [RFC3501] Crispin, M., "INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1", RFC 3501, March 2003.
  • [RFC5230] Showalter, T. and N. Freed, "Sieve Email Filtering: Vacation Extension", RFC 5230, January 2008.

Appendix A. Julian Date Conversions ๐Ÿงฎ

Here are some C routines that show how to translate day/month/year info to and from modified Julian dates โœจ They're straightforward translations of the Algol routines in CACM Algorithm 199 [CALGO199], so credit where it's due ๐Ÿซถ

First up, given the day, month, and year, jday returns the modified Julian date:

int jday(int year, int month, int day)
{
    int j, c, ya;

    if (month > 2)
        month -= 3;
    else
    {
        month += 9;
        year--;
    }
    c = year / 100;
    ya = year - c * 100;
    return (c * 146097 / 4 + ya * 1461 / 4 + (month * 153 + 2) / 5 +
            day + 1721119);
}

And then, given j, the modified Julian date, jdate gives you back the day, month, and year ๐Ÿ”„

void jdate(int j, int *year, int *month, int *day)
{
    int y, m, d;

    j -= 1721119;
    y = (j * 4 - 1) / 146097;
    j = j * 4 - y * 146097 - 1;
    d = j / 4;
    j = (d * 4 + 3) / 1461;
    d = d * 4 - j * 1461 + 3;
    d = (d + 4) / 4;
    m = (d * 5 - 3) / 153;
    d = d * 5 - m * 153 - 3;
    *day = (d + 5) / 5;
    *year = y * 100 + j;
    if (m < 10)
        *month = m + 3;
    else
    {
        *month = m - 9;
        *year += 1;
    }
}

Appendix B. Acknowledgements ๐Ÿ’–

Dave Cridland wrote the text describing the proper comparators to use with different date-parts, so huge thanks to him ๐Ÿ‘‘ Cyrus Daboo, Frank Ellerman, Alexey Melnikov, Chris Newman, Dilyan Palauzov, and Aaron Stone also gave helpful suggestions and corrections ๐Ÿ™Œ

Author's Address ๐Ÿ–‹๏ธ

Ned Freed
Sun Microsystems
800 Royal Oaks
Monrovia, CA  91016-6347
USA

Phone: +1 909 457 4293
EMail: ned.freed@mrochek.com

Copyright (C) The IETF Trust (2008).

This document is subject to the rights, licenses and restrictions contained in BCP 78, and except as set forth therein, the authors retain all their rights.

This document and the information contained herein are provided on an "AS IS" basis and THE CONTRIBUTOR, THE ORGANIZATION HE/SHE REPRESENTS OR IS SPONSORED BY (IF ANY), THE INTERNET SOCIETY, THE IETF TRUST AND THE INTERNET ENGINEERING TASK FORCE DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.

Intellectual Property ๐Ÿฆ„

The IETF takes no position regarding the validity or scope of any Intellectual Property Rights or other rights that might be claimed to pertain to the implementation or use of the technology described in this document, or the extent to which any license under such rights might or might not be available. It also doesn't represent that it has made any independent effort to identify any such rights. Information on the procedures with respect to rights in RFC documents can be found in BCP 78 and BCP 79.

Copies of IPR disclosures made to the IETF Secretariat and any assurances of licenses to be made available, or the result of an attempt made to obtain a general license or permission for the use of such proprietary rights by implementers or users of this specification, can be obtained from the IETF on-line IPR repository at http://www.ietf.org/ipr.

The IETF invites any interested party to bring to its attention any copyrights, patents or patent applications, or other proprietary rights that may cover technology that may be required to implement this standard. Please address the information to the IETF at ietf-ipr@ietf.org. ๐Ÿ’…โœจ

๐Ÿ“ฆ where this page comes from