Tribulations trying to pin-down viable sieve script constraints in connection with imap events.
Hi Dovecot experts...
I've recently started what I expected to be a tiny project to migrate from a legacy ad-hoc procmail by embracing Sieve scripting.
I have two different frustrations - depending upon which approach I adopt to trigger spam-training actions.
*Option 1*: Trigger actions when messages are moved to/from Spam folder.
This mostly works... but has has a niggle I can't see how to overcome.
I don't have only one 'Spam' folder - I have a hierarchy of folders - with names like "Spam.2026.July" and "Spam.Unsubscribed.2023". My 'learn ham' sieve script uses "imap.mailbox" from environment to determine if the email is being moved out of the Spam hierarchy. This works well. Unfortunately, when triggering 'learn spam' - the value of imap.mailbox is always the destination mailbox 'Spam' - not the source mailbox from which the spammy email has been moved. I can't see any way to trigger a spam reporting action, within Sieve scripts, if (and only if) a message is moved from a folder that does not have a name matching 'Spam.*' into the folder 'Spam'. This restriction is required so that adjustments to filed spam messages do not result in a single email being wrongly presented multiple times for learning as a spam example. This multiple reporting might otherwise happen if, for example, a message was moved between 'Spam' and 'Spam.2025.July" ; "Spam.2025.August" and "Spam.ForReview.2025" several times.
Have I overlooked a reliable way to determine the source folder, in a Sieve script, when a message is moved (copied) to the Spam folder?
*Option 2*: Trigger actions when the Thunderbird 'Junk' and 'Not Junk' buttons are pressed
I accept it would be preferable to avoid depending upon client-specific flags... but I do expect Thunderbird to be the mail client... and marking messages as Junk/Spam in Thunderbird does promise a neat user interface experience.
I've read RFC 6785 (also RFC 5228 and RFC 5232). I understand that the "imap.changedflags" environment value is set both when flags have been added and when removed... and that it is expected that hasflag should be used to determine if the imap.changedflag flag was added or removed. This makes perfect sense, in principle... but the actual behaviour I see is peculiar. With a sieve script /var/dovecot-sieve/scripts/flags_changed.sieve (compiled with sievec) like this:
require ["environment", "imap4flags", "imapsieve", "variables", "vnd.dovecot.debug"];
if header :matches "subject" "*" { debug_log "FLAG-EMAIL Subject: ${1}"; }
if environment :matches "imap.changedflags" "*" { debug_log "FLAG-EMAIL changedflags : ${1}"; }
if hasflag :matches "*" { debug_log "FLAG-EMAIL hasflag : ${1}"; }
and configuration in /etc/dovecot/conf.d/90-sieve.conf like this (N.B. user sieve script does not interact with flags... type=after and type=before behave similarly):
sieve_script flags_changed {
type = after
cause = flag
path = /var/dovecot-sieve/scripts/flags_changed.sieve
}
I experimented with the "Junk" and "Not Junk" buttons for an email with a distinctive subject. The following nested bullet-list summarises the values I found for various scenarios. My experiment followed this order of scenarios - there should have been no flag changes, for the email in question, between each of the scenarios in the test sequence.
- Scenario 1 - Fresh, not-deterimed-to-be-spam email arrives in Inbox...
- Sieve script invocation 1
- imap.changedflags => NonJunk
- hasflag => NonJunk
- Notes: As expected.
- Sieve script invocation 1
- Scenario 2 - Same email is marked as read by Thunderbird after it has been displayed onscreen for a while...
- Sieve script invocation 2
- imap.changedflags => \Seen
- hasflag => \seen
- Notes: Flags are case insensitive - but odd lower case distinction. What happened to NonJunk flag from Scenario 1?
- Sieve script invocation 2
- Scenario 3 - Junk button pressed to manually mark this email as spam...
- Sieve script invocation 3
- imap.changedflags => Junk
- hasflag => \seen
- Sieve script invocation 4
- imap.changedflags => NonJunk
- hasflag => \seen
- Notes: Why neither Junk nor NonJunk flags set for hasflag?
- Scenario 4 - "Not Junk" button is pressed to manually mark this email as ham...
- Sieve script invocation 5
- imap.changedflags => NonJunk
- hasflag => \seen
- Sieve script invocation 6
- imap.changedflags => Junk
- hasflag => \seen
- Sieve script invocation 7
- imap.changedflags => \Seen
- hasflag => NonJunk
- Sieve script invocation 5
Perhaps it is relevant... where I have captured the value from hasflag... that only one token is ever reported - even where I'd expect multiple flags present. I would have expected at least to see both 'Junk' and 'NotJunk' alongside '\seen' for hasflag at appropriate stages in the sequence of scenarios.
Is there a problem with how I've used the :match idiom (with "*") intending to capture all the flags associated with the message? (If so, what approach should I have used instead?)
Is there a bug which prevents using the hasflag feature to disambiguate between a flag reported for imap.changedflags having been added versus having been removed? If not, where have I gone wrong?
Versions...
Server-side: Debian Trixie: dovecot-core/stable,stable-security,now 1:2.4.1+dfsg1-6+deb13u6 amd64 [installed] dovecot-imapd/stable,stable-security,now 1:2.4.1+dfsg1-6+deb13u6 amd64 [installed] dovecot-sieve/stable,stable-security,now 1:2.4.1+dfsg1-6+deb13u6 amd64 [installed]
Client-side: Windows 11 Thunderbird 140.12.1esr (64-bit)
Steve
NonJunk \seen 5
Junk \seen 6 \Seen NonJunk 7 Weird... Is NonJunk reported for hasflag because it is now the only flag associated with the message (after \Seen flag removed)?
Please ignore the garbage below "Steve" in my last email. The text "Weird... Is NonJunk reported for hasflag because it is now the only flag associated with the message (after \Seen flag removed)?" was intended as a note for 'scenario 4'.
Steve
Update...
Using tags to drive actions does work properly - exactly as the RFCs suggest - I had not found a bug. I'd made the following errors:
- I'd assumed that 'environment :matches "*"' would mean '${1}' would contain a representation of all the flags... but that doesn't seem to be the case. Anecdotally, it looks as if an arbitrarily chosen flag is returned for the variable '${1}'. This sort-of makes sense - but it did misled me somewhat.
- I'd mistakenly assumed that 'hasflag :contains' would act on a collection of strings/tokens and tell me if the specified token is present in the collection. In practise, it tells me if any of the flags for 'hasflag' have the specified string as a substring. This was maximally misleading where the two tokens that were being manipulated were 'Spam' and "NonSpam" - as the latter contains the former. Altering my script to use ':is' gave the results I had originally expected.
While the above solves most mysteries for my 'Option 2' - it raises a new (perhaps mostly academic) question: If I wanted to list all the flags for a message, as debug output from a sieve script, how would I do this? Is it possible using only sieve syntax?
Of course, I remain interested to know if dovecot supports initiating a sieve script whenever a message moves (is copied) from any folder to another - where the sieve script can determine the source and destination folders before choosing the correct action. If Dovecot were to have this feature, I'd find it very useful... I'm aware that this functionality isn't demonstrated by the sample scripts I've found to date. If it doesn't exist, would it be feasible to implement it as a third-party extension?
Would https://doc.dovecot.org/latest/core/config/spam_reporting.html be of any help?
Aki
On 09/08/2026 12:32 EEST sjh_dovecot_org--- via dovecot <dovecot@dovecot.org> wrote:
Hi Dovecot experts...
I've recently started what I expected to be a tiny project to migrate from a legacy ad-hoc procmail by embracing Sieve scripting.
I have two different frustrations - depending upon which approach I adopt to trigger spam-training actions.
*Option 1*: Trigger actions when messages are moved to/from Spam folder.
This mostly works... but has has a niggle I can't see how to overcome.
I don't have only one 'Spam' folder - I have a hierarchy of folders - with names like "Spam.2026.July" and "Spam.Unsubscribed.2023". My 'learn ham' sieve script uses "imap.mailbox" from environment to determine if the email is being moved out of the Spam hierarchy. This works well. Unfortunately, when triggering 'learn spam' - the value of imap.mailbox is always the destination mailbox 'Spam' - not the source mailbox from which the spammy email has been moved. I can't see any way to trigger a spam reporting action, within Sieve scripts, if (and only if) a message is moved from a folder that does not have a name matching 'Spam.*' into the folder 'Spam'. This restriction is required so that adjustments to filed spam messages do not result in a single email being wrongly presented multiple times for learning as a spam example. This multiple reporting might otherwise happen if, for example, a message was moved between 'Spam' and 'Spam.2025.July" ; "Spam.2025.August" and "Spam.Fo rR eview.2025" several times.
Have I overlooked a reliable way to determine the source folder, in a Sieve script, when a message is moved (copied) to the Spam folder?
*Option 2*: Trigger actions when the Thunderbird 'Junk' and 'Not Junk' buttons are pressed
I accept it would be preferable to avoid depending upon client-specific flags... but I do expect Thunderbird to be the mail client... and marking messages as Junk/Spam in Thunderbird does promise a neat user interface experience.
I've read RFC 6785 (also RFC 5228 and RFC 5232). I understand that the "imap.changedflags" environment value is set both when flags have been added and when removed... and that it is expected that hasflag should be used to determine if the imap.changedflag flag was added or removed. This makes perfect sense, in principle... but the actual behaviour I see is peculiar. With a sieve script /var/dovecot-sieve/scripts/flags_changed.sieve (compiled with sievec) like this:
require ["environment", "imap4flags", "imapsieve", "variables", "vnd.dovecot.debug"]; if header :matches "subject" "*" { debug_log "FLAG-EMAIL Subject: ${1}"; } if environment :matches "imap.changedflags" "*" { debug_log "FLAG-EMAIL changedflags : ${1}"; } if hasflag :matches "*" { debug_log "FLAG-EMAIL hasflag : ${1}"; }and configuration in /etc/dovecot/conf.d/90-sieve.conf like this (N.B. user sieve script does not interact with flags... type=after and type=before behave similarly):
sieve_script flags_changed { type = after cause = flag path = /var/dovecot-sieve/scripts/flags_changed.sieve }I experimented with the "Junk" and "Not Junk" buttons for an email with a distinctive subject. The following nested bullet-list summarises the values I found for various scenarios. My experiment followed this order of scenarios - there should have been no flag changes, for the email in question, between each of the scenarios in the test sequence.
- Scenario 1 - Fresh, not-deterimed-to-be-spam email arrives in Inbox...
- Sieve script invocation 1
- imap.changedflags => NonJunk
- hasflag => NonJunk
- Notes: As expected.
- Scenario 2 - Same email is marked as read by Thunderbird after it has been displayed onscreen for a while...
- Sieve script invocation 2
- imap.changedflags => \Seen
- hasflag => \seen
- Notes: Flags are case insensitive - but odd lower case distinction. What happened to NonJunk flag from Scenario 1?
- Scenario 3 - Junk button pressed to manually mark this email as spam...
- Sieve script invocation 3
- imap.changedflags => Junk
- hasflag => \seen
- Sieve script invocation 4
- imap.changedflags => NonJunk
- hasflag => \seen
- Notes: Why neither Junk nor NonJunk flags set for hasflag?
- Scenario 4 - "Not Junk" button is pressed to manually mark this email as ham...
- Sieve script invocation 5
- imap.changedflags => NonJunk
- hasflag => \seen
- Sieve script invocation 6
- imap.changedflags => Junk
- hasflag => \seen
- Sieve script invocation 7
- imap.changedflags => \Seen
- hasflag => NonJunk
Perhaps it is relevant... where I have captured the value from hasflag... that only one token is ever reported - even where I'd expect multiple flags present. I would have expected at least to see both 'Junk' and 'NotJunk' alongside '\seen' for hasflag at appropriate stages in the sequence of scenarios.
Is there a problem with how I've used the :match idiom (with "*") intending to capture all the flags associated with the message? (If so, what approach should I have used instead?)
Is there a bug which prevents using the hasflag feature to disambiguate between a flag reported for imap.changedflags having been added versus having been removed? If not, where have I gone wrong?
Versions...
Server-side: Debian Trixie: dovecot-core/stable,stable-security,now 1:2.4.1+dfsg1-6+deb13u6 amd64 [installed] dovecot-imapd/stable,stable-security,now 1:2.4.1+dfsg1-6+deb13u6 amd64 [installed] dovecot-sieve/stable,stable-security,now 1:2.4.1+dfsg1-6+deb13u6 amd64 [installed]
Client-side: Windows 11 Thunderbird 140.12.1esr (64-bit)
Steve
NonJunk \seen 5 Junk \seen 6 \Seen NonJunk 7 Weird... Is NonJunk reported for hasflag because it is now the only flag associated with the message (after \Seen flag removed)?
dovecot mailing list -- dovecot@dovecot.org To unsubscribe send an email to dovecot-leave@dovecot.org
Hi Aki... Thanks for the link - I had read that page in connection with Option 1. The /etc/dovecot/sieve/report-ham.sieve script it proposes is easily modified to exclude training for for ham if emails are moved from the root Spam folder to some more-specific spam folder - e.g. Spam.2026.June as well as 'Trash'.
The /etc/dovecot/sieve/report-spam.sieve script is less easily adjusted so as not to train (again) as spam if an email is moved from Spam.2026.June back to Spam. If it were feasible to discover the source folder for emails copied/moved into Spam in that script, I could easily adapt it to meet my requirements. If I take the approach that moving to a folder (c.f. applying a tag) is how I am going to control training... then the example it provides is 99% complete for my purposes. The only niggle with it, for me, is that it assumes only one Spam folder - whereas I have many (nested) Spam folders.
While I may ultimately adopt a strategy focused upon moving messages to particular folders... Dovecot (and the relevant RFCs) suggest that driving actions when messages are tagged in specific ways (rather than only when moving messages to/from specific folders) should also be fully supported. It just doesn't seem to work.. it looks as if the hasflag feature's results are inconsistent with flags events notified by imap.changedflags.
Steve
Op 9-8-2026 om 23:24 schreef sjh_dovecot_org--- via dovecot:
Hi Aki... Thanks for the link - I had read that page in connection with Option 1. The /etc/dovecot/sieve/report-ham.sieve script it proposes is easily modified to exclude training for for ham if emails are moved from the root Spam folder to some more-specific spam folder - e.g. Spam.2026.June as well as 'Trash'.
The /etc/dovecot/sieve/report-spam.sieve script is less easily adjusted so as not to train (again) as spam if an email is moved from Spam.2026.June back to Spam. If it were feasible to discover the source folder for emails copied/moved into Spam in that script, I could easily adapt it to meet my requirements. If I take the approach that moving to a folder (c.f. applying a tag) is how I am going to control training... then the example it provides is 99% complete for my purposes. The only niggle with it, for me, is that it assumes only one Spam folder - whereas I have many (nested) Spam folders.
While I may ultimately adopt a strategy focused upon moving messages to particular folders... Dovecot (and the relevant RFCs) suggest that driving actions when messages are tagged in specific ways (rather than only when moving messages to/from specific folders) should also be fully supported. It just doesn't seem to work.. it looks as if the hasflag feature's results are inconsistent with flags events notified by imap.changedflags.
These are apparently currently not documented at doc.dovecot.org:
https://github.com/dovecot/pigeonhole/blob/96fe62fe9d1530ef9de584e30571b520d...
Regards,
Stephan.
participants (3)
-
Aki Tuomi
-
sjh_dovecot_org@shic.co.uk
-
Stephan Bosch