├── .gitignore ├── test ├── initial.json ├── parser-test.js ├── api-test.js └── batched.json ├── LICENSE ├── lib └── parser.js ├── package.json ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | -------------------------------------------------------------------------------- /test/initial.json: -------------------------------------------------------------------------------- 1 | { 2 | "threads": [ 3 | { 4 | "id": "147dae", 5 | "snippet": "", 6 | "historyId": "123" 7 | } 8 | ], 9 | "messages": [ 10 | { 11 | "id": "147dae", 12 | "snippet": "", 13 | "historyId": "123" 14 | } 15 | ], 16 | "resultSizeEstimate": 1 17 | } 18 | -------------------------------------------------------------------------------- /test/parser-test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | , Parser = require('../lib/parser') 3 | 4 | test('parses objects as string, not object mode', function (t) { 5 | var p = new Parser 6 | 7 | p.on('data', function (d) { 8 | t.equal(d.toString(), '{}') 9 | t.deepEqual(JSON.parse(d.toString()), {}) 10 | t.end() 11 | }) 12 | 13 | p.write('{}') 14 | }) 15 | 16 | test('parses objects, object mode', function (t) { 17 | var p = new Parser({objectMode: true}) 18 | 19 | p.on('data', function (d) { 20 | t.deepEqual(d, {}) 21 | t.end() 22 | }) 23 | 24 | p.write('{}') 25 | }) 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 4-digit year, Company or Person's Name 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | var Transform = require('stream').Transform 2 | , util = require('util') 3 | 4 | function Parser (opts) { 5 | if (!(this instanceof Parser)) { 6 | return new Parser(opts) 7 | } 8 | opts = opts || {} 9 | this.opts = opts 10 | Transform.call(this, opts) 11 | } 12 | 13 | util.inherits(Parser, Transform) 14 | 15 | // A stream that reduces data elements 16 | Parser.prototype._transform = function (data, encoding, done) { 17 | var obj 18 | try { 19 | if (data) { 20 | obj = JSON.parse(data.toString()) 21 | } 22 | } catch (e) { 23 | // Don't care 24 | } 25 | 26 | if (obj !== undefined) { 27 | this.push(this.opts.objectMode ? obj : JSON.stringify(obj)) 28 | } 29 | done() 30 | } 31 | 32 | module.exports = Parser 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-gmail-api", 3 | "version": "1.0.1", 4 | "description": "Interacts with the gmail api, fetching emails", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "tape test/*.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/SpiderStrategies/node-gmail-api" 12 | }, 13 | "keywords": [ 14 | "gmail", 15 | "gmail-api" 16 | ], 17 | "author": "Nathan Bowser ", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/SpiderStrategies/node-gmail-api/issues" 21 | }, 22 | "homepage": "https://github.com/SpiderStrategies/node-gmail-api", 23 | "dependencies": { 24 | "multiparty": "^4.2.2", 25 | "request": "^2.40.0", 26 | "split": "^0.3.0", 27 | "stream-stream": "^1.2.6", 28 | "through2": "^2.0.0" 29 | }, 30 | "devDependencies": { 31 | "nock": "^0.44.3", 32 | "tape": "^2.14.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-gmail-api 2 | ============== 3 | 4 | Node module to interact with the gmail api. 5 | 6 | Why not the [google official library](https://github.com/google/google-api-nodejs-client)? Well it does too much and doesn't implement batching. 7 | Which means fetching a bunch of email is insanely painful. This module exposes a function which will query the api searching for messages and hit the google batch api to fetch all the messages that are returned. 8 | 9 | ### Authenticating users 10 | 11 | To use this module, you'll need an oauth access token key. See more details here: https://developers.google.com/gmail/api/overview#auth_and_the_gmail_api. 12 | 13 | You may use [passport-google-oauth](https://github.com/jaredhanson/passport-google-oauth) to get an access key for a user, then use this module to make requests on behalf of the authenticated user. 14 | 15 | Example authentication call (cf. [passport-google-oauth](https://github.com/jaredhanson/passport-google-oauth) for more complete usage examples): 16 | 17 | ````javascript 18 | passport.use(new GoogleStrategy({ 19 | clientID: config.googleApp.clientId 20 | , clientSecret: config.googleApp.clientSecret 21 | , userProfileURL: 'https://www.googleapis.com/oauth2/v3/userinfo' 22 | , callbackURL: config.baseurl + '/oauth2callback' 23 | } 24 | , function(accessToken, refreshToken, profile, done) { 25 | // do your thing here 26 | } 27 | )) 28 | ```` 29 | 30 | Example 31 | ======= 32 | 33 | ### Fetch latest 10 emails and show the snippet 34 | 35 | ````javascript 36 | // assuming access token has already been retrieved inside variable `accessToken` 37 | 38 | var Gmail = require('node-gmail-api') 39 | , gmail = new Gmail(accessToken) 40 | , s = gmail.messages('label:inbox', {max: 10}) 41 | 42 | s.on('data', function (d) { 43 | console.log(d.snippet) 44 | }) 45 | ```` 46 | 47 | ### Optionally request the fields you want (for performance) 48 | 49 | cf. [gmail API performance guide](https://developers.google.com/gmail/api/guides/performance). 50 | 51 | ````javascript 52 | 53 | var Gmail = require('node-gmail-api') 54 | , gmail = new Gmail(accessToken) 55 | , s = gmail.messages('label:inbox', { fields: ['id', 'internalDate', 'labelIds', 'payload']}) 56 | 57 | s.on('data', function (d) { 58 | console.log(d.id) 59 | }) 60 | ```` 61 | 62 | ### Optionally request the format you want (e.g full (default), raw, minimal, metadata) 63 | 64 | ````javascript 65 | 66 | var Gmail = require('node-gmail-api') 67 | , gmail = new Gmail(accessToken) 68 | , s = gmail.messages('label:inbox', {format: 'raw'}) 69 | 70 | s.on('data', function (d) { 71 | console.log(d.raw) 72 | }) 73 | ```` 74 | 75 | License 76 | ======= 77 | 78 | ISC 79 | -------------------------------------------------------------------------------- /test/api-test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | , nock = require('nock') 3 | , Gmail = require('../') 4 | 5 | test('retrieves message count', function (t) { 6 | t.plan(1) 7 | nock('https://www.googleapis.com') 8 | .get('/gmail/v1/users/me/messages?q=label%3Ainbox&fields=resultSizeEstimate') 9 | .reply(200, {resultSizeEstimate: 2}) 10 | 11 | var gmail = new Gmail('key') 12 | gmail.estimatedMessages('label:inbox', {timeout: 3000}, function (err, count) { 13 | t.equal(count, 2) 14 | t.end() 15 | }) 16 | }) 17 | 18 | test('retrieves message count', function (t) { 19 | t.plan(1) 20 | 21 | nock('https://www.googleapis.com') 22 | .get('/gmail/v1/users/me/threads?q=label%3Ainbox&fields=resultSizeEstimate') 23 | .reply(200, {resultSizeEstimate: 3}) 24 | 25 | var gmail = new Gmail('key') 26 | gmail.estimatedThreads('label:inbox', function (err, count) { 27 | t.equal(count, 3) 28 | t.end() 29 | }) 30 | }) 31 | 32 | test('retrieves threads', function (t) { 33 | t.plan(5) 34 | nock('https://www.googleapis.com') 35 | .get('/gmail/v1/users/me/threads?q=label%3Ainbox') 36 | .replyWithFile(200, __dirname + '/initial.json') 37 | 38 | nock('https://www.googleapis.com') 39 | .post('/batch/gmail/v1') 40 | .replyWithFile(200, __dirname + '/batched.json', { 41 | 'content-type': 'multipart/mixed; boundary=batch_FmDEX85qSFQ=_AAlNL3-GN3E=' 42 | }) 43 | 44 | var gmail = new Gmail('key') 45 | , stream = gmail.threads('label:inbox') 46 | , data 47 | 48 | stream.on('data', function (d) { 49 | data = d 50 | }) 51 | stream.on('end', function () { 52 | t.equal(data.id, '147dae72a4bab6b4') 53 | t.equal(data.historyId, '6435511') 54 | t.equal(data.messages.length, 1) 55 | t.equal(data.messages[0].snippet, 'This is a test email.') 56 | t.equal(stream.resultSizeEstimate, 1) 57 | t.end() 58 | }) 59 | }) 60 | 61 | test('retrieves messages', function (t) { 62 | t.plan(5) 63 | nock('https://www.googleapis.com') 64 | .get('/gmail/v1/users/me/messages?q=label%3Ainbox') 65 | .replyWithFile(200, __dirname + '/initial.json') 66 | 67 | nock('https://www.googleapis.com') 68 | .post('/batch') 69 | .replyWithFile(200, __dirname + '/batched.json', { 70 | 'content-type': 'multipart/mixed; boundary=batch_FmDEX85qSFQ=_AAlNL3-GN3E=' 71 | }) 72 | 73 | var gmail = new Gmail('key') 74 | , stream = gmail.messages('label:inbox', {format : 'raw'}) 75 | , data 76 | 77 | stream.on('data', function (d) { 78 | data = d 79 | }) 80 | stream.on('end', function () { 81 | t.equal(data.id, '147dae72a4bab6b4') 82 | t.equal(data.historyId, '6435511') 83 | t.equal(data.messages.length, 1) 84 | t.equal(data.messages[0].snippet, 'This is a test email.') 85 | t.equal(stream.resultSizeEstimate, 1) 86 | t.end() 87 | }) 88 | }) 89 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var request = require('request') 2 | , Parser = require('./lib/parser') 3 | , split = require('split') 4 | , multiparty = require('multiparty') 5 | , ss = require('stream-stream') 6 | , through = require('through2') 7 | , querystring = require('querystring') 8 | , api = 'https://www.googleapis.com' 9 | 10 | var Gmail = function (key) { 11 | if (!key) { 12 | throw new Error('Access key required') 13 | } 14 | this.key = key 15 | } 16 | 17 | var retrieveCount = function (key, q, endpoint, opts, next) { 18 | request({ 19 | url: api + '/gmail/v1/users/me/' + endpoint, 20 | json: true, 21 | timeout: opts.timeout, 22 | qs: { 23 | q: q, 24 | fields: 'resultSizeEstimate' 25 | }, 26 | headers: { 27 | 'Authorization': 'Bearer ' + key 28 | } 29 | }, function (err, response, body) { 30 | if (err) { 31 | return next(err) 32 | } 33 | if (body.error) { 34 | return next(new Error(body.error.message)) 35 | } 36 | return next(null, body.resultSizeEstimate) 37 | }) 38 | } 39 | 40 | var formQuery = function (query) { 41 | var formedQuery = '?' 42 | , params = {} 43 | 44 | Object.keys(query).forEach(function(key) { 45 | if(query[key]) { 46 | params[key] = query[key] instanceof Array ? query[key].join(',') : query[key] 47 | } 48 | }) 49 | 50 | formedQuery += querystring.stringify(params) 51 | 52 | return formedQuery 53 | } 54 | 55 | var retrieve = function (key, q, endpoint, opts) { 56 | var result = new Parser({objectMode: true}) 57 | , combined = ss() 58 | , opts = opts || {} 59 | , i = opts.max 60 | , partsFound = 0 61 | 62 | var loop = function(page) { 63 | var reqOpts = { 64 | url: api + '/gmail/v1/users/me/' + endpoint, 65 | json: true, 66 | timeout: opts.timeout, 67 | qs: { 68 | q: q, 69 | maxResults: i < 100 ? i : 100 70 | }, 71 | headers: { 72 | 'Authorization': 'Bearer ' + key 73 | } 74 | } 75 | 76 | var query = formQuery({fields : opts.fields, format : opts.format}) 77 | 78 | if (page) reqOpts.qs.pageToken = page 79 | request(reqOpts, function (err, response, body) { 80 | if (err) { 81 | return result.emit('error', err) 82 | } 83 | 84 | if (body.error) { 85 | return result.emit('error', new Error(body.error.message)) 86 | } 87 | 88 | result.resultSizeEstimate = body.resultSizeEstimate 89 | 90 | if (!result.resultSizeEstimate) { 91 | return result.end() 92 | } 93 | var messages = body[endpoint].map(function (m) { 94 | return { 95 | 'Content-Type': 'application/http', 96 | body: 'GET ' + api + '/gmail/v1/users/me/' + endpoint + '/' + m.id + query + '\n' 97 | } 98 | }) 99 | 100 | var r = request({ 101 | method: 'POST', 102 | url: api + '/batch/gmail/v1', 103 | multipart: messages, 104 | timeout: opts.timeout, 105 | headers: { 106 | 'Authorization': 'Bearer ' + key, 107 | 'content-type': 'multipart/mixed' 108 | } 109 | }) 110 | 111 | r.on('error', function (e) { 112 | result.emit('error', e) 113 | }) 114 | 115 | r.on('response', function (res) { 116 | var type = res.headers['content-type'] 117 | , form = new multiparty.Form 118 | 119 | res.headers['content-type'] = type.replace('multipart/mixed', 'multipart/related') 120 | form.on('part', function (part) { 121 | partsFound++; 122 | combined.write(part.pipe(split('\r\n')).pipe(new Parser)) 123 | }).parse(res) 124 | form.on('close', function () { 125 | if (opts.max !== partsFound && body.nextPageToken) return loop(body.nextPageToken) 126 | combined.end() 127 | }) 128 | 129 | }) 130 | }) 131 | } 132 | loop() 133 | 134 | var counter = through(function(obj, enc, cb) { 135 | i-- 136 | cb(null, obj) 137 | }) 138 | 139 | return combined.pipe(counter).pipe(result) 140 | } 141 | 142 | /* 143 | * Fetches the number of estimated messages matching the query 144 | * Invokes callback with err and estimated number 145 | */ 146 | Gmail.prototype.estimatedMessages = function (q, opts, next) { 147 | if (!next) { 148 | next = opts 149 | opts = {} 150 | } 151 | return retrieveCount(this.key, q, 'messages', opts, next) 152 | } 153 | 154 | /* 155 | * Fetches email that matches the query. Returns a stream of messages with a max of 100 messages 156 | * since the batch api sets a limit of 100. 157 | * 158 | * e.g. to search an inbox: gmail.messages('label:inbox') 159 | */ 160 | Gmail.prototype.messages = function (q, opts) { 161 | return retrieve(this.key, q, 'messages', opts) 162 | } 163 | 164 | /* 165 | * Fetches the number of estimated threads matching the query 166 | * Invokes callback with err and estimated number 167 | */ 168 | Gmail.prototype.estimatedThreads = function (q, opts, next) { 169 | if (!next) { 170 | next = opts 171 | opts = {} 172 | } 173 | return retrieveCount(this.key, q, 'threads', opts, next) 174 | } 175 | 176 | Gmail.prototype.threads = function (q, opts) { 177 | return retrieve(this.key, q, 'threads', opts) 178 | } 179 | 180 | module.exports = Gmail 181 | -------------------------------------------------------------------------------- /test/batched.json: -------------------------------------------------------------------------------- 1 | --batch_FmDEX85qSFQ=_AAlNL3-GN3E= 2 | Content-Type: application/http 3 | 4 | HTTP/1.1 200 OK 5 | ETag: "QLs4pSnnOYcKsc2Nteng63h1s-w/row_ug9PS4K-fkQqY8_S86FeLcw" 6 | Content-Type: application/json; charset=UTF-8 7 | Date: Mon, 18 Aug 2014 14:12:41 GMT 8 | Expires: Mon, 18 Aug 2014 14:12:41 GMT 9 | Cache-Control: private, max-age=0 10 | Content-Length: 4942 11 | 12 | { 13 | "id": "147dae72a4bab6b4", 14 | "historyId": "6435511", 15 | "messages": [ 16 | { 17 | "id": "147dae72a4bab6b4", 18 | "threadId": "147dae72a4bab6b4", 19 | "labelIds": [ 20 | "INBOX", 21 | "CATEGORY_PERSONAL" 22 | ], 23 | "snippet": "This is a test email.", 24 | "historyId": "6435511", 25 | "payload": { 26 | "mimeType": "multipart/alternative", 27 | "filename": "", 28 | "headers": [ 29 | { 30 | "name": "Delivered-To", 31 | "value": "nathan.bowser@spiderstrategies.com" 32 | }, 33 | { 34 | "name": "Received", 35 | "value": "by 10.66.11.229 with SMTP id t5csp59025pab; Fri, 15 Aug 2014 11:21:29 -0700 (PDT)" 36 | }, 37 | { 38 | "name": "X-Received", 39 | "value": "by 10.70.134.140 with SMTP id pk12mr11818608pdb.165.1408126888641; Fri, 15 Aug 2014 11:21:28 -0700 (PDT)" 40 | }, 41 | { 42 | "name": "Return-Path", 43 | "value": "\u003cnbowser@gmail.com\u003e" 44 | }, 45 | { 46 | "name": "Received", 47 | "value": "from psmtp.com (exprod7mx237.postini.com [64.18.2.238]) by mx.google.com with SMTP id qp5si9574745pbc.42.2014.08.15.11.21.28 for \u003cnathan.bowser@spiderstrategies.com\u003e; Fri, 15 Aug 2014 11:21:28 -0700 (PDT)" 48 | }, 49 | { 50 | "name": "Received-SPF", 51 | "value": "pass (google.com: domain of nbowser@gmail.com designates 74.125.82.50 as permitted sender) client-ip=74.125.82.50;" 52 | }, 53 | { 54 | "name": "Authentication-Results", 55 | "value": "mx.google.com; spf=pass (google.com: domain of nbowser@gmail.com designates 74.125.82.50 as permitted sender) smtp.mail=nbowser@gmail.com; dkim=pass header.i=@gmail.com" 56 | }, 57 | { 58 | "name": "Received", 59 | "value": "from mail-wg0-f50.google.com ([74.125.82.50]) (using TLSv1) by exprod7mx237.postini.com ([64.18.6.10]) with SMTP; Fri, 15 Aug 2014 14:21:28 EDT" 60 | }, 61 | { 62 | "name": "Received", 63 | "value": "by mail-wg0-f50.google.com with SMTP id n12so2663996wgh.9 for \u003cnathan.bowser@spiderstrategies.com\u003e; Fri, 15 Aug 2014 11:21:26 -0700 (PDT)" 64 | }, 65 | { 66 | "name": "DKIM-Signature", 67 | "value": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20120113; h=mime-version:from:date:message-id:subject:to:content-type; bh=InI1Ez8SoGcwqoAtiKI+9cHA8M7sc6pVe8RcESK/vE0=; b=aElHpawJVfdznnn0+Pg9BJnDr6G/UTS/2gYYcaLAxOqIgmQJuskVJpDSu6W5eRRAI0 30V5rifXHLrG98Xm7MFC7AlNSnqQKmRzFlwydtztmDMQJY1MKcTKxhbsNr9nJAlgJl31 M0cEMW10ZJPL5kp8pXZ8GK72BHXgsbCiltSh1m43f2ewZbC/31ntRyzlzLFnsnQ1oFe4 d7FFe585iWZsJvmtR480aA0YR9a4nJ/AxKxdri1lm0YoQzAcSy7I3gC2eG7BgsrSy0vJ WnhVT2oi01H7NLJ7EXk+B7LzEZ+BVokjIjJQTKTLFvDpesZXc8O0dduwykhNmM9k/k0k MV6g==" 68 | }, 69 | { 70 | "name": "X-Received", 71 | "value": "by 10.180.77.193 with SMTP id u1mr22820731wiw.45.1408126886582; Fri, 15 Aug 2014 11:21:26 -0700 (PDT)" 72 | }, 73 | { 74 | "name": "MIME-Version", 75 | "value": "1.0" 76 | }, 77 | { 78 | "name": "Received", 79 | "value": "by 10.216.200.201 with HTTP; Fri, 15 Aug 2014 11:21:05 -0700 (PDT)" 80 | }, 81 | { 82 | "name": "From", 83 | "value": "Nathan Bowser \u003cnbowser@gmail.com\u003e" 84 | }, 85 | { 86 | "name": "Date", 87 | "value": "Fri, 15 Aug 2014 14:21:05 -0400" 88 | }, 89 | { 90 | "name": "Message-ID", 91 | "value": "\u003cCANn0XTC-RPhS=2jQ-B3uadmOoJ0r4XTUZFSE+0ONA3U+zjx5sg@mail.gmail.com\u003e" 92 | }, 93 | { 94 | "name": "Subject", 95 | "value": "Testing" 96 | }, 97 | { 98 | "name": "To", 99 | "value": "Nathan Bowser \u003cnathan.bowser@spiderstrategies.com\u003e" 100 | }, 101 | { 102 | "name": "Content-Type", 103 | "value": "multipart/alternative; boundary=f46d043d6759df9e970500af167a" 104 | }, 105 | { 106 | "name": "X-pstn-mail-from", 107 | "value": "\u003cnbowser@gmail.com\u003e" 108 | }, 109 | { 110 | "name": "X-pstn-dkim", 111 | "value": "1 skipped:not-enabled" 112 | }, 113 | { 114 | "name": "X-pstn-nxpr", 115 | "value": "disp=neutral, envrcpt=nathan.bowser@spiderstrategies.com" 116 | }, 117 | { 118 | "name": "X-pstn-nxp", 119 | "value": "bodyHash=a49b32b297d5268dd7ce37746f80bfefb7c5074f, headerHash=0aac1d0ebc54a02939e1f7250b2c45457474b872, keyName=4, rcptHash=0358cfc489271652d17e2942689847646c100e40, sourceip=74.125.82.50, version=1" 120 | } 121 | ], 122 | "body": { 123 | "size": 0 124 | }, 125 | "parts": [ 126 | { 127 | "partId": "0", 128 | "mimeType": "text/plain", 129 | "filename": "", 130 | "headers": [ 131 | { 132 | "name": "Content-Type", 133 | "value": "text/plain; charset=ISO-8859-1" 134 | } 135 | ], 136 | "body": { 137 | "size": 23, 138 | "data": "VGhpcyBpcyBhIHRlc3QgZW1haWwuDQo=" 139 | } 140 | }, 141 | { 142 | "partId": "1", 143 | "mimeType": "text/html", 144 | "filename": "", 145 | "headers": [ 146 | { 147 | "name": "Content-Type", 148 | "value": "text/html; charset=ISO-8859-1" 149 | } 150 | ], 151 | "body": { 152 | "size": 44, 153 | "data": "PGRpdiBkaXI9Imx0ciI-VGhpcyBpcyBhIHRlc3QgZW1haWwuPC9kaXY-DQo=" 154 | } 155 | } 156 | ] 157 | }, 158 | "sizeEstimate": 2989 159 | } 160 | ] 161 | } 162 | 163 | --batch_FmDEX85qSFQ=_AAlNL3-GN3E=-- 164 | --------------------------------------------------------------------------------