mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Merge pull request #2000 from paperless-ngx/feature-frontend-paperless-mail
Feature: frontend paperless mail
This commit is contained in:
		| @@ -35,6 +35,16 @@ describe('settings', () => { | |||||||
|             req.reply(response) |             req.reply(response) | ||||||
|           } |           } | ||||||
|         ).as('savedViews') |         ).as('savedViews') | ||||||
|  |  | ||||||
|  |         cy.intercept('http://localhost:8000/api/mail_accounts/*', { | ||||||
|  |           fixture: 'mail_accounts/mail_accounts.json', | ||||||
|  |         }) | ||||||
|  |         cy.intercept('http://localhost:8000/api/mail_rules/*', { | ||||||
|  |           fixture: 'mail_rules/mail_rules.json', | ||||||
|  |         }).as('mailRules') | ||||||
|  |         cy.intercept('http://localhost:8000/api/tasks/', { | ||||||
|  |           fixture: 'tasks/tasks.json', | ||||||
|  |         }) | ||||||
|       }) |       }) | ||||||
|  |  | ||||||
|       cy.fixture('documents/documents.json').then((documentsJson) => { |       cy.fixture('documents/documents.json').then((documentsJson) => { | ||||||
| @@ -48,7 +58,6 @@ describe('settings', () => { | |||||||
|  |  | ||||||
|     cy.viewport(1024, 1600) |     cy.viewport(1024, 1600) | ||||||
|     cy.visit('/settings') |     cy.visit('/settings') | ||||||
|     cy.wait('@savedViews') |  | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   it('should activate / deactivate save button when settings change and are saved', () => { |   it('should activate / deactivate save button when settings change and are saved', () => { | ||||||
| @@ -64,7 +73,7 @@ describe('settings', () => { | |||||||
|     cy.contains('a', 'Dashboard').click() |     cy.contains('a', 'Dashboard').click() | ||||||
|     cy.contains('You have unsaved changes') |     cy.contains('You have unsaved changes') | ||||||
|     cy.contains('button', 'Cancel').click() |     cy.contains('button', 'Cancel').click() | ||||||
|     cy.contains('button', 'Save').click().wait('@savedViews') |     cy.contains('button', 'Save').click().wait('@savedViews').wait(2000) | ||||||
|     cy.contains('a', 'Dashboard').click() |     cy.contains('a', 'Dashboard').click() | ||||||
|     cy.contains('You have unsaved changes').should('not.exist') |     cy.contains('You have unsaved changes').should('not.exist') | ||||||
|   }) |   }) | ||||||
| @@ -77,16 +86,16 @@ describe('settings', () => { | |||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   it('should remove saved view from sidebar when unset', () => { |   it('should remove saved view from sidebar when unset', () => { | ||||||
|     cy.contains('a', 'Saved views').click() |     cy.contains('a', 'Saved views').click().wait(2000) | ||||||
|     cy.get('#show_in_sidebar_1').click() |     cy.get('#show_in_sidebar_1').click() | ||||||
|     cy.contains('button', 'Save').click().wait('@savedViews') |     cy.contains('button', 'Save').click().wait('@savedViews').wait(2000) | ||||||
|     cy.contains('li', 'Inbox').should('not.exist') |     cy.contains('li', 'Inbox').should('not.exist') | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   it('should remove saved view from dashboard when unset', () => { |   it('should remove saved view from dashboard when unset', () => { | ||||||
|     cy.contains('a', 'Saved views').click() |     cy.contains('a', 'Saved views').click() | ||||||
|     cy.get('#show_on_dashboard_1').click() |     cy.get('#show_on_dashboard_1').click() | ||||||
|     cy.contains('button', 'Save').click().wait('@savedViews') |     cy.contains('button', 'Save').click().wait('@savedViews').wait(2000) | ||||||
|     cy.visit('/dashboard') |     cy.visit('/dashboard') | ||||||
|     cy.get('app-saved-view-widget').contains('Inbox').should('not.exist') |     cy.get('app-saved-view-widget').contains('Inbox').should('not.exist') | ||||||
|   }) |   }) | ||||||
|   | |||||||
							
								
								
									
										27
									
								
								src-ui/cypress/fixtures/mail_accounts/mail_accounts.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src-ui/cypress/fixtures/mail_accounts/mail_accounts.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | { | ||||||
|  |     "count": 2, | ||||||
|  |     "next": null, | ||||||
|  |     "previous": null, | ||||||
|  |     "results": [ | ||||||
|  |         { | ||||||
|  |             "id": 1, | ||||||
|  |             "name": "IMAP Server", | ||||||
|  |             "imap_server": "imap.example.com", | ||||||
|  |             "imap_port": 993, | ||||||
|  |             "imap_security": 2, | ||||||
|  |             "username": "inbox@example.com", | ||||||
|  |             "password": "pass", | ||||||
|  |             "character_set": "UTF-8" | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             "id": 2, | ||||||
|  |             "name": "Gmail", | ||||||
|  |             "imap_server": "imap.gmail.com", | ||||||
|  |             "imap_port": 993, | ||||||
|  |             "imap_security": 2, | ||||||
|  |             "username": "user@gmail.com", | ||||||
|  |             "password": "pass", | ||||||
|  |             "character_set": "UTF-8" | ||||||
|  |         } | ||||||
|  |     ] | ||||||
|  | } | ||||||
							
								
								
									
										29
									
								
								src-ui/cypress/fixtures/mail_rules/mail_rules.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src-ui/cypress/fixtures/mail_rules/mail_rules.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | { | ||||||
|  |     "count": 1, | ||||||
|  |     "next": null, | ||||||
|  |     "previous": null, | ||||||
|  |     "results": [ | ||||||
|  |         { | ||||||
|  |             "id": 1, | ||||||
|  |             "name": "Gmail", | ||||||
|  |             "account": 2, | ||||||
|  |             "folder": "INBOX", | ||||||
|  |             "filter_from": null, | ||||||
|  |             "filter_subject": "[paperless]", | ||||||
|  |             "filter_body": null, | ||||||
|  |             "filter_attachment_filename": null, | ||||||
|  |             "maximum_age": 30, | ||||||
|  |             "action": 3, | ||||||
|  |             "action_parameter": null, | ||||||
|  |             "assign_title_from": 1, | ||||||
|  |             "assign_tags": [ | ||||||
|  |                 9 | ||||||
|  |             ], | ||||||
|  |             "assign_correspondent_from": 1, | ||||||
|  |             "assign_correspondent": 2, | ||||||
|  |             "assign_document_type": null, | ||||||
|  |             "order": 0, | ||||||
|  |             "attachment_type": 2 | ||||||
|  |         } | ||||||
|  |     ] | ||||||
|  | } | ||||||
| @@ -1 +1,44 @@ | |||||||
| {"count":3,"next":null,"previous":null,"results":[{"id":1,"name":"Inbox","show_on_dashboard":true,"show_in_sidebar":true,"sort_field":"created","sort_reverse":true,"filter_rules":[{"rule_type":6,"value":"18"}]},{"id":2,"name":"Recently Added","show_on_dashboard":true,"show_in_sidebar":false,"sort_field":"created","sort_reverse":true,"filter_rules":[]},{"id":11,"name":"Taxes","show_on_dashboard":false,"show_in_sidebar":true,"sort_field":"created","sort_reverse":true,"filter_rules":[{"rule_type":6,"value":"39"}]}]} | { | ||||||
|  |     "count": 3, | ||||||
|  |     "next": null, | ||||||
|  |     "previous": null, | ||||||
|  |     "results": [ | ||||||
|  |         { | ||||||
|  |             "id": 1, | ||||||
|  |             "name": "Inbox", | ||||||
|  |             "show_on_dashboard": true, | ||||||
|  |             "show_in_sidebar": true, | ||||||
|  |             "sort_field": "created", | ||||||
|  |             "sort_reverse": true, | ||||||
|  |             "filter_rules": [ | ||||||
|  |                 { | ||||||
|  |                     "rule_type": 6, | ||||||
|  |                     "value": "18" | ||||||
|  |                 } | ||||||
|  |             ] | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             "id": 2, | ||||||
|  |             "name": "Recently Added", | ||||||
|  |             "show_on_dashboard": true, | ||||||
|  |             "show_in_sidebar": false, | ||||||
|  |             "sort_field": "created", | ||||||
|  |             "sort_reverse": true, | ||||||
|  |             "filter_rules": [] | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             "id": 11, | ||||||
|  |             "name": "Taxes", | ||||||
|  |             "show_on_dashboard": false, | ||||||
|  |             "show_in_sidebar": true, | ||||||
|  |             "sort_field": "created", | ||||||
|  |             "sort_reverse": true, | ||||||
|  |             "filter_rules": [ | ||||||
|  |                 { | ||||||
|  |                     "rule_type": 6, | ||||||
|  |                     "value": "39" | ||||||
|  |                 } | ||||||
|  |             ] | ||||||
|  |         } | ||||||
|  |     ] | ||||||
|  | } | ||||||
|   | |||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -47,6 +47,11 @@ const routes: Routes = [ | |||||||
|         component: SettingsComponent, |         component: SettingsComponent, | ||||||
|         canDeactivate: [DirtyFormGuard], |         canDeactivate: [DirtyFormGuard], | ||||||
|       }, |       }, | ||||||
|  |       { | ||||||
|  |         path: 'settings/:section', | ||||||
|  |         component: SettingsComponent, | ||||||
|  |         canDeactivate: [DirtyFormGuard], | ||||||
|  |       }, | ||||||
|       { path: 'tasks', component: TasksComponent }, |       { path: 'tasks', component: TasksComponent }, | ||||||
|     ], |     ], | ||||||
|   }, |   }, | ||||||
|   | |||||||
| @@ -191,21 +191,13 @@ export class AppComponent implements OnInit, OnDestroy { | |||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         anchorId: 'tour.settings', |         anchorId: 'tour.settings', | ||||||
|         content: $localize`Check out the settings for various tweaks to the web app or to toggle settings for saved views.`, |         content: $localize`Check out the settings for various tweaks to the web app, toggle settings for saved views or setup e-mail checking.`, | ||||||
|         route: '/settings', |         route: '/settings', | ||||||
|         enableBackdrop: true, |         enableBackdrop: true, | ||||||
|         prevBtnTitle, |         prevBtnTitle, | ||||||
|         nextBtnTitle, |         nextBtnTitle, | ||||||
|         endBtnTitle, |         endBtnTitle, | ||||||
|       }, |       }, | ||||||
|       { |  | ||||||
|         anchorId: 'tour.admin', |  | ||||||
|         content: $localize`The Admin area contains more advanced controls as well as the settings for automatic e-mail fetching.`, |  | ||||||
|         enableBackdrop: true, |  | ||||||
|         prevBtnTitle, |  | ||||||
|         nextBtnTitle, |  | ||||||
|         endBtnTitle, |  | ||||||
|       }, |  | ||||||
|       { |       { | ||||||
|         anchorId: 'tour.outro', |         anchorId: 'tour.outro', | ||||||
|         title: $localize`Thank you! 🙏`, |         title: $localize`Thank you! 🙏`, | ||||||
|   | |||||||
| @@ -39,6 +39,7 @@ import { NgxFileDropModule } from 'ngx-file-drop' | |||||||
| import { TextComponent } from './components/common/input/text/text.component' | import { TextComponent } from './components/common/input/text/text.component' | ||||||
| import { SelectComponent } from './components/common/input/select/select.component' | import { SelectComponent } from './components/common/input/select/select.component' | ||||||
| import { CheckComponent } from './components/common/input/check/check.component' | import { CheckComponent } from './components/common/input/check/check.component' | ||||||
|  | import { PasswordComponent } from './components/common/input/password/password.component' | ||||||
| import { SaveViewConfigDialogComponent } from './components/document-list/save-view-config-dialog/save-view-config-dialog.component' | import { SaveViewConfigDialogComponent } from './components/document-list/save-view-config-dialog/save-view-config-dialog.component' | ||||||
| import { TagsComponent } from './components/common/input/tags/tags.component' | import { TagsComponent } from './components/common/input/tags/tags.component' | ||||||
| import { SortableDirective } from './directives/sortable.directive' | import { SortableDirective } from './directives/sortable.directive' | ||||||
| @@ -76,6 +77,8 @@ import { StoragePathEditDialogComponent } from './components/common/edit-dialog/ | |||||||
| import { SettingsService } from './services/settings.service' | import { SettingsService } from './services/settings.service' | ||||||
| import { TasksComponent } from './components/manage/tasks/tasks.component' | import { TasksComponent } from './components/manage/tasks/tasks.component' | ||||||
| import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap' | import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap' | ||||||
|  | import { MailAccountEditDialogComponent } from './components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component' | ||||||
|  | import { MailRuleEditDialogComponent } from './components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component' | ||||||
|  |  | ||||||
| import localeBe from '@angular/common/locales/be' | import localeBe from '@angular/common/locales/be' | ||||||
| import localeCs from '@angular/common/locales/cs' | import localeCs from '@angular/common/locales/cs' | ||||||
| @@ -157,6 +160,7 @@ function initializeApp(settings: SettingsService) { | |||||||
|     TextComponent, |     TextComponent, | ||||||
|     SelectComponent, |     SelectComponent, | ||||||
|     CheckComponent, |     CheckComponent, | ||||||
|  |     PasswordComponent, | ||||||
|     SaveViewConfigDialogComponent, |     SaveViewConfigDialogComponent, | ||||||
|     TagsComponent, |     TagsComponent, | ||||||
|     SortableDirective, |     SortableDirective, | ||||||
| @@ -180,6 +184,8 @@ function initializeApp(settings: SettingsService) { | |||||||
|     DocumentAsnComponent, |     DocumentAsnComponent, | ||||||
|     DocumentCommentsComponent, |     DocumentCommentsComponent, | ||||||
|     TasksComponent, |     TasksComponent, | ||||||
|  |     MailAccountEditDialogComponent, | ||||||
|  |     MailRuleEditDialogComponent, | ||||||
|   ], |   ], | ||||||
|   imports: [ |   imports: [ | ||||||
|     BrowserModule, |     BrowserModule, | ||||||
|   | |||||||
| @@ -174,13 +174,6 @@ | |||||||
|               </svg><span> <ng-container i18n>Settings</ng-container></span> |               </svg><span> <ng-container i18n>Settings</ng-container></span> | ||||||
|             </a> |             </a> | ||||||
|           </li> |           </li> | ||||||
|           <li class="nav-item" tourAnchor="tour.admin"> |  | ||||||
|             <a class="nav-link" href="admin/" ngbPopover="Admin" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> |  | ||||||
|               <svg class="sidebaricon" fill="currentColor"> |  | ||||||
|                 <use xlink:href="assets/bootstrap-icons.svg#toggles"/> |  | ||||||
|               </svg><span> <ng-container i18n>Admin</ng-container></span> |  | ||||||
|             </a> |  | ||||||
|           </li> |  | ||||||
|         </ul> |         </ul> | ||||||
|  |  | ||||||
|         <h6 class="sidebar-heading px-3 mt-auto pt-4 mb-1 text-muted"> |         <h6 class="sidebar-heading px-3 mt-auto pt-4 mb-1 text-muted"> | ||||||
|   | |||||||
| @@ -5,7 +5,6 @@ import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit- | |||||||
| import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' | import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' | ||||||
| import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent' | import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent' | ||||||
| import { CorrespondentService } from 'src/app/services/rest/correspondent.service' | import { CorrespondentService } from 'src/app/services/rest/correspondent.service' | ||||||
| import { ToastService } from 'src/app/services/toast.service' |  | ||||||
|  |  | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-correspondent-edit-dialog', |   selector: 'app-correspondent-edit-dialog', | ||||||
| @@ -13,12 +12,8 @@ import { ToastService } from 'src/app/services/toast.service' | |||||||
|   styleUrls: ['./correspondent-edit-dialog.component.scss'], |   styleUrls: ['./correspondent-edit-dialog.component.scss'], | ||||||
| }) | }) | ||||||
| export class CorrespondentEditDialogComponent extends EditDialogComponent<PaperlessCorrespondent> { | export class CorrespondentEditDialogComponent extends EditDialogComponent<PaperlessCorrespondent> { | ||||||
|   constructor( |   constructor(service: CorrespondentService, activeModal: NgbActiveModal) { | ||||||
|     service: CorrespondentService, |     super(service, activeModal) | ||||||
|     activeModal: NgbActiveModal, |  | ||||||
|     toastService: ToastService |  | ||||||
|   ) { |  | ||||||
|     super(service, activeModal, toastService) |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getCreateTitle() { |   getCreateTitle() { | ||||||
|   | |||||||
| @@ -5,7 +5,6 @@ import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit- | |||||||
| import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' | import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' | ||||||
| import { PaperlessDocumentType } from 'src/app/data/paperless-document-type' | import { PaperlessDocumentType } from 'src/app/data/paperless-document-type' | ||||||
| import { DocumentTypeService } from 'src/app/services/rest/document-type.service' | import { DocumentTypeService } from 'src/app/services/rest/document-type.service' | ||||||
| import { ToastService } from 'src/app/services/toast.service' |  | ||||||
|  |  | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-document-type-edit-dialog', |   selector: 'app-document-type-edit-dialog', | ||||||
| @@ -13,12 +12,8 @@ import { ToastService } from 'src/app/services/toast.service' | |||||||
|   styleUrls: ['./document-type-edit-dialog.component.scss'], |   styleUrls: ['./document-type-edit-dialog.component.scss'], | ||||||
| }) | }) | ||||||
| export class DocumentTypeEditDialogComponent extends EditDialogComponent<PaperlessDocumentType> { | export class DocumentTypeEditDialogComponent extends EditDialogComponent<PaperlessDocumentType> { | ||||||
|   constructor( |   constructor(service: DocumentTypeService, activeModal: NgbActiveModal) { | ||||||
|     service: DocumentTypeService, |     super(service, activeModal) | ||||||
|     activeModal: NgbActiveModal, |  | ||||||
|     toastService: ToastService |  | ||||||
|   ) { |  | ||||||
|     super(service, activeModal, toastService) |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getCreateTitle() { |   getCreateTitle() { | ||||||
|   | |||||||
| @@ -2,11 +2,9 @@ import { Directive, EventEmitter, Input, OnInit, Output } from '@angular/core' | |||||||
| import { FormGroup } from '@angular/forms' | import { FormGroup } from '@angular/forms' | ||||||
| import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' | import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' | ||||||
| import { Observable } from 'rxjs' | import { Observable } from 'rxjs' | ||||||
| import { map } from 'rxjs/operators' |  | ||||||
| import { MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model' | import { MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model' | ||||||
| import { ObjectWithId } from 'src/app/data/object-with-id' | import { ObjectWithId } from 'src/app/data/object-with-id' | ||||||
| import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service' | import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service' | ||||||
| import { ToastService } from 'src/app/services/toast.service' |  | ||||||
|  |  | ||||||
| @Directive() | @Directive() | ||||||
| export abstract class EditDialogComponent<T extends ObjectWithId> | export abstract class EditDialogComponent<T extends ObjectWithId> | ||||||
| @@ -14,8 +12,7 @@ export abstract class EditDialogComponent<T extends ObjectWithId> | |||||||
| { | { | ||||||
|   constructor( |   constructor( | ||||||
|     private service: AbstractPaperlessService<T>, |     private service: AbstractPaperlessService<T>, | ||||||
|     private activeModal: NgbActiveModal, |     private activeModal: NgbActiveModal | ||||||
|     private toastService: ToastService |  | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   @Input() |   @Input() | ||||||
| @@ -95,16 +92,16 @@ export abstract class EditDialogComponent<T extends ObjectWithId> | |||||||
|         break |         break | ||||||
|     } |     } | ||||||
|     this.networkActive = true |     this.networkActive = true | ||||||
|     serverResponse.subscribe( |     serverResponse.subscribe({ | ||||||
|       (result) => { |       next: (result) => { | ||||||
|         this.activeModal.close() |         this.activeModal.close() | ||||||
|         this.success.emit(result) |         this.success.emit(result) | ||||||
|       }, |       }, | ||||||
|       (error) => { |       error: (error) => { | ||||||
|         this.error = error.error |         this.error = error.error | ||||||
|         this.networkActive = false |         this.networkActive = false | ||||||
|       } |       }, | ||||||
|     ) |     }) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   cancel() { |   cancel() { | ||||||
|   | |||||||
| @@ -0,0 +1,26 @@ | |||||||
|  | <form [formGroup]="objectForm" (ngSubmit)="save()"> | ||||||
|  |   <div class="modal-header"> | ||||||
|  |     <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4> | ||||||
|  |     <button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()"> | ||||||
|  |     </button> | ||||||
|  |   </div> | ||||||
|  |   <div class="modal-body"> | ||||||
|  |     <div class="row"> | ||||||
|  |       <div class="col"> | ||||||
|  |         <app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text> | ||||||
|  |         <app-input-text i18n-title title="IMAP Server" formControlName="imap_server" [error]="error?.imap_server"></app-input-text> | ||||||
|  |         <app-input-text i18n-title title="IMAP Port" formControlName="imap_port" [error]="error?.imap_port"></app-input-text> | ||||||
|  |         <app-input-select i18n-title title="IMAP Security" [items]="imapSecurityOptions" formControlName="imap_security"></app-input-select> | ||||||
|  |       </div> | ||||||
|  |       <div class="col"> | ||||||
|  |         <app-input-text i18n-title title="Username" formControlName="username" [error]="error?.username"></app-input-text> | ||||||
|  |         <app-input-password i18n-title title="Password" formControlName="password" [error]="error?.password"></app-input-password> | ||||||
|  |         <app-input-text i18n-title title="Character Set" formControlName="character_set" [error]="error?.character_set"></app-input-text> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  |   <div class="modal-footer"> | ||||||
|  |     <button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button> | ||||||
|  |     <button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button> | ||||||
|  |   </div> | ||||||
|  | </form> | ||||||
| @@ -0,0 +1,50 @@ | |||||||
|  | import { Component } from '@angular/core' | ||||||
|  | import { FormControl, FormGroup } from '@angular/forms' | ||||||
|  | import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' | ||||||
|  | import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' | ||||||
|  | import { | ||||||
|  |   IMAPSecurity, | ||||||
|  |   PaperlessMailAccount, | ||||||
|  | } from 'src/app/data/paperless-mail-account' | ||||||
|  | import { MailAccountService } from 'src/app/services/rest/mail-account.service' | ||||||
|  |  | ||||||
|  | const IMAP_SECURITY_OPTIONS = [ | ||||||
|  |   { id: IMAPSecurity.None, name: $localize`No encryption` }, | ||||||
|  |   { id: IMAPSecurity.SSL, name: $localize`SSL` }, | ||||||
|  |   { id: IMAPSecurity.STARTTLS, name: $localize`STARTTLS` }, | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | @Component({ | ||||||
|  |   selector: 'app-mail-account-edit-dialog', | ||||||
|  |   templateUrl: './mail-account-edit-dialog.component.html', | ||||||
|  |   styleUrls: ['./mail-account-edit-dialog.component.scss'], | ||||||
|  | }) | ||||||
|  | export class MailAccountEditDialogComponent extends EditDialogComponent<PaperlessMailAccount> { | ||||||
|  |   constructor(service: MailAccountService, activeModal: NgbActiveModal) { | ||||||
|  |     super(service, activeModal) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   getCreateTitle() { | ||||||
|  |     return $localize`Create new mail account` | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   getEditTitle() { | ||||||
|  |     return $localize`Edit mail account` | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   getForm(): FormGroup { | ||||||
|  |     return new FormGroup({ | ||||||
|  |       name: new FormControl(null), | ||||||
|  |       imap_server: new FormControl(null), | ||||||
|  |       imap_port: new FormControl(null), | ||||||
|  |       imap_security: new FormControl(IMAPSecurity.SSL), | ||||||
|  |       username: new FormControl(null), | ||||||
|  |       password: new FormControl(null), | ||||||
|  |       character_set: new FormControl('UTF-8'), | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get imapSecurityOptions() { | ||||||
|  |     return IMAP_SECURITY_OPTIONS | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -0,0 +1,39 @@ | |||||||
|  | <form [formGroup]="objectForm" (ngSubmit)="save()"> | ||||||
|  |   <div class="modal-header"> | ||||||
|  |     <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4> | ||||||
|  |     <button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()"> | ||||||
|  |     </button> | ||||||
|  |   </div> | ||||||
|  |   <div class="modal-body"> | ||||||
|  |     <div class="row"> | ||||||
|  |       <div class="col"> | ||||||
|  |         <app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text> | ||||||
|  |         <app-input-select i18n-title title="Account" [items]="accounts" formControlName="account"></app-input-select> | ||||||
|  |         <app-input-text i18n-title title="Folder" formControlName="folder" i18n-hint hint="Subfolders must be separated by a delimiter, often a dot ('.') or slash ('/'), but it varies by mail server." [error]="error?.folder"></app-input-text> | ||||||
|  |         <app-input-number i18n-title title="Maximum age (days)" formControlName="maximum_age" [showAdd]="false" [error]="error?.maximum_age"></app-input-number> | ||||||
|  |         <app-input-select i18n-title title="Attachment type" [items]="attachmentTypeOptions" formControlName="attachment_type"></app-input-select> | ||||||
|  |       </div> | ||||||
|  |       <div class="col"> | ||||||
|  |         <p class="small" i18n>Paperless will only process mails that match <em>all</em> of the filters specified below.</p> | ||||||
|  |         <app-input-text i18n-title title="Filter from" formControlName="filter_from" [error]="error?.filter_from"></app-input-text> | ||||||
|  |         <app-input-text i18n-title title="Filter subject" formControlName="filter_subject" [error]="error?.filter_subject"></app-input-text> | ||||||
|  |         <app-input-text i18n-title title="Filter body" formControlName="filter_body" [error]="error?.filter_body"></app-input-text> | ||||||
|  |         <app-input-text i18n-title title="Filter attachment filename" formControlName="filter_attachment_filename" i18n-hint hint="Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_attachment_filename"></app-input-text> | ||||||
|  |       </div> | ||||||
|  |       <div class="col"> | ||||||
|  |         <app-input-select i18n-title title="Action" [items]="actionOptions" formControlName="action" i18n-hint hint="Action is only performed when documents are consumed from the mail. Mails without attachments remain entirely untouched."></app-input-select> | ||||||
|  |         <app-input-text i18n-title title="Action parameter" *ngIf="showActionParamField" formControlName="action_parameter" [error]="error?.action_parameter"></app-input-text> | ||||||
|  |         <app-input-select i18n-title title="Assign title from" [items]="metadataTitleOptions" formControlName="assign_title_from"></app-input-select> | ||||||
|  |         <app-input-tags [allowCreate]="false" formControlName="assign_tags"></app-input-tags> | ||||||
|  |         <app-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></app-input-select> | ||||||
|  |         <app-input-select i18n-title title="Assign correspondent from" [items]="metadataCorrespondentOptions" formControlName="assign_correspondent_from"></app-input-select> | ||||||
|  |         <app-input-select *ngIf="showCorrespondentField" i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></app-input-select> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  |   <div class="modal-footer"> | ||||||
|  |     <span class="text-danger" *ngIf="error?.non_field_errors"><ng-container i18n>Error</ng-container>: {{error.non_field_errors}}</span> | ||||||
|  |     <button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button> | ||||||
|  |     <button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button> | ||||||
|  |   </div> | ||||||
|  | </form> | ||||||
| @@ -0,0 +1,180 @@ | |||||||
|  | import { Component } from '@angular/core' | ||||||
|  | import { FormControl, FormGroup } from '@angular/forms' | ||||||
|  | import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' | ||||||
|  | import { first } from 'rxjs' | ||||||
|  | import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' | ||||||
|  | import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent' | ||||||
|  | import { PaperlessDocumentType } from 'src/app/data/paperless-document-type' | ||||||
|  | import { PaperlessMailAccount } from 'src/app/data/paperless-mail-account' | ||||||
|  | import { | ||||||
|  |   MailAction, | ||||||
|  |   MailFilterAttachmentType, | ||||||
|  |   MailMetadataCorrespondentOption, | ||||||
|  |   MailMetadataTitleOption, | ||||||
|  |   PaperlessMailRule, | ||||||
|  | } from 'src/app/data/paperless-mail-rule' | ||||||
|  | import { CorrespondentService } from 'src/app/services/rest/correspondent.service' | ||||||
|  | import { DocumentTypeService } from 'src/app/services/rest/document-type.service' | ||||||
|  | import { MailAccountService } from 'src/app/services/rest/mail-account.service' | ||||||
|  | import { MailRuleService } from 'src/app/services/rest/mail-rule.service' | ||||||
|  |  | ||||||
|  | const ATTACHMENT_TYPE_OPTIONS = [ | ||||||
|  |   { | ||||||
|  |     id: MailFilterAttachmentType.Attachments, | ||||||
|  |     name: $localize`Only process attachments.`, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     id: MailFilterAttachmentType.Everything, | ||||||
|  |     name: $localize`Process all files, including 'inline' attachments.`, | ||||||
|  |   }, | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | const ACTION_OPTIONS = [ | ||||||
|  |   { | ||||||
|  |     id: MailAction.Delete, | ||||||
|  |     name: $localize`Delete`, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     id: MailAction.Move, | ||||||
|  |     name: $localize`Move to specified folder`, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     id: MailAction.MarkRead, | ||||||
|  |     name: $localize`Mark as read, don't process read mails`, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     id: MailAction.Flag, | ||||||
|  |     name: $localize`Flag the mail, don't process flagged mails`, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     id: MailAction.Tag, | ||||||
|  |     name: $localize`Tag the mail with specified tag, don't process tagged mails`, | ||||||
|  |   }, | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | const METADATA_TITLE_OPTIONS = [ | ||||||
|  |   { | ||||||
|  |     id: MailMetadataTitleOption.FromSubject, | ||||||
|  |     name: $localize`Use subject as title`, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     id: MailMetadataTitleOption.FromFilename, | ||||||
|  |     name: $localize`Use attachment filename as title`, | ||||||
|  |   }, | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | const METADATA_CORRESPONDENT_OPTIONS = [ | ||||||
|  |   { | ||||||
|  |     id: MailMetadataCorrespondentOption.FromNothing, | ||||||
|  |     name: $localize`Do not assign a correspondent`, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     id: MailMetadataCorrespondentOption.FromEmail, | ||||||
|  |     name: $localize`Use mail address`, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     id: MailMetadataCorrespondentOption.FromName, | ||||||
|  |     name: $localize`Use name (or mail address if not available)`, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     id: MailMetadataCorrespondentOption.FromCustom, | ||||||
|  |     name: $localize`Use correspondent selected below`, | ||||||
|  |   }, | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | @Component({ | ||||||
|  |   selector: 'app-mail-rule-edit-dialog', | ||||||
|  |   templateUrl: './mail-rule-edit-dialog.component.html', | ||||||
|  |   styleUrls: ['./mail-rule-edit-dialog.component.scss'], | ||||||
|  | }) | ||||||
|  | export class MailRuleEditDialogComponent extends EditDialogComponent<PaperlessMailRule> { | ||||||
|  |   accounts: PaperlessMailAccount[] | ||||||
|  |   correspondents: PaperlessCorrespondent[] | ||||||
|  |   documentTypes: PaperlessDocumentType[] | ||||||
|  |  | ||||||
|  |   constructor( | ||||||
|  |     service: MailRuleService, | ||||||
|  |     activeModal: NgbActiveModal, | ||||||
|  |     accountService: MailAccountService, | ||||||
|  |     correspondentService: CorrespondentService, | ||||||
|  |     documentTypeService: DocumentTypeService | ||||||
|  |   ) { | ||||||
|  |     super(service, activeModal) | ||||||
|  |  | ||||||
|  |     accountService | ||||||
|  |       .listAll() | ||||||
|  |       .pipe(first()) | ||||||
|  |       .subscribe((result) => (this.accounts = result.results)) | ||||||
|  |  | ||||||
|  |     correspondentService | ||||||
|  |       .listAll() | ||||||
|  |       .pipe(first()) | ||||||
|  |       .subscribe((result) => (this.correspondents = result.results)) | ||||||
|  |  | ||||||
|  |     documentTypeService | ||||||
|  |       .listAll() | ||||||
|  |       .pipe(first()) | ||||||
|  |       .subscribe((result) => (this.documentTypes = result.results)) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   getCreateTitle() { | ||||||
|  |     return $localize`Create new mail rule` | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   getEditTitle() { | ||||||
|  |     return $localize`Edit mail rule` | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   getForm(): FormGroup { | ||||||
|  |     return new FormGroup({ | ||||||
|  |       name: new FormControl(null), | ||||||
|  |       account: new FormControl(null), | ||||||
|  |       folder: new FormControl('INBOX'), | ||||||
|  |       filter_from: new FormControl(null), | ||||||
|  |       filter_subject: new FormControl(null), | ||||||
|  |       filter_body: new FormControl(null), | ||||||
|  |       filter_attachment_filename: new FormControl(null), | ||||||
|  |       maximum_age: new FormControl(null), | ||||||
|  |       attachment_type: new FormControl(MailFilterAttachmentType.Attachments), | ||||||
|  |       action: new FormControl(MailAction.MarkRead), | ||||||
|  |       action_parameter: new FormControl(null), | ||||||
|  |       assign_title_from: new FormControl(MailMetadataTitleOption.FromSubject), | ||||||
|  |       assign_tags: new FormControl([]), | ||||||
|  |       assign_document_type: new FormControl(null), | ||||||
|  |       assign_correspondent_from: new FormControl( | ||||||
|  |         MailMetadataCorrespondentOption.FromNothing | ||||||
|  |       ), | ||||||
|  |       assign_correspondent: new FormControl(null), | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get showCorrespondentField(): boolean { | ||||||
|  |     return ( | ||||||
|  |       this.objectForm?.get('assign_correspondent_from')?.value == | ||||||
|  |       MailMetadataCorrespondentOption.FromCustom | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get showActionParamField(): boolean { | ||||||
|  |     return ( | ||||||
|  |       this.objectForm?.get('action')?.value == MailAction.Move || | ||||||
|  |       this.objectForm?.get('action')?.value == MailAction.Tag | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get attachmentTypeOptions() { | ||||||
|  |     return ATTACHMENT_TYPE_OPTIONS | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get actionOptions() { | ||||||
|  |     return ACTION_OPTIONS | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get metadataTitleOptions() { | ||||||
|  |     return METADATA_TITLE_OPTIONS | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get metadataCorrespondentOptions() { | ||||||
|  |     return METADATA_CORRESPONDENT_OPTIONS | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -5,7 +5,6 @@ import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit- | |||||||
| import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' | import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' | ||||||
| import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path' | import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path' | ||||||
| import { StoragePathService } from 'src/app/services/rest/storage-path.service' | import { StoragePathService } from 'src/app/services/rest/storage-path.service' | ||||||
| import { ToastService } from 'src/app/services/toast.service' |  | ||||||
|  |  | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-storage-path-edit-dialog', |   selector: 'app-storage-path-edit-dialog', | ||||||
| @@ -13,12 +12,8 @@ import { ToastService } from 'src/app/services/toast.service' | |||||||
|   styleUrls: ['./storage-path-edit-dialog.component.scss'], |   styleUrls: ['./storage-path-edit-dialog.component.scss'], | ||||||
| }) | }) | ||||||
| export class StoragePathEditDialogComponent extends EditDialogComponent<PaperlessStoragePath> { | export class StoragePathEditDialogComponent extends EditDialogComponent<PaperlessStoragePath> { | ||||||
|   constructor( |   constructor(service: StoragePathService, activeModal: NgbActiveModal) { | ||||||
|     service: StoragePathService, |     super(service, activeModal) | ||||||
|     activeModal: NgbActiveModal, |  | ||||||
|     toastService: ToastService |  | ||||||
|   ) { |  | ||||||
|     super(service, activeModal, toastService) |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get pathHint() { |   get pathHint() { | ||||||
|   | |||||||
| @@ -4,7 +4,6 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' | |||||||
| import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' | import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' | ||||||
| import { PaperlessTag } from 'src/app/data/paperless-tag' | import { PaperlessTag } from 'src/app/data/paperless-tag' | ||||||
| import { TagService } from 'src/app/services/rest/tag.service' | import { TagService } from 'src/app/services/rest/tag.service' | ||||||
| import { ToastService } from 'src/app/services/toast.service' |  | ||||||
| import { randomColor } from 'src/app/utils/color' | import { randomColor } from 'src/app/utils/color' | ||||||
| import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' | import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' | ||||||
|  |  | ||||||
| @@ -14,12 +13,8 @@ import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' | |||||||
|   styleUrls: ['./tag-edit-dialog.component.scss'], |   styleUrls: ['./tag-edit-dialog.component.scss'], | ||||||
| }) | }) | ||||||
| export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> { | export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> { | ||||||
|   constructor( |   constructor(service: TagService, activeModal: NgbActiveModal) { | ||||||
|     service: TagService, |     super(service, activeModal) | ||||||
|     activeModal: NgbActiveModal, |  | ||||||
|     toastService: ToastService |  | ||||||
|   ) { |  | ||||||
|     super(service, activeModal, toastService) |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getCreateTitle() { |   getCreateTitle() { | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
|   <label class="form-label" [for]="inputId">{{title}}</label> |   <label class="form-label" [for]="inputId">{{title}}</label> | ||||||
|   <div class="input-group" [class.is-invalid]="error"> |   <div class="input-group" [class.is-invalid]="error"> | ||||||
|     <input type="number" class="form-control" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [class.is-invalid]="error"> |     <input type="number" class="form-control" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [class.is-invalid]="error"> | ||||||
|     <button class="btn btn-outline-secondary" type="button" id="button-addon1" (click)="nextAsn()" [disabled]="value">+1</button> |     <button *ngIf="showAdd" class="btn btn-outline-secondary" type="button" id="button-addon1" (click)="nextAsn()" [disabled]="value">+1</button> | ||||||
|   </div> |   </div> | ||||||
|   <div class="invalid-feedback"> |   <div class="invalid-feedback"> | ||||||
|     {{error}} |     {{error}} | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { Component, forwardRef } from '@angular/core' | import { Component, forwardRef, Input } from '@angular/core' | ||||||
| import { NG_VALUE_ACCESSOR } from '@angular/forms' | import { NG_VALUE_ACCESSOR } from '@angular/forms' | ||||||
| import { FILTER_ASN_ISNULL } from 'src/app/data/filter-rule-type' | import { FILTER_ASN_ISNULL } from 'src/app/data/filter-rule-type' | ||||||
| import { DocumentService } from 'src/app/services/rest/document.service' | import { DocumentService } from 'src/app/services/rest/document.service' | ||||||
| @@ -17,6 +17,9 @@ import { AbstractInputComponent } from '../abstract-input' | |||||||
|   styleUrls: ['./number.component.scss'], |   styleUrls: ['./number.component.scss'], | ||||||
| }) | }) | ||||||
| export class NumberComponent extends AbstractInputComponent<number> { | export class NumberComponent extends AbstractInputComponent<number> { | ||||||
|  |   @Input() | ||||||
|  |   showAdd: boolean = true | ||||||
|  |  | ||||||
|   constructor(private documentService: DocumentService) { |   constructor(private documentService: DocumentService) { | ||||||
|     super() |     super() | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -0,0 +1,8 @@ | |||||||
|  | <div class="mb-3"> | ||||||
|  |   <label class="form-label" [for]="inputId">{{title}}</label> | ||||||
|  |   <input #inputField type="password" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)"> | ||||||
|  |   <small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small> | ||||||
|  |   <div class="invalid-feedback"> | ||||||
|  |     {{error}} | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
| @@ -0,0 +1,21 @@ | |||||||
|  | import { Component, forwardRef } from '@angular/core' | ||||||
|  | import { NG_VALUE_ACCESSOR } from '@angular/forms' | ||||||
|  | import { AbstractInputComponent } from '../abstract-input' | ||||||
|  |  | ||||||
|  | @Component({ | ||||||
|  |   providers: [ | ||||||
|  |     { | ||||||
|  |       provide: NG_VALUE_ACCESSOR, | ||||||
|  |       useExisting: forwardRef(() => PasswordComponent), | ||||||
|  |       multi: true, | ||||||
|  |     }, | ||||||
|  |   ], | ||||||
|  |   selector: 'app-input-password', | ||||||
|  |   templateUrl: './password.component.html', | ||||||
|  |   styleUrls: ['./password.component.scss'], | ||||||
|  | }) | ||||||
|  | export class PasswordComponent extends AbstractInputComponent<string> { | ||||||
|  |   constructor() { | ||||||
|  |     super() | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -7,7 +7,7 @@ | |||||||
|       [closeOnSelect]="false" |       [closeOnSelect]="false" | ||||||
|       [clearSearchOnAdd]="true" |       [clearSearchOnAdd]="true" | ||||||
|       [hideSelected]="true" |       [hideSelected]="true" | ||||||
|       [addTag]="createTagRef" |       [addTag]="allowCreate ? createTagRef : false" | ||||||
|       addTagText="Add tag" |       addTagText="Add tag" | ||||||
|       i18n-addTagText |       i18n-addTagText | ||||||
|       (change)="onChange(value)" |       (change)="onChange(value)" | ||||||
| @@ -31,7 +31,7 @@ | |||||||
|       </ng-template> |       </ng-template> | ||||||
|     </ng-select> |     </ng-select> | ||||||
|  |  | ||||||
|     <button class="btn btn-outline-secondary" type="button" (click)="createTag()"> |     <button *ngIf="allowCreate" class="btn btn-outline-secondary" type="button" (click)="createTag()"> | ||||||
|       <svg class="buttonicon" fill="currentColor"> |       <svg class="buttonicon" fill="currentColor"> | ||||||
|         <use xlink:href="assets/bootstrap-icons.svg#plus" /> |         <use xlink:href="assets/bootstrap-icons.svg#plus" /> | ||||||
|       </svg> |       </svg> | ||||||
|   | |||||||
| @@ -54,6 +54,9 @@ export class TagsComponent implements OnInit, ControlValueAccessor { | |||||||
|   @Input() |   @Input() | ||||||
|   suggestions: number[] |   suggestions: number[] | ||||||
|  |  | ||||||
|  |   @Input() | ||||||
|  |   allowCreate: boolean = true | ||||||
|  |  | ||||||
|   value: number[] |   value: number[] | ||||||
|  |  | ||||||
|   tags: PaperlessTag[] |   tags: PaperlessTag[] | ||||||
|   | |||||||
| @@ -120,8 +120,20 @@ export abstract class ManagementListComponent<T extends ObjectWithId> | |||||||
|       backdrop: 'static', |       backdrop: 'static', | ||||||
|     }) |     }) | ||||||
|     activeModal.componentInstance.dialogMode = 'create' |     activeModal.componentInstance.dialogMode = 'create' | ||||||
|     activeModal.componentInstance.success.subscribe((o) => { |     activeModal.componentInstance.success.subscribe({ | ||||||
|       this.reloadData() |       next: () => { | ||||||
|  |         this.reloadData() | ||||||
|  |         this.toastService.showInfo( | ||||||
|  |           $localize`Successfully created ${this.typeName}.` | ||||||
|  |         ) | ||||||
|  |       }, | ||||||
|  |       error: (e) => { | ||||||
|  |         this.toastService.showInfo( | ||||||
|  |           $localize`Error occurred while creating ${ | ||||||
|  |             this.typeName | ||||||
|  |           } : ${e.toString()}.` | ||||||
|  |         ) | ||||||
|  |       }, | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -131,8 +143,20 @@ export abstract class ManagementListComponent<T extends ObjectWithId> | |||||||
|     }) |     }) | ||||||
|     activeModal.componentInstance.object = object |     activeModal.componentInstance.object = object | ||||||
|     activeModal.componentInstance.dialogMode = 'edit' |     activeModal.componentInstance.dialogMode = 'edit' | ||||||
|     activeModal.componentInstance.success.subscribe((o) => { |     activeModal.componentInstance.success.subscribe({ | ||||||
|       this.reloadData() |       next: () => { | ||||||
|  |         this.reloadData() | ||||||
|  |         this.toastService.showInfo( | ||||||
|  |           $localize`Successfully updated ${this.typeName}.` | ||||||
|  |         ) | ||||||
|  |       }, | ||||||
|  |       error: (e) => { | ||||||
|  |         this.toastService.showInfo( | ||||||
|  |           $localize`Error occurred while saving ${ | ||||||
|  |             this.typeName | ||||||
|  |           } : ${e.toString()}.` | ||||||
|  |         ) | ||||||
|  |       }, | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,12 +1,17 @@ | |||||||
| <app-page-header title="Settings" i18n-title> | <app-page-header title="Settings" i18n-title> | ||||||
|   <button class="btn btn-sm btn-outline-primary" (click)="tourService.start()"><ng-container i18n>Start tour</ng-container></button> |   <button class="btn btn-sm btn-outline-primary" (click)="tourService.start()"><ng-container i18n>Start tour</ng-container></button> | ||||||
|  |   <a class="btn btn-sm btn-primary ms-3" href="admin/" target="_blank"> | ||||||
|  |       <ng-container i18n>Open Django Admin</ng-container> | ||||||
|  |       <svg class="sidebaricon ms-1" fill="currentColor"> | ||||||
|  |         <use xlink:href="assets/bootstrap-icons.svg#arrow-up-right"/> | ||||||
|  |       </svg> | ||||||
|  |   </a> | ||||||
| </app-page-header> | </app-page-header> | ||||||
|  |  | ||||||
| <!-- <p>items per page, documents per view type</p> --> |  | ||||||
| <form [formGroup]="settingsForm" (ngSubmit)="saveSettings()"> | <form [formGroup]="settingsForm" (ngSubmit)="saveSettings()"> | ||||||
|  |  | ||||||
|   <ul ngbNav #nav="ngbNav" class="nav-tabs"> |   <ul ngbNav #nav="ngbNav" (navChange)="onNavChange($event)" [(activeId)]="activeNavID" class="nav-tabs"> | ||||||
|     <li [ngbNavItem]="1"> |     <li [ngbNavItem]="SettingsNavIDs.General"> | ||||||
|       <a ngbNavLink i18n>General</a> |       <a ngbNavLink i18n>General</a> | ||||||
|       <ng-template ngbNavContent> |       <ng-template ngbNavContent> | ||||||
|  |  | ||||||
| @@ -162,7 +167,7 @@ | |||||||
|       </ng-template> |       </ng-template> | ||||||
|     </li> |     </li> | ||||||
|  |  | ||||||
|     <li [ngbNavItem]="2"> |     <li [ngbNavItem]="SettingsNavIDs.Notifications"> | ||||||
|       <a ngbNavLink i18n>Notifications</a> |       <a ngbNavLink i18n>Notifications</a> | ||||||
|       <ng-template ngbNavContent> |       <ng-template ngbNavContent> | ||||||
|  |  | ||||||
| @@ -180,7 +185,7 @@ | |||||||
|       </ng-template> |       </ng-template> | ||||||
|     </li> |     </li> | ||||||
|  |  | ||||||
|     <li [ngbNavItem]="3"> |     <li [ngbNavItem]="SettingsNavIDs.SavedViews" (mouseover)="maybeInitializeTab(SettingsNavIDs.SavedViews)" (focusin)="maybeInitializeTab(SettingsNavIDs.SavedViews)"> | ||||||
|       <a ngbNavLink i18n>Saved views</a> |       <a ngbNavLink i18n>Saved views</a> | ||||||
|       <ng-template ngbNavContent> |       <ng-template ngbNavContent> | ||||||
|  |  | ||||||
| @@ -210,8 +215,97 @@ | |||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
|  |  | ||||||
|             <div *ngIf="savedViews.length == 0" i18n>No saved views defined.</div> |             <div *ngIf="savedViews && savedViews.length == 0" i18n>No saved views defined.</div> | ||||||
|  |  | ||||||
|  |             <div *ngIf="!savedViews"> | ||||||
|  |               <div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div> | ||||||
|  |               <div class="visually-hidden" i18n>Loading...</div> | ||||||
|  |             </div> | ||||||
|  |  | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |       </ng-template> | ||||||
|  |     </li> | ||||||
|  |  | ||||||
|  |     <li [ngbNavItem]="SettingsNavIDs.Mail" (mouseover)="maybeInitializeTab(SettingsNavIDs.Mail)" (focusin)="maybeInitializeTab(SettingsNavIDs.Mail)"> | ||||||
|  |       <a ngbNavLink i18n>Mail</a> | ||||||
|  |       <ng-template ngbNavContent> | ||||||
|  |  | ||||||
|  |         <ng-container *ngIf="mailAccounts && mailRules"> | ||||||
|  |           <h4> | ||||||
|  |             <ng-container i18n>Mail accounts</ng-container> | ||||||
|  |             <button type="button" class="btn btn-sm btn-primary ms-4" (click)="editMailAccount()"> | ||||||
|  |               <svg class="sidebaricon me-1" fill="currentColor"> | ||||||
|  |                 <use xlink:href="assets/bootstrap-icons.svg#plus-circle" /> | ||||||
|  |               </svg> | ||||||
|  |               <ng-container i18n>Add Account</ng-container> | ||||||
|  |             </button> | ||||||
|  |           </h4> | ||||||
|  |           <ul class="list-group" formGroupName="mailAccounts"> | ||||||
|  |  | ||||||
|  |               <li class="list-group-item"> | ||||||
|  |                 <div class="row"> | ||||||
|  |                   <div class="col" i18n>Name</div> | ||||||
|  |                   <div class="col" i18n>Server</div> | ||||||
|  |                   <div class="col" i18n>Actions</div> | ||||||
|  |                 </div> | ||||||
|  |               </li> | ||||||
|  |  | ||||||
|  |               <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">{{account.imap_server}}</div> | ||||||
|  |                   <div class="col"> | ||||||
|  |                     <div class="btn-group"> | ||||||
|  |                       <button class="btn btn-sm btn-primary" type="button" (click)="editMailAccount(account)" i18n>Edit</button> | ||||||
|  |                       <button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteMailAccount(account)" i18n>Delete</button> | ||||||
|  |                     </div> | ||||||
|  |                   </div> | ||||||
|  |                 </div> | ||||||
|  |               </li> | ||||||
|  |  | ||||||
|  |               <div *ngIf="mailAccounts.length == 0" i18n>No mail accounts defined.</div> | ||||||
|  |           </ul> | ||||||
|  |  | ||||||
|  |           <h4 class="mt-4"> | ||||||
|  |             <ng-container i18n>Mail rules</ng-container> | ||||||
|  |             <button type="button" class="btn btn-sm btn-primary ms-4" (click)="editMailRule()"> | ||||||
|  |               <svg class="sidebaricon me-1" fill="currentColor"> | ||||||
|  |                 <use xlink:href="assets/bootstrap-icons.svg#plus-circle" /> | ||||||
|  |               </svg> | ||||||
|  |               <ng-container i18n>Add Rule</ng-container> | ||||||
|  |             </button> | ||||||
|  |           </h4> | ||||||
|  |           <ul class="list-group" formGroupName="mailRules"> | ||||||
|  |  | ||||||
|  |               <li class="list-group-item"> | ||||||
|  |                 <div class="row"> | ||||||
|  |                   <div class="col" i18n>Name</div> | ||||||
|  |                   <div class="col" i18n>Account</div> | ||||||
|  |                   <div class="col" i18n>Actions</div> | ||||||
|  |                 </div> | ||||||
|  |               </li> | ||||||
|  |  | ||||||
|  |               <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">{{(mailAccountService.getCached(rule.account) | async)?.name}}</div> | ||||||
|  |                   <div class="col"> | ||||||
|  |                     <div class="btn-group"> | ||||||
|  |                       <button class="btn btn-sm btn-primary" type="button" (click)="editMailRule(rule)" i18n>Edit</button> | ||||||
|  |                       <button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteMailRule(rule)" i18n>Delete</button> | ||||||
|  |                     </div> | ||||||
|  |                   </div> | ||||||
|  |                 </div> | ||||||
|  |               </li> | ||||||
|  |  | ||||||
|  |               <div *ngIf="mailRules.length == 0" i18n>No mail rules defined.</div> | ||||||
|  |           </ul> | ||||||
|  |         </ng-container> | ||||||
|  |  | ||||||
|  |         <div *ngIf="!mailAccounts || !mailRules"> | ||||||
|  |           <div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div> | ||||||
|  |           <div class="visually-hidden" i18n>Loading...</div> | ||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|       </ng-template> |       </ng-template> | ||||||
|   | |||||||
| @@ -26,9 +26,26 @@ import { | |||||||
|   Subject, |   Subject, | ||||||
| } from 'rxjs' | } from 'rxjs' | ||||||
| import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings' | import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings' | ||||||
| import { ActivatedRoute } from '@angular/router' | import { ActivatedRoute, Router } from '@angular/router' | ||||||
| import { ViewportScroller } from '@angular/common' | import { ViewportScroller } from '@angular/common' | ||||||
| import { TourService } from 'ngx-ui-tour-ng-bootstrap' | import { TourService } from 'ngx-ui-tour-ng-bootstrap' | ||||||
|  | import { PaperlessMailAccount } from 'src/app/data/paperless-mail-account' | ||||||
|  | import { PaperlessMailRule } from 'src/app/data/paperless-mail-rule' | ||||||
|  | import { MailAccountService } from 'src/app/services/rest/mail-account.service' | ||||||
|  | import { MailRuleService } from 'src/app/services/rest/mail-rule.service' | ||||||
|  | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||||||
|  | import { MailAccountEditDialogComponent } from '../../common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component' | ||||||
|  | import { MailRuleEditDialogComponent } from '../../common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component' | ||||||
|  | import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' | ||||||
|  | import { NgbNavChangeEvent } from '@ng-bootstrap/ng-bootstrap' | ||||||
|  |  | ||||||
|  | enum SettingsNavIDs { | ||||||
|  |   General = 1, | ||||||
|  |   Notifications = 2, | ||||||
|  |   SavedViews = 3, | ||||||
|  |   Mail = 4, | ||||||
|  |   UsersGroups = 5, | ||||||
|  | } | ||||||
|  |  | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-settings', |   selector: 'app-settings', | ||||||
| @@ -38,8 +55,14 @@ import { TourService } from 'ngx-ui-tour-ng-bootstrap' | |||||||
| export class SettingsComponent | export class SettingsComponent | ||||||
|   implements OnInit, AfterViewInit, OnDestroy, DirtyComponent |   implements OnInit, AfterViewInit, OnDestroy, DirtyComponent | ||||||
| { | { | ||||||
|  |   SettingsNavIDs = SettingsNavIDs | ||||||
|  |   activeNavID: number | ||||||
|  |  | ||||||
|   savedViewGroup = new FormGroup({}) |   savedViewGroup = new FormGroup({}) | ||||||
|  |  | ||||||
|  |   mailAccountGroup = new FormGroup({}) | ||||||
|  |   mailRuleGroup = new FormGroup({}) | ||||||
|  |  | ||||||
|   settingsForm = new FormGroup({ |   settingsForm = new FormGroup({ | ||||||
|     bulkEditConfirmationDialogs: new FormControl(null), |     bulkEditConfirmationDialogs: new FormControl(null), | ||||||
|     bulkEditApplyOnClose: new FormControl(null), |     bulkEditApplyOnClose: new FormControl(null), | ||||||
| @@ -50,20 +73,28 @@ export class SettingsComponent | |||||||
|     darkModeInvertThumbs: new FormControl(null), |     darkModeInvertThumbs: new FormControl(null), | ||||||
|     themeColor: new FormControl(null), |     themeColor: new FormControl(null), | ||||||
|     useNativePdfViewer: new FormControl(null), |     useNativePdfViewer: new FormControl(null), | ||||||
|     savedViews: this.savedViewGroup, |  | ||||||
|     displayLanguage: new FormControl(null), |     displayLanguage: new FormControl(null), | ||||||
|     dateLocale: new FormControl(null), |     dateLocale: new FormControl(null), | ||||||
|     dateFormat: new FormControl(null), |     dateFormat: new FormControl(null), | ||||||
|  |     commentsEnabled: new FormControl(null), | ||||||
|  |     updateCheckingEnabled: new FormControl(null), | ||||||
|  |  | ||||||
|     notificationsConsumerNewDocument: new FormControl(null), |     notificationsConsumerNewDocument: new FormControl(null), | ||||||
|     notificationsConsumerSuccess: new FormControl(null), |     notificationsConsumerSuccess: new FormControl(null), | ||||||
|     notificationsConsumerFailed: new FormControl(null), |     notificationsConsumerFailed: new FormControl(null), | ||||||
|     notificationsConsumerSuppressOnDashboard: new FormControl(null), |     notificationsConsumerSuppressOnDashboard: new FormControl(null), | ||||||
|     commentsEnabled: new FormControl(null), |  | ||||||
|     updateCheckingEnabled: new FormControl(null), |     savedViews: this.savedViewGroup, | ||||||
|  |  | ||||||
|  |     mailAccounts: this.mailAccountGroup, | ||||||
|  |     mailRules: this.mailRuleGroup, | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   savedViews: PaperlessSavedView[] |   savedViews: PaperlessSavedView[] | ||||||
|  |  | ||||||
|  |   mailAccounts: PaperlessMailAccount[] | ||||||
|  |   mailRules: PaperlessMailRule[] | ||||||
|  |  | ||||||
|   store: BehaviorSubject<any> |   store: BehaviorSubject<any> | ||||||
|   storeSub: Subscription |   storeSub: Subscription | ||||||
|   isDirty$: Observable<boolean> |   isDirty$: Observable<boolean> | ||||||
| @@ -81,19 +112,40 @@ export class SettingsComponent | |||||||
|  |  | ||||||
|   constructor( |   constructor( | ||||||
|     public savedViewService: SavedViewService, |     public savedViewService: SavedViewService, | ||||||
|  |     public mailAccountService: MailAccountService, | ||||||
|  |     public mailRuleService: MailRuleService, | ||||||
|     private documentListViewService: DocumentListViewService, |     private documentListViewService: DocumentListViewService, | ||||||
|     private toastService: ToastService, |     private toastService: ToastService, | ||||||
|     private settings: SettingsService, |     private settings: SettingsService, | ||||||
|     @Inject(LOCALE_ID) public currentLocale: string, |     @Inject(LOCALE_ID) public currentLocale: string, | ||||||
|     private viewportScroller: ViewportScroller, |     private viewportScroller: ViewportScroller, | ||||||
|     private activatedRoute: ActivatedRoute, |     private activatedRoute: ActivatedRoute, | ||||||
|     public readonly tourService: TourService |     private router: Router, | ||||||
|  |     public readonly tourService: TourService, | ||||||
|  |     private modalService: NgbModal | ||||||
|   ) { |   ) { | ||||||
|     this.settings.settingsSaved.subscribe(() => { |     this.settings.settingsSaved.subscribe(() => { | ||||||
|       if (!this.savePending) this.initialize() |       if (!this.savePending) this.initialize() | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   ngOnInit() { | ||||||
|  |     this.initialize() | ||||||
|  |  | ||||||
|  |     this.activatedRoute.paramMap.subscribe((paramMap) => { | ||||||
|  |       const section = paramMap.get('section') | ||||||
|  |       if (section) { | ||||||
|  |         const navIDKey: string = Object.keys(SettingsNavIDs).find( | ||||||
|  |           (navID) => navID.toLowerCase() == section | ||||||
|  |         ) | ||||||
|  |         if (navIDKey) { | ||||||
|  |           this.activeNavID = SettingsNavIDs[navIDKey] | ||||||
|  |           this.maybeInitializeTab(this.activeNavID) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|   ngAfterViewInit(): void { |   ngAfterViewInit(): void { | ||||||
|     if (this.activatedRoute.snapshot.fragment) { |     if (this.activatedRoute.snapshot.fragment) { | ||||||
|       this.viewportScroller.scrollToAnchor( |       this.viewportScroller.scrollToAnchor( | ||||||
| @@ -123,10 +175,13 @@ export class SettingsComponent | |||||||
|       useNativePdfViewer: this.settings.get( |       useNativePdfViewer: this.settings.get( | ||||||
|         SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER |         SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER | ||||||
|       ), |       ), | ||||||
|       savedViews: {}, |  | ||||||
|       displayLanguage: this.settings.getLanguage(), |       displayLanguage: this.settings.getLanguage(), | ||||||
|       dateLocale: this.settings.get(SETTINGS_KEYS.DATE_LOCALE), |       dateLocale: this.settings.get(SETTINGS_KEYS.DATE_LOCALE), | ||||||
|       dateFormat: this.settings.get(SETTINGS_KEYS.DATE_FORMAT), |       dateFormat: this.settings.get(SETTINGS_KEYS.DATE_FORMAT), | ||||||
|  |       commentsEnabled: this.settings.get(SETTINGS_KEYS.COMMENTS_ENABLED), | ||||||
|  |       updateCheckingEnabled: this.settings.get( | ||||||
|  |         SETTINGS_KEYS.UPDATE_CHECKING_ENABLED | ||||||
|  |       ), | ||||||
|       notificationsConsumerNewDocument: this.settings.get( |       notificationsConsumerNewDocument: this.settings.get( | ||||||
|         SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT |         SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT | ||||||
|       ), |       ), | ||||||
| @@ -139,41 +194,147 @@ export class SettingsComponent | |||||||
|       notificationsConsumerSuppressOnDashboard: this.settings.get( |       notificationsConsumerSuppressOnDashboard: this.settings.get( | ||||||
|         SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD |         SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD | ||||||
|       ), |       ), | ||||||
|       commentsEnabled: this.settings.get(SETTINGS_KEYS.COMMENTS_ENABLED), |       savedViews: {}, | ||||||
|       updateCheckingEnabled: this.settings.get( |       mailAccounts: {}, | ||||||
|         SETTINGS_KEYS.UPDATE_CHECKING_ENABLED |       mailRules: {}, | ||||||
|       ), |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   ngOnInit() { |   onNavChange(navChangeEvent: NgbNavChangeEvent) { | ||||||
|     this.savedViewService.listAll().subscribe((r) => { |     this.maybeInitializeTab(navChangeEvent.nextId) | ||||||
|       this.savedViews = r.results |     const [foundNavIDkey, foundNavIDValue] = Object.entries( | ||||||
|       this.initialize() |       SettingsNavIDs | ||||||
|     }) |     ).find(([navIDkey, navIDValue]) => navIDValue == navChangeEvent.nextId) | ||||||
|  |     if (foundNavIDkey) | ||||||
|  |       // if its dirty we need to wait for confirmation | ||||||
|  |       this.router | ||||||
|  |         .navigate(['settings', foundNavIDkey.toLowerCase()]) | ||||||
|  |         .then((navigated) => { | ||||||
|  |           if (!navigated && this.isDirty) { | ||||||
|  |             this.activeNavID = navChangeEvent.activeId | ||||||
|  |           } else if (navigated && this.isDirty) { | ||||||
|  |             this.initialize() | ||||||
|  |           } | ||||||
|  |         }) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   initialize() { |   // Load tab contents 'on demand', either on mouseover or focusin (i.e. before click) or called from nav change event | ||||||
|  |   maybeInitializeTab(navID: number): void { | ||||||
|  |     if (navID == SettingsNavIDs.SavedViews && !this.savedViews) { | ||||||
|  |       this.savedViewService.listAll().subscribe((r) => { | ||||||
|  |         this.savedViews = r.results | ||||||
|  |         this.initialize(false) | ||||||
|  |       }) | ||||||
|  |     } else if ( | ||||||
|  |       navID == SettingsNavIDs.Mail && | ||||||
|  |       (!this.mailAccounts || !this.mailRules) | ||||||
|  |     ) { | ||||||
|  |       this.mailAccountService.listAll().subscribe((r) => { | ||||||
|  |         this.mailAccounts = r.results | ||||||
|  |  | ||||||
|  |         this.mailRuleService.listAll().subscribe((r) => { | ||||||
|  |           this.mailRules = r.results | ||||||
|  |           this.initialize(false) | ||||||
|  |         }) | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   initialize(resetSettings: boolean = true) { | ||||||
|     this.unsubscribeNotifier.next(true) |     this.unsubscribeNotifier.next(true) | ||||||
|  |  | ||||||
|  |     const currentFormValue = this.settingsForm.value | ||||||
|  |  | ||||||
|     let storeData = this.getCurrentSettings() |     let storeData = this.getCurrentSettings() | ||||||
|  |  | ||||||
|     for (let view of this.savedViews) { |     if (this.savedViews) { | ||||||
|       storeData.savedViews[view.id.toString()] = { |       for (let view of this.savedViews) { | ||||||
|         id: view.id, |         storeData.savedViews[view.id.toString()] = { | ||||||
|         name: view.name, |           id: view.id, | ||||||
|         show_on_dashboard: view.show_on_dashboard, |           name: view.name, | ||||||
|         show_in_sidebar: view.show_in_sidebar, |           show_on_dashboard: view.show_on_dashboard, | ||||||
|  |           show_in_sidebar: view.show_in_sidebar, | ||||||
|  |         } | ||||||
|  |         this.savedViewGroup.addControl( | ||||||
|  |           view.id.toString(), | ||||||
|  |           new FormGroup({ | ||||||
|  |             id: new FormControl(null), | ||||||
|  |             name: new FormControl(null), | ||||||
|  |             show_on_dashboard: new FormControl(null), | ||||||
|  |             show_in_sidebar: new FormControl(null), | ||||||
|  |           }) | ||||||
|  |         ) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (this.mailAccounts && this.mailRules) { | ||||||
|  |       for (let account of this.mailAccounts) { | ||||||
|  |         storeData.mailAccounts[account.id.toString()] = { | ||||||
|  |           id: account.id, | ||||||
|  |           name: account.name, | ||||||
|  |           imap_server: account.imap_server, | ||||||
|  |           imap_port: account.imap_port, | ||||||
|  |           imap_security: account.imap_security, | ||||||
|  |           username: account.username, | ||||||
|  |           password: account.password, | ||||||
|  |           character_set: account.character_set, | ||||||
|  |         } | ||||||
|  |         this.mailAccountGroup.addControl( | ||||||
|  |           account.id.toString(), | ||||||
|  |           new FormGroup({ | ||||||
|  |             id: new FormControl(null), | ||||||
|  |             name: new FormControl(null), | ||||||
|  |             imap_server: new FormControl(null), | ||||||
|  |             imap_port: new FormControl(null), | ||||||
|  |             imap_security: new FormControl(null), | ||||||
|  |             username: new FormControl(null), | ||||||
|  |             password: new FormControl(null), | ||||||
|  |             character_set: new FormControl(null), | ||||||
|  |           }) | ||||||
|  |         ) | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       for (let rule of this.mailRules) { | ||||||
|  |         storeData.mailRules[rule.id.toString()] = { | ||||||
|  |           name: rule.name, | ||||||
|  |           account: rule.account, | ||||||
|  |           folder: rule.folder, | ||||||
|  |           filter_from: rule.filter_from, | ||||||
|  |           filter_subject: rule.filter_subject, | ||||||
|  |           filter_body: rule.filter_body, | ||||||
|  |           filter_attachment_filename: rule.filter_attachment_filename, | ||||||
|  |           maximum_age: rule.maximum_age, | ||||||
|  |           attachment_type: rule.attachment_type, | ||||||
|  |           action: rule.action, | ||||||
|  |           action_parameter: rule.action_parameter, | ||||||
|  |           assign_title_from: rule.assign_title_from, | ||||||
|  |           assign_tags: rule.assign_tags, | ||||||
|  |           assign_document_type: rule.assign_document_type, | ||||||
|  |           assign_correspondent_from: rule.assign_correspondent_from, | ||||||
|  |           assign_correspondent: rule.assign_correspondent, | ||||||
|  |         } | ||||||
|  |         this.mailRuleGroup.addControl( | ||||||
|  |           rule.id.toString(), | ||||||
|  |           new FormGroup({ | ||||||
|  |             name: new FormControl(null), | ||||||
|  |             account: new FormControl(null), | ||||||
|  |             folder: new FormControl(null), | ||||||
|  |             filter_from: new FormControl(null), | ||||||
|  |             filter_subject: new FormControl(null), | ||||||
|  |             filter_body: new FormControl(null), | ||||||
|  |             filter_attachment_filename: new FormControl(null), | ||||||
|  |             maximum_age: new FormControl(null), | ||||||
|  |             attachment_type: new FormControl(null), | ||||||
|  |             action: new FormControl(null), | ||||||
|  |             action_parameter: new FormControl(null), | ||||||
|  |             assign_title_from: new FormControl(null), | ||||||
|  |             assign_tags: new FormControl(null), | ||||||
|  |             assign_document_type: new FormControl(null), | ||||||
|  |             assign_correspondent_from: new FormControl(null), | ||||||
|  |             assign_correspondent: new FormControl(null), | ||||||
|  |           }) | ||||||
|  |         ) | ||||||
|       } |       } | ||||||
|       this.savedViewGroup.addControl( |  | ||||||
|         view.id.toString(), |  | ||||||
|         new FormGroup({ |  | ||||||
|           id: new FormControl(null), |  | ||||||
|           name: new FormControl(null), |  | ||||||
|           show_on_dashboard: new FormControl(null), |  | ||||||
|           show_in_sidebar: new FormControl(null), |  | ||||||
|         }) |  | ||||||
|       ) |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     this.store = new BehaviorSubject(storeData) |     this.store = new BehaviorSubject(storeData) | ||||||
| @@ -202,6 +363,11 @@ export class SettingsComponent | |||||||
|           this.settingsForm.get('themeColor').value |           this.settingsForm.get('themeColor').value | ||||||
|         ) |         ) | ||||||
|       }) |       }) | ||||||
|  |  | ||||||
|  |     if (!resetSettings && currentFormValue) { | ||||||
|  |       // prevents loss of unsaved changes | ||||||
|  |       this.settingsForm.patchValue(currentFormValue) | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   ngOnDestroy() { |   ngOnDestroy() { | ||||||
| @@ -372,4 +538,121 @@ export class SettingsComponent | |||||||
|   clearThemeColor() { |   clearThemeColor() { | ||||||
|     this.settingsForm.get('themeColor').patchValue('') |     this.settingsForm.get('themeColor').patchValue('') | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   editMailAccount(account: PaperlessMailAccount) { | ||||||
|  |     const modal = this.modalService.open(MailAccountEditDialogComponent, { | ||||||
|  |       backdrop: 'static', | ||||||
|  |       size: 'xl', | ||||||
|  |     }) | ||||||
|  |     modal.componentInstance.dialogMode = account ? 'edit' : 'create' | ||||||
|  |     modal.componentInstance.object = account | ||||||
|  |     modal.componentInstance.success | ||||||
|  |       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||||
|  |       .subscribe({ | ||||||
|  |         next: (newMailAccount) => { | ||||||
|  |           this.toastService.showInfo( | ||||||
|  |             $localize`Saved account "${newMailAccount.name}".` | ||||||
|  |           ) | ||||||
|  |           this.mailAccountService.clearCache() | ||||||
|  |           this.mailAccountService.listAll().subscribe((r) => { | ||||||
|  |             this.mailAccounts = r.results | ||||||
|  |             this.initialize() | ||||||
|  |           }) | ||||||
|  |         }, | ||||||
|  |         error: (e) => { | ||||||
|  |           this.toastService.showError( | ||||||
|  |             $localize`Error saving account: ${e.toString()}.` | ||||||
|  |           ) | ||||||
|  |         }, | ||||||
|  |       }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   deleteMailAccount(account: PaperlessMailAccount) { | ||||||
|  |     const modal = this.modalService.open(ConfirmDialogComponent, { | ||||||
|  |       backdrop: 'static', | ||||||
|  |     }) | ||||||
|  |     modal.componentInstance.title = $localize`Confirm delete mail account` | ||||||
|  |     modal.componentInstance.messageBold = $localize`This operation will permanently this mail account.` | ||||||
|  |     modal.componentInstance.message = $localize`This operation cannot be undone.` | ||||||
|  |     modal.componentInstance.btnClass = 'btn-danger' | ||||||
|  |     modal.componentInstance.btnCaption = $localize`Proceed` | ||||||
|  |     modal.componentInstance.confirmClicked.subscribe(() => { | ||||||
|  |       modal.componentInstance.buttonsEnabled = false | ||||||
|  |       this.mailAccountService.delete(account).subscribe({ | ||||||
|  |         next: () => { | ||||||
|  |           modal.close() | ||||||
|  |           this.toastService.showInfo($localize`Deleted mail account`) | ||||||
|  |           this.mailAccountService.clearCache() | ||||||
|  |           this.mailAccountService.listAll().subscribe((r) => { | ||||||
|  |             this.mailAccounts = r.results | ||||||
|  |             this.initialize() | ||||||
|  |           }) | ||||||
|  |         }, | ||||||
|  |         error: (e) => { | ||||||
|  |           this.toastService.showError( | ||||||
|  |             $localize`Error deleting mail account: ${e.toString()}.` | ||||||
|  |           ) | ||||||
|  |         }, | ||||||
|  |       }) | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   editMailRule(rule: PaperlessMailRule) { | ||||||
|  |     const modal = this.modalService.open(MailRuleEditDialogComponent, { | ||||||
|  |       backdrop: 'static', | ||||||
|  |       size: 'xl', | ||||||
|  |     }) | ||||||
|  |     modal.componentInstance.dialogMode = rule ? 'edit' : 'create' | ||||||
|  |     modal.componentInstance.object = rule | ||||||
|  |     modal.componentInstance.success | ||||||
|  |       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||||
|  |       .subscribe({ | ||||||
|  |         next: (newMailRule) => { | ||||||
|  |           this.toastService.showInfo( | ||||||
|  |             $localize`Saved rule "${newMailRule.name}".` | ||||||
|  |           ) | ||||||
|  |           this.mailRuleService.clearCache() | ||||||
|  |           this.mailRuleService.listAll().subscribe((r) => { | ||||||
|  |             this.mailRules = r.results | ||||||
|  |  | ||||||
|  |             this.initialize() | ||||||
|  |           }) | ||||||
|  |         }, | ||||||
|  |         error: (e) => { | ||||||
|  |           this.toastService.showError( | ||||||
|  |             $localize`Error saving rule: ${e.toString()}.` | ||||||
|  |           ) | ||||||
|  |         }, | ||||||
|  |       }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   deleteMailRule(rule: PaperlessMailRule) { | ||||||
|  |     const modal = this.modalService.open(ConfirmDialogComponent, { | ||||||
|  |       backdrop: 'static', | ||||||
|  |     }) | ||||||
|  |     modal.componentInstance.title = $localize`Confirm delete mail rule` | ||||||
|  |     modal.componentInstance.messageBold = $localize`This operation will permanently this mail rule.` | ||||||
|  |     modal.componentInstance.message = $localize`This operation cannot be undone.` | ||||||
|  |     modal.componentInstance.btnClass = 'btn-danger' | ||||||
|  |     modal.componentInstance.btnCaption = $localize`Proceed` | ||||||
|  |     modal.componentInstance.confirmClicked.subscribe(() => { | ||||||
|  |       modal.componentInstance.buttonsEnabled = false | ||||||
|  |       this.mailRuleService.delete(rule).subscribe({ | ||||||
|  |         next: () => { | ||||||
|  |           modal.close() | ||||||
|  |           this.toastService.showInfo($localize`Deleted mail rule`) | ||||||
|  |           this.mailRuleService.clearCache() | ||||||
|  |           this.mailRuleService.listAll().subscribe((r) => { | ||||||
|  |             this.mailRules = r.results | ||||||
|  |             this.initialize() | ||||||
|  |           }) | ||||||
|  |         }, | ||||||
|  |         error: (e) => { | ||||||
|  |           this.toastService.showError( | ||||||
|  |             $localize`Error deleting mail rule: ${e.toString()}.` | ||||||
|  |           ) | ||||||
|  |         }, | ||||||
|  |       }) | ||||||
|  |     }) | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								src-ui/src/app/data/paperless-mail-account.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src-ui/src/app/data/paperless-mail-account.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | import { ObjectWithId } from './object-with-id' | ||||||
|  |  | ||||||
|  | export enum IMAPSecurity { | ||||||
|  |   None = 1, | ||||||
|  |   SSL = 2, | ||||||
|  |   STARTTLS = 3, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface PaperlessMailAccount extends ObjectWithId { | ||||||
|  |   name: string | ||||||
|  |  | ||||||
|  |   imap_server: string | ||||||
|  |  | ||||||
|  |   imap_port: number | ||||||
|  |  | ||||||
|  |   imap_security: IMAPSecurity | ||||||
|  |  | ||||||
|  |   username: string | ||||||
|  |  | ||||||
|  |   password: string | ||||||
|  |  | ||||||
|  |   character_set?: string | ||||||
|  | } | ||||||
							
								
								
									
										60
									
								
								src-ui/src/app/data/paperless-mail-rule.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src-ui/src/app/data/paperless-mail-rule.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | |||||||
|  | import { ObjectWithId } from './object-with-id' | ||||||
|  |  | ||||||
|  | export enum MailFilterAttachmentType { | ||||||
|  |   Attachments = 1, | ||||||
|  |   Everything = 2, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export enum MailAction { | ||||||
|  |   Delete = 1, | ||||||
|  |   Move = 2, | ||||||
|  |   MarkRead = 3, | ||||||
|  |   Flag = 4, | ||||||
|  |   Tag = 5, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export enum MailMetadataTitleOption { | ||||||
|  |   FromSubject = 1, | ||||||
|  |   FromFilename = 2, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export enum MailMetadataCorrespondentOption { | ||||||
|  |   FromNothing = 1, | ||||||
|  |   FromEmail = 2, | ||||||
|  |   FromName = 3, | ||||||
|  |   FromCustom = 4, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface PaperlessMailRule extends ObjectWithId { | ||||||
|  |   name: string | ||||||
|  |  | ||||||
|  |   account: number // PaperlessMailAccount.id | ||||||
|  |  | ||||||
|  |   folder: string | ||||||
|  |  | ||||||
|  |   filter_from: string | ||||||
|  |  | ||||||
|  |   filter_subject: string | ||||||
|  |  | ||||||
|  |   filter_body: string | ||||||
|  |  | ||||||
|  |   filter_attachment_filename: string | ||||||
|  |  | ||||||
|  |   maximum_age: number | ||||||
|  |  | ||||||
|  |   attachment_type: MailFilterAttachmentType | ||||||
|  |  | ||||||
|  |   action: MailAction | ||||||
|  |  | ||||||
|  |   action_parameter?: string | ||||||
|  |  | ||||||
|  |   assign_title_from: MailMetadataTitleOption | ||||||
|  |  | ||||||
|  |   assign_tags?: number[] // PaperlessTag.id | ||||||
|  |  | ||||||
|  |   assign_document_type?: number // PaperlessDocumentType.id | ||||||
|  |  | ||||||
|  |   assign_correspondent_from?: MailMetadataCorrespondentOption | ||||||
|  |  | ||||||
|  |   assign_correspondent?: number // PaperlessCorrespondent.id | ||||||
|  | } | ||||||
| @@ -1,7 +1,6 @@ | |||||||
| import { Injectable } from '@angular/core' | import { Injectable } from '@angular/core' | ||||||
| import { DirtyCheckGuard } from '@ngneat/dirty-check-forms' | import { DirtyCheckGuard } from '@ngneat/dirty-check-forms' | ||||||
| import { Observable, Subject } from 'rxjs' | import { Observable, Subject } from 'rxjs' | ||||||
| import { map } from 'rxjs/operators' |  | ||||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||||||
| import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component' | import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component' | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										51
									
								
								src-ui/src/app/services/rest/mail-account.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src-ui/src/app/services/rest/mail-account.service.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | |||||||
|  | import { HttpClient } from '@angular/common/http' | ||||||
|  | import { Injectable } from '@angular/core' | ||||||
|  | import { combineLatest, Observable } from 'rxjs' | ||||||
|  | import { tap } from 'rxjs/operators' | ||||||
|  | import { PaperlessMailAccount } from 'src/app/data/paperless-mail-account' | ||||||
|  | import { AbstractPaperlessService } from './abstract-paperless-service' | ||||||
|  |  | ||||||
|  | @Injectable({ | ||||||
|  |   providedIn: 'root', | ||||||
|  | }) | ||||||
|  | export class MailAccountService extends AbstractPaperlessService<PaperlessMailAccount> { | ||||||
|  |   loading: boolean | ||||||
|  |  | ||||||
|  |   constructor(http: HttpClient) { | ||||||
|  |     super(http, 'mail_accounts') | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private reload() { | ||||||
|  |     this.loading = true | ||||||
|  |     this.listAll().subscribe((r) => { | ||||||
|  |       this.mailAccounts = r.results | ||||||
|  |       this.loading = false | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private mailAccounts: PaperlessMailAccount[] = [] | ||||||
|  |  | ||||||
|  |   get allAccounts() { | ||||||
|  |     return this.mailAccounts | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   create(o: PaperlessMailAccount) { | ||||||
|  |     return super.create(o).pipe(tap(() => this.reload())) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   update(o: PaperlessMailAccount) { | ||||||
|  |     return super.update(o).pipe(tap(() => this.reload())) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   patchMany( | ||||||
|  |     objects: PaperlessMailAccount[] | ||||||
|  |   ): Observable<PaperlessMailAccount[]> { | ||||||
|  |     return combineLatest(objects.map((o) => super.patch(o))).pipe( | ||||||
|  |       tap(() => this.reload()) | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   delete(o: PaperlessMailAccount) { | ||||||
|  |     return super.delete(o).pipe(tap(() => this.reload())) | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										49
									
								
								src-ui/src/app/services/rest/mail-rule.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src-ui/src/app/services/rest/mail-rule.service.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | |||||||
|  | import { HttpClient } from '@angular/common/http' | ||||||
|  | import { Injectable } from '@angular/core' | ||||||
|  | import { combineLatest, Observable } from 'rxjs' | ||||||
|  | import { tap } from 'rxjs/operators' | ||||||
|  | import { PaperlessMailRule } from 'src/app/data/paperless-mail-rule' | ||||||
|  | import { AbstractPaperlessService } from './abstract-paperless-service' | ||||||
|  |  | ||||||
|  | @Injectable({ | ||||||
|  |   providedIn: 'root', | ||||||
|  | }) | ||||||
|  | export class MailRuleService extends AbstractPaperlessService<PaperlessMailRule> { | ||||||
|  |   loading: boolean | ||||||
|  |  | ||||||
|  |   constructor(http: HttpClient) { | ||||||
|  |     super(http, 'mail_rules') | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private reload() { | ||||||
|  |     this.loading = true | ||||||
|  |     this.listAll().subscribe((r) => { | ||||||
|  |       this.mailRules = r.results | ||||||
|  |       this.loading = false | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private mailRules: PaperlessMailRule[] = [] | ||||||
|  |  | ||||||
|  |   get allRules() { | ||||||
|  |     return this.mailRules | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   create(o: PaperlessMailRule) { | ||||||
|  |     return super.create(o).pipe(tap(() => this.reload())) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   update(o: PaperlessMailRule) { | ||||||
|  |     return super.update(o).pipe(tap(() => this.reload())) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   patchMany(objects: PaperlessMailRule[]): Observable<PaperlessMailRule[]> { | ||||||
|  |     return combineLatest(objects.map((o) => super.patch(o))).pipe( | ||||||
|  |       tap(() => this.reload()) | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   delete(o: PaperlessMailRule) { | ||||||
|  |     return super.delete(o).pipe(tap(() => this.reload())) | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -242,10 +242,12 @@ a, a:hover, | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   .input-group { |   .input-group { | ||||||
|     .ng-select-container { |     ng-select:not(:last-child) { | ||||||
|       height: 100%; |       .ng-select-container { | ||||||
|       border-top-right-radius: 0; |         height: 100%; | ||||||
|       border-bottom-right-radius: 0; |         border-top-right-radius: 0; | ||||||
|  |         border-bottom-right-radius: 0; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -27,6 +27,8 @@ from documents.views import UiSettingsView | |||||||
| from documents.views import UnifiedSearchViewSet | from documents.views import UnifiedSearchViewSet | ||||||
| from paperless.consumers import StatusConsumer | from paperless.consumers import StatusConsumer | ||||||
| from paperless.views import FaviconView | from paperless.views import FaviconView | ||||||
|  | from paperless_mail.views import MailAccountViewSet | ||||||
|  | from paperless_mail.views import MailRuleViewSet | ||||||
| from rest_framework.authtoken import views | from rest_framework.authtoken import views | ||||||
| from rest_framework.routers import DefaultRouter | from rest_framework.routers import DefaultRouter | ||||||
|  |  | ||||||
| @@ -39,6 +41,8 @@ api_router.register(r"tags", TagViewSet) | |||||||
| api_router.register(r"saved_views", SavedViewViewSet) | api_router.register(r"saved_views", SavedViewViewSet) | ||||||
| api_router.register(r"storage_paths", StoragePathViewSet) | api_router.register(r"storage_paths", StoragePathViewSet) | ||||||
| api_router.register(r"tasks", TasksViewSet, basename="tasks") | api_router.register(r"tasks", TasksViewSet, basename="tasks") | ||||||
|  | api_router.register(r"mail_accounts", MailAccountViewSet) | ||||||
|  | api_router.register(r"mail_rules", MailRuleViewSet) | ||||||
|  |  | ||||||
|  |  | ||||||
| urlpatterns = [ | urlpatterns = [ | ||||||
|   | |||||||
							
								
								
									
										110
									
								
								src/paperless_mail/serialisers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								src/paperless_mail/serialisers.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | |||||||
|  | from documents.serialisers import CorrespondentField | ||||||
|  | from documents.serialisers import DocumentTypeField | ||||||
|  | from documents.serialisers import TagsField | ||||||
|  | from paperless_mail.models import MailAccount | ||||||
|  | from paperless_mail.models import MailRule | ||||||
|  | from rest_framework import serializers | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ObfuscatedPasswordField(serializers.Field): | ||||||
|  |     """ | ||||||
|  |     Sends *** string instead of password in the clear | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def to_representation(self, value): | ||||||
|  |         return "*" * len(value) | ||||||
|  |  | ||||||
|  |     def to_internal_value(self, data): | ||||||
|  |         return data | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class MailAccountSerializer(serializers.ModelSerializer): | ||||||
|  |     password = ObfuscatedPasswordField() | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         model = MailAccount | ||||||
|  |         depth = 1 | ||||||
|  |         fields = [ | ||||||
|  |             "id", | ||||||
|  |             "name", | ||||||
|  |             "imap_server", | ||||||
|  |             "imap_port", | ||||||
|  |             "imap_security", | ||||||
|  |             "username", | ||||||
|  |             "password", | ||||||
|  |             "character_set", | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |     def update(self, instance, validated_data): | ||||||
|  |         if "password" in validated_data: | ||||||
|  |             if len(validated_data.get("password").replace("*", "")) == 0: | ||||||
|  |                 validated_data.pop("password") | ||||||
|  |         super().update(instance, validated_data) | ||||||
|  |         return instance | ||||||
|  |  | ||||||
|  |     def create(self, validated_data): | ||||||
|  |         mail_account = MailAccount.objects.create(**validated_data) | ||||||
|  |         return mail_account | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AccountField(serializers.PrimaryKeyRelatedField): | ||||||
|  |     def get_queryset(self): | ||||||
|  |         return MailAccount.objects.all().order_by("-id") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class MailRuleSerializer(serializers.ModelSerializer): | ||||||
|  |     account = AccountField(required=True) | ||||||
|  |     action_parameter = serializers.CharField( | ||||||
|  |         allow_null=True, | ||||||
|  |         required=False, | ||||||
|  |         default="", | ||||||
|  |     ) | ||||||
|  |     assign_correspondent = CorrespondentField(allow_null=True, required=False) | ||||||
|  |     assign_tags = TagsField(many=True, allow_null=True, required=False) | ||||||
|  |     assign_document_type = DocumentTypeField(allow_null=True, required=False) | ||||||
|  |     order = serializers.IntegerField(required=False) | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         model = MailRule | ||||||
|  |         depth = 1 | ||||||
|  |         fields = [ | ||||||
|  |             "id", | ||||||
|  |             "name", | ||||||
|  |             "account", | ||||||
|  |             "folder", | ||||||
|  |             "filter_from", | ||||||
|  |             "filter_subject", | ||||||
|  |             "filter_body", | ||||||
|  |             "filter_attachment_filename", | ||||||
|  |             "maximum_age", | ||||||
|  |             "action", | ||||||
|  |             "action_parameter", | ||||||
|  |             "assign_title_from", | ||||||
|  |             "assign_tags", | ||||||
|  |             "assign_correspondent_from", | ||||||
|  |             "assign_correspondent", | ||||||
|  |             "assign_document_type", | ||||||
|  |             "order", | ||||||
|  |             "attachment_type", | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |     def update(self, instance, validated_data): | ||||||
|  |         super().update(instance, validated_data) | ||||||
|  |         return instance | ||||||
|  |  | ||||||
|  |     def create(self, validated_data): | ||||||
|  |         if "assign_tags" in validated_data: | ||||||
|  |             assign_tags = validated_data.pop("assign_tags") | ||||||
|  |         mail_rule = MailRule.objects.create(**validated_data) | ||||||
|  |         if assign_tags: | ||||||
|  |             mail_rule.assign_tags.set(assign_tags) | ||||||
|  |         return mail_rule | ||||||
|  |  | ||||||
|  |     def validate(self, attrs): | ||||||
|  |         if ( | ||||||
|  |             attrs["action"] == MailRule.MailAction.TAG | ||||||
|  |             or attrs["action"] == MailRule.MailAction.MOVE | ||||||
|  |         ) and attrs["action_parameter"] is None: | ||||||
|  |             raise serializers.ValidationError("An action parameter is required.") | ||||||
|  |  | ||||||
|  |         return attrs | ||||||
							
								
								
									
										429
									
								
								src/paperless_mail/tests/test_api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										429
									
								
								src/paperless_mail/tests/test_api.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,429 @@ | |||||||
|  | from django.contrib.auth.models import User | ||||||
|  | from documents.models import Correspondent | ||||||
|  | from documents.models import DocumentType | ||||||
|  | from documents.models import Tag | ||||||
|  | from paperless_mail.models import MailAccount | ||||||
|  | from paperless_mail.models import MailRule | ||||||
|  | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestAPIMailAccounts(APITestCase): | ||||||
|  |     ENDPOINT = "/api/mail_accounts/" | ||||||
|  |  | ||||||
|  |     def setUp(self): | ||||||
|  |         super().setUp() | ||||||
|  |  | ||||||
|  |         self.user = User.objects.create_superuser(username="temp_admin") | ||||||
|  |         self.client.force_authenticate(user=self.user) | ||||||
|  |  | ||||||
|  |     def test_get_mail_accounts(self): | ||||||
|  |         """ | ||||||
|  |         GIVEN: | ||||||
|  |             - Configured mail accounts | ||||||
|  |         WHEN: | ||||||
|  |             - API call is made to get mail accounts | ||||||
|  |         THEN: | ||||||
|  |             - Configured mail accounts are provided | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         account1 = MailAccount.objects.create( | ||||||
|  |             name="Email1", | ||||||
|  |             username="username1", | ||||||
|  |             password="password1", | ||||||
|  |             imap_server="server.example.com", | ||||||
|  |             imap_port=443, | ||||||
|  |             imap_security=MailAccount.ImapSecurity.SSL, | ||||||
|  |             character_set="UTF-8", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         response = self.client.get(self.ENDPOINT) | ||||||
|  |  | ||||||
|  |         self.assertEqual(response.status_code, 200) | ||||||
|  |         self.assertEqual(response.data["count"], 1) | ||||||
|  |         returned_account1 = response.data["results"][0] | ||||||
|  |  | ||||||
|  |         self.assertEqual(returned_account1["name"], account1.name) | ||||||
|  |         self.assertEqual(returned_account1["username"], account1.username) | ||||||
|  |         self.assertEqual( | ||||||
|  |             returned_account1["password"], | ||||||
|  |             "*" * len(account1.password), | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(returned_account1["imap_server"], account1.imap_server) | ||||||
|  |         self.assertEqual(returned_account1["imap_port"], account1.imap_port) | ||||||
|  |         self.assertEqual(returned_account1["imap_security"], account1.imap_security) | ||||||
|  |         self.assertEqual(returned_account1["character_set"], account1.character_set) | ||||||
|  |  | ||||||
|  |     def test_create_mail_account(self): | ||||||
|  |         """ | ||||||
|  |         WHEN: | ||||||
|  |             - API request is made to add a mail account | ||||||
|  |         THEN: | ||||||
|  |             - A new mail account is created | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         account1 = { | ||||||
|  |             "name": "Email1", | ||||||
|  |             "username": "username1", | ||||||
|  |             "password": "password1", | ||||||
|  |             "imap_server": "server.example.com", | ||||||
|  |             "imap_port": 443, | ||||||
|  |             "imap_security": MailAccount.ImapSecurity.SSL, | ||||||
|  |             "character_set": "UTF-8", | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         response = self.client.post( | ||||||
|  |             self.ENDPOINT, | ||||||
|  |             data=account1, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.assertEqual(response.status_code, 201) | ||||||
|  |  | ||||||
|  |         returned_account1 = MailAccount.objects.get(name="Email1") | ||||||
|  |  | ||||||
|  |         self.assertEqual(returned_account1.name, account1["name"]) | ||||||
|  |         self.assertEqual(returned_account1.username, account1["username"]) | ||||||
|  |         self.assertEqual(returned_account1.password, account1["password"]) | ||||||
|  |         self.assertEqual(returned_account1.imap_server, account1["imap_server"]) | ||||||
|  |         self.assertEqual(returned_account1.imap_port, account1["imap_port"]) | ||||||
|  |         self.assertEqual(returned_account1.imap_security, account1["imap_security"]) | ||||||
|  |         self.assertEqual(returned_account1.character_set, account1["character_set"]) | ||||||
|  |  | ||||||
|  |     def test_delete_mail_account(self): | ||||||
|  |         """ | ||||||
|  |         GIVEN: | ||||||
|  |             - Existing mail account | ||||||
|  |         WHEN: | ||||||
|  |             - API request is made to delete a mail account | ||||||
|  |         THEN: | ||||||
|  |             - Account is deleted | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         account1 = MailAccount.objects.create( | ||||||
|  |             name="Email1", | ||||||
|  |             username="username1", | ||||||
|  |             password="password1", | ||||||
|  |             imap_server="server.example.com", | ||||||
|  |             imap_port=443, | ||||||
|  |             imap_security=MailAccount.ImapSecurity.SSL, | ||||||
|  |             character_set="UTF-8", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         response = self.client.delete( | ||||||
|  |             f"{self.ENDPOINT}{account1.pk}/", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.assertEqual(response.status_code, 204) | ||||||
|  |  | ||||||
|  |         self.assertEqual(len(MailAccount.objects.all()), 0) | ||||||
|  |  | ||||||
|  |     def test_update_mail_account(self): | ||||||
|  |         """ | ||||||
|  |         GIVEN: | ||||||
|  |             - Existing mail accounts | ||||||
|  |         WHEN: | ||||||
|  |             - API request is made to update mail account | ||||||
|  |         THEN: | ||||||
|  |             - The mail account is updated, password only updated if not '****' | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         account1 = MailAccount.objects.create( | ||||||
|  |             name="Email1", | ||||||
|  |             username="username1", | ||||||
|  |             password="password1", | ||||||
|  |             imap_server="server.example.com", | ||||||
|  |             imap_port=443, | ||||||
|  |             imap_security=MailAccount.ImapSecurity.SSL, | ||||||
|  |             character_set="UTF-8", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         response = self.client.patch( | ||||||
|  |             f"{self.ENDPOINT}{account1.pk}/", | ||||||
|  |             data={ | ||||||
|  |                 "name": "Updated Name 1", | ||||||
|  |                 "password": "******", | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.assertEqual(response.status_code, 200) | ||||||
|  |  | ||||||
|  |         returned_account1 = MailAccount.objects.get(pk=account1.pk) | ||||||
|  |         self.assertEqual(returned_account1.name, "Updated Name 1") | ||||||
|  |         self.assertEqual(returned_account1.password, account1.password) | ||||||
|  |  | ||||||
|  |         response = self.client.patch( | ||||||
|  |             f"{self.ENDPOINT}{account1.pk}/", | ||||||
|  |             data={ | ||||||
|  |                 "name": "Updated Name 2", | ||||||
|  |                 "password": "123xyz", | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.assertEqual(response.status_code, 200) | ||||||
|  |  | ||||||
|  |         returned_account2 = MailAccount.objects.get(pk=account1.pk) | ||||||
|  |         self.assertEqual(returned_account2.name, "Updated Name 2") | ||||||
|  |         self.assertEqual(returned_account2.password, "123xyz") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestAPIMailRules(APITestCase): | ||||||
|  |     ENDPOINT = "/api/mail_rules/" | ||||||
|  |  | ||||||
|  |     def setUp(self): | ||||||
|  |         super().setUp() | ||||||
|  |  | ||||||
|  |         self.user = User.objects.create_superuser(username="temp_admin") | ||||||
|  |         self.client.force_authenticate(user=self.user) | ||||||
|  |  | ||||||
|  |     def test_get_mail_rules(self): | ||||||
|  |         """ | ||||||
|  |         GIVEN: | ||||||
|  |             - Configured mail accounts and rules | ||||||
|  |         WHEN: | ||||||
|  |             - API call is made to get mail rules | ||||||
|  |         THEN: | ||||||
|  |             - Configured mail rules are provided | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         account1 = MailAccount.objects.create( | ||||||
|  |             name="Email1", | ||||||
|  |             username="username1", | ||||||
|  |             password="password1", | ||||||
|  |             imap_server="server.example.com", | ||||||
|  |             imap_port=443, | ||||||
|  |             imap_security=MailAccount.ImapSecurity.SSL, | ||||||
|  |             character_set="UTF-8", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         rule1 = MailRule.objects.create( | ||||||
|  |             name="Rule1", | ||||||
|  |             account=account1, | ||||||
|  |             folder="INBOX", | ||||||
|  |             filter_from="from@example.com", | ||||||
|  |             filter_subject="subject", | ||||||
|  |             filter_body="body", | ||||||
|  |             filter_attachment_filename="file.pdf", | ||||||
|  |             maximum_age=30, | ||||||
|  |             action=MailRule.MailAction.MARK_READ, | ||||||
|  |             assign_title_from=MailRule.TitleSource.FROM_SUBJECT, | ||||||
|  |             assign_correspondent_from=MailRule.CorrespondentSource.FROM_NOTHING, | ||||||
|  |             order=0, | ||||||
|  |             attachment_type=MailRule.AttachmentProcessing.ATTACHMENTS_ONLY, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         response = self.client.get(self.ENDPOINT) | ||||||
|  |  | ||||||
|  |         self.assertEqual(response.status_code, 200) | ||||||
|  |         self.assertEqual(response.data["count"], 1) | ||||||
|  |         returned_rule1 = response.data["results"][0] | ||||||
|  |  | ||||||
|  |         self.assertEqual(returned_rule1["name"], rule1.name) | ||||||
|  |         self.assertEqual(returned_rule1["account"], account1.pk) | ||||||
|  |         self.assertEqual(returned_rule1["folder"], rule1.folder) | ||||||
|  |         self.assertEqual(returned_rule1["filter_from"], rule1.filter_from) | ||||||
|  |         self.assertEqual(returned_rule1["filter_subject"], rule1.filter_subject) | ||||||
|  |         self.assertEqual(returned_rule1["filter_body"], rule1.filter_body) | ||||||
|  |         self.assertEqual( | ||||||
|  |             returned_rule1["filter_attachment_filename"], | ||||||
|  |             rule1.filter_attachment_filename, | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(returned_rule1["maximum_age"], rule1.maximum_age) | ||||||
|  |         self.assertEqual(returned_rule1["action"], rule1.action) | ||||||
|  |         self.assertEqual(returned_rule1["assign_title_from"], rule1.assign_title_from) | ||||||
|  |         self.assertEqual( | ||||||
|  |             returned_rule1["assign_correspondent_from"], | ||||||
|  |             rule1.assign_correspondent_from, | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(returned_rule1["order"], rule1.order) | ||||||
|  |         self.assertEqual(returned_rule1["attachment_type"], rule1.attachment_type) | ||||||
|  |  | ||||||
|  |     def test_create_mail_rule(self): | ||||||
|  |         """ | ||||||
|  |         GIVEN: | ||||||
|  |             - Configured mail account exists | ||||||
|  |         WHEN: | ||||||
|  |             - API request is made to add a mail rule | ||||||
|  |         THEN: | ||||||
|  |             - A new mail rule is created | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         account1 = MailAccount.objects.create( | ||||||
|  |             name="Email1", | ||||||
|  |             username="username1", | ||||||
|  |             password="password1", | ||||||
|  |             imap_server="server.example.com", | ||||||
|  |             imap_port=443, | ||||||
|  |             imap_security=MailAccount.ImapSecurity.SSL, | ||||||
|  |             character_set="UTF-8", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         tag = Tag.objects.create( | ||||||
|  |             name="t", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         correspondent = Correspondent.objects.create( | ||||||
|  |             name="c", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         document_type = DocumentType.objects.create( | ||||||
|  |             name="dt", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         rule1 = { | ||||||
|  |             "name": "Rule1", | ||||||
|  |             "account": account1.pk, | ||||||
|  |             "folder": "INBOX", | ||||||
|  |             "filter_from": "from@example.com", | ||||||
|  |             "filter_subject": "subject", | ||||||
|  |             "filter_body": "body", | ||||||
|  |             "filter_attachment_filename": "file.pdf", | ||||||
|  |             "maximum_age": 30, | ||||||
|  |             "action": MailRule.MailAction.MARK_READ, | ||||||
|  |             "assign_title_from": MailRule.TitleSource.FROM_SUBJECT, | ||||||
|  |             "assign_correspondent_from": MailRule.CorrespondentSource.FROM_NOTHING, | ||||||
|  |             "order": 0, | ||||||
|  |             "attachment_type": MailRule.AttachmentProcessing.ATTACHMENTS_ONLY, | ||||||
|  |             "action_parameter": "parameter", | ||||||
|  |             "assign_tags": [tag.pk], | ||||||
|  |             "assign_correspondent": correspondent.pk, | ||||||
|  |             "assign_document_type": document_type.pk, | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         response = self.client.post( | ||||||
|  |             self.ENDPOINT, | ||||||
|  |             data=rule1, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.assertEqual(response.status_code, 201) | ||||||
|  |  | ||||||
|  |         response = self.client.get(self.ENDPOINT) | ||||||
|  |  | ||||||
|  |         self.assertEqual(response.status_code, 200) | ||||||
|  |         self.assertEqual(response.data["count"], 1) | ||||||
|  |         returned_rule1 = response.data["results"][0] | ||||||
|  |  | ||||||
|  |         self.assertEqual(returned_rule1["name"], rule1["name"]) | ||||||
|  |         self.assertEqual(returned_rule1["account"], account1.pk) | ||||||
|  |         self.assertEqual(returned_rule1["folder"], rule1["folder"]) | ||||||
|  |         self.assertEqual(returned_rule1["filter_from"], rule1["filter_from"]) | ||||||
|  |         self.assertEqual(returned_rule1["filter_subject"], rule1["filter_subject"]) | ||||||
|  |         self.assertEqual(returned_rule1["filter_body"], rule1["filter_body"]) | ||||||
|  |         self.assertEqual( | ||||||
|  |             returned_rule1["filter_attachment_filename"], | ||||||
|  |             rule1["filter_attachment_filename"], | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(returned_rule1["maximum_age"], rule1["maximum_age"]) | ||||||
|  |         self.assertEqual(returned_rule1["action"], rule1["action"]) | ||||||
|  |         self.assertEqual( | ||||||
|  |             returned_rule1["assign_title_from"], | ||||||
|  |             rule1["assign_title_from"], | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             returned_rule1["assign_correspondent_from"], | ||||||
|  |             rule1["assign_correspondent_from"], | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(returned_rule1["order"], rule1["order"]) | ||||||
|  |         self.assertEqual(returned_rule1["attachment_type"], rule1["attachment_type"]) | ||||||
|  |         self.assertEqual(returned_rule1["action_parameter"], rule1["action_parameter"]) | ||||||
|  |         self.assertEqual( | ||||||
|  |             returned_rule1["assign_correspondent"], | ||||||
|  |             rule1["assign_correspondent"], | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             returned_rule1["assign_document_type"], | ||||||
|  |             rule1["assign_document_type"], | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(returned_rule1["assign_tags"], rule1["assign_tags"]) | ||||||
|  |  | ||||||
|  |     def test_delete_mail_rule(self): | ||||||
|  |         """ | ||||||
|  |         GIVEN: | ||||||
|  |             - Existing mail rule | ||||||
|  |         WHEN: | ||||||
|  |             - API request is made to delete a mail rule | ||||||
|  |         THEN: | ||||||
|  |             - Rule is deleted | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         account1 = MailAccount.objects.create( | ||||||
|  |             name="Email1", | ||||||
|  |             username="username1", | ||||||
|  |             password="password1", | ||||||
|  |             imap_server="server.example.com", | ||||||
|  |             imap_port=443, | ||||||
|  |             imap_security=MailAccount.ImapSecurity.SSL, | ||||||
|  |             character_set="UTF-8", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         rule1 = MailRule.objects.create( | ||||||
|  |             name="Rule1", | ||||||
|  |             account=account1, | ||||||
|  |             folder="INBOX", | ||||||
|  |             filter_from="from@example.com", | ||||||
|  |             filter_subject="subject", | ||||||
|  |             filter_body="body", | ||||||
|  |             filter_attachment_filename="file.pdf", | ||||||
|  |             maximum_age=30, | ||||||
|  |             action=MailRule.MailAction.MARK_READ, | ||||||
|  |             assign_title_from=MailRule.TitleSource.FROM_SUBJECT, | ||||||
|  |             assign_correspondent_from=MailRule.CorrespondentSource.FROM_NOTHING, | ||||||
|  |             order=0, | ||||||
|  |             attachment_type=MailRule.AttachmentProcessing.ATTACHMENTS_ONLY, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         response = self.client.delete( | ||||||
|  |             f"{self.ENDPOINT}{rule1.pk}/", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.assertEqual(response.status_code, 204) | ||||||
|  |  | ||||||
|  |         self.assertEqual(len(MailRule.objects.all()), 0) | ||||||
|  |  | ||||||
|  |     def test_update_mail_rule(self): | ||||||
|  |         """ | ||||||
|  |         GIVEN: | ||||||
|  |             - Existing mail rule | ||||||
|  |         WHEN: | ||||||
|  |             - API request is made to update mail rule | ||||||
|  |         THEN: | ||||||
|  |             - The mail rule is updated | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         account1 = MailAccount.objects.create( | ||||||
|  |             name="Email1", | ||||||
|  |             username="username1", | ||||||
|  |             password="password1", | ||||||
|  |             imap_server="server.example.com", | ||||||
|  |             imap_port=443, | ||||||
|  |             imap_security=MailAccount.ImapSecurity.SSL, | ||||||
|  |             character_set="UTF-8", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         rule1 = MailRule.objects.create( | ||||||
|  |             name="Rule1", | ||||||
|  |             account=account1, | ||||||
|  |             folder="INBOX", | ||||||
|  |             filter_from="from@example.com", | ||||||
|  |             filter_subject="subject", | ||||||
|  |             filter_body="body", | ||||||
|  |             filter_attachment_filename="file.pdf", | ||||||
|  |             maximum_age=30, | ||||||
|  |             action=MailRule.MailAction.MARK_READ, | ||||||
|  |             assign_title_from=MailRule.TitleSource.FROM_SUBJECT, | ||||||
|  |             assign_correspondent_from=MailRule.CorrespondentSource.FROM_NOTHING, | ||||||
|  |             order=0, | ||||||
|  |             attachment_type=MailRule.AttachmentProcessing.ATTACHMENTS_ONLY, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         response = self.client.patch( | ||||||
|  |             f"{self.ENDPOINT}{rule1.pk}/", | ||||||
|  |             data={ | ||||||
|  |                 "name": "Updated Name 1", | ||||||
|  |                 "action": MailRule.MailAction.DELETE, | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.assertEqual(response.status_code, 200) | ||||||
|  |  | ||||||
|  |         returned_rule1 = MailRule.objects.get(pk=rule1.pk) | ||||||
|  |         self.assertEqual(returned_rule1.name, "Updated Name 1") | ||||||
|  |         self.assertEqual(returned_rule1.action, MailRule.MailAction.DELETE) | ||||||
							
								
								
									
										41
									
								
								src/paperless_mail/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/paperless_mail/views.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | from paperless.views import StandardPagination | ||||||
|  | from paperless_mail.models import MailAccount | ||||||
|  | from paperless_mail.models import MailRule | ||||||
|  | from paperless_mail.serialisers import MailAccountSerializer | ||||||
|  | from paperless_mail.serialisers import MailRuleSerializer | ||||||
|  | from rest_framework.permissions import IsAuthenticated | ||||||
|  | from rest_framework.viewsets import ModelViewSet | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class MailAccountViewSet(ModelViewSet): | ||||||
|  |     model = MailAccount | ||||||
|  |  | ||||||
|  |     queryset = MailAccount.objects.all().order_by("pk") | ||||||
|  |     serializer_class = MailAccountSerializer | ||||||
|  |     pagination_class = StandardPagination | ||||||
|  |     permission_classes = (IsAuthenticated,) | ||||||
|  |  | ||||||
|  |     # TODO: user-scoped | ||||||
|  |     # def get_queryset(self): | ||||||
|  |     #     user = self.request.user | ||||||
|  |     #     return MailAccount.objects.filter(user=user) | ||||||
|  |  | ||||||
|  |     # def perform_create(self, serializer): | ||||||
|  |     #     serializer.save(user=self.request.user) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class MailRuleViewSet(ModelViewSet): | ||||||
|  |     model = MailRule | ||||||
|  |  | ||||||
|  |     queryset = MailRule.objects.all().order_by("pk") | ||||||
|  |     serializer_class = MailRuleSerializer | ||||||
|  |     pagination_class = StandardPagination | ||||||
|  |     permission_classes = (IsAuthenticated,) | ||||||
|  |  | ||||||
|  |     # TODO: user-scoped | ||||||
|  |     # def get_queryset(self): | ||||||
|  |     #     user = self.request.user | ||||||
|  |     #     return MailRule.objects.filter(user=user) | ||||||
|  |  | ||||||
|  |     # def perform_create(self, serializer): | ||||||
|  |     #     serializer.save(user=self.request.user) | ||||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon