mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-03-01 10:33:14 +08:00
Gmail auth at http://localhost:5100/auth/gmail
This commit is contained in:
parent
18ad15937f
commit
69e87cbf49
5 changed files with 235 additions and 71 deletions
|
@ -8,6 +8,7 @@
|
|||
"author": "Nylas",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"googleapis": "^9.0.0",
|
||||
"hapi": "13.4.1",
|
||||
"hapi-auth-basic": "4.2.0",
|
||||
"hapi-swagger": "6.1.0",
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
const Joi = require('joi');
|
||||
const _ = require('underscore');
|
||||
const google = require('googleapis');
|
||||
const OAuth2 = google.auth.OAuth2;
|
||||
|
||||
const Serialization = require('../serialization');
|
||||
const {
|
||||
|
@ -8,6 +10,19 @@ const {
|
|||
SyncPolicy,
|
||||
} = require('nylas-core');
|
||||
|
||||
// TODO: Move these to config somehow / somewhere
|
||||
const CLIENT_ID = '271342407743-nibas08fua1itr1utq9qjladbkv3esdm.apps.googleusercontent.com';
|
||||
const CLIENT_SECRET = 'WhmxErj-ei6vJXLocNhBbfBF';
|
||||
const REDIRECT_URL = 'http://localhost:5100/auth/gmail/oauthcallback';
|
||||
|
||||
const SCOPES = [
|
||||
'https://www.googleapis.com/auth/userinfo.email', // email address
|
||||
'https://www.googleapis.com/auth/userinfo.profile', // G+ profile
|
||||
'https://mail.google.com/', // email
|
||||
'https://www.google.com/m8/feeds', // contacts
|
||||
'https://www.googleapis.com/auth/calendar', // calendar
|
||||
];
|
||||
|
||||
const imapSmtpSettings = Joi.object().keys({
|
||||
imap_host: [Joi.string().ip().required(), Joi.string().hostname().required()],
|
||||
imap_port: Joi.number().integer().required(),
|
||||
|
@ -26,6 +41,28 @@ const exchangeSettings = Joi.object().keys({
|
|||
eas_server_host: [Joi.string().ip().required(), Joi.string().hostname().required()],
|
||||
}).required();
|
||||
|
||||
const buildAccountWith = ({name, email, settings, credentials}) => {
|
||||
return DatabaseConnector.forShared().then((db) => {
|
||||
const {AccountToken, Account} = db;
|
||||
|
||||
const account = Account.build({
|
||||
name: name,
|
||||
emailAddress: email,
|
||||
syncPolicy: SyncPolicy.defaultPolicy(),
|
||||
connectionSettings: settings,
|
||||
})
|
||||
account.setCredentials(credentials);
|
||||
|
||||
return account.save().then((saved) =>
|
||||
AccountToken.create({
|
||||
AccountId: saved.id,
|
||||
}).then((token) =>
|
||||
Promise.resolve({account: saved, token: token})
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = (server) => {
|
||||
server.route({
|
||||
method: 'POST',
|
||||
|
@ -54,48 +91,118 @@ module.exports = (server) => {
|
|||
},
|
||||
},
|
||||
handler: (request, reply) => {
|
||||
const dbStub = {};
|
||||
const connectionChecks = [];
|
||||
const {settings, email, provider, name} = request.payload;
|
||||
|
||||
if (provider === 'imap') {
|
||||
const dbStub = {};
|
||||
const conn = new IMAPConnection(dbStub, settings);
|
||||
connectionChecks.push(conn.connect())
|
||||
connectionChecks.push(new IMAPConnection(dbStub, settings).connect())
|
||||
}
|
||||
|
||||
Promise.all(connectionChecks).then(() => {
|
||||
DatabaseConnector.forShared().then((db) => {
|
||||
const {AccountToken, Account} = db;
|
||||
|
||||
const account = Account.build({
|
||||
name: name,
|
||||
emailAddress: email,
|
||||
syncPolicy: SyncPolicy.defaultPolicy(),
|
||||
connectionSettings: _.pick(settings, [
|
||||
'imap_host', 'imap_port',
|
||||
'smtp_host', 'smtp_port',
|
||||
'ssl_required',
|
||||
]),
|
||||
})
|
||||
account.setCredentials(_.pick(settings, [
|
||||
buildAccountWith({
|
||||
name,
|
||||
email,
|
||||
settings: _.pick(settings, [
|
||||
'imap_host', 'imap_port',
|
||||
'smtp_host', 'smtp_port',
|
||||
'ssl_required',
|
||||
]),
|
||||
credentials: _.pick(settings, [
|
||||
'imap_username', 'imap_password',
|
||||
'smtp_username', 'smtp_password',
|
||||
]));
|
||||
account.save().then((saved) =>
|
||||
AccountToken.create({
|
||||
AccountId: saved.id,
|
||||
}).then((accountToken) => {
|
||||
const response = saved.toJSON();
|
||||
response.token = accountToken.value;
|
||||
reply(Serialization.jsonStringify(response));
|
||||
})
|
||||
);
|
||||
]),
|
||||
})
|
||||
})
|
||||
.then(({account, token}) => {
|
||||
const response = account.toJSON();
|
||||
response.token = token.value;
|
||||
reply(Serialization.jsonStringify(response));
|
||||
})
|
||||
.catch((err) => {
|
||||
// TODO: Lots more of this
|
||||
reply({error: err.toString()});
|
||||
})
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/auth/gmail',
|
||||
config: {
|
||||
description: 'Redirects to Gmail OAuth',
|
||||
notes: 'Notes go here',
|
||||
tags: ['accounts'],
|
||||
auth: false,
|
||||
},
|
||||
handler: (request, reply) => {
|
||||
const oauthClient = new OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URL);
|
||||
reply.redirect(oauthClient.generateAuthUrl({
|
||||
access_type: 'offline',
|
||||
prompt: 'consent',
|
||||
scope: SCOPES,
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/auth/gmail/oauthcallback',
|
||||
config: {
|
||||
description: 'Authenticates a new account.',
|
||||
notes: 'Notes go here',
|
||||
tags: ['accounts'],
|
||||
auth: false,
|
||||
validate: {
|
||||
query: {
|
||||
code: Joi.string().required(),
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: (request, reply) => {
|
||||
const oauthClient = new OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URL);
|
||||
oauthClient.getToken(request.query.code, (err, tokens) => {
|
||||
if (err) {
|
||||
reply(err.message).code(400);
|
||||
return;
|
||||
}
|
||||
oauthClient.setCredentials(tokens);
|
||||
google.oauth2({version: 'v2', auth: oauthClient}).userinfo.get((error, profile) => {
|
||||
if (error) {
|
||||
reply(error.message).code(400);
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = {
|
||||
imap_username: profile.email,
|
||||
imap_host: 'imap.gmail.com',
|
||||
imap_port: 993,
|
||||
ssl_required: true,
|
||||
}
|
||||
const credentials = {
|
||||
access_token: tokens.access_token,
|
||||
refresh_token: tokens.refresh_token,
|
||||
client_id: CLIENT_ID,
|
||||
client_secret: CLIENT_SECRET,
|
||||
}
|
||||
|
||||
Promise.all([
|
||||
new IMAPConnection({}, Object.assign({}, settings, credentials)).connect(),
|
||||
])
|
||||
.then(() =>
|
||||
buildAccountWith({name: profile.name, email: profile.email, settings, credentials})
|
||||
)
|
||||
.then(({account, token}) => {
|
||||
const response = account.toJSON();
|
||||
response.token = token.value;
|
||||
reply(Serialization.jsonStringify(response));
|
||||
})
|
||||
.catch((connectionErr) => {
|
||||
// TODO: Lots more of this
|
||||
reply({error: connectionErr.toString()});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
const Imap = require('imap');
|
||||
const EventEmitter = require('events');
|
||||
const xoauth2 = require("xoauth2");
|
||||
const _ = require('underscore');
|
||||
|
||||
const Capabilities = {
|
||||
Gmail: 'X-GM-EXT-1',
|
||||
|
@ -18,69 +20,114 @@ class IMAPConnection extends EventEmitter {
|
|||
this._db = db;
|
||||
this._queue = [];
|
||||
this._current = null;
|
||||
this._imap = Promise.promisifyAll(new Imap({
|
||||
host: settings.imap_host,
|
||||
port: settings.imap_port,
|
||||
user: settings.imap_username,
|
||||
password: settings.imap_password,
|
||||
tls: settings.ssl_required,
|
||||
}));
|
||||
|
||||
this._imap.once('end', () => {
|
||||
console.log('Connection ended');
|
||||
});
|
||||
|
||||
this._imap.on('alert', (msg) => {
|
||||
console.log(`IMAP SERVER SAYS: ${msg}`)
|
||||
})
|
||||
|
||||
// Emitted when new mail arrives in the currently open mailbox.
|
||||
// Fix https://github.com/mscdex/node-imap/issues/445
|
||||
let lastMailEventBox = null;
|
||||
this._imap.on('mail', () => {
|
||||
if (lastMailEventBox === this._imap._box.name) {
|
||||
this.emit('mail');
|
||||
}
|
||||
lastMailEventBox = this._imap._box.name
|
||||
});
|
||||
|
||||
// Emitted if the UID validity value for the currently open mailbox
|
||||
// changes during the current session.
|
||||
this._imap.on('uidvalidity', () => this.emit('uidvalidity'))
|
||||
|
||||
// Emitted when message metadata (e.g. flags) changes externally.
|
||||
this._imap.on('update', () => this.emit('update'))
|
||||
}
|
||||
|
||||
serverSupports(cap) {
|
||||
this._imap.serverSupports(cap);
|
||||
this._settings = settings;
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (!this._connectPromise) {
|
||||
this._connectPromise = new Promise((resolve, reject) => {
|
||||
this._imap.once('ready', resolve);
|
||||
this._imap.once('error', reject);
|
||||
this._imap.connect();
|
||||
});
|
||||
this._connectPromise = this._resolveIMAPSettings().then((settings) =>
|
||||
this._buildUnderlyingConnection(settings)
|
||||
)
|
||||
}
|
||||
return this._connectPromise;
|
||||
}
|
||||
|
||||
_resolveIMAPSettings() {
|
||||
const result = {
|
||||
host: this._settings.imap_host,
|
||||
port: this._settings.imap_port,
|
||||
user: this._settings.imap_username,
|
||||
password: this._settings.imap_password,
|
||||
tls: this._settings.ssl_required,
|
||||
}
|
||||
|
||||
if (this._settings.refresh_token) {
|
||||
const xoauthFields = ['client_id', 'client_secret', 'imap_username', 'refresh_token'];
|
||||
if (Object.keys(_.pick(this._settings, xoauthFields)).length !== 4) {
|
||||
throw new Error(`IMAPConnection: Expected ${xoauthFields.join(',')} when given refresh_token`)
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
xoauth2.createXOAuth2Generator({
|
||||
clientId: this._settings.client_id,
|
||||
clientSecret: this._settings.client_secret,
|
||||
user: this._settings.imap_username,
|
||||
refreshToken: this._settings.refresh_token,
|
||||
}).getToken((err, token) => {
|
||||
if (err) { return reject(err) }
|
||||
delete result.password;
|
||||
result.xoauth2 = token;
|
||||
return resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
|
||||
_buildUnderlyingConnection(settings) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._imap = Promise.promisifyAll(new Imap(settings));
|
||||
|
||||
this._imap.once('end', () => {
|
||||
console.log('Connection ended');
|
||||
});
|
||||
|
||||
this._imap.on('alert', (msg) => {
|
||||
console.log(`IMAP SERVER SAYS: ${msg}`)
|
||||
})
|
||||
|
||||
// Emitted when new mail arrives in the currently open mailbox.
|
||||
// Fix https://github.com/mscdex/node-imap/issues/445
|
||||
let lastMailEventBox = null;
|
||||
this._imap.on('mail', () => {
|
||||
if (lastMailEventBox === this._imap._box.name) {
|
||||
this.emit('mail');
|
||||
}
|
||||
lastMailEventBox = this._imap._box.name
|
||||
});
|
||||
|
||||
// Emitted if the UID validity value for the currently open mailbox
|
||||
// changes during the current session.
|
||||
this._imap.on('uidvalidity', () => this.emit('uidvalidity'))
|
||||
|
||||
// Emitted when message metadata (e.g. flags) changes externally.
|
||||
this._imap.on('update', () => this.emit('update'))
|
||||
this._imap.once('ready', resolve);
|
||||
this._imap.once('error', reject);
|
||||
this._imap.connect();
|
||||
});
|
||||
}
|
||||
|
||||
end() {
|
||||
this._queue = [];
|
||||
this._imap.end();
|
||||
}
|
||||
|
||||
serverSupports(cap) {
|
||||
if (!this._imap) {
|
||||
throw new Error("IMAPConnection.serverSupports: You need to call connect() first.")
|
||||
}
|
||||
this._imap.serverSupports(cap);
|
||||
}
|
||||
|
||||
openBox(box) {
|
||||
if (!this._imap) {
|
||||
throw new Error("IMAPConnection.openBox: You need to call connect() first.")
|
||||
}
|
||||
return this._imap.openBoxAsync(box, true);
|
||||
}
|
||||
|
||||
getBoxes() {
|
||||
if (!this._imap) {
|
||||
throw new Error("IMAPConnection.getBoxes: You need to call connect() first.")
|
||||
}
|
||||
return this._imap.getBoxesAsync();
|
||||
}
|
||||
|
||||
fetch(range, messageCallback) {
|
||||
if (!this._imap) {
|
||||
throw new Error("IMAPConnection.fetch: You need to call connect() first.")
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const f = this._imap.fetch(range, {
|
||||
bodies: ['HEADER', 'TEXT'],
|
||||
|
@ -94,6 +141,9 @@ class IMAPConnection extends EventEmitter {
|
|||
}
|
||||
|
||||
fetchMessages(uids, messageCallback) {
|
||||
if (!this._imap) {
|
||||
throw new Error("IMAPConnection.fetchMessages: You need to call connect() first.")
|
||||
}
|
||||
if (uids.length === 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
@ -101,6 +151,9 @@ class IMAPConnection extends EventEmitter {
|
|||
}
|
||||
|
||||
fetchUIDAttributes(range) {
|
||||
if (!this._imap) {
|
||||
throw new Error("IMAPConnection.fetchUIDAttributes: You need to call connect() first.")
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const latestUIDAttributes = {};
|
||||
const f = this._imap.fetch(range, {});
|
||||
|
@ -146,6 +199,9 @@ class IMAPConnection extends EventEmitter {
|
|||
}
|
||||
|
||||
runOperation(operation) {
|
||||
if (!this._imap) {
|
||||
throw new Error("IMAPConnection.runOperation: You need to call connect() first.")
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
this._queue.push({operation, resolve, reject});
|
||||
if (this._imap.state === 'authenticated' && !this._current) {
|
||||
|
|
|
@ -158,7 +158,7 @@ class SyncProcessManager {
|
|||
addWorkerForAccountId(accountId) {
|
||||
DatabaseConnector.forShared().then(({Account}) => {
|
||||
Account.find({where: {id: accountId}}).then((account) => {
|
||||
if (!account) {
|
||||
if (!account || this._workers[account.id]) {
|
||||
return;
|
||||
}
|
||||
DatabaseConnector.forAccount(account.id).then((db) => {
|
||||
|
|
|
@ -80,7 +80,7 @@ class SyncWorker {
|
|||
if (!settings || !settings.imap_host) {
|
||||
throw new Error("ensureConnection: There are no IMAP connection settings for this account.")
|
||||
}
|
||||
if (!credentials || !credentials.imap_username) {
|
||||
if (!credentials) {
|
||||
throw new Error("ensureConnection: There are no IMAP connection credentials for this account.")
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue