Add test coverage for the multi-VRF IPv6 FIB API.

Signed-off-by: Vladimir Medvedkin <[email protected]>
---
 app/test-fib/main.c  |  92 +++++++++++--
 app/test/test_fib6.c | 319 ++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 395 insertions(+), 16 deletions(-)

diff --git a/app/test-fib/main.c b/app/test-fib/main.c
index 5593fdd47e..0feac72b87 100644
--- a/app/test-fib/main.c
+++ b/app/test-fib/main.c
@@ -717,7 +717,7 @@ check_config(void)
         * get_vrf_bits(nb_vrfs) must be strictly less than
         * the total nexthop width.
         */
-       if ((config.nb_vrfs > 1) && !(config.flags & IPV6_FLAG)) {
+       if (config.nb_vrfs > 1) {
                uint8_t nh_sz = rte_ctz32(config.ent_sz);
                uint8_t vrf_bits = get_vrf_bits(config.nb_vrfs);
                /* - 2 to leave at least 1 bit for nexthop and 1 bit for 
ext_ent flag */
@@ -1165,6 +1165,7 @@ run_v6(void)
 {
        uint64_t start, acc;
        uint64_t def_nh = 0;
+       uint8_t nh_sz = rte_ctz32(config.ent_sz);
        struct rte_fib6 *fib;
        struct rte_fib6_conf conf = {0};
        struct rt_rule_6 *rt;
@@ -1175,6 +1176,7 @@ run_v6(void)
        struct rte_ipv6_addr *tbl6;
        uint64_t fib_nh[BURST_SZ];
        int32_t lpm_nh[BURST_SZ];
+       uint16_t *vrf_ids = NULL;
 
        rt = (struct rt_rule_6 *)config.rt;
        tbl6 = config.lookup_tbl;
@@ -1189,16 +1191,38 @@ run_v6(void)
                return ret;
        }
 
+       /* Allocate VRF IDs array for lookups if using multiple VRFs */
+       if (config.nb_vrfs > 1) {
+               vrf_ids = rte_malloc(NULL, sizeof(uint16_t) * 
config.nb_lookup_ips, 0);
+               if (vrf_ids == NULL) {
+                       printf("Can not alloc VRF IDs array\n");
+                       return -ENOMEM;
+               }
+               /* Generate random VRF IDs for each lookup */
+               for (i = 0; i < config.nb_lookup_ips; i++)
+                       vrf_ids[i] = rte_rand() % config.nb_vrfs;
+       }
+
        conf.type = get_fib_type();
        conf.default_nh = def_nh;
        conf.max_routes = config.nb_routes * 2;
        conf.rib_ext_sz = 0;
+       conf.max_vrfs = config.nb_vrfs;
+       conf.vrf_default_nh = NULL;
        if (conf.type == RTE_FIB6_TRIE) {
                conf.trie.nh_sz = rte_ctz32(config.ent_sz);
                conf.trie.num_tbl8 = RTE_MIN(config.tbl8,
                        get_max_nh(conf.trie.nh_sz));
        }
 
+       conf.vrf_default_nh = rte_malloc(NULL, conf.max_vrfs * 
sizeof(uint64_t), 0);
+       if (conf.vrf_default_nh == NULL) {
+               printf("Can not alloc VRF default nexthops array\n");
+               return -ENOMEM;
+       }
+       for (i = 0; i < conf.max_vrfs; i++)
+               conf.vrf_default_nh[i] = encode_vrf_nh(i, def_nh, nh_sz);
+
        fib = rte_fib6_create("test", -1, &conf);
        if (fib == NULL) {
                printf("Can not alloc FIB, err %d\n", rte_errno);
@@ -1223,12 +1247,28 @@ run_v6(void)
        for (k = config.print_fract, i = 0; k > 0; k--) {
                start = rte_rdtsc_precise();
                for (j = 0; j < (config.nb_routes - i) / k; j++) {
-                       ret = rte_fib6_add(fib, &rt[i + j].addr,
-                               rt[i + j].depth, rt[i + j].nh);
-                       if (unlikely(ret != 0)) {
-                               printf("Can not add a route to FIB, err %d\n",
-                                       ret);
-                               return -ret;
+                       uint32_t idx = i + j;
+                       if (config.nb_vrfs > 1) {
+                               uint16_t vrf_id;
+                               for (vrf_id = 0; vrf_id < config.nb_vrfs; 
vrf_id++) {
+                                       uint64_t nh = encode_vrf_nh(vrf_id, 
rt[idx].nh,
+                                               nh_sz);
+                                       ret = rte_fib6_vrf_add(fib, vrf_id, 
&rt[idx].addr,
+                                               rt[idx].depth, nh);
+                                       if (unlikely(ret != 0)) {
+                                               printf("Can not add a route to 
FIB, err %d\n",
+                                                       ret);
+                                               return -ret;
+                                       }
+                               }
+                       } else {
+                               ret = rte_fib6_add(fib, &rt[idx].addr,
+                                       rt[idx].depth, rt[idx].nh);
+                               if (unlikely(ret != 0)) {
+                                       printf("Can not add a route to FIB, err 
%d\n",
+                                               ret);
+                                       return -ret;
+                               }
                        }
                }
                printf("AVG FIB add %"PRIu64"\n",
@@ -1268,15 +1308,33 @@ run_v6(void)
        acc = 0;
        for (i = 0; i < config.nb_lookup_ips; i += BURST_SZ) {
                start = rte_rdtsc_precise();
-               ret = rte_fib6_lookup_bulk(fib, &tbl6[i],
-                       fib_nh, BURST_SZ);
+               if (config.nb_vrfs > 1)
+                       ret = rte_fib6_vrf_lookup_bulk(fib, vrf_ids + i,
+                               &tbl6[i], fib_nh, BURST_SZ);
+               else
+                       ret = rte_fib6_lookup_bulk(fib, &tbl6[i],
+                               fib_nh, BURST_SZ);
                acc += rte_rdtsc_precise() - start;
                if (ret != 0) {
                        printf("FIB lookup fails, err %d\n", ret);
                        return -ret;
                }
+               /* Validate VRF IDs in returned nexthops */
+               if (config.nb_vrfs > 1) {
+                       for (j = 0; j < BURST_SZ; j++) {
+                               uint16_t returned_vrf = decode_vrf_nh(fib_nh[j],
+                                       nh_sz);
+                               if (returned_vrf != vrf_ids[i + j]) {
+                                       printf("VRF validation failed: expected 
VRF %u, got %u\n",
+                                               vrf_ids[i + j], returned_vrf);
+                                       return -1;
+                               }
+                       }
+               }
        }
        printf("AVG FIB lookup %.1f\n", (double)acc / (double)i);
+       if (config.nb_vrfs > 1)
+               printf("VRF validation passed\n");
 
        if (config.flags & CMP_FLAG) {
                acc = 0;
@@ -1314,8 +1372,17 @@ run_v6(void)
 
        for (k = config.print_fract, i = 0; k > 0; k--) {
                start = rte_rdtsc_precise();
-               for (j = 0; j < (config.nb_routes - i) / k; j++)
-                       rte_fib6_delete(fib, &rt[i + j].addr, rt[i + j].depth);
+               for (j = 0; j < (config.nb_routes - i) / k; j++) {
+                       uint32_t idx = i + j;
+                       if (config.nb_vrfs > 1) {
+                               uint16_t vrf_id;
+                               for (vrf_id = 0; vrf_id < config.nb_vrfs; 
vrf_id++)
+                                       rte_fib6_vrf_delete(fib, vrf_id, 
&rt[idx].addr,
+                                               rt[idx].depth);
+                       } else {
+                               rte_fib6_delete(fib, &rt[idx].addr, 
rt[idx].depth);
+                       }
+               }
 
                printf("AVG FIB delete %"PRIu64"\n",
                        (rte_rdtsc_precise() - start) / j);
@@ -1334,6 +1401,9 @@ run_v6(void)
                        i += j;
                }
        }
+
+       if (vrf_ids != NULL)
+               rte_free(vrf_ids);
        return 0;
 }
 
diff --git a/app/test/test_fib6.c b/app/test/test_fib6.c
index fffb590dbf..1143a338e6 100644
--- a/app/test/test_fib6.c
+++ b/app/test/test_fib6.c
@@ -6,6 +6,7 @@
 #include <stdio.h>
 #include <stdint.h>
 #include <stdlib.h>
+#include <inttypes.h>
 
 #include <rte_memory.h>
 #include <rte_log.h>
@@ -25,6 +26,11 @@ static int32_t test_get_invalid(void);
 static int32_t test_lookup(void);
 static int32_t test_invalid_rcu(void);
 static int32_t test_fib_rcu_sync_rw(void);
+static int32_t test_create_vrf(void);
+static int32_t test_vrf_add_del(void);
+static int32_t test_vrf_lookup(void);
+static int32_t test_vrf_isolation(void);
+static int32_t test_vrf_all_nh_sizes(void);
 
 #define MAX_ROUTES     (1 << 16)
 /** Maximum number of tbl8 for 2-byte entries */
@@ -38,7 +44,7 @@ int32_t
 test_create_invalid(void)
 {
        struct rte_fib6 *fib = NULL;
-       struct rte_fib6_conf config;
+       struct rte_fib6_conf config = { 0 };
 
        config.max_routes = MAX_ROUTES;
        config.rib_ext_sz = 0;
@@ -97,7 +103,7 @@ int32_t
 test_multiple_create(void)
 {
        struct rte_fib6 *fib = NULL;
-       struct rte_fib6_conf config;
+       struct rte_fib6_conf config = { 0 };
        int32_t i;
 
        config.rib_ext_sz = 0;
@@ -124,7 +130,7 @@ int32_t
 test_free_null(void)
 {
        struct rte_fib6 *fib = NULL;
-       struct rte_fib6_conf config;
+       struct rte_fib6_conf config = { 0 };
 
        config.max_routes = MAX_ROUTES;
        config.rib_ext_sz = 0;
@@ -148,7 +154,7 @@ int32_t
 test_add_del_invalid(void)
 {
        struct rte_fib6 *fib = NULL;
-       struct rte_fib6_conf config;
+       struct rte_fib6_conf config = { 0 };
        uint64_t nh = 100;
        struct rte_ipv6_addr ip = RTE_IPV6_ADDR_UNSPEC;
        int ret;
@@ -342,7 +348,7 @@ int32_t
 test_lookup(void)
 {
        struct rte_fib6 *fib = NULL;
-       struct rte_fib6_conf config;
+       struct rte_fib6_conf config = { 0 };
        uint64_t def_nh = 100;
        int ret;
 
@@ -599,6 +605,304 @@ test_fib_rcu_sync_rw(void)
        return status == 0 ? TEST_SUCCESS : TEST_FAILED;
 }
 
+/*
+ * Test VRF creation and basic operations
+ */
+static int32_t
+test_create_vrf(void)
+{
+       struct rte_fib6 *fib = NULL;
+       struct rte_fib6_conf config = { 0 };
+       uint64_t def_nh = 100;
+       uint64_t vrf_def_nh[4] = {100, 200, 300, 400};
+
+       config.max_routes = MAX_ROUTES;
+       config.rib_ext_sz = 0;
+       config.default_nh = def_nh;
+       config.type = RTE_FIB6_TRIE;
+       config.trie.nh_sz = RTE_FIB6_TRIE_4B;
+       config.trie.num_tbl8 = MAX_TBL8;
+
+       /* Test single VRF (backward compat) */
+       config.max_vrfs = 0;
+       config.vrf_default_nh = NULL;
+       fib = rte_fib6_create(__func__, SOCKET_ID_ANY, &config);
+       RTE_TEST_ASSERT(fib != NULL, "Failed to create FIB with max_vrfs=0\n");
+       rte_fib6_free(fib);
+
+       /* Test single VRF explicitly */
+       config.max_vrfs = 1;
+       fib = rte_fib6_create(__func__, SOCKET_ID_ANY, &config);
+       RTE_TEST_ASSERT(fib != NULL, "Failed to create FIB with max_vrfs=1\n");
+       rte_fib6_free(fib);
+
+       /* Test multi-VRF with per-VRF defaults */
+       config.max_vrfs = 4;
+       config.vrf_default_nh = vrf_def_nh;
+       fib = rte_fib6_create(__func__, SOCKET_ID_ANY, &config);
+       RTE_TEST_ASSERT(fib != NULL, "Failed to create FIB with max_vrfs=4\n");
+       rte_fib6_free(fib);
+
+       return TEST_SUCCESS;
+}
+
+/*
+ * Test VRF route add/delete operations
+ */
+static int32_t
+test_vrf_add_del(void)
+{
+       struct rte_fib6 *fib = NULL;
+       struct rte_fib6_conf config = { 0 };
+       uint64_t def_nh = 100;
+       uint64_t vrf_def_nh[4] = {100, 200, 300, 400};
+       struct rte_ipv6_addr ip = RTE_IPV6(0x2001, 0, 0, 0, 0, 0, 0, 0);
+       uint8_t depth = 64;
+       uint64_t nh = 1000;
+       int ret;
+
+       config.max_routes = MAX_ROUTES;
+       config.rib_ext_sz = 0;
+       config.default_nh = def_nh;
+       config.type = RTE_FIB6_TRIE;
+       config.trie.nh_sz = RTE_FIB6_TRIE_4B;
+       config.trie.num_tbl8 = MAX_TBL8;
+       config.max_vrfs = 4;
+       config.vrf_default_nh = vrf_def_nh;
+
+       fib = rte_fib6_create(__func__, SOCKET_ID_ANY, &config);
+       RTE_TEST_ASSERT(fib != NULL, "Failed to create FIB\n");
+
+       /* Add route to VRF 0 */
+       ret = rte_fib6_vrf_add(fib, 0, &ip, depth, nh);
+       RTE_TEST_ASSERT(ret == 0, "Failed to add route to VRF 0\n");
+
+       /* Add route to VRF 1 with different nexthop */
+       ret = rte_fib6_vrf_add(fib, 1, &ip, depth, nh + 1);
+       RTE_TEST_ASSERT(ret == 0, "Failed to add route to VRF 1\n");
+
+       /* Add route to VRF 2 */
+       ret = rte_fib6_vrf_add(fib, 2, &ip, depth, nh + 2);
+       RTE_TEST_ASSERT(ret == 0, "Failed to add route to VRF 2\n");
+
+       /* Test invalid VRF ID */
+       ret = rte_fib6_vrf_add(fib, 10, &ip, depth, nh);
+       RTE_TEST_ASSERT(ret != 0, "Should fail with invalid VRF ID\n");
+
+       /* Delete route from VRF 1 */
+       ret = rte_fib6_vrf_delete(fib, 1, &ip, depth);
+       RTE_TEST_ASSERT(ret == 0, "Failed to delete route from VRF 1\n");
+
+       /* Delete non-existent route - implementation may return error */
+       ret = rte_fib6_vrf_delete(fib, 3, &ip, depth);
+       (void)ret;  /* Accept any return value */
+
+       rte_fib6_free(fib);
+       return TEST_SUCCESS;
+}
+
+/*
+ * Test VRF lookup functionality
+ */
+static int32_t
+test_vrf_lookup(void)
+{
+       struct rte_fib6 *fib = NULL;
+       struct rte_fib6_conf config = { 0 };
+       uint64_t def_nh = 100;
+       uint64_t vrf_def_nh[4] = {1000, 2000, 3000, 4000};
+       struct rte_ipv6_addr ip_base = RTE_IPV6(0x2001, 0, 0, 0, 0, 0, 0, 0);
+       uint16_t vrf_ids[4];
+       struct rte_ipv6_addr ips[4];
+       uint64_t next_hops[4];
+       int ret;
+       uint32_t i;
+
+       config.max_routes = MAX_ROUTES;
+       config.rib_ext_sz = 0;
+       config.default_nh = def_nh;
+       config.type = RTE_FIB6_TRIE;
+       config.trie.nh_sz = RTE_FIB6_TRIE_4B;
+       config.trie.num_tbl8 = MAX_TBL8;
+       config.max_vrfs = 4;
+       config.vrf_default_nh = vrf_def_nh;
+
+       fib = rte_fib6_create(__func__, SOCKET_ID_ANY, &config);
+       RTE_TEST_ASSERT(fib != NULL, "Failed to create FIB\n");
+
+       /* Add routes to different VRFs with VRF-specific nexthops */
+       for (i = 0; i < 4; i++) {
+               struct rte_ipv6_addr ip = ip_base;
+               ip.a[1] = (uint8_t)i;
+               ret = rte_fib6_vrf_add(fib, i, &ip, 64, 100 + i);
+               RTE_TEST_ASSERT(ret == 0, "Failed to add route to VRF %u\n", i);
+       }
+
+       /* Prepare lookup: each IP should match its VRF-specific route */
+       for (i = 0; i < 4; i++) {
+               vrf_ids[i] = i;
+               ips[i] = ip_base;
+               ips[i].a[1] = (uint8_t)i;
+               ips[i].a[15] = 0x34; /* within /64 */
+       }
+
+       /* Lookup should return VRF-specific nexthops */
+       ret = rte_fib6_vrf_lookup_bulk(fib, vrf_ids, ips, next_hops, 4);
+       RTE_TEST_ASSERT(ret == 0, "VRF lookup failed\n");
+
+       for (i = 0; i < 4; i++) {
+               RTE_TEST_ASSERT(next_hops[i] == 100 + i,
+                       "Wrong nexthop for VRF %u: expected %"PRIu64", got 
%"PRIu64"\n",
+                       i, (uint64_t)(100 + i), next_hops[i]);
+       }
+
+       /* Test default nexthops for unmatched IPs */
+       {
+               struct rte_ipv6_addr ip_unmatch = RTE_IPV6(0x3001, 0, 0, 0, 0, 
0, 0, 1);
+               for (i = 0; i < 4; i++) {
+                       vrf_ids[i] = i;
+                       ips[i] = ip_unmatch;
+               }
+       }
+
+       ret = rte_fib6_vrf_lookup_bulk(fib, vrf_ids, ips, next_hops, 4);
+       RTE_TEST_ASSERT(ret == 0, "VRF lookup failed\n");
+
+       for (i = 0; i < 4; i++) {
+               RTE_TEST_ASSERT(next_hops[i] == vrf_def_nh[i],
+                       "Wrong default nexthop for VRF %u: expected %"PRIu64", 
got %"PRIu64"\n",
+                       i, vrf_def_nh[i], next_hops[i]);
+       }
+
+       rte_fib6_free(fib);
+       return TEST_SUCCESS;
+}
+
+/*
+ * Test VRF isolation - routes in one VRF shouldn't affect others
+ */
+static int32_t
+test_vrf_isolation(void)
+{
+       struct rte_fib6 *fib = NULL;
+       struct rte_fib6_conf config = { 0 };
+       uint64_t vrf_def_nh[3] = {100, 200, 300};
+       struct rte_ipv6_addr ip = RTE_IPV6(0x2001, 0, 0, 0, 0, 0, 0, 0);
+       uint16_t vrf_ids[3] = {0, 1, 2};
+       struct rte_ipv6_addr ips[3];
+       uint64_t next_hops[3];
+       int ret;
+       uint32_t i;
+
+       config.max_routes = MAX_ROUTES;
+       config.rib_ext_sz = 0;
+       config.default_nh = 0;
+       config.type = RTE_FIB6_TRIE;
+       config.trie.nh_sz = RTE_FIB6_TRIE_4B;
+       config.trie.num_tbl8 = MAX_TBL8;
+       config.max_vrfs = 3;
+       config.vrf_default_nh = vrf_def_nh;
+
+       fib = rte_fib6_create("test_vrf6_isol", SOCKET_ID_ANY, &config);
+       RTE_TEST_ASSERT(fib != NULL, "Failed to create FIB\n");
+
+       /* Add route only to VRF 1 */
+       ret = rte_fib6_vrf_add(fib, 1, &ip, 64, 777);
+       RTE_TEST_ASSERT(ret == 0, "Failed to add route to VRF 1\n");
+
+       /* Lookup same IP in all three VRFs */
+       for (i = 0; i < 3; i++) {
+               ips[i] = ip;
+               ips[i].a[15] = 0x22; /* within /64 */
+       }
+
+       ret = rte_fib6_vrf_lookup_bulk(fib, vrf_ids, ips, next_hops, 3);
+       RTE_TEST_ASSERT(ret == 0, "VRF lookup failed\n");
+
+       /* VRF 0 should get default */
+       RTE_TEST_ASSERT(next_hops[0] == vrf_def_nh[0],
+               "VRF 0 should return default nexthop\n");
+
+       /* VRF 1 should get the route */
+       RTE_TEST_ASSERT(next_hops[1] == 777,
+               "VRF 1 should return route nexthop 777, got %"PRIu64"\n", 
next_hops[1]);
+
+       /* VRF 2 should get default */
+       RTE_TEST_ASSERT(next_hops[2] == vrf_def_nh[2],
+               "VRF 2 should return default nexthop\n");
+
+       rte_fib6_free(fib);
+       return TEST_SUCCESS;
+}
+
+/*
+ * Test multi-VRF with all nexthop sizes
+ */
+static int32_t
+test_vrf_all_nh_sizes(void)
+{
+       struct rte_fib6 *fib = NULL;
+       struct rte_fib6_conf config = { 0 };
+       uint64_t vrf_def_nh[2] = {10, 20};
+       struct rte_ipv6_addr ip = RTE_IPV6(0x2001, 0, 0, 0, 0, 0, 0, 0);
+       uint16_t vrf_ids[2] = {0, 1};
+       struct rte_ipv6_addr ips[2];
+       uint64_t next_hops[2];
+       int ret;
+       enum rte_fib_trie_nh_sz nh_sizes[] = {
+               RTE_FIB6_TRIE_2B,
+               RTE_FIB6_TRIE_4B,
+               RTE_FIB6_TRIE_8B
+       };
+       uint64_t max_nhs[] = {32767, 2147483647ULL, 9223372036854775807ULL};
+       int i;
+
+       config.max_routes = MAX_ROUTES;
+       config.rib_ext_sz = 0;
+       config.default_nh = 0;
+       config.type = RTE_FIB6_TRIE;
+       config.trie.num_tbl8 = MAX_TBL8 - 1;
+       config.max_vrfs = 2;
+       config.vrf_default_nh = vrf_def_nh;
+
+       for (i = 0; i < (int)RTE_DIM(nh_sizes); i++) {
+               char name[32];
+               config.trie.nh_sz = nh_sizes[i];
+               snprintf(name, sizeof(name), "vrf6_nh%d", i);
+
+               fib = rte_fib6_create(name, SOCKET_ID_ANY, &config);
+               RTE_TEST_ASSERT(fib != NULL, "Failed to create FIB\n");
+
+               /* Add routes with max nexthop for this size */
+               ret = rte_fib6_vrf_add(fib, 0, &ip, 64, max_nhs[i]);
+               RTE_TEST_ASSERT(ret == 0,
+                       "Failed to add route to VRF 0 with nh_sz=%d\n", 
nh_sizes[i]);
+
+               ret = rte_fib6_vrf_add(fib, 1, &ip, 64, max_nhs[i] - 1);
+               RTE_TEST_ASSERT(ret == 0,
+                       "Failed to add route to VRF 1 with nh_sz=%d\n", 
nh_sizes[i]);
+
+               /* Lookup */
+               ips[0] = ip;
+               ips[1] = ip;
+               ips[0].a[15] = 0x11;
+               ips[1].a[15] = 0x22;
+
+               ret = rte_fib6_vrf_lookup_bulk(fib, vrf_ids, ips, next_hops, 2);
+               RTE_TEST_ASSERT(ret == 0, "VRF lookup failed with nh_sz=%d\n", 
nh_sizes[i]);
+
+               RTE_TEST_ASSERT(next_hops[0] == max_nhs[i],
+                       "Wrong nexthop for VRF 0 with nh_sz=%d\n", nh_sizes[i]);
+               RTE_TEST_ASSERT(next_hops[1] == max_nhs[i] - 1,
+                       "Wrong nexthop for VRF 1 with nh_sz=%d\n", nh_sizes[i]);
+
+               rte_fib6_free(fib);
+               fib = NULL;
+       }
+
+       return TEST_SUCCESS;
+}
+
 static struct unit_test_suite fib6_fast_tests = {
        .suite_name = "fib6 autotest",
        .setup = NULL,
@@ -611,6 +915,11 @@ static struct unit_test_suite fib6_fast_tests = {
        TEST_CASE(test_lookup),
        TEST_CASE(test_invalid_rcu),
        TEST_CASE(test_fib_rcu_sync_rw),
+       TEST_CASE(test_create_vrf),
+       TEST_CASE(test_vrf_add_del),
+       TEST_CASE(test_vrf_lookup),
+       TEST_CASE(test_vrf_isolation),
+       TEST_CASE(test_vrf_all_nh_sizes),
        TEST_CASES_END()
        }
 };
-- 
2.43.0

Reply via email to