Hi!

I discovered that PDO::FETCH_KEY_PAIR does not work as documented.
Documented is:
'Fetch into an array where the 1st column is a key and all subsequent
columns are values'

Actually, all but two column result sets throw an error...

Then I thought about generalizing the fetch mode. Have 1..n 'key'
columns. If only one value is left, assign it as a scalar. If multiple
columns are left over, have it taken as FETCH_ASSOC.

Please find attached a humble suggestion to generalize FETCH_KEY_PAIR to
FETCH_KEYS, including an attribute to define the number of 'key' columns
(defaults to one).

The result should be fully backwards compatible.

Please give me your suggestions.

HPO

PS: My diff includes my 'old' FETCH_2D patch.
diff -u -r1.82.2.31.2.17.2.2 pdo_dbh.c
--- ext/pdo/pdo_dbh.c   7 Oct 2007 05:22:05 -0000       1.82.2.31.2.17.2.2
+++ ext/pdo/pdo_dbh.c   24 Nov 2007 21:19:47 -0000
@@ -784,6 +784,15 @@
                        return SUCCESS;
                }
                        
+               case PDO_ATTR_2D_NULLBASE:
+                       if( dbh->nullbase ) {
+                               efree( dbh->nullbase );
+                       }
+
+                       dbh->nullbase = (value ? estrdup( Z_STRVAL_P(value) ) : 
NULL );
+
+                       return SUCCESS;
+
                default:
                        ;
        }
@@ -872,6 +881,8 @@
                case PDO_ATTR_DEFAULT_FETCH_MODE:
                        RETURN_LONG(dbh->default_fetch_type);
 
+               case PDO_ATTR_2D_NULLBASE:
+                       RETURN_STRING(dbh->nullbase, 1);
        }
        
        if (!dbh->methods->get_attribute) {
@@ -1331,10 +1342,11 @@
        REGISTER_PDO_CLASS_CONST_LONG("PARAM_EVT_FETCH_POST",   
(long)PDO_PARAM_EVT_FETCH_POST);
        REGISTER_PDO_CLASS_CONST_LONG("PARAM_EVT_NORMALIZE",    
(long)PDO_PARAM_EVT_NORMALIZE);
 
-       REGISTER_PDO_CLASS_CONST_LONG("FETCH_LAZY", (long)PDO_FETCH_LAZY);
        REGISTER_PDO_CLASS_CONST_LONG("FETCH_ASSOC",(long)PDO_FETCH_ASSOC);
        REGISTER_PDO_CLASS_CONST_LONG("FETCH_NUM",  (long)PDO_FETCH_NUM);
        REGISTER_PDO_CLASS_CONST_LONG("FETCH_BOTH", (long)PDO_FETCH_BOTH);
+       REGISTER_PDO_CLASS_CONST_LONG("FETCH_2D",   (long)PDO_FETCH_2D);
+       REGISTER_PDO_CLASS_CONST_LONG("FETCH_LAZY", (long)PDO_FETCH_LAZY);
        REGISTER_PDO_CLASS_CONST_LONG("FETCH_OBJ",  (long)PDO_FETCH_OBJ);
        REGISTER_PDO_CLASS_CONST_LONG("FETCH_BOUND",(long)PDO_FETCH_BOUND);
        REGISTER_PDO_CLASS_CONST_LONG("FETCH_COLUMN",(long)PDO_FETCH_COLUMN);
@@ -1343,7 +1355,8 @@
        REGISTER_PDO_CLASS_CONST_LONG("FETCH_FUNC", (long)PDO_FETCH_FUNC);
        REGISTER_PDO_CLASS_CONST_LONG("FETCH_GROUP",(long)PDO_FETCH_GROUP);
        REGISTER_PDO_CLASS_CONST_LONG("FETCH_UNIQUE",(long)PDO_FETCH_UNIQUE);
-       
REGISTER_PDO_CLASS_CONST_LONG("FETCH_KEY_PAIR",(long)PDO_FETCH_KEY_PAIR);
+       REGISTER_PDO_CLASS_CONST_LONG("FETCH_KEY_PAIR",(long)PDO_FETCH_KEYS);
+       REGISTER_PDO_CLASS_CONST_LONG("FETCH_KEYS",(long)PDO_FETCH_KEYS);
        
REGISTER_PDO_CLASS_CONST_LONG("FETCH_CLASSTYPE",(long)PDO_FETCH_CLASSTYPE);
 #if PHP_MAJOR_VERSION > 5 || PHP_MINOR_VERSION >= 1
        
REGISTER_PDO_CLASS_CONST_LONG("FETCH_SERIALIZE",(long)PDO_FETCH_SERIALIZE);
@@ -1372,7 +1385,7 @@
        
REGISTER_PDO_CLASS_CONST_LONG("ATTR_MAX_COLUMN_LEN",(long)PDO_ATTR_MAX_COLUMN_LEN);
        
REGISTER_PDO_CLASS_CONST_LONG("ATTR_EMULATE_PREPARES",(long)PDO_ATTR_EMULATE_PREPARES);
        
REGISTER_PDO_CLASS_CONST_LONG("ATTR_DEFAULT_FETCH_MODE",(long)PDO_ATTR_DEFAULT_FETCH_MODE);
-       
+       REGISTER_PDO_CLASS_CONST_LONG("ATTR_2D_NULLBASE", 
(long)PDO_ATTR_2D_NULLBASE);  
        REGISTER_PDO_CLASS_CONST_LONG("ERRMODE_SILENT", 
(long)PDO_ERRMODE_SILENT);
        REGISTER_PDO_CLASS_CONST_LONG("ERRMODE_WARNING",        
(long)PDO_ERRMODE_WARNING);
        REGISTER_PDO_CLASS_CONST_LONG("ERRMODE_EXCEPTION",      
(long)PDO_ERRMODE_EXCEPTION);
@@ -1472,13 +1485,17 @@
                dbh->methods->rollback(dbh TSRMLS_CC);
                dbh->in_txn = 0;
        }
-
+       
        if (dbh->properties) {
                zend_hash_destroy(dbh->properties);
                efree(dbh->properties);
                dbh->properties = NULL;
        }
        
+       if( dbh->nullbase ) {
+               efree( dbh->nullbase );
+       }
+
        if (!dbh->is_persistent) {
                dbh_free(dbh TSRMLS_CC);
        } else if (dbh->methods && dbh->methods->persistent_shutdown) {
@@ -1496,6 +1513,7 @@
        memset(dbh, 0, sizeof(*dbh));
        dbh->ce = ce;
        dbh->refcount = 1;
+       dbh->nullbase = estrdup( "" );
        ALLOC_HASHTABLE(dbh->properties);
        zend_hash_init(dbh->properties, 0, NULL, ZVAL_PTR_DTOR, 0);
        zend_hash_copy(dbh->properties, &ce->default_properties, 
(copy_ctor_func_t) zval_add_ref, (void *) &tmp, sizeof(zval *));
diff -u -r1.118.2.38.2.24.2.7 pdo_stmt.c
--- ext/pdo/pdo_stmt.c  20 Nov 2007 23:12:17 -0000      1.118.2.38.2.24.2.7
+++ ext/pdo/pdo_stmt.c  24 Nov 2007 21:19:47 -0000
@@ -886,6 +886,77 @@
 }
 /* }}} */
 
+/* recursively create array out of a column */
+static void do_fetch_keys_recursive( pdo_stmt_t *stmt, zval *basearray, zval 
*index, int i ) {
+       zval *val = NULL;
+       zval **valp;
+       zval *newval;
+       int islong;
+       int indexlong;
+       char* indexstr;
+       int indexlen;
+       int isfound;
+       
+       MAKE_STD_ZVAL(newval);
+       fetch_value(stmt, newval, i, NULL TSRMLS_CC);
+
+       if( index ) {
+               if( (islong = (Z_TYPE_P(index) == IS_LONG)) ) {
+                       indexlong = Z_LVAL_P(index);
+               }
+               else {
+                       convert_to_string(index);
+                       indexstr = Z_STRVAL_P(index);
+                       indexlen = Z_STRLEN_P(index);
+               }
+       }
+       
+       if( i < stmt->fetch.num_keys ) {
+               /* we stay in the recursion */
+               isfound = (( islong
+                       ? zend_hash_index_find(Z_ARRVAL_P(basearray), 
indexlong, (void**)&valp)
+                       : zend_hash_find(Z_ARRVAL_P(basearray), indexstr, 
indexlen+1, (void**)&valp)
+                       ) == SUCCESS);
+               if( !isfound ) {
+                       MAKE_STD_ZVAL(val);
+                       array_init(val);
+               }
+               do_fetch_keys_recursive( stmt, (isfound ? *valp : val), newval, 
i+1 );                  
+       }
+       else {
+               /* more than one column left */
+               if( stmt->column_count - stmt->fetch.num_keys > 1 ) {
+                       if( i == stmt->fetch.num_keys ) {
+                               MAKE_STD_ZVAL(val);
+                               array_init(val);
+                       }
+                       else {
+                               valp = &basearray;
+                       }
+                       add_assoc_zval( (val ? val : *valp), 
stmt->columns[i].name, newval );                                           
+
+                       if( (i+1) < stmt->column_count ) {
+                               do_fetch_keys_recursive( stmt, (val ? val : 
*valp), NULL, i+1 );
+                       }
+               }
+               /* only one column left */
+               else {
+                       val = newval;
+               }
+       }
+       
+       if( index && val ) {
+               if( islong ) {
+                       add_index_zval( basearray, indexlong, val );
+               } else {
+                       add_assoc_zval( basearray, indexstr, val );
+               }
+       }
+/*     if( newval  != val ) {
+               zval_ptr_dtor(&newval);
+       }
+*/}
+
 /* perform a fetch.  If do_bind is true, update any bound columns.
  * If return_value is not null, store values into it according to HOW. */
 static int do_fetch(pdo_stmt_t *stmt, int do_bind, zval *return_value,
@@ -894,7 +965,8 @@
        int flags = how & PDO_FETCH_FLAGS, idx, old_arg_count = 0;
        zend_class_entry *ce = NULL, *old_ce = NULL;
        zval grp_val, *grp, **pgrp, *retval, *old_ctor_args = NULL;
-
+       zval *nullbase = NULL;
+       
        if (how == PDO_FETCH_USE_DEFAULT) {
                how = stmt->default_fetch_type;
        }
@@ -925,6 +997,10 @@
                        case PDO_FETCH_BOTH:
                        case PDO_FETCH_NUM:
                        case PDO_FETCH_NAMED:
+                       case PDO_FETCH_2D:
+                       case PDO_FETCH_2D_NUM:
+                       case PDO_FETCH_2D_ASSOC:
+                       case PDO_FETCH_2D_BOTH:
                                if (!return_all) {
                                        ALLOC_HASHTABLE(return_value->value.ht);
                                        zend_hash_init(return_value->value.ht, 
stmt->column_count, NULL, ZVAL_PTR_DTOR, 0);
@@ -934,9 +1010,9 @@
                                }
                                break;
 
-                       case PDO_FETCH_KEY_PAIR:
-                               if (stmt->column_count != 2) {
-                                       pdo_raise_impl_error(stmt->dbh, stmt, 
"HY000", "PDO::FETCH_KEY_PAIR fetch mode requires the result set to contain 
extactly 2 columns." TSRMLS_CC);
+                       case PDO_FETCH_KEYS:
+                               if (stmt->column_count <= stmt->fetch.num_keys) 
{
+                                       pdo_raise_impl_error(stmt->dbh, stmt, 
"HY000", "PDO::FETCH_KEYS needs at least one value column." TSRMLS_CC);
                                        return 0;
                                }
                                if (!return_all) {
@@ -1047,7 +1123,7 @@
                                return 0;
                }
                
-               if (return_all && how != PDO_FETCH_KEY_PAIR) {
+               if (return_all && how != PDO_FETCH_KEYS) {
                        INIT_PZVAL(&grp_val);
                        fetch_value(stmt, &grp_val, i, NULL TSRMLS_CC);
                        convert_to_string(&grp_val);
@@ -1060,27 +1136,66 @@
 
                for (idx = 0; i < stmt->column_count; i++, idx++) {
                        zval *val;
+                       zval *base2d;
                        MAKE_STD_ZVAL(val);
                        fetch_value(stmt, val, i, NULL TSRMLS_CC);
 
                        switch (how) {
+                               case PDO_FETCH_2D:
+                               case PDO_FETCH_2D_NUM:
+                               case PDO_FETCH_2D_ASSOC:
+                               case PDO_FETCH_2D_BOTH:
+                                       if( !nullbase && ((how & 
PDO_FETCH_BOTH) || !stmt->columns[i].relname) ) {
+                                               if( stmt->dbh->nullbase ) {
+                                                       MAKE_STD_ZVAL(nullbase);
+                                                       array_init(nullbase);
+                                                       
add_assoc_zval(return_value, stmt->dbh->nullbase, nullbase); 
+                                               }
+                                               else {
+                                                       nullbase = return_value;
+                                               }
+                                       }
+
+                                       if( stmt->columns[i].relname ) { 
+                                               zval **curr_val = NULL;
+                                               if 
(zend_hash_find(Z_ARRVAL_P(return_value), stmt->columns[i].relname,
+                                                                       
stmt->columns[i].relnamelen+1,
+                                                                       
(void**)&curr_val) == SUCCESS) {
+                                                       if( Z_TYPE_PP(curr_val) 
!= IS_ARRAY ) {
+                                                               /* TODO: ERROR 
OUT OR NOT? */
+                                                               return 0;
+                                                       }
+                                                       base2d = *curr_val;
+                                               }
+                                               else {
+                                                       MAKE_STD_ZVAL(base2d);
+                                                       array_init(base2d);
+                                                       
add_assoc_zval(return_value, stmt->columns[i].relname, base2d);
+                                               }
+                                       }
+                                       else {
+                                               base2d = nullbase;      
+                                       }
+
+                                       add_assoc_zval(base2d, 
stmt->columns[i].name, val);
+
+                                       if( how & PDO_FETCH_ASSOC ) {
+                                               Z_ADDREF_P(val);
+                                               add_assoc_zval(nullbase, 
stmt->columns[i].name, val); 
+                                       }       
+                                       if( how & PDO_FETCH_NUM ) {
+                                               Z_ADDREF_P(val);
+                                               add_next_index_zval(nullbase, 
val); 
+                                       }
+                                       break;
+
                                case PDO_FETCH_ASSOC:
                                        add_assoc_zval(return_value, 
stmt->columns[i].name, val);
                                        break;
                                        
-                               case PDO_FETCH_KEY_PAIR:
+                               case PDO_FETCH_KEYS:
                                        {
-                                               zval *tmp;
-                                               MAKE_STD_ZVAL(tmp);
-                                               fetch_value(stmt, tmp, ++i, 
NULL TSRMLS_CC);
-
-                                               if (Z_TYPE_P(val) == IS_LONG) {
-                                                       
zend_hash_index_update((return_all ? Z_ARRVAL_P(return_all) : 
Z_ARRVAL_P(return_value)), Z_LVAL_P(val), &tmp, sizeof(zval *), NULL);
-                                               } else {
-                                                       convert_to_string(val);
-                                                       
zend_symtable_update((return_all ? Z_ARRVAL_P(return_all) : 
Z_ARRVAL_P(return_value)), Z_STRVAL_P(val), Z_STRLEN_P(val) + 1, &tmp, 
sizeof(zval *), NULL);
-                                               }
-                                               zval_ptr_dtor(&val);
+                                               do_fetch_keys_recursive( stmt, 
(return_all ? return_all : return_value), val, i+1 );
                                                return 1;
                                        }
                                        break;
@@ -1539,6 +1654,15 @@
                }
                break;
 
+       case PDO_FETCH_KEYS:
+               if( ZEND_NUM_ARGS() == 2 ) {
+                       stmt->fetch.num_keys = Z_LVAL_P(arg2);
+               }
+               else {
+                       stmt->fetch.num_keys = 1;
+               }
+               break;
+               
        default:
                if (ZEND_NUM_ARGS() > 1) {
                        pdo_raise_impl_error(stmt->dbh, stmt, "HY000", 
"Extraneous additional parameters" TSRMLS_CC);
@@ -1553,8 +1677,8 @@
        if (!error)     {
                PDO_STMT_CLEAR_ERR();
                MAKE_STD_ZVAL(data);
-               if (    (how & PDO_FETCH_GROUP) || how == PDO_FETCH_KEY_PAIR || 
-                       (how == PDO_FETCH_USE_DEFAULT && 
stmt->default_fetch_type == PDO_FETCH_KEY_PAIR)
+               if (    (how & PDO_FETCH_GROUP) || how == PDO_FETCH_KEYS || 
+                       (how == PDO_FETCH_USE_DEFAULT && 
stmt->default_fetch_type == PDO_FETCH_KEYS)
                ) {
                        array_init(return_value);
                        return_all = return_value;
@@ -1571,7 +1695,7 @@
                        do {
                                MAKE_STD_ZVAL(data);
                        } while (do_fetch(stmt, TRUE, data, how, 
PDO_FETCH_ORI_NEXT, 0, return_all TSRMLS_CC));
-               } else if (how == PDO_FETCH_KEY_PAIR || (how == 
PDO_FETCH_USE_DEFAULT && stmt->default_fetch_type == PDO_FETCH_KEY_PAIR)) {
+               } else if (how == PDO_FETCH_KEYS || (how == 
PDO_FETCH_USE_DEFAULT && stmt->default_fetch_type == PDO_FETCH_KEYS)) {
                        while (do_fetch(stmt, TRUE, data, how, 
PDO_FETCH_ORI_NEXT, 0, return_all TSRMLS_CC));
                } else {
                        array_init(return_value);
@@ -1922,7 +2046,10 @@
                case PDO_FETCH_OBJ:
                case PDO_FETCH_BOUND:
                case PDO_FETCH_NAMED:
-               case PDO_FETCH_KEY_PAIR:
+               case PDO_FETCH_2D:
+               case PDO_FETCH_2D_NUM:
+               case PDO_FETCH_2D_ASSOC:
+               case PDO_FETCH_2D_BOTH:
                        break;
 
                case PDO_FETCH_COLUMN:
@@ -1988,6 +2115,18 @@
                        zend_objects_store_add_ref(stmt->fetch.into TSRMLS_CC);
                        break;
                
+               case PDO_FETCH_KEYS:
+                       if( argc != 2 ) {
+                               stmt->fetch.num_keys = 1;
+                       }
+                       else if( Z_TYPE_PP(args[skip+1]) == IS_LONG ) {
+                               stmt->fetch.num_keys = Z_LVAL_PP(args[skip+1]); 
+                       }
+                       else {
+                               pdo_raise_impl_error(stmt->dbh, stmt, "HY000", 
"num_keys must be integer" TSRMLS_CC);
+                       }
+                       break;
+                       
                default:
                        pdo_raise_impl_error(stmt->dbh, stmt, "22003", "Invalid 
fetch mode specified" TSRMLS_CC);
                        goto fail_out;
@@ -2348,6 +2487,10 @@
                                efree(cols[i].name);
                                cols[i].name = NULL;
                        }
+                       if (cols[i].relname) {
+                               efree(cols[i].relname);
+                               cols[i].relname = NULL;
+                       }
                }
                efree(stmt->columns);
                stmt->columns = NULL;
diff -u -r1.66.2.11.2.6.2.1 php_pdo_driver.h
--- ext/pdo/php_pdo_driver.h    27 Sep 2007 18:00:42 -0000      
1.66.2.11.2.6.2.1
+++ ext/pdo/php_pdo_driver.h    24 Nov 2007 21:19:47 -0000
@@ -79,10 +79,14 @@
 
 enum pdo_fetch_type {
        PDO_FETCH_USE_DEFAULT,
+       PDO_FETCH_ASSOC,        /* BEGIN */
+       PDO_FETCH_NUM,          /* All values from BEGIN to END are specially */
+       PDO_FETCH_BOTH,         /* arranged to be bitwise combineable. */
+       PDO_FETCH_2D,           /* e.g. ASSOC | NUM = BOTH */
+       PDO_FETCH_2D_ASSOC,     /* e.g. 2D | ASSOC  = 2D_ASSOC */
+       PDO_FETCH_2D_NUM,
+       PDO_FETCH_2D_BOTH,      /* END */
        PDO_FETCH_LAZY,
-       PDO_FETCH_ASSOC,
-       PDO_FETCH_NUM,
-       PDO_FETCH_BOTH,
        PDO_FETCH_OBJ,
        PDO_FETCH_BOUND, /* return true/false only; rely on bound columns */
        PDO_FETCH_COLUMN,       /* fetch a numbered column only */
@@ -90,7 +94,7 @@
        PDO_FETCH_INTO,         /* fetch row into an existing object */
        PDO_FETCH_FUNC,         /* fetch into function and return its result */
        PDO_FETCH_NAMED,    /* like PDO_FETCH_ASSOC, but can handle duplicate 
names */
-       PDO_FETCH_KEY_PAIR,     /* fetch into an array where the 1st column is 
a key and all subsequent columns are values */
+       PDO_FETCH_KEYS,     /* fetch into an array where the first column(s) 
are keys and all subsequent columns are values */
        PDO_FETCH__MAX /* must be last */
 };
 
@@ -124,6 +128,7 @@
        PDO_ATTR_CURSOR_NAME,           /* name a cursor for use in "WHERE 
CURRENT OF <name>" */
        PDO_ATTR_CURSOR,                        /* cursor type */
        PDO_ATTR_ORACLE_NULLS,          /* convert empty strings to NULL */
+       PDO_ATTR_CONNECTION_LAZY,  /* only connect to database upon use */
        PDO_ATTR_PERSISTENT,            /* pconnect style connection */
        PDO_ATTR_STATEMENT_CLASS,       /* array(classname, array(ctor_args)) 
to specify the class of the constructed statement */
        PDO_ATTR_FETCH_TABLE_NAMES, /* include table names in the column names, 
where available */
@@ -133,6 +138,7 @@
        PDO_ATTR_MAX_COLUMN_LEN,        /* make database calculate maximum 
length of data found in a column */
        PDO_ATTR_DEFAULT_FETCH_MODE, /* Set the default fetch mode */
        PDO_ATTR_EMULATE_PREPARES,  /* use query emulation rather than native */
+       PDO_ATTR_2D_NULLBASE,           /* array name for not further qualified 
columns */
 
        /* this defines the start of the range for driver specific options.
         * Drivers should define their own attribute constants beginning with 
this
@@ -470,6 +476,9 @@
         * equal 32 */
        unsigned _reserved_flags:21;
 
+       /* 2d nullbase */
+       char *nullbase;
+
        /* data source string used to open this handle */
        const char *data_source;
        unsigned long data_source_len;
@@ -508,6 +517,8 @@
 
 /* describes a column */
 struct pdo_column_data {
+       char *relname;
+       int relnamelen;
        char *name;
        int namelen;
        unsigned long maxlen;
@@ -620,6 +631,7 @@
                        zend_fcall_info_cache fcc;
                        zval **values;              /* freed */
                } func;
+               int num_keys;
                zval *into;
        } fetch;
 
diff -u -r1.18.2.1.2.5.2.8 firebird_statement.c
--- ext/pdo_firebird/firebird_statement.c       19 Nov 2007 21:55:30 -0000      
1.18.2.1.2.5.2.8
+++ ext/pdo_firebird/firebird_statement.c       24 Nov 2007 21:19:48 -0000
@@ -164,8 +164,10 @@
        colname_len = (S->H->fetch_table_names && var->relname_length)
                                        ? (var->aliasname_length + 
var->relname_length + 1)
                                        : (var->aliasname_length);
-       col->precision = -var->sqlscale;
+       col->precision = -var->sqlscale; 
        col->maxlen = var->sqllen;
+       col->relname = (var->relname_length ? estrndup(var->relname, 
var->relname_length) : NULL);
+       col->relnamelen = var->relname_length;
        col->namelen = colname_len;
        col->name = cp = emalloc(colname_len + 1);
        if (colname_len > var->aliasname_length) {
diff -u -r1.48.2.14.2.6 mysql_statement.c
--- ext/pdo_mysql/mysql_statement.c     17 May 2007 15:12:23 -0000      
1.48.2.14.2.6
+++ ext/pdo_mysql/mysql_statement.c     24 Nov 2007 21:19:48 -0000
@@ -454,6 +454,8 @@
                cols[i].maxlen = S->fields[i].length;
                cols[i].namelen = namelen;
                cols[i].name = estrndup(S->fields[i].name, namelen);
+               cols[i].relnamelen = strlen(S->fields[i].table);  /* TODO: 
WHERE are names efree'd? */
+               cols[i].relname = (cols[i].relnamelen ? 
estrndup(S->fields[i].table, cols[i].relnamelen) : NULL);
                cols[i].param_type = PDO_PARAM_STR;
        }
        return 1;

-- 
PHP Internals - PHP Runtime Development Mailing List
To unsubscribe, visit: http://www.php.net/unsub.php

Reply via email to