Author: mrdon
Date: Sun Sep 24 23:54:57 2006
New Revision: 449584

URL: http://svn.apache.org/viewvc?view=rev&rev=449584
Log:
Added support for ActionForms that use Commons Validator
WW-1454

Added:
    struts/struts2/trunk/apps/showcase/src/main/webapp/WEB-INF/validation.xml
    
struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/Struts1Action.java
      - copied, changed from r449583, 
struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/LegacyAction.java
    
struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/Struts1Factory.java
      - copied, changed from r449583, 
struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/StrutsFactory.java
    
struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/WrapperMessageResources.java
    
struts/struts2/trunk/integration/src/test/java/org/apache/struts2/s1/Struts1FactoryTest.java
      - copied, changed from r449583, 
struts/struts2/trunk/integration/src/test/java/org/apache/struts2/s1/StrutsFactoryTest.java
Removed:
    
struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/LegacyAction.java
    
struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/StrutsFactory.java
    
struts/struts2/trunk/integration/src/test/java/org/apache/struts2/s1/StrutsFactoryTest.java
Modified:
    
struts/struts2/trunk/apps/showcase/src/main/java/org/apache/struts2/showcase/integration/GangsterForm.java
    struts/struts2/trunk/apps/showcase/src/main/resources/struts-integration.xml
    
struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/ActionFormValidationInterceptor.java
    
struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/WrapperActionMapping.java

Modified: 
struts/struts2/trunk/apps/showcase/src/main/java/org/apache/struts2/showcase/integration/GangsterForm.java
URL: 
http://svn.apache.org/viewvc/struts/struts2/trunk/apps/showcase/src/main/java/org/apache/struts2/showcase/integration/GangsterForm.java?view=diff&rev=449584&r1=449583&r2=449584
==============================================================================
--- 
struts/struts2/trunk/apps/showcase/src/main/java/org/apache/struts2/showcase/integration/GangsterForm.java
 (original)
+++ 
struts/struts2/trunk/apps/showcase/src/main/java/org/apache/struts2/showcase/integration/GangsterForm.java
 Sun Sep 24 23:54:57 2006
@@ -6,8 +6,9 @@
 import org.apache.struts.action.ActionForm;
 import org.apache.struts.action.ActionMapping;
 import org.apache.struts.action.ActionMessage;
+import org.apache.struts.validator.ValidatorForm;
 
-public class GangsterForm extends ActionForm {
+public class GangsterForm extends ValidatorForm {
     
     private String name;
     private String age;
@@ -26,11 +27,12 @@
      * @see 
org.apache.struts.action.ActionForm#validate(org.apache.struts.action.ActionMapping,
 javax.servlet.http.HttpServletRequest)
      */
     @Override
-    public ActionErrors validate(ActionMapping arg0, HttpServletRequest arg1) {
-        ActionErrors errors = new ActionErrors();
+    public ActionErrors validate(ActionMapping mapping, HttpServletRequest 
request) {
+        ActionErrors errors = super.validate(mapping, request);
         if (name == null || name.length() == 0) {
             errors.add("name", new ActionMessage("The name must not be 
blank"));
         }
+        
         return errors;
     }
     

Modified: 
struts/struts2/trunk/apps/showcase/src/main/resources/struts-integration.xml
URL: 
http://svn.apache.org/viewvc/struts/struts2/trunk/apps/showcase/src/main/resources/struts-integration.xml?view=diff&rev=449584&r1=449583&r2=449584
==============================================================================
--- 
struts/struts2/trunk/apps/showcase/src/main/resources/struts-integration.xml 
(original)
+++ 
struts/struts2/trunk/apps/showcase/src/main/resources/struts-integration.xml 
Sun Sep 24 23:54:57 2006
@@ -10,6 +10,10 @@
            <interceptors>
                <interceptor name="gangsterForm" 
class="com.opensymphony.xwork2.interceptor.ScopedModelDrivenInterceptor">
                        <param 
name="className">org.apache.struts2.showcase.integration.GangsterForm</param>
+                       <param name="name">gangsterForm</param>
+               </interceptor>
+               <interceptor name="gangsterValidation" 
class="org.apache.struts2.s1.ActionFormValidationInterceptor">
+                       <param 
name="pathnames">/org/apache/struts/validator/validator-rules.xml,/WEB-INF/validation.xml</param>
                </interceptor>
            
                <interceptor-stack name="integration">
@@ -18,7 +22,7 @@
                        <interceptor-ref name="model-driven"/>
                 <interceptor-ref name="actionForm-reset"/>
                 <interceptor-ref name="basicStack"/>
-                <interceptor-ref name="actionForm-validation"/>
+                <interceptor-ref name="gangsterValidation"/>
                 <interceptor-ref name="workflow"/>
                </interceptor-stack>
            </interceptors>

Added: struts/struts2/trunk/apps/showcase/src/main/webapp/WEB-INF/validation.xml
URL: 
http://svn.apache.org/viewvc/struts/struts2/trunk/apps/showcase/src/main/webapp/WEB-INF/validation.xml?view=auto&rev=449584
==============================================================================
--- struts/struts2/trunk/apps/showcase/src/main/webapp/WEB-INF/validation.xml 
(added)
+++ struts/struts2/trunk/apps/showcase/src/main/webapp/WEB-INF/validation.xml 
Sun Sep 24 23:54:57 2006
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+
+<!DOCTYPE form-validation PUBLIC
+     "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 
1.3.0//EN"
+     "http://jakarta.apache.org/commons/dtds/validator_1_3_0.dtd";>
+
+<form-validation>
+
+<!--
+     This is a minimal Validator form file with a couple of examples.
+-->
+
+    <global>
+
+        <!-- An example global constant
+        <constant>
+            <constant-name>postalCode</constant-name>
+            <constant-value>^\d{5}\d*$</constant-value>
+        </constant>
+        end example-->
+
+    </global>
+
+    <formset>
+
+        <!-- An example form -->
+        <form name="gangsterForm">
+            <field
+                property="age"
+                depends="required">
+                    <msg name="required" key="The age is required" 
resource="false"/>
+            </field>
+        </form>
+
+    </formset>
+
+</form-validation>

Modified: 
struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/ActionFormValidationInterceptor.java
URL: 
http://svn.apache.org/viewvc/struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/ActionFormValidationInterceptor.java?view=diff&rev=449584&r1=449583&r2=449584
==============================================================================
--- 
struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/ActionFormValidationInterceptor.java
 (original)
+++ 
struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/ActionFormValidationInterceptor.java
 Sun Sep 24 23:54:57 2006
@@ -18,15 +18,36 @@
 
 package org.apache.struts2.s1;
 
+import java.io.IOException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.UnavailableException;
 import javax.servlet.http.HttpServletRequest;
 
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.commons.validator.ValidatorResources;
+import org.apache.struts.Globals;
 import org.apache.struts.action.ActionErrors;
 import org.apache.struts.action.ActionForm;
 import org.apache.struts.action.ActionMapping;
+import org.apache.struts.action.ActionServlet;
+import org.apache.struts.config.ModuleConfig;
+import org.apache.struts.validator.ValidatorPlugIn;
 import org.apache.struts2.ServletActionContext;
+import org.apache.struts2.StrutsException;
 import org.apache.struts2.dispatcher.Dispatcher;
+import org.apache.struts2.util.ServletContextAware;
+import org.xml.sax.SAXException;
 
+import com.opensymphony.xwork2.ActionContext;
 import com.opensymphony.xwork2.ActionInvocation;
+import com.opensymphony.xwork2.TextProvider;
 import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
 import com.opensymphony.xwork2.interceptor.ScopedModelDriven;
 
@@ -37,8 +58,52 @@
  */
 public class ActionFormValidationInterceptor extends AbstractInterceptor {
 
+    private String pathnames;
+    private boolean stopOnFirstError;
+    private boolean initialized = false;
+    
+    private static final Log log = 
LogFactory.getLog(ActionFormValidationInterceptor.class);
+    
+    /**
+     * Delimitter for Validator resources.
+     */
+    private final static String RESOURCE_DELIM = ",";
+    
+    /**
+     * Initializes the validation resources
+     */
+    private void initResources(ServletContext servletContext) {
+        if (pathnames != null) {
+            ActionContext ctx = ActionContext.getContext();
+            try {
+                
+                ValidatorResources resources = 
this.loadResources(servletContext);
+    
+                
+                String prefix = 
ctx.getActionInvocation().getProxy().getNamespace();
+                
+                
+                servletContext.setAttribute(ValidatorPlugIn.VALIDATOR_KEY + 
prefix, resources);
+    
+                servletContext.setAttribute(ValidatorPlugIn.STOP_ON_ERROR_KEY 
+ '.'
+                    + prefix,
+                    (this.stopOnFirstError ? Boolean.TRUE : Boolean.FALSE));
+            } catch (Exception e) {
+                throw new StrutsException(
+                    "Cannot load a validator resource from '" + pathnames + 
"'", e);
+            }
+        }
+    }
+
     @Override
     public String intercept(ActionInvocation invocation) throws Exception {
+        // Lazy load the resources because the servlet context isn't available 
at init() time
+        synchronized (this) {
+            if (!initialized) {
+                initResources(ServletActionContext.getServletContext());
+                initialized = true;
+            }
+        }
         Object action = invocation.getAction();
 
         
@@ -47,13 +112,114 @@
             ScopedModelDriven modelDriven = (ScopedModelDriven) action;
             Object model = modelDriven.getModel();
             if (model != null) {
+                HttpServletRequest req = ServletActionContext.getRequest();
                 Struts1Factory strutsFactory = new 
Struts1Factory(Dispatcher.getInstance().getConfigurationManager().getConfiguration());
                 ActionMapping mapping = 
strutsFactory.createActionMapping(invocation.getProxy().getConfig());
-                HttpServletRequest req = ServletActionContext.getRequest();
-                ActionErrors errors = ((ActionForm)model).validate(mapping, 
req);
+                ModuleConfig moduleConfig = 
strutsFactory.createModuleConfig(invocation.getProxy().getConfig().getPackageName());
+                req.setAttribute(Globals.MODULE_KEY, moduleConfig);
+                req.setAttribute(Globals.MESSAGES_KEY, new 
WrapperMessageResources((TextProvider)invocation.getAction()));
+                
+                mapping.setAttribute(modelDriven.getScopeKey());
+                
+                ActionForm form = (ActionForm) model;
+                form.setServlet(new ActionServlet(){
+                    public ServletContext getServletContext() {
+                        return ServletActionContext.getServletContext();
+                    }
+                });
+                ActionErrors errors = form.validate(mapping, req);
                 strutsFactory.convertErrors(errors, action);                
             }
         }
         return invocation.invoke();
     }
+    
+    /**
+     * Initialize the validator resources for this module.
+     *
+     * @throws IOException      if an input/output error is encountered
+     * @throws ServletException if we cannot initialize these resources
+     */
+    protected ValidatorResources loadResources(ServletContext ctx)
+        throws IOException, ServletException {
+        if ((pathnames == null) || (pathnames.length() <= 0)) {
+            return null;
+        }
+
+        StringTokenizer st = new StringTokenizer(pathnames, RESOURCE_DELIM);
+
+        List urlList = new ArrayList();
+        ValidatorResources resources = null;
+        try {
+            while (st.hasMoreTokens()) {
+                String validatorRules = st.nextToken().trim();
+
+                if (log.isInfoEnabled()) {
+                    log.info("Loading validation rules file from '"
+                        + validatorRules + "'");
+                }
+
+                URL input =
+                    ctx.getResource(validatorRules);
+
+                // If the config isn't in the servlet context, try the class
+                // loader which allows the config files to be stored in a jar
+                if (input == null) {
+                    input = getClass().getResource(validatorRules);
+                }
+
+                if (input != null) {
+                    urlList.add(input);
+                } else {
+                    throw new ServletException(
+                        "Skipping validation rules file from '"
+                        + validatorRules + "'.  No url could be located.");
+                }
+            }
+
+            int urlSize = urlList.size();
+            String[] urlArray = new String[urlSize];
+
+            for (int urlIndex = 0; urlIndex < urlSize; urlIndex++) {
+                URL url = (URL) urlList.get(urlIndex);
+
+                urlArray[urlIndex] = url.toExternalForm();
+            }
+
+            resources =  new ValidatorResources(urlArray);
+        } catch (SAXException sex) {
+            log.error("Skipping all validation", sex);
+            throw new StrutsException("Skipping all validation because the 
validation files cannot be loaded", sex);
+        }
+        return resources;
+    }
+
+    /**
+     * @return the pathnames
+     */
+    public String getPathnames() {
+        return pathnames;
+    }
+
+    /**
+     * @param pathnames the pathnames to set
+     */
+    public void setPathnames(String pathNames) {
+        this.pathnames = pathNames;
+    }
+
+    /**
+     * @return the stopOnFirstError
+     */
+    public boolean isStopOnFirstError() {
+        return stopOnFirstError;
+    }
+
+    /**
+     * @param stopOnFirstError the stopOnFirstError to set
+     */
+    public void setStopOnFirstError(boolean stopOnFirstError) {
+        this.stopOnFirstError = stopOnFirstError;
+    }
+
 }

Copied: 
struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/Struts1Action.java
 (from r449583, 
struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/LegacyAction.java)
URL: 
http://svn.apache.org/viewvc/struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/Struts1Action.java?view=diff&rev=449584&p1=struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/LegacyAction.java&r1=449583&p2=struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/Struts1Action.java&r2=449584
==============================================================================
--- 
struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/LegacyAction.java
 (original)
+++ 
struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/Struts1Action.java
 Sun Sep 24 23:54:57 2006
@@ -55,11 +55,12 @@
  *  <li>Most everything else...</li>
  * </ul>
  */
-public class LegacyAction extends DefaultActionSupport implements 
ScopedModelDriven<ActionForm> {
+public class Struts1Action extends DefaultActionSupport implements 
ScopedModelDriven<ActionForm> {
 
     private ActionForm actionForm;
     private String className;
     private boolean validate;
+    private String scopeKey;
     
     public String execute() throws Exception {
         ActionContext ctx = ActionContext.getContext();
@@ -73,7 +74,7 @@
         
         // We should call setServlet() here, but let's stub that out later
         
-        StrutsFactory strutsFactory = new 
StrutsFactory(Dispatcher.getInstance().getConfigurationManager().getConfiguration());
+        Struts1Factory strutsFactory = new 
Struts1Factory(Dispatcher.getInstance().getConfigurationManager().getConfiguration());
         ActionMapping mapping = 
strutsFactory.createActionMapping(actionConfig);
         HttpServletRequest request = ServletActionContext.getRequest();
         HttpServletResponse response = ServletActionContext.getResponse();
@@ -125,5 +126,13 @@
      */
     public void setClassName(String className) {
         this.className = className;
+    }
+
+    public String getScopeKey() {
+        return scopeKey;
+    }
+
+    public void setScopeKey(String key) {
+        this.scopeKey = key;
     }
 }

Copied: 
struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/Struts1Factory.java
 (from r449583, 
struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/StrutsFactory.java)
URL: 
http://svn.apache.org/viewvc/struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/Struts1Factory.java?view=diff&rev=449584&p1=struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/StrutsFactory.java&r1=449583&p2=struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/Struts1Factory.java&r2=449584
==============================================================================
--- 
struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/StrutsFactory.java
 (original)
+++ 
struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/Struts1Factory.java
 Sun Sep 24 23:54:57 2006
@@ -15,7 +15,6 @@
  * limitations under the License.
  *
  */
-
 package org.apache.struts2.s1;
 
 import com.opensymphony.xwork2.*;
@@ -23,22 +22,27 @@
 import com.opensymphony.xwork2.config.entities.ActionConfig;
 import com.opensymphony.xwork2.config.entities.ResultConfig;
 import com.opensymphony.xwork2.config.entities.ExceptionMappingConfig;
+
+import org.apache.struts.Globals;
 import org.apache.struts.action.*;
 import org.apache.struts.config.*;
 
 import java.util.Iterator;
 import java.util.Arrays;
+import java.util.Map;
+
+import javax.servlet.ServletContext;
 
 
 /**
  *  Provides conversion methods between the Struts Action 1.x and XWork
  *  classes.
  */
-public class StrutsFactory {
+public class Struts1Factory {
     
     private Configuration configuration;
 
-    public StrutsFactory(Configuration config) {
+    public Struts1Factory(Configuration config) {
         this.configuration = config;
     }
     
@@ -53,7 +57,7 @@
         assert packageName != null;
         return new WrapperModuleConfig(this, 
configuration.getPackageConfig(packageName));
     }
-
+    
     /**
      * Create a Struts 1.x ActionMapping from an XWork ActionConfig.
      * 

Modified: 
struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/WrapperActionMapping.java
URL: 
http://svn.apache.org/viewvc/struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/WrapperActionMapping.java?view=diff&rev=449584&r1=449583&r2=449584
==============================================================================
--- 
struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/WrapperActionMapping.java
 (original)
+++ 
struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/WrapperActionMapping.java
 Sun Sep 24 23:54:57 2006
@@ -39,6 +39,7 @@
 
     private ActionConfig delegate;
     private String actionPath;
+    private String attribute;
     private Struts1Factory strutsFactory;
 
     public WrapperActionMapping(Struts1Factory factory, ActionConfig delegate) 
{
@@ -135,11 +136,11 @@
     }
 
     public String getAttribute() {
-        throw new UnsupportedOperationException("NYI");
+        return attribute;
     }
 
     public void setAttribute(String attribute) {
-        throw new UnsupportedOperationException("Not implemented - immutable");
+        this.attribute = attribute;
     }
 
     public String getForward() {

Added: 
struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/WrapperMessageResources.java
URL: 
http://svn.apache.org/viewvc/struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/WrapperMessageResources.java?view=auto&rev=449584
==============================================================================
--- 
struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/WrapperMessageResources.java
 (added)
+++ 
struts/struts2/trunk/integration/src/main/java/org/apache/struts2/s1/WrapperMessageResources.java
 Sun Sep 24 23:54:57 2006
@@ -0,0 +1,44 @@
+/*
+ * $Id$
+ * Copyright 2004 The Apache Software Foundation.
+ *
+ * Licensed 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
+ * 
+ *     http://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.struts2.s1;
+
+import java.util.Locale;
+
+import org.apache.struts.util.MessageResources;
+
+import com.opensymphony.xwork2.TextProvider;
+
+/**
+ * Wraps the Struts 1 message resources, delegating to Struts 2 resources
+ */
+public class WrapperMessageResources extends MessageResources {
+
+    private TextProvider textProvider;
+
+    public WrapperMessageResources(TextProvider provider) {
+        super(null, null, true);
+        this.textProvider = provider;
+    }
+
+    @Override
+    public String getMessage(Locale locale, String key) {
+        String msg = textProvider.getText(key);
+        return msg; 
+    }
+
+}

Copied: 
struts/struts2/trunk/integration/src/test/java/org/apache/struts2/s1/Struts1FactoryTest.java
 (from r449583, 
struts/struts2/trunk/integration/src/test/java/org/apache/struts2/s1/StrutsFactoryTest.java)
URL: 
http://svn.apache.org/viewvc/struts/struts2/trunk/integration/src/test/java/org/apache/struts2/s1/Struts1FactoryTest.java?view=diff&rev=449584&p1=struts/struts2/trunk/integration/src/test/java/org/apache/struts2/s1/StrutsFactoryTest.java&r1=449583&p2=struts/struts2/trunk/integration/src/test/java/org/apache/struts2/s1/Struts1FactoryTest.java&r2=449584
==============================================================================
--- 
struts/struts2/trunk/integration/src/test/java/org/apache/struts2/s1/StrutsFactoryTest.java
 (original)
+++ 
struts/struts2/trunk/integration/src/test/java/org/apache/struts2/s1/Struts1FactoryTest.java
 Sun Sep 24 23:54:57 2006
@@ -23,22 +23,22 @@
 import com.opensymphony.xwork2.config.entities.ResultConfig;
 
 /**
- * Test of StrutsFactory, which creates Struts 1.x wrappers around XWork 
config objects.
+ * Test of Struts1Factory, which creates Struts 1.x wrappers around XWork 
config objects.
  */
-public class StrutsFactoryTest extends TestCase {
+public class Struts1FactoryTest extends TestCase {
 
     private static final String PACKAGE_NAME = "org/apache/struts2/s1";
     
-    protected StrutsFactory factory = null;
+    protected Struts1Factory factory = null;
     protected Configuration config;
 
-    public StrutsFactoryTest(String name) throws Exception {
+    public Struts1FactoryTest(String name) throws Exception {
         super(name);
     }
 
 
     public static void main(String args[]) {
-        junit.textui.TestRunner.run(StrutsFactoryTest.class);
+        junit.textui.TestRunner.run(Struts1FactoryTest.class);
     }
 
     /**
@@ -49,7 +49,7 @@
         ConfigurationProvider provider = new 
StrutsXmlConfigurationProvider(PACKAGE_NAME + "/test-struts-factory.xml", true);
         manager.addConfigurationProvider(provider);
         config = manager.getConfiguration();
-        factory = new StrutsFactory(config);
+        factory = new Struts1Factory(config);
     }
 
     /**
@@ -137,7 +137,6 @@
         
         // These methods are currently not implemented -- replace as 
functionality is added.
         assertNYI(mapping, "getInputForward", null);
-        assertNYI(mapping, "getAttribute", null);
         assertNYI(mapping, "getForward", null);
         assertNYI(mapping, "getInclude", null);
         assertNYI(mapping, "getInput", null);


Reply via email to