diff --git a/src/app/contacts-app/contacts-settings.component.html b/src/app/contacts-app/contacts-settings.component.html index f1882acd6..d605b6d0e 100644 --- a/src/app/contacts-app/contacts-settings.component.html +++ b/src/app/contacts-app/contacts-settings.component.html @@ -40,7 +40,8 @@

Avatars (contact pictures)

(change)="saveAvatarSource()" > - Load from external services (like gravatar.com) + Load from external services (like gravatar.com + and libravatar.org) Only show pictures stored locally diff --git a/src/app/contacts-app/contacts.service.spec.ts b/src/app/contacts-app/contacts.service.spec.ts index 409cc88e6..01d6e7d6d 100644 --- a/src/app/contacts-app/contacts.service.spec.ts +++ b/src/app/contacts-app/contacts.service.spec.ts @@ -59,15 +59,22 @@ describe('ContactsService', () => { let storage: StorageService; const prefService = new MockPrefService() as unknown as PreferencesService; let sut: ContactsService; + let fetchSpy: jasmine.Spy; beforeEach(async () => { + fetchSpy = spyOn(window, 'fetch').and.callFake((url: RequestInfo): Promise => { + const avatarUrl = url.toString(); + const responseOk = (avatarUrl.includes('gravatar.com') && avatarUrl.includes('d770e753373e7b886680847fd773396d')) + || (avatarUrl.includes('libravatar.org') && avatarUrl.includes('291167d1fdc699e76ee26f977b87a906')); + return Promise.resolve({ ok: responseOk } as Response); + }); rmmapi = new MockRMMAPI(); storage = new StorageService(rmmapi as unknown as RunboxWebmailAPI); sut = new ContactsService(rmmapi as unknown as RunboxWebmailAPI, prefService, storage); }); describe('Avatar lookup', () => { - it('should look up remote avatars (gravatar)', (done) => { + it('should look up remote avatars', (done) => { prefService.set(prefService.prefGroup, 'avatarSource', 'remote' ); prefService.preferences.pipe(take(1)).subscribe(async _ => { @@ -75,12 +82,22 @@ describe('ContactsService', () => { let avatarUrl = await sut.lookupAvatar('test+gravatar@runbox.com'); expect(avatarUrl).toMatch(/gravatar/); + avatarUrl = await sut.lookupAvatar('test+libravatar@runbox.com'); + expect(avatarUrl).toMatch(/libravatar/); + // local avatar wins over gravatar avatarUrl = await sut.lookupAvatar('test@runbox.com'); expect(avatarUrl).toMatch(/test.url/); - avatarUrl = await sut.lookupAvatar('test+no+gravatar@runbox.com'); + avatarUrl = await sut.lookupAvatar('test+no+avatar@runbox.com'); expect(avatarUrl).toBeFalsy(); + expect(fetchSpy.calls.allArgs().map(args => args[0].toString())).toEqual([ + jasmine.stringMatching(/gravatar\.com/), + jasmine.stringMatching(/gravatar\.com/), + jasmine.stringMatching(/libravatar\.org/), + jasmine.stringMatching(/gravatar\.com/), + jasmine.stringMatching(/libravatar\.org/), + ]); done(); }); diff --git a/src/app/contacts-app/contacts.service.ts b/src/app/contacts-app/contacts.service.ts index 01c3001cc..cd6a8e5c4 100644 --- a/src/app/contacts-app/contacts.service.ts +++ b/src/app/contacts-app/contacts.service.ts @@ -45,7 +45,7 @@ interface AvatarCacheEntry { /* This caches avatar URLs, or `null`s in their absence. * Mostly useful for avatars not available in Contacts, - * and loaded from external services like gravatar. + * and loaded from external services like Gravatar and Libravatar. * * Putting (gr)avatar URLs in will cache them nicely, * except if they 404d last time around @@ -349,14 +349,26 @@ export class ContactsService implements OnDestroy { return Promise.resolve(null); } + const resolvedUrl = await this.lookupRemoteAvatar(email); + this.avatarCache.add(email, resolvedUrl); + return resolvedUrl; + } + + private async lookupRemoteAvatar(email: string): Promise { const hash = Md5.hashStr(email.toLowerCase()); - const url = 'https://gravatar.com/avatar/' + hash + '?d=404'; + const urls = [ + 'https://gravatar.com/avatar/' + hash + '?d=404', + 'https://seccdn.libravatar.org/avatar/' + hash + '?d=404', + ]; + + for (const url of urls) { + const response = await fetch(url); + if (response.ok) { + return url; + } + } - return fetch(url).then(response => { - const resolvedUrl = response.ok ? url : null; - this.avatarCache.add(email, resolvedUrl); - return Promise.resolve(resolvedUrl); - }); + return null; } lookupContact(email: string): Promise {