We can now ask Google for auth/contacts permission (not readonly!)

This commit is contained in:
Ben Gotow 2020-02-16 10:14:36 -06:00
parent 56f18a340b
commit c4d7b229f4
11 changed files with 79 additions and 51 deletions

View file

@ -17,17 +17,19 @@ class AddContactToolbarWithData extends React.Component<AddContactToolbarProps>
}
onAdd = () => {
if (showGPeopleReadonlyNotice(this.props.perspective.accountId)) {
return;
}
const { perspective } = this.props;
if (!('accountId' in perspective)) return;
if (showGPeopleReadonlyNotice(perspective.accountId)) return;
Actions.setFocus({ collection: 'contact', item: null });
Store.setEditing('new');
};
render() {
const { editing, perspective } = this.props;
const enabled = editing === false && perspective && perspective.accountId;
const acct = perspective && AccountStore.accountForId(perspective.accountId);
const enabled = 'accountId' in perspective && editing === false && perspective.accountId;
const acct = 'accountId' in perspective && AccountStore.accountForId(perspective.accountId);
return (
<div style={{ display: 'flex', order: 1000 }}>

View file

@ -73,7 +73,7 @@ class ContactDetailWithFocus extends React.Component<ContactDetailProps, Contact
const { editing, contacts, focusedId, perspective } = this.props;
const contact =
editing === 'new'
editing === 'new' && 'accountId' in perspective
? emptyContactForAccountId(perspective.accountId)
: contacts.find(c => c.id === focusedId);
@ -95,11 +95,14 @@ class ContactDetailWithFocus extends React.Component<ContactDetailProps, Contact
};
onSaveChanges = () => {
const { perspective } = this.props;
const contact = apply(this.state.contact, this.state.data);
if (!('accountId' in perspective)) return;
const task = contact.id
? SyncbackContactTask.forUpdating({ contact })
: SyncbackContactTask.forCreating({ contact, accountId: this.props.perspective.accountId });
: SyncbackContactTask.forCreating({ contact, accountId: perspective.accountId });
Actions.queueTask(task);
Store.setEditing(false);
};

View file

@ -50,22 +50,19 @@ class ContactDetailToolbarWithData extends React.Component<ContactDetailToolbarP
};
_onEdit = () => {
if (showGPeopleReadonlyNotice(this.props.perspective.accountId)) {
const contacts = this.actionSet();
const contact = contacts[0];
if (!contact || showGPeopleReadonlyNotice(contact.accountId)) {
return;
}
const actionSet = this.actionSet();
Store.setEditing(actionSet[0].id);
Store.setEditing(contact.id);
};
_onDelete = () => {
const contacts = this.actionSet();
if (
contacts.some(c => c.source === 'gpeople') &&
showGPeopleReadonlyNotice(this.props.perspective.accountId)
) {
if (contacts.some(c => c.source === 'gpeople' && showGPeopleReadonlyNotice(c.accountId))) {
return;
}
Actions.queueTask(
DestroyContactTask.forRemoving({
contacts: this.actionSet(),
@ -90,7 +87,7 @@ class ContactDetailToolbarWithData extends React.Component<ContactDetailToolbarP
}
const commands = {};
if (perspective && perspective.type === 'group' && actionSet.length > 0) {
if (perspective.type === 'group' && actionSet.length > 0) {
commands['core:remove-from-view'] = this._onRemoveFromSource;
}
if (actionSet.length > 0) {
@ -103,7 +100,7 @@ class ContactDetailToolbarWithData extends React.Component<ContactDetailToolbarP
return (
<BindGlobalCommands key={Object.keys(commands).join(',')} commands={commands}>
<div style={{ display: 'flex', order: 1000, marginRight: 10 }}>
{perspective && perspective.type === 'group' && (
{perspective.type === 'group' && (
<button
tabIndex={-1}
title={localized('Remove from Group')}

View file

@ -110,7 +110,7 @@ const ContactListSearchWithData = (props: ContactListSearchWithDataProps) => {
ref={this._searchEl}
value={props.search}
placeholder={`${localized('Search')} ${
props.perspective ? props.perspective.label : 'All Contacts'
props.perspective.type === 'unified' ? 'All Contacts' : props.perspective.label
}`}
onChange={e => props.setSearch(e.currentTarget.value)}
/>

View file

@ -11,7 +11,7 @@ import {
ChangeContactGroupMembershipTask,
localized,
} from 'mailspring-exports';
import { ContactsPerspective, Store } from './Store';
import { ContactsPerspective, Store, ContactsPerspectiveForGroup } from './Store';
import {
ScrollRegion,
OutlineView,
@ -28,11 +28,11 @@ interface ContactsPerspectivesProps {
groups: ContactGroup[];
books: ContactBook[];
findInMailDisabled: string[];
selected: ContactsPerspective | null;
onSelect: (item: ContactsPerspective | null) => void;
selected: ContactsPerspective;
onSelect: (item: ContactsPerspective) => void;
}
function perspectiveForGroup(g: ContactGroup): ContactsPerspective {
function perspectiveForGroup(g: ContactGroup): ContactsPerspectiveForGroup {
return {
accountId: g.accountId,
type: 'group',
@ -47,7 +47,7 @@ interface OutlineViewForAccountProps {
books: ContactBook[];
findInMailDisabled: boolean;
selected: ContactsPerspective | null;
onSelect: (item: ContactsPerspective | null) => void;
onSelect: (item: ContactsPerspective) => void;
}
const OutlineViewForAccount = ({
@ -178,8 +178,8 @@ const ContactsPerspectivesWithData: React.FunctionComponent<ContactsPerspectives
name: 'All Contacts',
iconName: 'people.png',
children: [],
selected: selected === null,
onSelect: () => onSelect(null),
selected: selected.type === 'unified',
onSelect: () => onSelect({ type: 'unified' }),
}}
/>
</section>
@ -190,7 +190,7 @@ const ContactsPerspectivesWithData: React.FunctionComponent<ContactsPerspectives
findInMailDisabled={findInMailDisabled.includes(a.id)}
books={books.filter(b => b.accountId === a.id)}
groups={groups.filter(b => b.accountId === a.id)}
selected={selected && selected.accountId === a.id ? selected : null}
selected={'accountId' in selected && selected.accountId === a.id ? selected : null}
onSelect={onSelect}
/>
))}

View file

@ -1,13 +1,22 @@
import { AccountStore } from 'mailspring-exports';
import { AccountStore, localized } from 'mailspring-exports';
import { remote } from 'electron';
const CONTACTS_OAUTH_SCOPE_ADDED = new Date(1581867440474);
export const showGPeopleReadonlyNotice = (accountId: string) => {
const acct = AccountStore.accountForId(accountId);
if (acct && acct.provider === 'gmail') {
if (
acct &&
acct.provider === 'gmail' &&
(!acct.authedAt || acct.authedAt < CONTACTS_OAUTH_SCOPE_ADDED)
) {
remote.dialog.showMessageBox({
message: 'Coming Soon for Google Accounts',
detail:
"We've added support for creating, updating and deleting contacts in Google accounts, but Google is still reviewing Mailspring's use of the Google People API so we're unable to sync these changes back to their servers. Stay tuned!",
message: localized(`Please re-authenticate with Google`),
detail: localized(
`To make changes to contacts in this account, you'll need to re-authorize Mailspring to access your data.\n\n` +
`In Mailspring's main window, go to Preferences > Accounts, select this account, and click "Re-authenticate". ` +
`You'll be prompted to give Mailspring additional permission to update and delete your contacts.`
),
});
return true;
}

View file

@ -10,7 +10,7 @@ import MailspringStore from 'mailspring-store';
import { ListTabular } from 'mailspring-component-kit';
class ContactsWindowStore extends MailspringStore {
_perspective: ContactsPerspective | null = null;
_perspective: ContactsPerspective = { type: 'unified' };
_listSource = new ListTabular.DataSource.DumbArrayDataSource<Contact>();
_contacts: Contact[] = [];
@ -83,15 +83,15 @@ class ContactsWindowStore extends MailspringStore {
this.trigger();
}
setPerspective(perspective: ContactsPerspective | null) {
setPerspective(perspective: ContactsPerspective) {
let q = DatabaseStore.findAll<Contact>(Contact)
.where(Contact.attributes.refs.greaterThan(0))
.where(Contact.attributes.hidden.equal(false));
if (perspective && perspective.type === 'all') {
if (perspective.type === 'all') {
q.where(Contact.attributes.source.not('mail'));
}
if (perspective && perspective.type === 'group') {
if (perspective.type === 'group') {
q.where(Contact.attributes.contactGroups.contains(perspective.groupId));
}
@ -116,13 +116,14 @@ class ContactsWindowStore extends MailspringStore {
}
repopulate() {
const perspective = this._perspective;
let filtered = [...this._contacts];
if (this._perspective) {
if (perspective.type !== 'unified') {
filtered = filtered.filter(c => {
if (c.accountId !== this._perspective.accountId) return false;
if (c.source !== 'mail' && this._perspective.type === 'found-in-mail') return false;
if (c.source === 'mail' && this._perspective.type !== 'found-in-mail') return false;
if (c.accountId !== perspective.accountId) return false;
if (c.source !== 'mail' && perspective.type === 'found-in-mail') return false;
if (c.source === 'mail' && perspective.type !== 'found-in-mail') return false;
return true;
});
}
@ -140,7 +141,15 @@ class ContactsWindowStore extends MailspringStore {
}
}
export type ContactsPerspectiveForGroup = {
label: string;
accountId: string;
groupId: string;
type: 'group';
};
export type ContactsPerspective =
| { type: 'unified' }
| {
label: string;
accountId: string;
@ -151,11 +160,6 @@ export type ContactsPerspective =
accountId: string;
type: 'found-in-mail';
}
| {
label: string;
accountId: string;
groupId: string;
type: 'group';
};
| ContactsPerspectiveForGroup;
export const Store = new ContactsWindowStore();

View file

@ -130,5 +130,5 @@ export const parseName = (name: { _data: string } | null) => {
};
export const formatDisplayName = (name: ContactBase['name']) => {
return `${name.givenName} ${name.familyName}`;
return `${name.givenName || ''} ${name.familyName || ''}`.trim();
};

View file

@ -16,7 +16,7 @@ const GMAIL_SCOPES = [
'https://www.googleapis.com/auth/userinfo.email', // email address
'https://www.googleapis.com/auth/userinfo.profile', // G+ profile
'https://mail.google.com/', // email
'https://www.googleapis.com/auth/contacts.readonly', // contacts
'https://www.googleapis.com/auth/contacts', // contacts
'https://www.googleapis.com/auth/calendar', // calendar
];
@ -206,7 +206,7 @@ export function buildGmailAuthURL() {
)}&access_type=offline&select_account%20consent`;
}
export async function finalizeAndValidateAccount(account) {
export async function finalizeAndValidateAccount(account: Account) {
if (account.settings.imap_host) {
account.settings.imap_host = account.settings.imap_host.trim();
}
@ -217,7 +217,9 @@ export async function finalizeAndValidateAccount(account) {
account.id = idForAccount(account.emailAddress, account.settings);
// handle special case for exchange/outlook/hotmail username field
account.settings.username = account.settings.username || account.settings.email;
// TODO BG: I don't think this line is in use but not 100% sure
(account.settings as any).username =
(account.settings as any).username || (account.settings as any).email;
if (account.settings.imap_port) {
account.settings.imap_port /= 1;
@ -233,6 +235,8 @@ export async function finalizeAndValidateAccount(account) {
const proc = new MailsyncProcess(AppEnv.getLoadSettings());
proc.identity = IdentityStore.identity();
proc.account = account;
const { response } = await proc.test();
return new Account(response.account);
await proc.test();
// Record the date of successful auth
account.authedAt = new Date();
}

View file

@ -77,18 +77,25 @@ export class Account extends ModelWithMetadata {
syncError: Attributes.Object({
modelKey: 'syncError',
}),
authedAt: Attributes.DateTime({
modelKey: 'authedAt',
}),
};
public name: string;
public provider: string;
public emailAddress: string;
public authedAt: Date;
public settings: {
imap_host: string;
imap_port: number;
imap_username: string;
imap_password: string;
imap_allow_insecure_ssl: boolean;
imap_security: 'SSL / TLS' | 'STARTTLS' | 'none';
smtp_host: string;
smtp_port: number;
smtp_username: string;
smtp_password: string;
smtp_allow_insecure_ssl: boolean;
@ -107,6 +114,7 @@ export class Account extends ModelWithMetadata {
this.aliases = this.aliases || [];
this.label = this.label || this.emailAddress;
this.syncState = this.syncState || Account.SYNC_STATE_OK;
this.authedAt = this.authedAt || new Date(0);
this.autoaddress = this.autoaddress || {
type: 'bcc',
value: '',

View file

@ -240,6 +240,7 @@ class _AccountStore extends MailspringStore {
const existing = this._accounts[existingIdx];
existing.syncState = Account.SYNC_STATE_OK;
existing.name = cleanAccount.name;
existing.authedAt = cleanAccount.authedAt;
existing.emailAddress = cleanAccount.emailAddress;
existing.settings = cleanAccount.settings;
}