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
2021-10-11 19:51:29 -07:00
generateDkimKey ,
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 ,
2022-05-31 17:53:09 -07:00
validateDisplayName ,
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
2021-09-26 22:48:14 -07:00
startMail ,
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-12-02 22:14:41 -08:00
OWNERTYPE _APP : 'app' ,
2020-11-12 23:25:33 -08:00
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
_updateDomain : updateDomain
2017-06-28 17:06:12 -05:00
} ;
2021-01-21 11:31:35 -08:00
const assert = require ( 'assert' ) ,
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-10-11 19:51:29 -07:00
crypto = require ( 'crypto' ) ,
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' ) ,
2022-02-03 16:15:14 -08:00
dig = require ( './dig.js' ) ,
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' ) ,
2021-10-11 19:51:29 -07:00
os = require ( 'os' ) ,
2018-01-24 21:30:06 -08:00
path = require ( 'path' ) ,
2018-01-20 18:56:17 -08:00
paths = require ( './paths.js' ) ,
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' ) ,
2021-08-25 19:41:46 -07:00
superagent = require ( 'superagent' ) ,
2017-06-28 17:06:12 -05:00
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 REMOVE _MAILBOX _CMD = path . join ( _ _dirname , 'scripts/rmmailbox.sh' ) ;
2018-01-20 18:56:17 -08:00
2021-12-02 22:14:41 -08:00
const OWNERTYPES = [ exports . OWNERTYPE _USER , exports . OWNERTYPE _GROUP , exports . OWNERTYPE _APP ] ;
2021-10-26 11:11:07 -07:00
// if you add a field here, listMailboxes has to be updated
2021-10-08 10:15:48 -07:00
const MAILBOX _FIELDS = [ 'name' , 'type' , 'ownerId' , 'ownerType' , 'aliasName' , 'aliasDomain' , 'creationTime' , 'membersJson' , 'membersOnly' , 'domain' , 'active' , 'enablePop3' ] . join ( ',' ) ;
2021-10-11 19:51:29 -07:00
const MAILDB _FIELDS = [ 'domain' , 'enabled' , 'mailFromValidation' , 'catchAllJson' , 'relayJson' , 'dkimKeyJson' , 'dkimSelector' , 'bannerJson' ] . join ( ',' ) ;
2021-06-29 14:26:34 -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 ;
2021-10-08 10:15:48 -07:00
data . enablePop3 = ! ! data . enablePop3 ;
2021-08-17 15:45:57 -07:00
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 ;
2021-10-11 19:51:29 -07:00
data . dkimKey = safe . JSON . parse ( data . dkimKeyJson ) || null ;
delete data . dkimKeyJson ;
2021-06-29 14:26:34 -07:00
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
2022-01-05 09:55:08 -08:00
// also need to consider valid LDAP characters here (e.g '+' is reserved). keep hyphen at the end so it doesn't become a range.
if ( /[^a-zA-Z0-9._-]/ . test ( name ) ) return new BoxError ( BoxError . BAD _FIELD , 'mailbox name can only contain alphanumerals, dot, hyphen or underscore' ) ;
2018-01-25 18:03:02 +01:00
return null ;
}
2022-05-31 17:53:09 -07:00
function validateDisplayName ( name ) {
assert . strictEqual ( typeof name , 'string' ) ;
if ( name . length < 1 ) return new BoxError ( BoxError . BAD _FIELD , 'mailbox display name must be atleast 1 char' ) ;
if ( name . length >= 100 ) return new BoxError ( BoxError . BAD _FIELD , 'mailbox name too long' ) ;
return null ;
}
2021-08-27 09:52:24 -07:00
async function checkOutboundPort25 ( ) {
const relay = {
2017-06-28 21:38:51 -05:00
value : 'OK' ,
2021-08-27 09:52:24 -07:00
status : false ,
errorMessage : ''
2017-06-28 21:38:51 -05:00
} ;
2021-08-27 09:52:24 -07:00
return await new Promise ( ( resolve ) => {
const client = new net . Socket ( ) ;
client . setTimeout ( 5000 ) ;
2022-01-31 16:55:45 -08:00
client . connect ( 25 , constants . PORT25 _CHECK _SERVER ) ;
2021-08-27 09:52:24 -07:00
client . on ( 'connect' , function ( ) {
relay . status = true ;
relay . value = 'OK' ;
client . destroy ( ) ; // do not use end() because it still triggers timeout
resolve ( relay ) ;
} ) ;
client . on ( 'timeout' , function ( ) {
relay . status = false ;
2022-01-31 16:55:45 -08:00
relay . value = ` Connect to ${ constants . PORT25 _CHECK _SERVER } timed out. Check if port 25 (outbound) is blocked ` ;
2021-08-27 09:52:24 -07:00
client . destroy ( ) ;
2022-01-31 16:55:45 -08:00
relay . errorMessage = ` Connect to ${ constants . PORT25 _CHECK _SERVER } timed out. ` ;
2021-08-27 09:52:24 -07:00
resolve ( relay ) ;
} ) ;
client . on ( 'error' , function ( error ) {
relay . status = false ;
2022-01-31 16:55:45 -08:00
relay . value = ` Connect to ${ constants . PORT25 _CHECK _SERVER } failed: ${ error . message } . Check if port 25 (outbound) is blocked ` ;
2021-08-27 09:52:24 -07:00
client . destroy ( ) ;
2022-03-03 09:58:58 -08:00
relay . errorMessage = ` Connect to ${ constants . PORT25 _CHECK _SERVER } failed: ${ error . message } ` ;
2021-08-27 09:52:24 -07:00
resolve ( relay ) ;
} ) ;
2017-06-28 21:38:51 -05:00
} ) ;
}
2021-08-27 09:52:24 -07:00
async function checkSmtpRelay ( relay ) {
const result = {
2017-06-28 21:38:51 -05:00
value : 'OK' ,
2021-08-27 09:52:24 -07:00
status : false ,
errorMessage : ''
2017-06-28 21:38:51 -05:00
} ;
2017-06-28 17:06:12 -05:00
2021-08-27 09:52:24 -07:00
const 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 } ;
2021-08-27 09:52:24 -07:00
const transporter = nodemailer . createTransport ( smtpTransport ( options ) ) ;
2017-06-28 17:06:12 -05:00
2021-08-27 09:52:24 -07:00
const [ error ] = await safe ( util . promisify ( transporter . verify ) ( ) ) ;
result . status = ! error ;
if ( error ) {
result . value = result . errorMessage = error . message ;
return result ;
}
2017-06-28 17:06:12 -05:00
2021-08-27 09:52:24 -07:00
return result ;
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-08-27 09:52:24 -07:00
if ( relay . provider === 'cloudron-smtp' || relay . provider === 'noop' ) return null ;
2017-06-28 17:06:12 -05:00
2021-08-27 09:52:24 -07:00
const result = await checkSmtpRelay ( relay ) ;
if ( result . errorMessage ) return new BoxError ( BoxError . BAD _FIELD , result . errorMessage ) ;
2017-06-28 21:38:51 -05:00
}
2017-06-28 17:06:12 -05:00
2021-08-27 09:52:24 -07:00
async function checkDkim ( mailDomain ) {
2019-06-10 12:23:29 -07:00
assert . strictEqual ( typeof mailDomain , 'object' ) ;
const domain = mailDomain . domain ;
2021-08-27 09:52:24 -07:00
const dkim = {
2019-06-10 12:23:29 -07:00
domain : ` ${ mailDomain . dkimSelector } ._domainkey. ${ domain } ` ,
name : ` ${ mailDomain . dkimSelector } ._domainkey ` ,
2017-06-28 21:38:51 -05:00
type : 'TXT' ,
expected : null ,
value : null ,
2021-08-27 09:52:24 -07:00
status : false ,
errorMessage : ''
2017-06-28 21:38:51 -05:00
} ;
2017-06-28 17:06:12 -05:00
2021-10-11 19:51:29 -07:00
const publicKey = mailDomain . dkimKey . publicKey . split ( '\n' ) . slice ( 1 , - 2 ) . join ( '' ) ; // remove header, footer and new lines
2017-06-28 17:06:12 -05:00
2021-10-11 19:51:29 -07:00
dkim . expected = ` v=DKIM1; t=s; p= ${ publicKey } ` ;
2017-06-28 17:06:12 -05:00
2022-02-03 16:15:14 -08:00
const [ error , txtRecords ] = await safe ( dig . resolve ( dkim . domain , dkim . type , DNS _OPTIONS ) ) ;
2021-08-27 09:52:24 -07:00
if ( error ) {
dkim . errorMessage = error . message ;
return dkim ;
}
2017-06-28 17:06:12 -05:00
2021-08-27 09:52:24 -07:00
if ( txtRecords . length !== 0 ) {
dkim . value = txtRecords [ 0 ] . join ( '' ) ;
const actual = txtToDict ( dkim . value ) ;
2021-10-11 19:51:29 -07:00
dkim . status = actual . p === publicKey ;
2021-08-27 09:52:24 -07:00
}
2017-06-28 17:06:12 -05:00
2021-08-27 09:52:24 -07:00
return dkim ;
2017-06-28 21:38:51 -05:00
}
2017-06-28 17:06:12 -05:00
2021-08-27 09:52:24 -07:00
async function checkSpf ( domain , mailFqdn ) {
2019-01-31 15:08:14 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof mailFqdn , 'string' ) ;
2021-08-27 09:52:24 -07:00
const 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' ,
2021-08-27 09:52:24 -07:00
status : false ,
errorMessage : ''
2017-06-28 21:38:51 -05:00
} ;
2022-02-03 16:15:14 -08:00
const [ error , txtRecords ] = await safe ( dig . resolve ( spf . domain , spf . type , DNS _OPTIONS ) ) ;
2021-08-27 09:52:24 -07:00
if ( error ) {
spf . errorMessage = error . message ;
return spf ;
}
2017-06-28 21:38:51 -05:00
2021-08-27 09:52:24 -07:00
let i ;
for ( i = 0 ; i < txtRecords . length ; i ++ ) {
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 ;
spf . status = spf . value . indexOf ( ' a:' + settings . mailFqdn ( ) ) !== - 1 ;
break ;
}
2017-06-28 17:06:12 -05:00
2021-08-27 09:52:24 -07:00
if ( spf . status ) {
spf . expected = spf . value ;
} else if ( i !== txtRecords . length ) {
spf . expected = 'v=spf1 a:' + settings . mailFqdn ( ) + ' ' + spf . value . slice ( 'v=spf1 ' . length ) ;
}
2017-06-28 17:06:12 -05:00
2021-08-27 09:52:24 -07:00
return spf ;
2017-06-28 21:38:51 -05:00
}
2017-06-28 17:06:12 -05:00
2021-08-27 09:52:24 -07:00
async function checkMx ( domain , mailFqdn ) {
2019-01-31 15:08:14 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof mailFqdn , 'string' ) ;
2021-08-27 09:52:24 -07:00
const 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 + '.' ,
2021-08-27 09:52:24 -07:00
status : false ,
errorMessage : ''
2017-06-28 21:38:51 -05:00
} ;
2022-02-03 16:15:14 -08:00
const [ error , mxRecords ] = await safe ( dig . resolve ( mx . domain , mx . type , DNS _OPTIONS ) ) ;
2021-08-27 09:52:24 -07:00
if ( error ) {
mx . errorMessage = error . message ;
return mx ;
}
if ( mxRecords . length === 0 ) return mx ;
2017-06-28 21:38:51 -05:00
2021-08-27 09:52:24 -07:00
mx . status = mxRecords . some ( mx => mx . exchange === mailFqdn ) ; // this lets use change priority and/or setup backup MX
mx . value = mxRecords . map ( function ( r ) { return r . priority + ' ' + r . exchange + '.' ; } ) . join ( ' ' ) ;
2019-05-20 17:56:16 -07:00
2021-08-27 09:52:24 -07:00
if ( mx . status ) return mx ; // MX record is "my."
2019-05-20 17:56:16 -07:00
2021-08-27 09:52:24 -07:00
// cloudflare might create a conflict subdomain (https://support.cloudflare.com/hc/en-us/articles/360020296512-DNS-Troubleshooting-FAQ)
2022-02-03 16:15:14 -08:00
const [ error2 , mxIps ] = await safe ( dig . resolve ( mxRecords [ 0 ] . exchange , 'A' , DNS _OPTIONS ) ) ;
2021-08-27 09:52:24 -07:00
if ( error2 || mxIps . length !== 1 ) return mx ;
2019-05-20 17:56:16 -07:00
2022-01-05 18:07:36 -08:00
const [ error3 , ip ] = await safe ( sysinfo . getServerIPv4 ( ) ) ;
2021-08-27 09:52:24 -07:00
if ( error3 ) return mx ;
2019-05-20 17:56:16 -07:00
2021-08-27 09:52:24 -07:00
mx . status = mxIps [ 0 ] === ip ;
2017-06-28 17:06:12 -05:00
2021-08-27 09:52:24 -07:00
return 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 ) {
2022-04-14 17:41:41 -05:00
const dict = { } ;
2018-08-12 13:43:45 -07:00
txt . split ( ';' ) . forEach ( function ( v ) {
2022-04-14 17:41:41 -05:00
const p = v . trim ( ) . split ( '=' ) ;
2018-08-12 13:43:45 -07:00
dict [ p [ 0 ] ] = p [ 1 ] ;
} ) ;
return dict ;
}
2021-08-27 09:52:24 -07:00
async function checkDmarc ( domain ) {
const 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' ,
2021-08-27 09:52:24 -07:00
status : false ,
errorMessage : ''
2017-06-28 21:38:51 -05:00
} ;
2022-02-03 16:15:14 -08:00
const [ error , txtRecords ] = await safe ( dig . resolve ( dmarc . domain , dmarc . type , DNS _OPTIONS ) ) ;
2017-06-28 21:38:51 -05:00
2021-08-27 09:52:24 -07:00
if ( error ) {
dmarc . errorMessage = error . message ;
return dmarc ;
}
2017-06-28 17:06:12 -05:00
2021-08-27 09:52:24 -07:00
if ( txtRecords . length !== 0 ) {
dmarc . value = txtRecords [ 0 ] . join ( '' ) ;
const actual = txtToDict ( dmarc . value ) ;
dmarc . status = actual . v === 'DMARC1' ; // see box#666
}
return dmarc ;
2017-06-28 21:38:51 -05:00
}
2017-06-28 17:06:12 -05:00
2021-08-27 09:52:24 -07:00
async function checkPtr ( mailFqdn ) {
2019-01-31 15:08:14 -08:00
assert . strictEqual ( typeof mailFqdn , 'string' ) ;
2021-08-27 09:52:24 -07:00
const ptr = {
2017-06-28 21:38:51 -05:00
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)
2021-08-27 09:52:24 -07:00
status : false ,
errorMessage : ''
2017-06-28 21:38:51 -05:00
} ;
2017-06-28 17:06:12 -05:00
2022-01-05 18:07:36 -08:00
const [ error , ip ] = await safe ( sysinfo . getServerIPv4 ( ) ) ;
2021-08-27 09:52:24 -07:00
if ( error ) {
ptr . errorMessage = error . message ;
return ptr ;
}
2017-06-28 17:06:12 -05:00
2021-08-27 09:52:24 -07:00
ptr . domain = ip . split ( '.' ) . reverse ( ) . join ( '.' ) + '.in-addr.arpa' ;
ptr . name = ip ;
2017-06-28 17:06:12 -05:00
2022-02-03 16:15:14 -08:00
const [ error2 , ptrRecords ] = await safe ( dig . resolve ( ptr . domain , 'PTR' , DNS _OPTIONS ) ) ;
2021-08-27 09:52:24 -07:00
if ( error2 ) {
ptr . errorMessage = error2 . message ;
return ptr ;
}
2017-06-28 17:06:12 -05:00
2021-08-27 09:52:24 -07:00
if ( ptrRecords . length !== 0 ) {
ptr . value = ptrRecords . join ( ' ' ) ;
ptr . status = ptrRecords . some ( function ( v ) { return v === ptr . expected ; } ) ;
}
2017-06-28 17:06:12 -05:00
2021-08-27 09:52:24 -07:00
return ptr ;
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
2021-08-27 09:52:24 -07:00
async function checkRblStatus ( domain ) {
2022-01-05 18:07:36 -08:00
const ip = await sysinfo . getServerIPv4 ( ) ;
2017-09-08 11:50:11 -07:00
2021-08-27 09:52:24 -07:00
const flippedIp = ip . split ( '.' ) . reverse ( ) . join ( '.' ) ;
2017-09-08 11:50:11 -07:00
2021-08-27 09:52:24 -07:00
// https://tools.ietf.org/html/rfc5782
const blacklistedServers = [ ] ;
for ( const rblServer of RBL _LIST ) {
2022-02-03 16:15:14 -08:00
const [ error , records ] = await safe ( dig . resolve ( flippedIp + '.' + rblServer . dns , 'A' , DNS _OPTIONS ) ) ;
2021-08-27 09:52:24 -07:00
if ( error || ! records ) continue ; // not listed
2017-09-08 11:50:11 -07:00
2021-08-27 09:52:24 -07:00
debug ( ` checkRblStatus: ${ domain } (ip: ${ flippedIp } ) is in the blacklist of ${ JSON . stringify ( rblServer ) } ` ) ;
2017-09-08 11:50:11 -07:00
2021-08-27 09:52:24 -07:00
const result = _ . extend ( { } , rblServer ) ;
2017-09-08 11:50:11 -07:00
2022-02-03 16:15:14 -08:00
const [ error2 , txtRecords ] = await safe ( dig . resolve ( flippedIp + '.' + rblServer . dns , 'TXT' , DNS _OPTIONS ) ) ;
2021-08-27 09:52:24 -07:00
result . txtRecords = error2 || ! txtRecords ? 'No txt record' : txtRecords . map ( x => x . join ( '' ) ) ;
2017-09-08 11:50:11 -07:00
2021-08-27 09:52:24 -07:00
debug ( ` checkRblStatus: ${ domain } (error: ${ error2 . message } ) (txtRecords: ${ JSON . stringify ( txtRecords ) } ) ` ) ;
2017-09-08 11:50:11 -07:00
2021-08-27 09:52:24 -07:00
blacklistedServers . push ( result ) ;
}
2017-09-08 11:50:11 -07:00
2022-03-03 10:08:34 -08:00
debug ( ` checkRblStatus: ${ domain } (ip: ${ ip } ) blacklistedServers: ${ JSON . stringify ( blacklistedServers ) } ) ` ) ;
2017-09-13 22:39:42 -07:00
2021-08-27 09:52:24 -07:00
return { status : blacklistedServers . length === 0 , ip , servers : blacklistedServers } ;
2017-09-13 22:39:42 -07:00
}
2021-08-27 09:52:24 -07:00
async function getStatus ( domain ) {
2018-01-21 00:40:30 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
2017-09-13 22:39:42 -07:00
2018-01-23 12:00:24 +01:00
// ensure we always have a valid toplevel properties for the api
2021-08-27 09:52:24 -07:00
const 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
2021-08-27 09:52:24 -07:00
const mailFqdn = settings . mailFqdn ( ) ;
2017-09-13 22:39:42 -07:00
2021-08-27 09:52:24 -07:00
const mailDomain = await getDomain ( domain ) ;
if ( ! mailDomain ) throw new BoxError ( BoxError . NOT _FOUND , 'mail domain not found' ) ;
2017-09-13 22:39:42 -07:00
2021-08-27 09:52:24 -07:00
const checks = [ ] ;
if ( mailDomain . enabled ) {
checks . push (
{ what : 'dns.mx' , promise : checkMx ( domain , mailFqdn ) } ,
{ what : 'dns.dmarc' , promise : checkDmarc ( domain ) }
) ;
2017-09-13 22:39:42 -07:00
}
2017-09-08 11:50:11 -07:00
2021-08-27 09:52:24 -07:00
if ( mailDomain . relay . provider === 'cloudron-smtp' ) {
// these tests currently only make sense when using Cloudron's SMTP server at this point
checks . push (
{ what : 'dns.spf' , promise : checkSpf ( domain , mailFqdn ) } ,
{ what : 'dns.dkim' , promise : checkDkim ( mailDomain ) } ,
{ what : 'dns.ptr' , promise : checkPtr ( mailFqdn ) } ,
{ what : 'relay' , promise : checkOutboundPort25 ( ) } ,
{ what : 'rbl' , promise : checkRblStatus ( domain ) } ,
) ;
} else if ( mailDomain . relay . provider !== 'noop' ) {
checks . push ( { what : 'relay' , promise : checkSmtpRelay ( mailDomain . relay ) } ) ;
}
2017-09-13 22:39:42 -07:00
2021-08-27 09:52:24 -07:00
// wait for all the checks and record the result
const responses = await Promise . allSettled ( checks . map ( c => c . promise ) ) ;
for ( let i = 0 ; i < checks . length ; i ++ ) {
const response = responses [ i ] , check = checks [ i ] ;
if ( response . status !== 'fulfilled' ) {
debug ( ` check ${ check . what } was rejected. This is not expected ` ) ;
continue ;
2017-09-13 22:39:42 -07:00
}
2021-08-27 09:52:24 -07:00
if ( response . value . errorMessage ) debug ( ` Ignored error - ${ check . what } : ${ response . value . errorMessage } ` ) ;
safe . set ( results , checks [ i ] . what , response . value || { } ) ;
}
return 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 ) {
2021-09-03 11:38:21 -07:00
const result = await 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-11-25 13:34:22 -08:00
async function createMailConfig ( mailFqdn ) {
2019-01-31 15:08:14 -08:00
assert . strictEqual ( typeof mailFqdn , 'string' ) ;
2018-01-20 18:56:17 -08:00
2021-11-25 13:34:22 -08:00
debug ( ` createMailConfig: generating mail config with ${ mailFqdn } ` ) ;
2018-01-20 18:56:17 -08:00
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
2021-10-11 19:51:29 -07:00
if ( ! safe . fs . writeFileSync ( ` ${ paths . MAIL _CONFIG _DIR } /mail.ini ` ,
2021-11-25 13:34:22 -08:00
` mail_in_domains= ${ mailInDomains } \n mail_out_domains= ${ mailOutDomains } \n mail_server_name= ${ mailFqdn } \n \n ` , 'utf8' ) ) {
2021-08-17 15:45:57 -07:00
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
2021-10-11 19:51:29 -07:00
if ( ! safe . fs . writeFileSync ( ` ${ paths . MAIL _CONFIG _DIR } /smtp_forward.ini ` , 'enable_outbound=false\ndomain_selector=mail_from\n' , 'utf8' ) ) {
2021-08-17 15:45:57 -07:00
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-10-11 19:51:29 -07:00
if ( ! safe . fs . appendFileSync ( ` ${ paths . MAIL _CONFIG _DIR } /mail.ini ` ,
2021-08-17 15:45:57 -07:00
` [ ${ 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-10-11 19:51:29 -07:00
if ( ! safe . fs . writeFileSync ( ` ${ paths . MAIL _CONFIG _DIR } /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 . MAIL _CONFIG _DIR } /banner/ ${ domain . domain } .html ` , domain . banner . html || '' ) ) throw new BoxError ( BoxError . FS _ERROR , ` Could not create html banner file: ${ safe . error . message } ` ) ;
safe . fs . mkdirSync ( ` ${ paths . MAIL _CONFIG _DIR } /dkim/ ${ domain . domain } ` , { recursive : true } ) ;
if ( ! safe . fs . writeFileSync ( ` ${ paths . MAIL _CONFIG _DIR } /dkim/ ${ domain . domain } /public ` , domain . dkimKey . publicKey ) ) throw new BoxError ( BoxError . FS _ERROR , ` Could not create public key file: ${ safe . error . message } ` ) ;
if ( ! safe . fs . writeFileSync ( ` ${ paths . MAIL _CONFIG _DIR } /dkim/ ${ domain . domain } /private ` , domain . dkimKey . privateKey ) ) throw new BoxError ( BoxError . FS _ERROR , ` Could not create private key file: ${ safe . error . message } ` ) ;
if ( ! safe . fs . writeFileSync ( ` ${ paths . MAIL _CONFIG _DIR } /dkim/ ${ domain . domain } /selector ` , domain . dkimSelector ) ) throw new BoxError ( BoxError . FS _ERROR , ` Could not create selector file: ${ safe . error . message } ` ) ;
// if the 'yellowtent' user of OS and the 'cloudron' user of mail container don't match, the keys become inaccessible by mail code
2021-10-16 16:25:01 -07:00
if ( ! safe . fs . chmodSync ( ` ${ paths . MAIL _CONFIG _DIR } /dkim/ ${ domain . domain } /private ` , 0o644 ) ) throw new BoxError ( BoxError . FS _ERROR , safe . error ) ;
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 || '' ,
2021-10-16 21:47:23 -07:00
password = relay . password || '' ,
forceFromAddress = relay . forceFromAddress ? 'true' : 'false' ;
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-10-16 21:47:23 -07:00
const relayData = ` [ ${ 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 force_from_address= ${ forceFromAddress } \n \n ` ;
if ( ! safe . fs . appendFileSync ( paths . MAIL _CONFIG _DIR + '/smtp_forward.ini' , relayData , 'utf8' ) ) {
2021-08-17 15:45:57 -07:00
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 ) ;
2021-10-11 19:51:29 -07:00
const dhparamsFilePath = ` ${ paths . MAIL _CONFIG _DIR } /dhparams.pem ` ;
const mailCertFilePath = ` ${ paths . MAIL _CONFIG _DIR } /tls_cert.pem ` ;
const mailKeyFilePath = ` ${ paths . MAIL _CONFIG _DIR } /tls_key.pem ` ;
2021-08-17 15:45:57 -07:00
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 ) ;
2021-10-16 16:26:40 -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
if ( ! safe . fs . chmodSync ( mailKeyFilePath , 0o644 ) ) throw new BoxError ( BoxError . FS _ERROR , ` Could not chmod key file: ${ safe . error . message } ` ) ;
2021-08-17 15:45:57 -07:00
await shell . promises . exec ( 'stopMail' , 'docker stop mail || true' ) ;
await shell . promises . exec ( 'removeMail' , 'docker rm -f mail || true' ) ;
2021-11-25 13:34:22 -08:00
const allowInbound = await createMailConfig ( mailFqdn ) ;
2021-08-17 15:45:57 -07:00
2021-10-07 21:53:43 -07:00
const ports = allowInbound ? '-p 587:2587 -p 993:9993 -p 4190:4190 -p 25:2587 -p 465:2465 -p 995:9995' : '' ;
2021-10-01 12:09:13 -07:00
const readOnly = ! serviceConfig . recoveryMode ? '--read-only' : '' ;
const cmd = serviceConfig . recoveryMode ? '/bin/bash -c \'echo "Debug mode. Sleeping" && sleep infinity\'' : '' ;
2021-10-16 16:07:29 -07:00
const logLevel = serviceConfig . recoveryMode ? 'data' : 'info' ;
2021-08-17 15:45:57 -07:00
2021-10-01 12:09:13 -07:00
const runCmd = ` docker run --restart=always -d --name="mail" \
2021-08-17 15:45:57 -07:00
-- 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}" \
2021-10-16 16:07:29 -07:00
- e LOGLEVEL = $ { logLevel } \
2021-08-17 15:45:57 -07:00
- v "${paths.MAIL_DATA_DIR}:/app/data" \
2021-10-16 16:26:40 -07:00
- v "${paths.MAIL_CONFIG_DIR}:/etc/mail:ro" \
2021-08-17 15:45:57 -07:00
$ { ports } \
-- label isCloudronManaged = true \
2021-10-01 12:09:13 -07:00
$ { readOnly } - v / run - v / tmp $ { tag } $ { cmd } ` ;
2021-08-17 15:45:57 -07:00
2021-10-01 12:09:13 -07:00
await shell . promises . exec ( 'startMail' , runCmd ) ;
2018-01-20 20:34:30 -08:00
}
2021-08-19 12:32:23 -07:00
async function getMailAuth ( ) {
2021-08-25 19:41:46 -07:00
const data = await docker . inspect ( 'mail' ) ;
2021-08-19 12:32:23 -07:00
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
}
2021-08-25 19:41:46 -07:00
async function restartMail ( ) {
if ( process . env . BOX _ENV === 'test' && ! process . env . TEST _CREATE _INFRA ) return ;
2019-02-04 17:10:07 -08:00
2021-08-25 19:41:46 -07:00
const servicesConfig = await settings . getServicesConfig ( ) ;
const mailConfig = servicesConfig [ 'mail' ] || { } ;
2021-01-21 12:53:38 -08:00
2021-11-25 13:34:22 -08:00
debug ( ` restartMail: restarting mail container with mailFqdn: ${ settings . mailFqdn ( ) } mailDomain: ${ settings . mailDomain ( ) } ` ) ;
await configureMail ( settings . mailFqdn ( ) , settings . mailDomain ( ) , mailConfig ) ;
2019-02-04 17:10:07 -08:00
}
2021-09-26 22:48:14 -07:00
async function startMail ( existingInfra ) {
assert . strictEqual ( typeof existingInfra , 'object' ) ;
await restartMail ( ) ;
}
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-08-25 19:41:46 -07:00
await 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 = [ ] ;
2021-08-27 09:52:24 -07:00
for ( const k in data ) {
2021-06-29 14:26:34 -07:00
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-
2021-08-27 09:52:24 -07:00
async function txtRecordsWithSpf ( domain , mailFqdn ) {
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
2021-08-27 09:52:24 -07:00
const txtRecords = await dns . getDnsRecords ( '' , domain , 'TXT' ) ;
2018-01-25 13:48:53 -08:00
2021-08-27 09:52:24 -07:00
debug ( 'txtRecordsWithSpf: current txt records - %j' , txtRecords ) ;
2018-01-25 13:48:53 -08:00
2021-08-27 09:52:24 -07:00
let i , matches , validSpf ;
2018-01-25 13:48:53 -08:00
2021-08-27 09:52:24 -07:00
for ( i = 0 ; i < txtRecords . length ; i ++ ) {
matches = txtRecords [ i ] . match ( /^("?v=spf1) / ) ; // DO backend may return without quotes
if ( matches === null ) continue ;
2018-01-25 13:48:53 -08:00
2021-08-27 09:52:24 -07:00
// this won't work if the entry is arbitrarily "split" across quoted strings
validSpf = txtRecords [ i ] . indexOf ( 'a:' + mailFqdn ) !== - 1 ;
break ; // there can only be one SPF record
}
2018-01-25 13:48:53 -08:00
2021-08-27 09:52:24 -07:00
if ( validSpf ) return null ;
2018-01-25 13:48:53 -08:00
2021-08-27 09:52:24 -07:00
if ( ! matches ) { // no spf record was found, create one
txtRecords . push ( '"v=spf1 a:' + mailFqdn + ' ~all"' ) ;
debug ( 'txtRecordsWithSpf: adding txt record' ) ;
} else { // just add ourself
txtRecords [ i ] = matches [ 1 ] + ' a:' + mailFqdn + txtRecords [ i ] . slice ( matches [ 1 ] . length ) ;
debug ( 'txtRecordsWithSpf: inserting txt record' ) ;
}
2018-01-25 13:48:53 -08:00
2021-08-27 09:52:24 -07:00
return txtRecords ;
2018-01-25 13:48:53 -08:00
}
2021-10-11 19:51:29 -07:00
async function generateDkimKey ( ) {
const publicKeyFilePath = path . join ( os . tmpdir ( ) , ` dkim- ${ crypto . randomBytes ( 4 ) . readUInt32LE ( 0 ) } .public ` ) ;
const privateKeyFilePath = path . join ( os . tmpdir ( ) , ` dkim- ${ crypto . randomBytes ( 4 ) . readUInt32LE ( 0 ) } .private ` ) ;
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
2021-10-11 19:51:29 -07:00
if ( ! safe . child _process . execSync ( ` openssl genrsa -out ${ privateKeyFilePath } 1024 ` ) ) return new BoxError ( BoxError . OPENSSL _ERROR , safe . error ) ;
if ( ! safe . child _process . execSync ( ` openssl rsa -in ${ privateKeyFilePath } -out ${ publicKeyFilePath } -pubout -outform PEM ` ) ) return new BoxError ( BoxError . OPENSSL _ERROR , safe . error ) ;
2018-03-08 12:04:32 -08:00
2021-10-11 19:51:29 -07:00
const publicKey = safe . fs . readFileSync ( publicKeyFilePath , 'utf8' ) ;
if ( ! publicKey ) throw new BoxError ( BoxError . FS _ERROR , safe . error . message ) ;
safe . fs . unlinkSync ( publicKeyFilePath ) ;
2018-03-08 12:04:32 -08:00
2021-10-11 19:51:29 -07:00
const privateKey = safe . fs . readFileSync ( privateKeyFilePath , 'utf8' ) ;
if ( ! privateKey ) throw new BoxError ( BoxError . FS _ERROR , safe . error . message ) ;
safe . fs . unlinkSync ( privateKeyFilePath ) ;
2019-06-06 14:37:57 -07:00
2021-10-11 19:51:29 -07:00
return { publicKey , privateKey } ;
2018-01-25 14:51:07 -08:00
}
2021-08-27 09:52:24 -07:00
async function upsertDnsRecords ( domain , mailFqdn ) {
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
2019-04-08 12:23:11 -07:00
debug ( ` upsertDnsRecords: updating mail dns records of domain ${ domain } and mail fqdn ${ mailFqdn } ` ) ;
2021-08-27 09:52:24 -07:00
const mailDomain = await getDomain ( domain ) ;
if ( ! mailDomain ) throw new BoxError ( BoxError . NOT _FOUND , 'mail domain not found' ) ;
2018-03-08 12:04:32 -08:00
2021-08-27 09:52:24 -07:00
if ( process . env . BOX _ENV === 'test' ) return ;
2018-01-25 13:48:53 -08:00
2021-10-11 19:51:29 -07:00
const publicKey = mailDomain . dkimKey . publicKey . split ( '\n' ) . slice ( 1 , - 2 ) . join ( '' ) ; // remove header, footer and new lines
2018-01-25 13:48:53 -08:00
2021-08-27 09:52:24 -07:00
// t=s limits the domainkey to this domain and not it's subdomains
2021-10-11 19:51:29 -07:00
const dkimRecord = { subdomain : ` ${ mailDomain . dkimSelector } ._domainkey ` , domain : domain , type : 'TXT' , values : [ ` "v=DKIM1; t=s; p= ${ publicKey } " ` ] } ;
2018-01-25 13:48:53 -08:00
2021-10-11 19:51:29 -07:00
const records = [ ] ;
2021-08-27 09:52:24 -07:00
records . push ( dkimRecord ) ;
if ( mailDomain . enabled ) records . push ( { subdomain : '' , domain : domain , type : 'MX' , values : [ '10 ' + mailFqdn + '.' ] } ) ;
2018-01-25 13:48:53 -08:00
2021-08-27 09:52:24 -07:00
const txtRecords = await txtRecordsWithSpf ( domain , mailFqdn ) ;
if ( txtRecords ) records . push ( { subdomain : '' , domain : domain , type : 'TXT' , values : txtRecords } ) ;
2018-01-25 13:48:53 -08:00
2021-08-27 09:52:24 -07:00
const dmarcRecords = await dns . getDnsRecords ( '_dmarc' , domain , 'TXT' ) ; // only update dmarc if absent. this allows user to set email for reporting
if ( dmarcRecords . length === 0 ) records . push ( { subdomain : '_dmarc' , domain : domain , type : 'TXT' , values : [ '"v=DMARC1; p=reject; pct=100"' ] } ) ;
2018-01-25 13:48:53 -08:00
2021-08-27 09:52:24 -07:00
debug ( 'upsertDnsRecords: will update %j' , records ) ;
2018-09-06 12:26:11 -07:00
2021-08-27 09:52:24 -07:00
for ( const record of records ) {
await dns . upsertDnsRecords ( record . subdomain , record . domain , record . type , record . values ) ;
}
2021-02-24 09:02:32 -08:00
2021-08-27 09:52:24 -07:00
debug ( 'upsertDnsRecords: records %j added' , records ) ;
2018-01-25 13:48:53 -08:00
}
2021-08-27 09:52:24 -07:00
async function setDnsRecords ( domain ) {
2019-02-04 20:51:26 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
2021-08-27 09:52:24 -07:00
await upsertDnsRecords ( domain , settings . mailFqdn ( ) ) ;
2019-02-04 20:51:26 -08:00
}
2021-09-03 11:38:21 -07:00
async function getLocation ( ) {
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
2021-09-03 11:38:21 -07:00
return { domain , subdomain } ;
2020-08-15 23:17:47 -07:00
}
2021-08-27 09:52:24 -07:00
async function changeLocation ( auditSource , progressCallback ) {
2020-08-15 23:17:47 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof progressCallback , '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 } ` } ) ;
2022-02-06 11:21:32 -08:00
await cloudron . setupDnsAndCert ( subdomain , domain , auditSource , ( progress ) => progressCallback ( { message : progress . message } ) ) ; // remove the percent
2021-08-27 09:52:24 -07:00
const allDomains = await domains . list ( ) ;
2019-02-04 14:49:51 -08:00
2021-08-27 09:52:24 -07:00
for ( let idx = 0 ; idx < allDomains . length ; idx ++ ) {
const domainObject = allDomains [ idx ] ;
progressCallback ( { percent : progress , message : ` Updating DNS of ${ domainObject . domain } ` } ) ;
progress += Math . round ( 70 / allDomains . length ) ;
2020-08-15 23:17:47 -07:00
2021-09-03 11:38:21 -07:00
const [ error ] = await safe ( upsertDnsRecords ( domainObject . domain , fqdn ) ) ; // ignore any errors. we anyway report dns errors in status tab
2021-08-27 09:52:24 -07:00
progressCallback ( { percent : progress , message : ` Updated DNS of ${ domainObject . domain } : ${ error ? error . message : 'success' } ` } ) ;
}
2020-08-15 23:17:47 -07:00
2021-08-27 09:52:24 -07:00
progressCallback ( { percent : 90 , message : 'Restarting mail server' } ) ;
2020-08-15 23:17:47 -07:00
2021-08-27 09:52:24 -07:00
await restartMailIfActivated ( ) ;
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 ] ) ;
2021-09-17 09:22:46 -07:00
tasks . startTask ( taskId , { } ) ;
2021-08-19 13:24:38 -07:00
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
}
2021-08-27 09:52:24 -07:00
async function onDomainAdded ( domain ) {
2018-01-24 21:15:58 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
2021-08-27 09:52:24 -07:00
if ( ! settings . mailFqdn ( ) ) return ; // mail domain is not set yet (when provisioning)
2020-08-15 23:17:47 -07:00
2021-10-11 19:51:29 -07:00
await upsertDnsRecords ( domain , settings . mailFqdn ( ) ) ;
2021-08-27 09:52:24 -07:00
await restartMailIfActivated ( ) ;
2018-01-24 21:15:58 -08:00
}
2021-08-25 19:41:46 -07:00
async function onDomainRemoved ( domain ) {
2018-01-24 21:15:58 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
2021-08-25 19:41:46 -07:00
await restartMail ( ) ;
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-09-23 17:28:22 -07:00
safe ( restartMail ( ) , { debug } ) ; // 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-09-23 17:28:22 -07:00
safe ( restartMail ( ) , { debug } ) ;
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-09-23 17:28:22 -07:00
safe ( restartMail ( ) , { debug } ) ; // 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-08-27 09:52:24 -07:00
if ( ! options . skipVerify ) {
const error = await verifyRelay ( relay ) ;
if ( error ) throw error ;
}
2019-02-15 10:55:15 -08:00
2021-10-16 21:47:23 -07:00
await updateDomain ( domain , { relay } ) ;
2019-02-15 10:55:15 -08:00
2021-09-23 17:28:22 -07:00
safe ( restartMail ( ) , { debug } ) ;
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-09-23 17:28:22 -07:00
safe ( restartMail ( ) , { debug } ) ;
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-22 09:40:06 -07:00
await mailer . sendTestMail ( result . domain , to ) ;
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-10-26 11:19:30 -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, m1.enablePop3 AS enablePop3 '
2021-08-17 15:45:57 -07:00
+ ` 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-10-26 11:19:30 -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, m1.enablePop3 AS enablePop3 '
2021-08-17 15:45:57 -07:00
+ ` 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-12-02 22:14:41 -08:00
if ( ! OWNERTYPES . includes ( ownerType ) ) 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' ) ;
2022-02-17 18:03:56 -08:00
if ( error && error . code === 'ER_NO_REFERENCED_ROW_2' && error . sqlMessage . includes ( 'mailboxes_domain_constraint' ) ) throw new BoxError ( BoxError . NOT _FOUND , ` no such domain ' ${ domain } ' ` ) ;
2021-08-17 15:45:57 -07:00
if ( error ) throw error ;
2018-11-09 18:45:44 -08:00
2022-02-24 20:04:46 -08:00
await 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-10-08 10:15:48 -07:00
const { ownerId , ownerType , active , enablePop3 } = data ;
2021-04-14 22:37:01 -07:00
assert . strictEqual ( typeof ownerId , 'string' ) ;
assert . strictEqual ( typeof ownerType , 'string' ) ;
assert . strictEqual ( typeof active , 'boolean' ) ;
2021-10-08 10:15:48 -07:00
assert . strictEqual ( typeof enablePop3 , 'boolean' ) ;
2021-04-14 22:37:01 -07:00
2018-04-05 16:07:51 -07:00
name = name . toLowerCase ( ) ;
2021-12-02 22:14:41 -08:00
if ( ! OWNERTYPES . includes ( ownerType ) ) 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-10-08 10:15:48 -07:00
const result = await database . query ( 'UPDATE mailboxes SET ownerId = ?, ownerType = ?, active = ?, enablePop3 = ? WHERE name = ? AND domain = ?' , [ ownerId , ownerType , active , enablePop3 , name , domain ] ) ;
2021-08-17 15:45:57 -07:00
if ( result . affectedRows === 0 ) throw new BoxError ( BoxError . NOT _FOUND , 'Mailbox not found' ) ;
2020-01-24 16:55:41 -08:00
2022-02-24 20:04:46 -08:00
await eventlog . add ( eventlog . ACTION _MAIL _MAILBOX _UPDATE , auditSource , { name , domain , oldUserId : mailbox . userId , ownerId , ownerType , active } ) ;
2018-04-03 14:12:43 -07:00
}
2021-08-25 19:41:46 -07:00
async function removeSolrIndex ( mailbox ) {
2020-12-02 00:24:15 -08:00
assert . strictEqual ( typeof mailbox , 'string' ) ;
2021-08-25 19:41:46 -07:00
const addonDetails = await services . getContainerDetails ( 'mail' , 'CLOUDRON_MAIL_TOKEN' ) ;
2020-12-02 00:24:15 -08:00
2021-12-19 00:30:22 -08:00
const [ error , response ] = await safe ( superagent . post ( ` http:// ${ addonDetails . ip } :3000/solr_delete_index?access_token= ${ addonDetails . token } ` )
2021-08-25 19:41:46 -07:00
. timeout ( 2000 )
. send ( { mailbox } )
. ok ( ( ) => true ) ) ;
2020-12-02 00:24:15 -08:00
2021-08-25 19:41:46 -07:00
if ( error ) throw new BoxError ( BoxError . MAIL _ERROR , ` Could not remove solr index: ${ error . message } ` ) ;
2020-12-02 00:24:15 -08:00
2021-08-25 19:41:46 -07:00
if ( response . status !== 200 ) throw new BoxError ( BoxError . MAIL _ERROR , ` Error removing solr index - ${ response . status } ${ JSON . stringify ( response . body ) } ` ) ;
2020-12-02 00:24:15 -08:00
}
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-25 19:41:46 -07:00
const [ error ] = await safe ( removeSolrIndex ( mailbox ) ) ;
if ( error ) debug ( ` delMailbox: failed to remove solr index: ${ error . message } ` ) ;
2022-02-24 20:04:46 -08:00
await 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
}
2022-02-24 20:30:13 -08:00
async function setAliases ( name , domain , aliases , auditSource ) {
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 ) ) ;
2022-02-24 20:30:13 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2018-01-25 18:03:02 +01:00
2021-08-17 15:45:57 -07:00
for ( let i = 0 ; i < aliases . length ; i ++ ) {
2022-01-10 22:06:37 -08:00
const name = aliases [ i ] . name . toLowerCase ( ) ;
const domain = aliases [ i ] . domain . toLowerCase ( ) ;
2018-01-25 18:03:02 +01:00
2022-01-10 22:06:37 -08:00
const error = validateName ( name ) ;
2021-08-17 15:45:57 -07:00
if ( error ) throw error ;
2020-04-19 18:44:16 -07:00
2022-01-10 22:06:37 -08:00
const mailDomain = await getDomain ( domain ) ;
if ( ! mailDomain ) throw new BoxError ( BoxError . NOT _FOUND , ` mail domain ${ domain } not found ` ) ;
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' ) ;
2022-02-24 20:30:13 -08:00
const queries = [ ] ;
2021-08-17 15:45:57 -07:00
// clear existing aliases
queries . push ( { query : 'DELETE FROM mailboxes WHERE aliasName = ? AND aliasDomain = ? AND type = ?' , args : [ name , domain , exports . TYPE _ALIAS ] } ) ;
2022-02-24 20:30:13 -08:00
for ( const alias of aliases ) {
2021-08-17 15:45:57 -07:00
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 ] } ) ;
2022-02-24 20:30:13 -08: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 ;
2022-02-24 20:30:13 -08:00
await eventlog . add ( eventlog . ACTION _MAIL _MAILBOX _UPDATE , auditSource , { name , domain , aliases } ) ;
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
2022-02-24 20:04:46 -08:00
await 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
2022-02-24 20:04:46 -08:00
await eventlog . add ( eventlog . ACTION _MAIL _LIST _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
2022-02-24 20:04:46 -08:00
await 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
}