├── .gitignore
├── .travis.yml
├── test
├── background.js
├── manifest.json
├── integration.js
├── unit.html
├── integration.html
├── unit.js
├── integration-test.js
├── local-integration-test.js
└── unit-test.js
├── .jshintrc
├── package.json
├── LICENSE.txt
├── Gruntfile.js
├── README.md
└── src
└── imap-client.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | test/lib
3 | node_modules
4 | npm-debug.log
5 | lib
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "0.10"
4 | - 0.12
5 | before_script:
6 | - npm install -g grunt-cli
7 | notifications:
8 | email:
9 | - build@whiteout.io
--------------------------------------------------------------------------------
/test/background.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | chrome.app.runtime.onLaunched.addListener(function() {
4 | chrome.app.window.create('integration.html', {
5 | 'bounds': {
6 | 'width': 1024,
7 | 'height': 650
8 | }
9 | });
10 | });
--------------------------------------------------------------------------------
/test/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "imap-client integration tests",
3 | "description": "Testing the imap-client in the browser",
4 | "version": "0.0.1",
5 | "manifest_version": 2,
6 | "offline_enabled": true,
7 | "permissions": ["http://*/*", "https://*/*", {
8 | "socket": ["tcp-connect"]
9 | }],
10 | "app": {
11 | "background": {
12 | "scripts": ["background.js"]
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/test/integration.js:
--------------------------------------------------------------------------------
1 | require.config({
2 | baseUrl: 'lib',
3 | paths: {
4 | 'test': '..',
5 | 'forge': 'forge.min'
6 | },
7 | shim: {
8 | forge: {
9 | exports: 'forge'
10 | }
11 | }
12 |
13 | });
14 |
15 | require([], function() {
16 | 'use strict';
17 |
18 | mocha.setup('bdd');
19 |
20 | require(['test/integration-test'], function() {
21 | mocha.run();
22 | });
23 | });
--------------------------------------------------------------------------------
/test/unit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | JavaScript Integration Tests
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/test/integration.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | JavaScript Integration Tests
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "indent": 4,
3 | "strict": true,
4 | "globalstrict": true,
5 | "node": true,
6 | "browser": true,
7 | "camelcase": false,
8 | "nonew": true,
9 | "curly": true,
10 | "eqeqeq": true,
11 | "immed": true,
12 | "newcap": true,
13 | "regexp": true,
14 | "evil": true,
15 | "eqnull": true,
16 | "expr": true,
17 | "trailing": true,
18 | "undef": true,
19 | "unused": true,
20 |
21 | "predef": [
22 | "Promise",
23 | "define",
24 | "importScripts",
25 | "self",
26 | "setImmediate",
27 | "mocha",
28 | "chrome",
29 | "mock",
30 | "mockFunction",
31 | "when",
32 | "anything",
33 | "beforeEach",
34 | "afterEach",
35 | "before",
36 | "after",
37 | "console",
38 | "process",
39 | "describe",
40 | "it",
41 | "ES6Promise"
42 | ],
43 |
44 | "globals": {
45 | }
46 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "imap-client",
3 | "version": "0.14.3",
4 | "scripts": {
5 | "pretest": "dir=$(pwd) && cd node_modules/browserbox/node_modules/tcp-socket/node_modules/node-forge/ && npm install && npm run minify && cd $dir",
6 | "test": "grunt"
7 | },
8 | "main": "src/imap-client.js",
9 | "dependencies": {
10 | "browserbox": "~0.9.0",
11 | "axe-logger": "~0.0.2"
12 | },
13 | "devDependencies": {
14 | "es6-promise": "^2.0.1",
15 | "chai": "~1.9.2",
16 | "grunt": "~0.4.1",
17 | "grunt-mocha-phantomjs": "~0.7.0",
18 | "grunt-contrib-connect": "~0.6.0",
19 | "grunt-contrib-jshint": "~0.8.0",
20 | "grunt-contrib-copy": "^0.5.0",
21 | "grunt-contrib-clean": "^0.5.0",
22 | "grunt-contrib-watch": "^0.6.1",
23 | "phantomjs": "~1.9.7-1",
24 | "sinon": "1.7.3",
25 | "requirejs": "2.1.x",
26 | "hoodiecrow": "1.1.x",
27 | "mocha": "^1.18.2",
28 | "grunt-mocha-test": "^0.10.2"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Whiteout Networks GmbH
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/test/unit.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require.config({
4 | baseUrl: 'lib',
5 | paths: {
6 | 'test': '..',
7 | 'forge': 'forge.min'
8 | },
9 | shim: {
10 | forge: {
11 | exports: 'forge'
12 | },
13 | sinon: {
14 | exports: 'sinon'
15 | }
16 | }
17 | });
18 |
19 | // add function.bind polyfill
20 | if (!Function.prototype.bind) {
21 | Function.prototype.bind = function(oThis) {
22 | if (typeof this !== "function") {
23 | // closest thing possible to the ECMAScript 5 internal IsCallable function
24 | throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
25 | }
26 |
27 | var aArgs = Array.prototype.slice.call(arguments, 1),
28 | fToBind = this,
29 | FNOP = function() {},
30 | fBound = function() {
31 | return fToBind.apply(this instanceof FNOP && oThis ? this : oThis,
32 | aArgs.concat(Array.prototype.slice.call(arguments)));
33 | };
34 |
35 | FNOP.prototype = this.prototype;
36 | fBound.prototype = new FNOP();
37 |
38 | return fBound;
39 | };
40 | }
41 |
42 | mocha.setup('bdd');
43 | require(['test/unit-test'], function() {
44 | (window.mochaPhantomJS || window.mocha).run();
45 | });
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function(grunt) {
2 | 'use strict';
3 |
4 | // Project configuration.
5 | grunt.initConfig({
6 | jshint: {
7 | all: ['*.js', 'src/*.js', 'test/*.js'],
8 | options: {
9 | jshintrc: '.jshintrc'
10 | }
11 | },
12 |
13 | connect: {
14 | dev: {
15 | options: {
16 | port: 10000,
17 | base: '.',
18 | keepalive: true
19 | }
20 | }
21 | },
22 |
23 | mocha_phantomjs: {
24 | all: {
25 | options: {
26 | reporter: 'spec'
27 | },
28 | src: ['test/unit.html']
29 | }
30 | },
31 |
32 | mochaTest: {
33 | tonline: {
34 | options: {
35 | reporter: 'spec'
36 | },
37 | src: ['test/integration-test.js']
38 | },
39 | local: {
40 | options: {
41 | reporter: 'spec'
42 | },
43 | src: ['test/local-integration-test.js']
44 | },
45 | unit: {
46 | options: {
47 | reporter: 'spec'
48 | },
49 | src: ['test/unit-test.js']
50 | }
51 | },
52 |
53 | watch: {
54 | js: {
55 | files: ['src/*.js', 'test/*.js', 'test/*.html'],
56 | tasks: ['deps']
57 | }
58 | },
59 |
60 | copy: {
61 | npm: {
62 | expand: true,
63 | flatten: true,
64 | cwd: 'node_modules/',
65 | src: [
66 | 'mocha/mocha.js',
67 | 'mocha/mocha.css',
68 | 'chai/chai.js',
69 | 'axe-logger/axe.js',
70 | 'sinon/pkg/sinon.js',
71 | 'requirejs/require.js',
72 | 'browserbox/node_modules/tcp-socket/src/*.js',
73 | 'browserbox/node_modules/tcp-socket/node_modules/node-forge/js/forge.min.js',
74 | 'browserbox/src/*.js',
75 | 'browserbox/node_modules/wo-addressparser/src/*.js',
76 | 'browserbox/node_modules/wo-utf7/src/*.js',
77 | 'browserbox/node_modules/wo-imap-handler/src/*.js',
78 | 'browserbox/node_modules/mimefuncs/src/*.js',
79 | 'browserbox/node_modules/mimefuncs/node_modules/wo-stringencoding/dist/stringencoding.js',
80 | 'es6-promise/dist/es6-promise.js'
81 | ],
82 | dest: 'test/lib/'
83 | },
84 | app: {
85 | expand: true,
86 | flatten: true,
87 | cwd: 'src/',
88 | src: [
89 | '*.js',
90 | ],
91 | dest: 'test/lib/'
92 | }
93 | },
94 |
95 | clean: ['test/lib/**/*']
96 | });
97 |
98 | // Load the plugin(s)
99 | grunt.loadNpmTasks('grunt-mocha-test');
100 | grunt.loadNpmTasks('grunt-mocha-phantomjs');
101 | grunt.loadNpmTasks('grunt-contrib-jshint');
102 | grunt.loadNpmTasks('grunt-contrib-connect');
103 | grunt.loadNpmTasks('grunt-contrib-watch');
104 | grunt.loadNpmTasks('grunt-contrib-copy');
105 | grunt.loadNpmTasks('grunt-contrib-clean');
106 |
107 | // Default task(s).
108 | grunt.registerTask('deps', ['clean', 'copy']);
109 | grunt.registerTask('dev', ['deps', 'connect:dev']);
110 | grunt.registerTask('default', ['jshint', 'deps', 'mochaTest:unit', 'mocha_phantomjs', 'mochaTest:local', 'mochaTest:tonline']);
111 | };
--------------------------------------------------------------------------------
/test/integration-test.js:
--------------------------------------------------------------------------------
1 | (function(factory) {
2 | 'use strict';
3 |
4 | if (typeof define === 'function' && define.amd) {
5 | ES6Promise.polyfill(); // load ES6 Promises polyfill
6 | define(['chai', 'imap-client', 'axe'], factory);
7 | } else if (typeof exports === 'object') {
8 | require('es6-promise').polyfill(); // load ES6 Promises polyfill
9 | module.exports = factory(require('chai'), require('../src/imap-client'), require('axe-logger'));
10 | }
11 | })(function(chai, ImapClient, axe) {
12 | 'use strict';
13 |
14 | var expect = chai.expect,
15 | loginOptions;
16 |
17 | loginOptions = {
18 | port: 993,
19 | host: 'secureimap.t-online.de',
20 | auth: {
21 | user: 'whiteout.testaccount@t-online.de',
22 | pass: 'HelloSafer'
23 | },
24 | secure: true,
25 | tlsWorkerPath: 'lib/tcp-socket-tls-worker.js'
26 | };
27 |
28 | describe('ImapClient t-online integration tests', function() {
29 | this.timeout(50000);
30 | chai.config.includeStack = true;
31 |
32 | // don't log in the tests
33 | axe.removeAppender(axe.defaultAppender);
34 |
35 | var ic;
36 |
37 | beforeEach(function(done) {
38 | ic = new ImapClient(loginOptions);
39 | ic.onSyncUpdate = function() {};
40 | ic.onCert = function() {};
41 | ic.onError = function(error) {
42 | console.error(error);
43 | };
44 | ic.login().then(done);
45 | });
46 |
47 |
48 | afterEach(function(done) {
49 | ic.logout().then(done);
50 | });
51 |
52 | it('should list well known folders', function(done) {
53 | ic.listWellKnownFolders().then(function(folders) {
54 | expect(folders).to.exist;
55 |
56 | expect(folders.Inbox).to.be.instanceof(Array);
57 | expect(folders.Inbox[0]).to.exist;
58 | expect(folders.Inbox[0].name).to.exist;
59 | expect(folders.Inbox[0].type).to.exist;
60 | expect(folders.Inbox[0].path).to.exist;
61 |
62 | expect(folders.Drafts).to.be.instanceof(Array);
63 | expect(folders.Drafts).to.not.be.empty;
64 |
65 | expect(folders.Sent).to.be.instanceof(Array);
66 | expect(folders.Sent).to.not.be.empty;
67 |
68 | expect(folders.Trash).to.be.instanceof(Array);
69 | expect(folders.Trash).to.not.be.empty;
70 |
71 | expect(folders.Other).to.be.instanceof(Array);
72 | }).then(done);
73 | });
74 |
75 | it('should search messages', function(done) {
76 | ic.search({
77 | path: 'INBOX',
78 | unread: false,
79 | answered: false
80 | }).then(function(uids) {
81 | expect(uids).to.not.be.empty;
82 | }).then(done);
83 | });
84 |
85 | it('should list messages by uid', function(done) {
86 | ic.listMessages({
87 | path: 'INBOX',
88 | firstUid: 1
89 | }).then(function(messages) {
90 | expect(messages).to.not.be.empty;
91 | }).then(done);
92 | });
93 |
94 | it('should create folder hierarchy', function(done) {
95 | ic.createFolder({
96 | path: ['bar', 'baz']
97 | }).then(function(fullPath) {
98 | expect(fullPath).to.equal('INBOX.bar.baz');
99 | return ic.listWellKnownFolders();
100 |
101 | }).then(function(folders) {
102 | var hasFoo = false;
103 | folders.Other.forEach(function(folder) {
104 | hasFoo = hasFoo || folder.path === 'INBOX.bar.baz';
105 | });
106 |
107 | expect(hasFoo).to.be.true;
108 | expect(ic._delimiter).to.exist;
109 | expect(ic._prefix).to.exist;
110 | expect(hasFoo).to.be.true;
111 | }).then(done);
112 | });
113 |
114 | it('should upload Message', function(done) {
115 | var msg = 'MIME-Version: 1.0\r\nDate: Wed, 9 Jul 2014 15:07:47 +0200\r\nDelivered-To: test@test.com\r\nMessage-ID: \r\nSubject: integration test\r\nFrom: Test Test \r\nTo: Test Test \r\nContent-Type: text/plain; charset=UTF-8\r\n\r\nintegration test',
116 | path = 'INBOX',
117 | msgCount;
118 |
119 | ic.listMessages({
120 | path: path,
121 | firstUid: 1
122 | }).then(function(messages) {
123 | expect(messages).to.not.be.empty;
124 | msgCount = messages.length;
125 |
126 | return ic.uploadMessage({
127 | path: path,
128 | message: msg
129 | });
130 | }).then(function() {
131 | return ic.listMessages({
132 | path: path,
133 | firstUid: 1
134 | });
135 | }).then(function(messages) {
136 | expect(messages.length).to.equal(msgCount + 1);
137 | }).then(done);
138 | });
139 |
140 | it('should get message parts', function(done) {
141 | var msg;
142 | ic.listMessages({
143 | path: 'INBOX',
144 | firstUid: 1
145 | }).then(function(messages) {
146 | msg = messages.pop();
147 | return ic.getBodyParts({
148 | path: 'INBOX',
149 | uid: msg.uid,
150 | bodyParts: msg.bodyParts
151 | });
152 | }).then(function(bodyParts) {
153 | expect(msg.bodyParts).to.equal(bodyParts);
154 | expect(bodyParts[0].raw).to.not.be.empty;
155 | }).then(done);
156 | });
157 | });
158 | });
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # imap-client
2 |
3 | Repos under this github org are not longer maintained. Please use the new [emailjs](http://emailjs.org/) repos.
4 |
5 | High-level UMD module wrapper for [browserbox](https://github.com/whiteout-io/browserbox). This module encapsulates the most commonly used IMAP commands.
6 |
7 | Needs ES6 Promises, [supply polyfills where necessary](https://github.com/jakearchibald/es6-promise).
8 |
9 | ## API
10 |
11 | ### Constructor
12 |
13 | ```
14 | var ImapClient = require(‘imap-client’);
15 | var imap = new ImapClient({
16 | port: 993, // the port to connect to
17 | host: ’imap.example.com’, // the host to connect to
18 | secure: true/false, // use SSL?
19 | ignoreTLS: true/false, // if true, do not call STARTTLS before authentication even if the host advertises support for it
20 | requireTLS: true/false, // if true, always use STARTTLS before authentication even if the host does not advertise it. If STARTTLS fails, do not try to authenticate the user
21 | auth.user: ’john.q@example.com’, // username of the user (also applies to oauth2)
22 | auth.pass: ‘examplepassword’, // password for the user
23 | auth.xoauth2: ‘EXAMPLEOAUTH2TOKEN’, // OAuth2 access token to be used instead of password
24 | ca: ‘PEM ENCODED CERT’, // (optional, used only in conjunction with the TCPSocket shim) if you use TLS with forge, pin a PEM-encoded certificate as a string. Please refer to the [tcp-socket documentation](https://github.com/whiteout-io/tcp-socket) for more information!
25 | maxUpdateSize: 20 // (optional) the maximum number of messages you want to receive in one update from the server
26 | });
27 | ```
28 |
29 | ### #login() and #logout()
30 |
31 | Log in to an IMAP Session. No-op if already logged in.
32 |
33 | ```
34 | imap.login().then(function() {
35 | // yay, we’re logged in
36 | })
37 |
38 | imap.logout().then(function() {
39 | // yay, we’re logged out
40 | });
41 | ```
42 |
43 | ### #listenForChanges() and #stopListeningForChanges()
44 |
45 | Set up a connection dedicated to listening for changes published by the IMAP server on one specific inbox.
46 |
47 | ```
48 | imap.listenForChanges({
49 | path: ‘mailboxpath’
50 | }).then(function() {
51 | // the audience is listening
52 | ...
53 | })
54 |
55 | imap.stopListeningForChanges().then(function() {
56 | // we’re not listening anymore
57 | })
58 | ```
59 |
60 | ### #listWellKnownFolders()
61 |
62 | Lists folders, grouped to folders that are in the following categories: Inbox, Drafts, All, Flagged, Sent, Trash, Junk, Archive, Other.
63 |
64 | ### #createFolder(path)
65 |
66 | Creates a folder...
67 |
68 | ```
69 | imap.createFolder({
70 | path: ['foo', 'bar']
71 | }).then(function(path) {
72 | // folder created
73 | console.log('created folder: ' + path);
74 | })
75 | ```
76 |
77 | ### #search(options, callback)
78 |
79 | Returns the uids of messages containing the search terms in the options.
80 |
81 | ```
82 | imap.search({
83 | answered: true,
84 | unread: true,
85 | header: ['X-Foobar', '123qweasdzxc']
86 | }).then(function(uids) {
87 | console.log(‘uids: ‘ + uids.join(‘, ‘))
88 | });
89 | ```
90 |
91 | ### #listMessages(options, callback)
92 |
93 | Lists messages in the mailbox based on their UID.
94 |
95 | ```
96 | imap.listMessages({
97 | path: ‘path’, the folder's path
98 | firstUid: 15, (optional) the uid of the first messagem defaults to 1
99 | lastUid: 30 (optional) the uid of the last message, defaults to ‘*’
100 | }).then(function(messages) {})
101 | ```
102 |
103 | Messages have the following attributes:
104 |
105 | * uid: The UID in the mailbox
106 | * id: The Mesage-ID header (without "<>")
107 | * inReplyTo: The Message-ID that this message is a reply to
108 | * references: The Message-IDs that this message references
109 | * from, replyTo, to, cc, bcc: The Sender/Receivers
110 | * modseq: The MODSEQ number of this message (as a string – javascript numbers do not tolerate 64 bit uints)
111 | * subject: The message's subject
112 | * sentDate: The date the message was sent
113 | * unread: The unread flag
114 | * answered: The answered flag
115 | * bodyParts: Array of message parts, simplified version of a MIME tree. Used by #getBodyParts
116 |
117 | ### #getBodyParts()
118 |
119 | Fetches parts of a message from the imap server
120 |
121 | ```
122 | imap.getBodyParts({
123 | path: 'foo/bar',
124 | uid: someMessage.uid,
125 | bodyParts: someMessage.bodyParts
126 | }).then(function() {
127 | // all done, bodyparts can now be fed to the mailreader
128 | })
129 | ```
130 |
131 | ### #updateFlags(options, callback)
132 |
133 | Marks a message as un-/read or un-/answered.
134 |
135 | ```
136 | imap.updateFlags({
137 | path: 'foo/bar',
138 | uid: someMessage.uid,
139 | unread: true/false/undefined, // (optional) Marks the message as un-/read, no action if omitted
140 | answered: true/false/undefined // (optional) Marks the message as answered, no action if omitted
141 | }).then(function(
142 | // all done
143 | });
144 | ```
145 |
146 | ### #moveMessage(options, callback)
147 |
148 | Moves a message from mailbox A to mailbox B.
149 |
150 | ```
151 | imap.moveMessage({
152 | path: 'foo/bar', // the origin folder
153 | uid: someMessage.uid, // the message's uid
154 | destination: 'bla/bli' // the destination folder
155 | }).then(function(
156 | // all done
157 | });
158 | ```
159 |
160 | ### uploadMessage(options, callback)
161 |
162 | Uploads a message to a folder
163 |
164 | ```
165 | imap.uploadMessage({
166 | path: 'foo/bar', // the target folder
167 | message: '...' // RFC-2822 compliant string
168 | }).then(function(
169 | // all done
170 | });
171 | ```
172 |
173 | ### #deleteMessage(options, callback)
174 |
175 | Deletes a message from a folder
176 |
177 | ```
178 | imap.deleteMessage({
179 | path: 'foo/bar', // the folder from which to delete the message
180 | uid: someMessage.uid, // the message's uid
181 | }).then(function(
182 | // all done
183 | });
184 | ```
185 |
186 | ### #onSyncUpdate
187 |
188 | If there are updates available for an IMAP folder, you will receive the changed UIDs in the `#onSyncUpdate` callback. The IMAP client invokes the callback if there are new/changes messages after a mailbox has been selected and on IMAP expunge/exists/fetch updates have been pushed from the server.
189 | If this handler is not set, you will not receive updates from IMAP.
190 |
191 | ```
192 | var SYNC_TYPE_NEW = 'new';
193 | var SYNC_TYPE_DELETED = 'deleted';
194 | var SYNC_TYPE_MSGS = 'messages';
195 |
196 | imap.onSyncUpdate = function(options) {
197 | var updatedMesages = options.list,
198 | updatesMailbox = options.path
199 |
200 | if (options.type === SYNC_TYPE_NEW) {
201 | // new messages available on imap
202 | // updatedMesages is an array of the newly available UIDs
203 | } else if (options.type === SYNC_TYPE_DELETED) {
204 | // messages have been deleted
205 | // updatedMesages is an array of the deleted UIDs
206 | } else if (options.type === SYNC_TYPE_MSGS) {
207 | // NB! several possible reasons why this could be called.
208 | // updatedMesages is an array of objects
209 | // if an object in the array has uid value and flags array, it had a possible flag update
210 | }
211 | };
212 | ```
213 |
214 | ## Getting started
215 |
216 | Run the following commands to get started:
217 |
218 | npm install && grunt
219 |
220 | ## License
221 |
222 | ```
223 | The MIT License (MIT)
224 |
225 | Copyright (c) 2014 Whiteout Networks GmbH
226 |
227 | Permission is hereby granted, free of charge, to any person obtaining a copy of
228 | this software and associated documentation files (the "Software"), to deal in
229 | the Software without restriction, including without limitation the rights to
230 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
231 | the Software, and to permit persons to whom the Software is furnished to do so,
232 | subject to the following conditions:
233 |
234 | The above copyright notice and this permission notice shall be included in all
235 | copies or substantial portions of the Software.
236 |
237 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
238 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
239 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
240 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
241 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
242 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
243 | ```
244 |
--------------------------------------------------------------------------------
/test/local-integration-test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require('es6-promise').polyfill(); // load ES6 Promises polyfill
4 |
5 | // this test is node-only (hoodiecrow is fired up)
6 |
7 | var chai = require('chai'),
8 | expect = chai.expect,
9 | ImapClient = require('../src/imap-client'),
10 | hoodiecrow = require('hoodiecrow'),
11 | loginOptions = {
12 | port: 12345,
13 | host: 'localhost',
14 | auth: {
15 | user: 'testuser',
16 | pass: 'testpass'
17 | },
18 | secure: false
19 | };
20 |
21 | describe('ImapClient local integration tests', function() {
22 | var ic, imap;
23 |
24 | chai.config.includeStack = true;
25 | before(function() {
26 | imap = hoodiecrow({
27 | storage: {
28 | 'INBOX': {
29 | messages: [{
30 | raw: 'Message-Id: \r\nX-Foobar: 123qweasdzxc\r\nSubject: hello 1\r\n\r\nWorld 1!'
31 | }, {
32 | raw: 'Message-Id: \r\nSubject: hello 2\r\n\r\nWorld 2!',
33 | flags: ['\\Seen']
34 | }, {
35 | raw: 'Message-Id: \r\nSubject: hello 3\r\n\r\nWorld 3!'
36 | }, {
37 | raw: 'MIME-Version: 1.0\r\nDate: Tue, 01 Oct 2013 07:08:55 GMT\r\nMessage-Id: <1380611335900.56da46df@Nodemailer>\r\nFrom: alice@example.com\r\nTo: bob@example.com\r\nSubject: Hello\r\nContent-Type: multipart/mixed;\r\n boundary="----Nodemailer-0.5.3-dev-?=_1-1380611336047"\r\n\r\n------Nodemailer-0.5.3-dev-?=_1-1380611336047\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\nHello world\r\n------Nodemailer-0.5.3-dev-?=_1-1380611336047\r\nContent-Type: text/plain; name="foo.txt"\r\nContent-Disposition: attachment; filename="foo.txt"\r\nContent-Transfer-Encoding: base64\r\n\r\nZm9vZm9vZm9vZm9vZm9v\r\n------Nodemailer-0.5.3-dev-?=_1-1380611336047\r\nContent-Type: text/plain; name="bar.txt"\r\nContent-Disposition: attachment; filename="bar.txt"\r\nContent-Transfer-Encoding: base64\r\n\r\nYmFyYmFyYmFyYmFyYmFy\r\n------Nodemailer-0.5.3-dev-?=_1-1380611336047--'
38 | }, {
39 | raw: 'Content-Type: multipart/encrypted; boundary="Apple-Mail=_CC38E51A-DB4D-420E-AD14-02653EB88B69"; protocol="application/pgp-encrypted";\r\nSubject: [whiteout] attachment only\r\nFrom: Felix Hammerl \r\nDate: Thu, 16 Jan 2014 14:55:56 +0100\r\nContent-Transfer-Encoding: 7bit\r\nMessage-Id: <3ECDF9DC-895E-4475-B2A9-52AF1F117652@gmail.com>\r\nContent-Description: OpenPGP encrypted message\r\nTo: safewithme.testuser@gmail.com\r\n\r\nThis is an OpenPGP/MIME encrypted message (RFC 2440 and 3156)\r\n--Apple-Mail=_CC38E51A-DB4D-420E-AD14-02653EB88B69\r\nContent-Transfer-Encoding: 7bit\r\nContent-Type: application/pgp-encrypted\r\nContent-Description: PGP/MIME Versions Identification\r\n\r\nVersion: 1\r\n\r\n--Apple-Mail=_CC38E51A-DB4D-420E-AD14-02653EB88B69\r\nContent-Transfer-Encoding: 7bit\r\nContent-Disposition: inline;\r\n filename=encrypted.asc\r\nContent-Type: application/octet-stream;\r\n name=encrypted.asc\r\nContent-Description: OpenPGP encrypted message\r\n\r\ninsert pgp here.\r\n\r\n--Apple-Mail=_CC38E51A-DB4D-420E-AD14-02653EB88B69--',
40 | }, {
41 | raw: 'MIME-Version: 1.0\r\nDate: Tue, 01 Oct 2013 07:08:55 GMT\r\nMessage-Id: <1380611335900.56da46df@Nodemailer>\r\nFrom: alice@example.com\r\nTo: bob@example.com\r\nSubject: Hello\r\nContent-Type: multipart/mixed;\r\n boundary="----Nodemailer-0.5.3-dev-?=_1-1380611336047"\r\n\r\n------Nodemailer-0.5.3-dev-?=_1-1380611336047\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\nHello world\r\n------Nodemailer-0.5.3-dev-?=_1-1380611336047\r\nContent-Type: text/plain; name="foo.txt"\r\nContent-Disposition: attachment; filename="foo.txt"\r\nContent-Transfer-Encoding: base64\r\n\r\nZm9vZm9vZm9vZm9vZm9v\r\n------Nodemailer-0.5.3-dev-?=_1-1380611336047\r\nContent-Type: text/plain; name="bar.txt"\r\nContent-Disposition: attachment; filename="bar.txt"\r\nContent-Transfer-Encoding: base64\r\n\r\nYmFyYmFyYmFyYmFyYmFy\r\n------Nodemailer-0.5.3-dev-?=_1-1380611336047--'
42 | }]
43 | },
44 | '': {
45 | 'separator': '/',
46 | 'folders': {
47 | '[Gmail]': {
48 | 'flags': ['\\Noselect'],
49 | 'folders': {
50 | 'All Mail': {
51 | 'flags': '\\All'
52 | },
53 | 'Drafts': {
54 | 'flags': '\\Drafts'
55 | },
56 | 'Important': {
57 | 'flags': '\\Important'
58 | },
59 | 'Sent Mail': {
60 | 'flags': '\\Sent'
61 | },
62 | 'Spam': {
63 | 'flags': '\\Junk'
64 | },
65 | 'Starred': {
66 | 'flags': '\\Flagged'
67 | },
68 | 'Trash': {
69 | 'flags': '\\Trash'
70 | }
71 | }
72 | }
73 | }
74 | }
75 | }
76 | });
77 |
78 | imap.listen(loginOptions.port);
79 | });
80 |
81 | after(function(done) {
82 | imap.close(done);
83 | });
84 |
85 | beforeEach(function(done) {
86 | ic = new ImapClient(loginOptions);
87 | ic.onSyncUpdate = function() {};
88 | ic.login().then(done);
89 | });
90 |
91 | afterEach(function(done) {
92 | ic.logout().then(done);
93 | });
94 |
95 | it('should notify about new messages', function(done) {
96 | var invocations = 0; // counts the message updates
97 |
98 | ic.onSyncUpdate = function(options) {
99 | invocations++;
100 |
101 | expect(options.list.length).to.equal(6);
102 | expect(options.type).to.equal('new');
103 | done();
104 | };
105 |
106 | ic.selectMailbox({
107 | path: 'INBOX'
108 | });
109 | });
110 |
111 | it('should list well known folders', function(done) {
112 | ic.listWellKnownFolders().then(function(folders) {
113 | expect(folders).to.exist;
114 |
115 | expect(folders.Inbox).to.be.instanceof(Array);
116 | expect(folders.Inbox[0]).to.exist;
117 | expect(folders.Inbox[0].name).to.exist;
118 | expect(folders.Inbox[0].type).to.exist;
119 | expect(folders.Inbox[0].path).to.exist;
120 |
121 | expect(folders.Drafts).to.be.instanceof(Array);
122 | expect(folders.Drafts).to.not.be.empty;
123 |
124 | expect(folders.Sent).to.be.instanceof(Array);
125 | expect(folders.Sent).to.not.be.empty;
126 |
127 | expect(folders.Trash).to.be.instanceof(Array);
128 | expect(folders.Trash).to.not.be.empty;
129 |
130 | expect(folders.Other).to.be.instanceof(Array);
131 | expect(folders.Other).to.not.be.empty;
132 | }).then(done);
133 | });
134 |
135 | it('should search messages', function(done) {
136 | ic.search({
137 | path: 'INBOX',
138 | unread: false,
139 | answered: false
140 | }).then(function(uids) {
141 | expect(uids).to.not.be.empty;
142 | }).then(done);
143 | });
144 |
145 | it('should create folder', function(done) {
146 | ic.createFolder({
147 | path: 'foo'
148 | }).then(function(fullPath) {
149 | expect(fullPath).to.equal('foo');
150 | return ic.listWellKnownFolders();
151 |
152 | }).then(function(folders) {
153 | var hasFoo = false;
154 |
155 | folders.Other.forEach(function(folder) {
156 | hasFoo = hasFoo || folder.path === 'foo';
157 | });
158 |
159 | expect(hasFoo).to.be.true;
160 | expect(ic._delimiter).to.exist;
161 | expect(ic._prefix).to.exist;
162 | }).then(done);
163 | });
164 |
165 | it('should create folder hierarchy', function(done) {
166 | ic.createFolder({
167 | path: ['bar', 'baz']
168 | }).then(function(fullPath) {
169 | expect(fullPath).to.equal('bar/baz');
170 | return ic.listWellKnownFolders();
171 | }).then(function(folders) {
172 | var hasFoo = false;
173 |
174 | folders.Other.forEach(function(folder) {
175 | hasFoo = hasFoo || folder.path === 'bar/baz';
176 | });
177 |
178 | expect(hasFoo).to.be.true;
179 | }).then(done);
180 | });
181 |
182 | it('should search messages for header', function(done) {
183 | ic.search({
184 | path: 'INBOX',
185 | header: ['X-Foobar', '123qweasdzxc']
186 | }).then(function(uids) {
187 | expect(uids).to.deep.equal([1]);
188 | }).then(done);
189 | });
190 |
191 | it('should list messages by uid', function(done) {
192 | ic.listMessages({
193 | path: 'INBOX',
194 | firstUid: 1,
195 | lastUid: 3
196 | }).then(function(messages) {
197 | expect(messages).to.not.be.empty;
198 | expect(messages.length).to.equal(3);
199 | expect(messages[0].id).to.not.be.empty;
200 | expect(messages[0].bodyParts.length).to.equal(1);
201 | }).then(done);
202 | });
203 |
204 | it('should list all messages by uid', function(done) {
205 | ic.listMessages({
206 | path: 'INBOX',
207 | firstUid: 1
208 | }).then(function(messages) {
209 | expect(messages).to.not.be.empty;
210 | expect(messages.length).to.equal(6);
211 | }).then(done);
212 | });
213 |
214 | it('should get message parts', function(done) {
215 | var msgs;
216 | ic.listMessages({
217 | path: 'INBOX',
218 | firstUid: 4,
219 | lastUid: 4
220 | }).then(function(messages) {
221 | msgs = messages;
222 | return ic.getBodyParts({
223 | path: 'INBOX',
224 | uid: messages[0].uid,
225 | bodyParts: messages[0].bodyParts
226 | });
227 |
228 | }).then(function(bodyParts) {
229 | expect(msgs[0].bodyParts).to.equal(bodyParts);
230 | expect(bodyParts[0].type).to.equal('text');
231 | expect(bodyParts[0].raw).to.equal('Content-Type: text/plain; charset=utf-8\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\nHello world');
232 |
233 | }).then(done);
234 | });
235 |
236 | it('should update flags', function(done) {
237 | ic.updateFlags({
238 | path: 'INBOX',
239 | uid: 1,
240 | unread: true,
241 | flagged: true,
242 | answered: true
243 | }).then(function() {
244 | done();
245 | });
246 | });
247 |
248 | it('should purge message', function(done) {
249 | ic.listMessages({
250 | path: 'INBOX',
251 | firstUid: 1
252 | }).then(function(messages) {
253 | expect(messages).to.not.be.empty;
254 | return ic.deleteMessage({
255 | path: 'INBOX',
256 | uid: 2
257 | });
258 |
259 | }).then(function() {
260 | return ic.listMessages({
261 | path: 'INBOX',
262 | firstUid: 1
263 | });
264 |
265 | }).then(function(messages) {
266 | expect(messages).to.not.be.empty;
267 | messages.forEach(function(message) {
268 | expect(message.uid).to.not.equal(2);
269 | });
270 |
271 | }).then(done);
272 | });
273 |
274 | it('should upload Message', function(done) {
275 | var msg = 'MIME-Version: 1.0\r\nDate: Wed, 9 Jul 2014 15:07:47 +0200\r\nDelivered-To: test@test.com\r\nMessage-ID: \r\nSubject: test\r\nFrom: Test Test \r\nTo: Test Test \r\nContent-Type: text/plain; charset=UTF-8\r\n\r\ntest',
276 | path = 'INBOX',
277 | msgCount;
278 |
279 | ic.listMessages({
280 | path: path,
281 | firstUid: 1
282 | }).then(function(messages) {
283 | expect(messages).to.not.be.empty;
284 | msgCount = messages.length;
285 |
286 | return ic.uploadMessage({
287 | path: path,
288 | message: msg,
289 | flags: ['\\Seen']
290 | });
291 | }).then(function() {
292 | return ic.listMessages({
293 | path: path,
294 | firstUid: 1
295 | });
296 | }).then(function(messages) {
297 | expect(messages.length).to.equal(msgCount + 1);
298 | }).then(done);
299 | });
300 |
301 | it('should move message', function(done) {
302 | var destination = '[Gmail]/Trash';
303 |
304 | ic.listMessages({
305 | path: destination,
306 | firstUid: 1
307 | }).then(function(messages) {
308 | expect(messages).to.be.empty;
309 | return ic.listMessages({
310 | path: 'INBOX',
311 | firstUid: 1
312 | });
313 |
314 | }).then(function(messages) {
315 | expect(messages).to.not.be.empty;
316 | return ic.moveMessage({
317 | path: 'INBOX',
318 | uid: messages[0].uid,
319 | destination: destination
320 | });
321 |
322 | }).then(function() {
323 | return ic.listMessages({
324 | path: destination,
325 | firstUid: 1
326 | });
327 |
328 | }).then(function(messages) {
329 | expect(messages).to.not.be.empty;
330 | }).then(done);
331 | });
332 |
333 | it('should timeout', function(done) {
334 | ic.onError = function(err) {
335 | expect(err).to.exist;
336 | expect(ic._loggedIn).to.be.false;
337 | done();
338 | };
339 |
340 | ic._client.client.TIMEOUT_SOCKET_LOWER_BOUND = 20; // fails 20ms after writing to socket
341 | ic._client.client.socket.ondata = function() {}; // browserbox won't be receiving data anymore
342 |
343 | // fire anything at the socket
344 | ic.listMessages({
345 | path: 'INBOX',
346 | firstUid: 1
347 | });
348 | });
349 |
350 | it.skip('should not error for listening client timeout', function(done) {
351 | ic.listenForChanges({
352 | path: 'INBOX'
353 | }).then(function() {
354 | ic._listeningClient.client.TIMEOUT_SOCKET_LOWER_BOUND = 20; // fails 20ms after dropping into idle/noop
355 | ic._listeningClient.client.socket.ondata = function() {}; // browserbox won't be receiving data anymore
356 |
357 | // the listening client does not cause an error, so we let it fail silently
358 | // in the background and check back after a 1 s delay
359 | setTimeout(function() {
360 | expect(ic._listenerLoggedIn).to.be.false;
361 | done();
362 | }, 1000);
363 | });
364 | });
365 | });
--------------------------------------------------------------------------------
/test/unit-test.js:
--------------------------------------------------------------------------------
1 | (function(factory) {
2 | 'use strict';
3 |
4 | if (typeof define === 'function' && define.amd) {
5 | ES6Promise.polyfill(); // load ES6 Promises polyfill
6 | define(['chai', 'sinon', 'browserbox', 'axe', 'imap-client'], factory);
7 | } else if (typeof exports === 'object') {
8 | require('es6-promise').polyfill(); // load ES6 Promises polyfill
9 | module.exports = factory(require('chai'), require('sinon'), require('browserbox'), require('axe-logger'), require('../src/imap-client'));
10 | }
11 | })(function(chai, sinon, browserbox, axe, ImapClient) {
12 | 'use strict';
13 |
14 | // don't log in the tests
15 | axe.removeAppender(axe.defaultAppender);
16 |
17 | describe('ImapClient', function() {
18 | var expect = chai.expect;
19 | chai.config.includeStack = true;
20 |
21 | var imap, bboxMock;
22 |
23 | beforeEach(function() {
24 | bboxMock = sinon.createStubInstance(browserbox);
25 | imap = new ImapClient({}, bboxMock);
26 |
27 | expect(imap._client).to.equal(bboxMock);
28 |
29 | imap._loggedIn = true;
30 | });
31 |
32 | describe('#login', function() {
33 | it('should login', function(done) {
34 | imap._loggedIn = false;
35 |
36 | imap.login().then(function() {
37 | expect(imap._loggedIn).to.be.true;
38 | expect(bboxMock.connect.calledOnce).to.be.true;
39 | }).then(done);
40 | bboxMock.onauth();
41 | });
42 |
43 | it('should reject on login failure', function(done) {
44 | imap._loggedIn = false;
45 | imap.login()
46 | .catch(function() {
47 | expect(imap._loggedIn).to.be.false;
48 | expect(bboxMock.connect.calledOnce).to.be.true;
49 | done();
50 | });
51 | bboxMock.onerror('login error');
52 | });
53 |
54 | it('should not login when logged in', function(done) {
55 | imap._loggedIn = true;
56 | imap.login().then(done);
57 | });
58 | });
59 |
60 | describe('#logout', function() {
61 | it('should logout', function(done) {
62 | imap.logout().then(function() {
63 | expect(bboxMock.close.calledOnce).to.be.true;
64 | expect(imap._loggedIn).to.be.false;
65 | }).then(done);
66 | bboxMock.onclose();
67 | });
68 |
69 | it('should not logout when not logged in', function(done) {
70 | imap._loggedIn = false;
71 | imap.logout().then(done);
72 | });
73 | });
74 |
75 | describe('#_onError', function() {
76 | it('should report error for main imap connection', function(done) {
77 | imap.onError = function(error) {
78 | expect(error).to.exist;
79 | expect(imap._loggedIn).to.be.false;
80 | expect(bboxMock.close.calledOnce).to.be.true;
81 |
82 | done();
83 | };
84 |
85 | bboxMock.onerror();
86 | });
87 |
88 | it('should not error for listening imap connection', function() {
89 | imap._loggedIn = false;
90 | imap._listenerLoggedIn = true;
91 | imap._client = {}; // _client !== _listeningClient
92 |
93 | bboxMock.onerror();
94 |
95 | expect(imap._listenerLoggedIn).to.be.false;
96 | expect(bboxMock.close.calledOnce).to.be.true;
97 | });
98 | });
99 |
100 | describe('#_onClose', function() {
101 | it('should error for main imap connection', function(done) {
102 | imap.onError = function(error) {
103 | expect(error).to.exist;
104 | expect(imap._loggedIn).to.be.false;
105 |
106 | done();
107 | };
108 |
109 | bboxMock.onclose();
110 | });
111 |
112 | it('should not error for listening imap connection', function() {
113 | imap._loggedIn = false;
114 | imap._listenerLoggedIn = true;
115 | imap._client = {}; // _client !== _listeningClient
116 |
117 | bboxMock.onclose();
118 |
119 | expect(imap._listenerLoggedIn).to.be.false;
120 | });
121 |
122 | });
123 |
124 | describe('#selectMailbox', function() {
125 | var path = 'foo';
126 |
127 | it('should select a different mailbox', function(done) {
128 | bboxMock.selectMailbox.withArgs(path).returns(resolves());
129 |
130 | imap.selectMailbox({
131 | path: path
132 | }).then(done);
133 | });
134 | });
135 |
136 | describe('#listWellKnownFolders', function() {
137 | it('should list well known folders', function(done) {
138 | // setup fixture
139 | bboxMock.listMailboxes.returns(resolves({
140 | children: [{
141 | path: 'INBOX'
142 | }, {
143 | name: 'drafts',
144 | path: 'drafts',
145 | specialUse: '\\Drafts'
146 | }, {
147 | name: 'sent',
148 | path: 'sent',
149 | specialUse: '\\Sent'
150 | }]
151 | }));
152 |
153 | // execute test case
154 | imap.listWellKnownFolders().then(function(folders) {
155 | expect(folders).to.exist;
156 |
157 | expect(folders.Inbox).to.be.instanceof(Array);
158 | expect(folders.Inbox[0]).to.exist;
159 | expect(folders.Inbox[0].name).to.exist;
160 | expect(folders.Inbox[0].type).to.exist;
161 | expect(folders.Inbox[0].path).to.exist;
162 |
163 | expect(folders.Drafts).to.be.instanceof(Array);
164 | expect(folders.Drafts).to.not.be.empty;
165 |
166 | expect(folders.Sent).to.be.instanceof(Array);
167 | expect(folders.Sent).to.not.be.empty;
168 |
169 | expect(folders.Trash).to.be.instanceof(Array);
170 | expect(folders.Trash).to.be.empty;
171 |
172 | expect(folders.Other).to.be.instanceof(Array);
173 |
174 | expect(bboxMock.listMailboxes.calledOnce).to.be.true;
175 | }).then(done);
176 | });
177 |
178 | it('should not list folders when not logged in', function(done) {
179 | imap._loggedIn = false;
180 | imap.listWellKnownFolders().catch(function() {
181 | done();
182 | });
183 | });
184 |
185 | it('should error while listing folders', function(done) {
186 | // setup fixture
187 | bboxMock.listMailboxes.returns(rejects());
188 |
189 | // execute test case
190 | imap.listWellKnownFolders().catch(function() {
191 | done();
192 | });
193 | });
194 | });
195 |
196 | describe('#createFolder', function() {
197 | it('should create folder with namespaces', function(done) {
198 | bboxMock.listNamespaces.returns(resolves({
199 | "personal": [{
200 | "prefix": "BLA/",
201 | "delimiter": "/"
202 | }],
203 | "users": false,
204 | "shared": false
205 | }));
206 | bboxMock.createMailbox.withArgs('BLA/foo').returns(resolves());
207 |
208 | imap.createFolder({
209 | path: 'foo'
210 | }).then(function(fullPath) {
211 | expect(fullPath).to.equal('BLA/foo');
212 | expect(bboxMock.listNamespaces.calledOnce).to.be.true;
213 | expect(bboxMock.createMailbox.calledOnce).to.be.true;
214 | expect(imap._delimiter).to.exist;
215 | expect(imap._prefix).to.exist;
216 | done();
217 | });
218 | });
219 |
220 | it('should create folder without namespaces', function(done) {
221 | bboxMock.listNamespaces.returns(resolves());
222 | bboxMock.listMailboxes.returns(resolves({
223 | "root": true,
224 | "children": [{
225 | "name": "INBOX",
226 | "delimiter": "/",
227 | "path": "INBOX"
228 | }]
229 | }));
230 | bboxMock.createMailbox.withArgs('foo').returns(resolves());
231 |
232 | imap.createFolder({
233 | path: 'foo'
234 | }).then(function(fullPath) {
235 | expect(fullPath).to.equal('foo');
236 | expect(bboxMock.listNamespaces.calledOnce).to.be.true;
237 | expect(bboxMock.createMailbox.calledOnce).to.be.true;
238 | expect(imap._delimiter).to.exist;
239 | expect(imap._prefix).to.exist;
240 | done();
241 | });
242 | });
243 |
244 | it('should create folder hierarchy with namespaces', function(done) {
245 | bboxMock.listNamespaces.returns(resolves({
246 | "personal": [{
247 | "prefix": "BLA/",
248 | "delimiter": "/"
249 | }],
250 | "users": false,
251 | "shared": false
252 | }));
253 | bboxMock.createMailbox.withArgs('foo/bar').returns(resolves());
254 | bboxMock.createMailbox.withArgs('foo/baz').returns(resolves());
255 |
256 | imap.createFolder({
257 | path: ['foo', 'bar']
258 | }).then(function(fullPath) {
259 | expect(fullPath).to.equal('BLA/foo/bar');
260 |
261 | return imap.createFolder({
262 | path: ['foo', 'baz']
263 | });
264 | }).then(function(fullPath) {
265 | expect(fullPath).to.equal('BLA/foo/baz');
266 |
267 | expect(bboxMock.listNamespaces.calledOnce).to.be.true;
268 | expect(bboxMock.createMailbox.calledTwice).to.be.true;
269 | expect(imap._delimiter).to.exist;
270 | expect(imap._prefix).to.exist;
271 | done();
272 | });
273 | });
274 |
275 | it('should create folder hierarchy without namespaces', function(done) {
276 | bboxMock.listNamespaces.returns(resolves());
277 | bboxMock.listMailboxes.returns(resolves({
278 | "root": true,
279 | "children": [{
280 | "name": "INBOX",
281 | "delimiter": "/",
282 | "path": "INBOX"
283 | }]
284 | }));
285 | bboxMock.createMailbox.withArgs('foo').returns(resolves());
286 |
287 | imap.createFolder({
288 | path: ['foo', 'bar']
289 | }).then(function(fullPath) {
290 | expect(fullPath).to.equal('foo/bar');
291 | expect(bboxMock.listNamespaces.calledOnce).to.be.true;
292 | expect(bboxMock.createMailbox.calledOnce).to.be.true;
293 | expect(imap._delimiter).to.exist;
294 | expect(imap._prefix).to.exist;
295 | done();
296 | });
297 | });
298 | });
299 |
300 | describe('#search', function() {
301 | it('should search answered', function(done) {
302 | bboxMock.search.withArgs({
303 | all: true,
304 | answered: true
305 | }).returns(resolves([1, 3, 5]));
306 |
307 | imap.search({
308 | path: 'foobar',
309 | answered: true
310 | }).then(function(uids) {
311 | expect(uids.length).to.equal(3);
312 | }).then(done);
313 | });
314 |
315 | it('should search unanswered', function(done) {
316 | bboxMock.search.withArgs({
317 | all: true,
318 | unanswered: true
319 | }).returns(resolves([1, 3, 5]));
320 |
321 | imap.search({
322 | path: 'foobar',
323 | answered: false
324 | }).then(function(uids) {
325 | expect(uids.length).to.equal(3);
326 | }).then(done);
327 | });
328 |
329 | it('should search header', function(done) {
330 | bboxMock.search.withArgs({
331 | all: true,
332 | header: ['Foo', 'bar']
333 | }).returns(resolves([1, 3, 5]));
334 |
335 | imap.search({
336 | path: 'foobar',
337 | header: ['Foo', 'bar']
338 | }).then(function(uids) {
339 | expect(uids.length).to.equal(3);
340 | }).then(done);
341 | });
342 |
343 | it('should search read', function(done) {
344 | bboxMock.search.withArgs({
345 | all: true,
346 | seen: true
347 | }).returns(resolves([1, 3, 5]));
348 |
349 | imap.search({
350 | path: 'foobar',
351 | unread: false
352 | }).then(function(uids) {
353 | expect(uids.length).to.equal(3);
354 | }).then(done);
355 | });
356 |
357 | it('should search unread', function(done) {
358 | bboxMock.search.withArgs({
359 | all: true,
360 | unseen: true
361 | }).returns(resolves([1, 3, 5]));
362 |
363 | imap.search({
364 | path: 'foobar',
365 | unread: true
366 | }).then(function(uids) {
367 | expect(uids.length).to.equal(3);
368 | }).then(done);
369 | });
370 |
371 | it('should not search when not logged in', function(done) {
372 | imap._loggedIn = false;
373 | imap.search({
374 | path: 'foobar',
375 | subject: 'whiteout '
376 | }).catch(function() {
377 | done();
378 | });
379 | });
380 | });
381 |
382 | describe('#listMessages', function() {
383 | it('should list messages by uid', function(done) {
384 | var listing = [{
385 | uid: 1,
386 | envelope: {
387 | 'message-id': 'beepboop',
388 | from: ['zuhause@aol.com'],
389 | 'reply-to': ['zzz@aol.com'],
390 | to: ['bankrupt@duh.com'],
391 | subject: 'SHIAAAT',
392 | date: new Date()
393 | },
394 | flags: ['\\Seen', '\\Answered', '\\Flagged'],
395 | bodystructure: {
396 | type: 'multipart/mixed',
397 | childNodes: [{
398 | part: '1',
399 | type: 'text/plain'
400 | }, {
401 | part: '2',
402 | type: 'text/plain',
403 | size: 211,
404 | disposition: 'attachment',
405 | dispositionParameters: {
406 | filename: 'foobar.md'
407 | }
408 | }]
409 | },
410 | 'body[header.fields (references)]': 'References: \n \n\n'
411 | }, {
412 | uid: 2,
413 | envelope: {
414 | 'message-id': 'ajabwelvzbslvnasd',
415 | },
416 | bodystructure: {
417 | type: 'multipart/mixed',
418 | childNodes: [{
419 | part: '1',
420 | type: 'text/plain',
421 | }, {
422 | part: '2',
423 | type: 'multipart/encrypted',
424 | childNodes: [{
425 | part: '2.1',
426 | type: 'application/pgp-encrypted',
427 | }, {
428 | part: '2.2',
429 | type: 'application/octet-stream',
430 | }]
431 | }]
432 | }
433 | }];
434 | bboxMock.listMessages.withArgs('1:2', ['uid', 'bodystructure', 'flags', 'envelope', 'body.peek[header.fields (references)]']).returns(resolves(listing));
435 |
436 | imap.listMessages({
437 | path: 'foobar',
438 | firstUid: 1,
439 | lastUid: 2
440 | }).then(function(msgs) {
441 | expect(bboxMock.listMessages.calledOnce).to.be.true;
442 |
443 | expect(msgs.length).to.equal(2);
444 |
445 | expect(msgs[0].uid).to.equal(1);
446 | expect(msgs[0].id).to.equal('beepboop');
447 | expect(msgs[0].from).to.be.instanceof(Array);
448 | expect(msgs[0].replyTo).to.be.instanceof(Array);
449 | expect(msgs[0].to).to.be.instanceof(Array);
450 | expect(msgs[0].subject).to.equal('SHIAAAT');
451 | expect(msgs[0].unread).to.be.false;
452 | expect(msgs[0].answered).to.be.true;
453 | expect(msgs[0].flagged).to.be.true;
454 | expect(msgs[0].references).to.deep.equal(['abc', 'def']);
455 |
456 | expect(msgs[0].encrypted).to.be.false;
457 | expect(msgs[1].encrypted).to.be.true;
458 |
459 | expect(msgs[0].bodyParts).to.not.be.empty;
460 | expect(msgs[0].bodyParts[0].type).to.equal('text');
461 | expect(msgs[0].bodyParts[0].partNumber).to.equal('1');
462 | expect(msgs[0].bodyParts[1].type).to.equal('attachment');
463 | expect(msgs[0].bodyParts[1].partNumber).to.equal('2');
464 |
465 | expect(msgs[1].flagged).to.be.false;
466 | expect(msgs[1].bodyParts[0].type).to.equal('text');
467 | expect(msgs[1].bodyParts[1].type).to.equal('encrypted');
468 | expect(msgs[1].bodyParts[1].partNumber).to.equal('2');
469 | expect(msgs[1].references).to.deep.equal([]);
470 | }).then(done);
471 | });
472 |
473 | it('should list messages by uid', function(done) {
474 | var listing = [{
475 | uid: 1,
476 | envelope: {
477 | 'message-id': 'beepboop',
478 | from: ['zuhause@aol.com'],
479 | 'reply-to': ['zzz@aol.com'],
480 | to: ['bankrupt@duh.com'],
481 | subject: 'SHIAAAT',
482 | date: new Date()
483 | },
484 | flags: ['\\Seen', '\\Answered', '\\Flagged'],
485 | bodystructure: {
486 | type: 'multipart/mixed',
487 | childNodes: [{
488 | part: '1',
489 | type: 'text/plain'
490 | }, {
491 | part: '2',
492 | type: 'text/plain',
493 | size: 211,
494 | disposition: 'attachment',
495 | dispositionParameters: {
496 | filename: 'foobar.md'
497 | }
498 | }]
499 | },
500 | 'body[header.fields (references)]': 'References: \n \n\n'
501 | }, {
502 | uid: 4,
503 | envelope: {
504 | 'message-id': 'ajabwelvzbslvnasd',
505 | },
506 | bodystructure: {
507 | type: 'multipart/mixed',
508 | childNodes: [{
509 | part: '1',
510 | type: 'text/plain',
511 | }, {
512 | part: '2',
513 | type: 'multipart/encrypted',
514 | childNodes: [{
515 | part: '2.1',
516 | type: 'application/pgp-encrypted',
517 | }, {
518 | part: '2.2',
519 | type: 'application/octet-stream',
520 | }]
521 | }]
522 | }
523 | }];
524 | bboxMock.listMessages.withArgs('1,4', ['uid', 'bodystructure', 'flags', 'envelope', 'body.peek[header.fields (references)]']).returns(resolves(listing));
525 |
526 | imap.listMessages({
527 | path: 'foobar',
528 | uids: [1,4]
529 | }).then(function(msgs) {
530 | expect(bboxMock.listMessages.calledOnce).to.be.true;
531 |
532 | expect(msgs.length).to.equal(2);
533 |
534 | expect(msgs[0].uid).to.equal(1);
535 | expect(msgs[0].id).to.equal('beepboop');
536 | expect(msgs[0].from).to.be.instanceof(Array);
537 | expect(msgs[0].replyTo).to.be.instanceof(Array);
538 | expect(msgs[0].to).to.be.instanceof(Array);
539 | expect(msgs[0].subject).to.equal('SHIAAAT');
540 | expect(msgs[0].unread).to.be.false;
541 | expect(msgs[0].answered).to.be.true;
542 | expect(msgs[0].flagged).to.be.true;
543 | expect(msgs[0].references).to.deep.equal(['abc', 'def']);
544 |
545 | expect(msgs[0].encrypted).to.be.false;
546 | expect(msgs[1].encrypted).to.be.true;
547 |
548 | expect(msgs[0].bodyParts).to.not.be.empty;
549 | expect(msgs[0].bodyParts[0].type).to.equal('text');
550 | expect(msgs[0].bodyParts[0].partNumber).to.equal('1');
551 | expect(msgs[0].bodyParts[1].type).to.equal('attachment');
552 | expect(msgs[0].bodyParts[1].partNumber).to.equal('2');
553 |
554 | expect(msgs[1].flagged).to.be.false;
555 | expect(msgs[1].bodyParts[0].type).to.equal('text');
556 | expect(msgs[1].bodyParts[1].type).to.equal('encrypted');
557 | expect(msgs[1].bodyParts[1].partNumber).to.equal('2');
558 | expect(msgs[1].references).to.deep.equal([]);
559 | }).then(done);
560 | });
561 |
562 | it('should not list messages by uid due to list error', function(done) {
563 | bboxMock.listMessages.returns(rejects());
564 |
565 | imap.listMessages({
566 | path: 'foobar',
567 | firstUid: 1,
568 | lastUid: 2
569 | }).catch(function() {
570 | done();
571 | });
572 | });
573 |
574 | it('should not list messages by uid when not logged in', function(done) {
575 | imap._loggedIn = false;
576 | imap.listMessages({}).catch(function() {
577 | done();
578 | });
579 | });
580 | });
581 |
582 | describe('#getBodyParts', function() {
583 | it('should get the plain text body', function(done) {
584 | bboxMock.listMessages.withArgs('123:123', ['body.peek[1.mime]', 'body.peek[1]', 'body.peek[2.mime]', 'body.peek[2]']).returns(resolves([{
585 | 'body[1.mime]': 'qwe',
586 | 'body[1]': 'asd',
587 | 'body[2.mime]': 'bla',
588 | 'body[2]': 'blubb'
589 | }]));
590 |
591 | var parts = [{
592 | partNumber: '1'
593 | }, {
594 | partNumber: '2'
595 | }];
596 | imap.getBodyParts({
597 | path: 'foobar',
598 | uid: 123,
599 | bodyParts: parts
600 | }).then(function(cbParts) {
601 | expect(cbParts).to.equal(parts);
602 |
603 | expect(parts[0].raw).to.equal('qweasd');
604 | expect(parts[1].raw).to.equal('blablubb');
605 | expect(parts[0].partNumber).to.not.exist;
606 | expect(parts[1].partNumber).to.not.exist;
607 | }).then(done);
608 | });
609 |
610 | it('should do nothing for malformed body parts', function(done) {
611 | var parts = [{}, {}];
612 |
613 | imap.getBodyParts({
614 | path: 'foobar',
615 | uid: 123,
616 | bodyParts: parts
617 | }).then(function(cbParts) {
618 | expect(cbParts).to.equal(parts);
619 | expect(bboxMock.listMessages.called).to.be.false;
620 | }).then(done);
621 | });
622 |
623 | it('should fail when list fails', function(done) {
624 | bboxMock.listMessages.returns(rejects());
625 |
626 | imap.getBodyParts({
627 | path: 'foobar',
628 | uid: 123,
629 | bodyParts: [{
630 | partNumber: '1'
631 | }, {
632 | partNumber: '2'
633 | }]
634 | }).catch(function() {
635 | done();
636 | });
637 | });
638 |
639 | it('should not work when not logged in', function(done) {
640 | imap._loggedIn = false;
641 | imap.getBodyParts({
642 | path: 'foobar',
643 | uid: 123,
644 | bodyParts: [{
645 | partNumber: '1'
646 | }, {
647 | partNumber: '2'
648 | }]
649 | }).catch(function() {
650 | done();
651 | });
652 | });
653 | });
654 |
655 | describe('#updateFlags', function() {
656 | it('should update flags', function(done) {
657 | bboxMock.setFlags.withArgs('123:123', {
658 | add: ['\\Flagged', '\\Answered']
659 | }).returns(resolves());
660 |
661 | bboxMock.setFlags.withArgs('123:123', {
662 | remove: ['\\Seen']
663 | }).returns(resolves());
664 |
665 | imap.updateFlags({
666 | path: 'INBOX',
667 | uid: 123,
668 | unread: true,
669 | flagged: true,
670 | answered: true
671 | }).then(function() {
672 | expect(bboxMock.setFlags.calledTwice).to.be.true;
673 | }).then(done);
674 | });
675 |
676 | it('should update flags and skip add', function(done) {
677 | bboxMock.setFlags.withArgs('123:123', {
678 | remove: ['\\Answered']
679 | }).returns(resolves());
680 |
681 | imap.updateFlags({
682 | path: 'INBOX',
683 | uid: 123,
684 | answered: false
685 | }).then(function() {
686 | expect(bboxMock.setFlags.calledOnce).to.be.true;
687 | }).then(done);
688 | });
689 |
690 | it('should update flags and skip remove', function(done) {
691 | bboxMock.setFlags.withArgs('123:123', {
692 | add: ['\\Answered']
693 | }).returns(resolves());
694 |
695 | imap.updateFlags({
696 | path: 'INBOX',
697 | uid: 123,
698 | answered: true
699 | }).then(function() {
700 | expect(bboxMock.setFlags.calledOnce).to.be.true;
701 | }).then(done);
702 | });
703 |
704 | it('should update flags and skip add', function(done) {
705 | bboxMock.setFlags.withArgs('123:123', {
706 | remove: ['\\Seen']
707 | }).returns(resolves());
708 |
709 | imap.updateFlags({
710 | path: 'INBOX',
711 | uid: 123,
712 | unread: true
713 | }).then(function() {
714 | expect(bboxMock.setFlags.calledOnce).to.be.true;
715 | }).then(done);
716 | });
717 |
718 | it('should fail due to set flags error', function(done) {
719 | bboxMock.setFlags.returns(rejects());
720 |
721 | imap.updateFlags({
722 | path: 'INBOX',
723 | uid: 123,
724 | unread: false,
725 | answered: true
726 | }).catch(function() {
727 | done();
728 | });
729 | });
730 |
731 | it('should not update flags when not logged in', function(done) {
732 | imap._loggedIn = false;
733 | imap.updateFlags({}).catch(function() {
734 | done();
735 | });
736 | });
737 | });
738 |
739 | describe('#moveMessage', function() {
740 | it('should work', function(done) {
741 | bboxMock.moveMessages.withArgs('123:123', 'asdasd').returns(resolves());
742 |
743 | imap.moveMessage({
744 | path: 'INBOX',
745 | uid: 123,
746 | destination: 'asdasd'
747 | }).then(function() {
748 | expect(bboxMock.moveMessages.calledOnce).to.be.true;
749 | }).then(done);
750 | });
751 |
752 | it('should fail due to move error', function(done) {
753 | bboxMock.moveMessages.returns(rejects());
754 |
755 | imap.moveMessage({
756 | path: 'INBOX',
757 | uid: 123,
758 | destination: 'asdasd'
759 | }).catch(function() {
760 | done();
761 | });
762 | });
763 |
764 | it('should fail due to not logged in', function(done) {
765 | imap._loggedIn = false;
766 |
767 | imap.moveMessage({}).catch(function() {
768 | done();
769 | });
770 | });
771 |
772 | });
773 |
774 | describe('#uploadMessage', function() {
775 | var msg = 'asdasdasdasd',
776 | path = 'INBOX';
777 |
778 | it('should work', function(done) {
779 | bboxMock.upload.withArgs(path, msg).returns(resolves());
780 |
781 | imap.uploadMessage({
782 | path: path,
783 | message: msg
784 | }).then(function() {
785 | expect(bboxMock.upload.calledOnce).to.be.true;
786 | }).then(done);
787 | });
788 |
789 | it('should fail due to move error', function(done) {
790 | bboxMock.upload.returns(rejects());
791 |
792 | imap.uploadMessage({
793 | path: path,
794 | message: msg
795 | }).catch(function() {
796 | done();
797 | });
798 | });
799 | });
800 |
801 | describe('#deleteMessage', function() {
802 | it('should work', function(done) {
803 | bboxMock.deleteMessages.withArgs('123:123').returns(resolves());
804 |
805 | imap.deleteMessage({
806 | path: 'INBOX',
807 | uid: 123,
808 | }).then(function() {
809 | expect(bboxMock.deleteMessages.calledOnce).to.be.true;
810 | }).then(done);
811 |
812 | });
813 |
814 | it('should not fail due to delete error', function(done) {
815 | bboxMock.deleteMessages.returns(rejects());
816 |
817 | imap.deleteMessage({
818 | path: 'INBOX',
819 | uid: 123,
820 | }).catch(function() {
821 | done();
822 | });
823 | });
824 |
825 | it('should not fail due to not logged in', function(done) {
826 | imap._loggedIn = false;
827 |
828 | imap.deleteMessage({}).catch(function() {
829 | done();
830 | });
831 | });
832 | });
833 |
834 | describe('#listenForChanges', function() {
835 | it('should start listening', function(done) {
836 | bboxMock.selectMailbox.withArgs('INBOX').returns(resolves());
837 |
838 | imap.listenForChanges({
839 | path: 'INBOX'
840 | }).then(function() {
841 | expect(imap._listenerLoggedIn).to.be.true;
842 | expect(bboxMock.connect.calledOnce).to.be.true;
843 | expect(bboxMock.selectMailbox.calledOnce).to.be.true;
844 | }).then(done);
845 | bboxMock.onauth();
846 | });
847 |
848 | it('should return an error when inbox could not be opened', function(done) {
849 | bboxMock.selectMailbox.withArgs('INBOX').returns(rejects());
850 | imap.listenForChanges({
851 | path: 'INBOX'
852 | }).catch(function() {
853 | done();
854 | });
855 | bboxMock.onauth();
856 | });
857 | });
858 |
859 | describe('#stopListeningForChanges', function() {
860 | it('should stop listening', function(done) {
861 | imap._listenerLoggedIn = true;
862 |
863 | imap.stopListeningForChanges().then(function() {
864 | expect(bboxMock.close.calledOnce).to.be.true;
865 | expect(imap._listenerLoggedIn).to.be.false;
866 | }).then(done);
867 | bboxMock.onclose();
868 | });
869 | });
870 |
871 | describe('#_ensurePath', function() {
872 | var ctx = {};
873 |
874 | it('should switch mailboxes', function(done) {
875 | bboxMock.selectMailbox.withArgs('qweasdzxc', {
876 | ctx: ctx
877 | }).yields();
878 | imap._ensurePath('qweasdzxc')(ctx, function(err) {
879 | expect(err).to.not.exist;
880 | expect(bboxMock.selectMailbox.calledOnce).to.be.true;
881 | done();
882 | });
883 | });
884 |
885 | it('should error during switching mailboxes', function(done) {
886 | bboxMock.selectMailbox.withArgs('qweasdzxc', {
887 | ctx: ctx
888 | }).yields(new Error());
889 | imap._ensurePath('qweasdzxc')(ctx, function(err) {
890 | expect(err).to.exist;
891 | expect(bboxMock.selectMailbox.calledOnce).to.be.true;
892 | done();
893 | });
894 | });
895 | });
896 | });
897 |
898 | function resolves(val) {
899 | return new Promise(function(res) {
900 | res(val);
901 | });
902 | }
903 |
904 | function rejects(val) {
905 | return new Promise(function(res, rej) {
906 | rej(val || new Error());
907 | });
908 | }
909 | });
--------------------------------------------------------------------------------
/src/imap-client.js:
--------------------------------------------------------------------------------
1 | (function(factory) {
2 | 'use strict';
3 |
4 | if (typeof define === 'function' && define.amd) {
5 | define(['browserbox', 'axe'], factory);
6 | } else if (typeof exports === 'object') {
7 | module.exports = factory(require('browserbox'), require('axe-logger'));
8 | }
9 | })(function(BrowserBox, axe) {
10 | 'use strict';
11 |
12 | var DEBUG_TAG = 'imap-client';
13 |
14 | /**
15 | * Create an instance of ImapClient.
16 | * @param {Number} options.port Port is the port to the server (defaults to 143 on non-secure and to 993 on secure connection).
17 | * @param {String} options.host Hostname of the server.
18 | * @param {Boolean} options.secure Indicates if the connection is using TLS or not
19 | * @param {String} options.auth.user Username for login
20 | * @param {String} options.auth.pass Password for login
21 | * @param {String} options.auth.xoauth2 xoauth2 token for login
22 | * @param {Array} options.ca Array of PEM-encoded certificates that should be pinned.
23 | * @param {Number} options.maxUpdateSize (optional) The maximum number of messages to receive in an onSyncUpdate of type "new". 0 = all messages. Defaults to 0.
24 | */
25 | var ImapClient = function(options, browserbox) {
26 | var self = this;
27 |
28 | /*
29 | * Holds the login state.
30 | */
31 | self._loggedIn = false;
32 | self._listenerLoggedIn = false;
33 |
34 | /*
35 | * Instance of our imap library
36 | * (only relevant in unit test environment)
37 | */
38 | if (browserbox) {
39 | self._client = self._listeningClient = browserbox;
40 | } else {
41 | var credentials = {
42 | useSecureTransport: options.secure,
43 | ignoreTLS: options.ignoreTLS,
44 | requireTLS: options.requireTLS,
45 | auth: options.auth,
46 | ca: options.ca,
47 | tlsWorkerPath: options.tlsWorkerPath,
48 | enableCompression: true, // enable compression by default
49 | compressionWorkerPath: options.compressionWorkerPath
50 | };
51 | self._client = new BrowserBox(options.host, options.port, credentials);
52 | self._listeningClient = new BrowserBox(options.host, options.port, credentials);
53 | }
54 |
55 | /*
56 | * Calls the upper layer if the TLS certificate has to be updated
57 | */
58 | self._client.oncert = self._listeningClient.oncert = function(certificate) {
59 | self.onCert(certificate);
60 | };
61 |
62 | /**
63 | * Cache object with the following structure:
64 | *
65 | * {
66 | * "INBOX": {
67 | * exists: 5,
68 | * uidNext: 6,
69 | * uidlist: [1, 2, 3, 4, 5],
70 | * highestModseq: "555"
71 | * }
72 | * }
73 | *
74 | * @type {Object}
75 | */
76 | self.mailboxCache = {};
77 |
78 | self._registerEventHandlers(self._client);
79 | self._registerEventHandlers(self._listeningClient);
80 | };
81 |
82 | /**
83 | * Register the event handlers for the respective imap client
84 | */
85 | ImapClient.prototype._registerEventHandlers = function(client) {
86 | client.onselectmailbox = this._onSelectMailbox.bind(this, client);
87 | client.onupdate = this._onUpdate.bind(this, client);
88 | client.onclose = this._onClose.bind(this, client);
89 | client.onerror = this._onError.bind(this, client);
90 | };
91 |
92 | /**
93 | * Informs the upper layer if the main IMAP connection errors and cleans up.
94 | * If the listening IMAP connection fails, it only logs the error.
95 | */
96 | ImapClient.prototype._onError = function(client, err) {
97 | var msg = 'IMAP connection encountered an error! ' + err;
98 |
99 | if (client === this._client) {
100 | this._loggedIn = false;
101 | client.close();
102 | axe.error(DEBUG_TAG, new Error(msg));
103 | this.onError(new Error(msg)); // report the error
104 | } else if (client === this._listeningClient) {
105 | this._listenerLoggedIn = false;
106 | client.close();
107 | axe.warn(DEBUG_TAG, new Error('Listening ' + msg));
108 | }
109 | };
110 |
111 | /**
112 | * Informs the upper layer if the main IMAP connection has been unexpectedly closed remotely.
113 | * If the listening IMAP connection is closed unexpectedly, it only logs the error
114 | */
115 | ImapClient.prototype._onClose = function(client) {
116 | var msg = 'IMAP connection closed unexpectedly!';
117 |
118 | if (client === this._client && this._loggedIn) {
119 | this._loggedIn = false;
120 | axe.error(DEBUG_TAG, new Error(msg));
121 | this.onError(new Error(msg)); // report the error
122 | } else if (client === this._listeningClient && this._listenerLoggedIn) {
123 | this._listenerLoggedIn = false;
124 | axe.warn(DEBUG_TAG, new Error('Listening ' + msg));
125 | }
126 | };
127 |
128 | /**
129 | * Executed whenever 'onselectmailbox' event is emitted in BrowserBox
130 | *
131 | * @param {Object} client Listening client object
132 | * @param {String} path Path to currently opened mailbox
133 | * @param {Object} mailbox Information object for the opened mailbox
134 | */
135 | ImapClient.prototype._onSelectMailbox = function(client, path, mailbox) {
136 | var self = this,
137 | cached;
138 |
139 | // If both clients are currently listening the same mailbox, ignore data from listeningClient
140 | if (client === self._listeningClient && self._listeningClient.selectedMailbox === self._client.selectedMailbox) {
141 | return;
142 | }
143 |
144 | if (!self.onSyncUpdate) {
145 | return;
146 | }
147 |
148 | axe.debug(DEBUG_TAG, 'selected mailbox ' + path);
149 |
150 | // populate the cahce object for current path
151 | if (!self.mailboxCache[path]) {
152 | axe.debug(DEBUG_TAG, 'populating cache object for mailbox ' + path);
153 | self.mailboxCache[path] = {
154 | exists: 0,
155 | uidNext: 0,
156 | uidlist: []
157 | };
158 | }
159 |
160 | cached = self.mailboxCache[path];
161 |
162 | // if exists count does not match, there might be new messages
163 | // if exists count matches but uidNext is different, then something has been deleted and something added
164 | if (cached.exists !== mailbox.exists || cached.uidNext !== mailbox.uidNext) {
165 | axe.debug(DEBUG_TAG, 'possible updates available in ' + path + '. exists: ' + mailbox.exists + ', uidNext: ' + mailbox.uidNext);
166 |
167 | var firstUpdate = cached.exists === 0;
168 |
169 | cached.exists = mailbox.exists;
170 | cached.uidNext = mailbox.uidNext;
171 |
172 | // list all uid values in the selected mailbox
173 | self.search({
174 | path: path,
175 | client: client
176 | }).then(function(imapUidList) {
177 | // normalize the uidlist
178 | cached.uidlist = cached.uidlist || [];
179 |
180 | // determine deleted uids
181 | var deltaDeleted = cached.uidlist.filter(function(i) {
182 | return imapUidList.indexOf(i) < 0;
183 | });
184 |
185 | // notify about deleted messages
186 | if (deltaDeleted.length) {
187 | axe.debug(DEBUG_TAG, 'onSyncUpdate for deleted uids in ' + path + ': ' + deltaDeleted);
188 | self.onSyncUpdate({
189 | type: 'deleted',
190 | path: path,
191 | list: deltaDeleted
192 | });
193 | }
194 |
195 | // determine new uids
196 | var deltaNew = imapUidList.filter(function(i) {
197 | return cached.uidlist.indexOf(i) < 0;
198 | }).sort(sortNumericallyDescending);
199 |
200 | // notify about new messages
201 | if (deltaNew.length) {
202 | axe.debug(DEBUG_TAG, 'new uids in ' + path + ': ' + deltaNew);
203 | self.onSyncUpdate({
204 | type: 'new',
205 | path: path,
206 | list: deltaNew
207 | });
208 | }
209 |
210 | // update mailbox info
211 | cached.uidlist = imapUidList;
212 |
213 | if (!firstUpdate) {
214 | axe.debug(DEBUG_TAG, 'no changes in message count in ' + path + '. exists: ' + mailbox.exists + ', uidNext: ' + mailbox.uidNext);
215 | self._checkModseq({
216 | highestModseq: mailbox.highestModseq,
217 | client: client
218 | }).catch(function(error) {
219 | axe.error(DEBUG_TAG, 'error checking modseq: ' + error + '\n' + error.stack);
220 | });
221 | }
222 | });
223 | } else {
224 | // check for changed flags
225 | axe.debug(DEBUG_TAG, 'no changes in message count in ' + path + '. exists: ' + mailbox.exists + ', uidNext: ' + mailbox.uidNext);
226 | self._checkModseq({
227 | highestModseq: mailbox.highestModseq,
228 | client: client
229 | }).catch(function(error) {
230 | axe.error(DEBUG_TAG, 'error checking modseq: ' + error + '\n' + error.stack);
231 | });
232 | }
233 | };
234 |
235 | ImapClient.prototype._onUpdate = function(client, type, value) {
236 | var self = this,
237 | path = client.selectedMailbox,
238 | cached = self.mailboxCache[path];
239 |
240 | if (!self.onSyncUpdate) {
241 | return;
242 | }
243 |
244 | // If both clients are currently listening the same mailbox, ignore data from listeningClient
245 | if (client === self._listeningClient && self._listeningClient.selectedMailbox === self._client.selectedMailbox) {
246 | return;
247 | }
248 |
249 | if (!cached) {
250 | return;
251 | }
252 |
253 | if (type === 'expunge') {
254 | axe.debug(DEBUG_TAG, 'expunge notice received for ' + path + ' with sequence number: ' + value);
255 | // a message has been deleted
256 | // input format: "* EXPUNGE 123" where 123 is the sequence number of the deleted message
257 |
258 | var deletedUid = cached.uidlist[value - 1];
259 | // reorder the uidlist by removing deleted item
260 | cached.uidlist.splice(value - 1, 1);
261 |
262 | if (deletedUid) {
263 | axe.debug(DEBUG_TAG, 'deleted uid in ' + path + ': ' + deletedUid);
264 | self.onSyncUpdate({
265 | type: 'deleted',
266 | path: path,
267 | list: [deletedUid]
268 | });
269 | }
270 | } else if (type === 'exists') {
271 | axe.debug(DEBUG_TAG, 'exists notice received for ' + path + ', checking for updates');
272 |
273 | // there might be new messages (or something was deleted) as the message count in the mailbox has changed
274 | // input format: "* EXISTS 123" where 123 is the count of messages in the mailbox
275 | cached.exists = value;
276 | self.search({
277 | path: path,
278 | // search for messages with higher UID than last known uidNext
279 | uid: cached.uidNext + ':*',
280 | client: client
281 | }).then(function(imapUidList) {
282 | // if we do not find anything or the returned item was already known then return
283 | // if there was no new messages then we get back a single element array where the element
284 | // is the message with the highest UID value ('*' -> highest UID)
285 | // ie. if the largest UID in the mailbox is 100 and we search for 123:* then the query is
286 | // translated to 100:123 as '*' is 100 and this matches the element 100 that we already know about
287 | if (!imapUidList.length || (imapUidList.length === 1 && cached.uidlist.indexOf(imapUidList[0]) >= 0)) {
288 | return;
289 | }
290 |
291 | imapUidList.sort(sortNumericallyDescending);
292 | axe.debug(DEBUG_TAG, 'new uids in ' + path + ': ' + imapUidList);
293 | // update cahced uid list
294 | cached.uidlist = cached.uidlist.concat(imapUidList);
295 | // predict the next UID, might not be the actual value set by the server
296 | cached.uidNext = cached.uidlist[cached.uidlist.length - 1] + 1;
297 |
298 | // notify about new messages
299 | axe.debug(DEBUG_TAG, 'new uids in ' + path + ': ' + imapUidList);
300 | self.onSyncUpdate({
301 | type: 'new',
302 | path: path,
303 | list: imapUidList
304 | });
305 | }).catch(function(error) {
306 | axe.error(DEBUG_TAG, 'error handling exists notice: ' + error + '\n' + error.stack);
307 | });
308 | } else if (type === 'fetch') {
309 | axe.debug(DEBUG_TAG, 'fetch notice received for ' + path);
310 |
311 | // probably some flag updates. A message or messages have been altered in some way
312 | // and the server sends an unsolicited FETCH response
313 | // input format: "* 123 FETCH (FLAGS (\Seen))"
314 | // UID is probably not listed, only the sequence number
315 | self.onSyncUpdate({
316 | type: 'messages',
317 | path: path,
318 | // listed message object does not contain uid by default
319 | list: [value].map(function(message) {
320 | if (!message.uid && cached.uidlist) {
321 | message.uid = cached.uidlist[message['#'] - 1];
322 | }
323 | return message;
324 | })
325 | });
326 | }
327 | };
328 |
329 | /**
330 | * Lists messages with the last check
331 | *
332 | * @param {String} options.highestModseq MODSEQ value
333 | *
334 | * @return {Promise}
335 | */
336 | ImapClient.prototype._checkModseq = function(options) {
337 | var self = this,
338 | highestModseq = options.highestModseq,
339 | client = options.client || self._client,
340 | path = client.selectedMailbox;
341 |
342 | // do nothing if we do not have highestModseq value. it should be at least 1. if it is
343 | // undefined then the server does not support CONDSTORE extension.
344 | // Yahoo supports a custom MODSEQ related extension called XYMHIGHESTMODSEQ which
345 | // returns HIGHESTMODSEQ value when doing SELECT but does not allow to use the CHANGEDSINCE modifier
346 | // or query the message MODSEQ value. Returned HIGHESTMODSEQ also happens to be a 64 bit number that
347 | // is larger than Number.MAX_SAFE_INTEGER so it can't be used as a numeric value. To fix errors
348 | // with Yahoo we double check if the CONDSTORE is listed as a capability or not as checking just
349 | // the highestModseq value would give us a false positive.
350 | if (!client.hasCapability('CONDSTORE') || !highestModseq || !path) {
351 | axe.info(DEBUG_TAG, 'can not check MODSEQ, server does not support CONDSTORE extension');
352 | return new Promise(function(resolve) {
353 | resolve([]);
354 | });
355 | }
356 |
357 | var cached = self.mailboxCache[path];
358 |
359 | // only do this when we actually do have a last know change number
360 | if (!(cached && cached.highestModseq && cached.highestModseq !== highestModseq)) {
361 | return new Promise(function(resolve) {
362 | resolve([]);
363 | });
364 | }
365 |
366 | var msgs = cached.uidlist.slice(-100);
367 | var firstUid = (msgs.shift() || '1');
368 | var lastUid = (msgs.pop() || '*');
369 |
370 | axe.debug(DEBUG_TAG, 'listing changes since MODSEQ ' + highestModseq + ' for ' + path);
371 | return client.listMessages(firstUid + ':' + lastUid, ['uid', 'flags', 'modseq'], {
372 | byUid: true,
373 | changedSince: cached.highestModseq
374 | }).then(function(messages) {
375 | cached.highestModseq = highestModseq;
376 |
377 | if (!messages || !messages.length) {
378 | return [];
379 | }
380 |
381 | axe.debug(DEBUG_TAG, 'changes since MODSEQ ' + highestModseq + ' for ' + path + ' available!');
382 | self.onSyncUpdate({
383 | type: 'messages',
384 | path: path,
385 | list: messages
386 | });
387 |
388 | return messages;
389 | }).catch(function(error) {
390 | axe.error(DEBUG_TAG, 'error handling exists notice: ' + error + '\n' + error.stack);
391 | });
392 | };
393 |
394 | /**
395 | * Synchronization handler
396 | *
397 | * type 'new' returns an array of UID values that are new messages
398 | * type 'deleted' returns an array of UID values that are deleted
399 | * type 'messages' returns an array of message objects that are somehow updated
400 | *
401 | * @param {Object} options Notification options
402 | * @param {String} options.type Type of the update
403 | * @param {Array} options.list List of uids/messages
404 | * @param {String} options.path Selected mailbox
405 | */
406 | ImapClient.prototype.onSyncUpdate = false;
407 |
408 | /**
409 | * Log in to an IMAP Session. No-op if already logged in.
410 | *
411 | * @return {Prmomise}
412 | */
413 | ImapClient.prototype.login = function() {
414 | var self = this;
415 |
416 | return new Promise(function(resolve, reject) {
417 | if (self._loggedIn) {
418 | axe.debug(DEBUG_TAG, 'refusing login while already logged in!');
419 | return resolve();
420 | }
421 | var authenticating = true;
422 |
423 | self._client.onauth = function() {
424 | axe.debug(DEBUG_TAG, 'login completed, ready to roll!');
425 | self._loggedIn = true;
426 | authenticating = false;
427 | return resolve();
428 | };
429 |
430 | self._client.onerror = function(error) {
431 | if (!self._loggedIn && authenticating) {
432 | axe.debug(DEBUG_TAG, 'login failed! ' + error);
433 | authenticating = false;
434 | return reject(error);
435 | }
436 | };
437 |
438 | self._client.connect();
439 | });
440 | };
441 |
442 | /**
443 | * Log out of the current IMAP session
444 | *
445 | * @return {Promise}
446 | */
447 | ImapClient.prototype.logout = function() {
448 | var self = this;
449 |
450 | return new Promise(function(resolve) {
451 | if (!self._loggedIn) {
452 | axe.debug(DEBUG_TAG, 'refusing logout while already logged out!');
453 | return resolve();
454 | }
455 |
456 | self._client.onclose = function() {
457 | axe.debug(DEBUG_TAG, 'logout completed, kthxbye!');
458 | resolve();
459 | };
460 |
461 | self._loggedIn = false;
462 | self._client.close();
463 | });
464 | };
465 |
466 | /**
467 | * Starts dedicated listener for updates on a specific IMAP folder, calls back when a change occurrs,
468 | * or includes information in case of an error
469 |
470 | * @param {String} options.path The path to the folder to subscribe to
471 | *
472 | * @return {Promise}
473 | */
474 | ImapClient.prototype.listenForChanges = function(options) {
475 | var self = this;
476 |
477 | return new Promise(function(resolve, reject) {
478 | if (self._listenerLoggedIn) {
479 | axe.debug(DEBUG_TAG, 'refusing login listener while already logged in!');
480 | return resolve();
481 | }
482 |
483 | self._listeningClient.onauth = function() {
484 | axe.debug(DEBUG_TAG, 'listener login completed, ready to roll!');
485 | self._listenerLoggedIn = true;
486 | axe.debug(DEBUG_TAG, 'listening for changes in ' + options.path);
487 | self._listeningClient.selectMailbox(options.path).then(resolve).catch(reject);
488 | };
489 | self._listeningClient.connect();
490 | });
491 | };
492 |
493 | /**
494 | * Stops dedicated listener for updates
495 | *
496 | * @return {Promise}
497 | */
498 | ImapClient.prototype.stopListeningForChanges = function() {
499 | var self = this;
500 |
501 | return new Promise(function(resolve) {
502 | if (!self._listenerLoggedIn) {
503 | axe.debug(DEBUG_TAG, 'refusing logout listener already logged out!');
504 | return resolve();
505 | }
506 |
507 | self._listeningClient.onclose = function() {
508 | axe.debug(DEBUG_TAG, 'logout completed, kthxbye!');
509 | resolve();
510 | };
511 |
512 | self._listenerLoggedIn = false;
513 | self._listeningClient.close();
514 | });
515 | };
516 |
517 | ImapClient.prototype.selectMailbox = function(options) {
518 | axe.debug(DEBUG_TAG, 'selecting mailbox ' + options.path);
519 | return this._client.selectMailbox(options.path);
520 | };
521 |
522 | /**
523 | * Provides the well known folders: Drafts, Sent, Inbox, Trash, Flagged, etc. No-op if not logged in.
524 | * Since there may actually be multiple sent folders (e.g. one is default, others were created by Thunderbird,
525 | * Outlook, another accidentally matched the naming), we return the well known folders as an array to avoid false positives.
526 | *
527 | * @return {Promise} Array of folders
528 | */
529 | ImapClient.prototype.listWellKnownFolders = function() {
530 | var self = this;
531 |
532 | var wellKnownFolders = {
533 | Inbox: [],
534 | Drafts: [],
535 | All: [],
536 | Flagged: [],
537 | Sent: [],
538 | Trash: [],
539 | Junk: [],
540 | Archive: [],
541 | Other: []
542 | };
543 |
544 | axe.debug(DEBUG_TAG, 'listing folders');
545 |
546 | return self._checkOnline().then(function() {
547 | return self._client.listMailboxes();
548 | }).then(function(mailbox) {
549 | axe.debug(DEBUG_TAG, 'folder list received!');
550 | walkMailbox(mailbox);
551 | return wellKnownFolders;
552 |
553 | }).catch(function(error) {
554 | axe.error(DEBUG_TAG, 'error listing folders: ' + error + '\n' + error.stack);
555 | throw error;
556 | });
557 |
558 | function walkMailbox(mailbox) {
559 | if (mailbox.path && (mailbox.flags || []).indexOf("\\Noselect") === -1) {
560 | // only list mailboxes here that have a path and are selectable
561 | axe.debug(DEBUG_TAG, 'name: ' + mailbox.name + ', path: ' + mailbox.path + (mailbox.flags ? (', flags: ' + mailbox.flags) : '') + (mailbox.specialUse ? (', special use: ' + mailbox.specialUse) : ''));
562 |
563 | var folder = {
564 | name: mailbox.name || mailbox.path,
565 | path: mailbox.path
566 | };
567 |
568 | if (folder.name.toUpperCase() === 'INBOX') {
569 | folder.type = 'Inbox';
570 | self._delimiter = mailbox.delimiter;
571 | wellKnownFolders.Inbox.push(folder);
572 | } else if (mailbox.specialUse === '\\Drafts') {
573 | folder.type = 'Drafts';
574 | wellKnownFolders.Drafts.push(folder);
575 | } else if (mailbox.specialUse === '\\All') {
576 | folder.type = 'All';
577 | wellKnownFolders.All.push(folder);
578 | } else if (mailbox.specialUse === '\\Flagged') {
579 | folder.type = 'Flagged';
580 | wellKnownFolders.Flagged.push(folder);
581 | } else if (mailbox.specialUse === '\\Sent') {
582 | folder.type = 'Sent';
583 | wellKnownFolders.Sent.push(folder);
584 | } else if (mailbox.specialUse === '\\Trash') {
585 | folder.type = 'Trash';
586 | wellKnownFolders.Trash.push(folder);
587 | } else if (mailbox.specialUse === '\\Junk') {
588 | folder.type = 'Junk';
589 | wellKnownFolders.Junk.push(folder);
590 | } else if (mailbox.specialUse === '\\Archive') {
591 | folder.type = 'Archive';
592 | wellKnownFolders.Archive.push(folder);
593 | } else {
594 | folder.type = 'Other';
595 | wellKnownFolders.Other.push(folder);
596 | }
597 | }
598 |
599 | if (mailbox.children) {
600 | // walk the child mailboxes recursively
601 | mailbox.children.forEach(walkMailbox);
602 | }
603 | }
604 | };
605 |
606 | /**
607 | * Creates a folder with the provided path under the personal namespace
608 | *
609 | * @param {String or Array} options.path
610 | * The folder's path. If path is a hierarchy as an array (e.g. ['foo', 'bar', 'baz'] to create foo/bar/bar),
611 | * will create a hierarchy with all intermediate folders if needed.
612 | * @returns {Promise} Fully qualified path of the folder just created
613 | */
614 | ImapClient.prototype.createFolder = function(options) {
615 | var self = this,
616 | path = options.path,
617 | fullPath;
618 |
619 | if (!Array.isArray(path)) {
620 | path = [path];
621 | }
622 |
623 | return self._checkOnline().then(function() {
624 | // spare the check
625 | if (typeof self._delimiter !== 'undefined' && typeof self._prefix !== 'undefined') {
626 | return;
627 | }
628 |
629 | // try to get the namespace prefix and delimiter
630 | return self._client.listNamespaces().then(function(namespaces) {
631 | if (namespaces && namespaces.personal && namespaces.personal[0]) {
632 | // personal namespace is available
633 | self._delimiter = namespaces.personal[0].delimiter;
634 | self._prefix = namespaces.personal[0].prefix.split(self._delimiter).shift();
635 | return;
636 | }
637 |
638 | // no namespaces, falling back to empty prefix
639 | self._prefix = "";
640 |
641 | // if we already have the delimiter, there's no need to retrieve the lengthy folder list
642 | if (self._delimiter) {
643 | return;
644 | }
645 |
646 | // find the delimiter by listing the folders
647 | return self._client.listMailboxes().then(function(response) {
648 | findDelimiter(response);
649 | });
650 | });
651 |
652 | }).then(function() {
653 | if (!self._delimiter) {
654 | throw new Error('Could not determine delimiter for mailbox hierarchy');
655 | }
656 |
657 | if (self._prefix) {
658 | path.unshift(self._prefix);
659 | }
660 |
661 | fullPath = path.join(self._delimiter);
662 |
663 | // create path [prefix/]foo/bar/baz
664 | return self._client.createMailbox(fullPath);
665 |
666 | }).then(function() {
667 | return fullPath;
668 |
669 | }).catch(function(error) {
670 | axe.error(DEBUG_TAG, 'error creating folder ' + options.path + ': ' + error + '\n' + error.stack);
671 | throw error;
672 | });
673 |
674 | // Helper function to find the hierarchy delimiter from a client.listMailboxes() response
675 | function findDelimiter(mailbox) {
676 | if ((mailbox.path || '').toUpperCase() === 'INBOX') {
677 | // found the INBOX, use its hierarchy delimiter, we're done.
678 | self._delimiter = mailbox.delimiter;
679 | return;
680 | }
681 |
682 | if (mailbox.children) {
683 | // walk the child mailboxes recursively
684 | mailbox.children.forEach(findDelimiter);
685 | }
686 | }
687 | };
688 |
689 | /**
690 | * Returns the uids of messages containing the search terms in the options
691 | * @param {String} options.path The folder's path
692 | * @param {Boolean} options.answered (optional) Mails with or without the \Answered flag set.
693 | * @param {Boolean} options.unread (optional) Mails with or without the \Seen flag set.
694 | * @param {Array} options.header (optional) Query an arbitrary header, e.g. ['Subject', 'Foobar'], or ['X-Foo', 'bar']
695 | *
696 | * @returns {Promise} Array of uids for messages matching the search terms
697 | */
698 | ImapClient.prototype.search = function(options) {
699 | var self = this,
700 | client = options.client || self._client;
701 |
702 | var query = {},
703 | queryOptions = {
704 | byUid: true,
705 | precheck: self._ensurePath(options.path, client)
706 | };
707 |
708 | // initial request to AND the following properties
709 | query.all = true;
710 |
711 | if (options.unread === true) {
712 | query.unseen = true;
713 | } else if (options.unread === false) {
714 | query.seen = true;
715 | }
716 |
717 | if (options.answered === true) {
718 | query.answered = true;
719 | } else if (options.answered === false) {
720 | query.unanswered = true;
721 | }
722 |
723 | if (options.header) {
724 | query.header = options.header;
725 | }
726 |
727 | if (options.uid) {
728 | query.uid = options.uid;
729 | }
730 |
731 | axe.debug(DEBUG_TAG, 'searching in ' + options.path + ' for ' + Object.keys(query).join(','));
732 | return self._checkOnline().then(function() {
733 | return client.search(query, queryOptions);
734 | }).then(function(uids) {
735 | axe.debug(DEBUG_TAG, 'searched in ' + options.path + ' for ' + Object.keys(query).join(',') + ': ' + uids);
736 | return uids;
737 | }).catch(function(error) {
738 | axe.error(DEBUG_TAG, 'error searching ' + options.path + ': ' + error + '\n' + error.stack);
739 | throw error;
740 | });
741 | };
742 |
743 | /**
744 | * List messages in an IMAP folder based on their uid
745 | * @param {String} options.path The folder's path
746 | * @param {Number} options.firstUid (optional) If you want to fetch a range, this is the uid of the first message. if omitted, defaults to 1
747 | * @param {Number} options.lastUid (optional) The uid of the last message. if omitted, defaults to *
748 | * @param {Array} options.uids (optional) If used, fetched individual uids
749 | *
750 | * @returns {Promise} Array of messages with their respective envelope data.
751 | */
752 | ImapClient.prototype.listMessages = function(options) {
753 | var self = this;
754 |
755 | var query = ['uid', 'bodystructure', 'flags', 'envelope', 'body.peek[header.fields (references)]'],
756 | queryOptions = {
757 | byUid: true,
758 | precheck: self._ensurePath(options.path)
759 | },
760 | interval;
761 |
762 | if (options.uids) {
763 | interval = options.uids.join(',');
764 | } else {
765 | interval = (options.firstUid || 1) + ':' + (options.lastUid || '*');
766 | }
767 |
768 | // only if client has CONDSTORE capability
769 | if (this._client.hasCapability('CONDSTORE')) {
770 | query.push('modseq');
771 | }
772 |
773 | axe.debug(DEBUG_TAG, 'listing messages in ' + options.path + ' for interval ' + interval);
774 | return self._checkOnline().then(function() {
775 | return self._client.listMessages(interval, query, queryOptions);
776 | }).then(function(messages) {
777 | // a message without uid will be ignored as malformed
778 | messages = messages.filter(function(message) {
779 | return !!message.uid;
780 | });
781 |
782 | var cleansedMessages = [];
783 | messages.forEach(function(message) {
784 | // construct a cleansed message object
785 |
786 | var references = (message['body[header.fields (references)]'] || '').replace(/^references:\s*/i, '').trim();
787 |
788 | var cleansed = {
789 | uid: message.uid,
790 | id: (message.envelope['message-id'] || '').replace(/[<>]/g, ''),
791 | from: message.envelope.from || [],
792 | replyTo: message.envelope['reply-to'] || [],
793 | to: message.envelope.to || [],
794 | cc: message.envelope.cc || [],
795 | bcc: message.envelope.bcc || [],
796 | modseq: message.modseq || '0',
797 | subject: message.envelope.subject || '(no subject)',
798 | inReplyTo: (message.envelope['in-reply-to'] || '').replace(/[<>]/g, ''),
799 | references: references ? references.split(/\s+/).map(function(reference) {
800 | return reference.replace(/[<>]/g, '');
801 | }) : [],
802 | sentDate: message.envelope.date ? new Date(message.envelope.date) : new Date(),
803 | unread: (message.flags || []).indexOf('\\Seen') === -1,
804 | flagged: (message.flags || []).indexOf('\\Flagged') > -1,
805 | answered: (message.flags || []).indexOf('\\Answered') > -1,
806 | bodyParts: []
807 | };
808 |
809 | walkMimeTree((message.bodystructure || {}), cleansed);
810 | cleansed.encrypted = cleansed.bodyParts.filter(function(bodyPart) {
811 | return bodyPart.type === 'encrypted';
812 | }).length > 0;
813 | cleansed.signed = cleansed.bodyParts.filter(function(bodyPart) {
814 | return bodyPart.type === 'signed';
815 | }).length > 0;
816 |
817 | axe.debug(DEBUG_TAG, 'listing message: [uid: ' + cleansed.uid + '][encrypted: ' + cleansed.encrypted + '][signed: ' + cleansed.signed + ']');
818 | cleansedMessages.push(cleansed);
819 | });
820 |
821 | return cleansedMessages;
822 | }).catch(function(error) {
823 | axe.error(DEBUG_TAG, 'error listing messages in ' + options.path + ': ' + error + '\n' + error.stack);
824 | throw error;
825 | });
826 | };
827 |
828 | /**
829 | * Fetches parts of a message from the imap server
830 | * @param {String} options.path The folder's path
831 | * @param {Number} options.uid The uid of the message
832 | * @param {Array} options.bodyParts Parts of a message, as returned by #listMessages
833 |
834 | * @returns {Promise} Body parts that have been received from the server
835 | */
836 | ImapClient.prototype.getBodyParts = function(options) {
837 | var self = this,
838 | query = [],
839 | queryOptions = {
840 | byUid: true,
841 | precheck: self._ensurePath(options.path)
842 | },
843 | interval = options.uid + ':' + options.uid,
844 | bodyParts = options.bodyParts || [];
845 |
846 | // formulate a query for each text part. for part 2.1 to be parsed, we need 2.1.MIME and 2.1
847 | bodyParts.forEach(function(bodyPart) {
848 | if (typeof bodyPart.partNumber === 'undefined') {
849 | return;
850 | }
851 |
852 | if (bodyPart.partNumber === '') {
853 | query.push('body.peek[]');
854 | } else {
855 | query.push('body.peek[' + bodyPart.partNumber + '.mime]');
856 | query.push('body.peek[' + bodyPart.partNumber + ']');
857 | }
858 | });
859 |
860 | if (query.length === 0) {
861 | return new Promise(function(resolve) {
862 | resolve(bodyParts);
863 | });
864 | }
865 |
866 | axe.debug(DEBUG_TAG, 'retrieving body parts for uid ' + options.uid + ' in folder ' + options.path + ': ' + query);
867 | return self._checkOnline().then(function() {
868 | return self._client.listMessages(interval, query, queryOptions);
869 | }).then(function(messages) {
870 | axe.debug(DEBUG_TAG, 'successfully retrieved body parts for uid ' + options.uid + ' in folder ' + options.path + ': ' + query);
871 |
872 | var message = messages[0];
873 | if (!message) {
874 | // message has been deleted while waiting for the command to return
875 | return bodyParts;
876 | }
877 |
878 | bodyParts.forEach(function(bodyPart) {
879 | if (typeof bodyPart.partNumber === 'undefined') {
880 | return;
881 | }
882 |
883 | if (bodyPart.partNumber === '') {
884 | bodyPart.raw = message['body[]'];
885 | } else {
886 | bodyPart.raw = message['body[' + bodyPart.partNumber + '.mime]'] + message['body[' + bodyPart.partNumber + ']'];
887 | }
888 |
889 | delete bodyPart.partNumber;
890 | });
891 |
892 | return bodyParts;
893 | }).catch(function(error) {
894 | axe.error(DEBUG_TAG, 'error fetching body parts for uid ' + options.uid + ' in folder ' + options.path + ': ' + error + '\n' + error.stack);
895 | throw error;
896 | });
897 | };
898 |
899 | /**
900 | * Update IMAP flags for a message with a given UID
901 | * @param {String} options.path The folder's path
902 | * @param {Number} options.uid The uid of the message
903 | * @param {Boolean} options.unread (optional) Marks the message as unread
904 | * @param {Boolean} options.answered (optional) Marks the message as answered
905 | * @param {Boolean} options.flagged (optional) Marks the message as answered
906 | *
907 | * @returns {Promise}
908 | */
909 | ImapClient.prototype.updateFlags = function(options) {
910 | var self = this,
911 | interval = options.uid + ':' + options.uid,
912 | queryOptions = {
913 | byUid: true,
914 | precheck: self._ensurePath(options.path)
915 | },
916 | queryAdd,
917 | queryRemove,
918 | remove = [],
919 | add = [],
920 | READ_FLAG = '\\Seen',
921 | FLAGGED_FLAG = '\\Flagged',
922 | ANSWERED_FLAG = '\\Answered';
923 |
924 | if (options.unread === true) {
925 | remove.push(READ_FLAG);
926 | } else if (options.unread === false) {
927 | add.push(READ_FLAG);
928 | }
929 |
930 | if (options.flagged === true) {
931 | add.push(FLAGGED_FLAG);
932 | } else if (options.flagged === false) {
933 | remove.push(FLAGGED_FLAG);
934 | }
935 |
936 | if (options.answered === true) {
937 | add.push(ANSWERED_FLAG);
938 | } else if (options.answered === false) {
939 | remove.push(ANSWERED_FLAG);
940 | }
941 |
942 | if (add.length === 0 && remove.length === 0) {
943 | return new Promise(function() {
944 | throw new Error('Can not update flags, cause: Not logged in!');
945 | });
946 | }
947 |
948 | queryAdd = {
949 | add: add
950 | };
951 | queryRemove = {
952 | remove: remove
953 | };
954 |
955 | axe.debug(DEBUG_TAG, 'updating flags for uid ' + options.uid + ' in folder ' + options.path + ': ' + (remove.length > 0 ? (' removing ' + remove) : '') + (add.length > 0 ? (' adding ' + add) : ''));
956 | return self._checkOnline().then(function() {
957 | return new Promise(function(resolve) {
958 | if (add.length > 0) {
959 | resolve(self._client.setFlags(interval, queryAdd, queryOptions));
960 | } else {
961 | resolve();
962 | }
963 | });
964 | }).then(function() {
965 | if (remove.length > 0) {
966 | return self._client.setFlags(interval, queryRemove, queryOptions);
967 | }
968 | }).then(function() {
969 | axe.debug(DEBUG_TAG, 'successfully updated flags for uid ' + options.uid + ' in folder ' + options.path + ': added ' + add + ' and removed ' + remove);
970 | }).catch(function(error) {
971 | axe.error(DEBUG_TAG, 'error updating flags for uid ' + options.uid + ' in folder ' + options.path + ' : ' + error + '\n' + error.stack);
972 | throw error;
973 | });
974 | };
975 |
976 | /**
977 | * Move a message to a destination folder
978 | * @param {String} options.path The origin path where the message resides
979 | * @param {Number} options.uid The uid of the message
980 | * @param {String} options.destination The destination folder
981 | *
982 | * @returns {Promise}
983 | */
984 | ImapClient.prototype.moveMessage = function(options) {
985 | var self = this,
986 | interval = options.uid + ':' + options.uid,
987 | queryOptions = {
988 | byUid: true,
989 | precheck: self._ensurePath(options.path)
990 | };
991 |
992 | axe.debug(DEBUG_TAG, 'moving uid ' + options.uid + ' from ' + options.path + ' to ' + options.destination);
993 | return self._checkOnline().then(function() {
994 | return self._client.moveMessages(interval, options.destination, queryOptions);
995 | }).then(function() {
996 | axe.debug(DEBUG_TAG, 'successfully moved uid ' + options.uid + ' from ' + options.path + ' to ' + options.destination);
997 | }).catch(function(error) {
998 | axe.error(DEBUG_TAG, 'error moving uid ' + options.uid + ' from ' + options.path + ' to ' + options.destination + ' : ' + error + '\n' + error.stack);
999 | throw error;
1000 | });
1001 | };
1002 |
1003 | /**
1004 | * Move a message to a folder
1005 | * @param {String} options.path The path the message should be uploaded to
1006 | * @param {String} options.message A RFC-2822 compliant message
1007 | *
1008 | * @returns {Promise}
1009 | */
1010 | ImapClient.prototype.uploadMessage = function(options) {
1011 | var self = this;
1012 |
1013 | axe.debug(DEBUG_TAG, 'uploading a message of ' + options.message.length + ' bytes to ' + options.path);
1014 | return self._checkOnline().then(function() {
1015 | return self._client.upload(options.path, options.message);
1016 | }).then(function() {
1017 | axe.debug(DEBUG_TAG, 'successfully uploaded message to ' + options.path);
1018 | }).catch(function(error) {
1019 | axe.error(DEBUG_TAG, 'error uploading <' + options.message.length + '> bytes to ' + options.path + ' : ' + error + '\n' + error.stack);
1020 | throw error;
1021 | });
1022 | };
1023 |
1024 | /**
1025 | * Purges a message from a folder
1026 | * @param {String} options.path The origin path where the message resides
1027 | * @param {Number} options.uid The uid of the message
1028 | *
1029 | * @returns {Promise}
1030 | */
1031 | ImapClient.prototype.deleteMessage = function(options) {
1032 | var self = this,
1033 | interval = options.uid + ':' + options.uid,
1034 | queryOptions = {
1035 | byUid: true,
1036 | precheck: self._ensurePath(options.path)
1037 | };
1038 |
1039 | axe.debug(DEBUG_TAG, 'deleting uid ' + options.uid + ' from ' + options.path);
1040 | return self._checkOnline().then(function() {
1041 | return self._client.deleteMessages(interval, queryOptions);
1042 | }).then(function() {
1043 | axe.debug(DEBUG_TAG, 'successfully deleted uid ' + options.uid + ' from ' + options.path);
1044 | }).catch(function(error) {
1045 | axe.error(DEBUG_TAG, 'error deleting uid ' + options.uid + ' from ' + options.path + ' : ' + error + '\n' + error.stack);
1046 | throw error;
1047 | });
1048 | };
1049 |
1050 | //
1051 | // Helper methods
1052 | //
1053 |
1054 | /**
1055 | * Makes sure that the respective instance of browserbox is in the correct mailbox to run the command
1056 | *
1057 | * @param {String} path The mailbox path
1058 | */
1059 | ImapClient.prototype._ensurePath = function(path, client) {
1060 | var self = this;
1061 | client = client || self._client;
1062 |
1063 | return function(ctx, next) {
1064 | if (client.selectedMailbox === path) {
1065 | return next();
1066 | }
1067 |
1068 | axe.debug(DEBUG_TAG, 'selecting mailbox ' + path);
1069 | client.selectMailbox(path, {
1070 | ctx: ctx
1071 | }, next);
1072 | };
1073 | };
1074 |
1075 | ImapClient.prototype._checkOnline = function() {
1076 | var self = this;
1077 |
1078 | return new Promise(function(resolve) {
1079 | if (!self._loggedIn) {
1080 | throw new Error('Not logged in!');
1081 | }
1082 |
1083 | resolve();
1084 | });
1085 | };
1086 |
1087 | /*
1088 | * Mime Tree Handling
1089 | * ==================
1090 | *
1091 | * matchEncrypted, matchSigned, ... are matchers that are called on each node of the mimde tree
1092 | * when it is being traversed in a DFS. if one of the matchers returns true, it indicates that it
1093 | * matched respective mime node, hence there is no need to look any further down in the tree.
1094 | *
1095 | */
1096 |
1097 | var mimeTreeMatchers = [matchEncrypted, matchSigned, matchAttachment, matchText, matchHtml];
1098 |
1099 | /**
1100 | * Helper function that walks the MIME tree in a dfs and calls the handlers
1101 | * @param {Object} mimeNode The initial MIME node whose subtree should be traversed
1102 | * @param {Object} message The initial root MIME node whose subtree should be traversed
1103 | */
1104 | function walkMimeTree(mimeNode, message) {
1105 | var i = mimeTreeMatchers.length;
1106 | while (i--) {
1107 | if (mimeTreeMatchers[i](mimeNode, message)) {
1108 | return;
1109 | }
1110 | }
1111 |
1112 | if (mimeNode.childNodes) {
1113 | mimeNode.childNodes.forEach(function(childNode) {
1114 | walkMimeTree(childNode, message);
1115 | });
1116 | }
1117 | }
1118 |
1119 | /**
1120 | * Matches encrypted PGP/MIME nodes
1121 | *
1122 | * multipart/encrypted
1123 | * |
1124 | * |-- application/pgp-encrypted
1125 | * |-- application/octet-stream <-- ciphertext
1126 | */
1127 | function matchEncrypted(node, message) {
1128 | var isEncrypted = /^multipart\/encrypted/i.test(node.type) && node.childNodes && node.childNodes[1];
1129 | if (!isEncrypted) {
1130 | return false;
1131 | }
1132 |
1133 | message.bodyParts.push({
1134 | type: 'encrypted',
1135 | partNumber: node.part || '',
1136 | });
1137 | return true;
1138 | }
1139 |
1140 | /**
1141 | * Matches signed PGP/MIME nodes
1142 | *
1143 | * multipart/signed
1144 | * |
1145 | * |-- *** (signed mime sub-tree)
1146 | * |-- application/pgp-signature
1147 | */
1148 | function matchSigned(node, message) {
1149 | var c = node.childNodes;
1150 |
1151 | var isSigned = /^multipart\/signed/i.test(node.type) && c && c[0] && c[1] && /^application\/pgp-signature/i.test(c[1].type);
1152 | if (!isSigned) {
1153 | return false;
1154 | }
1155 |
1156 | message.bodyParts.push({
1157 | type: 'signed',
1158 | partNumber: node.part || '',
1159 | });
1160 | return true;
1161 | }
1162 |
1163 | /**
1164 | * Matches non-attachment text/plain nodes
1165 | */
1166 | function matchText(node, message) {
1167 | var isText = (/^text\/plain/i.test(node.type) && node.disposition !== 'attachment');
1168 | if (!isText) {
1169 | return false;
1170 | }
1171 |
1172 | message.bodyParts.push({
1173 | type: 'text',
1174 | partNumber: node.part || ''
1175 | });
1176 | return true;
1177 | }
1178 |
1179 | /**
1180 | * Matches non-attachment text/html nodes
1181 | */
1182 | function matchHtml(node, message) {
1183 | var isHtml = (/^text\/html/i.test(node.type) && node.disposition !== 'attachment');
1184 | if (!isHtml) {
1185 | return false;
1186 | }
1187 |
1188 | message.bodyParts.push({
1189 | type: 'html',
1190 | partNumber: node.part || ''
1191 | });
1192 | return true;
1193 | }
1194 |
1195 | /**
1196 | * Matches non-attachment text/html nodes
1197 | */
1198 | function matchAttachment(node, message) {
1199 | var isAttachment = (/^text\//i.test(node.type) && node.disposition) || (!/^text\//i.test(node.type) && !/^multipart\//i.test(node.type));
1200 | if (!isAttachment) {
1201 | return false;
1202 | }
1203 |
1204 | var bodyPart = {
1205 | type: 'attachment',
1206 | partNumber: node.part || '',
1207 | mimeType: node.type || 'application/octet-stream',
1208 | id: node.id ? node.id.replace(/[<>]/g, '') : undefined
1209 | };
1210 |
1211 | if (node.dispositionParameters && node.dispositionParameters.filename) {
1212 | bodyPart.filename = node.dispositionParameters.filename;
1213 | } else if (node.parameters && node.parameters.name) {
1214 | bodyPart.filename = node.parameters.name;
1215 | } else {
1216 | bodyPart.filename = 'attachment';
1217 | }
1218 |
1219 | message.bodyParts.push(bodyPart);
1220 | return true;
1221 | }
1222 |
1223 | /**
1224 | * Compares numbers, sorts them ascending
1225 | */
1226 | function sortNumericallyDescending(a, b) {
1227 | return b - a;
1228 | }
1229 |
1230 | return ImapClient;
1231 | });
--------------------------------------------------------------------------------