diff --git a/common/source/java/ch/systemsx/cisd/common/parser/TabFileLoader.java b/common/source/java/ch/systemsx/cisd/common/parser/TabFileLoader.java
index 889496388a1ba60b3a46d043fa10d26d87dcbf71..ba4b9765f89ba7a99ae78e30ab029ce49e4eaa7a 100644
--- a/common/source/java/ch/systemsx/cisd/common/parser/TabFileLoader.java
+++ b/common/source/java/ch/systemsx/cisd/common/parser/TabFileLoader.java
@@ -44,7 +44,6 @@ import ch.systemsx.cisd.common.parser.filter.ILineFilter;
  * <pre>
  *     column1  column2 column2
  * </pre>
- * 
  * <li>Comment section:
  * 
  * <pre>
@@ -53,7 +52,6 @@ import ch.systemsx.cisd.common.parser.filter.ILineFilter;
  *     # ...
  *     column1  column2 column2
  * </pre>
- * 
  * <li>Column headers at the end of the comment section:
  * 
  * <pre>
@@ -71,7 +69,7 @@ import ch.systemsx.cisd.common.parser.filter.ILineFilter;
 public class TabFileLoader<T>
 {
 
-    private static final String PREFIX = "#";
+    public static final String COMMENT_PREFIX = "#";
 
     private final IParserObjectFactoryFactory<T> factory;
 
@@ -129,10 +127,10 @@ public class TabFileLoader<T>
         while (lineIterator.hasNext())
         {
             previousLineHasColumnHeaders =
-                    (previousLine != null) && PREFIX.equals(previousLine.getText());
+                    (previousLine != null) && COMMENT_PREFIX.equals(previousLine.getText());
             previousLine = line;
             line = lineIterator.next();
-            if (line.getText().startsWith(PREFIX) == false)
+            if (line.getText().startsWith(COMMENT_PREFIX) == false)
             {
                 break;
             }
@@ -180,7 +178,8 @@ public class TabFileLoader<T>
      * Note that the search is case-insensitive.
      * </p>
      * 
-     * @throws IllegalArgumentException if there is at least one duplicate in the given <var>tokens</var>.
+     * @throws IllegalArgumentException if there is at least one duplicate in the given
+     *             <var>tokens</var>.
      */
     private final static void notUnique(final String[] tokens)
     {
diff --git a/rtd_yeastx/source/java/ch/systemsx/cisd/yeastx/etl/BatchDataSetHandler.java b/rtd_yeastx/source/java/ch/systemsx/cisd/yeastx/etl/BatchDataSetHandler.java
index eac9ffef91860c50fad4954649d3f79b5bb9a859..597ab729006d190fb9e435595b35b0e7e4ef6e0e 100644
--- a/rtd_yeastx/source/java/ch/systemsx/cisd/yeastx/etl/BatchDataSetHandler.java
+++ b/rtd_yeastx/source/java/ch/systemsx/cisd/yeastx/etl/BatchDataSetHandler.java
@@ -58,25 +58,26 @@ public class BatchDataSetHandler implements IDataSetHandler
         {
             return processedDatasetFiles;
         }
+        LogUtils log = new LogUtils(datasetsParentDir);
         TableMap<String, DataSetMappingInformation> datasetsMapping =
                 DatasetMappingUtil.tryGetDatasetsMapping(datasetsParentDir);
         if (datasetsMapping == null)
         {
-            touchErrorMarkerFile(datasetsParentDir);
+            touchErrorMarkerFile(datasetsParentDir, log);
             return processedDatasetFiles;
         }
         Set<String> processedFiles = new HashSet<String>();
         List<File> files = listAll(datasetsParentDir);
         for (File file : files)
         {
-            if (canDatasetBeProcessed(file, datasetsMapping))
+            if (canDatasetBeProcessed(file, datasetsMapping, log))
             {
                 processedDatasetFiles.addAll(delegator.handleDataSet(file));
                 processedFiles.add(file.getName().toLowerCase());
             }
         }
-        cleanMappingFile(datasetsParentDir, processedFiles);
-        finish(datasetsParentDir, datasetsMapping.values().size() - processedFiles.size());
+        clean(datasetsParentDir, processedFiles, log, datasetsMapping.values().size());
+        log.sendNotificationsIfNecessary();
         return processedDatasetFiles;
     }
 
@@ -108,30 +109,33 @@ public class BatchDataSetHandler implements IDataSetHandler
         return new File(datasetsParentDir, ERROR_MARKER_FILE).isFile();
     }
 
-    private void cleanMappingFile(File datasetsParentDir, Set<String> processedFiles)
+    private void cleanMappingFile(File datasetsParentDir, Set<String> processedFiles, LogUtils log)
     {
         try
         {
             DatasetMappingUtil.cleanMappingFile(datasetsParentDir, processedFiles);
         } catch (IOException ex)
         {
-            LogUtils.error(datasetsParentDir, "Cannot clean dataset mappings file: "
-                    + ex.getMessage());
+            log.userError("Cannot clean dataset mappings file: " + ex.getMessage());
         }
     }
 
-    private void finish(File datasetsParentDir, int unprocessedDatasetsCounter)
+    private void clean(File datasetsParentDir, Set<String> processedFiles, LogUtils log,
+            int datasetMappingsNumber)
     {
+        cleanMappingFile(datasetsParentDir, processedFiles, log);
+
+        int unprocessedDatasetsCounter = datasetMappingsNumber - processedFiles.size();
         if (unprocessedDatasetsCounter == 0 && hasNoPotentialDatasetFiles(datasetsParentDir))
         {
-            clean(datasetsParentDir);
+            cleanDatasetsDir(datasetsParentDir);
         } else
         {
-            touchErrorMarkerFile(datasetsParentDir);
+            touchErrorMarkerFile(datasetsParentDir, log);
         }
     }
 
-    private static void touchErrorMarkerFile(File parentDir)
+    private static void touchErrorMarkerFile(File parentDir, LogUtils log)
     {
         File errorMarkerFile = new File(parentDir, ERROR_MARKER_FILE);
         if (errorMarkerFile.isFile())
@@ -151,13 +155,13 @@ public class BatchDataSetHandler implements IDataSetHandler
                     .getPath());
         } else
         {
-            LogUtils.warn(parentDir,
+            log.userWarning(
                     "Correct the errors and delete the '%s' file to start processing again.",
                     ERROR_MARKER_FILE);
         }
     }
 
-    private void clean(File datasetsParentDir)
+    private void cleanDatasetsDir(File datasetsParentDir)
     {
         LogUtils.deleteUserLog(datasetsParentDir);
         DatasetMappingUtil.deleteMappingFile(datasetsParentDir);
@@ -167,7 +171,7 @@ public class BatchDataSetHandler implements IDataSetHandler
     // Checks that the sample from the mapping exists and is assigned to the experiment - we do not
     // want to move datasets to unidentified directory in this case.
     private boolean canDatasetBeProcessed(File file,
-            TableMap<String, DataSetMappingInformation> datasetsMapping)
+            TableMap<String, DataSetMappingInformation> datasetsMapping, LogUtils log)
     {
         if (DatasetMappingUtil.isMappingFile(file))
         {
@@ -179,7 +183,7 @@ public class BatchDataSetHandler implements IDataSetHandler
         {
             return false;
         }
-        return datasetMappingResolver.isMappingCorrect(mapping, file.getParentFile());
+        return datasetMappingResolver.isMappingCorrect(mapping, log);
     }
 
     private void deleteEmptyDir(File dir)
diff --git a/rtd_yeastx/source/java/ch/systemsx/cisd/yeastx/etl/BatchDataSetInfoExtractor.java b/rtd_yeastx/source/java/ch/systemsx/cisd/yeastx/etl/BatchDataSetInfoExtractor.java
index 3462b82a9768c899598b341c35e05fc249cf24e1..8a1a974929dc3599e2625dbd9d03d1a7765d9948 100644
--- a/rtd_yeastx/source/java/ch/systemsx/cisd/yeastx/etl/BatchDataSetInfoExtractor.java
+++ b/rtd_yeastx/source/java/ch/systemsx/cisd/yeastx/etl/BatchDataSetInfoExtractor.java
@@ -33,12 +33,9 @@ public class BatchDataSetInfoExtractor implements IDataSetInfoExtractor
 {
     private final Properties properties;
 
-    private final String groupCode;
-
     public BatchDataSetInfoExtractor(final Properties globalProperties)
     {
         this.properties = ExtendedProperties.getSubset(globalProperties, EXTRACTOR_KEY + '.', true);
-        this.groupCode = DatasetMappingResolver.getGroupCode(properties);
     }
 
     public DataSetInformation getDataSetInformation(File incomingDataSetPath,
@@ -55,7 +52,7 @@ public class BatchDataSetInfoExtractor implements IDataSetInfoExtractor
             String sampleCode =
                     getSampleCode(plainInfo, openbisService, incomingDataSetPath.getParentFile());
             info.setSampleCode(sampleCode);
-            info.setGroupCode(groupCode);
+            info.setGroupCode(plainInfo.getGroupCode());
             MLConversionType conversion = getConversion(plainInfo.getConversion());
             info.setConversion(conversion);
             return info;
@@ -83,7 +80,7 @@ public class BatchDataSetInfoExtractor implements IDataSetInfoExtractor
     {
         String sampleCode =
                 new DatasetMappingResolver(properties, openbisService).tryFigureSampleCode(mapping,
-                        logDir);
+                        new LogUtils(logDir));
         if (sampleCode == null)
         {
             // should not happen, the dataset handler should skip datasets with incorrect mapping
diff --git a/rtd_yeastx/source/java/ch/systemsx/cisd/yeastx/etl/DataSetMappingInformation.java b/rtd_yeastx/source/java/ch/systemsx/cisd/yeastx/etl/DataSetMappingInformation.java
index 8274cb3e21f07886b4f335fca8e18e237347732f..9af7d942ed061b96b9ed56874de03d1658c34a96 100644
--- a/rtd_yeastx/source/java/ch/systemsx/cisd/yeastx/etl/DataSetMappingInformation.java
+++ b/rtd_yeastx/source/java/ch/systemsx/cisd/yeastx/etl/DataSetMappingInformation.java
@@ -39,6 +39,8 @@ public class DataSetMappingInformation
 
     private String projectCode;
 
+    private String groupCode;
+
     private String conversion;
 
     private List<NewProperty> properties;
@@ -87,6 +89,17 @@ public class DataSetMappingInformation
         this.projectCode = StringUtils.trimToNull(projectCode);
     }
 
+    public String getGroupCode()
+    {
+        return groupCode;
+    }
+
+    @BeanProperty(label = "group")
+    public void setGroupCode(String groupCode)
+    {
+        this.groupCode = groupCode;
+    }
+
     public String getConversion()
     {
         return conversion;
diff --git a/rtd_yeastx/source/java/ch/systemsx/cisd/yeastx/etl/DataSetMappingInformationParser.java b/rtd_yeastx/source/java/ch/systemsx/cisd/yeastx/etl/DataSetMappingInformationParser.java
index 4e4d2b04abeed46ea94aa63880369eed6b51c696..f87a74cb737aae6b8a7f993c69b3044cc3a553de 100644
--- a/rtd_yeastx/source/java/ch/systemsx/cisd/yeastx/etl/DataSetMappingInformationParser.java
+++ b/rtd_yeastx/source/java/ch/systemsx/cisd/yeastx/etl/DataSetMappingInformationParser.java
@@ -71,7 +71,7 @@ class DataSetMappingInformationParser
         {
             causeMsg = "\nThe exception was caused by: " + cause.getMessage();
         }
-        LogUtils.error(mappingFile.getParentFile(),
+        LogUtils.basicError(mappingFile.getParentFile(),
                 "The datasets cannot be processed because the mapping file '%s' has incorrect format."
                         + " The following exception occured:\n%s%s", mappingFile.getPath(), e
                         .getMessage(), causeMsg);
diff --git a/rtd_yeastx/source/java/ch/systemsx/cisd/yeastx/etl/DatasetMappingResolver.java b/rtd_yeastx/source/java/ch/systemsx/cisd/yeastx/etl/DatasetMappingResolver.java
index 869d79cab62f7fbb12140f42aea32fb54d40364c..be8d4350427a7edf0fcd98227472f7de74724e48 100644
--- a/rtd_yeastx/source/java/ch/systemsx/cisd/yeastx/etl/DatasetMappingResolver.java
+++ b/rtd_yeastx/source/java/ch/systemsx/cisd/yeastx/etl/DatasetMappingResolver.java
@@ -16,14 +16,12 @@
 
 package ch.systemsx.cisd.yeastx.etl;
 
-import java.io.File;
 import java.util.List;
 import java.util.Properties;
 
 import org.apache.commons.io.FilenameUtils;
 
 import ch.systemsx.cisd.common.collections.CollectionUtils;
-import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException;
 import ch.systemsx.cisd.common.exceptions.UserFailureException;
 import ch.systemsx.cisd.openbis.dss.generic.shared.IEncapsulatedOpenBISService;
 import ch.systemsx.cisd.openbis.generic.shared.dto.ExperimentPE;
@@ -48,23 +46,8 @@ class DatasetMappingResolver
      */
     private final static String PROPERTY_TYPE_CODE_PROPERTY_NAME = "unique-property-type-code";
 
-    private static final String GROUP_CODE_PROPERTY_NAME = "group-code";
-
     private static final String PROPERTIES_PREFIX = "USER.";
 
-    public static String getGroupCode(Properties properties)
-    {
-        String groupCode = properties.getProperty(GROUP_CODE_PROPERTY_NAME);
-        if (groupCode == null)
-        {
-            throw ConfigurationFailureException
-                    .fromTemplate(
-                            "No group code defined in server configuration. Use '%s' property to specify it.",
-                            GROUP_CODE_PROPERTY_NAME);
-        }
-        return groupCode;
-    }
-
     public static String tryGetUniquePropertyTypeCode(Properties properties)
     {
         return tryGetPropertyTypeCode(properties);
@@ -72,15 +55,12 @@ class DatasetMappingResolver
 
     private final IEncapsulatedOpenBISService openbisService;
 
-    private final String groupCode;
-
     private final String propertyCodeOrNull;
 
     public DatasetMappingResolver(Properties properties, IEncapsulatedOpenBISService openbisService)
     {
         this.openbisService = openbisService;
         this.propertyCodeOrNull = tryGetPropertyTypeCode(properties);
-        this.groupCode = properties.getProperty(GROUP_CODE_PROPERTY_NAME);
     }
 
     private static String tryGetPropertyTypeCode(Properties properties)
@@ -95,7 +75,7 @@ class DatasetMappingResolver
         }
     }
 
-    public String tryFigureSampleCode(DataSetMappingInformation mapping, File logDir)
+    public String tryFigureSampleCode(DataSetMappingInformation mapping, LogUtils log)
     {
         String sampleCodeOrLabel = mapping.getSampleCodeOrLabel();
         if (propertyCodeOrNull == null)
@@ -107,21 +87,21 @@ class DatasetMappingResolver
             // The main purpose of this checks is to ensure that sample with the given code exists.
             // If it is not a case, we will try to check if the specified sample label is unique (in
             // all experiments).
-            if (tryFigureExperiment(sampleCodeOrLabel) != null)
+            if (tryFigureExperiment(sampleCodeOrLabel, mapping, log) != null)
             {
                 return sampleCodeOrLabel;
             }
         }
         ListSamplesByPropertyCriteria criteria =
-                new ListSamplesByPropertyCriteria(propertyCodeOrNull, sampleCodeOrLabel, groupCode,
-                        tryGetExperimentIdentifier(mapping));
+                new ListSamplesByPropertyCriteria(propertyCodeOrNull, sampleCodeOrLabel, mapping
+                        .getGroupCode(), tryGetExperimentIdentifier(mapping));
         List<String> samples;
         try
         {
             samples = openbisService.listSamplesByCriteria(criteria);
         } catch (UserFailureException e)
         {
-            logMappingError(mapping, logDir, e.getMessage());
+            logMappingError(mapping, log, e.getMessage());
             return null;
         }
         if (samples.size() == 1)
@@ -129,7 +109,7 @@ class DatasetMappingResolver
             return samples.get(0);
         } else if (samples.size() == 0)
         {
-            logMappingError(mapping, logDir, "there is no sample which matches the criteria <"
+            logMappingError(mapping, log, "there is no sample which matches the criteria <"
                     + criteria + ">");
             return null;
         } else
@@ -139,7 +119,7 @@ class DatasetMappingResolver
                             "there should be exacty one sample which matches the criteria '%s', but %d of them were found."
                                     + " Consider using the unique sample code.", criteria, samples
                                     .size());
-            logMappingError(mapping, logDir, errMsg);
+            logMappingError(mapping, log, errMsg);
             return null;
         }
     }
@@ -158,29 +138,29 @@ class DatasetMappingResolver
         }
     }
 
-    public boolean isMappingCorrect(DataSetMappingInformation mapping, File logDir)
+    public boolean isMappingCorrect(DataSetMappingInformation mapping, LogUtils log)
     {
-        if (isExperimentColumnCorrect(mapping, logDir) == false)
+        if (isExperimentColumnCorrect(mapping, log) == false)
         {
             return false;
         }
-        String sampleCode = tryFigureSampleCode(mapping, logDir);
+        String sampleCode = tryFigureSampleCode(mapping, log);
         if (sampleCode == null)
         {
             return false;
         }
-        return isConversionColumnValid(mapping, logDir)
-                && existsAndBelongsToExperiment(mapping, logDir, sampleCode);
+        return isConversionColumnValid(mapping, log)
+                && existsAndBelongsToExperiment(mapping, log, sampleCode);
     }
 
     private static boolean isConversionColumnValid(final DataSetMappingInformation mapping,
-            File logDir)
+            LogUtils log)
     {
         String conversionText = mapping.getConversion();
         MLConversionType conversion = MLConversionType.tryCreate(conversionText);
         if (conversion == null)
         {
-            LogUtils.error(logDir, String.format(
+            log.userError(String.format(
                     "Error for file '%s'. Unexpected value '%s' in 'conversion' column. "
                             + "Leave the column empty or use one of the allowed values: %s.",
                     mapping.getFileName(), conversionText, CollectionUtils.abbreviate(
@@ -191,18 +171,14 @@ class DatasetMappingResolver
         boolean conversionRequired = isConversionRequired(mapping);
         if (conversion == MLConversionType.NONE && conversionRequired)
         {
-            LogUtils
-                    .error(
-                            logDir,
-                            "Error for file '%s'. Conversion column cannot be empty for this type of file.",
-                            mapping.getFileName());
+            log.userError("Error for file '%s'. Conversion column cannot be empty "
+                    + "for this type of file.", mapping.getFileName());
             return false;
         }
         if (conversion != MLConversionType.NONE && conversionRequired == false)
         {
-            LogUtils.error(logDir,
-                    "Error for file '%s'. Conversion column must be empty for this type of file.",
-                    mapping.getFileName());
+            log.userError("Error for file '%s'. Conversion column must be empty "
+                    + "for this type of file.", mapping.getFileName());
             return false;
         }
         return true;
@@ -215,36 +191,46 @@ class DatasetMappingResolver
         return conversionRequired;
     }
 
-    private boolean existsAndBelongsToExperiment(DataSetMappingInformation mapping, File logDir,
+    private boolean existsAndBelongsToExperiment(DataSetMappingInformation mapping, LogUtils log,
             String sampleCode)
     {
-        ExperimentPE experiment = tryFigureExperiment(sampleCode);
+        ExperimentPE experiment = tryFigureExperiment(sampleCode, mapping, log);
         if (experiment == null)
         {
-            logMappingError(mapping, logDir, String.format(
-                    "sample with the code '%s' does not exist"
-                            + " or is not connected to any experiment", sampleCode));
+            logMappingError(mapping, log, String.format("sample with the code '%s' does not exist"
+                    + " or is not connected to any experiment", sampleCode));
             return false;
         }
         return true;
     }
 
-    private ExperimentPE tryFigureExperiment(String sampleCode)
+    private ExperimentPE tryFigureExperiment(String sampleCode, DataSetMappingInformation mapping,
+            LogUtils log)
     {
-        SampleIdentifier sampleIdentifier = createSampleIdentifier(sampleCode);
-        return openbisService.getBaseExperiment(sampleIdentifier);
+        SampleIdentifier sampleIdentifier = createSampleIdentifier(sampleCode, mapping);
+        try
+        {
+            return openbisService.getBaseExperiment(sampleIdentifier);
+        } catch (UserFailureException e)
+        {
+            log.userError("Error when checking if sample '%s' belongs to an experiment: %s",
+                    sampleIdentifier, e.getMessage());
+            return null;
+        }
     }
 
-    private SampleIdentifier createSampleIdentifier(String sampleCode)
+    private SampleIdentifier createSampleIdentifier(String sampleCode,
+            DataSetMappingInformation mapping)
     {
-        return new SampleIdentifier(new GroupIdentifier((String) null, groupCode), sampleCode);
+        return new SampleIdentifier(new GroupIdentifier((String) null, mapping.getGroupCode()),
+                sampleCode);
     }
 
-    private boolean isExperimentColumnCorrect(DataSetMappingInformation mapping, File logDir)
+    private boolean isExperimentColumnCorrect(DataSetMappingInformation mapping, LogUtils log)
     {
         if ((mapping.getExperimentCode() == null) != (mapping.getProjectCode() == null))
         {
-            logMappingError(mapping, logDir,
+            logMappingError(mapping, log,
                     "experiment and project columns should be both empty or should be both filled.");
             return false;
         }
@@ -252,7 +238,7 @@ class DatasetMappingResolver
         {
             logMappingError(
                     mapping,
-                    logDir,
+                    log,
                     "openBis is not configured to use the sample label to identify the sample."
                             + " You can still identify the sample by the code (clear the experiment column in this case)."
                             + " You can also contact your administrator to change the server configuration and set the property type code which should be used.");
@@ -261,10 +247,10 @@ class DatasetMappingResolver
         return true;
     }
 
-    private void logMappingError(DataSetMappingInformation mapping, File logDir, String errorMessage)
+    private void logMappingError(DataSetMappingInformation mapping, LogUtils log,
+            String errorMessage)
     {
-        LogUtils.error(logDir, "Mapping for file " + mapping.getFileName() + " is incorrect: "
-                + errorMessage);
+        log.userError("Mapping for file " + mapping.getFileName() + " is incorrect: " + errorMessage);
     }
 
     public static void adaptPropertyCodes(List<DataSetMappingInformation> list)
diff --git a/rtd_yeastx/source/java/ch/systemsx/cisd/yeastx/etl/DatasetMappingUtil.java b/rtd_yeastx/source/java/ch/systemsx/cisd/yeastx/etl/DatasetMappingUtil.java
index e5135eaf1a968d8c74d3dc6adc259a4bd60538fd..895b1eeda37e56de7b8e04c84bd934c43f135d7b 100644
--- a/rtd_yeastx/source/java/ch/systemsx/cisd/yeastx/etl/DatasetMappingUtil.java
+++ b/rtd_yeastx/source/java/ch/systemsx/cisd/yeastx/etl/DatasetMappingUtil.java
@@ -27,6 +27,7 @@ import java.util.Set;
 
 import org.apache.commons.io.FilenameUtils;
 import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.StringUtils;
 
 import ch.systemsx.cisd.common.collections.CollectionUtils;
 import ch.systemsx.cisd.common.collections.IKeyExtractor;
@@ -34,6 +35,7 @@ import ch.systemsx.cisd.common.collections.TableMap;
 import ch.systemsx.cisd.common.collections.TableMap.UniqueKeyViolationException;
 import ch.systemsx.cisd.common.collections.TableMap.UniqueKeyViolationStrategy;
 import ch.systemsx.cisd.common.filesystem.FileUtilities;
+import ch.systemsx.cisd.common.parser.TabFileLoader;
 
 /**
  * @author Tomasz Pylak
@@ -62,7 +64,8 @@ class DatasetMappingUtil
                     UniqueKeyViolationStrategy.ERROR);
         } catch (UniqueKeyViolationException e)
         {
-            LogUtils.error(logDir,
+            // TODO 2009-06-16, Tomasz Pylak: use email to send notifications
+            LogUtils.basicError(logDir,
                     "The file '%s' appears more than once. No datasets will be processed.", e
                             .getInvalidKey());
             return null;
@@ -87,18 +90,82 @@ class DatasetMappingUtil
         return datasetsMapping.tryGet(datasetFileName.toLowerCase());
     }
 
+    // returns the content of the first line comment or null if there is no comment or it is empty
+    private static String tryGetFirstLineCommentContent(File mappingFile)
+    {
+        List<String> lines;
+        try
+        {
+            lines = readLines(mappingFile);
+        } catch (IOException e)
+        {
+            errorInFile(mappingFile, e.getMessage());
+            return null;
+        }
+        if (lines.size() == 0)
+        {
+            return null;
+        }
+        String firstLine = lines.get(0);
+        if (StringUtils.isBlank(firstLine))
+        {
+            return null;
+        }
+        firstLine = firstLine.trim();
+        if (firstLine.startsWith(TabFileLoader.COMMENT_PREFIX) == false)
+        {
+            return null;
+        }
+        firstLine = firstLine.substring(1).trim();
+        return firstLine;
+    }
+
+    private static void errorInFile(File file, String message)
+    {
+        LogUtils.basicError(file.getParentFile(), "Error while reading the file '%s': %s", file
+                .getPath(), message);
+    }
+
+    // returns email address from the first line of the mapping file or null if there is no emai or
+    // it is invalid
+    private static String tryGetEmail(File mappingFile)
+    {
+        String email = tryGetFirstLineCommentContent(mappingFile);
+        if (email == null)
+        {
+            errorInFile(mappingFile, String.format(
+                    "There should be a '%s' character followed by an email address "
+                            + "in the first line of the file. "
+                            + "The email is needed to send messages about errors.",
+                    TabFileLoader.COMMENT_PREFIX));
+            return null;
+        }
+        if (email.contains("@") == false || email.contains(".") == false)
+        {
+            errorInFile(mappingFile, String.format(
+                    "The text '%s' does not seem to be an email address.", email));
+            return null;
+        }
+        return email;
+    }
+
     public static TableMap<String/* file name in lowercase */, DataSetMappingInformation> tryGetDatasetsMapping(
             File parentDir)
     {
         File mappingFile = tryGetMappingFile(parentDir);
         if (mappingFile == null)
         {
-            LogUtils.warn(parentDir, "Cannot process the directory '%s' "
+            LogUtils.basicWarn(parentDir, "Cannot process the directory '%s' "
                     + "because a file with extension '%s' which contains dataset descriptions "
                     + "does not exist or there is more than one.", parentDir.getPath(),
                     CollectionUtils.abbreviate(MAPPING_FILE_EXTENSIONS, -1));
             return null;
         }
+        String notificationEmail = tryGetEmail(mappingFile);
+        if (notificationEmail == null)
+        {
+            return null;
+        }
         List<DataSetMappingInformation> list =
                 DataSetMappingInformationParser.tryParse(mappingFile);
         if (list == null)
diff --git a/rtd_yeastx/source/java/ch/systemsx/cisd/yeastx/etl/LogUtils.java b/rtd_yeastx/source/java/ch/systemsx/cisd/yeastx/etl/LogUtils.java
index 936f638062ded6d8066e334ed91d64227d790f13..c7664344e461f08433366ebf9079b4583e69fa8e 100644
--- a/rtd_yeastx/source/java/ch/systemsx/cisd/yeastx/etl/LogUtils.java
+++ b/rtd_yeastx/source/java/ch/systemsx/cisd/yeastx/etl/LogUtils.java
@@ -40,34 +40,75 @@ class LogUtils
     private static final Logger operationLog =
             LogFactory.getLogger(LogCategory.OPERATION, LogUtils.class);
 
-    public static void error(File loggingDir, String messageFormat, Object... arguments)
+    private final File loggingDir;
+
+    private final StringBuffer messageToSend;
+
+    public LogUtils(File loggingDir)
     {
-        notifyUser(loggingDir, "ERROR", messageFormat, arguments);
-        adminError(messageFormat, arguments);
+        this.loggingDir = loggingDir;
+        this.messageToSend = new StringBuffer();
     }
 
-    public static void warn(File loggingDir, String messageFormat, Object... arguments)
+    public void userError(String messageFormat, Object... arguments)
     {
-        notifyUser(loggingDir, "WARNING", messageFormat, arguments);
-        adminWarn(messageFormat, arguments);
+        String message = basicError(loggingDir, messageFormat, arguments);
+        messageToSend.append(message);
     }
 
-    private static void notifyUser(File loggingDir, String messageKind, String messageFormat,
-            Object... arguments)
+    public void userWarning(String messageFormat, Object... arguments)
+    {
+        String message = basicWarn(loggingDir, messageFormat, arguments);
+        messageToSend.append(message);
+    }
+
+    public void sendNotificationsIfNecessary()
+    {
+        // TODO 2009-06-16, Tomasz Pylak: add email notification
+        if (messageToSend.length() > 0)
+        {
+            System.out.println("Email content: ");
+            System.out.println(messageToSend);
+        }
+    }
+
+    /** Adds an entry about an error to the user log file. Does not send an email. */
+    public static String basicError(File loggingDir, String messageFormat, Object... arguments)
+    {
+        String message = createUserMessage("ERROR", messageFormat, arguments);
+        notifyUserByLogFile(loggingDir, message);
+        return message;
+    }
+
+    /** Adds an entry about a warning to the user log file. Does not send an email. */
+    public static String basicWarn(File loggingDir, String messageFormat, Object... arguments)
+    {
+        String message = createUserMessage("WARNING", messageFormat, arguments);
+        notifyUserByLogFile(loggingDir, message);
+        return message;
+    }
+
+    private static void notifyUserByLogFile(File loggingDir, String message)
     {
-        String now = new Date().toString();
-        String message = now + " " + messageKind + ": " + format(messageFormat, arguments);
         OutputStream output;
         try
         {
             output = new FileOutputStream(getUserLogFile(loggingDir), true);
-            IOUtils.writeLines(Arrays.asList(message), "\n", output);
+            IOUtils.writeLines(Arrays.asList(message), "", output);
         } catch (IOException ex)
         {
             adminError("Cannot notify a user: " + ex.getMessage());
         }
     }
 
+    private static String createUserMessage(String messageKind, String messageFormat,
+            Object... arguments)
+    {
+        String now = new Date().toString();
+        String message = now + " " + messageKind + ": " + format(messageFormat, arguments) + "\n";
+        return message;
+    }
+
     private static File getUserLogFile(File loggingDir)
     {
         return new File(loggingDir, ConstantsYeastX.USER_LOG_FILE);
@@ -83,7 +124,7 @@ class LogUtils
         operationLog.warn(format(messageFormat, arguments));
     }
 
-    public static void info(String messageFormat, Object... arguments)
+    public static void adminInfo(String messageFormat, Object... arguments)
     {
         operationLog.info(format(messageFormat, arguments));
     }