├── .npmrc ├── .jshintignore ├── .gitignore ├── index.js ├── .jshintrc ├── .editorconfig ├── lib ├── errors.js ├── helpers │ └── getMessage.js └── imapSimple.js ├── test ├── imapTestServer.js └── index.spec.js ├── LICENSE-MIT ├── CONTRIBUTING.md ├── package.json ├── .jscsrc ├── CHANGELOG.md └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .idea 4 | coverage 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./lib/imapSimple'); 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "latedef": false, 4 | "undef": true, 5 | "unused": true, 6 | "strict": true, 7 | "mocha": true, 8 | "globals": { 9 | "Promise": true, 10 | "xit": true, 11 | "xdescribe": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | # 4 space indentation 13 | [{*.js,*.json,.jshint*,.jscsrc}] 14 | indent_style = space 15 | indent_size = 4 16 | 17 | # 2 space indentation for package.json 18 | [package.json] 19 | indent_size = 2 20 | indent_style = space 21 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var util = require('util'); 3 | 4 | /** 5 | * Error thrown when a connection attempt has timed out 6 | * 7 | * @param {number} timeout timeout in milliseconds that the connection waited before timing out 8 | * @constructor 9 | */ 10 | function ConnectionTimeoutError(timeout) { 11 | Error.call(this); 12 | Error.captureStackTrace(this, this.constructor); 13 | this.message = 'connection timed out'; 14 | 15 | if (timeout) { 16 | this.message += '. timeout = ' + timeout + ' ms'; 17 | } 18 | 19 | this.name = 'ConnectionTimeoutError'; 20 | } 21 | 22 | util.inherits(ConnectionTimeoutError, Error); 23 | 24 | exports.ConnectionTimeoutError = ConnectionTimeoutError; 25 | -------------------------------------------------------------------------------- /test/imapTestServer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var hoodiecrow = require("hoodiecrow-imap"); 4 | 5 | function startTestServer(port=1143, debug=false) { 6 | var server = hoodiecrow({ 7 | plugins: ["ID", "STARTTLS" /*, "LOGINDISABLED"*/ , "SASL-IR", "AUTH-PLAIN", "NAMESPACE", "IDLE", "ENABLE", "CONDSTORE", "XTOYBIRD", "LITERALPLUS", "UNSELECT", "SPECIAL-USE", "CREATE-SPECIAL-USE"], 8 | id: { 9 | name: "hoodiecrow", 10 | version: "0.1" 11 | }, 12 | 13 | storage: { 14 | INBOX: {} 15 | }, 16 | debug: debug 17 | }); 18 | 19 | return new Promise(function (resolve, reject) { 20 | server.listen(port, function () { 21 | resolve(server); 22 | }); 23 | }); 24 | } 25 | 26 | function appendMessage(connection, to, subject, flags = '') { 27 | var message = `Content-Type: text/plain 28 | To: ${to} 29 | Subject: ${subject} 30 | 31 | This is a test message`; 32 | connection.append(message, { mailbox: 'INBOX', flags: flags }); 33 | } 34 | 35 | module.exports = { startTestServer, appendMessage }; 36 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018, Chad McElligott. 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 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This project is **OPEN** open source 4 | 5 | ### What? 6 | 7 | Individuals making significant and valuable contributions are given commit-access to the project to contribute as they 8 | see fit. 9 | 10 | ### Rules 11 | 12 | There are a few basic ground-rules for contributors: 13 | 14 | 1. **No `--force` pushes** or modifying the git history in any way. 15 | 1. **Non-master branches** ought to be used for ongoing work. 16 | 1. **External API changes and significant modifications** ought to be subject to an **internal pull-request** to solicit 17 | feedback from other contributors. 18 | 1. Internal pull-requests to solicit feedback are *encouraged* for any other non-trivial contribution but left to the 19 | discretion of the contributor. 20 | 1. Contributors should attempt to adhere to the prevailing code-style. 21 | 22 | ### Releases 23 | 24 | Declaring formal releases remains the prerogative of the project maintainer(s). 25 | 26 | ### Changes to this arrangement 27 | 28 | This is an experiment and feedback is welcome! This document may also be subject to pull-requests or changes by contributors where you believe you have something valuable to add or change. 29 | 30 | *OPEN open source inspired by the [level](https://github.com/Level/community/blob/master/CONTRIBUTING.md) community* 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "imap-simple", 3 | "version": "6.0.0", 4 | "description": "Wrapper over node-imap, providing a simpler api for common use cases", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha", 8 | "lint": "jscs . && jshint .", 9 | "cover": "istanbul cover --report html _mocha" 10 | }, 11 | "keywords": [ 12 | "imap", 13 | "node-imap" 14 | ], 15 | "contributors": [ 16 | "Aravindo Wingeier ", 17 | "Brian Beaird", 18 | "Bruce V. Schwartz ", 19 | "Chad McElligott ", 20 | "Dominik Beste ", 21 | "Erik Bernhardsson ", 22 | "Ilari Aarnio", 23 | "Johannes Brodwall ", 24 | "John Kawakami ", 25 | "Julian Bilcke ", 26 | "Maxiem ", 27 | "Nate Watson ", 28 | "Robert Vulpe ", 29 | "Tuomas Tanner", 30 | "u2ros ", 31 | "Kaung Htet Aung " 32 | ], 33 | "license": "MIT", 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/chadxz/imap-simple.git" 37 | }, 38 | "engines": { 39 | "node": ">=6" 40 | }, 41 | "dependencies": { 42 | "iconv-lite": "~0.4.13", 43 | "imap": "^0.8.18", 44 | "nodeify": "^1.0.0", 45 | "quoted-printable": "^1.0.0", 46 | "utf8": "^2.1.1", 47 | "uuencode": "0.0.4" 48 | }, 49 | "devDependencies": { 50 | "hoodiecrow-imap": "^2.1.0", 51 | "chai": "^3.5.0", 52 | "istanbul": "^0.4.5", 53 | "jscs": "^3.0.7", 54 | "jshint": "^2.9.3", 55 | "mocha": "^3.1.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "requireCurlyBraces": [ 3 | "if", 4 | "else", 5 | "for", 6 | "while", 7 | "do", 8 | "try", 9 | "catch", 10 | "default" 11 | ], 12 | "requireSpaceAfterKeywords": [ 13 | "do", 14 | "for", 15 | "if", 16 | "else", 17 | "switch", 18 | "case", 19 | "try", 20 | "void", 21 | "while", 22 | "return", 23 | "function" 24 | ], 25 | "disallowKeywords": ["with"], 26 | "requireSpaceBeforeBlockStatements": true, 27 | "requireSpacesInConditionalExpression": true, 28 | "disallowSpacesInNamedFunctionExpression": { 29 | "beforeOpeningRoundBrace": true 30 | }, 31 | "disallowSpacesInFunctionDeclaration": { 32 | "beforeOpeningRoundBrace": true 33 | }, 34 | "requireSpacesInFunction": { 35 | "beforeOpeningCurlyBrace": true 36 | }, 37 | "disallowMultipleVarDecl": true, 38 | "disallowMultipleLineBreaks": true, 39 | "requireBlocksOnNewline": 1, 40 | "requireSpacesInsideObjectBrackets": "allButNested", 41 | "disallowQuotedKeysInObjects": "allButReserved", 42 | "disallowSpaceAfterObjectKeys": true, 43 | "requireSpaceBeforeObjectValues": true, 44 | "requireCommaBeforeLineBreak": true, 45 | "disallowSpaceBeforePostfixUnaryOperators": true, 46 | "requireSpaceBeforeBinaryOperators": true, 47 | "requireSpaceAfterBinaryOperators": true, 48 | "disallowMixedSpacesAndTabs": true, 49 | "disallowTrailingWhitespace": true, 50 | "disallowTrailingComma": true, 51 | "requireLineFeedAtFileEnd": true, 52 | "requireCapitalizedConstructors": true, 53 | "disallowYodaConditions": true, 54 | "requireSpaceAfterLineComment": true, 55 | "validateParameterSeparator": ", ", 56 | "validateIndentation": 4, 57 | "excludeFiles": [ 58 | "node_modules/**", 59 | "coverage/**" 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /lib/helpers/getMessage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Imap = require('imap'); 3 | 4 | /** 5 | * Given an 'ImapMessage' from the node-imap library, 6 | * retrieves the message formatted as: 7 | * 8 | * { 9 | * attributes: object, 10 | * parts: [ { which: string, size: number, body: string }, ... ] 11 | * } 12 | * 13 | * @param {object} message an ImapMessage from the node-imap library 14 | * @returns {Promise} a promise resolving to `message` with schema as described above 15 | */ 16 | module.exports = function getMessage(message) { 17 | return new Promise(function (resolve) { 18 | var attributes; 19 | var messageParts = []; 20 | var isHeader = /^HEADER/g; 21 | 22 | function messageOnBody(stream, info) { 23 | var body = ''; 24 | 25 | function streamOnData(chunk) { 26 | body += chunk.toString('utf8'); 27 | } 28 | 29 | stream.on('data', streamOnData); 30 | 31 | stream.once('end', function streamOnEnd() { 32 | stream.removeListener('data', streamOnData); 33 | 34 | var part = { 35 | which: info.which, 36 | size: info.size, 37 | body: body 38 | }; 39 | 40 | if (isHeader.test(part.which)) { 41 | part.body = Imap.parseHeader(part.body); 42 | } 43 | 44 | messageParts.push(part); 45 | }); 46 | } 47 | 48 | function messageOnAttributes(attrs) { 49 | attributes = attrs; 50 | } 51 | 52 | function messageOnEnd() { 53 | message.removeListener('body', messageOnBody); 54 | message.removeListener('attributes', messageOnAttributes); 55 | resolve({ 56 | attributes: attributes, 57 | parts: messageParts 58 | }); 59 | } 60 | 61 | message.on('body', messageOnBody); 62 | message.once('attributes', messageOnAttributes); 63 | message.once('end', messageOnEnd); 64 | }); 65 | }; 66 | -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var {startTestServer, appendMessage} = require('./imapTestServer'); 3 | var expect = require('chai').expect; 4 | 5 | var serverInstance = null; 6 | beforeEach(function () { 7 | return startTestServer() 8 | .then(function (server) { 9 | serverInstance = server; 10 | }); 11 | }); 12 | afterEach(function () { 13 | serverInstance.close(); 14 | }); 15 | 16 | var config = { 17 | imap: { 18 | user: 'testuser', 19 | password: 'testpass', 20 | host: 'localhost', 21 | port: 1143, 22 | tls: false, 23 | authTimeout: 3000 24 | } 25 | }; 26 | 27 | describe('imap-simple', function () { 28 | this.timeout(20000); 29 | 30 | var imaps = require('../'); 31 | 32 | it('lists unseen emails only', function () { 33 | 34 | return imaps.connect(config).then(function (connection) { 35 | 36 | return connection.openBox('INBOX') 37 | .then(function () {return appendMessage(connection, 'jim@example.com', 'unseen 1');}) 38 | .then(function () {return appendMessage(connection, 'john@example.com', 'seen 2', '\\Seen');}) 39 | .then(function () {return appendMessage(connection, 'james@example.com', 'unseen 3');}) 40 | .then(function () { 41 | var searchCriteria = [ 42 | 'UNSEEN' 43 | ]; 44 | 45 | var fetchOptions = { 46 | bodies: ['HEADER', 'TEXT'], 47 | markSeen: false 48 | }; 49 | 50 | return connection 51 | .search(searchCriteria, fetchOptions) 52 | .then(function (results) { 53 | var subjects = results.map(function (res) { 54 | return res.parts.filter(function (part) { 55 | return part.which === 'HEADER'; 56 | })[0].body.subject[0]; 57 | }); 58 | 59 | expect(subjects).to.eql([ 60 | 'unseen 1', 61 | 'unseen 3' 62 | ]); 63 | console.log(subjects); 64 | }); 65 | }); 66 | }); 67 | 68 | }); 69 | 70 | it('deletes messages', function () { 71 | 72 | return imaps.connect(config).then(function (connection) { 73 | 74 | return connection.openBox('INBOX') 75 | .then(function () {return appendMessage(connection, 'jim@example.com', 'hello from jim');}) 76 | .then(function () {return appendMessage(connection, 'bob@example.com', 'hello from bob');}) 77 | .then(function () {return appendMessage(connection, 'bob@example.com', 'hello again from bob');}) 78 | .then(function () {return connection.search(['ALL'], {bodies: ['HEADER']});}) 79 | .then(function (messages) { 80 | 81 | var uidsToDelete = messages 82 | .filter(function (message) { 83 | return message.parts.filter(function (part) { 84 | return part.which === 'HEADER'; 85 | })[0].body.to[0] === 'bob@example.com'; 86 | }) 87 | .map(function (message) { 88 | return message.attributes.uid; 89 | }); 90 | 91 | return connection.deleteMessage(uidsToDelete); 92 | }) 93 | .then(function () { 94 | return connection.search(['ALL'], {bodies: ['HEADER']}); 95 | }).then(function (messages) { 96 | 97 | var subjects = messages.map(function (res) { 98 | return res.parts.filter(function (part) { 99 | return part.which === 'HEADER'; 100 | })[0].body.subject[0]; 101 | }); 102 | 103 | expect(subjects).to.eql([ 104 | 'hello from jim' 105 | ]); 106 | console.log(subjects); 107 | }); 108 | }); 109 | }); 110 | }); 111 | 112 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## 6.0.0 - 2021-06-01 6 | 7 | #### EOL 8 | 9 | - This library is no longer actively maintained. @chadxz no longer uses it and hasn't 10 | for years. It has been archived on Github and deprecated on NPM. No replacement is 11 | suggested. Good luck. 12 | 13 | ## 5.1.0 - 2021-06-01 14 | 15 | #### Added 16 | 17 | - \#89 - @mgkha 18 | - Added wrapper for node-imap's `removeMessageLabel` 19 | 20 | ## 5.0.0 - 2020-03-30 21 | 22 | #### Added 23 | 24 | - \#40 - @brbeaird 25 | - Added wrapper for node-imap's `closeBox` with support for autoExpunge 26 | - This change makes use of default parameters in javascript, which was first 27 | supported in Node.js v6. Previously this library did not explicitly specify 28 | what Node.js versions it supported, so using this opportunity to specify that 29 | and bump major version to ensure it does not inadvertently break people. 30 | 31 | - \#60 - @synox 32 | - Added `delete` which allows for deleting messages by uid. 33 | 34 | ## 4.3.0 - 2019-01-21 35 | 36 | #### Added 37 | 38 | - \#53 - @u2ros 39 | - Added support for `UUENCODE` encoded attachment part decoding. 40 | 41 | ## 4.2.0 - 2018-11-08 42 | 43 | #### Added 44 | 45 | - \#50 - @iaarnio 46 | - Added `ImapSimple.prototype.addBox()` and `ImapSimple.prototype.delBox()` 47 | as wrappers around the same-named functions in the underlying node-imap 48 | library. 49 | 50 | ## 4.1.0 - 2018-05-31 51 | 52 | #### Added 53 | 54 | - \#47 - @AurisAudentis 55 | - Added `ImapSimple.prototype.getBoxes()` as a wrapper around the same-named 56 | function in the underlying node-imap library. 57 | 58 | ## 4.0.0 - 2018-01-09 59 | 60 | Between v3.1.0 and v3.2.0 #29 was merged to remove the `es6-promise` library from 61 | this package's dependencies, but was never released as it was a semver major change. 62 | 63 | Later, #41 was merged to add a new feature. #29 had been forgotten about, and 64 | v3.2.0 (a semver-minor release) was issued for the library. 65 | 66 | Because v3.2.0 contained breaking changes for users of the library on versions 67 | of Node that don't include Promise support, we marked it as deprecated on the 68 | npm registry and are issuing this 4.0.0 release as the current recommended 69 | version. 70 | 71 | Sorry :( 72 | 73 | ## 3.2.0 - 2017-08-21 74 | 75 | #### Added 76 | 77 | - \#41 - @jhannes 78 | - Added wrapper function `append` on the connection object to append a message 79 | to a mailbox. 80 | 81 | ## 3.1.0 - 2016-11-15 82 | 83 | #### Added 84 | 85 | - \#19 - @redpandatronicsuk 86 | - Added wrapper functions to add and delete flags from messages. 87 | - Added event listeners and corresponding options for listening for receiving 88 | new mails, message updates (such as flag changes) and external message delete 89 | events. 90 | - Added `seqno` property to retrieved messages, so the message can be 91 | correlated to received events. 92 | 93 | ## 3.0.0 - 2016-10-26 94 | 95 | #### Fixed 96 | 97 | - The ConnectionTimeoutError previously had its name set to 'BaseUrlNotSetError'. 98 | This version fixes that, but since the error was part of the library's public API 99 | and the name is technically something people could code against, the version has 100 | received a major bump. 101 | 102 | ## 2.0.0 - 2016-09-28 103 | 104 | Updated dependencies. 105 | 106 | #### Changed 107 | 108 | - The `es6-promise` module has changed its scheduler from `setImmediate()` to 109 | `nextTick()` on Node 0.10. This directly affects this module's promise API, 110 | so the major version has been bumped to indicate this. See 111 | [the es6-promise changelog](https://github.com/stefanpenner/es6-promise/blob/master/CHANGELOG.md#300) 112 | for more details about the change. 113 | 114 | ## 1.6.3 - 2016-07-20 115 | 116 | #### Fixed 117 | 118 | - \#15 - @johnkawakami - Parts of an email with 'BINARY' encoding will now be 119 | decoded as such. 120 | 121 | ## 1.6.2 - 2016-05-17 122 | 123 | #### Fixed 124 | 125 | - \#11 - @nytr0gen - Library will now reject properly when a close or end event 126 | is received when trying to connect. 127 | 128 | ## 1.6.1 - 2016-04-25 129 | 130 | #### Fixed 131 | 132 | - \#10 - @tuomastanner - fixed issue with decoding utf8 parts, specifically with 133 | respect to interacting with gmail. 134 | 135 | 136 | ## 1.6.0 - 2016-03-11 137 | 138 | #### Added 139 | 140 | - \#9 - @bvschwartz - `getPartData` is now using [iconv-lite][iconv-lite] to automatically 141 | decode message parts with an '8BIT' encoding, with a default 'utf-8' encoding set. 142 | 143 | [iconv-lite]: https://github.com/ashtuchkin/iconv-lite 144 | 145 | ## 1.5.2 - 2016-02-04 146 | 147 | #### Fixed 148 | 149 | - \#7 - @srinath-imaginea - `fetchOptions` is now properly passed when using the callback 150 | api of `search()` 151 | 152 | ## 1.5.1 - 2015-12-04 153 | 154 | #### Fixed 155 | 156 | - \#5 - @jbilcke - fixed incompatible use of all upper-case encoding name, instead of treating 157 | the encoding as case-insensitive. 158 | 159 | ## 1.5.0 - 2015-05-22 160 | 161 | #### Added 162 | 163 | - added `addMessageLabel` and `moveMessage` wrapper methods to ImapSimple class 164 | 165 | ## 1.4.0 - 2015-05-22 166 | 167 | #### Added 168 | 169 | - added `getParts` to module export and `getPartData` to ImapSimple class 170 | 171 | #### Fixed 172 | 173 | - fixed strange bug where header was sometimes not being parsed 174 | 175 | ## 1.3.2 - 2015-03-06 176 | 177 | #### Fixed 178 | 179 | - fixed property used to determine whether an error was an authTimeout 180 | 181 | ## 1.3.1 - 2015-03-04 182 | 183 | #### Fixed 184 | 185 | - fixed `connect()` option `imap.authTimeout` default not being properly set. 186 | 187 | ## 1.3.0 - 2015-03-04 188 | 189 | #### Removed 190 | 191 | - removed `options.connectTimeout`. Support has remained for backwards 192 | compatibility, but the recommended option for setting a connection timeout 193 | moving forward is `options.imap.authTimeout`. Support for 194 | `options.connectTimeout` will be removed on the next major release. 195 | 196 | ## 1.2.0 - 2015-03-02 197 | 198 | #### Added 199 | 200 | - made `ImapSimple` an event emitter 201 | 202 | ## 1.1.2 - 2015-03-02 203 | 204 | #### Fixed 205 | 206 | - Put ECONNRESET error in better place, and only ignored error when calling .end() 207 | - 'ready' and 'error' event handlers will now only fire once when connecting 208 | 209 | ## 1.1.1 - 2015-02-27 210 | 211 | #### Fixed 212 | 213 | - Put in basic fix for ECONNRESET error when calling .end() 214 | 215 | ## 1.1.0 - 2015-02-27 216 | 217 | #### Added 218 | 219 | - added .end() method to `ImapSimple` for disconnecting from imap server 220 | 221 | ## 1.0.0 - 2015-02-27 222 | 223 | #### Added 224 | 225 | - Initial commit. 226 | 227 | For more information about keeping a changelog, check out [keepachangelog.com/](http://keepachangelog.com/) 228 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # imap-simple 2 | 3 | **This library is no longer maintained and has been archived.** 4 | 5 | A library providing a simpler interface for common use cases of [node-imap][], a robust imap client for node.js. 6 | 7 | **Warning**: This library is missing a great deal of functionality from node-imap. If you have functionality you would 8 | like to see, we're accepting pull requests! 9 | 10 | ### Examples 11 | 12 | #### Retrieve the subject lines of all unread email 13 | 14 | ```js 15 | var imaps = require('imap-simple'); 16 | 17 | var config = { 18 | imap: { 19 | user: 'your@email.address', 20 | password: 'yourpassword', 21 | host: 'imap.gmail.com', 22 | port: 993, 23 | tls: true, 24 | authTimeout: 3000 25 | } 26 | }; 27 | 28 | imaps.connect(config).then(function (connection) { 29 | 30 | return connection.openBox('INBOX').then(function () { 31 | var searchCriteria = [ 32 | 'UNSEEN' 33 | ]; 34 | 35 | var fetchOptions = { 36 | bodies: ['HEADER', 'TEXT'], 37 | markSeen: false 38 | }; 39 | 40 | return connection.search(searchCriteria, fetchOptions).then(function (results) { 41 | var subjects = results.map(function (res) { 42 | return res.parts.filter(function (part) { 43 | return part.which === 'HEADER'; 44 | })[0].body.subject[0]; 45 | }); 46 | 47 | console.log(subjects); 48 | // => 49 | // [ 'Hey Chad, long time no see!', 50 | // 'Your amazon.com monthly statement', 51 | // 'Hacker Newsletter Issue #445' ] 52 | }); 53 | }); 54 | }); 55 | ``` 56 | 57 | #### Retrieve Body Content 58 | ```js 59 | var imaps = require('imap-simple'); 60 | const _ = require('lodash'); 61 | 62 | var config = { 63 | imap: { 64 | user: 'your@email.address', 65 | password: 'yourpassword', 66 | host: 'imap.gmail.com', 67 | port: 993, 68 | tls: true, 69 | authTimeout: 3000 70 | } 71 | }; 72 | 73 | imaps.connect(config).then(function (connection) { 74 | return connection.openBox('INBOX').then(function () { 75 | var searchCriteria = ['1:5']; 76 | var fetchOptions = { 77 | bodies: ['HEADER', 'TEXT'], 78 | }; 79 | return connection.search(searchCriteria, fetchOptions).then(function (messages) { 80 | messages.forEach(function (item) { 81 | var all = _.find(item.parts, { "which": "TEXT" }) 82 | var html = (Buffer.from(all.body, 'base64').toString('ascii')); 83 | console.log(html) 84 | }); 85 | }); 86 | }); 87 | }); 88 | 89 | ``` 90 | 91 | #### Usage of Mailparser in combination with imap-simple 92 | ```js 93 | var imaps = require('imap-simple'); 94 | const simpleParser = require('mailparser').simpleParser; 95 | const _ = require('lodash'); 96 | 97 | var config = { 98 | imap: { 99 | user: 'your@email.address', 100 | password: 'yourpassword', 101 | host: 'imap.gmail.com', 102 | port: 993, 103 | tls: true, 104 | authTimeout: 3000 105 | } 106 | }; 107 | 108 | imaps.connect(config).then(function (connection) { 109 | return connection.openBox('INBOX').then(function () { 110 | var searchCriteria = ['1:5']; 111 | var fetchOptions = { 112 | bodies: ['HEADER', 'TEXT', ''], 113 | }; 114 | return connection.search(searchCriteria, fetchOptions).then(function (messages) { 115 | messages.forEach(function (item) { 116 | var all = _.find(item.parts, { "which": "" }) 117 | var id = item.attributes.uid; 118 | var idHeader = "Imap-Id: "+id+"\r\n"; 119 | simpleParser(idHeader+all.body, (err, mail) => { 120 | // access to the whole mail object 121 | console.log(mail.subject) 122 | console.log(mail.html) 123 | }); 124 | }); 125 | }); 126 | }); 127 | }); 128 | ``` 129 | 130 | #### Download all attachments from all unread email since yesterday 131 | 132 | ```js 133 | var imaps = require('imap-simple'); 134 | 135 | var config = { 136 | imap: { 137 | user: 'your@email.address', 138 | password: 'yourpassword', 139 | host: 'imap.gmail.com', 140 | port: 993, 141 | tls: true, 142 | authTimeout: 3000 143 | } 144 | }; 145 | 146 | imaps.connect(config).then(function (connection) { 147 | 148 | connection.openBox('INBOX').then(function () { 149 | 150 | // Fetch emails from the last 24h 151 | var delay = 24 * 3600 * 1000; 152 | var yesterday = new Date(); 153 | yesterday.setTime(Date.now() - delay); 154 | yesterday = yesterday.toISOString(); 155 | var searchCriteria = ['UNSEEN', ['SINCE', yesterday]]; 156 | var fetchOptions = { bodies: ['HEADER.FIELDS (FROM TO SUBJECT DATE)'], struct: true }; 157 | 158 | // retrieve only the headers of the messages 159 | return connection.search(searchCriteria, fetchOptions); 160 | }).then(function (messages) { 161 | 162 | var attachments = []; 163 | 164 | messages.forEach(function (message) { 165 | var parts = imaps.getParts(message.attributes.struct); 166 | attachments = attachments.concat(parts.filter(function (part) { 167 | return part.disposition && part.disposition.type.toUpperCase() === 'ATTACHMENT'; 168 | }).map(function (part) { 169 | // retrieve the attachments only of the messages with attachments 170 | return connection.getPartData(message, part) 171 | .then(function (partData) { 172 | return { 173 | filename: part.disposition.params.filename, 174 | data: partData 175 | }; 176 | }); 177 | })); 178 | }); 179 | 180 | return Promise.all(attachments); 181 | }).then(function (attachments) { 182 | console.log(attachments); 183 | // => 184 | // [ { filename: 'cats.jpg', data: Buffer() }, 185 | // { filename: 'pay-stub.pdf', data: Buffer() } ] 186 | }); 187 | }); 188 | ``` 189 | 190 | ### Append a message to your drafts folder 191 | 192 | ```js 193 | var imaps = require('imap-simple'); 194 | 195 | var config = { 196 | imap: { 197 | user: 'your@email.address', 198 | password: 'yourpassword', 199 | host: 'imap.gmail.com', 200 | port: 993, 201 | tls: true, 202 | authTimeout: 3000 203 | } 204 | }; 205 | 206 | imaps.connect(config).then(function (connection) { 207 | const message = `Content-Type: text/plain 208 | To: jhannes@gmail.com 209 | Subject: Hello world 210 | 211 | Hi 212 | This is a test message 213 | `; 214 | connection.append(message.toString(), {mailbox: 'Drafts', flags: '\\Draft'}); 215 | }); 216 | ``` 217 | 218 | ### Open messages and delete them 219 | 220 | ```js 221 | 222 | imaps.connect(config).then(function (connection) { 223 | connection.openBox('INBOX').then(function () { 224 | 225 | var searchCriteria = ['ALL']; 226 | var fetchOptions = { bodies: ['TEXT'], struct: true }; 227 | return connection.search(searchCriteria, fetchOptions); 228 | 229 | //Loop over each message 230 | }).then(function (messages) { 231 | let taskList = messages.map(function (message) { 232 | return new Promise((res, rej) => { 233 | var parts = imaps.getParts(message.attributes.struct); 234 | parts.map(function (part) { 235 | return connection.getPartData(message, part) 236 | .then(function (partData) { 237 | 238 | //Display e-mail body 239 | if (part.disposition == null && part.encoding != "base64"){ 240 | console.log(partData); 241 | } 242 | 243 | //Mark message for deletion 244 | connection.addFlags(message.attributes.uid, "\Deleted", (err) => { 245 | if (err){ 246 | console.log('Problem marking message for deletion'); 247 | rej(err); 248 | } 249 | 250 | res(); //Final resolve 251 | }) 252 | }); 253 | }); 254 | }); 255 | }) 256 | 257 | return Promise.all(taskList).then(() => { 258 | connection.imap.closeBox(true, (err) => { //Pass in false to avoid delete-flagged messages being removed 259 | if (err){ 260 | console.log(err); 261 | } 262 | }); 263 | connection.end(); 264 | }); 265 | }); 266 | }); 267 | ``` 268 | 269 | 270 | ### delete messages by uid 271 | 272 | ```js 273 | imaps.connect(config).then(connection => { 274 | 275 | return connection.openBox('INBOX') 276 | .then(() => connection.search(['ALL'], {bodies: ['HEADER']})) 277 | .then( messages => { 278 | 279 | // select messages from bob 280 | const uidsToDelete = messages 281 | .filter( message => { 282 | return message.parts 283 | .filter( part => part.which === 'HEADER')[0].body.to[0] === 'bob@example.com'; 284 | }) 285 | .map(message => message.attributes.uid); 286 | 287 | return connection.deleteMessage(uidsToDelete); 288 | }); 289 | }); 290 | ``` 291 | 292 | ## API 293 | 294 | ### Exported module 295 | - **connect**(<*object*> options, [<*function*> callback]) - *Promise* - Main entry point. Connect to an Imap server. 296 | Upon successfully connecting to the Imap server, either calls the provided callback with signature `(err, connection)`, 297 | or resolves the returned promise with `connection`, where `connection` is an instance of *ImapSimple*. If the connection 298 | times out, either the callback will be called with the `err` property set to an instance of *ConnectionTimeoutError*, or 299 | the returned promise will be rejected with the same. Valid `options` properties are: 300 | 301 | - **imap**: Options to pass to node-imap constructor 1:1 302 | - **connectTimeout**: Time in milliseconds to wait before giving up on a connection attempt. *(Deprecated: please 303 | use `options.imap.authTimeout` instead)* 304 | 305 | - **errors.ConnectionTimeoutError**(<*number*> timeout) - *ConnectionTimeoutError* - Error thrown when a connection 306 | attempt has timed out. 307 | 308 | - **getParts**(<*Array*> struct) - *Array* - Given the `message.attributes.struct`, retrieve a flattened array of `parts` 309 | objects that describe the structure of the different parts of the message's body. Useful for getting a simple list to 310 | iterate for the purposes of, for example, finding all attachments. 311 | 312 | - **ImapSimple**(<*object*> imap) - *ImapSimple* - constructor for creating an instance of ImapSimple. Mostly used for 313 | testing. 314 | 315 | ### ImapSimple class 316 | 317 | - **addFlags**(<*mixed*> uid, <*string*> flag, [<*function*> callback]) - *Promise* - Adds the provided 318 | flag(s) to the specified message(s). `uid` is the *uid* of the message you want to add the flag to or an array of 319 | *uids*. `flag` is either a string or array of strings indicating the flags to add. When completed, either calls 320 | the provided callback with signature `(err)`, or resolves the returned promise. 321 | 322 | - **addMessageLabel**(<*mixed*> source, <*mixed*> label, [<*function*> callback]) - *Promise* - Adds the provided 323 | label(s) to the specified message(s). `source` corresponds to a node-imap *MessageSource* which specifies the messages 324 | to be moved. `label` is either a string or array of strings indicating the labels to add. When completed, either calls 325 | the provided callback with signature `(err)`, or resolves the returned promise. 326 | 327 | - **removeMessageLabel**(<*mixed*> source, <*mixed*> label, [<*function*> callback]) - *Promise* - Removes the provided 328 | label(s) from the specified message(s). `source` corresponds to a node-imap *MessageSource* which specifies the messages 329 | to be removed. `label` is either a string or array of strings indicating the labels to remove. When completed, either calls 330 | the provided callback with signature `(err)`, or resolves the returned promise. 331 | 332 | - **append**(<*mixed*> message, [<*object*> options], [<*function*> callback]) - *Promise* - Appends the argument 333 | message to the currently open mailbox or another mailbox. `message` is a RFC-822 compatible MIME message. Valid `options` 334 | are *mailbox*, *flags* and *date*. When completed, either calls the provided callback with signature `(err)`, or resolves 335 | the returned promise. 336 | 337 | - **delFlags**(<*mixed*> uid, <*string*> flag, [<*function*> callback]) - *Promise* - Removes the provided 338 | flag(s) from the specified message(s). `uid` is the *uid* of the message you want to remove the flag from or an array of 339 | *uids*. `flag` is either a string or array of strings indicating the flags to remove. When completed, either calls 340 | the provided callback with signature `(err)`, or resolves the returned promise. 341 | 342 | - **end**() - *undefined* - Close the connection to the imap server. 343 | 344 | - **getBoxes**([<*function*> callback]) - *Promise* - Returns the full list of mailboxes (folders). Upon success, either 345 | the provided callback will be called with signature `(err, boxes)`, or the returned promise will be resolved with `boxes`. 346 | `boxes` is the exact object returned from the node-imap *getBoxes()* result. 347 | 348 | - **getPartData**(<*object*> message, <*object*> part, [<*function*> callback]) - *Promise* - Downloads part data 349 | (which is either part of the message body, or an attachment). Upon success, either the provided callback will be called 350 | with signature `(err, data)`, or the returned promise will be resolved with `data`. The data will be automatically 351 | decoded based on its encoding. If the encoding of the part is not supported, an error will occur. 352 | 353 | - **deleteMessage**(<*mixed*> uid, [<*function*> callback]) - *Promise* - Deletes the specified 354 | message(s). `uid` is the *uid* of the message you want to add the flag to or an array of *uids*. 355 | When completed, either calls the provided callback with signature `(err)`, or resolves the returned promise. 356 | 357 | - **moveMessage**(<*mixed*> source, <*string*> boxName, [<*function*> callback]) - *Promise* - Moves the specified 358 | message(s) in the currently open mailbox to another mailbox. `source` corresponds to a node-imap *MessageSource* which 359 | specifies the messages to be moved. When completed, either calls the provided callback with signature `(err)`, or 360 | resolves the returned promise. 361 | 362 | - **openBox**(<*string*> boxName, [<*function*> callback]) - *Promise* - Open a mailbox, calling the provided callback 363 | with signature `(err, boxName)`, or resolves the returned promise with `boxName`. 364 | 365 | - **closeBox**(<*boolean*> [autoExpunge = true], [<*function*> callback]) - *Promise* - Close a mailbox, calling the provided callback 366 | with signature `(err)`, or resolves the returned promise. If autoExpunge is true, any messages marked as Deleted in the currently 367 | open mailbox will be removed. 368 | 369 | - **addBox**(<*string*> boxName, [<*function*> callback]) - *Promise* - Create a mailbox, calling the provided callback 370 | with signature `(err, boxName)`, or resolves the returned promise with `boxName`. 371 | 372 | - **delBox**(<*string*> boxName, [<*function*> callback]) - *Promise* - Delete a mailbox, calling the provided callback 373 | with signature `(err, boxName)`, or resolves the returned promise with `boxName`. 374 | 375 | - **search**(<*object*> searchCriteria, [<*object*> fetchOptions], [<*function*> callback]) - *Promise* - Search for and 376 | retrieve mail in the currently open mailbox. The search is performed based on the provided `searchCriteria`, which is 377 | the exact same format as [node-imap][] requires. All results will be subsequently downloaded, according to the options 378 | provided by `fetchOptions`, which are also identical to those passed to `fetch` of [node-imap][]. Upon a successful 379 | search+fetch operation, either the provided callback will be called with signature `(err, results)`, or the returned 380 | promise will be resolved with `results`. The format of `results` is detailed below. See node-imap's *ImapMessage* 381 | signature for information about `attributes`, `which`, `size`, and `body`. For any message part that is a `HEADER`, the 382 | body is automatically parsed into an object. 383 | ```js 384 | // [{ 385 | // attributes: object, 386 | // parts: [ { which: string, size: number, body: string }, ... ] 387 | // }, ...] 388 | ``` 389 | 390 | ## Server events 391 | Functions to listen to server events are configured in the configuration object that is passed to the `connect` function. 392 | ImapSimple only implements a subset of the server event functions that *node-imap* supports, [see here](https://github.com/mscdex/node-imap#connection-events), 393 | which are `mail`, `expunge` and `update`. Add them to the configuration object as follows: 394 | 395 | ``` 396 | var config = { 397 | imap: { 398 | ... 399 | }, 400 | onmail: function (numNewMail) { 401 | ... 402 | }, 403 | onexpunge: function (seqno) { 404 | ... 405 | }, 406 | onupdate: function (seqno, info) { 407 | ... 408 | } 409 | }; 410 | ``` 411 | 412 | For more information [see here](https://github.com/mscdex/node-imap#connection-events). 413 | 414 | ## Contributing 415 | Pull requests welcome! This project really needs tests, so those would be very welcome. If you have a use case you want 416 | supported, please feel free to add, but be sure to follow the patterns established thus far, mostly: 417 | 418 | - support promises **AND** callbacks 419 | - make your api as simple as possible 420 | - don't worry about exposing implementation details of [node-imap][] when needed 421 | 422 | This project is **OPEN** open source. See [CONTRIBUTING.md](CONTRIBUTING.md) for more details about contributing. 423 | 424 | ## Semver 425 | This project follows [semver](http://semver.org/). Namely: 426 | 427 | - new MAJOR versions when incompatible API changes are made, 428 | - new MINOR versions for backwards-compatible feature additions, 429 | - new PATCH versions for backwards-compatible bug fixes 430 | 431 | ## License 432 | [MIT](LICENSE-MIT) 433 | 434 | [node-imap]: https://github.com/mscdex/node-imap 435 | -------------------------------------------------------------------------------- /lib/imapSimple.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Imap = require('imap'); 3 | var nodeify = require('nodeify'); 4 | var getMessage = require('./helpers/getMessage'); 5 | var errors = require('./errors'); 6 | var util = require('util'); 7 | var EventEmitter = require('events').EventEmitter; 8 | var qp = require('quoted-printable'); 9 | var iconvlite = require('iconv-lite'); 10 | var utf8 = require('utf8'); 11 | var uuencode = require('uuencode'); 12 | 13 | /** 14 | * Constructs an instance of ImapSimple 15 | * 16 | * @param {object} imap a constructed node-imap connection 17 | * @constructor 18 | * @class ImapSimple 19 | */ 20 | function ImapSimple(imap) { 21 | var self = this; 22 | self.imap = imap; 23 | 24 | // flag to determine whether we should suppress ECONNRESET from bubbling up to listener 25 | self.ending = false; 26 | 27 | // pass most node-imap `Connection` events through 1:1 28 | ['alert', 'mail', 'expunge', 'uidvalidity', 'update', 'close', 'end'].forEach(function (event) { 29 | self.imap.on(event, self.emit.bind(self, event)); 30 | }); 31 | 32 | // special handling for `error` event 33 | self.imap.on('error', function (err) { 34 | // if .end() has been called and an 'ECONNRESET' error is received, don't bubble 35 | if (err && self.ending && (err.code.toUpperCase() === 'ECONNRESET')) { 36 | return; 37 | } 38 | 39 | self.emit('error', err); 40 | }); 41 | } 42 | 43 | util.inherits(ImapSimple, EventEmitter); 44 | 45 | /** 46 | * disconnect from the imap server 47 | */ 48 | ImapSimple.prototype.end = function () { 49 | var self = this; 50 | 51 | // set state flag to suppress 'ECONNRESET' errors that are triggered when .end() is called. 52 | // it is a known issue that has no known fix. This just temporarily ignores that error. 53 | // https://github.com/mscdex/node-imap/issues/391 54 | // https://github.com/mscdex/node-imap/issues/395 55 | self.ending = true; 56 | 57 | // using 'close' event to unbind ECONNRESET error handler, because the node-imap 58 | // maintainer claims it is the more reliable event between 'end' and 'close'. 59 | // https://github.com/mscdex/node-imap/issues/394 60 | self.imap.once('close', function () { 61 | self.ending = false; 62 | }); 63 | 64 | self.imap.end(); 65 | }; 66 | 67 | /** 68 | * Open a mailbox 69 | * 70 | * @param {string} boxName The name of the box to open 71 | * @param {function} [callback] Optional callback, receiving signature (err, boxName) 72 | * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving to `boxName` 73 | * @memberof ImapSimple 74 | */ 75 | ImapSimple.prototype.openBox = function (boxName, callback) { 76 | var self = this; 77 | 78 | if (callback) { 79 | return nodeify(this.openBox(boxName), callback); 80 | } 81 | 82 | return new Promise(function (resolve, reject) { 83 | 84 | self.imap.openBox(boxName, function (err, result) { 85 | 86 | if (err) { 87 | reject(err); 88 | return; 89 | } 90 | 91 | resolve(result); 92 | }); 93 | }); 94 | }; 95 | 96 | /** 97 | * Close a mailbox 98 | * 99 | * @param {boolean} [autoExpunge=true] If autoExpunge is true, any messages marked as Deleted in the currently open mailbox will be remove 100 | * @param {function} [callback] Optional callback, receiving signature (err) 101 | * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving to `boxName` 102 | * @memberof ImapSimple 103 | */ 104 | ImapSimple.prototype.closeBox = function (autoExpunge=true, callback) { 105 | var self = this; 106 | 107 | if (typeof(autoExpunge) == 'function'){ 108 | callback = autoExpunge; 109 | autoExpunge = true; 110 | } 111 | 112 | if (callback) { 113 | return nodeify(this.closeBox(autoExpunge), callback); 114 | } 115 | 116 | return new Promise(function (resolve, reject) { 117 | 118 | self.imap.closeBox(autoExpunge, function (err, result) { 119 | 120 | if (err) { 121 | reject(err); 122 | return; 123 | } 124 | 125 | resolve(result); 126 | }); 127 | }); 128 | }; 129 | 130 | /** 131 | * Search the currently open mailbox, and retrieve the results 132 | * 133 | * Results are in the form: 134 | * 135 | * [{ 136 | * attributes: object, 137 | * parts: [ { which: string, size: number, body: string }, ... ] 138 | * }, ...] 139 | * 140 | * See node-imap's ImapMessage signature for information about `attributes`, `which`, `size`, and `body`. 141 | * For any message part that is a `HEADER`, the body is automatically parsed into an object. 142 | * 143 | * @param {object} searchCriteria Criteria to use to search. Passed to node-imap's .search() 1:1 144 | * @param {object} fetchOptions Criteria to use to fetch the search results. Passed to node-imap's .fetch() 1:1 145 | * @param {function} [callback] Optional callback, receiving signature (err, results) 146 | * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving to `results` 147 | * @memberof ImapSimple 148 | */ 149 | ImapSimple.prototype.search = function (searchCriteria, fetchOptions, callback) { 150 | var self = this; 151 | 152 | if (!callback && typeof fetchOptions === 'function') { 153 | callback = fetchOptions; 154 | fetchOptions = null; 155 | } 156 | 157 | if (callback) { 158 | return nodeify(this.search(searchCriteria, fetchOptions), callback); 159 | } 160 | 161 | return new Promise(function (resolve, reject) { 162 | 163 | self.imap.search(searchCriteria, function (err, uids) { 164 | 165 | if (err) { 166 | reject(err); 167 | return; 168 | } 169 | 170 | if (!uids.length) { 171 | resolve([]); 172 | return; 173 | } 174 | 175 | var fetch = self.imap.fetch(uids, fetchOptions); 176 | var messagesRetrieved = 0; 177 | var messages = []; 178 | 179 | function fetchOnMessage(message, seqNo) { 180 | getMessage(message).then(function (message) { 181 | message.seqNo = seqNo; 182 | messages[seqNo] = message; 183 | 184 | messagesRetrieved++; 185 | if (messagesRetrieved === uids.length) { 186 | fetchCompleted(); 187 | } 188 | }); 189 | } 190 | 191 | function fetchCompleted() { 192 | // pare array down while keeping messages in order 193 | var pared = messages.filter(function (m) { return !!m; }); 194 | resolve(pared); 195 | } 196 | 197 | function fetchOnError(err) { 198 | fetch.removeListener('message', fetchOnMessage); 199 | fetch.removeListener('end', fetchOnEnd); 200 | reject(err); 201 | } 202 | 203 | function fetchOnEnd() { 204 | fetch.removeListener('message', fetchOnMessage); 205 | fetch.removeListener('error', fetchOnError); 206 | } 207 | 208 | fetch.on('message', fetchOnMessage); 209 | fetch.once('error', fetchOnError); 210 | fetch.once('end', fetchOnEnd); 211 | }); 212 | }); 213 | }; 214 | 215 | /** 216 | * Download a "part" (either a portion of the message body, or an attachment) 217 | * 218 | * @param {object} message The message returned from `search()` 219 | * @param {object} part The message part to be downloaded, from the `message.attributes.struct` Array 220 | * @param {function} [callback] Optional callback, receiving signature (err, data) 221 | * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving to `data` 222 | * @memberof ImapSimple 223 | */ 224 | ImapSimple.prototype.getPartData = function (message, part, callback) { 225 | var self = this; 226 | 227 | if (callback) { 228 | return nodeify(self.getPartData(message, part), callback); 229 | } 230 | 231 | return new Promise(function (resolve, reject) { 232 | var fetch = self.imap.fetch(message.attributes.uid, { 233 | bodies: [part.partID], 234 | struct: true 235 | }); 236 | 237 | function fetchOnMessage(msg) { 238 | getMessage(msg).then(function (result) { 239 | if (result.parts.length !== 1) { 240 | reject(new Error('Got ' + result.parts.length + ' parts, should get 1')); 241 | return; 242 | } 243 | 244 | var data = result.parts[0].body; 245 | 246 | var encoding = part.encoding.toUpperCase(); 247 | 248 | if (encoding === 'BASE64') { 249 | resolve(new Buffer(data, 'base64')); 250 | return; 251 | } 252 | 253 | if (encoding === 'QUOTED-PRINTABLE') { 254 | if (part.params && part.params.charset && 255 | part.params.charset.toUpperCase() === 'UTF-8') { 256 | resolve((new Buffer(utf8.decode(qp.decode(data)))).toString()); 257 | } else { 258 | resolve((new Buffer(qp.decode(data))).toString()); 259 | } 260 | return; 261 | } 262 | 263 | if (encoding === '7BIT') { 264 | resolve((new Buffer(data)).toString('ascii')); 265 | return; 266 | } 267 | 268 | if (encoding === '8BIT' || encoding === 'BINARY') { 269 | var charset = (part.params && part.params.charset) || 'utf-8'; 270 | resolve(iconvlite.decode(new Buffer(data), charset)); 271 | return; 272 | } 273 | 274 | if (encoding === 'UUENCODE') { 275 | var parts = data.toString().split('\n'); // remove newline characters 276 | var merged = parts.splice(1, parts.length - 4).join(''); // remove excess lines and join lines with empty string 277 | resolve(uuencode.decode(merged)); 278 | return; 279 | } 280 | 281 | // if it gets here, the encoding is not currently supported 282 | reject(new Error('Unknown encoding ' + part.encoding)); 283 | }); 284 | } 285 | 286 | function fetchOnError(err) { 287 | fetch.removeListener('message', fetchOnMessage); 288 | fetch.removeListener('end', fetchOnEnd); 289 | reject(err); 290 | } 291 | 292 | function fetchOnEnd() { 293 | fetch.removeListener('message', fetchOnMessage); 294 | fetch.removeListener('error', fetchOnError); 295 | } 296 | 297 | fetch.once('message', fetchOnMessage); 298 | fetch.once('error', fetchOnError); 299 | fetch.once('end', fetchOnEnd); 300 | }); 301 | }; 302 | 303 | /** 304 | * Moves the specified message(s) in the currently open mailbox to another mailbox. 305 | * 306 | * @param {string|Array} source The node-imap `MessageSource` indicating the message(s) from the current open mailbox 307 | * to move. 308 | * @param {string} boxName The mailbox to move the message(s) to. 309 | * @param {function} [callback] Optional callback, receiving signature (err) 310 | * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving when the action succeeds. 311 | * @memberof ImapSimple 312 | */ 313 | ImapSimple.prototype.moveMessage = function (source, boxName, callback) { 314 | var self = this; 315 | 316 | if (callback) { 317 | return nodeify(self.moveMessage(source, boxName), callback); 318 | } 319 | 320 | return new Promise(function (resolve, reject) { 321 | self.imap.move(source, boxName, function (err) { 322 | if (err) { 323 | reject(err); 324 | return; 325 | } 326 | 327 | resolve(); 328 | }); 329 | }); 330 | }; 331 | 332 | /** 333 | * Adds the provided label(s) to the specified message(s). 334 | * 335 | * This is a Gmail extension method (X-GM-EXT-1) 336 | * 337 | * @param {string|Array} source The node-imap `MessageSource` indicating the message(s) to add the label(s) to. 338 | * @param {string|Array} labels Either a single string or an array of strings indicating the labels to add to the 339 | * message(s). 340 | * @param {function} [callback] Optional callback, receiving signature (err) 341 | * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving when the action succeeds. 342 | * @memberof ImapSimple 343 | */ 344 | ImapSimple.prototype.addMessageLabel = function (source, labels, callback) { 345 | var self = this; 346 | 347 | if (callback) { 348 | return nodeify(self.addMessageLabel(source, labels), callback); 349 | } 350 | 351 | return new Promise(function (resolve, reject) { 352 | self.imap.addLabels(source, labels, function (err) { 353 | if (err) { 354 | reject(err); 355 | return; 356 | } 357 | 358 | resolve(); 359 | }); 360 | }); 361 | }; 362 | 363 | /** 364 | * Remove the provided label(s) from the specified message(s). 365 | * 366 | * This is a Gmail extension method (X-GM-EXT-1) 367 | * 368 | * @param {string|Array} source The node-imap `MessageSource` indicating the message(s) to remove the label(s) from. 369 | * @param {string|Array} labels Either a single string or an array of strings indicating the labels to remove from the 370 | * message(s). 371 | * @param {function} [callback] Optional callback, receiving signature (err) 372 | * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving when the action succeeds. 373 | * @memberof ImapSimple 374 | */ 375 | ImapSimple.prototype.removeMessageLabel = function (source, labels, callback) { 376 | var self = this; 377 | 378 | if (callback) { 379 | return nodeify(self.removeMessageLabel(source, labels), callback); 380 | } 381 | 382 | return new Promise(function (resolve, reject) { 383 | self.imap.delLabels(source, labels, function (err) { 384 | if (err) { 385 | reject(err); 386 | return; 387 | } 388 | 389 | resolve(); 390 | }); 391 | }); 392 | }; 393 | 394 | /** 395 | * Adds the provided flag(s) to the specified message(s). 396 | * 397 | * @param {string|Array} uid The messages uid 398 | * @param {string|Array} flags Either a single string or an array of strings indicating the flags to add to the 399 | * message(s). 400 | * @param {function} [callback] Optional callback, receiving signature (err) 401 | * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving when the action succeeds. 402 | * @memberof ImapSimple 403 | */ 404 | ImapSimple.prototype.addFlags = function (uid, flags, callback) { 405 | var self = this; 406 | 407 | if (callback) { 408 | return nodeify(self.addFlags(uid, flags), callback); 409 | } 410 | 411 | return new Promise(function (resolve, reject) { 412 | self.imap.addFlags(uid, flags, function (err) { 413 | if (err) { 414 | reject(err); 415 | return; 416 | } 417 | 418 | resolve(); 419 | }); 420 | }); 421 | }; 422 | 423 | /** 424 | * Removes the provided flag(s) to the specified message(s). 425 | * 426 | * @param {string|Array} uid The messages uid 427 | * @param {string|Array} flags Either a single string or an array of strings indicating the flags to remove from the 428 | * message(s). 429 | * @param {function} [callback] Optional callback, receiving signature (err) 430 | * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving when the action succeeds. 431 | * @memberof ImapSimple 432 | */ 433 | ImapSimple.prototype.delFlags = function (uid, flags, callback) { 434 | var self = this; 435 | 436 | if (callback) { 437 | return nodeify(self.delFlags(uid, flags), callback); 438 | } 439 | 440 | return new Promise(function (resolve, reject) { 441 | self.imap.delFlags(uid, flags, function (err) { 442 | if (err) { 443 | reject(err); 444 | return; 445 | } 446 | 447 | resolve(); 448 | }); 449 | }); 450 | }; 451 | 452 | /** 453 | * Deletes the specified message(s). 454 | * 455 | * @param {string|Array} uid The uid or array of uids indicating the messages to be deleted 456 | * @param {function} [callback] Optional callback, receiving signature (err) 457 | * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving when the action succeeds. 458 | * @memberof ImapSimple 459 | */ 460 | ImapSimple.prototype.deleteMessage = function (uid, callback) { 461 | var self = this; 462 | 463 | if (callback) { 464 | return nodeify(self.deleteMessage(uid), callback); 465 | } 466 | 467 | return new Promise(function (resolve, reject) { 468 | self.imap.addFlags(uid, '\\Deleted', function (err) { 469 | if (err) { 470 | reject(err); 471 | return; 472 | } 473 | self.imap.expunge( function (err) { 474 | if (err) { 475 | reject(err); 476 | return; 477 | } 478 | resolve(); 479 | }); 480 | }); 481 | }); 482 | }; 483 | 484 | /** 485 | * Appends a mime-encoded message to a mailbox 486 | * 487 | * @param {string|Buffer} message The messages to append to the mailbox 488 | * @param {object} [options] 489 | * @param {string} [options.mailbox] The mailbox to append the message to. 490 | Defaults to the currently open mailbox. 491 | * @param {string|Array} [options.flag] A single flag (e.g. 'Seen') or an array 492 | of flags (e.g. ['Seen', 'Flagged']) to append to the message. Defaults to 493 | no flags. 494 | * @param {function} [callback] Optional callback, receiving signature (err) 495 | * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving when the action succeeds. 496 | * @memberof ImapSimple 497 | */ 498 | ImapSimple.prototype.append = function (message, options, callback) { 499 | var self = this; 500 | 501 | if (callback) { 502 | return nodeify(self.append(message, options), callback); 503 | } 504 | 505 | return new Promise(function (resolve, reject) { 506 | self.imap.append(message, options, function (err) { 507 | if (err) { 508 | reject(err); 509 | return; 510 | } 511 | 512 | resolve(); 513 | }); 514 | }); 515 | }; 516 | 517 | /** 518 | * Returns a list of mailboxes (folders). 519 | * 520 | * @param {function} [callback] Optional callback containing 'boxes' object. 521 | * @returns {undefined|Promise} Returns a promise when no callback is specified, 522 | * resolving when the action succeeds. 523 | */ 524 | 525 | ImapSimple.prototype.getBoxes = function (callback) { 526 | var self = this; 527 | 528 | if (callback) { 529 | return nodeify(self.getBoxes(), callback); 530 | } 531 | 532 | return new Promise(function (resolve, reject) { 533 | self.imap.getBoxes(function (err, boxes) { 534 | if (err) { 535 | reject(err); 536 | return; 537 | } 538 | 539 | resolve(boxes); 540 | }); 541 | }); 542 | }; 543 | 544 | /** 545 | * Add new mailbox (folder) 546 | * 547 | * @param {string} boxName The name of the box to added 548 | * @param {function} [callback] Optional callback, receiving signature (err, boxName) 549 | * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving to `boxName` 550 | * @memberof ImapSimple 551 | */ 552 | ImapSimple.prototype.addBox = function (boxName, callback) { 553 | var self = this; 554 | 555 | if (callback) { 556 | return nodeify(this.addBox(boxName), callback); 557 | } 558 | 559 | return new Promise(function (resolve, reject) { 560 | 561 | self.imap.addBox(boxName, function (err) { 562 | 563 | if (err) { 564 | reject(err); 565 | return; 566 | } 567 | 568 | resolve(boxName); 569 | }); 570 | }); 571 | }; 572 | 573 | /** 574 | * Delete mailbox (folder) 575 | * 576 | * @param {string} boxName The name of the box to deleted 577 | * @param {function} [callback] Optional callback, receiving signature (err, boxName) 578 | * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving to `boxName` 579 | * @memberof ImapSimple 580 | */ 581 | ImapSimple.prototype.delBox = function (boxName, callback) { 582 | var self = this; 583 | 584 | if (callback) { 585 | return nodeify(this.delBox(boxName), callback); 586 | } 587 | 588 | return new Promise(function (resolve, reject) { 589 | 590 | self.imap.delBox(boxName, function (err) { 591 | 592 | if (err) { 593 | reject(err); 594 | return; 595 | } 596 | 597 | resolve(boxName); 598 | }); 599 | }); 600 | }; 601 | 602 | /** 603 | * Connect to an Imap server, returning an ImapSimple instance, which is a wrapper over node-imap to 604 | * simplify it's api for common use cases. 605 | * 606 | * @param {object} options 607 | * @param {object} options.imap Options to pass to node-imap constructor 1:1 608 | * @param {function} [callback] Optional callback, receiving signature (err, connection) 609 | * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving to `connection` 610 | */ 611 | function connect(options, callback) { 612 | options = options || {}; 613 | options.imap = options.imap || {}; 614 | 615 | // support old connectTimeout config option. Remove in v2.0.0 616 | if (options.hasOwnProperty('connectTimeout')) { 617 | console.warn('[imap-simple] connect: options.connectTimeout is deprecated. ' + 618 | 'Please use options.imap.authTimeout instead.'); 619 | options.imap.authTimeout = options.connectTimeout; 620 | } 621 | 622 | // set default authTimeout 623 | options.imap.authTimeout = options.imap.hasOwnProperty('authTimeout') ? options.imap.authTimeout : 2000; 624 | 625 | if (callback) { 626 | return nodeify(connect(options), callback); 627 | } 628 | 629 | return new Promise(function (resolve, reject) { 630 | var imap = new Imap(options.imap); 631 | 632 | function imapOnReady() { 633 | imap.removeListener('error', imapOnError); 634 | imap.removeListener('close', imapOnClose); 635 | imap.removeListener('end', imapOnEnd); 636 | resolve(new ImapSimple(imap)); 637 | } 638 | 639 | function imapOnError(err) { 640 | if (err.source === 'timeout-auth') { 641 | err = new errors.ConnectionTimeoutError(options.imap.authTimeout); 642 | } 643 | 644 | imap.removeListener('ready', imapOnReady); 645 | imap.removeListener('close', imapOnClose); 646 | imap.removeListener('end', imapOnEnd); 647 | reject(err); 648 | } 649 | 650 | function imapOnEnd() { 651 | imap.removeListener('ready', imapOnReady); 652 | imap.removeListener('error', imapOnError); 653 | imap.removeListener('close', imapOnClose); 654 | reject(new Error('Connection ended unexpectedly')); 655 | } 656 | 657 | function imapOnClose() { 658 | imap.removeListener('ready', imapOnReady); 659 | imap.removeListener('error', imapOnError); 660 | imap.removeListener('end', imapOnEnd); 661 | reject(new Error('Connection closed unexpectedly')); 662 | } 663 | 664 | imap.once('ready', imapOnReady); 665 | imap.once('error', imapOnError); 666 | imap.once('close', imapOnClose); 667 | imap.once('end', imapOnEnd); 668 | 669 | if (options.hasOwnProperty('onmail')) { 670 | imap.on('mail', options.onmail); 671 | } 672 | 673 | if (options.hasOwnProperty('onexpunge')) { 674 | imap.on('expunge', options.onexpunge); 675 | } 676 | 677 | if (options.hasOwnProperty('onupdate')) { 678 | imap.on('update', options.onupdate); 679 | } 680 | 681 | imap.connect(); 682 | }); 683 | } 684 | 685 | /** 686 | * Given the `message.attributes.struct`, retrieve a flattened array of `parts` objects that describe the structure of 687 | * the different parts of the message's body. Useful for getting a simple list to iterate for the purposes of, 688 | * for example, finding all attachments. 689 | * 690 | * Code taken from http://stackoverflow.com/questions/25247207/how-to-read-and-save-attachments-using-node-imap 691 | * 692 | * @param {Array} struct The `message.attributes.struct` value from the message you wish to retrieve parts for. 693 | * @param {Array} [parts] The list of parts to push to. 694 | * @returns {Array} a flattened array of `parts` objects that describe the structure of the different parts of the 695 | * message's body 696 | */ 697 | function getParts(struct, parts) { 698 | parts = parts || []; 699 | for (var i = 0; i < struct.length; i++) { 700 | if (Array.isArray(struct[i])) { 701 | getParts(struct[i], parts); 702 | } else if (struct[i].partID) { 703 | parts.push(struct[i]); 704 | } 705 | } 706 | return parts; 707 | } 708 | 709 | module.exports = { 710 | connect: connect, 711 | ImapSimple: ImapSimple, 712 | parseHeader: Imap.parseHeader, 713 | getParts: getParts, 714 | errors: errors 715 | }; 716 | --------------------------------------------------------------------------------