diff --git a/src/app/Console/Commands/User/CreateCommand.php b/src/app/Console/Commands/User/CreateCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/User/CreateCommand.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace App\Console\Commands\User;
+
+use Illuminate\Support\Facades\DB;
+use App\Http\Controllers\API\V4\UsersController;
+
+class CreateCommand extends \App\Console\Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'user:create {email} {--package=*} {--password=} {--role=}';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = "Create a user.";
+
+    /**
+     * Execute the console command.
+     *
+     * @return mixed
+     */
+    public function handle()
+    {
+        $email = $this->argument('email');
+        $packages = $this->option('package');
+        $password = $this->option('password');
+        $role = $this->option('role');
+
+        list($local, $domainName) = explode('@', $email, 2);
+
+        $domain = $this->getDomain($domainName);
+
+        if (!$domain) {
+            $this->error("No such domain {$domainName}.");
+            return 1;
+        }
+
+        if ($domain->isPublic()) {
+            $this->error("Domain {$domainName} is public.");
+            return 1;
+        }
+
+        $owner = $domain->wallet()->owner;
+        $existingUser = null;
+
+        // Validate email address
+        if ($error = UsersController::validateEmail($email, $owner, $existingUser)) {
+            $this->error("{$email}: {$error}");
+            return 1;
+        }
+
+        if ($existingUser) {
+            $this->info("The user with the email: {$email} exists already");
+            return 0;
+        }
+
+        DB::beginTransaction();
+
+        if ($existingUser) {
+            $this->info("Force deleting existing but deleted user {$email}");
+            $existingUser->forceDelete();
+        }
+
+        if (!$password) {
+            $password = \App\Utils::generatePassphrase();
+        }
+
+        $user = \App\User::create(
+            [
+                'email' => $email,
+                'password' => $password
+            ]
+        );
+        $user->role = $role;
+        $user->save();
+
+        foreach ($packages as $package) {
+            $userPackage = $this->getObject(\App\Package::class, $package, 'title', false);
+            if (!$userPackage) {
+                $this->error("Invalid package: {$package}");
+                return 1;
+            }
+            $user->assignPackage($userPackage);
+        }
+
+        DB::commit();
+
+        $this->info($user->id);
+    }
+}
diff --git a/src/tests/Feature/Console/User/CreateTest.php b/src/tests/Feature/Console/User/CreateTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Console/User/CreateTest.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Tests\Feature\Console\User;
+
+use App\User;
+use Illuminate\Support\Facades\Queue;
+use Tests\TestCase;
+
+class CreateTest extends TestCase
+{
+    /**
+     * {@inheritDoc}
+     */
+    public function setUp(): void
+    {
+        parent::setUp();
+
+        $this->deleteTestUser('user@kolab.org');
+        $this->deleteTestUser('admin@kolab.org');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function tearDown(): void
+    {
+        $this->deleteTestUser('user@kolab.org');
+        $this->deleteTestUser('admin@kolab.org');
+
+        parent::tearDown();
+    }
+
+    /**
+     * Test the command
+     */
+    public function testHandle(): void
+    {
+        Queue::fake();
+
+        // Warning: We're not using artisan() here, as this will not
+        // allow us to test "empty output" cases
+
+        // Existing email
+        $code = \Artisan::call("user:create jack@kolab.org");
+        $output = trim(\Artisan::output());
+        $this->assertSame(1, $code);
+        $this->assertSame("jack@kolab.org: The specified email is not available.", $output);
+
+        // Existing email (of a user alias)
+        $code = \Artisan::call("user:create jack.daniels@kolab.org");
+        $output = trim(\Artisan::output());
+        $this->assertSame(1, $code);
+        $this->assertSame("jack.daniels@kolab.org: The specified email is not available.", $output);
+
+        // Public domain not allowed in the group email address
+        $code = \Artisan::call("user:create user@kolabnow.com");
+        $output = trim(\Artisan::output());
+        $this->assertSame(1, $code);
+        $this->assertSame("Domain kolabnow.com is public.", $output);
+
+        // Valid
+        $code = \Artisan::call("user:create user@kolab.org");
+        $output = trim(\Artisan::output());
+        $user = User::where('email', 'user@kolab.org')->first();
+        $this->assertSame(0, $code);
+        $this->assertEquals($user->id, $output);
+
+        // Valid
+        $code = \Artisan::call("user:create admin@kolab.org --package=kolab --role=admin --password=simple123");
+        $output = trim(\Artisan::output());
+        $user = User::where('email', 'admin@kolab.org')->first();
+        $this->assertSame(0, $code);
+        $this->assertEquals($user->id, $output);
+        $this->assertEquals($user->role, "admin");
+    }
+}