diff --git a/src/app/dkim/dkim.component.html b/src/app/dkim/dkim.component.html index bd355570e..77c704fa2 100644 --- a/src/app/dkim/dkim.component.html +++ b/src/app/dkim/dkim.component.html @@ -1,108 +1,134 @@ - - -
- - - -

DKIM Signing of Domain Names

-

{{domain}}

-
-
-

- DKIM (DomainKeys Identified Mail) is an email authentication method that places a digital signature (a piece of code) in the headers of your outgoing mail. This helps receiving email servers identify if the message was genuinely sent by you, the domain owner, and confirms that certain aspects of the message have been unchanged since the digital signature was added. More information about DKIM -

-

- If you have your domain name hosted by Runbox, your DNS (Domain Name System) records are also managed by us. This makes it very easy to set up DKIM signing as you just need to activate it below. If your domain is not hosted/registered via Runbox then we probably don't host your DNS records and you will need to make some changes at your domain host/DNS host as shown below. -

-

- The exact format required by your Domain/DNS provider's interface may vary as may what they call the "hostname" entry. If you need help please contact Runbox support or your Domain/DNS provider's support team for advice. -

-

- Note: selector2 will become active on our DNS servers three months after you activate DKIM signing. -

- -
+ + +
+
+

DKIM Signing of Domain Names

+

+ DKIM (DomainKeys Identified Mail) is an email authentication method that places a digital signature (a piece of code) in the headers of your outgoing mail. This helps receiving email servers identify if the message was genuinely sent by you, the domain owner, and confirms that certain aspects of the message have been unchanged since the digital signature was added. More information about DKIM +

+

+ If you have your domain name hosted by Runbox, your DNS (Domain Name System) records are also managed by us. This makes it very easy to set up DKIM signing as you just need to activate it below. If your domain is not hosted/registered via Runbox then we probably don't host your DNS records and you will need to make some changes at your domain host/DNS host as shown below. +

+

+ The exact format required by your Domain/DNS provider's interface may vary as may what they call the "hostname" entry. If you need help please contact Runbox support or your Domain/DNS provider's support team for advice. +

+

+ Note: selector2 will become active on our DNS servers three months after you activate DKIM signing. +

+ + + + + + + + {{ domain.name }} + + + Keys being deleted + + No keys yet + +
+ Key 1: {{domain.keys[0].is_cname_correct ? 'Ok, ' : 'Bad, '}} + Key 2: {{domain.keys[1].is_cname_correct ? 'Ok' : 'Bad'}} +
+
+
+ + +
+ + +
+
+ The DKIM keys are being deleted. +
+ -
{{dkim_domain.type}}:
- -
- - Domain: {{domain}} - - - {{d.domain}} - - - -
-
- -
-
+
{{domain.type}}:
+ +
+ +
+
-
+
-
DNS CNAMEs required
+
DNS CNAMEs required
-
+
-
Hostname
-
TTL
-
Record Type
-
Address
+
Hostname
+
TTL
+
Record Type
+
Address
- -
{{k.selector_recordset_name}}
-
3600
-
CNAME
-
{{k.public_recordset_name}}.
+ +
{{k.selector_recordset_name}}
+
3600
+
CNAME
+
{{k.public_recordset_name}}.
-
-
If you are editing your DNS zone file directly you can add the two lines below:
-
-
{{k.selector_recordset_name}} 3600 IN CNAME {{k.public_recordset_name}}.
+
+
If you are editing your DNS zone file directly you can add the two lines below:
+
+
{{k.selector_recordset_name}} 3600 IN CNAME {{k.public_recordset_name}}.
-
-
-
-
+
+
+
+
-
Selector status
-
CNAME found in DNS
-
Active for signing
-
Active from
+
Selector status
+
CNAME found in DNS
+
Active for signing
+
Active from
- -
{{k.selector}}
- -
- {{k.is_cname_correct ? 'Yes' : 'No'}}Check -
-
-
{{k.is_active ? 'Yes' : 'No'}}
-
{{ - k.date_expected_to_activate || k.date_active || k.date_rotation_finalized || '--' - }} -
+ +
{{k.selector}}
+ +
+ {{k.is_cname_correct ? 'Yes ' : 'No '}} +
+
+
{{k.is_active ? 'Yes' : 'No'}}
+
{{ + k.date_expected_to_activate || k.date_active || k.date_rotation_finalized || '--' + }} +
+
+
+
+ No keys found for {{domain.name}} + +
-
- No keys found for {{domain}} - - -
- -
-
+ + + + + + + +
+
+
diff --git a/src/app/dkim/dkim.component.ts b/src/app/dkim/dkim.component.ts index 6b49a51d8..c1d76785d 100644 --- a/src/app/dkim/dkim.component.ts +++ b/src/app/dkim/dkim.component.ts @@ -1,5 +1,5 @@ // --------- BEGIN RUNBOX LICENSE --------- -// Copyright (C) 2016-2018 Runbox Solutions AS (runbox.com). +// Copyright (C) 2016-2025 Runbox Solutions AS (runbox.com). // // This file is part of Runbox 7. // @@ -23,12 +23,14 @@ create initial keys: POST /rest/v1/dkim/$domain/keys/create replace key: PUT /rest/v1/dkim/$domain/key/update/$selector -- or selector2 delete key: DELETE /rest/v1/dkim/$domain/key/remove/$selector -- or selector2 */ -import { Component, Output, EventEmitter, ViewChild, AfterViewInit } from '@angular/core'; +import { Component, Output, EventEmitter, ViewChild, OnInit } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { ConfirmDialog } from '../dialog/dialog.module'; 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 { Router, ActivatedRoute } from '@angular/router'; +import { DomainService, Domain, DomainKey } from './domain.service'; @Component({ selector: 'app-dkim', @@ -55,7 +57,7 @@ import { MatLegacySnackBar as MatSnackBar } from '@angular/material/legacy-snack `], }) -export class DkimComponent implements AfterViewInit { +export class DkimComponent implements OnInit { panelOpenState = false; @ViewChild(MatPaginator) paginator: MatPaginator; @@ -63,22 +65,49 @@ export class DkimComponent implements AfterViewInit { dkim_domain; dkim_domains = []; - domain; + chosenDomain: string; is_creating_keys = false; is_rotating = 0; key = {}; keys = []; is_deleting_keys = false; - ngAfterViewInit() { + constructor( + private dialog: MatDialog, + private http: HttpClient, + private snackBar: MatSnackBar, + private route: ActivatedRoute, + private router: Router, + public domainService: DomainService, + ) { + } + + ngOnInit(): void { + this.route.fragment.subscribe( + fragment => { + if (fragment !== this.chosenDomain) { + this.chosenDomain = fragment; + console.log(`scrolling to ${this.chosenDomain}`); + const el = document.getElementById(`panel-${this.chosenDomain}`); + el.scrollIntoView(); + } + } + ); } - disable () { - const del_dkim_domain = this.http.delete('/rest/v1/dkim/domain/' + this.domain); + updateFragment(domainName: string) { + if(domainName !== this.chosenDomain) { + this.chosenDomain = domainName; + this.router.navigate(['/dkim'], { fragment: domainName }); + } + } + + disable (domain: string) { + const del_dkim_domain = this.http.delete('/rest/v1/dkim/domain/' + domain); const confirmDialog = this.dialog.open(ConfirmDialog); - confirmDialog.componentInstance.title = `Delete dkim for domain ${this.domain}?`; + confirmDialog.componentInstance.title = `Delete dkim for domain ${domain}?`; confirmDialog.componentInstance.question = - `Are you sure that you want to delete DKIM settings for domain ${this.domain}?`; + `Are you sure that you want to delete DKIM settings for domain ${domain}?`; confirmDialog.componentInstance.noOptionTitle = 'cancel'; confirmDialog.componentInstance.yesOptionTitle = 'ok'; confirmDialog.afterClosed().subscribe(result => { @@ -88,147 +117,60 @@ export class DkimComponent implements AfterViewInit { (r: any) => { this.is_deleting_keys = false; if ( r.status === 'success' ) { - this.keys = []; - this.key = {}; - this.load_dkim_domains(); - this.load_keys(); - return this.show_error( 'Settings will be deleted shortly. Please check in a few minutes!', 'Dismiss' ); + this.domainService.refresh(); + return this.show_snackbar( 'Settings will be deleted shortly. Please check in a few minutes!', 'Dismiss' ); } else if ( r.status === 'error' ) { - return this.show_error( r.errors.join('\n'), 'Dismiss' ); + return this.show_snackbar( r.errors.join('\n'), 'Dismiss' ); } else { - return this.show_error( 'Unknown error has happened.', 'Dismiss' ); + return this.show_snackbar( 'Unknown error has happened.', 'Dismiss' ); } }, error => { this.is_deleting_keys = false; - return this.show_error('Could not list dkim domains list.', 'Dismiss'); + return this.show_snackbar('Could not list dkim domains list.', 'Dismiss'); } ); } }); } - ev_get_domain () { - this.load_dkim_domains(); - this.load_keys(); - } - - load_dkim_domains () { - // const get_dkim_domains = this.http.get() - const get_domains = this.http.get('/rest/v1/dkim/domains'); - get_domains.subscribe( - (r: any) => { - if ( r.status === 'success' ) { - this.dkim_domains = r.result.domains; - const filtered = this.dkim_domains.filter((o) => o.domain === this.domain); - this.dkim_domain = filtered[0]; - if ( ! this.dkim_domain ) { - return setTimeout(() => { this.load_dkim_domains(); }, 5000); - } - if ( !this.domain && this.dkim_domain ) { - this.domain = this.dkim_domain.domain; - } - } else if ( r.status === 'error' ) { - return this.show_error( r.errors.join('\n'), 'Dismiss' ); - } else { - return this.show_error( 'Unknown error has happened.', 'Dismiss' ); - } - }, - error => { - return this.show_error('Could not list dkim domains list.', 'Dismiss'); - } - ); - } - - load_keys () { - if ( ! this.domain ) { return; } - const get_keys = this.http.get('/rest/v1/dkim/' + this.domain + '/keys'); - get_keys.subscribe( - (r: any) => { - if ( r.status === 'success' ) { - this.key = {}; - this.keys = r.result.keys; - this.domain = r.result.domain.name; - this.is_rotating = r.result.domain.is_rotating; - this.keys.forEach( (k) => this.key[k.selector] = k ); - } else if ( r.status === 'error' ) { - this.keys = []; - return this.show_error( r.errors.join('\n'), 'Dismiss' ); - } else { - return this.show_error( 'Unknown error has happened.', 'Dismiss' ); - } - }, - error => { - } - ); - } create_keys () { this.is_creating_keys = true; - const req = this.http.post('/rest/v1/dkim/' + this.domain + '/keys/create', {}); + const req = this.http.post('/rest/v1/dkim/' + this.chosenDomain + '/keys/create', {}); req.subscribe( (r: any) => { if ( r.status === 'success' ) { this.is_creating_keys = false; this.keys = r.result.keys; - this.load_dkim_domains(); - this.load_keys(); + this.domainService.refresh(); } else if ( r.status === 'error' ) { this.is_creating_keys = false; - return this.show_error( r.errors.join('\n'), 'Dismiss' ); + return this.show_snackbar( r.errors.join('\n'), 'Dismiss' ); } else { this.is_creating_keys = false; - return this.show_error( 'Unknown error has happened.', 'Dismiss' ); + return this.show_snackbar( 'Unknown error has happened.', 'Dismiss' ); } }, error => { this.is_creating_keys = false; - return this.show_error('Could not create dkim keys', 'Dismiss'); + return this.show_snackbar('Could not create dkim keys', 'Dismiss'); } ); } - check_cname (item) { - const url = '/rest/v1/dkim/' + this.domain + '/check_cname/' + item.selector; - const req = this.http.put(url, {}); - req.subscribe( - (r: any) => { - if ( r.status === 'success' ) { - this.show_error( 'This CNAME will be checked shortly.', 'Dismiss'); - this.load_keys(); - this.load_dkim_domains(); - } else if ( r.status === 'error' ) { - return this.show_error( r.errors.join('\n'), 'Dismiss' ); - } else { - return this.show_error( 'Unknown error has happened.', 'Dismiss' ); - } - }, - error => { - return this.show_error('There was an error.', 'Dismiss'); - } + check_cname (domain: Domain, key: DomainKey) { + this.domainService.check_cname(domain.name, key).subscribe( + res => this.show_snackbar(res ? 'CNAME is correct' : 'CNAME not found', 'Dismiss') ); } - show_error (message, action) { + show_snackbar (message, action) { this.snackBar.open(message, action, { duration: 2000, }); } - constructor( - private dialog: MatDialog, - private http: HttpClient, - private snackBar: MatSnackBar, - ) { - const domain = window.location.href.match(/domain=([^&]+)/); - if (domain && domain[1]) { - this.domain = domain[1]; - this.load_dkim_domains(); - if ( this.domain ) { - this.load_keys(); - } - } - } } diff --git a/src/app/dkim/domain.service.ts b/src/app/dkim/domain.service.ts new file mode 100644 index 000000000..707ca47bf --- /dev/null +++ b/src/app/dkim/domain.service.ts @@ -0,0 +1,69 @@ +// --------- BEGIN RUNBOX LICENSE --------- +// Copyright (C) 2016-2025 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 { RunboxWebmailAPI } from '../rmmapi/rbwebmail'; + +export interface DomainKey { + selector: string; + created: string; + is_active: boolean; + is_cname_correct: boolean; +} + +export class Domain { + name: string; + status: number; + is_rotating: boolean; + keys: DomainKey[]; + + public static fromObject(obj: any): Domain { + const ret = Object.assign(new Domain(), obj); + return ret; + } +} + +@Injectable({ providedIn: 'root' }) +export class DomainService { + public userDomains: BehaviorSubject = new BehaviorSubject([]); + + constructor( + public rmmapi: RunboxWebmailAPI + ) { + this.refresh(); + } + + refresh() { + // fetch all user (virtual+hosted) domains, regardless of dkim status: + this.rmmapi.getUserDomains().subscribe( + (res: Domain[]) => { + this.userDomains.next(res); + } + ); + } + + check_cname(domain: string, key: DomainKey): Observable { + return this.rmmapi.checkDomainCName(domain, key.selector).pipe( + map((res: boolean) => { + this.refresh(); + return res; + })); + } +} diff --git a/src/app/rmmapi/rbwebmail.ts b/src/app/rmmapi/rbwebmail.ts index 59460b43d..4e3697d39 100644 --- a/src/app/rmmapi/rbwebmail.ts +++ b/src/app/rmmapi/rbwebmail.ts @@ -39,6 +39,7 @@ import { LRUMessageCache } from './lru-message-cache'; import moment from 'moment'; import { SavedSearchStorage } from '../saved-searches/saved-searches.service'; import { PreferencesResult } from '../common/preferences.service'; +import { Domain } from '../dkim/domain.service'; export class MessageFields { id: number; @@ -972,4 +973,20 @@ export class RunboxWebmailAPI { map((res: HttpResponse) => res['result']) ); } + + public getUserDomains(): Observable { + return this.http.get('/rest/v1/dkim/domains').pipe( + map((res: HttpResponse) => { + return res['result']['domains'].map(d => Domain.fromObject(d)); + }) + ); + } + + public checkDomainCName(domain: string, selector: string): Observable { + return this.http.get('/rest/v1/dkim/' + domain + '/check_cname/' + selector).pipe( + map((res: HttpResponse) => { + return res['result']; + }) + ); + } }