mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-11-09 16:01:42 +08:00
Cleanup onboarding, enforce metadata.expiration as timestamp
This commit is contained in:
parent
44b00cbbf5
commit
bfaae56b59
32 changed files with 331 additions and 357 deletions
|
|
@ -3,7 +3,7 @@
|
||||||
"main": "./lib/main",
|
"main": "./lib/main",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"serverUrl": {
|
"serverUrl": {
|
||||||
"development": "http://localhost:5100",
|
"development": "http://localhost:5101",
|
||||||
"staging": "https://link-staging.getmailspring.com",
|
"staging": "https://link-staging.getmailspring.com",
|
||||||
"production": "https://link.getmailspring.com"
|
"production": "https://link.getmailspring.com"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,34 @@
|
||||||
const AccountTypes = [
|
const AccountProviders = [
|
||||||
{
|
{
|
||||||
type: 'gmail',
|
provider: 'gmail',
|
||||||
displayName: 'Gmail or G Suite',
|
displayName: 'Gmail or G Suite',
|
||||||
icon: 'ic-settings-account-gmail.png',
|
icon: 'ic-settings-account-gmail.png',
|
||||||
headerIcon: 'setup-icon-provider-gmail.png',
|
headerIcon: 'setup-icon-provider-gmail.png',
|
||||||
color: '#e99999',
|
color: '#e99999',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'office365',
|
provider: 'office365',
|
||||||
displayName: 'Office 365',
|
displayName: 'Office 365',
|
||||||
icon: 'ic-settings-account-outlook.png',
|
icon: 'ic-settings-account-outlook.png',
|
||||||
headerIcon: 'setup-icon-provider-outlook.png',
|
headerIcon: 'setup-icon-provider-outlook.png',
|
||||||
color: '#0078d7',
|
color: '#0078d7',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'yahoo',
|
provider: 'yahoo',
|
||||||
displayName: 'Yahoo',
|
displayName: 'Yahoo',
|
||||||
icon: 'ic-settings-account-yahoo.png',
|
icon: 'ic-settings-account-yahoo.png',
|
||||||
headerIcon: 'setup-icon-provider-yahoo.png',
|
headerIcon: 'setup-icon-provider-yahoo.png',
|
||||||
color: '#a76ead',
|
color: '#a76ead',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'icloud',
|
provider: 'icloud',
|
||||||
displayName: 'iCloud',
|
displayName: 'iCloud',
|
||||||
icon: 'ic-settings-account-icloud.png',
|
icon: 'ic-settings-account-icloud.png',
|
||||||
headerIcon: 'setup-icon-provider-icloud.png',
|
headerIcon: 'setup-icon-provider-icloud.png',
|
||||||
color: '#61bfe9',
|
color: '#61bfe9',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'fastmail',
|
provider: 'fastmail',
|
||||||
displayName: 'FastMail',
|
displayName: 'FastMail',
|
||||||
title: 'Set up your account',
|
title: 'Set up your account',
|
||||||
icon: 'ic-settings-account-fastmail.png',
|
icon: 'ic-settings-account-fastmail.png',
|
||||||
|
|
@ -36,7 +36,7 @@ const AccountTypes = [
|
||||||
color: '#24345a',
|
color: '#24345a',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'imap',
|
provider: 'imap',
|
||||||
displayName: 'IMAP / SMTP',
|
displayName: 'IMAP / SMTP',
|
||||||
title: 'Set up your IMAP account',
|
title: 'Set up your IMAP account',
|
||||||
icon: 'ic-settings-account-imap.png',
|
icon: 'ic-settings-account-imap.png',
|
||||||
|
|
@ -45,4 +45,4 @@ const AccountTypes = [
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export default AccountTypes;
|
export default AccountProviders;
|
||||||
|
|
@ -5,26 +5,26 @@ import {RetinaImg} from 'nylas-component-kit';
|
||||||
import {NylasAPIRequest, Actions} from 'nylas-exports';
|
import {NylasAPIRequest, Actions} from 'nylas-exports';
|
||||||
|
|
||||||
import OnboardingActions from '../onboarding-actions';
|
import OnboardingActions from '../onboarding-actions';
|
||||||
import {buildAndValidateAccount} from '../onboarding-helpers';
|
import {finalizeAndValidateAccount} from '../onboarding-helpers';
|
||||||
import FormErrorMessage from '../form-error-message';
|
import FormErrorMessage from '../form-error-message';
|
||||||
import AccountTypes from '../account-types'
|
import AccountProviders from '../account-providers'
|
||||||
|
|
||||||
const CreatePageForForm = (FormComponent) => {
|
const CreatePageForForm = (FormComponent) => {
|
||||||
return class Composed extends React.Component {
|
return class Composed extends React.Component {
|
||||||
static displayName = FormComponent.displayName;
|
static displayName = FormComponent.displayName;
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
accountInfo: React.PropTypes.object,
|
account: React.PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = Object.assign({
|
this.state = Object.assign({
|
||||||
accountInfo: JSON.parse(JSON.stringify(this.props.accountInfo)),
|
account: this.props.account.clone(),
|
||||||
errorFieldNames: [],
|
errorFieldNames: [],
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
}, FormComponent.validateAccountInfo(this.props.accountInfo));
|
}, FormComponent.validateAccount(this.props.account));
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
|
@ -61,24 +61,36 @@ const CreatePageForForm = (FormComponent) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
onFieldChange = (event) => {
|
onFieldChange = (event) => {
|
||||||
const changes = {};
|
const next = this.state.account.clone();
|
||||||
|
|
||||||
|
let val = event.target.value;
|
||||||
if (event.target.type === 'checkbox') {
|
if (event.target.type === 'checkbox') {
|
||||||
changes[event.target.id] = event.target.checked;
|
val = event.target.checked;
|
||||||
} else {
|
}
|
||||||
changes[event.target.id] = event.target.value;
|
if (event.target.id === 'emailAddress') {
|
||||||
if (event.target.id === 'email') {
|
val = val.trim();
|
||||||
changes[event.target.id] = event.target.value.trim();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const accountInfo = Object.assign({}, this.state.accountInfo, changes);
|
if (event.target.id.includes('.')) {
|
||||||
const {errorFieldNames, errorMessage, populated} = FormComponent.validateAccountInfo(accountInfo);
|
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 = () => {
|
onSubmit = () => {
|
||||||
OnboardingActions.setAccountInfo(this.state.accountInfo);
|
OnboardingActions.setAccount(this.state.account);
|
||||||
this._formEl.submit();
|
this._formEl.submit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,24 +102,24 @@ const CreatePageForForm = (FormComponent) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
onBack = () => {
|
onBack = () => {
|
||||||
OnboardingActions.setAccountInfo(this.state.accountInfo);
|
OnboardingActions.setAccount(this.state.account);
|
||||||
OnboardingActions.moveToPreviousPage();
|
OnboardingActions.moveToPreviousPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
onConnect = (updatedAccountInfo) => {
|
onConnect = (updatedAccount) => {
|
||||||
const accountInfo = updatedAccountInfo || this.state.accountInfo;
|
const account = updatedAccount || this.state.account;
|
||||||
|
|
||||||
this.setState({submitting: true});
|
this.setState({submitting: true});
|
||||||
|
|
||||||
buildAndValidateAccount(accountInfo)
|
finalizeAndValidateAccount(account)
|
||||||
.then(({account}) => {
|
.then((validated) => {
|
||||||
OnboardingActions.moveToPage('account-onboarding-success')
|
OnboardingActions.moveToPage('account-onboarding-success')
|
||||||
OnboardingActions.accountJSONReceived(account)
|
OnboardingActions.finishAndAddAccount(validated)
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
Actions.recordUserEvent('Email Account Auth Failed', {
|
Actions.recordUserEvent('Email Account Auth Failed', {
|
||||||
errorMessage: err.message,
|
errorMessage: err.message,
|
||||||
provider: accountInfo.type,
|
provider: account.provider,
|
||||||
})
|
})
|
||||||
|
|
||||||
const errorFieldNames = err.body ? (err.body.missing_fields || err.body.missing_settings || []) : []
|
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") {
|
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.';
|
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('eas_server_host')
|
||||||
errorFieldNames.push('username');
|
errorFieldNames.push('username');
|
||||||
}
|
}
|
||||||
|
|
@ -144,8 +156,8 @@ const CreatePageForForm = (FormComponent) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderButton() {
|
_renderButton() {
|
||||||
const {accountInfo, submitting} = this.state;
|
const {account, submitting} = this.state;
|
||||||
const buttonLabel = FormComponent.submitLabel(accountInfo);
|
const buttonLabel = FormComponent.submitLabel(account);
|
||||||
|
|
||||||
// We're not on the last page.
|
// We're not on the last page.
|
||||||
if (submitting) {
|
if (submitting) {
|
||||||
|
|
@ -172,11 +184,11 @@ const CreatePageForForm = (FormComponent) => {
|
||||||
// help with common problems. For instance, they may need an app password,
|
// help with common problems. For instance, they may need an app password,
|
||||||
// or to enable specific settings with their provider.
|
// or to enable specific settings with their provider.
|
||||||
_renderCredentialsNote() {
|
_renderCredentialsNote() {
|
||||||
const {errorStatusCode, accountInfo} = this.state;
|
const {errorStatusCode, account} = this.state;
|
||||||
if (errorStatusCode !== 401) { return false; }
|
if (errorStatusCode !== 401) { return false; }
|
||||||
let message;
|
let message;
|
||||||
let articleURL;
|
let articleURL;
|
||||||
if (accountInfo.email.includes("@yahoo.com")) {
|
if (account.emailAddress.includes("@yahoo.com")) {
|
||||||
message = "Have you enabled access through Yahoo?";
|
message = "Have you enabled access through Yahoo?";
|
||||||
articleURL = "https://support.getmailspring.com/hc/en-us/articles/115001076128";
|
articleURL = "https://support.getmailspring.com/hc/en-us/articles/115001076128";
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -200,11 +212,11 @@ const CreatePageForForm = (FormComponent) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {accountInfo, errorMessage, errorFieldNames, submitting} = this.state;
|
const {account, errorMessage, errorFieldNames, submitting} = this.state;
|
||||||
const AccountType = AccountTypes.find(a => a.type === accountInfo.type);
|
const providerConfig = AccountProviders.find(({provider}) => provider === account.provider);
|
||||||
|
|
||||||
if (!AccountType) {
|
if (!providerConfig) {
|
||||||
throw new Error(`Cannot find account type ${accountInfo.type}`);
|
throw new Error(`Cannot find account provider ${account.provider}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideTitle = errorMessage && errorMessage.length > 120;
|
const hideTitle = errorMessage && errorMessage.length > 120;
|
||||||
|
|
@ -213,21 +225,21 @@ const CreatePageForForm = (FormComponent) => {
|
||||||
<div className={`page account-setup ${FormComponent.displayName}`}>
|
<div className={`page account-setup ${FormComponent.displayName}`}>
|
||||||
<div className="logo-container">
|
<div className="logo-container">
|
||||||
<RetinaImg
|
<RetinaImg
|
||||||
style={{backgroundColor: AccountType.color, borderRadius: 44}}
|
style={{backgroundColor: providerConfig.color, borderRadius: 44}}
|
||||||
name={AccountType.headerIcon}
|
name={providerConfig.headerIcon}
|
||||||
mode={RetinaImg.Mode.ContentPreserve}
|
mode={RetinaImg.Mode.ContentPreserve}
|
||||||
className="logo"
|
className="logo"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{hideTitle ? <div style={{height: 20}} /> : <h2>{FormComponent.titleLabel(AccountType)}</h2>}
|
{hideTitle ? <div style={{height: 20}} /> : <h2>{FormComponent.titleLabel(providerConfig)}</h2>}
|
||||||
<FormErrorMessage
|
<FormErrorMessage
|
||||||
message={errorMessage}
|
message={errorMessage}
|
||||||
empty={FormComponent.subtitleLabel(AccountType)}
|
empty={FormComponent.subtitleLabel(providerConfig)}
|
||||||
/>
|
/>
|
||||||
{ this._renderCredentialsNote() }
|
{ this._renderCredentialsNote() }
|
||||||
<FormComponent
|
<FormComponent
|
||||||
ref={(el) => { this._formEl = el; }}
|
ref={(el) => { this._formEl = el; }}
|
||||||
accountInfo={accountInfo}
|
account={account}
|
||||||
errorFieldNames={errorFieldNames}
|
errorFieldNames={errorFieldNames}
|
||||||
submitting={submitting}
|
submitting={submitting}
|
||||||
onFieldChange={this.onFieldChange}
|
onFieldChange={this.onFieldChange}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const FormField = (props) => {
|
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 (
|
return (
|
||||||
<span>
|
<span>
|
||||||
<label htmlFor={props.field}>{props.title}:</label>
|
<label htmlFor={props.field}>{props.title}:</label>
|
||||||
|
|
@ -8,9 +14,10 @@ const FormField = (props) => {
|
||||||
type={props.type || "text"}
|
type={props.type || "text"}
|
||||||
id={props.field}
|
id={props.field}
|
||||||
style={props.style}
|
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}
|
disabled={props.submitting}
|
||||||
value={props.accountInfo[props.field] || ''}
|
spellCheck="false"
|
||||||
|
value={val || ''}
|
||||||
onKeyPress={props.onFieldKeyPress}
|
onKeyPress={props.onFieldKeyPress}
|
||||||
onChange={props.onFieldChange}
|
onChange={props.onFieldChange}
|
||||||
/>
|
/>
|
||||||
|
|
@ -27,7 +34,7 @@ FormField.propTypes = {
|
||||||
onFieldKeyPress: React.PropTypes.func,
|
onFieldKeyPress: React.PropTypes.func,
|
||||||
onFieldChange: React.PropTypes.func,
|
onFieldChange: React.PropTypes.func,
|
||||||
errorFieldNames: React.PropTypes.array,
|
errorFieldNames: React.PropTypes.array,
|
||||||
accountInfo: React.PropTypes.object,
|
account: React.PropTypes.object,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FormField;
|
export default FormField;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import Reflux from 'reflux';
|
import Reflux from 'reflux';
|
||||||
|
|
||||||
const OnboardingActions = Reflux.createActions([
|
const OnboardingActions = Reflux.createActions([
|
||||||
"setAccountInfo",
|
|
||||||
"setAccountType",
|
|
||||||
"moveToPreviousPage",
|
"moveToPreviousPage",
|
||||||
"moveToPage",
|
"moveToPage",
|
||||||
|
"setAccount",
|
||||||
|
"chooseAccountProvider",
|
||||||
"identityJSONReceived",
|
"identityJSONReceived",
|
||||||
"accountJSONReceived",
|
"finishAndAddAccount",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
for (const key of Object.keys(OnboardingActions)) {
|
for (const key of Object.keys(OnboardingActions)) {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import {CommonProviderSettings} from 'imap-provider-settings';
|
import {CommonProviderSettings} from 'imap-provider-settings';
|
||||||
import {
|
import {
|
||||||
|
Account,
|
||||||
NylasAPIRequest,
|
NylasAPIRequest,
|
||||||
IdentityStore,
|
IdentityStore,
|
||||||
RegExpUtils,
|
RegExpUtils,
|
||||||
|
|
@ -11,22 +12,7 @@ import {
|
||||||
|
|
||||||
const {makeRequest, rootURLForServer} = NylasAPIRequest;
|
const {makeRequest, rootURLForServer} = NylasAPIRequest;
|
||||||
|
|
||||||
const IMAP_FIELDS = new Set([
|
function base64URL(inBuffer) {
|
||||||
"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) {
|
|
||||||
let buffer;
|
let buffer;
|
||||||
if (typeof inBuffer === "string") {
|
if (typeof inBuffer === "string") {
|
||||||
buffer = new Buffer(inBuffer);
|
buffer = new Buffer(inBuffer);
|
||||||
|
|
@ -55,114 +41,92 @@ function idForAccount(emailAddress, connectionSettings) {
|
||||||
return crypto.createHash('sha256').update(idString, 'utf8').digest('hex');
|
return crypto.createHash('sha256').update(idString, 'utf8').digest('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeGmailOAuthRequest(sessionKey) {
|
export function expandAccountWithCommonSettings(account) {
|
||||||
return makeRequest({
|
const domain = account.emailAddress.split('@').pop().toLowerCase();
|
||||||
server: 'accounts',
|
let template = CommonProviderSettings[domain] || CommonProviderSettings[account.provider] || {};
|
||||||
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] || {};
|
|
||||||
if (template.alias) {
|
if (template.alias) {
|
||||||
template = CommonProviderSettings[template.alias];
|
template = CommonProviderSettings[template.alias];
|
||||||
}
|
}
|
||||||
|
|
||||||
const usernameWithFormat = (format) => {
|
const usernameWithFormat = (format) => {
|
||||||
if (format === 'email') {
|
if (format === 'email') {
|
||||||
return email
|
return account.emailAddress
|
||||||
}
|
}
|
||||||
if (format === 'email-without-domain') {
|
if (format === 'email-without-domain') {
|
||||||
return email.split('@').shift();
|
return account.emailAddress.split('@').shift();
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaults = {
|
const populated = account.clone();
|
||||||
|
|
||||||
|
populated.settings = Object.assign({
|
||||||
imap_host: template.imap_host,
|
imap_host: template.imap_host,
|
||||||
imap_port: template.imap_port || 993,
|
imap_port: template.imap_port || 993,
|
||||||
imap_username: usernameWithFormat(template.imap_user_format),
|
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_security: template.imap_security || "SSL / TLS",
|
||||||
imap_allow_insecure_ssl: template.imap_allow_insecure_ssl || false,
|
imap_allow_insecure_ssl: template.imap_allow_insecure_ssl || false,
|
||||||
smtp_host: template.smtp_host,
|
smtp_host: template.smtp_host,
|
||||||
smtp_port: template.smtp_port || 587,
|
smtp_port: template.smtp_port || 587,
|
||||||
smtp_username: usernameWithFormat(template.smtp_user_format),
|
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_security: template.smtp_security || "STARTTLS",
|
||||||
smtp_allow_insecure_ssl: template.smtp_allow_insecure_ssl || false,
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ export default class OnboardingRoot extends React.Component {
|
||||||
return {
|
return {
|
||||||
page: OnboardingStore.page(),
|
page: OnboardingStore.page(),
|
||||||
pageDepth: OnboardingStore.pageDepth(),
|
pageDepth: OnboardingStore.pageDepth(),
|
||||||
accountInfo: OnboardingStore.accountInfo(),
|
account: OnboardingStore.account(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,7 +79,7 @@ export default class OnboardingRoot extends React.Component {
|
||||||
transitionEnterTimeout={150}
|
transitionEnterTimeout={150}
|
||||||
>
|
>
|
||||||
<div key={this.state.page} className="page-container">
|
<div key={this.state.page} className="page-container">
|
||||||
<Component accountInfo={this.state.accountInfo} />
|
<Component account={this.state.account} />
|
||||||
</div>
|
</div>
|
||||||
</ReactCSSTransitionGroup>
|
</ReactCSSTransitionGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,65 +1,51 @@
|
||||||
import {AccountStore, Actions, IdentityStore} from 'nylas-exports';
|
import {AccountStore, Account, Actions, IdentityStore} from 'nylas-exports';
|
||||||
import {ipcRenderer} from 'electron';
|
import {ipcRenderer} from 'electron';
|
||||||
import NylasStore from 'nylas-store';
|
import NylasStore from 'nylas-store';
|
||||||
|
|
||||||
import OnboardingActions from './onboarding-actions';
|
import OnboardingActions from './onboarding-actions';
|
||||||
|
|
||||||
function accountTypeForProvider(provider) {
|
|
||||||
if (provider === 'eas') {
|
|
||||||
return 'exchange';
|
|
||||||
}
|
|
||||||
if (provider === 'custom') {
|
|
||||||
return 'imap';
|
|
||||||
}
|
|
||||||
return provider;
|
|
||||||
}
|
|
||||||
|
|
||||||
class OnboardingStore extends NylasStore {
|
class OnboardingStore extends NylasStore {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.listenTo(OnboardingActions.moveToPreviousPage, this._onMoveToPreviousPage)
|
this.listenTo(OnboardingActions.moveToPreviousPage, this._onMoveToPreviousPage)
|
||||||
this.listenTo(OnboardingActions.moveToPage, this._onMoveToPage)
|
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.identityJSONReceived, this._onIdentityJSONReceived)
|
||||||
this.listenTo(OnboardingActions.setAccountInfo, this._onSetAccountInfo);
|
|
||||||
this.listenTo(OnboardingActions.setAccountType, this._onSetAccountType);
|
ipcRenderer.on('set-account-provider', (e, provider) => {
|
||||||
ipcRenderer.on('set-account-type', (e, type) => {
|
if (provider) {
|
||||||
if (type) {
|
this._onChooseAccountProvider(provider)
|
||||||
this._onSetAccountType(type)
|
|
||||||
} else {
|
} else {
|
||||||
this._pageStack = ['account-choose']
|
this._pageStack = ['account-choose']
|
||||||
this.trigger()
|
this.trigger()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const {existingAccount, addingAccount, accountType} = NylasEnv.getWindowProps();
|
const {existingAccount, addingAccount, accountProvider} = NylasEnv.getWindowProps();
|
||||||
|
|
||||||
const hasAccounts = (AccountStore.accounts().length > 0)
|
const hasAccounts = (AccountStore.accounts().length > 0)
|
||||||
const identity = IdentityStore.identity();
|
const identity = IdentityStore.identity();
|
||||||
|
|
||||||
if (identity) {
|
this._account = new Account({
|
||||||
this._accountInfo = {
|
name: identity ? `${identity.firstName || ""} ${identity.lastName || ""}` : '',
|
||||||
name: `${identity.firstName || ""} ${identity.lastName || ""}`,
|
emailAddress: identity ? identity.emailAddress : '',
|
||||||
};
|
settings: {},
|
||||||
} else {
|
});
|
||||||
this._accountInfo = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingAccount) {
|
if (existingAccount) {
|
||||||
// Used when re-adding an account after re-connecting
|
// Used when re-adding an account after re-connecting
|
||||||
const existingAccountType = accountTypeForProvider(existingAccount.provider);
|
this._pageStack = ['account-choose'];
|
||||||
this._pageStack = ['account-choose']
|
this._account.name = existingAccount.name;
|
||||||
this._accountInfo = {
|
this._account.emailAddress = existingAccount.emailAddress;
|
||||||
name: existingAccount.name,
|
this._onChooseAccountProvider(existingAccount.provider);
|
||||||
email: existingAccount.emailAddress,
|
|
||||||
};
|
|
||||||
this._onSetAccountType(existingAccountType);
|
|
||||||
} else if (addingAccount) {
|
} else if (addingAccount) {
|
||||||
// Adding a new, unknown account
|
// Adding a new, unknown account
|
||||||
this._pageStack = ['account-choose'];
|
this._pageStack = ['account-choose'];
|
||||||
if (accountType) {
|
if (accountProvider) {
|
||||||
this._onSetAccountType(accountType);
|
this._onChooseAccountProvider(accountProvider);
|
||||||
}
|
}
|
||||||
} else if (identity) {
|
} else if (identity) {
|
||||||
// Should only happen if config was edited to remove all accounts,
|
// Should only happen if config was edited to remove all accounts,
|
||||||
|
|
@ -88,26 +74,34 @@ class OnboardingStore extends NylasStore {
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSetAccountType = (type) => {
|
_onChooseAccountProvider = (provider) => {
|
||||||
let nextPage = "account-settings";
|
let nextPage = "account-settings";
|
||||||
if (type === 'gmail') {
|
if (provider === 'gmail') {
|
||||||
nextPage = "account-settings-gmail";
|
nextPage = "account-settings-gmail";
|
||||||
} else if (type === 'exchange') {
|
} else if (provider === 'exchange') {
|
||||||
nextPage = "account-settings-exchange";
|
nextPage = "account-settings-exchange";
|
||||||
}
|
}
|
||||||
|
|
||||||
Actions.recordUserEvent('Selected Account Type', {
|
Actions.recordUserEvent('Selected Account Provider', {
|
||||||
provider: type,
|
provider,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Don't carry over any type-specific account information
|
// Don't carry over any type-specific account information
|
||||||
const {email, name, password} = this._accountInfo;
|
this._onSetAccount(new Account({
|
||||||
this._onSetAccountInfo({email, name, password, type});
|
emailAddress: this._account.emailAddress,
|
||||||
|
name: this._account.name,
|
||||||
|
settings: {},
|
||||||
|
provider,
|
||||||
|
}));
|
||||||
|
|
||||||
this._onMoveToPage(nextPage);
|
this._onMoveToPage(nextPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSetAccountInfo = (info) => {
|
_onSetAccount = (acct) => {
|
||||||
this._accountInfo = info;
|
if (!(acct instanceof Account)) {
|
||||||
|
throw new Error("OnboardingActions.setAccount expects an Account instance.");
|
||||||
|
}
|
||||||
|
this._account = acct;
|
||||||
this.trigger();
|
this.trigger();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -128,10 +122,10 @@ class OnboardingStore extends NylasStore {
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (isFirstAccount) {
|
if (isFirstAccount) {
|
||||||
this._onSetAccountInfo(Object.assign({}, this._accountInfo, {
|
const next = this._account.clone();
|
||||||
name: `${json.firstName || ""} ${json.lastName || ""}`,
|
next.name = `${json.firstName || ""} ${json.lastName || ""}`;
|
||||||
email: json.emailAddress,
|
next.emailAddress = json.emailAddress;
|
||||||
}));
|
this._onSetAccount(next);
|
||||||
OnboardingActions.moveToPage('account-choose');
|
OnboardingActions.moveToPage('account-choose');
|
||||||
} else {
|
} else {
|
||||||
this._onOnboardingComplete();
|
this._onOnboardingComplete();
|
||||||
|
|
@ -139,22 +133,21 @@ class OnboardingStore extends NylasStore {
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onAccountJSONReceived = async (json) => {
|
_onFinishAndAddAccount = async (account) => {
|
||||||
try {
|
try {
|
||||||
const isFirstAccount = AccountStore.accounts().length === 0;
|
const isFirstAccount = AccountStore.accounts().length === 0;
|
||||||
AccountStore.addAccountFromJSON(json);
|
|
||||||
|
AccountStore.addAccount(account);
|
||||||
|
NylasEnv.displayWindow();
|
||||||
|
|
||||||
Actions.recordUserEvent('Email Account Auth Succeeded', {
|
Actions.recordUserEvent('Email Account Auth Succeeded', {
|
||||||
provider: json.provider,
|
provider: account.provider,
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcRenderer.send('new-account-added');
|
|
||||||
NylasEnv.displayWindow();
|
|
||||||
|
|
||||||
if (isFirstAccount) {
|
if (isFirstAccount) {
|
||||||
this._onMoveToPage('initial-preferences');
|
this._onMoveToPage('initial-preferences');
|
||||||
Actions.recordUserEvent('First Account Linked', {
|
Actions.recordUserEvent('First Account Linked', {
|
||||||
provider: json.provider,
|
provider: account.provider,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// let them see the "success" screen for a moment
|
// let them see the "success" screen for a moment
|
||||||
|
|
@ -177,8 +170,8 @@ class OnboardingStore extends NylasStore {
|
||||||
return this._pageStack.length;
|
return this._pageStack.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
accountInfo() {
|
account() {
|
||||||
return this._accountInfo;
|
return this._account;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,30 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {RetinaImg} from 'nylas-component-kit';
|
import {RetinaImg} from 'nylas-component-kit';
|
||||||
import OnboardingActions from './onboarding-actions';
|
import OnboardingActions from './onboarding-actions';
|
||||||
import AccountTypes from './account-types';
|
import AccountProviders from './account-providers';
|
||||||
|
|
||||||
export default class AccountChoosePage extends React.Component {
|
export default class AccountChoosePage extends React.Component {
|
||||||
static displayName = "AccountChoosePage";
|
static displayName = "AccountChoosePage";
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
accountInfo: React.PropTypes.object,
|
account: React.PropTypes.object,
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderAccountTypes() {
|
_renderProviders() {
|
||||||
return AccountTypes.map((accountType) =>
|
return AccountProviders.map(({icon, displayName, provider}) =>
|
||||||
<div
|
<div
|
||||||
key={accountType.type}
|
key={provider}
|
||||||
className={`provider ${accountType.type}`}
|
className={`provider ${provider}`}
|
||||||
onClick={() => OnboardingActions.setAccountType(accountType.type)}
|
onClick={() => OnboardingActions.chooseAccountProvider(provider)}
|
||||||
>
|
>
|
||||||
<div className="icon-container">
|
<div className="icon-container">
|
||||||
<RetinaImg
|
<RetinaImg
|
||||||
name={accountType.icon}
|
name={icon}
|
||||||
mode={RetinaImg.Mode.ContentPreserve}
|
mode={RetinaImg.Mode.ContentPreserve}
|
||||||
className="icon"
|
className="icon"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="provider-name">{accountType.displayName}</span>
|
<span className="provider-name">{displayName}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -36,7 +36,7 @@ export default class AccountChoosePage extends React.Component {
|
||||||
Connect an email account
|
Connect an email account
|
||||||
</h2>
|
</h2>
|
||||||
<div className="provider-list">
|
<div className="provider-list">
|
||||||
{this._renderAccountTypes()}
|
{this._renderProviders()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,32 @@
|
||||||
import React, {Component} from 'react';
|
import React, {Component} from 'react';
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import {RetinaImg} from 'nylas-component-kit';
|
import {RetinaImg} from 'nylas-component-kit';
|
||||||
import AccountTypes from './account-types'
|
import AccountProviders from './account-providers'
|
||||||
|
|
||||||
|
|
||||||
class AccountOnboardingSuccess extends Component { // eslint-disable-line
|
class AccountOnboardingSuccess extends Component { // eslint-disable-line
|
||||||
static displayName = 'AccountOnboardingSuccess'
|
static displayName = 'AccountOnboardingSuccess'
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
accountInfo: PropTypes.object,
|
account: PropTypes.object,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {accountInfo} = this.props
|
const {account} = this.props;
|
||||||
const accountType = AccountTypes.find(a => a.type === accountInfo.type);
|
const providerConfig = AccountProviders.find(({provider}) => provider === account.provider);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`page account-setup AccountOnboardingSuccess`}>
|
<div className={`page account-setup AccountOnboardingSuccess`}>
|
||||||
<div className="logo-container">
|
<div className="logo-container">
|
||||||
<RetinaImg
|
<RetinaImg
|
||||||
style={{backgroundColor: accountType.color, borderRadius: 44}}
|
style={{backgroundColor: providerConfig.color, borderRadius: 44}}
|
||||||
name={accountType.headerIcon}
|
name={providerConfig.headerIcon}
|
||||||
mode={RetinaImg.Mode.ContentPreserve}
|
mode={RetinaImg.Mode.ContentPreserve}
|
||||||
className="logo"
|
className="logo"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2>Successfully connected to {accountType.displayName}!</h2>
|
<h2>Successfully connected to {providerConfig.displayName}!</h2>
|
||||||
<h3>Adding your account to Mailspring…</h3>
|
<h3>Adding your account to Mailspring…</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ class AccountExchangeSettingsForm extends React.Component {
|
||||||
static displayName = 'AccountExchangeSettingsForm';
|
static displayName = 'AccountExchangeSettingsForm';
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
accountInfo: React.PropTypes.object,
|
account: React.PropTypes.object,
|
||||||
errorFieldNames: React.PropTypes.array,
|
errorFieldNames: React.PropTypes.array,
|
||||||
submitting: React.PropTypes.bool,
|
submitting: React.PropTypes.bool,
|
||||||
onConnect: React.PropTypes.func,
|
onConnect: React.PropTypes.func,
|
||||||
|
|
@ -28,28 +28,28 @@ class AccountExchangeSettingsForm extends React.Component {
|
||||||
return 'Enter your Exchange credentials to get started.';
|
return 'Enter your Exchange credentials to get started.';
|
||||||
}
|
}
|
||||||
|
|
||||||
static validateAccountInfo = (accountInfo) => {
|
static validateAccount = (account) => {
|
||||||
const {email, password, name} = accountInfo;
|
const {emailAddress, password, name} = account;
|
||||||
const errorFieldNames = [];
|
const errorFieldNames = [];
|
||||||
let errorMessage = null;
|
let errorMessage = null;
|
||||||
|
|
||||||
if (!email || !password || !name) {
|
if (!emailAddress || !password || !name) {
|
||||||
return {errorMessage, errorFieldNames, populated: false};
|
return {errorMessage, errorFieldNames, populated: false};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!RegExpUtils.emailRegex().test(accountInfo.email)) {
|
if (!RegExpUtils.emailRegex().test(emailAddress)) {
|
||||||
errorFieldNames.push('email')
|
errorFieldNames.push('email')
|
||||||
errorMessage = "Please provide a valid email address."
|
errorMessage = "Please provide a valid email address."
|
||||||
}
|
}
|
||||||
if (!accountInfo.password) {
|
if (!account.settings.password) {
|
||||||
errorFieldNames.push('password')
|
errorFieldNames.push('password')
|
||||||
errorMessage = "Please provide a password for your account."
|
errorMessage = "Please provide a password for your account."
|
||||||
}
|
}
|
||||||
if (!accountInfo.name) {
|
if (!account.name) {
|
||||||
errorFieldNames.push('name')
|
errorFieldNames.push('name')
|
||||||
errorMessage = "Please provide your 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')
|
errorFieldNames.push('eas_server_host')
|
||||||
errorMessage = "Please provide a valid host name."
|
errorMessage = "Please provide a valid host name."
|
||||||
}
|
}
|
||||||
|
|
@ -67,13 +67,13 @@ class AccountExchangeSettingsForm extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {errorFieldNames, accountInfo} = this.props;
|
const {errorFieldNames, account} = this.props;
|
||||||
const showAdvanced = (
|
const showAdvanced = (
|
||||||
this.state.showAdvanced ||
|
this.state.showAdvanced ||
|
||||||
errorFieldNames.includes('eas_server_host') ||
|
errorFieldNames.includes('eas_server_host') ||
|
||||||
errorFieldNames.includes('username') ||
|
errorFieldNames.includes('username') ||
|
||||||
accountInfo.eas_server_host ||
|
account.eas_server_host ||
|
||||||
accountInfo.username
|
account.username
|
||||||
);
|
);
|
||||||
|
|
||||||
let classnames = "twocol";
|
let classnames = "twocol";
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,20 @@ import {OAuthSignInPage} from 'nylas-component-kit';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
makeGmailOAuthRequest,
|
makeGmailOAuthRequest,
|
||||||
authIMAPForGmail,
|
buildGmailAccountFromToken,
|
||||||
buildGmailSessionKey,
|
buildGmailSessionKey,
|
||||||
buildGmailAuthURL,
|
buildGmailAuthURL,
|
||||||
} from './onboarding-helpers';
|
} from './onboarding-helpers';
|
||||||
|
|
||||||
import OnboardingActions from './onboarding-actions';
|
import OnboardingActions from './onboarding-actions';
|
||||||
import AccountTypes from './account-types';
|
import AccountProviders from './account-providers';
|
||||||
|
|
||||||
|
|
||||||
export default class AccountSettingsPageGmail extends React.Component {
|
export default class AccountSettingsPageGmail extends React.Component {
|
||||||
static displayName = "AccountSettingsPageGmail";
|
static displayName = "AccountSettingsPageGmail";
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
accountInfo: React.PropTypes.object,
|
account: React.PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -30,9 +30,10 @@ export default class AccountSettingsPageGmail extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {accountInfo} = this.props;
|
const providerConfig = AccountProviders.find(a =>
|
||||||
const accountType = AccountTypes.find(a => a.type === accountInfo.type)
|
a.provider === this.props.account.provider
|
||||||
const {headerIcon} = accountType;
|
)
|
||||||
|
const {headerIcon} = providerConfig;
|
||||||
const goBack = () => OnboardingActions.moveToPreviousPage()
|
const goBack = () => OnboardingActions.moveToPreviousPage()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -41,7 +42,7 @@ export default class AccountSettingsPageGmail extends React.Component {
|
||||||
providerAuthPageUrl={this._gmailAuthUrl}
|
providerAuthPageUrl={this._gmailAuthUrl}
|
||||||
iconName={headerIcon}
|
iconName={headerIcon}
|
||||||
tokenRequestPollFn={makeGmailOAuthRequest}
|
tokenRequestPollFn={makeGmailOAuthRequest}
|
||||||
accountFromTokenFn={authIMAPForGmail}
|
accountFromTokenFn={buildGmailAccountFromToken}
|
||||||
onSuccess={this.onSuccess}
|
onSuccess={this.onSuccess}
|
||||||
onTryAgain={goBack}
|
onTryAgain={goBack}
|
||||||
sessionKey={this._sessionKey}
|
sessionKey={this._sessionKey}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ class AccountIMAPSettingsForm extends React.Component {
|
||||||
static displayName = 'AccountIMAPSettingsForm';
|
static displayName = 'AccountIMAPSettingsForm';
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
accountInfo: React.PropTypes.object,
|
account: React.PropTypes.object,
|
||||||
errorFieldNames: React.PropTypes.array,
|
errorFieldNames: React.PropTypes.array,
|
||||||
submitting: React.PropTypes.bool,
|
submitting: React.PropTypes.bool,
|
||||||
onConnect: React.PropTypes.func,
|
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.';
|
return 'Complete the IMAP and SMTP settings below to connect your account.';
|
||||||
}
|
}
|
||||||
|
|
||||||
static validateAccountInfo = (accountInfo) => {
|
static validateAccount = (account) => {
|
||||||
let errorMessage = null;
|
let errorMessage = null;
|
||||||
const errorFieldNames = [];
|
const errorFieldNames = [];
|
||||||
|
|
||||||
for (const type of ['imap', 'smtp']) {
|
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};
|
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.";
|
errorMessage = "Please provide a valid hostname or IP adddress.";
|
||||||
errorFieldNames.push(`${type}_host`);
|
errorFieldNames.push(`${type}_host`);
|
||||||
}
|
}
|
||||||
// todo bg
|
if (!Number.isInteger(account.settings[`${type}_port`] / 1)) {
|
||||||
// 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)) {
|
|
||||||
errorMessage = "Please provide a valid port number.";
|
errorMessage = "Please provide a valid port number.";
|
||||||
errorFieldNames.push(`${type}_port`);
|
errorFieldNames.push(`${type}_port`);
|
||||||
}
|
}
|
||||||
|
|
@ -61,16 +56,16 @@ class AccountIMAPSettingsForm extends React.Component {
|
||||||
if (!["imap", "smtp"].includes(protocol)) {
|
if (!["imap", "smtp"].includes(protocol)) {
|
||||||
throw new Error(`Can't render port dropdown for protocol '${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") {
|
if (protocol === "imap") {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
<label htmlFor="imap_port">Port:</label>
|
<label htmlFor="settings.imap_port">Port:</label>
|
||||||
<select
|
<select
|
||||||
id="imap_port"
|
id="settings.imap_port"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
value={accountInfo.imap_port}
|
value={settings.imap_port}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
onKeyPress={onFieldKeyPress}
|
onKeyPress={onFieldKeyPress}
|
||||||
onChange={onFieldChange}
|
onChange={onFieldChange}
|
||||||
|
|
@ -84,11 +79,11 @@ class AccountIMAPSettingsForm extends React.Component {
|
||||||
if (protocol === "smtp") {
|
if (protocol === "smtp") {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
<label htmlFor="smtp_port">Port:</label>
|
<label htmlFor="settings.smtp_port">Port:</label>
|
||||||
<select
|
<select
|
||||||
id="smtp_port"
|
id="settings.smtp_port"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
value={accountInfo.smtp_port}
|
value={settings.smtp_port}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
onKeyPress={onFieldKeyPress}
|
onKeyPress={onFieldKeyPress}
|
||||||
onChange={onFieldChange}
|
onChange={onFieldChange}
|
||||||
|
|
@ -104,16 +99,18 @@ class AccountIMAPSettingsForm extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSecurityDropdown(protocol) {
|
renderSecurityDropdown(protocol) {
|
||||||
const {accountInfo, submitting, onFieldKeyPress, onFieldChange} = this.props;
|
const {account: {settings}, submitting, onFieldKeyPress, onFieldChange} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<span>
|
<span>
|
||||||
<label htmlFor={`${protocol}_security`}>Security:</label>
|
<label htmlFor={`settings.${protocol}_security`}>
|
||||||
|
Security:
|
||||||
|
</label>
|
||||||
<select
|
<select
|
||||||
id={`${protocol}_security`}
|
id={`settings.${protocol}_security`}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
value={accountInfo[`${protocol}_security`]}
|
value={settings[`${protocol}_security`]}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
onKeyPress={onFieldKeyPress}
|
onKeyPress={onFieldKeyPress}
|
||||||
onChange={onFieldChange}
|
onChange={onFieldChange}
|
||||||
|
|
@ -126,9 +123,9 @@ class AccountIMAPSettingsForm extends React.Component {
|
||||||
<span style={{paddingLeft: '20px', paddingTop: '10px'}}>
|
<span style={{paddingLeft: '20px', paddingTop: '10px'}}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id={`${protocol}_allow_insecure_ssl`}
|
id={`settings.${protocol}_allow_insecure_ssl`}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
checked={accountInfo[`${protocol}_allow_insecure_ssl`] || false}
|
checked={settings[`${protocol}_allow_insecure_ssl`] || false}
|
||||||
onKeyPress={onFieldKeyPress}
|
onKeyPress={onFieldKeyPress}
|
||||||
onChange={onFieldChange}
|
onChange={onFieldChange}
|
||||||
/>
|
/>
|
||||||
|
|
@ -141,13 +138,13 @@ class AccountIMAPSettingsForm extends React.Component {
|
||||||
renderFieldsForType(type) {
|
renderFieldsForType(type) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FormField field={`${type}_host`} title={"Server"} {...this.props} />
|
<FormField field={`settings.${type}_host`} title={"Server"} {...this.props} />
|
||||||
<div style={{textAlign: 'left'}}>
|
<div style={{textAlign: 'left'}}>
|
||||||
{this.renderPortDropdown(type)}
|
{this.renderPortDropdown(type)}
|
||||||
{this.renderSecurityDropdown(type)}
|
{this.renderSecurityDropdown(type)}
|
||||||
</div>
|
</div>
|
||||||
<FormField field={`${type}_username`} title={"Username"} {...this.props} />
|
<FormField field={`settings.${type}_username`} title={"Username"} {...this.props} />
|
||||||
<FormField field={`${type}_password`} title={"Password"} type="password" {...this.props} />
|
<FormField field={`settings.${type}_password`} title={"Password"} type="password" {...this.props} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,14 @@ import {RegExpUtils} from 'nylas-exports';
|
||||||
|
|
||||||
import OnboardingActions from './onboarding-actions';
|
import OnboardingActions from './onboarding-actions';
|
||||||
import CreatePageForForm from './decorators/create-page-for-form';
|
import CreatePageForForm from './decorators/create-page-for-form';
|
||||||
import {expandAccountInfoWithCommonSettings} from './onboarding-helpers';
|
import {expandAccountWithCommonSettings} from './onboarding-helpers';
|
||||||
import FormField from './form-field';
|
import FormField from './form-field';
|
||||||
|
|
||||||
class AccountBasicSettingsForm extends React.Component {
|
class AccountBasicSettingsForm extends React.Component {
|
||||||
static displayName = 'AccountBasicSettingsForm';
|
static displayName = 'AccountBasicSettingsForm';
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
accountInfo: React.PropTypes.object,
|
account: React.PropTypes.object,
|
||||||
errorFieldNames: React.PropTypes.array,
|
errorFieldNames: React.PropTypes.array,
|
||||||
submitting: React.PropTypes.bool,
|
submitting: React.PropTypes.bool,
|
||||||
onConnect: React.PropTypes.func,
|
onConnect: React.PropTypes.func,
|
||||||
|
|
@ -18,53 +18,52 @@ class AccountBasicSettingsForm extends React.Component {
|
||||||
onFieldKeyPress: React.PropTypes.func,
|
onFieldKeyPress: React.PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
static submitLabel = (accountInfo) => {
|
static submitLabel = (account) => {
|
||||||
return (accountInfo.type === 'imap') ? 'Continue' : 'Connect Account';
|
return (account.provider === 'imap') ? 'Continue' : 'Connect Account';
|
||||||
}
|
}
|
||||||
|
|
||||||
static titleLabel = (AccountType) => {
|
static titleLabel = (providerConfig) => {
|
||||||
return AccountType.title || `Add your ${AccountType.displayName} account`;
|
return providerConfig.title || `Add your ${providerConfig.displayName} account`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static subtitleLabel = () => {
|
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) => {
|
static validateAccount = (account) => {
|
||||||
const {email, password, name} = accountInfo;
|
|
||||||
const errorFieldNames = [];
|
const errorFieldNames = [];
|
||||||
let errorMessage = null;
|
let errorMessage = null;
|
||||||
|
|
||||||
if (!email || !password || !name) {
|
if (!account.emailAddress || !account.settings.imap_password || !account.name) {
|
||||||
return {errorMessage, errorFieldNames, populated: false};
|
return {errorMessage, errorFieldNames, populated: false};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!RegExpUtils.emailRegex().test(accountInfo.email)) {
|
if (!RegExpUtils.emailRegex().test(account.emailAddress)) {
|
||||||
errorFieldNames.push('email')
|
errorFieldNames.push('email')
|
||||||
errorMessage = "Please provide a valid email address."
|
errorMessage = "Please provide a valid email address."
|
||||||
}
|
}
|
||||||
if (!accountInfo.password) {
|
if (!account.name) {
|
||||||
errorFieldNames.push('password')
|
|
||||||
errorMessage = "Please provide a password for your account."
|
|
||||||
}
|
|
||||||
if (!accountInfo.name) {
|
|
||||||
errorFieldNames.push('name')
|
errorFieldNames.push('name')
|
||||||
errorMessage = "Please provide your 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};
|
return {errorMessage, errorFieldNames, populated: true};
|
||||||
}
|
}
|
||||||
|
|
||||||
submit() {
|
submit() {
|
||||||
const accountInfo = expandAccountInfoWithCommonSettings(this.props.accountInfo);
|
const account = expandAccountWithCommonSettings(this.props.account);
|
||||||
OnboardingActions.setAccountInfo(accountInfo);
|
OnboardingActions.setAccount(account);
|
||||||
if (this.props.accountInfo.type === 'imap') {
|
if (this.props.account.provider === 'imap') {
|
||||||
OnboardingActions.moveToPage('account-settings-imap');
|
OnboardingActions.moveToPage('account-settings-imap');
|
||||||
} else {
|
} 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
|
// we're calling exists on a component that won't have had it's state
|
||||||
// updated from the OnboardingStore change yet.
|
// updated from the OnboardingStore change yet.
|
||||||
this.props.onConnect(accountInfo);
|
this.props.onConnect(account);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,8 +71,8 @@ class AccountBasicSettingsForm extends React.Component {
|
||||||
return (
|
return (
|
||||||
<form className="settings">
|
<form className="settings">
|
||||||
<FormField field="name" title="Name" {...this.props} />
|
<FormField field="name" title="Name" {...this.props} />
|
||||||
<FormField field="email" title="Email" {...this.props} />
|
<FormField field="emailAddress" title="Email" {...this.props} />
|
||||||
<FormField field="password" title="Password" type="password" {...this.props} />
|
<FormField field="settings.imap_password" title="Password" type="password" {...this.props} />
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ export default class AuthenticatePage extends React.Component {
|
||||||
static displayName = "AuthenticatePage";
|
static displayName = "AuthenticatePage";
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
accountInfo: React.PropTypes.object,
|
account: React.PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
_src() {
|
_src() {
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,7 @@
|
||||||
margin-bottom:15px;
|
margin-bottom:15px;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
|
||||||
&.error {
|
&.error {
|
||||||
color: #A33;
|
color: #A33;
|
||||||
|
|
@ -116,6 +117,9 @@
|
||||||
form.settings {
|
form.settings {
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
|
span:last-child input {
|
||||||
|
margin-bottom:0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
input {
|
input {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
@ -169,10 +173,6 @@
|
||||||
text-align: right;
|
text-align: right;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
|
||||||
margin-top:8px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page.authenticate {
|
.page.authenticate {
|
||||||
|
|
@ -339,7 +339,7 @@
|
||||||
}
|
}
|
||||||
.twocol {
|
.twocol {
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.page.account-setup.google, .page.account-setup.AccountOnboardingSuccess {
|
.page.account-setup.google, .page.account-setup.AccountOnboardingSuccess {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
"main": "./lib/main",
|
"main": "./lib/main",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"serverUrl": {
|
"serverUrl": {
|
||||||
"development": "http://localhost:5100",
|
"development": "http://localhost:5101",
|
||||||
"staging": "https://link-staging.getmailspring.com",
|
"staging": "https://link-staging.getmailspring.com",
|
||||||
"production": "https://link.getmailspring.com"
|
"production": "https://link.getmailspring.com"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -194,12 +194,12 @@ export function getLatestMessageWithReminder(thread, messages) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getReminderLabel(reminderDate, {fromNow = false, shortFormat = false} = {}) {
|
export function getReminderLabel(reminderDate, {fromNow = false, shortFormat = false} = {}) {
|
||||||
const momentDate = DateUtils.futureDateFromString(reminderDate);
|
const momentDate = moment(reminderDate);
|
||||||
if (shortFormat) {
|
if (shortFormat) {
|
||||||
return momentDate ? `in ${momentDate.fromNow(true)}` : 'now'
|
return momentDate ? `in ${momentDate.fromNow(true)}` : 'now'
|
||||||
}
|
}
|
||||||
if (fromNow) {
|
if (fromNow) {
|
||||||
return momentDate ? `Reminder set for ${momentDate.fromNow(true)} from now` : `Reminder set`;
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import _ from 'underscore';
|
|
||||||
import NylasStore from 'nylas-store';
|
import NylasStore from 'nylas-store';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -7,7 +6,6 @@ import {
|
||||||
Actions,
|
Actions,
|
||||||
DatabaseStore,
|
DatabaseStore,
|
||||||
Message,
|
Message,
|
||||||
CategoryStore,
|
|
||||||
} from 'nylas-exports';
|
} from 'nylas-exports';
|
||||||
|
|
||||||
import SnoozeUtils from './snooze-utils'
|
import SnoozeUtils from './snooze-utils'
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "thread-sharing",
|
"name": "thread-sharing",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"serverUrl": {
|
"serverUrl": {
|
||||||
"development": "http://localhost:5100",
|
"development": "http://localhost:5101",
|
||||||
"staging": "https://share-staging.getmailspring.com",
|
"staging": "https://share-staging.getmailspring.com",
|
||||||
"production": "https://share.getmailspring.com"
|
"production": "https://share.getmailspring.com"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -288,18 +288,18 @@ export default class Application extends EventEmitter {
|
||||||
win.browserWindow.inspectElement(x, y);
|
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);
|
const onboarding = this.windowManager.get(WindowManager.ONBOARDING_WINDOW);
|
||||||
if (onboarding) {
|
if (onboarding) {
|
||||||
if (onboarding.browserWindow.webContents) {
|
if (onboarding.browserWindow.webContents) {
|
||||||
onboarding.browserWindow.webContents.send('set-account-type', accountType)
|
onboarding.browserWindow.webContents.send('set-account-provider', accountProvider)
|
||||||
}
|
}
|
||||||
onboarding.show();
|
onboarding.show();
|
||||||
onboarding.focus();
|
onboarding.focus();
|
||||||
} else {
|
} else {
|
||||||
this.windowManager.ensureWindow(WindowManager.ONBOARDING_WINDOW, {
|
this.windowManager.ensureWindow(WindowManager.ONBOARDING_WINDOW, {
|
||||||
title: "Add an Account",
|
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) => {
|
ipcMain.on('run-in-window', (event, params) => {
|
||||||
const sourceWindow = BrowserWindow.fromWebContents(event.sender);
|
const sourceWindow = BrowserWindow.fromWebContents(event.sender);
|
||||||
this._sourceWindows = this._sourceWindows || {};
|
this._sourceWindows = this._sourceWindows || {};
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,11 @@ The value of this attribute is always an array of other model objects.
|
||||||
Section: Database
|
Section: Database
|
||||||
*/
|
*/
|
||||||
export default class AttributeCollection extends Attribute {
|
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});
|
super({modelKey, jsonKey, queryable});
|
||||||
this.itemClass = itemClass;
|
this.itemClass = itemClass;
|
||||||
this.joinOnField = joinOnField;
|
this.joinOnField = joinOnField;
|
||||||
|
this.joinTableName = joinTableName;
|
||||||
this.joinQueryableBy = joinQueryableBy || [];
|
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.
|
// Public: Returns a {Matcher} for objects containing the provided value.
|
||||||
contains(val) {
|
contains(val) {
|
||||||
this._assertPresentAndQueryable('contains', val);
|
this._assertPresentAndQueryable('contains', val);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import {tableNameForJoin} from '../models/utils';
|
|
||||||
import LocalSearchQueryBackend from '../../services/search/search-query-backend-local'
|
import LocalSearchQueryBackend from '../../services/search/search-query-backend-local'
|
||||||
|
|
||||||
// https://www.sqlite.org/faq.html#q14
|
// https://www.sqlite.org/faq.html#q14
|
||||||
|
|
@ -115,7 +114,7 @@ class Matcher {
|
||||||
switch (this.comparator) {
|
switch (this.comparator) {
|
||||||
case 'contains':
|
case 'contains':
|
||||||
case 'containsAny': {
|
case 'containsAny': {
|
||||||
const joinTable = tableNameForJoin(klass, this.attr.itemClass);
|
const joinTable = this.attr.tableNameForJoinAgainst(klass);
|
||||||
const joinTableRef = this.joinTableRef();
|
const joinTableRef = this.joinTableRef();
|
||||||
return `INNER JOIN \`${joinTable}\` AS \`${joinTableRef}\` ON \`${joinTableRef}\`.\`id\` = \`${klass.name}\`.\`id\``;
|
return `INNER JOIN \`${joinTable}\` AS \`${joinTableRef}\` ON \`${joinTableRef}\`.\`id\` = \`${klass.name}\`.\`id\``;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@ export default class MailsyncBridge {
|
||||||
// Private
|
// Private
|
||||||
|
|
||||||
_launchClient(account, {force} = {}) {
|
_launchClient(account, {force} = {}) {
|
||||||
const fullAccountJSON = KeyManager.insertAccountSecrets(account.toJSON());
|
const fullAccountJSON = KeyManager.insertAccountSecrets(account).toJSON();
|
||||||
const identity = IdentityStore.identity();
|
const identity = IdentityStore.identity();
|
||||||
const id = account.id;
|
const id = account.id;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,9 @@ export default class ModelWithMetadata extends Model {
|
||||||
static attributes = Object.assign({}, Model.attributes, {
|
static attributes = Object.assign({}, Model.attributes, {
|
||||||
pluginMetadata: Attributes.Collection({
|
pluginMetadata: Attributes.Collection({
|
||||||
queryable: true,
|
queryable: true,
|
||||||
joinOnField: 'pluginId',
|
|
||||||
itemClass: PluginMetadata,
|
itemClass: PluginMetadata,
|
||||||
|
joinOnField: 'pluginId',
|
||||||
|
joinTableName: 'ModelPluginMetadata',
|
||||||
modelKey: 'pluginMetadata',
|
modelKey: 'pluginMetadata',
|
||||||
jsonKey: 'metadata',
|
jsonKey: 'metadata',
|
||||||
}),
|
}),
|
||||||
|
|
@ -70,7 +71,11 @@ export default class ModelWithMetadata extends Model {
|
||||||
if (!metadata) {
|
if (!metadata) {
|
||||||
return null;
|
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
|
// Private helpers
|
||||||
|
|
@ -88,7 +93,10 @@ export default class ModelWithMetadata extends Model {
|
||||||
metadata = new PluginMetadata({pluginId});
|
metadata = new PluginMetadata({pluginId});
|
||||||
this.pluginMetadata.push(metadata);
|
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;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -409,7 +409,7 @@ export default class ModelQuery {
|
||||||
_subselectSQL(returningMatcher, subselectMatchers, order, limit) {
|
_subselectSQL(returningMatcher, subselectMatchers, order, limit) {
|
||||||
const returningAttribute = returningMatcher.attribute()
|
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);
|
const wheres = subselectMatchers.map(c => c.whereSQL(this._klass)).filter(c => !!c);
|
||||||
|
|
||||||
let innerSQL = `SELECT \`id\` FROM \`${table}\` WHERE ${wheres.join(' AND ')} ${order} ${limit}`;
|
let innerSQL = `SELECT \`id\` FROM \`${table}\` WHERE ${wheres.join(' AND ')} ${order} ${limit}`;
|
||||||
|
|
|
||||||
|
|
@ -159,9 +159,6 @@ Utils =
|
||||||
return false unless id and _.isString(id)
|
return false unless id and _.isString(id)
|
||||||
id[0..5] is 'local-'
|
id[0..5] is 'local-'
|
||||||
|
|
||||||
tableNameForJoin: (primaryKlass, secondaryKlass) ->
|
|
||||||
"#{primaryKlass.name}#{secondaryKlass.name}"
|
|
||||||
|
|
||||||
imageNamed: (fullname, resourcePath) ->
|
imageNamed: (fullname, resourcePath) ->
|
||||||
[name, ext] = fullname.split('.')
|
[name, ext] = fullname.split('.')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,14 +25,6 @@ export function rootURLForServer(server) {
|
||||||
production: "https://id.getmailspring.com",
|
production: "https://id.getmailspring.com",
|
||||||
}[env];
|
}[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");
|
throw new Error("rootURLForServer: You must provide a valid `server` value");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -203,28 +203,29 @@ class AccountStore extends NylasStore {
|
||||||
this._save()
|
this._save()
|
||||||
}
|
}
|
||||||
|
|
||||||
addAccountFromJSON = (json) => {
|
addAccount = (account) => {
|
||||||
if (!json.emailAddress || !json.provider) {
|
if (!account.emailAddress || !account.provider || !(account instanceof Account)) {
|
||||||
throw new Error(`Returned account data is invalid: ${JSON.stringify(json)}`)
|
throw new Error(`Returned account data is invalid: ${JSON.stringify(account)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// send the account JSON and cloud token to the KeyManager,
|
// send the account JSON and cloud token to the KeyManager,
|
||||||
// which gives us back a version with no secrets.
|
// which gives us back a version with no secrets.
|
||||||
const cleanJSON = KeyManager.extractAccountSecrets(json);
|
const cleanAccount = KeyManager.extractAccountSecrets(account);
|
||||||
|
|
||||||
this._loadAccounts();
|
this._loadAccounts();
|
||||||
|
|
||||||
const existingIdx = this._accounts.findIndex((a) =>
|
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) {
|
if (existingIdx === -1) {
|
||||||
const account = (new Account()).fromJSON(cleanJSON);
|
this._accounts.push(cleanAccount);
|
||||||
this._accounts.push(account);
|
|
||||||
} else {
|
} else {
|
||||||
const account = this._accounts[existingIdx];
|
const existing = this._accounts[existingIdx];
|
||||||
account.syncState = Account.SYNC_STATE_OK;
|
existing.syncState = Account.SYNC_STATE_OK;
|
||||||
account.fromJSON(cleanJSON);
|
existing.name = cleanAccount.name;
|
||||||
|
existing.emailAddress = cleanAccount.emailAddress;
|
||||||
|
existing.settings = cleanAccount.settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._save();
|
this._save();
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,11 @@ export default class SyncbackMetadataTask extends Task {
|
||||||
|
|
||||||
constructor(data = {}) {
|
constructor(data = {}) {
|
||||||
super(data);
|
super(data);
|
||||||
|
|
||||||
|
if (data.value && data.value.expiration) {
|
||||||
|
data.value.expiration = Math.round(new Date(data.value.expiration).getTime() / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
if (data.model) {
|
if (data.model) {
|
||||||
this.modelId = data.model.id;
|
this.modelId = data.model.id;
|
||||||
this.modelClassName = data.model.constructor.name.toLowerCase();
|
this.modelClassName = data.model.constructor.name.toLowerCase();
|
||||||
|
|
|
||||||
|
|
@ -33,27 +33,27 @@ class KeyManager {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
extractAccountSecrets(accountJSON) {
|
extractAccountSecrets(account) {
|
||||||
const next = Object.assign({}, accountJSON);
|
const next = account.clone();
|
||||||
this._try(() => {
|
this._try(() => {
|
||||||
const keys = this._getKeyHash();
|
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;
|
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;
|
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;
|
delete next.settings.refresh_token;
|
||||||
return this._writeKeyHash(keys);
|
return this._writeKeyHash(keys);
|
||||||
});
|
});
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
insertAccountSecrets(accountJSON) {
|
insertAccountSecrets(account) {
|
||||||
const next = Object.assign({}, accountJSON);
|
const next = account.clone();
|
||||||
const keys = this._getKeyHash();
|
const keys = this._getKeyHash();
|
||||||
next.settings.imap_password = keys[`${accountJSON.emailAddress}-imap`];
|
next.settings.imap_password = keys[`${account.emailAddress}-imap`];
|
||||||
next.settings.smtp_password = keys[`${accountJSON.emailAddress}-smtp`];
|
next.settings.smtp_password = keys[`${account.emailAddress}-smtp`];
|
||||||
next.settings.refresh_token = keys[`${accountJSON.emailAddress}-refresh-token`];
|
next.settings.refresh_token = keys[`${account.emailAddress}-refresh-token`];
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,12 +53,10 @@ export default class MailsyncProcess extends EventEmitter {
|
||||||
const env = {
|
const env = {
|
||||||
CONFIG_DIR_PATH: this.configDirPath,
|
CONFIG_DIR_PATH: this.configDirPath,
|
||||||
IDENTITY_SERVER: 'unknown',
|
IDENTITY_SERVER: 'unknown',
|
||||||
ACCOUNTS_SERVER: 'unknown',
|
|
||||||
};
|
};
|
||||||
if (process.type === 'renderer') {
|
if (process.type === 'renderer') {
|
||||||
const rootURLForServer = require('./flux/nylas-api-request').rootURLForServer;
|
const rootURLForServer = require('./flux/nylas-api-request').rootURLForServer;
|
||||||
env.IDENTITY_SERVER = rootURLForServer('identity');
|
env.IDENTITY_SERVER = rootURLForServer('identity');
|
||||||
env.ACCOUNTS_SERVER = rootURLForServer('accounts');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._proc = spawn(this.binaryPath, [`--mode`, mode], {env});
|
this._proc = spawn(this.binaryPath, [`--mode`, mode], {env});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue