Title: [260865] trunk
Revision
260865
Author
[email protected]
Date
2020-04-28 22:16:23 -0700 (Tue, 28 Apr 2020)

Log Message

[Text manipulation] Add a userInfo dictionary to _WKTextManipulationToken
https://bugs.webkit.org/show_bug.cgi?id=211151
<rdar://problem/62329534>

Reviewed by Darin Adler.

Source/WebCore:

Add an extensible mechanism for the text manipulation controller to send additional
metadata for each text manipulation token through to the WebKit client, for debugging
purposes.

Test: TextManipulation.StartTextManipulationExtractsUserInfo

* editing/TextManipulationController.cpp:
(WebCore::tokenInfo):
(WebCore::TextManipulationController::observeParagraphs):
* editing/TextManipulationController.h:

Add TextManipulationTokenInfo, and add an optional TextManipulationTokenInfo member to
TextManipulationToken. For now, just send over the document URL, element tag name, and
the value of the role attribute.

(WebCore::TextManipulationController::ManipulationTokenInfo::encode const):
(WebCore::TextManipulationController::ManipulationTokenInfo::decode):
(WebCore::TextManipulationController::ManipulationToken::encode const):
(WebCore::TextManipulationController::ManipulationToken::decode):

Source/WebKit:

* UIProcess/API/Cocoa/WKWebView.mm:
(createUserInfo):
(-[WKWebView _startTextManipulationsWithConfiguration:completion:]):
(-[WKWebView _completeTextManipulation:completion:]):
(-[WKWebView _completeTextManipulationForItems:completion:]):
* UIProcess/API/Cocoa/_WKTextManipulationToken.h:
* UIProcess/API/Cocoa/_WKTextManipulationToken.mm:
(-[_WKTextManipulationToken setUserInfo:]):
(-[_WKTextManipulationToken userInfo]):

Add a new `userInfo` dictionary to `_WKTextManipulationToken`, which contains several named
`NSString` keys.

(-[_WKTextManipulationToken isEqualToTextManipulationToken:includingContentEquality:]):
(-[_WKTextManipulationToken _descriptionPreservingPrivacy:]):

Tools:

Add a new API test to check the userInfo dictionary on text manipulation tokens.

* TestWebKitAPI/Tests/WebKitCocoa/TextManipulation.mm:

Modified Paths

Diff

Modified: trunk/Source/WebCore/ChangeLog (260864 => 260865)


--- trunk/Source/WebCore/ChangeLog	2020-04-29 04:26:12 UTC (rev 260864)
+++ trunk/Source/WebCore/ChangeLog	2020-04-29 05:16:23 UTC (rev 260865)
@@ -1,3 +1,31 @@
+2020-04-28  Wenson Hsieh  <[email protected]>
+
+        [Text manipulation] Add a userInfo dictionary to _WKTextManipulationToken
+        https://bugs.webkit.org/show_bug.cgi?id=211151
+        <rdar://problem/62329534>
+
+        Reviewed by Darin Adler.
+
+        Add an extensible mechanism for the text manipulation controller to send additional
+        metadata for each text manipulation token through to the WebKit client, for debugging
+        purposes.
+
+        Test: TextManipulation.StartTextManipulationExtractsUserInfo
+
+        * editing/TextManipulationController.cpp:
+        (WebCore::tokenInfo):
+        (WebCore::TextManipulationController::observeParagraphs):
+        * editing/TextManipulationController.h:
+
+        Add TextManipulationTokenInfo, and add an optional TextManipulationTokenInfo member to
+        TextManipulationToken. For now, just send over the document URL, element tag name, and
+        the value of the role attribute.
+
+        (WebCore::TextManipulationController::ManipulationTokenInfo::encode const):
+        (WebCore::TextManipulationController::ManipulationTokenInfo::decode):
+        (WebCore::TextManipulationController::ManipulationToken::encode const):
+        (WebCore::TextManipulationController::ManipulationToken::decode):
+
 2020-04-28  Simon Fraser  <[email protected]>
 
         Update the xcfilelists.

Modified: trunk/Source/WebCore/editing/TextManipulationController.cpp (260864 => 260865)


--- trunk/Source/WebCore/editing/TextManipulationController.cpp	2020-04-29 04:26:12 UTC (rev 260864)
+++ trunk/Source/WebCore/editing/TextManipulationController.cpp	2020-04-29 05:16:23 UTC (rev 260865)
@@ -258,6 +258,21 @@
     return true;
 }
 
+static Optional<TextManipulationController::ManipulationTokenInfo> tokenInfo(Node* node)
+{
+    if (!node)
+        return WTF::nullopt;
+
+    TextManipulationController::ManipulationTokenInfo result;
+    result.documentURL = node->document().url();
+    if (auto element = is<Element>(node) ? makeRefPtr(downcast<Element>(*node)) : makeRefPtr(node->parentElement())) {
+        result.tagName = element->tagName();
+        if (element->hasAttributeWithoutSynchronization(HTMLNames::roleAttr))
+            result.roleAttribute = element->attributeWithoutSynchronization(HTMLNames::roleAttr);
+    }
+    return result;
+}
+
 void TextManipulationController::observeParagraphs(const Position& start, const Position& end)
 {
     if (start.isNull() || end.isNull())
@@ -321,13 +336,13 @@
                 auto& currentElement = downcast<Element>(*content.node);
                 if (!content.isTextContent && canPerformTextManipulationByReplacingEntireTextContent(currentElement)) {
                     addItem(ManipulationItemData { Position(), Position(), makeWeakPtr(currentElement), nullQName(),
-                        { ManipulationToken { m_tokenIdentifier.generate(), currentElement.textContent() } } });
+                        { ManipulationToken { m_tokenIdentifier.generate(), currentElement.textContent(), tokenInfo(&currentElement) } } });
                 }
                 if (currentElement.hasAttributes()) {
                     for (auto& attribute : currentElement.attributesIterator()) {
                         if (isAttributeForTextManipulation(attribute.name())) {
                             addItem(ManipulationItemData { Position(), Position(), makeWeakPtr(currentElement), attribute.name(),
-                                { ManipulationToken { m_tokenIdentifier.generate(), attribute.value() } } });
+                                { ManipulationToken { m_tokenIdentifier.generate(), attribute.value(), tokenInfo(&currentElement) } } });
                         }
                     }
                 }
@@ -346,7 +361,7 @@
                 continue;
 
             endOfCurrentParagraph = currentEndOfCurrentParagraph;
-            tokensInCurrentParagraph.append(ManipulationToken { m_tokenIdentifier.generate(), "[]", true });
+            tokensInCurrentParagraph.append(ManipulationToken { m_tokenIdentifier.generate(), "[]", tokenInfo(content.node.get()), true });
 
             continue;
         }
@@ -365,7 +380,7 @@
                 if (tokensInCurrentParagraph.isEmpty())
                     startOfCurrentParagraph = Position(&textNode, startOfCurrentLine);
 
-                tokensInCurrentParagraph.append(ManipulationToken { m_tokenIdentifier.generate(), stringUntilEndOfLine, exclusionRuleMatcher.isExcluded(content.node.get()) });
+                tokensInCurrentParagraph.append(ManipulationToken { m_tokenIdentifier.generate(), stringUntilEndOfLine, tokenInfo(&textNode), exclusionRuleMatcher.isExcluded(content.node.get()) });
             }
 
             if (!tokensInCurrentParagraph.isEmpty()) {
@@ -385,7 +400,7 @@
                     startOfCurrentParagraph = iterator.startPosition();
             }
             endOfCurrentParagraph = iterator.endPosition();
-            tokensInCurrentParagraph.append(ManipulationToken { m_tokenIdentifier.generate(), remainingText.toString(), exclusionRuleMatcher.isExcluded(content.node.get()) });
+            tokensInCurrentParagraph.append(ManipulationToken { m_tokenIdentifier.generate(), remainingText.toString(), tokenInfo(content.node.get()), exclusionRuleMatcher.isExcluded(content.node.get()) });
         }
     }
 

Modified: trunk/Source/WebCore/editing/TextManipulationController.h (260864 => 260865)


--- trunk/Source/WebCore/editing/TextManipulationController.h	2020-04-29 04:26:12 UTC (rev 260864)
+++ trunk/Source/WebCore/editing/TextManipulationController.h	2020-04-29 05:16:23 UTC (rev 260865)
@@ -48,9 +48,19 @@
     enum TokenIdentifierType { };
     using TokenIdentifier = ObjectIdentifier<TokenIdentifierType>;
 
+    struct ManipulationTokenInfo {
+        String tagName;
+        String roleAttribute;
+        URL documentURL;
+
+        template<class Encoder> void encode(Encoder&) const;
+        template<class Decoder> static Optional<ManipulationTokenInfo> decode(Decoder&);
+    };
+
     struct ManipulationToken {
         TokenIdentifier identifier;
         String content;
+        Optional<ManipulationTokenInfo> info;
         bool isExcluded { false };
 
         template<class Encoder> void encode(Encoder&) const;
@@ -158,9 +168,33 @@
 };
 
 template<class Encoder>
+void TextManipulationController::ManipulationTokenInfo::encode(Encoder& encoder) const
+{
+    encoder << tagName;
+    encoder << roleAttribute;
+    encoder << documentURL;
+}
+
+template<class Decoder>
+Optional<TextManipulationController::ManipulationTokenInfo> TextManipulationController::ManipulationTokenInfo::decode(Decoder& decoder)
+{
+    ManipulationTokenInfo result;
+    if (!decoder.decode(result.tagName))
+        return WTF::nullopt;
+
+    if (!decoder.decode(result.roleAttribute))
+        return WTF::nullopt;
+
+    if (!decoder.decode(result.documentURL))
+        return WTF::nullopt;
+
+    return result;
+}
+
+template<class Encoder>
 void TextManipulationController::ManipulationToken::encode(Encoder& encoder) const
 {
-    encoder << identifier << content << isExcluded;
+    encoder << identifier << content << info << isExcluded;
 }
 
 template<class Decoder>
@@ -171,6 +205,8 @@
         return WTF::nullopt;
     if (!decoder.decode(result.content))
         return WTF::nullopt;
+    if (!decoder.decode(result.info))
+        return WTF::nullopt;
     if (!decoder.decode(result.isExcluded))
         return WTF::nullopt;
     return result;

Modified: trunk/Source/WebKit/ChangeLog (260864 => 260865)


--- trunk/Source/WebKit/ChangeLog	2020-04-29 04:26:12 UTC (rev 260864)
+++ trunk/Source/WebKit/ChangeLog	2020-04-29 05:16:23 UTC (rev 260865)
@@ -1,3 +1,27 @@
+2020-04-28  Wenson Hsieh  <[email protected]>
+
+        [Text manipulation] Add a userInfo dictionary to _WKTextManipulationToken
+        https://bugs.webkit.org/show_bug.cgi?id=211151
+        <rdar://problem/62329534>
+
+        Reviewed by Darin Adler.
+
+        * UIProcess/API/Cocoa/WKWebView.mm:
+        (createUserInfo):
+        (-[WKWebView _startTextManipulationsWithConfiguration:completion:]):
+        (-[WKWebView _completeTextManipulation:completion:]):
+        (-[WKWebView _completeTextManipulationForItems:completion:]):
+        * UIProcess/API/Cocoa/_WKTextManipulationToken.h:
+        * UIProcess/API/Cocoa/_WKTextManipulationToken.mm:
+        (-[_WKTextManipulationToken setUserInfo:]):
+        (-[_WKTextManipulationToken userInfo]):
+
+        Add a new `userInfo` dictionary to `_WKTextManipulationToken`, which contains several named
+        `NSString` keys.
+
+        (-[_WKTextManipulationToken isEqualToTextManipulationToken:includingContentEquality:]):
+        (-[_WKTextManipulationToken _descriptionPreservingPrivacy:]):
+
 2020-04-28  David Kilzer  <[email protected]>
 
         REGRESSION (r260407): Over-release of NSGraphicsContext in WebKit::convertPlatformImageToBitmap()

Modified: trunk/Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm (260864 => 260865)


--- trunk/Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm	2020-04-29 04:26:12 UTC (rev 260864)
+++ trunk/Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm	2020-04-29 05:16:23 UTC (rev 260865)
@@ -1606,6 +1606,22 @@
     _textManipulationDelegate = delegate;
 }
 
+static RetainPtr<NSDictionary<NSString *, id>> createUserInfo(const Optional<WebCore::TextManipulationController::ManipulationTokenInfo>& info)
+{
+    if (!info)
+        return { };
+
+    auto result = adoptNS([[NSMutableDictionary alloc] initWithCapacity:3]);
+    if (!info->documentURL.isNull())
+        [result setObject:(NSURL *)info->documentURL forKey:_WKTextManipulationTokenUserInfoDocumentURLKey];
+    if (!info->tagName.isNull())
+        [result setObject:(NSString *)info->tagName forKey:_WKTextManipulationTokenUserInfoTagNameKey];
+    if (!info->roleAttribute.isNull())
+        [result setObject:(NSString *)info->roleAttribute forKey:_WKTextManipulationTokenUserInfoRoleAttributeKey];
+
+    return result;
+}
+
 - (void)_startTextManipulationsWithConfiguration:(_WKTextManipulationConfiguration *)configuration completion:(void(^)())completionHandler
 {
     using ExclusionRule = WebCore::TextManipulationController::ExclusionRule;
@@ -1643,6 +1659,7 @@
                 [wkToken setIdentifier:String::number(token.identifier.toUInt64())];
                 [wkToken setContent:token.content];
                 [wkToken setExcluded:token.isExcluded];
+                [wkToken setUserInfo:createUserInfo(token.info).get()];
                 return wkToken;
             });
             return adoptNS([[_WKTextManipulationItem alloc] initWithIdentifier:String::number(item.identifier.toUInt64()) tokens:tokens.get()]);
@@ -1680,7 +1697,7 @@
 
     Vector<WebCore::TextManipulationController::ManipulationToken> tokens;
     for (_WKTextManipulationToken *wkToken in item.tokens)
-        tokens.append(WebCore::TextManipulationController::ManipulationToken { coreTextManipulationTokenIdentifierFromString(wkToken.identifier), wkToken.content });
+        tokens.append(WebCore::TextManipulationController::ManipulationToken { coreTextManipulationTokenIdentifierFromString(wkToken.identifier), wkToken.content, WTF::nullopt });
 
     Vector<WebCore::TextManipulationController::ManipulationItem> coreItems;
     coreItems.reserveInitialCapacity(1);
@@ -1739,7 +1756,7 @@
         Vector<WebCore::TextManipulationController::ManipulationToken> coreTokens;
         coreTokens.reserveInitialCapacity(wkItem.tokens.count);
         for (_WKTextManipulationToken *wkToken in wkItem.tokens)
-            coreTokens.uncheckedAppend(WebCore::TextManipulationController::ManipulationToken { coreTextManipulationTokenIdentifierFromString(wkToken.identifier), wkToken.content });
+            coreTokens.uncheckedAppend(WebCore::TextManipulationController::ManipulationToken { coreTextManipulationTokenIdentifierFromString(wkToken.identifier), wkToken.content, WTF::nullopt });
         coreItems.uncheckedAppend(WebCore::TextManipulationController::ManipulationItem { coreTextManipulationItemIdentifierFromString(wkItem.identifier), WTFMove(coreTokens) });
     }
 

Modified: trunk/Source/WebKit/UIProcess/API/Cocoa/_WKTextManipulationToken.h (260864 => 260865)


--- trunk/Source/WebKit/UIProcess/API/Cocoa/_WKTextManipulationToken.h	2020-04-29 04:26:12 UTC (rev 260864)
+++ trunk/Source/WebKit/UIProcess/API/Cocoa/_WKTextManipulationToken.h	2020-04-29 05:16:23 UTC (rev 260865)
@@ -28,6 +28,10 @@
 
 NS_ASSUME_NONNULL_BEGIN
 
+WK_EXTERN NSString * const _WKTextManipulationTokenUserInfoDocumentURLKey WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA));
+WK_EXTERN NSString * const _WKTextManipulationTokenUserInfoTagNameKey WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA));
+WK_EXTERN NSString * const _WKTextManipulationTokenUserInfoRoleAttributeKey WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA));
+
 WK_CLASS_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA))
 @interface _WKTextManipulationToken : NSObject
 
@@ -38,6 +42,8 @@
 - (BOOL)isEqualToTextManipulationToken:(nullable _WKTextManipulationToken *)otherToken includingContentEquality:(BOOL)includingContentEquality;
 @property (nonatomic, copy, readonly) NSString *debugDescription;
 
+@property (nonatomic, nullable, copy) NSDictionary<NSString *, id> *userInfo;
+
 @end
 
 NS_ASSUME_NONNULL_END

Modified: trunk/Source/WebKit/UIProcess/API/Cocoa/_WKTextManipulationToken.mm (260864 => 260865)


--- trunk/Source/WebKit/UIProcess/API/Cocoa/_WKTextManipulationToken.mm	2020-04-29 04:26:12 UTC (rev 260864)
+++ trunk/Source/WebKit/UIProcess/API/Cocoa/_WKTextManipulationToken.mm	2020-04-29 05:16:23 UTC (rev 260865)
@@ -26,8 +26,16 @@
 #import "config.h"
 #import "_WKTextManipulationToken.h"
 
-@implementation _WKTextManipulationToken
+#import <wtf/RetainPtr.h>
 
+NSString * const _WKTextManipulationTokenUserInfoDocumentURLKey = @"_WKTextManipulationTokenUserInfoDocumentURLKey";
+NSString * const _WKTextManipulationTokenUserInfoTagNameKey = @"_WKTextManipulationTokenUserInfoTagNameKey";
+NSString * const _WKTextManipulationTokenUserInfoRoleAttributeKey = @"_WKTextManipulationTokenUserInfoRoleAttributeKey";
+
+@implementation _WKTextManipulationToken {
+    RetainPtr<NSDictionary<NSString *, id>> _userInfo;
+}
+
 - (void)dealloc
 {
     [_identifier release];
@@ -38,6 +46,19 @@
     [super dealloc];
 }
 
+- (void)setUserInfo:(NSDictionary<NSString *, id> *)userInfo
+{
+    if (userInfo == _userInfo || [_userInfo isEqual:userInfo])
+        return;
+
+    _userInfo = adoptNS(userInfo.copy);
+}
+
+- (NSDictionary<NSString *, id> *)userInfo
+{
+    return _userInfo.get();
+}
+
 static BOOL isEqualOrBothNil(id a, id b)
 {
     if (a == b)
@@ -65,8 +86,9 @@
     BOOL equalIdentifiers = isEqualOrBothNil(self.identifier, otherToken.identifier);
     BOOL equalExclusion = self.isExcluded == otherToken.isExcluded;
     BOOL equalContent = !includingContentEquality || isEqualOrBothNil(self.content, otherToken.content);
+    BOOL equalUserInfo = isEqualOrBothNil(self.userInfo, otherToken.userInfo);
 
-    return equalIdentifiers && equalExclusion && equalContent;
+    return equalIdentifiers && equalExclusion && equalContent && equalUserInfo;
 }
 
 - (NSString *)description
@@ -85,7 +107,7 @@
     if (preservePrivacy)
         [description appendFormat:@"; content length = %lu", (unsigned long)self.content.length];
     else
-        [description appendFormat:@"; content = %@", self.content];
+        [description appendFormat:@"; content = %@; user info = %@", self.content, self.userInfo];
 
     [description appendString:@">"];
 

Modified: trunk/Tools/ChangeLog (260864 => 260865)


--- trunk/Tools/ChangeLog	2020-04-29 04:26:12 UTC (rev 260864)
+++ trunk/Tools/ChangeLog	2020-04-29 05:16:23 UTC (rev 260865)
@@ -1,3 +1,15 @@
+2020-04-28  Wenson Hsieh  <[email protected]>
+
+        [Text manipulation] Add a userInfo dictionary to _WKTextManipulationToken
+        https://bugs.webkit.org/show_bug.cgi?id=211151
+        <rdar://problem/62329534>
+
+        Reviewed by Darin Adler.
+
+        Add a new API test to check the userInfo dictionary on text manipulation tokens.
+
+        * TestWebKitAPI/Tests/WebKitCocoa/TextManipulation.mm:
+
 2020-04-28  David Kilzer  <[email protected]>
 
         check-webkit-style should suggest CheckedSize for Checked<size_t, RecordOverflow>

Modified: trunk/Tools/TestWebKitAPI/Tests/WebKitCocoa/TextManipulation.mm (260864 => 260865)


--- trunk/Tools/TestWebKitAPI/Tests/WebKitCocoa/TextManipulation.mm	2020-04-29 04:26:12 UTC (rev 260864)
+++ trunk/Tools/TestWebKitAPI/Tests/WebKitCocoa/TextManipulation.mm	2020-04-29 05:16:23 UTC (rev 260865)
@@ -704,6 +704,59 @@
     EXPECT_WK_STREQ("Garply", items[3].tokens[0].content);
 }
 
+TEST(TextManipulation, StartTextManipulationExtractsUserInfo)
+{
+    auto delegate = adoptNS([[TextManipulationDelegate alloc] init]);
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 400, 400)]);
+    [webView _setTextManipulationDelegate:delegate.get()];
+
+    [webView synchronouslyLoadHTMLString:@"<!DOCTYPE html>"
+        "<body>"
+        "    <title>This is a test</title>"
+        "    <p>First</p>"
+        "    <div role='button'>Second</div>"
+        "    <span>Third</span>"
+        "</body>"];
+
+    done = false;
+    [webView _startTextManipulationsWithConfiguration:nil completion:^{
+        done = true;
+    }];
+    TestWebKitAPI::Util::run(&done);
+
+    auto items = [delegate items];
+    EXPECT_EQ(items.count, 4UL);
+    EXPECT_EQ(items[0].tokens.count, 1UL);
+    EXPECT_EQ(items[1].tokens.count, 1UL);
+    EXPECT_EQ(items[2].tokens.count, 1UL);
+    EXPECT_EQ(items[3].tokens.count, 1UL);
+    EXPECT_WK_STREQ("This is a test", items[0].tokens[0].content);
+    EXPECT_WK_STREQ("First", items[1].tokens[0].content);
+    EXPECT_WK_STREQ("Second", items[2].tokens[0].content);
+    EXPECT_WK_STREQ("Third", items[3].tokens[0].content);
+    {
+        auto userInfo = items[0].tokens[0].userInfo;
+        EXPECT_WK_STREQ("TestWebKitAPI.resources", [(NSURL *)userInfo[_WKTextManipulationTokenUserInfoDocumentURLKey] lastPathComponent]);
+        EXPECT_WK_STREQ("TITLE", (NSString *)userInfo[_WKTextManipulationTokenUserInfoTagNameKey]);
+    }
+    {
+        auto userInfo = items[1].tokens[0].userInfo;
+        EXPECT_WK_STREQ("TestWebKitAPI.resources", [(NSURL *)userInfo[_WKTextManipulationTokenUserInfoDocumentURLKey] lastPathComponent]);
+        EXPECT_WK_STREQ("P", (NSString *)userInfo[_WKTextManipulationTokenUserInfoTagNameKey]);
+    }
+    {
+        auto userInfo = items[2].tokens[0].userInfo;
+        EXPECT_WK_STREQ("TestWebKitAPI.resources", [(NSURL *)userInfo[_WKTextManipulationTokenUserInfoDocumentURLKey] lastPathComponent]);
+        EXPECT_WK_STREQ("DIV", (NSString *)userInfo[_WKTextManipulationTokenUserInfoTagNameKey]);
+        EXPECT_WK_STREQ("button", (NSString *)userInfo[_WKTextManipulationTokenUserInfoRoleAttributeKey]);
+    }
+    {
+        auto userInfo = items[3].tokens[0].userInfo;
+        EXPECT_WK_STREQ("TestWebKitAPI.resources", [(NSURL *)userInfo[_WKTextManipulationTokenUserInfoDocumentURLKey] lastPathComponent]);
+        EXPECT_WK_STREQ("SPAN", (NSString *)userInfo[_WKTextManipulationTokenUserInfoTagNameKey]);
+    }
+}
+
 struct Token {
     NSString *identifier;
     NSString *content;
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to