2017-06-28 17:06:12 -05:00
'use strict' ;
exports = module . exports = {
getStatus : getStatus ,
2018-01-20 22:56:45 -08:00
get : get ,
2018-01-24 11:33:09 -08:00
getAll : getAll ,
2018-01-20 18:56:17 -08:00
2018-01-24 21:15:58 -08:00
add : add ,
del : del ,
2018-01-20 22:56:45 -08:00
setMailFromValidation : setMailFromValidation ,
2018-01-20 18:56:17 -08:00
setCatchAllAddress : setCatchAllAddress ,
setMailRelay : setMailRelay ,
2018-01-20 23:17:39 -08:00
setMailEnabled : setMailEnabled ,
2018-01-20 18:56:17 -08:00
2018-01-20 20:34:30 -08:00
startMail : restartMail ,
2018-01-20 18:56:17 -08:00
2018-01-23 16:10:23 -08:00
sendTestMail : sendTestMail ,
2018-01-24 13:11:35 +01:00
getMailboxes : getMailboxes ,
getUserMailbox : getUserMailbox ,
enableUserMailbox : enableUserMailbox ,
disableUserMailbox : disableUserMailbox ,
2018-01-25 18:03:02 +01:00
getAliases : getAliases ,
setAliases : setAliases ,
2018-01-20 18:19:26 -08:00
MailError : MailError
2017-06-28 17:06:12 -05:00
} ;
var assert = require ( 'assert' ) ,
async = require ( 'async' ) ,
2018-01-20 20:34:30 -08:00
certificates = require ( './certificates.js' ) ,
2017-06-28 17:06:12 -05:00
cloudron = require ( './cloudron.js' ) ,
config = require ( './config.js' ) ,
2018-01-25 18:03:02 +01:00
constants = require ( './constants.js' ) ,
2018-01-20 18:56:17 -08:00
DatabaseError = require ( './databaseerror.js' ) ,
debug = require ( 'debug' ) ( 'box:mail' ) ,
2017-06-28 17:06:12 -05:00
dig = require ( './dig.js' ) ,
2018-01-20 20:34:30 -08:00
domains = require ( './domains.js' ) ,
infra = require ( './infra_version.js' ) ,
2018-01-24 13:11:35 +01:00
mailboxdb = require ( './mailboxdb.js' ) ,
2018-01-20 22:56:45 -08:00
maildb = require ( './maildb.js' ) ,
2018-01-23 16:10:23 -08:00
mailer = require ( './mailer.js' ) ,
2017-06-28 17:06:12 -05:00
net = require ( 'net' ) ,
nodemailer = require ( 'nodemailer' ) ,
2018-01-20 20:34:30 -08: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' ) ,
2017-06-28 21:38:51 -05:00
safe = require ( 'safetydance' ) ,
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' ) ,
2018-01-20 18:56:17 -08:00
user = require ( './user.js' ) ,
2018-01-24 13:11:35 +01:00
UserError = user . UserError ,
2017-06-28 17:06:12 -05:00
util = require ( 'util' ) ,
_ = require ( 'underscore' ) ;
2017-06-28 21:38:51 -05:00
const digOptions = { server : '127.0.0.1' , port : 53 , timeout : 5000 } ;
2018-01-20 18:56:17 -08:00
var NOOP _CALLBACK = function ( error ) { if ( error ) debug ( error ) ; } ;
2018-01-20 18:19:26 -08:00
function MailError ( reason , errorOrMessage ) {
2017-06-28 17:06:12 -05:00
assert . strictEqual ( typeof reason , 'string' ) ;
assert ( errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined' ) ;
Error . call ( this ) ;
Error . captureStackTrace ( this , this . constructor ) ;
this . name = this . constructor . name ;
this . reason = reason ;
if ( typeof errorOrMessage === 'undefined' ) {
this . message = reason ;
} else if ( typeof errorOrMessage === 'string' ) {
this . message = errorOrMessage ;
} else {
this . message = 'Internal error' ;
this . nestedError = errorOrMessage ;
}
}
2018-01-20 18:19:26 -08:00
util . inherits ( MailError , Error ) ;
MailError . INTERNAL _ERROR = 'Internal Error' ;
MailError . BAD _FIELD = 'Bad Field' ;
2018-01-24 15:38:19 +01:00
MailError . ALREADY _EXISTS = 'Already Exists' ;
2018-01-21 00:16:58 -08:00
MailError . NOT _FOUND = 'Not Found' ;
2017-06-28 17:06:12 -05:00
2018-01-25 18:03:02 +01:00
function validateAlias ( alias ) {
assert . strictEqual ( typeof alias , 'string' ) ;
if ( alias . length < 1 ) return new MailError ( MailError . BAD _FIELD , 'alias must be atleast 1 char' ) ;
if ( alias . length >= 200 ) return new MailError ( MailError . BAD _FIELD , 'alias too long' ) ;
if ( constants . RESERVED _NAMES . indexOf ( alias ) !== - 1 ) return new MailError ( MailError . BAD _FIELD , 'alias is reserved' ) ;
// +/- can be tricky in emails. also need to consider valid LDAP characters here (e.g '+' is reserved)
if ( /[^a-zA-Z0-9.]/ . test ( alias ) ) return new MailError ( MailError . BAD _FIELD , 'alias can only contain alphanumerals and dot' ) ;
// app emails are sent using the .app suffix
if ( alias . indexOf ( '.app' ) !== - 1 ) return new MailError ( MailError . BAD _FIELD , 'alias pattern is reserved for apps' ) ;
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' ,
'smtp.mail.yahoo.com' ,
'smtp.o2.ie' ,
'smtp.comcast.net' ,
'outgoing.verizon.net'
] ) ;
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 ;
relay . value = 'Connect to ' + smtpServer + ' timed out' ;
client . destroy ( ) ;
callback ( new Error ( 'Timeout' ) , relay ) ;
} ) ;
client . on ( 'error' , function ( error ) {
relay . status = false ;
relay . value = 'Connect to ' + smtpServer + ' failed: ' + error . message ;
client . destroy ( ) ;
callback ( error , relay ) ;
} ) ;
}
function checkSmtpRelay ( relay , callback ) {
var result = {
value : 'OK' ,
status : false
} ;
2017-06-28 17:06:12 -05:00
var transporter = nodemailer . createTransport ( smtpTransport ( {
host : relay . host ,
port : relay . port ,
auth : {
user : relay . username ,
pass : relay . password
}
} ) ) ;
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
}
2017-06-28 21:38:51 -05:00
function verifyRelay ( relay , callback ) {
assert . strictEqual ( typeof relay , 'object' ) ;
2017-06-28 17:06:12 -05:00
assert . strictEqual ( typeof callback , 'function' ) ;
2017-06-28 21:38:51 -05:00
var verifier = relay . provider === 'cloudron-smtp' ? checkOutboundPort25 : checkSmtpRelay . bind ( null , relay ) ;
2017-06-28 17:06:12 -05:00
2017-06-28 21:38:51 -05:00
verifier ( function ( error ) {
2018-01-20 18:19:26 -08:00
if ( error ) return callback ( new MailError ( MailError . BAD _FIELD , error . message ) ) ;
2017-06-28 17:06:12 -05:00
2017-06-28 21:38:51 -05:00
callback ( ) ;
} ) ;
}
2017-06-28 17:06:12 -05:00
2018-01-24 11:33:09 -08:00
function checkDkim ( domain , callback ) {
2017-06-28 21:38:51 -05:00
var dkim = {
2018-01-24 11:33:09 -08:00
domain : config . dkimSelector ( ) + '._domainkey.' + domain ,
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 ) ;
2017-06-28 21:38:51 -05:00
if ( ! dkimKey ) return callback ( new Error ( 'Failed to read dkim public key' ) , dkim ) ;
2017-06-28 17:06:12 -05:00
2017-06-28 21:38:51 -05:00
dkim . expected = '"v=DKIM1; t=s; p=' + dkimKey + '"' ;
2017-06-28 17:06:12 -05:00
2017-06-28 21:38:51 -05:00
dig . resolve ( dkim . domain , dkim . type , digOptions , function ( error , txtRecords ) {
if ( error && error . code === 'ENOTFOUND' ) return callback ( null , dkim ) ; // not setup
if ( error ) return callback ( error , dkim ) ;
2017-06-28 17:06:12 -05:00
2017-06-28 21:38:51 -05:00
if ( Array . isArray ( txtRecords ) && txtRecords . length !== 0 ) {
dkim . value = txtRecords [ 0 ] ;
dkim . status = ( dkim . value === dkim . expected ) ;
}
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
2018-01-24 11:33:09 -08:00
function checkSpf ( domain , callback ) {
2017-06-28 21:38:51 -05:00
var spf = {
2018-01-24 11:33:09 -08:00
domain : domain ,
2017-06-28 21:38:51 -05:00
type : 'TXT' ,
value : null ,
2018-01-24 11:33:09 -08:00
expected : '"v=spf1 a:' + config . mailFqdn ( ) + ' ~all"' ,
2017-06-28 21:38:51 -05:00
status : false
} ;
// https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
dig . resolve ( spf . domain , spf . type , digOptions , function ( error , txtRecords ) {
if ( error && error . code === 'ENOTFOUND' ) return callback ( null , spf ) ; // not setup
if ( error ) return callback ( error , spf ) ;
if ( ! Array . isArray ( txtRecords ) ) return callback ( null , spf ) ;
var i ;
for ( i = 0 ; i < txtRecords . length ; i ++ ) {
if ( txtRecords [ i ] . indexOf ( '"v=spf1 ' ) !== 0 ) continue ; // not SPF
spf . value = txtRecords [ i ] ;
spf . status = spf . value . indexOf ( ' a:' + config . adminFqdn ( ) ) !== - 1 ;
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 ) {
spf . expected = '"v=spf1 a:' + config . adminFqdn ( ) + ' ' + spf . value . slice ( '"v=spf1 ' . length ) ;
}
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
2018-01-24 11:33:09 -08:00
function checkMx ( domain , callback ) {
2017-06-28 21:38:51 -05:00
var mx = {
2018-01-24 11:33:09 -08:00
domain : domain ,
2017-06-28 21:38:51 -05:00
type : 'MX' ,
value : null ,
expected : '10 ' + config . mailFqdn ( ) + '.' ,
status : false
} ;
dig . resolve ( mx . domain , mx . type , digOptions , function ( error , mxRecords ) {
if ( error && error . code === 'ENOTFOUND' ) return callback ( null , mx ) ; // not setup
if ( error ) return callback ( error , mx ) ;
if ( Array . isArray ( mxRecords ) && mxRecords . length !== 0 ) {
mx . status = mxRecords . length == 1 && mxRecords [ 0 ] . exchange === ( config . mailFqdn ( ) + '.' ) ;
mx . value = mxRecords . map ( function ( r ) { return r . priority + ' ' + r . exchange ; } ) . join ( ' ' ) ;
}
2017-06-28 17:06:12 -05:00
2017-06-28 21:38:51 -05:00
callback ( null , mx ) ;
} ) ;
}
2017-06-28 17:06:12 -05:00
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 ,
2017-06-28 21:38:51 -05:00
type : 'TXT' ,
value : null ,
expected : '"v=DMARC1; p=reject; pct=100"' ,
status : false
} ;
dig . resolve ( dmarc . domain , dmarc . type , digOptions , function ( error , txtRecords ) {
if ( error && error . code === 'ENOTFOUND' ) return callback ( null , dmarc ) ; // not setup
if ( error ) return callback ( error , dmarc ) ;
if ( Array . isArray ( txtRecords ) && txtRecords . length !== 0 ) {
dmarc . value = txtRecords [ 0 ] ;
dmarc . status = ( dmarc . value === dmarc . expected ) ;
}
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
2017-06-28 21:38:51 -05:00
function checkPtr ( callback ) {
var ptr = {
domain : null ,
type : 'PTR' ,
value : null ,
expected : config . mailFqdn ( ) + '.' ,
status : false
} ;
2017-06-28 17:06:12 -05:00
2017-06-28 21:38:51 -05:00
sysinfo . getPublicIp ( function ( error , ip ) {
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' ;
2017-06-28 17:06:12 -05:00
2017-06-28 21:38:51 -05:00
dig . resolve ( ip , 'PTR' , digOptions , function ( error , ptrRecords ) {
if ( error && error . code === 'ENOTFOUND' ) return callback ( null , ptr ) ; // not setup
if ( error ) return callback ( error , ptr ) ;
2017-06-28 17:06:12 -05:00
2017-06-28 21:38:51 -05:00
if ( Array . isArray ( ptrRecords ) && 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
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 = [
{
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
} ,
{
2017-12-04 17:10:02 +05:30
'name' : 'SpamCop' ,
'dns' : 'bl.spamcop.net' ,
'site' : 'http://spamcop.net'
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
} ,
{
2017-12-04 17:10:02 +05:30
'name' : 'Composite Blocking List' ,
'dns' : 'cbl.abuseat.org' ,
'site' : 'http://www.abuseat.org'
2017-09-08 11:50:11 -07:00
} ,
{
2017-12-04 17:10:02 +05:30
'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' : 'Multi SURBL' ,
'dns' : 'multi.surbl.org' ,
'site' : 'http://www.surbl.org'
2017-09-08 11:50:11 -07:00
} ,
{
2017-12-04 17:10:02 +05:30
'name' : 'Spam Cannibal' ,
'dns' : 'bl.spamcannibal.org' ,
'site' : 'http://www.spamcannibal.org/cannibal.cgi'
2017-09-08 11:50:11 -07:00
} ,
{
2017-12-04 17:10:02 +05:30
'name' : 'dnsbl.abuse.ch' ,
'dns' : 'spam.abuse.ch' ,
'site' : 'http://dnsbl.abuse.ch/'
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
sysinfo . getPublicIp ( function ( error , ip ) {
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 ) {
dig . resolve ( flippedIp + '.' + rblServer . dns , 'A' , digOptions , function ( error , records ) {
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 ) ;
dig . resolve ( flippedIp + '.' + rblServer . dns , 'TXT' , digOptions , function ( error , txtRecords ) {
result . txtRecords = error || ! txtRecords ? 'No txt record' : txtRecords ;
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 = {
dns : { } ,
rbl : { } ,
relay : { }
} ;
2017-09-13 22:39:42 -07:00
function recordResult ( what , func ) {
return function ( callback ) {
func ( function ( error , result ) {
if ( error ) debug ( 'Ignored error - ' + what + ':' , error ) ;
safe . set ( results , what , result ) ;
callback ( ) ;
} ) ;
} ;
}
2017-09-08 11:50:11 -07:00
2018-01-24 11:33:09 -08:00
get ( domain , function ( error , result ) {
2017-09-13 22:39:42 -07:00
if ( error ) return callback ( error ) ;
2017-09-08 11:50:11 -07:00
2017-09-13 22:39:42 -07:00
var checks = [
2018-01-24 11:33:09 -08:00
recordResult ( 'dns.mx' , checkMx . bind ( null , domain ) ) ,
recordResult ( 'dns.dmarc' , checkDmarc . bind ( null , domain ) )
2017-09-13 22:39:42 -07:00
] ;
2018-01-24 11:33:09 -08:00
if ( result . 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 (
2018-01-24 11:33:09 -08:00
recordResult ( 'dns.spf' , checkSpf . bind ( null , domain ) ) ,
recordResult ( 'dns.dkim' , checkDkim . bind ( null , domain ) ) ,
2017-09-13 22:39:42 -07:00
recordResult ( 'dns.ptr' , checkPtr ) ,
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
) ;
} else {
2018-01-24 11:33:09 -08:00
checks . push ( recordResult ( 'relay' , checkSmtpRelay . bind ( null , result . 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
function createMailConfig ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
const mailFqdn = config . mailFqdn ( ) ;
debug ( 'createMailConfig: generating mail config' ) ;
2018-01-24 11:33:09 -08:00
maildb . getAll ( function ( error , mailOutDomains ) {
if ( error ) return callback ( error ) ;
2018-01-20 18:56:17 -08:00
2018-01-24 11:33:09 -08:00
var mailDomain = mailOutDomains [ 0 ] ; // mail container can only handle one domain at this point
const alertsFrom = ` no-reply@ ${ mailDomain . domain } ` ;
user . getOwner ( function ( error , owner ) {
const alertsTo = config . provider ( ) === 'caas' ? [ 'support@cloudron.io' ] : [ ] ;
alertsTo . concat ( error ? [ ] : owner . email ) . join ( ',' ) ; // owner may not exist yet
2018-01-20 18:56:17 -08:00
2018-01-24 11:33:09 -08:00
const mailOutDomain = mailDomain . domain ;
const mailInDomain = mailDomain . enabled ? mailDomain . domain : '' ;
const catchAll = mailDomain . catchAll . map ( function ( c ) { return ` ${ c } @ ${ mailDomain . domain } ` ; } ) . join ( ',' ) ;
const mailFromValidation = mailDomain . mailFromValidation ;
2018-01-20 18:56:17 -08:00
if ( ! safe . fs . writeFileSync ( paths . ADDON _CONFIG _DIR + '/mail/mail.ini' ,
2018-01-24 11:33:09 -08:00
` mail_in_domains= ${ mailInDomain } \n mail_out_domains= ${ mailOutDomain } \n mail_default_domain= ${ mailDomain . domain } \n mail_server_name= ${ mailFqdn } \n alerts_from= ${ alertsFrom } \n alerts_to= ${ alertsTo } \n catch_all= ${ catchAll } \n mail_from_validation= ${ mailFromValidation } \n ` , 'utf8' ) ) {
2018-01-20 18:56:17 -08:00
return callback ( new Error ( 'Could not create mail var file:' + safe . error . message ) ) ;
}
2018-01-24 11:33:09 -08:00
var relay = mailDomain . relay ;
2018-01-20 18:56:17 -08:00
const enabled = relay . provider !== 'cloudron-smtp' ? true : false ,
host = relay . host || '' ,
port = relay . port || 25 ,
username = relay . username || '' ,
password = relay . password || '' ;
if ( ! safe . fs . writeFileSync ( paths . ADDON _CONFIG _DIR + '/mail/smtp_forward.ini' ,
` enable_outbound= ${ enabled } \n host= ${ host } \n port= ${ port } \n enable_tls=true \n auth_type=plain \n auth_user= ${ username } \n auth_pass= ${ password } ` , 'utf8' ) ) {
return callback ( new Error ( 'Could not create mail var file:' + safe . error . message ) ) ;
}
2018-01-24 11:33:09 -08:00
callback ( null , mailInDomain . length !== 0 ) ;
2018-01-20 18:56:17 -08:00
} ) ;
} ) ;
}
2018-01-20 20:34:30 -08:00
function restartMail ( callback ) {
// mail (note: 2525 is hardcoded in mail container and app use this port)
// 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
2018-01-21 00:16:58 -08:00
if ( process . env . BOX _ENV === 'test' && ! process . env . TEST _CREATE _INFRA ) return callback ( ) ;
2018-01-21 00:06:08 -08:00
2018-01-20 20:34:30 -08:00
function onCertificateChanged ( domain ) {
if ( domain === '*.' + config . fqdn ( ) || domain === config . adminFqdn ( ) ) restartMail ( NOOP _CALLBACK ) ;
}
certificates . events . removeListener ( certificates . EVENT _CERT _CHANGED , onCertificateChanged ) ;
certificates . events . on ( certificates . EVENT _CERT _CHANGED , onCertificateChanged ) ;
const tag = infra . images . mail . tag ;
const memoryLimit = Math . max ( ( 1 + Math . round ( os . totalmem ( ) / ( 1024 * 1024 * 1024 ) / 4 ) ) * 128 , 256 ) ;
// admin and mail share the same certificate
certificates . getAdminCertificate ( function ( error , cert , key ) {
if ( error ) return callback ( error ) ;
// the setup script copies dhparams.pem to /addons/mail
if ( ! safe . fs . writeFileSync ( paths . ADDON _CONFIG _DIR + '/mail/tls_cert.pem' , cert ) ) return callback ( new Error ( 'Could not create cert file:' + safe . error . message ) ) ;
if ( ! safe . fs . writeFileSync ( paths . ADDON _CONFIG _DIR + '/mail/tls_key.pem' , key ) ) return callback ( new Error ( 'Could not create key file:' + safe . error . message ) ) ;
2018-01-24 11:33:09 -08:00
shell . execSync ( 'startMail' , 'docker rm -f mail || true' ) ;
2018-01-20 20:34:30 -08:00
2018-01-24 11:33:09 -08:00
createMailConfig ( function ( error , allowInbound ) {
if ( error ) return callback ( error ) ;
2018-01-20 20:34:30 -08:00
2018-01-24 11:33:09 -08:00
var ports = allowInbound ? '-p 587:2525 -p 993:9993 -p 4190:4190 -p 25:2525' : '' ;
2018-01-20 20:34:30 -08:00
2018-01-24 11:33:09 -08:00
const cmd = ` docker run --restart=always -d --name="mail" \
-- net cloudron \
-- net - alias mail \
- m $ { memoryLimit } m \
-- memory - swap $ { memoryLimit * 2 } m \
-- dns 172.18 . 0.1 \
-- dns - search = . \
- v "${paths.MAIL_DATA_DIR}:/app/data" \
- v "${paths.PLATFORM_DATA_DIR}/addons/mail:/etc/mail" \
$ { ports } \
-- read - only - v / run - v / tmp $ { tag } ` ;
2018-01-20 20:34:30 -08:00
2018-01-24 11:33:09 -08:00
shell . execSync ( 'startMail' , cmd ) ;
2018-01-20 20:34:30 -08:00
2018-01-24 11:33:09 -08:00
callback ( ) ;
2018-01-20 20:34:30 -08:00
} ) ;
} ) ;
}
2018-01-20 22:56:45 -08:00
function get ( domain , callback ) {
assert . strictEqual ( typeof domain , 'string' ) ;
2018-01-20 18:56:17 -08:00
assert . strictEqual ( typeof callback , 'function' ) ;
2018-01-20 22:56:45 -08:00
maildb . get ( domain , function ( error , result ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new MailError ( MailError . NOT _FOUND ) ) ;
2018-01-20 18:56:17 -08:00
if ( error ) return callback ( new MailError ( MailError . INTERNAL _ERROR , error ) ) ;
2018-01-20 22:56:45 -08:00
return callback ( null , result ) ;
2018-01-20 18:56:17 -08:00
} ) ;
}
2018-01-24 11:33:09 -08:00
function getAll ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
maildb . getAll ( function ( error , results ) {
if ( error ) return callback ( new MailError ( MailError . INTERNAL _ERROR , error ) ) ;
return callback ( null , results ) ;
} ) ;
}
2018-01-24 21:30:06 -08:00
function ensureDkimKey ( domain , callback ) {
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
var dkimPath = path . join ( paths . MAIL _DATA _DIR , ` dkim/ ${ domain } ` ) ;
var dkimPrivateKeyFile = path . join ( dkimPath , 'private' ) ;
var dkimPublicKeyFile = path . join ( dkimPath , 'public' ) ;
var dkimSelectorFile = path . join ( dkimPath , 'selector' ) ;
debug ( 'Generating new DKIM keys' ) ;
if ( ! safe . fs . mkdirSync ( dkimPath ) && safe . error . code !== 'EEXIST' ) {
debug ( 'Error creating dkim.' , safe . error ) ;
return new MailError ( MailError . INTERNAL _ERROR , safe . error ) ;
}
if ( ! safe . child _process . execSync ( 'openssl genrsa -out ' + dkimPrivateKeyFile + ' 1024' ) ) return new MailError ( MailError . INTERNAL _ERROR , safe . error ) ;
if ( ! safe . child _process . execSync ( 'openssl rsa -in ' + dkimPrivateKeyFile + ' -out ' + dkimPublicKeyFile + ' -pubout -outform PEM' ) ) return new MailError ( MailError . INTERNAL _ERROR , safe . error ) ;
if ( ! safe . fs . writeFileSync ( dkimSelectorFile , config . dkimSelector ( ) , 'utf8' ) ) return new MailError ( MailError . INTERNAL _ERROR , safe . error ) ;
callback ( ) ;
}
2018-01-25 13:48:53 -08:00
// https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
function txtRecordsWithSpf ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
domains . getDNSRecords ( '' , config . fqdn ( ) , 'TXT' , function ( error , txtRecords ) {
if ( error ) return callback ( error ) ;
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
validSpf = txtRecords [ i ] . indexOf ( 'a:' + config . adminFqdn ( ) ) !== - 1 ;
break ; // there can only be one SPF record
}
if ( validSpf ) return callback ( null , null ) ;
if ( ! matches ) { // no spf record was found, create one
txtRecords . push ( '"v=spf1 a:' + config . adminFqdn ( ) + ' ~all"' ) ;
debug ( 'txtRecordsWithSpf: adding txt record' ) ;
} else { // just add ourself
txtRecords [ i ] = matches [ 1 ] + ' a:' + config . adminFqdn ( ) + txtRecords [ i ] . slice ( matches [ 1 ] . length ) ;
debug ( 'txtRecordsWithSpf: inserting txt record' ) ;
}
return callback ( null , txtRecords ) ;
} ) ;
}
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 ;
}
2018-01-25 13:48:53 -08:00
function addDnsRecords ( domain , callback ) {
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
if ( process . env . BOX _ENV === 'test' ) return callback ( ) ;
2018-01-25 14:51:07 -08:00
var dkimKey = readDkimPublicKeySync ( domain ) ;
2018-01-25 13:48:53 -08:00
if ( ! dkimKey ) return callback ( new MailError ( MailError . INTERNAL _ERROR , new Error ( 'Failed to read dkim public key' ) ) ) ;
// t=s limits the domainkey to this domain and not it's subdomains
var dkimRecord = { subdomain : config . dkimSelector ( ) + '._domainkey' , domain : domain , type : 'TXT' , values : [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] } ;
var records = [ ] ;
records . push ( dkimRecord ) ;
debug ( 'addDnsRecords: %j' , records ) ;
async . retry ( { times : 10 , interval : 20000 } , function ( retryCallback ) {
txtRecordsWithSpf ( function ( error , txtRecords ) {
if ( error ) return retryCallback ( error ) ;
if ( txtRecords ) records . push ( { subdomain : '' , domain : domain , type : 'TXT' , values : txtRecords } ) ;
debug ( 'addDnsRecords: will update %j' , records ) ;
async . mapSeries ( records , function ( record , iteratorCallback ) {
domains . upsertDNSRecords ( record . subdomain , record . domain , record . type , record . values , iteratorCallback ) ;
} , function ( error , changeIds ) {
if ( error ) debug ( 'addDnsRecords: failed to update : %s. will retry' , error ) ;
else debug ( 'addDnsRecords: records %j added with changeIds %j' , records , changeIds ) ;
retryCallback ( error ) ;
} ) ;
} ) ;
} , function ( error ) {
if ( error ) debug ( 'addDnsRecords: done updating records with error:' , error ) ;
else debug ( 'addDnsRecords: done' ) ;
callback ( error ) ;
} ) ;
}
2018-01-24 21:15:58 -08:00
function add ( domain , callback ) {
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2018-01-24 21:30:06 -08:00
ensureDkimKey ( domain , function ( error ) {
2018-01-24 21:15:58 -08:00
if ( error ) return callback ( new MailError ( MailError . INTERNAL _ERROR , error ) ) ;
2018-01-24 21:30:06 -08:00
maildb . add ( domain , function ( error ) {
2018-01-25 10:46:15 -08:00
if ( error && error . reason === DatabaseError . ALREADY _EXISTS ) return callback ( new MailError ( MailError . ALREADY _EXISTS , 'Domain already exists' ) ) ;
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new MailError ( MailError . NOT _FOUND , 'No such domain' ) ) ;
2018-01-24 21:30:06 -08:00
if ( error ) return callback ( new MailError ( MailError . INTERNAL _ERROR , error ) ) ;
2018-01-25 13:48:53 -08:00
addDnsRecords ( domain , NOOP _CALLBACK ) ; // add the required dns records asynchronously
2018-01-24 21:30:06 -08:00
callback ( ) ;
} ) ;
2018-01-24 21:15:58 -08:00
} ) ;
}
function del ( domain , callback ) {
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
maildb . del ( domain , function ( error ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new MailError ( MailError . NOT _FOUND , error . message ) ) ;
if ( error ) return callback ( new MailError ( MailError . INTERNAL _ERROR , error ) ) ;
callback ( ) ;
} ) ;
}
2018-01-20 23:17:39 -08:00
function setMailFromValidation ( domain , enabled , callback ) {
assert . strictEqual ( typeof domain , 'string' ) ;
2018-01-20 18:56:17 -08:00
assert . strictEqual ( typeof enabled , 'boolean' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2018-01-21 00:06:08 -08:00
maildb . update ( domain , { mailFromValidation : enabled } , function ( error ) {
2018-01-20 23:17:39 -08:00
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new MailError ( MailError . NOT _FOUND ) ) ;
2018-01-20 18:56:17 -08:00
if ( error ) return callback ( new MailError ( MailError . INTERNAL _ERROR , error ) ) ;
createMailConfig ( NOOP _CALLBACK ) ;
callback ( null ) ;
} ) ;
}
2018-01-20 23:17:39 -08:00
function setCatchAllAddress ( domain , address , callback ) {
assert . strictEqual ( typeof domain , 'string' ) ;
2018-01-20 18:56:17 -08:00
assert ( Array . isArray ( address ) ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2018-01-20 23:17:39 -08:00
maildb . update ( domain , { catchAll : address } , function ( error ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new MailError ( MailError . NOT _FOUND ) ) ;
2018-01-20 18:56:17 -08:00
if ( error ) return callback ( new MailError ( MailError . INTERNAL _ERROR , error ) ) ;
createMailConfig ( NOOP _CALLBACK ) ;
callback ( null ) ;
} ) ;
}
2018-01-20 23:17:39 -08:00
function setMailRelay ( domain , relay , callback ) {
assert . strictEqual ( typeof domain , 'string' ) ;
2018-01-20 18:56:17 -08:00
assert . strictEqual ( typeof relay , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
verifyRelay ( relay , function ( error ) {
if ( error ) return callback ( error ) ;
2018-01-20 23:17:39 -08:00
maildb . update ( domain , { relay : relay } , function ( error ) {
2018-01-20 18:56:17 -08:00
if ( error ) return callback ( new MailError ( MailError . INTERNAL _ERROR , error ) ) ;
2018-01-20 20:34:30 -08:00
restartMail ( NOOP _CALLBACK ) ;
2018-01-20 18:56:17 -08:00
callback ( null ) ;
} ) ;
} ) ;
}
2018-01-20 23:17:39 -08:00
function setMailEnabled ( domain , enabled , callback ) {
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof enabled , 'boolean' ) ;
2018-01-20 18:56:17 -08:00
assert . strictEqual ( typeof callback , 'function' ) ;
2018-01-20 23:17:39 -08:00
maildb . update ( domain , { enabled : enabled } , function ( error ) {
2018-01-20 18:56:17 -08:00
if ( error ) return callback ( new MailError ( MailError . INTERNAL _ERROR , error ) ) ;
2018-01-20 20:34:30 -08:00
restartMail ( NOOP _CALLBACK ) ;
2018-01-20 18:56:17 -08:00
2018-01-24 11:33:09 -08:00
if ( ! enabled || process . env . BOX _ENV === 'test' ) return callback ( ) ;
// Add MX and DMARC record. Note that DMARC policy depends on DKIM signing and thus works
// only if we use our internal mail server.
var records = [
{ subdomain : '_dmarc' , type : 'TXT' , values : [ '"v=DMARC1; p=reject; pct=100"' ] } ,
{ subdomain : '' , type : 'MX' , values : [ '10 ' + config . mailFqdn ( ) + '.' ] }
] ;
async . mapSeries ( records , function ( record , iteratorCallback ) {
domains . upsertDNSRecords ( record . subdomain , domain , record . type , record . values , iteratorCallback ) ;
} , NOOP _CALLBACK ) ;
2018-01-20 18:56:17 -08:00
callback ( null ) ;
} ) ;
}
2018-01-23 16:10:23 -08:00
function sendTestMail ( domain , to , callback ) {
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof to , 'object' ) ;
2018-01-24 13:11:35 +01:00
assert . strictEqual ( typeof callback , 'function' ) ;
2018-01-23 16:10:23 -08:00
get ( domain , function ( error , result ) {
if ( error ) return callback ( error ) ;
mailer . sendTestMail ( result . domain , to ) ;
callback ( ) ;
} ) ;
2018-01-24 13:11:35 +01:00
}
function getMailboxes ( domain , callback ) {
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
mailboxdb . listMailboxes ( domain , function ( error , result ) {
if ( error ) return callback ( new MailError ( MailError . INTERNAL _ERROR , error ) ) ;
callback ( null , result ) ;
} ) ;
}
function getUserMailbox ( domain , userId , callback ) {
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof userId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
user . get ( userId , function ( error , result ) {
if ( error && error . reason === UserError . NOT _FOUND ) return callback ( new MailError ( MailError . NOT _FOUND , 'no such user' ) ) ;
if ( error ) return callback ( new MailError ( MailError . INTERNAL _ERROR , error ) ) ;
mailboxdb . getMailbox ( result . username , domain , function ( error , result ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new MailError ( MailError . NOT _FOUND , 'no such mailbox' ) ) ;
if ( error ) return callback ( new MailError ( MailError . INTERNAL _ERROR , error ) ) ;
callback ( null , result ) ;
} ) ;
} ) ;
}
function enableUserMailbox ( domain , userId , callback ) {
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof userId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
user . get ( userId , function ( error , result ) {
if ( error && error . reason === UserError . NOT _FOUND ) return callback ( new MailError ( MailError . NOT _FOUND , 'no such user' ) ) ;
if ( error ) return callback ( new MailError ( MailError . INTERNAL _ERROR ) ) ;
mailboxdb . add ( result . username , domain , userId , mailboxdb . TYPE _USER , function ( error ) {
if ( error && error . reason === DatabaseError . ALREADY _EXISTS ) return callback ( new MailError ( MailError . ALREADY _EXISTS , 'mailbox already exists' ) ) ;
if ( error ) return callback ( new MailError ( MailError . INTERNAL _ERROR , error ) ) ;
callback ( null ) ;
} ) ;
} ) ;
}
function disableUserMailbox ( domain , userId , callback ) {
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof userId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
user . get ( userId , function ( error , result ) {
if ( error && error . reason === UserError . NOT _FOUND ) return callback ( new MailError ( MailError . NOT _FOUND , 'no such user' ) ) ;
if ( error ) return callback ( new MailError ( MailError . INTERNAL _ERROR , error ) ) ;
mailboxdb . del ( result . username , domain , function ( error ) {
2018-01-24 15:40:32 +01:00
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new MailError ( MailError . NOT _FOUND , 'no such mailbox' ) ) ;
2018-01-24 13:11:35 +01:00
if ( error ) return callback ( new MailError ( MailError . INTERNAL _ERROR , error ) ) ;
callback ( null ) ;
} ) ;
} ) ;
}
2018-01-25 18:03:02 +01:00
function getAliases ( domain , userId , callback ) {
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof userId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
user . get ( userId , function ( error , result ) {
if ( error && error . reason === UserError . NOT _FOUND ) return callback ( new MailError ( MailError . NOT _FOUND , 'no such user' ) ) ;
if ( error ) return callback ( new MailError ( MailError . INTERNAL _ERROR , error ) ) ;
if ( ! result . username ) return callback ( null , [ ] ) ;
mailboxdb . getAliasesForName ( result . username , domain , function ( error , aliases ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new MailError ( MailError . NOT _FOUND , 'no such mailbox' ) ) ;
if ( error ) return callback ( new MailError ( MailError . INTERNAL _ERROR , error ) ) ;
callback ( null , aliases ) ;
} ) ;
} ) ;
}
function setAliases ( domain , userId , aliases , callback ) {
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof userId , 'string' ) ;
assert ( Array . isArray ( aliases ) ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
for ( var i = 0 ; i < aliases . length ; i ++ ) {
aliases [ i ] = aliases [ i ] . toLowerCase ( ) ;
var error = validateAlias ( aliases [ i ] ) ;
if ( error ) return callback ( error ) ;
}
user . get ( userId , function ( error , result ) {
if ( error && error . reason === UserError . NOT _FOUND ) return callback ( new MailError ( MailError . NOT _FOUND , 'no such user' ) ) ;
if ( error ) return callback ( new MailError ( MailError . INTERNAL _ERROR , error ) ) ;
mailboxdb . setAliasesForName ( result . username , domain , aliases , function ( error ) {
if ( error && error . reason === DatabaseError . ALREADY _EXISTS ) return callback ( new MailError ( MailError . ALREADY _EXISTS , error . message ) ) ;
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new MailError ( MailError . NOT _FOUND , 'no such mailbox' ) ) ;
if ( error ) return callback ( new MailError ( MailError . INTERNAL _ERROR , error ) ) ;
callback ( null ) ;
} ) ;
} ) ;
}