Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/app/contacts-app/contacts-settings.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ <h4> Avatars (contact pictures) </h4>
(change)="saveAvatarSource()"
>
<mat-radio-button [value]="AvatarSource.REMOTE">
Load from external services (like <a href="https://gravatar.com">gravatar.com</a>)
Load from external services (like <a href="https://gravatar.com">gravatar.com</a>
and <a href="https://www.libravatar.org">libravatar.org</a>)
</mat-radio-button>
<mat-radio-button [value]="AvatarSource.LOCAL">
Only show pictures stored locally
Expand Down
21 changes: 19 additions & 2 deletions src/app/contacts-app/contacts.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,28 +59,45 @@ 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<Response> => {
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 _ => {
// grab gravatar if there's no local picture
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();
});
Expand Down
26 changes: 19 additions & 7 deletions src/app/contacts-app/contacts.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <img src's> will cache them nicely,
* except if they 404d last time around
Expand Down Expand Up @@ -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<string> {
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<Contact> {
Expand Down