From bfaae56b598fbd8a50d2c9b9f62449ba86b57eaf Mon Sep 17 00:00:00 2001 From: Ben Gotow Date: Sun, 10 Sep 2017 22:45:48 -0700 Subject: [PATCH] Cleanup onboarding, enforce metadata.expiration as timestamp --- .../link-tracking/package.json | 2 +- ...ccount-types.es6 => account-providers.es6} | 16 +- .../lib/decorators/create-page-for-form.jsx | 86 +++++---- .../onboarding/lib/form-field.jsx | 13 +- .../onboarding/lib/onboarding-actions.es6 | 6 +- .../onboarding/lib/onboarding-helpers.es6 | 168 +++++++----------- .../onboarding/lib/onboarding-root.jsx | 4 +- .../onboarding/lib/onboarding-store.es6 | 105 +++++------ .../onboarding/lib/page-account-choose.jsx | 20 +-- .../lib/page-account-onboarding-success.jsx | 15 +- .../lib/page-account-settings-exchange.jsx | 22 +-- .../lib/page-account-settings-gmail.jsx | 15 +- .../lib/page-account-settings-imap.jsx | 49 +++-- .../onboarding/lib/page-account-settings.jsx | 45 +++-- .../onboarding/lib/page-authenticate.jsx | 2 +- .../onboarding/styles/onboarding.less | 10 +- .../open-tracking/package.json | 2 +- .../lib/send-reminders-utils.jsx | 4 +- .../thread-snooze/lib/snooze-store.es6 | 2 - .../thread-sharing/package.json | 2 +- app/src/browser/application.es6 | 11 +- .../flux/attributes/attribute-collection.es6 | 9 +- app/src/flux/attributes/matcher.es6 | 3 +- app/src/flux/mailsync-bridge.es6 | 2 +- app/src/flux/models/model-with-metadata.es6 | 14 +- app/src/flux/models/query.es6 | 2 +- app/src/flux/models/utils.coffee | 3 - app/src/flux/nylas-api-request.es6 | 8 - app/src/flux/stores/account-store.es6 | 21 +-- app/src/flux/tasks/syncback-metadata-task.es6 | 5 + app/src/key-manager.es6 | 20 +-- app/src/mailsync-process.es6 | 2 - 32 files changed, 331 insertions(+), 357 deletions(-) rename app/internal_packages/onboarding/lib/{account-types.es6 => account-providers.es6} (83%) diff --git a/app/internal_packages/link-tracking/package.json b/app/internal_packages/link-tracking/package.json index 4073d7848..1560929a4 100644 --- a/app/internal_packages/link-tracking/package.json +++ b/app/internal_packages/link-tracking/package.json @@ -3,7 +3,7 @@ "main": "./lib/main", "version": "0.1.0", "serverUrl": { - "development": "http://localhost:5100", + "development": "http://localhost:5101", "staging": "https://link-staging.getmailspring.com", "production": "https://link.getmailspring.com" }, diff --git a/app/internal_packages/onboarding/lib/account-types.es6 b/app/internal_packages/onboarding/lib/account-providers.es6 similarity index 83% rename from app/internal_packages/onboarding/lib/account-types.es6 rename to app/internal_packages/onboarding/lib/account-providers.es6 index 7c25b0bf3..9ed4e053e 100644 --- a/app/internal_packages/onboarding/lib/account-types.es6 +++ b/app/internal_packages/onboarding/lib/account-providers.es6 @@ -1,34 +1,34 @@ -const AccountTypes = [ +const AccountProviders = [ { - type: 'gmail', + provider: 'gmail', displayName: 'Gmail or G Suite', icon: 'ic-settings-account-gmail.png', headerIcon: 'setup-icon-provider-gmail.png', color: '#e99999', }, { - type: 'office365', + provider: 'office365', displayName: 'Office 365', icon: 'ic-settings-account-outlook.png', headerIcon: 'setup-icon-provider-outlook.png', color: '#0078d7', }, { - type: 'yahoo', + provider: 'yahoo', displayName: 'Yahoo', icon: 'ic-settings-account-yahoo.png', headerIcon: 'setup-icon-provider-yahoo.png', color: '#a76ead', }, { - type: 'icloud', + provider: 'icloud', displayName: 'iCloud', icon: 'ic-settings-account-icloud.png', headerIcon: 'setup-icon-provider-icloud.png', color: '#61bfe9', }, { - type: 'fastmail', + provider: 'fastmail', displayName: 'FastMail', title: 'Set up your account', icon: 'ic-settings-account-fastmail.png', @@ -36,7 +36,7 @@ const AccountTypes = [ color: '#24345a', }, { - type: 'imap', + provider: 'imap', displayName: 'IMAP / SMTP', title: 'Set up your IMAP account', icon: 'ic-settings-account-imap.png', @@ -45,4 +45,4 @@ const AccountTypes = [ }, ] -export default AccountTypes; +export default AccountProviders; diff --git a/app/internal_packages/onboarding/lib/decorators/create-page-for-form.jsx b/app/internal_packages/onboarding/lib/decorators/create-page-for-form.jsx index 85bd6b2e0..fb124323d 100644 --- a/app/internal_packages/onboarding/lib/decorators/create-page-for-form.jsx +++ b/app/internal_packages/onboarding/lib/decorators/create-page-for-form.jsx @@ -5,26 +5,26 @@ import {RetinaImg} from 'nylas-component-kit'; import {NylasAPIRequest, Actions} from 'nylas-exports'; import OnboardingActions from '../onboarding-actions'; -import {buildAndValidateAccount} from '../onboarding-helpers'; +import {finalizeAndValidateAccount} from '../onboarding-helpers'; import FormErrorMessage from '../form-error-message'; -import AccountTypes from '../account-types' +import AccountProviders from '../account-providers' const CreatePageForForm = (FormComponent) => { return class Composed extends React.Component { static displayName = FormComponent.displayName; static propTypes = { - accountInfo: React.PropTypes.object, + account: React.PropTypes.object, }; constructor(props) { super(props); this.state = Object.assign({ - accountInfo: JSON.parse(JSON.stringify(this.props.accountInfo)), + account: this.props.account.clone(), errorFieldNames: [], errorMessage: null, - }, FormComponent.validateAccountInfo(this.props.accountInfo)); + }, FormComponent.validateAccount(this.props.account)); } componentDidMount() { @@ -61,24 +61,36 @@ const CreatePageForForm = (FormComponent) => { } onFieldChange = (event) => { - const changes = {}; + const next = this.state.account.clone(); + + let val = event.target.value; if (event.target.type === 'checkbox') { - changes[event.target.id] = event.target.checked; - } else { - changes[event.target.id] = event.target.value; - if (event.target.id === 'email') { - changes[event.target.id] = event.target.value.trim(); - } + val = event.target.checked; + } + if (event.target.id === 'emailAddress') { + val = val.trim(); } - const accountInfo = Object.assign({}, this.state.accountInfo, changes); - const {errorFieldNames, errorMessage, populated} = FormComponent.validateAccountInfo(accountInfo); + if (event.target.id.includes('.')) { + const [parent, key] = event.target.id.split('.'); + next[parent][key] = val; + } else { + next[event.target.id] = val; + } - this.setState({accountInfo, errorFieldNames, errorMessage, populated, errorStatusCode: null}); + const {errorFieldNames, errorMessage, populated} = FormComponent.validateAccount(next); + + this.setState({ + account: next, + errorFieldNames, + errorMessage, + populated, + errorStatusCode: null, + }); } onSubmit = () => { - OnboardingActions.setAccountInfo(this.state.accountInfo); + OnboardingActions.setAccount(this.state.account); this._formEl.submit(); } @@ -90,24 +102,24 @@ const CreatePageForForm = (FormComponent) => { } onBack = () => { - OnboardingActions.setAccountInfo(this.state.accountInfo); + OnboardingActions.setAccount(this.state.account); OnboardingActions.moveToPreviousPage(); } - onConnect = (updatedAccountInfo) => { - const accountInfo = updatedAccountInfo || this.state.accountInfo; + onConnect = (updatedAccount) => { + const account = updatedAccount || this.state.account; this.setState({submitting: true}); - buildAndValidateAccount(accountInfo) - .then(({account}) => { + finalizeAndValidateAccount(account) + .then((validated) => { OnboardingActions.moveToPage('account-onboarding-success') - OnboardingActions.accountJSONReceived(account) + OnboardingActions.finishAndAddAccount(validated) }) .catch((err) => { Actions.recordUserEvent('Email Account Auth Failed', { errorMessage: err.message, - provider: accountInfo.type, + provider: account.provider, }) const errorFieldNames = err.body ? (err.body.missing_fields || err.body.missing_settings || []) : [] @@ -117,7 +129,7 @@ const CreatePageForForm = (FormComponent) => { if (err.errorType === "setting_update_error") { errorMessage = 'The IMAP/SMTP servers for this account do not match our records. Please verify that any server names you entered are correct. If your IMAP/SMTP server has changed, first remove this account from Mailspring, then try logging in again.'; } - if (err.errorType && err.errorType.includes("autodiscover") && (accountInfo.type === 'exchange')) { + if (err.errorType && err.errorType.includes("autodiscover") && (account.provider === 'exchange')) { errorFieldNames.push('eas_server_host') errorFieldNames.push('username'); } @@ -144,8 +156,8 @@ const CreatePageForForm = (FormComponent) => { } _renderButton() { - const {accountInfo, submitting} = this.state; - const buttonLabel = FormComponent.submitLabel(accountInfo); + const {account, submitting} = this.state; + const buttonLabel = FormComponent.submitLabel(account); // We're not on the last page. if (submitting) { @@ -172,11 +184,11 @@ const CreatePageForForm = (FormComponent) => { // help with common problems. For instance, they may need an app password, // or to enable specific settings with their provider. _renderCredentialsNote() { - const {errorStatusCode, accountInfo} = this.state; + const {errorStatusCode, account} = this.state; if (errorStatusCode !== 401) { return false; } let message; let articleURL; - if (accountInfo.email.includes("@yahoo.com")) { + if (account.emailAddress.includes("@yahoo.com")) { message = "Have you enabled access through Yahoo?"; articleURL = "https://support.getmailspring.com/hc/en-us/articles/115001076128"; } else { @@ -200,11 +212,11 @@ const CreatePageForForm = (FormComponent) => { } render() { - const {accountInfo, errorMessage, errorFieldNames, submitting} = this.state; - const AccountType = AccountTypes.find(a => a.type === accountInfo.type); + const {account, errorMessage, errorFieldNames, submitting} = this.state; + const providerConfig = AccountProviders.find(({provider}) => provider === account.provider); - if (!AccountType) { - throw new Error(`Cannot find account type ${accountInfo.type}`); + if (!providerConfig) { + throw new Error(`Cannot find account provider ${account.provider}`); } const hideTitle = errorMessage && errorMessage.length > 120; @@ -213,21 +225,21 @@ const CreatePageForForm = (FormComponent) => {
- {hideTitle ?
:

{FormComponent.titleLabel(AccountType)}

} + {hideTitle ?
:

{FormComponent.titleLabel(providerConfig)}

} { this._renderCredentialsNote() } { this._formEl = el; }} - accountInfo={accountInfo} + account={account} errorFieldNames={errorFieldNames} submitting={submitting} onFieldChange={this.onFieldChange} diff --git a/app/internal_packages/onboarding/lib/form-field.jsx b/app/internal_packages/onboarding/lib/form-field.jsx index 2e187bf43..88d7fd271 100644 --- a/app/internal_packages/onboarding/lib/form-field.jsx +++ b/app/internal_packages/onboarding/lib/form-field.jsx @@ -1,6 +1,12 @@ import React from 'react'; const FormField = (props) => { + const field = props.field; + let val = props.account[field]; + if (props.field.includes('.')) { + const [parent, key] = props.field.split('.'); + val = props.account[parent][key]; + } return ( @@ -8,9 +14,10 @@ const FormField = (props) => { type={props.type || "text"} id={props.field} style={props.style} - className={(props.accountInfo[props.field] && props.errorFieldNames.includes(props.field)) ? 'error' : ''} + className={(val && props.errorFieldNames.includes(props.field)) ? 'error' : ''} disabled={props.submitting} - value={props.accountInfo[props.field] || ''} + spellCheck="false" + value={val || ''} onKeyPress={props.onFieldKeyPress} onChange={props.onFieldChange} /> @@ -27,7 +34,7 @@ FormField.propTypes = { onFieldKeyPress: React.PropTypes.func, onFieldChange: React.PropTypes.func, errorFieldNames: React.PropTypes.array, - accountInfo: React.PropTypes.object, + account: React.PropTypes.object, } export default FormField; diff --git a/app/internal_packages/onboarding/lib/onboarding-actions.es6 b/app/internal_packages/onboarding/lib/onboarding-actions.es6 index 33904dfdd..4def86098 100644 --- a/app/internal_packages/onboarding/lib/onboarding-actions.es6 +++ b/app/internal_packages/onboarding/lib/onboarding-actions.es6 @@ -1,12 +1,12 @@ import Reflux from 'reflux'; const OnboardingActions = Reflux.createActions([ - "setAccountInfo", - "setAccountType", "moveToPreviousPage", "moveToPage", + "setAccount", + "chooseAccountProvider", "identityJSONReceived", - "accountJSONReceived", + "finishAndAddAccount", ]); for (const key of Object.keys(OnboardingActions)) { diff --git a/app/internal_packages/onboarding/lib/onboarding-helpers.es6 b/app/internal_packages/onboarding/lib/onboarding-helpers.es6 index 291b43c87..52c2f9d2d 100644 --- a/app/internal_packages/onboarding/lib/onboarding-helpers.es6 +++ b/app/internal_packages/onboarding/lib/onboarding-helpers.es6 @@ -3,6 +3,7 @@ import crypto from 'crypto'; import {CommonProviderSettings} from 'imap-provider-settings'; import { + Account, NylasAPIRequest, IdentityStore, RegExpUtils, @@ -11,22 +12,7 @@ import { const {makeRequest, rootURLForServer} = NylasAPIRequest; -const IMAP_FIELDS = new Set([ - "imap_host", - "imap_port", - "imap_username", - "imap_password", - "imap_security", - "imap_allow_insecure_ssl", - "smtp_host", - "smtp_port", - "smtp_username", - "smtp_password", - "smtp_security", - "smtp_allow_insecure_ssl", -]); - -function base64url(inBuffer) { +function base64URL(inBuffer) { let buffer; if (typeof inBuffer === "string") { buffer = new Buffer(inBuffer); @@ -55,114 +41,92 @@ function idForAccount(emailAddress, connectionSettings) { return crypto.createHash('sha256').update(idString, 'utf8').digest('hex'); } -export function makeGmailOAuthRequest(sessionKey) { - return makeRequest({ - server: 'accounts', - path: `/auth/gmail/token?key=${sessionKey}`, - method: 'GET', - auth: false, - }); -} - -export async function authIMAPForGmail(serverTokenResponse) { - // At this point, the Mailspring server has retrieved the Gmail token, - // created an account object in the database and tested it. All we - // need to do is save it locally, since we're confident Gmail will be - // accessible from the local sync worker. - const {emailAddress, refreshToken} = serverTokenResponse; - const settings = expandAccountInfoWithCommonSettings({email: emailAddress, refreshToken, type: 'gmail'}); - - return { - id: idForAccount(emailAddress, settings), - provider: 'gmail', - name, - settings, - emailAddress, - }; -} - -export function buildGmailSessionKey() { - return base64url(crypto.randomBytes(40)); -} - -export function buildGmailAuthURL(sessionKey) { - return `${rootURLForServer('accounts')}/auth/gmail?state=${sessionKey}`; -} - -export async function buildAndValidateAccount(accountInfo) { - const {username, type, email, name} = accountInfo; - - const data = { - id: idForAccount(email, accountInfo), - provider: type, - name: name, - emailAddress: email, - settings: Object.assign({}, accountInfo), - }; - - // handle special case for exchange/outlook/hotmail username field - data.settings.username = username || email; - - if (data.settings.imap_port) { - data.settings.imap_port /= 1; - } - if (data.settings.smtp_port) { - data.settings.smtp_port /= 1; - } - - // Only include the required IMAP fields. Auth validation does not allow extra fields - if (type !== "gmail") { - for (const key of Object.keys(data.settings)) { - if (!IMAP_FIELDS.has(key)) { - delete data.settings[key]; - } - } - } - - // Test the account locally - if it succeeds, send it to the server and test it there - - const proc = new MailsyncProcess(NylasEnv.getLoadSettings(), IdentityStore.identity(), data); - const {account} = await proc.test(); - - return account; -} - -export function isValidHost(value) { - return RegExpUtils.domainRegex().test(value) || RegExpUtils.ipAddressRegex().test(value); -} - -export function expandAccountInfoWithCommonSettings(accountInfo) { - const {email, type} = accountInfo; - const domain = email.split('@').pop().toLowerCase(); - let template = CommonProviderSettings[domain] || CommonProviderSettings[type] || {}; +export function expandAccountWithCommonSettings(account) { + const domain = account.emailAddress.split('@').pop().toLowerCase(); + let template = CommonProviderSettings[domain] || CommonProviderSettings[account.provider] || {}; if (template.alias) { template = CommonProviderSettings[template.alias]; } const usernameWithFormat = (format) => { if (format === 'email') { - return email + return account.emailAddress } if (format === 'email-without-domain') { - return email.split('@').shift(); + return account.emailAddress.split('@').shift(); } return undefined; } - const defaults = { + const populated = account.clone(); + + populated.settings = Object.assign({ imap_host: template.imap_host, imap_port: template.imap_port || 993, imap_username: usernameWithFormat(template.imap_user_format), - imap_password: accountInfo.password, + imap_password: populated.settings.imap_password, imap_security: template.imap_security || "SSL / TLS", imap_allow_insecure_ssl: template.imap_allow_insecure_ssl || false, smtp_host: template.smtp_host, smtp_port: template.smtp_port || 587, smtp_username: usernameWithFormat(template.smtp_user_format), - smtp_password: accountInfo.password, + smtp_password: populated.settings.smtp_password || populated.settings.imap_password, smtp_security: template.smtp_security || "STARTTLS", smtp_allow_insecure_ssl: template.smtp_allow_insecure_ssl || false, + }, populated.settings); + + return populated; +} + +export function makeGmailOAuthRequest(sessionKey) { + return makeRequest({ + server: 'identity', + path: `/auth/gmail/token?key=${sessionKey}`, + method: 'GET', + auth: false, + }); +} + +export async function buildGmailAccountFromToken(serverTokenResponse) { + // At this point, the Mailspring server has retrieved the Gmail token, + // created an account object in the database and tested it. All we + // need to do is save it locally, since we're confident Gmail will be + // accessible from the local sync worker. + const {emailAddress, refreshToken} = serverTokenResponse; + const account = new Account(); + account.emailAddress = emailAddress; + account.provider = 'gmail'; + account.settings.refresh_token = refreshToken; + return expandAccountWithCommonSettings(account); +} + +export function buildGmailSessionKey() { + return base64URL(crypto.randomBytes(40)); +} + +export function buildGmailAuthURL(sessionKey) { + return `${rootURLForServer('identity')}/auth/gmail?state=${sessionKey}`; +} + +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; + + if (account.settings.imap_port) { + account.settings.imap_port /= 1; + } + if (account.settings.smtp_port) { + account.settings.smtp_port /= 1; } - return Object.assign({}, accountInfo, defaults); + // Test connections to IMAP and SMTP + const proc = new MailsyncProcess(NylasEnv.getLoadSettings(), IdentityStore.identity(), account); + const response = await proc.test(); + return new Account(response.account); +} + +export function isValidHost(value) { + return RegExpUtils.domainRegex().test(value) || RegExpUtils.ipAddressRegex().test(value); } diff --git a/app/internal_packages/onboarding/lib/onboarding-root.jsx b/app/internal_packages/onboarding/lib/onboarding-root.jsx index 5a0266292..9b9ba8a9e 100644 --- a/app/internal_packages/onboarding/lib/onboarding-root.jsx +++ b/app/internal_packages/onboarding/lib/onboarding-root.jsx @@ -53,7 +53,7 @@ export default class OnboardingRoot extends React.Component { return { page: OnboardingStore.page(), pageDepth: OnboardingStore.pageDepth(), - accountInfo: OnboardingStore.accountInfo(), + account: OnboardingStore.account(), }; } @@ -79,7 +79,7 @@ export default class OnboardingRoot extends React.Component { transitionEnterTimeout={150} >
- +
diff --git a/app/internal_packages/onboarding/lib/onboarding-store.es6 b/app/internal_packages/onboarding/lib/onboarding-store.es6 index be8729e7b..8bc497a1f 100644 --- a/app/internal_packages/onboarding/lib/onboarding-store.es6 +++ b/app/internal_packages/onboarding/lib/onboarding-store.es6 @@ -1,65 +1,51 @@ -import {AccountStore, Actions, IdentityStore} from 'nylas-exports'; +import {AccountStore, Account, Actions, IdentityStore} from 'nylas-exports'; import {ipcRenderer} from 'electron'; import NylasStore from 'nylas-store'; import OnboardingActions from './onboarding-actions'; -function accountTypeForProvider(provider) { - if (provider === 'eas') { - return 'exchange'; - } - if (provider === 'custom') { - return 'imap'; - } - return provider; -} - class OnboardingStore extends NylasStore { constructor() { super(); this.listenTo(OnboardingActions.moveToPreviousPage, this._onMoveToPreviousPage) this.listenTo(OnboardingActions.moveToPage, this._onMoveToPage) - this.listenTo(OnboardingActions.accountJSONReceived, this._onAccountJSONReceived) + this.listenTo(OnboardingActions.setAccount, this._onSetAccount); + this.listenTo(OnboardingActions.chooseAccountProvider, this._onChooseAccountProvider); + this.listenTo(OnboardingActions.finishAndAddAccount, this._onFinishAndAddAccount) this.listenTo(OnboardingActions.identityJSONReceived, this._onIdentityJSONReceived) - this.listenTo(OnboardingActions.setAccountInfo, this._onSetAccountInfo); - this.listenTo(OnboardingActions.setAccountType, this._onSetAccountType); - ipcRenderer.on('set-account-type', (e, type) => { - if (type) { - this._onSetAccountType(type) + + ipcRenderer.on('set-account-provider', (e, provider) => { + if (provider) { + this._onChooseAccountProvider(provider) } else { this._pageStack = ['account-choose'] this.trigger() } }) - const {existingAccount, addingAccount, accountType} = NylasEnv.getWindowProps(); + const {existingAccount, addingAccount, accountProvider} = NylasEnv.getWindowProps(); const hasAccounts = (AccountStore.accounts().length > 0) const identity = IdentityStore.identity(); - if (identity) { - this._accountInfo = { - name: `${identity.firstName || ""} ${identity.lastName || ""}`, - }; - } else { - this._accountInfo = {}; - } + this._account = new Account({ + name: identity ? `${identity.firstName || ""} ${identity.lastName || ""}` : '', + emailAddress: identity ? identity.emailAddress : '', + settings: {}, + }); if (existingAccount) { // Used when re-adding an account after re-connecting - const existingAccountType = accountTypeForProvider(existingAccount.provider); - this._pageStack = ['account-choose'] - this._accountInfo = { - name: existingAccount.name, - email: existingAccount.emailAddress, - }; - this._onSetAccountType(existingAccountType); + this._pageStack = ['account-choose']; + this._account.name = existingAccount.name; + this._account.emailAddress = existingAccount.emailAddress; + this._onChooseAccountProvider(existingAccount.provider); } else if (addingAccount) { // Adding a new, unknown account this._pageStack = ['account-choose']; - if (accountType) { - this._onSetAccountType(accountType); + if (accountProvider) { + this._onChooseAccountProvider(accountProvider); } } else if (identity) { // Should only happen if config was edited to remove all accounts, @@ -88,26 +74,34 @@ class OnboardingStore extends NylasStore { }, 100); } - _onSetAccountType = (type) => { + _onChooseAccountProvider = (provider) => { let nextPage = "account-settings"; - if (type === 'gmail') { + if (provider === 'gmail') { nextPage = "account-settings-gmail"; - } else if (type === 'exchange') { + } else if (provider === 'exchange') { nextPage = "account-settings-exchange"; } - Actions.recordUserEvent('Selected Account Type', { - provider: type, + Actions.recordUserEvent('Selected Account Provider', { + provider, }); // Don't carry over any type-specific account information - const {email, name, password} = this._accountInfo; - this._onSetAccountInfo({email, name, password, type}); + this._onSetAccount(new Account({ + emailAddress: this._account.emailAddress, + name: this._account.name, + settings: {}, + provider, + })); + this._onMoveToPage(nextPage); } - _onSetAccountInfo = (info) => { - this._accountInfo = info; + _onSetAccount = (acct) => { + if (!(acct instanceof Account)) { + throw new Error("OnboardingActions.setAccount expects an Account instance."); + } + this._account = acct; this.trigger(); } @@ -128,10 +122,10 @@ class OnboardingStore extends NylasStore { setTimeout(() => { if (isFirstAccount) { - this._onSetAccountInfo(Object.assign({}, this._accountInfo, { - name: `${json.firstName || ""} ${json.lastName || ""}`, - email: json.emailAddress, - })); + const next = this._account.clone(); + next.name = `${json.firstName || ""} ${json.lastName || ""}`; + next.emailAddress = json.emailAddress; + this._onSetAccount(next); OnboardingActions.moveToPage('account-choose'); } else { this._onOnboardingComplete(); @@ -139,22 +133,21 @@ class OnboardingStore extends NylasStore { }, 1000); } - _onAccountJSONReceived = async (json) => { + _onFinishAndAddAccount = async (account) => { try { const isFirstAccount = AccountStore.accounts().length === 0; - AccountStore.addAccountFromJSON(json); + + AccountStore.addAccount(account); + NylasEnv.displayWindow(); Actions.recordUserEvent('Email Account Auth Succeeded', { - provider: json.provider, + provider: account.provider, }); - ipcRenderer.send('new-account-added'); - NylasEnv.displayWindow(); - if (isFirstAccount) { this._onMoveToPage('initial-preferences'); Actions.recordUserEvent('First Account Linked', { - provider: json.provider, + provider: account.provider, }); } else { // let them see the "success" screen for a moment @@ -177,8 +170,8 @@ class OnboardingStore extends NylasStore { return this._pageStack.length; } - accountInfo() { - return this._accountInfo; + account() { + return this._account; } } diff --git a/app/internal_packages/onboarding/lib/page-account-choose.jsx b/app/internal_packages/onboarding/lib/page-account-choose.jsx index b4326ffe0..556db6cff 100644 --- a/app/internal_packages/onboarding/lib/page-account-choose.jsx +++ b/app/internal_packages/onboarding/lib/page-account-choose.jsx @@ -1,30 +1,30 @@ import React from 'react'; import {RetinaImg} from 'nylas-component-kit'; import OnboardingActions from './onboarding-actions'; -import AccountTypes from './account-types'; +import AccountProviders from './account-providers'; export default class AccountChoosePage extends React.Component { static displayName = "AccountChoosePage"; static propTypes = { - accountInfo: React.PropTypes.object, + account: React.PropTypes.object, } - _renderAccountTypes() { - return AccountTypes.map((accountType) => + _renderProviders() { + return AccountProviders.map(({icon, displayName, provider}) =>
OnboardingActions.setAccountType(accountType.type)} + key={provider} + className={`provider ${provider}`} + onClick={() => OnboardingActions.chooseAccountProvider(provider)} >
- {accountType.displayName} + {displayName}
); } @@ -36,7 +36,7 @@ export default class AccountChoosePage extends React.Component { Connect an email account
- {this._renderAccountTypes()} + {this._renderProviders()}
); diff --git a/app/internal_packages/onboarding/lib/page-account-onboarding-success.jsx b/app/internal_packages/onboarding/lib/page-account-onboarding-success.jsx index a034ad484..ff633b9c4 100644 --- a/app/internal_packages/onboarding/lib/page-account-onboarding-success.jsx +++ b/app/internal_packages/onboarding/lib/page-account-onboarding-success.jsx @@ -1,31 +1,32 @@ import React, {Component} from 'react'; import PropTypes from 'prop-types' import {RetinaImg} from 'nylas-component-kit'; -import AccountTypes from './account-types' +import AccountProviders from './account-providers' class AccountOnboardingSuccess extends Component { // eslint-disable-line static displayName = 'AccountOnboardingSuccess' static propTypes = { - accountInfo: PropTypes.object, + account: PropTypes.object, } render() { - const {accountInfo} = this.props - const accountType = AccountTypes.find(a => a.type === accountInfo.type); + const {account} = this.props; + const providerConfig = AccountProviders.find(({provider}) => provider === account.provider); + return (
-

Successfully connected to {accountType.displayName}!

+

Successfully connected to {providerConfig.displayName}!

Adding your account to Mailspring…

diff --git a/app/internal_packages/onboarding/lib/page-account-settings-exchange.jsx b/app/internal_packages/onboarding/lib/page-account-settings-exchange.jsx index 5a02d2780..be170306b 100644 --- a/app/internal_packages/onboarding/lib/page-account-settings-exchange.jsx +++ b/app/internal_packages/onboarding/lib/page-account-settings-exchange.jsx @@ -8,7 +8,7 @@ class AccountExchangeSettingsForm extends React.Component { static displayName = 'AccountExchangeSettingsForm'; static propTypes = { - accountInfo: React.PropTypes.object, + account: React.PropTypes.object, errorFieldNames: React.PropTypes.array, submitting: React.PropTypes.bool, onConnect: React.PropTypes.func, @@ -28,28 +28,28 @@ class AccountExchangeSettingsForm extends React.Component { return 'Enter your Exchange credentials to get started.'; } - static validateAccountInfo = (accountInfo) => { - const {email, password, name} = accountInfo; + static validateAccount = (account) => { + const {emailAddress, password, name} = account; const errorFieldNames = []; let errorMessage = null; - if (!email || !password || !name) { + if (!emailAddress || !password || !name) { return {errorMessage, errorFieldNames, populated: false}; } - if (!RegExpUtils.emailRegex().test(accountInfo.email)) { + if (!RegExpUtils.emailRegex().test(emailAddress)) { errorFieldNames.push('email') errorMessage = "Please provide a valid email address." } - if (!accountInfo.password) { + if (!account.settings.password) { errorFieldNames.push('password') errorMessage = "Please provide a password for your account." } - if (!accountInfo.name) { + if (!account.name) { errorFieldNames.push('name') errorMessage = "Please provide your name." } - if (accountInfo.eas_server_host && !isValidHost(accountInfo.eas_server_host)) { + if (account.settings.eas_server_host && !isValidHost(account.settings.eas_server_host)) { errorFieldNames.push('eas_server_host') errorMessage = "Please provide a valid host name." } @@ -67,13 +67,13 @@ class AccountExchangeSettingsForm extends React.Component { } render() { - const {errorFieldNames, accountInfo} = this.props; + const {errorFieldNames, account} = this.props; const showAdvanced = ( this.state.showAdvanced || errorFieldNames.includes('eas_server_host') || errorFieldNames.includes('username') || - accountInfo.eas_server_host || - accountInfo.username + account.eas_server_host || + account.username ); let classnames = "twocol"; diff --git a/app/internal_packages/onboarding/lib/page-account-settings-gmail.jsx b/app/internal_packages/onboarding/lib/page-account-settings-gmail.jsx index f5ecf3048..4765688fd 100644 --- a/app/internal_packages/onboarding/lib/page-account-settings-gmail.jsx +++ b/app/internal_packages/onboarding/lib/page-account-settings-gmail.jsx @@ -3,20 +3,20 @@ import {OAuthSignInPage} from 'nylas-component-kit'; import { makeGmailOAuthRequest, - authIMAPForGmail, + buildGmailAccountFromToken, buildGmailSessionKey, buildGmailAuthURL, } from './onboarding-helpers'; import OnboardingActions from './onboarding-actions'; -import AccountTypes from './account-types'; +import AccountProviders from './account-providers'; export default class AccountSettingsPageGmail extends React.Component { static displayName = "AccountSettingsPageGmail"; static propTypes = { - accountInfo: React.PropTypes.object, + account: React.PropTypes.object, }; constructor() { @@ -30,9 +30,10 @@ export default class AccountSettingsPageGmail extends React.Component { } render() { - const {accountInfo} = this.props; - const accountType = AccountTypes.find(a => a.type === accountInfo.type) - const {headerIcon} = accountType; + const providerConfig = AccountProviders.find(a => + a.provider === this.props.account.provider + ) + const {headerIcon} = providerConfig; const goBack = () => OnboardingActions.moveToPreviousPage() return ( @@ -41,7 +42,7 @@ export default class AccountSettingsPageGmail extends React.Component { providerAuthPageUrl={this._gmailAuthUrl} iconName={headerIcon} tokenRequestPollFn={makeGmailOAuthRequest} - accountFromTokenFn={authIMAPForGmail} + accountFromTokenFn={buildGmailAccountFromToken} onSuccess={this.onSuccess} onTryAgain={goBack} sessionKey={this._sessionKey} diff --git a/app/internal_packages/onboarding/lib/page-account-settings-imap.jsx b/app/internal_packages/onboarding/lib/page-account-settings-imap.jsx index abe670fa0..22be155cc 100644 --- a/app/internal_packages/onboarding/lib/page-account-settings-imap.jsx +++ b/app/internal_packages/onboarding/lib/page-account-settings-imap.jsx @@ -7,7 +7,7 @@ class AccountIMAPSettingsForm extends React.Component { static displayName = 'AccountIMAPSettingsForm'; static propTypes = { - accountInfo: React.PropTypes.object, + account: React.PropTypes.object, errorFieldNames: React.PropTypes.array, submitting: React.PropTypes.bool, onConnect: React.PropTypes.func, @@ -27,24 +27,19 @@ class AccountIMAPSettingsForm extends React.Component { return 'Complete the IMAP and SMTP settings below to connect your account.'; } - static validateAccountInfo = (accountInfo) => { + static validateAccount = (account) => { let errorMessage = null; const errorFieldNames = []; for (const type of ['imap', 'smtp']) { - if (!accountInfo[`${type}_host`] || !accountInfo[`${type}_username`] || !accountInfo[`${type}_password`]) { + if (!account.settings[`${type}_host`] || !account.settings[`${type}_username`] || !account.settings[`${type}_password`]) { return {errorMessage, errorFieldNames, populated: false}; } - if (!isValidHost(accountInfo[`${type}_host`])) { + if (!isValidHost(account.settings[`${type}_host`])) { errorMessage = "Please provide a valid hostname or IP adddress."; errorFieldNames.push(`${type}_host`); } - // todo bg - // if (accountInfo[`${type}_host`] === 'imap.gmail.com') { - // errorMessage = "Please link Gmail accounts by choosing 'Google' on the account type screen."; - // errorFieldNames.push(`${type}_host`); - // } - if (!Number.isInteger(accountInfo[`${type}_port`] / 1)) { + if (!Number.isInteger(account.settings[`${type}_port`] / 1)) { errorMessage = "Please provide a valid port number."; errorFieldNames.push(`${type}_port`); } @@ -61,16 +56,16 @@ class AccountIMAPSettingsForm extends React.Component { if (!["imap", "smtp"].includes(protocol)) { throw new Error(`Can't render port dropdown for protocol '${protocol}'`); } - const {accountInfo, submitting, onFieldKeyPress, onFieldChange} = this.props; + const {account: {settings}, submitting, onFieldKeyPress, onFieldChange} = this.props; if (protocol === "imap") { return ( - + - + @@ -141,13 +138,13 @@ class AccountIMAPSettingsForm extends React.Component { renderFieldsForType(type) { return (
- +
{this.renderPortDropdown(type)} {this.renderSecurityDropdown(type)}
- - + +
); } diff --git a/app/internal_packages/onboarding/lib/page-account-settings.jsx b/app/internal_packages/onboarding/lib/page-account-settings.jsx index 4982db858..ec37f2d5a 100644 --- a/app/internal_packages/onboarding/lib/page-account-settings.jsx +++ b/app/internal_packages/onboarding/lib/page-account-settings.jsx @@ -3,14 +3,14 @@ import {RegExpUtils} from 'nylas-exports'; import OnboardingActions from './onboarding-actions'; import CreatePageForForm from './decorators/create-page-for-form'; -import {expandAccountInfoWithCommonSettings} from './onboarding-helpers'; +import {expandAccountWithCommonSettings} from './onboarding-helpers'; import FormField from './form-field'; class AccountBasicSettingsForm extends React.Component { static displayName = 'AccountBasicSettingsForm'; static propTypes = { - accountInfo: React.PropTypes.object, + account: React.PropTypes.object, errorFieldNames: React.PropTypes.array, submitting: React.PropTypes.bool, onConnect: React.PropTypes.func, @@ -18,53 +18,52 @@ class AccountBasicSettingsForm extends React.Component { onFieldKeyPress: React.PropTypes.func, }; - static submitLabel = (accountInfo) => { - return (accountInfo.type === 'imap') ? 'Continue' : 'Connect Account'; + static submitLabel = (account) => { + return (account.provider === 'imap') ? 'Continue' : 'Connect Account'; } - static titleLabel = (AccountType) => { - return AccountType.title || `Add your ${AccountType.displayName} account`; + static titleLabel = (providerConfig) => { + return providerConfig.title || `Add your ${providerConfig.displayName} account`; } static subtitleLabel = () => { - return 'Enter your email account credentials to get started.'; + return `Enter your email account credentials to get started. Mailspring\nstores your email password securely and it is never sent to our servers.`; } - static validateAccountInfo = (accountInfo) => { - const {email, password, name} = accountInfo; + static validateAccount = (account) => { const errorFieldNames = []; let errorMessage = null; - if (!email || !password || !name) { + if (!account.emailAddress || !account.settings.imap_password || !account.name) { return {errorMessage, errorFieldNames, populated: false}; } - if (!RegExpUtils.emailRegex().test(accountInfo.email)) { + if (!RegExpUtils.emailRegex().test(account.emailAddress)) { errorFieldNames.push('email') errorMessage = "Please provide a valid email address." } - if (!accountInfo.password) { - errorFieldNames.push('password') - errorMessage = "Please provide a password for your account." - } - if (!accountInfo.name) { + if (!account.name) { errorFieldNames.push('name') errorMessage = "Please provide your name." } + if (!account.settings.imap_password) { + errorFieldNames.push('password') + errorMessage = "Please provide a password for your account." + } return {errorMessage, errorFieldNames, populated: true}; } submit() { - const accountInfo = expandAccountInfoWithCommonSettings(this.props.accountInfo); - OnboardingActions.setAccountInfo(accountInfo); - if (this.props.accountInfo.type === 'imap') { + const account = expandAccountWithCommonSettings(this.props.account); + OnboardingActions.setAccount(account); + if (this.props.account.provider === 'imap') { OnboardingActions.moveToPage('account-settings-imap'); } else { - // We have to pass in the updated accountInfo, because the onConnect() + // We have to pass in the updated account, because the onConnect() // we're calling exists on a component that won't have had it's state // updated from the OnboardingStore change yet. - this.props.onConnect(accountInfo); + this.props.onConnect(account); } } @@ -72,8 +71,8 @@ class AccountBasicSettingsForm extends React.Component { return (
- - + + ) } diff --git a/app/internal_packages/onboarding/lib/page-authenticate.jsx b/app/internal_packages/onboarding/lib/page-authenticate.jsx index f4ff34e65..f0075a16f 100644 --- a/app/internal_packages/onboarding/lib/page-authenticate.jsx +++ b/app/internal_packages/onboarding/lib/page-authenticate.jsx @@ -7,7 +7,7 @@ export default class AuthenticatePage extends React.Component { static displayName = "AuthenticatePage"; static propTypes = { - accountInfo: React.PropTypes.object, + account: React.PropTypes.object, }; _src() { diff --git a/app/internal_packages/onboarding/styles/onboarding.less b/app/internal_packages/onboarding/styles/onboarding.less index 4244f91ce..de39eab73 100644 --- a/app/internal_packages/onboarding/styles/onboarding.less +++ b/app/internal_packages/onboarding/styles/onboarding.less @@ -100,6 +100,7 @@ margin-bottom:15px; max-width: 600px; margin: auto; + white-space: pre-wrap; &.error { color: #A33; @@ -116,6 +117,9 @@ form.settings { padding: 0 20px; padding-bottom: 20px; + span:last-child input { + margin-bottom:0; + } } input { display: inline-block; @@ -169,10 +173,6 @@ text-align: right; padding: 0; } - - .btn { - margin-top:8px; - } } .page.authenticate { @@ -339,7 +339,7 @@ } .twocol { padding-top: 20px; - padding-bottom: 10px; + padding-bottom: 20px; } } .page.account-setup.google, .page.account-setup.AccountOnboardingSuccess { diff --git a/app/internal_packages/open-tracking/package.json b/app/internal_packages/open-tracking/package.json index 19ed5aeac..cb64e7d83 100644 --- a/app/internal_packages/open-tracking/package.json +++ b/app/internal_packages/open-tracking/package.json @@ -3,7 +3,7 @@ "main": "./lib/main", "version": "0.1.0", "serverUrl": { - "development": "http://localhost:5100", + "development": "http://localhost:5101", "staging": "https://link-staging.getmailspring.com", "production": "https://link.getmailspring.com" }, diff --git a/app/internal_packages/send-reminders/lib/send-reminders-utils.jsx b/app/internal_packages/send-reminders/lib/send-reminders-utils.jsx index 8b0c8a7d8..61210f9ef 100644 --- a/app/internal_packages/send-reminders/lib/send-reminders-utils.jsx +++ b/app/internal_packages/send-reminders/lib/send-reminders-utils.jsx @@ -194,12 +194,12 @@ export function getLatestMessageWithReminder(thread, messages) { } export function getReminderLabel(reminderDate, {fromNow = false, shortFormat = false} = {}) { - const momentDate = DateUtils.futureDateFromString(reminderDate); + const momentDate = moment(reminderDate); if (shortFormat) { return momentDate ? `in ${momentDate.fromNow(true)}` : 'now' } if (fromNow) { return momentDate ? `Reminder set for ${momentDate.fromNow(true)} from now` : `Reminder set`; } - return moment(reminderDate).format(DATE_FORMAT_LONG_NO_YEAR) + return momentDate.format(DATE_FORMAT_LONG_NO_YEAR) } diff --git a/app/internal_packages/thread-snooze/lib/snooze-store.es6 b/app/internal_packages/thread-snooze/lib/snooze-store.es6 index 50e768071..9b2026d43 100644 --- a/app/internal_packages/thread-snooze/lib/snooze-store.es6 +++ b/app/internal_packages/thread-snooze/lib/snooze-store.es6 @@ -1,4 +1,3 @@ -import _ from 'underscore'; import NylasStore from 'nylas-store'; import { @@ -7,7 +6,6 @@ import { Actions, DatabaseStore, Message, - CategoryStore, } from 'nylas-exports'; import SnoozeUtils from './snooze-utils' diff --git a/app/internal_packages_disabled/thread-sharing/package.json b/app/internal_packages_disabled/thread-sharing/package.json index 53c6cd7e0..bb378105e 100644 --- a/app/internal_packages_disabled/thread-sharing/package.json +++ b/app/internal_packages_disabled/thread-sharing/package.json @@ -2,7 +2,7 @@ "name": "thread-sharing", "version": "0.1.0", "serverUrl": { - "development": "http://localhost:5100", + "development": "http://localhost:5101", "staging": "https://share-staging.getmailspring.com", "production": "https://share.getmailspring.com" }, diff --git a/app/src/browser/application.es6 b/app/src/browser/application.es6 index 6991b3218..897da8f1c 100644 --- a/app/src/browser/application.es6 +++ b/app/src/browser/application.es6 @@ -288,18 +288,18 @@ export default class Application extends EventEmitter { win.browserWindow.inspectElement(x, y); }); - this.on('application:add-account', ({existingAccount, accountType} = {}) => { + this.on('application:add-account', ({existingAccount, accountProvider} = {}) => { const onboarding = this.windowManager.get(WindowManager.ONBOARDING_WINDOW); if (onboarding) { if (onboarding.browserWindow.webContents) { - onboarding.browserWindow.webContents.send('set-account-type', accountType) + onboarding.browserWindow.webContents.send('set-account-provider', accountProvider) } onboarding.show(); onboarding.focus(); } else { this.windowManager.ensureWindow(WindowManager.ONBOARDING_WINDOW, { title: "Add an Account", - windowProps: { addingAccount: true, existingAccount, accountType }, + windowProps: { addingAccount: true, existingAccount, accountProvider }, }); } }); @@ -526,11 +526,6 @@ export default class Application extends EventEmitter { } }); - ipcMain.on('new-account-added', () => { - // TODO BEN - // this.windowManager.ensureWindow(WindowManager.WORK_WINDOW) - }); - ipcMain.on('run-in-window', (event, params) => { const sourceWindow = BrowserWindow.fromWebContents(event.sender); this._sourceWindows = this._sourceWindows || {}; diff --git a/app/src/flux/attributes/attribute-collection.es6 b/app/src/flux/attributes/attribute-collection.es6 index 59c6f6cd6..06f1fd753 100644 --- a/app/src/flux/attributes/attribute-collection.es6 +++ b/app/src/flux/attributes/attribute-collection.es6 @@ -30,10 +30,11 @@ The value of this attribute is always an array of other model objects. Section: Database */ export default class AttributeCollection extends Attribute { - constructor({modelKey, jsonKey, itemClass, joinOnField, joinQueryableBy, queryable}) { + constructor({modelKey, jsonKey, itemClass, joinOnField, joinQueryableBy, joinTableName, queryable}) { super({modelKey, jsonKey, queryable}); this.itemClass = itemClass; this.joinOnField = joinOnField; + this.joinTableName = joinTableName; this.joinQueryableBy = joinQueryableBy || []; } @@ -68,6 +69,12 @@ export default class AttributeCollection extends Attribute { }); } + // Private: The Matcher interface uses this method to determine how to + // constuct a SQL join: + tableNameForJoinAgainst(primaryKlass) { + return this.joinTableName || `${primaryKlass.name}${this.itemClass.name}`; + } + // Public: Returns a {Matcher} for objects containing the provided value. contains(val) { this._assertPresentAndQueryable('contains', val); diff --git a/app/src/flux/attributes/matcher.es6 b/app/src/flux/attributes/matcher.es6 index 11788a213..7c83cb7f3 100644 --- a/app/src/flux/attributes/matcher.es6 +++ b/app/src/flux/attributes/matcher.es6 @@ -1,4 +1,3 @@ -import {tableNameForJoin} from '../models/utils'; import LocalSearchQueryBackend from '../../services/search/search-query-backend-local' // https://www.sqlite.org/faq.html#q14 @@ -115,7 +114,7 @@ class Matcher { switch (this.comparator) { case 'contains': case 'containsAny': { - const joinTable = tableNameForJoin(klass, this.attr.itemClass); + const joinTable = this.attr.tableNameForJoinAgainst(klass); const joinTableRef = this.joinTableRef(); return `INNER JOIN \`${joinTable}\` AS \`${joinTableRef}\` ON \`${joinTableRef}\`.\`id\` = \`${klass.name}\`.\`id\``; } diff --git a/app/src/flux/mailsync-bridge.es6 b/app/src/flux/mailsync-bridge.es6 index 76fc87fde..46863775a 100644 --- a/app/src/flux/mailsync-bridge.es6 +++ b/app/src/flux/mailsync-bridge.es6 @@ -191,7 +191,7 @@ export default class MailsyncBridge { // Private _launchClient(account, {force} = {}) { - const fullAccountJSON = KeyManager.insertAccountSecrets(account.toJSON()); + const fullAccountJSON = KeyManager.insertAccountSecrets(account).toJSON(); const identity = IdentityStore.identity(); const id = account.id; diff --git a/app/src/flux/models/model-with-metadata.es6 b/app/src/flux/models/model-with-metadata.es6 index 021145b84..778c6af31 100644 --- a/app/src/flux/models/model-with-metadata.es6 +++ b/app/src/flux/models/model-with-metadata.es6 @@ -47,8 +47,9 @@ export default class ModelWithMetadata extends Model { static attributes = Object.assign({}, Model.attributes, { pluginMetadata: Attributes.Collection({ queryable: true, - joinOnField: 'pluginId', itemClass: PluginMetadata, + joinOnField: 'pluginId', + joinTableName: 'ModelPluginMetadata', modelKey: 'pluginMetadata', jsonKey: 'metadata', }), @@ -70,7 +71,11 @@ export default class ModelWithMetadata extends Model { if (!metadata) { return null; } - return JSON.parse(JSON.stringify(metadata.value)); + const m = JSON.parse(JSON.stringify(metadata.value)); + if (m.expiration) { + m.expiration = new Date(m.expiration * 1000); + } + return m; } // Private helpers @@ -88,7 +93,10 @@ export default class ModelWithMetadata extends Model { metadata = new PluginMetadata({pluginId}); this.pluginMetadata.push(metadata); } - metadata.value = metadataValue; + metadata.value = Object.assign({}, metadataValue); + if (metadata.value.expiration) { + metadata.value.expiration = Math.round(new Date(metadata.value.expiration).getTime() / 1000); + } return this; } diff --git a/app/src/flux/models/query.es6 b/app/src/flux/models/query.es6 index 0f8589828..b6cbad246 100644 --- a/app/src/flux/models/query.es6 +++ b/app/src/flux/models/query.es6 @@ -409,7 +409,7 @@ export default class ModelQuery { _subselectSQL(returningMatcher, subselectMatchers, order, limit) { const returningAttribute = returningMatcher.attribute() - const table = Utils.tableNameForJoin(this._klass, returningAttribute.itemClass); + const table = returningAttribute.tableNameForJoinAgainst(this._klass); const wheres = subselectMatchers.map(c => c.whereSQL(this._klass)).filter(c => !!c); let innerSQL = `SELECT \`id\` FROM \`${table}\` WHERE ${wheres.join(' AND ')} ${order} ${limit}`; diff --git a/app/src/flux/models/utils.coffee b/app/src/flux/models/utils.coffee index d501d1a0a..ea27d4d04 100644 --- a/app/src/flux/models/utils.coffee +++ b/app/src/flux/models/utils.coffee @@ -159,9 +159,6 @@ Utils = return false unless id and _.isString(id) id[0..5] is 'local-' - tableNameForJoin: (primaryKlass, secondaryKlass) -> - "#{primaryKlass.name}#{secondaryKlass.name}" - imageNamed: (fullname, resourcePath) -> [name, ext] = fullname.split('.') diff --git a/app/src/flux/nylas-api-request.es6 b/app/src/flux/nylas-api-request.es6 index c1df8b20d..0fd59b128 100644 --- a/app/src/flux/nylas-api-request.es6 +++ b/app/src/flux/nylas-api-request.es6 @@ -25,14 +25,6 @@ export function rootURLForServer(server) { production: "https://id.getmailspring.com", }[env]; } - if (server === 'accounts') { - return { - development: "http://localhost:5100", - staging: "https://accounts-staging.getmailspring.com", - production: "https://accounts.getmailspring.com", - }[env]; - } - throw new Error("rootURLForServer: You must provide a valid `server` value"); } diff --git a/app/src/flux/stores/account-store.es6 b/app/src/flux/stores/account-store.es6 index 677d5d237..324daf4c5 100644 --- a/app/src/flux/stores/account-store.es6 +++ b/app/src/flux/stores/account-store.es6 @@ -203,28 +203,29 @@ class AccountStore extends NylasStore { this._save() } - addAccountFromJSON = (json) => { - if (!json.emailAddress || !json.provider) { - throw new Error(`Returned account data is invalid: ${JSON.stringify(json)}`) + addAccount = (account) => { + if (!account.emailAddress || !account.provider || !(account instanceof Account)) { + throw new Error(`Returned account data is invalid: ${JSON.stringify(account)}`) } // send the account JSON and cloud token to the KeyManager, // which gives us back a version with no secrets. - const cleanJSON = KeyManager.extractAccountSecrets(json); + const cleanAccount = KeyManager.extractAccountSecrets(account); this._loadAccounts(); const existingIdx = this._accounts.findIndex((a) => - a.id === cleanJSON.id || a.emailAddress === cleanJSON.emailAddress + a.id === cleanAccount.id || a.emailAddress === cleanAccount.emailAddress ); if (existingIdx === -1) { - const account = (new Account()).fromJSON(cleanJSON); - this._accounts.push(account); + this._accounts.push(cleanAccount); } else { - const account = this._accounts[existingIdx]; - account.syncState = Account.SYNC_STATE_OK; - account.fromJSON(cleanJSON); + const existing = this._accounts[existingIdx]; + existing.syncState = Account.SYNC_STATE_OK; + existing.name = cleanAccount.name; + existing.emailAddress = cleanAccount.emailAddress; + existing.settings = cleanAccount.settings; } this._save(); diff --git a/app/src/flux/tasks/syncback-metadata-task.es6 b/app/src/flux/tasks/syncback-metadata-task.es6 index c3937b1ce..5e588cbe7 100644 --- a/app/src/flux/tasks/syncback-metadata-task.es6 +++ b/app/src/flux/tasks/syncback-metadata-task.es6 @@ -23,6 +23,11 @@ export default class SyncbackMetadataTask extends Task { constructor(data = {}) { super(data); + + if (data.value && data.value.expiration) { + data.value.expiration = Math.round(new Date(data.value.expiration).getTime() / 1000); + } + if (data.model) { this.modelId = data.model.id; this.modelClassName = data.model.constructor.name.toLowerCase(); diff --git a/app/src/key-manager.es6 b/app/src/key-manager.es6 index 151cc014b..7e3aa6763 100644 --- a/app/src/key-manager.es6 +++ b/app/src/key-manager.es6 @@ -33,27 +33,27 @@ class KeyManager { }); } - extractAccountSecrets(accountJSON) { - const next = Object.assign({}, accountJSON); + extractAccountSecrets(account) { + const next = account.clone(); this._try(() => { const keys = this._getKeyHash(); - keys[`${accountJSON.emailAddress}-imap`] = next.settings.imap_password; + keys[`${account.emailAddress}-imap`] = next.settings.imap_password; delete next.settings.imap_password; - keys[`${accountJSON.emailAddress}-smtp`] = next.settings.smtp_password; + keys[`${account.emailAddress}-smtp`] = next.settings.smtp_password; delete next.settings.smtp_password; - keys[`${accountJSON.emailAddress}-refresh-token`] = next.settings.refresh_token; + keys[`${account.emailAddress}-refresh-token`] = next.settings.refresh_token; delete next.settings.refresh_token; return this._writeKeyHash(keys); }); return next; } - insertAccountSecrets(accountJSON) { - const next = Object.assign({}, accountJSON); + insertAccountSecrets(account) { + const next = account.clone(); const keys = this._getKeyHash(); - next.settings.imap_password = keys[`${accountJSON.emailAddress}-imap`]; - next.settings.smtp_password = keys[`${accountJSON.emailAddress}-smtp`]; - next.settings.refresh_token = keys[`${accountJSON.emailAddress}-refresh-token`]; + next.settings.imap_password = keys[`${account.emailAddress}-imap`]; + next.settings.smtp_password = keys[`${account.emailAddress}-smtp`]; + next.settings.refresh_token = keys[`${account.emailAddress}-refresh-token`]; return next; } diff --git a/app/src/mailsync-process.es6 b/app/src/mailsync-process.es6 index cc14e3b3a..7efc9a88b 100644 --- a/app/src/mailsync-process.es6 +++ b/app/src/mailsync-process.es6 @@ -53,12 +53,10 @@ export default class MailsyncProcess extends EventEmitter { const env = { CONFIG_DIR_PATH: this.configDirPath, IDENTITY_SERVER: 'unknown', - ACCOUNTS_SERVER: 'unknown', }; if (process.type === 'renderer') { const rootURLForServer = require('./flux/nylas-api-request').rootURLForServer; env.IDENTITY_SERVER = rootURLForServer('identity'); - env.ACCOUNTS_SERVER = rootURLForServer('accounts'); } this._proc = spawn(this.binaryPath, [`--mode`, mode], {env});