pop3-emitter
Motivation
This repository is a companion to node-smtp-receiver, and shares a similar design. Together they provide a the ability to implement a simple mail server capable of receiving messages from other servers via SMTP and retrieving those message via POP3.
As with the companion module, the design goals here are minimalist:
- sufficiently complete coverage of POP3 to allow retrieving mail with common clients and libraries
- event oriented (so listeners, not callbacks or promises)
- no dependencies
- low resource requirements (so can run in any context)
- easy to configure and extend
NOTE: This module only implements the protocol layer, it delegates authentication and maildrop management to the parent application by emitting events.
Basic Usage
Similar to node's HTTPSever
, you call createServer
to create a POP3Server
object and then call
its listen
method to tell it what port to listen on:
const pop3 = require('pop3-emitter');
var server = pop3.createServer('example.com');
server.listen(110);
To form an actually useful service the application must implement authentication and maildrop
management functionality by listening to events emitted by the server and invoking the provided
callbacks. The first parameter to almost every event is user
as a string, this should be used
by the application to identify the appropriate mailbox to take action on.
Event: 'authenticate'
- user: a string (as given to
USER
orAPOP
) - password: either a plaintext password or a an APOP style hash
- method: string indicating command used to provide password (
"PASS"
or"APOP"
) - hashfunc: a method that applied to a plaintext secret known to the server should yield a match to password
- callback: function taking a boolean indicating success or failure of authentication
NOTE: you must listen for this event if you expect clients to attempt to authorize. If you do not any attempt will emit an error event and return an error to the client.
In its most common form POP3 accepts passwords in two ways, neither of which is
particularly secure. With the APOP
command the password is an MD5 hash of the
password prepended with a connection specific salt, in this case hashfunc
will
implement that hash. With the PASS
command the password is given in plaintext
(the username is given first in a separate USER
command). In this case hashfunc
will be a no op, which allows a listener like the following to work in both cases:
server.on('authenticate', function (user, password, method, hashfunc, callback)) {
// assume get_user_password() returns a plaintext password
ok = hashfunc(get_user_password(user)) === password
// ok will be true iff the passwords matched
return callback(ok);
}
Note that while APOP
may feel more secure (since the password isn't transmitted
in plaintext) it does require that the server knows the plaintext of the password
so it can feed it to the hashfunc, which is definitely a security anti-pattern.
With PASS
the server can store a hash of the password (e.g. using bcrypt) and
apply that same hash to the given plaintext. To prevent eavesdropping PASS
should probably only be used on a secure port or after an STLS
command.
Event: 'list'
- user: a string (as previously given to
USER
orAPOP
) - which: optional message index, so either null or a 1-based integer
- callback: function expecting a list of integer message sizes
If which
is null a list with the sizes for all messages in the user's mailbox
should be passed to callback. If which
is not null a list containing only
the matching message (or an empty list if no match) should be passed.
NOTE: this event is emitted for both the STAT
comand and the LIST
command.
Event: 'uidl'
- user: a string (as previously given to
USER
orAPOP
) - which: optional message uid, so either null or a string
- callback: function expecting a list of message uids
This method's behavior is the same as list
except it passes the callback
a list of unique message ids instead of integer indexes.
NOTE: UIDL is an optional feature of POP3 servers, if no listener is provided for this event the UIDL capability will not be advertised.
Event: 'retrieve'
- user: a string (as previously given to
USER
orAPOP
) - which: message index (as given to
RETR
) - callback: function expecting given message as a string
Event: 'quit'
- user: a string (as previously given to
USER
orAPOP
) - dele: a (potentially empty) list of message indexes to delete
- callback: function expecting a boolean (indicating success of deletion operation)
Per the original RFC a POP3 server is not supposed to actually
delete any messages until a valid QUIT
is issued. To support this
the connection will collect any indexes provided in DELE
commands
and pass them all to the application with this event.
Advanced Usage
It is possible to pass additional options to createServer
(as an object),
as well as a listener for the connected
event (see below).
var options = {
log: console.log.bind(console, "POP3:"), // defaults to util.log
debug: true, // if true logs all sent and received messages
apop: ..., // enable apop support (see below for details)
key: fs.readFileSync("privkey.pem"), // passed to tls.createSecureContext()
cert: fs.readFileSync("cert.pem") // passed to tls.createSecureContext()
};
var server = pop3.createServer('example.com', options, function (connection, callback) {
console.log("received connection from", connection.socket.remoteAddress);
return callback(true);
});
server.listen(110);
Option: apop
This option controls whether the server advertises support for the APOP
command by
including a salt in in the hello message. Possible values are:
- false: no APOP support (any falsy value including null works as well)
- true: (this is the default) generate a per-connection salt using
crypto.randomBytes
- string: use the given string as the salt for all connections
- function: call the given no-argument function to generate a salt for each connection
APOP salts as sent to the client are delimited by angle brackets <...>
(those brackets
are part of the salt, and are included when hashing). It's also a common convention
(but not, as far as I can tell, a requirement) that the salt include the hostname of
the server (preceded by @
). If the string provided does not include the brackets
then the hostname as given to createServer
will be appended (with an @
) and the
brackets added. The same rules are applied to the response value of the function as
to fixed values, so you could e.g. enable APOP
for some connections and not others
(say based on the port number). Some examples and the resulting hello message:
// default, generate using crypto.randomBytes
createServer("example.com")
// +OK POP3 server ready <074a5c25397c@example.com>
// false, no apop support
createServer("example.com", { apop: false })
// +OK POP3 server ready
// function (this is roughly the salt suggested in the original RFC)
createServer("example.com", { apop: () => [process.pid, Date.now()].join('.') })
// +OK POP3 server ready <51400.1561405680277@example.com>
// string without brackets, just the "random" part of the salt
createServer("example.com", { apop: "sea" })
// +OK POP3 server ready <sea@example.com>
// string with brackets, used verbatim
createServer("example.com", { apop: "<pink@himalayan>" })
// +OK POP3 server ready <pink@himalayan>
Event: 'connected'
- connection: a
POP3Connection
object - callback: a function expecting a boolean
This is issued after the POP3Server
's underlying net.Server
's connection
event but before
communcation with the client. The connection's net.Socket
is avaiable as connection.socket
.
This method can be used to enforce network rules, set a custom salt, etc. Passing any
falsy value to the callback will immediately close the connection.
It is also possible to extend/modify the behavior of the connection as it will emit a
lower level event for each received command (e.g. you could listen to the DELE
event
to mark messages for deletion immediately rather than waiting for the server's quit
event). See the code for the full list of commands/events.
Event: 'capabilities'
- state: string with the current state of the connection (
AUTHORIZATION
orTRANSACTION
) - capabilities: list of strings with capabilities for given state
- callback: a function expecting a list of strings
See RFC 2449 for details.
Listening to this event is optional, but it can be used to add or remove capabilities
to the list, e.g. if you implement some custom command in a connected
listener.
Alternatives
This module is similar in functionality to pop3-server, which is LGPL licensed if you're looking for that flavor. If you're looking for a turnkey POP3 server implementation (including mail storage and authentication) take a look at Nodemailer's wildduck.