pespin has submitted this change. ( 
https://gerrit.osmocom.org/c/libosmo-sigtran/+/40543?usp=email )

Change subject: Differentiate between dynamic and static routes
......................................................................

Differentiate between dynamic and static routes

Differentiation is important because for instance
* It shall not be possible to delete dynamic routes over VTY
  "remove route" command.
* Dynamic routes should not be stored in config, ie. not appear
  during "show running-config" or "write running-config".
* During VTY "show cs7 instance 0 route", a "dyn" string keyword
  sohuld be displayed on each dynamic route row.

Related: OS#6755
Change-Id: Ic6c6b46084a1e4063ebf1f5d13e0e03386bb4c45
---
M src/sccp_user.c
M src/ss7_as_vty.c
M src/ss7_route.c
M src/ss7_route.h
M src/ss7_route_table.c
M src/ss7_route_table.h
M src/ss7_vty.c
M src/xua_as_fsm.c
M src/xua_rkm.c
M tests/ss7/ss7_test.c
M tests/vty/osmo_stp_route_prio.vty
11 files changed, 85 insertions(+), 50 deletions(-)

Approvals:
  Jenkins Builder: Verified
  osmith: Looks good to me, but someone else must approve
  lynxis lazus: Looks good to me, but someone else must approve
  pespin: Looks good to me, approved




diff --git a/src/sccp_user.c b/src/sccp_user.c
index 18561c4..bf92c1a 100644
--- a/src/sccp_user.c
+++ b/src/sccp_user.c
@@ -618,12 +618,12 @@
        LOGP(DLSCCP, LOGL_NOTICE, "%s: Using AS instance %s\n", name,
             as->cfg.name);

-       /* Create a default route if necessary */
-       rt = ss7_route_table_find_route_by_dpc_mask(ss7->rtable_system, 0, 0);
+       /* Create a default dynamic route if necessary */
+       rt = ss7_route_table_find_route_by_dpc_mask(ss7->rtable_system, 0, 0, 
true);
        if (!rt) {
                LOGP(DLSCCP, LOGL_NOTICE, "%s: Creating default route\n", name);
                rt = ss7_route_create(ss7->rtable_system, 0, 0,
-                                          as->cfg.name);
+                                     true, as->cfg.name);
                if (!rt)
                        goto out_as;
                rt_created = true;
@@ -867,7 +867,7 @@
                goto out_strings;

        /* route only selected PC to the client */
-       rt = ss7_route_create(ss7->rtable_system, pc, 0xffff, as_name);
+       rt = ss7_route_create(ss7->rtable_system, pc, 0xffff, true, as_name);
        if (!rt)
                goto out_as;

diff --git a/src/ss7_as_vty.c b/src/ss7_as_vty.c
index c7658d1..c34bbed 100644
--- a/src/ss7_as_vty.c
+++ b/src/ss7_as_vty.c
@@ -299,12 +299,14 @@

        /* When libosmo-sigtran is used in ASP role, the VTY routing table node
         * (config-cs7-rt) is not available. However, when we add a routing key
-        * to an AS we still have to put a matching route into the routing
-        * table. This is done automatically by first removing the old route
+        * to an AS we still have to put a matching dynamic route into the 
routing
+        * table. This is done automatically by first removing the old dynamic 
route
         * (users may change the routing key via VTY during runtime) and then
-        * putting a new route (see below). */
+        * putting a new dynamic route (see below). */
        if (cs7_role == CS7_ROLE_ASP) {
-               rt = 
ss7_route_table_find_route_by_dpc_mask(as->inst->rtable_system, rkey->pc, 
0xffffff);
+               rt = 
ss7_route_table_find_route_by_dpc_mask(as->inst->rtable_system,
+                                                           rkey->pc, 0xffffff,
+                                                           true);
                if (rt)
                        ss7_route_destroy(rt);
        }
@@ -315,9 +317,9 @@
        rkey->si = si ? get_string_value(mtp_si_vals, si) : 0;  /* FIXME: input 
validation */
        rkey->ssn = ssn ? atoi(ssn) : 0;                        /* FIXME: input 
validation */

-       /* automatically add new route (see also comment above) */
+       /* automatically add new dynamic route (see also comment above) */
        if (cs7_role == CS7_ROLE_ASP) {
-               if (!ss7_route_create(as->inst->rtable_system, rkey->pc, 
0xffffff, as->cfg.name)) {
+               if (!ss7_route_create(as->inst->rtable_system, rkey->pc, 
0xffffff, true, as->cfg.name)) {
                        vty_out(vty, "Cannot create route (pc=%s, linkset=%s) 
to AS %s", dpc, as->cfg.name, VTY_NEWLINE);
                        return CMD_WARNING;
                }
diff --git a/src/ss7_route.c b/src/ss7_route.c
index 6350f71..e71b543 100644
--- a/src/ss7_route.c
+++ b/src/ss7_route.c
@@ -42,6 +42,7 @@
  *  \param[in] rtbl Routing Table where the route belongs
  *  \param[in] pc Point Code of the destination of the route
  *  \param[in] mask Mask of the destination Point Code \ref pc
+ *  \param[in] dynamic Whether the route is dynamic
  *  \returns Allocated route (not yet inserted into its rtbl), NULL on error
  *
  * The returned route has no linkset associated yet, user *must* associate it
@@ -58,9 +59,14 @@
  * The route entry allocated with this API can be destroyed/freed at any point 
using API
  * ss7_route_destroy(), regardless of it being already inserted or not in
  * its routing table.
+ *
+ * Dynamic routes are not configured by the user (VTY), and hence cannot be
+ * removed by the user. Dynamic routes are not stored in the config and hence
+ * they don't show up in eg "show running-config"; they can be listed using
+ * specific VTY commands like "show cs7 instance 0 route".
  */
 struct osmo_ss7_route *
-ss7_route_alloc(struct osmo_ss7_route_table *rtbl, uint32_t pc, uint32_t mask)
+ss7_route_alloc(struct osmo_ss7_route_table *rtbl, uint32_t pc, uint32_t mask, 
bool dynamic)
 {
        struct osmo_ss7_route *rt;

@@ -78,6 +84,7 @@
        rt->cfg.mask = osmo_ss7_pc_normalize(&rtbl->inst->cfg.pc_fmt, mask);
        rt->cfg.pc = osmo_ss7_pc_normalize(&rtbl->inst->cfg.pc_fmt, pc);
        rt->cfg.priority = OSMO_SS7_ROUTE_PRIO_DEFAULT;
+       rt->cfg.dyn_allocated = dynamic;
        return rt;
 }

@@ -160,7 +167,8 @@
        if (clset) { /* check for duplicates */
                struct osmo_ss7_route *prev_rt;
                llist_for_each_entry(prev_rt, &clset->routes, list) {
-                       if (!strcmp(prev_rt->cfg.linkset_name, 
rt->cfg.linkset_name)) {
+                       if (strcmp(prev_rt->cfg.linkset_name, 
rt->cfg.linkset_name) == 0 &&
+                           prev_rt->cfg.dyn_allocated == 
rt->cfg.dyn_allocated) {
                                LOGSS7(rtbl->inst, LOGL_ERROR,
                                       "Refusing to create route with existing 
linkset name: pc=%u=%s mask=0x%x via linkset/AS '%s'\n",
                                       rt->cfg.pc, 
osmo_ss7_pointcode_print(rtbl->inst, rt->cfg.pc),
@@ -181,6 +189,7 @@
  *  \param[in] rtbl Routing Table in which the route is to be created
  *  \param[in] pc Point Code of the destination of the route
  *  \param[in] mask Mask of the destination Point Code \ref pc
+ *  \param[in] dynamic Whether the route is dynamic
  *  \param[in] linkset_name string name of the linkset to be used
  *  \returns callee-allocated + initialized route, NULL on error
  *
@@ -192,12 +201,12 @@
  */
 struct osmo_ss7_route *
 ss7_route_create(struct osmo_ss7_route_table *rtbl, uint32_t pc,
-                     uint32_t mask, const char *linkset_name)
+                uint32_t mask, bool dynamic, const char *linkset_name)
 {
        struct osmo_ss7_route *rt;
        int rc;

-       rt = ss7_route_alloc(rtbl, pc, mask);
+       rt = ss7_route_alloc(rtbl, pc, mask, dynamic);
        if (!rt)
                return NULL;

@@ -210,7 +219,7 @@
        /* Keep old behavior, return already existing route: */
        if (rc == -EADDRINUSE) {
                talloc_free(rt);
-               return ss7_route_table_find_route_by_dpc_mask(rtbl, pc, mask);
+               return ss7_route_table_find_route_by_dpc_mask(rtbl, pc, mask, 
dynamic);
        }

        return rt;
@@ -309,8 +318,11 @@
        } while (0)

        APPEND("pc=%u=%s mask=0x%x=%s",
-       rt->cfg.pc, osmo_ss7_pointcode_print_buf(pc_str, sizeof(pc_str), inst, 
rt->cfg.pc),
-       rt->cfg.mask, osmo_ss7_pointcode_print_buf(mask_str, sizeof(mask_str), 
inst, rt->cfg.mask));
+              rt->cfg.pc, osmo_ss7_pointcode_print_buf(pc_str, sizeof(pc_str), 
inst, rt->cfg.pc),
+              rt->cfg.mask, osmo_ss7_pointcode_print_buf(mask_str, 
sizeof(mask_str), inst, rt->cfg.mask));
+
+       if (rt->cfg.dyn_allocated)
+               APPEND(" dyn");

        if (rt->dest.as) {
                struct osmo_ss7_as *as = rt->dest.as;
diff --git a/src/ss7_route.h b/src/ss7_route.h
index 0dcc34e..4bb7a2b 100644
--- a/src/ss7_route.h
+++ b/src/ss7_route.h
@@ -1,6 +1,7 @@
 #pragma once

 #include <stdint.h>
+#include <stdbool.h>
 #include <osmocom/core/linuxlist.h>

 /***********************************************************************
@@ -38,14 +39,15 @@
                /*! lower priority is higher */
                uint32_t priority;
                uint8_t qos_class;
+               bool dyn_allocated;
        } cfg;
 };

 struct osmo_ss7_route *
-ss7_route_alloc(struct osmo_ss7_route_table *rtbl, uint32_t pc, uint32_t mask);
+ss7_route_alloc(struct osmo_ss7_route_table *rtbl, uint32_t pc, uint32_t mask, 
bool dynamic);
 struct osmo_ss7_route *
 ss7_route_create(struct osmo_ss7_route_table *rtbl, uint32_t dpc,
-                       uint32_t mask, const char *linkset_name);
+                uint32_t mask, bool dynamic, const char *linkset_name);
 void ss7_route_destroy(struct osmo_ss7_route *rt);

 struct osmo_ss7_route *
diff --git a/src/ss7_route_table.c b/src/ss7_route_table.c
index d2f34a6..fb10df4 100644
--- a/src/ss7_route_table.c
+++ b/src/ss7_route_table.c
@@ -110,7 +110,7 @@
  */
 struct osmo_ss7_route *
 ss7_route_table_find_route_by_dpc_mask(struct osmo_ss7_route_table *rtbl, 
uint32_t dpc,
-                               uint32_t mask)
+                                      uint32_t mask, bool dynamic)
 {
        struct osmo_ss7_combined_linkset *clset;
        struct osmo_ss7_route *rt;
@@ -120,11 +120,21 @@
        dpc = osmo_ss7_pc_normalize(&rtbl->inst->cfg.pc_fmt, dpc);
        mask = osmo_ss7_pc_normalize(&rtbl->inst->cfg.pc_fmt, mask);

-       clset = ss7_route_table_find_combined_linkset_by_dpc_mask(rtbl, dpc, 
mask);
-       if (!clset)
-               return NULL;
-       rt = llist_first_entry_or_null(&clset->routes, struct osmo_ss7_route, 
list);
-       return rt;
+       /* we assume the combined_links are sorted by mask length, i.e. more
+        * specific combined links first, and less specific combined links with 
shorter
+        * mask later */
+       llist_for_each_entry(clset, &rtbl->combined_linksets, list) {
+               if ((dpc & clset->cfg.mask) != clset->cfg.pc)
+                       continue;
+               if (mask != clset->cfg.mask)
+                       continue;
+               llist_for_each_entry(rt, &clset->routes, list) {
+                       if (rt->cfg.dyn_allocated != dynamic)
+                               continue;
+                       return rt;
+               }
+       }
+       return NULL;
 }

 struct osmo_ss7_combined_linkset *
diff --git a/src/ss7_route_table.h b/src/ss7_route_table.h
index 1de3be5..9f27412 100644
--- a/src/ss7_route_table.h
+++ b/src/ss7_route_table.h
@@ -39,7 +39,7 @@

 struct osmo_ss7_route *
 ss7_route_table_find_route_by_dpc_mask(struct osmo_ss7_route_table *rtbl, 
uint32_t dpc,
-                       uint32_t mask);
+                                      uint32_t mask, bool dynamic);
 struct osmo_ss7_route *
 ss7_route_table_lookup_route(struct osmo_ss7_route_table *rtbl, const struct 
osmo_ss7_route_label *rtlabel);

diff --git a/src/ss7_vty.c b/src/ss7_vty.c
index 66efc03..1f443d2 100644
--- a/src/ss7_vty.c
+++ b/src/ss7_vty.c
@@ -398,7 +398,7 @@
                return CMD_WARNING;
        }

-       rt = ss7_route_alloc(rtable, dpc, mask);
+       rt = ss7_route_alloc(rtable, dpc, mask, false);
        if (!rt) {
                vty_out(vty, "%% Cannot allocate new route%s", VTY_NEWLINE);
                return CMD_WARNING;
@@ -464,7 +464,7 @@
                return CMD_WARNING;
        }

-       rt = ss7_route_table_find_route_by_dpc_mask(rtable, dpc, mask);
+       rt = ss7_route_table_find_route_by_dpc_mask(rtable, dpc, mask, false);
        if (!rt) {
                vty_out(vty, "cannot find route to be deleted%s", VTY_NEWLINE);
                return CMD_WARNING;
@@ -484,6 +484,8 @@
                vty_out(vty, "  description %s%s", rtable->cfg.description, 
VTY_NEWLINE);
        llist_for_each_entry(clset, &rtable->combined_linksets, list) {
                llist_for_each_entry(rt, &clset->routes, list) {
+                       if (rt->cfg.dyn_allocated)
+                               continue;
                        vty_out(vty, "  update route %s %s linkset %s",
                                osmo_ss7_pointcode_print(rtable->inst, 
rt->cfg.pc),
                                osmo_ss7_pointcode_print2(rtable->inst, 
rt->cfg.mask),
@@ -516,7 +518,7 @@
                llist_for_each_entry(rt, &clset->routes, list) {
                        bool rt_avail = ss7_route_is_available(rt);

-                       vty_out(vty, "%-16s %-5s %c %c %u %-19s %-7s %-7s 
%-7s%s",
+                       vty_out(vty, "%-16s %-5s %c %c %u %-19s %-7s %-7s %-7s 
%-3s%s",
                                osmo_ss7_route_print(rt),
                                rt_avail ? "acces" : "INACC",
                                ' ',
@@ -526,6 +528,7 @@
                                rt_avail ? "avail" : "UNAVAIL",
                                "?",
                                rt_avail ? "avail" : "UNAVAIL",
+                               rt->cfg.dyn_allocated ? "dyn" : "",
                                VTY_NEWLINE);
                }
        }
diff --git a/src/xua_as_fsm.c b/src/xua_as_fsm.c
index 3e117ee..6a8941b 100644
--- a/src/xua_as_fsm.c
+++ b/src/xua_as_fsm.c
@@ -277,12 +277,14 @@
        struct osmo_ss7_as *as = xafp->as;
        struct osmo_ss7_instance *inst = as->inst;

-       if (ss7_route_table_find_route_by_dpc_mask(inst->rtable_system, 
as->cfg.routing_key.pc, 0xffffff))
+       if (ss7_route_table_find_route_by_dpc_mask(inst->rtable_system,
+                                                  as->cfg.routing_key.pc, 
0xffffff,
+                                                  true))
                return;

        /* As opposed to M3UA, there is no RKM and we have to implicitly
-        * automatically add a route once an IPA connection has come up */
-       if (ss7_route_create(inst->rtable_system, as->cfg.routing_key.pc, 
0xffffff, as->cfg.name))
+        * automatically add a dynamic route once an IPA connection has come up 
*/
+       if (ss7_route_create(inst->rtable_system, as->cfg.routing_key.pc, 
0xffffff, true, as->cfg.name))
                xafp->ipa_route_created = true;
 }

@@ -298,7 +300,9 @@
                return;

        /* find the route which we have created if we ever reached 
ipa_asp_fsm_wait_id_ack2 */
-       rt = ss7_route_table_find_route_by_dpc_mask(inst->rtable_system, 
as->cfg.routing_key.pc, 0xffffff);
+       rt = ss7_route_table_find_route_by_dpc_mask(inst->rtable_system,
+                                                   as->cfg.routing_key.pc, 
0xffffff,
+                                                   true);
        /* no route found, bail out */
        if (!rt) {
                LOGPFSML(fi, LOGL_NOTICE, "Attempting to delete route for this 
IPA AS, but cannot "
diff --git a/src/xua_rkm.c b/src/xua_rkm.c
index 1ae5ea6..f03397c 100644
--- a/src/xua_rkm.c
+++ b/src/xua_rkm.c
@@ -279,8 +279,8 @@
                as->cfg.routing_key.pc = dpc;
                as->cfg.routing_key.context = rctx;

-               /* add route for that routing key */
-               rt = ss7_route_create(as->inst->rtable_system, dpc, 0xFFFFFF, 
namebuf);
+               /* add dynamic route for that routing key */
+               rt = ss7_route_create(as->inst->rtable_system, dpc, 0xFFFFFF, 
true, namebuf);
                if (!rt) {
                        LOGPASP(asp, DLSS7, LOGL_ERROR, "RKM: Cannot insert 
route for DPC %s / as %s\n",
                                osmo_ss7_pointcode_print(asp->inst, dpc), 
namebuf);
@@ -393,7 +393,9 @@
                return -1;
        }

-       rt = ss7_route_table_find_route_by_dpc_mask(inst->rtable_system, 
as->cfg.routing_key.pc, 0xffffff);
+       rt = ss7_route_table_find_route_by_dpc_mask(inst->rtable_system,
+                                                   as->cfg.routing_key.pc, 
0xffffff,
+                                                   true);
        if (!rt) {
                msgb_append_dereg_res(resp, M3UA_RKM_DEREG_ERR_UNKNOWN, 0);
                return -1;
diff --git a/tests/ss7/ss7_test.c b/tests/ss7/ss7_test.c
index bb96e71..c8e1c67 100644
--- a/tests/ss7/ss7_test.c
+++ b/tests/ss7/ss7_test.c
@@ -188,7 +188,7 @@
        /* route with full mask */
        route_label = RT_LABEL(0, 12, 0);
        OSMO_ASSERT(ss7_route_table_lookup_route(rtbl, &route_label) == NULL);
-       rt = ss7_route_create(rtbl, 12, 0xffff, "a");
+       rt = ss7_route_create(rtbl, 12, 0xffff, false, "a");
        printf("route with full mask: %s\n", osmo_ss7_route_print(rt));
        OSMO_ASSERT(rt);
        route_label = RT_LABEL(0, 12, 0);
@@ -196,7 +196,7 @@
        ss7_route_destroy(rt);

        /* route with partial mask */
-       rt = ss7_route_create(rtbl, 8, 0xfff8, "a");
+       rt = ss7_route_create(rtbl, 8, 0xfff8, false, "a");
        printf("route with partial mask: %s\n", osmo_ss7_route_print(rt));
        route_label = RT_LABEL(0, 8, 0);
        OSMO_ASSERT(ss7_route_table_lookup_route(rtbl, &route_label) == rt);
@@ -210,7 +210,7 @@
        OSMO_ASSERT(ss7_route_table_lookup_route(rtbl, &route_label) == NULL);
        /* insert more specific route for 12, must have higher priority
         * than existing one */
-       rt12 = ss7_route_create(rtbl, 12, 0xffff, "b");
+       rt12 = ss7_route_create(rtbl, 12, 0xffff, false, "b");
        route_label = RT_LABEL(0, 12, 0);
        OSMO_ASSERT(ss7_route_table_lookup_route(rtbl, &route_label) == rt12);
        route_label = RT_LABEL(0, 15, 0);
@@ -218,7 +218,7 @@
        route_label = RT_LABEL(0, 16, 0);
        OSMO_ASSERT(ss7_route_table_lookup_route(rtbl, &route_label) == NULL);
        /* add a default route, which should have lowest precedence */
-       rtdef = ss7_route_create(rtbl, 0, 0, "a");
+       rtdef = ss7_route_create(rtbl, 0, 0, false, "a");
        route_label = RT_LABEL(0, 12, 0);
        OSMO_ASSERT(ss7_route_table_lookup_route(rtbl, &route_label) == rt12);
        route_label = RT_LABEL(0, 15, 0);
@@ -230,7 +230,7 @@
        ss7_route_destroy(rt12);
        ss7_route_destroy(rt);

-       rt = ss7_route_create(rtbl, 8, 0xfff9, "a");
+       rt = ss7_route_create(rtbl, 8, 0xfff9, false, "a");
        printf("route with non-consecutive mask: %s\n", 
osmo_ss7_route_print(rt));
        ss7_route_destroy(rt);

diff --git a/tests/vty/osmo_stp_route_prio.vty 
b/tests/vty/osmo_stp_route_prio.vty
index 1a3b516..fe89e44 100644
--- a/tests/vty/osmo_stp_route_prio.vty
+++ b/tests/vty/osmo_stp_route_prio.vty
@@ -92,9 +92,9 @@

 Destination            C Q P Linkset Name        Linkset Non-adj Route
 ---------------------- - - - ------------------- ------- ------- -------
-3.2.1/14         INACC   0 2 as3                 UNAVAIL ?       UNAVAIL
-3.2.1/14         INACC   0 5 as2                 UNAVAIL ?       UNAVAIL
-3.2.1/14         INACC   7 6 as1                 UNAVAIL ?       UNAVAIL
+3.2.1/14         INACC   0 2 as3                 UNAVAIL ?       UNAVAIL
+3.2.1/14         INACC   0 5 as2                 UNAVAIL ?       UNAVAIL
+3.2.1/14         INACC   7 6 as1                 UNAVAIL ?       UNAVAIL

 OsmoSTP(config-cs7-rt)# ! NOW ADD MORE GENERIC ROUTES (SMALLER BITMASK LENGTH)
 OsmoSTP(config-cs7-rt)# update route 3.2.0 7.255.0 linkset as1 priority 1
@@ -118,12 +118,12 @@

 Destination            C Q P Linkset Name        Linkset Non-adj Route
 ---------------------- - - - ------------------- ------- ------- -------
-3.2.1/14         INACC   0 2 as3                 UNAVAIL ?       UNAVAIL
-3.2.1/14         INACC   0 5 as2                 UNAVAIL ?       UNAVAIL
-3.2.1/14         INACC   7 6 as1                 UNAVAIL ?       UNAVAIL
-3.2.0/11         INACC   0 1 as1                 UNAVAIL ?       UNAVAIL
-3.2.0/11         INACC   0 1 as2                 UNAVAIL ?       UNAVAIL
-3.2.0/11         INACC   0 1 as3                 UNAVAIL ?       UNAVAIL
+3.2.1/14         INACC   0 2 as3                 UNAVAIL ?       UNAVAIL
+3.2.1/14         INACC   0 5 as2                 UNAVAIL ?       UNAVAIL
+3.2.1/14         INACC   7 6 as1                 UNAVAIL ?       UNAVAIL
+3.2.0/11         INACC   0 1 as1                 UNAVAIL ?       UNAVAIL
+3.2.0/11         INACC   0 1 as2                 UNAVAIL ?       UNAVAIL
+3.2.0/11         INACC   0 1 as3                 UNAVAIL ?       UNAVAIL

 OsmoSTP(config-cs7-rt)# do show cs7 instance 0 route binding-table 3.2.1


--
To view, visit https://gerrit.osmocom.org/c/libosmo-sigtran/+/40543?usp=email
To unsubscribe, or for help writing mail filters, visit 
https://gerrit.osmocom.org/settings?usp=email

Gerrit-MessageType: merged
Gerrit-Project: libosmo-sigtran
Gerrit-Branch: master
Gerrit-Change-Id: Ic6c6b46084a1e4063ebf1f5d13e0e03386bb4c45
Gerrit-Change-Number: 40543
Gerrit-PatchSet: 5
Gerrit-Owner: pespin <[email protected]>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: laforge <[email protected]>
Gerrit-Reviewer: lynxis lazus <[email protected]>
Gerrit-Reviewer: osmith <[email protected]>
Gerrit-Reviewer: pespin <[email protected]>

Reply via email to