+
+
+
+
+
+
diff --git a/src/app/account-app/filters/account-filters.component.ts b/src/app/account-app/filters/account-filters.component.ts
new file mode 100644
index 000000000..780505414
--- /dev/null
+++ b/src/app/account-app/filters/account-filters.component.ts
@@ -0,0 +1,240 @@
+// --------- BEGIN RUNBOX LICENSE ---------
+// Copyright (C) 2016-2020 Runbox Solutions AS (runbox.com).
+//
+// This file is part of Runbox 7.
+//
+// Runbox 7 is free software: You can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the
+// Free Software Foundation, either version 3 of the License, or (at your
+// option) any later version.
+//
+// Runbox 7 is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Runbox 7. If not, see .
+// ---------- END RUNBOX LICENSE ----------
+
+import { Component, QueryList, ViewChildren } from '@angular/core';
+import { ReplaySubject, Subject } from 'rxjs';
+import { MatSnackBar } from '@angular/material/snack-bar';
+
+import { Filter, RunboxWebmailAPI, FilteredSender } from '../../rmmapi/rbwebmail';
+import { take, debounceTime } from 'rxjs/operators';
+import { FilterEditorComponent } from './filter-editor.component';
+
+@Component({
+ selector: 'app-account-filters-component',
+ templateUrl: './account-filters.component.html',
+})
+export class AccountFiltersComponent {
+ @ViewChildren(FilterEditorComponent) filterComponents: QueryList;
+ filters: ReplaySubject = new ReplaySubject(1);
+ shownFilters: Subject = new Subject();
+ blockedSenders: ReplaySubject = new ReplaySubject(1);
+ allowedSenders: ReplaySubject = new ReplaySubject(1);
+
+ filtersReordered: Subject = new Subject();
+
+ filterPageSize = 50;
+ filtersShown = this.filterPageSize;
+ filtersTotal: number;
+
+ constructor(
+ private rmmapi: RunboxWebmailAPI,
+ private snackbar: MatSnackBar,
+ ) {
+ this.rmmapi.getFilters().subscribe(filters => {
+ this.filters.next(filters.filters);
+ this.allowedSenders.next(filters.allowed);
+ this.blockedSenders.next(filters.blocked);
+ });
+
+ this.filters.subscribe(_ => this.updateShownFilters());
+
+ this.filtersReordered.pipe(debounceTime(1500)).subscribe(() => {
+ this.filters.pipe(take(1)).subscribe(filters => {
+ const order = filters.map(f => f.id);
+ this.rmmapi.reorderFilters(order).subscribe(() => {
+ console.log('Filters reordered');
+ });
+ });
+ });
+ }
+
+ newFilter(): void {
+ const template = {
+ id: null,
+ str: '',
+ action: 't',
+ active: true,
+ target: 'Inbox',
+ negated: false,
+ location: '0',
+ priority: -1,
+ };
+ this.updateFilters(
+ filters => [template, ...filters]
+ );
+ }
+
+ deleteFilter(target: Filter): void {
+ if (target.id) {
+ console.log(`Deleting filter #${target.id}`);
+ this.rmmapi.deleteFilter(target.id).subscribe(
+ () => console.log(`Filter #${target.id} deleted`),
+ );
+ }
+ this.updateFilters(
+ filters => filters.filter(f => f !== target)
+ );
+ }
+
+ saveFilter(existing: Filter, replacement: Filter): void {
+ console.log(`Uploading filter to server ${JSON.stringify(replacement)}`);
+ this.rmmapi.saveFilter(replacement).subscribe(
+ id => {
+ replacement.id = id; // only needed when a new one is created, but no difference to us
+ this.updateFilters(
+ filters => filters.map(f => {
+ if (f === existing) {
+ return replacement;
+ } else {
+ return f;
+ }
+ })
+ );
+ },
+ _err => this.showError(`Error ${existing.id ? 'updating' : 'creating'} filter.`),
+ );
+ }
+
+ updateFilters(transform: (_: Filter[]) => Filter[]): void {
+ this.filters.pipe(take(1)).subscribe(
+ filters => this.filters.next(transform(filters))
+ );
+ }
+
+ moveFilterUp(filter: Filter): void {
+ this.updateFilters(filters => {
+ const index = filters.findIndex(f => f === filter);
+ if (index === 0) {
+ return filters;
+ }
+ const head = filters.slice(0, index);
+ let tail = filters.slice(index + 1);
+ tail = [head.pop(), ...tail];
+ setTimeout(() => this.hilightFilter(filter), 50);
+ return [...head, filter, ...tail];
+ });
+ this.filtersReordered.next();
+ }
+
+ moveFilterDown(filter: Filter): void {
+ this.updateFilters(filters => {
+ const index = filters.findIndex(f => f === filter);
+ if (index === filters.length - 1) {
+ return filters;
+ }
+ const head = filters.slice(0, index);
+ const tail = filters.slice(index + 1);
+ setTimeout(() => this.hilightFilter(filter), 50);
+ return [...head, tail.shift(), filter, ...tail];
+ });
+ this.filtersReordered.next();
+ }
+
+ hilightFilter(filter: Filter): void {
+ this.filterComponents.find(fc => fc.filter === filter).hilight();
+ }
+
+ showAllFilters(): void {
+ this.filtersShown = Number.MAX_SAFE_INTEGER;
+ this.updateShownFilters();
+ }
+
+ showMoreFilters(): void {
+ this.filtersShown += this.filterPageSize;
+ this.updateShownFilters();
+ }
+
+ updateShownFilters(): void {
+ this.filters.pipe(take(1)).subscribe(filters => {
+ this.shownFilters.next(filters.slice(0, this.filtersShown));
+ this.filtersTotal = filters.length;
+ });
+ }
+
+ addAllowed(address: string): void {
+ this.rmmapi.whitelistSender(address).subscribe(
+ () => {
+ this.allowedSenders.pipe(take(1)).subscribe(allowed => {
+ this.allowedSenders.next(
+ allowed.concat({
+ id: address,
+ address: address,
+ })
+ );
+ });
+ },
+ _err => this.showError('Error whitelisting an address.'),
+ );
+ }
+
+ removeAllowed(id: any): void {
+ this.rmmapi.dewhitelistSender(id).subscribe(
+ () => {
+ this.allowedSenders.pipe(take(1)).subscribe(allowed => {
+ this.allowedSenders.next(
+ allowed.filter(s => s.id !== id)
+ );
+ });
+ },
+ _err => this.showError('Error dewhitelisting an address.'),
+ );
+ }
+
+ addBlocked(address: string): void {
+ const filter = {
+ id: null,
+ str: address,
+ action: 'k',
+ active: true,
+ target: '',
+ negated: false,
+ location: '1',
+ priority: -2,
+ };
+ this.rmmapi.saveFilter(filter).subscribe(
+ id => {
+ this.blockedSenders.pipe(take(1)).subscribe(blocked => {
+ this.blockedSenders.next(
+ blocked.concat({ id, address })
+ );
+ });
+ },
+ _err => this.showError('Error blocking an address.'),
+ );
+ }
+
+ removeBlocked(id: any): void {
+ this.rmmapi.deleteFilter(id).subscribe(
+ () => {
+ this.blockedSenders.pipe(take(1)).subscribe(blocked => {
+ this.blockedSenders.next(
+ blocked.filter(f => f.id !== id)
+ );
+ });
+ },
+ _err => this.showError('Error unblocking an address.'),
+ );
+ }
+
+ showError(message: string): void {
+ this.snackbar.open(message + ' Try again or contact Runbox Support', 'Ok', {
+ duration: 3000,
+ });
+ }
+}
diff --git a/src/app/account-app/filters/filter-editor.component.html b/src/app/account-app/filters/filter-editor.component.html
new file mode 100644
index 000000000..51c75359b
--- /dev/null
+++ b/src/app/account-app/filters/filter-editor.component.html
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/account-app/filters/filter-editor.component.ts b/src/app/account-app/filters/filter-editor.component.ts
new file mode 100644
index 000000000..a604d14fb
--- /dev/null
+++ b/src/app/account-app/filters/filter-editor.component.ts
@@ -0,0 +1,97 @@
+// --------- BEGIN RUNBOX LICENSE ---------
+// Copyright (C) 2016-2019 Runbox Solutions AS (runbox.com).
+//
+// This file is part of Runbox 7.
+//
+// Runbox 7 is free software: You can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the
+// Free Software Foundation, either version 3 of the License, or (at your
+// option) any later version.
+//
+// Runbox 7 is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Runbox 7. If not, see .
+// ---------- END RUNBOX LICENSE ----------
+
+import { Component, Input, OnInit, Output, EventEmitter, ViewChild, ElementRef, Renderer2 } from '@angular/core';
+import { Filter } from '../../rmmapi/rbwebmail';
+import { MessageListService } from '../../rmmapi/messagelist.service';
+import { FormGroup, FormBuilder } from '@angular/forms';
+
+@Component({
+ selector: 'app-account-filter-editor',
+ templateUrl: './filter-editor.component.html',
+})
+export class FilterEditorComponent implements OnInit {
+ @ViewChild('cardComponent', { read: ElementRef }) cardComponent: ElementRef;
+ @Input() filter: Filter;
+
+ @Output() delete: EventEmitter = new EventEmitter();
+ @Output() save: EventEmitter = new EventEmitter();
+ @Output() moveUp: EventEmitter = new EventEmitter();
+ @Output() moveDown: EventEmitter = new EventEmitter();
+
+ isNegated: boolean;
+ folders: {
+ key: string,
+ name: string,
+ }[] = [];
+ form: FormGroup;
+
+ constructor(
+ private fb: FormBuilder,
+ private renderer: Renderer2,
+ messageListService: MessageListService,
+ ) {
+ messageListService.folderListSubject.subscribe(folders => {
+ this.folders = folders.map(f => {
+ return {
+ key: f.folderPath.replace('/', '.'),
+ name: f.folderPath,
+ };
+ });
+ });
+ }
+
+ negate(): void {
+ this.isNegated = !this.isNegated;
+ this.form.get('negated').setValue(this.isNegated);
+ this.form.get('negated').markAsDirty();
+ }
+
+ ngOnInit() {
+ this.reloadForm();
+ }
+
+ reloadForm(): void {
+ this.form = this.fb.group({
+ active: this.fb.control(this.filter.active),
+ location: this.fb.control(this.filter.location),
+ negated: this.fb.control(this.filter.negated),
+ str: this.fb.control(this.filter.str),
+ action: this.fb.control(this.filter.action),
+ target: this.fb.control(this.filter.target),
+ });
+ this.isNegated = this.filter.negated;
+ }
+
+ deleteFilter(): void {
+ this.delete.emit();
+ }
+
+ saveFilter(): void {
+ const newFilter = {id: this.filter.id};
+ Object.assign(newFilter, this.form.value);
+ this.save.emit(newFilter as Filter);
+ }
+
+ hilight(): void {
+ this.renderer.addClass(this.cardComponent.nativeElement, 'hilight');
+ this.cardComponent.nativeElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ setTimeout(() => this.renderer.removeClass(this.cardComponent.nativeElement, 'hilight'), 250);
+ }
+}
diff --git a/src/app/account-app/filters/sender-list.component.html b/src/app/account-app/filters/sender-list.component.html
new file mode 100644
index 000000000..b85c5805f
--- /dev/null
+++ b/src/app/account-app/filters/sender-list.component.html
@@ -0,0 +1,36 @@
+
+
+
+ {{ sender.address }}
+
+
+
+ Add new
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/account-app/filters/sender-list.component.ts b/src/app/account-app/filters/sender-list.component.ts
new file mode 100644
index 000000000..5334a3f59
--- /dev/null
+++ b/src/app/account-app/filters/sender-list.component.ts
@@ -0,0 +1,43 @@
+// --------- BEGIN RUNBOX LICENSE ---------
+// Copyright (C) 2016-2020 Runbox Solutions AS (runbox.com).
+//
+// This file is part of Runbox 7.
+//
+// Runbox 7 is free software: You can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the
+// Free Software Foundation, either version 3 of the License, or (at your
+// option) any later version.
+//
+// Runbox 7 is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Runbox 7. If not, see .
+// ---------- END RUNBOX LICENSE ----------
+
+import { Component, Input, OnChanges, Output, EventEmitter } from '@angular/core';
+
+import { FilteredSender } from '../../rmmapi/rbwebmail';
+
+@Component({
+ selector: 'app-account-sender-list',
+ templateUrl: './sender-list.component.html',
+})
+export class SenderListComponent implements OnChanges {
+ @Input() senders: FilteredSender[];
+
+ @Output() add: EventEmitter = new EventEmitter();
+ @Output() remove: EventEmitter = new EventEmitter();
+
+ senderList: FilteredSender[];
+
+ ngOnChanges() {
+ // adding a placeholder for the input field
+ this.senderList = this.senders.concat({
+ id: null,
+ address: '',
+ });
+ }
+}
diff --git a/src/app/menu/headertoolbar.component.html b/src/app/menu/headertoolbar.component.html
index 25763ec76..7007f6941 100644
--- a/src/app/menu/headertoolbar.component.html
+++ b/src/app/menu/headertoolbar.component.html
@@ -18,7 +18,7 @@
Manager Retrieve
- Filter
+ Filter Access