Page MenuHomestyx hydra

No OneTemporary

diff --git a/resources/celerity/map.php b/resources/celerity/map.php
index 1846c7d1a3..3864b6d6c9 100644
--- a/resources/celerity/map.php
+++ b/resources/celerity/map.php
@@ -1,2191 +1,2191 @@
<?php
/**
* This file is automatically generated. Use 'bin/celerity map' to rebuild it.
* @generated
*/
return array(
'names' =>
array(
'core.pkg.css' => 'c88bd7c6',
'core.pkg.js' => '417722ff',
'darkconsole.pkg.js' => 'ca8671ce',
'differential.pkg.css' => '12c11318',
'differential.pkg.js' => '11a5b750',
'diffusion.pkg.css' => '3783278d',
'diffusion.pkg.js' => '5b4010f4',
'javelin.pkg.js' => '0452e69d',
'maniphest.pkg.css' => 'f1887d71',
'maniphest.pkg.js' => '2fe8af22',
'rsrc/css/aphront/aphront-bars.css' => '231ac33c',
'rsrc/css/aphront/context-bar.css' => '1c3b0529',
'rsrc/css/aphront/dark-console.css' => '6378ef3d',
'rsrc/css/aphront/dialog-view.css' => 'c01d24b4',
'rsrc/css/aphront/error-view.css' => '9f1d5518',
'rsrc/css/aphront/lightbox-attachment.css' => '7acac05d',
'rsrc/css/aphront/list-filter-view.css' => 'ef989c67',
'rsrc/css/aphront/multi-column.css' => '12f65921',
'rsrc/css/aphront/notification.css' => '6901121e',
'rsrc/css/aphront/pager-view.css' => '2e3539af',
'rsrc/css/aphront/panel-view.css' => '5846dfa2',
'rsrc/css/aphront/phabricator-nav-view.css' => '80e60fc1',
'rsrc/css/aphront/request-failure-view.css' => 'da14df31',
'rsrc/css/aphront/table-view.css' => 'de599000',
'rsrc/css/aphront/tokenizer.css' => '36903077',
'rsrc/css/aphront/tooltip.css' => '9c90229d',
'rsrc/css/aphront/transaction.css' => 'ce491938',
'rsrc/css/aphront/two-column.css' => '16ab3ad2',
'rsrc/css/aphront/typeahead.css' => '271456a1',
'rsrc/css/application/auth/auth.css' => '1e655982',
'rsrc/css/application/base/main-menu-view.css' => 'd36e0c11',
'rsrc/css/application/base/notification-menu.css' => 'fc9a363c',
'rsrc/css/application/base/phabricator-application-launch-view.css' => 'd290ba21',
'rsrc/css/application/base/standard-page-view.css' => '517cdfb1',
'rsrc/css/application/chatlog/chatlog.css' => '852140ff',
'rsrc/css/application/config/config-options.css' => '7fedf08b',
'rsrc/css/application/config/config-template.css' => '25d446d6',
'rsrc/css/application/config/setup-issue.css' => '69e640e7',
'rsrc/css/application/conpherence/menu.css' => '561348ac',
'rsrc/css/application/conpherence/message-pane.css' => '2aedca89',
'rsrc/css/application/conpherence/notification.css' => '403cf598',
'rsrc/css/application/conpherence/update.css' => '1099a660',
'rsrc/css/application/conpherence/widget-pane.css' => '87b12e0c',
'rsrc/css/application/contentsource/content-source-view.css' => '4b8b05d4',
'rsrc/css/application/countdown/timer.css' => '86b7b0a0',
'rsrc/css/application/diff/inline-comment-summary.css' => '14a91639',
'rsrc/css/application/differential/add-comment.css' => 'c478bcaa',
'rsrc/css/application/differential/changeset-view.css' => '1570a1ff',
'rsrc/css/application/differential/core.css' => '7ac3cabc',
'rsrc/css/application/differential/results-table.css' => '239924f9',
'rsrc/css/application/differential/revision-comment.css' => '48186045',
'rsrc/css/application/differential/revision-history.css' => '0e8eb855',
'rsrc/css/application/differential/revision-list.css' => 'f3c47d33',
'rsrc/css/application/differential/table-of-contents.css' => '6bf8e1d2',
'rsrc/css/application/diffusion/commit-view.css' => '92d1e8f9',
'rsrc/css/application/diffusion/diffusion-icons.css' => '384a0f7d',
'rsrc/css/application/diffusion/diffusion-source.css' => '66fdf661',
'rsrc/css/application/directory/phabricator-jump-nav.css' => 'f0c5e726',
'rsrc/css/application/feed/feed.css' => '0d17c209',
'rsrc/css/application/files/global-drag-and-drop.css' => '697324ad',
'rsrc/css/application/flag/flag.css' => '5337623f',
'rsrc/css/application/harbormaster/harbormaster.css' => 'cec833b7',
'rsrc/css/application/herald/herald-test.css' => '2b7d0f54',
'rsrc/css/application/herald/herald.css' => '59d48f01',
'rsrc/css/application/maniphest/batch-editor.css' => '8f380ebc',
'rsrc/css/application/maniphest/report.css' => '6fc16517',
'rsrc/css/application/maniphest/task-edit.css' => '8e23031b',
'rsrc/css/application/maniphest/task-summary.css' => '6df1a768',
'rsrc/css/application/objectselector/object-selector.css' => '029a133d',
'rsrc/css/application/owners/owners-path-editor.css' => '2f00933b',
'rsrc/css/application/paste/paste.css' => 'aa1767d1',
'rsrc/css/application/people/people-profile.css' => 'ba7b2762',
'rsrc/css/application/phame/phame.css' => '450826e1',
'rsrc/css/application/pholio/pholio-edit.css' => 'b9e59b6d',
'rsrc/css/application/pholio/pholio-inline-comments.css' => '52be33f0',
'rsrc/css/application/pholio/pholio.css' => '2fa97dbe',
'rsrc/css/application/phortune/phortune-credit-card-form.css' => 'b25b4beb',
'rsrc/css/application/phrequent/phrequent.css' => 'ffc185ad',
'rsrc/css/application/phriction/phriction-document-css.css' => '7d7f0071',
'rsrc/css/application/policy/policy-edit.css' => '05cca26a',
'rsrc/css/application/policy/policy.css' => '957ea14c',
'rsrc/css/application/ponder/comments.css' => '6cdccea7',
'rsrc/css/application/ponder/feed.css' => 'e62615b6',
'rsrc/css/application/ponder/post.css' => 'ebab8a70',
'rsrc/css/application/ponder/vote.css' => '8ed6ed8b',
'rsrc/css/application/profile/profile-view.css' => '33e6f703',
'rsrc/css/application/projects/project-tag.css' => '095c9404',
'rsrc/css/application/releeph/releeph-core.css' => '9b3c5733',
'rsrc/css/application/releeph/releeph-preview-branch.css' => 'b7a6f4a5',
'rsrc/css/application/releeph/releeph-request-differential-create-dialog.css' => '8d8b92cd',
'rsrc/css/application/releeph/releeph-request-typeahead.css' => '667a48ae',
'rsrc/css/application/search/search-results.css' => 'f240504c',
'rsrc/css/application/settings/settings.css' => 'ea8f5915',
'rsrc/css/application/slowvote/slowvote.css' => '266df6a1',
'rsrc/css/application/subscriptions/subscribers-list.css' => '5bb30c78',
'rsrc/css/application/tokens/tokens.css' => '5f7bca25',
'rsrc/css/application/uiexample/example.css' => '81adeffe',
'rsrc/css/core/core.css' => 'da26ddb2',
'rsrc/css/core/remarkup.css' => '0923dbd6',
'rsrc/css/core/syntax.css' => '3c18c1cb',
'rsrc/css/core/z-index.css' => '0d89d53c',
'rsrc/css/diviner/diviner-shared.css' => '38813222',
'rsrc/css/font/font-awesome.css' => '62bc244d',
'rsrc/css/font/font-glyphicons-halflings.css' => '0c69dbc6',
'rsrc/css/font/font-source-sans-pro.css' => '91d53463',
'rsrc/css/font/phui-font-icon-base.css' => 'cc5465ba',
'rsrc/css/layout/phabricator-action-header-view.css' => 'c14dfc57',
'rsrc/css/layout/phabricator-action-list-view.css' => '81383e25',
'rsrc/css/layout/phabricator-crumbs-view.css' => '2d9db584',
'rsrc/css/layout/phabricator-filetree-view.css' => 'a8c86ace',
'rsrc/css/layout/phabricator-hovercard-view.css' => '46a13cf0',
'rsrc/css/layout/phabricator-side-menu-view.css' => '503699d0',
'rsrc/css/layout/phabricator-source-code-view.css' => '62a99814',
'rsrc/css/phui/calendar/phui-calendar-day.css' => 'de035c8a',
'rsrc/css/phui/calendar/phui-calendar-list.css' => 'c1d0ca59',
'rsrc/css/phui/calendar/phui-calendar-month.css' => 'a92e47d2',
'rsrc/css/phui/calendar/phui-calendar.css' => '5e1ad989',
'rsrc/css/phui/phui-box.css' => '7b3a2eed',
'rsrc/css/phui/phui-button.css' => '653ac588',
'rsrc/css/phui/phui-document.css' => '3b078dc0',
'rsrc/css/phui/phui-feed-story.css' => '3a59c2cf',
'rsrc/css/phui/phui-fontkit.css' => 'de84aa4a',
'rsrc/css/phui/phui-form-view.css' => '867463b4',
'rsrc/css/phui/phui-form.css' => 'b78ec020',
'rsrc/css/phui/phui-header-view.css' => '5b79f0ef',
'rsrc/css/phui/phui-icon.css' => '322274f9',
'rsrc/css/phui/phui-info-panel.css' => '27ea50a1',
'rsrc/css/phui/phui-list.css' => 'ef8035b6',
'rsrc/css/phui/phui-object-box.css' => 'ce92d8ec',
'rsrc/css/phui/phui-object-item-list-view.css' => '8b459abe',
'rsrc/css/phui/phui-pinboard-view.css' => '4b346c2a',
'rsrc/css/phui/phui-property-list-view.css' => 'dbf53b12',
'rsrc/css/phui/phui-remarkup-preview.css' => '19ad512b',
'rsrc/css/phui/phui-spacing.css' => '042804d6',
'rsrc/css/phui/phui-status.css' => '2f562399',
'rsrc/css/phui/phui-tag-view.css' => '295d81c4',
'rsrc/css/phui/phui-text.css' => '23e9b4b7',
- 'rsrc/css/phui/phui-timeline-view.css' => '17905388',
+ 'rsrc/css/phui/phui-timeline-view.css' => '18035042',
'rsrc/css/phui/phui-workboard-view.css' => '84f2c272',
'rsrc/css/phui/phui-workpanel-view.css' => '97b69459',
'rsrc/css/sprite-actions.css' => '969ad0e5',
'rsrc/css/sprite-apps-large.css' => '5abf49e9',
'rsrc/css/sprite-apps-xlarge.css' => 'db66c878',
'rsrc/css/sprite-apps.css' => '6973a52b',
'rsrc/css/sprite-buttonbar.css' => 'ba1c5738',
'rsrc/css/sprite-conpherence.css' => '3b4a0487',
'rsrc/css/sprite-docs.css' => '5f65d0da',
'rsrc/css/sprite-gradient.css' => 'a10def53',
'rsrc/css/sprite-icons.css' => 'f19a828c',
'rsrc/css/sprite-login.css' => '9fbaec81',
'rsrc/css/sprite-main-header.css' => '92720ee2',
'rsrc/css/sprite-menu.css' => '8da53882',
'rsrc/css/sprite-minicons.css' => 'df4f76fe',
'rsrc/css/sprite-payments.css' => 'cc085d44',
'rsrc/css/sprite-projects.css' => '7578fa56',
'rsrc/css/sprite-status.css' => '8bce1c97',
'rsrc/css/sprite-tokens.css' => '1706b943',
'rsrc/externals/font/fontawesome/fontawesome-webfont.eot' => 'b676fe4f',
'rsrc/externals/font/fontawesome/fontawesome-webfont.ttf' => 'af66fc5c',
'rsrc/externals/font/fontawesome/fontawesome-webfont.woff' => 'c713570f',
'rsrc/externals/font/glyphicons/glyphicons-halflings-regular.eot' => '282d455e',
'rsrc/externals/font/glyphicons/glyphicons-halflings-regular.ttf' => 'b7c56991',
'rsrc/externals/font/glyphicons/glyphicons-halflings-regular.woff' => 'bac768cd',
'rsrc/externals/font/sourcesans/SourceSansPro.woff' => '3614608c',
'rsrc/externals/font/sourcesans/SourceSansProBold.woff' => 'cbf46566',
'rsrc/externals/javelin/core/Event.js' => '79473b62',
'rsrc/externals/javelin/core/Stratcom.js' => 'c293f7b9',
'rsrc/externals/javelin/core/__tests__/event-stop-and-kill.js' => '717554e4',
'rsrc/externals/javelin/core/__tests__/install.js' => 'c432ee85',
'rsrc/externals/javelin/core/__tests__/stratcom.js' => 'da194d4b',
'rsrc/externals/javelin/core/__tests__/util.js' => 'd3b157a9',
'rsrc/externals/javelin/core/init.js' => 'b88ab49e',
'rsrc/externals/javelin/core/init_node.js' => 'd7dde471',
'rsrc/externals/javelin/core/install.js' => '52a92793',
'rsrc/externals/javelin/core/util.js' => '65b0b249',
'rsrc/externals/javelin/docs/Base.js' => '897bb199',
'rsrc/externals/javelin/docs/onload.js' => '81fb4862',
'rsrc/externals/javelin/ext/fx/Color.js' => '7e41274a',
'rsrc/externals/javelin/ext/fx/FX.js' => '54b612ba',
'rsrc/externals/javelin/ext/reactor/core/DynVal.js' => 'f6555212',
'rsrc/externals/javelin/ext/reactor/core/Reactor.js' => '77b1cf6f',
'rsrc/externals/javelin/ext/reactor/core/ReactorNode.js' => 'b4c30592',
'rsrc/externals/javelin/ext/reactor/core/ReactorNodeCalmer.js' => '76f4ebed',
'rsrc/externals/javelin/ext/reactor/dom/RDOM.js' => 'b6d401d6',
'rsrc/externals/javelin/ext/view/HTMLView.js' => 'e5b406f9',
'rsrc/externals/javelin/ext/view/View.js' => '0f764c35',
'rsrc/externals/javelin/ext/view/ViewInterpreter.js' => '0c33c1a0',
'rsrc/externals/javelin/ext/view/ViewPlaceholder.js' => '2fa810fc',
'rsrc/externals/javelin/ext/view/ViewRenderer.js' => '6c2b09a2',
'rsrc/externals/javelin/ext/view/ViewVisitor.js' => 'efe49472',
'rsrc/externals/javelin/ext/view/__tests__/HTMLView.js' => 'f92d7bcb',
'rsrc/externals/javelin/ext/view/__tests__/View.js' => 'bda69c40',
'rsrc/externals/javelin/ext/view/__tests__/ViewInterpreter.js' => '7a94d6a5',
'rsrc/externals/javelin/ext/view/__tests__/ViewRenderer.js' => '5426001c',
'rsrc/externals/javelin/lib/Cookie.js' => '6b3dcf44',
'rsrc/externals/javelin/lib/DOM.js' => '32a4d380',
'rsrc/externals/javelin/lib/History.js' => 'c60f4327',
'rsrc/externals/javelin/lib/JSON.js' => '08e56a4e',
'rsrc/externals/javelin/lib/Mask.js' => 'b9f26029',
'rsrc/externals/javelin/lib/Request.js' => '23f9bb8d',
'rsrc/externals/javelin/lib/Resource.js' => '356de121',
'rsrc/externals/javelin/lib/URI.js' => 'd9a9b862',
'rsrc/externals/javelin/lib/Vector.js' => '039fb90d',
'rsrc/externals/javelin/lib/Workflow.js' => 'f28bf201',
'rsrc/externals/javelin/lib/__tests__/Cookie.js' => '5ed109e8',
'rsrc/externals/javelin/lib/__tests__/DOM.js' => 'c984504b',
'rsrc/externals/javelin/lib/__tests__/JSON.js' => '2295d074',
'rsrc/externals/javelin/lib/__tests__/URI.js' => '003ed329',
'rsrc/externals/javelin/lib/__tests__/behavior.js' => '1ea62783',
'rsrc/externals/javelin/lib/behavior.js' => '8a3ed18b',
'rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js' => 'e7c21fb3',
'rsrc/externals/javelin/lib/control/typeahead/Typeahead.js' => 'c54eeefb',
'rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js' => '5f850b5c',
'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadCompositeSource.js' => '84f34ab1',
'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js' => 'a79b75a4',
'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js' => 'f778a573',
'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js' => '62e18640',
'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadStaticSource.js' => 'cdde23f1',
'rsrc/externals/raphael/g.raphael.js' => '40dde778',
'rsrc/externals/raphael/g.raphael.line.js' => '40da039e',
'rsrc/externals/raphael/raphael.js' => '51ee6b43',
'rsrc/image/BFCFDA.png' => 'd5ec91f4',
'rsrc/image/actions/edit.png' => '2fc41442',
'rsrc/image/apple-touch-icon.png' => '8458dda7',
'rsrc/image/avatar.png' => '3eb28cd9',
'rsrc/image/checker_dark.png' => 'd8e65881',
'rsrc/image/checker_light.png' => 'a0155918',
'rsrc/image/credit_cards.png' => '72b8ede8',
'rsrc/image/darkload.gif' => '1ffd3ec6',
'rsrc/image/divot.png' => '94dded62',
'rsrc/image/grippy_texture.png' => 'aca81e2f',
'rsrc/image/icon/fatcow/arrow_branch.png' => '2537c01c',
'rsrc/image/icon/fatcow/arrow_merge.png' => '21b660e0',
'rsrc/image/icon/fatcow/bullet_black.png' => 'ff190031',
'rsrc/image/icon/fatcow/bullet_orange.png' => 'e273e5bb',
'rsrc/image/icon/fatcow/bullet_red.png' => 'c0b75434',
'rsrc/image/icon/fatcow/calendar_edit.png' => '24632275',
'rsrc/image/icon/fatcow/document_black.png' => '45fe1c60',
'rsrc/image/icon/fatcow/flag_blue.png' => 'a01abb1d',
'rsrc/image/icon/fatcow/flag_finish.png' => '67825cee',
'rsrc/image/icon/fatcow/flag_ghost.png' => '20ca8783',
'rsrc/image/icon/fatcow/flag_green.png' => '7e0eaa7a',
'rsrc/image/icon/fatcow/flag_orange.png' => '9e73df66',
'rsrc/image/icon/fatcow/flag_pink.png' => '7e92f3b2',
'rsrc/image/icon/fatcow/flag_purple.png' => 'cc517522',
'rsrc/image/icon/fatcow/flag_red.png' => '04ec726f',
'rsrc/image/icon/fatcow/flag_yellow.png' => '73946fd4',
'rsrc/image/icon/fatcow/folder.png' => '95a435af',
'rsrc/image/icon/fatcow/folder_go.png' => '001cbc94',
'rsrc/image/icon/fatcow/key_question.png' => '52a0c26a',
'rsrc/image/icon/fatcow/link.png' => '7afd4d5e',
'rsrc/image/icon/fatcow/page_white_edit.png' => '39a2eed8',
'rsrc/image/icon/fatcow/page_white_link.png' => 'a90023c7',
'rsrc/image/icon/fatcow/page_white_put.png' => '08c95a0c',
'rsrc/image/icon/fatcow/page_white_text.png' => '1e1f79c3',
'rsrc/image/icon/fatcow/source/conduit.png' => '4ea01d2f',
'rsrc/image/icon/fatcow/source/email.png' => '9bab3239',
'rsrc/image/icon/fatcow/source/fax.png' => '04195e68',
'rsrc/image/icon/fatcow/source/mobile.png' => 'f1321264',
'rsrc/image/icon/fatcow/source/tablet.png' => '49396799',
'rsrc/image/icon/fatcow/source/web.png' => '136ccb5d',
'rsrc/image/icon/fatcow/thumbnails/default160x120.png' => 'f2e8a2eb',
'rsrc/image/icon/fatcow/thumbnails/default60x45.png' => '0118abed',
'rsrc/image/icon/fatcow/thumbnails/image160x120.png' => '79bb556a',
'rsrc/image/icon/fatcow/thumbnails/image60x45.png' => 'c5e1685e',
'rsrc/image/icon/fatcow/thumbnails/pdf160x120.png' => 'ac9edbf5',
'rsrc/image/icon/fatcow/thumbnails/pdf60x45.png' => 'c0db4143',
'rsrc/image/icon/fatcow/thumbnails/zip160x120.png' => '75f9cd0f',
'rsrc/image/icon/fatcow/thumbnails/zip60x45.png' => 'af11bf3e',
'rsrc/image/icon/lightbox/close-2.png' => 'cc40e7c8',
'rsrc/image/icon/lightbox/close-hover-2.png' => 'fb5d6d9e',
'rsrc/image/icon/lightbox/left-arrow-2.png' => '8426133b',
'rsrc/image/icon/lightbox/left-arrow-hover-2.png' => '701e5ee3',
'rsrc/image/icon/lightbox/right-arrow-2.png' => '6d5519a0',
'rsrc/image/icon/lightbox/right-arrow-hover-2.png' => '3a04aa21',
'rsrc/image/icon/subscribe.png' => 'd03ed5a5',
'rsrc/image/icon/tango/attachment.png' => 'ecc8022e',
'rsrc/image/icon/tango/edit.png' => '929a1363',
'rsrc/image/icon/tango/go-down.png' => '96d95e43',
'rsrc/image/icon/tango/log.png' => 'b08cc63a',
'rsrc/image/icon/tango/upload.png' => '7bbb7984',
'rsrc/image/icon/unsubscribe.png' => '25725013',
'rsrc/image/lightblue-header.png' => '5c168b6d',
'rsrc/image/loading.gif' => '75d384cc',
'rsrc/image/loading/boating_24.gif' => '5c90f086',
'rsrc/image/loading/compass_24.gif' => 'b36b4f46',
'rsrc/image/loading/loading_24.gif' => '26bc9adc',
'rsrc/image/loading/loading_48.gif' => '6a4994c7',
'rsrc/image/loading/loading_d48.gif' => 'cdcbe900',
'rsrc/image/loading/loading_w24.gif' => '7662fa2b',
'rsrc/image/main_texture.png' => '29a2c5ad',
'rsrc/image/menu_texture.png' => '5a17580d',
'rsrc/image/people/harding.png' => '45aa614e',
'rsrc/image/people/jefferson.png' => 'afca0e53',
'rsrc/image/people/lincoln.png' => '9369126d',
'rsrc/image/people/mckinley.png' => 'fb8f16ce',
'rsrc/image/people/taft.png' => 'd7bc402c',
'rsrc/image/people/washington.png' => '40dd301c',
'rsrc/image/phrequent_active.png' => 'a466a8ed',
'rsrc/image/phrequent_inactive.png' => 'bfc15a69',
'rsrc/image/search-white.png' => '64cc0d45',
'rsrc/image/search.png' => '82625a7e',
'rsrc/image/sprite-actions-X2.png' => '7dfd5652',
'rsrc/image/sprite-actions.png' => '2ddd18c3',
'rsrc/image/sprite-apps-X2.png' => 'c091b2d3',
'rsrc/image/sprite-apps-large-X2.png' => 'e1396a83',
'rsrc/image/sprite-apps-large.png' => '48cef7bd',
'rsrc/image/sprite-apps-xlarge.png' => 'a751a580',
'rsrc/image/sprite-apps.png' => '4f788e21',
'rsrc/image/sprite-buttonbar-X2.png' => '2c09a184',
'rsrc/image/sprite-buttonbar.png' => 'e98e96af',
'rsrc/image/sprite-conpherence-X2.png' => 'cd2d08d7',
'rsrc/image/sprite-conpherence.png' => 'a5ab2eb7',
'rsrc/image/sprite-docs-X2.png' => '6dc1adad',
'rsrc/image/sprite-docs.png' => '4636297f',
'rsrc/image/sprite-gradient.png' => '4ece0b62',
'rsrc/image/sprite-icons-X2.png' => '0d5867c0',
'rsrc/image/sprite-icons.png' => '3f754bda',
'rsrc/image/sprite-login-X2.png' => '81c1344f',
'rsrc/image/sprite-login.png' => '7c729508',
'rsrc/image/sprite-main-header.png' => '83521873',
'rsrc/image/sprite-menu-X2.png' => '949974c6',
'rsrc/image/sprite-menu.png' => '307d5da0',
'rsrc/image/sprite-minicons-X2.png' => '55377e4e',
'rsrc/image/sprite-minicons.png' => '272644ea',
'rsrc/image/sprite-payments.png' => 'd8576309',
'rsrc/image/sprite-projects-X2.png' => '218fdc8b',
'rsrc/image/sprite-projects.png' => '631ff9a7',
'rsrc/image/sprite-status-X2.png' => '82445ee0',
'rsrc/image/sprite-status.png' => '926a896a',
'rsrc/image/sprite-tokens-X2.png' => 'b4776580',
'rsrc/image/sprite-tokens.png' => '25b75533',
'rsrc/image/texture/card-gradient.png' => '815f26e8',
'rsrc/image/texture/dark-menu-hover.png' => '5fa7ece8',
'rsrc/image/texture/dark-menu.png' => '7e22296e',
'rsrc/image/texture/grip.png' => '719404f3',
'rsrc/image/texture/panel-header-gradient.png' => 'e3b8dcfe',
'rsrc/image/texture/phlnx-bg.png' => '8d819209',
'rsrc/image/texture/pholio-background.gif' => 'ba29239c',
'rsrc/image/texture/table_header.png' => '5c433037',
'rsrc/image/texture/table_header_hover.png' => '038ec3b9',
'rsrc/image/texture/table_header_tall.png' => 'd56b434f',
'rsrc/js/application/aphlict/Aphlict.js' => '493665ee',
'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => '2a2dba85',
'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => '845731b8',
'rsrc/js/application/auth/behavior-persona-login.js' => '9414ff18',
'rsrc/js/application/config/behavior-reorder-fields.js' => '938aed89',
'rsrc/js/application/conpherence/behavior-menu.js' => '7ee23816',
'rsrc/js/application/conpherence/behavior-pontificate.js' => '53f6f2dd',
'rsrc/js/application/conpherence/behavior-widget-pane.js' => 'd8ef8659',
'rsrc/js/application/countdown/timer.js' => '889c96f3',
'rsrc/js/application/differential/DifferentialInlineCommentEditor.js' => 'f2441746',
'rsrc/js/application/differential/behavior-add-reviewers-and-ccs.js' => '533a187b',
'rsrc/js/application/differential/behavior-comment-jump.js' => '71755c79',
'rsrc/js/application/differential/behavior-comment-preview.js' => '127f2018',
'rsrc/js/application/differential/behavior-diff-radios.js' => 'e1ff79b1',
'rsrc/js/application/differential/behavior-dropdown-menus.js' => '5f004630',
'rsrc/js/application/differential/behavior-edit-inline-comments.js' => '00861799',
'rsrc/js/application/differential/behavior-keyboard-nav.js' => '173ce7e7',
'rsrc/js/application/differential/behavior-populate.js' => 'ce0c217a',
'rsrc/js/application/differential/behavior-show-all-comments.js' => '7c273581',
'rsrc/js/application/differential/behavior-show-field-details.js' => '441f2137',
'rsrc/js/application/differential/behavior-show-more.js' => 'dd7e8ef5',
'rsrc/js/application/differential/behavior-toggle-files.js' => 'ca3f91eb',
'rsrc/js/application/differential/behavior-user-select.js' => 'a8d8459d',
'rsrc/js/application/diffusion/behavior-audit-preview.js' => 'be81801d',
'rsrc/js/application/diffusion/behavior-commit-branches.js' => 'bdaf4d04',
'rsrc/js/application/diffusion/behavior-commit-graph.js' => 'f7f1289f',
'rsrc/js/application/diffusion/behavior-jump-to.js' => '9db3d160',
'rsrc/js/application/diffusion/behavior-load-blame.js' => '42126667',
'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => '75903ee1',
'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => 'e5822781',
'rsrc/js/application/files/behavior-icon-composer.js' => '8ef9ab58',
'rsrc/js/application/files/behavior-launch-icon-composer.js' => '48086888',
'rsrc/js/application/harbormaster/behavior-reorder-steps.js' => '957a7fde',
'rsrc/js/application/herald/HeraldRuleEditor.js' => '22d2966a',
'rsrc/js/application/herald/PathTypeahead.js' => 'f7fc67ec',
'rsrc/js/application/herald/herald-rule-editor.js' => '7ebaeed3',
'rsrc/js/application/maniphest/behavior-batch-editor.js' => 'fe80fb6d',
'rsrc/js/application/maniphest/behavior-batch-selector.js' => 'ead554ec',
'rsrc/js/application/maniphest/behavior-line-chart.js' => '64ef2fd2',
'rsrc/js/application/maniphest/behavior-list-edit.js' => 'cf76cfd5',
'rsrc/js/application/maniphest/behavior-subpriorityeditor.js' => '84845b5b',
'rsrc/js/application/maniphest/behavior-transaction-controls.js' => 'dddd43ac',
'rsrc/js/application/maniphest/behavior-transaction-expand.js' => '2f2e18aa',
'rsrc/js/application/maniphest/behavior-transaction-preview.js' => 'f8248bc5',
'rsrc/js/application/owners/OwnersPathEditor.js' => '46efd18e',
'rsrc/js/application/owners/owners-path-editor.js' => '7a68dda3',
'rsrc/js/application/passphrase/phame-credential-control.js' => '1e1c8a59',
'rsrc/js/application/phame/phame-post-preview.js' => '61d927ec',
'rsrc/js/application/pholio/behavior-pholio-mock-edit.js' => '1e1e8bb0',
'rsrc/js/application/pholio/behavior-pholio-mock-view.js' => '28497740',
'rsrc/js/application/phortune/behavior-balanced-payment-form.js' => '3b3e1664',
'rsrc/js/application/phortune/behavior-stripe-payment-form.js' => '1693a296',
'rsrc/js/application/phortune/behavior-test-payment-form.js' => 'b3e5ee60',
'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef',
'rsrc/js/application/policy/behavior-policy-control.js' => 'c01153ea',
'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '263aeb8c',
'rsrc/js/application/ponder/behavior-votebox.js' => '327dbe61',
'rsrc/js/application/projects/behavior-project-boards.js' => 'd8e135db',
'rsrc/js/application/projects/behavior-project-create.js' => '065227cc',
'rsrc/js/application/releeph/releeph-preview-branch.js' => '9eb2cedb',
'rsrc/js/application/releeph/releeph-request-state-change.js' => 'd259e7c9',
'rsrc/js/application/releeph/releeph-request-typeahead.js' => 'cd9e7094',
'rsrc/js/application/repository/repository-crossreference.js' => '8ab282be',
'rsrc/js/application/search/behavior-reorder-queries.js' => '37871df4',
'rsrc/js/application/slowvote/behavior-slowvote-embed.js' => 'a51fdb2e',
'rsrc/js/application/transactions/behavior-transaction-comment-form.js' => '9084a36f',
'rsrc/js/application/transactions/behavior-transaction-list.js' => '925c9bab',
'rsrc/js/application/uiexample/JavelinViewExample.js' => 'd4a14807',
'rsrc/js/application/uiexample/ReactorButtonExample.js' => '44524435',
'rsrc/js/application/uiexample/ReactorCheckboxExample.js' => '7ba325ee',
'rsrc/js/application/uiexample/ReactorFocusExample.js' => '82f568cd',
'rsrc/js/application/uiexample/ReactorInputExample.js' => 'd6ca6b1c',
'rsrc/js/application/uiexample/ReactorMouseoverExample.js' => '4e37e4de',
'rsrc/js/application/uiexample/ReactorRadioExample.js' => '858f9728',
'rsrc/js/application/uiexample/ReactorSelectExample.js' => '189e4fe3',
'rsrc/js/application/uiexample/ReactorSendClassExample.js' => 'bf97561d',
'rsrc/js/application/uiexample/ReactorSendPropertiesExample.js' => '551add57',
'rsrc/js/application/uiexample/busy-example.js' => 'fbbce3bf',
'rsrc/js/application/uiexample/gesture-example.js' => 'f42bb8c6',
'rsrc/js/application/uiexample/notification-example.js' => 'c51a6616',
'rsrc/js/core/Busy.js' => '6453c869',
'rsrc/js/core/DragAndDropFileUpload.js' => 'ae6abfba',
'rsrc/js/core/DraggableList.js' => '1681c4d4',
'rsrc/js/core/DropdownMenu.js' => 'fb342e18',
'rsrc/js/core/DropdownMenuItem.js' => '0f386ef4',
'rsrc/js/core/FileUpload.js' => 'a4ae61bf',
'rsrc/js/core/Hovercard.js' => '4f344388',
'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2',
'rsrc/js/core/KeyboardShortcutManager.js' => 'ad7a69ca',
'rsrc/js/core/MultirowRowManager.js' => '50395a1b',
'rsrc/js/core/Notification.js' => '0c6946e7',
'rsrc/js/core/Prefab.js' => '0326e5d0',
'rsrc/js/core/ShapedRequest.js' => 'dfa181a4',
'rsrc/js/core/TextAreaUtils.js' => 'b3ec3cfc',
'rsrc/js/core/ToolTip.js' => '3915d490',
'rsrc/js/core/behavior-active-nav.js' => 'c81bc98f',
'rsrc/js/core/behavior-audio-source.js' => '59b251eb',
'rsrc/js/core/behavior-autofocus.js' => '7319e029',
'rsrc/js/core/behavior-crop.js' => 'b98fc918',
'rsrc/js/core/behavior-dark-console.js' => 'e9fdb5e5',
'rsrc/js/core/behavior-device.js' => '03d6ed07',
'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '4a11ea9c',
'rsrc/js/core/behavior-error-log.js' => 'a5d7cf86',
'rsrc/js/core/behavior-fancy-datepicker.js' => '5d584426',
'rsrc/js/core/behavior-file-tree.js' => 'c8728c70',
'rsrc/js/core/behavior-form.js' => 'a9aaba0c',
'rsrc/js/core/behavior-gesture.js' => 'fe2e0ba4',
'rsrc/js/core/behavior-global-drag-and-drop.js' => '8fd76bab',
'rsrc/js/core/behavior-history-install.js' => '7ee2b591',
'rsrc/js/core/behavior-hovercard.js' => '9c808199',
'rsrc/js/core/behavior-keyboard-pager.js' => 'b657bdf8',
'rsrc/js/core/behavior-keyboard-shortcuts.js' => 'd75709e6',
'rsrc/js/core/behavior-konami.js' => '5bc2cb21',
'rsrc/js/core/behavior-lightbox-attachments.js' => '3aa45ad9',
'rsrc/js/core/behavior-line-linker.js' => 'bc778103',
'rsrc/js/core/behavior-more.js' => '9b9197be',
'rsrc/js/core/behavior-object-selector.js' => 'b4eef37b',
'rsrc/js/core/behavior-oncopy.js' => 'c3e218fe',
'rsrc/js/core/behavior-phabricator-nav.js' => 'b5842a5e',
'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => 'c021950a',
'rsrc/js/core/behavior-refresh-csrf.js' => 'c4b31646',
'rsrc/js/core/behavior-remarkup-preview.js' => 'f7379f45',
'rsrc/js/core/behavior-reveal-content.js' => '8f24abfc',
'rsrc/js/core/behavior-search-typeahead.js' => 'f6b56f7a',
'rsrc/js/core/behavior-select-on-click.js' => '0e34ca02',
'rsrc/js/core/behavior-toggle-class.js' => 'a82a7769',
'rsrc/js/core/behavior-tokenizer.js' => 'b3a4b884',
'rsrc/js/core/behavior-tooltip.js' => '48db4145',
'rsrc/js/core/behavior-watch-anchor.js' => '06e05112',
'rsrc/js/core/behavior-workflow.js' => 'fee00761',
'rsrc/js/core/phtize.js' => 'd254d646',
'rsrc/js/phui/behavior-phui-object-box-tabs.js' => 'a3e2244e',
'rsrc/swf/aphlict.swf' => 'abac967d',
),
'symbols' =>
array(
'aphront-bars' => '231ac33c',
'aphront-contextbar-view-css' => '1c3b0529',
'aphront-dark-console-css' => '6378ef3d',
'aphront-dialog-view-css' => 'c01d24b4',
'aphront-error-view-css' => '9f1d5518',
'aphront-list-filter-view-css' => 'ef989c67',
'aphront-multi-column-view-css' => '12f65921',
'aphront-pager-view-css' => '2e3539af',
'aphront-panel-view-css' => '5846dfa2',
'aphront-request-failure-view-css' => 'da14df31',
'aphront-table-view-css' => 'de599000',
'aphront-tokenizer-control-css' => '36903077',
'aphront-tooltip-css' => '9c90229d',
'aphront-two-column-view-css' => '16ab3ad2',
'aphront-typeahead-control-css' => '271456a1',
'auth-css' => '1e655982',
'config-options-css' => '7fedf08b',
'conpherence-menu-css' => '561348ac',
'conpherence-message-pane-css' => '2aedca89',
'conpherence-notification-css' => '403cf598',
'conpherence-update-css' => '1099a660',
'conpherence-widget-pane-css' => '87b12e0c',
'differential-changeset-view-css' => '1570a1ff',
'differential-core-view-css' => '7ac3cabc',
'differential-inline-comment-editor' => 'f2441746',
'differential-results-table-css' => '239924f9',
'differential-revision-add-comment-css' => 'c478bcaa',
'differential-revision-comment-css' => '48186045',
'differential-revision-history-css' => '0e8eb855',
'differential-revision-list-css' => 'f3c47d33',
'differential-table-of-contents-css' => '6bf8e1d2',
'diffusion-commit-view-css' => '92d1e8f9',
'diffusion-icons-css' => '384a0f7d',
'diffusion-source-css' => '66fdf661',
'diviner-shared-css' => '38813222',
'font-fontawesome' => '62bc244d',
'font-glyphicons-halflings' => '0c69dbc6',
'font-source-sans-pro' => '91d53463',
'global-drag-and-drop-css' => '697324ad',
'harbormaster-css' => 'cec833b7',
'herald-css' => '59d48f01',
'herald-rule-editor' => '22d2966a',
'herald-test-css' => '2b7d0f54',
'inline-comment-summary-css' => '14a91639',
'javelin-aphlict' => '493665ee',
'javelin-behavior' => '8a3ed18b',
'javelin-behavior-aphlict-dropdown' => '2a2dba85',
'javelin-behavior-aphlict-listen' => '845731b8',
'javelin-behavior-aphront-basic-tokenizer' => 'b3a4b884',
'javelin-behavior-aphront-crop' => 'b98fc918',
'javelin-behavior-aphront-drag-and-drop-textarea' => '4a11ea9c',
'javelin-behavior-aphront-form-disable-on-submit' => 'a9aaba0c',
'javelin-behavior-aphront-more' => '9b9197be',
'javelin-behavior-audio-source' => '59b251eb',
'javelin-behavior-audit-preview' => 'be81801d',
'javelin-behavior-balanced-payment-form' => '3b3e1664',
'javelin-behavior-config-reorder-fields' => '938aed89',
'javelin-behavior-conpherence-menu' => '7ee23816',
'javelin-behavior-conpherence-pontificate' => '53f6f2dd',
'javelin-behavior-conpherence-widget-pane' => 'd8ef8659',
'javelin-behavior-countdown-timer' => '889c96f3',
'javelin-behavior-dark-console' => 'e9fdb5e5',
'javelin-behavior-device' => '03d6ed07',
'javelin-behavior-differential-add-reviewers-and-ccs' => '533a187b',
'javelin-behavior-differential-comment-jump' => '71755c79',
'javelin-behavior-differential-diff-radios' => 'e1ff79b1',
'javelin-behavior-differential-dropdown-menus' => '5f004630',
'javelin-behavior-differential-edit-inline-comments' => '00861799',
'javelin-behavior-differential-feedback-preview' => '127f2018',
'javelin-behavior-differential-keyboard-navigation' => '173ce7e7',
'javelin-behavior-differential-populate' => 'ce0c217a',
'javelin-behavior-differential-show-field-details' => '441f2137',
'javelin-behavior-differential-show-more' => 'dd7e8ef5',
'javelin-behavior-differential-toggle-files' => 'ca3f91eb',
'javelin-behavior-differential-user-select' => 'a8d8459d',
'javelin-behavior-diffusion-commit-branches' => 'bdaf4d04',
'javelin-behavior-diffusion-commit-graph' => 'f7f1289f',
'javelin-behavior-diffusion-jump-to' => '9db3d160',
'javelin-behavior-diffusion-pull-lastmodified' => '75903ee1',
'javelin-behavior-doorkeeper-tag' => 'e5822781',
'javelin-behavior-error-log' => 'a5d7cf86',
'javelin-behavior-fancy-datepicker' => '5d584426',
'javelin-behavior-global-drag-and-drop' => '8fd76bab',
'javelin-behavior-harbormaster-reorder-steps' => '957a7fde',
'javelin-behavior-herald-rule-editor' => '7ebaeed3',
'javelin-behavior-history-install' => '7ee2b591',
'javelin-behavior-icon-composer' => '8ef9ab58',
'javelin-behavior-konami' => '5bc2cb21',
'javelin-behavior-launch-icon-composer' => '48086888',
'javelin-behavior-lightbox-attachments' => '3aa45ad9',
'javelin-behavior-line-chart' => '64ef2fd2',
'javelin-behavior-load-blame' => '42126667',
'javelin-behavior-maniphest-batch-editor' => 'fe80fb6d',
'javelin-behavior-maniphest-batch-selector' => 'ead554ec',
'javelin-behavior-maniphest-list-editor' => 'cf76cfd5',
'javelin-behavior-maniphest-subpriority-editor' => '84845b5b',
'javelin-behavior-maniphest-transaction-controls' => 'dddd43ac',
'javelin-behavior-maniphest-transaction-expand' => '2f2e18aa',
'javelin-behavior-maniphest-transaction-preview' => 'f8248bc5',
'javelin-behavior-owners-path-editor' => '7a68dda3',
'javelin-behavior-passphrase-credential-control' => '1e1c8a59',
'javelin-behavior-persona-login' => '9414ff18',
'javelin-behavior-phabricator-active-nav' => 'c81bc98f',
'javelin-behavior-phabricator-autofocus' => '7319e029',
'javelin-behavior-phabricator-busy-example' => 'fbbce3bf',
'javelin-behavior-phabricator-file-tree' => 'c8728c70',
'javelin-behavior-phabricator-gesture' => 'fe2e0ba4',
'javelin-behavior-phabricator-gesture-example' => 'f42bb8c6',
'javelin-behavior-phabricator-hovercards' => '9c808199',
'javelin-behavior-phabricator-keyboard-pager' => 'b657bdf8',
'javelin-behavior-phabricator-keyboard-shortcuts' => 'd75709e6',
'javelin-behavior-phabricator-line-linker' => 'bc778103',
'javelin-behavior-phabricator-nav' => 'b5842a5e',
'javelin-behavior-phabricator-notification-example' => 'c51a6616',
'javelin-behavior-phabricator-object-selector' => 'b4eef37b',
'javelin-behavior-phabricator-oncopy' => 'c3e218fe',
'javelin-behavior-phabricator-remarkup-assist' => 'c021950a',
'javelin-behavior-phabricator-reveal-content' => '8f24abfc',
'javelin-behavior-phabricator-search-typeahead' => 'f6b56f7a',
'javelin-behavior-phabricator-show-all-transactions' => '7c273581',
'javelin-behavior-phabricator-tooltips' => '48db4145',
'javelin-behavior-phabricator-transaction-comment-form' => '9084a36f',
'javelin-behavior-phabricator-transaction-list' => '925c9bab',
'javelin-behavior-phabricator-watch-anchor' => '06e05112',
'javelin-behavior-phame-post-preview' => '61d927ec',
'javelin-behavior-pholio-mock-edit' => '1e1e8bb0',
'javelin-behavior-pholio-mock-view' => '28497740',
'javelin-behavior-phui-object-box-tabs' => 'a3e2244e',
'javelin-behavior-policy-control' => 'c01153ea',
'javelin-behavior-policy-rule-editor' => '263aeb8c',
'javelin-behavior-ponder-votebox' => '327dbe61',
'javelin-behavior-project-boards' => 'd8e135db',
'javelin-behavior-project-create' => '065227cc',
'javelin-behavior-refresh-csrf' => 'c4b31646',
'javelin-behavior-releeph-preview-branch' => '9eb2cedb',
'javelin-behavior-releeph-request-state-change' => 'd259e7c9',
'javelin-behavior-releeph-request-typeahead' => 'cd9e7094',
'javelin-behavior-remarkup-preview' => 'f7379f45',
'javelin-behavior-repository-crossreference' => '8ab282be',
'javelin-behavior-search-reorder-queries' => '37871df4',
'javelin-behavior-select-on-click' => '0e34ca02',
'javelin-behavior-slowvote-embed' => 'a51fdb2e',
'javelin-behavior-stripe-payment-form' => '1693a296',
'javelin-behavior-test-payment-form' => 'b3e5ee60',
'javelin-behavior-toggle-class' => 'a82a7769',
'javelin-behavior-view-placeholder' => '2fa810fc',
'javelin-behavior-workflow' => 'fee00761',
'javelin-color' => '7e41274a',
'javelin-cookie' => '6b3dcf44',
'javelin-dom' => '32a4d380',
'javelin-dynval' => 'f6555212',
'javelin-event' => '79473b62',
'javelin-fx' => '54b612ba',
'javelin-history' => 'c60f4327',
'javelin-install' => '52a92793',
'javelin-json' => '08e56a4e',
'javelin-magical-init' => 'b88ab49e',
'javelin-mask' => 'b9f26029',
'javelin-reactor' => '77b1cf6f',
'javelin-reactor-dom' => 'b6d401d6',
'javelin-reactor-node-calmer' => '76f4ebed',
'javelin-reactornode' => 'b4c30592',
'javelin-request' => '23f9bb8d',
'javelin-resource' => '356de121',
'javelin-stratcom' => 'c293f7b9',
'javelin-tokenizer' => 'e7c21fb3',
'javelin-typeahead' => 'c54eeefb',
'javelin-typeahead-composite-source' => '84f34ab1',
'javelin-typeahead-normalizer' => '5f850b5c',
'javelin-typeahead-ondemand-source' => 'a79b75a4',
'javelin-typeahead-preloaded-source' => 'f778a573',
'javelin-typeahead-source' => '62e18640',
'javelin-typeahead-static-source' => 'cdde23f1',
'javelin-uri' => 'd9a9b862',
'javelin-util' => '65b0b249',
'javelin-vector' => '039fb90d',
'javelin-view' => '0f764c35',
'javelin-view-html' => 'e5b406f9',
'javelin-view-interpreter' => '0c33c1a0',
'javelin-view-renderer' => '6c2b09a2',
'javelin-view-visitor' => 'efe49472',
'javelin-workflow' => 'f28bf201',
'lightbox-attachment-css' => '7acac05d',
'maniphest-batch-editor' => '8f380ebc',
'maniphest-report-css' => '6fc16517',
'maniphest-task-edit-css' => '8e23031b',
'maniphest-task-summary-css' => '6df1a768',
'multirow-row-manager' => '50395a1b',
'owners-path-editor' => '46efd18e',
'owners-path-editor-css' => '2f00933b',
'paste-css' => 'aa1767d1',
'path-typeahead' => 'f7fc67ec',
'people-profile-css' => 'ba7b2762',
'phabricator-action-header-view-css' => 'c14dfc57',
'phabricator-action-list-view-css' => '81383e25',
'phabricator-application-launch-view-css' => 'd290ba21',
'phabricator-busy' => '6453c869',
'phabricator-chatlog-css' => '852140ff',
'phabricator-content-source-view-css' => '4b8b05d4',
'phabricator-core-css' => 'da26ddb2',
'phabricator-countdown-css' => '86b7b0a0',
'phabricator-crumbs-view-css' => '2d9db584',
'phabricator-drag-and-drop-file-upload' => 'ae6abfba',
'phabricator-draggable-list' => '1681c4d4',
'phabricator-dropdown-menu' => 'fb342e18',
'phabricator-fatal-config-template-css' => '25d446d6',
'phabricator-feed-css' => '0d17c209',
'phabricator-file-upload' => 'a4ae61bf',
'phabricator-filetree-view-css' => 'a8c86ace',
'phabricator-flag-css' => '5337623f',
'phabricator-hovercard' => '4f344388',
'phabricator-hovercard-view-css' => '46a13cf0',
'phabricator-jump-nav' => 'f0c5e726',
'phabricator-keyboard-shortcut' => '1ae869f2',
'phabricator-keyboard-shortcut-manager' => 'ad7a69ca',
'phabricator-main-menu-view' => 'd36e0c11',
'phabricator-menu-item' => '0f386ef4',
'phabricator-nav-view-css' => '80e60fc1',
'phabricator-notification' => '0c6946e7',
'phabricator-notification-css' => '6901121e',
'phabricator-notification-menu-css' => 'fc9a363c',
'phabricator-object-selector-css' => '029a133d',
'phabricator-phtize' => 'd254d646',
'phabricator-prefab' => '0326e5d0',
'phabricator-profile-css' => '33e6f703',
'phabricator-project-tag-css' => '095c9404',
'phabricator-remarkup-css' => '0923dbd6',
'phabricator-search-results-css' => 'f240504c',
'phabricator-settings-css' => 'ea8f5915',
'phabricator-shaped-request' => 'dfa181a4',
'phabricator-side-menu-view-css' => '503699d0',
'phabricator-slowvote-css' => '266df6a1',
'phabricator-source-code-view-css' => '62a99814',
'phabricator-standard-page-view' => '517cdfb1',
'phabricator-textareautils' => 'b3ec3cfc',
'phabricator-tooltip' => '3915d490',
'phabricator-transaction-view-css' => 'ce491938',
'phabricator-ui-example-css' => '81adeffe',
'phabricator-uiexample-javelin-view' => 'd4a14807',
'phabricator-uiexample-reactor-button' => '44524435',
'phabricator-uiexample-reactor-checkbox' => '7ba325ee',
'phabricator-uiexample-reactor-focus' => '82f568cd',
'phabricator-uiexample-reactor-input' => 'd6ca6b1c',
'phabricator-uiexample-reactor-mouseover' => '4e37e4de',
'phabricator-uiexample-reactor-radio' => '858f9728',
'phabricator-uiexample-reactor-select' => '189e4fe3',
'phabricator-uiexample-reactor-sendclass' => 'bf97561d',
'phabricator-uiexample-reactor-sendproperties' => '551add57',
'phabricator-zindex-css' => '0d89d53c',
'phame-css' => '450826e1',
'pholio-css' => '2fa97dbe',
'pholio-edit-css' => 'b9e59b6d',
'pholio-inline-comments-css' => '52be33f0',
'phortune-credit-card-form' => '2290aeef',
'phortune-credit-card-form-css' => 'b25b4beb',
'phrequent-css' => 'ffc185ad',
'phriction-document-css' => '7d7f0071',
'phui-box-css' => '7b3a2eed',
'phui-button-css' => '653ac588',
'phui-calendar-css' => '5e1ad989',
'phui-calendar-day-css' => 'de035c8a',
'phui-calendar-list-css' => 'c1d0ca59',
'phui-calendar-month-css' => 'a92e47d2',
'phui-document-view-css' => '3b078dc0',
'phui-feed-story-css' => '3a59c2cf',
'phui-font-icon-base-css' => 'cc5465ba',
'phui-fontkit-css' => 'de84aa4a',
'phui-form-css' => 'b78ec020',
'phui-form-view-css' => '867463b4',
'phui-header-view-css' => '5b79f0ef',
'phui-icon-view-css' => '322274f9',
'phui-info-panel-css' => '27ea50a1',
'phui-list-view-css' => 'ef8035b6',
'phui-object-box-css' => 'ce92d8ec',
'phui-object-item-list-view-css' => '8b459abe',
'phui-pinboard-view-css' => '4b346c2a',
'phui-property-list-view-css' => 'dbf53b12',
'phui-remarkup-preview-css' => '19ad512b',
'phui-spacing-css' => '042804d6',
'phui-status-list-view-css' => '2f562399',
'phui-tag-view-css' => '295d81c4',
'phui-text-css' => '23e9b4b7',
- 'phui-timeline-view-css' => '17905388',
+ 'phui-timeline-view-css' => '18035042',
'phui-workboard-view-css' => '84f2c272',
'phui-workpanel-view-css' => '97b69459',
'policy-css' => '957ea14c',
'policy-edit-css' => '05cca26a',
'ponder-comment-table-css' => '6cdccea7',
'ponder-feed-view-css' => 'e62615b6',
'ponder-post-css' => 'ebab8a70',
'ponder-vote-css' => '8ed6ed8b',
'raphael-core' => '51ee6b43',
'raphael-g' => '40dde778',
'raphael-g-line' => '40da039e',
'releeph-core' => '9b3c5733',
'releeph-preview-branch' => 'b7a6f4a5',
'releeph-request-differential-create-dialog' => '8d8b92cd',
'releeph-request-typeahead-css' => '667a48ae',
'setup-issue-css' => '69e640e7',
'sprite-actions-css' => '969ad0e5',
'sprite-apps-css' => '6973a52b',
'sprite-apps-large-css' => '5abf49e9',
'sprite-apps-xlarge-css' => 'db66c878',
'sprite-buttonbar-css' => 'ba1c5738',
'sprite-conpherence-css' => '3b4a0487',
'sprite-docs-css' => '5f65d0da',
'sprite-gradient-css' => 'a10def53',
'sprite-icons-css' => 'f19a828c',
'sprite-login-css' => '9fbaec81',
'sprite-main-header-css' => '92720ee2',
'sprite-menu-css' => '8da53882',
'sprite-minicons-css' => 'df4f76fe',
'sprite-payments-css' => 'cc085d44',
'sprite-projects-css' => '7578fa56',
'sprite-status-css' => '8bce1c97',
'sprite-tokens-css' => '1706b943',
'subscribers-list-css' => '5bb30c78',
'syntax-highlighting-css' => '3c18c1cb',
'tokens-css' => '5f7bca25',
),
'requires' =>
array(
'00861799' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-stratcom',
2 => 'javelin-dom',
3 => 'javelin-util',
4 => 'javelin-vector',
5 => 'differential-inline-comment-editor',
),
'029a133d' =>
array(
0 => 'aphront-dialog-view-css',
),
'0326e5d0' =>
array(
0 => 'javelin-install',
1 => 'javelin-util',
2 => 'javelin-dom',
3 => 'javelin-typeahead',
4 => 'javelin-tokenizer',
5 => 'javelin-typeahead-preloaded-source',
6 => 'javelin-typeahead-ondemand-source',
7 => 'javelin-dom',
8 => 'javelin-stratcom',
9 => 'javelin-util',
),
'039fb90d' =>
array(
0 => 'javelin-install',
1 => 'javelin-event',
),
'03d6ed07' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-stratcom',
2 => 'javelin-dom',
3 => 'javelin-vector',
4 => 'javelin-install',
),
'065227cc' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-stratcom',
3 => 'javelin-workflow',
),
'06e05112' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-stratcom',
2 => 'javelin-dom',
3 => 'javelin-vector',
),
'08e56a4e' =>
array(
0 => 'javelin-install',
),
'0c33c1a0' =>
array(
0 => 'javelin-view',
1 => 'javelin-install',
2 => 'javelin-dom',
),
'0c6946e7' =>
array(
0 => 'javelin-install',
1 => 'javelin-dom',
2 => 'javelin-stratcom',
3 => 'javelin-util',
4 => 'phabricator-notification-css',
),
'0e34ca02' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-stratcom',
2 => 'javelin-dom',
),
'0f386ef4' =>
array(
0 => 'javelin-install',
1 => 'javelin-dom',
),
'0f764c35' =>
array(
0 => 'javelin-install',
1 => 'javelin-util',
),
'127f2018' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-stratcom',
2 => 'javelin-dom',
3 => 'javelin-request',
4 => 'javelin-util',
5 => 'phabricator-shaped-request',
),
'1681c4d4' =>
array(
0 => 'javelin-install',
1 => 'javelin-dom',
2 => 'javelin-stratcom',
3 => 'javelin-util',
4 => 'javelin-vector',
5 => 'javelin-magical-init',
),
'1693a296' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'phortune-credit-card-form',
),
'173ce7e7' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-stratcom',
3 => 'phabricator-keyboard-shortcut',
),
'189e4fe3' =>
array(
0 => 'javelin-install',
1 => 'javelin-dom',
2 => 'javelin-reactor-dom',
),
'1ae869f2' =>
array(
0 => 'javelin-install',
1 => 'javelin-util',
2 => 'phabricator-keyboard-shortcut-manager',
),
'1e1c8a59' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-stratcom',
3 => 'javelin-workflow',
4 => 'javelin-util',
5 => 'javelin-uri',
),
'1e1e8bb0' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-stratcom',
2 => 'javelin-dom',
3 => 'javelin-workflow',
4 => 'phabricator-phtize',
5 => 'phabricator-drag-and-drop-file-upload',
6 => 'phabricator-draggable-list',
),
'2290aeef' =>
array(
0 => 'javelin-install',
1 => 'javelin-dom',
2 => 'javelin-json',
3 => 'javelin-workflow',
4 => 'javelin-util',
),
'22d2966a' =>
array(
0 => 'multirow-row-manager',
1 => 'javelin-install',
2 => 'javelin-util',
3 => 'javelin-dom',
4 => 'javelin-stratcom',
5 => 'javelin-json',
6 => 'phabricator-prefab',
),
'23f9bb8d' =>
array(
0 => 'javelin-install',
1 => 'javelin-stratcom',
2 => 'javelin-util',
3 => 'javelin-behavior',
4 => 'javelin-json',
5 => 'javelin-dom',
6 => 'javelin-resource',
),
'263aeb8c' =>
array(
0 => 'javelin-behavior',
1 => 'multirow-row-manager',
2 => 'javelin-dom',
3 => 'javelin-util',
4 => 'phabricator-prefab',
5 => 'javelin-tokenizer',
6 => 'javelin-typeahead',
7 => 'javelin-typeahead-preloaded-source',
8 => 'javelin-json',
),
'2a2dba85' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-request',
2 => 'javelin-stratcom',
3 => 'javelin-vector',
4 => 'javelin-dom',
5 => 'javelin-uri',
6 => 'javelin-behavior-device',
),
'2f2e18aa' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-workflow',
3 => 'javelin-stratcom',
),
'2fa810fc' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-view-renderer',
3 => 'javelin-install',
),
'327dbe61' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-util',
3 => 'javelin-stratcom',
4 => 'javelin-request',
),
'32a4d380' =>
array(
0 => 'javelin-magical-init',
1 => 'javelin-install',
2 => 'javelin-util',
3 => 'javelin-vector',
4 => 'javelin-stratcom',
),
'356de121' =>
array(
0 => 'javelin-util',
1 => 'javelin-uri',
2 => 'javelin-install',
),
'37871df4' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-stratcom',
2 => 'javelin-workflow',
3 => 'javelin-dom',
4 => 'phabricator-draggable-list',
),
'3915d490' =>
array(
0 => 'javelin-install',
1 => 'javelin-util',
2 => 'javelin-dom',
3 => 'javelin-vector',
),
'3aa45ad9' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-stratcom',
2 => 'javelin-dom',
3 => 'javelin-mask',
4 => 'javelin-util',
5 => 'phabricator-busy',
),
'3b3e1664' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'phortune-credit-card-form',
),
'441f2137' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-stratcom',
2 => 'javelin-dom',
),
'46efd18e' =>
array(
0 => 'multirow-row-manager',
1 => 'javelin-install',
2 => 'path-typeahead',
3 => 'javelin-dom',
4 => 'javelin-util',
5 => 'phabricator-prefab',
),
'48db4145' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-behavior-device',
2 => 'javelin-stratcom',
3 => 'phabricator-tooltip',
),
'493665ee' =>
array(
0 => 'javelin-install',
1 => 'javelin-util',
),
'4a11ea9c' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'phabricator-drag-and-drop-file-upload',
3 => 'phabricator-textareautils',
),
'4e37e4de' =>
array(
0 => 'javelin-install',
1 => 'javelin-dom',
2 => 'javelin-reactor-dom',
),
'4f344388' =>
array(
0 => 'javelin-install',
1 => 'javelin-dom',
2 => 'javelin-vector',
3 => 'javelin-request',
4 => 'javelin-uri',
),
'50395a1b' =>
array(
0 => 'javelin-install',
1 => 'javelin-stratcom',
2 => 'javelin-dom',
3 => 'javelin-util',
),
'52a92793' =>
array(
0 => 'javelin-util',
1 => 'javelin-magical-init',
),
'533a187b' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'phabricator-prefab',
),
'53f6f2dd' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-util',
3 => 'javelin-workflow',
4 => 'javelin-stratcom',
),
'54b612ba' =>
array(
0 => 'javelin-color',
1 => 'javelin-install',
2 => 'javelin-util',
),
'551add57' =>
array(
0 => 'javelin-install',
1 => 'javelin-dom',
2 => 'javelin-reactor-dom',
),
'59b251eb' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-stratcom',
2 => 'javelin-vector',
3 => 'javelin-dom',
),
'5bc2cb21' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-stratcom',
),
'5d584426' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-util',
2 => 'javelin-dom',
3 => 'javelin-stratcom',
4 => 'javelin-vector',
),
'5f004630' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-util',
3 => 'javelin-stratcom',
4 => 'phabricator-dropdown-menu',
5 => 'phabricator-menu-item',
6 => 'phabricator-phtize',
),
'5f850b5c' =>
array(
0 => 'javelin-install',
),
'61d927ec' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-util',
3 => 'phabricator-shaped-request',
),
'62e18640' =>
array(
0 => 'javelin-install',
1 => 'javelin-util',
2 => 'javelin-dom',
3 => 'javelin-typeahead-normalizer',
),
'6453c869' =>
array(
0 => 'javelin-install',
1 => 'javelin-dom',
2 => 'javelin-fx',
),
'64ef2fd2' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-vector',
),
'6b3dcf44' =>
array(
0 => 'javelin-install',
1 => 'javelin-util',
),
'6c2b09a2' =>
array(
0 => 'javelin-install',
1 => 'javelin-util',
),
'71755c79' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-stratcom',
2 => 'javelin-dom',
),
'7319e029' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
),
'75903ee1' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-util',
3 => 'javelin-request',
),
'76f4ebed' =>
array(
0 => 'javelin-install',
1 => 'javelin-reactor',
2 => 'javelin-util',
),
'77b1cf6f' =>
array(
0 => 'javelin-install',
1 => 'javelin-util',
),
'79473b62' =>
array(
0 => 'javelin-install',
),
'7a68dda3' =>
array(
0 => 'owners-path-editor',
1 => 'javelin-behavior',
),
'7ba325ee' =>
array(
0 => 'javelin-install',
1 => 'javelin-dom',
2 => 'javelin-reactor-dom',
),
'7c273581' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-stratcom',
2 => 'javelin-dom',
),
'7e41274a' =>
array(
0 => 'javelin-install',
),
'7ebaeed3' =>
array(
0 => 'herald-rule-editor',
1 => 'javelin-behavior',
),
'7ee23816' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-util',
3 => 'javelin-stratcom',
4 => 'javelin-workflow',
5 => 'javelin-behavior-device',
6 => 'javelin-history',
7 => 'javelin-vector',
8 => 'phabricator-shaped-request',
),
'7ee2b591' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-history',
),
'82f568cd' =>
array(
0 => 'javelin-install',
1 => 'javelin-dom',
2 => 'javelin-reactor-dom',
),
'845731b8' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-aphlict',
2 => 'javelin-stratcom',
3 => 'javelin-request',
4 => 'javelin-uri',
5 => 'javelin-dom',
6 => 'javelin-json',
7 => 'phabricator-notification',
),
'84845b5b' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-stratcom',
3 => 'javelin-workflow',
4 => 'phabricator-draggable-list',
),
'84f34ab1' =>
array(
0 => 'javelin-install',
1 => 'javelin-typeahead-source',
2 => 'javelin-util',
),
'858f9728' =>
array(
0 => 'javelin-install',
1 => 'javelin-dom',
2 => 'javelin-reactor-dom',
),
'889c96f3' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
),
'8a3ed18b' =>
array(
0 => 'javelin-magical-init',
1 => 'javelin-util',
),
'8ab282be' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-stratcom',
3 => 'javelin-uri',
),
'8ef9ab58' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-stratcom',
),
'8f24abfc' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-stratcom',
2 => 'javelin-dom',
),
'8fd76bab' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-uri',
3 => 'javelin-mask',
4 => 'phabricator-drag-and-drop-file-upload',
),
'9084a36f' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-util',
3 => 'javelin-fx',
4 => 'javelin-request',
5 => 'phabricator-shaped-request',
),
'925c9bab' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-stratcom',
2 => 'javelin-workflow',
3 => 'javelin-dom',
4 => 'javelin-fx',
5 => 'javelin-util',
),
'938aed89' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-stratcom',
2 => 'javelin-dom',
3 => 'javelin-json',
4 => 'phabricator-draggable-list',
),
'9414ff18' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-resource',
2 => 'javelin-stratcom',
3 => 'javelin-workflow',
4 => 'javelin-util',
),
'957a7fde' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-stratcom',
2 => 'javelin-workflow',
3 => 'javelin-dom',
4 => 'phabricator-draggable-list',
),
'9b9197be' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-stratcom',
2 => 'javelin-dom',
),
'9c808199' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-behavior-device',
2 => 'javelin-stratcom',
3 => 'javelin-vector',
4 => 'phabricator-hovercard',
),
'9db3d160' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-vector',
2 => 'javelin-dom',
),
'9eb2cedb' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-uri',
3 => 'javelin-request',
),
'a3e2244e' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-stratcom',
2 => 'javelin-dom',
),
'a4ae61bf' =>
array(
0 => 'javelin-install',
1 => 'javelin-dom',
2 => 'phabricator-notification',
),
'a51fdb2e' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-request',
2 => 'javelin-stratcom',
3 => 'javelin-dom',
),
'a5d7cf86' =>
array(
0 => 'javelin-dom',
),
'a79b75a4' =>
array(
0 => 'javelin-install',
1 => 'javelin-util',
2 => 'javelin-request',
3 => 'javelin-typeahead-source',
),
'a82a7769' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-stratcom',
2 => 'javelin-dom',
),
'a8d8459d' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-stratcom',
),
'a9aaba0c' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-stratcom',
2 => 'javelin-dom',
),
'ad7a69ca' =>
array(
0 => 'javelin-install',
1 => 'javelin-util',
2 => 'javelin-stratcom',
3 => 'javelin-dom',
4 => 'javelin-vector',
),
'ae6abfba' =>
array(
0 => 'javelin-install',
1 => 'javelin-util',
2 => 'javelin-request',
3 => 'javelin-dom',
4 => 'javelin-uri',
5 => 'phabricator-file-upload',
),
'b3a4b884' =>
array(
0 => 'javelin-behavior',
1 => 'phabricator-prefab',
),
'b3e5ee60' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'phortune-credit-card-form',
),
'b3ec3cfc' =>
array(
0 => 'javelin-install',
),
'b4c30592' =>
array(
0 => 'javelin-install',
1 => 'javelin-reactor',
2 => 'javelin-util',
3 => 'javelin-reactor-node-calmer',
),
'b4eef37b' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-request',
3 => 'javelin-util',
),
'b5842a5e' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-behavior-device',
2 => 'javelin-stratcom',
3 => 'javelin-dom',
4 => 'javelin-magical-init',
5 => 'javelin-vector',
6 => 'javelin-request',
7 => 'javelin-util',
),
'b657bdf8' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-uri',
2 => 'phabricator-keyboard-shortcut',
),
'b6d401d6' =>
array(
0 => 'javelin-dom',
1 => 'javelin-dynval',
2 => 'javelin-reactor',
3 => 'javelin-reactornode',
4 => 'javelin-install',
5 => 'javelin-util',
),
'b98fc918' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-vector',
3 => 'javelin-magical-init',
),
'b9f26029' =>
array(
0 => 'javelin-install',
1 => 'javelin-dom',
),
'bc778103' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-stratcom',
2 => 'javelin-dom',
3 => 'javelin-history',
),
'bdaf4d04' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-util',
3 => 'javelin-request',
),
'be81801d' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-util',
3 => 'phabricator-shaped-request',
),
'bf97561d' =>
array(
0 => 'javelin-install',
1 => 'javelin-dom',
2 => 'javelin-reactor-dom',
),
'c01153ea' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-util',
3 => 'phabricator-dropdown-menu',
4 => 'phabricator-menu-item',
5 => 'javelin-workflow',
),
'c021950a' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-stratcom',
2 => 'javelin-dom',
3 => 'phabricator-phtize',
4 => 'phabricator-textareautils',
5 => 'javelin-workflow',
6 => 'javelin-vector',
),
'c293f7b9' =>
array(
0 => 'javelin-install',
1 => 'javelin-event',
2 => 'javelin-util',
3 => 'javelin-magical-init',
),
'c3e218fe' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
),
'c4b31646' =>
array(
0 => 'javelin-request',
1 => 'javelin-behavior',
2 => 'javelin-dom',
3 => 'phabricator-busy',
),
'c51a6616' =>
array(
0 => 'phabricator-notification',
1 => 'javelin-stratcom',
2 => 'javelin-behavior',
),
'c54eeefb' =>
array(
0 => 'javelin-install',
1 => 'javelin-dom',
2 => 'javelin-vector',
3 => 'javelin-util',
),
'c60f4327' =>
array(
0 => 'javelin-stratcom',
1 => 'javelin-install',
2 => 'javelin-uri',
3 => 'javelin-util',
),
'c81bc98f' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-stratcom',
2 => 'javelin-vector',
3 => 'javelin-dom',
4 => 'javelin-uri',
),
'c8728c70' =>
array(
0 => 'javelin-behavior',
1 => 'phabricator-keyboard-shortcut',
2 => 'javelin-stratcom',
),
'ca3f91eb' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-stratcom',
3 => 'phabricator-phtize',
),
'cd9e7094' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-typeahead',
3 => 'javelin-typeahead-ondemand-source',
4 => 'javelin-dom',
),
'cdde23f1' =>
array(
0 => 'javelin-install',
1 => 'javelin-typeahead-source',
),
'ce0c217a' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-workflow',
2 => 'javelin-util',
3 => 'javelin-dom',
4 => 'javelin-stratcom',
5 => 'javelin-behavior-device',
6 => 'javelin-vector',
7 => 'phabricator-tooltip',
),
'cf76cfd5' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-stratcom',
3 => 'javelin-workflow',
4 => 'javelin-fx',
5 => 'javelin-util',
),
'd254d646' =>
array(
0 => 'javelin-util',
),
'd259e7c9' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-stratcom',
3 => 'javelin-workflow',
4 => 'javelin-util',
5 => 'phabricator-keyboard-shortcut',
),
'd4a14807' =>
array(
0 => 'javelin-install',
1 => 'javelin-dom',
2 => 'javelin-view',
),
'd6ca6b1c' =>
array(
0 => 'javelin-install',
1 => 'javelin-reactor-dom',
2 => 'javelin-view-html',
3 => 'javelin-view-interpreter',
4 => 'javelin-view-renderer',
),
'd75709e6' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-workflow',
2 => 'javelin-json',
3 => 'javelin-dom',
4 => 'phabricator-keyboard-shortcut',
),
'd8e135db' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-util',
3 => 'javelin-stratcom',
4 => 'javelin-workflow',
5 => 'phabricator-draggable-list',
),
'd8ef8659' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-stratcom',
3 => 'javelin-workflow',
4 => 'javelin-util',
5 => 'phabricator-notification',
6 => 'javelin-behavior-device',
7 => 'phabricator-dropdown-menu',
8 => 'phabricator-menu-item',
),
'd9a9b862' =>
array(
0 => 'javelin-install',
1 => 'javelin-util',
2 => 'javelin-stratcom',
),
'dd7e8ef5' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-workflow',
3 => 'javelin-util',
4 => 'javelin-stratcom',
),
'dddd43ac' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'phabricator-prefab',
),
'dfa181a4' =>
array(
0 => 'javelin-install',
1 => 'javelin-util',
2 => 'javelin-request',
),
'e1ff79b1' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-stratcom',
2 => 'javelin-dom',
),
'e5822781' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-json',
3 => 'javelin-workflow',
4 => 'javelin-magical-init',
),
'e5b406f9' =>
array(
0 => 'javelin-install',
1 => 'javelin-dom',
2 => 'javelin-view-visitor',
3 => 'javelin-util',
),
'e7c21fb3' =>
array(
0 => 'javelin-dom',
1 => 'javelin-util',
2 => 'javelin-stratcom',
3 => 'javelin-install',
),
'e9fdb5e5' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-stratcom',
2 => 'javelin-util',
3 => 'javelin-dom',
4 => 'javelin-request',
5 => 'phabricator-keyboard-shortcut',
),
'ead554ec' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-stratcom',
3 => 'javelin-util',
),
'efe49472' =>
array(
0 => 'javelin-install',
1 => 'javelin-util',
),
'f2441746' =>
array(
0 => 'javelin-dom',
1 => 'javelin-util',
2 => 'javelin-stratcom',
3 => 'javelin-install',
4 => 'javelin-request',
5 => 'javelin-workflow',
),
'f28bf201' =>
array(
0 => 'javelin-stratcom',
1 => 'javelin-request',
2 => 'javelin-dom',
3 => 'javelin-vector',
4 => 'javelin-install',
5 => 'javelin-util',
6 => 'javelin-mask',
7 => 'javelin-uri',
),
'f42bb8c6' =>
array(
0 => 'javelin-stratcom',
1 => 'javelin-behavior',
2 => 'javelin-vector',
3 => 'javelin-dom',
),
'f6555212' =>
array(
0 => 'javelin-install',
1 => 'javelin-reactornode',
2 => 'javelin-util',
3 => 'javelin-reactor',
),
'f6b56f7a' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-typeahead-ondemand-source',
2 => 'javelin-typeahead',
3 => 'javelin-dom',
4 => 'javelin-uri',
5 => 'javelin-util',
6 => 'javelin-stratcom',
),
'f7379f45' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-util',
3 => 'phabricator-shaped-request',
),
'f778a573' =>
array(
0 => 'javelin-install',
1 => 'javelin-util',
2 => 'javelin-request',
3 => 'javelin-typeahead-source',
),
'f7f1289f' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-stratcom',
),
'f7fc67ec' =>
array(
0 => 'javelin-install',
1 => 'javelin-typeahead',
2 => 'javelin-dom',
3 => 'javelin-request',
4 => 'javelin-typeahead-ondemand-source',
5 => 'javelin-util',
),
'f8248bc5' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-util',
3 => 'javelin-json',
4 => 'javelin-stratcom',
5 => 'phabricator-shaped-request',
),
'fb342e18' =>
array(
0 => 'javelin-install',
1 => 'javelin-util',
2 => 'javelin-dom',
3 => 'javelin-vector',
4 => 'javelin-stratcom',
5 => 'phabricator-menu-item',
),
'fbbce3bf' =>
array(
0 => 'phabricator-busy',
1 => 'javelin-behavior',
),
'fe2e0ba4' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-behavior-device',
2 => 'javelin-stratcom',
3 => 'javelin-vector',
4 => 'javelin-dom',
5 => 'javelin-magical-init',
),
'fe80fb6d' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-util',
3 => 'phabricator-prefab',
4 => 'multirow-row-manager',
5 => 'javelin-json',
),
'fee00761' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-stratcom',
2 => 'javelin-workflow',
3 => 'javelin-dom',
),
28497740 =>
array(
0 => 'javelin-behavior',
1 => 'javelin-util',
2 => 'javelin-stratcom',
3 => 'javelin-dom',
4 => 'javelin-vector',
5 => 'javelin-magical-init',
6 => 'javelin-request',
7 => 'javelin-history',
8 => 'javelin-workflow',
9 => 'javelin-mask',
10 => 'javelin-behavior-device',
11 => 'phabricator-keyboard-shortcut',
),
36903077 =>
array(
0 => 'aphront-typeahead-control-css',
),
42126667 =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-request',
),
44524435 =>
array(
0 => 'javelin-install',
1 => 'javelin-dom',
2 => 'javelin-util',
3 => 'javelin-dynval',
4 => 'javelin-reactor-dom',
),
48086888 =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-workflow',
),
),
'packages' =>
array(
'core.pkg.css' =>
array(
0 => 'phabricator-core-css',
1 => 'phabricator-zindex-css',
2 => 'phui-button-css',
3 => 'phabricator-standard-page-view',
4 => 'aphront-dialog-view-css',
5 => 'phui-form-view-css',
6 => 'aphront-panel-view-css',
7 => 'aphront-table-view-css',
8 => 'aphront-tokenizer-control-css',
9 => 'aphront-typeahead-control-css',
10 => 'aphront-list-filter-view-css',
11 => 'phabricator-jump-nav',
12 => 'phabricator-remarkup-css',
13 => 'syntax-highlighting-css',
14 => 'aphront-pager-view-css',
15 => 'phabricator-transaction-view-css',
16 => 'aphront-tooltip-css',
17 => 'phabricator-flag-css',
18 => 'aphront-error-view-css',
19 => 'sprite-icons-css',
20 => 'sprite-gradient-css',
21 => 'sprite-menu-css',
22 => 'sprite-apps-large-css',
23 => 'sprite-status-css',
24 => 'phabricator-main-menu-view',
25 => 'phabricator-notification-css',
26 => 'phabricator-notification-menu-css',
27 => 'lightbox-attachment-css',
28 => 'phui-header-view-css',
29 => 'phabricator-filetree-view-css',
30 => 'phabricator-nav-view-css',
31 => 'phabricator-side-menu-view-css',
32 => 'phabricator-crumbs-view-css',
33 => 'phui-object-item-list-view-css',
34 => 'global-drag-and-drop-css',
35 => 'phui-spacing-css',
36 => 'phui-form-css',
37 => 'phui-icon-view-css',
38 => 'phabricator-application-launch-view-css',
39 => 'phabricator-action-list-view-css',
40 => 'phui-property-list-view-css',
41 => 'phui-tag-view-css',
42 => 'phui-list-view-css',
),
'core.pkg.js' =>
array(
0 => 'javelin-behavior-aphront-basic-tokenizer',
1 => 'javelin-behavior-workflow',
2 => 'javelin-behavior-aphront-form-disable-on-submit',
3 => 'phabricator-keyboard-shortcut-manager',
4 => 'phabricator-keyboard-shortcut',
5 => 'javelin-behavior-phabricator-keyboard-shortcuts',
6 => 'javelin-behavior-refresh-csrf',
7 => 'javelin-behavior-phabricator-watch-anchor',
8 => 'javelin-behavior-phabricator-autofocus',
9 => 'phabricator-menu-item',
10 => 'phabricator-dropdown-menu',
11 => 'phabricator-phtize',
12 => 'javelin-behavior-phabricator-oncopy',
13 => 'phabricator-tooltip',
14 => 'javelin-behavior-phabricator-tooltips',
15 => 'phabricator-prefab',
16 => 'javelin-behavior-device',
17 => 'javelin-behavior-toggle-class',
18 => 'javelin-behavior-lightbox-attachments',
19 => 'phabricator-busy',
20 => 'javelin-aphlict',
21 => 'phabricator-notification',
22 => 'javelin-behavior-aphlict-listen',
23 => 'javelin-behavior-phabricator-search-typeahead',
24 => 'javelin-behavior-konami',
25 => 'javelin-behavior-aphlict-dropdown',
26 => 'javelin-behavior-history-install',
27 => 'javelin-behavior-phabricator-gesture',
28 => 'javelin-behavior-phabricator-active-nav',
29 => 'javelin-behavior-phabricator-nav',
30 => 'javelin-behavior-phabricator-remarkup-assist',
31 => 'phabricator-textareautils',
32 => 'phabricator-file-upload',
33 => 'javelin-behavior-global-drag-and-drop',
34 => 'javelin-behavior-phabricator-reveal-content',
35 => 'phabricator-hovercard',
36 => 'javelin-behavior-phabricator-hovercards',
37 => 'javelin-color',
38 => 'javelin-fx',
),
'darkconsole.pkg.js' =>
array(
0 => 'javelin-behavior-dark-console',
1 => 'javelin-behavior-error-log',
),
'differential.pkg.css' =>
array(
0 => 'differential-core-view-css',
1 => 'differential-changeset-view-css',
2 => 'differential-results-table-css',
3 => 'differential-revision-history-css',
4 => 'differential-revision-list-css',
5 => 'differential-table-of-contents-css',
6 => 'differential-revision-comment-css',
7 => 'differential-revision-add-comment-css',
8 => 'phabricator-object-selector-css',
9 => 'phabricator-content-source-view-css',
10 => 'inline-comment-summary-css',
),
'differential.pkg.js' =>
array(
0 => 'phabricator-drag-and-drop-file-upload',
1 => 'phabricator-shaped-request',
2 => 'javelin-behavior-differential-feedback-preview',
3 => 'javelin-behavior-differential-edit-inline-comments',
4 => 'javelin-behavior-differential-populate',
5 => 'javelin-behavior-differential-show-more',
6 => 'javelin-behavior-differential-diff-radios',
7 => 'javelin-behavior-differential-comment-jump',
8 => 'javelin-behavior-differential-add-reviewers-and-ccs',
9 => 'javelin-behavior-differential-keyboard-navigation',
10 => 'javelin-behavior-aphront-drag-and-drop-textarea',
11 => 'javelin-behavior-phabricator-object-selector',
12 => 'javelin-behavior-repository-crossreference',
13 => 'javelin-behavior-load-blame',
14 => 'differential-inline-comment-editor',
15 => 'javelin-behavior-differential-dropdown-menus',
16 => 'javelin-behavior-differential-toggle-files',
17 => 'javelin-behavior-differential-user-select',
),
'diffusion.pkg.css' =>
array(
0 => 'diffusion-commit-view-css',
1 => 'diffusion-icons-css',
),
'diffusion.pkg.js' =>
array(
0 => 'javelin-behavior-diffusion-pull-lastmodified',
1 => 'javelin-behavior-diffusion-commit-graph',
2 => 'javelin-behavior-audit-preview',
),
'javelin.pkg.js' =>
array(
0 => 'javelin-util',
1 => 'javelin-install',
2 => 'javelin-event',
3 => 'javelin-stratcom',
4 => 'javelin-behavior',
5 => 'javelin-resource',
6 => 'javelin-request',
7 => 'javelin-vector',
8 => 'javelin-dom',
9 => 'javelin-json',
10 => 'javelin-uri',
11 => 'javelin-workflow',
12 => 'javelin-mask',
13 => 'javelin-typeahead',
14 => 'javelin-typeahead-normalizer',
15 => 'javelin-typeahead-source',
16 => 'javelin-typeahead-preloaded-source',
17 => 'javelin-typeahead-ondemand-source',
18 => 'javelin-tokenizer',
19 => 'javelin-history',
),
'maniphest.pkg.css' =>
array(
0 => 'maniphest-task-summary-css',
1 => 'phabricator-project-tag-css',
),
'maniphest.pkg.js' =>
array(
0 => 'javelin-behavior-maniphest-batch-selector',
1 => 'javelin-behavior-maniphest-transaction-controls',
2 => 'javelin-behavior-maniphest-transaction-preview',
3 => 'javelin-behavior-maniphest-transaction-expand',
4 => 'javelin-behavior-maniphest-subpriority-editor',
),
),
);
diff --git a/src/applications/metamta/PhabricatorMetaMTAWorker.php b/src/applications/metamta/PhabricatorMetaMTAWorker.php
index d9defcd8df..60fc1dcb92 100644
--- a/src/applications/metamta/PhabricatorMetaMTAWorker.php
+++ b/src/applications/metamta/PhabricatorMetaMTAWorker.php
@@ -1,49 +1,46 @@
<?php
final class PhabricatorMetaMTAWorker
extends PhabricatorWorker {
public function getMaximumRetryCount() {
return 250;
}
public function getWaitBeforeRetry(PhabricatorWorkerTask $task) {
return ($task->getFailureCount() * 15);
}
public function doWork() {
$message = $this->loadMessage();
if (!$message) {
throw new PhabricatorWorkerPermanentFailureException(
pht('Unable to load message!'));
}
if ($message->getStatus() != PhabricatorMetaMTAMail::STATUS_QUEUE) {
return;
}
- $id = $message->getID();
- $message->sendNow();
-
- // task failed if the message is still queued
- // (instead of sent, void, or failed)
- if ($message->getStatus() == PhabricatorMetaMTAMail::STATUS_QUEUE) {
- throw new Exception(
- pht('Failed to send message.'));
+ try {
+ $message->sendNow();
+ } catch (PhabricatorMetaMTAPermanentFailureException $ex) {
+ // If the mailer fails permanently, fail this task permanently.
+ throw new PhabricatorWorkerPermanentFailureException($ex->getMessage());
}
}
private function loadMessage() {
$message_id = $this->getTaskData();
return id(new PhabricatorMetaMTAMail())->load($message_id);
}
public function renderForDisplay(PhabricatorUser $viewer) {
return phutil_tag(
'pre',
array(
),
'phabricator/ $ ./bin/mail show-outbound --id '.$this->getTaskData());
}
}
diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php
index 04d328a692..c7cd05753b 100644
--- a/src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php
@@ -1,124 +1,137 @@
<?php
/**
* Mail adapter that uses Mailgun's web API to deliver email.
*/
final class PhabricatorMailImplementationMailgunAdapter
extends PhabricatorMailImplementationAdapter {
private $params = array();
+ private $attachments = array();
public function setFrom($email, $name = '') {
$this->params['from'] = $email;
$this->params['from-name'] = $name;
return $this;
}
public function addReplyTo($email, $name = '') {
if (empty($this->params['reply-to'])) {
$this->params['reply-to'] = array();
}
$this->params['reply-to'][] = "{$name} <{$email}>";
return $this;
}
public function addTos(array $emails) {
foreach ($emails as $email) {
$this->params['tos'][] = $email;
}
return $this;
}
public function addCCs(array $emails) {
foreach ($emails as $email) {
$this->params['ccs'][] = $email;
}
return $this;
}
public function addAttachment($data, $filename, $mimetype) {
- // TODO: implement attachments. Requires changes in HTTPSFuture
- throw new Exception(
- "Mailgun adapter does not currently support attachments.");
+ $this->attachments[] = array(
+ 'data' => $data,
+ 'name' => $filename,
+ 'type' => $mimetype,
+ );
+
+ return $this;
}
public function addHeader($header_name, $header_value) {
$this->params['headers'][] = array($header_name, $header_value);
return $this;
}
public function setBody($body) {
$this->params['body'] = $body;
return $this;
}
public function setSubject($subject) {
$this->params['subject'] = $subject;
return $this;
}
public function setIsHTML($is_html) {
$this->params['is-html'] = $is_html;
return $this;
}
public function supportsMessageIDHeader() {
return false;
}
public function send() {
$key = PhabricatorEnv::getEnvConfig('mailgun.api-key');
$domain = PhabricatorEnv::getEnvConfig('mailgun.domain');
$params = array();
- $params['to'] = idx($this->params, 'tos', array());
+ $params['to'] = implode(', ', idx($this->params, 'tos', array()));
$params['subject'] = idx($this->params, 'subject');
if (idx($this->params, 'is-html')) {
$params['html'] = idx($this->params, 'body');
} else {
$params['text'] = idx($this->params, 'body');
}
$from = idx($this->params, 'from');
if (idx($this->params, 'from-name')) {
$params['from'] = "{$this->params['from-name']} <{$from}>";
} else {
$params['from'] = $from;
}
if (idx($this->params, 'reply-to')) {
$replyto = $this->params['reply-to'];
$params['h:reply-to'] = implode(', ', $replyto);
}
if (idx($this->params, 'ccs')) {
- $params['cc'] = $this->params['ccs'];
+ $params['cc'] = implode(', ', $this->params['ccs']);
}
foreach (idx($this->params, 'headers', array()) as $header) {
list($name, $value) = $header;
$params['h:'.$name] = $value;
}
$future = new HTTPSFuture(
"https://api:{$key}@api.mailgun.net/v2/{$domain}/messages",
$params);
$future->setMethod('POST');
+ foreach ($this->attachments as $attachment) {
+ $future->attachFileData(
+ 'attachment',
+ $attachment['data'],
+ $attachment['name'],
+ $attachment['type']);
+ }
+
list($body) = $future->resolvex();
$response = json_decode($body, true);
if (!is_array($response)) {
throw new Exception("Failed to JSON decode response: {$body}");
}
if (!idx($response, 'id')) {
$message = $response['message'];
throw new Exception("Request failed with errors: {$message}.");
}
return true;
}
}
diff --git a/src/applications/metamta/management/PhabricatorMailManagementResendWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementResendWorkflow.php
index 1a4ef6ba70..c586997172 100644
--- a/src/applications/metamta/management/PhabricatorMailManagementResendWorkflow.php
+++ b/src/applications/metamta/management/PhabricatorMailManagementResendWorkflow.php
@@ -1,72 +1,60 @@
<?php
final class PhabricatorMailManagementResendWorkflow
extends PhabricatorMailManagementWorkflow {
protected function didConstruct() {
$this
->setName('resend')
->setSynopsis('Send mail again.')
->setExamples(
"**resend** --id 1 --id 2")
->setArguments(
array(
array(
'name' => 'id',
'param' => 'id',
'help' => 'Send mail with a given ID again.',
'repeat' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$ids = $args->getArg('id');
if (!$ids) {
throw new PhutilArgumentUsageException(
"Use the '--id' flag to specify one or more messages to resend.");
}
$messages = id(new PhabricatorMetaMTAMail())->loadAllWhere(
'id IN (%Ld)',
$ids);
if ($ids) {
$ids = array_fuse($ids);
$missing = array_diff_key($ids, $messages);
if ($missing) {
throw new PhutilArgumentUsageException(
"Some specified messages do not exist: ".
implode(', ', array_keys($missing)));
}
}
foreach ($messages as $message) {
- if ($message->getStatus() == PhabricatorMetaMTAMail::STATUS_QUEUE) {
- if ($message->getWorkerTaskID()) {
- $console->writeOut(
- "Message #%d is already queued with an assigned send task.\n",
- $message->getID());
- continue;
- }
- }
-
$message->setStatus(PhabricatorMetaMTAMail::STATUS_QUEUE);
$message->save();
$mailer_task = PhabricatorWorker::scheduleTask(
'PhabricatorMetaMTAWorker',
$message->getID());
- $message->setWorkerTaskID($mailer_task->getID());
- $message->save();
-
$console->writeOut(
"Queued message #%d for resend.\n",
$message->getID());
}
}
}
diff --git a/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php
index ac18387ac7..4af5d5a79e 100644
--- a/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php
+++ b/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php
@@ -1,138 +1,146 @@
<?php
final class PhabricatorMailManagementSendTestWorkflow
extends PhabricatorMailManagementWorkflow {
protected function didConstruct() {
$this
->setName('send-test')
->setSynopsis(
pht(
'Simulate sending mail. This may be useful to test your mail '.
'configuration, or while developing new mail adapters.'))
->setExamples(
"**send-test** --to alincoln --subject hi < body.txt")
->setArguments(
array(
array(
'name' => 'from',
'param' => 'user',
'help' => 'Send mail from the specified user.',
),
array(
'name' => 'to',
'param' => 'user',
'help' => 'Send mail "To:" the specified users.',
'repeat' => true,
),
array(
'name' => 'cc',
'param' => 'user',
'help' => 'Send mail which "Cc:"s the specified users.',
'repeat' => true,
),
array(
'name' => 'subject',
'param' => 'text',
'help' => 'Use the provided subject.',
),
array(
'name' => 'tag',
'param' => 'text',
'help' => 'Add the given mail tags.',
'repeat' => true,
),
array(
'name' => 'attach',
'param' => 'file',
'help' => 'Attach a file.',
'repeat' => true,
),
array(
'name' => 'html',
'help' => 'Send as HTML mail.',
),
array(
'name' => 'bulk',
'help' => 'Send with bulk headers.',
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$viewer = $this->getViewer();
$from = $args->getArg('from');
if ($from) {
$user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withUsernames(array($from))
->executeOne();
if (!$user) {
throw new PhutilArgumentUsageException(
pht("No such user '%s' exists.", $from));
}
$from = $user;
}
$tos = $args->getArg('to');
$ccs = $args->getArg('cc');
$names = array_merge($tos, $ccs);
$users = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withUsernames($names)
->execute();
$users = mpull($users, null, 'getUsername');
foreach ($tos as $key => $username) {
if (empty($users[$username])) {
throw new PhutilArgumentUsageException(
pht("No such user '%s' exists.", $username));
}
$tos[$key] = $users[$username]->getPHID();
}
foreach ($ccs as $key => $username) {
if (empty($users[$username])) {
throw new PhutilArgumentUsageException(
pht("No such user '%s' exists.", $username));
}
$ccs[$key] = $users[$username]->getPHID();
}
$subject = $args->getArg('subject');
$tags = $args->getArg('tag');
$attach = $args->getArg('attach');
$is_html = $args->getArg('html');
$is_bulk = $args->getArg('bulk');
$console->writeErr("%s\n", pht('Reading message body from stdin...'));
$body = file_get_contents('php://stdin');
$mail = id(new PhabricatorMetaMTAMail())
->addTos($tos)
->addCCs($ccs)
->setSubject($subject)
->setBody($body)
->setOverrideNoSelfMailPreference(true)
->setIsHTML($is_html)
->setIsBulk($is_bulk)
->setMailTags($tags);
if ($from) {
$mail->setFrom($from->getPHID());
}
+ foreach ($attach as $attachment) {
+ $data = Filesystem::readFile($attachment);
+ $name = basename($attachment);
+ $mime = Filesystem::getMimeType($attachment);
+ $file = new PhabricatorMetaMTAAttachment($data, $name, $mime);
+ $mail->addAttachment($file);
+ }
+
+ PhabricatorWorker::setRunAllTasksInProcess(true);
$mail->save();
- $mail->sendNow();
$console->writeErr(
"%s\n\n phabricator/ $ ./bin/mail show-outbound --id %d\n\n",
pht("Mail sent! You can view details by running this command:"),
$mail->getID());
}
}
diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php
index 9899226ec9..6e1b5132da 100644
--- a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php
+++ b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php
@@ -1,890 +1,889 @@
<?php
/**
* @task recipients Managing Recipients
*/
final class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO {
const STATUS_QUEUE = 'queued';
const STATUS_SENT = 'sent';
const STATUS_FAIL = 'fail';
const STATUS_VOID = 'void';
const RETRY_DELAY = 5;
protected $parameters;
protected $status;
protected $message;
protected $relatedPHID;
private $excludePHIDs = array();
private $overrideNoSelfMail = false;
private $recipientExpansionMap;
public function __construct() {
$this->status = self::STATUS_QUEUE;
$this->parameters = array();
parent::__construct();
}
public function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'parameters' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
protected function setParam($param, $value) {
$this->parameters[$param] = $value;
return $this;
}
protected function getParam($param, $default = null) {
return idx($this->parameters, $param, $default);
}
/**
* Set tags (@{class:MetaMTANotificationType} constants) which identify the
* content of this mail in a general way. These tags are used to allow users
* to opt out of receiving certain types of mail, like updates when a task's
* projects change.
*
* @param list<const> List of @{class:MetaMTANotificationType} constants.
* @return this
*/
public function setMailTags(array $tags) {
$this->setParam('mailtags', array_unique($tags));
return $this;
}
public function getMailTags() {
return $this->getParam('mailtags', array());
}
/**
* In Gmail, conversations will be broken if you reply to a thread and the
* server sends back a response without referencing your Message-ID, even if
* it references a Message-ID earlier in the thread. To avoid this, use the
* parent email's message ID explicitly if it's available. This overwrites the
* "In-Reply-To" and "References" headers we would otherwise generate. This
* needs to be set whenever an action is triggered by an email message. See
* T251 for more details.
*
* @param string The "Message-ID" of the email which precedes this one.
* @return this
*/
public function setParentMessageID($id) {
$this->setParam('parent-message-id', $id);
return $this;
}
public function getParentMessageID() {
return $this->getParam('parent-message-id');
}
public function getSubject() {
return $this->getParam('subject');
}
public function addTos(array $phids) {
$phids = array_unique($phids);
$this->setParam('to', $phids);
return $this;
}
public function addRawTos(array $raw_email) {
$this->setParam('raw-to', $raw_email);
return $this;
}
public function addCCs(array $phids) {
$phids = array_unique($phids);
$this->setParam('cc', $phids);
return $this;
}
public function setExcludeMailRecipientPHIDs($exclude) {
$this->excludePHIDs = $exclude;
return $this;
}
private function getExcludeMailRecipientPHIDs() {
return $this->excludePHIDs;
}
public function getOverrideNoSelfMailPreference() {
return $this->overrideNoSelfMail;
}
public function setOverrideNoSelfMailPreference($override) {
$this->overrideNoSelfMail = $override;
return $this;
}
public function getTranslation(array $objects) {
$default_translation = PhabricatorEnv::getEnvConfig('translation.provider');
$return = null;
$recipients = array_merge(
idx($this->parameters, 'to', array()),
idx($this->parameters, 'cc', array()));
foreach (array_select_keys($objects, $recipients) as $object) {
$translation = null;
if ($object instanceof PhabricatorUser) {
$translation = $object->getTranslation();
}
if (!$translation) {
$translation = $default_translation;
}
if ($return && $translation != $return) {
return $default_translation;
}
$return = $translation;
}
if (!$return) {
$return = $default_translation;
}
return $return;
}
public function addPHIDHeaders($name, array $phids) {
foreach ($phids as $phid) {
$this->addHeader($name, '<'.$phid.'>');
}
return $this;
}
public function addHeader($name, $value) {
$this->parameters['headers'][] = array($name, $value);
return $this;
}
public function addAttachment(PhabricatorMetaMTAAttachment $attachment) {
$this->parameters['attachments'][] = $attachment->toDictionary();
return $this;
}
public function getAttachments() {
$dicts = $this->getParam('attachments');
$result = array();
foreach ($dicts as $dict) {
$result[] = PhabricatorMetaMTAAttachment::newFromDictionary($dict);
}
return $result;
}
public function setAttachments(array $attachments) {
assert_instances_of($attachments, 'PhabricatorMetaMTAAttachment');
$this->setParam('attachments', mpull($attachments, 'toDictionary'));
return $this;
}
public function setFrom($from) {
$this->setParam('from', $from);
return $this;
}
public function setReplyTo($reply_to) {
$this->setParam('reply-to', $reply_to);
return $this;
}
public function setSubject($subject) {
$this->setParam('subject', $subject);
return $this;
}
public function setSubjectPrefix($prefix) {
$this->setParam('subject-prefix', $prefix);
return $this;
}
public function setVarySubjectPrefix($prefix) {
$this->setParam('vary-subject-prefix', $prefix);
return $this;
}
public function setBody($body) {
$this->setParam('body', $body);
return $this;
}
public function getBody() {
return $this->getParam('body');
}
public function setIsHTML($html) {
$this->setParam('is-html', $html);
return $this;
}
public function setIsErrorEmail($is_error) {
$this->setParam('is-error', $is_error);
return $this;
}
public function getIsErrorEmail() {
return $this->getParam('is-error', false);
}
- public function getWorkerTaskID() {
- return $this->getParam('worker-task');
- }
-
- public function setWorkerTaskID($id) {
- $this->setParam('worker-task', $id);
- return $this;
- }
-
public function getToPHIDs() {
return $this->getParam('to', array());
}
public function getRawToAddresses() {
return $this->getParam('raw-to', array());
}
public function getCcPHIDs() {
return $this->getParam('cc', array());
}
/**
* Flag that this is an auto-generated bulk message and should have bulk
* headers added to it if appropriate. Broadly, this means some flavor of
* "Precedence: bulk" or similar, but is implementation and configuration
* dependent.
*
* @param bool True if the mail is automated bulk mail.
* @return this
*/
public function setIsBulk($is_bulk) {
$this->setParam('is-bulk', $is_bulk);
return $this;
}
/**
* Use this method to set an ID used for message threading. MetaMTA will
* set appropriate headers (Message-ID, In-Reply-To, References and
* Thread-Index) based on the capabilities of the underlying mailer.
*
* @param string Unique identifier, appropriate for use in a Message-ID,
* In-Reply-To or References headers.
* @param bool If true, indicates this is the first message in the thread.
* @return this
*/
public function setThreadID($thread_id, $is_first_message = false) {
$this->setParam('thread-id', $thread_id);
$this->setParam('is-first-message', $is_first_message);
return $this;
}
/**
* Save a newly created mail to the database. The mail will eventually be
* delivered by the MetaMTA daemon.
*
* @return this
*/
public function saveAndSend() {
return $this->save();
}
public function save() {
if ($this->getID()) {
return parent::save();
}
// NOTE: When mail is sent from CLI scripts that run tasks in-process, we
// may re-enter this method from within scheduleTask(). The implementation
// is intended to avoid anything awkward if we end up reentering this
// method.
$this->openTransaction();
// Save to generate a task ID.
- parent::save();
+ $result = parent::save();
// Queue a task to send this mail.
$mailer_task = PhabricatorWorker::scheduleTask(
'PhabricatorMetaMTAWorker',
$this->getID());
- // Save again to update the task ID.
- $this->setWorkerTaskID($mailer_task->getID());
- $result = parent::save();
$this->saveTransaction();
return $result;
}
public function buildDefaultMailer() {
return PhabricatorEnv::newObjectFromConfig('metamta.mail-adapter');
}
/**
* Attempt to deliver an email immediately, in this process.
*
* @param bool Try to deliver this email even if it has already been
* delivered or is in backoff after a failed delivery attempt.
* @param PhabricatorMailImplementationAdapter Use a specific mail adapter,
* instead of the default.
*
* @return void
*/
public function sendNow(
$force_send = false,
PhabricatorMailImplementationAdapter $mailer = null) {
if ($mailer === null) {
$mailer = $this->buildDefaultMailer();
}
if (!$force_send) {
if ($this->getStatus() != self::STATUS_QUEUE) {
throw new Exception("Trying to send an already-sent mail!");
}
}
try {
$params = $this->parameters;
$actors = $this->loadAllActors();
$deliverable_actors = $this->filterDeliverableActors($actors);
$default_from = PhabricatorEnv::getEnvConfig('metamta.default-address');
if (empty($params['from'])) {
$mailer->setFrom($default_from);
}
$is_first = idx($params, 'is-first-message');
unset($params['is-first-message']);
$is_threaded = (bool)idx($params, 'thread-id');
$reply_to_name = idx($params, 'reply-to-name', '');
unset($params['reply-to-name']);
$add_cc = array();
$add_to = array();
foreach ($params as $key => $value) {
switch ($key) {
case 'from':
$from = $value;
$actor_email = null;
$actor_name = null;
$actor = idx($actors, $from);
if ($actor) {
$actor_email = $actor->getEmailAddress();
$actor_name = $actor->getName();
}
$can_send_as_user = $actor_email &&
PhabricatorEnv::getEnvConfig('metamta.can-send-as-user');
if ($can_send_as_user) {
$mailer->setFrom($actor_email, $actor_name);
} else {
$from_email = coalesce($actor_email, $default_from);
$from_name = coalesce($actor_name, pht('Phabricator'));
if (empty($params['reply-to'])) {
$params['reply-to'] = $from_email;
$params['reply-to-name'] = $from_name;
}
$mailer->setFrom($default_from, $from_name);
}
break;
case 'reply-to':
$mailer->addReplyTo($value, $reply_to_name);
break;
case 'to':
$to_phids = $this->expandRecipients($value);
$to_actors = array_select_keys($deliverable_actors, $to_phids);
$add_to = array_merge(
$add_to,
mpull($to_actors, 'getEmailAddress'));
break;
case 'raw-to':
$add_to = array_merge($add_to, $value);
break;
case 'cc':
$cc_phids = $this->expandRecipients($value);
$cc_actors = array_select_keys($deliverable_actors, $cc_phids);
$add_cc = array_merge(
$add_cc,
mpull($cc_actors, 'getEmailAddress'));
break;
case 'headers':
foreach ($value as $pair) {
list($header_key, $header_value) = $pair;
// NOTE: If we have \n in a header, SES rejects the email.
$header_value = str_replace("\n", " ", $header_value);
$mailer->addHeader($header_key, $header_value);
}
break;
case 'attachments':
$value = $this->getAttachments();
foreach ($value as $attachment) {
$mailer->addAttachment(
$attachment->getData(),
$attachment->getFilename(),
$attachment->getMimeType());
}
break;
case 'body':
$max = PhabricatorEnv::getEnvConfig('metamta.email-body-limit');
if (strlen($value) > $max) {
$value = phutil_utf8_shorten($value, $max);
$value .= "\n";
$value .= pht('(This email was truncated at %d bytes.)', $max);
}
$mailer->setBody($value);
break;
case 'subject':
// Only try to use preferences if everything is multiplexed, so we
// get consistent behavior.
$use_prefs = self::shouldMultiplexAllMail();
$prefs = null;
if ($use_prefs) {
// If multiplexing is enabled, some recipients will be in "Cc"
// rather than "To". We'll move them to "To" later (or supply a
// dummy "To") but need to look for the recipient in either the
// "To" or "Cc" fields here.
$target_phid = head(idx($params, 'to', array()));
if (!$target_phid) {
$target_phid = head(idx($params, 'cc', array()));
}
if ($target_phid) {
$user = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$target_phid);
if ($user) {
$prefs = $user->loadPreferences();
}
}
}
$subject = array();
if ($is_threaded) {
$add_re = PhabricatorEnv::getEnvConfig('metamta.re-prefix');
if ($prefs) {
$add_re = $prefs->getPreference(
PhabricatorUserPreferences::PREFERENCE_RE_PREFIX,
$add_re);
}
if ($add_re) {
$subject[] = 'Re:';
}
}
$subject[] = trim(idx($params, 'subject-prefix'));
$vary_prefix = idx($params, 'vary-subject-prefix');
if ($vary_prefix != '') {
$use_subject = PhabricatorEnv::getEnvConfig(
'metamta.vary-subjects');
if ($prefs) {
$use_subject = $prefs->getPreference(
PhabricatorUserPreferences::PREFERENCE_VARY_SUBJECT,
$use_subject);
}
if ($use_subject) {
$subject[] = $vary_prefix;
}
}
$subject[] = $value;
$mailer->setSubject(implode(' ', array_filter($subject)));
break;
case 'is-html':
if ($value) {
$mailer->setIsHTML(true);
}
break;
case 'is-bulk':
if ($value) {
if (PhabricatorEnv::getEnvConfig('metamta.precedence-bulk')) {
$mailer->addHeader('Precedence', 'bulk');
}
}
break;
case 'thread-id':
// NOTE: Gmail freaks out about In-Reply-To and References which
// aren't in the form "<string@domain.tld>"; this is also required
// by RFC 2822, although some clients are more liberal in what they
// accept.
$domain = PhabricatorEnv::getEnvConfig('metamta.domain');
$value = '<'.$value.'@'.$domain.'>';
if ($is_first && $mailer->supportsMessageIDHeader()) {
$mailer->addHeader('Message-ID', $value);
} else {
$in_reply_to = $value;
$references = array($value);
$parent_id = $this->getParentMessageID();
if ($parent_id) {
$in_reply_to = $parent_id;
// By RFC 2822, the most immediate parent should appear last
// in the "References" header, so this order is intentional.
$references[] = $parent_id;
}
$references = implode(' ', $references);
$mailer->addHeader('In-Reply-To', $in_reply_to);
$mailer->addHeader('References', $references);
}
$thread_index = $this->generateThreadIndex($value, $is_first);
$mailer->addHeader('Thread-Index', $thread_index);
break;
case 'mailtags':
// Handled below.
break;
case 'subject-prefix':
case 'vary-subject-prefix':
// Handled above.
break;
default:
// Just discard.
}
}
if (!$add_to && !$add_cc) {
$this->setStatus(self::STATUS_VOID);
$this->setMessage(
"Message has no valid recipients: all To/Cc are disabled, invalid, ".
"or configured not to receive this mail.");
return $this->save();
}
if ($this->getIsErrorEmail()) {
$all_recipients = array_merge($add_to, $add_cc);
if ($this->shouldRateLimitMail($all_recipients)) {
$this->setStatus(self::STATUS_VOID);
$this->setMessage(
pht(
'This is an error email, but one or more recipients have '.
'exceeded the error email rate limit. Declining to deliver '.
'message.'));
return $this->save();
}
}
$mailer->addHeader('X-Phabricator-Sent-This-Message', 'Yes');
$mailer->addHeader('X-Mail-Transport-Agent', 'MetaMTA');
// Some clients respect this to suppress OOF and other auto-responses.
$mailer->addHeader('X-Auto-Response-Suppress', 'All');
// If the message has mailtags, filter out any recipients who don't want
// to receive this type of mail.
$mailtags = $this->getParam('mailtags');
if ($mailtags) {
$tag_header = array();
foreach ($mailtags as $mailtag) {
$tag_header[] = '<'.$mailtag.'>';
}
$tag_header = implode(', ', $tag_header);
$mailer->addHeader('X-Phabricator-Mail-Tags', $tag_header);
}
// Some mailers require a valid "To:" in order to deliver mail. If we
// don't have any "To:", try to fill it in with a placeholder "To:".
// If that also fails, move the "Cc:" line to "To:".
if (!$add_to) {
$placeholder_key = 'metamta.placeholder-to-recipient';
$placeholder = PhabricatorEnv::getEnvConfig($placeholder_key);
if ($placeholder !== null) {
$add_to = array($placeholder);
} else {
$add_to = $add_cc;
$add_cc = array();
}
}
$add_to = array_unique($add_to);
$add_cc = array_unique($add_cc);
$mailer->addTos($add_to);
if ($add_cc) {
$mailer->addCCs($add_cc);
}
} catch (Exception $ex) {
- $this->setStatus(self::STATUS_FAIL);
- $this->setMessage($ex->getMessage());
- return $this->save();
+ $this
+ ->setStatus(self::STATUS_FAIL)
+ ->setMessage($ex->getMessage())
+ ->save();
+
+ throw $ex;
}
try {
$ok = $mailer->send();
- $error = null;
+ if (!$ok) {
+ // TODO: At some point, we should clean this up and make all mailers
+ // throw.
+ throw new Exception(
+ pht('Mail adapter encountered an unexpected, unspecified failure.'));
+ }
+
+ $this->setStatus(self::STATUS_SENT);
+ $this->save();
+
+ return $this;
} catch (PhabricatorMetaMTAPermanentFailureException $ex) {
- $this->setStatus(self::STATUS_FAIL);
- $this->setMessage($ex->getMessage());
- return $this->save();
+ $this
+ ->setStatus(self::STATUS_FAIL)
+ ->setMessage($ex->getMessage())
+ ->save();
+
+ throw $ex;
} catch (Exception $ex) {
- $ok = false;
- $error = $ex->getMessage()."\n".$ex->getTraceAsString();
- }
+ $this
+ ->setMessage($ex->getMessage()."\n".$ex->getTraceAsString())
+ ->save();
- if (!$ok) {
- $this->setMessage($error);
- } else {
- $this->setStatus(self::STATUS_SENT);
+ throw $ex;
}
-
- return $this->save();
}
public static function getReadableStatus($status_code) {
static $readable = array(
self::STATUS_QUEUE => "Queued for Delivery",
self::STATUS_FAIL => "Delivery Failed",
self::STATUS_SENT => "Sent",
self::STATUS_VOID => "Void",
);
$status_code = coalesce($status_code, '?');
return idx($readable, $status_code, $status_code);
}
private function generateThreadIndex($seed, $is_first_mail) {
// When threading, Outlook ignores the 'References' and 'In-Reply-To'
// headers that most clients use. Instead, it uses a custom 'Thread-Index'
// header. The format of this header is something like this (from
// camel-exchange-folder.c in Evolution Exchange):
/* A new post to a folder gets a 27-byte-long thread index. (The value
* is apparently unique but meaningless.) Each reply to a post gets a
* 32-byte-long thread index whose first 27 bytes are the same as the
* parent's thread index. Each reply to any of those gets a
* 37-byte-long thread index, etc. The Thread-Index header contains a
* base64 representation of this value.
*/
// The specific implementation uses a 27-byte header for the first email
// a recipient receives, and a random 5-byte suffix (32 bytes total)
// thereafter. This means that all the replies are (incorrectly) siblings,
// but it would be very difficult to keep track of the entire tree and this
// gets us reasonable client behavior.
$base = substr(md5($seed), 0, 27);
if (!$is_first_mail) {
// Not totally sure, but it seems like outlook orders replies by
// thread-index rather than timestamp, so to get these to show up in the
// right order we use the time as the last 4 bytes.
$base .= ' '.pack('N', time());
}
return base64_encode($base);
}
public static function shouldMultiplexAllMail() {
return PhabricatorEnv::getEnvConfig('metamta.one-mail-per-recipient');
}
/* -( Managing Recipients )------------------------------------------------ */
/**
* Get all of the recipients for this mail, after preference filters are
* applied. This list has all objects to whom delivery will be attempted.
*
* @return list<phid> A list of all recipients to whom delivery will be
* attempted.
* @task recipients
*/
public function buildRecipientList() {
$actors = $this->loadActors(
array_merge(
$this->getToPHIDs(),
$this->getCcPHIDs()));
$actors = $this->filterDeliverableActors($actors);
return mpull($actors, 'getPHID');
}
public function loadAllActors() {
$actor_phids = array_merge(
array($this->getParam('from')),
$this->getToPHIDs(),
$this->getCcPHIDs());
$this->loadRecipientExpansions($actor_phids);
$actor_phids = $this->expandRecipients($actor_phids);
return $this->loadActors($actor_phids);
}
private function loadRecipientExpansions(array $phids) {
$expansions = id(new PhabricatorMetaMTAMemberQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($phids)
->execute();
$this->recipientExpansionMap = $expansions;
return $this;
}
/**
* Expand a list of recipient PHIDs (possibly including aggregate recipients
* like projects) into a deaggregated list of individual recipient PHIDs.
* For example, this will expand project PHIDs into a list of the project's
* members.
*
* @param list<phid> List of recipient PHIDs, possibly including aggregate
* recipients.
* @return list<phid> Deaggregated list of mailable recipients.
*/
private function expandRecipients(array $phids) {
if ($this->recipientExpansionMap === null) {
throw new Exception(
pht(
'Call loadRecipientExpansions() before expandRecipients()!'));
}
$results = array();
foreach ($phids as $phid) {
if (!isset($this->recipientExpansionMap[$phid])) {
$results[$phid] = $phid;
} else {
foreach ($this->recipientExpansionMap[$phid] as $recipient_phid) {
$results[$recipient_phid] = $recipient_phid;
}
}
}
return array_keys($results);
}
private function filterDeliverableActors(array $actors) {
assert_instances_of($actors, 'PhabricatorMetaMTAActor');
$deliverable_actors = array();
foreach ($actors as $phid => $actor) {
if ($actor->isDeliverable()) {
$deliverable_actors[$phid] = $actor;
}
}
return $deliverable_actors;
}
private function loadActors(array $actor_phids) {
$actor_phids = array_filter($actor_phids);
$viewer = PhabricatorUser::getOmnipotentUser();
$actors = id(new PhabricatorMetaMTAActorQuery())
->setViewer($viewer)
->withPHIDs($actor_phids)
->execute();
if (!$actors) {
return array();
}
// Exclude explicit recipients.
foreach ($this->getExcludeMailRecipientPHIDs() as $phid) {
$actor = idx($actors, $phid);
if (!$actor) {
continue;
}
$actor->setUndeliverable(
pht(
'This message is a response to another email message, and this '.
'recipient received the original email message, so we are not '.
'sending them this substantially similar message (for example, '.
'the sender used "Reply All" instead of "Reply" in response to '.
'mail from Phabricator).'));
}
// Exclude the actor if their preferences are set.
$from_phid = $this->getParam('from');
$from_actor = idx($actors, $from_phid);
if ($from_actor) {
$from_user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withPHIDs(array($from_phid))
->execute();
$from_user = head($from_user);
if ($from_user && !$this->getOverrideNoSelfMailPreference()) {
$pref_key = PhabricatorUserPreferences::PREFERENCE_NO_SELF_MAIL;
$exclude_self = $from_user
->loadPreferences()
->getPreference($pref_key);
if ($exclude_self) {
$from_actor->setUndeliverable(
pht(
'This recipient is the user whose actions caused delivery of '.
'this message, but they have set preferences so they do not '.
'receive mail about their own actions (Settings > Email '.
'Preferences > Self Actions).'));
}
}
}
// Exclude all recipients who have set preferences to not receive this type
// of email (for example, a user who says they don't want emails about task
// CC changes).
$tags = $this->getParam('mailtags');
if ($tags) {
$all_prefs = id(new PhabricatorUserPreferences())->loadAllWhere(
'userPHID in (%Ls)',
$actor_phids);
$all_prefs = mpull($all_prefs, null, 'getUserPHID');
foreach ($all_prefs as $phid => $prefs) {
$user_mailtags = $prefs->getPreference(
PhabricatorUserPreferences::PREFERENCE_MAILTAGS,
array());
// The user must have elected to receive mail for at least one
// of the mailtags.
$send = false;
foreach ($tags as $tag) {
if (idx($user_mailtags, $tag, true)) {
$send = true;
break;
}
}
if (!$send) {
$actors[$phid]->setUndeliverable(
pht(
'This mail has tags which control which users receive it, and '.
'this recipient has not elected to receive mail with any of '.
'the tags on this message (Settings > Email Preferences).'));
}
}
}
return $actors;
}
private function shouldRateLimitMail(array $all_recipients) {
try {
PhabricatorSystemActionEngine::willTakeAction(
$all_recipients,
new PhabricatorMetaMTAErrorMailAction(),
1);
return false;
} catch (PhabricatorSystemActionRateLimitException $ex) {
return true;
}
}
}
diff --git a/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php b/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php
index a64b302c1a..6337c4764f 100644
--- a/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php
+++ b/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php
@@ -1,179 +1,187 @@
<?php
final class PhabricatorMetaMTAMailTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testMailSendFailures() {
$user = $this->generateNewTestUser();
$phid = $user->getPHID();
// Normally, the send should succeed.
$mail = new PhabricatorMetaMTAMail();
$mail->addTos(array($phid));
$mailer = new PhabricatorMailImplementationTestAdapter();
$mail->sendNow($force = true, $mailer);
$this->assertEqual(
PhabricatorMetaMTAMail::STATUS_SENT,
$mail->getStatus());
// When the mailer fails temporarily, the mail should remain queued.
$mail = new PhabricatorMetaMTAMail();
$mail->addTos(array($phid));
$mailer = new PhabricatorMailImplementationTestAdapter();
$mailer->setFailTemporarily(true);
- $mail->sendNow($force = true, $mailer);
+ try {
+ $mail->sendNow($force = true, $mailer);
+ } catch (Exception $ex) {
+ // Ignore.
+ }
$this->assertEqual(
PhabricatorMetaMTAMail::STATUS_QUEUE,
$mail->getStatus());
// When the mailer fails permanently, the mail should be failed.
$mail = new PhabricatorMetaMTAMail();
$mail->addTos(array($phid));
$mailer = new PhabricatorMailImplementationTestAdapter();
$mailer->setFailPermanently(true);
- $mail->sendNow($force = true, $mailer);
+ try {
+ $mail->sendNow($force = true, $mailer);
+ } catch (Exception $ex) {
+ // Ignore.
+ }
$this->assertEqual(
PhabricatorMetaMTAMail::STATUS_FAIL,
$mail->getStatus());
}
public function testRecipients() {
$user = $this->generateNewTestUser();
$phid = $user->getPHID();
$prefs = $user->loadPreferences();
$mailer = new PhabricatorMailImplementationTestAdapter();
$mail = new PhabricatorMetaMTAMail();
$mail->addTos(array($phid));
$this->assertTrue(
in_array($phid, $mail->buildRecipientList()),
'"To" is a recipient.');
// Test that the "No Self Mail" preference works correctly.
$mail->setFrom($phid);
$this->assertTrue(
in_array($phid, $mail->buildRecipientList()),
'"From" does not exclude recipients by default.');
$prefs->setPreference(
PhabricatorUserPreferences::PREFERENCE_NO_SELF_MAIL,
true);
$prefs->save();
$this->assertFalse(
in_array($phid, $mail->buildRecipientList()),
'"From" excludes recipients with no-self-mail set.');
$prefs->unsetPreference(
PhabricatorUserPreferences::PREFERENCE_NO_SELF_MAIL);
$prefs->save();
$this->assertTrue(
in_array($phid, $mail->buildRecipientList()),
'"From" does not exclude recipients by default.');
// Test that explicit exclusion works correctly.
$mail->setExcludeMailRecipientPHIDs(array($phid));
$this->assertFalse(
in_array($phid, $mail->buildRecipientList()),
'Explicit exclude excludes recipients.');
$mail->setExcludeMailRecipientPHIDs(array());
// Test that mail tag preferences exclude recipients.
$prefs->setPreference(
PhabricatorUserPreferences::PREFERENCE_MAILTAGS,
array(
'test-tag' => false,
));
$prefs->save();
$mail->setMailTags(array('test-tag'));
$this->assertFalse(
in_array($phid, $mail->buildRecipientList()),
'Tag preference excludes recipients.');
$prefs->unsetPreference(PhabricatorUserPreferences::PREFERENCE_MAILTAGS);
$prefs->save();
$this->assertTrue(
in_array($phid, $mail->buildRecipientList()),
'Recipients restored after tag preference removed.');
}
public function testThreadIDHeaders() {
$this->runThreadIDHeadersWithConfiguration(true, true);
$this->runThreadIDHeadersWithConfiguration(true, false);
$this->runThreadIDHeadersWithConfiguration(false, true);
$this->runThreadIDHeadersWithConfiguration(false, false);
}
private function runThreadIDHeadersWithConfiguration(
$supports_message_id,
$is_first_mail) {
$mailer = new PhabricatorMailImplementationTestAdapter(
array(
'supportsMessageIDHeader' => $supports_message_id,
));
$thread_id = '<somethread-12345@somedomain.tld>';
$mail = new PhabricatorMetaMTAMail();
$mail->setThreadID($thread_id, $is_first_mail);
$mail->sendNow($force = true, $mailer);
$guts = $mailer->getGuts();
$dict = ipull($guts['headers'], 1, 0);
if ($is_first_mail && $supports_message_id) {
$expect_message_id = true;
$expect_in_reply_to = false;
$expect_references = false;
} else {
$expect_message_id = false;
$expect_in_reply_to = true;
$expect_references = true;
}
$case = "<message-id = ".($supports_message_id ? 'Y' : 'N').", ".
"first = ".($is_first_mail ? 'Y' : 'N').">";
$this->assertTrue(
isset($dict['Thread-Index']),
"Expect Thread-Index header for case {$case}.");
$this->assertEqual(
$expect_message_id,
isset($dict['Message-ID']),
"Expectation about existence of Message-ID header for case {$case}.");
$this->assertEqual(
$expect_in_reply_to,
isset($dict['In-Reply-To']),
"Expectation about existence of In-Reply-To header for case {$case}.");
$this->assertEqual(
$expect_references,
isset($dict['References']),
"Expectation about existence of References header for case {$case}.");
}
}
diff --git a/src/docs/user/configuration/configuring_inbound_email.diviner b/src/docs/user/configuration/configuring_inbound_email.diviner
index fc796ae758..48e25e499d 100644
--- a/src/docs/user/configuration/configuring_inbound_email.diviner
+++ b/src/docs/user/configuration/configuring_inbound_email.diviner
@@ -1,236 +1,252 @@
@title Configuring Inbound Email
@group config
This document contains instructions for configuring inbound email, so users
may update Differential and Maniphest by replying to messages and create
Maniphest tasks via email.
= Preamble =
This can be extremely difficult to configure correctly. This is doubly true if
-you use sendmail.
+you use a local MTA.
-There are basically a few approaches available:
+There are a few approaches available:
- - Use SendGrid (<http://sendgrid.com/>), which is very easy but is not free.
- - Run your own MTA, which can be quite harrowing to configure but is free.
- - Tell the Phabricator devteam about another service you'd like support for,
- this stuff is seriously terrible to configure on your own.
+| Receive Mail With | Setup | Cost | Notes |
+|--------|-------|------|-------|
+| Mailgun | Easy | Cheap | Recommended |
+| SendGrid | Easy | Cheap | |
+| Local MTA | Extremely Difficult | Free | Strongly discouraged! |
+
+The remainder of this document walks through configuring Phabricator to
+receive mail, and then configuring your chosen transport to deliver mail
+to Phabricator.
= Configuring Phabricator =
-By default, Phabricator uses a "noreply@phabricator.example.com" email address
-as the 'From' (configurable with ##metamta.default-address##) and sets
+By default, Phabricator uses a `noreply@phabricator.example.com` email address
+as the 'From' (configurable with `metamta.default-address`) and sets
'Reply-To' to the user generating the email (e.g., by making a comment), if the
mail was generated by a user action. This means that users can reply (or
reply-all) to email to discuss changes, but the conversation won't be recorded
in Phabricator and users will not be able to take actions like claiming tasks or
requesting changes to revisions.
To change this behavior so that users can interact with objects in Phabricator
over email, set these configuration keys:
- ##metamta.differential.reply-handler-domain##: enables email replies for
Differential.
- ##metamta.maniphest.reply-handler-domain##: enables email replies for
Maniphest.
Set these keys to some domain which you configure according to the instructions
-below, e.g. "##phabricator.example.com##". You can set these both to the same
+below, e.g. `phabricator.example.com`. You can set these both to the same
domain, and will generally want to. Once you set these keys, emails will use a
-'Reply-To' like "##T123+273+af310f9220ad@example.com##", which -- when
+'Reply-To' like `T123+273+af310f9220ad@example.com`, which -- when
configured correctly, according to the instructions below -- will parse incoming
email and allow users to interact with Maniphest tasks and Differential
revisions over email.
-If you don't want phabricator to take up an entire domain (or subdomain) you
+If you don't want Phabricator to take up an entire domain (or subdomain) you
can configure a general prefix so you can use a single mailbox to receive mail
-on. To make use of this set ##metamta.single-reply-handler-prefix## to the
-prefix of your choice, and phabricator will prepend this to the 'Reply-To'
+on. To make use of this set `metamta.single-reply-handler-prefix` to the
+prefix of your choice, and Phabricator will prepend this to the 'Reply-To'
mail address. This works because everything up to the first (optional) '+'
character in an email-address is considered the receiver, and everything
-after is essentially "ignored".
+after is essentially ignored.
-You can also set up a task creation email address, like ##bugs@example.com##,
+You can also set up a task creation email address, like `bugs@example.com`,
which will create a Maniphest task out of any email which is set to it. To do
-this, set ##metamta.maniphest.public-create-email## in your configuration. This
+this, set `metamta.maniphest.public-create-email` in your configuration. This
has some mild security implications, see below.
= Security =
The email reply channel is "somewhat" authenticated. Each reply-to address is
unique to the recipient and includes a hash of user information and a unique
object ID, so it can only be used to update that object and only be used to act
on behalf of the recipient.
However, if an address is leaked (which is fairly easy -- for instance,
forwarding an email will leak a live reply address, or a user might take a
screenshot), //anyone// who can send mail to your reply-to domain may interact
with the object the email relates to as the user who leaked the mail. Because
the authentication around email has this weakness, some actions (like accepting
revisions) are not permitted over email.
This implementation is an attempt to balance utility and security, but makes
some sacrifices on both sides to achieve it because of the difficulty of
authenticating senders in the general case (e.g., where you are an open source
project and need to interact with users whose email accounts you have no control
over).
If you leak a bunch of reply-to addresses by accident, you can change
-##phabricator.mail-key## in your configuration to invalidate all the old hashes.
+`phabricator.mail-key` in your configuration to invalidate all the old hashes.
-You can also set ##metamta.public-replies##, which will change how Phabricator
+You can also set `metamta.public-replies`, which will change how Phabricator
delivers email. Instead of sending each recipient a unique mail with a personal
reply-to address, it will send a single email to everyone with a public reply-to
address. This decreases security because anyone who can spoof a "From" address
can act as another user, but increases convenience if you use mailing lists and,
practically, is a reasonable setting for many installs. The reply-to address
will still contain a hash unique to the object it represents, so users who have
not received an email about an object can not blindly interact with it.
-If you enable ##metamta.maniphest.public-create-email##, that address also uses
+If you enable `metamta.maniphest.public-create-email`, that address also uses
the weaker "From" authentication mechanism.
NOTE: Phabricator does not currently attempt to verify "From" addresses because
this is technically complex, seems unreasonably difficult in the general case,
and no installs have had a need for it yet. If you have a specific case where a
reasonable mechanism exists to provide sender verification (e.g., DKIM
signatures are sufficient to authenticate the sender under your configuration,
or you are willing to require all users to sign their email), file a feature
request.
= Testing and Debugging Inbound Email =
You can use the `bin/mail` utility to test and review inbound mail. This can
help you determine if mail is being delivered to Phabricator or not:
phabricator/ $ ./bin/mail list-inbound # List inbound messages.
phabricator/ $ ./bin-mail show-inbound # Show details about a message.
You can also test receiving mail, but note that this just simulates receiving
the mail and doesn't send any information over the network. It is
primarily aimed at developing email handlers: it will still work properly
if your inbound email configuration is incorrect or even disabled.
phabricator/ $ ./bin/mail receive-test # Receive test message.
Run `bin/mail help <command>` for detailed help on using these commands.
-= SendGrid =
+= Mailgun Setup =
+
+To use Mailgun, you need a Mailgun account. You can sign up at
+<http://www.mailgun.com>. Provided you have such an account, configure it
+like this:
+
+ - Configure a mail domain according to Mailgun's instructions.
+ - Add a Mailgun route with a `catch_all()` rule which takes the action
+ `forward("https://phabricator.example.com/mail/mailgun/")`. Replace the
+ example domain with your actual domain.
+
+= SendGrid Setup =
To use SendGrid, you need a SendGrid account with access to the "Parse API" for
inbound email. Provided you have such an account, configure it like this:
- Configure an MX record according to SendGrid's instructions, i.e. add
##phabricator.example.com MX 10 mx.sendgrid.net.## or similar.
- Go to the "Parse Incoming Emails" page on SendGrid
(<http://sendgrid.com/developer/reply>) and add the domain as the
"Hostname".
- Add the URL ##https://phabricator.example.com/mail/sendgrid/## as the "Url",
using your domain (and HTTP instead of HTTPS if you are not configured with
SSL).
- If you get an error that the hostname "can't be located or verified", it
means your MX record is either incorrectly configured or hasn't propagated
yet.
- Set ##metamta.maniphest.reply-handler-domain## and/or
##metamta.differential.reply-handler-domain## to
"##phabricator.example.com##" (whatever you configured the MX record for),
depending on whether you want to support email replies for Maniphest,
Differential, or both.
That's it! If everything is working properly you should be able to send email
to ##anything@phabricator.example.com## and it should appear in
`bin/mail list-inbound` within a few seconds.
-= Installing Mailparse =
+= Local MTA: Installing Mailparse =
If you're going to run your own MTA, you need to install the PECL mailparse
extension. In theory, you can do that with:
$ sudo pecl install mailparse
You may run into an error like "needs mbstring". If so, try:
$ sudo yum install php-mbstring # or equivalent
$ sudo pecl install -n mailparse
If you get a linker error like this:
COUNTEREXAMPLE
PHP Warning: PHP Startup: Unable to load dynamic library
'/usr/lib64/php/modules/mailparse.so' - /usr/lib64/php/modules/mailparse.so:
undefined symbol: mbfl_name2no_encoding in Unknown on line 0
...you need to edit your php.ini file so that mbstring.so is loaded **before**
mailparse.so. This is not the default if you have individual files in
##php.d/##.
-= MTA: Configuring Sendmail =
+= Local MTA: Configuring Sendmail =
Before you can configure Sendmail, you need to install Mailparse. See the
section "Installing Mailparse" above.
Sendmail is very difficult to configure. First, you need to configure it for
your domain so that mail can be delivered correctly. In broad strokes, this
probably means something like this:
- add an MX record;
- make sendmail listen on external interfaces;
- open up port 25 if necessary (e.g., in your EC2 security policy);
- add your host to /etc/mail/local-host-names; and
- restart sendmail.
Now, you can actually configure sendmail to deliver to Phabricator. In
##/etc/aliases##, add an entry like this:
phabricator: "| /path/to/phabricator/scripts/mail/mail_handler.php <ENV>"
...where <ENV> 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##.
-= MTA: Configuring Lamson =
+= Local MTA: Configuring Lamson =
Before you can configure Lamson, you need to install Mailparse. See the section
"Installing Mailparse" above.
In contrast to Sendmail, Lamson is relatively easy to configure. It is fairly
minimal, and is suitable for a development or testing environment. Lamson
listens for incoming SMTP mails and passes the content directly to Phabricator.
To get started, follow the provided instructions
(<http://lamsonproject.org/docs/getting_started.html>) to set up an instance.
One likely deployment issue is that binding to port 25 requires root
privileges. Lamson is capable of starting as root then dropping privileges, but
you must supply ##-uid## and ##-gid## arguments to do so, as demonstrated by
Step 8 in Lamson's deployment tutorial (located here:
<http://lamsonproject.org/docs/deploying_oneshotblog.html>).
The Lamson handler code itself is very concise; it merely needs to pass the
content of the email to Phabricator:
import logging, subprocess
from lamson.routing import route, stateless
from lamson import view
PHABRICATOR_ROOT = "/path/to/phabricator"
PHABRICATOR_ENV = "custom/myconf"
LOGGING_ENABLED = True
@route("(address)@(host)", address=".+")
@stateless
def START(message, address=None, host=None):
if LOGGING_ENABLED:
logging.debug("%s", message.original)
process = subprocess.Popen([PHABRICATOR_ROOT + "scripts/mail/mail_handler.php",PHABRICATOR_ENV],stdin=subprocess.PIPE)
process.communicate(message.original)
diff --git a/src/docs/user/configuration/configuring_outbound_email.diviner b/src/docs/user/configuration/configuring_outbound_email.diviner
index 7489ef645e..fdc5abf153 100644
--- a/src/docs/user/configuration/configuring_outbound_email.diviner
+++ b/src/docs/user/configuration/configuring_outbound_email.diviner
@@ -1,176 +1,189 @@
@title Configuring Outbound Email
@group config
Instructions for configuring Phabricator to send mail.
= Overview =
-Phabricator can send outbound email via several different adapters:
-
- - by running ##sendmail## on the local host with SMTP; or
- - by running postfix on the local host with SMTP; or
- - by using Amazon SES (Simple Email Service); or
- - by using SendGrid's REST API; or
- - via a custom adapter you write; or
- - by dropping email into a hole and not delivering it.
-
-Of these, ##sendmail## is the default but requires some configuration. SES and
-SendGrid are easier, but cost money and have some limitations. Writing a custom
-solution requires digging into the code. See below for details on how to set up
-each method.
-
-Phabricator can also send outbound email in two ways:
-
- - immediately, when messages are generated (default); or
- - in the background, via a daemon.
-
-Sending mail in the background requires that you be running the Phabricator
-daemons, but can greatly improve the performance of the application if your mail
-handler is slow. For more information on using daemons, see
+Phabricator can send outbound email via several different providers, called
+"Adapters".
+
+| Send Mail With | Setup | Cost | Inbound | Notes |
+|---------|-------|------|---------|-------|
+| Mailgun | Easy | Cheap | Yes | Recommended |
+| Amazon SES | Easy | Cheap | No | Recommended |
+| SendGrid | Easy | Cheap | Yes | |
+| External SMTP | Medium | Varies | No | Gmail, etc. |
+| Local SMTP | Hard | Free | No | (Default) sendmail, postfix, etc |
+| Custom | Hard | Free | No | Write an adapter for some other service. |
+| Drop in a Hole | Easy | Free | No | Drops mail in a deep, dark hole. |
+
+Of these options, sending mail via local SMTP is the default, but usually
+requires some configuration to get working. See below for details on how to
+select and configure a delivery method.
+
+Overall, Mailgun and SES are much easier to set up, and using one of them is
+recommended. In particular, Mailgun will also let you set up inbound email
+easily.
+
+If you have some internal mail service you'd like to use you can also
+write a custom adapter, but this requires digging into the code.
+
+Phabricator sends mail in the background, so the daemons need to be running for
+it to be able to deliver mail. You should receive setup warnings if they are
+not. For more information on using daemons, see
@{article:Managing Daemons with phd}.
= Basics =
Regardless of how outbound email is delivered, you should configure these keys
-in your configuration file:
+in your configuration:
- **metamta.default-address** determines where mail is sent "From" by
- default. If your domain is ##example.org##, set this to something like
- "##noreply@example.org##".
- - **metamta.domain** should be set to your domain, e.g. "##example.org##".
+ default. If your domain is `example.org`, set this to something like
+ `noreply@example.org`.
+ - **metamta.domain** should be set to your domain, e.g. `example.org`.
- **metamta.can-send-as-user** should be left as ##false## in most cases,
- but see the documentation in ##default.conf.php## for details.
+ but see the documentation for details.
= Configuring Mail Adapters =
-To choose how mail will be sent, change the **metamta.mail-adapter** key in
-your configuration. Possible values are:
+To choose how mail will be sent, change the `metamta.mail-adapter` key in
+your configuration. Possible values are listed in the UI:
+ - ##PhabricatorMailImplementationAmazonMailgunAdapter##: use Mailgun, see
+ "Adapter: Mailgun".
+ - ##PhabricatorMailImplementationAmazonSESAdapter##: use Amazon SES, see
+ "Adapter: Amazon SES".
- ##PhabricatorMailImplementationPHPMailerLiteAdapter##: default, uses
"sendmail", see "Adapter: Sendmail".
- ##PhabricatorMailImplementationPHPMailerAdapter##: uses SMTP, see
"Adapter: SMTP".
- - ##PhabricatorMailImplementationAmazonSESAdapter##: use Amazon SES, see
- "Adapter: Amazon SES".
- ##PhabricatorMailImplementationSendGridAdapter##: use SendGrid, see
"Adapter: SendGrid".
- ##Some Custom Class You Write##: use a custom adapter you write, see
"Adapter: Custom".
- ##PhabricatorMailImplementationTestAdapter##: this will
**completely disable** outbound mail. You can use this if you don't want to
send outbound mail, or want to skip this step for now and configure it
later.
= Adapter: Sendmail =
This is the default, and selected by choosing
-##PhabricatorMailImplementationPHPMailerLiteAdapter## as the value for
-**metamta.mail-adapter**. This requires a 'sendmail' binary to be installed on
+`PhabricatorMailImplementationPHPMailerLiteAdapter` as the value for
+**metamta.mail-adapter**. This requires a `sendmail` binary to be installed on
the system. Most MTAs (e.g., sendmail, qmail, postfix) should do this, but your
machine may not have one installed by default. For install instructions, consult
the documentation for your favorite MTA.
Since you'll be sending the mail yourself, you are subject to things like SPF
rules, blackholes, and MTA configuration which are beyond the scope of this
document. If you can already send outbound email from the command line or know
how to configure it, this option is straightforward. If you have no idea how to
-do any of this, consider using Amazon SES.
+do any of this, strongly consider using Mailgun or Amazon SES instead.
= Adapter: SMTP =
-For most situations of using SMTP to send email, you could actually use
-'sendmail' or 'postfix' to do it. But some SMTP server requires authentication
-and the 'sendmail' mailer doesn't work. If you want to try with postfix, for
-install instructions, consult the documentation for postfix as MTA and you
-could configure to use SMTP then.
-
-To configure Phabricator to use SMTP, set these configuration keys:
+You can use this adapter to send mail via an external SMTP server, like Gmail.
+To do this, set these configuration keys:
- **metamta.mail-adapter**: set to
- "PhabricatorMailImplementationPHPMailerAdapter".
- - **phpmailer.mailer**: set to "smtp".
- - **phpmailer.smtp-host**: set to hostname of your smtp server.
- - **phpmailer.smtp-port**: set to port of your smtp server.
+ `PhabricatorMailImplementationPHPMailerAdapter`.
+ - **phpmailer.mailer**: set to `smtp`.
+ - **phpmailer.smtp-host**: set to hostname of your SMTP server.
+ - **phpmailer.smtp-port**: set to port of your SMTP server.
- **phpmailer.smtp-user**: set to your username used for authentication.
- **phpmailer.smtp-password**: set to your password used for authentication.
+ - **phpmailer.smtp-protocol**: set to `tls` or `ssl` if necessary. Use
+ `ssl` for Gmail.
+
+= Adapter: Mailgun =
+
+Mailgun is an email delivery service. You can learn more at
+<http://www.mailgun.com>. Mailgun isn't free, but is very easy to configure
+and works well.
+
+To use Mailgun, sign up for an account, then set these configuration keys:
+
+ - **metamta.mail-adapter**: set to
+ `PhabricatorMailImplementationMailgunAdapter`.
+ - **mailgun.api-key**: set to your Mailgun API key.
+ - **mailgun.domain**: set to your Mailgun domain.
= Adapter: Amazon SES =
Amazon SES is Amazon's cloud email service. It is not free, but is easier to
configure than sendmail and can simplify outbound email configuration. To use
Amazon SES, you need to sign up for an account with Amazon at
<http://aws.amazon.com/ses/>.
To configure Phabricator to use Amazon SES, set these configuration keys:
- **metamta.mail-adapter**: set to
"PhabricatorMailImplementationAmazonSESAdapter".
- **amazon-ses.access-key**: set to your Amazon SES access key.
- **amazon-ses.secret-key**: set to your Amazon SES secret key.
NOTE: Amazon SES **requires you to verify your "From" address**. Configure which
"From" address to use by setting "##metamta.default-address##" in your config,
then follow the Amazon SES verification process to verify it. You won't be able
to send email until you do this!
-NOTE: Amazon SES is slow to accept mail (often 1-2 seconds) and application
-performance will improve greatly if you configure outbound email to send in
-the background.
-
= Adapter: SendGrid =
SendGrid is an email delivery service like Amazon SES. You can learn more at
<http://sendgrid.com/>. It is easy to configure, but not free.
You can configure SendGrid in two ways: you can send via SMTP or via the REST
API. To use SMTP, just configure ##sendmail## and leave Phabricator's setup
with defaults. To use the REST API, follow the instructions in this section.
To configure Phabricator to use SendGrid, set these configuration keys:
- **metamta.mail-adapter**: set to
"PhabricatorMailImplementationSendGridAdapter".
- **sendgrid.api-user**: set to your SendGrid login name.
- **sendgrid.api-key**: set to your SendGrid password.
If you're logged into your SendGrid account, you may be able to find this
information easily by visiting <http://sendgrid.com/developer>.
= Adapter: Custom =
You can provide a custom adapter by writing a concrete subclass of
@{class:PhabricatorMailImplementationAdapter} and setting it as the
-**metamta.mail-adapter**.
+`metamta.mail-adapter`.
-TODO: This needs to be better documented once extending Phabricator is better
+TODO: This should be better documented once extending Phabricator is better
documented.
= Adapter: Disable Outbound Mail =
You can use the @{class:PhabricatorMailImplementationTestAdapter} to completely
disable outbound mail, if you don't want to send mail or don't want to configure
it yet. Just set **metamta.mail-adapter** to
-"PhabricatorMailImplementationTestAdapter".
+`PhabricatorMailImplementationTestAdapter`.
= Testing and Debugging Outbound Email =
You can use the `bin/mail` utility to test, debug, and examine outbound mail. In
particular:
phabricator/ $ ./bin/mail list-outbound # List outbound mail.
phabricator/ $ ./bin/mail show-outbound # Show details about messages.
phabricator/ $ ./bin/mail send-test # Send test messages.
Run `bin/mail help <command>` for more help on using these commands.
You can monitor daemons using the Daemon Console (##/daemon/##, or click
**Daemon Console** from the homepage).
= Next Steps =
Continue by:
- @{article:Configuring Inbound Email} so users can reply to email they
receive about revisions and tasks to interact with them; or
- learning about daemons with @{article:Managing Daemons with phd}; or
- returning to the @{article:Configuration Guide}.

File Metadata

Mime Type
text/x-diff
Expires
Sun, Nov 16, 10:54 AM (27 m, 38 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
338917
Default Alt Text
(152 KB)

Event Timeline