Snoozing messages outside office hours in Fastmail
I spent an entire(ish) day learning to use a single sieve command. Hopefully this writeup eases your pain if you are on the same path.
Solution
If you just want to see what the solution looks like, here it is. This snoozes messages sent during the weekend or between 6pm and 8am until the next 8am Mon-Fri.
if allof(
not string :is "${stop}" "Y",
anyof(
date :zone "Europe/Helsinki" :value "lt" "received" "hour" "08",
date :zone "Europe/Helsinki" :value "ge" "received" "hour" "18",
date :zone "Europe/Helsinki" :is "received" "weekday" ["0", "6"]
)
) {
if specialuse_exists "\\Snoozed" {
snooze
:tzid "Europe/Helsinki"
:addflags ["$new"]
:weekdays ["1","2","3","4","5"]
["08:00:00"];
set "hasmailbox" "Y";
set "skipinbox" "Y";
}
}
You don't have to type all of this. You can grab the anyof
section and feed it
to a Sieve condition when creating a new rule in Fastmail. Adjust timezones
as appropriate. Fastmail can generate the rest. If you don't see the Sieve
condition option, click "Switch to no-preview rules (regular expressions supported)" below the rules.
But how did you get there?
Finding information about how sieve filters are written ended up surprisingly difficult. The first promising resource I found is Protonmail's sieve filter documentation. It showcases a bunch of things you might want to do, but I found the explanations very vague. Luckily, the last section links to authoritative resources for a bunch of normal sieve packages.
The one we care about is RFC5260 - Date and Index Extensions. This RFC conveniently also links RFC5228, which is the only good resource for learning the sieve language.
The RFC defines two sieve test commands with the following form:
date [<":zone" <time-zone: string>> / ":originalzone"]
[COMPARATOR] [MATCH-TYPE] <header-name: string>
<date-part: string> <key-list: string-list>
currentdate [":zone" <time-zone: string>]
[COMPARATOR] [MATCH-TYPE]
<date-part: string>
<key-list: string-list>
These work almost identically, except date
grabs the datetime from a specific
email header, while currentdate
uses the current datetime, at the time the
sieve filter is run. If the filter is run on a
message that just arrived, date <match-type> "received"
and currentdate <match-type>
are in practice identical. If running against old messages, the
behavior of currentdate
can be surprising.
Not all headers can be trusted. For example, Sent
is always set by the sender
and can hold any garbage value. Received
, however, is generally set by the
receiving server so we can use it freely.
If timezone is omitted, the filter defaults to the system's local timezone. I could not find which timezone Fastmail uses for sieve.
The :value
match type we use is defined in RFC5231 - Relational Extension.
It is defined as follows:
VALUE = :value ("gt" / "ge" / "lt" / "le" / "eq" / "ne")
Alright, this is starting to form a picture. date
lets us specify how we
want to compare, which header we want to compare against and what part of the
date we care about. Notable that date :value "eq"
is identical to date :is
,
which is actually identical to date
(:is
is the default match type).
Section 4.2. of the date extension RFC explains the available date parts. Reproduced here for convenience.
"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.
All you have to do then is define which part you care about and what you want it to look like. For example, to match Saturday and Sunday, we can do:
anyof(
date :is "received" "weekday" "0", # Sunday
date :is "received" "weekday" "6" # Saturday
)
Since the matched value can be a list, we can simplify this to the identical version below.
date :is "received" "weekday" ["0", "6"]
I hope that's everything you need to know (and all the resources you need to
read) to start using date
in sieve.