2017-06-28 17:06:12 -05:00
'use strict' ;
exports = module . exports = {
2020-07-15 15:33:53 -07:00
getStatus ,
checkConfiguration ,
2017-06-28 17:06:12 -05:00
2020-08-15 23:17:47 -07:00
getLocation ,
setLocation , // triggers the change task
changeLocation , // does the actual changing
2021-06-29 14:26:34 -07:00
listDomains ,
2018-01-20 18:56:17 -08:00
2020-07-15 15:33:53 -07:00
getDomain ,
clearDomains ,
2018-01-24 21:15:58 -08:00
2020-07-15 15:33:53 -07:00
onDomainAdded ,
onDomainRemoved ,
2020-03-31 12:04:46 -07:00
2020-07-15 15:33:53 -07:00
removePrivateFields ,
2019-02-15 10:55:15 -08:00
2020-07-15 15:33:53 -07:00
setDnsRecords ,
2018-03-08 20:08:01 -08:00
2020-07-15 15:33:53 -07:00
validateName ,
2018-05-24 16:25:32 -07:00
2020-07-15 15:33:53 -07:00
setMailFromValidation ,
setCatchAllAddress ,
setMailRelay ,
setMailEnabled ,
2020-08-23 14:33:58 -07:00
setBanner ,
2018-01-20 18:56:17 -08:00
2018-01-20 20:34:30 -08:00
startMail : restartMail ,
2020-07-15 15:33:53 -07:00
restartMail ,
handleCertChanged ,
getMailAuth ,
sendTestMail ,
getMailboxCount ,
listMailboxes ,
2021-08-17 15:45:57 -07:00
listAllMailboxes ,
2020-07-15 15:33:53 -07:00
getMailbox ,
addMailbox ,
2021-04-14 22:37:01 -07:00
updateMailbox ,
2021-08-17 15:45:57 -07:00
delMailbox ,
2020-07-15 15:33:53 -07:00
2021-08-17 15:45:57 -07:00
getAlias ,
2020-07-15 15:33:53 -07:00
getAliases ,
setAliases ,
getLists ,
getList ,
addList ,
updateList ,
2021-08-17 15:45:57 -07:00
delList ,
2020-07-15 15:33:53 -07:00
resolveList ,
2018-01-26 10:22:50 +01:00
2020-11-12 23:25:33 -08:00
OWNERTYPE _USER : 'user' ,
OWNERTYPE _GROUP : 'group' ,
2021-01-21 12:53:38 -08:00
DEFAULT _MEMORY _LIMIT : 512 * 1024 * 1024 ,
2021-08-17 15:45:57 -07:00
TYPE _MAILBOX : 'mailbox' ,
TYPE _LIST : 'list' ,
TYPE _ALIAS : 'alias' ,
_delByDomain : delByDomain ,
2021-07-07 12:59:17 -07:00
_readDkimPublicKeySync : readDkimPublicKeySync ,
_updateDomain : updateDomain
2017-06-28 17:06:12 -05:00
} ;
2021-01-21 11:31:35 -08:00
const assert = require ( 'assert' ) ,
2017-06-28 17:06:12 -05:00
async = require ( 'async' ) ,
2019-10-24 13:34:14 -07:00
BoxError = require ( './boxerror.js' ) ,
2020-08-15 23:17:47 -07:00
cloudron = require ( './cloudron.js' ) ,
2019-02-15 10:55:15 -08:00
constants = require ( './constants.js' ) ,
2021-06-29 14:26:34 -07:00
database = require ( './database.js' ) ,
2018-01-20 18:56:17 -08:00
debug = require ( 'debug' ) ( 'box:mail' ) ,
2021-08-13 17:22:28 -07:00
dns = require ( './dns.js' ) ,
2019-11-05 19:54:53 -08:00
docker = require ( './docker.js' ) ,
2018-01-20 20:34:30 -08:00
domains = require ( './domains.js' ) ,
2018-11-09 18:45:44 -08:00
eventlog = require ( './eventlog.js' ) ,
2018-12-01 21:15:42 -08:00
hat = require ( './hat.js' ) ,
2018-01-20 20:34:30 -08:00
infra = require ( './infra_version.js' ) ,
2018-01-23 16:10:23 -08:00
mailer = require ( './mailer.js' ) ,
2021-08-17 15:45:57 -07:00
mysql = require ( 'mysql' ) ,
2017-06-28 17:06:12 -05:00
net = require ( 'net' ) ,
nodemailer = require ( 'nodemailer' ) ,
2018-01-24 21:30:06 -08:00
path = require ( 'path' ) ,
2018-01-20 18:56:17 -08:00
paths = require ( './paths.js' ) ,
2020-12-02 00:24:15 -08:00
request = require ( 'request' ) ,
2018-01-30 12:23:27 -08:00
reverseProxy = require ( './reverseproxy.js' ) ,
2017-06-28 21:38:51 -05:00
safe = require ( 'safetydance' ) ,
2021-01-21 11:31:35 -08:00
services = require ( './services.js' ) ,
2019-07-26 10:49:29 -07:00
settings = require ( './settings.js' ) ,
2018-01-20 20:34:30 -08:00
shell = require ( './shell.js' ) ,
2017-06-28 17:06:12 -05:00
smtpTransport = require ( 'nodemailer-smtp-transport' ) ,
sysinfo = require ( './sysinfo.js' ) ,
2021-01-21 12:53:38 -08:00
system = require ( './system.js' ) ,
2020-08-15 23:17:47 -07:00
tasks = require ( './tasks.js' ) ,
2018-04-29 10:58:45 -07:00
users = require ( './users.js' ) ,
2021-06-29 14:26:34 -07:00
util = require ( 'util' ) ,
2019-09-11 12:44:15 -07:00
validator = require ( 'validator' ) ,
2017-06-28 17:06:12 -05:00
_ = require ( 'underscore' ) ;
2019-03-25 11:43:01 -07:00
const DNS _OPTIONS = { timeout : 5000 } ;
2021-08-17 15:45:57 -07:00
const NOOP _CALLBACK = function ( error ) { if ( error ) debug ( error ) ; } ;
const REMOVE _MAILBOX _CMD = path . join ( _ _dirname , 'scripts/rmmailbox.sh' ) ;
2018-01-20 18:56:17 -08:00
2021-08-17 15:45:57 -07:00
const MAILBOX _FIELDS = [ 'name' , 'type' , 'ownerId' , 'ownerType' , 'aliasName' , 'aliasDomain' , 'creationTime' , 'membersJson' , 'membersOnly' , 'domain' , 'active' ] . join ( ',' ) ;
2021-06-29 14:26:34 -07:00
const MAILDB _FIELDS = [ 'domain' , 'enabled' , 'mailFromValidation' , 'catchAllJson' , 'relayJson' , 'dkimSelector' , 'bannerJson' ] . join ( ',' ) ;
2021-08-19 13:24:38 -07:00
const domainsList = util . callbackify ( domains . list ) ;
2021-08-13 17:22:28 -07:00
2021-08-17 15:45:57 -07:00
function postProcessMailbox ( data ) {
data . members = safe . JSON . parse ( data . membersJson ) || [ ] ;
delete data . membersJson ;
data . membersOnly = ! ! data . membersOnly ;
data . active = ! ! data . active ;
return data ;
}
function postProcessAliases ( data ) {
const aliasNames = JSON . parse ( data . aliasNames ) , aliasDomains = JSON . parse ( data . aliasDomains ) ;
delete data . aliasNames ;
delete data . aliasDomains ;
data . aliases = [ ] ;
for ( let i = 0 ; i < aliasNames . length ; i ++ ) { // NOTE: aliasNames is [ null ] when no aliases
if ( aliasNames [ i ] ) data . aliases [ i ] = { name : aliasNames [ i ] , domain : aliasDomains [ i ] } ;
}
return data ;
}
function postProcessDomain ( data ) {
2021-06-29 14:26:34 -07:00
data . enabled = ! ! data . enabled ; // int to boolean
data . mailFromValidation = ! ! data . mailFromValidation ; // int to boolean
data . catchAll = safe . JSON . parse ( data . catchAllJson ) || [ ] ;
delete data . catchAllJson ;
data . relay = safe . JSON . parse ( data . relayJson ) || { provider : 'cloudron-smtp' } ;
delete data . relayJson ;
data . banner = safe . JSON . parse ( data . bannerJson ) || { text : null , html : null } ;
delete data . bannerJson ;
return data ;
}
2018-04-03 09:36:41 -07:00
function validateName ( name ) {
assert . strictEqual ( typeof name , 'string' ) ;
2018-01-25 18:03:02 +01:00
2019-10-24 13:34:14 -07:00
if ( name . length < 1 ) return new BoxError ( BoxError . BAD _FIELD , 'mailbox name must be atleast 1 char' ) ;
if ( name . length >= 200 ) return new BoxError ( BoxError . BAD _FIELD , 'mailbox name too long' ) ;
2018-01-25 18:03:02 +01:00
2018-07-26 23:48:18 -07:00
// also need to consider valid LDAP characters here (e.g '+' is reserved)
2019-10-24 13:34:14 -07:00
if ( /[^a-zA-Z0-9.-]/ . test ( name ) ) return new BoxError ( BoxError . BAD _FIELD , 'mailbox name can only contain alphanumerals and dot' ) ;
2018-01-25 18:03:02 +01:00
return null ;
}
2017-06-28 21:38:51 -05:00
function checkOutboundPort25 ( callback ) {
2017-06-28 17:06:12 -05:00
assert . strictEqual ( typeof callback , 'function' ) ;
2017-06-28 21:38:51 -05:00
var smtpServer = _ . sample ( [
'smtp.gmail.com' ,
'smtp.live.com' ,
2018-02-08 11:48:55 -08:00
'smtp.1und1.de' ,
2017-06-28 21:38:51 -05:00
] ) ;
var relay = {
value : 'OK' ,
status : false
} ;
var client = new net . Socket ( ) ;
client . setTimeout ( 5000 ) ;
client . connect ( 25 , smtpServer ) ;
client . on ( 'connect' , function ( ) {
relay . status = true ;
relay . value = 'OK' ;
client . destroy ( ) ; // do not use end() because it still triggers timeout
callback ( null , relay ) ;
} ) ;
client . on ( 'timeout' , function ( ) {
relay . status = false ;
2019-01-25 10:27:44 -08:00
relay . value = ` Connect to ${ smtpServer } timed out. Check if port 25 (outbound) is blocked ` ;
2017-06-28 21:38:51 -05:00
client . destroy ( ) ;
2019-12-04 10:29:06 -08:00
callback ( new BoxError ( BoxError . TIMEOUT , ` Connect to ${ smtpServer } timed out. ` ) , relay ) ;
2017-06-28 21:38:51 -05:00
} ) ;
client . on ( 'error' , function ( error ) {
relay . status = false ;
2019-01-25 10:27:44 -08:00
relay . value = ` Connect to ${ smtpServer } failed: ${ error . message } . Check if port 25 (outbound) is blocked ` ;
2017-06-28 21:38:51 -05:00
client . destroy ( ) ;
2019-12-05 09:54:29 -08:00
callback ( new BoxError ( BoxError . NETWORK _ERROR , ` Connect to ${ smtpServer } failed. ` ) , relay ) ;
2017-06-28 21:38:51 -05:00
} ) ;
}
function checkSmtpRelay ( relay , callback ) {
var result = {
value : 'OK' ,
status : false
} ;
2017-06-28 17:06:12 -05:00
2019-04-22 14:41:44 +02:00
var options = {
2018-07-23 17:05:15 -07:00
connectionTimeout : 5000 ,
greetingTimeout : 5000 ,
2017-06-28 17:06:12 -05:00
host : relay . host ,
2019-04-22 14:41:44 +02:00
port : relay . port
} ;
// only set auth if either username or password is provided, some relays auth based on IP (range)
if ( relay . username || relay . password ) {
options . auth = {
2017-06-28 17:06:12 -05:00
user : relay . username ,
pass : relay . password
2019-04-22 14:41:44 +02:00
} ;
}
2019-04-23 15:19:33 -07:00
if ( relay . acceptSelfSignedCerts ) options . tls = { rejectUnauthorized : false } ;
2019-04-22 14:41:44 +02:00
var transporter = nodemailer . createTransport ( smtpTransport ( options ) ) ;
2017-06-28 17:06:12 -05:00
transporter . verify ( function ( error ) {
2017-06-29 10:11:55 -05:00
result . status = ! error ;
2017-06-28 21:38:51 -05:00
if ( error ) {
result . value = error . message ;
return callback ( error , result ) ;
}
2017-06-28 17:06:12 -05:00
2017-12-04 17:10:02 +05:30
callback ( null , result ) ;
2017-06-28 21:38:51 -05:00
} ) ;
2017-06-28 17:06:12 -05:00
}
2021-06-29 14:26:34 -07:00
async function verifyRelay ( relay ) {
2017-06-28 21:38:51 -05:00
assert . strictEqual ( typeof relay , 'object' ) ;
2017-06-28 17:06:12 -05:00
2018-03-08 23:23:02 -08:00
// we used to verify cloudron-smtp with checkOutboundPort25 but that is unreliable given that we just
// randomly select some smtp server
2021-06-29 14:26:34 -07:00
if ( relay . provider === 'cloudron-smtp' || relay . provider === 'noop' ) return ;
2017-06-28 17:06:12 -05:00
2021-06-29 14:26:34 -07:00
const checkSmtpRelayAsync = util . promisify ( checkSmtpRelay ) ;
2017-06-28 17:06:12 -05:00
2021-06-29 14:26:34 -07:00
const [ error ] = await safe ( checkSmtpRelayAsync ( relay ) ) ;
if ( error ) throw new BoxError ( BoxError . BAD _FIELD , error . message ) ;
2017-06-28 21:38:51 -05:00
}
2017-06-28 17:06:12 -05:00
2019-06-10 12:23:29 -07:00
function checkDkim ( mailDomain , callback ) {
assert . strictEqual ( typeof mailDomain , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
const domain = mailDomain . domain ;
let dkim = {
domain : ` ${ mailDomain . dkimSelector } ._domainkey. ${ domain } ` ,
name : ` ${ mailDomain . dkimSelector } ._domainkey ` ,
2017-06-28 21:38:51 -05:00
type : 'TXT' ,
expected : null ,
value : null ,
status : false
} ;
2017-06-28 17:06:12 -05:00
2018-01-25 14:51:07 -08:00
var dkimKey = readDkimPublicKeySync ( domain ) ;
2019-12-12 20:36:27 -08:00
if ( ! dkimKey ) return callback ( new BoxError ( BoxError . FS _ERROR , ` Failed to read dkim public key of ${ domain } ` ) , dkim ) ;
2017-06-28 17:06:12 -05:00
2018-02-08 10:21:31 -08:00
dkim . expected = 'v=DKIM1; t=s; p=' + dkimKey ;
2017-06-28 17:06:12 -05:00
2018-02-08 10:21:31 -08:00
dns . resolve ( dkim . domain , dkim . type , DNS _OPTIONS , function ( error , txtRecords ) {
2017-06-28 21:38:51 -05:00
if ( error ) return callback ( error , dkim ) ;
2017-06-28 17:06:12 -05:00
2018-02-08 10:21:31 -08:00
if ( txtRecords . length !== 0 ) {
dkim . value = txtRecords [ 0 ] . join ( '' ) ;
2020-04-17 12:37:57 -07:00
const actual = txtToDict ( dkim . value ) ;
dkim . status = actual . p === dkimKey ;
2017-06-28 21:38:51 -05:00
}
2017-06-28 17:06:12 -05:00
2017-06-28 21:38:51 -05:00
callback ( null , dkim ) ;
} ) ;
}
2017-06-28 17:06:12 -05:00
2019-01-31 15:08:14 -08:00
function checkSpf ( domain , mailFqdn , callback ) {
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof mailFqdn , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2017-06-28 21:38:51 -05:00
var spf = {
2018-01-24 11:33:09 -08:00
domain : domain ,
2018-07-24 14:03:39 -07:00
name : '@' ,
2017-06-28 21:38:51 -05:00
type : 'TXT' ,
value : null ,
2019-01-31 15:08:14 -08:00
expected : 'v=spf1 a:' + mailFqdn + ' ~all' ,
2017-06-28 21:38:51 -05:00
status : false
} ;
2018-02-08 10:21:31 -08:00
dns . resolve ( spf . domain , spf . type , DNS _OPTIONS , function ( error , txtRecords ) {
2017-06-28 21:38:51 -05:00
if ( error ) return callback ( error , spf ) ;
var i ;
for ( i = 0 ; i < txtRecords . length ; i ++ ) {
2018-02-08 10:21:31 -08:00
let txtRecord = txtRecords [ i ] . join ( '' ) ; // https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
if ( txtRecord . indexOf ( 'v=spf1 ' ) !== 0 ) continue ; // not SPF
spf . value = txtRecord ;
2020-08-15 23:17:47 -07:00
spf . status = spf . value . indexOf ( ' a:' + settings . mailFqdn ( ) ) !== - 1 ;
2017-06-28 21:38:51 -05:00
break ;
}
2017-06-28 17:06:12 -05:00
2017-06-28 21:38:51 -05:00
if ( spf . status ) {
spf . expected = spf . value ;
} else if ( i !== txtRecords . length ) {
2020-08-15 23:17:47 -07:00
spf . expected = 'v=spf1 a:' + settings . mailFqdn ( ) + ' ' + spf . value . slice ( 'v=spf1 ' . length ) ;
2017-06-28 21:38:51 -05:00
}
2017-06-28 17:06:12 -05:00
2017-06-28 21:38:51 -05:00
callback ( null , spf ) ;
} ) ;
}
2017-06-28 17:06:12 -05:00
2019-01-31 15:08:14 -08:00
function checkMx ( domain , mailFqdn , callback ) {
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof mailFqdn , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2017-06-28 21:38:51 -05:00
var mx = {
2018-01-24 11:33:09 -08:00
domain : domain ,
2018-07-24 14:03:39 -07:00
name : '@' ,
2017-06-28 21:38:51 -05:00
type : 'MX' ,
value : null ,
2019-01-31 15:08:14 -08:00
expected : '10 ' + mailFqdn + '.' ,
2017-06-28 21:38:51 -05:00
status : false
} ;
2018-02-08 10:21:31 -08:00
dns . resolve ( mx . domain , mx . type , DNS _OPTIONS , function ( error , mxRecords ) {
2017-06-28 21:38:51 -05:00
if ( error ) return callback ( error , mx ) ;
2019-05-20 17:56:16 -07:00
if ( mxRecords . length === 0 ) return callback ( null , mx ) ;
2017-06-28 21:38:51 -05:00
2020-04-22 17:16:52 -07:00
mx . status = mxRecords . some ( mx => mx . exchange === mailFqdn ) ; // this lets use change priority and/or setup backup MX
2019-05-20 17:56:16 -07:00
mx . value = mxRecords . map ( function ( r ) { return r . priority + ' ' + r . exchange + '.' ; } ) . join ( ' ' ) ;
if ( mx . status ) return callback ( null , mx ) ; // MX record is "my."
// cloudflare might create a conflict subdomain (https://support.cloudflare.com/hc/en-us/articles/360020296512-DNS-Troubleshooting-FAQ)
dns . resolve ( mxRecords [ 0 ] . exchange , 'A' , DNS _OPTIONS , function ( error , mxIps ) {
if ( error || mxIps . length !== 1 ) return callback ( null , mx ) ;
2019-10-29 15:46:33 -07:00
sysinfo . getServerIp ( function ( error , ip ) {
2019-05-20 17:56:16 -07:00
if ( error ) return callback ( null , mx ) ;
mx . status = mxIps [ 0 ] === ip ;
2017-06-28 17:06:12 -05:00
2019-05-20 17:56:16 -07:00
callback ( null , mx ) ;
} ) ;
} ) ;
2017-06-28 21:38:51 -05:00
} ) ;
}
2017-06-28 17:06:12 -05:00
2018-08-12 13:43:45 -07:00
function txtToDict ( txt ) {
var dict = { } ;
txt . split ( ';' ) . forEach ( function ( v ) {
var p = v . trim ( ) . split ( '=' ) ;
dict [ p [ 0 ] ] = p [ 1 ] ;
} ) ;
return dict ;
}
2018-01-24 11:33:09 -08:00
function checkDmarc ( domain , callback ) {
2017-06-28 21:38:51 -05:00
var dmarc = {
2018-01-24 11:33:09 -08:00
domain : '_dmarc.' + domain ,
2018-07-24 14:03:39 -07:00
name : '_dmarc' ,
2017-06-28 21:38:51 -05:00
type : 'TXT' ,
value : null ,
2018-02-08 10:21:31 -08:00
expected : 'v=DMARC1; p=reject; pct=100' ,
2017-06-28 21:38:51 -05:00
status : false
} ;
2018-02-08 10:21:31 -08:00
dns . resolve ( dmarc . domain , dmarc . type , DNS _OPTIONS , function ( error , txtRecords ) {
2017-06-28 21:38:51 -05:00
if ( error ) return callback ( error , dmarc ) ;
2018-02-08 10:21:31 -08:00
if ( txtRecords . length !== 0 ) {
dmarc . value = txtRecords [ 0 ] . join ( '' ) ;
2020-03-30 19:32:05 -07:00
const actual = txtToDict ( dmarc . value ) ;
dmarc . status = actual . v === 'DMARC1' ; // see box#666
2017-06-28 21:38:51 -05:00
}
2017-06-28 17:06:12 -05:00
2017-06-28 21:38:51 -05:00
callback ( null , dmarc ) ;
} ) ;
}
2017-06-28 17:06:12 -05:00
2019-01-31 15:08:14 -08:00
function checkPtr ( mailFqdn , callback ) {
assert . strictEqual ( typeof mailFqdn , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2017-06-28 21:38:51 -05:00
var ptr = {
domain : null ,
2019-10-25 10:05:53 -07:00
name : null ,
2017-06-28 21:38:51 -05:00
type : 'PTR' ,
value : null ,
2019-01-31 15:08:14 -08:00
expected : mailFqdn , // any trailing '.' is added by client software (https://lists.gt.net/spf/devel/7918)
2017-06-28 21:38:51 -05:00
status : false
} ;
2017-06-28 17:06:12 -05:00
2019-10-29 15:46:33 -07:00
sysinfo . getServerIp ( function ( error , ip ) {
2017-06-28 21:38:51 -05:00
if ( error ) return callback ( error , ptr ) ;
2017-06-28 17:06:12 -05:00
2017-06-28 21:38:51 -05:00
ptr . domain = ip . split ( '.' ) . reverse ( ) . join ( '.' ) + '.in-addr.arpa' ;
2019-10-25 10:05:53 -07:00
ptr . name = ip ;
2017-06-28 17:06:12 -05:00
2018-02-08 11:52:43 -08:00
dns . resolve ( ptr . domain , 'PTR' , DNS _OPTIONS , function ( error , ptrRecords ) {
2017-06-28 21:38:51 -05:00
if ( error ) return callback ( error , ptr ) ;
2017-06-28 17:06:12 -05:00
2018-02-08 10:21:31 -08:00
if ( ptrRecords . length !== 0 ) {
2017-06-28 21:38:51 -05:00
ptr . value = ptrRecords . join ( ' ' ) ;
ptr . status = ptrRecords . some ( function ( v ) { return v === ptr . expected ; } ) ;
}
2017-06-28 17:06:12 -05:00
2017-06-28 21:38:51 -05:00
return callback ( null , ptr ) ;
2017-06-28 17:06:12 -05:00
} ) ;
2017-06-28 21:38:51 -05:00
} ) ;
}
2017-06-28 17:06:12 -05:00
2017-09-08 11:50:11 -07:00
// https://raw.githubusercontent.com/jawsome/node-dnsbl/master/list.json
const RBL _LIST = [
2018-03-05 14:26:53 -08:00
{
'name' : 'Abuse.ch' ,
'dns' : 'spam.abuse.ch' ,
'site' : 'http://abuse.ch/'
} ,
2017-09-08 11:50:11 -07:00
{
2017-12-04 17:10:02 +05:30
'name' : 'Barracuda' ,
'dns' : 'b.barracudacentral.org' ,
'site' : 'http://www.barracudacentral.org/rbl/removal-request'
2017-09-08 11:50:11 -07:00
} ,
{
2018-03-05 14:26:53 -08:00
'name' : 'Composite Blocking List' ,
'dns' : 'cbl.abuseat.org' ,
'site' : 'http://www.abuseat.org'
} ,
{
'name' : 'Multi SURBL' ,
'dns' : 'multi.surbl.org' ,
'site' : 'http://www.surbl.org'
} ,
{
'name' : 'Passive Spam Block List' ,
'dns' : 'psbl.surriel.com' ,
'site' : 'https://psbl.org'
2017-09-08 11:50:11 -07:00
} ,
{
2017-12-04 17:10:02 +05:30
'name' : 'Sorbs Aggregate Zone' ,
'dns' : 'dnsbl.sorbs.net' ,
'site' : 'http://dnsbl.sorbs.net/'
2017-09-08 11:50:11 -07:00
} ,
{
2017-12-04 17:10:02 +05:30
'name' : 'Sorbs spam.dnsbl Zone' ,
'dns' : 'spam.dnsbl.sorbs.net' ,
'site' : 'http://sorbs.net'
2017-09-08 11:50:11 -07:00
} ,
{
2018-03-05 14:26:53 -08:00
'name' : 'SpamCop' ,
'dns' : 'bl.spamcop.net' ,
'site' : 'http://spamcop.net'
} ,
{
'name' : 'SpamHaus Zen' ,
'dns' : 'zen.spamhaus.org' ,
'site' : 'http://spamhaus.org'
2017-09-08 11:50:11 -07:00
} ,
{
2017-12-04 17:10:02 +05:30
'name' : 'The Unsubscribe Blacklist(UBL)' ,
'dns' : 'ubl.unsubscore.com ' ,
'site' : 'http://www.lashback.com/blacklist/'
2017-09-08 11:50:11 -07:00
} ,
{
2017-12-04 17:10:02 +05:30
'name' : 'UCEPROTECT Network' ,
'dns' : 'dnsbl-1.uceprotect.net' ,
'site' : 'http://www.uceprotect.net/en'
2017-09-08 11:50:11 -07:00
}
] ;
2018-01-24 11:33:09 -08:00
// this function currently only looks for black lists based on IP. TODO: also look up by domain
function checkRblStatus ( domain , callback ) {
2017-09-13 22:39:42 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2017-09-08 11:50:11 -07:00
2019-10-29 15:46:33 -07:00
sysinfo . getServerIp ( function ( error , ip ) {
2017-09-08 11:50:11 -07:00
if ( error ) return callback ( error , ip ) ;
var flippedIp = ip . split ( '.' ) . reverse ( ) . join ( '.' ) ;
// https://tools.ietf.org/html/rfc5782
async . map ( RBL _LIST , function ( rblServer , iteratorDone ) {
2018-02-08 10:21:31 -08:00
dns . resolve ( flippedIp + '.' + rblServer . dns , 'A' , DNS _OPTIONS , function ( error , records ) {
2017-09-08 11:50:11 -07:00
if ( error || ! records ) return iteratorDone ( null , null ) ; // not listed
2018-01-24 11:33:09 -08:00
debug ( 'checkRblStatus: %s (ip: %s) is in the blacklist of %j' , domain , flippedIp , rblServer ) ;
2017-09-08 11:50:11 -07:00
var result = _ . extend ( { } , rblServer ) ;
2018-02-08 10:21:31 -08:00
dns . resolve ( flippedIp + '.' + rblServer . dns , 'TXT' , DNS _OPTIONS , function ( error , txtRecords ) {
result . txtRecords = error || ! txtRecords ? 'No txt record' : txtRecords . map ( x => x . join ( '' ) ) ;
2017-09-08 11:50:11 -07:00
2018-01-24 11:33:09 -08:00
debug ( 'checkRblStatus: %s (error: %s) (txtRecords: %j)' , domain , error , txtRecords ) ;
2017-09-08 11:50:11 -07:00
return iteratorDone ( null , result ) ;
} ) ;
} ) ;
} , function ( ignoredError , blacklistedServers ) {
blacklistedServers = blacklistedServers . filter ( function ( b ) { return b !== null ; } ) ;
2018-01-24 11:33:09 -08:00
debug ( 'checkRblStatus: %s (ip: %s) servers: %j' , domain , ip , blacklistedServers ) ;
2017-09-13 22:39:42 -07:00
return callback ( null , { status : blacklistedServers . length === 0 , ip : ip , servers : blacklistedServers } ) ;
} ) ;
} ) ;
}
2018-01-21 00:40:30 -08:00
function getStatus ( domain , callback ) {
assert . strictEqual ( typeof domain , 'string' ) ;
2017-09-13 22:39:42 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2018-01-23 12:00:24 +01:00
// ensure we always have a valid toplevel properties for the api
var results = {
2020-02-27 10:36:35 -08:00
dns : { } , // { mx/dmarc/dkim/spf/ptr: { expected, value, name, domain, type, status } }
2019-03-06 19:48:18 -08:00
rbl : { } , // { status, ip, servers: [{name,site,dns}]} optional. only for cloudron-smtp
relay : { } // { status, value } always checked
2018-01-23 12:00:24 +01:00
} ;
2017-09-13 22:39:42 -07:00
function recordResult ( what , func ) {
return function ( callback ) {
func ( function ( error , result ) {
2021-06-29 14:26:34 -07:00
if ( error ) debug ( ` Ignored error - ${ what } : ${ error . message } ` ) ;
2017-09-13 22:39:42 -07:00
2019-12-12 20:36:27 -08:00
safe . set ( results , what , result || { } ) ;
2017-09-13 22:39:42 -07:00
callback ( ) ;
} ) ;
} ;
}
2017-09-08 11:50:11 -07:00
2019-07-26 10:49:29 -07:00
const mailFqdn = settings . mailFqdn ( ) ;
2021-06-29 14:26:34 -07:00
const getDomainFunc = util . callbackify ( getDomain ) ;
2019-01-31 15:08:14 -08:00
2021-06-29 14:26:34 -07:00
getDomainFunc ( domain , function ( error , mailDomain ) {
2017-09-13 22:39:42 -07:00
if ( error ) return callback ( error ) ;
2021-06-29 14:26:34 -07:00
if ( ! mailDomain ) return callback ( new BoxError ( BoxError . NOT _FOUND , 'mail domain not found' ) ) ;
2017-09-08 11:50:11 -07:00
2019-02-06 15:23:41 -08:00
let checks = [ ] ;
2019-06-10 12:23:29 -07:00
if ( mailDomain . enabled ) {
2019-02-06 15:23:41 -08:00
checks . push (
recordResult ( 'dns.mx' , checkMx . bind ( null , domain , mailFqdn ) ) ,
recordResult ( 'dns.dmarc' , checkDmarc . bind ( null , domain ) )
) ;
}
2017-09-13 22:39:42 -07:00
2019-06-10 12:23:29 -07:00
if ( mailDomain . relay . provider === 'cloudron-smtp' ) {
2017-09-13 22:39:42 -07:00
// these tests currently only make sense when using Cloudron's SMTP server at this point
checks . push (
2019-01-31 15:08:14 -08:00
recordResult ( 'dns.spf' , checkSpf . bind ( null , domain , mailFqdn ) ) ,
2019-06-10 12:23:29 -07:00
recordResult ( 'dns.dkim' , checkDkim . bind ( null , mailDomain ) ) ,
2019-01-31 15:08:14 -08:00
recordResult ( 'dns.ptr' , checkPtr . bind ( null , mailFqdn ) ) ,
2017-09-13 22:39:42 -07:00
recordResult ( 'relay' , checkOutboundPort25 ) ,
2018-01-24 11:33:09 -08:00
recordResult ( 'rbl' , checkRblStatus . bind ( null , domain ) )
2017-09-13 22:39:42 -07:00
) ;
2019-06-10 12:23:29 -07:00
} else if ( mailDomain . relay . provider !== 'noop' ) {
checks . push ( recordResult ( 'relay' , checkSmtpRelay . bind ( null , mailDomain . relay ) ) ) ;
2017-09-13 22:39:42 -07:00
}
async . parallel ( checks , function ( ) {
callback ( null , results ) ;
2017-09-08 11:50:11 -07:00
} ) ;
} ) ;
}
2018-01-20 18:56:17 -08:00
2021-08-17 15:45:57 -07:00
async function checkConfiguration ( ) {
2019-02-28 16:46:30 -08:00
let messages = { } ;
2021-08-17 15:45:57 -07:00
const allDomains = await listDomains ( ) ;
2019-02-28 16:46:30 -08:00
2021-08-17 15:45:57 -07:00
for ( const domainObject of allDomains ) {
const result = await util . promisify ( getStatus ) ( domainObject . domain ) ;
2019-02-28 16:46:30 -08:00
2021-08-17 15:45:57 -07:00
let message = [ ] ;
2019-02-28 16:46:30 -08:00
2021-08-17 15:45:57 -07:00
Object . keys ( result . dns ) . forEach ( ( type ) => {
const record = result . dns [ type ] ;
if ( ! record . status ) message . push ( ` ${ type . toUpperCase ( ) } DNS record ( ${ record . type } ) did not match. \n * Hostname: \` ${ record . name } \` \n * Expected: \` ${ record . expected } \` \n * Actual: \` ${ record . value } \` ` ) ;
} ) ;
if ( result . relay && result . relay . status === false ) message . push ( ` Relay error: ${ result . relay . value } ` ) ;
if ( result . rbl && result . rbl . status === false ) { // rbl field contents is optional
const servers = result . rbl . servers . map ( ( bs ) => ` [ ${ bs . name } ]( ${ bs . site } ) ` ) ; // in markdown
message . push ( ` This server's IP \` ${ result . rbl . ip } \` is blacklisted in the following servers - ${ servers . join ( ', ' ) } ` ) ;
}
2019-02-28 16:46:30 -08:00
2021-08-17 15:45:57 -07:00
if ( message . length ) messages [ domainObject . domain ] = message ;
}
2019-02-28 16:46:30 -08:00
2021-08-17 15:45:57 -07:00
// create bulleted list for each domain
let markdownMessage = '' ;
Object . keys ( messages ) . forEach ( ( domain ) => {
markdownMessage += ` ** ${ domain } ** \n ` ;
markdownMessage += messages [ domain ] . map ( ( m ) => ` * ${ m } \n ` ) . join ( '' ) ;
markdownMessage += '\n\n' ;
} ) ;
2019-02-28 16:46:30 -08:00
2021-08-17 15:45:57 -07:00
if ( markdownMessage ) markdownMessage += 'Email Status is checked every 30 minutes.\n See the [troubleshooting docs](https://docs.cloudron.io/troubleshooting/#mail-dns) for more information.\n' ;
2019-03-01 11:24:10 -08:00
2021-08-17 15:45:57 -07:00
return markdownMessage ; // empty message means all status checks succeeded
2019-02-28 16:46:30 -08:00
}
2021-08-17 15:45:57 -07:00
async function createMailConfig ( mailFqdn , mailDomain ) {
2019-01-31 15:08:14 -08:00
assert . strictEqual ( typeof mailFqdn , 'string' ) ;
2019-09-12 16:08:29 -07:00
assert . strictEqual ( typeof mailDomain , 'string' ) ;
2018-01-20 18:56:17 -08:00
debug ( 'createMailConfig: generating mail config' ) ;
2021-08-17 15:45:57 -07:00
const mailDomains = await listDomains ( ) ;
2018-01-20 18:56:17 -08:00
2021-08-17 15:45:57 -07:00
const mailOutDomains = mailDomains . filter ( d => d . relay . provider !== 'noop' ) . map ( d => d . domain ) . join ( ',' ) ;
const mailInDomains = mailDomains . filter ( function ( d ) { return d . enabled ; } ) . map ( function ( d ) { return d . domain ; } ) . join ( ',' ) ;
2018-02-05 15:02:34 -08:00
2021-08-17 15:45:57 -07:00
// mail_domain is used for SRS
if ( ! safe . fs . writeFileSync ( path . join ( paths . ADDON _CONFIG _DIR , 'mail/mail.ini' ) ,
` mail_in_domains= ${ mailInDomains } \n mail_out_domains= ${ mailOutDomains } \n mail_server_name= ${ mailFqdn } \n mail_domain= ${ mailDomain } \n \n ` , 'utf8' ) ) {
throw new BoxError ( BoxError . FS _ERROR , ` Could not create mail var file: ${ safe . error . message } ` ) ;
}
2018-01-20 18:56:17 -08:00
2021-08-17 15:45:57 -07:00
// enable_outbound makes plugin forward email for relayed mail. non-relayed mail always hits LMTP plugin first
if ( ! safe . fs . writeFileSync ( path . join ( paths . ADDON _CONFIG _DIR , 'mail/smtp_forward.ini' ) , 'enable_outbound=false\ndomain_selector=mail_from\n' , 'utf8' ) ) {
throw new BoxError ( BoxError . FS _ERROR , ` Could not create smtp forward file: ${ safe . error . message } ` ) ;
}
2018-01-20 18:56:17 -08:00
2021-08-17 15:45:57 -07:00
// create sections for per-domain configuration
for ( const domain of mailDomains ) {
const catchAll = domain . catchAll . map ( function ( c ) { return ` ${ c } @ ${ domain . domain } ` ; } ) . join ( ',' ) ;
const mailFromValidation = domain . mailFromValidation ;
2018-01-20 18:56:17 -08:00
2021-08-17 15:45:57 -07:00
if ( ! safe . fs . appendFileSync ( path . join ( paths . ADDON _CONFIG _DIR , 'mail/mail.ini' ) ,
` [ ${ domain . domain } ] \n catch_all= ${ catchAll } \n mail_from_validation= ${ mailFromValidation } \n \n ` , 'utf8' ) ) {
throw new BoxError ( BoxError . FS _ERROR , ` Could not create mail var file: ${ safe . error . message } ` ) ;
}
2020-08-24 10:28:53 -07:00
2021-08-17 15:45:57 -07:00
if ( ! safe . fs . writeFileSync ( ` ${ paths . ADDON _CONFIG _DIR } /mail/banner/ ${ domain . domain } .text ` , domain . banner . text || '' ) ) throw new BoxError ( BoxError . FS _ERROR , ` Could not create text banner file: ${ safe . error . message } ` ) ;
if ( ! safe . fs . writeFileSync ( ` ${ paths . ADDON _CONFIG _DIR } /mail/banner/ ${ domain . domain } .html ` , domain . banner . html || '' ) ) throw new BoxError ( BoxError . FS _ERROR , ` Could not create html banner file: ${ safe . error . message } ` ) ;
2018-02-05 15:02:34 -08:00
2021-08-17 15:45:57 -07:00
const relay = domain . relay ;
2018-02-06 14:36:11 -08:00
2021-08-17 15:45:57 -07:00
const enableRelay = relay . provider !== 'cloudron-smtp' && relay . provider !== 'noop' ,
host = relay . host || '' ,
port = relay . port || 25 ,
authType = relay . username ? 'plain' : '' ,
username = relay . username || '' ,
password = relay . password || '' ;
2018-01-20 18:56:17 -08:00
2021-08-17 15:45:57 -07:00
if ( ! enableRelay ) continue ;
2020-08-23 14:33:58 -07:00
2021-08-17 15:45:57 -07:00
if ( ! safe . fs . appendFileSync ( paths . ADDON _CONFIG _DIR + '/mail/smtp_forward.ini' ,
` [ ${ domain . domain } ] \n enable_outbound=true \n host= ${ host } \n port= ${ port } \n enable_tls=true \n auth_type= ${ authType } \n auth_user= ${ username } \n auth_pass= ${ password } \n \n ` , 'utf8' ) ) {
throw new BoxError ( BoxError . FS _ERROR , ` Could not create mail var file: ${ safe . error . message } ` ) ;
}
}
2019-05-07 11:29:59 -07:00
2021-08-17 15:45:57 -07:00
return mailInDomains . length !== 0 /* allowInbound */ ;
2018-01-20 18:56:17 -08:00
}
2021-08-17 15:45:57 -07:00
async function configureMail ( mailFqdn , mailDomain , serviceConfig ) {
2019-02-04 17:10:07 -08:00
assert . strictEqual ( typeof mailFqdn , 'string' ) ;
assert . strictEqual ( typeof mailDomain , 'string' ) ;
2021-01-21 12:53:38 -08:00
assert . strictEqual ( typeof serviceConfig , 'object' ) ;
2019-02-04 17:10:07 -08:00
2021-03-18 21:53:01 -07:00
// mail (note: 2587 is hardcoded in mail container and app use this port)
2018-01-20 20:34:30 -08:00
// MAIL_SERVER_NAME is the hostname of the mailserver i.e server uses these certs
// MAIL_DOMAIN is the domain for which this server is relaying mails
// mail container uses /app/data for backed up data and /run for restart-able data
const tag = infra . images . mail . tag ;
2021-01-21 12:53:38 -08:00
const memoryLimit = serviceConfig . memoryLimit || exports . DEFAULT _MEMORY _LIMIT ;
const memory = system . getMemoryAllocation ( memoryLimit ) ;
2018-12-28 13:32:37 -08:00
const cloudronToken = hat ( 8 * 128 ) , relayToken = hat ( 8 * 128 ) ;
2018-01-20 20:34:30 -08:00
2021-08-17 15:45:57 -07:00
const bundle = await reverseProxy . getCertificatePath ( mailFqdn , mailDomain ) ;
const dhparamsFilePath = path . join ( paths . ADDON _CONFIG _DIR , 'mail/dhparams.pem' ) ;
const mailCertFilePath = path . join ( paths . ADDON _CONFIG _DIR , 'mail/tls_cert.pem' ) ;
const mailKeyFilePath = path . join ( paths . ADDON _CONFIG _DIR , 'mail/tls_key.pem' ) ;
if ( ! safe . child _process . execSync ( ` cp ${ paths . DHPARAMS _FILE } ${ dhparamsFilePath } ` ) ) throw new BoxError ( BoxError . FS _ERROR , 'Could not copy dhparams:' + safe . error . message ) ;
if ( ! safe . child _process . execSync ( ` cp ${ bundle . certFilePath } ${ mailCertFilePath } ` ) ) throw new BoxError ( BoxError . FS _ERROR , 'Could not create cert file:' + safe . error . message ) ;
if ( ! safe . child _process . execSync ( ` cp ${ bundle . keyFilePath } ${ mailKeyFilePath } ` ) ) throw new BoxError ( BoxError . FS _ERROR , 'Could not create key file:' + safe . error . message ) ;
await shell . promises . exec ( 'stopMail' , 'docker stop mail || true' ) ;
await shell . promises . exec ( 'removeMail' , 'docker rm -f mail || true' ) ;
const allowInbound = await createMailConfig ( mailFqdn , mailDomain ) ;
const ports = allowInbound ? '-p 587:2587 -p 993:9993 -p 4190:4190 -p 25:2587' : '' ;
const cmd = ` docker run --restart=always -d --name="mail" \
-- net cloudron \
-- net - alias mail \
-- log - driver syslog \
-- log - opt syslog - address = udp : //127.0.0.1:2514 \
-- log - opt syslog - format = rfc5424 \
-- log - opt tag = mail \
- m $ { memory } \
-- memory - swap $ { memoryLimit } \
-- dns 172.18 . 0.1 \
-- dns - search = . \
- e CLOUDRON _MAIL _TOKEN = "${cloudronToken}" \
- e CLOUDRON _RELAY _TOKEN = "${relayToken}" \
- v "${paths.MAIL_DATA_DIR}:/app/data" \
- v "${paths.PLATFORM_DATA_DIR}/addons/mail:/etc/mail" \
$ { ports } \
-- label isCloudronManaged = true \
-- read - only - v / run - v / tmp $ { tag } ` ;
await shell . promises . exec ( 'startMail' , cmd ) ;
2018-01-20 20:34:30 -08:00
}
2021-08-19 12:32:23 -07:00
async function getMailAuth ( ) {
const dockerInspect = util . promisify ( docker . inspect ) ;
const data = await dockerInspect ( 'mail' ) ;
const ip = safe . query ( data , 'NetworkSettings.Networks.cloudron.IPAddress' ) ;
if ( ! ip ) throw new BoxError ( BoxError . MAIL _ERROR , 'Error querying mail server IP' ) ;
// extract the relay token for auth
const env = safe . query ( data , 'Config.Env' , null ) ;
if ( ! env ) throw new BoxError ( BoxError . MAIL _ERROR , 'Error getting mail env' ) ;
const tmp = env . find ( function ( e ) { return e . indexOf ( 'CLOUDRON_RELAY_TOKEN' ) === 0 ; } ) ;
if ( ! tmp ) throw new BoxError ( BoxError . MAIL _ERROR , 'Error getting CLOUDRON_RELAY_TOKEN env var' ) ;
const relayToken = tmp . slice ( 'CLOUDRON_RELAY_TOKEN' . length + 1 ) ; // +1 for the = sign
if ( ! relayToken ) throw new BoxError ( BoxError . MAIL _ERROR , 'Error parsing CLOUDRON_RELAY_TOKEN' ) ;
return {
ip ,
port : constants . INTERNAL _SMTP _PORT ,
relayToken
} ;
2019-11-05 19:54:53 -08:00
}
2019-02-04 17:10:07 -08:00
function restartMail ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
if ( process . env . BOX _ENV === 'test' && ! process . env . TEST _CREATE _INFRA ) return callback ( ) ;
2021-08-17 15:45:57 -07:00
services . getServiceConfig ( 'mail' , async function ( error , serviceConfig ) {
2021-01-21 12:53:38 -08:00
if ( error ) return callback ( error ) ;
2021-05-05 13:14:48 -07:00
debug ( ` restartMail: restarting mail container with mailFqdn: ${ settings . mailFqdn ( ) } dashboardDomain: ${ settings . dashboardDomain ( ) } ` ) ;
2021-08-17 15:45:57 -07:00
[ error ] = await safe ( configureMail ( settings . mailFqdn ( ) , settings . dashboardDomain ( ) , serviceConfig ) ) ;
callback ( error ) ;
2021-01-21 12:53:38 -08:00
} ) ;
2019-02-04 17:10:07 -08:00
}
2021-07-15 09:50:11 -07:00
async function restartMailIfActivated ( ) {
const activated = await users . isActivated ( ) ;
2019-10-24 13:34:14 -07:00
2021-07-15 09:50:11 -07:00
if ( ! activated ) {
debug ( 'restartMailIfActivated: skipping restart of mail container since Cloudron is not activated yet' ) ;
return ; // not provisioned yet, do not restart container after dns setup
}
2018-09-26 10:26:33 -07:00
2021-07-15 09:50:11 -07:00
await util . promisify ( restartMail ) ( ) ;
2018-09-26 10:26:33 -07:00
}
2021-07-15 09:50:11 -07:00
async function handleCertChanged ( ) {
2019-03-04 18:11:07 -08:00
debug ( 'handleCertChanged: will restart if activated' ) ;
2021-07-15 09:50:11 -07:00
await restartMailIfActivated ( ) ;
2019-03-04 15:20:58 -08:00
}
2021-06-29 14:26:34 -07:00
async function getDomain ( domain ) {
2018-01-20 22:56:45 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
2018-01-20 18:56:17 -08:00
2021-06-29 14:26:34 -07:00
const result = await database . query ( ` SELECT ${ MAILDB _FIELDS } FROM mail WHERE domain = ? ` , [ domain ] ) ;
if ( result . length === 0 ) return null ;
2021-08-17 15:45:57 -07:00
return postProcessDomain ( result [ 0 ] ) ;
2018-01-20 18:56:17 -08:00
}
2021-06-29 14:26:34 -07:00
async function updateDomain ( domain , data ) {
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof data , 'object' ) ;
2018-01-24 11:33:09 -08:00
2021-06-29 14:26:34 -07:00
const args = [ ] ;
const fields = [ ] ;
for ( var k in data ) {
if ( k === 'catchAll' || k === 'banner' ) {
fields . push ( ` ${ k } Json = ? ` ) ;
args . push ( JSON . stringify ( data [ k ] ) ) ;
} else if ( k === 'relay' ) {
fields . push ( 'relayJson = ?' ) ;
args . push ( JSON . stringify ( data [ k ] ) ) ;
} else {
fields . push ( k + ' = ?' ) ;
args . push ( data [ k ] ) ;
}
}
args . push ( domain ) ;
2018-01-24 11:33:09 -08:00
2021-06-29 14:26:34 -07:00
const result = await database . query ( 'UPDATE mail SET ' + fields . join ( ', ' ) + ' WHERE domain=?' , args ) ;
if ( result . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'Mail domain not found' ) ;
}
async function listDomains ( ) {
const results = await database . query ( ` SELECT ${ MAILDB _FIELDS } FROM mail ORDER BY domain ` ) ;
2021-08-17 15:45:57 -07:00
results . forEach ( function ( result ) { postProcessDomain ( result ) ; } ) ;
2021-06-29 14:26:34 -07:00
return results ;
2018-01-24 11:33:09 -08:00
}
2018-01-25 13:48:53 -08:00
// https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
2019-01-31 15:08:14 -08:00
function txtRecordsWithSpf ( domain , mailFqdn , callback ) {
2018-02-06 23:04:27 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
2019-01-31 15:08:14 -08:00
assert . strictEqual ( typeof mailFqdn , 'string' ) ;
2018-01-25 13:48:53 -08:00
assert . strictEqual ( typeof callback , 'function' ) ;
2021-08-13 17:22:28 -07:00
dns . getDnsRecords ( '' , domain , 'TXT' , function ( error , txtRecords ) {
2020-12-07 00:02:56 -08:00
if ( error ) return callback ( error ) ;
2018-01-25 13:48:53 -08:00
debug ( 'txtRecordsWithSpf: current txt records - %j' , txtRecords ) ;
var i , matches , validSpf ;
for ( i = 0 ; i < txtRecords . length ; i ++ ) {
matches = txtRecords [ i ] . match ( /^("?v=spf1) / ) ; // DO backend may return without quotes
if ( matches === null ) continue ;
// this won't work if the entry is arbitrarily "split" across quoted strings
2019-01-31 15:08:14 -08:00
validSpf = txtRecords [ i ] . indexOf ( 'a:' + mailFqdn ) !== - 1 ;
2018-01-25 13:48:53 -08:00
break ; // there can only be one SPF record
}
if ( validSpf ) return callback ( null , null ) ;
if ( ! matches ) { // no spf record was found, create one
2019-01-31 15:08:14 -08:00
txtRecords . push ( '"v=spf1 a:' + mailFqdn + ' ~all"' ) ;
2018-01-25 13:48:53 -08:00
debug ( 'txtRecordsWithSpf: adding txt record' ) ;
} else { // just add ourself
2019-01-31 15:08:14 -08:00
txtRecords [ i ] = matches [ 1 ] + ' a:' + mailFqdn + txtRecords [ i ] . slice ( matches [ 1 ] . length ) ;
2018-01-25 13:48:53 -08:00
debug ( 'txtRecordsWithSpf: inserting txt record' ) ;
}
return callback ( null , txtRecords ) ;
} ) ;
}
2019-06-10 12:23:29 -07:00
function ensureDkimKeySync ( mailDomain ) {
assert . strictEqual ( typeof mailDomain , 'object' ) ;
2018-03-08 12:04:32 -08:00
2019-06-10 12:23:29 -07:00
const domain = mailDomain . domain ;
2018-03-08 17:26:07 -08:00
const dkimPath = path . join ( paths . MAIL _DATA _DIR , ` dkim/ ${ domain } ` ) ;
const dkimPrivateKeyFile = path . join ( dkimPath , 'private' ) ;
const dkimPublicKeyFile = path . join ( dkimPath , 'public' ) ;
const dkimSelectorFile = path . join ( dkimPath , 'selector' ) ;
2018-03-08 15:29:18 -08:00
2018-03-08 17:26:07 -08:00
if ( safe . fs . existsSync ( dkimPublicKeyFile ) &&
safe . fs . existsSync ( dkimPublicKeyFile ) &&
safe . fs . existsSync ( dkimPublicKeyFile ) ) {
debug ( ` Reusing existing DKIM keys for ${ domain } ` ) ;
return null ;
}
debug ( ` Generating new DKIM keys for ${ domain } ` ) ;
2018-03-08 12:04:32 -08:00
if ( ! safe . fs . mkdirSync ( dkimPath ) && safe . error . code !== 'EEXIST' ) {
debug ( 'Error creating dkim.' , safe . error ) ;
2019-10-24 13:34:14 -07:00
return new BoxError ( BoxError . FS _ERROR , safe . error ) ;
2018-03-08 12:04:32 -08:00
}
2020-04-17 10:29:12 -07:00
// https://www.unlocktheinbox.com/dkim-key-length-statistics/ and https://docs.aws.amazon.com/ses/latest/DeveloperGuide/send-email-authentication-dkim-easy.html for key size
2019-10-24 13:34:14 -07:00
if ( ! safe . child _process . execSync ( 'openssl genrsa -out ' + dkimPrivateKeyFile + ' 1024' ) ) return new BoxError ( BoxError . OPENSSL _ERROR , safe . error ) ;
if ( ! safe . child _process . execSync ( 'openssl rsa -in ' + dkimPrivateKeyFile + ' -out ' + dkimPublicKeyFile + ' -pubout -outform PEM' ) ) return new BoxError ( BoxError . OPENSSL _ERROR , safe . error ) ;
2018-03-08 12:04:32 -08:00
2019-10-24 13:34:14 -07:00
if ( ! safe . fs . writeFileSync ( dkimSelectorFile , mailDomain . dkimSelector , 'utf8' ) ) return new BoxError ( BoxError . FS _ERROR , safe . error ) ;
2018-03-08 12:04:32 -08:00
2019-06-06 14:37:57 -07:00
// if the 'yellowtent' user of OS and the 'cloudron' user of mail container don't match, the keys become inaccessible by mail code
2019-10-24 13:34:14 -07:00
if ( ! safe . fs . chmodSync ( dkimPrivateKeyFile , 0o644 ) ) return new BoxError ( BoxError . FS _ERROR , safe . error ) ;
2019-06-06 14:37:57 -07:00
2018-03-08 12:04:32 -08:00
return null ;
}
2018-01-25 14:51:07 -08:00
function readDkimPublicKeySync ( domain ) {
assert . strictEqual ( typeof domain , 'string' ) ;
var dkimPath = path . join ( paths . MAIL _DATA _DIR , ` dkim/ ${ domain } ` ) ;
var dkimPublicKeyFile = path . join ( dkimPath , 'public' ) ;
var publicKey = safe . fs . readFileSync ( dkimPublicKeyFile , 'utf8' ) ;
if ( publicKey === null ) {
debug ( 'Error reading dkim public key.' , safe . error ) ;
return null ;
}
// remove header, footer and new lines
publicKey = publicKey . split ( '\n' ) . slice ( 1 , - 2 ) . join ( '' ) ;
return publicKey ;
}
2019-02-04 20:51:26 -08:00
function upsertDnsRecords ( domain , mailFqdn , callback ) {
2018-01-25 13:48:53 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
2019-01-31 15:08:14 -08:00
assert . strictEqual ( typeof mailFqdn , 'string' ) ;
2018-01-25 13:48:53 -08:00
assert . strictEqual ( typeof callback , 'function' ) ;
2019-04-08 12:23:11 -07:00
debug ( ` upsertDnsRecords: updating mail dns records of domain ${ domain } and mail fqdn ${ mailFqdn } ` ) ;
2021-06-29 14:26:34 -07:00
const getDomainFunc = util . callbackify ( getDomain ) ;
getDomainFunc ( domain , function ( error , mailDomain ) {
2019-10-24 13:34:14 -07:00
if ( error ) return callback ( error ) ;
2021-06-29 14:26:34 -07:00
if ( ! mailDomain ) return callback ( new BoxError ( BoxError . NOT _FOUND , 'mail domain not found' ) ) ;
2018-07-25 10:44:59 -07:00
2019-06-10 12:23:29 -07:00
error = ensureDkimKeySync ( mailDomain ) ;
2018-07-25 10:44:59 -07:00
if ( error ) return callback ( error ) ;
2018-03-08 12:04:32 -08:00
2018-07-25 10:44:59 -07:00
if ( process . env . BOX _ENV === 'test' ) return callback ( ) ;
2018-03-08 16:02:13 -08:00
2021-02-24 09:02:32 -08:00
const dkimKey = readDkimPublicKeySync ( domain ) ;
2019-12-04 10:29:06 -08:00
if ( ! dkimKey ) return callback ( new BoxError ( BoxError . FS _ERROR , 'Failed to read dkim public key' ) ) ;
2018-01-25 13:48:53 -08:00
2018-07-25 10:44:59 -07:00
// t=s limits the domainkey to this domain and not it's subdomains
2021-02-24 09:02:32 -08:00
const dkimRecord = { subdomain : ` ${ mailDomain . dkimSelector } ._domainkey ` , domain : domain , type : 'TXT' , values : [ ` "v=DKIM1; t=s; p= ${ dkimKey } " ` ] } ;
2018-01-25 13:48:53 -08:00
2021-02-24 09:02:32 -08:00
let records = [ ] ;
2018-07-25 10:44:59 -07:00
records . push ( dkimRecord ) ;
2021-02-24 09:02:32 -08:00
if ( mailDomain . enabled ) records . push ( { subdomain : '' , domain : domain , type : 'MX' , values : [ '10 ' + mailFqdn + '.' ] } ) ;
2018-01-25 13:48:53 -08:00
2019-02-04 20:24:28 -08:00
txtRecordsWithSpf ( domain , mailFqdn , function ( error , txtRecords ) {
2018-07-25 10:44:59 -07:00
if ( error ) return callback ( error ) ;
2018-01-25 13:48:53 -08:00
2018-07-25 10:44:59 -07:00
if ( txtRecords ) records . push ( { subdomain : '' , domain : domain , type : 'TXT' , values : txtRecords } ) ;
2018-01-25 13:48:53 -08:00
2021-08-13 17:22:28 -07:00
dns . getDnsRecords ( '_dmarc' , domain , 'TXT' , function ( error , dmarcRecords ) { // only update dmarc if absent. this allows user to set email for reporting
2021-02-24 09:02:32 -08:00
if ( error ) return callback ( error ) ;
2018-01-25 13:48:53 -08:00
2021-02-24 09:02:32 -08:00
if ( dmarcRecords . length === 0 ) records . push ( { subdomain : '_dmarc' , domain : domain , type : 'TXT' , values : [ '"v=DMARC1; p=reject; pct=100"' ] } ) ;
2018-09-06 12:26:11 -07:00
2021-02-24 09:02:32 -08:00
debug ( 'upsertDnsRecords: will update %j' , records ) ;
2018-01-25 13:48:53 -08:00
2021-02-24 09:02:32 -08:00
async . mapSeries ( records , function ( record , iteratorCallback ) {
2021-08-13 17:22:28 -07:00
dns . upsertDnsRecords ( record . subdomain , record . domain , record . type , record . values , iteratorCallback ) ;
2021-02-24 09:02:32 -08:00
} , function ( error , changeIds ) {
if ( error ) {
debug ( ` upsertDnsRecords: failed to update: ${ error } ` ) ;
return callback ( error ) ;
}
debug ( 'upsertDnsRecords: records %j added with changeIds %j' , records , changeIds ) ;
callback ( null ) ;
} ) ;
2018-07-25 10:44:59 -07:00
} ) ;
2018-01-25 13:48:53 -08:00
} ) ;
} ) ;
}
2019-02-04 20:51:26 -08:00
function setDnsRecords ( domain , callback ) {
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2019-07-26 10:49:29 -07:00
upsertDnsRecords ( domain , settings . mailFqdn ( ) , callback ) ;
2019-02-04 20:51:26 -08:00
}
2020-08-15 23:17:47 -07:00
function getLocation ( callback ) {
2019-02-04 14:49:51 -08:00
assert . strictEqual ( typeof callback , 'function' ) ;
2020-08-15 23:17:47 -07:00
const domain = settings . mailDomain ( ) , fqdn = settings . mailFqdn ( ) ;
const subdomain = fqdn . substr ( 0 , fqdn . length - domain . length - 1 ) ;
2019-02-26 19:43:18 -08:00
2020-08-15 23:17:47 -07:00
callback ( null , { domain , subdomain } ) ;
}
function changeLocation ( auditSource , progressCallback , callback ) {
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
const fqdn = settings . mailFqdn ( ) , domain = settings . mailDomain ( ) ;
const subdomain = fqdn . substr ( 0 , fqdn . length - domain . length - 1 ) ;
2020-09-09 21:49:44 -07:00
let progress = 20 ;
2020-08-15 23:17:47 -07:00
progressCallback ( { percent : progress , message : ` Setting up DNS of certs of mail server ${ fqdn } ` } ) ;
cloudron . setupDnsAndCert ( subdomain , domain , auditSource , progressCallback , function ( error ) {
2019-10-24 20:48:38 -07:00
if ( error ) return callback ( error ) ;
2019-02-04 14:49:51 -08:00
2021-08-13 17:22:28 -07:00
domainsList ( function ( error , allDomains ) {
2019-10-24 13:34:14 -07:00
if ( error ) return callback ( error ) ;
2019-02-04 14:49:51 -08:00
2020-08-15 23:17:47 -07:00
async . eachOfSeries ( allDomains , function ( domainObject , idx , iteratorDone ) {
progressCallback ( { percent : progress , message : ` Updating DNS of ${ domainObject . domain } ` } ) ;
2020-09-09 21:49:44 -07:00
progress += Math . round ( 70 / allDomains . length ) ;
2020-08-15 23:17:47 -07:00
2020-12-07 00:02:56 -08:00
upsertDnsRecords ( domainObject . domain , fqdn , function ( error ) { // ignore any errors. we anyway report dns errors in status tab
progressCallback ( { percent : progress , message : ` Updated DNS of ${ domainObject . domain } : ${ error ? error . message : 'success' } ` } ) ;
iteratorDone ( ) ;
} ) ;
2021-07-15 09:50:11 -07:00
} , async function ( error ) {
2020-08-15 23:17:47 -07:00
if ( error ) return callback ( error ) ;
progressCallback ( { percent : 90 , message : 'Restarting mail server' } ) ;
2021-07-15 09:50:11 -07:00
[ error ] = await safe ( restartMailIfActivated ( ) ) ;
callback ( error ) ;
2020-08-15 23:17:47 -07:00
} ) ;
} ) ;
} ) ;
}
2021-08-19 13:24:38 -07:00
async function setLocation ( subdomain , domain , auditSource ) {
2020-08-15 23:17:47 -07:00
assert . strictEqual ( typeof subdomain , 'string' ) ;
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
2021-08-19 13:24:38 -07:00
const domainObject = await domains . get ( domain ) ;
const fqdn = dns . fqdn ( subdomain , domainObject ) ;
2020-08-15 23:17:47 -07:00
2021-08-19 13:24:38 -07:00
await settings . setMailLocation ( domain , fqdn ) ;
2020-08-15 23:17:47 -07:00
2021-08-19 13:24:38 -07:00
const taskId = await tasks . add ( tasks . TASK _CHANGE _MAIL _LOCATION , [ auditSource ] ) ;
tasks . startTask ( taskId , { } , NOOP _CALLBACK ) ;
await eventlog . add ( eventlog . ACTION _MAIL _LOCATION , auditSource , { subdomain , domain , taskId } ) ;
2020-08-15 23:17:47 -07:00
2021-08-19 13:24:38 -07:00
return taskId ;
2019-02-04 14:49:51 -08:00
}
2020-03-31 12:04:46 -07:00
function onDomainAdded ( domain , callback ) {
2018-01-24 21:15:58 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2020-08-15 23:17:47 -07:00
if ( ! settings . mailFqdn ( ) ) return callback ( ) ; // mail domain is not set yet (when provisioning)
2020-03-31 12:04:46 -07:00
async . series ( [
upsertDnsRecords . bind ( null , domain , settings . mailFqdn ( ) ) , // do this first to ensure DKIM keys
restartMailIfActivated
] , callback ) ;
2018-01-24 21:15:58 -08:00
}
2020-03-31 12:04:46 -07:00
function onDomainRemoved ( domain , callback ) {
2018-01-24 21:15:58 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2020-03-31 12:04:46 -07:00
restartMail ( callback ) ;
2018-01-24 21:15:58 -08:00
}
2021-06-29 14:26:34 -07:00
async function clearDomains ( ) {
await database . query ( 'DELETE FROM mail' , [ ] ) ;
2018-12-07 14:35:04 -08:00
}
2019-02-15 10:55:15 -08:00
// remove all fields that should never be sent out via REST API
function removePrivateFields ( domain ) {
2020-08-23 14:33:58 -07:00
let result = _ . pick ( domain , 'domain' , 'enabled' , 'mailFromValidation' , 'catchAll' , 'relay' , 'banner' ) ;
2019-02-15 11:44:33 -08:00
if ( result . relay . provider !== 'cloudron-smtp' ) {
if ( result . relay . username === result . relay . password ) result . relay . username = constants . SECRET _PLACEHOLDER ;
result . relay . password = constants . SECRET _PLACEHOLDER ;
}
2019-02-15 10:55:15 -08:00
return result ;
}
2021-06-29 14:26:34 -07:00
async function setMailFromValidation ( domain , enabled ) {
2018-01-20 23:17:39 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
2018-01-20 18:56:17 -08:00
assert . strictEqual ( typeof enabled , 'boolean' ) ;
2021-06-29 14:26:34 -07:00
await updateDomain ( domain , { mailFromValidation : enabled } ) ;
2018-01-20 18:56:17 -08:00
2021-06-29 14:26:34 -07:00
restartMail ( NOOP _CALLBACK ) ; // have to restart mail container since haraka cannot watch symlinked config files (mail.ini)
2018-01-20 18:56:17 -08:00
}
2021-06-29 14:26:34 -07:00
async function setBanner ( domain , banner ) {
2020-08-23 14:33:58 -07:00
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof banner , 'object' ) ;
2021-06-29 14:26:34 -07:00
await updateDomain ( domain , { banner } ) ;
2020-08-23 14:33:58 -07:00
2021-06-29 14:26:34 -07:00
restartMail ( NOOP _CALLBACK ) ;
2020-08-23 14:33:58 -07:00
}
2021-06-29 14:26:34 -07:00
async function setCatchAllAddress ( domain , addresses ) {
2018-01-20 23:17:39 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
2018-04-12 12:35:56 +02:00
assert ( Array . isArray ( addresses ) ) ;
2018-01-20 18:56:17 -08:00
2021-06-29 14:26:34 -07:00
await updateDomain ( domain , { catchAll : addresses } ) ;
2018-01-20 18:56:17 -08:00
2021-06-29 14:26:34 -07:00
restartMail ( NOOP _CALLBACK ) ; // have to restart mail container since haraka cannot watch symlinked config files (mail.ini)
2018-01-20 18:56:17 -08:00
}
2021-06-29 14:26:34 -07:00
async function setMailRelay ( domain , relay , options ) {
2018-01-20 23:17:39 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
2018-01-20 18:56:17 -08:00
assert . strictEqual ( typeof relay , 'object' ) ;
2021-06-29 14:26:34 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
2018-01-20 18:56:17 -08:00
2021-06-29 14:26:34 -07:00
const result = await getDomain ( domain ) ;
if ( ! domain ) throw new BoxError ( BoxError . NOT _FOUND , 'Mail domain not found' ) ;
2018-01-20 18:56:17 -08:00
2021-06-29 14:26:34 -07:00
// inject current username/password
if ( result . relay . provider === relay . provider ) {
if ( relay . username === constants . SECRET _PLACEHOLDER ) relay . username = result . relay . username ;
if ( relay . password === constants . SECRET _PLACEHOLDER ) relay . password = result . relay . password ;
}
2018-01-20 18:56:17 -08:00
2021-06-29 14:26:34 -07:00
if ( ! options . skipVerify ) await verifyRelay ( relay ) ;
2019-02-15 10:55:15 -08:00
2021-06-29 14:26:34 -07:00
await updateDomain ( domain , { relay : relay } ) ;
2019-02-15 10:55:15 -08:00
2021-06-29 14:26:34 -07:00
restartMail ( NOOP _CALLBACK ) ;
2018-01-20 18:56:17 -08:00
}
2021-06-29 14:26:34 -07:00
async function setMailEnabled ( domain , enabled , auditSource ) {
2018-01-20 23:17:39 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof enabled , 'boolean' ) ;
2018-11-09 18:51:58 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2018-01-24 11:33:09 -08:00
2021-06-29 14:26:34 -07:00
await updateDomain ( domain , { enabled : enabled } ) ;
2018-01-24 11:33:09 -08:00
2021-06-29 14:26:34 -07:00
restartMail ( NOOP _CALLBACK ) ;
2018-11-09 18:51:58 -08:00
2021-06-29 14:26:34 -07:00
await eventlog . add ( enabled ? eventlog . ACTION _MAIL _ENABLED : eventlog . ACTION _MAIL _DISABLED , auditSource , { domain } ) ;
2018-07-25 10:29:26 -07:00
}
2021-08-17 15:45:57 -07:00
async function sendTestMail ( domain , to ) {
2018-01-23 16:10:23 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
2018-02-03 18:27:55 -08:00
assert . strictEqual ( typeof to , 'string' ) ;
2018-01-23 16:10:23 -08:00
2021-08-17 15:45:57 -07:00
const result = await getDomain ( domain ) ;
if ( ! result ) throw new BoxError ( BoxError . NOT _FOUND , 'mail domain not found' ) ;
2018-01-23 16:10:23 -08:00
2021-08-19 12:32:23 -07:00
await mailer . sendTestMail ( result . domain ) ;
2018-01-24 13:11:35 +01:00
}
2021-08-17 15:45:57 -07:00
async function listMailboxes ( domain , search , page , perPage ) {
2018-01-24 13:11:35 +01:00
assert . strictEqual ( typeof domain , 'string' ) ;
2020-07-05 11:23:53 -07:00
assert ( typeof search === 'string' || search === null ) ;
2019-10-22 10:11:35 -07:00
assert . strictEqual ( typeof page , 'number' ) ;
assert . strictEqual ( typeof perPage , 'number' ) ;
2018-01-24 13:11:35 +01:00
2021-08-17 15:45:57 -07:00
const escapedSearch = mysql . escape ( '%' + search + '%' ) ; // this also quotes the string
const searchQuery = search ? ` HAVING (name LIKE ${ escapedSearch } OR aliasNames LIKE ${ escapedSearch } OR aliasDomains LIKE ${ escapedSearch } ) ` : '' ; // having instead of where because of aggregated columns use
2018-01-24 13:11:35 +01:00
2021-08-17 15:45:57 -07:00
const query = 'SELECT m1.name AS name, m1.domain AS domain, m1.ownerId AS ownerId, m1.ownerType as ownerType, m1.active as active, JSON_ARRAYAGG(m2.name) AS aliasNames, JSON_ARRAYAGG(m2.domain) AS aliasDomains '
+ ` FROM (SELECT * FROM mailboxes WHERE type=' ${ exports . TYPE _MAILBOX } ') AS m1 `
+ ` LEFT JOIN (SELECT * FROM mailboxes WHERE type=' ${ exports . TYPE _ALIAS } ') AS m2 `
+ ' ON m1.name=m2.aliasName AND m1.domain=m2.aliasDomain AND m1.ownerId=m2.ownerId'
+ ' WHERE m1.domain = ?'
+ ' GROUP BY m1.name, m1.domain, m1.ownerId'
+ searchQuery
+ ' ORDER BY name LIMIT ?,?' ;
const results = await database . query ( query , [ domain , ( page - 1 ) * perPage , perPage ] ) ;
results . forEach ( postProcessMailbox ) ;
results . forEach ( postProcessAliases ) ;
return results ;
2018-01-24 13:11:35 +01:00
}
2021-08-17 15:45:57 -07:00
async function listAllMailboxes ( page , perPage ) {
assert . strictEqual ( typeof page , 'number' ) ;
assert . strictEqual ( typeof perPage , 'number' ) ;
2020-07-15 15:33:53 -07:00
2021-08-17 15:45:57 -07:00
const query = 'SELECT m1.name AS name, m1.domain AS domain, m1.ownerId AS ownerId, m1.ownerType as ownerType, m1.active as active, JSON_ARRAYAGG(m2.name) AS aliasNames, JSON_ARRAYAGG(m2.domain) AS aliasDomains '
+ ` FROM (SELECT * FROM mailboxes WHERE type=' ${ exports . TYPE _MAILBOX } ') AS m1 `
+ ` LEFT JOIN (SELECT * FROM mailboxes WHERE type=' ${ exports . TYPE _ALIAS } ') AS m2 `
+ ' ON m1.name=m2.aliasName AND m1.domain=m2.aliasDomain AND m1.ownerId=m2.ownerId'
+ ' GROUP BY m1.name, m1.domain, m1.ownerId'
+ ' ORDER BY name LIMIT ?,?' ;
2020-07-15 15:33:53 -07:00
2021-08-17 15:45:57 -07:00
const results = await database . query ( query , [ ( page - 1 ) * perPage , perPage ] ) ;
results . forEach ( postProcessMailbox ) ;
results . forEach ( postProcessAliases ) ;
return results ;
2020-07-15 15:33:53 -07:00
}
2021-08-17 15:45:57 -07:00
async function getMailboxCount ( domain ) {
2018-02-11 01:18:29 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
2021-08-17 15:45:57 -07:00
const results = await database . query ( 'SELECT COUNT(*) AS total FROM mailboxes WHERE type = ? AND domain = ?' , [ exports . TYPE _MAILBOX , domain ] ) ;
2018-02-11 01:18:29 -08:00
2021-08-17 15:45:57 -07:00
return results [ 0 ] . total ;
}
async function delByDomain ( domain ) {
assert . strictEqual ( typeof domain , 'string' ) ;
await database . query ( 'DELETE FROM mailboxes WHERE domain = ?' , [ domain ] ) ;
2018-02-11 01:18:29 -08:00
}
2021-08-17 15:45:57 -07:00
async function get ( name , domain ) {
2018-04-03 12:18:26 -07:00
assert . strictEqual ( typeof name , 'string' ) ;
2018-01-24 13:11:35 +01:00
assert . strictEqual ( typeof domain , 'string' ) ;
2021-08-17 15:45:57 -07:00
const results = await database . query ( 'SELECT ' + MAILBOX _FIELDS + ' FROM mailboxes WHERE name = ? AND domain = ?' , [ name , domain ] ) ;
if ( results . length === 0 ) return null ;
2018-01-24 13:11:35 +01:00
2021-08-17 15:45:57 -07:00
return postProcessMailbox ( results [ 0 ] ) ;
2018-01-24 13:11:35 +01:00
}
2021-08-17 15:45:57 -07:00
async function getMailbox ( name , domain ) {
assert . strictEqual ( typeof name , 'string' ) ;
assert . strictEqual ( typeof domain , 'string' ) ;
const results = await database . query ( 'SELECT ' + MAILBOX _FIELDS + ' FROM mailboxes WHERE name = ? AND type = ? AND domain = ?' , [ name , exports . TYPE _MAILBOX , domain ] ) ;
if ( results . length === 0 ) return null ;
return postProcessMailbox ( results [ 0 ] ) ;
}
async function addMailbox ( name , domain , data , auditSource ) {
2018-04-03 12:18:26 -07:00
assert . strictEqual ( typeof name , 'string' ) ;
2018-01-24 13:11:35 +01:00
assert . strictEqual ( typeof domain , 'string' ) ;
2021-04-14 22:37:01 -07:00
assert . strictEqual ( typeof data , 'object' ) ;
2018-11-09 18:45:44 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2018-01-24 13:11:35 +01:00
2021-04-14 22:37:01 -07:00
const { ownerId , ownerType , active } = data ;
assert . strictEqual ( typeof ownerId , 'string' ) ;
assert . strictEqual ( typeof ownerType , 'string' ) ;
assert . strictEqual ( typeof active , 'boolean' ) ;
2018-04-05 16:07:51 -07:00
name = name . toLowerCase ( ) ;
2018-01-24 13:11:35 +01:00
2021-08-17 15:45:57 -07:00
let error = validateName ( name ) ;
if ( error ) throw error ;
2020-11-12 23:25:33 -08:00
2021-08-17 15:45:57 -07:00
if ( ownerType !== exports . OWNERTYPE _USER && ownerType !== exports . OWNERTYPE _GROUP ) throw new BoxError ( BoxError . BAD _FIELD , 'bad owner type' ) ;
2018-04-05 16:07:51 -07:00
2021-08-17 15:45:57 -07:00
[ error ] = await safe ( database . query ( 'INSERT INTO mailboxes (name, type, domain, ownerId, ownerType, active) VALUES (?, ?, ?, ?, ?, ?)' , [ name , exports . TYPE _MAILBOX , domain , ownerId , ownerType , active ] ) ) ;
if ( error && error . code === 'ER_DUP_ENTRY' ) throw new BoxError ( BoxError . ALREADY _EXISTS , 'mailbox already exists' ) ;
if ( error ) throw error ;
2018-11-09 18:45:44 -08:00
2021-08-17 15:45:57 -07:00
eventlog . add ( eventlog . ACTION _MAIL _MAILBOX _ADD , auditSource , { name , domain , ownerId , ownerType , active } ) ;
2018-01-24 13:11:35 +01:00
}
2021-08-17 15:45:57 -07:00
async function updateMailbox ( name , domain , data , auditSource ) {
2018-04-03 14:12:43 -07:00
assert . strictEqual ( typeof name , 'string' ) ;
assert . strictEqual ( typeof domain , 'string' ) ;
2021-04-14 22:37:01 -07:00
assert . strictEqual ( typeof data , 'object' ) ;
2020-01-24 16:55:41 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2018-04-03 14:12:43 -07:00
2021-04-14 22:37:01 -07:00
const { ownerId , ownerType , active } = data ;
assert . strictEqual ( typeof ownerId , 'string' ) ;
assert . strictEqual ( typeof ownerType , 'string' ) ;
assert . strictEqual ( typeof active , 'boolean' ) ;
2018-04-05 16:07:51 -07:00
name = name . toLowerCase ( ) ;
2021-08-17 15:45:57 -07:00
if ( ownerType !== exports . OWNERTYPE _USER && ownerType !== exports . OWNERTYPE _GROUP ) throw new BoxError ( BoxError . BAD _FIELD , 'bad owner type' ) ;
2020-11-12 23:25:33 -08:00
2021-08-17 15:45:57 -07:00
const mailbox = await getMailbox ( name , domain ) ;
if ( ! mailbox ) throw new BoxError ( BoxError . NOT _FOUND , 'No such mailbox' ) ;
2020-01-24 16:55:41 -08:00
2021-08-17 15:45:57 -07:00
const result = await database . query ( 'UPDATE mailboxes SET ownerId = ?, ownerType = ?, active = ? WHERE name = ? AND domain = ?' , [ ownerId , ownerType , active , name , domain ] ) ;
if ( result . affectedRows === 0 ) throw new BoxError ( BoxError . NOT _FOUND , 'Mailbox not found' ) ;
2020-01-24 16:55:41 -08:00
2021-08-17 15:45:57 -07:00
eventlog . add ( eventlog . ACTION _MAIL _MAILBOX _UPDATE , auditSource , { name , domain , oldUserId : mailbox . userId , ownerId , ownerType , active } ) ;
2018-04-03 14:12:43 -07:00
}
2020-12-02 00:24:15 -08:00
function removeSolrIndex ( mailbox , callback ) {
assert . strictEqual ( typeof mailbox , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2021-01-21 11:31:35 -08:00
services . getContainerDetails ( 'mail' , 'CLOUDRON_MAIL_TOKEN' , function ( error , addonDetails ) {
2020-12-02 00:24:15 -08:00
if ( error ) return callback ( error ) ;
request . post ( ` https:// ${ addonDetails . ip } :3000/solr_delete_index?access_token= ${ addonDetails . token } ` , { timeout : 2000 , rejectUnauthorized : false , json : { mailbox } } , function ( error , response ) {
if ( error ) return callback ( error ) ;
if ( response . statusCode !== 200 ) return callback ( new Error ( ` Error removing solr index - ${ response . statusCode } ${ JSON . stringify ( response . body ) } ` ) ) ;
callback ( null ) ;
} ) ;
} ) ;
}
2021-08-17 15:45:57 -07:00
async function delMailbox ( name , domain , options , auditSource ) {
2018-01-24 13:11:35 +01:00
assert . strictEqual ( typeof domain , 'string' ) ;
2018-04-03 12:18:26 -07:00
assert . strictEqual ( typeof name , 'string' ) ;
2020-07-27 22:26:10 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
2018-11-09 18:45:44 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2018-01-24 13:11:35 +01:00
2020-12-02 00:24:15 -08:00
const mailbox = ` ${ name } @ ${ domain } ` ;
2021-08-17 15:45:57 -07:00
if ( options . deleteMails ) {
const [ error ] = await safe ( shell . promises . sudo ( 'removeMailbox' , [ REMOVE _MAILBOX _CMD , mailbox ] , { } ) ) ;
if ( error ) throw new BoxError ( BoxError . FS _ERROR , ` Error removing mailbox: ${ error . message } ` ) ;
}
2020-07-27 22:26:10 -07:00
2021-08-17 15:45:57 -07:00
// deletes aliases as well
const result = await database . query ( 'DELETE FROM mailboxes WHERE ((name=? AND domain=?) OR (aliasName = ? AND aliasDomain=?))' , [ name , domain , name , domain ] ) ;
if ( result . affectedRows === 0 ) throw new BoxError ( BoxError . NOT _FOUND , 'Mailbox not found' ) ;
2020-07-27 22:26:10 -07:00
2021-08-17 15:45:57 -07:00
removeSolrIndex ( mailbox , NOOP _CALLBACK ) ;
eventlog . add ( eventlog . ACTION _MAIL _MAILBOX _REMOVE , auditSource , { name , domain } ) ;
2018-01-24 13:11:35 +01:00
}
2018-01-25 18:03:02 +01:00
2021-08-17 15:45:57 -07:00
async function getAlias ( name , domain ) {
2018-04-03 12:18:26 -07:00
assert . strictEqual ( typeof name , 'string' ) ;
2018-01-25 18:03:02 +01:00
assert . strictEqual ( typeof domain , 'string' ) ;
2021-08-17 15:45:57 -07:00
const results = await database . query ( ` SELECT ${ MAILBOX _FIELDS } FROM mailboxes WHERE name = ? AND type = ? AND domain = ? ` , [ name , exports . TYPE _ALIAS , domain ] ) ;
if ( results . length === 0 ) return null ;
2018-01-25 18:03:02 +01:00
2021-08-17 15:45:57 -07:00
results . forEach ( function ( result ) { postProcessMailbox ( result ) ; } ) ;
2018-01-25 18:03:02 +01:00
2021-08-17 15:45:57 -07:00
return results [ 0 ] ;
}
async function getAliases ( name , domain ) {
assert . strictEqual ( typeof name , 'string' ) ;
assert . strictEqual ( typeof domain , 'string' ) ;
const result = await getMailbox ( name , domain ) ; // check if mailbox exists
if ( result === null ) throw new BoxError ( BoxError . NOT _FOUND , 'No such mailbox' ) ;
return await database . query ( 'SELECT name, domain FROM mailboxes WHERE type = ? AND aliasName = ? AND aliasDomain = ? ORDER BY name' , [ exports . TYPE _ALIAS , name , domain ] ) ;
2018-01-25 18:03:02 +01:00
}
2021-08-17 15:45:57 -07:00
async function setAliases ( name , domain , aliases ) {
2018-04-03 12:18:26 -07:00
assert . strictEqual ( typeof name , 'string' ) ;
2018-01-25 18:03:02 +01:00
assert . strictEqual ( typeof domain , 'string' ) ;
assert ( Array . isArray ( aliases ) ) ;
2021-08-17 15:45:57 -07:00
for ( let i = 0 ; i < aliases . length ; i ++ ) {
2020-04-19 18:44:16 -07:00
let name = aliases [ i ] . name . toLowerCase ( ) ;
let domain = aliases [ i ] . domain . toLowerCase ( ) ;
2018-01-25 18:03:02 +01:00
2020-04-19 18:44:16 -07:00
let error = validateName ( name ) ;
2021-08-17 15:45:57 -07:00
if ( error ) throw error ;
2020-04-19 18:44:16 -07:00
2021-08-17 15:45:57 -07:00
if ( ! validator . isEmail ( ` ${ name } @ ${ domain } ` ) ) throw new BoxError ( BoxError . BAD _FIELD , ` Invalid email: ${ name } @ ${ domain } ` ) ;
2020-04-19 18:44:16 -07:00
aliases [ i ] = { name , domain } ;
2018-01-25 18:03:02 +01:00
}
2021-08-17 15:45:57 -07:00
const results = await database . query ( 'SELECT ' + MAILBOX _FIELDS + ' FROM mailboxes WHERE name = ? AND domain = ?' , [ name , domain ] ) ;
if ( results . length === 0 ) throw new BoxError ( BoxError . NOT _FOUND , 'Mailbox not found' ) ;
let queries = [ ] ;
// clear existing aliases
queries . push ( { query : 'DELETE FROM mailboxes WHERE aliasName = ? AND aliasDomain = ? AND type = ?' , args : [ name , domain , exports . TYPE _ALIAS ] } ) ;
aliases . forEach ( function ( alias ) {
queries . push ( { query : 'INSERT INTO mailboxes (name, domain, type, aliasName, aliasDomain, ownerId, ownerType) VALUES (?, ?, ?, ?, ?, ?, ?)' ,
args : [ alias . name , alias . domain , exports . TYPE _ALIAS , name , domain , results [ 0 ] . ownerId , results [ 0 ] . ownerType ] } ) ;
2018-01-25 18:03:02 +01:00
} ) ;
2021-08-17 15:45:57 -07:00
const [ error ] = await safe ( database . transaction ( queries ) ) ;
if ( error && error . code === 'ER_DUP_ENTRY' && error . message . indexOf ( 'mailboxes_name_domain_unique_index' ) !== - 1 ) {
const aliasMatch = error . message . match ( new RegExp ( ` ^ER_DUP_ENTRY: Duplicate entry '(.*)- ${ domain } ' for key 'mailboxes_name_domain_unique_index' $ ` ) ) ;
if ( ! aliasMatch ) throw new BoxError ( BoxError . ALREADY _EXISTS , error . message ) ;
throw new BoxError ( BoxError . ALREADY _EXISTS , ` Mailbox, mailinglist or alias for ${ aliasMatch [ 1 ] } already exists ` ) ;
}
if ( error ) throw error ;
2018-01-25 18:03:02 +01:00
}
2018-01-26 10:22:50 +01:00
2021-08-17 15:45:57 -07:00
async function getLists ( domain , search , page , perPage ) {
2018-01-26 10:22:50 +01:00
assert . strictEqual ( typeof domain , 'string' ) ;
2020-07-05 11:23:53 -07:00
assert ( typeof search === 'string' || search === null ) ;
2020-07-05 10:36:17 -07:00
assert . strictEqual ( typeof page , 'number' ) ;
assert . strictEqual ( typeof perPage , 'number' ) ;
2018-01-26 10:22:50 +01:00
2021-08-17 15:45:57 -07:00
let query = ` SELECT ${ MAILBOX _FIELDS } FROM mailboxes WHERE type = ? AND domain = ? ` ;
if ( search ) query += ' AND (name LIKE ' + mysql . escape ( '%' + search + '%' ) + ' OR membersJson LIKE ' + mysql . escape ( '%' + search + '%' ) + ')' ;
2018-01-26 10:22:50 +01:00
2021-08-17 15:45:57 -07:00
query += 'ORDER BY name LIMIT ?,?' ;
const results = await database . query ( query , [ exports . TYPE _LIST , domain , ( page - 1 ) * perPage , perPage ] ) ;
results . forEach ( function ( result ) { postProcessMailbox ( result ) ; } ) ;
return results ;
2018-01-26 10:22:50 +01:00
}
2021-08-17 15:45:57 -07:00
async function getList ( name , domain ) {
2020-01-24 16:54:14 -08:00
assert . strictEqual ( typeof name , 'string' ) ;
2018-01-26 10:22:50 +01:00
assert . strictEqual ( typeof domain , 'string' ) ;
2021-08-17 15:45:57 -07:00
const results = await database . query ( 'SELECT ' + MAILBOX _FIELDS + ' FROM mailboxes WHERE type = ? AND name = ? AND domain = ?' , [ exports . TYPE _LIST , name , domain ] ) ;
if ( results . length === 0 ) return null ;
2018-01-26 10:22:50 +01:00
2021-08-17 15:45:57 -07:00
return postProcessMailbox ( results [ 0 ] ) ;
2018-01-26 10:22:50 +01:00
}
2021-08-17 15:45:57 -07:00
async function addList ( name , domain , data , auditSource ) {
2018-01-26 10:22:50 +01:00
assert . strictEqual ( typeof domain , 'string' ) ;
2018-04-05 16:07:51 -07:00
assert . strictEqual ( typeof name , 'string' ) ;
2021-04-14 22:37:01 -07:00
assert . strictEqual ( typeof data , 'object' ) ;
2018-11-09 18:49:55 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2018-01-26 10:22:50 +01:00
2021-04-14 22:37:01 -07:00
const { members , membersOnly , active } = data ;
assert ( Array . isArray ( members ) ) ;
assert . strictEqual ( typeof membersOnly , 'boolean' ) ;
assert . strictEqual ( typeof active , 'boolean' ) ;
2018-04-05 16:07:51 -07:00
name = name . toLowerCase ( ) ;
2021-08-17 15:45:57 -07:00
let error = validateName ( name ) ;
if ( error ) throw error ;
2018-04-05 16:07:51 -07:00
2021-08-17 15:45:57 -07:00
for ( let i = 0 ; i < members . length ; i ++ ) {
if ( ! validator . isEmail ( members [ i ] ) ) throw new BoxError ( BoxError . BAD _FIELD , 'Invalid mail member: ' + members [ i ] ) ;
2018-04-05 16:07:51 -07:00
}
2021-08-17 15:45:57 -07:00
[ error ] = await safe ( database . query ( 'INSERT INTO mailboxes (name, type, domain, ownerId, ownerType, membersJson, membersOnly, active) VALUES (?, ?, ?, ?, ?, ?, ?, ?)' , [ name , exports . TYPE _LIST , domain , 'admin' , 'user' , JSON . stringify ( members ) , membersOnly , active ] ) ) ;
if ( error && error . code === 'ER_DUP_ENTRY' ) throw new BoxError ( BoxError . ALREADY _EXISTS , 'mailbox already exists' ) ;
if ( error ) throw error ;
2018-11-09 18:49:55 -08:00
2021-08-17 15:45:57 -07:00
eventlog . add ( eventlog . ACTION _MAIL _LIST _ADD , auditSource , { name , domain , members , membersOnly , active } ) ;
2018-01-26 10:22:50 +01:00
}
2021-08-17 15:45:57 -07:00
async function updateList ( name , domain , data , auditSource ) {
2018-04-03 14:12:43 -07:00
assert . strictEqual ( typeof name , 'string' ) ;
assert . strictEqual ( typeof domain , 'string' ) ;
2021-04-14 22:37:01 -07:00
assert . strictEqual ( typeof data , 'object' ) ;
2020-01-24 16:55:41 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2018-04-03 14:12:43 -07:00
2021-04-14 22:37:01 -07:00
const { members , membersOnly , active } = data ;
assert ( Array . isArray ( members ) ) ;
assert . strictEqual ( typeof membersOnly , 'boolean' ) ;
assert . strictEqual ( typeof active , 'boolean' ) ;
2018-04-05 16:07:51 -07:00
name = name . toLowerCase ( ) ;
2021-08-17 15:45:57 -07:00
let error = validateName ( name ) ;
if ( error ) throw error ;
2018-04-05 16:07:51 -07:00
2021-08-17 15:45:57 -07:00
for ( let i = 0 ; i < members . length ; i ++ ) {
if ( ! validator . isEmail ( members [ i ] ) ) throw new BoxError ( BoxError . BAD _FIELD , 'Invalid email: ' + members [ i ] ) ;
2018-04-05 16:07:51 -07:00
}
2021-08-17 15:45:57 -07:00
const result = await database . query ( 'UPDATE mailboxes SET membersJson = ?, membersOnly = ?, active = ? WHERE name = ? AND domain = ?' ,
[ JSON . stringify ( members ) , membersOnly , active , name , domain ] ) ;
if ( result . affectedRows === 0 ) throw new BoxError ( BoxError . NOT _FOUND , 'Mailbox not found' ) ;
2020-01-24 16:55:41 -08:00
2021-08-17 15:45:57 -07:00
eventlog . add ( eventlog . ACTION _MAIL _MAILBOX _UPDATE , auditSource , { name , domain , oldMembers : result . members , members , membersOnly , active } ) ;
2018-04-03 14:12:43 -07:00
}
2021-08-17 15:45:57 -07:00
async function delList ( name , domain , auditSource ) {
2018-11-09 18:49:55 -08:00
assert . strictEqual ( typeof name , 'string' ) ;
2018-01-26 10:22:50 +01:00
assert . strictEqual ( typeof domain , 'string' ) ;
2018-11-09 18:49:55 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2018-01-26 10:22:50 +01:00
2021-08-17 15:45:57 -07:00
// deletes aliases as well
const result = await database . query ( 'DELETE FROM mailboxes WHERE ((name=? AND domain=?) OR (aliasName = ? AND aliasDomain=?))' , [ name , domain , name , domain ] ) ;
if ( result . affectedRows === 0 ) throw new BoxError ( BoxError . NOT _FOUND , 'Mailbox not found' ) ;
2018-11-09 18:49:55 -08:00
2021-08-17 15:45:57 -07:00
eventlog . add ( eventlog . ACTION _MAIL _LIST _REMOVE , auditSource , { name , domain } ) ;
2018-01-26 10:22:50 +01:00
}
2019-11-06 16:45:44 -08:00
2020-04-19 18:44:16 -07:00
// resolves the members of a list. i.e the lists and aliases
2021-08-17 15:45:57 -07:00
async function resolveList ( listName , listDomain ) {
2019-11-06 16:45:44 -08:00
assert . strictEqual ( typeof listName , 'string' ) ;
assert . strictEqual ( typeof listDomain , 'string' ) ;
2021-06-29 14:26:34 -07:00
2021-08-17 15:45:57 -07:00
const mailDomains = await listDomains ( ) ;
const mailInDomains = mailDomains . filter ( function ( d ) { return d . enabled ; } ) . map ( function ( d ) { return d . domain ; } ) . join ( ',' ) ;
2019-11-06 16:45:44 -08:00
2021-08-17 15:45:57 -07:00
const list = await getList ( listName , listDomain ) ;
if ( ! list ) throw new BoxError ( BoxError . NOT _FOUND , 'List not found' ) ;
2019-11-06 16:45:44 -08:00
2021-08-17 15:45:57 -07:00
let resolvedMembers = [ ] , toResolve = list . members . slice ( ) , visited = [ ] ; // slice creates a copy of array
2019-11-06 16:45:44 -08:00
2021-08-17 15:45:57 -07:00
while ( toResolve . length != 0 ) {
const toProcess = toResolve . shift ( ) ;
const parts = toProcess . split ( '@' ) ;
const memberName = parts [ 0 ] . split ( '+' ) [ 0 ] , memberDomain = parts [ 1 ] ;
2019-11-06 16:45:44 -08:00
2021-08-17 15:45:57 -07:00
if ( ! mailInDomains . includes ( memberDomain ) ) { // external domain
resolvedMembers . push ( toProcess ) ;
continue ;
}
2019-11-06 16:45:44 -08:00
2021-08-17 15:45:57 -07:00
const member = ` ${ memberName } @ ${ memberDomain } ` ; // cleaned up without any '+' subaddress
if ( visited . includes ( member ) ) {
debug ( ` resolveList: list ${ listName } @ ${ listDomain } has a recursion at member ${ member } ` ) ;
continue ;
}
visited . push ( member ) ;
2019-11-06 16:45:44 -08:00
2021-08-17 15:45:57 -07:00
const entry = await get ( memberName , memberDomain ) ;
if ( ! entry ) { // let it bounce
resolvedMembers . push ( member ) ;
continue ;
}
2019-11-06 16:45:44 -08:00
2021-08-17 15:45:57 -07:00
if ( entry . type === exports . TYPE _MAILBOX ) { // concrete mailbox
resolvedMembers . push ( member ) ;
} else if ( entry . type === exports . TYPE _ALIAS ) { // resolve aliases
toResolve = toResolve . concat ( ` ${ entry . aliasName } @ ${ entry . aliasDomain } ` ) ;
} else { // resolve list members
toResolve = toResolve . concat ( entry . members ) ;
}
}
2019-11-06 16:45:44 -08:00
2021-08-17 15:45:57 -07:00
return { resolvedMembers , list } ;
2019-11-20 14:11:52 -08:00
}