Index: src/__phutil_library_map__.php =================================================================== --- src/__phutil_library_map__.php +++ src/__phutil_library_map__.php @@ -1574,6 +1574,7 @@ 'PhabricatorMail' => 'applications/metamta/PhabricatorMail.php', 'PhabricatorMailImplementationAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationAdapter.php', 'PhabricatorMailImplementationAmazonSESAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationAmazonSESAdapter.php', + 'PhabricatorMailImplementationMailgunAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php', 'PhabricatorMailImplementationPHPMailerAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php', 'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php', 'PhabricatorMailImplementationSendGridAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php', @@ -1589,6 +1590,7 @@ 'PhabricatorMailReceiver' => 'applications/metamta/receiver/PhabricatorMailReceiver.php', 'PhabricatorMailReceiverTestCase' => 'applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php', 'PhabricatorMailReplyHandler' => 'applications/metamta/replyhandler/PhabricatorMailReplyHandler.php', + 'PhabricatorMailgunConfigOptions' => 'applications/config/option/PhabricatorMailgunConfigOptions.php', 'PhabricatorMailingListPHIDTypeList' => 'applications/mailinglists/phid/PhabricatorMailingListPHIDTypeList.php', 'PhabricatorMailingListQuery' => 'applications/mailinglists/query/PhabricatorMailingListQuery.php', 'PhabricatorMailingListSearchEngine' => 'applications/mailinglists/query/PhabricatorMailingListSearchEngine.php', @@ -1620,6 +1622,7 @@ 'PhabricatorMetaMTAMailBody' => 'applications/metamta/view/PhabricatorMetaMTAMailBody.php', 'PhabricatorMetaMTAMailBodyTestCase' => 'applications/metamta/view/__tests__/PhabricatorMetaMTAMailBodyTestCase.php', 'PhabricatorMetaMTAMailTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php', + 'PhabricatorMetaMTAMailgunReceiveController' => 'applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php', 'PhabricatorMetaMTAMailingList' => 'applications/mailinglists/storage/PhabricatorMetaMTAMailingList.php', 'PhabricatorMetaMTAPermanentFailureException' => 'applications/metamta/exception/PhabricatorMetaMTAPermanentFailureException.php', 'PhabricatorMetaMTAReceivedMail' => 'applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php', @@ -4194,6 +4197,7 @@ 'PhabricatorMacroTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorMacroViewController' => 'PhabricatorMacroController', 'PhabricatorMailImplementationAmazonSESAdapter' => 'PhabricatorMailImplementationPHPMailerLiteAdapter', + 'PhabricatorMailImplementationMailgunAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMailImplementationPHPMailerAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMailImplementationSendGridAdapter' => 'PhabricatorMailImplementationAdapter', @@ -4207,6 +4211,7 @@ 'PhabricatorMailManagementShowOutboundWorkflow' => 'PhabricatorMailManagementWorkflow', 'PhabricatorMailManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorMailReceiverTestCase' => 'PhabricatorTestCase', + 'PhabricatorMailgunConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorMailingListPHIDTypeList' => 'PhabricatorPHIDType', 'PhabricatorMailingListQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorMailingListSearchEngine' => 'PhabricatorApplicationSearchEngine', @@ -4235,6 +4240,7 @@ 'PhabricatorMetaMTAMail' => 'PhabricatorMetaMTADAO', 'PhabricatorMetaMTAMailBodyTestCase' => 'PhabricatorTestCase', 'PhabricatorMetaMTAMailTestCase' => 'PhabricatorTestCase', + 'PhabricatorMetaMTAMailgunReceiveController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAMailingList' => array( 0 => 'PhabricatorMetaMTADAO', Index: src/applications/config/option/PhabricatorMailgunConfigOptions.php =================================================================== --- /dev/null +++ src/applications/config/option/PhabricatorMailgunConfigOptions.php @@ -0,0 +1,28 @@ +newOption('mailgun.api-key', 'string', null) + ->setMasked(true) + ->setDescription(pht('Mailgun API key.')), + $this->newOption('mailgun.domain', 'string', null) + ->setDescription( + pht( + 'Mailgun domain name. See https://mailgun.com/cp/domains')) + ->addExample('mycompany.com', 'Use specific domain'), + ); + + } + +} Index: src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php =================================================================== --- /dev/null +++ src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php @@ -0,0 +1,122 @@ +params['from'] = $email; + $this->params['from-name'] = $name; + return $this; + } + + public function addReplyTo($email, $name = '') { + if (empty($this->params['reply-to'])) { + $this->params['reply-to'] = array(); + } + $this->params['reply-to'][] = array( + 'email' => $email, + 'name' => $name, + ); + return $this; + } + + public function addTos(array $emails) { + foreach ($emails as $email) { + $this->params['tos'][] = $email; + } + return $this; + } + + public function addCCs(array $emails) { + foreach ($emails as $email) { + $this->params['ccs'][] = $email; + } + return $this; + } + + public function addAttachment($data, $filename, $mimetype) { + // TODO: implement attachments. Requires changes in HTTPSFuture + throw new Exception( + "Mailgun adapter does not currently support attachments."); + } + + public function addHeader($header_name, $header_value) { + $this->params['headers'][] = array($header_name, $header_value); + return $this; + } + + public function setBody($body) { + $this->params['body'] = $body; + return $this; + } + + public function setSubject($subject) { + $this->params['subject'] = $subject; + return $this; + } + + public function setIsHTML($is_html) { + $this->params['is-html'] = $is_html; + return $this; + } + + public function supportsMessageIDHeader() { + return false; + } + + public function send() { + $key = PhabricatorEnv::getEnvConfig('mailgun.api-key'); + $domain = PhabricatorEnv::getEnvConfig('mailgun.domain'); + $params = array(); + + $params['to'] = idx($this->params, 'tos', array()); + $params['subject'] = idx($this->params, 'subject'); + + if (idx($this->params, 'is-html')) { + $params['html'] = idx($this->params, 'body'); + } else { + $params['text'] = idx($this->params, 'body'); + } + + $from = idx($this->params, 'from'); + if (idx($this->params, 'from-name')) { + $params['from'] = "{$this->params['from-name']} <{$from}>"; + } else { + $params['from'] = $from; + } + + if (idx($this->params, 'reply-to')) { + $replyto = $this->params['reply-to']; + $params['h:reply-to'] = $replyto; + } + + if (idx($this->params, 'ccs')) { + $params['cc'] = $this->params['ccs']; + } + + $future = new HTTPSFuture( + "https://api:{$key}@api.mailgun.net/v2/{$domain}/messages", + $params); + $future->setMethod('POST'); + + list($body) = $future->resolvex(); + + $response = json_decode($body, true); + if (!is_array($response)) { + throw new Exception("Failed to JSON decode response: {$body}"); + } + + if (!idx($response, 'id')) { + $message = $response['message']; + throw new Exception("Request failed with errors: {$message}."); + } + + return true; + } + +} Index: src/applications/metamta/application/PhabricatorApplicationMetaMTA.php =================================================================== --- src/applications/metamta/application/PhabricatorApplicationMetaMTA.php +++ src/applications/metamta/application/PhabricatorApplicationMetaMTA.php @@ -34,6 +34,7 @@ return array( $this->getBaseURI() => array( 'sendgrid/' => 'PhabricatorMetaMTASendGridReceiveController', + 'mailgun/' => 'PhabricatorMetaMTAMailgunReceiveController', ), ); } Index: src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php =================================================================== --- /dev/null +++ src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php @@ -0,0 +1,76 @@ +getRequest(); + $timestamp = $request->getStr('timestamp'); + $token = $request->getStr('token'); + $sig = $request->getStr('signature'); + return hash_hmac('sha256', $timestamp.$token, $api_key) == $sig; + + } + public function processRequest() { + + // No CSRF for Mailgun. + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + + if (!$this->verifyMessage()) { + throw new Exception( + 'Mail signature is not valid. Check your Mailgun API key.'); + } + + $request = $this->getRequest(); + $user = $request->getUser(); + + $raw_headers = $request->getStr('headers'); + $raw_headers = explode("\n", rtrim($raw_headers)); + $raw_dict = array(); + foreach (array_filter($raw_headers) as $header) { + list($name, $value) = explode(':', $header, 2); + $raw_dict[$name] = ltrim($value); + } + + $headers = array( + 'to' => $request->getStr('recipient'), + 'from' => $request->getStr('from'), + 'subject' => $request->getStr('subject'), + ) + $raw_dict; + + $received = new PhabricatorMetaMTAReceivedMail(); + $received->setHeaders($headers); + $received->setBodies(array( + 'text' => $request->getStr('stripped-text'), + 'html' => $request->getStr('stripped-html'), + )); + + $file_phids = array(); + foreach ($_FILES as $file_raw) { + try { + $file = PhabricatorFile::newFromPHPUpload( + $file_raw, + array( + 'authorPHID' => $user->getPHID(), + )); + $file_phids[] = $file->getPHID(); + } catch (Exception $ex) { + phlog($ex); + } + } + $received->setAttachments($file_phids); + $received->save(); + + $received->processReceivedMail(); + + $response = new AphrontWebpageResponse(); + $response->setContent(pht("Got it! Thanks, Mailgun!\n")); + return $response; + } + +}