Cleanup onboarding, enforce metadata.expiration as timestamp

This commit is contained in:
Ben Gotow 2017-09-10 22:45:48 -07:00
parent 44b00cbbf5
commit bfaae56b59
32 changed files with 331 additions and 357 deletions

View file

@ -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"
},

View file

@ -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;

View file

@ -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) => {
<div className={`page account-setup ${FormComponent.displayName}`}>
<div className="logo-container">
<RetinaImg
style={{backgroundColor: AccountType.color, borderRadius: 44}}
name={AccountType.headerIcon}
style={{backgroundColor: providerConfig.color, borderRadius: 44}}
name={providerConfig.headerIcon}
mode={RetinaImg.Mode.ContentPreserve}
className="logo"
/>
</div>
{hideTitle ? <div style={{height: 20}} /> : <h2>{FormComponent.titleLabel(AccountType)}</h2>}
{hideTitle ? <div style={{height: 20}} /> : <h2>{FormComponent.titleLabel(providerConfig)}</h2>}
<FormErrorMessage
message={errorMessage}
empty={FormComponent.subtitleLabel(AccountType)}
empty={FormComponent.subtitleLabel(providerConfig)}
/>
{ this._renderCredentialsNote() }
<FormComponent
ref={(el) => { this._formEl = el; }}
accountInfo={accountInfo}
account={account}
errorFieldNames={errorFieldNames}
submitting={submitting}
onFieldChange={this.onFieldChange}

View file

@ -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 (
<span>
<label htmlFor={props.field}>{props.title}:</label>
@ -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;

View file

@ -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)) {

View file

@ -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);
}

View file

@ -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}
>
<div key={this.state.page} className="page-container">
<Component accountInfo={this.state.accountInfo} />
<Component account={this.state.account} />
</div>
</ReactCSSTransitionGroup>
</div>

View file

@ -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;
}
}

View file

@ -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}) =>
<div
key={accountType.type}
className={`provider ${accountType.type}`}
onClick={() => OnboardingActions.setAccountType(accountType.type)}
key={provider}
className={`provider ${provider}`}
onClick={() => OnboardingActions.chooseAccountProvider(provider)}
>
<div className="icon-container">
<RetinaImg
name={accountType.icon}
name={icon}
mode={RetinaImg.Mode.ContentPreserve}
className="icon"
/>
</div>
<span className="provider-name">{accountType.displayName}</span>
<span className="provider-name">{displayName}</span>
</div>
);
}
@ -36,7 +36,7 @@ export default class AccountChoosePage extends React.Component {
Connect an email account
</h2>
<div className="provider-list">
{this._renderAccountTypes()}
{this._renderProviders()}
</div>
</div>
);

View file

@ -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 (
<div className={`page account-setup AccountOnboardingSuccess`}>
<div className="logo-container">
<RetinaImg
style={{backgroundColor: accountType.color, borderRadius: 44}}
name={accountType.headerIcon}
style={{backgroundColor: providerConfig.color, borderRadius: 44}}
name={providerConfig.headerIcon}
mode={RetinaImg.Mode.ContentPreserve}
className="logo"
/>
</div>
<div>
<h2>Successfully connected to {accountType.displayName}!</h2>
<h2>Successfully connected to {providerConfig.displayName}!</h2>
<h3>Adding your account to Mailspring</h3>
</div>
</div>

View file

@ -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";

View file

@ -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}

View file

@ -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 (
<span>
<label htmlFor="imap_port">Port:</label>
<label htmlFor="settings.imap_port">Port:</label>
<select
id="imap_port"
id="settings.imap_port"
tabIndex={0}
value={accountInfo.imap_port}
value={settings.imap_port}
disabled={submitting}
onKeyPress={onFieldKeyPress}
onChange={onFieldChange}
@ -84,11 +79,11 @@ class AccountIMAPSettingsForm extends React.Component {
if (protocol === "smtp") {
return (
<span>
<label htmlFor="smtp_port">Port:</label>
<label htmlFor="settings.smtp_port">Port:</label>
<select
id="smtp_port"
id="settings.smtp_port"
tabIndex={0}
value={accountInfo.smtp_port}
value={settings.smtp_port}
disabled={submitting}
onKeyPress={onFieldKeyPress}
onChange={onFieldChange}
@ -104,16 +99,18 @@ class AccountIMAPSettingsForm extends React.Component {
}
renderSecurityDropdown(protocol) {
const {accountInfo, submitting, onFieldKeyPress, onFieldChange} = this.props;
const {account: {settings}, submitting, onFieldKeyPress, onFieldChange} = this.props;
return (
<div>
<span>
<label htmlFor={`${protocol}_security`}>Security:</label>
<label htmlFor={`settings.${protocol}_security`}>
Security:
</label>
<select
id={`${protocol}_security`}
id={`settings.${protocol}_security`}
tabIndex={0}
value={accountInfo[`${protocol}_security`]}
value={settings[`${protocol}_security`]}
disabled={submitting}
onKeyPress={onFieldKeyPress}
onChange={onFieldChange}
@ -126,9 +123,9 @@ class AccountIMAPSettingsForm extends React.Component {
<span style={{paddingLeft: '20px', paddingTop: '10px'}}>
<input
type="checkbox"
id={`${protocol}_allow_insecure_ssl`}
id={`settings.${protocol}_allow_insecure_ssl`}
disabled={submitting}
checked={accountInfo[`${protocol}_allow_insecure_ssl`] || false}
checked={settings[`${protocol}_allow_insecure_ssl`] || false}
onKeyPress={onFieldKeyPress}
onChange={onFieldChange}
/>
@ -141,13 +138,13 @@ class AccountIMAPSettingsForm extends React.Component {
renderFieldsForType(type) {
return (
<div>
<FormField field={`${type}_host`} title={"Server"} {...this.props} />
<FormField field={`settings.${type}_host`} title={"Server"} {...this.props} />
<div style={{textAlign: 'left'}}>
{this.renderPortDropdown(type)}
{this.renderSecurityDropdown(type)}
</div>
<FormField field={`${type}_username`} title={"Username"} {...this.props} />
<FormField field={`${type}_password`} title={"Password"} type="password" {...this.props} />
<FormField field={`settings.${type}_username`} title={"Username"} {...this.props} />
<FormField field={`settings.${type}_password`} title={"Password"} type="password" {...this.props} />
</div>
);
}

View file

@ -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 (
<form className="settings">
<FormField field="name" title="Name" {...this.props} />
<FormField field="email" title="Email" {...this.props} />
<FormField field="password" title="Password" type="password" {...this.props} />
<FormField field="emailAddress" title="Email" {...this.props} />
<FormField field="settings.imap_password" title="Password" type="password" {...this.props} />
</form>
)
}

View file

@ -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() {

View file

@ -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 {

View file

@ -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"
},

View file

@ -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)
}

View file

@ -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'

View file

@ -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"
},

View file

@ -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 || {};

View file

@ -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);

View file

@ -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\``;
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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}`;

View file

@ -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('.')

View file

@ -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");
}

View file

@ -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();

View file

@ -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();

View file

@ -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;
}

View file

@ -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});