From cb2f60e31b35d9cb1c1f43a3ee87a440bcd692ee Mon Sep 17 00:00:00 2001
From: Stephan Bosch <stephan@rename-it.nl>
Date: Sun, 27 Mar 2016 23:48:44 +0200
Subject: [PATCH] lib-sieve: Implemented the dovecot-specific
 "vnd.dovecot.report".

It allows sending feedback report (RFC 5965) messages.
---
 Makefile.am                                   |   2 +
 configure.ac                                  |   1 +
 doc/rfc/spec-bosch-sieve-report.txt           | 392 +++++++++++
 doc/rfc/xml/reference.IMAPSIEVE.xml           |  12 +
 doc/rfc/xml/reference.MARF.xml                |  14 +
 doc/rfc/xml/reference.RFC.2045.xml            |  13 +
 doc/rfc/xml/reference.RFC.3834.xml            |  12 +
 doc/rfc/xml/spec-bosch-sieve-report.xml       | 299 ++++++++
 src/lib-sieve/Makefile.am                     |   1 +
 src/lib-sieve/plugins/vnd.dovecot/Makefile.am |   2 +-
 .../plugins/vnd.dovecot/report/Makefile.am    |  17 +
 .../plugins/vnd.dovecot/report/cmd-report.c   | 654 ++++++++++++++++++
 .../report/ext-vnd-report-common.c            |  35 +
 .../report/ext-vnd-report-common.h            |  32 +
 .../vnd.dovecot/report/ext-vnd-report.c       |  51 ++
 src/lib-sieve/sieve-extensions.c              |   3 +-
 .../vnd.dovecot/report/errors.svtest          |  13 +
 .../vnd.dovecot/report/errors/syntax.sieve    |  28 +
 .../vnd.dovecot/report/execute.svtest         |  70 ++
 19 files changed, 1649 insertions(+), 2 deletions(-)
 create mode 100644 doc/rfc/spec-bosch-sieve-report.txt
 create mode 100644 doc/rfc/xml/reference.IMAPSIEVE.xml
 create mode 100644 doc/rfc/xml/reference.MARF.xml
 create mode 100644 doc/rfc/xml/reference.RFC.2045.xml
 create mode 100644 doc/rfc/xml/reference.RFC.3834.xml
 create mode 100644 doc/rfc/xml/spec-bosch-sieve-report.xml
 create mode 100644 src/lib-sieve/plugins/vnd.dovecot/report/Makefile.am
 create mode 100644 src/lib-sieve/plugins/vnd.dovecot/report/cmd-report.c
 create mode 100644 src/lib-sieve/plugins/vnd.dovecot/report/ext-vnd-report-common.c
 create mode 100644 src/lib-sieve/plugins/vnd.dovecot/report/ext-vnd-report-common.h
 create mode 100644 src/lib-sieve/plugins/vnd.dovecot/report/ext-vnd-report.c
 create mode 100644 tests/extensions/vnd.dovecot/report/errors.svtest
 create mode 100644 tests/extensions/vnd.dovecot/report/errors/syntax.sieve
 create mode 100644 tests/extensions/vnd.dovecot/report/execute.svtest

diff --git a/Makefile.am b/Makefile.am
index 9ca86784a..bf968e1d0 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -179,6 +179,8 @@ test_cases = \
 	tests/extensions/vnd.dovecot/debug/execute.svtest \
 	tests/extensions/vnd.dovecot/environment/basic.svtest \
 	tests/extensions/vnd.dovecot/environment/variables.svtest \
+	tests/extensions/vnd.dovecot/report/errors.svtest \
+	tests/extensions/vnd.dovecot/report/execute.svtest \
 	tests/deprecated/notify/basic.svtest \
 	tests/deprecated/notify/mailto.svtest \
 	tests/deprecated/notify/errors.svtest \
diff --git a/configure.ac b/configure.ac
index fcd3854bb..02d79665c 100644
--- a/configure.ac
+++ b/configure.ac
@@ -217,6 +217,7 @@ src/lib-sieve/plugins/mime/Makefile
 src/lib-sieve/plugins/vnd.dovecot/Makefile
 src/lib-sieve/plugins/vnd.dovecot/debug/Makefile
 src/lib-sieve/plugins/vnd.dovecot/environment/Makefile
+src/lib-sieve/plugins/vnd.dovecot/report/Makefile
 src/lib-sieve-tool/Makefile
 src/lib-managesieve/Makefile
 src/plugins/Makefile
diff --git a/doc/rfc/spec-bosch-sieve-report.txt b/doc/rfc/spec-bosch-sieve-report.txt
new file mode 100644
index 000000000..c2585562b
--- /dev/null
+++ b/doc/rfc/spec-bosch-sieve-report.txt
@@ -0,0 +1,392 @@
+
+
+
+
+                                                                S. Bosch
+
+                                                          March 19, 2016
+
+
+         Sieve Email Filtering: Sending Abuse Feedback Reports
+                        spec-bosch-sieve-report
+
+Abstract
+
+   This document defines a new vendor-defined action command "report"
+   for the "Sieve" email filtering language.  It provides the means to
+   send Messaging Abuse Reporting Format (MARF) reports (RFC 5965).
+   This format is intended for communications regarding email abuse and
+   related issues.  The "report" command allows (partially) automating
+   the exchange of these reports, which is particularly useful when the
+   Sieve script is executed for an IMAP event (RFC 6785) that is
+   triggered by direct user action.
+
+Table of Contents
+
+   1.  Introduction  . . . . . . . . . . . . . . . . . . . . . . . .   1
+   2.  Conventions Used in This Document . . . . . . . . . . . . . .   2
+   3.  Action "report" . . . . . . . . . . . . . . . . . . . . . . .   2
+   4.  Sieve Capability Strings  . . . . . . . . . . . . . . . . . .   3
+   5.  Example . . . . . . . . . . . . . . . . . . . . . . . . . . .   3
+   6.  Security Considerations . . . . . . . . . . . . . . . . . . .   6
+   7.  References  . . . . . . . . . . . . . . . . . . . . . . . . .   6
+     7.1.  Normative References  . . . . . . . . . . . . . . . . . .   6
+     7.2.  Informative References  . . . . . . . . . . . . . . . . .   6
+   Author's Address  . . . . . . . . . . . . . . . . . . . . . . . .   7
+
+1.  Introduction
+
+   This is an extension to the Sieve filtering language defined by RFC
+   5228 [SIEVE].  It provides the means to send Messaging Abuse
+   Reporting Format (MARF) [MARF] report messages.  This format is
+   intended to exchange reports on abuse and related issues.
+
+   This extension adds the "report" action command, which sends a MARF
+   report to the indicated recipient address.  The author of the Sieve
+   script must also specify a human-readable messsage and the type of
+   the report.  The recipient is usually an automated system that will
+   take the appropriate action based on the report.
+
+   Normally, Sieve scripts are executed at message delivery.  In that
+   case, the "report" action would be of little use, since sending such
+   reports or performing the resulting actions is already part of the
+
+
+
+Bosch                  Expires September 20, 2016               [Page 1]
+
+                      Sieve: Sending Abuse Reports            March 2016
+
+
+   abuse detection software that commonly runs before the message is
+   even delivered.
+
+   Instead, the "report" action is mainly useful when used in Sieve
+   scripts that are run as triggered by an IMAP event [IMAPSIEVE].
+   Then, the Sieve script is executed as a direct result of user action
+   in most cases.  Certain user actions, such as moving or copying a
+   message into a particular mailbox, can then be defined to have a
+   special meaning.  For example, placing a message into the "Spam
+   Report" mailbox could mean that the user reports the message as
+   unsolicited bulk mail.  Conversely, placing a message in a mailbox
+   called "Ham Report" could mean that the user reports that the message
+   is erroneously classified as unsolicited bulk mail.  Using the
+   "report" action, the Sieve script that is run as a result of these
+   actions can convey the report in the standard MARF format to the
+   entity responsible for handling such reports.  This avoids the need
+   for the user's MUA software to support the MARF report format and the
+   need to configure where the reports are to be sent.
+
+   The "report" action is mainly intended to be used in Sieve scripts
+   that are controlled by the system administrator.  Also, 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 and GUI-based Sieve editors.
+
+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.  Action "report"
+
+   Usage: report [":headers_only"] <feedback-type: string>
+                 <message: string> <recipient: string>
+
+   The "report" action is used to send a Messaging Abuse Reporting
+   Format (MARF) [MARF] report to the supplied recipient address.  The
+   "report" action composes the MARF report (the "report message") based
+   on the provided arguments and the "triggering message" that Sieve is
+   currently evaluating.  The envelope sender address on the report
+   message is chosen by the sieve implementation.
+
+
+
+
+
+Bosch                  Expires September 20, 2016               [Page 2]
+
+                      Sieve: Sending Abuse Reports            March 2016
+
+
+   The "feedback-type" argument is used as the value for the "Feedback-
+   Type" field in the machine-readable "message/feedback-report" MIME
+   part of the MARF report message.  The "feedback-type" string value
+   must conform to the MIME "token" syntax [RFC2045].
+
+   The "message" argument is a UTF-8 [UTF-8] string specifying a human-
+   readable text explaining the nature of the report.  It is directly
+   used as the body of the first MIME part of the MARF report with
+   content type "text/plain".
+
+   By default, the MARF report includes the triggering message in its
+   entirety as its third MIME part.  When the ":headers_only" tag is
+   specified, it contains only a copy of the entire header block from
+   the triggering message.  In that case the MIME type of the third part
+   is "text/rfc822-headers", rather than "message/rfc822" [MARF].
+
+   Since the "report" action is not normally used at message delivery,
+   the report message is not sent as a result of a newly delivered
+   message.  In such cases, mail loops are very unlikely to occur.
+   However, for the uncommon case that the "report" action is used at
+   message delivery, implementations MUST always endeavor to avoid mail
+   loops.  To that end, implementations MUST always include an "Auto-
+   Submitted:" field [RFC3834] in the report message.  Furthermore,
+   implementations MUST NOT allow sending report messages to the mail
+   account running this Sieve script.
+
+   The "report" action is compatible with all other actions, and does
+   not affect the operation of other actions.  In particular, the
+   "report" action MUST NOT cancel the implicit keep.  Substitution of
+   variables [VARIABLES] is supported for all arguments.
+
+   Multiple executed "report" actions are allowed.  However, only one
+   report message is sent for each unique combination of feedback type
+   and report recipient.  Subsequent duplicate report actions are
+   ignored without error and only the human-readable message from the
+   first instance of the duplicate action is used in the report.
+
+4.  Sieve Capability Strings
+
+   A Sieve implementation that defines the "report" action command will
+   advertise the capability string "vnd.dovecot.report".
+
+5.  Example
+
+   In this example, Victor receives some unsolicited bulk email at his
+   account "victim@example.org" and his mail filter failed to recognize
+   it as such.  The message is delivered to his INBOX and looks as
+   follows:
+
+
+
+Bosch                  Expires September 20, 2016               [Page 3]
+
+                      Sieve: Sending Abuse Reports            March 2016
+
+
+       Return-Path: <spammer@example.com>
+       Received: from mail.example.com by mail.example.org
+           for <victim@example.org>; Wed, 17 Thu 2016 03:01:02 +0100
+       Message-ID: <1234567.89ABCDEF@example.com>
+       Date: Thu, 17 Mar 2016 2:59:19 +0100
+       From: "Steve Pammer" <spammer@example.com>
+       To: "Victor Timson" <victim@example.org>
+       Subject: Male enhancement products
+       X-Spam-Score: 3.3/5.0
+       X-Spam-Status: not spam
+
+       We have very interesting offers!
+
+   The administrator of Victor's email account has created a "Spam
+   Report" mailbox and has asked Victor to move any misclassified e-mail
+   into that mailbox.  The following administrator-controlled Sieve
+   script is linked to that mailbox:
+
+   require ["imapsieve", "environment", "vnd.dovecot.report"];
+
+   if allof(
+       environment "imap.mailbox" "Spam Report",
+       environment "imap.cause" "COPY",
+       header "x-spam-status" "not spam") {
+       report "abuse" "This spam message slipped through."
+       "spam-report@example.org";
+   }
+
+   As instructed, Victor moves the message into the "Spam Report"
+   mailbox and the following message is sent to "spam-
+   report@example.org":
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Bosch                  Expires September 20, 2016               [Page 4]
+
+                      Sieve: Sending Abuse Reports            March 2016
+
+
+       Message-ID: <1458401523-377250-0@example.org>
+       Date: Sat, 19 Mar 2016 16:32:03 +0100
+       From: Postmaster <postmaster@example.org>
+       To: <spam-report@example.org>
+       Subject: Report: Male enhancement products
+       Auto-Submitted: auto-generated (report)
+       MIME-Version: 1.0
+       Content-Type: multipart/report; report-type=feedback-report;
+           boundary="324/example.org"
+
+       This is a MIME-encapsulated message
+
+       --324/example.org
+       Content-Type: text/plain; charset=utf-8
+       Content-Transfer-Encoding: 8bit
+       Content-Disposition: inline
+
+       This spam message slipped through.
+
+       --324/example.org
+       Content-Type: message/feedback-report
+
+       Version: 1
+       Feedback-Type: abuse
+       User-Agent: Exemplimail/1.3.15.rc1
+       Original-Mail-From: <spammer@example.com>
+
+       --324/example.org
+       Content-Type: message/rfc822
+       Content-Disposition: attachment
+
+       Return-Path: <spammer@example.com>
+       Received: from mail.example.com by mail.example.org
+         for <victim@example.org>; Wed, 17 Thu 2016 03:01:02 +0100
+       Message-ID: <1234567.89ABCDEF@example.com>
+       Date: Thu, 17 Mar 2016 2:59:19 +0100
+       From: "Steve Pammer" <spammer@example.com>
+       To: "Victor Timson" <victim@example.org>
+       Subject: Male enhancement products
+       X-Spam-Score: 3.3/5.0
+       X-Spam-Status: not spam
+
+       We have very interesting offers!
+
+       --324/example.org--
+
+
+
+
+
+
+Bosch                  Expires September 20, 2016               [Page 5]
+
+                      Sieve: Sending Abuse Reports            March 2016
+
+
+6.  Security Considerations
+
+   When used in IMAP context, the "report" command is executed for each
+   action and message that matches the criteria defined by the Sieve
+   script.  If the user performs matching activities with a large volume
+   of messages, a great many reports can be sent at one time.
+   Implementations and deployments MUST be designed to cope with such
+   incidents, either by limiting the number of reports or by otherwise
+   mitigating their collective impact.
+
+7.  References
+
+7.1.  Normative References
+
+   [KEYWORDS]
+              Bradner, S., "Key words for use in RFCs to Indicate
+              Requirement Levels", BCP 14, RFC 2119, March 1997.
+
+   [MARF]     Shafranovich, Y., Levine, J., and M. Kucherawy, "An
+              Extensible Format for Email Feedback Reports", RFC 5965,
+              DOI 10.17487/RFC5965, August 2010,
+              <http://www.rfc-editor.org/info/rfc5965>.
+
+   [RFC2045]  Freed, N. and N. Borenstein, "Multipurpose Internet Mail
+              Extensions (MIME) Part One: Format of Internet Message
+              Bodies", RFC 2045, DOI 10.17487/RFC2045, November 1996,
+              <http://www.rfc-editor.org/info/rfc2045>.
+
+   [RFC3834]  Moore, K., "Recommendations for Automatic Responses to
+              Electronic Mail", RFC 3834, DOI 10.17487/RFC3834, August
+              2004, <http://www.rfc-editor.org/info/rfc3834>.
+
+   [SIEVE]    Guenther, P. and T. Showalter, "Sieve: An Email Filtering
+              Language", RFC 5228, January 2008.
+
+   [UTF-8]    Yergeau, F., "UTF-8, a transformation format of ISO
+              10646", STD 63, RFC 3629, November 2003.
+
+   [VARIABLES]
+              Homme, K., "Sieve Email Filtering: Variables Extension",
+              RFC 5229, January 2008.
+
+7.2.  Informative References
+
+   [IMAPSIEVE]
+              Leiba, B., "Support for Internet Message Access Protocol
+              (IMAP) Events in Sieve", RFC 6785, DOI 10.17487/RFC6785,
+              November 2012, <http://www.rfc-editor.org/info/rfc6785>.
+
+
+
+Bosch                  Expires September 20, 2016               [Page 6]
+
+                      Sieve: Sending Abuse Reports            March 2016
+
+
+Author's Address
+
+   Stephan Bosch
+   Enschede
+   NL
+
+   Email: stephan@rename-it.nl
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Bosch                  Expires September 20, 2016               [Page 7]
diff --git a/doc/rfc/xml/reference.IMAPSIEVE.xml b/doc/rfc/xml/reference.IMAPSIEVE.xml
new file mode 100644
index 000000000..5567c8741
--- /dev/null
+++ b/doc/rfc/xml/reference.IMAPSIEVE.xml
@@ -0,0 +1,12 @@
+<?xml version='1.0' encoding='UTF-8'?>
+
+<reference  anchor='IMAPSIEVE' target='http://www.rfc-editor.org/info/rfc6785'>
+<front>
+<title>Support for Internet Message Access Protocol (IMAP) Events in Sieve</title>
+<author initials='B.' surname='Leiba' fullname='B. Leiba'><organization /></author>
+<date year='2012' month='November' />
+<abstract><t>Sieve defines an email filtering language that can, in principle, plug into any point in the processing of an email message.  As defined in the base specification, it plugs into mail delivery.  This document defines how Sieve can plug into points in IMAP where messages are created or changed, adding the option of user-defined or installation-defined filtering (or, with Sieve extensions, features such as notifications).  Because this requires future Sieve extensions to specify their interactions with this one, this document updates the base Sieve specification, RFC 5228.  [STANDARDS-TRACK]</t></abstract>
+</front>
+<seriesInfo name='RFC' value='6785'/>
+<seriesInfo name='DOI' value='10.17487/RFC6785'/>
+</reference>
diff --git a/doc/rfc/xml/reference.MARF.xml b/doc/rfc/xml/reference.MARF.xml
new file mode 100644
index 000000000..e9bd3b98a
--- /dev/null
+++ b/doc/rfc/xml/reference.MARF.xml
@@ -0,0 +1,14 @@
+<?xml version='1.0' encoding='UTF-8'?>
+
+<reference  anchor='MARF' target='http://www.rfc-editor.org/info/rfc5965'>
+<front>
+<title>An Extensible Format for Email Feedback Reports</title>
+<author initials='Y.' surname='Shafranovich' fullname='Y. Shafranovich'><organization /></author>
+<author initials='J.' surname='Levine' fullname='J. Levine'><organization /></author>
+<author initials='M.' surname='Kucherawy' fullname='M. Kucherawy'><organization /></author>
+<date year='2010' month='August' />
+<abstract><t>This document defines an extensible format and MIME type that may be used by mail operators to report feedback about received email to other parties.  This format is intended as a machine-readable replacement for various existing report formats currently used in Internet email.  [STANDARDS-TRACK]</t></abstract>
+</front>
+<seriesInfo name='RFC' value='5965'/>
+<seriesInfo name='DOI' value='10.17487/RFC5965'/>
+</reference>
diff --git a/doc/rfc/xml/reference.RFC.2045.xml b/doc/rfc/xml/reference.RFC.2045.xml
new file mode 100644
index 000000000..41ea5761c
--- /dev/null
+++ b/doc/rfc/xml/reference.RFC.2045.xml
@@ -0,0 +1,13 @@
+<?xml version='1.0' encoding='UTF-8'?>
+
+<reference  anchor='RFC2045' target='http://www.rfc-editor.org/info/rfc2045'>
+<front>
+<title>Multipurpose Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies</title>
+<author initials='N.' surname='Freed' fullname='N. Freed'><organization /></author>
+<author initials='N.' surname='Borenstein' fullname='N. Borenstein'><organization /></author>
+<date year='1996' month='November' />
+<abstract><t>This initial document specifies the various headers used to describe the structure of MIME messages.  [STANDARDS-TRACK]</t></abstract>
+</front>
+<seriesInfo name='RFC' value='2045'/>
+<seriesInfo name='DOI' value='10.17487/RFC2045'/>
+</reference>
diff --git a/doc/rfc/xml/reference.RFC.3834.xml b/doc/rfc/xml/reference.RFC.3834.xml
new file mode 100644
index 000000000..8a8463f61
--- /dev/null
+++ b/doc/rfc/xml/reference.RFC.3834.xml
@@ -0,0 +1,12 @@
+<?xml version='1.0' encoding='UTF-8'?>
+
+<reference  anchor='RFC3834' target='http://www.rfc-editor.org/info/rfc3834'>
+<front>
+<title>Recommendations for Automatic Responses to Electronic Mail</title>
+<author initials='K.' surname='Moore' fullname='K. Moore'><organization /></author>
+<date year='2004' month='August' />
+<abstract><t>This memo makes recommendations for software that automatically responds to incoming electronic mail messages, including &quot;out of the office&quot; or &quot;vacation&quot; response generators, mail filtering software, email-based information services, and other automatic responders.  The purpose of these recommendations is to discourage undesirable behavior which is caused or aggravated by such software, to encourage uniform behavior (where appropriate) among automatic mail responders, and to clear up some sources of confusion among implementors of automatic email responders.  [STANDARDS-TRACK]</t></abstract>
+</front>
+<seriesInfo name='RFC' value='3834'/>
+<seriesInfo name='DOI' value='10.17487/RFC3834'/>
+</reference>
diff --git a/doc/rfc/xml/spec-bosch-sieve-report.xml b/doc/rfc/xml/spec-bosch-sieve-report.xml
new file mode 100644
index 000000000..2e4733301
--- /dev/null
+++ b/doc/rfc/xml/spec-bosch-sieve-report.xml
@@ -0,0 +1,299 @@
+<?xml version="1.0" encoding="US-ASCII"?>
+
+<!DOCTYPE rfc SYSTEM "rfc2629.dtd">
+
+<?xml-stylesheet type='text/xsl' href='rfc2629.xslt' ?>
+<!-- used by XSLT processors -->
+<?rfc strict="yes" ?>
+<?rfc toc="yes"?>
+<?rfc tocdepth="4"?>
+<?rfc symrefs="yes"?>
+<?rfc sortrefs="yes" ?>
+<?rfc compact="yes" ?>
+<?rfc subcompact="no" ?>
+<?rfc private="Pigeonhole Project" ?>
+
+<rfc category="info" docName="spec-bosch-sieve-report">
+<!-- ***** FRONT MATTER ***** -->
+
+<front>
+<title abbrev="Sieve: Sending Abuse Reports">
+Sieve Email Filtering: Sending Abuse Feedback Reports
+</title>
+
+<author fullname="Stephan Bosch" initials="S." surname="Bosch">
+  <organization/>
+  <address>
+    <postal>
+      <street></street>
+      <city>Enschede</city>
+      <country>NL</country>
+    </postal>
+    <email>stephan@rename-it.nl</email>
+  </address>
+</author>
+
+<date/>
+
+<area>General</area>
+<workgroup>Pigeonhole Project</workgroup>
+<keyword>sieve</keyword>
+<keyword>abuse</keyword>
+<keyword>feedback</keyword>
+<keyword>report</keyword>
+
+<abstract>
+<t>
+This document defines a new vendor-defined action command "report" for the
+"Sieve" email filtering language. It provides the means to send
+Messaging Abuse Reporting Format (MARF) reports (RFC 5965). This format is
+intended for communications regarding email abuse and related issues. The
+"report" command allows (partially) automating the exchange of these reports,
+which is particularly useful when the Sieve script is executed for an IMAP event
+(RFC 6785) that is triggered by direct user action.
+</t>
+</abstract>
+</front>
+
+<middle>
+
+<section title="Introduction">
+<t>This is an extension to the Sieve filtering language defined by
+<xref target="SIEVE">RFC 5228</xref>. It provides the means to send
+<xref target="MARF">Messaging Abuse Reporting Format (MARF)</xref> report
+messages. This format is intended to exchange reports on abuse and related
+issues.</t>
+
+<t>This extension adds the "report" action command, which sends a MARF report to
+the indicated recipient address. The author of the Sieve script must also
+specify a human-readable messsage and the type of the report. The recipient is
+usually an automated system that will take the appropriate action based on the
+report.
+</t>
+
+<t>Normally, Sieve scripts are executed at message delivery. In that case,
+the "report" action would be of little use, since sending such reports or
+performing the resulting actions is already part of the abuse detection
+software that commonly runs before the message is even delivered.</t>
+
+<t>Instead, the "report" action is mainly useful when used in Sieve scripts
+that are run as triggered by an IMAP event <xref target="IMAPSIEVE"/>. Then, the
+Sieve script is executed as a direct result of user action in most cases.
+Certain user actions, such as moving or copying a message into a particular
+mailbox, can then be defined to have a special meaning. For example, placing a
+message into the "Spam Report" mailbox could mean that the user reports the
+message as unsolicited bulk mail. Conversely, placing a message in a mailbox
+called "Ham Report" could mean that the user reports that the message is
+erroneously classified as unsolicited bulk mail. Using the "report" action,
+the Sieve script that is run as a result of these actions can convey the report
+in the standard MARF format to the entity responsible for handling such
+reports. This avoids the need for the user's MUA software to support the
+MARF report format and the need to configure where the reports are to be sent.
+</t>
+
+<t>The "report" action is mainly intended to be used in Sieve scripts that
+are controlled by the system administrator. Also, 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 and GUI-based
+Sieve editors.</t>
+</section>
+
+<section title="Conventions Used in This Document">
+<t>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 <xref target="KEYWORDS"/>.</t>
+<t>Conventions for notations are as in <xref target="SIEVE"/> Section 1.1,
+including use of the "Usage:" label for the definition of action and tagged
+arguments syntax.</t>
+</section>
+
+<section title="Action &quot;report&quot;">
+<?rfc needLines="3" ?>
+<figure>
+<artwork><![CDATA[
+Usage: report [":headers_only"] <feedback-type: string>
+              <message: string> <recipient: string>
+]]></artwork>
+</figure>
+
+<t>The "report" action is used to send a <xref target="MARF">Messaging Abuse
+Reporting Format (MARF)</xref> report to the supplied recipient address.
+The "report" action composes the MARF report (the "report message") based on
+the provided arguments and the "triggering message" that Sieve is currently
+evaluating. The envelope sender address on the report message is chosen by the
+sieve implementation.</t>
+
+<t>The "feedback-type" argument is used as the value for the "Feedback-Type"
+field in the machine-readable "message/feedback-report" MIME part of the MARF
+report message. The "feedback-type" string value must conform to the
+<xref target="RFC2045">MIME "token" syntax</xref>.</t>
+
+<t>The "message" argument is a <xref target="UTF-8">UTF-8</xref> string
+specifying a human-readable text explaining the nature of the report. It is
+directly used as the body of the first MIME part of the MARF report with content
+type "text/plain".</t>
+
+<t>By default, the MARF report includes the triggering message in its entirety
+as its third MIME part. When the ":headers_only" tag is specified, it contains
+only a copy of the entire header block from the triggering message. In that case
+the MIME type of the third part is "text/rfc822-headers", rather than
+"message/rfc822" <xref target="MARF"/>.</t>
+
+<t>Since the "report" action is not normally used at message delivery, the
+report message is not sent as a result of a newly delivered message. In such
+cases, mail loops are very unlikely to occur. However, for the uncommon case
+that the "report" action is used at message delivery, implementations MUST
+always endeavor to avoid mail loops. To that end, implementations MUST always
+include an "Auto-Submitted:" field <xref target="RFC3834"/> in the report
+message. Furthermore, implementations MUST NOT allow sending report messages to
+the mail account running this Sieve script.
+</t>
+
+<t>The "report" action is compatible with all other actions, and does not affect
+the operation of other actions. In particular, the "report" action MUST NOT
+cancel the implicit keep. Substitution of
+<xref target="VARIABLES">variables</xref> is supported for all arguments.</t>
+
+<t>Multiple executed "report" actions are allowed. However, only one report
+message is sent for each unique combination of feedback type and report
+recipient. Subsequent duplicate report actions are ignored without error and
+only the human-readable message from the first instance of the duplicate action
+is used in the report.</t>
+</section>
+
+<section title="Sieve Capability Strings">
+<t>A Sieve implementation that defines the "report" action command
+will advertise the capability string "vnd.dovecot.report".
+</t>
+</section>
+
+<section title="Example">
+
+<t>In this example, Victor receives some unsolicited bulk email at his
+account "victim@example.org" and his mail filter failed to recognize it as such.
+The message is delivered to his INBOX and looks as follows: </t>
+
+<?rfc needLines="13" ?>
+<figure>
+<artwork align="center"><![CDATA[
+Return-Path: <spammer@example.com>
+Received: from mail.example.com by mail.example.org
+    for <victim@example.org>; Wed, 17 Thu 2016 03:01:02 +0100
+Message-ID: <1234567.89ABCDEF@example.com>
+Date: Thu, 17 Mar 2016 2:59:19 +0100
+From: "Steve Pammer" <spammer@example.com>
+To: "Victor Timson" <victim@example.org>
+Subject: Male enhancement products
+X-Spam-Score: 3.3/5.0
+X-Spam-Status: not spam
+
+We have very interesting offers!
+]]></artwork>
+</figure>
+
+<t>The administrator of Victor's email account has created a "Spam Report"
+mailbox and has asked Victor to move any misclassified e-mail into that
+mailbox. The following administrator-controlled Sieve script is linked to that
+mailbox:</t>
+
+<?rfc needLines="10" ?>
+<figure>
+<artwork><![CDATA[
+require ["imapsieve", "environment", "vnd.dovecot.report"];
+
+if allof(
+	environment "imap.mailbox" "Spam Report",
+	environment "imap.cause" "COPY",
+	header "x-spam-status" "not spam") {
+	report "abuse" "This spam message slipped through."
+	"spam-report@example.org";
+}
+]]></artwork>
+</figure>
+
+<t>As instructed, Victor moves the message into the "Spam Report" mailbox and
+the following message is sent to "spam-report@example.org":</t>
+
+<figure>
+<artwork align="center"><![CDATA[
+Message-ID: <1458401523-377250-0@example.org>
+Date: Sat, 19 Mar 2016 16:32:03 +0100
+From: Postmaster <postmaster@example.org>
+To: <spam-report@example.org>
+Subject: Report: Male enhancement products
+Auto-Submitted: auto-generated (report)
+MIME-Version: 1.0
+Content-Type: multipart/report; report-type=feedback-report;
+	boundary="324/example.org"
+
+This is a MIME-encapsulated message
+
+--324/example.org
+Content-Type: text/plain; charset=utf-8
+Content-Transfer-Encoding: 8bit
+Content-Disposition: inline
+
+This spam message slipped through.
+
+--324/example.org
+Content-Type: message/feedback-report
+
+Version: 1
+Feedback-Type: abuse
+User-Agent: Exemplimail/1.3.15.rc1
+Original-Mail-From: <spammer@example.com>
+
+--324/example.org
+Content-Type: message/rfc822
+Content-Disposition: attachment
+
+Return-Path: <spammer@example.com>
+Received: from mail.example.com by mail.example.org
+  for <victim@example.org>; Wed, 17 Thu 2016 03:01:02 +0100
+Message-ID: <1234567.89ABCDEF@example.com>
+Date: Thu, 17 Mar 2016 2:59:19 +0100
+From: "Steve Pammer" <spammer@example.com>
+To: "Victor Timson" <victim@example.org>
+Subject: Male enhancement products
+X-Spam-Score: 3.3/5.0
+X-Spam-Status: not spam
+
+We have very interesting offers!
+
+--324/example.org--
+]]></artwork>
+</figure>
+
+</section>
+
+<section anchor="Security" title="Security Considerations">
+<t>When used in IMAP context, the "report" command is executed for each
+action and message that matches the criteria defined by the Sieve script. If
+the user performs matching activities with a large volume of messages, a
+great many reports can be sent at one time. Implementations and deployments MUST
+be designed to cope with such incidents, either by limiting the number of
+reports or by otherwise mitigating their collective impact.</t>
+</section>
+</middle>
+
+<!--  *****BACK MATTER ***** -->
+
+<back>
+<!-- References split into informative and normative -->
+
+<references title="Normative References">
+  <?rfc include="reference.KEYWORDS.xml"?>
+  <?rfc include="reference.MARF.xml"?>
+  <?rfc include="reference.RFC.2045.xml"?>
+  <?rfc include="reference.RFC.3834.xml"?>
+  <?rfc include="reference.SIEVE.xml"?>
+  <?rfc include="reference.VARIABLES.xml"?>
+  <?rfc include="reference.UTF-8.xml"?>
+</references>
+
+<references title="Informative References">
+	<?rfc include="reference.IMAPSIEVE.xml"?>
+</references>
+</back>
+
+</rfc>
diff --git a/src/lib-sieve/Makefile.am b/src/lib-sieve/Makefile.am
index 99ca22bb2..a998ae67b 100644
--- a/src/lib-sieve/Makefile.am
+++ b/src/lib-sieve/Makefile.am
@@ -80,6 +80,7 @@ plugins = \
 	$(extdir)/mime/libsieve_ext_mime.la \
 	$(extdir)/vnd.dovecot/debug/libsieve_ext_debug.la \
 	$(extdir)/vnd.dovecot/environment/libsieve_ext_vnd_environment.la \
+	$(extdir)/vnd.dovecot/report/libsieve_ext_vnd_report.la \
 	$(unfinished_plugins)
 
 libdovecot_sieve_la_DEPENDENCIES = \
diff --git a/src/lib-sieve/plugins/vnd.dovecot/Makefile.am b/src/lib-sieve/plugins/vnd.dovecot/Makefile.am
index 65d394e41..f409e2b69 100644
--- a/src/lib-sieve/plugins/vnd.dovecot/Makefile.am
+++ b/src/lib-sieve/plugins/vnd.dovecot/Makefile.am
@@ -1,2 +1,2 @@
-SUBDIRS = debug environment
+SUBDIRS = debug environment report
 
diff --git a/src/lib-sieve/plugins/vnd.dovecot/report/Makefile.am b/src/lib-sieve/plugins/vnd.dovecot/report/Makefile.am
new file mode 100644
index 000000000..599765f0a
--- /dev/null
+++ b/src/lib-sieve/plugins/vnd.dovecot/report/Makefile.am
@@ -0,0 +1,17 @@
+noinst_LTLIBRARIES = libsieve_ext_vnd_report.la
+
+AM_CPPFLAGS = \
+	-I$(srcdir)/../../.. \
+	-I$(srcdir)/../../../util \
+	$(LIBDOVECOT_INCLUDE)
+
+commands = \
+	cmd-report.c
+
+libsieve_ext_vnd_report_la_SOURCES = \
+	ext-vnd-report.c \
+	ext-vnd-report-common.c \
+	$(commands)
+
+noinst_HEADERS = \
+	ext-vnd-report-common.h
diff --git a/src/lib-sieve/plugins/vnd.dovecot/report/cmd-report.c b/src/lib-sieve/plugins/vnd.dovecot/report/cmd-report.c
new file mode 100644
index 000000000..d7c20bcf5
--- /dev/null
+++ b/src/lib-sieve/plugins/vnd.dovecot/report/cmd-report.c
@@ -0,0 +1,654 @@
+/* Copyright (c) 2016 Pigeonhole authors, see the included COPYING file
+ */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "ioloop.h"
+#include "hostpid.h"
+#include "str-sanitize.h"
+#include "istream.h"
+#include "ostream.h"
+#include "message-date.h"
+#include "message-size.h"
+#include "mail-storage.h"
+
+#include "rfc2822.h"
+
+#include "sieve-common.h"
+#include "sieve-stringlist.h"
+#include "sieve-code.h"
+#include "sieve-address.h"
+#include "sieve-extensions.h"
+#include "sieve-commands.h"
+#include "sieve-message.h"
+#include "sieve-actions.h"
+#include "sieve-validator.h"
+#include "sieve-generator.h"
+#include "sieve-interpreter.h"
+#include "sieve-dump.h"
+#include "sieve-result.h"
+#include "sieve-address.h"
+#include "sieve-message.h"
+#include "sieve-smtp.h"
+
+#include "ext-vnd-report-common.h"
+
+#include <ctype.h>
+
+/* Report command
+ *
+ * Syntax:
+ *    report [:headers_only] <feedback-type: string>
+ *           <message: string> <address: string>
+ *
+ */
+
+static bool cmd_report_registered
+	(struct sieve_validator *valdtr, const struct sieve_extension *ext,
+		struct sieve_command_registration *cmd_reg);
+static bool cmd_report_validate
+	(struct sieve_validator *valdtr, struct sieve_command *cmd);
+static bool cmd_report_generate
+	(const struct sieve_codegen_env *cgenv,
+		struct sieve_command *ctx);
+
+const struct sieve_command_def cmd_report = {
+	.identifier = "report",
+	.type = SCT_COMMAND,
+	.positional_args = 3,
+	.subtests = 0,
+	.block_allowed = FALSE,
+	.block_required = FALSE,
+	.registered = cmd_report_registered,
+	.validate = cmd_report_validate,
+	.generate = cmd_report_generate
+};
+
+/*
+ * Tagged arguments
+ */
+
+static const struct sieve_argument_def report_headers_only_tag = {
+	.identifier = "headers_only"
+};
+
+/*
+ * Report operation
+ */
+
+static bool cmd_report_operation_dump
+	(const struct sieve_dumptime_env *denv, sieve_size_t *address);
+static int cmd_report_operation_execute
+	(const struct sieve_runtime_env *renv, sieve_size_t *address);
+
+const struct sieve_operation_def report_operation = {
+	.mnemonic = "REPORT",
+	.ext_def = &vnd_report_extension,
+	.code = 0,
+	.dump = cmd_report_operation_dump,
+	.execute = cmd_report_operation_execute
+};
+
+/* Codes for optional operands */
+
+enum cmd_report_optional {
+  OPT_END,
+  OPT_HEADERS_ONLY
+};
+
+/*
+ * Report action
+ */
+
+/* Forward declarations */
+
+static int act_report_check_duplicate
+	(const struct sieve_runtime_env *renv,
+		const struct sieve_action *act,
+		const struct sieve_action *act_other);
+static void act_report_print
+	(const struct sieve_action *action, const struct sieve_result_print_env *rpenv,
+		bool *keep);
+static int act_report_commit
+	(const struct sieve_action *action,	const struct sieve_action_exec_env *aenv,
+		void *tr_context, bool *keep);
+
+/* Action object */
+
+const struct sieve_action_def act_report = {
+	.name = "report",
+	.check_duplicate = act_report_check_duplicate,
+	.print = act_report_print,
+	.commit = act_report_commit
+};
+
+/* Action data */
+
+struct act_report_data {
+	const char *feedback_type;
+	const char *message;
+	const char *to_address;
+	unsigned int headers_only:1;
+};
+
+/*
+ * Command registration
+ */
+
+static bool cmd_report_registered
+(struct sieve_validator *valdtr, const struct sieve_extension *ext,
+	struct sieve_command_registration *cmd_reg)
+{
+	sieve_validator_register_tag
+		(valdtr, cmd_reg, ext, &report_headers_only_tag, OPT_HEADERS_ONLY);
+
+	return TRUE;
+}
+
+/*
+ * Command validation
+ */
+
+static bool cmd_report_validate
+(struct sieve_validator *valdtr, struct sieve_command *cmd)
+{
+	struct sieve_ast_argument *arg = cmd->first_positional;
+
+	/* type */
+	if ( !sieve_validate_positional_argument
+		(valdtr, cmd, arg, "feedback-type", 1, SAAT_STRING) ) {
+		return FALSE;
+	}
+	if ( !sieve_validator_argument_activate
+		(valdtr, cmd, arg, FALSE) )
+		return FALSE;
+
+	if ( sieve_argument_is_string_literal(arg) ) {
+		string_t *fbtype = sieve_ast_argument_str(arg);
+		const char *feedback_type;
+
+		T_BEGIN {
+			/* Check feedback type */
+			feedback_type = ext_vnd_report_parse_feedback_type
+				(str_c(fbtype));
+
+			if ( feedback_type == NULL ) {
+				sieve_argument_validate_error(valdtr, arg,
+					"specified feedback type `%s' is invalid",
+					str_sanitize(str_c(fbtype),128));
+			}
+		} T_END;
+
+		if ( feedback_type == NULL )
+			return FALSE;
+	}
+	arg = sieve_ast_argument_next(arg);
+
+	/* message */
+	if ( !sieve_validate_positional_argument
+		(valdtr, cmd, arg, "message", 2, SAAT_STRING) ) {
+		return FALSE;
+	}
+	if ( !sieve_validator_argument_activate
+		(valdtr, cmd, arg, FALSE) )
+		return FALSE;
+	arg = sieve_ast_argument_next(arg);
+
+	/* address */
+	if ( !sieve_validate_positional_argument
+		(valdtr, cmd, arg, "address", 3, SAAT_STRING) ) {
+		return FALSE;
+	}
+	if ( !sieve_validator_argument_activate
+		(valdtr, cmd, arg, FALSE) )
+		return FALSE;
+
+	/* We can only assess the validity of the outgoing address when it is
+	 * a string literal. For runtime-generated strings this needs to be
+	 * done at runtime.
+	 */
+	if ( sieve_argument_is_string_literal(arg) ) {
+		string_t *address = sieve_ast_argument_str(arg);
+		const char *error;
+		const char *norm_address;
+
+		T_BEGIN {
+			/* Verify and normalize the address to 'local_part@domain' */
+			norm_address = sieve_address_normalize(address, &error);
+
+			if ( norm_address == NULL ) {
+				sieve_argument_validate_error(valdtr, arg,
+					"specified redirect address `%s' is invalid: %s",
+					str_sanitize(str_c(address),128), error);
+			} else {
+				/* Replace string literal in AST */
+				sieve_ast_argument_string_setc(arg, norm_address);
+			}
+		} T_END;
+
+		return ( norm_address != NULL );
+	}
+
+	return TRUE;
+}
+
+/*
+ * Code generation
+ */
+
+static bool cmd_report_generate
+(const struct sieve_codegen_env *cgenv, struct sieve_command *cmd)
+{
+	sieve_operation_emit(cgenv->sblock, cmd->ext, &report_operation);
+
+	/* Generate arguments */
+	return sieve_generate_arguments(cgenv, cmd, NULL);
+}
+
+/*
+ * Code dump
+ */
+
+static bool cmd_report_operation_dump
+(const struct sieve_dumptime_env *denv, sieve_size_t *address)
+{
+	int opt_code = 0;
+
+	sieve_code_dumpf(denv, "REPORT");
+	sieve_code_descend(denv);
+
+	/* Dump optional operands */
+	for (;;) {
+		int opt;
+		bool opok = TRUE;
+
+		if ( (opt=sieve_opr_optional_dump
+			(denv, address, &opt_code)) < 0 )
+			return FALSE;
+
+		if ( opt == 0 ) break;
+
+		switch ( opt_code ) {
+		case OPT_HEADERS_ONLY:
+			sieve_code_dumpf(denv, "headers_only");
+			break;
+		default:
+			return FALSE;
+		}
+
+		if ( !opok ) return FALSE;
+	}
+
+	return
+		sieve_opr_string_dump(denv, address, "feedback-type") &&
+		sieve_opr_string_dump(denv, address, "message") &&
+		sieve_opr_string_dump(denv, address, "address");
+}
+
+/*
+ * Code execution
+ */
+
+
+static int cmd_report_operation_execute
+(const struct sieve_runtime_env *renv, sieve_size_t *address)
+{
+	struct act_report_data *act;
+	string_t *fbtype, *message, *to_address;
+	const char *norm_address, *feedback_type, *error;
+	int opt_code = 0, ret = 0;
+	bool headers_only = FALSE;
+	pool_t pool;
+
+	/*
+	 * Read operands
+	 */
+
+	/* Optional operands */
+
+	for (;;) {
+		int opt;
+
+		if ( (opt=sieve_opr_optional_read(renv, address, &opt_code)) < 0 )
+			return SIEVE_EXEC_BIN_CORRUPT;
+
+		if ( opt == 0 ) break;
+
+		switch ( opt_code ) {
+		case OPT_HEADERS_ONLY:
+			headers_only = TRUE;
+			ret = SIEVE_EXEC_OK;
+			break;
+		default:
+			sieve_runtime_trace_error(renv, "unknown optional operand");
+			return SIEVE_EXEC_BIN_CORRUPT;
+		}
+
+		if ( ret <= 0 ) return ret;
+	}
+
+	/* Fixed operands */
+
+	if ( (ret=sieve_opr_string_read
+		(renv, address, "feedback-type", &fbtype)) <= 0 )
+		return ret;
+
+	if ( (ret=sieve_opr_string_read
+		(renv, address, "message", &message)) <= 0 )
+		return ret;
+
+	if ( (ret=sieve_opr_string_read
+		(renv, address, "address", &to_address)) <= 0 )
+		return ret;
+
+	/*
+	 * Perform operation
+	 */
+
+	/* Verify and trim feedback type */
+	feedback_type = ext_vnd_report_parse_feedback_type(str_c(fbtype));
+	if ( feedback_type == NULL ) {
+		sieve_runtime_error(renv, NULL,
+			"specified report feedback type `%s' is invalid",
+			str_sanitize(str_c(fbtype), 256));
+		return SIEVE_EXEC_FAILURE;
+	}
+
+	/* Verify and normalize the address to 'local_part@domain' */
+	norm_address = sieve_address_normalize(to_address, &error);
+	if ( norm_address == NULL ) {
+		sieve_runtime_error(renv, NULL,
+			"specified report address `%s' is invalid: %s",
+			str_sanitize(str_c(to_address), 256), error);
+		return SIEVE_EXEC_FAILURE;
+	}
+
+	/* Trace */
+	if ( sieve_runtime_trace_active(renv, SIEVE_TRLVL_ACTIONS) ) {
+		sieve_runtime_trace(renv, 0, "report action");
+		sieve_runtime_trace_descend(renv);
+		sieve_runtime_trace(renv, 0,
+			"report incoming message as `%s' to address `%s'",
+			str_sanitize(str_c(fbtype), 32),
+			str_sanitize(norm_address, 80));
+	}
+
+	/* Add report action to the result */
+
+	pool = sieve_result_pool(renv->result);
+	act = p_new(pool, struct act_report_data, 1);
+	act->headers_only = headers_only;
+	act->feedback_type = p_strdup(pool, feedback_type);
+	act->message = p_strdup(pool, str_c(message));
+	act->to_address = p_strdup(pool, norm_address);
+
+	if ( sieve_result_add_action(renv,
+		NULL, &act_report, NULL, (void *) act, 0, TRUE) < 0 )
+		return SIEVE_EXEC_FAILURE;
+
+	return SIEVE_EXEC_OK;
+}
+
+/*
+ * Action
+ */
+
+/* Runtime verification */
+
+static bool act_report_equals
+(const struct sieve_script_env *senv ATTR_UNUSED,
+	const struct sieve_action *act1,
+	const struct sieve_action *act2)
+{
+	struct act_report_data *rdd1 =
+		(struct act_report_data *) act1->context;
+	struct act_report_data *rdd2 =
+		(struct act_report_data *) act2->context;
+
+	/* Address is already normalized */
+	return ( sieve_address_compare
+		(rdd1->to_address, rdd2->to_address, TRUE) == 0 );
+}
+
+static int act_report_check_duplicate
+(const struct sieve_runtime_env *renv ATTR_UNUSED,
+	const struct sieve_action *act,
+	const struct sieve_action *act_other)
+{
+	return ( act_report_equals
+		(renv->scriptenv, act, act_other) ? 1 : 0 );
+}
+
+/* Result printing */
+
+static void act_report_print
+(const struct sieve_action *action,
+	const struct sieve_result_print_env *rpenv,
+	bool *keep ATTR_UNUSED)
+{
+	const struct act_report_data *rdd =
+		(struct act_report_data *) action->context;
+
+	sieve_result_action_printf(rpenv,
+		"report incoming message as `%s' to: %s",
+		str_sanitize(rdd->feedback_type, 32),
+		str_sanitize(rdd->to_address, 256));
+}
+
+/* Result execution */
+
+static bool _contains_8bit(const char *msg)
+{
+	const unsigned char *s = (const unsigned char *)msg;
+
+	for (; *s != '\0'; s++) {
+		if ((*s & 0x80) != 0)
+			return TRUE;
+	}
+	return FALSE;
+}
+
+static int act_report_send
+(const struct sieve_action_exec_env *aenv,
+	const struct act_report_data *act)
+{
+	struct sieve_instance *svinst = aenv->svinst;
+	struct sieve_message_context *msgctx = aenv->msgctx;
+	const struct sieve_script_env *senv = aenv->scriptenv;
+	const struct sieve_message_data *msgdata = aenv->msgdata;
+	struct sieve_smtp_context *sctx;
+	struct istream *input;
+	struct ostream *output;
+	string_t *msg;
+	const char *const *headers;
+	const char *sender, *orig_recipient;
+	const char *outmsgid, *boundary, *error, *subject;
+	int ret;
+
+	/* Just to be sure */
+	if ( !sieve_smtp_available(senv) ) {
+		sieve_result_global_warning(aenv,
+			"report action has no means to send mail");
+		return SIEVE_EXEC_OK;
+	}
+
+	sender = sieve_message_get_sender(msgctx);
+	orig_recipient = sieve_message_get_orig_recipient(msgctx);
+
+	/* Make sure we have a subject for our report */
+	if ( mail_get_headers_utf8
+		(msgdata->mail, "subject", &headers) < 0 ) {
+		return sieve_result_mail_error(aenv, msgdata->mail,
+			"report action: "
+			"failed to read header field `subject'");
+	}
+	if ( headers[0] != NULL ) {
+		subject = t_strconcat("Report: ", headers[0], NULL);
+	}	else {
+		subject = "Report: (message without subject)";
+	}
+
+	/* Start message */
+	sctx = sieve_smtp_start_single
+		(senv, act->to_address, NULL, &output);
+
+	outmsgid = sieve_message_get_new_id(aenv->svinst);
+	boundary = t_strdup_printf("%s/%s", my_pid, svinst->hostname);
+
+	/* Compose main report headers */
+	msg = t_str_new(512);
+	rfc2822_header_write(msg, "X-Sieve", SIEVE_IMPLEMENTATION);
+	rfc2822_header_write(msg, "Message-ID", outmsgid);
+	rfc2822_header_write(msg, "Date", message_date_create(ioloop_time));
+
+	rfc2822_header_printf(msg, "From",
+		"Postmaster <%s>", senv->postmaster_address);
+
+	rfc2822_header_printf(msg, "To",
+		"<%s>", act->to_address);
+
+	if ( _contains_8bit(subject) )
+		rfc2822_header_utf8_printf(msg, "Subject", "%s", subject);
+	else
+		rfc2822_header_printf(msg, "Subject", "%s", subject);
+
+	rfc2822_header_write(msg, "Auto-Submitted", "auto-generated (report)");
+
+	rfc2822_header_write(msg, "MIME-Version", "1.0");
+	rfc2822_header_printf(msg, "Content-Type",
+		"multipart/report; report-type=feedback-report;\n"
+		"boundary=\"%s\"", boundary);
+
+	str_append(msg, "\r\nThis is a MIME-encapsulated message\r\n\r\n");
+
+	/* Human-readable report */
+	str_printfa(msg, "--%s\r\n", boundary);
+	if (_contains_8bit(act->message)) {
+		rfc2822_header_write(msg,
+			"Content-Type", "text/plain; charset=utf-8");
+		rfc2822_header_write(msg, "Content-Transfer-Encoding", "8bit");
+	} else {
+		rfc2822_header_write(msg,
+			"Content-Type", "text/plain; charset=us-ascii");
+		rfc2822_header_write(msg, "Content-Transfer-Encoding", "7bit");
+	}
+	rfc2822_header_write(msg, "Content-Disposition", "inline");
+
+	str_printfa(msg, "\r\n%s\r\n\r\n", act->message);
+	o_stream_send(output, str_data(msg), str_len(msg));
+
+	/* Machine-readable report */
+  str_truncate(msg, 0);
+	str_printfa(msg, "--%s\r\n", boundary);
+	rfc2822_header_write(msg,
+		"Content-Type", "message/feedback-report");
+	str_append(msg, "\r\n");
+
+	rfc2822_header_write(msg,	"Version", "1");
+	rfc2822_header_write(msg,
+		"Feedback-Type", act->feedback_type);
+	rfc2822_header_write(msg,	"User-Agent",
+		PACKAGE_NAME "/" PACKAGE_VERSION " "
+		PIGEONHOLE_NAME "/" PIGEONHOLE_VERSION);
+	if (sender == NULL) {
+		rfc2822_header_write(msg,
+			"Original-Mail-From", "<>");
+	} else {
+		rfc2822_header_printf(msg,
+			"Original-Mail-From", "<%s>", sender);
+	}
+	if (orig_recipient != NULL) {
+		rfc2822_header_printf(msg,
+			"Original-Rcpt-To", "<%s>", orig_recipient);
+	}
+	str_append(msg, "\r\n");
+
+	o_stream_send(output, str_data(msg), str_len(msg));
+
+	/* Original message */
+  str_truncate(msg, 0);
+	str_printfa(msg, "--%s\r\n", boundary);
+	if (act->headers_only) {
+		rfc2822_header_write(msg,
+			"Content-Type", "text/rfc822-headers");
+	} else {
+		rfc2822_header_write(msg,
+			"Content-Type", "message/rfc822");
+	}
+	rfc2822_header_write(msg,
+		"Content-Disposition", "attachment");
+	str_append(msg, "\r\n");
+	o_stream_send(output, str_data(msg), str_len(msg));
+
+	if (act->headers_only) {
+		struct message_size hdr_size;
+		ret = mail_get_hdr_stream(msgdata->mail, &hdr_size, &input);
+		if (ret >= 0)
+			input = i_stream_create_limit(input, hdr_size.physical_size);
+	} else {
+		ret = mail_get_stream(msgdata->mail, NULL, NULL, &input);
+		if (ret >= 0)
+			i_stream_ref(input);
+	}
+	if (ret < 0) {
+		return sieve_result_mail_error(aenv, msgdata->mail,
+			"report action: failed to read input message");
+	}
+  ret = o_stream_send_istream(output, input);
+  i_assert(ret != 0);
+	i_stream_unref(&input);
+
+  str_truncate(msg, 0);
+	if (!act->headers_only)
+		str_printfa(msg, "\r\n");
+	str_printfa(msg, "\r\n--%s--\r\n", boundary);
+  o_stream_send(output, str_data(msg), str_len(msg));
+
+	/* Finish sending message */
+	if ( (ret=sieve_smtp_finish(sctx, &error)) <= 0 ) {
+		if (ret < 0) {
+			sieve_result_global_error(aenv,
+				"failed to send `%s' report to <%s>: %s "
+				"(temporary failure)",
+				str_sanitize(act->feedback_type, 32),
+				str_sanitize(act->to_address, 256),
+				str_sanitize(error, 512));
+		} else {
+			sieve_result_global_log_error(aenv,
+				"failed to send `%s' report to <%s>: %s "
+				"(permanent failure)",
+				str_sanitize(act->feedback_type, 32),
+				str_sanitize(act->to_address, 256),
+				str_sanitize(error, 512));
+		}
+	} else {
+		sieve_result_global_log(aenv,
+			"sent `%s' report to <%s>",
+			str_sanitize(act->feedback_type, 32),
+			str_sanitize(act->to_address, 256));
+	}
+
+	return SIEVE_EXEC_OK;
+}
+
+static int act_report_commit
+(const struct sieve_action *action,
+	const struct sieve_action_exec_env *aenv,
+	void *tr_context ATTR_UNUSED,
+	bool *keep ATTR_UNUSED)
+{
+	const struct act_report_data *act =
+		(const struct act_report_data *) action->context;
+	int ret;
+
+	T_BEGIN {
+		ret = act_report_send(aenv, act);
+	} T_END;
+
+	if ( ret == SIEVE_EXEC_TEMP_FAILURE )
+		return SIEVE_EXEC_TEMP_FAILURE;
+
+	/* Ignore all other errors */
+	return SIEVE_EXEC_OK;
+}
diff --git a/src/lib-sieve/plugins/vnd.dovecot/report/ext-vnd-report-common.c b/src/lib-sieve/plugins/vnd.dovecot/report/ext-vnd-report-common.c
new file mode 100644
index 000000000..93827cb58
--- /dev/null
+++ b/src/lib-sieve/plugins/vnd.dovecot/report/ext-vnd-report-common.c
@@ -0,0 +1,35 @@
+/* Copyright (c) 2016 Pigeonhole authors, see the included COPYING file
+ */
+
+#include "lib.h"
+#include "str.h"
+#include "rfc822-parser.h"
+
+#include "sieve-common.h"
+
+#include "ext-vnd-report-common.h"
+
+const char *
+ext_vnd_report_parse_feedback_type(const char *feedback_type)
+{
+	struct rfc822_parser_context parser;
+	string_t *token;
+
+	/* Initialize parsing */
+	rfc822_parser_init(&parser,
+		(const unsigned char *)feedback_type, strlen(feedback_type), NULL);
+	(void)rfc822_skip_lwsp(&parser);
+
+	/* Parse MIME token */
+	token = t_str_new(64);
+	if (rfc822_parse_mime_token(&parser, token) < 0)
+		return NULL;
+
+	/* Content-type value must end here, otherwise it is invalid after all */
+	(void)rfc822_skip_lwsp(&parser);
+	if ( parser.data != parser.end )
+		return NULL;
+
+	/* Success */
+	return str_c(token);
+}
diff --git a/src/lib-sieve/plugins/vnd.dovecot/report/ext-vnd-report-common.h b/src/lib-sieve/plugins/vnd.dovecot/report/ext-vnd-report-common.h
new file mode 100644
index 000000000..67b499bca
--- /dev/null
+++ b/src/lib-sieve/plugins/vnd.dovecot/report/ext-vnd-report-common.h
@@ -0,0 +1,32 @@
+/* Copyright (c)2016 Pigeonhole authors, see the included COPYING file
+ */
+
+#ifndef __EXT_REPORT_COMMON_H
+#define __EXT_REPORT_COMMON_H
+
+/*
+ * Extension
+ */
+
+extern const struct sieve_extension_def vnd_report_extension;
+
+/*
+ * Commands
+ */
+
+extern const struct sieve_command_def cmd_report;
+
+/*
+ * Operations
+ */
+
+extern const struct sieve_operation_def report_operation;
+
+/*
+ * RFC 5965 feedback-type
+ */
+
+const char *
+ext_vnd_report_parse_feedback_type(const char *feedback_type);
+
+#endif /* __EXT_NOTIFY_COMMON_H */
diff --git a/src/lib-sieve/plugins/vnd.dovecot/report/ext-vnd-report.c b/src/lib-sieve/plugins/vnd.dovecot/report/ext-vnd-report.c
new file mode 100644
index 000000000..6db8189c5
--- /dev/null
+++ b/src/lib-sieve/plugins/vnd.dovecot/report/ext-vnd-report.c
@@ -0,0 +1,51 @@
+/* Copyright (c) 2016 Pigeonhole authors, see the included COPYING file
+ */
+
+/* Extension report
+ * ----------------
+ *
+ * Authors: Stephan Bosch
+ * Specification: draft-ietf-sieve-report-00.txt
+ * Implementation: full, but deprecated; provided for backwards compatibility
+ * Status: testing
+ *
+ */
+
+#include "sieve-common.h"
+
+#include "sieve-code.h"
+#include "sieve-extensions.h"
+#include "sieve-actions.h"
+#include "sieve-commands.h"
+#include "sieve-validator.h"
+#include "sieve-generator.h"
+#include "sieve-interpreter.h"
+#include "sieve-result.h"
+
+#include "ext-vnd-report-common.h"
+
+/*
+ * Extension
+ */
+
+static bool ext_report_validator_load
+	(const struct sieve_extension *ext, struct sieve_validator *valdtr);
+
+const struct sieve_extension_def vnd_report_extension = {
+	.name = "vnd.dovecot.report",
+	.validator_load = ext_report_validator_load,
+	SIEVE_EXT_DEFINE_OPERATION(report_operation)
+};
+
+/*
+ * Extension validation
+ */
+
+static bool ext_report_validator_load
+(const struct sieve_extension *ext, struct sieve_validator *valdtr)
+{
+	/* Register new commands */
+	sieve_validator_register_command(valdtr, ext, &cmd_report);
+
+	return TRUE;
+}
diff --git a/src/lib-sieve/sieve-extensions.c b/src/lib-sieve/sieve-extensions.c
index 2b70b1c32..cd0564ea4 100644
--- a/src/lib-sieve/sieve-extensions.c
+++ b/src/lib-sieve/sieve-extensions.c
@@ -144,6 +144,7 @@ extern const struct sieve_extension_def editheader_extension;
 
 extern const struct sieve_extension_def vnd_debug_extension;
 extern const struct sieve_extension_def vnd_environment_extension;
+extern const struct sieve_extension_def vnd_report_extension;
 
 const struct sieve_extension_def *sieve_extra_extensions[] = {
 	&vacation_seconds_extension, &spamtest_extension, &spamtestplus_extension,
@@ -151,7 +152,7 @@ const struct sieve_extension_def *sieve_extra_extensions[] = {
 	&mboxmetadata_extension, &servermetadata_extension,
 
 	/* vnd.dovecot. */
-	&vnd_debug_extension, &vnd_environment_extension
+	&vnd_debug_extension, &vnd_environment_extension, &vnd_report_extension
 };
 
 const unsigned int sieve_extra_extensions_count =
diff --git a/tests/extensions/vnd.dovecot/report/errors.svtest b/tests/extensions/vnd.dovecot/report/errors.svtest
new file mode 100644
index 000000000..82ab992f7
--- /dev/null
+++ b/tests/extensions/vnd.dovecot/report/errors.svtest
@@ -0,0 +1,13 @@
+require "vnd.dovecot.testsuite";
+require "comparator-i;ascii-numeric";
+require "relational";
+
+test "Invalid syntax (FIXME: count only)" {
+	if test_script_compile "errors/syntax.sieve" {
+		test_fail "compile should have failed";
+	}
+
+	if not test_error :count "eq" :comparator "i;ascii-numeric" "9" {
+		test_fail "wrong number of errors reported";
+	}
+}
diff --git a/tests/extensions/vnd.dovecot/report/errors/syntax.sieve b/tests/extensions/vnd.dovecot/report/errors/syntax.sieve
new file mode 100644
index 000000000..250ad6005
--- /dev/null
+++ b/tests/extensions/vnd.dovecot/report/errors/syntax.sieve
@@ -0,0 +1,28 @@
+require "vnd.dovecot.report";
+
+# 1: Too few arguments
+report;
+
+# 2: Too few arguments
+report "abuse";
+
+# 3: Too few arguments
+report "abuse" "Message is spam.";
+
+# Not an error
+report "abuse" "Message is spam." "frop@example.com";
+
+# 4: Bad arguments
+report "abuse" "Message is spam." 1;
+
+# 5: Bad tag
+report :frop "abuse" "Message is spam." "frop@example.com";
+
+# 6: Bad sub-test
+report "abuse" "Message is spam." "frop@example.com" frop;
+
+# 7: Bad block
+report "abuse" "Message is spam." "frop@example.com" { }
+
+# 8: Bad feedback type
+report "?????" "Message is spam." "frop@example.com";
diff --git a/tests/extensions/vnd.dovecot/report/execute.svtest b/tests/extensions/vnd.dovecot/report/execute.svtest
new file mode 100644
index 000000000..fdb80433a
--- /dev/null
+++ b/tests/extensions/vnd.dovecot/report/execute.svtest
@@ -0,0 +1,70 @@
+require "vnd.dovecot.testsuite";
+require "vnd.dovecot.report";
+require "relational";
+require "comparator-i;ascii-numeric";
+require "body";
+
+/*
+ * Simple test
+ */
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: Frop!
+
+Klutsefluts.
+.
+;
+
+test "Simple" {
+	report "abuse" "This message is spam!" "abuse@example.com";
+
+	if not test_result_execute {
+		test_fail "failed to execute notify";
+	}
+
+	test_message :smtp 0;
+
+	if not body :raw :contains "This message is spam!" {
+		test_fail "report does not contain user text";
+	}
+
+	if not body :raw :contains "Klutsefluts" {
+		test_fail "report does not contain message body";
+	}
+}
+
+/*
+ * Simple - :headers_only test
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: Frop!
+
+Klutsefluts.
+.
+;
+
+test "Simple - :headers_only" {
+	report :headers_only "abuse"
+		"This message is spam!" "abuse@example.com";
+
+	if not test_result_execute {
+		test_fail "failed to execute notify";
+	}
+
+	test_message :smtp 0;
+
+	if not body :raw :contains "This message is spam!" {
+		test_fail "report does not contain user text";
+	}
+
+	if body :raw :contains "Klutsefluts" {
+		test_fail "report contains message body";
+	}
+}
-- 
GitLab