mirror of
https://github.com/nodemailer/wildduck.git
synced 2024-09-20 07:16:05 +08:00
Initial preview
This commit is contained in:
commit
afd8abccc4
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
node_modules
|
||||
.DS_Store
|
||||
npm-debug.log
|
||||
.npmrc
|
27
Gruntfile.js
Normal file
27
Gruntfile.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = function (grunt) {
|
||||
|
||||
// Project configuration.
|
||||
grunt.initConfig({
|
||||
eslint: {
|
||||
all: ['lib/**/*.js', 'imap-core/**/*.js', 'test/**/*.js', 'examples/**/*.js', 'Gruntfile.js']
|
||||
},
|
||||
|
||||
mochaTest: {
|
||||
all: {
|
||||
options: {
|
||||
reporter: 'spec'
|
||||
},
|
||||
src: ['test/**/*-test.js', 'imap-core/test/**/*-test.js']
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Load the plugin(s)
|
||||
grunt.loadNpmTasks('grunt-eslint');
|
||||
grunt.loadNpmTasks('grunt-mocha-test');
|
||||
|
||||
// Tasks
|
||||
grunt.registerTask('default', ['eslint', 'mochaTest']);
|
||||
};
|
298
LICENSE
Normal file
298
LICENSE
Normal file
|
@ -0,0 +1,298 @@
|
|||
Copyright (c) 2017 Andris Reinman
|
||||
|
||||
European Union Public Licence
|
||||
V. 1.1
|
||||
|
||||
EUPL (c) the European Community 2007
|
||||
|
||||
|
||||
This European Union Public Licence (the "EUPL") applies to the Work or Software
|
||||
(as defined below) which is provided under the terms of this Licence. Any use
|
||||
of the Work, other than as authorised under this Licence is prohibited (to the
|
||||
extent such use is covered by a right of the copyright holder of the Work).
|
||||
|
||||
The Original Work is provided under the terms of this Licence when the Licensor
|
||||
(as defined below) has placed the following notice immediately following the
|
||||
copyright notice for the Original Work:
|
||||
|
||||
Licensed under the EUPL V.1.1
|
||||
|
||||
or has expressed by any other mean his willingness to license under the EUPL.
|
||||
|
||||
|
||||
1. Definitions
|
||||
|
||||
In this Licence, the following terms have the following meaning:
|
||||
|
||||
* The Licence: this Licence.
|
||||
|
||||
* The Original Work or the Software: the software distributed and/or
|
||||
communicated by the Licensor under this Licence, available as Source Code
|
||||
and also as Executable Code as the case may be.
|
||||
|
||||
* Derivative Works: the works or software that could be created by the
|
||||
Licensee, based upon the Original Work or modifications thereof. This
|
||||
Licence does not define the extent of modification or dependence on the
|
||||
Original Work required in order to classify a work as a Derivative Work;
|
||||
this extent is determined by copyright law applicable in the country
|
||||
mentioned in Article 15.
|
||||
|
||||
* The Work: the Original Work and/or its Derivative Works.
|
||||
|
||||
* The Source Code: the human-readable form of the Work which is the most
|
||||
convenient for people to study and modify.
|
||||
|
||||
* The Executable Code: any code which has generally been compiled and which is
|
||||
meant to be interpreted by a computer as a program.
|
||||
|
||||
* The Licensor: the natural or legal person that distributes and/or
|
||||
communicates the Work under the Licence.
|
||||
|
||||
* Contributor(s): any natural or legal person who modifies the Work under the
|
||||
Licence, or otherwise contributes to the creation of a Derivative Work.
|
||||
|
||||
* The Licensee or "You": any natural or legal person who makes any usage of
|
||||
the Software under the terms of the Licence.
|
||||
|
||||
* Distribution and/or Communication: any act of selling, giving, lending,
|
||||
renting, distributing, communicating, transmitting, or otherwise making
|
||||
available, on-line or off-line, copies of the Work or providing access to
|
||||
its essential functionalities at the disposal of any other natural or legal
|
||||
person.
|
||||
|
||||
|
||||
2. Scope of the rights granted by the Licence
|
||||
|
||||
The Licensor hereby grants You a world-wide, royalty-free, non-exclusive,
|
||||
sublicensable licence to do the following, for the duration of copyright vested
|
||||
in the Original Work:
|
||||
|
||||
* use the Work in any circumstance and for all usage,
|
||||
* reproduce the Work,
|
||||
* modify the Original Work, and make Derivative Works based upon the Work,
|
||||
* communicate to the public, including the right to make available or display
|
||||
the Work or copies thereof to the public and perform publicly, as the case
|
||||
may be, the Work,
|
||||
* distribute the Work or copies thereof,
|
||||
* lend and rent the Work or copies thereof,
|
||||
* sub-license rights in the Work or copies thereof.
|
||||
|
||||
Those rights can be exercised on any media, supports and formats, whether now
|
||||
known or later invented, as far as the applicable law permits so.
|
||||
|
||||
In the countries where moral rights apply, the Licensor waives his right to
|
||||
exercise his moral right to the extent allowed by law in order to make
|
||||
effective the licence of the economic rights here above listed.
|
||||
|
||||
The Licensor grants to the Licensee royalty-free, non exclusive usage rights to
|
||||
any patents held by the Licensor, to the extent necessary to make use of the
|
||||
rights granted on the Work under this Licence.
|
||||
|
||||
|
||||
3. Communication of the Source Code
|
||||
|
||||
The Licensor may provide the Work either in its Source Code form, or as
|
||||
Executable Code. If the Work is provided as Executable Code, the Licensor
|
||||
provides in addition a machine-readable copy of the Source Code of the Work
|
||||
along with each copy of the Work that the Licensor distributes or indicates, in
|
||||
a notice following the copyright notice attached to the Work, a repository
|
||||
where the Source Code is easily and freely accessible for as long as the
|
||||
Licensor continues to distribute and/or communicate the Work.
|
||||
|
||||
|
||||
4. Limitations on copyright
|
||||
|
||||
Nothing in this Licence is intended to deprive the Licensee of the benefits
|
||||
from any exception or limitation to the exclusive rights of the rights owners
|
||||
in the Original Work or Software, of the exhaustion of those rights or of other
|
||||
applicable limitations thereto.
|
||||
|
||||
|
||||
5. Obligations of the Licensee
|
||||
|
||||
The grant of the rights mentioned above is subject to some restrictions and
|
||||
obligations imposed on the Licensee. Those obligations are the following:
|
||||
|
||||
- Attribution right: the Licensee shall keep intact all copyright, patent or
|
||||
trademarks notices and all notices that refer to the Licence and to the
|
||||
disclaimer of warranties. The Licensee must include a copy of such notices
|
||||
and a copy of the Licence with every copy of the Work he/she distributes
|
||||
and/or communicates. The Licensee must cause any Derivative Work to carry
|
||||
prominent notices stating that the Work has been modified and the date of
|
||||
modification.
|
||||
|
||||
- Copyleft clause: If the Licensee distributes and/or communicates copies of
|
||||
the Original Works or Derivative Works based upon the Original Work, this
|
||||
Distribution and/or Communication will be done under the terms of this
|
||||
Licence or of a later version of this Licence unless the Original Work is
|
||||
expressly distributed only under this version of the Licence. The Licensee
|
||||
(becoming Licensor) cannot offer or impose any additional terms or
|
||||
conditions on the Work or Derivative Work that alter or restrict the terms
|
||||
of the Licence.
|
||||
|
||||
- Compatibility clause: If the Licensee Distributes and/or Communicates
|
||||
Derivative Works or copies thereof based upon both the Original Work and
|
||||
another work licensed under a Compatible Licence, this Distribution and/or
|
||||
Communication can be done under the terms of this Compatible Licence. For
|
||||
the sake of this clause, "Compatible Licence" refers to the licences listed
|
||||
in the appendix attached to this Licence. Should the Licensee's obligations
|
||||
under the Compatible Licence conflict with his/her obligations under this
|
||||
Licence, the obligations of the Compatible Licence shall prevail.
|
||||
|
||||
- Provision of Source Code: When distributing and/or communicating copies of
|
||||
the Work, the Licensee will provide a machine-readable copy of the Source
|
||||
Code or indicate a repository where this Source will be easily and freely
|
||||
available for as long as the Licensee continues to distribute and/or
|
||||
communicate the Work. Legal Protection: This Licence does not grant
|
||||
permission to use the trade names, trademarks, service marks, or names of
|
||||
the Licensor, except as required for reasonable and customary use in
|
||||
describing the origin of the Work and reproducing the content of the
|
||||
copyright notice.
|
||||
|
||||
|
||||
6. Chain of Authorship
|
||||
|
||||
The original Licensor warrants that the copyright in the Original Work granted
|
||||
hereunder is owned by him/her or licensed to him/her and that he/she has the
|
||||
power and authority to grant the Licence.
|
||||
|
||||
Each Contributor warrants that the copyright in the modifications he/she brings
|
||||
to the Work are owned by him/her or licensed to him/her and that he/she has the
|
||||
power and authority to grant the Licence.
|
||||
|
||||
Each time You accept the Licence, the original Licensor and subsequent
|
||||
Contributors grant You a licence to their contributions to the Work, under the
|
||||
terms of this Licence.
|
||||
|
||||
|
||||
7. Disclaimer of Warranty
|
||||
|
||||
The Work is a work in progress, which is continuously improved by numerous
|
||||
contributors. It is not a finished work and may therefore contain defects or
|
||||
"bugs" inherent to this type of software development.
|
||||
|
||||
For the above reason, the Work is provided under the Licence on an "as is"
|
||||
basis and without warranties of any kind concerning the Work, including without
|
||||
limitation merchantability, fitness for a particular purpose, absence of
|
||||
defects or errors, accuracy, non-infringement of intellectual property rights
|
||||
other than copyright as stated in Article 6 of this Licence.
|
||||
|
||||
This disclaimer of warranty is an essential part of the Licence and a condition
|
||||
for the grant of any rights to the Work.
|
||||
|
||||
|
||||
8. Disclaimer of Liability
|
||||
|
||||
Except in the cases of wilful misconduct or damages directly caused to natural
|
||||
persons, the Licensor will in no event be liable for any direct or indirect,
|
||||
material or moral, damages of any kind, arising out of the Licence or of the
|
||||
use of the Work, including without limitation, damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, loss of data or any commercial
|
||||
damage, even if the Licensor has been advised of the possibility of such
|
||||
damage. However, the Licensor will be liable under statutory product liability
|
||||
laws as far such laws apply to the Work.
|
||||
|
||||
|
||||
9. Additional agreements
|
||||
|
||||
While distributing the Original Work or Derivative Works, You may choose to
|
||||
conclude an additional agreement to offer, and charge a fee for, acceptance of
|
||||
support, warranty, indemnity, or other liability obligations and/or services
|
||||
consistent with this Licence. However, in accepting such obligations, You may
|
||||
act only on your own behalf and on your sole responsibility, not on behalf of
|
||||
the original Licensor or any other Contributor, and only if You agree to
|
||||
indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against such Contributor by the fact You have
|
||||
accepted any such warranty or additional liability.
|
||||
|
||||
|
||||
10. Acceptance of the Licence
|
||||
|
||||
The provisions of this Licence can be accepted by clicking on an icon "I agree"
|
||||
placed under the bottom of a window displaying the text of this Licence or by
|
||||
affirming consent in any other similar way, in accordance with the rules of
|
||||
applicable law. Clicking on that icon indicates your clear and irrevocable
|
||||
acceptance of this Licence and all of its terms and conditions.
|
||||
|
||||
Similarly, you irrevocably accept this Licence and all of its terms and
|
||||
conditions by exercising any rights granted to You by Article 2 of this
|
||||
Licence, such as the use of the Work, the creation by You of a Derivative Work
|
||||
or the Distribution and/or Communication by You of the Work or copies thereof.
|
||||
|
||||
|
||||
11. Information to the public
|
||||
|
||||
In case of any Distribution and/or Communication of the Work by means of
|
||||
electronic communication by You (for example, by offering to download the Work
|
||||
from a remote location) the distribution channel or media (for example, a
|
||||
website) must at least provide to the public the information requested by the
|
||||
applicable law regarding the Licensor, the Licence and the way it may be
|
||||
accessible, concluded, stored and reproduced by the Licensee.
|
||||
|
||||
|
||||
12. Termination of the Licence
|
||||
|
||||
The Licence and the rights granted hereunder will terminate automatically upon
|
||||
any breach by the Licensee of the terms of the Licence.
|
||||
|
||||
Such a termination will not terminate the licences of any person who has
|
||||
received the Work from the Licensee under the Licence, provided such persons
|
||||
remain in full compliance with the Licence.
|
||||
|
||||
|
||||
13. Miscellaneous
|
||||
|
||||
Without prejudice of Article 9 above, the Licence represents the complete
|
||||
agreement between the Parties as to the Work licensed hereunder.
|
||||
|
||||
If any provision of the Licence is invalid or unenforceable under applicable
|
||||
law, this will not affect the validity or enforceability of the Licence as a
|
||||
whole. Such provision will be construed and/or reformed so as necessary to make
|
||||
it valid and enforceable.
|
||||
|
||||
The European Commission may publish other linguistic versions and/or new
|
||||
versions of this Licence, so far this is required and reasonable, without
|
||||
reducing the scope of the rights granted by the Licence. New versions of the
|
||||
Licence will be published with a unique version number.
|
||||
|
||||
All linguistic versions of this Licence, approved by the European Commission,
|
||||
have identical value. Parties can take advantage of the linguistic version of
|
||||
their choice.
|
||||
|
||||
|
||||
14. Jurisdiction
|
||||
|
||||
Any litigation resulting from the interpretation of this License, arising
|
||||
between the European Commission, as a Licensor, and any Licensee, will be
|
||||
subject to the jurisdiction of the Court of Justice of the European
|
||||
Communities, as laid down in article 238 of the Treaty establishing the
|
||||
European Community.
|
||||
|
||||
Any litigation arising between Parties, other than the European Commission, and
|
||||
resulting from the interpretation of this License, will be subject to the
|
||||
exclusive jurisdiction of the competent court where the Licensor resides or
|
||||
conducts its primary business.
|
||||
|
||||
|
||||
15. Applicable Law
|
||||
|
||||
This Licence shall be governed by the law of the European Union country where
|
||||
the Licensor resides or has his registered office.
|
||||
|
||||
This licence shall be governed by the Belgian law if:
|
||||
|
||||
* a litigation arises between the European Commission, as a Licensor, and any
|
||||
Licensee;
|
||||
* the Licensor, other than the European Commission, has no residence or
|
||||
registered office inside a European Union country.
|
||||
|
||||
|
||||
Appendix
|
||||
|
||||
"Compatible Licences" according to article 5 EUPL are:
|
||||
|
||||
* GNU General Public License (GNU GPL) v. 2
|
||||
* Open Software License (OSL) v. 2.1, v. 3.0
|
||||
* Common Public License v. 1.0
|
||||
* Eclipse Public License v. 1.0
|
||||
* Cecill v. 2.0
|
24
README.md
Normal file
24
README.md
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Wild Duck Mail Agent
|
||||
|
||||
This is a very early preview of an IMAP server built with Node.js and MongoDB.
|
||||
|
||||
### Goals of the Project
|
||||
|
||||
1. Build a scalable IMAP server that uses clustered database instead of single machine file system as mail store
|
||||
2. Push notifications. Your application (eg. a webmail client) should be able to request changes (new and deleted messages, flag changes) to be pushed to client instead of using IMAP to fetch stuff from the server
|
||||
|
||||
## Usage
|
||||
|
||||
Install dependencies
|
||||
|
||||
npm install --production
|
||||
|
||||
Modify [config file](./config/default.js)
|
||||
|
||||
Run the server
|
||||
|
||||
npm start
|
||||
|
||||
## License
|
||||
|
||||
Wild Duck Mail Agent is licensed under the [European Union Public License 1.1](http://ec.europa.eu/idabc/eupl.html).
|
21
config/default.js
Normal file
21
config/default.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
log: {
|
||||
level: 'silly'
|
||||
},
|
||||
|
||||
imap: {
|
||||
port: 9993,
|
||||
host: '127.0.0.1',
|
||||
maxUnflaggedMessages: 10
|
||||
},
|
||||
|
||||
mongo: 'mongodb://127.0.0.1:27017/wildduck',
|
||||
|
||||
mx: {
|
||||
port: 2525,
|
||||
host: '0.0.0.0',
|
||||
maxMB: 2
|
||||
}
|
||||
};
|
98
imap-core/README.md
Normal file
98
imap-core/README.md
Normal file
|
@ -0,0 +1,98 @@
|
|||
# imap-core
|
||||
|
||||
Node.js module to create custom IMAP servers.
|
||||
|
||||
This is something I have used mostly for client work as the lower level dependency for some specific applications that serve IMAP. I don't have any such clients right no so I published the code if anyone finds it useful. I removed all proprietary code developed for clients, the module is only about the lower level protocol usage and does not contain any actual server logic.
|
||||
|
||||
You can see an example implementation of an IMAP server from the [example script](examples/index.js). Most of the code is inherited from the Hoodiecrow test-IMAP server module but this module can be used for asynchronous data access while in Hoodiecrow everything was synchronous (storage was an in-memory object that was accessed and updated synchronously).
|
||||
|
||||
## Demo
|
||||
|
||||
Install dependencies
|
||||
|
||||
npm install
|
||||
|
||||
Run the example
|
||||
|
||||
node examples/index.js
|
||||
|
||||
Connect to the server on port 9993
|
||||
|
||||
openssl s_client -crlf -connect localhost:9993
|
||||
|
||||
Once connected use testuser:pass to log in
|
||||
|
||||
< * OK test ready
|
||||
> A LOGIN testuser pass
|
||||
< A OK testuser authenticated
|
||||
|
||||
## IMAP extension support
|
||||
|
||||
This project is going to support only selected extensions, that are minimally required.
|
||||
|
||||
## Sequence number handling
|
||||
|
||||
Sequence numbers are handled automatically, no need to do this in the application side – you only need to keep count of the incrementing UID's. All sequence number based operations are converted to use UID values instead.
|
||||
|
||||
## Handling large input
|
||||
|
||||
Unfortunately input handling for a single command is not stream based, so everything sent to the server is loaded into memory before being processed. Literal size can be limited though and in this case the server refuses to process literals bigger than configured size.
|
||||
|
||||
## SEARCH query
|
||||
|
||||
Search query is provided as a tree structure.
|
||||
|
||||
Possible SEARCH terms
|
||||
|
||||
- Array – a list of AND terms
|
||||
- **or** - in the form of `{key: 'or', value: [terms]}` where _terms_ is a list of OR terms
|
||||
- **not** - inverts another term. In the form of `{key: 'not', value: term}` where _term_ is the term that must be inverted
|
||||
- **flag** - describes a flag term. In the form of `{key: 'flag', value: 'term', exists: bool}` where _term_ is the flag name to look for and _bool_ indicates if the flag must be bresent (_true_) or missing (_false_)
|
||||
- **header** - describes a header value. Header key is a case insensitive exact match (eg. 'X-Foo' matches header 'X-Foo:' but not 'X-Fooz:'). Header value is a partial match. In the form of `{key: 'header', header: 'keyterm', value: 'valueterm'}` where _keyterm_ is the header key name and _valueterm_ is the value of the header. If value is empty then the query acts as boolean, if header key is present, then it matches, otherwise it does not match
|
||||
- **uid** - is a an array of UID values (numbers)
|
||||
- **all** - if present then indicates that all messages should match
|
||||
- **internaldate** - operates on the date the message was received. Date value is day based, so timezone and time should be discarded. In the form of `{key: 'internaldate', operator: 'op', value: 'val'}` where _op_ is one of '<', '=', '>=' and _val_ is a date string
|
||||
- **date** - operates on the date listed in the massage _Date:_ header. Date value is day based, so timezone and time should be discarded. In the form of `{key: 'date', operator: 'op', value: 'val'}` where _op_ is one of '<', '=', '>=' and _val_ is a date string
|
||||
- **body** - looks for a partial match in the message BODY (does not match header fields). In the form of `{key: 'body', value: 'term'}` where _term_ is the partial match to look for
|
||||
- **text** - looks for a partial match in the entire message, including the body and headers. In the form of `{key: 'text', value: 'term'}` where _term_ is the partial match to look for
|
||||
- **size** - matches message size. In the form of `{key: 'size', value: num, operator: 'op'}` where _op_ is one of '<', '=', '>' and _num_ is the size of the message
|
||||
- **charset** - sets the charset to be used in the text fields. Can be ignored as everything should be UTF-8 by default
|
||||
|
||||
## Currently implemented RFC3501 commands
|
||||
|
||||
- **APPEND**
|
||||
- **CAPABILITY**
|
||||
- **CHECK**
|
||||
- **CLOSE**
|
||||
- **COPY**
|
||||
- **CREATE**
|
||||
- **DELETE**
|
||||
- **EXPUNGE**
|
||||
- **FETCH**
|
||||
- **LIST**
|
||||
- **LOGIN**
|
||||
- **LOGOUT**
|
||||
- **LSUB**
|
||||
- **NOOP**
|
||||
- **RENAME**
|
||||
- **SEARCH**
|
||||
- **SELECT**
|
||||
- **STARTTLS**
|
||||
- **STATUS**
|
||||
- **STORE**
|
||||
- **SUBSCRIBE**
|
||||
- **UID COPY**
|
||||
- **UID STORE**
|
||||
- **UNSUBSCRIBE**
|
||||
|
||||
Extensions
|
||||
|
||||
- **Conditional STORE** rfc4551 and **ENABLE**
|
||||
- **Special-Use Mailboxes** rfc6154
|
||||
- **ID extension** rfc2971
|
||||
- **IDLE command** rfc2177
|
||||
- **NAMESPACE** rfc2342 (hard coded single user namespace)
|
||||
- **UNSELECT** rfc3691
|
||||
- **AUTHENTICATE PLAIN** and **SASL-IR**
|
||||
|
||||
Unlike the Hoodiecrow project you can not enable or disable extensions, everything is as it is.
|
543
imap-core/examples/index.js
Normal file
543
imap-core/examples/index.js
Normal file
|
@ -0,0 +1,543 @@
|
|||
'use strict';
|
||||
|
||||
// Replace '../index' with 'imap-core' when running this script outside this directory
|
||||
|
||||
let IMAPServerModule = require('../index');
|
||||
let IMAPServer = IMAPServerModule.IMAPServer;
|
||||
let MemoryNotifier = IMAPServerModule.MemoryNotifier;
|
||||
|
||||
const SERVER_PORT = 9993;
|
||||
const SERVER_HOST = '127.0.0.1';
|
||||
|
||||
// Connect to this example server by running
|
||||
// openssl s_client -crlf -connect localhost:9993
|
||||
// Username is "testuser" and password is "pass"
|
||||
|
||||
// This example uses global folders and subscriptions
|
||||
let folders = new Map();
|
||||
let subscriptions = new WeakSet();
|
||||
|
||||
// configure initial mailbox state
|
||||
[
|
||||
// INBOX
|
||||
{
|
||||
path: 'INBOX',
|
||||
uidValidity: 123,
|
||||
uidNext: 70,
|
||||
modifyIndex: 6,
|
||||
messages: [{
|
||||
uid: 45,
|
||||
flags: [],
|
||||
date: new Date(),
|
||||
modseq: 1,
|
||||
raw: Buffer.from('from: sender@example.com\r\nto: to@example.com\r\ncc: cc@example.com\r\nsubject: test')
|
||||
}, {
|
||||
uid: 49,
|
||||
flags: ['\\Seen'],
|
||||
date: new Date(),
|
||||
modseq: 2
|
||||
}, {
|
||||
uid: 50,
|
||||
flags: ['\\Seen'],
|
||||
date: new Date(),
|
||||
modseq: 3
|
||||
}, {
|
||||
uid: 52,
|
||||
flags: [],
|
||||
date: new Date(),
|
||||
modseq: 4
|
||||
}, {
|
||||
uid: 53,
|
||||
flags: [],
|
||||
date: new Date(),
|
||||
modseq: 5
|
||||
}, {
|
||||
uid: 60,
|
||||
flags: [],
|
||||
date: new Date(),
|
||||
modseq: 6
|
||||
}],
|
||||
journal: []
|
||||
},
|
||||
// [Gmail]/Sent Mail
|
||||
{
|
||||
path: '[Gmail]/Sent Mail',
|
||||
specialUse: '\\Sent',
|
||||
uidValidity: 123,
|
||||
uidNext: 90,
|
||||
modifyIndex: 0,
|
||||
messages: [],
|
||||
journal: []
|
||||
}
|
||||
].forEach(folder => {
|
||||
folders.set(folder.path, folder);
|
||||
subscriptions.add(folder);
|
||||
});
|
||||
|
||||
// Setup server
|
||||
let server = new IMAPServer({
|
||||
secure: true,
|
||||
id: {
|
||||
name: 'test'
|
||||
}
|
||||
});
|
||||
|
||||
// setup notification system for updates
|
||||
server.notifier = new MemoryNotifier({
|
||||
folders
|
||||
});
|
||||
|
||||
server.onAuth = function (login, session, callback) {
|
||||
if (login.username !== 'testuser' || login.password !== 'pass') {
|
||||
return callback();
|
||||
}
|
||||
|
||||
callback(null, {
|
||||
user: {
|
||||
username: login.username
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// LIST "" "*"
|
||||
// Returns all folders, query is informational
|
||||
// folders is either an Array or a Map
|
||||
server.onList = function (query, session, callback) {
|
||||
this.logger.debug('[%s] LIST for "%s"', session.id, query);
|
||||
|
||||
callback(null, folders);
|
||||
};
|
||||
|
||||
// LSUB "" "*"
|
||||
// Returns all subscribed folders, query is informational
|
||||
// folders is either an Array or a Map
|
||||
server.onLsub = function (query, session, callback) {
|
||||
this.logger.debug('[%s] LSUB for "%s"', session.id, query);
|
||||
|
||||
let subscribed = [];
|
||||
folders.forEach(folder => {
|
||||
if (subscriptions.has(folder)) {
|
||||
subscribed.push(folder);
|
||||
}
|
||||
});
|
||||
|
||||
callback(null, subscribed);
|
||||
};
|
||||
|
||||
// SUBSCRIBE "path/to/mailbox"
|
||||
server.onSubscribe = function (mailbox, session, callback) {
|
||||
this.logger.debug('[%s] SUBSCRIBE to "%s"', session.id, mailbox);
|
||||
|
||||
if (!folders.has(mailbox)) {
|
||||
return callback(null, 'NONEXISTENT');
|
||||
}
|
||||
|
||||
subscriptions.add(folders.get(mailbox));
|
||||
callback(null, true);
|
||||
};
|
||||
|
||||
// UNSUBSCRIBE "path/to/mailbox"
|
||||
server.onUnsubscribe = function (mailbox, session, callback) {
|
||||
this.logger.debug('[%s] UNSUBSCRIBE from "%s"', session.id, mailbox);
|
||||
|
||||
if (!folders.has(mailbox)) {
|
||||
return callback(null, 'NONEXISTENT');
|
||||
}
|
||||
|
||||
subscriptions.delete(folders.get(mailbox));
|
||||
callback(null, true);
|
||||
};
|
||||
|
||||
// CREATE "path/to/mailbox"
|
||||
server.onCreate = function (mailbox, session, callback) {
|
||||
this.logger.debug('[%s] CREATE "%s"', session.id, mailbox);
|
||||
|
||||
if (folders.has(mailbox)) {
|
||||
return callback(null, 'ALREADYEXISTS');
|
||||
}
|
||||
|
||||
folders.set(mailbox, {
|
||||
path: mailbox,
|
||||
uidValidity: Date.now(),
|
||||
uidNext: 1,
|
||||
modifyIndex: 0,
|
||||
messages: [],
|
||||
journal: []
|
||||
});
|
||||
|
||||
subscriptions.add(folders.get(mailbox));
|
||||
callback(null, true);
|
||||
};
|
||||
|
||||
// RENAME "path/to/mailbox" "new/path"
|
||||
// NB! RENAME affects child and hierarchy mailboxes as well, this example does not do this
|
||||
server.onRename = function (mailbox, newname, session, callback) {
|
||||
this.logger.debug('[%s] RENAME "%s" to "%s"', session.id, mailbox, newname);
|
||||
|
||||
if (!folders.has(mailbox)) {
|
||||
return callback(null, 'NONEXISTENT');
|
||||
}
|
||||
|
||||
if (folders.has(newname)) {
|
||||
return callback(null, 'ALREADYEXISTS');
|
||||
}
|
||||
|
||||
let oldMailbox = folders.get(mailbox);
|
||||
folders.delete(mailbox);
|
||||
|
||||
oldMailbox.path = newname;
|
||||
folders.set(newname, oldMailbox);
|
||||
|
||||
callback(null, true);
|
||||
};
|
||||
|
||||
// DELETE "path/to/mailbox"
|
||||
server.onDelete = function (mailbox, session, callback) {
|
||||
this.logger.debug('[%s] DELETE "%s"', session.id, mailbox);
|
||||
|
||||
if (!folders.has(mailbox)) {
|
||||
return callback(null, 'NONEXISTENT');
|
||||
}
|
||||
|
||||
// keep SPECIAL-USE folders
|
||||
if (folders.get(mailbox).specialUse) {
|
||||
return callback(null, 'CANNOT');
|
||||
}
|
||||
|
||||
folders.delete(mailbox);
|
||||
callback(null, true);
|
||||
};
|
||||
|
||||
// SELECT/EXAMINE
|
||||
server.onOpen = function (mailbox, session, callback) {
|
||||
this.logger.debug('[%s] Opening "%s"', session.id, mailbox);
|
||||
|
||||
if (!folders.has(mailbox)) {
|
||||
return callback(null, 'NONEXISTENT');
|
||||
}
|
||||
|
||||
let folder = folders.get(mailbox);
|
||||
|
||||
return callback(null, {
|
||||
specialUse: folder.specialUse,
|
||||
uidValidity: folder.uidValidity,
|
||||
uidNext: folder.uidNext,
|
||||
modifyIndex: folder.modifyIndex,
|
||||
uidList: folder.messages.map(message => message.uid)
|
||||
});
|
||||
};
|
||||
|
||||
// STATUS (X Y X)
|
||||
server.onStatus = function (mailbox, session, callback) {
|
||||
this.logger.debug('[%s] Requested status for "%s"', session.id, mailbox);
|
||||
|
||||
if (!folders.has(mailbox)) {
|
||||
return callback(null, 'NONEXISTENT');
|
||||
}
|
||||
|
||||
let folder = folders.get(mailbox);
|
||||
|
||||
return callback(null, {
|
||||
messages: folder.messages.length,
|
||||
uidNext: folder.uidNext,
|
||||
uidValidity: folder.uidValidity,
|
||||
unseen: folder.messages.filter(message => message.flags.indexOf('\\Seen') < 0).length
|
||||
});
|
||||
};
|
||||
|
||||
// APPEND mailbox (flags) date message
|
||||
server.onAppend = function (mailbox, flags, date, raw, session, callback) {
|
||||
this.logger.debug('[%s] Appending message to "%s"', session.id, mailbox);
|
||||
|
||||
if (!folders.has(mailbox)) {
|
||||
return callback(null, 'TRYCREATE');
|
||||
}
|
||||
|
||||
date = date && new Date(date) || new Date();
|
||||
|
||||
let folder = folders.get(mailbox);
|
||||
let message = {
|
||||
uid: folder.uidNext++,
|
||||
date: date && new Date(date) || new Date(),
|
||||
raw,
|
||||
flags
|
||||
};
|
||||
|
||||
folder.messages.push(message);
|
||||
|
||||
// do not write directly to stream, use notifications as the currently selected mailbox might not be the one that receives the message
|
||||
this.notifier.addEntries(session.user.username, mailbox, {
|
||||
command: 'EXISTS',
|
||||
uid: message.uid
|
||||
}, () => {
|
||||
this.notifier.fire(session.user.username, mailbox);
|
||||
|
||||
return callback(null, true, {
|
||||
uidValidity: folder.uidValidity,
|
||||
uid: message.uid
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// STORE / UID STORE, updates flags for selected UIDs
|
||||
server.onUpdate = function (mailbox, update, session, callback) {
|
||||
this.logger.debug('[%s] Updating messages in "%s"', session.id, mailbox);
|
||||
|
||||
if (!folders.has(mailbox)) {
|
||||
return callback(null, 'NONEXISTENT');
|
||||
}
|
||||
|
||||
let folder = folders.get(mailbox);
|
||||
let i = 0;
|
||||
|
||||
let processMessages = () => {
|
||||
if (i >= folder.messages.length) {
|
||||
this.notifier.fire(session.user.username, mailbox);
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
let message = folder.messages[i++];
|
||||
let updated = false;
|
||||
|
||||
if (update.messages.indexOf(message.uid) < 0) {
|
||||
return processMessages();
|
||||
}
|
||||
|
||||
switch (update.action) {
|
||||
case 'set':
|
||||
// check if update set matches current or is different
|
||||
if (message.flags.length !== update.value.length || update.value.filter(flag => message.flags.indexOf(flag) < 0).length) {
|
||||
updated = true;
|
||||
}
|
||||
// set flags
|
||||
message.flags = [].concat(update.value);
|
||||
break;
|
||||
|
||||
case 'add':
|
||||
message.flags = message.flags.concat(update.value.filter(flag => {
|
||||
if (message.flags.indexOf(flag) < 0) {
|
||||
updated = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}));
|
||||
break;
|
||||
|
||||
case 'remove':
|
||||
message.flags = message.flags.filter(flag => {
|
||||
if (update.value.indexOf(flag) < 0) {
|
||||
return true;
|
||||
}
|
||||
updated = true;
|
||||
return false;
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// Onlsy show response if not silent
|
||||
if (!update.silent) {
|
||||
session.writeStream.write(session.formatResponse('FETCH', message.uid, {
|
||||
uid: update.isUid ? message.uid : false,
|
||||
flags: message.flags
|
||||
}));
|
||||
}
|
||||
|
||||
// notifiy other clients only if something changed
|
||||
if (updated) {
|
||||
this.notifier.addEntries(session.user.username, mailbox, {
|
||||
command: 'FETCH',
|
||||
ignore: session.id,
|
||||
uid: message.uid,
|
||||
flags: message.flags
|
||||
}, processMessages);
|
||||
} else {
|
||||
processMessages();
|
||||
}
|
||||
};
|
||||
|
||||
processMessages();
|
||||
};
|
||||
|
||||
// EXPUNGE deletes all messages in selected mailbox marked with \Delete
|
||||
// EXPUNGE deletes all messages in selected mailbox marked with \Delete
|
||||
server.onExpunge = function (mailbox, update, session, callback) {
|
||||
this.logger.debug('[%s] Deleting messages from "%s"', session.id, mailbox);
|
||||
|
||||
if (!folders.has(mailbox)) {
|
||||
return callback(null, 'NONEXISTENT');
|
||||
}
|
||||
|
||||
let folder = folders.get(mailbox);
|
||||
let deleted = [];
|
||||
let i, len;
|
||||
|
||||
for (i = folder.messages.length - 1; i >= 0; i--) {
|
||||
if (
|
||||
(
|
||||
(update.isUid && update.messages.indexOf(folder.messages[i].uid) >= 0) ||
|
||||
!update.isUid
|
||||
) && folder.messages[i].flags.indexOf('\\Deleted') >= 0) {
|
||||
|
||||
deleted.unshift(folder.messages[i].uid);
|
||||
folder.messages.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
let entries = [];
|
||||
for (i = 0, len = deleted.length; i < len; i++) {
|
||||
entries.push({
|
||||
command: 'EXPUNGE',
|
||||
ignore: session.id,
|
||||
uid: deleted[i]
|
||||
});
|
||||
if (!update.silent) {
|
||||
session.writeStream.write(session.formatResponse('EXPUNGE', deleted[i]));
|
||||
}
|
||||
}
|
||||
|
||||
this.notifier.addEntries(session.user.username, mailbox, entries, () => {
|
||||
this.notifier.fire(session.user.username, mailbox);
|
||||
return callback(null, true);
|
||||
});
|
||||
};
|
||||
|
||||
// COPY / UID COPY sequence mailbox
|
||||
server.onCopy = function (mailbox, update, session, callback) {
|
||||
this.logger.debug('[%s] Copying messages from "%s" to "%s"', session.id, mailbox, update.destination);
|
||||
|
||||
if (!folders.has(mailbox)) {
|
||||
return callback(null, 'NONEXISTENT');
|
||||
}
|
||||
|
||||
if (!folders.has(update.destination)) {
|
||||
return callback(null, 'TRYCREATE');
|
||||
}
|
||||
|
||||
let sourceFolder = folders.get(mailbox);
|
||||
let destinationFolder = folders.get(update.destination);
|
||||
|
||||
let messages = [];
|
||||
let sourceUid = [];
|
||||
let destinationUid = [];
|
||||
let i, len;
|
||||
let entries = [];
|
||||
|
||||
for (i = sourceFolder.messages.length - 1; i >= 0; i--) {
|
||||
if (update.messages.indexOf(sourceFolder.messages[i].uid) >= 0) {
|
||||
messages.unshift(JSON.parse(JSON.stringify(sourceFolder.messages[i])));
|
||||
sourceUid.unshift(sourceFolder.messages[i].uid);
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0, len = messages.length; i < len; i++) {
|
||||
messages[i].uid = destinationFolder.uidNext++;
|
||||
destinationUid.push(messages[i].uid);
|
||||
destinationFolder.messages.push(messages[i]);
|
||||
|
||||
// do not write directly to stream, use notifications as the currently selected mailbox might not be the one that receives the message
|
||||
entries.push({
|
||||
command: 'EXISTS',
|
||||
uid: messages[i].uid
|
||||
});
|
||||
}
|
||||
|
||||
this.notifier.addEntries(update.destination, session.user.username, entries, () => {
|
||||
this.notifier.fire(update.destination, session.user.username);
|
||||
|
||||
return callback(null, true, {
|
||||
uidValidity: destinationFolder.uidValidity,
|
||||
sourceUid,
|
||||
destinationUid
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// sends results to socket
|
||||
server.onFetch = function (mailbox, options, session, callback) {
|
||||
this.logger.debug('[%s] Requested FETCH for "%s"', session.id, mailbox);
|
||||
if (!folders.has(mailbox)) {
|
||||
return callback(null, 'NONEXISTENT');
|
||||
}
|
||||
|
||||
let folder = folders.get(mailbox);
|
||||
let entries = [];
|
||||
|
||||
if (options.markAsSeen) {
|
||||
// mark all matching messages as seen
|
||||
folder.messages.forEach(message => {
|
||||
if (options.messages.indexOf(message.uid) < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if BODY[] is touched, then add \Seen flag and notify other clients
|
||||
if (message.flags.indexOf('\\Seen') < 0) {
|
||||
message.flags.unshift('\\Seen');
|
||||
entries.push({
|
||||
command: 'FETCH',
|
||||
ignore: session.id,
|
||||
uid: message.uid,
|
||||
flags: message.flags
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
this.notifier.addEntries(session.user.username, mailbox, entries, () => {
|
||||
|
||||
folder.messages.forEach(message => {
|
||||
if (options.messages.indexOf(message.uid) < 0) {
|
||||
return;
|
||||
}
|
||||
// send formatted response to socket
|
||||
session.writeStream.write(session.formatResponse('FETCH', message.uid, {
|
||||
query: options.query,
|
||||
values: session.getQueryResponse(options.query, message)
|
||||
}));
|
||||
});
|
||||
|
||||
// once messages are processed show relevant updates
|
||||
this.notifier.fire(session.user.username, mailbox);
|
||||
|
||||
callback(null, true);
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
// returns an array of matching UID values
|
||||
server.onSearch = function (mailbox, options, session, callback) {
|
||||
if (!folders.has(mailbox)) {
|
||||
return callback(null, 'NONEXISTENT');
|
||||
}
|
||||
|
||||
let folder = folders.get(mailbox);
|
||||
let highestModseq = 0;
|
||||
|
||||
let uidList = folder.messages.filter(message => {
|
||||
let match = session.matchSearchQuery(message, options.query);
|
||||
if (match && highestModseq < message.modseq) {
|
||||
highestModseq = message.modseq;
|
||||
}
|
||||
return match;
|
||||
}).map(message => message.uid);
|
||||
|
||||
callback(null, {
|
||||
uidList,
|
||||
highestModseq
|
||||
});
|
||||
};
|
||||
|
||||
// -------
|
||||
|
||||
server.on('error', err => {
|
||||
console.log('Error occurred\n%s', err.stack); // eslint-disable-line no-console
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
server.close(() => {
|
||||
process.exit();
|
||||
});
|
||||
});
|
||||
|
||||
// start listening
|
||||
server.listen(SERVER_PORT, SERVER_HOST);
|
6
imap-core/index.js
Normal file
6
imap-core/index.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
module.exports.IMAPServer = require('./lib/imap-server').IMAPServer;
|
||||
module.exports.RedisNotifier = require('./lib/redis-notifier');
|
||||
module.exports.MemoryNotifier = require('./lib/memory-notifier');
|
||||
module.exports.imapHandler = require('./lib/handler/imap-handler');
|
118
imap-core/lib/commands/append.js
Normal file
118
imap-core/lib/commands/append.js
Normal file
|
@ -0,0 +1,118 @@
|
|||
'use strict';
|
||||
|
||||
let imapTools = require('../imap-tools');
|
||||
|
||||
module.exports = {
|
||||
state: ['Authenticated', 'Selected'],
|
||||
|
||||
// we do not show * EXIST response for added message, so keep other notifications quet as well
|
||||
// otherwise we might end up in situation where APPEND emits an unrelated * EXISTS response
|
||||
// which does not yet take into account the appended message
|
||||
disableNotifications: true,
|
||||
|
||||
schema: [{
|
||||
name: 'mailbox',
|
||||
type: 'string'
|
||||
}, {
|
||||
name: 'flags',
|
||||
type: 'array',
|
||||
optional: true
|
||||
}, {
|
||||
name: 'datetime',
|
||||
type: 'string',
|
||||
optional: true
|
||||
}, {
|
||||
name: 'message',
|
||||
type: 'literal'
|
||||
}],
|
||||
|
||||
handler(command, callback) {
|
||||
|
||||
// Check if APPEND method is set
|
||||
if (typeof this._server.onAppend !== 'function') {
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
message: 'APPEND not implemented'
|
||||
});
|
||||
}
|
||||
|
||||
let mailbox = imapTools.normalizeMailbox((command.attributes.shift() || {}).value);
|
||||
let message = command.attributes.pop();
|
||||
let flags = [];
|
||||
let internaldate = false;
|
||||
let parsedDate;
|
||||
|
||||
if (command.attributes.length === 2) {
|
||||
flags = command.attributes[0] || [];
|
||||
internaldate = command.attributes[1] && command.attributes[1].value || '';
|
||||
} else if (command.attributes.length === 1) {
|
||||
if (Array.isArray(command.attributes[0])) {
|
||||
flags = command.attributes[0];
|
||||
} else {
|
||||
internaldate = command.attributes[0] && command.attributes[0].value || '';
|
||||
}
|
||||
}
|
||||
|
||||
flags = flags.map(flag => (flag.value || '').toString());
|
||||
|
||||
if (!mailbox) {
|
||||
return callback(new Error('Invalid mailbox argument for APPEND'));
|
||||
}
|
||||
|
||||
if (!/^literal$/i.test(message.type)) {
|
||||
return callback(new Error('Invalid message argument for APPEND'));
|
||||
}
|
||||
|
||||
if (internaldate) {
|
||||
if (!validateInternalDate(internaldate)) {
|
||||
return callback(new Error('Invalid date argument for APPEND'));
|
||||
}
|
||||
|
||||
parsedDate = new Date(internaldate);
|
||||
if (parsedDate.toString() === 'Invalid Date' || parsedDate.getTime() > Date.now() + 24 * 3600 * 1000 || parsedDate.getTime() <= 1000) {
|
||||
return callback(new Error('Invalid date-time argument for APPEND'));
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = flags.length - 1; i >= 0; i--) {
|
||||
if (flags[i].charAt(0) === '\\') {
|
||||
if (imapTools.systemFlags.indexOf(flags[i].toLowerCase()) < 0) {
|
||||
return callback(new Error('Invalid system flag argument for APPEND'));
|
||||
} else {
|
||||
// fix flag case
|
||||
flags[i] = flags[i].toLowerCase().replace(/^\\./, c => c.toUpperCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// keep only unique flags
|
||||
flags = flags.filter((flag, i) => {
|
||||
if (i && flags.slice(0, i).indexOf(flag) >= 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
this._server.onAppend(mailbox, flags, internaldate, new Buffer(typeof message.value === 'string' ? message.value : (message.value || '').toString(), 'binary'), this.session, (err, success, info) => {
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let code = typeof success === 'string' ? success.toUpperCase() : 'APPENDUID ' + info.uidValidity + ' ' + info.uid;
|
||||
|
||||
callback(null, {
|
||||
response: success === true ? 'OK' : 'NO',
|
||||
code
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function validateInternalDate(internaldate) {
|
||||
if (!internaldate || typeof internaldate !== 'string') {
|
||||
return false;
|
||||
}
|
||||
return /^([ \d]\d)\-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\-(\d{4}) (\d{2}):(\d{2}):(\d{2}) ([\-+])(\d{2})(\d{2})$/i.test(internaldate);
|
||||
}
|
88
imap-core/lib/commands/authenticate-plain.js
Normal file
88
imap-core/lib/commands/authenticate-plain.js
Normal file
|
@ -0,0 +1,88 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
state: 'Not Authenticated',
|
||||
|
||||
schema: [{
|
||||
name: 'token',
|
||||
type: 'string',
|
||||
optional: true
|
||||
}],
|
||||
|
||||
handler(command, callback, next) {
|
||||
|
||||
let token = (command.attributes && command.attributes[0] && command.attributes[0].value || '').toString().trim();
|
||||
|
||||
if (!this.secure && !this._server.options.ignoreSTARTTLS) {
|
||||
// Only allow authentication using TLS
|
||||
return callback(null, {
|
||||
response: 'BAD',
|
||||
message: 'Run STARTTLS first'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if authentication method is set
|
||||
if (typeof this._server.onAuth !== 'function') {
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
message: 'Authentication not implemented'
|
||||
});
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
this._nextHandler = (token, next) => {
|
||||
this._nextHandler = false;
|
||||
next(); // keep the parser flowing
|
||||
authenticate(this, token, callback);
|
||||
};
|
||||
this.send('+');
|
||||
return next(); // resume input parser. Normally this is done by callback() but we need the next input sooner
|
||||
}
|
||||
|
||||
authenticate(this, token, callback);
|
||||
}
|
||||
};
|
||||
|
||||
function authenticate(connection, token, callback) {
|
||||
let data = new Buffer(token, 'base64').toString().split('\x00');
|
||||
|
||||
if (data.length !== 3) {
|
||||
return callback(null, {
|
||||
response: 'BAD',
|
||||
message: 'Invalid SASL argument'
|
||||
});
|
||||
}
|
||||
|
||||
let username = (data[1] || '').toString().trim();
|
||||
let password = (data[2] || '').toString().trim();
|
||||
|
||||
// Do auth
|
||||
connection._server.onAuth({
|
||||
method: 'PLAIN',
|
||||
username,
|
||||
password
|
||||
}, connection.session, (err, response) => {
|
||||
if (err) {
|
||||
connection._server.logger.info('[%s] Authentication error for %s using %s\n%s', connection.id, username, 'PLAIN', err.message);
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!response || !response.user) {
|
||||
connection._server.logger.info('[%s] Authentication failed for %s using %s', connection.id, username, 'PLAIN');
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
message: 'Authentication failure'
|
||||
});
|
||||
}
|
||||
|
||||
connection._server.logger.info('[%s] %s authenticated using %s', connection.id, username, 'PLAIN');
|
||||
connection.session.user = response.user;
|
||||
connection.state = 'Authenticated';
|
||||
|
||||
callback(null, {
|
||||
response: 'OK',
|
||||
message: username + ' authenticated'
|
||||
});
|
||||
|
||||
});
|
||||
}
|
39
imap-core/lib/commands/capability.js
Normal file
39
imap-core/lib/commands/capability.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
handler(command, callback) {
|
||||
|
||||
let capabilities = [];
|
||||
|
||||
if (!this.secure) {
|
||||
capabilities.push('STARTTLS');
|
||||
if (!this._server.options.ignoreSTARTTLS) {
|
||||
capabilities.push('LOGINDISABLED');
|
||||
}
|
||||
capabilities.push('ENABLE');
|
||||
capabilities.push('CONDSTORE');
|
||||
}
|
||||
|
||||
if (this.state === 'Not Authenticated') {
|
||||
capabilities.push('AUTH=PLAIN');
|
||||
capabilities.push('ID');
|
||||
capabilities.push('SASL-IR');
|
||||
} else {
|
||||
capabilities.push('CHILDREN');
|
||||
capabilities.push('ID');
|
||||
capabilities.push('IDLE');
|
||||
capabilities.push('NAMESPACE');
|
||||
capabilities.push('SPECIAL-USE');
|
||||
capabilities.push('UIDPLUS');
|
||||
capabilities.push('UNSELECT');
|
||||
}
|
||||
|
||||
capabilities.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
this.send('* CAPABILITY ' + ['IMAP4rev1'].concat(capabilities).join(' '));
|
||||
|
||||
callback(null, {
|
||||
response: 'OK'
|
||||
});
|
||||
}
|
||||
};
|
10
imap-core/lib/commands/check.js
Normal file
10
imap-core/lib/commands/check.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
state: ['Selected'],
|
||||
handler(command, callback) {
|
||||
callback(null, {
|
||||
response: 'OK'
|
||||
});
|
||||
}
|
||||
};
|
42
imap-core/lib/commands/close.js
Normal file
42
imap-core/lib/commands/close.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
state: 'Selected',
|
||||
|
||||
handler(command, callback) {
|
||||
|
||||
// Check if EXPUNGE method is set
|
||||
if (typeof this._server.onExpunge !== 'function') {
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
message: 'EXPUNGE not implemented'
|
||||
});
|
||||
}
|
||||
|
||||
// Just unselect if in read only mode
|
||||
if (this.selected.readOnly) {
|
||||
this.session.selected = this.selected = false;
|
||||
this.state = 'Authenticated';
|
||||
return callback(null, {
|
||||
response: 'OK'
|
||||
});
|
||||
}
|
||||
|
||||
let mailbox = this.selected.mailbox;
|
||||
|
||||
this.session.selected = this.selected = false;
|
||||
this.state = 'Authenticated';
|
||||
|
||||
this.updateNotificationListener(() => {
|
||||
this._server.onExpunge(mailbox, {
|
||||
isUid: false,
|
||||
silent: true
|
||||
}, this.session, () => {
|
||||
// don't care if expunging succeeded, the mailbox is now closed anyway
|
||||
callback(null, {
|
||||
response: 'OK'
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
58
imap-core/lib/commands/copy.js
Normal file
58
imap-core/lib/commands/copy.js
Normal file
|
@ -0,0 +1,58 @@
|
|||
'use strict';
|
||||
|
||||
let imapTools = require('../imap-tools');
|
||||
|
||||
module.exports = {
|
||||
state: 'Selected',
|
||||
|
||||
schema: [{
|
||||
name: 'range',
|
||||
type: 'sequence'
|
||||
}, {
|
||||
name: 'mailbox',
|
||||
type: 'string'
|
||||
}],
|
||||
|
||||
handler(command, callback) {
|
||||
|
||||
let cmd = (command.command || '').toString().toUpperCase();
|
||||
|
||||
// Check if COPY method is set
|
||||
if (typeof this._server.onCopy !== 'function') {
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
message: cmd + ' not implemented'
|
||||
});
|
||||
}
|
||||
|
||||
let range = command.attributes[0] && command.attributes[0].value || '';
|
||||
let mailbox = imapTools.normalizeMailbox(command.attributes[1] && command.attributes[1].value || '');
|
||||
|
||||
if (!mailbox) {
|
||||
return callback(new Error('Invalid mailbox argument for ' + cmd));
|
||||
}
|
||||
|
||||
if (!imapTools.validateSequnce(range)) {
|
||||
return callback(new Error('Invalid sequence set for ' + cmd));
|
||||
}
|
||||
|
||||
let messages = imapTools.getMessageRange(this.selected.uidList, range, cmd === 'UID COPY');
|
||||
|
||||
this._server.onCopy(this.selected.mailbox, {
|
||||
destination: mailbox,
|
||||
messages
|
||||
}, this.session, (err, success, info) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let code = typeof success === 'string' ? success.toUpperCase() : 'COPYUID ' + info.uidValidity + ' ' + imapTools.packMessageRange(info.sourceUid) + ' ' + imapTools.packMessageRange(info.destinationUid);
|
||||
|
||||
callback(null, {
|
||||
response: success === true ? 'OK' : 'NO',
|
||||
code
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
};
|
69
imap-core/lib/commands/create.js
Normal file
69
imap-core/lib/commands/create.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
'use strict';
|
||||
|
||||
let imapTools = require('../imap-tools');
|
||||
|
||||
// tag CREATE "mailbox"
|
||||
|
||||
module.exports = {
|
||||
state: ['Authenticated', 'Selected'],
|
||||
|
||||
schema: [{
|
||||
name: 'mailbox',
|
||||
type: 'string'
|
||||
}],
|
||||
|
||||
handler(command, callback) {
|
||||
|
||||
let mailbox = command.attributes[0] && command.attributes[0].value || '';
|
||||
|
||||
// Check if CREATE method is set
|
||||
if (typeof this._server.onCreate !== 'function') {
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
message: 'CREATE not implemented'
|
||||
});
|
||||
}
|
||||
|
||||
if (!mailbox) {
|
||||
// nothing to check for if mailbox is not defined
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
code: 'CANNOT',
|
||||
message: 'No folder name given'
|
||||
});
|
||||
}
|
||||
|
||||
// ignore commands that try to create hierarchy
|
||||
if (/\/$/.test(mailbox)) {
|
||||
return callback(null, {
|
||||
response: 'OK',
|
||||
code: 'CANNOT',
|
||||
message: 'Ignoring hierarchy declaration'
|
||||
});
|
||||
}
|
||||
|
||||
// ignore commands with adjacent spaces
|
||||
if (/\/{2,}/.test(mailbox)) {
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
code: 'CANNOT',
|
||||
message: 'Adjacent hierarchy separators are not supported'
|
||||
});
|
||||
}
|
||||
|
||||
mailbox = imapTools.normalizeMailbox(mailbox);
|
||||
|
||||
this._server.onCreate(mailbox, this.session, (err, success) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, {
|
||||
response: success === true ? 'OK' : 'NO',
|
||||
code: typeof success === 'string' ? success.toUpperCase() : false
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
};
|
69
imap-core/lib/commands/delete.js
Normal file
69
imap-core/lib/commands/delete.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
'use strict';
|
||||
|
||||
let imapTools = require('../imap-tools');
|
||||
|
||||
// tag DELETE "mailbox"
|
||||
|
||||
module.exports = {
|
||||
state: ['Authenticated', 'Selected'],
|
||||
|
||||
schema: [{
|
||||
name: 'mailbox',
|
||||
type: 'string'
|
||||
}],
|
||||
|
||||
handler(command, callback) {
|
||||
|
||||
let mailbox = command.attributes[0] && command.attributes[0].value || '';
|
||||
|
||||
// Check if DELETE method is set
|
||||
if (typeof this._server.onDelete !== 'function') {
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
message: 'DELETE not implemented'
|
||||
});
|
||||
}
|
||||
|
||||
mailbox = imapTools.normalizeMailbox(mailbox);
|
||||
|
||||
if (!mailbox) {
|
||||
// nothing to check for if mailbox is not defined
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
code: 'CANNOT',
|
||||
message: 'No folder name given'
|
||||
});
|
||||
}
|
||||
|
||||
if (mailbox === 'INBOX') {
|
||||
// nothing to check for if mailbox is not defined
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
code: 'CANNOT',
|
||||
message: 'INBOX can not be deleted'
|
||||
});
|
||||
}
|
||||
|
||||
this._server.onDelete(mailbox, this.session, (err, success) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (success !== true) {
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
code: typeof success === 'string' ? success.toUpperCase() : false
|
||||
});
|
||||
}
|
||||
|
||||
this._server.notifier.fire(this.session.user.username, mailbox, {
|
||||
action: 'DELETE',
|
||||
mailbox
|
||||
});
|
||||
|
||||
callback(null, {
|
||||
response: 'OK'
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
25
imap-core/lib/commands/enable.js
Normal file
25
imap-core/lib/commands/enable.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
state: ['Authenticated'],
|
||||
schema: false,
|
||||
|
||||
handler(command, callback) {
|
||||
let enabled = [];
|
||||
|
||||
command.attributes.map(attr => {
|
||||
// only CONDSTORE is supported for now
|
||||
if ((attr && attr.value || '').toString().toUpperCase() === 'CONDSTORE') {
|
||||
this.condstoreEnabled = true;
|
||||
enabled.push('CONDSTORE');
|
||||
}
|
||||
});
|
||||
|
||||
this.send('* ENABLED' + (enabled.length ? ' ' : '') + enabled.join(' '));
|
||||
|
||||
callback(null, {
|
||||
response: 'OK',
|
||||
message: 'Conditional Store enabled'
|
||||
});
|
||||
}
|
||||
};
|
36
imap-core/lib/commands/expunge.js
Normal file
36
imap-core/lib/commands/expunge.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
state: 'Selected',
|
||||
|
||||
handler(command, callback) {
|
||||
|
||||
// Check if EXPUNGE method is set
|
||||
if (typeof this._server.onExpunge !== 'function') {
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
message: 'EXPUNGE not implemented'
|
||||
});
|
||||
}
|
||||
|
||||
// Do nothing if in read only mode
|
||||
if (this.selected.readOnly) {
|
||||
return callback(null, {
|
||||
response: 'OK'
|
||||
});
|
||||
}
|
||||
|
||||
this._server.onExpunge(this.selected.mailbox, {
|
||||
isUid: false
|
||||
}, this.session, (err, success) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, {
|
||||
response: success === true ? 'OK' : 'NO',
|
||||
code: typeof success === 'string' ? success.toUpperCase() : false
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
303
imap-core/lib/commands/fetch.js
Normal file
303
imap-core/lib/commands/fetch.js
Normal file
|
@ -0,0 +1,303 @@
|
|||
'use strict';
|
||||
|
||||
let imapTools = require('../imap-tools');
|
||||
let imapHandler = require('../handler/imap-handler');
|
||||
|
||||
/*
|
||||
|
||||
handles both FETCH and UID FETCH
|
||||
|
||||
a1 FETCH 1:* (FLAGS BODY BODY.PEEK[HEADER.FIELDS (SUBJECT DATE FROM)] BODY.PEEK[]<0.28> BODY.PEEK[]<0> BODY[HEADER] BODY[1.2])
|
||||
a1 FETCH 1 (BODY.PEEK[HEADER] BODY.PEEK[TEXT])
|
||||
a1 FETCH 1 (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc bcc message-id in-reply-to references)])
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
state: 'Selected',
|
||||
disableNotifications: true,
|
||||
|
||||
schema: [{
|
||||
name: 'range',
|
||||
type: 'sequence'
|
||||
}, {
|
||||
name: 'data',
|
||||
type: 'mixed'
|
||||
}, {
|
||||
name: 'extensions',
|
||||
type: 'array',
|
||||
optional: true
|
||||
}],
|
||||
|
||||
handler(command, callback) {
|
||||
|
||||
// Check if FETCH method is set
|
||||
if (typeof this._server.onFetch !== 'function') {
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
message: command.command + ' not implemented'
|
||||
});
|
||||
}
|
||||
|
||||
let isUid = (command.command || '').toString().toUpperCase() === 'UID FETCH' ? true : false;
|
||||
let range = command.attributes[0] && command.attributes[0].value || '';
|
||||
if (!imapTools.validateSequnce(range)) {
|
||||
return callback(new Error('Invalid sequence set for ' + command.command));
|
||||
}
|
||||
let messages = imapTools.getMessageRange(this.selected.uidList, range, isUid);
|
||||
let flagsExist = false;
|
||||
let uidExist = false;
|
||||
let modseqExist = false;
|
||||
let markAsSeen = false;
|
||||
let metadataOnly = true;
|
||||
let changedSince = 0;
|
||||
let query = [];
|
||||
|
||||
let params = [].concat(command.attributes[1] || []);
|
||||
let extensions = [].concat(command.attributes[2] || []).map(val => (val && val.value));
|
||||
|
||||
if (extensions.length) {
|
||||
if (extensions.length !== 2 || (extensions[0] || '').toString().toUpperCase() !== 'CHANGEDSINCE' || isNaN(extensions[1])) {
|
||||
return callback(new Error('Invalid modifier for ' + command.command));
|
||||
}
|
||||
changedSince = Number(extensions[1]);
|
||||
if (changedSince && !this.selected.condstoreEnabled) {
|
||||
this.condstoreEnabled = this.selected.condstoreEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
let macros = new Map(
|
||||
// Map iterator is a list of tuples
|
||||
[
|
||||
// ALL
|
||||
['ALL', ['FLAGS', 'INTERNALDATE', 'RFC822.SIZE', 'ENVELOPE']],
|
||||
// FAST
|
||||
['FAST', ['FLAGS', 'INTERNALDATE', 'RFC822.SIZE']],
|
||||
// FULL
|
||||
['FULL', ['FLAGS', 'INTERNALDATE', 'RFC822.SIZE', 'ENVELOPE', 'BODY']]
|
||||
]
|
||||
);
|
||||
|
||||
let i, len, param, section;
|
||||
|
||||
// normalize query
|
||||
|
||||
// replace macro with actual items
|
||||
if (command.attributes[1].type === 'ATOM' && macros.has(command.attributes[1].value.toUpperCase())) {
|
||||
params = macros.get(command.attributes[1].value.toUpperCase());
|
||||
}
|
||||
|
||||
// checks conditions – does the messages need to be marked as seen, is the full body needed etc.
|
||||
for (i = 0, len = params.length; i < len; i++) {
|
||||
param = params[i];
|
||||
if (!param || (typeof param !== 'string' && param.type !== 'ATOM')) {
|
||||
return callback(new Error('Invalid message data item name for ' + command.command));
|
||||
}
|
||||
|
||||
if (typeof param === 'string') {
|
||||
param = params[i] = {
|
||||
type: 'ATOM',
|
||||
value: param
|
||||
};
|
||||
}
|
||||
|
||||
if (param.value.toUpperCase() === 'FLAGS') {
|
||||
flagsExist = true;
|
||||
}
|
||||
|
||||
if (param.value.toUpperCase() === 'UID') {
|
||||
uidExist = true;
|
||||
}
|
||||
|
||||
if (param.value.toUpperCase() === 'MODSEQ') {
|
||||
modseqExist = true;
|
||||
}
|
||||
|
||||
if (!this.selected.readOnly) {
|
||||
if (param.value.toUpperCase() === 'BODY' && param.section) {
|
||||
// BODY[...]
|
||||
markAsSeen = true;
|
||||
} else if (param.value.toUpperCase() === 'RFC822') {
|
||||
// RFC822
|
||||
markAsSeen = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (param.value.toUpperCase() === 'BODY.PEEK' && param.section) {
|
||||
param.value = 'BODY';
|
||||
}
|
||||
|
||||
if (['BODY', 'RFC822', 'RFC822.SIZE', 'RFC822.HEADER', 'RFC822.TEXT', 'BODYSTRUCTURE'].indexOf(param.value.toUpperCase()) >= 0) {
|
||||
metadataOnly = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Adds FLAGS to the response if needed. If the query touches BODY[] then this message
|
||||
// must be marked as \Seen. To inform the client about flags change, include the updated
|
||||
// flags in the response
|
||||
if (markAsSeen && !flagsExist) {
|
||||
params.push({
|
||||
type: 'ATOM',
|
||||
value: 'FLAGS'
|
||||
});
|
||||
}
|
||||
|
||||
// ensure UID is listed if the command is UID FETCH
|
||||
if (isUid && !uidExist) {
|
||||
params.push({
|
||||
type: 'ATOM',
|
||||
value: 'UID'
|
||||
});
|
||||
}
|
||||
|
||||
// ensure MODSEQ is listed if the command uses CHANGEDSINCE modifier
|
||||
if (changedSince && !modseqExist) {
|
||||
params.push({
|
||||
type: 'ATOM',
|
||||
value: 'MODSEQ'
|
||||
});
|
||||
}
|
||||
|
||||
// returns header field name from a IMAP command object
|
||||
let getFieldName = field => (field.value || '').toString().toLowerCase();
|
||||
|
||||
// compose query object from parsed IMAP command
|
||||
for (i = 0, len = params.length; i < len; i++) {
|
||||
param = params[i];
|
||||
let item = {
|
||||
query: imapHandler.compiler({
|
||||
attributes: param
|
||||
}),
|
||||
item: (param.value || '').toString().toLowerCase(),
|
||||
original: param
|
||||
};
|
||||
|
||||
if (param.section) {
|
||||
if (!param.section.length) {
|
||||
item.path = '';
|
||||
item.type = 'content';
|
||||
} else {
|
||||
// we are expecting stuff like 'TEXT' or '1.2.3.TEXT' or '1.2.3'
|
||||
// the numeric part ('1.2.3') is the path to the MIME node
|
||||
// and 'TEXT' or '' is the queried item (empty means entire content)
|
||||
section = (param.section[0].value || '').toString().toLowerCase();
|
||||
item.path = section.match(/^(\d+\.)*(\d+$)?/);
|
||||
|
||||
if (item.path && item.path[0].length) {
|
||||
item.path = item.path[0].replace(/\.$/, '');
|
||||
item.type = section.substr(item.path.length + 1) || 'content';
|
||||
} else {
|
||||
item.path = isNaN(section) ? '' : section;
|
||||
item.type = section;
|
||||
}
|
||||
|
||||
/*
|
||||
item.type = (param.section[0].value || '').toString().toLowerCase();
|
||||
*/
|
||||
if (/^HEADER.FIELDS(\.NOT)?$/i.test(item.type) && Array.isArray(param.section[1])) {
|
||||
item.headers = param.section[1].map(getFieldName);
|
||||
}
|
||||
}
|
||||
// return this element as literal value
|
||||
item.isLiteral = true;
|
||||
}
|
||||
|
||||
if (['RFC822', 'RFC822.HEADER', 'RFC822.TEXT'].indexOf(param.value.toUpperCase()) >= 0) {
|
||||
item.isLiteral = true;
|
||||
}
|
||||
|
||||
if (param.partial) {
|
||||
item.partial = {
|
||||
startFrom: Number(param.partial[0]) || 0,
|
||||
maxLength: Number(param.partial[1]) || 0
|
||||
};
|
||||
}
|
||||
if (!imapTools.fetchSchema.hasOwnProperty(item.item) || !checkSchema(imapTools.fetchSchema[item.item], item)) {
|
||||
return callback(null, {
|
||||
response: 'BAD',
|
||||
message: 'Invalid message data item ' + item.query + ' for ' + command.command
|
||||
});
|
||||
}
|
||||
|
||||
query.push(item);
|
||||
}
|
||||
|
||||
this._server.logger.debug('[%s] FETCH: %s', this.id, JSON.stringify({
|
||||
metadataOnly: !!metadataOnly,
|
||||
markAsSeen: !!markAsSeen,
|
||||
messages,
|
||||
query,
|
||||
changedSince,
|
||||
isUid
|
||||
}));
|
||||
|
||||
this._server.onFetch(this.selected.mailbox, {
|
||||
metadataOnly: !!metadataOnly,
|
||||
markAsSeen: !!markAsSeen,
|
||||
messages,
|
||||
query,
|
||||
changedSince,
|
||||
isUid
|
||||
}, this.session, (err, success) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, {
|
||||
response: success === true ? 'OK' : 'NO',
|
||||
code: typeof success === 'string' ? success.toUpperCase() : false
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function checkSchema(schema, item) {
|
||||
let i, len;
|
||||
if (Array.isArray(schema)) {
|
||||
for (i = 0, len = schema.length; i < len; i++) {
|
||||
if (checkSchema(schema[i], item)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (schema === true) {
|
||||
if (item.hasOwnProperty('type') || item.partial) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof schema === 'object' && schema) {
|
||||
|
||||
// check.type
|
||||
switch (Object.prototype.toString.call(schema.type)) {
|
||||
case '[object RegExp]':
|
||||
if (!schema.type.test(item.type)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '[object String]':
|
||||
if (schema.type !== item.type) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '[object Boolean]':
|
||||
if (item.hasOwnProperty('type') || item.partial || schema.type !== true) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if headers must be present
|
||||
if (schema.headers && schema.headers.test(item.type) && !Array.isArray(item.headers)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
80
imap-core/lib/commands/id.js
Normal file
80
imap-core/lib/commands/id.js
Normal file
|
@ -0,0 +1,80 @@
|
|||
'use strict';
|
||||
|
||||
let packageInfo = require('../../../package');
|
||||
let imapHandler = require('../handler/imap-handler');
|
||||
|
||||
let allowedKeys = ['name', 'version', 'os', 'os-version', 'vendor', 'support-url', 'address', 'date', 'command', 'arguments', 'environment'];
|
||||
|
||||
module.exports = {
|
||||
schema: [{
|
||||
name: 'id',
|
||||
type: ['null', 'array']
|
||||
}],
|
||||
handler(command, callback) {
|
||||
let clientId = {};
|
||||
let serverId = {};
|
||||
let serverIdList = [];
|
||||
let key = false;
|
||||
let maxKeyLen = 0;
|
||||
|
||||
if (this._server.options.id && typeof this._server.options.id === 'object') {
|
||||
Object.keys(this._server.options.id).forEach(key => {
|
||||
serverId[key] = this._server.options.id[key];
|
||||
});
|
||||
} else {
|
||||
serverId.name = packageInfo.name;
|
||||
serverId.version = packageInfo.version;
|
||||
serverId.vendor = 'Kreata';
|
||||
}
|
||||
|
||||
// Log ID information proviced by the client
|
||||
if (Array.isArray(command.attributes[0])) {
|
||||
command.attributes[0].forEach(val => {
|
||||
if (key === false) {
|
||||
key = (val.value || '').toString().toLowerCase().trim();
|
||||
} else {
|
||||
if (allowedKeys.indexOf(key) >= 0) {
|
||||
clientId[key] = (val.value || '').toString();
|
||||
maxKeyLen = Math.max(maxKeyLen, key.length);
|
||||
}
|
||||
key = false;
|
||||
}
|
||||
});
|
||||
|
||||
this._server.logger.info('[%s] Client identification data received', this.id);
|
||||
|
||||
Object.keys(clientId).
|
||||
sort((a, b) => (allowedKeys.indexOf(a) - allowedKeys.indexOf(b))).
|
||||
forEach(key => {
|
||||
this._server.logger.info('[%s] %s%s: %s', this.id, key, new Array(maxKeyLen - key.length + 1).join(' '), clientId[key]);
|
||||
});
|
||||
}
|
||||
|
||||
// Create response ID serverIdList
|
||||
if (Object.keys(serverId).length) {
|
||||
Object.keys(serverId).forEach(key => {
|
||||
serverIdList.push({
|
||||
type: 'string',
|
||||
value: (key || '').toString()
|
||||
});
|
||||
serverIdList.push({
|
||||
type: 'string',
|
||||
value: (serverId[key] || '').toString()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.send(imapHandler.compiler({
|
||||
tag: '*',
|
||||
command: 'ID',
|
||||
attributes: serverIdList.length ? [serverIdList] : {
|
||||
type: 'atom',
|
||||
value: 'NIL'
|
||||
}
|
||||
}));
|
||||
|
||||
callback(null, {
|
||||
response: 'OK'
|
||||
});
|
||||
}
|
||||
};
|
46
imap-core/lib/commands/idle.js
Normal file
46
imap-core/lib/commands/idle.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
state: ['Authenticated', 'Selected'],
|
||||
|
||||
handler(command, callback, next) {
|
||||
|
||||
let idleTimeout = setTimeout(() => {
|
||||
if (typeof this._server.onIdleEnd === 'function') {
|
||||
this._server.onIdleEnd(this.selected && this.selected.mailbox, this.session);
|
||||
}
|
||||
this.send('* BYE IDLE terminated');
|
||||
this.close();
|
||||
}, this._server.options.idleTimeout || 30 * 60 * 1000);
|
||||
|
||||
this._nextHandler = (token, next) => {
|
||||
this._nextHandler = false;
|
||||
this.idling = false;
|
||||
clearTimeout(idleTimeout);
|
||||
next(); // keep the parser flowing
|
||||
|
||||
if (typeof this._server.onIdleEnd === 'function') {
|
||||
this._server.onIdleEnd(this.selected && this.selected.mailbox, this.session);
|
||||
}
|
||||
|
||||
if (token.toUpperCase().trim() !== 'DONE') {
|
||||
return callback(new Error('Invalid Idle continuation'));
|
||||
}
|
||||
|
||||
callback(null, {
|
||||
response: 'OK',
|
||||
message: 'IDLE terminated'
|
||||
});
|
||||
};
|
||||
|
||||
this.idling = true;
|
||||
this.send('+ idling');
|
||||
this.emitNotifications(); // emit any pending notifications
|
||||
|
||||
if (typeof this._server.onIdleStart === 'function') {
|
||||
this._server.onIdleStart(this.selected && this.selected.mailbox, this.session);
|
||||
}
|
||||
|
||||
return next(); // resume input parser. Normally this is done by callback() but we need the next input sooner
|
||||
}
|
||||
};
|
141
imap-core/lib/commands/list.js
Normal file
141
imap-core/lib/commands/list.js
Normal file
|
@ -0,0 +1,141 @@
|
|||
'use strict';
|
||||
|
||||
let imapHandler = require('../handler/imap-handler');
|
||||
let imapTools = require('../imap-tools');
|
||||
|
||||
// tag LIST (SPECIAL-USE) "" "%" RETURN (SPECIAL-USE)
|
||||
|
||||
module.exports = {
|
||||
state: ['Authenticated', 'Selected'],
|
||||
|
||||
schema: [{
|
||||
name: 'selection',
|
||||
type: ['array'],
|
||||
optional: true
|
||||
}, {
|
||||
name: 'reference',
|
||||
type: 'string'
|
||||
}, {
|
||||
name: 'mailbox',
|
||||
type: 'string'
|
||||
}, {
|
||||
name: 'return',
|
||||
type: 'atom',
|
||||
optional: true
|
||||
}, {
|
||||
name: 'return',
|
||||
type: 'array',
|
||||
optional: true
|
||||
}],
|
||||
|
||||
handler(command, callback) {
|
||||
|
||||
let filterSpecialUseFolders = false;
|
||||
let filterSpecialUseFlags = false;
|
||||
let reference;
|
||||
let mailbox;
|
||||
|
||||
let arrPos = 0;
|
||||
|
||||
// (SPECIAL-USE)
|
||||
if (Array.isArray(command.attributes[0])) {
|
||||
if (command.attributes[0].length) {
|
||||
if (command.attributes[0].length === 1 && command.attributes[0][0].type === 'ATOM' && command.attributes[0][0].value.toUpperCase() === 'SPECIAL-USE') {
|
||||
filterSpecialUseFolders = true;
|
||||
} else {
|
||||
return callback(new Error('Invalid argument provided for LIST'));
|
||||
}
|
||||
}
|
||||
arrPos++;
|
||||
}
|
||||
|
||||
// ""
|
||||
reference = command.attributes[arrPos] && command.attributes[arrPos].value || '';
|
||||
arrPos++;
|
||||
|
||||
// "%"
|
||||
mailbox = command.attributes[arrPos] && command.attributes[arrPos].value || '';
|
||||
arrPos++;
|
||||
|
||||
// RETURN (SPECIAL-USE)
|
||||
if (arrPos < command.attributes.length) {
|
||||
if (command.attributes[arrPos].type === 'ATOM' && command.attributes[arrPos].value.toUpperCase() === 'RETURN') {
|
||||
arrPos++;
|
||||
if (Array.isArray(command.attributes[arrPos]) && command.attributes[arrPos].length === 1 && command.attributes[arrPos][0].type === 'ATOM' && command.attributes[arrPos][0].value.toUpperCase() === 'SPECIAL-USE') {
|
||||
filterSpecialUseFlags = true;
|
||||
} else {
|
||||
return callback(new Error('Invalid argument provided for LIST'));
|
||||
}
|
||||
} else {
|
||||
return callback(new Error('Invalid argument provided for LIST'));
|
||||
}
|
||||
}
|
||||
|
||||
// Check if LIST method is set
|
||||
if (typeof this._server.onList !== 'function') {
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
message: 'LIST not implemented'
|
||||
});
|
||||
}
|
||||
|
||||
let query = imapTools.normalizeMailbox(reference + mailbox);
|
||||
|
||||
let listResponse = (err, list) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
imapTools.filterFolders(imapTools.generateFolderListing(list), query).forEach(folder => {
|
||||
if (!folder) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (filterSpecialUseFolders && !folder.specialUse) {
|
||||
return;
|
||||
}
|
||||
|
||||
let response = {
|
||||
tag: '*',
|
||||
command: 'LIST',
|
||||
attributes: []
|
||||
};
|
||||
|
||||
let flags = [];
|
||||
|
||||
if (!filterSpecialUseFlags) {
|
||||
flags = flags.concat(folder.flags || []);
|
||||
}
|
||||
|
||||
flags = flags.concat(folder.specialUse || []);
|
||||
|
||||
response.attributes.push(flags.map(flag => ({
|
||||
type: 'atom',
|
||||
value: flag
|
||||
})));
|
||||
|
||||
response.attributes.push('/');
|
||||
response.attributes.push(folder.path);
|
||||
|
||||
this.send(imapHandler.compiler(response));
|
||||
});
|
||||
|
||||
callback(null, {
|
||||
response: 'OK'
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
if (!mailbox) {
|
||||
// return delimiter only
|
||||
return listResponse(null, [{
|
||||
path: '/',
|
||||
flags: '\\Noselect'
|
||||
}]);
|
||||
}
|
||||
|
||||
// Do folder listing
|
||||
// Concat reference and mailbox. No special reference handling whatsoever
|
||||
this._server.onList(query, this.session, listResponse);
|
||||
}
|
||||
};
|
67
imap-core/lib/commands/login.js
Normal file
67
imap-core/lib/commands/login.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
state: 'Not Authenticated',
|
||||
|
||||
schema: [{
|
||||
name: 'username',
|
||||
type: 'string'
|
||||
}, {
|
||||
name: 'password',
|
||||
type: 'string'
|
||||
}],
|
||||
|
||||
handler(command, callback) {
|
||||
|
||||
let username = (command.attributes[0].value || '').toString().trim();
|
||||
let password = (command.attributes[1].value || '').toString().trim();
|
||||
|
||||
if (!this.secure && !this._server.options.ignoreSTARTTLS) {
|
||||
// Only allow authentication using TLS
|
||||
return callback(null, {
|
||||
response: 'BAD',
|
||||
message: 'Run STARTTLS first'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if authentication method is set
|
||||
if (typeof this._server.onAuth !== 'function') {
|
||||
this._server.logger.info('[%s] Authentication failed for %s using %s', this.id, username, 'LOGIN');
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
message: 'Authentication not implemented'
|
||||
});
|
||||
}
|
||||
|
||||
// Do auth
|
||||
this._server.onAuth({
|
||||
method: 'LOGIN',
|
||||
username,
|
||||
password
|
||||
}, this.session, (err, response) => {
|
||||
|
||||
if (err) {
|
||||
this._server.logger.info('[%s] Authentication error for %s using %s\n%s', this.id, username, 'LOGIN', err.message);
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!response || !response.user) {
|
||||
this._server.logger.info('[%s] Authentication failed for %s using %s', this.id, username, 'LOGIN');
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
message: 'Authentication failure'
|
||||
});
|
||||
}
|
||||
|
||||
this._server.logger.info('[%s] %s authenticated using %s', this.id, username, 'LOGIN');
|
||||
this.session.user = response.user;
|
||||
this.state = 'Authenticated';
|
||||
|
||||
callback(null, {
|
||||
response: 'OK',
|
||||
message: username + ' authenticated'
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
};
|
14
imap-core/lib/commands/logout.js
Normal file
14
imap-core/lib/commands/logout.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
handler(command) {
|
||||
this.session.selected = this.selected = false;
|
||||
this.state = 'Logout';
|
||||
|
||||
this.updateNotificationListener(() => {
|
||||
this.send('* BYE Logout requested');
|
||||
this.send(command.tag + ' OK All dreams are but another reality. Never forget...');
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
};
|
78
imap-core/lib/commands/lsub.js
Normal file
78
imap-core/lib/commands/lsub.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
'use strict';
|
||||
|
||||
let imapHandler = require('../handler/imap-handler');
|
||||
let imapTools = require('../imap-tools');
|
||||
|
||||
// tag LSUB "" "%"
|
||||
|
||||
module.exports = {
|
||||
state: ['Authenticated', 'Selected'],
|
||||
|
||||
schema: [{
|
||||
name: 'reference',
|
||||
type: 'string'
|
||||
}, {
|
||||
name: 'mailbox',
|
||||
type: 'string'
|
||||
}],
|
||||
|
||||
handler(command, callback) {
|
||||
|
||||
let reference = command.attributes[0] && command.attributes[0].value || '';
|
||||
let mailbox = command.attributes[1] && command.attributes[1].value || '';
|
||||
|
||||
// Check if LIST method is set
|
||||
if (typeof this._server.onLsub !== 'function') {
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
message: 'LSUB not implemented'
|
||||
});
|
||||
}
|
||||
|
||||
let query = imapTools.normalizeMailbox(reference + mailbox);
|
||||
|
||||
let lsubResponse = (err, list) => {
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
imapTools.filterFolders(imapTools.generateFolderListing(list, true), query).forEach(folder => {
|
||||
if (!folder) {
|
||||
return;
|
||||
}
|
||||
|
||||
let response = {
|
||||
tag: '*',
|
||||
command: 'LSUB',
|
||||
attributes: [
|
||||
[].concat(folder.flags || []).map(flag => ({
|
||||
type: 'atom',
|
||||
value: flag
|
||||
})),
|
||||
'/', folder.path
|
||||
]
|
||||
};
|
||||
|
||||
this.send(imapHandler.compiler(response));
|
||||
});
|
||||
|
||||
callback(null, {
|
||||
response: 'OK'
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
if (!mailbox) {
|
||||
// return delimiter only
|
||||
return lsubResponse(null, {
|
||||
path: '/',
|
||||
flags: '\\Noselect'
|
||||
});
|
||||
}
|
||||
|
||||
// Do folder listing
|
||||
// Concat reference and mailbox. No special reference handling whatsoever
|
||||
this._server.onLsub(imapTools.normalizeMailbox(reference + mailbox), this.session, lsubResponse);
|
||||
}
|
||||
};
|
15
imap-core/lib/commands/namespace.js
Normal file
15
imap-core/lib/commands/namespace.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
state: ['Authenticated', 'Selected'],
|
||||
|
||||
handler(command, callback) {
|
||||
|
||||
// fixed structre
|
||||
this.send('* NAMESPACE (("" "/")) NIL NIL');
|
||||
|
||||
callback(null, {
|
||||
response: 'OK'
|
||||
});
|
||||
}
|
||||
};
|
10
imap-core/lib/commands/noop.js
Normal file
10
imap-core/lib/commands/noop.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
handler(command, callback) {
|
||||
callback(null, {
|
||||
response: 'OK',
|
||||
message: 'Nothing done'
|
||||
});
|
||||
}
|
||||
};
|
82
imap-core/lib/commands/rename.js
Normal file
82
imap-core/lib/commands/rename.js
Normal file
|
@ -0,0 +1,82 @@
|
|||
'use strict';
|
||||
|
||||
let imapTools = require('../imap-tools');
|
||||
|
||||
// tag RENAME "mailbox"
|
||||
|
||||
module.exports = {
|
||||
state: ['Authenticated', 'Selected'],
|
||||
|
||||
schema: [{
|
||||
name: 'mailbox',
|
||||
type: 'string'
|
||||
}, {
|
||||
name: 'newname',
|
||||
type: 'string'
|
||||
}],
|
||||
|
||||
handler(command, callback) {
|
||||
|
||||
let mailbox = command.attributes[0] && command.attributes[0].value || '';
|
||||
let newname = command.attributes[1] && command.attributes[1].value || '';
|
||||
|
||||
// Check if RENAME method is set
|
||||
if (typeof this._server.onRename !== 'function') {
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
message: 'RENAME not implemented'
|
||||
});
|
||||
}
|
||||
|
||||
if (!mailbox || !newname) {
|
||||
// nothing to check for if mailbox is not defined
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
code: 'CANNOT',
|
||||
message: 'No folder name given'
|
||||
});
|
||||
}
|
||||
|
||||
// ignore commands with adjacent spaces
|
||||
if (/\/{2,}/.test(newname)) {
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
code: 'CANNOT',
|
||||
message: 'Adjacent hierarchy separators are not supported'
|
||||
});
|
||||
}
|
||||
|
||||
mailbox = imapTools.normalizeMailbox(mailbox);
|
||||
newname = imapTools.normalizeMailbox(newname);
|
||||
|
||||
// Renaming INBOX is permitted by RFC3501 but not by this implementation
|
||||
if (mailbox === 'INBOX') {
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
code: 'CANNOT',
|
||||
message: 'INBOX can not be renamed'
|
||||
});
|
||||
}
|
||||
|
||||
if (newname === 'INBOX') {
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
code: 'ALREADYEXISTS',
|
||||
message: 'INBOX already exists'
|
||||
});
|
||||
}
|
||||
|
||||
this._server.onRename(mailbox, newname, this.session, (err, success) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, {
|
||||
response: success === true ? 'OK' : 'NO',
|
||||
code: typeof success === 'string' ? success.toUpperCase() : false
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
};
|
281
imap-core/lib/commands/search.js
Normal file
281
imap-core/lib/commands/search.js
Normal file
|
@ -0,0 +1,281 @@
|
|||
'use strict';
|
||||
|
||||
let imapHandler = require('../handler/imap-handler');
|
||||
let imapTools = require('../imap-tools');
|
||||
|
||||
module.exports = {
|
||||
state: 'Selected',
|
||||
|
||||
schema: false, // recursive, can't predefine
|
||||
|
||||
handler(command, callback) {
|
||||
|
||||
// Check if SEARCH method is set
|
||||
if (typeof this._server.onSearch !== 'function') {
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
message: command.command + ' not implemented'
|
||||
});
|
||||
}
|
||||
|
||||
let isUid = (command.command || '').toString().toUpperCase() === 'UID SEARCH' ? true : false;
|
||||
|
||||
let terms = [];
|
||||
let getTerms = elements => {
|
||||
elements.forEach(element => {
|
||||
if (Array.isArray(element)) {
|
||||
return getTerms(element);
|
||||
}
|
||||
terms.push(element.value);
|
||||
});
|
||||
};
|
||||
getTerms([].concat(command.attributes || []));
|
||||
|
||||
let parsed;
|
||||
|
||||
try {
|
||||
parsed = parseQueryTerms(terms, this.selected.uidList);
|
||||
} catch (E) {
|
||||
return callback(E);
|
||||
}
|
||||
|
||||
// mark CONDSTORE as enabled
|
||||
if (parsed.terms.indexOf('modseq') >= 0 && !this.selected.condstoreEnabled) {
|
||||
this.condstoreEnabled = this.selected.condstoreEnabled = true;
|
||||
}
|
||||
|
||||
this._server.onSearch(this.selected.mailbox, {
|
||||
query: parsed.query,
|
||||
terms: parsed.terms,
|
||||
isUid
|
||||
}, this.session, (err, results) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let matches = results.uidList;
|
||||
|
||||
if (typeof matches === 'string') {
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
code: matches.toUpperCase()
|
||||
});
|
||||
}
|
||||
|
||||
let response = {
|
||||
tag: '*',
|
||||
command: 'SEARCH',
|
||||
attributes: []
|
||||
};
|
||||
|
||||
if (Array.isArray(matches) && matches.length) {
|
||||
matches.sort((a, b) => (a - b));
|
||||
|
||||
matches.forEach(nr => {
|
||||
let seq;
|
||||
|
||||
if (!isUid) {
|
||||
seq = this.selected.uidList.indexOf(nr) + 1;
|
||||
if (seq) {
|
||||
response.attributes.push({
|
||||
type: 'atom',
|
||||
value: String(seq)
|
||||
});
|
||||
}
|
||||
} else {
|
||||
response.attributes.push({
|
||||
type: 'atom',
|
||||
value: String(nr)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// append (MODSEQ 123) for queries that include MODSEQ criteria
|
||||
if (results.highestModseq && parsed.terms.indexOf('modseq') >= 0) {
|
||||
response.attributes.push(
|
||||
[{
|
||||
type: 'atom',
|
||||
value: 'MODSEQ'
|
||||
}, {
|
||||
type: 'atom',
|
||||
value: String(results.highestModseq)
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
this.send(imapHandler.compiler(response));
|
||||
|
||||
return callback(null, {
|
||||
response: 'OK'
|
||||
});
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
parseQueryTerms // expose for testing
|
||||
};
|
||||
|
||||
function parseQueryTerms(terms, uidList) {
|
||||
terms = [].concat(terms || []);
|
||||
|
||||
let pos = 0;
|
||||
let term;
|
||||
let returnTerms = [];
|
||||
let parsed = {
|
||||
terms: []
|
||||
};
|
||||
|
||||
let getTerm = level => {
|
||||
level = level || 0;
|
||||
if (pos >= terms.length) {
|
||||
return undefined; // eslint-disable-line no-undefined
|
||||
}
|
||||
|
||||
let term = terms[pos++];
|
||||
let termType = imapTools.searchSchema[term.toLowerCase()];
|
||||
let termCount = termType && termType.length;
|
||||
let curTerm = [term.toLowerCase()];
|
||||
|
||||
// MODSEQ is special case as it includes 2 optional arguments
|
||||
// If the next argument is a number then there is only one argument,
|
||||
// otherwise there is 3 arguments
|
||||
if (curTerm[0] === 'modseq') {
|
||||
termType = isNaN(terms[pos]) ? termType[0] : termType[1];
|
||||
termCount = termType.length;
|
||||
}
|
||||
|
||||
if (!termType) {
|
||||
// try if it is a sequence set
|
||||
if (imapTools.validateSequnce(term)) {
|
||||
// resolve sequence list to an array of UID values
|
||||
curTerm = ['uid', imapTools.getMessageRange(uidList, term, false)];
|
||||
} else {
|
||||
// no idea what the term is for
|
||||
throw new Error('Unknown search term ' + term.toUpperCase());
|
||||
}
|
||||
} else if (termCount) {
|
||||
for (let i = 0, len = termCount; i < len; i++) {
|
||||
if (termType[i] === 'expression') {
|
||||
curTerm.push(getTerm(level + 1));
|
||||
} else if (termType[i] === 'sequence') {
|
||||
if (!imapTools.validateSequnce(terms[pos])) {
|
||||
throw new Error('Invalid sequence set for ' + term.toUpperCase());
|
||||
}
|
||||
// resolve sequence list to an array of UID values
|
||||
curTerm.push(imapTools.getMessageRange(uidList, terms[pos++], true));
|
||||
} else {
|
||||
curTerm.push(terms[pos++]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (imapTools.searchMapping.hasOwnProperty(curTerm[0])) {
|
||||
curTerm = normalizeTerm(curTerm, imapTools.searchMapping[curTerm[0]]);
|
||||
}
|
||||
|
||||
// return multiple values at once, should be already formatted
|
||||
if (typeof curTerm[0] === 'object') {
|
||||
return curTerm;
|
||||
}
|
||||
|
||||
// keep a list of used terms
|
||||
if (parsed.terms.indexOf(curTerm[0]) < 0) {
|
||||
parsed.terms.push(curTerm[0]);
|
||||
}
|
||||
|
||||
let response = {
|
||||
key: curTerm[0]
|
||||
};
|
||||
|
||||
switch (response.key) {
|
||||
|
||||
case 'not':
|
||||
// make sure not is not an array, instead return several 'not' expressions
|
||||
response = [].concat(curTerm[1] || []).map(val => ({
|
||||
key: 'not',
|
||||
value: val
|
||||
}));
|
||||
|
||||
if (response.length === 1) {
|
||||
response = response[0];
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'or':
|
||||
// ensure that value is alwas an array
|
||||
response.value = [].concat(curTerm.slice(1) || []);
|
||||
break;
|
||||
|
||||
case 'header':
|
||||
response.header = (curTerm[1] || '').toString().toLowerCase();
|
||||
response.value = (curTerm[2] || '').toString(); // empty header value means that the header key must be present
|
||||
break;
|
||||
|
||||
case 'date':
|
||||
case 'internaldate':
|
||||
response.operator = curTerm[1];
|
||||
response.value = (curTerm[2] || '').toString();
|
||||
break;
|
||||
|
||||
case 'size':
|
||||
if (isNaN(curTerm[2])) {
|
||||
throw new Error('Invalid size argument for ' + response.key);
|
||||
}
|
||||
response.operator = curTerm[1];
|
||||
response.value = Number(curTerm[2]) || 0;
|
||||
break;
|
||||
|
||||
case 'modseq':
|
||||
if (isNaN(curTerm[curTerm.length - 1])) {
|
||||
throw new Error('Invalid MODSEQ argument');
|
||||
}
|
||||
response.value = Number(curTerm[curTerm.length - 1]) || 0;
|
||||
break;
|
||||
|
||||
default:
|
||||
if (curTerm.length) {
|
||||
response.value = curTerm.length > 2 ? curTerm.slice(1) : curTerm[1];
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
while (typeof (term = getTerm()) !== 'undefined') {
|
||||
if (Array.isArray(term)) {
|
||||
// flatten arrays
|
||||
returnTerms = returnTerms.concat(term);
|
||||
} else {
|
||||
returnTerms.push(term);
|
||||
}
|
||||
}
|
||||
|
||||
parsed.terms.sort();
|
||||
parsed.query = returnTerms;
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function normalizeTerm(term, mapping) {
|
||||
let flags;
|
||||
|
||||
let result = [mapping.key].concat(mapping.value.map(val => (val === '$1' ? term[1] : val)));
|
||||
|
||||
if (result[0] === 'flag') {
|
||||
flags = [];
|
||||
result.forEach((val, i) => {
|
||||
if (i && (i % 2 !== 0)) {
|
||||
flags.push({
|
||||
key: 'flag',
|
||||
value: val,
|
||||
exists: !!result[i + 1]
|
||||
});
|
||||
}
|
||||
});
|
||||
return flags;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
214
imap-core/lib/commands/select.js
Normal file
214
imap-core/lib/commands/select.js
Normal file
|
@ -0,0 +1,214 @@
|
|||
'use strict';
|
||||
|
||||
let imapHandler = require('../handler/imap-handler');
|
||||
let imapTools = require('../imap-tools');
|
||||
|
||||
// tag SELECT "mailbox"
|
||||
// tag EXAMINE "mailbox"
|
||||
|
||||
module.exports = {
|
||||
state: ['Authenticated', 'Selected'],
|
||||
|
||||
schema: [{
|
||||
name: 'mailbox',
|
||||
type: 'string'
|
||||
}, {
|
||||
name: 'extensions',
|
||||
type: 'array',
|
||||
optional: true
|
||||
}],
|
||||
|
||||
handler(command, callback) {
|
||||
|
||||
let mailbox = imapTools.normalizeMailbox(command.attributes[0] && command.attributes[0].value || '');
|
||||
|
||||
let extensions = [].
|
||||
concat(command.attributes[1] || []).
|
||||
map(attr => (attr && attr.value || '').toString().toUpperCase());
|
||||
|
||||
// Is CONDSTORE found from the optional arguments list?
|
||||
if (extensions.indexOf('CONDSTORE') >= 0) {
|
||||
this.condstoreEnabled = true;
|
||||
}
|
||||
|
||||
if (typeof this._server.onOpen !== 'function') {
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
message: command.command + ' not implemented'
|
||||
});
|
||||
}
|
||||
|
||||
if (!mailbox) {
|
||||
// nothing to check for if mailbox is not defined
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
code: 'NONEXISTENT'
|
||||
});
|
||||
}
|
||||
|
||||
this._server.onOpen(mailbox, this.session, (err, folder) => {
|
||||
if (err) {
|
||||
this.session.selected = this.selected = false;
|
||||
this.state = 'Authenticated';
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!folder || typeof folder === 'string') {
|
||||
this.session.selected = this.selected = false;
|
||||
this.state = 'Authenticated';
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
code: typeof folder === 'string' ? folder : 'NONEXISTENT'
|
||||
});
|
||||
}
|
||||
|
||||
// Set current state as selected
|
||||
this.session.selected = this.selected = {
|
||||
modifyIndex: folder.modifyIndex,
|
||||
uidList: folder.uidList,
|
||||
notifications: [],
|
||||
condstoreEnabled: this.condstoreEnabled,
|
||||
readOnly: (command.command || '').toString().toUpperCase() === 'EXAMINE' ? true : false,
|
||||
mailbox
|
||||
};
|
||||
this.state = 'Selected';
|
||||
|
||||
// * FLAGS (\Answered \Flagged \Draft \Deleted \Seen)
|
||||
this.send(imapHandler.compiler({
|
||||
tag: '*',
|
||||
command: 'FLAGS',
|
||||
attributes: [
|
||||
[{
|
||||
type: 'atom',
|
||||
value: '\\Answered'
|
||||
}, {
|
||||
type: 'atom',
|
||||
value: '\\Flagged'
|
||||
}, {
|
||||
type: 'atom',
|
||||
value: '\\Draft'
|
||||
}, {
|
||||
type: 'atom',
|
||||
value: '\\Deleted'
|
||||
}, {
|
||||
type: 'atom',
|
||||
value: '\\Seen'
|
||||
}]
|
||||
]
|
||||
}));
|
||||
|
||||
// * OK [PERMANENTFLAGS (\Answered \Flagged \Draft \Deleted \Seen \*)] Flags permitted
|
||||
this.send(imapHandler.compiler({
|
||||
tag: '*',
|
||||
command: 'OK',
|
||||
attributes: [{
|
||||
type: 'section',
|
||||
section: [
|
||||
// unrelated comment to enforce eslint-happy indentation
|
||||
{
|
||||
type: 'atom',
|
||||
value: 'PERMANENTFLAGS'
|
||||
},
|
||||
[{
|
||||
type: 'atom',
|
||||
value: '\\Answered'
|
||||
}, {
|
||||
type: 'atom',
|
||||
value: '\\Flagged'
|
||||
}, {
|
||||
type: 'atom',
|
||||
value: '\\Draft'
|
||||
}, {
|
||||
type: 'atom',
|
||||
value: '\\Deleted'
|
||||
}, {
|
||||
type: 'atom',
|
||||
value: '\\Seen'
|
||||
}, {
|
||||
type: 'text',
|
||||
value: '\\*'
|
||||
}]
|
||||
]
|
||||
}, {
|
||||
type: 'text',
|
||||
value: 'Flags permitted'
|
||||
}]
|
||||
}));
|
||||
|
||||
// * OK [UIDVALIDITY 123] UIDs valid
|
||||
this.send(imapHandler.compiler({
|
||||
tag: '*',
|
||||
command: 'OK',
|
||||
attributes: [{
|
||||
type: 'section',
|
||||
section: [{
|
||||
type: 'atom',
|
||||
value: 'UIDVALIDITY'
|
||||
}, {
|
||||
type: 'atom',
|
||||
value: String(Number(folder.uidValidity) || 1)
|
||||
}]
|
||||
}, {
|
||||
type: 'text',
|
||||
value: 'UIDs valid'
|
||||
}]
|
||||
}));
|
||||
|
||||
// * 0 EXISTS
|
||||
this.send('* ' + folder.uidList.length + ' EXISTS');
|
||||
|
||||
// * 0 RECENT
|
||||
this.send('* 0 RECENT');
|
||||
|
||||
// * OK [HIGHESTMODSEQ 123]
|
||||
if ('modifyIndex' in folder && Number(folder.modifyIndex)) {
|
||||
this.send(imapHandler.compiler({
|
||||
tag: '*',
|
||||
command: 'OK',
|
||||
attributes: [{
|
||||
type: 'section',
|
||||
section: [{
|
||||
type: 'atom',
|
||||
value: 'HIGHESTMODSEQ'
|
||||
}, {
|
||||
type: 'atom',
|
||||
value: String(Number(folder.modifyIndex) || 0)
|
||||
}]
|
||||
}, {
|
||||
type: 'text',
|
||||
value: 'Highest'
|
||||
}]
|
||||
}));
|
||||
}
|
||||
|
||||
// * OK [UIDNEXT 1] Predicted next UID
|
||||
this.send(imapHandler.compiler({
|
||||
tag: '*',
|
||||
command: 'OK',
|
||||
attributes: [{
|
||||
type: 'section',
|
||||
section: [{
|
||||
type: 'atom',
|
||||
value: 'UIDNEXT'
|
||||
}, {
|
||||
type: 'atom',
|
||||
value: String(Number(folder.uidNext) || 1)
|
||||
}]
|
||||
}, {
|
||||
type: 'text',
|
||||
value: 'Predicted next UID'
|
||||
}]
|
||||
}));
|
||||
|
||||
// start listening for EXPUNGE, EXISTS and FETCH FLAGS notifications
|
||||
this.updateNotificationListener(() => {
|
||||
callback(null, {
|
||||
response: 'OK',
|
||||
code: this.selected.readOnly ? 'READ-ONLY' : 'READ-WRITE',
|
||||
message: command.command + ' completed' + (this.selected.condstoreEnabled ? ', CONDSTORE is now enabled' : '')
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
};
|
70
imap-core/lib/commands/starttls.js
Normal file
70
imap-core/lib/commands/starttls.js
Normal file
|
@ -0,0 +1,70 @@
|
|||
'use strict';
|
||||
|
||||
// openssl s_client -starttls imap -crlf -connect localhost:1143
|
||||
|
||||
let tls = require('tls');
|
||||
let tlsOptions = require('../tls-options');
|
||||
|
||||
let SOCKET_TIMEOUT = 30 * 60 * 1000;
|
||||
|
||||
module.exports = {
|
||||
handler(command, callback) {
|
||||
if (this.secure) {
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
message: 'Connection is already secured'
|
||||
});
|
||||
}
|
||||
|
||||
setImmediate(upgrade.bind(null, this));
|
||||
|
||||
callback(null, {
|
||||
response: 'OK'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Upgrades current socket to use TLS
|
||||
* @param {Object} connection IMAPConnection instance
|
||||
*/
|
||||
function upgrade(connection) {
|
||||
connection._socket.unpipe(connection._parser);
|
||||
connection.writeStream.unpipe(connection._socket);
|
||||
connection._upgrading = true;
|
||||
|
||||
let secureContext = tls.createSecureContext(tlsOptions(connection._server.options));
|
||||
let socketOptions = {
|
||||
isServer: true,
|
||||
secureContext
|
||||
};
|
||||
|
||||
// Apply additional socket options if these are set in the server options
|
||||
['requestCert', 'rejectUnauthorized', 'session'].forEach(key => {
|
||||
if (key in connection._server.options) {
|
||||
socketOptions[key] = connection._server.options[key];
|
||||
}
|
||||
});
|
||||
|
||||
// remove all listeners from the original socket besides the error handler
|
||||
connection._socket.removeAllListeners();
|
||||
connection._socket.on('error', connection._onError.bind(connection));
|
||||
|
||||
// upgrade connection
|
||||
let secureSocket = new tls.TLSSocket(connection._socket, socketOptions);
|
||||
|
||||
secureSocket.on('close', connection._onClose.bind(connection));
|
||||
secureSocket.on('error', connection._onError.bind(connection));
|
||||
secureSocket.on('clientError', connection._onError.bind(connection));
|
||||
secureSocket.setTimeout(connection._server.options.socketTimeout || SOCKET_TIMEOUT, connection._onTimeout.bind(connection));
|
||||
|
||||
secureSocket.on('secure', () => {
|
||||
connection.secure = true;
|
||||
connection._socket = secureSocket;
|
||||
connection._upgrading = false;
|
||||
|
||||
connection._server.logger.info('[%s] Connection upgraded to TLS', connection.id);
|
||||
connection._socket.pipe(connection._parser);
|
||||
connection.writeStream.pipe(connection._socket);
|
||||
});
|
||||
}
|
135
imap-core/lib/commands/status.js
Normal file
135
imap-core/lib/commands/status.js
Normal file
|
@ -0,0 +1,135 @@
|
|||
'use strict';
|
||||
|
||||
let imapTools = require('../imap-tools');
|
||||
let imapHandler = require('../handler/imap-handler');
|
||||
|
||||
// tag STATUS "mailbox" (UNSEEN UIDNEXT)
|
||||
|
||||
module.exports = {
|
||||
state: ['Authenticated', 'Selected'],
|
||||
|
||||
schema: [{
|
||||
name: 'mailbox',
|
||||
type: 'string'
|
||||
}, {
|
||||
name: 'query',
|
||||
type: 'array'
|
||||
}],
|
||||
|
||||
handler(command, callback) {
|
||||
|
||||
let mailbox = command.attributes[0] && command.attributes[0].value || '';
|
||||
let query = command.attributes[1] && command.attributes[1];
|
||||
|
||||
let statusElements = ['MESSAGES', 'RECENT', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN', 'HIGHESTMODSEQ'];
|
||||
let statusItem;
|
||||
let statusQuery = [];
|
||||
|
||||
// Check if STATUS method is set
|
||||
if (typeof this._server.onStatus !== 'function') {
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
message: 'STATUS not implemented'
|
||||
});
|
||||
}
|
||||
|
||||
if (!mailbox) {
|
||||
// nothing to check for if mailbox is not defined
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
code: 'CANNOT',
|
||||
message: 'No folder name given'
|
||||
});
|
||||
}
|
||||
|
||||
if (!Array.isArray(query)) {
|
||||
return callback(null, {
|
||||
response: 'BAD',
|
||||
message: 'Invalid arguments for STATUS'
|
||||
});
|
||||
}
|
||||
|
||||
// check if status elements are listed
|
||||
if (!query.length) {
|
||||
return callback(null, {
|
||||
response: 'BAD',
|
||||
message: 'Empty status list'
|
||||
});
|
||||
}
|
||||
|
||||
// check if only known status items are used
|
||||
for (let i = 0, len = query.length; i < len; i++) {
|
||||
statusItem = (query[i] && query[i].value || '').toString().toUpperCase();
|
||||
if (statusElements.indexOf(statusItem) < 0) {
|
||||
return callback(null, {
|
||||
response: 'BAD',
|
||||
message: 'Invalid status items'
|
||||
});
|
||||
}
|
||||
if (statusQuery.indexOf(statusItem) < 0) {
|
||||
statusQuery.push(statusItem);
|
||||
}
|
||||
}
|
||||
|
||||
mailbox = imapTools.normalizeMailbox(mailbox);
|
||||
|
||||
// mark CONDSTORE as enabled
|
||||
if (statusQuery.indexOf('HIGHESTMODSEQ') >= 0 && !this.condstoreEnabled) {
|
||||
this.condstoreEnabled = true;
|
||||
if (this.selected) {
|
||||
this.selected.condstoreEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
this._server.onStatus(mailbox, this.session, (err, data) => {
|
||||
let response;
|
||||
let values = {
|
||||
RECENT: 0
|
||||
};
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (typeof data === 'string') {
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
code: data.toUpperCase()
|
||||
});
|
||||
}
|
||||
|
||||
if (data) {
|
||||
response = {
|
||||
tag: '*',
|
||||
command: 'STATUS',
|
||||
attributes: [
|
||||
command.attributes[0], // reuse the mailbox declaration from client command
|
||||
[]
|
||||
]
|
||||
};
|
||||
Object.keys(data).forEach(key => {
|
||||
values[key.toUpperCase()] = (data[key] || '').toString();
|
||||
});
|
||||
|
||||
statusQuery.forEach(key => {
|
||||
response.attributes[1].push({
|
||||
type: 'atom',
|
||||
value: key.toUpperCase()
|
||||
});
|
||||
response.attributes[1].push({
|
||||
type: 'atom',
|
||||
value: (values[key] || '0').toString()
|
||||
});
|
||||
});
|
||||
|
||||
this.send(imapHandler.compiler(response));
|
||||
}
|
||||
|
||||
callback(null, {
|
||||
response: 'OK'
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
};
|
169
imap-core/lib/commands/store.js
Normal file
169
imap-core/lib/commands/store.js
Normal file
|
@ -0,0 +1,169 @@
|
|||
'use strict';
|
||||
|
||||
let imapTools = require('../imap-tools');
|
||||
|
||||
module.exports = {
|
||||
state: 'Selected',
|
||||
disableNotifications: true,
|
||||
|
||||
schema: [{
|
||||
name: 'range',
|
||||
type: 'sequence'
|
||||
}, {
|
||||
name: 'extensions',
|
||||
type: 'array',
|
||||
optional: true
|
||||
}, {
|
||||
name: 'action',
|
||||
type: 'string'
|
||||
}, {
|
||||
name: 'flags',
|
||||
type: 'array'
|
||||
}],
|
||||
|
||||
handler(command, callback) {
|
||||
|
||||
// Check if STORE method is set
|
||||
if (typeof this._server.onUpdate !== 'function') {
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
message: 'STORE not implemented'
|
||||
});
|
||||
}
|
||||
|
||||
// Do nothing if in read only mode
|
||||
if (this.selected.readOnly) {
|
||||
return callback(null, {
|
||||
response: 'OK',
|
||||
message: 'STORE ignored with read-only mailbox'
|
||||
});
|
||||
}
|
||||
|
||||
let type = 'flags'; // currently hard coded, in the future might support other values as well, eg. X-GM-LABELS
|
||||
let range = command.attributes[0] && command.attributes[0].value || '';
|
||||
|
||||
// if arguments include extenstions at index 1, then length is 4, otherwise 3
|
||||
let pos = command.attributes.length === 4 ? 1 : 0;
|
||||
|
||||
let action = (command.attributes[pos + 1] && command.attributes[pos + 1].value || '').toString().toUpperCase();
|
||||
|
||||
let flags = [].
|
||||
concat(command.attributes[pos + 2] || []).
|
||||
map(flag => (flag && flag.value || '').toString());
|
||||
|
||||
let unchangedSince = 0;
|
||||
let silent = false;
|
||||
|
||||
// extensions are available as the optional argument at index 1
|
||||
let extensions = !pos ? [] : [].
|
||||
concat(command.attributes[pos] || []).
|
||||
map(val => (val && val.value));
|
||||
|
||||
if (extensions.length) {
|
||||
if (extensions.length !== 2 || (extensions[0] || '').toString().toUpperCase() !== 'UNCHANGEDSINCE' || isNaN(extensions[1])) {
|
||||
return callback(new Error('Invalid modifier for STORE'));
|
||||
}
|
||||
unchangedSince = Number(extensions[1]);
|
||||
if (unchangedSince && !this.selected.condstoreEnabled) {
|
||||
this.condstoreEnabled = this.selected.condstoreEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (action.substr(-7) === '.SILENT') {
|
||||
action = action.substr(0, action.length - 7);
|
||||
silent = true;
|
||||
}
|
||||
|
||||
if (!imapTools.validateSequnce(range)) {
|
||||
return callback(new Error('Invalid sequence set for STORE'));
|
||||
}
|
||||
|
||||
if (!/^[\-+]?FLAGS$/.test(action)) {
|
||||
return callback(new Error('Invalid message data item name for STORE'));
|
||||
}
|
||||
|
||||
switch (action.charAt(0)) {
|
||||
case '+':
|
||||
action = 'add';
|
||||
break;
|
||||
case '-':
|
||||
action = 'remove';
|
||||
break;
|
||||
default:
|
||||
action = 'set';
|
||||
}
|
||||
|
||||
for (let i = flags.length - 1; i >= 0; i--) {
|
||||
if (flags[i].charAt(0) === '\\') {
|
||||
if (imapTools.systemFlags.indexOf(flags[i].toLowerCase()) < 0) {
|
||||
return callback(new Error('Invalid system flag argument for STORE'));
|
||||
} else {
|
||||
// fix flag case
|
||||
flags[i] = flags[i].toLowerCase().replace(/^\\./, c => c.toUpperCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// keep only unique flags
|
||||
flags = flags.filter((flag, i) => {
|
||||
if (i && flags.slice(0, i).indexOf(flag) >= 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
let messages = imapTools.getMessageRange(this.selected.uidList, range, false);
|
||||
|
||||
this._server.onUpdate(this.selected.mailbox, {
|
||||
value: flags,
|
||||
action,
|
||||
type,
|
||||
silent,
|
||||
messages,
|
||||
unchangedSince
|
||||
}, this.session, (err, success, modified) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// STORE returns MODIFIED as sequence numbers, so convert UIDs to sequence list
|
||||
if (modified && modified.length) {
|
||||
modified = modified.
|
||||
map(uid => this.selected.uidList.indexOf(uid) + 1).
|
||||
filter(seq =>
|
||||
// ensure that deleted items (eg seq=0) do not end up in the list
|
||||
seq > 0
|
||||
);
|
||||
}
|
||||
|
||||
let message = success === true ? 'STORE completed' : false;
|
||||
if (modified && modified.length) {
|
||||
message = 'Conditional STORE failed';
|
||||
} else if (message && unchangedSince) {
|
||||
message = 'Conditional STORE completed';
|
||||
}
|
||||
|
||||
let response = {
|
||||
response: success === true ? 'OK' : 'NO',
|
||||
code: typeof success === 'string' ? success.toUpperCase() : (modified && modified.length ? 'MODIFIED ' + imapTools.packMessageRange(modified) : false),
|
||||
message
|
||||
};
|
||||
|
||||
// check if only messages that exist are referenced
|
||||
if (!this._server.options.allowStoreExpunged && success === true && !silent && messages.length) {
|
||||
for (let i = this.selected.notifications.length - 1; i >= 0; i--) {
|
||||
if (this.selected.notifications[i].command === 'EXPUNGE' && messages.indexOf(this.selected.notifications[i].uid) >= 0) {
|
||||
response = {
|
||||
response: 'NO',
|
||||
message: 'Some of the messages no longer exist'
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callback(null, response);
|
||||
|
||||
});
|
||||
}
|
||||
};
|
54
imap-core/lib/commands/subscribe.js
Normal file
54
imap-core/lib/commands/subscribe.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
'use strict';
|
||||
|
||||
let imapTools = require('../imap-tools');
|
||||
|
||||
// tag SUBSCRIBE "mailbox"
|
||||
|
||||
module.exports = {
|
||||
state: ['Authenticated', 'Selected'],
|
||||
|
||||
schema: [{
|
||||
name: 'mailbox',
|
||||
type: 'string'
|
||||
}],
|
||||
|
||||
handler(command, callback) {
|
||||
|
||||
let mailbox = imapTools.normalizeMailbox(command.attributes[0] && command.attributes[0].value || '');
|
||||
|
||||
// Check if SUBSCRIBE method is set
|
||||
if (typeof this._server.onSubscribe !== 'function') {
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
message: 'SUBSCRIBE not implemented'
|
||||
});
|
||||
}
|
||||
|
||||
if (!mailbox) {
|
||||
// nothing to check for if mailbox is not defined
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
code: 'NONEXISTENT'
|
||||
});
|
||||
}
|
||||
|
||||
if (mailbox === 'INBOX') {
|
||||
return callback(null, {
|
||||
response: 'OK'
|
||||
});
|
||||
}
|
||||
|
||||
this._server.onSubscribe(mailbox, this.session, (err, success) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, {
|
||||
response: success === true ? 'OK' : 'NO',
|
||||
code: typeof success === 'string' ? success.toUpperCase() : false
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
};
|
50
imap-core/lib/commands/uid-expunge.js
Normal file
50
imap-core/lib/commands/uid-expunge.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
'use strict';
|
||||
|
||||
let imapTools = require('../imap-tools');
|
||||
|
||||
module.exports = {
|
||||
state: 'Selected',
|
||||
|
||||
schema: [{
|
||||
name: 'range',
|
||||
type: 'sequence'
|
||||
}],
|
||||
|
||||
handler(command, callback) {
|
||||
|
||||
// Check if EXPUNGE method is set
|
||||
if (typeof this._server.onExpunge !== 'function') {
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
message: 'EXPUNGE not implemented'
|
||||
});
|
||||
}
|
||||
|
||||
// Do nothing if in read only mode
|
||||
if (this.selected.readOnly) {
|
||||
return callback(null, {
|
||||
response: 'OK'
|
||||
});
|
||||
}
|
||||
|
||||
let range = command.attributes[0] && command.attributes[0].value || '';
|
||||
if (!imapTools.validateSequnce(range)) {
|
||||
return callback(new Error('Invalid sequence set for UID EXPUNGE'));
|
||||
}
|
||||
let messages = imapTools.getMessageRange(this.selected.uidList, range, true);
|
||||
|
||||
this._server.onExpunge(this.selected.mailbox, {
|
||||
isUid: true,
|
||||
messages
|
||||
}, this.session, (err, success) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, {
|
||||
response: success === true ? 'OK' : 'NO',
|
||||
code: typeof success === 'string' ? success.toUpperCase() : false
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
136
imap-core/lib/commands/uid-store.js
Normal file
136
imap-core/lib/commands/uid-store.js
Normal file
|
@ -0,0 +1,136 @@
|
|||
'use strict';
|
||||
|
||||
let imapTools = require('../imap-tools');
|
||||
|
||||
module.exports = {
|
||||
state: 'Selected',
|
||||
|
||||
schema: [{
|
||||
name: 'range',
|
||||
type: 'sequence'
|
||||
}, {
|
||||
name: 'extensions',
|
||||
type: 'array',
|
||||
optional: true
|
||||
}, {
|
||||
name: 'action',
|
||||
type: 'string'
|
||||
}, {
|
||||
name: 'flags',
|
||||
type: 'array'
|
||||
}],
|
||||
|
||||
handler(command, callback) {
|
||||
|
||||
// Check if STORE method is set
|
||||
if (typeof this._server.onUpdate !== 'function') {
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
message: 'STORE not implemented'
|
||||
});
|
||||
}
|
||||
|
||||
let type = 'flags'; // currently hard coded, in the future might support other values as well, eg. X-GM-LABELS
|
||||
let range = command.attributes[0] && command.attributes[0].value || '';
|
||||
|
||||
// if arguments include extenstions at index 1, then length is 4, otherwise 3
|
||||
let pos = command.attributes.length === 4 ? 1 : 0;
|
||||
|
||||
let action = (command.attributes[pos + 1] && command.attributes[pos + 1].value || '').toString().toUpperCase();
|
||||
|
||||
let flags = [].
|
||||
concat(command.attributes[pos + 2] || []).
|
||||
map(flag => (flag && flag.value || '').toString());
|
||||
|
||||
let unchangedSince = 0;
|
||||
let silent = false;
|
||||
|
||||
// extensions are available as the optional argument at index 1
|
||||
let extensions = !pos ? [] : [].
|
||||
concat(command.attributes[pos] || []).
|
||||
map(val => (val && val.value));
|
||||
|
||||
if (extensions.length) {
|
||||
if (extensions.length !== 2 || (extensions[0] || '').toString().toUpperCase() !== 'UNCHANGEDSINCE' || isNaN(extensions[1])) {
|
||||
return callback(new Error('Invalid modifier for STORE'));
|
||||
}
|
||||
unchangedSince = Number(extensions[1]);
|
||||
if (unchangedSince && !this.selected.condstoreEnabled) {
|
||||
this.condstoreEnabled = this.selected.condstoreEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (action.substr(-7) === '.SILENT') {
|
||||
action = action.substr(0, action.length - 7);
|
||||
silent = true;
|
||||
}
|
||||
|
||||
if (!imapTools.validateSequnce(range)) {
|
||||
return callback(new Error('Invalid sequence set for UID STORE'));
|
||||
}
|
||||
|
||||
if (!/^[\-+]?FLAGS$/.test(action)) {
|
||||
return callback(new Error('Invalid message data item name for UID STORE'));
|
||||
}
|
||||
|
||||
switch (action.charAt(0)) {
|
||||
case '+':
|
||||
action = 'add';
|
||||
break;
|
||||
case '-':
|
||||
action = 'remove';
|
||||
break;
|
||||
default:
|
||||
action = 'set';
|
||||
}
|
||||
|
||||
for (let i = flags.length - 1; i >= 0; i--) {
|
||||
if (flags[i].charAt(0) === '\\') {
|
||||
if (imapTools.systemFlags.indexOf(flags[i].toLowerCase()) < 0) {
|
||||
return callback(new Error('Invalid system flag argument for UID STORE'));
|
||||
} else {
|
||||
// fix flag case
|
||||
flags[i] = flags[i].toLowerCase().replace(/^\\./, c => c.toUpperCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// keep only unique flags
|
||||
flags = flags.filter((flag, i) => {
|
||||
if (i && flags.slice(0, i).indexOf(flag) >= 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
let messages = imapTools.getMessageRange(this.selected.uidList, range, true);
|
||||
|
||||
this._server.onUpdate(this.selected.mailbox, {
|
||||
isUid: true,
|
||||
value: flags,
|
||||
action,
|
||||
type,
|
||||
silent,
|
||||
messages,
|
||||
unchangedSince
|
||||
}, this.session, (err, success, modified) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let message = success === true ? 'UID STORE completed' : false;
|
||||
if (modified && modified.length) {
|
||||
message = 'Conditional UID STORE failed';
|
||||
} else if (message && unchangedSince) {
|
||||
message = 'Conditional UID STORE completed';
|
||||
}
|
||||
|
||||
callback(null, {
|
||||
response: success === true ? 'OK' : 'NO',
|
||||
code: typeof success === 'string' ? success.toUpperCase() : (modified && modified.length ? 'MODIFIED ' + imapTools.packMessageRange(modified) : false),
|
||||
message
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
};
|
16
imap-core/lib/commands/unselect.js
Normal file
16
imap-core/lib/commands/unselect.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
state: 'Selected',
|
||||
|
||||
handler(command, callback) {
|
||||
this.session.selected = this.selected = false;
|
||||
this.state = 'Authenticated';
|
||||
|
||||
this.updateNotificationListener(() => {
|
||||
callback(null, {
|
||||
response: 'OK'
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
55
imap-core/lib/commands/unsubscribe.js
Normal file
55
imap-core/lib/commands/unsubscribe.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
'use strict';
|
||||
|
||||
let imapTools = require('../imap-tools');
|
||||
|
||||
// tag UNSUBSCRIBE "mailbox"
|
||||
|
||||
module.exports = {
|
||||
state: ['Authenticated', 'Selected'],
|
||||
|
||||
schema: [{
|
||||
name: 'mailbox',
|
||||
type: 'string'
|
||||
}],
|
||||
|
||||
handler(command, callback) {
|
||||
|
||||
let mailbox = imapTools.normalizeMailbox(command.attributes[0] && command.attributes[0].value || '');
|
||||
|
||||
// Check if UNSUBSCRIBE method is set
|
||||
if (typeof this._server.onUnsubscribe !== 'function') {
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
message: 'UNSUBSCRIBE not implemented'
|
||||
});
|
||||
}
|
||||
|
||||
if (!mailbox) {
|
||||
// nothing to check for if mailbox is not defined
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
code: 'NONEXISTENT'
|
||||
});
|
||||
}
|
||||
|
||||
if (mailbox === 'INBOX') {
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
message: 'Can not unsubscribe from INBOX'
|
||||
});
|
||||
}
|
||||
|
||||
this._server.onUnsubscribe(mailbox, this.session, (err, success) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, {
|
||||
response: success === true ? 'OK' : 'NO',
|
||||
code: typeof success === 'string' ? success.toUpperCase() : false
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
};
|
151
imap-core/lib/handler/README.md
Normal file
151
imap-core/lib/handler/README.md
Normal file
|
@ -0,0 +1,151 @@
|
|||
# IMAP Handler
|
||||
|
||||
Server specific fork of [emailjs-imap-handler](https://github.com/emailjs/emailjs-imap-handler) for Node.js (v5+). Mostly differs from the upstream in the behavior for compiling – instead of compiling a command into long string, a Stream object is returned that can be piped directly to socket. Goal is to pass around large messages as streams instead of keeping these in memory.
|
||||
|
||||
This is more suitable for servers than clients as it is currently not possible to pause the output stream to wait for '+' tagged server response for literal values.
|
||||
|
||||
## Usage
|
||||
|
||||
```javascript
|
||||
let handler = require('./lib/handler/imap-handler');
|
||||
```
|
||||
|
||||
### Parse IMAP commands
|
||||
|
||||
To parse a command you need to have the command as one complete string (including all literals) without the ending <CR><LF>
|
||||
|
||||
imapHandler.parser(imapCommand);
|
||||
|
||||
Where
|
||||
|
||||
* **imapCommand** is an IMAP string without the final line break
|
||||
|
||||
The function returns an object in the following form:
|
||||
|
||||
```javascript
|
||||
{
|
||||
tag: "TAG",
|
||||
command: "COMMAND",
|
||||
attributes: [
|
||||
{type: "SEQUENCE", value: "sequence-set"},
|
||||
{type: "ATOM", value: "atom", section:[section_elements], partial: [start, end]},
|
||||
{type: "STRING", value: "string"},
|
||||
{type: "LITERAL", value: "literal"},
|
||||
[list_elements]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Where
|
||||
|
||||
* **tag** is a string containing the tag
|
||||
* **command** is the first element after tag
|
||||
* **attributes** (if present) is an array of next elements
|
||||
|
||||
If section or partial values are not specified in the command, the values are also missing from the ATOM element
|
||||
|
||||
**NB!** Sequence numbers are identified as ATOM values if the value contains only numbers.
|
||||
**NB!** NIL atoms are always identified as `null` values, even though in some cases it might be an ATOM with value `"NIL"`
|
||||
|
||||
For example
|
||||
|
||||
```javascript
|
||||
let imapHandler = require("imap-handler-1");
|
||||
|
||||
imapHandler.parser("A1 FETCH *:4 (BODY[HEADER.FIELDS ({4}\r\nDate Subject)]<12.45> UID)");
|
||||
```
|
||||
|
||||
Results in the following value:
|
||||
|
||||
```json
|
||||
{
|
||||
"tag": "A1",
|
||||
"command": "FETCH",
|
||||
"attributes": [
|
||||
[
|
||||
{
|
||||
"type": "SEQUENCE",
|
||||
"value": "*:4"
|
||||
},
|
||||
{
|
||||
"type": "ATOM",
|
||||
"value": "BODY",
|
||||
"section": [
|
||||
{
|
||||
"type": "ATOM",
|
||||
"value": "HEADER.FIELDS"
|
||||
},
|
||||
[
|
||||
{
|
||||
"type": "LITERAL",
|
||||
"value": "Date"
|
||||
},
|
||||
{
|
||||
"type": "ATOM",
|
||||
"value": "Subject"
|
||||
}
|
||||
]
|
||||
],
|
||||
"partial": [
|
||||
12,
|
||||
45
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ATOM",
|
||||
"value": "UID"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compile command objects into IMAP commands
|
||||
|
||||
You can "compile" parsed or self generated IMAP command objects to IMAP command strings with
|
||||
|
||||
imapHandler.compileStream(commandObject, isLogging);
|
||||
|
||||
Where
|
||||
|
||||
* **commandObject** is an object parsed with `imapHandler.parser()` or self generated
|
||||
* **isLogging** if set to true, do not include literals and long strings, useful when logging stuff and do not want to include message bodies etc. Additionally nodes with `sensitive: true` options are also not displayed (useful with logging passwords) if `logging` is used.
|
||||
|
||||
The function returns a Stream.
|
||||
|
||||
The input object differs from the parsed object with the following aspects:
|
||||
|
||||
* **string**, **number** and **null** (null values are all non-number and non-string falsy values) are allowed to use directly - `{type: "STRING", value: "hello"}` can be replaced with `"hello"`
|
||||
* Additional types are used: `SECTION` which is an alias for `ATOM` and `TEXT` which returns the input string as given with no modification (useful for server messages).
|
||||
* **LITERAL** can takes streams as values. You do need to know the expected length beforehand though `{type:'LITERAL', expectedLength: 1024, value: stream}`. If the provided length does not match actual stream output length, then the output is either truncated or padded with space symbols to match the expected length.
|
||||
|
||||
```javascript
|
||||
{
|
||||
type: 'LITERAL',
|
||||
value: stream,
|
||||
expectedLength: 100, // full stream length
|
||||
startFrom: 10, // optional start marker, do not emit bytes before it
|
||||
maxLength: 30 // optional length of the output stream
|
||||
}
|
||||
```
|
||||
|
||||
For example
|
||||
|
||||
```javascript
|
||||
let command = {
|
||||
tag: "*",
|
||||
command: "OK",
|
||||
attributes: [
|
||||
{
|
||||
type: "SECTION",
|
||||
section: [
|
||||
{type: "ATOM", value: "ALERT"}
|
||||
]
|
||||
},
|
||||
{type:"TEXT", value: "NB! The server is shutting down"}
|
||||
]
|
||||
};
|
||||
|
||||
imapHandler.compileStream(command).pipe(process.stdout);
|
||||
// * OK [ALERT] NB! The server is shutting down
|
||||
```
|
253
imap-core/lib/handler/imap-compile-stream.js
Normal file
253
imap-core/lib/handler/imap-compile-stream.js
Normal file
|
@ -0,0 +1,253 @@
|
|||
/* eslint no-console: 0, new-cap: 0 */
|
||||
|
||||
'use strict';
|
||||
|
||||
let imapFormalSyntax = require('./imap-formal-syntax');
|
||||
let streams = require('stream');
|
||||
let PassThrough = streams.PassThrough;
|
||||
let LengthLimiter = require('../length-limiter');
|
||||
|
||||
/**
|
||||
* Compiles an input object into
|
||||
*/
|
||||
module.exports = function (response, isLogging) {
|
||||
let output = new PassThrough();
|
||||
|
||||
let resp = (response.tag || '') + (response.command ? ' ' + response.command : '');
|
||||
let lr = resp; // this value is going to store last known `resp` state for later usage
|
||||
|
||||
let val, lastType;
|
||||
|
||||
let waiting = false;
|
||||
let queue = [];
|
||||
let ended = false;
|
||||
|
||||
let emit = function (stream, expectedLength, startFrom, maxLength) {
|
||||
expectedLength = expectedLength || 0;
|
||||
startFrom = startFrom || 0;
|
||||
maxLength = maxLength || 0;
|
||||
|
||||
if (resp.length) {
|
||||
queue.push(new Buffer(resp, 'binary'));
|
||||
lr = resp;
|
||||
resp = '';
|
||||
}
|
||||
|
||||
if (stream) {
|
||||
queue.push({
|
||||
type: 'stream',
|
||||
stream,
|
||||
expectedLength,
|
||||
startFrom,
|
||||
maxLength
|
||||
});
|
||||
}
|
||||
|
||||
if (waiting) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!queue.length) {
|
||||
if (ended) {
|
||||
output.end();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let value = queue.shift();
|
||||
|
||||
if (value.type === 'stream') {
|
||||
if (!value.expectedLength) {
|
||||
return emit();
|
||||
}
|
||||
waiting = true;
|
||||
let expectedLength = value.maxLength ? Math.min(value.expectedLength, value.startFrom + value.maxLength) : value.expectedLength;
|
||||
let startFrom = value.startFrom;
|
||||
|
||||
let limiter = new LengthLimiter(expectedLength, ' ', startFrom);
|
||||
|
||||
value.stream.pipe(limiter).pipe(output, {
|
||||
end: false
|
||||
});
|
||||
|
||||
// pass errors to output
|
||||
value.stream.on('error', err => {
|
||||
output.emit('error', err);
|
||||
});
|
||||
|
||||
limiter.on('end', () => {
|
||||
waiting = false;
|
||||
return emit();
|
||||
});
|
||||
} else if (value instanceof Buffer) {
|
||||
output.write(value);
|
||||
return emit();
|
||||
} else {
|
||||
if (typeof value === 'number') {
|
||||
value = value.toString();
|
||||
} else if (typeof value !== 'string') {
|
||||
value = (value || '').toString();
|
||||
}
|
||||
output.write(new Buffer(value, 'binary'));
|
||||
return emit();
|
||||
}
|
||||
};
|
||||
|
||||
let walk = function (node, callback) {
|
||||
if (lastType === 'LITERAL' || (['(', '<', '['].indexOf((resp || lr).substr(-1)) < 0 && (resp || lr).length)) {
|
||||
resp += ' ';
|
||||
}
|
||||
|
||||
if (Array.isArray(node)) {
|
||||
lastType = 'LIST';
|
||||
resp += '(';
|
||||
|
||||
let pos = 0;
|
||||
let next = () => {
|
||||
if (pos >= node.length) {
|
||||
resp += ')';
|
||||
return setImmediate(callback);
|
||||
}
|
||||
walk(node[pos++], next);
|
||||
};
|
||||
|
||||
return setImmediate(next);
|
||||
}
|
||||
|
||||
if (!node && typeof node !== 'string' && typeof node !== 'number') {
|
||||
resp += 'NIL';
|
||||
return setImmediate(callback);
|
||||
}
|
||||
|
||||
if (typeof node === 'string') {
|
||||
if (isLogging && node.length > 20) {
|
||||
resp += '"(* ' + node.length + 'B string *)"';
|
||||
} else {
|
||||
resp += JSON.stringify(node);
|
||||
}
|
||||
return setImmediate(callback);
|
||||
}
|
||||
|
||||
if (typeof node === 'number') {
|
||||
resp += Math.round(node) || 0; // Only integers allowed
|
||||
return setImmediate(callback);
|
||||
}
|
||||
|
||||
lastType = node.type;
|
||||
|
||||
if (isLogging && node.sensitive) {
|
||||
resp += '"(* value hidden *)"';
|
||||
return setImmediate(callback);
|
||||
}
|
||||
|
||||
switch (node.type.toUpperCase()) {
|
||||
case 'LITERAL':
|
||||
{
|
||||
let nval = node.value;
|
||||
if (typeof nval === 'number') {
|
||||
nval = nval.toString();
|
||||
}
|
||||
|
||||
let len;
|
||||
|
||||
if (nval && typeof nval.pipe === 'function') {
|
||||
len = node.expectedLength || 0;
|
||||
if (node.startFrom) {
|
||||
len -= node.startFrom;
|
||||
}
|
||||
if (node.maxLength) {
|
||||
len = Math.min(len, node.maxLength);
|
||||
}
|
||||
} else {
|
||||
len = (nval || '').toString().length;
|
||||
}
|
||||
|
||||
if (isLogging) {
|
||||
resp += '"(* ' + len + 'B literal *)"';
|
||||
} else {
|
||||
resp += '{' + len + '}\r\n';
|
||||
emit();
|
||||
|
||||
if (nval && typeof nval.pipe === 'function') {
|
||||
//value is a stream object
|
||||
emit(nval, node.expectedLength, node.startFrom, node.maxLength);
|
||||
} else {
|
||||
resp = nval || '';
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'STRING':
|
||||
if (isLogging && node.value.length > 20) {
|
||||
resp += '"(* ' + node.value.length + 'B string *)"';
|
||||
} else {
|
||||
resp += JSON.stringify(node.value || '');
|
||||
}
|
||||
break;
|
||||
case 'TEXT':
|
||||
case 'SEQUENCE':
|
||||
resp += node.value || '';
|
||||
break;
|
||||
|
||||
case 'NUMBER':
|
||||
resp += (node.value || 0);
|
||||
break;
|
||||
|
||||
case 'ATOM':
|
||||
case 'SECTION':
|
||||
{
|
||||
val = node.value || '';
|
||||
|
||||
if (imapFormalSyntax.verify(val.charAt(0) === '\\' ? val.substr(1) : val, imapFormalSyntax['ATOM-CHAR']()) >= 0) {
|
||||
val = JSON.stringify(val);
|
||||
}
|
||||
|
||||
resp += val;
|
||||
|
||||
let finalize = () => {
|
||||
if (node.partial) {
|
||||
resp += '<' + node.partial.join('.') + '>';
|
||||
}
|
||||
setImmediate(callback);
|
||||
};
|
||||
|
||||
if (node.section) {
|
||||
resp += '[';
|
||||
|
||||
let pos = 0;
|
||||
let next = () => {
|
||||
if (pos >= node.section.length) {
|
||||
resp += ']';
|
||||
return setImmediate(finalize);
|
||||
}
|
||||
walk(node.section[pos++], next);
|
||||
};
|
||||
|
||||
return setImmediate(next);
|
||||
}
|
||||
|
||||
return finalize();
|
||||
}
|
||||
}
|
||||
setImmediate(callback);
|
||||
};
|
||||
|
||||
let finalize = () => {
|
||||
ended = true;
|
||||
emit();
|
||||
};
|
||||
let pos = 0;
|
||||
let attribs = [].concat(response.attributes || []);
|
||||
let next = () => {
|
||||
if (pos >= attribs.length) {
|
||||
return setImmediate(finalize);
|
||||
}
|
||||
walk(attribs[pos++], next);
|
||||
};
|
||||
setImmediate(next);
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
// expose for testing
|
||||
module.exports.LengthLimiter = LengthLimiter;
|
116
imap-core/lib/handler/imap-compiler.js
Normal file
116
imap-core/lib/handler/imap-compiler.js
Normal file
|
@ -0,0 +1,116 @@
|
|||
/* eslint no-console: 0, new-cap: 0 */
|
||||
|
||||
'use strict';
|
||||
|
||||
let imapFormalSyntax = require('./imap-formal-syntax');
|
||||
|
||||
/**
|
||||
* Compiles an input object into
|
||||
*/
|
||||
module.exports = function (response, asArray, isLogging) {
|
||||
let respParts = [];
|
||||
let resp = (response.tag || '') + (response.command ? ' ' + response.command : '');
|
||||
let val;
|
||||
let lastType;
|
||||
let walk = function (node) {
|
||||
|
||||
if (lastType === 'LITERAL' || (['(', '<', '['].indexOf(resp.substr(-1)) < 0 && resp.length)) {
|
||||
resp += ' ';
|
||||
}
|
||||
|
||||
if (Array.isArray(node)) {
|
||||
lastType = 'LIST';
|
||||
resp += '(';
|
||||
node.forEach(walk);
|
||||
resp += ')';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!node && typeof node !== 'string' && typeof node !== 'number') {
|
||||
resp += 'NIL';
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof node === 'string') {
|
||||
if (isLogging && node.length > 20) {
|
||||
resp += '"(* ' + node.length + 'B string *)"';
|
||||
} else {
|
||||
resp += JSON.stringify(node);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof node === 'number') {
|
||||
resp += Math.round(node) || 0; // Only integers allowed
|
||||
return;
|
||||
}
|
||||
|
||||
lastType = node.type;
|
||||
|
||||
if (isLogging && node.sensitive) {
|
||||
resp += '"(* value hidden *)"';
|
||||
return;
|
||||
}
|
||||
|
||||
switch (node.type.toUpperCase()) {
|
||||
case 'LITERAL':
|
||||
if (isLogging) {
|
||||
resp += '"(* ' + node.value.length + 'B literal *)"';
|
||||
} else {
|
||||
if (!node.value) {
|
||||
resp += '{0}\r\n';
|
||||
} else {
|
||||
resp += '{' + node.value.length + '}\r\n';
|
||||
}
|
||||
respParts.push(resp);
|
||||
resp = node.value || '';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'STRING':
|
||||
if (isLogging && node.value.length > 20) {
|
||||
resp += '"(* ' + node.value.length + 'B string *)"';
|
||||
} else {
|
||||
resp += JSON.stringify(node.value || '');
|
||||
}
|
||||
break;
|
||||
case 'TEXT':
|
||||
case 'SEQUENCE':
|
||||
resp += node.value || '';
|
||||
break;
|
||||
|
||||
case 'NUMBER':
|
||||
resp += (node.value || 0);
|
||||
break;
|
||||
|
||||
case 'ATOM':
|
||||
case 'SECTION':
|
||||
val = node.value || '';
|
||||
|
||||
if (imapFormalSyntax.verify(val.charAt(0) === '\\' ? val.substr(1) : val, imapFormalSyntax['ATOM-CHAR']()) >= 0) {
|
||||
val = JSON.stringify(val);
|
||||
}
|
||||
|
||||
resp += val;
|
||||
|
||||
if (node.section) {
|
||||
resp += '[';
|
||||
node.section.forEach(walk);
|
||||
resp += ']';
|
||||
}
|
||||
if (node.partial) {
|
||||
resp += '<' + node.partial.join('.') + '>';
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
[].concat(response.attributes || []).forEach(walk);
|
||||
|
||||
if (resp.length) {
|
||||
respParts.push(resp);
|
||||
}
|
||||
|
||||
return asArray ? respParts : respParts.join('');
|
||||
};
|
149
imap-core/lib/handler/imap-formal-syntax.js
Normal file
149
imap-core/lib/handler/imap-formal-syntax.js
Normal file
|
@ -0,0 +1,149 @@
|
|||
/* eslint object-shorthand:0, new-cap: 0, no-useless-concat: 0 */
|
||||
|
||||
'use strict';
|
||||
|
||||
// IMAP Formal Syntax
|
||||
// http://tools.ietf.org/html/rfc3501#section-9
|
||||
|
||||
function expandRange(start, end) {
|
||||
let chars = [];
|
||||
for (let i = start; i <= end; i++) {
|
||||
chars.push(i);
|
||||
}
|
||||
return String.fromCharCode(...chars);
|
||||
}
|
||||
|
||||
function excludeChars(source, exclude) {
|
||||
let sourceArr = Array.prototype.slice.call(source);
|
||||
for (let i = sourceArr.length - 1; i >= 0; i--) {
|
||||
if (exclude.indexOf(sourceArr[i]) >= 0) {
|
||||
sourceArr.splice(i, 1);
|
||||
}
|
||||
}
|
||||
return sourceArr.join('');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
CHAR: function () {
|
||||
let value = expandRange(0x01, 0x7F);
|
||||
this.CHAR = function () {
|
||||
return value;
|
||||
};
|
||||
return value;
|
||||
},
|
||||
|
||||
CHAR8: function () {
|
||||
let value = expandRange(0x01, 0xFF);
|
||||
this.CHAR8 = function () {
|
||||
return value;
|
||||
};
|
||||
return value;
|
||||
},
|
||||
|
||||
SP: function () {
|
||||
return ' ';
|
||||
},
|
||||
|
||||
CTL: function () {
|
||||
let value = expandRange(0x00, 0x1F) + '\x7F';
|
||||
this.CTL = function () {
|
||||
return value;
|
||||
};
|
||||
return value;
|
||||
},
|
||||
|
||||
DQUOTE: function () {
|
||||
return '"';
|
||||
},
|
||||
|
||||
ALPHA: function () {
|
||||
let value = expandRange(0x41, 0x5A) + expandRange(0x61, 0x7A);
|
||||
this.ALPHA = function () {
|
||||
return value;
|
||||
};
|
||||
return value;
|
||||
},
|
||||
|
||||
DIGIT: function () {
|
||||
let value = expandRange(0x30, 0x39) + expandRange(0x61, 0x7A);
|
||||
this.DIGIT = function () {
|
||||
return value;
|
||||
};
|
||||
return value;
|
||||
},
|
||||
|
||||
'ATOM-CHAR': function () {
|
||||
let value = excludeChars(this.CHAR(), this['atom-specials']());
|
||||
this['ATOM-CHAR'] = function () {
|
||||
return value;
|
||||
};
|
||||
return value;
|
||||
},
|
||||
|
||||
'ASTRING-CHAR': function () {
|
||||
let value = this['ATOM-CHAR']() + this['resp-specials']();
|
||||
this['ASTRING-CHAR'] = function () {
|
||||
return value;
|
||||
};
|
||||
return value;
|
||||
},
|
||||
|
||||
'TEXT-CHAR': function () {
|
||||
let value = excludeChars(this.CHAR(), '\r\n');
|
||||
this['TEXT-CHAR'] = function () {
|
||||
return value;
|
||||
};
|
||||
return value;
|
||||
},
|
||||
|
||||
'atom-specials': function () {
|
||||
let value = '(' + ')' + '{' + this.SP() + this.CTL() + this['list-wildcards']() +
|
||||
this['quoted-specials']() + this['resp-specials']();
|
||||
this['atom-specials'] = function () {
|
||||
return value;
|
||||
};
|
||||
return value;
|
||||
},
|
||||
|
||||
'list-wildcards': function () {
|
||||
return '%' + '*';
|
||||
},
|
||||
|
||||
'quoted-specials': function () {
|
||||
let value = this.DQUOTE() + '\\';
|
||||
this['quoted-specials'] = function () {
|
||||
return value;
|
||||
};
|
||||
return value;
|
||||
},
|
||||
|
||||
'resp-specials': function () {
|
||||
return ']';
|
||||
},
|
||||
|
||||
tag: function () {
|
||||
let value = excludeChars(this['ASTRING-CHAR'](), '+');
|
||||
this.tag = function () {
|
||||
return value;
|
||||
};
|
||||
return value;
|
||||
},
|
||||
|
||||
command: function () {
|
||||
let value = this.ALPHA() + this.DIGIT();
|
||||
this.command = function () {
|
||||
return value;
|
||||
};
|
||||
return value;
|
||||
},
|
||||
|
||||
verify: function (str, allowedChars) {
|
||||
for (let i = 0, len = str.length; i < len; i++) {
|
||||
if (allowedChars.indexOf(str.charAt(i)) < 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
};
|
11
imap-core/lib/handler/imap-handler.js
Normal file
11
imap-core/lib/handler/imap-handler.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
'use strict';
|
||||
|
||||
let parser = require('./imap-parser');
|
||||
let compiler = require('./imap-compiler');
|
||||
let compileStream = require('./imap-compile-stream');
|
||||
|
||||
module.exports = {
|
||||
parser,
|
||||
compiler,
|
||||
compileStream
|
||||
};
|
636
imap-core/lib/handler/imap-parser.js
Normal file
636
imap-core/lib/handler/imap-parser.js
Normal file
|
@ -0,0 +1,636 @@
|
|||
/* eslint new-cap: 0 */
|
||||
|
||||
'use strict';
|
||||
|
||||
let imapFormalSyntax = require('./imap-formal-syntax');
|
||||
|
||||
class TokenParser {
|
||||
|
||||
constructor(parent, startPos, str, options) {
|
||||
this.str = (str || '').toString();
|
||||
this.options = options || {};
|
||||
this.parent = parent;
|
||||
|
||||
this.tree = this.currentNode = this.createNode();
|
||||
this.pos = startPos || 0;
|
||||
|
||||
this.currentNode.type = 'TREE';
|
||||
|
||||
this.state = 'NORMAL';
|
||||
|
||||
this.processString();
|
||||
}
|
||||
|
||||
getAttributes() {
|
||||
let attributes = [],
|
||||
branch = attributes;
|
||||
|
||||
let walk = function (node) {
|
||||
let curBranch = branch;
|
||||
let elm;
|
||||
let partial;
|
||||
|
||||
if (!node.closed && node.type === 'SEQUENCE' && node.value === '*') {
|
||||
node.closed = true;
|
||||
node.type = 'ATOM';
|
||||
}
|
||||
|
||||
// If the node was never closed, throw it
|
||||
if (!node.closed) {
|
||||
throw new Error('Unexpected end of input at position ' + (this.pos + this.str.length - 1));
|
||||
}
|
||||
|
||||
let type = (node.type || '').toString().toUpperCase();
|
||||
|
||||
switch (type) {
|
||||
case 'LITERAL':
|
||||
case 'STRING':
|
||||
case 'SEQUENCE':
|
||||
elm = {
|
||||
type: node.type.toUpperCase(),
|
||||
value: node.value
|
||||
};
|
||||
branch.push(elm);
|
||||
break;
|
||||
|
||||
case 'ATOM':
|
||||
if (node.value.toUpperCase() === 'NIL') {
|
||||
branch.push(null);
|
||||
break;
|
||||
}
|
||||
elm = {
|
||||
type: node.type.toUpperCase(),
|
||||
value: node.value
|
||||
};
|
||||
branch.push(elm);
|
||||
break;
|
||||
|
||||
case 'SECTION':
|
||||
branch = branch[branch.length - 1].section = [];
|
||||
break;
|
||||
|
||||
case 'LIST':
|
||||
elm = [];
|
||||
branch.push(elm);
|
||||
branch = elm;
|
||||
break;
|
||||
|
||||
case 'PARTIAL':
|
||||
partial = node.value.split('.').map(Number);
|
||||
branch[branch.length - 1].partial = partial;
|
||||
break;
|
||||
}
|
||||
|
||||
node.childNodes.forEach(childNode => walk(childNode));
|
||||
branch = curBranch;
|
||||
}.bind(this);
|
||||
|
||||
walk(this.tree);
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
createNode(parentNode, startPos) {
|
||||
let node = {
|
||||
childNodes: [],
|
||||
type: false,
|
||||
value: '',
|
||||
closed: true
|
||||
};
|
||||
|
||||
if (parentNode) {
|
||||
node.parentNode = parentNode;
|
||||
}
|
||||
|
||||
if (typeof startPos === 'number') {
|
||||
node.startPos = startPos;
|
||||
}
|
||||
|
||||
if (parentNode) {
|
||||
parentNode.childNodes.push(node);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
processString() {
|
||||
let chr, i, len,
|
||||
checkSP = function () {
|
||||
// jump to the next non whitespace pos
|
||||
while (this.str.charAt(i + 1) === ' ') {
|
||||
i++;
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
for (i = 0, len = this.str.length; i < len; i++) {
|
||||
|
||||
chr = this.str.charAt(i);
|
||||
|
||||
switch (this.state) {
|
||||
|
||||
case 'NORMAL':
|
||||
|
||||
switch (chr) {
|
||||
|
||||
// DQUOTE starts a new string
|
||||
case '"':
|
||||
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
||||
this.currentNode.type = 'string';
|
||||
this.state = 'STRING';
|
||||
this.currentNode.closed = false;
|
||||
break;
|
||||
|
||||
// ( starts a new list
|
||||
case '(':
|
||||
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
||||
this.currentNode.type = 'LIST';
|
||||
this.currentNode.closed = false;
|
||||
break;
|
||||
|
||||
// ) closes a list
|
||||
case ')':
|
||||
if (this.currentNode.type !== 'LIST') {
|
||||
throw new Error('Unexpected list terminator ) at position ' + (this.pos + i));
|
||||
}
|
||||
|
||||
this.currentNode.closed = true;
|
||||
this.currentNode.endPos = this.pos + i;
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
|
||||
checkSP();
|
||||
break;
|
||||
|
||||
// ] closes section group
|
||||
case ']':
|
||||
if (this.currentNode.type !== 'SECTION') {
|
||||
throw new Error('Unexpected section terminator ] at position ' + (this.pos + i));
|
||||
}
|
||||
this.currentNode.closed = true;
|
||||
this.currentNode.endPos = this.pos + i;
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
checkSP();
|
||||
break;
|
||||
|
||||
// < starts a new partial
|
||||
case '<':
|
||||
if (this.str.charAt(i - 1) !== ']') {
|
||||
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
||||
this.currentNode.type = 'ATOM';
|
||||
this.currentNode.value = chr;
|
||||
this.state = 'ATOM';
|
||||
} else {
|
||||
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
||||
this.currentNode.type = 'PARTIAL';
|
||||
this.state = 'PARTIAL';
|
||||
this.currentNode.closed = false;
|
||||
}
|
||||
break;
|
||||
|
||||
// { starts a new literal
|
||||
case '{':
|
||||
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
||||
this.currentNode.type = 'LITERAL';
|
||||
this.state = 'LITERAL';
|
||||
this.currentNode.closed = false;
|
||||
break;
|
||||
|
||||
// ( starts a new sequence
|
||||
case '*':
|
||||
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
||||
this.currentNode.type = 'SEQUENCE';
|
||||
this.currentNode.value = chr;
|
||||
this.currentNode.closed = false;
|
||||
this.state = 'SEQUENCE';
|
||||
break;
|
||||
|
||||
// normally a space should never occur
|
||||
case ' ':
|
||||
// just ignore
|
||||
break;
|
||||
|
||||
// [ starts section
|
||||
case '[':
|
||||
// If it is the *first* element after response command, then process as a response argument list
|
||||
if (['OK', 'NO', 'BAD', 'BYE', 'PREAUTH'].indexOf(this.parent.command.toUpperCase()) >= 0 && this.currentNode === this.tree) {
|
||||
this.currentNode.endPos = this.pos + i;
|
||||
|
||||
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
||||
this.currentNode.type = 'ATOM';
|
||||
|
||||
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
||||
this.currentNode.type = 'SECTION';
|
||||
this.currentNode.closed = false;
|
||||
this.state = 'NORMAL';
|
||||
|
||||
// RFC2221 defines a response code REFERRAL whose payload is an
|
||||
// RFC2192/RFC5092 imapurl that we will try to parse as an ATOM but
|
||||
// fail quite badly at parsing. Since the imapurl is such a unique
|
||||
// (and crazy) term, we just specialize that case here.
|
||||
if (this.str.substr(i + 1, 9).toUpperCase() === 'REFERRAL ') {
|
||||
// create the REFERRAL atom
|
||||
this.currentNode = this.createNode(this.currentNode, this.pos + i + 1);
|
||||
this.currentNode.type = 'ATOM';
|
||||
this.currentNode.endPos = this.pos + i + 8;
|
||||
this.currentNode.value = 'REFERRAL';
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
|
||||
// eat all the way through the ] to be the IMAPURL token.
|
||||
this.currentNode = this.createNode(this.currentNode, this.pos + i + 10);
|
||||
// just call this an ATOM, even though IMAPURL might be more correct
|
||||
this.currentNode.type = 'ATOM';
|
||||
// jump i to the ']'
|
||||
i = this.str.indexOf(']', i + 10);
|
||||
this.currentNode.endPos = this.pos + i - 1;
|
||||
this.currentNode.value = this.str.substring(this.currentNode.startPos - this.pos,
|
||||
this.currentNode.endPos - this.pos + 1);
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
|
||||
// close out the SECTION
|
||||
this.currentNode.closed = true;
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
checkSP();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
/* falls through */
|
||||
default:
|
||||
// Any ATOM supported char starts a new Atom sequence, otherwise throw an error
|
||||
// Allow \ as the first char for atom to support system flags
|
||||
// Allow % to support LIST '' %
|
||||
if (imapFormalSyntax['ATOM-CHAR']().indexOf(chr) < 0 && chr !== '\\' && chr !== '%') {
|
||||
throw new Error('Unexpected char at position ' + (this.pos + i));
|
||||
}
|
||||
|
||||
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
||||
this.currentNode.type = 'ATOM';
|
||||
this.currentNode.value = chr;
|
||||
this.state = 'ATOM';
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ATOM':
|
||||
|
||||
// space finishes an atom
|
||||
if (chr === ' ') {
|
||||
this.currentNode.endPos = this.pos + i - 1;
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
this.state = 'NORMAL';
|
||||
break;
|
||||
}
|
||||
|
||||
//
|
||||
if (
|
||||
this.currentNode.parentNode &&
|
||||
(
|
||||
(chr === ')' && this.currentNode.parentNode.type === 'LIST') ||
|
||||
(chr === ']' && this.currentNode.parentNode.type === 'SECTION')
|
||||
)
|
||||
) {
|
||||
this.currentNode.endPos = this.pos + i - 1;
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
|
||||
this.currentNode.closed = true;
|
||||
this.currentNode.endPos = this.pos + i;
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
this.state = 'NORMAL';
|
||||
|
||||
checkSP();
|
||||
break;
|
||||
}
|
||||
|
||||
if ((chr === ',' || chr === ':') && this.currentNode.value.match(/^\d+$/)) {
|
||||
this.currentNode.type = 'SEQUENCE';
|
||||
this.currentNode.closed = true;
|
||||
this.state = 'SEQUENCE';
|
||||
}
|
||||
|
||||
// [ starts a section group for this element
|
||||
if (chr === '[') {
|
||||
// allowed only for selected elements
|
||||
if (['BODY', 'BODY.PEEK'].indexOf(this.currentNode.value.toUpperCase()) < 0) {
|
||||
throw new Error('Unexpected section start char [ at position ' + this.pos);
|
||||
}
|
||||
this.currentNode.endPos = this.pos + i;
|
||||
this.currentNode = this.createNode(this.currentNode.parentNode, this.pos + i);
|
||||
this.currentNode.type = 'SECTION';
|
||||
this.currentNode.closed = false;
|
||||
this.state = 'NORMAL';
|
||||
break;
|
||||
}
|
||||
|
||||
if (chr === '<') {
|
||||
throw new Error('Unexpected start of partial at position ' + this.pos);
|
||||
}
|
||||
|
||||
// if the char is not ATOM compatible, throw. Allow \* as an exception
|
||||
if (imapFormalSyntax['ATOM-CHAR']().indexOf(chr) < 0 && chr !== ']' && !(chr === '*' && this.currentNode.value === '\\')) {
|
||||
throw new Error('Unexpected char at position ' + (this.pos + i));
|
||||
} else if (this.currentNode.value === '\\*') {
|
||||
throw new Error('Unexpected char at position ' + (this.pos + i));
|
||||
}
|
||||
|
||||
this.currentNode.value += chr;
|
||||
break;
|
||||
|
||||
case 'STRING':
|
||||
|
||||
// DQUOTE ends the string sequence
|
||||
if (chr === '"') {
|
||||
this.currentNode.endPos = this.pos + i;
|
||||
this.currentNode.closed = true;
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
this.state = 'NORMAL';
|
||||
|
||||
checkSP();
|
||||
break;
|
||||
}
|
||||
|
||||
// \ Escapes the following char
|
||||
if (chr === '\\') {
|
||||
i++;
|
||||
if (i >= len) {
|
||||
throw new Error('Unexpected end of input at position ' + (this.pos + i));
|
||||
}
|
||||
chr = this.str.charAt(i);
|
||||
}
|
||||
|
||||
/* // skip this check, otherwise the parser might explode on binary input
|
||||
if (imapFormalSyntax['TEXT-CHAR']().indexOf(chr) < 0) {
|
||||
throw new Error('Unexpected char at position ' + (this.pos + i));
|
||||
}
|
||||
*/
|
||||
|
||||
this.currentNode.value += chr;
|
||||
break;
|
||||
|
||||
case 'PARTIAL':
|
||||
if (chr === '>') {
|
||||
if (this.currentNode.value.substr(-1) === '.') {
|
||||
throw new Error('Unexpected end of partial at position ' + this.pos);
|
||||
}
|
||||
this.currentNode.endPos = this.pos + i;
|
||||
this.currentNode.closed = true;
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
this.state = 'NORMAL';
|
||||
checkSP();
|
||||
break;
|
||||
}
|
||||
|
||||
if (chr === '.' && (!this.currentNode.value.length || this.currentNode.value.match(/\./))) {
|
||||
throw new Error('Unexpected partial separator . at position ' + this.pos);
|
||||
}
|
||||
|
||||
if (imapFormalSyntax.DIGIT().indexOf(chr) < 0 && chr !== '.') {
|
||||
throw new Error('Unexpected char at position ' + (this.pos + i));
|
||||
}
|
||||
|
||||
if (this.currentNode.value.match(/^0$|\.0$/) && chr !== '.') {
|
||||
throw new Error('Invalid partial at position ' + (this.pos + i));
|
||||
}
|
||||
|
||||
this.currentNode.value += chr;
|
||||
break;
|
||||
|
||||
case 'LITERAL':
|
||||
if (this.currentNode.started) {
|
||||
//if(imapFormalSyntax['CHAR8']().indexOf(chr) < 0){
|
||||
if (chr === '\u0000') {
|
||||
throw new Error('Unexpected \\x00 at position ' + (this.pos + i));
|
||||
}
|
||||
this.currentNode.value += chr;
|
||||
|
||||
if (this.currentNode.value.length >= this.currentNode.literalLength) {
|
||||
this.currentNode.endPos = this.pos + i;
|
||||
this.currentNode.closed = true;
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
this.state = 'NORMAL';
|
||||
checkSP();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (chr === '+' && this.options.literalPlus) {
|
||||
this.currentNode.literalPlus = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (chr === '}') {
|
||||
if (!('literalLength' in this.currentNode)) {
|
||||
throw new Error('Unexpected literal prefix end char } at position ' + (this.pos + i));
|
||||
}
|
||||
if (this.str.charAt(i + 1) === '\n') {
|
||||
i++;
|
||||
} else if (this.str.charAt(i + 1) === '\r' && this.str.charAt(i + 2) === '\n') {
|
||||
i += 2;
|
||||
} else {
|
||||
throw new Error('Unexpected char at position ' + (this.pos + i));
|
||||
}
|
||||
this.currentNode.literalLength = Number(this.currentNode.literalLength);
|
||||
this.currentNode.started = true;
|
||||
|
||||
if (!this.currentNode.literalLength) {
|
||||
// special case where literal content length is 0
|
||||
// close the node right away, do not wait for additional input
|
||||
this.currentNode.endPos = this.pos + i;
|
||||
this.currentNode.closed = true;
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
this.state = 'NORMAL';
|
||||
checkSP();
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (imapFormalSyntax.DIGIT().indexOf(chr) < 0) {
|
||||
throw new Error('Unexpected char at position ' + (this.pos + i));
|
||||
}
|
||||
if (this.currentNode.literalLength === '0') {
|
||||
throw new Error('Invalid literal at position ' + (this.pos + i));
|
||||
}
|
||||
this.currentNode.literalLength = (this.currentNode.literalLength || '') + chr;
|
||||
break;
|
||||
|
||||
case 'SEQUENCE':
|
||||
// space finishes the sequence set
|
||||
if (chr === ' ') {
|
||||
if (!this.currentNode.value.substr(-1).match(/\d/) && this.currentNode.value.substr(-1) !== '*') {
|
||||
throw new Error('Unexpected whitespace at position ' + (this.pos + i));
|
||||
}
|
||||
|
||||
if (this.currentNode.value.substr(-1) === '*' && this.currentNode.value.substr(-2, 1) !== ':') {
|
||||
throw new Error('Unexpected whitespace at position ' + (this.pos + i));
|
||||
}
|
||||
|
||||
this.currentNode.closed = true;
|
||||
this.currentNode.endPos = this.pos + i - 1;
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
this.state = 'NORMAL';
|
||||
break;
|
||||
} else if (this.currentNode.parentNode &&
|
||||
chr === ']' &&
|
||||
this.currentNode.parentNode.type === 'SECTION') {
|
||||
this.currentNode.endPos = this.pos + i - 1;
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
|
||||
this.currentNode.closed = true;
|
||||
this.currentNode.endPos = this.pos + i;
|
||||
this.currentNode = this.currentNode.parentNode;
|
||||
this.state = 'NORMAL';
|
||||
|
||||
checkSP();
|
||||
break;
|
||||
}
|
||||
|
||||
if (chr === ':') {
|
||||
if (!this.currentNode.value.substr(-1).match(/\d/) && this.currentNode.value.substr(-1) !== '*') {
|
||||
throw new Error('Unexpected range separator : at position ' + (this.pos + i));
|
||||
}
|
||||
} else if (chr === '*') {
|
||||
if ([',', ':'].indexOf(this.currentNode.value.substr(-1)) < 0) {
|
||||
throw new Error('Unexpected range wildcard at position ' + (this.pos + i));
|
||||
}
|
||||
} else if (chr === ',') {
|
||||
if (!this.currentNode.value.substr(-1).match(/\d/) && this.currentNode.value.substr(-1) !== '*') {
|
||||
throw new Error('Unexpected sequence separator , at position ' + (this.pos + i));
|
||||
}
|
||||
if (this.currentNode.value.substr(-1) === '*' && this.currentNode.value.substr(-2, 1) !== ':') {
|
||||
throw new Error('Unexpected sequence separator , at position ' + (this.pos + i));
|
||||
}
|
||||
} else if (!chr.match(/\d/)) {
|
||||
throw new Error('Unexpected char at position ' + (this.pos + i));
|
||||
}
|
||||
|
||||
if (chr.match(/\d/) && this.currentNode.value.substr(-1) === '*') {
|
||||
throw new Error('Unexpected number at position ' + (this.pos + i));
|
||||
}
|
||||
|
||||
this.currentNode.value += chr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class ParserInstance {
|
||||
|
||||
constructor(input, options) {
|
||||
this.input = (input || '').toString();
|
||||
this.options = options || {};
|
||||
this.remainder = this.input;
|
||||
this.pos = 0;
|
||||
}
|
||||
|
||||
getTag() {
|
||||
if (!this.tag) {
|
||||
this.tag = this.getElement(imapFormalSyntax.tag() + '*+', true);
|
||||
}
|
||||
return this.tag;
|
||||
}
|
||||
|
||||
getCommand() {
|
||||
let responseCode;
|
||||
|
||||
if (!this.command) {
|
||||
this.command = this.getElement(imapFormalSyntax.command());
|
||||
}
|
||||
|
||||
switch ((this.command || '').toString().toUpperCase()) {
|
||||
case 'OK':
|
||||
case 'NO':
|
||||
case 'BAD':
|
||||
case 'PREAUTH':
|
||||
case 'BYE':
|
||||
responseCode = this.remainder.match(/^ \[(?:[^\]]*\])+/);
|
||||
if (responseCode) {
|
||||
this.humanReadable = this.remainder.substr(responseCode[0].length).trim();
|
||||
this.remainder = responseCode[0];
|
||||
} else {
|
||||
this.humanReadable = this.remainder.trim();
|
||||
this.remainder = '';
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return this.command;
|
||||
}
|
||||
|
||||
getElement(syntax) {
|
||||
let match, element, errPos;
|
||||
if (this.remainder.match(/^\s/)) {
|
||||
throw new Error('Unexpected whitespace at position ' + this.pos);
|
||||
}
|
||||
|
||||
if ((match = this.remainder.match(/^[^\s]+(?=\s|$)/))) {
|
||||
element = match[0];
|
||||
|
||||
if ((errPos = imapFormalSyntax.verify(element, syntax)) >= 0) {
|
||||
throw new Error('Unexpected char at position ' + (this.pos + errPos));
|
||||
}
|
||||
} else {
|
||||
throw new Error('Unexpected end of input at position ' + this.pos);
|
||||
}
|
||||
|
||||
this.pos += match[0].length;
|
||||
this.remainder = this.remainder.substr(match[0].length);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
getSpace() {
|
||||
if (!this.remainder.length) {
|
||||
throw new Error('Unexpected end of input at position ' + this.pos);
|
||||
}
|
||||
|
||||
if (imapFormalSyntax.verify(this.remainder.charAt(0), imapFormalSyntax.SP()) >= 0) {
|
||||
throw new Error('Unexpected char at position ' + this.pos);
|
||||
}
|
||||
|
||||
this.pos++;
|
||||
this.remainder = this.remainder.substr(1);
|
||||
}
|
||||
|
||||
getAttributes() {
|
||||
if (!this.remainder.length) {
|
||||
throw new Error('Unexpected end of input at position ' + this.pos);
|
||||
}
|
||||
|
||||
if (this.remainder.match(/^\s/)) {
|
||||
throw new Error('Unexpected whitespace at position ' + this.pos);
|
||||
}
|
||||
|
||||
return new TokenParser(this, this.pos, this.remainder, this.options).getAttributes();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function (command, options) {
|
||||
let parser, response = {};
|
||||
|
||||
options = options || {};
|
||||
|
||||
parser = new ParserInstance(command, options);
|
||||
|
||||
response.tag = parser.getTag();
|
||||
parser.getSpace();
|
||||
response.command = parser.getCommand();
|
||||
|
||||
if (['UID', 'AUTHENTICATE'].indexOf((response.command || '').toUpperCase()) >= 0) {
|
||||
parser.getSpace();
|
||||
response.command += ' ' + parser.getElement(imapFormalSyntax.command());
|
||||
}
|
||||
|
||||
if (parser.remainder.trim().length) {
|
||||
parser.getSpace();
|
||||
response.attributes = parser.getAttributes();
|
||||
}
|
||||
|
||||
if (parser.humanReadable) {
|
||||
response.attributes = (response.attributes || []).concat({
|
||||
type: 'TEXT',
|
||||
value: parser.humanReadable
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
228
imap-core/lib/imap-command.js
Normal file
228
imap-core/lib/imap-command.js
Normal file
|
@ -0,0 +1,228 @@
|
|||
'use strict';
|
||||
|
||||
let imapHandler = require('./handler/imap-handler');
|
||||
|
||||
const MAX_MESSAGE_SIZE = 1 * 1024 * 1024;
|
||||
|
||||
let commands = new Map([
|
||||
/*eslint-disable global-require*/
|
||||
// require must normally be on top of the module
|
||||
['NOOP', require('./commands/noop')],
|
||||
['CAPABILITY', require('./commands/capability')],
|
||||
['LOGOUT', require('./commands/logout')],
|
||||
['ID', require('./commands/id')],
|
||||
['STARTTLS', require('./commands/starttls')],
|
||||
['LOGIN', require('./commands/login')],
|
||||
['AUTHENTICATE PLAIN', require('./commands/authenticate-plain')],
|
||||
['NAMESPACE', require('./commands/namespace')],
|
||||
['LIST', require('./commands/list')],
|
||||
['LSUB', require('./commands/lsub')],
|
||||
['SUBSCRIBE', require('./commands/subscribe')],
|
||||
['UNSUBSCRIBE', require('./commands/unsubscribe')],
|
||||
['CREATE', require('./commands/create')],
|
||||
['DELETE', require('./commands/delete')],
|
||||
['RENAME', require('./commands/rename')],
|
||||
['SELECT', require('./commands/select')],
|
||||
['EXAMINE', require('./commands/select')],
|
||||
['IDLE', require('./commands/idle')],
|
||||
['CHECK', require('./commands/check')],
|
||||
['STATUS', require('./commands/status')],
|
||||
['APPEND', require('./commands/append')],
|
||||
['STORE', require('./commands/store')],
|
||||
['UID STORE', require('./commands/uid-store')],
|
||||
['EXPUNGE', require('./commands/expunge')],
|
||||
['UID EXPUNGE', require('./commands/uid-expunge')],
|
||||
['CLOSE', require('./commands/close')],
|
||||
['UNSELECT', require('./commands/unselect')],
|
||||
['COPY', require('./commands/copy')],
|
||||
['UID COPY', require('./commands/copy')],
|
||||
['FETCH', require('./commands/fetch')],
|
||||
['UID FETCH', require('./commands/fetch')],
|
||||
['SEARCH', require('./commands/search')],
|
||||
['UID SEARCH', require('./commands/search')],
|
||||
['ENABLE', require('./commands/enable')]
|
||||
/*eslint-enable global-require*/
|
||||
]);
|
||||
|
||||
class IMAPCommand {
|
||||
|
||||
constructor(connection) {
|
||||
this.connection = connection;
|
||||
this.payload = '';
|
||||
this.first = true;
|
||||
}
|
||||
|
||||
append(command, callback) {
|
||||
let chunks = [];
|
||||
let chunklen = 0;
|
||||
|
||||
this.payload += command.value;
|
||||
|
||||
if (this.first) {
|
||||
// fetch tag and command name
|
||||
this.first = false;
|
||||
|
||||
// only check payload if it is a regular command, not input for something else
|
||||
if (typeof this.connection._nextHandler !== 'function') {
|
||||
let match = /^([^\s]+)(?:\s+((?:AUTHENTICATE |UID )?[^\s]+)|$)/i.exec(command.value) || [];
|
||||
this.tag = match[1];
|
||||
this.command = (match[2] || '').trim().toUpperCase();
|
||||
|
||||
if (!this.command || !this.tag) {
|
||||
this.connection.send('* BAD Invalid tag');
|
||||
return callback(new Error('Invalid tag'));
|
||||
}
|
||||
|
||||
if (!commands.has(this.command)) {
|
||||
this.connection.send(this.tag + ' BAD Unknown command: ' + this.command);
|
||||
return callback(new Error('Unknown command'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (command.literal) {
|
||||
if (
|
||||
// Allow large literals for selected commands only
|
||||
(['APPEND'].indexOf(this.command) < 0 && command.expecting > 1024) ||
|
||||
// Deny all literals bigger than maxMessage
|
||||
command.expecting > Math.max(Number(this.connection._server.options.maxMessage) || 0, MAX_MESSAGE_SIZE)) {
|
||||
|
||||
this.connection._server.logger.debug('[%s] C:', this.connection.id, this.payload);
|
||||
this.connection.send(this.tag + ' NO Literal too big');
|
||||
return callback(new Error('Literal too big'));
|
||||
}
|
||||
|
||||
// Accept literal input
|
||||
this.connection.send('+ Go ahead');
|
||||
|
||||
// currently the stream is buffered into a large string and thats it.
|
||||
// in the future we might consider some kind of actual stream usage
|
||||
command.literal.on('data', chunk => {
|
||||
chunks.push(chunk);
|
||||
chunklen += chunk.length;
|
||||
});
|
||||
|
||||
command.literal.on('end', () => {
|
||||
this.payload += '\r\n' + Buffer.concat(chunks, chunklen).toString('binary');
|
||||
command.readyCallback(); // call this once stream is fully processed and ready to accept next data
|
||||
});
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
end(command, callback) {
|
||||
let callbackSent = false;
|
||||
let next = err => {
|
||||
if (!callbackSent) {
|
||||
callbackSent = true;
|
||||
return callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
this.append(command, err => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
this.connection._server.logger.debug('[%s] C:', this.connection.id, this.payload);
|
||||
|
||||
// check if the payload needs to be directod to a preset handler
|
||||
if (typeof this.connection._nextHandler === 'function') {
|
||||
return this.connection._nextHandler(this.payload, next);
|
||||
}
|
||||
|
||||
try {
|
||||
this.parsed = imapHandler.parser(this.payload);
|
||||
} catch (E) {
|
||||
this.connection.send(this.tag + ' BAD ' + E.message);
|
||||
return next();
|
||||
}
|
||||
|
||||
let handler = commands.get(this.command);
|
||||
|
||||
this.validateCommand(this.parsed, handler, err => {
|
||||
if (err) {
|
||||
this.connection.send(this.tag + ' ' + (err.response || 'BAD') + ' ' + err.message);
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (typeof handler.handler === 'function') {
|
||||
handler.handler.call(this.connection, this.parsed, (err, response) => {
|
||||
if (err) {
|
||||
this.connection.send(this.tag + ' ' + (err.response || 'BAD') + ' ' + err.message);
|
||||
return next(err);
|
||||
}
|
||||
|
||||
// send EXPUNGE, EXISTS etc queued notices
|
||||
this.sendNotifications(handler, () => {
|
||||
|
||||
// send command ready response
|
||||
this.connection.writeStream.write({
|
||||
tag: this.tag,
|
||||
command: response.response,
|
||||
attributes: [].concat(response.code ? {
|
||||
type: 'SECTION',
|
||||
section: [{
|
||||
type: 'TEXT',
|
||||
value: response.code
|
||||
}]
|
||||
} : []).concat({
|
||||
type: 'TEXT',
|
||||
value: response.message || this.command + ' completed'
|
||||
})
|
||||
});
|
||||
|
||||
next();
|
||||
});
|
||||
}, next);
|
||||
} else {
|
||||
this.connection.send(this.tag + ' NO Not implemented: ' + this.command);
|
||||
return next();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
sendNotifications(handler, callback) {
|
||||
if (this.connection.state !== 'Selected' || !!handler.disableNotifications) {
|
||||
// nothing to advertise if not in Selected state
|
||||
return callback();
|
||||
}
|
||||
|
||||
this.connection.emitNotifications();
|
||||
|
||||
return callback();
|
||||
}
|
||||
|
||||
validateCommand(parsed, handler, callback) {
|
||||
let schema = handler.schema || [];
|
||||
let maxArgs = schema.length;
|
||||
let minArgs = schema.filter(item => !item.optional).length;
|
||||
|
||||
// Check if the command can be run in current state
|
||||
if (handler.state && [].concat(handler.state || []).indexOf(this.connection.state) < 0) {
|
||||
return callback(new Error(parsed.command.toUpperCase() + ' not allowed now'));
|
||||
}
|
||||
|
||||
if (handler.schema === false) {
|
||||
//schema check is disabled
|
||||
return callback();
|
||||
}
|
||||
|
||||
// Deny commands with too many arguments
|
||||
if (parsed.attributes && parsed.attributes.length > maxArgs) {
|
||||
return callback(new Error('Too many arguments provided'));
|
||||
}
|
||||
|
||||
// Deny commands with too little arguments
|
||||
if ((parsed.attributes && parsed.attributes.length || 0) < minArgs) {
|
||||
return callback(new Error('Not enough arguments provided'));
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports.IMAPCommand = IMAPCommand;
|
49
imap-core/lib/imap-composer.js
Normal file
49
imap-core/lib/imap-composer.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
'use strict';
|
||||
|
||||
let imapHandler = require('./handler/imap-handler');
|
||||
let Transform = require('stream').Transform;
|
||||
|
||||
class IMAPComposer extends Transform {
|
||||
|
||||
constructor(options) {
|
||||
super();
|
||||
Transform.call(this, {
|
||||
writableObjectMode: true
|
||||
});
|
||||
this.connection = options.connection;
|
||||
}
|
||||
|
||||
|
||||
_transform(obj, encoding, done) {
|
||||
if (!obj) {
|
||||
return done();
|
||||
}
|
||||
|
||||
if (typeof obj.pipe === 'function') {
|
||||
// pipe stream to socket and wait until it finishes before continuing
|
||||
this.connection._server.logger.debug('[%s] S: <pipe message stream to socket>', this.connection.id);
|
||||
obj.pipe(this.connection._socket, {
|
||||
end: false
|
||||
});
|
||||
obj.on('error', err => this.emit('error', err));
|
||||
obj.on('end', () => {
|
||||
this.push('\r\n');
|
||||
done();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let compiled = imapHandler.compiler(obj);
|
||||
|
||||
this.connection._server.logger.debug('[%s] S:', this.connection.id, compiled);
|
||||
this.push(new Buffer(compiled + '\r\n', 'binary'));
|
||||
done();
|
||||
}
|
||||
|
||||
_flush(done) {
|
||||
done();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports.IMAPComposer = IMAPComposer;
|
622
imap-core/lib/imap-connection.js
Normal file
622
imap-core/lib/imap-connection.js
Normal file
|
@ -0,0 +1,622 @@
|
|||
'use strict';
|
||||
|
||||
let IMAPStream = require('./imap-stream').IMAPStream;
|
||||
let IMAPCommand = require('./imap-command').IMAPCommand;
|
||||
let IMAPComposer = require('./imap-composer').IMAPComposer;
|
||||
let imapTools = require('./imap-tools');
|
||||
let search = require('./search');
|
||||
let dns = require('dns');
|
||||
let crypto = require('crypto');
|
||||
let os = require('os');
|
||||
let EventEmitter = require('events').EventEmitter;
|
||||
let packageInfo = require('../../package');
|
||||
|
||||
const SOCKET_TIMEOUT = 30 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Creates a handler for new socket
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} server Server instance
|
||||
* @param {Object} socket Socket instance
|
||||
*/
|
||||
class IMAPConnection extends EventEmitter {
|
||||
|
||||
constructor(server, socket) {
|
||||
super();
|
||||
|
||||
// Random session ID, used for logging
|
||||
this.id = crypto.randomBytes(9).toString('base64');
|
||||
|
||||
this._server = server;
|
||||
this._socket = socket;
|
||||
|
||||
this.writeStream = new IMAPComposer({
|
||||
connection: this
|
||||
});
|
||||
this.writeStream.pipe(this._socket);
|
||||
this.writeStream.on('error', this._onError.bind(this));
|
||||
|
||||
// session data (envelope, user etc.)
|
||||
this.session = false;
|
||||
|
||||
// If true then the connection is currently being upgraded to TLS
|
||||
this._upgrading = false;
|
||||
|
||||
// Parser instance for the incoming stream
|
||||
this._parser = new IMAPStream();
|
||||
|
||||
// Set handler for incoming commands
|
||||
this._parser.oncommand = this._onCommand.bind(this);
|
||||
|
||||
// Manage multi part command
|
||||
this._currentCommand = false;
|
||||
|
||||
// If set, then data payload is not executed as a command but as an argument for this function
|
||||
this._nextHandler = false;
|
||||
|
||||
// If true, then the connection is using TLS
|
||||
this.secure = !!this._server.options.secure;
|
||||
|
||||
// Store remote address for later usage
|
||||
this.remoteAddress = this._socket.remoteAddress;
|
||||
|
||||
// Server hostname for the greegins
|
||||
this.name = this._server.options.name || os.hostname();
|
||||
|
||||
this.state = 'Not Authenticated';
|
||||
|
||||
this._listenerData = false;
|
||||
|
||||
// selected mailbox metadata
|
||||
this.selected = false;
|
||||
|
||||
// ignore timeouts if true
|
||||
this.idling = false;
|
||||
|
||||
// indicates if CONDSTORE is enabled for the session
|
||||
this.condstoreEnabled = false;
|
||||
|
||||
// Resolved hostname for remote IP address
|
||||
this.clientHostname = false;
|
||||
|
||||
// increment connection count
|
||||
this._closing = false;
|
||||
this._closed = false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initiates the connection. Checks connection limits and reverse resolves client hostname. The client
|
||||
* is not allowed to send anything before init has finished otherwise 'You talk too soon' error is returned
|
||||
*/
|
||||
init() {
|
||||
// Setup event handlers for the socket
|
||||
this._setListeners();
|
||||
|
||||
// Resolve hostname for the remote IP
|
||||
// we do not care for errors as we consider the ip as unresolved in this case, no big deal
|
||||
dns.reverse(this.remoteAddress, (err, hostnames) => { // eslint-disable-line handle-callback-err
|
||||
if (this._closing || this._closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.clientHostname = hostnames && hostnames.shift() || '[' + this.remoteAddress + ']';
|
||||
|
||||
this._startSession();
|
||||
|
||||
this._server.logger.info('[%s] Connection from %s', this.id, this.clientHostname);
|
||||
this.send('* OK ' + (this._server.options.id && this._server.options.id.name || packageInfo.name) + ' ready');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send data to socket
|
||||
*
|
||||
* @param {Number} code Response code
|
||||
* @param {String|Array} data If data is Array, send a multi-line response
|
||||
*/
|
||||
send(payload, callback) {
|
||||
if (this._socket && this._socket.writable) {
|
||||
this._socket.write(payload + '\r\n', 'binary', callback);
|
||||
this._server.logger.debug('[%s] S:', this.id, payload);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close socket
|
||||
*/
|
||||
close() {
|
||||
if (!this._socket.destroyed && this._socket.writable) {
|
||||
this._socket.end();
|
||||
}
|
||||
|
||||
this._server.connections.delete(this);
|
||||
|
||||
this._closing = true;
|
||||
}
|
||||
|
||||
// PRIVATE METHODS
|
||||
|
||||
/**
|
||||
* Setup socket event handlers
|
||||
*/
|
||||
_setListeners() {
|
||||
this._socket.on('close', this._onClose.bind(this));
|
||||
this._socket.on('end', this._onEnd.bind(this));
|
||||
this._socket.on('error', this._onError.bind(this));
|
||||
this._socket.setTimeout(this._server.options.socketTimeout || SOCKET_TIMEOUT, this._onTimeout.bind(this));
|
||||
this._socket.pipe(this._parser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired when the socket is closed
|
||||
* @event
|
||||
*/
|
||||
_onEnd() {
|
||||
this._server.logger.info('[%s] Connection END', this.id);
|
||||
if (!this._closed) {
|
||||
this._onClose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired when the socket is closed
|
||||
* @event
|
||||
*/
|
||||
_onClose( /* hadError */ ) {
|
||||
if (this._closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._parser = false;
|
||||
|
||||
this.state = 'Closed';
|
||||
|
||||
if (this._dataStream) {
|
||||
this._dataStream.unpipe();
|
||||
this._dataStream = null;
|
||||
}
|
||||
|
||||
if (this._listenerData) {
|
||||
this._listenerData.clear();
|
||||
}
|
||||
|
||||
this._server.connections.delete(this);
|
||||
|
||||
if (this._closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._closed = true;
|
||||
this._closing = false;
|
||||
|
||||
this._server.logger.info('[%s] Connection closed to %s', this.id, this.clientHostname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired when an error occurs with the socket
|
||||
*
|
||||
* @event
|
||||
* @param {Error} err Error object
|
||||
*/
|
||||
_onError(err) {
|
||||
if (err.code === 'ECONNRESET' || err.code === 'EPIPE') {
|
||||
this.close(); // mark connection as 'closing'
|
||||
return;
|
||||
}
|
||||
|
||||
this._server.logger.error('[%s] %s', this.id, err.message);
|
||||
this.emit('error', err);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired when socket timeouts. Closes connection
|
||||
*
|
||||
* @event
|
||||
*/
|
||||
_onTimeout() {
|
||||
this._server.logger.info('[%s] Connection TIMEOUT', this.id);
|
||||
if (this.idling) {
|
||||
return; // ignore timeouts when IDLEing
|
||||
}
|
||||
this.send('* BYE Idle timeout, closing connection');
|
||||
this.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a selected command is available and ivokes it
|
||||
*
|
||||
* @param {Buffer} command Single line of data from the client
|
||||
* @param {Function} callback Callback to run once the command is processed
|
||||
*/
|
||||
_onCommand(command, callback) {
|
||||
let currentCommand = this._currentCommand;
|
||||
|
||||
callback = callback || (() => false);
|
||||
|
||||
if (this._upgrading) {
|
||||
// ignore any commands before TLS upgrade is finished
|
||||
return callback();
|
||||
}
|
||||
|
||||
if (!currentCommand) {
|
||||
this._currentCommand = currentCommand = new IMAPCommand(this);
|
||||
}
|
||||
|
||||
if (!command.final) {
|
||||
currentCommand.append(command, callback);
|
||||
} else {
|
||||
this._currentCommand = false;
|
||||
currentCommand.end(command, callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a new session
|
||||
*/
|
||||
_startSession() {
|
||||
this.session = {
|
||||
|
||||
id: this.id,
|
||||
|
||||
selected: this.selected,
|
||||
|
||||
remoteAddress: this.remoteAddress,
|
||||
clientHostname: this.clientHostname,
|
||||
writeStream: this.writeStream,
|
||||
socket: this._socket,
|
||||
|
||||
formatResponse: this.formatResponse.bind(this),
|
||||
getQueryResponse: imapTools.getQueryResponse,
|
||||
matchSearchQuery: search.matchSearchQuery
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up notification listener from upstream
|
||||
*
|
||||
* @param {Function} done Called once listeners are updated
|
||||
*/
|
||||
updateNotificationListener(done) {
|
||||
if (this._listenerData) {
|
||||
if (!this.selected || this._listenerData.mailbox !== this.selected.mailbox) {
|
||||
// registered against some mailbox, unregister from it
|
||||
this._listenerData.clear();
|
||||
} else if (this._listenerData.mailbox === this.selected.mailbox) {
|
||||
// already registered
|
||||
return done();
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.selected) {
|
||||
this._listenerData = false;
|
||||
return done();
|
||||
}
|
||||
|
||||
let cleared = false;
|
||||
let listenerData = this._listenerData = {
|
||||
mailbox: this.selected.mailbox,
|
||||
lock: false,
|
||||
clear: () => {
|
||||
this._server.notifier.removeListener(this.session, listenerData.mailbox, listenerData.callback);
|
||||
if (listenerData === this._listenerData) {
|
||||
this._listenerData = false;
|
||||
}
|
||||
listenerData = false;
|
||||
cleared = true;
|
||||
},
|
||||
callback: message => {
|
||||
if (message) {
|
||||
if (this.selected && message.action === 'DELETE' && message.mailbox === this.selected.mailbox) {
|
||||
this.send('* BYE Selected mailbox was deleted, have to disconnect');
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (listenerData.lock) {
|
||||
// race condition, do not allow fetching data before previous fetch is finished
|
||||
return;
|
||||
}
|
||||
|
||||
if (cleared) {
|
||||
// some kind of a race condition, just ignore
|
||||
return;
|
||||
}
|
||||
|
||||
// if not selected anymore, remove itself
|
||||
if (this.state !== 'Selected' || !this.selected) {
|
||||
listenerData.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
listenerData.lock = true;
|
||||
this._server.notifier.getUpdates(this.session, this._listenerData.mailbox, this.selected.modifyIndex, (err, updates) => {
|
||||
if (cleared) {
|
||||
// client probably switched mailboxes while processing, just ignore all results
|
||||
return;
|
||||
}
|
||||
listenerData.lock = false;
|
||||
|
||||
if (err) {
|
||||
this._server.logger.info('[%s] Notification Error: %s', this.id, err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
// if not selected anymore, remove itself
|
||||
if (this.state !== 'Selected' || !this.selected) {
|
||||
listenerData.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!updates || !updates.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// store new incremental modify index
|
||||
if (updates[updates.length - 1].modseq > this.selected.modifyIndex) {
|
||||
this.selected.modifyIndex = updates[updates.length - 1].modseq;
|
||||
}
|
||||
|
||||
// append received notifications to the list
|
||||
this.selected.notifications = this.selected.notifications.concat(updates);
|
||||
if (this.idling) {
|
||||
// when idling emit notifications immediatelly
|
||||
this.emitNotifications();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this._server.notifier.addListener(this.session, this._listenerData.mailbox, this._listenerData.callback);
|
||||
|
||||
return done();
|
||||
}
|
||||
|
||||
// send notifications to client
|
||||
emitNotifications() {
|
||||
if (this.state !== 'Selected' || !this.selected || !this.selected.notifications.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
let existsResponse;
|
||||
|
||||
// show notifications
|
||||
this._server.logger.info('[%s] Pending notifications: %s', this.id, this.selected.notifications.length);
|
||||
|
||||
// find UIDs that are both added and removed
|
||||
let added = new Set(); // added UIDs
|
||||
let removed = new Set(); // removed UIDs
|
||||
let skip = new Set(); // UIDs that are removed before ever seen
|
||||
|
||||
for (let i = 0, len = this.selected.notifications.length; i < len; i++) {
|
||||
let update = this.selected.notifications[i];
|
||||
if (update.command === 'EXISTS') {
|
||||
added.add(update.uid);
|
||||
} else if (update.command === 'EXPUNGE') {
|
||||
removed.add(update.uid);
|
||||
}
|
||||
}
|
||||
|
||||
removed.forEach(uid => {
|
||||
if (added.has(uid)) {
|
||||
skip.add(uid);
|
||||
}
|
||||
});
|
||||
|
||||
// filter multiple FETCH calls, only keep latest, otherwise might mess up MODSEQ responses
|
||||
let fetches = new Set();
|
||||
for (let i = this.selected.notifications.length - 1; i >= 0; i--) {
|
||||
let update = this.selected.notifications[i];
|
||||
if (update.command === 'FETCH') {
|
||||
// skip multiple flag updates and updates for removed or newly added messages
|
||||
if (fetches.has(update.uid) || added.has(update.uid) || removed.has(update.uid)) {
|
||||
this.selected.notifications.splice(i, 1);
|
||||
} else {
|
||||
fetches.add(update.uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0, len = this.selected.notifications.length; i < len; i++) {
|
||||
let update = this.selected.notifications[i];
|
||||
|
||||
// skip unnecessary entries that are already removed
|
||||
if (skip.has(update.uid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (update.modseq > this.selected.modifyIndex) {
|
||||
this.selected.modifyIndex = update.modseq;
|
||||
}
|
||||
|
||||
this._server.logger.info('[%s] Processing notification: %s', this.id, JSON.stringify(update));
|
||||
|
||||
if (update.ignore === this.id) {
|
||||
continue; // skip this
|
||||
}
|
||||
|
||||
this._server.logger.info('[%s] UIDS: %s', this.id, JSON.stringify(this.selected.uidList));
|
||||
switch (update.command) {
|
||||
|
||||
case 'EXISTS':
|
||||
|
||||
// Generate the response but do not send it yet (EXIST response generation is needed to modify the UID list)
|
||||
// This way we can accumulate consecutive EXISTS responses into single one as
|
||||
// only the last one actually matters to the client
|
||||
existsResponse = this.formatResponse('EXISTS', update.uid);
|
||||
changed = false;
|
||||
|
||||
break;
|
||||
|
||||
case 'EXPUNGE':
|
||||
{
|
||||
let seq = (this.selected.uidList || []).indexOf(update.uid);
|
||||
this._server.logger.info('[%s] EXPUNGE %s', this.id, seq);
|
||||
if (seq >= 0) {
|
||||
let output = this.formatResponse('EXPUNGE', update.uid);
|
||||
this.writeStream.write(output);
|
||||
changed = true; // if no more EXISTS after this, then generate an additional EXISTS
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'FETCH':
|
||||
|
||||
this.writeStream.write(this.formatResponse('FETCH', update.uid, {
|
||||
flags: update.flags,
|
||||
modseq: this.selected.condstoreEnabled && update.modseq || false
|
||||
}));
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (existsResponse) {
|
||||
// send cached EXISTS response
|
||||
this.writeStream.write(existsResponse);
|
||||
existsResponse = false;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
this.writeStream.write({
|
||||
tag: '*',
|
||||
command: String(this.selected.uidList.length),
|
||||
attributes: [{
|
||||
type: 'atom',
|
||||
value: 'EXISTS'
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
// clear queue
|
||||
this.selected.notifications = [];
|
||||
|
||||
if (typeof this._server.onNotifications === 'function') {
|
||||
setImmediate(this._server.onNotifications.bind(this._server, this.selected.mailbox, this.selected.modifyIndex, this.session));
|
||||
}
|
||||
}
|
||||
|
||||
formatResponse(command, uid, data) {
|
||||
command = command.toUpperCase();
|
||||
let seq;
|
||||
|
||||
if (command === 'EXISTS') {
|
||||
this.selected.uidList.push(uid);
|
||||
seq = this.selected.uidList.length;
|
||||
} else {
|
||||
seq = (this.selected.uidList || []).indexOf(uid);
|
||||
if (seq < 0) {
|
||||
return false;
|
||||
}
|
||||
seq++;
|
||||
}
|
||||
|
||||
if (command === 'EXPUNGE') {
|
||||
this.selected.uidList.splice(seq - 1, 1);
|
||||
}
|
||||
|
||||
let response = {
|
||||
tag: '*',
|
||||
command: String(seq),
|
||||
attributes: [{
|
||||
type: 'atom',
|
||||
value: command
|
||||
}]
|
||||
};
|
||||
|
||||
if (data) {
|
||||
response.attributes.push([]);
|
||||
if ('query' in data) {
|
||||
// Response for FETCH command
|
||||
data.query.forEach((item, i) => {
|
||||
response.attributes[1].push(item.original);
|
||||
if (['flags', 'modseq'].indexOf(item.item) >= 0) {
|
||||
response.attributes[1].
|
||||
push([].concat(data.values[i] || []).map(value => ({
|
||||
type: 'ATOM',
|
||||
value: (value || value === 0 ? value : '').toString()
|
||||
})));
|
||||
} else if (Object.prototype.toString.call(data.values[i]) === '[object Date]') {
|
||||
response.attributes[1].push({
|
||||
type: 'ATOM',
|
||||
value: imapTools.formatInternalDate(data.values[i])
|
||||
});
|
||||
} else if (Array.isArray(data.values[i])) {
|
||||
response.attributes[1].push(data.values[i]);
|
||||
} else if (item.isLiteral) {
|
||||
if (data.values[i] && data.values[i].type === 'stream') {
|
||||
response.attributes[1].push({
|
||||
type: 'LITERAL',
|
||||
value: data.values[i].value,
|
||||
expectedLength: data.values[i].expectedLength,
|
||||
startFrom: data.values[i].startFrom,
|
||||
maxLength: data.values[i].maxLength
|
||||
});
|
||||
} else {
|
||||
response.attributes[1].push({
|
||||
type: 'LITERAL',
|
||||
value: data.values[i]
|
||||
});
|
||||
}
|
||||
} else if (data.values[i] === '') {
|
||||
response.attributes[1].push(data.values[i]);
|
||||
} else {
|
||||
response.attributes[1].push({
|
||||
type: 'ATOM',
|
||||
value: (data.values[i]).toString()
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Notification response
|
||||
Object.keys(data).forEach(key => {
|
||||
let value = data[key];
|
||||
key = key.toUpperCase();
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case 'FLAGS':
|
||||
value = [].concat(value || []).map(flag => (flag && flag.value ? flag : {
|
||||
type: 'ATOM',
|
||||
value: flag
|
||||
}));
|
||||
break;
|
||||
|
||||
case 'UID':
|
||||
value = value && value.value ? value : {
|
||||
type: 'ATOM',
|
||||
value: (value || '0').toString()
|
||||
};
|
||||
break;
|
||||
|
||||
case 'MODSEQ':
|
||||
value = [].concat(value && value.value ? value : {
|
||||
type: 'ATOM',
|
||||
value: (value || '0').toString()
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
response.attributes[1].push({
|
||||
type: 'ATOM',
|
||||
value: key
|
||||
});
|
||||
|
||||
response.attributes[1].push(value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Expose to the world
|
||||
module.exports.IMAPConnection = IMAPConnection;
|
184
imap-core/lib/imap-server.js
Normal file
184
imap-core/lib/imap-server.js
Normal file
|
@ -0,0 +1,184 @@
|
|||
'use strict';
|
||||
|
||||
let net = require('net');
|
||||
let tls = require('tls');
|
||||
let IMAPConnection = require('./imap-connection').IMAPConnection;
|
||||
let tlsOptions = require('./tls-options');
|
||||
let EventEmitter = require('events').EventEmitter;
|
||||
let util = require('util');
|
||||
let clone = require('clone');
|
||||
|
||||
const CLOSE_TIMEOUT = 1 * 1000; // how much to wait until pending connections are terminated
|
||||
|
||||
/**
|
||||
* Creates a IMAP server instance.
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} options Connection and IMAP optionsž
|
||||
*/
|
||||
class IMAPServer extends EventEmitter {
|
||||
|
||||
constructor(options) {
|
||||
super();
|
||||
|
||||
this.options = options ? clone(options) : {};
|
||||
|
||||
// apply TLS defaults if needed
|
||||
if (this.options.secure) {
|
||||
this.options = tlsOptions(this.options);
|
||||
}
|
||||
|
||||
// setup logger
|
||||
if ('logger' in this.options) {
|
||||
// use provided logger or use vanity logger if option is set to false
|
||||
this.logger = this.options.logger || {
|
||||
info: () => false,
|
||||
debug: () => false,
|
||||
error: () => false
|
||||
};
|
||||
} else {
|
||||
// create default console logger
|
||||
this.logger = this._createDefaultLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Timeout after close has been called until pending connections are forcibly closed
|
||||
*/
|
||||
this._closeTimeout = false;
|
||||
|
||||
/**
|
||||
* A set of all currently open connections
|
||||
*/
|
||||
this.connections = new Set();
|
||||
|
||||
// setup server listener and connection handler
|
||||
this.server = (this.options.secure ? tls : net).createServer(this.options, socket => {
|
||||
let connection = new IMAPConnection(this, socket);
|
||||
this.connections.add(connection);
|
||||
connection.on('error', this._onError.bind(this));
|
||||
connection.init();
|
||||
});
|
||||
|
||||
this._setListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start listening on selected port and interface
|
||||
*/
|
||||
listen(...args) {
|
||||
this.server.listen(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the server
|
||||
*
|
||||
* @param {Function} callback Callback to run once the server is fully closed
|
||||
*/
|
||||
close(callback) {
|
||||
let connections = this.connections.size;
|
||||
let timeout = this.options.closeTimeout || CLOSE_TIMEOUT;
|
||||
|
||||
// stop accepting new connections
|
||||
this.server.close(() => {
|
||||
clearTimeout(this._closeTimeout);
|
||||
callback();
|
||||
});
|
||||
|
||||
// close active connections
|
||||
if (connections) {
|
||||
this.logger.info('Server closing with %s pending connection%s, waiting %s seconds before terminating', connections, connections !== 1 ? 's' : '', timeout / 1000);
|
||||
}
|
||||
|
||||
this._closeTimeout = setTimeout(() => {
|
||||
connections = this.connections.size;
|
||||
if (connections) {
|
||||
this.logger.info('Closing %s pending connection%s to close the server', connections, connections !== 1 ? 's' : '');
|
||||
|
||||
this.connections.forEach(connection => {
|
||||
connection.send('* BYE System shutdown');
|
||||
connection.close();
|
||||
});
|
||||
}
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
// PRIVATE METHODS
|
||||
|
||||
/**
|
||||
* Generates a bunyan-like logger that prints to console
|
||||
*
|
||||
* @returns {Object} Bunyan logger instance
|
||||
*/
|
||||
_createDefaultLogger() {
|
||||
|
||||
let logger = {
|
||||
_print: (...args) => {
|
||||
let level = args.shift();
|
||||
let message;
|
||||
|
||||
if (args.length > 1) {
|
||||
message = util.format(...args);
|
||||
} else {
|
||||
message = args[0];
|
||||
}
|
||||
|
||||
console.log('[%s] %s: %s', // eslint-disable-line no-console
|
||||
new Date().toISOString().substr(0, 19).replace(/T/, ' '),
|
||||
level.toUpperCase(),
|
||||
message);
|
||||
}
|
||||
};
|
||||
|
||||
logger.info = logger._print.bind(null, 'info');
|
||||
logger.debug = logger._print.bind(null, 'debug');
|
||||
logger.error = logger._print.bind(null, 'error');
|
||||
|
||||
return logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup server event handlers
|
||||
*/
|
||||
_setListeners() {
|
||||
this.server.on('listening', this._onListening.bind(this));
|
||||
this.server.on('close', this._onClose.bind(this));
|
||||
this.server.on('error', this._onError.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when server started listening
|
||||
*
|
||||
* @event
|
||||
*/
|
||||
_onListening() {
|
||||
let address = this.server.address();
|
||||
this.logger.info(
|
||||
'%sIMAP Server listening on %s:%s',
|
||||
this.options.secure ? 'Secure ' : '',
|
||||
address.family === 'IPv4' ? address.address : '[' + address.address + ']',
|
||||
address.port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when server is closed
|
||||
*
|
||||
* @event
|
||||
*/
|
||||
_onClose() {
|
||||
this.logger.info('IMAP Server closed');
|
||||
this.emit('close');
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an error occurs with the server
|
||||
*
|
||||
* @event
|
||||
*/
|
||||
_onError(err) {
|
||||
this.emit('error', err);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Expose to the world
|
||||
module.exports.IMAPServer = IMAPServer;
|
171
imap-core/lib/imap-stream.js
Executable file
171
imap-core/lib/imap-stream.js
Executable file
|
@ -0,0 +1,171 @@
|
|||
'use strict';
|
||||
|
||||
let stream = require('stream');
|
||||
let Writable = stream.Writable;
|
||||
let PassThrough = stream.PassThrough;
|
||||
|
||||
/**
|
||||
* Incoming IMAP stream parser. Detects and emits command payloads.
|
||||
* If literal values are encountered the command payload is split into parts
|
||||
* and all parts are emitted separately. The client must send the +\r\n or
|
||||
* return a NO error for the literal
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} [options] Optional Stream options object
|
||||
*/
|
||||
class IMAPStream extends Writable {
|
||||
|
||||
constructor(options) {
|
||||
// init Writable
|
||||
super();
|
||||
|
||||
this.options = options || {};
|
||||
Writable.call(this, this.options);
|
||||
|
||||
// unprocessed chars from the last parsing iteration
|
||||
this._remainder = '';
|
||||
this._literal = false;
|
||||
this._literalReady = false;
|
||||
|
||||
// how many literal bytes to wait for
|
||||
this._expecting = 0;
|
||||
|
||||
// once the input stream ends, flush all output without expecting the newline
|
||||
this.on('finish', this._flushData.bind(this));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Placeholder command handler. Override this with your own.
|
||||
*/
|
||||
oncommand( /* command, callback */ ) {
|
||||
throw new Error('Command handler is not set');
|
||||
}
|
||||
|
||||
// PRIVATE METHODS
|
||||
|
||||
/**
|
||||
* Writable._write method.
|
||||
*/
|
||||
_write(chunk, encoding, done) {
|
||||
if (!chunk || !chunk.length) {
|
||||
return done();
|
||||
}
|
||||
|
||||
let data = this._remainder + chunk.toString('binary');
|
||||
this._remainder = '';
|
||||
|
||||
// start reading data
|
||||
// regex is passed as an argument because we need to keep count of the lastIndex property
|
||||
this._readValue(/\r?\n/g, data, 0, done);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads next command from incoming stream
|
||||
*
|
||||
* @param {RegExp} regex Regular expression object. Needed to keep lastIndex value
|
||||
* @param {String} data Incoming data as binary string
|
||||
* @param {Number} pos Cursor position in current data chunk
|
||||
* @param {Function} done Function to call once data is processed
|
||||
*/
|
||||
_readValue(regex, data, pos, done) {
|
||||
let match;
|
||||
let line;
|
||||
|
||||
// Handle literal mode where we know how many bytes to expect before switching back to
|
||||
// normal line based mode. All the data we receive is pumped to a passthrough stream
|
||||
if (this._expecting > 0) {
|
||||
|
||||
if (data.length - pos <= 0) {
|
||||
return done();
|
||||
}
|
||||
|
||||
if (data.length - pos >= this._expecting) {
|
||||
// all bytes received
|
||||
this._literal.end(new Buffer(data.substr(pos, this._expecting), 'binary'));
|
||||
pos += this._expecting;
|
||||
this._expecting = 0;
|
||||
this._literal = false;
|
||||
|
||||
if (this._literalReady) {
|
||||
// can continue
|
||||
this._literalReady = false;
|
||||
} else {
|
||||
this._literalReady = this._readValue.bind(this, /\r?\n/g, data.substr(pos), 0, done);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// data still pending
|
||||
this._literal.write(new Buffer(data.substr(pos), 'binary'), done);
|
||||
this._expecting -= data.length - pos;
|
||||
return; // wait for the next chunk
|
||||
}
|
||||
}
|
||||
|
||||
// search for the next newline
|
||||
// exec keeps count of the last match with lastIndex
|
||||
// so it knows from where to start with the next iteration
|
||||
if ((match = regex.exec(data))) {
|
||||
line = data.substr(pos, match.index - pos);
|
||||
pos += line.length + match[0].length;
|
||||
} else {
|
||||
this._remainder = pos < data.length ? data.substr(pos) : '';
|
||||
return done();
|
||||
}
|
||||
|
||||
if ((match = /\{(\d+)\}$/.exec(line))) {
|
||||
this._expecting = Number(match[1]);
|
||||
if (!isNaN(match[1])) {
|
||||
this._literal = new PassThrough();
|
||||
|
||||
this.oncommand({
|
||||
value: line,
|
||||
final: false,
|
||||
expecting: this._expecting,
|
||||
literal: this._literal,
|
||||
|
||||
// called once the stream has been processed
|
||||
readyCallback: () => {
|
||||
let next = this._literalReady;
|
||||
if (typeof next === 'function') {
|
||||
this._literalReady = false;
|
||||
next();
|
||||
} else {
|
||||
this._literalReady = true;
|
||||
}
|
||||
}
|
||||
}, err => {
|
||||
if (err) {
|
||||
this._expecting = 0;
|
||||
this._literal = false;
|
||||
this._literalReady = false;
|
||||
}
|
||||
setImmediate(this._readValue.bind(this, regex, data, pos, done));
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.oncommand({
|
||||
value: line,
|
||||
final: true
|
||||
}, this._readValue.bind(this, regex, data, pos, done));
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes remaining bytes
|
||||
*/
|
||||
_flushData() {
|
||||
let line;
|
||||
if (this._remainder) {
|
||||
line = this._remainder;
|
||||
this._remainder = '';
|
||||
this.oncommand(new Buffer(line, 'binary'));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Expose to the world
|
||||
module.exports.IMAPStream = IMAPStream;
|
587
imap-core/lib/imap-tools.js
Normal file
587
imap-core/lib/imap-tools.js
Normal file
|
@ -0,0 +1,587 @@
|
|||
'use strict';
|
||||
|
||||
let Indexer = require('./indexer/indexer');
|
||||
|
||||
module.exports.systemFlags = ['\\answered', '\\flagged', '\\draft', '\\deleted', '\\seen'];
|
||||
|
||||
module.exports.fetchSchema = {
|
||||
body: [true, {
|
||||
type: /^(\d+\.)*(CONTENT|HEADER|HEADER\.FIELDS|HEADER\.FIELDS\.NOT|TEXT|MIME|\d+)$/i,
|
||||
headers: /^(\d+\.)*(HEADER\.FIELDS|HEADER\.FIELDS\.NOT)$/i,
|
||||
startFrom: 'optional',
|
||||
maxLength: 'optional'
|
||||
}],
|
||||
bodystructure: true,
|
||||
envelope: true,
|
||||
flags: true,
|
||||
internaldate: true,
|
||||
rfc822: true,
|
||||
'rfc822.header': true,
|
||||
'rfc822.size': true,
|
||||
'rfc822.text': true,
|
||||
modseq: true,
|
||||
uid: true
|
||||
};
|
||||
|
||||
module.exports.searchSchema = {
|
||||
charset: ['string'],
|
||||
all: true,
|
||||
answered: true,
|
||||
bcc: ['string'],
|
||||
before: ['date'],
|
||||
body: ['string'],
|
||||
cc: ['string'],
|
||||
deleted: true,
|
||||
draft: true,
|
||||
flagged: true,
|
||||
from: ['string'],
|
||||
header: ['string', 'string'],
|
||||
keyword: ['string'],
|
||||
larger: ['number'],
|
||||
modseq: [
|
||||
['string', 'string', 'number'],
|
||||
['number']
|
||||
],
|
||||
new: true,
|
||||
not: ['expression'],
|
||||
old: true,
|
||||
on: ['date'],
|
||||
or: ['expression', 'expression'],
|
||||
recent: true,
|
||||
seen: true,
|
||||
sentbefore: ['date'],
|
||||
senton: ['date'],
|
||||
sentsince: ['date'],
|
||||
since: ['date'],
|
||||
smaller: ['number'],
|
||||
subject: ['string'],
|
||||
text: ['string'],
|
||||
to: ['string'],
|
||||
uid: ['sequence'],
|
||||
unanswered: true,
|
||||
undeleted: true,
|
||||
undraft: true,
|
||||
unflagged: true,
|
||||
unkeyword: ['string'],
|
||||
unseen: true
|
||||
};
|
||||
|
||||
module.exports.searchMapping = {
|
||||
all: {
|
||||
key: 'all',
|
||||
value: [true]
|
||||
},
|
||||
answered: {
|
||||
key: 'flag',
|
||||
value: ['\\Answered', true]
|
||||
},
|
||||
bcc: {
|
||||
key: 'header',
|
||||
value: ['bcc', '$1']
|
||||
},
|
||||
before: {
|
||||
key: 'internaldate',
|
||||
value: ['<', '$1']
|
||||
},
|
||||
cc: {
|
||||
key: 'header',
|
||||
value: ['cc', '$1']
|
||||
},
|
||||
deleted: {
|
||||
key: 'flag',
|
||||
value: ['\\Deleted', true]
|
||||
},
|
||||
draft: {
|
||||
key: 'flag',
|
||||
value: ['\\Draft', true]
|
||||
},
|
||||
flagged: {
|
||||
key: 'flag',
|
||||
value: ['\\Flagged', true]
|
||||
},
|
||||
from: {
|
||||
key: 'header',
|
||||
value: ['from', '$1']
|
||||
},
|
||||
keyword: {
|
||||
key: 'flag',
|
||||
value: ['$1', true]
|
||||
},
|
||||
larger: {
|
||||
key: 'size',
|
||||
value: ['>', '$1']
|
||||
},
|
||||
new: {
|
||||
key: 'flag',
|
||||
value: ['\\Recent', true, '\\Seen', false]
|
||||
},
|
||||
old: {
|
||||
key: 'flag',
|
||||
value: ['\\Recent', false]
|
||||
},
|
||||
on: {
|
||||
key: 'internaldate',
|
||||
value: ['=', '$1']
|
||||
},
|
||||
recent: {
|
||||
key: 'flag',
|
||||
value: ['\\Recent', true]
|
||||
},
|
||||
seen: {
|
||||
key: 'flag',
|
||||
value: ['\\Seen', true]
|
||||
},
|
||||
sentbefore: {
|
||||
key: 'date',
|
||||
value: ['<', '$1']
|
||||
},
|
||||
senton: {
|
||||
key: 'date',
|
||||
value: ['=', '$1']
|
||||
},
|
||||
sentsince: {
|
||||
key: 'date',
|
||||
value: ['>=', '$1']
|
||||
},
|
||||
since: {
|
||||
key: 'internaldate',
|
||||
value: ['>=', '$1']
|
||||
},
|
||||
smaller: {
|
||||
key: 'size',
|
||||
value: ['<', '$1']
|
||||
},
|
||||
subject: {
|
||||
key: 'header',
|
||||
value: ['subject', '$1']
|
||||
},
|
||||
to: {
|
||||
key: 'header',
|
||||
value: ['to', '$1']
|
||||
},
|
||||
unanswered: {
|
||||
key: 'flag',
|
||||
value: ['\\Answered', false]
|
||||
},
|
||||
undeleted: {
|
||||
key: 'flag',
|
||||
value: ['\\Deleted', false]
|
||||
},
|
||||
undraft: {
|
||||
key: 'flag',
|
||||
value: ['\\Draft', false]
|
||||
},
|
||||
unflagged: {
|
||||
key: 'flag',
|
||||
value: ['\\Flagged', false]
|
||||
},
|
||||
unkeyword: {
|
||||
key: 'flag',
|
||||
value: ['$1', false]
|
||||
},
|
||||
unseen: {
|
||||
key: 'flag',
|
||||
value: ['\\Seen', false]
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a sequence range string is valid or not
|
||||
*
|
||||
* @param {range} range Sequence range, eg "1,2,3:7"
|
||||
* @returns {Boolean} True if the string looks like a sequence range
|
||||
*/
|
||||
module.exports.validateSequnce = function (range) {
|
||||
return !!(range.length && /^(\d+|\*)(:\d+|:\*)?(,(\d+|\*)(:\d+|:\*)?)*$/.test(range));
|
||||
};
|
||||
|
||||
module.exports.normalizeMailbox = function (mailbox) {
|
||||
if (!mailbox) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// trim slashes
|
||||
mailbox = mailbox.replace(/^\/|\/$/g, () => '');
|
||||
|
||||
// Normalize case insensitive INBOX to always use uppercase
|
||||
let parts = mailbox.split('/');
|
||||
if (parts[0].toUpperCase() === 'INBOX') {
|
||||
parts[0] = 'INBOX';
|
||||
}
|
||||
mailbox = parts.join('/');
|
||||
|
||||
return mailbox;
|
||||
};
|
||||
|
||||
module.exports.generateFolderListing = function (folders, skipHierarchy) {
|
||||
let items = new Map();
|
||||
let parents = [];
|
||||
|
||||
folders.forEach(folder => {
|
||||
let item;
|
||||
|
||||
if (typeof folder === 'string') {
|
||||
folder = {
|
||||
path: folder
|
||||
};
|
||||
}
|
||||
|
||||
if (!folder || typeof folder !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
let path = module.exports.normalizeMailbox(folder.path);
|
||||
let parent, parentPath;
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
parent = path.split('/');
|
||||
parent.pop();
|
||||
|
||||
while (parent.length) {
|
||||
parentPath = parent.join('/');
|
||||
if (parent && parents.indexOf(parentPath) < 0) {
|
||||
parents.push(parentPath);
|
||||
}
|
||||
parent.pop();
|
||||
}
|
||||
|
||||
item = {
|
||||
flags: [].concat(folder.flags || []),
|
||||
path
|
||||
};
|
||||
|
||||
if (typeof folder.specialUse === 'string' && folder.specialUse) {
|
||||
item.specialUse = folder.specialUse;
|
||||
}
|
||||
|
||||
items.set(path, item);
|
||||
});
|
||||
|
||||
// ensure INBOX
|
||||
if (!items.has('INBOX')) {
|
||||
items.set('INBOX', {
|
||||
path: 'INBOX',
|
||||
flags: []
|
||||
});
|
||||
}
|
||||
|
||||
// Adds \HasChildren flag for parent folders
|
||||
parents.forEach(path => {
|
||||
if (!items.has(path) && !skipHierarchy) {
|
||||
// add virtual hierarchy folders
|
||||
items.set(path, {
|
||||
flags: ['\\Noselect'],
|
||||
path
|
||||
});
|
||||
}
|
||||
let parent = items.get(path);
|
||||
|
||||
if (parent && parent.flags.indexOf('\\HasChildren') < 0) {
|
||||
parent.flags.push('\\HasChildren');
|
||||
}
|
||||
});
|
||||
|
||||
// converts cache Map to a response array
|
||||
let result = [];
|
||||
items.forEach(folder => {
|
||||
// Adds \HasNoChildren flag for leaf folders
|
||||
if (folder.flags.indexOf('\\HasChildren') < 0 && folder.flags.indexOf('\\HasNoChildren') < 0) {
|
||||
folder.flags.push('\\HasNoChildren');
|
||||
}
|
||||
result.push(folder);
|
||||
});
|
||||
|
||||
// sorts folders
|
||||
result.sort((a, b) => {
|
||||
let aParts = a.path.split('/');
|
||||
let bParts = b.path.split('/');
|
||||
for (let i = 0; i < aParts.length; i++) {
|
||||
if (!bParts[i]) {
|
||||
return 1;
|
||||
}
|
||||
if (aParts[i] !== bParts[i]) {
|
||||
// prefer INBOX when sorting
|
||||
if (i === 0 && aParts[i] === 'INBOX') {
|
||||
return -1;
|
||||
} else if (i === 0 && bParts[i] === 'INBOX') {
|
||||
return 1;
|
||||
}
|
||||
return aParts[i].localeCompare(bParts[i]);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
module.exports.filterFolders = function (folders, query) {
|
||||
query = query
|
||||
// remove excess * and %
|
||||
.replace(/\*\*+/g, '*').replace(/%%+/g, '%')
|
||||
// escape special characters
|
||||
.replace(/([\\^$+?!.():=\[\]|,\-])/g, '\\$1')
|
||||
// setup *
|
||||
.replace(/[*]/g, '.*')
|
||||
// setup %
|
||||
.replace(/[%]/g, '[^\/]*');
|
||||
|
||||
let regex = new RegExp('^' + query + '$', '');
|
||||
|
||||
return folders.filter(folder => !!regex.test(folder.path));
|
||||
};
|
||||
|
||||
module.exports.getMessageRange = function (uidList, range, isUid) {
|
||||
range = (range || '').toString();
|
||||
|
||||
let result = [];
|
||||
let rangeParts = range.split(',');
|
||||
let uid, i, len;
|
||||
let totalMessages = uidList.length;
|
||||
let maxUid = 0;
|
||||
|
||||
let inRange = (nr, ranges, total) => {
|
||||
let range, from, to;
|
||||
for (let i = 0, len = ranges.length; i < len; i++) {
|
||||
range = ranges[i];
|
||||
to = range.split(':');
|
||||
from = to.shift();
|
||||
if (from === '*') {
|
||||
from = total;
|
||||
}
|
||||
from = Number(from) || 1;
|
||||
to = to.pop() || from;
|
||||
to = Number(to === '*' && total || to) || from;
|
||||
|
||||
if (nr >= Math.min(from, to) && nr <= Math.max(from, to)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
for (i = 0, len = uidList.length; i < len; i++) {
|
||||
if (uidList[i] > maxUid) {
|
||||
maxUid = uidList[i];
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0, len = uidList.length; i < len; i++) {
|
||||
uid = uidList[i] || 1;
|
||||
if (inRange(isUid ? uid : i + 1, rangeParts, isUid ? maxUid : totalMessages)) {
|
||||
result.push(uidList[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
module.exports.packMessageRange = function (uidList) {
|
||||
if (!Array.isArray(uidList)) {
|
||||
uidList = [].concat(uidList || []);
|
||||
}
|
||||
|
||||
if (!uidList.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
uidList.sort((a, b) => (a - b));
|
||||
|
||||
let last = uidList[uidList.length - 1];
|
||||
let result = [
|
||||
[last]
|
||||
];
|
||||
for (let i = uidList.length - 2; i >= 0; i--) {
|
||||
if (uidList[i] === uidList[i + 1] - 1) {
|
||||
result[0].unshift(uidList[i]);
|
||||
continue;
|
||||
}
|
||||
result.unshift([uidList[i]]);
|
||||
}
|
||||
|
||||
result = result.map(item => {
|
||||
if (item.length === 1) {
|
||||
return item[0];
|
||||
}
|
||||
return item.shift() + ':' + item.pop();
|
||||
});
|
||||
|
||||
return result.join(',');
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a date in GMT timezone
|
||||
*
|
||||
* @param {Date} date Date object to parse
|
||||
* @returns {String} Internaldate formatted date
|
||||
*/
|
||||
module.exports.formatInternalDate = function (date) {
|
||||
let day = date.getUTCDate(),
|
||||
month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
||||
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
|
||||
][date.getUTCMonth()],
|
||||
year = date.getUTCFullYear(),
|
||||
hour = date.getUTCHours(),
|
||||
minute = date.getUTCMinutes(),
|
||||
second = date.getUTCSeconds(),
|
||||
tz = 0, //date.getTimezoneOffset(),
|
||||
tzHours = Math.abs(Math.floor(tz / 60)),
|
||||
tzMins = Math.abs(tz) - tzHours * 60;
|
||||
|
||||
return (day < 10 ? '0' : '') + day + '-' + month + '-' + year + ' ' +
|
||||
(hour < 10 ? '0' : '') + hour + ':' + (minute < 10 ? '0' : '') +
|
||||
minute + ':' + (second < 10 ? '0' : '') + second + ' ' +
|
||||
(tz > 0 ? '-' : '+') + (tzHours < 10 ? '0' : '') + tzHours +
|
||||
(tzMins < 10 ? '0' : '') + tzMins;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts query data and message into an array of query responses.
|
||||
*
|
||||
* Message object must have the following properties:
|
||||
*
|
||||
* * raw – string (binary) or buffer with the rfc822 contents of the message
|
||||
* * uid – message UID
|
||||
* * flags - an array with message flags
|
||||
* * date - internaldate date object
|
||||
*
|
||||
* Additionally the message object *should* have the following properties (if not present then generated automatically):
|
||||
*
|
||||
* * mimeTree - message MIME tree object
|
||||
* * envelope - message IMAP envelope object
|
||||
* * bodystructure - message bodustructure object
|
||||
* * bodystructureShort - bodyscructure for the BODY query
|
||||
*
|
||||
* @param {Array} query Query objects
|
||||
* @param {Object} message Message object
|
||||
* @param {Object} options Options for the indexer
|
||||
* @returns {Array} Resolved responses
|
||||
*/
|
||||
module.exports.getQueryResponse = function (query, message, options) {
|
||||
|
||||
// for optimization purposes try to use cached mimeTree etc. if available
|
||||
// If these values are missing then generate these when first time required
|
||||
// So if the query is for (UID FLAGS) then mimeTree is never generated
|
||||
let mimeTree = message.mimeTree;
|
||||
let indexer = new Indexer(options);
|
||||
|
||||
// generate response object
|
||||
let values = [];
|
||||
query.forEach(item => {
|
||||
let value = '';
|
||||
switch (item.item) {
|
||||
|
||||
case 'uid':
|
||||
value = message.uid;
|
||||
break;
|
||||
|
||||
case 'modseq':
|
||||
value = message.modseq;
|
||||
break;
|
||||
|
||||
case 'flags':
|
||||
value = message.flags;
|
||||
break;
|
||||
|
||||
case 'internaldate':
|
||||
if (!message.internaldate) {
|
||||
message.internaldate = new Date();
|
||||
}
|
||||
value = message.internaldate;
|
||||
break;
|
||||
|
||||
case 'bodystructure':
|
||||
if (!mimeTree) {
|
||||
mimeTree = indexer.parseMimeTree(message.raw);
|
||||
}
|
||||
value = indexer.getBodyStructure(mimeTree);
|
||||
break;
|
||||
|
||||
case 'envelope':
|
||||
if (!mimeTree) {
|
||||
mimeTree = indexer.parseMimeTree(message.raw);
|
||||
}
|
||||
value = indexer.getEnvelope(mimeTree);
|
||||
break;
|
||||
|
||||
case 'rfc822':
|
||||
if (!mimeTree) {
|
||||
mimeTree = indexer.parseMimeTree(message.raw);
|
||||
}
|
||||
value = indexer.getContents(mimeTree);
|
||||
break;
|
||||
|
||||
case 'rfc822.size':
|
||||
if (!mimeTree) {
|
||||
mimeTree = indexer.parseMimeTree(message.raw);
|
||||
}
|
||||
value = indexer.getSize(mimeTree);
|
||||
break;
|
||||
|
||||
case 'rfc822.header':
|
||||
// Equivalent to BODY[HEADER]
|
||||
if (!mimeTree) {
|
||||
mimeTree = indexer.parseMimeTree(message.raw);
|
||||
}
|
||||
value = [].concat(mimeTree.header || []).join('\r\n') + '\r\n\r\n';
|
||||
break;
|
||||
|
||||
case 'rfc822.text':
|
||||
// Equivalent to BODY[TEXT]
|
||||
if (!mimeTree) {
|
||||
mimeTree = indexer.parseMimeTree(message.raw);
|
||||
}
|
||||
value = indexer.getContents(mimeTree, {
|
||||
path: '',
|
||||
type: 'text'
|
||||
});
|
||||
break;
|
||||
|
||||
case 'body':
|
||||
if (!item.hasOwnProperty('type')) {
|
||||
// BODY
|
||||
if (!mimeTree) {
|
||||
mimeTree = indexer.parseMimeTree(message.raw);
|
||||
}
|
||||
value = indexer.getBody(mimeTree);
|
||||
} else if (item.path === '' && item.type === 'content') {
|
||||
// BODY[]
|
||||
if (!mimeTree) {
|
||||
mimeTree = indexer.parseMimeTree(message.raw);
|
||||
}
|
||||
value = indexer.getContents(mimeTree);
|
||||
} else {
|
||||
// BODY[SELECTOR]
|
||||
if (!mimeTree) {
|
||||
mimeTree = indexer.parseMimeTree(message.raw);
|
||||
}
|
||||
value = indexer.getContents(mimeTree, item);
|
||||
}
|
||||
|
||||
if (item.partial) {
|
||||
let len;
|
||||
|
||||
if (value && value.type === 'stream') {
|
||||
value.startFrom = item.partial.startFrom;
|
||||
value.maxLength = item.partial.maxLength;
|
||||
len = value.expectedLength;
|
||||
} else {
|
||||
value = value.toString('binary').substr(item.partial.startFrom, item.partial.maxLength);
|
||||
len = value.length;
|
||||
}
|
||||
|
||||
// If start+length is larger than available value length, then do not return the length value
|
||||
// Instead of BODY[]<10.20> return BODY[]<10> which means that the response is from offset 10 to the end
|
||||
if (item.original.partial.length === 2 && (item.partial.maxLength - item.partial.startFrom > len)) {
|
||||
item.original.partial.pop();
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
values.push(value);
|
||||
});
|
||||
|
||||
return values;
|
||||
};
|
270
imap-core/lib/indexer/body-structure.js
Normal file
270
imap-core/lib/indexer/body-structure.js
Normal file
|
@ -0,0 +1,270 @@
|
|||
'use strict';
|
||||
|
||||
let createEnvelope = require('./create-envelope');
|
||||
|
||||
class BodyStructure {
|
||||
|
||||
constructor(tree, options) {
|
||||
this.tree = tree;
|
||||
this.options = options || {};
|
||||
this.currentPath = '';
|
||||
this.bodyStructure = this.createBodystructure(this.tree, this.options);
|
||||
}
|
||||
|
||||
create() {
|
||||
return this.bodyStructure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an object out of parsed mime tree, that can be
|
||||
* serialized into a BODYSTRUCTURE string
|
||||
*
|
||||
* @param {Object} tree Parsed mime tree (see mimeparser.js for input)
|
||||
* @param {Object} [options] Optional options object
|
||||
* @param {Boolean} [options.contentLanguageString] If true, convert single element array to string for Content-Language
|
||||
* @param {Boolean} [options.upperCaseKeys] If true, use only upper case key names
|
||||
* @param {Boolean} [options.skipContentLocation] If true, do not include Content-Location in the output
|
||||
* @param {Boolean} [options.body] If true, skip extension fields (needed for BODY)
|
||||
* @param {Object} Object structure in the form of BODYSTRUCTURE
|
||||
*/
|
||||
createBodystructure(tree, options) {
|
||||
options = options || {};
|
||||
|
||||
let walker = node => {
|
||||
switch ((node.parsedHeader['content-type'] || {}).type) {
|
||||
case 'multipart':
|
||||
return this.processMultipartNode(node, options);
|
||||
case 'text':
|
||||
return this.processTextNode(node, options);
|
||||
case 'message':
|
||||
if (!options.attachmentRFC822) {
|
||||
return this.processRFC822Node(node, options);
|
||||
}
|
||||
return this.processAttachmentNode(node, options);
|
||||
default:
|
||||
return this.processAttachmentNode(node, options);
|
||||
}
|
||||
};
|
||||
return walker(tree);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a list of basic fields any non-multipart part should have
|
||||
*
|
||||
* @param {Object} node A tree node of the parsed mime tree
|
||||
* @param {Object} [options] Optional options object (see createBodystructure for details)
|
||||
* @return {Array} A list of basic fields
|
||||
*/
|
||||
getBasicFields(node, options) {
|
||||
let bodyType = node.parsedHeader['content-type'] && node.parsedHeader['content-type'].type || null;
|
||||
let bodySubtype = node.parsedHeader['content-type'] && node.parsedHeader['content-type'].subtype || null;
|
||||
let contentTransfer = node.parsedHeader['content-transfer-encoding'] || '7bit';
|
||||
|
||||
return [
|
||||
// body type
|
||||
options.upperCaseKeys ? bodyType && bodyType.toUpperCase() || null : bodyType,
|
||||
|
||||
// body subtype
|
||||
options.upperCaseKeys ? bodySubtype && bodySubtype.toUpperCase() || null : bodySubtype,
|
||||
|
||||
// body parameter parenthesized list
|
||||
node.parsedHeader['content-type'] &&
|
||||
node.parsedHeader['content-type'].hasParams &&
|
||||
this.flatten(Object.keys(node.parsedHeader['content-type'].params).map(key => [
|
||||
options.upperCaseKeys ? key.toUpperCase() : key,
|
||||
node.parsedHeader['content-type'].params[key]
|
||||
])) || null,
|
||||
|
||||
// body id
|
||||
node.parsedHeader['content-id'] || null,
|
||||
|
||||
// body description
|
||||
node.parsedHeader['content-description'] || null,
|
||||
|
||||
// body encoding
|
||||
options.upperCaseKeys ? contentTransfer && contentTransfer.toUpperCase() || '7bit' : contentTransfer,
|
||||
|
||||
// body size
|
||||
node.size
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a list of extension fields any non-multipart part should have
|
||||
*
|
||||
* @param {Object} node A tree node of the parsed mime tree
|
||||
* @param {Object} [options] Optional options object (see createBodystructure for details)
|
||||
* @return {Array} A list of extension fields
|
||||
*/
|
||||
getExtensionFields(node, options) {
|
||||
options = options || {};
|
||||
|
||||
let languageString = node.parsedHeader['content-language'] &&
|
||||
node.parsedHeader['content-language'].replace(/[ ,]+/g, ',').replace(/^,+|,+$/g, '');
|
||||
let language = languageString && languageString.split(',') || null;
|
||||
let data;
|
||||
|
||||
// if `contentLanguageString` is true, then use a string instead of single element array
|
||||
if (language && language.length === 1 && options.contentLanguageString) {
|
||||
language = language[0];
|
||||
}
|
||||
|
||||
data = [
|
||||
// body MD5
|
||||
node.parsedHeader['content-md5'] || null,
|
||||
|
||||
// body disposition
|
||||
node.parsedHeader['content-disposition'] && [
|
||||
options.upperCaseKeys ?
|
||||
node.parsedHeader['content-disposition'].value.toUpperCase() :
|
||||
node.parsedHeader['content-disposition'].value,
|
||||
node.parsedHeader['content-disposition'].params &&
|
||||
node.parsedHeader['content-disposition'].hasParams &&
|
||||
this.flatten(Object.keys(node.parsedHeader['content-disposition'].params).map(key => [
|
||||
options.upperCaseKeys ? key.toUpperCase() : key,
|
||||
node.parsedHeader['content-disposition'].params[key]
|
||||
])) || null
|
||||
] || null,
|
||||
|
||||
// body language
|
||||
language
|
||||
];
|
||||
|
||||
// if `skipContentLocation` is true, do not include Content-Location in output
|
||||
//
|
||||
// NB! RFC3501 has an errata with content-location type, it is described as
|
||||
// 'A string list' (eg. an array) in RFC but the errata page states
|
||||
// that it is a string (http://www.rfc-editor.org/errata_search.php?rfc=3501)
|
||||
// see note for 'Section 7.4.2, page 75'
|
||||
if (!options.skipContentLocation) {
|
||||
// body location
|
||||
data.push(node.parsedHeader['content-location'] || null);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a node with content-type=multipart/*
|
||||
*
|
||||
* @param {Object} node A tree node of the parsed mime tree
|
||||
* @param {Object} [options] Optional options object (see createBodystructure for details)
|
||||
* @return {Array} BODYSTRUCTURE for a multipart part
|
||||
*/
|
||||
processMultipartNode(node, options) {
|
||||
options = options || {};
|
||||
|
||||
let data = (node.childNodes && node.childNodes.map(tree => this.createBodystructure(tree, options)) || [
|
||||
[]
|
||||
]).
|
||||
concat([
|
||||
// body subtype
|
||||
options.upperCaseKeys ? node.multipart && node.multipart.toUpperCase() || null : node.multipart,
|
||||
|
||||
// body parameter parenthesized list
|
||||
node.parsedHeader['content-type'] &&
|
||||
node.parsedHeader['content-type'].hasParams &&
|
||||
this.flatten(Object.keys(node.parsedHeader['content-type'].params).map(key => [
|
||||
options.upperCaseKeys ? key.toUpperCase() : key,
|
||||
node.parsedHeader['content-type'].params[key]
|
||||
])) || null
|
||||
]);
|
||||
|
||||
if (options.body) {
|
||||
return data;
|
||||
} else {
|
||||
return data.
|
||||
// skip body MD5 from extension fields
|
||||
concat(this.getExtensionFields(node, options).slice(1));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a node with content-type=text/*
|
||||
*
|
||||
* @param {Object} node A tree node of the parsed mime tree
|
||||
* @param {Object} [options] Optional options object (see createBodystructure for details)
|
||||
* @return {Array} BODYSTRUCTURE for a text part
|
||||
*/
|
||||
processTextNode(node, options) {
|
||||
options = options || {};
|
||||
|
||||
let data = [].concat(this.getBasicFields(node, options)).concat([
|
||||
node.lineCount
|
||||
]);
|
||||
|
||||
if (!options.body) {
|
||||
data = data.concat(this.getExtensionFields(node, options));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a non-text, non-multipart node
|
||||
*
|
||||
* @param {Object} node A tree node of the parsed mime tree
|
||||
* @param {Object} [options] Optional options object (see createBodystructure for details)
|
||||
* @return {Array} BODYSTRUCTURE for the part
|
||||
*/
|
||||
processAttachmentNode(node, options) {
|
||||
options = options || {};
|
||||
|
||||
let data = [].concat(this.getBasicFields(node, options));
|
||||
|
||||
if (!options.body) {
|
||||
data = data.concat(this.getExtensionFields(node, options));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a node with content-type=message/rfc822
|
||||
*
|
||||
* @param {Object} node A tree node of the parsed mime tree
|
||||
* @param {Object} [options] Optional options object (see createBodystructure for details)
|
||||
* @return {Array} BODYSTRUCTURE for a text part
|
||||
*/
|
||||
processRFC822Node(node, options) {
|
||||
options = options || {};
|
||||
|
||||
let data = [].concat(this.getBasicFields(node, options));
|
||||
|
||||
data.push(createEnvelope(node.message.parsedHeader));
|
||||
data.push(this.createBodystructure(node.message, options));
|
||||
data = data.concat(
|
||||
node.lineCount
|
||||
).
|
||||
concat(this.getExtensionFields(node, options));
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts all sub-arrays into one level array
|
||||
* flatten([1,[2,3]]) -> [1,2,3]
|
||||
*
|
||||
* @param {Array} arr An array with possible sub-arrays
|
||||
* @return {Array} Flat array
|
||||
*/
|
||||
flatten(arr) {
|
||||
let result = [];
|
||||
if (Array.isArray(arr)) {
|
||||
arr.forEach(elm => {
|
||||
if (Array.isArray(elm)) {
|
||||
result = result.concat(this.flatten(elm));
|
||||
} else {
|
||||
result.push(elm);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
result.push(arr);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Expose to the world
|
||||
module.exports = BodyStructure;
|
56
imap-core/lib/indexer/create-envelope.js
Normal file
56
imap-core/lib/indexer/create-envelope.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
'use strict';
|
||||
|
||||
// This module converts message structure into an ENVELOPE object
|
||||
|
||||
/**
|
||||
* Convert a message header object to an ENVELOPE object
|
||||
*
|
||||
* @param {Object} message A parsed mime tree node
|
||||
* @return {Object} ENVELOPE compatible object
|
||||
*/
|
||||
module.exports = function (header) {
|
||||
return [
|
||||
header.date || null,
|
||||
header.subject || '',
|
||||
processAddress(header.from),
|
||||
processAddress(header.sender, header.from),
|
||||
processAddress(header['reply-to'], header.from),
|
||||
processAddress(header.to),
|
||||
processAddress(header.cc),
|
||||
processAddress(header.bcc),
|
||||
header['in-reply-to'] || null,
|
||||
header['message-id'] || null
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts an address object to a list of arrays
|
||||
* [{name: 'User Name', addres:'user@example.com'}] -> [['User Name', null, 'user', 'example.com']]
|
||||
*
|
||||
* @param {Array} arr An array of address objects
|
||||
* @return {Array} A list of addresses
|
||||
*/
|
||||
function processAddress(arr, defaults) {
|
||||
arr = [].concat(arr || []);
|
||||
if (!arr.length) {
|
||||
arr = [].concat(defaults || []);
|
||||
}
|
||||
if (!arr.length) {
|
||||
return null;
|
||||
}
|
||||
let result = [];
|
||||
arr.forEach(addr => {
|
||||
if (!addr.group) {
|
||||
result.push([
|
||||
addr.name || null, null, (addr.address || '').split('@').shift() || null, (addr.address || '').split('@').pop() || null
|
||||
]);
|
||||
} else {
|
||||
// Handle group syntax
|
||||
result.push([null, null, addr.name || '', null]);
|
||||
result = result.concat(processAddress(addr.group) || []);
|
||||
result.push([null, null, null, null]);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
465
imap-core/lib/indexer/indexer.js
Normal file
465
imap-core/lib/indexer/indexer.js
Normal file
|
@ -0,0 +1,465 @@
|
|||
/* eslint no-console: 0 */
|
||||
|
||||
'use strict';
|
||||
|
||||
let stream = require('stream');
|
||||
let PassThrough = stream.PassThrough;
|
||||
|
||||
let BodyStructure = require('./body-structure');
|
||||
let createEnvelope = require('./create-envelope');
|
||||
let parseMimeTree = require('./parse-mime-tree');
|
||||
let fetch = require('nodemailer-fetch');
|
||||
let libbase64 = require('libbase64');
|
||||
let util = require('util');
|
||||
let LengthLimiter = require('../length-limiter');
|
||||
|
||||
class Indexer {
|
||||
|
||||
constructor(options) {
|
||||
this.options = options || {};
|
||||
this.fetchOptions = this.options.fetchOptions || {};
|
||||
|
||||
// create logger
|
||||
this.logger = this.options.logger || {
|
||||
info: () => false,
|
||||
debug: () => false,
|
||||
error: () => false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns expected size for a node
|
||||
*
|
||||
* @param {Object} mimeTree Parsed mimeTree object (or sub node)
|
||||
* @param {Boolean} textOnly If true, do not include the message header in the response
|
||||
* @return {String} Expected message size
|
||||
*/
|
||||
getSize(mimeTree, textOnly) {
|
||||
let size = 0;
|
||||
let first = true;
|
||||
let root = true;
|
||||
|
||||
// make sure that mixed body + mime gets rebuilt correctly
|
||||
let append = (data, force) => {
|
||||
if (Array.isArray(data)) {
|
||||
data = data.join('\r\n');
|
||||
}
|
||||
if (data || force) {
|
||||
size += new Buffer((first ? '' : '\r\n') + (data || ''), 'binary').length;
|
||||
first = false;
|
||||
}
|
||||
};
|
||||
|
||||
let walk = (node, next) => {
|
||||
|
||||
if (!textOnly || !root) {
|
||||
append(filterHeaders(node.header).join('\r\n') + '\r\n');
|
||||
}
|
||||
|
||||
let finalize = () => {
|
||||
if (node.boundary) {
|
||||
append('--' + node.boundary + '--\r\n');
|
||||
}
|
||||
|
||||
append();
|
||||
next();
|
||||
};
|
||||
|
||||
root = false;
|
||||
|
||||
if (node.body || node.parsedHeader['x-attachment-stream-url']) {
|
||||
append(false, true); // force newline
|
||||
size += node.size;
|
||||
}
|
||||
|
||||
if (node.boundary) {
|
||||
append('--' + node.boundary);
|
||||
} else if (node.parsedHeader['x-attachment-stream-url']) {
|
||||
return finalize();
|
||||
}
|
||||
|
||||
let pos = 0;
|
||||
let processChildNodes = () => {
|
||||
if (pos >= node.childNodes.length) {
|
||||
return finalize();
|
||||
}
|
||||
let childNode = node.childNodes[pos++];
|
||||
walk(childNode, () => {
|
||||
if (pos < node.childNodes.length) {
|
||||
append('--' + node.boundary);
|
||||
}
|
||||
return processChildNodes();
|
||||
});
|
||||
};
|
||||
|
||||
if (Array.isArray(node.childNodes)) {
|
||||
processChildNodes();
|
||||
} else {
|
||||
finalize();
|
||||
}
|
||||
};
|
||||
|
||||
walk(mimeTree, () => false);
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a parsed mime tree into a rfc822 message
|
||||
*
|
||||
* @param {Object} mimeTree Parsed mimeTree object
|
||||
* @param {Boolean} textOnly If true, do not include the message header in the response
|
||||
* @return {Stream} Message stream
|
||||
*/
|
||||
rebuild(mimeTree, textOnly) {
|
||||
let res = new PassThrough();
|
||||
let first = true;
|
||||
let root = true;
|
||||
let remainder = '';
|
||||
|
||||
// make sure that mixed body + mime gets rebuilt correctly
|
||||
let append = (data, force) => {
|
||||
if (Array.isArray(data)) {
|
||||
data = data.join('\r\n');
|
||||
}
|
||||
if (remainder || data || force) {
|
||||
res.write(new Buffer((first ? '' : '\r\n') + (remainder || '') + (data || ''), 'binary'));
|
||||
first = false;
|
||||
}
|
||||
remainder = '';
|
||||
};
|
||||
|
||||
let walk = (node, next) => {
|
||||
|
||||
if (!textOnly || !root) {
|
||||
append(filterHeaders(node.header).join('\r\n') + '\r\n');
|
||||
}
|
||||
|
||||
root = false;
|
||||
|
||||
remainder = node.body || '';
|
||||
|
||||
let finalize = () => {
|
||||
if (node.boundary) {
|
||||
append('--' + node.boundary + '--\r\n');
|
||||
}
|
||||
|
||||
append();
|
||||
next();
|
||||
};
|
||||
|
||||
if (node.boundary) {
|
||||
append('--' + node.boundary);
|
||||
} else if (node.parsedHeader['x-attachment-stream-url']) {
|
||||
let streamUrl = node.parsedHeader['x-attachment-stream-url'].replace(/^<|>$/g, '');
|
||||
let streamEncoded = /^\s*YES\s*$/i.test(node.parsedHeader['x-attachment-stream-encoded']);
|
||||
|
||||
append(false, true); // force newline between header and contents
|
||||
|
||||
let headers = {};
|
||||
if (this.fetchOptions.userAgent) {
|
||||
headers['User-Agent'] = this.fetchOptions.userAgent;
|
||||
}
|
||||
|
||||
if (this.fetchOptions.cookies) {
|
||||
headers.Cookie = this.fetchOptions.cookies.get(streamUrl);
|
||||
}
|
||||
|
||||
this.logger.debug('Fetching <%s>\nHeaders: %s', streamUrl, util.inspect(headers, false, 22));
|
||||
|
||||
let limiter = new LengthLimiter(node.size);
|
||||
|
||||
let fetchStream = fetch(streamUrl, {
|
||||
userAgent: this.fetchOptions.userAgent,
|
||||
maxRedirects: this.fetchOptions.maxRedirects,
|
||||
cookies: this.fetchOptions.cookies,
|
||||
timeout: 60 * 1000 // timeout after one minute of inactivity
|
||||
});
|
||||
|
||||
fetchStream.on('error', err => {
|
||||
res.emit('error', err);
|
||||
});
|
||||
|
||||
limiter.on('error', err => {
|
||||
res.emit('error', err);
|
||||
});
|
||||
|
||||
limiter.on('end', () => finalize());
|
||||
|
||||
if (!streamEncoded) {
|
||||
let b64encoder = new libbase64.Encoder();
|
||||
b64encoder.on('error', err => {
|
||||
res.emit('error', err);
|
||||
});
|
||||
// encode stream as base64
|
||||
fetchStream.pipe(b64encoder).pipe(limiter).pipe(res, {
|
||||
end: false
|
||||
});
|
||||
} else {
|
||||
// already encoded, pipe directly to output
|
||||
fetchStream.pipe(limiter).pipe(res, {
|
||||
end: false
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let pos = 0;
|
||||
let processChildNodes = () => {
|
||||
if (pos >= node.childNodes.length) {
|
||||
return finalize();
|
||||
}
|
||||
let childNode = node.childNodes[pos++];
|
||||
walk(childNode, () => {
|
||||
if (pos < node.childNodes.length) {
|
||||
append('--' + node.boundary);
|
||||
}
|
||||
setImmediate(processChildNodes);
|
||||
});
|
||||
};
|
||||
|
||||
if (Array.isArray(node.childNodes)) {
|
||||
processChildNodes();
|
||||
} else {
|
||||
finalize();
|
||||
}
|
||||
};
|
||||
|
||||
setImmediate(walk.bind(null, mimeTree, () => {
|
||||
res.end();
|
||||
}));
|
||||
|
||||
return {
|
||||
type: 'stream',
|
||||
value: res,
|
||||
expectedLength: this.getSize(mimeTree, textOnly)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses structured MIME tree from a rfc822 message source
|
||||
*
|
||||
* @param {String|Buffer} rfc822 E-mail message as 'binary'-string or Buffer
|
||||
* @return {Object} Parsed mime tree
|
||||
*/
|
||||
parseMimeTree(rfc822) {
|
||||
return parseMimeTree(rfc822);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates IMAP compatible BODY object from message tree
|
||||
*
|
||||
* @param {Object} mimeTree Parsed mimeTree object
|
||||
* @return {Array} BODY object as a structured Array
|
||||
*/
|
||||
getBody(mimeTree) {
|
||||
|
||||
// BODY – BODYSTRUCTURE without extension data
|
||||
let body = new BodyStructure(mimeTree, {
|
||||
upperCaseKeys: true,
|
||||
body: true
|
||||
});
|
||||
|
||||
return body.create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates IMAP compatible BODYSTRUCUTRE object from message tree
|
||||
*
|
||||
* @param {Object} mimeTree Parsed mimeTree object
|
||||
* @return {Array} BODYSTRUCTURE object as a structured Array
|
||||
*/
|
||||
getBodyStructure(mimeTree) {
|
||||
|
||||
// full BODYSTRUCTURE
|
||||
let bodystructure = new BodyStructure(mimeTree, {
|
||||
upperCaseKeys: true,
|
||||
skipContentLocation: false
|
||||
});
|
||||
|
||||
return bodystructure.create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates IMAP compatible ENVELOPE object from message headers
|
||||
*
|
||||
* @param {Object} mimeTree Parsed mimeTree object
|
||||
* @return {Array} ENVELOPE object as a structured Array
|
||||
*/
|
||||
getEnvelope(mimeTree) {
|
||||
return createEnvelope(mimeTree.parsedHeader || {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves numeric path to a node in the parsed MIME tree
|
||||
*
|
||||
* @param {Object} mimeTree Parsed mimeTree object
|
||||
* @param {String} path Dot-separated numeric path
|
||||
* @return {Object} Mime node
|
||||
*/
|
||||
resolveContentNode(mimeTree, path) {
|
||||
if (!mimeTree.childNodes && path === '1') {
|
||||
path = '';
|
||||
}
|
||||
|
||||
let pathNumbers = (path || '').toString().split('.');
|
||||
let contentNode = mimeTree;
|
||||
let pathNumber;
|
||||
|
||||
while ((pathNumber = pathNumbers.shift())) {
|
||||
pathNumber = Number(pathNumber) - 1;
|
||||
if (contentNode.message) {
|
||||
// redirect to message/rfc822
|
||||
contentNode = contentNode.message;
|
||||
}
|
||||
|
||||
if (contentNode.childNodes && contentNode.childNodes[pathNumber]) {
|
||||
contentNode = contentNode.childNodes[pathNumber];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return contentNode;
|
||||
}
|
||||
|
||||
bodyQuery(mimeTree, selector, callback) {
|
||||
let data = this.getContents(mimeTree, selector);
|
||||
|
||||
if (data && data.type === 'stream') {
|
||||
let sent = false;
|
||||
let buffers = [];
|
||||
let buflen = 0;
|
||||
|
||||
data.value.on('readable', () => {
|
||||
let buf;
|
||||
while ((buf = data.value.read())) {
|
||||
buffers.push(buf);
|
||||
buflen += buf.length;
|
||||
}
|
||||
});
|
||||
|
||||
data.value.on('error', err => {
|
||||
if (sent) {
|
||||
return;
|
||||
}
|
||||
sent = true;
|
||||
return callback(err);
|
||||
});
|
||||
|
||||
data.value.on('end', () => {
|
||||
if (sent) {
|
||||
return;
|
||||
}
|
||||
sent = true;
|
||||
return callback(null, Buffer.concat(buffers, buflen));
|
||||
});
|
||||
|
||||
} else {
|
||||
return setImmediate(() => callback(null, new Buffer((data || '').toString(), 'binary')));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get node contents
|
||||
*
|
||||
* *selector* is an object with the following properties:
|
||||
* * *path* – numeric path 1.2.3
|
||||
* * *type* - one of content|header|header.fields|header.fields.not|text|mime
|
||||
* * *headers* - an array of headers to include/exclude
|
||||
*
|
||||
* @param {Object} mimeTree Parsed mimeTree object
|
||||
* @param {Object} selector What data to return
|
||||
* @return {String} node contents
|
||||
*/
|
||||
getContents(mimeTree, selector) {
|
||||
let node = mimeTree;
|
||||
|
||||
if (typeof selector === 'string') {
|
||||
selector = {
|
||||
type: selector
|
||||
};
|
||||
}
|
||||
selector = selector || {
|
||||
type: ''
|
||||
};
|
||||
|
||||
if (selector.path) {
|
||||
node = this.resolveContentNode(mimeTree, selector.path);
|
||||
}
|
||||
|
||||
if (!node) {
|
||||
return '';
|
||||
}
|
||||
|
||||
switch (selector.type) {
|
||||
case '':
|
||||
case 'content':
|
||||
if (!selector.path) {
|
||||
// BODY[]
|
||||
return this.rebuild(node);
|
||||
}
|
||||
// BODY[1.2.3]
|
||||
return this.rebuild(node, true);
|
||||
|
||||
case 'header':
|
||||
if (!selector.path) {
|
||||
// BODY[HEADER] mail header
|
||||
return filterHeaders(node.header).join('\r\n') + '\r\n\r\n';
|
||||
} else if (node.message) {
|
||||
// BODY[1.2.3.HEADER] embedded message/rfc822 header
|
||||
return (node.message.header || []).join('\r\n') + '\r\n\r\n';
|
||||
}
|
||||
return '';
|
||||
|
||||
case 'header.fields':
|
||||
// BODY[HEADER.FIELDS.NOT (Key1 Key2 KeyN)] only selected header keys
|
||||
if (!selector.headers || !selector.headers.length) {
|
||||
return '\r\n\r\n';
|
||||
}
|
||||
return filterHeaders(node.header).filter(line => {
|
||||
let key = line.split(':').shift().toLowerCase().trim();
|
||||
return selector.headers.indexOf(key) >= 0;
|
||||
}).join('\r\n') + '\r\n\r\n';
|
||||
|
||||
case 'header.fields.not':
|
||||
// BODY[HEADER.FIELDS.NOT (Key1 Key2 KeyN)] all but selected header keys
|
||||
if (!selector.headers || !selector.headers.length) {
|
||||
return filterHeaders(node.header).join('\r\n') + '\r\n\r\n';
|
||||
}
|
||||
return filterHeaders(node.header).filter(line => {
|
||||
let key = line.split(':').shift().toLowerCase().trim();
|
||||
return selector.headers.indexOf(key) < 0;
|
||||
}).join('\r\n') + '\r\n\r\n';
|
||||
|
||||
case 'mime':
|
||||
// BODY[1.2.3.MIME] mime node header
|
||||
return filterHeaders(node.header).join('\r\n') + '\r\n\r\n';
|
||||
|
||||
case 'text':
|
||||
if (!selector.path) {
|
||||
// BODY[TEXT] mail body without headers
|
||||
return this.rebuild(node, true);
|
||||
} else if (node.message) {
|
||||
// BODY[1.2.3.TEXT] embedded message/rfc822 body without headers
|
||||
return this.rebuild(node.message, true);
|
||||
}
|
||||
|
||||
return '';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function filterHeaders(headers) {
|
||||
headers = headers || [];
|
||||
if (!Array.isArray(headers)) {
|
||||
headers = [].concat(headers || []);
|
||||
}
|
||||
return headers.filter(header => !/^X-Attachment-Stream/i.test(header));
|
||||
}
|
||||
|
||||
module.exports = Indexer;
|
328
imap-core/lib/indexer/parse-mime-tree.js
Normal file
328
imap-core/lib/indexer/parse-mime-tree.js
Normal file
|
@ -0,0 +1,328 @@
|
|||
'use strict';
|
||||
|
||||
let addressparser = require('addressparser');
|
||||
|
||||
/**
|
||||
* Parses a RFC822 message into a structured object (JSON compatible)
|
||||
*
|
||||
* @constructor
|
||||
* @param {String|Buffer} rfc822 Raw body of the message
|
||||
*/
|
||||
class MIMEParser {
|
||||
|
||||
constructor(rfc822) {
|
||||
|
||||
// ensure the input is a binary string
|
||||
this.rfc822 = (rfc822 || '').toString('binary');
|
||||
|
||||
this._br = '';
|
||||
this._pos = 0;
|
||||
|
||||
this.rawBody = '';
|
||||
|
||||
this.tree = {
|
||||
childNodes: []
|
||||
};
|
||||
this._node = this.createNode(this.tree);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the message, line by line
|
||||
*/
|
||||
parse() {
|
||||
let line, prevBr = '';
|
||||
|
||||
// keep parsing until the last linebreak is not a string (no linebreaks anymore)
|
||||
while (typeof this._br === 'string') {
|
||||
line = this.readLine();
|
||||
|
||||
switch (this._node.state) {
|
||||
|
||||
case 'header': // process header section
|
||||
if (this.rawBody) {
|
||||
this.rawBody += prevBr + line;
|
||||
}
|
||||
|
||||
if (!line) {
|
||||
this.processNodeHeader();
|
||||
this.processContentType();
|
||||
|
||||
this._node.state = 'body';
|
||||
} else {
|
||||
this._node.header.push(line);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'body': // process body section
|
||||
this.rawBody += prevBr + line;
|
||||
|
||||
if (this._node.parentBoundary && (line === '--' + this._node.parentBoundary || line === '--' + this._node.parentBoundary + '--')) {
|
||||
|
||||
if (this._node.parsedHeader['content-type'].value === 'message/rfc822') {
|
||||
this._node.message = module.exports(this._node.body.join(''));
|
||||
}
|
||||
|
||||
if (line === '--' + this._node.parentBoundary) {
|
||||
this._node = this.createNode(this._node.parentNode);
|
||||
} else {
|
||||
this._node = this._node.parentNode;
|
||||
}
|
||||
} else if (this._node.boundary && line === '--' + this._node.boundary) {
|
||||
this._node = this.createNode(this._node);
|
||||
} else {
|
||||
// push the line with previous linebreak value
|
||||
// if the array is joined together to a one string,
|
||||
// then the linebreaks in the string are the 'original' ones
|
||||
this._node.body.push((this._node.body.length ? prevBr : '') + line);
|
||||
}
|
||||
break;
|
||||
|
||||
default: // never should be reached
|
||||
throw new Error('Unexpected state');
|
||||
}
|
||||
|
||||
// store the linebreak for later usage
|
||||
prevBr = this._br;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a line from the message body
|
||||
*
|
||||
* @return {String|Boolean} A line from the message
|
||||
*/
|
||||
readLine() {
|
||||
let match = this.rfc822.substr(this._pos).match(/(.*?)(\r?\n|$)/);
|
||||
if (match) {
|
||||
this._br = match[2] || false;
|
||||
this._pos += match[0].length;
|
||||
return match[1];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Join body arrays into strings. Removes unnecessary fields
|
||||
* from the tree (circular references prohibit conversion to JSON)
|
||||
*/
|
||||
finalizeTree() {
|
||||
let walker = node => {
|
||||
if (node.body) {
|
||||
let lineCount = node.body.length;
|
||||
node.body = node.body.join('').
|
||||
// ensure proper line endings
|
||||
replace(/\r?\n/g, '\r\n');
|
||||
node.size = this.getNodeSize(node);
|
||||
node.lineCount = this.getLineCount(node, lineCount);
|
||||
}
|
||||
node.childNodes.forEach(walker);
|
||||
|
||||
// remove unneeded properties
|
||||
delete node.parentNode;
|
||||
delete node.state;
|
||||
if (!node.childNodes.length) {
|
||||
delete node.childNodes;
|
||||
}
|
||||
delete node.parentBoundary;
|
||||
};
|
||||
walker(this.tree);
|
||||
}
|
||||
|
||||
getNodeSize(node) {
|
||||
let bodyLength = (node.body || '').length;
|
||||
let streamSize = 0;
|
||||
let streamEncoded = /^\s*YES\s*$/i.test(node.parsedHeader['x-attachment-stream-encoded']);
|
||||
|
||||
if (node.parsedHeader['x-attachment-stream-url']) {
|
||||
streamSize = Number(node.parsedHeader['x-attachment-stream-size']) || 0;
|
||||
|
||||
if (!streamEncoded) {
|
||||
// stream needs base64 encoding, calculate post-encoded size
|
||||
streamSize = Math.ceil(streamSize / 3 * 4); // convert to base64 length
|
||||
if (streamSize % 4) {
|
||||
// add base64 padding
|
||||
streamSize += (4 - (streamSize % 4));
|
||||
}
|
||||
streamSize += Math.floor(streamSize / 76) * 2; // add newlines
|
||||
}
|
||||
}
|
||||
|
||||
return streamSize + bodyLength;
|
||||
}
|
||||
|
||||
getLineCount(node, lineCount) {
|
||||
if (node.parsedHeader['x-attachment-stream-lines']) {
|
||||
// use pre-calculated line count
|
||||
return Math.max(lineCount - 1, 0) + (Number(node.parsedHeader['x-attachment-stream-lines']) || 0);
|
||||
} else if (node.parsedHeader['x-attachment-stream-url']) {
|
||||
// calculate line count for standard base64 encoded content
|
||||
let streamSize = this.getNodeSize(node);
|
||||
return Math.max(lineCount - 1, 0) + Math.ceil(streamSize / 78);
|
||||
}
|
||||
|
||||
return lineCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new node with default values for the parse tree
|
||||
*/
|
||||
createNode(parentNode) {
|
||||
let node = {
|
||||
state: 'header',
|
||||
childNodes: [],
|
||||
header: [],
|
||||
parsedHeader: {},
|
||||
body: [],
|
||||
multipart: false,
|
||||
parentBoundary: parentNode.boundary,
|
||||
boundary: false,
|
||||
parentNode
|
||||
};
|
||||
parentNode.childNodes.push(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes header lines. Splits lines to key-value pairs
|
||||
* and processes special values
|
||||
*/
|
||||
processNodeHeader() {
|
||||
let key, value;
|
||||
|
||||
for (let i = this._node.header.length - 1; i >= 0; i--) {
|
||||
if (i && this._node.header[i].match(/^\s/)) {
|
||||
this._node.header[i - 1] = this._node.header[i - 1] + '\r\n' + this._node.header[i];
|
||||
this._node.header.splice(i, 1);
|
||||
} else {
|
||||
value = this._node.header[i].split(':');
|
||||
key = (value.shift() || '').trim().toLowerCase();
|
||||
value = value.join(':').trim();
|
||||
|
||||
if (key in this._node.parsedHeader) {
|
||||
if (Array.isArray(this._node.parsedHeader[key])) {
|
||||
this._node.parsedHeader[key].unshift(value);
|
||||
} else {
|
||||
this._node.parsedHeader[key] = [value, this._node.parsedHeader[key]];
|
||||
}
|
||||
} else {
|
||||
this._node.parsedHeader[key] = value.replace(/\s*\r?\n\s*/g, ' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// always ensure the presence of Content-Type
|
||||
if (!this._node.parsedHeader['content-type']) {
|
||||
this._node.parsedHeader['content-type'] = 'text/plain';
|
||||
}
|
||||
|
||||
// parse additional params for Content-Type and Content-Disposition
|
||||
['content-type', 'content-disposition'].forEach(key => {
|
||||
if (this._node.parsedHeader[key]) {
|
||||
this._node.parsedHeader[key] = this.parseValueParams([].concat(this._node.parsedHeader[key] || []).pop());
|
||||
}
|
||||
});
|
||||
|
||||
// ensure single value for selected fields
|
||||
['content-transfer-encoding', 'content-id', 'content-description', 'content-language', 'content-md5', 'content-location'].forEach(key => {
|
||||
if (Array.isArray(this._node.parsedHeader[key])) {
|
||||
this._node.parsedHeader[key] = this._node.parsedHeader[key].pop();
|
||||
}
|
||||
});
|
||||
|
||||
// Parse address fields (join several fields with same key)
|
||||
['from', 'sender', 'reply-to', 'to', 'cc', 'bcc'].forEach(key => {
|
||||
let addresses = [];
|
||||
if (this._node.parsedHeader[key]) {
|
||||
[].concat(this._node.parsedHeader[key] || []).forEach(value => {
|
||||
if (value) {
|
||||
addresses = addresses.concat(addressparser(value) || []);
|
||||
}
|
||||
});
|
||||
this._node.parsedHeader[key] = addresses;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a value to an object.
|
||||
* eg. 'text/plain; charset=utf-8' -> {value: 'text/plain', params:{charset: 'utf-8'}}
|
||||
*
|
||||
* @param {String} headerValue A string value for a header key
|
||||
* @return {Object} Parsed value
|
||||
*/
|
||||
parseValueParams(headerValue) {
|
||||
let data = {
|
||||
value: '',
|
||||
type: '',
|
||||
subtype: '',
|
||||
params: {}
|
||||
},
|
||||
match, processEncodedWords = {};
|
||||
|
||||
(headerValue || '').split(';').forEach((part, i) => {
|
||||
let key, value;
|
||||
if (!i) {
|
||||
data.value = part.trim();
|
||||
data.subtype = data.value.split('/');
|
||||
data.type = (data.subtype.shift() || '').toLowerCase();
|
||||
data.subtype = data.subtype.join('/');
|
||||
return;
|
||||
}
|
||||
value = part.split('=');
|
||||
key = (value.shift() || '').trim().toLowerCase();
|
||||
value = value.join('=').replace(/^['"\s]*|['"\s]*$/g, '');
|
||||
|
||||
// This regex allows for an optional trailing asterisk, for headers
|
||||
// which are encoded with lang/charset info as well as a continuation.
|
||||
// See https://tools.ietf.org/html/rfc2231 section 4.1.
|
||||
if ((match = key.match(/^([^*]+)\*(\d)?\*?$/))) {
|
||||
if (!processEncodedWords[match[1]]) {
|
||||
processEncodedWords[match[1]] = [];
|
||||
}
|
||||
processEncodedWords[match[1]][Number(match[2]) || 0] = value;
|
||||
} else {
|
||||
data.params[key] = value;
|
||||
}
|
||||
data.hasParams = true;
|
||||
});
|
||||
|
||||
// convert extended mime word into a regular one
|
||||
Object.keys(processEncodedWords).forEach(key => {
|
||||
let charset = '';
|
||||
let value = '';
|
||||
processEncodedWords[key].forEach(val => {
|
||||
let parts = val.split('\'');
|
||||
charset = charset || parts.shift();
|
||||
value += (parts.pop() || '').replace(/%/g, '=');
|
||||
});
|
||||
data.params[key] = '=?' + (charset || 'ISO-8859-1').toUpperCase() + '?Q?' + value + '?=';
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks Content-Type value for the current tree node.
|
||||
*/
|
||||
processContentType() {
|
||||
if (!this._node.parsedHeader['content-type']) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._node.parsedHeader['content-type'].type === 'multipart' && this._node.parsedHeader['content-type'].params.boundary) {
|
||||
this._node.multipart = this._node.parsedHeader['content-type'].subtype;
|
||||
this._node.boundary = this._node.parsedHeader['content-type'].params.boundary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function (rfc822) {
|
||||
let parser = new MIMEParser(rfc822);
|
||||
let response;
|
||||
|
||||
parser.parse();
|
||||
parser.finalizeTree();
|
||||
|
||||
response = parser.tree.childNodes[0] || false;
|
||||
return response;
|
||||
};
|
75
imap-core/lib/length-limiter.js
Normal file
75
imap-core/lib/length-limiter.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
'use strict';
|
||||
|
||||
let streams = require('stream');
|
||||
let Transform = streams.Transform;
|
||||
|
||||
// make sure that a stream piped to this transform stream
|
||||
// always emits a fixed amounts of bytes. Either by truncating
|
||||
// input or emitting padding characters
|
||||
class LengthLimiter extends Transform {
|
||||
constructor(expectedLength, padding, startFrom, byteCounter) {
|
||||
super();
|
||||
this.expectedLength = expectedLength;
|
||||
this.padding = padding || ' ';
|
||||
this.byteCounter = byteCounter || 0;
|
||||
this.startFrom = startFrom || 0;
|
||||
this.finished = false;
|
||||
Transform.call(this);
|
||||
}
|
||||
|
||||
_transform(chunk, encoding, done) {
|
||||
if (encoding !== 'buffer') {
|
||||
chunk = new Buffer(chunk, encoding);
|
||||
}
|
||||
|
||||
if (!chunk || !chunk.length || this.finished) {
|
||||
return done();
|
||||
}
|
||||
|
||||
// not yet at allowed position
|
||||
if (chunk.length + this.byteCounter <= this.startFrom) {
|
||||
// ignore
|
||||
this.byteCounter += chunk.length;
|
||||
return done();
|
||||
}
|
||||
|
||||
// start emitting at middle of chunk
|
||||
if (this.byteCounter < this.startFrom) {
|
||||
// split the chunk and ignore the first part
|
||||
chunk = chunk.slice(this.startFrom - this.byteCounter);
|
||||
this.byteCounter += (this.startFrom - this.byteCounter);
|
||||
}
|
||||
|
||||
// can emit full chunk
|
||||
if (chunk.length + this.byteCounter <= this.expectedLength) {
|
||||
this.byteCounter += chunk.length;
|
||||
this.push(chunk);
|
||||
if (this.byteCounter >= this.expectedLength) {
|
||||
this.finished = true;
|
||||
this.emit('done', false);
|
||||
}
|
||||
return setImmediate(done);
|
||||
}
|
||||
|
||||
// stop emitting in the middle of chunk
|
||||
let buf = chunk.slice(0, this.expectedLength - this.byteCounter);
|
||||
let remaining = chunk.slice(this.expectedLength - this.byteCounter);
|
||||
this.push(buf);
|
||||
this.finished = true;
|
||||
this.emit('done', remaining);
|
||||
return setImmediate(done);
|
||||
}
|
||||
|
||||
_flush(done) {
|
||||
if (!this.finished) {
|
||||
// add padding if incoming stream stopped too early
|
||||
let buf = new Buffer(this.padding.repeat(this.expectedLength - this.byteCounter));
|
||||
this.push(buf);
|
||||
this.finished = true;
|
||||
}
|
||||
done();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = LengthLimiter;
|
150
imap-core/lib/memory-notifier/index.js
Normal file
150
imap-core/lib/memory-notifier/index.js
Normal file
|
@ -0,0 +1,150 @@
|
|||
'use strict';
|
||||
|
||||
let crypto = require('crypto');
|
||||
let EventEmitter = require('events').EventEmitter;
|
||||
|
||||
// Expects that the folder listing is a Map
|
||||
|
||||
class MemoryNotifier extends EventEmitter {
|
||||
|
||||
constructor(options) {
|
||||
super();
|
||||
this.folders = options.folders || new Map();
|
||||
|
||||
let logfunc = (...args) => {
|
||||
let level = args.shift() || 'DEBUG';
|
||||
let message = args.shift() || '';
|
||||
|
||||
console.log([level].concat(message || '').join(' '), ...args); // eslint-disable-line no-console
|
||||
};
|
||||
|
||||
this.logger = options.logger || {
|
||||
info: logfunc.bind(null, 'INFO'),
|
||||
debug: logfunc.bind(null, 'DEBUG'),
|
||||
error: logfunc.bind(null, 'ERROR')
|
||||
};
|
||||
|
||||
this._listeners = new EventEmitter();
|
||||
this._listeners.setMaxListeners(0);
|
||||
|
||||
EventEmitter.call(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates hashed event names for mailbox:username pairs
|
||||
*
|
||||
* @param {String} mailbox
|
||||
* @param {String} username
|
||||
* @returns {String} md5 hex
|
||||
*/
|
||||
_eventName(mailbox, username) {
|
||||
return crypto.createHash('md5').update(username + ':' + mailbox).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an event handler for mailbox:username events
|
||||
*
|
||||
* @param {String} username
|
||||
* @param {String} mailbox
|
||||
* @param {Function} handler Function to run once there are new entries in the journal
|
||||
*/
|
||||
addListener(session, mailbox, handler) {
|
||||
let eventName = this._eventName(session.user.username, mailbox);
|
||||
this._listeners.addListener(eventName, handler);
|
||||
|
||||
this.logger.debug('New journal listener for %s ("%s:%s")', eventName, session.user.username, mailbox);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters an event handler for mailbox:username events
|
||||
*
|
||||
* @param {String} username
|
||||
* @param {String} mailbox
|
||||
* @param {Function} handler Function to run once there are new entries in the journal
|
||||
*/
|
||||
removeListener(session, mailbox, handler) {
|
||||
let eventName = this._eventName(session.user.username, mailbox);
|
||||
this._listeners.removeListener(eventName, handler);
|
||||
|
||||
this.logger.debug('Removed journal listener from %s ("%s:%s")', eventName, session.user.username, mailbox);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores multiple journal entries to db
|
||||
*
|
||||
* @param {String} username
|
||||
* @param {String} mailbox
|
||||
* @param {Array|Object} entries An array of entries to be journaled
|
||||
* @param {Function} callback Runs once the entry is either stored or an error occurred
|
||||
*/
|
||||
addEntries(username, mailbox, entries, callback) {
|
||||
let folder = this.folders.get(mailbox);
|
||||
|
||||
if (!folder) {
|
||||
return callback(null, new Error('Selected mailbox does not exist'));
|
||||
}
|
||||
|
||||
if (entries && !Array.isArray(entries)) {
|
||||
entries = [entries];
|
||||
} else if (!entries || !entries.length) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
// store entires in the folder object
|
||||
if (!folder.journal) {
|
||||
folder.journal = [];
|
||||
}
|
||||
|
||||
entries.forEach(entry => {
|
||||
entry.modseq = ++folder.modifyIndex;
|
||||
folder.journal.push(entry);
|
||||
});
|
||||
|
||||
setImmediate(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a notification that there are new updates in the selected mailbox
|
||||
*
|
||||
* @param {String} username
|
||||
* @param {String} mailbox
|
||||
*/
|
||||
fire(username, mailbox, payload) {
|
||||
let eventName = this._eventName(username, mailbox);
|
||||
setImmediate(() => {
|
||||
this._listeners.emit(eventName, payload);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all entries from the journal that have higher than provided modification index
|
||||
*
|
||||
* @param {String} username
|
||||
* @param {String} mailbox
|
||||
* @param {Number} modifyIndex Last known modification id
|
||||
* @param {Function} callback Returns update entries as an array
|
||||
*/
|
||||
getUpdates(session, mailbox, modifyIndex, callback) {
|
||||
modifyIndex = Number(modifyIndex) || 0;
|
||||
|
||||
if (!this.folders.has(mailbox)) {
|
||||
return callback(null, 'NONEXISTENT');
|
||||
}
|
||||
|
||||
let folder = this.folders.get(mailbox);
|
||||
let minIndex = folder.journal.length;
|
||||
|
||||
for (let i = folder.journal.length - 1; i >= 0; i--) {
|
||||
if (folder.journal[i].modseq > modifyIndex) {
|
||||
minIndex = i;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return callback(null, folder.journal.slice(minIndex));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = MemoryNotifier;
|
22
imap-core/lib/redis-notifier/add-entries.lua
Normal file
22
imap-core/lib/redis-notifier/add-entries.lua
Normal file
|
@ -0,0 +1,22 @@
|
|||
-- inserts JSON values to a sorted set where score value is an incrementing number resolved from a KEYS[1].modifyIndex
|
||||
|
||||
-- check if the mailbox even exists
|
||||
if not redis.call('exists', KEYS[1]) then
|
||||
return {err='Selected mailbox does not exist'}
|
||||
end;
|
||||
|
||||
local len = table.getn(ARGV);
|
||||
|
||||
-- do a single increment to get id values for all elements instead of incrementing it one by one
|
||||
local score = redis.call('hincrby', KEYS[1], 'modifyIndex', len) - len;
|
||||
|
||||
for i = 1, #ARGV do
|
||||
-- we include modification index in the stored value to ensure that all values are always unique
|
||||
-- otherwise adding new element with the same data does not insert a new entry but overrides
|
||||
-- an existing one
|
||||
|
||||
redis.call('zadd', KEYS[2], score + i, tostring(score + i) .. ':' .. ARGV[i]);
|
||||
end;
|
||||
|
||||
-- return the largest modification index
|
||||
return score + len;
|
244
imap-core/lib/redis-notifier/index.js
Normal file
244
imap-core/lib/redis-notifier/index.js
Normal file
|
@ -0,0 +1,244 @@
|
|||
'use strict';
|
||||
|
||||
let redis = require('redis');
|
||||
let EventEmitter = require('events').EventEmitter;
|
||||
let crypto = require('crypto');
|
||||
let fs = require('fs');
|
||||
let scripts = {
|
||||
addEntries: fs.readFileSync(__dirname + '/add-entries.lua')
|
||||
};
|
||||
|
||||
// Assumes that there are following hash keys in Redis:
|
||||
// u:[username]:folder:[md5(path)]
|
||||
// with the following key:
|
||||
// modifyIndex: Number
|
||||
|
||||
class RedisNotifier extends EventEmitter {
|
||||
|
||||
constructor(options) {
|
||||
super();
|
||||
|
||||
options = options || {};
|
||||
|
||||
this.options = {
|
||||
port: options.port || 6379,
|
||||
host: options.host || 'localhost',
|
||||
db: options.db || 0,
|
||||
prefix: options.prefix || 'imap:'
|
||||
};
|
||||
|
||||
let logfunc = (...args) => {
|
||||
let level = args.shift() || 'DEBUG';
|
||||
let message = args.shift() || '';
|
||||
|
||||
console.log([level].concat(message || '').join(' '), ...args); // eslint-disable-line no-console
|
||||
};
|
||||
|
||||
this.logger = options.logger || {
|
||||
info: logfunc.bind(null, 'INFO'),
|
||||
debug: logfunc.bind(null, 'DEBUG'),
|
||||
error: logfunc.bind(null, 'ERROR')
|
||||
};
|
||||
|
||||
// we need two db connections as subscriber can't manage data
|
||||
this._db = redis.createClient(options.port, options.host);
|
||||
this._subscriber = redis.createClient(options.port, options.host);
|
||||
|
||||
this._pubsubListeners = new Map();
|
||||
this._listeners = new EventEmitter();
|
||||
this._listeners.setMaxListeners(0);
|
||||
|
||||
this._subscriber.on('message', (channel, message) => {
|
||||
try {
|
||||
message = JSON.parse(message);
|
||||
} catch (E) {
|
||||
// ignore
|
||||
}
|
||||
this.logger.debug(
|
||||
'Journal update notification for %s, updating %s subscribers',
|
||||
channel.slice(this.options.prefix.length),
|
||||
this._listeners.listenerCount(channel.slice(this.options.prefix.length)));
|
||||
|
||||
this._listeners.emit(channel.slice(this.options.prefix.length), message);
|
||||
});
|
||||
|
||||
EventEmitter.call(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates hashed event names for mailbox:username pairs
|
||||
*
|
||||
* @param {String} username
|
||||
* @param {String} mailbox
|
||||
* @returns {String} md5 hex
|
||||
*/
|
||||
_eventName(username, mailbox) {
|
||||
return crypto.createHash('md5').update(username + ':' + mailbox).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an event handler for mailbox:username events
|
||||
*
|
||||
* @param {String} username
|
||||
* @param {String} mailbox
|
||||
* @param {Function} handler Function to run once there are new entries in the journal
|
||||
*/
|
||||
addListener(session, mailbox, handler) {
|
||||
let eventName = this._eventName(session.user.username, mailbox);
|
||||
this._listeners.addListener(eventName, handler);
|
||||
|
||||
if (!this._pubsubListeners.has(eventName)) {
|
||||
this._pubsubListeners.set(eventName, 1);
|
||||
this._subscriber.subscribe(this.options.prefix + eventName);
|
||||
} else {
|
||||
this._pubsubListeners.set(eventName, this._pubsubListeners.get(eventName) + 1);
|
||||
}
|
||||
|
||||
this.logger.debug('New journal listener for %s ("%s:%s", total %s subscribers)', eventName, session.user.username, mailbox, this._listeners.listenerCount(eventName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters an event handler for mailbox:username events
|
||||
*
|
||||
* @param {String} username
|
||||
* @param {String} mailbox
|
||||
* @param {Function} handler Function to run once there are new entries in the journal
|
||||
*/
|
||||
removeListener(session, mailbox, handler) {
|
||||
let count, eventName = this._eventName(session.user.username, mailbox);
|
||||
this._listeners.removeListener(eventName, handler);
|
||||
|
||||
if (this._pubsubListeners.has(eventName) && (count = this._pubsubListeners.get(eventName)) && count > 0) {
|
||||
count--;
|
||||
if (!count) {
|
||||
this._subscriber.unsubscribe(this.options.prefix + eventName);
|
||||
this._pubsubListeners.delete(eventName);
|
||||
} else {
|
||||
this._pubsubListeners.set(eventName, 1);
|
||||
}
|
||||
|
||||
this.logger.debug('Removed journal listener from %s ("%s:%s", total %s subscribers)', eventName, session.user.username, mailbox, this._listeners.listenerCount(eventName));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores multiple journal entries to db
|
||||
*
|
||||
* @param {String} username
|
||||
* @param {String} mailbox
|
||||
* @param {Array|Object} entries An array of entries to be journaled
|
||||
* @param {Function} callback Runs once the entry is either stored or an error occurred
|
||||
*/
|
||||
addEntries(username, mailbox, entries, callback) {
|
||||
let mailboxHash = crypto.createHash('md5').update(mailbox).digest('hex');
|
||||
|
||||
if (entries && !Array.isArray(entries)) {
|
||||
entries = [entries];
|
||||
} else if (!entries || !entries.length) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
entries = entries.map(entry => JSON.stringify(entry));
|
||||
|
||||
this.logger.debug('Adding journal entries for %s (%s)\n%s', mailbox, mailboxHash, entries.join('\n'));
|
||||
|
||||
this._db.multi().
|
||||
select(this.options.db).
|
||||
eval([
|
||||
scripts.addEntries,
|
||||
2,
|
||||
'u:' + username + ':folder:' + mailboxHash,
|
||||
'u:' + username + ':journal:' + mailboxHash
|
||||
].concat(entries)).
|
||||
exec(err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(null, true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a notification that there are new updates in the selected mailbox
|
||||
*
|
||||
* @param {String} username
|
||||
* @param {String} mailbox
|
||||
*/
|
||||
fire(username, mailbox, payload) {
|
||||
let eventName = this._eventName(username, mailbox);
|
||||
|
||||
payload = payload || false;
|
||||
|
||||
setImmediate(this._db.publish.bind(this._db, this.options.prefix + eventName, JSON.stringify(payload)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all entries from the journal that have higher than provided modification index
|
||||
*
|
||||
* @param {String} username
|
||||
* @param {String} mailbox
|
||||
* @param {Number} modifyIndex Last known modification id
|
||||
* @param {Function} callback Returns update entries as an array
|
||||
*/
|
||||
getUpdates(session, mailbox, modifyIndex, callback) {
|
||||
modifyIndex = Number(modifyIndex) || 0;
|
||||
|
||||
let mailboxHash = crypto.createHash('md5').update(mailbox).digest('hex');
|
||||
let username = session.user.username;
|
||||
|
||||
this._db.multi().
|
||||
select(this.options.db).
|
||||
exists('u:' + username + ':journal:' + mailboxHash).
|
||||
zrangebyscore('u:' + username + ':journal:' + mailboxHash, modifyIndex + 1, Infinity).
|
||||
exec((err, replies) => {
|
||||
let updates;
|
||||
|
||||
this.logger.debug('[%s] Loaded journal updates for "%s:%s" since %s', session.id, username, mailbox, modifyIndex + 1);
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!replies || !replies[1]) {
|
||||
return callback(null, 'NONEXISTENT');
|
||||
}
|
||||
|
||||
updates = (replies[2] || []).
|
||||
map(entry => {
|
||||
let data;
|
||||
let m = (entry || '').toString().match(/^(\d+)\:/);
|
||||
|
||||
if (!m) {
|
||||
// invalidly formatted entry
|
||||
this.logger.debug('[%s] Invalidly formatted entry for "%s:%s" (%s)', session.id, username, mailbox, (entry).toString());
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
data = JSON.parse(entry.substr(m[0].length));
|
||||
data.modseq = Number(m[1]) || false;
|
||||
// we mess around with json in redis lua but lua does not make
|
||||
// a distinction between an object and an array, if an array
|
||||
// is empty then it will be invalidly detected as an object
|
||||
if (data.flags && !Array.isArray(data.flags)) {
|
||||
data.flags = [];
|
||||
}
|
||||
} catch (E) {
|
||||
this.logger.error('[%s] Failed parsing journal update for "%s:%s" (%s): %s', session.id, username, mailbox, entry.substr(m[0].length), E.message);
|
||||
}
|
||||
|
||||
return data;
|
||||
}).filter(entry =>
|
||||
// only include entries with data
|
||||
(entry && entry.uid)
|
||||
);
|
||||
|
||||
this.logger.debug('[%s] Processing journal updates for "%s:%s": %s', session.id, username, mailbox, JSON.stringify(updates));
|
||||
|
||||
callback(null, updates);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = RedisNotifier;
|
210
imap-core/lib/search.js
Normal file
210
imap-core/lib/search.js
Normal file
|
@ -0,0 +1,210 @@
|
|||
'use strict';
|
||||
|
||||
const Indexer = require('./indexer/indexer');
|
||||
let indexer = new Indexer();
|
||||
|
||||
module.exports.matchSearchQuery = matchSearchQuery;
|
||||
|
||||
let queryHandlers = {
|
||||
|
||||
// atom beautify uses invalid indentation that messes up shorthand methods
|
||||
/*eslint-disable object-shorthand */
|
||||
|
||||
// always matches
|
||||
all: function () {
|
||||
return true;
|
||||
},
|
||||
|
||||
// matches if the message object includes (exists:true) or does not include (exists:false) specifiec flag
|
||||
flag: function (message, query) {
|
||||
let pos = [].concat(message.flags || []).indexOf(query.value);
|
||||
return query.exists ? pos >= 0 : pos < 0;
|
||||
},
|
||||
|
||||
// matches message receive date
|
||||
internaldate: function (message, query) {
|
||||
switch (query.operator) {
|
||||
case '<':
|
||||
return getShortDate(message.internaldate) < getShortDate(query.value);
|
||||
case '=':
|
||||
return getShortDate(message.internaldate) === getShortDate(query.value);
|
||||
case '>=':
|
||||
return getShortDate(message.internaldate) >= getShortDate(query.value);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
// matches message header date
|
||||
date: function (message, query) {
|
||||
let mimeTree = message.mimeTree;
|
||||
if (!mimeTree) {
|
||||
mimeTree = indexer.parseMimeTree(message.raw);
|
||||
}
|
||||
|
||||
let date = mimeTree.parsedHeader.date || message.internaldate;
|
||||
switch (query.operator) {
|
||||
case '<':
|
||||
return getShortDate(date) < getShortDate(query.value);
|
||||
case '=':
|
||||
return getShortDate(date) === getShortDate(query.value);
|
||||
case '>=':
|
||||
return getShortDate(date) >= getShortDate(query.value);
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
// matches message body
|
||||
body: function (message, query) {
|
||||
let body = (message.raw || '').toString();
|
||||
let bodyStart = body.match(/\r?\r?\n/);
|
||||
if (!bodyStart) {
|
||||
return false;
|
||||
}
|
||||
return body.substr(bodyStart.index + bodyStart[0].length).toLowerCase().indexOf((query.value || '').toString().toLowerCase()) >= 0;
|
||||
},
|
||||
|
||||
// matches message source
|
||||
text: function (message, query) {
|
||||
return (message.raw || '').toString().toLowerCase().indexOf((query.value || '').toString().toLowerCase()) >= 0;
|
||||
},
|
||||
|
||||
// matches message UID number. Sequence queries are also converted to UID queries
|
||||
uid: function (message, query) {
|
||||
return query.value.indexOf(message.uid) >= 0;
|
||||
},
|
||||
|
||||
// matches message source size
|
||||
size: function (message, query) {
|
||||
let raw = message.raw || '';
|
||||
|
||||
switch (query.operator) {
|
||||
case '<':
|
||||
return raw.length < query.value;
|
||||
case '=':
|
||||
return raw.length === query.value;
|
||||
case '>':
|
||||
return raw.length > query.value;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
// matches message headers
|
||||
header: function (message, query) {
|
||||
let mimeTree = message.mimeTree;
|
||||
if (!mimeTree) {
|
||||
mimeTree = indexer.parseMimeTree(message.raw || '');
|
||||
}
|
||||
|
||||
let headers = (mimeTree.header || []);
|
||||
let header = query.header;
|
||||
let term = (query.value || '').toString().toLowerCase();
|
||||
let key, value, parts;
|
||||
|
||||
for (let i = 0, len = headers.length; i < len; i++) {
|
||||
parts = headers[i].split(':');
|
||||
key = (parts.shift() || '').trim().toLowerCase();
|
||||
|
||||
if (/^X-Attachment-Stream/i.test(key)) {
|
||||
// skip special headers
|
||||
continue;
|
||||
}
|
||||
|
||||
value = (parts.join(':') || '');
|
||||
|
||||
if (key === header && (!term || value.toLowerCase().indexOf(term) >= 0)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
// matches messages with modifyIndex exual or greater than criteria
|
||||
modseq: function (message, query) {
|
||||
return message.modseq >= query.value;
|
||||
},
|
||||
|
||||
// charset argument is ignored
|
||||
charset: function () {
|
||||
return true;
|
||||
}
|
||||
|
||||
/*eslint-enable object-shorthand */
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a date object with time set to 00:00 on UTC timezone
|
||||
*
|
||||
* @param {String|Date} date Date to convert
|
||||
* @returns {Date} Date object without time
|
||||
*/
|
||||
function getShortDate(date) {
|
||||
date = date || new Date();
|
||||
if (typeof date === 'string' || typeof date === 'number') {
|
||||
date = new Date(date);
|
||||
}
|
||||
return date.toISOString().substr(0, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a specific search term match the message or not
|
||||
*
|
||||
* @param {Object} message Stored message object
|
||||
* @param {Object} query Query term object
|
||||
* @returns {Boolean} Term matched (true) or not (false)
|
||||
*/
|
||||
function matchSearchTerm(message, query) {
|
||||
|
||||
if (Array.isArray(query)) {
|
||||
// AND, all terms need to match
|
||||
return matchSearchQuery(message, query);
|
||||
}
|
||||
|
||||
if (!query || typeof query !== 'object') {
|
||||
// unknown query term
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (query.key) {
|
||||
case 'or':
|
||||
// OR, only single match needed
|
||||
for (let i = query.value.length - 1; i >= 0; i--) {
|
||||
if (matchSearchTerm(message, query.value[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
case 'not':
|
||||
// return reverse match
|
||||
return !matchSearchTerm(message, query.value);
|
||||
default:
|
||||
// check if there is a handler for the term and use it
|
||||
if (queryHandlers.hasOwnProperty(query.key)) {
|
||||
return queryHandlers[query.key](message, query);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverses query tree and checks if all query terms match or not. Stops on first false match occurence
|
||||
*
|
||||
* @param {Object} message Stored message object
|
||||
* @param {Object} query Query term object
|
||||
* @returns {Boolean} Term matched (true) or not (false)
|
||||
*/
|
||||
function matchSearchQuery(message, query) {
|
||||
if (!Array.isArray(query)) {
|
||||
query = [].concat(query || []);
|
||||
}
|
||||
|
||||
for (let i = 0, len = query.length; i < len; i++) {
|
||||
if (!matchSearchTerm(message, query[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
98
imap-core/lib/tls-options.js
Normal file
98
imap-core/lib/tls-options.js
Normal file
|
@ -0,0 +1,98 @@
|
|||
'use strict';
|
||||
|
||||
// Expose to the world
|
||||
module.exports = getTLSOptions;
|
||||
|
||||
let tlsDefaults = {
|
||||
key: '-----BEGIN RSA PRIVATE KEY-----\n' +
|
||||
'MIIEpAIBAAKCAQEA6Z5Qqhw+oWfhtEiMHE32Ht94mwTBpAfjt3vPpX8M7DMCTwHs\n' +
|
||||
'1xcXvQ4lQ3rwreDTOWdoJeEEy7gMxXqH0jw0WfBx+8IIJU69xstOyT7FRFDvA1yT\n' +
|
||||
'RXY2yt9K5s6SKken/ebMfmZR+03ND4UFsDzkz0FfgcjrkXmrMF5Eh5UXX/+9YHeU\n' +
|
||||
'xlp0gMAt+/SumSmgCaysxZLjLpd4uXz+X+JVxsk1ACg1NoEO7lWJC/3WBP7MIcu2\n' +
|
||||
'wVsMd2XegLT0gWYfT1/jsIH64U/mS/SVXC9QhxMl9Yfko2kx1OiYhDxhHs75RJZh\n' +
|
||||
'rNRxgfiwgSb50Gw4NAQaDIxr/DJPdLhgnpY6UQIDAQABAoIBAE+tfzWFjJbgJ0ql\n' +
|
||||
's6Ozs020Sh4U8TZQuonJ4HhBbNbiTtdDgNObPK1uNadeNtgW5fOeIRdKN6iDjVeN\n' +
|
||||
'AuXhQrmqGDYVZ1HSGUfD74sTrZQvRlWPLWtzdhybK6Css41YAyPFo9k4bJ2ZW2b/\n' +
|
||||
'p4EEQ8WsNja9oBpttMU6YYUchGxo1gujN8hmfDdXUQx3k5Xwx4KA68dveJ8GasIt\n' +
|
||||
'd+0Jd/FVwCyyx8HTiF1FF8QZYQeAXxbXJgLBuCsMQJghlcpBEzWkscBR3Ap1U0Zi\n' +
|
||||
'4oat8wrPZGCblaA6rNkRUVbc/+Vw0stnuJ/BLHbPxyBs6w495yBSjBqUWZMvljNz\n' +
|
||||
'm9/aK0ECgYEA9oVIVAd0enjSVIyAZNbw11ElidzdtBkeIJdsxqhmXzeIFZbB39Gd\n' +
|
||||
'bjtAVclVbq5mLsI1j22ER2rHA4Ygkn6vlLghK3ZMPxZa57oJtmL3oP0RvOjE4zRV\n' +
|
||||
'dzKexNGo9gU/x9SQbuyOmuauvAYhXZxeLpv+lEfsZTqqrvPUGeBiEQcCgYEA8poG\n' +
|
||||
'WVnykWuTmCe0bMmvYDsWpAEiZnFLDaKcSbz3O7RMGbPy1cypmqSinIYUpURBT/WY\n' +
|
||||
'wVPAGtjkuTXtd1Cy58m7PqziB7NNWMcsMGj+lWrTPZ6hCHIBcAImKEPpd+Y9vGJX\n' +
|
||||
'oatFJguqAGOz7rigBq6iPfeQOCWpmprNAuah++cCgYB1gcybOT59TnA7mwlsh8Qf\n' +
|
||||
'bm+tSllnin2A3Y0dGJJLmsXEPKtHS7x2Gcot2h1d98V/TlWHe5WNEUmx1VJbYgXB\n' +
|
||||
'pw8wj2ACxl4ojNYqWPxegaLd4DpRbtW6Tqe9e47FTnU7hIggR6QmFAWAXI+09l8y\n' +
|
||||
'amssNShqjE9lu5YDi6BTKwKBgQCuIlKGViLfsKjrYSyHnajNWPxiUhIgGBf4PI0T\n' +
|
||||
'/Jg1ea/aDykxv0rKHnw9/5vYGIsM2st/kR7l5mMecg/2Qa145HsLfMptHo1ZOPWF\n' +
|
||||
'9gcuttPTegY6aqKPhGthIYX2MwSDMM+X0ri6m0q2JtqjclAjG7yG4CjbtGTt/UlE\n' +
|
||||
'WMlSZwKBgQDslGeLUnkW0bsV5EG3AKRUyPKz/6DVNuxaIRRhOeWVKV101claqXAT\n' +
|
||||
'wXOpdKrvkjZbT4AzcNrlGtRl3l7dEVXTu+dN7/ZieJRu7zaStlAQZkIyP9O3DdQ3\n' +
|
||||
'rIcetQpfrJ1cAqz6Ng0pD0mh77vQ13WG1BBmDFa2A9BuzLoBituf4g==\n' +
|
||||
'-----END RSA PRIVATE KEY-----',
|
||||
cert: '-----BEGIN CERTIFICATE-----\n' +
|
||||
'MIICpDCCAYwCCQCuVLVKVTXnAjANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDEwls\n' +
|
||||
'b2NhbGhvc3QwHhcNMTUwMjEyMTEzMjU4WhcNMjUwMjA5MTEzMjU4WjAUMRIwEAYD\n' +
|
||||
'VQQDEwlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDp\n' +
|
||||
'nlCqHD6hZ+G0SIwcTfYe33ibBMGkB+O3e8+lfwzsMwJPAezXFxe9DiVDevCt4NM5\n' +
|
||||
'Z2gl4QTLuAzFeofSPDRZ8HH7wgglTr3Gy07JPsVEUO8DXJNFdjbK30rmzpIqR6f9\n' +
|
||||
'5sx+ZlH7Tc0PhQWwPOTPQV+ByOuReaswXkSHlRdf/71gd5TGWnSAwC379K6ZKaAJ\n' +
|
||||
'rKzFkuMul3i5fP5f4lXGyTUAKDU2gQ7uVYkL/dYE/swhy7bBWwx3Zd6AtPSBZh9P\n' +
|
||||
'X+OwgfrhT+ZL9JVcL1CHEyX1h+SjaTHU6JiEPGEezvlElmGs1HGB+LCBJvnQbDg0\n' +
|
||||
'BBoMjGv8Mk90uGCeljpRAgMBAAEwDQYJKoZIhvcNAQELBQADggEBABXm8GPdY0sc\n' +
|
||||
'mMUFlgDqFzcevjdGDce0QfboR+M7WDdm512Jz2SbRTgZD/4na42ThODOZz9z1AcM\n' +
|
||||
'zLgx2ZNZzVhBz0odCU4JVhOCEks/OzSyKeGwjIb4JAY7dh+Kju1+6MNfQJ4r1Hza\n' +
|
||||
'SVXH0+JlpJDaJ73NQ2JyfqELmJ1mTcptkA/N6rQWhlzycTBSlfogwf9xawgVPATP\n' +
|
||||
'4AuwgjHl12JI2HVVs1gu65Y3slvaHRCr0B4+Kg1GYNLLcbFcK+NEHrHmPxy9TnTh\n' +
|
||||
'Zwp1dsNQU+Xkylz8IUANWSLHYZOMtN2e5SKIdwTtl5C8YxveuY8YKb1gDExnMraT\n' +
|
||||
'VGXQDqPleug=\n' +
|
||||
'-----END CERTIFICATE-----',
|
||||
|
||||
/*
|
||||
// default iojs cipher set, copied from https://certsimple.com/blog/a-plus-node-js-ssl
|
||||
ciphers: [
|
||||
'ECDHE-RSA-AES256-SHA384',
|
||||
'DHE-RSA-AES256-SHA384',
|
||||
'ECDHE-RSA-AES256-SHA256',
|
||||
'DHE-RSA-AES256-SHA256',
|
||||
'ECDHE-RSA-AES128-SHA256',
|
||||
'DHE-RSA-AES128-SHA256',
|
||||
'HIGH',
|
||||
'!aNULL',
|
||||
'!eNULL',
|
||||
'!EXPORT',
|
||||
'!DES',
|
||||
'!RC4',
|
||||
'!MD5',
|
||||
'!PSK',
|
||||
'!SRP',
|
||||
'!CAMELLIA'
|
||||
].join(':'),
|
||||
*/
|
||||
honorCipherOrder: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Mixes existing values with the default ones.
|
||||
*
|
||||
* @param {Object} [opts] TLS options
|
||||
* @returns {Object} Object with mixed TLS values
|
||||
*/
|
||||
function getTLSOptions(opts) {
|
||||
let result = {};
|
||||
|
||||
opts = opts || {};
|
||||
|
||||
Object.keys(opts).forEach(key => {
|
||||
result[key] = opts[key];
|
||||
});
|
||||
|
||||
Object.keys(tlsDefaults).forEach(key => {
|
||||
if (!(key in result)) {
|
||||
result[key] = tlsDefaults[key];
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
1196
imap-core/test/fixtures/append.eml
vendored
Normal file
1196
imap-core/test/fixtures/append.eml
vendored
Normal file
File diff suppressed because it is too large
Load diff
24
imap-core/test/fixtures/chunks.js
vendored
Normal file
24
imap-core/test/fixtures/chunks.js
vendored
Normal file
File diff suppressed because one or more lines are too long
599
imap-core/test/fixtures/mimetorture.eml
vendored
Normal file
599
imap-core/test/fixtures/mimetorture.eml
vendored
Normal file
|
@ -0,0 +1,599 @@
|
|||
Subject: Ryan Finnie's MIME Torture Test v1.0
|
||||
From: Ryan Finnie <rfinnie@domain.dom>
|
||||
To: bob@domain.dom
|
||||
Content-Type: multipart/mixed; boundary="=-qYxqvD9rbH0PNeExagh1"
|
||||
Message-Id: <1066976914.4721.5.camel@localhost>
|
||||
Mime-Version: 1.0
|
||||
Date: 23 Oct 2003 23:28:34 -0700
|
||||
|
||||
|
||||
--=-qYxqvD9rbH0PNeExagh1
|
||||
Content-Type: text/plain; CHARSET=US-ASCII
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Welcome to Ryan Finnie's MIME torture test. This message was designed
|
||||
to introduce a couple of the newer features of MIME-aware MUAs, features
|
||||
that have come around since the days of the original MIME torture test.
|
||||
|
||||
Just to be clear, this message SUPPLEMENTS the original torture test,
|
||||
not replaces it. The original test is still very much valid these days,
|
||||
and new MUAs should strive to first pass the original test, then this
|
||||
one.
|
||||
|
||||
By the way, the message/rfc822 parts have Content-Descriptions
|
||||
containing Futurama quotes. Bonus points if the MUA display these
|
||||
somewhere.
|
||||
|
||||
Have fun!
|
||||
|
||||
Ryan Finnie
|
||||
|
||||
--=-qYxqvD9rbH0PNeExagh1
|
||||
Content-Disposition: inline
|
||||
Content-Description: I'll be whatever I wanna do. --Fry
|
||||
Content-Type: message/rfc822
|
||||
|
||||
Subject: plain jane message
|
||||
From: Ryan Finnie <rfinnie@domain.dom>
|
||||
To: bob@domain.dom
|
||||
Message-Id: <1066973156.4264.42.camel@localhost>
|
||||
Mime-Version: 1.0
|
||||
Date: 23 Oct 2003 22:25:56 -0700
|
||||
Content-Type: text/plain; CHARSET=US-ASCII
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Subject: plain jane message
|
||||
From: Ryan Finnie <rfinnie@domain.dom>
|
||||
To: bob@domain.dom
|
||||
Content-Type: text/plain
|
||||
Message-Id: <1066973156.4264.42.camel@localhost>
|
||||
Mime-Version: 1.0
|
||||
Date: 23 Oct 2003 22:25:56 -0700
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
This is a plain text/plain message. Nothing fancy here...
|
||||
--=-qYxqvD9rbH0PNeExagh1
|
||||
Content-Disposition: inline
|
||||
Content-Description: Would you kindly shut your noise-hole? --Bender
|
||||
Content-Type: message/rfc822
|
||||
|
||||
Subject: messages inside messages inside...
|
||||
From: Ryan Finnie <rfinnie@domain.dom>
|
||||
To: bob@domain.dom
|
||||
Content-Type: multipart/mixed; boundary="=-9Brg7LoMERBrIDtMRose"
|
||||
Message-Id: <1066976111.4263.74.camel@localhost>
|
||||
Mime-Version: 1.0
|
||||
Date: 23 Oct 2003 23:15:11 -0700
|
||||
|
||||
|
||||
--=-9Brg7LoMERBrIDtMRose
|
||||
Content-Type: text/plain; CHARSET=US-ASCII
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
While a message/rfc822 part inside another message/rfc822 part in a
|
||||
message isn't too strange, 200 iterations of that would be. The MUA
|
||||
should have some sense when to stop looping through.
|
||||
|
||||
--=-9Brg7LoMERBrIDtMRose
|
||||
Content-Disposition: inline
|
||||
Content-Description: At the risk of sounding negative, no. --Leela
|
||||
Content-Type: message/rfc822
|
||||
|
||||
Subject: the original message
|
||||
From: Ryan Finnie <rfinnie@domain.dom>
|
||||
To: bob@domain.dom
|
||||
Content-Type: multipart/mixed; boundary="=-XFYecI7w+0shpolXq8bb"
|
||||
Message-Id: <1066975745.4263.70.camel@localhost>
|
||||
Mime-Version: 1.0
|
||||
Date: 23 Oct 2003 23:09:05 -0700
|
||||
|
||||
|
||||
--=-XFYecI7w+0shpolXq8bb
|
||||
Content-Type: text/plain; CHARSET=US-ASCII
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
by this point, I should be the 3rd layer deep!
|
||||
|
||||
I also have an attachment.
|
||||
|
||||
--=-XFYecI7w+0shpolXq8bb
|
||||
Content-Disposition: attachment; filename=foo.gz
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Type: application/x-gzip; NAME=foo.gz
|
||||
|
||||
H4sIAOHBmD8AA4vML1XPyVHISy1LLVJIy8xLUchNVeQCAHbe764WAAAA
|
||||
|
||||
--=-XFYecI7w+0shpolXq8bb--
|
||||
|
||||
--=-9Brg7LoMERBrIDtMRose--
|
||||
|
||||
--=-qYxqvD9rbH0PNeExagh1
|
||||
Content-Disposition: inline
|
||||
Content-Description: Dirt doesn't need luck! --Professor
|
||||
Content-Type: message/rfc822
|
||||
|
||||
Subject: this message JUST contains an attachment
|
||||
From: Ryan Finnie <rfinnie@domain.dom>
|
||||
To: bob@domain.dom
|
||||
Content-Disposition: attachment; filename=blah.gz
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Description: Attachment has identical content to above foo.gz
|
||||
Message-Id: <1066974048.4264.62.camel@localhost>
|
||||
Mime-Version: 1.0
|
||||
Date: 23 Oct 2003 22:40:49 -0700
|
||||
Content-Type: application/x-gzip; NAME=blah.gz
|
||||
|
||||
SubjectthismessageJUSTcontainsanattachmentFromRyanFinnierfinniedomaindomTobo
|
||||
bdomaindomContentDispositionattachmentfilenameAblahgzContentTypeapplication/
|
||||
xgzipnameAblahgzContentTransferEncodingbase64ContentDescriptionAttachmenthas
|
||||
identicalcontenttoabovefoogzMessageId1066974048426462camellocalhostMimeVersi
|
||||
on10Date23Oct20032240490700H4sIAOHBmD8AA4vML1XPyVHISy1LLVJIy8xLUchNVeQCAHbe7
|
||||
64WA
|
||||
|
||||
--=-qYxqvD9rbH0PNeExagh1
|
||||
Content-Disposition: inline
|
||||
Content-Description: Hold still, I don't have good depth perception! --Leela
|
||||
Content-Type: message/rfc822
|
||||
|
||||
Subject: Attachment filename vs. name
|
||||
From: Ryan Finnie <rfinnie@domain.dom>
|
||||
To: bob@domain.dom
|
||||
Content-Type: multipart/mixed; boundary="=-1066975756jd02"
|
||||
Message-Id: <1066975756.4263.70.camel@localhost>
|
||||
Mime-Version: 1.0
|
||||
Date: 23 Oct 2003 23:09:16 -0700
|
||||
|
||||
|
||||
--=-1066975756jd02
|
||||
Content-Type: text/plain; CHARSET=US-ASCII
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
In this message's attachment, the Content-Disposition has a
|
||||
filename of blah1.gz, while the Content-Type has a name of
|
||||
blah2.gz. What should be done? Well, since this is an attachment
|
||||
(as indicated in the Content-Disposition), the MUA should
|
||||
suggest a filename of blah1.gz. The MUA *COULD* find a way to
|
||||
represent the name of blah2.gz somewhere else, it's not needed.
|
||||
|
||||
--=-1066975756jd02
|
||||
Content-Disposition: attachment; filename=blah1.gz
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Description: filename is blah1.gz, name is blah2.gz
|
||||
Content-Type: application/x-gzip; NAME=blah2.gz
|
||||
|
||||
H4sIAOHBmD8AA4vML1XPyVHISy1LLVJIy8xLUchNVeQCAHbe764WAAAA
|
||||
|
||||
--=-1066975756jd02--
|
||||
|
||||
--=-qYxqvD9rbH0PNeExagh1
|
||||
Content-Disposition: inline
|
||||
Content-Description: Hello little man. I WILL DESTROY YOU! --Moro
|
||||
Content-Type: message/rfc822
|
||||
|
||||
Subject: No filename? No problem!
|
||||
From: Ryan Finnie <rfinnie@domain.dom>
|
||||
To: bob@domain.dom
|
||||
Content-Type: multipart/mixed; boundary="=-1066975756jd03"
|
||||
Message-Id: <1066975761.4263.70.camel@localhost>
|
||||
Mime-Version: 1.0
|
||||
Date: 23 Oct 2003 23:09:21 -0700
|
||||
|
||||
|
||||
--=-1066975756jd03
|
||||
Content-Type: text/plain; CHARSET=US-ASCII
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
When searching for a suitable name to suggest for a filename,
|
||||
the MUA should probably follow this order. First, look for
|
||||
Content-Disposition's filename attribute. If that is missing,
|
||||
look for Content-Type's file attribute. If that is also missing,
|
||||
I would recomment taking the Content-Description, stripping off
|
||||
any characters that cannot be used in a filename, and suggesting
|
||||
that.
|
||||
|
||||
If none of those fields are available, the MUA could just make
|
||||
up a random filename. SOMETHING is better than nothing.
|
||||
|
||||
--=-1066975756jd03
|
||||
Content-Disposition: attachment
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Description: I'm getting sick of witty things to say
|
||||
Content-Type: application/x-gzip
|
||||
|
||||
H4sIAOHBmD8AA4vML1XPyVHISy1LLVJIy8xLUchNVeQCAHbe764WAAAA
|
||||
|
||||
--=-1066975756jd03--
|
||||
|
||||
--=-qYxqvD9rbH0PNeExagh1
|
||||
Content-Disposition: inline
|
||||
Content-Description: Friends! Help! A guinea pig tricked me! --Zoidberg
|
||||
Content-Type: message/rfc822
|
||||
|
||||
Subject: html and text, both inline
|
||||
From: Ryan Finnie <rfinnie@domain.dom>
|
||||
To: bob@domain.dom
|
||||
Content-Type: multipart/mixed; boundary="=-ZCKMfHzvHMyK1iBu4kff"
|
||||
Message-Id: <1066974044.4264.62.camel@localhost>
|
||||
Mime-Version: 1.0
|
||||
Date: 23 Oct 2003 22:40:45 -0700
|
||||
|
||||
|
||||
--=-ZCKMfHzvHMyK1iBu4kff
|
||||
Content-Type: text/html; CHARSET=utf-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 TRANSITIONAL//EN">
|
||||
<HTML>
|
||||
<HEAD>
|
||||
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; CHARSET=UTF-8">
|
||||
<META NAME="GENERATOR" CONTENT="GtkHTML/1.1.10">
|
||||
</HEAD>
|
||||
<BODY>
|
||||
<FONT COLOR="#f8cc00">This is the HTML part.</FONT><BR>
|
||||
It should be displayed inline.
|
||||
</BODY>
|
||||
</HTML>
|
||||
|
||||
--=-ZCKMfHzvHMyK1iBu4kff
|
||||
Content-Type: text/plain; CHARSET=US-ASCII
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
This is the text part.
|
||||
It should ALSO be displayed inline.
|
||||
|
||||
--=-ZCKMfHzvHMyK1iBu4kff--
|
||||
|
||||
--=-qYxqvD9rbH0PNeExagh1
|
||||
Content-Disposition: inline
|
||||
Content-Description: Smeesh! --Amy
|
||||
Content-Type: message/rfc822
|
||||
|
||||
Subject: text and text, both inline
|
||||
From: Ryan Finnie <rfinnie@domain.dom>
|
||||
To: bob@domain.dom
|
||||
Content-Type: multipart/mixed; boundary="=-pNc4wtlOIxs8RcX7H/AK"
|
||||
Message-Id: <1066974089.4265.64.camel@localhost>
|
||||
Mime-Version: 1.0
|
||||
Date: 23 Oct 2003 22:41:29 -0700
|
||||
|
||||
|
||||
--=-pNc4wtlOIxs8RcX7H/AK
|
||||
Content-Type: text/plain; CHARSET=US-ASCII
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
This is the first text part.
|
||||
It should be displayed inline.
|
||||
|
||||
--=-pNc4wtlOIxs8RcX7H/AK
|
||||
Content-Type: text/plain; CHARSET=US-ASCII
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
This is the second text part.
|
||||
It should also be displayed inline.
|
||||
|
||||
--=-pNc4wtlOIxs8RcX7H/AK--
|
||||
|
||||
--=-qYxqvD9rbH0PNeExagh1
|
||||
Content-Disposition: inline
|
||||
Content-Description: That's not a cigar. Uh... and it's not mine. --Hermes
|
||||
Content-Type: message/rfc822
|
||||
|
||||
Subject: HTML and... HTML?
|
||||
From: Ryan Finnie <rfinnie@domain.dom>
|
||||
To: bob@domain.dom
|
||||
Content-Type: multipart/mixed; boundary="=-zxh/IezwzZITiphpcbJZ"
|
||||
Message-Id: <1066973957.4263.59.camel@localhost>
|
||||
Mime-Version: 1.0
|
||||
Date: 23 Oct 2003 22:39:17 -0700
|
||||
|
||||
|
||||
--=-zxh/IezwzZITiphpcbJZ
|
||||
Content-Type: text/html; CHARSET=utf-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 TRANSITIONAL//EN">
|
||||
<HTML>
|
||||
<HEAD>
|
||||
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; CHARSET=UTF-8">
|
||||
<META NAME="GENERATOR" CONTENT="GtkHTML/1.1.10">
|
||||
</HEAD>
|
||||
<BODY>
|
||||
<B>Bold!!!</B><BR>
|
||||
<BR>
|
||||
What do we have here... This message is an HTML message. Also attached
|
||||
is an HTML FILE. Both of these are in a multipart/mixed part.<BR>
|
||||
<BR>
|
||||
Now, the first HTML part (what you're reading now) should be displayed
|
||||
if the MUA is HTML-capable. If it is not, the MUA could possibly offer
|
||||
this part up as an attachment to download, seeing as how no plaintext
|
||||
part is offered as an alternative.<BR>
|
||||
<BR>
|
||||
However, the second HTML part is listed with a disposition as
|
||||
attachment. Therefore, it should be offered as an attachment, not
|
||||
displayed inline.
|
||||
</BODY>
|
||||
</HTML>
|
||||
|
||||
--=-zxh/IezwzZITiphpcbJZ
|
||||
Content-Disposition: attachment; filename=htmlfile.html
|
||||
Content-Type: text/html; NAME=htmlfile.html; CHARSET=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
<html>
|
||||
<head><title>This is an Attachment</title></head>
|
||||
<body>
|
||||
<p>The title says it all...</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
--=-zxh/IezwzZITiphpcbJZ--
|
||||
|
||||
--=-qYxqvD9rbH0PNeExagh1
|
||||
Content-Disposition: inline
|
||||
Content-Description: The spirit is willing, but the flesh is spongy, and
|
||||
bruised. --Zapp
|
||||
Content-Type: message/rfc822
|
||||
|
||||
Subject: smiley!
|
||||
From: Ryan Finnie <rfinnie@domain.dom>
|
||||
To: bob@domain.dom
|
||||
Content-Type: multipart/signed; micalg=pgp-sha1; protocol="application/pgp-signature"; boundary="=-vH3FQO9a8icUn1ROCoAi"
|
||||
Message-Id: <1066972996.4264.39.camel@localhost>
|
||||
Mime-Version: 1.0
|
||||
Date: 23 Oct 2003 22:23:16 -0700
|
||||
|
||||
|
||||
--=-vH3FQO9a8icUn1ROCoAi
|
||||
Content-Type: multipart/mixed; boundary="=-CgV5jm9HAY9VbUlAuneA"
|
||||
|
||||
|
||||
--=-CgV5jm9HAY9VbUlAuneA
|
||||
Content-Type: multipart/related; type="multipart/alternative";
|
||||
boundary="=-GpwozF9CQ7NdF+fd+vMG"
|
||||
|
||||
|
||||
--=-GpwozF9CQ7NdF+fd+vMG
|
||||
Content-Type: multipart/alternative; boundary="=-dHujWM/Xizz57x/JOmDF"
|
||||
|
||||
|
||||
--=-dHujWM/Xizz57x/JOmDF
|
||||
Content-Type: text/plain
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
If this sentence is red, you are viewing the HTML part.
|
||||
Wow, what a complicated message. This message is laid out as so:
|
||||
|
||||
multipart/signed
|
||||
| multipart/mixed
|
||||
| | mutipart/related
|
||||
| | | multipart/alternative
|
||||
| | | | text/plain
|
||||
| | | | text/html
|
||||
| | | image/png (smiley)
|
||||
| | image/gif (dot)
|
||||
| application/pgp-signature
|
||||
|
||||
:)
|
||||
|
||||
A smiley face should be embedded into the HTML part above (if you are
|
||||
viewing the HTML part), while the red square dot should be attached, not
|
||||
displayed inline. Additionally, this whole message is PGP signed.
|
||||
|
||||
This message introduces a few tricks that the MUA should cope with.=20
|
||||
First of all, the related / alternative combination doesn't make much
|
||||
sense. Here's the current setup in pseudo-code:
|
||||
|
||||
relationship between (alternative: HTML or text part) and PNG part
|
||||
|
||||
Why would the text part be related in anyway to the PNG? Instead, the
|
||||
correct and more logical way to do things would be:
|
||||
|
||||
alternative: (relationship between HTML and PNG parts) or text part
|
||||
|
||||
However, many MUAs compose a message using the first method, so this
|
||||
should be taken care of when parsing the message.
|
||||
|
||||
Additionally, notice that the inline image has a disposition of
|
||||
"attachment". Despite this being in there, the smiley should be
|
||||
embedded inline in the HTML part, not offered as an attachment.=20
|
||||
Conversely, the GIF image should be offered as an attachment, not
|
||||
displayed inline.
|
||||
|
||||
If the MUA is not PGP capable, at the very least it should recognize
|
||||
multipart/signed the same as multipart/mixed, and offer the
|
||||
application/pgp-signature part as an attachment.
|
||||
|
||||
--=-dHujWM/Xizz57x/JOmDF
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 TRANSITIONAL//EN">
|
||||
<HTML>
|
||||
<HEAD>
|
||||
<META HTTP-EQUIV=3D"Content-Type" CONTENT=3D"text/html; CHARSET=3DUTF-8">
|
||||
<META NAME=3D"GENERATOR" CONTENT=3D"GtkHTML/1.1.10">
|
||||
</HEAD>
|
||||
<BODY>
|
||||
<FONT COLOR=3D"#f80000">If this sentence is red, you are viewing the HTML p=
|
||||
art.</FONT><BR>
|
||||
Wow, what a complicated message. This message is laid out as so:<BR>
|
||||
<BR>
|
||||
multipart/signed<BR>
|
||||
| multipart/mixed<BR>
|
||||
| | mutipart/related<BR>
|
||||
| | | multipart/alternative<BR>
|
||||
| | | | text/plain<BR>
|
||||
| | | | text/html<BR>
|
||||
| | | image/png (smiley)<BR>
|
||||
| | image/gif (dot)<BR>
|
||||
| application/pgp-signature<BR>
|
||||
<BR>
|
||||
<IMG SRC=3D"cid:1066971953.4232.15.camel@localhost" ALIGN=3D"bottom" ALT=3D=
|
||||
":)" BORDER=3D"0"><BR>
|
||||
<BR>
|
||||
A smiley face should be embedded into the HTML part above (if you are viewi=
|
||||
ng the HTML part), while the red square dot should be attached, not display=
|
||||
ed inline. Additionally, this whole message is PGP signed.<BR>
|
||||
<BR>
|
||||
This message introduces a few tricks that the MUA should cope with. F=
|
||||
irst of all, the related / alternative combination doesn't make much sense.=
|
||||
Here's the current setup in pseudo-code:<BR>
|
||||
<BR>
|
||||
<I>relationship between (alternative: HTML or text part) and PNG part</I><B=
|
||||
R>
|
||||
<BR>
|
||||
Why would the text part be related in anyway to the PNG? Instead, the=
|
||||
correct and more logical way to do things would be:<BR>
|
||||
<BR>
|
||||
alternative: (relationship between HTML and PNG parts) or text part<BR>
|
||||
<BR>
|
||||
However, many MUAs compose a message using the first method, so this should=
|
||||
be taken care of when parsing the message.<BR>
|
||||
<BR>
|
||||
Additionally, notice that the inline image has a disposition of "attac=
|
||||
hment". Despite this being in there, the smiley should be embedd=
|
||||
ed inline in the HTML part, not offered as an attachment. Conversely,=
|
||||
the GIF image should be offered as an attachment, not displayed inline.<BR=
|
||||
>
|
||||
<BR>
|
||||
If the MUA is not PGP capable, at the very least it should recognize multip=
|
||||
art/signed the same as multipart/mixed, and offer the application/pgp-signa=
|
||||
ture part as an attachment.
|
||||
</BODY>
|
||||
</HTML>
|
||||
|
||||
--=-dHujWM/Xizz57x/JOmDF--
|
||||
|
||||
--=-GpwozF9CQ7NdF+fd+vMG
|
||||
Content-ID: <1066971953.4232.15.camel@localhost>
|
||||
Content-Disposition: attachment; filename=smiley-3.png
|
||||
Content-Type: image/png; name=smiley-3.png
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC+klEQVR42n2TbUjVdxTHP/+H69Xd
|
||||
a2VWlFe69rzthZJUoxeNOWoFGxEhYRRFmZSVW2u9ab2KejWE1qDNBkEQhS82VoiaZkVPmoWaKNM5
|
||||
mA+opbd771//997//T/+epHBarEPHA6Hc84XDnwP/JcwcBS4AVgzcR04ONN7C+md+pcPCz44dPLA
|
||||
arZs/gg1UABuGkvvp7X1Iad+itE/YtUAle8TuH26sujzqq/LkJQsnOQQVmIASVJQMhehZORiJwc5
|
||||
d76FH2pf3gY2Aigzy7+eObqmtOqbXbjGGHZqCM+eQpJ9AHhWFCc5CAjWf1KAkppc+qg3vRCol4Fw
|
||||
0aqcisOVW3HTE7hmBElSKD/5GFkNMhH1KDvegST78CwNSfZxeM88VuYrh4CwAuxqvxL6MnPuWiy9
|
||||
H1kNUPH9fZofDKPpHn8/z+Z6Yw8JK5stX5VhRO6h+OfiV3WaHxtPVKAwmF+KqXUDMkgqZ0+UoKcE
|
||||
P57/GXOqh46ODqrPXUQfufb6YOGxJOQD2CaHQnnlAJ4zDXggHBYvK6ap6Rau+RIz1k7djd+YHrqM
|
||||
pXUC4KQnWTRPAdiuRqNRkFQG/omRNJOsKVQw408xtS4QDsI10AaqEY6O8Fzq70fJy3XI8gsA5HTa
|
||||
rBdOkvwFKj39EWrr/sJzEnj29OvsphGugfBsLlwbZnjcYN36LxiLuADtMtCUetFAcE4ee8s+pbHV
|
||||
YtOemwhHx3MSaPEY3X9OUnqsk5a2OMeP7KC3t4u+3gRALUC4cEW2eN62Q4ze3SAiz74TDxvOiI+X
|
||||
BcTsoCoyfJKYn6OKmrMbxGRnlXhyJSSqv80Vq0KSAFa+ceKl0wcK9lfsW42TGsE/pxhfcDmKfz6e
|
||||
FUPg4iRH6Ov6g9EJh1t341xusWuAyn9b+c7BrbklJ8oDZGTOQpL9ePY08SmDpCEwbcHwuE370yku
|
||||
Nlj3gM/e90yXliyU9+8sCVJYlEUgU8IwBZruMThm83uzxsAYV4Hd/A9h4BjQBthAFOgDLgDF7w6/
|
||||
ArI6YJ0eTQeGAAAAAElFTkSuQmCC
|
||||
|
||||
--=-GpwozF9CQ7NdF+fd+vMG--
|
||||
|
||||
--=-CgV5jm9HAY9VbUlAuneA
|
||||
Content-Disposition: attachment; filename=dot.gif
|
||||
Content-Type: image/gif; name=dot.gif
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
R0lGODdhCgAKAKEAAAAAANUAAP///8PDwywAAAAACgAKAEACHZSPMssLKoIMYLyR1I2z3sZsE2VB
|
||||
owcBqlqurloAADs=
|
||||
|
||||
--=-CgV5jm9HAY9VbUlAuneA--
|
||||
|
||||
--=-vH3FQO9a8icUn1ROCoAi
|
||||
Content-Type: application/pgp-signature; name=signature.asc
|
||||
Content-Description: This is a digitally signed message part
|
||||
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
Version: GnuPG v1.2.2 (GNU/Linux)
|
||||
|
||||
iD8DBQA/mLdEKZYQqSA+yiURAjAnAJ90G22jbX/Broy0F541R0UUbsb6zgCeJn0d
|
||||
02Vq9Sv6aXE+YM0lRn3jZDc=
|
||||
=uwCM
|
||||
-----END PGP SIGNATURE-----
|
||||
|
||||
--=-vH3FQO9a8icUn1ROCoAi--
|
||||
--=-qYxqvD9rbH0PNeExagh1
|
||||
Content-Disposition: inline
|
||||
Content-Description: Kittens give Morbo gas. --Morbo
|
||||
Content-Type: message/rfc822
|
||||
|
||||
Subject: the PROPER way to do alternative/related
|
||||
From: Ryan Finnie <rfinnie@domain.dom>
|
||||
To: bob@domain.dom
|
||||
Content-Type: multipart/alternative; type="multipart/alternative"; boundary="=-tyGlQ9JvB5uvPWzozI+y"
|
||||
Message-Id: <1066973557.4265.51.camel@localhost>
|
||||
Mime-Version: 1.0
|
||||
X-Mailer: Not Evolution
|
||||
Date: 23 Oct 2003 22:32:37 -0700
|
||||
|
||||
|
||||
--=-tyGlQ9JvB5uvPWzozI+y
|
||||
Content-Type: text/plain; CHARSET=US-ASCII
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
If this sentence is green, you're viewing the HTML part.
|
||||
|
||||
Now, this is the way that all MUAs SHOULD treat this kind of situation.
|
||||
The layout is like so:
|
||||
|
||||
multipart/alternative
|
||||
| text/plain
|
||||
| multipart/related
|
||||
| | text/html
|
||||
| | image/gif
|
||||
|
||||
See? The GIF (which by the way should be inline towards the top of this
|
||||
message) is related to the HTML, and that whole block is an alternative
|
||||
to a text/plain part. This is the opposite of the way shown in the
|
||||
previous email.
|
||||
|
||||
Also, the embedded image here does not have a filename. As mentioned
|
||||
above, the MUA should suggest something as a filename, even here (the
|
||||
user may want to save the embedded image, so a filename would be
|
||||
helpful). In this case, I would recommend appending the random text
|
||||
to be suggested to the user with the part's subtype, in this case
|
||||
something like c20vsidlkvm.gif.
|
||||
|
||||
--=-tyGlQ9JvB5uvPWzozI+y
|
||||
Content-Type: multipart/related; boundary="=-bFkxH1S3HVGcxi+o/5jG"
|
||||
|
||||
|
||||
--=-bFkxH1S3HVGcxi+o/5jG
|
||||
Content-Type: text/html; CHARSET=utf-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 TRANSITIONAL//EN">
|
||||
<HTML>
|
||||
<HEAD>
|
||||
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; CHARSET=UTF-8">
|
||||
<META NAME="GENERATOR" CONTENT="GtkHTML/1.1.10">
|
||||
</HEAD>
|
||||
<BODY>
|
||||
<FONT COLOR="#00fc00">If this sentence is green, you're viewing the HTML part.</FONT><BR>
|
||||
<IMG SRC="cid:1066973340.4232.46.camel@localhost" ALIGN="top" ALT="" BORDER="0"><BR>
|
||||
Now, this is the way that all MUAs <B>SHOULD</B> treat this kind of situation. The layout is like so:<BR>
|
||||
<BR>
|
||||
multipart/alternative<BR>
|
||||
| text/plain<BR>
|
||||
| multipart/related<BR>
|
||||
| | text/html<BR>
|
||||
| | image/gif<BR>
|
||||
<BR>
|
||||
See? The GIF (which by the way should be inline towards the top of this message) is related to the HTML, and that whole block is an alternative to a text/plain part. This is the opposite of the way shown in the previous email.<BR>
|
||||
<BR>
|
||||
Also, the embedded image here does not have a filename. As mentioned above, the MUA should suggest something as a filename, even here (the user may want to save the embedded image, so a filename would be helpful). In this case, I would recommend appending the random text to be suggested to the user with the part's subtype, in this case something like c20vsidlkvm.gif.
|
||||
</BODY>
|
||||
</HTML>
|
||||
|
||||
--=-bFkxH1S3HVGcxi+o/5jG
|
||||
Content-ID: <1066973340.4232.46.camel@localhost>
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Type: image/gif
|
||||
|
||||
R0lGODlhBQALAPIAAKIA/64A/8ZL/////8BS/2QDANZ//wAAACH5BAEAAAMALAAAAAAFAAsAAAMY
|
||||
OBIytsIYEoiEl0lqFWgKM4zkUJzjWQwJADs=
|
||||
|
||||
--=-bFkxH1S3HVGcxi+o/5jG--
|
||||
|
||||
--=-tyGlQ9JvB5uvPWzozI+y--
|
||||
|
||||
--=-qYxqvD9rbH0PNeExagh1--
|
2180
imap-core/test/fixtures/mimetorture.js
vendored
Normal file
2180
imap-core/test/fixtures/mimetorture.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1411
imap-core/test/fixtures/mimetorture.json
vendored
Normal file
1411
imap-core/test/fixtures/mimetorture.json
vendored
Normal file
File diff suppressed because one or more lines are too long
104
imap-core/test/fixtures/mimetree.js
vendored
Normal file
104
imap-core/test/fixtures/mimetree.js
vendored
Normal file
|
@ -0,0 +1,104 @@
|
|||
'use strict';
|
||||
|
||||
module.exports.rfc822 = '' +
|
||||
'Subject: test\ r\ n ' +
|
||||
'Content-type: multipart/mixed; boundary=abc\r\n' +
|
||||
'\r\n' +
|
||||
'--abc\r\n' +
|
||||
'Content-Type: text/plain\r\n' +
|
||||
'\r\n' +
|
||||
'Hello world!\r\n' +
|
||||
'--abc\r\n' +
|
||||
'Content-Type: image/png\r\n' +
|
||||
'\r\n' +
|
||||
'BinaryContent\r\n' +
|
||||
'--abc--\r\n';
|
||||
|
||||
module.exports.mimetree = {
|
||||
childNodes: [{
|
||||
header: ['Content-Type: text/plain'],
|
||||
parsedHeader: {
|
||||
'content-type': {
|
||||
value: 'text/plain',
|
||||
type: 'text',
|
||||
subtype: 'plain',
|
||||
params: {}
|
||||
}
|
||||
},
|
||||
body: 'Hello world!',
|
||||
multipart: false,
|
||||
boundary: false,
|
||||
lineCount: 1,
|
||||
size: 12
|
||||
}, {
|
||||
header: ['Content-Type: image/png'],
|
||||
parsedHeader: {
|
||||
'content-type': {
|
||||
value: 'image/png',
|
||||
type: 'image',
|
||||
subtype: 'png',
|
||||
params: {}
|
||||
}
|
||||
},
|
||||
body: 'BinaryContent',
|
||||
multipart: false,
|
||||
boundary: false,
|
||||
lineCount: 1,
|
||||
size: 13
|
||||
}],
|
||||
header: ['Subject: test',
|
||||
'Content-type: multipart/mixed; boundary=abc'
|
||||
],
|
||||
parsedHeader: {
|
||||
'content-type': {
|
||||
value: 'multipart/mixed',
|
||||
type: 'multipart',
|
||||
subtype: 'mixed',
|
||||
params: {
|
||||
boundary: 'abc'
|
||||
},
|
||||
hasParams: true
|
||||
},
|
||||
subject: 'test'
|
||||
},
|
||||
body: '',
|
||||
multipart: 'mixed',
|
||||
boundary: 'abc',
|
||||
lineCount: 1,
|
||||
size: 0,
|
||||
text: '--abc\r\nHello world!\r\n--abc\r\nBinaryContent\r\n--abc--\r\n'
|
||||
};
|
||||
|
||||
module.exports.bodystructure = [
|
||||
['text',
|
||||
'plain',
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
'7bit',
|
||||
12,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
],
|
||||
['image',
|
||||
'png',
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
'7bit',
|
||||
13,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
],
|
||||
'mixed', ['boundary', 'abc'],
|
||||
null,
|
||||
null,
|
||||
null
|
||||
];
|
||||
|
||||
module.exports.command = '* FETCH (BODYSTRUCTURE (("text" "plain" NIL NIL NIL "7bit" 12 1 NIL NIL NIL NIL) ("image" "png" NIL NIL NIL "7bit" 13 NIL NIL NIL NIL) "mixed" ("boundary" "abc") NIL NIL NIL))';
|
582
imap-core/test/fixtures/nodemailer.eml
vendored
Normal file
582
imap-core/test/fixtures/nodemailer.eml
vendored
Normal file
|
@ -0,0 +1,582 @@
|
|||
Content-Type: multipart/mixed;
|
||||
boundary="----sinikael-?=_1-14507714776260.4520441491622478"
|
||||
From: =?UTF-8?Q?Sender_Name_=F0=9F=91=BB?= <sender@example.com>
|
||||
To: =?UTF-8?Q?Receiver_Name_=F0=9F=91=A5?= <receiver@example.com>
|
||||
Subject: Nodemailer is unicode friendly =?UTF-8?Q?=E2=9C=94?=
|
||||
X-Laziness-Level: 1000
|
||||
X-Mailer: nodemailer (1.10.0; +http://www.nodemailer.com; Stub/1.0.0)
|
||||
Date: Tue, 22 Dec 2015 08:04:37 +0000
|
||||
Message-Id: <1450771477632-949c9a1f-bddfb7f5-b8b49a6d@example.com>
|
||||
MIME-Version: 1.0
|
||||
|
||||
------sinikael-?=_1-14507714776260.4520441491622478
|
||||
Content-Type: multipart/alternative;
|
||||
boundary="----sinikael-?=_2-14507714776260.4520441491622478"
|
||||
|
||||
------sinikael-?=_2-14507714776260.4520441491622478
|
||||
Content-Type: text/plain
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
Hello to myself!
|
||||
------sinikael-?=_2-14507714776260.4520441491622478
|
||||
Content-Type: text/watch-html; charset=utf-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<b>Hello</b> to myself =F0=9F=8E=A9
|
||||
------sinikael-?=_2-14507714776260.4520441491622478
|
||||
Content-Type: multipart/related; type="text/html";
|
||||
boundary="----sinikael-?=_5-14507714776260.4520441491622478"
|
||||
|
||||
------sinikael-?=_5-14507714776260.4520441491622478
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<p><b>Hello</b> to myself <img src=3D"cid:note@example.com"/> =
|
||||
=F0=9F=91=A3</p><p>Here's a nyan cat for you as an embedded =
|
||||
attachment:<br/><img src=3D"cid:nyan@example.=
|
||||
com"/></p>
|
||||
------sinikael-?=_5-14507714776260.4520441491622478
|
||||
Content-Type: image/png; name=image.png
|
||||
Content-Id: <note@example.com>
|
||||
Content-Disposition: attachment; filename=image.png
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lE
|
||||
QVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQ
|
||||
AAAAAElFTkSuQmCC
|
||||
------sinikael-?=_5-14507714776260.4520441491622478
|
||||
Content-Type: image/gif; name="nyan cat =?UTF-8?Q?=E2=9C=94=2Egif?="
|
||||
Content-Id: <nyan@example.com>
|
||||
Content-Disposition: attachment;
|
||||
filename*0*=utf-8''nyan%20cat%20%E2%9C%94.gif
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
R0lGODlh9AFeAaIHAAAAAP+Z/5mZmf/Mmf8zmf+Zmf///wAAACH/C05FVFNDQVBFMi4wAwEAAAAh
|
||||
+QQJBwAHACwAAAAA9AFeAUAD/3i63P4wykmrvTjrzbv/YCiOZGmeaKqubOu+cCzPdG3fQK7vfO//
|
||||
wKBwSCwaj8ikcsks3p7QqFTSrFqv2Kx2yxVOv+AwiTgom8/otHrNbrvf8Lh8Tq/b7/j4UMzv04x5
|
||||
eAGDhIWGh4iJiouMjY6PkJGSk4+BdkZ+mZocgJZ1lKChoqOkpaaGnnSYm6ytVEWpZaeztAEEt7i5
|
||||
urW6vQS1wI2xq67Fxp2pwcqgvs28zbjL0oXDTsbXrchtz9C509/g4eKLb8TY533abNzdt+Pv8PG1
|
||||
5dbo9mHqa/L7/IhZs+280aJH5IKWewhfkXnTr6EyIwIiSpxIsWJFIw4TEcxisf+jx4nmErLKpyaj
|
||||
yVMQP6qkiPEkNTdcVsqUGHJKlywjSKZxqSzgL1opZwpVSUTeRiFDk36sKeUmlpywGPIE5rNWUKVY
|
||||
IxaNdzRI1q9a64EhArYjTgs60UxdK+lq2bcgnVaBS9ci0yhk64a9YjCqG7aAHbnVC1buXMJ670LR
|
||||
gtiwDzjyAgYWXASx5cuYMy8V+4UxYcc9IMeTPJnRYM2oU6tWqlhTTLpcRJee5vPW6dW4c+u23DrT
|
||||
a7hpz0Cq/XM2qNuZDShfzrx53t11m0tn/rxubz+/3wY3M7y28VDIMU8fXx362/HTywPnLJIB6CWy
|
||||
v/MLf9nz7iFC7Vtn317BeyX/8cknD328cQQdfjPpBxt//UGlRYACEjcLgRIVYOGFGGaooYbmdZja
|
||||
hiCGqOF1DXYQm1QCEiIhSpXNJOKLHHoo42Uw1pghiSWqsB0d3VWVYgAURmRjjfkhmFiL66ln0ZBD
|
||||
4pgjCjvO0SNpKQYpAJMvFonUZ0hq16VHWNbo5JMmRBkLdz9a9WVq/7XJxJEFkTmDmzmcaeedeFbj
|
||||
1Yx83segnHjRmeeghBYK05Z9JqramICWYCRudEYq6aSUVupUo9ghCqmlnHbq6aecYpqOpgs+hdaa
|
||||
XzHaGaqp/umAlSupKqpNpCbJ16lKZiVrU6zq6moDsBL166yjglqmm8Qq1Oax/7cm6+yz0EYr7bTU
|
||||
Vmvttdhmq+223Hbr7bfghivuuOSWa+656Kar7rrstuvuu/DGK68roNZr771nzesuvvz2668X+oa7
|
||||
kKEEF2zwwQh31UPA1Q6c8MMQRyxxHnsw/KSZdqapMVcI72oxDn4dvPHI4iTs8cc1YAwhyYasCFA7
|
||||
I+tZMcojhbwOLS6zrPM+Cv9AczY264OzdzsXzXEbJ/8cQ5RGy/fPKTmb0rMPfZmqdAZMN23c06ZE
|
||||
XcrUC+Pa7NVVD7Gy1u8Eu9lWU4G9BJwzX5012vuo7VFLbR+qYFlJ64ivg2ajSLctVLKYa2Zsv+P2
|
||||
Dn7Gic/fIsw9uI8T9opZ4v/jLK5D43GvWquXY08g+eBpW24evoqyNGzKBsL93tnfFP6j3anXrlvf
|
||||
KewN+n+w0wbzxrTbLjybq/ORHd9b9E56y7UFP/zz0O/VeaC6+/qg4MsvQpzz0XdfO+6sV4/V6NoT
|
||||
TTf3WKEn3eHeq+/co6VOD7T4rAVd0iNeG42+Uu5TB7/3E+nfcthnPfn1B1/Ky15bTFcgLJzuc2Zp
|
||||
3X4c960T/UVj+QMPAysUJhABEIAdfBH4GmXBbWDQfKWwUgg39EHvrTBEI7SWyrBXPsoJSIUvxJCW
|
||||
9uQ6Hu4OghTJ4YZi2DD7CQJ/KHTaBoUkRAvtEAiNWaJMKNTEGxVvXDNMhgL/TSPFCf7LMT0EAtn8
|
||||
Y8RCbZGLBGzgFw0TRp/BK1ITi6McB/C/FtoxLhRUGhznyMeH1fGOdyTitf6omTUa8pCIpNMY3QPE
|
||||
VlmNAvuTHsCuEUlJBqFsjUyKIOVFSEeGTlmZHMomX1BJATipkqN8Y72Y9R8ZKtJRj1wk4DzFyve4
|
||||
Elmw/KQsd8nLXvryl8AMpjCHScxiGvOYyEymMpfJzGY685nQjKY0p0nNalqTmonMpjZxec0cbfOb
|
||||
4LxJNy8WznKaswnj9OY518nOPKZzEw7rozznSU+ZufGdtBJCPffJz37KwYD4DF8Q/EnQghYUoAEd
|
||||
QxkJdcaGvuRgqZRmFj3h/9CKmuyKCX3VQgdVUYde1J0ZFZs+E9bRhn4UoSGNwERvRrcMikJ2KbLn
|
||||
JVOKtY3yaHJJLAVMBSRTMdIUAysVWktzSoqdyqen9/xpBYJ6v6HasGu/0xhSqaZUkQ6UhkUlakm3
|
||||
OgjN5aCqVgVCAifhUq521KsAAOtSbToAdkTVrHBFq1ohyVa3dgOueA2AXOcKyqteMK+lu8LLnvo1
|
||||
vaGUkbEcJvkA+w2u6VSrotjrWhMrzMUyVhqOzSphSSFZulI2mJa97CRQWQS2oLWUEfVWaEUbCdJi
|
||||
7iSn7WKsMKrKeAqVtRpM4xODYFrDOtCLkxwL5EKw2p1tdhSlTNAQjOJbH/+uJrUe4Ncs/WpC0h1X
|
||||
FMmd4nKPxobsahek1LPXdMWK1aZdN7edrM924YFWzgXXc+KNXF2Xd97jyNZWQGAu0u6L35k+LpT1
|
||||
0yUEiovbRnj3h58aHnRNlN7xffYBBC7wIg6MPHspmLYw0K0mH6xR2zZVwqPlL2pQJ7wFc0KC8Xtd
|
||||
eWP31tmJGJAwFhZ4F4Pi/jpmrMEw6ncoHOMWmngD9Cugiv/6Dh1v7cU9TjIeDytQK0SxTTimSour
|
||||
hGQlK/nHJzheYZK34oZur8pWDnOK/ZvPGnsSC1G2bvPALOY2n5nMvAqyKLlM5K1+WcNuzjNw4Rze
|
||||
39q4CmnGqU94rOdC0wT/w2X2M4LJW+caGpllhLaIAJWDZ+FN2gCVVu6MXbOFPTO6uo0oK6TZnJRL
|
||||
Z7p2pm5wgJlsvE6P+dMsDTVkdRbpiqQawM+7tXP/TFVKuprXoeky8+pLslpTRNdQjDGyf/BkVqMD
|
||||
gcK+rLGX7OQD4dqUZq6ws88B7UZLeNqHVnRuVG3JJjT7vSXqNqhBDCRSz1ncm9r13bK95U1jq4Sx
|
||||
TpGoF4jnKhbA0Obx94WwbA9831bfsw5xv6sIcOgI3EIEfzad1x2hhPOb3FdieMN18/ACRBxQTN0J
|
||||
EonNFhw2cbc+OLe8hbzyID7843IKuVpG/mjAmFyIKO+BypMNbB7IpOMw/yeTzIVD8ykfeeEn13TL
|
||||
38zzRTN7JUBHtGrZKocpGX02N89hzn3OpVPPW7dRt7e5hv4JuIK73O10N7bFri+y39SsZ1972pHQ
|
||||
xl7LjeoZM7valT531Eq9W3s0qODngPGN3+7v6wr84BffXJ0b/nuIV5fiGU/5NBT+8c+NfLomX/nO
|
||||
0/HamM882891+Vf3/fSolxQvS9/z1Lv+9V1YPeg9Dfva257DKGO902/P+94fQfZLZzk6w/r01uuA
|
||||
22o/pbuDjsXZf1fAEF6+5m3g99EfwLXbXqTu1wb9Dm//ItNvcvA3bH3so1uxzp8t7hH7fdVZ/7/t
|
||||
d7+zzc9n9Bsrl60sIv83x7uEbsZXocuifwEIgFbgf6uEf7YkgPlHgFXAVw74gBAYgRI4gRRYgRZ4
|
||||
gRiYgRq4gRzYgR74gSAYgiI4giRYgiZ4giiYgiq4gizYgi74gjAYgzI4gzRYgzZ4g8bkezp4LzgY
|
||||
XTv4g7TUgycGhERIKUI4hEWYhPt3hJikhE5oGEyoAU84hY4RhTVFhVjIBVa4AB7meV74hRRzfi7Y
|
||||
hWBYhma4ODVIhme4hmxYBtlHgmrYhnIIhm84gnE4h3hYeXUogneYh34oeHsYgn34h4TYT4EYgW4n
|
||||
JewWMx0TfuOUiFW3iBtzUmL4gJAYB5I4iY34fj91iYGWiS5BifUHgZ7/GG2g2FsQ5YgGOIh5d4o8
|
||||
tYmHGFCl6G2uOBmi6FMVmIhqRnKSUHO2mArMV0y6KGi+2ItX9x1TFTa5iHefWHG8GAnFCBjJyAMX
|
||||
OIxOFY1W5wuM6AnBSEzWiDb7RlbHaBzTuAPVyIymuDHhaIx3pYncqIrX9I1as47QOI6zUY7HZ4Hy
|
||||
aF4WJ47tKFXACI/WFGHsiI21eFSNl1QTSJD1+IwHiZD7xYk0xZDZCA0P2TSdtZDzNVgGeZGlkZES
|
||||
SJFF948eyTIgiYgbCTX9WJLkmJB2p5F9aFcWyZIm6ZLKSIEiSZOEkFmjQI+QcJIDli/CmJI6CQk8
|
||||
+VIrGQlAGX3rB3yB/0OLRTlhWMCR9kgJS+l9BXhMOVmURxkKPlkJNkmNxNd/WkmUUWlgU6mSDgmW
|
||||
ETl/QumNZnmWUilYatmRwhCW5jiWSoBMW8lu9JdfaxFbXvd1sbh5cVmSf/kDqNhdv2Z8XzVGfQli
|
||||
iekDi7kGWlZvhYkukflt0vdaJiGY8Udto/gxm1lgk9kDlakGl8l0CvkzpYlbp8kDqWl5jbl7N6lH
|
||||
h+mRsbkDs4kGqyl8rUkzr8lau6kDvXkGv+lgEklKwwUCw1kaa0kZg4mZgMleeLk5h7ecLiBd8hWT
|
||||
9GWXaBSa5Mdb3GWZe8d3o/kE3ElcuVk00YmW08maslmeqnmez1eJ6v/ZnB/wnJPxnuGZftpWnYpz
|
||||
nTngXukJMjzYnU9JcfMInnMpnu9GntbZlgAaoAdKffrpg965iyRpCnHnmeDQXtmZmVCSoQy2ocQ4
|
||||
k5UTn8A5nxPKmCzaoreZaMVHe3uplzzQjH5pn7ZZKRemnS0Qo+DXfcDSng8Zd/eZYCUmkPzHdXWX
|
||||
BE0IawdHk0iqfvXyoySaZRAablk5WQuab1TKoxbqKViKnwg6fqvWgDi6AzoqmWJKnUpqO92oUluK
|
||||
djfqpSMFlRdZpTJ2pUsKpCwgpPKnpnhKXWDKknzKfaBSphcqfubWdbyTjlLWoVQmqKGXOnMalPDW
|
||||
o13QppqlommSqJf/ejlMqqDV9qQ34ak9WZWTIaqjqkZmiqGbOqaGoapISak39KavaqC4KFyzCqc3
|
||||
JqkyuQvAo6u7Om6lyp6/Kp9OYateyaqB4arHaqMzGmdXsHO1KqxDg6tKZKnT6iGZioTLOp5opq3X
|
||||
2A7S+q2YmqwaSm9pegXOyjJ3VqfqmmThyn7jGqHlqqdnNK8VWq95dq9cWJvACmjmCo5r5q0A22MC
|
||||
S0ZytnVMEK8k469ourAb17DXR7DMCh8H26CDZqwW2z0Ym5zkCq8dy48fq7AhG0jseqLuWrJWILEj
|
||||
Q7FNt7KjirFRehiOCQBR9pWVSq8rsWwpp2yTprI4u6ZJQK05Kqk+/5urKhu0RQu0uSG0joeqeelr
|
||||
D4ueUvphjNC03Sq1H0G1TgpIYss4kBqrxfKy+mqoU6oIXrtjIHtsUfuvfFK22Gm1+Yi1aguxS8uv
|
||||
hfC2Rwe2HmG3BUq0AmS0LbsCJLu2WytyjgC4WBe3ATS3FTs8hAsA2PqS9KKxMtq3DJoIkFsacXe5
|
||||
PUa6Z9uoBXcvMps9H7q36lW5Wcu4wXlAqnuyHdW6+UqqsOu6squ5DaJuh2qaksulpzqiNdqnxWt6
|
||||
vku79rK6y4O712ptu5u7MDu77QG8bSu8T4u8Omu8Q5ukj4q3jyl0teu3jAW9yRtvNauo3au81bot
|
||||
Bse1zuig/iBF/v9ms2URdlkKchMXvMYRuoWQdS+Ev2Chv2irgCZrvoABwDtpvxpHwFhhwKhLLfHr
|
||||
uD/CwIMgwCsEwVkhwb06ddejwGyBwe2GdDjHwUrhwdabLRU8c2lCwhocQiicwi+XuCLRwkT3wkkp
|
||||
nRh3vzM8FCq8vNqCw2iiw/6ZCDHcQT8MxDUMqPCLjiJMwoHVww9spevLqXeLxYWrEkH8vobJij27
|
||||
wy6RxGHCt1ustGa7sz/XxPuLwI17BxXpDC5mwlqntVWLxllMq9/7EV0slu+yj24rxidBxlhixph7
|
||||
ulfcuWnMx2x8wM0HxkwryCZByExiyJl7x3qMyR3Rx1e7L1D8uYj/IMXjQMlDYsmIfLwFu8dg0sgT
|
||||
LC6ADLqSnBGkbCOmLL6HrMZQx8ofzC6zCMoNhb46aMva98mWgFfA7HvCDJnEHAjGPLx2envJTDa9
|
||||
7L+/7Mxyt4PRfHeQrEVwZ81FmM24uc0UpXfby77IjMfjq81fWjDNXM6Eic3onFa1BWWFWE+Cy8FH
|
||||
y79uUs/2TLdLbBc2/MT7zM/zdM8QnM8xJygELU8GTcAITb4DvdB81ND4+9DkFNESLUcUbbMWrU4Y
|
||||
ndETs9Er29F6i8rEk4UoPXdOadIjltIuzU4rrcqL8tI0XU4xrcknXdM6nU03Pbait9NAbUg9vcg/
|
||||
HdRG3S9Dnccz/33UTG2i4ZzIhdTUUn2AskSvB4G0Z5zJfnwMyXdFxZnOQ1m5hqx8lkrSTLmlZA2h
|
||||
Zg0tVv2WfQXV77rL84PWXt2ZbTwvbd2UDqvWAV2i3prW/ky8rTzMYm3HZFmocF29Qsxpf13XZd3X
|
||||
X5zYykmk+FrY5rzCvtHV5WfXjlzVgT2ohOpZfO3EZ/rZz7zVosPZg63M98eAoHFLA9ikUBqPVC3b
|
||||
4uTGVYiAwzeQtW2qr43bUKjbTLCKn1JLv03Br+Taw03brW3bsQfccmHcy83bze3buY3cS6isXbqF
|
||||
3N3d3v3d4B3e4j3e5F3e5n3e6J3e6r3e7N3e7v3e8B3f8j3f9Ctd3/Z93/id3/q93/zd3/793wAe
|
||||
4AI+4ARe4AZ+4Aie4Aq+4Aze4A7u4AkAACH5BAkHAAcALAAAAAD0AV4BQAP/eLrc/jDKSau9OOvN
|
||||
u/9gKI5kaZ5oqq5s675wLM90bd9Aru987//AoHBILBqPyKRyySzentCoVNKsWq/YrHbLFU6/4DCJ
|
||||
OCibz+i0es1uu9/wuHxOr9vv+PhQzO/TjHl4AYOEhYaHiImKi4yNjo+QkZKTj4F2Rn6ZmhyAlnWU
|
||||
oKGio6SlpoaedJibrK1URallp7O0AQS3uLm6tbq9BLXAjbGrrsXGnanByqC+zbzNuMvShcNOxtet
|
||||
yG3P0LnT3+Dh4otvxNjnfdps3N234++0XPCQ5dZjWejo6mvz/f6i8hq180arHhETWvKd26fmn8OH
|
||||
AYYImEixosWLGCcagWdw/48FIxlDihRgTmEUhmkgqpwncaTLixvfdfTyscjLmyTtmTwJ681KaQN/
|
||||
sRSCs+hNIrVmXjHKNGPJnTh6uvm5LGi/lk2zVkRa0A0XrWCfSkkYAyUaqminYQXLVmQXLG3jhhTL
|
||||
E4sMs2fS6g22Vq7fnG+r/B0M2CMMkH7pbsBrpt/AvbX6Ep5MubJlpzpdIJarWANjWfMeQ54l+bLp
|
||||
06i1dkaxOW5gK3BGiwvq7lHr1Lhz67Zs+G7p3Rg/D4BEW6jsb7dPG1jOvLlzIsD/Op/eHPrg3mV/
|
||||
R98qdduj4sfBJTdNvbz17W3LUz+fmKaYr5y7rwtPf9D4y1p03w+ehTD2L//wuSYfP/XRt19l+eV2
|
||||
oEUJ/vUfVA689kNsBRoC3jsLClDAhhx26OGHIG6I3oiVhWjiiRw+CCEDEvpAYYWEXDhOhijWCCJO
|
||||
7MUXhH82uWTjjym6t6IKwslBHG0wSkIjkD/iqJ1qRF3X40hM/qjikPeQEcuRViXJ5UBP4tfimHC1
|
||||
dxCWNhQZC2hewhMmb2TG2YSDmaGZHRZr5qnnnnyqkSGJgFK2mp1QaNHnoYgmesmUgTZ62qCEssZo
|
||||
W3JWaumlmGaqaZ2RTvGnk5uGKuqopJZ6RKd8fHoUpxC8ydSZneYoIKwRqPoSpKimY6lvleZaQaYj
|
||||
XOorNsLeKeewEwArQrH/yDbr7LPQRivttNRWa+212Gar7bbcduvtt+CGK+645JZr7rnopqvuuuy2
|
||||
6+678Ma7k6n01msvPvLCe+++/PYrZL7faqnowAQXbPDBaVwJcLMCI+zwwxBHrMq/Cw+p5pptZiyT
|
||||
wbhWDMbFW2os8jcHd+yxpwPmMfIiMp4iWpvV0HryJsKx087KOP+jFBAzu1IzLS3nLHQ4O//QczYp
|
||||
pzT0ygExEnQpRfuAEL5HL/Dz0iI3zTKSXbVhck12VW110mdhfZyrRsU0TtQ9XGCrS1+De7XZkKE9
|
||||
GVezsL3EbnEHTHZedA/SpZtR8j1EUl5tYbjM7M5t9uAYFu6oRvZOzh3j/2NR/YLjgaNl9+L0Wk5R
|
||||
3x+8PRLpD3AezsuBfy7668Ch3oHpbrG62N+NhXZz5xFJDvvvjcrOyaRs3fsi71tDzgjtwDcPvMLD
|
||||
yxqWvccjn8jTijDv/PaWQ0+k67ipfj3X1mdPPHnqPQf+5Omr7/usPP+xPmriI4J9+YRoD2WZ4Z+P
|
||||
Gf9mit97FKejIVQPf8vQX1YalBpVMbCARksVAeEnhAMW6H7SWFKVUMQ97m2wRt6DiqUsWB8MJtB/
|
||||
FfkgBzvoPBWeKITYAtlUvkM+rGnQhR8C1Y6ktEM6Se8iOAwRDK8lQ+84woQju2EQOaRDIPCohwF8
|
||||
H0aW+KEhWquIlkDgKP/mR0F/vcaHVgwXFgOhRYBIkX5eHBMYKbYuQ0nsjXBkgwJZODnhpcuNccxj
|
||||
HudIx+DZDl18HF0aB0nIQtpLbGP74QL/2AAuziWMC3Hk/7wXSMphDpGvUGRTOiZJ/rARS5pc5CVZ
|
||||
hMJNMhKTjdyVseJ0RUwFS5Wo/BUsNzdLaSkrBMyKpS53ycte+vKXwAymMIdJzGIa85jITKYyl8nM
|
||||
ZjrzmdCMpjSnSc1q6tKQ2Mxmr6xpJ21685tv4WY3wUnOcjZBnGgypzrXeUp0hqFheoynPOcZCEi6
|
||||
kwXwpKc+98nPhH3ynjXIZz8HSlA92hOgnsEdn8qIv5K1E6GtUuieGFr/PoeOEqLJkqieKGo9ix4U
|
||||
omNUGUd559F/YjSTBgzZ42poCtYlKWYfRWhIlbZS5ZXCpTCCqUlPGlGBCoJuSKQETiuk0yDwNAMz
|
||||
LVtNhzoKptanqAI8qix9qlSXsXSkHNXbDqTqNonarBtYDWsAtKoDroItpTOcRVDFStLEXdSsiUSr
|
||||
Ea1qU7YWQmtUIWsOphY2RNbPrlfZwhGvSgq9AoCvV0DlXwFLOC0Mtq6jMCxiraBYrzJWJXgdH2RF
|
||||
IdkSkMWvlr0scs44PbyJo7NT7eTlYrqtxYr2FKplUBE44tYhVrIwOzWXa19bitiuVgi09dpDD3Bb
|
||||
O2ZrtyLbrFpIaxrT/5rCsLdtolH1FVqgOnW5UFTQ4bomxwn27626re5SoRHY7H43CIgT7gPRCN5y
|
||||
IVdjys0gc8UEXO6uIUDnzW1ANacZ8fIWGL69W+VeZ1xJ9ZWWVAXcf5UR4CeGjsDD5VViVxkEEi7Y
|
||||
FA3m4YNFV+ATfBbBcp0PPK6bswz38cQwifCySvkqFaO0gj7RHVh5Z2IU2xi30z0Mi9Pm4lr5d3W7
|
||||
61yNb3ziDnc1lDxu71lhnNZ3kBhnQyYyHY28ZPOWll4Wxt9a7bNjKXu5gfpFapelS6osl2/LvUPy
|
||||
l9dM36iuWM1kHpWZrYfm6LL5zqvK8SvhnGcsx3ikdR6zdNrHHD6/jv/Qhc4waz1gaO0mOHePfTLy
|
||||
7OwSRC+n0ZaztAEwDbcwezjKxfuxZiVNY0H7RdOcdhSqFe3pyf4Awo9mk0AIq0VKj2TV820erq0c
|
||||
aj1T2AewDjGBIh1kjtq6dgBkr93WS6lWSzjZV2byXC9sG1NDUDCOXnZ/NBzBATJblMJuCLUjcexH
|
||||
Qru5gv52tKUmQXW3ONbDGTckyj1JK+gn3dteI7tRNcI/JwnNpNkxFT2E5xENvEOLRpqc5jwagMNW
|
||||
4AcXUcG3E/ENJZwV/W4yjByOYYhHfOIUr/jFh5VUBc86vlnz+MHjzAMHv5rbvLZIxQswcl+VHNIn
|
||||
J3WblEhFlu/A5cD/hrkTfSRyZ1Pr5rJ2Gq2hrPKB+1wHQO9B1F9OpaL7Wm4ajcOXir00ni/x6TmY
|
||||
+s+FTnWRzLzmuUL6HOQNamSzMwn6dnPjso6xcbfd3G9HQty77S61G8nuuUZQ3uEexau3MQsFTfya
|
||||
6A3yLsq9Z3hUvOTxwPjG9xrt44r85Dc/h8pbfn+YF5fmOU/62gb+87HrsbeKO/jWu56QlU11vV9P
|
||||
+9qXKvYBtr3udx8q3J++z7wP/r7gCgLWK5m4vwf7YfnNavAaP/TUlb0nbZt84POdUNKf/eNTmX3Z
|
||||
Ht/3MTfl8ZtveFCSP7fPN7rYctnfbR7dlW8+ljHZ3wL6Q+uWxa+l/zDtvwL+Owv/paN/weR/KUCA
|
||||
yAKAjCaAxLeADNiADviAEBiBEjiBFFiBFniBGJiBGriBHNiBHviBIBiCIjiCJFiCJniCKJiCKriC
|
||||
LNiCLviCMBiDMjiDNNhMwneDw1eDCYiDPHh7Ojg7PRiEovKDQCiERgh/RJhQR7iE8peEYsaEUNgi
|
||||
TqiEUViFXTCFpCRtpbeFXJgH0DeB8NaFYjiGZ/CFEhiGZJiGXGiGEYiGaviGm8eGEOiGcFiHBSWH
|
||||
D0iHdriH+4SHDqiHfBiI8eSH9+R3WidvL8UxqgdSdKdSiFggJVV+DWiIDPeIaBGJ2zeJjZgMlgiJ
|
||||
ivh9R0WJ/taJx/+Bidc3h5voCaT4VJ9IiMskihq3ipBhivt2hqkoYljDcTnnDBkDVaeYh7c4bLm4
|
||||
dKGgc3vhi7XYhsEobuM1Y2rFdTmVClRWTWoXOLqodM7oJcjYNhRYjdaFcpNgjHqxjTxQgd7YjOQF
|
||||
NNBIVNK4iDK1jDQ1jOAoCeKYFuS4Vd0Ij1W1NNeYPOkIM+0Iijz1XluXjbIoMqhli/D2Vf94kAhp
|
||||
epLIgARJQ/PokKUIkZkokaImCv1okXmFkb/4hxtZjMTokfSRkMq4kOpYkSZ5jCCZjKioks/IkhSV
|
||||
WSuBkrjEX0czkS3ZCDZ5CB2ZCDiZfwdWNTzZk4vwkxZSkpQwlAH/WJQ7OZJIqSSCtYsGCTUvyY1Z
|
||||
MmGgJZNTuUVViY31KAxZWY6epZOQJ5VfWW2OZZUN+VxliY9bSVmYdJRraQhKWQhBiQhOuYNcuX5q
|
||||
uZbnh14bo17U132/FZHnYpcWOZhAEFzdNX7Wdjru2FqB+ZWO+QOQeV/DlX6KGV5eeZeIkJk+sJl+
|
||||
0pmT6Xbqt3qXOZWk2QOm6U+SiZiCJJB+E5qiiZfVdyuzVZiReZi5V5nawpgO+Zo8EJtowEmpiXer
|
||||
2S3EeZB3p32waV+n6W6CZ5usiZv8OJYdF35t9pjUKZv5ll+f6V6tmTE0GXDeCSf1lTdxSXjZ1pzc
|
||||
8pz0kZ4Pt57X/0mY7mmY54Zu2Omc59km9tmdQwc64Lmfv9mf31me5EKf4TGgvbWbhVea4Zmc3gVm
|
||||
/zmfAeolEEoK0Tl9B3oK0LWclyeflqmdQ9OhYImf0XEvsDONe/aX7Rdu8ZibusmiqVcvLyqcBYiW
|
||||
/behJvmh4rdh3cOjBiaj9QekHimk70akdWSknwaV+KSkjSmhGOqkjgKj8UeXOoaiNsqkSYalfpSh
|
||||
PyqlZUqj+2ij+WOlymYqO0qm32OmcYqmJqema4qjBloqb+qKT4ikU+ql08CdMAKmqJejfIp8tGlJ
|
||||
I+egzLCOXcemhco9WvpieNppcBpXWiiM4yCoFUKokRqfDDqXlf9KmZeqAIwqVI46NJ76qVdqoreT
|
||||
qDiWkX2aqczoZKkqNKvKqo8CpUcWnKWKqHSKc5t6qyUGqbr6O5PqY7DaYacajsTKdKN6rM6TrD3l
|
||||
q34Wi2UUaLAqrWyGeZ5ZZqPIUNqaq9yKrK4aISQKoqVSiZ0zrsZargXnremaYvXCrtbIlHdKrvDK
|
||||
YefKfdZqKvb6jTrnefsqqf2ahf+6ruGarfjKZdtasDcmr8tKPQurRe4arWyxawV6YhpbdtemlTH6
|
||||
rokJrthqsQ2bZmDasUGHYiordWQnl8/msWMarEnnj886afPaFC3bcja2s2O3dzD7ay5bpIC6lCoK
|
||||
rSlraQ+rGz7/C3UvW1byI7KC0pp7yTQ5yxRNG3Y9q7TGuXwzsLQfOyEVaz8n2zpXaxRZCwA3lrZi
|
||||
B7Vfq6/MWWFjC5RlSzcESxFsu7WI9rCHiqkyGyjvVbVJdLZFkbcsy7VSO7Jm+baJ27Y8wK6Cm3Jg
|
||||
q5rYRp4bS6pL8bR71W7jWaJyW7KyeLeKam+germUOyea67XvdKHrBgQBS22iG6tMcG98Zp1DGpJ1
|
||||
0bmtK7agu4qxa7uO+7OWmrlA67arC7zW57pzS4q/q7v+qW0KCnq4WyisK72fO21VOrlxi7qW+7fS
|
||||
uTepqyvIy5tF24nNG71TW7vO67kg+zHVC260WqMVErnk1nQ9/wexfnF2Bxul63u714uLG1e3BJpr
|
||||
M4e/+Wt1sgohGYe9FyTAEYpkBWzAcaG/oaoQCwzA8+vAHmq/XyfBbUHBCSxClfK6VEG/88bBQeTB
|
||||
H4zA07siF6ypGXy0lOB1KazCYAHCLazAI7y8smHCbPkmEWzDWYHDMDlOC8fDDafBKwrEFad8wRu8
|
||||
Tlt1Ede3+XCObsmLZkPDOOTEqQvFWivFB0fF+qCPdWqzV4mrKLzFyTu0E8qzxevGZsfCRZydNLt2
|
||||
FMmp4aHFLsTFb6y2qRvFcTzF+5t2ZCysZvyWqprGe7zGcBy2wuvIbBwSRNy+WAeIGKwIPuw5iqxC
|
||||
fNzGj+x43v8rc3JMybdZx39HbGdcrBDcxIz8yc2Gsacbyik0yosreoVcs5isxPVJG6tqhH1svO0C
|
||||
iwz8Wr0shL+8ue8izJdMzI0bpjx4zKobzLdsx7DbzEXhy55MyuqizDDMW8UchNAcL9xcqwv2zT0Y
|
||||
ztFnynnCdtasfLWHzsk8zad8Yeb8zNlcy9KMJ4I4ebGLv9QKoPq8z4rXzxD7zxoa0AJ9h4QrxNub
|
||||
wwAzegnNTwRdsAZ9olcQ0Yk30fta0cOJeBhNUBoNrxx9XB790f0U0uU60oQ8sVbY0kEIfqa7uy49
|
||||
07YH07LcpDSd0zVdlwut0z69012ZsD891Otk0ysLykSd1N//ZNSRzL5KrdQh+K1PndRR3dOoCcsN
|
||||
jc9GjNXfq80I27gqDZpC7dCmas2/mgl8e0pSPce/tNZeja5cra5s7cJdC8ypY9VnnS9urdXKGtNO
|
||||
zdfmZ9ZqjddibMssDZw37b9zrcOC7XyEPcjystdBS6lN/coVPMaNTUmPfdlBzUpCKyGtZIAYINq7
|
||||
RNpn6X7TgoBF2ITEZNqiOiahrYCvytrD5NohSyaxjdpPSdv7J9s9qtvRotrRw9sD6NtH6tnvZ9sZ
|
||||
BdzFzdxnitypjYRESdxYWN3Wfd3Ynd3avd3c3d3e/d3gHd7iPd7kXd7mfd7ond7qvd7s3d7u/d7w
|
||||
Hd/yPd/0Il3f9n3f+J3f+r3f/N3f/v3fAB7gAj7gBF7gBn7gCM4BCQAAIfkECQcABwAsAAAAAPQB
|
||||
XgFAA/94utz+MMpJq7046827/2AojmRpnmiqrmzrvnAsz3Rt33iu73zv/8CgcEgsGo9IHGDJbDqf
|
||||
0Kh0Sq1ar9isdsvteq/JsJhmHZjP6LR6zW673/C4fE6v2+/4vLw67vs/ZXqCg4SFhoeIiW58f41/
|
||||
WIpwAZOUlZaXmJmam5ydnp+goaKjn5GLYI6pPJCmbaSvsLGys7S1lq1sWKq7OayFtsDBAQTExcbH
|
||||
wsfKBMLNnYa6vNJkV4fO16/L2snaxdjfldCo0+Qvvm7c3cbg7O3u75tw0eX0K+euwerI8PzAX1H9
|
||||
QMkbF+IflHpD7rEJyLBhLINPPOlbF2ygFRIQnSAUonD/jcOPIANUEUCypMmTKFOStMLQIqMKVlTK
|
||||
nFly3kZz1SSF3NlvJM2fKFkGdEnlQkygSFcSvNmioxqe3yYyC+gzqdWZWIQRhXi1q0qbTFk4TQMV
|
||||
m1SGVb2qFZC14puMTdbKBRtWxVg0ZfOCSyu3r0y4gJf4Hfx3ad0Ud8/oXeyML+HHgQE/nlzT8OET
|
||||
ic0wnMhYmGPKoEOLHv2T7mUNcON0hieVGCgspGPLnk1b7cvTEFLrXN2u9VRPsGsLH0589m0fkZNT
|
||||
Uf3JN293wYUbmE69OvXoxQlb3z4dO+HjPZSLh8JcYuvn7bzP5r5dfXa57K277wsehOT3NDMPQM+f
|
||||
0nzS//cJ919QcIFWHyAF4odVTm/019+AogVYG4QmSfhdUSVQGNp4UZTnICXO8UNhASSWaOKJKKZI
|
||||
ooIsjqbiizCWeKAHGhrI4RMefjjMeSJe8VOMQKbY4pCUBWmkjBjOcOMXFOg3Bygh6viajzQdeSRQ
|
||||
NVr1GX1bpmSlkTPiBhODuJAlJVpUFLfkmlxRZpqYIjiJyJloTqEmm3h2YeNFcGKmW5mABipoGlkS
|
||||
aehgb/bpAmCDNupoK4UeKqltlim6KJX55anpppx26umnT1iaEKYLgmrqqaimquoUonJEql+JOtDl
|
||||
WnxudNRksSoQaaa1toqcpjJs2mqnIwjr6w/GwpCsov/ExgnssdBGK+201FZr7bXYZqvtttx26+23
|
||||
4IYr7rjklmvuueimq+667Lbr7rvwxrvBqvTWa+8/8n5777789ntQvtAG8ujABBdssB1hAlyOwAc3
|
||||
7PDDAyesMBJyWkPnxfA4muvESpA5CMadRGkLZxeL0yvHvzL8McibiFwLyXSaLDHKNeiXDsws59zQ
|
||||
VlLQDITN+fCo89D98ByFz8h6vBDROkdm3llunXIygoBxDDTTLDsdstC2GP2vs1VPfDXWvM1KKRUt
|
||||
vVXpA7dCtra+SntE9nNmeyVU0WpPHUHbF+od7thzQ91TmsS1BYzXXtzpN7iAz71Y3fjZO2lli8Ot
|
||||
8tL/jncG+XuST87W29427g7Ojm/u+enZbcyt6L3pk7l/hKMuu6GqH/snOq8/Tfomu87uO+ozH3Y7
|
||||
Prlz4jLvr/6u/PKU9xyEv1Pk2DLXxWfSe1/xVXe9odlflzysSRab4KSsY3J89ZZsT+v4E37/Fft9
|
||||
O48R/LTH/RT60LkPIP2yZWkhouEDm+lwlSfpOeh8e9FfSb4UJOb5joFACh4G1Lc+PBmwPwj8xogg
|
||||
CCMHzo6DMJKgCe7VJPvhAUrUY9oGQSik0ijwbFLYk52QwkIViTBD9irh5fSAQsGRbYU1NBGWXmi3
|
||||
2LnNiFUK4oluaKmKGQJ/sxjgEaE3HhkGcFxO/AUU/x+CRNpQ8UZWZNW5GAWxMj6Mgh6sX+XapaEv
|
||||
uvGNcDQV0ibQxjja8Y54VM4cJYDGCoGOAVIc4hUXFkgXrvEAfWzeIPeoKyJq6Y8LKCSvxGgrSZZK
|
||||
YolUyiF9tqxL5WlYnBLfJxnJtmcpy5TMCqUA2UTKVrrylbCMpSxnScta2vKWuMylLnfJy1768pfA
|
||||
DKYwh0nMYhrzmMhMpjKXycxm2ieP0Iwmk5zpB2la85qQpObzsMnNbjZBm33wpji5Cc6kLceM6Eyn
|
||||
OvXAxHI+85zrjKc850moRboTMVWgpz73mc523lNWJmzFFqGosWxqM4uEGCj+CrpJcCJ0ZQotHkP9
|
||||
qf/MhwoiotWbqD3/aVG5BS6Fs9jdh2S20Xt29H4f9eHLXFeyQtTuoAG1Q+YyCAuROoiklPwnQHeY
|
||||
h5mCVBY27Q9O5afTBpSvpj/FqEIRp5Gi7jSfuxlZUpW6RaZ+06lGjalmgqZSqkbUqkzAalZ5aiav
|
||||
5kVrxpvqK8C6hPmFTWFHNWtD0Dq9rs6CrQBwK1ysptX9yLUsdNUETdeaN4qW8q0Ai+tfm2HJwqBt
|
||||
KIUtKSAb+76Gakuxiw0GZVNyN37g1SibJZBls4XZzNoitCfpbMYim1M6otaPo8VWaU1Li9cqEiCQ
|
||||
lZoE+QbA2F5rtheza3q6aJwraIW1BlGcYaUFXDr/CZcdtk2K4bqG3H8oV7Kr62vunpvAGQ5nurXA
|
||||
ayYf6VtrNfdM3NUgcYnUucm9lLTapS101zuk9pLPoPAlK17k290Yes6+knqvbOPLX2xE1031Op2A
|
||||
f0tgfgSVaAdOo4Rhu9xonVeq6ihehCc84QWbt8GsYenrNszhNHq4WhdeaYZzR+ISO/DEcBoe5jA6
|
||||
2EqM18U45lJrm5iRC0KxxrDjbY6H/F3sCq/HUY0okCdxYyI7GSkVnoaMPUpjtdrYkU/OcvyOtk07
|
||||
+vgSSx4xlifTve6M2XNlNkCTo4xPLye5rg9GX5N/kuY5D6fOZ4aykanh5gY1x8oazvNg8Czk5RH6
|
||||
/8BsnmxG/gviAk+p0PtbdJEhLdo2EXDH77R0gBvtaOAIGnySFtCY/wdqoq7Suu7ldKc5YedLJnfS
|
||||
piO1jk1dkE+Xek1ffk6Y/UFEJZ5Iy/jxtYkSHUlbzxrXbz4goD3Ta2GvCNjZcTaJiN1ISo9GU7nm
|
||||
za5P22xnQzva0qY2Io09lwImG4PL1my3hf3t4ki7AOIGbb10CFUt6k7EP1y3rwWJaPpW0Ls/Cvee
|
||||
R5hD1+pXpn9OL8iAuG9D9hvgW45CUt4dbwuQ0OD1TmjC47xwfSuR3/6GocQRHHKTUHzgOJw3xuGp
|
||||
8XuvOKUTabFjx2ntIpYXxX1NxKqZXPIN0Zzclf+teIxzPqedyzzoP4d4b4UuppOe0Og9D2PSoSB1
|
||||
LpuLjPzM+iBa3e5bYxpeWNe62O/A9a6X++ZYnPLY115dqpt902gXV9jZTnfdRv3too773xw59b77
|
||||
HVSzrOPfB0/4JQWe74VPvOIDc/iaf27xkI88Fxpv25c+/OtSvrxkxwvjeHHeoJq3OkIcf5WNfV7v
|
||||
7zr9bu8uXaaHJ/S0firrHY5yuKLSk3gCZSdpdHtX7l4svW+6Kms9ylj+3h7Bx02ziJ97sTr/+dCP
|
||||
vvSnT/3qW//62M++9rfP/e57//vgD7/4x0/+8pv//OhPv/rXz/72u//98I+//OdP//rb//74z7//
|
||||
/vfP//77v5aSF4DD93+yJ4AGyCEEmBsHuIAImIAFyIAQmBEO+IARWIFeMIFjZYEaeIEYWGwb+IFb
|
||||
0IEeCIIkiHrjd3B1l4IqaFXvh4Ir+IIwaAauh1UuGIM2WHcz6FQ1eIM8KHY5WFQ72INCqE8/qFNB
|
||||
OIRIuE5FaExOdwc7FzON0nnI1IQI94Q6olGYx1FEZzFWOFJRiF/ORIV10IVX+IUmWExiSAdk6IWD
|
||||
IoXHlIZPsoZCZYZLaEtwKAdyOIdtCIbNdIfZloc8gYWxB4RbeG46s20upwwYM1SiJ1Zp6FMKNwoc
|
||||
xxuM+DXO94iOg4hb83JnUomhAn2YCHP4pmLd/7CILsWHzBSKZKOJacWJUuKJTfV8qog1rAhn29BS
|
||||
hOCGTFiIfiaKrkgLk7gasHhVsqhqsVCLgLgYnwWKxohUkZiM/LGMxeiCNzOK0AiFdpeFJtWM2ZBu
|
||||
1yiMbfeJ05hxxINhwfiN4JiNg2iE3EgKyIiOOyGNl9iOkuiNERVYUCGPmSaBYkOPovCO1YOPgRiO
|
||||
sch8/Gh71AiPvAYYiXiLh0OQxGiQEMFXCamQtSCQYGaPnqCPVLNX/ViRFhlFgdGQy3Bc6tiIHXmQ
|
||||
ieWPIdkJGGkJAHkJHMl7iJUvKdaS6TOSm3iOAgGRYSVKHomQ5DhjOPkKL1kJMXkLPtlWQKmSNv/J
|
||||
khYJexGRW21geVJpieRyk9B4lU2QNieJlXzElQWZlVCpkGK5BF5ZlaB3lj95dWUJj2yZlrmwlrMH
|
||||
ctrYLVqZjHFJlXNpWaRHXnV4E3kJiHuJN18pjvTmX5cWmJUEkkWJCYXpWUuZVxbHlkzplo65ijyp
|
||||
bkpXXKpFC+IFdCJ3l9mVmbS4mQupmHlXBSaplrIWG7poO28pJc/IWHW5dI/1kIepJ4WDijhnmkxT
|
||||
m8zWmf1jXFHjmvxTnGfoK4OJHsLJmarZPp95V5NZdkhHmtvSnLqGmtxGnLBpnLqJnKG2mrX3YcBJ
|
||||
NM+ZmiMHa7lJXbvJBdeFnZc1mzqSnt0ZnYf/AmD56ZvUop1reHRntyoKxp/T4p9kCKD/pioDupy6
|
||||
N5RU9piXgKCjiSoLypijR5/XKKE2J6CMxqCiYqBdqKFdoZ9qZKH1AKJWKKKll2AdaqL0gKI9VIos
|
||||
dpt4pzyxyZwYGlLWmG/eWaMldqMNynLlGGK/yKP46aM4BqQfmqNAtaNYo6JI6kUEylxMeoxOqkI0
|
||||
GqXAM6UWVqXOKKNi1qNa6kFKymPnCQ7cKSVQOqaR5qKEdKZRcaUQlqVsel8eKphI1otVVpvWWafM
|
||||
46Y/k6e4o1Rh1qd+aqPleaEQ8YfblW6Geqi+A6jmZBCM+jqFKpqQmqSJeqKCOqQKdal/manA/yap
|
||||
KUOphog/oLqmorqf8hkWaldWe3qOj7qqLYqSq3BHlZqUHReq2lFmsyoah0an14mYpQpHuaqRRiqs
|
||||
XhGsYjo7zHqkxwaWt9png7qTcipnmHoVz7qeabStbreYtroDeHSs9vmk2WoV3voEHJauTlB1EVms
|
||||
b0SuaYqlvDpovnqu78GucUFy69gLuHqqMIms5lqvfqGvTLCu90qwkxSuNKlprOqgKGWtRbpFv/p4
|
||||
DuuZsZacAcqwHfCaLDJbuooxFeuxETJqGpug0pqSqGanEAur3ziyJ1uyf0myG8qxHECznOOlmQWz
|
||||
4ymlMxuzNZuyDftqLCukRJmh+KpnPYuxd/+HsyvaqpW5tOyls4vFsxernBkrtdFKrBL5BbUaPQD7
|
||||
n0lrl17Lns16W2ULrkLbsUCbs3Aqtgq7sSsrnVl7tVs7ll3LrXSLbHpanwIrCwz3cbQqFycHtWMS
|
||||
t3J7I5W6GCH7aHXzboNLuAJnuIl5tu4qHourF437CYEbRJG7FoXbrzc7tmTLIZmbF5vraTUHuZ/r
|
||||
FaFrs/NCurS3JKdbFqnrkh7nua3bFa+7tqOLuCiruGG7GrfLarlbQ7vLu5MrurELvBNqusPbGcWL
|
||||
PKsrbcl7Fb3LtTihcmF5hH1rixPbNMfLQqX7rVOkt7iJvjKRvXi7vfRSuVLAhRILpsnarKz/u7CW
|
||||
C5jqe7ftSkPLC7tt9r4rB7b2Nr8OWb/QanLWO7sJHLz9q7bmm0TOZqIX170t64Qbd627+rgLjL8N
|
||||
/Lz7CsHq6r8TvKn7qCrw2yFPlMHhmzOdi7wMvL+J+8DnK8Mowb7vekrcuze8WK2tOK/88cLkG8MR
|
||||
nL4jLMI0LMHCRsEFZ8FGy0MsTL8Dy8HeRsRHXMNXHHFFvL7/67t20cQ87L0+DL5SHJytAaWSd7nt
|
||||
u3diDFGdhsaRp8Y5LHc97MaOBseQJ8dtWS5+GL1mhceLp8eXycd1fFFQl79I/HeCTJmYecGm8ISA
|
||||
rHiLjC59/L38FcmJN8lj9KpJOE8VC6ll/5pfi9rJPii7yRvKA9appEyEpry7qMxgqrzKntzKrfvK
|
||||
5jnKsrxPn3yotvybuJzL9LTLftrL/cnJwGxGwlynxFygxnzMEJPMbLrMVOp4JVjN3kR5PWfN2nxN
|
||||
2Hy22/zN0NTNH6xJ4FzObyTONoy25rzO+4LOW1xp7BzPFQxLgifP9izAxoevVonISku5Y+C88HyX
|
||||
qmfCYKfPdDnOI0qqTWGZjBzGlcel7jLQ8smWTAHQqfVHEs28nGTQfqmsAe3FqmDRFLZ5HK3QzCzS
|
||||
FrtnFI2nHn3Rh5TRAEwzMA3S1YbQT+vPYoDS5EzSKC3NjJN8X1x8qXR8zdt8+SzUuMdKS4hK1KgB
|
||||
1HvE1CgA1XWxfCdseLIk1QSH1EOH1VFr1PTs1FEN1lM9gCpr1Uft1Um9JkGK1kOr1lct1lnN1sLH
|
||||
1Ycr16RE103p1mYK112t1CL414Ad2II92IRd2IZ92Iid2Iq92Izd2I792JAd2ZI92ZRd2ZZ92Zid
|
||||
2Zq92Zzd2Z792aAd2qJNAgkAACH5BAkHAAcALAAAAAD0AV4BQAP/eLrc/jDKSau9OOvNu/9gKI5k
|
||||
aZ5oqq5s675wLM90bd94ru987//AoHBILBqPSBxgyWw6n9CodEqtWq/YrHbL7XqvybCYZh2Yz+i0
|
||||
es1uu9/wuHxOr9vv+Ly8Ou77P2V6goOEhYaHiIlufH+Nf1iKcAGTlJWWl5iZmpucnZ6foKGio5+R
|
||||
i2COqTyQpm2kr7CxsrO0tZatbFiquzmshbbAwQEExMXGx8LHygTCzZ2GurzSZFeHztevy9rJ2sXY
|
||||
35XQqNPkL75u3N3G4Ozt7u+bcNHl9CvnrsHqyPD8wF9R/UDJGzfin5N6Q+6xCciwYSyDTzzpWxds
|
||||
oBUTEJkgFKJw/43DjyADVBFAsqTJkyhTksTSzyKjClhUypwpYN5Gc9UkhdzZbyTNnyhZ8nNJ5UJM
|
||||
oEht3mzRUQ3PbxOZBfSJtGrSKsKIQrTKdabSpfZyvnmKLSpDql3TnrSS9U3GJmrjriQINmygsWTz
|
||||
fkMrt6/Mt4CX+B38l27dFE3T6F3sjC/hx4EBP55c8uvhE4nRMJzIWJhjyqBDix790/JlBZGrxOkM
|
||||
LyoxUEdJy55Nu3bXlxtTU1nN2p1rqZ5i2x5OvLhs3D4iQxaL7tPv3u6E1zZAvbr161aMU77O3Xp2
|
||||
ysh7KCec+Qyo59DZSafdvf137YTbd3+/fEoN+vBVljeTvj+l9f+zSUYcgPq9BVp4OFGRH037DeCf
|
||||
fwSSJuBwEQZlIHhFlVDhbboZxNuDlaAHz4YCFGDiiSimqOKKJi7o4mgsxijjiQgCcsVkHXqoE4iU
|
||||
iPgOiTMGuWJV+A32WV8klijkkibW6EGSRBrWQYNznOcaj6IAyeSSUSqI4RQH3vjTlks6yQJgMlCJ
|
||||
CJYNHUlbjnCmhqOUOqAZg5rWsDmVl8bF6eeF5NF5WkFv4WLooYi6JeaLjBZn2qB3FpropJQaCmWj
|
||||
mAZ6EaQoXArUn6CGKuqopJZqJ6f3LWqkqay26uqrUtRnJqpIeFqaoA7Y6hWuqui666Yw8SmrFLQS
|
||||
QWqqoRaL2qj/hIqqLEfMUpOssseKUO2z2Gar7bbcduvtt+CGK+645JZr7rnopqvuuuy26+678MYr
|
||||
77z01mvvvfjmqy2s/PbrL6j6rvvvwAQX/E/A3d5V6cIMN+ywHrMiTI/CD1ds8cUPRyxxEngaoufH
|
||||
Q1H66MarMDcIyJ34WAtnH4sDLMlBdIwXypqoTAvLerqsMcw2NJgOzjQH3ZBWxPIcs8kL5XOl0EwH
|
||||
RHQURh9NcdJNBx2ZREsD8zQUGL3Fs89VWx0Y1mZVpOjL1noNM9hhQ+dmXELBs/UTRqnq18jost02
|
||||
a28fh5XZp0xIIa/n6r132T0JO+DfWp8N6OBot2v43ov1vWDB/5muRbi5k1Oel+X5YZ55ZZuX23k7
|
||||
QFMO+uisv4h34Uh7FFDqe6/e+u3avQ7WqB96nnLWnfiK+/DE15ThTbzv6DsnNmsivFzycVdk8SZF
|
||||
j53tXRYNxHirTi072bQvj8nzcVnvHfasm1/d9Egenyb6kKumPPPAi58J+WoJXtul+nev/fuKw9Tp
|
||||
7HcN/KWlfwGy26+28qX/pY19+SNY73jUPHBoiUwzop4GlYRBGe0MAwY0ScEmCKIK7kWBKOlgBjdY
|
||||
PBV60H0PhN8CYfik2OXBSohr2wVdqKLsxaqBUQgTBE3CQxZ9sG5D5FDkpmRDPOAwfEzbYRFP5MMg
|
||||
AhEKQgTdFP9VdEQNqQ0GMiMEAWkhQ00ZDFRzWmKdvpgg+VFqjLMoo//O6Kc0djFcgMGYHvc4gBCy
|
||||
UIClI5cf50LHQhrykIjUyNpQCLdEOvKRkHTV1xgZQTU+YJC6a8QgjWfJXAXwbnfc2CYzeQBMBtIP
|
||||
owykHAtDw6hdMlozgCWtrgUCWroSArYEo7OoJUsb7fKWwAymMIdJzGIa85jITKYyl8nMZjrzmdCM
|
||||
pjSnSc1qWvOa2MymNrfJzW5685vg1GYkx0lOOIWzV+VMpzohcs5UrPOd8NRCOx0Rz3raE2rzhNZu
|
||||
+MjPfvoTYq3MZxul8M+CGvSgawilQGNIUIQ69KH8VOhCIxD/xpPBEY6VIqVAKyqIi2JUZKfMJ0f1
|
||||
4NExZjSk8xzpDUtqv5N2cqILUKliKGdCWEDRPzoLKEwZIFPN0LR+N9MHyHJqn51KoKfm+WkOVybU
|
||||
lhVCoyJt4h08V9NsNDVnT0VpOwdoU6Cy9KsBmNtBjEpRqSYVGFUFq0kdp1OyctWqS1UrS8XaBLKW
|
||||
1XtOkaterva7uM6CrookwalEaVb+6PVzY+vrTWEB2CV0LSOTxOtMD/sUvtLPr7JoLAAey85FStan
|
||||
lP3RJ9vHlpAF7oOpfCnsPnvW0LZjlQUqrdzYWtRgJZErUMVjYR3k2uiMVi5xe4dmkQhbzamWc7vt
|
||||
rW/BdEUp/7SEtg48KiUPqNVvvTVomFXPbxNIhbacNiN9qq63rkuz7Fpwu37rLuDawL34tTVvyfWd
|
||||
eU/I3PCqt3HfZeDijmu6+FJ1sfT9IetEN2DxJsy/yg2wFQs8sNvlFlzkTXAc0Xs5gjnYwNyKsIQf
|
||||
QuHQWbh1D7YugltzVc8V948ovgp/BTnidwBYaCdOsYxZKdEDs9aw/Xix2Oo74x4Pq7bv0vDPtrG8
|
||||
GPv4yCEmR/Jm9tW0/me6R47yfqM7MVGR0KNOnsQmpcxl6gIZIUtuDlizLBIod/nMoqlxGMKMjzF7
|
||||
9X5mBo36qHPb4c3ZAHXG7Xtv0F7SulHMii0xS7cMlDvn+f92hjYy6b4snsD8uKFMvqyOixzn7cz5
|
||||
0K1LdIf1zOiSIbCSf25zoNUhV0L/RNM8RjGqBfzoscZS0c2NwpU3HJxK29EgjrL1p4G7Z8TA+tZT
|
||||
mDWtOWFqBj1uf7o+9hy5Jq1UA/LGvB12KIo9w3/k+tC7bmSnfanoEc6vhG9WsLNTuEUUodlF5UaR
|
||||
ml+J6VsNTNjpIbM/zJxuKp47P/VuUq8zkNp/wRs68raFFLd4b3zne92ebLex3/3tBwW8FgOfYsHh
|
||||
k+8CILwB/d63bUPdUeeEe8eWq3gVsRhruJT8CUCp+MV5autPYZjl0K6Sx+eLsogXceQoP7lgdG5y
|
||||
mqhc48T/3bSKV45UHI+6Gyamd75x7oQsLrjVJPf5wYFugYxvewNFj/bRiXw41/ya6fcct7ZXzlmD
|
||||
AJDjh5K2loXu9LArvNpUXiNkIxXzNan960N3+9tpTHVdzt3vaDeU2sss9jfp3epxV1ceIcp4QlB7
|
||||
4mbse5Al1fjK4+HxkAfliuW1eMt7ng6YzzyvNx+vzn/+9NB9uuifTXZe7j22h4+97P8V2W7P/va4
|
||||
b1Xt2e7u3Pv+93HafeG9DPzi+wvqdLMm4vG5cd7DnflVfn1KSPlr0pNs+cxu/vA53frttbz3Gq9+
|
||||
9zn//YWjtvx8v/ousJ987ecc2Nk/Zi4H6qdnzZ8D9xdm//7P9MtZ9rKG/WdM+2cXAON60xICA2hX
|
||||
CriADNiADviAEBiBEjiBFFiBFniBGJiBGriBHNiBHviBIBiCIjiCJFiCJniCKJiCKriCLNiCLviC
|
||||
MBiDMjiDNFiDNniDOJiDOriDkmN8PsgvPEgBPziEkhSE0kWESPh/RghzSdiE9beEuOSEUmhOUMhu
|
||||
U3iFgVGFVoiFXGh2WohxXRiGB6ODdYd6ZniGuSB5HFiGaNiGaDh+EMiGbjiHnweHDyiHdJiHjGeH
|
||||
DoiHeviHB8WHDeiHgFiI/SSI4ZR1Mjd4POJSiBhNiigHjIgljqiGibhbiTCJjQhS1rdVmGh3mohT
|
||||
nPiI0P8Uif8WinlRiepnVKbYcKjIGKqYeG71iXnyitARi9A3iLToMbZ4i6NoieCUdf9Fc6MwaZ1B
|
||||
VLLIirsYaVXzcFtHEVjleC/XTcKoVMb4RFzHJsiYiwxYjV1HjKJwjYuxjfGni4T4PW3jjJKWjZSY
|
||||
VZ14Tt6Yjh8HV0jnVNL4jpd4jnn1jeIIPuy4ifdIis8kZDPXj714jKlXjt3YYrOgjgepF8MVhwwp
|
||||
Cw75kGQRkXc4kbFQkRbJExjZhxrZVeDYkQiZX8m4UwTpj6RGktHIXtPITSn5jMowbJaVignZfs3S
|
||||
WdcXkvQoaMpVkxd5k66Wk164k9DGkvMGGCpZj/jlkvj/eFc6SVhHiZQQl1jruJJNmYZPGYV/J5WB
|
||||
N1lUKQtAmQkciQkfyVBjaJRfCVphyWFKKZP7kJUJ9ZJMWJReuU/M2JajMJaYUJaXcJYIyEYSE5N6
|
||||
WQl8eQl+eQtCWVde1JWDyZN6KX6yJVyLGVhCiH7pd5LINZWFCWfbR3z3RZkmyY1Q6XzPp5DsQpgP
|
||||
KZmMI5pOeX7SZyFbuS2qeZCsGZruAJil+ZlWkWQ2tpat1ZmWcJtT8FyjiZpcaXuzuS+Q2ZbE6Vym
|
||||
9Zrhh5mwB4wZ1pxhiXcuh5u0oFmhN310aX/YCTIj6RmmGXkAsV5aqWyGt5zZUpv+UZ7BoJ3gl55y
|
||||
qQZ9/4Zs7okt8Nkf8pmUrDZl0HmfaZCf7SmQuzOeH/OfAneeywYF3iWd+uVeq7hawGl0/MiU/ECf
|
||||
5jegtuCd1Dl6CLoU/RlvBlmVvJlmrVkLIBqboGadtKmgesKgKBqgFAqh6jmX2TYavnmdnGmNGjoi
|
||||
Dnqgxwdi4WmAeAlowmmYQ8pd/3Jh+ymeP7qkT5aiufNhDDaiyCOjJMmhflakWQqjzDmlVEp4Nsp6
|
||||
YDo6PRqjZEqlXiqiaZo5azqmF6p1ZWqmqpcpBKamR1osJdqTyyA+b7p6LjKn78mltnCiPDKohOph
|
||||
UYqkweaK7KCoIMKojXqlj+qniMpUWJl0Vnqpf2So/P+5qUHVqarTpKAKpVqaG6TakD6pQ6iaqmFa
|
||||
oZtZp5vxqmFjqbJqG6IqpbY6O7haNbq6q0RKqyRqZZI6RmT2ncQaqmJ6BGxGNU02j0zqos16ZquK
|
||||
LKByiuKzrCF6rWiWrc32J9y6PN5qreAqZeL6aqFSrvJFrVU6rOmqqqRZDtGKjix1rvI6r7OKk2CG
|
||||
rHl5Ufoaq1yxannKQgYbdcjnWFLDni8aqQHbl/BKaehqFQn7fil2sU3Hc/rksKAJsUq6CYlZc9+a
|
||||
Fhrbcxl7ac85lMnhaOgJaSFbMxPrO8xaPSpLsPlxskzQdv7aaDvam+M5siBTsyWhszsnY0YLADzL
|
||||
sj7/67FK9KtL+Y8eRbQkkbQ9ZrUc27Dg9bKylqyWILQfQ7UCgLVIe7OfmnfIyRT7up1QC5fQ+FVi
|
||||
+7NLq7Adam1Zy65ne6NdG7GIObOeOqxyy3M7i7ZfMLeWSXd5y6utmmBx67Qqim2O+7E9S390i6Yg
|
||||
K2oW2bhbq7eVm5leYLgMi7dnyih/GlqaO6H6aTuBC7TGihlrW58wi7mrWbJPi2sCaqWrC3ahe3aj
|
||||
6zqL+5O0y322y7kYW7eFe7e8e7Ck+7u9dbrDq7iQu7kLu1mNqZz+5rW9AbafYHM8xK+E8XOtG3SJ
|
||||
67mv4q6Lob21dlsi571+Ab6ayW/BS0jXy7cA57cN/6q+S8e+feG+9QqA1usv5qsX6Bs8Sldv+ru/
|
||||
Uxe+VRe/nDS/MRuf9lujFLa+B6wW/Ju2TFSxDQzA2MsaA0xsBZxuFRwXFzy53IazG9wvAZwXH7wJ
|
||||
3OtCunu00wu6KCsTJcy0tcTAohqPVxmkwhrC5RbDSstzNDy4Uldv3cd+OIx/y/jAEkujEALEBEe4
|
||||
xfugGwt/ynsSN8yYgqXDdMnDIhvBlSrFEkfFVzzDgivDNpzA7wtCXryfYCyzUNwfL6xCQkzDRazG
|
||||
KrHFhxuYGrzDTSy7ckyp6VHHHXTHRJzGQ3zE6ZbEb0x0gSytPSy1UUTGN2fGNWzFmax5WsTG/ZvB
|
||||
//+rwEdYp4vott5wqvhrwJhsxGiMxZ1Lbkj8rHU5vtUpyrsZux0XtYEKpOqwtkToyibcC4Kptvro
|
||||
RHeHwsJbfMC8xMLsmMRMyqBIk8jMuj64zFzcss7Mf8U8VcdMy5xsfNbcxzswWC7QivRrutMsxLIX
|
||||
zrvbtHZZzpFMUt3cu04KztMrznIXlfC8zXYweL48hOxMvfBieobYeGI7r73aXxlR0J530Oma0Cy2
|
||||
0AxdeQ4NrhA9LgQ90Q5V0dd60eKS0RodiAw8wuCZqRhNeSG90SNN0sa1rqgC0in9TxzdrB6tWxId
|
||||
0wg108Ra05CixGL40/UkfPRcu0Bd1Pck1FksuUb/vdTrhNSv/LBMHdXl5NRVDKdSfdWRRNVn/M1Y
|
||||
3dWJpNWbPHZevdT33M7S5NP4vIXe3NKy3LEoTH3TbNKl98jTWbE8Pc503bory8zBhNZmfctJncxt
|
||||
jEp53cZ7fc0CWNifDIZ23ac/4NcCfZlrXdK2/Jh/rErxe9f5/NaYPdmyWdkIA9niO9TU3NZa69mL
|
||||
BpukTdQYDEwJ6GsH6H8B6L8FiEyv3Smz3dNKyMS5PUy37bqx/dK7jXXD3dfFTYB/4qu1ncO9rX/H
|
||||
rQLPnaDNrQG/Ddapoa3JDalP6MfBXUzVXb3ZranTDb/jfUvf3cXdrdvlPdrB94Xu/d7wHd/yPd/0
|
||||
L13f9n3f+J3f+r3f/N3f/v3fAB7gAj7gBF7gBn7gCJ7gCr7gDN7gDv7gEB7hyJQAACH5BAkHAAcA
|
||||
LAAAAAD0AV4BQAP/eLrc/jDKSau9OOvNu/9gKI5kaZ5oqq5s675wLM90bd94ru987//AoHBILBqP
|
||||
SBxgyWw6n9CodEqtWq/YrHbL7XqvybCYZh2Yz+i0es1uu9/wuHxOr9vv+Ly8Ou77P2V6goOEhYaH
|
||||
iIlufH+Nf1iKcAGTlJWWl5iZmpucnZ6foKGio5+Ri2COqTyQpm2kr7CxsrO0tZatbFiquzmshbbA
|
||||
wQEExMXGx8LHygTCzZ2GurzSZFeHztevy9rJ2sXY35XQqNPkL75u3N3G4Ozt7u+bcNHl9CvnrsHq
|
||||
yPD8nl9R/YTJG0fhn5N6qe6xCciwISaDT4DpWxdsoJULEJkgdKRw/43Dj9+qCBhJsqTJkyhHYgGp
|
||||
yWJGACljyhQwb6OPjmpY6gwmcqbPkyt33nrzksnPozQJ2lxVTZLQaxOZ8aSCtOpRKwxdUrXKFWXN
|
||||
pTtwpnkKdaKwnl3TlsQaUOsUtXC/Mi1adIRYNGTzkkILt69MukX9Co4pNyzgl3abvtHLGBTfwZCT
|
||||
HjYYubJkRjCwWP45GWIchmYb+9u6ubTp06iBKm0ROHVawJ9Fu4tKTLPr27hz615LxUTr3ZzpxpbN
|
||||
jjYB28CTK1/+uneQzpSvKkb3yThxWchxG9jOvbt3K8wFex/fHXxlzD+g/0N69wwo69dhZb9Nvr75
|
||||
8HDrk78PGT3YCf/qQTFcfB/N59pvuBlI2EuW+fdfBAE+MSCBDSmIGoK3WegVg+c596AL7ZlB4SXw
|
||||
1aIhSQWkqOKKLLboYor4xVjZizTWqKKDH6YQ4gAjWlIiLSeOZOOQLrL3mGBH+hWkkEQ2mSKOOZ6w
|
||||
4xzv0dbjJEs66aSRpPXX5WBLCqBlk1BGaQFsuHh05Vlf5hbhmxBFVpiZIqCZ5lhrTvVWcnD26YWc
|
||||
q9F5pp+B3GnooYWEKeOiXl4kqI6EVoHopJTeoSijmMYV6KMAXrFbpKCGKuqopPbJKQqXNlrqqqy2
|
||||
6uqpNqU605wNyBrcFGAl2ZejFdi6IK+wpufnDITCGqlvwwYLRLH/MjDL6bElOKvstNRWa+212Gar
|
||||
7bbcduvtt+CGK+645JZr7rnopqvuuuy26+678MYr77z0EuHqvfjmi1i94err778Ae8hvsIVWavDB
|
||||
CCdcR5kD01OwwhBHLHGlDDecxJSI5KkxP5TSarEOGE+4MSY/1hJanuIA+/FN0w0yMicl03LymilX
|
||||
vLINO6Yz88s8Z0XUpjcr0fJC+VjZ89Ft/axy0CAPrSbSsgEmkdHAuCUFRvsyDaHTOUEdNV1TRyWQ
|
||||
0jYvUJfWWz9MtNcO6aopW3lZvZ6qAvObM9ttt5lgFT6fwt/eS9N7N96TiK2nFMrBzTHZeqfm8Q3+
|
||||
Jqb204T34zZ+//pmStLjOOsruaROVW554zFmrvlldbPm6af5ijybPhtffvrs4XGO7N8Zth46Pzv3
|
||||
KDvtwANne7QcZmondaJ3YpyvwTc/e9k1mH4r6IslrzxtzOen33e/A78996SrBT01+HJJPfLXG453
|
||||
9mp9X173tLvPHe5Kpv4ooa5nEjPS7DeXEev0SwmGkGQ/QeFvd9bDzurcVDzAwQ91cwMUrto1uCvt
|
||||
Tz4LjMmYiOS8DoppgzYa37cq2KMLviJLIKyRB52XwhqJ0FshQ+AmTPi1AJakhTQyH+I6tCcJdg+H
|
||||
L3pht2JYPU/QUDQoBCKLdBiFBoXPfz9UIouEeD/hUCqBnHigE/8DFiEfFnBcxzsUFjehRR5yUT1e
|
||||
nOAQ7zWxNt6pfyvE1PAIlsHcnfGOeMzjqyhYxwPp8Y+ADOSb3AXHzQGNAYU0CRX7UMbpXW1QNuTK
|
||||
HFeWSAg+slORhGIUcvXErgQOApWc5MekZY5kPYuUIUAl2hBpysy00oCqBMQrV0nLWtrylrjMpS53
|
||||
ycte+vKXwAymMIdJzGIa85jITKYyl8nMZjrzmdCMpjSnSU1YCvKa2MxaNRmZzW56kwvbfMQ3x0lO
|
||||
NYZTDOVMpzqXcM5lnc+N8IynPC31xXaqYHLzzKc+97lIe3oAn/sMqEAl1k9/1opruBijQifRsUMa
|
||||
lIiEWKhCG/r/SYMqAKIukygWKVpQaGJUEBrd6KREWc2PUs5rRyRF70ZUs3o+FKF3qFxKR7FSCrXU
|
||||
nBY9KEBjSriZiqKmBLrpJXOq03eCtKdUswVQ4yPUTRLVASSUWVJDSlVKyA0KT4UqTN1TNPVV9atX
|
||||
fUJWi0qF/MXCp19NXlgPMlazbVVEaYWH1JQ6VVqstQlYy8guoxrXkICNrl6txV01AkmI7PWtPOqr
|
||||
O+ZqsrrOYrDsLKxBDrtTrioWSJ2U5BUYA9kspHGodkPsZU2UWasEJW6MixPdcFovvo52FI30yWnJ
|
||||
0tlKWtKpDXPta0MR21ltVi+17ePbOgpG0YousJjtIQCn0Lc2/9iWiaBtrXFlutQTlnYzioNHZ4Xn
|
||||
0Hfp9mjInUVvCUiF5uZCuI7rbvQ8V6fpIhV2h2vi6aRnPPWSD1+fK6sMdyuK8fqxfPO1L7HYm0r3
|
||||
8he21+UugDVH0lLmK79TMOuBO+Hf9C64vhWFVCZLoy8Jf6O6xKlwHEcMXdw6WMSe1F0ReQffPKGY
|
||||
xDD+FXE18Fzp4MvD2ABxDXcY4x5/1sQgaiCjwrg2Fqsjdgn2sZL/ImAIfyHARcGx9ZaH3iVbWTcz
|
||||
JmsE5WjFFYeUyhu+sphNk2VW/o/BXUbfl7FX5TG7mcMuhVy+Siyg/S4UzC+uivy2E+b47bnPVSmz
|
||||
lOZsY6OedP+Gjj1ajX2yZwMA+nSNfnShgdwD+vq2snA1YqJ7tuiZRDrPyvl0kh2J1ecQmtRSkHLh
|
||||
Ns2zTstE1ModMax5vFqxKuuAXp6wq2UcHQYCeoD1Y20V+6TquO5agEL+7wOBvas4fwjXatZ1m+F8
|
||||
Zl8vO9nBji66vksctI5mw1Jc4puXE+4VCfrWBh6Rtyk87Q+WuwDjJve7C3BuOmI6sWtadxbbPW94
|
||||
xzs5/a63sdJNIX2Tkd/z/jfA5y3wU5m0a5oO73WSWG46N2GLtAZTu91d7oaf8t5Uqg6rkYjwd1vc
|
||||
KGaU74+h8JOAO7tfiA15xHWsF4qH++RLwLjKa71zDTL85fb/fok1RC5xTo+a5+vMeLMzDPMoX/Gr
|
||||
oDZt0m3bYGwR+U5pjXqgp75xnBM2XVdPU9aPrnGug7rqdLrxQNe+XYVD2ePDbhXb5+7crrtdwXC3
|
||||
ptzpznc0HPvu4msyDNnY98L/HfApZjq1Dq9Iszv+8aQipN2RDvnKW/4wkpc0eS/P+c6fjV2M543n
|
||||
R3/GcIZSvTXOe6XJzmSgn17xTHt92VIPdI6wnteUTlue0S5dzeNe21r1/aWBTw6tw/6ikx9+7Uc5
|
||||
S9WZyuGx/Gfzaxn9e04/R9AiQfXRtn1UXf/Z3afx99tK/vKb//zoT7/618/+9rv//fCPv/znT//6
|
||||
2//++M+///73z//++///ABiAAjiABFiABniACJiACriADNiADviAEBiBEjiBFEhJpHeBo1KBoISB
|
||||
HJh9GmhmHRiCEfKBwSeCJtgZJKhlJ7iCepWCbsWCMNiCLngAMViDkzWDNGiDOghODwhyhfeDQDgH
|
||||
qvdUPhiERniEaTCERFWESNiEQaiEOcWETjiFfAeFFiWFVJiFA2WFL6VfWviFR8iFzPRweDBhL8NR
|
||||
y1dSMZcxZrgxaChsS7iGQ9eGKDNSgqeGWJhRdEgzdnh850SGPLWHV/KGxHeFcmgIgsiHiMJ7HnWI
|
||||
v5CIPUKIuReHeXhUkGhTfSiGvASIlsU2BgczLaaIg8CIz//EiZnmiSOnUqE4iIlyh9RkiviGikUH
|
||||
CzTXGE1VauQHi9Q1i9mwipHYin5oeo4YbUjziYh2ZBpzi7aWi8OID3hjjJpQi5wFjJpIWYaWB7so
|
||||
jUTXDW5IjWn4igQnC9B4iaLRWebHbarIi+QoiucVjNuEjjSViuv4i35TjboEjz8lj/OIifX4jdOE
|
||||
j6EwjvtIW6lViF14jXgSNto4kOVYkJMYheF4Vvq4Pn9Vh/0Ihw/webgEkFWijpXDWOy4BnOkkbfE
|
||||
kQyJJRUpVR4pCuYoWf9gjV5IjCd5cEWhkL74WA6JiwWhTSUZkTP5ECk5CwJ5CS3ZKzxpSyZ5kiAp
|
||||
lBMJCkX/uZMyuJE++ZOWsJTi2JSlkJPLCJWGdY9TSZWUYJUSuZKh8JSY1JW5lJRtaHy/hVoX2Wub
|
||||
54/mopZmyJbZJRTBJXzI5o5g95WXaJd8A1xa2QUrt5WhdW9gGQCAWV6C+ZZblm1yWS50KW2xFpfM
|
||||
1Zh1x2yahJHxMpkHtpiX6ZaZiW2Bx5fb5peQqHVeZ14imXxlZ49xF5PO+F7ICAyqOWkAkTSOyXLL
|
||||
FZnk4pkEQpaOcXtLF5qLs5tPgHe+WVyIeVwLyW6V6UBSwJpqEHqiB5t6J5tF9ozPuW/RaUfGqV2D
|
||||
eXG9yZnwApzxIZyfcJuoFhG6OZrEuZkG6V2ouTHq+W1K/yedudkPbYdlrngu6Hkd9wmd+Qme0/me
|
||||
7cierYedEhA57dWc2ciN8cWbaHZhXGaaQfZgD4qQeAGWCgqZrfI8/+l8+LWh2nloDPmhxekqIoqh
|
||||
JHovThYFxSaIKjpcLPp2y6lhJVpgEPqTNVqa99KiDKp7MGqiEWZn+/ij8rkqQpqj3qehPMqhnTiT
|
||||
Spp4QYqj5jloemmje5drr1Oba1KliMcnI9oB1mlIauel7dCdQiGmY1qe83k7bnpbpTKjXSWhLhaf
|
||||
b9qkWSqneqpZaSqTxXGTFDKne2qgfUo8WwqkrmKnNrkMSPadh9pjpCh+i7qkdYqk4MCmO2Gok3oh
|
||||
ZcoBmv9ZO2k2m/DAqTrhqZ9KZqG6AaPKHGGXkEaGp2H6p6sqI5WaAa+6HLHaoVWFZ7Z6q1ZWb7ua
|
||||
OKW6nRoFrJIqrIdKrKSJOceKonfGZpfKrFfmrNV2oUKnqVikrAVqrXuKraqFYduqpgrlrT0HrpMq
|
||||
rnA5ZNEKcVSFrhSqrp86pEQaoriZatyaQPKanLoxa+m6QgA7r6/5kL1warJVn0NJctVqFQPrrzD2
|
||||
sE6gczrJMha6oCcKr6A4oIXqmn4hseQZsX8Gmoa5ehf7ezK6r5SwsI1xpgIAsignsvLTsNdpsEJz
|
||||
snsppae4sagaYh7bFzCbczEWtDCRchVrslfannVmriT/gpW+87PaM7OqOhhES7ElOxdJm7A9yrOE
|
||||
anQ0ixRVO7QjG6wq4aS8AG2meow9u2Nkq7RcAKcBm7PtaplXm50RUmwsyxguW6xG+62qka0Fe7Sx
|
||||
ebcq21d7+6ys+muIi6lfN3DEVrjGBrWM+pjKdl18u3WJ+h9oi6yfKbmMuwVwS7AYS7kgWrdp5yeO
|
||||
SlWHC7iVu6yNx7qly1ZB9yapG1KrO6766bevi7uBa7pmsrnSulu3O7cWdm2wu6KCC6D1qRd5a3NS
|
||||
RK+C4XKZO0LLmxfNW3IVB719Ib1x2nQ6G4sl5LQ0+Tv9pr3b+3PTO3hbq27iqwnOq0TmCxfca7PM
|
||||
+b0a/3O94JZw8ZsW85u8c1m9ZIG/5Ku/+8sV/eu7vwnATyHAnVS++QqxlCe0hTmxPnHAsrsuuriN
|
||||
Xcu2rstxN/fAFDzBRSvCMSsTFoxXfFSJwdu0HMsS7wtEXme1IRzBIWvC6Nu94JLBM7fBDDvAJgfC
|
||||
Ndy7MyzEoosSJ9y46qLD6bO2DPHCOBTDfTvEdBu3JnHEkQV6zci5+tO+Loy9H+y2JUzEYTzFRXwS
|
||||
VgwAmfe9MrfEPNyyXvy8QDzGsRvEZAzBPvdu9lp8WbzCPsLFIOHELQTFIizDZVzFN0y/09KrdtCR
|
||||
TOzGHZy4o0fCV3yaTjcpY/fI1BbJNIzE/1vJiHLJuv97GhgoyWicxO+aUFDXtl5ndqSMwafcCqBM
|
||||
xcXrea1syp4sRqmMydh1gbXcl7dsKLFcyLPceb3MLYEKhm3ksuCaq4tHeMjsRspsrcycyM78zBMT
|
||||
zcw6zejWqNaczJ5bwMqXvmvEzd1MUN8Mzijrv5JZzeWsMNgsrNo8u6vSzub8tehcs+JszOxMzwfz
|
||||
zrcazxsxvDs40OSUxqoczgSd0Nlk0Lq8yQr90KWXwm4K0RR9TQwdyoRc0Rr9Lxcty5m80SANMB0t
|
||||
zB8d0iYtjDRrO7SXz9zU0HE8yWdpqwDtyimNeue8u4isCiSLwCAo060qL7LnbCuNw7bn0mAM0w16
|
||||
03TQytOxp9QqrdRoStSNsNMXHNNGnc5MHTRB3adDndMJ0bZ+uNVSbTFijchdrc7SQNUobJQ17aK9
|
||||
BycD9nwfJ9cxCh2bOH5PCtfQh9eWqtdeSdcnNkh7DdhRKth/7deuRNi/G366ytda7diKith2O4KR
|
||||
bdhpCdnah9kI4YFGGiB3rdgswNixItp5Jdk9Cdr2oNn1wNmFTdk4+NqwHduyPdu0Xdu2fdu4ndu6
|
||||
vdu83du+/dvAHdzCPdzEXdzGfdzIndzKvdzM3dzO/dzQHd3SrQIJAAAh+QQJBwAHACwAAAAA9AFe
|
||||
AUAD/3i63P4wykmrvTjrzbv/YCiOZGmeaKqubOu+cCzPdG3feK7vfO//wKBwSCwaj0gcYMlsOp/Q
|
||||
qHRKrVqv2Kx2y+16r8mwmGYdmM/otHrNbrvf8Lh8Tq/b7/i8vDru+z9leoKDhIWGh4iJbnx/jX9Y
|
||||
inABk5SVlpeYmZqbnJ2en6ChoqOfkYtgjqk8kKZtpK+wsbKztLWWrWxYqrs5rIW2wMEBBMTFxsfC
|
||||
x8oEws2dhrq80mRXh87Xr8vatl9R2LPQqBXdTtPSvm7J2srf7bXkT+7tcNHj8EvmvOiuwevs8gAx
|
||||
VRFAsKDBgwgTEsQScBS9e0wUSpwooF4+IfvYNNzIsf/SQIogETLs5A9ZsIcQAYRcWVHcxSAZ13Sc
|
||||
2fAjy5sgrcxEaROnz4MWX/6IqYamUXc9fypdWGXnGyxLowYVk1IG0TRHAZYkJixp1K8KU4oFS1bi
|
||||
1DBVY1xFk1XeVmbBvJYtKzbl3LsFz+qAOlfvhrVn2gqOSwWv4cOIE/v0q+TKXcYZAJsZTJmWXMWY
|
||||
M2sGC9kGX7p178WZubWyR8ebU6tevZoRxsusE0oeYJr0WwKfUxvYzbu3byux8foe3hs4XtcwYQc3
|
||||
OLt2x9u4UasmTt34crLUiVvvS8VRXe6B3oCC7hxY7s12W0vPCdEw8j7fQYdP94l8+Xfr0bdXv51i
|
||||
+sf/3QmlQWhUjHafaeclVMCCDDbo4IMQLnjdhJhFaOGFDL4noD0ERmHggZQliBCGJELIUn9kKaeU
|
||||
iCOW6OKCGm7oQnMgbmIffii2+GKJJ6r4k4+L5afQji7GKCMLNNaYyY0BAYlYh1B2CKAVR/ow2xzj
|
||||
3aZkLE4eFuWXY4FnZJUm1IWLRlsaxSKFbOpHJZk7mHlmUWnStGabeCbWGZwjdJnSnIAGKugZYBZq
|
||||
qFh89uAnRIM26qgih0YqaReJtnDnk5NmqummnBLI3puVGnGpl52WauqpqELx6ZihWinkUnuOGuQU
|
||||
R3bZY4Ac2uqfS62+digJmva6gKQzECvsEMaKEOyx/8nC0Oyx0EYr7bTUVmvttdhmq+223Hbr7bfg
|
||||
hivuuOSWa+656Kar7rrstuvuu/DGK2+ZqdZr7733zEsuvvz26y+u+k4736MEF2zwwU8BHHCVAyPs
|
||||
8MMQA8rqwlRV02idGA/W6J4UN9awHhmLwmQtpYVMSTigdpzcx3mYnOVb6vjj8iQoT6yyx1V8aMvI
|
||||
M79SV888KSxBWjfnWqAk/WjZsyw/zxw0rRYQXfQESS4dYmHHXeEJz+Ak/F+KvAZctdWC6TrlFFzL
|
||||
8rQUsXHM7thkZ2W2mFI4dUqOmbm9Ltxxq4l1nszdCzhQYaMFkVUWi9f3kiWb9/fgLdkLeV6F7/Uq
|
||||
rP+V/5U4fYvL/fjkoE+ut1qXr5j5gJvz0/lRc4fuumajO1v6j/fq3FDjCM7++u68yyb0C7LeWq/t
|
||||
AeFeWfC9J++6zTizLXrqaK4u8m3Iz5XdcHgnf/1vrU/E/A3ZU8i39JxAV7188LCG/NfoR+FdmGCz
|
||||
jFV9SpN/fvzp87foPe79bjj7UoGeTOgHM/IFYFRE4pHyeJdAEn2PYZEingEtozsBNBBDC2TgBS30
|
||||
wG+Nb0tp81kFN2gh4TnvbFHoX/gKQsIIddBbH1RSCEmBwBY+yIQpzNoUVAgkGz7ohd26khxeZrzO
|
||||
dY8z/yoUCqHWMSFKkHEFtN/nVJNEJdKNieiS06D/JsiNCmYwebHLolgcxUUcHfGLgAvjtvYHj4i5
|
||||
0WFVjGMW3HU/JMrxjnjk16r8NzUH1PEreQykIE+1Ryz2MQJ/9F3KEOlFnADRD2dUpCGH1siQqHFh
|
||||
iRTJ6YZVSRw+oVZTtOMkGbnCm1xSbJECVqag9awZpfKQVHulslbJLFnK7lewzKUud8nLXvryl8AM
|
||||
pjCHScxiGvOYyEymMpfJzGY685nQjKY0p0nNalrzmtjMZjMHyc1uIkqb7/OmOMe5BXCGk5zoTKf7
|
||||
zPkIdbrznQBgp6+m8MZ62vOed3ikPGd5NHz6858AHYA+9wkC+QX0oAh91EAJ+gAnGqKMEN3ExjbJ
|
||||
/9ADOPQXEc3oLQZ1ymtelBAaDSnNOEpRhn50ECIN6UQXWVE/CrBlBpxhNkoCtEJ01JonjZ70ZEqK
|
||||
ImKsZnxsqUVfiocJ8nQUPq0TUEcpVAXkdIDkO+onmlabpUrhAlLzZQxnGsWUUtVka1un0bohzK32
|
||||
tH5eFUtN7/bArPbSrEhFq0i/GrKwQgGrhwsmXFPKtFACUmskkSst7OqFJV5VZXvlKywiqUmdBLar
|
||||
tSBsFwwrVoolVrE09GsAHVs+wXaNrfy74mGbSFS2YNYZjDUdFezWhkxijqX6uuxpQ5Fa2q22I5Jt
|
||||
QttK2i7Zznaqms1bU3DrtdqakrdvK21gfkuYHf8Gh7MbyW1E1IfclcEDcQZdrlGTyqXgik9woLtp
|
||||
88hKuuxOhouQpaBznye58FYXCG5FknLPu12aNveEkMNX6MQLvrzeMmdIq6/M7ptDNBoYU7C1XCmP
|
||||
m2DNmZc2zHXceg9MYdFWVlGdLORoCzpfCEfYjAWusIj/2uDx4leUQY1Mhz/cxQmP+MWzWugKXEs4
|
||||
ez3RHdz13IJhzGMEb7i8xsXXjduRY9ZluMdIRvFdqbHjlQg5wBspst+anOQqb/bH//WuI2sH5dvZ
|
||||
tzY09sn2imPc4IyZN1R2cop7UWbKQmHIGTXfkcFy5t2kGXJ1NsCdNVwOZLXZwm/uMl/lvOev5Ln/
|
||||
0Hk6dJtlbKk/t4+egk4poR09EUVr+XWWdjH+LjwUSitZCnCO6KQvrcP8UbGSAPx0n9uZatUCWHFb
|
||||
82zfwrzl0J66ya2+MqcrlusY95Nzj5Vyz2jNYFOnZn37cTMTzmlrVQca1sH+svSI7cnC6o/UkTP2
|
||||
oz8pLUmF+sM19CGDrLwccTeI0dPwdqRZfJpSmnvc5I7Nu2G0ZjipG9rsvkS4zR1vec8b3dXybWWk
|
||||
Clx3z7vaTuBhiLe98CH9u97hEjhlCO6JfYsb4botdcOd/YSQzLsAAKeWxAdD8U5Y3IcYn66yl6Bw
|
||||
TSvo4Uw118gFU3JOnNyGKWe5xlW18xM7/N0h/xdYh7FEQGHPzNO7gie1+Quup9Ip1uldHNK9p/Sl
|
||||
vzddTp8f1I3usqmbpepz/nqJIz7GhxZd2qvbCrX73fNdz0uLgsq3vsPOduGO/V1wD5TcLbH2ujN8
|
||||
yaT908X2Tom++53jq94btgWf0MbrAez46iMbyeH4yuMB8veS/OIZZfnOzwHz9tK8y8PCec+b/vSD
|
||||
CLrovV5j0Lt+nHwGPDANn/TX256bsee2Xun+2tv7Po+5T/wvaU/13xtfjsFvwjCJHSvekz7m6cZ2
|
||||
zuMZNecH7u6HZH5J+656P49e125vqPUph/3VS7/29eY+xFOx6LaOnynln1orPbAsYc1fvrjcff/+
|
||||
Q1D/Xt1/xrY0fAEICLRkfwPYaPvXVAq4gAzYgA74gBAYgRI4gRRYgRZ4gRiYgRq4gRzYgR74gSAY
|
||||
giI4giRYgiZ4giiYgiq4gizYgi74gjAYgzI4gzRYgzZ4gziYgzq4g2V1fD5YKjxIAT84hJsShLFE
|
||||
hEiYgEbYAEnYhIWyhKTkhFIYGlAIAVN4hXVRheKHhVxIDlroUl0YhpSigw+GemZ4hmjQfRFYhmjY
|
||||
hqenhhDIhm44h5UHhw8oh3SYhwdlhw6Ih3r4h/jEhw3oh4BYiG4kiNmUdXdAeGSzUoiYTIpoB4xo
|
||||
NY64fvIUiXUwiUtTidDXUphIB5q4VoHCdNL/9IlEF4pgRVLxd4lDhwio6DKciGULaIpD9Ip1pYqP
|
||||
iEy0+G22WFW4aIns9IkChna0wHXlYVXhJ1TCGFOyFgvG6BzIKHuD2Iq86DI1F23LIIqCQIrRtIxR
|
||||
1Yyw8Iy+SAjcCE3euFPgyFXroI2Pd3XgdI5pl45nNWCwaFPuqE3w2DnXmAl0RRnRqHtC6F8C+GAx
|
||||
I45Sp1ZOU1zrF1+7NHNQZJCzhpD1CFqdyAAMqUsOiQn7GDf9+FMKWZGclC/692rARjLyOEEdqVQf
|
||||
KYtWKJBatWJJE3UZlZJpIl34UH0iOXswCQwbSTY0uSU2SX1j9QU9SJAxCZFp0n7QVRlBaXWr/6gu
|
||||
GXkJPakkSjlc49ha8LNpINlbO9mLm1CVt9VZMhkLTfl+8JeLIteVXsmP52dJgCWWSKkJZYlokgSM
|
||||
WKeWazl339d7YWkjJykKcxlk9yhzeJmX7eZziNcEW0eMn4WVveZrW5lcRmmYAtGWyccEi0mPthCY
|
||||
lil2aCl0k0mZfNeZWgkFU4kJQblbT3mXoSmahUeaiQkArJULZllsdilGremaBwSbJDYFs7kGxFd8
|
||||
n9lthSmarFeX3kBcFImYdjec0RKV7HacjeWbyumY0kl+zslKxUmZ13l91MkRqUldq4mbJKk6ujkJ
|
||||
3YmddVOdtEmXfBmZipebrpme05cp+zWYcf/ikghYnjr1jYzZV3v5Xe3FXtlZAheZAtAJImO5WLyJ
|
||||
GfrlXuOJYTkJPNvZNwsqQgE6IQ9KoLcpodcFZL9mnv6pmSDGcxyKKvcZoaugn/jHn1DFjP/ZXRl6
|
||||
HRuaX/ipYB+aZZCGb+gYoww6o8tRo4NTjtiVoxQqn/oYl2wJpM8FXifKkkhwoCiQoAdyoZnFpIf3
|
||||
RUTKT4KpomDook93nruJpVm6QFvKf7XJdFQaYfRZprBzoyrglCG3pszVpm7qoHCKoGmap05VoXlp
|
||||
p3eqJ3x6AnLaoVsIplonpoAaqKTipQDonq1nqF8aov2pm4vKqG0njSsKqd6ZKtVYkNvgHMH/ialo
|
||||
VKBDxanqiSqfepShCma1SaoiZqqFqqrrhmM+amSoCqsjJqt7amO1SmS3OmWXqqt4wqu5mm2e+qvf
|
||||
oKTfMKrECkaSSoDH+mQ8qhXBaiev+qylGq3016vDo6zYwKzY4KwIkWnMqT11dqxnmYyP2qXfWq1u
|
||||
ca0zQa4HYa4bpzz2aqKAtmzeR6a3hqimdVqj5q90lq7DGhX52nGZyq/zdK4CCrDahVkD67CIkbAJ
|
||||
d2AWm3ErJ5TWRbE06qeiRj3ZChIZq3JoVLI6t7FEoK4bywSrOqIlQa8GgbIqgbEG26DTya5shrM+
|
||||
RqkvqlgTe6+KQbMURrQLe5MNK7QP67Nh/wq0IsuyJHuzBAs4Rquy/eqxQQqyEBW0+uom2vamuJZs
|
||||
+4q0kJSVvcm0iQqX8tp1I4t+3SCek/e2R8ux8GG24Adq4EoJpwkiMpuqXwC3m9dsf8ewZfuYtrmj
|
||||
JemXVjpsbSuc5AC4TGq4kAmQdSu5aoakGvmXbAu1bvu31xa5Yju4ZFu5oXu3HpK3k7C3B9K36yq3
|
||||
/xq4X5uYzBa7roa2AYuN62hArIusrntsqFa6stsI8VGapwuvmbu4RzdCB6etYPFxpqpKluuWmCuV
|
||||
mmsyN9dCzNu8MAellXJviUt410tC2fsVzsut0XcoL4tZ4btB4xsV5QufMuK9IsqI63tB7f+7FO/L
|
||||
vYkiv5UKvsr7bverFPmrs/YWQahbp//LbwHsEwOsqf5nwMabb/XbQNPXcgrbsik7EQ1MuawJsfQV
|
||||
MqqLngl8cZc7tY7btcSLwi8HdOYbcFp7HyE8pj7ycRU8txastCy0vQRMdtNbIzE8wQlUwxt7wyqs
|
||||
I+b2vEJBp1FWvSY3wihXwlhba1F8uFi7wcLXwbYbsRnzw06Mc1CMw7Y1xfVZsxRhxcrHlT2soExs
|
||||
c12MvV9cxKarsWMrxxJhxoQLlS9cHjEMCm06hBhMt6hEiPOrCXvsqiYcm5D3xzeTj4S8xhjTxz+o
|
||||
yIhFjQdcyLlzyGf7e5IceB4MimdHokb/xLNhbHybbFmUHMGWYMnHI8qTe3uljEmn/L0PubbWy8pS
|
||||
TMpzfMeB3MmniLutOm22TMWunMujS5iMB1KfnLswuwy7S6xnmi15Byia2My6+szYEs1zMs2Nu8DW
|
||||
fC3YfCbazLkLzLv6i3dlt0WTSM2w2s3W8s24EM4HW6bs7MLHrHfpvM0BPM98ErdfYIh1KIbfVDT8
|
||||
7AX+7HgAHdCLDLuUV9AJddBSKi8D3QUM3dAOPaGTDLqiMdF7WNFGetFTXM8aHYgc7YXyp9DdENIo
|
||||
XU9I7NFg7LcEndIw/TArjcWYPMojfdMVpM/jsrs43dOjotPiwtM+PdQZCtQ83J1EndRw/+zSDsxL
|
||||
Qq3UUE3GntnC8PLUUZ3Ul1nMb4XPyHnVPZ3VgLzVudp8LDvTRyrGb8zBUSjKRt10ZknWYNnU5hDX
|
||||
cj2pNc3Uau3Ub7192WrWrnTXkbrDp1pbbe1Be4196ge/rIbW0huZ2ueob3fYzJPY5bwLdJ3Xh8rW
|
||||
g2rOY83XZU3VRXDZV7zWgE3Ogi3QB9gB/dcq/xenqd2Qr+1grU0msz2lsZ1LtY0Bqx0quU0vSijW
|
||||
hgK9vb0hwy3cT1iUwd0nBfjAv92ixz2Sz42mk6Kdzd2u0T2Qyc2ltw1B1e3a3Y3b263by83a4e3b
|
||||
2f2F6J3e6r3e7N3e7v3e8B3f8j3f9CZd3/Z93/id3/q93/zd3/793wAe4AI+4ARe4AZ+4Aie4Aq+
|
||||
4NqUAAA7
|
||||
------sinikael-?=_5-14507714776260.4520441491622478--
|
||||
|
||||
------sinikael-?=_2-14507714776260.4520441491622478--
|
||||
|
||||
------sinikael-?=_1-14507714776260.4520441491622478
|
||||
Content-Type: text/plain; charset=utf-8; name="notes
|
||||
=?UTF-8?Q?=F0=9F=95=B6=2Etxt?="
|
||||
Content-Disposition: attachment;
|
||||
filename*0*=utf-8''notes%20%F0%9F%95%B6.txt
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
Some notes about this e-mail =F0=9F=90=B1
|
||||
------sinikael-?=_1-14507714776260.4520441491622478--
|
599
imap-core/test/fixtures/ryan_finnie_mime_torture.eml
vendored
Normal file
599
imap-core/test/fixtures/ryan_finnie_mime_torture.eml
vendored
Normal file
|
@ -0,0 +1,599 @@
|
|||
Subject: Ryan Finnie's MIME Torture Test v1.0
|
||||
From: Ryan Finnie <rfinnie@domain.dom>
|
||||
To: bob@domain.dom
|
||||
Content-Type: multipart/mixed; boundary="=-qYxqvD9rbH0PNeExagh1"
|
||||
Message-Id: <1066976914.4721.5.camel@localhost>
|
||||
Mime-Version: 1.0
|
||||
Date: 23 Oct 2003 23:28:34 -0700
|
||||
|
||||
|
||||
--=-qYxqvD9rbH0PNeExagh1
|
||||
Content-Type: text/plain; CHARSET=US-ASCII
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Welcome to Ryan Finnie's MIME torture test. This message was designed
|
||||
to introduce a couple of the newer features of MIME-aware MUAs, features
|
||||
that have come around since the days of the original MIME torture test.
|
||||
|
||||
Just to be clear, this message SUPPLEMENTS the original torture test,
|
||||
not replaces it. The original test is still very much valid these days,
|
||||
and new MUAs should strive to first pass the original test, then this
|
||||
one.
|
||||
|
||||
By the way, the message/rfc822 parts have Content-Descriptions
|
||||
containing Futurama quotes. Bonus points if the MUA display these
|
||||
somewhere.
|
||||
|
||||
Have fun!
|
||||
|
||||
Ryan Finnie
|
||||
|
||||
--=-qYxqvD9rbH0PNeExagh1
|
||||
Content-Disposition: inline
|
||||
Content-Description: I'll be whatever I wanna do. --Fry
|
||||
Content-Type: message/rfc822
|
||||
|
||||
Subject: plain jane message
|
||||
From: Ryan Finnie <rfinnie@domain.dom>
|
||||
To: bob@domain.dom
|
||||
Message-Id: <1066973156.4264.42.camel@localhost>
|
||||
Mime-Version: 1.0
|
||||
Date: 23 Oct 2003 22:25:56 -0700
|
||||
Content-Type: text/plain; CHARSET=US-ASCII
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Subject: plain jane message
|
||||
From: Ryan Finnie <rfinnie@domain.dom>
|
||||
To: bob@domain.dom
|
||||
Content-Type: text/plain
|
||||
Message-Id: <1066973156.4264.42.camel@localhost>
|
||||
Mime-Version: 1.0
|
||||
Date: 23 Oct 2003 22:25:56 -0700
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
This is a plain text/plain message. Nothing fancy here...
|
||||
--=-qYxqvD9rbH0PNeExagh1
|
||||
Content-Disposition: inline
|
||||
Content-Description: Would you kindly shut your noise-hole? --Bender
|
||||
Content-Type: message/rfc822
|
||||
|
||||
Subject: messages inside messages inside...
|
||||
From: Ryan Finnie <rfinnie@domain.dom>
|
||||
To: bob@domain.dom
|
||||
Content-Type: multipart/mixed; boundary="=-9Brg7LoMERBrIDtMRose"
|
||||
Message-Id: <1066976111.4263.74.camel@localhost>
|
||||
Mime-Version: 1.0
|
||||
Date: 23 Oct 2003 23:15:11 -0700
|
||||
|
||||
|
||||
--=-9Brg7LoMERBrIDtMRose
|
||||
Content-Type: text/plain; CHARSET=US-ASCII
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
While a message/rfc822 part inside another message/rfc822 part in a
|
||||
message isn't too strange, 200 iterations of that would be. The MUA
|
||||
should have some sense when to stop looping through.
|
||||
|
||||
--=-9Brg7LoMERBrIDtMRose
|
||||
Content-Disposition: inline
|
||||
Content-Description: At the risk of sounding negative, no. --Leela
|
||||
Content-Type: message/rfc822
|
||||
|
||||
Subject: the original message
|
||||
From: Ryan Finnie <rfinnie@domain.dom>
|
||||
To: bob@domain.dom
|
||||
Content-Type: multipart/mixed; boundary="=-XFYecI7w+0shpolXq8bb"
|
||||
Message-Id: <1066975745.4263.70.camel@localhost>
|
||||
Mime-Version: 1.0
|
||||
Date: 23 Oct 2003 23:09:05 -0700
|
||||
|
||||
|
||||
--=-XFYecI7w+0shpolXq8bb
|
||||
Content-Type: text/plain; CHARSET=US-ASCII
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
by this point, I should be the 3rd layer deep!
|
||||
|
||||
I also have an attachment.
|
||||
|
||||
--=-XFYecI7w+0shpolXq8bb
|
||||
Content-Disposition: attachment; filename=foo.gz
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Type: application/x-gzip; NAME=foo.gz
|
||||
|
||||
H4sIAOHBmD8AA4vML1XPyVHISy1LLVJIy8xLUchNVeQCAHbe764WAAAA
|
||||
|
||||
--=-XFYecI7w+0shpolXq8bb--
|
||||
|
||||
--=-9Brg7LoMERBrIDtMRose--
|
||||
|
||||
--=-qYxqvD9rbH0PNeExagh1
|
||||
Content-Disposition: inline
|
||||
Content-Description: Dirt doesn't need luck! --Professor
|
||||
Content-Type: message/rfc822
|
||||
|
||||
Subject: this message JUST contains an attachment
|
||||
From: Ryan Finnie <rfinnie@domain.dom>
|
||||
To: bob@domain.dom
|
||||
Content-Disposition: attachment; filename=blah.gz
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Description: Attachment has identical content to above foo.gz
|
||||
Message-Id: <1066974048.4264.62.camel@localhost>
|
||||
Mime-Version: 1.0
|
||||
Date: 23 Oct 2003 22:40:49 -0700
|
||||
Content-Type: application/x-gzip; NAME=blah.gz
|
||||
|
||||
SubjectthismessageJUSTcontainsanattachmentFromRyanFinnierfinniedomaindomTobo
|
||||
bdomaindomContentDispositionattachmentfilenameAblahgzContentTypeapplication/
|
||||
xgzipnameAblahgzContentTransferEncodingbase64ContentDescriptionAttachmenthas
|
||||
identicalcontenttoabovefoogzMessageId1066974048426462camellocalhostMimeVersi
|
||||
on10Date23Oct20032240490700H4sIAOHBmD8AA4vML1XPyVHISy1LLVJIy8xLUchNVeQCAHbe7
|
||||
64WA
|
||||
|
||||
--=-qYxqvD9rbH0PNeExagh1
|
||||
Content-Disposition: inline
|
||||
Content-Description: Hold still, I don't have good depth perception! --Leela
|
||||
Content-Type: message/rfc822
|
||||
|
||||
Subject: Attachment filename vs. name
|
||||
From: Ryan Finnie <rfinnie@domain.dom>
|
||||
To: bob@domain.dom
|
||||
Content-Type: multipart/mixed; boundary="=-1066975756jd02"
|
||||
Message-Id: <1066975756.4263.70.camel@localhost>
|
||||
Mime-Version: 1.0
|
||||
Date: 23 Oct 2003 23:09:16 -0700
|
||||
|
||||
|
||||
--=-1066975756jd02
|
||||
Content-Type: text/plain; CHARSET=US-ASCII
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
In this message's attachment, the Content-Disposition has a
|
||||
filename of blah1.gz, while the Content-Type has a name of
|
||||
blah2.gz. What should be done? Well, since this is an attachment
|
||||
(as indicated in the Content-Disposition), the MUA should
|
||||
suggest a filename of blah1.gz. The MUA *COULD* find a way to
|
||||
represent the name of blah2.gz somewhere else, it's not needed.
|
||||
|
||||
--=-1066975756jd02
|
||||
Content-Disposition: attachment; filename=blah1.gz
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Description: filename is blah1.gz, name is blah2.gz
|
||||
Content-Type: application/x-gzip; NAME=blah2.gz
|
||||
|
||||
H4sIAOHBmD8AA4vML1XPyVHISy1LLVJIy8xLUchNVeQCAHbe764WAAAA
|
||||
|
||||
--=-1066975756jd02--
|
||||
|
||||
--=-qYxqvD9rbH0PNeExagh1
|
||||
Content-Disposition: inline
|
||||
Content-Description: Hello little man. I WILL DESTROY YOU! --Moro
|
||||
Content-Type: message/rfc822
|
||||
|
||||
Subject: No filename? No problem!
|
||||
From: Ryan Finnie <rfinnie@domain.dom>
|
||||
To: bob@domain.dom
|
||||
Content-Type: multipart/mixed; boundary="=-1066975756jd03"
|
||||
Message-Id: <1066975761.4263.70.camel@localhost>
|
||||
Mime-Version: 1.0
|
||||
Date: 23 Oct 2003 23:09:21 -0700
|
||||
|
||||
|
||||
--=-1066975756jd03
|
||||
Content-Type: text/plain; CHARSET=US-ASCII
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
When searching for a suitable name to suggest for a filename,
|
||||
the MUA should probably follow this order. First, look for
|
||||
Content-Disposition's filename attribute. If that is missing,
|
||||
look for Content-Type's file attribute. If that is also missing,
|
||||
I would recomment taking the Content-Description, stripping off
|
||||
any characters that cannot be used in a filename, and suggesting
|
||||
that.
|
||||
|
||||
If none of those fields are available, the MUA could just make
|
||||
up a random filename. SOMETHING is better than nothing.
|
||||
|
||||
--=-1066975756jd03
|
||||
Content-Disposition: attachment
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Description: I'm getting sick of witty things to say
|
||||
Content-Type: application/x-gzip
|
||||
|
||||
H4sIAOHBmD8AA4vML1XPyVHISy1LLVJIy8xLUchNVeQCAHbe764WAAAA
|
||||
|
||||
--=-1066975756jd03--
|
||||
|
||||
--=-qYxqvD9rbH0PNeExagh1
|
||||
Content-Disposition: inline
|
||||
Content-Description: Friends! Help! A guinea pig tricked me! --Zoidberg
|
||||
Content-Type: message/rfc822
|
||||
|
||||
Subject: html and text, both inline
|
||||
From: Ryan Finnie <rfinnie@domain.dom>
|
||||
To: bob@domain.dom
|
||||
Content-Type: multipart/mixed; boundary="=-ZCKMfHzvHMyK1iBu4kff"
|
||||
Message-Id: <1066974044.4264.62.camel@localhost>
|
||||
Mime-Version: 1.0
|
||||
Date: 23 Oct 2003 22:40:45 -0700
|
||||
|
||||
|
||||
--=-ZCKMfHzvHMyK1iBu4kff
|
||||
Content-Type: text/html; CHARSET=utf-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 TRANSITIONAL//EN">
|
||||
<HTML>
|
||||
<HEAD>
|
||||
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; CHARSET=UTF-8">
|
||||
<META NAME="GENERATOR" CONTENT="GtkHTML/1.1.10">
|
||||
</HEAD>
|
||||
<BODY>
|
||||
<FONT COLOR="#f8cc00">This is the HTML part.</FONT><BR>
|
||||
It should be displayed inline.
|
||||
</BODY>
|
||||
</HTML>
|
||||
|
||||
--=-ZCKMfHzvHMyK1iBu4kff
|
||||
Content-Type: text/plain; CHARSET=US-ASCII
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
This is the text part.
|
||||
It should ALSO be displayed inline.
|
||||
|
||||
--=-ZCKMfHzvHMyK1iBu4kff--
|
||||
|
||||
--=-qYxqvD9rbH0PNeExagh1
|
||||
Content-Disposition: inline
|
||||
Content-Description: Smeesh! --Amy
|
||||
Content-Type: message/rfc822
|
||||
|
||||
Subject: text and text, both inline
|
||||
From: Ryan Finnie <rfinnie@domain.dom>
|
||||
To: bob@domain.dom
|
||||
Content-Type: multipart/mixed; boundary="=-pNc4wtlOIxs8RcX7H/AK"
|
||||
Message-Id: <1066974089.4265.64.camel@localhost>
|
||||
Mime-Version: 1.0
|
||||
Date: 23 Oct 2003 22:41:29 -0700
|
||||
|
||||
|
||||
--=-pNc4wtlOIxs8RcX7H/AK
|
||||
Content-Type: text/plain; CHARSET=US-ASCII
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
This is the first text part.
|
||||
It should be displayed inline.
|
||||
|
||||
--=-pNc4wtlOIxs8RcX7H/AK
|
||||
Content-Type: text/plain; CHARSET=US-ASCII
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
This is the second text part.
|
||||
It should also be displayed inline.
|
||||
|
||||
--=-pNc4wtlOIxs8RcX7H/AK--
|
||||
|
||||
--=-qYxqvD9rbH0PNeExagh1
|
||||
Content-Disposition: inline
|
||||
Content-Description: That's not a cigar. Uh... and it's not mine. --Hermes
|
||||
Content-Type: message/rfc822
|
||||
|
||||
Subject: HTML and... HTML?
|
||||
From: Ryan Finnie <rfinnie@domain.dom>
|
||||
To: bob@domain.dom
|
||||
Content-Type: multipart/mixed; boundary="=-zxh/IezwzZITiphpcbJZ"
|
||||
Message-Id: <1066973957.4263.59.camel@localhost>
|
||||
Mime-Version: 1.0
|
||||
Date: 23 Oct 2003 22:39:17 -0700
|
||||
|
||||
|
||||
--=-zxh/IezwzZITiphpcbJZ
|
||||
Content-Type: text/html; CHARSET=utf-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 TRANSITIONAL//EN">
|
||||
<HTML>
|
||||
<HEAD>
|
||||
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; CHARSET=UTF-8">
|
||||
<META NAME="GENERATOR" CONTENT="GtkHTML/1.1.10">
|
||||
</HEAD>
|
||||
<BODY>
|
||||
<B>Bold!!!</B><BR>
|
||||
<BR>
|
||||
What do we have here... This message is an HTML message. Also attached
|
||||
is an HTML FILE. Both of these are in a multipart/mixed part.<BR>
|
||||
<BR>
|
||||
Now, the first HTML part (what you're reading now) should be displayed
|
||||
if the MUA is HTML-capable. If it is not, the MUA could possibly offer
|
||||
this part up as an attachment to download, seeing as how no plaintext
|
||||
part is offered as an alternative.<BR>
|
||||
<BR>
|
||||
However, the second HTML part is listed with a disposition as
|
||||
attachment. Therefore, it should be offered as an attachment, not
|
||||
displayed inline.
|
||||
</BODY>
|
||||
</HTML>
|
||||
|
||||
--=-zxh/IezwzZITiphpcbJZ
|
||||
Content-Disposition: attachment; filename=htmlfile.html
|
||||
Content-Type: text/html; NAME=htmlfile.html; CHARSET=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
<html>
|
||||
<head><title>This is an Attachment</title></head>
|
||||
<body>
|
||||
<p>The title says it all...</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
--=-zxh/IezwzZITiphpcbJZ--
|
||||
|
||||
--=-qYxqvD9rbH0PNeExagh1
|
||||
Content-Disposition: inline
|
||||
Content-Description: The spirit is willing, but the flesh is spongy, and
|
||||
bruised. --Zapp
|
||||
Content-Type: message/rfc822
|
||||
|
||||
Subject: smiley!
|
||||
From: Ryan Finnie <rfinnie@domain.dom>
|
||||
To: bob@domain.dom
|
||||
Content-Type: multipart/signed; micalg=pgp-sha1; protocol="application/pgp-signature"; boundary="=-vH3FQO9a8icUn1ROCoAi"
|
||||
Message-Id: <1066972996.4264.39.camel@localhost>
|
||||
Mime-Version: 1.0
|
||||
Date: 23 Oct 2003 22:23:16 -0700
|
||||
|
||||
|
||||
--=-vH3FQO9a8icUn1ROCoAi
|
||||
Content-Type: multipart/mixed; boundary="=-CgV5jm9HAY9VbUlAuneA"
|
||||
|
||||
|
||||
--=-CgV5jm9HAY9VbUlAuneA
|
||||
Content-Type: multipart/related; type="multipart/alternative";
|
||||
boundary="=-GpwozF9CQ7NdF+fd+vMG"
|
||||
|
||||
|
||||
--=-GpwozF9CQ7NdF+fd+vMG
|
||||
Content-Type: multipart/alternative; boundary="=-dHujWM/Xizz57x/JOmDF"
|
||||
|
||||
|
||||
--=-dHujWM/Xizz57x/JOmDF
|
||||
Content-Type: text/plain
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
If this sentence is red, you are viewing the HTML part.
|
||||
Wow, what a complicated message. This message is laid out as so:
|
||||
|
||||
multipart/signed
|
||||
| multipart/mixed
|
||||
| | mutipart/related
|
||||
| | | multipart/alternative
|
||||
| | | | text/plain
|
||||
| | | | text/html
|
||||
| | | image/png (smiley)
|
||||
| | image/gif (dot)
|
||||
| application/pgp-signature
|
||||
|
||||
:)
|
||||
|
||||
A smiley face should be embedded into the HTML part above (if you are
|
||||
viewing the HTML part), while the red square dot should be attached, not
|
||||
displayed inline. Additionally, this whole message is PGP signed.
|
||||
|
||||
This message introduces a few tricks that the MUA should cope with.=20
|
||||
First of all, the related / alternative combination doesn't make much
|
||||
sense. Here's the current setup in pseudo-code:
|
||||
|
||||
relationship between (alternative: HTML or text part) and PNG part
|
||||
|
||||
Why would the text part be related in anyway to the PNG? Instead, the
|
||||
correct and more logical way to do things would be:
|
||||
|
||||
alternative: (relationship between HTML and PNG parts) or text part
|
||||
|
||||
However, many MUAs compose a message using the first method, so this
|
||||
should be taken care of when parsing the message.
|
||||
|
||||
Additionally, notice that the inline image has a disposition of
|
||||
"attachment". Despite this being in there, the smiley should be
|
||||
embedded inline in the HTML part, not offered as an attachment.=20
|
||||
Conversely, the GIF image should be offered as an attachment, not
|
||||
displayed inline.
|
||||
|
||||
If the MUA is not PGP capable, at the very least it should recognize
|
||||
multipart/signed the same as multipart/mixed, and offer the
|
||||
application/pgp-signature part as an attachment.
|
||||
|
||||
--=-dHujWM/Xizz57x/JOmDF
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 TRANSITIONAL//EN">
|
||||
<HTML>
|
||||
<HEAD>
|
||||
<META HTTP-EQUIV=3D"Content-Type" CONTENT=3D"text/html; CHARSET=3DUTF-8">
|
||||
<META NAME=3D"GENERATOR" CONTENT=3D"GtkHTML/1.1.10">
|
||||
</HEAD>
|
||||
<BODY>
|
||||
<FONT COLOR=3D"#f80000">If this sentence is red, you are viewing the HTML p=
|
||||
art.</FONT><BR>
|
||||
Wow, what a complicated message. This message is laid out as so:<BR>
|
||||
<BR>
|
||||
multipart/signed<BR>
|
||||
| multipart/mixed<BR>
|
||||
| | mutipart/related<BR>
|
||||
| | | multipart/alternative<BR>
|
||||
| | | | text/plain<BR>
|
||||
| | | | text/html<BR>
|
||||
| | | image/png (smiley)<BR>
|
||||
| | image/gif (dot)<BR>
|
||||
| application/pgp-signature<BR>
|
||||
<BR>
|
||||
<IMG SRC=3D"cid:1066971953.4232.15.camel@localhost" ALIGN=3D"bottom" ALT=3D=
|
||||
":)" BORDER=3D"0"><BR>
|
||||
<BR>
|
||||
A smiley face should be embedded into the HTML part above (if you are viewi=
|
||||
ng the HTML part), while the red square dot should be attached, not display=
|
||||
ed inline. Additionally, this whole message is PGP signed.<BR>
|
||||
<BR>
|
||||
This message introduces a few tricks that the MUA should cope with. F=
|
||||
irst of all, the related / alternative combination doesn't make much sense.=
|
||||
Here's the current setup in pseudo-code:<BR>
|
||||
<BR>
|
||||
<I>relationship between (alternative: HTML or text part) and PNG part</I><B=
|
||||
R>
|
||||
<BR>
|
||||
Why would the text part be related in anyway to the PNG? Instead, the=
|
||||
correct and more logical way to do things would be:<BR>
|
||||
<BR>
|
||||
alternative: (relationship between HTML and PNG parts) or text part<BR>
|
||||
<BR>
|
||||
However, many MUAs compose a message using the first method, so this should=
|
||||
be taken care of when parsing the message.<BR>
|
||||
<BR>
|
||||
Additionally, notice that the inline image has a disposition of "attac=
|
||||
hment". Despite this being in there, the smiley should be embedd=
|
||||
ed inline in the HTML part, not offered as an attachment. Conversely,=
|
||||
the GIF image should be offered as an attachment, not displayed inline.<BR=
|
||||
>
|
||||
<BR>
|
||||
If the MUA is not PGP capable, at the very least it should recognize multip=
|
||||
art/signed the same as multipart/mixed, and offer the application/pgp-signa=
|
||||
ture part as an attachment.
|
||||
</BODY>
|
||||
</HTML>
|
||||
|
||||
--=-dHujWM/Xizz57x/JOmDF--
|
||||
|
||||
--=-GpwozF9CQ7NdF+fd+vMG
|
||||
Content-ID: <1066971953.4232.15.camel@localhost>
|
||||
Content-Disposition: attachment; filename=smiley-3.png
|
||||
Content-Type: image/png; name=smiley-3.png
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC+klEQVR42n2TbUjVdxTHP/+H69Xd
|
||||
a2VWlFe69rzthZJUoxeNOWoFGxEhYRRFmZSVW2u9ab2KejWE1qDNBkEQhS82VoiaZkVPmoWaKNM5
|
||||
mA+opbd771//997//T/+epHBarEPHA6Hc84XDnwP/JcwcBS4AVgzcR04ONN7C+md+pcPCz44dPLA
|
||||
arZs/gg1UABuGkvvp7X1Iad+itE/YtUAle8TuH26sujzqq/LkJQsnOQQVmIASVJQMhehZORiJwc5
|
||||
d76FH2pf3gY2Aigzy7+eObqmtOqbXbjGGHZqCM+eQpJ9AHhWFCc5CAjWf1KAkppc+qg3vRCol4Fw
|
||||
0aqcisOVW3HTE7hmBElSKD/5GFkNMhH1KDvegST78CwNSfZxeM88VuYrh4CwAuxqvxL6MnPuWiy9
|
||||
H1kNUPH9fZofDKPpHn8/z+Z6Yw8JK5stX5VhRO6h+OfiV3WaHxtPVKAwmF+KqXUDMkgqZ0+UoKcE
|
||||
P57/GXOqh46ODqrPXUQfufb6YOGxJOQD2CaHQnnlAJ4zDXggHBYvK6ap6Rau+RIz1k7djd+YHrqM
|
||||
pXUC4KQnWTRPAdiuRqNRkFQG/omRNJOsKVQw408xtS4QDsI10AaqEY6O8Fzq70fJy3XI8gsA5HTa
|
||||
rBdOkvwFKj39EWrr/sJzEnj29OvsphGugfBsLlwbZnjcYN36LxiLuADtMtCUetFAcE4ee8s+pbHV
|
||||
YtOemwhHx3MSaPEY3X9OUnqsk5a2OMeP7KC3t4u+3gRALUC4cEW2eN62Q4ze3SAiz74TDxvOiI+X
|
||||
BcTsoCoyfJKYn6OKmrMbxGRnlXhyJSSqv80Vq0KSAFa+ceKl0wcK9lfsW42TGsE/pxhfcDmKfz6e
|
||||
FUPg4iRH6Ov6g9EJh1t341xusWuAyn9b+c7BrbklJ8oDZGTOQpL9ePY08SmDpCEwbcHwuE370yku
|
||||
Nlj3gM/e90yXliyU9+8sCVJYlEUgU8IwBZruMThm83uzxsAYV4Hd/A9h4BjQBthAFOgDLgDF7w6/
|
||||
ArI6YJ0eTQeGAAAAAElFTkSuQmCC
|
||||
|
||||
--=-GpwozF9CQ7NdF+fd+vMG--
|
||||
|
||||
--=-CgV5jm9HAY9VbUlAuneA
|
||||
Content-Disposition: attachment; filename=dot.gif
|
||||
Content-Type: image/gif; name=dot.gif
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
R0lGODdhCgAKAKEAAAAAANUAAP///8PDwywAAAAACgAKAEACHZSPMssLKoIMYLyR1I2z3sZsE2VB
|
||||
owcBqlqurloAADs=
|
||||
|
||||
--=-CgV5jm9HAY9VbUlAuneA--
|
||||
|
||||
--=-vH3FQO9a8icUn1ROCoAi
|
||||
Content-Type: application/pgp-signature; name=signature.asc
|
||||
Content-Description: This is a digitally signed message part
|
||||
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
Version: GnuPG v1.2.2 (GNU/Linux)
|
||||
|
||||
iD8DBQA/mLdEKZYQqSA+yiURAjAnAJ90G22jbX/Broy0F541R0UUbsb6zgCeJn0d
|
||||
02Vq9Sv6aXE+YM0lRn3jZDc=
|
||||
=uwCM
|
||||
-----END PGP SIGNATURE-----
|
||||
|
||||
--=-vH3FQO9a8icUn1ROCoAi--
|
||||
--=-qYxqvD9rbH0PNeExagh1
|
||||
Content-Disposition: inline
|
||||
Content-Description: Kittens give Morbo gas. --Morbo
|
||||
Content-Type: message/rfc822
|
||||
|
||||
Subject: the PROPER way to do alternative/related
|
||||
From: Ryan Finnie <rfinnie@domain.dom>
|
||||
To: bob@domain.dom
|
||||
Content-Type: multipart/alternative; type="multipart/alternative"; boundary="=-tyGlQ9JvB5uvPWzozI+y"
|
||||
Message-Id: <1066973557.4265.51.camel@localhost>
|
||||
Mime-Version: 1.0
|
||||
X-Mailer: Not Evolution
|
||||
Date: 23 Oct 2003 22:32:37 -0700
|
||||
|
||||
|
||||
--=-tyGlQ9JvB5uvPWzozI+y
|
||||
Content-Type: text/plain; CHARSET=US-ASCII
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
If this sentence is green, you're viewing the HTML part.
|
||||
|
||||
Now, this is the way that all MUAs SHOULD treat this kind of situation.
|
||||
The layout is like so:
|
||||
|
||||
multipart/alternative
|
||||
| text/plain
|
||||
| multipart/related
|
||||
| | text/html
|
||||
| | image/gif
|
||||
|
||||
See? The GIF (which by the way should be inline towards the top of this
|
||||
message) is related to the HTML, and that whole block is an alternative
|
||||
to a text/plain part. This is the opposite of the way shown in the
|
||||
previous email.
|
||||
|
||||
Also, the embedded image here does not have a filename. As mentioned
|
||||
above, the MUA should suggest something as a filename, even here (the
|
||||
user may want to save the embedded image, so a filename would be
|
||||
helpful). In this case, I would recommend appending the random text
|
||||
to be suggested to the user with the part's subtype, in this case
|
||||
something like c20vsidlkvm.gif.
|
||||
|
||||
--=-tyGlQ9JvB5uvPWzozI+y
|
||||
Content-Type: multipart/related; boundary="=-bFkxH1S3HVGcxi+o/5jG"
|
||||
|
||||
|
||||
--=-bFkxH1S3HVGcxi+o/5jG
|
||||
Content-Type: text/html; CHARSET=utf-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 TRANSITIONAL//EN">
|
||||
<HTML>
|
||||
<HEAD>
|
||||
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; CHARSET=UTF-8">
|
||||
<META NAME="GENERATOR" CONTENT="GtkHTML/1.1.10">
|
||||
</HEAD>
|
||||
<BODY>
|
||||
<FONT COLOR="#00fc00">If this sentence is green, you're viewing the HTML part.</FONT><BR>
|
||||
<IMG SRC="cid:1066973340.4232.46.camel@localhost" ALIGN="top" ALT="" BORDER="0"><BR>
|
||||
Now, this is the way that all MUAs <B>SHOULD</B> treat this kind of situation. The layout is like so:<BR>
|
||||
<BR>
|
||||
multipart/alternative<BR>
|
||||
| text/plain<BR>
|
||||
| multipart/related<BR>
|
||||
| | text/html<BR>
|
||||
| | image/gif<BR>
|
||||
<BR>
|
||||
See? The GIF (which by the way should be inline towards the top of this message) is related to the HTML, and that whole block is an alternative to a text/plain part. This is the opposite of the way shown in the previous email.<BR>
|
||||
<BR>
|
||||
Also, the embedded image here does not have a filename. As mentioned above, the MUA should suggest something as a filename, even here (the user may want to save the embedded image, so a filename would be helpful). In this case, I would recommend appending the random text to be suggested to the user with the part's subtype, in this case something like c20vsidlkvm.gif.
|
||||
</BODY>
|
||||
</HTML>
|
||||
|
||||
--=-bFkxH1S3HVGcxi+o/5jG
|
||||
Content-ID: <1066973340.4232.46.camel@localhost>
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Type: image/gif
|
||||
|
||||
R0lGODlhBQALAPIAAKIA/64A/8ZL/////8BS/2QDANZ//wAAACH5BAEAAAMALAAAAAAFAAsAAAMY
|
||||
OBIytsIYEoiEl0lqFWgKM4zkUJzjWQwJADs=
|
||||
|
||||
--=-bFkxH1S3HVGcxi+o/5jG--
|
||||
|
||||
--=-tyGlQ9JvB5uvPWzozI+y--
|
||||
|
||||
--=-qYxqvD9rbH0PNeExagh1--
|
42
imap-core/test/fixtures/simple.eml
vendored
Normal file
42
imap-core/test/fixtures/simple.eml
vendored
Normal file
|
@ -0,0 +1,42 @@
|
|||
Content-Type: multipart/mixed;
|
||||
boundary="----sinikael-?=_1-14508625179060.6947296333964914"
|
||||
From: =?UTF-8?Q?Sender_Name_=F0=9F=91=BB?= <sender@example.com>
|
||||
To: =?UTF-8?Q?Receiver_Name_=F0=9F=91=A5?= <receiver@example.com>
|
||||
Subject: Nodemailer is unicode friendly =?UTF-8?Q?=E2=9C=94?=
|
||||
Date: Wed, 23 Dec 2015 09:21:57 +0000
|
||||
Message-Id: <1450862517911-ebf3ac1e-e421bda1-4d17b820@example.com>
|
||||
MIME-Version: 1.0
|
||||
|
||||
------sinikael-?=_1-14508625179060.6947296333964914
|
||||
Content-Type: multipart/alternative;
|
||||
boundary="----sinikael-?=_2-14508625179060.6947296333964914"
|
||||
|
||||
------sinikael-?=_2-14508625179060.6947296333964914
|
||||
Content-Type: text/plain
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
hello
|
||||
------sinikael-?=_2-14508625179060.6947296333964914
|
||||
Content-Type: text/html
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
<p>hello</p>
|
||||
------sinikael-?=_2-14508625179060.6947296333964914--
|
||||
|
||||
------sinikael-?=_1-14508625179060.6947296333964914
|
||||
Content-Type: text/plain; charset=utf-8; name="notes
|
||||
=?UTF-8?Q?=F0=9F=95=B6=2Etxt?="
|
||||
Content-Disposition: attachment;
|
||||
filename*0*=utf-8''notes%20%F0%9F%95%B6.txt
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
Some notes about this e-mail =F0=9F=90=B1
|
||||
------sinikael-?=_1-14508625179060.6947296333964914
|
||||
Content-Type: image/png; name=image.png
|
||||
Content-Disposition: attachment; filename=image.png
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lE
|
||||
QVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQ
|
||||
AAAAAElFTkSuQmCC
|
||||
------sinikael-?=_1-14508625179060.6947296333964914--
|
164
imap-core/test/fixtures/simple.json
vendored
Normal file
164
imap-core/test/fixtures/simple.json
vendored
Normal file
|
@ -0,0 +1,164 @@
|
|||
{
|
||||
"childNodes": [{
|
||||
"childNodes": [{
|
||||
"header": [
|
||||
"Content-Type: text/plain",
|
||||
"Content-Transfer-Encoding: 7bit"
|
||||
],
|
||||
"parsedHeader": {
|
||||
"content-transfer-encoding": "7bit",
|
||||
"content-type": {
|
||||
"value": "text/plain",
|
||||
"type": "text",
|
||||
"subtype": "plain",
|
||||
"params": {}
|
||||
}
|
||||
},
|
||||
"body": "hello",
|
||||
"multipart": false,
|
||||
"boundary": false,
|
||||
"lineCount": 1,
|
||||
"size": 5
|
||||
}, {
|
||||
"header": [
|
||||
"Content-Type: text/html",
|
||||
"Content-Transfer-Encoding: 7bit"
|
||||
],
|
||||
"parsedHeader": {
|
||||
"content-transfer-encoding": "7bit",
|
||||
"content-type": {
|
||||
"value": "text/html",
|
||||
"type": "text",
|
||||
"subtype": "html",
|
||||
"params": {}
|
||||
}
|
||||
},
|
||||
"body": "<p>hello</p>",
|
||||
"multipart": false,
|
||||
"boundary": false,
|
||||
"lineCount": 1,
|
||||
"size": 12
|
||||
}],
|
||||
"header": [
|
||||
"Content-Type: multipart/alternative;\r\n boundary=\"----sinikael-?=_2-14508625179060.6947296333964914\""
|
||||
],
|
||||
"parsedHeader": {
|
||||
"content-type": {
|
||||
"value": "multipart/alternative",
|
||||
"type": "multipart",
|
||||
"subtype": "alternative",
|
||||
"params": {
|
||||
"boundary": "----sinikael-?=_2-14508625179060.6947296333964914"
|
||||
},
|
||||
"hasParams": true
|
||||
}
|
||||
},
|
||||
"body": "",
|
||||
"multipart": "alternative",
|
||||
"boundary": "----sinikael-?=_2-14508625179060.6947296333964914",
|
||||
"lineCount": 1,
|
||||
"size": 0
|
||||
}, {
|
||||
"header": [
|
||||
"Content-Type: text/plain; charset=utf-8; name=\"notes\r\n =?UTF-8?Q?=F0=9F=95=B6=2Etxt?=\"",
|
||||
"Content-Disposition: attachment;\r\n filename*0*=utf-8''notes%20%F0%9F%95%B6.txt",
|
||||
"Content-Transfer-Encoding: quoted-printable"
|
||||
],
|
||||
"parsedHeader": {
|
||||
"content-transfer-encoding": "quoted-printable",
|
||||
"content-disposition": {
|
||||
"value": "attachment",
|
||||
"type": "attachment",
|
||||
"subtype": "",
|
||||
"params": {
|
||||
"filename": "=?UTF-8?Q?notes=20=F0=9F=95=B6.txt?="
|
||||
},
|
||||
"hasParams": true
|
||||
},
|
||||
"content-type": {
|
||||
"value": "text/plain",
|
||||
"type": "text",
|
||||
"subtype": "plain",
|
||||
"params": {
|
||||
"charset": "utf-8",
|
||||
"name": "notes =?UTF-8?Q?=F0=9F=95=B6=2Etxt?="
|
||||
},
|
||||
"hasParams": true
|
||||
}
|
||||
},
|
||||
"body": "Some notes about this e-mail =F0=9F=90=B1",
|
||||
"multipart": false,
|
||||
"boundary": false,
|
||||
"lineCount": 1,
|
||||
"size": 41
|
||||
}, {
|
||||
"header": [
|
||||
"Content-Type: image/png; name=image.png",
|
||||
"Content-Disposition: attachment; filename=image.png",
|
||||
"Content-Transfer-Encoding: base64"
|
||||
],
|
||||
"parsedHeader": {
|
||||
"content-transfer-encoding": "base64",
|
||||
"content-disposition": {
|
||||
"value": "attachment",
|
||||
"type": "attachment",
|
||||
"subtype": "",
|
||||
"params": {
|
||||
"filename": "image.png"
|
||||
},
|
||||
"hasParams": true
|
||||
},
|
||||
"content-type": {
|
||||
"value": "image/png",
|
||||
"type": "image",
|
||||
"subtype": "png",
|
||||
"params": {
|
||||
"name": "image.png"
|
||||
},
|
||||
"hasParams": true
|
||||
}
|
||||
},
|
||||
"body": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lE\r\nQVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQ\r\nAAAAAElFTkSuQmCC",
|
||||
"multipart": false,
|
||||
"boundary": false,
|
||||
"lineCount": 3,
|
||||
"size": 172
|
||||
}],
|
||||
"header": [
|
||||
"Content-Type: multipart/mixed;\r\n boundary=\"----sinikael-?=_1-14508625179060.6947296333964914\"",
|
||||
"From: =?UTF-8?Q?Sender_Name_=F0=9F=91=BB?= <sender@example.com>",
|
||||
"To: =?UTF-8?Q?Receiver_Name_=F0=9F=91=A5?= <receiver@example.com>",
|
||||
"Subject: Nodemailer is unicode friendly =?UTF-8?Q?=E2=9C=94?=",
|
||||
"Date: Wed, 23 Dec 2015 09:21:57 +0000",
|
||||
"Message-Id: <1450862517911-ebf3ac1e-e421bda1-4d17b820@example.com>",
|
||||
"MIME-Version: 1.0"
|
||||
],
|
||||
"parsedHeader": {
|
||||
"mime-version": "1.0",
|
||||
"message-id": "<1450862517911-ebf3ac1e-e421bda1-4d17b820@example.com>",
|
||||
"date": "Wed, 23 Dec 2015 09:21:57 +0000",
|
||||
"subject": "Nodemailer is unicode friendly =?UTF-8?Q?=E2=9C=94?=",
|
||||
"to": [{
|
||||
"address": "receiver@example.com",
|
||||
"name": "=?UTF-8?Q?Receiver_Name_=F0=9F=91=A5?="
|
||||
}],
|
||||
"from": [{
|
||||
"address": "sender@example.com",
|
||||
"name": "=?UTF-8?Q?Sender_Name_=F0=9F=91=BB?="
|
||||
}],
|
||||
"content-type": {
|
||||
"value": "multipart/mixed",
|
||||
"type": "multipart",
|
||||
"subtype": "mixed",
|
||||
"params": {
|
||||
"boundary": "----sinikael-?=_1-14508625179060.6947296333964914"
|
||||
},
|
||||
"hasParams": true
|
||||
}
|
||||
},
|
||||
"body": "",
|
||||
"multipart": "mixed",
|
||||
"boundary": "----sinikael-?=_1-14508625179060.6947296333964914",
|
||||
"lineCount": 1,
|
||||
"size": 0
|
||||
}
|
644
imap-core/test/imap-compile-stream-test.js
Normal file
644
imap-core/test/imap-compile-stream-test.js
Normal file
|
@ -0,0 +1,644 @@
|
|||
/*eslint no-unused-expressions: 0, prefer-arrow-callback: 0 */
|
||||
|
||||
'use strict';
|
||||
|
||||
let chai = require('chai');
|
||||
let imapHandler = require('../lib/handler/imap-handler');
|
||||
let PassThrough = require('stream').PassThrough;
|
||||
let crypto = require('crypto');
|
||||
let expect = chai.expect;
|
||||
chai.config.includeStack = true;
|
||||
|
||||
describe('IMAP Command Compile Stream', function () {
|
||||
|
||||
describe('#compile', function () {
|
||||
|
||||
it('should compile correctly', function (done) {
|
||||
let command = '* FETCH (ENVELOPE ("Mon, 2 Sep 2013 05:30:13 -0700 (PDT)" NIL ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "tr.ee")) NIL NIL NIL "<-4730417346358914070@unknownmsgid>") BODYSTRUCTURE (("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 105 (NIL NIL ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "pangalink.net")) NIL NIL "<test1>" NIL) ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 12 0 NIL NIL NIL) 5 NIL NIL NIL) ("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 83 (NIL NIL ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "pangalink.net")) NIL NIL "NIL" NIL) ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 12 0 NIL NIL NIL) 4 NIL NIL NIL) ("TEXT" "HTML" ("CHARSET" "utf-8") NIL NIL "QUOTED-PRINTABLE" 19 0 NIL NIL NIL) "MIXED" ("BOUNDARY" "----mailcomposer-?=_1-1328088797399") NIL NIL))',
|
||||
parsed = imapHandler.parser(command, {
|
||||
allowUntagged: true
|
||||
});
|
||||
|
||||
resolveStream(imapHandler.compileStream(parsed), (err, compiled) => {
|
||||
expect(err).to.not.exist;
|
||||
expect(compiled.toString('binary')).to.equal(command);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Types', function () {
|
||||
let parsed;
|
||||
|
||||
beforeEach(function () {
|
||||
parsed = {
|
||||
tag: '*',
|
||||
command: 'CMD'
|
||||
};
|
||||
});
|
||||
|
||||
describe('No attributes', function () {
|
||||
it('should compile correctly', function (done) {
|
||||
let command = '* CMD';
|
||||
|
||||
resolveStream(imapHandler.compileStream(parsed), (err, compiled) => {
|
||||
expect(err).to.not.exist;
|
||||
expect(compiled.toString('binary')).to.equal(command);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('TEXT', function () {
|
||||
it('should compile correctly', function (done) {
|
||||
parsed.attributes = [{
|
||||
type: 'TEXT',
|
||||
value: 'Tere tere!'
|
||||
}];
|
||||
let command = '* CMD Tere tere!';
|
||||
resolveStream(imapHandler.compileStream(parsed), (err, compiled) => {
|
||||
expect(err).to.not.exist;
|
||||
expect(compiled.toString('binary')).to.equal(command);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SECTION', function () {
|
||||
it('should compile correctly', function (done) {
|
||||
parsed.attributes = [{
|
||||
type: 'SECTION',
|
||||
section: [{
|
||||
type: 'ATOM',
|
||||
value: 'ALERT'
|
||||
}]
|
||||
}];
|
||||
let command = '* CMD [ALERT]';
|
||||
resolveStream(imapHandler.compileStream(parsed), (err, compiled) => {
|
||||
expect(err).to.not.exist;
|
||||
expect(compiled.toString('binary')).to.equal(command);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ATOM', function () {
|
||||
it('should compile correctly', function (done) {
|
||||
parsed.attributes = [{
|
||||
type: 'ATOM',
|
||||
value: 'ALERT'
|
||||
}, {
|
||||
type: 'ATOM',
|
||||
value: '\\ALERT'
|
||||
}, {
|
||||
type: 'ATOM',
|
||||
value: 'NO ALERT'
|
||||
}];
|
||||
let command = '* CMD ALERT \\ALERT "NO ALERT"';
|
||||
resolveStream(imapHandler.compileStream(parsed), (err, compiled) => {
|
||||
expect(err).to.not.exist;
|
||||
expect(compiled.toString('binary')).to.equal(command);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SEQUENCE', function () {
|
||||
it('should compile correctly', function (done) {
|
||||
parsed.attributes = [{
|
||||
type: 'SEQUENCE',
|
||||
value: '*:4,5,6'
|
||||
}];
|
||||
let command = '* CMD *:4,5,6';
|
||||
resolveStream(imapHandler.compileStream(parsed), (err, compiled) => {
|
||||
expect(err).to.not.exist;
|
||||
expect(compiled.toString('binary')).to.equal(command);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('NIL', function () {
|
||||
it('should compile correctly', function (done) {
|
||||
parsed.attributes = [
|
||||
null,
|
||||
null
|
||||
];
|
||||
|
||||
let command = '* CMD NIL NIL';
|
||||
resolveStream(imapHandler.compileStream(parsed), (err, compiled) => {
|
||||
expect(err).to.not.exist;
|
||||
expect(compiled.toString('binary')).to.equal(command);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('TEXT', function () {
|
||||
it('should compile correctly', function (done) {
|
||||
parsed.attributes = [
|
||||
// keep indentation
|
||||
{
|
||||
type: 'String',
|
||||
value: 'Tere tere!',
|
||||
sensitive: true
|
||||
},
|
||||
'Vana kere'
|
||||
];
|
||||
|
||||
let command = '* CMD "Tere tere!" "Vana kere"';
|
||||
resolveStream(imapHandler.compileStream(parsed), (err, compiled) => {
|
||||
expect(err).to.not.exist;
|
||||
expect(compiled.toString('binary')).to.equal(command);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should keep short strings', function (done) {
|
||||
parsed.attributes = [
|
||||
// keep indentation
|
||||
{
|
||||
type: 'String',
|
||||
value: 'Tere tere!'
|
||||
},
|
||||
'Vana kere'
|
||||
];
|
||||
|
||||
let command = '* CMD "Tere tere!" "Vana kere"';
|
||||
resolveStream(imapHandler.compileStream(parsed, true), (err, compiled) => {
|
||||
expect(err).to.not.exist;
|
||||
expect(compiled.toString('binary')).to.equal(command);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should hide strings', function (done) {
|
||||
parsed.attributes = [
|
||||
// keep indentation
|
||||
{
|
||||
type: 'String',
|
||||
value: 'Tere tere!',
|
||||
sensitive: true
|
||||
},
|
||||
'Vana kere'
|
||||
];
|
||||
|
||||
let command = '* CMD "(* value hidden *)" "Vana kere"';
|
||||
resolveStream(imapHandler.compileStream(parsed, true), (err, compiled) => {
|
||||
expect(err).to.not.exist;
|
||||
expect(compiled.toString('binary')).to.equal(command);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should hide long strings', function (done) {
|
||||
parsed.attributes = [
|
||||
// keep indentation
|
||||
{
|
||||
type: 'String',
|
||||
value: 'Tere tere! Tere tere! Tere tere! Tere tere! Tere tere!'
|
||||
},
|
||||
'Vana kere'
|
||||
];
|
||||
|
||||
let command = '* CMD "(* 54B string *)" "Vana kere"';
|
||||
resolveStream(imapHandler.compileStream(parsed, true), (err, compiled) => {
|
||||
expect(err).to.not.exist;
|
||||
expect(compiled.toString('binary')).to.equal(command);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('No Command', function () {
|
||||
it('should compile correctly', function (done) {
|
||||
parsed = {
|
||||
tag: '*',
|
||||
attributes: [
|
||||
1, {
|
||||
type: 'ATOM',
|
||||
value: 'EXPUNGE'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
let command = '* 1 EXPUNGE';
|
||||
resolveStream(imapHandler.compileStream(parsed), (err, compiled) => {
|
||||
expect(err).to.not.exist;
|
||||
expect(compiled.toString('binary')).to.equal(command);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('Literal', function () {
|
||||
it('shoud return as text', function (done) {
|
||||
let parsed = {
|
||||
tag: '*',
|
||||
command: 'CMD',
|
||||
attributes: [
|
||||
// keep indentation
|
||||
{
|
||||
type: 'LITERAL',
|
||||
value: 'Tere tere!'
|
||||
},
|
||||
'Vana kere'
|
||||
]
|
||||
};
|
||||
|
||||
let command = '* CMD {10}\r\nTere tere! "Vana kere"';
|
||||
resolveStream(imapHandler.compileStream(parsed), (err, compiled) => {
|
||||
expect(err).to.not.exist;
|
||||
expect(compiled.toString('binary')).to.equal(command);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should compile correctly without tag and command', function (done) {
|
||||
let parsed = {
|
||||
attributes: [{
|
||||
type: 'LITERAL',
|
||||
value: 'Tere tere!'
|
||||
}, {
|
||||
type: 'LITERAL',
|
||||
value: 'Vana kere'
|
||||
}]
|
||||
};
|
||||
let command = '{10}\r\nTere tere! {9}\r\nVana kere';
|
||||
resolveStream(imapHandler.compileStream(parsed), (err, compiled) => {
|
||||
expect(err).to.not.exist;
|
||||
expect(compiled.toString('binary')).to.equal(command);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('shoud return byte length', function (done) {
|
||||
let parsed = {
|
||||
tag: '*',
|
||||
command: 'CMD',
|
||||
attributes: [
|
||||
// keep indentation
|
||||
{
|
||||
type: 'LITERAL',
|
||||
value: 'Tere tere!'
|
||||
},
|
||||
'Vana kere'
|
||||
]
|
||||
};
|
||||
|
||||
let command = '* CMD "(* 10B literal *)" "Vana kere"';
|
||||
resolveStream(imapHandler.compileStream(parsed, true), (err, compiled) => {
|
||||
expect(err).to.not.exist;
|
||||
expect(compiled.toString('binary')).to.equal(command);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should mix with other values', function (done) {
|
||||
let s = new PassThrough();
|
||||
let str = 'abc'.repeat(100);
|
||||
let parsed = {
|
||||
attributes: [{
|
||||
type: 'ATOM',
|
||||
value: 'BODY'
|
||||
}, {
|
||||
type: 'LITERAL',
|
||||
value: 'Tere tere!'
|
||||
}, {
|
||||
type: 'ATOM',
|
||||
value: 'BODY'
|
||||
}, {
|
||||
type: 'LITERAL',
|
||||
//value: str
|
||||
value: s,
|
||||
expectedLength: str.length
|
||||
}, {
|
||||
type: 'ATOM',
|
||||
value: 'UID'
|
||||
}, {
|
||||
type: 'ATOM',
|
||||
value: '12345'
|
||||
}]
|
||||
};
|
||||
setImmediate(function(){
|
||||
s.end(str);
|
||||
});
|
||||
let command = 'BODY {10}\r\nTere tere! BODY {' + str.length + '}\r\n' + str + ' UID 12345';
|
||||
resolveStream(imapHandler.compileStream(parsed), (err, compiled) => {
|
||||
expect(err).to.not.exist;
|
||||
expect(compiled.toString('binary')).to.equal(command);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('shoud pipe literal streams', function (done) {
|
||||
let stream1 = new PassThrough();
|
||||
let stream2 = new PassThrough();
|
||||
let stream3 = new PassThrough();
|
||||
let parsed = {
|
||||
tag: '*',
|
||||
command: 'CMD',
|
||||
attributes: [
|
||||
// keep indentation
|
||||
{
|
||||
type: 'LITERAL',
|
||||
value: 'Tere tere!'
|
||||
}, {
|
||||
type: 'LITERAL',
|
||||
expectedLength: 5,
|
||||
value: stream1
|
||||
},
|
||||
'Vana kere', {
|
||||
type: 'LITERAL',
|
||||
expectedLength: 7,
|
||||
value: stream2
|
||||
}, {
|
||||
type: 'LITERAL',
|
||||
value: 'Kuidas laheb?'
|
||||
}, {
|
||||
type: 'LITERAL',
|
||||
expectedLength: 5,
|
||||
value: stream3
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
let command = '* CMD {10}\r\nTere tere! {5}\r\ntest1 "Vana kere" {7}\r\ntest2 {13}\r\nKuidas laheb? {5}\r\ntest3';
|
||||
resolveStream(imapHandler.compileStream(parsed, false), (err, compiled) => {
|
||||
expect(err).to.not.exist;
|
||||
expect(compiled.toString('binary')).to.equal(command);
|
||||
done();
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
stream2.end('test2');
|
||||
setTimeout(() => {
|
||||
stream1.end('test1');
|
||||
setTimeout(() => {
|
||||
stream3.end('test3');
|
||||
}, 100);
|
||||
}, 100);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('shoud pipe limited literal streams', function (done) {
|
||||
let stream1 = new PassThrough();
|
||||
let stream2 = new PassThrough();
|
||||
let stream3 = new PassThrough();
|
||||
let parsed = {
|
||||
tag: '*',
|
||||
command: 'CMD',
|
||||
attributes: [
|
||||
// keep indentation
|
||||
{
|
||||
type: 'LITERAL',
|
||||
value: 'Tere tere!'
|
||||
}, {
|
||||
type: 'LITERAL',
|
||||
expectedLength: 5,
|
||||
value: stream1,
|
||||
startFrom: 2,
|
||||
maxLength: 2
|
||||
},
|
||||
'Vana kere', {
|
||||
type: 'LITERAL',
|
||||
expectedLength: 7,
|
||||
value: stream2,
|
||||
startFrom: 2
|
||||
}, {
|
||||
type: 'LITERAL',
|
||||
value: 'Kuidas laheb?'
|
||||
}, {
|
||||
type: 'LITERAL',
|
||||
expectedLength: 7,
|
||||
value: stream3,
|
||||
startFrom: 2,
|
||||
maxLength: 2
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
let command = '* CMD {10}\r\nTere tere! {2}\r\nst "Vana kere" {5}\r\nst2 {13}\r\nKuidas laheb? {2}\r\nst';
|
||||
resolveStream(imapHandler.compileStream(parsed, false), (err, compiled) => {
|
||||
expect(err).to.not.exist;
|
||||
expect(compiled.toString('binary')).to.equal(command);
|
||||
done();
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
stream2.end('test2');
|
||||
setTimeout(() => {
|
||||
stream1.end('test1');
|
||||
setTimeout(() => {
|
||||
stream3.end('test3');
|
||||
}, 100);
|
||||
}, 100);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('shoud pipe errors for literal streams', function (done) {
|
||||
let stream1 = new PassThrough();
|
||||
let parsed = {
|
||||
tag: '*',
|
||||
command: 'CMD',
|
||||
attributes: [
|
||||
// keep indentation
|
||||
{
|
||||
type: 'LITERAL',
|
||||
value: 'Tere tere!'
|
||||
}, {
|
||||
type: 'LITERAL',
|
||||
expectedLength: 5,
|
||||
value: stream1
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
resolveStream(imapHandler.compileStream(parsed, false), err => {
|
||||
expect(err).to.exist;
|
||||
done();
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
stream1.emit('error', new Error('Stream error'));
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#LengthLimiter', function () {
|
||||
this.timeout(10000); //eslint-disable-line no-invalid-this
|
||||
|
||||
it('should emit exact length', function (done) {
|
||||
let len = 1024;
|
||||
let limiter = new imapHandler.compileStream.LengthLimiter(len);
|
||||
let expected = 'X'.repeat(len);
|
||||
|
||||
resolveStream(limiter, (err, value) => {
|
||||
value = value.toString();
|
||||
expect(err).to.not.exist;
|
||||
expect(value).to.equal(expected);
|
||||
done();
|
||||
});
|
||||
|
||||
let emitted = 0;
|
||||
let emitter = () => {
|
||||
let str = 'X'.repeat(128);
|
||||
emitted += str.length;
|
||||
limiter.write(new Buffer(str));
|
||||
if (emitted >= len) {
|
||||
limiter.end();
|
||||
} else {
|
||||
setTimeout(emitter, 100);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(emitter, 100);
|
||||
});
|
||||
|
||||
it('should truncate output', function (done) {
|
||||
let len = 1024;
|
||||
let limiter = new imapHandler.compileStream.LengthLimiter(len - 100);
|
||||
let expected = 'X'.repeat(len - 100);
|
||||
|
||||
resolveStream(limiter, (err, value) => {
|
||||
value = value.toString();
|
||||
expect(err).to.not.exist;
|
||||
expect(value).to.equal(expected);
|
||||
done();
|
||||
});
|
||||
|
||||
let emitted = 0;
|
||||
let emitter = () => {
|
||||
let str = 'X'.repeat(128);
|
||||
emitted += str.length;
|
||||
limiter.write(new Buffer(str));
|
||||
if (emitted >= len) {
|
||||
limiter.end();
|
||||
} else {
|
||||
setTimeout(emitter, 100);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(emitter, 100);
|
||||
});
|
||||
|
||||
it('should skip output', function (done) {
|
||||
let len = 1024;
|
||||
let limiter = new imapHandler.compileStream.LengthLimiter(len - 100, false, 30);
|
||||
let expected = 'X'.repeat(len - 100 - 30);
|
||||
|
||||
resolveStream(limiter, (err, value) => {
|
||||
value = value.toString();
|
||||
expect(err).to.not.exist;
|
||||
expect(value).to.equal(expected);
|
||||
done();
|
||||
});
|
||||
|
||||
let emitted = 0;
|
||||
let emitter = () => {
|
||||
let str = 'X'.repeat(128);
|
||||
emitted += str.length;
|
||||
limiter.write(new Buffer(str));
|
||||
if (emitted >= len) {
|
||||
limiter.end();
|
||||
} else {
|
||||
setTimeout(emitter, 100);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(emitter, 100);
|
||||
});
|
||||
|
||||
it('should pad output', function (done) {
|
||||
let len = 1024;
|
||||
let limiter = new imapHandler.compileStream.LengthLimiter(len + 100);
|
||||
let expected = 'X'.repeat(len) + ' '.repeat(100);
|
||||
|
||||
resolveStream(limiter, (err, value) => {
|
||||
value = value.toString();
|
||||
expect(err).to.not.exist;
|
||||
expect(value).to.equal(expected);
|
||||
done();
|
||||
});
|
||||
|
||||
let emitted = 0;
|
||||
let emitter = () => {
|
||||
let str = 'X'.repeat(128);
|
||||
emitted += str.length;
|
||||
limiter.write(new Buffer(str));
|
||||
if (emitted >= len) {
|
||||
limiter.end();
|
||||
} else {
|
||||
setTimeout(emitter, 100);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(emitter, 100);
|
||||
});
|
||||
|
||||
it('should pipe to several streams', function (done) {
|
||||
let len = 1024;
|
||||
let start = 30;
|
||||
let margin = 200;
|
||||
let limiter = new imapHandler.compileStream.LengthLimiter(len - margin, false, start);
|
||||
let stream = new PassThrough();
|
||||
let input = crypto.randomBytes(len / 2).toString('hex');
|
||||
let expected1 = input.substr(start, len - margin - start);
|
||||
let expected2 = input.substr(len - margin);
|
||||
|
||||
limiter.on('done', function (remainder) {
|
||||
stream.unpipe(limiter);
|
||||
if (remainder) {
|
||||
stream.unshift(remainder);
|
||||
}
|
||||
limiter.end();
|
||||
});
|
||||
|
||||
resolveStream(limiter, (err, value) => {
|
||||
value = value.toString();
|
||||
expect(err).to.not.exist;
|
||||
expect(value).to.equal(expected1);
|
||||
resolveStream(stream, (err, value) => {
|
||||
value = value.toString();
|
||||
expect(err).to.not.exist;
|
||||
expect(value).to.equal(expected2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
let emitted = 0;
|
||||
let emitter = () => {
|
||||
let str = input.substr(emitted, 128);
|
||||
emitted += str.length;
|
||||
|
||||
stream.write(new Buffer(str));
|
||||
if (emitted >= len) {
|
||||
stream.end();
|
||||
} else {
|
||||
setImmediate(emitter);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
stream.pipe(limiter);
|
||||
setImmediate(emitter);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function resolveStream(stream, callback) {
|
||||
let chunks = [];
|
||||
let chunklen = 0;
|
||||
|
||||
stream.on('readable', () => {
|
||||
let chunk;
|
||||
|
||||
while ((chunk = stream.read()) !== null) {
|
||||
chunks.push(chunk);
|
||||
chunklen += chunk.length;
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('error', err => callback(err));
|
||||
stream.on('end', () => callback(null, Buffer.concat(chunks, chunklen)));
|
||||
}
|
255
imap-core/test/imap-compiler-test.js
Normal file
255
imap-core/test/imap-compiler-test.js
Normal file
|
@ -0,0 +1,255 @@
|
|||
/*eslint no-unused-expressions: 0, prefer-arrow-callback: 0 */
|
||||
|
||||
'use strict';
|
||||
|
||||
let chai = require('chai');
|
||||
let imapHandler = require('../lib/handler/imap-handler');
|
||||
let expect = chai.expect;
|
||||
chai.config.includeStack = true;
|
||||
|
||||
describe('IMAP Command Compiler', function () {
|
||||
describe('#compile', function () {
|
||||
it('should compile correctly', function () {
|
||||
let command = '* FETCH (ENVELOPE ("Mon, 2 Sep 2013 05:30:13 -0700 (PDT)" NIL ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "tr.ee")) NIL NIL NIL "<-4730417346358914070@unknownmsgid>") BODYSTRUCTURE (("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 105 (NIL NIL ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "pangalink.net")) NIL NIL "<test1>" NIL) ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 12 0 NIL NIL NIL) 5 NIL NIL NIL) ("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 83 (NIL NIL ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "pangalink.net")) NIL NIL "NIL" NIL) ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 12 0 NIL NIL NIL) 4 NIL NIL NIL) ("TEXT" "HTML" ("CHARSET" "utf-8") NIL NIL "QUOTED-PRINTABLE" 19 0 NIL NIL NIL) "MIXED" ("BOUNDARY" "----mailcomposer-?=_1-1328088797399") NIL NIL))',
|
||||
parsed = imapHandler.parser(command, {
|
||||
allowUntagged: true
|
||||
}),
|
||||
compiled = imapHandler.compiler(parsed);
|
||||
|
||||
expect(compiled).to.equal(command);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Types', function () {
|
||||
let parsed;
|
||||
|
||||
beforeEach(function () {
|
||||
parsed = {
|
||||
tag: '*',
|
||||
command: 'CMD'
|
||||
};
|
||||
});
|
||||
|
||||
describe('No attributes', function () {
|
||||
it('should compile correctly', function () {
|
||||
expect(imapHandler.compiler(parsed)).to.equal('* CMD');
|
||||
});
|
||||
});
|
||||
|
||||
describe('TEXT', function () {
|
||||
it('should compile correctly', function () {
|
||||
parsed.attributes = [{
|
||||
type: 'TEXT',
|
||||
value: 'Tere tere!'
|
||||
}];
|
||||
expect(imapHandler.compiler(parsed)).to.equal('* CMD Tere tere!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SECTION', function () {
|
||||
it('should compile correctly', function () {
|
||||
parsed.attributes = [{
|
||||
type: 'SECTION',
|
||||
section: [{
|
||||
type: 'ATOM',
|
||||
value: 'ALERT'
|
||||
}]
|
||||
}];
|
||||
expect(imapHandler.compiler(parsed)).to.equal('* CMD [ALERT]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ATOM', function () {
|
||||
it('should compile correctly', function () {
|
||||
parsed.attributes = [{
|
||||
type: 'ATOM',
|
||||
value: 'ALERT'
|
||||
}, {
|
||||
type: 'ATOM',
|
||||
value: '\\ALERT'
|
||||
}, {
|
||||
type: 'ATOM',
|
||||
value: 'NO ALERT'
|
||||
}];
|
||||
expect(imapHandler.compiler(parsed)).to.equal('* CMD ALERT \\ALERT "NO ALERT"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SEQUENCE', function () {
|
||||
it('should compile correctly', function () {
|
||||
parsed.attributes = [{
|
||||
type: 'SEQUENCE',
|
||||
value: '*:4,5,6'
|
||||
}];
|
||||
expect(imapHandler.compiler(parsed)).to.equal('* CMD *:4,5,6');
|
||||
});
|
||||
});
|
||||
|
||||
describe('NIL', function () {
|
||||
it('should compile correctly', function () {
|
||||
parsed.attributes = [
|
||||
null,
|
||||
null
|
||||
];
|
||||
|
||||
expect(imapHandler.compiler(parsed)).to.equal('* CMD NIL NIL');
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('TEXT', function () {
|
||||
it('should compile correctly', function () {
|
||||
parsed.attributes = [
|
||||
// keep indentation
|
||||
{
|
||||
type: 'String',
|
||||
value: 'Tere tere!',
|
||||
sensitive: true
|
||||
},
|
||||
'Vana kere'
|
||||
];
|
||||
|
||||
expect(imapHandler.compiler(parsed)).to.equal('* CMD "Tere tere!" "Vana kere"');
|
||||
|
||||
});
|
||||
|
||||
it('should keep short strings', function () {
|
||||
parsed.attributes = [
|
||||
// keep indentation
|
||||
{
|
||||
type: 'String',
|
||||
value: 'Tere tere!'
|
||||
},
|
||||
'Vana kere'
|
||||
];
|
||||
|
||||
expect(imapHandler.compiler(parsed, false, true)).to.equal('* CMD "Tere tere!" "Vana kere"');
|
||||
});
|
||||
|
||||
it('should hide strings', function () {
|
||||
parsed.attributes = [
|
||||
// keep indentation
|
||||
{
|
||||
type: 'String',
|
||||
value: 'Tere tere!',
|
||||
sensitive: true
|
||||
},
|
||||
'Vana kere'
|
||||
];
|
||||
|
||||
expect(imapHandler.compiler(parsed, false, true)).to.equal('* CMD "(* value hidden *)" "Vana kere"');
|
||||
});
|
||||
|
||||
it('should hide long strings', function () {
|
||||
parsed.attributes = [
|
||||
// keep indentation
|
||||
{
|
||||
type: 'String',
|
||||
value: 'Tere tere! Tere tere! Tere tere! Tere tere! Tere tere!'
|
||||
},
|
||||
'Vana kere'
|
||||
];
|
||||
|
||||
expect(imapHandler.compiler(parsed, false, true)).to.equal('* CMD "(* 54B string *)" "Vana kere"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('No Command', function () {
|
||||
it('should compile correctly', function () {
|
||||
parsed = {
|
||||
tag: '*',
|
||||
attributes: [
|
||||
1, {
|
||||
type: 'ATOM',
|
||||
value: 'EXPUNGE'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
expect(imapHandler.compiler(parsed)).to.equal('* 1 EXPUNGE');
|
||||
});
|
||||
});
|
||||
describe('Literal', function () {
|
||||
it('shoud return as text', function () {
|
||||
let parsed = {
|
||||
tag: '*',
|
||||
command: 'CMD',
|
||||
attributes: [
|
||||
// keep indentation
|
||||
{
|
||||
type: 'LITERAL',
|
||||
value: 'Tere tere!'
|
||||
},
|
||||
'Vana kere'
|
||||
]
|
||||
};
|
||||
|
||||
expect(imapHandler.compiler(parsed)).to.equal('* CMD {10}\r\nTere tere! "Vana kere"');
|
||||
});
|
||||
|
||||
it('should return as an array text 1', function () {
|
||||
let parsed = {
|
||||
tag: '*',
|
||||
command: 'CMD',
|
||||
attributes: [{
|
||||
type: 'LITERAL',
|
||||
value: 'Tere tere!'
|
||||
}, {
|
||||
type: 'LITERAL',
|
||||
value: 'Vana kere'
|
||||
}]
|
||||
};
|
||||
expect(imapHandler.compiler(parsed, true)).to.deep.equal(['* CMD {10}\r\n', 'Tere tere! {9}\r\n', 'Vana kere']);
|
||||
});
|
||||
|
||||
it('should return as an array text 2', function () {
|
||||
let parsed = {
|
||||
tag: '*',
|
||||
command: 'CMD',
|
||||
attributes: [
|
||||
// keep indentation
|
||||
{
|
||||
type: 'LITERAL',
|
||||
value: 'Tere tere!'
|
||||
}, {
|
||||
type: 'LITERAL',
|
||||
value: 'Vana kere'
|
||||
},
|
||||
'zzz'
|
||||
]
|
||||
};
|
||||
expect(imapHandler.compiler(parsed, true)).to.deep.equal(['* CMD {10}\r\n', 'Tere tere! {9}\r\n', 'Vana kere "zzz"']);
|
||||
});
|
||||
|
||||
it('should compile correctly without tag and command', function () {
|
||||
let parsed = {
|
||||
attributes: [{
|
||||
type: 'LITERAL',
|
||||
value: 'Tere tere!'
|
||||
}, {
|
||||
type: 'LITERAL',
|
||||
value: 'Vana kere'
|
||||
}]
|
||||
};
|
||||
expect(imapHandler.compiler(parsed, true)).to.deep.equal(['{10}\r\n', 'Tere tere! {9}\r\n', 'Vana kere']);
|
||||
});
|
||||
|
||||
it('shoud return byte length', function () {
|
||||
let parsed = {
|
||||
tag: '*',
|
||||
command: 'CMD',
|
||||
attributes: [
|
||||
// keep indentation
|
||||
{
|
||||
type: 'LITERAL',
|
||||
value: 'Tere tere!'
|
||||
},
|
||||
'Vana kere'
|
||||
]
|
||||
};
|
||||
|
||||
expect(imapHandler.compiler(parsed, false, true)).to.equal('* CMD "(* 10B literal *)" "Vana kere"');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
232
imap-core/test/imap-indexer-test.js
Normal file
232
imap-core/test/imap-indexer-test.js
Normal file
|
@ -0,0 +1,232 @@
|
|||
/* eslint-disable global-require */
|
||||
/* eslint no-unused-expressions: 0, prefer-arrow-callback: 0 */
|
||||
|
||||
'use strict';
|
||||
|
||||
let chai = require('chai');
|
||||
let expect = chai.expect;
|
||||
|
||||
let http = require('http');
|
||||
let fs = require('fs');
|
||||
let Indexer = require('../lib/indexer/indexer');
|
||||
let indexer = new Indexer();
|
||||
|
||||
chai.config.includeStack = true;
|
||||
|
||||
const HTTP_PORT = 9998;
|
||||
|
||||
let fixtures = {
|
||||
simple: {
|
||||
eml: fs.readFileSync(__dirname + '/fixtures/simple.eml'),
|
||||
tree: require('./fixtures/simple.json')
|
||||
},
|
||||
mimetorture: {
|
||||
eml: fs.readFileSync(__dirname + '/fixtures/mimetorture.eml'),
|
||||
tree: require('./fixtures/mimetorture.json')
|
||||
}
|
||||
};
|
||||
|
||||
describe('#parseMimeTree', function () {
|
||||
it('should parse mime message', function (done) {
|
||||
let parsed = indexer.parseMimeTree(fixtures.simple.eml);
|
||||
|
||||
expect(parsed).to.deep.equal(fixtures.simple.tree);
|
||||
|
||||
parsed = indexer.parseMimeTree(fixtures.mimetorture.eml);
|
||||
|
||||
expect(parsed).to.deep.equal(fixtures.mimetorture.tree);
|
||||
|
||||
indexer.bodyQuery(parsed, '', (err, data) => {
|
||||
expect(err).to.not.exist;
|
||||
expect(data.toString('binary').replace(/\r?\n/g, '\n')).to.equal(fixtures.mimetorture.eml.toString('binary').replace(/\r?\n/g, '\n'));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#rebuild', function () {
|
||||
let httpServer;
|
||||
|
||||
beforeEach(function (done) {
|
||||
httpServer = http.createServer(function (req, res) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/plain'
|
||||
});
|
||||
if (req.url === '/qp') {
|
||||
res.end('<p>Krediitkaardiga on tehtud kulutusi, mida oleks saanud v=C3=B5i pidanud =\r\ntegema muul viisil kui kaardiga. Krediitkaardiga tehtud kulude kohta ei ole=\r\n t=C3=A4htaegselt esitatud aruandlust, kuludokumentidel ei kajastu =\r\npiisavaid selgitusi, mist=C3=B5ttu esineb olulisi piiranguid kulude =\r\nsihip=C3=A4rasuse ja otstarbekuse hindamisel ning kulude p=C3=B5hjendatuse =\r\nkontrollimine on raskendatud.</p>');
|
||||
} else {
|
||||
res.end('Hello World! '.repeat(20) + 'Bye!');
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
httpServer.listen(HTTP_PORT, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
httpServer.close(done);
|
||||
});
|
||||
|
||||
it('should rebuild using stream', function (done) {
|
||||
let message = `Content-Type: multipart/mixed;
|
||||
boundary="foo"
|
||||
|
||||
--foo
|
||||
Content-Type: text/plain
|
||||
Content-Transfer-Encoding: base64
|
||||
X-Attachment-Stream-Size: 264
|
||||
X-Attachment-Stream-Url: <http://localhost:${HTTP_PORT}/>
|
||||
|
||||
--foo--
|
||||
`;
|
||||
let parsed = indexer.parseMimeTree(message);
|
||||
indexer.bodyQuery(parsed, '', (err, data) => {
|
||||
expect(err).to.not.exist;
|
||||
expect(data.length).to.equal(492);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should rebuild stream part', function (done) {
|
||||
let message = `Content-Type: multipart/mixed;
|
||||
boundary="foo"
|
||||
|
||||
--foo
|
||||
Content-Type: text/plain
|
||||
Content-Transfer-Encoding: base64
|
||||
X-Attachment-Stream-Size: 264
|
||||
X-Attachment-Stream-Url: <http://localhost:${HTTP_PORT}/>
|
||||
|
||||
--foo--
|
||||
`;
|
||||
let parsed = indexer.parseMimeTree(message);
|
||||
indexer.bodyQuery(parsed, {
|
||||
path: '1',
|
||||
type: ''
|
||||
}, (err, data) => {
|
||||
expect(err).to.not.exist;
|
||||
expect(data.length).to.equal(360);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should rebuild using stream', function (done) {
|
||||
let message = `Content-Type: text/plain
|
||||
Content-Transfer-Encoding: base64
|
||||
X-Attachment-Stream-Size: 264
|
||||
X-Attachment-Stream-Url: <http://localhost:${HTTP_PORT}/>
|
||||
|
||||
`;
|
||||
let parsed = indexer.parseMimeTree(message);
|
||||
indexer.bodyQuery(parsed, '', (err, data) => {
|
||||
expect(err).to.not.exist;
|
||||
expect(data.length).to.equal(423);
|
||||
indexer.bodyQuery(parsed, {
|
||||
path: '1',
|
||||
type: ''
|
||||
}, (err, data) => {
|
||||
expect(err).to.not.exist;
|
||||
expect(data.length).to.equal(360);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should rebuild using stream with truncated content', function (done) {
|
||||
let message = `Content-Type: text/plain
|
||||
Content-Transfer-Encoding: base64
|
||||
X-Attachment-Stream-Size: 150
|
||||
X-Attachment-Stream-Url: <http://localhost:${HTTP_PORT}/>
|
||||
|
||||
`;
|
||||
let parsed = indexer.parseMimeTree(message);
|
||||
indexer.bodyQuery(parsed, '', (err, data) => {
|
||||
expect(err).to.not.exist;
|
||||
expect(data.length).to.equal(267);
|
||||
indexer.bodyQuery(parsed, {
|
||||
path: '1',
|
||||
type: ''
|
||||
}, (err, data) => {
|
||||
expect(err).to.not.exist;
|
||||
expect(data.length).to.equal(204);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should rebuild using stream with padded content', function (done) {
|
||||
let message = `Content-Type: text/plain
|
||||
Content-Transfer-Encoding: base64
|
||||
X-Attachment-Stream-Size: 280
|
||||
X-Attachment-Stream-Url: <http://localhost:${HTTP_PORT}/>
|
||||
|
||||
`;
|
||||
let parsed = indexer.parseMimeTree(message);
|
||||
indexer.bodyQuery(parsed, '', (err, data) => {
|
||||
expect(err).to.not.exist;
|
||||
expect(data.length).to.equal(447);
|
||||
indexer.bodyQuery(parsed, {
|
||||
path: '1',
|
||||
type: ''
|
||||
}, (err, data) => {
|
||||
expect(err).to.not.exist;
|
||||
expect(data.length).to.equal(384);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return correct attachment size in bodystructure', function () {
|
||||
let message = `Content-Type: multipart/mixed;
|
||||
boundary="foo"
|
||||
|
||||
--foo
|
||||
Content-Type: text/plain
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
Hello world!
|
||||
--foo
|
||||
Content-Type: application/octet-stream
|
||||
Content-Disposition: attachment; filename=normal.bin
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
12345678901234567890
|
||||
--foo
|
||||
Content-Type: application/octet-stream
|
||||
Content-Disposition: attachment; filename=stream.bin
|
||||
Content-Transfer-Encoding: base64
|
||||
X-Attachment-Stream-Size: 264
|
||||
X-Attachment-Stream-Url: <http://localhost:${HTTP_PORT}/>
|
||||
|
||||
--foo--
|
||||
`;
|
||||
let parsed = indexer.parseMimeTree(message);
|
||||
let bodystruct = indexer.getBodyStructure(parsed);
|
||||
expect(bodystruct[1][6]).to.equal(20);
|
||||
expect(bodystruct[2][6]).to.equal(360);
|
||||
});
|
||||
|
||||
it('should rebuild using encoded stream', function (done) {
|
||||
let message = `Content-Type: text/html; charset=utf-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
X-Attachment-Stream-Size: 407
|
||||
X-Attachment-Stream-Url: <http://localhost:${HTTP_PORT}/qp>
|
||||
X-Attachment-Stream-Lines: 6
|
||||
X-Attachment-Stream-Encoded: Yes
|
||||
|
||||
`;
|
||||
let parsed = indexer.parseMimeTree(message);
|
||||
indexer.bodyQuery(parsed, '', (err, data) => {
|
||||
expect(err).to.not.exist;
|
||||
expect(data.length).to.equal(494);
|
||||
indexer.bodyQuery(parsed, {
|
||||
path: '1',
|
||||
type: ''
|
||||
}, (err, data) => {
|
||||
expect(err).to.not.exist;
|
||||
expect(data.length).to.equal(407);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
772
imap-core/test/imap-parser-test.js
Normal file
772
imap-core/test/imap-parser-test.js
Normal file
|
@ -0,0 +1,772 @@
|
|||
/*eslint no-unused-expressions: 0, prefer-arrow-callback: 0 */
|
||||
|
||||
'use strict';
|
||||
|
||||
let chai = require('chai');
|
||||
let imapHandler = require('../lib/handler/imap-handler');
|
||||
let mimetorture = require('./fixtures/mimetorture');
|
||||
|
||||
let expect = chai.expect;
|
||||
chai.config.includeStack = true;
|
||||
|
||||
describe('IMAP Command Parser', function () {
|
||||
describe('get tag', function () {
|
||||
it('should succeed', function () {
|
||||
expect(imapHandler.parser('TAG1 CMD').tag).to.equal('TAG1');
|
||||
});
|
||||
|
||||
it('should fail for unexpected WS', function () {
|
||||
expect(function () {
|
||||
imapHandler.parser(' TAG CMD');
|
||||
}).to.throw(Error);
|
||||
});
|
||||
|
||||
it('should * OK ', function () {
|
||||
expect(function () {
|
||||
imapHandler.parser(' TAG CMD');
|
||||
}).to.throw(Error);
|
||||
});
|
||||
|
||||
it('should + OK ', function () {
|
||||
expect(imapHandler.parser('+ TAG CMD').tag).to.equal('+');
|
||||
});
|
||||
|
||||
it('should allow untagged', function () {
|
||||
expect(function () {
|
||||
imapHandler.parser('* CMD');
|
||||
}).to.not.throw(Error);
|
||||
});
|
||||
|
||||
it('should fail for empty tag', function () {
|
||||
expect(function () {
|
||||
imapHandler.parser('');
|
||||
}).to.throw(Error);
|
||||
});
|
||||
|
||||
it('should fail for unexpected end', function () {
|
||||
expect(function () {
|
||||
imapHandler.parser('TAG1');
|
||||
}).to.throw(Error);
|
||||
});
|
||||
|
||||
it('should fail for invalid char', function () {
|
||||
expect(function () {
|
||||
imapHandler.parser('TAG"1 CMD');
|
||||
}).to.throw(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get arguments', function () {
|
||||
it('should allow trailing whitespace and empty arguments', function () {
|
||||
expect(function () {
|
||||
imapHandler.parser('* SEARCH ');
|
||||
}).to.not.throw(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get command', function () {
|
||||
it('should succeed', function () {
|
||||
expect(imapHandler.parser('TAG1 CMD').command).to.equal('CMD');
|
||||
});
|
||||
|
||||
it('should work for multi word command', function () {
|
||||
expect(imapHandler.parser('TAG1 UID FETCH').command).to.equal('UID FETCH');
|
||||
});
|
||||
|
||||
it('should fail for unexpected WS', function () {
|
||||
expect(function () {
|
||||
imapHandler.parser('TAG1 CMD');
|
||||
}).to.throw(Error);
|
||||
});
|
||||
|
||||
it('should fail for empty command', function () {
|
||||
expect(function () {
|
||||
imapHandler.parser('TAG1 ');
|
||||
}).to.throw(Error);
|
||||
});
|
||||
|
||||
it('should fail for invalid char', function () {
|
||||
expect(function () {
|
||||
imapHandler.parser('TAG1 CM=D');
|
||||
}).to.throw(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get attribute', function () {
|
||||
it('should succeed', function () {
|
||||
expect(imapHandler.parser('TAG1 CMD FED').attributes).to.deep.equal([{
|
||||
type: 'ATOM',
|
||||
value: 'FED'
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should succeed for single whitespace between values', function () {
|
||||
expect(imapHandler.parser('TAG1 CMD FED TED').attributes).to.deep.equal([{
|
||||
type: 'ATOM',
|
||||
value: 'FED'
|
||||
}, {
|
||||
type: 'ATOM',
|
||||
value: 'TED'
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should succeed for ATOM', function () {
|
||||
expect(imapHandler.parser('TAG1 CMD ABCDE').attributes).to.deep.equal([{
|
||||
type: 'ATOM',
|
||||
value: 'ABCDE'
|
||||
}]);
|
||||
|
||||
expect(imapHandler.parser('TAG1 CMD ABCDE DEFGH').attributes).to.deep.equal([{
|
||||
type: 'ATOM',
|
||||
value: 'ABCDE'
|
||||
}, {
|
||||
type: 'ATOM',
|
||||
value: 'DEFGH'
|
||||
}]);
|
||||
|
||||
expect(imapHandler.parser('TAG1 CMD %').attributes).to.deep.equal([{
|
||||
type: 'ATOM',
|
||||
value: '%'
|
||||
}]);
|
||||
|
||||
expect(imapHandler.parser('TAG1 CMD \\*').attributes).to.deep.equal([{
|
||||
type: 'ATOM',
|
||||
value: '\\*'
|
||||
}]);
|
||||
|
||||
expect(imapHandler.parser('12.82 STATUS [Gmail].Trash (UIDNEXT UNSEEN HIGHESTMODSEQ)').attributes).to.deep.equal([
|
||||
// keep indentation
|
||||
{
|
||||
type: 'ATOM',
|
||||
value: '[Gmail].Trash'
|
||||
},
|
||||
[{
|
||||
type: 'ATOM',
|
||||
value: 'UIDNEXT'
|
||||
}, {
|
||||
type: 'ATOM',
|
||||
value: 'UNSEEN'
|
||||
}, {
|
||||
type: 'ATOM',
|
||||
value: 'HIGHESTMODSEQ'
|
||||
}]
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not succeed for ATOM', function () {
|
||||
expect(function () {
|
||||
imapHandler.parser('TAG1 CMD \\*a');
|
||||
}).to.throw(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get string', function () {
|
||||
it('should succeed', function () {
|
||||
expect(imapHandler.parser('TAG1 CMD "ABCDE"').attributes).to.deep.equal([{
|
||||
type: 'STRING',
|
||||
value: 'ABCDE'
|
||||
}]);
|
||||
|
||||
expect(imapHandler.parser('TAG1 CMD "ABCDE" "DEFGH"').attributes).to.deep.equal([{
|
||||
type: 'STRING',
|
||||
value: 'ABCDE'
|
||||
}, {
|
||||
type: 'STRING',
|
||||
value: 'DEFGH'
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should not explode on invalid char', function () {
|
||||
expect(imapHandler.parser('* 1 FETCH (BODY[] "\xc2")').attributes).to.deep.equal([
|
||||
// keep indentation
|
||||
{
|
||||
type: 'ATOM',
|
||||
value: 'FETCH'
|
||||
},
|
||||
[{
|
||||
type: 'ATOM',
|
||||
value: 'BODY',
|
||||
section: []
|
||||
}, {
|
||||
type: 'STRING',
|
||||
value: '\xc2'
|
||||
}]
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get list', function () {
|
||||
it('should succeed', function () {
|
||||
expect(imapHandler.parser('TAG1 CMD (1234)').attributes).to.deep.equal([
|
||||
[{
|
||||
type: 'ATOM',
|
||||
value: '1234'
|
||||
}]
|
||||
]);
|
||||
expect(imapHandler.parser('TAG1 CMD (1234 TERE)').attributes).to.deep.equal([
|
||||
[{
|
||||
type: 'ATOM',
|
||||
value: '1234'
|
||||
}, {
|
||||
type: 'ATOM',
|
||||
value: 'TERE'
|
||||
}]
|
||||
]);
|
||||
expect(imapHandler.parser('TAG1 CMD (1234)(TERE)').attributes).to.deep.equal([
|
||||
[{
|
||||
type: 'ATOM',
|
||||
value: '1234'
|
||||
}],
|
||||
[{
|
||||
type: 'ATOM',
|
||||
value: 'TERE'
|
||||
}]
|
||||
]);
|
||||
expect(imapHandler.parser('TAG1 CMD ( 1234)').attributes).to.deep.equal([
|
||||
[{
|
||||
type: 'ATOM',
|
||||
value: '1234'
|
||||
}]
|
||||
]);
|
||||
// Trailing whitespace in a BODYSTRUCTURE atom list has been
|
||||
// observed on yahoo.co.jp's
|
||||
expect(imapHandler.parser('TAG1 CMD (1234 )').attributes).to.deep.equal([
|
||||
[{
|
||||
type: 'ATOM',
|
||||
value: '1234'
|
||||
}]
|
||||
]);
|
||||
expect(imapHandler.parser('TAG1 CMD (1234) ').attributes).to.deep.equal([
|
||||
[{
|
||||
type: 'ATOM',
|
||||
value: '1234'
|
||||
}]
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('nested list', function () {
|
||||
it('should succeed', function () {
|
||||
expect(imapHandler.parser('TAG1 CMD (((TERE)) VANA)').attributes).to.deep.equal([
|
||||
[
|
||||
[
|
||||
[{
|
||||
type: 'ATOM',
|
||||
value: 'TERE'
|
||||
}]
|
||||
], {
|
||||
type: 'ATOM',
|
||||
value: 'VANA'
|
||||
}
|
||||
]
|
||||
]);
|
||||
expect(imapHandler.parser('TAG1 CMD (( (TERE)) VANA)').attributes).to.deep.equal([
|
||||
[
|
||||
[
|
||||
[{
|
||||
type: 'ATOM',
|
||||
value: 'TERE'
|
||||
}]
|
||||
], {
|
||||
type: 'ATOM',
|
||||
value: 'VANA'
|
||||
}
|
||||
]
|
||||
]);
|
||||
expect(imapHandler.parser('TAG1 CMD (((TERE) ) VANA)').attributes).to.deep.equal([
|
||||
[
|
||||
[
|
||||
[{
|
||||
type: 'ATOM',
|
||||
value: 'TERE'
|
||||
}]
|
||||
], {
|
||||
type: 'ATOM',
|
||||
value: 'VANA'
|
||||
}
|
||||
]
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get literal', function () {
|
||||
it('should succeed', function () {
|
||||
expect(imapHandler.parser('TAG1 CMD {4}\r\nabcd').attributes).to.deep.equal([{
|
||||
type: 'LITERAL',
|
||||
value: 'abcd'
|
||||
}]);
|
||||
|
||||
expect(imapHandler.parser('TAG1 CMD {4}\r\nabcd {4}\r\nkere').attributes).to.deep.equal([{
|
||||
type: 'LITERAL',
|
||||
value: 'abcd'
|
||||
}, {
|
||||
type: 'LITERAL',
|
||||
value: 'kere'
|
||||
}]);
|
||||
|
||||
expect(imapHandler.parser('TAG1 CMD ({4}\r\nabcd {4}\r\nkere)').attributes).to.deep.equal([
|
||||
[{
|
||||
type: 'LITERAL',
|
||||
value: 'abcd'
|
||||
}, {
|
||||
type: 'LITERAL',
|
||||
value: 'kere'
|
||||
}]
|
||||
]);
|
||||
});
|
||||
|
||||
it('should fail', function () {
|
||||
expect(function () {
|
||||
imapHandler.parser('TAG1 CMD {4}\r\nabcd{4} \r\nkere');
|
||||
}).to.throw(Error);
|
||||
});
|
||||
|
||||
it('should allow zero length literal in the end of a list', function () {
|
||||
expect(imapHandler.parser('TAG1 CMD ({0}\r\n)').attributes).to.deep.equal([
|
||||
[{
|
||||
type: 'LITERAL',
|
||||
value: ''
|
||||
}]
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('ATOM Section', function () {
|
||||
it('should succeed', function () {
|
||||
expect(imapHandler.parser('TAG1 CMD BODY[]').attributes).to.deep.equal([{
|
||||
type: 'ATOM',
|
||||
value: 'BODY',
|
||||
section: []
|
||||
}]);
|
||||
expect(imapHandler.parser('TAG1 CMD BODY[(KERE)]').attributes).to.deep.equal([{
|
||||
type: 'ATOM',
|
||||
value: 'BODY',
|
||||
section: [
|
||||
[{
|
||||
type: 'ATOM',
|
||||
value: 'KERE'
|
||||
}]
|
||||
]
|
||||
}]);
|
||||
});
|
||||
it('will not fail due to trailing whitespace', function () {
|
||||
// We intentionally have trailing whitespace in the section here
|
||||
// because we altered the parser to handle this when we made it
|
||||
// legal for lists and it makes sense to accordingly test it.
|
||||
// However, we have no recorded incidences of this happening in
|
||||
// reality (unlike for lists).
|
||||
expect(imapHandler.parser('TAG1 CMD BODY[HEADER.FIELDS (Subject From) ]').attributes).to.deep.equal([{
|
||||
type: 'ATOM',
|
||||
value: 'BODY',
|
||||
section: [
|
||||
// keep indentation
|
||||
{
|
||||
type: 'ATOM',
|
||||
value: 'HEADER.FIELDS'
|
||||
},
|
||||
[{
|
||||
type: 'ATOM',
|
||||
value: 'Subject'
|
||||
}, {
|
||||
type: 'ATOM',
|
||||
value: 'From'
|
||||
}]
|
||||
]
|
||||
}]);
|
||||
});
|
||||
it('should fail where default BODY and BODY.PEEK are allowed to have sections', function () {});
|
||||
expect(function () {
|
||||
imapHandler.parser('TAG1 CMD KODY[]');
|
||||
}).to.throw(Error);
|
||||
});
|
||||
|
||||
describe('Human readable', function () {
|
||||
it('should succeed', function () {
|
||||
expect(imapHandler.parser('* OK [CAPABILITY IDLE] Hello world!')).to.deep.equal({
|
||||
command: 'OK',
|
||||
tag: '*',
|
||||
attributes: [{
|
||||
section: [{
|
||||
type: 'ATOM',
|
||||
value: 'CAPABILITY'
|
||||
}, {
|
||||
type: 'ATOM',
|
||||
value: 'IDLE'
|
||||
}],
|
||||
type: 'ATOM',
|
||||
value: ''
|
||||
}, {
|
||||
type: 'TEXT',
|
||||
value: 'Hello world!'
|
||||
}]
|
||||
});
|
||||
|
||||
expect(imapHandler.parser('* OK Hello world!')).to.deep.equal({
|
||||
command: 'OK',
|
||||
tag: '*',
|
||||
attributes: [{
|
||||
type: 'TEXT',
|
||||
value: 'Hello world!'
|
||||
}]
|
||||
});
|
||||
|
||||
expect(imapHandler.parser('* OK')).to.deep.equal({
|
||||
command: 'OK',
|
||||
tag: '*'
|
||||
});
|
||||
|
||||
// USEATTR is from RFC6154; we are testing that just an ATOM
|
||||
// on its own will parse successfully here. (All of the
|
||||
// RFC5530 codes are also single atoms.)
|
||||
expect(imapHandler.parser('TAG1 OK [USEATTR] \\All not supported')).to.deep.equal({
|
||||
tag: 'TAG1',
|
||||
command: 'OK',
|
||||
attributes: [{
|
||||
type: 'ATOM',
|
||||
value: '',
|
||||
section: [{
|
||||
type: 'ATOM',
|
||||
value: 'USEATTR'
|
||||
}]
|
||||
}, {
|
||||
type: 'TEXT',
|
||||
value: '\\All not supported'
|
||||
}]
|
||||
});
|
||||
|
||||
// RFC5267 defines the NOUPDATE error. Including for quote /
|
||||
// string coverage.
|
||||
expect(imapHandler.parser('* NO [NOUPDATE "B02"] Too many contexts')).to.deep.equal({
|
||||
tag: '*',
|
||||
command: 'NO',
|
||||
attributes: [{
|
||||
type: 'ATOM',
|
||||
value: '',
|
||||
section: [{
|
||||
type: 'ATOM',
|
||||
value: 'NOUPDATE'
|
||||
}, {
|
||||
type: 'STRING',
|
||||
value: 'B02'
|
||||
}]
|
||||
}, {
|
||||
type: 'TEXT',
|
||||
value: 'Too many contexts'
|
||||
}]
|
||||
});
|
||||
|
||||
|
||||
// RFC5464 defines the METADATA response code; adding this to
|
||||
// ensure the transition for when '2199' hits ']' is handled
|
||||
// safely.
|
||||
expect(imapHandler.parser('TAG1 OK [METADATA LONGENTRIES 2199] GETMETADATA complete')).to.deep.equal({
|
||||
tag: 'TAG1',
|
||||
command: 'OK',
|
||||
attributes: [{
|
||||
type: 'ATOM',
|
||||
value: '',
|
||||
section: [{
|
||||
type: 'ATOM',
|
||||
value: 'METADATA'
|
||||
}, {
|
||||
type: 'ATOM',
|
||||
value: 'LONGENTRIES'
|
||||
}, {
|
||||
type: 'ATOM',
|
||||
value: '2199'
|
||||
}]
|
||||
}, {
|
||||
type: 'TEXT',
|
||||
value: 'GETMETADATA complete'
|
||||
}]
|
||||
});
|
||||
|
||||
// RFC4467 defines URLMECH. Included because of the example
|
||||
// third atom involves base64-encoding which is somewhat unusual
|
||||
expect(imapHandler.parser('TAG1 OK [URLMECH INTERNAL XSAMPLE=P34OKhO7VEkCbsiYY8rGEg==] done')).to.deep.equal({
|
||||
tag: 'TAG1',
|
||||
command: 'OK',
|
||||
attributes: [{
|
||||
type: 'ATOM',
|
||||
value: '',
|
||||
section: [{
|
||||
type: 'ATOM',
|
||||
value: 'URLMECH'
|
||||
}, {
|
||||
type: 'ATOM',
|
||||
value: 'INTERNAL'
|
||||
}, {
|
||||
type: 'ATOM',
|
||||
value: 'XSAMPLE=P34OKhO7VEkCbsiYY8rGEg=='
|
||||
}]
|
||||
}, {
|
||||
type: 'TEXT',
|
||||
value: 'done'
|
||||
}]
|
||||
});
|
||||
|
||||
// RFC2221 defines REFERRAL where the argument is an imapurl
|
||||
// (defined by RFC2192 which is obsoleted by RFC5092) which
|
||||
// is significantly more complicated than the rest of the IMAP
|
||||
// grammar and which was based on the RFC2060 grammar where
|
||||
// resp_text_code included:
|
||||
// atom [SPACE 1*<any TEXT_CHAR except ']'>]
|
||||
// So this is just a test case of our explicit special-casing
|
||||
// of REFERRAL.
|
||||
expect(imapHandler.parser('TAG1 NO [REFERRAL IMAP://user;AUTH=*@SERVER2/] Remote Server')).to.deep.equal({
|
||||
tag: 'TAG1',
|
||||
command: 'NO',
|
||||
attributes: [{
|
||||
type: 'ATOM',
|
||||
value: '',
|
||||
section: [{
|
||||
type: 'ATOM',
|
||||
value: 'REFERRAL'
|
||||
}, {
|
||||
type: 'ATOM',
|
||||
value: 'IMAP://user;AUTH=*@SERVER2/'
|
||||
}]
|
||||
}, {
|
||||
type: 'TEXT',
|
||||
value: 'Remote Server'
|
||||
}]
|
||||
});
|
||||
|
||||
// PERMANENTFLAGS is from RFC3501. Its syntax is also very
|
||||
// similar to BADCHARSET, except BADCHARSET has astrings
|
||||
// inside the list.
|
||||
expect(imapHandler.parser('* OK [PERMANENTFLAGS (de:hacking $label kt-evalution [css3-page] \\*)] Flags permitted.')).to.deep.equal({
|
||||
tag: '*',
|
||||
command: 'OK',
|
||||
attributes: [{
|
||||
type: 'ATOM',
|
||||
value: '',
|
||||
section: [
|
||||
// keep indentation
|
||||
{
|
||||
type: 'ATOM',
|
||||
value: 'PERMANENTFLAGS'
|
||||
},
|
||||
[{
|
||||
type: 'ATOM',
|
||||
value: 'de:hacking'
|
||||
}, {
|
||||
type: 'ATOM',
|
||||
value: '$label'
|
||||
}, {
|
||||
type: 'ATOM',
|
||||
value: 'kt-evalution'
|
||||
}, {
|
||||
type: 'ATOM',
|
||||
value: '[css3-page]'
|
||||
}, {
|
||||
type: 'ATOM',
|
||||
value: '\\*'
|
||||
}]
|
||||
]
|
||||
}, {
|
||||
type: 'TEXT',
|
||||
value: 'Flags permitted.'
|
||||
}]
|
||||
});
|
||||
|
||||
// COPYUID is from RFC4315 and included the previously failing
|
||||
// parsing situation of a sequence terminated by ']' rather than
|
||||
// whitespace.
|
||||
expect(imapHandler.parser('TAG1 OK [COPYUID 4 1417051618:1417051620 1421730687:1421730689] COPY completed')).to.deep.equal({
|
||||
tag: 'TAG1',
|
||||
command: 'OK',
|
||||
attributes: [{
|
||||
type: 'ATOM',
|
||||
value: '',
|
||||
section: [{
|
||||
type: 'ATOM',
|
||||
value: 'COPYUID'
|
||||
}, {
|
||||
type: 'ATOM',
|
||||
value: '4'
|
||||
}, {
|
||||
type: 'SEQUENCE',
|
||||
value: '1417051618:1417051620'
|
||||
}, {
|
||||
type: 'SEQUENCE',
|
||||
value: '1421730687:1421730689'
|
||||
}]
|
||||
}, {
|
||||
type: 'TEXT',
|
||||
value: 'COPY completed'
|
||||
}]
|
||||
});
|
||||
|
||||
// MODIFIED is from RFC4551 and is basically the same situation
|
||||
// as the COPYUID case, but in this case our example sequences
|
||||
// have commas in them. (Note that if there was no comma, the
|
||||
// '7,9' payload would end up an ATOM.)
|
||||
expect(imapHandler.parser('TAG1 OK [MODIFIED 7,9] Conditional STORE failed')).to.deep.equal({
|
||||
tag: 'TAG1',
|
||||
command: 'OK',
|
||||
attributes: [{
|
||||
type: 'ATOM',
|
||||
value: '',
|
||||
section: [{
|
||||
type: 'ATOM',
|
||||
value: 'MODIFIED'
|
||||
}, {
|
||||
type: 'SEQUENCE',
|
||||
value: '7,9'
|
||||
}]
|
||||
}, {
|
||||
type: 'TEXT',
|
||||
value: 'Conditional STORE failed'
|
||||
}]
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('ATOM Partial', function () {
|
||||
it('should succeed', function () {
|
||||
expect(imapHandler.parser('TAG1 CMD BODY[]<0>').attributes).to.deep.equal([{
|
||||
type: 'ATOM',
|
||||
value: 'BODY',
|
||||
section: [],
|
||||
partial: [0]
|
||||
}]);
|
||||
expect(imapHandler.parser('TAG1 CMD BODY[]<12.45>').attributes).to.deep.equal([{
|
||||
type: 'ATOM',
|
||||
value: 'BODY',
|
||||
section: [],
|
||||
partial: [12, 45]
|
||||
}]);
|
||||
expect(imapHandler.parser('TAG1 CMD BODY[HEADER.FIELDS (Subject From)]<12.45>').attributes).to.deep.equal([{
|
||||
type: 'ATOM',
|
||||
value: 'BODY',
|
||||
section: [
|
||||
// keep indentation
|
||||
{
|
||||
type: 'ATOM',
|
||||
value: 'HEADER.FIELDS'
|
||||
},
|
||||
[{
|
||||
type: 'ATOM',
|
||||
value: 'Subject'
|
||||
}, {
|
||||
type: 'ATOM',
|
||||
value: 'From'
|
||||
}]
|
||||
],
|
||||
partial: [12, 45]
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should fail', function () {
|
||||
expect(function () {
|
||||
imapHandler.parser('TAG1 CMD KODY<0.123>');
|
||||
}).to.throw(Error);
|
||||
|
||||
expect(function () {
|
||||
imapHandler.parser('TAG1 CMD BODY[]<01>');
|
||||
}).to.throw(Error);
|
||||
|
||||
expect(function () {
|
||||
imapHandler.parser('TAG1 CMD BODY[]<0.01>');
|
||||
}).to.throw(Error);
|
||||
|
||||
expect(function () {
|
||||
imapHandler.parser('TAG1 CMD BODY[]<0.1.>');
|
||||
}).to.throw(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SEQUENCE', function () {
|
||||
it('should succeed', function () {
|
||||
expect(imapHandler.parser('TAG1 CMD *:4,5:7 TEST').attributes).to.deep.equal([{
|
||||
type: 'SEQUENCE',
|
||||
value: '*:4,5:7'
|
||||
}, {
|
||||
type: 'ATOM',
|
||||
value: 'TEST'
|
||||
}]);
|
||||
|
||||
expect(imapHandler.parser('TAG1 CMD 1:* TEST').attributes).to.deep.equal([{
|
||||
type: 'SEQUENCE',
|
||||
value: '1:*'
|
||||
}, {
|
||||
type: 'ATOM',
|
||||
value: 'TEST'
|
||||
}]);
|
||||
|
||||
expect(imapHandler.parser('TAG1 CMD *:4 TEST').attributes).to.deep.equal([{
|
||||
type: 'SEQUENCE',
|
||||
value: '*:4'
|
||||
}, {
|
||||
type: 'ATOM',
|
||||
value: 'TEST'
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should fail', function () {
|
||||
expect(function () {
|
||||
imapHandler.parser('TAG1 CMD *:4,5:');
|
||||
}).to.throw(Error);
|
||||
|
||||
expect(function () {
|
||||
imapHandler.parser('TAG1 CMD *:4,5:TEST TEST');
|
||||
}).to.throw(Error);
|
||||
|
||||
expect(function () {
|
||||
imapHandler.parser('TAG1 CMD *:4,5: TEST');
|
||||
}).to.throw(Error);
|
||||
|
||||
expect(function () {
|
||||
imapHandler.parser('TAG1 CMD *4,5 TEST');
|
||||
}).to.throw(Error);
|
||||
|
||||
expect(function () {
|
||||
imapHandler.parser('TAG1 CMD *,5 TEST');
|
||||
}).to.throw(Error);
|
||||
|
||||
expect(function () {
|
||||
imapHandler.parser('TAG1 CMD 5,* TEST');
|
||||
}).to.throw(Error);
|
||||
|
||||
expect(function () {
|
||||
imapHandler.parser('TAG1 CMD 5, TEST');
|
||||
}).to.throw(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Escaped quotes', function () {
|
||||
it('should succeed', function () {
|
||||
expect(imapHandler.parser('* 331 FETCH (ENVELOPE ("=?ISO-8859-1?Q?\\"G=FCnter__Hammerl\\"?="))').attributes).to.deep.equal([
|
||||
// keep indentation
|
||||
{
|
||||
type: 'ATOM',
|
||||
value: 'FETCH'
|
||||
},
|
||||
[
|
||||
// keep indentation
|
||||
{
|
||||
type: 'ATOM',
|
||||
value: 'ENVELOPE'
|
||||
},
|
||||
[{
|
||||
type: 'STRING',
|
||||
value: '=?ISO-8859-1?Q?"G=FCnter__Hammerl"?='
|
||||
}]
|
||||
]
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MimeTorture', function () {
|
||||
it('should parse mimetorture input', function () {
|
||||
let parsed;
|
||||
expect(function () {
|
||||
parsed = imapHandler.parser(mimetorture.input);
|
||||
}).to.not.throw(Error);
|
||||
expect(parsed).to.deep.equal(mimetorture.output);
|
||||
});
|
||||
});
|
||||
});
|
1705
imap-core/test/protocol-test.js
Normal file
1705
imap-core/test/protocol-test.js
Normal file
File diff suppressed because it is too large
Load diff
883
imap-core/test/search-test.js
Normal file
883
imap-core/test/search-test.js
Normal file
|
@ -0,0 +1,883 @@
|
|||
/*eslint no-unused-expressions: 0, prefer-arrow-callback: 0 */
|
||||
|
||||
'use strict';
|
||||
|
||||
let parseQueryTerms = require('../lib/commands/search').parseQueryTerms;
|
||||
let matchSearchQuery = require('../lib/search').matchSearchQuery;
|
||||
let chai = require('chai');
|
||||
let expect = chai.expect;
|
||||
chai.config.includeStack = true;
|
||||
|
||||
describe('#parseQueryTerms', function () {
|
||||
let uidList = [39, 40, 44, 52, 53, 54, 59, 72];
|
||||
|
||||
describe('<sequence set>', function () {
|
||||
it('should detect sequence as first argument', function () {
|
||||
expect(parseQueryTerms('1,2,4:6'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'uid',
|
||||
value: [39, 40, 52, 53, 54]
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should detect sequence as subargument', function () {
|
||||
expect(parseQueryTerms('NOT 1,2,4:6'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'not',
|
||||
value: {
|
||||
key: 'uid',
|
||||
value: [39, 40, 52, 53, 54]
|
||||
}
|
||||
}]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle ALL', function () {
|
||||
expect(parseQueryTerms('ALL'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'all',
|
||||
value: true
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle ANSWERED', function () {
|
||||
expect(parseQueryTerms('ANSWERED'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'flag',
|
||||
value: '\\Answered',
|
||||
exists: true
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle BCC', function () {
|
||||
expect(parseQueryTerms('BCC query'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'header',
|
||||
header: 'bcc',
|
||||
value: 'query'
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle BEFORE', function () {
|
||||
expect(parseQueryTerms('BEFORE 1-Feb-1994'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'internaldate',
|
||||
operator: '<',
|
||||
value: '1-Feb-1994'
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle BODY', function () {
|
||||
expect(parseQueryTerms('BODY query'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'body',
|
||||
value: 'query'
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle CC', function () {
|
||||
expect(parseQueryTerms('CC query'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'header',
|
||||
header: 'cc',
|
||||
value: 'query'
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle DELETED', function () {
|
||||
expect(parseQueryTerms('DELETED'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'flag',
|
||||
value: '\\Deleted',
|
||||
exists: true
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle DRAFT', function () {
|
||||
expect(parseQueryTerms('DRAFT'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'flag',
|
||||
value: '\\Draft',
|
||||
exists: true
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle FLAGGED', function () {
|
||||
expect(parseQueryTerms('FLAGGED'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'flag',
|
||||
value: '\\Flagged',
|
||||
exists: true
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle FROM', function () {
|
||||
expect(parseQueryTerms('FROM query'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'header',
|
||||
header: 'from',
|
||||
value: 'query'
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle HEADER', function () {
|
||||
expect(parseQueryTerms('HEADER X-FOO query'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'header',
|
||||
header: 'x-foo',
|
||||
value: 'query'
|
||||
}]);
|
||||
|
||||
expect(parseQueryTerms(['HEADER', 'X-FOO', null], uidList).query).to.deep.equal([{
|
||||
key: 'header',
|
||||
header: 'x-foo',
|
||||
value: ''
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle KEYWORD', function () {
|
||||
expect(parseQueryTerms('KEYWORD $MyFlag'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'flag',
|
||||
value: '$MyFlag',
|
||||
exists: true
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle LARGER', function () {
|
||||
expect(parseQueryTerms('LARGER 123'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'size',
|
||||
value: 123,
|
||||
operator: '>'
|
||||
}]);
|
||||
});
|
||||
|
||||
describe('MODSEQ', function () {
|
||||
it('should handle only required param', function () {
|
||||
expect(parseQueryTerms('MODSEQ 123'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'modseq',
|
||||
value: 123
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle optional params', function () {
|
||||
expect(parseQueryTerms('MODSEQ "/flags/\\\\draft" all 123'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'modseq',
|
||||
value: 123
|
||||
}]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle NEW', function () {
|
||||
expect(parseQueryTerms('NEW'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'flag',
|
||||
value: '\\Recent',
|
||||
exists: true
|
||||
}, {
|
||||
key: 'flag',
|
||||
value: '\\Seen',
|
||||
exists: false
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle NOT', function () {
|
||||
expect(parseQueryTerms('NOT ALL'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'not',
|
||||
value: {
|
||||
key: 'all',
|
||||
value: true
|
||||
}
|
||||
}]);
|
||||
|
||||
expect(parseQueryTerms('NOT NOT ALL'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'not',
|
||||
value: {
|
||||
key: 'not',
|
||||
value: {
|
||||
key: 'all',
|
||||
value: true
|
||||
}
|
||||
}
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle OLD', function () {
|
||||
expect(parseQueryTerms('OLD'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'flag',
|
||||
value: '\\Recent',
|
||||
exists: false
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle ON', function () {
|
||||
expect(parseQueryTerms('ON 1-Feb-1994'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'internaldate',
|
||||
operator: '=',
|
||||
value: '1-Feb-1994'
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle OR', function () {
|
||||
expect(parseQueryTerms('OR ALL NOT ALL'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'or',
|
||||
value: [{
|
||||
key: 'all',
|
||||
value: true
|
||||
}, {
|
||||
key: 'not',
|
||||
value: {
|
||||
key: 'all',
|
||||
value: true
|
||||
}
|
||||
}]
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle RECENT', function () {
|
||||
expect(parseQueryTerms('RECENT'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'flag',
|
||||
value: '\\Recent',
|
||||
exists: true
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle SEEN', function () {
|
||||
expect(parseQueryTerms('SEEN'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'flag',
|
||||
value: '\\Seen',
|
||||
exists: true
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle SENTBEFORE', function () {
|
||||
expect(parseQueryTerms('SENTBEFORE 1-Feb-1994'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'date',
|
||||
operator: '<',
|
||||
value: '1-Feb-1994'
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle SENTON', function () {
|
||||
expect(parseQueryTerms('SENTON 1-Feb-1994'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'date',
|
||||
operator: '=',
|
||||
value: '1-Feb-1994'
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle SENTSINCE', function () {
|
||||
expect(parseQueryTerms('SENTSINCE 1-Feb-1994'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'date',
|
||||
operator: '>=',
|
||||
value: '1-Feb-1994'
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle SINCE', function () {
|
||||
expect(parseQueryTerms('SINCE 1-Feb-1994'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'internaldate',
|
||||
operator: '>=',
|
||||
value: '1-Feb-1994'
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle SMALLER', function () {
|
||||
expect(parseQueryTerms('SMALLER 123'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'size',
|
||||
value: 123,
|
||||
operator: '<'
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle SUBJECT', function () {
|
||||
expect(parseQueryTerms('SUBJECT query'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'header',
|
||||
header: 'subject',
|
||||
value: 'query'
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle TEXT', function () {
|
||||
expect(parseQueryTerms('TEXT query'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'text',
|
||||
value: 'query'
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle TO', function () {
|
||||
expect(parseQueryTerms('TO query'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'header',
|
||||
header: 'to',
|
||||
value: 'query'
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle UID', function () {
|
||||
expect(parseQueryTerms('UID 44,54:*'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'uid',
|
||||
value: [44, 54, 59, 72]
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle UNANSWERED', function () {
|
||||
expect(parseQueryTerms('UNANSWERED'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'flag',
|
||||
value: '\\Answered',
|
||||
exists: false
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle UNDELETED', function () {
|
||||
expect(parseQueryTerms('UNDELETED'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'flag',
|
||||
value: '\\Deleted',
|
||||
exists: false
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle UNDRAFT', function () {
|
||||
expect(parseQueryTerms('UNDRAFT'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'flag',
|
||||
value: '\\Draft',
|
||||
exists: false
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle UNFLAGGED', function () {
|
||||
expect(parseQueryTerms('UNFLAGGED'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'flag',
|
||||
value: '\\Flagged',
|
||||
exists: false
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle UNKEYWORD', function () {
|
||||
expect(parseQueryTerms('UNKEYWORD $MyFlag'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'flag',
|
||||
value: '$MyFlag',
|
||||
exists: false
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle UNSEEN', function () {
|
||||
expect(parseQueryTerms('UNSEEN'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'flag',
|
||||
value: '\\Seen',
|
||||
exists: false
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle complex query', function () {
|
||||
// this is a query by iOS Mail app
|
||||
// UID SEARCH (OR FROM "Time" (OR SUBJECT "Time" (OR TO "Time" (OR CC "Time" BODY "Time")))) NOT DELETED
|
||||
expect(parseQueryTerms('OR FROM Time OR SUBJECT Time OR TO Time OR CC Time BODY Time NOT DELETED'.split(' '), uidList).query).to.deep.equal([{
|
||||
key: 'or',
|
||||
value: [{
|
||||
key: 'header',
|
||||
header: 'from',
|
||||
value: 'Time'
|
||||
}, {
|
||||
key: 'or',
|
||||
value: [{
|
||||
key: 'header',
|
||||
header: 'subject',
|
||||
value: 'Time'
|
||||
}, {
|
||||
key: 'or',
|
||||
value: [{
|
||||
key: 'header',
|
||||
header: 'to',
|
||||
value: 'Time'
|
||||
}, {
|
||||
key: 'or',
|
||||
value: [{
|
||||
key: 'header',
|
||||
header: 'cc',
|
||||
value: 'Time'
|
||||
}, {
|
||||
key: 'body',
|
||||
value: 'Time'
|
||||
}]
|
||||
}]
|
||||
}]
|
||||
}]
|
||||
}, {
|
||||
key: 'not',
|
||||
value: {
|
||||
key: 'flag',
|
||||
value: '\\Deleted',
|
||||
exists: true
|
||||
}
|
||||
}]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Search term match tests', function () {
|
||||
describe('AND', function () {
|
||||
it('should find all matches', function () {
|
||||
expect(matchSearchQuery({}, [{
|
||||
key: 'all',
|
||||
value: true
|
||||
}, {
|
||||
key: 'all',
|
||||
value: true
|
||||
}, {
|
||||
key: 'all',
|
||||
value: true
|
||||
}, {
|
||||
key: 'all',
|
||||
value: true
|
||||
}])).to.be.true;
|
||||
});
|
||||
|
||||
it('should fail on single error', function () {
|
||||
expect(matchSearchQuery({}, [{
|
||||
key: 'all',
|
||||
value: true
|
||||
}, {
|
||||
key: 'all',
|
||||
value: true
|
||||
}, {
|
||||
key: 'all',
|
||||
value: true
|
||||
}, {
|
||||
key: 'flag',
|
||||
value: 'zzzzzzz',
|
||||
exists: true
|
||||
}])).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('OR', function () {
|
||||
it('should succeed with at least one match', function () {
|
||||
expect(matchSearchQuery({}, {
|
||||
key: 'or',
|
||||
value: [{
|
||||
key: 'flag',
|
||||
value: 'zzzzzzz',
|
||||
exists: true
|
||||
}, {
|
||||
key: 'all',
|
||||
value: true
|
||||
}, {
|
||||
key: 'flag',
|
||||
value: 'zzzzzzz',
|
||||
exists: true
|
||||
}]
|
||||
})).to.be.true;
|
||||
});
|
||||
|
||||
it('should fail with no matches', function () {
|
||||
expect(matchSearchQuery({}, {
|
||||
key: 'or',
|
||||
value: [{
|
||||
key: 'flag',
|
||||
value: 'zzzzzzz',
|
||||
exists: true
|
||||
}, {
|
||||
key: 'flag',
|
||||
value: 'zzzzzzz',
|
||||
exists: true
|
||||
}]
|
||||
})).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('NOT', function () {
|
||||
it('should succeed with false value', function () {
|
||||
expect(matchSearchQuery({}, {
|
||||
key: 'not',
|
||||
value: {
|
||||
key: 'flag',
|
||||
value: 'zzzzzzz',
|
||||
exists: true
|
||||
}
|
||||
})).to.be.true;
|
||||
});
|
||||
|
||||
it('should fail with thruthy value', function () {
|
||||
expect(matchSearchQuery({}, {
|
||||
key: 'not',
|
||||
value: {
|
||||
key: 'all',
|
||||
value: true
|
||||
}
|
||||
})).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('ALL', function () {
|
||||
it('should match ALL', function () {
|
||||
expect(matchSearchQuery({}, {
|
||||
key: 'all',
|
||||
value: true
|
||||
})).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('FLAG', function () {
|
||||
it('should match existing flag', function () {
|
||||
expect(matchSearchQuery({
|
||||
flags: ['abc', 'def', 'ghi']
|
||||
}, {
|
||||
key: 'flag',
|
||||
value: 'def',
|
||||
exists: true
|
||||
})).to.be.true;
|
||||
});
|
||||
|
||||
it('should match non-existing flag', function () {
|
||||
expect(matchSearchQuery({
|
||||
flags: ['abc', 'def', 'ghi']
|
||||
}, {
|
||||
key: 'flag',
|
||||
value: 'zzzzz',
|
||||
exists: false
|
||||
})).to.be.true;
|
||||
});
|
||||
|
||||
it('should fail non-existing flag', function () {
|
||||
expect(matchSearchQuery({
|
||||
flags: ['abc', 'def', 'ghi']
|
||||
}, {
|
||||
key: 'flag',
|
||||
value: 'zzzzz',
|
||||
exists: true
|
||||
})).to.be.false;
|
||||
});
|
||||
|
||||
it('should fail existing flag', function () {
|
||||
expect(matchSearchQuery({
|
||||
flags: ['abc', 'def', 'ghi']
|
||||
}, {
|
||||
key: 'flag',
|
||||
value: 'abc',
|
||||
exists: false
|
||||
})).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('INTERNALDATE', function () {
|
||||
it('should match <', function () {
|
||||
expect(matchSearchQuery({
|
||||
internaldate: new Date('1999-01-01')
|
||||
}, {
|
||||
key: 'internaldate',
|
||||
value: '2001-01-01',
|
||||
operator: '<'
|
||||
})).to.be.true;
|
||||
});
|
||||
|
||||
it('should not match <', function () {
|
||||
expect(matchSearchQuery({
|
||||
internaldate: new Date('1999-01-01')
|
||||
}, {
|
||||
key: 'internaldate',
|
||||
value: '1998-01-01',
|
||||
operator: '<'
|
||||
})).to.be.false;
|
||||
});
|
||||
|
||||
it('should match =', function () {
|
||||
expect(matchSearchQuery({
|
||||
internaldate: new Date('1999-01-01')
|
||||
}, {
|
||||
key: 'internaldate',
|
||||
value: '1999-01-01',
|
||||
operator: '='
|
||||
})).to.be.true;
|
||||
});
|
||||
|
||||
it('should not match <', function () {
|
||||
expect(matchSearchQuery({
|
||||
internaldate: new Date('1999-01-01')
|
||||
}, {
|
||||
key: 'internaldate',
|
||||
value: '1999-01-02',
|
||||
operator: '='
|
||||
})).to.be.false;
|
||||
});
|
||||
|
||||
it('should match >=', function () {
|
||||
expect(matchSearchQuery({
|
||||
internaldate: new Date('1999-01-01')
|
||||
}, {
|
||||
key: 'internaldate',
|
||||
value: '1999-01-01',
|
||||
operator: '>='
|
||||
})).to.be.true;
|
||||
|
||||
expect(matchSearchQuery({
|
||||
internaldate: new Date('1999-01-02')
|
||||
}, {
|
||||
key: 'internaldate',
|
||||
value: '1999-01-01',
|
||||
operator: '>='
|
||||
})).to.be.true;
|
||||
});
|
||||
|
||||
it('should not match >=', function () {
|
||||
expect(matchSearchQuery({
|
||||
internaldate: new Date('1999-01-01')
|
||||
}, {
|
||||
key: 'internaldate',
|
||||
value: '1999-01-02',
|
||||
operator: '>='
|
||||
})).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('DATE', function () {
|
||||
let raw = 'Subject: test\r\nDate: 1999-01-01\r\n\r\nHello world!';
|
||||
|
||||
it('should match <', function () {
|
||||
expect(matchSearchQuery({
|
||||
raw
|
||||
}, {
|
||||
key: 'date',
|
||||
value: '2001-01-01',
|
||||
operator: '<'
|
||||
})).to.be.true;
|
||||
});
|
||||
|
||||
it('should not match <', function () {
|
||||
expect(matchSearchQuery({
|
||||
raw
|
||||
}, {
|
||||
key: 'date',
|
||||
value: '1998-01-01',
|
||||
operator: '<'
|
||||
})).to.be.false;
|
||||
});
|
||||
|
||||
it('should match =', function () {
|
||||
expect(matchSearchQuery({
|
||||
raw
|
||||
}, {
|
||||
key: 'date',
|
||||
value: '1999-01-01',
|
||||
operator: '='
|
||||
})).to.be.true;
|
||||
});
|
||||
|
||||
it('should not match <', function () {
|
||||
expect(matchSearchQuery({
|
||||
raw
|
||||
}, {
|
||||
key: 'date',
|
||||
value: '1999-01-02',
|
||||
operator: '='
|
||||
})).to.be.false;
|
||||
});
|
||||
|
||||
it('should match >=', function () {
|
||||
expect(matchSearchQuery({
|
||||
raw
|
||||
}, {
|
||||
key: 'date',
|
||||
value: '1999-01-01',
|
||||
operator: '>='
|
||||
})).to.be.true;
|
||||
|
||||
expect(matchSearchQuery({
|
||||
raw
|
||||
}, {
|
||||
key: 'date',
|
||||
value: '1998-01-01',
|
||||
operator: '>='
|
||||
})).to.be.true;
|
||||
});
|
||||
|
||||
it('should not match >=', function () {
|
||||
expect(matchSearchQuery({
|
||||
raw
|
||||
}, {
|
||||
key: 'date',
|
||||
value: '1999-01-02',
|
||||
operator: '>='
|
||||
})).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('BODY', function () {
|
||||
let raw = 'Subject: test\r\n\r\nHello world!';
|
||||
|
||||
it('should match a string', function () {
|
||||
expect(matchSearchQuery({
|
||||
raw
|
||||
}, {
|
||||
key: 'body',
|
||||
value: 'hello'
|
||||
})).to.be.true;
|
||||
});
|
||||
|
||||
it('should not match a string', function () {
|
||||
expect(matchSearchQuery({
|
||||
raw
|
||||
}, {
|
||||
key: 'body',
|
||||
value: 'test'
|
||||
})).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('TEXT', function () {
|
||||
let raw = 'Subject: test\r\n\r\nHello world!';
|
||||
|
||||
it('should match a string', function () {
|
||||
expect(matchSearchQuery({
|
||||
raw
|
||||
}, {
|
||||
key: 'text',
|
||||
value: 'hello'
|
||||
})).to.be.true;
|
||||
|
||||
expect(matchSearchQuery({
|
||||
raw
|
||||
}, {
|
||||
key: 'text',
|
||||
value: 'test'
|
||||
})).to.be.true;
|
||||
});
|
||||
|
||||
it('should not match a string', function () {
|
||||
expect(matchSearchQuery({
|
||||
raw
|
||||
}, {
|
||||
key: 'text',
|
||||
value: 'zzzzz'
|
||||
})).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('UID', function () {
|
||||
it('should match message uid', function () {
|
||||
expect(matchSearchQuery({
|
||||
uid: 123
|
||||
}, {
|
||||
key: 'uid',
|
||||
value: [11, 123, 134]
|
||||
})).to.be.true;
|
||||
});
|
||||
|
||||
it('should not match message uid', function () {
|
||||
expect(matchSearchQuery({
|
||||
uid: 124
|
||||
}, {
|
||||
key: 'uid',
|
||||
value: [11, 123, 134]
|
||||
})).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('SIZE', function () {
|
||||
it('should match <', function () {
|
||||
expect(matchSearchQuery({
|
||||
raw: new Buffer(10)
|
||||
}, {
|
||||
key: 'size',
|
||||
value: 11,
|
||||
operator: '<'
|
||||
})).to.be.true;
|
||||
});
|
||||
|
||||
it('should not match <', function () {
|
||||
expect(matchSearchQuery({
|
||||
raw: new Buffer(10)
|
||||
}, {
|
||||
key: 'size',
|
||||
value: 9,
|
||||
operator: '<'
|
||||
})).to.be.false;
|
||||
});
|
||||
|
||||
it('should match =', function () {
|
||||
expect(matchSearchQuery({
|
||||
raw: new Buffer(10)
|
||||
}, {
|
||||
key: 'size',
|
||||
value: 10,
|
||||
operator: '='
|
||||
})).to.be.true;
|
||||
});
|
||||
|
||||
it('should not match =', function () {
|
||||
expect(matchSearchQuery({
|
||||
raw: new Buffer(10)
|
||||
}, {
|
||||
key: 'size',
|
||||
value: 11,
|
||||
operator: '='
|
||||
})).to.be.false;
|
||||
});
|
||||
|
||||
it('should match >', function () {
|
||||
expect(matchSearchQuery({
|
||||
raw: new Buffer(10)
|
||||
}, {
|
||||
key: 'size',
|
||||
value: 9,
|
||||
operator: '>'
|
||||
})).to.be.true;
|
||||
});
|
||||
|
||||
it('should not match <', function () {
|
||||
expect(matchSearchQuery({
|
||||
raw: new Buffer(10)
|
||||
}, {
|
||||
key: 'size',
|
||||
value: 11,
|
||||
operator: '>'
|
||||
})).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('header', function () {
|
||||
let raw = 'Subject: test\r\n\r\nHello world!';
|
||||
|
||||
it('should match header value', function () {
|
||||
expect(matchSearchQuery({
|
||||
raw
|
||||
}, {
|
||||
key: 'header',
|
||||
value: 'test',
|
||||
header: 'subject'
|
||||
})).to.be.true;
|
||||
});
|
||||
|
||||
it('should match empty header value', function () {
|
||||
expect(matchSearchQuery({
|
||||
raw
|
||||
}, {
|
||||
key: 'header',
|
||||
value: '',
|
||||
header: 'subject'
|
||||
})).to.be.true;
|
||||
});
|
||||
|
||||
it('should not match header value', function () {
|
||||
expect(matchSearchQuery({
|
||||
raw
|
||||
}, {
|
||||
key: 'header',
|
||||
value: 'tests',
|
||||
header: 'subject'
|
||||
})).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('MODSEQ', function () {
|
||||
it('should match equal modseq', function () {
|
||||
expect(matchSearchQuery({
|
||||
modseq: 500
|
||||
}, {
|
||||
key: 'modseq',
|
||||
value: [500]
|
||||
})).to.be.true;
|
||||
});
|
||||
|
||||
it('should match greater modseq', function () {
|
||||
expect(matchSearchQuery({
|
||||
modseq: 1000
|
||||
}, {
|
||||
key: 'modseq',
|
||||
value: [500]
|
||||
})).to.be.true;
|
||||
});
|
||||
|
||||
it('should not match lesser modseq', function () {
|
||||
expect(matchSearchQuery({
|
||||
modseq: 500
|
||||
}, {
|
||||
key: 'modseq',
|
||||
value: [100]
|
||||
})).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
142
imap-core/test/test-client.js
Normal file
142
imap-core/test/test-client.js
Normal file
|
@ -0,0 +1,142 @@
|
|||
/*eslint no-console: 0 */
|
||||
|
||||
'use strict';
|
||||
|
||||
let net = require('net');
|
||||
let tls = require('tls');
|
||||
|
||||
module.exports = runClientMockup;
|
||||
|
||||
function runClientMockup(options, callback) {
|
||||
options = options || {};
|
||||
|
||||
let host = options.host || 'localhost';
|
||||
let port = options.port || 25;
|
||||
let commands = [].concat(options.commands || []);
|
||||
let debug = options.debug;
|
||||
|
||||
let ignore_data = false;
|
||||
let responses = [];
|
||||
let command = '';
|
||||
let callbackSent = false;
|
||||
let delay;
|
||||
|
||||
let socket = (options.secure ? tls : net).connect({
|
||||
rejectUnauthorized: false,
|
||||
port,
|
||||
host
|
||||
}, () => {
|
||||
socket.on('close', () => {
|
||||
if (callbackSent) {
|
||||
return;
|
||||
}
|
||||
callbackSent = true;
|
||||
if (typeof callback === 'function') {
|
||||
return callback(Buffer.concat(responses));
|
||||
}
|
||||
});
|
||||
|
||||
let onData = function (chunk) {
|
||||
if (ignore_data) {
|
||||
return;
|
||||
}
|
||||
|
||||
responses.push(chunk);
|
||||
if (debug) {
|
||||
console.log('S: ' + chunk.toString('binary').trim());
|
||||
}
|
||||
|
||||
if (!commands.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof command === 'string' && command.match(/^[a-z0-9]+ STARTTLS$/i)) {
|
||||
// wait until server sends response to the STARTTLS command
|
||||
if (!/STARTTLS completed/.test(Buffer.concat(responses).toString())) {
|
||||
return;
|
||||
}
|
||||
|
||||
ignore_data = true;
|
||||
if (debug) {
|
||||
console.log('Initiated TLS connection');
|
||||
}
|
||||
|
||||
socket.removeAllListeners('data');
|
||||
let secureSocket = tls.connect({
|
||||
rejectUnauthorized: false,
|
||||
socket,
|
||||
host
|
||||
}, () => {
|
||||
ignore_data = false;
|
||||
|
||||
socket = secureSocket;
|
||||
|
||||
if (debug) {
|
||||
console.log('TLS connection secured');
|
||||
}
|
||||
|
||||
secureSocket.on('data', onData);
|
||||
|
||||
secureSocket.on('close', () => {
|
||||
if (callbackSent) {
|
||||
return;
|
||||
}
|
||||
callbackSent = true;
|
||||
if (typeof callback === 'function') {
|
||||
return callback(Buffer.concat(responses));
|
||||
}
|
||||
});
|
||||
|
||||
command = commands.shift();
|
||||
if (debug) {
|
||||
console.log('(Secure) C: ' + command);
|
||||
}
|
||||
secureSocket.write(command + '\r\n');
|
||||
});
|
||||
|
||||
secureSocket.on('error', err => {
|
||||
console.log('SECURE ERR');
|
||||
console.log(err.stack);
|
||||
});
|
||||
|
||||
} else {
|
||||
if (!/\r?\n$/.test(chunk.toString('binary'))) {
|
||||
return;
|
||||
}
|
||||
// only go forward with the next command if the last data ends with a newline
|
||||
// and there is no activity in the socket for 10ms
|
||||
clearTimeout(delay);
|
||||
delay = setTimeout(() => {
|
||||
command = commands.shift();
|
||||
|
||||
if (Array.isArray(command)) {
|
||||
let i = 0;
|
||||
let send = function () {
|
||||
if (i >= command.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let part = command[i++];
|
||||
|
||||
socket.write(new Buffer(part + (i >= command.length ? '\r\n' : ''), 'binary'));
|
||||
if (debug) {
|
||||
console.log('C: ' + part);
|
||||
}
|
||||
setTimeout(send, 10);
|
||||
};
|
||||
|
||||
send();
|
||||
} else {
|
||||
socket.write(new Buffer(command + '\r\n', 'binary'));
|
||||
if (debug) {
|
||||
console.log('C: ' + command);
|
||||
}
|
||||
}
|
||||
|
||||
}, 10);
|
||||
}
|
||||
};
|
||||
|
||||
socket.on('data', onData);
|
||||
});
|
||||
}
|
583
imap-core/test/test-server.js
Normal file
583
imap-core/test/test-server.js
Normal file
|
@ -0,0 +1,583 @@
|
|||
'use strict';
|
||||
|
||||
let IMAPServerModule = require('../index.js');
|
||||
let IMAPServer = IMAPServerModule.IMAPServer;
|
||||
let MemoryNotifier = IMAPServerModule.MemoryNotifier;
|
||||
let fs = require('fs');
|
||||
let imapHandler = require('../lib/handler/imap-handler');
|
||||
|
||||
module.exports = function (options) {
|
||||
|
||||
// This example uses global folders and subscriptions
|
||||
let folders = new Map();
|
||||
let subscriptions = new WeakSet();
|
||||
|
||||
[{
|
||||
path: 'INBOX',
|
||||
uidValidity: 123,
|
||||
uidNext: 70,
|
||||
modifyIndex: 5000,
|
||||
messages: [{
|
||||
uid: 45,
|
||||
flags: [],
|
||||
modseq: 100,
|
||||
internaldate: new Date('14-Sep-2013 21:22:28 -0300'),
|
||||
raw: new Buffer('from: sender@example.com\r\nto: to@example.com\r\ncc: cc@example.com\r\nsubject: test\r\n\r\nzzzz')
|
||||
}, {
|
||||
uid: 49,
|
||||
flags: ['\\Seen'],
|
||||
internaldate: new Date(),
|
||||
modseq: 5000,
|
||||
raw: fs.readFileSync(__dirname + '/fixtures/ryan_finnie_mime_torture.eml')
|
||||
}, {
|
||||
uid: 50,
|
||||
flags: ['\\Seen'],
|
||||
modseq: 45,
|
||||
internaldate: new Date(),
|
||||
raw: 'MIME-Version: 1.0\r\n' +
|
||||
'From: andris@kreata.ee\r\n' +
|
||||
'To: andris@tr.ee\r\n' +
|
||||
'Content-Type: multipart/mixed;\r\n' +
|
||||
' boundary=\'----mailcomposer-?=_1-1328088797399\'\r\n' +
|
||||
'Message-Id: <testmessage-for-bug>;\r\n' +
|
||||
'\r\n' +
|
||||
'------mailcomposer-?=_1-1328088797399\r\n' +
|
||||
'Content-Type: message/rfc822\r\n' +
|
||||
'Content-Transfer-Encoding: 7bit\r\n' +
|
||||
'\r\n' +
|
||||
'MIME-Version: 1.0\r\n' +
|
||||
'From: andris@kreata.ee\r\n' +
|
||||
'To: andris@pangalink.net\r\n' +
|
||||
'In-Reply-To: <test1>\r\n' +
|
||||
'\r\n' +
|
||||
'Hello world 1!\r\n' +
|
||||
'------mailcomposer-?=_1-1328088797399\r\n' +
|
||||
'Content-Type: message/rfc822\r\n' +
|
||||
'Content-Transfer-Encoding: 7bit\r\n' +
|
||||
'\r\n' +
|
||||
'MIME-Version: 1.0\r\n' +
|
||||
'From: andris@kreata.ee\r\n' +
|
||||
'To: andris@pangalink.net\r\n' +
|
||||
'\r\n' +
|
||||
'Hello world 2!\r\n' +
|
||||
'------mailcomposer-?=_1-1328088797399\r\n' +
|
||||
'Content-Type: text/html; charset=utf-8\r\n' +
|
||||
'Content-Transfer-Encoding: quoted-printable\r\n' +
|
||||
'\r\n' +
|
||||
'<b>Hello world 3!</b>\r\n' +
|
||||
'------mailcomposer-?=_1-1328088797399--'
|
||||
}, {
|
||||
uid: 52,
|
||||
flags: [],
|
||||
modseq: 4,
|
||||
internaldate: new Date(),
|
||||
raw: 'from: sender@example.com\r\nto: to@example.com\r\ncc: cc@example.com\r\nsubject: test\r\n\r\nHello World!'
|
||||
}, {
|
||||
uid: 53,
|
||||
flags: [],
|
||||
modseq: 5,
|
||||
internaldate: new Date()
|
||||
}, {
|
||||
uid: 60,
|
||||
flags: [],
|
||||
modseq: 6,
|
||||
internaldate: new Date()
|
||||
}],
|
||||
journal: []
|
||||
}, {
|
||||
path: '[Gmail]/Sent Mail',
|
||||
specialUse: '\\Sent',
|
||||
uidValidity: 123,
|
||||
uidNext: 90,
|
||||
modifyIndex: 1,
|
||||
messages: [],
|
||||
journal: []
|
||||
}].forEach(folder => {
|
||||
folders.set(folder.path, folder);
|
||||
subscriptions.add(folder);
|
||||
});
|
||||
|
||||
// Setup server
|
||||
let server = new IMAPServer(options);
|
||||
server.notifier = new MemoryNotifier({
|
||||
logger: {
|
||||
info: () => false,
|
||||
debug: () => false,
|
||||
error: () => false
|
||||
},
|
||||
folders
|
||||
});
|
||||
|
||||
server.on('error', err => {
|
||||
console.log('SERVER ERR\n%s', err.stack); // eslint-disable-line no-console
|
||||
});
|
||||
|
||||
server.onAuth = function (login, session, callback) {
|
||||
if (login.username !== 'testuser' || login.password !== 'pass') {
|
||||
return callback();
|
||||
}
|
||||
|
||||
callback(null, {
|
||||
user: {
|
||||
username: login.username
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// LIST "" "*"
|
||||
// Returns all folders, query is informational
|
||||
// folders is either an Array or a Map
|
||||
server.onList = function (query, session, callback) {
|
||||
this.logger.debug('[%s] LIST for "%s"', session.id, query);
|
||||
|
||||
callback(null, folders);
|
||||
};
|
||||
|
||||
// LSUB "" "*"
|
||||
// Returns all subscribed folders, query is informational
|
||||
// folders is either an Array or a Map
|
||||
server.onLsub = function (query, session, callback) {
|
||||
this.logger.debug('[%s] LSUB for "%s"', session.id, query);
|
||||
|
||||
let subscribed = [];
|
||||
folders.forEach(folder => {
|
||||
if (subscriptions.has(folder)) {
|
||||
subscribed.push(folder);
|
||||
}
|
||||
});
|
||||
|
||||
callback(null, subscribed);
|
||||
};
|
||||
|
||||
// SUBSCRIBE "path/to/mailbox"
|
||||
server.onSubscribe = function (mailbox, session, callback) {
|
||||
this.logger.debug('[%s] SUBSCRIBE to "%s"', session.id, mailbox);
|
||||
|
||||
if (!folders.has(mailbox)) {
|
||||
return callback(null, 'NONEXISTENT');
|
||||
}
|
||||
|
||||
subscriptions.add(folders.get(mailbox));
|
||||
callback(null, true);
|
||||
};
|
||||
|
||||
// UNSUBSCRIBE "path/to/mailbox"
|
||||
server.onUnsubscribe = function (mailbox, session, callback) {
|
||||
this.logger.debug('[%s] UNSUBSCRIBE from "%s"', session.id, mailbox);
|
||||
|
||||
if (!folders.has(mailbox)) {
|
||||
return callback(null, 'NONEXISTENT');
|
||||
}
|
||||
|
||||
subscriptions.delete(folders.get(mailbox));
|
||||
callback(null, true);
|
||||
};
|
||||
|
||||
// CREATE "path/to/mailbox"
|
||||
server.onCreate = function (mailbox, session, callback) {
|
||||
this.logger.debug('[%s] CREATE "%s"', session.id, mailbox);
|
||||
|
||||
if (folders.has(mailbox)) {
|
||||
return callback(null, 'ALREADYEXISTS');
|
||||
}
|
||||
|
||||
folders.set(mailbox, {
|
||||
path: mailbox,
|
||||
uidValidity: Date.now(),
|
||||
uidNext: 1,
|
||||
modifyIndex: 0,
|
||||
messages: [],
|
||||
journal: []
|
||||
});
|
||||
|
||||
subscriptions.add(folders.get(mailbox));
|
||||
callback(null, true);
|
||||
};
|
||||
|
||||
// RENAME "path/to/mailbox" "new/path"
|
||||
// NB! RENAME affects child and hierarchy mailboxes as well, this example does not do this
|
||||
server.onRename = function (mailbox, newname, session, callback) {
|
||||
this.logger.debug('[%s] RENAME "%s" to "%s"', session.id, mailbox, newname);
|
||||
|
||||
if (!folders.has(mailbox)) {
|
||||
return callback(null, 'NONEXISTENT');
|
||||
}
|
||||
|
||||
if (folders.has(newname)) {
|
||||
return callback(null, 'ALREADYEXISTS');
|
||||
}
|
||||
|
||||
let oldMailbox = folders.get(mailbox);
|
||||
folders.delete(mailbox);
|
||||
|
||||
oldMailbox.path = newname;
|
||||
folders.set(newname, oldMailbox);
|
||||
|
||||
callback(null, true);
|
||||
};
|
||||
|
||||
// DELETE "path/to/mailbox"
|
||||
server.onDelete = function (mailbox, session, callback) {
|
||||
this.logger.debug('[%s] DELETE "%s"', session.id, mailbox);
|
||||
|
||||
if (!folders.has(mailbox)) {
|
||||
return callback(null, 'NONEXISTENT');
|
||||
}
|
||||
|
||||
// keep SPECIAL-USE folders
|
||||
if (folders.get(mailbox).specialUse) {
|
||||
return callback(null, 'CANNOT');
|
||||
}
|
||||
|
||||
folders.delete(mailbox);
|
||||
callback(null, true);
|
||||
};
|
||||
|
||||
// SELECT/EXAMINE
|
||||
server.onOpen = function (mailbox, session, callback) {
|
||||
this.logger.debug('[%s] Opening "%s"', session.id, mailbox);
|
||||
|
||||
if (!folders.has(mailbox)) {
|
||||
return callback(null, 'NONEXISTENT');
|
||||
}
|
||||
|
||||
let folder = folders.get(mailbox);
|
||||
|
||||
return callback(null, {
|
||||
specialUse: folder.specialUse,
|
||||
uidValidity: folder.uidValidity,
|
||||
uidNext: folder.uidNext,
|
||||
modifyIndex: folder.modifyIndex,
|
||||
uidList: folder.messages.map(message => message.uid)
|
||||
});
|
||||
};
|
||||
|
||||
// STATUS (X Y X)
|
||||
server.onStatus = function (mailbox, session, callback) {
|
||||
this.logger.debug('[%s] Requested status for "%s"', session.id, mailbox);
|
||||
|
||||
if (!folders.has(mailbox)) {
|
||||
return callback(null, 'NONEXISTENT');
|
||||
}
|
||||
|
||||
let folder = folders.get(mailbox);
|
||||
|
||||
return callback(null, {
|
||||
messages: folder.messages.length,
|
||||
uidNext: folder.uidNext,
|
||||
uidValidity: folder.uidValidity,
|
||||
highestModseq: folder.modifyIndex,
|
||||
unseen: folder.messages.filter(message => message.flags.indexOf('\\Seen') < 0).length
|
||||
});
|
||||
};
|
||||
|
||||
// APPEND mailbox (flags) date message
|
||||
server.onAppend = function (mailbox, flags, date, raw, session, callback) {
|
||||
this.logger.debug('[%s] Appending message to "%s"', session.id, mailbox);
|
||||
|
||||
if (!folders.has(mailbox)) {
|
||||
return callback(null, 'TRYCREATE');
|
||||
}
|
||||
|
||||
date = date && new Date(date) || new Date();
|
||||
|
||||
let folder = folders.get(mailbox);
|
||||
let message = {
|
||||
uid: folder.uidNext++,
|
||||
modseq: ++folder.modifyIndex,
|
||||
date: date && new Date(date) || new Date(),
|
||||
raw,
|
||||
flags
|
||||
};
|
||||
|
||||
folder.messages.push(message);
|
||||
|
||||
// do not write directly to stream, use notifications as the currently selected mailbox might not be the one that receives the message
|
||||
this.notifier.addEntries(session.user.username, mailbox, {
|
||||
command: 'EXISTS',
|
||||
uid: message.uid
|
||||
}, () => {
|
||||
this.notifier.fire(session.user.username, mailbox);
|
||||
|
||||
return callback(null, true, {
|
||||
uidValidity: folder.uidValidity,
|
||||
uid: message.uid
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// STORE / UID STORE, updates flags for selected UIDs
|
||||
server.onUpdate = function (mailbox, update, session, callback) {
|
||||
this.logger.debug('[%s] Updating messages in "%s"', session.id, mailbox);
|
||||
|
||||
if (!folders.has(mailbox)) {
|
||||
return callback(null, 'NONEXISTENT');
|
||||
}
|
||||
|
||||
let condstoreEnabled = !!session.selected.condstoreEnabled;
|
||||
|
||||
let modified = [];
|
||||
let folder = folders.get(mailbox);
|
||||
let i = 0;
|
||||
|
||||
let processMessages = () => {
|
||||
if (i >= folder.messages.length) {
|
||||
this.notifier.fire(session.user.username, mailbox);
|
||||
return callback(null, true, modified);
|
||||
}
|
||||
|
||||
let message = folder.messages[i++];
|
||||
let updated = false;
|
||||
|
||||
if (update.messages.indexOf(message.uid) < 0) {
|
||||
return processMessages();
|
||||
}
|
||||
|
||||
if (update.unchangedSince && message.modseq > update.unchangedSince) {
|
||||
modified.push(message.uid);
|
||||
return processMessages();
|
||||
}
|
||||
|
||||
switch (update.action) {
|
||||
case 'set':
|
||||
// check if update set matches current or is different
|
||||
if (message.flags.length !== update.value.length ||
|
||||
update.value.filter(flag => message.flags.indexOf(flag) < 0).length) {
|
||||
updated = true;
|
||||
}
|
||||
// set flags
|
||||
message.flags = [].concat(update.value);
|
||||
break;
|
||||
|
||||
case 'add':
|
||||
message.flags = message.flags.concat(update.value.filter(flag => {
|
||||
if (message.flags.indexOf(flag) < 0) {
|
||||
updated = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}));
|
||||
break;
|
||||
|
||||
case 'remove':
|
||||
message.flags = message.flags.filter(flag => {
|
||||
if (update.value.indexOf(flag) < 0) {
|
||||
return true;
|
||||
}
|
||||
updated = true;
|
||||
return false;
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// notifiy only if something changed
|
||||
if (updated) {
|
||||
message.modseq = ++folder.modifyIndex;
|
||||
|
||||
// Only show response if not silent or modseq is required
|
||||
if (!update.silent || condstoreEnabled) {
|
||||
session.writeStream.write(session.formatResponse('FETCH', message.uid, {
|
||||
uid: update.isUid ? message.uid : false,
|
||||
flags: update.silent ? false : message.flags,
|
||||
modseq: condstoreEnabled ? message.modseq : false
|
||||
}));
|
||||
}
|
||||
|
||||
this.notifier.addEntries(session.user.username, mailbox, {
|
||||
command: 'FETCH',
|
||||
ignore: session.id,
|
||||
uid: message.uid,
|
||||
flags: message.flags
|
||||
}, processMessages);
|
||||
} else {
|
||||
processMessages();
|
||||
}
|
||||
};
|
||||
|
||||
processMessages();
|
||||
};
|
||||
|
||||
// EXPUNGE deletes all messages in selected mailbox marked with \Delete
|
||||
server.onExpunge = function (mailbox, update, session, callback) {
|
||||
this.logger.debug('[%s] Deleting messages from "%s"', session.id, mailbox);
|
||||
|
||||
if (!folders.has(mailbox)) {
|
||||
return callback(null, 'NONEXISTENT');
|
||||
}
|
||||
|
||||
let folder = folders.get(mailbox);
|
||||
let deleted = [];
|
||||
let i, len;
|
||||
|
||||
for (i = folder.messages.length - 1; i >= 0; i--) {
|
||||
if (
|
||||
(
|
||||
(update.isUid && update.messages.indexOf(folder.messages[i].uid) >= 0) ||
|
||||
!update.isUid
|
||||
) && folder.messages[i].flags.indexOf('\\Deleted') >= 0) {
|
||||
|
||||
deleted.unshift(folder.messages[i].uid);
|
||||
folder.messages.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
let entries = [];
|
||||
for (i = 0, len = deleted.length; i < len; i++) {
|
||||
entries.push({
|
||||
command: 'EXPUNGE',
|
||||
ignore: session.id,
|
||||
uid: deleted[i]
|
||||
});
|
||||
if (!update.silent) {
|
||||
session.writeStream.write(session.formatResponse('EXPUNGE', deleted[i]));
|
||||
}
|
||||
}
|
||||
|
||||
this.notifier.addEntries(session.user.username, mailbox, entries, () => {
|
||||
this.notifier.fire(session.user.username, mailbox);
|
||||
return callback(null, true);
|
||||
});
|
||||
};
|
||||
|
||||
// COPY / UID COPY sequence mailbox
|
||||
server.onCopy = function (mailbox, update, session, callback) {
|
||||
this.logger.debug('[%s] Copying messages from "%s" to "%s"', session.id, mailbox, update.destination);
|
||||
|
||||
if (!folders.has(mailbox)) {
|
||||
return callback(null, 'NONEXISTENT');
|
||||
}
|
||||
|
||||
if (!folders.has(update.destination)) {
|
||||
return callback(null, 'TRYCREATE');
|
||||
}
|
||||
|
||||
let sourceFolder = folders.get(mailbox);
|
||||
let destinationFolder = folders.get(update.destination);
|
||||
|
||||
let messages = [];
|
||||
let sourceUid = [];
|
||||
let destinationUid = [];
|
||||
let i, len;
|
||||
let entries = [];
|
||||
|
||||
for (i = sourceFolder.messages.length - 1; i >= 0; i--) {
|
||||
if (update.messages.indexOf(sourceFolder.messages[i].uid) >= 0) {
|
||||
messages.unshift(JSON.parse(JSON.stringify(sourceFolder.messages[i])));
|
||||
sourceUid.unshift(sourceFolder.messages[i].uid);
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0, len = messages.length; i < len; i++) {
|
||||
messages[i].uid = destinationFolder.uidNext++;
|
||||
destinationUid.push(messages[i].uid);
|
||||
destinationFolder.messages.push(messages[i]);
|
||||
|
||||
// do not write directly to stream, use notifications as the currently selected mailbox might not be the one that receives the message
|
||||
entries.push({
|
||||
command: 'EXISTS',
|
||||
uid: messages[i].uid
|
||||
});
|
||||
}
|
||||
|
||||
this.notifier.addEntries(update.destination, session.user.username, entries, () => {
|
||||
this.notifier.fire(update.destination, session.user.username);
|
||||
|
||||
return callback(null, true, {
|
||||
uidValidity: destinationFolder.uidValidity,
|
||||
sourceUid,
|
||||
destinationUid
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// sends results to socket
|
||||
server.onFetch = function (mailbox, options, session, callback) {
|
||||
this.logger.debug('[%s] Requested FETCH for "%s"', session.id, mailbox);
|
||||
this.logger.debug('[%s] FETCH: %s', session.id, JSON.stringify(options.query));
|
||||
if (!folders.has(mailbox)) {
|
||||
return callback(null, 'NONEXISTENT');
|
||||
}
|
||||
|
||||
let folder = folders.get(mailbox);
|
||||
let entries = [];
|
||||
|
||||
if (options.markAsSeen) {
|
||||
// mark all matching messages as seen
|
||||
folder.messages.forEach(message => {
|
||||
if (options.messages.indexOf(message.uid) < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if BODY[] is touched, then add \Seen flag and notify other clients
|
||||
if (message.flags.indexOf('\\Seen') < 0) {
|
||||
message.flags.unshift('\\Seen');
|
||||
entries.push({
|
||||
command: 'FETCH',
|
||||
ignore: session.id,
|
||||
uid: message.uid,
|
||||
flags: message.flags
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
this.notifier.addEntries(session.user.username, mailbox, entries, () => {
|
||||
let pos = 0;
|
||||
let processMessage = () => {
|
||||
if (pos >= folder.messages.length) {
|
||||
// once messages are processed show relevant updates
|
||||
this.notifier.fire(session.user.username, mailbox);
|
||||
return callback(null, true);
|
||||
}
|
||||
let message = folder.messages[pos++];
|
||||
|
||||
if (options.messages.indexOf(message.uid) < 0) {
|
||||
return setImmediate(processMessage);
|
||||
}
|
||||
|
||||
if (options.changedSince && message.modseq <= options.changedSince) {
|
||||
return setImmediate(processMessage);
|
||||
}
|
||||
|
||||
let stream = imapHandler.compileStream(session.formatResponse('FETCH', message.uid, {
|
||||
query: options.query,
|
||||
values: session.getQueryResponse(options.query, message)
|
||||
}));
|
||||
|
||||
// send formatted response to socket
|
||||
session.writeStream.write(stream, () => {
|
||||
setImmediate(processMessage);
|
||||
});
|
||||
};
|
||||
|
||||
setImmediate(processMessage);
|
||||
});
|
||||
};
|
||||
|
||||
// returns an array of matching UID values and the highest modseq of matching messages
|
||||
server.onSearch = function (mailbox, options, session, callback) {
|
||||
if (!folders.has(mailbox)) {
|
||||
return callback(null, 'NONEXISTENT');
|
||||
}
|
||||
|
||||
let folder = folders.get(mailbox);
|
||||
let highestModseq = 0;
|
||||
|
||||
let uidList = folder.messages.filter(message => {
|
||||
let match = session.matchSearchQuery(message, options.query);
|
||||
if (match && highestModseq < message.modseq) {
|
||||
highestModseq = message.modseq;
|
||||
}
|
||||
return match;
|
||||
}).map(message => message.uid);
|
||||
|
||||
callback(null, {
|
||||
uidList,
|
||||
highestModseq
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
return server;
|
||||
};
|
22
imap-core/test/tools-test.js
Normal file
22
imap-core/test/tools-test.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*eslint no-unused-expressions: 0, prefer-arrow-callback: 0 */
|
||||
|
||||
'use strict';
|
||||
|
||||
let imapTools = require('../lib/imap-tools');
|
||||
let chai = require('chai');
|
||||
let expect = chai.expect;
|
||||
chai.config.includeStack = true;
|
||||
|
||||
describe('#packMessageRange', function () {
|
||||
it('should return as is', function () {
|
||||
expect(imapTools.packMessageRange([1, 3, 5, 9])).to.equal('1,3,5,9');
|
||||
});
|
||||
|
||||
it('should return a range', function () {
|
||||
expect(imapTools.packMessageRange([1, 2, 3, 4])).to.equal('1:4');
|
||||
});
|
||||
|
||||
it('should return mixed ranges', function () {
|
||||
expect(imapTools.packMessageRange([1, 3, 4, 6, 8, 9, 10, 11, 13])).to.equal('1,3:4,6,8:11,13');
|
||||
});
|
||||
});
|
196
imap-notifier.js
Normal file
196
imap-notifier.js
Normal file
|
@ -0,0 +1,196 @@
|
|||
'use strict';
|
||||
|
||||
const crypto = require('crypto');
|
||||
const EventEmitter = require('events').EventEmitter;
|
||||
|
||||
class ImapNotifier extends EventEmitter {
|
||||
|
||||
constructor(options) {
|
||||
super();
|
||||
|
||||
this.database = options.database;
|
||||
|
||||
let logfunc = (...args) => {
|
||||
let level = args.shift() || 'DEBUG';
|
||||
let message = args.shift() || '';
|
||||
|
||||
console.log([level].concat(message || '').join(' '), ...args); // eslint-disable-line no-console
|
||||
};
|
||||
|
||||
this.logger = options.logger || {
|
||||
info: logfunc.bind(null, 'INFO'),
|
||||
debug: logfunc.bind(null, 'DEBUG'),
|
||||
error: logfunc.bind(null, 'ERROR')
|
||||
};
|
||||
|
||||
this._listeners = new EventEmitter();
|
||||
this._listeners.setMaxListeners(0);
|
||||
|
||||
EventEmitter.call(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates hashed event names for mailbox:username pairs
|
||||
*
|
||||
* @param {String} path
|
||||
* @param {String} username
|
||||
* @returns {String} md5 hex
|
||||
*/
|
||||
_eventName(path, username) {
|
||||
return crypto.createHash('md5').update(username + ':' + path).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an event handler for path:username events
|
||||
*
|
||||
* @param {String} username
|
||||
* @param {String} path
|
||||
* @param {Function} handler Function to run once there are new entries in the journal
|
||||
*/
|
||||
addListener(session, path, handler) {
|
||||
let eventName = this._eventName(session.user.username, path);
|
||||
this._listeners.addListener(eventName, handler);
|
||||
|
||||
this.logger.debug('New journal listener for %s ("%s:%s")', eventName, session.user.username, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters an event handler for path:username events
|
||||
*
|
||||
* @param {String} username
|
||||
* @param {String} path
|
||||
* @param {Function} handler Function to run once there are new entries in the journal
|
||||
*/
|
||||
removeListener(session, path, handler) {
|
||||
let eventName = this._eventName(session.user.username, path);
|
||||
this._listeners.removeListener(eventName, handler);
|
||||
|
||||
this.logger.debug('Removed journal listener from %s ("%s:%s")', eventName, session.user.username, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores multiple journal entries to db
|
||||
*
|
||||
* @param {String} username
|
||||
* @param {String} path
|
||||
* @param {Array|Object} entries An array of entries to be journaled
|
||||
* @param {Function} callback Runs once the entry is either stored or an error occurred
|
||||
*/
|
||||
addEntries(username, path, entries, callback) {
|
||||
if (entries && !Array.isArray(entries)) {
|
||||
entries = [entries];
|
||||
} else if (!entries || !entries.length) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
this.database.collection('mailboxes').findOneAndUpdate({
|
||||
username,
|
||||
path
|
||||
}, {
|
||||
$inc: {
|
||||
modifyIndex: entries.length
|
||||
}
|
||||
}, {}, (err, item) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!item || !item.value) {
|
||||
// was not able to acquire a lock
|
||||
return callback(null, new Error('Selected mailbox does not exist'));
|
||||
}
|
||||
|
||||
let mailbox = item.value;
|
||||
let startIndex = mailbox.modifyIndex;
|
||||
|
||||
let updated = 0;
|
||||
let updateNext = () => {
|
||||
if (updated >= entries.length) {
|
||||
return this.database.collection('journal').insertMany(entries, {
|
||||
w: 1,
|
||||
ordered: false
|
||||
}, (err, r) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, r.insertedCount);
|
||||
});
|
||||
}
|
||||
|
||||
let entry = entries[updated++];
|
||||
|
||||
entry.mailbox = mailbox._id;
|
||||
entry.modseq = ++startIndex;
|
||||
|
||||
if (entry.message) {
|
||||
this.database.collection('messages').findOneAndUpdate({
|
||||
_id: entry.message,
|
||||
modseq: {
|
||||
$lt: entry.modseq
|
||||
}
|
||||
}, {
|
||||
$set: {
|
||||
modseq: entry.modseq
|
||||
}
|
||||
}, {}, err => {
|
||||
if (err) {
|
||||
this.logger.error('Error updating modseq for message %s. %s', entry.message, err.message);
|
||||
}
|
||||
updateNext();
|
||||
});
|
||||
} else {
|
||||
updateNext();
|
||||
}
|
||||
};
|
||||
|
||||
updateNext();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a notification that there are new updates in the selected mailbox
|
||||
*
|
||||
* @param {String} username
|
||||
* @param {String} path
|
||||
*/
|
||||
fire(username, path, payload) {
|
||||
let eventName = this._eventName(username, path);
|
||||
setImmediate(() => {
|
||||
this._listeners.emit(eventName, payload);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all entries from the journal that have higher than provided modification index
|
||||
*
|
||||
* @param {String} session
|
||||
* @param {String} path
|
||||
* @param {Number} modifyIndex Last known modification id
|
||||
* @param {Function} callback Returns update entries as an array
|
||||
*/
|
||||
getUpdates(session, path, modifyIndex, callback) {
|
||||
modifyIndex = Number(modifyIndex) || 0;
|
||||
let username = session.user.username;
|
||||
|
||||
this.database.collection('mailboxes').findOne({
|
||||
username,
|
||||
path
|
||||
}, (err, mailbox) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!mailbox) {
|
||||
return callback(null, 'NONEXISTENT');
|
||||
}
|
||||
this.database.collection('journal').find({
|
||||
mailbox: mailbox._id,
|
||||
modseq: {
|
||||
$gt: modifyIndex
|
||||
}
|
||||
}).toArray(callback);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = ImapNotifier;
|
66
indexes.js
Normal file
66
indexes.js
Normal file
|
@ -0,0 +1,66 @@
|
|||
/* global db */
|
||||
'use strict';
|
||||
|
||||
db.users.createIndex({
|
||||
username: 1
|
||||
});
|
||||
|
||||
db.mailboxes.createIndex({
|
||||
username: 1
|
||||
});
|
||||
db.mailboxes.createIndex({
|
||||
username: 1,
|
||||
path: 1
|
||||
});
|
||||
db.mailboxes.createIndex({
|
||||
username: 1,
|
||||
subscribed: 1
|
||||
});
|
||||
|
||||
db.messages.createIndex({
|
||||
mailbox: 1
|
||||
});
|
||||
|
||||
db.messages.createIndex({
|
||||
mailbox: 1,
|
||||
unseen: 1
|
||||
});
|
||||
db.messages.createIndex({
|
||||
mailbox: 1,
|
||||
uid: 1
|
||||
});
|
||||
db.messages.createIndex({
|
||||
mailbox: 1,
|
||||
uid: 1,
|
||||
modseq: 1
|
||||
});
|
||||
db.messages.createIndex({
|
||||
mailbox: 1,
|
||||
flags: 1
|
||||
});
|
||||
|
||||
db.messages.createIndex({
|
||||
modseq: 1
|
||||
});
|
||||
|
||||
db.messages.createIndex({
|
||||
modseq: -1
|
||||
});
|
||||
|
||||
db.messages.createIndex({
|
||||
flags: 1
|
||||
});
|
||||
|
||||
db.messages.createIndex({
|
||||
date: 1
|
||||
});
|
||||
db.messages.createIndex({
|
||||
date: -1
|
||||
});
|
||||
|
||||
db.messages.createIndex({
|
||||
uid: 1
|
||||
});
|
||||
db.messages.createIndex({
|
||||
uid: -1
|
||||
});
|
41
package.json
Normal file
41
package.json
Normal file
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"name": "wildduck",
|
||||
"version": "1.0.0",
|
||||
"description": "IMAP server built with Node.js and MongoDB",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "grunt"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "EUPL-1.1",
|
||||
"devDependencies": {
|
||||
"chai": "^3.5.0",
|
||||
"eslint-config-nodemailer": "^1.0.0",
|
||||
"grunt": "^1.0.1",
|
||||
"grunt-cli": "^1.2.0",
|
||||
"grunt-mocha-test": "^0.13.2",
|
||||
"mocha": "^3.0.2",
|
||||
"grunt-eslint": "^19.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"addressparser": "^1.0.1",
|
||||
"clone": "^1.0.2",
|
||||
"libbase64": "^0.1.0",
|
||||
"nodemailer-fetch": "^1.6.0",
|
||||
"utf7": "^1.0.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"config": "^1.25.1",
|
||||
"mailparser": "^2.0.2",
|
||||
"mongodb": "^2.2.24",
|
||||
"npmlog": "^4.0.2",
|
||||
"redis": "^2.6.5",
|
||||
"smtp-server": "^2.0.2",
|
||||
"uuid": "^3.0.1",
|
||||
"toml": "^2.3.2"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/wildduck-email/wildduck.git"
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue