On 2026/07/06 20:09, Volker Schlecht wrote:
> Fixes
> 
> Use-after-free / double-free in c-ares query-completion handling, remotely
> triggerable via ares_getaddrinfo() over TCP
> https://github.com/c-ares/c-ares/security/advisories/GHSA-6wfj-rwm7-3542

and here's a backport of the relevant commits for -stable

Index: Makefile
===================================================================
RCS file: /cvs/ports/net/libcares/Makefile,v
diff -u -p -r1.41 Makefile
--- Makefile    16 Jan 2026 16:06:46 -0000      1.41
+++ Makefile    7 Jul 2026 09:56:43 -0000
@@ -1,6 +1,7 @@
 COMMENT=       asynchronous resolver library

 V=             1.34.6
+REVISION=      0
 DISTNAME=      c-ares-${V}
 PKGNAME=       libcares-${V}
 CATEGORIES=    net devel
Index: patches/patch-src_lib_ares_process_c
===================================================================
RCS file: patches/patch-src_lib_ares_process_c
diff -N patches/patch-src_lib_ares_process_c
--- /dev/null   1 Jan 1970 00:00:00 -0000
+++ patches/patch-src_lib_ares_process_c        7 Jul 2026 09:56:43 -0000
@@ -0,0 +1,305 @@
+From d823199b688052dcdc1646f2ab4cb8c16b1c644a Mon Sep 17 00:00:00 2001
+From: Brad House <[email protected]>
+Date: Mon, 6 Jul 2026 11:19:36 -0400
+Subject: [PATCH] [Backport v1.34] Fix double-free in process_timeouts() and
+ consolidate requeue handling (#1237)
+
+Index: src/lib/ares_process.c
+--- src/lib/ares_process.c.orig
++++ src/lib/ares_process.c
+@@ -66,6 +66,11 @@ static void        end_query(ares_channel_t *channel,
+                              ares_query_t *query, ares_status_t status,
+                              ares_dns_record_t *dnsrec,
+                              ares_array_t **requeue);
++static ares_status_t ares_send_query_int(ares_server_t        
*requested_server,
++                                         ares_query_t         *query,
++                                         const ares_timeval_t *now,
++                                         ares_array_t        **requeue);
++static void          ares_detach_query(ares_query_t *query);
+
+ static void        ares_query_remove_from_conn(ares_query_t *query)
+ {
+@@ -573,6 +578,79 @@ static ares_status_t ares_append_endqueue(ares_array_t
+     dnsrec);
+ }
+
++/* Drain the deferred requeue/endqueue list iteratively.  All flush sites
++ * (read_answers(), process_timeouts(), and ares_send_query()) funnel through
++ * here so that:
++ *   1. Retries are re-dispatched by appending to this same list and looping,
++ *      rather than recursing ares_requeue_query() -> ares_send_query() until
++ *      the stack is exhausted (issue #1043).
++ *   2. A query is fully detached from all lookup lists before its callback is
++ *      invoked, so a reentrant ares_cancel() from within that callback cannot
++ *      find and free the same query, which would otherwise double-free it
++ *      (CVE-2026-33630 / GHSA-6wfj-rwm7-3542).
++ *
++ * On return the list has been fully processed, an empty-queue notification 
has
++ * been sent if appropriate, and *requeue has been destroyed and set to NULL. 
*/
++static ares_status_t ares_flush_requeue(ares_channel_t       *channel,
++                                        const ares_timeval_t *now,
++                                        ares_array_t        **requeue)
++{
++  ares_status_t status = ARES_SUCCESS;
++
++  if (requeue == NULL) {
++    return status;
++  }
++
++  while (*requeue != NULL && ares_array_len(*requeue) > 0) {
++    ares_query_t  *query;
++    ares_requeue_t entry;
++    ares_status_t  internal_status;
++
++    internal_status = ares_array_claim_at(&entry, sizeof(entry), *requeue, 0);
++    if (internal_status != ARES_SUCCESS) {
++      break; /* LCOV_EXCL_LINE: DefensiveCoding */
++    }
++
++    query = ares_htable_szvp_get_direct(channel->queries_by_qid, entry.qid);
++
++    if (entry.type == REQUEUE_REQUEUE) {
++      /* Query disappeared (e.g. a prior callback in this drain cancelled it) 
*/
++      if (query == NULL) {
++        continue;
++      }
++      /* Re-dispatch via the internal entrypoint so any further requeues are
++       * appended back onto this same list and drained by the loop above,
++       * rather than recursing. */
++      internal_status = ares_send_query_int(entry.server, query, now, 
requeue);
++      /* We only care about ARES_ENOMEM */
++      if (internal_status == ARES_ENOMEM) {
++        status = ARES_ENOMEM;
++      }
++    } else { /* REQUEUE_ENDQUERY */
++      if (query != NULL) {
++        /* Detach the query from all lookup lists BEFORE invoking the 
callback.
++         * Otherwise a reentrant ares_cancel() from within the callback would
++         * find this query still linked in all_queries/queries_by_qid, free 
it,
++         * and the ares_free_query() below would then double-free it. */
++        ares_detach_query(query);
++        query->callback(query->arg, entry.status, query->timeouts,
++                        entry.dnsrec);
++        ares_free_query(query);
++      }
++      ares_dns_record_destroy(entry.dnsrec);
++    }
++  }
++
++  /* Don't forget to send notification if queue emptied */
++  if (*requeue != NULL) {
++    ares_queue_notify_empty(channel);
++  }
++  ares_array_destroy(*requeue);
++  *requeue = NULL;
++
++  return status;
++}
++
+ static ares_status_t read_answers(ares_conn_t *conn, const ares_timeval_t 
*now)
+ {
+   ares_status_t   status;
+@@ -625,43 +703,11 @@ static ares_status_t read_answers(ares_conn_t *conn, c
+   }
+
+ cleanup:
+-
+-  /* Flush requeue */
+-  while (ares_array_len(requeue) > 0) {
+-    ares_query_t  *query;
+-    ares_requeue_t entry;
+-    ares_status_t  internal_status;
+-
+-    internal_status = ares_array_claim_at(&entry, sizeof(entry), requeue, 0);
+-    if (internal_status != ARES_SUCCESS) {
+-      break;
+-    }
+-
+-    query = ares_htable_szvp_get_direct(channel->queries_by_qid, entry.qid);
+-
+-    if (entry.type == REQUEUE_REQUEUE) {
+-      /* query disappeared */
+-      if (query == NULL) {
+-        continue;
+-      }
+-      internal_status = ares_send_query(entry.server, query, now);
+-      /* We only care about ARES_ENOMEM */
+-      if (internal_status == ARES_ENOMEM) {
+-        status = ARES_ENOMEM;
+-      }
+-    } else { /* REQUEUE_ENDQUERY */
+-      if (query != NULL) {
+-        query->callback(query->arg, entry.status, query->timeouts, 
entry.dnsrec);
+-        ares_free_query(query);
+-      }
+-      ares_dns_record_destroy(entry.dnsrec);
+-    }
++  /* Flush requeue - re-dispatch retries and invoke deferred callbacks
++   * iteratively and safely */
++  if (ares_flush_requeue(channel, now, &requeue) == ARES_ENOMEM) {
++    status = ARES_ENOMEM;
+   }
+-  /* Don't forget to send notification if queue emptied */
+-  if (requeue != NULL) {
+-    ares_queue_notify_empty(channel);
+-  }
+-  ares_array_destroy(requeue);
+
+   return status;
+ }
+@@ -696,7 +742,8 @@ static ares_status_t process_timeouts(ares_channel_t
+                                       const ares_timeval_t *now)
+ {
+   ares_slist_node_t *node;
+-  ares_status_t      status = ARES_SUCCESS;
++  ares_status_t      status  = ARES_SUCCESS;
++  ares_array_t      *requeue = NULL;
+
+   /* Just keep popping off the first as this list will re-sort as things come
+    * and go.  We don't want to try to rely on 'next' as some operation might
+@@ -715,13 +762,19 @@ static ares_status_t process_timeouts(ares_channel_t
+
+     conn = query->conn;
+     server_increment_failures(conn->server, query->using_tcp);
+-    status = ares_requeue_query(query, now, ARES_ETIMEOUT, ARES_TRUE, NULL,
+-      NULL);
++    status =
++      ares_requeue_query(query, now, ARES_ETIMEOUT, ARES_TRUE, NULL, 
&requeue);
+     if (status == ARES_ENOMEM) {
+       goto done;
+     }
+   }
+ done:
++  /* Flush requeue - re-dispatch retries and invoke deferred callbacks
++   * iteratively and safely */
++  if (ares_flush_requeue(channel, now, &requeue) == ARES_ENOMEM) {
++    status = ARES_ENOMEM;
++  }
++
+   if (status == ARES_ENOMEM) {
+     return ARES_ENOMEM;
+   }
+@@ -871,7 +924,7 @@ static ares_status_t process_answer(ares_channel_t
+   if (issue_might_be_edns(query->query, rdnsrec)) {
+     status = rewrite_without_edns(query);
+     if (status != ARES_SUCCESS) {
+-      end_query(channel, server, query, status, NULL, NULL);
++      end_query(channel, server, query, status, NULL, requeue);
+       goto cleanup;
+     }
+
+@@ -1262,10 +1315,46 @@ static ares_status_t ares_conn_query_write(ares_conn_t
+   return ares_conn_flush(conn);
+ }
+
++/* Public entrypoint.  Establishes a requeue list and drives
++ * ares_send_query_int() plus any retries/deferred callbacks it produces
++ * iteratively, so a chain of retryable failures can never recurse until the
++ * stack is exhausted (#1043). */
+ ares_status_t ares_send_query(ares_server_t *requested_server,
+                               ares_query_t *query, const ares_timeval_t *now)
+ {
+   ares_channel_t *channel = query->channel;
++  ares_array_t   *requeue = NULL;
++  unsigned short  qid     = query->qid;
++  ares_status_t   status;
++
++  status = ares_send_query_int(requested_server, query, now, &requeue);
++
++  /* Drain any retries/deferred callbacks this send produced.
++   * ares_flush_requeue() always fully processes and destroys the list (even 
on
++   * ENOMEM), and sends the empty-queue notification if needed. */
++  if (ares_flush_requeue(channel, now, &requeue) == ARES_ENOMEM) {
++    status = ARES_ENOMEM;
++  }
++
++  /* A retry may have been deferred (returning ARES_SUCCESS from the append)
++   * and then terminally failed while draining, in which case the query has
++   * been freed.  Do not dereference 'query' here.  If it is no longer tracked
++   * it ended, so don't report success to the caller (which would, e.g., cause
++   * ares_send_nolock() to write to a now-freed *qid). */
++  if (status == ARES_SUCCESS &&
++      ares_htable_szvp_get_direct(channel->queries_by_qid, qid) == NULL) {
++    status = ARES_ETIMEOUT;
++  }
++
++  return status;
++}
++
++static ares_status_t ares_send_query_int(ares_server_t        
*requested_server,
++                                         ares_query_t         *query,
++                                         const ares_timeval_t *now,
++                                         ares_array_t        **requeue)
++{
++  ares_channel_t *channel = query->channel;
+   ares_server_t  *server;
+   ares_conn_t    *conn;
+   size_t          timeplus;
+@@ -1286,7 +1375,7 @@ ares_status_t ares_send_query(ares_server_t *requested
+   }
+
+   if (server == NULL) {
+-    end_query(channel, server, query, ARES_ENOSERVER /* ? */, NULL, NULL);
++    end_query(channel, server, query, ARES_ENOSERVER /* ? */, NULL, requeue);
+     return ARES_ENOSERVER;
+   }
+
+@@ -1310,11 +1399,11 @@ ares_status_t ares_send_query(ares_server_t *requested
+       case ARES_ECONNREFUSED:
+       case ARES_EBADFAMILY:
+         server_increment_failures(server, query->using_tcp);
+-        return ares_requeue_query(query, now, status, ARES_TRUE, NULL, NULL);
++        return ares_requeue_query(query, now, status, ARES_TRUE, NULL, 
requeue);
+
+       /* Anything else is not retryable, likely ENOMEM */
+       default:
+-        end_query(channel, server, query, status, NULL, NULL);
++        end_query(channel, server, query, status, NULL, requeue);
+         return status;
+     }
+   }
+@@ -1328,7 +1417,7 @@ ares_status_t ares_send_query(ares_server_t *requested
+
+     case ARES_ENOMEM:
+       /* Not retryable */
+-      end_query(channel, server, query, status, NULL, NULL);
++      end_query(channel, server, query, status, NULL, requeue);
+       return status;
+
+     /* These conditions are retryable as they are server-specific
+@@ -1336,7 +1425,7 @@ ares_status_t ares_send_query(ares_server_t *requested
+     case ARES_ECONNREFUSED:
+     case ARES_EBADFAMILY:
+       handle_conn_error(conn, ARES_TRUE, status);
+-      status = ares_requeue_query(query, now, status, ARES_TRUE, NULL, NULL);
++      status = ares_requeue_query(query, now, status, ARES_TRUE, NULL, 
requeue);
+       if (status == ARES_ETIMEOUT) {
+         status = ARES_ECONNREFUSED;
+       }
+@@ -1344,7 +1433,7 @@ ares_status_t ares_send_query(ares_server_t *requested
+
+     default:
+       server_increment_failures(server, query->using_tcp);
+-      status = ares_requeue_query(query, now, status, ARES_TRUE, NULL, NULL);
++      status = ares_requeue_query(query, now, status, ARES_TRUE, NULL, 
requeue);
+       return status;
+   }
+
+@@ -1360,7 +1449,7 @@ ares_status_t ares_send_query(ares_server_t *requested
+     ares_slist_insert(channel->queries_by_timeout, query);
+   if (!query->node_queries_by_timeout) {
+     /* LCOV_EXCL_START: OutOfMemory */
+-    end_query(channel, server, query, ARES_ENOMEM, NULL, NULL);
++    end_query(channel, server, query, ARES_ENOMEM, NULL, requeue);
+     return ARES_ENOMEM;
+     /* LCOV_EXCL_STOP */
+   }
+@@ -1373,7 +1462,7 @@ ares_status_t ares_send_query(ares_server_t *requested
+
+   if (query->node_queries_to_conn == NULL) {
+     /* LCOV_EXCL_START: OutOfMemory */
+-    end_query(channel, server, query, ARES_ENOMEM, NULL, NULL);
++    end_query(channel, server, query, ARES_ENOMEM, NULL, requeue);
+     return ARES_ENOMEM;
+     /* LCOV_EXCL_STOP */
+   }
Index: patches/patch-src_lib_record_ares_dns_name_c
===================================================================
RCS file: patches/patch-src_lib_record_ares_dns_name_c
diff -N patches/patch-src_lib_record_ares_dns_name_c
--- /dev/null   1 Jan 1970 00:00:00 -0000
+++ patches/patch-src_lib_record_ares_dns_name_c        7 Jul 2026 09:56:43 
-0000
@@ -0,0 +1,95 @@
+From 5c8341ba6ff3a8e4e4dfd616f8ed0418838b8b7b Mon Sep 17 00:00:00 2001
+From: "c-ares-backport-gha[bot]"
+ <299594594+c-ares-backport-gha[bot]@users.noreply.github.com>
+Date: Sun, 5 Jul 2026 05:41:49 +0000
+Subject: [PATCH] [Backport v1.34] enforce 255 octet name limit when parsing
+ dns names (#1210)
+
+Index: src/lib/record/ares_dns_name.c
+--- src/lib/record/ares_dns_name.c.orig
++++ src/lib/record/ares_dns_name.c
+@@ -25,6 +25,18 @@
+  */
+ #include "ares_private.h"
+
++/* RFC 1035 3.1 limits a name to 255 octets.  We track presentation length
++ * (label octets plus one separator before each label after the first), which 
is
++ * up to ~2 octets looser than the strict wire limit and so never rejects a
++ * compliant name.  Shared by the read and write paths. */
++#define ARES_MAX_NAME_PRESENTATION_LEN 255
++
++/* A name of <= 255 octets holds at most 128 labels, so it can never 
legitimately
++ * require more than that many compression-pointer jumps.  Bounding the 
number of
++ * indirections stops a name built purely from pointers (which never adds 
label
++ * bytes, so the length cap never fires) from being walked without limit. */
++#define ARES_MAX_INDIRS 128
++
+ typedef struct {
+   char  *name;
+   size_t name_len;
+@@ -48,7 +60,7 @@ static ares_status_t ares_nameoffset_create(ares_llist
+   ares_nameoffset_t *off = NULL;
+
+   if (list == NULL || name == NULL || ares_strlen(name) == 0 ||
+-      ares_strlen(name) > 255) {
++      ares_strlen(name) > ARES_MAX_NAME_PRESENTATION_LEN) {
+     return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */
+   }
+
+@@ -335,7 +347,8 @@ static ares_status_t ares_split_dns_name(ares_array_t
+   }
+
+   /* Can't exceed maximum (unescaped) length */
+-  if (ares_array_len(labels) && total_len + ares_array_len(labels) - 1 > 255) 
{
++  if (ares_array_len(labels) &&
++      total_len + ares_array_len(labels) - 1 > 
ARES_MAX_NAME_PRESENTATION_LEN) {
+     status = ARES_EBADNAME;
+     goto done;
+   }
+@@ -538,6 +551,8 @@ ares_status_t ares_dns_name_parse(ares_buf_t *buf, cha
+   ares_status_t status;
+   ares_buf_t   *namebuf     = NULL;
+   size_t        label_start = ares_buf_get_position(buf);
++  size_t        name_len    = 0;
++  size_t        indir       = 0;
+
+   if (buf == NULL) {
+     return ARES_EFORMERR;
+@@ -609,6 +624,16 @@ ares_status_t ares_dns_name_parse(ares_buf_t *buf, cha
+         goto fail;
+       }
+
++      /* Bound the number of indirections.  A name made purely of pointers 
never
++       * adds label bytes, so the length cap below can't stop it; the
++       * strictly-decreasing rule alone still allows thousands of jumps per 
name.
++       * No legitimate <= 255 octet name needs more than 128 labels/jumps. */
++      indir++;
++      if (indir > ARES_MAX_INDIRS) {
++        status = ARES_EBADNAME;
++        goto fail;
++      }
++
+       /* First time we make a jump, save the current position */
+       if (save_offset == 0) {
+         save_offset = ares_buf_get_position(buf);
+@@ -631,6 +656,20 @@ ares_status_t ares_dns_name_parse(ares_buf_t *buf, cha
+     }
+
+     /* New label */
++
++    /* RFC 1035 3.1 limits a name to 255 octets.  Enforce it during
++     * decompression so labels reached through a chain of pointers can't 
expand a
++     * single name without bound.  Track the presentation length (label data 
plus
++     * the separator that precedes each label after the first), matching
++     * ares_split_dns_name() on the write side. */
++    if (name_len) {
++      name_len++;
++    }
++    name_len += c;
++    if (name_len > ARES_MAX_NAME_PRESENTATION_LEN) {
++      status = ARES_EBADNAME;
++      goto fail;
++    }
+
+     /* Labels are separated by periods */
+     if (ares_buf_len(namebuf) != 0 && name != NULL) {
Index: patches/patch-src_lib_record_ares_dns_parse_c
===================================================================
RCS file: patches/patch-src_lib_record_ares_dns_parse_c
diff -N patches/patch-src_lib_record_ares_dns_parse_c
--- /dev/null   1 Jan 1970 00:00:00 -0000
+++ patches/patch-src_lib_record_ares_dns_parse_c       7 Jul 2026 09:56:43 
-0000
@@ -0,0 +1,85 @@
+From e47c203f91cd8b749c8736bc18d75a31ffdec8f4 Mon Sep 17 00:00:00 2001
+From: jmestwa-coder <[email protected]>
+Date: Sun, 12 Apr 2026 23:50:54 +0530
+Subject: [PATCH] validate RR counts before preallocation to reject malformed
+ packets early (#1134)
+
+Index: src/lib/record/ares_dns_parse.c
+--- src/lib/record/ares_dns_parse.c.orig
++++ src/lib/record/ares_dns_parse.c
+@@ -920,30 +920,6 @@ static ares_status_t ares_dns_parse_header(ares_buf_t
+
+   (*dnsrec)->raw_rcode = rcode;
+
+-  if (*ancount > 0) {
+-    status =
+-      ares_dns_record_rr_prealloc(*dnsrec, ARES_SECTION_ANSWER, *ancount);
+-    if (status != ARES_SUCCESS) {
+-      goto fail; /* LCOV_EXCL_LINE: OutOfMemory */
+-    }
+-  }
+-
+-  if (*nscount > 0) {
+-    status =
+-      ares_dns_record_rr_prealloc(*dnsrec, ARES_SECTION_AUTHORITY, *nscount);
+-    if (status != ARES_SUCCESS) {
+-      goto fail; /* LCOV_EXCL_LINE: OutOfMemory */
+-    }
+-  }
+-
+-  if (*arcount > 0) {
+-    status =
+-      ares_dns_record_rr_prealloc(*dnsrec, ARES_SECTION_ADDITIONAL, *arcount);
+-    if (status != ARES_SUCCESS) {
+-      goto fail; /* LCOV_EXCL_LINE: OutOfMemory */
+-    }
+-  }
+-
+   return ARES_SUCCESS;
+
+ fail:
+@@ -1208,6 +1184,8 @@ static ares_status_t ares_dns_parse_buf(ares_buf_t *bu
+                                         ares_dns_record_t **dnsrec)
+ {
+   ares_status_t  status;
++  size_t         total_rr_count;
++  const size_t   min_rr_wire_len = 11;
+   unsigned short qdcount;
+   unsigned short ancount;
+   unsigned short nscount;
+@@ -1265,6 +1243,35 @@ static ares_status_t ares_dns_parse_buf(ares_buf_t *bu
+     status = ares_dns_parse_qd(buf, *dnsrec);
+     if (status != ARES_SUCCESS) {
+       goto fail;
++    }
++  }
++
++  total_rr_count = (size_t)ancount + (size_t)nscount + (size_t)arcount;
++  if (total_rr_count > ares_buf_len(buf) / min_rr_wire_len) {
++    status = ARES_EBADRESP;
++    goto fail;
++  }
++
++  if (ancount > 0) {
++    status = ares_dns_record_rr_prealloc(*dnsrec, ARES_SECTION_ANSWER, 
ancount);
++    if (status != ARES_SUCCESS) {
++      goto fail; /* LCOV_EXCL_LINE: OutOfMemory */
++    }
++  }
++
++  if (nscount > 0) {
++    status =
++      ares_dns_record_rr_prealloc(*dnsrec, ARES_SECTION_AUTHORITY, nscount);
++    if (status != ARES_SUCCESS) {
++      goto fail; /* LCOV_EXCL_LINE: OutOfMemory */
++    }
++  }
++
++  if (arcount > 0) {
++    status =
++      ares_dns_record_rr_prealloc(*dnsrec, ARES_SECTION_ADDITIONAL, arcount);
++    if (status != ARES_SUCCESS) {
++      goto fail; /* LCOV_EXCL_LINE: OutOfMemory */
+     }
+   }
+


> Index: Makefile
> ===================================================================
> RCS file: /cvs/ports/net/libcares/Makefile,v
> retrieving revision 1.41
> diff -u -p -r1.41 Makefile
> --- Makefile  16 Jan 2026 16:06:46 -0000      1.41
> +++ Makefile  6 Jul 2026 18:05:54 -0000
> @@ -1,12 +1,12 @@
>  COMMENT=     asynchronous resolver library
>  
> -V=           1.34.6
> +V=           1.34.7
>  DISTNAME=    c-ares-${V}
>  PKGNAME=     libcares-${V}
>  CATEGORIES=  net devel
>  SITES=               
> https://github.com/c-ares/c-ares/releases/download/v${V}/
>  
> -SHARED_LIBS +=  cares                4.4      # 2.19.0
> +SHARED_LIBS +=  cares                4.5      # 2.19.0
>  
>  HOMEPAGE=    https://c-ares.org/
>  
> Index: distinfo
> ===================================================================
> RCS file: /cvs/ports/net/libcares/distinfo,v
> retrieving revision 1.25
> diff -u -p -r1.25 distinfo
> --- distinfo  16 Jan 2026 16:06:46 -0000      1.25
> +++ distinfo  6 Jul 2026 18:05:54 -0000
> @@ -1,2 +1,2 @@
> -SHA256 (c-ares-1.34.6.tar.gz) = kS3XzDs+innFL9f7nA9Ozwqqc+Re/aiAJmotbia4TvU=
> -SIZE (c-ares-1.34.6.tar.gz) = 1017864
> +SHA256 (c-ares-1.34.7.tar.gz) = VW94HdGIrZMtyCY/7grTqrpnW0zY5U2GkIaBtDzj4yc=
> +SIZE (c-ares-1.34.7.tar.gz) = 1034872

Reply via email to