On Thu, Feb 14, 2008 at 11:38:50AM +0100, Markus Liljergren wrote:
> I'm using pure MD5, which is probably why it fails. I would very much 
> appreciate that patch.
>

Here's a new diff that merges the previous patch from FreeBSD with
another one i found that provides a few new things:
 - plain MD5 password encryption support
 - the ability to specify host:port or socket-path
 - additional where clause for sql statements
 - better logging of errors

Please test and provide feedback as i didn't do more than building it.

Kind regards,
Simon


Index: Makefile
===================================================================
RCS file: /cvs/ports/www/mod_auth_mysql/Makefile,v
retrieving revision 1.12
diff -u -p -r1.12 Makefile
--- Makefile    15 Sep 2007 20:38:22 -0000      1.12
+++ Makefile    15 Feb 2008 15:17:50 -0000
@@ -4,7 +4,7 @@ COMMENT=        Apache MySQL authentication mod
 
 VERSION=       3.2
 DISTNAME=      mod_auth_mysql-${VERSION}
-PKGNAME=       ${DISTNAME}p2
+PKGNAME=       ${DISTNAME}p3
 CATEGORIES=    www
 
 HOMEPAGE=      http://sourceforge.net/projects/mod-auth-mysql
Index: patches/patch-Makefile
===================================================================
RCS file: /cvs/ports/www/mod_auth_mysql/patches/patch-Makefile,v
retrieving revision 1.1.1.1
diff -u -p -r1.1.1.1 patch-Makefile
--- patches/patch-Makefile      15 Sep 2002 19:28:36 -0000      1.1.1.1
+++ patches/patch-Makefile      15 Feb 2008 15:17:50 -0000
@@ -1,7 +1,7 @@
 $OpenBSD: patch-Makefile,v 1.1.1.1 2002/09/15 19:28:36 jakob Exp $
---- Makefile   Mon Sep 10 15:12:08 2001
-+++ Makefile   Tue Sep 10 20:17:09 2002
-@@ -3,7 +3,7 @@
+--- Makefile.orig      Mon Sep 10 16:12:08 2001
++++ Makefile   Fri Feb 15 16:14:36 2008
+@@ -3,7 +3,7 @@ APXSFLAGS =
  DSO   = mod_auth_mysql.so
  SRCS  = mod_auth_mysql.c
  HDRS  = mod_auth_mysql.h
Index: patches/patch-README
===================================================================
RCS file: patches/patch-README
diff -N patches/patch-README
--- /dev/null   1 Jan 1970 00:00:00 -0000
+++ patches/patch-README        15 Feb 2008 15:17:50 -0000
@@ -0,0 +1,45 @@
+$OpenBSD$
+--- README.orig        Mon Sep 10 16:11:37 2001
++++ README     Thu Feb 14 13:32:37 2008
+@@ -193,3 +193,41 @@ Author
+ ------
+ 
+ Email:  J. R. Westmoreland <[EMAIL PROTECTED]>
++
++
++
++===================================================================
++Additional feature
++------------------
++I added next feature to mod_auth_mysql-2.20:
++
++1. socket/port specify
++2. where clauses
++3. MD5
++4. to write error log
++5. little change Makefile.in
++
++Email: [EMAIL PROTECTED]
++URL: http://www.softagency.co.jp/mysql/modauth.html
++
++
++Change log of this patch
++
++2001-10-23
++  change: select count(*) -> select count(id)
++  add: write to apache error log file.
++  [EMAIL PROTECTED]
++
++2001-10-03
++  change: note_basic_auth_failure() -> ap_note_basic_auth_failure()
++  add: MD5 auth
++  [EMAIL PROTECTED]
++
++2001-04-20
++  add: WHERE clause
++  [EMAIL PROTECTED]
++
++2000-04
++  add: port/socket
++  [EMAIL PROTECTED]
++
Index: patches/patch-USAGE
===================================================================
RCS file: /cvs/ports/www/mod_auth_mysql/patches/patch-USAGE,v
retrieving revision 1.1.1.1
diff -u -p -r1.1.1.1 patch-USAGE
--- patches/patch-USAGE 15 Sep 2002 19:28:36 -0000      1.1.1.1
+++ patches/patch-USAGE 15 Feb 2008 15:17:50 -0000
@@ -1,12 +1,119 @@
 $OpenBSD: patch-USAGE,v 1.1.1.1 2002/09/15 19:28:36 jakob Exp $
---- USAGE      Mon Sep 10 15:11:37 2001
-+++ USAGE      Tue Sep 10 21:17:21 2002
-@@ -31,7 +31,7 @@
+--- USAGE.orig Thu Feb 14 10:37:00 2008
++++ USAGE      Thu Feb 14 13:32:37 2008
+@@ -28,6 +28,7 @@ can skip to the next phase.  Otherwise:
+     NOTE:  You *don't* have to have this table in a seperate database, you
+     can skip creating a new database and use an existing database if it fits
+     your needs.
++
  2.  Create the auth table, e.g.:
      prompt> mysql http_auth
      mysql> create table mysql_auth (
--        ->   username char(25),
-+        ->   username char(25) not null,
-         ->   passwd char(25),
-         ->   groups char(25),
-         ->   primary key (username)
+@@ -48,6 +49,7 @@ can skip to the next phase.  Otherwise:
+              that, you should have one row in the username/passwd table, and
+              multiple rows in the username/group table, one for each group
+              the user is in.
++
+ 3.  Insert the information into the table.  Both the username and group fields
+     are plaintext, whereas the password field should contain standard UNIX DES
+     encrypted passwords (this can be overriden using a directive as well, but
+@@ -62,40 +64,58 @@ Telling apache to protect the page using that informat
+     server, and/or you need to specify a password for that user, you'd need
+     to add the following line somewhere in your httpd.conf (doesn't really
+     matter where):
++
+     Auth_MySQL_Info <host> <user> <password>
++
+     This information can *only* be specified in the server's httpd.conf, since
+     it's used server-wide.
++
++    you can specify socket name or port number in <host>.
++    ex.1: Auth_MySQL_Info 'localhost:/tmp/mysql.sock' <user> <password>
++    ex.2: Auth_MySQL_Info 'remotesrv:3333' <user> <password>
++
+ 2.  If you're going to use mainly one MySQL database for all of your pages,
+     you should probably add the following line to your httpd.conf as well:
++
+     Auth_MySQL_General_DB <database_name>
++
+     The database can be set on a per-directory basis using a different
+     directive in .htaccess, as mentioned later in this file.
++
+ 3.  Create (or update) a file named .htaccess inside the directory you would
+     like to protect.  Here are a few simple .htaccess files (full
+     documentation about the various possible non-MySQL-auth specific 
directives
+     can be obtained from the apache docs):
++
+     
+ (I)  Protect your company's financial information (not recommended to put on
+      the web:) to any user that's in the SQL auth table:
++
+ AuthName        My Company's Financial Information   <-- the realm name, use 
some informative name
+ AuthType Basic                                       <-- keep it that way
+ require valid-user                                   <-- allow any valid user 
to access
+ 
++
+ (II)  Allow access only to specific users:
++
+ AuthName        My Company's Financial Information   <-- the realm name, use 
some informative name
+ AuthType Basic                                       <-- keep it that way
+ require user johndoe devnull                         <-- let only johndoe and 
devnull access
+ 
++
+ (III) Allow only members of group 'executives' access the information:
++
+ AuthName        My Company's Financial Information   <-- the realm name, use 
some informative name
+ AuthType Basic                                       <-- keep it that way
+ require group executives                             <-- allow only members 
of this group to access
+ 
+ Note that with Apache 1.3, you would have to encapsulate the AuthName
+ with double quotes if it contains spaces, e.g.
++
+ AuthName        "My Company's Financial Information"
+ 
+ 
++
+ 4.  Take a look at the following directives, and see if you need to
+     use any of them:
+ 
+@@ -130,16 +150,25 @@ Auth_MySQL_Empty_Passwords on/off
+     the page by just specifying their username without any password checking.
+     If this is 'Off', they would be denied access.  Default:  On.
+     
+-Auth_MySQL_Encryption_Types [Plaintext, Crypt_DES, Crypt_MD5, MySQL]
++Auth_MySQL_Encryption_Types [Plaintext, Crypt_DES, Crypt_MD5, MySQL, MD5]
+     This directive tells the authentication module which encryption type(s)
+     to use.  It overrides the Auth_MySQL_Scrambled_Passwords and
+     Auth_MySQL_Encrypted_Passwords directives if it appears after them.
+     More than one encryption type may be specified, to instruct the module to
+     check each password through more than one encryption scheme.  For example,
++
+     Auth_MySQL_Encryption_Types Plaintext Crypt_DES
++
+     will instruct the module to check each password both as-is, and through
+     DES crypt.
+ 
++    Crypt_MD5: if your system support crypt function which can handle md5,
++    apache compare password strings by using md5.
++
++    MD5: if you choise MD5, your passwd field must have md5 encrypted strings.
++    in this case, md5 password are stored into MySQL, and,
++    your system does not need to have crypt_md5 function.
++
+ Auth_MySQL_Encrypted_Passwords on/off
+     Whether or not to use standard UNIX DES encrypted passwords.  If turned
+     on, the module expects the password field to contain standard UNIX DES
+@@ -178,3 +207,13 @@ Auth_MYSQL on/off
+     authentication modules (e.g. the flatfile auth module).  If it's on,
+     and a database name was specified - the MySQL module will be used for
+     authentication.
++
++
++Auth_MySQL_Where "strings..."
++    if you set:
++
++       Auth_MySQL_Where "active='Y'"
++
++    then, mod_mysql send next SQL statment to MySQL server:
++
++       SELECT passwd FROM mysql_auth WHERE username='id' AND active='Y'
Index: patches/patch-mod_auth_mysql_c
===================================================================
RCS file: patches/patch-mod_auth_mysql_c
diff -N patches/patch-mod_auth_mysql_c
--- /dev/null   1 Jan 1970 00:00:00 -0000
+++ patches/patch-mod_auth_mysql_c      15 Feb 2008 15:17:50 -0000
@@ -0,0 +1,318 @@
+$OpenBSD$
+--- mod_auth_mysql.c.orig      Mon Sep 10 16:12:08 2001
++++ mod_auth_mysql.c   Fri Feb 15 16:01:37 2008
+@@ -10,6 +10,34 @@
+  * and Brent Metz <[EMAIL PROTECTED]>
+  *
+  * Please read the README and USAGE for further information.
++
++** changes (2.20) **
++2001-04-20
++  add: WHERE clause
++  [EMAIL PROTECTED]
++
++2001-10-03
++  change: note_basic_auth_failure() -> ap_note_basic_auth_failure()
++  add: MD5 auth
++  [EMAIL PROTECTED]
++
++2001-10-23
++  change: select count(*) -> select count(id)
++  add: write to apache error log file.
++  [EMAIL PROTECTED]
++
++2002-07-03
++  fix: Auth_MySQL_Empty_Passwords could not work correctly. original bug. 
++  change: Auth_MySQL_Empty_Passwords default is `off' 
++
++2002-07-04
++  fix: some fears for empty passwd
++
++*****
++
++2001-12-08
++  merge my patch for 2.20 and mod_auth_mysql 3.1
++  [EMAIL PROTECTED]
+  */
+ 
+ #define AUTH_MYSQL_VERSION "3.1"
+@@ -35,6 +63,8 @@
+ 
+ static MYSQL auth_sql_server, *mysql_auth = NULL;
+ static char *auth_db_host = NULL, *auth_db_name = NULL, *auth_db_user = NULL, 
*auth_db_pwd = NULL;
++static char *socket_file = NULL, *tmp_host = NULL;
++static unsigned int port_num = 0;
+ 
+ #define MYSQL_ERROR(mysql) ((mysql)?(mysql_error(mysql)):"mysql server has 
gone away")
+ 
+@@ -45,6 +75,7 @@ static char *auth_db_host = NULL, *auth_db_name = NULL
+ #define CRYPT_DES_ENCRYPTION_FLAG     1<<1
+ #define MYSQL_ENCRYPTION_FLAG         1<<2
+ #define CRYPT_MD5_ENCRYPTION_FLAG     1<<3
++#define MD5_ENCRYPTION_FLAG           1<<4
+ 
+ static int check_no_encryption(const char *passwd, char *enc_passwd)
+ {
+@@ -68,8 +99,14 @@ static int check_crypt_MD5_encryption(const char *pass
+ 
+ static int check_mysql_encryption(const char *passwd, char *enc_passwd)
+ {
+-      char scrambled_passwd[32];
+-      
++      /* Make more then big enough */
++      char scrambled_passwd[256];
++
++#if MYSQL_VERSION_ID >= 40000
++      make_scrambled_password_323(scrambled_passwd, passwd);
++      if (strcmp(scrambled_passwd, enc_passwd) == 0) return 1;
++#endif /* MYSQL_VERSION_ID >= 40000 */
++
+       make_scrambled_password(scrambled_passwd, passwd);
+       return (!strcmp(scrambled_passwd, enc_passwd));
+ }
+@@ -90,6 +127,7 @@ encryption_type_entry supported_encryption_types[] = {
+ #if MD5_CRYPT
+       { "Crypt_MD5",                  check_crypt_MD5_encryption,             
CRYPT_MD5_ENCRYPTION_FLAG },
+ #endif
++      { "MD5",                check_no_encryption,                    
MD5_ENCRYPTION_FLAG },
+       /* add additional encryption types below */
+       { NULL,                 NULL,                                           
0 }
+ };
+@@ -128,6 +166,8 @@ typedef struct {
+       unsigned char assume_authoritative;
+       unsigned char enable_mysql_auth;
+       unsigned char non_persistent;
++
++      char *where;
+ } mysql_auth_config_rec;
+ 
+ module auth_mysql_module;
+@@ -149,13 +189,15 @@ void *create_mysql_auth_dir_config(pool *p, char *d)
+       sec->user_field = sec->password_field = sec->group_field = NULL;
+ 
+       sec->assume_authoritative = 1;
+-      sec->allow_empty_passwords = 1;
++      sec->allow_empty_passwords = 0;
+       sec->enable_mysql_auth = 1;
+ 
+       sec->encryption_types = CRYPT_DES_ENCRYPTION_FLAG;
+       sec->encryption_types_initialized = 0;
+       
+       sec->non_persistent = 0;
++
++      sec->where = NULL;
+       
+       return sec;
+ }
+@@ -223,8 +265,27 @@ static const char *my_set_string_slot(cmd_parms *cmd, 
+ 
+ static const char *set_auth_mysql_info(cmd_parms * parms, void *dummy, char 
*host, char *user, char *pwd)
+ {
++      size_t   len;
++      int      i;
++      /*  host:3306  or host:/tmp/mysql.sock */
++
+       if (*host != '.') {
+-              auth_db_host = host;
++              len = strlen(host) + 2;
++              tmp_host = (char *)calloc(len, sizeof(char));
++              strlcpy(tmp_host, host, len);
++
++              for (i=0; i<strlen(host); i++) {
++                      if ( *(host + i) == ':' ) {
++                              tmp_host[i] = '\0';
++
++                              if ( *( host + i + 1 ) == '/' )
++                                      socket_file = (host + i + 1);
++                              else
++                                      port_num = (unsigned int)atoi( (host + 
i + 1) );
++                      }
++              }
++
++              auth_db_host = tmp_host;
+       }
+       if (*user != '.') {
+               auth_db_user = user;
+@@ -286,6 +347,9 @@ command_rec mysql_auth_cmds[] = {
+       { "Auth_MySQL",                                         
my_set_mysql_auth_flag,                 NULL,   OR_AUTHCFG, FLAG,       "Enable 
(on) or disable (off) MySQL authentication." },
+     { "Auth_MySQL_Encryption_Types",  my_set_encryption_types,                
NULL,   OR_AUTHCFG, ITERATE,"Encryption types to use" },
+     { "Auth_MySQL_Non_Persistent",            my_set_non_persistent,          
        NULL,   OR_AUTHCFG,     FLAG,   "Use non-persistent MySQL links" },
++      { "Auth_MySQL_Where",   my_set_string_slot,
++        (void *) XtOffsetOf(mysql_auth_config_rec, where),
++        OR_AUTHCFG,     TAKE1,  "WHERE clause" },
+       { NULL }
+ };
+ 
+@@ -388,7 +452,12 @@ static void open_auth_dblink(request_rec *r, mysql_aut
+       }
+       if (name != NULL) {                     /* open an SQL link */
+               /* link to the MySQL database and register its [EMAIL 
PROTECTED] */
++#if MYSQL_VERSION_ID >= 40000
++              mysql_init(&auth_sql_server);
++              mysql_auth = mysql_real_connect(&auth_sql_server, auth_db_host, 
user, pwd, name, 0, NULL, 0);
++#else /* MYSQL_VERSION_ID < 40000 */
+               mysql_auth = mysql_connect(&auth_sql_server, auth_db_host, 
user, pwd);
++#endif /* MYSQL_VERSION_ID < 40000 */
+               if (sec->non_persistent && mysql_auth) {
+                       note_cleanups_for_mysql_auth(r->pool, mysql_auth);
+               }
+@@ -460,6 +529,7 @@ static int mysql_check_user_password(request_rec *r, c
+       MYSQL_RES *result;
+       MYSQL_ROW sql_row;
+       encryption_type_entry *ete;
++      conn_rec *c = r->connection;
+ 
+       if (sec->user_table) {
+               auth_table = sec->user_table;
+@@ -470,49 +540,89 @@ static int mysql_check_user_password(request_rec *r, c
+       if (sec->password_field) {
+               auth_password_field = sec->password_field;
+       }
+-      query = (char *) pstrcat(r->pool, "select ", auth_password_field, " 
from ", auth_table,
+-                                " where ", auth_user_field, "='", esc_user, 
"'", NULL);
+-      if (!query) {
+-              return -1;
++
++      if (sec->where && strlen(sec->where)>0 ) {
++              if (sec->encryption_types == MD5_ENCRYPTION_FLAG)
++                      query = (char *) pstrcat(r->pool, "SELECT ", 
auth_password_field, ",MD5('", password, "') FROM ", auth_table,
++                          " WHERE ", auth_user_field, "='", esc_user, "' AND 
", sec->where, NULL);
++              else
++                      query = (char *) pstrcat(r->pool, "SELECT ", 
auth_password_field, " FROM ", auth_table,
++                          " WHERE ", auth_user_field, "='", esc_user, "' AND 
", sec->where, NULL);
++      } else {
++              if (sec->encryption_types == MD5_ENCRYPTION_FLAG)
++                      query = (char *) pstrcat(r->pool, "SELECT ", 
auth_password_field, ",MD5('", password, "') FROM ", auth_table,
++                          " WHERE ", auth_user_field, "='", esc_user, "'", 
NULL);
++              else
++                      query = (char *) pstrcat(r->pool, "SELECT ", 
auth_password_field, " FROM ", auth_table,
++                          " WHERE ", auth_user_field, "='", esc_user, "'", 
NULL);
+       }
+-      if (safe_mysql_query(r, query, sec)) {
++      if (!query || safe_mysql_query(r, query, sec) ||
++          !(result = safe_mysql_store_result(r->pool))) {
++              ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r,
++                  "MySQL auth: can not check user %s, unknown error was 
occured: %s", c->user, r->uri);
+               return -1;
+       }
+-      result = safe_mysql_store_result(r->pool);
+-      if (!result) {
+-              return -1;
+-      }
+       switch (mysql_num_rows(result)) {
+               case 0:
++                      ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r,
++                          "MySQL auth: user %s not found: %s", c->user, 
r->uri);
+                       return 0;
+                       break;
+               case 1:
+                       sql_row = mysql_fetch_row(result);
+                       /* ensure we have a row, and non NULL value */
+                       if (!sql_row || !sql_row[0]) {
++                              ap_log_rerror(APLOG_MARK, 
APLOG_NOERRNO|APLOG_ERR, r,
++                                  "MySQL auth: user %s not found, no record: 
%s", c->user, r->uri);
+                               return -1;
+                       }
+                       
+                       /* empty password support */
+-                      if (sec->allow_empty_passwords && !strlen(sql_row[0])) {
+-                              return 1;
++                      if (sec->allow_empty_passwords && strlen(sql_row[0])==0 
&& strlen(password)==0) {
++                              ap_log_rerror(APLOG_MARK, 
APLOG_NOERRNO|APLOG_WARNING, r,
++                                  "MySQL auth: user %s: empty passwd login: 
\"%s\"",
++                                  c->user, r->uri);
++                              return 1;  /* Success */
+                       }
+                       
++                      if (!sec->allow_empty_passwords) {
++                              if (strlen(password) <1 || 
strlen(sql_row[0])<1) {
++                                      ap_log_rerror(APLOG_MARK, 
APLOG_NOERRNO|APLOG_ERR, r,
++                                          "MySQL auth: user %s: 
authentication failure for \"%s\": empty password",
++                                          c->user, r->uri);
++                                      return 0; /*false*/
++                              }
++                      }
++
+                       for (ete=supported_encryption_types; ete->name; ete++) {
+                               if (sec->encryption_types & ete->flag) {
+-                                      if (ete->check_function(password, 
sql_row[0])) {
+-                                              return 1;
++                                      if (sec->encryption_types == 
MD5_ENCRYPTION_FLAG) {
++                                              if (!sql_row[1])
++                                                      return -1;
++                                              if 
(ete->check_function(sql_row[0], sql_row[1]))
++                                                      return 1;  /* Success */
+                                       }
++                                      else {
++                                              if 
(ete->check_function(password, sql_row[0]))
++                                                      return 1;  /* Success */
++                                      }
+                               }
+                       }
++                      ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r,
++                          "MySQL auth: user %s: authentication failure for 
\"%s\": invalid password",
++                          c->user, r->uri);
+                       return 0;
+                       
+ 
+                       break;
+               default:
++                      ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r,
++                          "MySQL auth: can not check user %s, unknown error 
was occured: %s", c->user, r->uri);
+                       return -1;
+                       break;
+       }
++      ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r,
++          "MySQL auth: can not check user %s, unknown error was occured: %s", 
c->user, r->uri);
+       return -1;
+ }
+ 
+@@ -537,9 +647,15 @@ static int mysql_check_group(request_rec *r, char *use
+               auth_user_field = sec->user_field;
+       }
+ 
+-   query = pstrcat(r->pool,"select count(*) from ",auth_table,
+-   " where ",auth_user_field,"='",esc_user,"'"
+-   " and (",groups_query,")",NULL);
++      if (sec->where && strlen(sec->where)>0 )
++              query = pstrcat(r->pool, "SELECT COUNT(", auth_user_field,
++                  ") FROM ", auth_table, " WHERE ", auth_user_field,
++                  "='", esc_user, "' AND (", groups_query, ") AND ",
++                  sec->where, NULL);
++      else
++              query = pstrcat(r->pool, "SELECT COUNT(", auth_user_field,
++                  ") FROM ", auth_table, " WHERE ", auth_user_field,
++                  "='", esc_user, "' AND (", groups_query, ")", NULL);
+ 
+       if (!query) {
+               return -1;
+@@ -575,6 +691,10 @@ int mysql_authenticate_basic_user(request_rec *r)
+ 
+       switch (mysql_check_user_password(r, c->user, sent_pw, sec)) {
+               case 0:
++                      ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r,
++                          "user %s: authentication failure for \"%s\": %s",
++                          c->user, r->uri);
++                      ap_note_basic_auth_failure(r);
+                       note_basic_auth_failure(r);
+                       return AUTH_REQUIRED;
+                       break;
+@@ -598,6 +718,7 @@ int mysql_check_auth(request_rec *r)
+ {
+       mysql_auth_config_rec *sec = (mysql_auth_config_rec *) 
get_module_config(r->per_dir_config, &auth_mysql_module);
+       char *user = r->connection->user;
++      conn_rec *c = r->connection;
+       int m = r->method_number;
+       int method_restricted = 0;
+       register int x;
+@@ -669,6 +790,10 @@ int mysql_check_auth(request_rec *r)
+       if (!(sec->assume_authoritative)) {
+               return DECLINED;
+       }
++      ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r,
++          "user %s: authentication failure for \"%s\": %s",
++          c->user, r->uri);
++      ap_note_basic_auth_failure(r);
+       note_basic_auth_failure(r);
+       return AUTH_REQUIRED;
+ }

Reply via email to