Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
   1  <?php
   2  /**
   3   * Copyright 2004-2017 Horde LLC (http://www.horde.org/)
   4   *
   5   * See the enclosed file LICENSE 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   */
  23  class 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  }