From f6eb7402eb82d7677ee4a928bdec1d64906f3ce6 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Wed, 27 Sep 2023 10:02:15 +0000 Subject: [PATCH 01/15] fix(identities): Split out "username" profile and protect it Fixes #1463 --- src/app/compose/compose.component.ts | 6 +- src/app/compose/draftdesk.service.ts | 62 ++---- src/app/compose/recipients.service.ts | 6 +- src/app/mailviewer/avatar-bar.component.ts | 6 +- src/app/profiles/profile.service.ts | 137 ++++++++++++++ src/app/profiles/profile.ts | 31 --- src/app/profiles/profiles.component.html | 27 +-- src/app/profiles/profiles.component.ts | 43 ++--- src/app/profiles/profiles.default.html | 4 +- src/app/profiles/profiles.default.ts | 92 +++------ src/app/profiles/profiles.editor.modal.html | 35 ++-- src/app/profiles/profiles.editor.modal.ts | 200 ++++++++------------ src/app/profiles/profiles.lister.html | 26 ++- src/app/profiles/profiles.lister.ts | 24 +-- src/app/rmm/profile.ts | 1 + src/app/rmmapi/from_address.ts | 58 ------ src/app/rmmapi/rbwebmail.ts | 85 +++++---- src/app/start/startdesk.component.ts | 6 +- 18 files changed, 382 insertions(+), 467 deletions(-) create mode 100644 src/app/profiles/profile.service.ts delete mode 100644 src/app/profiles/profile.ts delete mode 100644 src/app/rmmapi/from_address.ts diff --git a/src/app/compose/compose.component.ts b/src/app/compose/compose.component.ts index eac5f44ef..633774865 100644 --- a/src/app/compose/compose.component.ts +++ b/src/app/compose/compose.component.ts @@ -24,7 +24,6 @@ import { Location } from '@angular/common'; import { Router } from '@angular/router'; import { RunboxWebmailAPI } from '../rmmapi/rbwebmail'; -import { FromAddress } from '../rmmapi/from_address'; import { Observable, Subscription } from 'rxjs'; import { MatLegacySnackBar as MatSnackBar } from '@angular/material/legacy-snack-bar'; import { DraftDeskService, DraftFormModel } from './draftdesk.service'; @@ -37,6 +36,7 @@ import { TinyMCEPlugin } from '../rmm/plugin/tinymce.plugin'; import { RecipientsService } from './recipients.service'; import { isValidEmailArray } from './emailvalidator'; import { MailAddressInfo } from '../common/mailaddressinfo'; +import { Identity } from '../profiles/profile.service'; import { MessageTableRowTool} from '../messagetable/messagetablerow'; import { DefaultPrefGroups, PreferencesService } from '../common/preferences.service'; import { objectEqualWithKeys } from '../common/util'; @@ -124,7 +124,7 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit { if (this.model.isUnsaved()) { this.editing = true; this.isUnsaved = true; - const from: FromAddress = this.draftDeskservice.fromsSubject.value.find((f) => + const from: Identity = this.draftDeskservice.fromsSubject.value.find((f) => f.nameAndAddress === this.model.from || f.email === this.model.from); this.has_pasted_signature = false; @@ -206,7 +206,7 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit { this.formGroup.controls.from.valueChanges .pipe(debounceTime(1000)) .subscribe((selected_from_address) => { - const from: FromAddress = this.draftDeskservice.fromsSubject.value.find((f) => + const from: Identity = this.draftDeskservice.fromsSubject.value.find((f) => f.nameAndAddress === selected_from_address); if ( this.formGroup.controls.msg_body.pristine ) { if ( this.signature && from.signature ) { diff --git a/src/app/compose/draftdesk.service.ts b/src/app/compose/draftdesk.service.ts index b53d4e236..d0bcede21 100644 --- a/src/app/compose/draftdesk.service.ts +++ b/src/app/compose/draftdesk.service.ts @@ -21,12 +21,11 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { RunboxWebmailAPI } from '../rmmapi/rbwebmail'; import { FolderListEntry } from '../common/folderlistentry'; -import { FromAddress } from '../rmmapi/from_address'; import { MessageInfo } from '../common/messageinfo'; import { MailAddressInfo } from '../common/mailaddressinfo'; import { MessageListService } from '../rmmapi/messagelist.service'; import { MessageTableRowTool} from '../messagetable/messagetablerow'; -import { RMM } from '../rmm'; +import { Identity, ProfileService } from '../profiles/profile.service'; import { from, of, BehaviorSubject } from 'rxjs'; import { map, mergeMap, bufferCount, take, distinctUntilChanged } from 'rxjs/operators'; @@ -68,7 +67,7 @@ export class DraftFormModel { message_date = null; public static create(draftId: number, - fromAddress: FromAddress, + fromAddress: Identity, to: string, subject: string, preview?: string, message_date?: Date): DraftFormModel { @@ -87,7 +86,7 @@ export class DraftFormModel { return ret; } - public static reply(mailObj, froms: FromAddress[], all: boolean, useHTML: boolean): DraftFormModel { + public static reply(mailObj, froms: Identity[], all: boolean, useHTML: boolean): DraftFormModel { const ret = new DraftFormModel(); ret.reply_to_id = mailObj.mid; ret.in_reply_to = mailObj.headers['message-id']; @@ -165,7 +164,7 @@ export class DraftFormModel { return ret; } - public static forward(mailObj, froms: FromAddress[], useHTML: boolean): DraftFormModel { + public static forward(mailObj, froms: Identity[], useHTML: boolean): DraftFormModel { const ret = new DraftFormModel(); ret.setFromForResponse(mailObj, froms); @@ -214,7 +213,7 @@ ${mailObj.sanitized_html}`; return false; } - private setFromForResponse(mailObj, froms: FromAddress[]): void { + private setFromForResponse(mailObj, froms: Identity[]): void { if (froms.length > 0) { this.from = ( [].concat(mailObj.to || []).concat(mailObj.cc || []).find( @@ -234,21 +233,20 @@ ${mailObj.sanitized_html}`; @Injectable() export class DraftDeskService { draftModels: BehaviorSubject = new BehaviorSubject([]); - fromsSubject: BehaviorSubject = new BehaviorSubject([]); + fromsSubject: BehaviorSubject = new BehaviorSubject([]); isEditing = -1; composingNewDraft: DraftFormModel; shouldReturnToPreviousPage = false; constructor(public rmmapi: RunboxWebmailAPI, private messagelistservice: MessageListService, - private http: HttpClient, - private rmm: RMM) { - this.rmm.profile.profiles.subscribe((_) => { - this.refreshFroms(); + private profileService: ProfileService, + private http: HttpClient + ) { + this.profileService.validProfiles.subscribe((profiles) => { + this.fromsSubject.next(profiles); }); - // run these at least once (rmm.blah is not a service!) - this.refreshFroms(); // Recreate drafts when froms(identities) change this.fromsSubject .subscribe(froms => { @@ -272,36 +270,8 @@ export class DraftDeskService { } // default identity for creating an email - public mainIdentity(): FromAddress { - return this.fromsSubject.value[0]; - } - - public refreshFroms() { - this.rmmapi.getFromAddress().pipe( - map((froms) => { - froms.sort((a, b) => { - if (a.type === 'main') { - return -1; - } else if (b.type === 'main') { - return 1; - } else if (a.type === 'aliases') { - return -1; - } else if (b.type === 'aliases') { - return 1; - } else { - return 0; - } - }); - froms.sort((a, b) => { - return a.priority - b.priority; - }); - return froms; - })).subscribe( - froms => this.fromsSubject.next(froms), - err => { - console.error(err); - } - ); + public mainIdentity(): Identity { + return this.profileService.composeProfile; } private refreshDrafts() { @@ -319,7 +289,7 @@ export class DraftDeskService { newDrafts.push( DraftFormModel.create( msgInfo.id, - this.fromsSubject.value[0], + this.mainIdentity(), msgInfo.to.map((addr) => addr.name === null || addr.address.indexOf(addr.name + '@') === 0 ? addr.address : addr.name + '<' + addr.address + '>').join(','), msgInfo.subject, null, msgInfo.messageDate) @@ -349,7 +319,7 @@ export class DraftDeskService { ) { const draftObj = DraftFormModel.create( -1, - this.fromsSubject.value[0], + this.mainIdentity(), '"Runbox 7 Bug Reports" ', 'Runbox 7 Bug Report' ); @@ -386,7 +356,7 @@ export class DraftDeskService { {responseType: 'text'}).toPromise(); const draftObj = DraftFormModel.create( -1, - this.fromsSubject.value[0], + this.mainIdentity(), to, "Let's have a video call" ); diff --git a/src/app/compose/recipients.service.ts b/src/app/compose/recipients.service.ts index 28dd53fef..825d3c300 100644 --- a/src/app/compose/recipients.service.ts +++ b/src/app/compose/recipients.service.ts @@ -25,7 +25,7 @@ import { ContactKind, Contact } from '../contacts-app/contact'; import { isValidEmail } from './emailvalidator'; import { MailAddressInfo } from '../common/mailaddressinfo'; import { Recipient } from './recipient'; -import { RunboxWebmailAPI } from '../rmmapi/rbwebmail'; +import { ProfileService } from '../profiles/profile.service'; import moment from 'moment'; enum RecipientOrigin { @@ -45,9 +45,9 @@ export class RecipientsService { constructor( private searchService: SearchService, private contactsService: ContactsService, - rmmapi: RunboxWebmailAPI, + profileService: ProfileService ) { - rmmapi.getFromAddress().subscribe( + profileService.validProfiles.subscribe( froms => this.ownAddresses.next(new Set(froms.map(f => f.email))), _err => this.ownAddresses.next(new Set([])), ); diff --git a/src/app/mailviewer/avatar-bar.component.ts b/src/app/mailviewer/avatar-bar.component.ts index e58a981db..24bb623d7 100644 --- a/src/app/mailviewer/avatar-bar.component.ts +++ b/src/app/mailviewer/avatar-bar.component.ts @@ -20,7 +20,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { ReplaySubject} from 'rxjs'; import { take } from 'rxjs/operators'; -import { RunboxWebmailAPI } from '../rmmapi/rbwebmail'; +import { ProfileService } from '../profiles/profile.service'; import { ContactsService } from '../contacts-app/contacts.service'; import { PreferencesService } from '../common/preferences.service'; @@ -67,13 +67,13 @@ export class AvatarBarComponent implements OnInit { constructor( preferenceService: PreferencesService, private contactsservice: ContactsService, - private rmmapi: RunboxWebmailAPI, + private profileService: ProfileService ) { preferenceService.preferences.subscribe(_ => this.ngOnChanges()); } ngOnInit() { - this.rmmapi.getFromAddress().subscribe( + this.profileService.validProfiles.subscribe( froms => this.ownAddresses.next(new Set(froms.map(f => f.email.toLowerCase()))), _err => this.ownAddresses.next(new Set([])), ); diff --git a/src/app/profiles/profile.service.ts b/src/app/profiles/profile.service.ts new file mode 100644 index 000000000..e91fabc8d --- /dev/null +++ b/src/app/profiles/profile.service.ts @@ -0,0 +1,137 @@ +// --------- BEGIN RUNBOX LICENSE --------- +// Copyright (C) 2016-2018 Runbox Solutions AS (runbox.com). +// +// This file is part of Runbox 7. +// +// Runbox 7 is free software: You can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at your +// option) any later version. +// +// Runbox 7 is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Runbox 7. If not, see . +// ---------- END RUNBOX LICENSE ---------- +import { Injectable } from '@angular/core'; +import { map } from 'rxjs/operators'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { RunboxMe, RunboxWebmailAPI } from '../rmmapi/rbwebmail'; + +export interface FromPriority { + from_priority: number; + id: number; +} + +export class Identity { + email: string; + from_name: string; + from_priority: number; + id: number; + is_signature_html: boolean; + is_smtp_enabled: boolean; + name: string; + reference_type: string; + reply_to: string; + signature: string; + type: string; + smtp_address: string; + smtp_password: string; + smtp_port: string; + smtp_username: string; + is_verified: boolean; + reference: { status: number }; + preferred_runbox_domain: string; + // FIXME: Legacy rubbish for send-folder-options + folder: string; + + public nameAndAddress: string; + + public static fromObject(obj: any): Identity { + const ret = Object.assign(new Identity(), obj); + ret.resolveNameAndAddress(); + return ret; + } + + resolveNameAndAddress() { + this.nameAndAddress = this.name ? `${this.name} <${this.email}>` : this.email; + } +} + +@Injectable({ providedIn: 'root' }) +export class ProfileService { + public profiles: BehaviorSubject = new BehaviorSubject([]); + public aliases: BehaviorSubject = new BehaviorSubject([]); + public nonAliases: BehaviorSubject = new BehaviorSubject([]); + public validProfiles: BehaviorSubject = new BehaviorSubject([]); + public composeProfile: Identity; + public me: RunboxMe; + constructor( + public rmmapi: RunboxWebmailAPI + ) { + this.refresh(); + this.rmmapi.me.subscribe(me => this.me = me); + } + + refresh() { + this.rmmapi.getProfiles().subscribe( + (res: Identity[]) => { + this.validProfiles.next(res.filter(p => p.type === 'aliases' || (p.reference_type === 'preference' && p.reference.status === 0))); + this.aliases.next(res.filter(p => p.type === 'aliases')); + this.nonAliases.next(res.filter(p => p.type !== 'aliases')); + this.composeProfile = res.find(p => p.from_priority === 0); + if (!this.composeProfile) { + this.composeProfile = res.find(p => p.type === 'main'); + } + this.profiles.next(res); + } + ); + } + + create(values): Observable { + return this.rmmapi.createProfile(values).pipe( + map((res: boolean) => { + this.refresh(); + return res; + }) + ); + } + delete(id): Observable { + return this.rmmapi.deleteProfile(id).pipe( + map((res: boolean) => { + this.refresh(); + return res; + }) + ); + } + + update(id, values): Observable { + return this.rmmapi.updateProfile(id, values).pipe( + map((res: boolean) => { + this.refresh(); + return res; + }) + ); + } + re_validate(id) { + this.rmmapi.resendValidationEmail(id).subscribe( + reply => { + this.refresh(); + if ( !reply ) { + // snackbar error msg? + } + }, + ); + } + updateFromPriorities(values: FromPriority[]) { + this.rmmapi.updateFromPriorities(values).subscribe( + (reply) => { + this.refresh(); + } + ); + } + +} diff --git a/src/app/profiles/profile.ts b/src/app/profiles/profile.ts deleted file mode 100644 index 7500c478d..000000000 --- a/src/app/profiles/profile.ts +++ /dev/null @@ -1,31 +0,0 @@ -// --------- BEGIN RUNBOX LICENSE --------- -// Copyright (C) 2016-2018 Runbox Solutions AS (runbox.com). -// -// This file is part of Runbox 7. -// -// Runbox 7 is free software: You can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by the -// Free Software Foundation, either version 3 of the License, or (at your -// option) any later version. -// -// Runbox 7 is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Runbox 7. If not, see . -// ---------- END RUNBOX LICENSE ---------- -export class Profile { - id: number; - profile: string; - name: string; - from: string; - reply_to: string; - signature: string; - - constructor(properties: any) { - const self = this; - properties.forEach( key => self[key] = properties[key] ); - } -} diff --git a/src/app/profiles/profiles.component.html b/src/app/profiles/profiles.component.html index d36793a2f..a91c74e78 100644 --- a/src/app/profiles/profiles.component.html +++ b/src/app/profiles/profiles.component.html @@ -1,8 +1,8 @@

Identities

- +

Default Identity

@@ -12,7 +12,10 @@

Default Identity

- +

Select the email you want to use as your Default Identity:

@@ -20,8 +23,8 @@

Default Identity


+ *ngIf="profileService.aliases.value.length > 0" + [profiles]="profileService.aliases.value">

Identities for Aliases

@@ -29,7 +32,7 @@

Identities for Aliases

Aliases are extra email addresses that deliver to your account, just as your main username/email address does. Below are identities you can use if you want to send messages from your aliases.

These identities can be customised by adding a different From Name, Signature or Reply-to address. You can also change the Runbox domain an alias uses.

You can't delete these identities as they are tied to your aliases.

-

+

You have reached the maximum allowed number of Runbox aliases.

To manage your aliases, please visit Email Aliases.

@@ -38,15 +41,15 @@

Identities for Aliases



+ *ngIf="profileService.nonAliases.value.length > 0" + [profiles]="profileService.nonAliases.value">

Other identities

-

Other Identities can be used when you want to send from addresses that are not part of your Runbox - account. Adding this kind of address (e.g. a work email address) will require verification via - email that you have access to that account. You can also use Other Identities if you need +

Other Identities can be used when you want to send from addresses that are not part of your Runbox + account. Adding this kind of address (e.g. a work email address) will require verification via + email that you have access to that account. You can also use Other Identities if you need additional identities for you Runbox email addresses.

Added identities require verification via an email sent to the given address.

@@ -54,7 +57,7 @@

Other identities

- {{profiles.others.length}} identities created + {{profileService.nonAliases.value.length}} identities created
diff --git a/src/app/profiles/profiles.component.ts b/src/app/profiles/profiles.component.ts index 333721762..98f3c994a 100644 --- a/src/app/profiles/profiles.component.ts +++ b/src/app/profiles/profiles.component.ts @@ -16,12 +16,12 @@ // You should have received a copy of the GNU General Public License // along with Runbox 7. If not, see . // ---------- END RUNBOX LICENSE ---------- -import { Component, Output, EventEmitter, ViewChild, OnInit } from '@angular/core'; +import { Component, Output, EventEmitter, ViewChild } from '@angular/core'; import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; import { MatLegacyPaginator as MatPaginator } from '@angular/material/legacy-paginator'; import { MatLegacySnackBar as MatSnackBar } from '@angular/material/legacy-snack-bar'; import { ProfilesEditorModalComponent } from './profiles.editor.modal'; -import { RMM, AllIdentities } from '../rmm'; +import { ProfileService } from './profile.service'; @Component({ moduleId: 'angular2/app/profiles/', @@ -29,44 +29,27 @@ import { RMM, AllIdentities } from '../rmm'; templateUrl: 'profiles.component.html' }) -export class ProfilesComponent implements OnInit { +export class ProfilesComponent { panelOpenState = false; @ViewChild(MatPaginator) paginator: MatPaginator; @Output() Close: EventEmitter = new EventEmitter(); domain; - profiles: AllIdentities; - aliases = []; - aliases_counter = {}; - aliases_unique = []; +// profiles: Identity[]; + alias_limits; dialog_ref: any; - ngOnInit() { - this.rmm.profile.profiles.subscribe(profiles => this.profiles = profiles); - } - - ev_reload_emiter (ev) { - this.load_aliases(); - this.load_profiles(); - } - constructor( public snackBar: MatSnackBar, public dialog: MatDialog, - public rmm: RMM, + public profileService: ProfileService, ) { - this.rmm.runbox_domain.load(); - this.load_profiles(); - this.load_aliases(); +// this.profileService.profiles.subscribe((profiles) => this.profiles = profiles); + // FIXME: Need to refresh this if/when we make more aliases + this.profileService.rmmapi.getAliasLimits().subscribe( + res => this.alias_limits = res + ); } - - load_aliases () { - this.rmm.alias.load(); - } - - load_profiles () { - this.rmm.profile.load(); - } - + show_error (message, action) { this.snackBar.open(message, action, { duration: 2000, @@ -79,10 +62,8 @@ export class ProfilesComponent implements OnInit { width: '600px', data: item, }); - this.dialog_ref.componentInstance.aliases_unique = this.aliases_unique; this.dialog_ref.componentInstance.is_create = true; this.dialog_ref.afterClosed().subscribe(result => { - this.load_profiles(); item = result; }); } diff --git a/src/app/profiles/profiles.default.html b/src/app/profiles/profiles.default.html index 6074fab54..ef0e7f69b 100644 --- a/src/app/profiles/profiles.default.html +++ b/src/app/profiles/profiles.default.html @@ -3,8 +3,8 @@
- - + + {{profile.nameAndAddress}} diff --git a/src/app/profiles/profiles.default.ts b/src/app/profiles/profiles.default.ts index e6fa65aaa..eb50a1c1f 100644 --- a/src/app/profiles/profiles.default.ts +++ b/src/app/profiles/profiles.default.ts @@ -17,86 +17,48 @@ // along with Runbox 7. If not, see . // ---------- END RUNBOX LICENSE ---------- -import { Component } from '@angular/core'; -import { RMM } from '../rmm'; +import { Component, Input } from '@angular/core'; +import { Identity, FromPriority, ProfileService } from './profile.service'; import { RunboxWebmailAPI } from '../rmmapi/rbwebmail'; import { MatLegacySnackBar as MatSnackBar } from '@angular/material/legacy-snack-bar'; -export interface Profile { - email: string; - id: number; - name: string; - nameAndAddress: string; - priority: number; - reply_to: string; - signature: string; - type: string; -} - @Component({ - selector: 'app-profiles-default', - styleUrls: ['profiles.default.scss'], - templateUrl: 'profiles.default.html', + selector: 'app-profiles-default', + styleUrls: ['profiles.default.scss'], + templateUrl: 'profiles.default.html', }) export class DefaultProfileComponent { field_errors: any; - profiles: Profile[]; - selected: any; - constructor(public rmm: RMM, public rmmapi: RunboxWebmailAPI, private snackBar: MatSnackBar) { - this.selectCurrentDefault(); - } + @Input() validProfiles: Identity[]; + @Input() selectedProfile: Identity; - async fetchProfiles() { - const froms = await this.rmmapi.getFromAddress().toPromise(); - // Sort emails alphabetically - this.profiles = froms.sort((a, b) => (a.email < b.email ? -1 : 1)); - } - - async selectCurrentDefault() { - await this.fetchProfiles(); - const defaultProfiles = []; - for (const profile of this.profiles) { - if (profile.priority === 0) { - defaultProfiles.push(profile); - } - } - if (defaultProfiles.length === 1) { - this.selected = defaultProfiles[0]; - } else { - this.selected = this.profiles.find(p => p.type === 'main'); - } + constructor( + public profileService: ProfileService, + public rmmapi: RunboxWebmailAPI, + private snackBar: MatSnackBar + ) { + this.profileService.profiles.subscribe((_) => + this.selectedProfile = this.profileService.composeProfile + ); } updateDefaultProfile() { - const priorities: any[] = new Array(); - const priority_data = { from_priorities: priorities }; - const type_data = { type: 'main' }; - for (const profile of this.profiles) { - if (profile === this.selected) { - const values = { id: profile.id, from_priority: 0 }; - priorities.push(values); - this.updateType(profile.id, type_data, this.field_errors); + const priorities: FromPriority[] = new Array(); + let p_value = 1; + for (const profile of this.profileService.validProfiles.value) { + let from_p: FromPriority = {"from_priority": -1, "id": profile.id }; + if (profile.id === this.selectedProfile.id) { + from_p.from_priority = 0; + profile.from_priority = 0; + priorities.push(from_p); } else { - const values = { id: profile.id, from_priority: 1 }; - priorities.push(values); + from_p.from_priority = p_value++; + profile.from_priority = from_p.from_priority; + priorities.push(from_p); } } - this.rmm.profile.updateFromPriorities(priority_data); - } - - updateType(id: number, values: { type: string }, field_errors: any) { - const req = this.rmm.profile.update(id, values, field_errors); - req.subscribe( - (reply) => { - if (reply['status'] === 'success') { - this.rmm.profile.load(); - } else if (reply['status'] === 'error') { - this.showNotification('Could not update Identity Type'); - return; - } - } - ); + this.profileService.updateFromPriorities(priorities); } showNotification(message: string, action = 'Dismiss'): void { diff --git a/src/app/profiles/profiles.editor.modal.html b/src/app/profiles/profiles.editor.modal.html index d2b5cee1e..051b6e5af 100644 --- a/src/app/profiles/profiles.editor.modal.html +++ b/src/app/profiles/profiles.editor.modal.html @@ -3,17 +3,16 @@ Create profile Edit profile - Create main profile - {{data.profile.name}} + {{identity.name}}
-
ie. James Bond @@ -24,10 +23,10 @@ + *ngIf="is_aliases_global_domain(identity) && rmm.runbox_domain.data ; else other_content"> Email - @@ -39,8 +38,8 @@ + [readonly]="( identity && identity.type == 'aliases' )" + [(ngModel)]="identity.email" (ngModelChange)="onchange_field('email')" />
ie. jamesbond@runbox.com @@ -53,7 +52,7 @@ -
ie. My main identity @@ -71,7 +70,7 @@
-
ie. noreply@runbox.com @@ -83,7 +82,7 @@ + [(ngModel)]="identity.signature" (ngModelChange)="onchange_field('signature')">
ie. @@ -98,7 +97,7 @@
- Use HTML for signature @@ -106,25 +105,21 @@ -
+
Email not validated. Check your email or - + re-send.
-

To manage your aliases, please visit Email Aliases.

+

To manage your aliases, please visit Email Aliases.

- + - - - - diff --git a/src/app/profiles/profiles.editor.modal.ts b/src/app/profiles/profiles.editor.modal.ts index d2c1ea5af..39d2b654e 100644 --- a/src/app/profiles/profiles.editor.modal.ts +++ b/src/app/profiles/profiles.editor.modal.ts @@ -20,6 +20,7 @@ import { Component, Input, Inject } from '@angular/core'; import { MatLegacyDialogRef as MatDialogRef, MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA } from '@angular/material/legacy-dialog'; import { MatLegacySnackBar as MatSnackBar } from '@angular/material/legacy-snack-bar'; +import { Identity, ProfileService } from './profile.service'; import { RMM } from '../rmm'; import { Location } from '@angular/common'; import { DraftDeskService } from '../compose/draftdesk.service'; @@ -33,15 +34,12 @@ import { TinyMCEPlugin } from '../rmm/plugin/tinymce.plugin'; export class ProfilesEditorModalComponent { @Input() value: any[]; - field_errors; + field_errors: Identity; allowed_domains = []; is_valid = false; - aliases_unique = []; - is_busy = false; - is_delete = false; + is_update = false; is_create = false; - is_create_main = false; type; is_visible_smtp_detail = false; is_different_reply_to = false; @@ -50,131 +48,101 @@ export class ProfilesEditorModalComponent { selector: any; public tinymce_plugin: TinyMCEPlugin; constructor( + public profileService: ProfileService, public rmm: RMM, private location: Location, public snackBar: MatSnackBar, public dialog_ref: MatDialogRef, public draftDeskservice: DraftDeskService, - @Inject(MAT_DIALOG_DATA) public data: any + @Inject(MAT_DIALOG_DATA) public identity: Identity ) { this.tinymce_plugin = new TinyMCEPlugin(); - if (data && data.type) { - this.type = data.type; - delete data.type; - } - if (data.profile && data.profile.email) { - this.set_localpart(data); + if (identity.email) { + this.set_localpart(identity); } - if (!data || !Object.keys(data).length || !data.profile) { - data = { profile: {} }; + if (!identity || !Object.keys(identity).length) { + identity = new Identity; const self = this; - data.profile.name = ['first_name', 'last_name'].map((attr) => { - return self.rmm.me.data[attr]; + identity.name = ['first_name', 'last_name'].map((attr) => { + return self.profileService.me[attr]; }).join(' '); } - this.data = data; - if (this.data.profile.is_signature_html) { + this.identity = identity; + if (this.identity.is_signature_html) { this.init_tinymce(); } else { - this.data.profile.is_signature_html = false; + this.identity.is_signature_html = false; } - this.check_reply_to(this.data); + this.check_reply_to(this.identity); } - check_reply_to(data) { - if (data && data.profile.email && data.profile.reply_to && - data.profile.reply_to !== data.profile.email) { + check_reply_to(identity) { + if (identity && identity.email && identity.reply_to && + identity.reply_to !== identity.email) { this.is_different_reply_to = true; return; } this.is_different_reply_to = false; } - set_localpart(data) { - if (data.profile.email.match(/@/g)) { - this.localpart = data.profile.email.replace(/@.+/g, ''); + set_localpart(identity) { + if (identity.email.match(/@/g)) { + this.localpart = identity.email.replace(/@.+/g, ''); const regex = /(.+)@(.+)/g; - const match = regex.exec(data.profile.email); - data.profile.preferred_runbox_domain = match[2]; + const match = regex.exec(identity.email); + identity.preferred_runbox_domain = match[2]; } else { - this.localpart = data.profile.email; - data.profile.preferred_runbox_domain = this.localpart; + this.localpart = identity.email; + identity.preferred_runbox_domain = this.localpart; } } save() { - if (this.is_create || this.is_create_main) { + if (this.is_create) { this.create(); } else { this.update(); } } create() { - this.is_busy = true; - const data = this.data; + const identity = this.identity; const values = { - name: data.profile.name, - email: data.profile.email, - from_name: data.profile.from_name, - reply_to: data.profile.reply_to, - signature: data.profile.signature, - smtp_address: data.profile.smtp_address, - smtp_port: data.profile.smtp_port, - smtp_username: data.profile.smtp_username, - smtp_password: data.profile.smtp_password, + name: identity.name, + email: identity.email, + from_name: identity.from_name, + reply_to: identity.reply_to, + signature: identity.signature, + smtp_address: identity.smtp_address, + smtp_port: identity.smtp_port, + smtp_username: identity.smtp_username, + smtp_password: identity.smtp_password, type: this.type, - is_signature_html: (data.profile.is_signature_html ? 1 : 0), - is_smtp_enabled: (data.profile.is_smtp_enabled ? 1 : 0), + is_signature_html: (identity.is_signature_html ? 1 : 0), + is_smtp_enabled: (identity.is_smtp_enabled ? 1 : 0), }; - const req = this.rmm.profile.create(values, this.field_errors); - req.subscribe(reply => { - if (reply['status'] === 'success') { - this.rmm.profile.load(); - this.close(); - return; - } - this.is_busy = false; - }); + this.profileService.create(values).subscribe( + res => this.close + ); } delete() { - this.is_busy = true; - const data = this.data; - const req = this.rmm.profile.delete(data.profile.id); - req.subscribe(reply => { - if (reply['status'] === 'success') { - this.rmm.profile.load(); - this.close(); - return; - } else if (reply['status'] === 'error') { - this.show_error(reply['errors'].join(' '), 'Dismiss'); - } - this.is_busy = false; - }); + const identity = this.identity; + this.profileService.delete(identity.id).subscribe( + res => this.close() + ); } update() { - this.is_busy = true; - const data = this.data; + const identity = this.identity; const values = { - name: data.profile.name, - email: data.profile.email, - from_name: data.profile.from_name, - reply_to: data.profile.reply_to, - signature: data.profile.signature, - smtp_address: data.profile.smtp_address, - smtp_port: data.profile.smtp_port, - smtp_username: data.profile.smtp_username, - smtp_password: data.profile.smtp_password, - is_signature_html: (data.profile.is_signature_html ? 1 : 0), - is_smtp_enabled: (data.profile.is_smtp_enabled ? 1 : 0), + name: identity.name, + email: identity.email, + from_name: identity.from_name, + reply_to: identity.reply_to, + signature: identity.signature, + smtp_address: identity.smtp_address, + smtp_port: identity.smtp_port, + smtp_username: identity.smtp_username, + smtp_password: identity.smtp_password, + is_signature_html: (identity.is_signature_html ? 1 : 0), + is_smtp_enabled: (identity.is_smtp_enabled ? 1 : 0), }; - const req = this.rmm.profile.update(this.data.profile.id, values, this.field_errors); - req.subscribe(reply => { - this.is_busy = false; - if (reply['status'] === 'success') { - this.rmm.profile.load(); - this.close(); - return; - } else { - if (reply['field_errors']) { - this.field_errors = reply['field_errors']; - } - } - }); + this.profileService.update(this.identity.id, values).subscribe( + res => this.close() + ); } close() { this.dialog_ref.close({}); @@ -189,10 +157,10 @@ export class ProfilesEditorModalComponent { this.field_errors[field] = []; } if (field === 'preferred_runbox_domain') { - const data = this.data; - const selected_domain = data.profile.preferred_runbox_domain; + const identity = this.identity; + const selected_domain = identity.preferred_runbox_domain; ['email'].forEach((attr) => { - let email = data.profile[attr]; + let email = identity[attr]; if (email && email.match(/@/g)) { let is_replaced = false; this.rmm.runbox_domain.data @@ -202,25 +170,25 @@ export class ProfilesEditorModalComponent { const rgx = '@' + runbox_domain + '$'; const re = new RegExp(rgx, 'g'); if (email.match(re)) { - email = data.profile[attr].replace(re, '@' + selected_domain); - this.data.profile[attr] = email; + email = identity[attr].replace(re, '@' + selected_domain); + this.identity[attr] = email; is_replaced = true; } }); } else { - this.data.profile[attr] = [data.profile[attr], selected_domain].join('@'); + this.identity[attr] = [identity[attr], selected_domain].join('@'); } }); } if (field === 'is_different_reply_to') { if (!this.is_different_reply_to) { - this.data.profile.reply_to = ''; + this.identity.reply_to = ''; } } } get_form_field_style() { const styles = {}; - if (this.data.profile && this.data.profile.type === 'aliases') { + if (this.identity && this.identity.type === 'aliases') { styles['background'] = '#dedede'; } return styles; @@ -232,12 +200,12 @@ export class ProfilesEditorModalComponent { this.is_visible_smtp_detail = false; } } - is_aliases_global_domain(data) { - return (data.profile.reference_type === 'aliases' && !data.profile.email.match(/@/g)) - || (data.profile.reference_type === 'aliases' && data.profile.email && this.global_domains().filter((d) => { + is_aliases_global_domain(identity) { + return (identity.reference_type === 'aliases' && !identity.email.match(/@/g)) + || (identity.reference_type === 'aliases' && identity.email && this.global_domains().filter((d) => { const rgx = d.name; const re = new RegExp(rgx, 'g'); - if (data.profile.email.match(re)) { + if (identity.email.match(re)) { return true; } return false; @@ -251,7 +219,7 @@ export class ProfilesEditorModalComponent { } } toggle_signature_html() { - if (this.data.profile.is_signature_html) { + if (this.identity.is_signature_html) { this.init_tinymce(); } else { this.hide_tinymce(); @@ -259,13 +227,12 @@ export class ProfilesEditorModalComponent { } hide_tinymce() { if (this.editor) { - this.data.profile.signature = this.editor.getContent({ format: 'text' }); + this.identity.signature = this.editor.getContent({ format: 'text' }); this.tinymce_plugin.remove(this.editor); this.editor = null; } } init_tinymce() { - this.is_busy = true; const self = this; this.selector = `html-editor-${Math.floor(Math.random() * 10000000000)}`; const options = { @@ -274,14 +241,13 @@ export class ProfilesEditorModalComponent { setup: editor => { self.editor = editor; editor.on('Change', () => { - self.data.profile.signature = editor.getContent(); + self.identity.signature = editor.getContent(); }); }, init_instance_callback: (editor) => { - this.is_busy = false; editor.setContent( - self.data.profile.signature ? - self.data.profile.signature.replace(/\n/g, '
\n') : + self.identity.signature ? + self.identity.signature.replace(/\n/g, '
\n') : '' ); } @@ -289,16 +255,6 @@ export class ProfilesEditorModalComponent { this.tinymce_plugin.create(options); } resend_validate_email(id) { - const req = this.rmm.profile.resend(id); - req.subscribe( - data => { - const reply = data; - if (reply['status'] === 'success') { - this.show_error('Email validation sent', 'Dismiss'); - this.rmm.profile.load(); - return; - } - }, - ); + this.profileService.re_validate(id); } } diff --git a/src/app/profiles/profiles.lister.html b/src/app/profiles/profiles.lister.html index 5e964e363..daed822af 100644 --- a/src/app/profiles/profiles.lister.html +++ b/src/app/profiles/profiles.lister.html @@ -2,14 +2,14 @@ - + Email: - {{item.profile.email}} + {{item.email}} @@ -18,35 +18,31 @@ From Name: - {{item.profile.from_name}} + {{item.from_name}} - + Origin: - - {{ 'Runbox 6 Folder: ' + item.profile.reference.folder}} + + {{ 'Runbox 6 Folder: ' + item.reference.folder}} Reply To: - {{item.profile.reply_to}} + {{item.reply_to}} Signature: - {{item.profile.signature}} + {{item.signature}} - - You are not the owner of this alias. Changes will not be saved. + + Username profile + *ngIf="item.reference_type === 'preference' && item.reference.status === 1"> Email not validated. diff --git a/src/app/profiles/profiles.lister.ts b/src/app/profiles/profiles.lister.ts index d2bf47890..11162085c 100644 --- a/src/app/profiles/profiles.lister.ts +++ b/src/app/profiles/profiles.lister.ts @@ -16,12 +16,11 @@ // 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, Output, EventEmitter } from '@angular/core'; +import { Component, Input } from '@angular/core'; import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; import { MatLegacySnackBar as MatSnackBar } from '@angular/material/legacy-snack-bar'; import { ProfilesEditorModalComponent } from './profiles.editor.modal'; -import { RMM } from '../rmm'; import { MobileQueryService, ScreenSize } from '../mobile-query.service'; @Component({ @@ -30,25 +29,22 @@ import { MobileQueryService, ScreenSize } from '../mobile-query.service'; templateUrl: 'profiles.lister.html', }) export class ProfilesListerComponent { - @Input() values: any[]; - @Output() ev_reload = new EventEmitter(); + @Input() profiles: any[]; private dialog_ref: any; mobile: boolean; constructor( public dialog: MatDialog, - public rmm: RMM, public snackBar: MatSnackBar, mobileQuery: MobileQueryService, ) { - this.rmm.me.load(); this.mobile = mobileQuery.screenSize === ScreenSize.Phone; mobileQuery.screenSizeChanged.subscribe(size => this.mobile = size === ScreenSize.Phone); } edit(item): void { - item = JSON.parse(JSON.stringify(item)); + //item = JSON.parse(JSON.stringify(item)); this.dialog_ref = this.dialog.open(ProfilesEditorModalComponent, { width: '600px', data: item, @@ -57,24 +53,10 @@ export class ProfilesListerComponent { this.dialog_ref.componentInstance.is_update = true; this.dialog_ref.componentInstance.css_class = 'update'; this.dialog_ref.afterClosed().subscribe((result) => { - this.ev_reload.emit('updated'); item = result; }); } - delete(i, item) { - this.dialog_ref = this.dialog.open(ProfilesEditorModalComponent, { - width: '600px', - data: item, - }); - this.dialog_ref.componentInstance.is_delete = true; - this.dialog_ref.afterClosed().subscribe((result) => { - if (this.dialog_ref.componentInstance.has_deleted) { - this.ev_reload.emit('deleted'); - } - }); - } - show_error(message, action) { this.snackBar.open(message, action, { duration: 2000, diff --git a/src/app/rmm/profile.ts b/src/app/rmm/profile.ts index d65042559..b4e6aba52 100644 --- a/src/app/rmm/profile.ts +++ b/src/app/rmm/profile.ts @@ -36,6 +36,7 @@ export class Identity { smtp_password: string; smtp_port: string; smtp_username: string; + is_verified: boolean; } export class AllIdentities { diff --git a/src/app/rmmapi/from_address.ts b/src/app/rmmapi/from_address.ts deleted file mode 100644 index cf14b8752..000000000 --- a/src/app/rmmapi/from_address.ts +++ /dev/null @@ -1,58 +0,0 @@ -// --------- BEGIN RUNBOX LICENSE --------- -// Copyright (C) 2016-2018 Runbox Solutions AS (runbox.com). -// -// This file is part of Runbox 7. -// -// Runbox 7 is free software: You can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by the -// Free Software Foundation, either version 3 of the License, or (at your -// option) any later version. -// -// Runbox 7 is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Runbox 7. If not, see . -// ---------- END RUNBOX LICENSE ---------- - -export class FromAddress { - public email: string; - public reply_to: string; - public id: number; - public folder: string; - public name: string; - public signature: string; - public is_signature_html: boolean; - public type: string; - public priority: number; - - public nameAndAddress: string; - - public static fromNameAndAddress(name: string, address: string): FromAddress { - const ret = new FromAddress(); - ret.name = name; - ret.email = address; - ret.resolveNameAndAddress(); - return ret; - } - - public static fromObject(obj: any): FromAddress { - const ret = Object.assign(new FromAddress(), obj); - ret.resolveNameAndAddress(); - return ret; - } - - public static fromEmailAddress(email): FromAddress { - const ret = new FromAddress(); - ret.email = email; - ret.reply_to = email; - return ret; - } - - private resolveNameAndAddress() { - this.nameAndAddress = this.name ? `${this.name} <${this.email}>` : this.email; - } - -} diff --git a/src/app/rmmapi/rbwebmail.ts b/src/app/rmmapi/rbwebmail.ts index 7bcba45e3..b25bb794a 100644 --- a/src/app/rmmapi/rbwebmail.ts +++ b/src/app/rmmapi/rbwebmail.ts @@ -35,7 +35,7 @@ import { filter, map, mergeMap } from 'rxjs/operators'; import { HttpClient, HttpResponse } from '@angular/common/http'; import { RunboxLocale } from '../rmmapi/rblocale'; import { RMM } from '../rmm'; -import { FromAddress } from './from_address'; +import { Identity, FromPriority } from '../profiles/profile.service'; import { MessageCache } from './messagecache'; import { LRUMessageCache } from './lru-message-cache'; import moment from 'moment'; @@ -488,40 +488,61 @@ export class RunboxWebmailAPI { map((res: any) => res.result as MessageFields)); } - public getFromAddress(): Observable { - return this.http.get('/rest/v1/profiles/verified').pipe( + public getProfiles(): Observable { + return this.http.get('/rest/v1/profiles').pipe( map((res: any) => { - const results = []; - Object.keys(res['result']).forEach( (k) => { - res['result'][k].forEach( (item) => { - const profile = FromAddress.fromObject({ - id: item.profile.id, - email: item.profile.email, - reply_to: item.profile.reply_to, - name: item.profile.from_name, - signature: item.profile.signature, - type: k, - priority: item.profile.from_priority, - }); - results.push(profile); - }); - }); - return results; - }) - ); + return res.results.map(p => Identity.fromObject(p)); + })); } - public getAliases(): Observable { - return this.http.get('/ajax/aliases') - .pipe( - map((res: any) => res.aliases), - map((aliases: any[]) => - aliases.map((alias) => new Alias(alias.id, - alias.localpart, - alias.name, - alias.localpart + '@' + alias.name)) - ) - ); + public createProfile(profileData: any): Observable { + const req = this.http.post( + '/rest/v1/profile', + profileData + ).pipe(share()); + this.subscribeShowBackendErrors(req); + return req.pipe(filter((res: any) => res.status === 'success')); + } + + // FIXME: This should be PATCH + public updateProfile(profileId: number, profileData: any): Observable { + const req = this.http.put( + '/rest/v1/profile/' + profileId, + profileData + ).pipe(share()); + this.subscribeShowBackendErrors(req); + return req.pipe(filter((res: any) => res.status === 'success')); + + } + + public deleteProfile(profileId: number): Observable { + const req = this.http.delete( + '/rest/v1/profile/' + profileId + ).pipe(share()); + this.subscribeShowBackendErrors(req); + return req.pipe(filter((res: any) => res.status === 'success')); + } + + // FIXME: This should be a POST + public resendValidationEmail(profileId: number): Observable { + const req = this.http.put( + '/rest/v1/profile/' + profileId + '/resend_validation_email', + {} + ).pipe(share()); + this.subscribeShowBackendErrors(req); + return req.pipe(filter((res: any) => res.status === 'success')); + } + + public updateFromPriorities(priorities: FromPriority[]) { + const req = this.http.post( + '/rest/v1/profile/from_priority/', {"from_priorities": priorities } + ).pipe(share()); + this.subscribeShowBackendErrors(req); + return req.pipe(filter((res: any) => res.status === 'success')); + } + + public getAliasLimits(): Observable { + return this.http.get('/rest/v1/aliases/limits'); } public getRunboxDomains(): Observable { diff --git a/src/app/start/startdesk.component.ts b/src/app/start/startdesk.component.ts index dc37722cc..2721d1489 100644 --- a/src/app/start/startdesk.component.ts +++ b/src/app/start/startdesk.component.ts @@ -28,7 +28,7 @@ import { SearchService, SearchIndexDocumentData } from '../xapian/searchservice' import { isValidEmail } from '../compose/emailvalidator'; import { filter, take } from 'rxjs/operators'; import { ReplaySubject } from 'rxjs'; -import { RunboxWebmailAPI } from '../rmmapi/rbwebmail'; +import { ProfileService } from '../profiles/profile.service'; import { UsageReportsService } from '../common/usage-reports.service'; export interface ContactHilights { @@ -105,13 +105,13 @@ export class StartDeskComponent implements OnInit { constructor( private cdr: ChangeDetectorRef, private searchService: SearchService, - private rmmapi: RunboxWebmailAPI, + private profileService: ProfileService, private usage: UsageReportsService, ) { } ngOnInit() { this.usage.report('overview-desk'); - this.rmmapi.getFromAddress().subscribe( + this.profileService.validProfiles.subscribe( froms => this.ownAddresses.next(new Set(froms.map(f => f.email.toLowerCase()))), _err => this.ownAddresses.next(new Set([])), ); From 00be1963840d1f9c1f2092e2b89f31c131c7c03b Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Mon, 16 Oct 2023 10:49:11 +0000 Subject: [PATCH 02/15] fix(identities): Show identity type on edit, where no delete button --- src/app/profiles/profiles.editor.modal.html | 11 ++++++++++- src/app/profiles/profiles.lister.html | 3 --- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/app/profiles/profiles.editor.modal.html b/src/app/profiles/profiles.editor.modal.html index 051b6e5af..16749cf1b 100644 --- a/src/app/profiles/profiles.editor.modal.html +++ b/src/app/profiles/profiles.editor.modal.html @@ -115,7 +115,16 @@ - + + + Username Identity + + + Alias + + + Default Identity + diff --git a/src/app/profiles/profiles.lister.html b/src/app/profiles/profiles.lister.html index daed822af..5a125f526 100644 --- a/src/app/profiles/profiles.lister.html +++ b/src/app/profiles/profiles.lister.html @@ -38,9 +38,6 @@ {{item.signature}} - - Username profile - Email not validated. From b0a0435da3d496da550a919b37a0c133bb984a5d Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Tue, 17 Oct 2023 16:11:53 +0000 Subject: [PATCH 03/15] test(identities): Fixes unit tests after API changes --- src/app/compose/draftdesk.service.spec.ts | 30 +++++++++---------- src/app/compose/recipients.service.spec.ts | 2 +- .../singlemailviewer.component.spec.ts | 2 +- src/app/profiles/profile.service.ts | 6 ++++ 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/app/compose/draftdesk.service.spec.ts b/src/app/compose/draftdesk.service.spec.ts index e60d8b570..7c39df341 100644 --- a/src/app/compose/draftdesk.service.spec.ts +++ b/src/app/compose/draftdesk.service.spec.ts @@ -17,8 +17,8 @@ // along with Runbox 7. If not, see . // ---------- END RUNBOX LICENSE ---------- -import {DraftFormModel} from './draftdesk.service'; -import { FromAddress } from '../rmmapi/from_address'; +import { DraftFormModel } from './draftdesk.service'; +import { Identity } from '../profiles/profile.service'; import { MailAddressInfo } from '../common/mailaddressinfo'; @@ -45,7 +45,7 @@ describe('DraftDesk', () => { rawtext: 'blabla\nabcde', html: '

blabla

abcde

' }, - [ FromAddress.fromEmailAddress('to@runbox.com')], + [ Identity.fromEmailAddress('to@runbox.com')], false); expect(draft.subject).toBe('Fwd: Test subject'); @@ -84,7 +84,7 @@ blabla\nabcde`); html: '

blabla

abcde

', sanitized_html: '

blabla

abcde

' }, - [ FromAddress.fromEmailAddress('to@runbox.com')], + [ Identity.fromEmailAddress('to@runbox.com')], true); expect(draft.subject).toBe('Fwd: Test subject'); @@ -137,7 +137,7 @@ Subject: Test subject
rawtext: 'blabla\nabcde', html: '

blabla

abcde

' }, - [ FromAddress.fromEmailAddress('to@runbox.com')], + [ Identity.fromEmailAddress('to@runbox.com')], false, false); expect(draft.subject).toBe('Re: Test subject'); @@ -167,7 +167,7 @@ Subject: Test subject
rawtext: 'blabla\nabcde', html: '

blabla

abcde

' }, - [ FromAddress.fromEmailAddress('to@runbox.com')], + [ Identity.fromEmailAddress('to@runbox.com')], false, false); expect(draft.subject).toBe('Re: Test subject'); @@ -204,7 +204,7 @@ Subject: Test subject
rawtext: 'blabla\nabcde', html: '

blabla

abcde

' }, - [ FromAddress.fromEmailAddress('to@runbox.com')], + [ Identity.fromEmailAddress('to@runbox.com')], false, false); expect(draft.subject).toBe('Re: Test subject'); @@ -245,7 +245,7 @@ Subject: Test subject
rawtext: 'blabla\nabcde', html: '

blabla

abcde

' }, - [ FromAddress.fromEmailAddress('to@runbox.com')], + [ Identity.fromEmailAddress('to@runbox.com')], true, false); expect(draft.subject).toBe('Re: Test subject'); @@ -276,7 +276,7 @@ Subject: Test subject
rawtext: 'blabla\nabcde', html: '

blabla

abcde

' }, - [ FromAddress.fromEmailAddress('to@runbox.com')], + [ Identity.fromEmailAddress('to@runbox.com')], true, false); expect(draft.subject).toBe('Re: Test subject'); @@ -304,7 +304,7 @@ Subject: Test subject
text: 'blabla\nabcde', rawtext: 'blabla\nabcde' }, - [ FromAddress.fromEmailAddress('to@runbox.com') ], + [ Identity.fromEmailAddress('to@runbox.com') ], true, false); expect(draft.subject).toBe('Re: Test subject'); @@ -332,7 +332,7 @@ Subject: Test subject
text: 'blabla\nabcde', rawtext: 'blabla\nabcde' }, - [ FromAddress.fromEmailAddress('to@runbox.com') ], + [ Identity.fromEmailAddress('to@runbox.com') ], true, false); const replydraft = DraftFormModel.reply({ @@ -351,7 +351,7 @@ Subject: Test subject
text: draft.msg_body, rawtext: draft.msg_body }, - [ FromAddress.fromEmailAddress('from@runbox.com') ], + [ Identity.fromEmailAddress('from@runbox.com') ], false, false); expect(replydraft.subject).toBe('Re: Test subject'); @@ -370,7 +370,7 @@ Subject: Test subject
// compose?new=true let draft = DraftFormModel.create( -1, - FromAddress.fromEmailAddress('to@runbox.com'), + Identity.fromEmailAddress('to@runbox.com'), null, ''); expect(draft.isUnsaved()).toBe(true); @@ -378,7 +378,7 @@ Subject: Test subject
// Link on contact page: draft = DraftFormModel.create( -1, - FromAddress.fromEmailAddress('to@runbox.com'), + Identity.fromEmailAddress('to@runbox.com'), '"Test Runbox" ', ''); expect(draft.isUnsaved()).toBe(true); @@ -386,7 +386,7 @@ Subject: Test subject
// refreshDrafts draft = DraftFormModel.create( 12345, - FromAddress.fromEmailAddress('to@runbox.com'), + Identity.fromEmailAddress('to@runbox.com'), '"Test Runbox" ', 'Some blahblah'); expect(draft.isUnsaved()).toBe(false); diff --git a/src/app/compose/recipients.service.spec.ts b/src/app/compose/recipients.service.spec.ts index 8cb1dc2a1..13251ff1a 100644 --- a/src/app/compose/recipients.service.spec.ts +++ b/src/app/compose/recipients.service.spec.ts @@ -126,7 +126,7 @@ export class ContactsServiceMock { export class RunboxWebMailAPIMock { public me = of({ uid: 33 }); - public getFromAddress = () => of(['testuser@runbox.com']); + public getProfiles = () => of([{ 'email':'testuser@runbox.com'}]); } describe('RecipientsService', () => { diff --git a/src/app/mailviewer/singlemailviewer.component.spec.ts b/src/app/mailviewer/singlemailviewer.component.spec.ts index b4766bf69..54a40c99c 100644 --- a/src/app/mailviewer/singlemailviewer.component.spec.ts +++ b/src/app/mailviewer/singlemailviewer.component.spec.ts @@ -129,7 +129,7 @@ describe('SingleMailViewerComponent', () => { } }, { provide: RunboxWebmailAPI, useValue: { me: of({ uid: 9876 }), - getFromAddress() { return of([]); }, + getProfiles() { return of([]); }, getMessageContents(messageId: number): Observable { console.log('Get message contents for', messageId); return of(Object.assign(new MessageContents(), { diff --git a/src/app/profiles/profile.service.ts b/src/app/profiles/profile.service.ts index e91fabc8d..f838b4c8b 100644 --- a/src/app/profiles/profile.service.ts +++ b/src/app/profiles/profile.service.ts @@ -55,6 +55,12 @@ export class Identity { ret.resolveNameAndAddress(); return ret; } + public static fromEmailAddress(email): Identity { + const ret = new Identity(); + ret.email = email; + ret.reply_to = email; + return ret; + } resolveNameAndAddress() { this.nameAndAddress = this.name ? `${this.name} <${this.email}>` : this.email; From d0d0115650f85edc2246a13b7639ff21c5177bb4 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Tue, 17 Oct 2023 16:14:48 +0000 Subject: [PATCH 04/15] fix(drafts): Only display CurrentMax most recent Drafts --- src/app/compose/draftdesk.component.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/compose/draftdesk.component.ts b/src/app/compose/draftdesk.component.ts index d1991cac7..0c64d6597 100644 --- a/src/app/compose/draftdesk.component.ts +++ b/src/app/compose/draftdesk.component.ts @@ -101,6 +101,8 @@ export class DraftDeskComponent implements OnInit { } }); this.draftModelsInView.splice(0, 0, ...newEntries); + this.draftModelsInView.sort((a,b) => a.mid < b.mid ? -1 : 1); + this.draftModelsInView = this.draftModelsInView.slice(0, this.currentMaxDraftsInView); deletedEntries.forEach( (dMsgId) => this.draftModelsInView.splice(this.draftModelsInView.findIndex((dMsg) => dMsg.mid === dMsgId), 1)); } else { From 2be2247d5dff00af899924dbf3253254f289ee1d Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Tue, 17 Oct 2023 16:24:40 +0000 Subject: [PATCH 05/15] test(all): Enable e2e test debugging/logging --- e2e/cypress/support/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/e2e/cypress/support/index.js b/e2e/cypress/support/index.js index f65e7f7cd..5d5415faf 100644 --- a/e2e/cypress/support/index.js +++ b/e2e/cypress/support/index.js @@ -1 +1,2 @@ -import './commands' \ No newline at end of file +import './commands'; +require('cypress-terminal-report/src/installLogsCollector')(); From fa87cb206bf109310e26be14c52ebc077d0bc019 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Tue, 17 Oct 2023 16:26:10 +0000 Subject: [PATCH 06/15] test(identities): Updates mock test data to reflect api changes --- e2e/mockserver/mockserver.ts | 228 +++++++++++++++++------------------ 1 file changed, 109 insertions(+), 119 deletions(-) diff --git a/e2e/mockserver/mockserver.ts b/e2e/mockserver/mockserver.ts index 8440e5266..a04a2e20a 100644 --- a/e2e/mockserver/mockserver.ts +++ b/e2e/mockserver/mockserver.ts @@ -311,6 +311,9 @@ END:VCALENDAR } }, 1000); break; + case '/rest/v1/aliases/limits': + response.end(JSON.stringify({ "total": 10, "current": 4})); + break; case '/rest/v1/profiles': response.end(JSON.stringify(this.profiles_verified())); break; @@ -345,12 +348,6 @@ END:VCALENDAR case '/rest/v1/calendar/events_raw': this.handleEvents(request, response); break; - case '/ajax/from_address': - response.end(JSON.stringify(this.from_address())); - break; - case '/ajax/aliases': - response.end(JSON.stringify({ 'status': 'success', 'aliases': [] })); - break; case '/rest/v1/email_folder/create': this.createFolder(request, response); break; @@ -796,34 +793,32 @@ END:VCALENDAR profiles_verified() { return { - 'result': { - 'aliases': [{ - 'profile': { - 'smtp_username': null, - 'email': 'a2@example.com', - 'reference_type': 'aliases', - 'id': 16455, - 'smtp_port': null, - 'smtp_address': null, - 'is_smtp_enabled': 0, - 'signature': null, - 'reference': {}, - 'reply_to': 'a2@example.com', - 'name': 'a2@example.com', - 'smtp_password': null, - 'from_name': 'Hallucinogen', - 'type': 'aliases' - } - }, { - 'profile': { - 'id': 16456, - 'email': 'aa1@example.com', - 'reference_type': 'aliases', - 'smtp_username': null, - 'from_name': 'Astrix', - 'smtp_password': null, - 'type': 'aliases', - 'reference': { + 'result': + [{ + 'smtp_username': null, + 'email': 'a2@example.com', + 'reference_type': 'aliases', + 'id': 16455, + 'smtp_port': null, + 'smtp_address': null, + 'is_smtp_enabled': 0, + 'signature': null, + 'reference': {}, + 'reply_to': 'a2@example.com', + 'name': 'a2@example.com', + 'smtp_password': null, + 'from_name': 'Hallucinogen', + 'type': 'aliases' + }, + { + 'id': 16456, + 'email': 'aa1@example.com', + 'reference_type': 'aliases', + 'smtp_username': null, + 'from_name': 'Astrix', + 'smtp_password': null, + 'type': 'aliases', + 'reference': { 'domainid': null, 'id': 278, 'localpart': 'aa1', @@ -833,92 +828,87 @@ END:VCALENDAR 'status': 6, 'id': 16, 'name': 'example.com' - } - } - } - }, { - 'profile': { - 'smtp_username': null, - 'email': 'testmail@testmail.com', - 'reference_type': 'aliases', - 'id': 16457, - 'smtp_port': null, - 'smtp_address': null, - 'is_smtp_enabled': 0, - 'signature': null, - 'reference': {}, - 'name': 'John Doe', - 'smtp_password': null, - 'from_name': 'John Doe', - 'type': 'aliases' - } - }], - 'others': [{ - 'profile': { - 'smtp_password': null, - 'from_name': 'Electric Universe', - 'type': 'external_email', - 'name': 'Electric Universe', - 'reference': { - 'save_sent': 'n', - 'signature': 'xxx', - 'use_sig_for_reply': 'NO', - 'reply_to': 'admin@runbox.com', - 'name': 'Electric Universe', - 'default_bcc': '', - 'email': 'admin@runbox.com', - 'msg_per_page': 0, - 'folder': 'Encoding Test', - 'sig_above': 'NO', - 'charset': null, - 'comp_new_window': null, - 'status': 0 }, - 'reply_to': 'admin@runbox.com', - 'smtp_address': null, - 'is_smtp_enabled': 0, - 'signature': 'xxx', - 'smtp_port': null, - 'id': 16448, - 'email': 'admin@runbox.com', - 'reference_type': 'preference', - 'smtp_username': null - } - }, { - 'profile': { - 'smtp_address': null, - 'signature': '

ą

\r\n

eex

', - 'is_smtp_enabled': 0, - 'smtp_port': null, - 'from_name': 'folder1', - 'smtp_password': null, - 'type': 'external_email', - 'reply_to': 'admin@runbox.com', - 'reference': { - 'comp_new_window': null, - 'status': 0, - 'charset': null, - 'folder': 'LALA', - 'sig_above': 'NO', - 'email': 'admin@runbox.com', - 'msg_per_page': 0, - 'name': 'folder1', - 'reply_to': 'admin@runbox.com', - 'default_bcc': '', - 'use_sig_for_reply': 'YES', - 'signature': '

ą

\r\n

eex

', - 'save_sent': 'n' - }, - 'name': 'folder1', - 'email': 'admin@runbox.com', - 'reference_type': 'preference', - 'smtp_username': null, - 'id': 16450 - } - }], - 'main': [] - } - }; + }, + }, + { + 'smtp_username': null, + 'email': 'testmail@testmail.com', + 'reference_type': 'aliases', + 'id': 16457, + 'smtp_port': null, + 'smtp_address': null, + 'is_smtp_enabled': 0, + 'signature': null, + 'reference': {}, + 'name': 'John Doe', + 'smtp_password': null, + 'from_name': 'John Doe', + 'type': 'aliases' + }, + { + 'smtp_password': null, + 'from_name': 'Electric Universe', + 'type': 'external_email', + 'name': 'Electric Universe', + 'reference': { + 'save_sent': 'n', + 'signature': 'xxx', + 'use_sig_for_reply': 'NO', + 'reply_to': 'admin@runbox.com', + 'name': 'Electric Universe', + 'default_bcc': '', + 'email': 'admin@runbox.com', + 'msg_per_page': 0, + 'folder': 'Encoding Test', + 'sig_above': 'NO', + 'charset': null, + 'comp_new_window': null, + 'status': 0 + }, + 'reply_to': 'admin@runbox.com', + 'smtp_address': null, + 'is_smtp_enabled': 0, + 'signature': 'xxx', + 'smtp_port': null, + 'id': 16448, + 'email': 'admin@runbox.com', + 'reference_type': 'preference', + 'smtp_username': null + }, + { + + 'smtp_address': null, + 'signature': '

ą

\r\n

eex

', + 'is_smtp_enabled': 0, + 'smtp_port': null, + 'from_name': 'folder1', + 'smtp_password': null, + 'type': 'external_email', + 'reply_to': 'admin@runbox.com', + 'reference': { + 'comp_new_window': null, + 'status': 0, + 'charset': null, + 'folder': 'LALA', + 'sig_above': 'NO', + 'email': 'admin@runbox.com', + 'msg_per_page': 0, + 'name': 'folder1', + 'reply_to': 'admin@runbox.com', + 'default_bcc': '', + 'use_sig_for_reply': 'YES', + 'signature': '

ą

\r\n

eex

', + 'save_sent': 'n' + }, + 'name': 'folder1', + 'email': 'admin@runbox.com', + 'reference_type': 'preference', + 'smtp_username': null, + 'id': 16450 + } + ] + }; } contacts(): any[] { From 783a2f5b6d4c84afc48b98c776f1026be11adedc Mon Sep 17 00:00:00 2001 From: Geir Thomas Andersen Date: Tue, 17 Oct 2023 14:11:14 +0200 Subject: [PATCH 07/15] style(identities): Clarify identity type. --- src/app/profiles/profiles.component.html | 7 ++----- src/app/profiles/profiles.editor.modal.html | 22 +++++++++++---------- src/app/profiles/profiles.lister.html | 3 +++ src/app/profiles/profiles.lister.scss | 1 + 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/app/profiles/profiles.component.html b/src/app/profiles/profiles.component.html index a91c74e78..5a8cc79ab 100644 --- a/src/app/profiles/profiles.component.html +++ b/src/app/profiles/profiles.component.html @@ -47,11 +47,8 @@

Identities for Aliases

Other identities

-

Other Identities can be used when you want to send from addresses that are not part of your Runbox - account. Adding this kind of address (e.g. a work email address) will require verification via - email that you have access to that account. You can also use Other Identities if you need - additional identities for you Runbox email addresses.

-

Added identities require verification via an email sent to the given address.

+

Other Identities can be used when you want to send from addresses that are not part of your Runbox account. Adding this kind of address (e.g. a work email address) will require verification via email that you have access to that account. You can also use Other Identities if you need additional identities for you Runbox email addresses.

+

Adding new identities require verification via an email sent to the given address.

- - Username Identity - - - Alias - - - Default Identity + + This Identity cannot be deleted diff --git a/src/app/profiles/profiles.lister.html b/src/app/profiles/profiles.lister.html index 5a125f526..baa109e37 100644 --- a/src/app/profiles/profiles.lister.html +++ b/src/app/profiles/profiles.lister.html @@ -5,6 +5,9 @@ + +

Username Identity

+
Email: diff --git a/src/app/profiles/profiles.lister.scss b/src/app/profiles/profiles.lister.scss index a343bd4d7..d6a943a88 100644 --- a/src/app/profiles/profiles.lister.scss +++ b/src/app/profiles/profiles.lister.scss @@ -24,6 +24,7 @@ display: inline-flex; width: calc(100% - 10px); margin: 5px; + min-height: 200px; @media only screen and (min-width: 769px) { width: calc(50% - 10px); max-width: 600px; From 2cba8d4d4c06ef107eec8843f995f8e1dfc3071a Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Tue, 24 Oct 2023 16:15:43 +0000 Subject: [PATCH 08/15] test(various): Fixup existing tests, upgrade Cypress to latest v12 --- e2e/cypress/integration/account-access.ts | 4 +- e2e/cypress/integration/compose.ts | 11 +-- e2e/cypress/integration/message-caching.ts | 15 ++-- e2e/mockserver/mockserver.ts | 14 +++- package-lock.json | 84 ++++++++++++++++++---- package.json | 2 +- 6 files changed, 103 insertions(+), 27 deletions(-) diff --git a/e2e/cypress/integration/account-access.ts b/e2e/cypress/integration/account-access.ts index c25120028..c072124cc 100644 --- a/e2e/cypress/integration/account-access.ts +++ b/e2e/cypress/integration/account-access.ts @@ -3,13 +3,13 @@ describe('Account access control', () => { function becomeSubaccount() { cy.intercept('/rest/v1/me', (req) => { - req.reply((res) => { + req.continue((res) => { const payload = JSON.parse(res.body); payload.result.owner = { uid: 666, username: 'mastermind@runbox.com', }; - res.body = JSON.stringify(payload); + res.send(JSON.stringify(payload)); }); }); } diff --git a/e2e/cypress/integration/compose.ts b/e2e/cypress/integration/compose.ts index 135a6d01e..53632dbd6 100644 --- a/e2e/cypress/integration/compose.ts +++ b/e2e/cypress/integration/compose.ts @@ -1,10 +1,14 @@ /// describe('Composing emails', () => { - beforeEach(() => { + beforeEach(async () => { localStorage.setItem('221:Desktop:localSearchPromptDisplayed', JSON.stringify('true')); localStorage.setItem('221:Mobile:localSearchPromptDisplayed', JSON.stringify('true')); localStorage.setItem('221:preference_keys', '["Desktop:localSearchPromptDisplayed","Mobile:localSearchPromptDisplayed"]'); + + (await indexedDB.databases()) + .filter(db => db.name && /messageCache/.test(db.name)) + .forEach(db => indexedDB.deleteDatabase(db.name!)); }); Cypress.config('requestTimeout', 100000); @@ -55,10 +59,7 @@ describe('Composing emails', () => { cy.get('mailrecipient-input mat-error').should('not.exist'); }); - it('should open reply draft with HTML editor', async () => { - (await indexedDB.databases()) - .filter(db => db.name && /messageCache/.test(db.name)) - .forEach(db => indexedDB.deleteDatabase(db.name!)); + it('should open reply draft with HTML editor', () => { // cy.visit('/'); // cy.wait(1000); cy.intercept('/rest/v1/email/1').as('message1requested'); diff --git a/e2e/cypress/integration/message-caching.ts b/e2e/cypress/integration/message-caching.ts index a3ff7f388..e97f88f42 100644 --- a/e2e/cypress/integration/message-caching.ts +++ b/e2e/cypress/integration/message-caching.ts @@ -1,16 +1,16 @@ /// describe('Message caching', () => { - beforeEach(() => { + beforeEach(async () => { localStorage.setItem('Desktop:localSearchPromptDisplayed', 'true'); localStorage.setItem('Global:messageSubjectDragTipShown', 'true'); - }); - - it('should cache all messages on first time page load', async () => { (await indexedDB.databases()) .filter(db => db.name && /messageCache/.test(db.name)) .forEach(db => indexedDB.deleteDatabase(db.name!)); + }); + + it('should cache all messages on first time page load', () => { cy.intercept('/rest/v1/email/12').as('message12requested'); cy.visit('/'); @@ -19,6 +19,13 @@ describe('Message caching', () => { }); it('should not re-request messages after a page reload', () => { + cy.intercept('/rest/v1/email/12').as('message12requested'); + + cy.visit('/'); + cy.wait('@message12requested', {'timeout':10000}); + // This should have fetched/cached the message + + // Now don't fetch it again: cy.visit('/#Inbox:12'); let called = false; cy.intercept('/rest/v1/email/12', (_req) => { diff --git a/e2e/mockserver/mockserver.ts b/e2e/mockserver/mockserver.ts index a04a2e20a..8c547ddb0 100644 --- a/e2e/mockserver/mockserver.ts +++ b/e2e/mockserver/mockserver.ts @@ -378,6 +378,18 @@ END:VCALENDAR } )); break; + case '/rest/v1/webmail/preferences': + response.end(JSON.stringify({ + 'Global': {'version': 1, 'entries': {} }, + 'Desktop': {'version': 1, 'entries': {} }, + 'Mobile': {'version': 1, 'entries': {} } + })); + break; + case '/rest/v1/webmail/saved_searches': + response.end(JSON.stringify({ + 'version': 1, 'entries': [] + })); + break; case '/_ics/Europe/Oslo.ics': response.end(this.vtimezone_oslo); break; @@ -793,7 +805,7 @@ END:VCALENDAR profiles_verified() { return { - 'result': + 'results': [{ 'smtp_username': null, 'email': 'a2@example.com', diff --git a/package-lock.json b/package-lock.json index 3f99366d7..f5f676c92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,7 +63,7 @@ "@types/node": "^14.14.31", "@typescript-eslint/eslint-plugin": "^5.59.0", "@typescript-eslint/parser": "^5.59.0", - "cypress": "^12.10.0", + "cypress": "^12.17.4", "cypress-terminal-report": "^5.1.1", "eslint": "^8.38.0", "jasmine-core": "~4.6.0", @@ -3783,9 +3783,9 @@ } }, "node_modules/@cypress/request": { - "version": "2.88.11", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.11.tgz", - "integrity": "sha512-M83/wfQ1EkspjkE2lNWNV5ui2Cv7UCv1swW1DqljahbzLVWltcsexQh8jYtuS/vzFXP+HySntGM83ZXA9fn17w==", + "version": "2.88.12", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.12.tgz", + "integrity": "sha512-tOn+0mDZxASFM+cuAP9szGUGPI1HwWVSvdzm7V4cCsPdFTx6qMj29CwaQmRAMIEhORIUBFBsYROYJcveK4uOjA==", "dev": true, "dependencies": { "aws-sign2": "~0.7.0", @@ -3803,7 +3803,7 @@ "performance-now": "^2.1.0", "qs": "~6.10.3", "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", + "tough-cookie": "^4.1.3", "tunnel-agent": "^0.6.0", "uuid": "^8.3.2" }, @@ -3861,6 +3861,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/@cypress/request/node_modules/tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@cypress/request/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/@cypress/request/node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -8884,15 +8908,15 @@ "dev": true }, "node_modules/cypress": { - "version": "12.10.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.10.0.tgz", - "integrity": "sha512-Y0wPc221xKKW1/4iAFCphkrG2jNR4MjOne3iGn4mcuCaE7Y5EtXL83N8BzRsAht7GYfWVjJ/UeTqEdDKHz39HQ==", + "version": "12.17.4", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.17.4.tgz", + "integrity": "sha512-gAN8Pmns9MA5eCDFSDJXWKUpaL3IDd89N9TtIupjYnzLSmlpVr+ZR+vb4U/qaMp+lB6tBvAmt7504c3Z4RU5KQ==", "dev": true, "hasInstallScript": true, "dependencies": { - "@cypress/request": "^2.88.10", + "@cypress/request": "2.88.12", "@cypress/xvfb": "^1.2.4", - "@types/node": "^14.14.31", + "@types/node": "^16.18.39", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", "arch": "^2.2.0", @@ -8925,9 +8949,10 @@ "minimist": "^1.2.8", "ospath": "^1.2.2", "pretty-bytes": "^5.6.0", + "process": "^0.11.10", "proxy-from-env": "1.0.0", "request-progress": "^3.0.0", - "semver": "^7.3.2", + "semver": "^7.5.3", "supports-color": "^8.1.1", "tmp": "~0.2.1", "untildify": "^4.0.0", @@ -9054,6 +9079,12 @@ "node": ">=8" } }, + "node_modules/cypress/node_modules/@types/node": { + "version": "16.18.59", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.59.tgz", + "integrity": "sha512-PJ1w2cNeKUEdey4LiPra0ZuxZFOGvetswE8qHRriV/sUkL5Al4tTmPV9D2+Y/TPIxTHHgxTfRjZVKWhPw/ORhQ==", + "dev": true + }, "node_modules/cypress/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -9216,9 +9247,9 @@ } }, "node_modules/cypress/node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -18720,6 +18751,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -18983,6 +19023,12 @@ "node": ">=0.6" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -21572,6 +21618,16 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 692322fcc..b07d7db3e 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "@types/node": "^14.14.31", "@typescript-eslint/eslint-plugin": "^5.59.0", "@typescript-eslint/parser": "^5.59.0", - "cypress": "^12.10.0", + "cypress": "^12.17.4", "cypress-terminal-report": "^5.1.1", "eslint": "^8.38.0", "jasmine-core": "~4.6.0", From 3a7013bf6a21f56b868b0e375285c3dbcf8e2792 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Wed, 25 Oct 2023 14:19:39 +0000 Subject: [PATCH 09/15] fix(identities): More stability based on review --- src/app/profiles/profile.service.ts | 2 +- src/app/profiles/profiles.default.ts | 9 +++++---- src/app/profiles/profiles.editor.modal.ts | 19 +++++++++---------- src/app/profiles/profiles.lister.ts | 1 - 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/app/profiles/profile.service.ts b/src/app/profiles/profile.service.ts index f838b4c8b..e91868ffc 100644 --- a/src/app/profiles/profile.service.ts +++ b/src/app/profiles/profile.service.ts @@ -122,7 +122,7 @@ export class ProfileService { }) ); } - re_validate(id) { + reValidate(id) { this.rmmapi.resendValidationEmail(id).subscribe( reply => { this.refresh(); diff --git a/src/app/profiles/profiles.default.ts b/src/app/profiles/profiles.default.ts index eb50a1c1f..65c13b0de 100644 --- a/src/app/profiles/profiles.default.ts +++ b/src/app/profiles/profiles.default.ts @@ -38,15 +38,16 @@ export class DefaultProfileComponent { public rmmapi: RunboxWebmailAPI, private snackBar: MatSnackBar ) { - this.profileService.profiles.subscribe((_) => - this.selectedProfile = this.profileService.composeProfile - ); + this.profileService.profiles.subscribe((_) => { + this.selectedProfile = this.profileService.composeProfile; + this.validProfiles = this.profileService.validProfiles.value; + }); } updateDefaultProfile() { const priorities: FromPriority[] = new Array(); let p_value = 1; - for (const profile of this.profileService.validProfiles.value) { + for (const profile of this.profileService.profiles.value) { let from_p: FromPriority = {"from_priority": -1, "id": profile.id }; if (profile.id === this.selectedProfile.id) { from_p.from_priority = 0; diff --git a/src/app/profiles/profiles.editor.modal.ts b/src/app/profiles/profiles.editor.modal.ts index 39d2b654e..6449ff307 100644 --- a/src/app/profiles/profiles.editor.modal.ts +++ b/src/app/profiles/profiles.editor.modal.ts @@ -57,9 +57,6 @@ export class ProfilesEditorModalComponent { @Inject(MAT_DIALOG_DATA) public identity: Identity ) { this.tinymce_plugin = new TinyMCEPlugin(); - if (identity.email) { - this.set_localpart(identity); - } if (!identity || !Object.keys(identity).length) { identity = new Identity; const self = this; @@ -67,6 +64,9 @@ export class ProfilesEditorModalComponent { return self.profileService.me[attr]; }).join(' '); } + if (identity.email) { + this.set_localpart(identity); + } this.identity = identity; if (this.identity.is_signature_html) { this.init_tinymce(); @@ -116,7 +116,7 @@ export class ProfilesEditorModalComponent { is_smtp_enabled: (identity.is_smtp_enabled ? 1 : 0), }; this.profileService.create(values).subscribe( - res => this.close + res => this.close() ); } delete() { @@ -233,21 +233,20 @@ export class ProfilesEditorModalComponent { } } init_tinymce() { - const self = this; this.selector = `html-editor-${Math.floor(Math.random() * 10000000000)}`; const options = { base_url: this.location.prepareExternalUrl('/tinymce/'), // Base for assets such as skins, themes and plugins selector: '#' + this.selector, setup: editor => { - self.editor = editor; + this.editor = editor; editor.on('Change', () => { - self.identity.signature = editor.getContent(); + this.identity.signature = editor.getContent(); }); }, init_instance_callback: (editor) => { editor.setContent( - self.identity.signature ? - self.identity.signature.replace(/\n/g, '
\n') : + this.identity.signature ? + this.identity.signature.replace(/\n/g, '
\n') : '' ); } @@ -255,6 +254,6 @@ export class ProfilesEditorModalComponent { this.tinymce_plugin.create(options); } resend_validate_email(id) { - this.profileService.re_validate(id); + this.profileService.reValidate(id); } } diff --git a/src/app/profiles/profiles.lister.ts b/src/app/profiles/profiles.lister.ts index 11162085c..21c888f89 100644 --- a/src/app/profiles/profiles.lister.ts +++ b/src/app/profiles/profiles.lister.ts @@ -44,7 +44,6 @@ export class ProfilesListerComponent { } edit(item): void { - //item = JSON.parse(JSON.stringify(item)); this.dialog_ref = this.dialog.open(ProfilesEditorModalComponent, { width: '600px', data: item, From f02a010495f58d8154f8ae5ae0749937853a91cd Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Mon, 6 Nov 2023 12:59:04 +0000 Subject: [PATCH 10/15] test(identities): Add some profile service / identity unit+e2e tests Fixes #1463 --- e2e/cypress/integration/profile.ts | 46 +++++ e2e/mockserver/mockserver.ts | 2 +- src/app/compose/draftdesk.service.spec.ts | 26 +-- src/app/profiles/profile.service.spec.ts | 203 ++++++++++++++++++++ src/app/profiles/profile.service.ts | 10 +- src/app/profiles/profiles.component.html | 2 +- src/app/profiles/profiles.component.ts | 6 +- src/app/profiles/profiles.editor.modal.html | 2 +- src/app/profiles/profiles.editor.modal.ts | 3 +- 9 files changed, 271 insertions(+), 29 deletions(-) create mode 100644 e2e/cypress/integration/profile.ts create mode 100644 src/app/profiles/profile.service.spec.ts diff --git a/e2e/cypress/integration/profile.ts b/e2e/cypress/integration/profile.ts new file mode 100644 index 000000000..abd1c3c92 --- /dev/null +++ b/e2e/cypress/integration/profile.ts @@ -0,0 +1,46 @@ +/// + +describe('Profiles settings page', () => { + + const ALLOWED_DOMAINS = ['runbox.com', 'example.com']; + + it('lists currently existing profiles', () => { + cy.intercept('GET', '/rest/v1/profiles').as('getProfiles'); + cy.intercept('GET', '/rest/v1/aliases/limits').as('getAliasLimits'); + cy.visit('/account/identities'); + cy.wait('@getProfiles'); + + // 1 compose profile, 5 valid ones: + cy.get('mat-card-content.profile-content').should('have.length', 6); + }); + + it('can create new profiles', () => { + cy.intercept('GET', '/rest/v1/profiles').as('getProfiles'); + cy.intercept('GET', '/rest/v1/aliases/limits').as('getAliasLimits'); + cy.visit('/account/identities'); + cy.wait('@getAliasLimits'); + + cy.get('#add-identity').click(); + + // open dialog, fill in fields, submit + cy.get('app-profiles-edit').should('be.visible'); + cy.get('input[name="email"]').type('newprof@runbox.com'); + cy.get('input[name="from"]').type('My Name'); + cy.get('input[name="name"]').type('My Profile'); + cy.get('textarea[name="signature"]').type('My Sig'); + + cy.intercept('POST', '/rest/v1/profile', { + statusCode: 200, + body: { + status: 'success', + result: {id: 1} + } + }).as('postProfile'); + cy.get('button#save').click(); + cy.wait('@postProfile'); + + cy.get('button#save').click(); + cy.get('app-profiles-edit').should('not.exist'); + }); + +}); diff --git a/e2e/mockserver/mockserver.ts b/e2e/mockserver/mockserver.ts index 8c547ddb0..76212a1c0 100644 --- a/e2e/mockserver/mockserver.ts +++ b/e2e/mockserver/mockserver.ts @@ -820,7 +820,7 @@ END:VCALENDAR 'name': 'a2@example.com', 'smtp_password': null, 'from_name': 'Hallucinogen', - 'type': 'aliases' + 'type': 'main' }, { 'id': 16456, diff --git a/src/app/compose/draftdesk.service.spec.ts b/src/app/compose/draftdesk.service.spec.ts index 7c39df341..d460fe1fa 100644 --- a/src/app/compose/draftdesk.service.spec.ts +++ b/src/app/compose/draftdesk.service.spec.ts @@ -45,7 +45,7 @@ describe('DraftDesk', () => { rawtext: 'blabla\nabcde', html: '

blabla

abcde

' }, - [ Identity.fromEmailAddress('to@runbox.com')], + [ Identity.fromObject({'email':'to@runbox.com'})], false); expect(draft.subject).toBe('Fwd: Test subject'); @@ -84,7 +84,7 @@ blabla\nabcde`); html: '

blabla

abcde

', sanitized_html: '

blabla

abcde

' }, - [ Identity.fromEmailAddress('to@runbox.com')], + [ Identity.fromObject({'email':'to@runbox.com'})], true); expect(draft.subject).toBe('Fwd: Test subject'); @@ -137,7 +137,7 @@ Subject: Test subject
rawtext: 'blabla\nabcde', html: '

blabla

abcde

' }, - [ Identity.fromEmailAddress('to@runbox.com')], + [ Identity.fromObject({'email':'to@runbox.com'})], false, false); expect(draft.subject).toBe('Re: Test subject'); @@ -167,7 +167,7 @@ Subject: Test subject
rawtext: 'blabla\nabcde', html: '

blabla

abcde

' }, - [ Identity.fromEmailAddress('to@runbox.com')], + [ Identity.fromObject({'email':'to@runbox.com'})], false, false); expect(draft.subject).toBe('Re: Test subject'); @@ -204,7 +204,7 @@ Subject: Test subject
rawtext: 'blabla\nabcde', html: '

blabla

abcde

' }, - [ Identity.fromEmailAddress('to@runbox.com')], + [ Identity.fromObject({'email':'to@runbox.com'})], false, false); expect(draft.subject).toBe('Re: Test subject'); @@ -245,7 +245,7 @@ Subject: Test subject
rawtext: 'blabla\nabcde', html: '

blabla

abcde

' }, - [ Identity.fromEmailAddress('to@runbox.com')], + [ Identity.fromObject({'email':'to@runbox.com'})], true, false); expect(draft.subject).toBe('Re: Test subject'); @@ -276,7 +276,7 @@ Subject: Test subject
rawtext: 'blabla\nabcde', html: '

blabla

abcde

' }, - [ Identity.fromEmailAddress('to@runbox.com')], + [ Identity.fromObject({'email':'to@runbox.com'})], true, false); expect(draft.subject).toBe('Re: Test subject'); @@ -304,7 +304,7 @@ Subject: Test subject
text: 'blabla\nabcde', rawtext: 'blabla\nabcde' }, - [ Identity.fromEmailAddress('to@runbox.com') ], + [ Identity.fromObject({'email':'to@runbox.com'}) ], true, false); expect(draft.subject).toBe('Re: Test subject'); @@ -332,7 +332,7 @@ Subject: Test subject
text: 'blabla\nabcde', rawtext: 'blabla\nabcde' }, - [ Identity.fromEmailAddress('to@runbox.com') ], + [ Identity.fromObject({'email':'to@runbox.com'}) ], true, false); const replydraft = DraftFormModel.reply({ @@ -351,7 +351,7 @@ Subject: Test subject
text: draft.msg_body, rawtext: draft.msg_body }, - [ Identity.fromEmailAddress('from@runbox.com') ], + [ Identity.fromObject({'email':'from@runbox.com'}) ], false, false); expect(replydraft.subject).toBe('Re: Test subject'); @@ -370,7 +370,7 @@ Subject: Test subject
// compose?new=true let draft = DraftFormModel.create( -1, - Identity.fromEmailAddress('to@runbox.com'), + Identity.fromObject({'email':'to@runbox.com'}), null, ''); expect(draft.isUnsaved()).toBe(true); @@ -378,7 +378,7 @@ Subject: Test subject
// Link on contact page: draft = DraftFormModel.create( -1, - Identity.fromEmailAddress('to@runbox.com'), + Identity.fromObject({'email':'to@runbox.com'}), '"Test Runbox" ', ''); expect(draft.isUnsaved()).toBe(true); @@ -386,7 +386,7 @@ Subject: Test subject
// refreshDrafts draft = DraftFormModel.create( 12345, - Identity.fromEmailAddress('to@runbox.com'), + Identity.fromObject({'email':'to@runbox.com'}), '"Test Runbox" ', 'Some blahblah'); expect(draft.isUnsaved()).toBe(false); diff --git a/src/app/profiles/profile.service.spec.ts b/src/app/profiles/profile.service.spec.ts new file mode 100644 index 000000000..c5fa4841a --- /dev/null +++ b/src/app/profiles/profile.service.spec.ts @@ -0,0 +1,203 @@ +// --------- BEGIN RUNBOX LICENSE --------- +// Copyright (C) 2016-2023 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 { Identity, ProfileService } from "./profile.service"; +import { RunboxWebmailAPI } from '../rmmapi/rbwebmail'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { TestBed, waitForAsync } from '@angular/core/testing'; +import { of } from 'rxjs'; + +describe('Identity', () => { + it('Should create an identity with just email', () => { + const ident = Identity.fromObject({ 'email': 'test@example.com' }); + expect(ident.nameAndAddress).toEqual('test@example.com'); + }); + + it('Should create an identity with name and email', () => { + const ident = Identity.fromObject({ + 'email': 'test@example.com', + 'name': 'Fred Bloggs', + }); + expect(ident.nameAndAddress).toEqual('Fred Bloggs '); + }); +}); + +describe('ProfileService', () => { + let service: ProfileService; + + const DEFAULT_EMAIL = 'a2@example.com'; + let PROFILES = [{ + 'email': 'a2@example.com', + 'reference_type': 'aliases', + 'id': 16455, + 'signature': null, + 'reference': {}, + 'reply_to': 'a2@example.com', + 'name': 'a2@example.com', + 'smtp_password': null, + 'from_name': 'Hallucinogen', + 'type': 'main' + }, + { + 'id': 16456, + 'email': 'aa1@example.com', + 'reference_type': 'aliases', + 'from_name': 'Astrix', + 'type': 'aliases', + 'reference': { + 'domainid': null, + 'id': 278, + 'localpart': 'aa1', + 'virtual_domainid': 16, + 'virtual_domain': { + 'catch_all': '', + 'status': 6, + 'id': 16, + 'name': 'example.com' + }, + }, + }, + { + 'email': 'testmail@testmail.com', + 'reference_type': 'aliases', + 'id': 16457, + 'signature': null, + 'reference': {}, + 'name': 'John Doe', + 'from_name': 'John Doe', + 'type': 'aliases' + }, + { + 'from_name': 'Electric Universe', + 'type': 'external_email', + 'name': 'Electric Universe', + 'reference': { + 'save_sent': 'n', + 'signature': 'xxx', + 'use_sig_for_reply': 'NO', + 'reply_to': 'admin@runbox.com', + 'name': 'Electric Universe', + 'default_bcc': '', + 'email': 'admin@runbox.com', + 'msg_per_page': 0, + 'folder': 'Encoding Test', + 'sig_above': 'NO', + 'charset': null, + 'comp_new_window': null, + 'status': 0 + }, + 'reply_to': 'admin@runbox.com', + 'signature': 'xxx', + 'id': 16448, + 'email': 'admin@runbox.com', + 'reference_type': 'preference', + }, + { + 'signature': '

ą

\r\n

eex

', + 'from_name': 'folder1', + 'type': 'external_email', + 'reply_to': 'admin@runbox.com', + 'reference': { + 'comp_new_window': null, + 'status': 0, + 'charset': null, + 'folder': 'LALA', + 'sig_above': 'NO', + 'email': 'admin@runbox.com', + 'msg_per_page': 0, + 'name': 'folder1', + 'reply_to': 'admin@runbox.com', + 'default_bcc': '', + 'use_sig_for_reply': 'YES', + 'signature': '

ą

\r\n

eex

', + 'save_sent': 'n' + }, + 'name': 'folder1', + 'email': 'admin@runbox.com', + 'reference_type': 'preference', + 'id': 16450 + }]; + + const ALLOWED_DOMAINS = ['runbox.com', 'example.com']; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + ], + providers: [ + { provide: RunboxWebmailAPI, useValue: { + me: of({first_name: 'Test', last_name: 'User'}), + getProfiles: () => of(PROFILES), + createProfile: (newprofile) => { + newprofile['reference_type'] = 'aliases'; + PROFILES.unshift(newprofile); + return of(PROFILES.length); + }, + + } }, + ProfileService + ], + }).compileComponents(); + })); + + beforeEach(() => { service = service = TestBed.inject(ProfileService) }); + + it('loads valid profile subsets', (done) => { + service.validProfiles.subscribe(profiles => { + expect(profiles.length).toBe(4); + done(); + }); + }) + it('loads all profiles', (done) => { + service.profiles.subscribe(profiles => { + expect(profiles.length).toBe(PROFILES.length); + done(); + }); + }) + it('loads alias profile subsets', (done) => { + service.aliases.subscribe(profiles => { + expect(profiles.length).toBe(PROFILES.filter(p => p.reference_type === 'aliases').length); + done(); + }); + }) + it('loads non alias profile subsets', (done) => { + service.nonAliases.subscribe(profiles => { + expect(profiles.length).toBe(2); + done(); + }); + }) + it('loads a compose profile', () => { + expect(service.composeProfile).toBeDefined(); + expect(service.composeProfile.email).toEqual('a2@example.com'); + }) + + it('adds a new profile on create', (done) => { + service.create({ name: 'New Profile Name', + email: 'newp@runbox.com', + from_name: 'New Profile', + signature: 'My sig'}) + .subscribe((res) => { + expect(res).toBeTruthy(); + expect(PROFILES.length).toBe(PROFILES.length); + done(); + }); + + }); +}); diff --git a/src/app/profiles/profile.service.ts b/src/app/profiles/profile.service.ts index e91868ffc..47a28be68 100644 --- a/src/app/profiles/profile.service.ts +++ b/src/app/profiles/profile.service.ts @@ -55,12 +55,6 @@ export class Identity { ret.resolveNameAndAddress(); return ret; } - public static fromEmailAddress(email): Identity { - const ret = new Identity(); - ret.email = email; - ret.reply_to = email; - return ret; - } resolveNameAndAddress() { this.nameAndAddress = this.name ? `${this.name} <${this.email}>` : this.email; @@ -86,8 +80,8 @@ export class ProfileService { this.rmmapi.getProfiles().subscribe( (res: Identity[]) => { this.validProfiles.next(res.filter(p => p.type === 'aliases' || (p.reference_type === 'preference' && p.reference.status === 0))); - this.aliases.next(res.filter(p => p.type === 'aliases')); - this.nonAliases.next(res.filter(p => p.type !== 'aliases')); + this.aliases.next(res.filter(p => p.reference_type === 'aliases')); + this.nonAliases.next(res.filter(p => p.reference_type !== 'aliases')); this.composeProfile = res.find(p => p.from_priority === 0); if (!this.composeProfile) { this.composeProfile = res.find(p => p.type === 'main'); diff --git a/src/app/profiles/profiles.component.html b/src/app/profiles/profiles.component.html index 5a8cc79ab..8eb02ea4d 100644 --- a/src/app/profiles/profiles.component.html +++ b/src/app/profiles/profiles.component.html @@ -51,7 +51,7 @@

Other identities

Adding new identities require verification via an email sent to the given address.

- {{profileService.nonAliases.value.length}} identities created diff --git a/src/app/profiles/profiles.component.ts b/src/app/profiles/profiles.component.ts index 98f3c994a..8729895b0 100644 --- a/src/app/profiles/profiles.component.ts +++ b/src/app/profiles/profiles.component.ts @@ -43,7 +43,7 @@ export class ProfilesComponent { public dialog: MatDialog, public profileService: ProfileService, ) { -// this.profileService.profiles.subscribe((profiles) => this.profiles = profiles); + // FIXME: Need to refresh this if/when we make more aliases this.profileService.rmmapi.getAliasLimits().subscribe( res => this.alias_limits = res @@ -56,8 +56,8 @@ export class ProfilesComponent { }); } - add_profile (type): void { - let item = {type: type}; + add_profile (): void { + let item = {}; this.dialog_ref = this.dialog.open(ProfilesEditorModalComponent, { width: '600px', data: item, diff --git a/src/app/profiles/profiles.editor.modal.html b/src/app/profiles/profiles.editor.modal.html index c8b9eae44..82409e0a1 100644 --- a/src/app/profiles/profiles.editor.modal.html +++ b/src/app/profiles/profiles.editor.modal.html @@ -130,7 +130,7 @@ - + diff --git a/src/app/profiles/profiles.editor.modal.ts b/src/app/profiles/profiles.editor.modal.ts index 6449ff307..010cdbf2b 100644 --- a/src/app/profiles/profiles.editor.modal.ts +++ b/src/app/profiles/profiles.editor.modal.ts @@ -40,7 +40,6 @@ export class ProfilesEditorModalComponent { is_update = false; is_create = false; - type; is_visible_smtp_detail = false; is_different_reply_to = false; localpart; @@ -111,7 +110,7 @@ export class ProfilesEditorModalComponent { smtp_port: identity.smtp_port, smtp_username: identity.smtp_username, smtp_password: identity.smtp_password, - type: this.type, + type: identity.type, is_signature_html: (identity.is_signature_html ? 1 : 0), is_smtp_enabled: (identity.is_smtp_enabled ? 1 : 0), }; From 992fb66af9846f6ce99c735011b7b8afb70e9f6f Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Wed, 17 Jan 2024 11:03:53 +0000 Subject: [PATCH 11/15] fix(aliases): Making aliases API endpoints do one job each --- src/app/rmm/alias.ts | 47 +++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/src/app/rmm/alias.ts b/src/app/rmm/alias.ts index 3b9f6bd7f..1cfd8a1b8 100644 --- a/src/app/rmm/alias.ts +++ b/src/app/rmm/alias.ts @@ -30,29 +30,36 @@ export class Alias { ) { } load(): Observable { + this.app.ua.http.get('/rest/v1/aliases/limits').subscribe( + res => { + this.aliases_counter = { + total: res['result'].total, + current: res['result'].current, + }; + }, + error => { + return this.app.show_error('Could not load alias limits', 'Dismiss'); + } + ); + const req = this.app.ua.http.get('/rest/v1/aliases', {}).pipe(timeout(60000), share()); req.subscribe( - data => { - const reply = data; - if ( reply['status'] === 'error' ) { - this.app.show_error( reply['error'].join( '' ), 'Dismiss' ); - return; - } - this.aliases = reply['result'].aliases; - const _unique = {}; - for ( const value of this.aliases ) { - _unique[ value.localpart + '@' + value.domain ] = 1; + reply => { + if ( reply['status'] === 'error' ) { + this.app.show_error( reply['error'].join( '' ), 'Dismiss' ); + return; + } + this.aliases = reply['result'].aliases; + const _unique = {}; + for ( const value of this.aliases ) { + _unique[ value.localpart + '@' + value.domain ] = 1; + } + this.aliases_unique = Object.keys(_unique); + return; + }, + error => { + return this.app.show_error('Could not load aliases.', 'Dismiss'); } - this.aliases_unique = Object.keys(_unique); - this.aliases_counter = { - total: reply['result'].counter.total, - current: reply['result'].counter.current, - }; - return; - }, - error => { - return this.app.show_error('Could not load aliases.', 'Dismiss'); - } ); return req; } From d603cbead86d7b53fdf6a42b592ed38a3bdbf845 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Tue, 5 Mar 2024 12:29:28 +0000 Subject: [PATCH 12/15] test(profiles): Only calls save button once in create profile --- e2e/cypress/integration/profile.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/e2e/cypress/integration/profile.ts b/e2e/cypress/integration/profile.ts index abd1c3c92..0e948aab8 100644 --- a/e2e/cypress/integration/profile.ts +++ b/e2e/cypress/integration/profile.ts @@ -39,7 +39,6 @@ describe('Profiles settings page', () => { cy.get('button#save').click(); cy.wait('@postProfile'); - cy.get('button#save').click(); cy.get('app-profiles-edit').should('not.exist'); }); From 24b4b82ec769908b01ba871b039dac621dfa8175 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Tue, 5 Mar 2024 12:31:19 +0000 Subject: [PATCH 13/15] fix(identities): Compare used alias total correctly --- src/app/profiles/profiles.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/profiles/profiles.component.html b/src/app/profiles/profiles.component.html index 8eb02ea4d..1b4002c4f 100644 --- a/src/app/profiles/profiles.component.html +++ b/src/app/profiles/profiles.component.html @@ -32,7 +32,7 @@

Identities for Aliases

Aliases are extra email addresses that deliver to your account, just as your main username/email address does. Below are identities you can use if you want to send messages from your aliases.

These identities can be customised by adding a different From Name, Signature or Reply-to address. You can also change the Runbox domain an alias uses.

You can't delete these identities as they are tied to your aliases.

-

+

You have reached the maximum allowed number of Runbox aliases.

To manage your aliases, please visit Email Aliases.

From 3de0f4ab6341a95e3dbb9fcdb76c502136ee2613 Mon Sep 17 00:00:00 2001 From: Geir Thomas Andersen Date: Tue, 19 Mar 2024 08:12:16 +0100 Subject: [PATCH 14/15] style(identities): Improve formatting of username identity and warnings. --- src/app/profiles/profiles.editor.modal.html | 8 ++++---- src/app/profiles/profiles.lister.html | 4 ++-- src/app/profiles/profiles.lister.scss | 4 ---- src/styles.scss | 18 ++++++++++++++++++ 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/app/profiles/profiles.editor.modal.html b/src/app/profiles/profiles.editor.modal.html index 82409e0a1..0bbc83534 100644 --- a/src/app/profiles/profiles.editor.modal.html +++ b/src/app/profiles/profiles.editor.modal.html @@ -123,14 +123,14 @@ - + - This Identity cannot be deleted + This Identity cannot be deleted. - - + + diff --git a/src/app/profiles/profiles.lister.html b/src/app/profiles/profiles.lister.html index baa109e37..dd028e4d4 100644 --- a/src/app/profiles/profiles.lister.html +++ b/src/app/profiles/profiles.lister.html @@ -6,7 +6,7 @@ -

Username Identity

+

Username Identity

Email: @@ -43,7 +43,7 @@

Username Identity

- Email not validated. + Email not validated. diff --git a/src/app/profiles/profiles.lister.scss b/src/app/profiles/profiles.lister.scss index d6a943a88..9a82154cb 100644 --- a/src/app/profiles/profiles.lister.scss +++ b/src/app/profiles/profiles.lister.scss @@ -47,7 +47,3 @@ height: 50px; } } -.identity-warning { - font-weight: bold; - color: crimson; -} diff --git a/src/styles.scss b/src/styles.scss index 7c7cae140..7190097bb 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -20,6 +20,8 @@ $rmm-default-primary: mat.define-palette(mat.$light-blue-palette, 900, A400, A700); $rmm-default-accent: mat.define-palette(mat.$blue-palette, 200, 100, 400); $rmm-default-warn: mat.define-palette(mat.$orange-palette, 500, A400, A700); +$rmm-default-caution: mat.define-palette(mat.$deep-orange-palette, 500, A400, A700); +$rmm-default-error: mat.define-palette(mat.$red-palette, 500, A400, A700); $rmm-default-foreground: mat.define-palette(mat.$light-blue-palette, 400, 200, 600); $rmm-default-highlight: mat.define-palette(mat.$light-blue-palette, 100, 50, 200); $rmm-default-background: mat.define-palette(mat.$light-blue-palette, 900, A400, A700); @@ -323,6 +325,22 @@ mat-grid-tile.tableTitle { font-size: 12px; } ++.warning { + color: mat.get-color-from-palette($rmm-default-warn); + font-weight: bold; +} + +.caution { + color: mat.get-color-from-palette($rmm-default-caution); + font-weight: bold; +} + +.error { + color: mat.get-color-from-palette($rmm-default-error); + font-weight: bold; +} + + /* Palette colors */ .themePalettePrimary { From 9569d6cdc14b7cd0386977cfd1838a3f6221b3f9 Mon Sep 17 00:00:00 2001 From: Geir Thomas Andersen Date: Wed, 3 Apr 2024 21:34:07 +0200 Subject: [PATCH 15/15] style(identities): Improve indication of Account Identity. --- src/app/profiles/profiles.component.html | 2 +- src/app/profiles/profiles.editor.modal.html | 6 +++--- src/app/profiles/profiles.lister.html | 13 ++++--------- src/app/profiles/profiles.lister.scss | 7 +++++++ 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/app/profiles/profiles.component.html b/src/app/profiles/profiles.component.html index 1b4002c4f..ca9894b03 100644 --- a/src/app/profiles/profiles.component.html +++ b/src/app/profiles/profiles.component.html @@ -17,7 +17,7 @@

Default Identity

[selectedProfile] = profileService.composeProfile >
-

Select the email you want to use as your Default Identity:

+

Select the identity you want to use as your Default Identity:


diff --git a/src/app/profiles/profiles.editor.modal.html b/src/app/profiles/profiles.editor.modal.html index 0bbc83534..040fb5385 100644 --- a/src/app/profiles/profiles.editor.modal.html +++ b/src/app/profiles/profiles.editor.modal.html @@ -1,12 +1,12 @@ - Create profile - Edit profile: {{identity.name}} + Create identity + Edit identity: {{identity.name}}
- Username Identity + Account Identity
Alias diff --git a/src/app/profiles/profiles.lister.html b/src/app/profiles/profiles.lister.html index dd028e4d4..312602c94 100644 --- a/src/app/profiles/profiles.lister.html +++ b/src/app/profiles/profiles.lister.html @@ -5,17 +5,12 @@ - -

Username Identity

-
- Email: + Email Address: - {{item.email}} - - - + {{item.email}}
+ {{item.email}}
(Account Identity)
From Name: @@ -48,7 +43,7 @@

Username Identity

- +
diff --git a/src/app/profiles/profiles.lister.scss b/src/app/profiles/profiles.lister.scss index 9a82154cb..fa6639930 100644 --- a/src/app/profiles/profiles.lister.scss +++ b/src/app/profiles/profiles.lister.scss @@ -17,6 +17,9 @@ background-image: url(/_img/avatar.svg); background-size: cover; } +.mat-card { + padding: 20px; +} .mat_header { align-items: center; } @@ -47,3 +50,7 @@ height: 50px; } } + +.heading { + font-weight: bold; +}