From d2aa8fbed50703ab705ecdf7f257ff539230b3a5 Mon Sep 17 00:00:00 2001 From: Geir Thomas Andersen Date: Sat, 19 Oct 2024 15:44:27 +0200 Subject: [PATCH 001/131] feat(welcome): Show improved 3-year plans on Welcome page upon signup. --- .../account-upgrades.component.html | 29 +++++++++++-------- .../account-upgrades.component.scss | 6 ++-- src/app/welcome/welcomedesk.component.html | 12 ++++---- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/app/account-app/account-upgrades.component.html b/src/app/account-app/account-upgrades.component.html index 851d57464..a6b26cfce 100644 --- a/src/app/account-app/account-upgrades.component.html +++ b/src/app/account-app/account-upgrades.component.html @@ -21,10 +21,10 @@

By subscribing to Runbox you support an independent and sustainable business in Norway, where your email will stay secure and private indefinitely.

- To view your existing subscriptions, please go to Your Subscriptions. + Feel free to contact us via Runbox Support if you have any questions about our plans.

- Feel free to contact us via Runbox Support if you have any questions about our plans. + To view your existing subscriptions, please go to Your Subscriptions.

--> -

Our recommended plans

+

Our Recommended Plans

- We offer a 20% discount on all our plans if you renew for 3 years, which locks in the current price for your entire selected subcription period. To review and compare all our plans, please see the table further down. + We currently offer a 20% discount on all our main account plans when you subscribe or renew for 3 years at a time.

+

+ Lock in the current price for less than the price of a cup of coffee per month! +

-

+

@@ -90,6 +93,7 @@

One extra year for free!

+ {{ plan.name.replace('Runbox', '') }} @@ -99,7 +103,7 @@

Your current subscription

An affordable plan if you don't need a lot of email or file storage.

-

Our recommended plan with plenty of email and file storage, suitable for most people.

+

Suitable for most people, a popular plan with plenty of email and file storage.

A solid plan built to last, perfect for professionals and businesses.

- - + + @@ -42,7 +45,7 @@ {{t.nameAndAddress}} - +
- +
-
- +
+ - +

Drop files here

@@ -127,11 +130,11 @@

Drop files here

- + {{this.model.preview}} - +
+ +
From f60a906b5e6db597ef3666376429d9dcc6304690 Mon Sep 17 00:00:00 2001 From: Bas Date: Tue, 29 Oct 2024 17:12:37 +0200 Subject: [PATCH 008/131] fixup! feat(templates): Save draft as template --- src/app/compose/compose.component.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/app/compose/compose.component.ts b/src/app/compose/compose.component.ts index c0a5d4c94..b76291493 100644 --- a/src/app/compose/compose.component.ts +++ b/src/app/compose/compose.component.ts @@ -77,6 +77,7 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit { public uploadRequest: Subscription = null; public saved: Date = null; public tinymce_plugin: TinyMCEPlugin; + public isTemplate: boolean = false; finishImageUpload: AsyncSubject = null; uploadProgress: BehaviorSubject = new BehaviorSubject(-1); has_pasted_signature: boolean; @@ -673,6 +674,7 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit { } public submit(send: boolean = false) { + const { isTemplate } = this; if (this.savingInProgress) { return; } @@ -780,7 +782,7 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit { } else { this.rmmapi.me.pipe(mergeMap((me) => { return this.http.post('/rest/v1/draft', { - type: 'draft', + type: isTemplate ? 'template' : 'draft', username: me.username, from: from && from.id ? from.from_name + '%' + from.email + '%' + from.id : from ? from.email : undefined, from_email: from ? from.email : '', @@ -796,6 +798,7 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit { ctype: this.model.useHTML ? 'html' : null, save: send ? 'Send' : 'Save', mid: this.model.mid, + ...(isTemplate ? {tid: this.model.mid} : {}), attachments: this.model.attachments ? this.model.attachments .filter((att) => att.file !== 'UTF-8Q') @@ -839,6 +842,11 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit { } } + public saveTemplate() { + this.isTemplate = true + this.submit(false); + } + ngOnDestroy() { if (this.editor) { this.tinymce_plugin.remove(this.editor); From b44dc7311ee1cbc151712b920533bdbdc2b4663d Mon Sep 17 00:00:00 2001 From: Bas Date: Tue, 29 Oct 2024 17:50:23 +0200 Subject: [PATCH 009/131] fixup! fixup! feat(templates): Create draft on template item click --- src/app/compose/draftdesk.service.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/app/compose/draftdesk.service.ts b/src/app/compose/draftdesk.service.ts index b56b6645d..965890622 100644 --- a/src/app/compose/draftdesk.service.ts +++ b/src/app/compose/draftdesk.service.ts @@ -26,7 +26,7 @@ import { MailAddressInfo } from '../common/mailaddressinfo'; import { MessageListService } from '../rmmapi/messagelist.service'; import { MessageTableRowTool} from '../messagetable/messagetablerow'; import { Identity, ProfileService } from '../profiles/profile.service'; -import { from, of, BehaviorSubject, forkJoin } from 'rxjs'; +import { from, of, BehaviorSubject } from 'rxjs'; import { map, mergeMap, bufferCount, take, distinctUntilChanged } from 'rxjs/operators'; import moment from 'moment'; @@ -314,15 +314,17 @@ export class DraftDeskService { messageId: number, ) { - forkJoin([ - this.rmmapi.getMessageFields(messageId), - this.rbwebmailapi.getMessageContents(messageId) - ]).subscribe(([fields, contents]) => { + this.rbwebmailapi.getMessageContents(messageId).subscribe((contents) => { + const res: any = Object.assign({}, contents); + + const {to: {value: [{name, address}]}, subject} = res.headers + const to = new MailAddressInfo(name, address).nameAndAddress; + const draftFormModel = DraftFormModel.create( -1, this.mainIdentity(), - fields.from, - fields.subject + to, + subject ) draftFormModel.msg_body = contents.text.text; From 2558dcf2d4f333cea80dd5dbac14b926b217354a Mon Sep 17 00:00:00 2001 From: Bas Date: Wed, 30 Oct 2024 12:22:11 +0200 Subject: [PATCH 010/131] fixup! fixup! feat(templates): Create draft on template item click --- src/app/app.component.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index edb1fb8d4..aa9c1e2b6 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,18 +1,18 @@ // --------- BEGIN RUNBOX LICENSE --------- // Copyright (C) 2016-2022 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 ---------- @@ -74,7 +74,7 @@ const LOCAL_STORAGE_SHOW_UNREAD_ONLY = 'rmm7mailViewerShowUnreadOnly'; const LOCAL_STORAGE_SHOW_POPULAR_RECIPIENTS = 'showPopularRecipients'; const LOCAL_STORAGE_INDEX_PROMPT = 'localSearchPromptDisplayed'; const TOOLBAR_LIST_BUTTON_WIDTH = 30; - + @Component({ moduleId: 'angular2/app/', // eslint-disable-next-line @angular-eslint/component-selector @@ -718,7 +718,7 @@ export class AppComponent implements OnInit, AfterViewInit, CanvasTableSelectLis ) - 1; } } - + public openMarkOpMenu() { this.showSelectMarkOpMenu = true; @@ -929,7 +929,6 @@ export class AppComponent implements OnInit, AfterViewInit, CanvasTableSelectLis this.canvastable.rows.rowSelected(rowIndex, columnIndex, multiSelect); this.showSelectOperations = this.canvastable.rows.anySelected(); - if (this.canvastable.rows.hasChanges) { this.updateUrlFragment(this.canvastable.rows.getRowMessageId(rowIndex)); this.singlemailviewer.messageId = this.canvastable.rows.getRowMessageId(rowIndex); From 5378a3537453ef8ebb51c1dcfe81404e77447981 Mon Sep 17 00:00:00 2001 From: Bas Date: Wed, 30 Oct 2024 12:28:19 +0200 Subject: [PATCH 011/131] fixup! fixup! feat(templates): Save draft as template --- src/app/compose/compose.component.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/app/compose/compose.component.ts b/src/app/compose/compose.component.ts index b76291493..9affe5db0 100644 --- a/src/app/compose/compose.component.ts +++ b/src/app/compose/compose.component.ts @@ -1,18 +1,18 @@ // --------- BEGIN RUNBOX LICENSE --------- // Copyright (C) 2016-2022 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 ---------- @@ -675,10 +675,13 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit { public submit(send: boolean = false) { const { isTemplate } = this; + if (this.savingInProgress) { return; } + this.savingInProgress = true; + if (send) { let validemails = false; validemails = isValidEmailArray(this.model.to); From 345e85836cd6b19bb126582f1e33558614951e36 Mon Sep 17 00:00:00 2001 From: Bas Date: Wed, 30 Oct 2024 12:29:08 +0200 Subject: [PATCH 012/131] fixup! fixup! fixup! feat(templates): Create draft on template item click --- src/app/compose/draftdesk.service.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/app/compose/draftdesk.service.ts b/src/app/compose/draftdesk.service.ts index 965890622..4aeda173d 100644 --- a/src/app/compose/draftdesk.service.ts +++ b/src/app/compose/draftdesk.service.ts @@ -1,18 +1,18 @@ // --------- BEGIN RUNBOX LICENSE --------- // Copyright (C) 2016-2022 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 ---------- @@ -131,7 +131,7 @@ export class DraftFormModel { const localTZ = moment.tz.guess(); const replyHeaderHTML = 'On ' + moment(mailObj.date, localTZ).format('yyyy-MM-DD HH:mm Z') - + ' ' + moment.tz(localTZ).format('z') + + ' ' + moment.tz(localTZ).format('z') + ', ' + (mailObj.from[0].name ? `"${mailObj.from[0].name}" <${mailObj.from[0].address}> wrote:` @@ -316,10 +316,8 @@ export class DraftDeskService { this.rbwebmailapi.getMessageContents(messageId).subscribe((contents) => { const res: any = Object.assign({}, contents); - const {to: {value: [{name, address}]}, subject} = res.headers const to = new MailAddressInfo(name, address).nameAndAddress; - const draftFormModel = DraftFormModel.create( -1, this.mainIdentity(), From 33174e46ff33c0bbbfee1fe32fd59104da75c72f Mon Sep 17 00:00:00 2001 From: Bas Date: Wed, 30 Oct 2024 12:31:39 +0200 Subject: [PATCH 013/131] fixup! fixup! fixup! feat(templates): Create draft on template item click --- src/app/app.component.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index aa9c1e2b6..7473ac6f6 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -921,9 +921,10 @@ export class AppComponent implements OnInit, AfterViewInit, CanvasTableSelectLis if ((this.selectedFolder === this.messagelistservice.templateFolderName) && !isSelect) { this.draftDeskService.newTemplateDraft( this.canvastable.rows.getRowMessageId(rowIndex) - ) - this.drafts() - return + ); + this.drafts(); + + return; } this.canvastable.rows.rowSelected(rowIndex, columnIndex, multiSelect); From ce3a11725406cbc7d7b87aaa3342e940396314f1 Mon Sep 17 00:00:00 2001 From: Bas Date: Thu, 31 Oct 2024 16:13:49 +0200 Subject: [PATCH 014/131] fixup! fixup! fixup! fixup! feat(templates): Create draft on template item click --- src/app/compose/draftdesk.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/compose/draftdesk.service.ts b/src/app/compose/draftdesk.service.ts index 4aeda173d..678c3b2d4 100644 --- a/src/app/compose/draftdesk.service.ts +++ b/src/app/compose/draftdesk.service.ts @@ -240,7 +240,6 @@ export class DraftDeskService { constructor(public rmmapi: RunboxWebmailAPI, private messagelistservice: MessageListService, - private rbwebmailapi: RunboxWebmailAPI, private profileService: ProfileService, private http: HttpClient ) { @@ -314,7 +313,7 @@ export class DraftDeskService { messageId: number, ) { - this.rbwebmailapi.getMessageContents(messageId).subscribe((contents) => { + this.rmmapi.getMessageContents(messageId).subscribe((contents) => { const res: any = Object.assign({}, contents); const {to: {value: [{name, address}]}, subject} = res.headers const to = new MailAddressInfo(name, address).nameAndAddress; From c9fbaab5c400de2bd9b0315dd06ad8640d80ce84 Mon Sep 17 00:00:00 2001 From: Bas Date: Thu, 31 Oct 2024 16:16:12 +0200 Subject: [PATCH 015/131] fixup! feat(templates): Save draft as template --- src/app/compose/compose.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/compose/compose.component.html b/src/app/compose/compose.component.html index 5650272b3..ff3ac319a 100644 --- a/src/app/compose/compose.component.html +++ b/src/app/compose/compose.component.html @@ -25,7 +25,7 @@ - -
+
diff --git a/src/app/account-details/personal-details.component.ts b/src/app/account-details/personal-details.component.ts index 4b18882ae..5060eedf4 100644 --- a/src/app/account-details/personal-details.component.ts +++ b/src/app/account-details/personal-details.component.ts @@ -48,6 +48,7 @@ export class PersonalDetailsComponent { modal_password_ref; details: Subject = new Subject(); + is_alternative_email_validated = true; selectedCountry: any; selectedTimezone: any; @@ -60,6 +61,7 @@ export class PersonalDetailsComponent { ) { this.details.subscribe((details: AccountDetailsInterface) => { this.detailsForm.patchValue(details); + this.is_alternative_email_validated = details.email_alternative_status === 0; }); this.loadDetails(); @@ -158,10 +160,20 @@ export class PersonalDetailsComponent { .post('/rest/v1/account/details', updates) .pipe(map((res: HttpResponse) => res['result'])) .subscribe((details) => { - this.details.next(details); + if(details && details.email_alternative) { + this.details.next(details); + this.rmm.show_error('Account details updated', 'Dismiss'); + } else { + this.rmm.show_error('Failed to update details', 'Dismiss'); + } }); + } - this.rmm.show_error('Account details updated', 'Dismiss'); + public validate_alt_email() { + this.http.post('/rest/v1/account/alt_email_validation', {}) + .subscribe((res) => { + this.rmm.show_error('Validation email resent', 'Dismiss'); + });; } show_modal_password() { diff --git a/src/app/app.component.html b/src/app/app.component.html index 4b5552522..e6d1e1296 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -42,6 +42,9 @@

(Experimental) create a video call

+

+ Please confirm your Alternative Email address Visit the Account Details page to resend the email +

- +
diff --git a/src/app/app.component.html b/src/app/app.component.html index e6d1e1296..b0679f49b 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -43,7 +43,8 @@ (Experimental) create a video call

- Please confirm your Alternative Email address Visit the Account Details page to resend the email + Please confirm your Alternative Email address. + Visit the Account Details page to resend the email

From 55d8a41b6a5b43e157aa35d19cd582cca8c6e4f1 Mon Sep 17 00:00:00 2001 From: Geir Thomas Andersen Date: Sun, 17 Nov 2024 22:55:20 +0100 Subject: [PATCH 028/131] sstyle(account): Add alternative email address validation indicator. --- src/app/account-details/personal-details.component.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/account-details/personal-details.component.html b/src/app/account-details/personal-details.component.html index d04286053..07b8b6793 100644 --- a/src/app/account-details/personal-details.component.html +++ b/src/app/account-details/personal-details.component.html @@ -22,6 +22,8 @@

Personal Details

Alternative email address + + Phone number (optional) From fc4459429057fda7bbfffbf407e1e00b81b6db09 Mon Sep 17 00:00:00 2001 From: Geir Thomas Andersen Date: Sun, 17 Nov 2024 22:27:56 +0100 Subject: [PATCH 029/131] style(payment): Include instructions about crypto addresses (replaces PR 1582). --- .../account-receipt.component.html | 72 ++++++++++--------- 1 file changed, 40 insertions(+), 32 deletions(-) diff --git a/src/app/account-app/account-receipt.component.html b/src/app/account-app/account-receipt.component.html index 2c5999cae..1e5751f12 100644 --- a/src/app/account-app/account-receipt.component.html +++ b/src/app/account-app/account-receipt.component.html @@ -70,16 +70,24 @@

-

Cryptocurrency transfer

+

Cryptocurrency transfer

- Choose one of the available cryptocurencies below and send the correct amount to the specified address by copying and pasting it into your cryptocurency app or scanning the QR code with your mobile phone camera. + Choose one of the available cryptocurencies below and send the correct amount (converted from USD to the desired currency) to the specified address by copying and pasting it into your cryptocurency app or scanning the QR code with your mobile phone camera.

- Please let us know via an email to billing@runbox.com that you have paid with a cryptocurrency transfer so that we can apply the payment to your account. Be advised that it may take 1-2 days for the payment to be registered and your subscription updated. + Please notify us via billing@runbox.com that you have paid with a cryptocurrency transfer and include your crypto address so that we can apply the payment to your account.

+

+ Be advised that it may take 1-2 days for the payment to be registered and your subscription updated. +

+ +

+ Runbox Crypto Addresses +

+

Bitcoin:

@@ -140,43 +148,43 @@

-

SWIFT transfer

+

SWIFT transfer

Runbox Solutions accepts SWIFT and SEPA payments, but please be aware that this is an expensive payment alternative as the sender must pay the transaction cost. It may take a while for us to receive and register your payment as it cannot be automatically applied to your account.

- Payment should be made from a bank account in your name, otherwise delays may occur in payments being applied to your Runbox account. Please contact us upon paying to make sure your payment is registered as efficiently as possible. -

- - - - - - - - - - - -
IBAN: NO29 1503 2111 731
SWIFT: DNBANOKKXXX
Address: Runbox Solutions AS, Oscars gate 27, 0352 Oslo, Norway
Bank address: DNB Bank ASA, 0021 Oslo, Norway
- -

Cash payment

- -

- Send the cash via registered or regular mail (not recommended) to: Runbox Solutions AS, Oscars gate 27, 0352 Oslo, Norway. -

- -

+ Payment should be made from a bank account in your name, otherwise delays may occur in payments being applied to your Runbox account. Please contact us at billing@runbox.com upon paying to make sure your payment is registered as efficiently as possible. +

+ + + + + + + + + + + +
IBAN: NO29 1503 2111 731
SWIFT: DNBANOKKXXX
Address: Runbox Solutions AS, Oscars gate 27, 0352 Oslo, Norway
Bank address: DNB Bank ASA, 0021 Oslo, Norway
+ +

Cash payment

+ +

+ Send the cash via registered or regular mail (not recommended) to: Runbox Solutions AS, Oscars gate 27, 0352 Oslo, Norway. +

+ +

Be advised that payments sent by regular mail may be delayed or lost, and that they require manual processing. Therefore you should send such payments well ahead (at least 3 weeks) of your trial or subscription period's expiration.

You may avoid delays and service disruptions by scanning (or taking a picture of) the payment prior to sending it in the mail, and emailing it to billing@runbox.com. -

- -

- Note: Please mark the payment with your Runbox username and the Transaction ID above so we can apply the payment to your account. -

-
+

+ +

+ Note: Please mark the payment with your Runbox username and the Transaction ID above so we can apply the payment to your account. +

+

From c19eeae7a0896c4c9ce3a0a5c2378d1429231a35 Mon Sep 17 00:00:00 2001 From: Geir Thomas Andersen Date: Mon, 18 Nov 2024 19:48:57 +0100 Subject: [PATCH 030/131] fix(payment): Fix equality signs. --- src/app/account-app/account-receipt.component.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/account-app/account-receipt.component.html b/src/app/account-app/account-receipt.component.html index 1e5751f12..42c9aa5b4 100644 --- a/src/app/account-app/account-receipt.component.html +++ b/src/app/account-app/account-receipt.component.html @@ -1,4 +1,4 @@ -
+

Payment receipt

Thank you for your purchase. The details for your transaction are shown below. @@ -8,7 +8,7 @@

Payment receipt

-
+

Pending payment

The details for your pending transaction are shown below. @@ -69,7 +69,7 @@

-
+

Cryptocurrency transfer

@@ -146,7 +146,7 @@

-
+

SWIFT transfer

From a5b1c4385e4ffced3d6d61ee8d2e73cf83d41cca Mon Sep 17 00:00:00 2001 From: Bas Date: Mon, 18 Nov 2024 15:14:22 +0000 Subject: [PATCH 031/131] build(app): Update file hashes after sentry instrumentation --- package-lock.json | 268 ++++++++++++++++++++++++++++++++++++++++ package.json | 1 + src/build/post-build.js | 2 + 3 files changed, 271 insertions(+) diff --git a/package-lock.json b/package-lock.json index d919f1364..32dadb48e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,7 @@ "@angular/compiler-cli": "15.1.3", "@angular/language-service": "^15.2.7", "@cypress/webpack-preprocessor": "^5.17.0", + "@sentry/cli": "^2.38.2", "@types/jasmine": "~4.3.1", "@types/jasminewd2": "~2.0.10", "@types/node": "^14.14.31", @@ -5758,6 +5759,210 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, + "node_modules/@sentry/cli": { + "version": "2.38.2", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.38.2.tgz", + "integrity": "sha512-CR0oujpAnhegK2pBAv6ZReMqbFTuNJLDZLvoD1B+syrKZX+R+oxkgT2e1htsBbht+wGxAsluVWsIAydSws1GAA==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.7", + "progress": "^2.0.3", + "proxy-from-env": "^1.1.0", + "which": "^2.0.2" + }, + "bin": { + "sentry-cli": "bin/sentry-cli" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@sentry/cli-darwin": "2.38.2", + "@sentry/cli-linux-arm": "2.38.2", + "@sentry/cli-linux-arm64": "2.38.2", + "@sentry/cli-linux-i686": "2.38.2", + "@sentry/cli-linux-x64": "2.38.2", + "@sentry/cli-win32-i686": "2.38.2", + "@sentry/cli-win32-x64": "2.38.2" + } + }, + "node_modules/@sentry/cli-darwin": { + "version": "2.38.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.38.2.tgz", + "integrity": "sha512-21ywIcJCCFrCTyiF1o1PaT7rbelFC2fWmayKYgFElnQ55IzNYkcn8BYhbh/QknE0l1NBRaeWMXwTTdeoqETCCg==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm": { + "version": "2.38.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.38.2.tgz", + "integrity": "sha512-+AiKDBQKIdQe4NhBiHSHGl0KR+b//HHTrnfK1SaTrOm9HtM4ELXAkjkRF3bmbpSzSQCS5WzcbIxxCJOeaUaO0A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm64": { + "version": "2.38.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.38.2.tgz", + "integrity": "sha512-4Fp/jjQpNZj4Th+ZckMQvldAuuP0ZcyJ9tJCP1CCOn5poIKPYtY6zcbTP036R7Te14PS4ALOcDNX3VNKfpsifA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-i686": { + "version": "2.38.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.38.2.tgz", + "integrity": "sha512-6zVJN10dHIn4R1v+fxuzlblzVBhIVwsaN/S7aBED6Vn1HhAyAcNG2tIzeCLGeDfieYjXlE2sCI82sZkQBCbAGw==", + "cpu": [ + "x86", + "ia32" + ], + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-x64": { + "version": "2.38.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.38.2.tgz", + "integrity": "sha512-4UiLu9zdVtqPeltELR5MDGKcuqAdQY9xz3emISuA6bm+MXGbt2W1WgX+XY3GElwjZbmH8qpyLUEd34sw6sdcbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-i686": { + "version": "2.38.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.38.2.tgz", + "integrity": "sha512-DYfSvd5qLPerLpIxj3Xu2rRe3CIlpGOOfGSNI6xvJ5D8j6hqbOHlCzvfC4oBWYVYGtxnwQLMeDGJ7o7RMYulig==", + "cpu": [ + "x86", + "ia32" + ], + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-x64": { + "version": "2.38.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.38.2.tgz", + "integrity": "sha512-W5UX58PKY1hNUHo9YJxWNhGvgvv2uOYHI27KchRiUvFYBIqlUUcIdPZDfyzetDfd8qBCxlAsFnkL2VJSNdpA9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@sentry/cli/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@sentry/cli/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@sentry/cli/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/@sentry/core": { "version": "5.15.5", "resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.15.5.tgz", @@ -14437,6 +14642,27 @@ "dev": true, "optional": true }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -18771,6 +18997,16 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -18949,6 +19185,13 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, "node_modules/prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -21078,6 +21321,13 @@ "node": ">=0.8" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -21942,6 +22192,13 @@ "node": ">=0.8.0" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/webpack": { "version": "5.76.1", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz", @@ -22220,6 +22477,17 @@ "node": ">=0.8.0" } }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index fb3235879..b0d20cb7c 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "@angular/compiler-cli": "15.1.3", "@angular/language-service": "^15.2.7", "@cypress/webpack-preprocessor": "^5.17.0", + "@sentry/cli": "^2.38.2", "@types/jasmine": "~4.3.1", "@types/jasminewd2": "~2.0.10", "@types/node": "^14.14.31", diff --git a/src/build/post-build.js b/src/build/post-build.js index 50adca24f..18e08b114 100644 --- a/src/build/post-build.js +++ b/src/build/post-build.js @@ -3,6 +3,8 @@ const chalk = require('chalk'); execSync('git checkout src/app/buildtimestamp.ts'); execSync('git checkout ngsw-config.json'); +execSync('npx sentry-cli sourcemaps inject ./dist/'); +execSync('npx ngsw-config ./dist ngsw-config.json "/app/"'); const changelogUpdated = execSync('git status --porcelain src/app/changelog/changes.ts').toString().trim(); if (changelogUpdated) { From 1b00060c1b769cb7ffd848dfb2bf91a94dada81f Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Tue, 19 Nov 2024 15:59:08 +0000 Subject: [PATCH 032/131] fix(receipt): Ensure receipt is loaded before checking its status --- .../account-receipt.component.html | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/app/account-app/account-receipt.component.html b/src/app/account-app/account-receipt.component.html index 42c9aa5b4..3dd52c65e 100644 --- a/src/app/account-app/account-receipt.component.html +++ b/src/app/account-app/account-receipt.component.html @@ -1,31 +1,31 @@ -
-

Payment receipt

-

- Thank you for your purchase. The details for your transaction are shown below. -

-

- You may save or print this receipt, which you will also receive by email in your Runbox account. -

-
+
+
+

Payment receipt

+

+ Thank you for your purchase. The details for your transaction are shown below. +

+

+ You may save or print this receipt, which you will also receive by email in your Runbox account. +

+
-
-

Pending payment

-

- The details for your pending transaction are shown below. -

-

- To complete your payment, please follow the instructions further down. -

-

- You may save or print this receipt, which you will also receive by email in your Runbox account. -

-
+
+

Pending payment

+

+ The details for your pending transaction are shown below. +

+

+ To complete your payment, please follow the instructions further down. +

+

+ You may save or print this receipt, which you will also receive by email in your Runbox account. +

+
-

- Transaction summary -

+

+ Transaction summary +

-
From ca0f98f028955e46da67811b15603c2d8edc72b9 Mon Sep 17 00:00:00 2001 From: Bas Date: Tue, 19 Nov 2024 16:17:22 +0000 Subject: [PATCH 033/131] build(app): Update build ngsw.json with appData Set the appData after re-calculation of ngsw file hashes as it overwrites it on re-calculation. --- src/build/post-build.js | 20 +++++++++++++++++++- src/build/pre-build.js | 9 --------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/build/post-build.js b/src/build/post-build.js index 18e08b114..78b0e7bb2 100644 --- a/src/build/post-build.js +++ b/src/build/post-build.js @@ -1,11 +1,29 @@ -const execSync = require('child_process').execSync; +const path = require('node:path'); +const execSync = require('node:child_process').execSync; const chalk = require('chalk'); +const fs = require('node:fs') execSync('git checkout src/app/buildtimestamp.ts'); execSync('git checkout ngsw-config.json'); execSync('npx sentry-cli sourcemaps inject ./dist/'); execSync('npx ngsw-config ./dist ngsw-config.json "/app/"'); +const ngswPath = path.join(__dirname, '../../dist/ngsw.json') +const ngswJSON = require(ngswPath); + +const build_time = new Date().toJSON(); +const commit = execSync('git rev-parse --short HEAD').toString().trim(); +const build_epoch = execSync('git show --pretty="%ct" --no-patch').toString().trim(); + +ngswJSON.appData = { + ...ngswJSON.appData, + commit, + build_time, + build_epoch, +} + +fs.writeFileSync(ngswPath, JSON.stringify(ngswJSON, null, 2)) + const changelogUpdated = execSync('git status --porcelain src/app/changelog/changes.ts').toString().trim(); if (changelogUpdated) { console.log(chalk.green(` diff --git a/src/build/pre-build.js b/src/build/pre-build.js index eb20f441d..e71a1a43d 100644 --- a/src/build/pre-build.js +++ b/src/build/pre-build.js @@ -45,12 +45,3 @@ if (dirty) { console.log('Please commit your changes before the build'); process.exit(1); } - -let config = fs.readFileSync('ngsw-config.json').toString(); -const hash = cp.execSync('git rev-parse --short HEAD').toString().trim(); -const epoch = cp.execSync('git show --pretty="%ct" --no-patch').toString().trim(); -console.log(`Setting appData commit hash to ${hash}`); -config = config.replace('__COMMIT_HASH__', hash); -config = config.replace('__BUILD_TIME__', build_time); -config = config.replace('__BUILD_EPOCH__', epoch); -fs.writeFileSync('ngsw-config.json', config); From c5cefcd31157d85aebb21d6799ebc54a5bceacd1 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Tue, 19 Nov 2024 11:01:01 +0000 Subject: [PATCH 034/131] fix(sidebar): Display Alt validation warning if unvalidated Invert meaning, instead of display when validated! --- src/app/app.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/app.component.html b/src/app/app.component.html index b0679f49b..1ad61ca9f 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -42,7 +42,7 @@

(Experimental) create a video call

-

+

Please confirm your Alternative Email address. Visit the Account Details page to resend the email

From aa72582a1c23bf1e345ca762963d62b01143fdb1 Mon Sep 17 00:00:00 2001 From: Bas Date: Wed, 20 Nov 2024 12:14:06 +0000 Subject: [PATCH 035/131] fix(account-details): Use phone icon instead of email icon near phone input --- src/app/account-details/personal-details.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/account-details/personal-details.component.html b/src/app/account-details/personal-details.component.html index 07b8b6793..9b3f24568 100644 --- a/src/app/account-details/personal-details.component.html +++ b/src/app/account-details/personal-details.component.html @@ -21,9 +21,9 @@

Personal Details

Alternative email address + + - - Phone number (optional) From 3b46d622e98295bf7592d43472f6fea3b22823f4 Mon Sep 17 00:00:00 2001 From: Bas Date: Wed, 20 Nov 2024 16:55:53 +0000 Subject: [PATCH 036/131] fix(folders): Update folder count on send or save --- src/app/compose/compose.component.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/compose/compose.component.ts b/src/app/compose/compose.component.ts index 1e46cefe0..129cdcc15 100644 --- a/src/app/compose/compose.component.ts +++ b/src/app/compose/compose.component.ts @@ -37,6 +37,7 @@ import { RecipientsService } from './recipients.service'; import { isValidEmailArray } from './emailvalidator'; import { MailAddressInfo } from '../common/mailaddressinfo'; import { Identity } from '../profiles/profile.service'; +import { MessageListService } from '../rmmapi/messagelist.service'; import { MessageTableRowTool} from '../messagetable/messagetablerow'; import { DefaultPrefGroups, PreferencesService } from '../common/preferences.service'; import { objectEqualWithKeys } from '../common/util'; @@ -103,6 +104,7 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit { public snackBar: MatSnackBar, private rmmapi: RunboxWebmailAPI, public draftDeskservice: DraftDeskService, + private messageListService: MessageListService, private http: HttpClient, private formBuilder: UntypedFormBuilder, private location: Location, @@ -829,6 +831,7 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit { }); } )).subscribe((res: any) => { + this.messageListService.refreshFolderList(); if (this.isTemplate) { this.snackBar.open('Saved to templates', 'Ok', { duration: 3000 }); } From f2566dcc0002ef85adf51a1cdb1cc51ad8d59bae Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Thu, 21 Nov 2024 12:23:34 +0000 Subject: [PATCH 037/131] fix(receipt): Ensure receipt displays all sections --- src/app/account-app/account-receipt.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/account-app/account-receipt.component.html b/src/app/account-app/account-receipt.component.html index 3dd52c65e..c2940248e 100644 --- a/src/app/account-app/account-receipt.component.html +++ b/src/app/account-app/account-receipt.component.html @@ -1,5 +1,5 @@
-
+

Payment receipt

Thank you for your purchase. The details for your transaction are shown below. @@ -9,7 +9,7 @@

Payment receipt

-
+

Pending payment

The details for your pending transaction are shown below. From 5681b0af9611ce1b64edf855250f881eccc22b8f Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Mon, 25 Nov 2024 12:25:28 +0000 Subject: [PATCH 038/131] fix(messagelist): Show Templates folder contents from API Not indexed as it does not contain complete email messages. Also: fix #1222 --- src/app/app.component.ts | 6 ++---- src/app/rmmapi/messagelist.service.spec.ts | 4 ++-- src/app/rmmapi/messagelist.service.ts | 19 +++++-------------- src/app/xapian/index.worker.ts | 15 ++++++--------- 4 files changed, 15 insertions(+), 29 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 7473ac6f6..7e757931c 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1085,8 +1085,7 @@ export class AppComponent implements OnInit, AfterViewInit, CanvasTableSelectLis // FIXME: Make a "not indexed folder list" somewhere!? // moveMessagesToFolder cant see these cos not in index - if (this.selectedFolder !== this.messagelistservice.spamFolderName && - this.selectedFolder !== this.messagelistservice.trashFolderName) { + if (this.messagelistservice.unindexedFolders.includes(this.selectedFolder)) { // remove from current message display this.canvastable.rows.removeMessages(messageIds); this.searchService.moveMessagesToFolder(msgIds, folderPath); @@ -1267,8 +1266,7 @@ export class AppComponent implements OnInit, AfterViewInit, CanvasTableSelectLis console.log('us', this.usewebsocketsearch); if ( this.usewebsocketsearch || - this.selectedFolder === this.messagelistservice.spamFolderName || - this.selectedFolder === this.messagelistservice.trashFolderName + this.messagelistservice.unindexedFolders.includes(this.selectedFolder) ) { /* * Message table from database, shown if local search index is not present diff --git a/src/app/rmmapi/messagelist.service.spec.ts b/src/app/rmmapi/messagelist.service.spec.ts index ea7259f23..f3a650140 100644 --- a/src/app/rmmapi/messagelist.service.spec.ts +++ b/src/app/rmmapi/messagelist.service.spec.ts @@ -36,7 +36,7 @@ describe('MessageListService', () => { [3692892, 0, 12, 'inbox', 'Inbox', 'Inbox', 0], [3692893, 0, 125, 'sent', 'Sent', 'Sent', 0], [3693770, 0, 3, 'user', 'Subsent', 'Sent.Subsent', 1], - [3692894, 0, 2, 'spam', 'CustomSpamFolderName', 'CustomSpamFolderName', 0], + [3692894, 0, 2, 'spam', 'Spam', 'Spam', 0], [3692895, 3, 239, 'trash', 'Trash', 'Trash', 0], [3693665, 0, 6, 'user', 'EmailPrivacyTester', 'EmailPrivacyTester', 0] ].map(entry => new FolderListEntry( @@ -58,7 +58,7 @@ describe('MessageListService', () => { filter(folders => folders && folders.length > 0) ).subscribe(() => { - expect(msglistservice.spamFolderName).toBe('CustomSpamFolderName'); + expect(msglistservice.spamFolderName).toBe('Spam'); done(); }); }); diff --git a/src/app/rmmapi/messagelist.service.ts b/src/app/rmmapi/messagelist.service.ts index 8a29406ce..2588d6c49 100644 --- a/src/app/rmmapi/messagelist.service.ts +++ b/src/app/rmmapi/messagelist.service.ts @@ -62,6 +62,7 @@ export class MessageListService { trashFolderName = 'Trash'; spamFolderName = 'Spam'; + unindexedFolders = ['Trash', 'Spam', 'Templates']; templateFolderName = 'Templates'; ignoreUnreadInFolders = [ 'Sent' ]; @@ -125,9 +126,8 @@ export class MessageListService { this.searchservice.pipe(take(1)).subscribe(searchservice => { // searchservice / index worker uses currentFolder for checking counts searchservice.setCurrentFolder(folder); - if (!searchservice.localSearchActivated || - folder === this.spamFolderName || - folder === this.trashFolderName ) { + if (!searchservice.localSearchActivated + || this.unindexedFolders.includes(folder) ) { // Always fetch fresh folder listing when setting current folder this.fetchFolderMessages(true); @@ -186,15 +186,6 @@ export class MessageListService { return new Promise((resolve, _) => { this.rmmapi.getFolderList() .subscribe((folders) => { - const trashfolder = folders.find(folder => folder.folderType === 'trash'); - if (trashfolder) { - this.trashFolderName = trashfolder.folderName; - } - const spamfolder = folders.find(folder => folder.folderType === 'spam'); - if (spamfolder) { - this.spamFolderName = spamfolder.folderName; - } - this.folderListSubject.next(folders); resolve(folders); }); @@ -342,10 +333,10 @@ export class MessageListService { // we only have T/S messages now, so if index on // might not have this one // artificial count update - if (folderName === this.spamFolderName || folderName === this.trashFolderName) { + if (this.unindexedFolders.includes(folderName)) { this.folderCounts[folderName].total++; } - if (this.currentFolder === this.spamFolderName || this.currentFolder === this.trashFolderName) { + if (this.unindexedFolders.includes(this.currentFolder)) { this.folderCounts[folderName].total--; } return; diff --git a/src/app/xapian/index.worker.ts b/src/app/xapian/index.worker.ts index 797a40763..b8861ae43 100644 --- a/src/app/xapian/index.worker.ts +++ b/src/app/xapian/index.worker.ts @@ -83,8 +83,7 @@ class SearchIndexService { // postMessage ? // messagelistservice stuff! currentFolder = 'Inbox'; - spamFolderName = 'Spam'; - trashFolderName = 'Trash'; + unindexedFolders = ['Trash', 'Spam', 'Templates']; folderList: FolderListEntry[]; messageTextCache = new Map(); @@ -560,16 +559,14 @@ not matching with rest api counts for current folder`); const docid = this.api.getDocIdFromUniqueIdTerm(uniqueIdTerm); if ( docid === 0 && // document not found in the index - msginfo.folder !== this.spamFolderName && - msginfo.folder !== this.trashFolderName + !this.unindexedFolders.includes(msginfo.folder) ) { searchIndexDocumentUpdates.push( new SearchIndexDocumentUpdate(msginfo.id, async () => { try { - this.indexingTools.addMessageToIndex(msginfo, [ - this.spamFolderName, - this.trashFolderName - ]); + this.indexingTools.addMessageToIndex( + msginfo, this.unindexedFolders + ); // Add term about missing body text so that later stage can add this this.api.addTermToDocument(`Q${msginfo.id}`, XAPIAN_TERM_MISSING_BODY_TEXT); if (msginfo.deletedFlag) { @@ -606,7 +603,7 @@ not matching with rest api counts for current folder`); term.substr(XAPIAN_TERM_FOLDER.length) !== msginfo.folder) { // Folder changed const destinationFolder = folders.find(folder => folder.folderPath === msginfo.folder); - if (destinationFolder && (destinationFolder.folderType === 'spam' || destinationFolder.folderType === 'trash')) { + if (destinationFolder && (destinationFolder.folderType === 'spam' || destinationFolder.folderType === 'trash') || destinationFolder.folderType === 'templates') { addSearchIndexDocumentUpdate(() => this.api.deleteDocumentByUniqueTerm(uniqueIdTerm)); msgIsTrashed = true; } else { From b21b5d7f5b947c6897fe2d49a10229f17a1a7fb5 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Mon, 18 Nov 2024 15:49:54 +0000 Subject: [PATCH 039/131] fix(maillist): Ensure we only switch to index data after its loaded Fixes: #1564 --- src/app/app.component.ts | 7 ++-- src/app/rmmapi/messagelist.service.ts | 7 ++-- src/app/xapian/index.worker.ts | 10 ++++-- src/app/xapian/searchservice.ts | 47 ++++++++++++++------------- 4 files changed, 41 insertions(+), 30 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 7e757931c..7e8ff0e31 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -107,7 +107,7 @@ export class AppComponent implements OnInit, AfterViewInit, CanvasTableSelectLis entireHistoryInProgress = false; displayedFolders = new Observable(); - selectedFolder = 'Inbox'; + selectedFolder = ''; composeSelected: boolean; draftsSelected: boolean; overviewSelected: boolean; @@ -410,7 +410,7 @@ export class AppComponent implements OnInit, AfterViewInit, CanvasTableSelectLis fragment => { if (!fragment) { // This also runs when we load '/compose' .. but doesnt need to - this.messagelistservice.setCurrentFolder('Inbox'); + this.switchToFolder('Inbox'); if (this.singlemailviewer) { this.singlemailviewer.close(); } @@ -420,8 +420,8 @@ export class AppComponent implements OnInit, AfterViewInit, CanvasTableSelectLis if (fragment !== this.fragment) { this.fragment = fragment; + this.selectMessageFromFragment(this.fragment); if (this.canvastable.rows && this.canvastable.rows.rowCount() > 0) { - this.selectMessageFromFragment(this.fragment); this.canvastable.jumpToOpenMessage(); } else { this.jumpToFragment = true; @@ -996,6 +996,7 @@ export class AppComponent implements OnInit, AfterViewInit, CanvasTableSelectLis this.showingWebSocketSearchResults = false; this.usewebsocketsearch = false; + this.showingSearchResults = true; // don't scroll to top when redrawing after index updates if (!this.hasChildRouterOutlet) { diff --git a/src/app/rmmapi/messagelist.service.ts b/src/app/rmmapi/messagelist.service.ts index 2588d6c49..964df9792 100644 --- a/src/app/rmmapi/messagelist.service.ts +++ b/src/app/rmmapi/messagelist.service.ts @@ -447,8 +447,11 @@ export class MessageListService { folders.forEach((f) => this.staleFolders[f] = true); // check if current visible folder has updates // refresh if localsearch not activated (aka setCurrentFolder) - if (this.staleFolders[this.currentFolder]) { + this.searchservice.pipe(take(1)).subscribe(searchservice => { + if (!searchservice.localSearchActivated && + this.staleFolders[this.currentFolder]) { this.fetchFolderMessages(); - } + } + }); } } diff --git a/src/app/xapian/index.worker.ts b/src/app/xapian/index.worker.ts index b8861ae43..ea07620c9 100644 --- a/src/app/xapian/index.worker.ts +++ b/src/app/xapian/index.worker.ts @@ -204,8 +204,8 @@ class SearchIndexService { FS.syncfs(true, async () => { // console.log('Worker: Loading partitions'); this.openStoredPartitions(); - await this.updateIndexWithNewChanges(); ctx.postMessage({'action': PostMessageAction.indexUpdated}); + await this.updateIndexWithNewChanges(); }); @@ -1022,7 +1022,8 @@ ctx.addEventListener('message', ({ data }) => { if (searchIndexDocumentUpdates.length > 0 && searchIndexService.localSearchActivated) { searchIndexService.postMessagesToXapianWorker(searchIndexDocumentUpdates); } - } else if (data['action'] === PostMessageAction.addTermToDocument && searchIndexService.localSearchActivated) { + } else if (data['action'] === PostMessageAction.addTermToDocument) { + if (searchIndexService.localSearchActivated) { searchIndexService.postMessagesToXapianWorker([ new SearchIndexDocumentUpdate(data['messageId'], async () => { try { @@ -1032,7 +1033,9 @@ ctx.addEventListener('message', ({ data }) => { console.error(e); } }) ]); - } else if (data['action'] === PostMessageAction.removeTermFromDocument && searchIndexService.localSearchActivated) { + } + } else if (data['action'] === PostMessageAction.removeTermFromDocument) { + if (searchIndexService.localSearchActivated) { searchIndexService.postMessagesToXapianWorker([ new SearchIndexDocumentUpdate(data['messageId'], async () => { try { @@ -1044,6 +1047,7 @@ ctx.addEventListener('message', ({ data }) => { console.error(e); } }) ]); + } } else if (data['action'] === PostMessageAction.setCurrentFolder) { searchIndexService.currentFolder = data['folder']; } diff --git a/src/app/xapian/searchservice.ts b/src/app/xapian/searchservice.ts index 3d817acd5..f7ab8f84c 100644 --- a/src/app/xapian/searchservice.ts +++ b/src/app/xapian/searchservice.ts @@ -231,8 +231,13 @@ export class SearchService { ).subscribe((hasLocalIndex: boolean) => { if (!this.isExpired) { if (hasLocalIndex) { - this.openDBOnWorker(); this.init(); + this.initSubject.subscribe((opened) => { + if (opened) { + this.indexReloadedSubject.next(undefined); + this.openDBOnWorker(); + } + }); } else { // We have no local index - but still need the polling loop here this.indexLastUpdateTime = new Date().getTime(); // Set the last update time to now since we don't have a local index @@ -262,8 +267,8 @@ export class SearchService { this.indexReloadedSubject.subscribe(() => { // console.log('searchservice updating folderCounts'); // we're sending both "indexUpdated", and "updateMessageListService" - this.messagelistservice.refreshFolderCounts(); - // this.messagelistservice.refreshFolderList(); + // this.messagelistservice.refreshFolderCounts(); + this.messagelistservice.refreshFolderList(); }); this.messagelistservice.folderListSubject @@ -293,40 +298,38 @@ export class SearchService { // but it doesn't. this.rmmapi.messageFlagChangeSubject .subscribe((msgFlagChange) => { - if (msgFlagChange.flaggedFlag !== null) { - if (msgFlagChange.flaggedFlag === true) { - this.indexWorker.postMessage( + if (msgFlagChange.flaggedFlag !== null) { + if (msgFlagChange.flaggedFlag === true) { + this.indexWorker.postMessage( {'action': PostMessageAction.addTermToDocument, 'messageId': msgFlagChange.id, 'term': 'XFflagged' - }); - } else { - this.indexWorker.postMessage( + }); + } else { + this.indexWorker.postMessage( {'action': PostMessageAction.removeTermFromDocument, 'messageId': msgFlagChange.id, 'term': 'XFflagged' }); - } - } else if (msgFlagChange.seenFlag !== null) { - if (msgFlagChange.seenFlag === true) { - this.indexWorker.postMessage( + } + } else if (msgFlagChange.seenFlag !== null) { + if (msgFlagChange.seenFlag === true) { + this.indexWorker.postMessage( {'action': PostMessageAction.addTermToDocument, 'messageId': msgFlagChange.id, 'term': 'XFseen' - }); - } else { - this.indexWorker.postMessage( + }); + } else { + this.indexWorker.postMessage( {'action': PostMessageAction.removeTermFromDocument, 'messageId': msgFlagChange.id, 'term': 'XFseen' }); - } - // counts and list to ensure we have uptodate data - this.messagelistservice.refreshFolderList(); - } else { - console.error('Empty flag change message', msgFlagChange); } - // console.log('Flag Change: local index'); + } else { + console.error('Empty flag change message', msgFlagChange); + } + // console.log('Flag Change: local index'); }); // open for reading (for canvastable comms) From 5a86c7f2f9c4bc6ba1b7719c2db4e4f2d2fece09 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Tue, 26 Nov 2024 10:41:13 +0000 Subject: [PATCH 040/131] fix(messagelist): Show correct folder contents after Drafts view --- src/app/app.component.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 7e8ff0e31..86f611451 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -370,8 +370,11 @@ export class AppComponent implements OnInit, AfterViewInit, CanvasTableSelectLis this.messagelistservice.messagesInViewSubject.subscribe(res => { this.messagelist = res; - if (!this.showingSearchResults && !this.showingWebSocketSearchResults - && res) { + if ( + ( + (!this.showingSearchResults && !this.showingWebSocketSearchResults) + || this.messagelistservice.unindexedFolders.includes(this.selectedFolder) + ) && res) { this.setMessageDisplay('messagelist', this.messagelist); if (this.jumpToFragment && res.length > 0) { this.selectMessageFromFragment(this.fragment); From 7713596941af81599d5518cd63e7e55b1488cb76 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Tue, 26 Nov 2024 12:17:44 +0000 Subject: [PATCH 041/131] fix(messagelist): Use user selected folder after leaving non-folder --- src/app/app.component.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 86f611451..fc49b8618 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1011,10 +1011,8 @@ export class AppComponent implements OnInit, AfterViewInit, CanvasTableSelectLis public childRouteActivated(yes: boolean): void { this.hasChildRouterOutlet = yes; if (yes) { + // Don't highlight a folder if we're not viewing one this.selectedFolder = null; - } else { - // reset the default Folder - this.selectedFolder = 'Inbox'; } } From 26077e5b7da8bbf59288465e5d899cec12453cb7 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Thu, 28 Nov 2024 13:13:22 +0000 Subject: [PATCH 042/131] build(xapian): Fixes bug with delayed markSeen on message-open Reverting to runbox-searchindex 0.2.3 as that seems to work better --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 32dadb48e..9d3c1fc55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@danielmoncada/angular-datetime-picker": "^15.0.2", "@danielmoncada/angular-datetime-picker-moment-adapter": "^2.2.0", "@mdi/angular-material": "^7.2.96", - "@runboxcom/runbox-searchindex": "^0.2.4", + "@runboxcom/runbox-searchindex": "^0.2.3", "@sentry/browser": "^5.15.5", "angular-calendar": "0.31.0", "angular2-hotkeys": "13.3.0", @@ -5715,9 +5715,9 @@ } }, "node_modules/@runboxcom/runbox-searchindex": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@runboxcom/runbox-searchindex/-/runbox-searchindex-0.2.4.tgz", - "integrity": "sha512-PaYpAUIjmcb56Y1bXvpfeIp9nWfWhJW3mzRpPmvv/ZVHi7uuLsTBlHPbjyVjr4kSIvqqXouufUmOVLpxSwcfMA==" + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@runboxcom/runbox-searchindex/-/runbox-searchindex-0.2.3.tgz", + "integrity": "sha512-pEJ1EOmhiSXc0fnYHr+wi3qpal/ieSuulAtb45tBgWPoNxncKBLXFGHHd+9DsYuNPBSqHaKyFizaWLDfTq71Mw==" }, "node_modules/@scarf/scarf": { "version": "1.1.1", diff --git a/package.json b/package.json index b0d20cb7c..c6c6a4fe4 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "@danielmoncada/angular-datetime-picker": "^15.0.2", "@danielmoncada/angular-datetime-picker-moment-adapter": "^2.2.0", "@mdi/angular-material": "^7.2.96", - "@runboxcom/runbox-searchindex": "^0.2.4", + "@runboxcom/runbox-searchindex": "^0.2.3", "@sentry/browser": "^5.15.5", "angular-calendar": "0.31.0", "angular2-hotkeys": "13.3.0", From aa8bec8522cc76ea2fe8659e26cbe082f573e8e2 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Mon, 16 Dec 2024 11:27:50 +0000 Subject: [PATCH 043/131] fix(index): Restart index updates after syncing index --- src/app/xapian/searchservice.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/xapian/searchservice.ts b/src/app/xapian/searchservice.ts index f7ab8f84c..7d66d731a 100644 --- a/src/app/xapian/searchservice.ts +++ b/src/app/xapian/searchservice.ts @@ -568,6 +568,7 @@ export class SearchService { this.localSearchActivated = true; this.messagelistservice.refreshFolderList(); + this.indexWorker.postMessage({'action': PostMessageAction.updateIndexWithNewChanges }); this.updateIndexLastUpdateTime(); this.downloadProgress = null; From 792b064e683907d7d8b6ca8fd406daeb847d3c84 Mon Sep 17 00:00:00 2001 From: Bas Date: Wed, 18 Dec 2024 14:46:13 +0000 Subject: [PATCH 044/131] fix(src): Print errors to make development easier --- src/app/compose/recipients.service.ts | 14 ++++---- .../contact-details.component.ts | 5 ++- .../contacts-app/contacts-app.component.ts | 9 +++--- src/app/folder/folderlist.component.ts | 13 ++++---- src/app/rmmapi/messagecache.ts | 16 ++++++---- src/app/rmmapi/rblocale.ts | 9 +++--- src/app/xapian/index.worker.ts | 32 ++++++++++++------- src/app/xapian/searchservice.ts | 24 +++++++++----- 8 files changed, 76 insertions(+), 46 deletions(-) diff --git a/src/app/compose/recipients.service.ts b/src/app/compose/recipients.service.ts index 825d3c300..436e34aad 100644 --- a/src/app/compose/recipients.service.ts +++ b/src/app/compose/recipients.service.ts @@ -119,9 +119,10 @@ export class RecipientsService { groups.map(g => this.recipientFromGroup(g)) ).then((updateGroups) => { this.updateRecipients(updateGroups); - }).catch( - () => this.recipients.next([]) - ); + }).catch((error) => { + console.error(error) + return this.recipients.next([]) + }); }); this.recipients.subscribe(recipients => console.debug(recipients.length, 'recipients ready to use')); @@ -196,9 +197,10 @@ export class RecipientsService { return null; } } - ).catch( - () => null - ) + ).catch((error) => { + console.error(error) + return null + }) ); } else if (m.email) { if (m.name) { diff --git a/src/app/contacts-app/contact-details/contact-details.component.ts b/src/app/contacts-app/contact-details/contact-details.component.ts index 76b691d1c..da67a3c0e 100644 --- a/src/app/contacts-app/contact-details/contact-details.component.ts +++ b/src/app/contacts-app/contact-details/contact-details.component.ts @@ -294,7 +294,10 @@ export class ContactDetailsComponent { console.log('Saving contact:', this.contact); this.contactsservice.saveContact(this.contact).then( id => this.router.navigateByUrl('/contacts/' + id) - ).catch(err => this.snackBar.open(err.message, 'Ok')); + ).catch(err => { + console.error(err); + return this.snackBar.open(err.message, 'Ok'); + }); } rollback(): void { diff --git a/src/app/contacts-app/contacts-app.component.ts b/src/app/contacts-app/contacts-app.component.ts index f66bc3883..19943e9b9 100644 --- a/src/app/contacts-app/contacts-app.component.ts +++ b/src/app/contacts-app/contacts-app.component.ts @@ -1,18 +1,18 @@ // --------- 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 ---------- @@ -259,6 +259,7 @@ export class ContactsAppComponent { try { contacts = Contact.fromVcf(vcf); } catch (e) { + console.error(e) if (warning) { // we predicted this: this.showError('Only .vcf contacts files are supported, this does not look like one'); diff --git a/src/app/folder/folderlist.component.ts b/src/app/folder/folderlist.component.ts index 21cf69a9a..af85246f0 100644 --- a/src/app/folder/folderlist.component.ts +++ b/src/app/folder/folderlist.component.ts @@ -1,18 +1,18 @@ // --------- 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 ---------- @@ -118,6 +118,7 @@ export class FolderListComponent implements OnChanges { } } catch (e) { /* we don't care why it failed, it just means that we'll show all folders as collapsed */ + console.error(e) } this.treeControl = new FlatTreeControl(this._getLevel, this._isExpandable); @@ -197,8 +198,8 @@ export class FolderListComponent implements OnChanges { /** * Folderlist entry is 48 pixels, so if mouse is in the upper region suggest dropping above, * if in the middle suggest inside, or suggest below if in the lower region - * - * @param offsetY + * + * @param offsetY * @returns drop position above, below or inside (see enum DropPosition) */ isDropAboveOrBelowOrInside(offsetY: number): DropPosition { diff --git a/src/app/rmmapi/messagecache.ts b/src/app/rmmapi/messagecache.ts index abddd995c..c752bdb29 100644 --- a/src/app/rmmapi/messagecache.ts +++ b/src/app/rmmapi/messagecache.ts @@ -1,18 +1,18 @@ // --------- BEGIN RUNBOX LICENSE --------- // Copyright (C) 2016-2021 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 ---------- @@ -48,13 +48,17 @@ export class MessageCache { set(id: number, contents: MessageContents): void { contents.version = this.message_version; this.db?.table('messages').put(contents, id).catch( - _err => {}, + err => { + console.error(err); + }, ); } delete(id: number): void { this.db?.table('messages').delete(id).catch( - _err => {}, + err => { + console.error(err); + }, ); } } diff --git a/src/app/rmmapi/rblocale.ts b/src/app/rmmapi/rblocale.ts index 8d12c6d5b..cdbcdb0c5 100644 --- a/src/app/rmmapi/rblocale.ts +++ b/src/app/rmmapi/rblocale.ts @@ -1,18 +1,18 @@ // --------- 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 ---------- @@ -45,6 +45,7 @@ export class RunboxLocale { try { translated = window['getLocale'](key.split('.')); } catch (ex) { + console.error(ex) console.log('locale translations not found'); } return translated; diff --git a/src/app/xapian/index.worker.ts b/src/app/xapian/index.worker.ts index ea07620c9..c039c4b48 100644 --- a/src/app/xapian/index.worker.ts +++ b/src/app/xapian/index.worker.ts @@ -1,18 +1,18 @@ // --------- BEGIN RUNBOX LICENSE --------- // Copyright (C) 2016-2022 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 ---------- @@ -144,6 +144,7 @@ class SearchIndexService { db.close(); }; } catch (e) { + console.error(e) console.log('Worker: Unable to open local xapian index', (e ? e.message : '')); db.close(); // console.log('Worker: Req failed'); @@ -232,7 +233,9 @@ class SearchIndexService { this.api.addFolderXapianIndex(`${this.partitionsdir}/${f}`); } }); - } catch (e) {} + } catch (e) { + console.error(e) + } } private openStoredMainPartition() { @@ -249,7 +252,7 @@ class SearchIndexService { } this.localSearchActivated = false; - + return new Observable((observer) => { console.log('Worker: Closing xapian database'); if (this.api) { @@ -266,7 +269,9 @@ class SearchIndexService { FS.rmdir(XAPIAN_GLASS_WR); try { FS.unlink('xapianglass'); - } catch (e) {} + } catch (e) { + console.error(e) + } @@ -276,6 +281,7 @@ class SearchIndexService { try { FS.stat(this.partitionsdir); } catch (e) { + console.error(e); hasPartitionsDir = false; } if (hasPartitionsDir) { @@ -525,7 +531,10 @@ not matching with rest api counts for current folder`); updateFolderCounts(folderPath).then( (result) => console.log(result.result.result.msg) ).catch( - (err) => console.log('Error updating folder counts: ' + err.errors.join(',')) + (err) => { + console.error(err) + console.log('Error updating folder counts: ' + err.errors.join(',')) + } ); } }); @@ -544,6 +553,7 @@ not matching with rest api counts for current folder`); try { this.api.deleteDocumentByUniqueTerm(uniqueIdTerm); } catch (e) { + console.error(e) console.error('Worker: Unable to delete message from index', msgid); } }) @@ -928,7 +938,7 @@ not matching with rest api counts for current folder`); this.api.deleteDocumentByUniqueTerm('Q' + mid); console.log('Deleted msg id search index', mid); } catch (e) { - console.error('Unable to delete message from search index (not found?)', mid); + console.error('Unable to delete message from search index (not found?)', mid, e); } }) ) @@ -942,7 +952,7 @@ not matching with rest api counts for current folder`); this.api.changeDocumentsFolder('Q' + mid, dotSeparatedDestinationfolderPath); } catch (e) { console.error('Unable to change index document folder', mid, dotSeparatedDestinationfolderPath, - '(not found since moving from trash/spam folder?'); + '(not found since moving from trash/spam folder?', e); } }) ) @@ -1053,7 +1063,7 @@ ctx.addEventListener('message', ({ data }) => { } console.log('Dealt with message'); } catch (e) { - console.error('Failed to deal with message: ' + e); + console.error('Failed to deal with message: ', e); } }); diff --git a/src/app/xapian/searchservice.ts b/src/app/xapian/searchservice.ts index 7d66d731a..e55d48cb8 100644 --- a/src/app/xapian/searchservice.ts +++ b/src/app/xapian/searchservice.ts @@ -1,18 +1,18 @@ // --------- BEGIN RUNBOX LICENSE --------- // Copyright (C) 2016-2022 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 ---------- @@ -171,6 +171,7 @@ export class SearchService { tag: 'newmessages' }); } catch (e) { + console.error(e); console.log('Should have displayed notification about new messages:', newMessagesTitle); } } @@ -222,6 +223,7 @@ export class SearchService { db.close(); }; } catch (e) { + console.error(e) console.log('Unable to open local xapian index', (e ? e.message : '')); db.close(); observer.next(false); @@ -295,7 +297,7 @@ export class SearchService { // Update local index with message seen flag // FIXME: Is this seeing a different copy to the one the worker opened? // seems stuff ought to work without this, after the indexUpdate loop - // but it doesn't. + // but it doesn't. this.rmmapi.messageFlagChangeSubject .subscribe((msgFlagChange) => { if (msgFlagChange.flaggedFlag !== null) { @@ -364,6 +366,7 @@ export class SearchService { this.indexLastUpdateTime = new Date().getTime(); } } catch (e) { + console.error(e) if (!this.updateIndexLastUpdateTime()) { // Corrupt xapian index - delete it and subscribe to changes (fallback to websocket search) // Deal with this on the non-worker side, then tell it to @@ -383,6 +386,7 @@ export class SearchService { } catch (e) { + console.error(e) console.log('No xapian db'); this.initSubject.next(false); } @@ -406,7 +410,9 @@ export class SearchService { this.api.addFolderXapianIndex(`${this.partitionsdir}/${f}`); } }); - } catch (e) {} + } catch (e) { + console.error(e) + } } public openDBOnWorker() { @@ -554,6 +560,7 @@ export class SearchService { try { FS.stat(XAPIAN_GLASS_WR); } catch (e) { + console.error(e) FS.mkdir(XAPIAN_GLASS_WR); } @@ -780,6 +787,7 @@ export class SearchService { try { FS.stat(`${this.partitionsdir}/${dirname}`); } catch (e) { + console.error(e) FS.mkdir(`${this.partitionsdir}/${dirname}`); } @@ -829,13 +837,13 @@ export class SearchService { // // Abusing the fact that getDocData is called multiple times for the same message, // this cache ensures that at least the future calls will get the textcontent synchronously - // eslint-disable-next-line @typescript-eslint/member-ordering, + // eslint-disable-next-line @typescript-eslint/member-ordering, messageTextCache = new Map(); /** * Look up document data from the database. If the id is the same as from the previous request, it will use * the previous lookup result, otherwise look up from the database with the new id - * + * * @param docid the id of the document in the Xapian database (NOT the same as the RMM message id) */ public getDocData (docid: number): SearchIndexDocumentData { From 1b1b0e6876a258c13067f6c4b09ff9447ee2de1b Mon Sep 17 00:00:00 2001 From: Geir Thomas Andersen Date: Sat, 21 Dec 2024 18:12:34 +0100 Subject: [PATCH 045/131] style(welcome): Correct typo. --- src/app/welcome/welcomedesk.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/welcome/welcomedesk.component.html b/src/app/welcome/welcomedesk.component.html index 6ceeb8e8a..80b5c37e0 100644 --- a/src/app/welcome/welcomedesk.component.html +++ b/src/app/welcome/welcomedesk.component.html @@ -3,7 +3,7 @@

Congratulations -- your new Runbox account is ready!

Take advantage of our current 20% discount by proceeding to Plans & Upgrades below.

-

Please validate your alternative email addressm, Visit the Account Details page to resend the email.

+

Please validate your alternative email address, Visit the Account Details page to resend the email.

You can also continue setting up your account, or go straight to your Inbox.


From bafddb1f58056437e1d8a8a1d0ca8aa0d430e8c6 Mon Sep 17 00:00:00 2001 From: Bas Date: Tue, 7 Jan 2025 16:32:59 +0000 Subject: [PATCH 046/131] fix(gravatar): Prevent subsequent requests on gravatar not found --- src/app/contacts-app/contacts.service.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/contacts-app/contacts.service.ts b/src/app/contacts-app/contacts.service.ts index edb0987cc..dc192796b 100644 --- a/src/app/contacts-app/contacts.service.ts +++ b/src/app/contacts-app/contacts.service.ts @@ -96,6 +96,10 @@ class AvatarCache { return null; } + has(email: string): boolean { + return this.entries.hasOwnProperty(email); + } + trash(email: string = null) { if (email) { this.entries[email] = null; @@ -330,8 +334,8 @@ export class ContactsService implements OnDestroy { return Promise.resolve(null); } - if (this.avatarCache.get(email)) { - return Promise.resolve(this.avatarCache.get(email)); + if (this.avatarCache.has(email)) { + return this.avatarCache.get(email); } const contact = await this.lookupContact(email); From 93e84d17ed66a895c92ee48a3067051afbfb5ba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tadeusz=20So=C5=9Bnierz?= Date: Tue, 16 Mar 2021 15:38:12 +0100 Subject: [PATCH 047/131] feat(app): Use a new API to pre-cache messages in bulk --- e2e/mockserver/mockserver.ts | 84 +++++++++++++++++++++++------------- src/app/app.component.ts | 19 ++++---- src/app/rmmapi/rbwebmail.ts | 50 ++++++++++++++++++++- 3 files changed, 112 insertions(+), 41 deletions(-) diff --git a/e2e/mockserver/mockserver.ts b/e2e/mockserver/mockserver.ts index 5df643199..78776f96a 100644 --- a/e2e/mockserver/mockserver.ts +++ b/e2e/mockserver/mockserver.ts @@ -246,39 +246,26 @@ END:VCALENDAR } } + + const bulkemailendpiont = requesturl.match(/\/rest\/v1\/email\/download\/([0-9,]+)/); + if (bulkemailendpiont) { + const ids = bulkemailendpiont[1].split(',').map(id => parseInt(id, 10)); + + const messages = {}; + for (const id of ids) { + messages[id] = { json: this.getMessage(id).result }; + } + + response.end(JSON.stringify({ + 'status': 'success', + 'result': messages, + })); + } + const emailendpoint = requesturl.match(/\/rest\/v1\/email\/([0-9]+)/); if (emailendpoint) { - const mailid = emailendpoint[1]; - let message_obj = mail_message_obj; - if (mailid === '11') { - message_obj = JSON.parse(JSON.stringify(mail_message_obj)); - const to = message_obj.result.headers['to']; - delete message_obj.result.headers['to']; - message_obj.result.headers['cc'] = to; - message_obj.result.headers['subject'] = "No 'To', just 'CC'"; - } - if (mailid === '12') { - message_obj = JSON.parse(JSON.stringify(mail_message_obj)); - message_obj.result.headers['to'].value[0].address = "TESTMAIL@TESTMAIL.COM"; - message_obj.result.headers['to'].text = "TESTMAIL@TESTMAIL.COM"; - message_obj.result.headers['subject'] = "Default from fix test"; - } - if (mailid === '13') { - message_obj = JSON.parse(JSON.stringify(mail_message_obj)); - const to = message_obj.result.headers['to']; - delete message_obj.result.headers['to']; - message_obj.result.headers['cc'] = to; - message_obj.result.headers['subject'] = ""; - } - // This one warns, we couldnt find it! - if (mailid === '14') { - message_obj = { - 'status':'warning', - 'errors': [ - 'Email content missing' - ] - }; - } + const mailid = parseInt(emailendpoint[1], 10); + const message_obj = this.getMessage(mailid); if (requesturl.endsWith('/html')) { response.end(message_obj.result.text.html); @@ -1004,4 +991,39 @@ END:VCALENDAR ], ]; } + + getMessage(mailid: number): any { + let message_obj = mail_message_obj; + if (mailid === 11) { + message_obj = JSON.parse(JSON.stringify(mail_message_obj)); + const to = message_obj.result.headers['to']; + delete message_obj.result.headers['to']; + message_obj.result.headers['cc'] = to; + message_obj.result.headers['subject'] = "No 'To', just 'CC'"; + } + if (mailid === 12) { + message_obj = JSON.parse(JSON.stringify(mail_message_obj)); + message_obj.result.headers['to'].value[0].address = "TESTMAIL@TESTMAIL.COM"; + message_obj.result.headers['to'].text = "TESTMAIL@TESTMAIL.COM"; + message_obj.result.headers['subject'] = "Default from fix test"; + } + if (mailid === 13) { + message_obj = JSON.parse(JSON.stringify(mail_message_obj)); + const to = message_obj.result.headers['to']; + delete message_obj.result.headers['to']; + message_obj.result.headers['cc'] = to; + message_obj.result.headers['subject'] = ""; + } + // This one warns, we couldnt find it! + if (mailid === '14') { + message_obj = { + 'status':'warning', + 'errors': [ + 'Email content missing' + ] + }; + } + + return message_obj; + } } diff --git a/src/app/app.component.ts b/src/app/app.component.ts index fc49b8618..5107df1ef 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -460,16 +460,17 @@ export class AppComponent implements OnInit, AfterViewInit, CanvasTableSelectLis filter(() => !this.canvastable.isScrollInProgress()), throttleTime(1000) ).subscribe(() => { - const rowIndexes = this.canvastable.getVisibleRowIndexes(); - const messageIds = rowIndexes.filter( - idx => idx < this.canvastable.rows.rowCount() - ).map(idx => this.canvastable.rows.getRowMessageId(idx) - ).filter(id => id > 0); - for (const id of messageIds) { - if (this.searchService.updateMessageText(id)) { - this.canvastable.hasChanges = true; + const rowIndexes = this.canvastable.getVisibleRowIndexes(); + const messageIds = rowIndexes.filter( + idx => idx < this.canvastable.rows.rowCount() + ).map(idx => this.canvastable.rows.getRowMessageId(idx)); + this.rmmapi.downloadMessages(messageIds).then( + (messages) => { + for (const msg messages) { + this.searchService.updateMessageText(parseInt(msg['id'], 10)); } - } + this.canvastable.hasChanges = true; + }); }); if ('serviceWorker' in navigator) { diff --git a/src/app/rmmapi/rbwebmail.ts b/src/app/rmmapi/rbwebmail.ts index a7b31449c..f836112fe 100644 --- a/src/app/rmmapi/rbwebmail.ts +++ b/src/app/rmmapi/rbwebmail.ts @@ -32,7 +32,7 @@ import { DraftFormModel } from '../compose/draftdesk.service'; import { MatLegacySnackBar as MatSnackBar } from '@angular/material/legacy-snack-bar'; import { filter, map, mergeMap } from 'rxjs/operators'; -import { HttpClient, HttpResponse } from '@angular/common/http'; +import { HttpClient, HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { RunboxLocale } from '../rmmapi/rblocale'; import { RMM } from '../rmm'; import { Identity, FromPriority } from '../profiles/profile.service'; @@ -265,6 +265,54 @@ export class RunboxWebmailAPI { })); } + public downloadMessages(messageIds: number[]): Promise { + const missingMessages = []; + for (const msgid of messageIds) { + if (!this.messageContentsCache[msgid]) { + this.messageContentsCache[msgid] = new AsyncSubject(); + missingMessages.push(msgid); + } + } + + const messagePromises = messageIds.map(id => this.messageContentsCache[id].toPromise()); + + if (missingMessages.length > 0) { + this.http.get(`/rest/v1/email/download/${missingMessages.join(',')}`).pipe( + catchError((err: HttpErrorResponse) => throwError(err.message)), + concatMap((res: any) => { + if (res.status === 'success') { + return of(res.result); + } else { + return throwError(res.errors[0]); + } + }), + ).subscribe( + (result: any) => { + for (const resultKey of Object.keys(result)) { + const msgid = parseInt(resultKey, 10); + const contents = result[msgid]?.json; + if (contents) { + this.messageContentsCache[msgid].next(contents); + this.messageContentsCache[msgid].complete(); + } else { + this.messageContentsCache[msgid].error(result[msgid]?.error); + delete this.messageContentsCache[msgid]; + } + } + }, + (err: Error) => { + for (const msgid of missingMessages) { + this.messageContentsCache[msgid].error(err.toString()); + delete this.messageContentsCache[msgid]; + } + } + ); + } + + // return Promise.allSettled(messagePromises); + return Promise.all(messagePromises); + } + public updateLastOn(): Observable { return this.http.put('/rest/v1/last_on', {}); } From 8352a803490f87da1eed8b559d0628dedee3059b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tadeusz=20So=C5=9Bnierz?= Date: Wed, 17 Mar 2021 15:22:19 +0100 Subject: [PATCH 048/131] fix(mailviewer): Improve error handling in singlemailviewer Introduces a "contact support" component, since it's a UI pattern we repeat a lot. --- e2e/mockserver/mockserver.ts | 2 + .../bitpay-payment-dialog.component.html | 7 +- src/app/mailviewer/mailviewer.module.ts | 2 + .../singlemailviewer.component.html | 7 +- .../mailviewer/singlemailviewer.component.ts | 222 ++++++++++-------- src/app/rmmapi/rbwebmail.spec.ts | 9 +- 6 files changed, 138 insertions(+), 111 deletions(-) diff --git a/e2e/mockserver/mockserver.ts b/e2e/mockserver/mockserver.ts index 78776f96a..3af3a9e48 100644 --- a/e2e/mockserver/mockserver.ts +++ b/e2e/mockserver/mockserver.ts @@ -1024,6 +1024,8 @@ END:VCALENDAR }; } + message_obj.status = 'success'; + return message_obj; } } diff --git a/src/app/account-app/bitpay-payment-dialog.component.html b/src/app/account-app/bitpay-payment-dialog.component.html index 24a39630c..108bf6dee 100644 --- a/src/app/account-app/bitpay-payment-dialog.component.html +++ b/src/app/account-app/bitpay-payment-dialog.component.html @@ -25,8 +25,7 @@

Cryptocurrency payment - There was an error creating your payment.
- Try again later, or contact Runbox Support at support@runbox.com. - + + There was an error creating your payment. +

diff --git a/src/app/mailviewer/mailviewer.module.ts b/src/app/mailviewer/mailviewer.module.ts index 05cc39442..352868d84 100644 --- a/src/app/mailviewer/mailviewer.module.ts +++ b/src/app/mailviewer/mailviewer.module.ts @@ -39,11 +39,13 @@ import { ShowHTMLDialogComponent } from '../dialog/htmlconfirm.dialog'; import { ResizerModule } from '../directives/resizer.module'; import { AvatarBarComponent } from './avatar-bar.component'; import { ContactCardComponent } from './contactcard.component'; +import { RunboxCommonModule } from '../common/common.module'; export { SingleMailViewerComponent } from './singlemailviewer.component'; @NgModule({ imports: [ CommonModule, + RunboxCommonModule, FormsModule, MatCheckboxModule, MatButtonModule, diff --git a/src/app/mailviewer/singlemailviewer.component.html b/src/app/mailviewer/singlemailviewer.component.html index 3a9d1422e..00792691a 100644 --- a/src/app/mailviewer/singlemailviewer.component.html +++ b/src/app/mailviewer/singlemailviewer.component.html @@ -417,7 +417,10 @@
-
- {{err}} +
+
+ + There was an error loading your message: {{ err }}. +
diff --git a/src/app/mailviewer/singlemailviewer.component.ts b/src/app/mailviewer/singlemailviewer.component.ts index b311edcea..11a31404a 100644 --- a/src/app/mailviewer/singlemailviewer.component.ts +++ b/src/app/mailviewer/singlemailviewer.component.ts @@ -17,7 +17,7 @@ // along with Runbox 7. If not, see . // ---------- END RUNBOX LICENSE ---------- -import { map } from 'rxjs/operators'; +import { catchError, map } from 'rxjs/operators'; import { Component, Input, OnInit, Output, EventEmitter, ViewChild, ViewChildren, @@ -39,7 +39,7 @@ import { ProgressDialog } from '../dialog/progress.dialog'; import { MobileQueryService } from '../mobile-query.service'; import { HorizResizerDirective } from '../directives/horizresizer.directive'; -import { RunboxWebmailAPI } from '../rmmapi/rbwebmail'; +import { MessageContents, RunboxWebmailAPI } from '../rmmapi/rbwebmail'; import { of } from 'rxjs'; import { Router } from '@angular/router'; import { MessageListService } from '../rmmapi/messagelist.service'; @@ -62,6 +62,8 @@ const resizerPercentageKey = 'rmm7resizerpercentage'; const TOOLBAR_BUTTON_WIDTH = 30; +type Mail = any; + @Component({ moduleId: 'angular2/app/mailviewer/', // eslint-disable-next-line @angular-eslint/component-selector @@ -96,7 +98,7 @@ export class SingleMailViewerComponent implements OnInit, DoCheck, AfterViewInit public downloadProgress: number; - public mailObj: any; + public mailObj: Mail; public err: any; public mailContentHTML: string = null; @@ -392,105 +394,13 @@ export class SingleMailViewerComponent implements OnInit, DoCheck, AfterViewInit // ProgressDialog.open(this.dialog); this.rbwebmailapi.getMessageContents(this.messageId).pipe( - map((messageContents) => { - const res: any = Object.assign({}, messageContents); - if (res.status === 'warning') { - // status === 'error' already displayed in showBackendErrors? - // Skip if we previously had an issue loading this messge - throw res; - } - res.subject = res.headers.subject; - res.from = res.headers.from.value.map(f => new MailAddressInfo(f.name,f.address)); - res.to = res.headers.to ? res.headers.to.value : ''; - res.cc = res.headers.cc ? res.headers.cc.value : ''; - - // RFC 5322 says "Date" and "From" are the only 2 required fields - // and yet we get emails without em. - if (!res.headers.date) { - res.headers.date = '1970-01-01T00:00:00.000Z'; - } - res.date = ( - (arr: string[]): Date => - new Date( - parseInt(arr[1], 10), - parseInt(arr[2], 10) - 1, - parseInt(arr[3], 10), - parseInt(arr[4], 10), - parseInt(arr[5], 10), - parseInt(arr[6], 10), - parseInt(arr[7], 10) - ) - ) - ( - new RegExp('([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])T' + - '([0-9][0-9]):([0-9][0-9]):([0-9][0-9])\.([0-9][0-9][0-9])') - .exec(res.headers.date) - ); - - res.date.setMinutes(res.date.getMinutes() - res.date.getTimezoneOffset()); - - res.sanitized_html = this.expandAttachmentData(res.attachments, res.sanitized_html); - res.sanitized_html_without_images = this.expandAttachmentData(res.attachments, res.sanitized_html_without_images); - res.visible_attachment_count = res.attachments.filter((att) => !att.internal).length; - - - // Remove style tag otherwise angular sanitazion will display style tag content as text - - if (res.text.html) { - // Pre-sanitized, however we need to escape ampersands and - // quotes for srcdoc, let angular do it: - res.html = this.domSanitizer.bypassSecurityTrustHtml(DOMPurify.sanitize(res.sanitized_html)); - res.html_without_images = this.domSanitizer.bypassSecurityTrustHtml(DOMPurify.sanitize(res.sanitized_html_without_images)); - } else { - res.html = null; - } - - /** - * Transform the links so that they are clickable - */ - let text = res.text.text; - res.rawtext = text; - text = String(text).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); - - const LINKY_URL_REGEXP = - /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i, - MAILTO_REGEXP = /^mailto:/i; - - let match; - let raw = text; - const html = []; - let url; - let i; - - while ((match = raw.match(LINKY_URL_REGEXP))) { - // We can not end in these as they are sometimes found at the end of the sentence - url = match[0]; - // if we did not match ftp/http/www/mailto then assume mailto - if (!match[2] && !match[4]) { - url = (match[3] ? 'http://' : 'mailto:') + url; - } - i = match.index; - html.push(raw.substr(0, i)); - ((u, t) => { - html.push('
'); - html.push(t); - html.push(''); - })(url, match[0].replace(MAILTO_REGEXP, '')); - - raw = raw.substring(i + match[0].length); - } - html.push(raw); - text = html.join(''); - - // res.text = text; - res.text = this.domSanitizer.bypassSecurityTrustHtml(DOMPurify.sanitize(res.text.textAsHtml)); // res.text.textAsHtml; - return res; - })) - .subscribe((res) => { + catchError((err: Error) => { + console.warn(`Error loading message ${this.messageId}: ${err.toString()}, retrying`); + return this.rbwebmailapi.getMessageContents(this.messageId, true); + }), + map((res: MessageContents) => this.processMessageContents(res)) + ).subscribe( + (res: Mail) => { if (res.html) { // default to no images this.mailContentHTML = res.html_without_images; @@ -513,6 +423,13 @@ export class SingleMailViewerComponent implements OnInit, DoCheck, AfterViewInit if (this.mailObj.seen_flag === 0) { this.messageActionsHandler.markSeen(); } + setTimeout(() => { + // If forwarding HTML copy mail header from the visible mail viewer header + if (this.messageHeaderHTML) { + this.mailObj.origMailHeaderHTML = '
Account username:
' + this.messageHeaderHTML.nativeElement.innerHTML + '
'; + this.mailObj.origMailHeaderText = this.messageHeaderHTML.nativeElement.innerText; + } + }, 0); }, err => { console.error('Error fetching message: ' + this.messageId); @@ -528,8 +445,107 @@ export class SingleMailViewerComponent implements OnInit, DoCheck, AfterViewInit // close the viewer pane this.close(); // else - not outputting normal JS errors! - } + }, + ); + } + + private processMessageContents(messageContents: MessageContents): Mail { + console.log('processMessageContents processing', messageContents); + const res: any = Object.assign({}, messageContents); + if (res.status === 'warning') { + // status === 'error' already displayed in showBackendErrors? + // Skip if we previously had an issue loading this messge + throw res; + } + res.subject = res.headers.subject; + res.from = res.headers.from.value.map(f => new MailAddressInfo(f.name,f.address)); + res.to = res.headers.to ? res.headers.to.value : ''; + res.cc = res.headers.cc ? res.headers.cc.value : ''; + + // RFC 5322 says "Date" and "From" are the only 2 required fields + // and yet we get emails without em. + if (!res.headers.date) { + res.headers.date = '1970-01-01T00:00:00.000Z'; + } + res.date = ( + (arr: string[]): Date => + new Date( + parseInt(arr[1], 10), + parseInt(arr[2], 10) - 1, + parseInt(arr[3], 10), + parseInt(arr[4], 10), + parseInt(arr[5], 10), + parseInt(arr[6], 10), + parseInt(arr[7], 10) + ) + ) + ( + new RegExp('([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])T' + + '([0-9][0-9]):([0-9][0-9]):([0-9][0-9])\.([0-9][0-9][0-9])') + .exec(res.headers.date) ); + + res.date.setMinutes(res.date.getMinutes() - res.date.getTimezoneOffset()); + + res.sanitized_html = this.expandAttachmentData(res.attachments, res.sanitized_html); + res.sanitized_html_without_images = this.expandAttachmentData(res.attachments, res.sanitized_html_without_images); + res.visible_attachment_count = res.attachments.filter((att) => !att.inte + + // Remove style tag otherwise angular sanitazion will display style tag content as text + + if (res.text.html) { + // Pre-sanitized, however we need to escape ampersands and + // quotes for srcdoc, let angular do it: + res.html = this.domSanitizer.bypassSecurityTrustHtml(DOMPurify.sanitize(res.sanitized_html)); + res.html_without_images = this.domSanitizer.bypassSecurityTrustHtml(DOMPurify.sanitize(res.sanitized_html_without_images)); + } else { + res.html = null; + } + + /** + * Transform the links so that they are clickable + */ + let text = res.text.text; + res.rawtext = text; + text = String(text).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); + + const LINKY_URL_REGEXP = + /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i, + MAILTO_REGEXP = /^mailto:/i; + + let match; + let raw = text; + const html = []; + let url; + let i; + + while ((match = raw.match(LINKY_URL_REGEXP))) { + // We can not end in these as they are sometimes found at the end of the sentence + url = match[0]; + // if we did not match ftp/http/www/mailto then assume mailto + if (!match[2] && !match[4]) { + url = (match[3] ? 'http://' : 'mailto:') + url; + } + i = match.index; + html.push(raw.substr(0, i)); + ((u, t) => { + html.push(''); + html.push(t); + html.push(''); + })(url, match[0].replace(MAILTO_REGEXP, '')); + + raw = raw.substring(i + match[0].length); + } + html.push(raw); + text = html.join(''); + + // res.text = text; + res.text = this.domSanitizer.bypassSecurityTrustHtml(DOMPurify.sanitize(res.text.textAsHtml)); // res.text.textAsHtml; + return res; } expandAttachmentData(attachments: any[], html: string): string { diff --git a/src/app/rmmapi/rbwebmail.spec.ts b/src/app/rmmapi/rbwebmail.spec.ts index 25677d86d..4b79f0069 100644 --- a/src/app/rmmapi/rbwebmail.spec.ts +++ b/src/app/rmmapi/rbwebmail.spec.ts @@ -51,10 +51,12 @@ describe('RBWebMail', () => { // so we set it directly here rmmapi.setRunboxMe({'uid': '11', 'last_name': 'testuser'}); const httpTestingController = TestBed.inject(HttpTestingController); + // HACK: crappy solution to get the email request to resolve // see https://github.com/angular/angular/issues/25965 await new Promise(resolve => setTimeout(resolve, 500)); - httpTestingController.expectOne('/rest/v1/email/123').flush({ + let req = httpTestingController.expectOne('/rest/v1/email/123'); + req.flush({ status: 'success', result: { id: 123, @@ -77,6 +79,8 @@ describe('RBWebMail', () => { messageContentsObservable = rmmapi.getMessageContents(123, true); await new Promise(resolve => setTimeout(resolve, 0)); httpTestingController.expectOne('/rest/v1/email/123').flush({ + req = httpTestingController.expectOne('/rest/v1/email/123'); + req.flush({ status: 'success', result: { id: 123, @@ -93,7 +97,8 @@ describe('RBWebMail', () => { messageContentsObservable = rmmapi.getMessageContents(123); await new Promise(resolve => setTimeout(resolve, 0)); - httpTestingController.expectOne('/rest/v1/email/123').flush({ + req = httpTestingController.expectOne('/rest/v1/email/123'); + req.flush({ status: 'success', result: { id: 123, From d170bd7e12da3564192b48c216da8e97044a7cd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tadeusz=20So=C5=9Bnierz?= Date: Wed, 17 Mar 2021 15:23:00 +0100 Subject: [PATCH 049/131] fix(mailviewer): Fix a (harmless) bug that caused exception spam --- src/app/mailviewer/singlemailviewer.component.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/app/mailviewer/singlemailviewer.component.ts b/src/app/mailviewer/singlemailviewer.component.ts index 11a31404a..4c179a673 100644 --- a/src/app/mailviewer/singlemailviewer.component.ts +++ b/src/app/mailviewer/singlemailviewer.component.ts @@ -238,7 +238,18 @@ export class SingleMailViewerComponent implements OnInit, DoCheck, AfterViewInit } } } - }); + }, 0); + }); + + // messageHeaderHTML loads after message is loaded + this.messageHeaderHTMLQuery.changes.subscribe((forwardHeader: QueryList) => { + setTimeout(() => { + const nativeElement = forwardHeader.first?.nativeElement; + if (nativeElement) { + this.mailObj.origMailHeaderHTML = '' + nativeElement.innerHTML + '
'; + this.mailObj.origMailHeaderText = nativeElement.innerText; + } + }, 0); }); this.afterViewInit.emit(this.messageId); From d9ebc0a94df2116f8303163b21a83c9cd683171e Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Wed, 28 Apr 2021 12:06:54 +0000 Subject: [PATCH 050/131] refactor(app): Update cache to handle bulk message downloading --- e2e/cypress/integration/message-caching.ts | 6 +- src/app/rmmapi/messagecache.ts | 28 ++++++- src/app/rmmapi/rbwebmail.ts | 96 ++++++++++++---------- 3 files changed, 82 insertions(+), 48 deletions(-) diff --git a/e2e/cypress/integration/message-caching.ts b/e2e/cypress/integration/message-caching.ts index 996295e6b..6bb0c3536 100644 --- a/e2e/cypress/integration/message-caching.ts +++ b/e2e/cypress/integration/message-caching.ts @@ -9,8 +9,8 @@ describe('Message caching', () => { }); - it('should cache all messages on first time page load', () => { - cy.intercept('/rest/v1/email/12').as('message12requested'); + it('should cache all messages on first time page load', () => { + cy.intercept('/rest/v1/email/download/*').as('message12requested'); cy.visit('/'); cy.wait('@message12requested', {'timeout':10000}); @@ -27,7 +27,7 @@ describe('Message caching', () => { // Now don't fetch it again: cy.visit('/#Inbox:12'); let called = false; - cy.intercept('/rest/v1/email/12', (_req) => { + cy.intercept('/rest/v1/email/download/*', (_req) => { called = true; }); diff --git a/src/app/rmmapi/messagecache.ts b/src/app/rmmapi/messagecache.ts index c752bdb29..35105eb37 100644 --- a/src/app/rmmapi/messagecache.ts +++ b/src/app/rmmapi/messagecache.ts @@ -30,8 +30,10 @@ export class MessageCache { try { this.db = new Dexie(`messageCache-${userId}`); - this.db.version(2).stores({ - messages: '', // use out-of-line keys + this.db.version(3).stores({ + // use out-of-line keys, but index "id" + // yes, empty first arg is deliberate + messages: ',&id', }); } catch (err) { console.log(`Error initializing messagecache: ${err}`); @@ -45,6 +47,28 @@ export class MessageCache { ); } + // verify which ids we already have + async checkIds(ids: number[]): Promise { + return this.db?.table('messages') + .where('id') + .anyOf(ids) + .primaryKeys() + .then( + (keys) => keys.map((key) => key as number) + ); + } + + async getMany(ids: number[]): Promise { + return this.db?.table('messages') + .where('id') + .anyOf(ids) + .toArray() + .then( + result => result, + _error => null, + ); + } + set(id: number, contents: MessageContents): void { contents.version = this.message_version; this.db?.table('messages').put(contents, id).catch( diff --git a/src/app/rmmapi/rbwebmail.ts b/src/app/rmmapi/rbwebmail.ts index f836112fe..39883f57d 100644 --- a/src/app/rmmapi/rbwebmail.ts +++ b/src/app/rmmapi/rbwebmail.ts @@ -19,7 +19,7 @@ import { Injectable, NgZone } from '@angular/core'; import { Observable, from, Subject, AsyncSubject, firstValueFrom } from 'rxjs'; -import { share } from 'rxjs/operators'; +import { catchError, concatMap, share, map, mergeMap, tap } from 'rxjs/operators'; import { MessageInfo } from '../common/messageinfo'; import { MailAddressInfo } from '../common/mailaddressinfo'; import { FolderListEntry } from '../common/folderlistentry'; @@ -174,6 +174,10 @@ export class RunboxWebmailAPI { private messageCache: AsyncSubject = new AsyncSubject(); private messageContentsRequestCache = new LRUMessageCache>(); + // Track which messageids we're already fetching + // Else we attempt to refetch the same ones a fair bit on start-up + private downloadingMessages: number[] = []; + constructor( private http: HttpClient, private ngZone: NgZone, @@ -266,51 +270,57 @@ export class RunboxWebmailAPI { } public downloadMessages(messageIds: number[]): Promise { - const missingMessages = []; - for (const msgid of messageIds) { - if (!this.messageContentsCache[msgid]) { - this.messageContentsCache[msgid] = new AsyncSubject(); - missingMessages.push(msgid); - } - } - - const messagePromises = messageIds.map(id => this.messageContentsCache[id].toPromise()); - - if (missingMessages.length > 0) { - this.http.get(`/rest/v1/email/download/${missingMessages.join(',')}`).pipe( - catchError((err: HttpErrorResponse) => throwError(err.message)), - concatMap((res: any) => { - if (res.status === 'success') { - return of(res.result); - } else { - return throwError(res.errors[0]); - } - }), - ).subscribe( - (result: any) => { - for (const resultKey of Object.keys(result)) { - const msgid = parseInt(resultKey, 10); - const contents = result[msgid]?.json; - if (contents) { - this.messageContentsCache[msgid].next(contents); - this.messageContentsCache[msgid].complete(); + const cached = this.messageCache.checkIds([...messageIds]); + return cached.then((inCache) => { + const missingMessages = messageIds.filter( + (msgId) => !inCache.includes(msgId) + && !this.downloadingMessages.includes(msgId) + ); + const messagePromises = missingMessages.map(id => this.messageCache.get(id)); + + if (missingMessages.length > 0) { + this.downloadingMessages = this.downloadingMessages.concat(missingMessages); + this.http.get(`/rest/v1/email/download/${missingMessages.join(',')}`).pipe( + catchError((err: HttpErrorResponse) => throwError(err.message)), + concatMap((res: any) => { + if (res.status === 'success') { + return of(res.result); } else { - this.messageContentsCache[msgid].error(result[msgid]?.error); - delete this.messageContentsCache[msgid]; + return throwError(res.errors[0]); + } + }), + ).subscribe( + (result: any) => { + for (const resultKey of Object.keys(result)) { + const msgid = parseInt(resultKey, 10); + const contents = result[msgid]?.json; + if (contents) { + this.messageCache.set(msgid, contents); + } else { + this.deleteCachedMessageContents(msgid); + } + const msgIndex = this.downloadingMessages + .findIndex((id) => msgid === id); + if (msgIndex > -1) { + this.downloadingMessages.splice(msgIndex, 1); + } + } + }, + (err: Error) => { + for (const msgid of missingMessages) { + this.deleteCachedMessageContents(msgid); + const msgIndex = this.downloadingMessages + .findIndex((id) => msgid === id); + if (msgIndex > -1) { + this.downloadingMessages.splice(msgIndex, 1); + } } } - }, - (err: Error) => { - for (const msgid of missingMessages) { - this.messageContentsCache[msgid].error(err.toString()); - delete this.messageContentsCache[msgid]; - } - } - ); - } - - // return Promise.allSettled(messagePromises); - return Promise.all(messagePromises); + ); + } + // return Promise.allSettled(messagePromises); + return Promise.all(messagePromises); + }); } public updateLastOn(): Observable { From add531ddd92f558e938ab467abe8f55d3c2b4048 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Thu, 6 May 2021 12:40:26 +0000 Subject: [PATCH 051/131] perf(app): Load missing msg bodies in bulk, instead of one at a time --- src/app/rmmapi/rbwebmail.spec.ts | 7 +++ src/app/rmmapi/rbwebmail.ts | 83 +++++++++++++++------------- src/app/xapian/searchservice.spec.ts | 14 +++-- src/app/xapian/searchservice.ts | 2 +- 4 files changed, 63 insertions(+), 43 deletions(-) diff --git a/src/app/rmmapi/rbwebmail.spec.ts b/src/app/rmmapi/rbwebmail.spec.ts index 4b79f0069..2b6eb714e 100644 --- a/src/app/rmmapi/rbwebmail.spec.ts +++ b/src/app/rmmapi/rbwebmail.spec.ts @@ -35,6 +35,13 @@ describe('RBWebMail', () => { ], providers: [ RunboxWebmailAPI, + { provide: MessageCache, useValue: { + get: (_) => Promise.resolve(null), + set: (_, __) => {}, + delete: (_) => {}, + checkIds: (_) => Promise.resolve([]), + getMany: (_) => Promise.resolve([]), + } }, ] }); }); diff --git a/src/app/rmmapi/rbwebmail.ts b/src/app/rmmapi/rbwebmail.ts index 39883f57d..e62729032 100644 --- a/src/app/rmmapi/rbwebmail.ts +++ b/src/app/rmmapi/rbwebmail.ts @@ -272,53 +272,62 @@ export class RunboxWebmailAPI { public downloadMessages(messageIds: number[]): Promise { const cached = this.messageCache.checkIds([...messageIds]); return cached.then((inCache) => { + // Filter out items we already have + // or are busy fetching const missingMessages = messageIds.filter( (msgId) => !inCache.includes(msgId) && !this.downloadingMessages.includes(msgId) ); - const messagePromises = missingMessages.map(id => this.messageCache.get(id)); + const messagePromises = []; if (missingMessages.length > 0) { this.downloadingMessages = this.downloadingMessages.concat(missingMessages); - this.http.get(`/rest/v1/email/download/${missingMessages.join(',')}`).pipe( - catchError((err: HttpErrorResponse) => throwError(err.message)), - concatMap((res: any) => { - if (res.status === 'success') { - return of(res.result); - } else { - return throwError(res.errors[0]); - } - }), - ).subscribe( - (result: any) => { - for (const resultKey of Object.keys(result)) { - const msgid = parseInt(resultKey, 10); - const contents = result[msgid]?.json; - if (contents) { - this.messageCache.set(msgid, contents); - } else { - this.deleteCachedMessageContents(msgid); - } - const msgIndex = this.downloadingMessages - .findIndex((id) => msgid === id); - if (msgIndex > -1) { - this.downloadingMessages.splice(msgIndex, 1); - } - } - }, - (err: Error) => { - for (const msgid of missingMessages) { - this.deleteCachedMessageContents(msgid); - const msgIndex = this.downloadingMessages - .findIndex((id) => msgid === id); - if (msgIndex > -1) { - this.downloadingMessages.splice(msgIndex, 1); + messagePromises.push( + new Promise((resolve, reject) => { + this.http.get(`/rest/v1/email/download/${missingMessages.join(',')}`).pipe( + catchError((err: HttpErrorResponse) => throwError(err.message)), + concatMap((res: any) => { + if (res.status === 'success') { + return of(res.result); + } else { + return throwError(res.errors[0]); + } + }), + ).subscribe( + (result: any) => { + for (const resultKey of Object.keys(result)) { + const msgid = parseInt(resultKey, 10); + console.log('RMMAPI Got some result:', result); + const contents = result[msgid]?.json; + if (contents) { + console.log(`RMMAPI setting cache for msgid = ${msgid}`); + this.messageCache.set(msgid, contents); + } else { + this.deleteCachedMessageContents(msgid); + } + const msgIndex = this.downloadingMessages + .findIndex((id) => msgid === id); + if (msgIndex > -1) { + this.downloadingMessages.splice(msgIndex, 1); + } + } + resolve(); + }, + (err: Error) => { + for (const msgid of missingMessages) { + this.deleteCachedMessageContents(msgid); + const msgIndex = this.downloadingMessages + .findIndex((id) => msgid === id); + if (msgIndex > -1) { + this.downloadingMessages.splice(msgIndex, 1); + } + } + reject(err); } - } - } + ); + }) ); } - // return Promise.allSettled(messagePromises); return Promise.all(messagePromises); }); } diff --git a/src/app/xapian/searchservice.spec.ts b/src/app/xapian/searchservice.spec.ts index 72d089976..073ac2f8b 100644 --- a/src/app/xapian/searchservice.spec.ts +++ b/src/app/xapian/searchservice.spec.ts @@ -106,6 +106,7 @@ describe('SearchService', () => { ], providers: [ SearchService, + MessageCache, MessageListService, RunboxWebmailAPI // { provide: Worker, useValue: { @@ -245,6 +246,7 @@ describe('SearchService', () => { expect(await searchService.initSubject.toPromise()).toBeTruthy(); + console.log('search service initialised'); expect(searchService.localSearchActivated).toBeTruthy(); expect(localdir).toEqual(searchService.localdir); @@ -286,12 +288,16 @@ describe('SearchService', () => { await new Promise(resolve => setTimeout(resolve, 1000)); - req = httpMock.expectOne('/rest/v1/email/' + testMessageId); + req = httpMock.expectOne('/rest/v1/email/download/' + testMessageId); req.flush({ status: 'success', result: { - text: { - text: 'message body test text SecretSauceFormula' + [testMessageId]: { + json: { + text: { + text: 'message body test text SecretSauceFormula' + } + } } } }); @@ -314,7 +320,5 @@ describe('SearchService', () => { FS.chdir('/'); FS.unmount('/' + localdir); - - console.log(searchService.api.getXapianDocCount(), 'docs in xapian db'); }); }); diff --git a/src/app/xapian/searchservice.ts b/src/app/xapian/searchservice.ts index e55d48cb8..0ad909e87 100644 --- a/src/app/xapian/searchservice.ts +++ b/src/app/xapian/searchservice.ts @@ -678,7 +678,7 @@ export class SearchService { ).map((pair: number[]) => pair[0]); } - checkIfDownloadableIndexExists(): Observable { + checkIfDownloadableIndexExists(): Observable { return this.httpclient.get('/mail/download_xapian_index?exists=check').pipe( map((stat: any) => { this.serverIndexSize = stat.size; From 6de8e29891e223acb4e8098c352d2ec401dadcf9 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Thu, 19 Dec 2024 17:41:25 +0000 Subject: [PATCH 052/131] perf(messagelist): Upgrade bulk messages to multi-thread with index worker --- e2e/cypress/integration/mailviewer.ts | 20 +-- e2e/cypress/integration/message-caching.ts | 4 +- e2e/mockserver/mockserver.ts | 9 +- src/app/app.component.ts | 14 +- .../mailviewer/singlemailviewer.component.ts | 3 +- src/app/rmmapi/lru-message-cache.ts | 12 ++ src/app/rmmapi/messagecache.ts | 6 +- src/app/rmmapi/rbwebmail.spec.ts | 2 +- src/app/rmmapi/rbwebmail.ts | 120 +++++++++--------- src/app/xapian/index.worker.ts | 3 +- src/app/xapian/searchservice.spec.ts | 1 + src/app/xapian/searchservice.ts | 2 - 12 files changed, 111 insertions(+), 85 deletions(-) diff --git a/e2e/cypress/integration/mailviewer.ts b/e2e/cypress/integration/mailviewer.ts index f8e1a8f05..7abe6d3fc 100644 --- a/e2e/cypress/integration/mailviewer.ts +++ b/e2e/cypress/integration/mailviewer.ts @@ -26,7 +26,7 @@ describe('Interacting with mailviewer', () => { // }); it('can open an email and go back and forth in browser history', () => { - cy.intercept('/rest/v1/email/11').as('get11'); + cy.intercept('/rest/v1/email/download/*').as('get11'); cy.visit('/#Inbox:11'); cy.wait('@get11', {'timeout':10000}); @@ -45,7 +45,7 @@ describe('Interacting with mailviewer', () => { }); it('can reply to an email with no "To"', () => { - cy.intercept('/rest/v1/email/11').as('get11'); + cy.intercept('/rest/v1/email/download/*').as('get11'); cy.visit('/#Inbox:11') cy.wait('@get11', {'timeout':10000}); // cy.get('#messageContents'); @@ -59,7 +59,7 @@ describe('Interacting with mailviewer', () => { }); it('can forward an email with no "To"', () => { - cy.intercept('/rest/v1/email/11').as('get11'); + cy.intercept('/rest/v1/email/download/*').as('get11'); cy.visit('/'); cy.wait('@get11', {'timeout':10000}); cy.visit('/#Inbox:11'); @@ -74,7 +74,7 @@ describe('Interacting with mailviewer', () => { }); it('can reply to an email with no "To" or "Subject"', () => { - cy.intercept('/rest/v1/email/13').as('get13'); + cy.intercept('/rest/v1/email/download/*').as('get13'); cy.visit('/'); cy.wait('@get13', {'timeout':10000}); cy.visit('/#Inbox:13'); @@ -89,7 +89,7 @@ describe('Interacting with mailviewer', () => { }); it('can forward an email with no "To" or "Subject"', () => { - cy.intercept('/rest/v1/email/13').as('get13'); + cy.intercept('/rest/v1/email/download/*').as('get13'); cy.visit('/'); cy.wait('@get13', {'timeout':10000}); cy.visit('/#Inbox:13'); @@ -104,7 +104,7 @@ describe('Interacting with mailviewer', () => { }); it('Vertical to horizontal mode exposes full height button', () => { - cy.intercept('/rest/v1/email/11').as('get11'); + cy.intercept('/rest/v1/email/download/*').as('get11'); cy.visit('/'); cy.wait('@get11', {'timeout':10000}); cy.visit('/#Inbox:11'); @@ -116,7 +116,7 @@ describe('Interacting with mailviewer', () => { }); it('Changing viewpane height is stored', () => { - cy.intercept('/rest/v1/email/11').as('get11'); + cy.intercept('/rest/v1/email/download/*').as('get11'); cy.visit('/'); cy.wait('@get11', {'timeout':10000}); cy.visit('/#Inbox:11'); @@ -133,7 +133,7 @@ describe('Interacting with mailviewer', () => { }); it('Half height reduces stored pane height', () => { - cy.intercept('/rest/v1/email/11').as('get11'); + cy.intercept('/rest/v1/email/download/*').as('get11'); cy.visit('/'); cy.wait('@get11', {'timeout':10000}); cy.visit('/#Inbox:11'); @@ -161,7 +161,7 @@ describe('Interacting with mailviewer', () => { }); it('Revisit open email in horizontal mode loads it', () => { - cy.intercept('/rest/v1/email/11').as('get11'); + cy.intercept('/rest/v1/email/download/*').as('get11'); cy.visit('/'); cy.wait('@get11', {'timeout':10000}); cy.visit('/#Inbox:11'); @@ -177,7 +177,7 @@ describe('Interacting with mailviewer', () => { }); it('Can go out of mailviewer and back and still see our email', () => { - cy.intercept('/rest/v1/email/12').as('get12'); + cy.intercept('/rest/v1/email/download/*').as('get12'); cy.visit('/'); cy.wait('@get12',{'timeout':10000}); cy.visit('/#Inbox:12'); diff --git a/e2e/cypress/integration/message-caching.ts b/e2e/cypress/integration/message-caching.ts index 6bb0c3536..658217809 100644 --- a/e2e/cypress/integration/message-caching.ts +++ b/e2e/cypress/integration/message-caching.ts @@ -9,7 +9,7 @@ describe('Message caching', () => { }); - it('should cache all messages on first time page load', () => { + it('should fetch all messages on first time page load', () => { cy.intercept('/rest/v1/email/download/*').as('message12requested'); cy.visit('/'); @@ -18,7 +18,7 @@ describe('Message caching', () => { }); it('should not re-request messages after a page reload', () => { - cy.intercept('/rest/v1/email/12').as('message12requested'); + cy.intercept('/rest/v1/email/download/*').as('message12requested'); cy.visit('/'); cy.wait('@message12requested', {'timeout':10000}); diff --git a/e2e/mockserver/mockserver.ts b/e2e/mockserver/mockserver.ts index 3af3a9e48..32862235b 100644 --- a/e2e/mockserver/mockserver.ts +++ b/e2e/mockserver/mockserver.ts @@ -260,6 +260,7 @@ END:VCALENDAR 'status': 'success', 'result': messages, })); + return; } const emailendpoint = requesturl.match(/\/rest\/v1\/email\/([0-9]+)/); @@ -994,18 +995,21 @@ END:VCALENDAR getMessage(mailid: number): any { let message_obj = mail_message_obj; + message_obj.status = 'success'; if (mailid === 11) { message_obj = JSON.parse(JSON.stringify(mail_message_obj)); const to = message_obj.result.headers['to']; delete message_obj.result.headers['to']; message_obj.result.headers['cc'] = to; message_obj.result.headers['subject'] = "No 'To', just 'CC'"; + message_obj.result.mid = mailid; } if (mailid === 12) { message_obj = JSON.parse(JSON.stringify(mail_message_obj)); message_obj.result.headers['to'].value[0].address = "TESTMAIL@TESTMAIL.COM"; message_obj.result.headers['to'].text = "TESTMAIL@TESTMAIL.COM"; message_obj.result.headers['subject'] = "Default from fix test"; + message_obj.result.mid = mailid; } if (mailid === 13) { message_obj = JSON.parse(JSON.stringify(mail_message_obj)); @@ -1013,9 +1017,10 @@ END:VCALENDAR delete message_obj.result.headers['to']; message_obj.result.headers['cc'] = to; message_obj.result.headers['subject'] = ""; + message_obj.result.mid = mailid; } // This one warns, we couldnt find it! - if (mailid === '14') { + if (mailid === 14) { message_obj = { 'status':'warning', 'errors': [ @@ -1024,8 +1029,6 @@ END:VCALENDAR }; } - message_obj.status = 'success'; - return message_obj; } } diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 5107df1ef..46229b0aa 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -24,6 +24,7 @@ import { } from './canvastable/canvastable'; import { SingleMailViewerComponent } from './mailviewer/singlemailviewer.component'; import { SearchService } from './xapian/searchservice'; +import { PostMessageAction } from './xapian/messageactions'; import { MatLegacyDialogRef as MatDialogRef, MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; import { MatIconRegistry } from '@angular/material/icon'; @@ -464,12 +465,19 @@ export class AppComponent implements OnInit, AfterViewInit, CanvasTableSelectLis const messageIds = rowIndexes.filter( idx => idx < this.canvastable.rows.rowCount() ).map(idx => this.canvastable.rows.getRowMessageId(idx)); + // FIXME: promise errors? this.rmmapi.downloadMessages(messageIds).then( (messages) => { - for (const msg messages) { - this.searchService.updateMessageText(parseInt(msg['id'], 10)); + const updateWorker = new Map(); + for (const msg of messages) { + this.searchService.updateMessageText(msg['mid']); + updateWorker.set(msg['mid'], msg.text.text); + }; + // Send to the messageCache in the worker, so we can add the text to the index: + if(updateWorker.size > 0) { + this.searchService.indexWorker.postMessage({'action': PostMessageAction.messageCache, 'updates': updateWorker }); + this.canvastable.hasChanges = true; } - this.canvastable.hasChanges = true; }); }); diff --git a/src/app/mailviewer/singlemailviewer.component.ts b/src/app/mailviewer/singlemailviewer.component.ts index 4c179a673..b6652df59 100644 --- a/src/app/mailviewer/singlemailviewer.component.ts +++ b/src/app/mailviewer/singlemailviewer.component.ts @@ -461,7 +461,6 @@ export class SingleMailViewerComponent implements OnInit, DoCheck, AfterViewInit } private processMessageContents(messageContents: MessageContents): Mail { - console.log('processMessageContents processing', messageContents); const res: any = Object.assign({}, messageContents); if (res.status === 'warning') { // status === 'error' already displayed in showBackendErrors? @@ -500,7 +499,7 @@ export class SingleMailViewerComponent implements OnInit, DoCheck, AfterViewInit res.sanitized_html = this.expandAttachmentData(res.attachments, res.sanitized_html); res.sanitized_html_without_images = this.expandAttachmentData(res.attachments, res.sanitized_html_without_images); - res.visible_attachment_count = res.attachments.filter((att) => !att.inte + res.visible_attachment_count = res.attachments.filter((att) => !att.inteinternal).length; // Remove style tag otherwise angular sanitazion will display style tag content as text diff --git a/src/app/rmmapi/lru-message-cache.ts b/src/app/rmmapi/lru-message-cache.ts index 03181df5a..4b720150d 100644 --- a/src/app/rmmapi/lru-message-cache.ts +++ b/src/app/rmmapi/lru-message-cache.ts @@ -42,6 +42,12 @@ export class LRUMessageCache { } } + checkIds(ids: number[]): number[] { + const keys = [...this.messages.keys()]; + const matches = keys.filter((val) => ids.includes(val)); + return matches; + } + get(id: number): T { const row = this.messages.get(id); if (row) { @@ -50,6 +56,12 @@ export class LRUMessageCache { return row?.message; } + getMany(ids: number[]): T[] { + const found = this.checkIds(ids); + const results = found.map((id) => this.get(id)); + return results; + } + delete(id: number): void { this.messages.delete(id); } diff --git a/src/app/rmmapi/messagecache.ts b/src/app/rmmapi/messagecache.ts index 35105eb37..326011905 100644 --- a/src/app/rmmapi/messagecache.ts +++ b/src/app/rmmapi/messagecache.ts @@ -33,7 +33,7 @@ export class MessageCache { this.db.version(3).stores({ // use out-of-line keys, but index "id" // yes, empty first arg is deliberate - messages: ',&id', + messages: ',&mid', }); } catch (err) { console.log(`Error initializing messagecache: ${err}`); @@ -50,7 +50,7 @@ export class MessageCache { // verify which ids we already have async checkIds(ids: number[]): Promise { return this.db?.table('messages') - .where('id') + .where('mid') .anyOf(ids) .primaryKeys() .then( @@ -60,7 +60,7 @@ export class MessageCache { async getMany(ids: number[]): Promise { return this.db?.table('messages') - .where('id') + .where('mid') .anyOf(ids) .toArray() .then( diff --git a/src/app/rmmapi/rbwebmail.spec.ts b/src/app/rmmapi/rbwebmail.spec.ts index 2b6eb714e..e4dec0a89 100644 --- a/src/app/rmmapi/rbwebmail.spec.ts +++ b/src/app/rmmapi/rbwebmail.spec.ts @@ -23,6 +23,7 @@ import { FolderListEntry } from '../common/folderlistentry'; import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; import { MatLegacySnackBarModule as MatSnackBarModule } from '@angular/material/legacy-snack-bar'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { MessageCache } from './messagecache'; import { firstValueFrom } from 'rxjs'; describe('RBWebMail', () => { @@ -85,7 +86,6 @@ describe('RBWebMail', () => { messageContentsObservable = rmmapi.getMessageContents(123, true); await new Promise(resolve => setTimeout(resolve, 0)); - httpTestingController.expectOne('/rest/v1/email/123').flush({ req = httpTestingController.expectOne('/rest/v1/email/123'); req.flush({ status: 'success', diff --git a/src/app/rmmapi/rbwebmail.ts b/src/app/rmmapi/rbwebmail.ts index e62729032..05dfa2293 100644 --- a/src/app/rmmapi/rbwebmail.ts +++ b/src/app/rmmapi/rbwebmail.ts @@ -18,8 +18,8 @@ // ---------- END RUNBOX LICENSE ---------- import { Injectable, NgZone } from '@angular/core'; -import { Observable, from, Subject, AsyncSubject, firstValueFrom } from 'rxjs'; -import { catchError, concatMap, share, map, mergeMap, tap } from 'rxjs/operators'; +import { Observable, from, of, Subject, AsyncSubject, firstValueFrom, throwError } from 'rxjs'; +import { catchError, concatMap, share, filter, map, mergeMap } from 'rxjs/operators'; import { MessageInfo } from '../common/messageinfo'; import { MailAddressInfo } from '../common/mailaddressinfo'; import { FolderListEntry } from '../common/folderlistentry'; @@ -30,8 +30,6 @@ import { RunboxCalendarEvent } from '../calendar-app/runbox-calendar-event'; import { Product } from '../account-app/product'; import { DraftFormModel } from '../compose/draftdesk.service'; import { MatLegacySnackBar as MatSnackBar } from '@angular/material/legacy-snack-bar'; -import { filter, map, mergeMap } from 'rxjs/operators'; - import { HttpClient, HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { RunboxLocale } from '../rmmapi/rblocale'; import { RMM } from '../rmm'; @@ -228,6 +226,12 @@ export class RunboxWebmailAPI { return cached; } + public checkCachedMessageContents(messageIds: number[]): Promise { + const cached = firstValueFrom(this.messageCache) + .then(cache => cache.checkIds(messageIds)); + return cached; + } + public getMessageContents(messageId: number, refresh = false): Observable { const cached = refresh ? Promise.resolve(null) : this.getCachedMessageContents(messageId); return from(cached.then(contents => { @@ -269,69 +273,69 @@ export class RunboxWebmailAPI { })); } - public downloadMessages(messageIds: number[]): Promise { - const cached = this.messageCache.checkIds([...messageIds]); - return cached.then((inCache) => { - // Filter out items we already have - // or are busy fetching - const missingMessages = messageIds.filter( - (msgId) => !inCache.includes(msgId) - && !this.downloadingMessages.includes(msgId) - ); + // returns the ids for which we have updated the cache + public async downloadMessages(messageIds: number[]): Promise { + const cachedIds = await this.checkCachedMessageContents([...messageIds]); - const messagePromises = []; - if (missingMessages.length > 0) { - this.downloadingMessages = this.downloadingMessages.concat(missingMessages); - messagePromises.push( - new Promise((resolve, reject) => { - this.http.get(`/rest/v1/email/download/${missingMessages.join(',')}`).pipe( - catchError((err: HttpErrorResponse) => throwError(err.message)), - concatMap((res: any) => { - if (res.status === 'success') { - return of(res.result); + // Filter out items we already have + // or are busy fetching + const missingMessages = messageIds.filter( + (msgId) => !cachedIds.includes(msgId) + && !this.downloadingMessages.includes(msgId) + ); + + if (missingMessages.length === 0) { + return []; + } + this.downloadingMessages = this.downloadingMessages.concat(missingMessages); + return new Promise((resolve, reject) => { + this.http.get(`/rest/v1/email/download/${missingMessages.join(',')}`).pipe( + catchError((err: HttpErrorResponse) => throwError(err.message)), + concatMap((res: any) => { + if (res.status === 'success') { + return of(res.result); + } else { + return throwError(res.errors[0]); + } + }), + ).subscribe( + async (result: any) => { + const messageCache = await firstValueFrom(this.messageCache); + const updatedMsgs = []; + for (const resultKey of Object.keys(result)) { + const msgid = parseInt(resultKey, 10); + const contents = result[msgid]?.json; + if (contents) { + contents.status = 'success'; + messageCache.set(msgid, Object.assign( new MessageContents(), contents)); + updatedMsgs.push(contents); } else { - return throwError(res.errors[0]); + this.deleteCachedMessageContents(msgid); } - }), - ).subscribe( - (result: any) => { - for (const resultKey of Object.keys(result)) { - const msgid = parseInt(resultKey, 10); - console.log('RMMAPI Got some result:', result); - const contents = result[msgid]?.json; - if (contents) { - console.log(`RMMAPI setting cache for msgid = ${msgid}`); - this.messageCache.set(msgid, contents); - } else { - this.deleteCachedMessageContents(msgid); - } - const msgIndex = this.downloadingMessages - .findIndex((id) => msgid === id); - if (msgIndex > -1) { - this.downloadingMessages.splice(msgIndex, 1); - } + const msgIndex = this.downloadingMessages + .findIndex((id) => msgid === id); + if (msgIndex > -1) { + this.downloadingMessages.splice(msgIndex, 1); } - resolve(); - }, - (err: Error) => { - for (const msgid of missingMessages) { - this.deleteCachedMessageContents(msgid); - const msgIndex = this.downloadingMessages - .findIndex((id) => msgid === id); - if (msgIndex > -1) { - this.downloadingMessages.splice(msgIndex, 1); - } + } + resolve(updatedMsgs); + }, + (err: Error) => { + for (const msgid of missingMessages) { + this.deleteCachedMessageContents(msgid); + const msgIndex = this.downloadingMessages + .findIndex((id) => msgid === id); + if (msgIndex > -1) { + this.downloadingMessages.splice(msgIndex, 1); } - reject(err); } - ); - }) - ); - } - return Promise.all(messagePromises); + reject(err); + } + ); }); } + public updateLastOn(): Observable { return this.http.put('/rest/v1/last_on', {}); } diff --git a/src/app/xapian/index.worker.ts b/src/app/xapian/index.worker.ts index c039c4b48..7d89fe96e 100644 --- a/src/app/xapian/index.worker.ts +++ b/src/app/xapian/index.worker.ts @@ -1011,7 +1011,8 @@ ctx.addEventListener('message', ({ data }) => { searchIndexService.folderList = data['value']; } else if (data['action'] === PostMessageAction.messageCache) { // Add / update the text of a message which we can add to the search index - searchIndexService.messageTextCache.set(data['msgId'], data['value']); + const updates = data['updates']; + updates.forEach((val, key) => searchIndexService.messageTextCache.set(key, val)); } else if (data['action'] === PostMessageAction.moveMessagesToFolder) { searchIndexService.moveMessagesToFolder(data['messageIds'], data['value']); } else if (data['action'] === PostMessageAction.deleteMessages) { diff --git a/src/app/xapian/searchservice.spec.ts b/src/app/xapian/searchservice.spec.ts index 073ac2f8b..164e620f2 100644 --- a/src/app/xapian/searchservice.spec.ts +++ b/src/app/xapian/searchservice.spec.ts @@ -21,6 +21,7 @@ import { SearchService, XAPIAN_GLASS_WR } from './searchservice'; import { Type } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { RunboxWebmailAPI, RunboxMe } from '../rmmapi/rbwebmail'; +import { MessageCache } from '../rmmapi/messagecache'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; import { MatLegacySnackBarModule as MatSnackBarModule } from '@angular/material/legacy-snack-bar'; diff --git a/src/app/xapian/searchservice.ts b/src/app/xapian/searchservice.ts index 0ad909e87..cfc971c5a 100644 --- a/src/app/xapian/searchservice.ts +++ b/src/app/xapian/searchservice.ts @@ -916,8 +916,6 @@ export class SearchService { this.rmmapi.getMessageContents(messageId).subscribe((content) => { if (content['status'] === 'success') { this.messageTextCache.set(messageId, content.text.text); - // Send to the messageCache in the worker, so we can add the text to the index: - this.indexWorker.postMessage({'action': PostMessageAction.messageCache, 'msgId': messageId, 'value': content.text.text }); if (this.messagelistservice.messagesById[messageId]) { this.messagelistservice.messagesById[messageId].plaintext = content.text.text; } From fd03294eaa533916d54069b5106581ca953b7fe4 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Wed, 22 Jan 2025 15:04:41 +0000 Subject: [PATCH 053/131] fix(messagelist): Only show message action buttons when messages are selected Fixes: #1559 --- src/app/app.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/app.component.html b/src/app/app.component.html index 1ad61ca9f..71a765643 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -238,7 +238,7 @@

No Message Selected

-
+
From 8ca81cd796940878d6ffb242cf484cf656d6ce00 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Tue, 21 Jan 2025 17:45:59 +0000 Subject: [PATCH 054/131] fix(drag): Shows correct drag-image while moving images to folders Fixes: #1095 --- .../canvastable/canvastable.component.html | 1 - src/app/canvastable/canvastable.ts | 27 ++++++++++++------- src/app/folder/folderlist.component.ts | 1 + 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/app/canvastable/canvastable.component.html b/src/app/canvastable/canvastable.component.html index 4b5083a13..f6c53aa7e 100644 --- a/src/app/canvastable/canvastable.component.html +++ b/src/app/canvastable/canvastable.component.html @@ -15,4 +15,3 @@ (dragstart)="dragColumnOverlay($event)" >
- \ No newline at end of file diff --git a/src/app/canvastable/canvastable.ts b/src/app/canvastable/canvastable.ts index ddb707d82..4644da7a8 100644 --- a/src/app/canvastable/canvastable.ts +++ b/src/app/canvastable/canvastable.ts @@ -443,7 +443,6 @@ export class CanvasTableComponent implements AfterViewInit, DoCheck, OnInit { } } this.hoverRowIndex = newHoverRowIndex; - this.updateDragImage(newHoverRowIndex); } if (this.dragSelectionDirectionIsDown === null) { @@ -590,7 +589,7 @@ export class CanvasTableComponent implements AfterViewInit, DoCheck, OnInit { ); } - private updateDragImage(selectedRowIndex: number) { + private updateDragImage(selectedRowIndex: number) :HTMLCanvasElement { const dragImageYCoords: number[][] = []; let dragImageDestY = 0; @@ -609,21 +608,29 @@ export class CanvasTableComponent implements AfterViewInit, DoCheck, OnInit { } }); - const dragImageCanvas = document.getElementById('thedragimage') as HTMLCanvasElement; + const dragImageCanvas = document.createElement("canvas"); dragImageCanvas.width = this.canv.width - 20; dragImageCanvas.height = dragImageYCoords.length * this.rowheight; const dragContext = dragImageCanvas.getContext('2d'); - dragImageYCoords.forEach(ycoords => + dragContext.clearRect(0,0,dragImageCanvas.width,dragImageCanvas.height); + dragContext.fillStyle = 'red'; + dragContext.fillRect(0,0,dragImageCanvas.width,dragImageCanvas.height); + dragImageYCoords.forEach(ycoords => { dragContext.drawImage(this.canv, 0, ycoords[0], this.canv.width - 20, this.rowheight, 0, ycoords[1], this.canv.width - 20, this.rowheight - )); + ); + }); + + document.body.append(dragImageCanvas); + dragImageCanvas.setAttribute('id', 'thedragcanvas'); + dragImageCanvas.style.position = "absolute"; dragImageCanvas.style.top = "0px"; dragImageCanvas.style.left = "-"+ dragImageCanvas.width + "px"; - // const dragImage = document.getElementById('thedragimage') as HTMLImageElement; - // dragImage.src = dragImageCanvas.toDataURL(); + + return dragImageCanvas; } public dragColumnOverlay(event: DragEvent) { @@ -632,10 +639,11 @@ export class CanvasTableComponent implements AfterViewInit, DoCheck, OnInit { const selectedRowIndex = this.getRowIndexByClientY(event.clientY); if (!this.columns[selectedColIndex].checkbox) { + this.selectListener.rowSelected(selectedRowIndex, -1); + const dragCanvas = this.updateDragImage(selectedRowIndex); event.dataTransfer.dropEffect = 'move'; - event.dataTransfer.setDragImage(document.getElementById('thedragimage'), 0, 0); + event.dataTransfer.setDragImage(dragCanvas, 0, 0); event.dataTransfer.setData('text/plain', 'rowIndex:' + selectedRowIndex); - this.selectListener.rowSelected(selectedRowIndex, -1); } else { event.preventDefault(); this.lastMouseDownEvent = event; @@ -763,7 +771,6 @@ export class CanvasTableComponent implements AfterViewInit, DoCheck, OnInit { this.getColIndexByClientX(clientX), multiSelect); - this.updateDragImage(selectedRowIndex); this.hasChanges = true; } diff --git a/src/app/folder/folderlist.component.ts b/src/app/folder/folderlist.component.ts index af85246f0..2103a06d2 100644 --- a/src/app/folder/folderlist.component.ts +++ b/src/app/folder/folderlist.component.ts @@ -240,6 +240,7 @@ export class FolderListComponent implements OnChanges { this.dropAboveOrBelowOrInside = DropPosition.NONE; this.dragFolderInProgress = false; this.dropFolderId = 0; + document.getElementById('thedragcanvas').remove(); } dragFolderStart(event, folderId: NumberConstructor): void { From 4b42f5cc2455d178263f5edb4735c68710e75f24 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Tue, 14 Jan 2025 15:52:16 +0000 Subject: [PATCH 055/131] fix(messagelist): Clear message selection after user action Fixes #1552 --- src/app/app.component.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 46229b0aa..e78853dec 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -910,7 +910,7 @@ export class AppComponent implements OnInit, AfterViewInit, CanvasTableSelectLis } public clearSelection() { - if (this.canvastable.rows && this.canvastable.rows.rowCount() > 0) { + if (this.canvastable.rows) { this.canvastable.rows.clearSelection(); } this.canvastable.hasChanges = true; @@ -1102,6 +1102,10 @@ export class AppComponent implements OnInit, AfterViewInit, CanvasTableSelectLis this.searchService.moveMessagesToFolder(msgIds, folderPath); } this.messagelistservice.moveMessages(msgIds, folderPath); + this.clearSelection(); + if(this.singlemailviewer && this.singlemailviewer.messageId && messageIds.includes(this.singlemailviewer.messageId)) { + this.singlemailviewer.close(); + } }, updateRemote: (msgIds: number[]) => { const userFolders = this.messagelistservice.folderListSubject.value; @@ -1137,7 +1141,9 @@ export class AppComponent implements OnInit, AfterViewInit, CanvasTableSelectLis } this.messagelistservice.moveMessages(msgIds, folderPath); this.clearSelection(); - this.canvastable.rows.clearOpenedRow(); + if(this.singlemailviewer && this.singlemailviewer.messageId && messageIds.includes(this.singlemailviewer.messageId)) { + this.singlemailviewer.close(); + } }, updateRemote: (msgIds: number[]) => { const userFolders = this.messagelistservice.folderListSubject.value; From ae9803b0eee382618dd6bd25f4d72693a6ad69b2 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Tue, 14 Jan 2025 10:33:41 +0000 Subject: [PATCH 056/131] fix(welcome): Use icon that exists for person --- src/app/account-app/account-welcome.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/account-app/account-welcome.component.html b/src/app/account-app/account-welcome.component.html index 294cfd9f7..e4e54a289 100644 --- a/src/app/account-app/account-welcome.component.html +++ b/src/app/account-app/account-welcome.component.html @@ -117,7 +117,7 @@

- Personal Details + Personal Details

View and update your name, address, alternative email address, and other details.

From be070baa483b0b9038e3a526e76fe83d72487e7a Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Wed, 29 Jan 2025 11:01:44 +0000 Subject: [PATCH 057/131] fix(messagelist): Correct drag images for hidpi screens/zoomed in Fixes: #33 --- src/app/canvastable/canvastable.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/canvastable/canvastable.ts b/src/app/canvastable/canvastable.ts index 4644da7a8..9462fc639 100644 --- a/src/app/canvastable/canvastable.ts +++ b/src/app/canvastable/canvastable.ts @@ -601,16 +601,16 @@ export class CanvasTableComponent implements AfterViewInit, DoCheck, OnInit { && (this.rows.isSelectedRow(ndx) || ndx === selectedRowIndex) ) { - const dragImageDataY = Math.floor((ndx - this.topindex) * this.rowheight); + const dragImageDataY = Math.floor((ndx - this.topindex) * this.rowheight * devicePixelRatio); dragImageYCoords.push([dragImageDataY, dragImageDestY]); - dragImageDestY += this.rowheight; + dragImageDestY += this.rowheight * devicePixelRatio; } }); const dragImageCanvas = document.createElement("canvas"); dragImageCanvas.width = this.canv.width - 20; - dragImageCanvas.height = dragImageYCoords.length * this.rowheight; + dragImageCanvas.height = dragImageYCoords.length * this.rowheight * devicePixelRatio; const dragContext = dragImageCanvas.getContext('2d'); dragContext.clearRect(0,0,dragImageCanvas.width,dragImageCanvas.height); @@ -618,18 +618,18 @@ export class CanvasTableComponent implements AfterViewInit, DoCheck, OnInit { dragContext.fillRect(0,0,dragImageCanvas.width,dragImageCanvas.height); dragImageYCoords.forEach(ycoords => { dragContext.drawImage(this.canv, - 0, ycoords[0], this.canv.width - 20, this.rowheight, + 0, ycoords[0], this.canv.width - 20, this.rowheight * devicePixelRatio, 0, ycoords[1], - this.canv.width - 20, this.rowheight + this.canv.width - 20, this.rowheight * devicePixelRatio ); }); document.body.append(dragImageCanvas); dragImageCanvas.setAttribute('id', 'thedragcanvas'); dragImageCanvas.style.position = "absolute"; dragImageCanvas.style.top = "0px"; dragImageCanvas.style.left = "-"+ dragImageCanvas.width + "px"; + dragImageCanvas.style.width = Math.floor(((this.canv.width - 20) / devicePixelRatio)) + 'px'; - return dragImageCanvas; } From 4f423091a0ceab52c068b8511df4a10647cd79a4 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Wed, 22 Jan 2025 17:36:53 +0000 Subject: [PATCH 058/131] fix(messagelist): Ensure opened message row is visible in list Fixes: #33 --- src/app/app.component.html | 2 ++ src/app/app.component.ts | 4 ++++ src/app/canvastable/canvastable.ts | 15 ++++++++++----- src/app/mailviewer/singlemailviewer.component.ts | 2 ++ 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/app/app.component.html b/src/app/app.component.html index 71a765643..af34b26b4 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -160,6 +160,7 @@
No Message Selected

[adjustableHeight]="true" [showVerticalSplitButton]="allowMailViewerOrientationChange" [messageActionsHandler]="messageActionsHandler" + (afterLoadMessage)="updateMessageListHeight()" (orientationChangeRequest)="mailViewerOrientationChangeRequest($event)" (onClose)="singleMailViewerClosed($event)">
diff --git a/src/app/app.component.ts b/src/app/app.component.ts index e78853dec..48be26b95 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1064,6 +1064,10 @@ export class AppComponent implements OnInit, AfterViewInit, CanvasTableSelectLis this.updateUrlFragment(); } + updateMessageListHeight() { + this.canvastable.jumpToOpenMessage(); + } + searchTextFieldFocus() { if (!this.usewebsocketsearch && !this.dataReady) { this.usewebsocketsearch = true; diff --git a/src/app/canvastable/canvastable.ts b/src/app/canvastable/canvastable.ts index 9462fc639..0186e032d 100644 --- a/src/app/canvastable/canvastable.ts +++ b/src/app/canvastable/canvastable.ts @@ -133,6 +133,8 @@ export class CanvasTableComponent implements AfterViewInit, DoCheck, OnInit { columnResizeInProgress = false; private scrollbarArea = false; + private jumpToMessage = false; + visibleColumnSeparatorAlpha = 0; visibleColumnSeparatorIndex = 0; lastClientY: number; @@ -863,11 +865,7 @@ export class CanvasTableComponent implements AfterViewInit, DoCheck, OnInit { // When loading a url with a fragment containing a msg id - scroll to there public jumpToOpenMessage() { - // currently selected row in the centre: - if (this.rows.rowCount() > 0 && this.rows.openedRowIndex) { - this.topindex = this.rows.openedRowIndex - Math.round(this.maxVisibleRows / 2); - this.enforceScrollLimit(); - } + this.jumpToMessage = true; } private enforceScrollLimit() { @@ -978,6 +976,13 @@ export class CanvasTableComponent implements AfterViewInit, DoCheck, OnInit { this.canv.height = this.wantedCanvasHeight; this.maxVisibleRows = this.canv.scrollHeight / this.rowheight; + if(this.jumpToMessage) { + // currently selected row in the centre: + if (this.rows.rowCount() > 0 && this.rows.openedRowIndex) { + this.topindex = this.rows.openedRowIndex - Math.round(this.maxVisibleRows / 2); + } + this.jumpToMessage = false; + } this.enforceScrollLimit(); this.hasChanges = true; if (this.canv.clientWidth < this.autoRowWrapModeWidth) { diff --git a/src/app/mailviewer/singlemailviewer.component.ts b/src/app/mailviewer/singlemailviewer.component.ts index b6652df59..a1b8e0859 100644 --- a/src/app/mailviewer/singlemailviewer.component.ts +++ b/src/app/mailviewer/singlemailviewer.component.ts @@ -79,6 +79,7 @@ export class SingleMailViewerComponent implements OnInit, DoCheck, AfterViewInit @Output() onClose: EventEmitter = new EventEmitter(); @Output() afterViewInit: EventEmitter = new EventEmitter(); @Output() orientationChangeRequest: EventEmitter = new EventEmitter(); + @Output() afterLoadMessage: EventEmitter = new EventEmitter(); @Input() messageActionsHandler: MessageActions; @Input() adjustableHeight: boolean; @@ -777,6 +778,7 @@ export class SingleMailViewerComponent implements OnInit, DoCheck, AfterViewInit this.resizer.resizePercentage(50); } } + this.afterLoadMessage.emit(); }, 0); } From 01f97573c3aa4d6fd9ac85065120de857cf4d761 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Wed, 29 Jan 2025 17:22:19 +0000 Subject: [PATCH 059/131] feat(mailviewer): New allowlist sender option for mail viewer Fixes: #1609 --- src/app/mailviewer/rmm7messageactions.ts | 12 ++++++++++++ src/app/mailviewer/singlemailviewer.component.html | 7 +++++-- src/app/rmmapi/rbwebmail.ts | 6 +++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/app/mailviewer/rmm7messageactions.ts b/src/app/mailviewer/rmm7messageactions.ts index 8a444aaf4..3622908d3 100644 --- a/src/app/mailviewer/rmm7messageactions.ts +++ b/src/app/mailviewer/rmm7messageactions.ts @@ -210,6 +210,18 @@ export class RMM7MessageActions implements MessageActions { snackBarRef.dismiss(); } + allowListSender(param) { + const msg = `AllowListing sender: ${param}`; + const snackBarRef = this.snackBar.open(msg); + this.rmmapi.allowListSender(param).subscribe((res) => { + if ( res.status === 'error' ) { + snackBarRef.dismiss(); + this.snackBar.open('There was an error with Sender allowlisting functionality. Please try again.', 'Dismiss'); + } + }); + snackBarRef.dismiss(); + } + // Update mailviewer menu flag icon after flagging? flag() { this.updateMessages({ diff --git a/src/app/mailviewer/singlemailviewer.component.html b/src/app/mailviewer/singlemailviewer.component.html index 00792691a..c8ce1b3ad 100644 --- a/src/app/mailviewer/singlemailviewer.component.html +++ b/src/app/mailviewer/singlemailviewer.component.html @@ -63,7 +63,7 @@ - - + + diff --git a/src/app/rmmapi/rbwebmail.ts b/src/app/rmmapi/rbwebmail.ts index 05dfa2293..7958b355b 100644 --- a/src/app/rmmapi/rbwebmail.ts +++ b/src/app/rmmapi/rbwebmail.ts @@ -535,8 +535,12 @@ export class RunboxWebmailAPI { return this.http.post('/rest/v1/spam/', JSON.stringify(params)); } + public allowListSender(param): Observable { + return this.http.post('/rest/v1/rules/update_nospam_list', JSON.stringify({'email_addresses': [param]})); + } + public blockSender(param): Observable { - return this.http.post('/rest/v1/rules/block_sender', JSON.stringify({'sender': param})); + return this.http.post('/rest/v1/rules/block_sender', JSON.stringify({'sender': param})); } // Moves to Trash if not already in Trash From feec94fcad3e129c741bbe2a9a5f59aaf12f35d7 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Mon, 3 Feb 2025 16:20:51 +0000 Subject: [PATCH 060/131] fix(index): Disable service worker fetching/caching of index files Fixes: #1202 --- src/app/xapian/searchservice.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/xapian/searchservice.ts b/src/app/xapian/searchservice.ts index cfc971c5a..501907426 100644 --- a/src/app/xapian/searchservice.ts +++ b/src/app/xapian/searchservice.ts @@ -528,7 +528,7 @@ export class SearchService { if (!this.stopIndexDownloadingInProgress) { return this.httpclient.request( new HttpRequest( - 'GET', '/mail/download_xapian_index?fileno=' + fileno, + 'GET', '/mail/download_xapian_index?ngsw-bypass=1&fileno=' + fileno, {responseType: 'arraybuffer', reportProgress: true} )).pipe( map(event => { @@ -679,7 +679,7 @@ export class SearchService { } checkIfDownloadableIndexExists(): Observable { - return this.httpclient.get('/mail/download_xapian_index?exists=check').pipe( + return this.httpclient.get('/mail/download_xapian_index?ngsw-bypass=1&exists=check').pipe( map((stat: any) => { this.serverIndexSize = stat.size; this.serverIndexSizeUncompressed = stat.uncompressedsize; @@ -757,7 +757,7 @@ export class SearchService { this.httpclient.request( new HttpRequest( 'GET', - `/rest/v1/searchindex/file/${p.folder}/${file.filename}`, { + `/rest/v1/searchindex/file/${p.folder}/${file.filename}?ngsw-bypass=1`, { reportProgress: true, responseType: 'arraybuffer' } From b8025272b2077f7df51c8675c590f6fdd05c9607 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Mon, 27 Jan 2025 16:43:40 +0000 Subject: [PATCH 061/131] fix(compose): Stop creating new draft copies after sending Fixes: #644 --- src/app/compose/compose.component.ts | 4 ++++ src/app/compose/draftdesk.component.ts | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/compose/compose.component.ts b/src/app/compose/compose.component.ts index 129cdcc15..7a0deec90 100644 --- a/src/app/compose/compose.component.ts +++ b/src/app/compose/compose.component.ts @@ -877,6 +877,10 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit { ngOnDestroy() { if (this.editor) { + // turn off onChange handler because it changes the + // msg_body and causes auto-save to trigger. See + // https://github.com/runbox/runbox7/issues/644#issuecomment-1651655948 + this.editor.off('change'); this.tinymce_plugin.remove(this.editor); } } diff --git a/src/app/compose/draftdesk.component.ts b/src/app/compose/draftdesk.component.ts index d4c94dcaf..fb3efb22e 100644 --- a/src/app/compose/draftdesk.component.ts +++ b/src/app/compose/draftdesk.component.ts @@ -114,7 +114,6 @@ export class DraftDeskComponent implements OnInit { draftDeleted(messageId) { this.draftDeskservice.deleteDraft(messageId); - this.updateDraftsInView(); } exitToTable() { From 6647253d341519e3f8a83af79f4c988d9c68d0f6 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Wed, 5 Feb 2025 15:04:04 +0000 Subject: [PATCH 062/131] fix(mailviewer): Shows confirmation after block/allow sender actions --- src/app/mailviewer/rmm7messageactions.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/app/mailviewer/rmm7messageactions.ts b/src/app/mailviewer/rmm7messageactions.ts index 3622908d3..9b032e608 100644 --- a/src/app/mailviewer/rmm7messageactions.ts +++ b/src/app/mailviewer/rmm7messageactions.ts @@ -202,12 +202,13 @@ export class RMM7MessageActions implements MessageActions { const msg = `Blocking sender: ${param}`; const snackBarRef = this.snackBar.open(msg); this.rmmapi.blockSender(param).subscribe((res) => { + snackBarRef.dismiss(); if ( res.status === 'error' ) { - snackBarRef.dismiss(); this.snackBar.open('There was an error with Sender blocking functionality. Please try again.', 'Dismiss'); + } else { + this.snackBar.open(`${param} added to blocklist.`, 'Dismiss'); } }); - snackBarRef.dismiss(); } allowListSender(param) { @@ -217,9 +218,10 @@ export class RMM7MessageActions implements MessageActions { if ( res.status === 'error' ) { snackBarRef.dismiss(); this.snackBar.open('There was an error with Sender allowlisting functionality. Please try again.', 'Dismiss'); + } else { + this.snackBar.open(`${param} added to allowlist.`, 'Dismiss'); } }); - snackBarRef.dismiss(); } // Update mailviewer menu flag icon after flagging? From 209ccb43cafe2ce434aae4d9ee8ff5b73b087ea7 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Wed, 12 Feb 2025 17:33:14 +0000 Subject: [PATCH 063/131] fix(upgrades): Fetch the correct current subscription for the user --- src/app/account-app/account-upgrades.component.html | 8 ++++---- src/app/account-app/account-upgrades.component.ts | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/app/account-app/account-upgrades.component.html b/src/app/account-app/account-upgrades.component.html index a6b26cfce..19897a9a9 100644 --- a/src/app/account-app/account-upgrades.component.html +++ b/src/app/account-app/account-upgrades.component.html @@ -98,7 +98,7 @@

One extra year for free!

{{ plan.name.replace('Runbox', '') }} -

Your current subscription

+

Your current subscription

@@ -142,8 +142,8 @@

Subscribe for Purchase for Renew for - Upgrade to - Downgrade to + Upgrade to + Downgrade to {{ plan.currency.replace("USD","$") }}{{ plan.price | number:'1.2-2' }} @@ -222,7 +222,7 @@

Price for 1 year
- {{ plan.currency.replace("USD","$") }}{{ plan.price }} Your current {{ p.type }} + {{ plan.currency.replace("USD","$") }}{{ plan.price }} Your current {{ plan.type }}
+

{ + if(res) { + this.updateService.updateIsReady.next(false); + // Update is available, ask user if they want to restart: + this.snackBar.openFromComponent(UpdateAlertComponent, { + data: this.updateService.updateStatus, + duration: 10000, + }); + } + }); } ngAfterViewInit() { @@ -481,14 +495,18 @@ export class AppComponent implements OnInit, AfterViewInit, CanvasTableSelectLis }); }); - if ('serviceWorker' in navigator) { - try { - Notification.requestPermission(); - } catch (e) {} - } + if ('serviceWorker' in navigator) { + try { + Notification.requestPermission(); + } catch (e) {} + } - this.subscribeToNotifications(); - this.calculateWidthDependentElements(); + this.subscribeToNotifications(); + this.calculateWidthDependentElements(); + } + + reload(): void { + location.reload(); } selectMessageFromFragment(fragment: string): void { @@ -670,7 +688,7 @@ export class AppComponent implements OnInit, AfterViewInit, CanvasTableSelectLis public trainSpam(params) { const msg = params.is_spam ? 'Reporting spam' : 'Reporting not spam'; - const snackBarRef = this.snackBar.open( msg ); + this.snackBar.open( msg ); const unfilteredMessageIds = this.canvastable.rows.selectedMessageIds(); // ensure valid IDs const messageIds = unfilteredMessageIds.filter(id => Number.isInteger(id)); @@ -706,17 +724,12 @@ export class AppComponent implements OnInit, AfterViewInit, CanvasTableSelectLis const res = this.rmmapi.trainSpam({is_spam: params.is_spam, from_folder_id: currentFolderId, messages: messageIds}); res.subscribe(data => { if ( data.status === 'error' ) { - snackBarRef.dismiss(); this.snackBar.open('There was an error with Spam functionality. Please select the messages and try again.', 'Dismiss'); } - snackBarRef.dismiss(); }, (err) => { console.error('Error reporting spam', err); this.snackBar.open('There was an error with Spam functionality.', 'Dismiss'); - }, - () => { - snackBarRef.dismiss(); - }); + }); return res; } }); @@ -741,7 +754,7 @@ export class AppComponent implements OnInit, AfterViewInit, CanvasTableSelectLis } public setReadStatus(status: boolean) { - const snackBarRef = this.snackBar.open('Toggling read status...'); + this.snackBar.open('Toggling read status...'); const messageIds = this.canvastable.rows.selectedMessageIds(); this.messageActionsHandler.updateMessages({ @@ -766,15 +779,11 @@ export class AppComponent implements OnInit, AfterViewInit, CanvasTableSelectLis }, ids: msgIds }) - , - afterwards: (result) => { - snackBarRef.dismiss(); - } }); } public setFlaggedStatus(status: boolean) { - const snackBarRef = this.snackBar.open('Toggling flags...'); + this.snackBar.open('Toggling flags...'); const messageIds = this.canvastable.rows.selectedMessageIds(); this.messageActionsHandler.updateMessages({ @@ -799,10 +808,6 @@ export class AppComponent implements OnInit, AfterViewInit, CanvasTableSelectLis }, ids: msgIds }) - , - afterwards: (result) => { - snackBarRef.dismiss(); - } }); } diff --git a/src/app/compose/compose.component.ts b/src/app/compose/compose.component.ts index 7a0deec90..6fa75fe0d 100644 --- a/src/app/compose/compose.component.ts +++ b/src/app/compose/compose.component.ts @@ -609,11 +609,10 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit { } public trashDraft() { - const snackBarRef = this.snackBar.open('Deleting'); + this.snackBar.open('Deleting'); this.draftDeskservice.isEditing = -1; this.draftDeskservice.composingNewDraft = null; this.rmmapi.deleteMessages([this.model.mid]).subscribe(() => { - snackBarRef.dismiss(); this.draftDeleted.emit(this.model.mid); this.exitIfNeeded(); }); diff --git a/src/app/mailviewer/rmm7messageactions.ts b/src/app/mailviewer/rmm7messageactions.ts index 9b032e608..76dd9b82c 100644 --- a/src/app/mailviewer/rmm7messageactions.ts +++ b/src/app/mailviewer/rmm7messageactions.ts @@ -155,7 +155,7 @@ export class RMM7MessageActions implements MessageActions { // FIXME: How does the view change? close mailview, show "next" email or? trainSpam(params) { const msg = params.is_spam ? 'Reporting spam' : 'Reporting not spam'; - const snackBarRef = this.snackBar.open(msg); + this.snackBar.open(msg); this.updateMessages({ messageIds: [this.mailViewerComponent.messageId], updateLocal: (msgIds: number[]) => { @@ -182,7 +182,6 @@ export class RMM7MessageActions implements MessageActions { const res = this.rmmapi.trainSpam({is_spam: params.is_spam, messages: msgIds}); res.subscribe(data => { if ( data.status === 'error' ) { - snackBarRef.dismiss(); this.snackBar.open('There was an error with Spam functionality. Please try again.', 'Dismiss'); } }, (err) => { @@ -192,7 +191,6 @@ export class RMM7MessageActions implements MessageActions { return res; }, afterwards: (result) => { - snackBarRef.dismiss(); this.mailViewerComponent.close(); } }); @@ -200,9 +198,8 @@ export class RMM7MessageActions implements MessageActions { blockSender(param) { const msg = `Blocking sender: ${param}`; - const snackBarRef = this.snackBar.open(msg); + this.snackBar.open(msg); this.rmmapi.blockSender(param).subscribe((res) => { - snackBarRef.dismiss(); if ( res.status === 'error' ) { this.snackBar.open('There was an error with Sender blocking functionality. Please try again.', 'Dismiss'); } else { @@ -213,10 +210,9 @@ export class RMM7MessageActions implements MessageActions { allowListSender(param) { const msg = `AllowListing sender: ${param}`; - const snackBarRef = this.snackBar.open(msg); + this.snackBar.open(msg); this.rmmapi.allowListSender(param).subscribe((res) => { if ( res.status === 'error' ) { - snackBarRef.dismiss(); this.snackBar.open('There was an error with Sender allowlisting functionality. Please try again.', 'Dismiss'); } else { this.snackBar.open(`${param} added to allowlist.`, 'Dismiss'); diff --git a/src/app/updatealert/updatealert.component.html b/src/app/updatealert/updatealert.component.html index ce6e57956..4743de067 100644 --- a/src/app/updatealert/updatealert.component.html +++ b/src/app/updatealert/updatealert.component.html @@ -1,5 +1,4 @@ -

App update available!

- +

An update of the application is available, and you may reload the app now to use the latest version.

@@ -11,10 +10,10 @@

App update available!

and the changes can be seen in the Runbox 7 changelog here.
To provide feedback on Runbox 7 features, please visit our community forum. -

- - +

+
+
- - - + + +
diff --git a/src/app/updatealert/updatealert.component.ts b/src/app/updatealert/updatealert.component.ts index c9e8460c2..caebcea6e 100644 --- a/src/app/updatealert/updatealert.component.ts +++ b/src/app/updatealert/updatealert.component.ts @@ -18,22 +18,21 @@ // ---------- END RUNBOX LICENSE ---------- import { Component, Inject } from '@angular/core'; -import { MatLegacyDialogRef as MatDialogRef, MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA } from '@angular/material/legacy-dialog'; +import { MatSnackBarRef, MAT_SNACK_BAR_DATA } from "@angular/material/snack-bar"; @Component({ templateUrl: 'updatealert.component.html' }) export class UpdateAlertComponent { constructor( - private dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: any + @Inject(MAT_SNACK_BAR_DATA) public data: any, + public snackBarRef: MatSnackBarRef, ) { - console.log('Update details:', data); } close() { - this.dialogRef.close(); + this.snackBarRef.dismiss(); } reload() { diff --git a/src/app/updatealert/updatealert.service.ts b/src/app/updatealert/updatealert.service.ts index 4c9952bc6..85a54806a 100644 --- a/src/app/updatealert/updatealert.service.ts +++ b/src/app/updatealert/updatealert.service.ts @@ -17,16 +17,35 @@ // along with Runbox 7. If not, see . // ---------- END RUNBOX LICENSE ---------- -import { ApplicationRef, Injectable, NgZone } from '@angular/core'; +import { ApplicationRef, Injectable } from '@angular/core'; import { SwUpdate, VersionReadyEvent } from '@angular/service-worker'; import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; -import { UpdateAlertComponent } from './updatealert.component'; import { environment } from '../../environments/environment'; -import { concat, timer } from 'rxjs'; +import { concat, interval, BehaviorSubject } from 'rxjs'; import { filter, map, first } from 'rxjs/operators'; +interface UpdateStatus { + type: string; + current: { + hash: string; + appData?: object; + }; + available: { + hash: string; + appData?: object; + }; +} + @Injectable() export class UpdateAlertService { + public updateIsReady: BehaviorSubject = new BehaviorSubject(false); + public updateStatus: UpdateStatus = { + 'type':'UPDATE_AVAILABLE', + 'current': + {'hash':'blah', 'appData':{'build_epoch':'XX'}}, + 'available': + {'hash':'blah', 'appData':{'commit':'test', 'build_time': 'time', 'build_epoch':'XX'}}, + }; constructor( private appRef: ApplicationRef, private swupdate: SwUpdate, @@ -37,17 +56,22 @@ export class UpdateAlertService { const updatesAvailable = swupdate.versionUpdates.pipe( filter((evt): evt is VersionReadyEvent => evt.type === 'VERSION_READY'), - map(evt => ({ - type: 'UPDATE_AVAILABLE', - current: evt.currentVersion, - available: evt.latestVersion, - }))); + map(evt => { + const update: UpdateStatus = { + type: 'UPDATE_AVAILABLE', + current: evt.currentVersion, + available: evt.latestVersion, + }; + return update; + }) + ); updatesAvailable.subscribe(ev => { - dialog.open(UpdateAlertComponent, { data: ev }); + this.updateStatus = ev; + this.updateIsReady.next(true); }); const appIsStable = this.appRef.isStable.pipe(first(isStable => isStable === true)); - const everyFiveMins = timer(0, 5 * 60 * 1000); + const everyFiveMins = interval(5 * 60 * 1000); const everyFiveMinsOnceAppIsStable = concat(appIsStable, everyFiveMins); everyFiveMinsOnceAppIsStable.subscribe(() => this.checkForUpdates() From 9827c1359b35e97fef2f9e981b6ce07713300284 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Thu, 20 Feb 2025 11:42:28 +0000 Subject: [PATCH 067/131] fix(updates): Revert updates to NgZone, issues with isStable isStable doesn't happen in an app that starts scheduled tasks immediately --- src/app/app.module.ts | 4 +++- src/app/updatealert/updatealert.service.ts | 21 ++++++++++----------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/app/app.module.ts b/src/app/app.module.ts index de2216dad..bced66edb 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -71,7 +71,7 @@ import { ResizerModule } from './directives/resizer.module'; import { MainContainerComponent } from './maincontainer.component'; import { HeaderToolbarComponent } from './menu/headertoolbar.component'; import { LocalSearchIndexModule } from './xapian/localsearchindex.module'; -import { ServiceWorkerModule } from '@angular/service-worker'; +import { ServiceWorkerModule, SwRegistrationOptions } from '@angular/service-worker'; import { environment } from '../environments/environment'; import { SearchExpressionBuilderModule } from './xapian/search-expression-builder/search-expression-builder.module'; import { UpdateAlertModule } from './updatealert/updatealert.module'; @@ -201,6 +201,8 @@ const routes: Routes = [ StorageService, { provide: HTTP_INTERCEPTORS, useClass: RMMHttpInterceptorService, multi: true }, { provide: ErrorHandler, useClass: SentryErrorHandler }, + { provide: SwRegistrationOptions, + useFactory: () => ({ registrationStrategy: 'registerWithDelay:5000' }) } ], bootstrap: [MainContainerComponent] }) diff --git a/src/app/updatealert/updatealert.service.ts b/src/app/updatealert/updatealert.service.ts index 85a54806a..88b531284 100644 --- a/src/app/updatealert/updatealert.service.ts +++ b/src/app/updatealert/updatealert.service.ts @@ -17,12 +17,12 @@ // along with Runbox 7. If not, see . // ---------- END RUNBOX LICENSE ---------- -import { ApplicationRef, Injectable } from '@angular/core'; +import { Injectable, NgZone } from '@angular/core'; import { SwUpdate, VersionReadyEvent } from '@angular/service-worker'; import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; import { environment } from '../../environments/environment'; -import { concat, interval, BehaviorSubject } from 'rxjs'; -import { filter, map, first } from 'rxjs/operators'; +import { BehaviorSubject } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; interface UpdateStatus { type: string; @@ -47,7 +47,7 @@ export class UpdateAlertService { {'hash':'blah', 'appData':{'commit':'test', 'build_time': 'time', 'build_epoch':'XX'}}, }; constructor( - private appRef: ApplicationRef, + private ngZone: NgZone, private swupdate: SwUpdate, dialog: MatDialog ) { @@ -70,19 +70,18 @@ export class UpdateAlertService { this.updateIsReady.next(true); }); - const appIsStable = this.appRef.isStable.pipe(first(isStable => isStable === true)); - const everyFiveMins = interval(5 * 60 * 1000); - const everyFiveMinsOnceAppIsStable = concat(appIsStable, everyFiveMins); - everyFiveMinsOnceAppIsStable.subscribe(() => - this.checkForUpdates() - ); + this.ngZone.runOutsideAngular(() => { + this.checkForUpdates(); + setInterval(() => this.ngZone.run(() => + this.checkForUpdates() + ), 5 * 60 * 1000); + }); } } checkForUpdates() { console.log(' checking for updates'); this.swupdate.checkForUpdate() - .then(() => this.checkForUpdates()) .catch((err) => console.log('Unable to check for updates', err)); } } From 43778013193e0b26a432faed6b23c826c9e54c74 Mon Sep 17 00:00:00 2001 From: Geir Thomas Andersen Date: Fri, 21 Feb 2025 10:07:45 +0100 Subject: [PATCH 068/131] style(snackbar): Improve link colors and remove link to forum. --- src/app/updatealert/updatealert.component.html | 6 ++---- src/styles.scss | 11 +++++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/app/updatealert/updatealert.component.html b/src/app/updatealert/updatealert.component.html index 4743de067..a976ee332 100644 --- a/src/app/updatealert/updatealert.component.html +++ b/src/app/updatealert/updatealert.component.html @@ -1,4 +1,4 @@ -
+

An update of the application is available, and you may reload the app now to use the latest version.

@@ -7,9 +7,7 @@ built on {{ build_time }},
- and the changes can be seen in the Runbox 7 changelog - here.
- To provide feedback on Runbox 7 features, please visit our community forum. + and the changes can be seen in the Runbox 7 changelog.

diff --git a/src/styles.scss b/src/styles.scss index 62e3ea48a..03f7ae2e2 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -406,6 +406,17 @@ mat-grid-tile.tableTitle { justify-content: center; } +/* Snackbar */ + +.snackbar { + a { + color: #fff !important; + } +} + +.mat-button-wrapper { + color: mat.get-color-from-palette($rmm-default-foreground) !important; +} /*** Login screen ***/ From a294e696116d34d19c7b3dd091fa930d625be5c5 Mon Sep 17 00:00:00 2001 From: Geir Thomas Andersen Date: Mon, 24 Feb 2025 15:56:47 +0100 Subject: [PATCH 069/131] style(snackbar): Apply link colors correctly. --- src/styles.scss | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/styles.scss b/src/styles.scss index 03f7ae2e2..6e6d63eb2 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -408,14 +408,13 @@ mat-grid-tile.tableTitle { /* Snackbar */ -.snackbar { +.mat-snack-bar-container { a { color: #fff !important; } -} - -.mat-button-wrapper { - color: mat.get-color-from-palette($rmm-default-foreground) !important; + .mat-button-wrapper { + color: mat.get-color-from-palette($rmm-default-foreground) !important; + } } /*** Login screen ***/ From 6a6886843f21eb635f13031c15ae03bb898da228 Mon Sep 17 00:00:00 2001 From: Bas Date: Mon, 17 Mar 2025 14:42:30 +0000 Subject: [PATCH 070/131] fix(workflow): Update deprecated dependencies --- .github/workflows/ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a91deda5..d87f155eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,11 +13,12 @@ jobs: phase: ['lint', 'policy', 'unit', 'e2e', 'build'] name: Run ${{ matrix.phase }} tests steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: node-version: '16.x' - - uses: actions/cache@v2 + cache: 'npm' + - uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} From f6f26f28541a30ddf5dda430d1e3660f5b91ee73 Mon Sep 17 00:00:00 2001 From: Geir Thomas Andersen Date: Thu, 6 Mar 2025 20:40:33 +0100 Subject: [PATCH 071/131] style(general): Move "beta" from Mail to Contacts and Calendar. --- src/app/app.component.html | 2 +- src/app/calendar-app/calendar-app.component.html | 7 +++++++ src/app/contacts-app/contacts-welcome.component.ts | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/app/app.component.html b/src/app/app.component.html index a5efbc25f..5f6eb95fb 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -38,7 +38,7 @@

Good {{timeOfDay}}, {{(rmmapi.me | async)?.user_address}}.

-

Welcome to Runbox 7 (beta)! Please see help and our forum for support and suggestions.

+

Welcome to Runbox 7! Please see help and our forum for support and suggestions.

(Experimental) create a video call

diff --git a/src/app/calendar-app/calendar-app.component.html b/src/app/calendar-app/calendar-app.component.html index 99d55a6df..d539940ae 100644 --- a/src/app/calendar-app/calendar-app.component.html +++ b/src/app/calendar-app/calendar-app.component.html @@ -187,6 +187,13 @@

Calendar Menu

[activities]="calendarservice.activities.observable" > + + + + + Runbox 7 Calendar (beta) lets you manage and synchronize your calendars via CalDAV. You may also import and export calendar events using ICS files. + + diff --git a/src/app/contacts-app/contacts-welcome.component.ts b/src/app/contacts-app/contacts-welcome.component.ts index aea542dce..b4f5044f7 100644 --- a/src/app/contacts-app/contacts-welcome.component.ts +++ b/src/app/contacts-app/contacts-welcome.component.ts @@ -25,7 +25,7 @@ import { Component } from '@angular/core';

Runbox 7 Contacts

- Runbox 7 Contacts lets you easily import, manage, and synchronize all your contacts. + Runbox 7 Contacts (beta) lets you easily import, manage, and synchronize all your contacts.

If it's your first time here, you may want to check out the From 18742a4550cbc18f4790aaa7d915186d8da9630e Mon Sep 17 00:00:00 2001 From: Geir Thomas Andersen Date: Thu, 10 Apr 2025 20:20:06 +0200 Subject: [PATCH 072/131] style(payment): Make currency formats consistent. --- .../account-upgrades.component.html | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/app/account-app/account-upgrades.component.html b/src/app/account-app/account-upgrades.component.html index 19897a9a9..346565d97 100644 --- a/src/app/account-app/account-upgrades.component.html +++ b/src/app/account-app/account-upgrades.component.html @@ -113,7 +113,7 @@

Your current subsc
  • Unlimited aliases on your domains
  • - Save {{ orig_three_plans[i].currency.replace("USD","$") }}{{ orig_three_plans[i].price * 3 - plan.price | number: '1.2-2' }} in total + Save {{ orig_three_plans[i].currency }} {{ orig_three_plans[i].price * 3 - plan.price | number: '1.2-2' }} in total
    compared to 3 * {{ orig_three_plans[i].subtype | titlecase }} 1-year

    @@ -133,7 +133,7 @@

    -
     {{ orig_three_plans[i].currency.replace("USD","$") }}{{ orig_three_plans[i].price * 3 | number: '1.2-2' }} 
    +
     {{ orig_three_plans[i].currency }} {{ orig_three_plans[i].price * 3 | number: '1.2-2' }} 
    - Save {{ plan.currency.replace("USD","$") }}{{ plan.price * 3 - subs_three[i].price | number: '1.2-2' }} + Save {{ plan.currency }} {{ plan.price * 3 - subs_three[i].price | number: '1.2-2' }}
    @@ -306,7 +306,7 @@

    Sub-Accounts (additional accounts)

    - +
    From 98039c86da120676caefa4299eb4c4dd3dc613ed Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Mon, 28 Apr 2025 15:09:13 +0000 Subject: [PATCH 073/131] fix(index): More service worker skipping for index downloads More urls missed in #1202 --- e2e/cypress/integration/compose.ts | 12 ++++++------ src/app/rmmapi/rbwebmail.ts | 3 ++- src/app/rmmapi/restapi_standalone.ts | 3 ++- src/app/xapian/searchservice.spec.ts | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/e2e/cypress/integration/compose.ts b/e2e/cypress/integration/compose.ts index af1a1372b..bc87851a2 100644 --- a/e2e/cypress/integration/compose.ts +++ b/e2e/cypress/integration/compose.ts @@ -12,7 +12,7 @@ describe('Composing emails', () => { Cypress.config('requestTimeout', 100000); it('should display draft card', () => { - cy.intercept('/mail/download_xapian_index?listallmessages=1&page=0&sinceid=0&sincechangeddate=0&pagesize=100&skipcontent=1&folder=Drafts&avoidcacheuniqueparam=*').as('listAllmessages'); + cy.intercept('/mail/download_xapian_index?ngsw-bypass=1&listallmessages=1&page=0&sinceid=0&sincechangeddate=0&pagesize=100&skipcontent=1&folder=Drafts&avoidcacheuniqueparam=*').as('listAllmessages'); // /mail/download_xapian_index?listallmessages=1&page=0&sinceid=0&sincechangeddate=0&pagesize=100&skipcontent=1&folder=Drafts&avoidcacheuniqueparam=1654682375451 cy.visit('/compose?new=true'); cy.wait('@listAllmessages', {'timeout':10000}); @@ -21,7 +21,7 @@ describe('Composing emails', () => { }); it('should update action bar text to subject', () => { - cy.intercept('/mail/download_xapian_index?listallmessages=1&page=0&sinceid=0&sincechangeddate=0&pagesize=100&skipcontent=1&folder=Drafts&avoidcacheuniqueparam=*').as('listAllmessages'); + cy.intercept('/mail/download_xapian_index?ngsw-bypass=1&listallmessages=1&page=0&sinceid=0&sincechangeddate=0&pagesize=100&skipcontent=1&folder=Drafts&avoidcacheuniqueparam=*').as('listAllmessages'); // /mail/download_xapian_index?listallmessages=1&page=0&sinceid=0&sincechangeddate=0&pagesize=100&skipcontent=1&folder=Drafts&avoidcacheuniqueparam=1654682375451 cy.visit('/compose?new=true'); cy.wait('@listAllmessages', {'timeout':10000}); @@ -32,7 +32,7 @@ describe('Composing emails', () => { }); it('should complain on invalid email address, blur', () => { - cy.intercept('/mail/download_xapian_index?listallmessages=1&page=0&sinceid=0&sincechangeddate=0&pagesize=100&skipcontent=1&folder=Drafts&avoidcacheuniqueparam=*').as('listAllmessages'); + cy.intercept('/mail/download_xapian_index?ngsw-bypass=1&listallmessages=1&page=0&sinceid=0&sincechangeddate=0&pagesize=100&skipcontent=1&folder=Drafts&avoidcacheuniqueparam=*').as('listAllmessages'); // /mail/download_xapian_index?listallmessages=1&page=0&sinceid=0&sincechangeddate=0&pagesize=100&skipcontent=1&folder=Drafts&avoidcacheuniqueparam=1654682375451 cy.visit('/compose?new=true'); cy.wait('@listAllmessages', {'timeout':10000}); @@ -45,7 +45,7 @@ describe('Composing emails', () => { }); it('should complain on invalid email address, enter', () => { - cy.intercept('/mail/download_xapian_index?listallmessages=1&page=0&sinceid=0&sincechangeddate=0&pagesize=100&skipcontent=1&folder=Drafts&avoidcacheuniqueparam=*').as('listAllmessages'); + cy.intercept('/mail/download_xapian_index?ngsw-bypass=1&listallmessages=1&page=0&sinceid=0&sincechangeddate=0&pagesize=100&skipcontent=1&folder=Drafts&avoidcacheuniqueparam=*').as('listAllmessages'); // /mail/download_xapian_index?listallmessages=1&page=0&sinceid=0&sincechangeddate=0&pagesize=100&skipcontent=1&folder=Drafts&avoidcacheuniqueparam=1654682375451 cy.visit('/compose?new=true'); cy.wait('@listAllmessages', {'timeout':10000}); @@ -78,7 +78,7 @@ describe('Composing emails', () => { }); it('emailing a contact should not use their nickname', () => { - cy.intercept('/mail/download_xapian_index?listallmessages=1&page=0&sinceid=0&sincechangeddate=0&pagesize=100&skipcontent=1&folder=Drafts&avoidcacheuniqueparam=*').as('listAllmessages'); + cy.intercept('/mail/download_xapian_index?ngsw-bypass=1&listallmessages=1&page=0&sinceid=0&sincechangeddate=0&pagesize=100&skipcontent=1&folder=Drafts&avoidcacheuniqueparam=*').as('listAllmessages'); // /mail/download_xapian_index?listallmessages=1&page=0&sinceid=0&sincechangeddate=0&pagesize=100&skipcontent=1&folder=Drafts&avoidcacheuniqueparam=1654682375451 cy.visit('/compose?new=true'); cy.wait('@listAllmessages', {'timeout':10000}); @@ -90,7 +90,7 @@ describe('Composing emails', () => { }); it('closing a newly composed email should return where we started', () => { - cy.intercept('/mail/download_xapian_index?listallmessages=1&page=0&sinceid=0&sincechangeddate=0&pagesize=100&skipcontent=1&folder=Drafts&avoidcacheuniqueparam=*').as('listAllmessages'); + cy.intercept('/mail/download_xapian_index?ngsw-bypass=1&listallmessages=1&page=0&sinceid=0&sincechangeddate=0&pagesize=100&skipcontent=1&folder=Drafts&avoidcacheuniqueparam=*').as('listAllmessages'); // /mail/download_xapian_index?listallmessages=1&page=0&sinceid=0&sincechangeddate=0&pagesize=100&skipcontent=1&folder=Drafts&avoidcacheuniqueparam=1654682375451 cy.visit('/compose'); cy.wait('@listAllmessages', {'timeout':10000}); diff --git a/src/app/rmmapi/rbwebmail.ts b/src/app/rmmapi/rbwebmail.ts index 7958b355b..46043700e 100644 --- a/src/app/rmmapi/rbwebmail.ts +++ b/src/app/rmmapi/rbwebmail.ts @@ -359,7 +359,8 @@ export class RunboxWebmailAPI { folder?: string) : Observable { // TODO: Need a JSON based REST api endpoint for this - const url = '/mail/download_xapian_index?listallmessages=1' + + const url = '/mail/download_xapian_index?ngsw-bypass=1' + + '&listallmessages=1' + '&page=' + page + '&sinceid=' + sinceid + '&sincechangeddate=' + Math.floor(sincechangeddate / 1000) + diff --git a/src/app/rmmapi/restapi_standalone.ts b/src/app/rmmapi/restapi_standalone.ts index dfa2cec1c..90b0d8a2a 100644 --- a/src/app/rmmapi/restapi_standalone.ts +++ b/src/app/rmmapi/restapi_standalone.ts @@ -31,7 +31,8 @@ export function listAllMessages( folder?: string) : Promise { // TODO: Need a JSON based REST api endpoint for this - const url = '/mail/download_xapian_index?listallmessages=1' + + const url = '/mail/download_xapian_index?ngsw-bypass=1' + + '&listallmessages=1' + '&page=' + page + '&sinceid=' + sinceid + '&sincechangeddate=' + Math.floor(sincechangeddate / 1000) + diff --git a/src/app/xapian/searchservice.spec.ts b/src/app/xapian/searchservice.spec.ts index 164e620f2..c7401034c 100644 --- a/src/app/xapian/searchservice.spec.ts +++ b/src/app/xapian/searchservice.spec.ts @@ -264,7 +264,7 @@ describe('SearchService', () => { await new Promise(resolve => setTimeout(resolve, 100)); req = httpMock.expectOne(mockrequest => mockrequest.urlWithParams.indexOf('/mail/download_xapian_index?' + - 'listallmessages=1&page=0&sinceid=0&sincechangeddate=' + Math.floor(indexLastUpdateTime / 1000) + + 'ngsw-bypass=1&listallmessages=1&page=0&sinceid=0&sincechangeddate=' + Math.floor(indexLastUpdateTime / 1000) + '&pagesize=' + RunboxWebmailAPI.LIST_ALL_MESSAGES_CHUNK_SIZE + '&skipcontent=1&avoidcacheuniqueparam=') === 0); // message timestamps are in seconds: // message time must be later so that indexLastUpdateTime is updated From 08e19261ef8fbd908ab5cc8be15f294b79aeafc6 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Thu, 27 Feb 2025 18:08:26 +0000 Subject: [PATCH 074/131] fix(payments): Upgrade to latest version of Stripe SDK Fixes: #1448 --- .../stripe-add-card-dialog.component.html | 25 +---- .../stripe-add-card-dialog.component.ts | 67 +++++++------ src/app/account-app/payments.service.ts | 4 + .../stripe-payment-dialog.component.html | 64 +----------- .../stripe-payment-dialog.component.ts | 98 ++++++++----------- src/app/rmmapi/rbwebmail.ts | 10 +- 6 files changed, 92 insertions(+), 176 deletions(-) diff --git a/src/app/account-app/credit-cards/stripe-add-card-dialog.component.html b/src/app/account-app/credit-cards/stripe-add-card-dialog.component.html index f2caddc8e..fa6038444 100644 --- a/src/app/account-app/credit-cards/stripe-add-card-dialog.component.html +++ b/src/app/account-app/credit-cards/stripe-add-card-dialog.component.html @@ -42,31 +42,8 @@

    Credit card setup via Stripe

    -
    - VISA, MasterCard, AmEx -
    -
    -
    -
    - -
    -
    -
    -
    -
    - -
    -
    -
    - -
    -
    - - - -
    -
    +