Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package crmsh for openSUSE:Factory checked 
in at 2021-03-19 16:43:25
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/crmsh (Old)
 and      /work/SRC/openSUSE:Factory/.crmsh.new.2401 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "crmsh"

Fri Mar 19 16:43:25 2021 rev:204 rq:880013 version:4.3.0+git.20210319.b0adc897

Changes:
--------
--- /work/SRC/openSUSE:Factory/crmsh/crmsh.changes      2021-03-15 
10:56:19.869349531 +0100
+++ /work/SRC/openSUSE:Factory/.crmsh.new.2401/crmsh.changes    2021-03-19 
16:43:26.382136955 +0100
@@ -1,0 +2,27 @@
+Fri Mar 19 04:26:16 UTC 2021 - xli...@suse.com
+
+- Update to version 4.3.0+git.20210319.b0adc897:
+  * Fix: update verion and author (bsc#1183689)
+
+-------------------------------------------------------------------
+Wed Mar 17 01:31:04 UTC 2021 - xli...@suse.com
+
+- Update to version 4.3.0+git.20210317.5ee12f25:
+  * Dev: behave: adjust functional test for configuring qdevice on interactive 
mode
+  * Dev: unittest: unit test codes for configuring qdevice on interactive mode
+  * Dev: bootstrap: enable configuring qdevice on interactive mode
+
+-------------------------------------------------------------------
+Mon Mar 15 09:59:59 UTC 2021 - xli...@suse.com
+
+- Update to version 4.3.0+git.20210315.5d07d43e:
+  * Dev: behave: change the test case for failcount behavior change
+  * Fix: ui_resource: change return code and error to warning for some 
unharmful actions(bsc#1180332)
+
+-------------------------------------------------------------------
+Mon Mar 15 09:32:43 UTC 2021 - xli...@suse.com
+
+- Update to version 4.3.0+git.20210315.fae29920:
+  * Dev: README: change the build status link in README
+
+-------------------------------------------------------------------

Old:
----
  crmsh-4.3.0+git.20210311.c2e8856c.tar.bz2

New:
----
  crmsh-4.3.0+git.20210319.b0adc897.tar.bz2

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ crmsh.spec ++++++
--- /var/tmp/diff_new_pack.3EIOwR/_old  2021-03-19 16:43:27.134137970 +0100
+++ /var/tmp/diff_new_pack.3EIOwR/_new  2021-03-19 16:43:27.138137975 +0100
@@ -36,7 +36,7 @@
 Summary:        High Availability cluster command-line interface
 License:        GPL-2.0-or-later
 Group:          %{pkg_group}
-Version:        4.3.0+git.20210311.c2e8856c
+Version:        4.3.0+git.20210319.b0adc897
 Release:        0
 URL:            http://crmsh.github.io
 Source0:        %{name}-%{version}.tar.bz2

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.3EIOwR/_old  2021-03-19 16:43:27.170138019 +0100
+++ /var/tmp/diff_new_pack.3EIOwR/_new  2021-03-19 16:43:27.174138024 +0100
@@ -9,6 +9,6 @@
 </service>
 <service name="tar_scm">
   <param name="url">https://github.com/ClusterLabs/crmsh.git</param>
-  <param 
name="changesrevision">e0bb141ccc848feedda7cab741fc035f62f19bc5</param>
+  <param 
name="changesrevision">b0adc897a8c08e5b6cab39c2981367f811b1492f</param>
 </service>
 </servicedata>
\ No newline at end of file

++++++ crmsh-4.3.0+git.20210311.c2e8856c.tar.bz2 -> 
crmsh-4.3.0+git.20210319.b0adc897.tar.bz2 ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-4.3.0+git.20210311.c2e8856c/README.md 
new/crmsh-4.3.0+git.20210319.b0adc897/README.md
--- old/crmsh-4.3.0+git.20210311.c2e8856c/README.md     2021-03-11 
08:39:55.000000000 +0100
+++ new/crmsh-4.3.0+git.20210319.b0adc897/README.md     2021-03-19 
05:13:08.000000000 +0100
@@ -1,6 +1,7 @@
 # crmsh
 
-[![Build 
Status](https://travis-ci.org/ClusterLabs/crmsh.svg?branch=master)](https://travis-ci.org/ClusterLabs/crmsh)
+[![Build 
Status](https://github.com/ClusterLabs/crmsh/actions/workflows/crmsh-ci.yml/badge.svg)](https://github.com/ClusterLabs/crmsh/actions/workflows/crmsh-ci.yml)
+
 
 ## Introduction
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-4.3.0+git.20210311.c2e8856c/configure.ac 
new/crmsh-4.3.0+git.20210319.b0adc897/configure.ac
--- old/crmsh-4.3.0+git.20210311.c2e8856c/configure.ac  2021-03-11 
08:39:55.000000000 +0100
+++ new/crmsh-4.3.0+git.20210319.b0adc897/configure.ac  2021-03-19 
05:13:08.000000000 +0100
@@ -8,7 +8,7 @@
 
 AC_PREREQ([2.53])
 
-AC_INIT([crmsh],[4.2.0],[us...@clusterlabs.org])
+AC_INIT([crmsh],[4.3.0],[us...@clusterlabs.org])
 
 AC_ARG_WITH(version,
     [  --with-version=version   Override package version (if you're a packager 
needing to pretend) ],
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-4.3.0+git.20210311.c2e8856c/crmsh/bootstrap.py 
new/crmsh-4.3.0+git.20210319.b0adc897/crmsh/bootstrap.py
--- old/crmsh-4.3.0+git.20210311.c2e8856c/crmsh/bootstrap.py    2021-03-11 
08:39:55.000000000 +0100
+++ new/crmsh-4.3.0+git.20210319.b0adc897/crmsh/bootstrap.py    2021-03-19 
05:13:08.000000000 +0100
@@ -1873,15 +1873,42 @@
         return QdevicePolicy.QDEVICE_RESTART
 
 
+def configure_qdevice_interactive():
+    """
+    Configure qdevice on interactive mode
+    """
+    if _context.yes_to_all or not confirm("Do you want to configure QDevice?"):
+        return
+    qnetd_addr = prompt_for_string("HOST or IP of the QNetd server to be used")
+    if not qnetd_addr:
+        error("Address of QNetd is required")
+    qdevice_port = prompt_for_string("TCP PORT of QNetd server", default=5403)
+    qdevice_algo = prompt_for_string("QNetd decision ALGORITHM (ffsplit/lms)", 
default="ffsplit")
+    qdevice_tie_breaker = prompt_for_string("QNetd TIE_BREAKER 
(lowest/highest/valid node id)", default="lowest")
+    qdevice_tls = prompt_for_string("Whether using TLS on QDevice/QNetd 
(on/off/required)", default="on")
+    qdevice_heuristics = prompt_for_string("Heuristics COMMAND to run with 
absolute path; For multiple commands, use \";\" to separate")
+    qdevice_heuristics_mode = prompt_for_string("MODE of operation of 
heuristics (on/sync/off)", default="sync") if qdevice_heuristics else None
+    _context.qdevice_inst = corosync.QDevice(
+            qnetd_addr,
+            port=qdevice_port,
+            algo=qdevice_algo,
+            tie_breaker=qdevice_tie_breaker,
+            tls=qdevice_tls,
+            cmds=qdevice_heuristics,
+            mode=qdevice_heuristics_mode)
+    _context.qdevice_inst.valid_attr()
+
+
 def init_qdevice():
     """
     Setup qdevice and qnetd service
     """
+    if not _context.qdevice_inst:
+        configure_qdevice_interactive()
     # If don't want to config qdevice, return
     if not _context.qdevice_inst:
         utils.disable_service("corosync-qdevice.service")
         return
-
     if _context.stage == "qdevice":
         utils.check_all_nodes_reachable()
         _context.qdevice_reload_policy = 
evaluate_qdevice_quorum_effect(QDEVICE_ADD)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-4.3.0+git.20210311.c2e8856c/crmsh/corosync.py 
new/crmsh-4.3.0+git.20210319.b0adc897/crmsh/corosync.py
--- old/crmsh-4.3.0+git.20210311.c2e8856c/crmsh/corosync.py     2021-03-11 
08:39:55.000000000 +0100
+++ new/crmsh-4.3.0+git.20210319.b0adc897/crmsh/corosync.py     2021-03-19 
05:13:08.000000000 +0100
@@ -278,9 +278,15 @@
         if not utils.valid_port(self.port):
             raise ValueError("invalid qdevice port range(1024 - 65535)")
 
-        if self.tie_breaker not in ["lowest", "highest"] and not 
utils.valid_nodeid(self.tie_breaker):
+        if self.algo not in ("ffsplit", "lms"):
+            raise ValueError("invalid ALGORITHM choice: '{}' (choose from 
'ffsplit', 'lms')".format(self.algo))
+
+        if self.tie_breaker not in ("lowest", "highest") and not 
utils.valid_nodeid(self.tie_breaker):
             raise ValueError("invalid qdevice 
tie_breaker(lowest/highest/valid_node_id)")
 
+        if self.tls not in ("on", "off", "required"):
+            raise ValueError("invalid TLS choice: '{}' (choose from 'on', 
'off', 'required')".format(self.tls))
+
         if self.cmds:
             for cmd in self.cmds.strip(';').split(';'):
                 if not cmd.startswith('/'):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.3.0+git.20210311.c2e8856c/crmsh/ui_cluster.py 
new/crmsh-4.3.0+git.20210319.b0adc897/crmsh/ui_cluster.py
--- old/crmsh-4.3.0+git.20210311.c2e8856c/crmsh/ui_cluster.py   2021-03-11 
08:39:55.000000000 +0100
+++ new/crmsh-4.3.0+git.20210319.b0adc897/crmsh/ui_cluster.py   2021-03-19 
05:13:08.000000000 +0100
@@ -243,17 +243,17 @@
         qdevice_group.add_argument("--qnetd-hostname", dest="qnetd_addr", 
metavar="HOST",
                                    help="HOST or IP of the QNetd server to be 
used")
         qdevice_group.add_argument("--qdevice-port", dest="qdevice_port", 
metavar="PORT", type=int, default=5403,
-                                   help="TCP PORT of QNetd 
server(default:5403)")
+                                   help="TCP PORT of QNetd server 
(default:5403)")
         qdevice_group.add_argument("--qdevice-algo", dest="qdevice_algo", 
metavar="ALGORITHM", default="ffsplit", choices=['ffsplit', 'lms'],
-                                   help="QNetd decision ALGORITHM(ffsplit/lms, 
default:ffsplit)")
+                                   help="QNetd decision ALGORITHM 
(ffsplit/lms, default:ffsplit)")
         qdevice_group.add_argument("--qdevice-tie-breaker", 
dest="qdevice_tie_breaker", metavar="TIE_BREAKER", default="lowest",
-                                   help="QNetd 
TIE_BREAKER(lowest/highest/valid_node_id, default:lowest)")
+                                   help="QNetd TIE_BREAKER 
(lowest/highest/valid_node_id, default:lowest)")
         qdevice_group.add_argument("--qdevice-tls", dest="qdevice_tls", 
metavar="TLS", default="on", choices=['on', 'off', 'required'],
-                                   help="Whether using TLS on 
QDevice/QNetd(on/off/required, default:on)")
+                                   help="Whether using TLS on QDevice/QNetd 
(on/off/required, default:on)")
         qdevice_group.add_argument("--qdevice-heuristics", 
dest="qdevice_heuristics", metavar="COMMAND",
-                                   help="COMMAND to run with absolute path. 
For multiple commands, use \";\" to separate(details about heuristics can see 
man 8 corosync-qdevice)")
+                                   help="COMMAND to run with absolute path. 
For multiple commands, use \";\" to separate (details about heuristics can see 
man 8 corosync-qdevice)")
         qdevice_group.add_argument("--qdevice-heuristics-mode", 
dest="qdevice_heuristics_mode", metavar="MODE", choices=['on', 'sync', 'off'],
-                                   help="MODE of operation of 
heuristics(on/sync/off, default:sync)")
+                                   help="MODE of operation of heuristics 
(on/sync/off, default:sync)")
 
         storage_group = parser.add_argument_group("Storage configuration", 
"Options for configuring shared storage.")
         storage_group.add_argument("-p", "--partition-device", 
dest="shared_device", metavar="DEVICE",
@@ -277,7 +277,7 @@
             if options.qdevice_heuristics_mode and not 
options.qdevice_heuristics:
                 parser.error("Option --qdevice-heuristics is required if want 
to configure heuristics mode")
             options.qdevice_heuristics_mode = options.qdevice_heuristics_mode 
or "sync"
-        elif re.search("--qdevice-.*", ' '.join(sys.argv)) or stage == 
"qdevice":
+        elif re.search("--qdevice-.*", ' '.join(sys.argv)) or (stage == 
"qdevice" and options.yes_to_all):
             parser.error("Option --qnetd-hostname is required if want to 
configure qdevice")
 
         if options.sbd_devices and options.diskless_sbd:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.3.0+git.20210311.c2e8856c/crmsh/ui_resource.py 
new/crmsh-4.3.0+git.20210319.b0adc897/crmsh/ui_resource.py
--- old/crmsh-4.3.0+git.20210311.c2e8856c/crmsh/ui_resource.py  2021-03-11 
08:39:55.000000000 +0100
+++ new/crmsh-4.3.0+git.20210319.b0adc897/crmsh/ui_resource.py  2021-03-19 
05:13:08.000000000 +0100
@@ -496,7 +496,8 @@
             return m[:len(m)-3]
 
         if rsc not in compl.resources():
-            context.fatal_error("Resource {} not exists in this 
cluster".format(rsc))
+            context.warning("Resource {} not exists in this 
cluster".format(rsc))
+            return
         valid_cmd_list = ["set", "delete", "show"]
         if cmd not in valid_cmd_list:
             context.fatal_error("{} is not valid command(should be one of 
{})".format(cmd, valid_cmd_list))
@@ -515,7 +516,7 @@
             import re
             failcount_res = 
re.findall(r'fail-count-{}#(.*)_([0-9]+)'.format(rsc), out)
             if not failcount_res:
-                context.fatal_error("No failcount on node {} for resource 
{}".format(node, rsc))
+                return True if value and int(value) == 0 else False
             failcount_dict = dict(failcount_res)
 
             # validate for operation and interval
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-4.3.0+git.20210311.c2e8856c/setup.py 
new/crmsh-4.3.0+git.20210319.b0adc897/setup.py
--- old/crmsh-4.3.0+git.20210311.c2e8856c/setup.py      2021-03-11 
08:39:55.000000000 +0100
+++ new/crmsh-4.3.0+git.20210319.b0adc897/setup.py      2021-03-19 
05:13:08.000000000 +0100
@@ -4,10 +4,10 @@
 from setuptools import setup
 
 setup(name='crmsh',
-      version='4.2.0',
+      version='4.3.0',
       description='Command-line interface for High-Availability cluster 
management',
-      author='Kristoffer Gronlund',
-      author_email='kgronl...@suse.com',
+      author='Kristoffer Gronlund, Xin Liang',
+      author_email='xli...@suse.com',
       url='http://crmsh.github.io/',
       packages=['crmsh'],
       install_requires=['parallax', 'lxml', 'PyYAML', 'py-dateutil'],
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.3.0+git.20210311.c2e8856c/test/features/qdevice_validate.feature 
new/crmsh-4.3.0+git.20210319.b0adc897/test/features/qdevice_validate.feature
--- 
old/crmsh-4.3.0+git.20210311.c2e8856c/test/features/qdevice_validate.feature    
    2021-03-11 08:39:55.000000000 +0100
+++ 
new/crmsh-4.3.0+git.20210319.b0adc897/test/features/qdevice_validate.feature    
    2021-03-19 05:13:08.000000000 +0100
@@ -102,7 +102,7 @@
     Given   Cluster service is "stopped" on "hanode1"
     When    Run "crm cluster init -y" on "hanode1"
     Then    Cluster service is "started" on "hanode1"
-    When    Try "crm cluster init qdevice"
+    When    Try "crm cluster init qdevice -y"
     Then    Except multiple lines
       """
       usage: init [options] [STAGE]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.3.0+git.20210311.c2e8856c/test/features/resource_failcount.feature 
new/crmsh-4.3.0+git.20210319.b0adc897/test/features/resource_failcount.feature
--- 
old/crmsh-4.3.0+git.20210311.c2e8856c/test/features/resource_failcount.feature  
    2021-03-11 08:39:55.000000000 +0100
+++ 
new/crmsh-4.3.0+git.20210319.b0adc897/test/features/resource_failcount.feature  
    2021-03-19 05:13:08.000000000 +0100
@@ -12,14 +12,10 @@
 
   @clean
   Scenario: Validation, input the wrong parameters
-    When    Try "crm resource failcount ddd show hanode1"
-    Then    Except "ERROR: resource.failcount: Resource ddd not exists in this 
cluster"
     When    Try "crm resource failcount d showss hanode1"
     Then    Except "ERROR: resource.failcount: showss is not valid 
command(should be one of ['set', 'delete', 'show'])"
     When    Try "crm resource failcount d set hanode11 0"
     Then    Except "ERROR: resource.failcount: Node hanode11 not in this 
cluster"
-    When    Try "crm resource failcount d set hanode1 0"
-    Then    Except "ERROR: resource.failcount: No failcount on node hanode1 
for resource d"
 
   @clean
   Scenario: Set the failcount to 0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.3.0+git.20210311.c2e8856c/test/features/steps/const.py 
new/crmsh-4.3.0+git.20210319.b0adc897/test/features/steps/const.py
--- old/crmsh-4.3.0+git.20210311.c2e8856c/test/features/steps/const.py  
2021-03-11 08:39:55.000000000 +0100
+++ new/crmsh-4.3.0+git.20210319.b0adc897/test/features/steps/const.py  
2021-03-19 05:13:08.000000000 +0100
@@ -98,20 +98,21 @@
 
   --qnetd-hostname HOST
                         HOST or IP of the QNetd server to be used
-  --qdevice-port PORT   TCP PORT of QNetd server(default:5403)
+  --qdevice-port PORT   TCP PORT of QNetd server (default:5403)
   --qdevice-algo ALGORITHM
-                        QNetd decision ALGORITHM(ffsplit/lms, default:ffsplit)
+                        QNetd decision ALGORITHM (ffsplit/lms,
+                        default:ffsplit)
   --qdevice-tie-breaker TIE_BREAKER
-                        QNetd TIE_BREAKER(lowest/highest/valid_node_id,
+                        QNetd TIE_BREAKER (lowest/highest/valid_node_id,
                         default:lowest)
-  --qdevice-tls TLS     Whether using TLS on QDevice/QNetd(on/off/required,
+  --qdevice-tls TLS     Whether using TLS on QDevice/QNetd (on/off/required,
                         default:on)
   --qdevice-heuristics COMMAND
                         COMMAND to run with absolute path. For multiple
-                        commands, use ";" to separate(details about heuristics
-                        can see man 8 corosync-qdevice)
+                        commands, use ";" to separate (details about
+                        heuristics can see man 8 corosync-qdevice)
   --qdevice-heuristics-mode MODE
-                        MODE of operation of heuristics(on/sync/off,
+                        MODE of operation of heuristics (on/sync/off,
                         default:sync)
 
 Storage configuration:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.3.0+git.20210311.c2e8856c/test/unittests/test_bootstrap.py 
new/crmsh-4.3.0+git.20210319.b0adc897/test/unittests/test_bootstrap.py
--- old/crmsh-4.3.0+git.20210311.c2e8856c/test/unittests/test_bootstrap.py      
2021-03-11 08:39:55.000000000 +0100
+++ new/crmsh-4.3.0+git.20210319.b0adc897/test/unittests/test_bootstrap.py      
2021-03-19 05:13:08.000000000 +0100
@@ -1416,6 +1416,51 @@
         mock_status_done.assert_called_once_with()
         mock_start_qdevice.assert_called_once_with()
 
+    @mock.patch('crmsh.bootstrap.prompt_for_string')
+    def test_configure_qdevice_interactive_return(self, mock_prompt):
+        bootstrap._context = mock.Mock(yes_to_all=True)
+        bootstrap.configure_qdevice_interactive()
+        mock_prompt.assert_not_called()
+
+    @mock.patch('crmsh.bootstrap.error')
+    @mock.patch('crmsh.bootstrap.prompt_for_string')
+    @mock.patch('crmsh.bootstrap.confirm')
+    def test_configure_qdevice_interactive_error(self, mock_confirm, 
mock_prompt, mock_error):
+        bootstrap._context = mock.Mock(yes_to_all=False)
+        mock_confirm.return_value = True
+        mock_prompt.return_value = None
+        mock_error.side_effect = SystemExit
+
+        with self.assertRaises(SystemExit):
+            bootstrap.configure_qdevice_interactive()
+
+        mock_error.assert_called_once_with("Address of QNetd is required")
+        mock_confirm.assert_called_once_with("Do you want to configure 
QDevice?")
+        mock_prompt.assert_called_once_with("HOST or IP of the QNetd server to 
be used")
+
+    @mock.patch('crmsh.corosync.QDevice')
+    @mock.patch('crmsh.bootstrap.prompt_for_string')
+    @mock.patch('crmsh.bootstrap.confirm')
+    def test_configure_qdevice_interactive(self, mock_confirm, mock_prompt, 
mock_qdevice):
+        bootstrap._context = mock.Mock(yes_to_all=False)
+        mock_confirm.return_value = True
+        mock_prompt.side_effect = ["qnetd-node", 5403, "ffsplit", "lowest", 
"on", None]
+        mock_qdevice_inst = mock.Mock()
+        mock_qdevice.return_value = mock_qdevice_inst
+
+        bootstrap.configure_qdevice_interactive()
+        mock_confirm.assert_called_once_with("Do you want to configure 
QDevice?")
+        mock_prompt.assert_has_calls([
+            mock.call("HOST or IP of the QNetd server to be used"),
+            mock.call("TCP PORT of QNetd server", default=5403),
+            mock.call("QNetd decision ALGORITHM (ffsplit/lms)", 
default="ffsplit"),
+            mock.call("QNetd TIE_BREAKER (lowest/highest/valid node id)", 
default="lowest"),
+            mock.call("Whether using TLS on QDevice/QNetd (on/off/required)", 
default="on"),
+            mock.call("Heuristics COMMAND to run with absolute path; For 
multiple commands, use \";\" to separate")
+            ])
+        mock_qdevice.assert_called_once_with('qnetd-node', port=5403, 
algo='ffsplit', tie_breaker='lowest', tls='on', cmds=None, mode=None)
+        mock_qdevice_inst.valid_attr.assert_called_once_with()
+
     @mock.patch('crmsh.corosync.QDevice.start_qnetd')
     @mock.patch('crmsh.corosync.QDevice.enable_qnetd')
     @mock.patch('crmsh.utils.cluster_run_cmd')

Reply via email to