diff --git a/CHANGES b/CHANGES index 3f2dbc743..1cff88030 100644 --- a/CHANGES +++ b/CHANGES @@ -2355,4 +2355,5 @@ * postgresql: fix restore issue with long table names * recvmail: make the addon work again * mail: update solr to 8.10.0 +* mail: POP3 support diff --git a/migrations/20211008171126-mailboxes-add-enablePop3.js b/migrations/20211008171126-mailboxes-add-enablePop3.js new file mode 100644 index 000000000..6dc1f347c --- /dev/null +++ b/migrations/20211008171126-mailboxes-add-enablePop3.js @@ -0,0 +1,16 @@ +'use strict'; + +exports.up = function(db, callback) { + db.runSql('ALTER TABLE mailboxes ADD COLUMN enablePop3 BOOLEAN DEFAULT 0', function (error) { + if (error) console.error(error); + callback(error); + }); +}; + +exports.down = function(db, callback) { + db.runSql('ALTER TABLE mailboxes DROP COLUMN enablePop3', function (error) { + if (error) console.error(error); + callback(error); + }); +}; + diff --git a/migrations/schema.sql b/migrations/schema.sql index cb0e19a69..828bc3b90 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -206,6 +206,7 @@ CREATE TABLE IF NOT EXISTS mailboxes( creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, domain VARCHAR(128), active BOOLEAN DEFAULT 1, + enablePop3 BOOLEAN DEFAULT 0, FOREIGN KEY(domain) REFERENCES mail(domain), FOREIGN KEY(aliasDomain) REFERENCES mail(domain), diff --git a/src/ldap.js b/src/ldap.js index 27c68c605..afdbdb1d0 100644 --- a/src/ldap.js +++ b/src/ldap.js @@ -616,18 +616,20 @@ async function authenticateMail(req, res, next) { const parts = email.split('@'); if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString())); - const serviceId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); // msa, imap, sieve - if (serviceId !== 'msa' && serviceId !== 'imap' && serviceId !== 'sieve') return next(new ldap.OperationsError('Invalid DN')); + const knownServices = [ 'msa', 'imap', 'pop3', 'sieve' ]; + const serviceId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); + if (!knownServices.includes(serviceId)) return next(new ldap.OperationsError('Invalid DN. Unknown service')); const [error, domain] = await safe(mail.getDomain(parts[1])); if (error) return next(new ldap.OperationsError(error.message)); if (!domain) return next(new ldap.NoSuchObjectError(req.dn.toString())); - const serviceNeedsMailbox = serviceId === 'imap' || serviceId === 'sieve'; + const serviceNeedsMailbox = serviceId === 'imap' || serviceId === 'sieve' || serviceId === 'pop3'; if (serviceNeedsMailbox && !domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString())); const [getMailboxError, mailbox] = await safe(mail.getMailbox(parts[0], parts[1])); if (getMailboxError) return next(new ldap.OperationsError(getMailboxError.message)); + if (serviceId === 'pop3' && !mailbox.enablePop3) return next(new ldap.OperationsError('POP3 is not enabled')); const [appPasswordError] = await safe(verifyAppMailboxPassword(serviceId, email, req.credentials || '')); if (!appPasswordError) { // validated as app @@ -681,6 +683,7 @@ async function start() { gServer.bind('ou=imap,dc=cloudron', authenticateMail); // dovecot (IMAP auth) gServer.bind('ou=msa,dc=cloudron', authenticateMail); // haraka (MSA auth) gServer.bind('ou=sieve,dc=cloudron', authenticateMail); // dovecot (sieve auth) + gServer.bind('ou=pop3,dc=cloudron', authenticateMail); // dovecot (pop3 auth) gServer.bind('ou=sftp,dc=cloudron', authenticateSftp); // sftp gServer.search('ou=sftp,dc=cloudron', userSearchSftp); diff --git a/src/mail.js b/src/mail.js index ad4821751..e2148a0e1 100644 --- a/src/mail.js +++ b/src/mail.js @@ -106,7 +106,7 @@ const assert = require('assert'), const DNS_OPTIONS = { timeout: 5000 }; const REMOVE_MAILBOX_CMD = path.join(__dirname, 'scripts/rmmailbox.sh'); -const MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'ownerType', 'aliasName', 'aliasDomain', 'creationTime', 'membersJson', 'membersOnly', 'domain', 'active' ].join(','); +const MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'ownerType', 'aliasName', 'aliasDomain', 'creationTime', 'membersJson', 'membersOnly', 'domain', 'active', 'enablePop3' ].join(','); const MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson', 'dkimSelector', 'bannerJson' ].join(','); function postProcessMailbox(data) { @@ -115,6 +115,7 @@ function postProcessMailbox(data) { data.membersOnly = !!data.membersOnly; data.active = !!data.active; + data.enablePop3 = !!data.enablePop3; return data; } @@ -1216,10 +1217,11 @@ async function updateMailbox(name, domain, data, auditSource) { assert.strictEqual(typeof data, 'object'); assert.strictEqual(typeof auditSource, 'object'); - const { ownerId, ownerType, active } = data; + const { ownerId, ownerType, active, enablePop3 } = data; assert.strictEqual(typeof ownerId, 'string'); assert.strictEqual(typeof ownerType, 'string'); assert.strictEqual(typeof active, 'boolean'); + assert.strictEqual(typeof enablePop3, 'boolean'); name = name.toLowerCase(); @@ -1228,7 +1230,7 @@ async function updateMailbox(name, domain, data, auditSource) { const mailbox = await getMailbox(name, domain); if (!mailbox) throw new BoxError(BoxError.NOT_FOUND, 'No such mailbox'); - const result = await database.query('UPDATE mailboxes SET ownerId = ?, ownerType = ?, active = ? WHERE name = ? AND domain = ?', [ ownerId, ownerType, active, name, domain ]); + const result = await database.query('UPDATE mailboxes SET ownerId = ?, ownerType = ?, active = ?, enablePop3 = ? WHERE name = ? AND domain = ?', [ ownerId, ownerType, active, enablePop3, name, domain ]); if (result.affectedRows === 0) throw new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'); eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldUserId: mailbox.userId, ownerId, ownerType, active }); diff --git a/src/routes/mail.js b/src/routes/mail.js index f6e93d64b..6715d72a5 100644 --- a/src/routes/mail.js +++ b/src/routes/mail.js @@ -189,6 +189,7 @@ async function updateMailbox(req, res, next) { if (typeof req.body.ownerId !== 'string') return next(new HttpError(400, 'ownerId must be a string')); if (typeof req.body.ownerType !== 'string') return next(new HttpError(400, 'ownerType must be a string')); if (typeof req.body.active !== 'boolean') return next(new HttpError(400, 'active must be a boolean')); + if (typeof req.body.enablePop3 !== 'boolean') return next(new HttpError(400, 'enablePop3 must be a boolean')); const [error] = await safe(mail.updateMailbox(req.params.name, req.params.domain, req.body, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error));