diff --git a/docs/advanced_usage.md b/docs/advanced_usage.md
index 89530db7f..957d5287e 100644
--- a/docs/advanced_usage.md
+++ b/docs/advanced_usage.md
@@ -589,6 +589,12 @@ case, Paperless will remove the staging copy as well as the scan, and give you a
 message asking you to restart the process from scratch, by scanning the odd pages again,
 followed by the even pages.
 
+It's important that the scan files get consumed in the correct order, and one at a time.
+You therefore need to make sure that Paperless is running while you upload the files into
+the directory; and if you're using [polling](/configuration#polling), make sure that
+`CONSUMER_POLLING` is set to a value lower than it takes for the second scan to appear,
+like 5-10 or even lower.
+
 Another thing that might happen is that you start a double sided scan, but then forget
 to upload the second file. To avoid collating the wrong documents if you then come back
 a day later to scan a new double-sided document, Paperless will only keep an "odd numbered
@@ -597,11 +603,11 @@ scan a completely new "odd numbered pages" one. The old staging file will get di
 
 ### Interaction with "subdirs as tags"
 
-The collation feature can be used together with the "subdirs as tags" feature (but this is not
-a requirement). Just create a correctly named double-sided subdir in the hierachy and upload
-your scans there. For example, both `double-sided/foo/bar` as well as `foo/bar/double-sided` will
-cause the collated document to be treated as if it were uploaded into `foo/bar` and receive both
-`foo` and `bar` tags, but not `double-sided`.
+The collation feature can be used together with the [subdirs as tags](/configuration#consume_config)
+feature (but this is not a requirement). Just create a correctly named double-sided subdir
+in the hierachy and upload your scans there. For example, both `double-sided/foo/bar` as
+well as `foo/bar/double-sided` will cause the collated document to be treated as if it
+were uploaded into `foo/bar` and receive both `foo` and `bar` tags, but not `double-sided`.
 
 ### Interaction with document splitting
 
diff --git a/docs/configuration.md b/docs/configuration.md
index 0ed2218a6..74486660f 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -35,6 +35,12 @@ matcher.
 
     Defaults to `redis://localhost:6379`.
 
+`PAPERLESS_REDIS_PREFIX=<prefix>`
+
+: Prefix to be used in Redis for keys and channels. Useful for sharing one Redis server among multiple Paperless instances.
+
+    Defaults to no prefix.
+
 ### Database
 
 `PAPERLESS_DBENGINE=<engine_name>`
@@ -495,6 +501,19 @@ HTTP header/value expected by Django, eg `'["HTTP_X_FORWARDED_PROTO", "https"]'`
     Settings this value has security implications.  Read the Django documentation
     and be sure you understand its usage before setting it.
 
+`PAPERLESS_EMAIL_CERTIFICATE_FILE=<path>`
+
+: Configures an additional SSL certificate file containing a [certificate](https://docs.python.org/3/library/ssl.html#certificates)
+or certificate chain which should be trusted for validating SSL connections against mail providers.
+This is for use with self-signed certificates against local IMAP servers.
+
+    Defaults to None.
+
+!!! warning
+
+    Settings this value has security implications for the security of your email.
+    Understand what it does and be sure you need to before setting.
+
 ## OCR settings {#ocr}
 
 Paperless uses [OCRmyPDF](https://ocrmypdf.readthedocs.io/en/latest/)
diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf
index 865591166..1d07e98e4 100644
--- a/src-ui/messages.xlf
+++ b/src-ui/messages.xlf
@@ -723,7 +723,7 @@
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">600</context>
+          <context context-type="linenumber">648</context>
         </context-group>
       </trans-unit>
       <trans-unit id="2526035785704676448" datatype="html">
@@ -2913,19 +2913,19 @@
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">711</context>
+          <context context-type="linenumber">759</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">771</context>
+          <context context-type="linenumber">819</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">838</context>
+          <context context-type="linenumber">886</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">901</context>
+          <context context-type="linenumber">949</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1181910457994920507" datatype="html">
@@ -2940,19 +2940,19 @@
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">713</context>
+          <context context-type="linenumber">761</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">773</context>
+          <context context-type="linenumber">821</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">840</context>
+          <context context-type="linenumber">888</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">903</context>
+          <context context-type="linenumber">951</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5729001209753056399" datatype="html">
@@ -4489,235 +4489,263 @@
           <context context-type="linenumber">372</context>
         </context-group>
       </trans-unit>
+      <trans-unit id="3066660568529853846" datatype="html">
+        <source>Error retrieving groups</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
+          <context context-type="linenumber">278</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="1235706724900303689" datatype="html">
+        <source>Error retrieving users</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
+          <context context-type="linenumber">287</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="5241231471117657636" datatype="html">
+        <source>Error retrieving mail rules</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
+          <context context-type="linenumber">314</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="3178554336792037159" datatype="html">
+        <source>Error retrieving mail accounts</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
+          <context context-type="linenumber">323</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="5610279464668232148" datatype="html">
         <source>Saved view &quot;<x id="PH" equiv-text="savedView.name"/>&quot; deleted.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">482</context>
+          <context context-type="linenumber">530</context>
         </context-group>
       </trans-unit>
       <trans-unit id="3891152409365583719" datatype="html">
         <source>Settings saved</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">584</context>
+          <context context-type="linenumber">632</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7217000812750597833" datatype="html">
         <source>Settings were saved successfully.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">585</context>
+          <context context-type="linenumber">633</context>
         </context-group>
       </trans-unit>
       <trans-unit id="525012668859298131" datatype="html">
         <source>Settings were saved successfully. Reload is required to apply some changes.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">589</context>
+          <context context-type="linenumber">637</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8491974984518503778" datatype="html">
         <source>Reload now</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">590</context>
+          <context context-type="linenumber">638</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6839066544204061364" datatype="html">
         <source>Use system language</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">609</context>
+          <context context-type="linenumber">657</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7729897675462249787" datatype="html">
         <source>Use date format of display language</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">616</context>
+          <context context-type="linenumber">664</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5260584511980773458" datatype="html">
         <source>Error while storing settings on server.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">636</context>
+          <context context-type="linenumber">684</context>
         </context-group>
       </trans-unit>
       <trans-unit id="4510369340305901516" datatype="html">
         <source>Password has been changed, you will be logged out momentarily.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">679</context>
+          <context context-type="linenumber">727</context>
         </context-group>
       </trans-unit>
       <trans-unit id="2753185112875184719" datatype="html">
         <source>Saved user &quot;<x id="PH" equiv-text="newUser.username"/>&quot;.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">686</context>
+          <context context-type="linenumber">734</context>
         </context-group>
       </trans-unit>
       <trans-unit id="3471101514724661554" datatype="html">
         <source>Error saving user.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">698</context>
+          <context context-type="linenumber">746</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5565868288871970148" datatype="html">
         <source>Confirm delete user account</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">709</context>
+          <context context-type="linenumber">757</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8133663925694885325" datatype="html">
         <source>This operation will permanently delete this user account.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">710</context>
+          <context context-type="linenumber">758</context>
         </context-group>
       </trans-unit>
       <trans-unit id="857903183180440990" datatype="html">
         <source>Deleted user</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">719</context>
+          <context context-type="linenumber">767</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1942566571910298572" datatype="html">
         <source>Error deleting user.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">727</context>
+          <context context-type="linenumber">775</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5766640174051730159" datatype="html">
         <source>Saved group &quot;<x id="PH" equiv-text="newGroup.name"/>&quot;.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">748</context>
+          <context context-type="linenumber">796</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8382042988405122578" datatype="html">
         <source>Error saving group.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">758</context>
+          <context context-type="linenumber">806</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6538873300613683004" datatype="html">
         <source>Confirm delete user group</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">769</context>
+          <context context-type="linenumber">817</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7710984639498518244" datatype="html">
         <source>This operation will permanently delete this user group.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">770</context>
+          <context context-type="linenumber">818</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6834066329827670963" datatype="html">
         <source>Deleted group</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">779</context>
+          <context context-type="linenumber">827</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8850738980935204840" datatype="html">
         <source>Error deleting group.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">787</context>
+          <context context-type="linenumber">835</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6327501535846658797" datatype="html">
         <source>Saved account &quot;<x id="PH" equiv-text="newMailAccount.name"/>&quot;.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">813</context>
+          <context context-type="linenumber">861</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8067594003836508139" datatype="html">
         <source>Error saving account.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">825</context>
+          <context context-type="linenumber">873</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5641934153807844674" datatype="html">
         <source>Confirm delete mail account</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">836</context>
+          <context context-type="linenumber">884</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7176985344323395435" datatype="html">
         <source>This operation will permanently delete this mail account.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">837</context>
+          <context context-type="linenumber">885</context>
         </context-group>
       </trans-unit>
       <trans-unit id="4233826387148482123" datatype="html">
         <source>Deleted mail account</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">846</context>
+          <context context-type="linenumber">894</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6202503362522392111" datatype="html">
         <source>Error deleting mail account.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">855</context>
+          <context context-type="linenumber">903</context>
         </context-group>
       </trans-unit>
       <trans-unit id="123368655395433699" datatype="html">
         <source>Saved rule &quot;<x id="PH" equiv-text="newMailRule.name"/>&quot;.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">876</context>
+          <context context-type="linenumber">924</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8951124554918814321" datatype="html">
         <source>Error saving rule.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">888</context>
+          <context context-type="linenumber">936</context>
         </context-group>
       </trans-unit>
       <trans-unit id="3896080636020672118" datatype="html">
         <source>Confirm delete mail rule</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">899</context>
+          <context context-type="linenumber">947</context>
         </context-group>
       </trans-unit>
       <trans-unit id="2250372580580310337" datatype="html">
         <source>This operation will permanently delete this mail rule.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">900</context>
+          <context context-type="linenumber">948</context>
         </context-group>
       </trans-unit>
       <trans-unit id="9077981247971516916" datatype="html">
         <source>Deleted mail rule</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">909</context>
+          <context context-type="linenumber">957</context>
         </context-group>
       </trans-unit>
       <trans-unit id="2033194641751367552" datatype="html">
         <source>Error deleting mail rule.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
-          <context context-type="linenumber">918</context>
+          <context context-type="linenumber">966</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5101757640976222639" datatype="html">
diff --git a/src-ui/src/app/components/common/input/tags/tags.component.html b/src-ui/src/app/components/common/input/tags/tags.component.html
index eba8ef218..497a62335 100644
--- a/src-ui/src/app/components/common/input/tags/tags.component.html
+++ b/src-ui/src/app/components/common/input/tags/tags.component.html
@@ -2,7 +2,7 @@
   <label class="form-label" for="tags" i18n>Tags</label>
 
   <div class="input-group flex-nowrap">
-    <ng-select name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="value"
+    <ng-select #tagSelect name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="value"
       [disabled]="disabled"
       [multiple]="true"
       [closeOnSelect]="false"
@@ -11,11 +11,7 @@
       [addTag]="allowCreate ? createTagRef : false"
       addTagText="Add tag"
       i18n-addTagText
-      (change)="onChange(value)"
-      (search)="onSearch($event)"
-      (focus)="clearLastSearchTerm()"
-      (clear)="clearLastSearchTerm()"
-      (blur)="onBlur()">
+      (change)="onChange(value)">
 
       <ng-template ng-label-tmp let-item="item">
         <span class="tag-wrap tag-wrap-delete" (mousedown)="removeTag($event, item.id)">
diff --git a/src-ui/src/app/components/common/input/tags/tags.component.spec.ts b/src-ui/src/app/components/common/input/tags/tags.component.spec.ts
index f3ea05d5d..85c492aba 100644
--- a/src-ui/src/app/components/common/input/tags/tags.component.spec.ts
+++ b/src-ui/src/app/components/common/input/tags/tags.component.spec.ts
@@ -15,16 +15,28 @@ import {
   DEFAULT_MATCHING_ALGORITHM,
   MATCH_ALL,
 } from 'src/app/data/matching-model'
-import { NgSelectModule } from '@ng-select/ng-select'
+import { NgSelectComponent, NgSelectModule } from '@ng-select/ng-select'
 import { RouterTestingModule } from '@angular/router/testing'
 import { HttpClientTestingModule } from '@angular/common/http/testing'
 import { of } from 'rxjs'
 import { TagService } from 'src/app/services/rest/tag.service'
 import {
+  NgbAccordionModule,
   NgbModal,
   NgbModalModule,
   NgbModalRef,
+  NgbPopoverModule,
 } from '@ng-bootstrap/ng-bootstrap'
+import { TagEditDialogComponent } from '../../edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
+import { CheckComponent } from '../check/check.component'
+import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
+import { TextComponent } from '../text/text.component'
+import { ColorComponent } from '../color/color.component'
+import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import { PermissionsFormComponent } from '../permissions/permissions-form/permissions-form.component'
+import { SelectComponent } from '../select/select.component'
+import { ColorSliderModule } from 'ngx-color/slider'
+import { By } from '@angular/platform-browser'
 
 const tags: PaperlessTag[] = [
   {
@@ -56,12 +68,32 @@ describe('TagsComponent', () => {
 
   beforeEach(async () => {
     TestBed.configureTestingModule({
-      declarations: [TagsComponent],
+      declarations: [
+        TagsComponent,
+        TagEditDialogComponent,
+        TextComponent,
+        ColorComponent,
+        IfOwnerDirective,
+        SelectComponent,
+        TextComponent,
+        PermissionsFormComponent,
+        ColorComponent,
+        CheckComponent,
+      ],
       providers: [
         {
           provide: TagService,
           useValue: {
-            listAll: () => of(tags),
+            listAll: () =>
+              of({
+                results: tags,
+              }),
+            create: () =>
+              of({
+                name: 'bar',
+                id: 99,
+                color: '#fff000',
+              }),
           },
         },
       ],
@@ -72,6 +104,8 @@ describe('TagsComponent', () => {
         RouterTestingModule,
         HttpClientTestingModule,
         NgbModalModule,
+        NgbAccordionModule,
+        NgbPopoverModule,
       ],
     }).compileComponents()
 
@@ -85,7 +119,7 @@ describe('TagsComponent', () => {
   })
 
   it('should support suggestions', () => {
-    expect(component.value).toBeUndefined()
+    expect(component.value).toHaveLength(0)
     component.value = []
     component.tags = tags
     component.suggestions = [1, 2]
@@ -107,19 +141,19 @@ describe('TagsComponent', () => {
   it('should support create new using last search term and open a modal', () => {
     let activeInstances: NgbModalRef[]
     modalService.activeInstances.subscribe((v) => (activeInstances = v))
-    component.onSearch({ term: 'bar' })
+    component.select.searchTerm = 'foobar'
     component.createTag()
     expect(modalService.hasOpenModals()).toBeTruthy()
-    expect(activeInstances[0].componentInstance.object.name).toEqual('bar')
+    expect(activeInstances[0].componentInstance.object.name).toEqual('foobar')
+    const editDialog = activeInstances[0]
+      .componentInstance as TagEditDialogComponent
+    editDialog.save() // create is mocked
+    fixture.detectChanges()
+    fixture.whenStable().then(() => {
+      expect(fixture.debugElement.nativeElement.textContent).toContain('foobar')
+    })
   })
 
-  it('should clear search term on blur after delay', fakeAsync(() => {
-    const clearSpy = jest.spyOn(component, 'clearLastSearchTerm')
-    component.onBlur()
-    tick(3000)
-    expect(clearSpy).toHaveBeenCalled()
-  }))
-
   it('support remove tags', () => {
     component.tags = tags
     component.value = [1, 2]
@@ -132,6 +166,7 @@ describe('TagsComponent', () => {
   })
 
   it('should get tags', () => {
+    component.tags = null
     expect(component.getTag(2)).toBeNull()
     component.tags = tags
     expect(component.getTag(2)).toEqual(tags[1])
diff --git a/src-ui/src/app/components/common/input/tags/tags.component.ts b/src-ui/src/app/components/common/input/tags/tags.component.ts
index 4fb0151b6..b6bfddb3c 100644
--- a/src-ui/src/app/components/common/input/tags/tags.component.ts
+++ b/src-ui/src/app/components/common/input/tags/tags.component.ts
@@ -5,6 +5,7 @@ import {
   Input,
   OnInit,
   Output,
+  ViewChild,
 } from '@angular/core'
 import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
 import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
@@ -12,6 +13,8 @@ import { PaperlessTag } from 'src/app/data/paperless-tag'
 import { TagEditDialogComponent } from '../../edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
 import { TagService } from 'src/app/services/rest/tag.service'
 import { EditDialogMode } from '../../edit-dialog/edit-dialog.component'
+import { first, firstValueFrom, tap } from 'rxjs'
+import { NgSelectComponent } from '@ng-select/ng-select'
 
 @Component({
   providers: [
@@ -74,14 +77,14 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
   @Output()
   filterDocuments = new EventEmitter<PaperlessTag[]>()
 
-  value: number[]
+  @ViewChild('tagSelect') select: NgSelectComponent
 
-  tags: PaperlessTag[]
+  value: number[] = []
+
+  tags: PaperlessTag[] = []
 
   public createTagRef: (name) => void
 
-  private _lastSearchTerm: string
-
   getTag(id: number) {
     if (this.tags) {
       return this.tags.find((tag) => tag.id == id)
@@ -111,15 +114,20 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
     })
     modal.componentInstance.dialogMode = EditDialogMode.CREATE
     if (name) modal.componentInstance.object = { name: name }
-    else if (this._lastSearchTerm)
-      modal.componentInstance.object = { name: this._lastSearchTerm }
-    modal.componentInstance.succeeded.subscribe((newTag) => {
-      this.tagService.listAll().subscribe((tags) => {
-        this.tags = tags.results
-        this.value = [...this.value, newTag.id]
-        this.onChange(this.value)
-      })
-    })
+    else if (this.select.searchTerm)
+      modal.componentInstance.object = { name: this.select.searchTerm }
+    this.select.searchTerm = null
+    this.select.detectChanges()
+    return firstValueFrom(
+      (modal.componentInstance as TagEditDialogComponent).succeeded.pipe(
+        first(),
+        tap(() => {
+          this.tagService.listAll().subscribe((tags) => {
+            this.tags = tags.results
+          })
+        })
+      )
+    )
   }
 
   getSuggestions() {
@@ -137,20 +145,6 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
     this.onChange(this.value)
   }
 
-  clearLastSearchTerm() {
-    this._lastSearchTerm = null
-  }
-
-  onSearch($event) {
-    this._lastSearchTerm = $event.term
-  }
-
-  onBlur() {
-    setTimeout(() => {
-      this.clearLastSearchTerm()
-    }, 3000)
-  }
-
   get hasPrivate(): boolean {
     return this.value.some(
       (t) => this.tags?.find((t2) => t2.id === t) === undefined
diff --git a/src-ui/src/app/components/manage/settings/settings.component.html b/src-ui/src/app/components/manage/settings/settings.component.html
index 5090d531d..8b0132902 100644
--- a/src-ui/src/app/components/manage/settings/settings.component.html
+++ b/src-ui/src/app/components/manage/settings/settings.component.html
@@ -243,7 +243,7 @@
           <ng-container *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailAccount }">
             <h4>
               <ng-container i18n>Mail accounts</ng-container>
-              <button type="button" class="btn btn-sm btn-primary ms-4" (click)="editMailAccount()">
+              <button type="button" class="btn btn-sm btn-primary ms-4" (click)="editMailAccount()" *appIfPermissions="{ action: PermissionAction.Add, type: PermissionType.MailAccount }">
                 <svg class="sidebaricon me-1" fill="currentColor">
                   <use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
                 </svg>
@@ -262,7 +262,7 @@
 
                 <li *ngFor="let account of mailAccounts" class="list-group-item" [formGroupName]="account.id">
                   <div class="row">
-                    <div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailAccount(account)">{{account.name}}</button></div>
+                    <div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailAccount(account)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailAccount)">{{account.name}}</button></div>
                     <div class="col d-flex align-items-center">{{account.imap_server}}</div>
                     <div class="col">
                       <div class="btn-group">
@@ -280,7 +280,7 @@
           <ng-container *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailRule }">
             <h4 class="mt-4">
               <ng-container i18n>Mail rules</ng-container>
-              <button type="button" class="btn btn-sm btn-primary ms-4" (click)="editMailRule()">
+              <button type="button" class="btn btn-sm btn-primary ms-4" (click)="editMailRule()" *appIfPermissions="{ action: PermissionAction.Add, type: PermissionType.MailRule }">
                 <svg class="sidebaricon me-1" fill="currentColor">
                   <use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
                 </svg>
@@ -299,7 +299,7 @@
 
                 <li *ngFor="let rule of mailRules" class="list-group-item" [formGroupName]="rule.id">
                   <div class="row">
-                    <div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailRule(rule)">{{rule.name}}</button></div>
+                    <div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailRule(rule)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailRule)">{{rule.name}}</button></div>
                     <div class="col d-flex align-items-center">{{(mailAccountService.getCached(rule.account) | async)?.name}}</div>
                     <div class="col">
                       <div class="btn-group">
@@ -323,7 +323,7 @@
       </ng-template>
     </li>
 
-    <li [ngbNavItem]="SettingsNavIDs.UsersGroups" *appIfPermissions="{ action: PermissionAction.Add, type: PermissionType.User }" (mouseover)="maybeInitializeTab(SettingsNavIDs.UsersGroups)" (focusin)="maybeInitializeTab(SettingsNavIDs.UsersGroups)">
+    <li [ngbNavItem]="SettingsNavIDs.UsersGroups" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }" (mouseover)="maybeInitializeTab(SettingsNavIDs.UsersGroups)" (focusin)="maybeInitializeTab(SettingsNavIDs.UsersGroups)">
       <a ngbNavLink i18n>Users & Groups</a>
       <ng-template ngbNavContent>
 
@@ -334,7 +334,7 @@
             <svg class="sidebaricon me-1" fill="currentColor">
               <use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
             </svg>
-            <ng-container i18n>Add User</ng-container>
+            <ng-container *appIfPermissions="{ action: PermissionAction.Add, type: PermissionType.User }" i18n>Add User</ng-container>
           </button>
         </h4>
         <ul class="list-group" formGroupName="usersGroup">
@@ -350,13 +350,13 @@
 
           <li *ngFor="let user of users" class="list-group-item" [formGroupName]="user.id">
             <div class="row">
-              <div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editUser(user)">{{user.username}}</button></div>
+              <div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editUser(user)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.User)">{{user.username}}</button></div>
               <div class="col d-flex align-items-center">{{user.first_name}} {{user.last_name}}</div>
               <div class="col d-flex align-items-center">{{user.groups?.map(getGroupName, this).join(', ')}}</div>
               <div class="col">
                 <div class="btn-group">
-                  <button class="btn btn-sm btn-primary" type="button" (click)="editUser(user)" i18n>Edit</button>
-                  <button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteUser(user)" i18n>Delete</button>
+                  <button class="btn btn-sm btn-primary" type="button" (click)="editUser(user)" *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.User }" i18n>Edit</button>
+                  <button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteUser(user)" *appIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.User }" i18n>Delete</button>
                 </div>
               </div>
             </div>
@@ -369,7 +369,7 @@
             <svg class="sidebaricon me-1" fill="currentColor">
               <use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
             </svg>
-            <ng-container i18n>Add Group</ng-container>
+            <ng-container *appIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Group }" i18n>Add Group</ng-container>
           </button>
         </h4>
         <ul *ngIf="groups.length > 0" class="list-group" formGroupName="groupsGroup">
@@ -385,13 +385,13 @@
 
           <li *ngFor="let group of groups" class="list-group-item" [formGroupName]="group.id">
             <div class="row">
-              <div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editGroup(group)">{{group.name}}</button></div>
+              <div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editGroup(group)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.Group)">{{group.name}}</button></div>
               <div class="col"></div>
               <div class="col"></div>
               <div class="col">
                 <div class="btn-group">
-                  <button class="btn btn-sm btn-primary" type="button" (click)="editGroup(group)" i18n>Edit</button>
-                  <button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteGroup(group)" i18n>Delete</button>
+                  <button class="btn btn-sm btn-primary" type="button" (click)="editGroup(group)" *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Group }" i18n>Edit</button>
+                  <button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteGroup(group)" *appIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Group }" i18n>Delete</button>
                 </div>
               </div>
             </div>
diff --git a/src-ui/src/app/components/manage/settings/settings.component.spec.ts b/src-ui/src/app/components/manage/settings/settings.component.spec.ts
index c4a9d4a4b..fb8f0a7f4 100644
--- a/src-ui/src/app/components/manage/settings/settings.component.spec.ts
+++ b/src-ui/src/app/components/manage/settings/settings.component.spec.ts
@@ -15,6 +15,7 @@ import {
   NgbModule,
   NgbNavLink,
   NgbModalRef,
+  NgbAlertModule,
 } from '@ng-bootstrap/ng-bootstrap'
 import { of, throwError } from 'rxjs'
 import { routes } from 'src/app/app-routing.module'
@@ -42,6 +43,13 @@ import { CheckComponent } from '../../common/input/check/check.component'
 import { ColorComponent } from '../../common/input/color/color.component'
 import { PageHeaderComponent } from '../../common/page-header/page-header.component'
 import { SettingsComponent } from './settings.component'
+import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
+import { SelectComponent } from '../../common/input/select/select.component'
+import { TextComponent } from '../../common/input/text/text.component'
+import { PasswordComponent } from '../../common/input/password/password.component'
+import { NumberComponent } from '../../common/input/number/number.component'
+import { TagsComponent } from '../../common/input/tags/tags.component'
+import { NgSelectModule } from '@ng-select/ng-select'
 
 const savedViews = [
   { id: 1, name: 'view1' },
@@ -90,6 +98,14 @@ describe('SettingsComponent', () => {
         ConfirmDialogComponent,
         CheckComponent,
         ColorComponent,
+        SafeHtmlPipe,
+        SelectComponent,
+        TextComponent,
+        PasswordComponent,
+        NumberComponent,
+        TagsComponent,
+        MailAccountEditDialogComponent,
+        MailRuleEditDialogComponent,
       ],
       providers: [CustomDatePipe, DatePipe, PermissionsGuard],
       imports: [
@@ -98,6 +114,8 @@ describe('SettingsComponent', () => {
         RouterTestingModule.withRoutes(routes),
         FormsModule,
         ReactiveFormsModule,
+        NgbAlertModule,
+        NgSelectModule,
       ],
     }).compileComponents()
 
@@ -116,52 +134,66 @@ describe('SettingsComponent', () => {
     jest
       .spyOn(permissionsService, 'currentUserOwnsObject')
       .mockReturnValue(true)
-    jest.spyOn(userService, 'listAll').mockReturnValue(
-      of({
-        all: users.map((u) => u.id),
-        count: users.length,
-        results: users.concat([]),
-      })
-    )
     groupService = TestBed.inject(GroupService)
-    jest.spyOn(groupService, 'listAll').mockReturnValue(
-      of({
-        all: groups.map((g) => g.id),
-        count: groups.length,
-        results: groups.concat([]),
-      })
-    )
     savedViewService = TestBed.inject(SavedViewService)
-    jest.spyOn(savedViewService, 'listAll').mockReturnValue(
-      of({
-        all: savedViews.map((v) => v.id),
-        count: savedViews.length,
-        results: (savedViews as PaperlessSavedView[]).concat([]),
-      })
-    )
     mailAccountService = TestBed.inject(MailAccountService)
-    jest.spyOn(mailAccountService, 'listAll').mockReturnValue(
-      of({
-        all: mailAccounts.map((a) => a.id),
-        count: mailAccounts.length,
-        results: (mailAccounts as PaperlessMailAccount[]).concat([]),
-      })
-    )
     mailRuleService = TestBed.inject(MailRuleService)
-    jest.spyOn(mailRuleService, 'listAll').mockReturnValue(
-      of({
-        all: mailRules.map((r) => r.id),
-        count: mailRules.length,
-        results: (mailRules as PaperlessMailRule[]).concat([]),
-      })
-    )
+  })
+
+  function completeSetup(excludeService = null) {
+    if (excludeService !== userService) {
+      jest.spyOn(userService, 'listAll').mockReturnValue(
+        of({
+          all: users.map((u) => u.id),
+          count: users.length,
+          results: users.concat([]),
+        })
+      )
+    }
+    if (excludeService !== groupService) {
+      jest.spyOn(groupService, 'listAll').mockReturnValue(
+        of({
+          all: groups.map((g) => g.id),
+          count: groups.length,
+          results: groups.concat([]),
+        })
+      )
+    }
+    if (excludeService !== savedViewService) {
+      jest.spyOn(savedViewService, 'listAll').mockReturnValue(
+        of({
+          all: savedViews.map((v) => v.id),
+          count: savedViews.length,
+          results: (savedViews as PaperlessSavedView[]).concat([]),
+        })
+      )
+    }
+    if (excludeService !== mailAccountService) {
+      jest.spyOn(mailAccountService, 'listAll').mockReturnValue(
+        of({
+          all: mailAccounts.map((a) => a.id),
+          count: mailAccounts.length,
+          results: (mailAccounts as PaperlessMailAccount[]).concat([]),
+        })
+      )
+    }
+    if (excludeService !== mailRuleService) {
+      jest.spyOn(mailRuleService, 'listAll').mockReturnValue(
+        of({
+          all: mailRules.map((r) => r.id),
+          count: mailRules.length,
+          results: (mailRules as PaperlessMailRule[]).concat([]),
+        })
+      )
+    }
 
     fixture = TestBed.createComponent(SettingsComponent)
     component = fixture.componentInstance
     fixture.detectChanges()
-  })
+  }
 
   it('should support tabbed settings & change URL, prevent navigation if dirty confirmation rejected', () => {
+    completeSetup()
     const navigateSpy = jest.spyOn(router, 'navigate')
     const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
     tabButtons[1].nativeElement.dispatchEvent(new MouseEvent('click'))
@@ -187,6 +219,7 @@ describe('SettingsComponent', () => {
   })
 
   it('should support direct link to tab by URL, scroll if needed', () => {
+    completeSetup()
     jest
       .spyOn(activatedRoute, 'paramMap', 'get')
       .mockReturnValue(of(convertToParamMap({ section: 'mail' })))
@@ -199,6 +232,7 @@ describe('SettingsComponent', () => {
   })
 
   it('should lazy load tab data', () => {
+    completeSetup()
     const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
 
     expect(component.savedViews).toBeUndefined()
@@ -221,6 +255,7 @@ describe('SettingsComponent', () => {
   })
 
   it('should support save saved views, show error', () => {
+    completeSetup()
     component.maybeInitializeTab(3) // SavedViews
 
     const toastErrorSpy = jest.spyOn(toastService, 'showError')
@@ -248,6 +283,7 @@ describe('SettingsComponent', () => {
   })
 
   it('should support save local settings updating appearance settings and calling API, show error', () => {
+    completeSetup()
     const toastErrorSpy = jest.spyOn(toastService, 'showError')
     const toastSpy = jest.spyOn(toastService, 'show')
     const storeSpy = jest.spyOn(settingsService, 'storeSettings')
@@ -275,6 +311,7 @@ describe('SettingsComponent', () => {
   })
 
   it('should offer reload if settings changes require', () => {
+    completeSetup()
     let toast: Toast
     toastService.getToasts().subscribe((t) => (toast = t[0]))
     component.initialize(true) // reset
@@ -288,6 +325,7 @@ describe('SettingsComponent', () => {
   })
 
   it('should allow setting theme color, visually apply change immediately but not save', () => {
+    completeSetup()
     const appearanceSpy = jest.spyOn(
       settingsService,
       'updateAppearanceSettings'
@@ -304,6 +342,7 @@ describe('SettingsComponent', () => {
   })
 
   it('should support delete saved view', () => {
+    completeSetup()
     component.maybeInitializeTab(3) // SavedViews
     const toastSpy = jest.spyOn(toastService, 'showInfo')
     const deleteSpy = jest.spyOn(savedViewService, 'delete')
@@ -316,6 +355,7 @@ describe('SettingsComponent', () => {
   })
 
   it('should support edit / create user, show error if needed', () => {
+    completeSetup()
     let modal: NgbModalRef
     modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
     component.editUser(users[0])
@@ -332,6 +372,7 @@ describe('SettingsComponent', () => {
   })
 
   it('should support delete user, show error if needed', () => {
+    completeSetup()
     let modal: NgbModalRef
     modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
     component.deleteUser(users[0])
@@ -352,6 +393,7 @@ describe('SettingsComponent', () => {
   })
 
   it('should logout current user if password changed, after delay', fakeAsync(() => {
+    completeSetup()
     let modal: NgbModalRef
     modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
     component.editUser(users[0])
@@ -371,6 +413,7 @@ describe('SettingsComponent', () => {
   }))
 
   it('should support edit / create group, show error if needed', () => {
+    completeSetup()
     let modal: NgbModalRef
     modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
     component.editGroup(groups[0])
@@ -386,6 +429,7 @@ describe('SettingsComponent', () => {
   })
 
   it('should support delete group, show error if needed', () => {
+    completeSetup()
     let modal: NgbModalRef
     modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
     component.deleteGroup(users[0])
@@ -406,12 +450,71 @@ describe('SettingsComponent', () => {
   })
 
   it('should get group name', () => {
+    completeSetup()
     component.maybeInitializeTab(5) // UsersGroups
     expect(component.getGroupName(1)).toEqual(groups[0].name)
     expect(component.getGroupName(11)).toEqual('')
   })
 
+  it('should show errors on load if load mailAccounts failure', () => {
+    const toastErrorSpy = jest.spyOn(toastService, 'showError')
+    jest
+      .spyOn(mailAccountService, 'listAll')
+      .mockImplementation(() =>
+        throwError(() => new Error('failed to load mail accounts'))
+      )
+    completeSetup(mailAccountService)
+    const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
+    tabButtons[3].nativeElement.dispatchEvent(new MouseEvent('click')) // mail tab
+    fixture.detectChanges()
+    expect(toastErrorSpy).toBeCalled()
+  })
+
+  it('should show errors on load if load mailRules failure', () => {
+    const toastErrorSpy = jest.spyOn(toastService, 'showError')
+    jest
+      .spyOn(mailRuleService, 'listAll')
+      .mockImplementation(() =>
+        throwError(() => new Error('failed to load mail rules'))
+      )
+    completeSetup(mailRuleService)
+    const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
+    tabButtons[3].nativeElement.dispatchEvent(new MouseEvent('click')) // mail tab
+    fixture.detectChanges()
+    // tabButtons[4].nativeElement.dispatchEvent(new MouseEvent('click'))
+    expect(toastErrorSpy).toBeCalled()
+  })
+
+  it('should show errors on load if load users failure', () => {
+    const toastErrorSpy = jest.spyOn(toastService, 'showError')
+    jest
+      .spyOn(userService, 'listAll')
+      .mockImplementation(() =>
+        throwError(() => new Error('failed to load users'))
+      )
+    completeSetup(userService)
+    const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
+    tabButtons[4].nativeElement.dispatchEvent(new MouseEvent('click')) // users tab
+    fixture.detectChanges()
+    expect(toastErrorSpy).toBeCalled()
+  })
+
+  it('should show errors on load if load groups failure', () => {
+    const toastErrorSpy = jest.spyOn(toastService, 'showError')
+    jest
+      .spyOn(groupService, 'listAll')
+      .mockImplementation(() =>
+        throwError(() => new Error('failed to load groups'))
+      )
+    completeSetup(groupService)
+    const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
+    tabButtons[4].nativeElement.dispatchEvent(new MouseEvent('click')) // users tab
+    fixture.detectChanges()
+    expect(toastErrorSpy).toBeCalled()
+  })
+
   it('should support edit / create mail account, show error if needed', () => {
+    completeSetup()
     let modal: NgbModalRef
     modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
     component.editMailAccount(mailAccounts[0] as PaperlessMailAccount)
@@ -427,6 +530,7 @@ describe('SettingsComponent', () => {
   })
 
   it('should support delete mail account, show error if needed', () => {
+    completeSetup()
     let modal: NgbModalRef
     modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
     component.deleteMailAccount(mailAccounts[0] as PaperlessMailAccount)
@@ -447,6 +551,7 @@ describe('SettingsComponent', () => {
   })
 
   it('should support edit / create mail rule, show error if needed', () => {
+    completeSetup()
     let modal: NgbModalRef
     modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
     component.editMailRule(mailRules[0] as PaperlessMailRule)
@@ -462,6 +567,7 @@ describe('SettingsComponent', () => {
   })
 
   it('should support delete mail rule, show error if needed', () => {
+    completeSetup()
     let modal: NgbModalRef
     modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
     component.deleteMailRule(mailRules[0] as PaperlessMailRule)
diff --git a/src-ui/src/app/components/manage/settings/settings.component.ts b/src-ui/src/app/components/manage/settings/settings.component.ts
index a49f2dd21..785e5d347 100644
--- a/src-ui/src/app/components/manage/settings/settings.component.ts
+++ b/src-ui/src/app/components/manage/settings/settings.component.ts
@@ -146,7 +146,7 @@ export class SettingsComponent
     private groupsService: GroupService,
     private router: Router,
     private modalService: NgbModal,
-    private permissionsService: PermissionsService
+    public permissionsService: PermissionsService
   ) {
     super()
     this.settings.settingsSaved.subscribe(() => {
@@ -259,25 +259,73 @@ export class SettingsComponent
       navID == SettingsNavIDs.UsersGroups &&
       (!this.users || !this.groups)
     ) {
-      this.usersService.listAll().subscribe((r) => {
-        this.users = r.results
-        this.groupsService.listAll().subscribe((r) => {
-          this.groups = r.results
-          this.initialize(false)
+      this.usersService
+        .listAll()
+        .pipe(first())
+        .subscribe({
+          next: (r) => {
+            this.users = r.results
+            this.groupsService
+              .listAll()
+              .pipe(first())
+              .subscribe({
+                next: (r) => {
+                  this.groups = r.results
+                  this.initialize(false)
+                },
+                error: (e) => {
+                  this.toastService.showError(
+                    $localize`Error retrieving groups`,
+                    10000,
+                    JSON.stringify(e)
+                  )
+                },
+              })
+          },
+          error: (e) => {
+            this.toastService.showError(
+              $localize`Error retrieving users`,
+              10000,
+              JSON.stringify(e)
+            )
+          },
         })
-      })
     } else if (
       navID == SettingsNavIDs.Mail &&
       (!this.mailAccounts || !this.mailRules)
     ) {
-      this.mailAccountService.listAll().subscribe((r) => {
-        this.mailAccounts = r.results
+      this.mailAccountService
+        .listAll()
+        .pipe(first())
+        .subscribe({
+          next: (r) => {
+            this.mailAccounts = r.results
 
-        this.mailRuleService.listAll().subscribe((r) => {
-          this.mailRules = r.results
-          this.initialize(false)
+            this.mailRuleService
+              .listAll()
+              .pipe(first())
+              .subscribe({
+                next: (r) => {
+                  this.mailRules = r.results
+                  this.initialize(false)
+                },
+                error: (e) => {
+                  this.toastService.showError(
+                    $localize`Error retrieving mail rules`,
+                    10000,
+                    JSON.stringify(e)
+                  )
+                },
+              })
+          },
+          error: (e) => {
+            this.toastService.showError(
+              $localize`Error retrieving mail accounts`,
+              10000,
+              JSON.stringify(e)
+            )
+          },
         })
-      })
     }
   }
 
diff --git a/src-ui/src/app/services/toast.service.ts b/src-ui/src/app/services/toast.service.ts
index 2d11d663e..ef282c522 100644
--- a/src-ui/src/app/services/toast.service.ts
+++ b/src-ui/src/app/services/toast.service.ts
@@ -1,5 +1,5 @@
 import { Injectable } from '@angular/core'
-import { Subject, zip } from 'rxjs'
+import { Subject } from 'rxjs'
 
 export interface Toast {
   title: string
diff --git a/src-ui/src/environments/environment.prod.ts b/src-ui/src/environments/environment.prod.ts
index a36d733c3..3d1d968bf 100644
--- a/src-ui/src/environments/environment.prod.ts
+++ b/src-ui/src/environments/environment.prod.ts
@@ -5,7 +5,7 @@ export const environment = {
   apiBaseUrl: document.baseURI + 'api/',
   apiVersion: '3',
   appTitle: 'Paperless-ngx',
-  version: '1.17.1',
+  version: '1.17.1-dev',
   webSocketHost: window.location.host,
   webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
   webSocketBaseUrl: base_url.pathname + 'ws/',
diff --git a/src-ui/src/theme.scss b/src-ui/src/theme.scss
index d90afa6c1..33748c81b 100644
--- a/src-ui/src/theme.scss
+++ b/src-ui/src/theme.scss
@@ -80,6 +80,9 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,<svg xmlns='htt
   .btn {
     --bs-btn-disabled-opacity: 0.35;
   }
+  .btn.btn-link {
+    --bs-btn-disabled-opacity: 0.85;
+  }
 
   .btn-primary {
     &:hover, &:focus, &.active, &:active {
diff --git a/src/documents/tests/test_api.py b/src/documents/tests/test_api.py
index d788cf6a4..88180d4d8 100644
--- a/src/documents/tests/test_api.py
+++ b/src/documents/tests/test_api.py
@@ -3453,6 +3453,110 @@ class TestBulkEdit(DirectoriesMixin, APITestCase):
         self.assertCountEqual(args[0], [self.doc2.id, self.doc3.id])
         self.assertEqual(len(kwargs["set_permissions"]["view"]["users"]), 2)
 
+    @mock.patch("documents.serialisers.bulk_edit.set_permissions")
+    def test_insufficient_permissions_ownership(self, m):
+        """
+        GIVEN:
+            - Documents owned by user other than logged in user
+        WHEN:
+            - set_permissions bulk edit API endpoint is called
+        THEN:
+            - User is not able to change permissions
+        """
+        m.return_value = "OK"
+        self.doc1.owner = User.objects.get(username="temp_admin")
+        self.doc1.save()
+        user1 = User.objects.create(username="user1")
+        self.client.force_authenticate(user=user1)
+
+        permissions = {
+            "owner": user1.id,
+        }
+
+        response = self.client.post(
+            "/api/documents/bulk_edit/",
+            json.dumps(
+                {
+                    "documents": [self.doc1.id, self.doc2.id, self.doc3.id],
+                    "method": "set_permissions",
+                    "parameters": {"set_permissions": permissions},
+                },
+            ),
+            content_type="application/json",
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+        m.assert_not_called()
+        self.assertEqual(response.content, b"Insufficient permissions")
+
+        response = self.client.post(
+            "/api/documents/bulk_edit/",
+            json.dumps(
+                {
+                    "documents": [self.doc2.id, self.doc3.id],
+                    "method": "set_permissions",
+                    "parameters": {"set_permissions": permissions},
+                },
+            ),
+            content_type="application/json",
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        m.assert_called_once()
+
+    @mock.patch("documents.serialisers.bulk_edit.set_storage_path")
+    def test_insufficient_permissions_edit(self, m):
+        """
+        GIVEN:
+            - Documents for which current user only has view permissions
+        WHEN:
+            - API is called
+        THEN:
+            - set_storage_path is only called if user can edit all docs
+        """
+        m.return_value = "OK"
+        self.doc1.owner = User.objects.get(username="temp_admin")
+        self.doc1.save()
+        user1 = User.objects.create(username="user1")
+        assign_perm("view_document", user1, self.doc1)
+        self.client.force_authenticate(user=user1)
+
+        response = self.client.post(
+            "/api/documents/bulk_edit/",
+            json.dumps(
+                {
+                    "documents": [self.doc1.id, self.doc2.id, self.doc3.id],
+                    "method": "set_storage_path",
+                    "parameters": {"storage_path": self.sp1.id},
+                },
+            ),
+            content_type="application/json",
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+        m.assert_not_called()
+        self.assertEqual(response.content, b"Insufficient permissions")
+
+        assign_perm("change_document", user1, self.doc1)
+
+        response = self.client.post(
+            "/api/documents/bulk_edit/",
+            json.dumps(
+                {
+                    "documents": [self.doc1.id, self.doc2.id, self.doc3.id],
+                    "method": "set_storage_path",
+                    "parameters": {"storage_path": self.sp1.id},
+                },
+            ),
+            content_type="application/json",
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+        m.assert_called_once()
+
 
 class TestBulkDownload(DirectoriesMixin, APITestCase):
     ENDPOINT = "/api/documents/bulk_download/"
diff --git a/src/documents/views.py b/src/documents/views.py
index d57ad4eea..b04b87243 100644
--- a/src/documents/views.py
+++ b/src/documents/views.py
@@ -54,6 +54,7 @@ from rest_framework.viewsets import ModelViewSet
 from rest_framework.viewsets import ReadOnlyModelViewSet
 from rest_framework.viewsets import ViewSet
 
+from documents import bulk_edit
 from documents.filters import ObjectOwnedOrGrantedPermissionsFilter
 from documents.permissions import PaperlessAdminPermissions
 from documents.permissions import PaperlessObjectPermissions
@@ -694,7 +695,7 @@ class SavedViewViewSet(ModelViewSet, PassUserMixin):
         serializer.save(owner=self.request.user)
 
 
-class BulkEditView(GenericAPIView):
+class BulkEditView(GenericAPIView, PassUserMixin):
     permission_classes = (IsAuthenticated,)
     serializer_class = BulkEditSerializer
     parser_classes = (parsers.JSONParser,)
@@ -703,10 +704,25 @@ class BulkEditView(GenericAPIView):
         serializer = self.get_serializer(data=request.data)
         serializer.is_valid(raise_exception=True)
 
+        user = self.request.user
         method = serializer.validated_data.get("method")
         parameters = serializer.validated_data.get("parameters")
         documents = serializer.validated_data.get("documents")
 
+        if not user.is_superuser:
+            document_objs = Document.objects.filter(pk__in=documents)
+            has_perms = (
+                all((doc.owner == user or doc.owner is None) for doc in document_objs)
+                if method == bulk_edit.set_permissions
+                else all(
+                    has_perms_owner_aware(user, "change_document", doc)
+                    for doc in document_objs
+                )
+            )
+
+            if not has_perms:
+                return HttpResponseForbidden("Insufficient permissions")
+
         try:
             # TODO: parameter validation
             result = method(documents, **parameters)
diff --git a/src/paperless/checks.py b/src/paperless/checks.py
index cda14baad..d3009d036 100644
--- a/src/paperless/checks.py
+++ b/src/paperless/checks.py
@@ -177,6 +177,23 @@ def settings_values_check(app_configs, **kwargs):
             )
         return msgs
 
+    def _email_certificate_validate():
+        msgs = []
+        # Existence checks
+        if (
+            settings.EMAIL_CERTIFICATE_FILE is not None
+            and not settings.EMAIL_CERTIFICATE_FILE.is_file()
+        ):
+            msgs.append(
+                Error(
+                    f"Email cert {settings.EMAIL_CERTIFICATE_FILE} is not a file",
+                ),
+            )
+        return msgs
+
     return (
-        _ocrmypdf_settings_check() + _timezone_validate() + _barcode_scanner_validate()
+        _ocrmypdf_settings_check()
+        + _timezone_validate()
+        + _barcode_scanner_validate()
+        + _email_certificate_validate()
     )
diff --git a/src/paperless/settings.py b/src/paperless/settings.py
index b33d7fb7d..6b2ea56b2 100644
--- a/src/paperless/settings.py
+++ b/src/paperless/settings.py
@@ -67,11 +67,20 @@ def __get_float(key: str, default: float) -> float:
     return float(os.getenv(key, default))
 
 
-def __get_path(key: str, default: Union[PathLike, str]) -> Path:
+def __get_path(
+    key: str,
+    default: Optional[Union[PathLike, str]] = None,
+) -> Optional[Path]:
     """
-    Return a normalized, absolute path based on the environment variable or a default
+    Return a normalized, absolute path based on the environment variable or a default,
+    if provided.  If not set and no default, returns None
     """
-    return Path(os.environ.get(key, default)).resolve()
+    if key in os.environ:
+        return Path(os.environ[key]).resolve()
+    elif default is not None:
+        return Path(default).resolve()
+    else:
+        return None
 
 
 def __get_list(
@@ -364,6 +373,7 @@ CHANNEL_LAYERS = {
             "hosts": [_CHANNELS_REDIS_URL],
             "capacity": 2000,  # default 100
             "expiry": 15,  # default 60
+            "prefix": os.getenv("PAPERLESS_REDIS_PREFIX", ""),
         },
     },
 }
@@ -476,6 +486,8 @@ CSRF_COOKIE_NAME = f"{COOKIE_PREFIX}csrftoken"
 SESSION_COOKIE_NAME = f"{COOKIE_PREFIX}sessionid"
 LANGUAGE_COOKIE_NAME = f"{COOKIE_PREFIX}django_language"
 
+EMAIL_CERTIFICATE_FILE = __get_path("PAPERLESS_EMAIL_CERTIFICATE_FILE")
+
 
 ###############################################################################
 # Database                                                                    #
@@ -679,6 +691,9 @@ CELERY_TASK_SEND_SENT_EVENT = True
 CELERY_SEND_TASK_SENT_EVENT = True
 CELERY_BROKER_CONNECTION_RETRY = True
 CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True
+CELERY_BROKER_TRANSPORT_OPTIONS = {
+    "global_keyprefix": os.getenv("PAPERLESS_REDIS_PREFIX", ""),
+}
 
 CELERY_TASK_TRACK_STARTED = True
 CELERY_TASK_TIME_LIMIT: Final[int] = __get_int("PAPERLESS_WORKER_TIMEOUT", 1800)
diff --git a/src/paperless/tests/test_checks.py b/src/paperless/tests/test_checks.py
index cd706c532..6aac1a4c6 100644
--- a/src/paperless/tests/test_checks.py
+++ b/src/paperless/tests/test_checks.py
@@ -1,9 +1,11 @@
 import os
+from pathlib import Path
 
 from django.test import TestCase
 from django.test import override_settings
 
 from documents.tests.utils import DirectoriesMixin
+from documents.tests.utils import FileSystemAssertsMixin
 from paperless.checks import binaries_check
 from paperless.checks import debug_mode_check
 from paperless.checks import paths_check
@@ -57,7 +59,7 @@ class TestChecks(DirectoriesMixin, TestCase):
         self.assertEqual(len(debug_mode_check(None)), 1)
 
 
-class TestSettingsChecks(DirectoriesMixin, TestCase):
+class TestSettingsChecksAgainstDefaults(DirectoriesMixin, TestCase):
     def test_all_valid(self):
         """
         GIVEN:
@@ -70,6 +72,8 @@ class TestSettingsChecks(DirectoriesMixin, TestCase):
         msgs = settings_values_check(None)
         self.assertEqual(len(msgs), 0)
 
+
+class TestOcrSettingsChecks(DirectoriesMixin, TestCase):
     @override_settings(OCR_OUTPUT_TYPE="notapdf")
     def test_invalid_output_type(self):
         """
@@ -160,6 +164,8 @@ class TestSettingsChecks(DirectoriesMixin, TestCase):
 
         self.assertIn('OCR clean mode "cleanme"', msg.msg)
 
+
+class TestTimezoneSettingsChecks(DirectoriesMixin, TestCase):
     @override_settings(TIME_ZONE="TheMoon\\MyCrater")
     def test_invalid_timezone(self):
         """
@@ -178,6 +184,8 @@ class TestSettingsChecks(DirectoriesMixin, TestCase):
 
         self.assertIn('Timezone "TheMoon\\MyCrater"', msg.msg)
 
+
+class TestBarcodeSettingsChecks(DirectoriesMixin, TestCase):
     @override_settings(CONSUMER_BARCODE_SCANNER="Invalid")
     def test_barcode_scanner_invalid(self):
         msgs = settings_values_check(None)
@@ -200,3 +208,26 @@ class TestSettingsChecks(DirectoriesMixin, TestCase):
     def test_barcode_scanner_valid(self):
         msgs = settings_values_check(None)
         self.assertEqual(len(msgs), 0)
+
+
+class TestEmailCertSettingsChecks(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
+    @override_settings(EMAIL_CERTIFICATE_FILE=Path("/tmp/not_actually_here.pem"))
+    def test_not_valid_file(self):
+        """
+        GIVEN:
+            - Default settings
+            - Email certificate is set
+        WHEN:
+            - Email certificate file doesn't exist
+        THEN:
+            - system check error reported for email certificate
+        """
+        self.assertIsNotFile("/tmp/not_actually_here.pem")
+
+        msgs = settings_values_check(None)
+
+        self.assertEqual(len(msgs), 1)
+
+        msg = msgs[0]
+
+        self.assertIn("Email cert /tmp/not_actually_here.pem is not a file", msg.msg)
diff --git a/src/paperless_mail/mail.py b/src/paperless_mail/mail.py
index a0bda19ba..8b41ebacf 100644
--- a/src/paperless_mail/mail.py
+++ b/src/paperless_mail/mail.py
@@ -395,12 +395,16 @@ def get_mailbox(server, port, security) -> MailBox:
     """
     Returns the correct MailBox instance for the given configuration.
     """
+    ssl_context = ssl.create_default_context()
+    if settings.EMAIL_CERTIFICATE_FILE is not None:  # pragma: nocover
+        ssl_context.load_verify_locations(cafile=settings.EMAIL_CERTIFICATE_FILE)
+
     if security == MailAccount.ImapSecurity.NONE:
         mailbox = MailBoxUnencrypted(server, port)
     elif security == MailAccount.ImapSecurity.STARTTLS:
-        mailbox = MailBoxTls(server, port, ssl_context=ssl.create_default_context())
+        mailbox = MailBoxTls(server, port, ssl_context=ssl_context)
     elif security == MailAccount.ImapSecurity.SSL:
-        mailbox = MailBox(server, port, ssl_context=ssl.create_default_context())
+        mailbox = MailBox(server, port, ssl_context=ssl_context)
     else:
         raise NotImplementedError("Unknown IMAP security")  # pragma: nocover
     return mailbox
diff --git a/src/paperless_mail/parsers.py b/src/paperless_mail/parsers.py
index 4365d21a4..da9259a69 100644
--- a/src/paperless_mail/parsers.py
+++ b/src/paperless_mail/parsers.py
@@ -215,7 +215,11 @@ class MailDocumentParser(DocumentParser):
                         file_multi_part[2],
                     )
 
-                response = httpx.post(url_merge, files=pdf_collection, timeout=30.0)
+                response = httpx.post(
+                    url_merge,
+                    files=pdf_collection,
+                    timeout=settings.CELERY_TASK_TIME_LIMIT,
+                )
                 response.raise_for_status()  # ensure we notice bad responses
 
                 archive_path.write_bytes(response.content)
@@ -330,7 +334,7 @@ class MailDocumentParser(DocumentParser):
                     files=files,
                     headers=headers,
                     data=data,
-                    timeout=30.0,
+                    timeout=settings.CELERY_TASK_TIME_LIMIT,
                 )
                 response.raise_for_status()  # ensure we notice bad responses
             except Exception as err:
@@ -409,7 +413,12 @@ class MailDocumentParser(DocumentParser):
                     file_multi_part[2],
                 )
 
-            response = httpx.post(url, files=files, data=data, timeout=30.0)
+            response = httpx.post(
+                url,
+                files=files,
+                data=data,
+                timeout=settings.CELERY_TASK_TIME_LIMIT,
+            )
             response.raise_for_status()  # ensure we notice bad responses
         except Exception as err:
             raise ParseError(f"Error while converting document to PDF: {err}") from err
diff --git a/src/paperless_tika/parsers.py b/src/paperless_tika/parsers.py
index b6a9dd621..402a37215 100644
--- a/src/paperless_tika/parsers.py
+++ b/src/paperless_tika/parsers.py
@@ -100,7 +100,7 @@ class TikaDocumentParser(DocumentParser):
                     files=files,
                     headers=headers,
                     data=data,
-                    timeout=30.0,
+                    timeout=settings.CELERY_TASK_TIME_LIMIT,
                 )
                 response.raise_for_status()  # ensure we notice bad responses
             except Exception as err: