csantanapr closed pull request #86: Do not hard code API host of push 
notification service
URL: 
https://github.com/apache/incubator-openwhisk-package-pushnotifications/pull/86
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/README.md b/README.md
index 43a4dc6..b96f1c3 100644
--- a/README.md
+++ b/README.md
@@ -9,8 +9,8 @@ The package includes the following action and feed:
 
 | Entity | Type | Parameters | Description |
 | --- | --- | --- | --- |
-| `/whisk.system/pushnotifications` | package | appId, appSecret  | Work with 
the Push Service |
-| `/whisk.system/pushnotifications/sendMessage` | action | text, url, 
deviceIds, platforms, userIds, tagNames, gcmCollapseKey, gcmCategory, gcmIcon, 
gcmDelayWhileIdle, gcmSync, gcmVisibility, gcmPayload, gcmPriority, gcmSound, 
gcmTimeToLive, gcmStyleType, gcmStyleTitle, gcmStyleUrl, gcmStyleText, 
gcmStyleLines, gcmLightsLedArgb, gcmLightsLedOnMs, gcmLightsLedOffMs, 
apnsBadge, apnsCategory, apnsIosActionKey, apnsPayload, apnsType, apnsSound, 
apnsTitleLocKey, apnsLocKey, apnsLaunchImage, apnsTitleLocArgs, apnsLocArgs, 
apnstitle, apnsSubtitle, apnsAttachmentUrl, fireFoxTitle, fireFoxIconUrl, 
fireFoxTimeToLive, fireFoxPayload, safariTitle, safariUrlArgs, safariAction, 
chromeTitle, chromeIconUrl, chromeTimeToLive, chromePayload, chromeAppExtTitle, 
chromeAppExtCollapseKey, chromeAppExtDelayWhileIdle, chromeAppExtIconUrl, 
chromeAppExtTimeToLive, chromeAppExtPayload | Send push notification to one or 
more specified devices |
+| `/whisk.system/pushnotifications` | package | appId, appSecret, admin_url  | 
Work with the Push Service |
+| `/whisk.system/pushnotifications/sendMessage` | action | text, url, apiHost, 
deviceIds, platforms, userIds, tagNames, gcmCollapseKey, gcmCategory, gcmIcon, 
gcmDelayWhileIdle, gcmSync, gcmVisibility, gcmPayload, gcmPriority, gcmSound, 
gcmTimeToLive, gcmStyleType, gcmStyleTitle, gcmStyleUrl, gcmStyleText, 
gcmStyleLines, gcmLightsLedArgb, gcmLightsLedOnMs, gcmLightsLedOffMs, 
apnsBadge, apnsCategory, apnsIosActionKey, apnsPayload, apnsType, apnsSound, 
apnsTitleLocKey, apnsLocKey, apnsLaunchImage, apnsTitleLocArgs, apnsLocArgs, 
apnstitle, apnsSubtitle, apnsAttachmentUrl, fireFoxTitle, fireFoxIconUrl, 
fireFoxTimeToLive, fireFoxPayload, safariTitle, safariUrlArgs, safariAction, 
chromeTitle, chromeIconUrl, chromeTimeToLive, chromePayload, chromeAppExtTitle, 
chromeAppExtCollapseKey, chromeAppExtDelayWhileIdle, chromeAppExtIconUrl, 
chromeAppExtTimeToLive, chromeAppExtPayload | Send push notification to one or 
more specified devices |
 | `/whisk.system/pushnotifications/webhook` | feed | events | Fire trigger 
events on device activities (device registration, unregistration, subscription, 
or unsubscription) on the Push service |
 Creating a package binding with the `appId` and `appSecret` values is 
suggested. This way, you don't need to specify these credentials every time you 
invoke the actions in the package.
 
@@ -54,6 +54,7 @@ The `/whisk.system/pushnotifications/sendMessage` action 
sends push notification
 
 - `text`: The notification message to be shown to the user. For example: `-p 
text "Hi ,OpenWhisk send a notification"`.
 - `url`: An optional URL that can be sent along with the alert. For example: 
`-p url "https:\\www.w3.ibm.com"`.
+- `apiHost`: An optional string that specifies the API host.  The default is 
'mobile.ng.bluemix.net'.  For example: `-p apiHost "mobile.eu-gb.bluemix.net"`.
 - `deviceIds` The list of specified devices. For example: `-p deviceIds 
"[\"deviceID1\"]"`.
 - `platforms` Send notification to the devices of the specified platforms. 'A' 
for apple (iOS) devices and 'G' for google (Android) devices. For example `-p 
platforms ["A"]`.
 - `userIds` - Send notification to the devices of the specified users. For 
example: `-p userIds "[\"testUser\"]"`
diff --git a/packages/actions/sendMessage.js b/packages/actions/sendMessage.js
index c3c39ab..4ee1584 100644
--- a/packages/actions/sendMessage.js
+++ b/packages/actions/sendMessage.js
@@ -21,6 +21,7 @@
 *  @param {string} appGuid - appGuid to create webhook
 *  @param {string} appSecret - appSecret of the application
 *  @param {string} url - An optional URL that can be sent along with the 
alert. Eg : -p url "https:\\www.mycompany.com".
+*  @param {string} apiHost - An optional string that specifies the API host.  
The default is 'mobile.ng.bluemix.net'.  Eg : -p apiHost 
"mobile.eu-gb.bluemix.net".
 *  @param {object} text - The notification message to be shown to the user. 
Eg: -p text "Hi ,OpenWhisk send a notification"
 *  @param {string} deviceIds - Send notification to the list of specified 
devices. Eg: -p deviceIds "["deviceID1"]"
 *  @param {object} platforms - Send notification to the devices of the 
specified platforms. 'A' for apple (iOS) devices and 'G' for google (Android) 
devices. Eg: -p platforms ["A"]
@@ -85,6 +86,7 @@
 */
 module.paths.push('/usr/lib/node_modules');
 var https = require('https');
+var url = require('url');
 
 function main(params) {
 
@@ -426,10 +428,22 @@ function main(params) {
 
   var bodyData = JSON.stringify(sendMessage);
   var request = require('request');
+  var apiHost;
+  if (params.apiHost) {
+    apiHost = params.apiHost;
+  }
+  else if (params.admin_url) {
+    var adminURL = url.parse(params.admin_url).protocol === null ? 
`https:${params.admin_url}` : params.admin_url;
+    apiHost = url.parse(adminURL).host;
+  }
+  else {
+    apiHost = 'mobile.ng.bluemix.net';
+  }
+
   var promise = new Promise(function (resolve, reject) {
     request({
       method: 'post',
-      uri: 'https://mobile.ng.bluemix.net/imfpush/v1/apps/' + appId + 
'/messages',
+      uri: `https://${apiHost}/imfpush/v1/apps/${appId}/messages`,
       headers: {
         'appSecret': appSecret,
         'Accept': 'application/json',
diff --git a/packages/installCatalog.sh b/packages/installCatalog.sh
index 06ce2fe..0dffbff 100755
--- a/packages/installCatalog.sh
+++ b/packages/installCatalog.sh
@@ -36,11 +36,11 @@ echo Installing pushnotifications package.
 
 $WSK_CLI -i --apihost "$APIHOST"  package update --auth "$AUTH"  --shared yes 
"pushnotifications" \
 -a description "This package supports sending push notifications to your 
mobile device, using the IBM Bluemix Push Notifications service." \
--a parameters '[ {"name":"appGuid", "required":true, "bindTime":true, 
"description":"Bluemix application GUID"}, {"name":"appSecret", 
"required":true, "bindTime":true, "type":"password", "description":"Bluemix 
Push Service Secret"}]' \
+-a parameters '[ {"name":"appGuid", "required":true, "bindTime":true, 
"description":"Bluemix application GUID"}, {"name":"appSecret", 
"required":true, "bindTime":true, "type":"password", "description":"Bluemix 
Push Service Secret"}, {"name":"admin_url", "required":false, 
"bindTime":true}]' \
 -a prettyName "Push Notifications" \
 -p bluemixServiceName 'imfpush'
 
-$WSK_CLI -i --apihost "$APIHOST" action update --auth "$AUTH" 
"pushnotifications/webhook" "$PACKAGE_HOME/feeds/webhook.js" \
+$WSK_CLI -i --apihost "$APIHOST" action update --kind nodejs:6 --auth "$AUTH" 
"pushnotifications/webhook" "$PACKAGE_HOME/feeds/webhook.js" \
 -a feed true \
 -a description 'pushnotifications feed' \
 -a parameters '[ {"name":"appGuid", "required":true, "bindTime":true, 
"description":"Bluemix application GUID"}, {"name":"appSecret", 
"required":true, "bindTime":true, "type":"password", "description":"Bluemix 
Push Service Secret"},{"name":"events", "required":true, "description":"Name of 
the event user want to subscribe"} ]' \
@@ -48,8 +48,8 @@ $WSK_CLI -i --apihost "$APIHOST" action update --auth "$AUTH" 
"pushnotifications
 -a sampleOutput '{"tagName": "tagName","eventType": 
"onDeviceRegister","applicationId": "xxx-xxx-xx"}'
 
 
-$WSK_CLI -i --apihost "$APIHOST" action update --auth "$AUTH" 
"pushnotifications/sendMessage" "$PACKAGE_HOME/actions/sendMessage.js" \
+$WSK_CLI -i --apihost "$APIHOST" action update --kind nodejs:6 --auth "$AUTH" 
"pushnotifications/sendMessage" "$PACKAGE_HOME/actions/sendMessage.js" \
 -a description 'Send push notification to all application users or to a 
specific set of devices' \
--a parameters '[ {"name":"appGuid", "required":true, "bindTime":true, 
"description":"Bluemix application GUID"}, {"name":"appSecret", 
"required":true, "bindTime":true, "type":"password", "description":"Bluemix 
Push Service Secret"}, {"name":"text", "required":true, "description":"The 
notification message to be shown to the user"}, {"name":"url", 
"required":false, "description":"An optional URL that can be sent along with 
the alert"}, {"name":"deviceIds", "required":false, "description":"Array of 
device IDs"}, {"name":"platforms", "required":false, "description":"Array of 
device platform"},{"name":"userIds", "required":false, "description":"Array of 
UserIds"},{"name":"tagNames", "required":false, "description":"Array of tag 
names"},{"name":"gcmCollapseKey", "required":false, "description":"This 
parameter identifies a group of messages"},{"name":"gcmCategory", 
"required":false, "description":"The category identifier to be used for the 
interactive push notifications"},{"name":"gcmIcon", "required":false, 
"description":"Specify the name of the icon to be displayed for the 
notification"},{"name":"gcmDelayWhileIdle", "required":false, 
"description":"Send message when device is active"}, {"name":"gcmSync", 
"required":false, "description":"Device group messaging"}, 
{"name":"gcmVisibility", "required":false, "description":"private/public - 
Visibility of notification"},{"name":"gcmPayload", "required":false, 
"description":"Additional payload"}, {"name":"gcmPriority", "required":false, 
"description":"Sets the priority of the message"},{"name":"gcmSound", 
"required":false, "description":"Sound file name"}, {"name":"gcmTimeToLive", 
"required":false, "description":"Time limit for message to be delievered"}, 
{"name":"gcmStyleType", "required":false, "description":"Specifies the type of 
expandable notifications"}, {"name":"gcmStyleTitle", "required":false, 
"description":"Specifies the title of the notification"}, 
{"name":"gcmStyleUrl", "required":false, "description":"An URL from which the 
picture has to be obtained for the notification"}, {"name":"gcmStyleText", 
"required":false, "description":"The big text in bigtext_notification"}, 
{"name":"gcmStyleLines", "required":false, "description":"An array of strings 
for inbox_notification"},{"name":"gcmLightsLedArgb", "required":false, 
"description":"The color of the led. The hardware will do its best 
approximation"},{"name":"gcmLightsLedOnMs", "required":false, 
"description":"The number of milliseconds for the LED to be on while it is 
flashing. The hardware will do its best 
approximation"},{"name":"gcmLightsLedOffMs", "required":false, 
"description":"The number of milliseconds for the LED to be off while it iss 
flashing. The hardware will do its best approximation"},{"name":"apnsBadge", 
"required":false, "description":"Value for Badge"}, {"name":"apnsCategory", 
"required":false, "description":"The category name"}, 
{"name":"apnsIosActionKey", "required":false, "description":"Title for the push 
notification action Key"},{"name":"apnsPayload", "required":false, 
"description":"Additional payload"},{"name":"apnsType", "required":false, 
"description":"Push notification type name"},{"name":"apnsSound", 
"required":false, "description":"APNS sound name"}, {"name":"apnsTitleLocKey", 
"required":false, "description":"The key to a title string in the 
Localizable.strings file for the current localization"},{"name":"apnsLocKey", 
"required":false, "description":"A key to an alert-message string in a 
Localizable.strings file for the current 
localization"},{"name":"apnsLaunchImage", "required":false, "description":"The 
filename of an image file in the app bundle, with or without the filename 
extension"},{"name":"apnsTitleLocArgs", "required":false, 
"description":"Variable string values to appear in place of the format 
specifiers in title-loc-key"},{"name":"apnsLocArgs", "required":false, 
"description":"Variable string values to appear in place of the format 
specifiers in locKey"},{"name":"apnstitle", "required":false, 
"description":"The title of Rich Push notifications"},{"name":"apnsSubtitle", 
"required":false, "description":"The subtitle of the Rich 
Notifications"},{"name":"apnsAttachmentUrl", "required":false, 
"description":"The link to the iOS notifications 
media"},{"name":"fireFoxTitle", "required":false, "description":"Specifies the 
title to be set for the WebPush Notification"}, {"name":"fireFoxIconUrl", 
"required":false, "description":"The URL of the icon to be set for the WebPush 
Notification."}, {"name":"fireFoxTimeToLive", "required":false, 
"description":"This parameter specifies how long (in seconds) the message 
should be kept in GCM storage if the device is offline."}, 
{"name":"fireFoxPayload", "required":false, "description":"Custom JSON 
payload"}, {"name":"chromeTitle", "required":false, "description":"Specifies 
the title to be set for the WebPush Notification"}, {"name":"chromeIconUrl", 
"required":false, "description":"The URL of the icon to be set for the WebPush 
Notification"}, {"name":"chromeTimeToLive", "required":false, 
"description":"This parameter specifies how long (in seconds) the message 
should be kept in GCM storage if the device is offline."}, 
{"name":"chromePayload", "required":false, "description":"Custom JSON 
payload"},{"name":"safariTitle", "required":false, "description":"Specifies the 
title to be set for the Safari Push Notifications"},{"name":"safariUrlArgs", 
"required":false, "description":"The URL arguments that need to be used with 
this notification. This has to provided in the form of a JSON 
Array"},{"name":"safariAction", "required":false, "description":"The label of 
the action button"},{"name":"chromeAppExtTitle", "required":false, 
"description":"Specifies the title to be set for the WebPush Notification"}, 
{"name":"chromeAppExtCollapseKey", "required":false, "description":"This 
parameter identifies a group of 
messages"},{"name":"chromeAppExtDelayWhileIdle", "required":false, 
"description":"When this parameter is set to true, it indicates that the 
message should not be sent until the device becomes active"}, 
{"name":"chromeAppExtIconUrl", "required":false, "description":"The URL of the 
icon to be set for the WebPush Notification"}, 
{"name":"chromeAppExtTimeToLive", "required":false, "description":"This 
parameter specifies how long (in seconds) the message should be kept in GCM 
storage if the device is offline"}, {"name":"chromeAppExtPayload", 
"required":false, "description":"Custom JSON payload"}]' \
+-a parameters '[ {"name":"appGuid", "required":true, "bindTime":true, 
"description":"Bluemix application GUID"}, {"name":"appSecret", 
"required":true, "bindTime":true, "type":"password", "description":"Bluemix 
Push Service Secret"}, {"name":"text", "required":true, "description":"The 
notification message to be shown to the user"}, {"name":"url", 
"required":false, "description":"An optional URL that can be sent along with 
the alert"}, {"name":"apiHost", "required":false, "description":"API host"}, 
{"name":"deviceIds", "required":false, "description":"Array of device IDs"}, 
{"name":"platforms", "required":false, "description":"Array of device 
platform"},{"name":"userIds", "required":false, "description":"Array of 
UserIds"},{"name":"tagNames", "required":false, "description":"Array of tag 
names"},{"name":"gcmCollapseKey", "required":false, "description":"This 
parameter identifies a group of messages"},{"name":"gcmCategory", 
"required":false, "description":"The category identifier to be used for the 
interactive push notifications"},{"name":"gcmIcon", "required":false, 
"description":"Specify the name of the icon to be displayed for the 
notification"},{"name":"gcmDelayWhileIdle", "required":false, 
"description":"Send message when device is active"}, {"name":"gcmSync", 
"required":false, "description":"Device group messaging"}, 
{"name":"gcmVisibility", "required":false, "description":"private/public - 
Visibility of notification"},{"name":"gcmPayload", "required":false, 
"description":"Additional payload"}, {"name":"gcmPriority", "required":false, 
"description":"Sets the priority of the message"},{"name":"gcmSound", 
"required":false, "description":"Sound file name"}, {"name":"gcmTimeToLive", 
"required":false, "description":"Time limit for message to be delivered"}, 
{"name":"gcmStyleType", "required":false, "description":"Specifies the type of 
expandable notifications"}, {"name":"gcmStyleTitle", "required":false, 
"description":"Specifies the title of the notification"}, 
{"name":"gcmStyleUrl", "required":false, "description":"An URL from which the 
picture has to be obtained for the notification"}, {"name":"gcmStyleText", 
"required":false, "description":"The big text in bigtext_notification"}, 
{"name":"gcmStyleLines", "required":false, "description":"An array of strings 
for inbox_notification"},{"name":"gcmLightsLedArgb", "required":false, 
"description":"The color of the led. The hardware will do its best 
approximation"},{"name":"gcmLightsLedOnMs", "required":false, 
"description":"The number of milliseconds for the LED to be on while it is 
flashing. The hardware will do its best 
approximation"},{"name":"gcmLightsLedOffMs", "required":false, 
"description":"The number of milliseconds for the LED to be off while it iss 
flashing. The hardware will do its best approximation"},{"name":"apnsBadge", 
"required":false, "description":"Value for Badge"}, {"name":"apnsCategory", 
"required":false, "description":"The category name"}, 
{"name":"apnsIosActionKey", "required":false, "description":"Title for the push 
notification action Key"},{"name":"apnsPayload", "required":false, 
"description":"Additional payload"},{"name":"apnsType", "required":false, 
"description":"Push notification type name"},{"name":"apnsSound", 
"required":false, "description":"APNS sound name"}, {"name":"apnsTitleLocKey", 
"required":false, "description":"The key to a title string in the 
Localizable.strings file for the current localization"},{"name":"apnsLocKey", 
"required":false, "description":"A key to an alert-message string in a 
Localizable.strings file for the current 
localization"},{"name":"apnsLaunchImage", "required":false, "description":"The 
filename of an image file in the app bundle, with or without the filename 
extension"},{"name":"apnsTitleLocArgs", "required":false, 
"description":"Variable string values to appear in place of the format 
specifiers in title-loc-key"},{"name":"apnsLocArgs", "required":false, 
"description":"Variable string values to appear in place of the format 
specifiers in locKey"},{"name":"apnstitle", "required":false, 
"description":"The title of Rich Push notifications"},{"name":"apnsSubtitle", 
"required":false, "description":"The subtitle of the Rich 
Notifications"},{"name":"apnsAttachmentUrl", "required":false, 
"description":"The link to the iOS notifications 
media"},{"name":"fireFoxTitle", "required":false, "description":"Specifies the 
title to be set for the WebPush Notification"}, {"name":"fireFoxIconUrl", 
"required":false, "description":"The URL of the icon to be set for the WebPush 
Notification."}, {"name":"fireFoxTimeToLive", "required":false, 
"description":"This parameter specifies how long (in seconds) the message 
should be kept in GCM storage if the device is offline."}, 
{"name":"fireFoxPayload", "required":false, "description":"Custom JSON 
payload"}, {"name":"chromeTitle", "required":false, "description":"Specifies 
the title to be set for the WebPush Notification"}, {"name":"chromeIconUrl", 
"required":false, "description":"The URL of the icon to be set for the WebPush 
Notification"}, {"name":"chromeTimeToLive", "required":false, 
"description":"This parameter specifies how long (in seconds) the message 
should be kept in GCM storage if the device is offline."}, 
{"name":"chromePayload", "required":false, "description":"Custom JSON 
payload"},{"name":"safariTitle", "required":false, "description":"Specifies the 
title to be set for the Safari Push Notifications"},{"name":"safariUrlArgs", 
"required":false, "description":"The URL arguments that need to be used with 
this notification. This has to provided in the form of a JSON 
Array"},{"name":"safariAction", "required":false, "description":"The label of 
the action button"},{"name":"chromeAppExtTitle", "required":false, 
"description":"Specifies the title to be set for the WebPush Notification"}, 
{"name":"chromeAppExtCollapseKey", "required":false, "description":"This 
parameter identifies a group of 
messages"},{"name":"chromeAppExtDelayWhileIdle", "required":false, 
"description":"When this parameter is set to true, it indicates that the 
message should not be sent until the device becomes active"}, 
{"name":"chromeAppExtIconUrl", "required":false, "description":"The URL of the 
icon to be set for the WebPush Notification"}, 
{"name":"chromeAppExtTimeToLive", "required":false, "description":"This 
parameter specifies how long (in seconds) the message should be kept in GCM 
storage if the device is offline"}, {"name":"chromeAppExtPayload", 
"required":false, "description":"Custom JSON payload"}]' \
 -a sampleInput '{"appGuid":"xxx-xxx-xx", "appSecret":"yyy-yyy-yyy", "text":"hi 
there"}' \
 -a sampleOutput '{"pushResponse": 
{"messageId":"11111s","message":{"message":{"alert":"register for tag"}}}}'
diff --git a/tests/src/test/scala/packages/PushNotificationsTests.scala 
b/tests/src/test/scala/packages/PushNotificationsTests.scala
index cb6afc5..8da096e 100644
--- a/tests/src/test/scala/packages/PushNotificationsTests.scala
+++ b/tests/src/test/scala/packages/PushNotificationsTests.scala
@@ -31,6 +31,8 @@ class PushNotificationsTests
   val credentials = TestUtils.getVCAPcredentials("imfpush")
   val appSecret = credentials.get("appSecret").toJson;
   val credentialsUrl = credentials.get("url");
+  val adminURL = credentials.get("admin_url");
+  val apiHost = adminURL.split("/")(2);
   val appGuid = credentialsUrl.split("/").last.toJson;
   val url = "www.google.com".toJson;
 
@@ -67,4 +69,32 @@ class PushNotificationsTests
                 _.response.result.get.toString should include ("message")
              }
            }
+
+    it should "Send Notification action using admin_url" in {
+        val name = "/whisk.system/pushnotifications/sendMessage"
+        withActivation(wsk.activation,wsk.action.invoke(name, Map("appSecret" 
-> appSecret, "appGuid" -> appGuid, "text" -> messageText, "admin_url"-> 
adminURL.toJson))){
+            _.response.result.get.toString should include ("message")
+        }
+    }
+
+    it should "Send Notification action using bad admin_url" in {
+        val name = "/whisk.system/pushnotifications/sendMessage"
+        withActivation(wsk.activation,wsk.action.invoke(name, Map("appSecret" 
-> appSecret, "appGuid" -> appGuid, "text" -> messageText, "admin_url"-> 
"//mobile.bad.host/pathname".toJson))){
+            _.response.success shouldBe false
+        }
+    }
+
+    it should "Send Notification action using apiHost" in {
+        val name = "/whisk.system/pushnotifications/sendMessage"
+        withActivation(wsk.activation,wsk.action.invoke(name, Map("appSecret" 
-> appSecret, "appGuid" -> appGuid, "text" -> messageText, "apiHost"-> 
apiHost.toJson))){
+            _.response.result.get.toString should include ("message")
+        }
+    }
+
+    it should "Send Notification action using bad apiHost" in {
+        val name = "/whisk.system/pushnotifications/sendMessage"
+        withActivation(wsk.activation,wsk.action.invoke(name, Map("appSecret" 
-> appSecret, "appGuid" -> appGuid, "text" -> messageText, "apiHost"-> 
"mobile.bad.host".toJson))){
+            _.response.success shouldBe false
+        }
+    }
 }


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
us...@infra.apache.org


With regards,
Apache Git Services

Reply via email to