This is an automated email from the ASF dual-hosted git repository.

ntimofeev pushed a commit to branch STABLE-4.1
in repository https://gitbox.apache.org/repos/asf/cayenne.git

commit 8aaba91f13c4da5984845d0db4041cc28b7283a7
Author: keyris <keyris.xd.s...@gmail.com>
AuthorDate: Tue May 18 12:16:04 2021 +0400

    CAY-2808 Modeler 4.1.1 can't save on Windows
    
    (cherry picked from commit 847ada1ebe897deddfe54a1ee2c4490d953ce028)
---
 .../org/apache/cayenne/gen/CgenConfiguration.java  |  60 ++---
 .../apache/cayenne/gen/xml/CgenSaverDelegate.java  |   8 +-
 .../apache/cayenne/gen/CgenConfigurationTest.java  | 248 +++++++++++++++++++++
 .../editor/cgen/CodeGeneratorController.java       |  24 +-
 .../modeler/editor/cgen/GeneratorController.java   |   5 +-
 .../editor/cgen/GeneratorControllerPanel.java      |  10 +-
 6 files changed, 317 insertions(+), 38 deletions(-)

diff --git 
a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/CgenConfiguration.java 
b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/CgenConfiguration.java
index cb9fc2a04..41df105ec 100644
--- a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/CgenConfiguration.java
+++ b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/CgenConfiguration.java
@@ -36,12 +36,14 @@ import org.apache.cayenne.map.Embeddable;
 import org.apache.cayenne.map.ObjEntity;
 import org.apache.cayenne.util.XMLEncoder;
 import org.apache.cayenne.util.XMLSerializable;
+import org.apache.cayenne.validation.ValidationException;
 
 /**
  * Used to keep config of class generation action.
  * Previously was the part of ClassGeneretionAction class.
  * Now CgenConfiguration is saved in dataMap file.
  * You can reuse it in next cgen actions.
+ *
  * @since 4.1
  */
 public class CgenConfiguration implements Serializable, XMLSerializable {
@@ -97,7 +99,7 @@ public class CgenConfiguration implements Serializable, 
XMLSerializable {
 
         this.client = client;
 
-        if(!client) {
+        if (!client) {
             this.template = ClassGenerationAction.SUBCLASS_TEMPLATE;
             this.superTemplate = ClassGenerationAction.SUPERCLASS_TEMPLATE;
             this.queryTemplate = 
ClassGenerationAction.DATAMAP_SUBCLASS_TEMPLATE;
@@ -112,7 +114,7 @@ public class CgenConfiguration implements Serializable, 
XMLSerializable {
         this.embeddableSuperTemplate = 
ClassGenerationAction.EMBEDDABLE_SUPERCLASS_TEMPLATE;
     }
 
-    public void resetCollections(){
+    public void resetCollections() {
         embeddableArtifacts.clear();
         entityArtifacts.clear();
     }
@@ -141,7 +143,7 @@ public class CgenConfiguration implements Serializable, 
XMLSerializable {
         }
     }
 
-    public String getArtifactsGenerationMode(){
+    public String getArtifactsGenerationMode() {
         return artifactsGenerationMode.getLabel();
     }
 
@@ -168,12 +170,20 @@ public class CgenConfiguration implements Serializable, 
XMLSerializable {
 
     public void setRelPath(String pathStr) {
         Path path = Paths.get(pathStr);
-        if(path.isAbsolute() && rootPath != null) {
-            this.relPath = rootPath.relativize(path);
-        } else {
-            this.relPath = path;
+
+        if (rootPath != null) {
+
+            if (!rootPath.isAbsolute()) {
+                throw new ValidationException("Root path : " + '"' + 
rootPath.toString() + '"' + "should be absolute");
+            }
+
+            if (path.isAbsolute() && 
rootPath.getRoot().equals(path.getRoot())) {
+                this.relPath = rootPath.relativize(path);
+                return;
+            }
         }
-       }
+        this.relPath = path;
+    }
 
     public boolean isOverwrite() {
         return overwrite;
@@ -292,7 +302,7 @@ public class CgenConfiguration implements Serializable, 
XMLSerializable {
     }
 
     public String buildRelPath() {
-        if(relPath == null || relPath.toString().isEmpty()) {
+        if (relPath == null || relPath.toString().isEmpty()) {
             return ".";
         }
         return relPath.toString();
@@ -323,11 +333,11 @@ public class CgenConfiguration implements Serializable, 
XMLSerializable {
     }
 
     public Path buildPath() {
-               return rootPath != null ? relPath != null ? 
rootPath.resolve(relPath).toAbsolutePath().normalize() : rootPath : relPath;
-       }
+        return rootPath != null ? relPath != null ? 
rootPath.resolve(relPath).toAbsolutePath().normalize() : rootPath : relPath;
+    }
 
     public void loadEntity(ObjEntity entity) {
-        if(!entity.isGeneric()) {
+        if (!entity.isGeneric()) {
             entityArtifacts.add(entity.getName());
         }
     }
@@ -362,23 +372,23 @@ public class CgenConfiguration implements Serializable, 
XMLSerializable {
         return String.join(",", excludeEmbeddable);
     }
 
-       public void resolveExcludeEntities() {
-               entityArtifacts = dataMap.getObjEntities()
-                               .stream()
-                               .filter(entity -> 
!excludeEntityArtifacts.contains(entity.getName()))
+    public void resolveExcludeEntities() {
+        entityArtifacts = dataMap.getObjEntities()
+                .stream()
+                .filter(entity -> 
!excludeEntityArtifacts.contains(entity.getName()))
                                .map(ObjEntity::getName)
-                               .collect(Collectors.toSet());
-       }
+                .collect(Collectors.toSet());
+    }
 
-       public void resolveExcludeEmbeddables() {
-       embeddableArtifacts = dataMap.getEmbeddables()
-                               .stream()
-                               .filter(embeddable -> 
!excludeEmbeddableArtifacts.contains(embeddable.getClassName()))
+    public void resolveExcludeEmbeddables() {
+        embeddableArtifacts = dataMap.getEmbeddables()
+                .stream()
+                .filter(embeddable -> 
!excludeEmbeddableArtifacts.contains(embeddable.getClassName()))
                                .map(Embeddable::getClassName)
-                               .collect(Collectors.toSet());
-       }
+                .collect(Collectors.toSet());
+    }
 
-       public Collection<String> getExcludeEntityArtifacts() {
+    public Collection<String> getExcludeEntityArtifacts() {
         return excludeEntityArtifacts;
     }
 
diff --git 
a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/xml/CgenSaverDelegate.java 
b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/xml/CgenSaverDelegate.java
index ea94c7241..2724cb8c6 100644
--- 
a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/xml/CgenSaverDelegate.java
+++ 
b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/xml/CgenSaverDelegate.java
@@ -72,9 +72,15 @@ public class CgenSaverDelegate extends BaseSaverDelegate {
         Path prevPath = cgenConfiguration.buildPath();
         if(prevPath != null) {
             if(prevPath.isAbsolute()) {
-                Path relPath = resourcePath.relativize(prevPath).normalize();
+
+                Path relPath = prevPath;
+
+                if (resourcePath.getRoot().equals(prevPath.getRoot())) {
+                    relPath = resourcePath.relativize(prevPath).normalize();
+                }
                 cgenConfiguration.setRelPath(relPath);
             }
+
             Path templatePath = Paths.get(cgenConfiguration.getTemplate());
             if(templatePath.isAbsolute()) {
                 
cgenConfiguration.setTemplate(resourcePath.relativize(templatePath).normalize().toString());
diff --git 
a/cayenne-cgen/src/test/java/org/apache/cayenne/gen/CgenConfigurationTest.java 
b/cayenne-cgen/src/test/java/org/apache/cayenne/gen/CgenConfigurationTest.java
new file mode 100644
index 000000000..fd5572cbf
--- /dev/null
+++ 
b/cayenne-cgen/src/test/java/org/apache/cayenne/gen/CgenConfigurationTest.java
@@ -0,0 +1,248 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.gen;
+
+import org.apache.cayenne.validation.ValidationException;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.runners.Enclosed;
+import org.junit.runner.RunWith;
+
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Locale;
+
+import static org.junit.Assert.assertEquals;
+
+@RunWith(Enclosed.class)
+public class CgenConfigurationTest {
+
+    public static class СgenWindowsConfigurationTest {
+
+        CgenConfiguration configuration;
+
+        @Before
+        public void setUp() {
+            configuration = new CgenConfiguration(false);
+        }
+
+        @Before
+        public void checkPlatform() {
+            
Assume.assumeTrue(System.getProperty("os.name").toLowerCase(Locale.ROOT).startsWith("win"));
+        }
+
+        @Test
+        public void equalRootsEqualDirectories() {
+            configuration.setRootPath(Paths.get("C:\\test1\\test2\\test3"));
+            Path relPath = Paths.get("C:\\test1\\test2\\test3");
+            configuration.setRelPath(relPath.toString());
+
+            assertEquals(Paths.get(""), configuration.getRelPath());
+            assertEquals(relPath, configuration.buildPath());
+        }
+
+        @Test
+        public void equalRootsNotEqualDirectories() {
+            configuration.setRootPath(Paths.get("C:\\test1\\test2\\test3"));
+            Path relPath = Paths.get("C:\\test1\\test2\\testAnother");
+            configuration.setRelPath(relPath.toString());
+
+            assertEquals(Paths.get("..\\testAnother"), 
configuration.getRelPath());
+            assertEquals(relPath, configuration.buildPath());
+        }
+
+        @Test
+        public void equalRootsEmptyDirectories() {
+            configuration.setRootPath(Paths.get("C:\\"));
+            Path relPath = Paths.get("C:\\");
+            configuration.setRelPath(relPath.toString());
+
+            assertEquals(Paths.get(""), configuration.getRelPath());
+            assertEquals(relPath, configuration.buildPath());
+        }
+
+        @Test
+        public void notEqualRootsEqualDirectories() {
+            configuration.setRootPath(Paths.get("C:\\test1\\test2\\test3"));
+            Path relPath = Paths.get("E:\\test1\\test2\\test3");
+            configuration.setRelPath(relPath.toString());
+
+            assertEquals(Paths.get("E:\\test1\\test2\\test3"), 
configuration.getRelPath());
+            assertEquals(relPath, configuration.buildPath());
+        }
+
+        @Test
+        public void notEqualRootsNotEqualDirectories() {
+            configuration.setRootPath(Paths.get("C:\\test1\\test2\\test3"));
+            Path relPath = Paths.get("E:\\test1\\test2\\testAnother");
+            configuration.setRelPath(relPath.toString());
+
+            assertEquals(Paths.get("E:\\test1\\test2\\testAnother"), 
configuration.getRelPath());
+            assertEquals(relPath, configuration.buildPath());
+        }
+
+        @Test
+        public void notEqualRootsEmptyDirectories() {
+            configuration.setRootPath(Paths.get("C:\\"));
+            Path relPath = Paths.get("E:\\");
+            configuration.setRelPath(relPath.toString());
+
+            assertEquals(Paths.get("E:\\"), configuration.getRelPath());
+            assertEquals(relPath, configuration.buildPath());
+        }
+
+        @Test(expected = ValidationException.class)
+        public void emptyRootNotEmptyRelPath() {
+            configuration.setRootPath(Paths.get(""));
+            Path relPath = Paths.get("E:\\");
+            configuration.setRelPath(relPath.toString());
+
+            assertEquals(Paths.get("E:\\"), configuration.getRelPath());
+            assertEquals(relPath, configuration.buildPath());
+        }
+
+        @Test
+        public void notEmptyRootEmptyRelPath() {
+            configuration.setRootPath(Paths.get("E:\\"));
+            Path relPath = Paths.get("");
+
+            configuration.setRelPath(relPath);
+
+            assertEquals(relPath, configuration.getRelPath());
+            assertEquals(Paths.get("E:\\"), configuration.buildPath());
+        }
+
+        @Test(expected = InvalidPathException.class)
+        public void invalidRelPath() {
+            configuration.setRootPath(Paths.get("C:\\test1\\test2\\test3"));
+            configuration.setRelPath("invalidRoot:\\test");
+        }
+
+        @Test(expected = InvalidPathException.class)
+        public void invalidRootPath() {
+            configuration.setRootPath(Paths.get("invalidRoot:\\test"));
+            configuration.setRelPath("C:\\test1\\test2\\test3");
+        }
+
+        @Test
+        public void nullRootPath() {
+            configuration.setRelPath("C:\\test1\\test2\\test3");
+            assertEquals(Paths.get("C:\\test1\\test2\\test3"), 
configuration.getRelPath());
+            assertEquals(Paths.get("C:\\test1\\test2\\test3"), 
configuration.buildPath());
+        }
+    }
+
+    public static class СgenUnixConfigurationTest {
+
+        CgenConfiguration configuration;
+
+        @Before
+        public void setUp() {
+            configuration = new CgenConfiguration(false);
+        }
+
+        @Before
+        public void checkPlatform() {
+            
Assume.assumeFalse(System.getProperty("os.name").toLowerCase(Locale.ROOT).startsWith("win"));
+        }
+
+        @Test
+        public void equalRootsEqualDirectories() {
+            configuration.setRootPath(Paths.get("/test1/test2/test3"));
+            Path relPath = Paths.get("/test1/test2/test3");
+            configuration.setRelPath(relPath.toString());
+
+
+            assertEquals(Paths.get(""), configuration.getRelPath());
+            assertEquals(relPath, configuration.buildPath());
+        }
+
+        @Test
+        public void equalRootsNotEqualDirectories() {
+            configuration.setRootPath(Paths.get("/test1/test2/test3"));
+            Path relPath = Paths.get("/test1/test2/testAnother");
+            configuration.setRelPath(relPath.toString());
+
+            assertEquals(Paths.get("../testAnother"), 
configuration.getRelPath());
+            assertEquals(relPath, configuration.buildPath());
+        }
+
+        @Test
+        public void equalRootsEmptyDirectories() {
+            configuration.setRootPath(Paths.get("/"));
+            Path relPath = Paths.get("/");
+            configuration.setRelPath(relPath.toString());
+
+            assertEquals(Paths.get(""), configuration.getRelPath());
+            assertEquals(relPath, configuration.buildPath());
+        }
+
+        @Test
+        public void concatCorrectRootPathAndRelPath() {
+            configuration.setRootPath(Paths.get("/test1/test2/test3"));
+            Path relPath = Paths.get("test1/test2/test3");
+            configuration.setRelPath(relPath.toString());
+
+            assertEquals(Paths.get("test1/test2/test3"), 
configuration.getRelPath());
+            assertEquals(Paths.get("/test1/test2/test3/test1/test2/test3"), 
configuration.buildPath());
+        }
+
+        @Test(expected = ValidationException.class)
+        public void emptyRootNotEmptyRelPath() {
+            configuration.setRootPath(Paths.get(""));
+            Path relPath = Paths.get("/");
+            configuration.setRelPath(relPath.toString());
+
+            assertEquals(Paths.get("/"), configuration.getRelPath());
+            assertEquals(relPath, configuration.buildPath());
+        }
+
+        @Test
+        public void notEmptyRootEmptyRelPath() {
+            configuration.setRootPath(Paths.get("/"));
+            configuration.setRelPath("");
+
+            assertEquals(Paths.get(""), configuration.getRelPath());
+            assertEquals(Paths.get("/"), configuration.buildPath());
+        }
+
+        @Test(expected = ValidationException.class)
+        public void invalidRootPath() {
+            configuration.setRootPath(Paths.get("invalidRoot:/test"));
+            configuration.setRelPath("/test1/test2/test3");
+        }
+
+        @Test(expected = ValidationException.class)
+        public void concatInvalidRootPathAndRelPath() {
+            configuration.setRootPath(Paths.get("invalidRoot:/test"));
+            configuration.setRelPath("test1/test2/test3");
+        }
+
+        @Test
+        public void nullRootPath() {
+            configuration.setRelPath("/test1/test2/test3");
+            assertEquals(Paths.get("/test1/test2/test3"), 
configuration.getRelPath());
+            assertEquals(Paths.get("/test1/test2/test3"), 
configuration.buildPath());
+        }
+    }
+
+}
\ No newline at end of file
diff --git 
a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/cgen/CodeGeneratorController.java
 
b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/cgen/CodeGeneratorController.java
index bd4f19084..3b8a3efa8 100644
--- 
a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/cgen/CodeGeneratorController.java
+++ 
b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/cgen/CodeGeneratorController.java
@@ -76,7 +76,7 @@ public class CodeGeneratorController extends 
CodeGeneratorControllerBase impleme
         CgenConfiguration cgenConfiguration = createConfiguration();
         GeneratorController modeController = 
prevGeneratorController.get(dataMap) != null
                 ? prevGeneratorController.get(dataMap)
-                : isDefaultConfig(cgenConfiguration)
+                : isDefaultConfig (cgenConfiguration)
                     ? cgenConfiguration.isClient()
                         ? generatorSelector.getClientGeneratorController()
                         : generatorSelector.getStandartController()
@@ -101,7 +101,7 @@ public class CodeGeneratorController extends 
CodeGeneratorControllerBase impleme
 
     }
 
-    private void initListeners(){
+    private void initListeners() {
         projectController.addObjEntityListener(this);
         projectController.addEmbeddableListener(this);
         projectController.addDataMapListener(this);
@@ -173,7 +173,8 @@ public class CodeGeneratorController extends 
CodeGeneratorControllerBase impleme
     }
 
     @Override
-    public void objEntityChanged(EntityEvent e) {}
+    public void objEntityChanged(EntityEvent e) {
+    }
 
     @Override
     public void objEntityAdded(EntityEvent e) {
@@ -185,13 +186,14 @@ public class CodeGeneratorController extends 
CodeGeneratorControllerBase impleme
         super.removeFromSelectedEntities((ObjEntity) e.getEntity());
         DataMap map = e.getEntity().getDataMap();
         CgenConfiguration cgenConfiguration = 
projectController.getApplication().getMetaData().get(map, 
CgenConfiguration.class);
-        if(cgenConfiguration != null) {
+        if (cgenConfiguration != null) {
             cgenConfiguration.getEntities().remove(e.getEntity().getName());
         }
     }
 
     @Override
-    public void embeddableChanged(EmbeddableEvent e, DataMap map) {}
+    public void embeddableChanged(EmbeddableEvent e, DataMap map) {
+    }
 
     @Override
     public void embeddableAdded(EmbeddableEvent e, DataMap map) {
@@ -202,16 +204,16 @@ public class CodeGeneratorController extends 
CodeGeneratorControllerBase impleme
     public void embeddableRemoved(EmbeddableEvent e, DataMap map) {
         super.removeFromSelectedEmbeddables(e.getEmbeddable());
         CgenConfiguration cgenConfiguration = 
projectController.getApplication().getMetaData().get(map, 
CgenConfiguration.class);
-        if(cgenConfiguration != null) {
+        if (cgenConfiguration != null) {
             
cgenConfiguration.getEmbeddables().remove(e.getEmbeddable().getClassName());
         }
     }
 
     @Override
     public void dataMapChanged(DataMapEvent e) {
-        if(e.getSource() instanceof DbImportController) {
+        if (e.getSource() instanceof DbImportController) {
             CgenConfiguration cgenConfiguration = getCurrentConfiguration();
-            if(cgenConfiguration != null) {
+            if (cgenConfiguration != null) {
                 for(ObjEntity objEntity : e.getDataMap().getObjEntities()) {
                     
if(!cgenConfiguration.getExcludeEntityArtifacts().contains(objEntity.getName()))
 {
                         addEntity(cgenConfiguration.getDataMap(), objEntity);
@@ -222,8 +224,10 @@ public class CodeGeneratorController extends 
CodeGeneratorControllerBase impleme
     }
 
     @Override
-    public void dataMapAdded(DataMapEvent e) {}
+    public void dataMapAdded(DataMapEvent e) {
+    }
 
     @Override
-    public void dataMapRemoved(DataMapEvent e) {}
+    public void dataMapRemoved(DataMapEvent e) {
+    }
 }
diff --git 
a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/cgen/GeneratorController.java
 
b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/cgen/GeneratorController.java
index ec782f379..ece1cdd04 100644
--- 
a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/cgen/GeneratorController.java
+++ 
b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/cgen/GeneratorController.java
@@ -73,7 +73,10 @@ public abstract class GeneratorController extends 
CayenneController {
 
     protected void initForm(CgenConfiguration cgenConfiguration) {
         this.cgenConfiguration = cgenConfiguration;
-        
((GeneratorControllerPanel)getView()).getOutputFolder().setText(cgenConfiguration.buildPath().toString());
+
+        if (cgenConfiguration.getRootPath() != null) {
+            
((GeneratorControllerPanel)getView()).getOutputFolder().setText(cgenConfiguration.buildPath().toString());
+        }
         
if(cgenConfiguration.getArtifactsGenerationMode().equalsIgnoreCase("all")) {
             ((CodeGeneratorControllerBase) 
parent).setCurrentClass(cgenConfiguration.getDataMap());
             ((CodeGeneratorControllerBase) parent).setSelected(true);
diff --git 
a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/cgen/GeneratorControllerPanel.java
 
b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/cgen/GeneratorControllerPanel.java
index 8941860ee..3a763b12f 100644
--- 
a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/cgen/GeneratorControllerPanel.java
+++ 
b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/cgen/GeneratorControllerPanel.java
@@ -28,6 +28,8 @@ import org.apache.cayenne.validation.ValidationException;
 import javax.swing.JButton;
 import javax.swing.JPanel;
 import javax.swing.JTextField;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 
 /**
  * @since 4.1
@@ -44,8 +46,14 @@ public class GeneratorControllerPanel extends JPanel {
         this.outputFolder = new TextAdapter(new JTextField()) {
             @Override
             protected void updateModel(String text) throws ValidationException 
{
+
                 getCgenByDataMap().setRelPath(text);
-                if(!codeGeneratorControllerBase.isInitFromModel()) {
+
+                if (!codeGeneratorControllerBase.isInitFromModel()) {
+
+                    if (getCgenByDataMap().getRootPath() == null && 
!Paths.get(text).isAbsolute()) {
+                        throw new ValidationException("You should save project 
to use rel path as output directory ");
+                    }
                     projectController.setDirty(true);
                 }
             }

Reply via email to