diff --git a/resources/sql/autopatches/20150129.pastefileapplicationemails.php b/resources/sql/autopatches/20150129.pastefileapplicationemails.php new file mode 100644 index 000000000..68c3dd238 --- /dev/null +++ b/resources/sql/autopatches/20150129.pastefileapplicationemails.php @@ -0,0 +1,38 @@ +<?php + +$key_files = 'metamta.files.public-create-email'; +$key_paste = 'metamta.paste.public-create-email'; +echo "Migrating `$key_files` and `$key_paste` to new application email ". + "infrastructure...\n"; + +$value_files = PhabricatorEnv::getEnvConfigIfExists($key_files); +$files_app = new PhabricatorFilesApplication(); + +if ($value_files) { + try { + PhabricatorMetaMTAApplicationEmail::initializeNewAppEmail( + PhabricatorUser::getOmnipotentUser()) + ->setAddress($value_files) + ->setApplicationPHID($files_app->getPHID()) + ->save(); + } catch (AphrontDuplicateKeyQueryException $ex) { + // already migrated? + } +} + +$value_paste = PhabricatorEnv::getEnvConfigIfExists($key_paste); +$paste_app = new PhabricatorPasteApplication(); + +if ($value_paste) { + try { + PhabricatorMetaMTAApplicationEmail::initializeNewAppEmail( + PhabricatorUser::getOmnipotentUser()) + ->setAddress($value_paste) + ->setApplicationPHID($paste_app->getPHID()) + ->save(); + } catch (AphrontDuplicateKeyQueryException $ex) { + // already migrated? + } +} + +echo "Done.\n"; diff --git a/src/applications/files/application/PhabricatorFilesApplication.php b/src/applications/files/application/PhabricatorFilesApplication.php index cd280534c..cf49fa2fd 100644 --- a/src/applications/files/application/PhabricatorFilesApplication.php +++ b/src/applications/files/application/PhabricatorFilesApplication.php @@ -1,81 +1,96 @@ <?php final class PhabricatorFilesApplication extends PhabricatorApplication { public function getBaseURI() { return '/file/'; } public function getName() { return pht('Files'); } public function getShortDescription() { return pht('Store and Share Files'); } public function getIconName() { return 'files'; } public function getFontIcon() { return 'fa-file'; } public function getTitleGlyph() { return "\xE2\x87\xAA"; } public function getFlavorText() { return pht('Blob store for Pokemon pictures.'); } public function getApplicationGroup() { return self::GROUP_UTILITIES; } public function canUninstall() { return false; } public function getRemarkupRules() { return array( new PhabricatorEmbedFileRemarkupRule(), ); } + public function supportsEmailIntegration() { + return true; + } + + public function getAppEmailBlurb() { + return pht( + 'Send emails with file attachments to these addresses to upload '. + 'files. %s', + phutil_tag( + 'a', + array( + 'href' => $this->getInboundEmailSupportLink(),), + pht('Learn More'))); + } + protected function getCustomCapabilities() { return array( FilesDefaultViewCapability::CAPABILITY => array( 'caption' => pht( 'Default view policy for newly created files.'), ), ); } public function getRoutes() { return array( '/F(?P<id>[1-9]\d*)' => 'PhabricatorFileInfoController', '/file/' => array( '(query/(?P<key>[^/]+)/)?' => 'PhabricatorFileListController', 'upload/' => 'PhabricatorFileUploadController', 'dropupload/' => 'PhabricatorFileDropUploadController', 'compose/' => 'PhabricatorFileComposeController', 'comment/(?P<id>[1-9]\d*)/' => 'PhabricatorFileCommentController', 'delete/(?P<id>[1-9]\d*)/' => 'PhabricatorFileDeleteController', 'edit/(?P<id>[1-9]\d*)/' => 'PhabricatorFileEditController', 'info/(?P<phid>[^/]+)/' => 'PhabricatorFileInfoController', 'data/(?P<key>[^/]+)/(?P<phid>[^/]+)/(?P<token>[^/]+)/.*' => 'PhabricatorFileDataController', 'data/(?P<key>[^/]+)/(?P<phid>[^/]+)/.*' => 'PhabricatorFileDataController', 'proxy/' => 'PhabricatorFileProxyController', 'xform/(?P<transform>[^/]+)/(?P<phid>[^/]+)/(?P<key>[^/]+)/' => 'PhabricatorFileTransformController', 'uploaddialog/' => 'PhabricatorFileUploadDialogController', 'download/(?P<phid>[^/]+)/' => 'PhabricatorFileDialogController', ), ); } } diff --git a/src/applications/files/config/PhabricatorFilesConfigOptions.php b/src/applications/files/config/PhabricatorFilesConfigOptions.php index dec0db998..315ef3c2f 100644 --- a/src/applications/files/config/PhabricatorFilesConfigOptions.php +++ b/src/applications/files/config/PhabricatorFilesConfigOptions.php @@ -1,201 +1,210 @@ <?php final class PhabricatorFilesConfigOptions extends PhabricatorApplicationConfigOptions { public function getName() { return pht('Files'); } public function getDescription() { return pht('Configure files and file storage.'); } public function getOptions() { $viewable_default = array( 'image/jpeg' => 'image/jpeg', 'image/jpg' => 'image/jpg', 'image/png' => 'image/png', 'image/gif' => 'image/gif', 'text/plain' => 'text/plain; charset=utf-8', 'text/x-diff' => 'text/plain; charset=utf-8', // ".ico" favicon files, which have mime type diversity. See: // http://en.wikipedia.org/wiki/ICO_(file_format)#MIME_type 'image/x-ico' => 'image/x-icon', 'image/x-icon' => 'image/x-icon', 'image/vnd.microsoft.icon' => 'image/x-icon', 'audio/x-wav' => 'audio/x-wav', 'application/ogg' => 'application/ogg', 'audio/mpeg' => 'audio/mpeg', ); $image_default = array( 'image/jpeg' => true, 'image/jpg' => true, 'image/png' => true, 'image/gif' => true, 'image/x-ico' => true, 'image/x-icon' => true, 'image/vnd.microsoft.icon' => true, ); $audio_default = array( 'audio/x-wav' => true, 'application/ogg' => true, 'audio/mpeg' => true, ); // largely lifted from http://en.wikipedia.org/wiki/Internet_media_type $icon_default = array( // audio file icon 'audio/basic' => 'fa-file-audio-o', 'audio/L24' => 'fa-file-audio-o', 'audio/mp4' => 'fa-file-audio-o', 'audio/mpeg' => 'fa-file-audio-o', 'audio/ogg' => 'fa-file-audio-o', 'audio/vorbis' => 'fa-file-audio-o', 'audio/vnd.rn-realaudio' => 'fa-file-audio-o', 'audio/vnd.wave' => 'fa-file-audio-o', 'audio/webm' => 'fa-file-audio-o', // movie file icon 'video/mpeg' => 'fa-file-movie-o', 'video/mp4' => 'fa-file-movie-o', 'video/ogg' => 'fa-file-movie-o', 'video/quicktime' => 'fa-file-movie-o', 'video/webm' => 'fa-file-movie-o', 'video/x-matroska' => 'fa-file-movie-o', 'video/x-ms-wmv' => 'fa-file-movie-o', 'video/x-flv' => 'fa-file-movie-o', // pdf file icon 'application/pdf' => 'fa-file-pdf-o', // zip file icon 'application/zip' => 'fa-file-zip-o', // msword icon 'application/msword' => 'fa-file-word-o', // msexcel 'application/vnd.ms-excel' => 'fa-file-excel-o', // mspowerpoint 'application/vnd.ms-powerpoint' => 'fa-file-powerpoint-o', ) + array_fill_keys(array_keys($image_default), 'fa-file-image-o'); return array( $this->newOption('files.viewable-mime-types', 'wild', $viewable_default) ->setSummary( pht('Configure which MIME types are viewable in the browser.')) ->setDescription( pht( 'Configure which uploaded file types may be viewed directly '. 'in the browser. Other file types will be downloaded instead '. 'of displayed. This is mainly a usability consideration, since '. 'browsers tend to freak out when viewing enormous binary files.'. "\n\n". 'The keys in this map are vieweable MIME types; the values are '. 'the MIME types they are delivered as when they are viewed in '. 'the browser.')), $this->newOption('files.image-mime-types', 'set', $image_default) ->setSummary(pht('Configure which MIME types are images.')) ->setDescription( pht( 'List of MIME types which can be used as the `src` for an '. '`<img />` tag.')), $this->newOption('files.audio-mime-types', 'set', $audio_default) ->setSummary(pht('Configure which MIME types are audio.')) ->setDescription( pht( 'List of MIME types which can be used to render an '. '`<audio />` tag.')), $this->newOption('files.icon-mime-types', 'wild', $icon_default) ->setSummary(pht('Configure which MIME types map to which icons.')) ->setDescription( pht( 'Map of MIME type to icon name. MIME types which can not be '. 'found default to icon `doc_files`.')), $this->newOption('storage.mysql-engine.max-size', 'int', 1000000) ->setSummary( pht( 'Configure the largest file which will be put into the MySQL '. 'storage engine.')), $this->newOption('storage.local-disk.path', 'string', null) ->setLocked(true) ->setSummary(pht('Local storage disk path.')) ->setDescription( pht( "Phabricator provides a local disk storage engine, which just ". "writes files to some directory on local disk. The webserver ". "must have read/write permissions on this directory. This is ". "straightforward and suitable for most installs, but will not ". "scale past one web frontend unless the path is actually an NFS ". "mount, since you'll end up with some of the files written to ". "each web frontend and no way for them to share. To use the ". "local disk storage engine, specify the path to a directory ". "here. To disable it, specify null.")), $this->newOption('storage.s3.bucket', 'string', null) ->setSummary(pht('Amazon S3 bucket.')) ->setDescription( pht( "Set this to a valid Amazon S3 bucket to store files there. You ". "must also configure S3 access keys in the 'Amazon Web Services' ". "group.")), $this->newOption( 'storage.engine-selector', 'class', 'PhabricatorDefaultFileStorageEngineSelector') ->setBaseClass('PhabricatorFileStorageEngineSelector') ->setSummary(pht('Storage engine selector.')) ->setDescription( pht( 'Phabricator uses a storage engine selector to choose which '. 'storage engine to use when writing file data. If you add new '. 'storage engines or want to provide very custom rules (e.g., '. 'write images to one storage engine and other files to a '. 'different one), you can provide an alternate implementation '. 'here. The default engine will use choose MySQL, Local Disk, and '. 'S3, in that order, if they have valid configurations above and '. 'a file fits within configured limits.')), $this->newOption('storage.upload-size-limit', 'string', null) ->setSummary( pht('Limit to users in interfaces which allow uploading.')) ->setDescription( pht( "Set the size of the largest file a user may upload. This is ". "used to render text like 'Maximum file size: 10MB' on ". "interfaces where users can upload files, and files larger than ". "this size will be rejected. \n\n". "NOTE: **Setting this to a large size is NOT sufficient to ". "allow users to upload large files. You must also configure a ". "number of other settings.** To configure file upload limits, ". "consult the article 'Configuring File Upload Limits' in the ". "documentation. Once you've configured some limit across all ". "levels of the server, you can set this limit to an appropriate ". "value and the UI will then reflect the actual configured ". "limit.\n\n". "Specify this limit in bytes, or using a 'K', 'M', or 'G' ". "suffix.")) ->addExample('10M', pht('Allow Uploads 10MB or Smaller')), $this->newOption( 'metamta.files.public-create-email', 'string', null) - ->setDescription(pht('Allow uploaded files via email.')), + ->setLocked(true) + ->setLockedMessage(pht( + 'This configuration is deprecated. See description for details.')) + ->setSummary(pht('DEPRECATED - Allow uploaded files via email.')) + ->setDescription( + pht( + 'This config has been deprecated in favor of [[ '. + '/applications/view/PhabricatorFilesApplication/ | '. + 'application settings ]], which allow for multiple email '. + 'addresses and other functionality.')), $this->newOption( 'metamta.files.subject-prefix', 'string', '[File]') ->setDescription(pht('Subject prefix for Files email.')), $this->newOption('files.enable-imagemagick', 'bool', false) ->setBoolOptions( array( pht('Enable'), pht('Disable'), ))->setDescription( pht("This option will enable animated gif images". "to be set as profile pictures. The 'convert' binary ". "should be available to the webserver for this to work")), ); } } diff --git a/src/applications/files/mail/FileCreateMailReceiver.php b/src/applications/files/mail/FileCreateMailReceiver.php index 0c29aad61..9f0fcc157 100644 --- a/src/applications/files/mail/FileCreateMailReceiver.php +++ b/src/applications/files/mail/FileCreateMailReceiver.php @@ -1,68 +1,57 @@ <?php final class FileCreateMailReceiver extends PhabricatorMailReceiver { public function isEnabled() { $app_class = 'PhabricatorFilesApplication'; return PhabricatorApplication::isClassInstalled($app_class); } public function canAcceptMail(PhabricatorMetaMTAReceivedMail $mail) { - $config_key = 'metamta.files.public-create-email'; - $create_address = PhabricatorEnv::getEnvConfig($config_key); - if (!$create_address) { - return false; - } - - foreach ($mail->getToAddresses() as $to_address) { - if ($this->matchAddresses($create_address, $to_address)) { - return true; - } - } - - return false; + $files_app = new PhabricatorFilesApplication(); + return $this->canAcceptApplicationMail($files_app, $mail); } protected function processReceivedMail( PhabricatorMetaMTAReceivedMail $mail, PhabricatorUser $sender) { $attachment_phids = $mail->getAttachments(); if (empty($attachment_phids)) { throw new PhabricatorMetaMTAReceivedMailProcessingException( MetaMTAReceivedMailStatus::STATUS_UNHANDLED_EXCEPTION, 'Ignoring email to create files that did not include attachments.'); } $first_phid = head($attachment_phids); $mail->setRelatedPHID($first_phid); $attachment_count = count($attachment_phids); if ($attachment_count > 1) { $subject = pht('You successfully uploaded %d files.', $attachment_count); } else { $subject = pht('You successfully uploaded a file.'); } $subject_prefix = PhabricatorEnv::getEnvConfig('metamta.files.subject-prefix'); $file_uris = array(); foreach ($attachment_phids as $phid) { $file_uris[] = PhabricatorEnv::getProductionURI('/file/info/'.$phid.'/'); } $body = new PhabricatorMetaMTAMailBody(); $body->addRawSection($subject); $body->addTextSection(pht('FILE LINKS'), implode("\n", $file_uris)); id(new PhabricatorMetaMTAMail()) ->addTos(array($sender->getPHID())) ->setSubject($subject) ->setSubjectPrefix($subject_prefix) ->setFrom($sender->getPHID()) ->setRelatedPHID($first_phid) ->setBody($body->render()) ->saveAndSend(); } } diff --git a/src/applications/maniphest/mail/ManiphestCreateMailReceiver.php b/src/applications/maniphest/mail/ManiphestCreateMailReceiver.php index f654ea9a0..d28beb3b7 100644 --- a/src/applications/maniphest/mail/ManiphestCreateMailReceiver.php +++ b/src/applications/maniphest/mail/ManiphestCreateMailReceiver.php @@ -1,52 +1,37 @@ <?php final class ManiphestCreateMailReceiver extends PhabricatorMailReceiver { public function isEnabled() { $app_class = 'PhabricatorManiphestApplication'; return PhabricatorApplication::isClassInstalled($app_class); } public function canAcceptMail(PhabricatorMetaMTAReceivedMail $mail) { $maniphest_app = new PhabricatorManiphestApplication(); - $application_emails = id(new PhabricatorMetaMTAApplicationEmailQuery()) - ->setViewer($this->getViewer()) - ->withApplicationPHIDs(array($maniphest_app->getPHID())) - ->execute(); - - foreach ($mail->getToAddresses() as $to_address) { - foreach ($application_emails as $application_email) { - $create_address = $application_email->getAddress(); - if ($this->matchAddresses($create_address, $to_address)) { - $this->setApplicationEmail($application_email); - return true; - } - } - } - - return false; + return $this->canAcceptApplicationMail($maniphest_app, $mail); } protected function processReceivedMail( PhabricatorMetaMTAReceivedMail $mail, PhabricatorUser $sender) { $task = ManiphestTask::initializeNewTask($sender); $task->setOriginalEmailSource($mail->getHeader('From')); $handler = PhabricatorEnv::newObjectFromConfig( 'metamta.maniphest.reply-handler'); $handler->setMailReceiver($task); $handler->setActor($sender); $handler->setExcludeMailRecipientPHIDs( $mail->loadExcludeMailRecipientPHIDs()); if ($this->getApplicationEmail()) { $handler->setApplicationEmail($this->getApplicationEmail()); } $handler->processEmail($mail); $mail->setRelatedPHID($task->getPHID()); } } diff --git a/src/applications/metamta/receiver/PhabricatorMailReceiver.php b/src/applications/metamta/receiver/PhabricatorMailReceiver.php index ee309ffe0..b01a83f5a 100644 --- a/src/applications/metamta/receiver/PhabricatorMailReceiver.php +++ b/src/applications/metamta/receiver/PhabricatorMailReceiver.php @@ -1,250 +1,271 @@ <?php abstract class PhabricatorMailReceiver { private $applicationEmail; public function setApplicationEmail( PhabricatorMetaMTAApplicationEmail $email) { $this->applicationEmail = $email; return $this; } public function getApplicationEmail() { return $this->applicationEmail; } abstract public function isEnabled(); abstract public function canAcceptMail(PhabricatorMetaMTAReceivedMail $mail); + final protected function canAcceptApplicationMail( + PhabricatorApplication $app, + PhabricatorMetaMTAReceivedMail $mail) { + + $application_emails = id(new PhabricatorMetaMTAApplicationEmailQuery()) + ->setViewer($this->getViewer()) + ->withApplicationPHIDs(array($app->getPHID())) + ->execute(); + + foreach ($mail->getToAddresses() as $to_address) { + foreach ($application_emails as $application_email) { + $create_address = $application_email->getAddress(); + if ($this->matchAddresses($create_address, $to_address)) { + $this->setApplicationEmail($application_email); + return true; + } + } + } + + return false; + } abstract protected function processReceivedMail( PhabricatorMetaMTAReceivedMail $mail, PhabricatorUser $sender); final public function receiveMail( PhabricatorMetaMTAReceivedMail $mail, PhabricatorUser $sender) { $this->processReceivedMail($mail, $sender); } public function getViewer() { return PhabricatorUser::getOmnipotentUser(); } public function validateSender( PhabricatorMetaMTAReceivedMail $mail, PhabricatorUser $sender) { $failure_reason = null; if ($sender->getIsDisabled()) { $failure_reason = pht( 'Your account (%s) is disabled, so you can not interact with '. 'Phabricator over email.', $sender->getUsername()); } else if ($sender->getIsStandardUser()) { if (!$sender->getIsApproved()) { $failure_reason = pht( 'Your account (%s) has not been approved yet. You can not interact '. 'with Phabricator over email until your account is approved.', $sender->getUsername()); } else if (PhabricatorUserEmail::isEmailVerificationRequired() && !$sender->getIsEmailVerified()) { $failure_reason = pht( 'You have not verified the email address for your account (%s). '. 'You must verify your email address before you can interact '. 'with Phabricator over email.', $sender->getUsername()); } } if ($failure_reason) { throw new PhabricatorMetaMTAReceivedMailProcessingException( MetaMTAReceivedMailStatus::STATUS_DISABLED_SENDER, $failure_reason); } } /** * Identifies the sender's user account for a piece of received mail. Note * that this method does not validate that the sender is who they say they * are, just that they've presented some credential which corresponds to a * recognizable user. */ public function loadSender(PhabricatorMetaMTAReceivedMail $mail) { $raw_from = $mail->getHeader('From'); $from = self::getRawAddress($raw_from); $reasons = array(); // Try to find a user with this email address. $user = PhabricatorUser::loadOneWithEmailAddress($from); if ($user) { return $user; } else { $reasons[] = pht( 'This email was sent from "%s", but that address is not recognized by '. 'Phabricator and does not correspond to any known user account.', $raw_from); } // If we missed on "From", try "Reply-To" if we're configured for it. $raw_reply_to = $mail->getHeader('Reply-To'); if (strlen($raw_reply_to)) { $reply_to_key = 'metamta.insecure-auth-with-reply-to'; $allow_reply_to = PhabricatorEnv::getEnvConfig($reply_to_key); if ($allow_reply_to) { $reply_to = self::getRawAddress($raw_reply_to); $user = PhabricatorUser::loadOneWithEmailAddress($reply_to); if ($user) { return $user; } else { $reasons[] = pht( 'Phabricator is configured to authenticate users using the '. '"Reply-To" header, but the reply address ("%s") on this '. 'message does not correspond to any known user account.', $raw_reply_to); } } else { $reasons[] = pht( '(Phabricator is not configured to authenticate users using the '. '"Reply-To" header, so it was ignored.)'); } } // If we don't know who this user is, load or create an external user // account for them if we're configured for it. $email_key = 'phabricator.allow-email-users'; $allow_email_users = PhabricatorEnv::getEnvConfig($email_key); if ($allow_email_users) { $from_obj = new PhutilEmailAddress($from); $xuser = id(new PhabricatorExternalAccountQuery()) ->setViewer($this->getViewer()) ->withAccountTypes(array('email')) ->withAccountDomains(array($from_obj->getDomainName(), 'self')) ->withAccountIDs(array($from_obj->getAddress())) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->loadOneOrCreate(); return $xuser->getPhabricatorUser(); } else { $reasons[] = pht( 'Phabricator is also not configured to allow unknown external users '. 'to send mail to the system using just an email address.'); $reasons[] = pht( 'To interact with Phabricator, add this address ("%s") to your '. 'account.', $raw_from); } if ($this->getApplicationEmail()) { $application_email = $this->getApplicationEmail(); $default_user_phid = $application_email->getConfigValue( PhabricatorMetaMTAApplicationEmail::CONFIG_DEFAULT_AUTHOR); if ($default_user_phid) { $user = id(new PhabricatorUser())->loadOneWhere( 'phid = %s', $default_user_phid); if ($user) { return $user; } } $reasons[] = pht( "Phabricator is misconfigured, the application email ". "'%s' is set to user '%s' but that user does not exist.", $application_email->getAddress(), $default_user_phid); } $reasons = implode("\n\n", $reasons); throw new PhabricatorMetaMTAReceivedMailProcessingException( MetaMTAReceivedMailStatus::STATUS_UNKNOWN_SENDER, $reasons); } /** * Determine if two inbound email addresses are effectively identical. This * method strips and normalizes addresses so that equivalent variations are * correctly detected as identical. For example, these addresses are all * considered to match one another: * * "Abraham Lincoln" <alincoln@example.com> * alincoln@example.com * <ALincoln@example.com> * "Abraham" <phabricator+ALINCOLN@EXAMPLE.COM> # With configured prefix. * * @param string Email address. * @param string Another email address. * @return bool True if addresses match. */ public static function matchAddresses($u, $v) { $u = self::getRawAddress($u); $v = self::getRawAddress($v); $u = self::stripMailboxPrefix($u); $v = self::stripMailboxPrefix($v); return ($u === $v); } /** * Strip a global mailbox prefix from an address if it is present. Phabricator * can be configured to prepend a prefix to all reply addresses, which can * make forwarding rules easier to write. A prefix looks like: * * example@phabricator.example.com # No Prefix * phabricator+example@phabricator.example.com # Prefix "phabricator" * * @param string Email address, possibly with a mailbox prefix. * @return string Email address with any prefix stripped. */ public static function stripMailboxPrefix($address) { $address = id(new PhutilEmailAddress($address))->getAddress(); $prefix_key = 'metamta.single-reply-handler-prefix'; $prefix = PhabricatorEnv::getEnvConfig($prefix_key); $len = strlen($prefix); if ($len) { $prefix = $prefix.'+'; $len = $len + 1; } if ($len) { if (!strncasecmp($address, $prefix, $len)) { $address = substr($address, strlen($prefix)); } } return $address; } /** * Reduce an email address to its canonical form. For example, an adddress * like: * * "Abraham Lincoln" < ALincoln@example.com > * * ...will be reduced to: * * alincoln@example.com * * @param string Email address in noncanonical form. * @return string Canonical email address. */ public static function getRawAddress($address) { $address = id(new PhutilEmailAddress($address))->getAddress(); return trim(phutil_utf8_strtolower($address)); } } diff --git a/src/applications/paste/application/PhabricatorPasteApplication.php b/src/applications/paste/application/PhabricatorPasteApplication.php index edc1c63c2..6de712a92 100644 --- a/src/applications/paste/application/PhabricatorPasteApplication.php +++ b/src/applications/paste/application/PhabricatorPasteApplication.php @@ -1,75 +1,89 @@ <?php final class PhabricatorPasteApplication extends PhabricatorApplication { public function getName() { return pht('Paste'); } public function getBaseURI() { return '/paste/'; } public function getIconName() { return 'paste'; } public function getFontIcon() { return 'fa-paste'; } public function getTitleGlyph() { return "\xE2\x9C\x8E"; } public function getApplicationGroup() { return self::GROUP_UTILITIES; } public function getShortDescription() { return pht('Share Text Snippets'); } public function getRemarkupRules() { return array( new PhabricatorPasteRemarkupRule(), ); } public function getRoutes() { return array( '/P(?P<id>[1-9]\d*)(?:\$(?P<lines>\d+(?:-\d+)?))?' => 'PhabricatorPasteViewController', '/paste/' => array( '(query/(?P<queryKey>[^/]+)/)?' => 'PhabricatorPasteListController', 'create/' => 'PhabricatorPasteEditController', 'edit/(?P<id>[1-9]\d*)/' => 'PhabricatorPasteEditController', 'comment/(?P<id>[1-9]\d*)/' => 'PhabricatorPasteCommentController', ), ); } + public function supportsEmailIntegration() { + return true; + } + + public function getAppEmailBlurb() { + return pht( + 'Send email to these addresses to create pastes. %s', + phutil_tag( + 'a', + array( + 'href' => $this->getInboundEmailSupportLink(),), + pht('Learn More'))); + } + protected function getCustomCapabilities() { return array( PasteDefaultViewCapability::CAPABILITY => array( 'caption' => pht('Default view policy for newly created pastes.'), ), PasteDefaultEditCapability::CAPABILITY => array( 'caption' => pht('Default edit policy for newly created pastes.'), ), ); } public function getQuickCreateItems(PhabricatorUser $viewer) { $items = array(); $item = id(new PHUIListItemView()) ->setName(pht('Paste')) ->setIcon('fa-clipboard') ->setHref($this->getBaseURI().'create/'); $items[] = $item; return $items; } } diff --git a/src/applications/paste/config/PhabricatorPasteConfigOptions.php b/src/applications/paste/config/PhabricatorPasteConfigOptions.php index c58f5b139..81b90d3c3 100644 --- a/src/applications/paste/config/PhabricatorPasteConfigOptions.php +++ b/src/applications/paste/config/PhabricatorPasteConfigOptions.php @@ -1,29 +1,38 @@ <?php final class PhabricatorPasteConfigOptions extends PhabricatorApplicationConfigOptions { public function getName() { return pht('Paste'); } public function getDescription() { return pht('Configure Paste.'); } public function getOptions() { return array( $this->newOption( 'metamta.paste.public-create-email', 'string', null) - ->setDescription(pht('Allow creating pastes via email.')), + ->setLocked(true) + ->setLockedMessage(pht( + 'This configuration is deprecated. See description for details.')) + ->setSummary(pht('DEPRECATED - Allow creating pastes via email.')) + ->setDescription( + pht( + 'This config has been deprecated in favor of [[ '. + '/applications/view/PhabricatorPasteApplication/ | '. + 'application settings ]], which allow for multiple email '. + 'addresses and other functionality.')), $this->newOption( 'metamta.paste.subject-prefix', 'string', '[Paste]') ->setDescription(pht('Subject prefix for Paste email.')), ); } } diff --git a/src/applications/paste/mail/PasteCreateMailReceiver.php b/src/applications/paste/mail/PasteCreateMailReceiver.php index 02decaa26..d0c50d5fc 100644 --- a/src/applications/paste/mail/PasteCreateMailReceiver.php +++ b/src/applications/paste/mail/PasteCreateMailReceiver.php @@ -1,88 +1,77 @@ <?php final class PasteCreateMailReceiver extends PhabricatorMailReceiver { public function isEnabled() { $app_class = 'PhabricatorPasteApplication'; return PhabricatorApplication::isClassInstalled($app_class); } public function canAcceptMail(PhabricatorMetaMTAReceivedMail $mail) { - $config_key = 'metamta.paste.public-create-email'; - $create_address = PhabricatorEnv::getEnvConfig($config_key); - if (!$create_address) { - return false; - } - - foreach ($mail->getToAddresses() as $to_address) { - if ($this->matchAddresses($create_address, $to_address)) { - return true; - } - } - - return false; + $paste_app = new PhabricatorPasteApplication(); + return $this->canAcceptApplicationMail($paste_app, $mail); } protected function processReceivedMail( PhabricatorMetaMTAReceivedMail $mail, PhabricatorUser $sender) { $title = $mail->getSubject(); if (!$title) { $title = pht('Email Paste'); } $file = PhabricatorPasteEditor::initializeFileForPaste( $sender, $title, $mail->getCleanTextBody()); $xactions = array(); $xactions[] = id(new PhabricatorPasteTransaction()) ->setTransactionType(PhabricatorPasteTransaction::TYPE_CONTENT) ->setNewValue($file->getPHID()); $xactions[] = id(new PhabricatorPasteTransaction()) ->setTransactionType(PhabricatorPasteTransaction::TYPE_TITLE) ->setNewValue($title); $xactions[] = id(new PhabricatorPasteTransaction()) ->setTransactionType(PhabricatorPasteTransaction::TYPE_LANGUAGE) ->setNewValue(''); // auto-detect $paste = PhabricatorPaste::initializeNewPaste($sender); $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_EMAIL, array( 'id' => $mail->getID(), )); $editor = id(new PhabricatorPasteEditor()) ->setActor($sender) ->setContentSource($content_source) ->setContinueOnNoEffect(true); $xactions = $editor->applyTransactions($paste, $xactions); $mail->setRelatedPHID($paste->getPHID()); $subject_prefix = PhabricatorEnv::getEnvConfig('metamta.paste.subject-prefix'); $subject = pht('You successfully created a paste.'); $paste_uri = PhabricatorEnv::getProductionURI($paste->getURI()); $body = new PhabricatorMetaMTAMailBody(); $body->addRawSection($subject); $body->addTextSection(pht('PASTE LINK'), $paste_uri); id(new PhabricatorMetaMTAMail()) ->addTos(array($sender->getPHID())) ->setSubject($subject) ->setSubjectPrefix($subject_prefix) ->setFrom($sender->getPHID()) ->setRelatedPHID($paste->getPHID()) ->setBody($body->render()) ->saveAndSend(); } }