1<?php
2/**
3 * Copyright 2004-2017 Horde LLC (http://www.horde.org/)
4 *
5 * See the enclosed file COPYING for license information (LGPL). If you
6 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
7 *
8 * @category  Horde
9 * @copyright 2004-2017 Horde LLC
10 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
11 * @package   Mime
12 */
13
14/**
15 * Message Disposition Notifications (RFC 3798).
16 *
17 * @author    Michael Slusarz <slusarz@horde.org>
18 * @category  Horde
19 * @copyright 2004-2017 Horde LLC
20 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
21 * @package   Mime
22 */
23class Horde_Mime_Mdn
24{
25    /* RFC 3798 header for requesting a MDN. */
26    const MDN_HEADER = 'Disposition-Notification-To';
27
28    /**
29     * The Horde_Mime_Headers object.
30     *
31     * @var Horde_Mime_Headers
32     */
33    protected $_headers;
34
35    /**
36     * The text of the original message.
37     *
38     * @var string
39     */
40    protected $_msgtext = false;
41
42    /**
43     * Constructor.
44     *
45     * @param Horde_Mime_Headers $mime_headers  A headers object.
46     */
47    public function __construct(Horde_Mime_Headers $headers)
48    {
49        $this->_headers = $headers;
50    }
51
52    /**
53     * Returns the address(es) to return the MDN to.
54     *
55     * @return string  The address(es) to send the MDN to. Returns null if no
56     *                 MDN is requested.
57     */
58    public function getMdnReturnAddr()
59    {
60        /* RFC 3798 [2.1] requires the Disposition-Notification-To header
61         * for an MDN to be created. */
62        return ($hdr = $this->_headers[self::MDN_HEADER])
63            ? strval($hdr)
64            : null;
65    }
66
67    /**
68     * Is user input required to send the MDN?
69     * Explicit confirmation is needed in some cases to prevent mail loops
70     * and the use of MDNs for mail bombing.
71     *
72     * @return boolean  Is explicit user input required to send the MDN?
73     */
74    public function userConfirmationNeeded()
75    {
76        $return_path = $this->_headers['Return-Path'];
77
78        /* RFC 3798 [2.1]: Explicit confirmation is needed if there is no
79         * Return-Path in the header. Also, "if the message contains more
80         * than one Return-Path header, the implementation may [] treat the
81         * situation as a failure of the comparison." */
82        if (!$return_path || (count($return_path->value) > 1)) {
83            return true;
84        }
85
86        /* RFC 3798 [2.1]: Explicit confirmation is needed if there is more
87         * than one distinct address in the Disposition-Notification-To
88         * header. */
89        $addr_ob = ($hdr = $this->_headers[self::MDN_HEADER])
90            ? $hdr->getAddressList(true)
91            : array();
92
93        switch (count($addr_ob)) {
94        case 0:
95            return false;
96
97        case 1:
98            // No-op
99            break;
100
101        default:
102            return true;
103        }
104
105        /* RFC 3798 [2.1] states that "MDNs SHOULD NOT be sent automatically
106         * if the address in the Disposition-Notification-To header differs
107         * from the address in the Return-Path header." This comparison is
108         * case-sensitive for the mailbox part and case-insensitive for the
109         * host part. */
110        $ret_ob = new Horde_Mail_Rfc822_Address($return_path->value);
111        return (!$ret_ob->valid || !$addr_ob->match($ret_ob));
112    }
113
114    /**
115     * When generating the MDN, should we return the enitre text of the
116     * original message?  The default is no - we only return the headers of
117     * the original message. If the text is passed in via this method, we
118     * will return the entire message.
119     *
120     * @param string $text  The text of the original message.
121     */
122    public function originalMessageText($text)
123    {
124        $this->_msgtext = $text;
125    }
126
127    /**
128     * Generate the MDN according to the specifications listed in RFC
129     * 3798 [3].
130     *
131     * @param boolean $action   Was this MDN type a result of a manual
132     *                          action on part of the user?
133     * @param boolean $sending  Was this MDN sent as a result of a manual
134     *                          action on part of the user?
135     * @param string $type      The type of action performed by the user.
136     *                          Per RFC 3798 [3.2.6.2] the following types are
137     *                          valid:
138     *                            - deleted
139     *                            - displayed
140     * @param string $name      The name of the local server.
141     * @param Horde_Mail_Transport $mailer  Mail transport object.
142     * @param array $opts       Additional options:
143     *   - charset: (string) Default charset.
144     *              DEFAULT: NONE
145     *   - from_addr: (string) From address.
146     *                DEFAULT: NONE
147     * @param array $mod        The list of modifications. Per RFC 3798
148     *                          [3.2.6.3] the following modifications are
149     *                          valid:
150     *                            - error
151     * @param array $err        If $mod is 'error', the additional
152     *                          information to provide. Key is the type of
153     *                          modification, value is the text.
154     */
155    public function generate($action, $sending, $type, $name, $mailer,
156                             array $opts = array(), array $mod = array(),
157                             array $err = array())
158    {
159        $opts = array_merge(array(
160            'charset' => null,
161            'from_addr' => null
162        ), $opts);
163
164        if (!($hdr = $this->_headers[self::MDN_HEADER])) {
165            throw new RuntimeException(
166                'Need at least one address to send MDN to.'
167            );
168        }
169
170        $to = $hdr->getAddressList(true);
171        $ua = Horde_Mime_Headers_UserAgent::create();
172
173        if ($orig_recip = $this->_headers['Original-Recipient']) {
174            $orig_recip = $orig_recip->value_single;
175        }
176
177        /* Set up the mail headers. */
178        $msg_headers = new Horde_Mime_Headers();
179        $msg_headers->addHeaderOb(Horde_Mime_Headers_MessageId::create());
180        $msg_headers->addHeaderOb($ua);
181        /* RFC 3834 [5.2] */
182        $msg_headers->addHeader('Auto-Submitted', 'auto-replied');
183        $msg_headers->addHeaderOb(Horde_Mime_Headers_Date::create());
184        if ($opts['from_addr']) {
185            $msg_headers->addHeader('From', $opts['from_addr']);
186        }
187        $msg_headers->addHeader('To', $to);
188        $msg_headers->addHeader('Subject', Horde_Mime_Translation::t("Disposition Notification"));
189
190        /* MDNs are a subtype of 'multipart/report'. */
191        $msg = new Horde_Mime_Part();
192        $msg->setType('multipart/report');
193        $msg->setContentTypeParameter('report-type', 'disposition-notification');
194
195        /* The first part is a human readable message. */
196        $part_one = new Horde_Mime_Part();
197        $part_one->setType('text/plain');
198        $part_one->setCharset($opts['charset']);
199        if ($type == 'displayed') {
200            $contents = sprintf(
201                Horde_Mime_Translation::t("The message sent on %s to %s with subject \"%s\" has been displayed.\n\nThis is no guarantee that the message has been read or understood."),
202                $this->_headers['Date'],
203                $this->_headers['To'],
204                $this->_headers['Subject']
205            );
206            $flowed = new Horde_Text_Flowed($contents, $opts['charset']);
207            $flowed->setDelSp(true);
208            $part_one->setContentTypeParameter('format', 'flowed');
209            $part_one->setContentTypeParameter('DelSp', 'Yes');
210            $part_one->setContents($flowed->toFlowed());
211        }
212        // TODO: Messages for other notification types.
213        $msg[] = $part_one;
214
215        /* The second part is a machine-parseable description. */
216        $part_two = new Horde_Mime_Part();
217        $part_two->setType('message/disposition-notification');
218
219        $part_two_h = new Horde_Mime_Headers();
220        $part_two_h->addHeader('Reporting-UA', $name . '; ' . $ua);
221        if (!empty($orig_recip)) {
222            $part_two_h->addHeader('Original-Recipient', 'rfc822;' . $orig_recip);
223        }
224        if ($opts['from_addr']) {
225            $part_two_h->addHeader('Final-Recipient', 'rfc822;' . $opts['from_addr']);
226        }
227
228        if ($msg_id = $this->_headers['Message-ID']) {
229            $part_two_h->addHeader('Original-Message-ID', strval($msg_id));
230        }
231
232        /* Create the Disposition field now (RFC 3798 [3.2.6]). */
233        $dispo = (($action) ? 'manual-action' : 'automatic-action') .
234            '/' .
235            (($sending) ? 'MDN-sent-manually' : 'MDN-sent-automatically') .
236            '; ' .
237            $type;
238        if (!empty($mod)) {
239            $dispo .= '/' . implode(', ', $mod);
240        }
241        $part_two_h->addHeader('Disposition', $dispo);
242
243        if (in_array('error', $mod) && isset($err['error'])) {
244            $part_two_h->addHeader('Error', $err['error']);
245        }
246
247        $part_two->setContents(trim($part_two_h->toString()) . "\n");
248        $msg[] = $part_two;
249
250        /* The third part is the text of the original message.  RFC 3798 [3]
251         * allows us to return only a portion of the entire message - this
252         * is left up to the user. */
253        $part_three = new Horde_Mime_Part();
254        $part_three->setType('message/rfc822');
255        $part_three_text = array(trim($this->_headers->toString()) . "\n");
256        if (!empty($this->_msgtext)) {
257            $part_three_text[] = "\n" . $this->_msgtext;
258        }
259        $part_three->setContents($part_three_text);
260        $msg[] = $part_three;
261
262        return $msg->send($to, $msg_headers, $mailer);
263    }
264
265    /**
266     * Add a MDN (read receipt) request header.
267     *
268     * @param mixed $to  The address(es) the receipt should be mailed to.
269     */
270    public function addMdnRequestHeaders($to)
271    {
272        /* This is the RFC 3798 way of requesting a receipt. */
273        $this->_headers->addHeader(self::MDN_HEADER, $to);
274    }
275
276}
277