<?php

abstract class PhabricatorMailReplyHandler {

  private $mailReceiver;
  private $actor;
  private $excludePHIDs = array();

  final public function setMailReceiver($mail_receiver) {
    $this->validateMailReceiver($mail_receiver);
    $this->mailReceiver = $mail_receiver;
    return $this;
  }

  final public function getMailReceiver() {
    return $this->mailReceiver;
  }

  final public function setActor(PhabricatorUser $actor) {
    $this->actor = $actor;
    return $this;
  }

  final public function getActor() {
    return $this->actor;
  }

  final public function setExcludeMailRecipientPHIDs(array $exclude) {
    $this->excludePHIDs = $exclude;
    return $this;
  }

  final public function getExcludeMailRecipientPHIDs() {
    return $this->excludePHIDs;
  }

  abstract public function validateMailReceiver($mail_receiver);
  abstract public function getPrivateReplyHandlerEmailAddress(
    PhabricatorObjectHandle $handle);
  public function getReplyHandlerDomain() {
    return PhabricatorEnv::getEnvConfig(
      'metamta.reply-handler-domain');
  }
  abstract public function getReplyHandlerInstructions();
  abstract protected function receiveEmail(
    PhabricatorMetaMTAReceivedMail $mail);

  public function processEmail(PhabricatorMetaMTAReceivedMail $mail) {
    $error = $this->sanityCheckEmail($mail);

    if ($error) {
      if ($this->shouldSendErrorEmail($mail)) {
        $this->sendErrorEmail($error, $mail);
      }
      return null;
    }

    return $this->receiveEmail($mail);
  }

  private function sanityCheckEmail(PhabricatorMetaMTAReceivedMail $mail) {
    $body        = $mail->getCleanTextBody();
    $attachments = $mail->getAttachments();

    if (empty($body) && empty($attachments)) {
      return 'Empty email body. Email should begin with an !action and / or '.
             'text to comment. Inline replies and signatures are ignored.';
    }

    return null;
  }

  /**
   * Only send an error email if the user is talking to just Phabricator. We
   * can assume if there is only one To address it is a Phabricator address
   * since this code is running and everything.
   */
  private function shouldSendErrorEmail(PhabricatorMetaMTAReceivedMail $mail) {
    return (count($mail->getToAddresses()) == 1) &&
           (count($mail->getCCAddresses()) == 0);
  }

  private function sendErrorEmail($error,
                                  PhabricatorMetaMTAReceivedMail $mail) {
    $template = new PhabricatorMetaMTAMail();
    $template->setSubject('Exception: unable to process your mail request');
    $template->setBody($this->buildErrorMailBody($error, $mail));
    $template->setRelatedPHID($mail->getRelatedPHID());
    $phid = $this->getActor()->getPHID();
    $handle = id(new PhabricatorHandleQuery())
      ->setViewer($this->getActor())
      ->withPHIDs(array($phid))
      ->executeOne();
    $tos = array($phid => $handle);
    $mails = $this->multiplexMail($template, $tos, array());

    foreach ($mails as $email) {
      $email->saveAndSend();
    }

    return true;
  }

  private function buildErrorMailBody($error,
                                      PhabricatorMetaMTAReceivedMail $mail) {
    $original_body = $mail->getRawTextBody();

    $main_body = <<<EOBODY
Your request failed because an error was encoutered while processing it:

  ERROR: {$error}

  -- Original Body -------------------------------------------------------------

  {$original_body}

EOBODY;

    $body = new PhabricatorMetaMTAMailBody();
    $body->addRawSection($main_body);
    $body->addReplySection($this->getReplyHandlerInstructions());

    return $body->render();
  }

  public function supportsPrivateReplies() {
    return (bool)$this->getReplyHandlerDomain() &&
           !$this->supportsPublicReplies();
  }

  public function supportsPublicReplies() {
    if (!PhabricatorEnv::getEnvConfig('metamta.public-replies')) {
      return false;
    }
    if (!$this->getReplyHandlerDomain()) {
      return false;
    }
    return (bool)$this->getPublicReplyHandlerEmailAddress();
  }

  final public function supportsReplies() {
    return $this->supportsPrivateReplies() ||
           $this->supportsPublicReplies();
  }

  public function getPublicReplyHandlerEmailAddress() {
    return null;
  }

  final public function getRecipientsSummary(
    array $to_handles,
    array $cc_handles) {
    assert_instances_of($to_handles, 'PhabricatorObjectHandle');
    assert_instances_of($cc_handles, 'PhabricatorObjectHandle');

    $body = '';

    if (PhabricatorEnv::getEnvConfig('metamta.recipients.show-hints')) {
      if ($to_handles) {
        $body .= "To: ".implode(', ', mpull($to_handles, 'getName'))."\n";
      }
      if ($cc_handles) {
        $body .= "Cc: ".implode(', ', mpull($cc_handles, 'getName'))."\n";
      }
    }

    return $body;
  }

  final public function multiplexMail(
    PhabricatorMetaMTAMail $mail_template,
    array $to_handles,
    array $cc_handles) {
    assert_instances_of($to_handles, 'PhabricatorObjectHandle');
    assert_instances_of($cc_handles, 'PhabricatorObjectHandle');

    $result = array();

    // If MetaMTA is configured to always multiplex, skip the single-email
    // case.
    if (!PhabricatorMetaMTAMail::shouldMultiplexAllMail()) {
      // If private replies are not supported, simply send one email to all
      // recipients and CCs. This covers cases where we have no reply handler,
      // or we have a public reply handler.
      if (!$this->supportsPrivateReplies()) {
        $mail = clone $mail_template;
        $mail->addTos(mpull($to_handles, 'getPHID'));
        $mail->addCCs(mpull($cc_handles, 'getPHID'));

        if ($this->supportsPublicReplies()) {
          $reply_to = $this->getPublicReplyHandlerEmailAddress();
          $mail->setReplyTo($reply_to);
        }

        $result[] = $mail;

        return $result;
      }
    }

    $tos = mpull($to_handles, null, 'getPHID');
    $ccs = mpull($cc_handles, null, 'getPHID');

    // Merge all the recipients together. TODO: We could keep the CCs as real
    // CCs and send to a "noreply@domain.com" type address, but keep it simple
    // for now.
    $recipients = $tos + $ccs;

    // When multiplexing mail, explicitly include To/Cc information in the
    // message body and headers.

    $mail_template = clone $mail_template;

    $mail_template->addPHIDHeaders('X-Phabricator-To', array_keys($tos));
    $mail_template->addPHIDHeaders('X-Phabricator-Cc', array_keys($ccs));

    $body = $mail_template->getBody();
    $body .= "\n";
    $body .= $this->getRecipientsSummary($to_handles, $cc_handles);

    foreach ($recipients as $phid => $recipient) {
      $mail = clone $mail_template;
      if (isset($to_handles[$phid])) {
        $mail->addTos(array($phid));
      } else if (isset($cc_handles[$phid])) {
        $mail->addCCs(array($phid));
      } else {
        // not good - they should be a to or a cc
        continue;
      }

      $mail->setBody($body);

      $reply_to = null;
      if (!$reply_to && $this->supportsPrivateReplies()) {
        $reply_to = $this->getPrivateReplyHandlerEmailAddress($recipient);
      }

      if (!$reply_to && $this->supportsPublicReplies()) {
        $reply_to = $this->getPublicReplyHandlerEmailAddress();
      }

      if ($reply_to) {
        $mail->setReplyTo($reply_to);
      }

      $result[] = $mail;
    }

    return $result;
  }

  protected function getDefaultPublicReplyHandlerEmailAddress($prefix) {

    $receiver = $this->getMailReceiver();
    $receiver_id = $receiver->getID();
    $domain = $this->getReplyHandlerDomain();

    // We compute a hash using the object's own PHID to prevent an attacker
    // from blindly interacting with objects that they haven't ever received
    // mail about by just sending to D1@, D2@, etc...
    $hash = PhabricatorObjectMailReceiver::computeMailHash(
      $receiver->getMailKey(),
      $receiver->getPHID());

    $address = "{$prefix}{$receiver_id}+public+{$hash}@{$domain}";
    return $this->getSingleReplyHandlerPrefix($address);
  }

  protected function getSingleReplyHandlerPrefix($address) {
    $single_handle_prefix = PhabricatorEnv::getEnvConfig(
      'metamta.single-reply-handler-prefix');
    return ($single_handle_prefix)
      ? $single_handle_prefix . '+' . $address
      : $address;
  }

  protected function getDefaultPrivateReplyHandlerEmailAddress(
    PhabricatorObjectHandle $handle,
    $prefix) {

    if ($handle->getType() != PhabricatorPeoplePHIDTypeUser::TYPECONST) {
      // You must be a real user to get a private reply handler address.
      return null;
    }

    $user = id(new PhabricatorPeopleQuery())
      ->setViewer(PhabricatorUser::getOmnipotentUser())
      ->withPHIDs(array($handle->getPHID()))
      ->executeOne();

    if (!$user) {
      // This may happen if a user was subscribed to something, and was then
      // deleted.
      return null;
    }

    $receiver = $this->getMailReceiver();
    $receiver_id = $receiver->getID();
    $user_id = $user->getID();
    $hash = PhabricatorObjectMailReceiver::computeMailHash(
      $receiver->getMailKey(),
      $handle->getPHID());
    $domain = $this->getReplyHandlerDomain();

    $address = "{$prefix}{$receiver_id}+{$user_id}+{$hash}@{$domain}";
    return $this->getSingleReplyHandlerPrefix($address);
  }

 final protected function enhanceBodyWithAttachments(
    $body,
    array $attachments,
    $format = '- {F%d, layout=link}') {
    if (!$attachments) {
      return $body;
    }

    // TODO: (T603) What's the policy here?
    $files = id(new PhabricatorFile())
      ->loadAllWhere('phid in (%Ls)', $attachments);

    // if we have some text then double return before adding our file list
    if ($body) {
      $body .= "\n\n";
    }

    foreach ($files as $file) {
      $file_str = sprintf($format, $file->getID());
      $body .= $file_str."\n";
    }

    return rtrim($body);
  }

}
