diff --git a/src/app/compose/compose.component.ts b/src/app/compose/compose.component.ts
index e1aa9ecf2..5d24c503c 100644
--- a/src/app/compose/compose.component.ts
+++ b/src/app/compose/compose.component.ts
@@ -83,6 +83,7 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit {
has_pasted_signature: boolean;
signature: string;
selector: string;
+ private defaultBccRecipients: MailAddressInfo[] = [];
saveErrorMessage: string;
@@ -196,6 +197,15 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit {
this.formGroup = this.formBuilder.group(this.model);
+ if (this.model.isUnsaved()) {
+ this.updateDefaultBcc(this.formGroup.controls.from.value, false);
+ } else {
+ this.trackDefaultBcc(this.formGroup.controls.from.value);
+ }
+
+ this.formGroup.controls.from.valueChanges
+ .subscribe((selected_from_address) => this.updateDefaultBcc(selected_from_address));
+
// Mark not saved if changes
this.formGroup.valueChanges.subscribe(() => this.saved = null);
@@ -222,8 +232,10 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit {
this.formGroup.controls.from.valueChanges
.pipe(debounceTime(1000))
.subscribe((selected_from_address) => {
- const from: Identity = this.draftDeskservice.fromsSubject.value.find((f) =>
- f.nameAndAddress === selected_from_address);
+ const from: Identity = this.identityForAddress(selected_from_address);
+ if (!from) {
+ return;
+ }
if ( this.formGroup.controls.msg_body.pristine ) {
if ( this.signature && from.signature ) {
// replaces current signature with new one
@@ -525,6 +537,7 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit {
this.draftDeskservice.shouldReturnToPreviousPage = false;
this.formGroup.patchValue(this.model, { emitEvent: false });
+ this.trackDefaultBcc(this.formGroup.controls.from.value);
this.htmlToggled();
}
@@ -696,6 +709,8 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit {
this.savingInProgress = true;
if (send) {
+ this.updateDefaultBcc(this.formGroup.controls.from.value, false);
+
let validemails = false;
validemails = isValidEmailArray(this.model.to);
if (!validemails) {
@@ -736,8 +751,7 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit {
}
// this.model.from should have a value (cos it defaults)
- const from = this.draftDeskservice.fromsSubject.value.find(
- (f) => this.model.from === f.nameAndAddress || this.model.from === f.email);
+ const from = this.identityForAddress(this.model.from);
let draft_from = this.model.from;
if (!from) {
console.log(`Compose: Could not find ${this.model.from} in ${this.draftDeskservice.fromsSubject.value}`);
@@ -915,4 +929,50 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit {
s => !currentrecipients.find(r => r.address === s.address)
).slice(0, 10);
}
+
+ private updateDefaultBcc(selectedFromAddress: string, emitEvent = true): void {
+ if (!this.formGroup || !this.formGroup.controls.bcc) {
+ return;
+ }
+
+ const nextDefaultBcc = DraftFormModel.defaultBccRecipients(this.identityForAddress(selectedFromAddress));
+ const currentBcc: MailAddressInfo[] = this.model.bcc || [];
+ const bccWithoutPreviousDefault = currentBcc.filter((recipient) =>
+ !this.defaultBccRecipients.some((defaultRecipient) => this.sameEmailAddress(recipient, defaultRecipient)));
+ const mergedBcc = bccWithoutPreviousDefault.slice();
+
+ for (const defaultRecipient of nextDefaultBcc) {
+ if (!mergedBcc.some((recipient) => this.sameEmailAddress(recipient, defaultRecipient))) {
+ mergedBcc.push(defaultRecipient);
+ }
+ }
+
+ this.defaultBccRecipients = nextDefaultBcc;
+ this.model.bcc = mergedBcc;
+ this.hasBCC = mergedBcc.length > 0;
+ this.formGroup.controls.bcc.setValue(mergedBcc, { emitEvent });
+ }
+
+ private trackDefaultBcc(selectedFromAddress: string): void {
+ this.defaultBccRecipients = DraftFormModel.defaultBccRecipients(this.identityForAddress(selectedFromAddress));
+ }
+
+ private identityForAddress(selectedFromAddress: string): Identity {
+ if (!selectedFromAddress) {
+ return null;
+ }
+
+ const selectedAddresses = MailAddressInfo.parse(selectedFromAddress)
+ .map((address) => address.address.toLowerCase())
+ .filter((address) => address);
+ return this.draftDeskservice.fromsSubject.value.find((identity) =>
+ identity.nameAndAddress === selectedFromAddress ||
+ identity.email === selectedFromAddress ||
+ selectedAddresses.includes(identity.email.toLowerCase())
+ );
+ }
+
+ private sameEmailAddress(left: MailAddressInfo, right: MailAddressInfo): boolean {
+ return (left?.address || '').toLowerCase() === (right?.address || '').toLowerCase();
+ }
}
diff --git a/src/app/compose/draftdesk.service.spec.ts b/src/app/compose/draftdesk.service.spec.ts
index 0b4f6bbff..a81c2f399 100644
--- a/src/app/compose/draftdesk.service.spec.ts
+++ b/src/app/compose/draftdesk.service.spec.ts
@@ -410,6 +410,78 @@ Subject: Test subject
expect(draft.isUnsaved()).toBe(false);
done();
});
+
+ it('Create: applies identity default bcc only to new drafts', (done) => {
+ let draft = DraftFormModel.create(
+ -1,
+ Identity.fromObject({'email': 'sender@runbox.com', 'default_bcc': 'archive@runbox.com'}),
+ 'recipient@runbox.com',
+ 'Test subject');
+
+ expect(draft.bcc.length).toBe(1);
+ expect(draft.bcc[0].address).toBe('archive@runbox.com');
+
+ draft = DraftFormModel.create(
+ 12345,
+ Identity.fromObject({'email': 'sender@runbox.com', 'default_bcc': 'archive@runbox.com'}),
+ 'recipient@runbox.com',
+ 'Saved subject');
+
+ expect(draft.bcc).toEqual([]);
+ done();
+ });
+
+ it('Reply: applies identity default bcc from the selected From identity', (done) => {
+ const replydraft = DraftFormModel.reply({
+ headers: {
+ 'message-id': 'reply-default-bcc',
+ },
+ from: [
+ {address: 'sender@runbox.com', name: 'Sender'}
+ ],
+ to: [
+ {address: 'identity@runbox.com', name: 'Identity'}
+ ],
+ date: mailDate,
+ subject: 'Test subject',
+ rawtext: 'blabla',
+ sanitized_html: '
blabla
', + }, + [ Identity.fromObject({'email': 'identity@runbox.com', 'default_bcc': 'archive@runbox.com'})], + false, + false); + + expect(replydraft.from).toBe('identity@runbox.com'); + expect(replydraft.bcc.length).toBe(1); + expect(replydraft.bcc[0].address).toBe('archive@runbox.com'); + done(); + }); + + it('Forward: applies identity default bcc from the selected From identity', (done) => { + const draft = DraftFormModel.forward({ + headers: { + 'message-id': 'forward-default-bcc', + }, + from: [ + {address: 'sender@runbox.com', name: 'Sender'} + ], + to: [ + {address: 'identity@runbox.com', name: 'Identity'} + ], + attachments: [], + date: mailDate, + subject: 'Test subject', + rawtext: 'blabla', + sanitized_html: 'blabla
', + }, + [ Identity.fromObject({'email': 'identity@runbox.com', 'default_bcc': 'archive@runbox.com'})], + false); + + expect(draft.from).toBe('identity@runbox.com'); + expect(draft.bcc.length).toBe(1); + expect(draft.bcc[0].address).toBe('archive@runbox.com'); + done(); + }); }); describe('DraftDeskService', () => { diff --git a/src/app/compose/draftdesk.service.ts b/src/app/compose/draftdesk.service.ts index 6f1efa0be..6e131453e 100644 --- a/src/app/compose/draftdesk.service.ts +++ b/src/app/compose/draftdesk.service.ts @@ -76,6 +76,9 @@ export class DraftFormModel { ret.from = fromAddress.email; ret.mid = draftId; ret.to = to ? MailAddressInfo.parse(to) : []; + if (ret.isUnsaved()) { + ret.bcc = DraftFormModel.defaultBccRecipients(fromAddress); + } ret.subject = subject; if (preview) { // We create an element here because we want the plain text @@ -126,7 +129,8 @@ export class DraftFormModel { }); } } - ret.setFromForResponse(mailObj, froms); + const selectedFrom = ret.setFromForResponse(mailObj, froms); + ret.bcc = DraftFormModel.defaultBccRecipients(selectedFrom); ret.setSubjectForResponse(mailObj, 'Re: '); const localTZ = moment.tz.guess(); @@ -166,9 +170,18 @@ export class DraftFormModel { return ret; } + public static defaultBccRecipients(identity: Identity): MailAddressInfo[] { + if (!identity || !identity.default_bcc || !identity.default_bcc.trim()) { + return []; + } + return MailAddressInfo.parse(identity.default_bcc) + .filter(recipient => recipient.address); + } + public static forward(mailObj, froms: Identity[], useHTML: boolean): DraftFormModel { const ret = new DraftFormModel(); - ret.setFromForResponse(mailObj, froms); + const selectedFrom = ret.setFromForResponse(mailObj, froms); + ret.bcc = DraftFormModel.defaultBccRecipients(selectedFrom); const fwdFromNameStr = mailObj.from[0].name ? `"${mailObj.from[0].name}" <${mailObj.from[0].address}>` @@ -216,15 +229,19 @@ ${mailObj.sanitized_html}`; return false; } - private setFromForResponse(mailObj, froms: Identity[]): void { - if (froms.length > 0) { - this.from = ( - [].concat(mailObj.to || []).concat(mailObj.cc || []).find( - addr => froms.find(fromObj => fromObj.email === addr.address.toLowerCase()) - ) || { address: froms[0].email } - ).address.toLowerCase(); + private setFromForResponse(mailObj, froms: Identity[]): Identity { + if (froms && froms.length > 0) { + const recipientAddress = [].concat(mailObj.to || []).concat(mailObj.cc || []).find( + addr => froms.find(fromObj => fromObj.email.toLowerCase() === addr.address.toLowerCase()) + ); + const selectedFrom = recipientAddress + ? froms.find(fromObj => fromObj.email.toLowerCase() === recipientAddress.address.toLowerCase()) + : froms[0]; + this.from = selectedFrom.email.toLowerCase(); + return selectedFrom; } else { console.error('DraftDesk: No froms passed to setFromForResponse'); + return null; } } diff --git a/src/app/profiles/profile.service.ts b/src/app/profiles/profile.service.ts index 56ad178a8..0bf856c25 100644 --- a/src/app/profiles/profile.service.ts +++ b/src/app/profiles/profile.service.ts @@ -36,6 +36,7 @@ export class Identity { name: string; reference_type: string; reply_to: string; + default_bcc: string; signature: string; type: string; smtp_address: string; diff --git a/src/app/profiles/profiles.editor.modal.html b/src/app/profiles/profiles.editor.modal.html index 02007f5c2..bcfeec338 100644 --- a/src/app/profiles/profiles.editor.modal.html +++ b/src/app/profiles/profiles.editor.modal.html @@ -88,6 +88,17 @@ +