diff --git a/app/Auth/Access/EmailConfirmationService.php b/app/Auth/Access/EmailConfirmationService.php
index 4df014116..a9aecaf22 100644
--- a/app/Auth/Access/EmailConfirmationService.php
+++ b/app/Auth/Access/EmailConfirmationService.php
@@ -1,33 +1,18 @@
 <?php namespace BookStack\Auth\Access;
 
 use BookStack\Auth\User;
-use BookStack\Auth\UserRepo;
 use BookStack\Exceptions\ConfirmationEmailException;
-use BookStack\Exceptions\UserRegistrationException;
 use BookStack\Notifications\ConfirmEmail;
-use Carbon\Carbon;
-use Illuminate\Database\Connection as Database;
 
-class EmailConfirmationService
+class EmailConfirmationService extends UserTokenService
 {
-    protected $db;
-    protected $users;
-
-    /**
-     * EmailConfirmationService constructor.
-     * @param Database $db
-     * @param \BookStack\Auth\UserRepo $users
-     */
-    public function __construct(Database $db, UserRepo $users)
-    {
-        $this->db = $db;
-        $this->users = $users;
-    }
+    protected $tokenTable = 'email_confirmations';
+    protected $expiryTime = 24;
 
     /**
      * Create new confirmation for a user,
      * Also removes any existing old ones.
-     * @param \BookStack\Auth\User $user
+     * @param User $user
      * @throws ConfirmationEmailException
      */
     public function sendConfirmation(User $user)
@@ -36,76 +21,10 @@ class EmailConfirmationService
             throw new ConfirmationEmailException(trans('errors.email_already_confirmed'), '/login');
         }
 
-        $this->deleteConfirmationsByUser($user);
-        $token = $this->createEmailConfirmation($user);
+        $this->deleteByUser($user);
+        $token = $this->createTokenForUser($user);
 
         $user->notify(new ConfirmEmail($token));
     }
 
-    /**
-     * Creates a new email confirmation in the database and returns the token.
-     * @param User $user
-     * @return string
-     */
-    public function createEmailConfirmation(User $user)
-    {
-        $token = $this->getToken();
-        $this->db->table('email_confirmations')->insert([
-            'user_id' => $user->id,
-            'token' => $token,
-            'created_at' => Carbon::now(),
-            'updated_at' => Carbon::now()
-        ]);
-        return $token;
-    }
-
-    /**
-     * Gets an email confirmation by looking up the token,
-     * Ensures the token has not expired.
-     * @param string $token
-     * @return array|null|\stdClass
-     * @throws UserRegistrationException
-     */
-    public function getEmailConfirmationFromToken($token)
-    {
-        $emailConfirmation = $this->db->table('email_confirmations')->where('token', '=', $token)->first();
-
-        // If not found show error
-        if ($emailConfirmation === null) {
-            throw new UserRegistrationException(trans('errors.email_confirmation_invalid'), '/register');
-        }
-
-        // If more than a day old
-        if (Carbon::now()->subDay()->gt(new Carbon($emailConfirmation->created_at))) {
-            $user = $this->users->getById($emailConfirmation->user_id);
-            $this->sendConfirmation($user);
-            throw new UserRegistrationException(trans('errors.email_confirmation_expired'), '/register/confirm');
-        }
-
-        $emailConfirmation->user = $this->users->getById($emailConfirmation->user_id);
-        return $emailConfirmation;
-    }
-
-    /**
-     * Delete all email confirmations that belong to a user.
-     * @param \BookStack\Auth\User $user
-     * @return mixed
-     */
-    public function deleteConfirmationsByUser(User $user)
-    {
-        return $this->db->table('email_confirmations')->where('user_id', '=', $user->id)->delete();
-    }
-
-    /**
-     * Creates a unique token within the email confirmation database.
-     * @return string
-     */
-    protected function getToken()
-    {
-        $token = str_random(24);
-        while ($this->db->table('email_confirmations')->where('token', '=', $token)->exists()) {
-            $token = str_random(25);
-        }
-        return $token;
-    }
 }
diff --git a/app/Auth/Access/UserInviteService.php b/app/Auth/Access/UserInviteService.php
new file mode 100644
index 000000000..8e04d7b22
--- /dev/null
+++ b/app/Auth/Access/UserInviteService.php
@@ -0,0 +1,23 @@
+<?php namespace BookStack\Auth\Access;
+
+use BookStack\Auth\User;
+use BookStack\Notifications\UserInvite;
+
+class UserInviteService extends UserTokenService
+{
+    protected $tokenTable = 'user_invites';
+    protected $expiryTime = 336; // Two weeks
+
+    /**
+     * Send an invitation to a user to sign into BookStack
+     * Removes existing invitation tokens.
+     * @param User $user
+     */
+    public function sendInvitation(User $user)
+    {
+        $this->deleteByUser($user);
+        $token = $this->createTokenForUser($user);
+        $user->notify(new UserInvite($token));
+    }
+
+}
diff --git a/app/Auth/Access/UserTokenService.php b/app/Auth/Access/UserTokenService.php
new file mode 100644
index 000000000..40f363ee1
--- /dev/null
+++ b/app/Auth/Access/UserTokenService.php
@@ -0,0 +1,134 @@
+<?php namespace BookStack\Auth\Access;
+
+use BookStack\Auth\User;
+use BookStack\Exceptions\UserTokenExpiredException;
+use BookStack\Exceptions\UserTokenNotFoundException;
+use Carbon\Carbon;
+use Illuminate\Database\Connection as Database;
+use stdClass;
+
+class UserTokenService
+{
+
+    /**
+     * Name of table where user tokens are stored.
+     * @var string
+     */
+    protected $tokenTable = 'user_tokens';
+
+    /**
+     * Token expiry time in hours.
+     * @var int
+     */
+    protected $expiryTime = 24;
+
+    protected $db;
+
+    /**
+     * UserTokenService constructor.
+     * @param Database $db
+     */
+    public function __construct(Database $db)
+    {
+        $this->db = $db;
+    }
+
+    /**
+     * Delete all email confirmations that belong to a user.
+     * @param User $user
+     * @return mixed
+     */
+    public function deleteByUser(User $user)
+    {
+        return $this->db->table($this->tokenTable)
+            ->where('user_id', '=', $user->id)
+            ->delete();
+    }
+
+    /**
+     * Get the user id from a token, while check the token exists and has not expired.
+     * @param string $token
+     * @return int
+     * @throws UserTokenNotFoundException
+     * @throws UserTokenExpiredException
+     */
+    public function checkTokenAndGetUserId(string $token) : int
+    {
+        $entry = $this->getEntryByToken($token);
+
+        if (is_null($entry)) {
+            throw new UserTokenNotFoundException('Token "' . $token . '" not found');
+        }
+
+        if ($this->entryExpired($entry)) {
+            throw new UserTokenExpiredException("Token of id {$token->id} has expired.", $entry->user_id);
+        }
+
+        return $entry->user_id;
+    }
+
+    /**
+     * Creates a unique token within the email confirmation database.
+     * @return string
+     */
+    protected function generateToken() : string
+    {
+        $token = str_random(24);
+        while ($this->tokenExists($token)) {
+            $token = str_random(25);
+        }
+        return $token;
+    }
+
+    /**
+     * Generate and store a token for the given user.
+     * @param User $user
+     * @return string
+     */
+    protected function createTokenForUser(User $user) : string
+    {
+        $token = $this->generateToken();
+        $this->db->table($this->tokenTable)->insert([
+            'user_id' => $user->id,
+            'token' => $token,
+            'created_at' => Carbon::now(),
+            'updated_at' => Carbon::now()
+        ]);
+        return $token;
+    }
+
+    /**
+     * Check if the given token exists.
+     * @param string $token
+     * @return bool
+     */
+    protected function tokenExists(string $token) : bool
+    {
+        return $this->db->table($this->tokenTable)
+            ->where('token', '=', $token)->exists();
+    }
+
+    /**
+     * Get a token entry for the given token.
+     * @param string $token
+     * @return object|null
+     */
+    protected function getEntryByToken(string $token)
+    {
+        return $this->db->table($this->tokenTable)
+            ->where('token', '=', $token)
+            ->first();
+    }
+
+    /**
+     * Check if the given token entry has expired.
+     * @param stdClass $tokenEntry
+     * @return bool
+     */
+    protected function entryExpired(stdClass $tokenEntry) : bool
+    {
+        return Carbon::now()->subHours($this->expiryTime)
+            ->gt(new Carbon($tokenEntry->created_at));
+    }
+
+}
\ No newline at end of file
diff --git a/app/Auth/User.php b/app/Auth/User.php
index e5a8a3931..7ad14d9f0 100644
--- a/app/Auth/User.php
+++ b/app/Auth/User.php
@@ -3,6 +3,7 @@
 use BookStack\Model;
 use BookStack\Notifications\ResetPassword;
 use BookStack\Uploads\Image;
+use Carbon\Carbon;
 use Illuminate\Auth\Authenticatable;
 use Illuminate\Auth\Passwords\CanResetPassword;
 use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
@@ -10,6 +11,20 @@ use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
 use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 use Illuminate\Notifications\Notifiable;
 
+/**
+ * Class User
+ * @package BookStack\Auth
+ * @property string $id
+ * @property string $name
+ * @property string $email
+ * @property string $password
+ * @property Carbon $created_at
+ * @property Carbon $updated_at
+ * @property bool $email_confirmed
+ * @property int $image_id
+ * @property string $external_auth_id
+ * @property string $system_name
+ */
 class User extends Model implements AuthenticatableContract, CanResetPasswordContract
 {
     use Authenticatable, CanResetPassword, Notifiable;
diff --git a/app/Exceptions/UserTokenExpiredException.php b/app/Exceptions/UserTokenExpiredException.php
new file mode 100644
index 000000000..203e08c85
--- /dev/null
+++ b/app/Exceptions/UserTokenExpiredException.php
@@ -0,0 +1,19 @@
+<?php namespace BookStack\Exceptions;
+
+class UserTokenExpiredException extends \Exception {
+
+    public $userId;
+
+    /**
+     * UserTokenExpiredException constructor.
+     * @param string $message
+     * @param int $userId
+     */
+    public function __construct(string $message, int $userId)
+    {
+        $this->userId = $userId;
+        parent::__construct($message);
+    }
+
+
+}
\ No newline at end of file
diff --git a/app/Exceptions/UserTokenNotFoundException.php b/app/Exceptions/UserTokenNotFoundException.php
new file mode 100644
index 000000000..08c1fd830
--- /dev/null
+++ b/app/Exceptions/UserTokenNotFoundException.php
@@ -0,0 +1,3 @@
+<?php namespace BookStack\Exceptions;
+
+class UserTokenNotFoundException extends \Exception {}
\ No newline at end of file
diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php
index a285899cc..47541784b 100644
--- a/app/Http/Controllers/Auth/RegisterController.php
+++ b/app/Http/Controllers/Auth/RegisterController.php
@@ -7,10 +7,13 @@ use BookStack\Auth\Access\SocialAuthService;
 use BookStack\Auth\SocialAccount;
 use BookStack\Auth\User;
 use BookStack\Auth\UserRepo;
+use BookStack\Exceptions\ConfirmationEmailException;
 use BookStack\Exceptions\SocialDriverNotConfigured;
 use BookStack\Exceptions\SocialSignInAccountNotUsed;
 use BookStack\Exceptions\SocialSignInException;
 use BookStack\Exceptions\UserRegistrationException;
+use BookStack\Exceptions\UserTokenExpiredException;
+use BookStack\Exceptions\UserTokenNotFoundException;
 use BookStack\Http\Controllers\Controller;
 use Exception;
 use Illuminate\Foundation\Auth\RegistersUsers;
@@ -189,17 +192,38 @@ class RegisterController extends Controller
      * Confirms an email via a token and logs the user into the system.
      * @param $token
      * @return RedirectResponse|Redirector
-     * @throws UserRegistrationException
+     * @throws ConfirmationEmailException
+     * @throws Exception
      */
     public function confirmEmail($token)
     {
-        $confirmation = $this->emailConfirmationService->getEmailConfirmationFromToken($token);
-        $user = $confirmation->user;
+        try {
+            $userId = $this->emailConfirmationService->checkTokenAndGetUserId($token);
+        } catch (Exception $exception) {
+
+            if ($exception instanceof UserTokenNotFoundException) {
+                session()->flash('error', trans('errors.email_confirmation_invalid'));
+                return redirect('/register');
+            }
+
+            if ($exception instanceof UserTokenExpiredException) {
+                $user = $this->userRepo->getById($exception->userId);
+                $this->emailConfirmationService->sendConfirmation($user);
+                session()->flash('error', trans('errors.email_confirmation_expired'));
+                return redirect('/register/confirm');
+            }
+
+            throw $exception;
+        }
+
+        $user = $this->userRepo->getById($userId);
         $user->email_confirmed = true;
         $user->save();
+
         auth()->login($user);
         session()->flash('success', trans('auth.email_confirm_success'));
-        $this->emailConfirmationService->deleteConfirmationsByUser($user);
+        $this->emailConfirmationService->deleteByUser($user);
+
         return redirect($this->redirectPath);
     }
 
diff --git a/app/Notifications/UserInvite.php b/app/Notifications/UserInvite.php
new file mode 100644
index 000000000..b01911bcd
--- /dev/null
+++ b/app/Notifications/UserInvite.php
@@ -0,0 +1,31 @@
+<?php namespace BookStack\Notifications;
+
+class UserInvite extends MailNotification
+{
+    public $token;
+
+    /**
+     * Create a new notification instance.
+     * @param string $token
+     */
+    public function __construct($token)
+    {
+        $this->token = $token;
+    }
+
+    /**
+     * Get the mail representation of the notification.
+     *
+     * @param  mixed  $notifiable
+     * @return \Illuminate\Notifications\Messages\MailMessage
+     */
+    public function toMail($notifiable)
+    {
+        $appName = ['appName' => setting('app-name')];
+        return $this->newMailMessage()
+                ->subject(trans('auth.user_invite_email_subject', $appName))
+                ->greeting(trans('auth.user_invite_email_greeting', $appName))
+                ->line(trans('auth.user_invite_email_text'))
+                ->action(trans('auth.user_invite_email_action'), url('/register/invite/' . $this->token));
+    }
+}
diff --git a/database/migrations/2019_08_17_140214_add_user_invites_table.php b/database/migrations/2019_08_17_140214_add_user_invites_table.php
new file mode 100644
index 000000000..23bd6988c
--- /dev/null
+++ b/database/migrations/2019_08_17_140214_add_user_invites_table.php
@@ -0,0 +1,33 @@
+<?php
+
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+class AddUserInvitesTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('user_invites', function (Blueprint $table) {
+            $table->increments('id');
+            $table->integer('user_id')->index();
+            $table->string('token')->index();
+            $table->nullableTimestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('user_invites');
+    }
+}
diff --git a/resources/lang/en/auth.php b/resources/lang/en/auth.php
index 1065945c0..4474fb7ee 100644
--- a/resources/lang/en/auth.php
+++ b/resources/lang/en/auth.php
@@ -64,4 +64,10 @@ return [
     'email_not_confirmed_click_link' => 'Please click the link in the email that was sent shortly after you registered.',
     'email_not_confirmed_resend' => 'If you cannot find the email you can re-send the confirmation email by submitting the form below.',
     'email_not_confirmed_resend_button' => 'Resend Confirmation Email',
+
+    // User Invite
+    'user_invite_email_subject' => 'You have been invited to join :appName!',
+    'user_invite_email_greeting' => 'A user account has been created for you on :appName.',
+    'user_invite_email_text' => 'Click the button below to set an account password and gain access:',
+    'user_invite_email_action' => 'Set Account Password',
 ];
\ No newline at end of file
diff --git a/resources/lang/en/errors.php b/resources/lang/en/errors.php
index b91a0c3e1..d66dcc92d 100644
--- a/resources/lang/en/errors.php
+++ b/resources/lang/en/errors.php
@@ -27,6 +27,7 @@ return [
     'social_account_register_instructions' => 'If you do not yet have an account, You can register an account using the :socialAccount option.',
     'social_driver_not_found' => 'Social driver not found',
     'social_driver_not_configured' => 'Your :socialAccount social settings are not configured correctly.',
+    'invite_token_expired' => 'This invitation link has expired. You can try to reset your account password or request a new invite from an administrator.',
 
     // System
     'path_not_writable' => 'File path :filePath could not be uploaded to. Ensure it is writable to the server.',