diff --git a/README_SETUP.md b/README_SETUP.md
index 34d6c77f4eb32192465f4620658089f406836a64..426867f6e9c51f74104ac1c769ace12a20f9d50c 100644
--- a/README_SETUP.md
+++ b/README_SETUP.md
@@ -10,6 +10,12 @@
 * >=Screen 4.01
 * >=MySQL 5.5 (better >=5.6) or >=MariaDB 10.1
 
+### Install the requirements on Debian
+On Debian, the required packages can be installed with:
+
+    apt-get install git make mariadb-server maven openjdk-8-jdk-headless \
+      python3-pip screen
+
 ## System
 
 * >=Linux 4.0.0, x86\_64, e.g. Ubuntu 14.04.1
diff --git a/conf/cache.ccf b/conf/cache.ccf
new file mode 100644
index 0000000000000000000000000000000000000000..87d1338a5cf70da9b551744bc154190965d4bb46
--- /dev/null
+++ b/conf/cache.ccf
@@ -0,0 +1,3 @@
+jcs.default.cacheattributes=org.apache.commons.jcs.engine.CompositeCacheAttributes
+jcs.default.cacheattributes.MaxObjects=1000
+jcs.default.cacheattributes.MemoryCacheName=org.apache.commons.jcs.engine.memory.lru.LRUMemoryCache
\ No newline at end of file
diff --git a/conf/logging.conf b/conf/logging.conf
new file mode 100644
index 0000000000000000000000000000000000000000..c4ddcf78b8956daeb71768674812fc61ff3d8d19
--- /dev/null
+++ b/conf/logging.conf
@@ -0,0 +1,5 @@
+org.restlet.handlers = java.util.logging.ConsoleHandler
+caosdb.server.handlers = caosdb.server.logging.BackendLoggingHandler, java.util.logging.ConsoleHandler
+
+java.util.logging.ConsoleHandler.level = INFO
+caosdb.server.logging.BackendLogging.Handler.level = INFO
\ No newline at end of file
diff --git a/src/main/java/caosdb/server/CaosDBException.java b/src/main/java/caosdb/server/CaosDBException.java
index adfca3d06f83d990ac540318138093cd0485cf7b..3e0e50769cd3f08423ea83fa851b2cdfca54e50f 100644
--- a/src/main/java/caosdb/server/CaosDBException.java
+++ b/src/main/java/caosdb/server/CaosDBException.java
@@ -22,8 +22,8 @@
  */
 package caosdb.server;
 
-public class CaosDBException extends Exception {
-  /** */
+public class CaosDBException extends RuntimeException {
+
   private static final long serialVersionUID = 5317733089121727021L;
 
   public CaosDBException(final String string) {
diff --git a/src/main/java/caosdb/server/CaosDBServer.java b/src/main/java/caosdb/server/CaosDBServer.java
index 5f2d31409d831d2da39318286cdeedee1c7b3566..811137780cee9c83e787af92d0d9f50b67272ed0 100644
--- a/src/main/java/caosdb/server/CaosDBServer.java
+++ b/src/main/java/caosdb/server/CaosDBServer.java
@@ -1,22 +1,19 @@
 /*
- * ** header v3.0
- * This file is a part of the CaosDB Project.
+ * ** header v3.0 This file is a part of the CaosDB Project.
  *
- * Copyright (C) 2018 Research Group Biomedical Physics,
- * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ * Copyright (C) 2018 Research Group Biomedical Physics, Max-Planck-Institute for Dynamics and
+ * Self-Organization Göttingen
  *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
+ * This program is free software: you can redistribute it and/or modify it under the terms of the
+ * GNU Affero General Public License as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
  *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
+ * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Affero General Public License for more details.
  *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ * You should have received a copy of the GNU Affero General Public License along with this program.
+ * If not, see <https://www.gnu.org/licenses/>.
  *
  * ** end header
  */
@@ -48,6 +45,7 @@ import caosdb.server.resource.InfoResource;
 import caosdb.server.resource.LogoutResource;
 import caosdb.server.resource.PermissionRulesResource;
 import caosdb.server.resource.RolesResource;
+import caosdb.server.resource.ScriptingResource;
 import caosdb.server.resource.ServerLogsResource;
 import caosdb.server.resource.TestCaseFileSystemResource;
 import caosdb.server.resource.TestCaseResource;
@@ -533,6 +531,7 @@ public class CaosDBServer extends Application {
     // After authentication comes authorization...
     authenticator.setNext(authorizer);
 
+    protectedRouter.attach("/scripting", ScriptingResource.class);
     protectedRouter.attach("/Entities", EntityResource.class);
     protectedRouter.attach("/Entities/", EntityResource.class);
     protectedRouter.attach("/Entities/{specifier}", EntityResource.class);
@@ -582,12 +581,10 @@ public class CaosDBServer extends Application {
     protectedRouter.attachDefault(DefaultResource.class);
 
     /*
-     * Dirty Hack - no clean solution found yet (except for patching the
-     * restlet framework). The logging handler causes a NullPointerException
-     * when the Template.match method logs a warning. This warning
-     * (seemingly) always means that the RequestUri cannot be matched
-     * because its to long and causes a StackOverflow. Therefore we want to
-     * generate an HTTP 414 error.
+     * Dirty Hack - no clean solution found yet (except for patching the restlet framework). The
+     * logging handler causes a NullPointerException when the Template.match method logs a warning.
+     * This warning (seemingly) always means that the RequestUri cannot be matched because its to
+     * long and causes a StackOverflow. Therefore we want to generate an HTTP 414 error.
      */
 
     final Handler handler =
@@ -697,6 +694,20 @@ public class CaosDBServer extends Application {
   public static boolean isDebugMode() {
     return DEBUG_MODE;
   }
+
+  /**
+   * Set a server property to a new value. This might not have an immediate effect if classes did
+   * already read an older configuration and stick to that.
+   *
+   * @param key, the server property.
+   * @param value, the new value.
+   */
+  public static void setProperty(String key, String value) {
+    if (SERVER_PROPERTIES == null) {
+      SERVER_PROPERTIES = ServerProperties.initServerProperties();
+    }
+    SERVER_PROPERTIES.setProperty(key, value);
+  }
 }
 
 class CaosDBComponent extends Component {
diff --git a/src/main/java/caosdb/server/ServerProperties.java b/src/main/java/caosdb/server/ServerProperties.java
index 9a33d2281b152e4aa23ceef59c074d625e690f02..011c9a6f599b596895298b2b11b54f9d7351665c 100644
--- a/src/main/java/caosdb/server/ServerProperties.java
+++ b/src/main/java/caosdb/server/ServerProperties.java
@@ -113,6 +113,10 @@ public class ServerProperties extends Properties {
 
   public static final String KEY_SERVER_OWNER = "SERVER_OWNER";
 
+  public static final String KEY_SERVER_SIDE_SCRIPTING_BIN_DIR = "SERVER_SIDE_SCRIPTING_BIN_DIR";
+  public static final String KEY_SERVER_SIDE_SCRIPTING_WORKING_DIR =
+      "SERVER_SIDE_SCRIPTING_WORKING_DIR";
+
   /**
    * This init_server_properties method reads the config file which contains key-value-pairs for
    * such variables like the user name of the database, the port the server will be listening on
@@ -123,6 +127,9 @@ public class ServerProperties extends Properties {
     final String basepath = System.getProperty("user.dir");
     serverProperties.setProperty(KEY_SERVER_OWNER, "");
     serverProperties.setProperty(KEY_SERVER_NAME, "CaosDB Server");
+    serverProperties.setProperty(KEY_SERVER_SIDE_SCRIPTING_BIN_DIR, basepath + "/scripting/bin/");
+    serverProperties.setProperty(
+        KEY_SERVER_SIDE_SCRIPTING_WORKING_DIR, basepath + "/scripting/working/");
     serverProperties.setProperty(KEY_FILE_SYSTEM_ROOT, "CaosDBFileSystem/FileSystemRoot/");
     serverProperties.setProperty(KEY_DROP_OFF_BOX, "CaosDBFileSystem/DropOffBox/");
     serverProperties.setProperty(KEY_TMP_FILES, "CaosDBFileSystem/TMP/");
@@ -165,7 +172,7 @@ public class ServerProperties extends Properties {
     serverProperties.setProperty(KEY_ACTIVATION_TIMEOUT_MS, "604800000"); // 7days
 
     serverProperties.setProperty(KEY_MAIL_HANDLER_CLASS, "caosdb.server.utils.mail.ToFileHandler");
-    serverProperties.setProperty(KEY_MAIL_TO_FILE_HANDLER_LOC, "./OUTBOX");
+    serverProperties.setProperty(KEY_MAIL_TO_FILE_HANDLER_LOC, "./");
 
     serverProperties.setProperty(KEY_ADMIN_NAME, "CaosDB Admin");
     serverProperties.setProperty(KEY_ADMIN_EMAIL, "");
diff --git a/src/main/java/caosdb/server/entity/FileProperties.java b/src/main/java/caosdb/server/entity/FileProperties.java
index e31e9f9e2a253d59dd8164ec8c9a7a1c223ce006..7263b9b2b5e14f51988fa13dd969cf47f941478c 100644
--- a/src/main/java/caosdb/server/entity/FileProperties.java
+++ b/src/main/java/caosdb/server/entity/FileProperties.java
@@ -217,7 +217,7 @@ public class FileProperties {
   }
 
   private static Undoable delete(final File file)
-      throws IOException, InterruptedException, CaosDBException {
+      throws IOException, InterruptedException {
     if (file.getAbsolutePath().startsWith(FileSystem.getBasepath())) {
       final Undoable d;
       final File parent = file.getParentFile();
diff --git a/src/main/java/caosdb/server/resource/AbstractCaosDBServerResource.java b/src/main/java/caosdb/server/resource/AbstractCaosDBServerResource.java
index fb8196841e2e0ffb33273add737e8cf26e2034c0..e390d3c55576e774a78e89f17de73ed950b78cf4 100644
--- a/src/main/java/caosdb/server/resource/AbstractCaosDBServerResource.java
+++ b/src/main/java/caosdb/server/resource/AbstractCaosDBServerResource.java
@@ -39,6 +39,7 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.NoSuchElementException;
+import java.util.logging.Level;
 import org.apache.commons.fileupload.FileUploadException;
 import org.apache.shiro.SecurityUtils;
 import org.apache.shiro.authc.AuthenticationException;
@@ -255,6 +256,10 @@ public abstract class AbstractCaosDBServerResource extends ServerResource {
     this.xslScript = s;
   }
 
+  protected JdomRepresentation ok(Element root) {
+    return ok(new Document(root));
+  }
+
   protected JdomRepresentation ok(final Document doc) {
     return new JdomRepresentation(doc, MediaType.TEXT_XML, "  ", getReference(), getXSLScript());
   }
@@ -313,6 +318,7 @@ public abstract class AbstractCaosDBServerResource extends ServerResource {
     } catch (final JDOMException e) {
       return noWellFormedNess();
     } catch (final Throwable e) {
+      getLogger().log(Level.SEVERE, "UNKNOWN ERROR", e);
       return error(ServerMessages.UNKNOWN_ERROR(getSRID()));
     }
   }
@@ -337,6 +343,14 @@ public abstract class AbstractCaosDBServerResource extends ServerResource {
     return this.flags;
   }
 
+  protected Element generateRootElement(Element... elements) {
+    Element root = generateRootElement();
+    for (Element e : elements) {
+      root.addContent(e);
+    }
+    return root;
+  }
+
   public static class XMLParser {
     private final LinkedList<SAXBuilder> pool = new LinkedList<SAXBuilder>();
     private final int max = 25;
diff --git a/src/main/java/caosdb/server/resource/ScriptingResource.java b/src/main/java/caosdb/server/resource/ScriptingResource.java
new file mode 100644
index 0000000000000000000000000000000000000000..4687b2c5ad241baddc6662c5fb043ac955839ff6
--- /dev/null
+++ b/src/main/java/caosdb/server/resource/ScriptingResource.java
@@ -0,0 +1,210 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2018 Research Group Biomedical Physics,
+ * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+package caosdb.server.resource;
+
+import caosdb.server.FileSystem;
+import caosdb.server.accessControl.Principal;
+import caosdb.server.accessControl.SessionToken;
+import caosdb.server.entity.FileProperties;
+import caosdb.server.entity.Message;
+import caosdb.server.scripting.CallerSerializer;
+import caosdb.server.scripting.ServerSideScriptingCaller;
+import caosdb.server.utils.Serializer;
+import caosdb.server.utils.ServerMessages;
+import caosdb.server.utils.Utils;
+import com.ibm.icu.text.Collator;
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.apache.commons.fileupload.FileItemIterator;
+import org.apache.commons.fileupload.FileItemStream;
+import org.apache.commons.fileupload.FileUploadException;
+import org.apache.commons.fileupload.disk.DiskFileItemFactory;
+import org.jdom2.Element;
+import org.restlet.data.CharacterSet;
+import org.restlet.data.Form;
+import org.restlet.data.MediaType;
+import org.restlet.data.Parameter;
+import org.restlet.data.Status;
+import org.restlet.engine.header.ContentType;
+import org.restlet.ext.fileupload.RestletFileUpload;
+import org.restlet.representation.Representation;
+
+public class ScriptingResource extends AbstractCaosDBServerResource {
+
+  private ServerSideScriptingCaller caller;
+  private Collection<FileProperties> deleteFiles = new LinkedList<>();
+
+  @Override
+  public Logger getLogger() {
+    return Logger.getLogger(this.getClass().getName());
+  }
+
+  public Element generateRootElement(ServerSideScriptingCaller caller) {
+    Serializer<ServerSideScriptingCaller, Element> xmlSerializer = new CallerSerializer();
+    Element callerElement = xmlSerializer.serialize(caller);
+
+    return generateRootElement(callerElement);
+  }
+
+  @Override
+  protected Representation httpPostInChildClass(Representation entity) throws Exception {
+
+    MediaType mediaType = entity.getMediaType();
+    try {
+      if (mediaType.equals(MediaType.MULTIPART_FORM_DATA, true)) {
+        handleMultiparts(entity);
+      } else if (mediaType.equals(MediaType.APPLICATION_WWW_FORM)) {
+        handleForm(new Form(entity));
+      } else {
+        getResponse().setStatus(Status.CLIENT_ERROR_UNSUPPORTED_MEDIA_TYPE);
+        return null;
+      }
+    } catch (Message m) {
+      return error(m, Status.valueOf(m.getCode()));
+    } finally {
+      deleteTmpFiles();
+    }
+    return ok(generateRootElement(this.caller));
+  }
+
+  private void deleteTmpFiles() {
+    for (FileProperties p : deleteFiles) {
+      try {
+      p.getFile().delete();
+      } catch (Exception t) {
+        if (getLogger().isLoggable(Level.WARNING)) {
+          getLogger().warning("Could not delete tmp file: " + p.getPath() + "\nException: " + t.toString());
+        }
+      }
+    }
+  }
+
+  public int handleMultiparts(Representation entity)
+      throws FileUploadException, IOException, NoSuchAlgorithmException, Message {
+    final DiskFileItemFactory factory = new DiskFileItemFactory();
+    factory.setSizeThreshold(1000240);
+    final RestletFileUpload upload = new RestletFileUpload(factory);
+    final FileItemIterator iter = upload.getItemIterator(entity);
+
+    List<FileProperties> files = new ArrayList<>();
+    Form form = new Form();
+
+    while (iter.hasNext()) {
+      FileItemStream item = iter.next();
+      if (item.isFormField()) {
+        // put plain text form field into the form
+        CharacterSet characterSet = ContentType.readCharacterSet(item.getContentType());
+        String value =
+            Utils.InputStream2String(
+                item.openStream(),
+                (characterSet != null ? characterSet.toString() : CharacterSet.UTF_8.toString()));
+        form.add(new Parameter(item.getFieldName(), value));
+      } else {
+        // -> this is a file, store in tmp dir
+        final FileProperties file = FileSystem.upload(item, this.getSRID());
+        deleteTmpFileAfterTermination(file);
+        file.setPath(item.getName());
+        file.setTmpIdentifyer(item.getFieldName());
+        files.add(file);
+        form.add(
+            new Parameter(
+                item.getFieldName(),
+                ServerSideScriptingCaller.UPLOAD_FILES_DIR + "/" + item.getName()));
+      }
+    }
+    return callScript(form, files);
+  }
+
+  private void deleteTmpFileAfterTermination(FileProperties file) {
+    deleteFiles.add(file);
+  }
+
+  public int handleForm(Form form) throws Message {
+    return callScript(form, null);
+  }
+
+  public List<String> form2CommandLine(Form form) throws Message {
+    ArrayList<String> commandLine = new ArrayList<>(form.size());
+    ArrayList<Parameter> positionalArgs = new ArrayList<>(form.size() - 1);
+
+    commandLine.add(""); // will be replaced by the "call" value
+
+    for (Parameter p : form) {
+      if (p.getName().startsWith("-p")) {
+        positionalArgs.add(p);
+      } else if (p.getName().startsWith("-O")) {
+        commandLine.add(String.format("--%s=%s", p.getName().substring(2), p.getValue()));
+      } else if (p.getName().equals("call")) {
+        commandLine.set(0, p.getValue());
+      }
+    }
+
+    // sort positional arguments.
+    positionalArgs.sort((o1, o2) -> Collator.getInstance().compare(o1.getName(), o2.getName()));
+    for (Parameter p : positionalArgs) {
+      commandLine.add(p.getValue());
+    }
+
+    if (commandLine.get(0).length() == 0) {
+      // first item still empty
+      throw ServerMessages.SERVER_SIDE_SCRIPT_MISSING_CALL;
+    }
+    return commandLine;
+  }
+
+  public int callScript(Form form, List<FileProperties> files) throws Message {
+    List<String> commandLine = form2CommandLine(form);
+    Integer timeoutMs = Integer.valueOf(form.getFirstValue("timeout", "-1"));
+    return callScript(commandLine, timeoutMs, files);
+  }
+
+  public int callScript(List<String> commandLine, Integer timeoutMs, List<FileProperties> files)
+      throws Message {
+    return callScript(commandLine, timeoutMs, files, generateAuthToken());
+  }
+
+  public Object generateAuthToken() {
+    return SessionToken.generate((Principal) getUser().getPrincipal(), null);
+  }
+
+  public int callScript(
+      List<String> commandLine, Integer timeoutMs, List<FileProperties> files, Object authToken)
+      throws Message {
+    caller =
+        new ServerSideScriptingCaller(
+            commandLine.toArray(new String[commandLine.size()]), timeoutMs, files, authToken);
+    return caller.invoke();
+  }
+
+  @Override
+  protected Representation httpGetInChildClass() throws Exception {
+    getResponse().setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED);
+    return null;
+  }
+}
diff --git a/src/main/java/caosdb/server/scripting/CallerSerializer.java b/src/main/java/caosdb/server/scripting/CallerSerializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..1bd265650bed0d5de865cc91b29db3327b56174b
--- /dev/null
+++ b/src/main/java/caosdb/server/scripting/CallerSerializer.java
@@ -0,0 +1,36 @@
+package caosdb.server.scripting;
+
+import caosdb.server.CaosDBException;
+import caosdb.server.utils.Serializer;
+import java.io.IOException;
+import org.jdom2.Element;
+
+public class CallerSerializer implements Serializer<ServerSideScriptingCaller, Element> {
+
+  @Override
+  public Element serialize(ServerSideScriptingCaller caller) {
+    Element command = new Element("call");
+    command.setText(String.join(" ", caller.getCommandLine()));
+
+    Element stdout = new Element("stdout");
+    try {
+      stdout.addContent(caller.getStdOut());
+    } catch (IOException e) {
+      throw new CaosDBException(e);
+    }
+
+    Element stderr = new Element("stderr");
+    try {
+      stderr.addContent(caller.getStdErr());
+    } catch (IOException e) {
+      throw new CaosDBException(e);
+    }
+
+    Element script = new Element("script");
+    script.setAttribute("code", caller.getCode().toString());
+    script.addContent(command);
+    script.addContent(stdout);
+    script.addContent(stderr);
+    return script;
+  }
+}
diff --git a/src/main/java/caosdb/server/scripting/ScriptingUtils.java b/src/main/java/caosdb/server/scripting/ScriptingUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..cee65faf9b25d3b9bfe0b28cc508edf59626682c
--- /dev/null
+++ b/src/main/java/caosdb/server/scripting/ScriptingUtils.java
@@ -0,0 +1,96 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2018 Research Group Biomedical Physics,
+ * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+package caosdb.server.scripting;
+
+import caosdb.server.CaosDBServer;
+import caosdb.server.ServerProperties;
+import caosdb.server.entity.Message;
+import caosdb.server.utils.ConfigurationException;
+import caosdb.server.utils.ServerMessages;
+import caosdb.server.utils.Utils;
+import java.io.File;
+import java.nio.file.Path;
+
+public class ScriptingUtils {
+
+  private File bin;
+  private File working;
+
+  public ScriptingUtils() {
+    this.bin =
+        new File(
+            CaosDBServer.getServerProperty(ServerProperties.KEY_SERVER_SIDE_SCRIPTING_BIN_DIR));
+    this.working =
+        new File(
+            CaosDBServer.getServerProperty(ServerProperties.KEY_SERVER_SIDE_SCRIPTING_WORKING_DIR));
+    if (!bin.exists()) {
+      bin.mkdirs();
+    }
+    if (!bin.isDirectory()) {
+      throw new ConfigurationException(
+          "The ServerProperty `"
+              + ServerProperties.KEY_SERVER_SIDE_SCRIPTING_BIN_DIR
+              + "` must point to a directory");
+    }
+
+    if (!working.exists()) {
+      working.mkdirs();
+    }
+    if (!working.isDirectory()) {
+      throw new ConfigurationException(
+          "The ServerProperty `"
+              + ServerProperties.KEY_SERVER_SIDE_SCRIPTING_WORKING_DIR
+              + "` must point to a directory");
+    }
+  }
+
+  public File getScriptFile(final String command) {
+    final Path script = bin.toPath().resolve(command);
+    return script.toFile();
+  }
+
+  public void checkScriptExists(final String command) throws Message {
+    if (!getScriptFile(command).exists()) {
+      throw ServerMessages.SERVER_SIDE_SCRIPT_DOES_NOT_EXIST;
+    }
+  }
+
+  public void checkScriptExecutable(final String command) throws Message {
+    if (!getScriptFile(command).canExecute()) {
+      throw ServerMessages.SERVER_SIDE_SCRIPT_NOT_EXECUTABLE;
+    }
+  }
+
+  public File getTmpWorkingDir() {
+    String uid = Utils.getUID();
+    return working.toPath().resolve(uid).toFile();
+  }
+
+  public File getStdOutFile(File workingDir) {
+    return workingDir.toPath().resolve(".STDOUT").toFile();
+  }
+
+  public File getStdErrFile(File workingDir) {
+    return workingDir.toPath().resolve(".STDERR").toFile();
+  }
+}
diff --git a/src/main/java/caosdb/server/scripting/ServerSideScriptingCaller.java b/src/main/java/caosdb/server/scripting/ServerSideScriptingCaller.java
new file mode 100644
index 0000000000000000000000000000000000000000..2455916ec6245f1e721908b6e91f65b230b60a3a
--- /dev/null
+++ b/src/main/java/caosdb/server/scripting/ServerSideScriptingCaller.java
@@ -0,0 +1,237 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2018 Research Group Biomedical Physics,
+ * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+package caosdb.server.scripting;
+
+import caosdb.server.CaosDBException;
+import caosdb.server.entity.FileProperties;
+import caosdb.server.entity.Message;
+import caosdb.server.utils.ServerMessages;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.lang.ProcessBuilder.Redirect;
+import java.util.Collection;
+import java.util.List;
+import org.apache.commons.io.FileUtils;
+
+public class ServerSideScriptingCaller {
+
+  public static final String UPLOAD_FILES_DIR = ".upload_files";
+  public static final Integer STARTED = -1;
+  private final String[] commandLine;
+  private final int timeoutMs;
+  private ScriptingUtils utils;
+  private List<FileProperties> files;
+  private File workingDir;
+  private Object authToken;
+  private File stdOutFile;
+  private File stdErrFile;
+  private String stdErr = null;
+  private String stdOut;
+  private Integer code = null;
+
+  public Integer getCode() {
+    return code;
+  }
+
+  File getUploadFilesDir() {
+    return getTmpWorkingDir().toPath().resolve(UPLOAD_FILES_DIR).toFile();
+  }
+
+  public ServerSideScriptingCaller(
+      String[] commandLine, Integer timeoutMs, List<FileProperties> files, Object authToken) {
+    this(commandLine, timeoutMs, files, authToken, new ScriptingUtils());
+  }
+
+  public ServerSideScriptingCaller(
+      String[] commandLine,
+      Integer timeoutMs,
+      List<FileProperties> files,
+      Object authToken,
+      ScriptingUtils utils) {
+    this(commandLine, timeoutMs, files, authToken, utils, utils.getTmpWorkingDir());
+  }
+
+  public ServerSideScriptingCaller(
+      String[] commandLine,
+      Integer timeoutMs,
+      List<FileProperties> files,
+      Object authToken,
+      ScriptingUtils utils,
+      File workingDir) {
+    this.utils = utils;
+    this.files = files;
+    this.commandLine = commandLine;
+    this.timeoutMs = timeoutMs;
+    this.authToken = authToken;
+    this.workingDir = workingDir;
+    this.stdOutFile = utils.getStdOutFile(workingDir);
+    this.stdErrFile = utils.getStdErrFile(workingDir);
+  }
+
+  public int invoke() throws Message {
+    try {
+      checkCommandLine(commandLine);
+      try {
+        createWorkingDir();
+        putFilesInWorkingDir(files);
+      } catch (final Exception e) {
+        e.printStackTrace();
+        throw ServerMessages.SERVER_SIDE_SCRIPT_SETUP_ERROR;
+      }
+
+      try {
+        return callScript();
+      } catch (TimeoutException e) {
+        throw ServerMessages.SERVER_SIDE_SCRIPT_TIMEOUT;
+      } catch (final Throwable e) {
+        e.printStackTrace();
+        throw ServerMessages.SERVER_SIDE_SCRIPT_ERROR;
+      }
+    } finally {
+      cleanup();
+    }
+  }
+
+  void checkCommandLine(String[] commandLine) throws Message {
+    utils.checkScriptExists(commandLine[0]);
+    utils.checkScriptExecutable(commandLine[0]);
+  }
+
+  void putFilesInWorkingDir(final Collection<FileProperties> files)
+      throws FileNotFoundException, CaosDBException {
+    if (files == null) {
+      return;
+    }
+
+    // create files dir
+    if (!getUploadFilesDir().mkdir()) {
+      throw new FileNotFoundException("Could not crete the FILE_UPLOAD_DIR.");
+    }
+    for (final FileProperties f : files) {
+      if (f.getPath() == null || f.getPath().isEmpty()) {
+        throw new CaosDBException("The path must not be null or empty!");
+      }
+      caosdb.server.utils.FileUtils.createSymlink(
+          getUploadFilesDir().toPath().resolve(f.getPath()).toFile(), f.getFile());
+    }
+  }
+
+  void createWorkingDir() throws Exception {
+    if (getTmpWorkingDir().exists()) {
+      throw new Exception("The working directory must be non-existing when the caller is invoked.");
+    }
+
+    // create working dir
+    getTmpWorkingDir().mkdirs();
+  }
+
+  void cleanup() {
+    // catch exception and throw afterwards,
+    IOException e1 = null;
+    try {
+      // cache script outputs
+      getStdErr();
+      getStdOut();
+    } catch (final IOException e2) {
+      e1 = e2;
+    }
+    try {
+      deleteWorkingDir(getTmpWorkingDir());
+    } catch (final IOException e2) {
+      if (e1 == null) {
+        e1 = e2;
+      } else {
+        e1.addSuppressed(e2);
+      }
+    }
+    if (e1 != null) throw new RuntimeException("Cleanup failed.", e1);
+  }
+
+  void deleteWorkingDir(final File pwd) throws IOException {
+    if (pwd.exists()) FileUtils.forceDelete(pwd);
+  }
+
+  String makeCallAbsolute(String call) {
+    return utils.getScriptFile(call).getAbsolutePath();
+  }
+
+  String[] injectAuthToken(String[] commandLine) {
+    String[] newCommandLine = new String[commandLine.length + 1];
+    newCommandLine[0] = commandLine[0];
+    newCommandLine[1] = "--auth-token=" + authToken.toString();
+    for (int i = 2; i < newCommandLine.length; i++) {
+      newCommandLine[i] = commandLine[i - 1];
+    }
+    return newCommandLine;
+  }
+
+  int callScript() throws IOException, InterruptedException, TimeoutException {
+    String[] effectiveCommandLine = injectAuthToken(getCommandLine());
+    effectiveCommandLine[0] = makeCallAbsolute(effectiveCommandLine[0]);
+    final ProcessBuilder pb = new ProcessBuilder(effectiveCommandLine);
+    pb.redirectOutput(Redirect.to(getStdOutFile()));
+    pb.redirectError(Redirect.to(getStdErrFile()));
+    pb.directory(getTmpWorkingDir());
+
+    code = STARTED;
+    final TimeoutProcess process = new TimeoutProcess(pb.start(), getTimeoutMs());
+
+    code = process.waitFor();
+    return code;
+  }
+
+  public File getStdOutFile() {
+    return stdOutFile;
+  }
+
+  public File getStdErrFile() {
+    return stdErrFile;
+  }
+
+  public String[] getCommandLine() {
+    return this.commandLine;
+  }
+
+  File getTmpWorkingDir() {
+    return this.workingDir;
+  }
+
+  int getTimeoutMs() {
+    return this.timeoutMs;
+  }
+
+  public String getStdErr() throws IOException {
+    if (stdErr == null && getStdErrFile().exists()) {
+      stdErr = FileUtils.readFileToString(getStdErrFile(), "UTF-8");
+    }
+    return stdErr;
+  }
+
+  public String getStdOut() throws IOException {
+    if (stdOut == null && getStdOutFile().exists()) {
+      stdOut = FileUtils.readFileToString(getStdOutFile(), "UTF-8");
+    }
+    return stdOut;
+  }
+}
diff --git a/src/main/java/caosdb/server/scripting/TimeoutException.java b/src/main/java/caosdb/server/scripting/TimeoutException.java
new file mode 100644
index 0000000000000000000000000000000000000000..655cb6a7782c29a894607db0373a4baa7b98611e
--- /dev/null
+++ b/src/main/java/caosdb/server/scripting/TimeoutException.java
@@ -0,0 +1,32 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2018 Research Group Biomedical Physics,
+ * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+package caosdb.server.scripting;
+
+public class TimeoutException extends Exception {
+
+  public TimeoutException(final String string) {
+    super(string);
+  }
+
+  private static final long serialVersionUID = 4871406372815136756L;
+}
diff --git a/src/main/java/caosdb/server/scripting/TimeoutProcess.java b/src/main/java/caosdb/server/scripting/TimeoutProcess.java
new file mode 100644
index 0000000000000000000000000000000000000000..f41a2663c6aae169b0ce28d4e14ab43135653237
--- /dev/null
+++ b/src/main/java/caosdb/server/scripting/TimeoutProcess.java
@@ -0,0 +1,83 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2018 Research Group Biomedical Physics,
+ * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+package caosdb.server.scripting;
+
+import java.io.InputStream;
+
+public class TimeoutProcess {
+
+  private final Process process;
+  private boolean wasTimeouted = false;
+
+  public TimeoutProcess(final Process process, final int ms) {
+    this.process = process;
+    if (ms > 0) {
+      watchTimeout(this, ms).start();
+    }
+  }
+
+  public static Thread watchTimeout(final TimeoutProcess p, final int ms) {
+    return new Thread(
+        new Runnable() {
+
+          @Override
+          public void run() {
+            try {
+              Thread.sleep(ms);
+            } catch (final InterruptedException e) {
+              e.printStackTrace();
+            }
+            p.timeout();
+          }
+        },
+        "Timeout " + p.toString());
+  }
+
+  protected void timeout() {
+    if (this.process.isAlive()) {
+      this.wasTimeouted = true;
+      this.process.destroyForcibly();
+    }
+  }
+
+  public boolean wasTimeouted() {
+    return this.wasTimeouted;
+  }
+
+  public int waitFor() throws InterruptedException, TimeoutException {
+    int code = this.process.waitFor();
+    if (wasTimeouted) {
+      throw new TimeoutException("The process was terminated due to a timeout.");
+    } else {
+      return code;
+    }
+  }
+
+  public InputStream getErrorStream() {
+    return this.process.getErrorStream();
+  }
+
+  public int exitValue() {
+    return this.process.exitValue();
+  }
+}
diff --git a/src/main/java/caosdb/server/utils/ConfigurationException.java b/src/main/java/caosdb/server/utils/ConfigurationException.java
new file mode 100644
index 0000000000000000000000000000000000000000..74a741873b3d4e53f6d5af1dfcf2bc0bda1b125b
--- /dev/null
+++ b/src/main/java/caosdb/server/utils/ConfigurationException.java
@@ -0,0 +1,32 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2018 Research Group Biomedical Physics,
+ * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+package caosdb.server.utils;
+
+public class ConfigurationException extends RuntimeException {
+
+  private static final long serialVersionUID = -8445574584694720914L;
+
+  public ConfigurationException(String reason) {
+    super(reason);
+  }
+}
diff --git a/src/main/java/caosdb/server/utils/FileUtils.java b/src/main/java/caosdb/server/utils/FileUtils.java
index 878c17e43ae18733301e8ba4d790c92e412fd7d9..ca62cc61eda93d954b466bc124748c95d33def3f 100644
--- a/src/main/java/caosdb/server/utils/FileUtils.java
+++ b/src/main/java/caosdb/server/utils/FileUtils.java
@@ -460,6 +460,9 @@ public class FileUtils {
   }
 
   public static File createSymlink(final File link, final File target) {
+    if (target == null) {
+      throw new NullPointerException("target was null.");
+    }
     if (!target.exists()) {
       throw new TransactionException("target does not exist.");
     }
diff --git a/src/main/java/caosdb/server/utils/Serializer.java b/src/main/java/caosdb/server/utils/Serializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..5508e99388a9e4191fd524ccbfc3e30007f6759a
--- /dev/null
+++ b/src/main/java/caosdb/server/utils/Serializer.java
@@ -0,0 +1,6 @@
+package caosdb.server.utils;
+
+public interface Serializer<T, S> {
+
+  public S serialize(T object);
+}
diff --git a/src/main/java/caosdb/server/utils/ServerMessages.java b/src/main/java/caosdb/server/utils/ServerMessages.java
index f24f28ee0e264ae9102fe7159e655830f3847d0b..5d4a3c3a45eecf329fbc5eb72f9324919cd9d264 100644
--- a/src/main/java/caosdb/server/utils/ServerMessages.java
+++ b/src/main/java/caosdb/server/utils/ServerMessages.java
@@ -1,22 +1,19 @@
 /*
- * ** header v3.0
- * This file is a part of the CaosDB Project.
+ * ** header v3.0 This file is a part of the CaosDB Project.
  *
- * Copyright (C) 2018 Research Group Biomedical Physics,
- * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ * Copyright (C) 2018 Research Group Biomedical Physics, Max-Planck-Institute for Dynamics and
+ * Self-Organization Göttingen
  *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
+ * This program is free software: you can redistribute it and/or modify it under the terms of the
+ * GNU Affero General Public License as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
  *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
+ * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Affero General Public License for more details.
  *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ * You should have received a copy of the GNU Affero General Public License along with this program.
+ * If not, see <https://www.gnu.org/licenses/>.
  *
  * ** end header
  */
@@ -399,4 +396,26 @@ public class ServerMessages {
 
   public static final Message CANNOT_PARSE_UNIT =
       new Message(MessageType.Error, 0, "This unit cannot be parsed.");
+
+  public static final Message SERVER_SIDE_SCRIPT_DOES_NOT_EXIST =
+      new Message(
+          MessageType.Error, 404, "This server-side script does not exist. Did you install it?");
+
+  public static final Message SERVER_SIDE_SCRIPT_NOT_EXECUTABLE =
+      new Message(MessageType.Error, 400, "This server-side script is not executable.");
+
+  public static final Message SERVER_SIDE_SCRIPT_ERROR =
+      new Message(MessageType.Error, 500, "The invocation of this server-side script failed.");
+
+  public static final Message SERVER_SIDE_SCRIPT_SETUP_ERROR =
+      new Message(
+          MessageType.Error,
+          500,
+          "The setup routine for the server-side script failed. This might indicate a misconfiguration of the server. Please contact the administrator.");
+
+  public static final Message SERVER_SIDE_SCRIPT_TIMEOUT =
+      new Message(MessageType.Error, 400, "This server-side script did not finish in time.");
+
+  public static final Message SERVER_SIDE_SCRIPT_MISSING_CALL =
+      new Message(MessageType.Error, 400, "You must specify the `call` field.");
 }
diff --git a/src/main/java/caosdb/server/utils/Utils.java b/src/main/java/caosdb/server/utils/Utils.java
index 6901a4190288f0b1c47269ccb644c60ce2a6fb22..10f3de6c573332ef176f5ca1b8823aea5e04b1b4 100644
--- a/src/main/java/caosdb/server/utils/Utils.java
+++ b/src/main/java/caosdb/server/utils/Utils.java
@@ -165,7 +165,11 @@ public class Utils {
   }
 
   public static String InputStream2String(final InputStream is) {
-    try (Scanner s = new Scanner(is)) {
+    return InputStream2String(is, "UTF-8");
+  }
+
+  public static String InputStream2String(final InputStream is, String charset) {
+    try (Scanner s = new Scanner(is, charset)) {
       s.useDelimiter("\\A");
       return s.hasNext() ? s.next() : "";
     }
diff --git a/src/main/java/caosdb/server/utils/fsm/ActionNotAllowedException.java b/src/main/java/caosdb/server/utils/fsm/ActionNotAllowedException.java
new file mode 100644
index 0000000000000000000000000000000000000000..eedd272c5d18fae84dd17b1f2eec0e24aa0730e9
--- /dev/null
+++ b/src/main/java/caosdb/server/utils/fsm/ActionNotAllowedException.java
@@ -0,0 +1,39 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2018 Research Group Biomedical Physics,
+ * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+package caosdb.server.utils.fsm;
+
+public class ActionNotAllowedException extends RuntimeException {
+
+  private final String action;
+
+  public ActionNotAllowedException(final String action) {
+    this.action = action;
+  }
+
+  private static final long serialVersionUID = -7723481489788838572L;
+
+  @Override
+  public String getMessage() {
+    return "The action `" + this.action + "` is not allowed in the current state.";
+  }
+}
diff --git a/src/main/java/caosdb/server/utils/fsm/FiniteStateMachine.java b/src/main/java/caosdb/server/utils/fsm/FiniteStateMachine.java
new file mode 100644
index 0000000000000000000000000000000000000000..7439d95a1820c91e93290b805de4fa0bdb31c273
--- /dev/null
+++ b/src/main/java/caosdb/server/utils/fsm/FiniteStateMachine.java
@@ -0,0 +1,88 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2018 Research Group Biomedical Physics,
+ * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+package caosdb.server.utils.fsm;
+
+import java.util.List;
+import java.util.Map;
+
+public abstract class FiniteStateMachine<S extends State, T extends Transition> {
+
+  public FiniteStateMachine(final S initial, final Map<S, Map<T, S>> transitions)
+      throws StateNotReachableException {
+    this.currentState = initial;
+    this.transitions = transitions;
+    checkEveryStateReachable();
+  }
+
+  private void checkEveryStateReachable() throws StateNotReachableException {
+    for (final State s : getAllStates()) {
+      if (!s.equals(this.currentState) && !stateIsReachable(s)) {
+        throw new StateNotReachableException(s);
+      }
+    }
+  }
+
+  private boolean stateIsReachable(final State s) {
+    for (final Map<T, S> map : this.transitions.values()) {
+      if (map.containsValue(s)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private final Map<S, Map<T, S>> transitions;
+  private S currentState = null;
+
+  public void trigger(final T t) throws TransitionNotAllowedException {
+    final S old = this.currentState;
+    this.currentState = getNextState(t);
+    onAfterTransition(old, t, this.currentState);
+  }
+
+  S getNextState(final T t) throws TransitionNotAllowedException {
+    final Map<T, S> map = this.transitions.get(this.currentState);
+    if (map != null && map.containsKey(t)) {
+      return map.get(t);
+    }
+    throw new TransitionNotAllowedException(this.getCurrentState(), t);
+  }
+
+  public S getCurrentState() {
+    return this.currentState;
+  }
+
+  public List<? extends State> getAllStates() {
+    return this.currentState.getAllStates();
+  }
+
+  /**
+   * Override this method in subclasses. The method is called immediately after a transition
+   * finished.
+   *
+   * @param from
+   * @param transition
+   * @param to
+   */
+  protected void onAfterTransition(final S from, final T transition, final S to) {}
+}
diff --git a/src/main/java/caosdb/server/utils/fsm/MissingImplementationException.java b/src/main/java/caosdb/server/utils/fsm/MissingImplementationException.java
new file mode 100644
index 0000000000000000000000000000000000000000..761abf2fe7d8e45438ae06717d7de63ac56afb45
--- /dev/null
+++ b/src/main/java/caosdb/server/utils/fsm/MissingImplementationException.java
@@ -0,0 +1,32 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2018 Research Group Biomedical Physics,
+ * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+package caosdb.server.utils.fsm;
+
+public class MissingImplementationException extends Exception {
+
+  public MissingImplementationException(final State s) {
+    super("The state `" + s.toString() + "` has no implementation.");
+  }
+
+  private static final long serialVersionUID = -1844861392151564478L;
+}
diff --git a/src/main/java/caosdb/server/utils/fsm/State.java b/src/main/java/caosdb/server/utils/fsm/State.java
new file mode 100644
index 0000000000000000000000000000000000000000..66073d4d6b2e092d24cf94a18d73216d461f53c9
--- /dev/null
+++ b/src/main/java/caosdb/server/utils/fsm/State.java
@@ -0,0 +1,30 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2018 Research Group Biomedical Physics,
+ * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+package caosdb.server.utils.fsm;
+
+import java.util.List;
+
+public interface State {
+
+  public List<State> getAllStates();
+}
diff --git a/src/main/java/caosdb/server/utils/fsm/StateNotReachableException.java b/src/main/java/caosdb/server/utils/fsm/StateNotReachableException.java
new file mode 100644
index 0000000000000000000000000000000000000000..9dbafcd940d5d07bc89d0aa110a39224a760c340
--- /dev/null
+++ b/src/main/java/caosdb/server/utils/fsm/StateNotReachableException.java
@@ -0,0 +1,32 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2018 Research Group Biomedical Physics,
+ * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+package caosdb.server.utils.fsm;
+
+public class StateNotReachableException extends Exception {
+
+  public StateNotReachableException(final State s) {
+    super("The state `" + s.toString() + "` is not reachable.");
+  }
+
+  private static final long serialVersionUID = -162428821672960032L;
+}
diff --git a/src/main/java/caosdb/server/utils/fsm/StrategyFiniteStateMachine.java b/src/main/java/caosdb/server/utils/fsm/StrategyFiniteStateMachine.java
new file mode 100644
index 0000000000000000000000000000000000000000..ad5d50dc49d9e0feca0586ccfdb620c26b5d483d
--- /dev/null
+++ b/src/main/java/caosdb/server/utils/fsm/StrategyFiniteStateMachine.java
@@ -0,0 +1,60 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2018 Research Group Biomedical Physics,
+ * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+package caosdb.server.utils.fsm;
+
+import java.util.Map;
+
+public class StrategyFiniteStateMachine<S extends State, T extends Transition, I>
+    extends FiniteStateMachine<S, T> {
+
+  public StrategyFiniteStateMachine(
+      final S initial, final Map<S, I> stateImplementations, final Map<S, Map<T, S>> transitions)
+      throws MissingImplementationException, StateNotReachableException {
+    super(initial, transitions);
+    this.stateImplementations = stateImplementations;
+    checkImplementationsComplete();
+  }
+
+  /**
+   * Check if every state has it's implementation.
+   *
+   * @throws MissingImplementationException
+   */
+  private void checkImplementationsComplete() throws MissingImplementationException {
+    for (final State s : getAllStates()) {
+      if (!this.stateImplementations.containsKey(s)) {
+        throw new MissingImplementationException(s);
+      }
+    }
+  }
+
+  private final Map<S, I> stateImplementations;
+
+  public I getImplementation() {
+    return getImplementation(getCurrentState());
+  }
+
+  public I getImplementation(final S state) {
+    return this.stateImplementations.get(state);
+  }
+}
diff --git a/src/main/java/caosdb/server/utils/fsm/Transition.java b/src/main/java/caosdb/server/utils/fsm/Transition.java
new file mode 100644
index 0000000000000000000000000000000000000000..432adc17b9e1c664b95c0e85f4f6918793bf39ae
--- /dev/null
+++ b/src/main/java/caosdb/server/utils/fsm/Transition.java
@@ -0,0 +1,25 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2018 Research Group Biomedical Physics,
+ * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+package caosdb.server.utils.fsm;
+
+public interface Transition {}
diff --git a/src/main/java/caosdb/server/utils/fsm/TransitionNotAllowedException.java b/src/main/java/caosdb/server/utils/fsm/TransitionNotAllowedException.java
new file mode 100644
index 0000000000000000000000000000000000000000..e5b56f7f55438b1b40ee153c65eeafc60e959448
--- /dev/null
+++ b/src/main/java/caosdb/server/utils/fsm/TransitionNotAllowedException.java
@@ -0,0 +1,37 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2018 Research Group Biomedical Physics,
+ * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+package caosdb.server.utils.fsm;
+
+public class TransitionNotAllowedException extends Exception {
+
+  private static final long serialVersionUID = -7688041374190101361L;
+
+  public TransitionNotAllowedException(final State state, final Transition transition) {
+    super(
+        "The transition `"
+            + transition.toString()
+            + "` is not allowed in state `"
+            + state.toString()
+            + ".");
+  }
+}
diff --git a/src/main/java/caosdb/server/utils/mail/ToFileHandler.java b/src/main/java/caosdb/server/utils/mail/ToFileHandler.java
index 56491eadf0c6c36264261b674034a2da841cda29..8a7a012bc6e2f5b9dbf02ca9286c2c2c5ee3c6d5 100644
--- a/src/main/java/caosdb/server/utils/mail/ToFileHandler.java
+++ b/src/main/java/caosdb/server/utils/mail/ToFileHandler.java
@@ -39,7 +39,7 @@ public class ToFileHandler implements MailHandler {
     final String loc =
         CaosDBServer.getServerProperty(ServerProperties.KEY_MAIL_TO_FILE_HANDLER_LOC);
     File f = new File(loc);
-    if (f.isDirectory()) {
+    if ((f.exists() && f.isDirectory()) || !f.exists() && loc.endsWith("/")) {
       f = new File(f.getAbsolutePath() + "/OUTBOX");
     }
     if (!f.exists()) {
diff --git a/src/test/java/caosdb/CaosDBTestClass.java b/src/test/java/caosdb/CaosDBTestClass.java
new file mode 100644
index 0000000000000000000000000000000000000000..6b894c75b097b4aa324a432b6338f84a0527f84c
--- /dev/null
+++ b/src/test/java/caosdb/CaosDBTestClass.java
@@ -0,0 +1,43 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2018 Research Group Biomedical Physics,
+ * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+package caosdb;
+
+import java.io.File;
+import java.io.IOException;
+import org.apache.commons.io.FileUtils;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+public class CaosDBTestClass {
+
+  public static final File TEST_DIR = new File(".TEST_DIR");
+
+  @BeforeClass
+  public static final void setupAll() throws IOException {
+    TEST_DIR.mkdirs();
+    FileUtils.forceDeleteOnExit(TEST_DIR);
+  }
+
+  @AfterClass
+  public static final void teardownAll() {}
+}
diff --git a/src/test/java/caosdb/server/Misc.java b/src/test/java/caosdb/server/Misc.java
index 625213e9723ea536e737096cb73b01c70cbaea1e..7848a88f2dc9218d294103fab14c058327bcf4e9 100644
--- a/src/test/java/caosdb/server/Misc.java
+++ b/src/test/java/caosdb/server/Misc.java
@@ -31,7 +31,6 @@ import static org.junit.Assert.assertTrue;
 import caosdb.server.database.misc.TransactionBenchmark;
 import caosdb.server.jobs.core.CheckFileStorageConsistency;
 import caosdb.server.utils.CronJob;
-import caosdb.server.utils.mail.Mail;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
@@ -120,7 +119,7 @@ public class Misc {
     assertEquals("SOMETEST", matcher.group(2));
     assertNull(matcher.group(1));
     assertFalse(matcher.find());
-    
+
     matcher = parseargs.matcher("-t 12000/ExperimentalData");
     assertTrue(matcher.find());
     assertEquals("-t 12000", matcher.group(0));
@@ -175,19 +174,6 @@ public class Misc {
     assertEquals("m", unitStr);
   }
 
-  @Test
-  public void testMail() {
-    final Mail mail =
-        new Mail(
-            "The Admin",
-            "test@example.com",
-            "The User",
-            "test2@example.com",
-            "Test",
-            "This is a Test");
-    mail.send();
-  }
-
   @Test
   public void testBla() {
     String v = "3140m";
diff --git a/src/test/java/caosdb/server/resource/TestScriptingResource.java b/src/test/java/caosdb/server/resource/TestScriptingResource.java
new file mode 100644
index 0000000000000000000000000000000000000000..3b035fe703409605c59a77df70421e7516230376
--- /dev/null
+++ b/src/test/java/caosdb/server/resource/TestScriptingResource.java
@@ -0,0 +1,94 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2018 Research Group Biomedical Physics,
+ * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+package caosdb.server.resource;
+
+import static org.junit.Assert.assertEquals;
+
+import caosdb.server.entity.Message;
+import java.io.IOException;
+import java.util.Date;
+import java.util.List;
+import org.junit.Test;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.data.Form;
+import org.restlet.data.MediaType;
+import org.restlet.data.Method;
+import org.restlet.data.Status;
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+
+public class TestScriptingResource {
+
+  ScriptingResource resource =
+      new ScriptingResource() {
+        @Override
+        public int callScript(
+            java.util.List<String> invokation,
+            Integer timeout_ms,
+            java.util.List<caosdb.server.entity.FileProperties> files,
+            Object authToken)
+            throws Message {
+          return -1;
+        };
+
+        @Override
+        public Object generateAuthToken() {
+          return "";
+        }
+      };
+
+  @Test
+  public void testUnsupportedMediaType() {
+    Representation entity = new StringRepresentation("asdf");
+    entity.setMediaType(MediaType.TEXT_ALL);
+    Request request = new Request(Method.POST, "../test", entity);
+    request.getAttributes().put("SRID", "asdf1234");
+    request.setDate(new Date());
+    resource.init(null, request, new Response(null));
+    resource.handle();
+    assertEquals(Status.CLIENT_ERROR_UNSUPPORTED_MEDIA_TYPE, resource.getResponse().getStatus());
+  }
+
+  @Test
+  public void testForm2invocation() throws Message {
+    Form form =
+        new Form(
+            "-Ooption=OPTION&call=CA%20LL&-Ooption2=OPTION2&-p=POS1&-p4=POS3&-p2=POS2&IGNORED");
+    List<String> list = resource.form2CommandLine(form);
+    assertEquals("CA LL", list.get(0));
+    assertEquals("--option=OPTION", list.get(1));
+    assertEquals("--option2=OPTION2", list.get(2));
+    assertEquals("POS1", list.get(3));
+    assertEquals("POS2", list.get(4));
+    assertEquals("POS3", list.get(5));
+    assertEquals(6, list.size());
+  }
+
+  @Test
+  public void testHandleForm() throws Message, IOException {
+    Form form =
+        new Form("-Ooption=OPTION&call=CALL&-Ooption2=OPTION2&-p=POS1&-p4=POS3&-p2=POS2&IGNORED");
+    assertEquals(-1, resource.handleForm(form));
+  }
+}
diff --git a/src/test/java/caosdb/server/scripting/TestServerSideScriptingCaller.java b/src/test/java/caosdb/server/scripting/TestServerSideScriptingCaller.java
new file mode 100644
index 0000000000000000000000000000000000000000..b0ba596438a7c463f5e66c6d56dda1ad664493de
--- /dev/null
+++ b/src/test/java/caosdb/server/scripting/TestServerSideScriptingCaller.java
@@ -0,0 +1,560 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2018 Research Group Biomedical Physics,
+ * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+package caosdb.server.scripting;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import caosdb.CaosDBTestClass;
+import caosdb.server.CaosDBException;
+import caosdb.server.database.exceptions.TransactionException;
+import caosdb.server.entity.FileProperties;
+import caosdb.server.entity.Message;
+import caosdb.server.utils.ServerMessages;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import org.apache.commons.io.FileUtils;
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+public class TestServerSideScriptingCaller extends CaosDBTestClass {
+
+  public static File testFolder =
+      TEST_DIR.toPath().resolve("serverSideScriptingCallerTestFolder").toFile();
+  public static File testFile = testFolder.toPath().resolve("TestFile").toFile();
+  public static File testExecutable = testFolder.toPath().resolve("TestScript").toFile();
+
+  @BeforeClass
+  public static void setupTestFolder() throws IOException {
+    FileUtils.forceDeleteOnExit(testFolder);
+    FileUtils.forceDeleteOnExit(testFile);
+    FileUtils.forceDeleteOnExit(testExecutable);
+
+    assertFalse(testFolder.exists());
+    assertFalse(testFile.exists());
+    assertFalse(testExecutable.exists());
+    testFolder.mkdirs();
+    testFile.createNewFile();
+    FileUtils.write(testFile, "asdfhaskdjfhaskjdf", "UTF-8");
+
+    testExecutable.createNewFile();
+    testExecutable.setExecutable(true);
+  }
+
+  @AfterClass
+  public static void deleteTestFolder() throws IOException {
+    FileUtils.forceDelete(testFile);
+    FileUtils.forceDelete(testFolder);
+    assertFalse(testFolder.exists());
+    assertFalse(testFile.exists());
+  }
+
+  public void testExeExit(final int code) throws IOException {
+    FileUtils.write(testExecutable, "exit " + code, "UTF-8");
+  }
+
+  public void testSleep(final int seconds) throws IOException {
+    FileUtils.write(testExecutable, "sleep " + seconds, "UTF-8");
+  }
+
+  public void testPrintArgsToStdErr() throws IOException {
+    FileUtils.write(testExecutable, "(>&2 echo \"$@\")", "UTF-8");
+  }
+
+  public void testPrintArgsToStdOut() throws IOException {
+    FileUtils.write(testExecutable, "echo \"$@\"", "UTF-8");
+  }
+
+  private File pwd;
+
+  @Before
+  public void setupPWD() throws IOException {
+    this.pwd = testFolder.toPath().resolve("testPWD").toFile();
+    FileUtils.forceDeleteOnExit(this.pwd);
+    assertFalse(this.pwd.exists());
+  }
+
+  @After
+  public void deletePWD() throws IOException {
+    if (this.pwd.exists()) {
+      FileUtils.forceDelete(this.pwd);
+    }
+    assertFalse(this.pwd.exists());
+  }
+
+  @Rule public ExpectedException exception = ExpectedException.none();
+
+  @Test
+  public void testCreateWorkingDirFailure() throws Exception {
+    final ServerSideScriptingCaller caller =
+        new ServerSideScriptingCaller(null, -1, null, null, new ScriptingUtils(), this.pwd);
+
+    this.exception.expect(Exception.class);
+    this.exception.expectMessage(
+        "The working directory must be non-existing when the caller is invoked.");
+
+    this.pwd.mkdirs();
+    caller.createWorkingDir();
+  }
+
+  @Test
+  public void testCreateWorkingDirOk() throws Exception {
+    final ServerSideScriptingCaller caller =
+        new ServerSideScriptingCaller(null, -1, null, null, new ScriptingUtils(), this.pwd);
+
+    caller.createWorkingDir();
+
+    // exists
+    assertTrue(this.pwd.exists());
+    assertTrue(this.pwd.isDirectory());
+  }
+
+  /**
+   * Throw {@link FileNotFoundException} because PWD does not exist.
+   *
+   * @throws IOException
+   * @throws CaosDBException
+   */
+  @Test()
+  public void testPutFilesInWorkingDirFailure1() throws IOException, CaosDBException {
+    final ServerSideScriptingCaller caller =
+        new ServerSideScriptingCaller(null, -1, null, null, new ScriptingUtils(), this.pwd);
+
+    this.exception.expect(FileNotFoundException.class);
+    this.exception.expectMessage("FILE_UPLOAD_DIR");
+
+    caller.putFilesInWorkingDir(new ArrayList<>());
+  }
+
+  /**
+   * Throw {@link CaosDBException} because tmpIdentifyer is null or empty.
+   *
+   * @throws FileNotFoundException
+   * @throws CaosDBException
+   */
+  @Test
+  public void testPutFilesInWorkingDirFailure2() throws FileNotFoundException, CaosDBException {
+    final ServerSideScriptingCaller caller =
+        new ServerSideScriptingCaller(null, -1, null, null, new ScriptingUtils(), this.pwd);
+    this.pwd.mkdirs();
+
+    final ArrayList<FileProperties> files = new ArrayList<>();
+    files.add(new FileProperties(null, null, null));
+
+    this.exception.expect(CaosDBException.class);
+    this.exception.expectMessage("The path must not be null or empty!");
+
+    caller.putFilesInWorkingDir(files);
+  }
+
+  /**
+   * Throw {@link NullPointerException} because file is null.
+   *
+   * @throws FileNotFoundException
+   * @throws CaosDBException
+   */
+  @Test
+  public void testPutFilesInWorkingDirFailure3() throws FileNotFoundException, CaosDBException {
+    final ServerSideScriptingCaller caller =
+        new ServerSideScriptingCaller(null, -1, null, null, new ScriptingUtils(), this.pwd);
+    this.pwd.mkdirs();
+
+    final ArrayList<FileProperties> files = new ArrayList<>();
+    final FileProperties f = new FileProperties(null, null, null);
+    f.setTmpIdentifyer("a2s3d4f5");
+    f.setPath("testfile");
+    files.add(f);
+
+    this.exception.expect(NullPointerException.class);
+    this.exception.expectMessage("target was null.");
+
+    caller.putFilesInWorkingDir(files);
+  }
+
+  /**
+   * Throw {@link TransactionException} because file does not exist.
+   *
+   * @throws FileNotFoundException
+   * @throws CaosDBException
+   */
+  @Test
+  public void testPutFilesInWorkingDirFailure4() throws FileNotFoundException, CaosDBException {
+    final ServerSideScriptingCaller caller =
+        new ServerSideScriptingCaller(null, -1, null, null, new ScriptingUtils(), this.pwd);
+    this.pwd.mkdirs();
+
+    final ArrayList<FileProperties> files = new ArrayList<>();
+    final FileProperties f = new FileProperties(null, null, null);
+    f.setTmpIdentifyer("a2s3d4f5");
+    f.setFile(new File("blablabla_non_existing"));
+    f.setPath("bla");
+    files.add(f);
+
+    this.exception.expect(TransactionException.class);
+    this.exception.expectMessage("target does not exist");
+
+    caller.putFilesInWorkingDir(files);
+  }
+
+  /**
+   * putFilesInWorkingDir returns silently because files is null.
+   *
+   * @throws IOException
+   * @throws CaosDBException
+   */
+  @Test()
+  public void testPutFilesInWorkingDirReturnSilently() throws IOException, CaosDBException {
+    final ServerSideScriptingCaller caller =
+        new ServerSideScriptingCaller(null, -1, null, null, new ScriptingUtils(), this.pwd);
+
+    caller.putFilesInWorkingDir(null);
+  }
+
+  @Test
+  public void testPutFilesInWorkingDirOk() throws CaosDBException, IOException {
+    final ServerSideScriptingCaller caller =
+        new ServerSideScriptingCaller(null, -1, null, null, new ScriptingUtils(), this.pwd);
+    this.pwd.mkdirs();
+
+    final ArrayList<FileProperties> files = new ArrayList<>();
+    final FileProperties f = new FileProperties(null, null, null);
+    f.setTmpIdentifyer("a2s3d4f5");
+    f.setFile(testFile);
+    f.setPath("testfile");
+    files.add(f);
+
+    caller.putFilesInWorkingDir(files);
+
+    assertEquals(1, caller.getUploadFilesDir().listFiles().length);
+    final File file = caller.getUploadFilesDir().listFiles()[0];
+    FileUtils.contentEquals(file, testFile);
+  }
+
+  /**
+   * Throw Exception because Script returns with error code 1.
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testCallScriptFailure1() throws Exception {
+    final String[] cmd = {testExecutable.getAbsolutePath()};
+    final ServerSideScriptingCaller caller =
+        new ServerSideScriptingCaller(cmd, -1, null, "", new ScriptingUtils(), this.pwd);
+
+    caller.createWorkingDir();
+    assertTrue(caller.getTmpWorkingDir().exists());
+
+    testExeExit(1);
+
+    assertEquals(1, caller.callScript());
+  }
+
+  @Test
+  public void testCallScriptOkSimple() throws Exception {
+    final String[] cmd = {testExecutable.getAbsolutePath()};
+    final ServerSideScriptingCaller caller =
+        new ServerSideScriptingCaller(cmd, -1, null, "", new ScriptingUtils(), this.pwd);
+
+    caller.createWorkingDir();
+    assertTrue(caller.getTmpWorkingDir().exists());
+
+    testExeExit(0);
+
+    assertEquals(0, caller.callScript());
+  }
+
+  @Test
+  public void testWatchTimeout() throws InterruptedException {
+    final TimeoutProcess p =
+        new TimeoutProcess(
+            new Process() {
+
+              int exit = 0;
+
+              @Override
+              public int waitFor() throws InterruptedException {
+                return this.exit;
+              }
+
+              @Override
+              public OutputStream getOutputStream() {
+                return null;
+              }
+
+              @Override
+              public InputStream getInputStream() {
+                return null;
+              }
+
+              @Override
+              public InputStream getErrorStream() {
+                return null;
+              }
+
+              @Override
+              public int exitValue() {
+                return this.exit;
+              }
+
+              @Override
+              public void destroy() {
+                this.exit = 1;
+              }
+
+              @Override
+              public boolean isAlive() {
+                return this.exit == 0;
+              }
+            },
+            -1);
+
+    assertFalse(p.wasTimeouted());
+    final Thread watchTimeout = TimeoutProcess.watchTimeout(p, 100);
+    watchTimeout.start();
+    watchTimeout.join();
+    assertEquals(1, p.exitValue());
+    assertTrue(p.wasTimeouted());
+  }
+
+  @Test(timeout = 1000)
+  public void testTimeout() throws Exception {
+    final String[] cmd = {testExecutable.getAbsolutePath()};
+    final ServerSideScriptingCaller caller =
+        new ServerSideScriptingCaller(cmd, 500, null, "", new ScriptingUtils(), this.pwd);
+
+    caller.createWorkingDir();
+    assertTrue(caller.getTmpWorkingDir().exists());
+
+    testSleep(10);
+
+    this.exception.expect(TimeoutException.class);
+    caller.callScript();
+  }
+
+  @Test
+  public void testCleanup() throws Exception {
+    final String[] cmd = {testExecutable.getAbsolutePath()};
+    final ServerSideScriptingCaller caller =
+        new ServerSideScriptingCaller(cmd, 500, null, null, new ScriptingUtils(), this.pwd);
+
+    caller.createWorkingDir();
+    assertTrue(caller.getTmpWorkingDir().exists());
+    caller.cleanup();
+    assertFalse(caller.getTmpWorkingDir().exists());
+  }
+
+  @Test
+  public void testInvokeWithErrorInCreateWorkingDir() throws Message {
+    final ServerSideScriptingCaller caller =
+        new ServerSideScriptingCaller(
+            new String[] {""}, -1, null, null, new ScriptingUtils(), this.pwd) {
+          @Override
+          public void createWorkingDir() throws Exception {
+            throw new Exception();
+          }
+
+          @Override
+          public void putFilesInWorkingDir(final Collection<FileProperties> files)
+              throws FileNotFoundException, CaosDBException {}
+
+          @Override
+          public int callScript() {
+            return 0;
+          }
+
+          @Override
+          public void cleanup() {}
+        };
+    this.exception.expect(
+        new BaseMatcher<Exception>() {
+
+          @Override
+          public boolean matches(final Object item) {
+            return item == ServerMessages.SERVER_SIDE_SCRIPT_SETUP_ERROR;
+          }
+
+          @Override
+          public void describeTo(final Description description) {
+            description.appendValue(ServerMessages.SERVER_SIDE_SCRIPT_SETUP_ERROR);
+          }
+        });
+    caller.invoke();
+  }
+
+  @Test
+  public void testInvokeWithErrorInPutFilesInWorkingDir() throws Message {
+    final ServerSideScriptingCaller caller =
+        new ServerSideScriptingCaller(
+            new String[] {""}, -1, null, "", new ScriptingUtils(), this.pwd) {
+          @Override
+          public void createWorkingDir() throws Exception {}
+
+          @Override
+          public void putFilesInWorkingDir(final Collection<FileProperties> files)
+              throws FileNotFoundException, CaosDBException {
+            throw new CaosDBException("");
+          }
+
+          @Override
+          public int callScript() {
+            return 0;
+          }
+
+          @Override
+          public void cleanup() {}
+        };
+    this.exception.expect(
+        new BaseMatcher<Exception>() {
+
+          @Override
+          public boolean matches(final Object item) {
+            return item == ServerMessages.SERVER_SIDE_SCRIPT_SETUP_ERROR;
+          }
+
+          @Override
+          public void describeTo(final Description description) {
+            description.appendValue(ServerMessages.SERVER_SIDE_SCRIPT_SETUP_ERROR);
+          }
+        });
+    caller.invoke();
+  }
+
+  @Test
+  public void testInvokeWithErrorInCallScript() throws Message {
+    final String[] cmd = {testExecutable.getAbsolutePath()};
+    final ServerSideScriptingCaller caller =
+        new ServerSideScriptingCaller(cmd, -1, null, null, new ScriptingUtils(), this.pwd) {
+          @Override
+          public void createWorkingDir() throws Exception {}
+
+          @Override
+          public void putFilesInWorkingDir(final Collection<FileProperties> files)
+              throws FileNotFoundException, CaosDBException {}
+
+          @Override
+          public int callScript() throws IOException {
+            throw new IOException();
+          }
+
+          @Override
+          public void cleanup() {}
+        };
+    this.exception.expect(
+        new BaseMatcher<Exception>() {
+
+          @Override
+          public boolean matches(final Object item) {
+            return item == ServerMessages.SERVER_SIDE_SCRIPT_ERROR;
+          }
+
+          @Override
+          public void describeTo(final Description description) {
+            description.appendValue(ServerMessages.SERVER_SIDE_SCRIPT_ERROR);
+          }
+        });
+    caller.invoke();
+  }
+
+  @Test
+  public void testInvokeWithErrorInCleanup() throws Message {
+    final ServerSideScriptingCaller caller =
+        new ServerSideScriptingCaller(null, -1, null, null, new ScriptingUtils(), this.pwd) {
+          @Override
+          public void createWorkingDir() throws Exception {}
+
+          @Override
+          public void putFilesInWorkingDir(final Collection<FileProperties> files)
+              throws FileNotFoundException, CaosDBException {}
+
+          @Override
+          public int callScript() {
+            return 0;
+          }
+
+          @Override
+          public void cleanup() {
+            super.cleanup();
+          }
+
+          @Override
+          public void deleteWorkingDir(File pwd) throws IOException {
+            throw new IOException();
+          }
+        };
+    this.exception.expect(RuntimeException.class);
+    this.exception.expectMessage("Cleanup failed.");
+    caller.invoke();
+  }
+
+  @Test
+  public void testPrintStdErr() throws Exception {
+    final String[] cmd = {testExecutable.getAbsolutePath(), "opt1", "opt2"};
+    final ServerSideScriptingCaller caller =
+        new ServerSideScriptingCaller(cmd, -1, null, "authToken", new ScriptingUtils(), this.pwd);
+
+    caller.createWorkingDir();
+
+    testPrintArgsToStdErr();
+
+    caller.callScript();
+
+    assertEquals(
+        "--auth-token=authToken opt1 opt2\n", FileUtils.readFileToString(caller.getStdErrFile()));
+  }
+
+  @Test
+  public void testPrintStdOut() throws Exception {
+    final String[] cmd = {testExecutable.getAbsolutePath(), "opt1", "opt2"};
+    final ServerSideScriptingCaller caller =
+        new ServerSideScriptingCaller(cmd, -1, null, "authToken", new ScriptingUtils(), this.pwd);
+
+    caller.createWorkingDir();
+
+    testPrintArgsToStdOut();
+
+    caller.callScript();
+
+    assertEquals(
+        "--auth-token=authToken opt1 opt2\n", FileUtils.readFileToString(caller.getStdOutFile()));
+  }
+
+  @Test
+  public void testCleanupNonExistingWorkingDir() {
+    final ServerSideScriptingCaller caller =
+        new ServerSideScriptingCaller(null, -1, null, "authToken", new ScriptingUtils(), this.pwd);
+    caller.cleanup();
+  }
+}
diff --git a/src/test/java/caosdb/server/utils/fsm/TestFiniteStateMachine.java b/src/test/java/caosdb/server/utils/fsm/TestFiniteStateMachine.java
new file mode 100644
index 0000000000000000000000000000000000000000..3849df7dc66f8f812c36b0a4b49dcb2774d034b3
--- /dev/null
+++ b/src/test/java/caosdb/server/utils/fsm/TestFiniteStateMachine.java
@@ -0,0 +1,90 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2018 Research Group Biomedical Physics,
+ * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+package caosdb.server.utils.fsm;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.common.collect.Lists;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+class SimpleFiniteStateMachine extends FiniteStateMachine<State, Transition> {
+
+  public SimpleFiniteStateMachine(
+      final State initial, final Map<State, Map<Transition, State>> transitions)
+      throws StateNotReachableException {
+    super(initial, transitions);
+  }
+}
+
+enum TestState implements State {
+  State1,
+  State2,
+  State3;
+
+  @Override
+  public List<State> getAllStates() {
+    return Lists.newArrayList(values());
+  }
+}
+
+enum TestTransition implements Transition {
+  toState2,
+  toState3
+}
+
+public class TestFiniteStateMachine {
+
+  @Rule public ExpectedException exc = ExpectedException.none();
+
+  @Test
+  public void testTransitionNotAllowedException()
+      throws StateNotReachableException, TransitionNotAllowedException {
+    final Map<State, Map<Transition, State>> map = new HashMap<>();
+    final HashMap<Transition, State> from1 = new HashMap<>();
+    from1.put(TestTransition.toState2, TestState.State2);
+    from1.put(TestTransition.toState3, TestState.State3);
+    map.put(TestState.State1, from1);
+
+    final SimpleFiniteStateMachine fsm = new SimpleFiniteStateMachine(TestState.State1, map);
+    assertEquals(TestState.State1, fsm.getCurrentState());
+    fsm.trigger(TestTransition.toState2);
+    assertEquals(TestState.State2, fsm.getCurrentState());
+
+    // only 1->2 and from 1->3 is allowed. not 2->3
+    this.exc.expect(TransitionNotAllowedException.class);
+    fsm.trigger(TestTransition.toState3);
+  }
+
+  @Test
+  public void testStateNotReachable() throws StateNotReachableException {
+    final Map<State, Map<Transition, State>> empty = new HashMap<>();
+
+    this.exc.expect(StateNotReachableException.class);
+    new SimpleFiniteStateMachine(TestState.State1, empty);
+  }
+}
diff --git a/src/test/java/caosdb/server/utils/fsm/TestStrategyFiniteStateMachine.java b/src/test/java/caosdb/server/utils/fsm/TestStrategyFiniteStateMachine.java
new file mode 100644
index 0000000000000000000000000000000000000000..8d383383dd4a896f0befa1b3c2b1854129377353
--- /dev/null
+++ b/src/test/java/caosdb/server/utils/fsm/TestStrategyFiniteStateMachine.java
@@ -0,0 +1,50 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2018 Research Group Biomedical Physics,
+ * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+package caosdb.server.utils.fsm;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+public class TestStrategyFiniteStateMachine {
+
+  @Rule public ExpectedException exc = ExpectedException.none();
+
+  @Test
+  public void testStateHasNoImplementation()
+      throws MissingImplementationException, StateNotReachableException {
+    final Map<State, Map<Transition, State>> map = new HashMap<>();
+    final HashMap<Transition, State> from1 = new HashMap<>();
+    from1.put(TestTransition.toState2, TestState.State2);
+    from1.put(TestTransition.toState3, TestState.State3);
+    map.put(TestState.State1, from1);
+
+    final Map<State, Object> stateImplementations = new HashMap<>();
+
+    this.exc.expect(MissingImplementationException.class);
+    new StrategyFiniteStateMachine<State, Transition, Object>(
+        TestState.State1, stateImplementations, map);
+  }
+}
diff --git a/src/test/java/caosdb/server/utils/mail/TestMail.java b/src/test/java/caosdb/server/utils/mail/TestMail.java
new file mode 100644
index 0000000000000000000000000000000000000000..fc8bc887e71b6d02c31e47fb7125a17f4ea662c3
--- /dev/null
+++ b/src/test/java/caosdb/server/utils/mail/TestMail.java
@@ -0,0 +1,52 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2018 Research Group Biomedical Physics,
+ * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+package caosdb.server.utils.mail;
+
+import caosdb.CaosDBTestClass;
+import caosdb.server.CaosDBServer;
+import caosdb.server.ServerProperties;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class TestMail extends CaosDBTestClass {
+
+  @BeforeClass
+  public static void setupUp() {
+    // output mails to the test dir
+    CaosDBServer.setProperty(
+        ServerProperties.KEY_MAIL_TO_FILE_HANDLER_LOC, TEST_DIR.getAbsolutePath());
+  }
+
+  @Test
+  public void testMail() {
+    final Mail mail =
+        new Mail(
+            "The Admin",
+            "test@example.com",
+            "The User",
+            "test2@example.com",
+            "Test",
+            "This is a Test");
+    mail.send();
+  }
+}