Felix is not actively maintaining this library anymore. But this is the only IMAP client for JS that I am aware of, so I feel this library still has its value. Please let me know if you're interested in helping out, either via email or open an issue about that.
The work that's on the horizon is:
net
and tls
directlyLow-level IMAP client for all your IMAP needs.
npm install --save emailjs-imap-client
import ImapClient from 'emailjs-imap-client'
// Use this instead for CommonJS modules (Node.js)
var ImapClient = require('emailjs-imap-client').default
var client = new ImapClient(host[, port][, options])
Please note that instances cannot be reused! After terminating a connection or encountering an error, please create a new ImapClient instance!
Where
src/common.js
{name: 'myclient', version: '1'}
)Default STARTTLS support is opportunistic – if the server advertises STARTTLS capability, the client tries to use it. If STARTTLS is not advertised, the clients sends passwords in the plain. You can use ignoreTLS
and requireTLS
to change this behavior by explicitly enabling or disabling STARTTLS usage.
Example
var client = new ImapClient('localhost', 143, {
auth: {
user: 'testuser',
pass: 'testpass'
}
});
Use of web workers with compression: If you use compression, we can spin up a Web Worker to handle the TLS-related computation off the main thread. To do this, you need to browserify emailjs-imap-client-compressor-worker.js
, specify the path via options.compressionWorkerPath
client.onerror = function(error){}
Call client.openConnection()
and client.close()
without authentication to connect to server and get server capability before logging in:
var client = new ImapClient('localhost', 143);
client.openConnection().then(capability => {
client.close()
/* check capability too see, for example, if server is a gmail server and thereby decide on how to authenticate when connecting */
});
Call client.connect()
to establish an IMAP connection:
client.connect().then(() => { /* ready to roll */ });
There are two ways to close the connection.
The IMAP way is to send the LOGOUT command with logout()
.
client.logout().then(() => { /* connection terminated */ });
This method doesn't actually terminate the connection, it sends LOGOUT command to the server, to which the server responds by closing the connection.
The better way is to force-close the connection with close()
. This closes the TCP socket and is independent of the network status.
client.close().then(() => { /* connection terminated */ });
List all mailboxes with listMailboxes()
method
client.listMailboxes().then((mailboxes) => { ... })
Mailbox object is with the following structure
true
if the node is rootspecialUse
but without using folder name based heuristicsExample mailboxes
object:
{
"root": true,
"children": [
{
"name": "INBOX",
"delimiter": "/",
"path": "INBOX",
"children": [],
"flags": ["\\HasNoChildren"],
"listed": true,
"subscribed": true
},
{
"name": "[Gmail]",
"delimiter": "/",
"path": "[Gmail]",
"flags": ["\\Noselect","\\HasChildren"],
"listed": true,
"subscribed": true,
"children": [
{
"name": "All Mail",
"delimiter": "/",
"path": "[Gmail]/All Mail",
"children": [],
"flags": ["\\HasNoChildren","\\All"],
"listed": true,
"specialUse": "\\All",
"specialUseFlag": "\\All",
"subscribed": true
}
]
}
]
}
Root level INBOX
is case insensitive, so all subfolders of INBOX, Inbox etc. are mapped together. The first occurence of INBOX
defines the name
property for the parent element. path
values remain as listed.
For example the following IMAP response lists different INBOX names:
* LIST () "INBOX"
* LIST () "Inbox/test"
These different INBOX names are mapped to the following object:
{
"root": true,
"children": [
{
"name": "INBOX",
"delimiter": "/",
"path": "INBOX",
"children": [
{
"name": "test",
"delimiter": "/",
"path": "Inbox/test",
}
]
}
]
}
List available namespaces with listNamespaces()
. If NAMESPACE extension is not supported, the method is a no-op.
Namespace object is with the following structure
false
for Personal Namespacefalse
for Other Users' Namespacefalse
for Shared NamespaceNamespace element object has the following structure
NB! Namespace_Response_Extensions are not supported (extension data is silently skipped)
Namespaces should be checked before attempting to create new mailboxes - most probably creating mailboxes outside personal namespace fails. For example when the personal namespace is prefixed with 'INBOX.' you can create 'INBOX.Sent Mail' but you can't create 'Sent Mail'.
Example:
client.listNamespaces().then((namespaces) => { ... })
{
"personal": [
{
"prefix": "",
"delimiter": "/"
}
],
"users": false,
"shared": false
}
Subscribe to a mailbox with the given path with subscribeMailbox(path)
.
Subscribing to a mailbox that is already subscribed is redundant and does return an OK just as when subscribing to unsubscribed mailbox.
Command: SUBSCRIBE
Example
// On a server with unsubscribed Sent mailbox
client.subscribeMailbox('Sent').then(() => { ... });
Unsubscribe from a mailbox with the given path with unsubscribeMailbox(path)
.
Unsubscribing from a mailbox that is already unsubscribed is redundant and does return an OK just as when unsubscribing from a subscribed mailbox.
Command: UNSUBSCRIBE
Example
// On a server with subscribed Sent mailbox
client.unsubscribeMailbox('Sent').then(() => { ... });
Create a folder with the given path with createMailbox(path)
. You currently need to manually build the path string yourself.
If the server indicates a failure that the folder already exists, but responds with the ALREADYEXISTS response code, the request will be treated as a success.
Command: CREATE
Example
// On a server with a personal namesapce of INBOX and a delimiter of '/',
// create folder Foo. Note that folders using a non-empty personal namespace
// may automatically assume the personal namespace.
client.createMailbox('INBOX/Foo').then(() => { ... });
// Do the same on a server where the personal namespace is ''
client.createMailbox('Foo').then(() => { ... });
Delete a folder with the given path with deleteMailbox(path)
.
Command: DELETE
Example
client.deleteMailbox('Foo').then(() => { ... });
Select specific mailbox by path with selectMailbox(path, options)
Where
listMailboxes
)true
adds (CONDSTORE) option when selectingtrue
uses EXAMINE
instead of SELECT
Resolves with
true
if the mailbox is in read only modeExample
client.selectMailbox('INBOX').then((mailbox) => { ... });
{
"readOnly": false,
"exists": 6596,
"flags": [
"\\Answered",
"\\Flagged"
],
"permanentFlags": [
"\\Answered",
"\\Flagged"
],
"uidValidity": 2,
"uidNext": 38361,
"highestModseq": "3682918"
}
List messages with listMessages(path, sequence, query[, options])
Where
byUid
option is set to true). Example: '1', '1:*', '1,2:3,4' etc.true
executes UID FETCH
instead of FETCH
Resolves with
A note about sequence ranges: This method does not stream the values, so using
*
as a range selector might be a really bad idea. If the mailbox contains thousands of messages and you are running a1:*
query, it might choke your application. Additionally, remember that*
stands for the sequence number of the last message in the mailbox. This means that if you have 10 messages in a mailbox and you run a query for a range of5000:*
you still get a match as the query is treated as10:5000
by the server
IMAP Commands: FETCH, CHANGEDSINCE
Example
client.listMessages('INBOX', '1:10', ['uid', 'flags', 'body[]']).then((messages) => {
messages.forEach((message) => console.log('Flags for ' + message.uid + ': ' + message.flags.join(', ')));
});
A listed message item includes (but is not limited to), the selected fields from the query
argument (all keys are lowercase). Additionally the argument order and even argument names might not match. For example, when requesting for body.peek
you get body
back instead. Additionally the message includes a special key #
which stands for the sequence number of the message.
Most arguments return strings (eg. body[]
) and numbers (eg. uid
) while flags
return an array, envelope
and bodystructure
return a processed object.
{
"#": 123,
"uid": 456,
"flags": ["\\Seen", "$MyFlag"],
"envelope": {
"date": "Fri, 13 Sep 2013 15:01:00 +0300",
"subject": "hello 4",
"from": [{"name": "sender name", "address": "sender@example.com"}],
"to": [{"name": "Receiver name", "address": "receiver@example.com"}],
"message-id": "<abcde>"
}
}
Special keys - if a special key is used, eg.
BODY.PEEK[HEADER (Date Subject)]
, the response key is lowercase and in the form how the server responded it, eg.body[header (date subject)]
An envelope includes the following fields (a value is only included in the response if it is set).
from
headersender
headerreply-to
headerto
headercc
headerbcc
headerAll address fields are in the following format:
[{
"name": "MIME decoded name",
"address": "email@address"
}]
A bodystructure object includes the following fields (all values are lowercase, unless the value might be case sensitive, eg. Content-Id value):
BODY[x.x.x]
, eg. '4.1.1' (this value is not set for the root object){border: 'abc'}
{filename: 'foo.gif'}
text/*
and message/rfc822
) is the count of lines in the bodymessage/rfc822
) is the envelope object of the sub-partmultipart/*
and message/rfc822
) is an array of embedded bodystructure objectsExample
Bodystructure for the following sample message structure:
multipart/mixed
text/plain
multipart/alternative
text/plain
{
"type": "multipart/mixed",
"childNodes": [
{
"part": "1",
"type": "text/plain",
"encoding": "7bit",
"size": 8,
"lineCount": 1
},
{
"part": "2",
"type": "multipart/alternative",
"childNodes": [
{
"part": "2.1",
"type": "text/plain",
"encoding": "7bit",
"size": 8,
"lineCount": 1
}
]
}
]
}
Search for messages with search(path, query[, options])
Where
true
executes UID SEARCH
instead of SEARCH
Resolves with
* **results** is an array of sorted and unique message sequence numbers or UID numbers that match the specified search query
Queries are composed as objects where keys are search terms and values are term arguments. Only strings, numbers and Date values are used as arguments. If the value is an array, the members of it are processed separately (use this for terms that require multiple params). If the value is a Date, it is converted to the form of '1-Jan-1970'. Subqueries (OR, NOT) are made up of objects.
Command: SEARCH
Examples:
// SEARCH UNSEEN
query = {unseen: true}
// SEARCH KEYWORD 'flagname'
query = {keyword: 'flagname'}
// SEARCH HEADER 'subject' 'hello world'
query = {header: ['subject', 'hello world']};
// SEARCH UNSEEN HEADER 'subject' 'hello world'
query = {unseen: true, header: ['subject', 'hello world']};
// SEARCH OR UNSEEN SEEN
query = {or: {unseen: true, seen: true}};
// SEARCH UNSEEN NOT SEEN
query = {unseen: true, not: {seen: true}}
// SINCE 2011-11-23
query = {since: new Date(2011, 11, 23, 0, 0, 0)}
client.search('INBOX', {unseen: true}, {byUid: true}).then((result) => {
result.forEach((uid) => console.log('Message ' + uid + ' is unread'));
});
Update message flags with setFlags(path, sequence, flags[, options])
. This is a wrapper around store()
Where
byUid
option is set to true). Example: '1', '1:*', '1,2:3,4' etc.true
executes UID SEARCH
instead of SEARCH
true
does not return anything. Useful when updating large range of messages at once ('1:*'
)Resolves with
* **messages** is an array of messages from the provided sequence range (or empty when `silent:true` option is set). Includes `flags` property and `uid` if `byUid:true` option was used.
You can check the flags for a message or a range of messages with listMessages
- use ['flags']
as the query object.
{ set: arrFlags }
for setting flags{ add: arrFlags }
for adding new flags{ remove: arrFlags }
for removing specified flagsWhere arrFlags
is an array containing flag strings, ie. ['\\Seen', '$MyFlag']
client.setFlags('INBOX', '1:10', {set: ['\\Seen']}).then((messages) => { ... })
client.setFlags('INBOX', '1:10', {remove: ['\\Seen']}).then((messages) => { ... })
client.setFlags('INBOX', '1:10', {add: ['\\Seen']}).then((messages) => { ... })
The client also allows direct access to the STORE command, but please use setFlags()
for convenience. Anyway, store flags or labels with store(path, sequence, action, flags[, options])
.
Where
byUid
option is set to true). Example: '1', '1:*', '1,2:3,4' etc.'FLAGS'
for setting flagstrue
executes UID SEARCH
instead of SEARCH
true
does not return anything. Useful when updating large range of messages at once ('1:*'
)Resolves with
silent:true
option is set). Includes flags
property and uid
if byUid:true
option was used.Possible actions
Command: STORE
client.store('INBOX', '1:*', '+X-GM-LABELS', ['\\Sent']).then((messages) => { ... }); // adds GMail `\Sent` label to messages
Delete messages with deleteMessages(path, sequence[, options])
Where
byUid
option is set to true). Example: '1', '1:*', '1,2:3,4' etc.true
uses UID values instead of sequence numbers to define the rangeResolves when IMAP server completed the command.
If possible (byUid:true
is set and UIDPLUS extension is supported by the server) uses UID EXPUNGE
otherwise falls back to EXPUNGE to delete the messages – which means that this method might be
destructive. If EXPUNGE
is used, then any messages with \Deleted
flag set are deleted even if these
messages are not included in the specified sequence range.
Commands: EXPUNGE, UID EXPUNGE
client.deleteMessages('INBOX', '1:5').then(() => { ... });
Copy messages with copyMessages(sequence, destination[, options])
Where
byUid
option is set to true). Example: '1', '1:*', '1,2:3,4' etc.true
uses UID values instead of sequence numbers to define the rangeResolves with an object which contains uid sets of source and destination uids if the server supports UIDPLUS.
Command: COPY
client.copyMessages('INBOX', '1:5', '[Gmail]/Trash').then(({srcSeqSet, destSeqSet}) => { ... });
Upload a message with upload(destination, message, [, options])
Where
Resolves with the new uid of the message in the destination folder if the server supports UIDPLUS.
Command: APPEND
client.upload('INBOX', message).then((uid) => { ... });
Move messages with moveMessages(path, sequence, destination[, options])
Where
byUid
option is set to true). Example: '1', '1:*', '1,2:3,4' etc.true
uses UID values instead of sequence numbers to define the rangeResolves when IMAP server completed the command.
If possible (MOVE extension is supported by the server) uses MOVE
or UID MOVE
otherwise falls back to COPY + EXPUNGE.
Command: MOVE
client.moveMessages('INBOX', '1:5', '[Gmail]/Trash').then(() => { ... });
It is recommended to set up some sort of local caching for the messages. Please note that IMAP relies on a mixture of mailbox-unique identifiers (UID) and sequence numbers, so a mapping between both is definitely recommended.
There are two kinds of updates: 1) When something happens in the currently selected mailbox, and 2) when you select a mailbox
Your IMAP server sends you updates when something happens in the mailbox you have currently selected. Message updates can be listened for by setting the onupdate
handler. First argument for the callback is the path, the second is the update type, and the third one is the new value.
Example
client.onupdate = function(path, type, value){
if (type === 'expunge') {
// untagged EXPUNGE response, e.g. "* EXPUNGE 123"
// value is the sequence number of the deleted message prior to deletion, so adapt your cache accordingly
} else if (type === 'exists') {
// untagged EXISTS response, e.g. "* EXISTS 123"
// value is new EXISTS message count in the selected mailbox
} else if (type === 'fetch') {
// untagged FETCH response, e.g. "* 123 FETCH (FLAGS (\Seen))"
// add a considerable amount of input tolerance here!
// probably some flag updates, a message or messages have been altered in some way
// UID is probably not listed, probably includes only the sequence number `#` and `flags` array
}
}
For your everyday tasks, this client doesn't really require you to explicitly select a mailbox, even though having an eye on which mailbox is selected is useful to receive untagged updates. When a mailbox is opened or closed, the onselectmailbox
and onclosemailbox
handlers are called.
For onselectmailbox
handler the first argument is the path of the selected mailbox and the second argument
is the mailbox information object (see selectMailbox).
For onclosemailbox
handler the argument is the path of the selected mailbox.
Example
client.onselectmailbox = function(path, mailbox){
console.log('Opened %s with %s messages', path, mailbox.exists);
}
client.onclosemailbox = function(path){
console.log('Closed %s', path);
}
The IMAP client has several events you can attach to by setting a listener
The invocation of onerror
indicates an irrecoverable error. When onerror
is fired, the connection is already closed, hence there's no need for further cleanup.
$ git clone git@github.com:emailjs/emailjs-imap-client.git
$ cd emailjs-imap-client
$ npm install
$ npm test
Copyright (c) 2014 Andris Reinman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.