mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-21 07:46:06 +08:00
Refactor storage of secrets to include imap/smtp pass, pass identity to C++ workers
This commit is contained in:
parent
c71d8fad72
commit
b4f89d90d7
|
@ -4,6 +4,7 @@ import crypto from 'crypto';
|
||||||
import {CommonProviderSettings} from 'imap-provider-settings';
|
import {CommonProviderSettings} from 'imap-provider-settings';
|
||||||
import {
|
import {
|
||||||
NylasAPIRequest,
|
NylasAPIRequest,
|
||||||
|
IdentityStore,
|
||||||
RegExpUtils,
|
RegExpUtils,
|
||||||
MailsyncProcess,
|
MailsyncProcess,
|
||||||
} from 'nylas-exports';
|
} from 'nylas-exports';
|
||||||
|
@ -112,7 +113,7 @@ export async function runAuthValidation(accountInfo) {
|
||||||
// Send the form data directly to Nylas to get code
|
// Send the form data directly to Nylas to get code
|
||||||
// If this succeeds, send the received code to N1 server to register the account
|
// If this succeeds, send the received code to N1 server to register the account
|
||||||
// Otherwise process the error message from the server and highlight UI as needed
|
// Otherwise process the error message from the server and highlight UI as needed
|
||||||
const proc = new MailsyncProcess(NylasEnv.getLoadSettings(), data);
|
const proc = new MailsyncProcess(NylasEnv.getLoadSettings(), data, IdentityStore.identity());
|
||||||
const {account} = await proc.test();
|
const {account} = await proc.test();
|
||||||
|
|
||||||
delete data.id;
|
delete data.id;
|
||||||
|
|
|
@ -55,7 +55,7 @@ class SearchMailboxPerspective extends MailboxPerspective {
|
||||||
|
|
||||||
tasksForRemovingItems(threads) {
|
tasksForRemovingItems(threads) {
|
||||||
return TaskFactory.tasksForThreadsByAccountId(threads, (accountThreads, accountId) => {
|
return TaskFactory.tasksForThreadsByAccountId(threads, (accountThreads, accountId) => {
|
||||||
const account = AccountStore.accountForId(accountId)
|
const account = AccountStore.accountForId(accountId);
|
||||||
const dest = account.preferredRemovalDestination();
|
const dest = account.preferredRemovalDestination();
|
||||||
|
|
||||||
if (dest instanceof Folder) {
|
if (dest instanceof Folder) {
|
||||||
|
|
|
@ -59,7 +59,7 @@ class ThreadSearchBar extends Component {
|
||||||
if (this.props.perspective.isInbox()) {
|
if (this.props.perspective.isInbox()) {
|
||||||
return 'Search all email';
|
return 'Search all email';
|
||||||
}
|
}
|
||||||
return `Search ${this.props.perspective.name}`;
|
return `Search ${this.props.perspective.name || ""}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import Actions from './actions';
|
||||||
import Utils from './models/utils';
|
import Utils from './models/utils';
|
||||||
|
|
||||||
let AccountStore = null;
|
let AccountStore = null;
|
||||||
|
let IdentityStore = null;
|
||||||
let Task = null;
|
let Task = null;
|
||||||
|
|
||||||
export default class MailsyncBridge {
|
export default class MailsyncBridge {
|
||||||
|
@ -23,8 +24,16 @@ export default class MailsyncBridge {
|
||||||
this.clients = {};
|
this.clients = {};
|
||||||
|
|
||||||
Task = require('./tasks/task').default; //eslint-disable-line
|
Task = require('./tasks/task').default; //eslint-disable-line
|
||||||
|
|
||||||
|
IdentityStore = require('./stores/identity-store').default;
|
||||||
|
IdentityStore.listen(() => {
|
||||||
|
Object.values(this.clients).each(c => c.kill());
|
||||||
|
this.ensureClients();
|
||||||
|
}, this);
|
||||||
|
|
||||||
AccountStore = require('./stores/account-store').default; //eslint-disable-line
|
AccountStore = require('./stores/account-store').default; //eslint-disable-line
|
||||||
AccountStore.listen(this.ensureClients, this);
|
AccountStore.listen(this.ensureClients, this);
|
||||||
|
|
||||||
this.ensureClients();
|
this.ensureClients();
|
||||||
|
|
||||||
NylasEnv.onBeforeUnload(this.onBeforeUnload);
|
NylasEnv.onBeforeUnload(this.onBeforeUnload);
|
||||||
|
@ -33,6 +42,7 @@ export default class MailsyncBridge {
|
||||||
ensureClients() {
|
ensureClients() {
|
||||||
const toLaunch = [];
|
const toLaunch = [];
|
||||||
const clientsToStop = Object.assign({}, this.clients);
|
const clientsToStop = Object.assign({}, this.clients);
|
||||||
|
const identity = IdentityStore.identity();
|
||||||
|
|
||||||
for (const acct of AccountStore.accounts()) {
|
for (const acct of AccountStore.accounts()) {
|
||||||
if (!this.clients[acct.id]) {
|
if (!this.clients[acct.id]) {
|
||||||
|
@ -47,7 +57,7 @@ export default class MailsyncBridge {
|
||||||
}
|
}
|
||||||
|
|
||||||
toLaunch.forEach((acct) => {
|
toLaunch.forEach((acct) => {
|
||||||
const client = new MailsyncProcess(NylasEnv.getLoadSettings(), acct);
|
const client = new MailsyncProcess(NylasEnv.getLoadSettings(), identity, acct);
|
||||||
client.sync();
|
client.sync();
|
||||||
client.on('deltas', this.onIncomingMessages);
|
client.on('deltas', this.onIncomingMessages);
|
||||||
client.on('close', () => {
|
client.on('close', () => {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
/* eslint global-require: 0 */
|
||||||
import {APIError} from './errors'
|
import {APIError} from './errors'
|
||||||
import IdentityStore from './stores/identity-store'
|
|
||||||
|
|
||||||
// A 0 code is when an error returns without a status code, like "ESOCKETTIMEDOUT"
|
// A 0 code is when an error returns without a status code, like "ESOCKETTIMEDOUT"
|
||||||
export const TimeoutErrorCodes = [0, 408, "ETIMEDOUT", "ESOCKETTIMEDOUT", "ECONNRESET", "ENETDOWN", "ENETUNREACH"]
|
export const TimeoutErrorCodes = [0, 408, "ETIMEDOUT", "ESOCKETTIMEDOUT", "ECONNRESET", "ENETDOWN", "ENETUNREACH"]
|
||||||
|
@ -7,18 +7,19 @@ export const PermanentErrorCodes = [400, 401, 402, 403, 404, 405, 429, 500, "ENO
|
||||||
export const CanceledErrorCodes = [-123, "ECONNABORTED"]
|
export const CanceledErrorCodes = [-123, "ECONNABORTED"]
|
||||||
export const SampleTemporaryErrorCode = 504
|
export const SampleTemporaryErrorCode = 504
|
||||||
|
|
||||||
|
let IdentityStore = null;
|
||||||
|
|
||||||
// server option
|
// server option
|
||||||
|
|
||||||
export function rootURLForServer(server) {
|
export function rootURLForServer(server) {
|
||||||
const env = NylasEnv.config.get('env');
|
const env = NylasEnv.config.get('env');
|
||||||
|
|
||||||
if (!['development', 'local', 'staging', 'production'].includes(env)) {
|
if (!['development', 'staging', 'production'].includes(env)) {
|
||||||
throw new Error(`rootURLForServer: ${env} is not a valid environment.`);
|
throw new Error(`rootURLForServer: ${env} is not a valid environment.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (server === 'identity') {
|
if (server === 'identity') {
|
||||||
return {
|
return {
|
||||||
local: "http://localhost:5101",
|
|
||||||
development: "http://localhost:5101",
|
development: "http://localhost:5101",
|
||||||
staging: "https://id-staging.nylas.com",
|
staging: "https://id-staging.nylas.com",
|
||||||
production: "https://id.nylas.com",
|
production: "https://id.nylas.com",
|
||||||
|
@ -26,7 +27,6 @@ export function rootURLForServer(server) {
|
||||||
}
|
}
|
||||||
if (server === 'accounts') {
|
if (server === 'accounts') {
|
||||||
return {
|
return {
|
||||||
local: "http://localhost:5100",
|
|
||||||
development: "http://localhost:5100",
|
development: "http://localhost:5100",
|
||||||
staging: "https://accounts-staging.nylas.com",
|
staging: "https://accounts-staging.nylas.com",
|
||||||
production: "https://accounts.nylas.com",
|
production: "https://accounts.nylas.com",
|
||||||
|
@ -47,7 +47,9 @@ export async function makeRequest(options) {
|
||||||
|
|
||||||
if (!options.auth) {
|
if (!options.auth) {
|
||||||
if (options.server === 'identity') {
|
if (options.server === 'identity') {
|
||||||
options.headers.set('Authorization', `Basic ${btoa(`${IdentityStore._identity.token}:`)}`)
|
IdentityStore = IdentityStore || require('./stores/identity-store').default;
|
||||||
|
const username = IdentityStore.identity().token;
|
||||||
|
options.headers.set('Authorization', `Basic ${btoa(`${username}:`)}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,6 @@ class AccountStore extends NylasStore {
|
||||||
this.listenTo(Actions.removeAccount, this._onRemoveAccount)
|
this.listenTo(Actions.removeAccount, this._onRemoveAccount)
|
||||||
this.listenTo(Actions.updateAccount, this._onUpdateAccount)
|
this.listenTo(Actions.updateAccount, this._onUpdateAccount)
|
||||||
this.listenTo(Actions.reorderAccount, this._onReorderAccount)
|
this.listenTo(Actions.reorderAccount, this._onReorderAccount)
|
||||||
this.listenTo(Actions.apiAuthError, this._onAPIAuthError)
|
|
||||||
|
|
||||||
NylasEnv.config.onDidChange(configVersionKey, async (change) => {
|
NylasEnv.config.onDidChange(configVersionKey, async (change) => {
|
||||||
// If we already have this version of the accounts config, it means we
|
// If we already have this version of the accounts config, it means we
|
||||||
|
@ -69,29 +68,9 @@ class AccountStore extends NylasStore {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
_onAPIAuthError = (apiError, apiOptions) => {
|
|
||||||
// Prevent /auth errors from presenting auth failure notices
|
|
||||||
const apiToken = apiOptions.auth.user
|
|
||||||
if (!apiToken) {
|
|
||||||
return Promise.resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
const account = this.accounts().find((acc) =>
|
|
||||||
this.tokensForAccountId(acc.id) === apiToken
|
|
||||||
);
|
|
||||||
|
|
||||||
if (account) {
|
|
||||||
const n1CloudState = Account.N1_CLOUD_STATE_AUTH_FAILED
|
|
||||||
this._onUpdateAccount(account.id, {n1CloudState})
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
_loadAccounts = () => {
|
_loadAccounts = () => {
|
||||||
try {
|
try {
|
||||||
this._caches = {}
|
this._caches = {}
|
||||||
this._tokens = this._tokens || {};
|
|
||||||
this._version = NylasEnv.config.get(configVersionKey) || 0
|
this._version = NylasEnv.config.get(configVersionKey) || 0
|
||||||
|
|
||||||
const oldAccountIds = this._accounts ? this._accounts.map(a => a.id) : [];
|
const oldAccountIds = this._accounts ? this._accounts.map(a => a.id) : [];
|
||||||
|
@ -105,24 +84,15 @@ class AccountStore extends NylasStore {
|
||||||
// we really have to (i.e. we're loading a new Account)
|
// we really have to (i.e. we're loading a new Account)
|
||||||
const addedAccountIds = _.difference(accountIds, oldAccountIds);
|
const addedAccountIds = _.difference(accountIds, oldAccountIds);
|
||||||
const addedAccounts = this._accounts.filter((a) => addedAccountIds.includes(a.id));
|
const addedAccounts = this._accounts.filter((a) => addedAccountIds.includes(a.id));
|
||||||
const removedAccountIds = _.difference(oldAccountIds, accountIds);
|
|
||||||
const removedAccounts = this._accounts.filter((a) => removedAccountIds.includes(a.id));
|
|
||||||
|
|
||||||
// Run a few checks on account consistency. We want to display useful error
|
// Run a few checks on account consistency. We want to display useful error
|
||||||
// messages and these can result in very strange exceptions downstream otherwise.
|
// messages and these can result in very strange exceptions downstream otherwise.
|
||||||
this._enforceAccountsValidity()
|
this._enforceAccountsValidity()
|
||||||
|
|
||||||
for (const account of addedAccounts) {
|
for (const account of addedAccounts) {
|
||||||
this._tokens[account.emailAddress] = this._tokens[account.id] = KeyManager.getPassword(`${account.emailAddress}`);
|
account.settings.imap_password = KeyManager.getPassword(`${account.emailAddress}-imap`);
|
||||||
}
|
account.settings.smtp_password = KeyManager.getPassword(`${account.emailAddress}-smtp`);
|
||||||
for (const removedAccount of removedAccounts) {
|
account.cloudToken = KeyManager.getPassword(`${account.emailAddress}-cloud`);
|
||||||
const {id, emailAddress} = removedAccount
|
|
||||||
if (this._tokens[id]) {
|
|
||||||
delete this._tokens[id]
|
|
||||||
}
|
|
||||||
if (this._tokens[emailAddress]) {
|
|
||||||
delete this._tokens[emailAddress]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
NylasEnv.reportError(error)
|
NylasEnv.reportError(error)
|
||||||
|
@ -172,11 +142,16 @@ class AccountStore extends NylasStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
_save = () => {
|
_save = () => {
|
||||||
this._version += 1
|
this._version += 1;
|
||||||
const configAccounts = this._accounts.map(a => a.toJSON())
|
const configAccounts = this._accounts.map(a => a.toJSON());
|
||||||
configAccounts.forEach(a => delete a.sync_error)
|
configAccounts.forEach(a => {
|
||||||
NylasEnv.config.set(configAccountsKey, configAccounts)
|
delete a.sync_error
|
||||||
NylasEnv.config.set(configVersionKey, this._version)
|
delete a.settings.imap_password
|
||||||
|
delete a.settings.smtp_password
|
||||||
|
delete a.cloudToken
|
||||||
|
});
|
||||||
|
NylasEnv.config.set(configAccountsKey, configAccounts);
|
||||||
|
NylasEnv.config.set(configVersionKey, this._version);
|
||||||
this._trigger()
|
this._trigger()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,7 +178,7 @@ class AccountStore extends NylasStore {
|
||||||
_onRemoveAccount = (id) => {
|
_onRemoveAccount = (id) => {
|
||||||
const account = this._accounts.find(a => a.id === id);
|
const account = this._accounts.find(a => a.id === id);
|
||||||
if (!account) return
|
if (!account) return
|
||||||
KeyManager.deletePassword(account.emailAddress)
|
KeyManager.deletePassword(account.id)
|
||||||
|
|
||||||
this._caches = {}
|
this._caches = {}
|
||||||
|
|
||||||
|
@ -239,29 +214,28 @@ class AccountStore extends NylasStore {
|
||||||
this._save()
|
this._save()
|
||||||
}
|
}
|
||||||
|
|
||||||
addAccountFromJSON = (json, cloudToken) => {
|
addAccountFromJSON = (json) => {
|
||||||
if (!json.emailAddress || !json.provider) {
|
if (!json.emailAddress || !json.provider) {
|
||||||
console.error("Returned account data is invalid", json)
|
throw new Error(`Returned account data is invalid: ${JSON.stringify(json)}`)
|
||||||
console.log(JSON.stringify(json))
|
|
||||||
throw new Error("Returned account data is invalid")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._loadAccounts()
|
this._loadAccounts()
|
||||||
|
|
||||||
this._tokens[json.id] = cloudToken;
|
KeyManager.replacePassword(`${json.emailAddress}-cloud`, json.cloudToken);
|
||||||
KeyManager.replacePassword(`${json.emailAddress}`, cloudToken)
|
KeyManager.replacePassword(`${json.emailAddress}-imap`, json.settings.imap_password);
|
||||||
|
KeyManager.replacePassword(`${json.emailAddress}-smtp`, json.settings.smtp_passwowrd);
|
||||||
|
|
||||||
const existingIdx = this._accounts.findIndex((a) =>
|
const existingIdx = this._accounts.findIndex((a) =>
|
||||||
a.id === json.id || a.emailAddress === json.emailAddress
|
a.id === json.id || a.emailAddress === json.emailAddress
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existingIdx === -1) {
|
if (existingIdx === -1) {
|
||||||
const account = (new Account()).fromJSON(json)
|
const account = (new Account()).fromJSON(json);
|
||||||
this._accounts.push(account)
|
this._accounts.push(account);
|
||||||
} else {
|
} else {
|
||||||
const account = this._accounts[existingIdx]
|
const account = this._accounts[existingIdx];
|
||||||
account.syncState = Account.SYNC_STATE_RUNNING
|
account.syncState = Account.SYNC_STATE_RUNNING;
|
||||||
account.fromJSON(json)
|
account.fromJSON(json);
|
||||||
// Restart the connection in case account credentials have changed
|
// Restart the connection in case account credentials have changed
|
||||||
// todo bg
|
// todo bg
|
||||||
}
|
}
|
||||||
|
@ -349,11 +323,6 @@ class AccountStore extends NylasStore {
|
||||||
current() {
|
current() {
|
||||||
throw new Error("AccountStore.current() has been deprecated.")
|
throw new Error("AccountStore.current() has been deprecated.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private: This method is going away soon, do not rely on it.
|
|
||||||
tokenForAccountId(id) {
|
|
||||||
return this._tokens[id]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new AccountStore()
|
export default new AccountStore()
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
/* eslint global-require: 0 */
|
/* eslint global-require: 0 */
|
||||||
|
|
||||||
|
/*
|
||||||
|
Warning! This file is imported from the main process as well as the renderer process
|
||||||
|
*/
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {EventEmitter} from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
let Utils = null;
|
let Utils = null;
|
||||||
|
|
||||||
const LocalizedErrorStrings = {
|
const LocalizedErrorStrings = {
|
||||||
ErrorConnection: "Connection Error",
|
ErrorConnection: "Connection Error - Check that your internet connection is active.",
|
||||||
ErrorInvalidAccount: "This account is invalid, or does not have an inbox or all folder.",
|
ErrorInvalidAccount: "This account is invalid, or does not have an inbox or all folder.",
|
||||||
ErrorTLSNotAvailable: "TLS Not Available",
|
ErrorTLSNotAvailable: "TLS Not Available",
|
||||||
ErrorParse: "Parsing Error",
|
ErrorParse: "Parsing Error",
|
||||||
|
@ -27,23 +31,32 @@ const LocalizedErrorStrings = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class MailsyncProcess extends EventEmitter {
|
export default class MailsyncProcess extends EventEmitter {
|
||||||
constructor({configDirPath, resourcePath}, account) {
|
constructor({configDirPath, resourcePath}, account, identity) {
|
||||||
super();
|
super();
|
||||||
this.configDirPath = configDirPath;
|
this.configDirPath = configDirPath;
|
||||||
this.account = account;
|
this.account = account;
|
||||||
|
this.identity = identity;
|
||||||
this.binaryPath = path.join(resourcePath, 'MailSync').replace('app.asar', 'app.asar.unpacked');
|
this.binaryPath = path.join(resourcePath, 'MailSync').replace('app.asar', 'app.asar.unpacked');
|
||||||
this._proc = null;
|
this._proc = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_spawnProcess(mode) {
|
_spawnProcess(mode) {
|
||||||
this._proc = spawn(this.binaryPath, [`--mode`, mode], {
|
const env = {
|
||||||
env: {
|
CONFIG_DIR_PATH: this.configDirPath,
|
||||||
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});
|
||||||
if (this.account) {
|
if (this.account) {
|
||||||
this._proc.stdout.once('data', () => {
|
this._proc.stdout.once('data', () => {
|
||||||
this._proc.stdin.write(`${JSON.stringify(this.account)}\n`);
|
this._proc.stdin.write(`${JSON.stringify(this.account)}\n`);
|
||||||
|
this._proc.stdin.write(`${JSON.stringify(this.identity)}\n`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ class N1CloudAPI {
|
||||||
|
|
||||||
_onConfigChanged = () => {
|
_onConfigChanged = () => {
|
||||||
const env = NylasEnv.config.get('env')
|
const env = NylasEnv.config.get('env')
|
||||||
if (['development', 'local'].includes(env)) {
|
if (env === 'development') {
|
||||||
this.APIRoot = "http://lvh.me:5100";
|
this.APIRoot = "http://lvh.me:5100";
|
||||||
} else if (env === 'staging') {
|
} else if (env === 'staging') {
|
||||||
this.APIRoot = "https://n1-staging.nylas.com";
|
this.APIRoot = "https://n1-staging.nylas.com";
|
||||||
|
|
|
@ -39,14 +39,14 @@ class WindowTitle extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.unlisten = NylasEnv.onWindowPropsReceived(() =>
|
this.disposable = NylasEnv.onWindowPropsReceived(() =>
|
||||||
this.setState(NylasEnv.getLoadSettings())
|
this.setState(NylasEnv.getLoadSettings())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
if (this.unlisten) {
|
if (this.disposable) {
|
||||||
this.unlisten();
|
this.disposable.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue