Author: mcantelon
Date: Tue Jan 17 17:01:28 2012
New Revision: 10708

Log:
Grouped related methods in QubitFlatfiles to make source easier to understand.

Modified:
   trunk/lib/QubitFlatfileImport.class.php

Modified: trunk/lib/QubitFlatfileImport.class.php
==============================================================================
--- trunk/lib/QubitFlatfileImport.class.php     Tue Jan 17 16:33:49 2012        
(r10707)
+++ trunk/lib/QubitFlatfileImport.class.php     Tue Jan 17 17:01:28 2012        
(r10708)
@@ -66,6 +66,13 @@
     $this->rowStatusVars = array();
   }
 
+
+  /*
+   *
+   *  Helper methods
+   *  --------------
+   */
+
   /**
    * Use an array of properties and their respective values to set an object's
    * properties (restricting to a set of allowed properties and allowing the
@@ -122,19 +129,6 @@
   }
 
   /**
-   * Add an ad-hoc column handler
-   *
-   * @param string $column  name of column
-   * @param closure $handler  column handling logic
-   *
-   * @return void
-   */
-  public function addColumnHandler($column, $handler)
-  {
-    $this->handlers[$column] = $handler;
-  }
-
-  /**
    * Test whether a property is set and, if so, execute it
    *
    * @param string $property  name of property
@@ -152,179 +146,110 @@
   }
 
   /**
-   * Determine whether columns isn't handled by import logic
+   * Start import timer
    *
-   * @return boolean  TRUE if column not handled by import logic
+   * @return void
    */
-  public function unhandledColumn($column)
+  protected function startTimer()
   {
-    return !in_array($column, $this->ignoreColumns)
-      && !in_array($column, $this->standardColumns)
-      && !isset($this->columnMap[$column])
-      && !isset($this->propertyMap[$column])
-      && !isset($this->noteMap[$column])
-      && !isset($this->handlers[$column])
-      && !in_array($column, $this->variableColumns)
-      && !isset($this->arrayColumns[$column]);
+    $this->timer = new QubitTimer;
+    $this->timer->start();
   }
 
   /**
-   * Combine two or more arrays, eliminating any duplicates
+   * Stop import timer
    *
-   * @return array  combined array
+   * @return void
    */
-  protected function combineArraysWithoutDuplicates()
+  protected function stopTimer()
   {
-    $args = func_get_args();
-    $combined = array();
-
-    // go through each array providesd
-    for($index = 0; $index < count($args); $index++)
-    {
-      // for each element of array, add to combined array if element isn't a 
dupe
-      foreach($args[$index] as $element)
-      {
-        if (!in_array($element, $combined)) array_push($combined, $element);
-      }
-    }
-
-    return $combined;
+    $this->timer->stop();
   }
 
   /**
-   * Return an array of import columns that aren't handled by import logic
+   * Get time elapsed during import
    *
-   * @return array  array of column names
+   * @return int  microseconds since import began
    */
-  public function determineUnmatchedHandledColumns()
+  public function getTimeElapsed()
   {
-    $unmatchedColumns = array();
-
-    foreach($this->handledColumns() as $handledColumn)
-    {
-      if (!in_array($handledColumn, $this->columnNames)) 
array_push($unmatchedColumns, $handledColumn);
-    }
-
-    return $unmatchedColumns;
+    return $this->timer->elapsed();
   }
 
   /**
-   * Return an array of columns that are handled by import logic
+   * Log error message if an error log has been defined
    *
-   * @return array  array of column names
+   * @param string $message  error message
+   *
+   * @return string  message prefixed with current row number
    */
-  public function handledColumns()
+  public function logError($message)
   {
-     return $this->combineArraysWithoutDuplicates(
-       $this->standardColumns,
-       $this->variableColumns,
-       array_keys($this->columnMap),
-       array_keys($this->handlers),
-       array_keys($this->propertyMap),
-       array_keys($this->noteMap),
-       array_keys($this->arrayColumns)
-     );
+    $message = 'Row '. $this->getStatus('rows') .': '. $message ."\n";
+    if ($this->errorLog) file_put_contents($this->errorLog, $message, 
FILE_APPEND);
+    return $message;
   }
 
   /**
-   * Return an array of columns that are included in the import but not
-   * handled by logic.
+   * Append content to existing content, prepending a line break to new content
+   * if necessary
    *
-   * @return array  array of column names
+   * @param string $oldContent  existing content
+   * @param string $newContent  new content to add to existing content
+   *
+   * @return string  both strings appended
    */
-  public function determineUnhandledColumns()
+  public function appendWithLineBreakIfNeeded($oldContent, $newContent)
   {
-    $ignored = array();
-    foreach($this->columnNames as $column)
-    {
-      if ($this->unhandledColumn($column))
-      {
-        array_push($ignored, $column);
-      }
-    }
-
-    return $ignored;
+    return ($oldContent) ? $oldContent ."\n". $newContent : $newContent;
   }
 
   /**
-   * Render a string description of problem columns from an array
+   * Convert human readable (e.g. 'This string') strings to camelCase
+   * representation (e.g. 'thisString')
    *
-   * @return string  description 
+   * @param string $str  input string
+   *
+   * @return string  camelCase string
    */
-  public function renderProblemColumns($columns, $problemDescription)
+  public static function camelize($str)
   {
-    $output = '';
-
-    if (count($columns))
-    {
-      $output .= count($columns) . " columns ". $problemDescription .":\n";
-      $output .= '  '. implode("\n  ", $columns) ."\n"; 
-
-      $output .= "\n";
-    }
+    $str = str_replace(' ', '_', $str);
+    $str = sfInflector::camelize($str);
+    $str = lcfirst($str);
 
-    return $output;
+    return $str;
   }
 
   /**
-   * Render a string description of import columns that aren't handled by
-   * the import logic
+   * Combine two or more arrays, eliminating any duplicates
    *
-   * @return string  description 
+   * @return array  combined array
    */
-  public function renderUnhandledColumns()
+  protected function combineArraysWithoutDuplicates()
   {
-    return $this->renderProblemColumns(
-      $this->determineUnhandledColumns(),
-      "aren't handled or ignored"
-    );
-  }
+    $args = func_get_args();
+    $combined = array();
 
-  /**
-   * Render a string description of columns that are handled by the import
-   * logic but don't actually exist in the import itself
-   *
-   * @return string  description
-   */
-  public function renderUnmatchedColumns()
-  {
-    return $this->renderProblemColumns(
-      $this->determineUnmatchedHandledColumns(),
-      'are being handled but do not have an import column to work with'
-    );
-    return $output;
-  }
+    // go through each array providesd
+    for($index = 0; $index < count($args); $index++)
+    {
+      // for each element of array, add to combined array if element isn't a 
dupe
+      foreach($args[$index] as $element)
+      {
+        if (!in_array($element, $combined)) array_push($combined, $element);
+      }
+    }
 
-  /**
-   * Start import timer
-   *
-   * @return void
-   */
-  protected function startTimer()
-  {
-    $this->timer = new QubitTimer;
-    $this->timer->start();
+    return $combined;
   }
 
-  /**
-   * Stop import timer
-   *
-   * @return void
-   */
-  protected function stopTimer()
-  {
-    $this->timer->stop();
-  }
 
-  /**
-   * Get time elapsed during import
+  /*
    *
-   * @return int  microseconds since import began
+   *  Row processing methods
+   *  ----------------------
    */
-  public function getTimeElapsed()
-  {
-    return $this->timer->elapsed();
-  }
 
   /**
    * Pull data from a csv file and process each row
@@ -378,6 +303,157 @@
   }
 
   /**
+   * Process a row of imported data
+   *
+   * @param array $row  array of column data
+   *
+   * @return void
+   */
+  public function row($row = array())
+  {
+    // stash raw row data so it's accessible to closure logic
+    $this->status['row'] = $row;
+
+    // set row status variables that are based on column values
+    $this->rowProcessingBeforeObjectCreation($row);
+
+    if (isset($this->className))
+    {
+      // create new object
+      $this->object = new $this->className;
+    } else {
+      // execute ad-hoc row initialization logic (which can make objects, load
+      // them, etc.)
+      $this->executeClosurePropertyIfSet('rowInitLogic');
+    }
+
+    // set fields in information object and execute custom column handlers
+    $this->rowProcessingBeforeSave($row);
+
+    // execute pre-save ad-hoc import logic
+    $this->executeClosurePropertyIfSet('preSaveLogic');
+
+    if (isset($this->className))
+    {
+      $this->object->save();
+
+      // execute row completion logic
+      $this->executeClosurePropertyIfSet('postSaveLogic');
+    } else {
+      // execute row completion logic
+      $this->executeClosurePropertyIfSet('saveLogic');
+    }
+
+    // execute post-save ad-hoc import logic
+
+    // process import columns that produce child data (properties and notes)
+    $this->rowProcessingAfterSave($row);
+
+    // reset row-specific status variables
+    $this->rowStatusVars = array();
+  }
+
+  /**
+   * Log error message if an error log has been defined
+   *
+   * @param string $message  error message
+   *
+   * @return void
+   */
+  protected function rowProcessingBeforeObjectCreation($row)
+  { 
+    // process import columns that don't produce child data
+    $this->forEachRowColumn($row, function(&$self, $index, $columnName, $value)
+    {
+      if (
+        isset($self->columnNames[$index])
+        && in_array($self->columnNames[$index], $self->variableColumns)
+      )
+      {
+        $self->rowStatusVars[$self->columnNames[$index]] = $value;
+      }
+      else if (
+        isset($self->columnNames[$index])
+        && isset($self->arrayColumns[($self->columnNames[$index])])
+      )
+      {
+        $self->arrayColumnHandler($columnName, 
$self->arrayColumns[$columnName], $value);
+      }
+    });
+  }
+
+  /**
+   * Perform row processing for before an object is saved such as setting
+   * object properties and executing ad-hoc column handlers
+   *
+   * @param array $row  array of column data
+   *
+   * @return void
+   */
+  protected function rowProcessingBeforeSave($row)
+  {
+    // process import columns that don't produce child data
+    $this->forEachRowColumn($row, function(&$self, $index, $columnName, $value)
+    {
+      // if column maps to an attribute, set the attribute
+      if (isset($self->columnMap) && isset($self->columnMap[$columnName]))
+      {
+        $self->mappedColumnHandler($self->columnMap[$columnName], $value); 
+      }
+      else if (
+        isset($self->columnNames[$index])
+        && isset($self->handlers[($self->columnNames[$index])])
+      )
+      {
+        // otherwise, if column is data and a handler for it is set, use it
+        call_user_func_array(
+          $self->handlers[$columnName],
+          array($self, $value)
+        );
+      }
+      else if (
+        isset($self->columnNames[$index])
+        && in_array($self->columnNames[$index], $self->standardColumns)
+      )
+      {
+        // otherwise, if column is data and it's a standard column, use it
+        $self->object->{$self->columnNames[$index]} = $value;
+      }
+    });
+  }
+
+  /**
+   * Perform row processing for after an object is saved and has an ID such
+   * as creating child properties and notes
+   *
+   * @param array $row  array of column data
+   *
+   * @return void
+   */
+  protected function rowProcessingAfterSave($row)
+  {
+    $this->forEachRowColumn($row, function(&$self, $index, $columnName, $value)
+    {
+      // if column maps to a property, set the property
+      if (isset($self->propertyMap) && isset($self->propertyMap[$columnName]) 
&& trim($value))
+      {
+        $self->object->addProperty(
+          $self->propertyMap[$columnName], 
+          $value
+        );
+      }
+      else if (isset($self->noteMap) && isset($self->noteMap[$columnName]) && 
trim($value))
+      {
+        // otherwise, if maps to a note, create it
+        $transformationLogic = 
(isset($self->noteMap[$columnName]['transformationLogic']))
+          ? $self->noteMap[$columnName]['transformationLogic']
+          : false;
+        $self->createNote($self->noteMap[$columnName]['typeId'], $value, 
$transformationLogic);
+      }
+    });
+  }
+
+  /**
    * Execute logic, defined by a closure, on each column of a row
    *
    * @param array $row  array of column data
@@ -402,6 +478,58 @@
   }
 
   /**
+   * Output import progress, time elapsed, and memory usage
+   *
+   * @return string  description of import progress
+   */
+  public function renderProgressDescription()
+  {
+    $output = '.';
+
+    // return empty string if no intermittant progress display
+    if (!isset($this->rowsUntilProgressDisplay)
+      || !$this->rowsUntilProgressDisplay
+    ) return $output;
+
+    // row count isn't incremented until after this is displayed, so add one 
to reflect reality
+    $rowsProcessed = $this->getStatus('rows') - 
$this->getStatus('skippedRows');
+    $memoryUsageMB = round(memory_get_usage() / (1024 * 1024), 2);
+
+    // if this show should be displayed, display it
+    if (!($rowsProcessed % $this->rowsUntilProgressDisplay))
+    {
+      $elapsed = $this->getTimeElapsed();
+      $elapsedMinutes = round($elapsed / 60, 2);
+      $averageTime = round($elapsed / $rowsProcessed, 2);
+
+      $output .= "\n". $rowsProcessed ." rows processed in ". $elapsedMinutes
+      . " minutes (". $averageTime ." second/row average, ". $memoryUsageMB ." 
MB used).\n";
+    }
+
+    return $output;
+  }
+
+
+  /*
+   *
+   *  Column handlers
+   *  ---------------
+   */
+
+  /**
+   * Add an ad-hoc column handler
+   *
+   * @param string $column  name of column
+   * @param closure $handler  column handling logic
+   *
+   * @return void
+   */
+  public function addColumnHandler($column, $handler)
+  {
+    $this->handlers[$column] = $handler;
+  }
+
+  /**
    * Handle mapping of column to object property
    *
    * @param array $mapDefinition  array defining which property to map to and
@@ -448,201 +576,159 @@
     }
   }
 
-  /**
-   * Log error message if an error log has been defined
+
+  /*
    *
-   * @param string $message  error message
+   *  Import auditing methods
+   *  -----------------------
+   */
+
+  /**
+   * Determine whether columns isn't handled by import logic
    *
-   * @return void
+   * @return boolean  TRUE if column not handled by import logic
    */
-  protected function rowProcessingBeforeObjectCreation($row)
-  { 
-    // process import columns that don't produce child data
-    $this->forEachRowColumn($row, function(&$self, $index, $columnName, $value)
-    {
-      if (
-        isset($self->columnNames[$index])
-        && in_array($self->columnNames[$index], $self->variableColumns)
-      )
-      {
-        $self->rowStatusVars[$self->columnNames[$index]] = $value;
-      }
-      else if (
-        isset($self->columnNames[$index])
-        && isset($self->arrayColumns[($self->columnNames[$index])])
-      )
-      {
-        $self->arrayColumnHandler($columnName, 
$self->arrayColumns[$columnName], $value);
-      }
-    });
+  public function unhandledColumn($column)
+  {
+    return !in_array($column, $this->ignoreColumns)
+      && !in_array($column, $this->standardColumns)
+      && !isset($this->columnMap[$column])
+      && !isset($this->propertyMap[$column])
+      && !isset($this->noteMap[$column])
+      && !isset($this->handlers[$column])
+      && !in_array($column, $this->variableColumns)
+      && !isset($this->arrayColumns[$column]);
+  }
+
+  /**
+   * Return an array of import columns that aren't handled by import logic
+   *
+   * @return array  array of column names
+   */
+  public function determineUnmatchedHandledColumns()
+  {
+    $unmatchedColumns = array();
+
+    foreach($this->handledColumns() as $handledColumn)
+    {
+      if (!in_array($handledColumn, $this->columnNames)) 
array_push($unmatchedColumns, $handledColumn);
+    }
+
+    return $unmatchedColumns;
   }
 
   /**
-   * Perform row processing for before an object is saved such as setting
-   * object properties and executing ad-hoc column handlers
-   *
-   * @param array $row  array of column data
+   * Return an array of columns that are handled by import logic
    *
-   * @return void
+   * @return array  array of column names
    */
-  protected function rowProcessingBeforeSave($row)
+  public function handledColumns()
   {
-    // process import columns that don't produce child data
-    $this->forEachRowColumn($row, function(&$self, $index, $columnName, $value)
-    {
-      // if column maps to an attribute, set the attribute
-      if (isset($self->columnMap) && isset($self->columnMap[$columnName]))
-      {
-        $self->mappedColumnHandler($self->columnMap[$columnName], $value); 
-      }
-      else if (
-        isset($self->columnNames[$index])
-        && isset($self->handlers[($self->columnNames[$index])])
-      )
-      {
-        // otherwise, if column is data and a handler for it is set, use it
-        call_user_func_array(
-          $self->handlers[$columnName],
-          array($self, $value)
-        );
-      }
-      else if (
-        isset($self->columnNames[$index])
-        && in_array($self->columnNames[$index], $self->standardColumns)
-      )
-      {
-        // otherwise, if column is data and it's a standard column, use it
-        $self->object->{$self->columnNames[$index]} = $value;
-      }
-    });
+     return $this->combineArraysWithoutDuplicates(
+       $this->standardColumns,
+       $this->variableColumns,
+       array_keys($this->columnMap),
+       array_keys($this->handlers),
+       array_keys($this->propertyMap),
+       array_keys($this->noteMap),
+       array_keys($this->arrayColumns)
+     );
   }
 
   /**
-   * Perform row processing for after an object is saved and has an ID such
-   * as creating child properties and notes
-   *
-   * @param array $row  array of column data
+   * Return an array of columns that are included in the import but not
+   * handled by logic.
    *
-   * @return void
+   * @return array  array of column names
    */
-  protected function rowProcessingAfterSave($row)
+  public function determineUnhandledColumns()
   {
-    $this->forEachRowColumn($row, function(&$self, $index, $columnName, $value)
+    $ignored = array();
+    foreach($this->columnNames as $column)
     {
-      // if column maps to a property, set the property
-      if (isset($self->propertyMap) && isset($self->propertyMap[$columnName]) 
&& trim($value))
-      {
-        $self->object->addProperty(
-          $self->propertyMap[$columnName], 
-          $value
-        );
-      }
-      else if (isset($self->noteMap) && isset($self->noteMap[$columnName]) && 
trim($value))
+      if ($this->unhandledColumn($column))
       {
-        // otherwise, if maps to a note, create it
-        $transformationLogic = 
(isset($self->noteMap[$columnName]['transformationLogic']))
-          ? $self->noteMap[$columnName]['transformationLogic']
-          : false;
-        $self->createNote($self->noteMap[$columnName]['typeId'], $value, 
$transformationLogic);
+        array_push($ignored, $column);
       }
-    });
+    }
+
+    return $ignored;
   }
 
   /**
-   * Process a row of imported data
-   *
-   * @param array $row  array of column data
+   * Render a string description of problem columns from an array
    *
-   * @return void
+   * @return string  description 
    */
-  public function row($row = array())
+  public function renderProblemColumns($columns, $problemDescription)
   {
-    // stash raw row data so it's accessible to closure logic
-    $this->status['row'] = $row;
-
-    // set row status variables that are based on column values
-    $this->rowProcessingBeforeObjectCreation($row);
-
-    if (isset($this->className))
-    {
-      // create new object
-      $this->object = new $this->className;
-    } else {
-      // execute ad-hoc row initialization logic (which can make objects, load
-      // them, etc.)
-      $this->executeClosurePropertyIfSet('rowInitLogic');
-    }
-
-    // set fields in information object and execute custom column handlers
-    $this->rowProcessingBeforeSave($row);
-
-    // execute pre-save ad-hoc import logic
-    $this->executeClosurePropertyIfSet('preSaveLogic');
+    $output = '';
 
-    if (isset($this->className))
+    if (count($columns))
     {
-      $this->object->save();
+      $output .= count($columns) . " columns ". $problemDescription .":\n";
+      $output .= '  '. implode("\n  ", $columns) ."\n"; 
 
-      // execute row completion logic
-      $this->executeClosurePropertyIfSet('postSaveLogic');
-    } else {
-      // execute row completion logic
-      $this->executeClosurePropertyIfSet('saveLogic');
+      $output .= "\n";
     }
 
-    // execute post-save ad-hoc import logic
-
-    // process import columns that produce child data (properties and notes)
-    $this->rowProcessingAfterSave($row);
-
-    // reset row-specific status variables
-    $this->rowStatusVars = array();
+    return $output;
   }
 
   /**
-   * Log error message if an error log has been defined
-   *
-   * @param string $message  error message
+   * Render a string description of import columns that aren't handled by
+   * the import logic
    *
-   * @return string  message prefixed with current row number
+   * @return string  description 
    */
-  public function logError($message)
+  public function renderUnhandledColumns()
   {
-    $message = 'Row '. $this->getStatus('rows') .': '. $message ."\n";
-    if ($this->errorLog) file_put_contents($this->errorLog, $message, 
FILE_APPEND);
-    return $message;
+    return $this->renderProblemColumns(
+      $this->determineUnhandledColumns(),
+      "aren't handled or ignored"
+    );
   }
 
   /**
-   * Output import progress, time elapsed, and memory usage
+   * Render a string description of columns that are handled by the import
+   * logic but don't actually exist in the import itself
    *
-   * @return string  description of import progress
+   * @return string  description
    */
-  public function renderProgressDescription()
+  public function renderUnmatchedColumns()
   {
-    $output = '.';
+    return $this->renderProblemColumns(
+      $this->determineUnmatchedHandledColumns(),
+      'are being handled but do not have an import column to work with'
+    );
+    return $output;
+  }
 
-    // return empty string if no intermittant progress display
-    if (!isset($this->rowsUntilProgressDisplay)
-      || !$this->rowsUntilProgressDisplay
-    ) return $output;
 
-    // row count isn't incremented until after this is displayed, so add one 
to reflect reality
-    $rowsProcessed = $this->getStatus('rows') - 
$this->getStatus('skippedRows');
-    $memoryUsageMB = round(memory_get_usage() / (1024 * 1024), 2);
+  /*
+   *
+   *  Qubit data helpers
+   *  ------------------
+   */
 
-    // if this show should be displayed, display it
-    if (!($rowsProcessed % $this->rowsUntilProgressDisplay))
+  /**
+   * Issue an SQL query
+   *
+   * @param string $query  SQL query
+   * @param string $params  values to map to placeholders (optional)
+   *
+   * @return object  database statement object
+   */
+  public function sqlQuery($query, $params = array())
+  {
+    $connection = Propel::getConnection();
+    $statement = $connection->prepare($query);
+    for($index = 0; $index < count($params); $index++)
     {
-      $elapsed = $this->getTimeElapsed();
-      $elapsedMinutes = round($elapsed / 60, 2);
-      $averageTime = round($elapsed / $rowsProcessed, 2);
-
-      $output .= "\n". $rowsProcessed ." rows processed in ". $elapsedMinutes
-      . " minutes (". $averageTime ." second/row average, ". $memoryUsageMB ." 
MB used).\n";
+      $statement->bindValue($index + 1, $params[$index]);
     }
-
-    return $output;
+    $statement->execute();
+    return $statement;
   }
 
   /**
@@ -702,26 +788,6 @@
   }
 
   /**
-   * Issue an SQL query
-   *
-   * @param string $query  SQL query
-   * @param string $params  values to map to placeholders (optional)
-   *
-   * @return object  database statement object
-   */
-  public function sqlQuery($query, $params = array())
-  {
-    $connection = Propel::getConnection();
-    $statement = $connection->prepare($query);
-    for($index = 0; $index < count($params); $index++)
-    {
-      $statement->bindValue($index + 1, $params[$index]);
-    }
-    $statement->execute();
-    return $statement;
-  }
-
-  /**
    * Create a Qubit actor or, if one already exists, fetch it
    *
    * @param string $name  name of actor
@@ -964,37 +1030,6 @@
   }
 
   /**
-   * Append content to existing content, prepending a line break to new content
-   * if necessary
-   *
-   * @param string $oldContent  existing content
-   * @param string $newContent  new content to add to existing content
-   *
-   * @return string  both strings appended
-   */
-  public function appendWithLineBreakIfNeeded($oldContent, $newContent)
-  {
-    return ($oldContent) ? $oldContent ."\n". $newContent : $newContent;
-  }
-
-  /**
-   * Convert human readable (e.g. 'This string') strings to camelCase
-   * representation (e.g. 'thisString')
-   *
-   * @param string $str  input string
-   *
-   * @return string  camelCase string
-   */
-  public static function camelize($str)
-  {
-    $str = str_replace(' ', '_', $str);
-    $str = sfInflector::camelize($str);
-    $str = lcfirst($str);
-
-    return $str;
-  }
-
-  /**
    * Map a value to its corresponding term name then return the term ID
    * corresponding to the term name
    *

-- 
You received this message because you are subscribed to the Google Groups 
"Qubit Toolkit Commits" group.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to 
[email protected].
For more options, visit this group at 
http://groups.google.com/group/qubit-commits?hl=en.

Reply via email to