diff --git a/conf/default.conf.php b/conf/default.conf.php index 000bdc5e56..1c13a4db96 100644 --- a/conf/default.conf.php +++ b/conf/default.conf.php @@ -253,6 +253,13 @@ return array( // you might as well. 'phabricator.csrf-key' => '0b7ec0592e0a2829d8b71df2fa269b2c6172eca3', + // This is hashed with other inputs to generate mail tokens. If you want, you + // can change it to some other string which is unique to your install. In + // particular, you will want to do this if you accidentally send a bunch of + // mail somewhere you shouldn't have, to invalidate all old reply-to + // addresses. + 'phabricator.mail-key' => '5ce3e7e8787f6e40dfae861da315a5cdf1018f12', + // Version string displayed in the footer. You probably should leave this // alone. 'phabricator.version' => 'UNSTABLE', diff --git a/externals/mimemailparser/LICENSE b/externals/mimemailparser/LICENSE new file mode 100644 index 0000000000..f13a43b63d --- /dev/null +++ b/externals/mimemailparser/LICENSE @@ -0,0 +1,125 @@ + The "Artistic License" + + Preamble + +The intent of this document is to state the conditions under which a +Package may be copied, such that the Copyright Holder maintains some +semblance of artistic control over the development of the package, +while giving the users of the package the right to use and distribute +the Package in a more-or-less customary fashion, plus the right to make +reasonable modifications. + +Definitions: + + "Package" refers to the collection of files distributed by the + Copyright Holder, and derivatives of that collection of files + created through textual modification. + + "Standard Version" refers to such a Package if it has not been + modified, or has been modified in accordance with the wishes + of the Copyright Holder as specified below. + + "Copyright Holder" is whoever is named in the copyright or + copyrights for the package. + + "You" is you, if you're thinking about copying or distributing + this Package. + + "Reasonable copying fee" is whatever you can justify on the + basis of media cost, duplication charges, time of people involved, + and so on. (You will not be required to justify it to the + Copyright Holder, but only to the computing community at large + as a market that must bear the fee.) + + "Freely Available" means that no fee is charged for the item + itself, though there may be fees involved in handling the item. + It also means that recipients of the item may redistribute it + under the same conditions they received it. + +1. You may make and give away verbatim copies of the source form of the +Standard Version of this Package without restriction, provided that you +duplicate all of the original copyright notices and associated disclaimers. + +2. You may apply bug fixes, portability fixes and other modifications +derived from the Public Domain or from the Copyright Holder. A Package +modified in such a way shall still be considered the Standard Version. + +3. You may otherwise modify your copy of this Package in any way, provided +that you insert a prominent notice in each changed file stating how and +when you changed that file, and provided that you do at least ONE of the +following: + + a) place your modifications in the Public Domain or otherwise make them + Freely Available, such as by posting said modifications to Usenet or + an equivalent medium, or placing the modifications on a major archive + site such as uunet.uu.net, or by allowing the Copyright Holder to include + your modifications in the Standard Version of the Package. + + b) use the modified Package only within your corporation or organization. + + c) rename any non-standard executables so the names do not conflict + with standard executables, which must also be provided, and provide + a separate manual page for each non-standard executable that clearly + documents how it differs from the Standard Version. + + d) make other distribution arrangements with the Copyright Holder. + +4. You may distribute the programs of this Package in object code or +executable form, provided that you do at least ONE of the following: + + a) distribute a Standard Version of the executables and library files, + together with instructions (in the manual page or equivalent) on where + to get the Standard Version. + + b) accompany the distribution with the machine-readable source of + the Package with your modifications. + + c) give non-standard executables non-standard names, and clearly + document the differences in manual pages (or equivalent), together + with instructions on where to get the Standard Version. + + d) make other distribution arrangements with the Copyright Holder. + +5. You may charge a reasonable copying fee for any distribution of this +Package. You may charge any fee you choose for support of this +Package. You may not charge a fee for this Package itself. However, +you may distribute this Package in aggregate with other (possibly +commercial) programs as part of a larger (possibly commercial) software +distribution provided that you do not advertise this Package as a +product of your own. You may embed this Package's interpreter within +an executable of yours (by linking); this shall be construed as a mere +form of aggregation, provided that the complete Standard Version of the +interpreter is so embedded. + +6. The scripts and library files supplied as input to or produced as +output from the programs of this Package do not automatically fall +under the copyright of this Package, but belong to whoever generated +them, and may be sold commercially, and may be aggregated with this +Package. If such scripts or library files are aggregated with this +Package via the so-called "undump" or "unexec" methods of producing a +binary executable image, then distribution of such an image shall +neither be construed as a distribution of this Package nor shall it +fall under the restrictions of Paragraphs 3 and 4, provided that you do +not represent such an executable image as a Standard Version of this +Package. + +7. C subroutines (or comparably compiled subroutines in other +languages) supplied by you and linked into this Package in order to +emulate subroutines and variables of the language defined by this +Package shall not be considered part of this Package, but are the +equivalent of input as in Paragraph 6, provided these subroutines do +not change the language in any way that would cause it to fail the +regression tests for the language. + +8. Aggregation of this Package with a commercial distribution is always +permitted provided that the use of this Package is embedded; that is, +when no overt attempt is made to make this Package's interfaces visible +to the end user of the commercial distribution. Such use shall not be +construed as a distribution of this Package. + +9. The name of the Copyright Holder may not be used to endorse or promote +products derived from this software without specific prior written permission. + +10. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED +WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. diff --git a/externals/mimemailparser/MimeMailParser.class.php b/externals/mimemailparser/MimeMailParser.class.php new file mode 100644 index 0000000000..1447ffc4b8 --- /dev/null +++ b/externals/mimemailparser/MimeMailParser.class.php @@ -0,0 +1,439 @@ +attachment_streams = array(); + } + + /** + * Free the held resouces + * @return void + */ + public function __destruct() { + // clear the email file resource + if (is_resource($this->stream)) { + fclose($this->stream); + } + // clear the MailParse resource + if (is_resource($this->resource)) { + mailparse_msg_free($this->resource); + } + // remove attachment resources + foreach($this->attachment_streams as $stream) { + fclose($stream); + } + } + + /** + * Set the file path we use to get the email text + * @return Object MimeMailParser Instance + * @param $mail_path Object + */ + public function setPath($path) { + // should parse message incrementally from file + $this->resource = mailparse_msg_parse_file($path); + $this->stream = fopen($path, 'r'); + $this->parse(); + return $this; + } + + /** + * Set the Stream resource we use to get the email text + * @return Object MimeMailParser Instance + * @param $stream Resource + */ + public function setStream($stream) { + + // streams have to be cached to file first + if (get_resource_type($stream) == 'stream') { + $tmp_fp = tmpfile(); + if ($tmp_fp) { + while(!feof($stream)) { + fwrite($tmp_fp, fread($stream, 2028)); + } + fseek($tmp_fp, 0); + $this->stream =& $tmp_fp; + } else { + throw new Exception('Could not create temporary files for attachments. Your tmp directory may be unwritable by PHP.'); + return false; + } + fclose($stream); + } else { + $this->stream = $stream; + } + + $this->resource = mailparse_msg_create(); + // parses the message incrementally low memory usage but slower + while(!feof($this->stream)) { + mailparse_msg_parse($this->resource, fread($this->stream, 2082)); + } + $this->parse(); + return $this; + } + + /** + * Set the email text + * @return Object MimeMailParser Instance + * @param $data String + */ + public function setText($data) { + $this->resource = mailparse_msg_create(); + // does not parse incrementally, fast memory hog might explode + mailparse_msg_parse($this->resource, $data); + $this->data = $data; + $this->parse(); + return $this; + } + + /** + * Parse the Message into parts + * @return void + * @private + */ + private function parse() { + $structure = mailparse_msg_get_structure($this->resource); + $this->parts = array(); + foreach($structure as $part_id) { + $part = mailparse_msg_get_part($this->resource, $part_id); + $this->parts[$part_id] = mailparse_msg_get_part_data($part); + } + } + + /** + * Retrieve the Email Headers + * @return Array + */ + public function getHeaders() { + if (isset($this->parts[1])) { + return $this->getPartHeaders($this->parts[1]); + } else { + throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email headers.'); + } + return false; + } + /** + * Retrieve the raw Email Headers + * @return string + */ + public function getHeadersRaw() { + if (isset($this->parts[1])) { + return $this->getPartHeaderRaw($this->parts[1]); + } else { + throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email headers.'); + } + return false; + } + + /** + * Retrieve a specific Email Header + * @return String + * @param $name String Header name + */ + public function getHeader($name) { + if (isset($this->parts[1])) { + $headers = $this->getPartHeaders($this->parts[1]); + if (isset($headers[$name])) { + return $headers[$name]; + } + } else { + throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email headers.'); + } + return false; + } + + /** + * Returns the email message body in the specified format + * @return Mixed String Body or False if not found + * @param $type Object[optional] + */ + public function getMessageBody($type = 'text') { + $body = false; + $mime_types = array( + 'text'=> 'text/plain', + 'html'=> 'text/html' + ); + if (in_array($type, array_keys($mime_types))) { + foreach($this->parts as $part) { + if ($this->getPartContentType($part) == $mime_types[$type]) { + $headers = $this->getPartHeaders($part); + $body = $this->decode($this->getPartBody($part), array_key_exists('content-transfer-encoding', $headers) ? +$headers['content-transfer-encoding'] : ''); + } + } + } else { + throw new Exception('Invalid type specified for MimeMailParser::getMessageBody. "type" can either be text or html.'); + } + return $body; + } + + /** + * get the headers for the message body part. + * @return Array + * @param $type Object[optional] + */ + public function getMessageBodyHeaders($type = 'text') { + $headers = false; + $mime_types = array( + 'text'=> 'text/plain', + 'html'=> 'text/html' + ); + if (in_array($type, array_keys($mime_types))) { + foreach($this->parts as $part) { + if ($this->getPartContentType($part) == $mime_types[$type]) { + $headers = $this->getPartHeaders($part); + } + } + } else { + throw new Exception('Invalid type specified for MimeMailParser::getMessageBody. "type" can either be text or html.'); + } + return $headers; + } + + + /** + * Returns the attachments contents in order of appearance + * @return Array + * @param $type Object[optional] + */ + public function getAttachments() { + $attachments = array(); + $dispositions = array("attachment","inline"); + foreach($this->parts as $part) { + $disposition = $this->getPartContentDisposition($part); + if (in_array($disposition, $dispositions)) { + $attachments[] = new MimeMailParser_attachment( + $part['disposition-filename'], + $this->getPartContentType($part), + $this->getAttachmentStream($part), + $disposition, + $this->getPartHeaders($part) + ); + } + } + return $attachments; + } + + /** + * Return the Headers for a MIME part + * @return Array + * @param $part Array + */ + private function getPartHeaders($part) { + if (isset($part['headers'])) { + return $part['headers']; + } + return false; + } + + /** + * Return a Specific Header for a MIME part + * @return Array + * @param $part Array + * @param $header String Header Name + */ + private function getPartHeader($part, $header) { + if (isset($part['headers'][$header])) { + return $part['headers'][$header]; + } + return false; + } + + /** + * Return the ContentType of the MIME part + * @return String + * @param $part Array + */ + private function getPartContentType($part) { + if (isset($part['content-type'])) { + return $part['content-type']; + } + return false; + } + + /** + * Return the Content Disposition + * @return String + * @param $part Array + */ + private function getPartContentDisposition($part) { + if (isset($part['content-disposition'])) { + return $part['content-disposition']; + } + return false; + } + + /** + * Retrieve the raw Header of a MIME part + * @return String + * @param $part Object + */ + private function getPartHeaderRaw(&$part) { + $header = ''; + if ($this->stream) { + $header = $this->getPartHeaderFromFile($part); + } else if ($this->data) { + $header = $this->getPartHeaderFromText($part); + } else { + throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email parts.'); + } + return $header; + } + /** + * Retrieve the Body of a MIME part + * @return String + * @param $part Object + */ + private function getPartBody(&$part) { + $body = ''; + if ($this->stream) { + $body = $this->getPartBodyFromFile($part); + } else if ($this->data) { + $body = $this->getPartBodyFromText($part); + } else { + throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email parts.'); + } + return $body; + } + + /** + * Retrieve the Header from a MIME part from file + * @return String Mime Header Part + * @param $part Array + */ + private function getPartHeaderFromFile(&$part) { + $start = $part['starting-pos']; + $end = $part['starting-pos-body']; + fseek($this->stream, $start, SEEK_SET); + $header = fread($this->stream, $end-$start); + return $header; + } + /** + * Retrieve the Body from a MIME part from file + * @return String Mime Body Part + * @param $part Array + */ + private function getPartBodyFromFile(&$part) { + $start = $part['starting-pos-body']; + $end = $part['ending-pos-body']; + fseek($this->stream, $start, SEEK_SET); + $body = fread($this->stream, $end-$start); + return $body; + } + + /** + * Retrieve the Header from a MIME part from text + * @return String Mime Header Part + * @param $part Array + */ + private function getPartHeaderFromText(&$part) { + $start = $part['starting-pos']; + $end = $part['starting-pos-body']; + $header = substr($this->data, $start, $end-$start); + return $header; + } + /** + * Retrieve the Body from a MIME part from text + * @return String Mime Body Part + * @param $part Array + */ + private function getPartBodyFromText(&$part) { + $start = $part['starting-pos-body']; + $end = $part['ending-pos-body']; + $body = substr($this->data, $start, $end-$start); + return $body; + } + + /** + * Read the attachment Body and save temporary file resource + * @return String Mime Body Part + * @param $part Array + */ + private function getAttachmentStream(&$part) { + $temp_fp = tmpfile(); + + array_key_exists('content-transfer-encoding', $part['headers']) ? $encoding = $part['headers']['content-transfer-encoding'] : $encoding = ''; + + if ($temp_fp) { + if ($this->stream) { + $start = $part['starting-pos-body']; + $end = $part['ending-pos-body']; + fseek($this->stream, $start, SEEK_SET); + $len = $end-$start; + $written = 0; + $write = 2028; + $body = ''; + while($written < $len) { + if (($written+$write < $len )) { + $write = $len - $written; + } + $part = fread($this->stream, $write); + fwrite($temp_fp, $this->decode($part, $encoding)); + $written += $write; + } + } else if ($this->data) { + $attachment = $this->decode($this->getPartBodyFromText($part), $encoding); + fwrite($temp_fp, $attachment, strlen($attachment)); + } + fseek($temp_fp, 0, SEEK_SET); + } else { + throw new Exception('Could not create temporary files for attachments. Your tmp directory may be unwritable by PHP.'); + return false; + } + return $temp_fp; + } + + + /** + * Decode the string depending on encoding type. + * @return String the decoded string. + * @param $encodedString The string in its original encoded state. + * @param $encodingType The encoding type from the Content-Transfer-Encoding header of the part. + */ + private function decode($encodedString, $encodingType) { + if (strtolower($encodingType) == 'base64') { + return base64_decode($encodedString); + } else if (strtolower($encodingType) == 'quoted-printable') { + return quoted_printable_decode($encodedString); + } else { + return $encodedString; + } + } + +} + + +?> diff --git a/externals/mimemailparser/README b/externals/mimemailparser/README new file mode 100644 index 0000000000..2975add6d9 --- /dev/null +++ b/externals/mimemailparser/README @@ -0,0 +1,3 @@ +From: + + http://code.google.com/p/php-mime-mail-parser/ diff --git a/externals/mimemailparser/attachment.class.php b/externals/mimemailparser/attachment.class.php new file mode 100644 index 0000000000..d56aba1f8d --- /dev/null +++ b/externals/mimemailparser/attachment.class.php @@ -0,0 +1,136 @@ +filename = $filename; + $this->content_type = $content_type; + $this->stream = $stream; + $this->content = null; + $this->content_disposition = $content_disposition; + $this->headers = $headers; + } + + /** + * retrieve the attachment filename + * @return String + */ + public function getFilename() { + return $this->filename; + } + + /** + * Retrieve the Attachment Content-Type + * @return String + */ + public function getContentType() { + return $this->content_type; + } + + /** + * Retrieve the Attachment Content-Disposition + * @return String + */ + public function getContentDisposition() { + return $this->content_disposition; + } + + /** + * Retrieve the Attachment Headers + * @return String + */ + public function getHeaders() { + return $this->headers; + } + + /** + * Retrieve the file extension + * @return String + */ + public function getFileExtension() { + if (!$this->extension) { + $ext = substr(strrchr($this->filename, '.'), 1); + if ($ext == 'gz') { + // special case, tar.gz + // todo: other special cases? + $ext = preg_match("/\.tar\.gz$/i", $ext) ? 'tar.gz' : 'gz'; + } + $this->extension = $ext; + } + return $this->extension; + } + + /** + * Read the contents a few bytes at a time until completed + * Once read to completion, it always returns false + * @return String + * @param $bytes Int[optional] + */ + public function read($bytes = 2082) { + return feof($this->stream) ? false : fread($this->stream, $bytes); + } + + /** + * Retrieve the file content in one go + * Once you retreive the content you cannot use MimeMailParser_attachment::read() + * @return String + */ + public function getContent() { + if ($this->content === null) { + fseek($this->stream, 0); + while(($buf = $this->read()) !== false) { + $this->content .= $buf; + } + } + return $this->content; + } + + /** + * Allow the properties + * MimeMailParser_attachment::$name, + * MimeMailParser_attachment::$extension + * to be retrieved as public properties + * @param $name Object + */ + public function __get($name) { + if ($name == 'content') { + return $this->getContent(); + } else if ($name == 'extension') { + return $this->getFileExtension(); + } + return null; + } + +} + +?> diff --git a/resources/sql/patches/036.mailkey.sql b/resources/sql/patches/036.mailkey.sql new file mode 100644 index 0000000000..7831465365 --- /dev/null +++ b/resources/sql/patches/036.mailkey.sql @@ -0,0 +1,19 @@ +ALTER TABLE phabricator_differential.differential_revision + ADD mailKey VARCHAR(40) binary NOT NULL; + +ALTER TABLE phabricator_maniphest.maniphest_task + ADD mailKey VARCHAR(40) binary NOT NULL; + +CREATE TABLE phabricator_metamta.metamta_receivedmail ( + id int unsigned not null primary key auto_increment, + headers longblob not null, + bodies longblob not null, + attachments longblob not null, + relatedPHID varchar(64) binary, + key(relatedPHID), + authorPHID varchar(64) binary, + key(authorPHID), + message longblob, + dateCreated int unsigned not null, + dateModified int unsigned not null +) engine=innodb; \ No newline at end of file diff --git a/scripts/mail/mail_handler.php b/scripts/mail/mail_handler.php new file mode 100755 index 0000000000..f2f5412e75 --- /dev/null +++ b/scripts/mail/mail_handler.php @@ -0,0 +1,54 @@ +#!/usr/bin/php +setText(file_get_contents('php://stdin')); + +$received = new PhabricatorMetaMTAReceivedMail(); +$received->setHeaders($parser->getHeaders()); +$received->setBodies(array( + 'text' => $parser->getMessageBody('text'), + 'html' => $parser->getMessageBody('html'), +)); + +$attachments = array(); +foreach ($received->getAttachments() as $attachment) { + $file = PhabricatorFile::newFromFileData( + $attachment->getContent(), + array( + 'name' => $attachment->getFilename(), + )); + $attachments[] = $file->getPHID(); +} +$received->setAttachments($attachments); +$received->save(); + +$received->processReceivedMail(); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 1751d70910..7e16878725 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -334,6 +334,9 @@ phutil_register_library_map(array( 'PhabricatorMetaMTAMailingList' => 'applications/metamta/storage/mailinglist', 'PhabricatorMetaMTAMailingListEditController' => 'applications/metamta/controller/mailinglistedit', 'PhabricatorMetaMTAMailingListsController' => 'applications/metamta/controller/mailinglists', + 'PhabricatorMetaMTAReceiveController' => 'applications/metamta/controller/receive', + 'PhabricatorMetaMTAReceivedListController' => 'applications/metamta/controller/receivedlist', + 'PhabricatorMetaMTAReceivedMail' => 'applications/metamta/storage/receivedmail', 'PhabricatorMetaMTASendController' => 'applications/metamta/controller/send', 'PhabricatorMetaMTAViewController' => 'applications/metamta/controller/view', 'PhabricatorOAuthDefaultRegistrationController' => 'applications/auth/controller/oauthregistration/default', @@ -743,6 +746,9 @@ phutil_register_library_map(array( 'PhabricatorMetaMTAMailingList' => 'PhabricatorMetaMTADAO', 'PhabricatorMetaMTAMailingListEditController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAMailingListsController' => 'PhabricatorMetaMTAController', + 'PhabricatorMetaMTAReceiveController' => 'PhabricatorMetaMTAController', + 'PhabricatorMetaMTAReceivedListController' => 'PhabricatorMetaMTAController', + 'PhabricatorMetaMTAReceivedMail' => 'PhabricatorMetaMTADAO', 'PhabricatorMetaMTASendController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAViewController' => 'PhabricatorMetaMTAController', 'PhabricatorOAuthDefaultRegistrationController' => 'PhabricatorOAuthRegistrationController', diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php index 4aa8c1b68b..1342fcd211 100644 --- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php @@ -120,6 +120,8 @@ class AphrontDefaultApplicationConfiguration 'lists/$' => 'PhabricatorMetaMTAMailingListsController', 'lists/edit/(?:(?P\d+)/)?$' => 'PhabricatorMetaMTAMailingListEditController', + 'receive/$' => 'PhabricatorMetaMTAReceiveController', + 'received/$' => 'PhabricatorMetaMTAReceivedListController', ), '/login/' => array( diff --git a/src/applications/differential/storage/revision/DifferentialRevision.php b/src/applications/differential/storage/revision/DifferentialRevision.php index 162e777d50..fd432fce1f 100644 --- a/src/applications/differential/storage/revision/DifferentialRevision.php +++ b/src/applications/differential/storage/revision/DifferentialRevision.php @@ -35,6 +35,8 @@ class DifferentialRevision extends DifferentialDAO { protected $attached = array(); protected $unsubscribed = array(); + protected $mailKey; + private $relationships; private $commits; @@ -115,6 +117,13 @@ class DifferentialRevision extends DifferentialDAO { $this->getID()); } + public function save() { + if (!$this->getMailKey()) { + $this->mailKey = sha1(Filesystem::readRandomBytes(20)); + } + return parent::save(); + } + public function loadRelationships() { if (!$this->getID()) { $this->relationships = array(); diff --git a/src/applications/differential/storage/revision/__init__.php b/src/applications/differential/storage/revision/__init__.php index a6690c2650..bee7373c37 100644 --- a/src/applications/differential/storage/revision/__init__.php +++ b/src/applications/differential/storage/revision/__init__.php @@ -13,6 +13,7 @@ phutil_require_module('phabricator', 'applications/phid/constants'); phutil_require_module('phabricator', 'applications/phid/storage/phid'); phutil_require_module('phabricator', 'storage/queryfx'); +phutil_require_module('phutil', 'filesystem'); phutil_require_module('phutil', 'utils'); diff --git a/src/applications/maniphest/storage/task/ManiphestTask.php b/src/applications/maniphest/storage/task/ManiphestTask.php index 8c8f692429..42959023f4 100644 --- a/src/applications/maniphest/storage/task/ManiphestTask.php +++ b/src/applications/maniphest/storage/task/ManiphestTask.php @@ -29,6 +29,8 @@ class ManiphestTask extends ManiphestDAO { protected $title; protected $description; + protected $mailKey; + protected $attached = array(); protected $projectPHIDs = array(); @@ -56,4 +58,11 @@ class ManiphestTask extends ManiphestDAO { return nonempty($this->ccPHIDs, array()); } + public function save() { + if (!$this->mailKey) { + $this->mailKey = sha1(Filesystem::readRandomBytes(20)); + } + return parent::save(); + } + } diff --git a/src/applications/maniphest/storage/task/__init__.php b/src/applications/maniphest/storage/task/__init__.php index 95791239e1..22a5346f8c 100644 --- a/src/applications/maniphest/storage/task/__init__.php +++ b/src/applications/maniphest/storage/task/__init__.php @@ -10,6 +10,7 @@ phutil_require_module('phabricator', 'applications/maniphest/storage/base'); phutil_require_module('phabricator', 'applications/phid/constants'); phutil_require_module('phabricator', 'applications/phid/storage/phid'); +phutil_require_module('phutil', 'filesystem'); phutil_require_module('phutil', 'utils'); diff --git a/src/applications/metamta/controller/base/PhabricatorMetaMTAController.php b/src/applications/metamta/controller/base/PhabricatorMetaMTAController.php index 8e29ff71cb..76d3214649 100644 --- a/src/applications/metamta/controller/base/PhabricatorMetaMTAController.php +++ b/src/applications/metamta/controller/base/PhabricatorMetaMTAController.php @@ -34,6 +34,10 @@ abstract class PhabricatorMetaMTAController extends PhabricatorController { 'name' => 'Mailing Lists', 'href' => '/mail/lists/', ), + 'received' => array( + 'name' => 'Received', + 'href' => '/mail/received/', + ), ), idx($data, 'tab')); $page->setGlyph("@"); diff --git a/src/applications/metamta/controller/receive/PhabricatorMetaMTAReceiveController.php b/src/applications/metamta/controller/receive/PhabricatorMetaMTAReceiveController.php new file mode 100644 index 0000000000..900d67c2b7 --- /dev/null +++ b/src/applications/metamta/controller/receive/PhabricatorMetaMTAReceiveController.php @@ -0,0 +1,91 @@ +getRequest(); + $user = $request->getUser(); + + if ($request->isFormPost()) { + $receiver = PhabricatorMetaMTAReceivedMail::loadReceiverObject( + $request->getStr('obj')); + if (!$receiver) { + throw new Exception("No such task or revision!"); + } + + $hash = PhabricatorMetaMTAReceivedMail::computeMailHash( + $receiver, + $user); + + $received = new PhabricatorMetaMTAReceivedMail(); + $received->setHeaders( + array( + 'to' => $request->getStr('obj').'+'.$user->getID().'+'.$hash.'@', + )); + $received->setBodies( + array( + 'text' => $request->getStr('body'), + )); + $received->save(); + + $received->processReceivedMail(); + + $phid = $receiver->getPHID(); + $handles = id(new PhabricatorObjectHandleData(array($phid))) + ->loadHandles(); + $uri = $handles[$phid]->getURI(); + + return id(new AphrontRedirectResponse())->setURI($uri); + } + + $form = new AphrontFormView(); + $form->setUser($request->getUser()); + $form->setAction('/mail/receive/'); + $form + ->appendChild( + '

This form will simulate '. + 'sending mail to an object.

') + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('To') + ->setName('obj') + ->setCaption('e.g. D1234 or T1234')) + ->appendChild( + id(new AphrontFormTextAreaControl()) + ->setLabel('Body') + ->setName('body')) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue('Receive Mail')); + + $panel = new AphrontPanelView(); + $panel->setHeader('Receive Email'); + $panel->appendChild($form); + $panel->setWidth(AphrontPanelView::WIDTH_WIDE); + + return $this->buildStandardPageResponse( + $panel, + array( + 'title' => 'Receive Mail', + )); + } + +} diff --git a/src/applications/metamta/controller/receive/__init__.php b/src/applications/metamta/controller/receive/__init__.php new file mode 100644 index 0000000000..d7dcd448f5 --- /dev/null +++ b/src/applications/metamta/controller/receive/__init__.php @@ -0,0 +1,22 @@ +getRequest(); + + $pager = new AphrontPagerView(); + $pager->setOffset($request->getInt('page')); + $pager->setURI($request->getRequestURI(), 'page'); + + $mails = id(new PhabricatorMetaMTAReceivedMail())->loadAllWhere( + '1 = 1 ORDER BY id DESC LIMIT %d, %d', + $pager->getOffset(), + $pager->getPageSize() + 1); + $mails = $pager->sliceResults($mails); + + $phids = array_merge( + mpull($mails, 'getAuthorPHID'), + mpull($mails, 'getRelatedPHID') + ); + $phids = array_unique(array_filter($phids)); + + $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); + + $rows = array(); + foreach ($mails as $mail) { + $rows[] = array( + $mail->getID(), + date('M jS Y', $mail->getDateCreated()), + date('g:i:s A', $mail->getDateCreated()), + $mail->getAuthorPHID() + ? $handles[$mail->getAuthorPHID()]->renderLink() + : '-', + $mail->getRelatedPHID() + ? $handles[$mail->getRelatedPHID()]->renderLink() + : '-', + phutil_escape_html($mail->getMessage()), + ); + } + + $table = new AphrontTableView($rows); + $table->setHeaders( + array( + 'ID', + 'Date', + 'Time', + 'Author', + 'Object', + 'Message', + )); + $table->setColumnClasses( + array( + null, + null, + 'right', + null, + null, + 'wide', + )); + + $panel = new AphrontPanelView(); + $panel->setHeader('Received Mail'); + $panel->setCreateButton('Test Receiver', '/mail/receive/'); + $panel->appendChild($table); + $panel->appendChild($pager); + + return $this->buildStandardPageResponse( + $panel, + array( + 'title' => 'Received Mail', + 'tab' => 'received', + )); + } +} diff --git a/src/applications/metamta/controller/receivedlist/__init__.php b/src/applications/metamta/controller/receivedlist/__init__.php new file mode 100644 index 0000000000..c111e1ef8f --- /dev/null +++ b/src/applications/metamta/controller/receivedlist/__init__.php @@ -0,0 +1,20 @@ + array( + 'headers' => self::SERIALIZATION_JSON, + 'bodies' => self::SERIALIZATION_JSON, + 'attachments' => self::SERIALIZATION_JSON, + ), + ) + parent::getConfiguration(); + } + + public function processReceivedMail() { + $to = idx($this->headers, 'to'); + + $matches = null; + $ok = preg_match( + '/^((?:D|T)\d+)\+(\d+)\+([a-f0-9]{16})@/', + $to, + $matches); + + if (!$ok) { + return $this->setMessage("Unrecognized 'to' format: {$to}")->save(); + } + + $receiver_name = $matches[1]; + $user_id = $matches[2]; + $hash = $matches[3]; + + $user = id(new PhabricatorUser())->load($user_id); + if (!$user) { + return $this->setMessage("Invalid user '{$user_id}'")->save(); + } + + $this->setAuthorPHID($user->getPHID()); + + $receiver = self::loadReceiverObject($receiver_name); + if (!$receiver) { + return $this->setMessage("Invalid object '{$receiver_name}'")->save(); + } + + $this->setRelatedPHID($receiver->getPHID()); + + $expect_hash = self::computeMailHash($receiver, $user); + if ($expect_hash != $hash) { + return $this->setMessage("Invalid mail hash!")->save(); + } + + // TODO: Move this into the application logic instead. + if ($receiver instanceof ManiphestTask) { + $this->processManiphestMail($receiver, $user); + } else if ($receiver instanceof DifferentialRevision) { + $this->processDifferentialMail($receiver, $user); + } + + $this->setMessage('OK'); + + return $this->save(); + } + + private function processManiphestMail( + ManiphestTask $task, + PhabricatorUser $user) { + + // TODO: implement this + + } + + private function processDifferentialMail( + DifferentialRevision $revision, + PhabricatorUser $user) { + + // TODO: Support actions + + $editor = new DifferentialCommentEditor( + $revision, + $user->getPHID(), + DifferentialAction::ACTION_COMMENT); + $editor->setMessage($this->getCleanTextBody()); + $editor->save(); + + } + + private function getCleanTextBody() { + $body = idx($this->bodies, 'text'); + + // TODO: Detect quoted content and exclude it. + + return $body; + } + + public static function loadReceiverObject($receiver_name) { + if (!$receiver_name) { + return null; + } + + $receiver_type = $receiver_name[0]; + $receiver_id = substr($receiver_name, 1); + + $class_obj = null; + switch ($receiver_type) { + case 'T': + $class_obj = newv('ManiphestTask', array()); + break; + case 'D': + $class_obj = newv('DifferentialRevision', array()); + break; + default: + return null; + } + + return $class_obj->load($receiver_id); + } + + public static function computeMailHash( + $mail_receiver, + PhabricatorUser $user) { + $global_mail_key = PhabricatorEnv::getEnvConfig('phabricator.mail-key'); + $local_mail_key = $mail_receiver->getMailKey(); + + $hash = sha1($local_mail_key.$global_mail_key.$user->getPHID()); + return substr($hash, 0, 16); + } + + +} diff --git a/src/applications/metamta/storage/receivedmail/__init__.php b/src/applications/metamta/storage/receivedmail/__init__.php new file mode 100644 index 0000000000..b48e2acdbe --- /dev/null +++ b/src/applications/metamta/storage/receivedmail/__init__.php @@ -0,0 +1,18 @@ + /path/to/phabricator/scripts/mail/mail_handler.php" + +...where is the PHABRICATOR_ENV the script should run under. Run +##sudo newaliases##. Now you likely need to symlink this script into +##/etc/smrsh/##: + + sudo ln -s /path/to/phabricator/scripts/mail/mail_handler.php /etc/smrsh/ + +Finally, edit ##/etc/mail/virtusertable## and add an entry like this: + + @yourdomain.com phabricator@localhost + +That will forward all mail to @yourdomain.com to the Phabricator processing +script. Run ##sudo /etc/mail/make## or similar and then restart sendmail with +##sudo /etc/init.d/sendmail restart## +