diff --git a/2.System/0_setup.py b/2.System/0_setup.py
index 67b15b207bb32a9fa481a4e395e1ebe41858c246..456a4cc5be7fd31f72b5d644b79e3c914c086a73 100644
--- a/2.System/0_setup.py
+++ b/2.System/0_setup.py
@@ -18,7 +18,7 @@ def setup(interactive=True):
     ]
     # Create / update ubicast account
     cmds.append('echo "Checking ubicast account"')
-    code, out = utils.exec_cmd(['id', 'ubicast'], get_out=True)
+    code, out = utils.exec_cmd(['id', 'ubicast'])
     if code != 0:
         cmds.append('useradd -m -s /bin/bash ubicast')
         out = ''
@@ -29,7 +29,7 @@ def setup(interactive=True):
     # root
     cmds.append('mkdir -p /root/.ssh')
     cmds.append('chmod 700 /root/.ssh')
-    code, out = utils.exec_cmd(['rgrep', 'support@ubicast', '/root/.ssh'], get_out=True)
+    code, out = utils.exec_cmd(['rgrep', 'support@ubicast', '/root/.ssh'])
     if code != 0:
         cmds.append('cat "%s/ubicast_support.pub" >> /root/.ssh/authorized_keys' % dir_path)
     else:
@@ -37,7 +37,7 @@ def setup(interactive=True):
     # ubicast
     cmds.append('mkdir -p /home/ubicast/.ssh')
     cmds.append('chmod 700 /home/ubicast/.ssh')
-    code, out = utils.exec_cmd(['rgrep', 'support@ubicast', '/home/ubicast/.ssh'], get_out=True)
+    code, out = utils.exec_cmd(['rgrep', 'support@ubicast', '/home/ubicast/.ssh'])
     if code != 0:
         cmds.append('cat "%s/ubicast_support.pub" >> /home/ubicast/.ssh/authorized_keys' % dir_path)
     else:
diff --git a/4.Postfix/0_setup.py b/4.Postfix/0_setup.py
index e5f5cc5788f199eaf42bcc04b5891bf26e3a535d..68779a15ea95be54441668e2adfcfd5d5099d1d0 100644
--- a/4.Postfix/0_setup.py
+++ b/4.Postfix/0_setup.py
@@ -6,7 +6,7 @@ import utils
 def setup(interactive=True):
     # Get hostname
     utils.log('Getting system hostname.')
-    code, out = utils.exec_cmd(['hostname'], get_out=True)
+    code, out = utils.exec_cmd(['hostname'])
     if code == 0:
         hostname = out
         utils.log('Hostname is %s.' % hostname)
diff --git a/5.Wowza/0_setup.py b/5.Wowza/0_setup.py
index a1ca4691450fbe23610364234e50856cd6576909..62ee95e9e7df9e545ddb8738ec5cc82a81fc78cb 100644
--- a/5.Wowza/0_setup.py
+++ b/5.Wowza/0_setup.py
@@ -9,7 +9,7 @@ def setup(interactive=True):
     dir_path = utils.get_dir(__file__)
     wowza_setup_name = 'WowzaStreamingEngine-4.5.0-linux-x64-installer.deb'
     utils.log('It may take a while to download the Wowza installer from the UbiCast server.')
-    if utils.exec_cmd('lsb_release -a | grep 14') == 0:
+    if utils.check_cmd('lsb_release -a | grep 14') == 0:
         jre_package = 'openjdk-7-jre-headless'  # 14.04
     else:
         jre_package = 'openjdk-8-jre-headless'  # 16.04
diff --git a/6.Nginx/0_setup.py b/6.Nginx/0_setup.py
index 00053da72f29e7367b3bcade934544a2a4d8de77..14f931a8d4616712ba3507bc4c6f6d7092c66945 100644
--- a/6.Nginx/0_setup.py
+++ b/6.Nginx/0_setup.py
@@ -59,7 +59,7 @@ def setup(interactive=True):
         hosts.append(server_name)
     utils.run_commands(cmds)
     # Update hosts file
-    rc, hostname = utils.exec_cmd('hostname', get_out=True)
+    rc, hostname = utils.exec_cmd('hostname')
     if rc == 0 and hostname not in hosts:
         hosts.insert(0, hostname)
     with open('/etc/hosts', 'r') as fo:
diff --git a/default-conf.sh b/default-conf.sh
index 0612b4564fcf81d546c3c2b5db6e1e03917371d9..d510a7b91193169217b31e99bc3236c0fbbfa777 100644
--- a/default-conf.sh
+++ b/default-conf.sh
@@ -34,6 +34,8 @@ SHELL_ADMIN_PWD='test'
 # -- Emails --
 EMAIL_SMTP_SERVER=
 EMAIL_SENDER='support@ubicast.eu'
+# separate emails with comas in EMAIL_ADMINS
+EMAIL_ADMINS=
 
 # -- Wowza --
 WOWZA_LIVE_PWD='test'
diff --git a/envsetup.py b/envsetup.py
index 2939ded4617a41e8fbf427c3b5b5691247b7afe7..2cc3f7a8b1d2137134024c297290f9b91381418d 100755
--- a/envsetup.py
+++ b/envsetup.py
@@ -61,11 +61,11 @@ class EnvSetup():
             log('No action available.')
             sys.exit(1)
         # Check that this script is run by root
-        debug = '-d' in args
-        if debug:
+        self.debug = '-d' in args
+        if self.debug:
             args.remove('-d')
-        code, out = utils.exec_cmd(['whoami'], get_out=True)
-        if out != 'root' and not debug:
+        code, out = utils.exec_cmd(['whoami'])
+        if out != 'root' and not self.debug:
             log('This script should be run as root user.')
             sys.exit(1)
         # Load conf
@@ -95,6 +95,7 @@ class EnvSetup():
             else:
                 log('  \033[1;94m%s\033[0m' % (action['label']))
         log('')
+        log('  t: Run tests')
         log('  c: Configuration status')
         log('  e: Exit\n')
         log('Info:')
@@ -113,7 +114,13 @@ class EnvSetup():
             log('Exit')
             sys.exit(0)
         exit_code = 0
-        if target == 'c':
+        if target == 't':
+            # Run tests
+            args = [os.path.join(self.root_dir, 'tester.py'), 'tester.py']
+            if self.debug:
+                args.append('-d')
+            os.execl(*args)
+        elif target == 'c':
             # Display current configuration
             log('Configuration status:')
             override = utils.get_conf('_override')
diff --git a/tester.py b/tester.py
index b8878d78bd8926052a1a250e056153f2450c6535..43b9cca1178529089df15bd194e697cd5f28d746 100755
--- a/tester.py
+++ b/tester.py
@@ -3,13 +3,36 @@
 '''
 Script to start tests and to manage their results
 '''
+from io import StringIO
+import datetime
 import os
+import subprocess
 import sys
+import uuid
 
 import utils
 from utils import log
 
 
+class Logger(object):
+    def __init__(self, stream, log_buffer):
+        self.stream = stream
+        self.log_buffer = log_buffer
+
+    def write(self, text):
+        self.stream.write(text)
+        self.stream.flush()
+        self.log_buffer.write(text)
+        self.log_buffer.flush()
+
+    def flush(self):
+        pass
+
+log_buffer = StringIO()
+sys.stdout = Logger(sys.stdout, log_buffer)
+sys.stderr = sys.stdout
+
+
 class Tester():
     USAGE = '''%s [-e] [-d] [-h]
     -e: send email with report.
@@ -37,7 +60,7 @@ class Tester():
         debug = '-d' in args
         if debug:
             args.remove('-d')
-        code, out = utils.exec_cmd(['whoami'], get_out=True)
+        code, out = utils.exec_cmd(['whoami'])
         if out != 'root' and not debug:
             log('This script should be run as root user.')
             sys.exit(1)
@@ -49,7 +72,7 @@ class Tester():
         # Check for email value
         email = '-e' in args
         if email:
-            args.remove('-i')
+            args.remove('-e')
         exit_code = self.run_tests(email)
         sys.exit(exit_code)
 
@@ -63,13 +86,13 @@ class Tester():
                 start += 3
                 end = content.find('\'\'\'', start)
                 if end > 0:
-                    description = content[start:end].strip()
+                    description = content[start:end]
         else:
             for line in content.split('\n'):
                 if line.startswith('#!'):
                     continue
                 elif line.startswith('#'):
-                    description += line + '\n'
+                    description += line[1:].strip() + '\n'
                 else:
                     break
         return description.strip()
@@ -95,7 +118,7 @@ class Tester():
             description = self.get_file_description(test_path)
             # Run test
             try:
-                code = utils.exec_cmd(test_path)
+                code, out = utils.exec_cmd(test_path)
                 if code != 0:
                     raise Exception('Command exited with code %s.' % code)
             except Exception as e:
@@ -105,7 +128,7 @@ class Tester():
             else:
                 results.append((True, test_path, description))
         log('\nTests results:')
-        html_report = '<table>'
+        html_report = '<table border="1">'
         html_report += '\n<tr><th>Test</th><th>Result</th><th>Description</th></tr>'
         for success, test_path, description in results:
             file_name = os.path.basename(test_path)
@@ -116,8 +139,57 @@ class Tester():
                 html_result = '<span style="color: red;">failure</span>'
                 term_result = '\033[91mfailure\033[0m'
             log('  %s: %s' % (file_name, term_result))
-            html_report += '\n<tr><td>%s</td><td>%s</td><td>%s</td></tr>' % (file_name, html_result, description)
+            html_report += '\n<tr><td>%s</td><td>%s</td><td>%s</td></tr>' % (file_name, html_result, description.replace('\n', '<br/>\n'))
         html_report += '\n</table>'
+        # Send email
+        if email:
+            log('')
+            hostname = subprocess.check_output(['hostname'])
+            if not hostname:
+                log('Failed to get hostname (required to send email).')
+                return 1
+            recipients = utils.get_conf('EMAIL_ADMINS')
+            if not recipients:
+                log('No recipients defined for email sending. Set a value for EMAIL_ADMINS.')
+                return 1
+            boundary = str(uuid.uuid4())
+            now = datetime.datetime.utcnow()
+            mail = '''From: %(hostname)s <noreply@ubicast.eu>
+To: %(recipients)s
+Subject: UbiCast application test: %(status)s
+Mime-Version: 1.0
+Content-type: multipart/related; boundary="%(boundary)s"
+
+--%(boundary)s
+Content-Type: text/html; charset=UTF-8
+Content-transfer-encoding: utf-8
+
+<b>Date: %(date)s</b><br/><br/>
+%(report)s
+
+--%(boundary)s
+Content-type: application/octet-stream; name="%(log_name)s"
+Content-disposition: attachment; filename="%(log_name)s"
+Content-transfer-encoding: utf-8
+
+%(log_content)s
+''' % dict(
+                boundary=boundary,
+                hostname=hostname.decode('utf-8'),
+                recipients=recipients,
+                status='OK' if exit_code == 0 else 'KO',
+                date=now.strftime('%Y-%m-%d %H:%M:%S'),
+                log_name='results_' + now.strftime('%Y-%m-%d_%H-%M-%S') + '.txt',
+                report=html_report,
+                log_content=log_buffer.getvalue(),
+            )
+            p = subprocess.Popen(['sendmail', '-t'], stdin=subprocess.PIPE, stdout=sys.stdout.stream, stderr=sys.stderr.stream)
+            p.communicate(input=mail.encode('utf-8'))
+            if p.returncode != 0:
+                log('Failed to send email.')
+                return 1
+            else:
+                log('Email sent to: %s' % recipients)
         return exit_code
 
 
diff --git a/tests/test_apt.sh b/tests/test_apt.sh
index 6286b1f3204cbff76048e579e0a99338b967b671..04ca7dad57c5f67cc2c326d2de1f4131a2187925 100755
--- a/tests/test_apt.sh
+++ b/tests/test_apt.sh
@@ -1,4 +1,5 @@
 #!/bin/bash
+# Check that the installation of an Ubuntu package using APT works.
 set -e
 
 echo "Testing apt-get install"
diff --git a/tests/test_nginx_conf_valid.sh b/tests/test_nginx_conf_valid.sh
index 795259af998c35e11e3d3c631c1c8c33f66373ea..d6b5a3fa8b26816a9f2c0f7147a0ba9ec3dd898a 100755
--- a/tests/test_nginx_conf_valid.sh
+++ b/tests/test_nginx_conf_valid.sh
@@ -1,4 +1,5 @@
 #!/bin/bash
+# Check that the Nginx configuration files are valid.
 set -e
 
 if ( which nginx >/dev/null ); then
diff --git a/tests/test_ntp.sh b/tests/test_ntp.sh
index b8917be5175f6489ddf9ce452a0ffc79d62e2fc5..14652ae3bb57c70dd2ddc8b8229ecf0474e07f78 100755
--- a/tests/test_ntp.sh
+++ b/tests/test_ntp.sh
@@ -1,4 +1,5 @@
 #!/bin/bash
+# Check that the NTP server is reachable (date synchronization).
 set -e
 
 source /root/envsetup/conf.sh
diff --git a/tests/test_postfix.py b/tests/test_postfix.py
index 3e0ec03d2b449347429991ced0caa210baf6fdf0..eb825cec1e7d9bef514497d4c9801f305bbb2292 100755
--- a/tests/test_postfix.py
+++ b/tests/test_postfix.py
@@ -2,6 +2,7 @@
 # -*- coding: utf-8 -*-
 '''
 Check that postfix listen correctly the port 25.
+Postfix is the service which handles email sendings.
 '''
 import os
 import re
diff --git a/tests/test_ubicast_packages_access.py b/tests/test_ubicast_packages_access.py
index 0e9a9f4b02645d0aaee4e3347a7d476890f1837c..fa4e87ac3cc5c59a4d9c5607887577524f152906 100755
--- a/tests/test_ubicast_packages_access.py
+++ b/tests/test_ubicast_packages_access.py
@@ -34,7 +34,7 @@ if content:
 if not url or not api_key:
     print('The file "%s" is not correct: skyreach url not found.' % apt_source)
     sys.exit(1)
-print('SkyReach url is %s and API key is %s.' % (url, api_key))
+print('SkyReach url is %s and API key is %s[...].' % (url, api_key[:8]))
 
 # Test SkyReach responses
 req = requests.get(url)
@@ -46,6 +46,7 @@ else:
 
 apt_url = '%s/packaging/apt/%s/Packages' % (url, api_key)
 req = requests.get(apt_url)
+apt_url = apt_url.replace(api_key, api_key[:8] + '[...]')
 if not req.ok:
     print('Request to %s failed (%s):' % (apt_url, req.status_code))
     print(req.text)
diff --git a/utils.py b/utils.py
index c6badef48dffed5cc47268d091bad55971097361..cb47cc1349163654d23701c421e40187ded90aa3 100644
--- a/utils.py
+++ b/utils.py
@@ -21,23 +21,24 @@ def get_dir(file_path):
     return os.path.dirname(os.path.abspath(os.path.expanduser(file_path)))
 
 
-def exec_cmd(cmd, get_out=False):
-    stdout = subprocess.PIPE if get_out else sys.stdout
-    stderr = subprocess.PIPE if get_out else sys.stderr
+def exec_cmd(cmd, log_output=True):
     shell = not isinstance(cmd, (tuple, list))
-    p = subprocess.Popen(cmd, stdin=sys.stdin, stdout=stdout, stderr=stderr, shell=shell)
+    p = subprocess.Popen(cmd, stdin=sys.stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=shell)
     out, err = p.communicate()
-    if get_out:
-        out = out.decode('utf-8').strip() if out else ''
-        if err:
-            if out:
-                out += '\n'
-            out += 'Stderr:\n' + err.decode('utf-8').strip()
-        return p.returncode, out
-    else:
-        sys.stdout.flush()
-        sys.stderr.flush()
-    return p.returncode
+    out = out.decode('utf-8').strip() if out else ''
+    if err:
+        if out:
+            out += '\n'
+        out += err.decode('utf-8').strip()
+    out = out.strip()
+    if log_output:
+        log(out)
+    return p.returncode, out
+
+
+def check_cmd(cmd):
+    code, out = exec_cmd(cmd, log_output=False)
+    return code
 
 
 def load_conf():
@@ -97,7 +98,7 @@ def run_commands(cmds):
                 cond = cmd['cond']
                 negate = cmd.get('cond_neg')
                 skip = cmd.get('cond_skip')
-                code = exec_cmd(cond)
+                code = check_cmd(cond)
                 success = code != 0 if negate else code == 0
                 if not success:
                     msg = 'Condition for command "%s" not fullfilled.' % cmd['line']
@@ -135,7 +136,7 @@ def run_commands(cmds):
                     log('A backup file already exist for:\n%s' % cmd['target'])
             else:
                 log('>>> ' + cmd['line'])
-                code = exec_cmd(cmd['line'])
+                code = check_cmd(cmd['line'])
                 if code != 0:
                     raise Exception('Command exited with code %s.' % code)
     except Exception as e: