diff --git a/e2e/cypress/integration/compose.ts b/e2e/cypress/integration/compose.ts
index 4b048ad97..af1a1372b 100644
--- a/e2e/cypress/integration/compose.ts
+++ b/e2e/cypress/integration/compose.ts
@@ -95,7 +95,7 @@ describe('Composing emails', () => {
         cy.visit('/compose');
         cy.wait('@listAllmessages', {'timeout':10000});
         cy.visit('/compose?new=true');
-        
+
         cy.get('button[mattooltip="Close draft"').click();
         cy.location().should((loc) => {
             expect(loc.pathname).to.eq('/compose');
@@ -141,7 +141,7 @@ describe('Composing emails', () => {
         cy.location().should((loc) => {
             expect(loc.pathname).to.eq('/contacts/id-mr-postcode');
             expect(loc.search).to.eq('');
-        }); 
+        });
     });
 
     it('should find the same address in original "To" and our "From" field in Reply', () => {
@@ -152,4 +152,16 @@ describe('Composing emails', () => {
         cy.get('button[mattooltip="Reply"]').click();
         cy.get('.mat-select-value-text span').contains(address, { matchCase: false });
     });
+
+    it('should show a save template button and save on click', () => {
+        cy.visit('/compose?new=true');
+        cy.get('input[data-placeholder="Subject"]').type('Template subject here');
+        cy.get('button[mattooltip="Save as template"').click();
+        cy.location().should((loc) => {
+            expect(loc.pathname).to.eq('/compose');
+            expect(loc.search).to.eq('?new=true');
+        });
+
+        cy.get('snack-bar-container').should('contain', 'Saved to templates');
+    })
 });
diff --git a/e2e/cypress/integration/folders.ts b/e2e/cypress/integration/folders.ts
index 36f059be4..269270e01 100644
--- a/e2e/cypress/integration/folders.ts
+++ b/e2e/cypress/integration/folders.ts
@@ -1,6 +1,10 @@
 /// <reference types="cypress" />
 
 describe('Folder management', () => {
+    function canvas() {
+        return cy.get('canvastable canvas:first-of-type');
+    }
+
     it('should create folder at root level', () => {
         cy.visit('/');
 
@@ -26,4 +30,13 @@ describe('Folder management', () => {
         cy.contains('div.mat-menu-content button', 'Empty trash').click();
         cy.wait('@emptyTrashReq');
     });
+
+    it('should create new draft on templates folder message click', () => {
+	cy.visit('/')
+	cy.contains('mat-tree-node', 'Templates').click()
+        canvas().click({ x: 55, y: 40 });
+        cy.location().should((loc) => {
+            expect(loc.pathname).to.eq('/compose');
+        });
+    })
 });
diff --git a/e2e/mockserver/mockserver.ts b/e2e/mockserver/mockserver.ts
index 0905fe94e..5df643199 100644
--- a/e2e/mockserver/mockserver.ts
+++ b/e2e/mockserver/mockserver.ts
@@ -1,18 +1,18 @@
 // --------- BEGIN RUNBOX LICENSE ---------
 // Copyright (C) 2016-2018 Runbox Solutions AS (runbox.com).
-// 
+//
 // This file is part of Runbox 7.
-// 
+//
 // Runbox 7 is free software: You can redistribute it and/or modify it
 // under the terms of the GNU General Public License as published by the
 // Free Software Foundation, either version 3 of the License, or (at your
 // option) any later version.
-// 
+//
 // Runbox 7 is distributed in the hope that it will be useful, but
 // WITHOUT ANY WARRANTY; without even the implied warranty of
 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 // General Public License for more details.
-// 
+//
 // You should have received a copy of the GNU General Public License
 // along with Runbox 7. If not, see <https://www.gnu.org/licenses/>.
 // ---------- END RUNBOX LICENSE ----------
@@ -105,6 +105,22 @@ export class MockServer {
             'subfolders': [],
             'type': 'trash'
         },
+	{
+	    'id': '2',
+	    'total': 1,
+	    'size': '344',
+	    'name': 'Templates',
+	    'new': 0,
+	    'folder': 'Templates',
+	    'type': 'templates',
+	    'old': 296,
+	    'msg_total': 1,
+	    'priority': '4',
+	    'subfolders': [],
+	    'msg_new': 0,
+	    'msg_size': '344',
+	    'parent': null
+	},
     ];
 
     vtimezone_oslo =
@@ -355,7 +371,7 @@ END:VCALENDAR
                     response.end(JSON.stringify(this.emailFoldersListResponse()));
                     break;
                 case '/mail/download_xapian_index':
-                    response.end('');
+                    response.end(this.templatescontents());
                     break;
                 case '/mail/download_xapian_index?inbox':
                     response.end(this.inboxcontents());
@@ -413,6 +429,15 @@ END:VCALENDAR
         return trashlines.join('\n');
     }
 
+    templatescontents() {
+        const lines = [];
+        for (let msg_id = 1000; msg_id < 10000; msg_id++) {
+            lines.push(`${msg_id}	1548071429	1547830043	Templates	1	0	0	"Template" <template@runbox.com>	` +
+                `Template2<template@example.org>	TEMPLATE	709	n	 `);
+        }
+        return lines.join('\n');
+    }
+
     inboxcontents() {
         const inboxlines = [];
         for (let msg_id = 1; msg_id < 10; msg_id++) {
@@ -844,7 +869,7 @@ END:VCALENDAR
 
     profiles_verified() {
         return {
-                'results': 
+                'results':
                     [{
                         'smtp_username': null,
                         'email': 'a2@example.com',
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 99c9ddf12..7473ac6f6 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -1,18 +1,18 @@
 // --------- BEGIN RUNBOX LICENSE ---------
 // Copyright (C) 2016-2022 Runbox Solutions AS (runbox.com).
-// 
+//
 // This file is part of Runbox 7.
-// 
+//
 // Runbox 7 is free software: You can redistribute it and/or modify it
 // under the terms of the GNU General Public License as published by the
 // Free Software Foundation, either version 3 of the License, or (at your
 // option) any later version.
-// 
+//
 // Runbox 7 is distributed in the hope that it will be useful, but
 // WITHOUT ANY WARRANTY; without even the implied warranty of
 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 // General Public License for more details.
-// 
+//
 // You should have received a copy of the GNU General Public License
 // along with Runbox 7. If not, see <https://www.gnu.org/licenses/>.
 // ---------- END RUNBOX LICENSE ----------
@@ -74,7 +74,7 @@ const LOCAL_STORAGE_SHOW_UNREAD_ONLY = 'rmm7mailViewerShowUnreadOnly';
 const LOCAL_STORAGE_SHOW_POPULAR_RECIPIENTS = 'showPopularRecipients';
 const LOCAL_STORAGE_INDEX_PROMPT = 'localSearchPromptDisplayed';
 const TOOLBAR_LIST_BUTTON_WIDTH = 30;
-    
+
 @Component({
   moduleId: 'angular2/app/',
   // eslint-disable-next-line @angular-eslint/component-selector
@@ -718,7 +718,7 @@ export class AppComponent implements OnInit, AfterViewInit, CanvasTableSelectLis
       ) - 1;
     }
   }
-    
+
 
   public openMarkOpMenu() {
     this.showSelectMarkOpMenu = true;
@@ -916,6 +916,17 @@ export class AppComponent implements OnInit, AfterViewInit, CanvasTableSelectLis
   }
 
   public rowSelected(rowIndex: number, columnIndex: number, multiSelect?: boolean) {
+    const isSelect = (columnIndex === 0) || multiSelect
+
+    if ((this.selectedFolder === this.messagelistservice.templateFolderName) && !isSelect) {
+      this.draftDeskService.newTemplateDraft(
+        this.canvastable.rows.getRowMessageId(rowIndex)
+      );
+      this.drafts();
+
+      return;
+    }
+
     this.canvastable.rows.rowSelected(rowIndex, columnIndex, multiSelect);
     this.showSelectOperations = this.canvastable.rows.anySelected();
 
diff --git a/src/app/compose/compose.component.html b/src/app/compose/compose.component.html
index a4ad062fc..de7825bb1 100644
--- a/src/app/compose/compose.component.html
+++ b/src/app/compose/compose.component.html
@@ -24,13 +24,16 @@
             </button>
             <button *ngIf="!editing" mat-icon-button (click)="editDraft()" matTooltip="Edit draft" id="editDraftIcon">
                 <mat-icon svgIcon="pencil"></mat-icon>
-            </button>                 
+            </button>
+            <button *ngIf="editing" mat-icon-button matTooltip="Save as template" (click)="saveTemplate(true)" id="saveTemplate">
+                <mat-icon svgIcon="file-document"></mat-icon>
+            </button>
             <button *ngIf="editing" mat-icon-button matTooltip="Send mail" (click)="submit(true)" id="sendMail">
                 <mat-icon svgIcon="send"></mat-icon>
             </button>
         </div>
-    </mat-card-actions>        
-    <mat-card-subtitle>                
+    </mat-card-actions>
+    <mat-card-subtitle>
         <mat-form-field floatPlaceholder="always" *ngIf="editing" style="width: 100%" id="fieldFrom">
             <mat-select placeholder="From" formControlName="from">
                 <mat-option *ngFor="let from of draftDeskservice.fromsSubject.value" [value]="from.nameAndAddress">
@@ -42,7 +45,7 @@
             <span *ngFor="let t of model.to">
                 {{t.nameAndAddress}}
             </span>
-        </span>        
+        </span>
         <div class="fieldRecipient">
             <mailrecipient-input *ngIf="editing" style="flex-grow: 1"
                 [initialfocus]="model.to.length === 0"
@@ -86,7 +89,7 @@
                 [recipients]="model.cc"
                 (updateRecipient)="onUpdateRecipient('cc', $event)"
             ></mailrecipient-input>
-            <button mat-icon-button (click)="formGroup.controls.cc.setValue(null)"><mat-icon svgIcon="close"></mat-icon></button>              
+            <button mat-icon-button (click)="formGroup.controls.cc.setValue(null)"><mat-icon svgIcon="close"></mat-icon></button>
         </div>
         <div style="display: flex;" *ngIf="editing && hasBCC" class="fieldRecipient" (drop)="recipientDropped($event, 'bcc')">
             <mailrecipient-input *ngIf="editing" style="width: auto; flex-grow: 1"
@@ -96,11 +99,11 @@
             ></mailrecipient-input>
             <button mat-icon-button (click)="formGroup.controls.bcc.setValue(null)"><mat-icon svgIcon="close"></mat-icon></button>
         </div>
-    </mat-card-subtitle>    
-    <mat-card-content>       
+    </mat-card-subtitle>
+    <mat-card-content>
         <mat-form-field *ngIf="editing" floatPlaceholder="auto" id="fieldSubject">
             <input matInput placeholder="Subject" name="subject" formControlName="subject" />
-        </mat-form-field>                     
+        </mat-form-field>
         <section *ngIf="editing" [ngClass]="{'dropzonehighlight': showDropZone, 'overdropzone': draggingOverDropZone}" (dragover)="draggingOverDropZone=true" (dragleave)="draggingOverDropZone=false" (drop)="dropFiles($event)" id="dropZone">
           <h1 *ngIf="showDropZone" id="dropZoneText">Drop files here</h1>
           <ng-container *ngIf="uploadProgress | async as uprogress">
@@ -127,11 +130,11 @@ <h1 *ngIf="showDropZone" id="dropZoneText">Drop files here</h1>
               <button mat-icon-button id="deleteAttachment" (click)="removeAttachment(i)"><mat-icon svgIcon="delete"></mat-icon></button>
             </ng-container>
           </div>
-        </section>      
+        </section>
         <span [hidden]="editing">
             {{this.model.preview}}
         </span>
-            
+
         <div [hidden]="!editing">
             <div class="draft-buttons">
                 <button *ngIf="editing" mat-icon-button matTooltip="Attach files" id="attachMobile" (click)="attachFilesClicked()">
@@ -142,21 +145,21 @@ <h1 *ngIf="showDropZone" id="dropZoneText">Drop files here</h1>
           <textarea style="
 			   width: 100%;
 			   height: 300px;
-			   " 
+			   "
                     [id]="selector"
                     [hidden]="!formGroup.value.useHTML">
           </textarea>
           <mat-form-field id="messageTextArea" *ngIf="!formGroup.value.useHTML" floatPlaceholder="auto">
-            <textarea 
+            <textarea
                #messageTextArea
-               placeholder="Message text" 
-               matInput                                      
+               placeholder="Message text"
+               matInput
                formControlName="msg_body"
                rows="20"
                >
             </textarea>
-          </mat-form-field>   
-        </div>          
+          </mat-form-field>
+        </div>
     </mat-card-content>
 
 </mat-card>
diff --git a/src/app/compose/compose.component.ts b/src/app/compose/compose.component.ts
index c0a5d4c94..1e46cefe0 100644
--- a/src/app/compose/compose.component.ts
+++ b/src/app/compose/compose.component.ts
@@ -1,18 +1,18 @@
 // --------- BEGIN RUNBOX LICENSE ---------
 // Copyright (C) 2016-2022 Runbox Solutions AS (runbox.com).
-// 
+//
 // This file is part of Runbox 7.
-// 
+//
 // Runbox 7 is free software: You can redistribute it and/or modify it
 // under the terms of the GNU General Public License as published by the
 // Free Software Foundation, either version 3 of the License, or (at your
 // option) any later version.
-// 
+//
 // Runbox 7 is distributed in the hope that it will be useful, but
 // WITHOUT ANY WARRANTY; without even the implied warranty of
 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 // General Public License for more details.
-// 
+//
 // You should have received a copy of the GNU General Public License
 // along with Runbox 7. If not, see <https://www.gnu.org/licenses/>.
 // ---------- END RUNBOX LICENSE ----------
@@ -77,6 +77,7 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit {
     public uploadRequest: Subscription = null;
     public saved: Date = null;
     public tinymce_plugin: TinyMCEPlugin;
+    public isTemplate: boolean = false;
     finishImageUpload: AsyncSubject<any> = null;
     uploadProgress: BehaviorSubject<number> = new BehaviorSubject(-1);
     has_pasted_signature: boolean;
@@ -207,7 +208,15 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit {
                     'replying',
                 ]);
             }))
-            .subscribe(() => this.submit(false));
+            .subscribe(() => {
+                // Disable auto-save when in template edit mode. This prevents
+                // creating a draft while the user might be editing the template.
+                // The side-effect is that a draft created from a template won't
+                // have autosave until it was saved as a draft.
+                if (this.model.tid) return;
+
+                this.submit(false)
+            });
 
         this.formGroup.controls.from.valueChanges
             .pipe(debounceTime(1000))
@@ -460,6 +469,11 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit {
     }
 
     public loadDraft(msgObj) {
+        if (msgObj.errors) {
+            this.snackBar.open(msgObj.errors[0], 'Ok')
+            throw msgObj
+        }
+
         const model = new DraftFormModel();
         model.mid = typeof msgObj.mid === 'string' ? parseInt(msgObj.mid, 10) : msgObj.mid;
         this.draftDeskservice.isEditing = model.mid;
@@ -673,10 +687,14 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit {
     }
 
     public submit(send: boolean = false) {
+        const isTemplate = Boolean(this.isTemplate ?? this.model.tid);
+
         if (this.savingInProgress) {
             return;
         }
+
         this.savingInProgress = true;
+
         if (send) {
             let validemails = false;
             validemails = isValidEmailArray(this.model.to);
@@ -780,7 +798,14 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit {
         } else {
             this.rmmapi.me.pipe(mergeMap((me) => {
                 return this.http.post('/rest/v1/draft', {
-                    type: 'draft',
+                    ...(isTemplate ? {
+                        type: 'template',
+                        mid: this.model.tid ?? this.model.mid,
+                        tid: this.model.tid ?? this.model.mid,
+                    } : {
+                        type: 'draft',
+                        mid: this.model.mid
+                    }),
                     username: me.username,
                     from: from && from.id ? from.from_name + '%' + from.email + '%' + from.id : from ? from.email : undefined,
                     from_email: from ? from.email : '',
@@ -795,7 +820,6 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit {
                     tags: [],
                     ctype: this.model.useHTML ? 'html' : null,
                     save: send ? 'Send' : 'Save',
-                    mid: this.model.mid,
                     attachments: this.model.attachments ?
                         this.model.attachments
                             .filter((att) => att.file !== 'UTF-8Q')
@@ -805,6 +829,10 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit {
                 });
             }
             )).subscribe((res: any) => {
+                if (this.isTemplate)  {
+                    this.snackBar.open('Saved to templates', 'Ok', { duration: 3000 });
+                }
+
                 if (res.mid) {
                     const newMid = typeof res.mid === 'string' ? parseInt(res.mid, 10) : res.mid;
                     if (this.model.isUnsaved()) {
@@ -839,6 +867,11 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit {
         }
     }
 
+    public saveTemplate() {
+        this.isTemplate = true
+        this.submit(false);
+    }
+
     ngOnDestroy() {
         if (this.editor) {
             this.tinymce_plugin.remove(this.editor);
diff --git a/src/app/compose/draftdesk.service.ts b/src/app/compose/draftdesk.service.ts
index d0bcede21..c841094d7 100644
--- a/src/app/compose/draftdesk.service.ts
+++ b/src/app/compose/draftdesk.service.ts
@@ -1,18 +1,18 @@
 // --------- BEGIN RUNBOX LICENSE ---------
 // Copyright (C) 2016-2022 Runbox Solutions AS (runbox.com).
-// 
+//
 // This file is part of Runbox 7.
-// 
+//
 // Runbox 7 is free software: You can redistribute it and/or modify it
 // under the terms of the GNU General Public License as published by the
 // Free Software Foundation, either version 3 of the License, or (at your
 // option) any later version.
-// 
+//
 // Runbox 7 is distributed in the hope that it will be useful, but
 // WITHOUT ANY WARRANTY; without even the implied warranty of
 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 // General Public License for more details.
-// 
+//
 // You should have received a copy of the GNU General Public License
 // along with Runbox 7. If not, see <https://www.gnu.org/licenses/>.
 // ---------- END RUNBOX LICENSE ----------
@@ -49,6 +49,7 @@ export class DraftFormModel {
 
     from: string = null;
     mid: number = (DraftFormModel.newDraftCount--);
+    tid: number = null;
     to: MailAddressInfo[] = [];
     cc: MailAddressInfo[] = [];
     bcc: MailAddressInfo[] = [];
@@ -131,7 +132,7 @@ export class DraftFormModel {
         const localTZ = moment.tz.guess();
         const replyHeaderHTML = 'On '
             + moment(mailObj.date, localTZ).format('yyyy-MM-DD HH:mm Z')
-            + ' ' + moment.tz(localTZ).format('z') 
+            + ' ' + moment.tz(localTZ).format('z')
             + ', '
             + (mailObj.from[0].name
                 ? `"${mailObj.from[0].name}" &lt;${mailObj.from[0].address}&gt; wrote:`
@@ -157,7 +158,7 @@ export class DraftFormModel {
     }
 
     public static trimmedPreview(preview: string): string {
-        let ret = preview.substring(0, DraftFormModel.MAX_DRAFT_PREVIEW_LENGTH);
+        let ret = (preview ?? '').substring(0, DraftFormModel.MAX_DRAFT_PREVIEW_LENGTH);
         if (ret.length === DraftFormModel.MAX_DRAFT_PREVIEW_LENGTH) {
             ret += '...';
         }
@@ -309,6 +310,34 @@ export class DraftDeskService {
         this.draftModels.next(models);
     }
 
+    public async newTemplateDraft(
+        messageId: number,
+    ) {
+
+        this.rmmapi.getMessageContents(messageId).subscribe((contents) => {
+            const res: any = Object.assign({}, contents);
+            const {subject} = res.headers
+            let { to } = res.headers
+
+            if (to) {
+                to = new MailAddressInfo(to.value.name, to.value.address).nameAndAddress;
+            }
+
+            const draftFormModel = DraftFormModel.create(
+                -1,
+                this.mainIdentity(),
+                to,
+                subject
+            )
+
+            draftFormModel.tid = messageId;
+            draftFormModel.msg_body = contents.text.text;
+            draftFormModel.html = contents.text.html;
+
+            return this.newDraft(draftFormModel);
+        })
+    }
+
     public async newBugReport(
         local_search: boolean,
         keep_pane: boolean,
diff --git a/src/app/rmmapi/messagelist.service.ts b/src/app/rmmapi/messagelist.service.ts
index 75f8f589a..8a29406ce 100644
--- a/src/app/rmmapi/messagelist.service.ts
+++ b/src/app/rmmapi/messagelist.service.ts
@@ -62,6 +62,7 @@ export class MessageListService {
 
     trashFolderName = 'Trash';
     spamFolderName = 'Spam';
+    templateFolderName = 'Templates';
 
     ignoreUnreadInFolders = [ 'Sent' ];