Author: [email protected]
Date: Sun May 24 10:37:46 2009
New Revision: 5466

Added:
    trunk/user/test/com/google/gwt/dev/jjs/test/singlejso/
    trunk/user/test/com/google/gwt/dev/jjs/test/singlejso/A.java    
(contents, props changed)
    trunk/user/test/com/google/gwt/dev/jjs/test/singlejso/B1.java    
(contents, props changed)
    trunk/user/test/com/google/gwt/dev/jjs/test/singlejso/B2.java    
(contents, props changed)
    trunk/user/test/com/google/gwt/dev/jjs/test/singlejso/IA.java    
(contents, props changed)
    trunk/user/test/com/google/gwt/dev/jjs/test/singlejso/IB.java    
(contents, props changed)
     
trunk/user/test/com/google/gwt/dev/jjs/test/singlejso/TypeHierarchyTest.java    
(contents, props changed)
Modified:
    trunk/dev/core/src/com/google/gwt/dev/jjs/ast/JTypeOracle.java
    trunk/dev/core/src/com/google/gwt/dev/jjs/impl/ControlFlowAnalyzer.java
    trunk/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java
     
trunk/dev/core/src/com/google/gwt/dev/shell/rewrite/HostedModeClassRewriter.java
     
trunk/dev/core/src/com/google/gwt/dev/shell/rewrite/RewriteSingleJsoImplDispatches.java
    trunk/user/test/com/google/gwt/dev/jjs/CompilerSuite.java
    trunk/user/test/com/google/gwt/dev/jjs/test/SingleJsoImplTest.java

Log:
This patch improves the correctness of SingleJso types by following all  
super-interfaces when calculating singleJso data.  Additionally, casts to  
singleJso interfaces will trigger a rescue of the concrete JSO type.

Without these changes, singleJso types that are referred to only via cast  
operations would generate incorrect code at the cast site, as a  
Cast.throwClassCastExceptionUnlessNull() call would be made instead of a  
dynamicCastJso() as the compiler would decide there are no concrete  
implementations of the singleJso interface type.

Patch by: bobv, mtsui
Review by: spoon
Reported by: macpherson, mtsui


Modified: trunk/dev/core/src/com/google/gwt/dev/jjs/ast/JTypeOracle.java
==============================================================================
--- trunk/dev/core/src/com/google/gwt/dev/jjs/ast/JTypeOracle.java       
(original)
+++ trunk/dev/core/src/com/google/gwt/dev/jjs/ast/JTypeOracle.java      Sun May 
 
24 10:37:46 2009
@@ -183,8 +183,8 @@
     *
     * @param type any type
     * @param instantiatedTypes a set of types assumed to be instantiated. If
-   *          <code>null</code>, then there are no assumptions about which
-   *          types are instantiated.
+   *          <code>null</code>, then there are no assumptions about which  
types
+   *          are instantiated.
     * @return whether the type is instantiated
     */
    private static boolean isInstantiatedType(JReferenceType type,
@@ -536,6 +536,25 @@
    }

    /**
+   * Computes the set of all interfaces implemented by a type.
+   */
+  private Set<JInterfaceType> allSuperInterfaces(JDeclaredType type) {
+    Set<JInterfaceType> toReturn = new IdentityHashSet<JInterfaceType>();
+    List<JInterfaceType> q = new LinkedList<JInterfaceType>();
+    q.addAll(type.getImplements());
+
+    while (!q.isEmpty()) {
+      JInterfaceType t = q.remove(0);
+
+      if (toReturn.add(t)) {
+        q.addAll(t.getImplements());
+      }
+    }
+
+    return toReturn;
+  }
+
+  /**
     * Compute all of the things I might conceivably implement, either  
through
     * super types or sub types.
     */
@@ -653,7 +672,7 @@
      for (JDeclaredType type : program.getDeclaredTypes()) {
        if (!program.isJavaScriptObject(type)) {
          if (type instanceof JClassType) {
-          dualImpl.addAll(type.getImplements());
+          dualImpl.addAll(allSuperInterfaces(type));
          }
          continue;
        }

Modified:  
trunk/dev/core/src/com/google/gwt/dev/jjs/impl/ControlFlowAnalyzer.java
==============================================================================
--- trunk/dev/core/src/com/google/gwt/dev/jjs/impl/ControlFlowAnalyzer.java     
 
(original)
+++ trunk/dev/core/src/com/google/gwt/dev/jjs/impl/ControlFlowAnalyzer.java     
 
Sun May 24 10:37:46 2009
@@ -175,7 +175,23 @@
        JType targetType = x.getCastType();
        if (program.isJavaScriptObject(targetType)) {
          rescue((JReferenceType) targetType, true, true);
+      } else {
+        /*
+         * If there's a cast to a SingleJso interface, rescue the  
implementing
+         * JSO type. If the JSO type isn't rescued (and there's no other  
regular
+         * Java type implementing the interface), then the cast operation  
will
+         * be replaced with a throwCCEUnlessNull() call, since there's no  
type
+         * left in the type system that implements the interface. If there  
is an
+         * implementing Java type, then a dynamicCast() will be emitted  
which
+         * will throw a CCE if it hits a JSO type.
+         */
+        JClassType maybeSingleJso =  
program.typeOracle.getSingleJsoImpls().get(
+            targetType);
+        if (maybeSingleJso != null) {
+          rescue(maybeSingleJso, true, true);
+        }
        }
+
        return true;
      }


Modified:  
trunk/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java
==============================================================================
--- trunk/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java       
 
(original)
+++ trunk/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java       
 
Sun May 24 10:37:46 2009
@@ -993,7 +993,7 @@
        if (unit != null) {
          anonymousClassMap = unit.getAnonymousClassMap();
        }
-      byte[] newBytes = classRewriter.rewrite(className, classBytes,
+      byte[] newBytes = classRewriter.rewrite(this, className, classBytes,
            anonymousClassMap);
        if (CLASS_DUMP) {
          if (!Arrays.equals(classBytes, newBytes)) {

Modified:  
trunk/dev/core/src/com/google/gwt/dev/shell/rewrite/HostedModeClassRewriter.java
==============================================================================
---  
trunk/dev/core/src/com/google/gwt/dev/shell/rewrite/HostedModeClassRewriter.java
         
(original)
+++  
trunk/dev/core/src/com/google/gwt/dev/shell/rewrite/HostedModeClassRewriter.java
         
Sun May 24 10:37:46 2009
@@ -183,12 +183,13 @@
    /**
     * Performs rewriting transformations on a class.
     *
+   * @param ccl the ClassLoader requesting the rewrite
     * @param className the name of the class
     * @param classBytes the bytes of the class
     * @param anonymousClassMap a map between the anonymous class names of  
java
     *          compiler used to compile code and jdt. Emma-specific.
     */
-  public byte[] rewrite(String className, byte[] classBytes,
+  public byte[] rewrite(ClassLoader ccl, String className, byte[]  
classBytes,
        Map<String, String> anonymousClassMap) {
      String desc = toDescriptor(className);
      assert (!jsoIntfDescs.contains(desc));
@@ -200,7 +201,7 @@
      // v = new CheckClassAdapter(v);
      // v = new TraceClassVisitor(v, new PrintWriter(System.out));

-    v = new RewriteSingleJsoImplDispatches(v, singleJsoImplTypes,
+    v = new RewriteSingleJsoImplDispatches(v, ccl, singleJsoImplTypes,
          mangledNamesToImplementations);

      v = new RewriteRefsToJsoClasses(v, jsoIntfDescs, mapper);

Modified:  
trunk/dev/core/src/com/google/gwt/dev/shell/rewrite/RewriteSingleJsoImplDispatches.java
==============================================================================
---  
trunk/dev/core/src/com/google/gwt/dev/shell/rewrite/RewriteSingleJsoImplDispatches.java
  
(original)
+++  
trunk/dev/core/src/com/google/gwt/dev/shell/rewrite/RewriteSingleJsoImplDispatches.java
  
Sun May 24 10:37:46 2009
@@ -23,9 +23,9 @@
  import com.google.gwt.dev.asm.Type;
  import com.google.gwt.dev.asm.commons.Method;

-import java.util.ArrayList;
  import java.util.Collections;
  import java.util.HashSet;
+import java.util.LinkedList;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
@@ -62,8 +62,57 @@
      @Override
      public void visitMethodInsn(int opcode, String owner, String name,
          String desc) {
-      if (singleJsoImplTypes.contains(owner)) {
-        name = owner.replace('/', '_') + "_" + name;
+      if (opcode == Opcodes.INVOKEINTERFACE) {
+        if (singleJsoImplTypes.contains(owner)) {
+          // Simple case; referring directly to a SingleJso interface.
+          name = owner.replace('/', '_') + "_" + name;
+          assert  
mangledNamesToImplementations.containsKey(name) : "Missing "
+              + name;
+
+        } else {
+          /*
+           * Might be referring to a subtype of a SingleJso interface:
+           *
+           * interface IA { void foo() }
+           *
+           * interface JA extends JSO implements IA;
+           *
+           * interface IB extends IA {}
+           *
+           * void bar() { ((IB) object).foo(); }
+           */
+          for (String intf : computeAllInterfaces(owner)) {
+            if (singleJsoImplTypes.contains(intf)) {
+              /*
+               * Check that it really should be mangled and is not a  
reference
+               * to a method defined in a non-singleJso super-interface. If
+               * there are two super-interfaces that define methods with
+               * identical names and descriptors, the choice of  
implementation
+               * is undefined.
+               */
+              String maybeMangled = intf.replace('/', '_') + "_" + name;
+              Method method =  
mangledNamesToImplementations.get(maybeMangled);
+              if (method != null) {
+                /*
+                 * Found a method with the right name, but we need to  
check the
+                 * parameters and the return type. In order to do this,  
we'll
+                 * look at the arguments and return type of the target  
method,
+                 * removing the first argument, which is the instance.
+                 */
+                assert method.getArgumentTypes().length >= 1;
+                Type[] argumentTypes = new  
Type[method.getArgumentTypes().length - 1];
+                System.arraycopy(method.getArgumentTypes(), 1,  
argumentTypes,
+                    0, argumentTypes.length);
+                String maybeDescriptor = Type.getMethodDescriptor(
+                    method.getReturnType(), argumentTypes);
+                if (maybeDescriptor.equals(desc)) {
+                  name = maybeMangled;
+                  break;
+                }
+              }
+            }
+          }
+        }
        }

        super.visitMethodInsn(opcode, owner, name, desc);
@@ -76,10 +125,13 @@
    private final Set<String> singleJsoImplTypes;
    private boolean inSingleJsoImplInterfaceType;

-  public RewriteSingleJsoImplDispatches(ClassVisitor v,
+  private final ClassLoader ccl;
+
+  public RewriteSingleJsoImplDispatches(ClassVisitor v, ClassLoader ccl,
        Set<String> singleJsoImplTypes,
        SortedMap<String, Method> mangledNamesToImplementations) {
      super(v);
+    this.ccl = ccl;
      this.singleJsoImplTypes =  
Collections.unmodifiableSet(singleJsoImplTypes);
      this.mangledNamesToImplementations =  
Collections.unmodifiableSortedMap(mangledNamesToImplementations);
    }
@@ -90,6 +142,15 @@
      assert currentTypeName == null;
      super.visit(version, access, name, signature, superName, interfaces);

+    /*
+     * This visitor would mangle JSO$ since it acts as a roll-up of all
+     * SingleJso types and the result would be repeated method definitions  
due
+     * to the trampoline methods this visitor would create.
+     */
+    if (name.equals(HostedModeClassRewriter.JAVASCRIPTOBJECT_IMPL_DESC)) {
+      return;
+    }
+
      currentTypeName = name;
      inSingleJsoImplInterfaceType = singleJsoImplTypes.contains(name);

@@ -100,8 +161,7 @@
       * original methods.
       */
      if (interfaces != null && (access & Opcodes.ACC_INTERFACE) == 0) {
-      List<String> toStub = new ArrayList<String>();
-      Collections.addAll(toStub, interfaces);
+      Set<String> toStub = computeAllInterfaces(interfaces);
        toStub.retainAll(singleJsoImplTypes);

        for (String stubIntr : toStub) {
@@ -144,6 +204,29 @@
      }

      return new MyMethodVisitor(mv);
+  }
+
+  private Set<String> computeAllInterfaces(String... interfaces) {
+    Set<String> toReturn = new HashSet<String>();
+    List<String> q = new LinkedList<String>();
+    Collections.addAll(q, interfaces);
+
+    while (!q.isEmpty()) {
+      String intf = q.remove(0);
+      if (toReturn.add(intf)) {
+        try {
+          Class<?> intfClass = Class.forName(intf.replace('/', '.'),  
false, ccl);
+          for (Class<?> i : intfClass.getInterfaces()) {
+            q.add(i.getName().replace('.', '/'));
+          }
+        } catch (ClassNotFoundException e) {
+          assert false : intf;
+          e.printStackTrace();
+        }
+      }
+    }
+
+    return toReturn;
    }

    /**

Modified: trunk/user/test/com/google/gwt/dev/jjs/CompilerSuite.java
==============================================================================
--- trunk/user/test/com/google/gwt/dev/jjs/CompilerSuite.java   (original)
+++ trunk/user/test/com/google/gwt/dev/jjs/CompilerSuite.java   Sun May 24  
10:37:46 2009
@@ -44,6 +44,7 @@
  import com.google.gwt.dev.jjs.test.SingleJsoImplTest;
  import com.google.gwt.dev.jjs.test.UnstableGeneratorTest;
  import com.google.gwt.dev.jjs.test.VarargsTest;
+import com.google.gwt.dev.jjs.test.singlejso.TypeHierarchyTest;
  import com.google.gwt.junit.tools.GWTTestSuite;

  import junit.framework.Test;
@@ -84,6 +85,7 @@
      suite.addTestSuite(RunAsyncFailureTest.class);
      suite.addTestSuite(RunAsyncTest.class);
      suite.addTestSuite(SingleJsoImplTest.class);
+    suite.addTestSuite(TypeHierarchyTest.class);
      suite.addTestSuite(UnstableGeneratorTest.class);
      suite.addTestSuite(VarargsTest.class);
      // $JUnit-END$

Modified: trunk/user/test/com/google/gwt/dev/jjs/test/SingleJsoImplTest.java
==============================================================================
--- trunk/user/test/com/google/gwt/dev/jjs/test/SingleJsoImplTest.java   
(original)
+++ trunk/user/test/com/google/gwt/dev/jjs/test/SingleJsoImplTest.java  Sun  
May 24 10:37:46 2009
@@ -66,6 +66,10 @@
      String call(int a, int b);
    }

+  interface CreatedWithCast {
+    String foo();
+  }
+
    interface Divider extends Multiplier {
      int divide(int a, int b);
    }
@@ -81,6 +85,16 @@
    }

    /**
+   * Even though this type is never instantiated, it's necessary to ensure  
that
+   * the CreatedWithCast test isn't short-circuited due to type tightening.
+   */
+  static class JavaCreatedWithCast implements CreatedWithCast {
+    public String foo() {
+      return "foo";
+    }
+  }
+
+  /**
     * The extra declaration of implementing Multiplier should still be  
legal.
     */
    static class JavaDivider extends JavaMultiplier implements Divider,
@@ -189,6 +203,16 @@
      }
    }

+  static class JsoCreatedWithCast extends JavaScriptObject implements
+      CreatedWithCast {
+    protected JsoCreatedWithCast() {
+    }
+
+    public final String foo() {
+      return "foo";
+    }
+  }
+
    static class JsoDivider extends JsoMultiplier implements Divider, Tag {
      protected JsoDivider() {
      }
@@ -450,6 +474,16 @@
      a.acceptString3Array(a.returnString3Array());
      a.acceptObjectArray(a.returnStringArray());
      a.acceptObject3Array(a.returnString3Array());
+  }
+
+  /**
+   * Ensure that SingleJSO types that are referred to only via a cast to  
the
+   * interface type are retained. If the JsoCreatedWithCast type isn't  
rescued
+   * correctly, the cast in this test will throw a ClassCastException  
since the
+   * compiler would assume there are types that implement the interface.
+   */
+  public void testCreatedWithCast() {
+    Object o = (CreatedWithCast) JavaScriptObject.createObject();
    }

    public void testDualCase() {

Added: trunk/user/test/com/google/gwt/dev/jjs/test/singlejso/A.java
==============================================================================
--- (empty file)
+++ trunk/user/test/com/google/gwt/dev/jjs/test/singlejso/A.java        Sun May 
24  
10:37:46 2009
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2009 Google Inc.
+ *
+ * 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 com.google.gwt.dev.jjs.test.singlejso;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+/**
+ * A class.
+ */
+public class A extends JavaScriptObject implements IA {
+  protected A() {
+  }
+
+  public static A create() {
+    return JavaScriptObject.createObject().cast();
+  }
+
+  public final String whoAmI() {
+    return "A";
+  }
+}

Added: trunk/user/test/com/google/gwt/dev/jjs/test/singlejso/B1.java
==============================================================================
--- (empty file)
+++ trunk/user/test/com/google/gwt/dev/jjs/test/singlejso/B1.java       Sun May 
 
24 10:37:46 2009
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2009 Google Inc.
+ *
+ * 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 com.google.gwt.dev.jjs.test.singlejso;
+
+/**
+ * B1 class.
+ */
+public class B1 implements IB {
+  public String sayHello() {
+    return "Hey, I'm " + whoAmI();
+  }
+
+  public String whoAmI() {
+    return "B1";
+  }
+}

Added: trunk/user/test/com/google/gwt/dev/jjs/test/singlejso/B2.java
==============================================================================
--- (empty file)
+++ trunk/user/test/com/google/gwt/dev/jjs/test/singlejso/B2.java       Sun May 
 
24 10:37:46 2009
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2009 Google Inc.
+ *
+ * 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 com.google.gwt.dev.jjs.test.singlejso;
+
+/**
+ * B2 class.
+ */
+public class B2 implements IB {
+  public String sayHello() {
+    return "Hi, I'm " + whoAmI();
+  }
+
+  public String whoAmI() {
+    return "B2";
+  }
+}

Added: trunk/user/test/com/google/gwt/dev/jjs/test/singlejso/IA.java
==============================================================================
--- (empty file)
+++ trunk/user/test/com/google/gwt/dev/jjs/test/singlejso/IA.java       Sun May 
 
24 10:37:46 2009
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2009 Google Inc.
+ *
+ * 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 com.google.gwt.dev.jjs.test.singlejso;
+
+/**
+ * Interface A.
+ */
+public interface IA {
+  String whoAmI();
+}

Added: trunk/user/test/com/google/gwt/dev/jjs/test/singlejso/IB.java
==============================================================================
--- (empty file)
+++ trunk/user/test/com/google/gwt/dev/jjs/test/singlejso/IB.java       Sun May 
 
24 10:37:46 2009
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2009 Google Inc.
+ *
+ * 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 com.google.gwt.dev.jjs.test.singlejso;
+
+/**
+ * Interface B.
+ */
+public interface IB extends IA {
+  String sayHello();
+}

Added:  
trunk/user/test/com/google/gwt/dev/jjs/test/singlejso/TypeHierarchyTest.java
==============================================================================
--- (empty file)
+++  
trunk/user/test/com/google/gwt/dev/jjs/test/singlejso/TypeHierarchyTest.java    
 
Sun May 24 10:37:46 2009
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2009 Google Inc.
+ *
+ * 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 com.google.gwt.dev.jjs.test.singlejso;
+
+import com.google.gwt.junit.client.GWTTestCase;
+
+/**
+ * Tests SingleJso semantics in non-trivial type hierarchies.
+ */
+public class TypeHierarchyTest extends GWTTestCase {
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.dev.jjs.CompilerSuite";
+  }
+
+  public void testCase1() {
+    A a = A.create();
+    assertEquals("A", a.whoAmI());
+
+    B1 b1 = new B1();
+    assertEquals("B1", b1.whoAmI());
+
+    B2 b2 = new B2();
+    assertEquals("B2", b2.whoAmI());
+  }
+
+  public void testCase2() {
+    IA a = A.create();
+    assertEquals("A", a.whoAmI());
+
+    IA b1 = new B1();
+    assertEquals("B1", b1.whoAmI());
+
+    IA b2 = new B2();
+    assertEquals("B2", b2.whoAmI());
+  }
+
+  public void testCase3() {
+    IA a = A.create();
+    assertEquals("A", a.whoAmI());
+
+    IB b1 = new B1();
+    assertEquals("B1", b1.whoAmI());
+
+    IB b2 = new B2();
+    assertEquals("B2", b2.whoAmI());
+  }
+
+}

--~--~---------~--~----~------------~-------~--~----~
http://groups.google.com/group/Google-Web-Toolkit-Contributors
-~----------~----~----~----~------~----~------~--~---

Reply via email to