@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Support email replies in Phabricator

Summary:
Provides support for per-user x per-object unique reply-to email addresses, plus
SMTP integration.

This does not actually make Phabricator use these in outbound email.

Test Plan:
Used test console to validate in-Phabricator routing and handling.

Piped emails into the "mail_handler.php" script to validate mail parsing.

Configured sendmail and sent mail to Phabricator.

Technically I haven't conducted all parts of this test on the same machine since
I lost the will to configure more SMTP servers after configuring phabricator.com

Reviewed By: jungejason
Reviewers: jungejason, tuomaspelkonen, aran
CC: aran, epriestley, jungejason
Differential Revision: 226

+1276
+7
conf/default.conf.php
··· 253 253 // you might as well. 254 254 'phabricator.csrf-key' => '0b7ec0592e0a2829d8b71df2fa269b2c6172eca3', 255 255 256 + // This is hashed with other inputs to generate mail tokens. If you want, you 257 + // can change it to some other string which is unique to your install. In 258 + // particular, you will want to do this if you accidentally send a bunch of 259 + // mail somewhere you shouldn't have, to invalidate all old reply-to 260 + // addresses. 261 + 'phabricator.mail-key' => '5ce3e7e8787f6e40dfae861da315a5cdf1018f12', 262 + 256 263 // Version string displayed in the footer. You probably should leave this 257 264 // alone. 258 265 'phabricator.version' => 'UNSTABLE',
+125
externals/mimemailparser/LICENSE
··· 1 + The "Artistic License" 2 + 3 + Preamble 4 + 5 + The intent of this document is to state the conditions under which a 6 + Package may be copied, such that the Copyright Holder maintains some 7 + semblance of artistic control over the development of the package, 8 + while giving the users of the package the right to use and distribute 9 + the Package in a more-or-less customary fashion, plus the right to make 10 + reasonable modifications. 11 + 12 + Definitions: 13 + 14 + "Package" refers to the collection of files distributed by the 15 + Copyright Holder, and derivatives of that collection of files 16 + created through textual modification. 17 + 18 + "Standard Version" refers to such a Package if it has not been 19 + modified, or has been modified in accordance with the wishes 20 + of the Copyright Holder as specified below. 21 + 22 + "Copyright Holder" is whoever is named in the copyright or 23 + copyrights for the package. 24 + 25 + "You" is you, if you're thinking about copying or distributing 26 + this Package. 27 + 28 + "Reasonable copying fee" is whatever you can justify on the 29 + basis of media cost, duplication charges, time of people involved, 30 + and so on. (You will not be required to justify it to the 31 + Copyright Holder, but only to the computing community at large 32 + as a market that must bear the fee.) 33 + 34 + "Freely Available" means that no fee is charged for the item 35 + itself, though there may be fees involved in handling the item. 36 + It also means that recipients of the item may redistribute it 37 + under the same conditions they received it. 38 + 39 + 1. You may make and give away verbatim copies of the source form of the 40 + Standard Version of this Package without restriction, provided that you 41 + duplicate all of the original copyright notices and associated disclaimers. 42 + 43 + 2. You may apply bug fixes, portability fixes and other modifications 44 + derived from the Public Domain or from the Copyright Holder. A Package 45 + modified in such a way shall still be considered the Standard Version. 46 + 47 + 3. You may otherwise modify your copy of this Package in any way, provided 48 + that you insert a prominent notice in each changed file stating how and 49 + when you changed that file, and provided that you do at least ONE of the 50 + following: 51 + 52 + a) place your modifications in the Public Domain or otherwise make them 53 + Freely Available, such as by posting said modifications to Usenet or 54 + an equivalent medium, or placing the modifications on a major archive 55 + site such as uunet.uu.net, or by allowing the Copyright Holder to include 56 + your modifications in the Standard Version of the Package. 57 + 58 + b) use the modified Package only within your corporation or organization. 59 + 60 + c) rename any non-standard executables so the names do not conflict 61 + with standard executables, which must also be provided, and provide 62 + a separate manual page for each non-standard executable that clearly 63 + documents how it differs from the Standard Version. 64 + 65 + d) make other distribution arrangements with the Copyright Holder. 66 + 67 + 4. You may distribute the programs of this Package in object code or 68 + executable form, provided that you do at least ONE of the following: 69 + 70 + a) distribute a Standard Version of the executables and library files, 71 + together with instructions (in the manual page or equivalent) on where 72 + to get the Standard Version. 73 + 74 + b) accompany the distribution with the machine-readable source of 75 + the Package with your modifications. 76 + 77 + c) give non-standard executables non-standard names, and clearly 78 + document the differences in manual pages (or equivalent), together 79 + with instructions on where to get the Standard Version. 80 + 81 + d) make other distribution arrangements with the Copyright Holder. 82 + 83 + 5. You may charge a reasonable copying fee for any distribution of this 84 + Package. You may charge any fee you choose for support of this 85 + Package. You may not charge a fee for this Package itself. However, 86 + you may distribute this Package in aggregate with other (possibly 87 + commercial) programs as part of a larger (possibly commercial) software 88 + distribution provided that you do not advertise this Package as a 89 + product of your own. You may embed this Package's interpreter within 90 + an executable of yours (by linking); this shall be construed as a mere 91 + form of aggregation, provided that the complete Standard Version of the 92 + interpreter is so embedded. 93 + 94 + 6. The scripts and library files supplied as input to or produced as 95 + output from the programs of this Package do not automatically fall 96 + under the copyright of this Package, but belong to whoever generated 97 + them, and may be sold commercially, and may be aggregated with this 98 + Package. If such scripts or library files are aggregated with this 99 + Package via the so-called "undump" or "unexec" methods of producing a 100 + binary executable image, then distribution of such an image shall 101 + neither be construed as a distribution of this Package nor shall it 102 + fall under the restrictions of Paragraphs 3 and 4, provided that you do 103 + not represent such an executable image as a Standard Version of this 104 + Package. 105 + 106 + 7. C subroutines (or comparably compiled subroutines in other 107 + languages) supplied by you and linked into this Package in order to 108 + emulate subroutines and variables of the language defined by this 109 + Package shall not be considered part of this Package, but are the 110 + equivalent of input as in Paragraph 6, provided these subroutines do 111 + not change the language in any way that would cause it to fail the 112 + regression tests for the language. 113 + 114 + 8. Aggregation of this Package with a commercial distribution is always 115 + permitted provided that the use of this Package is embedded; that is, 116 + when no overt attempt is made to make this Package's interfaces visible 117 + to the end user of the commercial distribution. Such use shall not be 118 + construed as a distribution of this Package. 119 + 120 + 9. The name of the Copyright Holder may not be used to endorse or promote 121 + products derived from this software without specific prior written permission. 122 + 123 + 10. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 124 + IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 125 + WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
+439
externals/mimemailparser/MimeMailParser.class.php
··· 1 + <?php 2 + 3 + require_once('attachment.class.php'); 4 + 5 + /** 6 + * Fast Mime Mail parser Class using PHP's MailParse Extension 7 + * @author gabe@fijiwebdesign.com 8 + * @url http://www.fijiwebdesign.com/ 9 + * @license http://creativecommons.org/licenses/by-sa/3.0/us/ 10 + * @version $Id$ 11 + */ 12 + class MimeMailParser { 13 + 14 + /** 15 + * PHP MimeParser Resource ID 16 + */ 17 + public $resource; 18 + 19 + /** 20 + * A file pointer to email 21 + */ 22 + public $stream; 23 + 24 + /** 25 + * A text of an email 26 + */ 27 + public $data; 28 + 29 + /** 30 + * Stream Resources for Attachments 31 + */ 32 + public $attachment_streams; 33 + 34 + /** 35 + * Inialize some stuff 36 + * @return 37 + */ 38 + public function __construct() { 39 + $this->attachment_streams = array(); 40 + } 41 + 42 + /** 43 + * Free the held resouces 44 + * @return void 45 + */ 46 + public function __destruct() { 47 + // clear the email file resource 48 + if (is_resource($this->stream)) { 49 + fclose($this->stream); 50 + } 51 + // clear the MailParse resource 52 + if (is_resource($this->resource)) { 53 + mailparse_msg_free($this->resource); 54 + } 55 + // remove attachment resources 56 + foreach($this->attachment_streams as $stream) { 57 + fclose($stream); 58 + } 59 + } 60 + 61 + /** 62 + * Set the file path we use to get the email text 63 + * @return Object MimeMailParser Instance 64 + * @param $mail_path Object 65 + */ 66 + public function setPath($path) { 67 + // should parse message incrementally from file 68 + $this->resource = mailparse_msg_parse_file($path); 69 + $this->stream = fopen($path, 'r'); 70 + $this->parse(); 71 + return $this; 72 + } 73 + 74 + /** 75 + * Set the Stream resource we use to get the email text 76 + * @return Object MimeMailParser Instance 77 + * @param $stream Resource 78 + */ 79 + public function setStream($stream) { 80 + 81 + // streams have to be cached to file first 82 + if (get_resource_type($stream) == 'stream') { 83 + $tmp_fp = tmpfile(); 84 + if ($tmp_fp) { 85 + while(!feof($stream)) { 86 + fwrite($tmp_fp, fread($stream, 2028)); 87 + } 88 + fseek($tmp_fp, 0); 89 + $this->stream =& $tmp_fp; 90 + } else { 91 + throw new Exception('Could not create temporary files for attachments. Your tmp directory may be unwritable by PHP.'); 92 + return false; 93 + } 94 + fclose($stream); 95 + } else { 96 + $this->stream = $stream; 97 + } 98 + 99 + $this->resource = mailparse_msg_create(); 100 + // parses the message incrementally low memory usage but slower 101 + while(!feof($this->stream)) { 102 + mailparse_msg_parse($this->resource, fread($this->stream, 2082)); 103 + } 104 + $this->parse(); 105 + return $this; 106 + } 107 + 108 + /** 109 + * Set the email text 110 + * @return Object MimeMailParser Instance 111 + * @param $data String 112 + */ 113 + public function setText($data) { 114 + $this->resource = mailparse_msg_create(); 115 + // does not parse incrementally, fast memory hog might explode 116 + mailparse_msg_parse($this->resource, $data); 117 + $this->data = $data; 118 + $this->parse(); 119 + return $this; 120 + } 121 + 122 + /** 123 + * Parse the Message into parts 124 + * @return void 125 + * @private 126 + */ 127 + private function parse() { 128 + $structure = mailparse_msg_get_structure($this->resource); 129 + $this->parts = array(); 130 + foreach($structure as $part_id) { 131 + $part = mailparse_msg_get_part($this->resource, $part_id); 132 + $this->parts[$part_id] = mailparse_msg_get_part_data($part); 133 + } 134 + } 135 + 136 + /** 137 + * Retrieve the Email Headers 138 + * @return Array 139 + */ 140 + public function getHeaders() { 141 + if (isset($this->parts[1])) { 142 + return $this->getPartHeaders($this->parts[1]); 143 + } else { 144 + throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email headers.'); 145 + } 146 + return false; 147 + } 148 + /** 149 + * Retrieve the raw Email Headers 150 + * @return string 151 + */ 152 + public function getHeadersRaw() { 153 + if (isset($this->parts[1])) { 154 + return $this->getPartHeaderRaw($this->parts[1]); 155 + } else { 156 + throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email headers.'); 157 + } 158 + return false; 159 + } 160 + 161 + /** 162 + * Retrieve a specific Email Header 163 + * @return String 164 + * @param $name String Header name 165 + */ 166 + public function getHeader($name) { 167 + if (isset($this->parts[1])) { 168 + $headers = $this->getPartHeaders($this->parts[1]); 169 + if (isset($headers[$name])) { 170 + return $headers[$name]; 171 + } 172 + } else { 173 + throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email headers.'); 174 + } 175 + return false; 176 + } 177 + 178 + /** 179 + * Returns the email message body in the specified format 180 + * @return Mixed String Body or False if not found 181 + * @param $type Object[optional] 182 + */ 183 + public function getMessageBody($type = 'text') { 184 + $body = false; 185 + $mime_types = array( 186 + 'text'=> 'text/plain', 187 + 'html'=> 'text/html' 188 + ); 189 + if (in_array($type, array_keys($mime_types))) { 190 + foreach($this->parts as $part) { 191 + if ($this->getPartContentType($part) == $mime_types[$type]) { 192 + $headers = $this->getPartHeaders($part); 193 + $body = $this->decode($this->getPartBody($part), array_key_exists('content-transfer-encoding', $headers) ? 194 + $headers['content-transfer-encoding'] : ''); 195 + } 196 + } 197 + } else { 198 + throw new Exception('Invalid type specified for MimeMailParser::getMessageBody. "type" can either be text or html.'); 199 + } 200 + return $body; 201 + } 202 + 203 + /** 204 + * get the headers for the message body part. 205 + * @return Array 206 + * @param $type Object[optional] 207 + */ 208 + public function getMessageBodyHeaders($type = 'text') { 209 + $headers = false; 210 + $mime_types = array( 211 + 'text'=> 'text/plain', 212 + 'html'=> 'text/html' 213 + ); 214 + if (in_array($type, array_keys($mime_types))) { 215 + foreach($this->parts as $part) { 216 + if ($this->getPartContentType($part) == $mime_types[$type]) { 217 + $headers = $this->getPartHeaders($part); 218 + } 219 + } 220 + } else { 221 + throw new Exception('Invalid type specified for MimeMailParser::getMessageBody. "type" can either be text or html.'); 222 + } 223 + return $headers; 224 + } 225 + 226 + 227 + /** 228 + * Returns the attachments contents in order of appearance 229 + * @return Array 230 + * @param $type Object[optional] 231 + */ 232 + public function getAttachments() { 233 + $attachments = array(); 234 + $dispositions = array("attachment","inline"); 235 + foreach($this->parts as $part) { 236 + $disposition = $this->getPartContentDisposition($part); 237 + if (in_array($disposition, $dispositions)) { 238 + $attachments[] = new MimeMailParser_attachment( 239 + $part['disposition-filename'], 240 + $this->getPartContentType($part), 241 + $this->getAttachmentStream($part), 242 + $disposition, 243 + $this->getPartHeaders($part) 244 + ); 245 + } 246 + } 247 + return $attachments; 248 + } 249 + 250 + /** 251 + * Return the Headers for a MIME part 252 + * @return Array 253 + * @param $part Array 254 + */ 255 + private function getPartHeaders($part) { 256 + if (isset($part['headers'])) { 257 + return $part['headers']; 258 + } 259 + return false; 260 + } 261 + 262 + /** 263 + * Return a Specific Header for a MIME part 264 + * @return Array 265 + * @param $part Array 266 + * @param $header String Header Name 267 + */ 268 + private function getPartHeader($part, $header) { 269 + if (isset($part['headers'][$header])) { 270 + return $part['headers'][$header]; 271 + } 272 + return false; 273 + } 274 + 275 + /** 276 + * Return the ContentType of the MIME part 277 + * @return String 278 + * @param $part Array 279 + */ 280 + private function getPartContentType($part) { 281 + if (isset($part['content-type'])) { 282 + return $part['content-type']; 283 + } 284 + return false; 285 + } 286 + 287 + /** 288 + * Return the Content Disposition 289 + * @return String 290 + * @param $part Array 291 + */ 292 + private function getPartContentDisposition($part) { 293 + if (isset($part['content-disposition'])) { 294 + return $part['content-disposition']; 295 + } 296 + return false; 297 + } 298 + 299 + /** 300 + * Retrieve the raw Header of a MIME part 301 + * @return String 302 + * @param $part Object 303 + */ 304 + private function getPartHeaderRaw(&$part) { 305 + $header = ''; 306 + if ($this->stream) { 307 + $header = $this->getPartHeaderFromFile($part); 308 + } else if ($this->data) { 309 + $header = $this->getPartHeaderFromText($part); 310 + } else { 311 + throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email parts.'); 312 + } 313 + return $header; 314 + } 315 + /** 316 + * Retrieve the Body of a MIME part 317 + * @return String 318 + * @param $part Object 319 + */ 320 + private function getPartBody(&$part) { 321 + $body = ''; 322 + if ($this->stream) { 323 + $body = $this->getPartBodyFromFile($part); 324 + } else if ($this->data) { 325 + $body = $this->getPartBodyFromText($part); 326 + } else { 327 + throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email parts.'); 328 + } 329 + return $body; 330 + } 331 + 332 + /** 333 + * Retrieve the Header from a MIME part from file 334 + * @return String Mime Header Part 335 + * @param $part Array 336 + */ 337 + private function getPartHeaderFromFile(&$part) { 338 + $start = $part['starting-pos']; 339 + $end = $part['starting-pos-body']; 340 + fseek($this->stream, $start, SEEK_SET); 341 + $header = fread($this->stream, $end-$start); 342 + return $header; 343 + } 344 + /** 345 + * Retrieve the Body from a MIME part from file 346 + * @return String Mime Body Part 347 + * @param $part Array 348 + */ 349 + private function getPartBodyFromFile(&$part) { 350 + $start = $part['starting-pos-body']; 351 + $end = $part['ending-pos-body']; 352 + fseek($this->stream, $start, SEEK_SET); 353 + $body = fread($this->stream, $end-$start); 354 + return $body; 355 + } 356 + 357 + /** 358 + * Retrieve the Header from a MIME part from text 359 + * @return String Mime Header Part 360 + * @param $part Array 361 + */ 362 + private function getPartHeaderFromText(&$part) { 363 + $start = $part['starting-pos']; 364 + $end = $part['starting-pos-body']; 365 + $header = substr($this->data, $start, $end-$start); 366 + return $header; 367 + } 368 + /** 369 + * Retrieve the Body from a MIME part from text 370 + * @return String Mime Body Part 371 + * @param $part Array 372 + */ 373 + private function getPartBodyFromText(&$part) { 374 + $start = $part['starting-pos-body']; 375 + $end = $part['ending-pos-body']; 376 + $body = substr($this->data, $start, $end-$start); 377 + return $body; 378 + } 379 + 380 + /** 381 + * Read the attachment Body and save temporary file resource 382 + * @return String Mime Body Part 383 + * @param $part Array 384 + */ 385 + private function getAttachmentStream(&$part) { 386 + $temp_fp = tmpfile(); 387 + 388 + array_key_exists('content-transfer-encoding', $part['headers']) ? $encoding = $part['headers']['content-transfer-encoding'] : $encoding = ''; 389 + 390 + if ($temp_fp) { 391 + if ($this->stream) { 392 + $start = $part['starting-pos-body']; 393 + $end = $part['ending-pos-body']; 394 + fseek($this->stream, $start, SEEK_SET); 395 + $len = $end-$start; 396 + $written = 0; 397 + $write = 2028; 398 + $body = ''; 399 + while($written < $len) { 400 + if (($written+$write < $len )) { 401 + $write = $len - $written; 402 + } 403 + $part = fread($this->stream, $write); 404 + fwrite($temp_fp, $this->decode($part, $encoding)); 405 + $written += $write; 406 + } 407 + } else if ($this->data) { 408 + $attachment = $this->decode($this->getPartBodyFromText($part), $encoding); 409 + fwrite($temp_fp, $attachment, strlen($attachment)); 410 + } 411 + fseek($temp_fp, 0, SEEK_SET); 412 + } else { 413 + throw new Exception('Could not create temporary files for attachments. Your tmp directory may be unwritable by PHP.'); 414 + return false; 415 + } 416 + return $temp_fp; 417 + } 418 + 419 + 420 + /** 421 + * Decode the string depending on encoding type. 422 + * @return String the decoded string. 423 + * @param $encodedString The string in its original encoded state. 424 + * @param $encodingType The encoding type from the Content-Transfer-Encoding header of the part. 425 + */ 426 + private function decode($encodedString, $encodingType) { 427 + if (strtolower($encodingType) == 'base64') { 428 + return base64_decode($encodedString); 429 + } else if (strtolower($encodingType) == 'quoted-printable') { 430 + return quoted_printable_decode($encodedString); 431 + } else { 432 + return $encodedString; 433 + } 434 + } 435 + 436 + } 437 + 438 + 439 + ?>
+3
externals/mimemailparser/README
··· 1 + From: 2 + 3 + http://code.google.com/p/php-mime-mail-parser/
+136
externals/mimemailparser/attachment.class.php
··· 1 + <?php 2 + 3 + /** 4 + * Model of an Attachment 5 + */ 6 + class MimeMailParser_attachment { 7 + 8 + /** 9 + * @var $filename Filename 10 + */ 11 + public $filename; 12 + /** 13 + * @var $content_type Mime Type 14 + */ 15 + public $content_type; 16 + /** 17 + * @var $content File Content 18 + */ 19 + private $content; 20 + /** 21 + * @var $extension Filename extension 22 + */ 23 + private $extension; 24 + /** 25 + * @var $content_disposition Content-Disposition (attachment or inline) 26 + */ 27 + public $content_disposition; 28 + /** 29 + * @var $headers An Array of the attachment headers 30 + */ 31 + public $headers; 32 + 33 + private $stream; 34 + 35 + public function __construct($filename, $content_type, $stream, $content_disposition = 'attachment', $headers = array()) { 36 + $this->filename = $filename; 37 + $this->content_type = $content_type; 38 + $this->stream = $stream; 39 + $this->content = null; 40 + $this->content_disposition = $content_disposition; 41 + $this->headers = $headers; 42 + } 43 + 44 + /** 45 + * retrieve the attachment filename 46 + * @return String 47 + */ 48 + public function getFilename() { 49 + return $this->filename; 50 + } 51 + 52 + /** 53 + * Retrieve the Attachment Content-Type 54 + * @return String 55 + */ 56 + public function getContentType() { 57 + return $this->content_type; 58 + } 59 + 60 + /** 61 + * Retrieve the Attachment Content-Disposition 62 + * @return String 63 + */ 64 + public function getContentDisposition() { 65 + return $this->content_disposition; 66 + } 67 + 68 + /** 69 + * Retrieve the Attachment Headers 70 + * @return String 71 + */ 72 + public function getHeaders() { 73 + return $this->headers; 74 + } 75 + 76 + /** 77 + * Retrieve the file extension 78 + * @return String 79 + */ 80 + public function getFileExtension() { 81 + if (!$this->extension) { 82 + $ext = substr(strrchr($this->filename, '.'), 1); 83 + if ($ext == 'gz') { 84 + // special case, tar.gz 85 + // todo: other special cases? 86 + $ext = preg_match("/\.tar\.gz$/i", $ext) ? 'tar.gz' : 'gz'; 87 + } 88 + $this->extension = $ext; 89 + } 90 + return $this->extension; 91 + } 92 + 93 + /** 94 + * Read the contents a few bytes at a time until completed 95 + * Once read to completion, it always returns false 96 + * @return String 97 + * @param $bytes Int[optional] 98 + */ 99 + public function read($bytes = 2082) { 100 + return feof($this->stream) ? false : fread($this->stream, $bytes); 101 + } 102 + 103 + /** 104 + * Retrieve the file content in one go 105 + * Once you retreive the content you cannot use MimeMailParser_attachment::read() 106 + * @return String 107 + */ 108 + public function getContent() { 109 + if ($this->content === null) { 110 + fseek($this->stream, 0); 111 + while(($buf = $this->read()) !== false) { 112 + $this->content .= $buf; 113 + } 114 + } 115 + return $this->content; 116 + } 117 + 118 + /** 119 + * Allow the properties 120 + * MimeMailParser_attachment::$name, 121 + * MimeMailParser_attachment::$extension 122 + * to be retrieved as public properties 123 + * @param $name Object 124 + */ 125 + public function __get($name) { 126 + if ($name == 'content') { 127 + return $this->getContent(); 128 + } else if ($name == 'extension') { 129 + return $this->getFileExtension(); 130 + } 131 + return null; 132 + } 133 + 134 + } 135 + 136 + ?>
+19
resources/sql/patches/036.mailkey.sql
··· 1 + ALTER TABLE phabricator_differential.differential_revision 2 + ADD mailKey VARCHAR(40) binary NOT NULL; 3 + 4 + ALTER TABLE phabricator_maniphest.maniphest_task 5 + ADD mailKey VARCHAR(40) binary NOT NULL; 6 + 7 + CREATE TABLE phabricator_metamta.metamta_receivedmail ( 8 + id int unsigned not null primary key auto_increment, 9 + headers longblob not null, 10 + bodies longblob not null, 11 + attachments longblob not null, 12 + relatedPHID varchar(64) binary, 13 + key(relatedPHID), 14 + authorPHID varchar(64) binary, 15 + key(authorPHID), 16 + message longblob, 17 + dateCreated int unsigned not null, 18 + dateModified int unsigned not null 19 + ) engine=innodb;
+54
scripts/mail/mail_handler.php
··· 1 + #!/usr/bin/php 2 + <?php 3 + 4 + /* 5 + * Copyright 2011 Facebook, Inc. 6 + * 7 + * Licensed under the Apache License, Version 2.0 (the "License"); 8 + * you may not use this file except in compliance with the License. 9 + * You may obtain a copy of the License at 10 + * 11 + * http://www.apache.org/licenses/LICENSE-2.0 12 + * 13 + * Unless required by applicable law or agreed to in writing, software 14 + * distributed under the License is distributed on an "AS IS" BASIS, 15 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 + * See the License for the specific language governing permissions and 17 + * limitations under the License. 18 + */ 19 + 20 + $root = dirname(dirname(dirname(__FILE__))); 21 + require_once $root.'/scripts/__init_script__.php'; 22 + require_once $root.'/scripts/__init_env__.php'; 23 + require_once $root.'/externals/mimemailparser/MimeMailParser.class.php'; 24 + 25 + phutil_require_module( 26 + 'phabricator', 27 + 'applications/metamta/storage/receivedmail'); 28 + phutil_require_module( 29 + 'phabricator', 30 + 'applications/files/storage/file'); 31 + 32 + $parser = new MimeMailParser(); 33 + $parser->setText(file_get_contents('php://stdin')); 34 + 35 + $received = new PhabricatorMetaMTAReceivedMail(); 36 + $received->setHeaders($parser->getHeaders()); 37 + $received->setBodies(array( 38 + 'text' => $parser->getMessageBody('text'), 39 + 'html' => $parser->getMessageBody('html'), 40 + )); 41 + 42 + $attachments = array(); 43 + foreach ($received->getAttachments() as $attachment) { 44 + $file = PhabricatorFile::newFromFileData( 45 + $attachment->getContent(), 46 + array( 47 + 'name' => $attachment->getFilename(), 48 + )); 49 + $attachments[] = $file->getPHID(); 50 + } 51 + $received->setAttachments($attachments); 52 + $received->save(); 53 + 54 + $received->processReceivedMail();
+6
src/__phutil_library_map__.php
··· 334 334 'PhabricatorMetaMTAMailingList' => 'applications/metamta/storage/mailinglist', 335 335 'PhabricatorMetaMTAMailingListEditController' => 'applications/metamta/controller/mailinglistedit', 336 336 'PhabricatorMetaMTAMailingListsController' => 'applications/metamta/controller/mailinglists', 337 + 'PhabricatorMetaMTAReceiveController' => 'applications/metamta/controller/receive', 338 + 'PhabricatorMetaMTAReceivedListController' => 'applications/metamta/controller/receivedlist', 339 + 'PhabricatorMetaMTAReceivedMail' => 'applications/metamta/storage/receivedmail', 337 340 'PhabricatorMetaMTASendController' => 'applications/metamta/controller/send', 338 341 'PhabricatorMetaMTAViewController' => 'applications/metamta/controller/view', 339 342 'PhabricatorOAuthDefaultRegistrationController' => 'applications/auth/controller/oauthregistration/default', ··· 743 746 'PhabricatorMetaMTAMailingList' => 'PhabricatorMetaMTADAO', 744 747 'PhabricatorMetaMTAMailingListEditController' => 'PhabricatorMetaMTAController', 745 748 'PhabricatorMetaMTAMailingListsController' => 'PhabricatorMetaMTAController', 749 + 'PhabricatorMetaMTAReceiveController' => 'PhabricatorMetaMTAController', 750 + 'PhabricatorMetaMTAReceivedListController' => 'PhabricatorMetaMTAController', 751 + 'PhabricatorMetaMTAReceivedMail' => 'PhabricatorMetaMTADAO', 746 752 'PhabricatorMetaMTASendController' => 'PhabricatorMetaMTAController', 747 753 'PhabricatorMetaMTAViewController' => 'PhabricatorMetaMTAController', 748 754 'PhabricatorOAuthDefaultRegistrationController' => 'PhabricatorOAuthRegistrationController',
+2
src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php
··· 120 120 'lists/$' => 'PhabricatorMetaMTAMailingListsController', 121 121 'lists/edit/(?:(?P<id>\d+)/)?$' 122 122 => 'PhabricatorMetaMTAMailingListEditController', 123 + 'receive/$' => 'PhabricatorMetaMTAReceiveController', 124 + 'received/$' => 'PhabricatorMetaMTAReceivedListController', 123 125 ), 124 126 125 127 '/login/' => array(
+9
src/applications/differential/storage/revision/DifferentialRevision.php
··· 35 35 protected $attached = array(); 36 36 protected $unsubscribed = array(); 37 37 38 + protected $mailKey; 39 + 38 40 private $relationships; 39 41 private $commits; 40 42 ··· 113 115 return id(new DifferentialDiff())->loadOneWhere( 114 116 'revisionID = %d ORDER BY id DESC LIMIT 1', 115 117 $this->getID()); 118 + } 119 + 120 + public function save() { 121 + if (!$this->getMailKey()) { 122 + $this->mailKey = sha1(Filesystem::readRandomBytes(20)); 123 + } 124 + return parent::save(); 116 125 } 117 126 118 127 public function loadRelationships() {
+1
src/applications/differential/storage/revision/__init__.php
··· 13 13 phutil_require_module('phabricator', 'applications/phid/storage/phid'); 14 14 phutil_require_module('phabricator', 'storage/queryfx'); 15 15 16 + phutil_require_module('phutil', 'filesystem'); 16 17 phutil_require_module('phutil', 'utils'); 17 18 18 19
+9
src/applications/maniphest/storage/task/ManiphestTask.php
··· 29 29 protected $title; 30 30 protected $description; 31 31 32 + protected $mailKey; 33 + 32 34 protected $attached = array(); 33 35 protected $projectPHIDs = array(); 34 36 ··· 54 56 55 57 public function getCCPHIDs() { 56 58 return nonempty($this->ccPHIDs, array()); 59 + } 60 + 61 + public function save() { 62 + if (!$this->mailKey) { 63 + $this->mailKey = sha1(Filesystem::readRandomBytes(20)); 64 + } 65 + return parent::save(); 57 66 } 58 67 59 68 }
+1
src/applications/maniphest/storage/task/__init__.php
··· 10 10 phutil_require_module('phabricator', 'applications/phid/constants'); 11 11 phutil_require_module('phabricator', 'applications/phid/storage/phid'); 12 12 13 + phutil_require_module('phutil', 'filesystem'); 13 14 phutil_require_module('phutil', 'utils'); 14 15 15 16
+4
src/applications/metamta/controller/base/PhabricatorMetaMTAController.php
··· 34 34 'name' => 'Mailing Lists', 35 35 'href' => '/mail/lists/', 36 36 ), 37 + 'received' => array( 38 + 'name' => 'Received', 39 + 'href' => '/mail/received/', 40 + ), 37 41 ), 38 42 idx($data, 'tab')); 39 43 $page->setGlyph("@");
+91
src/applications/metamta/controller/receive/PhabricatorMetaMTAReceiveController.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2011 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + class PhabricatorMetaMTAReceiveController 20 + extends PhabricatorMetaMTAController { 21 + 22 + public function processRequest() { 23 + 24 + $request = $this->getRequest(); 25 + $user = $request->getUser(); 26 + 27 + if ($request->isFormPost()) { 28 + $receiver = PhabricatorMetaMTAReceivedMail::loadReceiverObject( 29 + $request->getStr('obj')); 30 + if (!$receiver) { 31 + throw new Exception("No such task or revision!"); 32 + } 33 + 34 + $hash = PhabricatorMetaMTAReceivedMail::computeMailHash( 35 + $receiver, 36 + $user); 37 + 38 + $received = new PhabricatorMetaMTAReceivedMail(); 39 + $received->setHeaders( 40 + array( 41 + 'to' => $request->getStr('obj').'+'.$user->getID().'+'.$hash.'@', 42 + )); 43 + $received->setBodies( 44 + array( 45 + 'text' => $request->getStr('body'), 46 + )); 47 + $received->save(); 48 + 49 + $received->processReceivedMail(); 50 + 51 + $phid = $receiver->getPHID(); 52 + $handles = id(new PhabricatorObjectHandleData(array($phid))) 53 + ->loadHandles(); 54 + $uri = $handles[$phid]->getURI(); 55 + 56 + return id(new AphrontRedirectResponse())->setURI($uri); 57 + } 58 + 59 + $form = new AphrontFormView(); 60 + $form->setUser($request->getUser()); 61 + $form->setAction('/mail/receive/'); 62 + $form 63 + ->appendChild( 64 + '<p class="aphront-form-instructions">This form will simulate '. 65 + 'sending mail to an object.</p>') 66 + ->appendChild( 67 + id(new AphrontFormTextControl()) 68 + ->setLabel('To') 69 + ->setName('obj') 70 + ->setCaption('e.g. <tt>D1234</tt> or <tt>T1234</tt>')) 71 + ->appendChild( 72 + id(new AphrontFormTextAreaControl()) 73 + ->setLabel('Body') 74 + ->setName('body')) 75 + ->appendChild( 76 + id(new AphrontFormSubmitControl()) 77 + ->setValue('Receive Mail')); 78 + 79 + $panel = new AphrontPanelView(); 80 + $panel->setHeader('Receive Email'); 81 + $panel->appendChild($form); 82 + $panel->setWidth(AphrontPanelView::WIDTH_WIDE); 83 + 84 + return $this->buildStandardPageResponse( 85 + $panel, 86 + array( 87 + 'title' => 'Receive Mail', 88 + )); 89 + } 90 + 91 + }
+22
src/applications/metamta/controller/receive/__init__.php
··· 1 + <?php 2 + /** 3 + * This file is automatically generated. Lint this module to rebuild it. 4 + * @generated 5 + */ 6 + 7 + 8 + 9 + phutil_require_module('phabricator', 'aphront/response/redirect'); 10 + phutil_require_module('phabricator', 'applications/metamta/controller/base'); 11 + phutil_require_module('phabricator', 'applications/metamta/storage/receivedmail'); 12 + phutil_require_module('phabricator', 'applications/phid/handle/data'); 13 + phutil_require_module('phabricator', 'view/form/base'); 14 + phutil_require_module('phabricator', 'view/form/control/submit'); 15 + phutil_require_module('phabricator', 'view/form/control/text'); 16 + phutil_require_module('phabricator', 'view/form/control/textarea'); 17 + phutil_require_module('phabricator', 'view/layout/panel'); 18 + 19 + phutil_require_module('phutil', 'utils'); 20 + 21 + 22 + phutil_require_source('PhabricatorMetaMTAReceiveController.php');
+93
src/applications/metamta/controller/receivedlist/PhabricatorMetaMTAReceivedListController.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2011 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + class PhabricatorMetaMTAReceivedListController 20 + extends PhabricatorMetaMTAController { 21 + 22 + public function processRequest() { 23 + 24 + $request = $this->getRequest(); 25 + 26 + $pager = new AphrontPagerView(); 27 + $pager->setOffset($request->getInt('page')); 28 + $pager->setURI($request->getRequestURI(), 'page'); 29 + 30 + $mails = id(new PhabricatorMetaMTAReceivedMail())->loadAllWhere( 31 + '1 = 1 ORDER BY id DESC LIMIT %d, %d', 32 + $pager->getOffset(), 33 + $pager->getPageSize() + 1); 34 + $mails = $pager->sliceResults($mails); 35 + 36 + $phids = array_merge( 37 + mpull($mails, 'getAuthorPHID'), 38 + mpull($mails, 'getRelatedPHID') 39 + ); 40 + $phids = array_unique(array_filter($phids)); 41 + 42 + $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); 43 + 44 + $rows = array(); 45 + foreach ($mails as $mail) { 46 + $rows[] = array( 47 + $mail->getID(), 48 + date('M jS Y', $mail->getDateCreated()), 49 + date('g:i:s A', $mail->getDateCreated()), 50 + $mail->getAuthorPHID() 51 + ? $handles[$mail->getAuthorPHID()]->renderLink() 52 + : '-', 53 + $mail->getRelatedPHID() 54 + ? $handles[$mail->getRelatedPHID()]->renderLink() 55 + : '-', 56 + phutil_escape_html($mail->getMessage()), 57 + ); 58 + } 59 + 60 + $table = new AphrontTableView($rows); 61 + $table->setHeaders( 62 + array( 63 + 'ID', 64 + 'Date', 65 + 'Time', 66 + 'Author', 67 + 'Object', 68 + 'Message', 69 + )); 70 + $table->setColumnClasses( 71 + array( 72 + null, 73 + null, 74 + 'right', 75 + null, 76 + null, 77 + 'wide', 78 + )); 79 + 80 + $panel = new AphrontPanelView(); 81 + $panel->setHeader('Received Mail'); 82 + $panel->setCreateButton('Test Receiver', '/mail/receive/'); 83 + $panel->appendChild($table); 84 + $panel->appendChild($pager); 85 + 86 + return $this->buildStandardPageResponse( 87 + $panel, 88 + array( 89 + 'title' => 'Received Mail', 90 + 'tab' => 'received', 91 + )); 92 + } 93 + }
+20
src/applications/metamta/controller/receivedlist/__init__.php
··· 1 + <?php 2 + /** 3 + * This file is automatically generated. Lint this module to rebuild it. 4 + * @generated 5 + */ 6 + 7 + 8 + 9 + phutil_require_module('phabricator', 'applications/metamta/controller/base'); 10 + phutil_require_module('phabricator', 'applications/metamta/storage/receivedmail'); 11 + phutil_require_module('phabricator', 'applications/phid/handle/data'); 12 + phutil_require_module('phabricator', 'view/control/pager'); 13 + phutil_require_module('phabricator', 'view/control/table'); 14 + phutil_require_module('phabricator', 'view/layout/panel'); 15 + 16 + phutil_require_module('phutil', 'markup'); 17 + phutil_require_module('phutil', 'utils'); 18 + 19 + 20 + phutil_require_source('PhabricatorMetaMTAReceivedListController.php');
+152
src/applications/metamta/storage/receivedmail/PhabricatorMetaMTAReceivedMail.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2011 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO { 20 + 21 + protected $headers = array(); 22 + protected $bodies = array(); 23 + protected $attachments = array(); 24 + 25 + protected $relatedPHID; 26 + protected $authorPHID; 27 + protected $message; 28 + 29 + public function getConfiguration() { 30 + return array( 31 + self::CONFIG_SERIALIZATION => array( 32 + 'headers' => self::SERIALIZATION_JSON, 33 + 'bodies' => self::SERIALIZATION_JSON, 34 + 'attachments' => self::SERIALIZATION_JSON, 35 + ), 36 + ) + parent::getConfiguration(); 37 + } 38 + 39 + public function processReceivedMail() { 40 + $to = idx($this->headers, 'to'); 41 + 42 + $matches = null; 43 + $ok = preg_match( 44 + '/^((?:D|T)\d+)\+(\d+)\+([a-f0-9]{16})@/', 45 + $to, 46 + $matches); 47 + 48 + if (!$ok) { 49 + return $this->setMessage("Unrecognized 'to' format: {$to}")->save(); 50 + } 51 + 52 + $receiver_name = $matches[1]; 53 + $user_id = $matches[2]; 54 + $hash = $matches[3]; 55 + 56 + $user = id(new PhabricatorUser())->load($user_id); 57 + if (!$user) { 58 + return $this->setMessage("Invalid user '{$user_id}'")->save(); 59 + } 60 + 61 + $this->setAuthorPHID($user->getPHID()); 62 + 63 + $receiver = self::loadReceiverObject($receiver_name); 64 + if (!$receiver) { 65 + return $this->setMessage("Invalid object '{$receiver_name}'")->save(); 66 + } 67 + 68 + $this->setRelatedPHID($receiver->getPHID()); 69 + 70 + $expect_hash = self::computeMailHash($receiver, $user); 71 + if ($expect_hash != $hash) { 72 + return $this->setMessage("Invalid mail hash!")->save(); 73 + } 74 + 75 + // TODO: Move this into the application logic instead. 76 + if ($receiver instanceof ManiphestTask) { 77 + $this->processManiphestMail($receiver, $user); 78 + } else if ($receiver instanceof DifferentialRevision) { 79 + $this->processDifferentialMail($receiver, $user); 80 + } 81 + 82 + $this->setMessage('OK'); 83 + 84 + return $this->save(); 85 + } 86 + 87 + private function processManiphestMail( 88 + ManiphestTask $task, 89 + PhabricatorUser $user) { 90 + 91 + // TODO: implement this 92 + 93 + } 94 + 95 + private function processDifferentialMail( 96 + DifferentialRevision $revision, 97 + PhabricatorUser $user) { 98 + 99 + // TODO: Support actions 100 + 101 + $editor = new DifferentialCommentEditor( 102 + $revision, 103 + $user->getPHID(), 104 + DifferentialAction::ACTION_COMMENT); 105 + $editor->setMessage($this->getCleanTextBody()); 106 + $editor->save(); 107 + 108 + } 109 + 110 + private function getCleanTextBody() { 111 + $body = idx($this->bodies, 'text'); 112 + 113 + // TODO: Detect quoted content and exclude it. 114 + 115 + return $body; 116 + } 117 + 118 + public static function loadReceiverObject($receiver_name) { 119 + if (!$receiver_name) { 120 + return null; 121 + } 122 + 123 + $receiver_type = $receiver_name[0]; 124 + $receiver_id = substr($receiver_name, 1); 125 + 126 + $class_obj = null; 127 + switch ($receiver_type) { 128 + case 'T': 129 + $class_obj = newv('ManiphestTask', array()); 130 + break; 131 + case 'D': 132 + $class_obj = newv('DifferentialRevision', array()); 133 + break; 134 + default: 135 + return null; 136 + } 137 + 138 + return $class_obj->load($receiver_id); 139 + } 140 + 141 + public static function computeMailHash( 142 + $mail_receiver, 143 + PhabricatorUser $user) { 144 + $global_mail_key = PhabricatorEnv::getEnvConfig('phabricator.mail-key'); 145 + $local_mail_key = $mail_receiver->getMailKey(); 146 + 147 + $hash = sha1($local_mail_key.$global_mail_key.$user->getPHID()); 148 + return substr($hash, 0, 16); 149 + } 150 + 151 + 152 + }
+18
src/applications/metamta/storage/receivedmail/__init__.php
··· 1 + <?php 2 + /** 3 + * This file is automatically generated. Lint this module to rebuild it. 4 + * @generated 5 + */ 6 + 7 + 8 + 9 + phutil_require_module('phabricator', 'applications/differential/constants/action'); 10 + phutil_require_module('phabricator', 'applications/differential/editor/comment'); 11 + phutil_require_module('phabricator', 'applications/metamta/storage/base'); 12 + phutil_require_module('phabricator', 'applications/people/storage/user'); 13 + phutil_require_module('phabricator', 'infrastructure/env'); 14 + 15 + phutil_require_module('phutil', 'utils'); 16 + 17 + 18 + phutil_require_source('PhabricatorMetaMTAReceivedMail.php');
+65
src/docs/configuring_inbound_email.diviner
··· 1 + @title Configuring Inbound Email 2 + @group config 3 + 4 + This document contains instructions for configuring inbound email, so users 5 + may update Differential and Maniphest by replying to messages. 6 + 7 + = Preamble = 8 + 9 + This is extremely difficult to configure correctly. This is doubly true if 10 + you use sendmail. 11 + 12 + = Installing Mailparse = 13 + 14 + You need to install the PECL mailparse extension. In theory, you can do that 15 + with: 16 + 17 + $ sudo pecl install mailparse 18 + 19 + You may run into an error like "needs mbstring". If so, try: 20 + 21 + $ sudo yum install php-mbstring # or equivalent 22 + $ sudo pecl install -n mailparse 23 + 24 + If you get a linker error like this: 25 + 26 + COUNTEREXAMPLE 27 + PHP Warning: PHP Startup: Unable to load dynamic library 28 + '/usr/lib64/php/modules/mailparse.so' - /usr/lib64/php/modules/mailparse.so: 29 + undefined symbol: mbfl_name2no_encoding in Unknown on line 0 30 + 31 + ...you need to edit your php.ini file so that mbstring.so is loaded **before** 32 + mailparse.so. This is not the default if you have individual files in 33 + ##php.d/##. 34 + 35 + = Configuring Sendmail = 36 + 37 + Sendmail is very difficult to configure. First, you need to configure it for 38 + your domain so that mail can be delievered correctly. In broad strokes, this 39 + probably means something like this: 40 + 41 + - add an MX record; 42 + - make sendmail listen on external interfaces; 43 + - open up port 25 if necessary (e.g., in your EC2 security policy); 44 + - add your host to /etc/mail/local-host-names; and 45 + - restart sendmail. 46 + 47 + Now, you can actually configure sendmail to deliver to Phabricator. In 48 + ##/etc/aliases##, add an entry like this: 49 + 50 + phabricator: "| PHABRICATOR_ENV=<ENV> /path/to/phabricator/scripts/mail/mail_handler.php" 51 + 52 + ...where <ENV> is the PHABRICATOR_ENV the script should run under. Run 53 + ##sudo newaliases##. Now you likely need to symlink this script into 54 + ##/etc/smrsh/##: 55 + 56 + sudo ln -s /path/to/phabricator/scripts/mail/mail_handler.php /etc/smrsh/ 57 + 58 + Finally, edit ##/etc/mail/virtusertable## and add an entry like this: 59 + 60 + @yourdomain.com phabricator@localhost 61 + 62 + That will forward all mail to @yourdomain.com to the Phabricator processing 63 + script. Run ##sudo /etc/mail/make## or similar and then restart sendmail with 64 + ##sudo /etc/init.d/sendmail restart## 65 +