From f5505c87e0b338a579691675cd80ce3617027247 Mon Sep 17 00:00:00 2001
From: Stephan Bosch <stephan@rename-it.nl>
Date: Tue, 4 Mar 2014 22:28:56 +0100
Subject: [PATCH] lib-sieve: Upgraded "vnd.dovecot.duplicate" to "duplicate".
 Backwards compatibility is provided for vnd.dovecot.duplicate. Still need to
 fix the constraint that it must not track duplicates from failed Sieve
 executions.

---
 Makefile.am                                   |   5 +-
 README                                        |   4 +-
 configure.ac                                  |   2 +-
 doc/extensions/duplicate.txt                  |  47 ++
 doc/extensions/vnd.dovecot.duplicate.txt      |  58 --
 .../draft-ietf-appsawg-sieve-duplicate-03.txt | 728 ++++++++++++++++++
 src/lib-sieve/Makefile.am                     |   2 +-
 src/lib-sieve/plugins/Makefile.am             |   1 +
 .../{vnd.dovecot => }/duplicate/Makefile.am   |   2 +-
 .../duplicate/ext-duplicate-common.c          |  71 +-
 .../duplicate/ext-duplicate-common.h          |   3 +-
 .../plugins/duplicate/ext-duplicate.c         | 107 +++
 .../duplicate/tst-duplicate.c                 | 110 ++-
 src/lib-sieve/plugins/vnd.dovecot/Makefile.am |   3 +-
 .../plugins/vnd.dovecot/debug/cmd-debug-log.c |   2 +-
 .../vnd.dovecot/debug/ext-debug-common.h      |   2 +-
 .../plugins/vnd.dovecot/debug/ext-debug.c     |   2 +-
 .../vnd.dovecot/duplicate/ext-duplicate.c     |  50 --
 src/lib-sieve/sieve-extensions.c              |  18 +-
 src/plugins/sieve-extprograms/cmd-execute.c   |   2 +-
 src/plugins/sieve-extprograms/cmd-filter.c    |   2 +-
 src/plugins/sieve-extprograms/cmd-pipe.c      |   2 +-
 src/plugins/sieve-extprograms/ext-execute.c   |   2 +-
 src/plugins/sieve-extprograms/ext-filter.c    |   2 +-
 src/plugins/sieve-extprograms/ext-pipe.c      |   4 +-
 .../sieve-extprograms-common.c                |   4 +-
 .../sieve-extprograms-common.h                |   6 +-
 .../sieve-extprograms-plugin.c                |   6 +-
 tests/extensions/duplicate/errors.svtest      |  54 ++
 .../duplicate/errors/conflict-vnd.sieve       |   4 +
 .../duplicate/errors/conflict.sieve           |   4 +
 .../errors/syntax-vnd.sieve}                  |   0
 .../extensions/duplicate/errors/syntax.sieve  |  54 ++
 .../execute-vnd.svtest}                       |   0
 tests/extensions/duplicate/execute.svtest     |  41 +
 .../vnd.dovecot/duplicate/errors.svtest       |  18 -
 36 files changed, 1210 insertions(+), 212 deletions(-)
 create mode 100644 doc/extensions/duplicate.txt
 delete mode 100644 doc/extensions/vnd.dovecot.duplicate.txt
 create mode 100644 doc/rfc/draft-ietf-appsawg-sieve-duplicate-03.txt
 rename src/lib-sieve/plugins/{vnd.dovecot => }/duplicate/Makefile.am (92%)
 rename src/lib-sieve/plugins/{vnd.dovecot => }/duplicate/ext-duplicate-common.c (77%)
 rename src/lib-sieve/plugins/{vnd.dovecot => }/duplicate/ext-duplicate-common.h (85%)
 create mode 100644 src/lib-sieve/plugins/duplicate/ext-duplicate.c
 rename src/lib-sieve/plugins/{vnd.dovecot => }/duplicate/tst-duplicate.c (73%)
 delete mode 100644 src/lib-sieve/plugins/vnd.dovecot/duplicate/ext-duplicate.c
 create mode 100644 tests/extensions/duplicate/errors.svtest
 create mode 100644 tests/extensions/duplicate/errors/conflict-vnd.sieve
 create mode 100644 tests/extensions/duplicate/errors/conflict.sieve
 rename tests/extensions/{vnd.dovecot/duplicate/errors/syntax.sieve => duplicate/errors/syntax-vnd.sieve} (100%)
 create mode 100644 tests/extensions/duplicate/errors/syntax.sieve
 rename tests/extensions/{vnd.dovecot/duplicate/execute.svtest => duplicate/execute-vnd.svtest} (100%)
 create mode 100644 tests/extensions/duplicate/execute.svtest
 delete mode 100644 tests/extensions/vnd.dovecot/duplicate/errors.svtest

diff --git a/Makefile.am b/Makefile.am
index 91429aec5..015403d68 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -136,9 +136,10 @@ test_cases = \
 	tests/extensions/editheader/utf8.svtest \
 	tests/extensions/editheader/protected.svtest \
 	tests/extensions/editheader/errors.svtest \
+	tests/extensions/duplicate/errors.svtest \
+	tests/extensions/duplicate/execute.svtest \
+	tests/extensions/duplicate/execute-vnd.svtest \
 	tests/extensions/vnd.dovecot/debug/execute.svtest \
-	tests/extensions/vnd.dovecot/duplicate/errors.svtest \
-	tests/extensions/vnd.dovecot/duplicate/execute.svtest \
 	tests/deprecated/notify/basic.svtest \
 	tests/deprecated/notify/mailto.svtest \
 	tests/deprecated/notify/errors.svtest \
diff --git a/README b/README
index d4e68b452..1b826c534 100644
--- a/README
+++ b/README
@@ -120,6 +120,7 @@ following list outlines the implementation status of each supported extension:
     mailbox (RFC 5490; Section 3): fully supported (v0.1.10+), but ACL
         permissions are not verified for mailboxexists.
     include (RFC 6609): fully supported (v0.4.0+)
+    duplicate (draft v03): fully supported (v0.4.3+).
     regex (draft v08; not latest version): almost fully supported, but
         UTF-8 is not supported.
 
@@ -135,9 +136,6 @@ following list outlines the implementation status of each supported extension:
 
     vnd.dovecot.debug (v0.3.0+):
         Allows logging debug messages
-    vnd.dovecot.duplicate (v0.3.1+):
-        Allows detecting duplicate message deliveries based on message ID and
-        other criteria.
     vnd.dovecot.pipe (v0.4.0+; sieve_extprograms plugin):
         Implements piping messages to a pre-defined set of external programs
     vnd.dovecot.filter (v0.4.0+; sieve_extprograms plugin):
diff --git a/configure.ac b/configure.ac
index 2adf4d843..b3761c665 100644
--- a/configure.ac
+++ b/configure.ac
@@ -122,9 +122,9 @@ src/lib-sieve/plugins/spamvirustest/Makefile
 src/lib-sieve/plugins/ihave/Makefile
 src/lib-sieve/plugins/editheader/Makefile
 src/lib-sieve/plugins/metadata/Makefile
+src/lib-sieve/plugins/duplicate/Makefile
 src/lib-sieve/plugins/vnd.dovecot/Makefile
 src/lib-sieve/plugins/vnd.dovecot/debug/Makefile
-src/lib-sieve/plugins/vnd.dovecot/duplicate/Makefile
 src/lib-sieve-tool/Makefile
 src/lib-sievestorage/Makefile
 src/lib-managesieve/Makefile
diff --git a/doc/extensions/duplicate.txt b/doc/extensions/duplicate.txt
new file mode 100644
index 000000000..e98e089db
--- /dev/null
+++ b/doc/extensions/duplicate.txt
@@ -0,0 +1,47 @@
+Duplicate Extension
+
+Relevant specifications
+=======================
+
+	doc/rfc/draft-ietf-appsawg-sieve-duplicate-03.txt
+
+Description
+===========
+
+The duplicate extension augments the Sieve filtering implementation with a test
+to verify whether the evaluated string value was seen before in an earlier
+execution of the Sieve script. The main application for this new test is 
+detecting and handling duplicate message deliveries, e.g. as caused by
+mailinglists when people reply both to the mailinglist and the user directly.
+
+Refer to doc/rfc/draft-ietf-appsawg-sieve-duplicate-03.txt for a specification
+of the Sieve language extension. Previously, this extension was Dovecot-specific
+and available under the name "vnd.dovecot.duplicate". That implementation
+differs significantly from what is now published as an internet draft, but
+for backwards compatibility the original extension is still supported.
+
+Configuration
+=============
+
+The "duplicate" extension is not enabled by default. 
+
+The following configuration settings are used:
+
+sieve_duplicate_default_period = 14d
+sieve_duplicate_max_period = 7d
+  These options respectively specify the default and the maximum value for the
+  period after which tracked values are purged from the duplicate tracking
+  database. The period is specified in s(econds), unless followed by a d(ay),
+  h(our) or m(inute) specifier character.
+
+Example
+=======
+
+plugin {
+  sieve = ~/.dovecot.sieve
+
+  sieve_extensions = +vnd.dovecot.duplicate
+
+  sieve_duplicate_default_period = 1h
+	sieve_duplicate_max_period = 1d
+}d
diff --git a/doc/extensions/vnd.dovecot.duplicate.txt b/doc/extensions/vnd.dovecot.duplicate.txt
deleted file mode 100644
index 59ff7599e..000000000
--- a/doc/extensions/vnd.dovecot.duplicate.txt
+++ /dev/null
@@ -1,58 +0,0 @@
-Vnd.dovecot.duplicate Extension
-
-Relevant specifications
-=======================
-
-	doc/rfc/spec-bosch-sieve-duplicate.txt
-
-Description
-===========
-
-The vnd.dovecot.duplicate extension augments the Sieve filtering implementation
-with a test to verify whether the evaluated string value was seen before in an
-earlier execution of the Sieve script. The main application for this new test is
-detecting and handling duplicate message deliveries, e.g. as caused by
-mailinglists when people reply both to the mailinglist and the user directly.
-
-This extension is specific to the Pigeonhole Sieve implementation for the
-Dovecot Secure IMAP server. It will therefore most likely not be supported by
-web interfaces or GUI-based Sieve editors.
-
-Refer to doc/rfc/spec-bosch-sieve-duplicate.txt for a specification of the Sieve
-language extension.
-
-Implementation Status
----------------------
-
-The "vnd.dovecot.duplicate" Sieve language extension is vendor-specific with
-draft status and its implementation for Pigeonhole is experimental, which means
-that the language extensions are still subject to change and that the current
-implementation is not thoroughly tested.
-
-Configuration
-=============
-
-The "vnd.dovecot.duplicate" extension is not enabled by default and thus it
-needs to be enabled explicitly by adding it to the `sieve_extensions' or the
-`sieve_global_extensions' setting. 
-
-The following configuration settings are used:
-
-sieve_duplicate_default_period = 12h
-sieve_duplicate_max_period = 2d
-  These options respectively specify the default and the maximum value for the
-  period after which tracked values are purged from the duplicate tracking
-  database. The period is specified in s(econds), unless followed by a d(ay),
-  h(our) or m(inute) specifier character.
-
-Example
-=======
-
-plugin {
-  sieve = ~/.dovecot.sieve
-
-  sieve_extensions = +vnd.dovecot.duplicate
-
-  sieve_duplicate_default_period = 1h
-	sieve_duplicate_max_period = 1d
-}
diff --git a/doc/rfc/draft-ietf-appsawg-sieve-duplicate-03.txt b/doc/rfc/draft-ietf-appsawg-sieve-duplicate-03.txt
new file mode 100644
index 000000000..1100934c7
--- /dev/null
+++ b/doc/rfc/draft-ietf-appsawg-sieve-duplicate-03.txt
@@ -0,0 +1,728 @@
+
+
+
+APPSAWG                                                         S. Bosch
+Internet-Draft                                             March 3, 2014
+Intended status: Standards Track
+Expires: September 4, 2014
+
+
+         Sieve Email Filtering: Detecting Duplicate Deliveries
+                 draft-ietf-appsawg-sieve-duplicate-03
+
+Abstract
+
+   This document defines a new test command "duplicate" for the "Sieve"
+   email filtering language.  This test adds the ability to detect
+   duplications.  The main application for this new test is handling
+   duplicate deliveries commonly caused by mailing list subscriptions or
+   redirected mail addresses.  The detection is normally performed by
+   matching the message ID to an internal list of message IDs from
+   previously delivered messages.  For more complex applications, the
+   "duplicate" test can also use the content of a specific header or
+   other parts of the message.
+
+Status of this Memo
+
+   This Internet-Draft is submitted in full conformance with the
+   provisions of BCP 78 and BCP 79.
+
+   Internet-Drafts are working documents of the Internet Engineering
+   Task Force (IETF).  Note that other groups may also distribute
+   working documents as Internet-Drafts.  The list of current Internet-
+   Drafts is at http://datatracker.ietf.org/drafts/current/.
+
+   Internet-Drafts are draft documents valid for a maximum of six months
+   and may be updated, replaced, or obsoleted by other documents at any
+   time.  It is inappropriate to use Internet-Drafts as reference
+   material or to cite them other than as "work in progress."
+
+   This Internet-Draft will expire on September 4, 2014.
+
+Copyright Notice
+
+   Copyright (c) 2014 IETF Trust and the persons identified as the
+   document authors.  All rights reserved.
+
+   This document is subject to BCP 78 and the IETF Trust's Legal
+   Provisions Relating to IETF Documents
+   (http://trustee.ietf.org/license-info) in effect on the date of
+   publication of this document.  Please review these documents
+   carefully, as they describe your rights and restrictions with respect
+
+
+
+Bosch                   Expires September 4, 2014               [Page 1]
+
+Internet-Draft    Sieve: Detecting Duplicate Deliveries       March 2014
+
+
+   to this document.  Code Components extracted from this document must
+   include Simplified BSD License text as described in Section 4.e of
+   the Trust Legal Provisions and are provided without warranty as
+   described in the Simplified BSD License.
+
+
+Table of Contents
+
+   1.  Introduction . . . . . . . . . . . . . . . . . . . . . . . . .  3
+   2.  Conventions Used in This Document  . . . . . . . . . . . . . .  3
+   3.  Test "duplicate" . . . . . . . . . . . . . . . . . . . . . . .  3
+     3.1.  Interaction with Other Sieve Extensions  . . . . . . . . .  8
+   4.  Sieve Capability Strings . . . . . . . . . . . . . . . . . . .  8
+   5.  Examples . . . . . . . . . . . . . . . . . . . . . . . . . . .  8
+     5.1.  Example 1  . . . . . . . . . . . . . . . . . . . . . . . .  8
+     5.2.  Example 2  . . . . . . . . . . . . . . . . . . . . . . . .  8
+     5.3.  Example 3  . . . . . . . . . . . . . . . . . . . . . . . .  9
+     5.4.  Example 4  . . . . . . . . . . . . . . . . . . . . . . . . 10
+   6.  Security Considerations  . . . . . . . . . . . . . . . . . . . 11
+   7.  IANA Considerations  . . . . . . . . . . . . . . . . . . . . . 11
+   8.  Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . 11
+   9.  References . . . . . . . . . . . . . . . . . . . . . . . . . . 12
+     9.1.  Normative References . . . . . . . . . . . . . . . . . . . 12
+     9.2.  Informative References . . . . . . . . . . . . . . . . . . 12
+   Author's Address . . . . . . . . . . . . . . . . . . . . . . . . . 13
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Bosch                   Expires September 4, 2014               [Page 2]
+
+Internet-Draft    Sieve: Detecting Duplicate Deliveries       March 2014
+
+
+1.  Introduction
+
+   This document specifies an extension to the Sieve filtering language
+   defined by RFC 5228 [SIEVE].  It adds a test to track whether or not
+   a text string was seen before by the delivery agent in an earlier
+   execution of the Sieve script.  This can be used to detect and handle
+   duplicate message deliveries.
+
+   Duplicate deliveries are a common side-effect of being subscribed to
+   a mailing list.  For example, if a member of the list decides to
+   reply to both the user and the mailing list itself, the user will
+   often get one copy of the message directly and another through the
+   mailing list.  Also, if someone cross-posts over several mailing
+   lists to which the user is subscribed, the user will likely receive a
+   copy from each of those lists.  In another scenario, the user has
+   several redirected mail addresses all pointing to his main mail
+   account.  If one of the user's contacts sends the message to more
+   than one of those addresses, the user will likely receive more than a
+   single copy.  Using the "duplicate" extension, users have the means
+   to detect and handle such duplicates, e.g. by discarding them,
+   marking them as "seen", or putting them in a special folder.
+
+   Duplicate messages are normally detected using the Message-ID header
+   field, which is required to be unique for each message.  However, the
+   "duplicate" test is flexible enough to use different criteria for
+   defining what makes a message a duplicate, for example on the subject
+   line or parts of the message body.  Other applications of this new
+   test command are also possible, as long as the tracked unique value
+   is a string.
+
+
+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 [KEYWORDS].
+
+   Conventions for notations are as in [SIEVE] Section 1.1, including
+   use of the "Usage:" label for the definition of action and tagged
+   arguments syntax.
+
+
+3.  Test "duplicate"
+
+   Usage: "duplicate" [":handle" <handle: string>]
+                      [":header" <header-name: string> /
+                          ":uniqueid" <value: string>]
+                      [":seconds" <timeout: number>] [":last"]
+
+
+
+Bosch                   Expires September 4, 2014               [Page 3]
+
+Internet-Draft    Sieve: Detecting Duplicate Deliveries       March 2014
+
+
+   In its basic form, the "duplicate" test keeps track of which messages
+   were seen before by this test during an earlier Sieve execution.
+   Messages are by default identified by their message ID as contained
+   in the Message-ID header.  The "duplicate" test evaluates to "true"
+   when the message was seen before and it evaluates to "false" when it
+   was not.
+
+   As a side-effect, the "duplicate" test adds the message ID to an
+   internal duplicate tracking list once the Sieve execution finishes
+   successfully.  This way, the same test will evaluate to "true" during
+   the next Sieve execution.  Note that this side-effect is performed
+   only when the "duplicate" test is actually evaluated.  If the
+   "duplicate" test is nested in a control structure or it is not the
+   first item of an "allof" or "anyof" test list, its evaluation depends
+   on the result of preceding tests, which may produce unexpected
+   results.
+
+   Implementations MUST only update the internal duplicate tracking list
+   when the Sieve script execution finishes successfully.  If failing
+   script executions add the message ID to the duplicate tracking list,
+   all "duplicate" tests in the Sieve script would erroneously yield
+   "true" for the next delivery attempt of the same message, which can
+   -- depending on the action taken for a duplicate -- easily lead to
+   discarding the message without further notice.
+
+   However, deferring the definitive modification of the tracking list
+   to the end of a successful Sieve script execution is not without
+   problems.  It can cause a race condition when a duplicate message is
+   delivered in parallel before the tracking list is updated.  This way,
+   a duplicate message could be missed by the "duplicate" test.  More
+   complex implementations could use a locking mechanism to prevent this
+   problem.  But, irrespective of what implementation is chosen,
+   situations in which the "duplicate" test erroneously yields "true"
+   MUST be prevented.
+
+   The "duplicate" test MUST only check for duplicates amongst message
+   ID values encountered in previous executions of the Sieve script; it
+   MUST NOT consider ID values encountered earlier in the current Sieve
+   script execution as potential duplicates.  This means that all
+   "duplicate" tests in a Sieve script execution, including those
+   located in scripts included using the "include" [INCLUDE] extension,
+   MUST always yield the same result if the arguments are identical.
+
+   Implementations SHOULD limit the number of entries in the duplicate
+   tracking list.  When limiting the number of entries, implementations
+   SHOULD discard the oldest ones first.
+
+   Also, implementations SHOULD let entries in the tracking list expire
+
+
+
+Bosch                   Expires September 4, 2014               [Page 4]
+
+Internet-Draft    Sieve: Detecting Duplicate Deliveries       March 2014
+
+
+   after a short period of time.  The user can explicitly control the
+   length of this expiration time by means of the ":seconds" argument,
+   which accepts an integer value specifying the timeout value in
+   seconds.  If the ":seconds" argument is omitted, an appropriate
+   default value MUST be used.  A default expiration time of around 7
+   days is usually appropriate.  Sites SHOULD impose a maximum limit on
+   the expiration time.  If that limit is exceeded by the ":seconds"
+   argument, the maximum value MUST silently be substituted; exceeding
+   the limit MUST NOT produce an error.  If the ":seconds" argument is
+   zero, the "duplicate" test MUST yield "false" unconditionally.
+
+   When the ":last" argument is omitted, the expiration time for entries
+   in the duplicate tracking list MUST be measured relative to the
+   moment at which the entry was first created; i.e., at the end of the
+   successful script execution during which "duplicate" test returned
+   "false" for a message with that particular message ID value.  This
+   means that subsequent duplicate messages have no influence on the
+   time at which the entry in the duplicate tracking list finally
+   expires.
+
+   In contrast, when the ":last" argument is specified, the expiration
+   time MUST be measured relative to the last script execution during
+   which the "duplicate" test was used to check the entry's message ID
+   value.  This effectively means that the entry in the duplicate
+   tracking will not expire while duplicate messages with the
+   corresponding message ID keep being delivered within intervals
+   smaller than the expiration time.
+
+   By default, the content of the message's Message-ID header field is
+   used as the unique ID for duplicate tracking.  For more complex
+   applications, the "duplicate" test can also be used to detect
+   duplicate deliveries based on other message text.  Then, the tracked
+   unique ID can be an arbitrary string value extracted from the
+   message.  By adding the ":header" argument with a message header
+   field name, the content of the specified header field can be used as
+   the tracked unique ID instead of the default Message-ID header.
+   Alternatively, the tracked unique ID can be specified explicitly
+   using the ":uniqueid" argument.  The ":header" and ":uniqueid"
+   arguments are mutually exclusive and specifying both for a single
+   "duplicate" test command MUST trigger an error.
+
+   The syntax rules for the header name parameter of the ":header"
+   argument are specified in Section 2.4.2.2 of RFC 5228 [SIEVE].  Note
+   that implementations MUST NOT trigger an error for an invalid header
+   name.  Instead, the "duplicate" test MUST yield "false"
+   unconditionally in this case.  The parameter of the ":uniqueid"
+   argument can be any string.
+
+
+
+
+Bosch                   Expires September 4, 2014               [Page 5]
+
+Internet-Draft    Sieve: Detecting Duplicate Deliveries       March 2014
+
+
+   The Messsage-ID header field is assumed to be globally unique as
+   required in Section 3.6.4 of RFC 5322 [IMAIL].  In practice, this
+   assumption may not aways prove to be true.  The "duplicate" tests
+   does not deal with this situation implicitly, which means that false
+   duplicates may be detected in this case.  However, the user can
+   address such situations by specifying an alternative means of message
+   identification using the ":header" or the ":uniqueid" argument.
+
+   If the tracked unique ID value is extracted directly from a message
+   header field, i.e., when the ":uniqueid" argument is not used, the
+   following operations MUST be performed before the actual duplicate
+   verification:
+
+   o  Unfold the header line as described in [IMAIL] Section 2.2.3. (see
+      also Section 2.4.2.2 of RFC 5228 [SIEVE]).
+
+   o  If possible, convert the header value to Unicode, encoded as UTF-8
+      (see Section 2.7.2 of RFC 5228 [SIEVE]).  If conversion is not
+      possible, the value is left unchanged.
+
+   o  Trim leading and trailing whitespace from the header value (see
+      Section 2.2 of RFC 5228 [SIEVE]).
+
+   Note that these rules also apply to the Message-ID header field used
+   by the basic "duplicate" test without a ":header" or ":uniqueid"
+   argument.  When the ":uniqueid" argument is used, such normalization
+   concerns are the responsibility of the user.
+
+   If the header field specified using the ":header" argument exists
+   multiple times in the message, only the first occurrence MUST be used
+   for duplicate tracking.  If the specified header field is not present
+   in the message, the "duplicate" test MUST yield "false"
+   unconditionally.  In that case the duplicate tracking list is left
+   unmodified by this test, since no unique ID value is available.  The
+   same rules apply with respect to the Message-ID header field for the
+   basic "duplicate" test without a ":header" or ":uniqueid" argument,
+   since that header field could also be missing or occur multiple
+   times.
+
+   The string parameter of the ":uniqueid" argument can be composed from
+   arbitrary text extracted from the message using the "variables"
+   [VARIABLES] extension.  To extract text from the message body, the
+   "foreverypart" and "extracttext" [SIEVE-MIME] extensions need to be
+   used as well.  This provides the user with detailed control over what
+   identifies a message as a duplicate.
+
+   The tracked unique ID value MUST be matched case-sensitively,
+   irrespective of whether it originates from a header or is specified
+
+
+
+Bosch                   Expires September 4, 2014               [Page 6]
+
+Internet-Draft    Sieve: Detecting Duplicate Deliveries       March 2014
+
+
+   explicitly using the ":uniqueid" argument.  To achieve case-
+   insensitive behavior, the "set" command added by the "variables"
+   [VARIABLES] extension can be used in combination with the ":uniqueid"
+   argument to normalize the tracked unique ID value to upper or lower
+   case.
+
+   The "duplicate" test MUST track a unique ID value independent of its
+   source.  This means that it does not matter whether values are
+   obtained from the message ID header, from an arbitrary header
+   specified using the ":header" argument or explicitly from the
+   ":uniqueid" argument.  For example, the following three examples are
+   equivalent and match the same entry in the duplicate tracking list:
+
+   require "duplicate";
+   if duplicate {
+     discard;
+   }
+
+
+   require "duplicate";
+   if duplicate :header "message-id" {
+     discard;
+   }
+
+
+   require ["duplicate", "variables"];
+   if header :matches "message-id" "*" {
+     if duplicate :uniqueid "${0}" {
+       discard;
+     }
+   }
+
+   The ":handle" argument can be used to override this default behavior.
+   The ":handle" argument separates a "duplicate" test from other
+   duplicate tests with a different or omitted ":handle" argument.
+   Using the ":handle" argument, unrelated "duplicate" tests can be
+   prevented from interfering with each other: a message is only
+   recognized as a duplicate when the tracked unique ID was seen before
+   in an earlier script execution by a "duplicate" test with the same
+   ":handle" argument.
+
+   NOTE: The necessary mechanism to track duplicate messages is very
+   similar to the mechanism that is needed for tracking duplicate
+   responses for the "vacation" [VACATION] action.  One way to implement
+   the necessary mechanism for the "duplicate" test is therefore to
+   store a hash of the tracked unique ID and, if provided, the ":handle"
+   argument.
+
+
+
+
+Bosch                   Expires September 4, 2014               [Page 7]
+
+Internet-Draft    Sieve: Detecting Duplicate Deliveries       March 2014
+
+
+3.1.  Interaction with Other Sieve Extensions
+
+   The "duplicate" test does not support either the "index"
+   [DATE-INDEX], or "mime" [SIEVE-MIME] extensions directly, meaning
+   that none of the ":index", ":mime" or associated arguments are added
+   to the "duplicate" test when these extensions are active.  The
+   ":uniqueid" argument can be used in combination with the "variables"
+   [VARIABLES] extension to achieve the same result indirectly.
+
+   Normally, Sieve scripts are executed at final delivery.  However,
+   with the "imapsieve" [IMAPSIEVE] extension, Sieve scripts are invoked
+   when the IMAP [IMAP] server performs operations on the message store,
+   e.g. when messages are uploaded, flagged, or moved to another
+   location.  The "duplicate" test is devised for use at final delivery
+   and the semantics in the "imapsieve" context are left undefined.
+   Therefore it is NOT RECOMMENDED to allow the "duplicate" test to be
+   used in the context of "imapsieve".
+
+
+4.  Sieve Capability Strings
+
+   A Sieve implementation that defines the "duplicate" test command will
+   advertise the capability string "duplicate".
+
+
+5.  Examples
+
+5.1.  Example 1
+
+   In this basic example, message duplicates are detected by tracking
+   the Message-ID header.  Duplicate deliveries are stored in a special
+   folder contained in the user's Trash folder.  If the folder does not
+   exist, it is created automatically using the "mailbox" [MAILBOX]
+   extension.  This way, the user has a chance to recover messages when
+   necessary.  Messages that are not recognized as duplicates are stored
+   in the user's inbox as normal.
+
+   require ["duplicate", "fileinto", "mailbox"];
+
+   if duplicate {
+     fileinto :create "Trash/Duplicate";
+   }
+
+5.2.  Example 2
+
+   This example shows a more complex use of the "duplicate" test.  The
+   user gets network alerts from a set of remote automated monitoring
+   systems.  Several notifications can be received about the same event
+
+
+
+Bosch                   Expires September 4, 2014               [Page 8]
+
+Internet-Draft    Sieve: Detecting Duplicate Deliveries       March 2014
+
+
+   from different monitoring systems.  The Message-ID of these messages
+   is different, because these are all distinct messages from different
+   senders.  To avoid being notified more than a single time about the
+   same event the user writes the following script:
+
+   require ["duplicate", "variables", "imap4flags",
+     "fileinto"];
+
+   if header :matches "subject" "ALERT: *" {
+     if duplicate :seconds 60 :uniqueid "${1}" {
+       setflag "\\seen";
+     }
+     fileinto "Alerts";
+   }
+
+   The subjects of the notification message are structured with a
+   predictable pattern which includes a description of the event.  In
+   the script above, the "duplicate" test is used to detect duplicate
+   alert events.  The message subject is matched against a pattern and
+   the event description is extracted using the "variables" [VARIABLES]
+   extension.  If a message with that event in the subject was received
+   before, but more than a minute ago, it is not detected as a duplicate
+   due to the specified ":seconds" argument.  In the the event of a
+   duplicate, the message is marked as "seen" using the "imap4flags"
+   [IMAP4FLAGS] extension.  All alert messages are put into the "Alerts"
+   mailbox irrespective of whether those messages are duplicates or not.
+
+5.3.  Example 3
+
+   This example shows how the "duplicate" test can be used to limit the
+   frequency of notifications sent using the "enotify" [NOTIFY]
+   extension.  Consider the following scenario: a mail user receives
+   XMPP notifications [NOTIFY-XMPP] about new mail through Sieve, but
+   sometimes a single contact sends many messages in a short period of
+   time.  Now the user wants to prevent being notified of all of those
+   messages.  The user wants to be notified about messages from each
+   person at most once per 30 minutes and writes the following script:
+
+   require ["variables", "envelope", "enotify", "duplicate"];
+
+   if envelope :matches "from" "*" { set "sender" "${1}"; }
+   if header :matches "subject" "*" { set "subject" "${1}"; }
+
+   if not duplicate :seconds 1800 :uniqueid "${sender}"
+   {
+     notify :message "[SIEVE] ${sender}: ${subject}"
+       "xmpp:user@im.example.com";
+   }
+
+
+
+Bosch                   Expires September 4, 2014               [Page 9]
+
+Internet-Draft    Sieve: Detecting Duplicate Deliveries       March 2014
+
+
+   The example shown above uses the message envelope sender rather than
+   the Message-ID header as the unique ID for duplicate tracking.
+
+   The example can be extended to allow more messages from the same
+   sender in close succession as long as the discussed subject is
+   different.  This can be achieved as follows:
+
+   require ["variables", "envelope", "enotify", "duplicate"];
+
+   if envelope :matches "from" "*" { set "sender" "${1}"; }
+   if header :matches "subject" "*" { set "subject" "${1}"; }
+
+   # account for 'Re:' prefix
+   if string :comparator "i;ascii-casemap"
+     :matches "${subject}" "Re:*"
+   {
+     set "subject" "${1}";
+   }
+   if not duplicate :seconds 1800
+     :uniqueid "${sender} ${subject}"
+   {
+     notify :message "[SIEVE] ${sender}: ${subject}"
+       "xmpp:user@im.example.com";
+   }
+
+   This uses a combination of the message envelope sender and the
+   subject of the message as the unique ID for duplicate tracking.
+
+5.4.  Example 4
+
+   For this example, the mail user uses the "duplicate" test for two
+   separate applications: for discarding duplicate events from a
+   notification system and to mark certain follow-up messages in a
+   software support mailing as "seen" using the "imap4flags"
+   [IMAP4FLAGS] extension.
+
+   The two "duplicate" tests in the following example each use a
+   different header to identify messages.  However, these "X-Event-ID"
+   and "X-Ticket-ID headers can have similar values in this case (e.g.
+   both based on a time stamp), meaning that one "duplicate" test can
+   erroneously detect duplicates based on ID values tracked by the
+   other.  Therefore, the user wants to prevent the second "duplicate"
+   test from matching ID values tracked by the first "duplicate" test
+   and vice versa.  This is achieved by specifying different ":handle"
+   arguments for these tests.
+
+
+
+
+
+
+Bosch                   Expires September 4, 2014              [Page 10]
+
+Internet-Draft    Sieve: Detecting Duplicate Deliveries       March 2014
+
+
+   require ["duplicate", "imap4flags"];
+
+   if duplicate :header "X-Event-ID" :handle "notifier" {
+     discard;
+   }
+   if allof (
+     duplicate :header "X-Ticket-ID" :handle "support",
+     address "to" "support@example.com",
+     header :contains "subject" "fileserver")
+   {
+     setflag "\\seen";
+   }
+
+
+6.  Security Considerations
+
+   A flood of unique messages could cause the list of tracked message ID
+   values to grow indefinitely.  Implementations SHOULD apply limits on
+   the number and lifespan of entries in that list.
+
+
+7.  IANA Considerations
+
+   The following template specifies the IANA registration of the Sieve
+   extension specified in this document:
+
+      To: iana@iana.org
+      Subject: Registration of new Sieve extension
+
+      Capability name: duplicate
+      Description:     Adds test 'duplicate' that can be used to test
+                       whether a particular message is a duplicate;
+                       i.e., whether a copy of it was seen before by the
+                       delivery agent that is executing the Sieve
+                       script.
+      RFC number:      this RFC
+      Contact address: Sieve mailing list <sieve@ietf.org>
+
+   This information should be added to the list of sieve extensions
+   given on http://www.iana.org/assignments/sieve-extensions.
+
+
+8.  Acknowledgements
+
+   Thanks to Cyrus Daboo, Arnt Gulbrandsen, Tony Hansen, Kristin Hubner,
+   Alexey Melnikov, Subramanian Moonesamy, Tom Petch, Hector Santos,
+   Robert Sparks, and Aaron Stone for reviews and suggestions.  With
+   special thanks to Ned Freed for his guidance and support.
+
+
+
+Bosch                   Expires September 4, 2014              [Page 11]
+
+Internet-Draft    Sieve: Detecting Duplicate Deliveries       March 2014
+
+
+9.  References
+
+9.1.  Normative References
+
+   [DATE-INDEX]
+              Freed, N., "Sieve Email Filtering: Date and Index
+              Extensions", RFC 5260, July 2008.
+
+   [IMAIL]    Resnick, P., Ed., "Internet Message Format", RFC 5322,
+              October 2008.
+
+   [IMAPSIEVE]
+              Leiba, B., "Support for Internet Message Access Protocol
+              (IMAP) Events in Sieve", RFC 6785, November 2012.
+
+   [INCLUDE]  Daboo, C. and A. Stone, "Sieve Email Filtering: Include
+              Extension", RFC 6609, May 2012.
+
+   [KEYWORDS]
+              Bradner, S., "Key words for use in RFCs to Indicate
+              Requirement Levels", BCP 14, RFC 2119, March 1997.
+
+   [SIEVE]    Guenther, P. and T. Showalter, "Sieve: An Email Filtering
+              Language", RFC 5228, January 2008.
+
+   [SIEVE-MIME]
+              Hansen, T. and C. Daboo, "Sieve Email Filtering: MIME Part
+              Tests, Iteration, Extraction, Replacement, and Enclosure",
+              RFC 5703, October 2009.
+
+   [VARIABLES]
+              Homme, K., "Sieve Email Filtering: Variables Extension",
+              RFC 5229, January 2008.
+
+9.2.  Informative References
+
+   [IMAP]     Crispin, M., "INTERNET MESSAGE ACCESS PROTOCOL - VERSION
+              4rev1", RFC 3501, March 2003.
+
+   [IMAP4FLAGS]
+              Melnikov, A., "Sieve Email Filtering: Imap4flags
+              Extension", RFC 5232, January 2008.
+
+   [MAILBOX]  Melnikov, A., "The Sieve Mail-Filtering Language --
+              Extensions for Checking Mailbox Status and Accessing
+              Mailbox Metadata", RFC 5490, March 2009.
+
+   [NOTIFY]   Melnikov, A., Leiba, B., Segmuller, W., and T. Martin,
+
+
+
+Bosch                   Expires September 4, 2014              [Page 12]
+
+Internet-Draft    Sieve: Detecting Duplicate Deliveries       March 2014
+
+
+              "Sieve Email Filtering: Extension for Notifications",
+              RFC 5435, January 2009.
+
+   [NOTIFY-XMPP]
+              Saint-Andre, P. and A. Melnikov, "Sieve Notification
+              Mechanism: Extensible Messaging and Presence Protocol
+              (XMPP)", RFC 5437, January 2009.
+
+   [VACATION]
+              Showalter, T. and N. Freed, "Sieve Email Filtering:
+              Vacation Extension", RFC 5230, January 2008.
+
+
+Author's Address
+
+   Stephan Bosch
+   Enschede
+   NL
+
+   Email: stephan@rename-it.nl
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Bosch                   Expires September 4, 2014              [Page 13]
+
diff --git a/src/lib-sieve/Makefile.am b/src/lib-sieve/Makefile.am
index 0f8f15619..d6007ca91 100644
--- a/src/lib-sieve/Makefile.am
+++ b/src/lib-sieve/Makefile.am
@@ -68,8 +68,8 @@ plugins = \
 	$(extdir)/spamvirustest/libsieve_ext_spamvirustest.la \
 	$(extdir)/ihave/libsieve_ext_ihave.la \
 	$(extdir)/editheader/libsieve_ext_editheader.la \
+	$(extdir)/duplicate/libsieve_ext_duplicate.la \
 	$(extdir)/vnd.dovecot/debug/libsieve_ext_debug.la \
-	$(extdir)/vnd.dovecot/duplicate/libsieve_ext_duplicate.la \
 	$(unfinished_plugins)
 
 libdovecot_sieve_la_DEPENDENCIES = \
diff --git a/src/lib-sieve/plugins/Makefile.am b/src/lib-sieve/plugins/Makefile.am
index a26c4a820..915ada271 100644
--- a/src/lib-sieve/plugins/Makefile.am
+++ b/src/lib-sieve/plugins/Makefile.am
@@ -21,6 +21,7 @@ SUBDIRS = \
 	spamvirustest \
 	ihave \
 	editheader \
+	duplicate \
 	vnd.dovecot \
 	$(UNFINISHED)
 
diff --git a/src/lib-sieve/plugins/vnd.dovecot/duplicate/Makefile.am b/src/lib-sieve/plugins/duplicate/Makefile.am
similarity index 92%
rename from src/lib-sieve/plugins/vnd.dovecot/duplicate/Makefile.am
rename to src/lib-sieve/plugins/duplicate/Makefile.am
index 3d72ae223..7469b987f 100644
--- a/src/lib-sieve/plugins/vnd.dovecot/duplicate/Makefile.am
+++ b/src/lib-sieve/plugins/duplicate/Makefile.am
@@ -1,7 +1,7 @@
 noinst_LTLIBRARIES = libsieve_ext_duplicate.la
 
 AM_CPPFLAGS = \
-	-I$(srcdir)/../../.. \
+	-I$(srcdir)/../.. \
 	$(LIBDOVECOT_INCLUDE)
 
 tests = \
diff --git a/src/lib-sieve/plugins/vnd.dovecot/duplicate/ext-duplicate-common.c b/src/lib-sieve/plugins/duplicate/ext-duplicate-common.c
similarity index 77%
rename from src/lib-sieve/plugins/vnd.dovecot/duplicate/ext-duplicate-common.c
rename to src/lib-sieve/plugins/duplicate/ext-duplicate-common.c
index 602d5ed2e..bdc0dcb2f 100644
--- a/src/lib-sieve/plugins/vnd.dovecot/duplicate/ext-duplicate-common.c
+++ b/src/lib-sieve/plugins/duplicate/ext-duplicate-common.c
@@ -72,6 +72,7 @@ struct act_duplicate_mark_data {
 	const char *handle;
 	unsigned int period;
 	unsigned char hash[MD5_RESULTLEN];
+	unsigned int last:1;
 };
 
 static void act_duplicate_mark_print
@@ -97,15 +98,18 @@ static void act_duplicate_mark_print
 {
 	struct act_duplicate_mark_data *data =
 		(struct act_duplicate_mark_data *) action->context;
+	const char *last = (data->last ? " last" : "");
 
 	if (data->handle != NULL) {
-		sieve_result_action_printf(rpenv, "track duplicate with handle: %s",
-			str_sanitize(data->handle, 128));
+		sieve_result_action_printf(rpenv, "track%s duplicate with handle: %s",
+			last, str_sanitize(data->handle, 128));
 	} else {
-		sieve_result_action_printf(rpenv, "track duplicate");		
+		sieve_result_action_printf(rpenv, "track%s duplicate", last);
 	}
 }
 
+// FIXME: at commit phase the sieve script is still not guaranteed to finish
+//         successfully. We need a new final stage in Sieve result execution.
 static int act_duplicate_mark_commit
 (const struct sieve_action *action,
 	const struct sieve_action_exec_env *aenv,
@@ -131,6 +135,7 @@ static int act_duplicate_mark_commit
 
 struct ext_duplicate_handle {
 	const char *handle;
+	unsigned int last:1;
 	unsigned int duplicate:1;
 };
 
@@ -141,18 +146,40 @@ struct ext_duplicate_context {
 	unsigned int nohandle_checked:1;
 };
 
+static void ext_duplicate_hash
+(string_t *handle, const char *value, size_t value_len, bool last,
+	unsigned char hash_r[])
+{
+	static const char *id = "sieve duplicate";
+	struct md5_context md5ctx;
+
+	md5_init(&md5ctx);
+	md5_update(&md5ctx, id, strlen(id));
+	if (last)
+		md5_update(&md5ctx, "0", 1);
+	else
+		md5_update(&md5ctx, "+", 1);
+	if (handle != NULL) {
+		md5_update(&md5ctx, "h-", 2);
+		md5_update(&md5ctx, str_c(handle), str_len(handle));
+	} else {
+		md5_update(&md5ctx, "default", 7);
+	}
+	md5_update(&md5ctx, value, value_len);
+	md5_final(&md5ctx, hash_r);
+}
+
 int ext_duplicate_check
 (const struct sieve_runtime_env *renv, string_t *handle,
-	const char *value, size_t value_len, sieve_number_t period)
+	const char *value, size_t value_len, sieve_number_t period,
+	bool last)
 {
 	const struct sieve_extension *this_ext = renv->oprtn->ext;
 	const struct sieve_script_env *senv = renv->scriptenv;
 	struct ext_duplicate_context *rctx;
 	bool duplicate = FALSE;
 	pool_t msg_pool = NULL, result_pool = NULL;
-	static const char *id = "sieve duplicate";
 	struct act_duplicate_mark_data *act;
-	struct md5_context ctx;
 
 	if ( !sieve_action_duplicate_check_available(senv) || value == NULL )
 		return 0;
@@ -175,7 +202,8 @@ int ext_duplicate_check
 		} else if ( array_is_created(&rctx->handles) ) {
 			const struct ext_duplicate_handle *record;
 			array_foreach (&rctx->handles, record) {
-				if ( strcmp(record->handle, str_c(handle)) == 0 )
+				if ( strcmp(record->handle, str_c(handle)) == 0 &&
+					record->last == last )
 					return ( record->duplicate ? 1 : 0 );
 			}
 		}
@@ -186,29 +214,31 @@ int ext_duplicate_check
 	if (handle != NULL)
 		act->handle = p_strdup(result_pool, str_c(handle));
 	act->period = period;
+	act->last = last;
 
 	/* Create hash */
-	md5_init(&ctx);
-	md5_update(&ctx, id, strlen(id));
-	if (handle != NULL) {
-		md5_update(&ctx, "h-", 2);
-		md5_update(&ctx, str_c(handle), str_len(handle));
-	} else {
-		md5_update(&ctx, "default", 7);
-	}
-	md5_update(&ctx, value, value_len);
-	md5_final(&ctx, act->hash);
+	ext_duplicate_hash(handle, value, value_len, last, act->hash);
 
 	/* Check duplicate */
 	duplicate = sieve_action_duplicate_check(senv, act->hash, sizeof(act->hash));
 
+	if (!duplicate && last) {
+		unsigned char no_last_hash[MD5_RESULTLEN];
+
+		/* Check for entry without :last */
+		ext_duplicate_hash(handle, value, value_len, FALSE, no_last_hash);
+		sieve_action_duplicate_check(senv, no_last_hash, sizeof(no_last_hash));
+	}
+
 	/* We may only mark the message as duplicate when Sieve script executes
 	 * successfully; therefore defer this operation until successful result
 	 * execution.
 	 */
-	if ( sieve_result_add_action
-		(renv, NULL, &act_duplicate_mark, NULL, (void *) act, 0, FALSE) < 0 )
-		return -1;
+	if (!duplicate || last) {
+		if ( sieve_result_add_action
+			(renv, NULL, &act_duplicate_mark, NULL, (void *) act, 0, FALSE) < 0 )
+			return -1;
+	}
 
 	/* Cache result */
 	if ( handle == NULL ) {
@@ -223,6 +253,7 @@ int ext_duplicate_check
 			p_array_init(&rctx->handles, msg_pool, 64);
 		record = array_append_space(&rctx->handles);
 		record->handle = p_strdup(msg_pool, str_c(handle));
+		record->last = last;
 		record->duplicate = duplicate;
 	}
 
diff --git a/src/lib-sieve/plugins/vnd.dovecot/duplicate/ext-duplicate-common.h b/src/lib-sieve/plugins/duplicate/ext-duplicate-common.h
similarity index 85%
rename from src/lib-sieve/plugins/vnd.dovecot/duplicate/ext-duplicate-common.h
rename to src/lib-sieve/plugins/duplicate/ext-duplicate-common.h
index 2ca6ce4b4..af1883de0 100644
--- a/src/lib-sieve/plugins/vnd.dovecot/duplicate/ext-duplicate-common.h
+++ b/src/lib-sieve/plugins/duplicate/ext-duplicate-common.h
@@ -21,6 +21,7 @@ void ext_duplicate_unload
 	(const struct sieve_extension *ext);
 
 extern const struct sieve_extension_def duplicate_extension;
+extern const struct sieve_extension_def vnd_duplicate_extension;
 
 /*
  * Tests
@@ -40,6 +41,6 @@ extern const struct sieve_operation_def tst_duplicate_operation;
 
 int ext_duplicate_check
 	(const struct sieve_runtime_env *renv, string_t *handle,
-		const char *value, size_t value_len, sieve_number_t period);
+		const char *value, size_t value_len, sieve_number_t period, bool last);
 
 #endif /* EXT_DUPLICATE_COMMON_H */
diff --git a/src/lib-sieve/plugins/duplicate/ext-duplicate.c b/src/lib-sieve/plugins/duplicate/ext-duplicate.c
new file mode 100644
index 000000000..0e440849f
--- /dev/null
+++ b/src/lib-sieve/plugins/duplicate/ext-duplicate.c
@@ -0,0 +1,107 @@
+/* Copyright (c) 2002-2014 Pigeonhole authors, see the included COPYING file
+ */
+
+/* Extension duplicate
+ * -------------------
+ *
+ * Authors: Stephan Bosch
+ * Specification: vendor-defined; spec-bosch-sieve-duplicate
+ * Implementation: full
+ * Status: experimental
+ *
+ */
+
+/* Extension vnd.dovecot.duplicate
+ * -------------------------------
+ *
+ * Authors: Stephan Bosch
+ * Specification: vendor-defined; spec-bosch-sieve-duplicate
+ * Implementation: full, but deprecated; provided for backwards compatibility
+ * Status: experimental
+ *
+ */
+
+#include "lib.h"
+
+#include "sieve-extensions.h"
+#include "sieve-commands.h"
+#include "sieve-binary.h"
+
+#include "sieve-validator.h"
+
+#include "ext-duplicate-common.h"
+
+/*
+ * Extensions
+ */
+
+static bool ext_duplicate_validator_load
+	(const struct sieve_extension *ext, struct sieve_validator *valdtr);
+
+const struct sieve_extension_def duplicate_extension = {
+	.name = "duplicate",
+	.load = ext_duplicate_load,
+	.unload = ext_duplicate_unload,
+	.validator_load = ext_duplicate_validator_load,
+	SIEVE_EXT_DEFINE_OPERATION(tst_duplicate_operation)
+};
+
+const struct sieve_extension_def vnd_duplicate_extension = {
+	.name = "vnd.dovecot.duplicate",
+	.load = ext_duplicate_load,
+	.unload = ext_duplicate_unload,
+	.validator_load = ext_duplicate_validator_load,
+	SIEVE_EXT_DEFINE_OPERATION(tst_duplicate_operation)
+};
+
+/*
+ * Validation
+ */
+
+static bool ext_duplicate_validator_extension_validate
+	(const struct sieve_extension *ext, struct sieve_validator *valdtr,
+		void *context, struct sieve_ast_argument *require_arg);
+
+const struct sieve_validator_extension duplicate_validator_extension = {
+	&vnd_duplicate_extension,
+	ext_duplicate_validator_extension_validate,
+	NULL
+};
+
+static bool ext_duplicate_validator_load
+(const struct sieve_extension *ext, struct sieve_validator *valdtr)
+{
+	/* Register validator extension to check for conflict between
+	   vnd.dovecot.duplicate and duplicate extensions */
+	if ( sieve_extension_is(ext, vnd_duplicate_extension) ) {
+		sieve_validator_extension_register
+			(valdtr, ext, &duplicate_validator_extension, NULL);
+	}
+
+	/* Register duplicate test */
+	sieve_validator_register_command(valdtr, ext, &tst_duplicate);
+
+	return TRUE;
+}
+
+static bool ext_duplicate_validator_extension_validate
+(const struct sieve_extension *ext, struct sieve_validator *valdtr,
+	void *context ATTR_UNUSED, struct sieve_ast_argument *require_arg)
+{
+	const struct sieve_extension *ext_dupl;
+
+	if ( (ext_dupl=sieve_extension_get_by_name
+		(ext->svinst, "duplicate")) != NULL ) {
+
+		/* Check for conflict with duplicate extension */
+		if ( sieve_validator_extension_loaded(valdtr, ext_dupl) ) {
+			sieve_argument_validate_error(valdtr, require_arg,
+				"the (deprecated) vnd.dovecot.duplicate extension cannot be used "
+				"together with the duplicate extension");
+			return FALSE;
+		}
+	}
+
+	return TRUE;
+}
+
diff --git a/src/lib-sieve/plugins/vnd.dovecot/duplicate/tst-duplicate.c b/src/lib-sieve/plugins/duplicate/tst-duplicate.c
similarity index 73%
rename from src/lib-sieve/plugins/vnd.dovecot/duplicate/tst-duplicate.c
rename to src/lib-sieve/plugins/duplicate/tst-duplicate.c
index 2e4de5a57..921f17108 100644
--- a/src/lib-sieve/plugins/vnd.dovecot/duplicate/tst-duplicate.c
+++ b/src/lib-sieve/plugins/duplicate/tst-duplicate.c
@@ -17,10 +17,10 @@
 /* Duplicate test
  *
  * Syntax:
- *   Usage: "duplicate" [":seconds" <timeout: number>]
+ *   Usage: "duplicate" [":handle" <handle: string>]
  *                      [":header" <header-name: string> /
- *                          ":value" <value: string>]
- *                      [":handle" <handle: string>]
+ *                          ":uniqueid" <value: string>]
+ *                      [":seconds" <timeout: number>] [":last"]
  */
 
 static bool tst_duplicate_registered
@@ -64,8 +64,15 @@ static const struct sieve_argument_def duplicate_header_tag = {
 	NULL, NULL, NULL
 };
 
+static const struct sieve_argument_def duplicate_uniqueid_tag = {
+	"uniqueid",
+	NULL,
+	tst_duplicate_validate_string_tag,
+	NULL, NULL, NULL
+};
+
 static const struct sieve_argument_def duplicate_value_tag = {
-	"value",
+	"value", /* vnd.dovecot.duplicate (deprecated) */
 	NULL,
 	tst_duplicate_validate_string_tag,
 	NULL, NULL, NULL
@@ -78,13 +85,19 @@ static const struct sieve_argument_def duplicate_handle_tag = {
 	NULL, NULL, NULL
 };
 
+static const struct sieve_argument_def duplicate_last_tag = {
+	"last",
+	NULL, NULL,	NULL, NULL, NULL
+};
+
 /* Codes for optional arguments */
 
 enum tst_duplicate_optional {
 	OPT_END,
 	OPT_SECONDS,
 	OPT_HEADER,
-	OPT_VALUE,
+	OPT_UNIQUEID,
+	OPT_LAST,
 	OPT_HANDLE
 };
 
@@ -143,7 +156,6 @@ static bool tst_duplicate_validate_number_tag
 
 	/* Skip parameter */
 	*arg = sieve_ast_argument_next(*arg);
-
 	return TRUE;
 }
 
@@ -151,6 +163,7 @@ static bool tst_duplicate_validate_string_tag
 (struct sieve_validator *valdtr, struct sieve_ast_argument **arg,
 	struct sieve_command *cmd)
 {
+	const struct sieve_extension *ext = cmd->ext;
 	struct sieve_ast_argument *tag = *arg;
 
 	/* Detach the tag itself */
@@ -166,19 +179,31 @@ static bool tst_duplicate_validate_string_tag
 		return FALSE;
 	}
 
-	if ((bool)cmd->data == TRUE) {
+	if ( (bool)cmd->data ) {
 		sieve_argument_validate_error(valdtr, *arg,
-			"conflicting :header and :value arguments specified "
-			"for the duplicate test");
-		return TRUE;
+			"conflicting :header and %s arguments specified "
+			"for the duplicate test",
+			(sieve_extension_is(ext, duplicate_extension) ? ":uniqueid" : ":value"));
+		return FALSE;
 	}
 
+	/* :header <header-name: string> */
 	if ( sieve_argument_is(tag, duplicate_header_tag) ) {
 		if ( !sieve_command_verify_headers_argument(valdtr, *arg) )
 			return FALSE;
-		cmd->data = (void*)TRUE;
+		cmd->data = (void *)TRUE;
+	/* :handle <handle: string> */
+	} else if ( sieve_argument_is(tag, duplicate_handle_tag) ) {
+		/* nothing to be done */
+	} else if ( sieve_argument_is(tag, duplicate_uniqueid_tag) ) {
+		i_assert(sieve_extension_is(ext, duplicate_extension));
+		cmd->data = (void *)TRUE;
+	/* :value <value: string> (vnd.dovecot.duplicate) */
 	} else if ( sieve_argument_is(tag, duplicate_value_tag) ) {
-		cmd->data = (void*)TRUE;
+		i_assert(sieve_extension_is(ext, vnd_duplicate_extension));
+		cmd->data = (void *)TRUE;
+	} else {
+		i_unreached();
 	}
 
 	/* Skip parameter */
@@ -197,9 +222,16 @@ static bool tst_duplicate_registered
 	sieve_validator_register_tag
 		(valdtr, cmd_reg, ext, &duplicate_seconds_tag, OPT_SECONDS);
 	sieve_validator_register_tag
-		(valdtr, cmd_reg, ext, &duplicate_header_tag, OPT_HEADER);
+		(valdtr, cmd_reg, ext, &duplicate_last_tag, OPT_LAST);
 	sieve_validator_register_tag
-		(valdtr, cmd_reg, ext, &duplicate_value_tag, OPT_VALUE);
+		(valdtr, cmd_reg, ext, &duplicate_header_tag, OPT_HEADER);
+	if ( sieve_extension_is(ext, duplicate_extension) ) {
+		sieve_validator_register_tag
+			(valdtr, cmd_reg, ext, &duplicate_uniqueid_tag, OPT_UNIQUEID);
+	} else {
+		sieve_validator_register_tag
+			(valdtr, cmd_reg, ext, &duplicate_value_tag, OPT_UNIQUEID);
+	}
 	sieve_validator_register_tag
 		(valdtr, cmd_reg, ext, &duplicate_handle_tag, OPT_HANDLE);
 	return TRUE;
@@ -227,6 +259,7 @@ static bool tst_duplicate_generate
 static bool tst_duplicate_operation_dump
 (const struct sieve_dumptime_env *denv, sieve_size_t *address)
 {
+	const struct sieve_extension *ext = denv->oprtn->ext;
 	int opt_code = 0;
 
 	sieve_code_dumpf(denv, "DUPLICATE");
@@ -247,11 +280,17 @@ static bool tst_duplicate_operation_dump
 		case OPT_SECONDS:
 			opok = sieve_opr_number_dump(denv, address, "seconds");
 			break;
+		case OPT_LAST:
+			sieve_code_dumpf(denv, "last");
+			break;
 		case OPT_HEADER:
 			opok = sieve_opr_string_dump(denv, address, "header");
 			break;
-		case OPT_VALUE:
-			opok = sieve_opr_string_dump(denv, address, "value");
+		case OPT_UNIQUEID:
+			if ( sieve_extension_is(ext, duplicate_extension) )
+				opok = sieve_opr_string_dump(denv, address, "uniqueid");
+			else
+				opok = sieve_opr_string_dump(denv, address, "value");
 			break;
 		case OPT_HANDLE:
 			opok = sieve_opr_string_dump(denv, address, "handle");
@@ -277,11 +316,11 @@ static int tst_duplicate_operation_execute
 	const struct ext_duplicate_config *config =
 		(const struct ext_duplicate_config *) ext->context;
 	int opt_code = 0;
-	string_t *handle = NULL, *header = NULL, *value = NULL;
+	string_t *handle = NULL, *header = NULL, *uniqueid = NULL;
 	const char *val = NULL;
 	size_t val_len = 0;
 	sieve_number_t seconds = config->default_period;
-	bool duplicate = FALSE;
+	bool last = FALSE, duplicate = FALSE;
 	int ret;
 
 	/*
@@ -302,11 +341,18 @@ static int tst_duplicate_operation_execute
 		case OPT_SECONDS:
 			ret = sieve_opr_number_read(renv, address, "seconds", &seconds);
 			break;
+		case OPT_LAST:
+			last = TRUE;
+			ret = SIEVE_EXEC_OK;
+			break;
 		case OPT_HEADER:
 			ret = sieve_opr_string_read(renv, address, "header", &header);
 			break;
-		case OPT_VALUE:
-			ret = sieve_opr_string_read(renv, address, "value", &value);
+		case OPT_UNIQUEID:
+			if ( sieve_extension_is(ext, duplicate_extension) )
+				ret = sieve_opr_string_read(renv, address, "uniqueid", &uniqueid);
+			else
+				ret = sieve_opr_string_read(renv, address, "value", &uniqueid);
 			break;
 		case OPT_HANDLE:
 			ret = sieve_opr_string_read(renv, address, "handle", &handle);
@@ -328,22 +374,28 @@ static int tst_duplicate_operation_execute
 	sieve_runtime_trace_descend(renv);
 
 	/* Get value */
-	if (header != NULL) {
-		if (mail_get_first_header(renv->msgdata->mail, str_c(header), &val) > 0)
+	if ( uniqueid != NULL ) {
+		val = str_c(uniqueid);
+		val_len = str_len(uniqueid);
+	} else {
+		if ( header == NULL ) {
+			ret = mail_get_first_header_utf8
+				(renv->msgdata->mail, "Message-ID", &val);
+		} else {
+			ret = mail_get_first_header_utf8
+				(renv->msgdata->mail, str_c(header), &val);
+		}
+			
+		if ( ret > 0 )
 			val_len = strlen(val);
-	} else if (value != NULL) {
-		val = str_c(value);
-		val_len = str_len(value);
-	} else if (renv->msgdata->id != NULL) {
-		val = renv->msgdata->id;
-		val_len = strlen(renv->msgdata->id);
 	}
 
 	/* Check duplicate */
 	if (val == NULL) {
 		duplicate = FALSE;
 	} else {	
-		if ((ret=ext_duplicate_check(renv, handle, val, val_len, seconds)) < 0)
+		if ((ret=ext_duplicate_check
+			(renv, handle, val, val_len, seconds, last)) < 0)
 			return SIEVE_EXEC_FAILURE;
 		duplicate = ( ret > 0 );
 	}
diff --git a/src/lib-sieve/plugins/vnd.dovecot/Makefile.am b/src/lib-sieve/plugins/vnd.dovecot/Makefile.am
index 200940813..3c46867ff 100644
--- a/src/lib-sieve/plugins/vnd.dovecot/Makefile.am
+++ b/src/lib-sieve/plugins/vnd.dovecot/Makefile.am
@@ -1,3 +1,2 @@
-SUBDIRS = debug duplicate
-
+SUBDIRS = debug
 
diff --git a/src/lib-sieve/plugins/vnd.dovecot/debug/cmd-debug-log.c b/src/lib-sieve/plugins/vnd.dovecot/debug/cmd-debug-log.c
index 4229cf192..585aa9bd6 100644
--- a/src/lib-sieve/plugins/vnd.dovecot/debug/cmd-debug-log.c
+++ b/src/lib-sieve/plugins/vnd.dovecot/debug/cmd-debug-log.c
@@ -50,7 +50,7 @@ static int cmd_debug_log_operation_execute
 
 const struct sieve_operation_def debug_log_operation = {
 	"debug_log",
-	&debug_extension,
+	&vnd_debug_extension,
 	0,
 	cmd_debug_log_operation_dump,
 	cmd_debug_log_operation_execute
diff --git a/src/lib-sieve/plugins/vnd.dovecot/debug/ext-debug-common.h b/src/lib-sieve/plugins/vnd.dovecot/debug/ext-debug-common.h
index 0d69de04c..5e7690ed8 100644
--- a/src/lib-sieve/plugins/vnd.dovecot/debug/ext-debug-common.h
+++ b/src/lib-sieve/plugins/vnd.dovecot/debug/ext-debug-common.h
@@ -8,7 +8,7 @@
  * Extensions
  */
 
-extern const struct sieve_extension_def debug_extension;
+extern const struct sieve_extension_def vnd_debug_extension;
 
 /*
  * Commands
diff --git a/src/lib-sieve/plugins/vnd.dovecot/debug/ext-debug.c b/src/lib-sieve/plugins/vnd.dovecot/debug/ext-debug.c
index 0af87771a..6d2eb26bf 100644
--- a/src/lib-sieve/plugins/vnd.dovecot/debug/ext-debug.c
+++ b/src/lib-sieve/plugins/vnd.dovecot/debug/ext-debug.c
@@ -39,7 +39,7 @@ static bool ext_debug_interpreter_load
 		const struct sieve_runtime_env *renv, sieve_size_t *address ATTR_UNUSED);
 
 
-const struct sieve_extension_def debug_extension = {
+const struct sieve_extension_def vnd_debug_extension = {
 	.name = "vnd.dovecot.debug",
 	.validator_load = ext_debug_validator_load,
 	.interpreter_load = ext_debug_interpreter_load,
diff --git a/src/lib-sieve/plugins/vnd.dovecot/duplicate/ext-duplicate.c b/src/lib-sieve/plugins/vnd.dovecot/duplicate/ext-duplicate.c
deleted file mode 100644
index acbc22f73..000000000
--- a/src/lib-sieve/plugins/vnd.dovecot/duplicate/ext-duplicate.c
+++ /dev/null
@@ -1,50 +0,0 @@
-/* Copyright (c) 2002-2014 Pigeonhole authors, see the included COPYING file
- */
-
-/* Extension vnd.dovecot.duplicate
- * -------------------------------
- *
- * Authors: Stephan Bosch
- * Specification: vendor-defined; spec-bosch-sieve-duplicate
- * Implementation: full
- * Status: experimental
- *
- */
-
-#include "lib.h"
-
-#include "sieve-extensions.h"
-#include "sieve-commands.h"
-#include "sieve-binary.h"
-
-#include "sieve-validator.h"
-
-#include "ext-duplicate-common.h"
-
-/*
- * Extension
- */
-
-static bool ext_duplicate_validator_load
-	(const struct sieve_extension *ext, struct sieve_validator *valdtr);
-
-const struct sieve_extension_def duplicate_extension = {
-	.name = "vnd.dovecot.duplicate",
-	.load = ext_duplicate_load,
-	.unload = ext_duplicate_unload,
-	.validator_load = ext_duplicate_validator_load,
-	SIEVE_EXT_DEFINE_OPERATION(tst_duplicate_operation)
-};
-
-/*
- * Validation
- */
-
-static bool ext_duplicate_validator_load
-(const struct sieve_extension *ext, struct sieve_validator *valdtr)
-{
-	/* Register duplicate test */
-	sieve_validator_register_command(valdtr, ext, &tst_duplicate);
-
-	return TRUE;
-}
diff --git a/src/lib-sieve/sieve-extensions.c b/src/lib-sieve/sieve-extensions.c
index eebb8cb44..c1aaf9a6d 100644
--- a/src/lib-sieve/sieve-extensions.c
+++ b/src/lib-sieve/sieve-extensions.c
@@ -99,12 +99,11 @@ extern const struct sieve_extension_def spamtestplus_extension;
 extern const struct sieve_extension_def virustest_extension;
 extern const struct sieve_extension_def ihave_extension;
 extern const struct sieve_extension_def editheader_extension;
-extern const struct sieve_extension_def mboxmetadata_extension;
-extern const struct sieve_extension_def servermetadata_extension;
+extern const struct sieve_extension_def duplicate_extension;
 
 /* vnd.dovecot. */
-extern const struct sieve_extension_def debug_extension;
-extern const struct sieve_extension_def duplicate_extension;
+extern const struct sieve_extension_def vnd_debug_extension;
+extern const struct sieve_extension_def vnd_duplicate_extension;
 
 /*
  * List of native extensions
@@ -131,7 +130,7 @@ const struct sieve_extension_def *sieve_core_extensions[] = {
 	&relational_extension, &regex_extension, &imap4flags_extension,
 	&copy_extension, &include_extension, &body_extension,
 	&variables_extension, &enotify_extension, &environment_extension,
-	&mailbox_extension, &date_extension, &ihave_extension,
+	&mailbox_extension, &date_extension, &ihave_extension, &duplicate_extension
 };
 
 const unsigned int sieve_core_extensions_count =
@@ -147,7 +146,7 @@ const struct sieve_extension_def *sieve_extra_extensions[] = {
 	&virustest_extension, &editheader_extension,
 
 	/* vnd.dovecot. */
-	&debug_extension, &duplicate_extension
+	&vnd_debug_extension
 };
 
 const unsigned int sieve_extra_extensions_count =
@@ -162,7 +161,8 @@ extern const struct sieve_extension_def notify_extension;
 
 const struct sieve_extension_def *sieve_deprecated_extensions[] = {
 	&imapflags_extension,
-	&notify_extension
+	&notify_extension,
+	&vnd_duplicate_extension
 };
 
 const unsigned int sieve_deprecated_extensions_count =
@@ -175,6 +175,8 @@ const unsigned int sieve_deprecated_extensions_count =
 #ifdef HAVE_SIEVE_UNFINISHED
 
 extern const struct sieve_extension_def ereject_extension;
+extern const struct sieve_extension_def mboxmetadata_extension;
+extern const struct sieve_extension_def servermetadata_extension;
 
 const struct sieve_extension_def *sieve_unfinished_extensions[] = {
 	&ereject_extension, &mboxmetadata_extension, &servermetadata_extension
@@ -696,7 +698,7 @@ const struct sieve_extension *sieve_get_address_part_extension
 
 void sieve_enable_debug_extension(struct sieve_instance *svinst)
 {
-	(void) sieve_extension_register(svinst, &debug_extension, TRUE);
+	(void) sieve_extension_register(svinst, &vnd_debug_extension, TRUE);
 }
 
 /*
diff --git a/src/plugins/sieve-extprograms/cmd-execute.c b/src/plugins/sieve-extprograms/cmd-execute.c
index a4f346e80..a15a26cad 100644
--- a/src/plugins/sieve-extprograms/cmd-execute.c
+++ b/src/plugins/sieve-extprograms/cmd-execute.c
@@ -105,7 +105,7 @@ static int cmd_execute_operation_execute
 	(const struct sieve_runtime_env *renv, sieve_size_t *address);
 
 const struct sieve_operation_def cmd_execute_operation = { 
-	"EXECUTE", &execute_extension, 
+	"EXECUTE", &vnd_execute_extension, 
 	0,
 	cmd_execute_operation_dump, 
 	cmd_execute_operation_execute
diff --git a/src/plugins/sieve-extprograms/cmd-filter.c b/src/plugins/sieve-extprograms/cmd-filter.c
index 5b7382e94..e1484db9d 100644
--- a/src/plugins/sieve-extprograms/cmd-filter.c
+++ b/src/plugins/sieve-extprograms/cmd-filter.c
@@ -62,7 +62,7 @@ static int cmd_filter_operation_execute
 	(const struct sieve_runtime_env *renv, sieve_size_t *address);
 
 const struct sieve_operation_def cmd_filter_operation = { 
-	"FILTER", &filter_extension, 
+	"FILTER", &vnd_filter_extension, 
 	0,
 	cmd_filter_operation_dump, 
 	cmd_filter_operation_execute
diff --git a/src/plugins/sieve-extprograms/cmd-pipe.c b/src/plugins/sieve-extprograms/cmd-pipe.c
index a73ec25b4..11bb1439a 100644
--- a/src/plugins/sieve-extprograms/cmd-pipe.c
+++ b/src/plugins/sieve-extprograms/cmd-pipe.c
@@ -67,7 +67,7 @@ static int cmd_pipe_operation_execute
 	(const struct sieve_runtime_env *renv, sieve_size_t *address);
 
 const struct sieve_operation_def cmd_pipe_operation = { 
-	"PIPE", &pipe_extension, 0,
+	"PIPE", &vnd_pipe_extension, 0,
 	cmd_pipe_operation_dump, 
 	cmd_pipe_operation_execute
 };
diff --git a/src/plugins/sieve-extprograms/ext-execute.c b/src/plugins/sieve-extprograms/ext-execute.c
index 855502b17..1ea2999c2 100644
--- a/src/plugins/sieve-extprograms/ext-execute.c
+++ b/src/plugins/sieve-extprograms/ext-execute.c
@@ -33,7 +33,7 @@ static void ext_execute_unload(const struct sieve_extension *ext);
 static bool ext_execute_validator_load
 	(const struct sieve_extension *ext, struct sieve_validator *valdtr);
 	
-const struct sieve_extension_def execute_extension = { 
+const struct sieve_extension_def vnd_execute_extension = { 
 	.name = "vnd.dovecot.execute",
 	.load = ext_execute_load,
 	.unload = ext_execute_unload,
diff --git a/src/plugins/sieve-extprograms/ext-filter.c b/src/plugins/sieve-extprograms/ext-filter.c
index c1a4212ba..886cc4346 100644
--- a/src/plugins/sieve-extprograms/ext-filter.c
+++ b/src/plugins/sieve-extprograms/ext-filter.c
@@ -33,7 +33,7 @@ static void ext_filter_unload(const struct sieve_extension *ext);
 static bool ext_filter_validator_load
 	(const struct sieve_extension *ext, struct sieve_validator *valdtr);
 	
-const struct sieve_extension_def filter_extension = { 
+const struct sieve_extension_def vnd_filter_extension = { 
 	.name = "vnd.dovecot.filter",
 	.load = ext_filter_load,
 	.unload = ext_filter_unload,
diff --git a/src/plugins/sieve-extprograms/ext-pipe.c b/src/plugins/sieve-extprograms/ext-pipe.c
index 47eebdfb1..cc1de8527 100644
--- a/src/plugins/sieve-extprograms/ext-pipe.c
+++ b/src/plugins/sieve-extprograms/ext-pipe.c
@@ -33,7 +33,7 @@ static void ext_pipe_unload(const struct sieve_extension *ext);
 static bool ext_pipe_validator_load
 	(const struct sieve_extension *ext, struct sieve_validator *valdtr);
 	
-const struct sieve_extension_def pipe_extension = { 
+const struct sieve_extension_def vnd_pipe_extension = { 
 	.name = "vnd.dovecot.pipe",
 	.load = ext_pipe_load,
 	.unload = ext_pipe_unload,
@@ -75,7 +75,7 @@ static bool ext_pipe_validator_extension_validate
 		void *context, struct sieve_ast_argument *require_arg);
 
 const struct sieve_validator_extension pipe_validator_extension = {
-	&pipe_extension,
+	&vnd_pipe_extension,
 	ext_pipe_validator_extension_validate,
 	NULL
 };
diff --git a/src/plugins/sieve-extprograms/sieve-extprograms-common.c b/src/plugins/sieve-extprograms/sieve-extprograms-common.c
index 36b19faca..6912640e7 100644
--- a/src/plugins/sieve-extprograms/sieve-extprograms-common.c
+++ b/src/plugins/sieve-extprograms/sieve-extprograms-common.c
@@ -96,9 +96,9 @@ struct sieve_extprograms_config *sieve_extprograms_config_init
 		}
 	}
 
-	if ( sieve_extension_is(ext, pipe_extension) ) 
+	if ( sieve_extension_is(ext, vnd_pipe_extension) ) 
 		ext_config->copy_ext = sieve_ext_copy_get_extension(ext->svinst);
-	if ( sieve_extension_is(ext, execute_extension) ) 
+	if ( sieve_extension_is(ext, vnd_execute_extension) ) 
 		ext_config->var_ext = sieve_ext_variables_get_extension(ext->svinst);
 	return ext_config;
 }
diff --git a/src/plugins/sieve-extprograms/sieve-extprograms-common.h b/src/plugins/sieve-extprograms/sieve-extprograms-common.h
index c865670ef..03850decb 100644
--- a/src/plugins/sieve-extprograms/sieve-extprograms-common.h
+++ b/src/plugins/sieve-extprograms/sieve-extprograms-common.h
@@ -29,9 +29,9 @@ void sieve_extprograms_config_deinit
  * Extensions
  */
 
-extern const struct sieve_extension_def pipe_extension;
-extern const struct sieve_extension_def filter_extension;
-extern const struct sieve_extension_def execute_extension;
+extern const struct sieve_extension_def vnd_pipe_extension;
+extern const struct sieve_extension_def vnd_filter_extension;
+extern const struct sieve_extension_def vnd_execute_extension;
 
 /* 
  * Commands 
diff --git a/src/plugins/sieve-extprograms/sieve-extprograms-plugin.c b/src/plugins/sieve-extprograms/sieve-extprograms-plugin.c
index ee445a9b5..9fb58c97f 100644
--- a/src/plugins/sieve-extprograms/sieve-extprograms-plugin.c
+++ b/src/plugins/sieve-extprograms/sieve-extprograms-plugin.c
@@ -24,11 +24,11 @@ void sieve_extprograms_plugin_load
 	struct _plugin_context *pctx = i_new(struct _plugin_context, 1);
 
 	pctx->ext_pipe = sieve_extension_register
-		(svinst, &pipe_extension, FALSE);
+		(svinst, &vnd_pipe_extension, FALSE);
 	pctx->ext_filter = sieve_extension_register
-		(svinst, &filter_extension, FALSE);
+		(svinst, &vnd_filter_extension, FALSE);
 	pctx->ext_execute = sieve_extension_register
-		(svinst, &execute_extension, FALSE);
+		(svinst, &vnd_execute_extension, FALSE);
 
 	if ( svinst->debug ) {
 		sieve_sys_debug(svinst, "Sieve Extprograms plugin for %s version %s loaded",
diff --git a/tests/extensions/duplicate/errors.svtest b/tests/extensions/duplicate/errors.svtest
new file mode 100644
index 000000000..108a0f06e
--- /dev/null
+++ b/tests/extensions/duplicate/errors.svtest
@@ -0,0 +1,54 @@
+require "vnd.dovecot.testsuite";
+
+require "relational";
+require "comparator-i;ascii-numeric";
+
+/*
+ * Invalid syntax
+ */
+
+test "Invalid Syntax" {
+        if test_script_compile "errors/syntax.sieve" {
+                test_fail "compile should have failed";
+        }
+
+        if not test_error :count "eq" :comparator "i;ascii-numeric" "17" {
+                test_fail "wrong number of errors reported";
+        }
+}
+
+test "Invalid Syntax (vnd)" {
+        if test_script_compile "errors/syntax-vnd.sieve" {
+                test_fail "compile should have failed";
+        }
+
+        if not test_error :count "eq" :comparator "i;ascii-numeric" "5" {
+                test_fail "wrong number of errors reported";
+        }
+}
+
+/*
+ * Extension conflict
+ */
+
+test "Extension conflict" {
+        if test_script_compile "errors/conflict.sieve" {
+                test_fail "compile should have failed";
+        }
+
+        if not test_error :count "eq" :comparator "i;ascii-numeric" "2" {
+                test_fail "wrong number of errors reported";
+        }
+}
+
+test "Extension conflict (vnd first)" {
+        if test_script_compile "errors/conflict-vnd.sieve" {
+                test_fail "compile should have failed";
+        }
+
+        if not test_error :count "eq" :comparator "i;ascii-numeric" "2" {
+                test_fail "wrong number of errors reported";
+        }
+}
+
+
diff --git a/tests/extensions/duplicate/errors/conflict-vnd.sieve b/tests/extensions/duplicate/errors/conflict-vnd.sieve
new file mode 100644
index 000000000..1c133df18
--- /dev/null
+++ b/tests/extensions/duplicate/errors/conflict-vnd.sieve
@@ -0,0 +1,4 @@
+require "vnd.dovecot.duplicate";
+require "duplicate";
+
+if duplicate { keep; }
diff --git a/tests/extensions/duplicate/errors/conflict.sieve b/tests/extensions/duplicate/errors/conflict.sieve
new file mode 100644
index 000000000..aa9b038f3
--- /dev/null
+++ b/tests/extensions/duplicate/errors/conflict.sieve
@@ -0,0 +1,4 @@
+require "duplicate";
+require "vnd.dovecot.duplicate";
+
+if duplicate { keep; }
diff --git a/tests/extensions/vnd.dovecot/duplicate/errors/syntax.sieve b/tests/extensions/duplicate/errors/syntax-vnd.sieve
similarity index 100%
rename from tests/extensions/vnd.dovecot/duplicate/errors/syntax.sieve
rename to tests/extensions/duplicate/errors/syntax-vnd.sieve
diff --git a/tests/extensions/duplicate/errors/syntax.sieve b/tests/extensions/duplicate/errors/syntax.sieve
new file mode 100644
index 000000000..a561cfb32
--- /dev/null
+++ b/tests/extensions/duplicate/errors/syntax.sieve
@@ -0,0 +1,54 @@
+require "duplicate";
+
+# Used as a command
+duplicate;
+
+# Used with no argument (not an error)
+if duplicate {}
+
+# Used with string argument
+if duplicate "frop" { }
+
+# Used with numner argument
+if duplicate 23423 { }
+
+# Used with numer argument
+if duplicate ["frop"] { }
+
+# Used with unknown tag
+if duplicate :test "frop" { }
+
+# Bad :header parameter
+if duplicate :header 23 {}
+
+# Bad :uniqueid parameter
+if duplicate :uniqueid 23 {}
+
+# Bad :handle parameter
+if duplicate :handle ["a", "b", "c"] {}
+
+# Bad seconds parameter
+if duplicate :seconds "a" {}
+
+# Missing :header parameter
+if duplicate :header {}
+
+# Missing :uniqueid parameter
+if duplicate :uniqueid {}
+
+# Missing :handle parameter
+if duplicate :handle {}
+
+# Missing seconds parameter
+if duplicate :seconds {}
+
+# :last with a parameter
+if duplicate :last "frop" {}
+
+# :last as :seconds parameter
+if duplicate :seconds :last {}
+
+# Conflicting tags
+if duplicate :header "X-Frop" :uniqueid "FROP!" { }
+
+
diff --git a/tests/extensions/vnd.dovecot/duplicate/execute.svtest b/tests/extensions/duplicate/execute-vnd.svtest
similarity index 100%
rename from tests/extensions/vnd.dovecot/duplicate/execute.svtest
rename to tests/extensions/duplicate/execute-vnd.svtest
diff --git a/tests/extensions/duplicate/execute.svtest b/tests/extensions/duplicate/execute.svtest
new file mode 100644
index 000000000..9e060ffbb
--- /dev/null
+++ b/tests/extensions/duplicate/execute.svtest
@@ -0,0 +1,41 @@
+require "vnd.dovecot.testsuite";
+require "duplicate";
+
+# Simple execution tests; no duplicate verification can be tested yet.
+test "Run" {
+	if duplicate {
+		test_fail "test erroneously reported a duplicate";
+	}
+
+	if duplicate :handle "handle" {
+		test_fail "test with :handle erroneously reported a duplicate";
+	}
+
+	if duplicate {
+		test_fail "test erroneously reported a duplicate";
+	}
+
+	if duplicate :handle "handle" {
+		test_fail "test with :handle erroneously reported a duplicate";
+	}
+
+	if duplicate :header "X-frop" {
+		test_fail "test with :header erroneously reported a duplicate";
+	}
+
+	if duplicate :uniqueid "FROP!" {
+		test_fail "test with :uniqueid erroneously reported a duplicate";
+	}
+
+	if duplicate :seconds 90 {
+		test_fail "test with :seconds erroneously reported a duplicate";
+	}
+
+	if duplicate :seconds 90 :last {
+		test_fail "test with :seconds :last erroneously reported a duplicate";
+	}
+
+	if duplicate :last {
+		test_fail "test with :seconds :last erroneously reported a duplicate";
+	}
+}
diff --git a/tests/extensions/vnd.dovecot/duplicate/errors.svtest b/tests/extensions/vnd.dovecot/duplicate/errors.svtest
deleted file mode 100644
index 769ca64d9..000000000
--- a/tests/extensions/vnd.dovecot/duplicate/errors.svtest
+++ /dev/null
@@ -1,18 +0,0 @@
-require "vnd.dovecot.testsuite";
-
-require "relational";
-require "comparator-i;ascii-numeric";
-
-/*
- * Invalid syntax
- */
-
-test "Invalid Syntax" {
-        if test_script_compile "errors/syntax.sieve" {
-                test_fail "compile should have failed";
-        }
-
-        if not test_error :count "eq" :comparator "i;ascii-numeric" "5" {
-                test_fail "wrong number of errors reported";
-        }
-}
-- 
GitLab