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