protected function __construct()
 {
     $config = Context::get()->getConfiguration();
     if (!$this->getDatabase()) {
         $this->setDatabase($config->object($this->getConfigKey()));
     }
 }
 public function setAccount($account)
 {
     require_once 'WSDL/Payment.php';
     $this->account = $account;
     $c = Context::get()->getConfiguration();
     $this->soapClient = new WSDL\Payment($c->val('payment-provider/adyen/payments-wsdl'), array('cache_wsdl' => WSDL_CACHE_BOTH, 'login' => $c->val("payment-provider/adyen/accounts/{$this->account}/ws-username"), 'password' => $c->val("payment-provider/adyen/accounts/{$this->account}/ws-password")));
 }
 public function setUp()
 {
     parent::setUp();
     $this->config = AdyenTestConfiguration::createWithSuccessfulApi();
     Context::initWithLogger($this->config);
     $this->jobQueue = $this->config->object('data-store/jobs-adyen');
     $this->jobQueue->createTable('jobs-adyen');
 }
 public function setUp()
 {
     parent::setUp();
     $config = SmashPigDatabaseTestConfiguration::instance();
     Context::initWithLogger($config);
     $this->db = DamagedDatabase::get();
     $this->db->createTable();
 }
 public function execute()
 {
     $context = Context::get();
     $config = $context->getConfiguration();
     $values = $config->val('/');
     $yaml = Yaml::dump($values);
     print $yaml;
 }
 public function setUp()
 {
     parent::setUp();
     $this->config = AdyenTestConfiguration::createWithSuccessfulApi();
     Context::initWithLogger($this->config);
     $this->jobQueue = BaseQueueConsumer::getQueue('jobs-adyen');
     $this->jobQueue->createTable('jobs-adyen');
 }
 public static function addToMessage($message)
 {
     $message->source_name = 'SmashPig';
     $message->source_type = 'listener';
     $message->source_host = gethostname();
     $message->source_run_id = getmypid();
     $message->source_version = Context::get()->getSourceRevision();
     $message->source_enqueued_time = time();
 }
 public function setUp()
 {
     parent::setUp();
     Context::initWithLogger(QueueTestConfiguration::instance());
     $this->queue = BaseQueueConsumer::getQueue('test');
     $this->queue->createTable('test');
     $damagedDb = DamagedDatabase::get();
     $damagedDb->createTable();
     $this->damaged = $damagedDb->getDatabase();
 }
 public function setUp()
 {
     parent::setUp();
     $this->config = AdyenTestConfiguration::createWithSuccessfulApi();
     Context::initWithLogger($this->config);
     $this->pendingDatabase = PendingDatabase::get();
     $this->pendingMessage = json_decode(file_get_contents(__DIR__ . '/../Data/pending.json'), true);
     $this->pendingDatabase->storeMessage($this->pendingMessage);
     $this->antifraudQueue = BaseQueueConsumer::getQueue('payments-antifraud');
 }
 public function setUp()
 {
     parent::setUp();
     $this->config = AdyenTestConfiguration::createWithSuccessfulApi();
     Context::initWithLogger($this->config);
     $this->pendingDatabase = PendingDatabase::get();
     $this->pendingMessage = json_decode(file_get_contents(__DIR__ . '/../Data/pending.json'), true);
     $this->pendingMessage['captured'] = true;
     $this->pendingDatabase->storeMessage($this->pendingMessage);
 }
 public function setUp()
 {
     parent::setUp();
     $this->setMwGlobals(array('wgDonationInterfaceOrphanCron' => array('enable' => true, 'target_execute_time' => self::TARGET_EXECUTE_TIME, 'time_buffer' => self::TIME_BUFFER), 'wgGlobalCollectGatewayEnabled' => true, 'wgDonationInterfaceGatewayAdapters' => array('globalcollect' => 'TestingGlobalCollectOrphanAdapter', 'globalcollect_orphan' => 'TestingGlobalCollectOrphanAdapter')));
     $config = SmashPigDatabaseTestConfiguration::instance();
     Context::init($config);
     $this->pendingDb = PendingDatabase::get();
     // Create the schema.
     $this->pendingDb->createTable();
 }
 public function setUp()
 {
     parent::setUp();
     $config = SmashPigDatabaseTestConfiguration::instance();
     Context::initWithLogger($config);
     $this->pendingDb = PendingDatabase::get();
     $this->pendingDb->createTable();
     $this->paymentsInitialDb = PaymentsInitialDatabase::get();
     $this->paymentsInitialDb->createTable();
 }
 public function setUp()
 {
     parent::setUp();
     // Merge db and queue test configs.
     $config = TestingConfiguration::loadConfigWithFileOverrides(array(__DIR__ . '/data/config_smashpig_db.yaml', __DIR__ . '/data/config_queue.yaml'));
     Context::initWithLogger($config);
     $this->pendingDb = PendingDatabase::get();
     $this->pendingDb->createTable();
     $this->paymentsInitialDb = PaymentsInitialDatabase::get();
     $this->paymentsInitialDb->createTable();
 }
 public function setUp()
 {
     parent::setUp();
     $this->config = PayPalTestConfiguration::get();
     // php-queue\PDO complains about pop() from non-existent table
     $this->config->object('data-store/jobs-paypal')->createTable('jobs-paypal');
     Context::initWithLogger($this->config);
     foreach (self::$message_data as $file => $type) {
         self::$messages[] = array('type' => $type, 'payload' => json_decode(file_get_contents(__DIR__ . '/../Data/' . $file), true));
     }
 }
 public function setUp()
 {
     parent::setUp();
     chdir(__DIR__ . '/..');
     // So the mock client can find its response files
     $config = AmazonTestConfiguration::instance();
     Context::initWithLogger($config);
     $this->mockClient = $config->object('payments-client', true);
     $this->mockClient->calls = array();
     $this->mockClient->returns = array();
     $this->mockClient->exceptions = array();
 }
 public function parseFile($path)
 {
     $config = Context::get()->getConfiguration();
     $fileTypes = $config->val('audit/file-types');
     $data = array();
     foreach ($fileTypes as $type) {
         if ($type::isMine($path)) {
             $parser = new $type();
             $data = $parser->parse($path);
         }
     }
     return $data;
 }
 public function execute(ListenerMessage $msg)
 {
     $destinationQueue = $msg->getDestinationQueue();
     if ($destinationQueue) {
         $queue = Context::get()->getConfiguration()->object("data-store/{$destinationQueue}");
         $queueMsg = $msg->normalizeForQueue();
         SourceFields::addToMessage($queueMsg);
         $queue->push($queueMsg);
     } else {
         $class = get_class($msg);
         Logger::warning("Ignoring message of type {$class}", $msg);
     }
     return true;
 }
 public function download()
 {
     $this->ensureAndScanFolder($this->archivePath);
     $this->ensureAndScanFolder($this->downloadPath);
     $this->reportsClient = Context::get()->getConfiguration()->object('reports-client', true);
     Logger::info('Getting report list');
     $startDate = new DateTime("-{$this->days} days", new DateTimeZone('UTC'));
     $list = $this->reportsClient->getReportList(array('available_from_date' => $startDate->format(DateTime::ATOM), 'max_count' => 100, 'report_type_list' => array(ReportsClient::OFFAMAZONPAYMENTS_SETTLEMENT, ReportsClient::OFFAMAZONPAYMENTS_REFUND)))->toArray();
     foreach ($list['GetReportListResult']['ReportInfo'] as $reportInfo) {
         // If you're planning to download more than 15 reports at a time, be
         // aware that the client will handle throttling by default, retrying
         // up to four times with successively longer wait times.
         $this->downloadReport($reportInfo);
     }
 }
 public function execute(ListenerMessage $msg)
 {
     // only close after successful capture
     if (get_class($msg) !== self::MESSAGE_CLASS) {
         return true;
     }
     $config = Context::get()->getConfiguration();
     $client = $config->object('payments-client', true);
     $captureId = $msg->getGatewayTransactionId();
     $orderReferenceId = substr($captureId, 0, 19);
     Logger::info("Closing order reference {$orderReferenceId}");
     $response = $client->closeOrderReference(array('amazon_order_reference_id' => $orderReferenceId))->toArray();
     if (!empty($response['Error'])) {
         Logger::info("Error losing order reference {$orderReferenceId}: " . $response['Error']['Code'] . ': ' . $response['Error']['Message']);
         return false;
     }
     return true;
 }
 /**
  * Do the actual work of the script.
  */
 public function execute()
 {
     $this->datastore = Context::get()->getConfiguration()->object('data-store/' . $this->getArgument(0, 'test'), false);
     // Generate a whole bunch of random data
     while (count($this->testObjects) < 10) {
         $this->testObjects[] = TestObject::factory();
     }
     // And repeat the objects and inject so we have something else to find
     foreach ($this->testObjects as $obj) {
         $this->datastore->addObject($obj);
         $this->datastore->addObject(TestObject::factory($obj->correlationId));
     }
     // Mix up the order of the objects to simulate real life
     shuffle($this->testObjects);
     // Now attempt to find them and their pairs!
     $this->datastore = Context::get()->getConfiguration()->object('data-store/' . $this->getArgument(0, 'test'), false);
     foreach ($this->testObjects as $obj) {
         $obj1 = $this->datastore->queueGetObject(null, $obj->correlationId);
         if ($obj1 !== null) {
             $this->datastore->queueAckObject();
         } else {
             $this->error("Could not find original object with id {$obj->correlationId}");
             continue;
         }
         $obj2 = $this->datastore->queueGetObject(null, $obj->correlationId);
         if ($obj2 !== null) {
             $this->datastore->queueAckObject();
         } else {
             $this->error("Could not find secondary object with id {$obj->correlationId}");
             continue;
         }
         $obj3 = $this->datastore->queueGetObject(null, $obj->correlationId);
         if ($obj3 !== null) {
             $this->datastore->queueAckObject();
             $this->error("Found tertiary object with id {$obj3->correlationId} " . "while looking for id {$obj->correlationId}");
             continue;
         }
         Logger::info("Successfully found id {$obj->correlationId}");
     }
     Logger::info("Done");
 }
 /**
  * Adds an object to the persistent data store.
  *
  * @param KeyedOpaqueStorableObject $obj
  *
  * @throws DataStoreException if the message could not be stored.
  * @return null
  */
 public function addObject(KeyedOpaqueStorableObject $obj)
 {
     $keys = $obj->getObjectKeys();
     $objFileName = $this->constructFileName(Context::get()->getContextId(), $keys, $this->insertCount++);
     $objFsPath = $this->basePath . '/objects/' . $objFileName;
     /* --- Create the root object file --- */
     if (file_exists($objFsPath) || ($fptr = fopen($objFsPath, 'xb')) === false) {
         throw new DataStoreException("Could not add object to store! Fully qualified key '{$objFileName}' already exists!");
     }
     fwrite($fptr, get_class($obj));
     fwrite($fptr, "\n");
     fwrite($fptr, $obj->toJson());
     fclose($fptr);
     /* === Create the helper linking files === */
     /* --- Class file first --- */
     $this->addKeyedLinkingFile('class', get_class($obj), $objFileName, $objFsPath);
     /* --- Everything else --- */
     foreach ($keys as $key => $value) {
         $this->addKeyedLinkingFile($key, $value, $objFileName, $objFsPath);
     }
 }
 /**
  * Will run all the actions that are loaded (from the 'actions' configuration
  * node) and that are applicable to this message type. Will return true
  * if all actions returned true. Otherwise will return false. This implicitly
  * means that the message will be re-queued if any action fails. Therefore
  * all actions need to be idempotent.
  *
  * @returns bool True if all actions were successful. False otherwise.
  */
 public function runActionChain()
 {
     $retval = true;
     // TODO: Cache this?
     $actions = Context::get()->getConfiguration()->val('actions');
     foreach ($actions as $actionClassName) {
         $action = new $actionClassName();
         if ($action instanceof IListenerMessageAction) {
             Logger::debug("Running action {$actionClassName}.");
             if (!$action->execute($this)) {
                 Logger::info("Action {$actionClassName} did not execute properly, will re-queue.");
                 $retval = false;
                 break;
             } else {
                 Logger::debug("Action returned success.");
             }
         } else {
             Logger::error("Entry under actions node '{$actionClassName}' does not implement IListenerActionMessage");
         }
     }
     return $retval;
 }
 public static function findRefundParentId($refundId)
 {
     $config = Context::get()->getConfiguration();
     $client = $config->object('payments-client', true);
     // The order reference ID is the first 19 characters of the refund ID
     $orderReferenceId = substr($refundId, 0, 19);
     $getDetailsResult = $client->getOrderReferenceDetails(array('amazon_order_reference_id' => $orderReferenceId))->toArray();
     if (!empty($getDetailsResult['Error'])) {
         throw new SmashPigException($getDetailsResult['Error']['Message']);
     }
     // The order reference details should contain an IdList with all of the
     // authorizations that have been made against the order reference.  We
     // should only ever have one authorization per order reference.
     $details = $getDetailsResult['GetOrderReferenceDetailsResult']['OrderReferenceDetails'];
     if (!isset($details['IdList']) || !isset($details['IdList']['member'])) {
         throw new SmashPigException("No authorizations found for order reference {$orderReferenceId}!");
     }
     $authorizationIds = (array) $details['IdList']['member'];
     // Check the status of each authorization against the order reference
     foreach ($authorizationIds as $id) {
         $authResult = $client->getAuthorizationDetails(array('amazon_authorization_id' => $id))->toArray();
         if (!empty($authResult['Error'])) {
             throw new SmashPigException($authResult['Error']['Message']);
         }
         $details = $authResult['GetAuthorizationDetailsResult']['AuthorizationDetails'];
         $state = $details['AuthorizationStatus']['State'];
         // Once we successfully capture payment against an authorization, it
         // transitions to the 'Closed' state. Failed attempts are 'Declined'
         if ($state === 'Closed') {
             // And guess what?  The authorization ID is exactly the same as the
             // capture ID (which we store as the gateway txn id), with one letter
             // changed.
             $captureId = substr($id, 0, 20) . 'C' . substr($id, 21);
             return $captureId;
         }
     }
     throw new SmashPigException("No successful authorizations found for order reference {$orderReferenceId}!");
 }
 public function execute()
 {
     $this->logger = new TaggedLogger(__CLASS__);
     $c = Context::get()->getConfiguration();
     // Construct the temporary file path
     $fileName = basename($this->reportUrl);
     $this->downloadLoc = $c->val("payment-provider/adyen/accounts/{$this->account}/report-location") . '/' . $fileName;
     $user = $c->val("payment-provider/adyen/accounts/{$this->account}/report-username");
     $pass = $c->val("payment-provider/adyen/accounts/{$this->account}/report-password");
     $this->logger->info("Beginning report download from {$this->reportUrl} using username {$user} into {$this->downloadLoc}");
     $fp = fopen($this->downloadLoc, 'w');
     if (!$fp) {
         $str = "Could not open {$this->downloadLoc} for writing! Will not download report.";
         $this->logger->error($str);
         throw new SmashPigException($str);
     }
     $ch = curl_init();
     curl_setopt($ch, CURLOPT_URL, $this->reportUrl);
     curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
     curl_setopt($ch, CURLOPT_FILE, $fp);
     curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
     curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
     curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
     curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
     curl_setopt($ch, CURLOPT_USERPWD, "{$user}:{$pass}");
     $result = curl_exec($ch);
     $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
     $error = curl_error($ch);
     curl_close($ch);
     if ($result === false) {
         $this->logger->error("Could not download report due to cURL error {$error}");
         throw new SmashPigException("Could not download report.");
     } elseif ($httpCode !== 200) {
         $this->logger->error("Report downloaded(?), but with incorrect HTTP code: {$httpCode}");
         throw new SmashPigException("Could not download report.");
     }
     return true;
 }
 /**
  * Sets the current context, returning the displaced context
  * @param Context $c
  * @return Context
  */
 public static function set(Context $c = null)
 {
     $old = Context::$instance;
     Context::$instance = $c;
     return $old;
 }
 /**
  * @return Response
  */
 public static function process()
 {
     // Can go away once we require PHP 5.6
     ini_set('default_charset', 'UTF-8');
     // --- Get the request and response objects
     $request = Request::createFromGlobals();
     $response = new Response();
     $response->setPrivate();
     // --- Break the request into parts ---
     $uri = $request->query->get('p', '');
     $parts = explode('/', $uri);
     $request->query->remove('p');
     if (count($parts) < 2) {
         $response->setStatusCode(403, 'Cannot process this request: bad URI format. A configuration node and an action is required');
         return $response;
     }
     $view = array_shift($parts);
     $action = array_shift($parts);
     // --- Initialize core services ---
     $config = Configuration::createForView($view);
     Context::init($config);
     Logger::init($config->val('logging/root-context'), $config->val('logging/log-level'), $config, Context::get()->getContextId());
     if ($config->nodeExists('disabled') && $config->val('disabled')) {
         Logger::debug('403 will be given for disabled view.', $uri);
         $response->setStatusCode(403, "View '{$view}' disabled. Cannot continue.");
         return $response;
     }
     if ($config->nodeExists('charset')) {
         // recreate the request with a different input encoding
         // FIXME: This is only converting the POST values.  Also,
         // is there really no better way to do this?
         $decoded = rawurldecode($request->getContent());
         $content = mb_convert_encoding($decoded, 'UTF-8', $config->val('charset'));
         parse_str($content, $data);
         $request->request = new ParameterBag($data);
     }
     set_error_handler('\\SmashPig\\Core\\Http\\RequestHandler::lastChanceErrorHandler');
     set_exception_handler('\\SmashPig\\Core\\Http\\RequestHandler::lastChanceExceptionHandler');
     register_shutdown_function('\\SmashPig\\Core\\Http\\RequestHandler::shutdownHandler');
     // Check to make sure there's even a point to continuing
     Logger::info("Starting processing for request, configuration view: '{$view}', action: '{$action}'");
     if (!$config->nodeExists("endpoints/{$action}")) {
         Logger::debug('403 will be given for unknown action on inbound URL.', $uri);
         $response->setStatusCode(403, "Action '{$action}' not configured. Cannot continue.");
         return $response;
     }
     // Inform the request object of our security environment
     $trustedHeader = $config->val('security/ip-header-name');
     if ($trustedHeader) {
         $request->setTrustedHeaderName(Request::HEADER_CLIENT_IP, $trustedHeader);
     }
     $trustedProxies = $config->val('security/ip-trusted-proxies');
     if ($trustedProxies) {
         $request->setTrustedProxies($trustedProxies);
     }
     // --- Actually get the endpoint object and start the request ---
     $endpointObj = $config->object("endpoints/{$action}");
     if ($endpointObj instanceof IHttpActionHandler) {
         $endpointObj->execute($request, $response);
     } else {
         $str = "Requested action '{$action}' does not implement a known handler. Cannot continue.";
         Logger::debug($str);
         $response->setStatusCode(500, $str);
     }
     $code = $response->getStatusCode();
     if ($code !== 200 && $code !== 302) {
         $response->setContent('');
     }
     return $response;
 }
if (!defined('RUN_MAINTENANCE_IF_MAIN')) {
    print "This file must be included after MaintenanceBase.php\n";
    exit(1);
}
// Wasn't included from the file scope, halt execution (probably wanted the class)
if (!MaintenanceBase::shouldExecute()) {
    return;
}
if (!$maintClass || !class_exists($maintClass)) {
    print "Cannot find maintenance class '{$maintClass}'; have you remembered to set it?\n";
    exit(1);
}
// Get an object to start us off
$maintenance = new $maintClass();
if ($maintenance instanceof MaintenanceBase) {
    // Perform setup
    $maintenance->setup();
    // Now that we have a config node, check for disablement
    $config = Context::get()->getConfiguration();
    if ($config->nodeExists('disabled') && $config->val('disabled')) {
        print 'Processor disabled, will not execute.';
        exit(1);
    }
    $retval = $maintenance->execute();
    if ($retval) {
        exit((int) $retval);
    }
} else {
    print "{$maintClass} is not a derivative of MaintenanceBase. Cannot execute.\n";
    exit(1);
}
 /**
  * Set a test configuration and initialize the context
  *
  * @param string $configNode node to use for configuration overrides
  * @param string $configPath path to configuration override file
  * @return Configuration
  */
 function setConfig($configNode = 'default', $configPath = null)
 {
     $config = Configuration::createForViewWithOverrideFile($configNode, $configPath);
     Context::initWithLogger($config);
     return $config;
 }
 /**
  * Get the value of a given option. Will return the value at configuration node
  * $defaultNode if the node exists and the option was not explicitly set, else
  * will return the default set when the option was created.
  *
  * @param string $name			Name of the option to retrieve
  * @param string $defaultNode	Config node holding override for the option
  *
  * @return mixed Value of the option or null if no default was provided
  */
 protected function getOptionOrConfig($name, $defaultNode)
 {
     $config = Context::get()->getConfiguration();
     if ($config->nodeExists($defaultNode)) {
         $default = $config->val($defaultNode);
     } else {
         $default = null;
     }
     return $this->getOption($name, $default);
 }
 /**
  * Monolithic function to send an email!
  *
  * Several configuration nodes are required for this function:
  * email/from-address      Default address for the From header
  * email/bounce-address    Default address to use when VERPing the email.
  *     IE: bounce+$1@contoso.com
  * email/archive-addresses A list of addresses to always BCC when this function is used
  *
  * @param string            $to        Email address of recipient
  * @param string            $subject   Subject line of email
  * @param string            $textBody  Non HTML text of email (fallback text if $htmlBody is defined)
  * @param null|string|array $from      Email address of sender, if null is the value of the configuration
  *                                     node 'email/from-address'. If passed as an array it is expected that
  *                                     index 0 is the address and index 1 is the friendly name of the address.
  * @param null|string       $replyTo   Address that recipient will reply to. If null will be set from the value
  *                                     of $from.
  * @param null|string       $htmlBody  HTML text of the email
  * @param array             $attach    Paths to any attachments. These can have any legal PHP file descriptor.
  * @param null|string|array $cc        Carbon-Copy addresses.
  * @param null|string|array $bcc       Blind carbon-copy addresses. If specified these will always be in addition
  *                                     to any archival addresses specified by the 'email/archive-addresses'
  *                                     configuration node.
  * @param bool|string       $useVerp   If true will set the MAIL FROM to the value specified under configuration
  *                                     node 'email/bounce-address'. This can be overriden if a string is passed
  *                                     instead of strict true. In either case, '$1' will be replaced by the
  *                                     first $to address, RFC-3986 encoded.
  *
  * @returns bool True if successfully sent. False if a PHPMailer exception occurred. Exceptions are logged at the
  * warning level.
  */
 public static function sendEmail($to, $subject, $textBody, $from = null, $replyTo = null, $htmlBody = null, $attach = array(), $cc = null, $bcc = null, $useVerp = true)
 {
     $config = Context::get()->getConfiguration();
     $mailer = static::mailbaseFactory();
     try {
         $to = (array) $to;
         $cc = (array) $cc;
         $bcc = (array) $bcc;
         $archives = (array) $config->val('email/archive-addresses');
         array_walk($to, function ($value, $key) use($mailer) {
             $mailer->AddAddress($value);
         });
         array_walk($cc, function ($value, $key) use($mailer) {
             $mailer->AddCC($value);
         });
         array_walk($bcc, function ($value, $key) use($mailer) {
             $mailer->AddBCC($value);
         });
         array_walk($archives, function ($value, $key) use($mailer) {
             $mailer->AddBCC($value);
         });
         array_walk($attach, function ($value, $key) use($mailer) {
             $mailer->AddAttachment($value);
         });
         // Set the from address
         if (!$from) {
             $from = $config->val('email/from-address');
         }
         if (is_array($from)) {
             $mailer->SetFrom($from[0], $from[1]);
         } else {
             $mailer->SetFrom((string) $from);
         }
         // Only add reply to manually if requested, otherwise it's set when we call SetFrom
         if ($replyTo) {
             $mailer->AddReplyTo($replyTo);
         }
         // Set subject and body
         $mailer->Subject = $subject;
         if ($htmlBody) {
             $mailer->MsgHTML($htmlBody);
             $mailer->AltBody = $textBody;
         } else {
             $mailer->Body = $textBody;
         }
         // We replace $1 in email/bounce-address or useVerp if string to create the bounce addr
         if ($useVerp) {
             $sourceAddr = (array) $to;
             $sourceAddr = rawurlencode($sourceAddr[0]);
             if (is_string($useVerp)) {
                 $bounceAddr = $useVerp;
             } else {
                 $bounceAddr = $config->val('email/bounce-address');
             }
             $bounceAddr = str_replace('$1', $sourceAddr, $bounceAddr);
             $mailer->Sender = $bounceAddr;
         }
         $mailer->Send();
     } catch (\phpmailerException $ex) {
         $toStr = implode(", ", $to);
         Logger::warning("Could not send email to {$toStr}. PHP Mailer had exception.", null, $ex);
         return false;
     }
     return true;
 }