diff --git a/actions/mldonkey b/actions/mldonkey
new file mode 100755
index 000000000..c4ea9f885
--- /dev/null
+++ b/actions/mldonkey
@@ -0,0 +1,70 @@
+#!/usr/bin/python3
+# -*- mode: python -*-
+#
+# This file is part of FreedomBox.
+#
+# 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 .
+#
+"""
+Configuration helper for mldonkey.
+"""
+
+import argparse
+import subprocess
+
+from plinth import action_utils
+
+def parse_arguments():
+ """Return parsed command line arguments as dictionary."""
+ parser = argparse.ArgumentParser()
+ subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
+
+ subparsers.add_parser('pre-install', help='Perform pre-install operations')
+ subparsers.add_parser('enable', help='Enable mldonkey')
+ subparsers.add_parser('disable', help='Disable mldonkey')
+
+ subparsers.required = True
+ return parser.parse_args()
+
+
+def subcommand_pre_install(_):
+ """Preseed debconf values before packages are installed."""
+ subprocess.check_output(
+ ['debconf-set-selections'],
+ input=b'mldonkey-server mldonkey-server/launch_at_startup boolean true')
+
+
+def subcommand_enable(_):
+ """Enable web configuration and reload."""
+ action_utils.service_enable('mldonkey-server')
+ action_utils.webserver_enable('mldonkey-freedombox')
+
+
+def subcommand_disable(_):
+ """Disable web configuration and reload."""
+ action_utils.webserver_disable('mldonkey-freedombox')
+ action_utils.service_disable('mldonkey-server')
+
+
+def main():
+ """Parse arguments and perform all duties."""
+ arguments = parse_arguments()
+
+ subcommand = arguments.subcommand.replace('-', '_')
+ subcommand_method = globals()['subcommand_' + subcommand]
+ subcommand_method(arguments)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/data/etc/apache2/conf-available/mldonkey-freedombox.conf b/data/etc/apache2/conf-available/mldonkey-freedombox.conf
new file mode 100644
index 000000000..29873dc40
--- /dev/null
+++ b/data/etc/apache2/conf-available/mldonkey-freedombox.conf
@@ -0,0 +1,23 @@
+##
+## On all sites, provide mldonkey web interface on a path: /mldonkey
+##
+
+# Redirect /mldonkey to /mldonkey/ as the MLdonkey server web interface does not
+# work without a slash at the end.
+
+
+
+ RewriteEngine On
+ RewriteCond %{REQUEST_URI} ^/mldonkey$
+ RewriteRule .* /mldonkey/ [R=301,L]
+
+
+
+
+
+ Include includes/freedombox-single-sign-on.conf
+ ProxyPass http://localhost:4080/
+
+ TKTAuthToken "admin" "ed2k"
+
+
diff --git a/data/etc/plinth/modules-enabled/mldonkey b/data/etc/plinth/modules-enabled/mldonkey
new file mode 100644
index 000000000..71bd026a0
--- /dev/null
+++ b/data/etc/plinth/modules-enabled/mldonkey
@@ -0,0 +1 @@
+plinth.modules.mldonkey
diff --git a/functional_tests/features/mldonkey.feature b/functional_tests/features/mldonkey.feature
new file mode 100644
index 000000000..09d6831b2
--- /dev/null
+++ b/functional_tests/features/mldonkey.feature
@@ -0,0 +1,50 @@
+#
+# This file is part of FreedomBox.
+#
+# 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 .
+#
+
+@apps @mldonkey @sso
+Feature: MLDonkey eDonkey Network Client
+ Run the eDonkey Network client.
+
+Background:
+ Given I'm a logged in user
+ Given the mldonkey application is installed
+
+Scenario: Enable mldonkey application
+ Given the mldonkey application is disabled
+ When I enable the mldonkey application
+ Then the mldonkey site should be available
+
+Scenario: Disable mldonkey application
+ Given the mldonkey application is enabled
+ When I disable the mldonkey application
+ Then the mldonkey site should not be available
+
+# Scenario: Upload an ed2k file to mldonkey
+# Given the mldonkey application is enabled
+# When all ed2k files are removed from mldonkey
+# And I upload a sample ed2k file to mldonkey
+# Then there should be 1 ed2k file listed in mldonkey
+#
+# Scenario: Backup and restore mldonkey
+# Given the mldonkey application is enabled
+# When all ed2k files are removed from mldonkey
+# And I upload a sample ed2k file to mldonkey
+# And I create a backup of the mldonkey app data
+# And all ed2k files are removed from mldonkey
+# And I restore the mldonkey app data backup
+# Then the mldonkey service should be running
+# And there should be 1 torrents listed in mldonkey
diff --git a/plinth/modules/mldonkey/__init__.py b/plinth/modules/mldonkey/__init__.py
new file mode 100644
index 000000000..b9052f6b6
--- /dev/null
+++ b/plinth/modules/mldonkey/__init__.py
@@ -0,0 +1,131 @@
+#
+# This file is part of FreedomBox.
+#
+# 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 .
+#
+"""
+FreedomBox app for mldonkey.
+"""
+
+from django.utils.translation import ugettext_lazy as _
+
+from plinth import service as service_module
+from plinth import action_utils, actions, cfg, frontpage
+from plinth.menu import main_menu
+from plinth.modules.users import register_group
+
+from .manifest import clients
+
+version = 1
+
+service = None
+
+managed_services = ['mldonkey-server']
+
+managed_packages = ['mldonkey-server']
+
+name = _('MLDonkey')
+
+short_description = _('Door to the eDonkey network')
+
+description = [
+ _('MLDonkey is a door to the eDonkey network, a decentralized network '
+ 'used to exchange big files on the Internet.'),
+]
+
+clients = clients
+
+reserved_usernames = ['mldonkey']
+
+group = ('ed2k', _('Download files using eDonkey applications'))
+
+manual_page = 'MLDonkey'
+
+def init():
+ """Intialize the MLDonkey module."""
+ menu = main_menu.get('apps')
+ menu.add_urlname(name, 'mldonkey', 'mldonkey:index',
+ short_description)
+ register_group(group)
+
+ global service
+ setup_helper = globals()['setup_helper']
+ if setup_helper.get_state() != 'needs-setup':
+ service = service_module.Service(managed_services[0], name, ports=[
+ 'http', 'https'
+ ], is_external=True, is_enabled=is_enabled, enable=enable,
+ disable=disable,
+ is_running=is_running)
+
+ if is_enabled():
+ add_shortcut()
+
+
+def setup(helper, old_version=None):
+ """Install and configure the module."""
+ helper.call('pre', actions.superuser_run, 'mldonkey', ['pre-install'])
+ helper.install(managed_packages)
+ helper.call('post', actions.superuser_run, 'mldonkey', ['enable'])
+ global service
+ if service is None:
+ service = service_module.Service(managed_services[0], name, ports=[
+ 'http', 'https'
+ ], is_external=True, is_enabled=is_enabled, enable=enable,
+ disable=disable,
+ is_running=is_running)
+ helper.call('post', service.notify_enabled, None, True)
+ helper.call('post', add_shortcut)
+
+
+def add_shortcut():
+ """Helper method to add a shortcut to the frontpage."""
+ frontpage.add_shortcut('mldonkey', name, short_description=short_description,
+ url='/mldonkey', login_required=True,
+ allowed_groups=[group[0]])
+
+
+def is_running():
+ """Return whether the service is running."""
+ return action_utils.service_is_running('mldonkey-server')
+
+
+def is_enabled():
+ """Return whether the module is enabled."""
+ return (action_utils.service_is_enabled('mldonkey-server') and
+ action_utils.webserver_is_enabled('mldonkey-freedombox'))
+
+
+def enable():
+ """Enable the module."""
+ actions.superuser_run('mldonkey', ['enable'])
+ add_shortcut()
+
+
+def disable():
+ """Disable the module."""
+ actions.superuser_run('mldonkey', ['disable'])
+ frontpage.remove_shortcut('mldonkey')
+
+
+def diagnose():
+ """Run diagnostics and return the results."""
+ results = []
+
+ results.append(action_utils.diagnose_port_listening(4080, 'tcp4'))
+ results.append(action_utils.diagnose_port_listening(4080, 'tcp6'))
+ results.extend(
+ action_utils.diagnose_url_on_all('https://{host}/mldonkey/',
+ check_certificate=False))
+
+ return results
diff --git a/plinth/modules/mldonkey/manifest.py b/plinth/modules/mldonkey/manifest.py
new file mode 100644
index 000000000..12a4d67fb
--- /dev/null
+++ b/plinth/modules/mldonkey/manifest.py
@@ -0,0 +1,36 @@
+#
+# This file is part of FreedomBox.
+#
+# 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 .
+#
+
+from django.utils.translation import ugettext_lazy as _
+
+from plinth.clients import validate
+from plinth.modules.backups.api import validate as validate_backup
+
+clients = validate([{
+ 'name': _('MLDonkey'),
+ 'platforms': [{
+ 'type': 'web',
+ 'url': '/mldonkey/'
+ }]
+}])
+
+backup = validate_backup({
+ 'config': {
+ 'files': ['/var/lib/mldonkey/*.ini']
+ },
+ 'services': ['mldonkey-server']
+})
diff --git a/plinth/modules/mldonkey/urls.py b/plinth/modules/mldonkey/urls.py
new file mode 100644
index 000000000..5e610c58f
--- /dev/null
+++ b/plinth/modules/mldonkey/urls.py
@@ -0,0 +1,35 @@
+#
+# This file is part of FreedomBox.
+#
+# 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 .
+#
+"""
+URLs for the mldonkey module.
+"""
+
+from django.conf.urls import url
+
+from plinth.views import ServiceView
+from plinth.modules import mldonkey
+
+urlpatterns = [
+ url(r'^apps/mldonkey/$',
+ ServiceView.as_view(
+ service_id=mldonkey.managed_services[0],
+ diagnostics_module_name='mldonkey',
+ description=mldonkey.description,
+ clients=mldonkey.clients,
+ show_status_block=True),
+ name='index'),
+]
diff --git a/static/themes/default/icons/mldonkey.png b/static/themes/default/icons/mldonkey.png
new file mode 100644
index 000000000..32bf32ce0
Binary files /dev/null and b/static/themes/default/icons/mldonkey.png differ